Design system
The admin has its own design system, browsable at /admin/design-system in any
site that installs Ounce. It is deliberately absent from the admin sidebar, so
you reach it by typing the URL. That browser shows you what exists. This page is
the part that a browser cannot show: when to reach for it, and when to add to
it.
The whole system exists for one reason. The admin sits beside a site it knows nothing about, so it cannot afford a personality. Black is the primary colour. Nothing in the admin should ever look like it is trying to be your brand.
Four rules
Section titled “Four rules”- Extract a composite the moment something is reused. The second call site is the trigger, not the third.
- Tokens live in the foundation.
src/ui/ui.tsfor surfaces, pills, rows and buttons;src/ui/type.tsfor the type scale. One definition, imported. - A base component is one that is used more than once. A component nothing imports is not a foundation, it is a guess.
- Use as little colour as you can. Reach for the neutral ramp first, and assume the answer is grey.
The palette is closed
Section titled “The palette is closed”ounce.tailwind.mjs sets theme.colors rather than theme.extend.colors, which
replaces Tailwind’s palette outright:
colors: { transparent, current, white, black, gray: colors.neutral, red: colors.red, green: colors.green, amber: colors.amber,}bg-blue-500 is not a class that renders the wrong blue. It is a class that does
not exist, so it renders nothing, and you find out by looking. Four ramps is the
whole vocabulary:
| Ramp | Means |
|---|---|
gray |
every surface, border and letter |
red |
destruction and failed validation, never decoration |
green |
published, live, saved: a state that is good, not an action |
amber |
hidden, draft, viewing-as: something needs a decision |
A tone is a 100 background with an 800 foreground. Any other pairing fails
contrast somewhere. Solid green or amber fills are for state dots only.
The type scale is closed
Section titled “The type scale is closed”src/ui/type.ts declares four heading levels and seven text roles, each with a
_USE sentence saying when it applies. Eight pixel sizes exist: 10, 11, 12, 13,
14, 16, 20, 28. A size outside that list has no role, and a role is the thing
that tells the next person when to use it.
Buttons
Section titled “Buttons”There is one button, and it is a function:
import { buttonClass } from '@ouncepage/core/ui/ui';
buttonClass('primary'); // bg-gray-900, hover to blackbuttonClass('ghost', 'sm');buttonClass('danger');buttonClass('quiet', 'md', true); // icon only, squareIt returns a class string, so it works on <button> and on <a> without a
wrapper. Six variants, two sizes. If a screen needs a seventh variant, that is a
conversation about the variant, not a one-off class.
A shared component must forward what it is given
Section titled “A shared component must forward what it is given”This is the rule with teeth, because breaking it fails silently.
type Props = { level?: HeadingLevel } & Record<string, unknown>;const { level = 2, ...rest } = Astro.props;---<h2 {...rest} class={classes}><slot /></h2>Without the ...rest, a caller writing <Heading data-dialog-title> gets a
heading with no attribute, no warning and no error. That exact omission once took
every dialog in the admin out of service at the same time: move to a new
path, move to trash, the unsaved-changes warning and the rich text link prompt
all stopped opening, because the script that fills the dialog could not find its
title element.
test/design-system.test.ts fails if any component in src/ui stops forwarding,
and on off-palette colours, raw hex, off-scale sizes and hand-built button
shapes. Run it before you add a component, not after.
Adding to the system
Section titled “Adding to the system”Add a token when a value is used in two places. Add a component when markup is
used in two places. Add a topic to src/design/topics.ts when you add either, so
the browser stays the truth rather than a snapshot.
If you genuinely need to go outside the system, do it in one file, and add the exact string to the allowlist in the test with a comment-free, self-evident name. There is one such exception today: the SEO preview imitates a Google search result, so it uses Google’s link blue and 18px type. That is not a precedent; it is a screenshot.