agentcss 0.11.14 → 0.12.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 (88) hide show
  1. package/01-tokens/README.md +54 -3
  2. package/01-tokens/market.css +48 -0
  3. package/01-tokens/tokens.css +66 -0
  4. package/02-primitives/combobox/combobox.js +1 -1
  5. package/02-primitives/date-range/date-range.js +1 -1
  6. package/02-primitives/delta/README.md +92 -0
  7. package/02-primitives/delta/delta.css +113 -0
  8. package/02-primitives/delta/demo.html +162 -0
  9. package/02-primitives/metric/README.md +93 -0
  10. package/02-primitives/metric/demo.html +176 -0
  11. package/02-primitives/metric/metric.css +157 -0
  12. package/02-primitives/popover/demo.html +1 -1
  13. package/02-primitives/popover/popover.js +1 -1
  14. package/02-primitives/select/select.js +1 -1
  15. package/03-patterns/accordion/accordion.js +25 -5
  16. package/03-patterns/drawer/drawer.js +1 -1
  17. package/03-patterns/dropdown/dropdown.js +1 -1
  18. package/03-patterns/list/flip.js +1 -1
  19. package/03-patterns/nav/nav.js +6 -3
  20. package/03-patterns/nav/toc.js +1 -1
  21. package/03-patterns/overlay/overlay.js +1 -1
  22. package/03-patterns/pagination/demo.html +1 -1
  23. package/03-patterns/pagination/pagination.js +9 -3
  24. package/03-patterns/reveal/reveal.js +4 -1
  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/04-recipes/tierA-tokens.css +12 -6
  30. package/09-assets/echarts-adapter/demo.html +5 -5
  31. package/09-assets/model-viewer/demo.html +2 -2
  32. package/API.md +52 -3
  33. package/CHANGELOG.md +356 -0
  34. package/CONTRIBUTING.md +2 -1
  35. package/README.zh-CN.md +9 -7
  36. package/START-HERE.md +7 -5
  37. package/ai/START-HERE.md +1 -1
  38. package/ai/components.json +103 -8
  39. package/ai/components.meta.json +8 -0
  40. package/ai/deprecations.schema.json +2 -2
  41. package/ai/tokens.audience.json +3 -2
  42. package/ai/tokens.json +21 -2
  43. package/ai/tokens.tree.json +20 -2
  44. package/dist/01-tokens/market.css +1 -0
  45. package/dist/01-tokens/tokens.css +1 -1
  46. package/dist/02-primitives/combobox/combobox.js +1 -1
  47. package/dist/02-primitives/date-range/date-range.js +1 -1
  48. package/dist/02-primitives/delta/delta.css +1 -0
  49. package/dist/02-primitives/metric/metric.css +1 -0
  50. package/dist/02-primitives/popover/popover.js +1 -1
  51. package/dist/02-primitives/select/select.js +1 -1
  52. package/dist/03-patterns/accordion/accordion.js +10 -4
  53. package/dist/03-patterns/drawer/drawer.js +1 -1
  54. package/dist/03-patterns/dropdown/dropdown.js +1 -1
  55. package/dist/03-patterns/list/flip.js +1 -1
  56. package/dist/03-patterns/nav/nav.js +4 -3
  57. package/dist/03-patterns/nav/toc.js +1 -1
  58. package/dist/03-patterns/overlay/overlay.js +1 -1
  59. package/dist/03-patterns/pagination/pagination.js +4 -3
  60. package/dist/03-patterns/reveal/reveal.js +2 -1
  61. package/dist/03-patterns/tabs/tabs.js +8 -3
  62. package/dist/03-patterns/tooltip/tooltip.js +1 -1
  63. package/dist/03-patterns/tree/tree.js +1 -1
  64. package/dist/04-recipes/tierA-tokens.css +1 -1
  65. package/dist/COSTS.md +35 -33
  66. package/docs/ACCESSIBILITY.md +3 -3
  67. package/docs/CONVENTIONS.md +28 -2
  68. package/docs/DEPRECATIONS.md +128 -0
  69. package/esm/accordion.mjs +7 -0
  70. package/esm/combobox.mjs +7 -0
  71. package/esm/date-range.mjs +7 -0
  72. package/esm/drawer.mjs +7 -0
  73. package/esm/dropdown.mjs +7 -0
  74. package/esm/flip.mjs +7 -0
  75. package/esm/index.mjs +38 -0
  76. package/esm/nav.mjs +7 -0
  77. package/esm/overlay.mjs +7 -0
  78. package/esm/pagination.mjs +7 -0
  79. package/esm/popover.mjs +7 -0
  80. package/esm/reveal.mjs +7 -0
  81. package/esm/select.mjs +7 -0
  82. package/esm/tabs.mjs +7 -0
  83. package/esm/toc.mjs +7 -0
  84. package/esm/tooltip.mjs +7 -0
  85. package/esm/tree.mjs +7 -0
  86. package/index.html +4 -2
  87. package/package.json +10 -3
  88. package/types/index.d.ts +131 -1
@@ -26,7 +26,7 @@
26
26
  * 只在初始化时设一次是最常见的 bug:用户激活第 3 个 tab 后,
27
27
  * Tab 键还是会回到第 1 个。
28
28
  */
