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)
This commit is contained in:
769
docs/wireframes/v2/theme.md
Normal file
769
docs/wireframes/v2/theme.md
Normal file
@@ -0,0 +1,769 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user