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

16 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
41-diagrams-icons-theme-engine 05 execute 3
41-01
41-03
ui/src/components/ThemeSeedInput.tsx
ui/src/components/ThemePaletteGrid.tsx
ui/src/components/ThemePreviewPanel.tsx
ui/src/components/ThemeExportTabs.tsx
ui/src/components/ThemeApplyConfirmDialog.tsx
ui/src/context/ThemeContext.tsx
ui/src/components/ThemePreviewPanel.test.tsx
true
THEME-01
THEME-04
THEME-05
THEME-07
truths artifacts key_links
User picks a seed color and sees a full palette grid with dark and light variants
WCAG AA pass/fail badges are shown on each swatch
Theme preview updates live without full page refresh, scoped to .nexus-theme-preview
User can export palette as CSS variables, Tailwind config, VS Code theme, or JSON via tabbed interface
User can apply the generated theme to their Nexus instance with a confirmation dialog
path provides
ui/src/components/ThemeSeedInput.tsx Color picker + hex text input for seed color
path provides
ui/src/components/ThemePaletteGrid.tsx Swatch grid with WCAG badges for dark and light variants
path provides
ui/src/components/ThemePreviewPanel.tsx Scoped mini Nexus UI mock with injected CSS variables
path provides
ui/src/components/ThemePreviewPanel.test.tsx Tests for THEME-04 scoped CSS variable injection
path provides
ui/src/components/ThemeExportTabs.tsx Tabbed code blocks for CSS, Tailwind, VS Code, JSON exports
path provides
ui/src/components/ThemeApplyConfirmDialog.tsx Confirmation dialog before applying theme to Nexus
path provides
ui/src/context/ThemeContext.tsx Extended to support custom theme token injection
from to via pattern
ui/src/components/ThemePreviewPanel.tsx .nexus-theme-preview container container.style.setProperty() in useEffect setProperty.*--background
from to via pattern
ui/src/components/ThemeApplyConfirmDialog.tsx server /api/nexus/settings PATCH request with customTheme payload customTheme
Build all theme UI components: seed input, palette grid with WCAG badges, scoped live preview, export tabs, and apply confirmation dialog. Extend ThemeContext to support custom theme injection. Include ThemePreviewPanel test (THEME-04 Wave 0 requirement).

Purpose: User-facing UI for theme generation, preview, export, and application. Output: Themes tab fully functional in ContentStudio with live preview, apply flow, and preview panel test coverage.

<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/phases/41-diagrams-icons-theme-engine/41-UI-SPEC.md @.planning/phases/41-diagrams-icons-theme-engine/41-RESEARCH.md @.planning/phases/41-diagrams-icons-theme-engine/41-01-SUMMARY.md @.planning/phases/41-diagrams-icons-theme-engine/41-03-SUMMARY.md ```typescript export function useContentJob(companyId: string): { state: { jobId: string | null; status: "idle" | "queued" | "running" | "done" | "failed"; progress: number; resultAssetId: string | null; errorMessage: string | null }; submit: (jobType: string, input: Record) => Promise; reset: () => void; } ```
interface ThemePaletteBundle {
  type: "theme-palette-bundle";
  seedHex: string;
  palette: PaletteRole[];
  exports: { css: string; tailwind: string; vscode: string; json: string };
}

interface PaletteRole {
  name: string;
  dark: { oklch: string; hex: string; wcagAA: boolean };
  light: { oklch: string; hex: string; wcagAA: boolean };
}
export type Theme = "catppuccin-mocha" | "tokyo-night" | "catppuccin-latte";
export const THEME_META: Record<Theme, { label: string; dark: boolean; bg: string; primary: string }>;
export function ThemeProvider({ children }: { children: ReactNode }): JSX.Element;
export function useTheme(): { theme: Theme; setTheme: (t: Theme) => void };
const ROLE_TO_TOKEN: Record<string, string> = {
  "background": "--background",
  "surface":    "--card",
  "overlay":    "--secondary",
  "text":       "--foreground",
  "accent-1":   "--primary",
  "accent-2":   "--accent",
  "accent-3":   "--muted",
};
Task 1: Theme seed input, palette grid, preview panel (with test), and export tabs ui/src/components/ThemeSeedInput.tsx, ui/src/components/ThemePaletteGrid.tsx, ui/src/components/ThemePreviewPanel.tsx, ui/src/components/ThemePreviewPanel.test.tsx, ui/src/components/ThemeExportTabs.tsx ui/src/pages/ContentStudio.tsx, ui/src/hooks/useContentJob.ts, ui/src/context/ThemeContext.tsx, ui/src/index.css, ui/src/components/ui/tabs.tsx, ui/src/components/ui/toggle.tsx, ui/src/components/ui/badge.tsx, ui/src/components/ui/progress.tsx, ui/src/components/ui/tooltip.tsx - ThemePreviewPanel renders a container with className "nexus-theme-preview" and aria-label="Theme preview" - When palette prop changes, CSS variables are set on the .nexus-theme-preview container element (NOT on document.documentElement) - For dark variant, container.style.setProperty("--background", palette[0].dark.hex) is called (background role) - For light variant, container.style.setProperty("--background", palette[0].light.hex) is called - Container includes an aria-live="polite" announcer that says "Palette updated" when palette changes - CSS variables are ONLY set on the scoped container ref, never on document.documentElement 1. Create `ui/src/components/ThemePreviewPanel.test.tsx` FIRST (Wave 0 for THEME-04): - Test that component renders a container with className "nexus-theme-preview" - Test that container has aria-label="Theme preview" - Test that passing a palette prop calls style.setProperty on the container element for each role token - Test that dark variant uses role.dark.hex values - Test that light variant uses role.light.hex values - Test that aria-live="polite" region exists and announces "Palette updated" on palette change - Test that document.documentElement.style.setProperty is NOT called (scoping check) - Use @testing-library/react; mock or spy on HTMLElement.prototype.style.setProperty to verify calls
  1. Create ui/src/components/ThemeSeedInput.tsx:

    • Props: value: string, onChange: (hex: string) => void
    • <input type="color"> styled with focus ring, 48px height (touch target)
    • Associated <label htmlFor="seed-color"> with text "Seed color"
    • Hex text Input side-by-side showing the hex value, editable
    • Helper text below: "We'll generate a full palette in OKLCH."
    • On either input change: call onChange with new hex value
    • Debounce the onChange by 150ms to avoid rapid palette recalculations
  2. Create ui/src/components/ThemePaletteGrid.tsx:

    • Props: palette: PaletteRole[], variant: "dark" | "light"
    • Display swatches in a row: one column per role (Background, Surface, Overlay, Text, Accent-1, Accent-2, Accent-3)
    • Show both dark and light variant rows with labels
    • Each swatch: 40x40px minimum, background-color set to the hex value of the current variant
    • Hex label below each swatch (text-xs, monospace)
    • WCAG badge inline per swatch:
      • If wcagAA is true: Badge variant="default" with text "AA" (use --chart-2 for green: #40a02b light / #a6e3a1 dark)
      • If wcagAA is false: Badge variant="destructive" with text "Fails AA"
    • 8px gap between swatches per UI spec
    • Empty state: heading "No palette yet", body "Pick a seed color to generate a full OKLCH palette with dark and light variants."
  3. Create ui/src/components/ThemePreviewPanel.tsx (must pass the tests from step 1):

    • Container div with className "nexus-theme-preview" and aria-label="Theme preview"
    • aria-live="polite" on a visually hidden announcer div that says "Palette updated" when palette changes
    • Use useRef for the container element
    • useEffect that imperatively sets CSS properties on the container ref:
      palette.forEach((role) => {
        const tokenName = ROLE_TO_TOKEN[role.name];
        if (!tokenName) return;
        const value = variant === "dark" ? role.dark.hex : role.light.hex;
        container.style.setProperty(tokenName, value);
      });
      
    • Inside the scoped container, render a mini Nexus UI mock:
      • A narrow sidebar strip (48px wide, uses --card background)
      • A main content area (uses --background)
      • One Card component inside (uses --card, --foreground for text)
      • A small Button (uses --primary)
    • This shows the palette in context without affecting the real Nexus UI
    • CRITICAL: CSS variables injected only on the .nexus-theme-preview element, NOT on document.documentElement
  4. Create ui/src/components/ThemeExportTabs.tsx:

    • Props: exports: { css: string; tailwind: string; vscode: string; json: string }
    • Tabs component with tabs: "CSS Variables", "Tailwind Config", "VS Code Theme", "JSON"
    • Each tab: pre/code block (monospace 14px) displaying the export string
    • "Copy {tab name}" IconButton top-right of each tab panel with:
      • aria-label="Copy CSS Variables" (or appropriate tab name)
      • title="Copy CSS Variables" (matching visible label)
    • On copy: navigator.clipboard.writeText, button text changes to "Copied!" for 2 seconds, then reverts
    • Keyboard: Tab order follows tab order; Copy button reachable via keyboard
  5. Wire all components into ContentStudio "Themes" tab:

    • State: seedHex (string), palette (PaletteRole[] | null), exports (object | null), variant ("dark" | "light")
    • ThemeVariantToggle using shadcn Toggle: dark/light switcher, default "dark"
    • "Generate Palette" Button (primary) -- submits useContentJob.submit("theme-palette", { seedHex })
    • On done: fetch asset, parse ThemePaletteBundle, populate palette + exports state
    • Progress bar during generation
    • Show ThemePaletteGrid, ThemePreviewPanel, ThemeExportTabs only when palette exists

    Copywriting: Use exact strings from UI-SPEC copywriting contract. Accessibility: All aria-labels and aria-live regions per UI-SPEC accessibility section. cd /opt/nexus && pnpm --filter ui exec vitest run src/components/ThemePreviewPanel.test.tsx && pnpm tsc --noEmit --project ui/tsconfig.json <acceptance_criteria>

    • grep 'htmlFor="seed-color"' ui/src/components/ThemeSeedInput.tsx matches
    • grep "nexus-theme-preview" ui/src/components/ThemePreviewPanel.tsx matches
    • grep "setProperty" ui/src/components/ThemePreviewPanel.tsx matches
    • grep 'aria-live="polite"' ui/src/components/ThemePreviewPanel.tsx matches
    • grep 'aria-label="Copy' ui/src/components/ThemeExportTabs.tsx matches
    • grep "Copied!" ui/src/components/ThemeExportTabs.tsx matches
    • grep "AA" ui/src/components/ThemePaletteGrid.tsx matches
    • grep "Fails AA" ui/src/components/ThemePaletteGrid.tsx matches
    • grep "No palette yet" ui/src/components/ThemePaletteGrid.tsx matches
    • ThemePreviewPanel.test.tsx exists and all tests pass
    • TypeScript compiles without errors </acceptance_criteria> Theme tab shows seed input, palette grid with WCAG badges, scoped live preview (with test coverage for THEME-04), and 4-format export tabs with copy buttons
Task 2: Apply theme flow (confirm dialog + ThemeContext extension + settings PATCH) ui/src/components/ThemeApplyConfirmDialog.tsx, ui/src/context/ThemeContext.tsx ui/src/context/ThemeContext.tsx, ui/src/components/ui/dialog.tsx, ui/src/components/ui/button.tsx, server/src/services/nexus-settings.ts, ui/src/api/client.ts 1. Create `ui/src/components/ThemeApplyConfirmDialog.tsx`: - Props: `open: boolean`, `onConfirm: () => void`, `onCancel: () => void` - Dialog component with: - Heading: "Apply theme?" - Body: "This will update your Nexus color scheme. You can revert from Settings." - Confirm button: "Apply theme" (primary, NOT destructive -- this is reversible) - Cancel button: "Keep current" (ghost variant) - On confirm: call onConfirm (parent handles the PATCH and ThemeContext update)
  1. Extend ui/src/context/ThemeContext.tsx:

    • Add "custom" to the Theme union type: export type Theme = "catppuccin-mocha" | "tokyo-night" | "catppuccin-latte" | "custom"
    • Add THEME_META["custom"] entry: { label: "Custom", dark: true, bg: "#1e1e2e", primary: "#89b4fa" } (defaults, overridden by actual palette)
    • Add applyCustomTheme(palette: PaletteRole[], variant: "dark" | "light"): void to the context value
    • applyCustomTheme implementation:
      • Set CSS variables on document.documentElement using ROLE_TO_TOKEN mapping
      • Update theme state to "custom"
      • Store in localStorage as "custom"
    • On provider mount: check if stored theme is "custom" -- if so, fetch nexus settings to get customTheme palette and apply it
    • Add PaletteRole type to the exports (or import from a shared types file)
  2. Wire "Apply to Nexus" button in the Themes tab of ContentStudio:

    • Button: "Apply to Nexus" (primary, full-width at panel bottom) -- only visible when palette exists
    • Click opens ThemeApplyConfirmDialog
    • On confirm: a. PATCH /api/nexus/settings with { customTheme: { seedHex, palette } } using the api client b. Call applyCustomTheme(palette, variant) from ThemeContext c. Show toast: "Theme applied. Reload to see full effect." (use the app's toast system -- check how toasts are done in existing code) d. Close dialog

    Copywriting: Use exact strings from UI-SPEC copywriting contract. cd /opt/nexus && pnpm tsc --noEmit --project ui/tsconfig.json <acceptance_criteria>

    • grep "Apply theme" ui/src/components/ThemeApplyConfirmDialog.tsx matches
    • grep "Keep current" ui/src/components/ThemeApplyConfirmDialog.tsx matches
    • grep "custom" ui/src/context/ThemeContext.tsx matches (custom theme type)
    • grep "applyCustomTheme" ui/src/context/ThemeContext.tsx matches
    • grep "customTheme" ui/src/context/ThemeContext.tsx matches (fetches from settings)
    • grep "Theme applied" ui/src/pages/ContentStudio.tsx or the component that triggers the toast matches
    • TypeScript compiles without errors </acceptance_criteria> Apply theme flow complete: confirm dialog, ThemeContext supports custom theme, PATCH to settings, CSS variables injected on document root, toast notification
- `pnpm --filter ui exec vitest run src/components/ThemePreviewPanel.test.tsx` — ThemePreviewPanel tests pass - `pnpm tsc --noEmit --project ui/tsconfig.json` passes - Themes tab has seed input, palette grid, live preview, export tabs, and apply flow - ThemePreviewPanel injects CSS only to .nexus-theme-preview (not document.documentElement) - ThemeApplyConfirmDialog applies theme to document.documentElement on confirm - All copywriting and accessibility requirements from UI-SPEC met

<success_criteria>

  • User picks a seed color and receives a full palette with WCAG badges (THEME-01, THEME-04)
  • ThemePreviewPanel has test coverage verifying scoped CSS injection (THEME-04)
  • Export works for all 4 formats (THEME-05)
  • Apply theme persists to settings and updates Nexus UI (THEME-07)
  • Preview is scoped to .nexus-theme-preview container (THEME-04) </success_criteria>
After completion, create `.planning/phases/41-diagrams-icons-theme-engine/41-05-SUMMARY.md`