29
- (function () {
29
+ (function (global) {
30
30
  'use strict';
31
31
 
32
32
  /* ==== BEHAVIOR INJECT BEGIN: emit ==== */
@@ -165,6 +165,9 @@
165
165
  }
166
166
 
167
167
  function Tabs(root, opts) {
168
+ /* 🔴 同一 root 只允许一个实例 —— 理由同 accordion.js。
169
+ 重复构造会让 keydown 监听叠两份:方向键按一次跳两格。 */
170
+ if (root && root.tabsInstance) return root.tabsInstance;
168
171
  this.root = root;
169
172
  this.opts = opts || {};
170
173
  this.list = root.querySelector('[role="tablist"]');
@@ -281,7 +284,7 @@
281
284
  };
282
285
 
283
286
  // 暴露
284
- window.Tabs = Tabs;
287
+ global.Tabs = Tabs;
285
288
 
286
289
  // 自动初始化:<div class="tabs" data-tabs>
287
290
  //
@@ -294,14 +297,20 @@
294
297
  function init() {
295
298
  var nodes = document.querySelectorAll('[data-tabs]');
296
299
  for (var i = 0; i < nodes.length; i++) {
300
+ /* 🔴 幂等:同 accordion.js 里的理由。重复 new 会让 keydown / click
301
+ 监听叠在同一个 root 上 ⇒ 方向键按一次跳两格。 */
302
+ if (nodes[i].tabsInstance) continue;
297
303
  nodes[i].tabsInstance = new Tabs(nodes[i], {});
298
304
  }
299
305
  }
300
306
 
301
307
  // @autoinit: [data-tabs] —— 扫属性自动建实例(详见 API.md「自动初始化」)
308
+ // ⚠️ 无 DOM 环境(SSR / 构建期 / Node 里 import)不自动初始化。
309
+ // 之前这里直接摸 document ⇒ Next.js 等 SSR 里 import 即 ReferenceError。
310
+ if (typeof document === 'undefined') return;
302
311
  if (document.readyState === 'loading') {
303
312
  document.addEventListener('DOMContentLoaded', init);
304
313
  } else {
305
314
  init();
306
315
  }
307
- })();
316
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -285,4 +285,4 @@ var flDismissable = function (opts) {
285
285
  }
286
286
 
287
287
  global.Tooltip = { attach: attach };
288
- })(window);
288
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -461,4 +461,4 @@
461
461
  }
462
462
 
463
463
  global.Tree = { create: create };
464
- })(window);
464
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -181,7 +181,7 @@
181
181
  写死数字的话,插入一张新图后面全会错位。
182
182
  同一句标记放在 financial 版里不会出编号 —— 那是 profile 决定的,不是标记决定的。 -->
183
183
  <figure class="rep__fig">
184
- <div class="table"><table>
184
+ <div class="table"><table aria-label="关键读数:包覆方案与保持率">
185
185
  <thead><tr>
186
186
  <th scope="col">方案</th>
187
187
  <th scope="col">保持率</th>
@@ -248,7 +248,7 @@
248
248
  <h3>关键假设</h3>
249
249
  <!-- 同一句标记放在 finance 版:没有编号,因为 financial profile 不开 counter -->
250
250
  <figure class="rep__fig">
251
- <div class="table"><table>
251
+ <div class="table"><table aria-label="关键假设:三种情形下的良率与毛利率">
252
252
  <thead><tr>
253
253
  <th scope="col">情形</th>
254
254
  <th scope="col">良率</th>
@@ -1,15 +1,16 @@
1
1
  /* ============================================================================
2
- 精简档 专用:内联用令牌(某项目 / 单文件)
2
+ 精简档:内联用令牌(单文件 / 无构建项目)
3
3
  ============================================================================
4
4
  本文件由 `python 05-audit/make-tiersnippet.py` 从 01-tokens/tokens.css
5
5
  **自动抽取**,请不要手改 —— 改源头,然后重新生成。
6
6
 
7
- 🔴 它为什么存在:
8
- "精简档只需复制 tokens.css 的 [核心] 段",
9
- 但**那个区块从来不存在**。复用的人照着做只会找不到。
10
- 这个文件就是那个"核心",真的、能复制、能用。
7
+ 用法:整段复制进 `<style>`,零外链、零构建。
11
8
 
12
- 体积:浅色 102 个令牌 + 暗色 33 个,零外链零构建。
9
+ 它只含**中间层语义令牌**(L1 原语已内联成具体值),所以体积小、能直接改;
10
+ 但它**不含** `market.css` 的涨跌口径切换 —— 需要国际口径时见
11
+ `01-tokens/README.md` 的「涨跌口径」一节。
12
+
13
+ 体积:浅色 104 个令牌 + 暗色 35 个,零外链零构建。
13
14
  --------------------------------------------------------------------- */
14
15
 
