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
@@ -246,8 +246,10 @@
246
246
  `<p class="small">` 描述"别这么做",不算反例 —— 读者看不见那个"错"。
247
247
  摆不出来的(运行时行为 / 读屏输出 / 这页没有图表实例)写在块上:
248
248
  `<div class="cmp__pane cmp__bad" data-prose="为什么摆不出来">`
249
- 单栏的 `box bad` 页没有成对结构,页内要写
250
- `<!-- demo-claim:skip 为什么没有成对断言 -->`
249
+ 单栏的 `box bad` 块没有成对结构,**在块上**写清为什么:
250
+ `<div class="box bad" data-claim-skip="为什么写不出互斥断言">`
251
+ 合法的两种典型:两栏在视觉上必然相同(差别只在无障碍树里)、
252
+ 差别是对比度(那件事由 `contrast.py` 单独测)。理由会被逐条打印。
251
253
  ========================================================================= */
252
254
  .page .cmp {
253
255
  display: grid;
@@ -17,7 +17,6 @@
17
17
  </head>
18
18
  <body class="page page">
19
19
  <main class="wrap">
20
- <!-- demo-claim:skip 反例的差别是「饱和底白字的对比度」和「div 能不能被 Tab」:前者 demo-claim 没有对比度指标(contrast.py 单独测),后者没有焦点指标 -->
21
20
  <h1>badge · 状态标记</h1>
22
21
  <p class="lede">
23
22
  badge 承载<strong>一个短状态词</strong>("已停牌""审计中")。
@@ -64,7 +63,7 @@
64
63
  </p>
65
64
 
66
65
  <h2 class="rule-top">反例</h2>
67
- <div class="box bad">
66
+ <div class="box bad" data-claim-skip="两栏画出来的都是同一批色块,尺寸与位置完全相同 —— 差别是暗色下 2.72:1 与 6.39:1 的对比度,而 demo-claim 没有对比度指标(那件事由 contrast.py 单独测,本页正文引的就是它的数)">
68
67
  <h3>反例:饱和底 + 白字</h3>
69
68
  <div class="row">
70
69
  <span class="badge badge--solid-success">已披露</span>
@@ -80,7 +79,7 @@
80
79
  </p>
81
80
  </div>
82
81
 
83
- <div class="box bad">
82
+ <div class="box bad" data-claim-skip="可点的正例本该是一个 a / button,但本库 badge 门禁要求所有可点元素命中区 ≥44px,而徽标本体只有 24px 高 —— 摆一个过得了门的正例,要么改组件,要么给演示塞内联样式把高度撑起来(那是拿样式骗门禁)。⇒ 配不成对的原因不是量不了,是组件本身还没解决这个冲突,见 CHANGELOG「已知冲突」">
84
83
  <h3>反例:用 badge 装可点的东西</h3>
85
84
  <div class="row">
86
85
  <div class="badge badge--info">待复核</div>
@@ -91,6 +90,10 @@
91
90
  —— div 加 onclick 对读屏是<strong>「没有角色的按钮」</strong>:
92
91
  读不出来它是能点的,键盘也 Tab 不到。
93
92
  要能点就用 <code>&lt;button&gt;</code> 或 <code>&lt;a&gt;</code>,样式照抄 <code>.badge</code>。
93
+ <br><br>
94
+ ⚠️ <strong>但这句建议目前有个坑</strong>:换成 button/a 之后,
95
+ 本库 badge 门禁会判「命中区 54×24 &lt; 44px」—— 徽标本体的高度不够。
96
+ <strong>这个问题还没解决</strong>,不是演示写错了。
94
97
  </p>
95
98
  </div>
96
99
 
@@ -32,7 +32,6 @@ p.small { font-size: var(--fs-xs); color: var(--text-tertiary); line-height: 1.6
32
32
  </head>
33
33
  <body class="page page">
34
34
  <main class="wrap">
35
- <!-- demo-claim:skip 可量(display:none 的 input 高度是 0,opacity:0 的不是),但这一页仍是单栏结构,升级成 cmp 要重排整页 —— 本轮没做,已知欠账 -->
36
35
 
37
36
  <h1>checkbox / radio / select</h1>
38
37
  <p class="lede">
@@ -187,22 +186,39 @@ p.small { font-size: var(--fs-xs); color: var(--text-tertiary); line-height: 1.6
187
186
  </p>
188
187
  </div>
189
188
 
190
- <h2 class="rule-bottom">反例:用 display:none 隐藏原生控件</h2>
191
- <div class="box bad">
192
- <h3>反例:把原生勾选框 <code>display:none</code> 藏掉</h3>
193
- <div class="choice choice--checkbox">
194
- <input class="choice__input" type="checkbox" style="display:none" id="bad1">
195
- <label class="choice__label" for="bad1">
196
- <span class="choice__mark"></span>
197
- <span>我看起来正常,但 Tab 到不了这里</span>
198
- </label>
189
+ <h2 class="rule-bottom">对照:把原生勾选框藏起来的两种写法</h2>
190
+ <div class="cmp">
191
+ <div class="cmp__pane cmp__bad" data-bad="input|focus|=0">
192
+ <h3>❌ display:none:看不见,也 Tab 不到</h3>
193
+ <div class="choice choice--checkbox">
194
+ <input class="choice__input" type="checkbox" style="display:none" id="bad1">
195
+ <label class="choice__label" for="bad1">
196
+ <span class="choice__mark"></span>
197
+ <span>我看起来正常,但 Tab 到不了这里</span>
198
+ </label>
199
+ </div>
200
+ <p class="cmp__why">
201
+ <strong>按一次 Tab 就知道:焦点直接跳过它。</strong>
202
+ <code>display:none</code> 把这个 input 从 tab 顺序里删掉了,
203
+ 键盘用户彻底到不了 —— 而鼠标点仍然有效,所以这个 bug 用鼠标永远测不出来。
204
+ </p>
205
+ </div>
206
+
207
+ <div class="cmp__pane cmp__good" data-good="input|focus|=1">
208
+ <h3>✅ opacity:0 + 绝对定位:看不见,但还在 Tab 顺序里</h3>
209
+ <div class="choice choice--checkbox">
210
+ <input class="choice__input" type="checkbox" id="good1">
211
+ <label class="choice__label" for="good1">
212
+ <span class="choice__mark"></span>
213
+ <span>同一行字,看起来一模一样,但 Tab 停得住</span>
214
+ </label>
215
+ </div>
216
+ <p class="cmp__why">
217
+ 本库的 input 是 <code>opacity:0</code> 铺满整个 label:
218
+ 你看得见的是 <code>.choice__mark</code>,焦点环也画在它身上
219
+ (<code>:focus-visible + .choice__mark</code>)。
220
+ </p>
199
221
  </div>
200
- <p class="small" style="margin-top:var(--sp-3)">
201
- <strong>用 Tab 试试——焦点会直接跳过它。</strong>
202
- <code>display:none</code> 把它从 tab 顺序里移除了,键盘用户彻底到不了这里。
203
- 鼠标点击还正常,所以这种 bug 用鼠标测不出来。<br><br>
204
- 本库的 input 用的是 <code>opacity:0</code> + 绝对定位:<strong>看不见,但仍在 tab 顺序里</strong>。
205
- </p>
206
222
  </div>
207
223
 
208
224
  <h2 class="rule-bottom">焦点环必须画在看得见的地方</h2>
@@ -482,4 +482,4 @@
482
482
  }
483
483
 
484
484
  global.Combobox = { create: create };
485
- })(window);
485
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -298,4 +298,4 @@
298
298
  iso: iso, addDays: addDays, addMonths: addMonths,
