CSS 治理
Tabula 局部性论证中的一切——一个元素的外观可以从它自身的标记中确定——都是围绕着 class 属性展开的。而一个样式表触及元素的方式不同: 通过选择器。因此,项目自身的 .css 文件是唯一一个能够仅凭一行代码就把整套模型推翻的地方,而在这一层治理机制出现之前, 这套系统里没有任何命令会去打开这样一个文件。
这个页面讲的是现在有什么在治理它们、还有什么没被覆盖到,以及两道门禁可能在哪里出现分歧。
五条规则
两道门禁运行的是同一份代码——规则内核位于 packages/stylelint-plugin/src/kernel.ts。 stylelint 插件把它包装给你的编辑器使用;tabula check:css 则在 CI 里把文件遍历送进它。如果写两份,它们就会产生漂移, 而这种漂移会朝着最要命的方向悄悄发生:你修复了编辑器标记出的问题,CI 又标记出别的问题,而那条没有任何编辑器提及过的规则, 就悄悄不再是一条规则了。
| 代码 | stylelint 规则 | 禁止 | 原因 |
|---|---|---|---|
TAB-E221 | tabula/no-apply | @apply | 把一份 class 列表编织进一个手写的选择器——正是这份配置档案要移除的那种间接层。被 Tailwind 自己的创作者所否定(draft-c §3.2 ✚16)。 |
TAB-E222 | tabula/no-raw-rules | 受核准入口之外、项目 CSS 里的任何手写规则 | .card .title { color: red } 按选择器来样式化,导致该元素不再能从其自身标记中被读出(draft-c §3.2 ✚18)。 |
TAB-E223 | tabula/no-scoped-custom-property | 在 :root/html 之外定义的 --tb-* / --d-* | T17——本系统中最重要的一条检查(SPEC J6)。一旦一个设计令牌能在一个 <div> 上被重新定义,每一个使用它的后代元素就变得无法从其标记中被读出。 |
TAB-E224 | tabula/no-theme-inline | @theme inline | T20——inline 会在构建时把设计令牌取值代入工具类,把每一个有条件的设计令牌都编译没了。深色模式会在没有任何诊断提示的情况下失效。它是 shadcn 生态圈的默认做法,因此一个在那套生态上训练出来的智能体会本能地伸手去用它。 |
TAB-E225 | tabula/no-important-css | 项目 CSS 中的 !important | 每一个工具类都被归一化到特异性 (0,1,0),因此只有 rank 才能决定冲突的胜负。一个 !important 就会把一条声明置于这套证明之外。 |
因此,一个项目 .css 文件只可以包含:@import、@source、@utility、@custom-variant、@charset、 一条无实体的 @layer a, b; 顺序声明,以及注释。别无其他。
两道门禁
在 CI 里 / 构建时
tabula check:css # every project .css file + the emitted stylesheets
tabula build --check # drift + scan + check:css, the one command CI runs
tabula build --check --no-css # drift + scan only; the CSS gate must be opted OUT of
tabula check:css --css-entry src/theme/entry.css # designate a non-conventional entry在编辑器里——.stylelintrc.json,映照 本仓库自身的配置:
{
"plugins": ["@tabula-css/stylelint-plugin"],
"rules": {
"tabula/no-apply": true,
"tabula/no-raw-rules": true,
"tabula/no-scoped-custom-property": true,
"tabula/no-theme-inline": true,
"tabula/no-important-css": true
},
"overrides": [
{ "files": ["src/app.css"], "rules": { "tabula/no-raw-rules": [true, { "sanctionedEntry": true }] } }
]
}@tabula-css/stylelint-plugin/config 导出了这份规则代码块,因此你可以直接展开(spread)它,而不必手动复制。 stylelint 是一个可选的 peer 依赖:这个插件的内核在其导入图里完全不含 stylelint,这正是让 CLI 得以使用它、 而不必因此获得一个作者工具作为运行时依赖的原因。
受核准的入口
每个应用都需要一份持有 @import "../.tabula/source.css"; 以及应用自身根级别自定义属性的样式表。 只有那一份文件——且仅有那一份——被豁免于 TAB-E222。
但它并不豁免于另外四条规则。入口文件里的一个 :root 代码块是可以的;而入口文件里出现的 --tb-color-accent: red 无论出现在哪里,都是一个 TAB-E223。参见 examples/reference-ui/src/app.css, 这是一份完整的实例。
一个文件是如何成为入口的:
- 按约定——
src/app.css、src/index.css、src/styles.css、app/globals.css以及其他几个(见packages/cli/src/commands/check-css.ts中的DEFAULT_CSS_ENTRIES); - 或者显式指定——
tabula check:css --css-entry <path>,可重复使用。
这两道门禁唯一可能出现漂移的地方就是这份列表:check:css 从命令行参数与约定中读取它, stylelint 则从你 .stylelintrc 里的一个 overrides 代码块中读取它。让它们保持一致。如果它们出现分歧, CI 门禁才是最终决定你能否发布的那一方。
已记录的决策与不一致之处
1. Draft C 的代码没有被沿用。 Draft C 把这些不变式分配为 PLANAR-E210(T17)、PLANAR-E211(T20)与 PLANAR-E212(✚18)。而这三个编号在 CSS 层被编写之前,早已在本目录中被用于构建不变式 I1/I2/I3。 一个含义有两种的代码,比一个谁都不认识的代码更糟——tabula explain TAB-E212 只能打印出一段说明文字, 它会把一个读者引向去追查 rank 冲突,而不是一条手写的 .card p。因此这个家族被重新铸造在 E221–E225 上, 且每一条目录记录都记下了与 draft 的对应关系。
2. T17 在生成出的样式表里读起来必须不一样,而这是必要的。 T17 原文说的是每一个配置档案自身的自定义属性都 只能在 :root 处定义。而这份配置档案自己的输出恰恰故意违反了这一点:.ring-accent { --tb-ring-color: … } 与 .shadow-sm { --tb-shadow: … } 是逐元素的**组合(composition)**属性,声明为 inherits: false 并带有一个初始值。所以:
- 项目 CSS——任何在根主体(root subject)之外定义的
--tb-*/--d-*,都是一次违规,没有任何豁免。 项目 CSS 根本没有任何正当理由去写一个配置档案属性;dyn()才是逐元素取值的受核准通道, 它写的是一个内联样式,而不是一份样式表。 - 生成出的 CSS——只有当一个属性确实在生成集合的某处根主体被定义过(也就是一个主题设计令牌)时, 在根主体之外的一次定义才算违规。这样就能捕捉到 T17 存在的意义所要捕捉的东西(一个在根之下被重新定义的设计令牌), 同时不会让配置档案自身正确的输出失败。
SPEC 本应说的是第二种情况,但它说的是第一种。这里如实记录,而不是通过弱化检查来"解决"它。
3. @utility 是一种元素绑定。 @utility card-shell { --tb-color-accent: red } 会编译成一个 class, 它在每一个携带它的元素上重新定义了一个主题设计令牌——这是同一种 T17 式的破坏,只是披着项目 CSS 被允许包含的那唯一一种 at 规则的外衣。内核因此把 @utility 内部的一条声明当作元素作用域来处理。
4. 这份入口列表本该属于 tabula.config.json。 它真正的归宿是配置模式(config schema,见 packages/core/src/schemas/config.ts)里、与 scan.sources 并列的一个 css.entries 键,该模式的 additionalProperties: false 意味着一个不被识别的 css 键会校验失败,而不是被悄悄忽略。当初构建这一层的任务 并不拥有那个文件的所有权,所以这份列表至今仍是约定加 --css-entry。把它挪过去只是一次单键的改动, 而且会消除上文提到的那个漂移点。
5. 一份无法解析的样式表是一个发现,而不是一次跳过。 一个 PostCSS 无法解析的 .css 文件会被报告为 TAB-E222。跳过它只会让"语法上就坏掉"变成绕过这个页面上每一条规则最省事的办法。
这一层目前仍未覆盖到什么
- T18——根之下的轴属性。
<div data-theme="dark">是惰性的(生成出的选择器都是:root[data-theme=…], 所以这个属性根本不会有任何效果),而 ESLint 规则tabula/no-axis-attribute-below-root正是用来向作者解释这一点的。 在 CSS 里,一个.panel[data-theme="dark"]选择器会被当作一条手写规则捕捉到(TAB-E222),而不是当作一次轴违规—— 这是一个名字挂错了、但结果本身正确的情形。 - 一个项目自己写的
@source行——这个封闭性漏洞,依然敞开着。 你某个样式表里的@source "./src"会重新打开 Tailwind 的源码扫描,而主题命名空间依然被定义着,因此bg-red-500与p-4会重新开始产出 CSS。check:css不会标记它,因为 draft-c §3.2 ✚18 把@source列在了项目 CSS 可以包含的 at 规则之中。那份清单与评审 A7 里的封闭性论证彼此矛盾,而这一层实现的是那份清单。在 SPEC 里解决这个问题之前:让.tabula/source.css的@import保持为你项目里唯一的 Tailwind 入口,不要添加任何@source行。grep -rn '@source' src就是检查它的办法。 - 导入的第三方 CSS。 这道门禁遍历的是项目自身的目录树,而不是
node_modules。一份你导入的第三方样式表, 和它以前一样,完全没有被审查过。 - 运行时的
<style>注入。 任何一个组件在运行时写入文档的内容,都在这里所有门禁的覆盖范围之外。