# Theme — Design System v2 Complete design system overhaul. Replaces the achromatic shadcn/ui defaults with an indigo-branded, status-aware, dark-mode-first token system. **Design rationale:** Indigo (#6366F1) was chosen deliberately — it sits between blue (trust/tech) and violet (creativity/intelligence), making it semantically appropriate for an AI orchestration tool. It is also perceptually distinct from every status hue in the system (blue-210, green-142, amber-38, red-0, purple-270), preventing brand/status confusion. ## Current State (v1 Problems) ``` :root (light) .dark --secondary: 0 0% 96.1% --secondary: 0 0% 14.9% --muted: 0 0% 96.1% <-- SAME --muted: 0 0% 14.9% <-- SAME --accent: 0 0% 96.1% <-- SAME --accent: 0 0% 14.9% <-- SAME --primary: 0 0% 9% <-- black --primary: 0 0% 98% <-- white --ring: 0 0% 3.9% <-- black --ring: 0 0% 83.1% <-- grey --radius: 0.5rem (8px) ``` Problems: - Zero brand identity (achromatic everywhere) - `secondary`, `muted`, and `accent` all resolve to the **same** gray value - No status color tokens (active/success/warning/error) - No terminal tokens (AgentOutputViewer always-dark surface) - No diff tokens (review components) - No dark mode toggle - Ring color matches foreground instead of brand color - 8px radius is visually heavy --- ## 1. Color System — Indigo Brand (#6366F1) **Palette range note:** The core palette covers: indigo (brand), zinc (neutrals), and destructive red. For richer UI expression, an extended indigo scale is provided below the base tokens for use in gradients, illustrations, and emphasis treatments where `--primary` alone is insufficient. ### Light Mode ```css :root { --background: 0 0% 99%; /* #FCFCFC — slight warmth, not pure white */ --foreground: 240 6% 10%; /* #18181B — zinc-900 equivalent */ --card: 0 0% 100%; /* #FFFFFF */ --card-foreground: 240 6% 10%; --popover: 0 0% 100%; /* #FFFFFF */ --popover-foreground: 240 6% 10%; --primary: 239 84% 67%; /* #6366F1 — indigo-500 */ --primary-foreground: 0 0% 100%; /* white on indigo */ --secondary: 240 5% 96%; /* #F4F4F5 — zinc-100 */ --secondary-foreground: 240 4% 16%;/* #27272A — zinc-800 */ --muted: 240 5% 93%; /* #ECECEE — distinguishable from secondary */ --muted-foreground: 240 4% 46%; /* #71717A — zinc-500 */ --accent: 226 100% 97%; /* #EEF0FF — light indigo tint */ --accent-foreground: 239 84% 67%; /* indigo on tint */ --destructive: 0 84% 60%; /* #EF4444 — red-500 */ --destructive-foreground: 0 0% 100%; --border: 240 6% 90%; /* #E4E4E7 — zinc-200 */ --input: 240 6% 90%; --ring: 239 84% 67%; /* indigo focus ring */ --radius: 0.375rem; /* 6px — down from 8px */ /* v2.1 — Extended indigo scale for gradients, highlights, hover states */ --indigo-50: 226 100% 97%; /* #EEF0FF — same as accent bg */ --indigo-100: 228 96% 93%; /* #DDD6FE */ --indigo-200: 232 92% 86%; /* #C4B5FD */ --indigo-300: 235 88% 78%; /* #A78BFA */ --indigo-400: 237 86% 72%; /* #818CF8 */ --indigo-500: 239 84% 67%; /* #6366F1 — primary */ --indigo-600: 243 75% 59%; /* #4F46E5 */ --indigo-700: 245 58% 51%; /* #4338CA */ --indigo-800: 244 47% 42%; /* #3730A3 */ --indigo-900: 242 47% 34%; /* #312E81 */ } ``` ### Dark Mode ```css .dark { --background: 240 6% 7%; /* #111114 */ --foreground: 240 5% 96%; /* #F4F4F5 */ --card: 240 5% 11%; /* #1B1B1F — surface level 1 (+4%) */ --card-foreground: 240 5% 96%; --popover: 240 5% 16%; /* #272729 — surface level 2 (+5%) */ --popover-foreground: 240 5% 96%; --primary: 239 84% 67%; /* #6366F1 — same indigo */ --primary-foreground: 0 0% 100%; --secondary: 240 4% 16%; /* #27272A */ --secondary-foreground: 240 5% 96%; --muted: 240 4% 16%; /* #27272A */ --muted-foreground: 240 5% 65%; /* #A1A1AA — zinc-400 */ --accent: 239 40% 16%; /* dark indigo tint */ --accent-foreground: 239 84% 75%; /* lighter indigo on dark */ --destructive: 0 63% 31%; --destructive-foreground: 0 0% 100%; --border: 240 4% 16%; --input: 240 4% 16%; --ring: 239 84% 67%; /* v2.1 — Surface level 3 for command palette and stacked overlays */ --surface-3: 240 5% 21%; /* #333336 */ } ``` ### Surface Hierarchy (Dark Mode) Four-tier elevation model using lightness alone (no box shadows in dark mode). Steps widened from 3% to 4-5% for clearer visual separation — the original 7→10→13 (3% steps) was too subtle on most displays, especially at low brightness. ``` Level 0 --background 240 6% 7% #111114 Page background Level 1 --card 240 5% 11% #1B1B1F Cards, panels, sidebars (+4%) Level 2 --popover 240 5% 16% #272729 Dropdowns, tooltips (+5%) Level 3 --surface-3 240 5% 21% #333336 Command palette, overlaid menus (+5%) ``` **Why widen the steps?** On a typical IPS panel at 200 nits, 3% lightness difference in the sub-15% range is nearly imperceptible. 4-5% steps guarantee that Level 0 vs Level 1 reads as distinct surfaces without squinting. Level 3 is new — the command palette and stacked dialogs need a surface above popover. Visual reference: ``` +============================================================================+ | Level 0 (7%) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ | | | | +------------------------------------------------------------------+ | | | Level 1 (11%) ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ | | | | | | | | +----------------------------------------------+ | | | | | Level 2 (16%) ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ | | | | | | | | | | | | +----------------------------------+ | | | | | | | Level 3 (21%) ████████████████ | | | | | | | +----------------------------------+ | | | | | +----------------------------------------------+ | | | | | | | +------------------------------------------------------------------+ | | | +============================================================================+ ``` --- ## 2. Status Tokens Six semantic status sets with bg/fg/border/dot variants for both light and dark modes. **Hue separation analysis:** The six status hues are: active (210 blue), success (142 green), warning (38 amber), error (0 red), neutral (240 zinc), urgent (270 purple). The tightest hue gap is active-210 vs brand-indigo-239 (29 degrees apart) — acceptable because status bg/fg pairs are never used in brand contexts. The urgent-270 vs active-210 gap is 60 degrees, which is sufficient but can be tight under protanopia. Consider `--status-urgent` at 285-290 (magenta-shifted) if accessibility testing reveals confusion. The primary concern: `urgent` (purple) may read as "special/brand-related" since indigo is nearby. If this becomes an issue during implementation, shift urgent to 330 (hot pink/fuchsia) for maximum perceptual distance from both blue and indigo. **Missing status: `queued`.** With 10+ agents and many tasks, a "queued/dispatched but not yet started" state is common. Consider adding a 7th token: `queued` at hue 195 (cyan) — sits between active-blue and success-green, reads as "ready but not running." ### Light Mode | Status | Use Case | bg | fg | border | dot | |--------|----------|----|----|--------|-----| | `active` | Running agents, in-progress tasks | `210 100% 95%` | `210 100% 40%` | `210 100% 80%` | `210 100% 50%` | | `success` | Completed tasks, healthy services | `142 72% 94%` | `142 72% 29%` | `142 72% 80%` | `142 72% 45%` | | `warning` | Pending approval, exhausted accounts | `38 92% 95%` | `38 92% 30%` | `38 92% 80%` | `38 92% 50%` | | `error` | Failed tasks, crashed agents | `0 84% 95%` | `0 84% 40%` | `0 84% 80%` | `0 84% 50%` | | `neutral` | Exited agents, idle states | `240 5% 96%` | `240 4% 46%` | `240 6% 90%` | `240 4% 46%` | | `urgent` | Blocked tasks, attention needed | `270 91% 95%` | `270 91% 40%` | `270 91% 80%` | `270 91% 55%` | ### Dark Mode | Status | bg | fg | border | dot | |--------|----|----|--------|-----| | `active` | `210 100% 12%` | `210 100% 70%` | `210 100% 25%` | `210 100% 50%` | | `success` | `142 72% 10%` | `142 72% 65%` | `142 72% 22%` | `142 72% 45%` | | `warning` | `38 92% 10%` | `38 92% 65%` | `38 92% 22%` | `38 92% 50%` | | `error` | `0 84% 12%` | `0 84% 65%` | `0 84% 25%` | `0 84% 50%` | | `neutral` | `240 4% 16%` | `240 5% 65%` | `240 4% 22%` | `240 5% 50%` | | `urgent` | `270 91% 12%` | `270 91% 70%` | `270 91% 25%` | `270 91% 55%` | ### CSS Custom Properties ```css :root { /* active */ --status-active-bg: 210 100% 95%; --status-active-fg: 210 100% 40%; --status-active-border: 210 100% 80%; --status-active-dot: 210 100% 50%; /* success */ --status-success-bg: 142 72% 94%; --status-success-fg: 142 72% 29%; --status-success-border: 142 72% 80%; --status-success-dot: 142 72% 45%; /* warning */ --status-warning-bg: 38 92% 95%; --status-warning-fg: 38 92% 30%; --status-warning-border: 38 92% 80%; --status-warning-dot: 38 92% 50%; /* error */ --status-error-bg: 0 84% 95%; --status-error-fg: 0 84% 40%; --status-error-border: 0 84% 80%; --status-error-dot: 0 84% 50%; /* neutral */ --status-neutral-bg: 240 5% 96%; --status-neutral-fg: 240 4% 46%; --status-neutral-border: 240 6% 90%; --status-neutral-dot: 240 4% 46%; /* urgent */ --status-urgent-bg: 270 91% 95%; --status-urgent-fg: 270 91% 40%; --status-urgent-border: 270 91% 80%; --status-urgent-dot: 270 91% 55%; } .dark { --status-active-bg: 210 100% 12%; --status-active-fg: 210 100% 70%; --status-active-border: 210 100% 25%; --status-active-dot: 210 100% 50%; --status-success-bg: 142 72% 10%; --status-success-fg: 142 72% 65%; --status-success-border: 142 72% 22%; --status-success-dot: 142 72% 45%; --status-warning-bg: 38 92% 10%; --status-warning-fg: 38 92% 65%; --status-warning-border: 38 92% 22%; --status-warning-dot: 38 92% 50%; --status-error-bg: 0 84% 12%; --status-error-fg: 0 84% 65%; --status-error-border: 0 84% 25%; --status-error-dot: 0 84% 50%; --status-neutral-bg: 240 4% 16%; --status-neutral-fg: 240 5% 65%; --status-neutral-border: 240 4% 22%; --status-neutral-dot: 240 5% 50%; --status-urgent-bg: 270 91% 12%; --status-urgent-fg: 270 91% 70%; --status-urgent-border: 270 91% 25%; --status-urgent-dot: 270 91% 55%; } ``` ### Tailwind Utility Class Mapping Extend `tailwind.config.ts` to expose status tokens as utilities: ```ts // tailwind.config.ts theme: { extend: { colors: { status: { 'active-bg': 'hsl(var(--status-active-bg))', 'active-fg': 'hsl(var(--status-active-fg))', 'active-border': 'hsl(var(--status-active-border))', 'active-dot': 'hsl(var(--status-active-dot))', 'success-bg': 'hsl(var(--status-success-bg))', 'success-fg': 'hsl(var(--status-success-fg))', 'success-border': 'hsl(var(--status-success-border))', 'success-dot': 'hsl(var(--status-success-dot))', 'warning-bg': 'hsl(var(--status-warning-bg))', 'warning-fg': 'hsl(var(--status-warning-fg))', 'warning-border': 'hsl(var(--status-warning-border))', 'warning-dot': 'hsl(var(--status-warning-dot))', 'error-bg': 'hsl(var(--status-error-bg))', 'error-fg': 'hsl(var(--status-error-fg))', 'error-border': 'hsl(var(--status-error-border))', 'error-dot': 'hsl(var(--status-error-dot))', 'neutral-bg': 'hsl(var(--status-neutral-bg))', 'neutral-fg': 'hsl(var(--status-neutral-fg))', 'neutral-border': 'hsl(var(--status-neutral-border))', 'neutral-dot': 'hsl(var(--status-neutral-dot))', 'urgent-bg': 'hsl(var(--status-urgent-bg))', 'urgent-fg': 'hsl(var(--status-urgent-fg))', 'urgent-border': 'hsl(var(--status-urgent-border))', 'urgent-dot': 'hsl(var(--status-urgent-dot))', }, }, }, } ``` Usage in components: ```tsx [RUNNING] {/* green dot */} ``` ### Status-to-Entity Mapping | Entity State | Status Token | |--------------|-------------| | Agent: running | `active` | | Agent: waiting_for_input | `warning` | | Agent: exited / completed | `neutral` | | Agent: crashed | `error` | | Task: in_progress | `active` | | Task: completed | `success` | | Task: pending | `neutral` | | Task: pending_approval | `warning` | | Task: blocked | `urgent` | | Task: failed | `error` | | Account: active | `success` | | Account: exhausted | `warning` | | Preview: building | `active` | | Preview: running | `success` | | Preview: failed | `error` | | Preview: stopped | `neutral` | | Server health: connected | `success` | | Server health: disconnected | `error` | --- ## 3. Terminal Tokens For `AgentOutputViewer` — always-dark surface even in light mode (terminal aesthetic), card-level in dark mode. ```css :root { --terminal-bg: 240 6% 7%; /* always dark — #111114 */ --terminal-fg: 120 100% 80%; /* green-on-dark */ --terminal-muted: 240 5% 55%; /* dimmed text */ --terminal-border: 240 4% 16%; --terminal-selection: 239 84% 67%; /* indigo selection, use with /0.2 alpha */ --terminal-system: 240 5% 55%; /* [System] badge text */ --terminal-tool: 217 91% 60%; /* [Tool Call] blue accent */ --terminal-result: 142 72% 45%; /* [Result] green accent */ --terminal-error: 0 84% 60%; /* [Error] red accent */ /* v2.1 — Additional terminal tokens */ --terminal-cursor: 120 100% 65%; /* blinking cursor color — bright green */ --terminal-selection-bg: 239 84% 67% / 0.25; /* indigo at 25% opacity */ --terminal-link: 217 91% 70%; /* clickable file paths, URLs */ --terminal-warning: 38 92% 60%; /* [Warning] amber accent */ --terminal-line-number: 240 5% 35%; /* gutter line numbers */ /* v2.1 — ANSI color overrides (for raw terminal output rendering) */ --terminal-ansi-black: 240 6% 7%; --terminal-ansi-red: 0 84% 60%; --terminal-ansi-green: 142 72% 45%; --terminal-ansi-yellow: 38 92% 60%; --terminal-ansi-blue: 217 91% 60%; --terminal-ansi-magenta: 270 91% 65%; --terminal-ansi-cyan: 195 80% 55%; --terminal-ansi-white: 240 5% 85%; } .dark { --terminal-bg: 240 5% 11%; /* matches card surface */ --terminal-fg: 120 100% 80%; --terminal-muted: 240 5% 55%; --terminal-border: 240 4% 16%; --terminal-selection: 239 84% 67%; --terminal-system: 240 5% 55%; --terminal-tool: 217 91% 60%; --terminal-result: 142 72% 45%; --terminal-error: 0 84% 60%; /* v2.1 — Additional terminal tokens (same values — always-dark context) */ --terminal-cursor: 120 100% 65%; --terminal-selection-bg: 239 84% 67% / 0.25; --terminal-link: 217 91% 70%; --terminal-warning: 38 92% 60%; --terminal-line-number: 240 5% 35%; /* v2.1 — ANSI colors (same as light — terminal is always dark) */ --terminal-ansi-black: 240 6% 7%; --terminal-ansi-red: 0 84% 60%; --terminal-ansi-green: 142 72% 45%; --terminal-ansi-yellow: 38 92% 60%; --terminal-ansi-blue: 217 91% 60%; --terminal-ansi-magenta: 270 91% 65%; --terminal-ansi-cyan: 195 80% 55%; --terminal-ansi-white: 240 5% 85%; } ``` Tailwind extension: ```ts terminal: { DEFAULT: 'hsl(var(--terminal-bg))', fg: 'hsl(var(--terminal-fg))', muted: 'hsl(var(--terminal-muted))', border: 'hsl(var(--terminal-border))', system: 'hsl(var(--terminal-system))', tool: 'hsl(var(--terminal-tool))', result: 'hsl(var(--terminal-result))', error: 'hsl(var(--terminal-error))', // v2.1 additions cursor: 'hsl(var(--terminal-cursor))', link: 'hsl(var(--terminal-link))', warning: 'hsl(var(--terminal-warning))', 'line-number': 'hsl(var(--terminal-line-number))', }, ``` Visual reference (AgentOutputViewer): ``` +------------------------------------------------------------------------+ | bg: --terminal-bg | | | | [System] Starting task... <-- text: --terminal-system | | | | > I'll examine the existing code. <-- text: --terminal-fg | | | | | [Tool Call] Read <-- border/text: --terminal-tool | | file_path: src/auth/index.ts <-- text: --terminal-muted | | | | | [Result] <-- border/text: --terminal-result | | import { Router } from 'express'; <-- text: --terminal-muted | | | | | [Error] <-- border/text: --terminal-error | | Permission denied: /etc/hosts | | | +------------------------------------------------------------------------+ ``` --- ## 4. Diff Tokens For review tab components — file diffs, proposal diffs, merge previews. ```css :root { --diff-add-bg: 142 72% 94%; /* light green background */ --diff-add-fg: 142 72% 29%; /* dark green text */ --diff-add-border: 142 72% 80%; --diff-remove-bg: 0 84% 95%; /* light red background */ --diff-remove-fg: 0 84% 40%; /* dark red text */ --diff-remove-border: 0 84% 80%; --diff-hunk-bg: 226 100% 97%; /* indigo-tinted hunk header */ } .dark { --diff-add-bg: 142 72% 10%; --diff-add-fg: 142 72% 65%; --diff-add-border: 142 72% 22%; --diff-remove-bg: 0 84% 12%; --diff-remove-fg: 0 84% 65%; --diff-remove-border: 0 84% 25%; --diff-hunk-bg: 239 40% 16%; } ``` Tailwind extension: ```ts diff: { 'add-bg': 'hsl(var(--diff-add-bg))', 'add-fg': 'hsl(var(--diff-add-fg))', 'add-border': 'hsl(var(--diff-add-border))', 'remove-bg': 'hsl(var(--diff-remove-bg))', 'remove-fg': 'hsl(var(--diff-remove-fg))', 'remove-border':'hsl(var(--diff-remove-border))', 'hunk-bg': 'hsl(var(--diff-hunk-bg))', }, ``` Visual reference: ``` +------------------------------------------------------------------------+ | @@ -12,6 +12,8 @@ function setup() bg: --diff-hunk-bg | | const router = Router(); | | router.get('/health', ...); | | + router.get('/oauth/callback', ...); bg: --diff-add-bg | | + router.get('/oauth/login', ...); fg: --diff-add-fg | | - router.get('/legacy', ...); bg: --diff-remove-bg | | export default router; fg: --diff-remove-fg | +------------------------------------------------------------------------+ ``` --- ## 5. Typography — Geist Sans + Geist Mono **Why Geist?** Geist is Vercel's open-source typeface (MIT licensed), purpose-built for developer tooling interfaces. It has tighter default letter-spacing than Inter, making it denser at small sizes — critical for the mission-control aesthetic where every pixel counts. The mono variant has consistent glyph widths optimized for terminal output, not just code editors. **Alternatives considered:** Inter (too ubiquitous, wider spacing), Berkeley Mono (license cost, proprietary), JetBrains Mono (excellent for code but no matching sans-serif), IBM Plex (good but the duo feels corporate, not dev-tool). Geist wins because both weights ship as one package with matching x-heights. ### Installation ```bash cd packages/web npm install geist ``` ### CSS Import Add to `packages/web/src/index.css` before `@tailwind` directives: ```css @import 'geist/font/sans.css'; @import 'geist/font/mono.css'; ``` ### Tailwind Config ```ts // packages/web/tailwind.config.ts import defaultTheme from 'tailwindcss/defaultTheme'; export default { theme: { extend: { fontFamily: { sans: ['Geist Sans', ...defaultTheme.fontFamily.sans], mono: ['Geist Mono', ...defaultTheme.fontFamily.mono], }, }, }, }; ``` ### Usage - Body text: `font-sans` (Geist Sans) — applied via Tailwind base - Code blocks, terminal output: `font-mono` (Geist Mono) - Agent names: `font-mono text-sm` - Timestamps, metadata: `font-mono text-xs text-muted-foreground` No font size scale changes; keep Tailwind defaults (`text-xs` through `text-4xl`). **Density guidance:** For mission-control panels, prefer `text-xs` (12px) for metadata/timestamps, `text-sm` (14px) for body content in dense tables, and `text-base` (16px) only for primary reading areas (page editor, proposal content). Headers rarely need `text-2xl` or above — `text-lg` to `text-xl` is the sweet spot for section headers in a dense UI. --- ## 6. Radius — 6px Base ```css :root { --radius: 0.375rem; /* 6px, down from 0.5rem/8px */ } ``` Tailwind border-radius mapping (shadcn/ui convention): ```ts borderRadius: { lg: 'var(--radius)', // 6px — cards, dialogs md: 'calc(var(--radius) - 2px)', // 4px — buttons, inputs sm: 'calc(var(--radius) - 4px)', // 2px — badges, small elements }, ``` v1 vs v2: | Token | v1 | v2 | |-------|----|----| | `--radius` | `0.5rem` (8px) | `0.375rem` (6px) | | `rounded-lg` | 8px | 6px | | `rounded-md` | 6px | 4px | | `rounded-sm` | 4px | 2px | --- ## 7. Shadows — Layered System ### Light Mode ```css :root { --shadow-xs: 0 1px 2px hsl(0 0% 0% / 0.04); --shadow-sm: 0 1px 3px hsl(0 0% 0% / 0.06), 0 1px 2px hsl(0 0% 0% / 0.04); --shadow-md: 0 4px 6px hsl(0 0% 0% / 0.06), 0 2px 4px hsl(0 0% 0% / 0.04); --shadow-lg: 0 10px 15px hsl(0 0% 0% / 0.06), 0 4px 6px hsl(0 0% 0% / 0.04); --shadow-xl: 0 20px 25px hsl(0 0% 0% / 0.08), 0 8px 10px hsl(0 0% 0% / 0.04); } ``` ### Dark Mode Elevated surfaces in dark mode use subtle inset highlights (top-edge light leak) and faint outer glow rather than traditional drop shadows. Pure `none` left elevated elements feeling flat and disconnected, especially stacked dialogs and the command palette. ```css .dark { --shadow-xs: none; --shadow-sm: inset 0 1px 0 hsl(0 0% 100% / 0.04); /* subtle top-edge highlight */ --shadow-md: inset 0 1px 0 hsl(0 0% 100% / 0.05), 0 2px 8px hsl(0 0% 0% / 0.3); /* highlight + ambient */ --shadow-lg: inset 0 1px 0 hsl(0 0% 100% / 0.06), 0 4px 16px hsl(0 0% 0% / 0.4); /* for dialogs */ --shadow-xl: inset 0 1px 0 hsl(0 0% 100% / 0.06), 0 8px 32px hsl(0 0% 0% / 0.5); /* command palette */ } ``` Dark mode uses **borders + surface lightness hierarchy** as the primary elevation signal. The inset highlights act as secondary reinforcement — they simulate a top-edge light source at extremely low opacity so they never look muddy. The outer shadow is pure black at high opacity, which reads as "depth" on dark backgrounds without the grey-wash problem of traditional shadows. Tailwind extension: ```ts boxShadow: { xs: 'var(--shadow-xs)', sm: 'var(--shadow-sm)', md: 'var(--shadow-md)', lg: 'var(--shadow-lg)', xl: 'var(--shadow-xl)', }, ``` Usage guidance: | Component | Shadow Level | |-----------|-------------| | Buttons (hover) | `xs` | | Cards, panels | `sm` | | Dropdowns, popovers | `md` | | Dialogs, modals | `lg` | | Command palette | `xl` | --- ## 7b. Transition & Animation Tokens Motion tokens ensure consistent feel across the UI. Mission control tools should feel *snappy* — never sluggish, never jarring. ```css :root { /* Durations */ --duration-instant: 50ms; /* tooltip show, dot pulse */ --duration-fast: 100ms; /* button hover, toggle states */ --duration-normal: 200ms; /* panel open/close, tab switch */ --duration-slow: 350ms; /* dialog enter, page transition */ --duration-glacial: 500ms; /* skeleton shimmer cycle */ /* Easing curves */ --ease-default: cubic-bezier(0.2, 0, 0, 1); /* emphatic decel — most UI transitions */ --ease-in: cubic-bezier(0.4, 0, 1, 1); /* element exiting — slide out */ --ease-out: cubic-bezier(0, 0, 0.2, 1); /* element entering — slide in */ --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);/* playful overshoot — toasts, notifications */ /* Composites (for Tailwind arbitrary values or direct use) */ --transition-colors: color, background-color, border-color, text-decoration-color, fill, stroke; --transition-transform: transform; } ``` Tailwind extension: ```ts transitionDuration: { instant: 'var(--duration-instant)', fast: 'var(--duration-fast)', normal: 'var(--duration-normal)', slow: 'var(--duration-slow)', }, transitionTimingFunction: { default: 'var(--ease-default)', in: 'var(--ease-in)', out: 'var(--ease-out)', spring: 'var(--ease-spring)', }, ``` Usage: `transition-colors duration-fast ease-default`, `transition-transform duration-normal ease-out`. **Reduced motion:** Respect `prefers-reduced-motion`: ```css @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } } ``` --- ## 7c. Z-Index Scale Predictable stacking order prevents z-index wars. Every layer is explicitly named. ```css :root { --z-base: 0; --z-raised: 1; /* cards with hover lift */ --z-sticky: 10; /* sticky headers, tab bars */ --z-sidebar: 20; /* collapsible sidebar */ --z-dropdown: 30; /* select menus, popovers */ --z-overlay: 40; /* backdrop behind modals */ --z-modal: 50; /* dialogs, modals */ --z-toast: 60; /* toast notifications */ --z-command: 70; /* command palette (above everything) */ --z-tooltip: 80; /* tooltips (top of stack) */ } ``` Tailwind extension: ```ts zIndex: { base: 'var(--z-base)', raised: 'var(--z-raised)', sticky: 'var(--z-sticky)', sidebar: 'var(--z-sidebar)', dropdown: 'var(--z-dropdown)', overlay: 'var(--z-overlay)', modal: 'var(--z-modal)', toast: 'var(--z-toast)', command: 'var(--z-command)', tooltip: 'var(--z-tooltip)', }, ``` --- ## 7d. Focus-Visible Styles Keyboard navigation must be unambiguous. All interactive elements get a visible focus ring on keyboard interaction only (not mouse clicks). ```css /* Global focus-visible style */ *:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: 2px; border-radius: var(--radius); } /* Remove default outline for mouse users */ *:focus:not(:focus-visible) { outline: none; } /* High-contrast focus for inputs (inset ring) */ input:focus-visible, textarea:focus-visible, select:focus-visible { outline: none; box-shadow: 0 0 0 2px hsl(var(--background)), 0 0 0 4px hsl(var(--ring)); } ``` The double-ring technique (background gap + ring color) ensures the focus indicator is visible on both light and dark backgrounds, and against any surface color. --- ## 7e. Responsive Breakpoints The app is a desktop-first power tool. Mobile is explicitly out of scope — the minimum supported width is 1024px. These breakpoints govern panel layout collapse behavior: ``` Breakpoint Width Behavior sm ≥640px (unused — below minimum) md ≥768px (unused — below minimum) lg ≥1024px Minimum supported. Sidebar collapsed by default. xl ≥1280px Sidebar open by default. Two-column layouts. 2xl ≥1536px Three-column layouts. Agent grid shows 4+ cards per row. ``` No custom breakpoints needed — Tailwind defaults are sufficient. The app shell uses `min-w-[1024px]` to enforce the minimum. --- ## 8. Dark Mode Implementation ### Default Behavior - First load: detect `prefers-color-scheme` media query - Persist choice in `localStorage` key: `cw-theme` - Values: `light`, `dark`, `system` - Default (no stored value): `system` ### Class Toggling Add/remove `.dark` class on the `` element: ```ts function applyTheme(theme: 'light' | 'dark' | 'system') { const root = document.documentElement; const isDark = theme === 'dark' || (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches); root.classList.toggle('dark', isDark); localStorage.setItem('cw-theme', theme); } ``` ### Inline Script (prevent flash) Add to `
` in `index.html` before any stylesheets: ```html ``` ### Toggle Component 3-state toggle in the header bar, right-aligned: ``` +-------------------------------------------+ | [logo] Codewalkers [Sun|Monitor|Moon] | +-------------------------------------------+ States: [*Sun*] Monitor Moon = light mode forced Sun [*Monitor*] Moon = system preference Sun Monitor [*Moon*] = dark mode forced ``` - Icons: `Sun` (Lucide `Sun`), `Monitor` (Lucide `Monitor`), `Moon` (Lucide `Moon`) - Active state: `bg-accent` highlight - Location: right side of header bar, same row as logo ### React Context ```ts // packages/web/src/lib/theme.tsx type Theme = 'light' | 'dark' | 'system'; const ThemeContext = createContext<{ theme: Theme; setTheme: (t: Theme) => void; isDark: boolean; }>(null!); function ThemeProvider({ children }: { children: ReactNode }) { const [theme, setThemeState] = useState