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,252 @@
1
+ /* ============================================================================
2
+ frontend-lib / 03-patterns / states / states.css
3
+ 容器四态:empty / loading / error / success
4
+
5
+ 依赖:01-tokens/tokens.css
6
+ 版本:A ✅ / B ✅ / C ✅
7
+
8
+ 🔴 为什么四态必须分开做:
9
+ 加载中和"没有数据"在视觉上都是"一片空白",但含义完全相反 ——
10
+ 前者是"在动",后者是"没东西"。混成一个,用户会以为系统坏了。
11
+ 这是 demo 和线上页面最容易露怯的一处。
12
+
13
+ 四态的共同要求:每一态都必须告诉用户**下一步能做什么**。
14
+ 只描述现状不给出口,等于把用户卡死。
15
+ ========================================================================= */
16
+
17
+ /* ---------------------------------------------------------------- 基类 */
18
+ .state {
19
+ display: flex;
20
+ flex-direction: column;
21
+ align-items: center;
22
+ justify-content: center;
23
+ gap: var(--sp-2);
24
+ padding: var(--sp-8) var(--sp-5);
25
+ text-align: center;
26
+ min-height: 200px;
27
+ }
28
+
29
+ .state__icon {
30
+ flex: none;
31
+ width: 48px;
32
+ height: 48px;
33
+ margin-bottom: var(--sp-2);
34
+ display: grid;
35
+ place-items: center;
36
+ border-radius: var(--r-pill);
37
+ background: var(--surface-sunken);
38
+ color: var(--text-tertiary);
39
+ }
40
+
41
+ .state__title {
42
+ margin: 0;
43
+ font-size: var(--fs-md);
44
+ font-weight: 500;
45
+ line-height: var(--lh-tight);
46
+ color: var(--text-primary);
47
+ }
48
+
49
+ .state__desc {
50
+ margin: 0;
51
+ max-width: 42ch; /* 说明文字要限制行长,否则在大屏上会拉成一条 */
52
+ font-size: var(--fs-sm);
53
+ color: var(--text-secondary);
54
+ line-height: var(--lh-base);
55
+ }
56
+
57
+ /* 🔴 动作区不是可选装饰,是四态存在的理由。
58
+ empty 没有动作 = 用户不知道怎么开始
59
+ error 没有动作 = 死路,只能刷新页面 */
60
+ .state__action {
61
+ margin-top: var(--sp-3);
62
+ display: flex;
63
+ gap: var(--sp-3);
64
+ flex-wrap: wrap;
65
+ justify-content: center;
66
+ }
67
+
68
+ /* ============================================================================
69
+ 1. empty —— 没有数据
70
+ =========================================================================
71
+ 必须三件事:说明为什么是空的 / 消除用户的自责 / 给出下一步
72
+ ------------------------------------------------------------------------ */
73
+ .state--empty .state__icon {
74
+ background: var(--surface-sunken);
75
+ color: var(--text-tertiary);
76
+ }
77
+
78
+ /* empty 的说明要写"为什么空",不是只写"没有数据"。
79
+ "还没有导入任何公司" 比 "暂无数据" 有用得多。 */
80
+
81
+ /* ============================================================================
82
+ 2. loading —— 加载中
83
+ =========================================================================
84
+ 两种形态,按结构是否已知选:
85
+
86
+ skeleton(骨架屏):结构已知 —— 知道会有标题+两行文字+一张图
87
+ spinner :结构未知 —— 不知道会来什么
88
+
89
+ 🔴 别在结构未知时用骨架屏,那是在撒谎:画出来的结构跟实际不一致,
90
+ 内容到位时布局会剧烈跳动。
91
+ ------------------------------------------------------------------------ */
92
+ .state--loading {
93
+ /* 没有图标和标题的复杂布局,只放一个指示 */
94
+ gap: var(--sp-3);
95
+ }
96
+
97
+ /* spinner */
98
+ .spinner {
99
+ width: 24px;
100
+ height: 24px;
101
+ border: 2.5px solid var(--border-control);
102
+ border-top-color: var(--accent);
103
+ border-radius: 50%;
104
+ animation: spinner-rotate 700ms linear infinite;
105
+ }
106
+ @keyframes spinner-rotate { to { transform: rotate(360deg); } }
107
+
108
+ /* ---------------------------------------------------------------- 骨架屏
109
+ 骨架屏的价值不在"好看",在于**内容到位时不跳动**。
110
+ 所以骨架的形状必须尽量贴合真实内容的结构和尺寸。
111
+ ---------------------------------------------------------------- */
112
+ .skeleton {
113
+ /* 装饰性色,不参与对比度检查(它不承载文字)。
114
+ 🔴 改为令牌:原值 #ece8de/#f5f2ea 是**硬编码的浅米色**,
115
+ 暗色模式下会是两块刺眼的白块 —— 与 --text-on-accent 同类问题:
116
+ **写死颜色 = 主题切换时必然失效。** */
117
+ --sk-base: var(--surface-sunken);
118
+ --sk-hi: var(--border-decor);
119
+
120
+ background: var(--sk-base);
121
+ border-radius: var(--r-sm);
122
+ position: relative;
123
+ overflow: hidden;
124
+ }
125
+ .skeleton::after {
126
+ content: "";
127
+ position: absolute;
128
+ inset: 0;
129
+ background: linear-gradient(90deg,
130
+ transparent 0%, var(--sk-hi) 50%, transparent 100%);
131
+ transform: translateX(-100%);
132
+ animation: skeleton-sweep 1.4s ease-in-out infinite;
133
+ }
134
+ @keyframes skeleton-sweep {
135
+ to { transform: translateX(100%); }
136
+ }
137
+
138
+ .skeleton--text { height: 1em; margin-block: 0.35em; border-radius: 4px; }
139
+ .skeleton--title { height: 1.4em; width: 60%; }
140
+ .skeleton--avatar { width: 40px; height: 40px; border-radius: var(--r-pill); }
141
+ .skeleton--block { height: 120px; }
142
+ /* 最后一行短一点,看起来更像真实段落 */
143
+ .skeleton--text:last-child { width: 70%; }
144
+
145
+ /* 骨架屏的"闪屏"问题:请求在 300ms 内返回时,骨架屏闪一下反而更糟,
146
+ 正确做法是延迟 300ms 再显示 loading 态。
147
+
148
+ ⚠️ 这个能力**不由本库提供**:曾经有一个 `.state--delayed` 类,
149
+ 但它承诺的"延迟显现"全库没有任何 JS 去驱动(`data-state="shown"`
150
+ 得使用者自己加),读起来像本库已实现 ⇒ 已在 0.8.0 移除(0.7.7 登记)。
151
+ 现在需要就自己写两行,见 `README.md` 的「移除记录」一节。 */
152
+
153
+ /* ============================================================================
154
+ 3. error —— 出错
155
+ =========================================================================
156
+ 🔴 error 态必须可恢复。只写"出错了"是死路,用户只能刷新页面,
157
+ 而刷新往往解决不了问题(比如参数错了)。
158
+
159
+ 三件事:说清出了什么事 / 说清是不是用户的错 / 给重试或返回的出口
160
+ ------------------------------------------------------------------------ */
161
+ .state--error .state__icon {
162
+ background: var(--danger-bg);
163
+ color: var(--danger); /* 6.26 AA */
164
+ }
165
+ .state--error .state__title { color: var(--danger); }
166
+
167
+ /* 技术细节(错误码、堆栈)单独折叠,别混进主文案。
168
+ 用户要看的是"我该怎么办",不是"NullPointerException"。 */
169
+ .state__detail {
170
+ margin-top: var(--sp-3);
171
+ font-family: var(--mono); /* 原为写死的 ui-monospace,统一走令牌 */
172
+ font-size: var(--fs-xs);
173
+ color: var(--text-tertiary);
174
+ background: var(--surface-sunken);
175
+ padding: var(--sp-2) var(--sp-3);
176
+ border-radius: var(--r-sm);
177
+ max-width: 100%;
178
+ overflow-x: auto;
179
+ }
180
+
181
+ /* ============================================================================
182
+ 4. success —— 成功
183
+ =========================================================================
184
+ 🔴 success 是**过渡态**,不是停留态。
185
+ 容器级 success 只在"这一步彻底结束"时出现(提交完成、支付完成),
186
+ 并且必须给出口:下一步去哪。
187
+
188
+ 普通操作的成功反馈用 button 的 `data-state="success"`(1.2s 自动消失),
189
+ 不要用容器级 success —— 那会把一个瞬间反馈做成一个页面。
190
+ ------------------------------------------------------------------------ */
191
+ /* 🔴 删除 .state--success
192
+ 理由:**它自己的注释就写着"不要用容器级 success"**,
193
+ 却把样式留在文件里 —— 这是最坏的一种:
194
+ 注释说别用,CSS 却摆在那里,读者只会看到"有这个能力"。
195
+ 瞬时反馈用 button 的 `data-state="success"`(1.2s 自动消失)。
196
+ ⇒ 要"落盘即用"的成功态,就该有一个真会被用到的类,
197
+ 而不是一个明确标注"别用"的。 */
198
+
199
+ /* ============================================================================
200
+ 无障碍
201
+ =========================================================================
202
+ 1) loading 容器要标 aria-busy="true"
203
+ 2) error / success 动态出现时用 role="alert" 或 aria-live="polite",
204
+ 否则屏幕阅读器用户完全不知道状态变了
205
+ 3) 骨架屏是纯装饰,标 aria-hidden="true" —— 不然会被读成一堆无意义的色块
206
+ ------------------------------------------------------------------------ */
207
+
208
+ /* ============================================================================
209
+ reduced-motion 的两种不同处理 —— 这里的区别很重要
210
+ =========================================================================
211
+ spinner : **保留但放慢**。它停了用户会以为页面卡死。
212
+ reduced-motion 针对的是大幅位移(前庭功能障碍),原地旋转不属于此类。
213
+
214
+ skeleton: **可以停**。静态的占位块仍然传达"这里有内容正在来",
215
+ 去掉扫光不损失信息,而扫光恰好是横向位移 —— 正是要减少的那类。
216
+
217
+ 同样是"加载中的动画",一个保留一个去掉,判断依据是**去掉后是否损失信息**。
218
+ ========================================================================= */
219
+ @media (prefers-reduced-motion: reduce) {
220
+ .spinner {
221
+ animation-duration: 1.2s !important; /* 放慢,保留 */
222
+ }
223
+ .skeleton::after {
224
+ animation: none; /* 停止扫光,保留静态占位 */
225
+ transform: none;
226
+ }
227
+ }
228
+
229
+ /* ============================================================================
230
+ 反模式
231
+ =========================================================================
232
+ 1. empty 和 loading 用同一个样式
233
+ 用户分不清是"在加载"还是"真没有"。
234
+
235
+ 2. empty 态不放动作按钮
236
+ "暂无数据" 四个字把用户卡死在那儿。
237
+
238
+ 3. error 态没有重试出口
239
+ 只能刷新页面,而刷新常常解决不了问题。
240
+
241
+ 4. 结构未知时用骨架屏
242
+ 画出来的结构跟实际不一致,内容到位时布局剧烈跳动。
243
+
244
+ 5. 请求 <300ms 也显示骨架屏
245
+ 闪一下,比不显示更糟。要延迟显示。
246
+
247
+ 6. 把普通操作的成功做成整页 success
248
+ 成功是过渡态。用 button 的 `data-state="success"`。
249
+
250
+ 7. 骨架屏不标 aria-hidden
251
+ 屏幕阅读器会把它读成一堆无意义内容。
252
+ ========================================================================= */
@@ -0,0 +1,84 @@
1
+ # tabs · 标签页
2
+
3
+ 同一位置切换**同层级**的几块内容。
4
+
5
+ ## 结构(APG 要求,不是我的设计)
6
+
7
+ ```html
8
+ <div class="tabs" data-tabs>
9
+ <div class="tabs__list" role="tablist" aria-orientation="horizontal" aria-label="…">
10
+ <button class="tabs__tab" role="tab" id="t1"
11
+ aria-controls="p1" aria-selected="true">基本信息</button>
12
+ <button class="tabs__tab" role="tab" id="t2"
13
+ aria-controls="p2" aria-selected="false">高级设置</button>
14
+ </div>
15
+ <div class="tabs__panel" role="tabpanel" id="p1" aria-labelledby="t1">…</div>
16
+ <div class="tabs__panel" role="tabpanel" id="p2" aria-labelledby="t2" hidden>…</div>
17
+ </div>
18
+ ```
19
+
20
+ **tab 必须是 `<button>`** —— APG 明确要求。
21
+ 用 `<div role="tab">` 键盘上根本 Tab 不到,也不响应 Enter/Space。
22
+
23
+ ## 键盘契约
24
+
25
+ | 键 | 行为 |
26
+ |---|---|
27
+ | `Tab` | 进入组时落在**当前选中**的 tab;再按 Tab 进入面板内容 |
28
+ | `← →` | 上一个 / 下一个 tab(并激活)|
29
+ | `Home` / `End` | 第一个 / 最后一个 tab |
30
+ | `Enter` / `Space` | 仅**手动模式**下用于激活 |
31
+
32
+ ## 🔴 roving tabindex(本组件最容易写错的地方)
33
+
34
+ 一组 tab 里**只有一个**是 `tabindex="0"`,其余 `-1`。
35
+
36
+ **为什么要这样**:否则 Tab 键会**逐个穿过所有 tab** ——
37
+ 一排 5 个 tab 要按 5 次 Tab 才能进内容区。
38
+
39
+ **更关键**:那个 `"0"` **必须跟着选中项走**。
40
+ 只在初始化时设一次是最常见的 bug(用户激活第 3 个 tab 后,Tab 键还会回到第 1 个)。
41
+
42
+ ## 两种激活模式
43
+
44
+ | 模式 | 类 | 方向键 | 何时用 |
45
+ |---|---|---|---|
46
+ | **自动**(默认)| — | 移动即切换 | 面板轻 |
47
+ | **手动** | `.tabs--manual` | 只移动焦点,Enter 才切 | 面板重(避免读屏不停播报)|
48
+
49
+ ## 纵向
50
+
51
+ `aria-orientation="vertical"` + `.tabs--vertical`。
52
+
53
+ ## 一个规范与现实的张力
54
+
55
+ APG 要求「在 tab 上按 Tab ⇒ 焦点进入面板内容」。
56
+ **但全是静态文字的面板没有任何可聚焦元素** ⇒ Tab 会直接跳过整个面板。
57
+
58
+ 实测确认(`tabs-contract.js` 里就是用这个测的):
59
+ demo 的面板里放了按钮,`Tab` 序列是
60
+ `t1 → 面板内按钮 → 下一个 tab 组` —— **符合规范**。
61
+
62
+ ⚠️ 纯文字面板请自己加可聚焦元素(链接、按钮、或 `tabindex="-1"` 的容器)。
63
+
64
+ ## 语法约束
65
+
66
+ `tabs.js` **刻意用 ES5**(无箭头函数 / const / let / 模板字符串)——
67
+ 精简档(某项目 上的 WebView)版本不确定。
68
+
69
+ 也**刻意不用 `Element.closest` / `Array.filter`**(ES5 之后才有),
70
+ 自己写了 15 行等价实现 —— 与同目录的 `list.js` / `overlay.js` 保持一致。
71
+ **一致比方便重要。**
72
+
73
+ ## 验证
74
+
75
+ `tabs-contract.js` **28 项全过**(用 `page.keyboard.press()` 真实按键):
76
+
77
+ - 结构:`<button>` · `role=tablist` · `aria-controls` 双向互指 · `aria-orientation`
78
+ - roving:唯一 `0` · **跟着选中项走** · 循环后仍唯一
79
+ - 键盘:`←→` · `Home`/`End` · **循环** · **跳过 `aria-disabled`**
80
+ - Tab 进出:进组落在选中项 · 进面板内容 · **不逐个穿过同组**
81
+ - 手动模式:方向键不切面板但移焦点 · Enter 才激活
82
+ - axe 0 违规 / 30 条通过
83
+
84
+ 判别力验证:破坏 roving(全部设成 `0`)⇒ **5 项立即被抓**。
@@ -0,0 +1,139 @@
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>tabs · 标签页</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/tabs/tabs.css">
13
+ <style>
14
+ .log { font-family: var(--mono); font-size: var(--fs-xs);
15
+ color: var(--text-secondary); min-height: 1.5em; }
16
+ </style>
17
+ </head>
18
+ <body class="page page">
19
+ <main class="wrap">
20
+ <h1>tabs · 标签页</h1>
21
+ <p class="lede">
22
+ 标签页用于在同一位置切换<strong>同层级的几块内容</strong>。
23
+ 键盘行为完全按 APG Tabs Pattern:方向键切换、Home/End 跳首尾。
24
+ </p>
25
+
26
+ <h2 class="rule-top">基本用法(自动激活)</h2>
27
+ <div class="tabs" data-tabs>
28
+ <div class="tabs__list" role="tablist" aria-orientation="horizontal" aria-label="设置分组">
29
+ <button class="tabs__tab" role="tab" id="t1" aria-controls="p1"
30
+ aria-selected="true">基本信息</button>
31
+ <button class="tabs__tab" role="tab" id="t2" aria-controls="p2"
32
+ aria-selected="false">高级设置</button>
33
+ <button class="tabs__tab" role="tab" id="t3" aria-controls="p3"
34
+ aria-selected="false">日志</button>
35
+ </div>
36
+ <div class="tabs__panel" role="tabpanel" id="p1" aria-labelledby="t1">
37
+ <p>公司名称、代码、行业分类。</p>
38
+ <!-- 🔴 面板里必须有**可聚焦元素**,否则按 Tab 会直接跳过整个面板
39
+ —— APG 要求「在 tab 上按 Tab ⇒ 焦点进入面板内容」。
40
+ 全是静态文字的面板天然没有停靠点,这是规范与现实的真实张力。 -->
41
+ <p><button class="btn btn--md btn--secondary">面板里的按钮(可 Tab 到)</button></p>
42
+ </div>
43
+ <div class="tabs__panel" role="tabpanel" id="p2" aria-labelledby="t2" hidden>
44
+ <p>数据源、口径、复核规则。</p>
45
+ </div>
46
+ <div class="tabs__panel" role="tabpanel" id="p3" aria-labelledby="t3" hidden>
47
+ <p>抓取记录与错误。</p>
48
+ </div>
49
+ </div>
50
+ <p class="log" id="log1">切到哪个:<b id="logv1">基本信息</b></p>
51
+
52
+ <h2 class="rule-top">禁用某个 tab</h2>
53
+ <div class="tabs" data-tabs>
54
+ <div class="tabs__list" role="tablist" aria-orientation="horizontal" aria-label="带禁用">
55
+ <button class="tabs__tab" role="tab" id="d1" aria-controls="dp1"
56
+ aria-selected="true">可用</button>
57
+ <button class="tabs__tab" role="tab" id="d2" aria-controls="dp2"
58
+ aria-selected="false" aria-disabled="true">未开通</button>
59
+ <button class="tabs__tab" role="tab" id="d3" aria-controls="dp3"
60
+ aria-selected="false">可用</button>
61
+ </div>
62
+ <div class="tabs__panel" role="tabpanel" id="dp1" aria-labelledby="d1">
63
+ <p>方向键会<strong>跳过</strong>禁用的 tab。</p>
64
+ </div>
65
+ <div class="tabs__panel" role="tabpanel" id="dp2" aria-labelledby="d2" hidden>
66
+ <p>你看不到这一页。</p>
67
+ </div>
68
+ <div class="tabs__panel" role="tabpanel" id="dp3" aria-labelledby="d3">
69
+ <p>按两次 ArrowRight 能到这里(跳过了未开通)。</p>
70
+ </div>
71
+ </div>
72
+
73
+ <h2 class="rule-top">手动激活(面板内容重时用)</h2>
74
+ <div class="tabs tabs--manual" data-tabs>
75
+ <div class="tabs__list" role="tablist" aria-orientation="horizontal" aria-label="手动激活">
76
+ <button class="tabs__tab" role="tab" id="m1" aria-controls="mp1"
77
+ aria-selected="true">第一段</button>
78
+ <button class="tabs__tab" role="tab" id="m2" aria-controls="mp2"
79
+ aria-selected="false">第二段</button>
80
+ </div>
81
+ <div class="tabs__panel" role="tabpanel" id="mp1" aria-labelledby="m1">
82
+ <p>方向键<strong>只移动焦点</strong>,需按 Enter 才切 —— 读屏不会不停播报。</p>
83
+ </div>
84
+ <div class="tabs__panel" role="tabpanel" id="mp2" aria-labelledby="m2" hidden>
85
+ <p>按了 Enter 才到这里。</p>
86
+ </div>
87
+ </div>
88
+
89
+ <h2 class="rule-top">键盘契约(APG / Radix)</h2>
90
+ <div class="box">
91
+ <p>
92
+ <strong>Tab</strong> 进入组时落在<strong>当前选中</strong>的 tab;再按 Tab 进面板<br>
93
+ <strong>← →</strong> 上一个 / 下一个 tab(并激活)<br>
94
+ <strong>Home / End</strong> 第一个 / 最后一个 tab
95
+ </p>
96
+ <p class="small">
97
+ <strong>roving tabindex</strong>:一组 tab 里只有一个是 <code>tabindex="0"</code>,
98
+ 其余全是 <code>-1</code>。否则 Tab 键会<strong>逐个穿过所有 tab</strong>
99
+ (一排 5 个要按 5 次)。
100
+ 更关键的是——<strong>那个 "0" 必须跟着选中项走</strong>,
101
+ 只在初始化时设一次是最常见的 bug。
102
+ </p>
103
+ </div>
104
+
105
+ <h2 class="rule-top">反例</h2>
106
+ <div class="box bad">
107
+ <h3>反例:用 div 当 tab</h3>
108
+ <p class="small">
109
+ <code>&lt;div role="tab"&gt;</code> 键盘上根本<strong>Tab 不到</strong>
110
+ (除非手写 <code>tabindex</code>),也不会响应 Enter/Space。
111
+ APG 明确要求 tab 是 <code>&lt;button role="tab"&gt;</code> ——
112
+ 用原生 button,键盘行为免费且必然正确。
113
+ </p>
114
+ </div>
115
+ <div class="box bad">
116
+ <h3>反例:每个 tab 都写 tabindex="0"</h3>
117
+ <p class="small">
118
+ 看起来"都能 Tab 到",但一排 5 个 tab 就要按 <strong>5 次 Tab</strong>
119
+ 才能进内容区。Tab 键应该是<strong>进出</strong>控件的,
120
+ 组内移动是方向键的活。
121
+ </p>
122
+ </div>
123
+ </main>
124
+ <script src="tabs.js"></script>
125
+ <script>
126
+ // 仅为 demo:显示切换到哪个(组件本身零依赖)
127
+ (function () {
128
+ var list = document.querySelectorAll('.tabs__list');
129
+ if (!list.length) return;
130
+ var first = document.querySelector('.tabs');
131
+ var v = document.getElementById('logv1');
132
+ first.addEventListener('click', function (e) {
133
+ var t = e.target.closest ? e.target.closest('[role="tab"]') : null;
134
+ if (t && v) v.textContent = t.textContent;
135
+ });
136
+ })();
137
+ </script>
138
+ </body>
139
+ </html>
@@ -0,0 +1,172 @@
1
+ /* ============================================================================
2
+ Tabs — 标签页(APG: Tabs Pattern)
3
+ ============================================================================
4
+ 🔴 键盘契约(照抄 Radix / APG,不是我的设计)
5
+ ------------------------------------------------
6
+ Tab 焦点进入 tab 组时,落在**当前选中**的 tab 上;
7
+ 在 tab 上再按 Tab ⇒ 走到**面板内容**
8
+ ArrowRight 下一个 tab(并激活)
9
+ ArrowLeft 上一个 tab(并激活)
10
+ ArrowDown 下一个 tab(纵向时)
11
+ ArrowUp 上一个 tab(纵向时)
12
+ Home 第一个 tab(并激活)
13
+ End 最后一个 tab(并激活)
14
+
15
+ 🔴 roving tabindex:一组 tab 里**只有一个**是 `tabindex="0`,
16
+ 其余是 `tabindex="-1"`。
17
+ 为什么要这样:否则 Tab 键会**逐个穿过所有 tab**(一排 5 个 tab 要按 5 次 Tab)。
18
+ roving 让整组只占**一个 Tab 停靠点**,组内用方向键移动 ——
19
+ 这是 APG 的要求,也是"键盘用户效率"的关键。
20
+
21
+ 🔴 激活方式有两种(Radix 叫 activationMode)
22
+ ------------------------------------------------
23
+ automatic(默认) 方向键移动即切换面板
24
+ manual 方向键只移动焦点,需按 Enter/Space 才切
25
+
26
+ 本组件**默认 automatic**(多数场景够用),
27
+ 加 `.tabs--manual` 变成手动 —— 面板内容重时用(避免读屏不停播报)。
28
+ ========================================================================= */
29
+
30
+ .tabs {
31
+ display: flex;
32
+ flex-direction: column;
33
+ gap: var(--sp-4);
34
+ }
35
+
36
+ /* 手动激活模式
37
+ 🔴 补上真实差异(不是补一条空规则凑数)。
38
+ `.tabs--manual` 是**行为开关**(tabs.js 读它决定方向键要不要切面板)——
39
+ 之前它只存在于 JS 与文档里、CSS 一个字都没有 ⇒ `refs` 门禁报「class 未定义」。
40
+
41
+ 为什么给它视觉差异:
42
+ 自动模式下,方向键移动 = 面板**立刻**切换,用户需要一眼看出现在是哪个;
43
+ 手动模式下,面板要按 Enter 才切,**选中项与"当前显示"可能不一致** ——
44
+ 这时反而**不该**太强调,否则会误导"面板已经切了"。
45
+
46
+ ⇒ 手动模式:选中项**不加深色、不加粗**,只保留底部强调线。
47
+ 这是诚实:还没生效的事,不该看起来已经生效。 */
48
+ .tabs--manual .tabs__tab[aria-selected="true"] {
49
+ color: var(--text-secondary);
50
+ font-weight: 400;
51
+ }
52
+
53
+ @media (hover: hover) and (pointer: fine) {
54
+ .tabs--manual .tabs__tab[aria-selected="true"]:hover {
55
+ color: var(--text-primary);
56
+ }
57
+ }
58
+
59
+ /* ---- tab 组(那一排) */
60
+ .tabs__list {
61
+ display: flex;
62
+ gap: var(--sp-1);
63
+ border-bottom: 1px solid var(--border-decor);
64
+ position: relative;
65
+
66
+ /* 🔴 窄屏横滑,不能撑破整页:320 视口 + 三个 tab ⇒ scrollWidth 320→331。
67
+ ⚠️ overflow-y 必须显式 hidden —— 一个轴 auto 会让另一轴也 auto 化,
68
+ 否则选中态的 border-bottom 会被裁。 */
69
+ overflow-x: auto;
70
+ overflow-y: hidden;
71
+ scrollbar-width: none; /* 下面那条 border 已经暗示"这是一条栏" */
72
+ }
73
+
74
+ .tabs__list::-webkit-scrollbar { display: none; }
75
+
76
+
77
+ /* 纵向变体 */
78
+ .tabs--vertical {
79
+ flex-direction: row;
80
+ gap: var(--sp-4);
81
+ }
82
+
83
+ .tabs--vertical .tabs__list {
84
+ flex-direction: column;
85
+ border-bottom: 0;
86
+ border-inline-end: 1px solid var(--border-decor);
87
+ }
88
+
89
+ /* ---- 单个 tab */
90
+ .tabs__tab {
91
+ appearance: none;
92
+ background: none;
93
+ border: 0;
94
+ /* 负 margin 让 tab 的下边框与 tablist 的重叠,选中时"接管"那条线 */
95
+ margin-bottom: -1px;
96
+ padding: var(--sp-3) var(--sp-4);
97
+ font: inherit;
98
+ font-size: var(--fs-base);
99
+ color: var(--text-secondary);
100
+ cursor: pointer;
101
+ border-bottom: 2px solid transparent;
102
+ transition: color var(--dur-fast) var(--ease-out),
103
+ border-color var(--dur-fast) var(--ease-out);
104
+ white-space: nowrap;
105
+ /* 配合上面 .tabs__list 的横滑:tab 不许被压缩,
106
+ 压窄了文字换行,比让用户滑一下更糟 */
107
+ flex: 0 0 auto;
108
+ }
109
+
110
+ .tabs--vertical .tabs__tab {
111
+ margin-bottom: 0;
112
+ margin-inline-end: -1px;
113
+ border-bottom: 0;
114
+ border-inline-end: 2px solid transparent;
115
+ text-align: start;
116
+ }
117
+
118
+ /* 选中态 */
119
+ .tabs__tab[aria-selected="true"] {
120
+ color: var(--text-primary);
121
+ font-weight: 600;
122
+ border-bottom-color: var(--accent);
123
+ }
124
+
125
+ .tabs--vertical .tabs__tab[aria-selected="true"] {
126
+ /* 🔴 必须与上面 `.tabs--vertical .tabs__tab` 的 border-inline-end 同一侧,
127
+ 否则 RTL 下选中线画在错误的一边(物理 border-right-* 不会跟着翻转)。 */
128
+ border-inline-end-color: var(--accent);
129
+ }
130
+
131
+ /* 🔴 hover 门控:触屏上"点一下"会先点亮 hover 再 click ⇒ 粘住不褪 */
132
+ @media (hover: hover) and (pointer: fine) {
133
+ .tabs__tab:not([aria-disabled="true"]):hover {
134
+ color: var(--text-primary);
135
+ background: var(--surface-sunken);
136
+ }
137
+ }
138
+
139
+ .tabs__tab[aria-disabled="true"] {
140
+ color: var(--text-tertiary);
141
+ cursor: not-allowed;
142
+ }
143
+
144
+ /* 🔴 焦点环:tab 是 button,用 :focus-visible(鼠标点击不该出环) */
145
+ .tabs__tab:focus-visible {
146
+ outline: 2px solid var(--accent);
147
+ outline-offset: -2px; /* 往内,避免被 tablist 裁掉 */
148
+ border-radius: var(--r-sm) var(--r-sm) 0 0;
149
+ }
150
+
151
+ /* ---- 面板 */
152
+ .tabs__panel {
153
+ padding: var(--sp-4) 0;
154
+ }
155
+
156
+ .tabs__panel[hidden] {
157
+ display: none; /* 🔴 必须是 display:none
158
+ hidden 属性在某些 CSS 重置下会被覆盖 */
159
+ }
160
+
161
+ /* 动画(可选):只做淡入,不做高度动画 ——
162
+ 高度动画需要 JS 测量,且在 reduced-motion 下要能被关掉 */
163
+ @media (prefers-reduced-motion: no-preference) {
164
+ .tabs__panel--enter {
165
+ animation: tabs-fade var(--dur-base) var(--ease-out);
166
+ }
167
+ }
168
+
169
+ @keyframes tabs-fade {
170
+ from { opacity: 0; }
171
+ to { opacity: 1; }
172
+ }