Frontend & Design System
The web UI is a SolidJS single-page app in frontend/,
built with Vite and Tailwind v4. It talks to the API over fetch and a WebSocket,
and is embedded into the tack binary at release time via the embed-spa feature.
This page covers how the frontend is organized and, in particular, the design token system every component relies on.
Layout
frontend/src/
├── app/ App shell — Router, Layout (sidebar + top bar), routes
├── features/ One folder per surface: board, list, table, calendar,
│ timeline, sprints, item-detail (incl. the Brief tab),
│ dashboard (Overview and Factory metrics), projects,
│ settings, templates, agents
├── shared/
│ ├── ui/ The component kit (Button, Badge, Modal, Drawer, Tabs,
│ │ CommandPalette, SearchBar, Sidebar, ToastContainer …) plus
│ │ redesign primitives: Avatar/AvatarStack, TypeBadge,
│ │ PriorityDot, WipChip, KbdHint, and the icon set (icons.tsx)
│ ├── state/ Context stores + signals (project, items, theme, palette,
│ │ commandPalette, optimistic updates, toasts)
│ ├── api/ Typed fetch client (api.*), one module per resource
│ ├── realtime/ Reconnecting board WebSocket
│ ├── runWithAgent/ The run dialog (RunFlow), attempt list with its pull-request
│ │ badge, decision inbox, merge-readiness panel, event timeline
│ ├── execution/, agents/ Execution types and API calls; the Agents page's pieces
│ ├── vocab/ Per-project terminology resolution (useVocab)
│ └── types/ DTOs mirroring the backend
└── index.css The design tokens (see below)
Module boundary
Features are isolated: a features/* file may import from shared/*, never from
another feature. This is enforced by frontend/src/architecture.test.ts — a
failing import shows up as a unit-test failure. Anything two features need goes in
shared/.
Design tokens
All colour, surface, and shadow values live as CSS custom properties in
frontend/src/index.css.
Components consume only these --color-* tokens (via inline style), never raw
hex or Tailwind colour literals. That single indirection is what lets one attribute
flip restyle the entire app.
The system has two axes:
- Mode — a
.darkclass on<html>(managed byshared/state/theme.ts).:rootholds the light values;.darkoverrides only what differs. - Palette — a
data-palette="harbor|clay|graphite"attribute on<html>(managed byshared/state/palette.ts). Harbor is the default the app applies; no attribute = the Teal base values in:root.
So the cascade is :root → .dark → :root[data-palette="…"] →
.dark[data-palette="…"]. Each block redefines only the primitive values
(backgrounds, text tiers, the accent, semantic solids); everything derived (the
primary ramp, hover/active surfaces, inverse text, focus ring) is expressed once as
var() aliases that re-resolve against whichever palette is active. Adding another
palette means adding one primitives block — nothing else changes.
A Tailwind @theme inline block re-exposes the runtime tokens under utility names
(bg-surface, text-content, border-line, bg-brand-*, …) for the places that
use classes instead of inline styles.
Accessibility constraint
Token values are tuned to WCAG 2.1 AA (4.5:1 text contrast) and verified by an
axe scan in the E2E suite (frontend/e2e/a11y.spec.ts). When changing a colour,
keep white-on-accent and faint-text-on-surface above 4.5:1 — the CI a11y job will
fail otherwise.
Typography
Three self-hosted fonts (via @fontsource, so they work offline):
- Caprasimo — the display face: headings, step numbers, buttons
(
--font-heading, utilityfont-heading). One weight; never bold it. - Figtree — the UI body (
--font-body, also--font-sans). - JetBrains Mono — ids, estimates, and keycaps (
--font-mono).
Shape
The shape language is over-rounded. Controls — buttons, inputs, tags, tab
pills, banners — are pills (--radius-pill). Containers such as board columns
and settings cards take --radius-card (28px) on the --color-bg-panel fill,
and the items inside them take --radius-item (20px) back on the page ground.
Adding a UI component
- Build it in
shared/ui/as a small Solid component. Style it with inline token styles —style={{ 'background-color': 'var(--color-bg-base)' }}— so it re-themes for free. Reuse existing primitives (Avatar,TypeBadge,PriorityDot,WipChip,KbdHint) rather than re-inlining markup. - Export it from
shared/ui/index.ts. - If it has pure logic (a colour map, a formatter), put that in a co-located
*.tsand unit-test it (shared/ui/primitives.test.tsis the pattern). - Consume it from a feature — never reach into another feature.
Design source
The current visual language was imported from a Claude Design project
(Tack.dc.html) and implemented onto the token system above. The design is a
reference; the source of truth is index.css plus the shared/ui kit.
Running it
cd frontend
npm install
npm run dev # http://localhost:5173, proxies /api to :3210
npm run type-check
npm test # Vitest unit tests
npm run build
Start the API (cargo run -p tack-cli -- serve) before the dev server. See
Testing for the Playwright E2E setup.