Public: anyone can read this; the crew can change it.
The repository page speaks the home page's language, with looks before and after #9621
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/9585-the-repository-page-speaks-the-home-page → main 4 commits fork 3 threads · 3 unresolved
5 files
changed +91
−90
docs/design/areas/code.md +2
| @@ -4,3 +4,5 @@ | ||
| 4 | 4 | - `.pdf-view`, `.pdf-links`, `.pdf-frame`, `.pdf-fallback` — a PDF on its blob page (`render/pdf.ts`): one card holding the browser's viewer in a full-width frame 80vh tall (480px at least), under an always-visible Open · Download line (the frame is 60vh on a phone so a thumb can scroll past it), with a padded link line inside for a browser that cannot draw it. |
| 5 | 5 | |
| 6 | 6 | `.ex-more` also closes a windowed directory page and the side Files column: the "Show all N entries" link to `?all=1`. |
| 7 | ||
| 8 | - `.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. |
src/web/pages/repo.ts +78 −84
| @@ -785,6 +785,15 @@ export function codeBar( | ||
| 785 | 785 | })}`; |
| 786 | 786 | } |
| 787 | 787 | |
| 788 | /** | |
| 789 | * One fact of the About column: a `dl` pair, the label over its value. The | |
| 790 | * rail is narrower than `.status-row`'s 168px label column, so it stacks and | |
| 791 | * separates the facts with space, not a rule per row. | |
| 792 | */ | |
| 793 | function fact(label: string, value: Html): Html { | |
| 794 | return html`<div class="rail-fact"><dt>${label}</dt><dd>${value}</dd></div>`; | |
| 795 | } | |
| 796 | ||
| 788 | 797 | /** |
| 789 | 798 | * The About rail: what this repo is, in one glance, plus how to clone it. |
| 790 | 799 | * |
| @@ -857,82 +866,70 @@ function aboutCard( | ||
| 857 | 866 | </p> |
| 858 | 867 | </div>`, |
| 859 | 868 | ); |
| 860 | return html`${card( | |
| 861 | html`${when( | |
| 862 | repo.description !== "", | |
| 863 | () => html`<p class="mb">${repo.description}</p>`, | |
| 864 | )} | |
| 865 | <ul class="list setting-list"> | |
| 866 | ${row({ title: "Visibility", trail: html`${repo.visibility}` })} | |
| 867 | ${row({ | |
| 868 | title: "Default", | |
| 869 | trail: html`<span class="mono">${repo.defaultBranch}</span>`, | |
| 870 | })} | |
| 871 | ${row({ title: "Protection", trail: html`${repo.protectDefault ? "on" : "off"}` })} | |
| 872 | ${row({ | |
| 873 | title: "Branches", | |
| 874 | trail: html`<span class="num">${refs.branches.length}</span>`, | |
| 875 | })} | |
| 876 | ${when( | |
| 877 | refs.tags.length > 0, | |
| 878 | () => | |
| 879 | row({ | |
| 880 | title: "Tags", | |
| 881 | trail: html`<span class="num">${refs.tags.length}</span>`, | |
| 882 | }), | |
| 883 | )} | |
| 884 | ${when( | |
| 885 | parent, | |
| 886 | (p) => | |
| 887 | // The standing and the one thing to do about it live in the | |
| 888 | // Following card above the code; the rail only names the parent. | |
| 889 | row({ | |
| 890 | title: "Fork of", | |
| 891 | trail: html`<a href="/${p.ownerName}/${p.name}" | |
| 892 | >${p.ownerName}/${p.name}</a | |
| 893 | >`, | |
| 894 | }), | |
| 895 | )} | |
| 896 | ${when( | |
| 897 | upstream, | |
| 898 | (up) => | |
| 899 | row({ | |
| 900 | title: "Fork of", | |
| 901 | trail: | |
| 902 | upstreamHref === undefined | |
| 903 | ? html`<span class="mono">${up}</span>` | |
| 904 | : html`<a href="${upstreamHref}">${up}</a>`, | |
| 905 | }), | |
| 906 | )} | |
| 907 | </ul> | |
| 908 | ${followingLine(ctx, repo)} | |
| 909 | ${whereItLives} | |
| 910 | ${when( | |
| 911 | repo.visibility === "public" && repo.origin === null, | |
| 912 | () => | |
| 913 | html`<p class="mt small muted"> | |
| 914 | ${icon("lock")} Found a security problem? | |
| 915 | <a href="${repoBase(repo)}/issues/new?sealed=1" | |
| 916 | >Report it sealed</a | |
| 917 | >: only the people who keep this repository and its crew see it | |
| 918 | until the fix is released. | |
| 919 | </p>`, | |
| 920 | )} | |
| 921 | <p class="mt small muted">Clone over HTTPS</p> | |
| 922 | ${copyField("clone-https", cloneUrl, "HTTPS clone address", "Copy clone URL")} | |
| 923 | ${when( | |
| 924 | sshUrl, | |
| 925 | (url) => | |
| 926 | html`<p class="mt small muted">Clone over SSH</p> | |
| 927 | ${copyField( | |
| 928 | "clone-ssh", | |
| 929 | url, | |
| 930 | "SSH clone address", | |
| 931 | "Copy SSH clone URL", | |
| 932 | )}`, | |
| 933 | )}`, | |
| 934 | { head: html`<h2>About</h2>` }, | |
| 935 | )}`; | |
| 869 | return html`<section class="settings-block"> | |
| 870 | <h2>About</h2> | |
| 871 | ${when( | |
| 872 | repo.description !== "", | |
| 873 | () => html`<p class="mb">${repo.description}</p>`, | |
| 874 | )} | |
| 875 | <dl class="rail-facts"> | |
| 876 | ${fact("Visibility", html`${repo.visibility}`)} | |
| 877 | ${fact("Default", html`<span class="mono">${repo.defaultBranch}</span>`)} | |
| 878 | ${fact("Protection", html`${repo.protectDefault ? "on" : "off"}`)} | |
| 879 | ${fact("Branches", html`<span class="num">${refs.branches.length}</span>`)} | |
| 880 | ${when( | |
| 881 | refs.tags.length > 0, | |
| 882 | () => | |
| 883 | fact("Tags", html`<span class="num">${refs.tags.length}</span>`), | |
| 884 | )} | |
| 885 | ${when( | |
| 886 | parent, | |
| 887 | (p) => | |
| 888 | // The standing and the one thing to do about it live in the | |
| 889 | // Following card above the code; the rail only names the parent. | |
| 890 | fact( | |
| 891 | "Fork of", | |
| 892 | html`<a href="/${p.ownerName}/${p.name}">${p.ownerName}/${p.name}</a>`, | |
| 893 | ), | |
| 894 | )} | |
| 895 | ${when( | |
| 896 | upstream, | |
| 897 | (up) => | |
| 898 | fact( | |
| 899 | "Fork of", | |
| 900 | upstreamHref === undefined | |
| 901 | ? html`<span class="mono">${up}</span>` | |
| 902 | : html`<a href="${upstreamHref}">${up}</a>`, | |
| 903 | ), | |
| 904 | )} | |
| 905 | </dl> | |
| 906 | ${followingLine(ctx, repo)} | |
| 907 | ${whereItLives} | |
| 908 | ${when( | |
| 909 | repo.visibility === "public" && repo.origin === null, | |
| 910 | () => | |
| 911 | html`<p class="mt small muted"> | |
| 912 | ${icon("lock")} Found a security problem? | |
| 913 | <a href="${repoBase(repo)}/issues/new?sealed=1" | |
| 914 | >Report it sealed</a | |
| 915 | >: only the people who keep this repository and its crew see it | |
| 916 | until the fix is released. | |
| 917 | </p>`, | |
| 918 | )} | |
| 919 | <p class="mt small muted">Clone over HTTPS</p> | |
| 920 | ${copyField("clone-https", cloneUrl, "HTTPS clone address", "Copy clone URL")} | |
| 921 | ${when( | |
| 922 | sshUrl, | |
| 923 | (url) => | |
| 924 | html`<p class="mt small muted">Clone over SSH</p> | |
| 925 | ${copyField( | |
| 926 | "clone-ssh", | |
| 927 | url, | |
| 928 | "SSH clone address", | |
| 929 | "Copy SSH clone URL", | |
| 930 | )}`, | |
| 931 | )} | |
| 932 | </section>`; | |
| 936 | 933 | } |
| 937 | 934 | |
| 938 | 935 | // ---- pages ---- |
| @@ -1316,18 +1313,15 @@ async function codeHome(ctx: Ctx, repo: Repo): Promise<Response> { | ||
| 1316 | 1313 | title: "An empty repository, ready for its first push", |
| 1317 | 1314 | note: "Point a local repository at it and push — the first branch you push becomes the code you see here.", |
| 1318 | 1315 | })} |
| 1319 | ${card( | |
| 1320 | copyField( | |
| 1316 | <section class="settings-block"> | |
| 1317 | <h2>Get started</h2> | |
| 1318 | ${copyField( | |
| 1321 | 1319 | "clone-https", |
| 1322 | 1320 | `git remote add origin ${cloneUrl} && git push -u origin ${repo.defaultBranch}`, |
| 1323 | 1321 | "Get started command", |
| 1324 | 1322 | "Copy the get started command", |
| 1325 | ), | |
| 1326 | { | |
| 1327 | head: html`${icon("command")} | |
| 1328 | <h2>Get started</h2>`, | |
| 1329 | }, | |
| 1330 | )} | |
| 1323 | )} | |
| 1324 | </section> | |
| 1331 | 1325 | </div>`; |
| 1332 | 1326 | const emptyResponse = page({ |
| 1333 | 1327 | ctx, |
src/web/style/content.ts +3
| @@ -414,6 +414,9 @@ form.inline { display: inline; margin-left: auto; } | ||
| 414 | 414 | |
| 415 | 415 | /* A rendered Markdown blob wears the same frame its source does: one card, |
| 416 | 416 | one path header, and the switch between the two views in it. */ |
| 417 | .rail-facts { margin: 12px 0 0; display: grid; gap: 12px; font-size: 13px; } | |
Non-blocking: the three .rail-* rules landed between the "A rendered Markdown blob..." comment and the .blob-view rules it describes. Move them below .blob-view, with their own one-line comment. |
||
| 418 | .rail-fact > dt { color: var(--muted-fg); font-size: 12.5px; } | |
| 419 | .rail-fact > dd { margin: 2px 0 0; min-width: 0; overflow-wrap: anywhere; } | |
| 417 | 420 | .blob-view { margin-left: 8px; } |
| 418 | 421 | .blob-view .seg { font-size: 12.5px; } |
| 419 | 422 | /* An audio or video file, played inline (render/player.ts). The video is as |
test/repo-about-card.test.ts +6 −3
| @@ -53,14 +53,17 @@ afterAll(async () => { | ||
| 53 | 53 | await rm(dataDir, { recursive: true, force: true }); |
| 54 | 54 | }); |
| 55 | 55 | |
| 56 | test("the About column is a card with §5 rows and no .about", async () => { | |
| 56 | test("the About column is a settings block with stacked `dl` facts and no .about", async () => { | |
| 57 | 57 | const r = await fetch(`${servers.url}/olive/yard`); |
| 58 | 58 | expect(r.status).toBe(200); |
| 59 | 59 | const html = await r.text(); |
| 60 | 60 | expect(html).not.toMatch(/class="[^"]*\babout\b/); |
| 61 | expect(html).not.toContain("<dl"); | |
| 62 | 61 | const rail = html.match(/<aside[^>]*>[\s\S]*?<\/aside>/)?.[0] ?? ""; |
| 63 | expect(rail).toContain('class="card"'); | |
| 62 | expect(rail).toContain('class="settings-block"'); | |
| 63 | expect(rail).toContain('<dl class="rail-facts">'); | |
outdated
The |
||
| 64 | expect(rail).toContain('<div class="rail-fact"><dt>Visibility</dt>'); | |
| 65 | expect(rail).not.toContain("status-row"); | |
| 66 | expect(rail).not.toContain('class="card"'); | |
| 64 | 67 | expect(rail).toContain("<h2>About</h2>"); |
| 65 | 68 | expect(rail).toContain("a small yard"); |
| 66 | 69 | for (const word of ["Visibility", "Default", "Protection", "Branches"]) |
test/web-origins.test.ts +2 −3
| @@ -515,10 +515,9 @@ describe("origins on the web", () => { | ||
| 515 | 515 | expect(html).toContain("Sync now"); |
| 516 | 516 | expect(html).toContain("Pushes here go through to"); |
| 517 | 517 | expect(html).not.toMatch(/class="about"/); |
| 518 | // The facts render as §5 rows inside a labelled complementary landmark. | |
| 518 | // The facts render as `dl` rows inside a labelled complementary landmark. | |
| 519 | 519 | expect(html).toContain('<aside aria-label="About">'); |
| 520 | expect(html).toMatch(/row-title"[^>]*>Visibility</); | |
| 521 | expect(html).not.toContain("<dl"); | |
| 520 | expect(html).toMatch(/<dt>Visibility<\/dt>/); | |
| 522 | 521 | }); |
| 523 | 522 | |
| 524 | 523 | test("an imported issue shows the origin's author, not the local one", async () => { |
Principles 3 and 6: this row becomes
.status-row, which draws a hairline under every fact and a 168px label column at 640px+, inside a narrow rail. Make a rail-sized variant (label over value, space not lines) or show in the looks that it holds at 1440 and 390.