299
299
  quarterOf: quarterOf, quarterRange: quarterRange,
300
300
  };
301
- })(window);
301
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -78,7 +78,7 @@ th { font-weight: 600; }
78
78
 
79
79
  <h2>怎么选?</h2>
80
80
  <div class="demo">
81
- <table>
81
+ <table aria-label="tooltip / popover / dialog 三者的选择依据">
82
82
  <tr><th>组件</th><th>能放交互内容</th><th>背景是否锁死</th><th>用在哪</th></tr>
83
83
  <tr><td><code>tooltip</code></td><td>❌ 只能纯文字</td><td>—</td><td>补充说明、术语解释</td></tr>
84
84
  <tr><td><b><code>popover</code></b></td><td>✅</td><td><b>不锁</b></td><td>筛选、快捷设置、补充表单</td></tr>
@@ -328,4 +328,4 @@
328
328
  };
329
329
 
330
330
  global.Popover = Popover;
331
- })(window);
331
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -546,4 +546,4 @@
546
546
  }
547
547
 
548
548
  global.Select = { create: create };
549
- })(window);
549
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -15,7 +15,6 @@
15
15
  </head>
16
16
  <body class="page page">
17
17
  <main class="wrap">
18
- <!-- demo-claim:skip 两块都可量(<hr> 的个数 0 vs 1),但这一页仍是单栏结构,升级成 cmp 要重排整页 —— 本轮没做,已知欠账 -->
19
18
  <h1>separator · 分隔线</h1>
