Public: anyone can read this; the crew can change it.
A page head's subtitle draws base.ts's muted, small text, and `.sub` leaves the stylesheet #4788
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/4749-a-page-head-s-subtitle-draws-base-ts-s-m → main 6 commits fork 3 threads · 3 unresolved
docs/DESIGN.md −1
| @@ -450,7 +450,6 @@ the same line (#4888). | ||
| 450 | 450 | - `.repo-lineage` |
| 451 | 451 | - `.repo-title` |
| 452 | 452 | - `.slash` |
| 453 | - `.sub` | |
| 454 | 453 | - `.tok` |
| 455 | 454 | - `.where-sep` |
| 456 | 455 | - **`style/content.ts`, code view** — the source view and the syntax-highlight token classes `highlight-html.ts` builds as `tok-${kind}`: |
docs/design/areas/layout.md +1
| @@ -10,3 +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. |
src/web/chrome.ts +1 −1
| @@ -455,7 +455,7 @@ export function pageHead( | ||
| 455 | 455 | ${o.lead ?? null} |
| 456 | 456 | <h1>${title}</h1> |
| 457 | 457 | ${o.actions ? html`<span class="spacer"></span>${o.actions}` : null} |
| 458 | ${o.sub ? html`<span class="sub">${o.sub}</span>` : null} | |
| 458 | ${o.sub ? html`<span class="page-head-sub muted small">${o.sub}</span>` : null} | |
| 459 | 459 | </div>`; |
| 460 | 460 | } |
| 461 | 461 |
src/web/pages/browse.ts +1 −1
| @@ -715,7 +715,7 @@ async function browseThread( | ||
| 715 | 715 | ${detail.issue.title} |
| 716 | 716 | <span class="number">#${detail.issue.number}</span> |
| 717 | 717 | </h1> |
| 718 | <div class="sub bar"> | |
| 718 | <div class="page-head-sub bar muted small"> | |
| 719 | 719 | ${stateBadge(state)} |
| 720 | 720 | <span |
| 721 | 721 | >${detail.issue.originAuthor ?? detail.issue.authorName} opened this |
src/web/studio/change.ts +1 −1
| @@ -194,7 +194,7 @@ export function changeHead(ctx: Ctx, repo: Repo, pull: Pull): Html { | ||
| 194 | 194 | const marks = marksByName(ctx.app.db, [pull.authorName, who ?? ""]); |
| 195 | 195 | return html`<div class="page-head"> |
| 196 | 196 | <h1>${pull.title} <span class="number">#${pull.number}</span></h1> |
| 197 | <div class="sub bar"> | |
| 197 | <div class="page-head-sub bar muted small"> | |
| 198 | 198 | ${stateBadge( |
| 199 | 199 | pullState(pull), |
| 200 | 200 | getIssueById(ctx.app.db, pull.id)?.closeReason === "parked", |
src/web/studio/run.ts +1 −1
| @@ -152,7 +152,7 @@ export async function runView( | ||
| 152 | 152 | : null} |
| 153 | 153 | > |
| 154 | 154 | <h1>${title}</h1> |
| 155 | <div class="sub bar"> | |
| 155 | <div class="page-head-sub bar muted small"> | |
| 156 | 156 | ${wordBadge(word, "run-word")} |
| 157 | 157 | ${when( |
| 158 | 158 | issue, |
src/web/studio/tell.ts +1 −1
| @@ -364,7 +364,7 @@ async function studioTell( | ||
| 364 | 364 | const marks = marksByName(db, [issue.authorName, issue.assigneeName ?? ""]); |
| 365 | 365 | const head = html`<div class="page-head"> |
| 366 | 366 | <h1>${issue.title} <span class="number">#${issue.number}</span></h1> |
| 367 | <div class="sub bar"> | |
| 367 | <div class="page-head-sub bar muted small"> | |
The bar is now muted small, so the inner span.muted.small a few lines down is redundant; drop its classes. |
||
| 368 | 368 | ${direction |
| 369 | 369 | ? html`<span class="muted small" |
| 370 | 370 | >${icon("map")} the repository's standing direction — always |
src/web/style/content.ts +1 −1
| @@ -16,7 +16,7 @@ export const content = /* css */ ` | ||
| 16 | 16 | margin: 20px 0 14px; |
| 17 | 17 | } |
| 18 | 18 | .page-head h1 { min-width: 0; } |
| 19 | .page-head .sub { flex-basis: 100%; color: var(--muted-fg); font-size: 13px; margin-top: -4px; } | |
| 19 | .page-head > .page-head-sub { flex-basis: 100%; margin-top: -4px; } | |
outdated
This rule carries only flex-basis and margin, so the bar rows lose the muted colour and 13px size
outdated
|
||
| 20 | 20 | |
| 21 | 21 | .repo-head { margin: 18px 0 0; view-transition-name: repo-head; } |
| 22 | 22 | .repo-title { |
test/page-head-sub.test.ts added +21
| @@ -0,0 +1,21 @@ | ||
| 1 | import { expect, test } from "bun:test"; | |
| 2 | import { pageHead } from "../src/web/chrome.ts"; | |
| 3 | import { html } from "../src/web/html.ts"; | |
| 4 | ||
| 5 | test("a page head's subtitle is the shared muted, small text, not a page-local class", () => { | |
| 6 | const out = String(pageHead("Title", { sub: "under the title" })); | |
| 7 | expect(out).toContain('<span class="page-head-sub muted small">under the title</span>'); | |
| 8 | expect(out).not.toContain('class="sub'); | |
| 9 | expect(String(pageHead("Bare"))).not.toContain("muted small"); | |
| 10 | expect(String(html`${pageHead("T", { sub: "<b>x</b>" })}`)).toContain("<b>"); | |
| 11 | }); | |
| 12 | ||
| 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"); | |
| 18 | // The system page's sort row is a plain muted small child, beside its heading. | |
| 19 | const src = await Bun.file(new URL("../src/web/pages/system.ts", import.meta.url)).text(); | |
| 20 | expect(src).not.toContain("page-head-sub"); | |
| 21 | }); |
test/studio-changes.test.ts +1
| @@ -700,6 +700,7 @@ describe("the studio, continued", () => { | ||
| 700 | 700 | expect(changeHtml).toContain(`href="/alice/notes/pulls/${p}/files"`); |
| 701 | 701 | expect(changeHtml).toContain('id="comment-body"'); |
| 702 | 702 | expect(changeHtml).toContain('class="page-head"'); |
| 703 | expect(changeHtml).toContain('class="page-head-sub bar muted small"'); | |
| 703 | 704 | expect(changeHtml).not.toContain("issue-head"); |
| 704 | 705 | expect(changeHtml).not.toContain("issue-meta"); |
| 705 | 706 | // The tabs are §5's bar, not the retired .filters wrapper. |
test/studio-runs.test.ts +2 −1
| @@ -508,7 +508,8 @@ describe("working: a run's own page in the studio", () => { | ||
| 508 | 508 | expect(page).not.toContain("issue-head"); |
| 509 | 509 | expect(page).not.toContain("issue-meta"); |
| 510 | 510 | expect(page).toContain('class="page-head"'); |
| 511 | expect(page).toContain('class="sub bar"'); | |
| 511 | expect(page).toContain('class="page-head-sub bar muted small"'); | |
| 512 | expect(page).not.toContain('class="sub'); | |
| 512 | 513 | } |
| 513 | 514 | }); |
| 514 | 515 |
test/web-browse.test.ts +1
| @@ -472,6 +472,7 @@ describe("browsing an origin repository on the web", () => { | ||
| 472 | 472 | expect(pulls).toContain("Tighten the bolt"); |
| 473 | 473 | const pull = await bodyOf(alice, `${browse}/pulls/9`); |
| 474 | 474 | expect(pull).toContain("tighten"); |
| 475 | expect(pull).toContain('class="page-head-sub bar muted small"'); | |
| 475 | 476 | // A live pull is merged where it lives, and the box says where. |
| 476 | 477 | expect(pull).toContain("Merge on"); |
| 477 | 478 | }); |
Emitting the bare shared utility means the layout rule has to key on utility classes, which is what caused the leak in content.ts. Emit
<span class="bar muted small">(or a div) here so the full-width rule can be.page-head > .baralone.