Files
design-system/AGENTS.md
T
developer d6c7464170 feat(app): add AppStateProvider for shared global settings
Central context for the settings a template app shares instead of prop
drilling: theme, viewMode, sidebarCollapsed and the search palette, with
the ⌘K/Ctrl+K shortcut moved out of App.

- settings persist on user action only (an effect-written theme would
  freeze the OS preference for users who never toggled)
- theme still boots from the <html data-theme> attribute the pre-paint
  script set; viewMode mirrors to data-view-mode as the audience-preset
  hook and deliberately gates nothing yet
- Topbar: viewMode segmented control, theme toggle and search now read
  the context; useAppState throws outside the provider
- tests/appState.test.tsx: defaults, persistence, rehydration, garbage
  storage values, ⌘K shortcut
2026-09-15 18:34:22 +00:00

100 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Northstar Design System — agent handbook
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 + 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
- `vite.config.ts` allows `*.preview.k6n.net`; expose port **5173** via `repo_instance_expose` for a preview URL.
- Removing `allowedHosts` breaks preview ingress with a 403 ("host not allowed"). Don't.
## 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 (`: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. 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
- **Global app state**: `src/app/` — `AppStateProvider` (mounted in `main.tsx`) owns the few settings the whole app shares: `theme`, `viewMode`, `sidebarCollapsed`, `searchOpen`, plus the ⌘K/Ctrl+K shortcut. Read them anywhere with `useAppState()` (import from `./app` or `../app`); it throws outside the provider on purpose. Add a setting to `appState.ts` (types) + `AppStateProvider.tsx` (state, DOM mirror, `STORAGE_KEYS` persistence) — never thread a new prop chain. Section-local state (active tab, selected pattern) stays in its section. Persisted settings are written **only on user action**, so an untouched app keeps following `prefers-color-scheme`. `theme` always boots from the `<html data-theme>` attribute the pre-paint script set (storage key `ns-theme` is shared with `index.html`); `viewMode` mirrors to `<html data-view-mode>` and currently gates nothing — it is the audience preset hook for per-persona views.
- **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-*`).
- **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**: 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
```
src/
tokens.css ← lab() design tokens + dark & light themes (edit this first)
utils.css ← token-compliant smart CSS utility classes
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 (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
SearchPalette.tsx ← ⌘K command palette (Overlay behavior + combobox a11y)
TypographySection.tsx ← type-scale showcase
IconMatrix.tsx ← interactive Lucide icon explorer (renders iconRegistry)
Icon.tsx ← <Icon name="…" /> 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
data/
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 <files>`, `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.
2. `npm run check` until green.
3. Verify live: dev server + preview URL; screenshot at desktop width; toggle light/dark via the topbar button.