12 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 41-diagrams-icons-theme-engine | 03 | execute | 2 |
|
|
true |
|
|
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
-
Create
server/src/services/renderers/theme-renderer.ts:- Import
converter,formatHexfrom "culori" andwcagContrastfrom "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
- Output a
exportToVSCode(palette: PaletteRole[]): string:- Output JSON with
editor.background,editor.foreground,activityBar.background, etc.
- Output JSON with
exportToJson(palette: PaletteRole[]): string:- Output
JSON.stringify({ palette, generated: ISO date }, null, 2)
- Output
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.tsreturns at least 10grep -ci "hsl" server/src/services/renderers/theme-renderer.tsreturns 0 (no HSL anywhere)grep "wcagContrast" server/src/services/renderers/theme-renderer.tsmatchesgrep "converter.*oklch" server/src/services/renderers/theme-renderer.tsmatchesgrep "formatHex" server/src/services/renderers/theme-renderer.tsmatchesgrep "theme-palette-bundle" server/src/services/renderers/theme-renderer.tsmatches- 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
- Import
- 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.tsmatchesgrep "paletteRoleSchema" server/src/services/nexus-settings.tsmatchesgrep "seedHex" server/src/services/nexus-settings.tsmatches- 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
<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>