Public: anyone can read this; the crew can change it.

A commit's head and body draw §5 components on the commit and report pages #4742

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/4585-a-commit-s-head-and-body-draw-5-componen → main 18 commits fork 5 threads · 5 unresolved

  • Merged. scout/4585-a-commit-s-head-and-body-draw-5-componen was merged into main as d106e29.

Review

Sign in to review this change.
8 files changed +75 −17
docs/design/sections/5-components.md +1 −1
@@ -44,7 +44,7 @@ specification.
4444 | **empty state** `empty()` | `.card.empty`, title `.empty-title` | — | icon, one title, one sentence, **exactly one** action | the same three lines, centered |
4545 | **diff file** | `.diff-file` | `data-status`, `<details open>`, `.diff-viewed` | collapsed, viewed | a foldable file section |
4646 | **code block** | `.code` | `:target` / `[data-hl]` line highlight | — | a numbered gutter |
47 | **prose** | `.prose` | — | 15px, 72ch measure | wrapped markdown |
47 | **prose** | `.prose` | `data-text=message`: a `<pre>` of written text that wraps, for commit messages and reports | 15px, 72ch measure; `data-text=message` is 12.5px | wrapped markdown |
4848 | **settings shell** `settingsLayout()` | `.settings` (`.settings-nav`, `.settings-main`) | — | sticky nav ≥860px, scroll strip below | a sidebar and a pane |
4949 | **repo header** `repoHeader()` | `.repo-head` | — | tab counts, Settings only for admins | a title line and a tab row |
5050 | **where strip** `whereStrip()` (`scope.ts`) | `.where > .where-place` | `data-state=reading\|present\|absent` — accent dot, success dot, faint dot — the dot is the place's `aria-hidden` glyph span, its aside `.muted` | three places, always in this order and these words; a place is a link where one exists, and the third is the pin toggle | `● codeberg.org · ● workbench · ◆ this machine` |
docs/design/sections/5d-page-local-classes.md −2
@@ -28,8 +28,6 @@ the same line (#4888).
2828 - `.preview`
2929 - `.swatch`
3030 - **`style/content.ts`, page furniture** — what frames a page's content: page and repo heads, the front page and its tiles, explore, backups, boards, the fleet chart's series colours, lineage:
31 - `.commit-body`
32 - `.commit-head`
3331 - `.filters`
3432 - `.page-head`
3533 - `.repo-actions`
src/web/pages/commits.ts +5 −5
@@ -31,6 +31,7 @@ import {
3131 empty,
3232 icon,
3333 listCard,
34 messageText,
3435 originTag,
3536 pager,
3637 sha,
@@ -105,12 +106,11 @@ export function renderCommit(
105106 syntax: SyntaxMap,
106107 ): Html {
107108 const head: Html = card(
108 html`<h2>${commit.subject}</h2>
109 ${when(
109 html`${when(
outdated

Bare <div class="prose" data-text="message"><pre> is duplicated here and in reports.ts; one small helper (e.g. in ui.ts) would keep the markup in one place. Also the test only asserts the CSS rule text exists, not that the page shows the body in it; add an assertion that a commit with a body contains the prose div.

110110 commit.body.trim() !== "",
111 () => html`<pre class="commit-body">${commit.body}</pre>`,
111 () => html`<div class="card-body">${messageText(commit.body)}</div>`,
112112 )}
113 <div class="bar small muted">
113 <div class="card-body bar small muted">
114114 ${commit.login
115115 ? originTag(
116116 commit.login,
@@ -132,7 +132,7 @@ export function renderCommit(
132132 >${icon("repo")}Browse files</a
133133 >
134134 </div>`,
135 { extra: "commit-head" },
135 { head: html`<h2>${commit.subject}</h2>`, pad: false },
136136 );
137137 return html`<div class="mt">${head}</div>
138138 <div class="mt">
src/web/pages/reports.ts +2 −2
@@ -40,7 +40,7 @@ import {
4040 import { field, readForm } from "../form.ts";
4141 import { html, when, type Html } from "../html.ts";
4242 import { flashTo, page } from "../layout.ts";
43 import { btn, card, empty, field as formField, time } from "../ui.ts";
43 import { btn, card, empty, field as formField, messageText, time } from "../ui.ts";
4444
4545 /** The repository, when it takes reports: public, and so seen by anyone. */
4646 function publicRepo(ctx: Ctx): Repo | undefined {
@@ -220,7 +220,7 @@ function reportCard(base: string, report: Report): Html {
220220 html`<p><strong>${report.title}</strong></p>
221221 ${when(
222222 report.body !== "",
223 () => html`<pre class="commit-body">${report.body}</pre>`,
223 () => messageText(report.body),
224224 )}
225225 ${when(report.page, (p) => html`<p class="hint">Sent from ${p}</p>`)}
226226 <form
src/web/style/content.ts +7 −7
@@ -156,7 +156,6 @@ a.origin-chip:hover { border-color: var(--border-strong); color: var(--fg); }
156156
157157 .repo-actions { display: flex; align-items: center; gap: 6px; margin-left: auto; }
158158
159
160159 .filters { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
161160 .filters form { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
162161 .filters .search { min-width: 180px; flex: 1; max-width: 320px; }
@@ -169,12 +168,6 @@ a.origin-chip:hover { border-color: var(--border-strong); color: var(--fg); }
169168 .bar .seg { max-width: 100%; overflow-x: auto; scrollbar-width: none; }
170169 @media (max-width: 420px) { .bar .seg > a { padding: 0 8px; } }
171170
172 .commit-head .card-body { padding: 14px 16px; }
173 .commit-body {
174 white-space: pre-wrap; font-family: var(--mono); font-size: 12.5px;
175 color: var(--muted-fg); margin: 8px 0 4px;
176 }
177
178171 .direction-text { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 13px; color: var(--muted-fg); margin: 6px 0 0; }
179172
180173 .chart-box { color: var(--accent); aspect-ratio: 720 / 260; max-width: 720px; }
@@ -321,6 +314,13 @@ form.inline { display: inline; margin-left: auto; }
321314 border-radius: var(--radius); max-width: none;
322315 padding: 12px 14px; overflow-x: auto; margin: 14px 0; scrollbar-width: thin;
323316 }
317 /* Written text kept as typed (a commit message, a report): plain muted mono, wraps, never scrolls sideways. */
318 .prose[data-text="message"] { padding: 0; margin: 8px 0 4px; font-size: 12.5px; line-height: 1.5; }
319 .prose[data-text="message"] pre {
320 background: none; border: 0; border-radius: 0; padding: 0; margin: 0;
321 font-family: var(--mono); color: var(--muted-fg);
322 white-space: pre-wrap; overflow-wrap: anywhere; overflow: visible;
323 }
324324 .prose code { background: var(--muted); border: 1px solid var(--border); padding: 1px 5px; border-radius: 4px; }
325325 .prose pre code { background: none; border: none; padding: 0; }
326326 .prose blockquote {
src/web/ui.ts +5
@@ -885,6 +885,11 @@ export function card(body: Html, o: CardOptions = {}): Html {
885885 </div>`;
886886 }
887887
888 /** Written text kept as typed (a commit message, a report body): §5's prose, wrapped. */
889 export function messageText(text: string): Html {
890 return html`<div class="prose" data-text="message"><pre>${text}</pre></div>`;
891 }
892
888893 /** A card wrapping a `<ul class="list">` — the shape every listing takes. */
889894 export function listCard(rows: Html[], o: CardOptions = {}): Html {
890895 return card(
test/commit-page-timing.test.ts +37
@@ -8,11 +8,14 @@ import { serve, type Servers } from "../src/http/server.ts";
88 import { gitClientEnv } from "./helpers/git-client.ts";
99 import { adminCookie } from "./helpers/admin.ts";
1010
11 const LONG = "x".repeat(300);
12
1113 let dataDir = "";
1214 let work = "";
1315 let app: App;
1416 let servers: Servers;
1517 let oid = "";
18 let bodyOid = "";
1619
1720 const env = gitClientEnv({
1821 GIT_AUTHOR_NAME: "Ada",
@@ -68,6 +71,11 @@ beforeAll(async () => {
6871 await sh(["git", "commit", "-m", "add a"], dir);
6972 await sh(["git", "push", "origin", "main"], dir);
7073 oid = await sh(["git", "rev-parse", "HEAD"], dir);
74 await Bun.write(join(dir, "b.ts"), "export const b = 2;\n");
75 await sh(["git", "add", "."], dir);
76 await sh(["git", "commit", "-m", "add b", "-m", "Why b <exists>:\n because a needs it\n" + LONG], dir);
77 await sh(["git", "push", "origin", "main"], dir);
78 bodyOid = await sh(["git", "rev-parse", "HEAD"], dir);
7179 });
7280
7381 afterAll(async () => {
@@ -95,3 +103,32 @@ test("a commit's head draws §5's bar, not .issue-meta", async () => {
95103 expect(body).toContain("Browse files");
96104 expect(body).toContain(oid.slice(0, 7));
97105 });
106
107 test("a commit's head and body draw §5's card-head and prose, not commit-head/commit-body", async () => {
108 const body = await (await fetch(`${servers.url}/ada/engine/commit/${oid}`)).text();
109 expect(body).not.toContain("commit-head");
110 expect(body).not.toContain("commit-body");
111 expect(body).toContain('<div class="card-head"><h2>add a</h2></div>');
112 });
113
114 test("a commit with a body shows it, escaped, inside the prose message div", async () => {
115 const page = await (await fetch(`${servers.url}/ada/engine/commit/${bodyOid}`)).text();
116 expect(page).toContain(
117 `<div class="prose" data-text="message"><pre>Why b &lt;exists&gt;:\n because a needs it\n${LONG}</pre></div>`,
118 );
119 const bare = await (await fetch(`${servers.url}/ada/engine/commit/${oid}`)).text();
120 expect(bare).not.toContain('data-text="message"');
121 });
122
123 test("the message rule drops the code box and padding, and wraps a long line instead of scrolling", async () => {
124 const page = await (await fetch(`${servers.url}/ada/engine/commit/${oid}`)).text();
125 const href = page.match(/href="(\/assets\/[^"]+\.css)"/)?.[1] ?? "";
126 const css = await (await fetch(`${servers.url}${href}`)).text();
127 const pre = css.match(/\.prose\[data-text="message"\] pre\s*\{[^}]*\}/)?.[0] ?? "";
128 expect(pre).toMatch(/background:\s*none/);
129 expect(pre).toMatch(/border:\s*0/);
130 expect(pre).toMatch(/padding:\s*0/);
131 expect(pre).toMatch(/white-space:\s*pre-wrap/);
132 expect(pre).toMatch(/overflow-wrap:\s*anywhere/);
133 expect(css).toMatch(/\.prose\[data-text="message"\]\s*\{[^}]*padding:\s*0/);
134 });
test/reports.test.ts +18
@@ -408,6 +408,24 @@ describe("reports from outside", () => {
408408 ).toBe(1);
409409 });
410410
411 test("a report's body draws in the prose message div, escaped; a report without one draws none", async () => {
412 await report("alice/game", { title: "tall menu", body: "Menu <b>breaks</b>\n on phones" });
413 await report("alice/game", { title: "silent bug", body: "" });
414 const withBody = (await waiting("tall menu"))!;
415 const without = (await waiting("silent bug"))!;
416 const page = await (await as(aliceToken, "/alice/game/reports")).text();
417 expect(page).not.toContain("commit-body");
418 expect(page).toContain(
419 '<div class="prose" data-text="message"><pre>Menu &lt;b&gt;breaks&lt;/b&gt;\n on phones</pre></div>',
420 );
421 // The card of the bodiless report, from its opening to its accept form.
422 const end = page.indexOf(`id="accept-${without}"`);
423 const card = page.slice(page.lastIndexOf('class="card', end), end);
424 expect(card).toContain("silent bug");
425 expect(card).not.toContain('data-text="message"');
426 expect(page.indexOf(`id="accept-${withBody}"`)).toBeGreaterThan(-1);
427 });
428
411429 test("the reports page offers the title to change, and the form opens the issue marked", async () => {
412430 await report("alice/game", { title: "menu bad", body: "On my phone." });
413431 const id = (await waiting("menu bad"))!;

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs