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. */}
-
+ }
+ >
+ {/* 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()}`}
+
+
);
})}