Page anatomy
Three devices build every screen: Screen, PageHeader, Panel
(components/brand/devices.tsx).
Screen — the page box, in two rhythms
<Screen> {/* a scrolling document */}
<Screen fill> {/* a viewport application */}
| padding | gap between blocks | bottom | |
|---|---|---|---|
default | 24px, 32px at lg | 40px | 80–96px |
fill | 24px, 32px at lg | 24px | 24–32px, nothing scrolls but the child |
Use fill when one child owns the scroll (a table, a map, a long list). Use the
default for documents, overviews and forms.
PageHeader
<PageHeader title="User management" meta={<Badge …/>} description="…" actions={…} />
- Actions share the title's line, not the header's box. Centring them against title-plus-description drops a button into the gap between two lines of prose, pointing at neither.
metais for badges that qualify the title — a count, a vintage, a scope.descriptionis capped at 76 characters wide, in secondary ink. One sentence.- The action box is
min-w-0, notshrink-0, so it wraps on a phone instead of being cut off by the main column'soverflow-x: hidden.
There is one <h1> per screen and it is this one: DM Sans 800 at
text-display, leading-none, tracking-tight.
Panel
A card that holds something with its own scroll — a table, a map, a list. Zero padding, because the thing inside owns its edges; a table's cell padding plus a card's padding is two paddings, and the sticky header then pins below a strip of card instead of at the top of the box.
<Panel className="min-h-0 flex-1"> is the standard pairing inside Screen fill.
StatTile and StatRow
The loudest voice in the system: a mono-caps label, a DM Sans 800 numeral at
text-stat with hero tracking, and one line of qualifying text underneath.
The qualifier is required — a number with nothing saying what it counts is the
thing this system exists to prevent.
Four to a desktop row, two on a phone (StatRow). Tone (positive, caution,
danger) colours the numeral when it is genuinely a state; accent when it is
the subject. Default is plain foreground, and most tiles should stay that way.
Metric — the composable figure
StatTile is the fixed device: label, numeral, qualifier. Metric is the same
figure with every extra part optional, for when one screen needs more of it than
another:
| Part | Prop | Notes |
|---|---|---|
The qualifier | sub | Required. A number with nothing saying what it counts is not a fact |
The change | delta | Needs a direction and a meaning, not a sign — churn going up is bad, and that is a fact about the metric, not the arithmetic |
A shape | series | Sparkline, on the numeral's baseline. Twelve points or so |
A target | progress | The 5px track, 0–100 |
The hint | info | The (?), one or two sentences |
A state | tone | Only when the figure genuinely has one. Most do not |
Sparkline and Delta are exported on their own, for a table cell or a line of
prose.
The sparkline is hand-drawn SVG, not Recharts, and that is deliberate. It has
no axes, no legend and no tooltip — it is a shape in a line of text. Mounting a
chart library to draw twelve points costs tens of kilobytes and a layout pass for
what forty lines of path arithmetic do exactly. Recharts (components/ui/chart.tsx)
earns its place on a real chart, where the axes, the ticks and the tooltip are
the work.
Callout
Two 14px accent bracket corners, top-left and bottom-right. No background, no
box, and never a coloured top or left border — that is on the never list.
tone="danger" swaps the marks to the danger colour for a do-not callout.
Eyebrow and Hint
Eyebrow is the mono-caps section label — a section is marked with type. Hint
is one line of secondary prose capped at 76 characters, for a caveat that
belongs beside the thing it qualifies.
Reading width and rhythm
Prose is capped at 76 characters. Marketing sections get 96 / 64 / 48px of vertical padding and a 1280px centred container. Product screens do not — they fill their column.
The hub
The workspace's front door: the tools as tiles, built from the same PRODUCTS
list the rail's switcher uses. soon tools render as tiles that do not link.
adminOnly tools appear only for an admin — except on an unconfigured deploy,
where the route really is open and hiding the tile would hide that fact.