From 9ecd9fd7ad8d4483c86249fd33d90fcda8a0ed3e Mon Sep 17 00:00:00 2001 From: Chad Miller Date: Tue, 25 Aug 2026 14:59:44 -0700 Subject: [PATCH] refactor(git-ui): one row draws both lists The two lists each held their own frame, and the sizes had drifted apart: a 16px mark against a 14px one, a 15px title against a 14.5px one, and a meta line that hung off an indent on one page and off a flex gutter on the other. ListRow owns the frame both need. The mark at the head, the title, the marks on its right, and the faint line of facts under it are one set of sizes now, so a size moves in one place. Dot is the separator between two facts, and ListCount is a number with the icon that says what was counted, which draws nothing for a count of none. An issue's reply count moves to the right of its title, where a pull request's review count already is. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RomHvaB4nyAckbRD7W36Ue --- .../src/components/molecules/list-row.jsx | 72 ++++++++ .../src/components/molecules/pull-card.jsx | 159 ++++++++---------- packages/git-ui/src/pages/issues.jsx | 54 +++--- 3 files changed, 168 insertions(+), 117 deletions(-) create mode 100644 packages/git-ui/src/components/molecules/list-row.jsx diff --git a/packages/git-ui/src/components/molecules/list-row.jsx b/packages/git-ui/src/components/molecules/list-row.jsx new file mode 100644 index 0000000..cc36cfe --- /dev/null +++ b/packages/git-ui/src/components/molecules/list-row.jsx @@ -0,0 +1,72 @@ +import { Link } from '#/components/atoms/link.jsx'; +import { cn } from '#/lib/utils.js'; + +/** + * One entry in a list of things that carry a state: a pull request, an issue. + * + * Both lists ask a reader the same three questions in the same order, so both + * are drawn here rather than each keeping its own sizes. The mark at the head + * is the state, because that is the question before the words are. The title + * is what a reader scans a column for. The line under it is who proposes + * what, read once a title has caught the eye. + * + * `marks` holds the right of the title line, which is what has been said + * about the entry. `footer` is what one entry has and the others do not, and + * needs the width the title has rather than the width left beside it. + */ +export function ListRow({ + Icon, + iconClassName, + title, + href, + marks, + footer, + children, +}) { + return ( +
+ +
+
+ + {title} + + {marks && ( +
+ {marks} +
+ )} +
+ +
+ {children} +
+ + {footer} +
+
+ ); +} + +/** What separates two facts on the line under a title. */ +export function Dot() { + return ; +} + +/** + * One count on the right of a title, with the icon that says what was + * counted. Nothing is drawn for a count of none. + * @param {{Icon: Function, count: number, className?: string, title?: string}} props + */ +export function ListCount({ Icon, count, className, title }) { + if (!count) return null; + return ( + + + {count} + + ); +} diff --git a/packages/git-ui/src/components/molecules/pull-card.jsx b/packages/git-ui/src/components/molecules/pull-card.jsx index 33df1d4..2204dd8 100644 --- a/packages/git-ui/src/components/molecules/pull-card.jsx +++ b/packages/git-ui/src/components/molecules/pull-card.jsx @@ -8,8 +8,8 @@ import { XIcon, } from 'lucide-react'; import { Badge } from '#/components/atoms/badge.jsx'; -import { Link } from '#/components/atoms/link.jsx'; import { Handle } from '#/components/molecules/handle.jsx'; +import { Dot, ListCount, ListRow } from '#/components/molecules/list-row.jsx'; import { PathList } from '#/components/molecules/path-list.jsx'; import { checkLook } from '#/lib/checks.js'; import { diffHref } from '#/lib/collab.js'; @@ -29,11 +29,6 @@ const MARKS = { * merged read the same, because they are the same thing at two moments, and * the mark at the head says which. * - * Two lines answering different questions. The first is what a reader scans - * for: what it is called, and what has been said about it. The second is who - * proposes what, read once a title has caught the eye. Putting both on one - * line made twelve facts of equal weight and no way in. - * * Every number is read from the object graph rather than from a stored state, * so there is nothing to fall out of date. Whether a check is stale is the * exception: a force-push leaves a sha nothing points at, and what remembers @@ -59,93 +54,83 @@ export function PullCard({ repo, pull, checks, reviews }) { ]; return ( -
- -
-
- - {pull.title} - - {/* What has been said about it, to the right of the title, so a - column of rows reads down rather than across. */} -
- {look && ( - - - {staleCheck && stale} - - )} - {approvals > 0 && ( - - - {approvals} - - )} - {changesRequested > 0 && ( - - - {changesRequested} - - )} - {talk > 0 && ( - - - {talk} - - )} -
-
- -
- - - {pull.branch} - - - {pull.ahead} {pull.ahead === 1 ? 'commit' : 'commits'},{' '} - {pull.files.length} {pull.files.length === 1 ? 'file' : 'files'} - - - {timeAgo(pull.when)} - {/* What the line cannot say, because each is true of this one rather - than of every row. */} - {pull.behind > 0 && ( - - {pull.behind} behind - + + {look && ( + + + {staleCheck && stale} + )} - {pull.fork && ( - - fork - - )} - {pull.status && ( - - {pull.status} - - )} - {!pull.rooted && no shared history} -
- - {pull.drift?.length > 0 && ( + + + + + } + footer={ + pull.drift?.length > 0 && (
- )} -
-
+ ) + } + > + + + {pull.branch} + + + {pull.ahead} {pull.ahead === 1 ? 'commit' : 'commits'},{' '} + {pull.files.length} {pull.files.length === 1 ? 'file' : 'files'} + + + {timeAgo(pull.when)} + {/* What the line cannot say, because each is true of this one rather + than of every row. */} + {pull.behind > 0 && ( + + {pull.behind} behind + + )} + {pull.fork && ( + + fork + + )} + {pull.status && ( + + {pull.status} + + )} + {!pull.rooted && no shared history} + ); } diff --git a/packages/git-ui/src/pages/issues.jsx b/packages/git-ui/src/pages/issues.jsx index 401722f..af36853 100644 --- a/packages/git-ui/src/pages/issues.jsx +++ b/packages/git-ui/src/pages/issues.jsx @@ -1,10 +1,12 @@ import { useQuery } from '@tanstack/react-query'; +import { MessageSquareIcon } from 'lucide-react'; import { buttonVariants } from '#/components/atoms/button.jsx'; import { Link } from '#/components/atoms/link.jsx'; import { Skeleton } from '#/components/atoms/skeleton.jsx'; import { EmptyState } from '#/components/molecules/empty-state.jsx'; import { Handle } from '#/components/molecules/handle.jsx'; import { issueLook } from '#/components/molecules/issue-state.jsx'; +import { Dot, ListCount, ListRow } from '#/components/molecules/list-row.jsx'; import { timeAgo } from '#/lib/format.js'; import { account, repoRecord } from '#/lib/git.js'; import { issueThreads, loadIssueStatements } from '#/lib/issues.js'; @@ -70,38 +72,30 @@ export function IssuesPage({ repo }) { const look = issueLook(state.state); const rkey = root.uri?.split('/').pop(); return ( -
-
- - - {root.title || '(untitled)'} - -
- {/* A sentence rather than a row of states: the icon already - says the state, so the words just tell what happened. */} -
- - - opened {timeAgo(root.createdAt)} - {state.state !== 'open' && - `, ${look.label.toLowerCase()}`} - - {thread.length > 1 && ( - - {thread.length - 1}{' '} - {thread.length === 2 ? 'reply' : 'replies'} - - )} -
-
+ } + > + {/* A sentence rather than a row of states: the mark already + says the state, so the words just tell what happened. */} + + + + opened {timeAgo(root.createdAt)} + {state.state !== 'open' && `, ${look.label.toLowerCase()}`} + + ); })} -- 2.51.2