Token 审计
tools/audit-tokens.mjs 是 Token 包的门禁。它是零依赖的 Node 实现,不依赖浏览器、PostCSS 或任何构建工具——因为它必须在任何工具链变动下都还能跑。
bash
corepack pnpm audit:tokens它检查三件事
- 对比度 — 32 组前景/背景配对 × 浅色/深色 = 每个目标 64 项,任何一项低于阈值即失败(正文 4.5:1,边界与焦点环 3:1)。
- 可解析性 — 每个 Token 在每个模式下都必须解析成功。悬空引用、循环引用、无法建模的选择器或
!important都会直接报错,而不是被跳过。 - 一致性 — 当审计多个目标时,所有目标在每个模式下必须解析出完全相同的结果。
为什么审计器要自己实现 CSS 解析
原型的审计跑在浏览器里,用 getComputedStyle 读取计算值。迁移到 Node 后,最容易出错的地方是把浏览器的行为简化掉:
- 层顺序优先于具体度。
@layer的顺序排在 specificity 之前,一个低具体度的后置层声明会覆盖高具体度的前置层声明。 - 未分层的声明优先于所有层。审计器按真实的 CSS 规则给未分层声明最高的层序号。
color-mix()需要预乘与 alpha 缩放,color-mix(in srgb, #1f1f1f 8%, transparent)必须得到「颜色不变、alpha 为 0.08」,否则叠在底色上算出来的对比度会完全失真。- 透明度需要逐层合成。幽灵按钮悬停这类配对是「半透明前景叠在基础表面上」,必须先合成再算对比度。
- 亮度阈值沿用原型的遗留分支(
0.03928 / 12.92)。差异远小于一档对比度,但「迁移后审计仍然通过」只有在两边算法一致时才是可验证的说法。
遇到无法建模的构造时,审计器直接失败,而不是忽略。这正是它能被当作门禁的原因:没有 Token 能悄悄逃出这套级联模型。
输出示例
text
Yue Design · Token Audit
──────────────────────────────────────────────────────────────────────────────
contract: 32 contrast pairs × 2 profiles = 64 gating checks per target
gating profiles: light/azure, dark/azure
▌ prototype — design-tokens-generic-v4/tokens/index.css
files: index.css ← primitives.css ← semantics.css ← components.css
layers: primitives < semantics < components < implementations < demo
tokens: light/azure 451, dark/azure 451 (declared 451)
...
→ 64/64 PASS (32 pairs × 2 profiles)
▌ parity prototype ↔ package
profiles probed: light/azure, dark/azure, light/neutral, dark/neutral
resolutions compared: 1804
differences: 0
→ IDENTICAL
RESULT: PASS这组配对从哪来
tools/token-audit.pairs.mjs 里的 32 组配对是从 design-tokens-generic-v4.html 内嵌的 const pairs 逐字转录的。测试会重新从该 HTML 中提取这组数组并与模块比对, 所以它不可能悄悄漂移;文档与门禁也共享同一份定义。
Accent 模式为什么只做诊断
原型从未采样 [data-accent=neutral] 模式,因此把 neutral 也设为门禁,会让一次 「忠实的迁移」因为原型本身没检查过的东西而失败。审计会报告 neutral 的结果 (当前两个目标都是 64/64),但不让它决定退出码。
即将补充
- 对比度审计页:直接读取 Token 包并按模式渲染完整矩阵
- Token 浏览器:列出每个 Token 在四种模式下的解析值
- Accent 切换与浅色/深色切换控件