agentcss 0.11.13 → 0.11.16

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/page.css +4 -2
  2. package/02-primitives/badge/demo.html +6 -3
  3. package/02-primitives/choice/demo.html +32 -16
  4. package/02-primitives/combobox/combobox.js +1 -1
  5. package/02-primitives/date-range/date-range.js +1 -1
  6. package/02-primitives/popover/demo.html +1 -1
  7. package/02-primitives/popover/popover.js +1 -1
  8. package/02-primitives/select/select.js +1 -1
  9. package/02-primitives/separator/demo.html +28 -10
  10. package/02-primitives/switch/demo.html +28 -14
  11. package/03-patterns/accordion/accordion.js +25 -5
  12. package/03-patterns/accordion/demo.html +65 -26
  13. package/03-patterns/drawer/drawer.js +1 -1
  14. package/03-patterns/dropdown/dropdown.js +1 -1
  15. package/03-patterns/form-validation/demo.html +2 -3
  16. package/03-patterns/list/flip.js +1 -1
  17. package/03-patterns/nav/nav.js +6 -3
  18. package/03-patterns/nav/toc.js +1 -1
  19. package/03-patterns/overlay/demo.html +1 -2
  20. package/03-patterns/overlay/overlay.js +1 -1
  21. package/03-patterns/pagination/demo.html +1 -1
  22. package/03-patterns/pagination/pagination.js +9 -3
  23. package/03-patterns/reveal/reveal.js +4 -1
  24. package/03-patterns/tabs/demo.html +71 -27
  25. package/03-patterns/tabs/tabs.js +12 -3
  26. package/03-patterns/tooltip/tooltip.js +1 -1
  27. package/03-patterns/tree/tree.js +1 -1
  28. package/04-recipes/report/demo.html +2 -2
  29. package/09-assets/echarts-adapter/demo.html +6 -7
  30. package/09-assets/model-viewer/demo.html +2 -2
  31. package/09-assets/sparkline/demo.html +1 -2
  32. package/API.md +37 -3
  33. package/CHANGELOG.md +309 -0
  34. package/CONTRIBUTING.md +9 -3
  35. package/README.zh-CN.md +1 -1
  36. package/START-HERE.md +5 -5
  37. package/ai/components.json +8 -4
  38. package/ai/deprecations.schema.json +2 -2
  39. package/dist/02-primitives/combobox/combobox.js +1 -1
  40. package/dist/02-primitives/date-range/date-range.js +1 -1
  41. package/dist/02-primitives/popover/popover.js +1 -1
  42. package/dist/02-primitives/select/select.js +1 -1
  43. package/dist/03-patterns/accordion/accordion.js +10 -4
  44. package/dist/03-patterns/drawer/drawer.js +1 -1
  45. package/dist/03-patterns/dropdown/dropdown.js +1 -1
  46. package/dist/03-patterns/list/flip.js +1 -1
  47. package/dist/03-patterns/nav/nav.js +4 -3
  48. package/dist/03-patterns/nav/toc.js +1 -1
  49. package/dist/03-patterns/overlay/overlay.js +1 -1
  50. package/dist/03-patterns/pagination/pagination.js +4 -3
  51. package/dist/03-patterns/reveal/reveal.js +2 -1
  52. package/dist/03-patterns/tabs/tabs.js +8 -3
  53. package/dist/03-patterns/tooltip/tooltip.js +1 -1
  54. package/dist/03-patterns/tree/tree.js +1 -1
  55. package/dist/COSTS.md +10 -10
  56. package/docs/ACCESSIBILITY.md +3 -3
  57. package/docs/DEPRECATIONS.md +128 -0
  58. package/esm/accordion.mjs +7 -0
  59. package/esm/combobox.mjs +7 -0
  60. package/esm/date-range.mjs +7 -0
  61. package/esm/drawer.mjs +7 -0
  62. package/esm/dropdown.mjs +7 -0
  63. package/esm/flip.mjs +7 -0
  64. package/esm/index.mjs +38 -0
  65. package/esm/nav.mjs +7 -0
  66. package/esm/overlay.mjs +7 -0
  67. package/esm/pagination.mjs +7 -0
  68. package/esm/popover.mjs +7 -0
  69. package/esm/reveal.mjs +7 -0
  70. package/esm/select.mjs +7 -0
  71. package/esm/tabs.mjs +7 -0
  72. package/esm/toc.mjs +7 -0
  73. package/esm/tooltip.mjs +7 -0
  74. package/esm/tree.mjs +7 -0
  75. package/index.html +1 -1
  76. package/package.json +10 -3
  77. package/types/index.d.ts +131 -1
package/CHANGELOG.md CHANGED
@@ -16,6 +16,315 @@
16
16
 
17
17
  (无)
18
18
 
19
+ ## [0.11.16] - 2026-10-29
20
+
21
+ **这一版没有新组件,补的是两处「说了但没做到」。**
22
+
23
+ 上一轮自评扣分最重的两条是:**没有对外弃用政策**(版本稳定性)
24
+ 和**无障碍树只验了 11 页**。两条都属于"文档里写得像有,实际上没有"的类型 ——
25
+ 比明说没做更坏,因为读者会按"有"的前提做决策。
26
+
27
+ ### ① 弃用政策:从「一句话承诺」变成「四类名字、双向门禁」
28
+
29
+ 此前 `ai/deprecations.json` 已经存在,但只覆盖**令牌名和类名**。
30
+ 于是「删掉 `Select.open()`」「把 `fl-select` 改名」这两件事
31
+ **不在任何门禁的视野里** —— 而它们比删类名更容易伤到下游:
32
+ 类名删了样式塌了,看得见;方法删了是运行时 `undefined is not a function`,
33
+ 往往在用户点击之后才炸;事件名改了则**完全静默**,订阅方只是再也不被调用。
34
+
35
+ 现在 `kind` 四类,各自的取法都是静态可复算的(本门禁跑在 `MODE=fast` 层,
36
+ 不能为了一份名单去起浏览器):
37
+
38
+ | 类型 | 从哪来 | 抓得到 |
39
+ |---|---|---|
40
+ | `token` | `ai/tokens.json` | 删令牌、改名 |
41
+ | `class` | `ai/components.json` | 删根类 / 变体 / 槽位 |
42
+ | `api` | 源码里的 `global.X =`(大写开头)+ `types/index.d.ts` 的成员 | 删全局、删成员、删参数对象字段 |
43
+ | `event` | `flEmit(el, 'fl-…')` 的字面量 | 改事件名 |
44
+
45
+ 实测(先红后修,不是"看着应该能抓"):
46
+
47
+ | 破坏方式 | 结果 |
48
+ |---|---|
49
+ | 把 `fl-nav-open` 改成 `fl-nav-opened` | 🔴 `fl-nav-open:基线里有、现在没了` |
50
+ | 把 `TocApi.init` 改名 | 🔴 `TocApi.init:基线里有、现在没了` |
51
+
52
+ 过程中自己踩了两个洞,都记进了 `internal/ERRORS.md`:
53
+
54
+ - **E131**:扩了 `KINDS`,但汇总行还写死 `cur_by['token'] | cur_by['class']`
55
+ ⇒ ⑦(未登记的移除)只对前两类算差集,新加的两类删了也不报。
56
+ 门禁跑得更多、看着更忙,覆盖面反而更窄。修法:汇总一律遍历 `KINDS`,
57
+ 且**每一类都要有自己的突变样本**(只打第一类证明不了后面接上了)。
58
+ - **E132**:成员解析正则少一个 `re.M` ⇒ `^` 只匹配 body 开头,
59
+ 而 `^\s+` 会吃掉前导换行,正好命中第一个成员,`AccordionInstance` 只收到 `root`。
60
+
61
+ 对外文档新建 **`docs/DEPRECATIONS.md`**,并与登记册**双向**比对
62
+ (新门禁 `deprecation-doc-gate`):
63
+
64
+ - 登记册有、文档没写 ⇒ **漏告知**(读者以为它不会消失)
65
+ - 文档写了、登记册没有 ⇒ **假承诺**(承诺了一个我们不会兑现的日期)
66
+ - 版本号写错、替代写错、把已移除的名字写进"迁窗中"表 ⇒ 各自有独立判据
67
+
68
+ 顺带纠正了 `API.md` 里两句**说错的话**:原文写「只在**大版本**移除」
69
+ 「弃用项先加警告并保留一个完整版本周期」—— 而本库在 `0.7.x → 0.8.0`
70
+ 就删过名字(`state--delayed`),且组件脚本从不打印运行时警告。
71
+ 把话说满比说清更伤人,已改成与登记册一致的四条。
72
+
73
+ ### ② 无障碍树:11 页 → 全部 38 页,跑出 12 张无名表格
74
+
75
+ `a11y-ax.js` 之前只转录 11 个「交互语义各不相同」的代表页。
76
+ 这个理由经不起一句追问:**没被转录的 27 页,凭什么认为它们没问题?**
77
+ 答案是没有依据 —— 判据只对被转录的页面生效。
78
+
79
+ 扩到全量后,第一次跑就红了 **12 条 `AX-1`,全部是同一类**:
80
+
81
+ ```text
82
+ 🔴 pagination AX-1 [table] 可交互角色没有 accessible name
83
+ 🔴 report AX-1 [table] …(2 张)
84
+ 🔴 echarts-adapter AX-1 [table] …(5 张)
85
+ 🔴 model-viewer AX-1 [table] …(2 张)
86
+ 🔴 composition AX-1 [table] …
87
+ 🔴 popover AX-1 [table] …
88
+ ```
89
+
90
+ 这些是 demo 页里的**数据表**,紧跟在一个可见标题后面 —— 视觉用户读得到上下文,
91
+ 读屏用户读到的是"表格,没有名字"。修法给每张表加 `aria-label`,
92
+ 文字取自它上方那个标题(**不用** `<caption>`:可见 caption 会改变像素,
93
+ 而三套视觉基线要重录,那属于另一件事,不该混在无障碍修复里)。
94
+
95
+ 同时补了一条**覆盖完整性判据**:手写的页面清单必须**等于**磁盘上的全部
96
+ `demo.html`。漏页(新增了 demo 忘了加清单)与幽灵页(清单里的文件不存在)
97
+ 都会直接红 —— 手写清单是「哪些页面要查」的第二份表述,两份必然漂移,
98
+ 这正是 `a11y-scan.js` 在 0.8.9 栽过的同一个坑。判据自己也做了反向控制
99
+ (删一页 / 加幽灵页 / 重复登记,三种都必须红)。
100
+
101
+ ### ③ 门禁 203 → 204
102
+
103
+ 新增 `deprec-doc`,排在 `deprecation` 之后,仍在静态层内(不需要浏览器)。
104
+ 静态层本机约 3 分钟。
105
+
106
+ ### 已知边界
107
+
108
+ - `api` 这一类里,只有 `api-contract-gate` 覆盖的组件是**实测**过
109
+ d.ts ↔ 运行时一致的;其余组件的成员**仅有声明**。
110
+ ⇒ 本判据保证「删声明之前必须登记」,不保证「运行时一定还有它」。
111
+ - `data-*` 属性名、`detail` 字段、令牌默认值、CSS 变量默认值
112
+ **不在**基线里。这是"没做",不是"量不了",已在 `docs/DEPRECATIONS.md`
113
+ 第四节逐条列出。
114
+ - a11y 的人工部分仍然为零:真机读屏(NVDA / VoiceOver)一条记录都没有。
115
+ 协议在 `docs/A11Y-MANUAL-PROTOCOL.md`,表格还是空的。
116
+
117
+ ---
118
+
119
+ ## [0.11.15] - 2026-10-29
120
+
121
+ **「在真实项目里能不能用」这件事,现在有证据了。**
122
+
123
+ 前几版把验证工程做到了很高的水准:174 道门禁、可证伪的反例断言、变异探针。
124
+ 但有一类问题它一直抓不到 —— **这些检查全是对自家 demo 页做的**。
125
+ 一个 React / Next.js 项目把库装进去会怎样,仓库里没有任何东西回答过。
126
+
127
+ 这一版补的是那一半。查的时候发现三个**不会报错**的坑,每个都足以让
128
+ 接入者在第一步就放弃。
129
+
130
+ ### ① SSR 里 `import` 就崩(16/16 全崩)
131
+
132
+ 16 个行为脚本的收尾都是 `(function (global) { … })(window)`。
133
+ 在浏览器里没问题,在 Node / Next.js / Nuxt / Astro 里:
134
+
135
+ ```text
136
+ ReferenceError: window is not defined
137
+ at Object.<anonymous> (03-patterns/accordion/accordion.js:485:3)
138
+ ```
139
+
140
+ 实测记录(修之前,逐个 `require`):
141
+
142
+ | 脚本 | 崩在 |
143
+ |---|---|
144
+ | 11 个 `… })(window)` | IIFE 调用处 |
145
+ | `accordion` | 顶层 `typeof window.requestAnimationFrame`(会**先解引用** window) |
146
+ | `nav` / `tabs` / `pagination` / `reveal` | 顶层 `document.readyState` |
147
+
148
+ 修法:IIFE 收尾统一为
149
+ `})(typeof window !== 'undefined' ? window : globalThis);`,
150
+ 顶层 `document` 访问前加 `if (typeof document === 'undefined') return;`。
151
+ 现在 16/16 在 Node 里 `require` 成功,且 `globalThis.X` 拿到真 API。
152
+
153
+ ### ② 重复初始化会「点了没反应」(静默双触发)
154
+
155
+ `init()` 没有幂等守卫。被调用两次时(SPA 路由切换、HMR、重复引入脚本):
156
+
157
+ - 旧实例的 click 监听**还挂在 root 上**(它的闭包还活着)
158
+ - 新实例又挂一份 ⇒ 一次点击 toggle **两次** ⇒ 展开又收起
159
+
160
+ 肉眼看到的是「按钮点了没反应」,控制台干干净净。
161
+
162
+ 修法两条:`init()` 跳过已初始化的节点;构造器 `if (root.accordionInstance) return root.accordionInstance;`
163
+ (返回对象覆盖 `this` 是 JS 合法行为)。`tabs` 同样处理。
164
+
165
+ ### ③ 有 `.d.ts`,却没有模块入口
166
+
167
+ `types/index.d.ts` 声明了 `export declare const Select …`,
168
+ 而 `package.json` 的 `exports` 里**没有任何路径指向 JS 模块** ——
169
+ `.` 指向 `index.html`,组件只能靠 `<script src>` 挂全局。
170
+ 类型在说谎:`import { Select } from 'agentcss'` 编译通过、运行时 undefined。
171
+
172
+ 修法:
173
+
174
+ - 新增 `esm/`(**生成物**,17 个 `.mjs`:16 个组件 + `index.mjs`),
175
+ 由 `05-audit/gen-esm.py` 从源码的 `global.X = …` 机械生成。
176
+ 内容是「导入副作用 + 把 global 上的对象 re-export」,
177
+ **不复制实现** —— 门禁会断言 `esm` 导出与 `window.X` 是同一个对象。
178
+ - `package.json` 新增 `"./esm"` 与 `"./esm/*"`,`files` 加 `esm/`。
179
+ - `types/index.d.ts` 从 5 个组件补到 **16 个**(新增 Accordion / Tabs / Nav /
180
+ Pagination / Drawer / Overlay / Tooltip / Popover / Toc / Reveal / FLIP),
181
+ 每条都对照过源码的真实赋值与原型方法名;收不窄的参数写 `object` 并注明,
182
+ 不给假签名。
183
+ - `sideEffects` 从 `["*.css", "*.js"]` 改为 `["**/*.css", "**/*.js", "**/*.mjs"]`。
184
+ **这是个真实的打包坑**:只匹配根目录的 glob 会让 bundler 把
185
+ `02-primitives/**/*.js` 当作无副作用模块 tree-shake 掉 —— 行为静默消失。
186
+
187
+ ### ④ 接入样板真的能跑了
188
+
189
+ `examples/react-vite/README.md` 曾写着「这是 T-01 的交付物:证明这个库能被
190
+ 真实项目用上」,但那个目录里只有 `index.html` + `README.md` ——
191
+ 没有 `package.json`、没有构建、从没被执行过。**声称的证明不是证明。**
192
+
193
+ 新增 `examples/esm-native/`:原生 `<script type="module">`,零依赖零构建,
194
+ 由 `05-audit/example-esm-gate.js` 真打开、真点击、真量 computed style。
195
+ 不用 Vite 是因为装依赖的样板在别人机器上会因网络/版本差异跑不起来,
196
+ 「证明」就退化成「我机器上曾经证明过」。
197
+
198
+ ### ⑤ 门禁从 20 分钟降到 5–7 分钟(同一套判据)
199
+
200
+ `check-all.sh` 改为「先入队、后并发执行、按序打印」:
201
+
202
+ - 默认 4 路并发(`--parallel=N` 可调,`--serial` 回到逐条)。
203
+ - 输出**顺序不变**:每条门禁的输出落到临时文件,全部跑完按注册顺序打印。
204
+ 并发最容易毁掉「日志还能不能读」,而失败诊断是门禁存在的一半理由。
205
+ - 会临时改工作区文件的门禁标 `@serial` 独占跑(`selftest-all` / `mutation-leak` /
206
+ `beh-reverse` / `pack-smoke` 等 9 条)。不标就互相污染 —— 实测出现过
207
+ 「6 个自检弄脏工作区」的假红。
208
+ - 实测:4 道最重的(zoom-reflow / rtl-render / beh-reverse / demo-claim)
209
+ 串行 354s → 并发 142s,全部返回 0。
210
+ - 全量实测:**203 道串行等价 1422 秒(23.7 分钟)→ 实际 9 分钟**,202 PASS
211
+ (唯一红的是 `release`,它要求先有 tag)。最慢的五道:`dark-cont` 142s、
212
+ `responsive` 140s、`zoom-reflow` 122s、`rtl-render` 89s、`visual-dark` 83s。
213
+
214
+ ### 门禁 197 → 203
215
+
216
+ | 新增 | 判据 |
217
+ |---|---|
218
+ | `esm-gen` / `esm-gen-rev` | `esm/` 与源码的 `global.X =` 一一对应(生成物不漂移) |
219
+ | `behavior-api` / `-rev` | A 无 DOM 加载 · B 重复构造返回同一实例且单击只切换一次 · C ESM 覆盖 |
220
+ | `example-esm` / `-rev` | 样板页真能被浏览器打开、import 成功、行为与样式都生效 |
221
+
222
+ 另外两处判据升级:
223
+
224
+ - `wiring` 现在认识 ESM:`import … from '../../esm/x.mjs'` 算作「引用了行为脚本」。
225
+ 之前它只会看 `<script src>`,一个正确使用 ESM 的页面会被判「用了标记但没引脚本」
226
+ (自检加了 ⑥/⑥b 两条反向控制)。
227
+ - `tsc-gate` 反例新增 4 条(无参 toggle / 字符串喂 pagesOf / attach 返回值当句柄 /
228
+ Nav 的静态 open),断言从 8 条增至 12 条。
229
+
230
+ ### 已知边界(如实写下来)
231
+
232
+ - `esm/` 只覆盖 `02-primitives` 与 `03-patterns` 的行为脚本。
233
+ `09-assets` 的绘图脚本(Chart / Bar / ScientificPlot)与
234
+ `01-tokens/theme-toggle.js` 仍是 script 形态 —— 它们一个文件挂多个全局,
235
+ 与「一文件一 API」的生成规则不同,硬套会生成名字打架的入口。
236
+ - `Pagination.create()` 的实例成员未在类型层收窄(源码只挂了内部状态),
237
+ 需要时读 `03-patterns/pagination/README.md`。
238
+ - 真机读屏(NVDA / VoiceOver)仍需人工执行,`docs/A11Y-MANUAL-PROTOCOL.md` 的表格仍是空的。
239
+
240
+ ## [0.11.14] - 2026-10-29
241
+
242
+ **「键盘到不了」这句话,现在能量了。**
243
+
244
+ 0.11.13 结束时,10 个页面仍是单栏反例。其中 5 页的登记里写着一句诚实话:
245
+ 「两块都可量,但这一页仍是单栏结构,升级成 cmp 要重排整页 —— 本轮没做,
246
+ 已知欠账」。这一版把那 5 页还掉了,并且在做的时候发现:**那句话有一半不准**
247
+ —— 那些"可量"的差别是「按 Tab 能不能停上去」,而当时的指标表里
248
+ 根本没有能量它的东西。剩下的只有高度、宽度、字号这些代理量,
249
+ 拿来判"键盘到不了"是**间接**的(高度 0 暗示焦点没了,但那只是暗示)。
250
+
251
+ ### 新指标 `focus`
252
+
253
+ `data-bad="input|focus|=0"` —— 数的是**真的能 Tab 到**的元素个数。
254
+ 实现是「`focus()` 成功 **且** `tabIndex >= 0`」,两个信号缺一不可,
255
+ 两种错都实测到了:
256
+
257
+ | 元素 | tabIndex | 聚焦得成 | Tab 到得了 |
258
+ |---|---|---|---|
259
+ | `a[href]` | 0 | ✅ | ✅ |
260
+ | `a`(没有 href) | 0 | ❌ | ❌ ← tabIndex 会撒谎 |
261
+ | `input[disabled]` | 0 | ❌ | ❌ ← tabIndex 会撒谎 |
262
+ | `div[tabindex="-1"]` | -1 | ✅ | ❌ ← 能聚焦 ≠ 能 Tab 到 |
263
+
264
+ 第二条是这一版第一次跑就踩到的:只判 `tabIndex >= 0`,把没有 href 的 `<a>`
265
+ 算成可聚焦(自检当场红了);只判 `focus()`,把 roving tabindex 里
266
+ `-1` 的那几项算成能 Tab 到(tabs 的正例量出 3,应该是 1)。
267
+ 两条都留在自检里,各占一条用例。
268
+
269
+ ### 7 个块从单栏改成成对
270
+
271
+ | 页面 | 反例栏 | 正例栏 | 量的 |
272
+ |---|---|---|---|
273
+ | `choice` | `display:none` 的 input | `opacity:0` + 绝对定位 | `focus` 0 vs 1 |
274
+ | `separator` | `div` + border-top | `<hr>` | `hr` 个数 0 vs 1 |
275
+ | `switch` | `div role="switch"` | 原生 checkbox | `input` 个数 0 vs 1 |
276
+ | `accordion` | `div` 当 trigger | heading 包 button | `button` 个数 0 vs 1 |
277
+ | `accordion` | `max-height:0` | `hidden` | 链接 `focus` 1 vs 0 |
278
+ | `tabs` | `div role="tab"` | `button role="tab"` | `focus` 0 vs 2 |
279
+ | `tabs` | 三个 `tabindex="0"` | roving(只有一个 0) | `focus` 3 vs 1 |
280
+
281
+ ### 已知冲突:可点的徽标过不了自己的命中区门禁
282
+
283
+ `badge` 本来也要改成成对(`div` 装可点 vs `a` 装可点),做到一半被
284
+ `badge` 组件门禁拦住了:
285
+
286
+ ```
287
+ FAIL ③ 命中区全部 ≥44px:A.badge badge--inf 视觉 54×24 命中 54×24
288
+ ```
289
+
290
+ 这不是演示写错 —— **是组件自身的矛盾**:README 说「要能点就用 button/a,
291
+ 样式照抄 `.badge`」,而徽标本体只有 24px 高,换上正确元素之后
292
+ 命中区就不达标(WCAG 2.5.5 要 44px)。也就是说,
293
+ **本库目前没有任何一种写法能同时满足「可点的徽标」和「44px 命中区」**。
294
+
295
+ 这一版**没有**用内联样式把演示撑到 44px 蒙过门禁 —— 那是拿样式骗判据。
296
+ 徽标照旧是单栏,块上写明"配不成对的原因不是量不了,是组件还没解决"。
297
+ 真要修得动 `.badge--clickable` 的垂直内边距(或干脆在文档里禁止可点徽标),
298
+ 那是破坏性视觉变更,单独一版做。
299
+
300
+ ### 判据改成**按块**登记(原来按页)
301
+
302
+ 单栏块的理由从页面注释 `<!-- demo-claim:skip -->` 搬到块上
303
+ `data-claim-skip="理由"`。原因不是审美,是这一版恰好会造出"混合页":
304
+
305
+ ```
306
+ separator:一块改成了 cmp(hr vs div),另一块(对比度)改不了
307
+ switch :一块改成了 cmp(div vs 原生),另一块(label)改不了
308
+ ```
309
+
310
+ 按页判的话,只要加**一个** `cmp` 块,同页剩下的单栏反例就全部"洗白"了 ——
311
+ 门禁不看了,也不需要理由。所以先把这个洞堵上再改内容,否则改完就是假绿。
312
+ 页面级注释现在会**报错**,不忽略:两套机制并存比没有机制更坏。
313
+
314
+ 顺带:写了 `data-prose`(连标本都摆不出来)的块,不再要求写
315
+ `data-claim-skip` —— 一条声明只覆盖一个问题,别让人把同一句话说两遍。
316
+
317
+ ### 覆盖
318
+
319
+ - 对照块 **24 → 31**(30 页),每块两栏互斥且各自成立
320
+ - 单栏块登记 **9 个**(逐块,理由是"视觉上必然相同"或"差别是时序/措辞/对比度")
321
+ - `data-prose` 声明 3 个(不变)
322
+ - `demo-claim` 自检 **32 → 48 条**(29 必须红 + 19 必须绿)
323
+
324
+ 判据**证不了**的那些也没藏着:`switch` 的「label 被藏掉」两栏视觉必然相同,
325
+ 差别只在无障碍树里 —— 那一块登记为"写不出互斥断言",证据指向
326
+ `10-review/a11y-tree/switch.txt`,不是"看着差不多就算了"。
327
+
19
328
  ## [0.11.13] - 2026-10-29
