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

The repository page's file listing, README and strips follow the opening principles #9651

Merged opened this

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

Merged

mason/forge:mason/9634-the-repository-page-s-file-listing-readm → main 3 commits fork 5 threads · 5 unresolved

  • Merged. mason/9634-the-repository-page-s-file-listing-readm was merged into main as 3bc3b75.

Review

Sign in to review this change.
7 files changed +57 −26
docs/design/areas/code.md +2
@@ -6,3 +6,5 @@
66 `.ex-more` also closes a windowed directory page and the side Files column: the "Show all N entries" link to `?all=1`.
77
88 - `.rail-facts`, `.rail-fact` — the facts in a repository's About rail (`aboutCard` in `src/web/pages/repo.ts`): a `dl` of label-over-value pairs, 12px apart, no rules between them, values wrapping anywhere. The rail is narrower than `.status-row`'s 168px label column, so it stacks instead.
9
10 - `.card.bare` — a card with its frame taken off (`card({ extra: "bare" })`): no border, shadow, fill or padding, no rules between rows, head and foot as plain lines. The repository page's file listing, README, Following panel and fork line use it; the markup, and so every hook, stays the same.
src/web/pages/following.ts +2
@@ -229,6 +229,7 @@ export function followingCard(
229229 ctx: Ctx,
230230 repo: Repo,
231231 following: Following,
232 o: { bare?: boolean } = {},
232233 ): Html {
233234 const { relation, converge } = following;
234235 const parent = relation.parent;
@@ -389,6 +390,7 @@ export function followingCard(
389390 )}`,
390391 {
391392 id: "following",
outdated

This card is shared with the studio app page; that page loses its box too. Scope it or state and check it.

393 ...(o.bare === true ? { extra: "bare" } : {}),
392394 head,
393395 ...(actions.length > 0 ? { foot: html`${actions}` } : {}),
394396 pad: false,
src/web/pages/repo.ts +26 −25
@@ -97,6 +97,7 @@ import {
9797 tokenCount,
9898 icon,
9999 listCard,
100 type CardOptions,
100101 row,
101102 seg,
102103 time,
@@ -274,29 +275,26 @@ function forkBar(ctx: Ctx, repo: Repo): Html {
274275 const canForge = ctx.auth.user?.kind === "user";
275276 const forkCount = forksFor(ctx, repo).length;
276277 if (!canFork && !canForge && forkCount === 0) return html``;
277 return html`<div class="mt">
278 ${listCard([
279 row({
280 lead: icon("fork"),
281 title:
282 forkCount > 0
283 ? html`<a href="${repoBase(repo)}/forks"
284 >${badge(`${forkCount} fork${forkCount === 1 ? "" : "s"}`)}</a
285 >`
286 : "Forks",
287 ...(guest && canFork ? { meta: html`${HELP_BUILD_NEXT}` } : {}),
288 trail: html`${canFork
289 ? html`<form method="post" action="${repoBase(repo)}/fork">
290 ${btn(guest ? HELP_BUILD : "Fork", { size: "sm", icon: "fork" })}
291 </form>`
292 : html``}${canForge
293 ? html`<form method="post" action="${repoBase(repo)}/forge">
294 ${btn("Forge it", { size: "sm", icon: "rocket", variant: "outline" })}
295 </form>`
296 : html``}`,
297 }),
298 ])}
299 </div>`;
278 const forks = row({
279 lead: icon("fork"),
280 title:
281 forkCount > 0
282 ? html`<a href="${repoBase(repo)}/forks"
283 >${badge(`${forkCount} fork${forkCount === 1 ? "" : "s"}`)}</a
284 >`
285 : "Forks",
286 ...(guest && canFork ? { meta: html`${HELP_BUILD_NEXT}` } : {}),
287 trail: html`${canFork
288 ? html`<form method="post" action="${repoBase(repo)}/fork">
289 ${btn(guest ? HELP_BUILD : "Fork", { size: "sm", icon: "fork" })}
290 </form>`
291 : html``}${canForge
292 ? html`<form method="post" action="${repoBase(repo)}/forge">
293 ${btn("Forge it", { size: "sm", icon: "rocket", variant: "outline" })}
294 </form>`
295 : html``}`,
296 });
297 return html`<div class="mt">${listCard([forks], { extra: "bare" })}</div>`;
300298 }
outdated

Wrap as the file does; and no test covers the fork line being bare.

301299
302300 // POST /:owner/:name/fork
@@ -517,7 +515,9 @@ export function treeListing(
517515 >
518516 ${total}`
519517 : total;
520 return listCard(rows, foot.text.trim() === "" ? {} : { foot });
518 const options: CardOptions = { extra: "bare" };
519 if (foot.text.trim() !== "") options.foot = foot;
520 return listCard(rows, options);
outdated

Long line; build the options once instead of two literals.

