agentcss 0.24.1 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/01-tokens/README.md +1 -1
  2. package/01-tokens/tokens.css +67 -1
  3. package/01-tokens/typography.css +1 -1
  4. package/02-primitives/button/demo.html +8 -0
  5. package/02-primitives/card/README.md +15 -0
  6. package/02-primitives/choice/README.md +30 -0
  7. package/02-primitives/choice/choice.css +1 -1
  8. package/02-primitives/input/README.md +1 -0
  9. package/02-primitives/select/README.md +42 -2
  10. package/02-primitives/select/demo.html +11 -6
  11. package/02-primitives/select/select.js +184 -94
  12. package/02-primitives/timeline/demo.html +2 -2
  13. package/02-primitives/timeline/timeline.css +1 -1
  14. package/03-patterns/accordion/README.md +15 -0
  15. package/03-patterns/content/README.md +11 -0
  16. package/03-patterns/drawer/README.md +1 -1
  17. package/03-patterns/dropdown/demo.html +1 -1
  18. package/03-patterns/form-validation/README.md +18 -0
  19. package/03-patterns/form-validation/form-validation.css +12 -1
  20. package/03-patterns/list/README.md +11 -0
  21. package/03-patterns/overlay/README.md +26 -6
  22. package/03-patterns/overlay/overlay.css +22 -3
  23. package/03-patterns/overlay/overlay.js +63 -17
  24. package/03-patterns/states/README.md +15 -0
  25. package/03-patterns/states/states.css +18 -1
  26. package/03-patterns/tooltip/README.md +12 -0
  27. package/04-recipes/longform/longform.html +3 -3
  28. package/04-recipes/report/README.md +9 -0
  29. package/04-recipes/report/report.css +5 -0
  30. package/04-recipes/table/README.md +16 -0
  31. package/04-recipes/tierA-tokens.css +3 -3
  32. package/09-assets/README.md +18 -0
  33. package/09-assets/bar/bar.css +25 -10
  34. package/09-assets/echarts-adapter/demo.html +3 -1
  35. package/09-assets/echarts-adapter/echarts-adapter.js +107 -34
  36. package/09-assets/scientific-plot/README.md +2 -1
  37. package/09-assets/scientific-plot/scientific-plot.js +0 -7
  38. package/AGENTS.md +2 -1
  39. package/CHANGELOG.md +370 -18
  40. package/CONTRIBUTING.md +6 -5
  41. package/README.md +9 -2
  42. package/README.zh-CN.md +25 -10
  43. package/SECURITY.md +5 -2
  44. package/START-HERE.md +7 -7
  45. package/SUPPORT.md +86 -24
  46. package/ai/START-HERE.md +3 -2
  47. package/ai/cli.js +126 -6
  48. package/ai/components.json +258 -2
  49. package/ai/components.meta.json +257 -40
  50. package/ai/components.schema.json +96 -0
  51. package/ai/contract.schema.json +11 -0
  52. package/ai/pdf-pages.js +79 -0
  53. package/ai/pdf.js +354 -0
  54. package/ai/tokens.json +2 -2
  55. package/ai/validate.js +48 -1
  56. package/dist/01-tokens/tokens.css +1 -1
  57. package/dist/02-primitives/select/select.js +158 -71
  58. package/dist/03-patterns/form-validation/form-validation.css +1 -1
  59. package/dist/03-patterns/overlay/overlay.css +1 -1
  60. package/dist/03-patterns/overlay/overlay.js +40 -11
  61. package/dist/03-patterns/states/states.css +1 -1
  62. package/dist/04-recipes/report/report.css +1 -1
  63. package/dist/04-recipes/tierA-tokens.css +1 -1
  64. package/dist/09-assets/bar/bar.css +1 -1
  65. package/dist/09-assets/echarts-adapter/echarts-adapter.js +75 -33
  66. package/dist/09-assets/scientific-plot/scientific-plot.js +0 -7
  67. package/dist/COSTS.md +37 -37
  68. package/docs/ACCESSIBILITY.md +3 -3
  69. package/docs/FACTS.md +144 -0
  70. package/docs/LAYER-BASELINE.md +28 -3
  71. package/docs/PRINT.md +450 -0
  72. package/docs/RELEASE-PROCESS.md +286 -0
  73. package/docs/VERIFY-RELEASE.md +167 -0
  74. package/index.html +1 -1
  75. package/llms-full.txt +8 -8
  76. package/llms.txt +5 -0
  77. package/package.json +6 -1
package/CHANGELOG.md CHANGED
@@ -16,6 +16,359 @@
16
16
 
17
17
  (无)
18
18
 
