nexus/.planning/phases/41-diagrams-icons-theme-engine/41-03-PLAN.md

12 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
41-diagrams-icons-theme-engine 03 execute 2
41-01
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
true
THEME-01
THEME-02
THEME-03
THEME-05
THEME-06
THEME-07
truths artifacts key_links
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
path provides exports
server/src/services/renderers/theme-renderer.ts OKLCH palette engine with WCAG validation and 4 export formatters
renderThemePalette
buildPalette
exportToCss
exportToTailwind
exportToVSCode
exportToJson
path provides
server/src/__tests__/theme-renderer.test.ts Tests for palette generation, WCAG validation, and export formats
path provides contains
server/src/services/nexus-settings.ts Extended schema with customTheme field customTheme
from to via pattern
server/src/services/renderers/theme-renderer.ts culori converter('oklch'), formatHex converter.*oklch
from to via pattern
server/src/services/renderers/theme-renderer.ts wcag-contrast wcagContrast.hex(fg, bg) 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.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.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 }; }


<!-- From server/src/services/nexus-settings.ts (existing) -->
```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
  1. 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<string, unknown>): Promise<RenderResult>:
      • 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 <acceptance_criteria>

    • 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 </acceptance_criteria> 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)
  1. 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 <acceptance_criteria>
    • 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 </acceptance_criteria> 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

<success_criteria>

  • 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 </success_criteria>
After completion, create `.planning/phases/41-diagrams-icons-theme-engine/41-03-SUMMARY.md`