Public: anyone can read this; the crew can change it.
The thread's comments are rows, not boxes #9701
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/9667-the-thread-s-comments-are-rows-not-boxes → main 5 commits fork 3 threads · 3 unresolved
4 files
changed +35
−6
docs/design/areas/comments.md +4 −2
| @@ -2,7 +2,9 @@ | ||
| 2 | 2 | |
| 3 | 3 | A comment on an issue or pull, and the form that writes one. Both are a `card()`; these classes give it its comment look. |
| 4 | 4 | |
| 5 | - `.comment` — one comment as a card: its `.card-head` (byline and time) on the muted ground, the body under it. Written by `commentCard()` in `src/web/comments.ts` and by `reviewRows()` in `src/web/diff-view.ts` (an inline review comment); styled in `src/web/style/components.ts`, and spaced inside a review thread by `.dc-review .comment` in `src/web/style/content.ts`. | |
| 6 | - `.comment-body` — a comment's rendered markdown, always beside `.prose`, padded inside its card. Written by `commentCard()` in `src/web/comments.ts`, `reviewRows()` in `src/web/diff-view.ts` and `reviewEntry()` in `src/web/studio/change.ts`; styled in `src/web/style/components.ts` (tighter in a timeline event's card). | |
| 5 | - `.comment-row` — a thread comment as a row of the timeline, no box: `.comment-head` (byline and time) then the body as `.prose`, spaced by padding under its `.tl-item`, no hairline of its own (the timeline line is the only rule). Written by `commentCard()` in `src/web/comments.ts` (issue page, pull page, browse); styled in `src/web/style/components.ts`. | |
| 6 | - `.comment-head` — the one line of byline, verb and time at the top of a `.comment-row`. | |
| 7 | - `.comment` — an inline review comment as a card: its `.card-head` (byline and time) on the muted ground, the body under it. Written only by `reviewRows()` in `src/web/diff-view.ts`; styled in `src/web/style/components.ts`, and spaced inside a review thread by `.dc-review .comment` in `src/web/style/content.ts`. | |
| 8 | - `.comment-body` — a comment's rendered markdown, always beside `.prose`: padded inside a `.comment` card, unpadded in a `.comment-row` (written by `commentCard()` in `src/web/comments.ts`); `reviewRows()` in `src/web/diff-view.ts` and `reviewEntry()` in `src/web/studio/change.ts`; styled in `src/web/style/components.ts` (tighter in a timeline event's card). | |
| 7 | 9 | - `.comment-form` — the card that writes a comment: a textarea and its `.form-actions`. Written by `composer()` and `editForm()` in `src/web/pages/thread.ts`, `reviewRows()` in `src/web/diff-view.ts` (a reply in a review thread) and `studioApp()` in `src/web/studio/app.ts` (the "Tell the crew" box); styled in `src/web/style/components.ts`, with its narrow-screen action line and its review-thread margin in `src/web/style/content.ts`. |
| 8 | 10 | - `.tl-earlier` — the line above a long thread's newest items, `Show N earlier items`, a plain link to `?all=1`. Written by `conversation()` in `src/web/pages/thread.ts`; styled in `src/web/style/components.ts` beside `.tl-item`, indented to the timeline's item column. |
src/web/comments.ts +3 −3
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | /** |
| 2 | * One comment card, shared by the full page render and the live-append | |
| 2 | * One comment row (a row of the timeline, no box), shared by the full page render and the live-append | |
| 3 | 3 | * fragment so a comment that arrives over SSE looks identical to one that was |
| 4 | 4 | * on the page at load. The `data-comment-id` is what the client tracks to know |
| 5 | 5 | * what it has already shown — it is load-bearing, not decoration. |
| @@ -38,8 +38,8 @@ export function commentCard(card: CommentCard): Html { | ||
| 38 | 38 | card.originAuthor != null && card.originAuthor !== "" |
| 39 | 39 | ? originTag(card.originAuthor, card.originKind ?? "globe") |
| 40 | 40 | : userLink(card.author, { mark: card.mark }); |
| 41 | return html`<div ${idAttr}class="card comment"> | |
| 42 | <div class="card-head"> | |
| 41 | return html`<div ${idAttr}class="comment-row"> | |
| 42 | <div class="comment-head"> | |
| 43 | 43 | ${byline} |
| 44 | 44 | <span class="muted">${card.verb} ${time(card.at)}</span> |
| 45 | 45 | ${when( |
src/web/style/components.ts +4 −1
| @@ -877,13 +877,16 @@ details[open] > summary.row > .row-lead { transform: rotate(90deg); } | ||
| 877 | 877 | min-height: 32px; |
| 878 | 878 | } |
| 879 | 879 | .tl-earlier { margin: 0; padding-left: 36px; color: var(--muted-fg); font-size: 13px; } |
| 880 | .tl-item[data-kind="comment"] .card { margin: 0; } | |
| 880 | .tl-item[data-kind="comment"] { padding-bottom: 16px; } | |
outdated
Clean: commentCard and threadComment no longer emit a .card inside a comment tl-item, so |
||
| 881 | 881 | .tl-item[data-kind="event"] > .card { flex-basis: 100%; margin: 4px 0 0; color: var(--fg); font-size: 14px; } |
outdated
Principles 3 and 6: a hairline under every comment plus the timeline line is a lot of rules. Prefer rhythm (padding between rows) and keep the hairline only where the thread ends, or show in the pull a look at 1440/390 that justifies it. |
||
| 882 | 882 | .tl-item[data-kind="event"] > .card .comment-body { padding: 8px 12px; } |
| 883 | 883 | |
| 884 | 884 | /* ---- comment card ---- */ |
| 885 | 885 | .comment .card-head { background: var(--muted); font-size: 13px; } |
| 886 | 886 | .comment .comment-body { padding: 2px 16px 8px; } |
| 887 | .comment-row { min-width: 0; } | |
| 888 | .comment-head { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 13px; min-height: 28px; } | |
| 889 | .comment-row .comment-body { padding: 4px 0 0; } | |
| 887 | 890 | .comment-form { margin-top: 16px; padding: 12px; overflow: visible; } |
| 888 | 891 | .comment-form textarea { margin-bottom: 10px; } |
| 889 | 892 |
test/comment-row.test.ts added +24
| @@ -0,0 +1,24 @@ | ||
| 1 | import { expect, test } from "bun:test"; | |
| 2 | import { commentCard } from "../src/web/comments.ts"; | |
| 3 | import { threadComment } from "../src/web/pages/thread.ts"; | |
| 4 | ||
| 5 | const card = { | |
| 6 | id: "c1", | |
| 7 | author: "ada", | |
| 8 | at: "2026-01-01T00:00:00Z", | |
| 9 | body: "Hello **there**", | |
| 10 | verb: "commented", | |
| 11 | }; | |
| 12 | ||
| 13 | test("a comment is a row, not a .card", () => { | |
| 14 | for (const out of [ | |
| 15 | String(commentCard(card)), | |
| 16 | String(threadComment("c1", "ada", card.at, card.body)), | |
| 17 | ]) { | |
| 18 | expect(out).not.toMatch(/class="[^"]*\bcard\b/); | |
| 19 | expect(out).not.toContain("card-head"); | |
| 20 | expect(out).toContain('class="comment-row"'); | |
| 21 | expect(out).toContain('class="prose comment-body"'); | |
| 22 | expect(out).toContain("<strong>there</strong>"); | |
| 23 | } | |
| 24 | }); |
new above
offline
Stale:
.commentis no longer written bycommentCard(); it is now only written byreviewRows(). Drop it from this line. The.comment-bodyentry below also sayscommentCard()writes it padded inside its card; say it is the row's body too (unpadded) and fix that.