19
+ ## [0.25.0] - 2026-11-03
20
+
21
+ **次版本:新增一个子命令(`agentcss pdf`)+ 新增契约字段(`printPolicy` / `pageBudget`)
22
+ + 修一处打印行为(模态遮罩)。不改类名、不改令牌、不改既有 JS 的调用方式 ——
23
+ 下游不改代码即可升级;不打印的话,`@media print` 之外一个字节都没变。**
24
+
25
+ ### 修复:`coverage-gate` 的 CSS 覆盖率从"写在注释里"变成"真的算"
26
+
27
+ docstring 承诺了「每个组件 CSS 里的 class,是否至少被一个契约或 demo 引用到」,
28
+ 但代码里**没有任何提取 CSS class 的逻辑** —— 实际只判"有没有 demo、契约里提没提路径"。
29
+ 现在实装成四条机械判据,全部 100% 硬门禁:
30
+
31
+ | 判据 | 含义 | 现状 |
32
+ |---|---|---|
33
+ | **孤儿类** | CSS 里定义了,demo / 组件 JS / 契约 / README 四处都没提 | 0(原有 25 个,全部补文档或删规则) |
34
+ | **幻影类** | README 写到、全库却没有这个类 | 0(3 个历史警告性提及显式豁免) |
35
+ | **未渲染类** | 只被 README 认领,demo / JS / 契约一次没碰 | 41 / 472(棘轮钉住,不得变多) |
36
+ | **跨组件同名主体** | 同一类名被两个组件声明为规则主体 | 5 条,全部登记在 `css-ownership.json` |
37
+
38
+ ⚠️ 三处**判据口径**是踩过坑才定下来的,写在这里免得后人改回去:
39
+
40
+ - **剥注释、`url(...)`、字符串**再取类名。否则 `data:image/svg+xml,...http://www.w3.org/`
41
+ 能读出 `.org` / `.w3` 这种假类名(实测 2 处)。
42
+ - **"定义"用全部选择器,"占用类名"只用规则主体。** `.switch--lg .switch__track` 这种
43
+ 修饰符没有自己的规则,只看主体会把它误判成"CSS 里不存在"(实测多报 12 条幻影);
44
+ 反过来 `.list__actions .btn` 是 list 在上下文里覆盖 button,不是 list 占用了 `.btn`
45
+ (按"任意出现"判重名会多报 9 条噪音)。
46
+ - **幻影要排除交叉引用**:form-validation 的 README 写 `.field`(那是 input 的)。
47
+ 判据改成"别处也没有才算幻影",并且**存在于本组件 JS 里也算真实存在**
48
+ (`.accordion--many` 是纯行为开关,一行样式都没有)。
49
+
50
+ 反向控制 5 条(孤儿 / 幻影 / 未登记重名 / 登记过期 / 水位上涨)全部在
51
+ `--selftest` 里,另在真仓库上逐条注入验证过会红。
52
+
53
+ ### 修复:三处样式缺陷(都是上面这把尺子量出来的)
54
+
55
+ | 缺陷 | 实测症状 | 修法 |
56
+ |---|---|---|
57
+ | `.field__error` 的"该不该显示"取决于 CSS 加载顺序 | input.css 先 → `display:none` ✅;form-validation.css 先 → `display:flex`,**首屏把全部错误提示挂出来** | 把默认隐藏抬成 `.field .field__error`(0,2,0),顺序不再是变量。错误元素必须在 `.field` 内(结构契约本来就如此) |
58
+ | `states.css` 里的骨架屏动画**无视** `prefers-reduced-motion` | 该文件自带一份完整的 `.skeleton` 实现,`:after` 的 `animation` 无条件生效;而 states 的 demo 没引 skeleton.css ⇒ 减少动态效果的用户照样看到闪烁 | 包进 `@media (prefers-reduced-motion: no-preference)` + 补 `reduce` 档 |
59
+ | `.select` 同名不同义(choice vs select) | choice 的是**原生 `<select>` 外壳**(`inline-flex`+`align-items:center`),select 的是**自定义下拉根**(`inline-block`+`min-width:12rem`)。实测两序结果不同:先 choice 后 select 时原生外壳丢垂直居中 | **未改**(改名是破坏性变更)。登记在 `css-ownership.json`,0.26.0 处理 |
60
+
61
+ 另:`.skeleton*` 两份实现已分叉(底色令牌、高度、变体集合、reduced-motion 全不同),
62
+ 同样登记待去重。
63
+
64
+ ### 文档:25 个类从"代码里有、文档里没有"变成能查到
65
+
66
+ 它们都是真实公开 API,只是从没写进 README —— 照着旧文档用的人根本不知道有这些能力。
67
+ 补齐的包括 `.card__footer`、`.choice__hint`、`.select__control--auto`、
68
+ `.field__input--textarea`、`.accordion--flush` / `.accordion__hint` / `cols-*` / `.acc-table`、
69
+ `.task-list-item`、`.drawer--left` / `--top` / `--bottom`、
70
+ `.field--checking` / `.field__checking-hint`、`.list__note`、`.skeleton--avatar`、
71
+ `.tooltip--bottom` / `--inset-inline-start` / `--inset-inline-end`、
72
+ `.rep__lede`、`.cell--na`、`.table--compact`。
73
+
74
+ 顺带修掉 **BEM 简写**:drawer 的 `--left` / overlay 的 `--warning` 这类写法
75
+ 没法直接抄进 `class=""`,一律改成完整类名。
76
+
77
+ ### 新增:`05-audit/css-ownership.json` 与 `05-audit/css-coverage-baseline.json`
78
+
79
+ 前者登记跨组件同名类的归属与理由(`kind: split` = 有意拆分,`kind: conflict` = 真冲突待改),
80
+ **登记过期也判失败**(现实中不再重名就必须删掉,白条不许腐烂)。
81
+ 后者是"文档有、demo 没渲染"的水位,只能降不能升,`--record` 显式重录。
82
+
83
+ ### 修复:外部审计查出的交互缺陷(select / overlay)
84
+
85
+ 对一个公开版本做外部抽查后,逐条复现并修掉。**都是行为修正,不改类名、不改 API 形状**
86
+ (只新增:`select.values`、`select.destroy()`)。
87
+
88
+ **select**
89
+
90
+ | 缺陷 | 实测症状 | 修法 |
91
+ |---|---|---|
92
+ | 按钮上挂了**两个** `keydown` | 按一次 `↓` 走**两格**;关闭态按 `Enter` 打开后立刻被第二个处理器提交关闭;多选 `Enter` 选上又取消 | 合并成一个处理器,`Enter` 在关闭态只负责打开 |
93
+ | 多选只有一个字符串状态 | `aria-multiselectable="true"` 是**谎报**;点第二项会把第一项顶掉 | 内部改成数组,`aria-selected`、按钮文本、提交值三处同源 |
94
+ | 空选项列表 | 初始化抛 `Cannot read properties of undefined (reading 'querySelector')` | `optText(null)` 返回空串,绘制/取值全部兜底 |
95
+ | 没有 `destroy()` | 动态移除组件后,`document` 上的 dismissable 监听与 typeahead 缓冲全留着 | 对外暴露 `destroy()`,摘 keydown/click/dismissable/typeahead 并删掉自己补的 hidden |
96
+ | 表单集成半截 | 根里没有 `hidden` 时不建;改值不发原生事件 | 没 hidden 就按 `name` → `data-name` → 根 `id` 兜底自建;每次变值在根上派发原生 `change` |
97
+ | 多选提交 | 一个 `hidden` 只能带一个值 | **一个值一个同名 `input[type=hidden]`**(`name="sector"` × N) |
98
+
99
+ 同时把「一次按键只走一格 / 关闭态 Enter 只打开 / 多选 Enter 只切一次 / 空列表不抛 /
100
+ destroy 后不再响应 / 无 hidden 自建并派发 change」写成**精确断言**。
101
+ 旧断言只判"序列有变化",所以「一次走两格」能从门禁底下过去。
102
+
103
+ **overlay**
104
+
105
+ | 缺陷 | 实测症状 | 修法 |
106
+ |---|---|---|
107
+ | `inert` 释放固定写 `false` | 页面原本 `inert` 的区域,开关一次弹层就被**误解锁** | 引用计数条目里记 `prev`,降为 0 时还原原值 |
108
+ | 嵌套时跳过所有 `.dialog-backdrop` | 开 A 再开 B,A 既没 `inert` 也没 `aria-hidden` ⇒ **两个 dialog 同时暴露给读屏** | 只跳过自己这一层,别的弹层照常隔离 |
109
+ | 背景只快照一次 | 弹层开着时插入 `body` 的节点(toast / portal / 埋点 iframe)不受隔离 | `MutationObserver` 盯 `body` 直接子节点;backdrop 被外部摘掉时**自愈回收**(防幽灵观察器锁死后续焦点) |
110
+ | `focusIsMine` 是死变量 | 关外层时会把焦点从内层抢走 | 真的用它把关:不是我的焦点就不还原 |
111
+
112
+ ### 修复:探针把「端口有人应答」当成「服务是我们的」
113
+
114
+ 发布工作流在 CI 上报过一句很吓人的话:
115
+
116
+ ```
117
+ 🔴 ai/components.json 声明了浏览器里不存在的 global:(16 个全部"探不到")
118
+ ```
119
+
120
+ 16 个组件同时消失,显然不是实现坏了。根因在量具:`_probe.py` 复用静态服务的
121
+ 判据是「8000 端口有人应答」,而 8000 上可能跑着**别的目录**的服务 ⇒ 每个 demo
122
+ 页 404 ⇒ 探针把"页面没加载"记成"全局不存在"。
123
+
124
+ 本地可复现(在 8000 起一个服务别的目录的 `http.server`,输出一模一样),
125
+ 修法是三条:canary 验身份(`/01-tokens/tokens.css` 200 且够大)、
126
+ 不复用就换空闲端口自己起、地址通过 `BASE` 传给探测脚本。
127
+ 另加一条判据:**全部 absent ⇒ 报"没有取证"**,不许把量具问题说成实现问题。
128
+
129
+ ### 新增:`agentcss pdf` —— 真的把页面导出成 PDF(零依赖)
130
+
131
+ 在这之前,本库能证明"按 Ctrl+P 印出来是合格的",但**产不出一份文件**。
132
+ 这两件事之间差一个 `@page`,而 `@page` 属于导出方、不属于组件库 ——
133
+ 库一旦声明它,使用者的边距控制会静默失效(实测同一页面 943 → 978 runs)。
134
+
135
+ ```bash
136
+ npx agentcss pdf report.html --out report.pdf --paper=a4 --margin="18mm 15mm" --max-pages=8
137
+ ```
138
+
139
+ | 参数 | 默认 | 说明 |
140
+ |---|---|---|
141
+ | `--paper` | `a4` | `a4` / `letter` / `a5` / `a3` / `legal` |
142
+ | `--margin` | `18mm 15mm` | CSS 简写(`18mm` / `"18mm 15mm"` / 四段) |
143
+ | `--max-pages` | 无 | 页数上限。**没有全局默认值**:页数取决于内容,猜一个数字比不给更坏 |
144
+ | `--chrome` | 自动探测 | 也可用 `AGENTCSS_CHROME` / `CHROME_PATH` |
145
+
146
+ **零运行时依赖**:用使用者已装好的 Chrome / Chromium / Edge(`--print-to-pdf`)。
147
+ 为了 5% 的导出需求让 95% 的人多装 300 MB 浏览器,是这条承诺唯一实质的内容。
148
+ 找不到浏览器 ⇒ 退出码 3 + 一句人话,不静默降级成空文件。
149
+
150
+ 超预算 ⇒ **退出码 5**,产物保留,并给出三条出路(拆文档 / 进附录 / 截断)。
151
+ `truncate` **故意不实现**:一个会替你悄悄删掉后半份报告的工具,比报错停下更危险。
152
+
153
+ ⚠️ 一条必须知道的差异:导出产物**包含底色**(headless 打印的默认),
154
+ 浏览器打印对话框默认**不勾**"背景图形"。所以同一页面在"导出"与"Ctrl+P"下,
155
+ 彩色块上的观感不同 —— 这不是 bug,是两个产品。
156
+
157
+ ### 新增:`--medium=screen|paper` 与 `pageBudget`
158
+
159
+ `ai check` 多了一根**与档位正交**的轴(`strict+paper` 与 `creative+paper` 都必须合法,
160
+ 所以不做成第四个 profile):
161
+
162
+ ```bash
163
+ npx agentcss ai check doc.json --medium=paper
164
+ ```
165
+
166
+ `screen`(默认)下行为完全不变 —— 95% 的人不该为打印付注意力。
167
+ `paper` 下追加三条义务:必须声明 `pageBudget`(错)、
168
+ 不允许 `pagination:"server"` 的组件(错,纸上只有当前一页而文件看起来完整)、
169
+ 用了 `printPolicy:"omit"` 的组件给提示(那部分不会印,作者得知道)。
170
+
171
+ `contract.schema.json` 新增可选字段 `pageBudget`(正整数,无默认值)。
172
+
173
+ ### 新增:`05-audit/pdf-export-gate.js` —— 导出路径的 15 条断言
174
+
175
+ `print-gate` 判的是 **CDP `page.pdf()`**(我们自己的渲染),
176
+ 使用者走的是 **Chrome 命令行 `--print-to-pdf`**(另一条路)——
177
+ `@page` 来源与底色默认都不同,所以前者全绿推不出后者能用。
178
+
179
+ ```
180
+ E1 真产出 / E2 确定性 / E3 纸幅 / E4 预算以内 / E5 预算超限 / E6 打印规则到产物
181
+ E7 边距生效 / E8 无浏览器 / E8′ 零候选 / E9 参数校验 / E9″ 报错回显原始输入
182
+ E10 默认输出 / E11 产物过分析器 / E12 定位一致
183
+ ```
184
+
185
+ 反向控制实测:把 `@page` 注入那行注释掉 ⇒ E3 报 `a4=612×792pt`(回落成 Letter)、
186
+ E7 报 `10mm=2 页 / 60mm=2 页`;把 `tokens.css` 里 `.tabs__panel[hidden]` 那条删掉 ⇒
187
+ E6 报 `runs on=4 off=4`。全部约 11s,不需要静态服务(走 file://)。
188
+
189
+ ### 新增:`print-gate` 判据 ⑬ —— 页数预算写在源码里
190
+
191
+ ③ 是"和上次比有没有变",⑬ 是"和该有的页数比"。两者缺一不可:
192
+ CSS 改动让 7 页变 13 页,跑一次 `--update` 之后 ③ 就绿了,
193
+ 而 ⑬ 不会 —— 预算**不经 `--update` 重录**(写进基线就等于把证据洗掉)。
194
+ 预算 = 基线页数 × 1.4(实测分页通胀断崖是 +92%,1.4 倍抓得住)。
195
+
196
+ ### 新增:打印契约 `printPolicy`(机器可读,进 `ai/components.json`)
197
+
198
+ 打印行为此前只存在于 `tokens.css` 的一个 `@media print` 块里,没有任何东西
199
+ 能回答三个问题:契约写的实现了吗、CSS 里每条隐藏都声明了吗、声明了真的生效吗。
200
+
201
+ 现在每个组件在 `ai/components.json` 里有一条 `printPolicy`,`policy` 是
202
+ **有限枚举**,每个值对应一组能在浏览器里量的断言:
203
+
204
+ | policy | 含义 | 断言 |
205
+ |---|---|---|
206
+ | `omit` | 内容不上纸 | 打印态 `display: none` |
207
+ | `expand` | 放开屏幕上的限制(隐藏 / 收起 / 限高 / 固定定位) | `display≠none`、`visibility≠hidden`、`position≠fixed`、`max-height:none`、`overflow` 不裁、有文字时高度 > 0 |
208
+ | `reveal` | 只在纸上出现 | 屏幕 `display:none` 且打印态不是 `none` |
209
+ | `keep-together` | 不跨页切断 | `break-inside: avoid` |
210
+ | `allow-break` | 退出 `keep-together` | `break-inside: auto` |
211
+
212
+ - 规则 31 条:库级 7(`printDefaults`)+ 组件 24(16 个组件)。
213
+ - `printPolicy: []` 是明确值「原样保留」;**缺字段才算漏**(门禁红)。
214
+ - 新增 `pagination`(`none` / `client` / `server`)。`server` 与 `expand`
215
+ 不能同时成立:服务端分页只有当前一页,展开它 = 印出一页却看起来完整。
216
+ 今天没有组件声明 `server`,这条规则处于守夜状态,由反向控制证明它能开火。
217
+ - 生成器(`gen-ai-contract.py`)在**写出契约之前**校验枚举、选择器形状、
218
+ `why` 不能是空话、同一选择器不许被认领两次。
219
+
220
+ `ai/components.json` 53.2 KB → 61.1 KB(+7.8 KB)。
221
+
222
+ ### 新增:`05-audit/print-policy-gate.js` —— 契约 / 实现 / 运行时三向核对
223
+
224
+ 四条腿,各抓一类失效:
225
+
226
+ | 腿 | 它问什么 | 实测抓到过什么 |
227
+ |---|---|---|
228
+ | L0 归属 | 选择器在库里真的存在吗 | `.overlay`(见下) |
229
+ | L1 声明→实现 | 契约里的规则在 print 块里有相符声明吗 | 故意删一条即红 |
230
+ | L2 实现→声明 | 每条会丢内容的声明都有规则认领吗 | 塞一条裸 `display:none` 即红 |
231
+ | L3 声明→生效 | 真浏览器 print 媒体下真的生效吗 | 自己刚写的规则被同特异性盖掉(E258) |
232
+
233
+ **反向控制 7 条(覆盖四条腿),全绿**,其中两条是历史事故的逐字复刻:
234
+ E254(去掉 `.tabs ` 前缀让组件 CSS 盖掉它)、E255(只删 `visibility: visible`,
235
+ display 与高度全对、一个字不印)。全量门禁从 223 → 225 道。
236
+
237
+ ### 修复:屏幕上开着 dialog 时打印,整页 40% 黑
238
+
239
+ `tokens.css` 的隐藏清单里有 `.overlay`,而**全库没有任何 CSS 定义过这个类**
240
+ (`grep -rn '\.overlay' --include=*.css` 只命中 tokens 自己那一行)。
241
+ 真正的遮罩叫 `.dialog-backdrop`,它不在清单里,于是照印:computed 是
242
+ `position: fixed; inset: 0; background: rgba(21,24,28,.4)`。
243
+
244
+ 现在**不隐藏遮罩,而是拉回正常流**(`position: static` / 无 padding / 无底色;
245
+ `.dialog` 取消 100vh 限高)。隐藏会让 `.dialog`(它是遮罩的子节点)里的字
246
+ 一起消失 —— 那正是本轮在修的静默截断,不能自己再造一个。
247
+
248
+ 规则写在 `03-patterns/overlay/overlay.css`,**不能**写进 `tokens.css`:
249
+ tokens 先加载、同特异性,会被 `position: fixed` 盖掉(写完当场被 L3 抓到)。
250
+
251
+ ### 修复:`03-patterns/overlay/README.md` 的三行假类名
252
+
253
+ `.overlay`、`.toast__icon`、`.dialog__body` 三个类在库里都不存在
254
+ (真实名字分别是 `.dialog-backdrop`、不存在、`.dialog__desc`)。
255
+ 照着写的人会得到「样式没生效,但也不报错」。
256
+
257
+ ### 新增:内容完整性审计转**硬判据**(`print-content-audit`)
258
+
259
+ 之前它只报告不判红,理由是「先出清单 → 写进 printPolicy → 再转硬判据」。
260
+ printPolicy 落地之后这一步做了,而且**没有引入白名单**:
261
+
262
+ | 判据 | 条件 |
263
+ |---|---|
264
+ | ① 契约违约 | 命中 `expand` 的字,打印仍不印 ⇒ 红 |
265
+ | ② 静默消失 | 屏幕可见 → 纸上消失,且无 `omit` 声明 ⇒ 红 |
266
+ | ③ 不算 | 屏幕也看不见 ⇒ 交互状态 / 未触发的空态 / a11y 反例 |
267
+
268
+ 判据① 咬的是「藏起它的**正是**契约承诺要展开的那个元素」—— 所以
269
+ `tabs` 的未选中面板(屏幕上也隐藏)照样能抓到,而那正是少印 68% 的那一次。
270
+ 实测:13 页采样集 / 47 页全站都是 0 违约;把 `tokens.css` 里那条
271
+ `.tabs .tabs__panel[hidden]` 临时删掉,`pattern-tabs` 立刻红出 **7 条契约违约
272
+ / 84 字**,并逐条指出是哪个 `#panel` 把字藏起来了。
273
+
274
+ 反向控制 14 条:判定表 + 真 DOM 扫描 + 一个 `data:` 页走完整管线,
275
+ 同时抓出 `silent` 与 `contract`。
276
+
277
+ ### 新增:消费者实装冒烟(`consumer-smoke`)
278
+
279
+ `pack-smoke.sh` 判的全是**结构**:包里有那些文件、exports 解析得出来、JS 语法对。
280
+ 结构对 ≠ 能用。新加的第 8 步真开一次浏览器,六条判据:
281
+
282
+ ```
283
+ C1 零 404 页面引的每个包内文件都在
284
+ C2 样式生效 --accent 解析得出、.btn 背景不是透明
285
+ C3 行为生效 tabs.js 自动初始化,点第二个 tab 真的切过去
286
+ C4 打印生效 打印态未选中面板被展开(引了 tokens.css 才成立)
287
+ C5 契约可查 npx agentcss ai components --json → 40 个组件、40 条 printPolicy
288
+ C6 A 档在包 04-recipes/tierA-tokens.css 在包里
289
+ C7 导出可用 npx agentcss pdf 在装好的包上产出一份真 PDF(1 页 / 注入 @page)
290
+ ```
291
+
292
+ 反向控制 4 条(少一个文件 / 不引 tokens 各自都必须红)。C7 是「下载之后能不能用」
293
+ 最后一块拼图:它跑的是 `node_modules/agentcss/ai/pdf.js`,不是仓库里的那份。全量门禁里也跑它
294
+ (自己 pack 一遍),`pack-smoke.sh` 则复用已经装好的目录 —— 判据只有一份。
295
+
296
+ ### 修复:文档里最靠前的那条命令不能用
297
+
298
+ `AGENTS.md` / `llms.txt` / `docs/STABILITY.md` / `docs/VERIFY-RELEASE.md` 与
299
+ CLI 自己的帮助都教 `npx agentcss ai components --json`,而分发器只认
300
+ `components` ⇒ 照抄第一条命令得到「未知子命令:ai」。
301
+
302
+ 现在 `ai` 是可选的**前缀别名**,两种写法都通;帮助里也写明了这一点。
303
+ 这是消费者视角最贵的一类错误:文档第一条就打脸,后面写得再准也读不到。
304
+
305
+ ### 体积
306
+
307
+ `overlay.css` 5.1 → 5.4 KB(+969 B,其中约 600 B 是注释)。
308
+ 全量 raw +1,331 B / gzip +504 B(+0.16%)。按惯例重录基线,数字在这里写明。
309
+
310
+ ### 屏幕零影响
311
+
312
+ 109 张截图**逐字节一致**(47 浅色 + 47 暗色 + 15 RTL)。只有 3 个
313
+ `_stamp.json` 变了 —— 因为渲染指纹含 CSS 全集,改了 CSS 就必然要重录戳。
314
+ `print-gate` 的 18 份 PDF 页数与 run 数**与改动前完全相同**(没有任何采样页
315
+ 开着 dialog,这正好是「无对话时改动是惰性的」那条控制)。
316
+
317
+ ---
318
+
319
+ ## [0.24.2] - 2026-11-01
320
+
321
+ **纯修订版:没有 CSS / JS / 类名 / 令牌的变更,下游不需要改任何代码。**
322
+ 改的是「发布出来的东西对不对」和「检查它们的手段对不对」。
323
+
324
+ ### 修复:`0.24.1` 的 npm 包里残留了旧数字
325
+
326
+ `agentcss@0.24.1` 的 `llms-full.txt` 第 13 行写 `39 components`,
327
+ 同一文件第 24 行写 `Components (40)`;而站点上同路径文件已是 40。
328
+ 两个渠道同版本号、两份内容(sha256 不同)——
329
+ 原因是修复只重建了站点用的生成物,没有重新发版。
330
+
331
+ 这一版由当前源码重新生成后发布,两份内容一致。
332
+ **不追溯修改历史版本**:`0.24.1` 在 registry 上保持原样,
333
+ 因为"已发布的版本不可变"比"历史看起来整齐"更重要。
334
+
335
+ ### 修复:`dist-fresh` 看不见产物改动
336
+
337
+ `build-dist.py --check` 原先只比「源 sha 与 manifest 记录是否一致」。
338
+ 实测:给 `dist/01-tokens/tokens.css` 加一段注释、再把 `--sp-4` 的值改掉,
339
+ `--check` 仍然打印 OK —— 而文档里写着「不要手改 dist,dist-fresh 会红」,
340
+ 等于一道不存在的保护。
341
+
342
+ 现在 `--check` 有两段判据:源 sha 一致 **且** 产物字节 == 生成器现算字节
343
+ (干跑一遍、逐文件比、不写盘)。手改产物会报出从第几个字节开始不一样。
344
+ 发布路径同样受益:`npm run publish:check` 里就包含这道。
345
+
346
+ ### 修复:`md-render --check` 的表格判据比措辞窄
347
+
348
+ 文档写「表格必须是 `<table>`(不能是 div 模拟的)」,
349
+ 实现只有一句 `<div ... class="...table`。
350
+ 用 `class="grid"`、`role="table"`、或让 `<tr>` 跑出 `<table>` —— 三种都抓不到。
351
+
352
+ 现在拆成三条可独立关闭的规则(表格子元素越界 / ARIA 角色模拟 / class 含独立词 table),
353
+ 措辞同步收窄到"三种已知形态",并写明"这是不是表格本身不可判定"。
354
+ 新增 `--selftest` 13 条,含三条元反向(关掉对应规则必须变绿)。
355
+
356
+ ### 发布治理
357
+
358
+ - `docs/VERIFY-RELEASE.md`:怎么核验一个版本 —— npm `gitHead`、
359
+ attestation `subject` 与 registry `integrity` 的对应关系、Pages deployment 的 `sha`,
360
+ 以及 L0–L3 证据分级(明确 L2「全量文件完整性」目前不存在)。
361
+ - `docs/RELEASE-PROCESS.md`:生成链八个生成器的顺序、冻结点覆盖范围、
362
+ npm 链与站点链两条独立状态机、六种失败组合的恢复动作。
363
+ - tag 从这一版起必须写成 `git tag -a vX.Y.Z -m "source-commit: <40 位>"`
364
+ ——`release-gate` 会验证它等于 tag 实际指向的提交,且该提交被远端分支包含。
365
+ `0.24.1` 及更早的 tag 没有该字段,走兼容判据。
366
+ - 发布工作流先 `probe` 再发布:npm / GitHub Release 各自按已存在与否门控,
367
+ 已存在的 npm 版本必须核 `provenance.gitCommit` 与 tag 指向一致,不一致判失败。
368
+ `dist` 的两道语义比对**无条件**执行,不与 npm 是否已存在共用跳过条件。
369
+
370
+ ---
371
+
19
372
  ## [0.24.1] - 2026-10-31
20
373
 
21
374
  ### `page.css` 补上 `<pre>` 的兜底(实测出来的,不是推演)
@@ -894,8 +1247,7 @@ combobox 的 `role`/`aria-haspopup` 是脚本后期设上去的 ⇒ 加载慢半
894
1247
 
895
1248
  `docs/CONVENTIONS.md` 第七节与 `docs/LAYER-BASELINE.md` 早有结论:7 行真机实测里
896
1249
  Android 14 · 百度浏览器(Chromium 97)**缺 `@layer`**,而 `@layer { … }` 不支持时是
897
- **整块丢弃** ⇒ 那一层样式直接消失。切层在这个库里不是改进,是会让部分浏览器变白屏。
898
- 记进 `internal/ERRORS.md` E167:**"0 处"只说明现状,不说明原因;有门禁守着的"没有",
1250
+ **整块丢弃** ⇒ 那一层样式直接消失。切层在这个库里不是改进,是会让部分浏览器变白屏。教训是:**"0 处"只说明现状,不说明原因;有门禁守着的"没有",
899
1251
  几乎都是决定。**
900
1252
 
901
1253
  ### 体积
@@ -1228,7 +1580,7 @@ Actions → release → Run workflow → 勾 `shoot_baseline`,
1228
1580
  (**不是门禁发现的** —— 门禁只验它写了判据的东西。这条是有人顺着 demo 源码
1229
1581
  读下来才看见的,记在这里提醒自己:判据的边界就是视野的边界。)
1230
1582
 
1231
- ### 本轮流程上踩的坑(详见 `internal/ERRORS.md` E146–E148)
1583
+ ### 本轮流程上踩的坑
1232
1584
 
1233
1585
  - **E146** 新判据 F7 的豁免条件第一版用 `or` 连接「版本号」与「过去时语汇」,
1234
1586
  结果 `docs/ACCESSIBILITY.md` 表格行里的 WCAG 编号「**4.1.2**」被当成版本号
@@ -1437,7 +1789,7 @@ CSS 体积预算 125 → **132 KB**(剥注释后口径):两个新组件 +
1437
1789
  | 把 `fl-nav-open` 改成 `fl-nav-opened` | 🔴 `fl-nav-open:基线里有、现在没了` |
1438
1790
  | 把 `TocApi.init` 改名 | 🔴 `TocApi.init:基线里有、现在没了` |
1439
1791
 
1440
- 过程中自己踩了两个洞,都记进了 `internal/ERRORS.md`:
1792
+ 过程中自己踩了两个洞:
1441
1793
 
1442
1794
  - **E131**:扩了 `KINDS`,但汇总行还写死 `cur_by['token'] | cur_by['class']`
1443
1795
  ⇒ ⑦(未登记的移除)只对前两类算差集,新加的两类删了也不报。
@@ -1745,7 +2097,7 @@ switch :一块改成了 cmp(div vs 原生),另一块(label)改不
1745
2097
 
1746
2098
  自检从 10 条加打到 32 条(21 条必须红 + 11 条必须绿)。
1747
2099
 
1748
- ### 判据本身走过的两条错路(都留在代码注释与 `internal/ERRORS.md` 里)
2100
+ ### 判据本身走过的两条错路
1749
2101
 
1750
2102
  - 第一版按「类名是否定义在组件样式里」判标本 → `sparkline.css` 里
1751
2103
  `.row > .sparkline` 这种后代选择器把排版类收进了类名表(假绿);
@@ -1780,7 +2132,7 @@ Tab 的链接)、`tabs` ×2(div 当 tab、三个 `tabindex="0"`)、`badge`
1780
2132
  原因是新加的反例真的写了 `aria-checked`(那一块讲的就是"手写这个属性"),
