agentcss 0.16.1 → 0.24.1

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 (93) hide show
  1. package/01-tokens/focus-ring-demo.html +1 -1
  2. package/01-tokens/page.css +32 -0
  3. package/01-tokens/responsive-demo.html +1 -1
  4. package/01-tokens/scrollbar-demo.html +1 -1
  5. package/02-primitives/badge/demo.html +1 -1
  6. package/02-primitives/button/demo.html +1 -1
  7. package/02-primitives/card/demo.html +1 -1
  8. package/02-primitives/choice/choice.css +31 -2
  9. package/02-primitives/choice/demo.html +1 -1
  10. package/02-primitives/combobox/demo.html +1 -1
  11. package/02-primitives/date-range/demo.html +1 -1
  12. package/02-primitives/delta/demo.html +1 -1
  13. package/02-primitives/input/demo.html +1 -1
  14. package/02-primitives/input/input.css +14 -0
  15. package/02-primitives/metric/demo.html +1 -1
  16. package/02-primitives/popover/demo.html +1 -1
  17. package/02-primitives/progress/demo.html +1 -1
  18. package/02-primitives/progress/progress.css +56 -0
  19. package/02-primitives/select/demo.html +1 -1
  20. package/02-primitives/separator/demo.html +1 -1
  21. package/02-primitives/skeleton/demo.html +1 -1
  22. package/02-primitives/skeleton/skeleton.css +18 -0
  23. package/02-primitives/switch/demo.html +1 -1
  24. package/02-primitives/switch/switch.css +61 -2
  25. package/02-primitives/timeline/README.md +70 -0
  26. package/02-primitives/timeline/demo.html +161 -0
  27. package/02-primitives/timeline/timeline.css +152 -0
  28. package/02-primitives/upload/README.md +142 -0
  29. package/02-primitives/upload/demo.html +318 -0
  30. package/02-primitives/upload/upload.css +490 -0
  31. package/02-primitives/upload/upload.js +676 -0
  32. package/03-patterns/accordion/demo.html +1 -1
  33. package/03-patterns/content/demo.html +1 -1
  34. package/03-patterns/drawer/demo.html +1 -1
  35. package/03-patterns/drawer/drawer.js +30 -5
  36. package/03-patterns/dropdown/demo.html +1 -1
  37. package/03-patterns/form-validation/demo.html +1 -1
  38. package/03-patterns/list/demo.html +1 -1
  39. package/03-patterns/nav/demo.html +1 -1
  40. package/03-patterns/overlay/demo.html +1 -1
  41. package/03-patterns/pagination/demo.html +1 -1
  42. package/03-patterns/states/demo.html +1 -1
  43. package/03-patterns/tabs/demo.html +1 -1
  44. package/03-patterns/tooltip/demo.html +1 -1
  45. package/03-patterns/tree/demo.html +1 -1
  46. package/04-recipes/app-shell/README.md +272 -0
  47. package/04-recipes/app-shell/app-shell.css +588 -0
  48. package/04-recipes/app-shell/demo.html +326 -0
  49. package/04-recipes/app-shell/shell-nav.js +288 -0
  50. package/04-recipes/table/table.css +22 -0
  51. package/09-assets/bar/demo.html +1 -1
  52. package/09-assets/echarts-adapter/demo.html +1 -1
  53. package/09-assets/model-viewer/demo.html +1 -1
  54. package/09-assets/sparkline/demo.html +1 -1
  55. package/AGENTS.md +8 -0
  56. package/API.md +4 -0
  57. package/CHANGELOG.md +795 -0
  58. package/CONTRIBUTING.md +22 -1
  59. package/README.md +17 -5
  60. package/README.zh-CN.md +16 -6
  61. package/START-HERE.md +12 -7
  62. package/ai/START-HERE.md +37 -3
  63. package/ai/behaviors.json +32 -0
  64. package/ai/cli.js +183 -25
  65. package/ai/components.json +185 -7
  66. package/ai/components.meta.json +13 -0
  67. package/ai/pitfalls-writing.json +119 -0
  68. package/ai/pitfalls.json +193 -0
  69. package/dist/01-tokens/page.css +1 -1
  70. package/dist/02-primitives/choice/choice.css +1 -1
  71. package/dist/02-primitives/input/input.css +1 -1
  72. package/dist/02-primitives/progress/progress.css +1 -1
  73. package/dist/02-primitives/skeleton/skeleton.css +1 -1
  74. package/dist/02-primitives/switch/switch.css +1 -1
  75. package/dist/02-primitives/timeline/timeline.css +1 -0
  76. package/dist/02-primitives/upload/upload.css +1 -0
  77. package/dist/02-primitives/upload/upload.js +515 -0
  78. package/dist/03-patterns/drawer/drawer.js +14 -4
  79. package/dist/04-recipes/app-shell/app-shell.css +1 -0
  80. package/dist/04-recipes/app-shell/shell-nav.js +173 -0
  81. package/dist/04-recipes/table/table.css +1 -1
  82. package/dist/COSTS.md +12 -10
  83. package/docs/ACCESSIBILITY.md +20 -7
  84. package/docs/AI-CONTRACT.md +2 -0
  85. package/docs/BUDGET.md +179 -0
  86. package/docs/CONVENTIONS.md +3 -0
  87. package/docs/STABILITY.md +126 -0
  88. package/esm/index.mjs +2 -0
  89. package/esm/upload.mjs +7 -0
  90. package/index.html +5 -2
  91. package/llms-full.txt +153 -0
  92. package/llms.txt +55 -0
  93. package/package.json +4 -1