20
19
  <p class="lede">
21
20
  分隔线有两种,<strong>必须分清</strong>:装饰性的只分开内容块,
@@ -61,7 +60,7 @@
61
60
  </div>
62
61
 
63
62
  <h2>反例</h2>
64
- <div class="box bad">
63
+ <div class="box bad" data-claim-skip="两栏都会画出一条 1px 的线,高度与宽度完全相同 —— 差别是 1.26:1 与 3.42:1 的对比度,而 demo-claim 没有对比度指标(那件事由 contrast.py / component-contrast.py 单独测)">
65
64
  <h3>反例:语义分隔用装饰色</h3>
66
65
  <div style="max-width:32em">
67
66
  <p class="small" style="margin:0">基本信息</p>
@@ -77,14 +76,33 @@
77
76
  </p>
78
77
  </div>
79
78
 
80
- <div class="box bad">
81
- <h3>反例:用 div + border-bottom 代替 hr</h3>
82
- <div style="border-bottom:1px solid var(--border-decor); margin:var(--sp-4) 0"></div>
83
- <p class="small">
84
- <code>&lt;div class="line"&gt;</code> 对读屏是<strong>什么都没有</strong>。
85
- 语义分隔必须用 <code>&lt;hr&gt;</code>(或 <code>role="separator"</code>),
86
- 装饰性的才可以用纯 div。
87
- </p>
79
+ <div class="cmp">
80
+ <div class="cmp__pane cmp__bad" data-bad="hr|count|=0">
81
+ <h3>❌ div + border-bottom</h3>
82
+ <div>
83
+ <p class="small" style="margin:0">基本信息</p>
84
+ <div style="border-top:1px solid var(--border-decor); margin:var(--sp-4) 0"></div>
85
+ <p class="small" style="margin:0">高级设置</p>
86
+ </div>
87
+ <p class="cmp__why">
88
+ 线画得一模一样 —— 连颜色都对齐了。但读屏走到这里是
89
+ <strong>两段独立的文字,中间什么都没有</strong>:
90
+ 它不知道这两段被分成了一组。
91
+ </p>
92
+ </div>
93
+
94
+ <div class="cmp__pane cmp__good" data-good="hr|count|=1">
95
+ <h3>✅ hr(或 role="separator")</h3>
96
+ <div>
97
+ <p class="small" style="margin:0">基本信息</p>
98
+ <hr class="separator separator--tight">
99
+ <p class="small" style="margin:0">高级设置</p>
100
+ </div>
101
+ <p class="cmp__why">
102
+ 同一条线,读屏会念「分隔符」。<strong>0 个 <code>&lt;hr&gt;</code> 与 1 个</strong>,
103
+ 就是"看得见"和"念得出"的全部差别。
104
+ </p>
105
+ </div>
88
106
  </div>
89
107
  </main>
90
108
  </body>
@@ -18,7 +18,6 @@
18
18
  </head>
19
19
  <body class="page page">
20
20
  <main class="wrap">
21
- <!-- demo-claim:skip 两块都可量(input 个数 0 vs 1、label 的高度),但这一页仍是单栏结构,升级成 cmp 要重排整页 —— 本轮没做,已知欠账 -->
22
21
  <h1>switch · 开关</h1>
23
22
  <p class="lede">
24
23
  开关用于<strong>立即生效的设置项</strong>("开/关通知")。
@@ -83,22 +82,37 @@
83
82
  </div>
84
83
 
85
84
  <h2 class="rule-top">反例</h2>