20
329
 
21
330
  **先得有东西可看,才谈得上「两栏不一样」。**
package/CONTRIBUTING.md CHANGED
@@ -8,7 +8,8 @@ npm run gate # 全量:含浏览器类
8
8
  npm run gate:fast # 只跑静态层,不用装浏览器
9
9
  ```
10
10
 
11
- 全量是 **197 道门禁**(本机约 15–20 分钟:组件层要起浏览器,还要逐张比 95 张视觉截图)。
11
+ 全量是 **204 道门禁**(本机约 5–7 分钟:组件层要起浏览器,还要逐张比 96 张视觉截图;
12
+ 0.11.15 起默认 4 路并发执行,之前串行要 15–20 分钟)。
12
13
  只想确认没改坏静态契约的话用 `gate:fast`,几秒到几分钟。
13
14
 
14
15
  环境依赖只有一套(Node + Chromium),缺了对应的门禁会**跳过**(跳过不算通过):
@@ -129,10 +130,15 @@ fix: <一句话说清改了什么、为什么>
129
130
  摆不出可见标本的(运行时行为、读屏输出、读者要自己按 Tab 才能发现、
130
131
  页面上没有图表实例),写在块上:
131
132
  `data-prose="为什么摆不出来"`。理由至少 8 个字,会被逐条打印出来,可以被反驳。
132
- 只有 `box bad` 单栏、没有 `cmp` 结构的页,还要在页面里写
133
- `<!-- demo-claim:skip 为什么没有成对断言 -->`。
133
+ 只有 `box bad` 单栏、配不出 `cmp` 结构的块,要在**块上**写
134
+ `data-claim-skip="为什么写不出互斥断言"`。
134
135
  **理由要区分"量不了"和"可量但没做"**:后者是欠账,写明白,别混进前者
135
136
  —— 一句"量化不了"掩盖一处没做的活,比不写更糟。
137
+ 9. **能配成对的,必须配成对。** `demo-claim` 里有一个指标(`focus`:
138
+ "按 Tab 能不能停上去")就是为"键盘到不了"这类反例加的 ——
139
+ `choice` / `switch` / `accordion` / `tabs` 四页的单栏反例,
140
+ 0.11.14 全部改成了成对结构,因为它们**本来就可量**,差的只是有人去重排。
141
+ ⇒ 写"量不了"之前先问一句:**是新指标的事,还是我没做?**
136
142
 
137
143
  ## 提 PR 之前
138
144
 
package/README.zh-CN.md CHANGED
@@ -343,7 +343,7 @@ import 'agentcss/dist/02-primitives/button/button.css';
343
343
  | `ai/` | **机器可读契约**:`components.json` / `tokens.json` / schema / 校验器 / 命令行 |
344
344
  | `adapters/` | 场景适配层(如演示文稿),**不进核心** |
345
345
  | `benchmark/` | 兼容性基准(10 类编辑意图,当前无模型实测记录) |
346
- | `docs/` | `INVARIANT.md`(不变式)· `CONVENTIONS.md`(全局约定)· `ACCESSIBILITY.md`(无障碍)· `AI-CONTRACT.md`(与 AI 协作的契约)· `LAYER-BASELINE.md`(引擎能力基线) |
346
+ | `docs/` | `INVARIANT.md`(不变式)· `CONVENTIONS.md`(全局约定)· `ACCESSIBILITY.md`(无障碍)· `DEPRECATIONS.md`(弃用与移除)· `AI-CONTRACT.md`(与 AI 协作的契约)· `LAYER-BASELINE.md`(引擎能力基线) |
347
347
  | `types/` | TypeScript 类型定义(描述 `window` 上的全局对象) |
348
348
 
349
349
  ## 示例与验证状态
package/START-HERE.md CHANGED
@@ -47,20 +47,20 @@
47
47
  | 进度条 / 环 | `02-primitives/progress/progress.css` | 172 | — | 有耗时过程要给进度就要 |
48
48
  | 浮层气泡 | `02-primitives/popover/popover.css` | 187 | **必需 331** (`popover.js`) | 要一段信息贴着触发元素浮出来就要 |
49
49
  | 日期区间 | `02-primitives/date-range/date-range.css` | 206 | **必需 301** (`date-range.js`) | 要选起止日期就要 |
50
- | 标签页 | `03-patterns/tabs/tabs.css` | 172 | **必需 307** (`tabs.js`) | 同层级内容切换就要 |
51
- | 折叠面板(beta) | `03-patterns/accordion/accordion.css` | 453 | **必需 369** (`accordion.js`) | 长表单分段就要 |
50
+ | 标签页 | `03-patterns/tabs/tabs.css` | 172 | **必需 316** (`tabs.js`) | 同层级内容切换就要 |
51
+ | 折叠面板(beta) | `03-patterns/accordion/accordion.css` | 453 | **必需 389** (`accordion.js`) | 长表单分段就要 |
52
52
  | 动作菜单(beta) | `03-patterns/dropdown/dropdown.css` | 135 | **必需 451** (`dropdown.js`) | 要一组动作(新建 / 导出 / 删除)就要 |
53
53
  | 文字提示 | `03-patterns/tooltip/tooltip.css` | 89 | **必需 288** (`tooltip.js`) | 图标按钮或缩写需要一句话解释就要 |
54
- | 分页 | `03-patterns/pagination/pagination.css` | 195 | **必需 395** (`pagination.js`) | 数据多到要翻页就要 |
54
+ | 分页 | `03-patterns/pagination/pagination.css` | 195 | **必需 401** (`pagination.js`) | 数据多到要翻页就要 |
55
55
  | 树形 | `03-patterns/tree/tree.css` | 122 | **必需 464** (`tree.js`) | 层级数据(文件 / 组织 / 分类)要展开就要 |
56
56
  | 侧边抽屉(beta) | `03-patterns/drawer/drawer.css` | 129 | **必需 258** (`drawer.js`) | 要从侧边滑出面板就要 |
57
57
  | 弹窗 / 吐司(beta) | `03-patterns/overlay/overlay.css` | 319 | **必需 563** (`overlay.js`) | 需要弹层(吐司 / 模态)就要 |
58
58
  | 状态(空 / 加载 / 错 / 成功) | `03-patterns/states/states.css` | 252 | — | **凡是会异步取数就要** |
59
59
  | 表单校验 | `03-patterns/form-validation/form-validation.css` | 263 | — | 提交前要校验就要 |
60
60
  | 列表增删 | `03-patterns/list/list.css` | 162 | 可选 201 (`flip.js`,FLIP 增删动画,纯增强) | 列表会动态增删就要 |
61
- | 导航 / 目录 | `03-patterns/nav/nav.css` | 314 | **必需 430** (`nav.js`) · 可选 111 (`toc.js`,自动生成目录,纯增强) | 顶部要导航,或长文要目录就要 |
61
+ | 导航 / 目录 | `03-patterns/nav/nav.css` | 314 | **必需 433** (`nav.js`) · 可选 111 (`toc.js`,自动生成目录,纯增强) | 顶部要导航,或长文要目录就要 |
62
62
  | 长文排版 | `03-patterns/content/content.css` | 286 | — | 写文档 / 长文就要 |
63
- | reveal | `03-patterns/reveal/reveal.css` | 93 | 可选 172 (`reveal.js`,入场动效,纯增强;没有它内容直接可见) | |
63
+ | reveal | `03-patterns/reveal/reveal.css` | 93 | 可选 175 (`reveal.js`,入场动效,纯增强;没有它内容直接可见) | |
64
64
  | analysis-report | `04-recipes/analysis-report/analysis-report.css` | 187 | — | |
65
65
  | detail-page | `04-recipes/detail-page/detail-page.css` | 190 | — | |
66
66
  | report | `04-recipes/report/report.css` | 364 | — | |
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
3
  "generatedBy": "05-audit/gen-ai-contract.py",
4
- "libraryVersion": "0.11.13",
4
+ "libraryVersion": "0.11.16",
5
5
  "$comment": "机器可读的组件契约。slots = BEM 元素名(在本库里元素名就是插槽名),states.enum = 组件会在 DOM 上写出的 data-* 取值,states.legacyClass = 仍未收敛到 data 属性的历史状态类(INVARIANT I-8 未达标)。",
6
6
  "maturityCriteria": {
7
7
  "alpha": "缺 demo.html 或缺 05-audit 下的专属门禁 ⇒ 还没被机器验证过,不要用",
@@ -125,7 +125,9 @@
125
125
  },
126
126
  "aria": {
127
127
  "roles": [],
128
- "attrs": []
128
+ "attrs": [
129
+ "label"
130
+ ]
129
131
  },
130
132
  "usesOtherNamespaces": [],
131
133
  "global": "ModelViewer",
@@ -1455,7 +1457,8 @@
1455
1457
  "main"
1456
1458
  ],
1457
1459
  "attrs": [
1458
- "hidden"
1460
+ "hidden",
1461
+ "label"
1459
1462
  ]
1460
1463
  },
1461
1464
  "usesOtherNamespaces": [],
@@ -1899,7 +1902,8 @@
1899
1902
  "aria": {
1900
1903
  "roles": [],
1901
1904
  "attrs": [
1902
- "hidden"
1905
+ "hidden",
1906
+ "label"
1903
1907
  ]
1904
1908
  },
1905
1909
  "usesOtherNamespaces": [],
@@ -39,7 +39,7 @@
39
39
  "required": ["id", "kind", "name", "deprecatedIn", "removeIn", "reason"],
40
40
  "properties": {
41
41
  "id": { "type": "string", "minLength": 1 },
42
- "kind": { "enum": ["token", "class"] },
42
+ "kind": { "enum": ["token", "class", "api", "event"] },
43
43
  "name": { "type": "string", "minLength": 1 },
44
44
  "deprecatedIn": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" },
45
45
  "removeIn": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" },
@@ -61,7 +61,7 @@
61
61
  "type": "object",
62
62
  "required": ["kind", "name", "removedIn"],
63
63
  "properties": {
64
- "kind": { "enum": ["token", "class"] },
64
+ "kind": { "enum": ["token", "class", "api", "event"] },
65
65
  "name": { "type": "string", "minLength": 1 },
66
66
  "removedIn": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" },
67
67
  "replacement": {
@@ -436,4 +436,4 @@ return String(s).replace(/[&<>"]/g, function (c) {
436
436
  }
437
437
 
438
438
  global.Combobox = { create: create };
439
- })(window);
439
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -264,4 +264,4 @@ create: create,
264
264
  iso: iso, addDays: addDays, addMonths: addMonths,
265
265
  quarterOf: quarterOf, quarterRange: quarterRange,
266
266
  };
267
- })(window);
267
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -254,4 +254,4 @@ get current() { return current; },
254
254
  };
255
255
 
256
256
  global.Popover = Popover;
257
- })(window);
257
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -446,4 +446,4 @@ set value(v) { selected = v == null ? null : String(v); paint(); },
446
446
  }
447
447
 
448
448
  global.Select = { create: create };
449
- })(window);
449
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -1,4 +1,4 @@
1
- (function () {
1
+ (function (global) {
2
2
  'use strict';
3
3
 
4
4
  var uid = 0;
@@ -28,7 +28,8 @@ return ev;
28
28
 
29
29
  function toArray(x) { return Array.prototype.slice.call(x); }
30
30
 
31
- var raf = (typeof window.requestAnimationFrame === 'function')
31
+ var raf = (typeof window !== 'undefined' &&
32
+ typeof window.requestAnimationFrame === 'function')
32
33
  ? window.requestAnimationFrame.bind(window)
33
34
  : function (fn) { return setTimeout(fn, 16); };
34
35
 
@@ -51,6 +52,8 @@ return null;
51
52
  }
52
53
 
53
54
  function Accordion(root, opts) {
55
+
56
+ if (root && root.accordionInstance) return root.accordionInstance;
54
57
  this.root = root;
55
58
  this.opts = opts || {};
56
59
  this.items = toArray(root.querySelectorAll('.accordion__item'));
@@ -202,18 +205,21 @@ if (item) self.toggle(item);
202
205
 
203
206
  };
204
207
 
205
- window.Accordion = Accordion;
208
+ global.Accordion = Accordion;
206
209
 
207
210
  function init() {
208
211
  var nodes = document.querySelectorAll('[data-accordion]');
209
212
  for (var i = 0; i < nodes.length; i++) {
213
+
214
+ if (nodes[i].accordionInstance) continue;
210
215
  nodes[i].accordionInstance = new Accordion(nodes[i]);
211
216
  }
212
217
  }
213
218
 
219
+ if (typeof document === 'undefined') return;
214
220
  if (document.readyState === 'loading') {
215
221
  document.addEventListener('DOMContentLoaded', init);
216
222
  } else {
217
223
  init();
218
224
  }
219
- })();
225
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -226,4 +226,4 @@ return { el: d, close: close };
226
226
  }
227
227
 
228
228
  global.Drawer = { open: open };
229
- })(window);
229
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -362,4 +362,4 @@ destroy: function () { roving.destroy(); dismiss.destroy(); menu.hidden = true;
362
362
  }
363
363
 
364
364
  global.Dropdown = { create: create };
365
- })(window);
365
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -114,4 +114,4 @@ if (done) { done(); }
114
114
 
115
115
  global.FLIP = { flip: flip, remove: removeWithAnimation };
116
116
 
117
- })(window);
117
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -1,4 +1,4 @@
1
- (function () {
1
+ (function (global) {
2
2
  'use strict';
3
3
 
4
4
  var flEmit = function (el, name, detail) {
@@ -306,7 +306,7 @@ root.navInstance = new Nav(root, opts);
306
306
  return root.navInstance;
307
307
  };
308
308
 
309
- window.Nav = Nav;
309
+ global.Nav = Nav;
310
310
 
311
311
  function init() {
312
312
  each(document.querySelectorAll('[data-nav]'), function (el) {
@@ -314,9 +314,10 @@ if (!el.navInstance) Nav.attach(el, {});
314
314
  });
315
315
  }
316
316
 
317
+ if (typeof document === 'undefined') return;
317
318
  if (document.readyState === 'loading') {
318
319
  document.addEventListener('DOMContentLoaded', init);
319
320
  } else {
320
321
  init();
321
322
  }
322
- })();
323
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -73,4 +73,4 @@ global.removeEventListener('hashchange', update);
73
73
 
74
74
  global.Toc = { init: init };
75
75
 
76
- })(window);
76
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -395,4 +395,4 @@ return { el: box, close: close };
395
395
 
396
396
  global.Overlay = { toast: toast, dialog: dialog };
397
397
 
398
- })(window);
398
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -1,4 +1,4 @@
1
- (function () {
1
+ (function (global) {
2
2
  'use strict';
3
3
 
4
4
  var flEmit = function (el, name, detail) {
@@ -263,11 +263,12 @@ return Math.max(1, Math.ceil((total || 1) / (pageSize || 20)));
263
263
  },
264
264
  };
265
265
 
266
- if (typeof window !== 'undefined') window.Pagination = API;
266
+ global.Pagination = API;
267
267
 
268
+ if (typeof document === 'undefined') return;
268
269
  if (document.readyState === 'loading') {
269
270
  document.addEventListener('DOMContentLoaded', function () { API.update(); });
270
271
  } else {
271
272
  API.update();
272
273
  }
273
- })();
274
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -130,9 +130,10 @@ enable();
130
130
  }
131
131
  }
132
132
 
133
+ if (typeof document === 'undefined') return;
133
134
  if (document.readyState === 'loading') {
134
135
  document.addEventListener('DOMContentLoaded', auto, false);
135
136
  } else {
136
137
  auto();
137
138
  }
138
- }(this));
139
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -1,4 +1,4 @@
1
- (function () {
1
+ (function (global) {
2
2
  'use strict';
3
3
 
4
4
  var flEmit = function (el, name, detail) {
@@ -125,6 +125,8 @@ return out;
125
125
  }
126
126
 
127
127
  function Tabs(root, opts) {
128
+
129
+ if (root && root.tabsInstance) return root.tabsInstance;
128
130
  this.root = root;
129
131
  this.opts = opts || {};
130
132
  this.list = root.querySelector('[role="tablist"]');
@@ -225,18 +227,21 @@ self.select(tab, false);
225
227
  });
226
228
  };
227
229
 
228
- window.Tabs = Tabs;
230
+ global.Tabs = Tabs;
229
231
 
230
232
  function init() {
231
233
  var nodes = document.querySelectorAll('[data-tabs]');
232
234
  for (var i = 0; i < nodes.length; i++) {
235
+
236
+ if (nodes[i].tabsInstance) continue;
233
237
  nodes[i].tabsInstance = new Tabs(nodes[i], {});
234
238
  }
235
239
  }
236
240
 
241
+ if (typeof document === 'undefined') return;
237
242
  if (document.readyState === 'loading') {
238
243
  document.addEventListener('DOMContentLoaded', init);
239
244
  } else {
240
245
  init();
241
246
  }
242
- })();
247
+ })(typeof window !== 'undefined' ? window : globalThis);