agentcss 0.0.0-stage → 0.11.3

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 +3138 -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 +497 -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 +311 -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,353 @@
1
+ /* ============================================================================
2
+ frontend-lib / 02-primitives / button / button.css
3
+ 按钮 · 状态矩阵完整实现
4
+
5
+ 依赖:01-tokens/tokens.css(必须先加载)
6
+ 版本:A ✅ / B ✅ / C ✅ —— 纯 CSS,无 JS 依赖,可整段复制
7
+
8
+ 🔴 关于"九态"的正确理解:不是每个组件都要九个态。
9
+ 按钮取前六态 + success;容器/列表取后四态。混着套会做出多余的东西。
10
+
11
+ ┌ 交互元素(按钮、链接、开关)
12
+ │ default → hover → active → focus-visible → disabled → loading → success
13
+ └ 容器(列表、卡片区、表单体)
14
+ content → empty → loading → error → success
15
+
16
+ 按钮没有 empty / error 态 —— "按钮是空的"没有意义。
17
+ 容器没有 hover / active 态 —— 容器不响应按压。
18
+ ========================================================================= */
19
+
20
+ /* ---------------------------------------------------------------- 基础
21
+ 🔴 改用 **grid 叠放**(原 inline-flex + absolute 定位):
22
+
23
+ 实测反馈"按着不动,加载内容就叠上来了"。根因是:
24
+ inline-flex 容器里,文字**没有溢出保护**,
25
+ 而 spinner 是 position:absolute 居中 —— 文字一长就溢出按钮边界,
26
+ 与 spinner **物理重叠**,两个词叠在一起。
27
+
28
+ 改法:把 label 与 spinner 放进**同一个 grid 单元**(grid-area: stack)。
29
+ 这样:
30
+ · 物理上不可能重叠(同一格)
31
+ · 容器宽度 = max(两者) ⇒ 切换时宽度**自动不变**,不需要手算
32
+ · 不再依赖 absolute 定位 ⇒ 不会和 :active 的 transform 打架
33
+ */
34
+ .btn {
35
+ /* 全部使用 token。出现任何硬编码的 px 色值都是分层崩了。 */
36
+ display: inline-grid;
37
+ grid-template-areas: "stack";
38
+ place-items: center;
39
+
40
+ font-family: inherit;
41
+ font-size: var(--fs-sm);
42
+ font-weight: 500;
43
+ line-height: 1;
44
+ white-space: nowrap;
45
+
46
+ /* 🔴 补回:改 grid 那一版把 height / padding 弄丢了,
47
+ 而 .btn--sm/md/lg 才有它们 —— 于是「不写 size 类」的地方(弹窗按钮、
48
+ JS 生成的按钮)就只剩内容高度 + 零 padding,**文字直接顶着边框**。
49
+ 实测反馈"框又小、文字顶着方框",根因就在这里。
50
+
51
+ 默认尺寸必须是**可用的**(44px 触摸目标),不该要求每个调用方都记得加 size 类。 */
52
+ min-height: 44px;
53
+ padding: 0 var(--sp-5);
54
+
55
+ border: 1px solid transparent;
56
+ border-radius: var(--r-sm);
57
+ cursor: pointer;
58
+ user-select: none;
59
+ text-decoration: none;
60
+
61
+ /* 🔴 兜底:即使内容异常长也不许溢出边界 */
62
+ overflow: hidden;
63
+ max-width: 100%;
64
+
65
+ /* 动效只用 token 里的那一组 —— 一源原则 */
66
+ transition: background-color var(--dur-fast) var(--ease-out),
67
+ border-color var(--dur-fast) var(--ease-out),
68
+ color var(--dur-fast) var(--ease-out),
69
+ transform var(--dur-fast) var(--ease-out);
70
+ }
71
+
72
+ /* ---------------------------------------------------------------- 尺寸
73
+ 三档。md 是 44px —— 不是随便定的:
74
+ WCAG 2.5.5 增强级要求触摸目标 >= 44x44,这是手指的物理尺寸,不是审美。
75
+ sm 只用在表格行内等密集处(那里用户用鼠标,不是手指)。
76
+ ---------------------------------------------------------------- */
77
+ /* 🔴 每个变体都要同时写 min-height,且选择器用 **.btn.btn--xx 双类**
78
+ (特异度 0,2,0 > .btn 的 0,1,0 ⇒ 不依赖源码顺序,改文件也不会失效)
79
+ —— 原来的 .btn 上的 min-height:44px
80
+ 在层叠里永远赢过 height,只写 height 的变体会被压回 44px,"小按钮"就没了。 */
81
+ /* ============================================================================
82
+ 🔴 **修正变体高度被压平**(clicktest 门禁实测:sm/md/lg 全是 44px)
83
+
84
+ 真因(做了 5 组浏览器实验才定位,不是猜的):
85
+ `.btn` 基类是 `display: inline-grid` + `grid-template-areas: "stack"`
86
+ ⇒ 网格行是 **auto**(由内容撑开)
87
+ ⇒ 变体里同时写 `min-height: 32px` **和** `height: 32px` 时,
88
+ **`height` 反而成了下限**,把 32px 顶不上去;
89
+ ⇒ 实测:单独把 `min-height` 压到 32px ⇒ 高度**立刻变成 32px** ✅
90
+ (加 `grid-template-rows: 32px` 也是 32,效果相同)
91
+
92
+ ⚠️ 教训(grid 容器特有):
93
+ **grid/flex 容器上不要同时写 `height` 与 `min-height`** ——
94
+ `height` 会变成硬下限,变体就再也压不下去。
95
+ 只写 `min-height`(下限可被内容顶大)才是变体该用的。
96
+
97
+ ⇒ 修法:变体**只保留 `min-height`**,删掉 `height`。
98
+ ============================================================================ */
99
+ .btn.btn--sm {
100
+ /* 🔴 修正(button 契约抓到:命中区 60×32,违反 WCAG 2.5.5 AAA)
101
+
102
+ ⚠️ 上一轮我写的注释是**错的**:「命中区由基类的 min-height 44px 兜底」
103
+ —— 事实是 `.btn.btn--sm` 的**特异性 (0,2,0) 高于**基类 `.btn` (0,1,0),
104
+ 所以 `min-height: 32px` **把基类的 44px 盖掉了** ⇒ 命中区真的只有 32px。
105
+
106
+ ⭐ 正确做法(业界标准):**视觉 32px,命中区 44px**,两者分开。
107
+ 用 `::after` 伪元素在垂直方向各扩 6px(不占布局、不影响相邻元素):
108
+
109
+ 视觉高度 = min-height 32px (看起来就是个小按钮)
110
+ 命中区域 = 32 + 6 + 6 = 44px (手指落点够大)
111
+
112
+ ⚠️ 为什么不用 `padding` 撑:会让按钮**真的变高**,
113
+ 视觉上就不是"小按钮"了,与设计意图相反。
114
+ ⚠️ 为什么 `position: relative` 是必需的:
115
+ 伪元素要相对按钮定位,按钮本身必须是定位上下文。 */
116
+ position: relative;
117
+ min-height: var(--sz-touch-sm, 32px);
118
+ padding: 0 var(--sp-3);
119
+ font-size: var(--fs-xs);
120
+ }
121
+
122
+ /* sm 变体的命中区扩展:垂直方向补足到 44px(WCAG 2.5.5 Level AAA) */
123
+ .btn.btn--sm::after {
124
+ content: "";
125
+ position: absolute;
126
+ inset-inline-start: 0;
127
+ inset-inline-end: 0;
128
+ top: 50%;
129
+ height: var(--sz-touch, 44px);
130
+ transform: translateY(-50%);
131
+ /* 伪元素不接收事件之外的任何样式;这里只是扩大命中区域 */
132
+ }
133
+ .btn.btn--md { min-height: 44px; padding: 0 var(--sp-5); }
134
+ .btn.btn--lg { min-height: 52px; padding: 0 var(--sp-6); font-size: var(--fs-base); }
135
+
136
+ /* ---------------------------------------------------------------- 变体 */
137
+
138
+ /* primary:一个视图里只能有一个。有两个就等于没有重点。 */
139
+ .btn--primary {
140
+ background: var(--accent);
141
+ color: var(--text-on-accent); /* 白字压 accent = 5.87 AA(已实测) */
142
+ }
143
+ @media (hover: hover) and (pointer: fine) {
144
+ .btn--primary:hover {
145
+ background: var(--accent-hover); /* 白字压 hover = 7.73 AAA */
146
+ }
147
+ }
148
+ /* secondary:次要动作。用边框区分,不是靠"更浅的底"。 */
149
+ .btn--secondary {
150
+ background: var(--surface);
151
+ color: var(--text-primary);
152
+ border-color: var(--border-control); /* 3.56 达标(UI 边界需 >= 3.0) */
153
+ }
154
+ @media (hover: hover) and (pointer: fine) {
155
+ .btn--secondary:hover {
156
+ background: var(--surface-sunken);
157
+ border-color: var(--text-tertiary);
158
+ }
159
+ }
160
+ /* ghost:最低优先级。没有边框也没有底 —— 靠文字色和间距存在。
161
+ 用在"取消""暂不"这类用户不该被引导去点的动作。 */
162
+ .btn--ghost {
163
+ background: transparent;
164
+ color: var(--text-secondary);
165
+ }
166
+ @media (hover: hover) and (pointer: fine) {
167
+ .btn--ghost:hover {
168
+ background: var(--surface-sunken);
169
+ color: var(--text-primary);
170
+ }
171
+ }
172
+ /* danger:破坏性动作。红底,且**永远不要**做成 primary 的大小。 */
173
+ .btn--danger {
174
+ background: var(--danger);
175
+ /* 🔴 写死白字。暗色下 --danger 变亮(#e07a6e),
176
+ 白字压在上面会看不清。改用 --text-on-accent(暗色下是深字)。 */
177
+ color: var(--text-on-accent);
178
+ }
179
+ @media (hover: hover) and (pointer: fine) {
180
+ .btn--danger:hover {
181
+ /* 加深而不是变亮 —— 危险动作 hover 变亮会显得"更安全",是反的。
182
+ 🔴 深色模式下方向相反 —— 深底上"加深"等于消失,
183
+ 所以暗色必须**变亮**(下方 @media dark 里覆盖)。 */
184
+ background: var(--danger-hover);
185
+ }
186
+ }
187
+ /* ---------------------------------------------------------------- 状态 2:active(按下)
188
+ 下沉 1px。幅度必须是 --lift-sm(1px),
189
+ 用 4px、6px 会显得像在玩玩具 —— 这种"果冻感"是廉价感的主要来源。
190
+ ---------------------------------------------------------------- */
191
+ .btn:active {
192
+ transform: translateY(var(--lift-sm));
193
+ }
194
+
195
+ /* ---------------------------------------------------------------- 状态 4:focus-visible
196
+ 🔴 用 :focus-visible 而不是 :focus。
197
+ :focus 会让鼠标点击也冒出框,难看;:focus-visible 只在键盘导航时出现。
198
+
199
+ tokens.css 里已给全局兜底,这里显式写一遍是因为按钮是最该被看见焦点的元素。
200
+ ---------------------------------------------------------------- */
201
+ .btn:focus-visible {
202
+ outline: 2px solid var(--accent);
203
+ outline-offset: 2px;
204
+ }
205
+
206
+ /* ---------------------------------------------------------------- 状态 5:disabled
207
+ 🔴 不要用 opacity。
208
+ opacity: 0.5 会让对比度变成不可控的随机数(取决于底下是什么)。
209
+ 正确做法是明确指定一组"不可点"的色值。
210
+
211
+ 对比度说明:WCAG 1.4.3 对 inactive 组件是豁免的,
212
+ 所以这里的低对比度合规 —— 但文字仍要能读,因为用户需要知道"这里写的是什么"。
213
+ ---------------------------------------------------------------- */
214
+ .btn:disabled,
215
+ .btn[aria-disabled="true"] {
216
+ background: var(--surface-sunken);
217
+ color: var(--text-tertiary); /* 5.16 AA,仍可读 */
218
+ border-color: var(--border-decor);
219
+ cursor: not-allowed;
220
+ }
221
+ @media (hover: hover) and (pointer: fine) {
222
+ .btn:disabled:hover,
223
+ .btn[aria-disabled="true"]:hover {
224
+ background: var(--surface-sunken); /* 关键:hover 不能有变化,否则看着像能点 */
225
+ transform: none;
226
+ }
227
+ }
228
+ .btn:disabled:active,
229
+ .btn[aria-disabled="true"]:active {
230
+ transform: none; /* 按下去也不许动 */
231
+ }
232
+
233
+ /* ---------------------------------------------------------------- 状态 6:loading
234
+ 🔴 核心品味点:loading 时按钮宽度**不能变**。
235
+ 常见做法是替换文字为 spinner,结果按钮缩窄 → 整行布局跳动 → 很难看。
236
+
237
+ 解法:**label 与 spinner 放在同一个 grid 单元**(grid-area: stack)。
238
+ 容器宽度 = max(两者) ⇒ 切换时宽度自动不变,且物理上不可能重叠。
239
+
240
+ ⚠️ 早期版本用的是「文字 visibility:hidden + spinner 绝对定位居中」,
241
+ **错在**:文字没有溢出保护,文案一长就溢出按钮边界,与 spinner 物理重叠
242
+ (实测反馈"按着不动,加载内容就叠上来了")。absolute 还会和 :active
243
+ 的 transform 打架。已改为 grid 叠放,见文件头。
244
+ ---------------------------------------------------------------- */
245
+ .btn[aria-busy="true"] {
246
+ cursor: progress;
247
+ pointer-events: none; /* 防重复提交 —— 这是 loading 态存在的真正理由 */
248
+ }
249
+
250
+ /* 🔴 label 与 spinner 都在 grid-area: stack ⇒ 同一格,物理上不可能重叠。
251
+
252
+ ⚠️ 这里**不能**写 `max-width: 100%`:
253
+ 容器是 inline-grid(宽度由内容决定),label 又是 grid 单元
254
+ (宽度由容器决定)—— `max-width:100%` 构成**循环依赖**,
255
+ 浏览器会把它解析成一个很小的值,于是**文字被压扁、按钮变窄**
256
+ (实测反馈"弹窗里按钮文字太大框太小",就是它造成的)。
257
+
258
+ `min-width: 0` 才是 grid/flex item 该用的:允许收缩,但不给错误的约束。 */
259
+ .btn__label {
260
+ grid-area: stack;
261
+ min-width: 0;
262
+ }
263
+
264
+ /* loading 时用 opacity 而不是 visibility:
265
+ opacity 可过渡(能做出淡入淡出),visibility 瞬间切换会显得"跳"。 */
266
+ .btn[aria-busy="true"] .btn__label { opacity: 0; }
267
+
268
+ .btn__spinner {
269
+ grid-area: stack;
270
+ opacity: 0;
271
+ width: 16px;
272
+ height: 16px;
273
+ border: 2px solid currentColor;
274
+ border-top-color: transparent;
275
+ border-radius: 50%;
276
+ animation: btn-spin 700ms linear infinite;
277
+ }
278
+
279
+ .btn[aria-busy="true"] .btn__spinner { opacity: 1; }
280
+
281
+ @keyframes btn-spin {
282
+ to { transform: rotate(360deg); }
283
+ }
284
+
285
+ /* 🔴 spinner 必须豁免 prefers-reduced-motion
286
+ tokens.css 的全局兜底会把动画压到 0.01ms —— 对位移类动效是对的,
287
+ 但 spinner 停掉会让用户以为页面卡死。
288
+
289
+ WCAG 的立场:reduced-motion 针对的是**大幅位移**(前庭功能障碍),
290
+ spinner 是小幅原地旋转,不属于此类,应当保留。
291
+ 这是"照抄规范"和"理解规范"的区别。 */
292
+ @media (prefers-reduced-motion: reduce) {
293
+ .btn__spinner {
294
+ animation-duration: 1.2s !important; /* 放慢但保留,不停 */
295
+ }
296
+ }
297
+
298
+ /* ---------------------------------------------------------------- 状态 7:success
299
+ 短暂反馈(约 1.2s),需要 JS 加类再移除。
300
+ 只在"动作不可逆且需要确认"时用(提交、支付)。
301
+ 普通操作加 success 是噪音。
302
+ ---------------------------------------------------------------- */
303
+
304
+ /* 🔴 与 combobox 同一处半自动改写事故,已修。
305
+ 经过见 `docs/INVARIANT.md` I-8,不在这里重复第二份。 */
306
+ .btn[data-state="success"] {
307
+ background: var(--success);
308
+ /* 🔴 暗色下底色变亮,写死的白字会看不清。 */
309
+ color: var(--text-on-accent);
310
+ }
311
+ .btn[data-state="success"] .btn__label { opacity: 1; }
312
+ .btn[data-state="success"] .btn__spinner { opacity: 0; }
313
+
314
+ /* ---------------------------------------------------------------- 图标按钮
315
+ 正方形。必须带 aria-label,因为里面没有文字。
316
+ ---------------------------------------------------------------- */
317
+ .btn--icon {
318
+ width: 44px;
319
+ padding: 0;
320
+ }
321
+ .btn--icon.btn--sm { width: 32px; }
322
+ .btn--icon.btn--lg { width: 52px; }
323
+
324
+ /* ---------------------------------------------------------------- 块级
325
+ 移动端常用。注意:block 按钮在桌面端慎用 ——
326
+ 全宽按钮会让页面的视觉动线变成一条竖线。
327
+ ---------------------------------------------------------------- */
328
+ .btn--block {
329
+ display: flex;
330
+ width: 100%;
331
+ }
332
+
333
+ /* ============================================================================
334
+ 废弃 / 反模式 —— 这些写法在本库内一律退回
335
+ =========================================================================
336
+ 1. filter: brightness(1.08) 当 hover
337
+ 实测来源:某项目 的 .send-btn 就是这么写的。
338
+ 问题:hover 色变成不可控的计算结果,无法验证对比度,
339
+ 且在不同底色上表现不一致。改用明确的 --accent-hover。
340
+
341
+ 2. transition: all
342
+ 会把 box-shadow、width 等一起动画化,性能和观感都失控。
343
+ 逐个属性写,像上面基础段那样。
344
+
345
+ 3. :disabled 用 opacity
346
+ 对比度变成随机数。见上。
347
+
348
+ 4. loading 时替换文字导致宽度变化
349
+ 布局跳动。见上。
350
+
351
+ 5. hover 用 transform: scale(1.05)
352
+ 按钮"长大"是廉价感来源之一。要反馈就用 active 的下沉 1px。
353
+ ========================================================================= */
@@ -0,0 +1,200 @@
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>button · 七态矩阵</title>
8
+ <!-- 直接引库内文件,不复制副本 —— token 一改这里同步生效。
9
+ 要单文件部署到 某项目 时,把这两个 CSS 的内容粘进 <style>
10
+ 即可。 --><link rel="stylesheet" href="../../01-tokens/tokens.css"><link rel="stylesheet" href="../../01-tokens/typography.css"><link rel="stylesheet" href="../../01-tokens/page.css"><link rel="stylesheet" href="button.css"><style>body { margin: 0; padding: var(--sp-7) var(--sp-5); background: var(--paper);
11
+ color: var(--text-primary); font: var(--fs-base)/var(--lh-base) system-ui, sans-serif; }
12
+ .grid { display: grid; gap: var(--sp-3); grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }
13
+ .cell { background: var(--surface); border: 1px solid var(--border-decor);
14
+ border-radius: var(--r-md); padding: var(--sp-5); }
15
+ .cell h3 { margin: 0 0 var(--sp-1); font-size: var(--fs-sm); font-weight: 500; }
16
+ .cell p { margin: 0 0 var(--sp-4); font-size: var(--fs-xs);
17
+ color: var(--text-tertiary); line-height: 1.55; }
18
+ .row { display: flex; gap: var(--sp-3); flex-wrap: wrap; align-items: center; }
19
+ .cmp { display: grid; gap: var(--sp-4); grid-template-columns: 1.55fr 1fr; }
20
+ @media (max-width: 640px) {.cmp { grid-template-columns: 1fr; }
21
+
22
+ }
23
+ .cmp>div { padding: var(--sp-4); border-radius: var(--r-md); border: 1px solid var(--border-decor); }
24
+ .bad { background: var(--surface-sunken); }
25
+ .good { background: var(--surface-sunken); }
26
+ .cmp h3 { margin: 0 0 var(--sp-3); font-size: var(--fs-sm); font-weight: 500; }
27
+ .bad h3 { color: var(--danger); }
28
+ .good h3 { color: var(--success); }
29
+ /* 反例演示用 */
30
+ .btn--naive { height: 44px; padding: 0 var(--sp-5); border: none; border-radius: var(--r-sm);
31
+ background: var(--accent); color: #fff; cursor: pointer; font-size: var(--fs-sm);
32
+ transition: all 200ms; }
33
+ .btn--naive:hover { filter: brightness(1.08); transform: scale(1.05); }
34
+ .btn--naive:disabled { opacity: 0.5; }
35
+ </style>
36
+ </head>
37
+ <body class="page page">
38
+ <main class="wrap">
39
+
40
+ <h1>button · 七态矩阵</h1>
41
+ <p class="lede">
42
+ 按钮取七态,不是九态。另外两态(empty / error)属于容器,按钮没有"空"的概念。
43
+ </p>
44
+
45
+ <div class="note">
46
+ 实测动机:某项目 的按钮只有 default + hover 两态,<code>:disabled</code> 0 处。
47
+ 用户不会抱怨"你缺 disabled 态",只会觉得"这个按钮点了没反应,是不是坏了"。
48
+ </div>
49
+
50
+ <h2 class="rule-bottom">七态</h2>
51
+ <div class="grid">
52
+
53
+ <div class="cell">
54
+ <h3>1. default</h3>
55
+ <p>静止。accent 底 + 白字,实测 5.87 AA。</p>
56
+ <button class="btn btn--md btn--primary">提交</button>
57
+ </div>
58
+
59
+ <div class="cell">
60
+ <h3>2. hover</h3>
61
+ <p>明确的 <code>--accent-hover</code> 色值(白字 7.73 AAA),不是 brightness 计算。</p>
62
+ <button class="btn btn--md btn--primary">鼠标移上来</button>
63
+ </div>
64
+
65
+ <div class="cell">
66
+ <h3>3. active</h3>
67
+ <p>下沉 1px。<strong>只用 1px</strong>——用 4px/6px 会有果冻感,那是廉价感的主要来源。</p>
68
+ <button class="btn btn--md btn--primary">按住不放</button>
69
+ </div>
70
+
71
+ <div class="cell">
72
+ <h3>4. focus-visible</h3>
73
+ <p>键盘 Tab 到这里才出现。<code>:focus</code> 会让鼠标点击也冒框,所以用 <code>-visible</code>。</p>
74
+ <button class="btn btn--md btn--primary">Tab 键切过来</button>
75
+ </div>
76
+
77
+ <div class="cell">
78
+ <h3>5. disabled</h3>
79
+ <p>明确色值,<strong>不用 opacity</strong>——opacity 让对比度变成不可控的随机数。hover 无任何变化。</p>
80
+ <button class="btn btn--md btn--primary" disabled>已禁用</button>
81
+ </div>
82
+
83
+ <div class="cell">
84
+ <h3>6. loading</h3>
85
+ <p>宽度不变(label 与 spinner 同一 grid 单元,容器宽 = max(两者))。<code>pointer-events:none</code> 防重复提交。</p>
86
+ <button class="btn btn--md btn--primary" aria-busy="true">
87
+ <span class="btn__label">提交</span><span class="btn__spinner"></span>
88
+ </button>
89
+ </div>
90
+
91
+ <div class="cell">
92
+ <h3>7. success</h3>
93
+ <p>仅用于不可逆动作(提交、支付)。普通操作加这个是噪音。</p>
94
+ <button class="btn btn--md btn--primary" data-state="success">
95
+ <span class="btn__label">已提交</span><span class="btn__spinner"></span>
96
+ </button>
97
+ </div>
98
+
99
+ <div class="cell">
100
+ <h3>交互演示</h3>
101
+ <p>点一下:default → loading(1.4s)→ success(1.2s)→ 复位。注意宽度全程不变。</p>
102
+ <button class="btn btn--md btn--primary" id="demo">
103
+ <span class="btn__label">点我走一遍</span><span class="btn__spinner"></span>
104
+ </button>
105
+ </div>
106
+
107
+ </div>
108
+
109
+ <h2 class="rule-bottom">变体</h2>
110
+ <div class="row">
111
+ <button class="btn btn--md btn--primary">primary</button>
112
+ <button class="btn btn--md btn--secondary">secondary</button>
113
+ <button class="btn btn--md btn--ghost">ghost</button>
114
+ <button class="btn btn--md btn--danger">danger</button>
115
+ </div>
116
+ <p style="font-size:var(--fs-xs); color:var(--text-tertiary); margin-top:var(--sp-3)">
117
+ 一个视图里只能有一个 primary。danger 不应该做得和 primary 一样显眼。
118
+ </p>
119
+
120
+ <h2 class="rule-bottom">尺寸</h2>
121
+ <div class="row">
122
+ <button class="btn btn--sm btn--primary">sm 32</button>
123
+ <button class="btn btn--md btn--primary">md 44</button>
124
+ <button class="btn btn--lg btn--primary">lg 52</button>
125
+ </div>
126
+ <p style="font-size:var(--fs-xs); color:var(--text-tertiary); margin-top:var(--sp-3)">
127
+ md = 44px 是 WCAG 2.5.5 增强级的触摸目标尺寸,来自手指物理尺寸,不是审美。
128
+ sm 只在表格行内等鼠标场景用。
129
+ </p>
130
+
131
+ <h2 class="rule-bottom">对比:常见的错写法 vs 本库写法</h2>
132
+ <div class="cmp">
133
+ <div class="box bad">
134
+ <h3>常见写法(某项目 现状)</h3>
135
+ <div class="row">
136
+ <button class="btn--naive">hover 看我</button>
137
+ <button class="btn--naive" disabled>disabled 看我</button>
138
+ </div>
139
+ <p style="font-size:var(--fs-xs); margin:var(--sp-3) 0 0; color:var(--text-secondary)">
140
+ <code>filter: brightness(1.08)</code> —— hover 色是算出来的,无法验证对比度,不同底色表现不一致。<br>
141
+ <code>transform: scale(1.05)</code> —— 按钮"长大",廉价感。<br>
142
+ <code>opacity: 0.5</code> —— 对比度变成随机数。<br>
143
+ <code>transition: all</code> —— 所有属性一起动画,性能和观感失控。
144
+ </p>
145
+ </div>
146
+ <div class="good">
147
+ <h3>本库写法</h3>
148
+ <div class="row">
149
+ <button class="btn btn--md btn--primary">hover 看我</button>
150
+ <button class="btn btn--md btn--primary" disabled>disabled 看我</button>
151
+ </div>
152
+ <p style="font-size:var(--fs-xs); margin:var(--sp-3) 0 0; color:var(--text-secondary)">
153
+ 明确的 <code>--accent-hover</code> 色值,实测 7.73 AAA。<br>
154
+ active 只下沉 1px。<br>
155
+ disabled 用明确的低对比色组,仍可读。<br>
156
+ 逐属性写 transition,只动颜色和 transform。
157
+ </p>
158
+ </div>
159
+ </div>
160
+
161
+ <h2 class="rule-bottom">一个容易被跳过的判断:disabled 还是 aria-disabled</h2>
162
+ <div class="grid">
163
+ <div class="cell">
164
+ <h3>disabled 属性</h3>
165
+ <p>从 Tab 顺序移除,键盘用户永远找不到它。适合<strong>永久</strong>不可用(权限不足)。</p>
166
+ <button class="btn btn--md btn--secondary" disabled>功能未开放</button>
167
+ </div>
168
+ <div class="cell">
169
+ <h3>aria-disabled="true"</h3>
170
+ <p>保留可聚焦,屏幕阅读器读"不可用"。表单提交按钮<strong>应该用这个</strong>——用户 Tab 到它才知道"还差点什么"。</p>
171
+ <button class="btn btn--md btn--secondary" aria-disabled="true">请先填写金额</button>
172
+ </div>
173
+ </div>
174
+
175
+ <h2 class="rule-bottom">spinner 与 prefers-reduced-motion</h2>
176
+ <div class="note">
177
+ 系统开启"减少动效"后,全局兜底会把动画压到 0.01ms——spinner 就停了,用户会以为页面卡死。<br><br>
178
+ WCAG 的 reduced-motion 针对的是<strong>大幅位移</strong>(前庭功能障碍),
179
+ 小幅原地旋转不属于此类。本库的做法是<strong>放慢到 1.2s 但保留,不停</strong>。<br><br>
180
+ 这是"照抄规范"和"理解规范"的区别。
181
+ </div>
182
+
183
+ <script>
184
+ var b = document.getElementById('demo');
185
+ b.addEventListener('click', function () {
186
+ if (b.getAttribute('aria-busy') === 'true') return;
187
+ b.setAttribute('aria-busy', 'true');
188
+ setTimeout(function () {
189
+ b.removeAttribute('aria-busy');
190
+ b.setAttribute('data-state', 'success');
191
+ b.querySelector('.btn__label').textContent = '已提交';
192
+ setTimeout(function () {
193
+ b.removeAttribute('data-state');
194
+ b.querySelector('.btn__label').textContent = '点我走一遍';
195
+ }, 1200);
196
+ }, 1400);
197
+ });
198
+ </script></main>
199
+ </body>
200
+ </html>