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

A fork's Following card and its forks lineage speak the home page's language #9912

Merged opened this

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

Merged

mason/forge:mason/9841-a-fork-s-following-card-and-its-forks-li → main 2 commits fork

  • Merged. mason/9841-a-fork-s-following-card-and-its-forks-li was merged into main as 4f685d7.

Review

Sign in to review this change.
3 files changed +48 −40
docs/design/areas/family.md +8
@@ -5,3 +5,11 @@ how far behind). It uses the shared list rows with `.list.bare-tree`
55 (components.ts): no card, no rules between rows, no vertical rule; children are
66 indented by `--tree-indent`. Version, last seen, proposals and placement are
77 not on the row; `/forges` carries them.
8
9 ## A fork's Following section and its forks lineage
10
11 On a repository page, `#following` and the lineage of forks are a heading over
12 rows with no card: `section.settings-block` with the heading (the parent, and
13 the state as a badge), the sentences in `.stack`, `row`s, and the actions in a
14 `.bar` beneath. No new class; the `bare` option of `followingCard` now only
15 drops the top margin.
src/web/pages/following.ts +31 −30
@@ -47,7 +47,7 @@ import { proposesViaFork } from "../../origins/fork-route.ts";
4747 import { field, readForm } from "../form.ts";
4848 import { html, when, type Html } from "../html.ts";
4949 import { flashOrJson } from "../layout.ts";
50 import { badge, btn, card, icon, row, type Tone } from "../ui.ts";
50 import { badge, btn, icon, row, type Tone } from "../ui.ts";
5151 import { disconnectedOn } from "./frozen.ts";
5252
5353 /**
@@ -74,20 +74,19 @@ export function lineageCard(lineage: Lineage, hereId: string): Html {
7474 children: n.children.map((c) => node(c, depth + 1)),
7575 });
7676 };
77 return card(
78 html`<ul class="list" aria-label="Forks, by who follows whom">
79 ${node(lineage.root, 0)}
80 </ul>
81 ${when(
82 lineage.truncated,
83 () =>
84 html`<p class="muted small card-body">
85 A large family: the newest and deepest forks are left out here; each
86 fork's own page shows where it stands.
87 </p>`,
88 )}`,
89 { pad: false },
90 );
77 return html`<section class="settings-block">
78 <ul class="list" aria-label="Forks, by who follows whom">
79 ${node(lineage.root, 0)}
80 </ul>
81 ${when(
82 lineage.truncated,
83 () =>
84 html`<p class="muted small">
85 A large family: the newest and deepest forks are left out here; each
86 fork's own page shows where it stands.
87 </p>`,
88 )}
89 </section>`;
9190 }
9291
9392 export interface Following {
@@ -381,21 +380,23 @@ export function followingCard(
381380 title: `${followers === 1 ? "One fork follows" : `${followers} forks follow`} this one.`,
382381 }),
383382 );
384 return card(
385 html`<div class="card-body stack">${prose}</div>${when(
386 rows.length > 0,
387 () => html`<ul class="list">
388 ${rows}
389 </ul>`,
390 )}`,
391 {
392 id: "following",
393 ...(o.bare === true ? { extra: "bare" } : {}),
394 head,
395 ...(actions.length > 0 ? { foot: html`${actions}` } : {}),
396 pad: false,
397 },
398 );
383 return html`<section
384 class="settings-block${o.bare === true ? " bare" : " mt"}"
385 id="following"
386 >
387 <div class="bar"><h2>${head}</h2></div>
388 <div class="stack">${prose}</div>
389 ${when(
390 rows.length > 0,
391 () => html`<ul class="list">
392 ${rows}
393 </ul>`,
394 )}
395 ${when(
396 actions.length > 0,
397 () => html`<div class="bar mt">${actions}</div>`,
398 )}
399 </section>`;
399400 }
400401
401402 /**
test/following-card.test.ts +9 −10
@@ -1,6 +1,6 @@
11 /**
2 * The "following" card on a fork's page is built by card() (#2891): one
3 * `div.card.following#following` with its badge and sentence, not hand-rolled
2 * The "following" section on a fork's page (#9841) is a heading over rows, no box: one
3 * `section#following` with its badge and sentence, not hand-rolled
44 * markup.
55 */
66 import { afterAll, beforeAll, expect, test } from "bun:test";
@@ -61,17 +61,16 @@ afterAll(async () => {
6161 test("a fork's page draws the following panel with card, row and btn", async () => {
6262 const res = await fetch(`${servers.url}/bob/lib`, { headers: { cookie: "forge_mode=dev" } });
6363 const page = await res.text();
64 const m = page.match(/<div\s+class="card bare"\s+id="following"[\s\S]*?<\/ul>/);
64 const m = page.match(/<section\s+class="settings-block bare"\s+id="following"[\s\S]*?<\/section>/);
6565 expect(m).not.toBeNull();
6666 const panel = m![0];
67 expect(panel).not.toContain('class="card');
6768 for (const old of ["following-actions", "following-eyebrow", "following-foot", "following-head", "following-sentence"])
6869 expect(page).not.toContain(old);
6970 expect(page).not.toMatch(/class="[^"]*\bfollowing\b[^"]*"/);
70 expect(panel).toContain("card-head");
71 // The fork line under it is bare too.
72 expect(page).toMatch(/class="mt">\s*<div\s+class="card bare"/);
71 expect(panel).toMatch(/<h2>/);
7372 expect(panel).toContain('class="badge');
74 expect(panel).toMatch(/<div class="card-body stack">\s*<p>\s*Everything here is up to date with alice\/lib\./);
73 expect(panel).toMatch(/<div class="stack">\s*<p>\s*Everything here is up to date with alice\/lib\./);
7574 expect(panel).toContain('href="/alice/lib"');
7675 expect(panel).toContain("following");
7776 }, 30_000);
@@ -99,10 +98,10 @@ test("the card foot holds the take and propose forms, each a post form with a bt
9998 if (!page.includes('value="take"')) await Bun.sleep(40);
10099 }
101100 if (!page.includes('value="take"')) throw new Error(`never came: still waiting while !page.includes('value="take"')`);
102 const foot = page.match(/<div class="card-foot">([\s\S]*?)<\/div>\s*<\/div>/);
101 const foot = page.match(/<div class="bar mt">([\s\S]*?)<\/div>\s*<\/section>/);
103102 expect(foot).not.toBeNull();
104103 // Prose wraps: sentences and notes are paragraphs, never a (nowrap) row title.
105 const body = page.match(/id="following"[\s\S]*?<div class="card-body stack">([\s\S]*?)<\/div>/);
104 const body = page.match(/id="following"[\s\S]*?<div class="stack">([\s\S]*?)<\/div>/);
106105 expect(body).not.toBeNull();
107106 expect(body![1]).toContain("<p>");
108107 expect(body![1]).not.toContain("row-title");
@@ -111,7 +110,7 @@ test("the card foot holds the take and propose forms, each a post form with a bt
111110 for (const f of forms) expect(f[1]).toMatch(/<button[^>]*type="submit"[^>]*class="btn|<button[^>]*class="btn[^>]*type="submit"/);
112111 }, 30_000);
113112
114 test("the studio app page keeps the Following panel boxed", async () => {
113 test("the studio app page draws the Following section unboxed, without the bare variant", async () => {
115114 const { followingCard } = await import("../src/web/pages/following.ts");
116115 expect(followingCard.length).toBeLessThanOrEqual(4);
117116 const src = await Bun.file(new URL("../src/web/studio/app.ts", import.meta.url)).text();

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs