Design system
The UI has a semantic token layer (ui/src/app.css — --color-* surfaces/text/accents, the --fs-* type scale, --status-*/--wash-*) and a live reference page that documents it plus the canonical component recipes: /design-system (ui/src/routes/design-system/+page.svelte). It exists to stop design drift — every session re-inventing buttons, spacing and colors.
No automated gate flags off-token colors. This rule and review are the only enforcement. The one exception: ui/src/lib/color-tokens.test.ts fails on any var(--color-*) that app.css never declares, because an undefined token renders as nothing and raises no error.
-
Consult
/design-systemfirst. It renders the live tokens (swatches read straight offapp.css, so they can’t drift) plus the button / form-field / badge / panel / scrim recipes, each with a when-to-use note and copy-paste markup. -
Use the tokens, never literals. Every color is
var(--color-*); every font size isvar(--fs-*). Every font family isvar(--font-sans)(the inherited default: titles, body, labels, buttons, prose) orvar(--font-mono)(identifiers likeTASK-…/#issue/PR #/SHAs/versions, repo names/owner/repo/@login, code, paths, terminal, numeric readouts) — never a literal family. Set mono on the identifier/readout element itself, never on a container root. Never introduce a raw hex,rgba(), or ad-hocpxfont size — if you reach for one, the token you need already exists (or belongs inapp.css). -
Reuse a recipe before authoring a new component. Match the existing
.gbtn/ field /.badge/.panelconventions; don’t grow a per-element Tailwind utility stack for headings or buttons. -
Accent hues are semantic, not decorative — pick by meaning.
--color-greenis reserved for genuinely actionable-complete (READY); a finished-but-parked session is slate (--status-done), never green. -
Every blocking (modal) dialog/drawer dims and blurs what’s behind it — when a surface seizes interaction and app content stays visible behind it, that surface must read as the focus (desktop and mobile alike). Use the canonical backdrop from
app.css: the global.scrimclass (full primitive) for a new backdrop, orclass="overlay"for modal overlays (which inherit the same blur). Never ship such a surface with a fully-lit background or a hand-rolled backdrop without the blur. See the Modal & scrim recipe on/design-system. Two scope notes so the rule isn’t over-applied:- Exempt — opaque full-cover view-swaps: an
aria-modalsurface that fully covers its area (e.g. BacklogView’s mobile master→detail.mobile-detail-overlay, a solid--color-insetpanel that replaces the list in-place) — there is nothing visible behind it to dim, and a translucent scrim would only let the covered view bleed through. The rule is about visible-background floating surfaces, not full-bleed navigation. - Exempt — small anchored, non-blocking popovers: a
role="dialog"that is notaria-modaland floats anchored to a trigger (e.g. AutomationPanel’s.auto-pop, EmojiPicker’s.ep) does not seize the app or warrant a full-screen backdrop — no scrim, dismiss on outside-click/Esc instead.
- Exempt — opaque full-cover view-swaps: an
-
Explanatory tooltips follow the rule in
CLAUDE.md. Use#lib/tooltips/TooltipBody.svelteandTooltipExplanationfor a title, summary and labelled sections;#lib/tooltips/statusTip.svelteowns status-trigger behavior. Glossary explanations supply the same structure via their registry entry. Read the live explanatory-tooltip examples on/design-systembefore adding one. A longer plain string or merely wider panel does not meet the rule.
The /design-system page is a developer/agent-facing internal reference (unlinked from the app), so it is exempt from i18n and the feature catalog.