Public: anyone can read this; the crew can change it.
The /contribute page speaks the home page's language #9843
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/9824-the-contribute-page-speaks-the-home-page → main 3 commits fork 2 threads · 2 unresolved
2 files
changed +76
−13
src/web/pages/contribute.ts +12 −13
| @@ -9,7 +9,6 @@ import type { Ctx } from "../../http/context.ts"; | ||
| 9 | 9 | import { welcome } from "../../system/telemetry.ts"; |
| 10 | 10 | import { html } from "../html.ts"; |
| 11 | 11 | import { page } from "../layout.ts"; |
| 12 | import { card } from "../ui.ts"; | |
| 13 | 12 | |
| 14 | 13 | // GET /contribute |
| 15 | 14 | export function contributePage(ctx: Ctx): Response { |
| @@ -22,20 +21,20 @@ export function contributePage(ctx: Ctx): Response { | ||
| 22 | 21 | <p class="muted">${w.forge}</p> |
| 23 | 22 | </div> |
| 24 | 23 | <p id="bottleneck"><strong>Where it hurts now:</strong> ${w.bottleneck.says}</p> |
| 25 | ${card( | |
| 26 | w.issues.length === 0 | |
| 27 | ? html`<div class="card-body"><p class="muted">No open issue carries the “${w.welcomeLabel}” label right now.</p></div>` | |
| 24 | <section class="stack" aria-labelledby="welcome-h"> | |
| 25 | <h2 id="welcome-h">Issues that welcome outside work</h2> | |
| 26 | ${w.issues.length === 0 | |
| 27 | ? html`<p class="muted">No open issue carries the “${w.welcomeLabel}” label right now.</p>` | |
| 28 | 28 | : html`<ul class="list">${w.issues.map( |
| 29 | 29 | (i) => html`<li class="row"><a href="${i.path}">${i.repo}#${String(i.number)}</a> ${i.title}</li>`, |
| 30 | )}</ul>`, | |
| 31 | { head: html`<h2>Issues that welcome outside work</h2>`, pad: false }, | |
| 32 | )} | |
| 33 | ${card( | |
| 34 | html`<div class="card-body"><ol id="propose">${w.propose.map((s) => html`<li>${s}</li>`)}</ol> | |
| 35 | <p class="muted small">${w.telemetry}</p> | |
| 36 | <p class="muted small">For agents: <span class="mono">GET /api/v1/contribute</span>, or the MCP tool <span class="mono">contribute</span>.</p></div>`, | |
| 37 | { head: html`<h2>How to propose</h2>` }, | |
| 38 | )}`; | |
| 30 | )}</ul>`} | |
| 31 | </section> | |
| 32 | <section class="stack" aria-labelledby="propose-h"> | |
outdated
Same as above (principle 3): the How to propose section needs the stack rhythm too. |
||
| 33 | <h2 id="propose-h">How to propose</h2> | |
| 34 | <ol id="propose">${w.propose.map((s) => html`<li>${s}</li>`)}</ol> | |
| 35 | <p class="muted small">${w.telemetry}</p> | |
| 36 | <p class="muted small">For agents: <span class="mono">GET /api/v1/contribute</span>, or the MCP tool <span class="mono">contribute</span>.</p> | |
| 37 | </section>`; | |
| 39 | 38 | const response = page({ ctx, title: "Contribute", body }); |
| 40 | 39 | notePhase(ctx.request, "render", performance.now() - rendering); |
| 41 | 40 | return response; |
test/contribute-language.test.ts added +64
| @@ -0,0 +1,64 @@ | ||
| 1 | /** /contribute speaks the home page's language (#9824): one heading, plain sections, no card. */ | |
| 2 | import { afterAll, beforeAll, expect, test } from "bun:test"; | |
| 3 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 4 | import { tmpdir } from "node:os"; | |
| 5 | import { join } from "node:path"; | |
| 6 | import { createRepo } from "../src/actions/repos.ts"; | |
| 7 | import { createApp } from "../src/app.ts"; | |
| 8 | import { createUser } from "../src/core/users.ts"; | |
| 9 | import { serve } from "../src/http/server.ts"; | |
| 10 | ||
| 11 | let dir = ""; | |
| 12 | let app: ReturnType<typeof createApp>; | |
| 13 | let servers: Awaited<ReturnType<typeof serve>>; | |
| 14 | ||
| 15 | beforeAll(async () => { | |
| 16 | dir = await mkdtemp(join(tmpdir(), "forge-contribute-lang-")); | |
| 17 | app = createApp({ dataDir: dir, port: 0 }); | |
| 18 | servers = await serve(app); | |
| 19 | }); | |
| 20 | afterAll(async () => { | |
| 21 | servers.stop(); | |
| 22 | app.close(); | |
| 23 | await rm(dir, { recursive: true, force: true }); | |
| 24 | }); | |
| 25 | ||
| 26 | const main = (page: string): string => page.slice(page.indexOf("<main"), page.indexOf("</main>")); | |
| 27 | ||
| 28 | async function contribute(): Promise<string> { | |
| 29 | const res = await fetch(servers.url + "/contribute"); | |
| 30 | expect(res.status).toBe(200); | |
| 31 | return main(await res.text()); | |
| 32 | } | |
| 33 | ||
| 34 | function plain(page: string): void { | |
| 35 | expect(page.match(/<h1[\s>]/g)?.length).toBe(1); | |
| 36 | expect(page).not.toContain('class="card'); | |
| 37 | expect(page).not.toContain("card-body"); | |
| 38 | expect(page).toContain('<ol id="propose">'); | |
| 39 | expect(page).toContain("GET /api/v1/contribute"); | |
| 40 | } | |
| 41 | ||
| 42 | test("signed out with no welcomed issue: the empty sentence and the steps, no card", async () => { | |
| 43 | const page = await contribute(); | |
| 44 | plain(page); | |
| 45 | expect(page).toContain("No open issue carries"); | |
| 46 | }); | |
| 47 | ||
| 48 | test("signed out with a welcomed issue: its link and the steps, no card", async () => { | |
| 49 | const u = await createUser(app.db, { | |
| 50 | origin: "seed", trusted: true, username: "alice", email: "[email protected]", password: "a-good-passphrase", adminIfFirst: true, | |
| 51 | }); | |
| 52 | if (!u.ok) throw new Error(u.error); | |
| 53 | const repo = await createRepo(app, u.value, { name: "garden", visibility: "public" }); | |
| 54 | if (!repo.ok) throw new Error(String(repo.error)); | |
| 55 | const rid = repo.value.id; | |
| 56 | app.db.query("INSERT INTO label (id, repo_id, name) VALUES ('LBLW', ?1, 'help wanted')").run(rid); | |
| 57 | app.db.query("INSERT INTO issue (id, repo_id, number, kind, title, author_id) VALUES ('ISSW', ?1, 1, 'issue', 'tidy the herbs', ?2)").run(rid, u.value.id); | |
| 58 | app.db.query("INSERT INTO issue_label (issue_id, label_id) VALUES ('ISSW', 'LBLW')").run(); | |
| 59 | const page = await contribute(); | |
| 60 | plain(page); | |
| 61 | expect(page).toContain('href="/alice/garden/issues/1"'); | |
| 62 | expect(page).toContain("tidy the herbs"); | |
| 63 | expect(page).not.toContain("No open issue carries"); | |
| 64 | }); |
Principle 3 (generous rhythm): these sections carry no spacing from the design system, whereas the DESIGN.md worked example uses <section class="stack">. Give the two sections the existing
stackrhythm (a wrapping <div class="stack"> or class on each) rather than relying on default margins.