diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..594519e
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,19 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+
+jobs:
+ check:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: npm
+ - run: npm ci
+ # The full gate: oxlint → token contract → vitest → tsc + vite build.
+ - run: npm run check
diff --git a/AGENTS.md b/AGENTS.md
index f417e48..c734192 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,14 +1,21 @@
# Northstar Design System — agent handbook
-React 19 + Vite 8 + TypeScript design-system showcase. Dark-first, token-driven UI with a light theme.
+React 19 + Vite 8 + TypeScript (strict) design-system showcase. Dark-first, token-driven UI with a light theme.
## Commands
| Task | Command |
|---|---|
| Dev server | `npm run dev` (vite.config.ts pins `0.0.0.0:5173`) |
-| Full gate: lint + token contract + typecheck + build | `npm run check` — run before declaring any change done |
+| Full gate: lint + token contract + tests + typecheck + build | `npm run check` — run before declaring any change done |
| Lint only | `npm run lint` (oxlint) |
+| Tests | `npm run test` (vitest, jsdom; suites in `tests/`) |
+
+TypeScript is **strict** (`strict` + `noUncheckedIndexedAccess` in `tsconfig.app.json`) — implicit `any` fails the build, not just the reviewer.
+
+## Debugging without a console
+
+Browser tooling here has no console access. Runtime errors therefore render into a `#dev-errors` element (see `src/dev/errorSink.ts` + `src/dev/ErrorBoundary.tsx`): after loading the page, `wait_for` / `inspect` `[data-dev-error]` to read error messages, and `wait_for vite-error-overlay` for build/HMR errors. If `#root` is empty but no `[data-dev-error]` exists, check the dev-server stdout.
## Sandboxed worker dev flow
@@ -18,20 +25,24 @@ React 19 + Vite 8 + TypeScript design-system showcase. Dark-first, token-driven
## Design tokens — the contract
1. **Single source of truth: `src/tokens.css`.** Every font size, color, spacing, radius, and shadow color comes from a token. The palette is defined in modern CSS Color 4 `lab(...)` color space:
- - Dark theme (`.dark`, `[data-theme='dark']`): Pitch black base (`lab(0% 0 0)`), deep cards (`lab(.903296% 0 0)`), radiant electric mint/cyan primary (`lab(89.9321% -47.9163 -15.7303)`), vivid destructive (`lab(54.5811% 71.684 50.9279)`), chart colors 1–5, and subtle dot grid (`--dot-color`).
- - Light theme (`:root`, `[data-theme='light']`): Pure white daylight base (`lab(100% 0 0)`), crisp charcoal primary (`lab(7.78201% -.0000149012 0)`), and high-contrast surfaces.
-2. The guard `scripts/check-tokens.mjs` (in `npm run check`) bans hardcoded hex colors, px font sizes, and raw `rgba()` in application CSS — new colors go in as tokens, accent washes use `color-mix(in srgb, var(--primary) N%, transparent)`.
+ - Dark theme (`:root:not([data-theme='light'])`, `[data-theme='dark']`, `.dark`): Pitch black base (`lab(0% 0 0)`), deep cards (`lab(.903296% 0 0)`), radiant electric mint/cyan primary (`lab(89.9321% -47.9163 -15.7303)`), vivid destructive (`lab(54.5811% 71.684 50.9279)`), chart colors 1–5, and subtle dot grid (`--dot-color`).
+ - Light theme (`:root`, `[data-theme='light']`, `.light`): Pure white daylight base (`lab(100% 0 0)`), crisp charcoal primary (`lab(7.78201% -.0000149012 0)`), and high-contrast surfaces.
+ - There are **no** legacy aliases (`--panel*`, `--line*`, `--text-soft`, …) anymore — canonical names only.
+2. The guard `scripts/check-tokens.mjs` (in `npm run check`) bans hardcoded colors — hex, named colors, `rgb()/hsl()/oklch()/lab()`, even across multiple lines — plus px font sizes and `var(--x)` references that no CSS file defines. It scans **CSS files and inline `style={{ … }}` in `.ts/.tsx`**. Allowed: `var(--token)`, `color-mix(in srgb, var(--token) N%, transparent)`, `light-dark(var(--a), var(--b))`, and neutral black/white rgba shines.
3. Typography scale (`--text-3xs` … `--text-2xl`, `--text-hero`) is rem-based on a 9→28px grid. Longform body `--text-md`, buttons/nav `--text-xs`, meta/eyebrows `--text-2xs`/`--text-3xs`.
4. **Global density knob: `--root-font-size`** (default `112%` readability preset, baseline 16px = `100%`). One line scales every token-driven size app-wide; spacing tokens follow.
-5. **Theming:** components read only `var(--*)` and never know the theme. Toggle synchronizes both `document.documentElement.dataset.theme` and `.dark`/`.light` classes.
+5. **Theming:** components read only `var(--*)` and never know the theme. A pre-paint script in `index.html` applies `localStorage['ns-theme']` or `prefers-color-scheme` before first paint (no flash); `useTheme` in `Topbar.tsx` reads what it set and keeps `data-theme` **and** the `.dark`/`.light` classes in sync on toggle.
## Smart Utils & AI Workflow
- **Class helper**: `src/utils/cn.ts` — zero-dependency classnames merging utility for conditional component styles.
+- **Timer helper**: `src/utils/useTimeout.ts` — `schedule(fn, ms)` / `clear()`; auto-cancels pending timers on unmount. Never fire-and-forget a raw `setTimeout` in a component.
- **Smart CSS**: `src/utils.css` — ready-to-use helpers (`.dot-canvas`, `.surface-card`, `.surface-sidebar`, `.badge-*`, `.hover-lift`, `.kbd-tag`, `.skeleton-pulse`, `.progress-*`, `.ui-table-*`, `.dialog-*`, `.sheet-*`, `.alert-*`, `.ui-tabs-*`, `.switch-*`).
-- **Composable Primitives**: `src/components/ui/` (`Dialog`, `Sheet`, `Tabs`, `Alert`, `EmptyState`, `Table`, `Progress`, `Switch`, `Badge`, `Card`, `StatCard`, `Skeleton`, `Tooltip`, `Kbd`, `Icon`).
+- **Overlays — one primitive**: `src/components/ui/Overlay.tsx` owns focus trap, Escape-to-close, scroll lock, focus restore, and conditional `aria-labelledby`/`aria-describedby` (wired only when a title/description is actually rendered). `Dialog` and `Sheet` are thin skins over it; `SearchPalette` reuses `trapTabKey` + `useOverlayLifecycle` from `src/components/ui/overlayBehavior.ts`. Build new modals on Overlay, not from scratch.
+- **Composable Primitives**: `src/components/ui/` (`Overlay`, `Dialog`, `Sheet`, `Tabs`, `Alert`, `EmptyState`, `Table`, `Progress`, `Switch`, `Badge`, `Card`, `StatCard`, `Skeleton`, `Tooltip`, `Kbd`, `Avatar`, `Spinner`; `Icon` lives at `src/components/Icon.tsx` and is re-exported from the barrel).
+- **StatCard**: `trend` is sentiment (badge color), `direction` is the arrow and defaults to the sign of `change` — a "good" latency drop is `trend="up"` with `change="-4.2ms"` rendering a green `↓ -4.2ms`.
- **Reusable Application Patterns**: `src/components/examples/` (`MetricsDashboard`, `SettingsPanel`, `ServiceTable`, `DeploymentModal`).
-- **Icon Library**: `lucide-react` — 1,400+ tree-shaken SVGs with uniform stroke and color inheritance (`` or direct import).
+- **Icon Library**: the registry in `src/components/icon-registry.ts` maps every `IconName` to a lucide component (strictly typed — typos fail `tsc`, unknown names at runtime **throw in dev** so the error sink surfaces them). 41 curated icons power the showcase; `lucide-react` ships 1,400+ for direct import.
## Structure
@@ -42,22 +53,44 @@ src/
index.css ← fonts, tokens & utils import, base resets
App.css ← application component styles (token-driven, plain CSS)
App.tsx ← page composition & showcase layout
+ main.tsx ← mounts App inside the ErrorBoundary, installs the dev error sink
+ dev/
+ ErrorBoundary.tsx, errorSink.ts ← render runtime errors into #dev-errors
utils/
cn.ts ← conditional classnames utility
+ useTimeout.ts ← auto-cleaning setTimeout hook
components/
- ui/ ← composable UI primitives (Dialog, Sheet, Tabs, Alert, EmptyState, Table, Progress, Switch, Badge, Card, StatCard, Skeleton, Tooltip, Kbd, Icon)
- examples/ ← reusable pattern components (MetricsDashboard, SettingsPanel, ServiceTable, DeploymentModal)
+ ui/ ← composable UI primitives (Overlay, Dialog, Sheet, Tabs, Alert,
+ EmptyState, Table, Progress, Switch, Badge, Card, StatCard,
+ Skeleton, Tooltip, Kbd, Avatar, Spinner) + overlayBehavior.ts
+ icon-registry.ts← the one icon name → lucide component map (single source)
+ examples/ ← reusable pattern components (MetricsDashboard, SettingsPanel,
+ ServiceTable, DeploymentModal)
AIWorkflowSection.tsx ← interactive pattern switcher & showcase
- IconMatrix.tsx ← interactive Lucide icon explorer & search
- Topbar.tsx ← owns the theme toggle (.dark & data-theme)
+ SearchPalette.tsx ← ⌘K command palette (Overlay behavior + combobox a11y)
+ TypographySection.tsx ← type-scale showcase
+ IconMatrix.tsx ← interactive Lucide icon explorer (renders iconRegistry)
+ Icon.tsx ← component (types from icon-registry.ts)
+ Topbar.tsx ← owns the theme toggle (data-theme + .dark/.light)
Hero.tsx, CodeSample.tsx, RightRail.tsx, ButtonMatrix.tsx,
Sidebar.tsx, Section.tsx, FormField.tsx, TokenGrid.tsx,
- MarkdownRenderer.tsx, CodeEditor.tsx, Button.tsx, Icon.tsx
+ MarkdownRenderer.tsx, CodeEditor.tsx, Button.tsx
data/
- library.ts ← demo content and color token data for the showcase
+ library.ts ← demo content, nav groups (patternId routing) & color tokens
+tests/ ← vitest suites (jsdom): overlay, icon-registry, useTimeout,
+ check-tokens guard
scripts/check-tokens.mjs ← token contract guard (part of npm run check)
```
+## Parallel subagent workflow
+
+Multiple agents can work the same checkout concurrently if tasks are **file-disjoint**:
+
+- Each agent gets an explicit allow-list of files; shared files (barrels like `components/ui/index.ts`, `utils.css`, `App.tsx`) are edited by the orchestrator only, after agents finish.
+- Agents verify with `npx oxlint `, `npx tsc --noEmit -p tsconfig.app.json`, and `node scripts/check-tokens.mjs` — never `npm run check`/`vite build` in parallel (incremental build info and `dist/` would race).
+- Read-only investigation tasks use the read-only spawn mode; they can run freely alongside writers.
+- New primitives: agents create self-contained files (component + own `.css` import if keyframes/classes are needed); the orchestrator registers exports and showcase usage.
+
## Iteration workflow for visual changes
1. Change the token (or component markup) — never a literal.
diff --git a/README.md b/README.md
index d6af7e3..6b8176f 100644
--- a/README.md
+++ b/README.md
@@ -1,32 +1,63 @@
-# React + TypeScript + Vite
+# Northstar Design System
-This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules.
+A token-driven design system showcase built for AI-assisted development workflows.
+React 19 · Vite 8 · TypeScript (strict) · Vitest · oxlint.
-Currently, two official plugins are available:
+## Quick start
-- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
-- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
-
-## React Compiler
-
-The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
-
-## Expanding the Oxlint configuration
-
-If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`:
-
-```json
-{
- "$schema": "./node_modules/oxlint/configuration_schema.json",
- "plugins": ["react", "typescript", "oxc"],
- "options": {
- "typeAware": true
- },
- "rules": {
- "react/rules-of-hooks": "error",
- "react/only-export-components": ["warn", { "allowConstantExport": true }]
- }
-}
+```bash
+npm install
+npm run dev # dev server on 0.0.0.0:5173
```
-See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories.
+## The quality gate
+
+```bash
+npm run check
+```
+
+One command, four gates, in order:
+
+1. **`oxlint`** — lint over every source file.
+2. **`scripts/check-tokens.mjs`** — the token contract: bans hardcoded colors
+ (hex / named / `rgb()` / `hsl()` / `oklch()` / `lab()`), px font sizes and
+ undefined `var(--x)` references — in **both** CSS files and inline
+ `style={{ … }}` objects. Values may span lines; comments are ignored.
+3. **`vitest run`** — unit + component tests (`tests/`).
+4. **`tsc -b && vite build`** — strict typecheck, then production build.
+
+CI runs the same gate on every push and PR (`.github/workflows/ci.yml`).
+
+## Design tokens
+
+`src/tokens.css` is the single source of truth. Every color, font size,
+spacing, radius and shadow goes through a `var(--token)` — components never
+hardcode values. Themes are plain CSS: `:root[data-theme='light']` /
+`.light` and `:root[data-theme='dark']` / `.dark`, with a pre-paint script in
+`index.html` that applies the saved (or OS-preferred) theme before first paint.
+
+The global density knob is `--root-font-size` — one line scales the whole app.
+
+## Layout
+
+```
+src/
+ tokens.css ← lab() design tokens, dark & light themes
+ index.css ← fonts, token import, base resets
+ utils.css ← token-compliant utility classes
+ App.css ← application showcase styles
+ components/
+ ui/ ← composable primitives (Overlay, Dialog, Sheet, Tabs, Alert, …)
+ icon-registry.ts← the one icon name → lucide component map
+ examples/ ← reusable patterns (MetricsDashboard, SettingsPanel, …)
+ dev/ ← ErrorBoundary + dev error sink (#dev-errors)
+ utils/ ← cn(), useTimeout()
+tests/ ← vitest suites (jsdom)
+scripts/
+ check-tokens.mjs ← token contract guard
+```
+
+## For agents
+
+Read [`AGENTS.md`](./AGENTS.md) before changing anything — it documents the
+token contract, the overlay/icon registries, and the parallel-subagent rules.
diff --git a/index.html b/index.html
index 9321b7f..2210280 100644
--- a/index.html
+++ b/index.html
@@ -4,10 +4,33 @@
- design-system
+
+ Northstar Design System
+
+