Warm on purpose. Never neutral grey.
Everything is oklch, so lightening a colour doesn’t change its hue and the dark theme is the light one with the lightness rebalanced rather than a second palette to maintain. The values below are the ones in the package’s stylesheet — flip the theme and they change with the swatches.
Brand
Build → Ship → Run, plus the flame the whole thing is named after
- --build
- oklch(0.79 0.155 70)
- Build — dev environments
- --ship
- oklch(0.72 0.185 52)
- Ship — deployments
- --run
- oklch(0.66 0.205 42)
- Run — logs and stats
- --flame
- oklch(0.64 0.205 41)
- the brand accent
Surface
warm cream by default, warm charcoal in the dark — never neutral grey
- --background
- oklch(0.975 0.012 82)
- the page
- --card
- oklch(0.995 0.006 86)
- raised surfaces
- --muted
- oklch(0.955 0.012 80)
- quiet fills
- --border
- oklch(0.89 0.016 76)
- every hairline on this site
Status
green means running, amber means look at me, red means it broke
- --success
- oklch(0.62 0.145 152)
- running · online · done
- --warning
- oklch(0.74 0.16 68)
- degraded · needs attention
- --destructive
- oklch(0.577 0.245 27.325)
- failed · destructive action
- --info
- oklch(0.58 0.13 245)
- neutral information
- radius
- 0.25rem — every other radius derives from it, so squaring the whole system is one line
- type
- Syne for display, Geist Sans for text, Geist Mono for anything a machine wrote
- shadows
- none. The app kills every box-shadow on purpose; depth comes from hairlines
- semantics
- Build / Ship / Run are utilities (text-build, bg-ship/15) — not hex codes in a component
on Base UI
Small, boring, consistent.
The parts a product actually spends its day on. Nothing here is trying to be clever — the value is that the console, the marketing site and the self-hosted app all reach for the same button.
Button — five variants, four sizes
Badge — status, at a glance
Spinner · Skeleton · Avatar
Input · Label
Switch · Checkbox · RadioGroup
Tabs — Base UI, sliding indicator
Alert — the persistent counterpart to toast()
Card
Quasi-square by design
--radius: 0.25rem — the whole scale derives from it
Separator · the 4px radius, everywhere
A terminal and a pull bar, as components.
Terminal wraps xterm.js with the theme, fit-on-resize and a search API, and is transport-agnostic — feed it from a WebSocket, a log file or a recording. TerminalPanel is the frame around it: title, status, actions, and the connecting/empty/error states you always end up writing twice.
ImagePull is the other one: a Docker pull drawn as a single segmented bar, one block per layer. Purely presentational — hand it a snapshot, it draws it.
- Terminal
- xterm.js + theme + fit + search, fed through a ref handle
- TerminalPanel
- the bar, the surface, and every state a connection can be in
- ImagePull
- one segmented bar per pull, one block per layer
- overlays
- Dialog, AlertDialog, Select, Tooltip and toast() under one provider
a real TerminalPanel, replaying a real session
Two lines to adopt the whole thing.
The tokens ship as one CSS file, so you can take the palette without the components, or the components and restyle them — they’re Tailwind classes over Base UI, not a black box.
npm i @spunto/design-system/* one import for the tokens — light by default, .dark for the rest */
@import "@spunto/design-system/styles.css";import { Button, Badge, TerminalPanel } from "@spunto/design-system"
export function Deploy() {
return (
<div className="flex items-center gap-2">
<Badge variant="secondary">v0.9.3</Badge>
<Button size="sm">Deploy</Button>
</div>
)
}documented twice
Once for you. Once for the agent.
The showcase is data-driven, so the same source feeds the visual pages and a plain-markdown surface an LLM can read without a browser: an index at /llms.txt, one /<component>.md per primitive with its full props API, the foundations, and /llms-full.txt for everything at once.
Which matters more than it sounds: most of the code in this ecosystem is written by agents, and an agent that can read the props API stops inventing a fifth button variant.