86
- <div class="box bad">
87
- <h3>反例:用 div + role="switch",自己管所有状态</h3>
88
- <div class="switch" role="switch" aria-checked="false" tabindex="0">
89
- <span class="switch__track"><span class="switch__thumb"></span></span>
90
- <span class="switch__label">开启动价推送</span>
85
+ <div class="cmp">
86
+ <div class="cmp__pane cmp__bad" data-bad="input|count|=0">
87
+ <h3>❌ div + role="switch",自己管所有状态</h3>
88
+ <div class="switch" role="switch" aria-checked="false" tabindex="0">
89
+ <span class="switch__track"><span class="switch__thumb"></span></span>
90
+ <span class="switch__label">开启动价推送</span>
91
+ </div>
92
+ <p class="cmp__why">
93
+ <strong>看起来和下面那个一模一样 —— 这正是坑所在。</strong>
94
+ 常见写法是 <code>&lt;div role="switch" tabindex="0" onclick="…"&gt;</code>
95
+ 再手写 <code>aria-checked</code> 切换,于是 Space 键、焦点样式、状态播报
96
+ 三件事都得自己管,<strong>漏一件就坏</strong>。
97
+ 而 axe 只看得见 DOM,看不见你的 JS 有没有绑对。
98
+ </p>
99
+ </div>
100
+
101
+ <div class="cmp__pane cmp__good" data-good="input|count|=1">
102
+ <h3>✅ 原生 checkbox + role="switch"</h3>
103
+ <span class="switch">
104
+ <input class="switch__input" type="checkbox" role="switch" id="s6">
105
+ <span class="switch__track"><span class="switch__thumb"></span></span>
106
+ <label class="switch__label" for="s6">开启动价推送</label>
107
+ </span>
108
+ <p class="cmp__why">
109
+ 同一个外观,但键盘响应、焦点环、状态播报<strong>全部由浏览器给</strong> ——
110
+ 少写的那部分不是"省事",是"少了一个会忘的地方"。
111
+ </p>
91
112
  </div>
92
- <p class="small">
93
- 它<strong>看起来和上面那个一模一样</strong> —— 这正是坑所在:
94
- 常见写法是 <code>&lt;div role="switch" tabindex="0" onclick="..."&gt;</code>,
95
- 然后手写 <code>aria-checked</code> 切换。这需要自己处理:
96
- Space 键、焦点样式、状态播报。<strong>三件事漏一件就坏</strong>,
97
- 而且测试工具(axe)只看得见 DOM,看不见你的 JS 有没有绑对。
98
- </p>
99
113
  </div>
100
114
 
101
- <div class="box bad">
115
+ <div class="box bad" data-claim-skip="两栏在视觉上必须完全相同 —— 那正是这一块要教的:差别只在无障碍树里(sr-only 的 label 仍在树中,display:none 的不在),可见部分都只有滑块那么大,写不出互斥断言。证据在 10-review/a11y-tree/switch.txt">
102
116
  <h3>反例:给 label 加 display:none</h3>
103
117
  <div class="switch">
104
118
  <span class="switch__track"><span class="switch__thumb"></span></span>
@@ -28,7 +28,7 @@
28
28
  * 否则造成 "landmark region proliferation"(地标泛滥)。
29
29
  * ⇒ 加 .accordion--many 即可关闭。
30
30
  */
