diff --git a/understand-anything-plugin/packages/core/src/types.ts b/understand-anything-plugin/packages/core/src/types.ts index f5fef33..da79bda 100644 --- a/understand-anything-plugin/packages/core/src/types.ts +++ b/understand-anything-plugin/packages/core/src/types.ts @@ -66,12 +66,19 @@ export interface KnowledgeGraph { tour: TourStep[]; } +// Theme configuration (for dashboard customization) +export interface ThemeConfig { + presetId: string; + accentId: string; +} + // AnalysisMeta (for persistence) export interface AnalysisMeta { lastAnalyzedAt: string; gitCommitHash: string; version: string; analyzedFiles: number; + theme?: ThemeConfig; } // Plugin interfaces diff --git a/understand-anything-plugin/packages/dashboard/src/App.tsx b/understand-anything-plugin/packages/dashboard/src/App.tsx index 58fef79..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,9 +212,10 @@ function App() {
+
+ ); } diff --git a/understand-anything-plugin/packages/dashboard/src/components/CodeViewer.tsx b/understand-anything-plugin/packages/dashboard/src/components/CodeViewer.tsx index d1df7c3..64fe249 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/CodeViewer.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/CodeViewer.tsx @@ -27,8 +27,8 @@ export default function CodeViewer() { className="text-[10px] font-semibold uppercase tracking-wider px-2 py-0.5 rounded border" style={{ color: "var(--color-node-file)", - borderColor: "rgba(74,124,155,0.3)", - backgroundColor: "rgba(74,124,155,0.1)", + borderColor: "color-mix(in srgb, var(--color-node-file) 30%, transparent)", + backgroundColor: "color-mix(in srgb, var(--color-node-file) 10%, transparent)", }} > {node.type} @@ -56,14 +56,14 @@ export default function CodeViewer() {
{/* Summary */}
-

Summary

+

Summary

{node.summary}

{/* Language notes callout */} {node.languageNotes && ( -
-

Language Notes

+
+

Language Notes

{node.languageNotes}

)} @@ -71,7 +71,7 @@ export default function CodeViewer() { {/* Tags */} {node.tags.length > 0 && (
-

Tags

+

Tags

{node.tags.map((tag) => ( diff --git a/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx b/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx index 111d7a7..d2ec838 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx @@ -20,7 +20,7 @@ const typeTextColors: Record = { const complexityColors: Record = { simple: "text-node-function", - moderate: "text-gold-dim", + moderate: "text-accent-dim", complex: "text-[#c97070]", }; @@ -51,17 +51,17 @@ function CustomNodeComponent({ let extraClass = ""; if (data.isSelected) { - extraClass = "ring-2 ring-gold node-glow"; + extraClass = "ring-2 ring-accent node-glow"; } else if (data.isTourHighlighted) { - extraClass = "ring-2 ring-gold-dim animate-gold-pulse"; + extraClass = "ring-2 ring-accent-dim animate-accent-pulse"; } else if (data.isHighlighted) { const score = data.searchScore ?? 1; if (score <= 0.1) { - extraClass = "ring-2 ring-gold-bright"; + extraClass = "ring-2 ring-accent-bright"; } else if (score <= 0.3) { - extraClass = "ring-2 ring-gold"; + extraClass = "ring-2 ring-accent"; } else { - extraClass = "ring-1 ring-gold-dim/60"; + extraClass = "ring-1 ring-accent-dim/60"; } } diff --git a/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx b/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx index b1d6fe3..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,16 +154,16 @@ 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 - ? { stroke: "rgba(212,165,116,0.08)", strokeWidth: 1 } - : { stroke: "rgba(212,165,116,0.3)", strokeWidth: 1.5 }, + ? { stroke: "var(--color-edge-dim)", strokeWidth: 1 } + : { stroke: "var(--color-edge)", strokeWidth: 1.5 }, labelStyle: diffMode && !isImpacted - ? { fill: "rgba(163,151,135,0.3)", fontSize: 10 } - : { fill: "#a39787", fontSize: 10 }, + ? { fill: "var(--color-text-muted)", fontSize: 10 } + : { fill: "var(--color-text-secondary)", fontSize: 10 }, }; }); @@ -262,13 +263,13 @@ function applyLayerGroups( style: { width: groupWidth, height: groupHeight, - backgroundColor: "rgba(212,165,116,0.05)", + backgroundColor: "var(--color-accent-overlay-bg)", borderRadius: 12, - border: "2px dashed rgba(212,165,116,0.25)", + border: "2px dashed var(--color-accent-overlay-border)", padding: 8, fontSize: 13, fontWeight: 600, - color: "#d4a574", + color: "var(--color-accent)", }, }); @@ -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); @@ -429,7 +431,7 @@ function GraphViewInner() { {layouting && (
-
+

Laying out {topoNodes.length.toLocaleString()} nodes...

@@ -453,13 +455,13 @@ 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/KeyboardShortcutsHelp.tsx b/understand-anything-plugin/packages/dashboard/src/components/KeyboardShortcutsHelp.tsx index 22178f5..c9fcd1f 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/KeyboardShortcutsHelp.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/KeyboardShortcutsHelp.tsx @@ -62,7 +62,7 @@ export default function KeyboardShortcutsHelp({
{Object.entries(groupedShortcuts).map(([category, categoryShortcuts]) => (
-

+

{category}

diff --git a/understand-anything-plugin/packages/dashboard/src/components/LayerLegend.tsx b/understand-anything-plugin/packages/dashboard/src/components/LayerLegend.tsx index 6d4fc24..f98e389 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/LayerLegend.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/LayerLegend.tsx @@ -47,7 +47,7 @@ export default function LayerLegend() { disabled={!hasLayers} className={`px-2 py-0.5 rounded text-[11px] font-medium transition-colors ${ showLayers && hasLayers - ? "bg-gold/20 text-gold" + ? "bg-accent/20 text-accent" : hasLayers ? "bg-elevated text-text-secondary hover:bg-surface" : "bg-elevated text-text-muted cursor-not-allowed" diff --git a/understand-anything-plugin/packages/dashboard/src/components/LearnPanel.tsx b/understand-anything-plugin/packages/dashboard/src/components/LearnPanel.tsx index 2534fd4..7e4344b 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/LearnPanel.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/LearnPanel.tsx @@ -47,13 +47,13 @@ export default function LearnPanel() {
-

+

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 */}
@@ -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} ); @@ -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) => ( +
+
+
+ )} +
+ ); +} diff --git a/understand-anything-plugin/packages/dashboard/src/index.css b/understand-anything-plugin/packages/dashboard/src/index.css index a0f8ffa..95b5440 100644 --- a/understand-anything-plugin/packages/dashboard/src/index.css +++ b/understand-anything-plugin/packages/dashboard/src/index.css @@ -1,46 +1,77 @@ @import "tailwindcss"; @theme { - /* Dark luxury color palette */ + /* Base */ --color-root: #0a0a0a; --color-surface: #111111; --color-elevated: #1a1a1a; --color-panel: #141414; - /* Gold accent spectrum */ - --color-gold: #d4a574; - --color-gold-dim: #c9a96e; - --color-gold-bright: #e8c49a; + /* 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 type colors (muted, refined) */ + /* Node types */ --color-node-file: #4a7c9b; --color-node-function: #5a9e6f; --color-node-class: #8b6fb0; --color-node-module: #c9a06c; --color-node-concept: #b07a8a; - /* Diff overlay colors */ + /* 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); - /* Fonts */ + /* 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; } /* Base styles */ +html { + transition: background-color 0.2s ease, color 0.2s ease; +} + body { font-family: var(--font-sans); background-color: var(--color-root); @@ -65,15 +96,15 @@ body { /* Glass utility */ .glass { - background: rgba(20, 20, 20, 0.8); - border: 1px solid rgba(212, 165, 116, 0.1); + background: var(--glass-bg); + border: 1px solid var(--glass-border); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); } .glass-heavy { - background: rgba(20, 20, 20, 0.95); - border: 1px solid rgba(212, 165, 116, 0.15); + background: var(--glass-bg-heavy); + border: 1px solid var(--glass-border-heavy); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); } @@ -89,11 +120,11 @@ body { font-family: var(--font-mono); font-size: 0.75rem; font-weight: 600; - color: var(--color-gold); - background: rgba(212, 165, 116, 0.1); - border: 1px solid rgba(212, 165, 116, 0.3); + color: var(--color-accent); + background: var(--kbd-bg); + 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 */ @@ -117,12 +148,12 @@ body { } } -@keyframes goldPulse { +@keyframes accentPulse { 0%, 100% { - box-shadow: 0 0 0 0 rgba(212, 165, 116, 0.4); + box-shadow: 0 0 8px var(--glow-accent-strong); } 50% { - box-shadow: 0 0 20px 4px rgba(212, 165, 116, 0.15); + box-shadow: 0 0 20px var(--glow-accent-pulse); } } @@ -135,13 +166,13 @@ body { animation: slideUp 0.3s ease-out forwards; } -.animate-gold-pulse { - animation: goldPulse 2s ease-in-out infinite; +.animate-accent-pulse { + animation: accentPulse 2s ease-in-out infinite; } /* Node selection glow */ .node-glow { - box-shadow: 0 0 20px rgba(212, 165, 116, 0.15); + box-shadow: 0 0 20px var(--glow-accent); } /* Diff overlay glow effects */ @@ -169,14 +200,37 @@ body { background: transparent; } ::-webkit-scrollbar-thumb { - background: rgba(212, 165, 116, 0.2); - border-radius: 3px; + background: var(--scrollbar-thumb); + border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { - background: rgba(212, 165, 116, 0.35); + background: var(--scrollbar-thumb-hover); } /* Override React Flow dark theme */ .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; +} diff --git a/understand-anything-plugin/packages/dashboard/src/themes/ThemeContext.tsx b/understand-anything-plugin/packages/dashboard/src/themes/ThemeContext.tsx new file mode 100644 index 0000000..dc12fcc --- /dev/null +++ b/understand-anything-plugin/packages/dashboard/src/themes/ThemeContext.tsx @@ -0,0 +1,101 @@ +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; +} diff --git a/understand-anything-plugin/packages/dashboard/src/themes/index.ts b/understand-anything-plugin/packages/dashboard/src/themes/index.ts new file mode 100644 index 0000000..c033d59 --- /dev/null +++ b/understand-anything-plugin/packages/dashboard/src/themes/index.ts @@ -0,0 +1,5 @@ +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"; diff --git a/understand-anything-plugin/packages/dashboard/src/themes/presets.ts b/understand-anything-plugin/packages/dashboard/src/themes/presets.ts new file mode 100644 index 0000000..35e0517 --- /dev/null +++ b/understand-anything-plugin/packages/dashboard/src/themes/presets.ts @@ -0,0 +1,143 @@ +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] + ); +} diff --git a/understand-anything-plugin/packages/dashboard/src/themes/theme-engine.ts b/understand-anything-plugin/packages/dashboard/src/themes/theme-engine.ts new file mode 100644 index 0000000..23004ca --- /dev/null +++ b/understand-anything-plugin/packages/dashboard/src/themes/theme-engine.ts @@ -0,0 +1,56 @@ +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"); +} diff --git a/understand-anything-plugin/packages/dashboard/src/themes/types.ts b/understand-anything-plugin/packages/dashboard/src/themes/types.ts new file mode 100644 index 0000000..2d09590 --- /dev/null +++ b/understand-anything-plugin/packages/dashboard/src/themes/types.ts @@ -0,0 +1,33 @@ +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", +};