Styleguide

The design this app is built on, shared by the product and this dashboard. Every token and component says who reaches for it. Derived from globals.css at build time. To change a value, change the CSS.

No feature doc carries area: design, so these pages are the only home the product's design has.

Shared components 9

Every shared component, in one shape: what it is and when to reach for it, from the component's own docblock; whose it is, from who imports it; a live demo in both themes, from the demo registry; then its composition, variants, measured contrast and callsites, derived at build. The reuse-first checklist starts here. Feature components live beside their features and aren't listed.

components/ui 9

8/9 demoedNo demo: ThemeWatcher

Badge

components/ui/Badge.tsxdashboard
What

Badge — a small status pill. Five tones mapped to the status token families (neutral / brand / success / warning / error). A basic starting point.

When

A short, non-interactive status mark beside the thing it describes — a state, a count, a category.

Not for: For anything clickable: this renders a `<span>`, so a badge that filters or navigates is PillToggle or a Button. And never as a second mark for a state the row already states in words — a thing marked twice reads as loud however you tune it (`component-patterns.md`).

Demo
light
neutral
Neutral
brand
Brand
success
Success
warning
Warning
error
Error
dark
neutral
Neutral
brand
Brand
success
Success
warning
Warning
error
Error
Composition
renders<span>
signatureinline-flex items-center rounded-pill px-sm py-tiny text-2xs font-semibold
Variants
TONES
neutralbg-surface-inset text-fg-secondary
brandbg-brand-subtle text-brand-strong
successbg-success-light text-success-strong
warningbg-warning-light text-warning-strong
errorbg-error-light text-error-strong
Contrast
neutral label--text-secondary on --surface-insetlight9.23:1dark11.78:1floor 4.5:1
brand label--brand-strong on --brand-subtlelight8.88:1dark7.63:1floor 4.5:1
success label--status-success-strong on --status-success-lightlight5.21:1dark10.27:1floor 4.5:1
warning label--status-warning-strong on --status-warning-lightlight4.84:1dark10.72:1floor 4.5:1
error label--status-error-strong on --status-error-lightlight5.91:1dark9.04:1floor 4.5:1
Used by
2 callsites across 2 files
app/system/styleguide/derived-ui.tsxcomponents/inspector/InspectorOverlay.tsx

Button

components/ui/Button.tsxdashboard
What

Button — the starter action control. Three variants (primary / secondary / ghost) × two sizes. Styled entirely from the semantic design tokens (globals.css), so it re-themes for dark automatically. A basic starting point — extend with icons, loading state, etc. as the project needs.

When

An action that happens in place — submitting, toggling a mode, opening a dialog, running something.

Not for: For anything that navigates. A call to action that goes somewhere has to render an anchor, or middle-click, copy-link-address and assistive semantics all break — so that is a separate component wearing this skin, not a prop on this one.

Demo
light
primary
secondary
ghost
disabled
small
dark
primary
secondary
ghost
disabled
small
Composition
renders<button>
signatureinline-flex items-center justify-center rounded-panel font-semibold transition-colors disabled:cursor-not-allowed disabled:opacity-50
Variants
VARIANTS
primarybg-brand-main text-fg-inverse hover:bg-brand-strong border border-transparent
secondarybg-surface-top text-fg-primary border border-edge-stronger hover:bg-surface-inset
ghostbg-transparent text-fg-secondary border border-transparent hover:bg-surface-inset
SIZES
smtext-xs px-md py-xs gap-xs
mdtext-sm px-lg py-sm gap-sm
Contrast
primary label--text-inverse on --brand-mainlight7.43:1dark5.54:1floor 4.5:1
primary label, hover--text-inverse on --brand-stronglight9.34:1dark8.29:1floor 4.5:1
secondary label--text-primary on --surface-toplight15.20:1dark14.34:1floor 4.5:1
secondary border--border-stronger on --surface-toplight3.68:1dark3.45:1floor 3:1
ghost label--text-secondary on --surface-toplight10.95:1dark10.22:1floor 4.5:1
ghost label, hover--text-secondary on --surface-insetlight9.23:1dark11.78:1floor 4.5:1
Used by
2 callsites across 1 file
components/inspector/InspectorOverlay.tsx

Input

components/ui/Input.tsxunused
What

Input — the starter text field, with an optional label + hint. Styled from the semantic tokens. A basic starting point — extend with icons, error state, and validation as the project needs.

When

A single-line value the reader types — a name, an email, a search term. Pass `id` and `label` together; the label is wired with `htmlFor`.

Not for: For a choice from a known set, which is PillToggle or a select, and for anything multi-line. There is no error state here yet, so a field that has to report one needs that built first rather than faked with a hint.

