298 lines
16 KiB
Markdown
298 lines
16 KiB
Markdown
---
|
|
phase: 41-diagrams-icons-theme-engine
|
|
plan: "05"
|
|
type: execute
|
|
wave: 3
|
|
depends_on: ["41-01", "41-03"]
|
|
files_modified:
|
|
- 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
|
|
autonomous: true
|
|
requirements: [THEME-01, THEME-04, THEME-05, THEME-07]
|
|
must_haves:
|
|
truths:
|
|
- "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"
|
|
artifacts:
|
|
- path: "ui/src/components/ThemeSeedInput.tsx"
|
|
provides: "Color picker + hex text input for seed color"
|
|
- path: "ui/src/components/ThemePaletteGrid.tsx"
|
|
provides: "Swatch grid with WCAG badges for dark and light variants"
|
|
- path: "ui/src/components/ThemePreviewPanel.tsx"
|
|
provides: "Scoped mini Nexus UI mock with injected CSS variables"
|
|
- path: "ui/src/components/ThemePreviewPanel.test.tsx"
|
|
provides: "Tests for THEME-04 scoped CSS variable injection"
|
|
- path: "ui/src/components/ThemeExportTabs.tsx"
|
|
provides: "Tabbed code blocks for CSS, Tailwind, VS Code, JSON exports"
|
|
- path: "ui/src/components/ThemeApplyConfirmDialog.tsx"
|
|
provides: "Confirmation dialog before applying theme to Nexus"
|
|
- path: "ui/src/context/ThemeContext.tsx"
|
|
provides: "Extended to support custom theme token injection"
|
|
key_links:
|
|
- from: "ui/src/components/ThemePreviewPanel.tsx"
|
|
to: ".nexus-theme-preview container"
|
|
via: "container.style.setProperty() in useEffect"
|
|
pattern: "setProperty.*--background"
|
|
- from: "ui/src/components/ThemeApplyConfirmDialog.tsx"
|
|
to: "server /api/nexus/settings"
|
|
via: "PATCH request with customTheme payload"
|
|
pattern: "customTheme"
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<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
|
|
|
|
<interfaces>
|
|
<!-- From ui/src/hooks/useContentJob.ts (created in Plan 01) -->
|
|
```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<string, unknown>) => Promise<void>;
|
|
reset: () => void;
|
|
}
|
|
```
|
|
|
|
<!-- ThemePaletteBundle from server response -->
|
|
```typescript
|
|
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 };
|
|
}
|
|
```
|
|
|
|
<!-- From ui/src/context/ThemeContext.tsx (existing) -->
|
|
```typescript
|
|
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 };
|
|
```
|
|
|
|
<!-- Role-to-token mapping for CSS injection -->
|
|
```typescript
|
|
const ROLE_TO_TOKEN: Record<string, string> = {
|
|
"background": "--background",
|
|
"surface": "--card",
|
|
"overlay": "--secondary",
|
|
"text": "--foreground",
|
|
"accent-1": "--primary",
|
|
"accent-2": "--accent",
|
|
"accent-3": "--muted",
|
|
};
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 1: Theme seed input, palette grid, preview panel (with test), and export tabs</name>
|
|
<files>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</files>
|
|
<read_first>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</read_first>
|
|
<behavior>
|
|
- 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
|
|
</behavior>
|
|
<action>
|
|
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
|
|
|
|
2. 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
|
|
|
|
3. 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."
|
|
|
|
4. 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:
|
|
```typescript
|
|
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
|
|
|
|
5. 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
|
|
|
|
6. 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.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /opt/nexus && pnpm --filter ui exec vitest run src/components/ThemePreviewPanel.test.tsx && pnpm tsc --noEmit --project ui/tsconfig.json</automated>
|
|
</verify>
|
|
<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>
|
|
<done>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</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Apply theme flow (confirm dialog + ThemeContext extension + settings PATCH)</name>
|
|
<files>ui/src/components/ThemeApplyConfirmDialog.tsx, ui/src/context/ThemeContext.tsx</files>
|
|
<read_first>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</read_first>
|
|
<action>
|
|
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)
|
|
|
|
2. 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)
|
|
|
|
3. 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.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /opt/nexus && pnpm tsc --noEmit --project ui/tsconfig.json</automated>
|
|
</verify>
|
|
<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>
|
|
<done>Apply theme flow complete: confirm dialog, ThemeContext supports custom theme, PATCH to settings, CSS variables injected on document root, toast notification</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `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
|
|
</verification>
|
|
|
|
<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>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/41-diagrams-icons-theme-engine/41-05-SUMMARY.md`
|
|
</output>
|