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)
25 KiB
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, andaccentall 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
: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
.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
: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:
// 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:
<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.
: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:
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.
: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:
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
cd packages/web
npm install geist
CSS Import
Add to packages/web/src/index.css before @tailwind directives:
@import 'geist/font/sans.css';
@import 'geist/font/mono.css';
Tailwind Config
// 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
:root {
--radius: 0.375rem; /* 6px, down from 0.5rem/8px */
}
Tailwind border-radius mapping (shadcn/ui convention):
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
: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
.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:
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-schememedia query - Persist choice in
localStoragekey:cw-theme - Values:
light,dark,system - Default (no stored value):
system
Class Toggling
Add/remove .dark class on the <html> element:
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:
<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(LucideSun),Monitor(LucideMonitor),Moon(LucideMoon) - Active state:
bg-accenthighlight - Location: right side of header bar, same row as logo
React Context
// 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
-
--primaryis no longer achromatic. Any component usingbg-primarywill render indigo instead of black/white. This is intentional — buttons, links, and focus rings gain brand color. -
--secondary,--muted,--accentare 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
-
--ringis now indigo, not foreground-colored. All focus outlines will be indigo. -
--radiusshrinks from 8px to 6px. All rounded corners tighten slightly. -
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,
nonein 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)