Skip to content

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.

  1. Consult /design-system first. It renders the live tokens (swatches read straight off app.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.

  2. Use the tokens, never literals. Every color is var(--color-*); every font size is var(--fs-*). Every font family is var(--font-sans) (the inherited default: titles, body, labels, buttons, prose) or var(--font-mono) (identifiers like TASK-…/#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-hoc px font size — if you reach for one, the token you need already exists (or belongs in app.css).

  3. Reuse a recipe before authoring a new component. Match the existing .gbtn / field / .badge / .panel conventions; don’t grow a per-element Tailwind utility stack for headings or buttons.

  4. Accent hues are semantic, not decorative — pick by meaning. --color-green is reserved for genuinely actionable-complete (READY); a finished-but-parked session is slate (--status-done), never green.

  5. 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 .scrim class (full primitive) for a new backdrop, or class="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-modal surface that fully covers its area (e.g. BacklogView’s mobile master→detail .mobile-detail-overlay, a solid --color-inset panel 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 not aria-modal and 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.
  6. Explanatory tooltips follow the rule in CLAUDE.md. Use #lib/tooltips/TooltipBody.svelte and TooltipExplanation for a title, summary and labelled sections; #lib/tooltips/statusTip.svelte owns status-trigger behavior. Glossary explanations supply the same structure via their registry entry. Read the live explanatory-tooltip examples on /design-system before 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.