31
- (function () {
31
+ (function (global) {
32
32
  'use strict';
33
33
  // 🔴 手风琴实例序号(保证 id 全局唯一,见 init 里的说明)
34
34
  var uid = 0;
@@ -60,8 +60,13 @@
60
60
 
61
61
  function toArray(x) { return Array.prototype.slice.call(x); }
62
62
 
63
- /* requestAnimationFrame 的 ES5 安全版 */
64
- var raf = (typeof window.requestAnimationFrame === 'function')
63
+ /* requestAnimationFrame 的 ES5 安全版
64
+ ⚠️ 必须先判 `window` 存在再摸 `.requestAnimationFrame`:
65
+ `typeof window.requestAnimationFrame` 会**先解引用 window**,
66
+ 在 Node / SSR 里直接 ReferenceError(不是返回 'undefined')。
67
+ 这条曾经让本文件在 Next.js 里 import 即崩。 */
68
+ var raf = (typeof window !== 'undefined' &&
69
+ typeof window.requestAnimationFrame === 'function')
65
70
  ? window.requestAnimationFrame.bind(window)
66
71
  : function (fn) { return setTimeout(fn, 16); };
67
72
 
@@ -85,6 +90,12 @@
85
90
  }
86
91
 
87
92
  function Accordion(root, opts) {
93
+ /* 🔴 同一 root 只允许一个实例。
94
+ 不加这条时 `new Accordion(root)` 两次会留下**两份** click 监听
95
+ (旧实例不会被回收:它的监听器闭包referencing this,还挂在 root 上)
96
+ ⇒ 一次点击 toggle 两次,肉眼看到的是"点了没反应"。
97
+ 构造器返回对象是 JS 合法行为,会覆盖 `this`,调用方拿到的就是旧实例。 */
98
+ if (root && root.accordionInstance) return root.accordionInstance;
88
99
  this.root = root;
89
100
  this.opts = opts || {};
90
101
  this.items = toArray(root.querySelectorAll('.accordion__item'));
@@ -351,19 +362,28 @@
351
362
  // 这是「用原生控件」的又一处收益:**无障碍行为免费且必然正确**。
352
363
  };
353
364
 
354
- window.Accordion = Accordion;
365
+ global.Accordion = Accordion;
355
366
 
356
367
  function init() {
357
368
  var nodes = document.querySelectorAll('[data-accordion]');
358
369
  for (var i = 0; i < nodes.length; i++) {
370
+ /* 🔴 幂等:已经初始化过就跳过。
371
+ 不加守卫的后果是**静默的**:第二次 new 会把 accordionInstance 覆盖成新实例,
372
+ 而**旧实例挂在 root 上的 click 监听还在** ⇒ 一次点击 toggle 两次
373
+ (展开又收起,看上去"点了没反应")。
374
+ SPA 路由切换、HMR、重复引入同一份脚本都会触发这条路。 */
375
+ if (nodes[i].accordionInstance) continue;
359
376
  nodes[i].accordionInstance = new Accordion(nodes[i]);
360
377
  }
361
378
  }
362
379
 
363
380
  // @autoinit: [data-accordion] —— 自动初始化;与 API.md「自动初始化」表由 autoinit-gate 双向校验
381
+ // ⚠️ 无 DOM 环境(SSR / 构建期 / Node 里 import)不自动初始化。
382
+ // 之前这里直接摸 document ⇒ Next.js 等 SSR 里 import 即 ReferenceError。
383
+ if (typeof document === 'undefined') return;
364
384
  if (document.readyState === 'loading') {
365
385
  document.addEventListener('DOMContentLoaded', init);
366
386
  } else {
367
387
  init();
368
388
  }
369
- })();
389
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -15,7 +15,6 @@ p { margin: 0 0 var(--sp-3); }
15
15
  </head>
16
16
  <body class="page page">
17
17
  <main class="wrap">
18
- <!-- demo-claim:skip 两块都可量(button 的个数、面板里链接的高度),但这一页仍是单栏结构,升级成 cmp 要重排整页 —— 本轮没做,已知欠账 -->
19
18
  <h1>accordion · 折叠面板</h1>
20
19
  <p class="lede">
21
20
  用于把长表单或长内容分成几段。<strong>默认只开一个</strong>(同一个问题的不同答案),
@@ -200,36 +199,76 @@ p { margin: 0 0 var(--sp-3); }
200
199
  </p>
201
200
  </div>
202
201
 
203
- <h2 class="rule-top">反例</h2>
204
- <div class="box bad">
205
- <h3>反例:用 div 当 trigger</h3>
206
- <div class="accordion">
207
- <div class="accordion__item">
208
- <div class="accordion__heading">
209
- <div class="accordion__trigger">用 div 写的标题(Tab 不到,也念不出状态)</div>
202
+ <h2 class="rule-top">对照:两处最容易写错的地方</h2>
203
+ <div class="cmp">
204
+ <div class="cmp__pane cmp__bad" data-bad="button|count|=0">
205
+ <h3>❌ 用 div 当 trigger</h3>
206
+ <div class="accordion">
207
+ <div class="accordion__item">
208
+ <div class="accordion__heading">
209
+ <div class="accordion__trigger">
210
+ <span class="accordion__label">数据源与口径</span>
211
+ <span class="accordion__marker" aria-hidden="true"></span>
212
+ </div>
213
+ </div>
210
214
  </div>
211
215
  </div>
216
+ <p class="cmp__why">
217
+ <code>&lt;div class="accordion__trigger"&gt;</code> 键盘上 Tab 不到、
218
+ 也不响应 Enter/Space —— 它<strong>一个 button 都不是</strong>。
219
+ APG 要求 header 里是 <code>role="button"</code> 的元素,
220
+ 用原生 <code>&lt;button&gt;</code> 最省事也最可靠。
221
+ </p>
222
+ </div>
223
+
224
+ <div class="cmp__pane cmp__good" data-good="button|count|=1">
225
+ <h3>✅ heading 包住原生 button</h3>
226
+ <div class="accordion">
227
+ <div class="accordion__item">
228
+ <h3 class="accordion__heading">
229
+ <button class="accordion__trigger" aria-expanded="false">
230
+ <span class="accordion__label">数据源与口径</span>
231
+ <span class="accordion__marker" aria-hidden="true"></span>
232
+ </button>
233
+ </h3>
234
+ </div>
235
+ </div>
236
+ <p class="cmp__why">
237
+ 同一个外观,一个按钮元素。Enter / Space 由浏览器负责 ——
238
+ 本组件的 JS <strong>一行 keydown 都没写</strong>,写了反而会双触发。
239
+ </p>
212
240
  </div>
213
- <p class="small">
214
- <code>&lt;div class="accordion__trigger"&gt;</code> 键盘上 Tab 不到、
215
- 也不响应 Enter/Space。<strong>APG 要求 header 里是
216
- <code>role="button"</code> 的元素</strong>,用原生
217
- <code>&lt;button&gt;</code> 最省事也最可靠。
218
- </p>
219
241
  </div>
220
- <div class="box bad">
221
- <h3>反例:折叠时只用 max-height: 0</h3>
222
- <div id="acc-bad" style="max-height:0; overflow:hidden">
223
- <p class="small" style="margin:0 0 var(--sp-2)">这一整段文字,读屏此刻仍然念得出来。</p>
224
- <a href="#acc-bad">里面的这个链接,Tab 也仍然进得去</a>
242
+
243
+ <div class="cmp">
244
+ <div class="cmp__pane cmp__bad" data-bad="a|focus|=1">
245
+ <h3>❌ 折叠时只用 max-height: 0</h3>
246
+ <div id="acc-bad" style="max-height:0; overflow:hidden">
247
+ <p class="small" style="margin:0 0 var(--sp-2)">这一整段文字,读屏此刻仍然念得出来。</p>
248
+ <a href="#acc-bad">里面的这个链接,Tab 也仍然进得去</a>
249
+ </div>
250
+ <p class="cmp__why">
251
+ <strong>这一栏看起来和右边一样空 —— 差别不在视觉上,在第 1 次按 Tab 之后。</strong>
252
+ 这块的高度被压成了 <code>0</code>,你<strong>看不见它</strong>,
253
+ 但按一下 Tab,焦点会停在里面的链接上;读屏也会把那句"仍然念得出来"念完。
254
+ 内容在无障碍树里<strong>仍然活着</strong>,只是视觉上消失。
255
+ </p>
256
+ </div>
257
+
258
+ <div class="cmp__pane cmp__good" data-good="a|focus|=0">
259
+ <h3>✅ hidden / display:none</h3>
260
+ <p class="small" style="margin:0 0 var(--sp-2)">
261
+ (这一栏故意是空的:内容被 <code>hidden</code> 之后连占位都不留,这正是重点。)
262
+ </p>
263
+ <div hidden>
264
+ <p class="small" style="margin:0 0 var(--sp-2)">这一整段文字,读屏此刻仍然念得出来。</p>
265
+ <a href="#acc-good">里面的这个链接,Tab 也仍然进得去</a>
266
+ </div>
267
+ <p class="cmp__why">
268
+ 同样看不见,但内容<strong>真的离开了无障碍树</strong>:
269
+ Tab 到不了、读屏也不念。折叠面板收起时必须这样。
270
+ </p>
225
271
  </div>
226
- <p class="small">
227
- 上面那块的高度被压成了 <code>0</code>,你<strong>看不见它</strong> ——
228
- 但按一下 Tab,焦点会停在里面的链接上;读屏也会把那句"仍然念得出来"念完。
229
- 内容在无障碍树里<strong>仍然存在</strong>,只是视觉上消失:
230
- <strong>必须用 <code>hidden</code> 或 <code>display:none</code></strong>,
231
- 让它真正离开无障碍树。
232
- </p>
233
272
  </div>
234
273
  </main>
235
274
  <script src="accordion.js"></script>
@@ -255,4 +255,4 @@
255
255
  }
256
256
 
257
257
  global.Drawer = { open: open };
258
- })(window);
258
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -448,4 +448,4 @@
448
448
  }
449
449
 
450
450
  global.Dropdown = { create: create };
451
- })(window);
451
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -42,7 +42,6 @@ p.small { font-size: var(--fs-xs); color: var(--text-tertiary); line-height: 1.6
42
42
  </head>
43
43
  <body class="page page">
44
44
  <main class="wrap">
45
- <!-- demo-claim:skip 两块反例的差别都是时序(什么时候报错)和措辞(文案怎么说),没有可量的元素属性 -->
46
45
 
47
46
  <h1>表单校验</h1>
48
47
  <p class="lede">
@@ -56,7 +55,7 @@ p.small { font-size: var(--fs-xs); color: var(--text-tertiary); line-height: 1.6
56
55
  </p>
57
56
 
58
57
  <div class="grid2" style="margin-top:var(--sp-4)">
59
- <div class="box bad">
58
+ <div class="box bad" data-claim-skip="差别是「什么时候报错」—— 时序。两个输入框在任何一个静态时刻的几何与文本都相同,只有交互过程中才分得出来,而 demo-claim 量的是静态 DOM">
60
59
  <h3>反例:oninput 立即报错</h3>
61
60
  <div class="field">
62
61
  <label class="field__label" for="naive">金额</label>
@@ -127,7 +126,7 @@ p.small { font-size: var(--fs-xs); color: var(--text-tertiary); line-height: 1.6
127
126
 
128
127
  <h2 class="rule-bottom">三、错误文案:给建议,不给判断</h2>
129
128
  <div class="grid2">
130
- <div class="box bad">
129
+ <div class="box bad" data-claim-skip="两栏都是三条错误文案(uniq 都是 3、字号行高全同),差别只在措辞本身 —— 机器判不了哪句更清楚,只能人读">
131
130
  <h3>反例</h3>
132
131
  <p class="naive-error">格式错误</p>
133
132
  <p class="naive-error">非法输入</p>
@@ -198,4 +198,4 @@
198
198
 
199
199
  global.FLIP = { flip: flip, remove: removeWithAnimation };
200
200
 
201
- })(window);
201
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -59,7 +59,7 @@
59
59
  * 手动:new Nav(rootEl, { labels: { open: '打开菜单', close: '关闭菜单' } })
60
60
  * 事件:fl-nav-open / fl-nav-close(detail = { root: <nav> })
61
61
  */
62
- (function () {
62
+ (function (global) {
63
63
  'use strict';
64
64
 
65
65
  /* ==== BEHAVIOR INJECT BEGIN: emit ==== */
@@ -413,7 +413,7 @@
413
413
  return root.navInstance;
414
414
  };
415
415
 
416
- window.Nav = Nav;
416
+ global.Nav = Nav;
417
417
 
418
418
  function init() {
419
419
  each(document.querySelectorAll('[data-nav]'), function (el) {
@@ -422,9 +422,12 @@
422
422
  }
423
423
 
424
424
  // @autoinit: [data-nav] —— 扫属性自动建实例(详见 README「自动初始化」)
425
+ // ⚠️ 无 DOM 环境(SSR / 构建期 / Node 里 import)不自动初始化。
426
+ // 之前这里直接摸 document ⇒ Next.js 等 SSR 里 import 即 ReferenceError。
427
+ if (typeof document === 'undefined') return;
425
428
  if (document.readyState === 'loading') {
426
429
  document.addEventListener('DOMContentLoaded', init);
427
430
  } else {
428
431
  init();
429
432
  }
430
- })();
433
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -108,4 +108,4 @@
108
108
 
109
109
  global.Toc = { init: init };
110
110
 
111
- })(window);
111
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -42,7 +42,6 @@ p.small { font-size: var(--fs-xs); color: var(--text-tertiary); line-height: 1.6
42
42
  </head>
43
43
  <body class="page page">
44
44
  <main class="wrap">
45
- <!-- demo-claim:skip 焦点落点与「点遮罩关不关」都是运行时行为,静态块里摆不出「错的那一版」 -->
46
45
 
47
46
  <h1>Toast vs Dialog</h1>
48
47
  <p class="lede">
@@ -112,7 +111,7 @@ p.small { font-size: var(--fs-xs); color: var(--text-tertiary); line-height: 1.6
112
111
 
113
112
  <h2 class="rule-bottom">用错比做丑严重得多</h2>
114
113
  <div class="two">
115
- <div class="box bad aside-col">
114
+ <div class="box bad aside-col" data-claim-skip="左栏是一个 toast 标本,右栏是一条判断规则(没有人能数的话)—— 两栏根本不是同一种东西,没有可比的量;而且「该 toast 还是 dialog」取决于要不要用户先确认,没有阈值可划">
116
115
  <h3 style="margin:0 0 var(--sp-3); font-size:var(--fs-sm); font-weight:500; color:var(--danger)">
117
116
  ❌ 错用</h3>
118
117
  <div class="toast toast--error" role="status" style="margin:0 0 var(--sp-3)">
@@ -560,4 +560,4 @@
560
560
 
561
561
  global.Overlay = { toast: toast, dialog: dialog };
562
562
 
563
- })(window);
563
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -69,7 +69,7 @@ th { font-size: var(--fs-xs); color: var(--text-secondary); font-weight: 500; }
69
69
  </div>
70
70
 
71
71
  <h2 class="rule-top">2 · 结构:六条硬要求</h2>
72
- <table>
72
+ <table aria-label="分页容器的六条结构要求与理由">
73
73
  <tr><th>要求</th><th>为什么</th></tr>
74
74
  <tr><td>包在 <code>&lt;nav aria-label&gt;</code> 里</td>
75
75
  <td>页面常有多个 nav(主导航/面包屑/分页),
@@ -35,7 +35,7 @@
35
35
  * });
