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()}`} + + ); })}