Demo
light
label + value
placeholder
with hint
disabled
dark
label + value
placeholder
with hint
disabled
Composition
renders<label>
signaturerounded-sm border border-edge-stronger bg-surface-top px-md py-sm text-sm text-fg-primary placeholder:text-fg-gray focus:border-brand-main focus:outline-none
Variants
No variant map — the component takes no styled variants.
Contrast
field text--text-primary on --surface-toplight15.20:1dark14.34:1floor 4.5:1
placeholder--text-gray on --surface-toplight5.50:1dark5.18:1floor 4.5:1
label--text-secondary on --surface-toplight10.95:1dark10.22:1floor 4.5:1
hint--text-tertiary on --surface-toplight7.74:1dark7.28:1floor 4.5:1
resting border--border-stronger on --surface-toplight3.68:1dark3.45:1floor 3:1
focus border--brand-main on --surface-toplight7.90:1dark5.54:1floor 3:1
Used by
No callsites outside its own file. Either it is new, or nothing reaches for it.

Mark

components/ui/Mark.tsxdashboard
What

Mark — your project's logo, for use inside the app. One home for the shape, the way lib/project.ts is the one home for the name. The kickoff sets both. Replacing the starter here changes the `/system` wordmark everywhere it renders, in one edit. The starter is a brand-coloured tile with a diamond cut out of it. It is meant to look like a placeholder, because it is one. The centre is a TRUE cutout: the tile and the diamond are one path with `fill-rule="evenodd"`, so the inner region is unpainted and whatever sits behind the mark shows through. That is why this needs no theme prop, no media query, and no colour for the centre at all — it is correct on a light header, a dark header, and anything you re-skin to. If you replace the shape, keeping that property is worth the trouble: a painted centre has to know what is behind it, and every callsite is a chance to get that backwards. The tile takes `var(--brand-main)`, so re-skinning the tokens in app/globals.css recolours the wordmark with everything else. NOT the only copy of this shape, and deliberately so. `app/icon.svg` is a standalone file because a favicon has no stylesheet to inherit from, and `app/opengraph-image.tsx` re-declares it because Satori renders outside CSS entirely. Three renderers, one set of path data — if the shape changes, it changes in all three. The cutout needs no per-renderer mechanism at all, and the **colour is the only thing that differs**: this one reads a custom property, the other two carry a literal, and the favicon's literal is a lighter step because its backdrop is browser chrome rather than a surface we theme. Geometry that lives in one renderer and not the others is a defect, not a feature — `app/icon.svg` records the outline that taught us that. No `viewBox` scaling logic: `size` drives width/height and the 32x32 box does the rest.

When

Your project's logo anywhere inside the app — a header wordmark, a nav, an empty state.

Not for: As a decorative bullet or a generic glyph: the mark is your project's identity, and spending it as ornament is what stops it reading as one. Outside the app it is not this component at all — `app/icon.svg` and `app/opengraph-image.tsx` carry the same path data for renderers that have no stylesheet.

Demo
light
16px
24px
40px
on brand
dark
16px
24px
40px
on brand
Composition
renders<svg>
signatureno static class run to identify it by
Variants
No variant map — the component takes no styled variants.
Contrast
copper on inset--brand-main on --surface-insetlight6.66:1dark6.39:1floor 3:1
copper on the page--brand-main on --surface-baselight7.10:1dark6.13:1floor 3:1
Used by
2 callsites across 2 files
app/system/nav.tsxapp/system/page.tsx

PillToggle

components/ui/PillToggle.tsxdashboard
What

PillToggle — a wrapping row of selectable pills. Single-select by default; pass `multi` to allow multiple selections. Uses the canonical `.pill` / `.pill.active` styles from globals.css. Active state is neutral — a strong neutral border + primary text, no action colour, so selection reads as chrome, not meaning.

When

Filtering or narrowing a set in place, where the options are few enough to show at once and the reader benefits from seeing them all.

Not for: For switching between peer views, which is TabBar — pills read as a filter over one view, not as a choice of views. And not for a single on/off, which is Toggle.

Demo
light
single-select
multi-select
dark
single-select
multi-select
Composition
renders<div><button>
signatureflex flex-wrap gap-xs
Variants
No variant map — the component takes no styled variants.
Contrast
resting label--text-secondary on --surface-toplight10.95:1dark10.22:1floor 4.5:1
selected label--text-primary on --surface-toplight15.20:1dark14.34:1floor 4.5:1
resting border--border-stronger on --surface-toplight3.68:1dark3.45:1floor 3:1
selected border--text-primary on --surface-toplight15.20:1dark14.34:1floor 3:1
Used by
1 callsite across 1 file
app/system/styleguide/section-nav.tsx

TabBar

components/ui/TabBar.tsxunused
What

TabBar — a segmented pill control for switching between sibling views. The track is `--surface-inset`, the active tab lifts onto `--surface-top`, and an optional per-tab badge count carries an unactioned signal. Switching is the caller's: this is controlled (`activeKey` + `onChange`), so a bar backed by routes navigates and one backed by state sets it.

When

Two to five peer views of one thing, where the reader is expected to move between them and no view is a destination in its own right.

Not for: For a filter over a list, which is PillToggle — a tab bar claims the views are exclusive and equal. Not for a settings triad either: an icon row in a track reads as navigation at a smaller size, which is why ThemeToggle is its own component and not a variant of this one.

