首页
/ Understand-Anything 仪表盘主题系统实现计划:CSS 变量注入、五套预设与本地持久化的完整落地路径

Understand-Anything 仪表盘主题系统实现计划:CSS 变量注入、五套预设与本地持久化的完整落地路径

2026-09-05 17:31:44作者:冯梦姬Eddie

本文基于 Understand-Anything 仓库中的主题系统实现计划(2026-03-26-theme-system-implementation.md)展开,讲解如何为 knowledge graph 仪表盘(dashboard)引入“精选主题预设 + 强调色自定义”能力:从 goldaccent 的 CSS 变量重命名、硬编码颜色收编、纯函数主题引擎、React 上下文与 localStorage/meta.json 双通道持久化,到浅色主题适配的 12 个任务如何分步落地。读完本文,你可以完整理解这套主题机制的分层设计(预设数据层 → 引擎注入层 → 状态管理层 → UI 层),并能对照 themes 目录下的真实源码验证每个设计决策,甚至在自己的项目中复刻同样的“CSS 自定义属性运行时注入”方案。

一、目标、架构与技术栈

计划文档开篇给出了三行核心约束,这也是整个方案的灵魂:

  • Goal:为 dashboard 增加带强调色(accent)自定义能力的精选主题预设;
  • Architecture:通过纯主题引擎在运行时注入 CSS 变量,用 React Context 管理状态,用 localStorage + meta.json 做持久化。共 5 个预设(4 暗色 + 1 浅色),每个预设配 8 个强调色色板(accent swatches);
  • Tech Stack:React、TypeScript、TailwindCSS v4、Zustand(不改动)、CSS 自定义属性。

这里的架构选择值得细究:主题状态不走已有的 Zustand store,而是独立的 React Context。从源码结构看,Zustand 的 store.ts 管理的是图谱数据这类高频更新的核心状态,而主题切换是低频的、且只需通过 CSS 变量“一次性写穿”到 document.documentElement,用 Context + useEffect 即可覆盖,避免把展示偏好混入数据 store。

计划同时声明了配套设计文档,当前仓库中对应 2026-03-26-theme-system-design.md

二、Task 1:把 gold 重命名为 accent

这是第一个任务,动机是“解耦语义”:原实现把强调色叫 gold,一旦支持 Ocean/Forest/Rose 等非金色强调,gold 这个名字就误导了。任务要求做纯机械替换、无行为变化,且必须最先执行,保证后续所有任务都用新命名。

涉及文件(均为 dashboard 包内):

CSS 变量重命名@theme 块内):

  • --color-gold--color-accent
  • --color-gold-dim--color-accent-dim
  • --color-gold-bright--color-accent-bright

同时把动画 @keyframes goldPulse 改名为 accentPulse.animate-gold-pulse 改为 .animate-accent-pulse

Tailwind 类替换清单(跨组件查找替换):

旧类名 新类名
text-gold-bright text-accent-bright
text-gold-dim text-accent-dim
text-gold text-accent
bg-gold bg-accent
border-gold border-accent
ring-gold-dim ring-accent-dim
ring-gold-bright ring-accent-bright
ring-gold ring-accent
animate-gold-pulse animate-accent-pulse

