Public: anyone can read this; the crew can change it.
The page head is a §5 component: `.page-head` moves from §5d into the table, with the function that draws it #4840
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/4832-the-page-head-is-a-5-component-page-head → main 7 commits fork
7 files
changed +43
−15
docs/design/areas/layout.md +1 −1
| @@ -10,4 +10,4 @@ The spacing and layout utilities every page leans on: one component, styled in ` | ||
| 10 | 10 | - `.grow` — a flex child that takes the free space and may shrink below its content (`min-width: 0`), so its text can truncate; `pages/edit.ts`, `pages/settings.ts` and `pages/board.ts` write it. Styled in `style/base.ts`; `style/components.ts` ellipsizes it inside `.menu-item` and `.palette-item`. |
| 11 | 11 | - `.narrow` — a 400px column centred a little below the top, for a single short form or notice; `pages/oauth.ts` writes it. Styled in `style/base.ts`. |
| 12 | 12 | - `.narrow-wide` — a 760px reading column for a page of one form or one text; `pages/edit.ts`, `pages/issues.ts` and `pages/forges.ts` write it. Styled in `style/base.ts`. |
| 13 | - `.page-head-sub` — the one line under a page head's title: `flex-basis: 100%` and a −4px top margin, so it breaks onto its own full-width line. A layout hook, not a look: it rides with `muted small` (or `bar muted small` for a row of facts); `pageHead({ sub })` in `chrome.ts` and the run, tell, change and browse heads write it. Styled in `style/content.ts`; a `.muted.small` child of a page head without it (the system page's Processes sort row) stays beside its heading. | |
| 13 | - `.page-head-sub` — the one line under a page head's title: `flex-basis: 100%` and a −4px top margin, so it breaks onto its own full-width line. A layout hook, not a look: it rides with `muted small` (or `bar muted small` for a row of facts); `pageHead({ sub })` in `chrome.ts` and the run, tell, change and browse heads write it. Styled in `style/components.ts`; a `.muted.small` child of a page head without it (the system page's Processes sort row) stays beside its heading. |
docs/design/sections/5-components.md +1
| @@ -38,6 +38,7 @@ specification. | ||
| 38 | 38 | | **tooltip** | `[data-tip]` | `data-tip-side=bottom` | hover/focus; hidden on coarse pointers | a status-bar hint | |
| 39 | 39 | | **toast / flash** `Flash` | `.toast` | `data-tone=success\|danger\|attn` | one per page load, cookie-driven | a one-line banner | |
| 40 | 40 | | **command palette** | `.palette` | — | JS only; degrades to `/search` | a fuzzy-find overlay | |
| 41 | | **page head** `pageHead()` (`chrome.ts`) | `.page-head` (`h1`, `.page-head-sub`; a `.spacer` before the actions) | `lead` before the title, `actions` right-aligned, `sub` on its own line below | wraps; the title's `min-width: 0` lets a long one shrink beside the actions | the title line, actions right, a dim subtitle under it | | |
| 41 | 42 | | **breadcrumb** `crumbs()` | `.crumbs` | `.mono` | `aria-current` on the last | `a / b / c` | |
| 42 | 43 | | **pagination** `pager()` | `.pager` | — | cursor links, absent ends | `‹ Newer Page 2 Older ›` | |
| 43 | 44 | | **timeline item** `tlEvent()` / `tlComment()` | `.tl-item` | `data-kind=comment\|event`, `data-tone` | — | a card, or one dim line with a glyph | |
docs/design/sections/5d-page-local-classes.md +1 −2
| @@ -27,8 +27,7 @@ the same line (#4888). | ||
| 27 | 27 | - `.menu-item` |
| 28 | 28 | - `.preview` |
| 29 | 29 | - `.swatch` |
| 30 | - **`style/content.ts`, page furniture** — what frames a page's content: page and repo heads, the front page and its tiles, explore, backups, boards, the fleet chart's series colours, lineage: | |
| 31 | - `.page-head` | |
| 30 | - **`style/content.ts`, page furniture** — what frames a page's content: repo heads, the front page and its tiles, explore, backups, boards, the fleet chart's series colours, lineage: | |
| 32 | 31 | - `.repo-actions` |
| 33 | 32 | - `.repo-desc` |
| 34 | 33 | - **`style/content.ts`, apps, runs, and the crew** — run pages, their logs and checks (a pull's look is §5's **look**). |
src/web/style/components.ts +8
| @@ -763,6 +763,14 @@ details[open] > summary.row > .row-lead { transform: rotate(90deg); } | ||
| 763 | 763 | [data-tip]:hover::after, [data-tip]:focus-visible::after { opacity: 1; } |
| 764 | 764 | @media (pointer: coarse) { [data-tip]::after { display: none; } } |
| 765 | 765 | |
| 766 | /* ---- page head: title, actions, subtitle ---- */ | |
| 767 | .page-head { | |
| 768 | display: flex; align-items: center; gap: 10px; flex-wrap: wrap; | |
| 769 | margin: 20px 0 14px; | |
| 770 | } | |
| 771 | .page-head h1 { min-width: 0; } | |
| 772 | .page-head > .page-head-sub { flex-basis: 100%; margin-top: -4px; } | |
| 773 | ||
| 766 | 774 | /* ---- breadcrumbs ---- */ |
| 767 | 775 | .crumbs { |
| 768 | 776 | display: flex; align-items: center; gap: 4px; min-width: 0; |
src/web/style/content.ts +1 −8
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | /** |
| 2 | 2 | * What pages put inside the components: rendered prose, a file's source, and |
| 3 | * the diff — plus the page furniture that frames them (page head, repo | |
| 3 | * the diff — plus the page furniture that frames them (repo | |
| 4 | 4 | * header, merge box, file tree). The diff is the product, so it gets the most |
| 5 | 5 | * attention here: hairline gutters, an opaque sticky file header, a `+`/`−` |
| 6 | 6 | * glyph that survives copy-paste, and an intra-line change signalled by two |
| @@ -11,13 +11,6 @@ export const content = /* css */ ` | ||
| 11 | 11 | @layer content { |
| 12 | 12 | |
| 13 | 13 | /* ---- page furniture ---- */ |
| 14 | .page-head { | |
| 15 | display: flex; align-items: center; gap: 10px; flex-wrap: wrap; | |
| 16 | margin: 20px 0 14px; | |
| 17 | } | |
| 18 | .page-head h1 { min-width: 0; } | |
| 19 | .page-head > .page-head-sub { flex-basis: 100%; margin-top: -4px; } | |
| 20 | ||
| 21 | 14 | .repo-head { margin: 18px 0 0; view-transition-name: repo-head; } |
| 22 | 15 | .repo-title { |
| 23 | 16 | display: flex; align-items: center; gap: 8px; flex-wrap: wrap; |
test/page-head-component.test.ts added +27
| @@ -0,0 +1,27 @@ | ||
| 1 | /** #4832: the page head is a §5 component — its rules live once, in the components layer. */ | |
| 2 | import { expect, test } from "bun:test"; | |
| 3 | import { readFileSync } from "node:fs"; | |
| 4 | import { pageHead } from "../src/web/chrome.ts"; | |
| 5 | import { css } from "../src/web/style.ts"; | |
| 6 | import { components } from "../src/web/style/components.ts"; | |
| 7 | import { content } from "../src/web/style/content.ts"; | |
| 8 | ||
| 9 | const rules = (sheet: string) => [...sheet.matchAll(/^\.page-head\b[^{]*\{/gm)].map((m) => m[0]); | |
| 10 | ||
| 11 | test("the served stylesheet defines .page-head once, in style/components.ts", () => { | |
| 12 | expect(rules(css)).toEqual([".page-head {", ".page-head h1 {", ".page-head > .page-head-sub {"]); | |
| 13 | expect(rules(components)).toEqual(rules(css)); | |
| 14 | expect(rules(content)).toEqual([]); | |
| 15 | }); | |
| 16 | ||
| 17 | test("§5's table names pageHead() as the page head", () => { | |
| 18 | const doc = readFileSync("docs/design/sections/5-components.md", "utf8"); | |
| 19 | expect(doc).toMatch(/^\| \*\*page head\*\* `pageHead\(\)` \(`chrome\.ts`\) +\| `\.page-head`/m); | |
| 20 | }); | |
| 21 | ||
| 22 | test("pageHead() draws the class and its parts", () => { | |
| 23 | const out = String(pageHead("Title", { sub: "Below" })); | |
| 24 | expect(out).toContain('<div class="page-head">'); | |
| 25 | expect(out).toContain("<h1>Title</h1>"); | |
| 26 | expect(out).toContain('<span class="page-head-sub muted small">Below</span>'); | |
| 27 | }); |
test/page-head-sub.test.ts +4 −4
| @@ -11,10 +11,10 @@ test("a page head's subtitle is the shared muted, small text, not a page-local c | ||
| 11 | 11 | }); |
| 12 | 12 | |
| 13 | 13 | test("only the subtitle is forced onto its own line: the stylesheet keys on .page-head-sub, not on shared utilities", async () => { |
| 14 | const { content } = await import("../src/web/style/content.ts"); | |
| 15 | expect(content).toContain(".page-head > .page-head-sub {"); | |
| 16 | expect(content).not.toContain(".page-head > .muted.small"); | |
| 17 | expect(content).not.toContain(".page-head > .bar"); | |
| 14 | const { components } = await import("../src/web/style/components.ts"); | |
| 15 | expect(components).toContain(".page-head > .page-head-sub {"); | |
| 16 | expect(components).not.toContain(".page-head > .muted.small"); | |
| 17 | expect(components).not.toContain(".page-head > .bar"); | |
| 18 | 18 | // The system page's sort row is a plain muted small child, beside its heading. |
| 19 | 19 | const src = await Bun.file(new URL("../src/web/pages/system.ts", import.meta.url)).text(); |
| 20 | 20 | expect(src).not.toContain("page-head-sub"); |
new above
offline