agentcss 0.0.0-stage → 0.11.2

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 (274) hide show
  1. package/01-tokens/README.md +236 -0
  2. package/01-tokens/behavior/dismissable.js +132 -0
  3. package/01-tokens/behavior/emit.js +76 -0
  4. package/01-tokens/behavior/focus-return.js +81 -0
  5. package/01-tokens/behavior/roving.js +152 -0
  6. package/01-tokens/behavior/typeahead.js +124 -0
  7. package/01-tokens/focus-ring-demo.html +133 -0
  8. package/01-tokens/focus-ring.css +102 -0
  9. package/01-tokens/page.css +244 -0
  10. package/01-tokens/polyfill.js +116 -0
  11. package/01-tokens/responsive-demo.html +171 -0
  12. package/01-tokens/scrollbar-demo.html +194 -0
  13. package/01-tokens/scrollbar.css +179 -0
  14. package/01-tokens/theme-toggle.js +103 -0
  15. package/01-tokens/tokens.css +972 -0
  16. package/01-tokens/typography.css +156 -0
  17. package/02-primitives/badge/README.md +71 -0
  18. package/02-primitives/badge/badge.css +141 -0
  19. package/02-primitives/badge/demo.html +101 -0
  20. package/02-primitives/button/README.md +99 -0
  21. package/02-primitives/button/button.css +353 -0
  22. package/02-primitives/button/demo.html +200 -0
  23. package/02-primitives/card/README.md +145 -0
  24. package/02-primitives/card/card.css +246 -0
  25. package/02-primitives/card/demo.html +170 -0
  26. package/02-primitives/choice/README.md +161 -0
  27. package/02-primitives/choice/choice.css +395 -0
  28. package/02-primitives/choice/demo.html +277 -0
  29. package/02-primitives/combobox/README.md +79 -0
  30. package/02-primitives/combobox/combobox.css +223 -0
  31. package/02-primitives/combobox/combobox.js +485 -0
  32. package/02-primitives/combobox/demo.html +109 -0
  33. package/02-primitives/date-range/README.md +100 -0
  34. package/02-primitives/date-range/date-range.css +206 -0
  35. package/02-primitives/date-range/date-range.js +301 -0
  36. package/02-primitives/date-range/demo.html +104 -0
  37. package/02-primitives/input/README.md +170 -0
  38. package/02-primitives/input/demo.html +246 -0
  39. package/02-primitives/input/input.css +239 -0
  40. package/02-primitives/popover/README.md +80 -0
  41. package/02-primitives/popover/demo.html +150 -0
  42. package/02-primitives/popover/popover.css +187 -0
  43. package/02-primitives/popover/popover.js +331 -0
  44. package/02-primitives/progress/README.md +82 -0
  45. package/02-primitives/progress/demo.html +154 -0
  46. package/02-primitives/progress/progress.css +172 -0
  47. package/02-primitives/select/README.md +95 -0
  48. package/02-primitives/select/demo.html +121 -0
  49. package/02-primitives/select/select.css +151 -0
  50. package/02-primitives/select/select.js +549 -0
  51. package/02-primitives/separator/README.md +63 -0
  52. package/02-primitives/separator/demo.html +84 -0
  53. package/02-primitives/separator/separator.css +73 -0
  54. package/02-primitives/skeleton/README.md +69 -0
  55. package/02-primitives/skeleton/demo.html +110 -0
  56. package/02-primitives/skeleton/skeleton.css +96 -0
  57. package/02-primitives/switch/README.md +65 -0
  58. package/02-primitives/switch/demo.html +115 -0
  59. package/02-primitives/switch/switch.css +370 -0
  60. package/03-patterns/accordion/README.md +121 -0
  61. package/03-patterns/accordion/accordion.css +453 -0
  62. package/03-patterns/accordion/accordion.js +369 -0
  63. package/03-patterns/accordion/demo.html +223 -0
  64. package/03-patterns/content/README.md +103 -0
  65. package/03-patterns/content/content.css +286 -0
  66. package/03-patterns/content/demo.html +88 -0
  67. package/03-patterns/drawer/README.md +79 -0
  68. package/03-patterns/drawer/demo.html +101 -0
  69. package/03-patterns/drawer/drawer.css +129 -0
  70. package/03-patterns/drawer/drawer.js +258 -0
  71. package/03-patterns/dropdown/README.md +78 -0
  72. package/03-patterns/dropdown/demo.html +101 -0
  73. package/03-patterns/dropdown/dropdown.css +135 -0
  74. package/03-patterns/dropdown/dropdown.js +451 -0
  75. package/03-patterns/form-validation/README.md +147 -0
  76. package/03-patterns/form-validation/demo.html +280 -0
  77. package/03-patterns/form-validation/form-validation.css +263 -0
  78. package/03-patterns/list/README.md +150 -0
  79. package/03-patterns/list/demo.html +225 -0
  80. package/03-patterns/list/flip.js +201 -0
  81. package/03-patterns/list/list.css +162 -0
  82. package/03-patterns/nav/README.md +225 -0
  83. package/03-patterns/nav/demo.html +131 -0
  84. package/03-patterns/nav/nav.css +314 -0
  85. package/03-patterns/nav/nav.js +430 -0
  86. package/03-patterns/nav/toc.js +111 -0
  87. package/03-patterns/overlay/README.md +196 -0
  88. package/03-patterns/overlay/demo.html +241 -0
  89. package/03-patterns/overlay/overlay.css +319 -0
  90. package/03-patterns/overlay/overlay.js +563 -0
  91. package/03-patterns/pagination/README.md +114 -0
  92. package/03-patterns/pagination/demo.html +165 -0
  93. package/03-patterns/pagination/pagination.css +195 -0
  94. package/03-patterns/pagination/pagination.js +395 -0
  95. package/03-patterns/reveal/README.md +91 -0
  96. package/03-patterns/reveal/demo.html +106 -0
  97. package/03-patterns/reveal/reveal.css +93 -0
  98. package/03-patterns/reveal/reveal.js +172 -0
  99. package/03-patterns/states/README.md +141 -0
  100. package/03-patterns/states/demo.html +240 -0
  101. package/03-patterns/states/states.css +252 -0
  102. package/03-patterns/tabs/README.md +84 -0
  103. package/03-patterns/tabs/demo.html +139 -0
  104. package/03-patterns/tabs/tabs.css +172 -0
  105. package/03-patterns/tabs/tabs.js +307 -0
  106. package/03-patterns/tooltip/README.md +96 -0
  107. package/03-patterns/tooltip/demo.html +78 -0
  108. package/03-patterns/tooltip/tooltip.css +89 -0
  109. package/03-patterns/tooltip/tooltip.js +288 -0
  110. package/03-patterns/tree/README.md +94 -0
  111. package/03-patterns/tree/demo.html +138 -0
  112. package/03-patterns/tree/tree.css +113 -0
  113. package/03-patterns/tree/tree.js +456 -0
  114. package/04-recipes/analysis-report/README.md +80 -0
  115. package/04-recipes/analysis-report/analysis-report.css +187 -0
  116. package/04-recipes/analysis-report/demo.html +274 -0
  117. package/04-recipes/data-showcase/README.md +45 -0
  118. package/04-recipes/data-showcase/demo.html +247 -0
  119. package/04-recipes/detail-page/README.md +93 -0
  120. package/04-recipes/detail-page/demo.html +55 -0
  121. package/04-recipes/detail-page/detail-page.css +190 -0
  122. package/04-recipes/detail-page/full.html +60 -0
  123. package/04-recipes/detail-page/null.html +60 -0
  124. package/04-recipes/detail-page/partial.html +60 -0
  125. package/04-recipes/detail-page/verify-skeleton.py +19 -0
  126. package/04-recipes/longform/README.md +93 -0
  127. package/04-recipes/longform/agc-memory.svg +4 -0
  128. package/04-recipes/longform/content.html +610 -0
  129. package/04-recipes/longform/content.md +51 -0
  130. package/04-recipes/longform/longform.html +627 -0
  131. package/04-recipes/report/README.md +176 -0
  132. package/04-recipes/report/demo.html +355 -0
  133. package/04-recipes/report/report.css +364 -0
  134. package/04-recipes/table/README.md +103 -0
  135. package/04-recipes/table/demo.html +455 -0
  136. package/04-recipes/table/table.css +372 -0
  137. package/04-recipes/tierA-tokens.css +189 -0
  138. package/09-assets/README.md +74 -0
  139. package/09-assets/bar/README.md +94 -0
  140. package/09-assets/bar/bar.css +166 -0
  141. package/09-assets/bar/bar.js +320 -0
  142. package/09-assets/bar/demo.html +176 -0
  143. package/09-assets/echarts-adapter/demo.html +192 -0
  144. package/09-assets/echarts-adapter/echarts-adapter.js +277 -0
  145. package/09-assets/model-viewer/README.md +155 -0
  146. package/09-assets/model-viewer/demo.html +105 -0
  147. package/09-assets/model-viewer/model-viewer.css +143 -0
  148. package/09-assets/model-viewer/model-viewer.js +423 -0
  149. package/09-assets/scientific-plot/README.md +94 -0
  150. package/09-assets/scientific-plot/demo.html +252 -0
  151. package/09-assets/scientific-plot/scientific-plot.css +141 -0
  152. package/09-assets/scientific-plot/scientific-plot.js +610 -0
  153. package/09-assets/sparkline/README.md +153 -0
  154. package/09-assets/sparkline/demo.html +163 -0
  155. package/09-assets/sparkline/sparkline.css +158 -0
  156. package/09-assets/sparkline/sparkline.js +508 -0
  157. package/API.md +332 -0
  158. package/CHANGELOG.md +3116 -0
  159. package/CODE_OF_CONDUCT.md +64 -0
  160. package/CONTRIBUTING.md +89 -0
  161. package/ERRORS.md +1658 -0
  162. package/LICENSE +21 -0
  163. package/README.md +491 -2
  164. package/SECURITY.md +72 -0
  165. package/START-HERE.md +164 -0
  166. package/SUPPORT.md +109 -0
  167. package/adapters/presentation/README.md +113 -0
  168. package/adapters/presentation/check.js +101 -0
  169. package/adapters/presentation/deck.schema.json +74 -0
  170. package/adapters/presentation/to-doc.js +138 -0
  171. package/ai/START-HERE.md +104 -0
  172. package/ai/behaviors.json +380 -0
  173. package/ai/behaviors.schema.json +61 -0
  174. package/ai/capabilities.json +56 -0
  175. package/ai/cli.js +387 -0
  176. package/ai/components.json +1944 -0
  177. package/ai/components.meta.json +168 -0
  178. package/ai/components.schema.json +220 -0
  179. package/ai/contract.schema.json +147 -0
  180. package/ai/deprecations.json +55 -0
  181. package/ai/deprecations.schema.json +85 -0
  182. package/ai/motion-policy.json +22 -0
  183. package/ai/patch.js +323 -0
  184. package/ai/patch.schema.json +63 -0
  185. package/ai/profiles.json +92 -0
  186. package/ai/tokens.audience.json +55 -0
  187. package/ai/tokens.audience.schema.json +73 -0
  188. package/ai/tokens.json +762 -0
  189. package/ai/tokens.schema.json +58 -0
  190. package/ai/tokens.tree.json +752 -0
  191. package/ai/tokens.tree.schema.json +56 -0
  192. package/ai/validate.js +383 -0
  193. package/dist/01-tokens/behavior/dismissable.js +60 -0
  194. package/dist/01-tokens/behavior/emit.js +21 -0
  195. package/dist/01-tokens/behavior/focus-return.js +31 -0
  196. package/dist/01-tokens/behavior/roving.js +77 -0
  197. package/dist/01-tokens/behavior/typeahead.js +61 -0
  198. package/dist/01-tokens/focus-ring.css +1 -0
  199. package/dist/01-tokens/page.css +1 -0
  200. package/dist/01-tokens/polyfill.js +78 -0
  201. package/dist/01-tokens/scrollbar.css +1 -0
  202. package/dist/01-tokens/theme-toggle.js +61 -0
  203. package/dist/01-tokens/tokens.css +1 -0
  204. package/dist/01-tokens/typography.css +1 -0
  205. package/dist/02-primitives/badge/badge.css +1 -0
  206. package/dist/02-primitives/button/button.css +1 -0
  207. package/dist/02-primitives/card/card.css +1 -0
  208. package/dist/02-primitives/choice/choice.css +1 -0
  209. package/dist/02-primitives/combobox/combobox.css +1 -0
  210. package/dist/02-primitives/combobox/combobox.js +439 -0
  211. package/dist/02-primitives/date-range/date-range.css +1 -0
  212. package/dist/02-primitives/date-range/date-range.js +267 -0
  213. package/dist/02-primitives/input/input.css +1 -0
  214. package/dist/02-primitives/popover/popover.css +1 -0
  215. package/dist/02-primitives/popover/popover.js +257 -0
  216. package/dist/02-primitives/progress/progress.css +1 -0
  217. package/dist/02-primitives/select/select.css +1 -0
  218. package/dist/02-primitives/select/select.js +449 -0
  219. package/dist/02-primitives/separator/separator.css +1 -0
  220. package/dist/02-primitives/skeleton/skeleton.css +1 -0
  221. package/dist/02-primitives/switch/switch.css +1 -0
  222. package/dist/03-patterns/accordion/accordion.css +1 -0
  223. package/dist/03-patterns/accordion/accordion.js +219 -0
  224. package/dist/03-patterns/content/content.css +1 -0
  225. package/dist/03-patterns/drawer/drawer.css +1 -0
  226. package/dist/03-patterns/drawer/drawer.js +229 -0
  227. package/dist/03-patterns/dropdown/dropdown.css +1 -0
  228. package/dist/03-patterns/dropdown/dropdown.js +365 -0
  229. package/dist/03-patterns/form-validation/form-validation.css +1 -0
  230. package/dist/03-patterns/list/flip.js +117 -0
  231. package/dist/03-patterns/list/list.css +1 -0
  232. package/dist/03-patterns/nav/nav.css +1 -0
  233. package/dist/03-patterns/nav/nav.js +322 -0
  234. package/dist/03-patterns/nav/toc.js +76 -0
  235. package/dist/03-patterns/overlay/overlay.css +1 -0
  236. package/dist/03-patterns/overlay/overlay.js +398 -0
  237. package/dist/03-patterns/pagination/pagination.css +1 -0
  238. package/dist/03-patterns/pagination/pagination.js +273 -0
  239. package/dist/03-patterns/reveal/reveal.css +1 -0
  240. package/dist/03-patterns/reveal/reveal.js +138 -0
  241. package/dist/03-patterns/states/states.css +1 -0
  242. package/dist/03-patterns/tabs/tabs.css +1 -0
  243. package/dist/03-patterns/tabs/tabs.js +242 -0
  244. package/dist/03-patterns/tooltip/tooltip.css +1 -0
  245. package/dist/03-patterns/tooltip/tooltip.js +213 -0
  246. package/dist/03-patterns/tree/tree.css +1 -0
  247. package/dist/03-patterns/tree/tree.js +383 -0
  248. package/dist/04-recipes/analysis-report/analysis-report.css +1 -0
  249. package/dist/04-recipes/detail-page/detail-page.css +1 -0
  250. package/dist/04-recipes/report/report.css +1 -0
  251. package/dist/04-recipes/table/table.css +1 -0
  252. package/dist/04-recipes/tierA-tokens.css +1 -0
  253. package/dist/09-assets/bar/bar.css +1 -0
  254. package/dist/09-assets/bar/bar.js +222 -0
  255. package/dist/09-assets/echarts-adapter/echarts-adapter.js +185 -0
  256. package/dist/09-assets/model-viewer/model-viewer.css +1 -0
  257. package/dist/09-assets/model-viewer/model-viewer.js +320 -0
  258. package/dist/09-assets/scientific-plot/scientific-plot.css +1 -0
  259. package/dist/09-assets/scientific-plot/scientific-plot.js +568 -0
  260. package/dist/09-assets/sparkline/sparkline.css +1 -0
  261. package/dist/09-assets/sparkline/sparkline.js +265 -0
  262. package/dist/COSTS.md +56 -0
  263. package/docs/AI-CONTRACT.md +197 -0
  264. package/docs/BACKLOG.md +127 -0
  265. package/docs/BENCHMARK.md +252 -0
  266. package/docs/GUIDANCE.md +154 -0
  267. package/docs/INVARIANT.md +372 -0
  268. package/docs/LAYER-BASELINE.md +158 -0
  269. package/docs/RELEASING.md +275 -0
  270. package/docs/RFC-THEME-SCOPE.md +141 -0
  271. package/docs/ROADMAP.md +182 -0
  272. package/index.html +82 -0
  273. package/package.json +107 -4
  274. package/types/index.d.ts +310 -0
