Skip to content

CSS 治理

Tabula 局部性论证中的一切——一个元素的外观可以从它自身的标记中确定——都是围绕着 class 属性展开的。而一个样式表触及元素的方式不同: 通过选择器。因此,项目自身的 .css 文件是唯一一个能够仅凭一行代码就把整套模型推翻的地方,而在这一层治理机制出现之前, 这套系统里没有任何命令会去打开这样一个文件

这个页面讲的是现在有什么在治理它们、还有什么没被覆盖到,以及两道门禁可能在哪里出现分歧。

五条规则

两道门禁运行的是同一份代码——规则内核位于 packages/stylelint-plugin/src/kernel.ts。 stylelint 插件把它包装给你的编辑器使用;tabula check:css 则在 CI 里把文件遍历送进它。如果写两份,它们就会产生漂移, 而这种漂移会朝着最要命的方向悄悄发生:你修复了编辑器标记出的问题,CI 又标记出别的问题,而那条没有任何编辑器提及过的规则, 就悄悄不再是一条规则了。

代码stylelint 规则禁止原因
TAB-E221tabula/no-apply@apply把一份 class 列表编织进一个手写的选择器——正是这份配置档案要移除的那种间接层。被 Tailwind 自己的创作者所否定(draft-c §3.2 ✚16)。
TAB-E222tabula/no-raw-rules受核准入口之外、项目 CSS 里的任何手写规则.card .title { color: red } 按选择器来样式化,导致该元素不再能从其自身标记中被读出(draft-c §3.2 ✚18)。
TAB-E223tabula/no-scoped-custom-property:root/html 之外定义的 --tb-* / --d-*T17——本系统中最重要的一条检查(SPEC J6)。一旦一个设计令牌能在一个 <div> 上被重新定义,每一个使用它的后代元素就变得无法从其标记中被读出。
TAB-E224tabula/no-theme-inline@theme inlineT20——inline 会在构建时把设计令牌取值代入工具类,把每一个有条件的设计令牌都编译没了。深色模式会在没有任何诊断提示的情况下失效。它是 shadcn 生态圈的默认做法,因此一个在那套生态上训练出来的智能体会本能地伸手去用它。
TAB-E225tabula/no-important-css项目 CSS 中的 !important每一个工具类都被归一化到特异性 (0,1,0),因此只有 rank 才能决定冲突的胜负。一个 !important 就会把一条声明置于这套证明之外。

因此,一个项目 .css 文件只可以包含:@import@source@utility@custom-variant@charset、 一条无实体的 @layer a, b; 顺序声明,以及注释。别无其他。

两道门禁

在 CI 里 / 构建时

bash
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,映照 本仓库自身的配置

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.csssrc/index.csssrc/styles.cssapp/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-500p-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> 注入。 任何一个组件在运行时写入文档的内容,都在这里所有门禁的覆盖范围之外。

Released under the MIT License.