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.
- package/01-tokens/README.md +236 -0
- package/01-tokens/behavior/dismissable.js +132 -0
- package/01-tokens/behavior/emit.js +76 -0
- package/01-tokens/behavior/focus-return.js +81 -0
- package/01-tokens/behavior/roving.js +152 -0
- package/01-tokens/behavior/typeahead.js +124 -0
- package/01-tokens/focus-ring-demo.html +133 -0
- package/01-tokens/focus-ring.css +102 -0
- package/01-tokens/page.css +244 -0
- package/01-tokens/polyfill.js +116 -0
- package/01-tokens/responsive-demo.html +171 -0
- package/01-tokens/scrollbar-demo.html +194 -0
- package/01-tokens/scrollbar.css +179 -0
- package/01-tokens/theme-toggle.js +103 -0
- package/01-tokens/tokens.css +972 -0
- package/01-tokens/typography.css +156 -0
- package/02-primitives/badge/README.md +71 -0
- package/02-primitives/badge/badge.css +141 -0
- package/02-primitives/badge/demo.html +101 -0
- package/02-primitives/button/README.md +99 -0
- package/02-primitives/button/button.css +353 -0
- package/02-primitives/button/demo.html +200 -0
- package/02-primitives/card/README.md +145 -0
- package/02-primitives/card/card.css +246 -0
- package/02-primitives/card/demo.html +170 -0
- package/02-primitives/choice/README.md +161 -0
- package/02-primitives/choice/choice.css +395 -0
- package/02-primitives/choice/demo.html +277 -0
- package/02-primitives/combobox/README.md +79 -0
- package/02-primitives/combobox/combobox.css +223 -0
- package/02-primitives/combobox/combobox.js +485 -0
- package/02-primitives/combobox/demo.html +109 -0
- package/02-primitives/date-range/README.md +100 -0
- package/02-primitives/date-range/date-range.css +206 -0
- package/02-primitives/date-range/date-range.js +301 -0
- package/02-primitives/date-range/demo.html +104 -0
- package/02-primitives/input/README.md +170 -0
- package/02-primitives/input/demo.html +246 -0
- package/02-primitives/input/input.css +239 -0
- package/02-primitives/popover/README.md +80 -0
- package/02-primitives/popover/demo.html +150 -0
- package/02-primitives/popover/popover.css +187 -0
- package/02-primitives/popover/popover.js +331 -0
- package/02-primitives/progress/README.md +82 -0
- package/02-primitives/progress/demo.html +154 -0
- package/02-primitives/progress/progress.css +172 -0
- package/02-primitives/select/README.md +95 -0
- package/02-primitives/select/demo.html +121 -0
- package/02-primitives/select/select.css +151 -0
- package/02-primitives/select/select.js +549 -0
- package/02-primitives/separator/README.md +63 -0
- package/02-primitives/separator/demo.html +84 -0
- package/02-primitives/separator/separator.css +73 -0
- package/02-primitives/skeleton/README.md +69 -0
- package/02-primitives/skeleton/demo.html +110 -0
- package/02-primitives/skeleton/skeleton.css +96 -0
- package/02-primitives/switch/README.md +65 -0
- package/02-primitives/switch/demo.html +115 -0
- package/02-primitives/switch/switch.css +370 -0
- package/03-patterns/accordion/README.md +121 -0
- package/03-patterns/accordion/accordion.css +453 -0
- package/03-patterns/accordion/accordion.js +369 -0
- package/03-patterns/accordion/demo.html +223 -0
- package/03-patterns/content/README.md +103 -0
- package/03-patterns/content/content.css +286 -0
- package/03-patterns/content/demo.html +88 -0
- package/03-patterns/drawer/README.md +79 -0
- package/03-patterns/drawer/demo.html +101 -0
- package/03-patterns/drawer/drawer.css +129 -0
- package/03-patterns/drawer/drawer.js +258 -0
- package/03-patterns/dropdown/README.md +78 -0
- package/03-patterns/dropdown/demo.html +101 -0
- package/03-patterns/dropdown/dropdown.css +135 -0
- package/03-patterns/dropdown/dropdown.js +451 -0
- package/03-patterns/form-validation/README.md +147 -0
- package/03-patterns/form-validation/demo.html +280 -0
- package/03-patterns/form-validation/form-validation.css +263 -0
- package/03-patterns/list/README.md +150 -0
- package/03-patterns/list/demo.html +225 -0
- package/03-patterns/list/flip.js +201 -0
- package/03-patterns/list/list.css +162 -0
- package/03-patterns/nav/README.md +225 -0
- package/03-patterns/nav/demo.html +131 -0
- package/03-patterns/nav/nav.css +314 -0
- package/03-patterns/nav/nav.js +430 -0
- package/03-patterns/nav/toc.js +111 -0
- package/03-patterns/overlay/README.md +196 -0
- package/03-patterns/overlay/demo.html +241 -0
- package/03-patterns/overlay/overlay.css +319 -0
- package/03-patterns/overlay/overlay.js +563 -0
- package/03-patterns/pagination/README.md +114 -0
- package/03-patterns/pagination/demo.html +165 -0
- package/03-patterns/pagination/pagination.css +195 -0
- package/03-patterns/pagination/pagination.js +395 -0
- package/03-patterns/reveal/README.md +91 -0
- package/03-patterns/reveal/demo.html +106 -0
- package/03-patterns/reveal/reveal.css +93 -0
- package/03-patterns/reveal/reveal.js +172 -0
- package/03-patterns/states/README.md +141 -0
- package/03-patterns/states/demo.html +240 -0
- package/03-patterns/states/states.css +252 -0
- package/03-patterns/tabs/README.md +84 -0
- package/03-patterns/tabs/demo.html +139 -0
- package/03-patterns/tabs/tabs.css +172 -0
- package/03-patterns/tabs/tabs.js +307 -0
- package/03-patterns/tooltip/README.md +96 -0
- package/03-patterns/tooltip/demo.html +78 -0
- package/03-patterns/tooltip/tooltip.css +89 -0
- package/03-patterns/tooltip/tooltip.js +288 -0
- package/03-patterns/tree/README.md +94 -0
- package/03-patterns/tree/demo.html +138 -0
- package/03-patterns/tree/tree.css +113 -0
- package/03-patterns/tree/tree.js +456 -0
- package/04-recipes/analysis-report/README.md +80 -0
- package/04-recipes/analysis-report/analysis-report.css +187 -0
- package/04-recipes/analysis-report/demo.html +274 -0
- package/04-recipes/data-showcase/README.md +45 -0
- package/04-recipes/data-showcase/demo.html +247 -0
- package/04-recipes/detail-page/README.md +93 -0
- package/04-recipes/detail-page/demo.html +55 -0
- package/04-recipes/detail-page/detail-page.css +190 -0
- package/04-recipes/detail-page/full.html +60 -0
- package/04-recipes/detail-page/null.html +60 -0
- package/04-recipes/detail-page/partial.html +60 -0
- package/04-recipes/detail-page/verify-skeleton.py +19 -0
- package/04-recipes/longform/README.md +93 -0
- package/04-recipes/longform/agc-memory.svg +4 -0
- package/04-recipes/longform/content.html +610 -0
- package/04-recipes/longform/content.md +51 -0
- package/04-recipes/longform/longform.html +627 -0
- package/04-recipes/report/README.md +176 -0
- package/04-recipes/report/demo.html +355 -0
- package/04-recipes/report/report.css +364 -0
- package/04-recipes/table/README.md +103 -0
- package/04-recipes/table/demo.html +455 -0
- package/04-recipes/table/table.css +372 -0
- package/04-recipes/tierA-tokens.css +189 -0
- package/09-assets/README.md +74 -0
- package/09-assets/bar/README.md +94 -0
- package/09-assets/bar/bar.css +166 -0
- package/09-assets/bar/bar.js +320 -0
- package/09-assets/bar/demo.html +176 -0
- package/09-assets/echarts-adapter/demo.html +192 -0
- package/09-assets/echarts-adapter/echarts-adapter.js +277 -0
- package/09-assets/model-viewer/README.md +155 -0
- package/09-assets/model-viewer/demo.html +105 -0
- package/09-assets/model-viewer/model-viewer.css +143 -0
- package/09-assets/model-viewer/model-viewer.js +423 -0
- package/09-assets/scientific-plot/README.md +94 -0
- package/09-assets/scientific-plot/demo.html +252 -0
- package/09-assets/scientific-plot/scientific-plot.css +141 -0
- package/09-assets/scientific-plot/scientific-plot.js +610 -0
- package/09-assets/sparkline/README.md +153 -0
- package/09-assets/sparkline/demo.html +163 -0
- package/09-assets/sparkline/sparkline.css +158 -0
- package/09-assets/sparkline/sparkline.js +508 -0
- package/API.md +332 -0
- package/CHANGELOG.md +3116 -0
- package/CODE_OF_CONDUCT.md +64 -0
- package/CONTRIBUTING.md +89 -0
- package/ERRORS.md +1658 -0
- package/LICENSE +21 -0
- package/README.md +491 -2
- package/SECURITY.md +72 -0
- package/START-HERE.md +164 -0
- package/SUPPORT.md +109 -0
- package/adapters/presentation/README.md +113 -0
- package/adapters/presentation/check.js +101 -0
- package/adapters/presentation/deck.schema.json +74 -0
- package/adapters/presentation/to-doc.js +138 -0
- package/ai/START-HERE.md +104 -0
- package/ai/behaviors.json +380 -0
- package/ai/behaviors.schema.json +61 -0
- package/ai/capabilities.json +56 -0
- package/ai/cli.js +387 -0
- package/ai/components.json +1944 -0
- package/ai/components.meta.json +168 -0
- package/ai/components.schema.json +220 -0
- package/ai/contract.schema.json +147 -0
- package/ai/deprecations.json +55 -0
- package/ai/deprecations.schema.json +85 -0
- package/ai/motion-policy.json +22 -0
- package/ai/patch.js +323 -0
- package/ai/patch.schema.json +63 -0
- package/ai/profiles.json +92 -0
- package/ai/tokens.audience.json +55 -0
- package/ai/tokens.audience.schema.json +73 -0
- package/ai/tokens.json +762 -0
- package/ai/tokens.schema.json +58 -0
- package/ai/tokens.tree.json +752 -0
- package/ai/tokens.tree.schema.json +56 -0
- package/ai/validate.js +383 -0
- package/dist/01-tokens/behavior/dismissable.js +60 -0
- package/dist/01-tokens/behavior/emit.js +21 -0
- package/dist/01-tokens/behavior/focus-return.js +31 -0
- package/dist/01-tokens/behavior/roving.js +77 -0
- package/dist/01-tokens/behavior/typeahead.js +61 -0
- package/dist/01-tokens/focus-ring.css +1 -0
- package/dist/01-tokens/page.css +1 -0
- package/dist/01-tokens/polyfill.js +78 -0
- package/dist/01-tokens/scrollbar.css +1 -0
- package/dist/01-tokens/theme-toggle.js +61 -0
- package/dist/01-tokens/tokens.css +1 -0
- package/dist/01-tokens/typography.css +1 -0
- package/dist/02-primitives/badge/badge.css +1 -0
- package/dist/02-primitives/button/button.css +1 -0
- package/dist/02-primitives/card/card.css +1 -0
- package/dist/02-primitives/choice/choice.css +1 -0
- package/dist/02-primitives/combobox/combobox.css +1 -0
- package/dist/02-primitives/combobox/combobox.js +439 -0
- package/dist/02-primitives/date-range/date-range.css +1 -0
- package/dist/02-primitives/date-range/date-range.js +267 -0
- package/dist/02-primitives/input/input.css +1 -0
- package/dist/02-primitives/popover/popover.css +1 -0
- package/dist/02-primitives/popover/popover.js +257 -0
- package/dist/02-primitives/progress/progress.css +1 -0
- package/dist/02-primitives/select/select.css +1 -0
- package/dist/02-primitives/select/select.js +449 -0
- package/dist/02-primitives/separator/separator.css +1 -0
- package/dist/02-primitives/skeleton/skeleton.css +1 -0
- package/dist/02-primitives/switch/switch.css +1 -0
- package/dist/03-patterns/accordion/accordion.css +1 -0
- package/dist/03-patterns/accordion/accordion.js +219 -0
- package/dist/03-patterns/content/content.css +1 -0
- package/dist/03-patterns/drawer/drawer.css +1 -0
- package/dist/03-patterns/drawer/drawer.js +229 -0
- package/dist/03-patterns/dropdown/dropdown.css +1 -0
- package/dist/03-patterns/dropdown/dropdown.js +365 -0
- package/dist/03-patterns/form-validation/form-validation.css +1 -0
- package/dist/03-patterns/list/flip.js +117 -0
- package/dist/03-patterns/list/list.css +1 -0
- package/dist/03-patterns/nav/nav.css +1 -0
- package/dist/03-patterns/nav/nav.js +322 -0
- package/dist/03-patterns/nav/toc.js +76 -0
- package/dist/03-patterns/overlay/overlay.css +1 -0
- package/dist/03-patterns/overlay/overlay.js +398 -0
- package/dist/03-patterns/pagination/pagination.css +1 -0
- package/dist/03-patterns/pagination/pagination.js +273 -0
- package/dist/03-patterns/reveal/reveal.css +1 -0
- package/dist/03-patterns/reveal/reveal.js +138 -0
- package/dist/03-patterns/states/states.css +1 -0
- package/dist/03-patterns/tabs/tabs.css +1 -0
- package/dist/03-patterns/tabs/tabs.js +242 -0
- package/dist/03-patterns/tooltip/tooltip.css +1 -0
- package/dist/03-patterns/tooltip/tooltip.js +213 -0
- package/dist/03-patterns/tree/tree.css +1 -0
- package/dist/03-patterns/tree/tree.js +383 -0
- package/dist/04-recipes/analysis-report/analysis-report.css +1 -0
- package/dist/04-recipes/detail-page/detail-page.css +1 -0
- package/dist/04-recipes/report/report.css +1 -0
- package/dist/04-recipes/table/table.css +1 -0
- package/dist/04-recipes/tierA-tokens.css +1 -0
- package/dist/09-assets/bar/bar.css +1 -0
- package/dist/09-assets/bar/bar.js +222 -0
- package/dist/09-assets/echarts-adapter/echarts-adapter.js +185 -0
- package/dist/09-assets/model-viewer/model-viewer.css +1 -0
- package/dist/09-assets/model-viewer/model-viewer.js +320 -0
- package/dist/09-assets/scientific-plot/scientific-plot.css +1 -0
- package/dist/09-assets/scientific-plot/scientific-plot.js +568 -0
- package/dist/09-assets/sparkline/sparkline.css +1 -0
- package/dist/09-assets/sparkline/sparkline.js +265 -0
- package/dist/COSTS.md +56 -0
- package/docs/AI-CONTRACT.md +197 -0
- package/docs/BACKLOG.md +127 -0
- package/docs/BENCHMARK.md +252 -0
- package/docs/GUIDANCE.md +154 -0
- package/docs/INVARIANT.md +372 -0
- package/docs/LAYER-BASELINE.md +158 -0
- package/docs/RELEASING.md +275 -0
- package/docs/RFC-THEME-SCOPE.md +141 -0
- package/docs/ROADMAP.md +182 -0
- package/index.html +82 -0
- package/package.json +107 -4
- package/types/index.d.ts +310 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# checkbox / radio / select
|
|
2
|
+
|
|
3
|
+
| 字段 | 内容 |
|
|
4
|
+
|---|---|
|
|
5
|
+
| **来源** | 原创 |
|
|
6
|
+
| **实测** | `demo.html` 顶部面板**实时显示原生键盘事件**(观测,不是模拟);门禁零 HIGH |
|
|
7
|
+
| **适用** | 精简档 / B / C,纯 CSS |
|
|
8
|
+
| **依赖** | `01-tokens/tokens.css` `02-primitives/input/input.css`(select 段落用它的 `.field__label` / `.field__hint`) + `01-tokens/typography.css`(`.note` 提示框) + `01-tokens/page.css`(页面骨架:`.wrap` / `.lede` / `.note`) |
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 三条铁律
|
|
13
|
+
|
|
14
|
+
**1. 绝不 `display:none` / `visibility:hidden` 隐藏原生控件。**
|
|
15
|
+
那会把它从 tab 顺序移除,键盘用户彻底到不了。**用鼠标测完全测不出来。**
|
|
16
|
+
|
|
17
|
+
**2. 不改原生控件的结构和 DOM 位置,只改外观。**
|
|
18
|
+
|
|
19
|
+
**3. 焦点环必须画在看得见的元素上。**
|
|
20
|
+
input 被 `opacity:0` 覆盖后,它自己的 `outline` 你根本看不见。
|
|
21
|
+
|
|
22
|
+
```css
|
|
23
|
+
.choice__input:focus-visible + .choice__mark { outline: 2px solid var(--accent); }
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`states.py` 的 choice 形态**只认 `:focus-visible +` 这种相邻兄弟写法**,
|
|
27
|
+
单纯写 `:focus-visible {}` 判为不通过——因为那正是会骗过门禁的错误写法。
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 实现手法
|
|
32
|
+
|
|
33
|
+
原生 input 用 `opacity:0` + 绝对定位铺满整个 label,于是三件事同时成立:
|
|
34
|
+
|
|
35
|
+
- 点击**整个标签**都能切换(不必精确点 20px 的方框)
|
|
36
|
+
- Space / 方向键行为 **100% 原生**
|
|
37
|
+
- 触摸目标是 **44px**(视觉方框只有 20px)
|
|
38
|
+
|
|
39
|
+
**为什么不用 `appearance:none` 直接美化 input**:那样方框和文字没法并排(input 只有一个盒子)。
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## checkbox
|
|
44
|
+
|
|
45
|
+
要支持 **indeterminate**(第三态)。"全选"复选框在"部分选中"时必须能表达中间状态,
|
|
46
|
+
否则用户不知道哪些被选中了。不做这个态的库基本都缺。
|
|
47
|
+
|
|
48
|
+
`:indeterminate` **只能由 JS 设置**(`el.indeterminate = true`),CSS 不影响它。
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## radio 组:必须 `fieldset + legend`
|
|
53
|
+
|
|
54
|
+
这是原生语义,屏幕阅读器会播报"第几组、共几项、当前选中第几项"。
|
|
55
|
+
|
|
56
|
+
单选互斥、同一个 `name`、方向键移动即选中——全部依赖原生行为。
|
|
57
|
+
用 `<div>` + `<span>` 模拟,这些全丢。
|
|
58
|
+
|
|
59
|
+
```html
|
|
60
|
+
<fieldset class="choice__group">
|
|
61
|
+
<legend class="choice__legend">时间范围</legend>
|
|
62
|
+
<div class="choice__options">
|
|
63
|
+
<label class="choice choice--radio">
|
|
64
|
+
<input class="choice__input" type="radio" name="range" value="30" checked>
|
|
65
|
+
...
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
🔴 **命名注意**:同族元素用 `__`(`.choice__group`),不用 `-`。
|
|
69
|
+
`states.py` 靠 BEM 根类归组状态,写成 `.choice-group` 会让容器脱离根类、被误判缺状态——
|
|
70
|
+
**这个门禁反过来纠正过我的命名**。
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## select:用原生的,别自做
|
|
75
|
+
|
|
76
|
+
原生 select 在移动端是**最好的**:系统弹出带 wheel 的选择器、语言跟随系统、无障碍由系统保证。
|
|
77
|
+
换成 `<div>` 模拟,这些全部要重写,且必然漏一部分。
|
|
78
|
+
|
|
79
|
+
只改外观:
|
|
80
|
+
|
|
81
|
+
```css
|
|
82
|
+
.select__control { appearance: none; }
|
|
83
|
+
.select__control::-ms-expand { display: none; }
|
|
84
|
+
.select__arrow { pointer-events: none; } /* 🔴 不写这句,箭头吃掉点击 */
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
键盘行为(↑↓ 选择、Enter 确认、Esc 关闭)**全部保留**。
|
|
88
|
+
|
|
89
|
+
select 用 `:focus` 不用 `:focus-visible`——和 input 一样,用户必须随时知道当前选的是哪个。
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## disabled
|
|
94
|
+
|
|
95
|
+
不用 `opacity`(对比度会变成不可控的随机数),用明确的低对比色组。
|
|
96
|
+
**disabled 之后 hover 不能有任何变化**,否则看着像还能点。
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 🔴 结构选型:两种写法都安全
|
|
101
|
+
|
|
102
|
+
我曾把结构从「input 在 label 内」改成「input 与 label 平级 + for」,
|
|
103
|
+
理由是怕**双触发**(input 铺满 label ⇒ 点击既命中 input 又冒泡到 label ⇒ 翻转两次)。
|
|
104
|
+
|
|
105
|
+
**用 puppeteer 真实鼠标点击实测两种结构:**
|
|
106
|
+
|
|
107
|
+
| 结构 | 连点三次 | 结论 |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| A:input 在 label 内 | `true → false → true` | **无双触发** |
|
|
110
|
+
| B:input 平级 + `for` | `true → false → true` | **无双触发** |
|
|
111
|
+
|
|
112
|
+
**HTML 规范本身就有防护**:
|
|
113
|
+
> label 的激活行为对「交互性后代」的事件**必须什么都不做**。
|
|
114
|
+
|
|
115
|
+
⇒ **我当初的担心是多余的。** 两种都能用,当前保持 B(已用真实点击验证过)。
|
|
116
|
+
仍选 B 的理由:input 与 label 分离后 label 内部可自由排版。
|
|
117
|
+
|
|
118
|
+
🔴 **但改结构有个真实的坑(我踩过)**:
|
|
119
|
+
`.choice__mark` 不再是 input 的**相邻兄弟**,所有 `X + .choice__mark` 规则**全部失效**。
|
|
120
|
+
本库一度有 **7 处**同时失效(勾号 / disabled / indeterminate / radio),
|
|
121
|
+
症状是「**属性变了但框看着没变**」—— 面板显示"已勾选",框里是空的。
|
|
122
|
+
|
|
123
|
+
**属性测试永远抓不到这类 bug**,必须测"视觉有没有跟着变"。
|
|
124
|
+
`el.click()` 也抓不到(它是程序化调用,**不做命中测试**)。
|
|
125
|
+
|
|
126
|
+
**验证**:
|
|
127
|
+
```bash
|
|
128
|
+
node 05-audit/clicktest.js choice # 真实鼠标点击,11 项
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## 门禁
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
python3 05-audit/states.py 01-tokens 02-primitives 03-patterns 04-recipes
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
choice 形态查三项,全是 HIGH:
|
|
140
|
+
|
|
141
|
+
| 查 | 为什么 HIGH |
|
|
142
|
+
|---|---|
|
|
143
|
+
| `disabled` | 以为坏了 |
|
|
144
|
+
| `:focus-visible +` | 焦点环必须画在可见标记上 |
|
|
145
|
+
| `:checked` / `:indeterminate` | 没有选中态视觉反馈 |
|
|
146
|
+
|
|
147
|
+
select 形态查 `disabled` / `:focus`(**不是** focus-visible)/ `hover`。
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## 反例
|
|
152
|
+
|
|
153
|
+
- ❌ **`display:none` 隐藏原生 input** —— 键盘不可达,鼠标测不出来
|
|
154
|
+
- ❌ **自己用 `<div>` 做 checkbox** —— Space / Tab / 表单提交 / 屏幕阅读器全要重写
|
|
155
|
+
- ❌ **焦点环画在 input 上** —— input 透明,outline 看不见
|
|
156
|
+
- ❌ **用 opacity 做 disabled** —— 对比度随机数
|
|
157
|
+
- ❌ **radio 组用 `<div>` 模拟** —— 丢掉 fieldset/legend 的原生语义
|
|
158
|
+
- ❌ **箭头不写 `pointer-events:none`** —— 吃掉点击
|
|
159
|
+
- ❌ **换掉原生 select 做自定义下拉** —— 移动端体验和无障碍全要重做
|
|
160
|
+
- ❌ **checkbox 不做 indeterminate** —— "全选"无法表达中间状态
|
|
161
|
+
- ❌ **同族元素用 `-` 而不是 `__`** —— BEM 违规,状态归组会失效
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
frontend-lib / 02-primitives / choice / choice.css
|
|
3
|
+
原生选择控件族:checkbox / radio / select
|
|
4
|
+
|
|
5
|
+
依赖:01-tokens/tokens.css
|
|
6
|
+
版本:A ✅ / B ✅ / C ✅
|
|
7
|
+
|
|
8
|
+
🔴 这一组是全库里最容易做坏的一组。为什么:
|
|
9
|
+
浏览器给这些控件的键盘行为是**无障碍的地基**——
|
|
10
|
+
checkbox : Space 切换
|
|
11
|
+
radio : 方向键在组内移动(且自动选中)
|
|
12
|
+
全部 : Tab 进入、Shift+Tab 退出
|
|
13
|
+
一旦用 display:none 或 appearance 改坏,这些行为会静默消失,
|
|
14
|
+
而且测试时用鼠标完全测不出来 —— 只有键盘用户会知道。
|
|
15
|
+
|
|
16
|
+
本文件的三条铁律:
|
|
17
|
+
1. 绝不 display:none / visibility:hidden 隐藏原生控件
|
|
18
|
+
2. 不改原生控件的结构和 DOM 位置,只改外观
|
|
19
|
+
3. 焦点环必须画在**看得见的元素**上(input 被透明覆盖了,
|
|
20
|
+
它自己的 outline 你根本看不见)
|
|
21
|
+
========================================================================= */
|
|
22
|
+
|
|
23
|
+
/* ============================================================================
|
|
24
|
+
通用容器 —— checkbox / radio 共用
|
|
25
|
+
========================================================================= */
|
|
26
|
+
/* ============================================================================
|
|
27
|
+
结构(🔴 改版,见下方说明)
|
|
28
|
+
----------------------------------------------------------------------------
|
|
29
|
+
旧结构:input 是 label 的后代
|
|
30
|
+
<label class="choice">
|
|
31
|
+
<input class="choice__input"> ← 铺满整个 label
|
|
32
|
+
<span class="choice__mark"></span>
|
|
33
|
+
<span>文字</span>
|
|
34
|
+
</label>
|
|
35
|
+
|
|
36
|
+
新结构:input 与 label 平级,用 for 关联
|
|
37
|
+
<div class="choice">
|
|
38
|
+
<input class="choice__input" id="c1"> ← 铺满整个 .choice
|
|
39
|
+
<label class="choice__label" for="c1">
|
|
40
|
+
<span class="choice__mark"></span>
|
|
41
|
+
<span>文字</span>
|
|
42
|
+
</label>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
为什么改(实测反馈"点方框没有任何反应"):
|
|
46
|
+
input 铺满 label 且**是 label 的后代**时,点击行为存在双触发风险 ——
|
|
47
|
+
规范虽规定"点击目标是交互性后代时 label 不作为",但这依赖实现,
|
|
48
|
+
一旦 label 上的点击冒泡被处理成"也转发一次",状态就会被翻转两次
|
|
49
|
+
⇒ **看起来完全没反应**(观察到的正是这个)。
|
|
50
|
+
|
|
51
|
+
把 input 挪到 label **外面**,双触发在结构上就不可能发生。
|
|
52
|
+
代价只是多一个包裹元素,以及 label 内部要自己排版。
|
|
53
|
+
========================================================================= */
|
|
54
|
+
|
|
55
|
+
.choice {
|
|
56
|
+
position: relative;
|
|
57
|
+
display: flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
min-height: 44px; /* 触摸目标 44px:视觉方框只有 20px */
|
|
60
|
+
cursor: pointer;
|
|
61
|
+
user-select: none;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.choice__label {
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
gap: var(--sp-2);
|
|
68
|
+
cursor: pointer;
|
|
69
|
+
min-height: 44px; /* 文字本身也可点 */
|
|
70
|
+
flex: 1; min-width: 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.choice--radio .choice__label { border-radius: var(--r-sm); }
|
|
74
|
+
|
|
75
|
+
/* ---------------------------------------------------------------- 原生 input
|
|
76
|
+
🔴 核心手法:input 保持 opacity:0 但**保留在 tab 顺序里**,
|
|
77
|
+
绝对定位铺满整个 label,于是:
|
|
78
|
+
- 点击整个标签都能切换(不必精确点中方框)
|
|
79
|
+
- Space / 方向键行为 100% 原生
|
|
80
|
+
- 焦点环由我们画在 .choice__mark 上
|
|
81
|
+
|
|
82
|
+
为什么不用 appearance:none 直接美化 input:
|
|
83
|
+
那样方框和文字没法并排(input 只有一个盒子)。
|
|
84
|
+
为什么不用 display:none:
|
|
85
|
+
那会把它从 tab 顺序移除,键盘用户彻底到不了这里。
|
|
86
|
+
---------------------------------------------------------------- */
|
|
87
|
+
.choice__input {
|
|
88
|
+
position: absolute;
|
|
89
|
+
inset: 0;
|
|
90
|
+
width: 100%;
|
|
91
|
+
height: 100%;
|
|
92
|
+
margin: 0;
|
|
93
|
+
opacity: 0;
|
|
94
|
+
cursor: pointer;
|
|
95
|
+
/* 🔴 z-index 必须在 mark 之上。DOM 顺序是 input → mark,
|
|
96
|
+
两者同属 z-index:auto/0 那一层时 **后画的 mark 会盖住 input**,
|
|
97
|
+
点击命中 mark 再冒泡到 label,行为链变长、容易出意外。
|
|
98
|
+
显式抬高一层,点击就直接命中 input,没有中间环节。 */
|
|
99
|
+
z-index: 1;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* ---------------------------------------------------------------- 可见标记 */
|
|
103
|
+
.choice__mark {
|
|
104
|
+
position: relative;
|
|
105
|
+
flex: none;
|
|
106
|
+
display: grid;
|
|
107
|
+
place-items: center;
|
|
108
|
+
width: 20px;
|
|
109
|
+
height: 20px;
|
|
110
|
+
background: var(--surface);
|
|
111
|
+
border: 1px solid var(--border-control); /* 3.56 达标(UI 边界需 >= 3.0) */
|
|
112
|
+
transition: background-color var(--dur-fast) var(--ease-out),
|
|
113
|
+
border-color var(--dur-fast) var(--ease-out);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@media (hover: hover) and (pointer: fine) {
|
|
117
|
+
.choice:hover .choice__mark { border-color: var(--text-tertiary); }
|
|
118
|
+
}
|
|
119
|
+
/* 🔴 焦点环画在 mark 上,不是 input 上。
|
|
120
|
+
input 是 opacity:0 的,它自己的 outline 你永远看不见。
|
|
121
|
+
|
|
122
|
+
⚠️ 选择器为什么是 `~` 而不是 `+`:
|
|
123
|
+
改了结构 —— input 与 label 平级(for 关联),
|
|
124
|
+
于是 input 的**下一个兄弟是 label**,mark 藏在 label 里。
|
|
125
|
+
`+`(相邻兄弟)会失效,焦点环就画不出来了。
|
|
126
|
+
`~`(后续任意兄弟)+ 后代选择器才成立。 */
|
|
127
|
+
.choice__input:focus-visible ~ .choice__label .choice__mark {
|
|
128
|
+
outline: 2px solid var(--accent);
|
|
129
|
+
outline-offset: 2px;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* ---------------------------------------------------------------- 勾(对勾) */
|
|
133
|
+
.choice__mark::after {
|
|
134
|
+
content: "";
|
|
135
|
+
position: absolute;
|
|
136
|
+
width: 5px;
|
|
137
|
+
height: 9px;
|
|
138
|
+
/* 🔴 原为写死的 #ffffff。实测暗色下 accent 变亮(#7aa9de),
|
|
139
|
+
白勾压在上面只有 2.45:1,低于 WCAG 1.4.11 的 3:1 ⇒ 改用令牌。
|
|
140
|
+
浅色下 = #ffffff(8.38:1),暗色下 = #0d1114(7.73:1)。 */
|
|
141
|
+
border: solid var(--text-on-accent);
|
|
142
|
+
border-width: 0 2px 2px 0;
|
|
143
|
+
transform: rotate(45deg) scale(0);
|
|
144
|
+
transform-origin: center;
|
|
145
|
+
transition: transform var(--dur-fast) var(--ease-out);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* 🔴🔴 修一个致命 bug:勾选时**没有背景色**,
|
|
149
|
+
而勾号是 `border: 2px solid #fff`(白)⇒ **白勾画在白底上,对比度 1.0,完全看不见**。
|
|
150
|
+
实测反馈"全部都不可以",真实截图确认:勾选后是空心白框,勾号一点都看不到。
|
|
151
|
+
|
|
152
|
+
注释里那句「压在 accent 上 = 5.87 AA」说明**原设计就该有 accent 底**,
|
|
153
|
+
但这条规则从来没写出来。radio 有(下面),checkbox 漏了。
|
|
154
|
+
|
|
155
|
+
勾号是白色的 ⇒ **勾选时底色必须是深的**,这是硬约束,不是偏好。 */
|
|
156
|
+
.choice__input:checked ~ .choice__label .choice__mark {
|
|
157
|
+
background: var(--accent);
|
|
158
|
+
border-color: var(--accent);
|
|
159
|
+
}
|
|
160
|
+
.choice__input:checked ~ .choice__label .choice__mark::after {
|
|
161
|
+
transform: rotate(45deg) scale(1);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* ---------------------------------------------------------------- disabled */
|
|
165
|
+
.choice__input:disabled { cursor: not-allowed; }
|
|
166
|
+
|
|
167
|
+
.choice__input:disabled ~ .choice__label .choice__mark,
|
|
168
|
+
.choice__input:disabled ~ .choice__label {
|
|
169
|
+
/* 和 input 一样:不用 opacity,用明确的低对比色组 */
|
|
170
|
+
background: var(--surface-sunken);
|
|
171
|
+
border-color: var(--border-decor);
|
|
172
|
+
}
|
|
173
|
+
.choice__input:disabled ~ .choice__label { color: var(--text-tertiary); cursor: not-allowed; }
|
|
174
|
+
|
|
175
|
+
/* 🔴 disabled 之后 hover 不能有任何变化,否则看着像还能点。
|
|
176
|
+
不用 `:has()`(精简档 已定为禁用,见 form-validation 里的理由)——
|
|
177
|
+
第一版就是写了 `:has()`,属于自己违反自己刚定的纪律。 */
|
|
178
|
+
|
|
179
|
+
/* ---------------------------------------------------------------- 文本 */
|
|
180
|
+
.choice__label {
|
|
181
|
+
font-size: var(--fs-sm);
|
|
182
|
+
line-height: var(--lh-base);
|
|
183
|
+
color: var(--text-primary);
|
|
184
|
+
}
|
|
185
|
+
/* ---------------------------------------------------------------- 说明文字 */
|
|
186
|
+
.choice__hint {
|
|
187
|
+
font-size: var(--fs-xs);
|
|
188
|
+
color: var(--text-tertiary);
|
|
189
|
+
line-height: 1.5;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* ============================================================================
|
|
193
|
+
checkbox
|
|
194
|
+
========================================================================= */
|
|
195
|
+
.choice--checkbox .choice__mark { border-radius: 5px; }
|
|
196
|
+
|
|
197
|
+
/* indeterminate(第三态)—— "全选"那个既没全选也没全不选的状态。
|
|
198
|
+
🔴 不做这个态,"全选"复选框就没法正确表达中间状态。 */
|
|
199
|
+
.choice--checkbox .choice__input:indeterminate ~ .choice__label .choice__mark {
|
|
200
|
+
/* 🔴 由「深蓝满色块」改为「白底 + accent 描边 + accent 横线」。
|
|
201
|
+
评审意见满色块"感觉比较差" —— 确实,半选塞一整块深色视觉太重。
|
|
202
|
+
描边式同样能表达"部分选中",且更轻。 */
|
|
203
|
+
background: var(--surface);
|
|
204
|
+
border-color: var(--accent);
|
|
205
|
+
}
|
|
206
|
+
.choice--checkbox .choice__input:indeterminate ~ .choice__label .choice__mark::after {
|
|
207
|
+
/* 换成横线。🔴 颜色必须是 **accent** 不是白色 ——
|
|
208
|
+
底现在是白色(描边式),白线画在白底上同样看不见。 */
|
|
209
|
+
width: 10px;
|
|
210
|
+
height: 2px;
|
|
211
|
+
border: none;
|
|
212
|
+
background: var(--accent);
|
|
213
|
+
border-radius: 1px;
|
|
214
|
+
transform: none;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* ============================================================================
|
|
218
|
+
radio
|
|
219
|
+
========================================================================= */
|
|
220
|
+
.choice--radio .choice__mark {
|
|
221
|
+
border-radius: var(--r-pill);
|
|
222
|
+
}
|
|
223
|
+
/* radio 的"选中"是圆点,不是对勾 */
|
|
224
|
+
.choice--radio .choice__mark::after {
|
|
225
|
+
width: 8px;
|
|
226
|
+
height: 8px;
|
|
227
|
+
border: none;
|
|
228
|
+
/* 🔴 同上:radio 圆心也不能写死白。 */
|
|
229
|
+
background: var(--text-on-accent);
|
|
230
|
+
border-radius: var(--r-pill);
|
|
231
|
+
transform: scale(0);
|
|
232
|
+
}
|
|
233
|
+
.choice--radio .choice__input:checked ~ .choice__label .choice__mark {
|
|
234
|
+
background: var(--accent);
|
|
235
|
+
border-color: var(--accent);
|
|
236
|
+
}
|
|
237
|
+
.choice--radio .choice__input:checked ~ .choice__label .choice__mark::after {
|
|
238
|
+
transform: scale(1);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/* ============================================================================
|
|
242
|
+
radio 组 —— 必须用 fieldset + legend
|
|
243
|
+
=========================================================================
|
|
244
|
+
🔴 这是原生语义,屏幕阅读器会播报"第几组,共几项,当前选中了第几项"。
|
|
245
|
+
用 <div> + <span> 模拟就全丢了。
|
|
246
|
+
radio 之间的互斥、单选、方向键导航全部依赖同一个 name 属性。
|
|
247
|
+
------------------------------------------------------------------------ */
|
|
248
|
+
.choice__group {
|
|
249
|
+
border: none;
|
|
250
|
+
margin: 0;
|
|
251
|
+
padding: 0;
|
|
252
|
+
min-width: 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.choice__legend {
|
|
256
|
+
padding: 0;
|
|
257
|
+
margin-bottom: var(--sp-2);
|
|
258
|
+
font-size: var(--fs-sm);
|
|
259
|
+
font-weight: 500;
|
|
260
|
+
color: var(--text-primary);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.choice__options {
|
|
264
|
+
display: flex;
|
|
265
|
+
flex-direction: column;
|
|
266
|
+
gap: var(--sp-1);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/* ============================================================================
|
|
270
|
+
select
|
|
271
|
+
=========================================================================
|
|
272
|
+
🔴 选原生 select 就好,不要换成自定义下拉。
|
|
273
|
+
|
|
274
|
+
原生控件在移动端是最好的:系统会弹出带 wheel 的原生选择器、
|
|
275
|
+
语言切换跟随系统、无障碍由系统保证。换成 <div> 模拟要重新实现
|
|
276
|
+
上面全部功能,且必然漏一部分。
|
|
277
|
+
|
|
278
|
+
能做的只有外观,且必须保留 appearance 的替换能力
|
|
279
|
+
—— 只改外观,不改行为。
|
|
280
|
+
------------------------------------------------------------------------ */
|
|
281
|
+
.select {
|
|
282
|
+
position: relative;
|
|
283
|
+
display: inline-flex;
|
|
284
|
+
align-items: center;
|
|
285
|
+
width: 100%;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.select__control {
|
|
289
|
+
width: 100%;
|
|
290
|
+
height: 44px;
|
|
291
|
+
padding: 0 40px 0 var(--sp-3);
|
|
292
|
+
|
|
293
|
+
font-family: inherit;
|
|
294
|
+
/* 🔴 16px 硬下限:iOS Safari 小于此字号会自动放大整个页面 */
|
|
295
|
+
font-size: 16px;
|
|
296
|
+
line-height: 1.5;
|
|
297
|
+
color: var(--text-primary);
|
|
298
|
+
background: var(--surface);
|
|
299
|
+
|
|
300
|
+
border: 1px solid var(--border-control);
|
|
301
|
+
border-radius: var(--r-sm);
|
|
302
|
+
|
|
303
|
+
/* 去掉原生外观,但**键盘和移动端行为全部保留** */
|
|
304
|
+
appearance: none;
|
|
305
|
+
-webkit-appearance: none;
|
|
306
|
+
-moz-appearance: none;
|
|
307
|
+
|
|
308
|
+
cursor: pointer;
|
|
309
|
+
transition: border-color var(--dur-fast) var(--ease-out),
|
|
310
|
+
box-shadow var(--dur-fast) var(--ease-out);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/* Chrome 内部的下拉箭头要单独去掉 */
|
|
314
|
+
.select__control::-ms-expand { display: none; }
|
|
315
|
+
.select__control::-webkit-calendar-picker-indicator { opacity: 0; }
|
|
316
|
+
|
|
317
|
+
@media (hover: hover) and (pointer: fine) {
|
|
318
|
+
.select__control:hover:not(:disabled) { border-color: var(--text-tertiary); }
|
|
319
|
+
}
|
|
320
|
+
/* select 用 :focus 不用 :focus-visible —— 和 input 一样,
|
|
321
|
+
用户必须随时知道当前选的是哪个。 */
|
|
322
|
+
.select__control:focus {
|
|
323
|
+
outline: none;
|
|
324
|
+
border-color: var(--accent);
|
|
325
|
+
box-shadow: 0 0 0 3px var(--accent-soft);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.select__control:disabled {
|
|
329
|
+
background: var(--surface-sunken);
|
|
330
|
+
color: var(--text-tertiary);
|
|
331
|
+
border-color: var(--border-decor);
|
|
332
|
+
cursor: not-allowed;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/* 箭头:纯装饰,pointer-events:none 让点击穿透到 select */
|
|
336
|
+
.select__arrow {
|
|
337
|
+
position: absolute;
|
|
338
|
+
inset-inline-end: var(--sp-3);
|
|
339
|
+
pointer-events: none; /* 🔴 不写这句,箭头会吃掉点击 */
|
|
340
|
+
color: var(--text-tertiary);
|
|
341
|
+
display: grid;
|
|
342
|
+
place-items: center;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/* 多选/长内容时给 min-height,不用固定 height */
|
|
346
|
+
.select__control--auto { height: auto; min-height: 44px; padding-block: var(--sp-2); }
|
|
347
|
+
|
|
348
|
+
/* ============================================================================
|
|
349
|
+
reduced-motion
|
|
350
|
+
=========================================================================
|
|
351
|
+
勾的弹出属于"小幅缩放 + 旋转",不涉及位移。
|
|
352
|
+
🔴 但 200ms 以上的缩放动画在 reduced-motion 下应当去掉,只保留颜色变化 ——
|
|
353
|
+
颜色变化仍然传达了"勾上了"。
|
|
354
|
+
========================================================================= */
|
|
355
|
+
@media (prefers-reduced-motion: reduce) {
|
|
356
|
+
.choice__mark::after {
|
|
357
|
+
transition: none;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/* ============================================================================
|
|
362
|
+
反模式
|
|
363
|
+
=========================================================================
|
|
364
|
+
1. display:none / visibility:hidden 隐藏原生 input
|
|
365
|
+
从 tab 顺序移除。键盘用户彻底到不了这里。这一组最致命的错误。
|
|
366
|
+
|
|
367
|
+
2. 自己用 <div> 做 checkbox
|
|
368
|
+
Space 键、Tab 顺序、表单提交、屏幕阅读器全部要重写,且必然漏。
|
|
369
|
+
|
|
370
|
+
3. 焦点环画在 input 上
|
|
371
|
+
input 是 opacity:0 的,outline 你根本看不见。必须画在 .choice__mark 上。
|
|
372
|
+
|
|
373
|
+
4. 用 opacity 做 disabled
|
|
374
|
+
对比度变成不可控的随机数。跟 input 一样用明确的低对比色组。
|
|
375
|
+
|
|
376
|
+
5. radio 组用 <div> + <span> 模拟
|
|
377
|
+
丢掉 fieldset/legend 的原生语义:屏幕阅读器不再播报
|
|
378
|
+
"第几组、共几项、当前选中第几项"。
|
|
379
|
+
|
|
380
|
+
6. 箭头不写 pointer-events:none
|
|
381
|
+
吃掉点击,用户点箭头没反应。
|
|
382
|
+
|
|
383
|
+
7. 换掉原生 select 做自定义下拉
|
|
384
|
+
移动端体验、无障碍全部要重做。原生是最好的。
|
|
385
|
+
|
|
386
|
+
8. checkbox 不做 indeterminate 态
|
|
387
|
+
"全选"复选框无法表达中间状态。
|
|
388
|
+
|
|
389
|
+
9. 用 transition: all
|
|
390
|
+
|
|
391
|
+
10. 🔴 用 `:has()`(比如 `.choice:has(input:disabled)`)
|
|
392
|
+
库内另一处(form-validation)已定为精简档 禁用 `:has()` ——
|
|
393
|
+
它是 CSS Level 4,某项目 上 WebView 版本不确定。
|
|
394
|
+
写这一行时已经违反了自己刚定的纪律,已改用相邻兄弟选择器提优先级。
|
|
395
|
+
========================================================================= */
|