计划特别强调了一个容易踩的坑:顺序敏感——必须先替换更长的 -bright/-dim 变体,否则先替换 text-gold 会产生部分匹配错误。另外内联样式里的 var(--color-gold 也要一并换成 var(--color-accent

验证与提交

cd understand-anything-plugin && pnpm --filter @understand-anything/dashboard build
# 期望:构建成功,无错误
cd understand-anything-plugin && pnpm dev:dashboard
# 期望:界面与之前完全一致(仍是金色强调),无视觉变化
git add -A
git commit -m "refactor(dashboard): rename gold CSS variables to accent"

在当前仓库的 index.css 中可以确认这一步已完成:@theme 块第 13–15 行即为 --color-accent: #d4a574 / --color-accent-dim: #c9a96e / --color-accent-bright: #e8c49a,组件里也全部使用 text-accent 等新类名。

三、Task 2:把散落的硬编码 RGBA 收编为 CSS 变量

这一步是主题系统能“一次换肤、全局生效”的前提。仪表盘里大量 rgba(212,165,116,x)(即金色 rgb 分量)散落在 GraphView 的边样式、节点辉光、玻璃拟态面板、滚动条等位置,只要把基色换成 --color-accent 就能自动跟随强调色变化,这些硬编码值就必须参数化。

Step 1:在 @theme 块新增变量(按语义分组):

/* 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);

/* Layer group (accent-based 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);

注意变量命名规范:Tailwind v4 生成工具类必须落在 --color-* 前缀下,而纯 CSS 内部使用的变量(glass、glow、scrollbar、kbd)不加该前缀,二者在 index.css 中清晰分层。

Step 2–5:改造 index.css 中的工具类,让 .glass.glass-heavy、滚动条、.node-glow.kbd 全部引用变量:

.glass {
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}

.glass-heavy {
  background: var(--glass-bg-heavy);
  border: 1px solid var(--glass-border-heavy);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
}

::-webkit-scrollbar-thumb {
  background: var(--scrollbar-thumb);
  border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
  background: var(--scrollbar-thumb-hover);
}

.node-glow {
  box-shadow: 0 0 20px var(--glow-accent);
}

@keyframes accentPulse {
  0%, 100% { box-shadow: 0 0 8px var(--glow-accent-strong); }
  50%      { box-shadow: 0 0 20px var(--glow-accent-pulse); }
}

.kbd {
  /* ... 保留原有尺寸/布局 ... */
  color: var(--color-accent);
  background: var(--kbd-bg);
}

Step 6:GraphView.tsx 内联样式替换表(react-flow 的边/节点样式无法用 Tailwind,只能写 CSS 变量字符串):

位置 旧值 新值
边默认 stroke "rgba(212,165,116,0.3)" "var(--color-edge)"
边 diff 淡化 stroke "rgba(212,165,116,0.08)" "var(--color-edge-dim)"
背景点阵颜色 "rgba(212,165,116,0.15)" "var(--color-edge-dot)"
MiniMap nodeColor "#1a1a1a" "var(--color-elevated)"
MiniMap maskColor "rgba(10,10,10,0.7)" "var(--glass-bg)"
分组节点背景 "rgba(212,165,116,0.05)" "var(--color-accent-overlay-bg)"
分组节点边框 "2px dashed rgba(212,165,116,0.25)" "2px dashed var(--color-accent-overlay-border)"
分组标签颜色 "#d4a574" "var(--color-accent)"
边标签 fill(正常) "#a39787" "var(--color-text-secondary)"
边标签 fill(diff 淡化) "rgba(163,151,135,0.3)" "var(--color-text-muted)"

Step 7:CodeViewer.tsx 文件类型徽章——这里引入了一个进阶技巧:用 color-mix() 从节点类型色动态派生透明变体,避免再为每种节点类型硬编码 rgba:

  • borderColor: "rgba(74,124,155,0.3)""color-mix(in srgb, var(--color-node-file) 30%, transparent)"
  • backgroundColor: "rgba(74,124,155,0.1)""color-mix(in srgb, var(--color-node-file) 10%, transparent)"

Step 8 明确了一个例外:CustomNode.tsxshadow-[0_2px_8px_rgba(0,0,0,0.3)] 的黑色投影在深浅两套主题下都成立,故保持原样——这是一个有意识的“不动”决策,说明参数化不等于无差别参数化。

完成后构建并提交:refactor(dashboard): consolidate hardcoded colors into CSS variables。当前 index.css 中可逐一验证:--glass-bg(第 55 行)、--color-accent-overlay-bg(第 75 行)等均已落位。

四、Task 3–5:类型定义、预设数据与纯函数主题引擎

这三个任务创建独立的新文件(计划标注“可并行”),构成本系统的数据层与引擎层,全部位于 themes 目录

4.1 类型定义(types.ts)

types.ts 定义了四个核心类型与默认配置:

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<string, string>;
  accentSwatches: AccentSwatch[];
  defaultAccentId: string;
}

export interface ThemeConfig {
  presetId: PresetId;
  accentId: string;
}

export const DEFAULT_THEME_CONFIG: ThemeConfig = {
  presetId: "dark-gold",
  accentId: "gold",
};

设计要点:ThemePreset.colorsRecord<string, string> 而非强类型联合——键名与 CSS 变量后缀一一对应(root--color-root),引擎只需统一加 --color- 前缀即可注入,预设因此可以低成本扩展(仓库中后来确实如此做了,见第六节)。AccentSwatch 携带 accent / accentDim / accentBright 三级亮度,对应 UI 中正文强调、弱化强调、高亮强调三种语境。

