Files
Fulfilled-Knowledge/Understand-Anything-main/docs/superpowers/specs/2026-03-26-theme-system-design.md
2026-05-27 15:40:32 +08:00

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) 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:

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:

  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:

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)