Files
Codewalkers/docs/wireframes/v2/theme.md
Lukas May 0ff65b0b02 feat: Rename application from "Codewalk District" to "Codewalkers"
Update all user-facing strings (HTML title, manifest, header logo,
browser title updater), code comments, and documentation references.
Folder name retained as-is.
2026-03-05 12:05:08 +01:00

1068 lines
44 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.
<!-- v2.1 --> **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)
<!-- v2.1 --> **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%) */ <!-- v2.1 widened from 10% -->
--card-foreground: 240 5% 96%;
--popover: 240 5% 16%; /* #272729 — surface level 2 (+5%) */ <!-- v2.1 widened from 13% -->
--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)
<!-- v2.1 --> 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%)
```
<!-- v2.1 --> **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.
<!-- v2.1 --> **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.
<!-- v2.1 --> **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
<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 */
/* 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 */ <!-- v2.1 updated from 10% to match new card -->
--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
<!-- v2.1 --> **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`).
<!-- v2.1 --> **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
<!-- v2.1 --> 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` |
---
<!-- v2.1 -->
## 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;
}
}
```
---
<!-- v2.1 -->
## 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)',
},
```
---
<!-- v2.1 -->
## 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.
---
<!-- v2.1 -->
## 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 `<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] 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<Theme>(
() => (localStorage.getItem('cw-theme') as Theme) || 'system'
);
// v2.1 — Track system preference as reactive state so isDark updates on OS theme change
const [systemDark, setSystemDark] = useState(
() => window.matchMedia('(prefers-color-scheme: dark)').matches
);
const isDark = theme === 'dark' || (theme === 'system' && systemDark);
const setTheme = useCallback((t: Theme) => {
setThemeState(t);
applyTheme(t);
}, []);
// Listen for system theme changes
useEffect(() => {
const mq = window.matchMedia('(prefers-color-scheme: dark)');
const handler = (e: MediaQueryListEvent) => {
setSystemDark(e.matches);
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>
);
}
export const useTheme = () => useContext(ThemeContext);
```
<!-- v2.1 --> **Bug fix in original:** `isDark` was a `useMemo` depending only on `[theme]`, which meant it would never re-derive when the OS theme toggled while in `system` mode. The fix introduces `systemDark` as tracked state that the `useEffect` updates, ensuring `isDark` is always correct. Also exported the `useTheme` hook — the original omitted it.
---
## 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% 11%` | **Elevated above bg (+4%)** | <!-- v2.1 widened -->
| `--card-foreground` | `0 0% 98%` | `240 5% 96%` | Zinc-tinted |
| `--popover` | `0 0% 3.9%` | `240 5% 16%` | **Elevated above card (+5%)** | <!-- v2.1 widened -->
| `--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 with wider steps: background 7% < card 11% < popover 16% < surface-3 21%. <!-- v2.1 widened from 10/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** (22 light + 22 dark = 44 new properties) <!-- v2.1 expanded from 10 -->
- **Diff tokens** (14 new properties)
- **Shadow tokens** (5 new properties, dark mode uses inset highlights + glow) <!-- v2.1 updated -->
- **Transition/animation tokens** (9 new properties) <!-- v2.1 added -->
- **Z-index scale** (10 new properties) <!-- v2.1 added -->
- **Extended indigo scale** (10 new properties, light mode only) <!-- v2.1 added -->
- **Surface level 3** (1 new property, dark mode only) <!-- v2.1 added -->
### File Changes Required
<!-- v2.1 expanded with implementation order and specific details -->
| # | File | Changes | Depends On |
|---|------|---------|------------|
| 1 | `packages/web/package.json` | Add `geist` dependency | — |
| 2 | `packages/web/src/index.css` | Replace all `:root` and `.dark` token values; add status, terminal, diff, shadow, transition, z-index tokens; add Geist font imports; add focus-visible styles; add reduced-motion media query | #1 |
| 3 | `packages/web/tailwind.config.ts` | Add `fontFamily`, `colors.status`, `colors.terminal`, `colors.diff`, `boxShadow`, `borderRadius`, `transitionDuration`, `transitionTimingFunction`, `zIndex` extensions | #2 |
| 4 | `packages/web/index.html` | Add dark mode inline script in `<head>` before stylesheets | — |
| 5 | `packages/web/src/lib/theme.tsx` | New file: ThemeProvider context + `useTheme` hook | — |
| 6 | `packages/web/src/App.tsx` (or root) | Wrap with `<ThemeProvider>` | #5 |
| 7 | `packages/web/src/layouts/AppLayout.tsx` | Add ThemeToggle to header (Sun/Monitor/Moon) | #5 |
| 8 | Existing components using `bg-primary` | Audit: buttons, badges, links — now indigo instead of black/white | #2 |
| 9 | `AgentOutputViewer` component | Migrate to terminal tokens (`bg-terminal`, `text-terminal-fg`, etc.) | #2, #3 |
| 10 | All status badge/dot components | Migrate from hardcoded colors to `bg-status-*-bg text-status-*-fg` | #2, #3 |
---
## Source
- `packages/web/src/index.css` (current theme)
- `packages/web/tailwind.config.ts` (current Tailwind config)
---
## Design Review Notes
**Reviewer:** Design system review, v2.1 pass
**Date:** 2026-03-02
**Method:** Inline edits marked with `<!-- v2.1 -->` comments throughout the document.
### What's Strong
1. **The v1 problem analysis is honest and complete.** Calling out that `secondary`, `muted`, and `accent` all resolve to the same gray — that's the kind of specificity that builds trust. Good.
2. **Status token structure is well-considered.** The bg/fg/border/dot quad covers every common badge pattern without requiring component-specific tokens. The status-to-entity mapping table is exactly what a developer needs.
3. **Terminal always-dark pattern is correct.** Not negotiable — terminal output on a white background is a usability crime. The spec nails this.
4. **Diff tokens are clean and complete.** Hunk/add/remove with border variants covers the review tab needs without overcomplicating.
5. **Migration table is thorough.** Showing v1 vs v2 side-by-side for every token eliminates guesswork.
### What Was Fixed (inline)
1. **Dark mode surface hierarchy widened from 3% to 4-5% steps.** The original 7→10→13 was too subtle. On a typical display at reasonable brightness, 3% lightness difference in the sub-15% range is nearly invisible. Changed to 7→11→16→21 (four tiers). Added Level 3 (`--surface-3`) for the command palette.
2. **Dark mode shadows: no longer `none`.** Pure nothing left elevated components feeling flat. Added subtle inset top-highlights (simulating edge light) and faint black outer glow for dialogs/command palette. The technique: `inset 0 1px 0 hsl(0 0% 100% / 0.05)` gives a crisp top edge without the grey-wash problem.
3. **Terminal tokens expanded.** Added: `--terminal-cursor`, `--terminal-selection-bg`, `--terminal-link`, `--terminal-warning`, `--terminal-line-number`, plus full ANSI color override set (8 colors). The original 10 tokens were not enough for a real terminal renderer — any agent output with ANSI escape codes would fall through to browser defaults.
4. **ThemeProvider `isDark` bug fixed.** The `useMemo` depended only on `[theme]`, which meant toggling the OS dark mode while in `system` mode would not update `isDark` until the next render triggered by something else. Replaced with tracked `systemDark` state that the `useEffect` listener updates.
5. **`useTheme` hook export was missing.** The context was defined but no consumer hook was exported. Fixed.
### What Was Added (inline)
1. **Design rationale for indigo.** The original spec just declared `#6366F1` without explaining why. Added positioning rationale (between blue and violet = trust + intelligence) and hue collision analysis against status tokens.
2. **Extended indigo scale (50-900).** The base palette had exactly one indigo value (`--primary`). That's not enough for hover states, gradient backgrounds, selected rows, badge variants, or any treatment that needs a lighter/darker brand shade. Added 10-step scale from indigo-50 to indigo-900.
3. **Transition/animation tokens (Section 7b).** Completely missing from the original. A design system without motion tokens guarantees inconsistency — one dev uses 150ms ease-in-out, another uses 300ms linear, and the UI feels schizophrenic. Defined 5 durations, 4 easing curves, and a reduced-motion media query.
4. **Z-index scale (Section 7c).** Missing, and critical for a layered UI with sidebars, dropdowns, modals, toasts, and a command palette all potentially visible simultaneously. Defined 10 named layers from `base` to `tooltip`.
5. **Focus-visible styles (Section 7d).** Missing. A keyboard-first tool *must* have visible focus indicators. Added global `:focus-visible` styles with a double-ring technique for inputs.
6. **Responsive breakpoints (Section 7e).** Missing. Documented that the app is desktop-first with a 1024px minimum, and mapped Tailwind breakpoints to panel layout behavior.
7. **Font rationale.** Geist is a defensible choice for 2026, but the original didn't explain *why*. Added comparison against Inter, Berkeley Mono, JetBrains Mono, and IBM Plex.
8. **Migration file changes table expanded.** Added dependency ordering (which file depends on which), implementation step numbers, and three new rows: component audit for `bg-primary` consumers, AgentOutputViewer terminal migration, and status badge migration.
### Still Missing / Future Considerations
1. **Spacing scale.** The spec doesn't define a spacing system beyond Tailwind defaults. For a dense mission-control UI, consider documenting preferred spacing patterns: 4px micro-gaps (between icon + label), 8px component padding, 12px card padding, 16px section gaps. Not blocking — Tailwind defaults work — but documenting the conventions prevents drift.
2. **Icon sizing tokens.** With Lucide icons throughout, standardize on 16px (inline/badges), 20px (buttons), 24px (navigation). Not in this spec's scope but should be in a shared-components doc.
3. **Color-blind testing.** The status token hues were chosen for maximum separation, but the amber-38 (warning) vs red-0 (error) distinction is the classic deuteranopia problem. Recommendation: rely on shape (icons/badges) and position in addition to color. The spec should note this explicitly — "never communicate status through color alone."
4. **`--chart-*` tokens.** If the app ever shows usage graphs, agent activity timelines, or cost charts, you will need a categorical color palette (6-8 colors, perceptually distinct). Not needed now, but reserve the `--chart-*` namespace.
5. **Print styles.** Not a priority for a real-time orchestration tool, but if anyone ever tries to print a proposal or review diff, the terminal always-dark tokens will produce solid black pages. Consider a `@media print` override that forces terminal tokens to light values.