实现演进提示:当前仓库的 types.ts 比计划多了一个 HeadingFont = "serif" | "sans" | "mono" 类型,且 ThemeConfig 增加了可选字段 headingFont?: HeadingFonttypes.ts#L25-L31)。说明该计划在落地后按同一模式扩展了“标题字体偏好”,是理解后续扩展方式的最佳范例。

4.2 预设数据(presets.ts)

presets.ts 导出五套预设与两个查找函数。每个预设包含 11 个基础色槽(root/surface/elevated/panel、三级文字、5 种图谱节点类型色)与 8 个强调色色板。

暗色系色板(dark-gold / dark-ocean / dark-forest / dark-rose 共用):

id name accent accentDim accentBright
gold Gold #d4a574 #c9a96e #e8c49a
ocean Ocean #5ba4cf #4e93ba #7abce0
emerald Emerald #5ea67a #4e9468 #78c492
rose Rose #cf7a8a #b96e7e #e094a4
purple Purple #9b7abf #876bb0 #b494d4
amber Amber #c9963a #b5862e #ddb05c
teal Teal #4aab9a #3d9686 #68c4b4
silver Silver #a0a8b0 #8e959c #b8bfc6

浅色系色板(light-minimal 专用,饱和度整体压低以适配亮底):

id name accent accentDim accentBright
indigo Indigo #4a6fa5 #3d5f8f #6088bf
ocean Ocean #3a8ab5 #2e7aa0 #55a0cc
emerald Emerald #3a8a5c #2e7a4e #55a878
rose Rose #a5566a #8f4a5c #bf6e82
purple Purple #6b5a9e #5c4d8a #8474b5
amber Amber #9e7a30 #8a6a28 #b5923e
teal Teal #2e8a7a #267a6c #45a595
slate Slate #5a6570 #4e5860 #6e7a85

五套预设的基础色(节选 dark-gold 与 light-minimal 对照)

{
  id: "dark-gold", name: "Dark Gold", isDark: true, defaultAccentId: "gold",
  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: "light-minimal", name: "Light Minimal", isDark: false, defaultAccentId: "indigo",
  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",
  },
},

其余三套暗色预设仅调整背景/文字色温(dark-ocean 偏海军蓝 #0a0e14、dark-forest 偏墨绿 #0a100a、dark-rose 偏暖深红 #100a0a),节点类型色保持一致以维持图谱语义。

两个查找函数都内置三级兜底,是健壮性的关键:

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]
  );
}

这意味着:localStorage 里存的旧/坏 presetId 会回退到 dark-gold;预设升级后旧 accentId 失效时会回退到该预设的默认色板,再退到第一个色板——任何脏数据都不会导致白屏或无色。

实现演进提示:当前仓库的 presets.ts 中每套预设的 colors 已从计划中的 5 个节点色扩展到 13 个(新增 node-confignode-documentnode-servicenode-tablenode-endpointnode-pipelinenode-schemanode-resource),与 dashboard 后续引入的知识图谱/设计图谱节点类型同步。

4.3 主题引擎(theme-engine.ts)

theme-engine.ts不依赖任何 React 的纯函数模块,职责有二:hexToRgb 颜色换算,以及 applyTheme(config) 一次性写穿全部 CSS 变量。

export function hexToRgb(hex: string): string {
  const h = hex.replace("#", "");
  const n = parseInt(h, 16);
  return `${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}`;
}

deriveFromAccent 是引擎的精华:它把约 17 个透明度变体从单一强调色 hex 派生出来,并针对明暗两套主题使用不同透明度基线(暗底上边框 0.12/0.25,亮底上 0.1/0.18;glass 背景在暗色用 rgba(20,20,20,0.8)、亮色用 rgba(255,255,255,0.8)):

