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

Design: the where-am-I trail uses the design system, and where-dot/where-note/where-sep leave §5d #3890

Merged opened this

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

Merged

mason/forge:mason/3875-design-the-where-am-i-trail-uses-the-des → main 3 commits fork 1 thread · 1 unresolved

  • Merged. mason/3875-design-the-where-am-i-trail-uses-the-des was merged into main as 67682e0.

Review

Sign in to review this change.
7 files changed +37 −16
docs/DESIGN.md +2 −2
@@ -178,7 +178,7 @@ specification.
178178 | **prose** | `.prose` | — | 15px, 72ch measure | wrapped markdown |
179179 | **settings shell** `settingsLayout()` | `.settings` (`.settings-nav`, `.settings-main`) | — | sticky nav ≥860px, scroll strip below | a sidebar and a pane |
180180 | **repo header** `repoHeader()` | `.repo-head` | — | tab counts, Settings only for admins | a title line and a tab row |
181 | **where strip** `whereStrip()` (`scope.ts`) | `.where > .where-place` | `data-state=reading\|present\|absent` — accent dot, success dot, faint dot | three places, always in this order and these words; a place is a link where one exists, and the third is the pin toggle | `● codeberg.org · ● workbench · ◆ this machine` |
181 | **where strip** `whereStrip()` (`scope.ts`) | `.where > .where-place` | `data-state=reading\|present\|absent` — accent dot, success dot, faint dot — the dot is the place's `aria-hidden` glyph span, its aside `.muted` | three places, always in this order and these words; a place is a link where one exists, and the third is the pin toggle | `● codeberg.org · ● workbench · ◆ this machine` |
182182 | **live-browse banner** `liveBanner()` | `.live-note` | — | only on a live browse, above the content, one sentence | one accent line under the tab row |
183183 | **where-it-lives block** | `.where-block` | — | origin, last fetch, Sync now and where pushes land, as one block in the About rail | the same four lines under a rule |
184184 | **mirror stages** `stageList()` | `.stages > li` | `data-state=done\|now\|failed\|todo` | the stage order is the markup order; a script updates it in place and goes to the mirror on done; without JavaScript the same markup renders and a `<noscript>` Reload link re-reads it. Nothing here refreshes itself: no `<meta refresh>`, no timer | a checklist with `●`/`◐`/`○` |
@@ -392,7 +392,7 @@ gone, so this list only shrinks.
392392 - **`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`
393393 - **`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`
394394 - **`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`
395 - **`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` `.hero` `.hero-terminal` `.issue-head` `.issue-meta` `.kv` `.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` `.quiet-details` `.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-dot` `.where-note` `.where-sep`
395 - **`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` `.hero` `.hero-terminal` `.issue-head` `.issue-meta` `.kv` `.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` `.quiet-details` `.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`
396396 - **`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`
397397 - **`style/content.ts`, prose (markdown)** — table cell alignment, built by `markdown.ts` as `ta-${align}`: `.ta-c` `.ta-r`
398398 - **`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/origins.ts +2 −2
@@ -1522,7 +1522,7 @@ export async function originReposPage(ctx: Ctx): Promise<Response> {
15221522 mirror,
15231523 (m) =>
15241524 html`<span class="where-place" data-state="present"
1525 ><span class="where-dot" aria-hidden="true">●</span
1525 ><span aria-hidden="true">●</span
15261526 >mirrored as
15271527 <a href="/${m.ownerName}/${m.name}"
15281528 >${m.ownerName}/${m.name}</a
@@ -1535,7 +1535,7 @@ export async function originReposPage(ctx: Ctx): Promise<Response> {
15351535 class="where-place"
15361536 data-state="present"
15371537 title="Kept on your machine"
1538 ><span class="where-dot" aria-hidden="true">◆</span
1538 ><span aria-hidden="true">◆</span
15391539 >this machine</span
15401540 >`,
15411541 )}
src/web/pages/search.ts +1 −1
@@ -149,7 +149,7 @@ function repoHitRow(ctx: Ctx, apps: Apps, hit: RepoHit): Html {
149149 meta: html`<span
150150 class="where-place"
151151 data-state="${mirror === null ? "absent" : "present"}"
152 ><span class="where-dot" aria-hidden="true"
152 ><span aria-hidden="true"
153153 >${mirror === null ? "○" : "●"}</span
154154 >${mirror === null
155155 ? "not mirrored here"
src/web/pages/site.ts +3 −3
@@ -117,16 +117,16 @@ export function downloadsCard(ctx: Ctx, user: User): Html {
117117 at,
118118 (o) =>
119119 html`<span class="where-place" data-state="present"
120 ><span class="where-dot" aria-hidden="true">●</span
120 ><span aria-hidden="true">●</span
121121 >${hostOf(o.htmlUrl) || o.fullName}</span
122122 ><span class="where-sep" aria-hidden="true">·</span>`,
123123 )}
124124 <span class="where-place" data-state="present"
125 ><span class="where-dot" aria-hidden="true">●</span
125 ><span aria-hidden="true">●</span
126126 >workbench</span
127127 ><span class="where-sep" aria-hidden="true">·</span
128128 ><span class="where-place" data-state="present"
129 ><span class="where-dot" aria-hidden="true">◆</span>this
129 ><span aria-hidden="true">◆</span>this
130130 machine</span
131131 >
132132 </span>`,
src/web/scope.ts +2 −2
@@ -426,12 +426,12 @@ function placesFor(ctx: Ctx, scope: RepoScope): Place[] {
426426 }
427427
428428 function placeNode(place: Place): Html {
429 const inner = html`<span class="where-dot" aria-hidden="true"
429 const inner = html`<span aria-hidden="true"
430430 >${place.glyph}</span
431431 >${place.icon ? icon(place.icon) : null}<span class="where-name"
432432 >${place.label}</span
433433 >${place.note
434 ? html`<span class="where-note">(${place.note})</span>`
434 ? html`<span class="muted">(${place.note})</span>`
435435 : null}`;
436436 const attrs = html`class="where-place" data-state="${place.state}"
437437 ${place.title ? html`title="${place.title}"` : null}`;
src/web/style/content.ts +7 −6
@@ -69,22 +69,23 @@ a.where-place:hover, button.where-place:hover {
6969 .where-place .ico { color: var(--faint); flex: none; }
7070 /* A thumb needs more than a 12px word to land on. */
7171 @media (pointer: coarse) { .where-place { min-height: 32px; padding: 4px 8px; } }
72 .where-dot { font-size: var(--fs-min); line-height: 1; }
73 .where-note { color: var(--faint); }
72 /* The dot is the place's aria-hidden span: ● ○ ◆ are decoration, and the
73 state and the word beside them already say what they mean. */
74 .where-place > span[aria-hidden="true"] { font-size: var(--fs-min); line-height: 1; }

This selector ties the colour to the markup shape (any aria-hidden span directly under .where-place). Give the glyph a named hook or reuse the §5 dot() so the rule is not implicit.

This structural selector is fragile: any future aria-hidden span directly under .where-place takes the dot size and colour. Prefer a named hook, or reuse §5 dot().

7475 /* Accent is interaction everywhere else in the product; here it is the one
7576 place a page is *reading from*, which is the page's own subject. */
7677 .where-place[data-state="reading"] { color: var(--fg); font-weight: 560; }
77 .where-place[data-state="reading"] .where-dot { color: var(--accent); }
78 .where-place[data-state="present"] .where-dot { color: var(--success); }
78 .where-place[data-state="reading"] > span[aria-hidden="true"] { color: var(--accent); }
79 .where-place[data-state="present"] > span[aria-hidden="true"] { color: var(--success); }
7980 .where-place[data-state="absent"] { color: var(--faint); }
80 .where-place[data-state="absent"] .where-dot { color: var(--faint); }
81 .where-place[data-state="absent"] > span[aria-hidden="true"] { color: var(--faint); }
8182 .where-pin { margin-left: auto; }
8283 /* Inside a list row the strip is text, not a layout: the row promises one
8384 honest line with an ellipsis, and only inline children can keep it. */
8485 .row-meta .where { display: inline; }
8586 .row-meta .where-place { display: inline; padding: 0; }
8687 .row-meta .where-place .ico { display: none; }
87 .row-meta .where-dot { margin-right: 3px; }
88 .row-meta .where-place > span[aria-hidden="true"] { margin-right: 3px; }
8889 .row-meta .where-sep { margin: 0 3px; }
8990 .row-title a { color: inherit; text-decoration: none; }
9091 .row-title a:hover { color: var(--accent); }
test/web-browse.test.ts +20
@@ -544,6 +544,26 @@ describe("browsing an origin repository on the web", () => {
544544 expect(strip).toContain("Stop keeping this repository on my machine");
545545 });
546546
547 test("every where trail is drawn from §5's where strip, not where-dot or where-note", async () => {
548 // The repository header, the origin picker's mirrored row, the home
549 // page's "On your machine" rail and search: each draws a trail.
550 const pages = {
551 repo: await bodyOf(alice, "/alice/widgets"),
552 picker: await bodyOf(alice, `/settings/origins/${originId}/repos`),
553 home: await bodyOf(alice, "/"),
554 search: await bodyOf(alice, "/search?q=widgets"),
555 };
556 const strip = stripOf(pages.repo);
557 expect(strip).toMatch(/<span aria-hidden="true"\s*>◆<\/span/);
558 expect(strip).toMatch(/<span class="muted">\(synced [^)]+\)<\/span>/);
559 expect(pages.picker).toContain("mirrored as");
560 expect(pages.home).toContain("On your machine");
561 for (const [name, html] of Object.entries(pages)) {
562 expect(`${name}:${html.includes("where-place")}`).toBe(`${name}:true`);
563 expect(`${name}:${/where-dot|where-note/.test(html)}`).toBe(`${name}:false`);
564 }
565 });
566
547567 test("starting a mirror that is already running joins it", async () => {
548568 // The repository is mirrored, so a second start is refused rather than
549569 // racing — but it must land somewhere that says so, not on a 500.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs