16 KiB
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) andmeta.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:
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
// 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):
// 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
// themes/ThemeContext.tsx
interface ThemeContextValue {
config: ThemeConfig;
preset: ThemePreset;
setPreset: (presetId: PresetId) => void;
setAccent: (accentId: string) => void;
}
The provider:
- On mount: resolves theme from
localStorage>meta.jsonfield in loaded graph > default (dark-gold) - Calls
applyTheme()on every config change - Persists to
localStorageon every change - Does NOT write to
meta.jsonfrom 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.2sonhtmlfor 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:
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 effectsrgba(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:
- Audit: grep for hardcoded hex/rgba values in component files
- Replace with CSS variables: create new variables where needed (e.g.,
--edge-color,--edge-color-dim) - Glass classes: update
.glassand.glass-heavyinindex.cssto use variables - Scrollbar: update scrollbar styles to use variables
- Glow effects: update
.node-glow,.diff-changed-glow,.diff-affected-glowto 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
AnalysisMetatype with optionaltheme?: ThemeConfig - Export
ThemeConfigandPresetIdtypes from./typessubpath
packages/dashboard
- New
themes/directory with types, presets, engine, and context - New
ThemePickercomponent 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)