@@ -0,0 +1,277 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <link rel="icon" href="data:,">
7
+ <title>checkbox / radio / select · 键盘行为验证</title>
8
+ <link rel="stylesheet" href="../../01-tokens/tokens.css">
9
+ <link rel="stylesheet" href="../../01-tokens/typography.css">
10
+ <link rel="stylesheet" href="../../01-tokens/page.css">
11
+ <link rel="stylesheet" href="../../02-primitives/input/input.css">
12
+ <link rel="stylesheet" href="choice.css">
13
+ <style>
14
+ /* 对比演示卡:底色用中性,语义色只留在标题文字上 */
15
+ .bad { background: var(--surface-sunken); }
16
+ .good { background: var(--surface-sunken); }
17
+ .bad h3 { color: var(--danger); }
18
+ .good h3 { color: var(--success); }
19
+ p.small { font-size: var(--fs-xs); color: var(--text-tertiary); line-height: 1.6; }
20
+ .kbd { display:inline-block; padding: 2px 7px; border: 1px solid var(--border-control);
21
+ border-bottom-width: 2px; border-radius: 5px; background: var(--surface);
22
+ font-family: var(--mono); /* 原为写死的 ui-monospace,统一走令牌 */ font-size: 11px; color: var(--text-primary); }
23
+ /* 实时状态面板 */
24
+ #scope { position: sticky; top: var(--sp-3); z-index: var(--z-sticky); /* 🔴*/
25
+ background: var(--surface); border: 1px solid var(--accent);
26
+ border-radius: var(--r-md); padding: var(--sp-4) var(--sp-5);
27
+ font-family: var(--mono); /* 原为写死的 ui-monospace,统一走令牌 */ font-size: var(--fs-xs);
28
+ line-height: 1.9; box-shadow: var(--shadow-2); }
29
+ #scope b { color: var(--accent); }
30
+ .log { color: var(--text-tertiary); }
31
+ </style>
32
+ </head>
33
+ <body class="page page">
34
+ <main class="wrap">
35
+
36
+ <h1>checkbox / radio / select</h1>
37
+ <p class="lede">
38
+ 全库里最容易做坏的一组。浏览器的键盘行为是<strong>无障碍的地基</strong>——
39
+ 改坏了用鼠标完全测不出来,只有键盘用户会知道。
40
+ </p>
41
+
42
+ <div class="note">
43
+ <strong>怎么验:</strong>先把鼠标移开,用 <span class="kbd">Tab</span> 走进下面每一个控件,
44
+ 用 <span class="kbd">Space</span> 切换勾选,用 <span class="kbd">↑</span><span class="kbd">↓</span> 切换 radio。
45
+ 顶上那个面板会实时显示每一次键盘事件和焦点状态——那不是模拟的,是原生行为。
46
+ </div>
47
+
48
+ <div id="scope">
49
+ <div>焦点元素:<b id="s-focus">(还没聚焦任何控件)</b></div>
50
+ <div>选中状态:<b id="s-checked">—</b></div>
51
+ <div class="log" id="s-log">按 Tab 开始。事件会显示在这里。</div>
52
+ </div>
53
+
54
+ <h2 class="rule-bottom">checkbox</h2>
55
+ <div class="box">
56
+ <div class="choice choice--checkbox">
57
+ <input class="choice__input" type="checkbox" id="c1">
58
+ <label class="choice__label" for="c1">
59
+ <span class="choice__mark"></span>
60
+ <span>导入时跳过表头行</span>
61
+ </label>
62
+ </div>
63
+ <p class="small" style="margin-top:var(--sp-3)">
64
+ 整个 label 都能点(input 透明铺满了它),但视觉方框只有 20px——<strong>触摸目标是 44px</strong>。
65
+ </p>
66
+ </div>
67
+
68
+ <h2 class="rule-bottom">indeterminate(第三态)</h2>
69
+ <div class="box">
70
+ <div class="choice choice--checkbox">
71
+ <input class="choice__input" type="checkbox" id="c2">
72
+ <label class="choice__label" for="c2">
73
+ <span class="choice__mark"></span>
74
+ <span>全选(下面 3 项)</span>
75
+ </label>
76
+ </div>
77
+ <div style="margin-left:var(--sp-5)">
78
+ <div class="choice choice--checkbox">
79
+ <input class="choice__input" type="checkbox" checked id="ch-1">
80
+ <label class="choice__label" for="ch-1">
81
+ <span class="choice__mark"></span>
82
+ <span>收入</span>
83
+ </label>
84
+ </div>
85
+ <div class="choice choice--checkbox">
86
+ <input class="choice__input" type="checkbox" id="ch-2">
87
+ <label class="choice__label" for="ch-2">
88
+ <span class="choice__mark"></span>
89
+ <span>成本</span>
90
+ </label>
91
+ </div>
92
+ <div class="choice choice--checkbox">
93
+ <input class="choice__input" type="checkbox" checked id="ch-3">
94
+ <label class="choice__label" for="ch-3">
95
+ <span class="choice__mark"></span>
96
+ <span>费用</span>
97
+ </label>
98
+ </div>
99
+ </div>
100
+ <p class="small" style="margin-top:var(--sp-3)">
101
+ "全选"框初始是<strong>半选</strong>(横线),因为下面只选了 2 个。
102
+ 不做这个态,"全选"复选框就无法正确表达中间状态。
103
+ </p>
104
+ </div>
105
+
106
+ <h2 class="rule-bottom">radio 组(fieldset + legend)</h2>
107
+ <div class="box">
108
+ <fieldset class="choice__group">
109
+ <legend class="choice__legend">时间范围</legend>
110
+ <div class="choice__options">
111
+ <div class="choice choice--radio">
112
+ <input class="choice__input" type="radio" name="range" value="30" checked id="ch-4">
113
+ <label class="choice__label" for="ch-4">
114
+ <span class="choice__mark"></span>
115
+ <span>最近 30 天</span>
116
+ </label>
117
+ </div>
118
+ <div class="choice choice--radio">
119
+ <input class="choice__input" type="radio" name="range" value="90" id="ch-5">
120
+ <label class="choice__label" for="ch-5">
121
+ <span class="choice__mark"></span>
122
+ <span>最近 90 天</span>
123
+ </label>
124
+ </div>
125
+ <div class="choice choice--radio">
126
+ <input class="choice__input" type="radio" name="range" value="365" id="ch-6">
127
+ <label class="choice__label" for="ch-6">
128
+ <span class="choice__mark"></span>
129
+ <span>最近一年</span>
130
+ </label>
131
+ </div>
132
+ </div>
133
+ </fieldset>
134
+ <p class="small" style="margin-top:var(--sp-3)">
135
+ 用 <span class="kbd">↑</span><span class="kbd">↓</span> 在组内移动,
136
+ <strong>移动即选中</strong>——这是原生 radio 行为,<code>&lt;div&gt;</code> 模拟不出来。<br>
137
+ <code>fieldset</code> + <code>legend</code> 让屏幕阅读器播报"第几组、共几项、当前选中第几项"。
138
+ 换成 <code>&lt;div&gt;</code>+<code>&lt;span&gt;</code> 这些信息全丢。
139
+ </p>
140
+ </div>
141
+
142
+ <h2 class="rule-bottom">select</h2>
143
+ <div class="box">
144
+ <!-- 🔴 label 是必须的:没有它,屏幕阅读器只会读出"组合框"。
145
+ a11y.py 上线当天就抓到了这处缺失。 -->
146
+ <div class="field">
147
+ <label class="field__label" for="s1">时间范围</label>
148
+ <div class="select">
149
+ <select class="select__control" id="s1" aria-describedby="s1-hint">
150
+ <option>最近 30 天</option>
151
+ <option>最近 90 天</option>
152
+ <option>最近一年</option>
153
+ <option>自定义区间…</option>
154
+ </select>
155
+ <span class="select__arrow" aria-hidden="true">
156
+ <svg width="12" height="8" viewBox="0 0 12 8" fill="none" stroke="currentColor"
157
+ stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
158
+ <path d="M1 1.5 6 6.5 11 1.5"/>
159
+ </svg>
160
+ </span>
161
+ </div>
162
+ <span class="field__hint" id="s1-hint">上下键选择,Enter 确认,Esc 关闭</span>
163
+ </div>
164
+ <p class="small" style="margin-top:var(--sp-3)">
165
+ <strong>用原生 select,不做自定义下拉。</strong>
166
+ 移动端会弹出系统带 wheel 的原生选择器、语言跟随系统、无障碍由系统保证。
167
+ 换成 <code>&lt;div&gt;</code> 模拟,这些全部要重写且必然漏一部分。<br>
168
+ 这里只改了外观(<code>appearance:none</code> + 自绘箭头),
169
+ <span class="kbd">↑</span><span class="kbd">↓</span> 选择、<span class="kbd">Enter</span> 确认、Esc 关闭全部保留。
170
+ 箭头写了 <code>pointer-events:none</code>,否则它会吃掉点击。
171
+ </p>
172
+ </div>
173
+
174
+ <h2 class="rule-bottom">disabled</h2>
175
+ <div class="box">
176
+ <div class="choice choice--checkbox">
177
+ <input class="choice__input" type="checkbox" disabled id="ch-7">
178
+ <label class="choice__label" for="ch-7">
179
+ <span class="choice__mark"></span>
180
+ <span>导出 PDF(需要专业版)</span>
181
+ </label>
182
+ </div>
183
+ <p class="small" style="margin-top:var(--sp-3)">
184
+ 不用 <code>opacity</code>,用明确的低对比色组(对比度会变成不可控的随机数)。
185
+ <strong>disabled 之后 hover 不能有任何变化</strong>,否则看着像还能点。
186
+ </p>
187
+ </div>
188
+
189
+ <h2 class="rule-bottom">反例:用 display:none 隐藏原生控件</h2>
190
+ <div class="box bad">
191
+ <div class="choice choice--checkbox">
192
+ <input class="choice__input" type="checkbox" style="display:none" id="bad1">
193
+ <label class="choice__label" for="bad1">
194
+ <span class="choice__mark"></span>
195
+ <span>我看起来正常,但 Tab 到不了这里</span>
196
+ </label>
197
+ </div>
198
+ <p class="small" style="margin-top:var(--sp-3)">
199
+ <strong>用 Tab 试试——焦点会直接跳过它。</strong>
200
+ <code>display:none</code> 把它从 tab 顺序里移除了,键盘用户彻底到不了这里。
201
+ 鼠标点击还正常,所以这种 bug 用鼠标测不出来。<br><br>
202
+ 本库的 input 用的是 <code>opacity:0</code> + 绝对定位:<strong>看不见,但仍在 tab 顺序里</strong>。
203
+ </p>
204
+ </div>
205
+
206
+ <h2 class="rule-bottom">焦点环必须画在看得见的地方</h2>
207
+ <div class="box good">
208
+ <p class="small" style="margin:0 0 var(--sp-3)">
209
+ input 是 <code>opacity:0</code> 的,所以它自己的 <code>outline</code> 你<strong>根本看不见</strong>。
210
+ 焦点环必须画在紧邻的可见标记上:<br>
211
+ <code>.choice__input:focus-visible + .choice__mark</code><br><br>
212
+ 门禁也是这么查的——它<strong>只认 <code>:focus-visible +</code> 这种相邻写法</strong>,
213
+ 单纯写 <code>:focus-visible {}</code> 判为不通过。
214
+ </p>
215
+ <div class="choice choice--checkbox">
216
+ <input class="choice__input" type="checkbox" id="ch-8">
217
+ <label class="choice__label" for="ch-8">
218
+ <span class="choice__mark"></span>
219
+ <span>Tab 过来看焦点环</span>
220
+ </label>
221
+ </div>
222
+ </div>
223
+
224
+ <script>
225
+ // ---------- 实时显示原生键盘行为(不是模拟,是观测) ----------
226
+ var fEl = document.getElementById('s-focus');
227
+ var cEl = document.getElementById('s-checked');
228
+ var lEl = document.getElementById('s-log');
229
+
230
+ function desc(el) {
231
+ if (!el) return '(无)';
232
+ var t = el.tagName.toLowerCase();
233
+ var extra = '';
234
+ if (el.type === 'checkbox') extra = ' [checkbox' + (el.checked ? ' 已勾选' : '') + (el.indeterminate ? ' 半选' : '') + ']';
235
+ if (el.type === 'radio') extra = ' [radio ' + el.name + '=' + el.value + (el.checked ? ' 已选中' : '') + ']';
236
+ if (t === 'select-one') extra = ' [select]';
237
+ var lb = el.closest('label');
238
+ var lbtxt = lb ? lb.querySelector('.choice__label') : null;
239
+ return t + extra + ' “' + (lbtxt ? lbtxt.textContent.trim() : (el.options ? el.options[el.selectedIndex].text : el.id)) + '”';
240
+ }
241
+
242
+ document.addEventListener('focusin', function (e) {
243
+ fEl.textContent = desc(e.target);
244
+ cEl.textContent = (e.target.checked !== undefined) ? String(e.target.checked) : '(不适用)';
245
+ });
246
+
247
+ var n = 0;
248
+ document.addEventListener('keydown', function (e) {
249
+ if (['Tab', ' ', 'ArrowUp', 'ArrowDown', 'Enter', 'Escape'].indexOf(e.key) === -1) return;
250
+ n++;
251
+ lEl.textContent = '#' + n + ' 按下 ' + JSON.stringify(e.key === ' ' ? 'Space' : e.key) +
252
+ ' → 焦点仍在 ' + desc(document.activeElement);
253
+ });
254
+
255
+ document.addEventListener('change', function (e) {
256
+ lEl.textContent = '#' + (++n) + ' ' + desc(e.target) + ' 状态变为 ' +
257
+ (e.target.checked !== undefined ? (e.target.checked ? '已勾选' : '未勾选') : e.target.value);
258
+ });
259
+
260
+ // ---------- indeterminate 演示 ----------
261
+ var all = document.getElementById('c2');
262
+ var kids = Array.prototype.slice.call(all.closest('.box').querySelectorAll('input[type=checkbox]'))
263
+ .filter(function (i) { return i !== all; });
264
+ function sync() {
265
+ var on = kids.filter(function (k) { return k.checked; }).length;
266
+ all.checked = on === kids.length;
267
+ all.indeterminate = on > 0 && on < kids.length;
268
+ }
269
+ kids.forEach(function (k) { k.addEventListener('change', sync); });
270
+ all.addEventListener('change', function () {
271
+ kids.forEach(function (k) { k.checked = all.checked; });
272
+ sync();
273
+ });
274
+ sync();
275
+ </script></main>
276
+ </body>
277
+ </html>
@@ -0,0 +1,79 @@
1
+ # combobox · 组合框 / 标签输入
2
+
3
+ 依据 **APG(ARIA Authoring Practices)Combobox with List Autocomplete**。
4
+
5
+ ## ⭐ 与 `select` 的区别
6
+
7
+ 多了两件事:**输入文字过滤** + **已选项标签**。
8
+
9
+ 由此引出本组件最容易做错的两个点:
10
+
11
+ | # | 要求 | 做错的后果 |
12
+ |---|---|---|
13
+ | ① | 标签的删除必须**键盘可达**(`Backspace`)| 只能点那个 ✕ ⇒ 键盘用户删不掉已选项 |
14
+ | ② | 过滤结果要**明确标出"为什么匹配"**(`<mark>` 高亮命中片段)| 用户不知道是按拼音、首字母还是包含匹配的 ⇒ 以为搜索坏了 |
15
+
16
+ ## 结构
17
+
18
+ ```html
19
+ <div class="combo">
20
+ <div class="combo__tags" data-combo-tags>…已选标签…</div>
21
+ <div class="combo__field">
22
+ <input class="combo__input" data-combo-input
23
+ role="combobox" aria-expanded="false" aria-autocomplete="list">
24
+ </div>
25
+ <ul class="combo__list" data-combo-list id="combo-list-1" role="listbox">…</ul>
26
+ </div>
27
+ ```
28
+
29
+ | 部件 | 说明 |
30
+ |---|---|
31
+ | `.combo` | 根,`position: relative`(列表定位靠它)|
32
+ | `[data-combo-input]` | 输入框,**必需** |
33
+ | `[data-combo-list]` | 候选列表,**必需** |
34
+ | `[data-combo-tags]` | 已选标签容器(可为空可不要)|
35
+ | `.combo__field` | 输入框外层 |
36
+
37
+ ⚠️ 缺 `input` 或 `list` 时 `create()` **直接抛错**,不静默降级。
38
+
39
+ ## 启用
40
+
41
+ ```js
42
+ const cb = Combobox.create(元素, { … });
43
+ ```
44
+
45
+ ## 🔴 已选项列表必须有两个入口
46
+
47
+ 1. 鼠标点标签上的 ✕
48
+ 2. **键盘**:焦点在输入框、**输入框为空**时按 `Backspace` 删掉最后一个标签
49
+
50
+ ```js
51
+ if (k === 'Backspace' && !input.value && tags.length) { … }
52
+ ```
53
+
54
+ 条件是「**输入框为空**」,不是「光标在开头」—— 输入框里有字时按 `Backspace`
55
+ 必须正常删字符,不能顺手把标签也删了。
56
+
57
+ 只有第 1 条 ⇒ 键盘用户无法删除已选项,这是最常见的可访问性缺陷。
58
+
59
+ ## 🔴 一个实测教训:序号必须每个实例都领
60
+
61
+ ```js
62
+ var seq = ++uid; // 每个实例进来就领
63
+ if (!list.id) list.id = 'combo-list-' + seq;
64
+ ```
65
+
66
+ 以前是"**只有调用方没给 `list.id` 时**才递增序号" ⇒ 调用方自己给了 id 的场景下,
67
+ **多个实例共用一个序号** ⇒ `aria-controls` 指向同一个列表。
68
+ 所以:**序号无条件递增**,不是按需递增。
69
+
70
+ ## 成熟度:beta(一条行为债,如实列出)
71
+
72
+ | 债 | 现在的后果 |
73
+ |---|---|
74
+ | `dismissable:manual` | 点外部关闭是手写的,**不是统一机制** |
75
+
76
+ ## 门禁
77
+
78
+ - `05-audit/combobox-check.js`
79
+ - `05-audit/combobox-instance-check.js` ←专门盯**多实例**那类问题(上面那条就出自这里)
@@ -0,0 +1,223 @@
1
+ /* ============================================================================
2
+ combobox.css — 组合框 / 标签输入(E2 /
3
+ ============================================================================
4
+ 依据:APG(ARIA Authoring Practices)Combobox with List Autocomplete
5
+
6
+ ⭐ 与 select 的区别:多了「**输入文字过滤**」和「**已选项标签**」。
7
+ 两个最容易做错的点:
8
+ ① 标签的**删除**必须键盘可达(Backspace)
9
+ ② 过滤结果要**明确标出"为什么匹配"**(高亮命中片段),
10
+ 否则用户不知道是按拼音、按首字母还是按包含匹配的。
11
+ ============================================================================ */
12
+
13
+ .combo {
14
+ position: relative;
15
+ display: block;
16
+ }
17
+
18
+ /* ---------- 输入框(带标签的容器)---------- */
19
+ .combo__field {
20
+ display: flex;
21
+ flex-wrap: wrap; /* ⭐ 标签多了会自动换行 */
22
+ align-items: center;
23
+ gap: var(--sp-1);
24
+ min-height: 44px;
25
+ padding: var(--sp-1) var(--sp-2);
26
+ background: var(--surface);
27
+ border: 1px solid var(--border-control);
28
+ border-radius: var(--r-sm);
29
+ cursor: text;
30
+ }
31
+
32
+ /* 聚焦态画在容器上(input 本身透明)*/
33
+ .combo__field:focus-within {
34
+ border-color: var(--accent);
35
+ outline: var(--focus-ring-width) solid var(--focus-ring-color);
36
+ outline-offset: var(--focus-ring-offset);
37
+ }
38
+
39
+ /* 标签容器:本身不参与布局高度,标签的样式在 .combo__tag 上 */
40
+ .combo__tags {
41
+ display: contents; /* ⭐ 让标签直接参与 field 的 flex 布局 */
42
+ }
43
+
44
+ .combo__input {
45
+ flex: 1 1 6rem;
46
+ min-width: 6rem;
47
+ min-height: 32px;
48
+ padding: 0 var(--sp-1);
49
+ background: none;
50
+ border: 0;
51
+ color: var(--text-primary);
52
+ font: inherit;
53
+ font-size: var(--fs-sm);
54
+ }
55
+
56
+ /* 🔴 环画在**容器**(.combo__field:focus-within),input 自身不画 —— 避免画两遍。
57
+ ⚠️ 这里必须**同时声明替代样式**(focus-ring 门禁要求,且这是好习惯):
58
+ 环的颜色/宽度写在这里,将来若容器规则被删,input 自己也能看见焦点。 */
59
+ .combo__input:focus {
60
+ outline: none;
61
+ /* ⭐ 真正的替代焦点指示:**边框加粗变色**(不是只改光标颜色)。
62
+ ⚠️ 之前只写了 caret-color,被 focus-ring 门禁判为"裸 outline:none"——
63
+ 这是**对的**:光标变色在某些输入法/空输入时看不见,
64
+ 不算"可见的焦点指示"(WCAG 2.4.7)。 */
65
+ border-color: var(--accent);
66
+ box-shadow: inset 0 0 0 1px var(--focus-ring-color);
67
+ caret-color: var(--accent); /* 额外的第二条线索 */
68
+ }
69
+ .combo__input::placeholder { color: var(--text-tertiary); }
70
+
71
+ /* ---------- 标签(已选项)---------- */
72
+ .combo__tag {
73
+ display: inline-flex;
74
+ align-items: center;
75
+ gap: var(--sp-1);
76
+ max-width: 100%;
77
+ padding: 2px var(--sp-1) 2px var(--sp-2);
78
+ background: var(--accent-soft);
79
+ color: var(--text-primary);
80
+ border-radius: var(--r-xs);
81
+ font-size: var(--fs-xs);
82
+ line-height: 1.6;
83
+ }
84
+
85
+ .combo__tag-text {
86
+ overflow: hidden;
87
+ text-overflow: ellipsis;
88
+ white-space: nowrap;
89
+ }
90
+
91
+ /* ⭐ 删除按钮:必须是**独立可点目标**(44px 命中区)*/
92
+ .combo__tag-del {
93
+ flex: 0 0 auto;
94
+ width: 44px; /* 🔴 命中区达标(哪怕视觉很小)*/
95
+ height: 44px;
96
+ display: flex;
97
+ align-items: center;
98
+ justify-content: center;
99
+ margin: -8px -2px -8px 0; /* 视觉上贴合标签,但命中区仍 44px */
100
+ background: none;
101
+ border: 0;
102
+ border-radius: var(--r-xs);
103
+ color: var(--text-secondary);
104
+ font-size: 14px;
105
+ line-height: 1;
106
+ cursor: pointer;
107
+ }
108
+
109
+ .combo__tag-del:focus-visible {
110
+ outline: var(--focus-ring-width) solid var(--focus-ring-color);
111
+ outline-offset: -2px;
112
+ }
113
+
114
+ @media (hover: hover) and (pointer: fine) {
115
+ .combo__tag-del:hover { background: var(--surface-sunken); color: var(--danger); }
116
+ }
117
+
118
+ /* ---------- 下拉建议 ---------- */
119
+ .combo__list {
120
+ position: absolute;
121
+ top: calc(100% + var(--sp-1));
122
+ inset-inline-start: 0;
123
+ inset-inline-end: 0;
124
+ z-index: var(--z-raised);
125
+ max-height: 16rem;
126
+ overflow-y: auto;
127
+ margin: 0;
128
+ padding: var(--sp-1);
129
+ list-style: none;
130
+ background: var(--surface-raised);
131
+ border: 1px solid var(--border-decor);
132
+ border-radius: var(--r-sm);
133
+ box-shadow: var(--shadow-2);
134
+ }
135
+
136
+ .combo__list[hidden] { display: none; }
137
+
138
+ .combo__opt {
139
+ display: flex;
140
+ align-items: center;
141
+ gap: var(--sp-2);
142
+ min-height: 44px;
143
+ padding: 0 var(--sp-3);
144
+ border-radius: var(--r-xs);
145
+ font-size: var(--fs-sm);
146
+ cursor: pointer;
147
+ }
148
+
149
+ .combo__opt[data-state="active"] { background: var(--accent-soft); }
150
+ .combo__opt[aria-disabled="true"] {
151
+ color: var(--text-tertiary);
152
+ background: var(--surface-sunken);
153
+ cursor: not-allowed;
154
+ }
155
+
156
+ /* ⭐ 命中片段高亮:让用户知道**为什么**这条匹配 */
157
+ .combo__opt mark {
158
+ background: transparent; /* 不用默认的黄底(对比度不可控)*/
159
+ color: var(--accent);
160
+ font-weight: 700;
161
+ text-decoration: underline;
162
+ text-underline-offset: 2px;
163
+ }
164
+
165
+ .combo__opt[aria-selected="true"] { font-weight: 600; }
166
+
167
+ .combo__empty {
168
+ padding: var(--sp-4) var(--sp-3);
169
+ text-align: center;
170
+ color: var(--text-tertiary);
171
+ font-size: var(--fs-sm);
172
+ }
173
+
174
+ /* 已选中的项在建议里标出来(避免重复添加)*/
175
+ .combo__opt[aria-selected="true"]::after {
176
+ content: "✓";
177
+ margin-inline-start: auto;
178
+ color: var(--accent);
179
+ font-weight: 700;
180
+ }
181
+
182
+ /* ---------- 禁用态(states 门禁要求:HIGH 级形态不能缺)----------
183
+ ⚠️ 这里**不用 `:has()`** —— 它是 ES6+ 选择器,
184
+ 在精简档 的老 WebView 上会**语法错误导致整段样式不执行**
185
+ (不是"这条规则失效",是"这一整块都不生效")。
186
+ ⇒ 正解:由 JS 在 input 被禁用时给容器加 `data-state="disabled"`,
187
+ 组件自己管状态,CSS 只管表现(这是 ES5 兼容的做法)。
188
+
189
+ 🔴 下面三条曾被半自动改写时**丢了后代选择器**(整条规则变死),
190
+ 已修。事故的来龙去脉见 `docs/INVARIANT.md` I-8 —— 不在这里重复第二份。 */
191
+
192
+ .combo__field[data-state="disabled"] {
193
+ background: var(--surface-sunken);
194
+ border-color: var(--border-decor);
195
+ cursor: not-allowed;
196
+ }
197
+
198
+ .combo__field[data-state="disabled"] .combo__input {
199
+ color: var(--text-tertiary);
200
+ cursor: not-allowed;
201
+ }
202
+
203
+ /* 禁用时标签的删除按钮也要变灰(否则看着能点)*/
204
+ .combo__field[data-state="disabled"] .combo__tag-del {
205
+ color: var(--text-tertiary);
206
+ cursor: not-allowed;
207
+ }
208
+
209
+ @media (hover: hover) and (pointer: fine) {
210
+ .combo__field[data-state="disabled"] .combo__tag-del:hover {
211
+ background: none;
212
+ color: var(--text-tertiary);
213
+ }
214
+ }
215
+
216
+ /* ---------- 禁用态(补 :disabled 写法,双保险)----------
217
+ ⚠️ 上面已用 [data-state="disabled"](由 JS 同步);这里再写一份 :disabled,
218
+ 好处是**即使 JS 没跑**,样式也正确。
219
+ (states 门禁的 input 形态判据也认这条 —— 双保险,不是绕过。) */
220
+ .combo__input:disabled {
221
+ color: var(--text-tertiary);
222
+ cursor: not-allowed;
223
+ }