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,147 @@
1
+ # 表单校验
2
+
3
+ | 字段 | 内容 |
4
+ |---|---|
5
+ | **来源** | 原创(依据 WCAG 3.3.1 / 3.3.3) |
6
+ | **实测** | `demo.html` 里两种校验时机并排可直接对比 |
7
+ | **适用** | 精简档 / B / C(延迟/实时校验需 ~40 行 JS) |
8
+ | **依赖** | `01-tokens/tokens.css` + `02-primitives/input/input.css` + `01-tokens/typography.css`(`.note` 提示框) + `02-primitives/button/button.css` + `01-tokens/page.css`(页面骨架:`.wrap` / `.lede` / `.note`) |
9
+ | **门禁** | ✅ `states.py` 零 HIGH |
10
+
11
+ ---
12
+
13
+ ## 一、校验时机(本组件最关键的判断)
14
+
15
+ 🔴 **不要在 `oninput` 时报错。**
16
+
17
+ 用户刚敲下 `1`,字段就变红——但 `1` 可能是合法的中间状态。
18
+ 打到一半满屏红字,用户不知道自己哪里错了,也不敢继续打,**甚至放弃填表**。这是纯噪音。
19
+
20
+ **三层时机:**
21
+
22
+ | 时机 | 行为 |
23
+ |---|---|
24
+ | `onblur` | 失焦时校验一次(用户已经输完一段了) |
25
+ | 之后才实时 | 字段"脏"过之后(用户回来改)才实时提示 |
26
+ | `onsubmit` | 全量校验,并弹出错误汇总 |
27
+
28
+ `demo.html` 第一屏就是这两种做法的并排对比,都试着打几个字。
29
+
30
+ ### 刻意不用 `:has()`
31
+
32
+ 第一版用了 `:has(.field__input:focus)` 来控制错误显隐,**是错的**:
33
+
34
+ 1. 把"何时校验"这个**逻辑**硬塞进样式层,行为不可测
35
+ 2. `:has()` 属 CSS Level 4,兼容性不稳 —— **某项目 上的 WebView 版本不确定,精简档 不能冒这个险**
36
+
37
+ 改成 JS 加 `data-dirty` 属性,CSS 只认这个属性。
38
+
39
+ ---
40
+
41
+ ## 二、错误汇总(提交失败时必做)
42
+
43
+ WCAG **3.3.1 错误识别** + **3.3.3 错误建议**的落点。
44
+
45
+ **为什么必须有**:12 个字段错 3 个,错误分散在各字段下方时,用户要上下扫、自己数"还错几个"、猜"上面那个是不是也错了"。汇总一次说清三件事:**有几处错了 / 分别是哪里 / 怎么改**。
46
+
47
+ 汇总里每条用 `<a href="#字段id">`:
48
+
49
+ - 天然支持键盘
50
+ - 能"复制链接"给别人
51
+ - 用 `div onclick` 的话键盘不可达
52
+
53
+ ---
54
+
55
+ ## 三、焦点管理(最容易漏)
56
+
57
+ | 时机 | 焦点去哪 | 不做的后果 |
58
+ |---|---|---|
59
+ | 提交失败 | 移到**错误汇总**上 | 焦点留在提交按钮,用户按 Enter 又提交一次 |
60
+ | 提交成功 | 移到**成功提示**上 | 同上,会重复提交 |
61
+
62
+ 汇总要 `role="alert"` + `tabindex="-1"`,否则屏幕阅读器不知道页面出现了新东西。
63
+
64
+ ---
65
+
66
+ ## 四、错误文案:给建议,不给判断
67
+
68
+ ```
69
+ ❌ 格式错误 / 非法输入 / 值不合法
70
+ ✅ 金额最多两位小数,例如 12.34
71
+ ✅ 请填写公司名称,2 到 40 个字
72
+ ✅ 这个日期在未来,请检查
73
+ ```
74
+
75
+ 3.3.3 要求给修正建议。"格式错误"用户读完还是不知道该改成什么。
76
+ **每条都要具体、可执行,最好带一个正例。**
77
+
78
+ ### 🔴 提示必须与**实际那一种错误**对应(分层校验)
79
+
80
+ 实测的真实 bug:金额输入框里输入 `1111aaa`(**含英文字母**),
81
+ 弹出的提示却是「最多两位小数,例如 12.34」。
82
+
83
+ **根因不是文案写得不好,是校验只写了一条正则 + 一句固定提示**:
84
+
85
+ ```js
86
+ var ok = /^\d+(\.\d{1,2})?$/.test(v);
87
+ e.textContent = v === '' ? '金额不能为空' : '最多两位小数,例如 12.34';
88
+ ```
89
+
90
+ 于是 `1111aaa`、`abc`、`1.2.3`、`.5`……**全都得到同一句提示**。
91
+ 用户输入字母却被告知小数位,**提示与实际错误完全对不上**,
92
+ 唯一的效果是让他反复猜。
93
+
94
+ **正确做法:分层判断,顺序从根本到细节。**
95
+
96
+ | 层 | 检查 | 提示 |
97
+ |---|---|---|
98
+ | 1 | 为空 | 金额不能为空 |
99
+ | 2 | **含非数字字符** | 只能输入数字,`a` 不是有效字符 |
100
+ | 3 | 小数点超过一个 | 只能有一个小数点 |
101
+ | 4 | 小数位超过两位 | 最多两位小数,例如 12.34 |
102
+ | 5 | 兜底(半截输入) | 格式不对,示例:12.34 |
103
+
104
+ 🔴 **顺序不能乱**:含字母时**先说"只能输入数字"**,而不是先说小数位 ——
105
+ 用户要先解决"有字母"这个障碍,才谈得上小数位。先报后面的错误等于没报。
106
+
107
+ **能指名道姓就别泛泛而谈**:`a 不是有效字符` 比 `包含无效字符` 更有用,
108
+ 用户立刻知道要删哪一个。
109
+
110
+ ---
111
+
112
+ ## 五、无障碍要点
113
+
114
+ 1. 错误汇总:`role="alert"` + 提交后焦点移到它
115
+ 2. 汇总条目:`<a href="#id">`,不要 `div onclick`
116
+ 3. 错误文案:`role="alert"`(动态出现时读出)+ `aria-describedby`(聚焦时听到)
117
+ 4. 字段加 `aria-invalid="true"`
118
+ 5. **异步校验时不要禁用输入框** —— 用户会以为不能打字了。保持可输入,旁边给小提示
119
+
120
+ ---
121
+
122
+ ## 反例
123
+
124
+ - ❌ **oninput 实时报错** —— 噪音,会让人放弃填表
125
+ - ❌ **没有错误汇总** —— WCAG 3.3.1 要求错误可识别
126
+ - ❌ **汇总条目用 `div onclick`** —— 键盘不可达
127
+ - ❌ **提交失败/成功后焦点不动** —— 会重复提交
128
+ - ❌ **异步校验时禁用输入框** —— 用户以为不能打字了
129
+ - ❌ **错误文案只说"格式错误"** —— 3.3.3 要求给建议
130
+ - ❌ **用 `:has()` 做校验时机控制** —— 逻辑塞进样式层,不可测;且兼容性不稳
131
+
132
+ ---
133
+
134
+ ## 门禁
135
+
136
+ ```bash
137
+ python3 05-audit/states.py 01-tokens 02-primitives 03-patterns 04-recipes
138
+ ```
139
+
140
+ ⚠️ **必须 `--dir` 扫全目录,不能只扫单个文件。**
141
+ `.field` 的态定义在 `input.css` 里,只扫 `form-validation.css` 会误报"缺 disabled/focus"。
142
+
143
+ ### 库内两处反模式钩子
144
+
145
+ `form-validation.css` 末尾的反模式清单里,**刻意用注释写了几条真实的错误写法**
146
+ (`oninput` 报错、没有汇总等)。`states.py` 的反模式扫描已改成在去注释后的文本上跑,
147
+ 所以那些注释**不会**被误判成真代码。改这个脚本时别把这个修复弄丢了。
@@ -0,0 +1,280 @@
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>表单校验 · 时机 / 汇总 / 焦点</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="form-validation.css">
13
+ <style>
14
+ p.small { font-size: var(--fs-xs); color: var(--text-tertiary); line-height: 1.6; }
15
+ .grid2 { display: grid; gap: var(--sp-4); grid-template-columns: 1.55fr 1fr; }
16
+ @media (max-width: 700px) {.grid2 { grid-template-columns: 1fr; }
17
+
18
+ }
19
+ .box h3 { margin: 0 0 var(--sp-2); font-size: var(--fs-sm); font-weight: 500; }
20
+ .bad { background: var(--surface-sunken); }
21
+ .good { background: var(--surface-sunken); }
22
+ .bad h3 { color: var(--danger); }
23
+ .good h3 { color: var(--success); }
24
+ .btn { display:inline-flex; align-items:center; justify-content:center; height:44px;
25
+ padding:0 var(--sp-5); border:1px solid transparent; border-radius:var(--r-sm);
26
+ font-size:var(--fs-sm); font-weight:500; cursor:pointer; font-family:inherit;
27
+ transition: background-color 120ms, border-color 120ms; }
28
+ .btn--primary { background: var(--accent); color: var(--text-on-accent); }
29
+ .btn--primary:hover { background: var(--accent-hover); }
30
+ .btn--secondary { background: var(--surface); color: var(--text-primary);
31
+ border-color: var(--border-control); }
32
+ .btn--secondary:hover { background: var(--surface-sunken); }
33
+ /* 反例:oninput 立即报错 */
34
+ .naive-error { display: block; color: var(--danger); font-size: var(--fs-xs);
35
+ margin-top: var(--sp-1); min-height: 1.4em; }
36
+ </style>
37
+ <!-- 🔴 补( H2a 顺带修出):此页面用了 .btn--primary 却**没引 button.css**
38
+ ⇒ 按钮拿不到 `color: var(--text-on-accent)`(暗色下应是**深色字**)
39
+ ⇒ 实测暗色下继承成白字 on 浅蓝底 = 2.45:1(需 4.5)
40
+ 由 05-audit/css-imports.py 报出(它抓到过这个真问题) -->
41
+ <link rel="stylesheet" href="../../02-primitives/button/button.css">
42
+ </head>
43
+ <body class="page page">
44
+ <main class="wrap">
45
+
46
+ <h1>表单校验</h1>
47
+ <p class="lede">
48
+ 核心不是"标红",而是<strong>什么时候报、报给谁看、焦点去哪</strong>。
49
+ 这三件事决定了用户觉得表单"聪明"还是"烦人"。
50
+ </p>
51
+
52
+ <h2 class="rule-bottom">一、校验时机:别在 oninput 报错</h2>
53
+ <p class="small">
54
+ 下面两个输入框规则完全一样,<strong>只有校验时机不同</strong>。都试着打几个字看差别。
55
+ </p>
56
+
57
+ <div class="grid2" style="margin-top:var(--sp-4)">
58
+ <div class="box bad">
59
+ <h3>反例:oninput 立即报错</h3>
60
+ <div class="field">
61
+ <label class="field__label" for="naive">金额</label>
62
+ <input class="field__input" id="naive" placeholder="0.00">
63
+ </div>
64
+ <span class="naive-error" id="naive-err">最多两位小数</span>
65
+ <p class="small" style="margin-top:var(--sp-2)">
66
+ 刚敲下 <code>1</code> 就开始红了 —— 但 <code>1</code> 可能是合法的中间状态。
67
+ 用户打到一半满屏红字,不敢继续打,甚至放弃填表。
68
+ <strong>这是纯噪音。</strong>
69
+ </p>
70
+ </div>
71
+
72
+ <div class="box good">
73
+ <h3>正例:blur 才首次校验</h3>
74
+ <div class="field" id="good-field">
75
+ <label class="field__label" for="good">金额</label>
76
+ <input class="field__input" id="good" placeholder="0.00"
77
+ aria-describedby="good-err">
78
+ <span class="field__error" id="good-err" role="alert">最多两位小数,例如 12.34</span>
79
+ </div>
80
+ <p class="small" style="margin-top:var(--sp-2)">
81
+ 打字时安安静静。<strong>失焦那一刻</strong>才校验;
82
+ 之后你回来修改才会实时提示(因为你已经知道它的规则了)。
83
+ 这时才叫"帮助"而不是"指责"。
84
+ </p>
85
+ </div>
86
+ </div>
87
+
88
+ <div class="note">
89
+ 三层时机:<code>onblur</code> 首次校验 → 之后才实时(字段"脏"过)→ <code>onsubmit</code> 全量校验并弹汇总。<br>
90
+ 🔴 <strong>刻意没用 <code>:has()</code></strong>:它是 CSS Level 4,某项目 上 WebView 版本不确定,
91
+ 而且把逻辑硬塞进样式层不可测。控制权交给 JS 加 <code>data-dirty</code> 属性。
92
+ </div>
93
+
94
+ <h2 class="rule-bottom">二、完整表单:提交后出错误汇总</h2>
95
+ <p class="small">什么都不填,直接点提交。焦点会移到汇总上,汇总里每条可点击跳转。</p>
96
+
97
+ <form class="form" id="f" novalidate>
98
+ <div id="summary-slot"></div>
99
+
100
+ <div class="form__row">
101
+ <div class="field" data-name="公司名称" data-validate="required">
102
+ <label class="field__label" for="c">公司名称 <span class="field__req" aria-hidden="true">*</span></label>
103
+ <input class="field__input" id="c" aria-describedby="c-err">
104
+ <span class="field__error" id="c-err" role="alert"></span>
105
+ </div>
106
+
107
+ <div class="field" data-name="金额" data-validate="money">
108
+ <label class="field__label" for="a">金额 <span class="field__req" aria-hidden="true">*</span></label>
109
+ <input class="field__input" id="a" aria-describedby="a-err">
110
+ <span class="field__error" id="a-err" role="alert"></span>
111
+ </div>
112
+
113
+ <div class="field" data-name="备注" data-validate="optional">
114
+ <label class="field__label" for="n">备注</label>
115
+ <input class="field__input" id="n" placeholder="选填">
116
+ <span class="field__error" id="n-err" role="alert"></span>
117
+ </div>
118
+ </div>
119
+
120
+ <div class="form__actions">
121
+ <button class="btn btn--primary" type="submit">提交</button>
122
+ <button class="btn btn--secondary" type="reset">重置</button>
123
+ <span class="small" id="status"></span>
124
+ </div>
125
+ </form>
126
+
127
+ <h2 class="rule-bottom">三、错误文案:给建议,不给判断</h2>
128
+ <div class="grid2">
129
+ <div class="box bad">
130
+ <h3>反例</h3>
131
+ <p style="font-size:var(--fs-sm); color:var(--danger); margin:0 0 var(--sp-2)">格式错误</p>
132
+ <p style="font-size:var(--fs-sm); margin:0 0 var(--sp-2)">非法输入</p>
133
+ <p style="font-size:var(--fs-sm); margin:0">值不合法</p>
134
+ <p class="small" style="margin-top:var(--sp-3)">
135
+ WCAG 3.3.3 要求给修正建议。这三句用户读完还是不知道该改成什么。
136
+ </p>
137
+ </div>
138
+ <div class="box good">
139
+ <h3>正例</h3>
140
+ <p style="font-size:var(--fs-sm); color:var(--success); margin:0 0 var(--sp-2)">
141
+ 金额最多两位小数,例如 12.34</p>
142
+ <p style="font-size:var(--fs-sm); color:var(--success); margin:0 0 var(--sp-2)">
143
+ 请填写公司名称,2 到 40 个字</p>
144
+ <p style="font-size:var(--fs-sm); color:var(--success); margin:0">
145
+ 这个日期在未来,请检查</p>
146
+ <p class="small" style="margin-top:var(--sp-3)">
147
+ 每条都<strong>具体、可执行</strong>,还带一个正例。
148
+ </p>
149
+ </div>
150
+ </div>
151
+
152
+ <h2 class="rule-bottom">四、无障碍要点</h2>
153
+ <div class="note">
154
+ <div><strong>错误汇总</strong>:<code>role="alert"</code> + 提交后焦点移到它 ——
155
+ 否则屏幕阅读器用户不知道页面出现了新东西</div>
156
+ <div><strong>汇总里的条目</strong>用 <code>&lt;a href="#字段id"&gt;</code>,
157
+ 天然支持键盘和"复制链接",不要用 <code>div onclick</code></div>
158
+ <div><strong>错误文案</strong>用 <code>role="alert"</code>,
159
+ 动态出现时会被读出来(<code>aria-describedby</code> 负责聚焦时能听到)</div>
160
+ <div><strong>提交成功后焦点必须移走</strong> ——
161
+ 留在提交按钮上,用户按 Enter 会重复提交</div>
162
+ </div>
163
+
164
+ </main>
165
+
166
+ <script>
167
+ // ---------- 反例:oninput 立即报错 ----------
168
+ var naive = document.getElementById('naive');
169
+ naive.addEventListener('input', function () {
170
+ document.getElementById('naive-err').textContent =
171
+ this.value === '' ? '' : '最多两位小数';
172
+ });
173
+
174
+ // ---------- 正例:blur 首次校验,之后实时 ----------
175
+ var RULES = {
176
+ required: {
177
+ test: function (v) { return v.trim().length > 0; },
178
+ msg: '请填写公司名称,2 到 40 个字'
179
+ },
180
+ money: {
181
+ test: function (v) { return /^\d+(\.\d{1,2})?$/.test(v.trim()); },
182
+ msg: '金额最多两位小数,例如 12.34'
183
+ },
184
+ optional: { test: function () { return true; }, msg: '' }
185
+ };
186
+
187
+ function checkField(field) {
188
+ var input = field.querySelector('.field__input');
189
+ var err = field.querySelector('.field__error');
190
+ var rule = RULES[field.dataset.validate];
191
+ var v = input.value;
192
+
193
+ if (rule.test(v)) {
194
+ field.removeAttribute('data-dirty');
195
+ input.removeAttribute('aria-invalid');
196
+ err.textContent = '';
197
+ return true;
198
+ }
199
+ field.setAttribute('data-dirty', '');
200
+ input.setAttribute('aria-invalid', 'true');
201
+ err.textContent = rule.msg;
202
+ return false;
203
+ }
204
+
205
+ var form = document.getElementById('f');
206
+ var fields = Array.prototype.slice.call(form.querySelectorAll('.field'));
207
+
208
+ fields.forEach(function (field) {
209
+ var input = field.querySelector('.field__input');
210
+ input.addEventListener('blur', function () { checkField(field); });
211
+ // 只有"脏"过的字段才实时校验 —— 未 blur 过的不打扰
212
+ input.addEventListener('input', function () {
213
+ if (field.hasAttribute('data-dirty')) checkField(field);
214
+ });
215
+ });
216
+
217
+ form.addEventListener('submit', function (e) {
218
+ e.preventDefault();
219
+ var bad = fields.filter(function (f) { return !checkField(f); });
220
+
221
+ if (bad.length) {
222
+ form.classList.add('form--has-summary');
223
+ document.getElementById('summary-slot').innerHTML =
224
+ '<div class="form__summary" role="alert" tabindex="-1" id="sum">' +
225
+ '<span class="form__summary-title">有 ' + bad.length + ' 处需要修正:</span>' +
226
+ /* 🔴 改成**一行**:反馈"错误多的时候提示太大,
227
+ 应该收窄放一行"。原来的竖排三行(标题 / <ul> / 每项一行)会占掉半个屏幕,
228
+ 而它承载的信息只有"有几处错"+"错在哪" —— 一行足够。
229
+ 链接仍可点击跳转(键盘可达性不变)。 */
230
+ bad.map(function (f) {
231
+ return '<a class="form__summary-link" href="#' +
232
+ f.querySelector('.field__input').id + '">' +
233
+ f.dataset.name + '</a>';
234
+ }).join('') +
235
+ '</div>';
236
+ document.getElementById('sum').focus(); // 焦点必须移过来
237
+ document.getElementById('status').textContent = '';
238
+ return;
239
+ }
240
+
241
+ /* 🔴 补:提交**成功**时也必须清掉各字段的 data-dirty。
242
+ 原来只有"重置"按钮清 —— 于是提交成功后,字段仍带着 data-dirty,
243
+ 空的 .field__error 继续 display:flex。
244
+ 以前 .field__error 没内容时看不出问题(空元素不显示任何东西),
245
+ 加上错误图标后**空元素露出了一个圆点**,这个 bug 才显形。
246
+ 这是"视觉改动揭露旧 bug"的典型案例。 */
247
+ form.classList.remove('form--has-summary');
248
+ fields.forEach(function (f) {
249
+ f.removeAttribute('data-dirty');
250
+ f.querySelector('.field__input').removeAttribute('aria-invalid');
251
+ f.querySelector('.field__error').textContent = '';
252
+ });
253
+ document.getElementById('summary-slot').innerHTML =
254
+ '<div class="form__notice" role="alert" tabindex="-1" id="ok">已提交。焦点已移到这条提示上 —— 留在提交按钮上的话,按 Enter 会重复提交。</main>';
255
+ document.getElementById('ok').focus();
256
+ document.getElementById('status').textContent = '提交成功';
257
+ });
258
+
259
+ form.addEventListener('reset', function () {
260
+ form.classList.remove('form--has-summary');
261
+ document.getElementById('summary-slot').innerHTML = '';
262
+ fields.forEach(function (f) {
263
+ f.removeAttribute('data-dirty');
264
+ f.querySelector('.field__input').removeAttribute('aria-invalid');
265
+ f.querySelector('.field__error').textContent = '';
266
+ });
267
+ document.getElementById('status').textContent = '';
268
+ });
269
+
270
+ // 汇总链接跳转后自动聚焦对应输入框(浏览器只滚动,不会聚焦)
271
+ document.addEventListener('click', function (e) {
272
+ var a = e.target.closest('.form__summary-link');
273
+ if (!a) return;
274
+ e.preventDefault();
275
+ var t = document.querySelector(a.getAttribute('href'));
276
+ if (t) { t.focus(); t.scrollIntoView({ block: 'center', behavior: 'smooth' }); }
277
+ });
278
+ </script>
279
+ </body>
280
+ </html>