From 9877a1d8e0d90130eb3d5da149eb1a9e9125b06a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Thu, 6 Aug 2026 21:20:15 +0000 Subject: [PATCH] adopt the design language across gitea's lists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gitea goes first because it is the first Officer surface anyone outside this machine will see. GiteaBits is now a thin adapter over components/Data rather than its own set of styles: it translates Gitea's vocabulary into the five tones and gets out of the way. Export names are unchanged so the ten call sites did not all have to move at once. The one judgement call worth flagging is CLOSED ISSUE -> neutral instead of a colour of its own. Most issues in any list are closed, so 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 scans an issue list for. A closed PULL request keeps danger — that one was rejected, and rejection is a real outcome. Merged is info, because a merged PR and an open one were previously the same green. Repositories and issues now build on DataRow. Repo rows put the owner prefix in muted and only the name in medium, so one weight per row survives; issue rows truncate rather than wrap so every row keeps one height and the list can be scanned down its left edge. Timestamps are RelativeTime, so "2 months ago" now carries the exact date as a hover title instead of losing it. Swept the whole app for drift: emerald/amber/sky/red palette numbers to success/warning/info/destructive, and every text-[10px] and text-[11px] to text-xs. ring-black/5 and text-black/50 are gone — both were invisible in dark mode. Nothing below 12px remains anywhere in Gitea. Empty states say why they are empty now. "No open issues" on a repo with fifty closed ones was technically true and useless. Typechecks clean. Still not rendered in a browser — needs pm2 restart officer and a hard refresh. text-xs is still 65 uses against 21 text-sm; the remaining pass is per-case judgement about which of those are meta and which are content that should be readable, and it is not a sweep. Co-Authored-By: Claude Opus 5 --- .../src/apps/Gitea/DashboardViews.tsx | 4 +- .../officerdev/src/apps/Gitea/GiteaBits.tsx | 112 ++++++++---------- .../src/apps/Gitea/GiteaConnection.tsx | 12 +- .../officerdev/src/apps/Gitea/GiteaNav.tsx | 4 +- .../src/apps/Gitea/GiteaViewHeader.tsx | 2 +- .../src/apps/Gitea/RepoCodeView.tsx | 10 +- .../src/apps/Gitea/RepoHistoryViews.tsx | 16 ++- .../src/apps/Gitea/RepoIssuesView.tsx | 64 ++++++---- .../src/apps/Gitea/RepoPullsView.tsx | 33 +++--- .../officerdev/src/apps/Gitea/RepoView.tsx | 8 +- .../src/apps/Gitea/RepositoriesView.tsx | 79 ++++++------ 11 files changed, 174 insertions(+), 170 deletions(-) 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 ? ( - +