diff --git a/hermes_cli/config.py b/hermes_cli/config.py
index a85997f8f..eed9d5c3a 100644
--- a/hermes_cli/config.py
+++ b/hermes_cli/config.py
@@ -559,6 +559,11 @@ DEFAULT_CONFIG = {
"platforms": {}, # Per-platform display overrides: {"telegram": {"tool_progress": "all"}, "slack": {"tool_progress": "off"}}
},
+ # Web dashboard settings
+ "dashboard": {
+ "theme": "default", # Dashboard visual theme: "default", "midnight", "ember", "mono", "cyberpunk", "rose"
+ },
+
# Privacy settings
"privacy": {
"redact_pii": False, # When True, hash user IDs and strip phone numbers from LLM context
diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py
index 22265faa5..4d39d379b 100644
--- a/hermes_cli/web_server.py
+++ b/hermes_cli/web_server.py
@@ -96,6 +96,7 @@ _PUBLIC_API_PATHS: frozenset = frozenset({
"/api/config/defaults",
"/api/config/schema",
"/api/model/info",
+ "/api/dashboard/themes",
})
@@ -166,6 +167,11 @@ _SCHEMA_OVERRIDES: Dict[str, Dict[str, Any]] = {
"description": "CLI visual theme",
"options": ["default", "ares", "mono", "slate"],
},
+ "dashboard.theme": {
+ "type": "select",
+ "description": "Web dashboard visual theme",
+ "options": ["default", "midnight", "ember", "mono", "cyberpunk", "rose"],
+ },
"display.resume_display": {
"type": "select",
"description": "How resumed sessions display history",
@@ -224,6 +230,7 @@ _CATEGORY_MERGE: Dict[str, str] = {
"approvals": "security",
"human_delay": "display",
"smart_model_routing": "agent",
+ "dashboard": "display",
}
# Display order for tabs — unlisted categories sort alphabetically after these.
@@ -2068,6 +2075,76 @@ def mount_spa(application: FastAPI):
return _serve_index()
+# ---------------------------------------------------------------------------
+# Dashboard theme endpoints
+# ---------------------------------------------------------------------------
+
+# Built-in dashboard themes — label + description only. The actual color
+# definitions live in the frontend (web/src/themes/presets.ts).
+_BUILTIN_DASHBOARD_THEMES = [
+ {"name": "default", "label": "Hermes Teal", "description": "Classic dark teal — the canonical Hermes look"},
+ {"name": "midnight", "label": "Midnight", "description": "Deep blue-violet with cool accents"},
+ {"name": "ember", "label": "Ember", "description": "Warm crimson and bronze — forge vibes"},
+ {"name": "mono", "label": "Mono", "description": "Clean grayscale — minimal and focused"},
+ {"name": "cyberpunk", "label": "Cyberpunk", "description": "Neon green on black — matrix terminal"},
+ {"name": "rose", "label": "Rosé", "description": "Soft pink and warm ivory — easy on the eyes"},
+]
+
+
+def _discover_user_themes() -> list:
+ """Scan ~/.hermes/dashboard-themes/*.yaml for user-created themes."""
+ themes_dir = get_hermes_home() / "dashboard-themes"
+ if not themes_dir.is_dir():
+ return []
+ result = []
+ for f in sorted(themes_dir.glob("*.yaml")):
+ try:
+ data = yaml.safe_load(f.read_text(encoding="utf-8"))
+ if isinstance(data, dict) and data.get("name"):
+ result.append({
+ "name": data["name"],
+ "label": data.get("label", data["name"]),
+ "description": data.get("description", ""),
+ })
+ except Exception:
+ continue
+ return result
+
+
+@app.get("/api/dashboard/themes")
+async def get_dashboard_themes():
+ """Return available themes and the currently active one."""
+ config = load_config()
+ active = config.get("dashboard", {}).get("theme", "default")
+ user_themes = _discover_user_themes()
+ # Merge built-in + user, user themes override built-in by name.
+ seen = set()
+ themes = []
+ for t in _BUILTIN_DASHBOARD_THEMES:
+ seen.add(t["name"])
+ themes.append(t)
+ for t in user_themes:
+ if t["name"] not in seen:
+ themes.append(t)
+ seen.add(t["name"])
+ return {"themes": themes, "active": active}
+
+
+class ThemeSetBody(BaseModel):
+ name: str
+
+
+@app.put("/api/dashboard/theme")
+async def set_dashboard_theme(body: ThemeSetBody):
+ """Set the active dashboard theme (persists to config.yaml)."""
+ config = load_config()
+ if "dashboard" not in config:
+ config["dashboard"] = {}
+ config["dashboard"]["theme"] = body.name
+ save_config(config)
+ return {"ok": True, "theme": body.name}
+
+
mount_spa(app)
diff --git a/web/src/App.tsx b/web/src/App.tsx
index 4bbc13fac..dfadf1067 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -9,6 +9,7 @@ import AnalyticsPage from "@/pages/AnalyticsPage";
import CronPage from "@/pages/CronPage";
import SkillsPage from "@/pages/SkillsPage";
import { LanguageSwitcher } from "@/components/LanguageSwitcher";
+import { ThemeSwitcher } from "@/components/ThemeSwitcher";
import { useI18n } from "@/i18n";
const NAV_ITEMS = [
@@ -67,6 +68,7 @@ export default function App() {
+
{t.app.webUi}
diff --git a/web/src/components/ThemeSwitcher.tsx b/web/src/components/ThemeSwitcher.tsx
new file mode 100644
index 000000000..03801bebf
--- /dev/null
+++ b/web/src/components/ThemeSwitcher.tsx
@@ -0,0 +1,115 @@
+import { useState, useRef, useEffect, useCallback } from "react";
+import { Palette, Check } from "lucide-react";
+import { useTheme } from "@/themes";
+import { useI18n } from "@/i18n";
+import { cn } from "@/lib/utils";
+
+/**
+ * Compact theme picker for the dashboard header.
+ * Shows a palette icon + current theme name; opens a dropdown of all
+ * available themes with color swatches for instant preview.
+ */
+export function ThemeSwitcher() {
+ const { themeName, availableThemes, setTheme } = useTheme();
+ const { t } = useI18n();
+ const [open, setOpen] = useState(false);
+ const ref = useRef(null);
+
+ const close = useCallback(() => setOpen(false), []);
+
+ // Close on outside click.
+ useEffect(() => {
+ if (!open) return;
+ const handler = (e: MouseEvent) => {
+ if (ref.current && !ref.current.contains(e.target as Node)) close();
+ };
+ document.addEventListener("mousedown", handler);
+ return () => document.removeEventListener("mousedown", handler);
+ }, [open, close]);
+
+ // Close on Escape.
+ useEffect(() => {
+ if (!open) return;
+ const handler = (e: KeyboardEvent) => {
+ if (e.key === "Escape") close();
+ };
+ document.addEventListener("keydown", handler);
+ return () => document.removeEventListener("keydown", handler);
+ }, [open, close]);
+
+ const current = availableThemes.find((t) => t.name === themeName);
+
+ return (
+
+
+
+ {open && (
+
+
+
+ {t.theme?.title ?? "Theme"}
+
+
+
+ {availableThemes.map((theme) => {
+ const isActive = theme.name === themeName;
+ return (
+
+ );
+ })}
+
+ )}
+
+ );
+}
diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts
index 3bf693f21..07e931995 100644
--- a/web/src/i18n/en.ts
+++ b/web/src/i18n/en.ts
@@ -275,4 +275,9 @@ export const en: Translations = {
language: {
switchTo: "Switch to Chinese",
},
+
+ theme: {
+ title: "Theme",
+ switchTheme: "Switch theme",
+ },
};
diff --git a/web/src/i18n/types.ts b/web/src/i18n/types.ts
index 34813c68f..55f5cffc4 100644
--- a/web/src/i18n/types.ts
+++ b/web/src/i18n/types.ts
@@ -287,4 +287,10 @@ export interface Translations {
language: {
switchTo: string;
};
+
+ // ── Theme switcher ──
+ theme: {
+ title: string;
+ switchTheme: string;
+ };
}
diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts
index 18cb3ee38..869ec9ed9 100644
--- a/web/src/i18n/zh.ts
+++ b/web/src/i18n/zh.ts
@@ -275,4 +275,9 @@ export const zh: Translations = {
language: {
switchTo: "切换到英文",
},
+
+ theme: {
+ title: "主题",
+ switchTheme: "切换主题",
+ },
};
diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts
index e61043993..9121b83cd 100644
--- a/web/src/lib/api.ts
+++ b/web/src/lib/api.ts
@@ -182,6 +182,16 @@ export const api = {
},
);
},
+
+ // Dashboard themes
+ getThemes: () =>
+ fetchJSON("/api/dashboard/themes"),
+ setTheme: (name: string) =>
+ fetchJSON<{ ok: boolean; theme: string }>("/api/dashboard/theme", {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ name }),
+ }),
};
export interface PlatformStatus {
@@ -415,3 +425,10 @@ export interface OAuthPollResponse {
error_message?: string | null;
expires_at?: number | null;
}
+
+// ── Dashboard theme types ──────────────────────────────────────────────
+
+export interface ThemeListResponse {
+ themes: Array<{ name: string; label: string; description: string }>;
+ active: string;
+}
diff --git a/web/src/main.tsx b/web/src/main.tsx
index 3b77464d5..f04290ada 100644
--- a/web/src/main.tsx
+++ b/web/src/main.tsx
@@ -3,11 +3,14 @@ import { BrowserRouter } from "react-router-dom";
import "./index.css";
import App from "./App";
import { I18nProvider } from "./i18n";
+import { ThemeProvider } from "./themes";
createRoot(document.getElementById("root")!).render(
-
+
+
+
,
);
diff --git a/web/src/themes/context.tsx b/web/src/themes/context.tsx
new file mode 100644
index 000000000..cdceb1532
--- /dev/null
+++ b/web/src/themes/context.tsx
@@ -0,0 +1,169 @@
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useState,
+ type ReactNode,
+} from "react";
+import type { DashboardTheme, ThemeColors, ThemeOverlay } from "./types";
+import { BUILTIN_THEMES, defaultTheme } from "./presets";
+import { api } from "@/lib/api";
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+/** Apply a theme's color overrides to `document.documentElement`. */
+function applyColors(colors: ThemeColors) {
+ const root = document.documentElement;
+ for (const [key, value] of Object.entries(colors)) {
+ root.style.setProperty(`--color-${key}`, value);
+ }
+}
+
+/** Apply overlay overrides (noise + warm-glow). */
+function applyOverlay(overlay: ThemeOverlay | undefined) {
+ const noiseEl = document.querySelector(".noise-overlay");
+ const glowEl = document.querySelector(".warm-glow");
+
+ if (noiseEl) {
+ noiseEl.style.opacity = String(overlay?.noiseOpacity ?? 0.10);
+ noiseEl.style.mixBlendMode = overlay?.noiseBlendMode ?? "color-dodge";
+ }
+ if (glowEl) {
+ glowEl.style.opacity = String(overlay?.warmGlowOpacity ?? 0.22);
+ if (overlay?.warmGlowColor) {
+ glowEl.style.background = `radial-gradient(ellipse at 0% 0%, ${overlay.warmGlowColor} 0%, rgba(0,0,0,0) 60%)`;
+ }
+ }
+}
+
+/** Remove all inline overrides — reverts to stylesheet defaults. */
+function clearOverrides() {
+ const root = document.documentElement;
+ // Clear color overrides
+ for (const key of Object.keys(defaultTheme.colors)) {
+ root.style.removeProperty(`--color-${key}`);
+ }
+ // Clear overlay overrides
+ const noiseEl = document.querySelector(".noise-overlay");
+ const glowEl = document.querySelector(".warm-glow");
+ if (noiseEl) {
+ noiseEl.style.opacity = "";
+ noiseEl.style.mixBlendMode = "";
+ }
+ if (glowEl) {
+ glowEl.style.opacity = "";
+ glowEl.style.background = "";
+ }
+}
+
+function applyTheme(theme: DashboardTheme) {
+ if (theme.name === "default") {
+ clearOverrides();
+ } else {
+ applyColors(theme.colors);
+ applyOverlay(theme.overlay);
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Context
+// ---------------------------------------------------------------------------
+
+interface ThemeContextValue {
+ /** Currently active theme name. */
+ themeName: string;
+ /** Currently active theme object. */
+ theme: DashboardTheme;
+ /** Available theme names (built-in + any server-provided custom themes). */
+ availableThemes: Array<{ name: string; label: string; description: string }>;
+ /** Switch theme — applies CSS immediately and persists to config.yaml. */
+ setTheme: (name: string) => void;
+ /** True while initial theme is loading from server. */
+ loading: boolean;
+}
+
+const ThemeContext = createContext({
+ themeName: "default",
+ theme: defaultTheme,
+ availableThemes: Object.values(BUILTIN_THEMES).map((t) => ({
+ name: t.name,
+ label: t.label,
+ description: t.description,
+ })),
+ setTheme: () => {},
+ loading: true,
+});
+
+// ---------------------------------------------------------------------------
+// Provider
+// ---------------------------------------------------------------------------
+
+export function ThemeProvider({ children }: { children: ReactNode }) {
+ const [themeName, setThemeName] = useState("default");
+ const [availableThemes, setAvailableThemes] = useState(
+ Object.values(BUILTIN_THEMES).map((t) => ({
+ name: t.name,
+ label: t.label,
+ description: t.description,
+ })),
+ );
+ const [loading, setLoading] = useState(true);
+
+ // Fetch active theme + available list from server on mount.
+ useEffect(() => {
+ api
+ .getThemes()
+ .then((resp) => {
+ if (resp.themes?.length) {
+ setAvailableThemes(resp.themes);
+ }
+ if (resp.active && resp.active !== "default") {
+ setThemeName(resp.active);
+ const t = BUILTIN_THEMES[resp.active];
+ if (t) applyTheme(t);
+ }
+ })
+ .catch(() => {
+ // Server might not support theme API yet — stay on default.
+ })
+ .finally(() => setLoading(false));
+ }, []);
+
+ const resolvedTheme = BUILTIN_THEMES[themeName] ?? defaultTheme;
+
+ const setTheme = useCallback(
+ (name: string) => {
+ const t = BUILTIN_THEMES[name] ?? defaultTheme;
+ setThemeName(t.name);
+ applyTheme(t);
+ // Persist to config.yaml — fire and forget.
+ api.setTheme(t.name).catch(() => {});
+ },
+ [],
+ );
+
+ return (
+
+ {children}
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Hook
+// ---------------------------------------------------------------------------
+
+export function useTheme() {
+ return useContext(ThemeContext);
+}
diff --git a/web/src/themes/index.ts b/web/src/themes/index.ts
new file mode 100644
index 000000000..2c3509e8e
--- /dev/null
+++ b/web/src/themes/index.ts
@@ -0,0 +1,3 @@
+export { ThemeProvider, useTheme } from "./context";
+export { BUILTIN_THEMES } from "./presets";
+export type { DashboardTheme, ThemeColors, ThemeOverlay, ThemeListResponse } from "./types";
diff --git a/web/src/themes/presets.ts b/web/src/themes/presets.ts
new file mode 100644
index 000000000..65fcd4655
--- /dev/null
+++ b/web/src/themes/presets.ts
@@ -0,0 +1,229 @@
+import type { DashboardTheme } from "./types";
+
+/**
+ * Built-in dashboard themes.
+ *
+ * The "default" theme matches the current index.css @theme values exactly,
+ * so applying it is a no-op (CSS vars stay at their stylesheet defaults).
+ * Other themes override only what they change.
+ */
+
+export const defaultTheme: DashboardTheme = {
+ name: "default",
+ label: "Hermes Teal",
+ description: "Classic dark teal — the canonical Hermes look",
+ colors: {
+ background: "#041C1C",
+ foreground: "#ffe6cb",
+ card: "#062424",
+ "card-foreground": "#ffe6cb",
+ primary: "#ffe6cb",
+ "primary-foreground": "#041C1C",
+ secondary: "#0a2e2e",
+ "secondary-foreground": "#ffe6cb",
+ muted: "#083030",
+ "muted-foreground": "#8aaa9a",
+ accent: "#0c3838",
+ "accent-foreground": "#ffe6cb",
+ destructive: "#fb2c36",
+ "destructive-foreground": "#fff",
+ success: "#4ade80",
+ warning: "#ffbd38",
+ border: "color-mix(in srgb, #ffe6cb 15%, transparent)",
+ input: "color-mix(in srgb, #ffe6cb 15%, transparent)",
+ ring: "#ffe6cb",
+ popover: "#062424",
+ "popover-foreground": "#ffe6cb",
+ },
+ overlay: {
+ noiseOpacity: 0.10,
+ noiseBlendMode: "color-dodge",
+ warmGlowOpacity: 0.22,
+ warmGlowColor: "rgba(255,189,56,0.35)",
+ },
+};
+
+export const midnightTheme: DashboardTheme = {
+ name: "midnight",
+ label: "Midnight",
+ description: "Deep blue-violet with cool accents",
+ colors: {
+ background: "#0a0a1a",
+ foreground: "#e0e0f0",
+ card: "#10102a",
+ "card-foreground": "#e0e0f0",
+ primary: "#a78bfa",
+ "primary-foreground": "#0a0a1a",
+ secondary: "#151530",
+ "secondary-foreground": "#e0e0f0",
+ muted: "#1a1a3a",
+ "muted-foreground": "#8888bb",
+ accent: "#1e1e44",
+ "accent-foreground": "#e0e0f0",
+ destructive: "#f43f5e",
+ "destructive-foreground": "#fff",
+ success: "#34d399",
+ warning: "#fbbf24",
+ border: "color-mix(in srgb, #a78bfa 15%, transparent)",
+ input: "color-mix(in srgb, #a78bfa 15%, transparent)",
+ ring: "#a78bfa",
+ popover: "#10102a",
+ "popover-foreground": "#e0e0f0",
+ },
+ overlay: {
+ noiseOpacity: 0.08,
+ noiseBlendMode: "color-dodge",
+ warmGlowOpacity: 0.15,
+ warmGlowColor: "rgba(120,80,220,0.3)",
+ },
+};
+
+export const emberTheme: DashboardTheme = {
+ name: "ember",
+ label: "Ember",
+ description: "Warm crimson and bronze — forge vibes",
+ colors: {
+ background: "#1a0a0a",
+ foreground: "#fde8d0",
+ card: "#241010",
+ "card-foreground": "#fde8d0",
+ primary: "#f97316",
+ "primary-foreground": "#1a0a0a",
+ secondary: "#2a1515",
+ "secondary-foreground": "#fde8d0",
+ muted: "#301818",
+ "muted-foreground": "#b08878",
+ accent: "#381e1e",
+ "accent-foreground": "#fde8d0",
+ destructive: "#ef4444",
+ "destructive-foreground": "#fff",
+ success: "#4ade80",
+ warning: "#fbbf24",
+ border: "color-mix(in srgb, #f97316 15%, transparent)",
+ input: "color-mix(in srgb, #f97316 15%, transparent)",
+ ring: "#f97316",
+ popover: "#241010",
+ "popover-foreground": "#fde8d0",
+ },
+ overlay: {
+ noiseOpacity: 0.10,
+ noiseBlendMode: "color-dodge",
+ warmGlowOpacity: 0.25,
+ warmGlowColor: "rgba(249,115,22,0.3)",
+ },
+};
+
+export const monoTheme: DashboardTheme = {
+ name: "mono",
+ label: "Mono",
+ description: "Clean grayscale — minimal and focused",
+ colors: {
+ background: "#111111",
+ foreground: "#e0e0e0",
+ card: "#1a1a1a",
+ "card-foreground": "#e0e0e0",
+ primary: "#e0e0e0",
+ "primary-foreground": "#111111",
+ secondary: "#1e1e1e",
+ "secondary-foreground": "#e0e0e0",
+ muted: "#222222",
+ "muted-foreground": "#888888",
+ accent: "#2a2a2a",
+ "accent-foreground": "#e0e0e0",
+ destructive: "#ef4444",
+ "destructive-foreground": "#fff",
+ success: "#4ade80",
+ warning: "#fbbf24",
+ border: "color-mix(in srgb, #e0e0e0 12%, transparent)",
+ input: "color-mix(in srgb, #e0e0e0 12%, transparent)",
+ ring: "#e0e0e0",
+ popover: "#1a1a1a",
+ "popover-foreground": "#e0e0e0",
+ },
+ overlay: {
+ noiseOpacity: 0.06,
+ noiseBlendMode: "color-dodge",
+ warmGlowOpacity: 0.0,
+ warmGlowColor: "rgba(255,255,255,0)",
+ },
+};
+
+export const cyberpunkTheme: DashboardTheme = {
+ name: "cyberpunk",
+ label: "Cyberpunk",
+ description: "Neon green on black — matrix terminal",
+ colors: {
+ background: "#050505",
+ foreground: "#00ff88",
+ card: "#0a0a0a",
+ "card-foreground": "#00ff88",
+ primary: "#00ff88",
+ "primary-foreground": "#050505",
+ secondary: "#0e0e0e",
+ "secondary-foreground": "#00ff88",
+ muted: "#121212",
+ "muted-foreground": "#00aa55",
+ accent: "#161616",
+ "accent-foreground": "#00ff88",
+ destructive: "#ff0055",
+ "destructive-foreground": "#fff",
+ success: "#00ff88",
+ warning: "#ffff00",
+ border: "color-mix(in srgb, #00ff88 12%, transparent)",
+ input: "color-mix(in srgb, #00ff88 12%, transparent)",
+ ring: "#00ff88",
+ popover: "#0a0a0a",
+ "popover-foreground": "#00ff88",
+ },
+ overlay: {
+ noiseOpacity: 0.12,
+ noiseBlendMode: "color-dodge",
+ warmGlowOpacity: 0.10,
+ warmGlowColor: "rgba(0,255,136,0.15)",
+ },
+};
+
+export const roseTheme: DashboardTheme = {
+ name: "rose",
+ label: "Rosé",
+ description: "Soft pink and warm ivory — easy on the eyes",
+ colors: {
+ background: "#1a1015",
+ foreground: "#f5e6e0",
+ card: "#221820",
+ "card-foreground": "#f5e6e0",
+ primary: "#f9a8d4",
+ "primary-foreground": "#1a1015",
+ secondary: "#281e28",
+ "secondary-foreground": "#f5e6e0",
+ muted: "#2e2230",
+ "muted-foreground": "#b08898",
+ accent: "#352838",
+ "accent-foreground": "#f5e6e0",
+ destructive: "#fb2c36",
+ "destructive-foreground": "#fff",
+ success: "#4ade80",
+ warning: "#fbbf24",
+ border: "color-mix(in srgb, #f9a8d4 14%, transparent)",
+ input: "color-mix(in srgb, #f9a8d4 14%, transparent)",
+ ring: "#f9a8d4",
+ popover: "#221820",
+ "popover-foreground": "#f5e6e0",
+ },
+ overlay: {
+ noiseOpacity: 0.08,
+ noiseBlendMode: "color-dodge",
+ warmGlowOpacity: 0.18,
+ warmGlowColor: "rgba(249,168,212,0.2)",
+ },
+};
+
+/** All built-in themes, keyed by name. */
+export const BUILTIN_THEMES: Record = {
+ default: defaultTheme,
+ midnight: midnightTheme,
+ ember: emberTheme,
+ mono: monoTheme,
+ cyberpunk: cyberpunkTheme,
+ rose: roseTheme,
+};
diff --git a/web/src/themes/types.ts b/web/src/themes/types.ts
new file mode 100644
index 000000000..b6cd371a5
--- /dev/null
+++ b/web/src/themes/types.ts
@@ -0,0 +1,44 @@
+/** Dashboard theme definition. Maps 1:1 to CSS custom properties in index.css. */
+export interface ThemeColors {
+ background: string;
+ foreground: string;
+ card: string;
+ "card-foreground": string;
+ primary: string;
+ "primary-foreground": string;
+ secondary: string;
+ "secondary-foreground": string;
+ muted: string;
+ "muted-foreground": string;
+ accent: string;
+ "accent-foreground": string;
+ destructive: string;
+ "destructive-foreground": string;
+ success: string;
+ warning: string;
+ border: string;
+ input: string;
+ ring: string;
+ popover: string;
+ "popover-foreground": string;
+}
+
+export interface ThemeOverlay {
+ noiseOpacity?: number;
+ noiseBlendMode?: string;
+ warmGlowOpacity?: number;
+ warmGlowColor?: string;
+}
+
+export interface DashboardTheme {
+ name: string;
+ label: string;
+ description: string;
+ colors: ThemeColors;
+ overlay?: ThemeOverlay;
+}
+
+export interface ThemeListResponse {
+ themes: Array<{ name: string; label: string; description: string }>;
+ active: string;
+}