--- phase: 41-diagrams-icons-theme-engine plan: "03" type: execute wave: 2 depends_on: ["41-01"] files_modified: - server/src/services/renderers/theme-renderer.ts - server/src/__tests__/theme-renderer.test.ts - server/src/services/nexus-settings.ts - server/src/__tests__/nexus-settings-custom-theme.test.ts autonomous: true requirements: [THEME-01, THEME-02, THEME-03, THEME-05, THEME-06, THEME-07] must_haves: truths: - "buildPalette returns 7 named roles with dark and light variants from a single hex seed" - "All palette computations use OKLCH via culori -- no HSL intermediates anywhere" - "WCAG AA contrast is validated per foreground/background pair" - "Four export formatters produce CSS variables, Tailwind config, VS Code theme, and JSON strings" - "nexus-settings.json schema accepts optional customTheme field with seed and palette" artifacts: - path: "server/src/services/renderers/theme-renderer.ts" provides: "OKLCH palette engine with WCAG validation and 4 export formatters" exports: ["renderThemePalette", "buildPalette", "exportToCss", "exportToTailwind", "exportToVSCode", "exportToJson"] - path: "server/src/__tests__/theme-renderer.test.ts" provides: "Tests for palette generation, WCAG validation, and export formats" - path: "server/src/services/nexus-settings.ts" provides: "Extended schema with customTheme field" contains: "customTheme" key_links: - from: "server/src/services/renderers/theme-renderer.ts" to: "culori" via: "converter('oklch'), formatHex" pattern: "converter.*oklch" - from: "server/src/services/renderers/theme-renderer.ts" to: "wcag-contrast" via: "wcagContrast.hex(fg, bg)" pattern: "wcagContrast\\.hex" --- Implement the OKLCH theme palette engine: seed color to 7-role palette (dark + light variants), WCAG AA validation, four export formatters (CSS, Tailwind, VS Code, JSON), and extend nexus-settings.json to persist custom themes. Purpose: Pure computational core of the theme engine -- no UI, no Playwright, no external services. Highly testable. Output: Theme renderer with export formatters, extended settings schema, comprehensive tests. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/41-diagrams-icons-theme-engine/41-RESEARCH.md @.planning/phases/41-diagrams-icons-theme-engine/41-01-SUMMARY.md ```typescript export interface RenderResult { filename: string; contentType: string; buffer: Buffer; } export interface ThemePaletteBundle { type: "theme-palette-bundle"; seedHex: string; palette: PaletteRole[]; exports: { css: string; tailwind: string; vscode: string; json: string }; } export interface PaletteRole { name: string; dark: { oklch: string; hex: string; wcagAA: boolean }; light: { oklch: string; hex: string; wcagAA: boolean }; } ``` ```typescript export const nexusSettingsSchema = z.object({ mode: z.enum(NEXUS_MODES).default("both"), voiceEnabled: z.boolean().default(false), voiceMode: z.enum(VOICE_MODES).default("text"), telegramToken: z.string().optional(), piperBinaryPath: z.string().optional(), whisperBinaryPath: z.string().optional(), }); ``` Task 1: OKLCH palette engine with WCAG validation and export formatters + tests server/src/services/renderers/theme-renderer.ts, server/src/__tests__/theme-renderer.test.ts server/src/services/renderers/types.ts, server/src/services/nexus-settings.ts, ui/src/index.css - buildPalette("#1e66f5") returns array of 7 PaletteRole objects with names: background, surface, overlay, text, accent-1, accent-2, accent-3 - Each PaletteRole has dark.oklch starting with "oklch(" and dark.hex starting with "#" - Each PaletteRole has light.oklch starting with "oklch(" and light.hex starting with "#" - dark.wcagAA for background role: true if contrast ratio of dark background hex against dark text hex >= 4.5 - text role always has wcagAA: true (it IS the text, not measured against text) - buildPalette with different seed hues produces different hex values but same role names - exportToCss(palette) contains "--background:" and "--foreground:" CSS custom property declarations - exportToTailwind(palette) contains valid JavaScript/TypeScript object with colors key - exportToVSCode(palette) contains "editor.background" and "editor.foreground" keys - exportToJson(palette) is valid parseable JSON - renderThemePalette({ seedHex: "#1e66f5" }) returns RenderResult with contentType "application/json" 1. Create `server/src/__tests__/theme-renderer.test.ts` FIRST (TDD red): - Test buildPalette returns 7 roles with correct names - Test all roles have dark and light variants with oklch and hex strings - Test WCAG AA computation: pick a known seed where we can verify contrast manually - Test text role always has wcagAA: true - Test different seeds produce different hex values - Test no HSL values anywhere in output (grep for "hsl" in all string fields) - Test exportToCss output contains "--background:" and "oklch(" values - Test exportToTailwind output contains "colors" object - Test exportToVSCode output is valid JSON with "editor.background" key - Test exportToJson is parseable JSON matching the palette structure - Test renderThemePalette returns valid RenderResult 2. Create `server/src/services/renderers/theme-renderer.ts`: - Import `converter`, `formatHex` from "culori" and `wcagContrast` from "wcag-contrast" - `const toOklch = converter("oklch")` - Define DARK_ROLES and LIGHT_ROLES arrays (L, C values from research): ``` DARK: bg(0.14, 0.010), surface(0.17, 0.012), overlay(0.22, 0.015), text(0.93, 0.008), accent-1(0.72, 0.15), accent-2(0.65, 0.13), accent-3(0.58, 0.10) LIGHT: bg(0.94, 0.005), surface(0.91, 0.008), overlay(0.85, 0.012), text(0.28, 0.008), accent-1(0.55, 0.16), accent-2(0.48, 0.14), accent-3(0.40, 0.11) ``` - `buildPalette(seedHex: string): PaletteRole[]`: - Parse seed with toOklch, extract hue (h ?? 0) - For each role pair (dark/light): compute hex via formatHex({ mode: "oklch", l, c, h: hue }) - Compute WCAG AA: for non-text roles, check contrast of role hex against the text role hex (dark text for dark variant, light text for light variant). Text role itself always wcagAA: true. - Return 7 PaletteRole objects - `exportToCss(palette: PaletteRole[], variant: "dark" | "light"): string`: - Map role names to CSS tokens: background->--background, surface->--card, overlay->--secondary, text->--foreground, accent-1->--primary, accent-2->--accent, accent-3->--muted - Output `:root { --background: oklch(...); ... }` format - `exportToTailwind(palette: PaletteRole[]): string`: - Output a `module.exports = { theme: { extend: { colors: { ... } } } }` config snippet - `exportToVSCode(palette: PaletteRole[]): string`: - Output JSON with `editor.background`, `editor.foreground`, `activityBar.background`, etc. - `exportToJson(palette: PaletteRole[]): string`: - Output `JSON.stringify({ palette, generated: ISO date }, null, 2)` - `renderThemePalette(input: Record): Promise`: - Extract seedHex from input - Build palette, build all 4 exports - Return ThemePaletteBundle as JSON buffer CRITICAL: No HSL anywhere. All color math in OKLCH. All hex conversions via culori formatHex from oklch mode. cd /opt/nexus && pnpm --filter server exec vitest run src/__tests__/theme-renderer.test.ts - `grep -c "oklch" server/src/services/renderers/theme-renderer.ts` returns at least 10 - `grep -ci "hsl" server/src/services/renderers/theme-renderer.ts` returns 0 (no HSL anywhere) - `grep "wcagContrast" server/src/services/renderers/theme-renderer.ts` matches - `grep "converter.*oklch" server/src/services/renderers/theme-renderer.ts` matches - `grep "formatHex" server/src/services/renderers/theme-renderer.ts` matches - `grep "theme-palette-bundle" server/src/services/renderers/theme-renderer.ts` matches - All tests in theme-renderer.test.ts pass Palette engine generates 7 roles with dark+light OKLCH variants, WCAG AA validated, 4 export formats working; all tests green Task 2: Extend nexus-settings schema with customTheme + test server/src/services/nexus-settings.ts, server/src/__tests__/nexus-settings-custom-theme.test.ts server/src/services/nexus-settings.ts, server/src/services/renderers/types.ts 1. Modify `server/src/services/nexus-settings.ts`: - Add paletteRoleSchema as a Zod object: ```typescript const paletteRoleSchema = z.object({ name: z.string(), dark: z.object({ oklch: z.string(), hex: z.string(), wcagAA: z.boolean() }), light: z.object({ oklch: z.string(), hex: z.string(), wcagAA: z.boolean() }), }); ``` - Add `customTheme` optional field to nexusSettingsSchema: ```typescript customTheme: z.object({ seedHex: z.string(), palette: z.array(paletteRoleSchema), }).optional(), ``` - Ensure existing tests still pass (the optional field with no default should not break anything) 2. Create `server/src/__tests__/nexus-settings-custom-theme.test.ts`: - Test that nexusSettingsSchema.parse({}) succeeds (customTheme is optional) - Test that nexusSettingsSchema.parse({ customTheme: { seedHex: "#1e66f5", palette: [...valid palette...] } }) succeeds - Test that invalid customTheme (missing seedHex) fails validation - Test set() with customTheme persists and get() retrieves it (use temp directory for settings file -- look at existing nexus-settings tests for the pattern) cd /opt/nexus && pnpm --filter server exec vitest run src/__tests__/nexus-settings-custom-theme.test.ts && pnpm tsc --noEmit --project server/tsconfig.json - `grep "customTheme" server/src/services/nexus-settings.ts` matches - `grep "paletteRoleSchema" server/src/services/nexus-settings.ts` matches - `grep "seedHex" server/src/services/nexus-settings.ts` matches - All tests pass - TypeScript compiles without errors nexus-settings.json schema accepts customTheme with palette array; persists and retrieves correctly; existing settings behavior unchanged - `pnpm --filter server exec vitest run src/__tests__/theme-renderer.test.ts src/__tests__/nexus-settings-custom-theme.test.ts` — all tests pass - `pnpm tsc --noEmit --project server/tsconfig.json` — no type errors - No `hsl` string appears anywhere in theme-renderer.ts - Palette engine produces 7 roles with OKLCH dark+light variants from single seed (THEME-01, THEME-02, THEME-06) - WCAG AA contrast validated for all foreground/background pairs (THEME-03) - Four export formatters produce CSS, Tailwind, VS Code, JSON (THEME-05) - nexus-settings.json schema extended with customTheme (THEME-07) - All tests pass, no HSL intermediates After completion, create `.planning/phases/41-diagrams-icons-theme-engine/41-03-SUMMARY.md`