1781
2133
  而抽取器只取 `aria-` 后面的名字(与 `expanded` 同约定)。原生
1782
2134
  `<input role="switch">` 并不需要它 —— 这条是**反例带进契约**的。
1783
- 选择保留并写在这里,而不是悄悄让它躺在 diff 里(`internal/ERRORS.md` E122)。
2135
+ 选择保留并写在这里,而不是悄悄让它躺在 diff 里。
1784
2136
 
1785
2137
  ### 门禁
1786
2138
 
@@ -1798,7 +2150,7 @@ Tab 的链接)、`tabs` ×2(div 当 tab、三个 `tabindex="0"`)、`badge`
1798
2150
  `tablist "基本信息 高级设置"`,`「文件已删除」` 那条 toast 会被念成
1799
2151
  `status [live=polite atomic=true]`。三处都逐条核过,不是照单全收。
1800
2152
 
1801
- ### 自己踩的坑(`internal/ERRORS.md` E119–E123)
2153
+ ### 自己踩的坑
1802
2154
 
1803
2155
  - **E119/E120**:判"有没有标本"的两条错路(类名定义表 / 边框底色),
1804
2156
  两头都有误判实例,都留在代码注释里。
@@ -1825,7 +2177,7 @@ Tab 的链接)、`tabs` ×2(div 当 tab、三个 `tabindex="0"`)、`badge`
1825
2177
  恰好绕开了 `<li>` 的真实 24px 坑。
1826
2178
 
1827
2179
  前两处已改成「两栏用同一句长文本,反例真的折行、正例真的省略」。
1828
- 第三处的教训写进了 `internal/ERRORS.md` —— 反例的坏必须来自**量过的**坑。
2180
+ 第三处的教训:反例的坏必须来自**量过的**坑。
1829
2181
 
1830
2182
  ### 新增
1831
2183
 
@@ -3889,11 +4241,11 @@ B 还开着,背景却能点了 —— 用户看到的是「弹窗明明还在
3889
4241
 
3890
4242
  ## [0.7.9] - 2026-10-09
3891
4243
 
3892
- > 依据两份外部严格工程评审。⚠️ 评审的 12 项指控**逐条对着源码核过**:
3893
- > 9+4 项成立(已修),2 项不成立(未改,理由见下),另外发现 3 项评审没提的新缺陷。
4244
+ > 依据两份外部严格工程审计。⚠️ 12 项问题**逐条对着源码核过**:
4245
+ > 9+4 项成立(已修),2 项不成立(未改,理由见下),另外自查出 3 项审计没提到的新缺陷。
3894
4246
  > ⚠️ 本版本起,门禁注册数由 `doc-facts-gate` 自动核对,文档里写错就红。
3895
4247
 
3896
- ### 修:CI 工作流的 YAML 语法错误(评审 P0-1)
4248
+ ### 修:CI 工作流的 YAML 语法错误
3897
4249
 
