← BACK

单一数据源的主题系统:从 13 块 CSS 到一行生成

·AstroCSS variablesdesign tokens

单一数据源的主题系统

本站在 v1 时有一套典型的“双源”主题系统:主题颜色既写在 src/lib/themes.ts(面板 + 色板条用),又手写在 global.css 的 13 个 :root[data-theme="…"] 块里。加一个主题 = 改两处, tsc 只能保证 key 合法,保证不了 CSS 同步。

重构目标:加主题只改一个文件。

生成色块,而不是手写

主题块的生产形态变成:BaseLayout.astro 在构建期由 THEMES 数组 直接生成 CSS,内联到 <head>:

const themeCss = THEMES.map(
  (t) =>
    `:root[data-theme="${t.key}"]{--gb-0:${t.colors[0]};--gb-1:${t.colors[1]};--gb-2:${t.colors[2]};--gb-3:${t.colors[3]};}`
).join('\n');

global.css 里只保留 :root(默认色 + 语义别名)。经典 CSS 变量分层: 语义别名(--color-text 等)定义一次,主题块只覆盖 --gb-0..3, var() 在实际消费元素上解析,所以深度嵌套的组件无需改动。

等价性怎么验证

评审时专门核过 var() 链路的等价性: :root[data-theme=…] 的 specificity(0,2,0)覆盖 :root(0,1,0)的 --gb-*;别名声明在 :root,custom property 在声明元素处完成代入再继承。 13 套色值逐一与旧手写块比对,大小写无关、值一致 ✓。

主题下的 CRT overlay

CRT 扫描线原本硬编码 GB 绿(rgba(15,56,15,…)),任何主题下都是绿线。 现在消费 --gb-3(对比色槽)而不是 --gb-0(背景槽)——后者在浅色 主题下会等于背景色、扫描线不可见(这是 code review 抓出来的回归):

:root {
  --crt-scan: color-mix(in srgb, var(--gb-3) 35%, transparent);
  --crt-vignette: color-mix(in srgb, var(--gb-3) 45%, transparent);
}

无 color-mix 的旧浏览器走 @supports not 回退到固定近似值。

教训

双源数据的“以防万一”同步注释是最大的谎言——有同步约束,就该消除 第二个源,而不是靠注释保证。加主题从“改两处 + 祈祷颜色一致”变成 “数组加一项”,正是 AI 维护场景最想要的形态。