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,263 @@
1
+ /* ============================================================================
2
+ frontend-lib / 03-patterns / form-validation / form-validation.css
3
+ 表单校验
4
+
5
+ 依赖:01-tokens/tokens.css + 02-primitives/input/input.css
6
+ 版本:A ✅ / B ✅ / C ✅
7
+
8
+ 🔴 本组件只做"编排",不重定义输入框 —— 那是 input 的事。
9
+ 这里负责三件 input 管不了的事:
10
+ 1. 校验时机(什么时候才报错了)
11
+ 2. 错误汇总(提交失败时怎么让人一次看到全部问题)
12
+ 3. 焦点管理(提交失败后焦点去哪)
13
+ ========================================================================= */
14
+
15
+ .form {
16
+ display: flex;
17
+ flex-direction: column;
18
+ gap: var(--sp-5);
19
+ }
20
+
21
+ .form__row {
22
+ display: flex;
23
+ flex-direction: column;
24
+ gap: var(--sp-5);
25
+ }
26
+
27
+ .form__actions {
28
+ display: flex;
29
+ gap: var(--sp-3);
30
+ align-items: center;
31
+ flex-wrap: wrap;
32
+ padding-top: var(--sp-2);
33
+ border-top: 1px solid var(--border-decor);
34
+ }
35
+
36
+ /* ============================================================================
37
+ 1. 错误汇总 —— 提交失败时必做
38
+ =========================================================================
39
+ 🔴 这是 WCAG 3.3.1(错误识别)和 3.3.3(错误建议)的落点。
40
+
41
+ 为什么必须有:
42
+ 一个 12 字段的表单,3 个字段错了。错误提示分散在各字段下方时,
43
+ 用户要上下扫、自己数"还错几个"、猜"上面那个是不是也错了"。
44
+ 汇总把三件事一次说清:有几处错了 / 分别是哪里 / 怎么改。
45
+ ========================================================================= */
46
+ .form__summary {
47
+ /* 🔴 改成**一行且宽度按内容**(要求:错误多的时候提示太大,
48
+ 应该收窄,然后放到一行去就可以了)。
49
+ 原来是 column 竖排:标题一行 + <ul> 里每项一行 ⇒ 错 5 处就占 7 行。
50
+ 现在 inline-flex:整条提示只占一行,宽度由内容决定。
51
+ —— 信息量没变(几处 / 哪几处 / 可点击跳转),但**占用的竖向空间从 O(n) 变成 O(1)**。 */
52
+ display: inline-flex;
53
+ align-items: center;
54
+ flex-wrap: wrap;
55
+ gap: var(--sp-2) var(--sp-3);
56
+ max-width: 100%;
57
+ padding: var(--sp-3) var(--sp-4);
58
+ background: var(--danger-bg);
59
+ border-radius: var(--r-sm);
60
+ font-size: var(--fs-sm);
61
+ line-height: 1.5;
62
+ }
63
+
64
+ /* 🔴 去掉 `border: 1px solid danger` + `border-left: 4px solid`。
65
+ 原来那两条的理由写得很清楚:「左边框加粗,色弱用户也能靠"这里有个条"发现它」。
66
+ **这个理由本身是对的,但手段选错了** ——
67
+ 4px 竖条在页面上拉出一条与内容无关的硬边,视觉噪声大于信息量。
68
+ 改用**图标**承担同一份职责:同样让色弱用户一眼看到"这里出错了",
69
+ 但它跟着文字走,不会形成横贯页面的竖线。 */
70
+ .form__summary::before {
71
+ content: "!";
72
+ display: inline-grid;
73
+ place-items: center;
74
+ width: 16px;
75
+ height: 16px;
76
+ margin-inline-end: var(--sp-2);
77
+ border-radius: 50%;
78
+ background: var(--danger);
79
+ /* 🔴 同上:暗色下 danger 变亮 ⇒ 白叹号看不清。 */
80
+ color: var(--text-on-accent);
81
+ font-size: 11px;
82
+ font-weight: 700;
83
+ line-height: 1;
84
+ }
85
+
86
+ .form__summary-title {
87
+ margin: 0;
88
+ font-size: var(--fs-sm);
89
+ font-weight: 500;
90
+ color: var(--danger); /* 6.26 AA on danger-bg */
91
+ display: flex;
92
+ align-items: center;
93
+ gap: var(--sp-2);
94
+ }
95
+
96
+ /* 汇总里每条都是可点击的链接,点了跳到对应字段并聚焦它。
97
+ <a href="#field-id"> 天然支持键盘和"复制链接",比 <div onclick> 好。 */
98
+ /* 🔴 汇总条目不再包 <ul>,直接是并列的 <a>。
99
+ <ul> 的竖排是"提示占满竖向空间"的元凶。 */
100
+ .form__summary-link + .form__summary-link { margin-inline-start: var(--sp-1); }
101
+
102
+ .form__summary-link {
103
+ display: inline-block;
104
+ color: var(--danger);
105
+ font-size: var(--fs-sm);
106
+ text-decoration: underline;
107
+ text-underline-offset: 2px; /* 下划线离字远一点,中文小字更易读 */
108
+ text-decoration-thickness: 1px;
109
+ }
110
+ @media (hover: hover) and (pointer: fine) {
111
+ .form__summary-link:hover { color: var(--text-primary); }
112
+ }
113
+ .form__summary-link:focus-visible {
114
+ outline: 2px solid var(--danger);
115
+ outline-offset: 2px;
116
+ border-radius: 2px;
117
+ }
118
+
119
+ /* ============================================================================
120
+ 2. 校验时机 —— 本组件最关键的判断
121
+ =========================================================================
122
+ 🔴 不要在 oninput 时报错。
123
+
124
+ 用户刚开始打字,字段就变红 —— 打到一半全是红字,用户根本不知道自己
125
+ 哪里错了,也不敢继续打。这个反馈是纯噪音,甚至会让人放弃填表。
126
+
127
+ 正确时机(三层):
128
+ a) onblur —— 字段失焦时校验一次(用户已经输完一段了)
129
+ b) 之后才实时 —— 字段"脏"过之后(用户回来修改)才实时报错
130
+ c) onsubmit —— 提交时全量校验,并弹出错误汇总
131
+
132
+ "脏"用 `data-dirty` 标记,由 JS 加。CSS 只负责让脏字段的错误显示出来。
133
+
134
+ 🔴 这里刻意**不用 `:has()`**(写第一版时用了,是错的):
135
+ 一是把"何时校验"这个逻辑硬塞进样式层,行为不可测;
136
+ 二是 `:has()` 属 CSS Level 4,兼容性不稳 —— 某项目 上的 WebView
137
+ 版本不确定,精简档 不能冒这个险。
138
+ 控制权交给 JS:字段校验后由 JS 加 `data-dirty`,CSS 只认这个属性。
139
+
140
+ ⚠️ 为什么是 `data-dirty` 而不是 `data-state="dirty"`:
141
+ `data-state` 是"这个组件当前处于哪个枚举状态"(open/closed/…),
142
+ "脏"是**正交标记**(一个字段可以同时是 dirty 且 state=invalid)。
143
+ 塞进同一个属性 ⇒ 两个维度互斥,表达不了组合。
144
+ ------------------------------------------------------------------------ */
145
+ .field__error { display: none; }
146
+ .field[data-dirty] .field__error { display: flex; }
147
+
148
+ /* 错误汇总展开时,全部错误强制显示,不管字段脏不脏 —— 那是用户主动要看清单。
149
+ ⚠️ 原来这里是 `.form--has-summary .field__error`(不带 :not(:empty)),
150
+ 它会把**空元素也显示出来** —— 于是没填错的字段下方挂着空红点。
151
+ 实测反馈"下方的红点一直存在",根因就在这里。 */
152
+ .form--has-summary .field__error:not(:empty) { display: flex; }
153
+
154
+ /* 🔴 兜底(必须放在上面两条**之后**):**没有文字的错误提示一律不显示**。
155
+ `[data-dirty]` 只表示"这个字段被校验过",**不表示"它现在有错"**。
156
+ 以前没内容时看不出问题(空元素不渲染任何东西),
157
+ 加了错误图标之后,空元素会露出一个圆点。
158
+
159
+ 🔴 教训:我第一次把这条写在**上面**,被后面同优先级的
160
+ `.form--has-summary .field__error` 覆盖掉了,等于没写。
161
+ **兜底规则必须放在它要约束的所有规则之后**,否则会被吃掉。 */
162
+ .field__error:empty { display: none; }
163
+
164
+ /* 错误图标 —— 兑现 README「三条线索」里的第 2 条(红边框 + 叹号 + 错误文字)。
165
+ 🔴 补的:README 一直这么写,但实现里一直缺这一条
166
+ (审核时才发现"文档承诺 ≠ 实现存在")。
167
+
168
+ 为什么不用 Unicode 符号(❌ ⚠ 等):
169
+ 缺字会渲染成**空心方框(tofu)**,而且小尺寸下与实心符号肉眼难分。
170
+ `!` 是 ASCII,任何字体都有,**不可能缺字**。
171
+
172
+ 为什么画成圆底而不直接放一个叹号:
173
+ 直接放字符在小字号下会像"多了一个字",不像"这是个状态标记"。 */
174
+ .field__error::before {
175
+ content: "!";
176
+ display: inline-grid;
177
+ place-items: center;
178
+ flex: none;
179
+ width: 14px;
180
+ height: 14px;
181
+ margin-inline-end: var(--sp-1);
182
+ border-radius: 50%;
183
+ background: var(--danger);
184
+ /* 🔴 暗色下底色变亮,写死的白字会看不清。 */
185
+ color: var(--text-on-accent);
186
+ font-size: 10px;
187
+ font-weight: 700;
188
+ line-height: 1;
189
+ }
190
+
191
+ /* ============================================================================
192
+ 3. 提交成功后的焦点去向
193
+ =========================================================================
194
+ 提交成功后焦点必须**移走**。留在提交按钮上,用户按 Enter 会重复提交。
195
+ 移到成功提示上,让屏幕阅读器读出来。
196
+ ------------------------------------------------------------------------ */
197
+ .form__notice {
198
+ padding: var(--sp-4) var(--sp-5);
199
+ background: var(--success-bg);
200
+ border-radius: var(--r-sm);
201
+ font-size: var(--fs-sm);
202
+ color: var(--text-primary);
203
+ }
204
+
205
+ /* ============================================================================
206
+ 4. 校验中的状态(异步校验,比如"检查用户名是否可用")
207
+ =========================================================================
208
+ 🔴 别让输入框变灰或禁用 —— 用户会以为不能打字了。
209
+ 正确做法:保持可输入,在旁边显示一个小小的 pending 指示。
210
+ ------------------------------------------------------------------------ */
211
+ .field--checking .field__input::after { content: ""; } /* 占位,禁止改用 padding 挤压 */
212
+ .field__checking-hint {
213
+ display: flex;
214
+ align-items: center;
215
+ gap: var(--sp-1);
216
+ font-size: var(--fs-xs);
217
+ color: var(--text-tertiary);
218
+ }
219
+ .field__checking-hint::before {
220
+ content: "";
221
+ width: 12px;
222
+ height: 12px;
223
+ border: 2px solid var(--border-control);
224
+ border-top-color: var(--accent);
225
+ border-radius: 50%;
226
+ animation: form-spin 700ms linear infinite;
227
+ }
228
+ @keyframes form-spin { to { transform: rotate(360deg); } }
229
+
230
+ /* ============================================================================
231
+ reduced-motion
232
+ =========================================================================
233
+ 与库内其他规则一致:原地旋转的 spinner 保留但放慢(停了以为卡死)。
234
+ ========================================================================= */
235
+ @media (prefers-reduced-motion: reduce) {
236
+ .field__checking-hint::before { animation-duration: 1.2s !important; }
237
+ }
238
+
239
+ /* ============================================================================
240
+ 反模式
241
+ =========================================================================
242
+ 1. oninput 时实时报错
243
+ 用户刚开始打字就一片红,噪音,且会让人放弃填表。
244
+
245
+ 2. 没有错误汇总
246
+ 12 个字段错 3 个,用户要自己数、自己找。WCAG 3.3.1 要求错误可识别。
247
+
248
+ 3. 汇总里的条目用 <div onclick> 跳转
249
+ 键盘不可达。用 <a href="#id">。
250
+
251
+ 4. 提交失败后焦点不动
252
+ 焦点还留在提交按钮,用户按 Enter 又提交一次。必须移到汇总上。
253
+
254
+ 5. 提交成功后焦点不动
255
+ 同上,会重复提交。移到成功提示上。
256
+
257
+ 6. 异步校验时禁用输入框
258
+ 用户以为不能打字了。保持可输入,旁边给小提示。
259
+
260
+ 7. 错误文案只说"格式错误"
261
+ 3.3.3 要求给修正建议。要写"最多两位小数,例如 12.34",
262
+ 不要写"格式错误"或"非法输入"。
263
+ ========================================================================= */
@@ -0,0 +1,150 @@
1
+ # list —— 列表
2
+
3
+ | 字段 | 内容 |
4
+ |---|---|
5
+ | **来源** | 原创。FLIP 原理是公开技术,无第三方代码 |
6
+ | **实测** | `demo.html` 四种交互可实际操作;JS 语法经 `node --check` 验证 |
7
+ | **适用** | 精简档 / B / C,`flip.js` 为 ES5 |
8
+ | **依赖** | `01-tokens/tokens.css` + `02-primitives/button/button.css` `03-patterns/states/states.css`(空状态用 `.state--empty`) + `01-tokens/typography.css`(`.note` 提示框) + `01-tokens/page.css`(页面骨架:`.wrap` / `.lede` / `.note`) |
9
+
10
+ ---
11
+
12
+ ## 🔴 为什么不用 auto-animate
13
+
14
+ 评估过 auto-animate(8 KB min+gzip,精简档 装得下,MIT 协议),
15
+ 但列表真正需要的只有**"增删时其余项平滑移动"**这一件事。
16
+
17
+ | | auto-animate | 自己写 FLIP |
18
+ |---|---|---|
19
+ | 体积 | ~8 KB | **~1.5 KB** |
20
+ | 协议尽调 | 必需 | **零风险** |
21
+ | 依赖风险 | 供应链 / 更新 | 无 |
22
+ | 能力 | 增删改 + 颜色 + 尺寸 + 表单值 | 只做平滑移动 |
23
+
24
+ **为不需要的能力引入 8 KB 第三方,要承担协议尽调与供应链风险,不划算。**
25
+ (协议已核实:MIT,无条款冲突。但核实这件事本身就花掉了 70 行 + 3 波调研,
26
+ 而且对方发新版时还要重新判断——这是持续的维护成本,不是一次性的。)
27
+
28
+ ---
29
+
30
+ ## FLIP 四步
31
+
32
+ ```
33
+ First 记录变化前的位置(getBoundingClientRect)
34
+ Last DOM 变化后记录新位置
35
+ Invert 用 transform 反向偏移回去(视觉上"没动")
36
+ Play 过渡到 transform: 0(看起来平滑滑过去)
37
+ ```
38
+
39
+ ### 用法
40
+
41
+ ```js
42
+ FLIP.flip(listEl, function () {
43
+ listEl.removeChild(row); // 任意 DOM 改动
44
+ });
45
+ ```
46
+
47
+ ```js
48
+ FLIP.remove(listEl, rowEl, function () {
49
+ // 退场动画结束、元素已从 DOM 移除,此时再触发其余项的 FLIP 重排
50
+ });
51
+ ```
52
+
53
+ 🔴 **顺序很关键**:`FLIP.remove` 先让被删的行淡出**并留在原位**,
54
+ 等它退场完毕再重排。直接先删再动画,其余项会瞬间跳过去,没有补间。
55
+
56
+ ### 必须给每个项一个 `data-flip-id`
57
+
58
+ ```html
59
+ <li class="list__item" data-flip-id="r42">…</li>
60
+ ```
61
+
62
+ 它是 FLIP 识别"这是同一个项"的**唯一依据**。缺了动画静默失效。
63
+
64
+ ---
65
+
66
+ ## 三个关键判断
67
+
68
+ ### 1. hover 绝不能让内容跳动
69
+
70
+ ```css
71
+ .list__item { border-left: 2px solid transparent; } /* 预留位 */
72
+ .list__item:hover { border-left-color: var(--border-decor-str); } /* 只改颜色 */
73
+ ```
74
+
75
+ 如果写成"hover 时才加 `border-left`",内容会**横移 2px**——列表"在抖"。
76
+ **border 的宽度必须始终占位。**
77
+
78
+ ### 2. Tab 不为每一行多停一次
79
+
80
+ ```css
81
+ .list__item:focus-within { border-left-color: var(--accent); }
82
+ ```
83
+
84
+ **不给整行加 `tabindex="0"`**。那样 20 行的列表,键盘用户要按 20 次 Tab 才能穿过。
85
+ 行内按钮本身可聚焦就够了,用 `:focus-within` 给整行视觉反馈。
86
+
87
+ 🔴 **配套要求**:行内操作用 `opacity: 0` 隐藏,**绝不能用 `display:none` /
88
+ `visibility:hidden`** —— 那会让"删除"按钮从 tab 顺序消失,键盘用户永远到不了。
89
+
90
+ ### 3. 涨跌色用**红涨绿跌**
91
+
92
+ ```css
93
+ .list__value--up { color: var(--danger); } /* 涨 = 红 */
94
+ .list__value--down { color: var(--success); } /* 跌 = 绿 */
95
+ ```
96
+
97
+ 🔴 与欧美惯例相反,是**刻意的**——本库面向中国 A 股用户,做反了会被当成数据出错。
98
+
99
+ ---
100
+
101
+ ## reduced-motion:跳过,不是缩短
102
+
103
+ ```js
104
+ if (prefersReducedMotion()) { return; } // 直接到最终态
105
+ ```
106
+
107
+ **"省略"优于"缩短"**。位置直接跳到最终态,不是"用 1ms 过渡"。
108
+
109
+ CSS 里另外关掉 hover 等微交互过渡——那是**不同层次**,别漏。
110
+
111
+ ---
112
+
113
+ ## 窄屏:行内操作必须常显
114
+
115
+ ```css
116
+ @media (max-width: 34rem) {
117
+ .list__actions { width: 100%; opacity: 1; }
118
+ }
119
+ ```
120
+
121
+ 触屏**没有 hover**,操作会彻底消失。
122
+
123
+ ---
124
+
125
+ ## 反模式
126
+
127
+ - ❌ **hover 时加 border / 改 padding** —— 内容横移,列表在抖
128
+ - ❌ **整行加 `tabindex="0"`** —— Tab 为每行多停一次
129
+ - ❌ **行内操作用 `display:none`** —— 从 tab 顺序消失,键盘到不了
130
+ - ❌ **缺 `data-flip-id`** —— 动画静默失效,且**不报错**
131
+ - ❌ **先删元素再动画** —— 其余项瞬间跳位,没有补间
132
+ - ❌ **长文本直接换行** —— 行高跳变,破坏列表节奏。用 `ellipsis` + `title`
133
+ - ❌ **涨跌色按欧美习惯** —— 会被当成数据出错
134
+ - ❌ **窄屏仍隐藏行内操作** —— 触屏没有 hover
135
+ - ❌ **reduced-motion 下"缩短"动画** —— 应当完全跳过
136
+
137
+ ---
138
+
139
+ ## 门禁能查与查不到的
140
+
141
+ | 项 | 能否机械检查 |
142
+ |---|---|
143
+ | class 引用完整性 | ✅ `refs.py` |
144
+ | JS 语法 | ✅ `node --check`(**不启动浏览器,零负载**)|
145
+ | 动画是否真的顺 | ❌ **必须真机看** |
146
+ | hover 是否真的不抖 | ❌ **必须真机看** |
147
+ | `data-flip-id` 是否齐全 | ❌ 目前靠人工,`refs.py` 查不了 |
148
+
149
+ 🔴 **诚实标注**:本 pattern 的核心价值(动画顺不顺、抖不抖)**静态检查查不出来**。
150
+ `demo.html` 的第一屏就是让你自己看的。
@@ -0,0 +1,225 @@
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/button/button.css">
12
+ <link rel="stylesheet" href="../../03-patterns/states/states.css">
13
+ <link rel="stylesheet" href="list.css">
14
+ <style>
15
+ .standfirst { font-size: var(--fs-md); color: var(--text-secondary);
16
+ max-width: var(--measure-lede); line-height: 1.55; margin: 0 0 var(--sp-6); }
17
+ p { max-width: var(--measure-prose); margin: 0 0 var(--sp-3); }
18
+ p.small { font-size: var(--fs-xs); color: var(--text-tertiary); line-height: 1.65; }
19
+ .bar { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin-bottom: var(--sp-4); }
20
+ </style>
21
+ </head>
22
+ <body class="page page">
23
+ <main class="wrap">">
24
+
25
+ <h1>删掉一行,其余行不该瞬移</h1>
26
+ <p class="standfirst">
27
+ 列表增删最容易做砸的地方不是动画本身,是<strong>动画缺失</strong>——
28
+ 项目瞬间跳位,用户还没看清删了哪条,下一秒已经找不到刚才在看的内容。
29
+ </p>
30
+
31
+ <div class="bar">
32
+ <button class="btn btn--sm btn--primary" id="add">添加一条</button>
33
+ <button class="btn btn--sm btn--secondary" id="sort">按记录数排序</button>
34
+ <button class="btn btn--sm btn--secondary" id="reverse">按名称倒序</button>
35
+ <button class="btn btn--sm btn--ghost" id="clear">清空</button>
36
+ </div>
37
+
38
+ <ul class="list" id="list" role="list"></ul>
39
+
40
+ <!-- 空状态:删光后显示,容器位置不变所以不跳动 -->
41
+ <div id="empty" hidden>
42
+ <div class="state state--empty">
43
+ <div class="state__icon" aria-hidden="true">□</div>
44
+ <h3 class="state__title">工作区里没有实体了</h3>
45
+ <p class="state__desc">导入一份 CSV,或者从沪深 300 全池里选一个起始集合。</p>
46
+ <div class="state__action">
47
+ <button class="btn btn--sm btn--primary" id="reload">导入 CSV</button>
48
+ </div>
49
+ </div>
50
+ </div>
51
+
52
+ <h2>请试这四件事</h2>
53
+
54
+ <p>
55
+ <strong>1. 删中间任意一条。</strong>上下两行会<strong>平滑滑过去</strong>,
56
+ 不是瞬间跳位。盯着两行之间的缝隙看。
57
+ </p>
58
+ <p>
59
+ <strong>2. 点"按记录数排序"。</strong>整列重排,每一行各自从旧位置滑到新位置——
60
+ 这也是 FLIP,只是位移不为零。
61
+ </p>
62
+ <p>
63
+ <strong>3. 鼠标横着扫过列表。</strong>行<strong>不会抖</strong>。
64
+ 做法是左侧线用 <code>transparent</code> 预留 2px,hover 时只改颜色;
65
+ 如果 hover 才加 border,内容会横移 1–2px,一眼能看出"在抖"。
66
+ </p>
67
+ <p>
68
+ <strong>4. 键盘 Tab 进列表。</strong>整行会亮起左边线——但 <strong>Tab 不会为每一行多停一次</strong>,
69
+ 只在行内按钮上停。这是 <code>:focus-within</code> 的用途。
70
+ </p>
71
+
72
+ <h2>为什么不用 auto-animate</h2>
73
+ <div class="note">
74
+ 评估过 auto-animate(8 KB min+gzip,精简档 装得下),但列表真正需要的
75
+ 只有"增删时其余项平滑移动"这一件事——<strong>为此引入 8 KB 第三方,
76
+ 要承担协议尽调与供应链风险,不划算</strong>。<br><br>
77
+ <code>flip.js</code> 用 <strong>FLIP</strong>(First-Last-Invert-Play)四步做这件事,
78
+ 约 40 行,<strong>零依赖</strong>。顺带保证了 <code>prefers-reduced-motion</code>
79
+ 下是<strong>完全跳过</strong>而不是"缩短"——位置直接到最终态。
80
+ </div>
81
+
82
+ <h2>涨跌色说明</h2>
83
+ <p class="small">
84
+ 本库用<strong>红涨绿跌</strong>,与中国 A 股用户习惯一致。
85
+ 这一点与欧美惯例相反,是刻意的——做反了会被当成数据出错。
86
+ </p>
87
+
88
+ </main>
89
+
90
+ <script src="flip.js"></script>
91
+ <script>
92
+ // 真实数据(某项目 企业分析工作区,WS_95 导入公司)——不是 User 123
93
+ var SEED = [
94
+ { name: '中国平安', rec: 26831, sector: '金融', chg: 2.41 },
95
+ { name: '宁德时代', rec: 19204, sector: '工业', chg: -0.87 },
96
+ { name: '招商银行', rec: 31556, sector: '金融', chg: 1.13 },
97
+ { name: '比亚迪', rec: 28912, sector: '工业', chg: 3.69 },
98
+ { name: '长江电力', rec: 17443, sector: '公用事业', chg: -1.24 },
99
+ { name: '中芯国际', rec: 15827, sector: '半导体', chg: 5.02 },
100
+ { name: '恒瑞医药', rec: 16205, sector: '医药', chg: -0.45 },
101
+ { name: '五粮液', rec: 14188, sector: '消费', chg: 1.87 }
102
+ ];
103
+
104
+ var EXTRA = [
105
+ { name: '美的集团', rec: 24310, sector: '家电', chg: 0.64 },
106
+ { name: '兴业银行', rec: 27905, sector: '金融', chg: -0.31 },
107
+ { name: '海康威视', rec: 18734, sector: '电子', chg: 2.11 }
108
+ ];
109
+
110
+ var list = document.getElementById('list');
111
+ var emptyBox = document.getElementById('empty');
112
+ var data = SEED.slice();
113
+ var seq = 100;
114
+
115
+ function esc(s) { return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;'); }
116
+
117
+ /* 千分位。🔴 不用 toLocaleString —— 老 WebView 上行为不一致,
118
+ 有些会返回带窄不换行空格(U+202F)的字符串,反而更糟。 */
119
+ function num(n) {
120
+ return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
121
+ }
122
+
123
+ /* closest 的兼容回退。老 WebView(尤其 IE 系)没有 Element.closest。 */
124
+ function closestOf(node, sel) {
125
+ if (node && node.closest) { return node.closest(sel); }
126
+ while (node && node !== document.body) {
127
+ if (node.getAttribute && node.getAttribute('data-del') !== null) { return node; }
128
+ node = node.parentNode;
129
+ }
130
+ return null;
131
+ }
132
+
133
+ function rowHtml(d) {
134
+ var id = d.id;
135
+ var dir = d.chg >= 0 ? 'up' : 'down';
136
+ var sign = d.chg >= 0 ? '+' : '';
137
+ /* 🔴 data-flip-id 是 FLIP 识别"同一个项"的唯一依据,缺了它动画整个失效。
138
+ <li> + .list__item 也必须有:容器是 <ul>,子元素必须是 <li>。 */
139
+ return '<li class="list__item" data-flip-id="' + id + '">' +
140
+ '<span class="list__main">' +
141
+ '<span class="list__title" title="' + esc(d.name) + '">' + esc(d.name) + '</span>' +
142
+ '<span class="list__meta">' + num(d.rec) +
143
+ ' 条记录 · ' + esc(d.sector) + '</span></span>' +
144
+ '<span class="list__value num list__value--' + dir + '">' +
145
+ sign + d.chg.toFixed(2) + '%</span>' +
146
+ '<span class="list__actions">' +
147
+ '<button class="btn btn--sm btn--ghost" data-del="' + id + '">移除</button>' +
148
+ '</span></li>';
149
+ }
150
+
151
+ function paint() {
152
+ // 一次性重建(真实项目里会做增量 diff;这里保持可读)
153
+ var html = '';
154
+ for (var i = 0; i < data.length; i++) { html += rowHtml(data[i]); }
155
+ list.innerHTML = html;
156
+ emptyBox.hidden = data.length > 0;
157
+ }
158
+
159
+ function repaint(mutate) {
160
+ FLIP.flip(list, function () {
161
+ mutate();
162
+ var html = '';
163
+ for (var i = 0; i < data.length; i++) { html += rowHtml(data[i]); }
164
+ list.innerHTML = html;
165
+ emptyBox.hidden = data.length > 0;
166
+ });
167
+ }
168
+
169
+ // 初始化:给每条一个稳定 id(FLIP 靠它识别"同一个项")
170
+ for (var i = 0; i < data.length; i++) { data[i].id = 'r' + (++seq); }
171
+
172
+ // 事件委托:一次绑定,处理所有行
173
+ list.addEventListener('click', function (e) {
174
+ var btn = closestOf(e.target, '[data-del]');
175
+ if (!btn || !btn.getAttribute) return;
176
+ var id = btn.getAttribute('data-del');
177
+ var victim = null;
178
+ for (var i = 0; i < data.length; i++) { if (data[i].id === id) victim = data[i]; }
179
+ if (!victim) return;
180
+
181
+ var node = list.querySelector('[data-flip-id="' + id + '"]');
182
+ var keep = victim;
183
+
184
+ // 先让被删的行淡出并留在原位,等它退场完毕再触发 FLIP 重排
185
+ FLIP.remove(list, node, function () {
186
+ repaint(function () {
187
+ for (var k = 0; k < data.length; k++) {
188
+ if (data[k].id === keep.id) { data.splice(k, 1); break; }
189
+ }
190
+ });
191
+ });
192
+ });
193
+
194
+ document.getElementById('add').onclick = function () {
195
+ var d = EXTRA.shift() || { name: '新增实体 ' + (seq), rec: 10000 + seq, sector: '未分类', chg: 0 };
196
+ d.id = 'r' + (++seq);
197
+ repaint(function () { data.unshift(d); });
198
+ };
199
+
200
+ document.getElementById('sort').onclick = function () {
201
+ repaint(function () {
202
+ data.sort(function (a, b) { return b.rec - a.rec; });
203
+ });
204
+ };
205
+
206
+ document.getElementById('reverse').onclick = function () {
207
+ repaint(function () {
208
+ data.sort(function (a, b) { return a.name.localeCompare(b.name, 'zh-Hans-CN'); });
209
+ });
210
+ };
211
+
212
+ document.getElementById('clear').onclick = function () {
213
+ repaint(function () { data = []; });
214
+ };
215
+
216
+ document.getElementById('reload').onclick = function () {
217
+ data = SEED.slice();
218
+ for (var i = 0; i < data.length; i++) { data[i].id = 'r' + (++seq); }
219
+ paint();
220
+ };
221
+
222
+ paint();
223
+ </script>
224
+ </body>
225
+ </html>