521521 }
522522
523523 /**
@@ -661,6 +661,7 @@ export async function readmeFor(
661661 ? html``
662662 : html`<p><a href="${`${whole}/${at.split("/").map(encodeURIComponent).join("/")}`}">Read the whole README</a></p>`;
663663 const made = card(html`<div class="prose">${rendered}${more}</div>`, {
664 extra: "bare",
664665 pad: false,
665666 head: html`${icon("file")}
666667 <h2>${readme.name}</h2>`,
@@ -1387,7 +1388,7 @@ async function codeHome(ctx: Ctx, repo: Repo): Promise<Response> {
13871388 ? contributionBar(repo)
13881389 : following === undefined
13891390 ? html`${archivedParentLine(ctx, repo)}${forkBar(ctx, repo)}`
1390 : followingCard(ctx, repo, following)}
1391 : followingCard(ctx, repo, following, { bare: true })}
13911392 ${await renderCode(scope, {
13921393 refs,
13931394 ref,
src/web/style/components.ts +9
@@ -374,6 +374,15 @@ a.sha:hover { color: var(--accent); }
374374 .held-pulls { display: flex; flex-wrap: wrap; gap: 0 4px; align-items: center; }
375375 .held-pulls a { display: inline-flex; align-items: center; min-height: 44px; padding: 0 8px; }
376376 :is(.stack, .split, .timeline) > .card + .card { margin-top: 0; }
377 /* A card that is not a box: space, not lines (DESIGN.md §1). Same markup, no frame. */
378 .card.bare { background: none; border: 0; box-shadow: none; border-radius: 0; overflow: visible; }
379 .card.bare > .card-head { background: none; border: 0; padding: 0 0 8px; }
380 .card.bare > .card-foot { background: none; border: 0; padding: 8px 0 0; }
381 .card.bare > .card-body { padding: 0; }
382 .card.bare .card-body.stack { padding: 0 0 8px; }
383 .card.bare .list > li + li { border-top: 0; }
384 .card.bare .prose { padding: 0; }
385 .card.bare .row { border-radius: var(--radius); }
377386 .card-head {
378387 display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
379388 padding: 10px 14px; border-bottom: 1px solid var(--border); background: var(--muted);
test/following-card.test.ts +11 −1
@@ -61,13 +61,15 @@ 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"\s+id="following"[\s\S]*?<\/ul>/);
64 const m = page.match(/<div\s+class="card bare"\s+id="following"[\s\S]*?<\/ul>/);
6565 expect(m).not.toBeNull();
6666 const panel = m![0];
6767 for (const old of ["following-actions", "following-eyebrow", "following-foot", "following-head", "following-sentence"])
6868 expect(page).not.toContain(old);
6969 expect(page).not.toMatch(/class="[^"]*\bfollowing\b[^"]*"/);
7070 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"/);
7173 expect(panel).toContain('class="badge');
7274 expect(panel).toMatch(/<div class="card-body stack">\s*<p>\s*Everything here is up to date with alice\/lib\./);
7375 expect(panel).toContain('href="/alice/lib"');
@@ -108,3 +110,11 @@ test("the card foot holds the take and propose forms, each a post form with a bt
108110 expect(forms.map((f) => f[1]!.match(/name="do" value="(\w+)"/)![1])).toEqual(["take", "propose"]);
109111 for (const f of forms) expect(f[1]).toMatch(/<button[^>]*type="submit"[^>]*class="btn|<button[^>]*class="btn[^>]*type="submit"/);
110112 }, 30_000);
113
114 test("the studio app page keeps the Following panel boxed", async () => {
115 const { followingCard } = await import("../src/web/pages/following.ts");
116 expect(followingCard.length).toBeLessThanOrEqual(4);
117 const src = await Bun.file(new URL("../src/web/studio/app.ts", import.meta.url)).text();

Non-blocking: asserting the function arity says nothing about behaviour; remove it. The not.toContain("bare") source check is brittle too; render the studio page instead.

118 expect(src).toContain("followingCard(ctx, repo, following)");
119 expect(src).not.toContain("bare");
120 });
test/readme-cache.test.ts +5
@@ -47,6 +47,11 @@ test("the second render of one blob does no markdown work", async () => {
4747 expect(String(second)).toBe(String(first));
4848 });
4949
50 test("the README is a bare card, not a box", async () => {
51 const out = String(await readmeFor(sourceOf("# hi"), "main", [entry(OID_A)], ""));
52 expect(out).toContain('class="card bare"');
53 });
54
5055 test("a changed README renders anew", async () => {
5156 await readmeFor(sourceOf(BIG), "main", [entry(OID_A)], "");
5257 const before = markdownWorkDone();
test/tree-listing.test.ts +2
@@ -24,6 +24,7 @@ describe("treeListing", () => {
2424 expect(out).toContain('href="/o/r/tree/main"'); // ..
2525 expect(out).toContain("a&lt;b&gt;.md");
2626 expect(out).toContain("row-trail");
27 expect(out).toContain('class="card bare"');
outdated

Principle 6 change needs a test that fails without it: assert class="card bare" here rather than loosening to card-foot.

2728 expect(out).toContain('class="card-foot"');
2829 expect(out).toMatch(/2 files here/);
2930 });
@@ -31,5 +32,6 @@ describe("treeListing", () => {
3132 test("no files, no foot", () => {
3233 const none = treeListing("/o/r", "main", "", [entries[0]] as never).text;
3334 expect(none).not.toContain("card-foot");
35 expect(none).toContain('class="card bare"');
3436 });
3537 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs