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,145 @@
1
+ # card
2
+
3
+ | 字段 | 内容 |
4
+ |---|---|
5
+ | **来源** | 原创。2px 抬起一条来自对 `animal-island-ui` 的观察(那套设计里最认同的一条) |
6
+ | **实测** | `states.py` 零缺失(card 形态:focus HIGH / hover LOW) |
7
+ | **适用** | 精简档 / B / C,纯 CSS |
8
+ | **依赖** | `01-tokens/tokens.css` + `01-tokens/typography.css`(`.note` 提示框) + `01-tokens/page.css`(页面骨架:`.wrap` / `.lede` / `.note`) |
9
+
10
+ ---
11
+
12
+ ## 用法
13
+
14
+ ```html
15
+ <div class="card">
16
+ <div class="card__body">
17
+ <h3>标题</h3>
18
+ <p class="small">说明文字</p>
19
+ </div>
20
+ </div>
21
+ ```
22
+
23
+ **`.card__body` 不是可选的。** `.card` 自身 `padding: 0`,内边距全在
24
+ `.card__body` 上(`padding: var(--sp-5)`)。直接往 `.card` 里放内容,文字会**贴边**。
25
+
26
+ **`.card` 是 `display: flex; flex-direction: column`**,子元素默认被拉伸到整宽。
27
+ 所以**行内小元素会被拉成一条整宽的色带**(实测:一个本该 54px 宽的状态标签
28
+ 被拉到 355px)。这不是 bug,是 flex 的默认 `align-items: stretch`。
29
+
30
+ 两种解法:
31
+
32
+ ```html
33
+ <!-- ① 包一层(推荐,语义更清楚) -->
34
+ <div class="card"><div class="card__body">
35
+ <h3>标题</h3>
36
+ <div><span>状态</span></div>
37
+ </div></div>
38
+
39
+ <!-- ② 直接给那个元素 align-self: flex-start -->
40
+ <div class="card"><div class="card__body">
41
+ <h3>标题</h3>
42
+ <span style="align-self:flex-start">状态</span>
43
+ </div></div>
44
+ ```
45
+
46
+ > 若要在卡片里放**状态标签**,请同时引入 `02-primitives/badge/badge.css` ——
47
+ > 它不在 card 的依赖里,本目录的 `demo.html` 也没有演示它。
48
+
49
+ | 变体 | 用途 |
50
+ |---|---|
51
+ | `.card--interactive` | 可点击(hover 抬起 2px + 阴影) |
52
+ | `.card--linked` | 整卡可点,语义由 `<a>` 承担 |
53
+ | `.card--flat` | 无边框,靠背景区分 |
54
+ | `.card--elevated` | 常驻阴影 |
55
+ | `.card--compact` | 紧凑内边距 |
56
+ | `.card--row` | 横向排列(图左文右) |
57
+ | `.card--selected` | 选中态 |
58
+
59
+ 依赖:`01-tokens/tokens.css` + `01-tokens/typography.css`(正文)+
60
+ `01-tokens/page.css`(`.wrap` / `.small` 等页面骨架)。
61
+
62
+ ---
63
+
64
+ ## 唯一的核心判据:hover 不能有布局跳动
65
+
66
+ hover 只改 **`transform`** 和 **`box-shadow`**,这两个属性不触发回流。
67
+ 改 `border-width` / `margin` / `padding` / `height` 任何一项都会引起重排,相邻卡片跟着抖。
68
+
69
+ 那个抖动很轻——1px 左右,轻到说不清哪里不对——**但整页会显得廉价**。
70
+ `demo.html` 把它放大到 4px,鼠标移上去一次就能记住。
71
+
72
+ **要"加粗"边框怎么办**:用 `box-shadow: inset 0 0 0 2px`,不要改 `border-width`。
73
+ `--selected` 变体就是这么做的。
74
+
75
+ ---
76
+
77
+ ## 变体
78
+
79
+ | 变体 | 用途 | 注意 |
80
+ |---|---|---|
81
+ | `.card` | 默认,1px 边框 | 一排卡片时边框会显得重 |
82
+ | `.card--flat` | 无边框,靠明度差分层 | **列表首选**,比全都有边框干净 |
83
+ | `.card--elevated` | 默认带阴影 | 只给浮层/弹窗;列表全用它阴影会打架 |
84
+ | `.card--interactive` | 可点,hover 抬 2px | 需要整卡可点时配 `--linked` |
85
+ | `.card--linked` | 整卡可点 | 内部必须有真实 `<a>`/`<button>` |
86
+ | `.card--selected` | 选中态 | inset shadow,不是改 border |
87
+ | `.card--compact` | 紧凑 | 两档够了,第三档说明密度没规划 | —— demo 未演示,是留给复用者的钩子
88
+ | `.card--row` | 横向 | 560px 以下自动回退纵向 |
89
+
90
+ ---
91
+
92
+ ## 🔴 整卡可点:正确做法 vs 错误做法
93
+
94
+ **错误**:给 `<div>` 加 `onclick`。
95
+ 键盘 Tab 不到、屏幕阅读器不认、不能中键新开、不能复制链接。
96
+
97
+ **正确**:卡片内放一个真实的 `<a>`,用它的 `::after` 铺满整卡。
98
+
99
+ ```html
100
+ <div class="card card--linked card--interactive">
101
+ <div class="card__body">
102
+ <h3 class="card__title"><a class="card__link" href="/x">标题</a></h3>
103
+ <p class="card__desc">摘要</p>
104
+ </div>
105
+ </div>
106
+ ```
107
+
108
+ 焦点用 `:focus-within`——内部链接获焦时**整张卡**显示 ring,而不是只有标题那一小块。
109
+ 链接本身的 `:focus-visible` 要关掉,否则会出现两个框。
110
+
111
+ 卡片里如果有其他可点元素(按钮、标签),要 `z-index: 1` 盖在热区之上。
112
+
113
+ ---
114
+
115
+ ## 抬起 2px,不是 8px
116
+
117
+ 卡片是"被拿起",不是"被弹起"。8px 以上会有游戏感——
118
+ **长文和数据类界面不能要那种感觉**。
119
+
120
+ 这条来自对 `animal-island-ui` 的观察:卡片 20px 圆角、无投影、hover 只抬 2px。
121
+ 那套库整体画风跟严肃内容不搭,但这一条是对的。
122
+
123
+ ---
124
+
125
+ ## 反例 —— 什么时候不该用
126
+
127
+ - ❌ **一排卡片全用 `--elevated`** —— 阴影互相打架,整页发灰。列表用 `--flat`
128
+ - ❌ **hover 抬起 8px 以上** —— 游戏感
129
+ - ❌ **整卡套 `div onclick`** —— 键盘不可达,见上
130
+ - ❌ **给链接本身再画焦点环** —— 会变成两个框
131
+ - ❌ **用 JS 截断描述文字** —— 会留下半个词或半个字,用 `-webkit-line-clamp`
132
+ - ❌ **手动覆盖 `--row` 在窄屏的回退** —— 那个回退是必要的
133
+
134
+ ---
135
+
136
+ ## 门禁
137
+
138
+ ```bash
139
+ python 05-audit/states.py 02-primitives/card/card.css
140
+ ```
141
+
142
+ card 形态查两项:`focus`(HIGH,整卡可点时键盘不可达)、`hover`(LOW)。
143
+ 不查 `disabled` / `active`——卡片没有禁用态,也不响应按压。
144
+
145
+ 跑在 某项目 上立刻抓出 `.card` / `.card-grid` / `.side-card` 三个类全缺 focus。
@@ -0,0 +1,246 @@
1
+ /* ============================================================================
2
+ frontend-lib / 02-primitives / card / card.css
3
+ 卡片
4
+
5
+ 依赖:01-tokens/tokens.css
6
+ 版本:A ✅ / B ✅ / C ✅
7
+
8
+ 🔴 本组件的核心判据:hover 时不能有布局跳动。
9
+ 达成方式:hover 只改 transform 和 box-shadow,
10
+ **绝不改 border-width / margin / padding / height**。
11
+ 改任何影响盒模型的属性都会引起回流,相邻元素会跟着抖一下 ——
12
+ 这个抖动很轻,轻到说不清哪里不对,但整页会显得廉价。
13
+
14
+ card 是容器,承载的内容态(empty/loading/error/success)在 03-patterns。
15
+ ========================================================================= */
16
+
17
+ .card {
18
+ position: relative;
19
+ display: flex;
20
+ flex-direction: column;
21
+
22
+ background: var(--surface);
23
+ /* 🔴 边框恒为 1px。hover 时想"加粗"边框?用 box-shadow 模拟,不要改 border-width。 */
24
+ border: 1px solid var(--border-decor);
25
+ border-radius: var(--r-md);
26
+
27
+ /* 只需动画 transform 和 box-shadow —— 这两个不触发回流 */
28
+ transition: transform var(--dur-base) var(--ease-out),
29
+ box-shadow var(--dur-base) var(--ease-out);
30
+ }
31
+
32
+ /* ---------------------------------------------------------------- 结构 */
33
+ .card__media {
34
+ /* 用 aspect-ratio 而不是 padding-top hack —— 后者要多一层包装 div */
35
+ aspect-ratio: 16 / 9;
36
+ overflow: hidden;
37
+ border-radius: calc(var(--r-md) - 1px) calc(var(--r-md) - 1px) 0 0;
38
+ background: var(--surface-sunken);
39
+ }
40
+ .card__media img {
41
+ width: 100%;
42
+ height: 100%;
43
+ object-fit: cover;
44
+ display: block;
45
+ }
46
+ /* 没有 media 时,body 要补上顶部圆角 */
47
+ .card__media + .card__body { border-radius: 0; }
48
+
49
+ .card__body {
50
+ padding: var(--sp-5);
51
+ display: flex;
52
+ flex-direction: column;
53
+ gap: var(--sp-2);
54
+ }
55
+
56
+ .card__title {
57
+ margin: 0;
58
+ font-size: var(--fs-md);
59
+ font-weight: 500;
60
+ line-height: var(--lh-tight);
61
+ color: var(--text-primary);
62
+ }
63
+
64
+ /* 副标题/摘要。行数限制用 -webkit-line-clamp,
65
+ 别用 JS 截字符串 —— 截出来会留下半个词或半个字。 */
66
+ .card__desc {
67
+ margin: 0;
68
+ font-size: var(--fs-sm);
69
+ color: var(--text-secondary);
70
+ line-height: var(--lh-base);
71
+ display: -webkit-box;
72
+ -webkit-line-clamp: 3;
73
+ -webkit-box-orient: vertical;
74
+ overflow: hidden;
75
+ }
76
+
77
+ .card__footer {
78
+ padding: var(--sp-3) var(--sp-5);
79
+ border-top: 1px solid var(--border-decor);
80
+ display: flex;
81
+ align-items: center;
82
+ justify-content: space-between;
83
+ gap: var(--sp-3);
84
+ font-size: var(--fs-xs);
85
+ color: var(--text-tertiary);
86
+ }
87
+
88
+ /* ---------------------------------------------------------------- 变体 */
89
+
90
+ /* 无边框:靠背景色和纸底的明度差分层,界面更轻。
91
+ 一排卡片时用它,比全都有边框干净。 */
92
+ .card--flat {
93
+ border-color: transparent;
94
+ background: var(--surface);
95
+ }
96
+
97
+ /* 浮起:默认就有阴影。用在浮层、弹窗类场景。
98
+ 🔴 常规列表里不要用 —— 满屏阴影会互相打架。 */
99
+ .card--elevated {
100
+ border-color: transparent;
101
+ box-shadow: var(--shadow-1);
102
+ }
103
+
104
+ /* ---------------------------------------------------------------- 可交互卡片
105
+ 🔴 hover 抬起的正确写法:
106
+ transform 抬 2px(--lift-md),阴影从 1 级升到 2 级。
107
+ 两个属性都不触发回流,所以旁边的卡片不会抖。
108
+
109
+ 为什么是 2px 不是 8px:卡片是"被拿起"不是"被弹起"。
110
+ 8px 会有游戏感,长文/数据类界面不能要那种感觉。
111
+ ---------------------------------------------------------------- */
112
+ .card--interactive {
113
+ cursor: pointer;
114
+ }
115
+ @media (hover: hover) and (pointer: fine) {
116
+ .card--interactive:hover {
117
+ transform: translateY(calc(-1 * var(--lift-md)));
118
+ box-shadow: var(--shadow-2);
119
+ }
120
+ }
121
+ /* 按下时落回原位 —— 和 button 的 active 是同一套语言 */
122
+ .card--interactive:active {
123
+ transform: translateY(0);
124
+ box-shadow: var(--shadow-1);
125
+ }
126
+
127
+ /* ---------------------------------------------------------------- 整卡可点
128
+ 🔴 正确做法:卡片里放一个真实的 <a> 或 <button>,
129
+ 用它的 ::after 铺满整张卡。
130
+
131
+ 错误做法:给 <div> 加 onclick。
132
+ 那样键盘 Tab 不到、屏幕阅读器不认、不能中键新开、不能复制链接。
133
+
134
+ 焦点样式用 :focus-within —— 内部的链接获得焦点时,整卡显示 ring。
135
+ ---------------------------------------------------------------- */
136
+ .card--linked { position: relative; }
137
+
138
+ .card__link {
139
+ /* 让链接文字本身还是普通样式,只把它当作"热区提供者" */
140
+ color: inherit;
141
+ text-decoration: none;
142
+ }
143
+ .card__link::after {
144
+ content: "";
145
+ position: absolute;
146
+ inset: 0; /* 铺满整张卡 */
147
+ border-radius: var(--r-md);
148
+ z-index: var(--z-base);
149
+ }
150
+ /* 卡片里如果有其他**可点元素**(按钮、链接、标签),要盖在热区之上。
151
+
152
+ 🔴 **修正一个真 bug**(契约实测抓到:卡片内采样 5 点,0 点命中链接):
153
+ 原来写的是 `.card__body > *:not(.card__link)`
154
+ ⇒ **把标题、段落、说明文字全都抬到热区之上**
155
+ ⇒ 结果**点卡片正文不跳转**,"整卡可点"名存实亡。
156
+ (实测:卡片中心点命中的是 `P.card__desc`,不是那条链接。)
157
+
158
+ ⚠️ 这类 bug 在视觉上**完全看不出来** —— 卡片照常显示、
159
+ 标题里那条链接也照常能点,只有"点卡片空白/正文"才发现失效。
160
+
161
+ ⇒ 现在只抬**真正可交互**的元素,纯内容不抬。
162
+ ⚠️ 顺带修掉硬编码的 `z-index: 1` → 走 `--z-raised` 令牌。 */
163
+ .card__body :is(button, a, input, select, textarea, [role="button"],
164
+ [role="checkbox"], [role="switch"], [tabindex]):not(.card__link) {
165
+ position: relative;
166
+ z-index: var(--z-raised);
167
+ }
168
+
169
+ .card--linked:focus-within {
170
+ outline: 2px solid var(--accent);
171
+ outline-offset: 2px;
172
+ }
173
+ /* 别给链接本身再画一次 ring,会变成两个框 */
174
+ .card__link:focus-visible { outline: none; }
175
+
176
+ /* ---------------------------------------------------------------- 选中态
177
+ 多选场景(如筛选后的项目列表)。
178
+ 🔴 用 inset box-shadow 加"内描边",而不是 border-width 变化 —— 同样的理由:不跳动。
179
+ ---------------------------------------------------------------- */
180
+ .card--selected {
181
+ box-shadow: inset 0 0 0 2px var(--accent);
182
+ }
183
+
184
+ /* ---------------------------------------------------------------- 尺寸
185
+ 两档够了。出现第三档说明信息密度没规划好。
186
+ ---------------------------------------------------------------- */
187
+ .card--compact .card__body { padding: var(--sp-4); gap: var(--sp-1); }
188
+ .card--compact .card__title { font-size: var(--fs-base); }
189
+
190
+ /* ---------------------------------------------------------------- 横向卡片
191
+ 窄屏回退为纵向。
192
+ ---------------------------------------------------------------- */
193
+ .card--row {
194
+ flex-direction: row;
195
+ align-items: stretch;
196
+ }
197
+ .card--row .card__media {
198
+ flex: none;
199
+ width: 160px;
200
+ aspect-ratio: auto;
201
+ border-radius: calc(var(--r-md) - 1px) 0 0 calc(var(--r-md) - 1px);
202
+ }
203
+ @media (max-width: 560px) {
204
+ .card--row { flex-direction: column; }
205
+ .card--row .card__media {
206
+ width: 100%;
207
+ aspect-ratio: 16 / 9;
208
+ border-radius: calc(var(--r-md) - 1px) calc(var(--r-md) - 1px) 0 0;
209
+ }
210
+ }
211
+
212
+ /* ---------------------------------------------------------------- 无障碍兜底
213
+ card 的 hover 位移属于"小幅位移",reduced-motion 下应当去掉抬起,
214
+ 只保留阴影变化 —— 阴影不属于前庭敏感类动效。
215
+ ---------------------------------------------------------------- */
216
+ @media (prefers-reduced-motion: reduce) {
217
+ @media (hover: hover) and (pointer: fine) {
218
+ .card--interactive:hover,
219
+ .card--interactive:active {
220
+ transform: none;
221
+ }
222
+ }
223
+ }
224
+
225
+ /* ============================================================================
226
+ 反模式 —— 库内一律退回
227
+ =========================================================================
228
+ 1. hover 时改 border-width(如 1px -> 2px)
229
+ 内容整体位移 1px,相邻卡片跟着抖。这是卡片最经典的 bug。
230
+ 要"加粗"边框用 box-shadow: inset 0 0 0 2px。
231
+
232
+ 2. hover 时改 margin / padding / height
233
+ 同上,且会引起整页重排。
234
+
235
+ 3. 整卡套 <div onclick>
236
+ 键盘不可达、屏幕阅读器不认、不能新标签打开。用 .card__link::after。
237
+
238
+ 4. 一排卡片全用 --elevated
239
+ 阴影会互相打架,整页发灰。列表用 --flat,浮层才用 --elevated。
240
+
241
+ 5. hover 抬起 8px 以上
242
+ 游戏感。卡片是"被拿起"不是"被弹起"。
243
+
244
+ 6. 用 JS 截断描述文字
245
+ 会留下半个词或半个字。用 -webkit-line-clamp。
246
+ ========================================================================= */
@@ -0,0 +1,170 @@
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>card · hover 无跳动</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="card.css">
12
+ <style>
13
+ p.small { font-size: var(--fs-xs); color: var(--text-tertiary); line-height: 1.6; }
14
+ .grid3 { display: grid; gap: var(--sp-4); grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
15
+ .cmp { display: grid; gap: var(--sp-5); grid-template-columns: 1.55fr 1fr; }
16
+ @media (max-width: 700px) {.cmp { grid-template-columns: 1fr; }
17
+
18
+ }
19
+ .cmp>div { padding: var(--sp-5); border-radius: var(--r-md); border: 1px solid var(--border-decor); }
20
+ .bad { background: var(--surface-sunken); }
21
+ .good { background: var(--surface-sunken); }
22
+ .cmp h3 { margin: 0 0 var(--sp-2); font-size: var(--fs-sm); font-weight: 500; }
23
+ .bad h3 { color: var(--danger); }
24
+ .good h3 { color: var(--success); }
25
+ .cmp .stack { display: flex; flex-direction: column; gap: var(--sp-3); }
26
+ /* ---- 反例卡片:hover 时改 border-width,引起回流 ---- */
27
+ .card-bad { position: relative; background: var(--surface);
28
+ border: 1px solid var(--border-decor); border-radius: var(--r-md);
29
+ padding: var(--sp-4); font-size: var(--fs-sm); cursor: pointer;
30
+ transition: border-color 180ms, box-shadow 180ms; }
31
+ .card-bad:hover { border-width: 4px; border-color: var(--accent); box-shadow: var(--shadow-2); }
32
+ .card-bad .ruler { height: 12px; background: repeating-linear-gradient(90deg,
33
+ var(--text-tertiary) 0 1px, transparent 1px 8px); opacity: .5; margin-bottom: var(--sp-2); }
34
+ /* ---- 正确卡片:用库里的 .card--interactive ---- */
35
+ .card-ok .ruler { height: 12px; background: repeating-linear-gradient(90deg,
36
+ var(--text-tertiary) 0 1px, transparent 1px 8px); opacity: .5; margin-bottom: var(--sp-2); }
37
+ </style>
38
+ </head>
39
+ <body class="page page">
40
+ <main class="wrap">
41
+
42
+ <h1>card · hover 无布局跳动</h1>
43
+ <p class="lede">
44
+ 卡片的核心判据:hover 时只改 <code>transform</code> 和 <code>box-shadow</code>,
45
+ <strong>绝不改 border-width / margin / padding / height</strong>。
46
+ </p>
47
+
48
+ <h2 class="rule-bottom">把 1px 的跳动放大给你看</h2>
49
+ <div class="note">
50
+ 真实项目里这个 bug 通常是 <code>border: 1px → 2px</code>,只跳 1px ——
51
+ 轻到说不清哪里不对,但整页会显得廉价。<br>
52
+ 下面<strong>故意放大到 4px</strong>,好让你一次看清楚。请分别把鼠标移到两张卡上。
53
+ </div>
54
+
55
+ <div class="cmp">
56
+ <div class="box bad">
57
+ <h3>反例:hover 改 border-width</h3>
58
+ <p class="small" style="margin:0 0 var(--sp-3)">
59
+ 移上来 —— 里面那把尺子和文字整体往下挪了,卡片也变高了。
60
+ 一排卡片时,邻居会跟着抖。
61
+ </p>
62
+ <div class="stack">
63
+ <div class="card-bad">
64
+ <div class="ruler"></div>
65
+ <strong>2026-09-24</strong><br>动物森友会 UI 库实拍
66
+ </div>
67
+ <div class="card-bad">
68
+ <div class="ruler"></div>
69
+ <strong>2026-09-27</strong><br>MICRO 微交互组件实拍
70
+ </div>
71
+ </div>
72
+ </div>
73
+
74
+ <div class="good">
75
+ <h3>本库:transform + box-shadow</h3>
76
+ <p class="small" style="margin:0 0 var(--sp-3)">
77
+ 移上来 —— 卡片整体抬起 2px,<strong>里面的内容一个像素都不动</strong>,
78
+ 邻居也不会被推挤。
79
+ </p>
80
+ <div class="stack">
81
+ <div class="card card--interactive card-ok">
82
+ <div class="card__body">
83
+ <div class="ruler"></div>
84
+ <span class="card__title">动物森友会 UI 库实拍</span>
85
+ <span class="card__desc" style="-webkit-line-clamp:1">2026-09-24</span>
86
+ </div>
87
+ </div>
88
+ <div class="card card--interactive card-ok">
89
+ <div class="card__body">
90
+ <div class="ruler"></div>
91
+ <span class="card__title">MICRO 微交互组件实拍</span>
92
+ <span class="card__desc" style="-webkit-line-clamp:1">2026-09-27</span>
93
+ </div>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ </div>
98
+
99
+ <h2 class="rule-bottom">抬起 2px,不是 8px</h2>
100
+ <p class="small">
101
+ 卡片是"被拿起",不是"被弹起"。8px 以上会有游戏感 ——
102
+ 长文和数据类界面不能要那种感觉。这也是森友会那套设计里我最认同的一条:
103
+ 卡片 20px 圆角、无投影、hover 只抬 2px。
104
+ </p>
105
+
106
+ <h2 class="rule-bottom">变体</h2>
107
+ <div class="grid3">
108
+ <div class="card card--flat">
109
+ <div class="card__body">
110
+ <h3 class="card__title">flat</h3>
111
+ <p class="card__desc">无边框,靠背景色和纸底的明度差分层。一排卡片时用它最干净。</p>
112
+ </div>
113
+ </div>
114
+ <div class="card card--elevated">
115
+ <div class="card__body">
116
+ <h3 class="card__title">elevated</h3>
117
+ <p class="card__desc">默认带阴影。只用在浮层、弹窗。列表里全用它,阴影会互相打架。</p>
118
+ </div>
119
+ </div>
120
+ <div class="card card--selected">
121
+ <div class="card__body">
122
+ <h3 class="card__title">selected</h3>
123
+ <p class="card__desc">用 inset box-shadow 做内描边,不是改 border-width —— 同样为了不跳动。</p>
124
+ </div>
125
+ </div>
126
+ </div>
127
+
128
+ <h2 class="rule-bottom">整卡可点:Tab 一下试试</h2>
129
+ <div class="grid3">
130
+ <div class="card card--linked card--interactive">
131
+ <div class="card__body">
132
+ <h3 class="card__title"><a class="card__link" href="#x">点标题,整卡可点</a></h3>
133
+ <p class="card__desc">用键盘 Tab 到这里 —— 整张卡会出现焦点环,而不是只有标题那一小块。</p>
134
+ </div>
135
+ </div>
136
+ <div class="card">
137
+ <div class="card__body">
138
+ <h3 class="card__title">对照:普通 div onclick</h3>
139
+ <p class="card__desc" style="-webkit-line-clamp:2">
140
+ (这张是正确实现的对照说明)给 div 加 onclick:键盘 Tab 不到、
141
+ 屏幕阅读器不认、不能中键新开、不能复制链接。
142
+ </p>
143
+ </div>
144
+ </div>
145
+ </div>
146
+ <p class="small">
147
+ 实现:卡内放一个真实的 <code>&lt;a&gt;</code>,用它的 <code>::after</code> 铺满整卡。
148
+ 焦点样式用 <code>:focus-within</code>,内部链接获焦时整卡显示 ring。
149
+ </p>
150
+
151
+ <h2 class="rule-bottom">横向卡片(窄屏自动回退)</h2>
152
+ <div class="card card--row">
153
+ <div class="card__media"></div>
154
+ <div class="card__body">
155
+ <h3 class="card__title">横向布局</h3>
156
+ <p class="card__desc">560px 以下自动变回纵向。别手动覆盖这个回退。</p>
157
+ </div>
158
+ </div>
159
+
160
+ <h2 class="rule-bottom">门禁抓到的真问题</h2>
161
+ <div class="note">
162
+ 给 <code>states.py</code> 加了 card 形态后,跑 某项目 立刻多报 4 项 HIGH:
163
+ <code>.card</code> / <code>.card-grid</code> / <code>.side-card</code> <strong>全都缺 focus</strong> ——
164
+ 那几张卡鼠标能 hover,但键盘 Tab 不到。<br><br>
165
+ 这就是"按形态分开检查"的价值:统一清单时这些根本不会被发现。
166
+ </div>
167
+
168
+ </main>
169
+ </body>
170
+ </html>