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

The waves page speaks the home page's language, with looks before and after #9696

Merged opened this

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

Merged

mason/forge:mason/9649-the-waves-page-speaks-the-home-page-s-la → main 5 commits fork 3 threads · 3 unresolved

  • Merged. mason/9649-the-waves-page-speaks-the-home-page-s-la was merged into main as cedd8b6.

Review

Sign in to review this change.
3 files changed +52 −9
docs/design/areas/waves.md +9
@@ -8,3 +8,12 @@ title and meta wrap and break long words anywhere, in place of the list's
88 one-line ellipsis. No fixed width; the page does not scroll sideways. No new
99 class. The owner's phone look of `/preview-admin/sample/waves` is attached to
1010 the pull from its preview.
11
12 ## The list reads as headlines (#9649)
13
14 The page's `<h1>` is `sr-only` "Waves"; the list is the shared `.list` of
15 `row()`s with no card and no heading over it. A row is the headline, the first
16 highlight's why as the one sentence under it (omitted when empty), and
17 `version · date · N pulls` as a muted trail. No tooltip repeats it. No new
18 class and no CSS beyond the wrapping above. The terminal's waves screen
19 (`src/tui/screens/waves.ts`) is unchanged.
src/web/pages/waves.ts +20 −5
@@ -42,6 +42,17 @@ export function waveLine(w: Wave, visible: number): string {
4242 return `${w.version} · ${w.date} · ${w.headline} · ${visible} ${visible === 1 ? "pull" : "pulls"}`;
4343 }
4444
45 /** One wave on the list: its headline, the first highlight's why as the one sentence under it, and version, date and pull count as a muted trail. */
46 export function waveRow(w: Wave, href: string, visible: number) {
47 const why = w.highlights[0]?.why;
48 return row({
49 href,
50 title: w.headline,
51 ...(why === undefined || why === "" ? {} : { meta: html`${why}` }),
52 trail: html`<span class="muted small">${w.version} · ${w.date} · ${visible} ${visible === 1 ? "pull" : "pulls"}</span>`,
53 });
outdated

Principle 1/8: the meta line packs why, date, version and count into one muted run, and the same facts repeat in the title tooltip, so the row says itself twice. Keep the one sentence and drop the duplicate tooltip. Also needs a test for the no-highlight case.

54 }
55
4556 // GET /:owner/:name/waves
4657 export async function wavesPage(ctx: Ctx): Promise<Response> {
4758 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
@@ -60,11 +71,12 @@ export async function wavesPage(ctx: Ctx): Promise<Response> {
6071
6172 const rows = entries.map((e) =>
6273 e.ok
63 ? row({
64 href: fileHref(e.file),
74 ? waveRow(
75 e.wave,
76 fileHref(e.file),
6577 // The wave's own page lists these same pulls, so the two agree.
66 title: waveLine(e.wave, carriedPulls(ctx.app.db, pullRefs(e.wave), ctx.auth.user).length),
67 })
78 carriedPulls(ctx.app.db, pullRefs(e.wave), ctx.auth.user).length,
79 )
6880 : row({
6981 href: fileHref(e.file),
7082 title: e.file,
@@ -74,6 +86,7 @@ export async function wavesPage(ctx: Ctx): Promise<Response> {
7486
7587 const body = html`${scopeHeader(ctx, scope, "none", { compact: true })}
7688 <div class="mt" data-waves>
89 <h1 class="sr-only">Waves</h1>
7790 ${entries.length === 0
7891 ? empty({
7992 title: "No waves yet",
@@ -86,7 +99,9 @@ export async function wavesPage(ctx: Ctx): Promise<Response> {
8699 }
87100 : {}),
88101 })
89 : listCard(rows, { head: html`<h2>Waves</h2>` })}
102 : html`<ul class="list">
103 ${rows}
104 </ul>`}
90105 </div>`;
91106
92107 const response = page({
test/waves-page.test.ts +23 −4
@@ -9,6 +9,7 @@ import { createSession } from "../src/core/sessions.ts";
99 import { createUser, type User } from "../src/core/users.ts";
1010 import { serve, type Servers } from "../src/http/server.ts";
1111 import { mergedPull, waveNaming } from "./helpers/wave-pulls.ts";
12 import { waveRow } from "../src/web/pages/waves.ts";
1213 import { writeWave, type Wave } from "../src/story/wave.ts";
1314
1415 const PASSWORD = "a-good-passphrase";
@@ -105,8 +106,8 @@ describe("waves page", () => {
105106 expect(at("Tenth")).toBeGreaterThan(-1);
106107 expect(at("Tenth")).toBeLessThan(at("Ninth"));
107108 expect(at("Ninth")).toBeLessThan(at("Zeta"));
108 expect(body).toContain("0.10.0 · 2026-02-01 · Tenth · 0 pulls");
109 expect(body).toContain("0.9.0 · 2026-01-01 · Ninth · 0 pulls");
109 expect(body).toContain("0.10.0 · 2026-02-01 · 0 pulls");
110 expect(body).toContain("0.9.0 · 2026-01-01 · 0 pulls");
110111 expect(at("0.10.0 · 2026-02-01")).toBeLessThan(at("0.9.0 · 2026-01-01"));
111112 expect(body).toContain('href="/alice/pub/waves/0.10.0"');
112113 });
@@ -226,9 +227,9 @@ describe("waves page", () => {
226227
227228 test("a row counts the pulls this viewer can see, as the wave's page lists them", async () => {
228229 const body = await (await get("/alice/pub/waves")).text();
229 expect(body).toContain("0.1.0 · 2026-03-05 · Mixed · 2 pulls");
230 expect(body).toContain("0.1.0 · 2026-03-05 · 2 pulls");
230231 const guest = await (await get("/alice/pub/waves", false)).text();
outdated

This only adjusts an old assertion. Add tests that the highlight's why is in the row, the sr-only h1 is present, and the card heading is gone.

231 expect(guest).toContain("0.1.0 · 2026-03-05 · Mixed · 1 pull<");
232 expect(guest).toContain("0.1.0 · 2026-03-05 · 1 pull<");
232233 expect(guest).not.toContain("2 pulls");
233234 expect(guest).not.toContain("Secret work");
234235 const page = await (await get("/alice/pub/waves/0.1.0", false)).text();
@@ -236,6 +237,24 @@ describe("waves page", () => {
236237 expect(page).not.toContain("Secret work");
237238 });
238239
240 test("a row is the headline with the first highlight's why under it, in a list with no card or Waves heading", async () => {
241 const body = await (await get("/alice/pub/waves")).text();
242 expect(body).toContain('<h1 class="sr-only">Waves</h1>');
243 expect(body).not.toContain("<h2>Waves</h2>");
244 expect(body).not.toContain('class="card');
245 expect(body).toMatch(/class="row-title"[^>]*>Tenth<[\s\S]*?class="row-meta">\s*because\s*</);
246 expect(body).not.toContain("title=\"0.10.0 ·");
247 });
248
249 test("a wave whose first why is empty has a row with no sentence and no stray separator", () => {
250 const w = { ...wave("0.9.0", "2026-01-01", "Quiet"), highlights: [{ title: "T", why: "" }] };
251 const r = waveRow(w, "/x", 1).text;
252 expect(r).toContain("Quiet");
253 expect(r).not.toContain("row-meta");
254 expect(r).toContain("0.9.0 · 2026-01-01 · 1 pull");
255 expect(r).not.toMatch(/·\s*·/);
256 });
257
239258 test("waves.atom titles an entry with the headline", async () => {
240259 const body = await (await get("/alice/pub/waves.atom")).text();
241260 expect(body).toContain("<title>Tenth</title>");

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs