16 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 41-diagrams-icons-theme-engine | 05 | execute | 3 |
|
|
true |
|
|
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
-
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
- Props:
-
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:
#40a02blight /#a6e3a1dark) - If wcagAA is false: Badge variant="destructive" with text "Fails AA"
- If wcagAA is true: Badge variant="default" with text "AA" (use --chart-2 for green:
- 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."
- Props:
-
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
useReffor the container element useEffectthat 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-previewelement, NOT on document.documentElement
- Container div with className "nexus-theme-preview" and
-
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
- Props:
-
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.tsxmatchesgrep "nexus-theme-preview" ui/src/components/ThemePreviewPanel.tsxmatchesgrep "setProperty" ui/src/components/ThemePreviewPanel.tsxmatchesgrep 'aria-live="polite"' ui/src/components/ThemePreviewPanel.tsxmatchesgrep 'aria-label="Copy' ui/src/components/ThemeExportTabs.tsxmatchesgrep "Copied!" ui/src/components/ThemeExportTabs.tsxmatchesgrep "AA" ui/src/components/ThemePaletteGrid.tsxmatchesgrep "Fails AA" ui/src/components/ThemePaletteGrid.tsxmatchesgrep "No palette yet" ui/src/components/ThemePaletteGrid.tsxmatches- 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
-
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"): voidto the context value applyCustomThemeimplementation:- Set CSS variables on
document.documentElementusing ROLE_TO_TOKEN mapping - Update theme state to "custom"
- Store in localStorage as "custom"
- Set CSS variables on
- On provider mount: check if stored theme is "custom" -- if so, fetch nexus settings to get customTheme palette and apply it
- Add
PaletteRoletype to the exports (or import from a shared types file)
- Add "custom" to the Theme union type:
-
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/settingswith{ customTheme: { seedHex, palette } }using the api client b. CallapplyCustomTheme(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.tsxmatchesgrep "Keep current" ui/src/components/ThemeApplyConfirmDialog.tsxmatchesgrep "custom" ui/src/context/ThemeContext.tsxmatches (custom theme type)grep "applyCustomTheme" ui/src/context/ThemeContext.tsxmatchesgrep "customTheme" ui/src/context/ThemeContext.tsxmatches (fetches from settings)grep "Theme applied" ui/src/pages/ContentStudio.tsxor 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
<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>