Files
Codewalkers/docs/wireframes/v2/theme.md
Lukas May 478a7f18e9 docs: Add v2 wireframes and theme specification
14 files in docs/wireframes/v2/ addressing 13 UX gaps from v1:
- Theme spec with indigo brand, status tokens, terminal/diff tokens,
  dark mode, Geist typography, 6px radius, layered shadows
- Wireframes for all pages with loading/error/empty states
- Shared component specs (SaveIndicator, EmptyState, ErrorState,
  CommandPalette, ThemeToggle)
2026-03-02 18:13:17 +09:00

770 lines
25 KiB
Markdown

# 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.
## 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)
### 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 */
}
```
### Dark Mode
```css
.dark {
--background: 240 6% 7%; /* #111114 */
--foreground: 240 5% 96%; /* #F4F4F5 */
--card: 240 5% 10%; /* #19191D — surface level 1 */
--card-foreground: 240 5% 96%;
--popover: 240 5% 13%; /* #202025 — surface level 2 */
--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%;
}
```
### Surface Hierarchy (Dark Mode)
Three-tier elevation model using lightness alone (no box shadows in dark mode):
```
Level 0 --background 240 6% 7% #111114 Page background
Level 1 --card 240 5% 10% #19191D Cards, panels, sidebars
Level 2 --popover 240 5% 13% #202025 Dropdowns, tooltips, command palette
```
Visual reference:
```
+============================================================================+
| Level 0 (7%) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ |
| |
| +------------------------------------------------------------------+ |
| | Level 1 (10%) ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ | |
| | | |
| | +----------------------------------------------+ | |
| | | Level 2 (13%) ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ | | |
| | +----------------------------------------------+ | |
| | | |
| +------------------------------------------------------------------+ |
| |
+============================================================================+
```
---
## 2. Status Tokens
Six semantic status sets with bg/fg/border/dot variants for both light and dark modes.
### 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
<span className="bg-status-active-bg text-status-active-fg border border-status-active-border">
[RUNNING]
</span>
<span className="h-2 w-2 rounded-full bg-status-success-dot" /> {/* 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 */
}
.dark {
--terminal-bg: 240 5% 10%; /* 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%;
}
```
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))',
},
```
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
### 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`).
---
## 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
```css
.dark {
--shadow-xs: none;
--shadow-sm: none;
--shadow-md: none;
--shadow-lg: none;
--shadow-xl: none;
}
```
Dark mode uses **borders + surface lightness hierarchy** instead of box shadows. This avoids the muddy appearance of shadows on dark backgrounds.
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` |
---
## 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 `<html>` 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 `<head>` in `index.html` before any stylesheets:
```html
<script>
(function() {
var t = localStorage.getItem('cw-theme') || 'system';
var d = t === 'dark' || (t === 'system' && matchMedia('(prefers-color-scheme: dark)').matches);
if (d) document.documentElement.classList.add('dark');
})();
</script>
```
### Toggle Component
3-state toggle in the header bar, right-aligned:
```
+-------------------------------------------+
| [logo] Codewalk District [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<Theme>(
() => (localStorage.getItem('cw-theme') as Theme) || 'system'
);
const isDark = useMemo(() => {
if (theme === 'system') {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
return theme === 'dark';
}, [theme]);
const setTheme = useCallback((t: Theme) => {
setThemeState(t);
applyTheme(t);
}, []);
// Listen for system theme changes when in 'system' mode
useEffect(() => {
const mq = window.matchMedia('(prefers-color-scheme: dark)');
const handler = () => { if (theme === 'system') applyTheme('system'); };
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, setTheme, isDark }}>
{children}
</ThemeContext.Provider>
);
}
```
---
## 9. Migration Notes
### Token Changes: v1 to v2
#### Light Mode (`:root`)
| Token | v1 | v2 | Notes |
|-------|----|----|-------|
| `--background` | `0 0% 100%` | `0 0% 99%` | Slight warmth |
| `--foreground` | `0 0% 3.9%` | `240 6% 10%` | Zinc-tinted, not pure black |
| `--card` | `0 0% 100%` | `0 0% 100%` | Unchanged |
| `--card-foreground` | `0 0% 3.9%` | `240 6% 10%` | Matches foreground |
| `--popover` | `0 0% 100%` | `0 0% 100%` | Unchanged |
| `--popover-foreground` | `0 0% 3.9%` | `240 6% 10%` | Matches foreground |
| `--primary` | `0 0% 9%` | `239 84% 67%` | **Black to indigo** |
| `--primary-foreground` | `0 0% 98%` | `0 0% 100%` | Near-white to white |
| `--secondary` | `0 0% 96.1%` | `240 5% 96%` | Zinc-tinted |
| `--secondary-foreground` | `0 0% 9%` | `240 4% 16%` | Zinc-tinted |
| `--muted` | `0 0% 96.1%` | `240 5% 93%` | **Now 93%, not 96.1%** |
| `--muted-foreground` | `0 0% 45.1%` | `240 4% 46%` | Zinc-tinted |
| `--accent` | `0 0% 96.1%` | `226 100% 97%` | **Grey to indigo tint** |
| `--accent-foreground` | `0 0% 9%` | `239 84% 67%` | **Black to indigo** |
| `--destructive` | `0 84.2% 60.2%` | `0 84% 60%` | Rounded values |
| `--destructive-foreground` | `0 0% 98%` | `0 0% 100%` | Near-white to white |
| `--border` | `0 0% 89.8%` | `240 6% 90%` | Zinc-tinted |
| `--input` | `0 0% 89.8%` | `240 6% 90%` | Zinc-tinted |
| `--ring` | `0 0% 3.9%` | `239 84% 67%` | **Black to indigo** |
| `--radius` | `0.5rem` | `0.375rem` | **8px to 6px** |
#### Dark Mode (`.dark`)
| Token | v1 | v2 | Notes |
|-------|----|----|-------|
| `--background` | `0 0% 3.9%` | `240 6% 7%` | Lighter, zinc-tinted |
| `--foreground` | `0 0% 98%` | `240 5% 96%` | Zinc-tinted |
| `--card` | `0 0% 3.9%` | `240 5% 10%` | **Elevated above bg** |
| `--card-foreground` | `0 0% 98%` | `240 5% 96%` | Zinc-tinted |
| `--popover` | `0 0% 3.9%` | `240 5% 13%` | **Elevated above card** |
| `--popover-foreground` | `0 0% 98%` | `240 5% 96%` | Zinc-tinted |
| `--primary` | `0 0% 98%` | `239 84% 67%` | **White to indigo** |
| `--primary-foreground` | `0 0% 9%` | `0 0% 100%` | Black to white |
| `--secondary` | `0 0% 14.9%` | `240 4% 16%` | Zinc-tinted |
| `--secondary-foreground` | `0 0% 98%` | `240 5% 96%` | Zinc-tinted |
| `--muted` | `0 0% 14.9%` | `240 4% 16%` | Zinc-tinted |
| `--muted-foreground` | `0 0% 63.9%` | `240 5% 65%` | Zinc-tinted |
| `--accent` | `0 0% 14.9%` | `239 40% 16%` | **Grey to dark indigo** |
| `--accent-foreground` | `0 0% 98%` | `239 84% 75%` | **White to light indigo** |
| `--destructive` | `0 62.8% 30.6%` | `0 63% 31%` | Rounded values |
| `--destructive-foreground` | `0 0% 98%` | `0 0% 100%` | Near-white to white |
| `--border` | `0 0% 14.9%` | `240 4% 16%` | Zinc-tinted |
| `--input` | `0 0% 14.9%` | `240 4% 16%` | Zinc-tinted |
| `--ring` | `0 0% 83.1%` | `239 84% 67%` | **Grey to indigo** |
### Key Breaking Changes
1. **`--primary` is no longer achromatic.** Any component using `bg-primary` will render indigo instead of black/white. This is intentional — buttons, links, and focus rings gain brand color.
2. **`--secondary`, `--muted`, `--accent` are now distinct values.** Components relying on them being identical need review:
- `--secondary` = neutral surface (96% light)
- `--muted` = recessed surface (93% light) — **3% darker than secondary**
- `--accent` = indigo-tinted surface (97% light, blue hue) — **colored, not grey**
3. **`--ring` is now indigo**, not foreground-colored. All focus outlines will be indigo.
4. **`--radius` shrinks from 8px to 6px.** All rounded corners tighten slightly.
5. **Dark mode card/popover surfaces are elevated.** v1 had card = background (both 3.9%). v2 separates them: background 7% < card 10% < popover 13%.
### New Token Categories
These are entirely new in v2 — no v1 equivalents exist:
- **Status tokens** (24 light + 24 dark = 48 new properties)
- **Terminal tokens** (10 new properties)
- **Diff tokens** (14 new properties)
- **Shadow tokens** (5 new properties, `none` in dark)
### File Changes Required
| File | Changes |
|------|---------|
| `packages/web/src/index.css` | Replace all `:root` and `.dark` token values; add status, terminal, diff, shadow tokens; add Geist font imports |
| `packages/web/tailwind.config.ts` | Add `fontFamily`, `colors.status`, `colors.terminal`, `colors.diff`, `boxShadow`, `borderRadius` extensions |
| `packages/web/package.json` | Add `geist` dependency |
| `packages/web/index.html` | Add dark mode inline script in `<head>` |
| `packages/web/src/lib/theme.tsx` | New file: ThemeProvider context |
| `packages/web/src/layouts/AppLayout.tsx` | Add ThemeToggle to header |
| `packages/web/src/App.tsx` (or root) | Wrap with ThemeProvider |
---
## Source
- `packages/web/src/index.css` (current theme)
- `packages/web/tailwind.config.ts` (current Tailwind config)