Sidebar, topbar, breadcrumbs, page title, activity rail, and the shell contract that binds them. Evidence: Speedway shell inventory (docs/evidence/20260807-0230-speedway-design-language/shell-nav-styles-inventory.md). Kit sources under packages/ui/src/app-shell and packages/ui/src/page-title.
Audience: an agent (or person) touching the shell, nav, or page identity.
§A Principles and mental models#
1. Two shell layers, and pages never draw chrome.
The outer layer (Speedway root.tsx:402-544) owns the topbar, global providers, toasts, and the nav progress bar. It renders once for every route. The inner layer (routes/shell.tsx:33-107) owns workspace chrome: sidebar, breadcrumbs, content outlet, activity rail, peek outlet, file-preview host. A page participates by exporting a ShellHandle (nav tab, layout mode, workspace view), read off the deepest matched route. The mental model: chrome is infrastructure a route declares into, never markup a page repeats.
2. One source of truth per page identity.
PAGE_META maps each page to its icon and label. The sidebar, the page title, and any tooltip naming a page all read the same entry, "so a page's icon/label can never drift from its sidebar entry" (page-meta.ts:5-8). When adding a page, the first edit is the meta map, not the nav.
3. Active state is explicit, never inferred by prefix.
Kit Sidebar accepts an active boolean per item. Speedway always supplies it, computed from a typed Tab union plus query params for sub-tabs. The lesson is recorded: NavLink prefix matching double-stamped aria-current on Home from every sub-page (run ledger SHELL-01, WorkspaceNav.tsx:85-88). Prefix matching is banned by scar tissue.
4. Pending feedback lives on the thing you clicked.
Every sidebar link wraps in RailLink: clicking an already-active destination revalidates instead of re-navigating. A pending spinner appears only after 120ms so instant transitions never flash it (WorkspaceNav.tsx:78-119). The topbar sweep bar (90ms delay, trickle to 88%, snap and fade) says "working" globally without dimming content (doc 00, model 4).
5. The right rail is a claimed slot.
Exactly one drawer occupies the right-hand outlet at a time (SidebarSlotContext, nav-collapse.tsx:59-92). A product peek claims it and Activity yields. Releasing returns Activity if the bell had left it open. Activity is closed by default, owner call: "the open panel squeezes the dashboard" (nav-collapse.tsx:36-38).
6. Identity sits at the sidebar foot. Sign-out is in the topbar.
Owner ruling 2026-07-17: no Organization nav group. The account destination is the identity row at the sidebar's foot (initials disc, name, email, straight link, no dropdown), and sign-out lives in the topbar. The foot row sits flush: "no padding around the component; its own inner padding stays" (app.css:1614-1618).
7. Breadcrumbs earn their row.
ShellCrumbs derives the trail from URL segments with a label map, resolves dynamic segments to loaded doc names (job name, org name) with a skeleton while pending, hides the entire workspace concept (doc-29), and never renders a single-crumb trail: "the trail earns its row only two or more levels deep" (owner 2026-07-29, ShellCrumbs.tsx:143-145). Separators stay quiet. Every segment navigates.
8. Staff gating is dual: the button and the gate agree.
Three booleans from the root loader: staff (effective, honors view-as-non-staff preview), actuallyStaff (real), viewingAsNonStaff. UI hides staff controls via staff. Routes independently 404 via requireStaff server-side, "the button and the gate agree" (root.tsx:325-326). The view-as toggle stays visible to actuallyStaff so the staffer can switch back.
9. Theme is a reader's choice, not the OS's.
Pre-paint inline script stamps data-theme before hydration (no flash). OS preference is deliberately ignored: "This app opens light for everyone; dark is a choice a reader makes here, and then it sticks" (owner 2026-07-17, theme.tsx:21-32). The toggle is currently staff-only (owner 2026-08-03). Themed assets swap by CSS visibility, never by themed img src (flash risk, app.css:185-197).
10. A long nav column shows its hierarchy by weight, indent and a rail, never by a lighter kicker; the treatment is opt-in.
Sidebar/AppShell take navGrouping: flat (the default, every app's look today: kicker and rows on one left edge) or railed, for a column with many groups. Railed: the group title is the house kicker idiom (doc 01 §B, doc 12 §A4) at its quiet weight, 500 rather than 600, in the /65 ink that is the ladder's floor for small text (doc 12 §A9); its rows sit one step in behind a hairline rail (border-base-300/70, kit Sidebar) in the resting /75 ink, full weight when active; a sub-group (NavGroup.groups, app-shell.md) repeats the step once more in. Never make a kicker lighter than /65 to push it back (AA at 11px, the owner's 2026-08-17 ruling); a kicker that still reads louder than its rows is a weight problem, not an ink problem. Owner, doc 70 E1 and the follow-up, 2026-08-19: the playground's docs and patterns columns opt in; the default stays flat so no app's rail changes on a bump unless it asks.
§B Presets#
- Shell skeleton: topbar (56px sticky, z-20) → 4-column grid
auto minmax(0,1fr) auto auto(sidebar, main, rail, peek outlet). Trailingautotracks collapse for free when panels hide (app.css:221-228,1057-1058). - Sidebar: kit
SidebarfedNavGroup[]. Icon-width rail belowlg(1024px), manual collapse persisted (sw:rail-collapsed), hamburger hosted in the topbar viahideToggle. Active stylingbg-primary/10 text-primary font-medium. Restingtext-base-content/75with hover.navGrouping="railed"for a long column (quiet kicker, rows one step in behind a hairline rail, §A10); defaultflat. Groups collapse, the active one opens on arrival and may still be closed; one level of sub-groups with their own kicker (NavGroup.groups,defaultCollapsed,Icon). - Nav badges: plain right-aligned numeric spans. An
undefinedcount renders no badge at all: "a skeleton here promised a number some pages never deliver" (WorkspaceNav.tsx:175-176). - PageTitle (kit):
Icon/avatar,title,subtitle,titleExtra,actions,back(own row above, kit adds the arrow), slot-awareskeletonvariant that draws bars only for slots in use. Only string titles auto-style, and "a JSX title must dress itself" (two documented callsites). - Breadcrumb recipe: segment label map + no-page plain-text list + hidden segments + doc-name resolution + per-page tab crumbs + hidden-trail predicate + the two-or-more rule.
- Toasts: mounted once globally (
bottom-right), pushed via kitpushAlert. Modals are NOT centrally mounted. Each callsite renders kitModal/useModallocally. SingleFilePreviewHostper shell (a second host would double-render). - The translucent bar (opt-in): a topbar that opens fully transparent (
border-transparent bg-transparent) and only picks upbg-base-100/80 border-base-300 backdrop-bluronce content has actually scrolled beneath it, withtransition-colors duration-200so the swap reads as a fade, not a snap. It exists for a bar that isfixedorstickyover a scrolling surface from first paint, so toggling the background never shifts layout. Opt in over a hero, artwork, or another surface with its own visual weight, where full opacity would read as heavy chrome sitting on top of it. Do not opt in over dense content, a table, a form, a dashboard, where blur costs legibility exactly where the reader needs it, and never on the kitTopbarasAppShellmounts it (seeapp-shell.md: that row never sits over scrolled content in the first place). Unaffected byprefers-reduced-motion:11-motion.md§A8 stills keyframe entrances, and this is a plain colour transition, nothing moves. Shipped case in §C below. The floating-bottom-bar version of the same recipe, for a modal or wizard footer, is10-overlays.md§A14.
§C Use cases through the apps#
| Surface | Shell usage | Evidence |
|---|---|---|
| Sidebar groups | Catalog (Home, Jobs+badge, Manual Review+badge) · Data (Audit Log; Files/Export deprecated in place) · Modules (5 entries via ?m=) | WorkspaceNav.tsx:214-315 |
| Removed nav | Schemas section removed 2026-08-03, routes URL-reachable | WorkspaceNav.tsx:313-314 |
| Topbar right cluster | ViewAsNonStaff → StaffAdmin → Activity bell (unread badge) → ThemeToggle (staff) → SignOut | root.tsx:513-531 |
| Brand | wordmark pair links to current workspace dashboard; click-in-place revalidates | root.tsx:460-491 |
| Activity rail | PeekPanel passive, 320px, width persisted, polls 3s while working and visible; read-only variant on dashboard card (owner: per-row links "too busy") | ActivityRail.tsx, workspace.tsx:549-551 |
| PageTitle callsites | Dashboard "Welcome back, {name}" (only if returning) · Jobs string + actions menu · Job/Run JSX InlineEdit titles · Review icon + explainer · Parts/Team with back links · Accounts skeleton reserves the back row's height with an empty span | inventory §2.2 table |
| Crumb behaviors | Jobs > {job name}; workspace segment invisible; parts/runs show raw ids; accounts appends active tab; dashboard hides trail | ShellCrumbs.tsx:87-127 |
| Workspace switching | hidden end-to-end (doc-29): / and /workspaces/select redirect to the default workspace; full selector UI preserved commented-out with revival notes | _index.tsx:4-25, workspaces/select.tsx:4-28 |
| Staff gating | /admin/* self-gate with requireStaff 404; view-as posts to /view-as cookie and redirects so every loader recomputes | root.tsx:277-322, admin/*.tsx:1 |
| Translucent bar (kit playground) | Landing header opens transparent and fixed from first paint; picks up the recipe past scrollY > 16 | apps/playground/src/app/landing.tsx:94-103 (scroll state), apps/playground/src/app/landing.tsx:107-113 (the bar) |
Portable checklist (for a consumer app like walmart-mvp)#
- Pages declare shell participation. No chrome markup in pages.
- A
PAGE_META-style single source for page icon+label, read everywhere a page is named. - Explicit
activebooleans. No prefix matching. - Pending feedback on the clicked link plus a global sweep. No content dim, no full-page spinner on navigation.
- Nav count badges only when the number is real and loaded.
- Breadcrumbs derived, doc-resolved, suppressed when shallow.
- Staff controls: UI gate plus server gate, always both.
- Toasts mounted once. Modals local. One file-preview host.