3898
4250
  `.github/workflows/gate.yml` 里躺着一行:
3899
4251
 
@@ -3920,7 +4272,7 @@ YAML 规范要求注释标记 `#` 必须与前面的内容由**空白**分隔。
3920
4272
  剥完留下的空格会让 `check.js` 重新变成独立 token。
3921
4273
  正确规则是「路径必须是**独立参数**」(前一个字符只能是空白/引号/行首)。
3922
4274
 
3923
- ### 修:浏览器矩阵**没有真的切换引擎**(评审 P0-1,这一版最严重的一条)
4275
+ ### 修:浏览器矩阵**没有真的切换引擎**(这一版最严重的一条)
3924
4276
 
3925
4277
  矩阵声明了 chromium / webkit / firefox 三个引擎,
3926
4278
  但 `matrix.engine` **只用在 `npx playwright install` 那一步**,
@@ -3928,7 +4280,7 @@ YAML 规范要求注释标记 `#` 必须与前面的内容由**空白**分隔。
3928
4280
  硬编码的 Chromium 路径。⇒ **三个作业跑的都是 Chromium**。
3929
4281
 
3930
4282
  ⚠️ 这比"没做跨浏览器"更坏:它产生了一份**看起来存在**的证据。
3931
- 评审拿它当覆盖率,使用者拿它当兼容承诺,而它一次都没测过
4283
+ 审计会拿它当覆盖率,使用者会拿它当兼容承诺,而它一次都没测过
3932
4284
  (与 I-10「报告通过却什么也没查」同一类失效)。
