From 348ba3102d717a21504508e3514bc3d26e936f72 Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Thu, 26 Mar 2026 23:37:08 +0800 Subject: [PATCH 01/12] docs: add theme customize design doc and implementation plan --- docs/plans/2026-03-26-theme-system-design.md | 415 ++++++ .../2026-03-26-theme-system-implementation.md | 1166 +++++++++++++++++ 2 files changed, 1581 insertions(+) create mode 100644 docs/plans/2026-03-26-theme-system-design.md create mode 100644 docs/plans/2026-03-26-theme-system-implementation.md diff --git a/docs/plans/2026-03-26-theme-system-design.md b/docs/plans/2026-03-26-theme-system-design.md new file mode 100644 index 0000000..6f94154 --- /dev/null +++ b/docs/plans/2026-03-26-theme-system-design.md @@ -0,0 +1,415 @@ +# Theme System Design + +## Overview + +Add a curated theme preset system with accent color customization to the dashboard. Users select from 5 hand-designed theme presets and optionally swap the accent color within each preset from a set of 8-10 tested swatches. + +### Goals +- Support 5 theme presets: Dark Gold (current), Dark Ocean, Dark Forest, Dark Rose, Light Minimal +- Allow accent color customization within each preset (curated swatches only, no free picker) +- Persist theme preference in both `localStorage` (personal) and `meta.json` (project-level) +- Maintain visual coherence — no user-breakable color combinations +- Zero-reload theme switching via CSS variable injection at runtime + +### Non-Goals +- Free color picker (risk of ugly/unreadable combos) +- Per-component color overrides +- Multiple simultaneous themes + +--- + +## 1. Theme Presets & Color System + +### 1.1 Preset Definitions + +Each preset is a complete mapping of CSS variable names to values. The 5 presets: + +| Token | Dark Gold | Dark Ocean | Dark Forest | Dark Rose | Light Minimal | +|-------|-----------|------------|-------------|-----------|---------------| +| `--color-root` | `#0a0a0a` | `#0a0e14` | `#0a100a` | `#100a0a` | `#f5f3f0` | +| `--color-surface` | `#111111` | `#111820` | `#111811` | `#181111` | `#eae7e3` | +| `--color-elevated` | `#1a1a1a` | `#1a222c` | `#1a241a` | `#221a1a` | `#ffffff` | +| `--color-panel` | `#141414` | `#141c24` | `#141c14` | `#1c1414` | `#f0ede9` | +| `--color-gold`* | `#d4a574` | `#5ba4cf` | `#5ea67a` | `#cf7a8a` | `#4a6fa5` | +| `--color-gold-dim`* | `#c9a96e` | `#4e93ba` | `#4e9468` | `#b96e7e` | `#3d5f8f` | +| `--color-gold-bright`* | `#e8c49a` | `#7abce0` | `#78c492` | `#e094a4` | `#6088bf` | +| `--color-text-primary` | `#f5f0eb` | `#e8edf2` | `#ebf0eb` | `#f2e8ea` | `#1a1a1a` | +| `--color-text-secondary` | `#a39787` | `#87939f` | `#87a38f` | `#9f8790` | `#6b6b6b` | +| `--color-text-muted` | `#6b5f53` | `#536b7a` | `#536b5a` | `#6b535a` | `#a0a0a0` | +| `--color-border-subtle` | `rgba(212,165,116,0.12)` | `rgba(91,164,207,0.12)` | `rgba(94,166,122,0.12)` | `rgba(207,122,138,0.12)` | `rgba(74,111,165,0.10)` | +| `--color-border-medium` | `rgba(212,165,116,0.25)` | `rgba(91,164,207,0.25)` | `rgba(94,166,122,0.25)` | `rgba(207,122,138,0.25)` | `rgba(74,111,165,0.18)` | + +*\* The CSS variable names stay as `--color-gold`, `--color-gold-dim`, `--color-gold-bright` even for non-gold themes. They represent "the accent color" generically. Renaming them to `--color-accent` is a refactor we can do, but not required — the variable name is an implementation detail invisible to users.* + +**Decision: Rename `--color-gold*` to `--color-accent*`** to avoid confusion. This is a find-and-replace across the codebase with no behavioral change. + +### 1.2 Glass Effects + +Glass effects derive from base colors and need per-preset values: + +| Token | Dark themes | Light Minimal | +|-------|-------------|---------------| +| `--glass-bg` | `rgba(20,20,20,0.8)` | `rgba(255,255,255,0.8)` | +| `--glass-bg-heavy` | `rgba(20,20,20,0.95)` | `rgba(255,255,255,0.95)` | +| `--glass-border` | `rgba(accent,0.1)` | `rgba(accent,0.08)` | +| `--glass-border-heavy` | `rgba(accent,0.15)` | `rgba(accent,0.12)` | + +The `.glass` and `.glass-heavy` CSS classes will reference these variables instead of hardcoded values. + +### 1.3 Scrollbar & Glow Colors + +These also derive from the accent color and need to become CSS variables: + +| Token | Purpose | +|-------|---------| +| `--scrollbar-thumb` | `rgba(accent, 0.2)` | +| `--scrollbar-thumb-hover` | `rgba(accent, 0.35)` | +| `--glow-color` | `rgba(accent, 0.4)` for node selection glow | +| `--glow-pulse` | `rgba(accent, 0.6)` for tour highlight pulse | + +### 1.4 Node-Type & Diff Colors + +These are **semantic** and stay fixed across all dark themes: + +| Variable | Value | Purpose | +|----------|-------|---------| +| `--color-node-file` | `#4a7c9b` | File nodes | +| `--color-node-function` | `#5a9e6f` | Function nodes | +| `--color-node-class` | `#8b6fb0` | Class nodes | +| `--color-node-module` | `#c9a06c` | Module nodes | +| `--color-node-concept` | `#b07a8a` | Concept nodes | +| `--color-diff-changed` | `#e05252` | Changed nodes | +| `--color-diff-affected` | `#d4a030` | Affected nodes | + +For **Light Minimal only**, these are slightly desaturated/darkened to maintain readability on light backgrounds: + +| Variable | Light Minimal Value | +|----------|-------------------| +| `--color-node-file` | `#3a6a87` | +| `--color-node-function` | `#488a5b` | +| `--color-node-class` | `#755d99` | +| `--color-node-module` | `#a88a56` | +| `--color-node-concept` | `#966674` | + +### 1.5 Accent Swatches + +Each preset offers 8 accent color options. The first is the "native" default for that preset. Each swatch provides 3 values (accent, accent-dim, accent-bright) plus auto-derived border and glass opacities. + +**Dark theme accent swatches** (shared across all 4 dark presets): + +| Name | Accent | Dim | Bright | +|------|--------|-----|--------| +| Gold | `#d4a574` | `#c9a96e` | `#e8c49a` | +| Ocean | `#5ba4cf` | `#4e93ba` | `#7abce0` | +| Emerald | `#5ea67a` | `#4e9468` | `#78c492` | +| Rose | `#cf7a8a` | `#b96e7e` | `#e094a4` | +| Purple | `#9b7abf` | `#876bb0` | `#b494d4` | +| Amber | `#c9963a` | `#b5862e` | `#ddb05c` | +| Teal | `#4aab9a` | `#3d9686` | `#68c4b4` | +| Silver | `#a0a8b0` | `#8e959c` | `#b8bfc6` | + +**Light Minimal accent swatches:** + +| Name | Accent | Dim | Bright | +|------|--------|-----|--------| +| Indigo | `#4a6fa5` | `#3d5f8f` | `#6088bf` | +| Ocean | `#3a8ab5` | `#2e7aa0` | `#55a0cc` | +| Emerald | `#3a8a5c` | `#2e7a4e` | `#55a878` | +| Rose | `#a5566a` | `#8f4a5c` | `#bf6e82` | +| Purple | `#6b5a9e` | `#5c4d8a` | `#8474b5` | +| Amber | `#9e7a30` | `#8a6a28` | `#b5923e` | +| Teal | `#2e8a7a` | `#267a6c` | `#45a595` | +| Slate | `#5a6570` | `#4e5860` | `#6e7a85` | + +### 1.6 Border & Glass Derivation + +When an accent swatch is selected, borders and glass effects are auto-derived: + +```typescript +function deriveFromAccent(accentHex: string, isDark: boolean) { + return { + borderSubtle: `rgba(${hexToRgb(accentHex)}, ${isDark ? 0.12 : 0.10})`, + borderMedium: `rgba(${hexToRgb(accentHex)}, ${isDark ? 0.25 : 0.18})`, + glassBorder: `rgba(${hexToRgb(accentHex)}, ${isDark ? 0.1 : 0.08})`, + glassBorderHeavy: `rgba(${hexToRgb(accentHex)}, ${isDark ? 0.15 : 0.12})`, + scrollbarThumb: `rgba(${hexToRgb(accentHex)}, 0.2)`, + scrollbarThumbHover: `rgba(${hexToRgb(accentHex)}, 0.35)`, + glowColor: `rgba(${hexToRgb(accentHex)}, 0.4)`, + glowPulse: `rgba(${hexToRgb(accentHex)}, 0.6)`, + }; +} +``` + +--- + +## 2. Architecture & Data Flow + +### 2.1 File Structure + +``` +packages/dashboard/src/ + themes/ + types.ts # ThemePreset, AccentSwatch, ThemeConfig types + presets.ts # 5 preset definitions + accent swatch arrays + theme-engine.ts # applyTheme(), deriveFromAccent(), hexToRgb() + ThemeContext.tsx # React context + provider + useTheme() hook + components/ + ThemePicker.tsx # Popover UI for preset + accent selection +``` + +### 2.2 Type Definitions + +```typescript +// themes/types.ts + +export type PresetId = 'dark-gold' | 'dark-ocean' | 'dark-forest' | 'dark-rose' | 'light-minimal'; + +export interface ThemePreset { + id: PresetId; + name: string; // Display name: "Dark Gold" + isDark: boolean; // true for dark themes, false for light + colors: Record; // CSS variable name -> value (without --) + accentSwatches: AccentSwatch[]; + defaultAccentId: string; // Which swatch is the native default +} + +export interface AccentSwatch { + id: string; // e.g. 'gold', 'ocean' + name: string; // Display name: "Gold" + accent: string; // Primary accent hex + accentDim: string; // Dimmed accent hex + accentBright: string; // Bright accent hex +} + +export interface ThemeConfig { + presetId: PresetId; + accentId: string; // Selected accent swatch ID +} +``` + +### 2.3 Theme Engine + +The theme engine is a pure function layer (no React dependency): + +```typescript +// themes/theme-engine.ts + +export function applyTheme(config: ThemeConfig): void { + const preset = getPreset(config.presetId); + const accent = getAccent(preset, config.accentId); + + // 1. Apply base preset colors + for (const [key, value] of Object.entries(preset.colors)) { + document.documentElement.style.setProperty(`--color-${key}`, value); + } + + // 2. Override accent colors from swatch + document.documentElement.style.setProperty('--color-accent', accent.accent); + document.documentElement.style.setProperty('--color-accent-dim', accent.accentDim); + document.documentElement.style.setProperty('--color-accent-bright', accent.accentBright); + + // 3. Apply derived values (borders, glass, scrollbar, glow) + const derived = deriveFromAccent(accent.accent, preset.isDark); + for (const [key, value] of Object.entries(derived)) { + document.documentElement.style.setProperty(`--${key}`, value); + } + + // 4. Set data-theme attribute for any CSS-only selectors needed + document.documentElement.setAttribute('data-theme', preset.isDark ? 'dark' : 'light'); +} +``` + +### 2.4 React Context + +```typescript +// themes/ThemeContext.tsx + +interface ThemeContextValue { + config: ThemeConfig; + preset: ThemePreset; + setPreset: (presetId: PresetId) => void; + setAccent: (accentId: string) => void; +} +``` + +The provider: +1. On mount: resolves theme from `localStorage` > `meta.json` field in loaded graph > default (`dark-gold`) +2. Calls `applyTheme()` on every config change +3. Persists to `localStorage` on every change +4. Does NOT write to `meta.json` from the dashboard (the dashboard is read-only for meta.json; meta.json is written by the CLI/plugin side) + +### 2.5 Integration with Zustand Store + +The theme system is **separate from the Zustand store** — it uses its own React context. Rationale: +- Theme state is orthogonal to graph/UI state +- Theme needs to apply before the graph even loads (avoid flash of wrong theme) +- Keeps the store focused on graph interaction + +The store does NOT gain any theme-related fields. + +--- + +## 3. UI Components + +### 3.1 Theme Picker Button (Header) + +A small palette icon button in the top header bar, positioned after existing controls (PersonaSelector, DiffToggle, etc.). + +- Click opens a popover/dropdown panel +- Popover has two sections: + - **Presets**: 5 cards/buttons showing preset name + small color preview circles + - **Accent Colors**: row of 8 color circles for the active preset +- Active preset and accent are highlighted with a ring/check +- Selecting a preset instantly applies it; selecting an accent instantly applies it +- Clicking outside or pressing Escape closes the popover + +### 3.2 Preset Preview + +Each preset card shows: +- Name (e.g., "Dark Gold") +- 3-4 small circles showing root, surface, and accent colors as a visual preview +- Check mark or ring on the active one + +### 3.3 Accent Swatch Row + +- 8 small filled circles in a horizontal row +- Tooltip or label on hover showing the accent name +- Active one has a ring/border indicator + +### 3.4 Transitions + +When switching themes: +- CSS variables update instantly (no transition needed for most properties) +- Optionally add a subtle `transition: background-color 0.2s, color 0.2s` on `html` for a smooth feel +- No page reload required + +--- + +## 4. Persistence & Resolution + +### 4.1 Storage Locations + +| Location | Format | Written by | Read by | +|----------|--------|-----------|---------| +| `localStorage` key: `ua-theme` | `JSON.stringify(ThemeConfig)` | Dashboard (on every change) | Dashboard (on mount) | +| `.understand-anything/meta.json` | `{ ..., theme?: ThemeConfig }` | CLI/plugin (during analysis or explicit set) | Dashboard (on mount, as fallback) | + +### 4.2 Resolution Order + +``` +1. localStorage('ua-theme') → user's personal preference (wins) +2. meta.json.theme → project-level default (fallback) +3. { presetId: 'dark-gold', accentId: 'gold' } → hard default +``` + +### 4.3 meta.json Schema Extension + +Extend `AnalysisMeta` in `packages/core/src/types.ts`: + +```typescript +export interface AnalysisMeta { + lastAnalyzedAt: string; + gitCommitHash: string; + version: string; + analyzedFiles: number; + theme?: ThemeConfig; // NEW — optional, project-level theme preference +} +``` + +### 4.4 Dashboard Reads meta.json Theme + +The dashboard currently loads `/knowledge-graph.json` on mount. It also needs to load `/meta.json` (or the theme field can be embedded in `knowledge-graph.json`). + +**Decision:** Load `/meta.json` separately — it's a small file and keeps concerns separated. The dashboard fetches `/meta.json` on mount, extracts the `theme` field if present, and uses it as fallback when `localStorage` has no theme. + +--- + +## 5. Hardcoded Color Consolidation + +### 5.1 Problem + +Many components use hardcoded RGBA values instead of CSS variables: +- `rgba(212,165,116,0.3)` scattered in GraphView, CustomNode, etc. +- `rgba(20,20,20,0.8)` in glass effects +- `rgba(224,82,82,0.25)` in diff overlays + +These won't respond to theme changes. + +### 5.2 Solution + +Before implementing theme switching, consolidate all hardcoded color references: + +1. **Audit**: grep for hardcoded hex/rgba values in component files +2. **Replace with CSS variables**: create new variables where needed (e.g., `--edge-color`, `--edge-color-dim`) +3. **Glass classes**: update `.glass` and `.glass-heavy` in `index.css` to use variables +4. **Scrollbar**: update scrollbar styles to use variables +5. **Glow effects**: update `.node-glow`, `.diff-changed-glow`, `.diff-affected-glow` to use variables + +Key hardcoded patterns to consolidate: + +| Hardcoded Value | Replace With | +|-----------------|-------------| +| `rgba(212,165,116,X)` | `var(--color-accent)` with opacity modifier or dedicated variable | +| `rgba(20,20,20,0.8)` | `var(--glass-bg)` | +| `rgba(20,20,20,0.95)` | `var(--glass-bg-heavy)` | +| `color="rgba(212,165,116,0.15)"` in React Flow | Variable reference | +| Amber colors in WarningBanner | Keep as-is (semantic warning color, theme-independent) | + +### 5.3 CSS Variable Rename + +Rename throughout codebase: +- `--color-gold` -> `--color-accent` +- `--color-gold-dim` -> `--color-accent-dim` +- `--color-gold-bright` -> `--color-accent-bright` +- All Tailwind class usages: `text-gold` -> `text-accent`, `bg-gold` -> `bg-accent`, etc. + +--- + +## 6. Light Theme Considerations + +The Light Minimal theme requires special attention: + +### 6.1 Inverted Contrast + +- Text is dark on light backgrounds (flipped from dark themes) +- Borders need lower opacity to avoid looking harsh +- Glass effects use white-based rgba instead of black-based + +### 6.2 Node Colors + +Slightly darker/desaturated variants for readability on light backgrounds (see Section 1.4). + +### 6.3 data-theme Attribute + +Set `data-theme="light"` on `` for any styles that can't be handled purely through CSS variables (e.g., third-party component overrides, box-shadow directions). + +### 6.4 React Flow + +React Flow's background, minimap, and edge colors all need to respect the theme. The existing `!important` override on `.react-flow__background` already uses `var(--color-root)`, which is good. MiniMap colors in GraphView.tsx are currently hardcoded and need to be updated. + +--- + +## 7. Summary of Changes by Package + +### packages/core +- Extend `AnalysisMeta` type with optional `theme?: ThemeConfig` +- Export `ThemeConfig` and `PresetId` types from `./types` subpath + +### packages/dashboard +- New `themes/` directory with types, presets, engine, and context +- New `ThemePicker` component in header +- Rename `--color-gold*` to `--color-accent*` across all files +- Consolidate hardcoded RGBA values into CSS variables +- Update `index.css`: glass classes, scrollbar, glow effects to use variables +- Update `App.tsx`: wrap with ThemeProvider, add ThemePicker to header, fetch meta.json +- Update components with hardcoded colors: GraphView, CustomNode, LayerLegend, etc. + +--- + +## 8. Out of Scope + +- Theme import/export +- Custom theme creation UI +- Per-node color customization +- Animated theme transitions beyond simple CSS transitions +- Syncing theme across browser tabs (nice-to-have for later) diff --git a/docs/plans/2026-03-26-theme-system-implementation.md b/docs/plans/2026-03-26-theme-system-implementation.md new file mode 100644 index 0000000..148c548 --- /dev/null +++ b/docs/plans/2026-03-26-theme-system-implementation.md @@ -0,0 +1,1166 @@ +# Theme System Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add curated theme presets with accent customization to the dashboard. + +**Architecture:** CSS variable injection at runtime via a pure theme engine, React context for state, localStorage + meta.json for persistence. Five presets (4 dark + 1 light) with 8 accent swatches each. + +**Tech Stack:** React, TypeScript, TailwindCSS v4, Zustand (untouched), CSS custom properties. + +**Design Doc:** `docs/plans/2026-03-26-theme-system-design.md` + +--- + +### Task 1: Rename `gold` to `accent` in CSS variables and Tailwind classes + +This is a mechanical find-and-replace with no behavioral change. Must be done first so all subsequent tasks use the new naming. + +**Files:** +- Modify: `understand-anything-plugin/packages/dashboard/src/index.css` +- Modify: `understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx` +- Modify: `understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx` +- Modify: `understand-anything-plugin/packages/dashboard/src/components/LearnPanel.tsx` +- Modify: `understand-anything-plugin/packages/dashboard/src/components/ProjectOverview.tsx` +- Modify: `understand-anything-plugin/packages/dashboard/src/components/SearchBar.tsx` +- Modify: `understand-anything-plugin/packages/dashboard/src/components/LayerLegend.tsx` +- Modify: `understand-anything-plugin/packages/dashboard/src/components/PersonaSelector.tsx` +- Modify: `understand-anything-plugin/packages/dashboard/src/components/CodeViewer.tsx` +- Modify: `understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx` +- Modify: `understand-anything-plugin/packages/dashboard/src/App.tsx` + +**Step 1: Rename CSS variables in index.css** + +In the `@theme` block, rename: +- `--color-gold` -> `--color-accent` +- `--color-gold-dim` -> `--color-accent-dim` +- `--color-gold-bright` -> `--color-accent-bright` + +Also rename the `@keyframes goldPulse` to `accentPulse` and `.animate-gold-pulse` to `.animate-accent-pulse`. + +**Step 2: Rename all Tailwind class references across components** + +Find and replace in all component files: +- `text-gold-bright` -> `text-accent-bright` +- `text-gold-dim` -> `text-accent-dim` +- `text-gold` -> `text-accent` +- `bg-gold` -> `bg-accent` +- `border-gold` -> `border-accent` +- `ring-gold-dim` -> `ring-accent-dim` +- `ring-gold-bright` -> `ring-accent-bright` +- `ring-gold` -> `ring-accent` +- `animate-gold-pulse` -> `animate-accent-pulse` + +Order matters — replace the longer `-bright` and `-dim` variants first to avoid partial matches. + +Also replace any `var(--color-gold` with `var(--color-accent` in inline styles. + +**Step 3: Verify the build compiles** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/dashboard build` +Expected: Build succeeds with no errors. + +**Step 4: Visually verify (optional)** + +Run: `cd understand-anything-plugin && pnpm dev:dashboard` +Expected: Dashboard looks identical — same gold accent, no visual changes. + +**Step 5: Commit** + +```bash +git add -A +git commit -m "refactor(dashboard): rename gold CSS variables to accent" +``` + +--- + +### Task 2: Consolidate hardcoded RGBA values into CSS variables + +Replace scattered hardcoded color values in components with CSS variables so they respond to theme changes. + +**Files:** +- Modify: `understand-anything-plugin/packages/dashboard/src/index.css` +- Modify: `understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx` +- Modify: `understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx` +- Modify: `understand-anything-plugin/packages/dashboard/src/components/CodeViewer.tsx` + +**Step 1: Add new CSS variables to index.css @theme block** + +Add these new variables after the existing border variables: + +```css +/* Glass */ +--glass-bg: rgba(20, 20, 20, 0.8); +--glass-bg-heavy: rgba(20, 20, 20, 0.95); +--glass-border: rgba(212, 165, 116, 0.1); +--glass-border-heavy: rgba(212, 165, 116, 0.15); + +/* Scrollbar */ +--scrollbar-thumb: rgba(212, 165, 116, 0.2); +--scrollbar-thumb-hover: rgba(212, 165, 116, 0.35); + +/* Glow */ +--glow-accent: rgba(212, 165, 116, 0.15); +--glow-accent-strong: rgba(212, 165, 116, 0.4); +--glow-accent-pulse: rgba(212, 165, 116, 0.6); + +/* Edges */ +--color-edge: rgba(212, 165, 116, 0.3); +--color-edge-dim: rgba(212, 165, 116, 0.08); +--color-edge-dot: rgba(212, 165, 116, 0.15); + +/* Layer group (accent-based overlays) */ +--color-accent-overlay-bg: rgba(212, 165, 116, 0.05); +--color-accent-overlay-border: rgba(212, 165, 116, 0.25); + +/* kbd */ +--kbd-bg: rgba(212, 165, 116, 0.1); +``` + +**Step 2: Update .glass, .glass-heavy classes in index.css** + +Replace hardcoded values with the new variables: + +```css +.glass { + background: var(--glass-bg); + border: 1px solid var(--glass-border); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +.glass-heavy { + background: var(--glass-bg-heavy); + border: 1px solid var(--glass-border-heavy); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); +} +``` + +**Step 3: Update scrollbar styles in index.css** + +```css +::-webkit-scrollbar-thumb { + background: var(--scrollbar-thumb); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--scrollbar-thumb-hover); +} +``` + +**Step 4: Update glow classes in index.css** + +```css +.node-glow { + box-shadow: 0 0 20px var(--glow-accent); +} +``` + +Update `@keyframes accentPulse` (renamed in Task 1): +```css +@keyframes accentPulse { + 0%, 100% { + box-shadow: 0 0 8px var(--glow-accent-strong); + } + 50% { + box-shadow: 0 0 20px var(--glow-accent-pulse); + } +} +``` + +**Step 5: Update .kbd class in index.css** + +```css +.kbd { + /* ... keep existing sizing/layout ... */ + color: var(--color-accent); + background: var(--kbd-bg); +} +``` + +**Step 6: Update GraphView.tsx hardcoded colors** + +Replace these inline style values: + +| Location | Old Value | New Value | +|----------|-----------|-----------| +| Edge default style stroke | `"rgba(212,165,116,0.3)"` | `"var(--color-edge)"` | +| Edge diff-faded stroke | `"rgba(212,165,116,0.08)"` | `"var(--color-edge-dim)"` | +| Background dots color prop | `"rgba(212,165,116,0.15)"` | `"var(--color-edge-dot)"` | +| MiniMap nodeColor | `"#1a1a1a"` | `"var(--color-elevated)"` | +| MiniMap maskColor | `"rgba(10,10,10,0.7)"` | `"var(--glass-bg)"` | +| Group node backgroundColor | `"rgba(212,165,116,0.05)"` | `"var(--color-accent-overlay-bg)"` | +| Group node border | `"2px dashed rgba(212,165,116,0.25)"` | `"2px dashed var(--color-accent-overlay-border)"` | +| Group node label color | `"#d4a574"` | `"var(--color-accent)"` | +| Edge label fill (normal) | `"#a39787"` | `"var(--color-text-secondary)"` | +| Edge label fill (diff faded) | `"rgba(163,151,135,0.3)"` | `"var(--color-text-muted)"` | +| Spinner border class | `border-gold` already renamed to `border-accent` | Already done in Task 1 | + +**Step 7: Update CodeViewer.tsx hardcoded colors** + +Replace inline styles for the file type badge: +- `color: "var(--color-node-file)"` — already uses CSS var, keep +- `borderColor: "rgba(74,124,155,0.3)"` -> `"color-mix(in srgb, var(--color-node-file) 30%, transparent)"` +- `backgroundColor: "rgba(74,124,155,0.1)"` -> `"color-mix(in srgb, var(--color-node-file) 10%, transparent)"` + +**Step 8: Update CustomNode.tsx hardcoded shadow** + +Replace `shadow-[0_2px_8px_rgba(0,0,0,0.3)]` — this black shadow is fine for dark themes but keep it. Leave as-is since it works on both dark and light. + +**Step 9: Verify build** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/dashboard build` +Expected: Build succeeds. + +**Step 10: Commit** + +```bash +git add -A +git commit -m "refactor(dashboard): consolidate hardcoded colors into CSS variables" +``` + +--- + +### Task 3: Create theme type definitions + +**Files:** +- Create: `understand-anything-plugin/packages/dashboard/src/themes/types.ts` + +**Step 1: Write the types file** + +```typescript +export type PresetId = + | "dark-gold" + | "dark-ocean" + | "dark-forest" + | "dark-rose" + | "light-minimal"; + +export interface AccentSwatch { + id: string; + name: string; + accent: string; + accentDim: string; + accentBright: string; +} + +export interface ThemePreset { + id: PresetId; + name: string; + isDark: boolean; + colors: Record; + accentSwatches: AccentSwatch[]; + defaultAccentId: string; +} + +export interface ThemeConfig { + presetId: PresetId; + accentId: string; +} + +export const DEFAULT_THEME_CONFIG: ThemeConfig = { + presetId: "dark-gold", + accentId: "gold", +}; +``` + +**Step 2: Commit** + +```bash +git add -A +git commit -m "feat(dashboard): add theme type definitions" +``` + +--- + +### Task 4: Create theme presets + +**Files:** +- Create: `understand-anything-plugin/packages/dashboard/src/themes/presets.ts` + +**Step 1: Write the presets file** + +```typescript +import type { AccentSwatch, ThemePreset } from "./types.ts"; + +const DARK_ACCENT_SWATCHES: AccentSwatch[] = [ + { id: "gold", name: "Gold", accent: "#d4a574", accentDim: "#c9a96e", accentBright: "#e8c49a" }, + { id: "ocean", name: "Ocean", accent: "#5ba4cf", accentDim: "#4e93ba", accentBright: "#7abce0" }, + { id: "emerald", name: "Emerald", accent: "#5ea67a", accentDim: "#4e9468", accentBright: "#78c492" }, + { id: "rose", name: "Rose", accent: "#cf7a8a", accentDim: "#b96e7e", accentBright: "#e094a4" }, + { id: "purple", name: "Purple", accent: "#9b7abf", accentDim: "#876bb0", accentBright: "#b494d4" }, + { id: "amber", name: "Amber", accent: "#c9963a", accentDim: "#b5862e", accentBright: "#ddb05c" }, + { id: "teal", name: "Teal", accent: "#4aab9a", accentDim: "#3d9686", accentBright: "#68c4b4" }, + { id: "silver", name: "Silver", accent: "#a0a8b0", accentDim: "#8e959c", accentBright: "#b8bfc6" }, +]; + +const LIGHT_ACCENT_SWATCHES: AccentSwatch[] = [ + { id: "indigo", name: "Indigo", accent: "#4a6fa5", accentDim: "#3d5f8f", accentBright: "#6088bf" }, + { id: "ocean", name: "Ocean", accent: "#3a8ab5", accentDim: "#2e7aa0", accentBright: "#55a0cc" }, + { id: "emerald", name: "Emerald", accent: "#3a8a5c", accentDim: "#2e7a4e", accentBright: "#55a878" }, + { id: "rose", name: "Rose", accent: "#a5566a", accentDim: "#8f4a5c", accentBright: "#bf6e82" }, + { id: "purple", name: "Purple", accent: "#6b5a9e", accentDim: "#5c4d8a", accentBright: "#8474b5" }, + { id: "amber", name: "Amber", accent: "#9e7a30", accentDim: "#8a6a28", accentBright: "#b5923e" }, + { id: "teal", name: "Teal", accent: "#2e8a7a", accentDim: "#267a6c", accentBright: "#45a595" }, + { id: "slate", name: "Slate", accent: "#5a6570", accentDim: "#4e5860", accentBright: "#6e7a85" }, +]; + +export const PRESETS: ThemePreset[] = [ + { + id: "dark-gold", + name: "Dark Gold", + isDark: true, + defaultAccentId: "gold", + accentSwatches: DARK_ACCENT_SWATCHES, + colors: { + root: "#0a0a0a", + surface: "#111111", + elevated: "#1a1a1a", + panel: "#141414", + "text-primary": "#f5f0eb", + "text-secondary": "#a39787", + "text-muted": "#6b5f53", + "node-file": "#4a7c9b", + "node-function": "#5a9e6f", + "node-class": "#8b6fb0", + "node-module": "#c9a06c", + "node-concept": "#b07a8a", + }, + }, + { + id: "dark-ocean", + name: "Dark Ocean", + isDark: true, + defaultAccentId: "ocean", + accentSwatches: DARK_ACCENT_SWATCHES, + colors: { + root: "#0a0e14", + surface: "#111820", + elevated: "#1a222c", + panel: "#141c24", + "text-primary": "#e8edf2", + "text-secondary": "#87939f", + "text-muted": "#536b7a", + "node-file": "#4a7c9b", + "node-function": "#5a9e6f", + "node-class": "#8b6fb0", + "node-module": "#c9a06c", + "node-concept": "#b07a8a", + }, + }, + { + id: "dark-forest", + name: "Dark Forest", + isDark: true, + defaultAccentId: "emerald", + accentSwatches: DARK_ACCENT_SWATCHES, + colors: { + root: "#0a100a", + surface: "#111811", + elevated: "#1a241a", + panel: "#141c14", + "text-primary": "#ebf0eb", + "text-secondary": "#87a38f", + "text-muted": "#536b5a", + "node-file": "#4a7c9b", + "node-function": "#5a9e6f", + "node-class": "#8b6fb0", + "node-module": "#c9a06c", + "node-concept": "#b07a8a", + }, + }, + { + id: "dark-rose", + name: "Dark Rose", + isDark: true, + defaultAccentId: "rose", + accentSwatches: DARK_ACCENT_SWATCHES, + colors: { + root: "#100a0a", + surface: "#181111", + elevated: "#221a1a", + panel: "#1c1414", + "text-primary": "#f2e8ea", + "text-secondary": "#9f8790", + "text-muted": "#6b535a", + "node-file": "#4a7c9b", + "node-function": "#5a9e6f", + "node-class": "#8b6fb0", + "node-module": "#c9a06c", + "node-concept": "#b07a8a", + }, + }, + { + id: "light-minimal", + name: "Light Minimal", + isDark: false, + defaultAccentId: "indigo", + accentSwatches: LIGHT_ACCENT_SWATCHES, + colors: { + root: "#f5f3f0", + surface: "#eae7e3", + elevated: "#ffffff", + panel: "#f0ede9", + "text-primary": "#1a1a1a", + "text-secondary": "#6b6b6b", + "text-muted": "#a0a0a0", + "node-file": "#3a6a87", + "node-function": "#488a5b", + "node-class": "#755d99", + "node-module": "#a88a56", + "node-concept": "#966674", + }, + }, +]; + +export function getPreset(id: string): ThemePreset { + return PRESETS.find((p) => p.id === id) ?? PRESETS[0]; +} + +export function getAccent(preset: ThemePreset, accentId: string): AccentSwatch { + return ( + preset.accentSwatches.find((s) => s.id === accentId) ?? + preset.accentSwatches.find((s) => s.id === preset.defaultAccentId) ?? + preset.accentSwatches[0] + ); +} +``` + +**Step 2: Commit** + +```bash +git add -A +git commit -m "feat(dashboard): add theme preset definitions" +``` + +--- + +### Task 5: Create theme engine + +Pure functions with no React dependency. Handles CSS variable injection and accent derivation. + +**Files:** +- Create: `understand-anything-plugin/packages/dashboard/src/themes/theme-engine.ts` + +**Step 1: Write the theme engine** + +```typescript +import type { ThemeConfig } from "./types.ts"; +import { getAccent, getPreset } from "./presets.ts"; + +export function hexToRgb(hex: string): string { + const h = hex.replace("#", ""); + const n = parseInt(h, 16); + return `${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}`; +} + +function deriveFromAccent(accentHex: string, isDark: boolean): Record { + const rgb = hexToRgb(accentHex); + return { + "color-border-subtle": `rgba(${rgb}, ${isDark ? 0.12 : 0.1})`, + "color-border-medium": `rgba(${rgb}, ${isDark ? 0.25 : 0.18})`, + "glass-bg": isDark ? "rgba(20, 20, 20, 0.8)" : "rgba(255, 255, 255, 0.8)", + "glass-bg-heavy": isDark ? "rgba(20, 20, 20, 0.95)" : "rgba(255, 255, 255, 0.95)", + "glass-border": `rgba(${rgb}, ${isDark ? 0.1 : 0.08})`, + "glass-border-heavy": `rgba(${rgb}, ${isDark ? 0.15 : 0.12})`, + "scrollbar-thumb": `rgba(${rgb}, 0.2)`, + "scrollbar-thumb-hover": `rgba(${rgb}, 0.35)`, + "glow-accent": `rgba(${rgb}, 0.15)`, + "glow-accent-strong": `rgba(${rgb}, 0.4)`, + "glow-accent-pulse": `rgba(${rgb}, 0.6)`, + "color-edge": `rgba(${rgb}, 0.3)`, + "color-edge-dim": `rgba(${rgb}, 0.08)`, + "color-edge-dot": `rgba(${rgb}, 0.15)`, + "color-accent-overlay-bg": `rgba(${rgb}, 0.05)`, + "color-accent-overlay-border": `rgba(${rgb}, 0.25)`, + "kbd-bg": `rgba(${rgb}, 0.1)`, + }; +} + +export function applyTheme(config: ThemeConfig): void { + const preset = getPreset(config.presetId); + const accent = getAccent(preset, config.accentId); + const style = document.documentElement.style; + + // 1. Apply base preset colors + for (const [key, value] of Object.entries(preset.colors)) { + style.setProperty(`--color-${key}`, value); + } + + // 2. Apply accent colors from swatch + style.setProperty("--color-accent", accent.accent); + style.setProperty("--color-accent-dim", accent.accentDim); + style.setProperty("--color-accent-bright", accent.accentBright); + + // 3. Apply derived values + const derived = deriveFromAccent(accent.accent, preset.isDark); + for (const [key, value] of Object.entries(derived)) { + style.setProperty(`--${key}`, value); + } + + // 4. Set data-theme for CSS-only selectors + document.documentElement.setAttribute("data-theme", preset.isDark ? "dark" : "light"); +} +``` + +**Step 2: Commit** + +```bash +git add -A +git commit -m "feat(dashboard): add theme engine with CSS variable injection" +``` + +--- + +### Task 6: Create ThemeContext + +React context + provider that manages theme state, persistence, and resolution. + +**Files:** +- Create: `understand-anything-plugin/packages/dashboard/src/themes/ThemeContext.tsx` + +**Step 1: Write the context** + +```typescript +import { + createContext, + useCallback, + useContext, + useEffect, + useRef, + useState, + type ReactNode, +} from "react"; +import type { PresetId, ThemeConfig, ThemePreset } from "./types.ts"; +import { DEFAULT_THEME_CONFIG } from "./types.ts"; +import { getPreset } from "./presets.ts"; +import { applyTheme } from "./theme-engine.ts"; + +const STORAGE_KEY = "ua-theme"; + +interface ThemeContextValue { + config: ThemeConfig; + preset: ThemePreset; + setPreset: (presetId: PresetId) => void; + setAccent: (accentId: string) => void; +} + +const ThemeContext = createContext(null); + +function loadFromLocalStorage(): ThemeConfig | null { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return null; + const parsed = JSON.parse(raw); + if (parsed && typeof parsed.presetId === "string" && typeof parsed.accentId === "string") { + return parsed as ThemeConfig; + } + return null; + } catch { + return null; + } +} + +function saveToLocalStorage(config: ThemeConfig): void { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(config)); + } catch { + // Storage full or unavailable — ignore + } +} + +function resolveInitialTheme(metaTheme?: ThemeConfig | null): ThemeConfig { + return loadFromLocalStorage() ?? metaTheme ?? DEFAULT_THEME_CONFIG; +} + +interface ThemeProviderProps { + metaTheme?: ThemeConfig | null; + children: ReactNode; +} + +export function ThemeProvider({ metaTheme, children }: ThemeProviderProps) { + const [config, setConfig] = useState(() => resolveInitialTheme(metaTheme)); + const initialized = useRef(false); + + // Apply theme on mount and config changes + useEffect(() => { + applyTheme(config); + if (initialized.current) { + saveToLocalStorage(config); + } + initialized.current = true; + }, [config]); + + // Update if metaTheme arrives later (async fetch) and no localStorage preference exists + useEffect(() => { + if (metaTheme && !loadFromLocalStorage()) { + setConfig(metaTheme); + } + }, [metaTheme]); + + const setPreset = useCallback((presetId: PresetId) => { + setConfig((prev) => { + const newPreset = getPreset(presetId); + return { presetId, accentId: newPreset.defaultAccentId }; + }); + }, []); + + const setAccent = useCallback((accentId: string) => { + setConfig((prev) => ({ ...prev, accentId })); + }, []); + + const preset = getPreset(config.presetId); + + return ( + + {children} + + ); +} + +export function useTheme(): ThemeContextValue { + const ctx = useContext(ThemeContext); + if (!ctx) throw new Error("useTheme must be used within ThemeProvider"); + return ctx; +} +``` + +**Step 2: Create barrel export** + +Create: `understand-anything-plugin/packages/dashboard/src/themes/index.ts` + +```typescript +export { ThemeProvider, useTheme } from "./ThemeContext.tsx"; +export { PRESETS, getPreset, getAccent } from "./presets.ts"; +export { applyTheme } from "./theme-engine.ts"; +export type { PresetId, ThemeConfig, ThemePreset, AccentSwatch } from "./types.ts"; +export { DEFAULT_THEME_CONFIG } from "./types.ts"; +``` + +**Step 3: Commit** + +```bash +git add -A +git commit -m "feat(dashboard): add ThemeContext with localStorage persistence" +``` + +--- + +### Task 7: Extend AnalysisMeta with theme field + +**Files:** +- Modify: `understand-anything-plugin/packages/core/src/types.ts` + +**Step 1: Add ThemeConfig type and extend AnalysisMeta** + +Add near the top of the file (after existing imports/types): + +```typescript +export interface ThemeConfig { + presetId: string; + accentId: string; +} +``` + +Add `theme` field to `AnalysisMeta`: + +```typescript +export interface AnalysisMeta { + lastAnalyzedAt: string; + gitCommitHash: string; + version: string; + analyzedFiles: number; + theme?: ThemeConfig; +} +``` + +**Step 2: Verify core builds** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core build` +Expected: Build succeeds. + +**Step 3: Verify core tests pass** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test` +Expected: All tests pass. + +**Step 4: Commit** + +```bash +git add -A +git commit -m "feat(core): add optional theme field to AnalysisMeta" +``` + +--- + +### Task 8: Create ThemePicker component + +The popover UI with preset selection and accent swatch row. + +**Files:** +- Create: `understand-anything-plugin/packages/dashboard/src/components/ThemePicker.tsx` + +**Step 1: Write the component** + +```tsx +import { useCallback, useEffect, useRef, useState } from "react"; +import { useTheme, PRESETS } from "../themes/index.ts"; + +export function ThemePicker() { + const { config, preset, setPreset, setAccent } = useTheme(); + const [open, setOpen] = useState(false); + const ref = useRef(null); + + // Close on outside click + useEffect(() => { + if (!open) return; + function handleClick(e: MouseEvent) { + if (ref.current && !ref.current.contains(e.target as Node)) { + setOpen(false); + } + } + document.addEventListener("mousedown", handleClick); + return () => document.removeEventListener("mousedown", handleClick); + }, [open]); + + // Close on Escape + useEffect(() => { + if (!open) return; + function handleKey(e: KeyboardEvent) { + if (e.key === "Escape") setOpen(false); + } + document.addEventListener("keydown", handleKey); + return () => document.removeEventListener("keydown", handleKey); + }, [open]); + + const handlePreset = useCallback( + (id: string) => { + setPreset(id as Parameters[0]); + }, + [setPreset], + ); + + return ( +
+ + + {open && ( +
+ {/* Presets */} +
+
+ Theme +
+
+ {PRESETS.map((p) => ( + + ))} +
+
+ + {/* Accent swatches */} +
+
+ Accent Color +
+
+ {preset.accentSwatches.map((swatch) => ( +
+
+
+ )} +
+ ); +} +``` + +**Step 2: Commit** + +```bash +git add -A +git commit -m "feat(dashboard): add ThemePicker popover component" +``` + +--- + +### Task 9: Integrate ThemeProvider and ThemePicker into App + +Wire everything together in the root component. + +**Files:** +- Modify: `understand-anything-plugin/packages/dashboard/src/App.tsx` + +**Step 1: Add imports** + +Add to imports at top of App.tsx: + +```typescript +import { ThemeProvider } from "./themes/index.ts"; +import { ThemePicker } from "./components/ThemePicker.tsx"; +import type { ThemeConfig } from "./themes/index.ts"; +``` + +**Step 2: Add meta.json theme loading** + +Inside the App component, add state and effect for meta.json theme: + +```typescript +const [metaTheme, setMetaTheme] = useState(null); + +useEffect(() => { + fetch("/meta.json") + .then((r) => (r.ok ? r.json() : null)) + .then((meta) => { + if (meta?.theme) setMetaTheme(meta.theme); + }) + .catch(() => {}); +}, []); +``` + +**Step 3: Wrap return JSX with ThemeProvider** + +Wrap the entire return value of App with `...`. + +**Step 4: Add ThemePicker to header** + +In the header bar (the `
` or top flex row), add `` after the existing controls (PersonaSelector, DiffToggle, LayerLegend) and before the help button. + +**Step 5: Verify build** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/dashboard build` +Expected: Build succeeds. + +**Step 6: Commit** + +```bash +git add -A +git commit -m "feat(dashboard): integrate ThemeProvider and ThemePicker into App" +``` + +--- + +### Task 10: Light theme CSS adjustments + +Handle edge cases where CSS variables alone aren't sufficient for the light theme. + +**Files:** +- Modify: `understand-anything-plugin/packages/dashboard/src/index.css` + +**Step 1: Add data-theme selectors for light theme overrides** + +Add at the end of index.css: + +```css +/* Light theme overrides */ +[data-theme="light"] { + color-scheme: light; +} + +[data-theme="light"] .diff-faded { + opacity: 0.35; +} + +[data-theme="light"] ::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.05); +} + +[data-theme="dark"] { + color-scheme: dark; +} +``` + +**Step 2: Add transition for smooth theme switching** + +Add to the `html` base styles: + +```css +html { + transition: background-color 0.2s ease, color 0.2s ease; +} +``` + +**Step 3: Update the WarningBanner consideration** + +WarningBanner uses Tailwind amber/orange colors directly (e.g., `bg-amber-900/20`). These are semantic warning colors and should NOT change with theme. However, for the light theme, the amber colors on a light background need adjustment. + +Add to light theme overrides if needed: + +```css +[data-theme="light"] .warning-banner { + background: rgba(180, 130, 30, 0.1); + border-color: rgba(180, 130, 30, 0.3); + color: #92600a; +} +``` + +Note: Only add this if the WarningBanner looks broken on the light theme during visual testing. It may work fine as-is with Tailwind's amber colors. + +**Step 4: Verify build** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/dashboard build` +Expected: Build succeeds. + +**Step 5: Commit** + +```bash +git add -A +git commit -m "feat(dashboard): add light theme CSS overrides" +``` + +--- + +### Task 11: Remove @theme defaults from index.css + +Now that the theme engine sets all CSS variables at runtime, the `@theme` block in index.css serves as the initial/fallback values before React mounts. Keep it but update it to use the accent naming. + +**Files:** +- Modify: `understand-anything-plugin/packages/dashboard/src/index.css` + +**Step 1: Update @theme block** + +The `@theme` block should already have `--color-accent` (from Task 1 rename). Ensure the new variables added in Task 2 are also present in the `@theme` block as defaults: + +```css +@theme { + /* Base */ + --color-root: #0a0a0a; + --color-surface: #111111; + --color-elevated: #1a1a1a; + --color-panel: #141414; + + /* Accent */ + --color-accent: #d4a574; + --color-accent-dim: #c9a96e; + --color-accent-bright: #e8c49a; + + /* Text */ + --color-text-primary: #f5f0eb; + --color-text-secondary: #a39787; + --color-text-muted: #6b5f53; + + /* Borders */ + --color-border-subtle: rgba(212, 165, 116, 0.12); + --color-border-medium: rgba(212, 165, 116, 0.25); + + /* Node types */ + --color-node-file: #4a7c9b; + --color-node-function: #5a9e6f; + --color-node-class: #8b6fb0; + --color-node-module: #c9a06c; + --color-node-concept: #b07a8a; + + /* Diff */ + --color-diff-changed: #e05252; + --color-diff-affected: #d4a030; + --color-diff-changed-dim: rgba(224, 82, 82, 0.25); + --color-diff-affected-dim: rgba(212, 160, 48, 0.25); + + /* Glass */ + --glass-bg: rgba(20, 20, 20, 0.8); + --glass-bg-heavy: rgba(20, 20, 20, 0.95); + --glass-border: rgba(212, 165, 116, 0.1); + --glass-border-heavy: rgba(212, 165, 116, 0.15); + + /* Scrollbar */ + --scrollbar-thumb: rgba(212, 165, 116, 0.2); + --scrollbar-thumb-hover: rgba(212, 165, 116, 0.35); + + /* Glow */ + --glow-accent: rgba(212, 165, 116, 0.15); + --glow-accent-strong: rgba(212, 165, 116, 0.4); + --glow-accent-pulse: rgba(212, 165, 116, 0.6); + + /* Edges */ + --color-edge: rgba(212, 165, 116, 0.3); + --color-edge-dim: rgba(212, 165, 116, 0.08); + --color-edge-dot: rgba(212, 165, 116, 0.15); + + /* Accent overlays */ + --color-accent-overlay-bg: rgba(212, 165, 116, 0.05); + --color-accent-overlay-border: rgba(212, 165, 116, 0.25); + + /* Kbd */ + --kbd-bg: rgba(212, 165, 116, 0.1); + + /* Typography */ + --font-serif: 'DM Serif Display', Georgia, serif; + --font-mono: 'JetBrains Mono', 'Fira Code', monospace; + --font-sans: 'Inter', system-ui, sans-serif; +} +``` + +This ensures: +- Tailwind v4 generates all the correct utility classes from the `@theme` block +- Before React mounts, the page shows the Dark Gold default (no flash of unstyled content) +- The theme engine overrides these values at runtime + +**Step 2: Verify build** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/dashboard build` +Expected: Build succeeds. + +**Step 3: Commit** + +```bash +git add -A +git commit -m "refactor(dashboard): align @theme defaults with theme engine variables" +``` + +--- + +### Task 12: Full build + visual verification + +**Files:** None (verification only) + +**Step 1: Build core** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core build` +Expected: Build succeeds. + +**Step 2: Build dashboard** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/dashboard build` +Expected: Build succeeds. + +**Step 3: Run core tests** + +Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test` +Expected: All tests pass. + +**Step 4: Run lint** + +Run: `cd understand-anything-plugin && pnpm lint` +Expected: No lint errors. + +**Step 5: Start dev server and visually verify** + +Run: `cd understand-anything-plugin && pnpm dev:dashboard` + +Verify: +1. Dashboard loads with Dark Gold theme (default) — looks identical to current +2. Theme picker button visible in header +3. Click theme picker — popover opens with 5 presets and 8 accent swatches +4. Select Dark Ocean — backgrounds turn navy-blue, accent turns cyan +5. Select Dark Forest — backgrounds turn dark green, accent turns emerald +6. Select Dark Rose — backgrounds turn dark warm, accent turns rose +7. Select Light Minimal — backgrounds turn light, text turns dark, accent turns indigo +8. Select different accent swatches within each preset — accent color, borders, glass, glow all update +9. Refresh page — theme persists from localStorage +10. Click outside popover — it closes +11. Press Escape — popover closes + +**Step 6: Commit (if any fixes needed)** + +```bash +git add -A +git commit -m "fix(dashboard): theme system visual adjustments" +``` + +--- + +## Dependency Graph + +``` +Task 1 (rename gold→accent) ─┐ + ├─> Task 3 (types) ──┐ +Task 2 (consolidate colors) ──┤ │ + │ Task 4 (presets) ─┤ + │ ├─> Task 6 (context) ─┐ + │ Task 5 (engine) ──┘ │ + │ ├─> Task 8 (picker) ─┐ + │ Task 7 (core types) ────────────────────┘ │ + │ │ + └───────────────────────────────────────────> Task 9 (integrate) ─┤ + │ + Task 10 (light CSS) ┤ + │ + Task 11 (defaults) ─┤ + │ + Task 12 (verify) ───┘ +``` + +**Parallelizable groups:** +- Tasks 1 + 2 can be done sequentially (both touch index.css) +- Tasks 3, 4, 5 can be done in parallel (independent new files) +- Task 6 depends on 3, 4, 5 +- Task 7 is independent (core package) +- Task 8 depends on 6 +- Task 9 depends on 1, 2, 7, 8 +- Tasks 10, 11 can be done after 9 +- Task 12 is final verification From dde40b8512db33d0a9eb58896f7ea8ee11775bc8 Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Fri, 27 Mar 2026 09:29:44 +0800 Subject: [PATCH 02/12] refactor(dashboard): rename gold CSS variables to accent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename all gold-related CSS custom properties, Tailwind classes, and keyframe animations from "gold" to "accent" to prepare for the theme system where the accent color is user-configurable. - --color-gold -> --color-accent (plus -dim and -bright variants) - @keyframes goldPulse -> accentPulse - .animate-gold-pulse -> .animate-accent-pulse - All text-gold, bg-gold, border-gold, ring-gold Tailwind classes No behavioral or visual change — pure mechanical rename. Co-Authored-By: Claude Opus 4.6 --- .../packages/dashboard/src/App.tsx | 2 +- .../dashboard/src/components/CodeViewer.tsx | 8 ++++---- .../dashboard/src/components/CustomNode.tsx | 12 +++++------ .../dashboard/src/components/GraphView.tsx | 2 +- .../src/components/KeyboardShortcutsHelp.tsx | 2 +- .../dashboard/src/components/LayerLegend.tsx | 2 +- .../dashboard/src/components/LearnPanel.tsx | 20 +++++++++---------- .../dashboard/src/components/NodeInfo.tsx | 12 +++++------ .../src/components/PersonaSelector.tsx | 2 +- .../src/components/ProjectOverview.tsx | 14 ++++++------- .../dashboard/src/components/SearchBar.tsx | 8 ++++---- .../packages/dashboard/src/index.css | 16 +++++++-------- 12 files changed, 50 insertions(+), 50 deletions(-) diff --git a/understand-anything-plugin/packages/dashboard/src/App.tsx b/understand-anything-plugin/packages/dashboard/src/App.tsx index 58fef79..634461d 100644 --- a/understand-anything-plugin/packages/dashboard/src/App.tsx +++ b/understand-anything-plugin/packages/dashboard/src/App.tsx @@ -200,7 +200,7 @@ function App() {
-

+

Steps

{tourSteps.map((step, i) => ( @@ -61,7 +61,7 @@ export default function LearnPanel() { key={step.order} className="flex items-start gap-2 text-xs bg-elevated rounded-lg px-3 py-2 border border-border-subtle" > - + {i + 1}. {step.title} @@ -86,7 +86,7 @@ export default function LearnPanel() { {/* Header with progress counter and exit */}
-

+

Tour

@@ -104,7 +104,7 @@ export default function LearnPanel() { {/* Progress bar */}
@@ -154,8 +154,8 @@ export default function LearnPanel() { {/* Language lesson */} {step.languageLesson && ( -
-

+
+

Language Lesson

@@ -167,7 +167,7 @@ export default function LearnPanel() { {/* Referenced component pills */} {step.nodeIds.length > 0 && (

-

+

Referenced Components

@@ -198,7 +198,7 @@ export default function LearnPanel() { onClick={() => setTourStep(i)} className={`w-2 h-2 rounded-full transition-colors ${ i === currentTourStep - ? "bg-gold" + ? "bg-accent" : "bg-elevated hover:bg-surface" }`} aria-label={`Go to step ${i + 1}`} @@ -217,7 +217,7 @@ export default function LearnPanel() { diff --git a/understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx b/understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx index 78c1041..ad60b1b 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx @@ -11,7 +11,7 @@ const typeBadgeColors: Record = { const complexityBadgeColors: Record = { simple: "text-node-function border border-node-function/30 bg-node-function/10", - moderate: "text-gold-dim border border-gold-dim/30 bg-gold-dim/10", + moderate: "text-accent-dim border border-accent-dim/30 bg-accent-dim/10", complex: "text-[#c97070] border border-[#c97070]/30 bg-[#c97070]/10", }; @@ -75,7 +75,7 @@ export default function NodeInfo() {
diff --git a/understand-anything-plugin/packages/dashboard/src/components/SearchBar.tsx b/understand-anything-plugin/packages/dashboard/src/components/SearchBar.tsx index 8a8f245..112b552 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/SearchBar.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/SearchBar.tsx @@ -94,14 +94,14 @@ export default function SearchBar() { onChange={handleInputChange} onFocus={() => setDropdownOpen(true)} placeholder="Search nodes by name, summary, or tags..." - className="flex-1 bg-elevated text-text-primary text-sm rounded-lg px-3 py-1.5 border border-border-subtle focus:outline-none focus:border-gold/50 placeholder-text-muted" + className="flex-1 bg-elevated text-text-primary text-sm rounded-lg px-3 py-1.5 border border-border-subtle focus:outline-none focus:border-accent/50 placeholder-text-muted" />
+ + {open && ( +
+ {/* Presets */} +
+
+ Theme +
+
+ {PRESETS.map((p) => ( + + ))} +
+
+ + {/* Accent swatches */} +
+
+ Accent Color +
+
+ {preset.accentSwatches.map((swatch) => ( +
+
+
+ )} +
+ ); +} From 17330081aba06f2949cea7db8db787e9cf096ce5 Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Fri, 27 Mar 2026 09:54:11 +0800 Subject: [PATCH 10/12] feat(dashboard): integrate ThemeProvider and ThemePicker into App Co-Authored-By: Claude Opus 4.6 --- .../packages/dashboard/src/App.tsx | 16 ++++++++++++++++ .../dashboard/src/themes/ThemeContext.tsx | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/understand-anything-plugin/packages/dashboard/src/App.tsx b/understand-anything-plugin/packages/dashboard/src/App.tsx index 634461d..78ded8f 100644 --- a/understand-anything-plugin/packages/dashboard/src/App.tsx +++ b/understand-anything-plugin/packages/dashboard/src/App.tsx @@ -15,6 +15,9 @@ import KeyboardShortcutsHelp from "./components/KeyboardShortcutsHelp"; import WarningBanner from "./components/WarningBanner"; import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts"; import type { KeyboardShortcut } from "./hooks/useKeyboardShortcuts"; +import { ThemeProvider } from "./themes/index.ts"; +import { ThemePicker } from "./components/ThemePicker.tsx"; +import type { ThemeConfig } from "./themes/index.ts"; function App() { const graph = useDashboardStore((s) => s.graph); @@ -28,6 +31,16 @@ function App() { const [loadError, setLoadError] = useState(null); const [graphIssues, setGraphIssues] = useState([]); const [showKeyboardHelp, setShowKeyboardHelp] = useState(false); + const [metaTheme, setMetaTheme] = useState(null); + + useEffect(() => { + fetch("/meta.json") + .then((r) => (r.ok ? r.json() : null)) + .then((meta) => { + if (meta?.theme) setMetaTheme(meta.theme); + }) + .catch(() => {}); + }, []); // Define keyboard shortcuts const shortcuts = useMemo( @@ -185,6 +198,7 @@ function App() { ); return ( +
{/* Header */}
@@ -198,6 +212,7 @@ function App() {
+
+ ); } diff --git a/understand-anything-plugin/packages/dashboard/src/themes/ThemeContext.tsx b/understand-anything-plugin/packages/dashboard/src/themes/ThemeContext.tsx index d166e55..dc12fcc 100644 --- a/understand-anything-plugin/packages/dashboard/src/themes/ThemeContext.tsx +++ b/understand-anything-plugin/packages/dashboard/src/themes/ThemeContext.tsx @@ -75,7 +75,7 @@ export function ThemeProvider({ metaTheme, children }: ThemeProviderProps) { }, [metaTheme]); const setPreset = useCallback((presetId: PresetId) => { - setConfig((prev) => { + setConfig((_prev) => { const newPreset = getPreset(presetId); return { presetId, accentId: newPreset.defaultAccentId }; }); From b4d541b312c464ed0b2c3b79a2f10db747503a4e Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Fri, 27 Mar 2026 09:56:15 +0800 Subject: [PATCH 11/12] feat(dashboard): add light theme CSS overrides and align @theme defaults Co-Authored-By: Claude Opus 4.6 --- .../packages/dashboard/src/index.css | 67 +++++++++++++------ 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/understand-anything-plugin/packages/dashboard/src/index.css b/understand-anything-plugin/packages/dashboard/src/index.css index 321702c..e702b6f 100644 --- a/understand-anything-plugin/packages/dashboard/src/index.css +++ b/understand-anything-plugin/packages/dashboard/src/index.css @@ -1,26 +1,39 @@ @import "tailwindcss"; @theme { - /* Dark luxury color palette */ + /* Base */ --color-root: #0a0a0a; --color-surface: #111111; --color-elevated: #1a1a1a; --color-panel: #141414; - /* Accent spectrum */ + /* Accent */ --color-accent: #d4a574; --color-accent-dim: #c9a96e; --color-accent-bright: #e8c49a; - /* Text hierarchy */ + /* Text */ --color-text-primary: #f5f0eb; --color-text-secondary: #a39787; --color-text-muted: #6b5f53; - /* Border tokens */ + /* Borders */ --color-border-subtle: rgba(212, 165, 116, 0.12); --color-border-medium: rgba(212, 165, 116, 0.25); + /* Node types */ + --color-node-file: #4a7c9b; + --color-node-function: #5a9e6f; + --color-node-class: #8b6fb0; + --color-node-module: #c9a06c; + --color-node-concept: #b07a8a; + + /* Diff */ + --color-diff-changed: #e05252; + --color-diff-affected: #d4a030; + --color-diff-changed-dim: rgba(224, 82, 82, 0.25); + --color-diff-affected-dim: rgba(212, 160, 48, 0.25); + /* Glass */ --glass-bg: rgba(20, 20, 20, 0.8); --glass-bg-heavy: rgba(20, 20, 20, 0.95); @@ -41,33 +54,24 @@ --color-edge-dim: rgba(212, 165, 116, 0.08); --color-edge-dot: rgba(212, 165, 116, 0.15); - /* Layer group (accent-based overlays) */ + /* Accent overlays */ --color-accent-overlay-bg: rgba(212, 165, 116, 0.05); --color-accent-overlay-border: rgba(212, 165, 116, 0.25); - /* kbd */ + /* Kbd */ --kbd-bg: rgba(212, 165, 116, 0.1); - /* Node type colors (muted, refined) */ - --color-node-file: #4a7c9b; - --color-node-function: #5a9e6f; - --color-node-class: #8b6fb0; - --color-node-module: #c9a06c; - --color-node-concept: #b07a8a; - - /* Diff overlay colors */ - --color-diff-changed: #e05252; - --color-diff-affected: #d4a030; - --color-diff-changed-dim: rgba(224, 82, 82, 0.25); - --color-diff-affected-dim: rgba(212, 160, 48, 0.25); - - /* Fonts */ + /* Typography */ --font-serif: 'DM Serif Display', Georgia, serif; --font-mono: 'JetBrains Mono', 'Fira Code', monospace; --font-sans: 'Inter', system-ui, sans-serif; } /* Base styles */ +html { + transition: background-color 0.2s ease, color 0.2s ease; +} + body { font-family: var(--font-sans); background-color: var(--color-root); @@ -207,3 +211,26 @@ body { .react-flow__background { background-color: var(--color-root) !important; } + +/* Light theme overrides */ +[data-theme="light"] { + color-scheme: light; +} + +[data-theme="light"] .diff-faded { + opacity: 0.35; +} + +[data-theme="light"] ::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.05); +} + +[data-theme="light"] .warning-banner { + background: rgba(180, 130, 30, 0.1); + border-color: rgba(180, 130, 30, 0.3); + color: #92600a; +} + +[data-theme="dark"] { + color-scheme: dark; +} From 34cfffc4cc66f50f809c2e3910b3a5dfbefe1e6b Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Fri, 27 Mar 2026 10:02:38 +0800 Subject: [PATCH 12/12] fix(dashboard): resolve light theme visual issues from code review - Use CSS variables for diff edge colors in GraphView - Make ReactFlow colorMode respond to theme preset - Replace hardcoded text-white/bg-gray-900 in LearnPanel - Use CSS variables for .kbd border and box-shadow Co-Authored-By: Claude Opus 4.6 --- .../packages/dashboard/src/components/GraphView.tsx | 8 +++++--- .../packages/dashboard/src/components/LearnPanel.tsx | 6 +++--- .../packages/dashboard/src/index.css | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx b/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx index 3f6db5c..561feff 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx @@ -16,6 +16,7 @@ import "@xyflow/react/dist/style.css"; import CustomNode from "./CustomNode"; import type { CustomFlowNode } from "./CustomNode"; import { useDashboardStore } from "../store"; +import { useTheme } from "../themes/index.ts"; import { applyDagreLayout, applyDagreLayoutAsync, NODE_WIDTH, NODE_HEIGHT } from "../utils/layout"; const LAYER_PADDING = 40; @@ -153,8 +154,8 @@ function buildTopologyData( style: isImpacted ? { stroke: sourceInDiff && targetInDiff - ? "rgba(224, 82, 82, 0.7)" - : "rgba(212, 160, 48, 0.5)", + ? "var(--color-diff-changed)" + : "var(--color-diff-affected)", strokeWidth: 2.5, } : diffMode @@ -309,6 +310,7 @@ function GraphViewInner() { const diffMode = useDashboardStore((s) => s.diffMode); const changedNodeIds = useDashboardStore((s) => s.changedNodeIds); const affectedNodeIds = useDashboardStore((s) => s.affectedNodeIds); + const { preset } = useTheme(); const [layouting, setLayouting] = useState(false); @@ -453,7 +455,7 @@ function GraphViewInner() { fitViewOptions={{ minZoom: 0.01, padding: 0.1 }} minZoom={0.01} maxZoom={2} - colorMode="dark" + colorMode={preset.isDark ? "dark" : "light"} > diff --git a/understand-anything-plugin/packages/dashboard/src/components/LearnPanel.tsx b/understand-anything-plugin/packages/dashboard/src/components/LearnPanel.tsx index 09e5a9c..7e4344b 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/LearnPanel.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/LearnPanel.tsx @@ -122,16 +122,16 @@ export default function LearnPanel() {

{children}

), strong: ({ children }) => ( - {children} + {children} ), code: ({ className, children }) => { const isBlock = className?.includes("language-"); return isBlock ? ( - + {children} ) : ( - + {children} ); diff --git a/understand-anything-plugin/packages/dashboard/src/index.css b/understand-anything-plugin/packages/dashboard/src/index.css index e702b6f..95b5440 100644 --- a/understand-anything-plugin/packages/dashboard/src/index.css +++ b/understand-anything-plugin/packages/dashboard/src/index.css @@ -122,9 +122,9 @@ body { font-weight: 600; color: var(--color-accent); background: var(--kbd-bg); - border: 1px solid rgba(212, 165, 116, 0.3); + border: 1px solid var(--color-border-medium); border-radius: 0.25rem; - box-shadow: 0 1px 0 rgba(212, 165, 116, 0.2); + box-shadow: 0 1px 0 var(--scrollbar-thumb); } /* Animation keyframes */