416 lines
16 KiB
Markdown
416 lines
16 KiB
Markdown
# 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<string, string>; // 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 `<html>` 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)
|