3933
4285
 
3934
4286
  ⇒ 三处改动:
@@ -3936,7 +4288,7 @@ YAML 规范要求注释标记 `#` 必须与前面的内容由**空白**分隔。
3936
4288
  写错引擎名**立刻报错**(不许静默退回 —— 退回就等于没选)。
3937
4289
  缺 playwright 也报错,**绝不退回 Chromium**。
3938
4290
  2. 新增 `05-audit/engine-probe.js`:每个作业**自己站起来报 UA**,
3939
- 并校验 UA 属于声明的引擎族(评审要的验收标准)。
4291
+ 并校验 UA 属于声明的引擎族(验收标准)。
3940
4292
  🔴 Chromium 的 UA 里**也含** `AppleWebKit`(它本是 WebKit 分叉)
3941
4293
  ⇒ 只判子串会把 Chromium 认成 WebKit —— 而那正是原来那个 bug 的形态。
3942
4294
  3. 新增 `05-audit/engine-gate.js`:逐个引擎启动,要求
@@ -3962,7 +4314,7 @@ YAML 规范要求注释标记 `#` 必须与前面的内容由**空白**分隔。
3962
4314
  不等于 iOS WKWebView / Android WebView 真机(软键盘、安全区、
3963
4315
  `100vh`、页面回弹、VoiceOver 一项都没覆盖)。真机仍是 ❌,见 `SUPPORT.md`。
