@boost33/ui
The app shell every Boost33 product renders inside. Fourteen components and the
design tokens they are drawn with, published to public npm. Eleven ship today;
AccountMenu, NotificationCenter and CommandPalette are in v1's scope and not
built yet. Outside the shell, it also ships LaunchLoader, the screen a product
shows while another one opens, and the DotMatrixLoader it is drawn with.
Built because the product switcher and the wallet chip worked in the Wallet and nowhere else, and because a sidebar fixed in one app fixed nothing in the other three.
Getting oriented
| How we build here | CLAUDE.md |
| What we are building | Linear document The shared app shell |
| Vocabulary | CONTEXT.md, and the initiative's Vocabulary document |
| Settled decisions | docs/adr/ |
| What changed between versions | CHANGELOG.md |
| Tickets | Linear, project Boost33 Shared UI |
Commands
npm run typecheck
npm run test # behaviour only — screenshots run in CI
npm run lint
npm run afk # one Linear issue, unattended, in a container
Before the first AFK run
cp ~/.config/toolkit/afk.env .sandcastle/.env
npx sandcastle docker build-image # once per machine
npm run afk # watch the whole first iteration
Watch for real test output in the log, not the agent's summary of it.
Consumers
The Wallet migrates first, Score second. No 1.0 until both render from this package — until then it has one consumer, which is a fork with a version number.
Fonts: map the hooks on :root, not body
The kit's faces read three hooks, --boost33-font-display, --boost33-font-sans
and --boost33-font-mono, and tokens.css resolves them on :root
(--font-sans: var(--boost33-font-sans, Inter), …). A custom property resolves
where it is declared, so a hook set only on body never reaches the kit, and
the kit silently falls back to Inter, Outfit and Geist Mono.
With next/font, put each font's .variable class on <html>, not <body>,
and map the hooks on :root:
:root {
--boost33-font-display: var(--font-outfit);
--boost33-font-sans: var(--font-inter);
}
To check it, inspect any kit element (a nav row) and read its computed
font-family. One false alarm: on a dev run where Google Fonts cannot be
reached, next/font substitutes its own 'Inter Fallback', and the dev log says
Failed to download `Inter` . That is the network, not the mapping (found by
Outbound).
Server Components: never pass a function to the kit's client components
Three modules in this package are client components ('use client'):
AppSwitcher, WalletChip and the internal NavHeightBudgetWarning.
Everything else -- AppShell, TopBar, Sidebar, NavItem, NavSection,
NotificationButton, PageHeader, Wordmark and the marks -- renders on the
server.
A Server Component cannot pass a function or a component to a client component:
props cross that boundary serialised, and Next refuses anything that is not
plain data. So from a Server Component, never pass linkComponent,
onTopUp, onOpenChange or any other function to AppSwitcher or
WalletChip. Next throws ("Functions cannot be passed directly to Client
Components") and every signed-in render returns a 500 (found by Passport).
renderToStaticMarkup tests do not catch it: they render one tree with no
boundary.
Build the two instead inside one small 'use client' file of your own, which
imports your router's Link and renders <AppSwitcher linkComponent={Link} …/>
and <WalletChip onTopUp={…} …/>, and pass that file's components into
AppShell's switcher and wallet slots. Serialisable props (products,
labels, locale, the balances) can still come from the server through it.
AppShell and NavItem take linkComponent safely: they are server modules,
so the function never crosses a boundary.
The launch screen
LaunchLoader is presentational. The page that renders it owns the timing and
passes in a caption and a progress (0–100, or null while there is no
honest number). The background is built in and the same for every product:
black, with one blurred brand-orange glow rising from below the middle of the
screen. There are no product colours to pass; stops is deprecated and ignored
since 0.5.1. The caption sits straight on the glow, and
npm run launch-contrast-check measures it.
The glow is drawn by the package stylesheet and takes no prop, so both sides of a hop paint the identical screen. Inline the ground colour too, so the instant before the stylesheet and the markup arrive is already black rather than white:
import { LaunchLoader, launchBackdropCss } from '@boost33/ui'
<style dangerouslySetInnerHTML={{ __html: launchBackdropCss() }} />
<LaunchLoader caption={caption} progress={progress} />
An animated backdrop, when one exists, goes in the backdrop slot. The screen
must stay complete without it. See CHANGELOG.md (B33-1389, B33-1401) for the full contract.