Public: anyone can read this; the crew can change it.
Design: a repository's about column uses §5's card, and about leaves §5d #4050
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/3952-design-a-repository-s-about-column-uses → main 11 commits fork 4 threads · 4 unresolved
6 files
changed +81
−67
docs/DESIGN.md +1 −1
| @@ -393,7 +393,7 @@ gone, so this list only shrinks. | ||
| 393 | 393 | - **`style/base.ts`** — the page shell (topbar, main, footer) and the layout and text utilities every page leans on: `.bar` `.bare` `.foot` `.grow` `.hide-lg` `.hide-sm` `.js-only` `.live-pill` `.mark` `.mb` `.mt` `.muted` `.narrow` `.narrow-wide` `.num` `.reduced` `.shell` `.shell-wide` `.skip` `.small` `.spacer` `.sr-only` `.stack` `.stack-tight` `.topbar` `.topbar-tools` `.truncate` `.wordmark` |
| 394 | 394 | - **`style/components.ts`** — parts of the §5 components that the table does not spell out — the palette's list, the menu's items, a dialog's head and foot, comments, the theme picker — and a few small helpers beside them: `.cat` `.clone` `.clone-text` `.comment` `.comment-body` `.comment-form` `.dash` `.dialog-body` `.dialog-foot` `.dialog-head` `.empty-title` `.error-note` `.field-row` `.form-actions` `.hex` `.ico` `.ico-loader` `.list` `.live-status` `.menu-item` `.menu-panel` `.menu-sep` `.menu-title` `.palette-empty` `.palette-foot` `.palette-group` `.palette-heading` `.palette-input` `.palette-item` `.palette-list` `.passkey` `.passkey-lead` `.passkey-note` `.passkey-off` `.passkey-on` `.preview` `.sep` `.split` `.sprite` `.swatch` `.table-scroll` `.tell-here` `.theme-apply` `.theme-option` `.tick` `.timeline` `.tl-dot` `.toast-body` `.topbar-search` |
| 395 | 395 | - **`style/components.ts`, settings rows** — a list whose rows are settings (limits, capacity): the `<ul class="list setting-list">` lets title and meta wrap instead of ellipsizing, and a row's controls take their own line under 640px. `.row-fields` is the form in that row's trail (input, Save, Default): always visible, never hover-gated, wrapping under the text before it would clip. Use both for any page whose rows show a name, a current value, and a control to change it; use `.row-actions` only for secondary actions (delete, wake, pin, revoke). Flags, telemetry, cloud, people, crew roles and subscription headroom/budget use `.row-fields`. Classes: `.row-fields` `.setting-list` |
| 396 | - **`style/content.ts`, page furniture** — what frames a page's content: page and repo heads, the merge box, the front page and its tiles, explore, backups, boards, the fleet chart's series colours, following, lineage: `.about` `.act` `.app-shelf` `.app-tile` `.app-tile-body` `.app-tile-desc` `.app-tile-look` `.app-tile-name` `.backups-drill` `.backups-error` `.backups-now` `.backups-restore` `.bform` `.bgrid` `.big` `.bmanage` `.boards-result` `.bp-h2` `.bp-h3` `.bp-w2` `.bp-w3` `.break` `.bt-bar` `.bt-grid` `.bt-group` `.bt-hist` `.bt-name` `.bt-procs` `.bt-row` `.bt-val` `.chart` `.chart-box` `.commit-body` `.commit-head` `.connect-more` `.connect-path` `.domain-records` `.explore-filters` `.explore-line` `.explore-main` `.explore-name` `.explore-row` `.fg-s0` `.fg-s1` `.fg-s2` `.fg-s3` `.fg-s4` `.fg-s5` `.fg-s6` `.fg-s7` `.filters` `.flag-form` `.following` `.following-actions` `.following-eyebrow` `.following-foot` `.following-head` `.following-sentence` `.fork-bar` `.front-admin` `.front-controls` `.front-crew` `.front-featured` `.front-featured-body` `.front-featured-look` `.front-issues` `.front-live` `.front-more` `.front-pick` `.front-pin-any` `.front-weights` `.frozen` `.frozen-form` `.frozen-sentence` `.handoff` `.handoff-links` `.headroom` `.issue-head` `.issue-meta` `.lineage` `.lineage-children` `.lineage-here` `.merge-box` `.merge-form` `.merge-headline` `.merge-sub` `.mirror-form` `.number` `.origin-link` `.origin-mark` `.origin-name` `.origin-row` `.owner` `.page-head` `.path-head` `.policy-line` `.qf` `.repo-actions` `.repo-audience` `.repo-desc` `.repo-layout` `.repo-lineage` `.repo-title` `.search` `.slash` `.stage-detail` `.stage-dot` `.steps` `.sub` `.sys-pair` `.table-wrap` `.terminal-line` `.tok` `.tree-row` `.tree-total` `.wall-cells` `.where-sep` | |
| 396 | - **`style/content.ts`, page furniture** — what frames a page's content: page and repo heads, the merge box, the front page and its tiles, explore, backups, boards, the fleet chart's series colours, following, lineage: `.act` `.app-shelf` `.app-tile` `.app-tile-body` `.app-tile-desc` `.app-tile-look` `.app-tile-name` `.backups-drill` `.backups-error` `.backups-now` `.backups-restore` `.bform` `.bgrid` `.big` `.bmanage` `.boards-result` `.bp-h2` `.bp-h3` `.bp-w2` `.bp-w3` `.break` `.bt-bar` `.bt-grid` `.bt-group` `.bt-hist` `.bt-name` `.bt-procs` `.bt-row` `.bt-val` `.chart` `.chart-box` `.commit-body` `.commit-head` `.connect-more` `.connect-path` `.domain-records` `.explore-filters` `.explore-line` `.explore-main` `.explore-name` `.explore-row` `.fg-s0` `.fg-s1` `.fg-s2` `.fg-s3` `.fg-s4` `.fg-s5` `.fg-s6` `.fg-s7` `.filters` `.flag-form` `.following` `.following-actions` `.following-eyebrow` `.following-foot` `.following-head` `.following-sentence` `.fork-bar` `.front-admin` `.front-controls` `.front-crew` `.front-featured` `.front-featured-body` `.front-featured-look` `.front-issues` `.front-live` `.front-more` `.front-pick` `.front-pin-any` `.front-weights` `.frozen` `.frozen-form` `.frozen-sentence` `.handoff` `.handoff-links` `.headroom` `.issue-head` `.issue-meta` `.lineage` `.lineage-children` `.lineage-here` `.merge-box` `.merge-form` `.merge-headline` `.merge-sub` `.mirror-form` `.number` `.origin-link` `.origin-mark` `.origin-name` `.origin-row` `.owner` `.page-head` `.path-head` `.policy-line` `.qf` `.repo-actions` `.repo-audience` `.repo-desc` `.repo-layout` `.repo-lineage` `.repo-title` `.search` `.slash` `.stage-detail` `.stage-dot` `.steps` `.sub` `.sys-pair` `.table-wrap` `.terminal-line` `.tok` `.tree-row` `.tree-total` `.wall-cells` `.where-sep` | |
| 397 | 397 | - **`style/content.ts`, code view** — the source view and the syntax-highlight token classes `highlight-html.ts` builds as `tok-${kind}`: `.blob-view` `.ln` `.tok-attribute` `.tok-comment` `.tok-constant` `.tok-decorator` `.tok-deleted` `.tok-emphasis` `.tok-function` `.tok-heading` `.tok-inserted` `.tok-keyword` `.tok-link` `.tok-meta` `.tok-number` `.tok-operator` `.tok-plain` `.tok-property` `.tok-punctuation` `.tok-regex` `.tok-string` `.tok-tag` `.tok-type` `.tok-variable` |
| 398 | 398 | - **`style/content.ts`, prose (markdown)** — table cell alignment, built by `markdown.ts` as `ta-${align}`: `.ta-c` `.ta-r` |
| 399 | 399 | - **`style/content.ts`, diff viewer** — the diff table: its gutters, line kinds, file head and inline review comments: `.add` `.del` `.dh` `.diff` `.diff-body` `.diff-head` `.diff-note` `.diff-path` `.diff-stat` `.diff-stub` `.diff-summary` `.diff-table` `.dl` `.dl-review` `.dn` `.dn-comment` `.nonl` `.rc-hidden` `.rc-resolve` `.rc-resolved` `.wd` |
src/web/pages/browse.ts +23 −22
| @@ -66,6 +66,7 @@ import { | ||
| 66 | 66 | listCard, |
| 67 | 67 | originTag, |
| 68 | 68 | pager, |
| 69 | row, | |
| 69 | 70 | seg, |
| 70 | 71 | stateBadge, |
| 71 | 72 | time, |
| @@ -556,32 +557,33 @@ export async function browseCommitPage(ctx: Ctx): Promise<Response> { | ||
| 556 | 557 | function aboutRail(scope: RepoScope): Html { |
| 557 | 558 | const facts = scope.facts; |
| 558 | 559 | const origin = scope.origin; |
| 559 | return html`<aside class="about"> | |
| 560 | ${card( | |
| 560 | return html`${card( | |
| 561 | 561 | html`${when( |
| 562 | 562 | facts.description !== "", |
| 563 | 563 | () => html`<p class="mb">${facts.description}</p>`, |
| 564 | 564 | )} |
| 565 | 565 | <div class="where-block"> |
| 566 | 566 | <h3>Where it lives</h3> |
| 567 | <dl> | |
| 568 | <dt>Origin</dt> | |
| 569 | <dd> | |
| 570 | ${scope.originUrl === "" | |
| 571 | ? html`${scope.fullName}` | |
| 572 | : html`<a href="${scope.originUrl}" rel="noreferrer noopener" | |
| 573 | >${scope.fullName}</a | |
| 574 | >`} | |
| 575 | </dd> | |
| 576 | <dt>Last push</dt> | |
| 577 | <dd> | |
| 578 | ${facts.pushedAt === null ? "unknown" : time(facts.pushedAt)} | |
| 579 | </dd> | |
| 580 | <dt>Visibility</dt> | |
| 581 | <dd>${facts.visibility}</dd> | |
| 582 | <dt>Default</dt> | |
| 583 | <dd class="mono">${facts.defaultBranch}</dd> | |
| 584 | </dl> | |
| 567 | <ul class="list setting-list"> | |
| 568 | ${row({ | |
| 569 | title: "Origin", | |
| 570 | trail: | |
| 571 | scope.originUrl === "" | |
| 572 | ? html`${scope.fullName}` | |
| 573 | : html`<a href="${scope.originUrl}" rel="noreferrer noopener" | |
| 574 | >${scope.fullName}</a | |
| 575 | >`, | |
| 576 | })} | |
| 577 | ${row({ | |
| 578 | title: "Last push", | |
| 579 | trail: html`${facts.pushedAt === null ? "unknown" : time(facts.pushedAt)}`, | |
| 580 | })} | |
| 581 | ${row({ title: "Visibility", trail: html`${facts.visibility}` })} | |
| 582 | ${row({ | |
| 583 | title: "Default", | |
| 584 | trail: html`<span class="mono">${facts.defaultBranch}</span>`, | |
| 585 | })} | |
| 586 | </ul> | |
| 585 | 587 | <p class="mt small muted"> |
| 586 | 588 | Nothing is stored here yet. Mirror it and this becomes a repository |
| 587 | 589 | you own — issues, pulls and comments come with it, and pushes go |
| @@ -606,8 +608,7 @@ function aboutRail(scope: RepoScope): Html { | ||
| 606 | 608 | )} |
| 607 | 609 | </div>`, |
| 608 | 610 | { head: html`<h2>About</h2>` }, |
| 609 | )} | |
| 610 | </aside>`; | |
| 611 | )}`; | |
| 611 | 612 | } |
| 612 | 613 | |
| 613 | 614 | // ---- issues and pulls ---- |
src/web/pages/repo.ts +46 −40
| @@ -571,18 +571,21 @@ function aboutCard( | ||
| 571 | 571 | (o) => |
| 572 | 572 | html`<div class="where-block"> |
| 573 | 573 | <h3>Where it lives</h3> |
| 574 | <dl> | |
| 575 | <dt>Origin</dt> | |
| 576 | <dd> | |
| 577 | ${o.htmlUrl === "" | |
| 578 | ? html`${o.fullName}` | |
| 579 | : html`<a href="${o.htmlUrl}" rel="noreferrer noopener" | |
| 580 | >${o.fullName}</a | |
| 581 | >`} | |
| 582 | </dd> | |
| 583 | <dt>Last fetch</dt> | |
| 584 | <dd>${o.lastFetchAt === null ? "never" : time(o.lastFetchAt)}</dd> | |
| 585 | </dl> | |
| 574 | <ul class="list setting-list"> | |
| 575 | ${row({ | |
| 576 | title: "Origin", | |
| 577 | trail: | |
| 578 | o.htmlUrl === "" | |
| 579 | ? html`${o.fullName}` | |
| 580 | : html`<a href="${o.htmlUrl}" rel="noreferrer noopener" | |
| 581 | >${o.fullName}</a | |
| 582 | >`, | |
| 583 | })} | |
| 584 | ${row({ | |
| 585 | title: "Last fetch", | |
| 586 | trail: html`${o.lastFetchAt === null ? "never" : time(o.lastFetchAt)}`, | |
| 587 | })} | |
| 588 | </ul> | |
| 586 | 589 | ${when( |
| 587 | 590 | o.fetchError !== "", |
| 588 | 591 | () => html`<p class="error-note mt">${o.fetchError}</p>`, |
| @@ -601,50 +604,54 @@ function aboutCard( | ||
| 601 | 604 | </p> |
| 602 | 605 | </div>`, |
| 603 | 606 | ); |
| 604 | return html`<aside class="about"> | |
| 605 | ${card( | |
| 607 | return html`${card( | |
| 606 | 608 | html`${when( |
| 607 | 609 | repo.description !== "", |
| 608 | 610 | () => html`<p class="mb">${repo.description}</p>`, |
| 609 | 611 | )} |
| 610 | <dl> | |
| 611 | <dt>Visibility</dt> | |
| 612 | <dd>${repo.visibility}</dd> | |
| 613 | <dt>Default</dt> | |
| 614 | <dd class="mono">${repo.defaultBranch}</dd> | |
| 615 | <dt>Protection</dt> | |
| 616 | <dd>${repo.protectDefault ? "on" : "off"}</dd> | |
| 617 | <dt>Branches</dt> | |
| 618 | <dd class="num">${refs.branches.length}</dd> | |
| 612 | <ul class="list setting-list"> | |
| 613 | ${row({ title: "Visibility", trail: html`${repo.visibility}` })} | |
| 614 | ${row({ | |
| 615 | title: "Default", | |
| 616 | trail: html`<span class="mono">${repo.defaultBranch}</span>`, | |
| 617 | })} | |
| 618 | ${row({ title: "Protection", trail: html`${repo.protectDefault ? "on" : "off"}` })} | |
| 619 | ${row({ | |
| 620 | title: "Branches", | |
| 621 | trail: html`<span class="num">${refs.branches.length}</span>`, | |
| 622 | })} | |
| 619 | 623 | ${when( |
| 620 | 624 | refs.tags.length > 0, |
| 621 | 625 | () => |
| 622 | html`<dt>Tags</dt> | |
| 623 | <dd class="num">${refs.tags.length}</dd>`, | |
| 626 | row({ | |
| 627 | title: "Tags", | |
| 628 | trail: html`<span class="num">${refs.tags.length}</span>`, | |
| 629 | }), | |
| 624 | 630 | )} |
| 625 | 631 | ${when( |
| 626 | 632 | parent, |
| 627 | 633 | (p) => |
| 628 | 634 | // The standing and the one thing to do about it live in the |
| 629 | 635 | // Following card above the code; the rail only names the parent. |
| 630 | html`<dt>Fork of</dt> | |
| 631 | <dd> | |
| 632 | <a href="/${p.ownerName}/${p.name}" | |
| 633 | >${p.ownerName}/${p.name}</a | |
| 634 | > | |
| 635 | </dd>`, | |
| 636 | row({ | |
| 637 | title: "Fork of", | |
| 638 | trail: html`<a href="/${p.ownerName}/${p.name}" | |
| 639 | >${p.ownerName}/${p.name}</a | |
| 640 | >`, | |
| 641 | }), | |
| 636 | 642 | )} |
| 637 | 643 | ${when( |
| 638 | 644 | upstream, |
| 639 | 645 | (up) => |
| 640 | html`<dt>Fork of</dt> | |
| 641 | <dd> | |
| 642 | ${upstreamHref === undefined | |
| 646 | row({ | |
| 647 | title: "Fork of", | |
| 648 | trail: | |
| 649 | upstreamHref === undefined | |
| 643 | 650 | ? html`<span class="mono">${up}</span>` |
| 644 | : html`<a href="${upstreamHref}">${up}</a>`} | |
| 645 | </dd>`, | |
| 651 | : html`<a href="${upstreamHref}">${up}</a>`, | |
| 652 | }), | |
| 646 | 653 | )} |
| 647 | </dl> | |
| 654 | </ul> | |
| 648 | 655 | ${whereItLives} |
| 649 | 656 | ${when( |
| 650 | 657 | repo.visibility === "public" && repo.origin === null, |
| @@ -679,8 +686,7 @@ function aboutCard( | ||
| 679 | 686 | >`, |
| 680 | 687 | )}`, |
| 681 | 688 | { head: html`<h2>About</h2>` }, |
| 682 | )} | |
| 683 | </aside>`; | |
| 689 | )}`; | |
| 684 | 690 | } |
| 685 | 691 | |
| 686 | 692 | // ---- pages ---- |
| @@ -763,7 +769,7 @@ export async function renderCode( | ||
| 763 | 769 | ? main |
| 764 | 770 | : html`<div class="repo-layout"> |
| 765 | 771 | <div>${main}</div> |
| 766 | ${o.rail} | |
| 772 | <aside aria-label="About">${o.rail}</aside> | |
| 767 | 773 | </div>`; |
| 768 | 774 | } |
| 769 | 775 |
src/web/style/content.ts +1 −4
| @@ -226,11 +226,8 @@ a.origin-chip:hover { border-color: var(--border-strong); color: var(--fg); } | ||
| 226 | 226 | .repo-layout { display: grid; gap: 16px; margin-top: 16px; align-items: start; } |
| 227 | 227 | @media (min-width: 960px) { |
| 228 | 228 | .repo-layout { grid-template-columns: minmax(0, 1fr) 280px; gap: 24px; } |
| 229 | .repo-layout > .about { position: sticky; top: calc(var(--topbar-h) + 16px); } | |
| 229 | .repo-layout > aside { position: sticky; top: calc(var(--topbar-h) + 16px); } | |
| 230 | 230 | } |
| 231 | .about dl { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 13px; margin: 0; } | |
| 232 | .about dt { color: var(--muted-fg); } | |
| 233 | .about dd { margin: 0; min-width: 0; overflow-wrap: anywhere; } | |
| 234 | 231 | |
outdated
Stale comment from the retired make-it-yours rule; it describes nothing here. Remove it together with |
||
| 235 | 232 | .issue-head { margin: 20px 0 14px; } |
outdated
|
||
| 236 | 233 | .issue-head h1 { font-size: clamp(20px, 1rem + 1.4vw, 26px); letter-spacing: -0.025em; } |
test/web-browse.test.ts +5
| @@ -260,6 +260,11 @@ describe("browsing an origin repository on the web", () => { | ||
| 260 | 260 | expect(html).toContain(new URL(fake.url).host); |
| 261 | 261 | expect(html).toContain("read-only"); |
| 262 | 262 | expect(html).toContain("Mirror it to"); |
| 263 | // The About rail is a §5 card, not the retired `.about` aside. | |
| 264 | expect(html).toContain("Where it lives"); | |
| 265 | expect(html).not.toMatch(/class="about"/); | |
| 266 | expect(html).toContain('<aside aria-label="About">'); | |
| 267 | expect(html).toMatch(/row-title"[^>]*>Visibility</); | |
| 263 | 268 | expect(html).toContain("keep it in sync"); |
| 264 | 269 | // And the two things it offers instead of the write buttons. |
| 265 | 270 | expect(html).toContain("Mirror into your workbench"); |
test/web-origins.test.ts +5
| @@ -364,6 +364,11 @@ describe("origins on the web", () => { | ||
| 364 | 364 | expect(html).toContain("Last fetch"); |
| 365 | 365 | expect(html).toContain("Sync now"); |
| 366 | 366 | expect(html).toContain("Pushes here go through to"); |
| 367 | expect(html).not.toMatch(/class="about"/); | |
| 368 | // The facts render as §5 rows inside a labelled complementary landmark. | |
| 369 | expect(html).toContain('<aside aria-label="About">'); | |
| 370 | expect(html).toMatch(/row-title"[^>]*>Visibility</); | |
| 371 | expect(html).not.toContain("<dl"); | |
| 367 | 372 | }); |
| 368 | 373 | |
| 369 | 374 | test("an imported issue shows the origin's author, not the local one", async () => { |
new above
offline
.kvre-enters §5d here after #3896 removed it. It should stay out.