3964
4316
 
3965
- ### 修:popover 在移动端真的横向溢出(开真引擎后**新发现**,评审没提)
4317
+ ### 修:popover 在移动端真的横向溢出(开真引擎后**新发现**,审计没提)
3966
4318
 
3967
4319
  开 WebKit 后 popover 契约红了一条:`483 ≤ 393`。
3968
4320
  逐元素量下来两引擎**布局完全一致**,差别只在 `window.innerWidth`:
@@ -3973,14 +4325,14 @@ YAML 规范要求注释标记 `#` 必须与前面的内容由**空白**分隔。
3973
4325
  | webkit | 483 | 393 | 判据红 |
3974
4326
 
3975
4327
  ⇒ **溢出是真的,Chromium 只是用"视口被内容撑大"的方式把它量成了没溢出。**
3976
- 这正是评审那句"错误的矩阵会掩盖真问题"的实例。
4328
+ 这正是"错误的矩阵会掩盖真问题"的实例。
3977
4329
 
3978
4330
  修法是让四个方向的演示**竖排**(`.row--stack`)——
3979
4331
  `end` 方向浮层挂在锚点右侧(`inset-inline-start: 100%`),横排时必然顶出视口。
3980
4332
  ⚠️ 这不是"改测试让它过":溢出是真的;竖排后演示成立。
3981
4333
  但**组件本身不会碰撞翻转**这件事没有变 ⇒ 已写进 `SUPPORT.md` 的「已知限制」。
3982
4334
 
3983
- ### 修:锁文件版本漂移(评审 P0-2)
4335
+ ### 修:锁文件版本漂移
3984
4336
 
3985
4337
  `package.json` = 0.7.7,`package-lock.json` 根包 = **0.3.0**。
3986
4338
 
@@ -3993,7 +4345,7 @@ YAML 规范要求注释标记 `#` 必须与前面的内容由**空白**分隔。
3993
4345
  (lockfile 的 `version` 与 `packages[""].version` 两处都查,连带 name)。
3994
4346
  突变体证明它是活的:把锁文件改回 0.3.0 ⇒ EXIT=1。
3995
4347
 
