Lane 0, third PR: the design system's shell — app bar, Contents, footer, live bar, banners, account menu, bell, search (#364) #741

Merged
claude-bot merged 1 commit from build/lane0-shell into main 2026-09-22 03:02:16 +00:00
Collaborator

Lane 0's third and last PR: the shell. Thirty-five components and 160 states on /design now, each with a test (1,127 in all). Nothing in the app uses the library yet; lane 1 (#727) puts the first pages inside this frame.

The frame: AppShell (the .app root, and the container every phone rule queries) and Book (the rail slot and the one <main>); AppBar — one markup, and @container app (max-width: 640px) makes it the phone bar: the wordmark's tails hide to leave "QB", the breadcrumb gives way to the page title and its campaign, the identity disc goes and the Contents button arrives, and a bar carrying a search box drops the wordmark and the title with it (SH-1, settled 15 September). There is no phone prop anywhere in the library, and the README now says so as a rule. Contents is the desktop rail; ContentsSheet is the phone's bottom sheet with the You group at its foot (the person, their entries, the theme as three choices, Sign out last) — Escape closes it, focus goes in on open and back on close, and a sheet rendered already open never takes focus. Footer (the version first, tabular; hidden in Table mode by the stylesheet). LiveBar in SH-6's three states, each in its own words, the pulsing dot only where something is being captured and still under prefers-reduced-motion. Banners: SH-8's stack, one at a time, worst first (live > warn > gm > quiet, or an item's own rank), the rest named behind a fold rather than counted.

The three popovers: AccountMenu (the disc opens a Menu: a person's name and pronouns, never a character's; Profile, Plan & billing, Help; the theme strip; Sign out). Bell — a role="dialog" panel, not a menu, because its head and foot have controls of their own (Mark all read; Notification settings) and Tab has to reach them; unread is a dot and heavier text and the word; each row can be marked read on its own (IS-160); empty, loading and did-not-load states. Search (SH-9): grouped by kind in the kit's order with the query marked where it fell (CA-096), "Spoken" and "GM only" chips on a row, three of a group and "All N in Sessions", CA-095's second letter, "Search is not answering", D16's transcript switch in the foot for the GM alone. No combobox, on purpose: the rows are places to go and the box keeps what you typed, so it is a search box that says it has a dialog, and a live region says "3 results" in words because a text box may not carry aria-expanded.

Decisions for the record, from the review of the delegates' work against the kit:

  • The crumb bolds the campaign, not the page (lane 1's CRUMB): you are in the campaign, and the page is where in it. The last item still carries aria-current="page".
  • The wordmark on the chrome is the look's pair (--chrome-accent then --chrome-ink), not the kit's base rule (--accent on the chrome would be two dark blues).
  • Escape for the sheet and the bell is a document listener: a role="dialog" is not an interactive element, so a key handler on it is what jsx-a11y stops, and the key has to work while focus is on the scrim anyway.
  • The theme strip inside the account menu keeps the arrow keys to itself; Menu's own Down would otherwise pull focus off it one press after arriving.
  • The render check caught a bug from the first PR: a link drawn as a primary button (Button href) had its label in the page's link colour on the accent, invisible, because .qb a outweighs .btn--primary. Button.css now restates the link forms' colours at a link's specificity.
  • /design grew ?only=<slug> (one component at a time, for the headless render check) and a full-width state (width: "full") so a desktop frame gets the whole row and the container query draws its desktop form.

Lint clean under jsx-a11y at error and the React Compiler rules; render-checked at /design in both themes with headless Edge.

Closes nothing yet: #364 stays open until lane 1 has used the shell in anger and the sheet pieces for lane 11 are added.

🤖 Generated with Claude Code

Lane 0's third and last PR: the shell. Thirty-five components and 160 states on `/design` now, each with a test (1,127 in all). Nothing in the app uses the library yet; lane 1 (#727) puts the first pages inside this frame. **The frame**: `AppShell` (the `.app` root, and the container every phone rule queries) and `Book` (the rail slot and the one `<main>`); `AppBar` — one markup, and `@container app (max-width: 640px)` makes it the phone bar: the wordmark's tails hide to leave "QB", the breadcrumb gives way to the page title and its campaign, the identity disc goes and the Contents button arrives, and a bar carrying a search box drops the wordmark and the title with it (SH-1, settled 15 September). There is no `phone` prop anywhere in the library, and the README now says so as a rule. `Contents` is the desktop rail; `ContentsSheet` is the phone's bottom sheet with the You group at its foot (the person, their entries, the theme as three choices, Sign out last) — Escape closes it, focus goes in on open and back on close, and a sheet rendered already open never takes focus. `Footer` (the version first, tabular; hidden in Table mode by the stylesheet). `LiveBar` in SH-6's three states, each in its own words, the pulsing dot only where something is being captured and still under `prefers-reduced-motion`. `Banners`: SH-8's stack, one at a time, worst first (`live` > `warn` > `gm` > `quiet`, or an item's own `rank`), the rest **named** behind a fold rather than counted. **The three popovers**: `AccountMenu` (the disc opens a `Menu`: a person's name and pronouns, never a character's; Profile, Plan & billing, Help; the theme strip; Sign out). `Bell` — a `role="dialog"` panel, not a menu, because its head and foot have controls of their own (Mark all read; Notification settings) and Tab has to reach them; unread is a dot *and* heavier text *and* the word; each row can be marked read on its own (IS-160); empty, loading and did-not-load states. `Search` (SH-9): grouped by kind in the kit's order with the query marked where it fell (CA-096), "Spoken" and "GM only" chips on a row, three of a group and "All N in Sessions", CA-095's second letter, "Search is not answering", D16's transcript switch in the foot for the GM alone. No combobox, on purpose: the rows are places to go and the box keeps what you typed, so it is a search box that says it has a dialog, and a live region says "3 results" in words because a text box may not carry `aria-expanded`. **Decisions for the record**, from the review of the delegates' work against the kit: - The crumb bolds the campaign, not the page (lane 1's `CRUMB`): you are in the campaign, and the page is where in it. The last item still carries `aria-current="page"`. - The wordmark on the chrome is the look's pair (`--chrome-accent` then `--chrome-ink`), not the kit's base rule (`--accent` on the chrome would be two dark blues). - Escape for the sheet and the bell is a `document` listener: a `role="dialog"` is not an interactive element, so a key handler on it is what jsx-a11y stops, and the key has to work while focus is on the scrim anyway. - The theme strip inside the account menu keeps the arrow keys to itself; Menu's own Down would otherwise pull focus off it one press after arriving. - The render check caught a bug from the first PR: a link drawn as a primary button (`Button href`) had its label in the page's link colour on the accent, invisible, because `.qb a` outweighs `.btn--primary`. `Button.css` now restates the link forms' colours at a link's specificity. - `/design` grew `?only=<slug>` (one component at a time, for the headless render check) and a full-width state (`width: "full"`) so a desktop frame gets the whole row and the container query draws its desktop form. Lint clean under jsx-a11y at error and the React Compiler rules; render-checked at `/design` in both themes with headless Edge. Closes nothing yet: #364 stays open until lane 1 has used the shell in anger and the sheet pieces for lane 11 are added. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(frontend): the design system's shell — app bar, Contents, footer, live bar, banners, account menu, bell, search (#364)
All checks were successful
CI / Backend lint (ruff) (pull_request) Successful in 1m15s
CI / Feedback site tests (pull_request) Successful in 1m38s
CI / Bot/backend version sync (pull_request) Successful in 36s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 2m4s
CI / Docker image build (pull_request) Successful in 34s
CI / Bot tests and audit (pull_request) Successful in 3m15s
CI / Frontend tests, audit, and build (pull_request) Successful in 3m27s
CI / Backend migration, tests, and audit (pull_request) Successful in 12m27s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 13m19s
cb749f7781
Lane 0's third PR. AppShell and Book (the frame; the Book owns the
<main>), AppBar (one markup: the kit's container query on the .app
root makes it the phone bar, so there is no phone prop), Contents (the
rail) and ContentsSheet (the phone's sheet with the You group), Footer,
LiveBar (SH-6's three states), Banners (SH-8: one at a time, worst
first, the rest named behind a fold), AccountMenu, Bell (a role=dialog
panel: its head and foot have controls of their own) and Search (SH-9:
grouped results with the query marked, Spoken and GM-only chips, three
of a group and "All N in Sessions", CA-095's second letter, D16's
transcript switch).

The render check caught a link drawn as a primary button losing its
label to the page's link colour (.qb a outweighs .btn--primary);
Button.css restates the link forms' colours. /design takes
?only=<component> and a full-width state for the desktop frames.

Thirty-five components, 160 states, 1,127 tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign in to join this conversation.
No description provided.