# BooCode — Theme System v1 Standalone BooCode (`/opt/boocode/`). Tailwind v4 + shadcn nova preset. 18 preset themes × 2 modes (dark/light) = 36 palettes. User-selectable in Settings only. Persists to `settings` table. ----- ## 1. Theme list (locked) | # | id | Display name | Family | Mode default | |---|---------------------|-------------------|----------------|----------------------| | 1 | `obsidian` | Obsidian | Charcoal/Black | dark (default theme) | | 2 | `gunmetal` | Gunmetal | Charcoal/Black | dark | | 3 | `espresso` | Espresso | Brown/Earth | dark | | 4 | `volcanic-brown` | Volcanic Brown | Brown/Earth | dark | | 5 | `copper` | Copper | Orange/Amber | dark | | 6 | `gold` | Gold | Orange/Amber | dark | | 7 | `oxblood` | Oxblood | Red/Crimson | dark | | 8 | `crimson` | Crimson | Red/Crimson | dark | | 9 | `elderflower` | Elderflower | Purple/Violet | dark | | 10| `plum` | Plum | Purple/Violet | dark | | 11| `steel-pink` | Steel Pink | Pink/Magenta | dark | | 12| `fuchsia-noir` | Fuchsia Noir | Pink/Magenta | dark | | 13| `matrix` | Matrix | Green | dark | | 14| `sage` | Sage | Green | dark | | 15| `ivory` | Ivory | Light | light (always) | | 16| `chalk` | Chalk | Light | light (always) | | 17| `cobalt` | Cobalt | Blue | dark | | 18| `midnight-sapphire` | Midnight Sapphire | Blue | dark | **Default on first load:** `obsidian` (dark). **Light variants:** every dark theme ships a paired light variant. `ivory` and `chalk` have no dark variant — they are light-only. ----- ## 2. Storage model ### Schema change Additive only. In `apps/server/src/schema.sql`: ```sql ALTER TABLE settings ADD COLUMN IF NOT EXISTS theme_id TEXT NOT NULL DEFAULT 'obsidian'; ALTER TABLE settings ADD COLUMN IF NOT EXISTS theme_mode TEXT NOT NULL DEFAULT 'dark' CHECK (theme_mode IN ('dark', 'light', 'system')); ``` ### API surface Extend `GET /api/settings` and `PATCH /api/settings`. No new routes. `GET` response includes `theme_id`, `theme_mode`. `PATCH` accepts both. Validation: - `theme_id` must be one of the 18 ids listed in section 1. - `theme_mode` ∈ `{dark, light, system}`. - Reject otherwise with 400. ----- ## 3. CSS token layer Tailwind v4 + shadcn nova uses CSS custom properties. ### File layout ``` apps/web/src/styles/ ├── globals.css # existing Tailwind entrypoint └── themes/ ├── obsidian.css ├── gunmetal.css ├── espresso.css ├── volcanic-brown.css ├── copper.css ├── gold.css ├── oxblood.css ├── crimson.css ├── elderflower.css ├── plum.css ├── steel-pink.css ├── fuchsia-noir.css ├── matrix.css ├── sage.css ├── ivory.css ├── chalk.css ├── cobalt.css └── midnight-sapphire.css ``` Each per-theme file declares `.theme-` (light tokens) and `.theme-.dark` (dark tokens), overriding the shadcn nova CSS variables. `ivory.css` and `chalk.css` declare only the light selector. `globals.css` imports all 18 theme files after the existing Tailwind/shadcn `@import` lines. ### Tokens overridden per theme ``` --background --foreground --card --card-foreground --popover --popover-foreground --primary --primary-foreground --secondary --secondary-foreground --muted --muted-foreground --accent --accent-foreground --destructive --destructive-foreground --border --input --ring --sidebar --sidebar-foreground --sidebar-primary --sidebar-primary-foreground --sidebar-accent --sidebar-accent-foreground --sidebar-border --sidebar-ring ``` `--radius` is locked at `0.5rem` (not per-theme). `--destructive` stays in the red family across all themes — error states are not theme-shifted. ### Anchor-to-token mapping Five anchor swatches per theme map as follows: ``` background ← anchor 1 (deepest) card / popover / sidebar ← anchor 2 (surface) border / input / muted ← anchor 3 (line) muted-foreground ← anchor 4 (dimmed text) primary / accent / ring / sidebar-primary / sidebar-accent ← anchor 5 (accent) foreground ← computed: anchor-5 hue, ~92% L, ~25% S (warm tint for warm themes, cool for cool) sidebar-foreground ← same as foreground sidebar-border ← same as border (anchor 3) sidebar-ring ← same as ring (anchor 5) destructive ← red family — dark mode: #dc2626, light mode: #b91c1c *-foreground variants ← derived high-contrast against parent ``` ### Dark anchor values ``` obsidian #0c0c0e #15151a #1f1f23 #6b6b75 #8b5cf6 gunmetal #0d1117 #161b22 #21262d #7d8590 #388bfd espresso #1c1410 #241a14 #2e2218 #8a7058 #c8a880 volcanic-brown #140906 #1e0e0a #2e1610 #7a4030 #cc4a1a copper #100800 #1c1408 #2e1f0a #8a6040 #b87333 gold #0e0800 #1a1200 #2a1f00 #a07c30 #d4af37 oxblood #0a0303 #180606 #2a0808 #7a3028 #8b1a1a crimson #0e0404 #1a0808 #2e0a0a #8a3030 #dc143c elderflower #100818 #1c1024 #2c1830 #8a78a0 #b89cd8 plum #0c0814 #180e20 #241830 #7a4878 #8e4585 steel-pink #0e0408 #1a080e #2e0c1a #9a4070 #cc33aa fuchsia-noir #0a0610 #14081a #2a0c2e #8a3878 #ff1493 matrix #000a00 #031403 #0a200a #208030 #00ff41 sage #0a0e08 #141a10 #1e2e1a #7a8870 #9caf88 cobalt #020817 #061434 #0c2244 #3060a0 #0047ab midnight-sapphire #02050e #060c1f #0e1a36 #4a6088 #1e3a8a ``` ### Light-only anchors `ivory` and `chalk` use these values directly as the light palette: ``` ivory #fdfcf8 #f5f2e8 #e8e4d8 #8a8478 #3a3328 chalk #fafaf7 #f0f0ec #e5e5e0 #75756e #2a2a28 ``` ### Light variants of the 16 dark themes - lightest anchor → background - accent darkens ~15% (reduce HSL lightness by 15 percentage points) - foreground → near-black tinted toward family hue - surfaces, borders scale up in lightness symmetrically ----- ## 4. Mode resolution (dark/light/system) ```ts function resolvedMode(mode: 'dark' | 'light' | 'system'): 'dark' | 'light' { if (mode === 'system') { return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } return mode; } ``` - If `theme_id` is light-only (`ivory`, `chalk`) and the resolved mode is `dark`, fall back to `obsidian` dark. - Otherwise apply ``. System-mode listener: subscribe to `matchMedia('(prefers-color-scheme: dark)').addEventListener('change', ...)` only when `theme_mode === 'system'`. ----- ## 5. Frontend wiring ### `apps/web/src/lib/theme.ts` (new) Exports: - `THEMES`: const array of `{ id, name, family, supportsDark, supportsLight, anchors }` - `applyTheme(id, mode)`: writes class to ``, updates localStorage cache - `useTheme()`: hook reading `theme_id` + `theme_mode` from settings; applies on mount and on change; owns the matchMedia listener (only mounted when mode === 'system') ### `apps/web/src/App.tsx` Call `useTheme()` at the top of the App component, before children, so the theme is applied before any child renders. ### `apps/web/index.html` Inline `