diff --git a/src/workspaces/officerdev/src/apps/Gitea/DashboardViews.tsx b/src/workspaces/officerdev/src/apps/Gitea/DashboardViews.tsx index 43a73550..073c1c9b 100644 --- a/src/workspaces/officerdev/src/apps/Gitea/DashboardViews.tsx +++ b/src/workspaces/officerdev/src/apps/Gitea/DashboardViews.tsx @@ -129,7 +129,7 @@ export const NotificationsView = () => { const to = subjectRoute(notification); const inner = ( <> - +

{notification.subject.title}

@@ -236,7 +236,7 @@ export const OrganizationsView = () => {

{org.full_name || org.username}

{org.description &&

{org.description}

} -

+

@{org.username} {org.visibility && ` · ${org.visibility}`} {org.location && ` · ${org.location}`} diff --git a/src/workspaces/officerdev/src/apps/Gitea/GiteaBits.tsx b/src/workspaces/officerdev/src/apps/Gitea/GiteaBits.tsx index 1b23dcbe..b8d6d94a 100644 --- a/src/workspaces/officerdev/src/apps/Gitea/GiteaBits.tsx +++ b/src/workspaces/officerdev/src/apps/Gitea/GiteaBits.tsx @@ -1,28 +1,47 @@ +import type { Tone } from '@/components/Data'; import type { ReactNode } from 'react'; import type { GiteaIssue, GiteaLabel, GiteaPullRequest, GiteaUser } from './shared'; -import { - CircleDot, - CircleCheck, - GitMerge, - GitPullRequest, - GitPullRequestClosed, - Loader2, - TriangleAlert, -} from 'lucide-react'; +import { CircleCheck, CircleDot, GitMerge, GitPullRequest, GitPullRequestClosed } from 'lucide-react'; +import { EmptyBlock, ErrorBlock, LoadingBlock, StatusIcon, StatusPill } from '@/components/Data'; import { serviceErrorMessage } from '../../hooks/useServiceConnection'; import { labelTextColor } from './shared'; -// The small pieces every Gitea view repeats: an avatar, a label chip, a state pill, and the three -// non-content states (loading / failed / empty). Kept together so the repo, issue and PR screens look -// like one app rather than three, and so a change to "what an error looks like" is one edit. +// The small pieces every Gitea view repeats. These are now thin adapters over `components/Data` — +// they translate Gitea's vocabulary (open / closed / merged) into the design language's five tones +// and otherwise get out of the way. See docs/design-language-interface.md. +// +// The export names are unchanged so the ten call sites did not all have to move at once. + +/** + * Gitea's state vocabulary, mapped once. + * + * The non-obvious choice is CLOSED ISSUE → neutral rather than a colour of its own. Most issues in + * any list are closed; giving them a tone paints most of the screen and leaves the open ones no + * quieter than the rest. Neutral is what makes "open" findable — which is the only thing anyone is + * scanning an issue list for. A closed PULL request is different: it was rejected, and that is a + * real outcome worth marking, so it keeps `danger`. + */ +function stateOf(item: GiteaIssue | GiteaPullRequest): { tone: Tone; icon: typeof CircleDot; text: string } { + const isPull = 'base' in item || !!(item as GiteaIssue).pull_request; + const merged = (item as GiteaPullRequest).merged || (item as GiteaIssue).pull_request?.merged; + + if (merged) return { tone: 'info', icon: GitMerge, text: 'Merged' }; + if (item.state === 'open') { + return { tone: 'success', icon: isPull ? GitPullRequest : CircleDot, text: 'Open' }; + } + return isPull + ? { tone: 'danger', icon: GitPullRequestClosed, text: 'Closed' } + : { tone: 'neutral', icon: CircleCheck, text: 'Closed' }; +} export const Avatar = ({ user, size = 20 }: { user?: GiteaUser; size?: number }) => { const initial = (user?.full_name || user?.login || '?').charAt(0).toUpperCase(); if (!user?.avatar_url) { return ( {initial} @@ -32,6 +51,7 @@ export const Avatar = ({ user, size = 20 }: { user?: GiteaUser; size?: number }) {user.login} ( @@ -49,79 +73,43 @@ export const LabelChip = ({ label }: { label: GiteaLabel }) => ( ); -/** - * The open/closed/merged pill. Merged is a genuinely distinct state from closed and Gitea colours it - * differently — a merged PR is not a rejected one — so it is worth the extra branch here. - */ export const StateBadge = ({ item }: { item: GiteaIssue | GiteaPullRequest }) => { - const isPull = 'base' in item || !!(item as GiteaIssue).pull_request; - const merged = (item as GiteaPullRequest).merged || (item as GiteaIssue).pull_request?.merged; - const open = item.state === 'open'; - - const [Icon, text, classes] = merged - ? [GitMerge, 'Merged', 'bg-purple-500/15 text-purple-500'] - : open - ? isPull - ? [GitPullRequest, 'Open', 'bg-emerald-500/15 text-emerald-500'] - : [CircleDot, 'Open', 'bg-emerald-500/15 text-emerald-500'] - : isPull - ? [GitPullRequestClosed, 'Closed', 'bg-red-500/15 text-red-500'] - : [CircleCheck, 'Closed', 'bg-purple-500/15 text-purple-500']; - + const { tone, icon, text } = stateOf(item); return ( - - + {text} - + ); }; -/** The open/closed marker in a dense list, where the full pill is too much furniture. */ +/** The state marker for a dense list, where the full pill is too much furniture. */ export const StateIcon = ({ item }: { item: GiteaIssue | GiteaPullRequest }) => { - const isPull = 'base' in item || !!(item as GiteaIssue).pull_request; - const merged = (item as GiteaPullRequest).merged || (item as GiteaIssue).pull_request?.merged; - if (merged) return ; - if (item.state === 'open') { - const Icon = isPull ? GitPullRequest : CircleDot; - return ; - } - const Icon = isPull ? GitPullRequestClosed : CircleCheck; - return ; + const { tone, icon, text } = stateOf(item); + return ; }; -export const Loading = ({ label = 'Loading…' }: { label?: string }) => ( -

- {label} -
-); +export const Loading = ({ label = 'Loading…' }: { label?: string }) => ; export const ErrorState = ({ title, error }: { title: string; error: unknown }) => ( -
- -

{title}

-

{serviceErrorMessage(error)}

-
+ ); export const EmptyState = ({ title, hint }: { title: string; hint?: ReactNode }) => ( -
-

{title}

- {hint &&

{hint}

} -
+ ); /** A segmented open/closed filter, the control Gitea puts above every issue and PR list. */ type StateFilterProps = { value: 'open' | 'closed' | 'all'; onChange: (value: 'open' | 'closed' | 'all') => void }; export const StateFilter = ({ value, onChange }: StateFilterProps) => ( -
+
{(['open', 'closed', 'all'] as const).map((state) => ( {open && (
-
- Branches -
+
Branches
{(branches ?? []).map((branch) => ( { {branch.name} {branch.name === repo.default_branch && ( - default + default )} ))} {!!tags?.length && ( <> -
+
Tags
{tags.map((tag) => ( @@ -217,7 +215,7 @@ const EntryRow = ({ entry, owner, name, refName }: EntryRowProps) => { to={giteaRepoPath(owner, name, { tab: 'code', path: entry.path, ref: refName })} className="flex items-center gap-3 px-4 py-2 text-sm transition-colors hover:bg-muted/50" > - + {entry.name} {entry.last_commit_message && ( diff --git a/src/workspaces/officerdev/src/apps/Gitea/RepoHistoryViews.tsx b/src/workspaces/officerdev/src/apps/Gitea/RepoHistoryViews.tsx index f55c0a3c..795a9375 100644 --- a/src/workspaces/officerdev/src/apps/Gitea/RepoHistoryViews.tsx +++ b/src/workspaces/officerdev/src/apps/Gitea/RepoHistoryViews.tsx @@ -44,7 +44,7 @@ export const RepoCommitsView = ({ owner, name, refName }: TabProps) => { href={commit.html_url} target="_blank" rel="noreferrer" - className="shrink-0 rounded border px-1.5 py-0.5 font-mono text-[11px] text-muted-foreground transition-colors hover:bg-muted hover:text-foreground" + className="shrink-0 rounded border px-1.5 py-0.5 font-mono text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground" title="Open commit in Gitea" > {shortSha(commit.sha)} @@ -102,11 +102,11 @@ export const RepoBranchesView = ({ repo, owner, name }: TabProps) => {
{branch.name} {branch.name === repo.default_branch && ( - + default )} - {branch.protected && } + {branch.protected && }
{branch.commit?.message && (

{commitTitle(branch.commit.message)}

@@ -129,9 +129,7 @@ export const RepoBranchesView = ({ repo, owner, name }: TabProps) => { > {tag.name} - - {shortSha(tag.commit?.sha)} - + {shortSha(tag.commit?.sha)} ))}
@@ -160,14 +158,14 @@ export const RepoReleasesView = ({ repo, owner, name }: TabProps) => {
{release.name || release.tag_name} - {release.tag_name} + {release.tag_name} {release.prerelease && ( - + Pre-release )} {release.draft && ( - + Draft )} diff --git a/src/workspaces/officerdev/src/apps/Gitea/RepoIssuesView.tsx b/src/workspaces/officerdev/src/apps/Gitea/RepoIssuesView.tsx index 0f84ac99..5e57da21 100644 --- a/src/workspaces/officerdev/src/apps/Gitea/RepoIssuesView.tsx +++ b/src/workspaces/officerdev/src/apps/Gitea/RepoIssuesView.tsx @@ -2,9 +2,10 @@ import type { GiteaIssue, GiteaRepo } from './shared'; import { useState } from 'react'; import { Link } from 'react-router'; import { ArrowLeft, ExternalLink, MessageSquare } from 'lucide-react'; +import { DataRow, RelativeTime } from '@/components/Data'; import { Avatar, EmptyState, ErrorState, LabelChip, Loading, StateBadge, StateFilter, StateIcon } from './GiteaBits'; import { GiteaMarkdown } from './GiteaMarkdown'; -import { giteaRepoPath, timeAgo } from './shared'; +import { giteaRepoPath } from './shared'; import { useGiteaIssue, useGiteaIssueComments, useGiteaRepoIssues } from './useGiteaData'; // Issues for one repository — the list, and one issue with its comment timeline. @@ -59,7 +60,14 @@ const IssueList = ({ owner, name, type }: ListProps) => { ) : error ? ( ) : !issues?.length ? ( - +