15
16
  :root {
@@ -68,6 +69,9 @@
68
69
  --warning-bg: #fbf1dd;
69
70
  --info-bg: #e8f1f8;
70
71
 
72
+ --dir-up: #c0392b;
73
+ --dir-down: #2f6b3a;
74
+
71
75
  --accent: #1b4d8f;
72
76
  --switch-on: #1b4d8f;
73
77
  --accent-hover: #163a6d;
@@ -169,6 +173,8 @@
169
173
  --danger-bg: #341D1A;
170
174
  --success: #6FAA7A;
171
175
  --success-bg: #16241A;
176
+ --dir-up: #E07A6E;
177
+ --dir-down: #6FAA7A;
172
178
  --warning: #C99A3F;
173
179
 
174
180
  --warning-bg: #242A31;
@@ -44,7 +44,7 @@ td.num { text-align: right; font-family: var(--mono); font-variant-numeric: tabu
44
44
  </p>
45
45
 
46
46
  <h2 class="rule-top">0 · 边界:什么该用本库,什么该用 ECharts</h2>
47
- <table>
47
+ <table aria-label="能力边界:什么场景用本库、什么用 ECharts">
48
48
  <tr><th>场景</th><th>用什么</th><th>谁维护</th></tr>
49
49
  <tr><td>列表右侧的<strong>迷你趋势</strong>(看走势)</td>
50
50
  <td class="ok">本库 <code>sparkline</code>(1.5KB,零依赖)</td><td>我们</td></tr>
@@ -94,7 +94,7 @@ td.num { text-align: right; font-family: var(--mono); font-variant-numeric: tabu
94
94
  <div class="flex">
95
95
  <div class="box">
96
96
  <h3 style="margin-top:0">✅ 规范:柱状图(分类轴从 0)</h3>
97
- <table>
97
+ <table aria-label="示例数据:柱状图的分季度营收">
98
98
  <tr><th>季度</th><th class="num">营收(亿)</th><th class="num">同比</th></tr>
99
99
  <tr><td>Q1</td><td class="num">128.4</td><td class="num up">+12.4%</td></tr>
100
100
  <tr><td>Q2</td><td class="num">136.1</td><td class="num up">+8.1%</td></tr>
@@ -109,7 +109,7 @@ td.num { text-align: right; font-family: var(--mono); font-variant-numeric: tabu
109
109
 
110
110
  <div class="box">
111
111
  <h3 style="margin-top:0">✅ 规范:多系列(≤ 5 条)</h3>
112
- <table>
112
+ <table aria-label="示例数据:A / B / C 三支股票的逐年对比">
113
113
  <tr><th>年份</th>
114
114
  <th class="num"><span class="swatch" style="background:var(--accent)"></span>A 股</th>
115
115
  <th class="num"><span class="swatch" style="background:var(--danger)"></span>B 股</th>
@@ -133,7 +133,7 @@ td.num { text-align: right; font-family: var(--mono); font-variant-numeric: tabu
133
133
  </div>
134
134
 
135
135
  <h2 class="rule-top">3 · 数字格式(已经在用)</h2>
136
- <table>
136
+ <table aria-label="数字格式规则与例子">
137
137
  <tr><th>类型</th><th>规则</th><th>例子</th></tr>
138
138
  <tr><td>千分位</td><td>每三位加逗号</td><td class="num">1,234,567</td></tr>
139
139
  <tr><td>百分比</td><td>固定 1–2 位小数</td><td class="num">42.6%</td></tr>
@@ -148,7 +148,7 @@ td.num { text-align: right; font-family: var(--mono); font-variant-numeric: tabu
148
148
 
149
149
  <h2 class="rule-top">4 · 明确禁止(这些是"看起来专业但会骗人"的)</h2>
150
150
  <div class="box bad" data-claim-skip="这一块是「不要做」清单本身(一张表),不是某个组件的错写法 —— 要给它配正例栏,得先真的渲染一张 3D 图与一张 2D 图做对照,而本页没有图表实例可摆;表格尺寸量出来的是排版,不是观点">
151
- <table>
151
+ <table aria-label="明确禁止的做法与原因">
152
152
  <tr><th>不要做</th><th>为什么</th></tr>
153
153
  <tr><td class="no">3D 图表</td><td>透视扭曲会<b>误导读数</b>(Tufte 的核心结论)</td></tr>
154
154
  <tr><td class="no">双 Y 轴</td><td>两轴的交叉点可以被随意搬动 ⇒ <b>视觉在说谎</b></td></tr>
@@ -59,7 +59,7 @@ td.num { text-align: right; font-family: var(--mono); font-variant-numeric: tabu
59
59
  data-mv-orbit="vendor/OrbitControls.js"></div>
60
60
 
61
61
  <h2 class="rule-top">为什么是这样设计的</h2>
62
- <table>
62
+ <table aria-label="3D 模型页的设计要求与做法">
63
63
  <tr><th>要求</th><th>做法</th></tr>
64
64
  <tr><td>首屏要快</td>
65
65
  <td>3D 引擎<strong>点击后</strong>才 <code>&lt;script&gt;</code> 注入,
@@ -85,7 +85,7 @@ td.num { text-align: right; font-family: var(--mono); font-variant-numeric: tabu
85
85
  </ul>
86
86
 
87
87
  <h2 class="rule-top">体积现状</h2>
88
- <table>
88
+ <table aria-label="各文件的体积与加载时机">
89
89
  <tr><th>文件</th><th class="num">体积</th><th>何时加载</th></tr>
90
90
  <tr><td><code>model-viewer.js</code>(本库)</td><td class="num">~9 KB</td><td><span class="ok">首屏</span></td></tr>
91
91
  <tr><td><code>models/fox.glb</code></td><td class="num">159 KB</td><td>点击后(可 XHR 显示进度)</td></tr>
package/API.md CHANGED
@@ -24,6 +24,30 @@
24
24
  各有各的调用方式。下表由`05-audit/global-api-probe.js`在真实浏览器里
25
25
  探测生成,门禁 `api-doc` 会核对本文与实现是否一致。
26
26
 
27
+ ### 模块化引入(ESM)
28
+
29
+ 除了 `<script src>`,16 个交互组件的脚本还有一个 ESM 入口:
30
+
31
+ ```js
32
+ // 只要一个
33
+ import { Accordion } from 'agentcss/esm/accordion.mjs';
34
+
35
+ // 或者一次拿全(体积更大,Vite/Rollup 会按需摇树)
36
+ import { Accordion, Tabs, Select } from 'agentcss/esm';
37
+ ```
38
+
39
+ 三条容易踩的:
40
+
41
+ 1. **入口是包装,不是第二份实现**。`esm/*.mjs` 只做
42
+ 「导入副作用 + 把全局上那个对象 re-export」,所以
43
+ `Accordion === window.Accordion` 恒成立(门禁 `behavior-api` 断言这一点)。
44
+ 它由 `05-audit/gen-esm.py` 生成,改源码后要重新生成,`esm-gen` 门禁会拦住漂移。
45
+ 2. **无 DOM 环境可以直接 import**(SSR 安全)。但自动初始化会被跳过 ——
46
+ 服务端没有 DOM 可扫,到浏览器里再调 `init()`/构造函数。16 个脚本
47
+ 在 Node 里 `require` 全部成功(门禁的判据 A)。
48
+ 3. **重复初始化是安全的**。`init()` 与构造函数都做了幂等处理:
49
+ 同一 root 只会有一个实例,不会出现「点一次切换两次」。
50
+
27
51
  ### 四种接入形态
28
52
 
29
53
  | 形态 | 调用方式 | 适合 |
@@ -305,9 +329,24 @@ python3 05-audit/prefix-build.py --prefix fl
305
329
  | `--border-decor` | ✅ | 分隔线 |
306
330
  | `--shadow-1` / `--shadow-2` | ✅ | 阴影 |
307
331
  | `--dur` / `--ease-out` | ✅ | 动效 |
332
+ | `--dir-up` / `--dir-down` | ✅ | 涨跌方向色(**方向 ≠ 好恶**:默认红涨绿跌,`[data-market="intl"]` 整体对调) |
308
333
 
309
334
  新增令牌算**小版本变更**;移除或改默认值算**破坏性变更**。
310
335
 
336
+ ### 口径切换:`[data-market]`
337
+
338
+ 涨跌配色是**全局约定**,不是组件参数:
339
+
340
+ | 写法 | 效果 |
341
+ |---|---|
342
+ | 不写 | A 股口径:涨 = 红、跌 = 绿 |
343
+ | `<html data-market="intl">` | 国际口径:涨 = 绿、跌 = 红 |
344
+ | `<section data-market="intl">` | 只切这一块 |
345
+
346
+ 实现方式是**换令牌的指向**(`--dir-up` 永远表示"涨"),组件 CSS 里没有任何分支
347
+ —— 与暗色同一手法。`05-audit/delta-check.js` 在浏览器里读 computed color,
348
+ 四种组合(两种口径 × 明暗)各判一遍,并断言 `intl` 是**完整互换**而不是只改一边。
349
+
311
350
  ---
312
351
 
313
352
  ## 无障碍保证
@@ -327,6 +366,16 @@ python3 05-audit/prefix-build.py --prefix fl
327
366
 
328
367
  ## 破坏性变更怎么走
329
368
 
330
- - 只在**大版本**移除或重命名公开类名、`data-*`、方法
331
- - 弃用项先加警告并保留一个完整版本周期
332
- - 变更记录写在 `CHANGELOG.md`
369
+ 公开**名字**(令牌名 / 类名 / 全局与类型成员 / 事件名)的移除走**登记制**:
370
+
371
+ - 先在 `ai/deprecations.json` 里登记,写清 `deprecatedIn`、`removeIn`、`reason`
372
+ - 迁移窗口**至少跨一个次版本**:`0.7.x` 里标弃用 ⇒ 最早 `0.8.0` 才允许消失
373
+ - 到期才删。**提前删**和**到期不删**都会被门禁拦下(`05-audit/deprecation-gate.py`)
374
+ - 对外说明在 `docs/DEPRECATIONS.md`,那份文档与登记册由门禁**双向**比对,
375
+ 漏告知(登记了没写进文档)与假承诺(文档写了、登记册没有)都会红
376
+
377
+ > 🔴 本节此前写的是「只在**大版本**移除」「弃用项先加警告并保留一个完整版本周期」——
378
+ > 两句都是错的:本库在 `0.7.x → 0.8.0` 就删过名字(`state--delayed`),
379
+ > 而且组件脚本从不打印运行时警告(移动端 WebView 里那是纯噪音)。
380
+ > `0.x` 期间按 semver 本就允许破坏性变更,把话说满比说清更伤人。
381
+ > 现在这两句被上面四条取代,覆盖范围与抓不到哪些名字见 `docs/DEPRECATIONS.md`。
package/CHANGELOG.md CHANGED
@@ -16,6 +16,362 @@
16
16
 
17
17
  (无)
18
18
 
19
+ ## [0.12.0] - 2026-10-29
20
+
21
+ **新增两个组件:`delta`(变化量)与 `metric`(指标块)。这是「数值展示层」的第一批。**
22
+
23
+ 起因是一条量出来的空白:拿「做一张 A 股财报分析页」当尺子逐段量现有 35 个组件,
24
+ **KPI 数字行那一整段是空的** —— 全库没有 `metric` / `statistic` / `kpi`,
25
+ `.stat__*` 只是 `table--stats` 预设下的统计单元格(标准误、p 值)。
26
+
27
+ 更具体的一处:`table` 的 `num--pos` / `num--neg` 是**会计约定**(正 = 绿、负数即红字),
28
+ 而 A 股涨跌幅是**市场约定**(涨 = 红)。两者塞进同一列会打架 ——
29
+ 这条此前登记在 `04-recipes/table/README.md` 的「已知未统一」里,列了两个候选解法
30
+ (给 `table` 加 `num--up/down`,或加 `data-polarity`),**都说明没解**。
31
+
32
+ 这一版走第三条路:涨跌不该由 `table` 的颜色类承担,而该由一个**独立原子**承担。
33
+ `delta` 自带市场语义、自带符号,放进任何单元格都不会被 `num--pos/neg` 命中;
34
+ `table` 一行不用改,会计约定保持不动。
35
+
36
+ ### ① 方向色独立成令牌:`--dir-up` / `--dir-down`
37
+
38
+ 不复用 `--danger` / `--success`。这两组读起来都是红绿,但它们是两件事:
39
+
40
+ | 令牌 | 语义 | 例子 |
41
+ |---|---|---|
42
+ | `--danger` / `--success` | **评价**(错了 / 成了) | 已停牌、审计失败 |
43
+ | `--dir-up` / `--dir-down` | **事实**(涨了 / 跌了) | 同比 +12.4% |
44
+
45
+ 一句就够说明为什么不能共用:「研发费用同比 +38%」是**涨**,但那是成本失控。
46
+ 用 `--danger` 渲染它,颜色说着“错了”,语义说着“涨了”,读者收到矛盾信号。
47
+
48
+ 当前取值与 `--danger`/`--success` 相同(这是巧,不是同一件事),但允许将来单独调
49
+ —— 金融终端普遍用比 `--danger` 更饱和的红。
50
+
51
+ 实测对比度(`contrast.py`,四组底各算一遍):
52
+ 浅色红 4.89–5.44 / 绿 5.75–5.96;暗色红 5.60–6.15 / 绿 6.01–6.60,全部 AA。
53
+
54
+ ### ② 口径可整体切换:`[data-market="intl"]`
55
+
56
+ 默认 A 股(红涨绿跌)。给任意祖先加 `data-market="intl"` 切成国际口径。
57
+ 实现与暗色同构:`--dir-up` 永远表示“涨”,切换只改**令牌的指向**,
58
+ 组件 CSS 里没有任何分支 —— 两栏 HTML 逐字相同,只差那个属性。
59
+
60
+ 交换规则放在**独立可选文件** `01-tokens/market.css`:引它才有国际口径,
61
+ 不引就永远是 A 股口径、`data-market` 被完全忽略。核心令牌不放条件规则
62
+ (原因见下面 E137 —— 那会让五六个工具静默读错)。
63
+
64
+ ### ③ `metric`:缺数是一等状态
65
+
66
+ `metric--null` + 值写 `—`(U+2014)+ 脚下说明原因。三条禁止都进了判据:
67
+ 不填 0(`0.00` 是“现金流为零”这个具体结论)、不留空、不用 `opacity` 压淡
68
+ (`--text-tertiary` 的 5.33:1 已是地板)。
69
+
70
+ 单位独立成槽位(`metric__unit`),不拼进值字符串 —— 与 `table__unit` 同一理由。
71
+
72
+ ### 过程中踩到并修掉的五个真缺陷
73
+
74
+ - **E134**:我在 `:root { }` 里写了一个 `[data-market]` 规则块。`theme-sync.py`
75
+ 按「声明序列」解析这个块,遇到嵌套规则把选择器切碎,生成了一段
76
+ `where([data-market="intl"]) { … }` —— 无效 CSS,且第二条声明丢失。
77
+ CSS 的 `:root` 只能装声明。(`theme-sync` 本身没坏,是我喂了它不认识的形状。)
78
+ - **E135**:口径交换写成了 `:where([data-market="intl"])`,想把特异性压到 0。
79
+ **实测完全不生效** —— `:where()` 是 0,`:root` 是 (0,1,0),两者都匹配 `<html>`,
80
+ `:root` 赢。压得越低越抢不过。改成 `:root[data-market="intl"]`,并**挪到两个主题
81
+ 属性段之后**(它们由 `theme-sync` 生成,会连 L2 一起重声明,同权重下靠顺序定胜负)。
82
+ - **E136**:`delta-check` 的「红涨」判据第一版写的是 `up === --danger`。
83
+ 这是**假判据**:把整个色板对调(danger 改成绿)之后它照样绿,因为两边一起变了。
84
+ 「红涨」说的是颜色本身,只能判**色相**(红区 ≤20° 或 ≥345°,绿区 90–165°)。
85
+ - **E137**:口径交换规则最初写在 `tokens.css` 末尾,直接污染了工具链。
86
+ `05-audit/_css.py` 的 `declared_tokens()` 是**纯词法**的 —— 遍历所有 `--x: y;`,
87
+ 后者覆盖前者,**不看选择器**。于是它把「条件性交换」当成默认值叠上去:
88
+ `contrast.py` 报告里「暗色 涨」显示成绿色,`token-tree-gate` 报「浏览器解析到了
89
+ `--dir-down`,导出里没有」,`gen-ai-contract --check` 报看似随机的错位 ——
90
+ 而浏览器里的值一直是对的。**门禁冤枉代码比漏判更难查。**
91
+ 修法:交换规则移到独立可选文件 `01-tokens/market.css`,`tokens.css` 只留说明注释。
92
+ - **E138**:E137 的修法又带出一个新缺陷 —— 把规则挪到 `market.css` 之后,
93
+ `delta` 的 demo **没跟着引新文件**,那一栏「国际口径」静默显示成 A 股配色:
94
+ 页面在说谎,而所有门禁都是绿的(CSS 不报错,`data-market` 属性也照样存在,
95
+ 只是没东西接它)。修法:demo 补引用,并给 `delta-check` 加判据 ⑧ ——
96
+ 用了 `data-market` 的页面必须引 `market.css` 且排在 `tokens.css` 之后,
97
+ 92 个页面全扫描(破坏实验确认删掉引用会红)。
98
+
99
+ ### 全量门禁又抓出六个(E139–E144)
100
+
101
+ 本地单项验证绿了不等于全量绿 —— 这一轮全量门禁(208 项)第二次跑才发现:
102
+
103
+ - **E139**:`tierA-tokens.css`(精简档)漂了两轮没人管。文件本身有 `--check`,
104
+ 但它**连 `--dir-*` 都没有**(用户写 `var(--dir-up)` 会拿到未定义令牌),
105
+ 文档里「浅色 102 + 暗色 33」也停在旧值。⇒ 重抽文件 + `doc-facts-gate` 加 F6,
106
+ 把「浅色 N + 暗色 M」变成可核对的判据。
107
+ - **E140**:两个新门禁起名 `dir-gate` / `metric-gate`,而成熟度探测器要求
108
+ 文件名含 `check`/`contract`/`geometry` ⇒ 「有没有专属门禁」判 False,
109
+ 两个组件被判成 `alpha`。⇒ 改名 `delta-check.js` / `metric-check.js`,
110
+ 重新生成契约(**stable 33 / beta 4**)。
111
+ - **E141**:脚本删块加注释时漏了 `*/`,`tokens.css` 后半段(含字体栈)
112
+ 被整段注释吞掉;而**视觉基线是在那之后录的**。⇒ 补闭合、先跑语法级判据再录基线。
113
+ - **E142**:库里早有 `.num` / `.num-lg`(`typography.css`),我又在两个组件里
114
+ 各写了一遍 `font-variant-numeric`。⇒ 不是加豁免,而是**删掉**:
115
+ 两个原子都用 `--mono`,等宽字族天然对齐,那条声明是无效动作。
116
+ - **E143**:`metric` 的值写了 `white-space: nowrap`,单位是它的子元素 ⇒
117
+ RTL 下阿语膨胀后顶破视口 212px。⇒ 值改 flex + `overflow-wrap: anywhere`,
118
+ 单位 `min-width: 0`。
119
+ - **E144**:精简档生成器的头注释里写着内部协作用语,
120
+ 随包发布 ⇒ `leak-scan` 报「过程痕迹」。⇒ 改生成器文案(产物注释只写给使用者看)。
121
+
122
+
123
+
124
+ `delta` 的 `+` / `−` 由 CSS `::before` 生成,所以它们**不在** `textContent` 里。
125
+ 实测(Chromium + CDP 可访问性树):元素的可访问名字只有 `3.21%`,**不含符号**;
126
+ 但符号作为**独立的 StaticText 节点**存在,读屏会按顺序念「加」「3.21%」。
127
+
128
+ 也就是说「符号对读屏可用」是成立的 —— 但成立在一个很容易被改坏的机制上:
129
+ 换成空串、换成背景图、或让伪元素 `visibility: hidden`,视觉上可能仍有符号,
130
+ 可访问性树里就没了,而颜色对读屏毫无意义。
131
+
132
+ ⇒ `delta-check` 加了判据 ⑦:直接数十字树里的 `+` / `−` 节点。
133
+ 破坏实验(给 `::before` 加 `visibility: hidden`)确认它会红。
134
+
135
+ ### 门禁 204 → 208
136
+
137
+ 新增 `dir-color` / `dir-color-rev` / `metric` / `metric-rev`。
138
+ 两道主门禁都要起浏览器,自检不需要(纯判据)。
139
+ 三个新组件页全部纳入视觉基线(明/暗/RTL 各三张)与无障碍树快照(38 → 40 页)。
140
+
141
+ CSS 体积预算 125 → **132 KB**(剥注释后口径):两个新组件 + `market.css` 的
142
+ 真实规则合计 +2.1 KB(127.1 KB 实测)。**这是有意增长,不是放宽**——
143
+ 涨的是规则,不是注释;单文件上限仍是 24 KB(最大 `tokens.css` 14.4 KB)。
144
+
145
+ ### 已知边界
146
+
147
+ - `delta` 的方向**不由数值推断**(没有 JS)。类名写错就颜色错且不报错 ——
148
+ 这是有意的:本库不带运行时,读数值意味着把格式化逻辑也拉进来。
149
+ - `metric` 不做数值格式化(千分位、小数位、单位换算归数据侧)。
150
+ - 真机读屏(NVDA / VoiceOver)仍是 0 条记录,`docs/A11Y-MANUAL-PROTOCOL.md` 表格还是空的。
151
+
152
+ ---
153
+
154
+ ## [0.11.16] - 2026-10-29
155
+
156
+ **这一版没有新组件,补的是两处「说了但没做到」。**
157
+
158
+ 上一轮自评扣分最重的两条是:**没有对外弃用政策**(版本稳定性)
159
+ 和**无障碍树只验了 11 页**。两条都属于"文档里写得像有,实际上没有"的类型 ——
160
+ 比明说没做更坏,因为读者会按"有"的前提做决策。
161
+
162
+ ### ① 弃用政策:从「一句话承诺」变成「四类名字、双向门禁」
163
+
164
+ 此前 `ai/deprecations.json` 已经存在,但只覆盖**令牌名和类名**。
165
+ 于是「删掉 `Select.open()`」「把 `fl-select` 改名」这两件事
166
+ **不在任何门禁的视野里** —— 而它们比删类名更容易伤到下游:
167
+ 类名删了样式塌了,看得见;方法删了是运行时 `undefined is not a function`,
168
+ 往往在用户点击之后才炸;事件名改了则**完全静默**,订阅方只是再也不被调用。
169
+
170
+ 现在 `kind` 四类,各自的取法都是静态可复算的(本门禁跑在 `MODE=fast` 层,
171
+ 不能为了一份名单去起浏览器):
172
+
173
+ | 类型 | 从哪来 | 抓得到 |
174
+ |---|---|---|
175
+ | `token` | `ai/tokens.json` | 删令牌、改名 |
176
+ | `class` | `ai/components.json` | 删根类 / 变体 / 槽位 |
177
+ | `api` | 源码里的 `global.X =`(大写开头)+ `types/index.d.ts` 的成员 | 删全局、删成员、删参数对象字段 |
178
+ | `event` | `flEmit(el, 'fl-…')` 的字面量 | 改事件名 |
179
+
180
+ 实测(先红后修,不是"看着应该能抓"):
181
+
182
+ | 破坏方式 | 结果 |
183
+ |---|---|
184
+ | 把 `fl-nav-open` 改成 `fl-nav-opened` | 🔴 `fl-nav-open:基线里有、现在没了` |
185
+ | 把 `TocApi.init` 改名 | 🔴 `TocApi.init:基线里有、现在没了` |
186
+
187
+ 过程中自己踩了两个洞,都记进了 `internal/ERRORS.md`:
188
+
189
+ - **E131**:扩了 `KINDS`,但汇总行还写死 `cur_by['token'] | cur_by['class']`
190
+ ⇒ ⑦(未登记的移除)只对前两类算差集,新加的两类删了也不报。
191
+ 门禁跑得更多、看着更忙,覆盖面反而更窄。修法:汇总一律遍历 `KINDS`,
192
+ 且**每一类都要有自己的突变样本**(只打第一类证明不了后面接上了)。
193
+ - **E132**:成员解析正则少一个 `re.M` ⇒ `^` 只匹配 body 开头,
194
+ 而 `^\s+` 会吃掉前导换行,正好命中第一个成员,`AccordionInstance` 只收到 `root`。
195
+
196
+ 对外文档新建 **`docs/DEPRECATIONS.md`**,并与登记册**双向**比对
197
+ (新门禁 `deprecation-doc-gate`):
198
+
199
+ - 登记册有、文档没写 ⇒ **漏告知**(读者以为它不会消失)
200
+ - 文档写了、登记册没有 ⇒ **假承诺**(承诺了一个我们不会兑现的日期)
201
+ - 版本号写错、替代写错、把已移除的名字写进"迁窗中"表 ⇒ 各自有独立判据
202
+
203
+ 顺带纠正了 `API.md` 里两句**说错的话**:原文写「只在**大版本**移除」
204
+ 「弃用项先加警告并保留一个完整版本周期」—— 而本库在 `0.7.x → 0.8.0`
205
+ 就删过名字(`state--delayed`),且组件脚本从不打印运行时警告。
206
+ 把话说满比说清更伤人,已改成与登记册一致的四条。
207
+
208
+ ### ② 无障碍树:11 页 → 全部 38 页,跑出 12 张无名表格
209
+
210
+ `a11y-ax.js` 之前只转录 11 个「交互语义各不相同」的代表页。
211
+ 这个理由经不起一句追问:**没被转录的 27 页,凭什么认为它们没问题?**
212
+ 答案是没有依据 —— 判据只对被转录的页面生效。
213
+
214
+ 扩到全量后,第一次跑就红了 **12 条 `AX-1`,全部是同一类**:
215
+
216
+ ```text
217
+ 🔴 pagination AX-1 [table] 可交互角色没有 accessible name
218
+ 🔴 report AX-1 [table] …(2 张)
219
+ 🔴 echarts-adapter AX-1 [table] …(5 张)
220
+ 🔴 model-viewer AX-1 [table] …(2 张)
221
+ 🔴 composition AX-1 [table] …
222
+ 🔴 popover AX-1 [table] …
223
+ ```
224
+
225
+ 这些是 demo 页里的**数据表**,紧跟在一个可见标题后面 —— 视觉用户读得到上下文,
226
+ 读屏用户读到的是"表格,没有名字"。修法给每张表加 `aria-label`,
227
+ 文字取自它上方那个标题(**不用** `<caption>`:可见 caption 会改变像素,
228
+ 而三套视觉基线要重录,那属于另一件事,不该混在无障碍修复里)。
229
+
230
+ 同时补了一条**覆盖完整性判据**:手写的页面清单必须**等于**磁盘上的全部
231
+ `demo.html`。漏页(新增了 demo 忘了加清单)与幽灵页(清单里的文件不存在)
232
+ 都会直接红 —— 手写清单是「哪些页面要查」的第二份表述,两份必然漂移,
233
+ 这正是 `a11y-scan.js` 在 0.8.9 栽过的同一个坑。判据自己也做了反向控制
234
+ (删一页 / 加幽灵页 / 重复登记,三种都必须红)。
235
+
236
+ ### ③ 门禁 203 → 204
237
+
238
+ 新增 `deprec-doc`,排在 `deprecation` 之后,仍在静态层内(不需要浏览器)。
239
+ 静态层本机约 3 分钟。
240
+
241
+ ### 已知边界
242
+
243
+ - `api` 这一类里,只有 `api-contract-gate` 覆盖的组件是**实测**过
244
+ d.ts ↔ 运行时一致的;其余组件的成员**仅有声明**。
245
+ ⇒ 本判据保证「删声明之前必须登记」,不保证「运行时一定还有它」。
246
+ - `data-*` 属性名、`detail` 字段、令牌默认值、CSS 变量默认值
247
+ **不在**基线里。这是"没做",不是"量不了",已在 `docs/DEPRECATIONS.md`
248
+ 第四节逐条列出。
249
+ - a11y 的人工部分仍然为零:真机读屏(NVDA / VoiceOver)一条记录都没有。
250
+ 协议在 `docs/A11Y-MANUAL-PROTOCOL.md`,表格还是空的。
251
+
252
+ ---
253
+
254
+ ## [0.11.15] - 2026-10-29
255
+
256
+ **「在真实项目里能不能用」这件事,现在有证据了。**
257
+
258
+ 前几版把验证工程做到了很高的水准:174 道门禁、可证伪的反例断言、变异探针。
259
+ 但有一类问题它一直抓不到 —— **这些检查全是对自家 demo 页做的**。
260
+ 一个 React / Next.js 项目把库装进去会怎样,仓库里没有任何东西回答过。
261
+
262
+ 这一版补的是那一半。查的时候发现三个**不会报错**的坑,每个都足以让
263
+ 接入者在第一步就放弃。
264
+
265
+ ### ① SSR 里 `import` 就崩(16/16 全崩)
266
+
267
+ 16 个行为脚本的收尾都是 `(function (global) { … })(window)`。
268
+ 在浏览器里没问题,在 Node / Next.js / Nuxt / Astro 里:
269
+
270
+ ```text
271
+ ReferenceError: window is not defined
272
+ at Object.<anonymous> (03-patterns/accordion/accordion.js:485:3)
273
+ ```
274
+
275
+ 实测记录(修之前,逐个 `require`):
276
+
277
+ | 脚本 | 崩在 |
278
+ |---|---|
279
+ | 11 个 `… })(window)` | IIFE 调用处 |
280
+ | `accordion` | 顶层 `typeof window.requestAnimationFrame`(会**先解引用** window) |
281
+ | `nav` / `tabs` / `pagination` / `reveal` | 顶层 `document.readyState` |
282
+
283
+ 修法:IIFE 收尾统一为
284
+ `})(typeof window !== 'undefined' ? window : globalThis);`,
285
+ 顶层 `document` 访问前加 `if (typeof document === 'undefined') return;`。
286
+ 现在 16/16 在 Node 里 `require` 成功,且 `globalThis.X` 拿到真 API。
287
+
288
+ ### ② 重复初始化会「点了没反应」(静默双触发)
289
+
290
+ `init()` 没有幂等守卫。被调用两次时(SPA 路由切换、HMR、重复引入脚本):
291
+
292
+ - 旧实例的 click 监听**还挂在 root 上**(它的闭包还活着)
293
+ - 新实例又挂一份 ⇒ 一次点击 toggle **两次** ⇒ 展开又收起
294
+
295
+ 肉眼看到的是「按钮点了没反应」,控制台干干净净。
296
+
297
+ 修法两条:`init()` 跳过已初始化的节点;构造器 `if (root.accordionInstance) return root.accordionInstance;`
298
+ (返回对象覆盖 `this` 是 JS 合法行为)。`tabs` 同样处理。
299
+
300
+ ### ③ 有 `.d.ts`,却没有模块入口
301
+
302
+ `types/index.d.ts` 声明了 `export declare const Select …`,
303
+ 而 `package.json` 的 `exports` 里**没有任何路径指向 JS 模块** ——
304
+ `.` 指向 `index.html`,组件只能靠 `<script src>` 挂全局。
305
+ 类型在说谎:`import { Select } from 'agentcss'` 编译通过、运行时 undefined。
306
+
307
+ 修法:
308
+
309
+ - 新增 `esm/`(**生成物**,17 个 `.mjs`:16 个组件 + `index.mjs`),
310
+ 由 `05-audit/gen-esm.py` 从源码的 `global.X = …` 机械生成。
311
+ 内容是「导入副作用 + 把 global 上的对象 re-export」,
312
+ **不复制实现** —— 门禁会断言 `esm` 导出与 `window.X` 是同一个对象。
313
+ - `package.json` 新增 `"./esm"` 与 `"./esm/*"`,`files` 加 `esm/`。
314
+ - `types/index.d.ts` 从 5 个组件补到 **16 个**(新增 Accordion / Tabs / Nav /
315
+ Pagination / Drawer / Overlay / Tooltip / Popover / Toc / Reveal / FLIP),
316
+ 每条都对照过源码的真实赋值与原型方法名;收不窄的参数写 `object` 并注明,
317
+ 不给假签名。
318
+ - `sideEffects` 从 `["*.css", "*.js"]` 改为 `["**/*.css", "**/*.js", "**/*.mjs"]`。
319
+ **这是个真实的打包坑**:只匹配根目录的 glob 会让 bundler 把
320
+ `02-primitives/**/*.js` 当作无副作用模块 tree-shake 掉 —— 行为静默消失。
321
+
322
+ ### ④ 接入样板真的能跑了
323
+
324
+ `examples/react-vite/README.md` 曾写着「这是 T-01 的交付物:证明这个库能被
325
+ 真实项目用上」,但那个目录里只有 `index.html` + `README.md` ——
326
+ 没有 `package.json`、没有构建、从没被执行过。**声称的证明不是证明。**
327
+
328
+ 新增 `examples/esm-native/`:原生 `<script type="module">`,零依赖零构建,
329
+ 由 `05-audit/example-esm-gate.js` 真打开、真点击、真量 computed style。
330
+ 不用 Vite 是因为装依赖的样板在别人机器上会因网络/版本差异跑不起来,
331
+ 「证明」就退化成「我机器上曾经证明过」。
332
+
333
+ ### ⑤ 门禁从 20 分钟降到 5–7 分钟(同一套判据)
334
+
335
+ `check-all.sh` 改为「先入队、后并发执行、按序打印」:
336
+
337
+ - 默认 4 路并发(`--parallel=N` 可调,`--serial` 回到逐条)。
338
+ - 输出**顺序不变**:每条门禁的输出落到临时文件,全部跑完按注册顺序打印。
339
+ 并发最容易毁掉「日志还能不能读」,而失败诊断是门禁存在的一半理由。
340
+ - 会临时改工作区文件的门禁标 `@serial` 独占跑(`selftest-all` / `mutation-leak` /
341
+ `beh-reverse` / `pack-smoke` 等 9 条)。不标就互相污染 —— 实测出现过
342
+ 「6 个自检弄脏工作区」的假红。
343
+ - 实测:4 道最重的(zoom-reflow / rtl-render / beh-reverse / demo-claim)
344
+ 串行 354s → 并发 142s,全部返回 0。
345
+ - 全量实测:**203 道串行等价 1422 秒(23.7 分钟)→ 实际 9 分钟**,202 PASS
346
+ (唯一红的是 `release`,它要求先有 tag)。最慢的五道:`dark-cont` 142s、
347
+ `responsive` 140s、`zoom-reflow` 122s、`rtl-render` 89s、`visual-dark` 83s。
348
+
349
+ ### 门禁 197 → 203
350
+
351
+ | 新增 | 判据 |
352
+ |---|---|
353
+ | `esm-gen` / `esm-gen-rev` | `esm/` 与源码的 `global.X =` 一一对应(生成物不漂移) |
354
+ | `behavior-api` / `-rev` | A 无 DOM 加载 · B 重复构造返回同一实例且单击只切换一次 · C ESM 覆盖 |
355
+ | `example-esm` / `-rev` | 样板页真能被浏览器打开、import 成功、行为与样式都生效 |
356
+
357
+ 另外两处判据升级:
358
+
359
+ - `wiring` 现在认识 ESM:`import … from '../../esm/x.mjs'` 算作「引用了行为脚本」。
360
+ 之前它只会看 `<script src>`,一个正确使用 ESM 的页面会被判「用了标记但没引脚本」
361
+ (自检加了 ⑥/⑥b 两条反向控制)。
362
+ - `tsc-gate` 反例新增 4 条(无参 toggle / 字符串喂 pagesOf / attach 返回值当句柄 /
363
+ Nav 的静态 open),断言从 8 条增至 12 条。
364
+
365
+ ### 已知边界(如实写下来)
366
+
367
+ - `esm/` 只覆盖 `02-primitives` 与 `03-patterns` 的行为脚本。
368
+ `09-assets` 的绘图脚本(Chart / Bar / ScientificPlot)与
369
+ `01-tokens/theme-toggle.js` 仍是 script 形态 —— 它们一个文件挂多个全局,
370
+ 与「一文件一 API」的生成规则不同,硬套会生成名字打架的入口。
371
+ - `Pagination.create()` 的实例成员未在类型层收窄(源码只挂了内部状态),
372
+ 需要时读 `03-patterns/pagination/README.md`。
373
+ - 真机读屏(NVDA / VoiceOver)仍需人工执行,`docs/A11Y-MANUAL-PROTOCOL.md` 的表格仍是空的。
374
+
19
375
  ## [0.11.14] - 2026-10-29
20
376
 
21
377
  **「键盘到不了」这句话,现在能量了。**
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
+ 全量是 **208 道门禁**(本机约 5–7 分钟:组件层要起浏览器,还要逐张比 96 张视觉截图;
12
+ 0.11.15 起默认 4 路并发执行,之前串行要 15–20 分钟)。
12
13
  只想确认没改坏静态契约的话用 `gate:fast`,几秒到几分钟。
13
14
 
14
15
  环境依赖只有一套(Node + Chromium),缺了对应的门禁会**跳过**(跳过不算通过):