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

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, 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

: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-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:

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 (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

// 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)