function deriveFromAccent(accentHex: string, isDark: boolean): Record<string, string> {
  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)`,
  };
}

这正是 Task 2 收编硬编码 RGBA 的回报:所有派生变量名与 CSS 中引用的 var(--...) 完全对齐,换一个强调色,边框、玻璃拟态、辉光、滚动条、图谱边线、快捷键样式全链路跟随。

applyTheme 按四步写入 <html> 元素(theme-engine.ts#L33-L55):

export function applyTheme(config: ThemeConfig): void {
  const preset = getPreset(config.presetId);
  const accent = getAccent(preset, config.accentId);
  const style = document.documentElement.style;

  // 1. 基础预设色
  for (const [key, value] of Object.entries(preset.colors)) {
    style.setProperty(`--color-${key}`, value);
  }
  // 2. 强调色三级
  style.setProperty("--color-accent", accent.accent);
  style.setProperty("--color-accent-dim", accent.accentDim);
  style.setProperty("--color-accent-bright", accent.accentBright);
  // 3. 派生值
  const derived = deriveFromAccent(accent.accent, preset.isDark);
  for (const [key, value] of Object.entries(derived)) {
    style.setProperty(`--${key}`, value);
  }
  // 4. 供 CSS-only 选择器使用的 data-theme 属性
  document.documentElement.setAttribute("data-theme", preset.isDark ? "dark" : "light");
}

第 4 步的 data-theme 属性是纯 CSS 侧的“开关”:@media (prefers-color-scheme) 无法覆盖 JS 决策后的主题,而 data-theme 允许 CSS 写 [data-theme="light"] ... 这类仅选择器可达的覆写(Task 10 会用到)。

实现演进提示:当前仓库的 applyTheme 在四步之后还新增了第 5 步——把 config.headingFont(默认 "serif")映射为 var(--font-serif)/var(--font-sans)/var(--font-mono) 并写入 --font-heading 变量(theme-engine.ts#L57-L65),即引擎层同样承担了“偏好 → CSS 变量”的统一职责。

五、Task 6:ThemeContext——状态管理与双通道持久化

ThemeContext.tsx 提供 ThemeProvideruseTheme hook,是状态管理层。它解决三个问题:初始主题从哪里来、用户改动如何持久化、异步元数据迟到后如何合并。

持久化通道设计

const STORAGE_KEY = "ua-theme";

function resolveInitialTheme(metaTheme?: ThemeConfig | null): ThemeConfig {
  return loadFromLocalStorage() ?? metaTheme ?? DEFAULT_THEME_CONFIG;
}

优先级为 localStorage > meta.json 中的 theme 字段 > 内置默认(dark-gold/gold)。其中 loadFromLocalStorage 对 JSON 做了结构校验(presetId/accentId 必须都是 string)并整体包在 try/catch 中,读取失败或存储被禁用时静默回退;saveToLocalStorage 同样吞掉“存储满/不可用”异常——主题偏好属于 nice-to-have,绝不能因它抛错。

Provider 的核心效果ThemeContext.tsx#L58-L76):

export function ThemeProvider({ metaTheme, children }: ThemeProviderProps) {
  const [config, setConfig] = useState<ThemeConfig>(() => resolveInitialTheme(metaTheme));
  const initialized = useRef(false);

  // 挂载与配置变更时应用主题
  useEffect(() => {
    applyTheme(config);
    if (initialized.current) {
      saveToLocalStorage(config);   // 首次挂载不写存储,避免“未选择即留痕”
    }
    initialized.current = true;
  }, [config]);

  // metaTheme 是异步 fetch 的结果;迟到时仅当用户尚无本地偏好才接管
  useEffect(() => {
    if (metaTheme && !loadFromLocalStorage()) {
      setConfig(metaTheme);
    }
  }, [metaTheme]);
  // ...
}

两处细节值得注意:

  1. initialized ref 区分首次渲染:首次 applyTheme 后立即把当前 config 写入 localStorage,会让“从未交互过的访问者”也留下持久化记录,从而锁死后续 meta.json 下发的团队默认主题。跳过首次写入后,用户第一次主动切换前,meta.json 始终有机会生效。
  2. metaTheme 迟到的竞态处理meta.json 是网络请求,ThemeProvider 挂载时拿到的 metaTheme 通常还是 null(走 localStorage 或默认值)。当异步结果到达时,只有“用户没有本地偏好”才 setConfig(metaTheme),避免覆盖用户选择。

对外 APIsetPreset / setAccent(实现中另有演进出的 setHeadingFont,见第六节)。其中 setPreset 的语义是“换预设即重置强调色为该预设默认色板”:

const setPreset = useCallback((presetId: PresetId) => {
  setConfig((_prev) => {
    const newPreset = getPreset(presetId);
    return { presetId, accentId: newPreset.defaultAccentId };
  });
}, []);

useTheme 在 Provider 之外调用会直接抛错("useTheme must be used within ThemeProvider"),符合 React Context 惯例。

模块通过桶文件 themes/index.ts 统一导出 ThemeProvideruseThemePRESETSgetPresetgetAccentapplyTheme 及相关类型,外部只依赖这一个入口。

六、Task 7:core 包——AnalysisMeta 增加 theme 字段

为了让“主题偏好”可以随分析产物一起落盘(跨设备/团队成员共享同一套主题),需要在 core 包的持久化结构中加字段。修改 core/src/types.ts,在 AnalysisMeta 旁新增:

// Theme configuration (for dashboard customization)
export interface ThemeConfig {
  presetId: string;
  accentId: string;
}

export interface AnalysisMeta {
  lastAnalyzedAt: string;
  gitCommitHash: string;
  version: string;
  analyzedFiles: number;
  theme?: ThemeConfig;
}

当前仓库中该定义位于 core/src/types.ts#L117-L130,与计划完全一致。注意此处 core 侧的 ThemeConfig 字段是宽松的 string 而非 dashboard 侧的 PresetId 联合——从源码结构看,这是有意为之:持久化层不做枚举约束,把合法性校验(getPreset?? PRESETS[0] 兜底)留给消费端,使 meta.json 的前向兼容更简单。

验证要求:pnpm --filter @understand-anything/core buildpnpm --filter @understand-anything/core test 均须通过(core 包自带 schema.test.ts 等测试保障图/元数据 schema 稳定)。

七、Task 8:ThemePicker——预设列表 + 色板行弹层

ThemePicker.tsx 是用户唯一的主题入口,结构为:头部一个 14px 图标按钮(月亮圆点 SVG + “Theme”文字,小屏隐藏文字)+ 展开后的 glass-heavy 弹层。

计划给出的组件骨架(与当前实现一致):

export function ThemePicker() {
  const { config, preset, setPreset, setAccent } = useTheme();
  const [open, setOpen] = useState(false);
  const ref = useRef<HTMLDivElement>(null);

  // 点击外部关闭
  useEffect(() => {
    if (!open) return;
    function handleClick(e: MouseEvent) {
      if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
    }
    document.addEventListener("mousedown", handleClick);
    return () => document.removeEventListener("mousedown", handleClick);
  }, [open]);

  // Escape 关闭
  useEffect(() => {
    if (!open) return;
    function handleKey(e: KeyboardEvent) {
      if (e.key === "Escape") setOpen(false);
    }
    document.addEventListener("keydown", handleKey);
    return () => document.removeEventListener("keydown", handleKey);
  }, [open]);
  // ... 渲染预设列表与色板行
}

两个交互细节值得复用:关闭逻辑用 mousedown 而非 click(避免在按钮上按下、在外部松开的边缘情况误关),且事件绑定挂在 document 上、组件卸载/关闭时解绑。弹层定位用 absolute right-0 top-full mt-2 w-64 z-50,无 portal,依赖 header 层叠上下文。

弹层内容分两区:

  1. 预设列表:每个预设渲染三个 3×3 的预览色点(rootsurface、默认色板 accent,直接取 p.colors.root 等值内联绘制,不依赖当前 CSS 变量——保证在预览其他预设时看到的是“那个预设自己的颜色”),当前项高亮 bg-accent/15 text-accent 并带对勾;
  2. 强调色色板:渲染 preset.accentSwatches(注意是当前预设的色板,换预设会随之切换),选中项加 ring-2 ring-text-primary ring-offset-1

实现演进提示:当前 ThemePicker.tsx 还做了两处增强:所有文案改走 useI18n()(对应 I18nContext 的多语言词条 themePicker.*),并在色板行下方新增第三区“标题字体”,提供 serif/sans/mono 三个按钮,按钮自身用对应 fontFamily 渲染“Aa”样例(ThemePicker.tsx#L143-L175)。

八、Task 9:App 集成——ThemeProvider 包裹与 meta.json 主题加载

集成点在 App.tsx。四步:

  1. 导入import { ThemeProvider } from "./themes/index.ts"import { ThemePicker } from "./components/ThemePicker.tsx"ThemeConfig 类型;
  2. 异步加载 meta.json 的 theme 字段——当前仓库实现(App.tsx#L126-L142)比计划更进一步,走了带 token 的 dataUrl 助手:
const [metaTheme, setMetaTheme] = useState<ThemeConfig | null>(null);

useEffect(() => {
  fetch(dataUrl("meta.json", accessToken))
    .then((r) => (r.ok ? r.json() : null))
    .then((meta) => {
      if (meta?.theme) setMetaTheme(meta.theme);
    })
    .catch(() => {});
}, []);

计划原文是 fetch("/meta.json"),实际实现因 dashboard 需要访问 token 改为 fetch(dataUrl("meta.json", accessToken))(同文件里 config.jsonknowledge-graph.json 等请求均走同一模式)——同一数据契约,更贴合实际的鉴权传输。

  1. 包裹<ThemeProvider metaTheme={metaTheme}> 包住整个应用 JSX(当前位于 App.tsx#L262 附近);
  2. 挂载入口:在 header 现有控件(PersonaSelector、DiffToggle、LayerLegend)之后、帮助按钮之前插入 <ThemePicker />

九、Task 10:浅色主题的 CSS 边界处理

CSS 变量覆盖 90% 场景,但有些差异是“结构性”的,只能靠选择器覆写。计划在 index.css 末尾追加:

/* 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="dark"] {
  color-scheme: dark;
}

color-scheme 会同步影响浏览器原生控件(滚动条、表单元素)的默认配色,是换肤时容易被漏掉的一环。同时给 html 加过渡,让切换不“跳变”:

html {
  transition: background-color 0.2s ease, color 0.2s ease;
}

计划还预置了一个条件项:WarningBanner 使用 Tailwind 语义 amber 色(如 bg-amber-900/20),语义上不应随主题变;但若浅色背景下观感破损,再加 [data-theme="light"] .warning-banner 覆写(background: rgba(180,130,30,0.1)border-color: rgba(180,130,30,0.3)color: #92600a)。当前仓库的 index.css 中这些 [data-theme="light"] 规则(含 .warning-banner 覆写,约在文件 245–263 行)均已落地,说明该条件项最终被确认需要。

十、Task 11:@theme 块作为“首屏兜底”

这是整套机制里最容易被忽略却最关键的一环。运行时注入意味着 React 挂载前的那一瞬间,主题变量必须由 CSS 自己提供。计划要求把 Task 1–2 的全部变量(含 accent 三件套、边框、玻璃、滚动条、辉光、边线、覆盖层、kbd 与字体族)完整写入 @theme 块作为默认值:

@theme {
  /* Base */
  --color-root: #0a0a0a;
  --color-surface: #111111;
  --color-elevated: #1a1a1a;
  --color-panel: #141414;

  /* Accent */
  --color-accent: #d4a574;
  --color-accent-dim: #c9a96e;
  --color-accent-bright: #e8c49a;

  /* Text */
  --color-text-primary: #f5f0eb;
  --color-text-secondary: #a39787;
  --color-text-muted: #6b5f53;

  /* Borders */
  --color-border-subtle: rgba(212, 165, 116, 0.12);
  --color-border-medium: rgba(212, 165, 116, 0.25);

  /* Node types */
  --color-node-file: #4a7c9b;
  --color-node-function: #5a9e6f;
  --color-node-class: #8b6fb0;
  --color-node-module: #c9a06c;
  --color-node-concept: #b07a8a;

  /* 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);

  /* Glass / Scrollbar / Glow / Edges / Accent overlays / Kbd / Typography 同 Task 2 所列 */
  --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-thumb: rgba(212, 165, 116, 0.2);
  --scrollbar-thumb-hover: rgba(212, 165, 116, 0.35);
  --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);
  --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);
  --color-accent-overlay-bg: rgba(212, 165, 116, 0.05);
  --color-accent-overlay-border: rgba(212, 165, 116, 0.25);
  --kbd-bg: rgba(212, 165, 116, 0.1);

  --font-serif: 'DM Serif Display', Georgia, serif;
  --font-mono: 'JetBrains Mono', 'Fira Code', monospace;
  --font-sans: 'Inter', system-ui, sans-serif;
}

这一块的三重作用(计划原文归纳):

  1. Tailwind v4 依据 @theme 块生成工具类——text-accentbg-rootborder-border-subtle 等类名的存在与否由它决定,删掉它构建即破;
  2. 首屏兜底——React 挂载前页面直接呈现 Dark Gold 默认外观,无“未样式化闪烁”(flash of unstyled content);
  3. 运行时覆盖基线——主题引擎用 style.setProperty 写到 <html>style 属性上,内联样式优先级高于 @theme 产出的 :root 变量,天然覆盖。

验证后提交 refactor(dashboard): align @theme defaults with theme engine variables

十一、Task 12:全量构建与视觉验收清单

最后一个任务是纯验证,命令序列为:

cd understand-anything-plugin && pnpm --filter @understand-anything/core build
cd understand-anything-plugin && pnpm --filter @understand-anything/dashboard build
cd understand-anything-plugin && pnpm --filter @understand-anything/core test
cd understand-anything-plugin && pnpm lint
cd understand-anything-plugin && pnpm dev:dashboard

计划的 11 条视觉验收清单是一份很好的回归测试脚本:

  1. 默认加载为 Dark Gold,与旧版外观一致;
  2. header 出现 Theme 按钮;
  3. 点击展开弹层,含 5 个预设与 8 个强调色色板;
  4. 选 Dark Ocean——背景转海军蓝、强调色转青;
  5. 选 Dark Forest——背景转墨绿、强调色转翡翠绿;
  6. 选 Dark Rose——背景转暖深红、强调色转玫瑰;
  7. 选 Light Minimal——浅底深字、强调色转靛蓝;
  8. 各预设内切换色板——强调色、边框、玻璃、辉光全链路更新;
  9. 刷新页面——主题经 localStorage 保持;
  10. 点击弹层外部——关闭;
  11. 按 Escape——关闭。

任何失败则回补修复并以 fix(dashboard): theme system visual adjustments 提交。

十二、依赖图与并行化分组

计划给出的任务依赖关系(原文结构整理):

Task 1 (rename gold→accent) ─┐
                              ├─> Task 3 (types) ──┐
Task 2 (consolidate colors) ─┤                     │
                              │   Task 4 (presets) ─┤
                              │                     ├─> Task 6 (context) ─┐
                              │   Task 5 (engine) ──┘                     │
                              │   Task 7 (core types) ────────────────────┘
                              └───────────────────────────────────────────> Task 9 (integrate)
                                                                           ├─> Task 10 (light CSS)
                                                                           ├─> Task 11 (defaults)
                                                                           └─> Task 12 (verify)

可并行分组:

  • Task 1 + 2 顺序执行(都动 index.css,避免冲突);
  • Task 3、4、5 可并行(彼此独立的新文件);
  • Task 6 依赖 3/4/5;Task 7 完全独立(core 包);
  • Task 8 依赖 6;Task 9 依赖 1/2/7/8;
  • Task 10、11 在 9 之后;Task 12 收尾验证。

十三、从计划到落地:仓库中的三处实现演进

将计划与当前仓库源码逐一对照,核心机制(五预设 × 8 色板、applyTheme 四步注入、ua-theme localStorage 键、meta.json theme 字段、getPreset/getAccent 三级兜底)均按原计划落地,同时有三处清晰的增量演进,可作为“如何在既定 CSS 变量架构上低成本扩展主题维度”的参考:

  1. headingFont 偏好types.ts 新增 HeadingFontThemeConfig.headingFont?ThemeContext.tsx 增加 setHeadingFonttheme-engine.tsapplyTheme 末尾写入 --font-headingThemePicker.tsx 增加字体三选区。全程只改四个文件,未触碰预设数据结构;
  2. 节点类型色扩充presets.ts 中每套预设从 5 个 node-* 色扩到 13 个(config/document/service/table/endpoint/pipeline/schema/resource),得益于 colors: Record<string, string> 的开放键名设计,引擎代码零改动;
  3. i18n 与鉴权化传输:ThemePicker 文案接入 I18nContext,App 侧 meta.json 加载改走带 token 的 dataUrl

小结

这套主题系统的可借鉴性在于分层克制:预设数据(presets)只描述“是什么颜色”,纯函数引擎(theme-engine)只负责“如何写进 DOM”,Context 只管理状态与持久化优先级,UI 组件只消费 hook。所有“换色”最终收敛为对 <html> 上约 30 个 CSS 自定义属性的一次批量 setProperty,配合 @theme 静态默认值消除首屏闪烁、data-theme 属性覆盖结构性差异、getPreset/getAccent 兜底抵御脏数据——对照 themes 目录 的五个文件与 dashboard 包结构,即可完整复现这条从 CSS 变量到 UI 弹层的实现链路。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.79 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
988
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384