圆角 token 门禁:从散落的 rounded 到可验证的设计尺度

Eyrie · HEAD...working tree · 2026-07-12 · 自包含,读完即弃

49 个文件
+401 / −89
60 行测试
0 个现存违规

写法说明:本文按「逐跳走读」展开。代码片段取自当前工作树并经裁剪;青色斜体是本文解释,灰色斜体保留源码注释。每条旅程结尾都有排查路标。

1TL;DR

这次变更把 renderer 中混用的 rounded-mdrounded-lg 和任意值收敛到一张由 Tailwind 配置声明的数值圆角表。门禁从配置读取允许列表,再分别解析 TS/TSX 类名与 CSS 声明。

新增 rounded-[...]、未定义旧 token、动态 rounded-${value} 或 CSS 原始 border-radius 时,检查命令会打印文件、行号和原因,并以非零状态结束。

2变更地图

49 个文件共变更 +401/−89 行,其中 60 行是测试。主要体量来自已有 UI 类名的等值迁移;真正承载设计的是检查器、token 配置与测试。

renderer UI
约 300 行 · 机械映射
checker
212 行 · 核心逻辑
tests
60 行 · 5 个行为
设计重心可放心略过
check-radius-tokens.mjstailwind.config.ts、token CSS 与测试renderer 中同值的 rounded-md → rounded-6 等机械替换

3旅程:一个 rounded 类如何变成 CI 结果

从一段 TSX 的 className 开始,先确定什么合法,再确定在哪里找,最后把违规变成可定位的 CI 输出。

全景 · 4 个环节
Tailwind 表
tailwind.config.ts
AST
findRadiusClassViolations
Git 文件
listScannedFiles
CI 结果
runRadiusTokenCheck

3.1允许列表先于扫描

配置声明设计像素尺度,类名和 CSS 变量共享同一来源;例如 rounded-6 最终引用 var(--radius-6)

apps/desktop/tailwind.config.tsborderRadius 表
borderRadius: {
  '2.5': 'var(--radius-2\\.5)',
  '6': 'var(--radius-6)',
  '8': 'var(--radius-8)',
  '16': 'var(--radius-16)',
  full: 'var(--radius-full)',
}

loadRadiusTokens 用大括号深度定位配置表并提取键名,避免检查器复制一份会漂移的 token 清单。

3.2语法树只看静态字符串

ts-morph 找到字符串字面量和模板字面量片段,注释里的示例不会被当成真实类名。插值前若留下 rounded- 前缀,则报告为动态违规。

scripts/check-radius-tokens.mjsAST 入口
const staticKinds = [SyntaxKind.StringLiteral, SyntaxKind.NoSubstitutionTemplateLiteral]
for (const kind of staticKinds) {
  for (const node of sourceFile.getDescendantsOfKind(kind)) {
    violations.push(...scanStaticText(node.getLiteralText(), node.getStartLineNumber(), allowedTokens))
  }
}

3.3失败输出保持可行动

入口只枚举 Git 已跟踪的 renderer 文件;违规输出文件、行号、原因和值。没有违规时静默退出 0,这与现有颜色、px、字号门禁一致。

scripts/check-radius-tokens.mjs统一入口
if (violations.length === 0) return true
console.error('Radius-token violations in the renderer:')
for (const violation of violations) console.error(`  - ${violation}`)
return false
排查路标 · 类名旅程
症状从哪下手
rounded-md 被拒绝tailwind.config.tsborderRadius 与调用组件
动态圆角被拒绝findRadiusClassViolations 的模板边界判断
命令没有输出runRadiusTokenCheck:静默 + 0 表示没有违规

4旅程:CSS 声明与组件边界

Tailwind 类不是唯一入口。CSS 直接写 border-radius 也可能绕过 class 门禁,因此第二条旅程从 PostCSS 声明节点开始。

全景 · 3 个环节
PostCSS ASTvar(--radius-*)renderer boundary

4.1标准半径属性必须引用 token

PostCSS 解析声明节点,识别四角和逻辑方向属性;值必须由已知 var(--radius-*) 组成。Incremark 覆盖因此使用 var(--radius-6)var(--radius-8)

scripts/check-radius-tokens.mjsCSS 判定
const tokens = cssRadiusTokens(declaration.value)
if (tokens.length > 0 && tokens.every((token) => token !== undefined && allowedTokens.has(token))) {
  return
}
violations.push({ value: `${declaration.prop}: ${declaration.value}` })

4.2shadcn 的边界是源码所有权

仓库里的 components/ui 是复制进来的 shadcn 风格源码,可以被项目修改,因此和 features 一样被扫描。真正的外部依赖位于 node_modules,不属于 Git 跟踪的 renderer 源码。

不豁免
components/ui
本地、可修改、随产品交付
边界外
node_modules / 非 renderer
不在扫描前缀与 Git 文件集合内
排查路标 · CSS/边界旅程
症状从哪下手
CSS 原始值被拒绝findCssRadiusViolations 与对应 CSS 声明
外部组件代码没有报错listScannedFiles 的 renderer 前缀与 Git 跟踪状态

5心智模型补丁

以前:Tailwind 默认 sm/md/lg 就是设计规范。现在:合法圆角由 theme.borderRadius 明确声明。
类名与设计尺度来自同一张表。
以前:任意值只要能编译就能进入 renderer。现在:rounded-[...] 是门禁错误。
编译成功不等于符合设计系统。
以前:shadcn 来源意味着可以暂时不管。现在:判断标准是源码所有权,本地复制源码同样受检。
目录名字不提供豁免。

6新词表

圆角门禁
radius tokenTailwind 类与 CSS 变量共同引用的命名圆角值。
allowedTokens从 Tailwind 配置动态读取的合法 token 集合。
dynamic radius圆角后缀由模板插值拼出,静态检查无法证明合法。
renderer boundaryGit 跟踪的 renderer CSS/TS/TSX 范围。

7测试与风险地图

有兜底的
  • 合法数值 token 与方向 variant
  • 任意值和旧 token 的失败诊断
  • 模板插值动态圆角
  • PostCSS 半径属性
  • 真实 renderer 全量基线
薄冰
  • 🟠复杂动态拼接可能没有可识别字面片段
  • 🟡inline style 的运行时圆角不在静态门禁范围
  • 🟡自定义属性本身不作为标准半径声明检查
合并前必办:确认 CI 运行 bun run check;圆角检查已接入主检查链。局部命令静默成功表示没有违规,不表示没有扫描。

8验收提示

9覆盖声明

本文精读了圆角配置、检测器、测试、CI 接线、CSS token 和代表性迁移文件;其余同值替换按统一映射归纳。未进行 code review,也没有把外部依赖源码当作本地变更阅读。