@@ -0,0 +1,490 @@
1
+ /* ============================================================================
2
+ upload — 文件上传
3
+ ============================================================================
4
+ 为什么需要它(本库此前唯一的空白)
5
+ ---------------------------------------------------------------------------
6
+ 37 个组件里,表单控件有 input / select / combobox / choice / switch,
7
+ **唯独没有"选文件"**。而上传是「导入金标准 CSV / 上传财报 PDF / 附凭证」
8
+ 这类操作的必经入口 —— 每个项目都在自己糊一个 `<input type=file>`,
9
+ 然后它长得和全站格格不入,且拖放区、多文件、失败重试全缺。
10
+
11
+ 两层形态(这不是"两个组件",是同一份 CSS 的一条升级路径)
12
+ ---------------------------------------------------------------------------
13
+ · 基础 `.upload` —— 原生 `<input type=file>` + `::file-selector-button`
14
+ 美化。**零 JS 完整可用**:选文件、原生文件名回显、
15
+ 表单提交全走浏览器原生路径。
16
+ · 增强 `.upload--drop` —— 加一个拖放区(`<label>`)+ 文件列表。需要
17
+ `upload.js`:拖入、多文件、大小校验、移除。
18
+
19
+ 🔴 为什么必须保留基础形态,而不是"直接上拖放区"
20
+ ---------------------------------------------------------------------------
21
+ 拖放区一旦成为唯一入口,**JS 没加载就等于没有上传功能**,而且用户看不到
22
+ 自己选了什么(原生文件名回显被视觉隐藏吃掉了)。
23
+ 本库的做法:`--drop` 只是**加**了一层,`<input>` 始终是真的、
24
+ 始终在表单里、始终能被键盘 Tab 到。
25
+
26
+ ⭐ 真值方向:`<input>.files` 是唯一真值,文件列表是它的**镜像**。
27
+ 拖放不是"另一条上传通道"——它只是把拖入的 File 塞进 `input.files`
28
+ (经 `DataTransfer`),然后派发 `change`。这样表单序列化、`FormData`、
29
+ 原生 `accept` 校验全都不用重写一遍。
30
+ ⚠️ 反过来做(拖放自己维护数组、提交时再拼)是这类组件最常见的错:
31
+ 两条通道的校验规则迟早不一致,而且 `<form>` 提交时丢文件。
32
+
33
+ 设计要点
34
+ ---------------------------------------------------------------------------
35
+ ① 状态全在 `data-*`(根 `data-state`,列表项 `data-state`),不用 `.is-*`
36
+ ② **信息不靠颜色**:每个状态都有一行文本("上传中 42%" / "失败:超过 20 MB")
37
+ ⇒ 色盲、强制色、灰度打印三种条件下都读得出来
38
+ ③ 图标用内联 `<svg>` + `currentColor`,**不用 mask + background-color**
39
+ —— 后者在强制色下被抹成 Canvas,白底白图形(0.17.0 六类缺陷之一)
40
+ ④ 拖放区边界用 `border`(不是 box-shadow),强制色下会自动变实线
41
+ ⑤ 逻辑属性,RTL 由 `rtl-check.js` 守
42
+ ⑥ 触控 44px;移除按钮视觉小、热区补足
43
+ ========================================================================== */
44
+
45
+ .upload {
46
+ /* 组件私有槽位:改这些就能整体调,不必碰下面的规则 */
47
+ --upload-pad: var(--sp-4);
48
+ --upload-gap: var(--sp-3);
49
+ --upload-icon: 28px;
50
+ --upload-item-pad: var(--sp-2) var(--sp-3);
51
+ --upload-radius: var(--r-md);
52
+
53
+ display: flex;
54
+ flex-direction: column;
55
+ gap: var(--upload-gap);
56
+ min-width: 0; /* 允许在 flex/grid 里收缩,否则长文件名撑破父级 */
57
+ }
58
+
59
+ /* ============================================================================
60
+ 1 · 基础形态:原生 input(零 JS 完整可用)
61
+ ============================================================================ */
62
+ .upload__input {
63
+ /* ⚠️ 不要写 `appearance: none`:那会把文件选择器退化成"没有按钮"的裸文本,
64
+ Chromium 与 Firefox 的行为还不一致。要改外观只能改 `::file-selector-button`。 */
65
+ font: inherit;
66
+ font-size: var(--fs-sm);
67
+ color: var(--text-primary);
68
+ max-width: 100%;
69
+ /* 强制色预留:原生控件本身有边框,但主题里可能被压掉 ⇒ 留一条透明 outline */
70
+ outline: 1px solid transparent;
71
+ outline-offset: 2px;
72
+ border-radius: var(--r-sm);
73
+ }
74
+
75
+ .upload__input::file-selector-button {
76
+ font: inherit;
77
+ font-size: var(--fs-sm);
78
+ font-weight: 500;
79
+ color: var(--text-primary);
80
+ background: var(--surface);
81
+ border: 1px solid var(--border-control);
82
+ border-radius: var(--r-sm);
83
+ padding: var(--sp-2) var(--sp-3);
84
+ margin-inline-end: var(--sp-3);
85
+ min-height: var(--sz-touch-sm);
86
+ cursor: pointer;
87
+ transition: background var(--dur-fast) var(--ease-out),
88
+ border-color var(--dur-fast) var(--ease-out);
89
+ }
90
+
91
+ /* 焦点在 input 上时,原生按钮自己是画不出环的(它是 shadow DOM 的一部分)
92
+ ⇒ 环画在 input 外侧,用户看到的是"整块被框住",仍然是同一个焦点位置。 */
93
+ .upload__input:focus-visible {
94
+ outline: 2px solid var(--accent);
95
+ outline-offset: 2px;
96
+ }
97
+
98
+ /* 禁用:用明确色值,不用 opacity(CONVENTIONS 第八节) */
99
+ .upload__input:disabled {
100
+ color: var(--text-tertiary);
101
+ cursor: not-allowed;
102
+ }
103
+ .upload__input:disabled::file-selector-button {
104
+ color: var(--text-tertiary);
105
+ background: var(--surface-sunken);
106
+ border-color: var(--border-decor);
107
+ cursor: not-allowed;
108
+ }
109
+
110
+ /* ============================================================================
111
+ 2 · 增强形态:拖放区(.upload--drop)
112
+ ============================================================================ */
113
+ .upload--drop {
114
+ gap: var(--sp-2);
115
+ /* 用于给视觉隐藏的 input 定位 —— 否则 `position:absolute` 会相对 body,
116
+ input 跑到页面左上角,Tab 聚焦时浏览器会**滚到那里**。 */
117
+ position: relative;
118
+ }
119
+
120
+ /* 增强形态隐藏原生控件(视觉,不隐藏语义)。
121
+ 🔴 三个属性缺一不可:
122
+ · `position:absolute` + 1px 尺寸 ⇒ 不占布局
123
+ · `opacity:0`(不是 `display:none`)⇒ 仍可聚焦、仍在表单里、仍能被 label 触发
124
+ · 不加 `pointer-events:none` —— 它只有 1px,挡不住 dropzone;
125
+ 加了反而在某些浏览器里让 label 的 for 转发失效(实测 Chromium 正常,
126
+ 但这属于"没必要冒的险")。
127
+ ⚠️ 视觉藏在左上角而不是 `inset-inline-start:-9999px`:后者在 RTL 下会跑到右边,
128
+ 且部分读写模式屏幕阅读器会因"元素在屏幕外"跳过它。 */
129
+ .upload--drop .upload__input {
130
+ position: absolute;
131
+ inset-block-start: 0;
132
+ inset-inline-start: 0;
133
+ width: 1px;
134
+ height: 1px;
135
+ opacity: 0;
136
+ }
137
+
138
+ /* 拖放区是 <label for="…">:点击 = 原生打开选择器,不需要 JS。
139
+ ⇒ 它**不该**进 Tab 序列(焦点属于被关联的 input),见下面的逻辑。 */
140
+ .upload__dropzone {
141
+ display: flex;
142
+ flex-direction: column;
143
+ align-items: center;
144
+ justify-content: center;
145
+ gap: var(--sp-2);
146
+ padding: var(--upload-pad) var(--sp-3);
147
+ min-height: calc(var(--sz-touch) * 2);
148
+ text-align: center;
149
+ color: var(--text-secondary);
150
+ background: var(--surface-sunken);
151
+ border: 1px dashed var(--border-control);
152
+ border-radius: var(--upload-radius);
153
+ cursor: pointer;
154
+ transition: background var(--dur-fast) var(--ease-out),
155
+ border-color var(--dur-fast) var(--ease-out);
156
+ }
157
+
158
+ /* 🔴 强制色下的边界预留(本节全部结论来自 0.17.0 的强制色逐页实拍)
159
+ ---------------------------------------------------------------------
160
+ `border: 1px dashed` 在强制色下会被保留并替换成 CanvasText —— 实拍确认
161
+ dashed 的**虚实也保留**(Chromium 不重画边框样式,只换颜色)。
162
+ 所以在强制色下这条边界不需要额外补救,这也是选 border 而不选
163
+ `box-shadow: inset 0 0 0 1px` 的原因:后者整个被移除,拖放区会消失。
164
+ 判据见 05-audit/upload-check.js ⑤。 */
165
+
166
+ /* 拖拽悬停:`data-state="dragover"` 由 upload.js 写。
167
+ ⚠️ 不能靠 CSS 的 `:hover` 表达拖拽 —— dragover 期间大部分浏览器**
168
+ 不会**更新 :hover,用户会看到"拖过来了但没反应"。 */
169
+ .upload--drop[data-state="dragover"] .upload__dropzone {
170
+ background: var(--accent-soft);
171
+ border-color: var(--accent);
172
+ border-style: solid; /* 虚线变实线:状态不只靠颜色差 */
173
+ color: var(--text-primary);
174
+ }
175
+
176
+ /* 拖放区不是独立焦点站:input 聚焦时把环画在 dropzone 上,
177
+ 用户看到的是"这个区域被选中了",而焦点实际仍在 input 上(无障碍正确)。 */
178
+ .upload--drop .upload__input:focus-visible ~ .upload__dropzone {
179
+ outline: 2px solid var(--accent);
180
+ outline-offset: 2px;
181
+ }
182
+
183
+ /* 禁用:input 禁用后 label 的 for 转发会被浏览器拦住 ⇒ 点击自然无反应。
184
+ 缺的是**看起来不能点**。用明确色值,不用 opacity(CONVENTIONS 第八节):
185
+ opacity 会让对比度随底色漂移,暗色下直接不达标,而且"只是淡了点"表达不了"不能点"。 */
186
+ .upload--drop .upload__input:disabled ~ .upload__dropzone {
187
+ color: var(--text-tertiary);
188
+ background: var(--surface-sunken);
189
+ border-color: var(--border-decor);
190
+ border-style: solid; /* 与可用态(虚线)再差一档 */
191
+ cursor: not-allowed;
192
+ }
193
+
194
+ .upload--drop .upload__input:disabled ~ .upload__dropzone .upload__title,
195
+ .upload--drop .upload__input:disabled ~ .upload__dropzone .upload__browse {
196
+ color: var(--text-tertiary);
197
+ text-decoration: none;
198
+ }
199
+
200
+ /* 🔴 hover 一律门控:触屏上"点一下"会先点亮 :hover 再 click ⇒ 状态粘住不褪,
201
+ 用户看到一块高亮的拖放区,以为还在拖。判据 hover-gate。
202
+ 三条集中成一个块而不是各包一层,是为了别在 dist 里重复三份 media 条件。
203
+ ⚠️ 位置在 dragover 规则**之后**:两者特异性相同(0,3,0),
204
+ 谁后面谁赢 —— 拖拽中不该被 hover 的高亮盖掉。 */
205
+ @media (hover: hover) and (pointer: fine) {
206
+ .upload__input::file-selector-button:hover {
207
+ background: var(--surface-sunken);
208
+ border-color: var(--accent);
209
+ }
210
+
211
+ .upload--drop .upload__dropzone:hover {
212
+ background: var(--accent-soft);
213
+ border-color: var(--accent);
214
+ }
215
+
216
+ .upload__remove:hover {
217
+ color: var(--danger);
218
+ background: var(--danger-bg);
219
+ }
220
+ }
221
+
222
+ /* 图标:内联 SVG,`stroke="currentColor"`。
223
+ ⭐ 这就是"用 color 画图形",强制色下 currentColor → CanvasText,
224
+ 与背景必然不同色 ⇒ 一定可见。对比 mask + background 的写法(必死)。 */
225
+ .upload__icon {
226
+ width: var(--upload-icon);
227
+ height: var(--upload-icon);
228
+ color: var(--text-tertiary);
229
+ flex: none;
230
+ }
231
+
232
+ .upload--drop[data-state="dragover"] .upload__icon {
233
+ color: var(--accent);
234
+ }
235
+
236
+ .upload__title {
237
+ font-size: var(--fs-sm);
238
+ color: var(--text-primary);
239
+ }
240
+
241
+ .upload__browse {
242
+ /* 视觉上是链接,但**不是** <a>:它不导航,点它等于点整个 dropzone。
243
+ role="link" 会谎报行为(读屏会说"链接"然后什么都不发生)⇒ 纯文本 + 下划线。 */
244
+ color: var(--accent);
245
+ text-decoration: underline;
246
+ text-underline-offset: 2px;
247
+ }
248
+
249
+ .upload__hint {
250
+ font-size: var(--fs-xs);
251
+ color: var(--text-tertiary);
252
+ /* 容器窄的时候允许换行成两行,不要 ellipsis —— 大小限制被截断会误导用户 */
253
+ max-width: 42ch;
254
+ }
255
+
256
+ /* ============================================================================
257
+ 3 · 文件列表
258
+ ============================================================================ */
259
+ .upload__list {
260
+ display: flex;
261
+ flex-direction: column;
262
+ gap: var(--sp-1);
263
+ margin: 0;
264
+ padding: 0;
265
+ list-style: none;
266
+ }
267
+
268
+ /* 空列表不占位:没有文件时不该出现一条空白缝 */
269
+ .upload__list:empty {
270
+ display: none;
271
+ }
272
+
273
+ .upload__item {
274
+ display: grid;
275
+ grid-template-columns: auto minmax(0, 1fr) auto auto;
276
+ grid-template-areas: "mark name meta remove";
277
+ align-items: center;
278
+ gap: var(--sp-1) var(--sp-2);
279
+ padding: var(--upload-item-pad);
280
+ background: var(--surface);
281
+ border: 1px solid var(--border-decor);
282
+ border-radius: var(--r-sm);
283
+ font-size: var(--fs-sm);
284
+ }
285
+
286
+ /* 🔴 被拒项没有移除按钮,但必须**占住那个格**。
287
+ -------------------------------------------------------------------------
288
+ 否则"有按钮的项"被 32px 热区撑到 50px,"没按钮的项"只有 42px 内容高
289
+ ⇒ 列表看起来"有的行空着"(0.17.0 实拍确认:54 vs 42)。
290
+
291
+ ⚠️ 不要用 `min-height: 50px` 抹平:本库的 `box-sizing: border-box` 挂在
292
+ `.page *` 上,是**选配**(CONVENTIONS 约定:组件库不改宿主的盒模型)。
293
+ 同一句 min-height 在有骨架的页面里算出 50,在没有骨架的页面里算出 68 ——
294
+ 一个"看起来对齐了"的假象,别人拿组件单独用时又不对了。
295
+ 给缺失的那一格补一个同尺寸的占位,两种盒模型下结果都是 50。 */
296
+ .upload__item[data-state="error"]::after {
297
+ content: "";
298
+ grid-area: remove;
299
+ width: var(--sz-touch-sm);
300
+ height: var(--sz-touch-sm);
301
+ }
302
+
303
+ .upload__mark {
304
+ grid-area: mark;
305
+ display: flex;
306
+ align-items: center;
307
+ justify-content: center;
308
+ width: 18px;
309
+ height: 18px;
310
+ color: var(--text-tertiary);
311
+ flex: none;
312
+ }
313
+
314
+ /* 状态标记的形状差异 —— 不靠颜色,靠**形状**和文本。
315
+ pending = 空心圆 / done = 勾 / error = 叹号方框 */
316
+ .upload__item[data-state="done"] .upload__mark { color: var(--success); }
317
+ .upload__item[data-state="error"] .upload__mark { color: var(--danger); }
318
+ .upload__item[data-state="pending"] .upload__mark { color: var(--text-secondary); }
319
+
320
+ .upload__mark svg {
321
+ width: 16px;
322
+ height: 16px;
323
+ display: block;
324
+ }
325
+
326
+ .upload__name {
327
+ grid-area: name;
328
+ /* 长文件名:中段省略比尾部省略更能保住扩展名和日期
329
+ ("2025年度审计报告-合并…v3.pdf" 比 "2025年度审计报…" 有用) */
330
+ overflow: hidden;
331
+ text-overflow: ellipsis;
332
+ white-space: nowrap;
333
+ color: var(--text-primary);
334
+ /* `direction: ltr` 不行(会打乱 RTL 下的排版)。
335
+ 用 `overflow-wrap: anywhere` 处理无空格的长文件名(GitHub 那种堆叠路径) */
336
+ overflow-wrap: anywhere;
337
+ }
338
+
339
+ .upload__meta {
340
+ grid-area: meta;
341
+ font-size: var(--fs-xs);
342
+ color: var(--text-secondary);
343
+ white-space: nowrap;
344
+ /* 数字等宽由 `.num` 类提供(upload.js 生成 meta 时会加上),
345
+ 这里不写等宽属性 —— 本库的数字排印只有一处定义(typography.css),
346
+ 组件 CSS 里自己写会多出第二份(numeric-gate 判这一条)。
347
+ ⚠️ 没引 typography.css 时 `.num` 无效果,退化成普通数字 —— 无害,
348
+ 因为百分比同时在文本里("上传中 42%"),不靠宽度对齐传意。 */
349
+ }
350
+
351
+ .upload__item[data-state="error"] .upload__meta {
352
+ color: var(--danger);
353
+ white-space: normal; /* 错误原因通常更长,允许换行 */
354
+ text-align: end;
355
+ }
356
+
357
+ .upload__progress {
358
+ grid-column: 1 / -1; /* 显式跨满整行 ⇒ 自动落到第二行(隐式轨道) */
359
+ /* ⚠️ 这里不写 `grid-template-areas` 的第二行、也不用 `:has()` 判"有没有进度条"。
360
+ 本库禁用 `:has()`(CSS Level 4,精简档 WebView 版本不确定,见 choice.css 第 390 行),
361
+ 而 `grid-column: 1 / -1` 已经让"有进度条就自动多一行、没有就保持一行"成立。 */
362
+ /* 进度条本体用 progress 组件(demo 里同时引入 progress.css)。
363
+ ⚠️ 这里不复制 progress 的样式:复制一份 = 两处真值 = 迟早漂移。 */
364
+ display: block;
365
+ min-width: 0;
366
+ }
367
+
368
+ .upload__item[data-state="done"] .upload__progress,
369
+ .upload__item[data-state="error"] .upload__progress {
370
+ display: none; /* 完成/失败后进度条没有意义了 */
371
+ }
372
+
373
+ .upload__remove {
374
+ grid-area: remove;
375
+ display: inline-flex;
376
+ align-items: center;
377
+ justify-content: center;
378
+ /* 32px 热区(`--sz-touch-sm`),16px 视觉。
379
+ 🔴 这里**不**用 44px(`--sz-touch`):列表项之间只隔 4px,44px 的热区会
380
+ 溢出到相邻项的可行点区域上 —— "想删第 2 个,点到了第 3 个"。
381
+ 密排列表用 sm 档是本库既有约定(tokens.css 第 426 行),
382
+ 真正的 44px 场景见 button / choice。 */
383
+ width: var(--sz-touch-sm);
384
+ height: var(--sz-touch-sm);
385
+ padding: 0;
386
+ color: var(--text-tertiary);
387
+ background: none;
388
+ border: 0;
389
+ border-radius: var(--r-sm);
390
+ cursor: pointer;
391
+ transition: color var(--dur-fast) var(--ease-out),
392
+ background var(--dur-fast) var(--ease-out);
393
+ }
394
+
395
+ .upload__remove:focus-visible {
396
+ outline: 2px solid var(--accent);
397
+ outline-offset: 2px;
398
+ }
399
+
400
+ .upload__remove svg {
401
+ width: 16px;
402
+ height: 16px;
403
+ display: block;
404
+ pointer-events: none; /* 点在图标上也要算点在按钮上 */
405
+ }
406
+
407
+ /* ============================================================================
408
+ 4 · 播报区(视觉隐藏,但**不是** display:none)
409
+ ============================================================================
410
+ 文件增减必须播报:`<ul>` 内容变化读屏默认**不读**(它不是 live region),
411
+ 用户在"移除"之后完全不知道列表变短了。
412
+ ⚠️ 别给 `<ul>` 直接加 aria-live:那会连"上传进度从 42% 到 43%"一起读,
413
+ 每 200ms 打断一次。单独一个 role="status" 元素,只播报**增删与最终结果**。
414
+ ========================================================================== */
415
+ .upload__status {
416
+ position: absolute;
417
+ width: 1px;
418
+ height: 1px;
419
+ overflow: hidden;
420
+ clip: rect(0 0 0 0);
421
+ clip-path: inset(50%);
422
+ white-space: nowrap;
423
+ }
424
+
425
+ /* ============================================================================
426
+ 5 · 变体
427
+ ============================================================================ */
428
+
429
+ /* 紧凑:图标与标题排一行,给侧栏/抽屉里的窄空间用 */
430
+ .upload--compact .upload__dropzone {
431
+ flex-direction: row;
432
+ justify-content: flex-start;
433
+ text-align: start;
434
+ min-height: var(--sz-touch);
435
+ padding: var(--sp-2) var(--sp-3);
436
+ }
437
+ .upload--compact .upload__icon {
438
+ --upload-icon: 20px;
439
+ }
440
+ .upload--compact .upload__hint {
441
+ display: none; /* 紧凑档不显示限制说明,靠 title 与页面文案 */
442
+ }
443
+
444
+ /* ============================================================================
445
+ 6 · 槽宽适配(CONVENTIONS 第十一节:看槽宽,不看视口)
446
+ ============================================================================
447
+ 两个方向都写:只写 max-width 的话,一旦宽槽里层叠了一条 max-width 规则,
448
+ 变宽时撤不回来(不匹配不会撤销任何规则)。
449
+ ⚠️ 基础样式不在这里面 —— 老引擎丢弃整块 @container,写进来等于没样式。
450
+ ========================================================================== */
451
+ @container (max-width: 26rem) {
452
+ .upload--drop .upload__dropzone {
453
+ padding: var(--sp-3);
454
+ }
455
+ .upload--drop .upload__icon {
456
+ --upload-icon: 22px;
457
+ }
458
+ /* 窄槽里拖放区只是"多一层装饰",限制说明要留着 ——
459
+ 它决定了用户选什么文件,不能为了省地方藏起来。 */
460
+ }
461
+
462
+ @container (min-width: 26.0625rem) {
463
+ .upload--drop .upload__dropzone {
464
+ padding: var(--upload-pad) var(--sp-3);
465
+ }
466
+ .upload--drop .upload__icon {
467
+ --upload-icon: 28px;
468
+ }
469
+ }
470
+
471
+ /* ============================================================================
472
+ 7 · 减少动效(CONVENTIONS 第四节第 1 档:位移/缩放关,颜色可留)
473
+ ============================================================================
474
+ 本组件没有任何位移动画,只有颜色过渡 ⇒ 无需整体关闭。
475
+ 显式写下这条是为了**声明"我没有第 1 档动效"**,而不是漏掉。
476
+ 判据:motion-policy.json —— 声明了没实现是红,实现了没声明也是红。
477
+ ========================================================================== */
478
+
479
+ /* ============================================================================
480
+ 8 · 强制色模式
481
+ ============================================================================
482
+ 本组件**不需要** @media (forced-colors: active)。
483
+ 理由(全部是 0.17.0 的实测结论,不是推测):
484
+ · 边界全是 `border`,强制色下保留并替换成 CanvasText
485
+ · 图标全是内联 SVG + currentColor,跟着文字色走
486
+ · 状态信息全在文本里(mark 只是辅助)
487
+ 写一条空的 media 块反而有害:它会让下一个人以为"这里有特殊处理过"。
488
+ 判据见 05-audit/upload-check.js ⑤(在 forced-colors 下断言
489
+ "dropzone 边框色 ≠ 背景色" + "图标色 ≠ 背景色")。
490
+ ========================================================================== */