36
36
  * </script>
37
37
  */
38
- (function () {
38
+ (function (global) {
39
39
  'use strict';
40
40
 
41
41
  /* ==== BEHAVIOR INJECT BEGIN: emit ==== */
@@ -385,11 +385,17 @@
385
385
  },
386
386
  };
387
387
 
388
- if (typeof window !== 'undefined') window.Pagination = API;
388
+ /* global 已在 IIFE 收尾处解析成 window 或 globalThis(无 DOM 环境),
389
+ 所以这里**无条件挂**——再套一层 `typeof window !== 'undefined'` 会让
390
+ SSR 里 require 得到 undefined(`esm/` 包装就取不到 API)。 */
391
+ global.Pagination = API;
389
392
  // @autoinit: [data-pagination] —— 扫属性自动 update()(详见 API.md「自动初始化」)
393
+ // ⚠️ 无 DOM 环境(SSR / 构建期 / Node 里 import)不自动初始化。
394
+ // 之前这里直接摸 document ⇒ Next.js 等 SSR 里 import 即 ReferenceError。
395
+ if (typeof document === 'undefined') return;
390
396
  if (document.readyState === 'loading') {
391
397
  document.addEventListener('DOMContentLoaded', function () { API.update(); });
392
398
  } else {
393
399
  API.update();
394
400
  }
395
- })();
401
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -164,9 +164,12 @@
164
164
  }
165
165
 
166
166
  // @autoinit: conditional[data-reveal-auto] —— 只有 <html data-reveal-auto> 才 enable();不写则完全不动
167
+ // ⚠️ 无 DOM 环境(SSR / 构建期 / Node 里 import)不自动初始化。
168
+ // 之前这里直接摸 document ⇒ Next.js 等 SSR 里 import 即 ReferenceError。
169
+ if (typeof document === 'undefined') return;
167
170
  if (document.readyState === 'loading') {
168
171
  document.addEventListener('DOMContentLoaded', auto, false);
169
172
  } else {
170
173
  auto();
171
174
  }
172
- }(this));
175
+ })(typeof window !== 'undefined' ? window : globalThis);