3996
- ### 修:`SUPPORT.md` 里三句假话(这一版**新发现**,评审没提)
4348
+ ### 修:`SUPPORT.md` 里三句假话(这一版**新发现**,审计没提)
3997
4349
 
3998
4350
  | 原文 | 事实 |
3999
4351
  |---|---|
package/CONTRIBUTING.md CHANGED
@@ -29,8 +29,9 @@ npm run gate # 全量:含浏览器类
29
29
  npm run gate:fast # 只跑静态层,不用装浏览器
30
30
  ```
31
31
 
32
- 全量是 **220 道门禁**(4 路并发实测约 9 分钟 / 退出码 0;单道耗时累加 1691 秒,
33
- 组件层要起浏览器,还要逐张比 96 张视觉截图。0.11.15 起默认 4 路并发)。
32
+ 全量是 **232 道门禁**(默认 `min(核数, 6)` 路并发,实测 583 秒 / 退出码 0;
33
+ 单道耗时累加 1658 秒,并发效率约 2.8×。组件层要起浏览器,还要逐张比 **109 张**视觉截图 ——
34
+ 47 浅色 + 47 暗色 + 15 RTL,按平台分集,只比本平台那套)。
34
35
  只想确认没改坏静态契约的话用 `gate:fast`,几秒到几分钟。
35
36
 
36
37
  ### 全量为什么**不**在 CI 上跑(这不是缺口,是结论)
@@ -69,7 +70,7 @@ shallow clone ⇒ 依赖 git 状态的判据语义不同;浏览器版本不同
69
70
 
70
71
  **`2` 不是跳过**:已有 9 个门禁用它表示「参数错 / 前置条件不满足」,往 2 上加语义会把真错误
71
72
  静默降级成跳过。**更不要 `return 0` 冒充跳过**——`rc=0` 天然等于通过,那正是假绿
72
- (见 `internal/ERRORS.md` 的 E152 / E153)。有未判定项时,汇总**不会**打印「全部通过」。
73
+ 。有未判定项时,汇总**不会**打印「全部通过」。
73
74
 
74
75
  环境依赖只有一套(Node + Chromium),缺了对应的门禁会**跳过**(跳过不算通过):
75
76
 
@@ -84,8 +85,8 @@ Pillow 只在你想复核那次对齐时才有用,装了不加分、不装也
84
85
  RTL 有**两套**证据,需要分别跑、分别更新:
85
86
 
86
87
  ```bash
87
- node 05-audit/rtl-render.js # 渲染判据(37 页)
88
- python 05-audit/shot-baseline.py --check --dir rtl # 视觉基线(13 页)
88
+ node 05-audit/rtl-render.js # 渲染判据(42 页)
89
+ python 05-audit/shot-baseline.py --check --dir rtl # 视觉基线(15 页,默认已随全量一起拍)
89
90
  ```
90
91
 
91
92
  **视觉基线按平台分集**:目录名是 `_baseline-<平台>[-<scheme>][-rtl]`,
package/README.md CHANGED
@@ -62,7 +62,8 @@ Two more paths that resolve to the same content:
62
62
 
63
63
  ```bash
64
64
  # Pin an exact version and get a tarball byte-identical to the published one
65
- npm install https://github.com/owain323/frontend-lib/releases/download/v0.11.3/agentcss-0.11.3.tgz
65
+ # (把 <版本> 换成 `npm view agentcss version` 的输出 —— 不写死,免得又过期)
66
+ npm install https://github.com/owain323/frontend-lib/releases/download/v<版本>/agentcss-<版本>.tgz
66
67
 
67
68
  # Track master (unreleased commits)
68
69
  npm install github:owain323/frontend-lib
@@ -192,6 +193,8 @@ Three mistakes the template above already avoids:
192
193
  | `separator` | `separator` | Divider, optionally with centred text |
193
194
  | `skeleton` | `skeleton` | Loading placeholder |
194
195
  | `switch` | `switch` | Toggle switch |
196
+ | `timeline` | `timeline` | Ordered event sequence: order is the information, so it is an `ol`; state belongs to `badge`, colour loses meaning here |
197
+ | `upload` | `upload` | File upload: zero-JS native form, plus an enhanced form with drop, multiple files and validation |
195
198
 
196
199
  ### patterns (14)
197
200
 
@@ -217,6 +220,7 @@ Three mistakes the template above already avoids:
217
220
  | Component | Root class | Purpose |
218
221
  |---|---|---|
219
222
  | `analysis-report` | `report` | Research report page: body and side rail side by side, collapsing to one column |
223
+ | `app-shell` | `shell` | App shell: sidebar, top bar, main area; sticky sidebar scrolls itself, folds away on narrow screens |
220
224
  | `detail-page` | `detail-page` | Entity detail skeleton: complete / partly missing / no records, three states sharing one DOM structure |
221
225
  | `report` | `rep` | Report layout system v1: shared base plus three layouts (magazine, scientific, financial research) |
222
226
  | `table` | `table` | Data table: sort / loading / empty / error states, three presets (financial, academic, statistical) |
@@ -263,11 +267,14 @@ not — is in [`docs/STABILITY.md`](docs/STABILITY.md).
263
267
 
264
268
  ## Browser support
265
269
 
266
- [`SUPPORT.md`](SUPPORT.md) lists every engine with the evidence behind its status. Two conventions matter there:
270
+ [`SUPPORT.md`](SUPPORT.md) lists every engine with the evidence behind its status. Three conventions matter there:
267
271
 
268
272
  - **Verified** means an automated check runs **in that engine** and the job reports its own user agent as proof.
273
+ - **Tested once** (`🔍`) means exactly that: one manual run on a real device, with a full UA and a per-capability result, but **no automated check watching it**. A browser update will not turn anything red. It never counts as verified, and the two never add up.
269
274
  - **Not claimed** means exactly that. A row that has not been tested is not written as supported.
270
275
 
276
+ Mobile WebView support is where that distinction bites. Seven entry points have been probed by hand — per-capability results in [`10-review/webview/RESULTS.md`](10-review/webview/RESULTS.md) — while Android System WebView still has **no data at all**. None of the seven was checked for soft-keyboard, safe-area, scroll-bounce or 200 % text behaviour: "15/15 capabilities" is not "the page works on a phone".
277
+
271
278
  ## Theming
272
279
 
273
280
  Themes are switched by a `data-theme` attribute on any container, not only on the root — a dark card can sit inside a light page.