Demo
light
default
with a badge
dark
default
with a badge
Composition
renders<div><button>
signatureno static class run to identify it by
Variants
No variant map — the component takes no styled variants.
Contrast
inactive label--text-gray on --surface-insetlight4.64:1dark5.97:1floor 4.5:1
active label--text-primary on --surface-toplight15.20:1dark14.34:1floor 4.5:1
badge count--text-inverse on --brand-mainlight7.43:1dark5.54:1floor 4.5:1
Used by
No callsites outside its own file. Either it is new, or nothing reaches for it.

ThemeToggle

components/ui/ThemeToggle.tsxboth
What

ThemeToggle — three-way appearance control: Light / Dark / System. `System` follows the OS `prefers-color-scheme` and updates live (via `<ThemeWatcher>`); Light / Dark pin an explicit theme. Writes the *preference* to `localStorage['theme-pref']` and applies the resolved `data-theme` to <html> (see `lib/theme.ts`); the pre-paint script in `app/layout.tsx` mirrors the resolution to avoid a flash. Deliberately NOT a `TabBar`: an icon triad reads as a settings control rather than navigation, which a row of text labels in a track never did — it just looked like the /system nav at a smaller size. Labels survive as `title` + `aria-label`. Re-syncs across instances via the `theme-changed` event.

When

The one place a surface offers appearance as a setting — a header, a preferences row. It is self-contained: no props are needed beyond an optional class.

Not for: More than once on a screen. Instances do stay in sync, but a second copy states a global setting twice, and this is a control rather than a status.

Demo
light
live — this is the real control
dark
live — this is the real control
Composition
renders<div><button>
signatureno static class run to identify it by
Variants
No variant map — the component takes no styled variants.
Contrast
inactive icon--text-gray on --surface-insetlight4.64:1dark5.97:1floor 3:1
active icon--text-primary on --surface-toplight15.20:1dark14.34:1floor 3:1
Used by
2 callsites across 2 files
app/page.tsxapp/system/nav.tsx

ThemeWatcher

components/ui/ThemeWatcher.tsxboth
What

ThemeWatcher — app-global listener that keeps the `system` theme preference live. Mounted once in the root layout so the OS-follow works on every page, not just where a ThemeToggle happens to be rendered. When the preference is `system`, an OS light/dark change re-applies `data-theme` immediately. When the preference is an explicit light/dark, OS changes are ignored. Re-checks on the theme-changed event so switching to/from System takes effect without a reload. Renders nothing.

When

Once, in the root layout. Every page needs the OS-follow, and a watcher mounted anywhere narrower stops working the moment the reader navigates away from it.

Not for: Anywhere else, and never more than once: each instance attaches its own listeners to do work that is already being done.

Demo

No demo. Renders nothing. It is an effect mounted once in the root layout so the `system` preference keeps following the OS; there is no surface to put in a canvas. Its behaviour is visible on the ThemeToggle demo above, which is the control it listens to.

Composition
rendersno root element parsed
signatureno static class run to identify it by
Variants
No variant map — the component takes no styled variants.
Used by
1 callsite across 1 file
app/layout.tsx

Toggle

components/ui/Toggle.tsxunused
What

Toggle — a starter on/off switch. Controlled: pass `checked` + `onChange`. Styled from the semantic tokens; the track turns brand when on. Off, it is drawn in the control boundary (`--border-stronger`): a ring around a sunken track and a knob in the same colour, so both the switch and its state clear the 3:1 floor for a component on any surface. A light knob on a grey track cannot: the pair the eye needs is the one that measures lowest. A basic starting point.

When

A single setting that takes effect immediately — the reader flips it and the thing is on.

Not for: For a choice that only applies on submit, which is a checkbox, and for one of several options, which is PillToggle. It carries no label of its own beyond `aria-label`, so a visible one belongs beside it.

Demo
light
on
on
off
off
disabled
on
dark
on
on
off
off
disabled
on
Composition
renders<button>
signatureinline-flex h-[24px] w-[42px] shrink-0 items-center rounded-full border px-[2px] transition-colors disabled:opacity-50
Variants
No variant map — the component takes no styled variants.
Contrast
knob on the on-track--surface-top on --brand-mainlight7.90:1dark5.54:1floor 3:1
knob on the off-track--border-stronger on --surface-insetlight3.10:1dark3.98:1floor 3:1
on-track against the card--brand-main on --surface-toplight7.90:1dark5.54:1floor 3:1
off-track ring against the card--border-stronger on --surface-toplight3.68:1dark3.45:1floor 3:1
Used by
No callsites outside its own file. Either it is new, or nothing reaches for it.

Sources: components/ui · overlays · layout, parsed by lib/styleguide.ts; the demo mounts and the pairs each one paints, from demos.tsx; the ratios computed at build by lib/contrast.ts from globals.css. The anatomy is fixed: what → when → demo → composition → variants → usage.