单一数据源的主题系统:从 13 块 CSS 到一行生成
单一数据源的主题系统
本站在 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 维护场景最想要的形态。