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.tsxdashboardBadge — a small status pill. Five tones mapped to the status token families (neutral / brand / success / warning / error). A basic starting point.
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`).
<span>inline-flex items-center rounded-pill px-sm py-tiny text-2xs font-semiboldTONESneutralbg-surface-inset text-fg-secondarybrandbg-brand-subtle text-brand-strongsuccessbg-success-light text-success-strongwarningbg-warning-light text-warning-strongerrorbg-error-light text-error-strong--text-secondary on --surface-insetlight9.23:1dark11.78:1floor 4.5:1--brand-strong on --brand-subtlelight8.88:1dark7.63:1floor 4.5:1--status-success-strong on --status-success-lightlight5.21:1dark10.27:1floor 4.5:1--status-warning-strong on --status-warning-lightlight4.84:1dark10.72:1floor 4.5:1--status-error-strong on --status-error-lightlight5.91:1dark9.04:1floor 4.5:1app/system/styleguide/derived-ui.tsxcomponents/inspector/InspectorOverlay.tsxButton
components/ui/Button.tsxdashboardButton — 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.
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.
<button>inline-flex items-center justify-center rounded-panel font-semibold transition-colors disabled:cursor-not-allowed disabled:opacity-50VARIANTSprimarybg-brand-main text-fg-inverse hover:bg-brand-strong border border-transparentsecondarybg-surface-top text-fg-primary border border-edge-stronger hover:bg-surface-insetghostbg-transparent text-fg-secondary border border-transparent hover:bg-surface-insetSIZESsmtext-xs px-md py-xs gap-xsmdtext-sm px-lg py-sm gap-sm--text-inverse on --brand-mainlight7.43:1dark5.54:1floor 4.5:1--text-inverse on --brand-stronglight9.34:1dark8.29:1floor 4.5:1--text-primary on --surface-toplight15.20:1dark14.34:1floor 4.5:1--border-stronger on --surface-toplight3.68:1dark3.45:1floor 3:1--text-secondary on --surface-toplight10.95:1dark10.22:1floor 4.5:1--text-secondary on --surface-insetlight9.23:1dark11.78:1floor 4.5:1components/inspector/InspectorOverlay.tsxInput
components/ui/Input.tsxunusedInput — 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.
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.
<label>rounded-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--text-primary on --surface-toplight15.20:1dark14.34:1floor 4.5:1--text-gray on --surface-toplight5.50:1dark5.18:1floor 4.5:1--text-secondary on --surface-toplight10.95:1dark10.22:1floor 4.5:1--text-tertiary on --surface-toplight7.74:1dark7.28:1floor 4.5:1--border-stronger on --surface-toplight3.68:1dark3.45:1floor 3:1--brand-main on --surface-toplight7.90:1dark5.54:1floor 3:1Mark
components/ui/Mark.tsxdashboardMark — 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.
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.
<svg>--brand-main on --surface-insetlight6.66:1dark6.39:1floor 3:1--brand-main on --surface-baselight7.10:1dark6.13:1floor 3:1app/system/nav.tsxapp/system/page.tsxPillToggle
components/ui/PillToggle.tsxdashboardPillToggle — 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.
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.
<div><button>flex flex-wrap gap-xs--text-secondary on --surface-toplight10.95:1dark10.22:1floor 4.5:1--text-primary on --surface-toplight15.20:1dark14.34:1floor 4.5:1--border-stronger on --surface-toplight3.68:1dark3.45:1floor 3:1--text-primary on --surface-toplight15.20:1dark14.34:1floor 3:1app/system/styleguide/section-nav.tsxTabBar
components/ui/TabBar.tsxunusedTabBar — 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.
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.
<div><button>--text-gray on --surface-insetlight4.64:1dark5.97:1floor 4.5:1--text-primary on --surface-toplight15.20:1dark14.34:1floor 4.5:1--text-inverse on --brand-mainlight7.43:1dark5.54:1floor 4.5:1ThemeToggle
components/ui/ThemeToggle.tsxbothThemeToggle — 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.
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.
<div><button>--text-gray on --surface-insetlight4.64:1dark5.97:1floor 3:1--text-primary on --surface-toplight15.20:1dark14.34:1floor 3:1app/page.tsxapp/system/nav.tsxThemeWatcher
components/ui/ThemeWatcher.tsxbothThemeWatcher — 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.
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.
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.
app/layout.tsxToggle
components/ui/Toggle.tsxunusedToggle — 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.
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.
<button>inline-flex h-[24px] w-[42px] shrink-0 items-center rounded-full border px-[2px] transition-colors disabled:opacity-50--surface-top on --brand-mainlight7.90:1dark5.54:1floor 3:1--border-stronger on --surface-insetlight3.10:1dark3.98:1floor 3:1--brand-main on --surface-toplight7.90:1dark5.54:1floor 3:1--border-stronger on --surface-toplight3.68:1dark3.45:1floor 3:1Sources: 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.