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.
- package/01-tokens/focus-ring-demo.html +1 -1
- package/01-tokens/page.css +32 -0
- package/01-tokens/responsive-demo.html +1 -1
- package/01-tokens/scrollbar-demo.html +1 -1
- package/02-primitives/badge/demo.html +1 -1
- package/02-primitives/button/demo.html +1 -1
- package/02-primitives/card/demo.html +1 -1
- package/02-primitives/choice/choice.css +31 -2
- package/02-primitives/choice/demo.html +1 -1
- package/02-primitives/combobox/demo.html +1 -1
- package/02-primitives/date-range/demo.html +1 -1
- package/02-primitives/delta/demo.html +1 -1
- package/02-primitives/input/demo.html +1 -1
- package/02-primitives/input/input.css +14 -0
- package/02-primitives/metric/demo.html +1 -1
- package/02-primitives/popover/demo.html +1 -1
- package/02-primitives/progress/demo.html +1 -1
- package/02-primitives/progress/progress.css +56 -0
- package/02-primitives/select/demo.html +1 -1
- package/02-primitives/separator/demo.html +1 -1
- package/02-primitives/skeleton/demo.html +1 -1
- package/02-primitives/skeleton/skeleton.css +18 -0
- package/02-primitives/switch/demo.html +1 -1
- package/02-primitives/switch/switch.css +61 -2
- package/02-primitives/timeline/README.md +70 -0
- package/02-primitives/timeline/demo.html +161 -0
- package/02-primitives/timeline/timeline.css +152 -0
- package/02-primitives/upload/README.md +142 -0
- package/02-primitives/upload/demo.html +318 -0
- package/02-primitives/upload/upload.css +490 -0
- package/02-primitives/upload/upload.js +676 -0
- package/03-patterns/accordion/demo.html +1 -1
- package/03-patterns/content/demo.html +1 -1
- package/03-patterns/drawer/demo.html +1 -1
- package/03-patterns/drawer/drawer.js +30 -5
- package/03-patterns/dropdown/demo.html +1 -1
- package/03-patterns/form-validation/demo.html +1 -1
- package/03-patterns/list/demo.html +1 -1
- package/03-patterns/nav/demo.html +1 -1
- package/03-patterns/overlay/demo.html +1 -1
- package/03-patterns/pagination/demo.html +1 -1
- package/03-patterns/states/demo.html +1 -1
- package/03-patterns/tabs/demo.html +1 -1
- package/03-patterns/tooltip/demo.html +1 -1
- package/03-patterns/tree/demo.html +1 -1
- package/04-recipes/app-shell/README.md +272 -0
- package/04-recipes/app-shell/app-shell.css +588 -0
- package/04-recipes/app-shell/demo.html +326 -0
- package/04-recipes/app-shell/shell-nav.js +288 -0
- package/04-recipes/table/table.css +22 -0
- package/09-assets/bar/demo.html +1 -1
- package/09-assets/echarts-adapter/demo.html +1 -1
- package/09-assets/model-viewer/demo.html +1 -1
- package/09-assets/sparkline/demo.html +1 -1
- package/AGENTS.md +8 -0
- package/API.md +4 -0
- package/CHANGELOG.md +795 -0
- package/CONTRIBUTING.md +22 -1
- package/README.md +17 -5
- package/README.zh-CN.md +16 -6
- package/START-HERE.md +12 -7
- package/ai/START-HERE.md +37 -3
- package/ai/behaviors.json +32 -0
- package/ai/cli.js +183 -25
- package/ai/components.json +185 -7
- package/ai/components.meta.json +13 -0
- package/ai/pitfalls-writing.json +119 -0
- package/ai/pitfalls.json +193 -0
- package/dist/01-tokens/page.css +1 -1
- package/dist/02-primitives/choice/choice.css +1 -1
- package/dist/02-primitives/input/input.css +1 -1
- package/dist/02-primitives/progress/progress.css +1 -1
- package/dist/02-primitives/skeleton/skeleton.css +1 -1
- package/dist/02-primitives/switch/switch.css +1 -1
- package/dist/02-primitives/timeline/timeline.css +1 -0
- package/dist/02-primitives/upload/upload.css +1 -0
- package/dist/02-primitives/upload/upload.js +515 -0
- package/dist/03-patterns/drawer/drawer.js +14 -4
- package/dist/04-recipes/app-shell/app-shell.css +1 -0
- package/dist/04-recipes/app-shell/shell-nav.js +173 -0
- package/dist/04-recipes/table/table.css +1 -1
- package/dist/COSTS.md +12 -10
- package/docs/ACCESSIBILITY.md +20 -7
- package/docs/AI-CONTRACT.md +2 -0
- package/docs/BUDGET.md +179 -0
- package/docs/CONVENTIONS.md +3 -0
- package/docs/STABILITY.md +126 -0
- package/esm/index.mjs +2 -0
- package/esm/upload.mjs +7 -0
- package/index.html +5 -2
- package/llms-full.txt +153 -0
- package/llms.txt +55 -0
- package/package.json +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,801 @@
|
|
|
16
16
|
|
|
17
17
|
(无)
|
|
18
18
|
|
|
19
|
+
## [0.24.1] - 2026-10-31
|
|
20
|
+
|
|
21
|
+
### `page.css` 补上 `<pre>` 的兜底(实测出来的,不是推演)
|
|
22
|
+
|
|
23
|
+
在 `.page` 环境里放一条 220 字符的长行,实测三种写法:
|
|
24
|
+
|
|
25
|
+
| 写法 | 结果 |
|
|
26
|
+
|---|---|
|
|
27
|
+
| 裸 `<pre>` | `doc=2048 / vw=375` ❌ 撑破 |
|
|
28
|
+
| `<pre class="code">`(没有 guide.css 时) | `doc=2211 / vw=375` ❌ 撑破 |
|
|
29
|
+
| `<div class="prose"><pre>`(没有 content.css 时) | `doc=2048 / vw=375` ❌ 撑破 |
|
|
30
|
+
|
|
31
|
+
三种都是 `overflow-x: visible` + `white-space: pre` ⇒ 长行顶到页面上,整页横向滚。
|
|
32
|
+
|
|
33
|
+
现有 84 个页面之所以没红,是因为每个 `<pre>` 恰好都另有来源
|
|
34
|
+
(`.prose > pre` 在 content.css、`pre.code` 在 guide.css)—— **那是巧合,不是契约**。
|
|
35
|
+
使用者照 `page.css` 写一页文档、贴一段长代码就会踩到。
|
|
36
|
+
|
|
37
|
+
修法只有两行,关键是选择器:
|
|
38
|
+
|
|
39
|
+
```css
|
|
40
|
+
:where(.page) pre { overflow-x: auto; }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`:where()` 把特异性压到 `0,0,1`(只算 `pre`),任何带类的规则都能盖掉它 ——
|
|
44
|
+
`.prose > pre` 与 `pre.code` 都是 `0,1,1`,稳赢。
|
|
45
|
+
写成 `.page pre`(同权重)则会变成"胜负取决于加载顺序"的随时会翻的脸。
|
|
46
|
+
|
|
47
|
+
只写 `overflow-x`,**不写** padding / background / font:那些是"代码块长什么样",
|
|
48
|
+
属于 content.css / guide.css 的决定;这里只管"它不许撑破页面"。
|
|
49
|
+
实测确认:`.prose > pre` 的 `padding 16.3px` / 灰底 / `3px` 竖条 / 12px mono 一点没变,
|
|
50
|
+
裸 `<pre>` 也没有因此获得任何"代码块外观"。
|
|
51
|
+
|
|
52
|
+
### 图标态的品牌补上悬停提示
|
|
53
|
+
|
|
54
|
+
rail 下品牌只剩一个 20px 图形,鼠标用户没有别的线索知道它是什么
|
|
55
|
+
⇒ `demo` 的 `.shell__brand` 加 `title="金标准 · 财务台"`。
|
|
56
|
+
|
|
57
|
+
⚠️ 它只负责悬停提示。名字仍然来自 `.shell__brand-text` —— `title` 会**兜出**名字,
|
|
58
|
+
但 `H2` 判的是"文字是否被 `display: none` 掉",不靠 `title` 兜,所以这条改动
|
|
59
|
+
不会掩盖"把 brand-text 改成 display:none"的破坏。
|
|
60
|
+
|
|
61
|
+
### 工程卫生:38 个页面写成了 `<body class="page page">`
|
|
62
|
+
|
|
63
|
+
`class` 里同一个类写了两遍。浏览器匹配一次,渲染无差异,但它是手误的化石,
|
|
64
|
+
而且会让人怀疑"是不是有两套 page 类"。
|
|
65
|
+
|
|
66
|
+
全库排查:**没有任何生成器产生它**(`gen-docs.py` 里是 `class="page"`),
|
|
67
|
+
也没有 CSS 选择器依赖双类(`.page.page` 零命中)、没有 JS 读 `className` 精确值
|
|
68
|
+
—— 是纯手写遗留。38 个文件各 1 处,已全部改回 `class="page"`。
|
|
69
|
+
|
|
70
|
+
### 版本同步
|
|
71
|
+
|
|
72
|
+
这一版三处改动都**不改渲染**(`overflow-x` 对已有页面本就是 `auto`;
|
|
73
|
+
`title` 不渲染;重复类名去掉后匹配结果不变),只改内容哈希。
|
|
74
|
+
所以两平台视觉基线重录后应当逐字节不变 —— 变的只有 `_stamp.json` 里的指纹。
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## [0.24.0] - 2026-10-31
|
|
79
|
+
|
|
80
|
+
### 图标态:把"文档说的"和"示例做的"合回一份
|
|
81
|
+
|
|
82
|
+
`app-shell` 的 `.shell--rail` 一直有两套说法:
|
|
83
|
+
|
|
84
|
+
- `README` 用整节说明「名字必须来自可见文字 `.shell__link-label`,**不要**用 `aria-label`」,
|
|
85
|
+
理由是名字只有一个来源,且抽屉里那份克隆离开 `.shell--rail` 作用域后能自动把文字补回来;
|
|
86
|
+
- 而 `demo.html` 的图标态实例**写的正是 `aria-label`**,`title` 也没有,可见文字压根没写。
|
|
87
|
+
|
|
88
|
+
门禁 46 条全绿,因为 `F12/F13/F14` 验的是内存里的 `railFixture()` ——
|
|
89
|
+
**fixture 是契约写法,demo 是相反写法**,判据只读前者。这不是某一条判据写错了,
|
|
90
|
+
而是判据落错了地方:使用者不会去读 fixture,他们照 demo 抄。
|
|
91
|
+
|
|
92
|
+
这一版补上三件事:
|
|
93
|
+
|
|
94
|
+
1. **demo 改用契约写法**:三个图标链接补上 `<span class="shell__link-label">`,
|
|
95
|
+
去掉 `aria-label`(`title` 保留,它只负责鼠标提示)。
|
|
96
|
+
2. **图标态的品牌分两段**(新槽位 `.shell__brand-mark` / `.shell__brand-text`)。
|
|
97
|
+
4.5rem 放不下品牌名,但"整个品牌 `display: none`"等于没有品牌;
|
|
98
|
+
两段结构下只留标记,品牌名与 `.shell__link-label` 同一套手法视觉隐藏,
|
|
99
|
+
仍在可访问树里,宽屏回来时字还在。标记用 `currentColor` 画 ——
|
|
100
|
+
强制色下会被换成 `CanvasText`,`<img>` 里的颜色则不受影响。
|
|
101
|
+
3. **图标态的分组不用 `.shell__side-label`**:72px 放不下「数据 / 校验」这种词,
|
|
102
|
+
折行会把图标挤散。改让两个 `.shell__side-group` 相邻,
|
|
103
|
+
`+` 那条规则本来就为"无标签分组"准备了分隔线与间距。
|
|
104
|
+
CSS 保留一条 `display: none` 兜底(防折行),但门禁判的是**demo 里不许出现**它 ——
|
|
105
|
+
兜底是给使用者的,不是给范本的。
|
|
106
|
+
|
|
107
|
+
### 新增判据 `H1/H2/H3`(落在 demo 的真实 DOM 上)
|
|
108
|
+
|
|
109
|
+
| 判据 | 判什么 | 改了会怎样 |
|
|
110
|
+
|---|---|---|
|
|
111
|
+
| `H1` | 每条 rail 链接都含 `.shell__link-label`,且**浏览器算出的可访问名**等于那段文字 | 删掉 label ⇒ 红 |
|
|
112
|
+
| `H2` | 带品牌的 rail 实例必须是「标记 + 文字」两段,文字 1×1 且 `display !== none` | 退回纯文本品牌 ⇒ 红 |
|
|
113
|
+
| `H3` | rail 实例里不许出现 `.shell__side-label`(比 CSS 的容错更严) | 塞回一个 ⇒ 红 |
|
|
114
|
+
|
|
115
|
+
三条都是"存在性断言",不需要反向控制;已逐条做过破坏验证(删 label / 拆品牌 / 塞标签),
|
|
116
|
+
每次都在跑完后从备份还原,并复跑确认回到全绿。
|
|
117
|
+
|
|
118
|
+
### 顺带:`perf-gate` 的两个洞(都在同一轮里被发现)
|
|
119
|
+
|
|
120
|
+
1. **单次测量把调度抖动当回归。** 夜里全量门禁报 `form-validation 首屏 303ms > 300`,
|
|
121
|
+
而同一台机器连测三次是 `121 / 125 / 124` —— 门槛 300、稳态 125,余量 2.4 倍,
|
|
122
|
+
却被一次抖动顶红。改成**首次超阈值时补测两次取中位数**;门槛与余量一个字没动,
|
|
123
|
+
真回归会让三次全超(已用 `MAX_DOM_MS=60` 验证:三次数值 `108/132/139`,中位数照样红)。
|
|
124
|
+
2. **`goto` 不会因 404 抛错。** 原来的写法是 `catch { continue; }` ——
|
|
125
|
+
于是"页面打不开"既不算通过也不算失败,在报告里直接消失(清单少一行,没人会数)。
|
|
126
|
+
一开始只补了 `catch` 分支,拿一条不存在的路径去破坏,门禁照样报"全部在基线内" ——
|
|
127
|
+
因为 **HTTP 状态码不是 JS 异常**,404 页也能读出 3ms 的 timing。
|
|
128
|
+
⇒ 补 `resp.status()` 判据(非 2xx/3xx 即失败),同一条破坏现在报
|
|
129
|
+
`❌ 打不开(HTTP 404)`。
|
|
130
|
+
|
|
131
|
+
### 顺带:重建的生成物与文档
|
|
132
|
+
|
|
133
|
+
- `START-HERE.md` 组件表、`10-review/a11y-tree/app-shell.txt` 朗读快照、
|
|
134
|
+
`size-baseline.json`、`dist/COSTS.md`:随本次改动重录(前两项是结构变了,
|
|
135
|
+
后两项是体积变了)。a11y 快照的 diff 只有一行:rail 里新增的品牌链接
|
|
136
|
+
`link "金标准 · 财务台"` —— 正好证明 `.shell__brand-text` 虽然 1×1,
|
|
137
|
+
名字仍在可访问树里。
|
|
138
|
+
- `README.zh-CN.md` / `docs/BUDGET.md` 里写死的「467.2 KB 里 318.4 KB」改成
|
|
139
|
+
「约七成是注释」,具体数指向 `dist/COSTS.md`(那份由 `dist-cost-gate.py`
|
|
140
|
+
每次现算并当场比对)。加一个组件就漂的数字,不该写在文档里。
|
|
141
|
+
- 视觉基线:Linux 三套仍停在 `3eb9923`(强制色修复之前),本轮在 CI 上重录;
|
|
142
|
+
Windows 三套随本次 HTML/CSS 改动一并重录。像素 diff 里 `switch.png` 的 3.7% 块差
|
|
143
|
+
就是那次强制色修复(滑块加了边框才显形)—— **旧基线 vs 已验收的修复**,不是新缺陷。
|
|
144
|
+
- `ai/pitfalls.json` 加 `fixture-demo-split-truth`:这个失败模式的通用形态。
|
|
145
|
+
|
|
146
|
+
## [0.23.0] - 2026-10-31
|
|
147
|
+
|
|
148
|
+
### 预算治理:换掉「谁能改这个数字」
|
|
149
|
+
|
|
150
|
+
体积上限原本是 `perf-gate.py` 里的一个手写常量 `MAX_ALL_CSS`,半年里被改过六次:
|
|
151
|
+
`120 → 124 → 125 → 132 → 139 → 142 → 143 KB`。每次都在注释里写一段「这次为什么合理」,
|
|
152
|
+
每次理由都成立。
|
|
153
|
+
|
|
154
|
+
**问题不在任何一次改动,而在结构**:被约束的人手里握着那个数字。
|
|
155
|
+
于是「加组件」和「改上限」永远是同一个动作的两步,而下一次必然重演;
|
|
156
|
+
更糟的是上限抬到「随手加什么都装得下」的那天,这道预算就再也不会失败 ——
|
|
157
|
+
它还在跑、还在打 `OK`,但已经不再约束任何东西。
|
|
158
|
+
|
|
159
|
+
换成一本账(`05-audit/budget.json`)+ 五层判据:
|
|
160
|
+
|
|
161
|
+
| 层 | 判据 | 拦的是什么 | 触发时 |
|
|
162
|
+
|---|---|---|---|
|
|
163
|
+
| L1 | 单文件 ≤ 24 KB | 一个文件变成怪物 | 拆文件 |
|
|
164
|
+
| L2 | 单组件 ≤ 10 KB | 一个组件变成子框架 | 拆组件 |
|
|
165
|
+
| L3 | 总量 ≤ 授予 + 储备 10 KB | 常规增长 | **不用管** |
|
|
166
|
+
| L4 | 总量 ≤ 天花板 172 KB | 产品承诺 | 删,或走 ADR |
|
|
167
|
+
| L5 | 总量 ≥ 授予 − 死区 2 KB | 降下来没人收线 | `--ratchet` |
|
|
168
|
+
|
|
169
|
+
L3 与 L5 是一对:**L3 让正常更新免费,L5 让免费额度不会自己长大**。
|
|
170
|
+
只有两条同时在场,「不用动数字」才不等于「没有预算」。
|
|
171
|
+
|
|
172
|
+
**172 KB 是夹出来的,不是拍的**:路线图还剩 8~12 个候选组件,实测每个组件生效 CSS
|
|
173
|
+
中位数 2.4 KB(`delta` 1.5 / `metric` 1.5 / `app-shell` 2.4 / `shell-nav` 0.9 / `upload` 4.2)
|
|
174
|
+
⇒ 172 − 146 = 26 KB ≈ 11 个中等组件 —— **这个数就是按「把剩下的做完也不用再开会」定的**。
|
|
175
|
+
低于 160 会撞线(退回老毛病),高于 172 则对应 >28 KB gzip,与「小而全、可整包引入」的定位冲突。
|
|
176
|
+
|
|
177
|
+
`MAX_ALL_CSS` 与它那 20 行「为什么又调大了」的注释一起删除。
|
|
178
|
+
规则写在 `docs/BUDGET.md`(含 ADR 表格、三种动作、以及「不允许的理由」清单)。
|
|
179
|
+
|
|
180
|
+
### 判据自己的判据(这一版最容易被忽略的一节)
|
|
181
|
+
|
|
182
|
+
五个分支里,**日常只会触发一个**。天花板永远不会红(除非已经晚了),
|
|
183
|
+
储备不会红(除非用光),棘轮只在删代码后才该红 —— 于是「写反不等号」可以绿着跑一百次。
|
|
184
|
+
|
|
185
|
+
- `perf-gate.py --selftest`:8 条合成数字验分支**顺序**(越过天花板不能被「储备用尽」抢先),
|
|
186
|
+
外加一条字节码接线检查(有人把比较内联回 `main()` 时它红)。
|
|
187
|
+
- `budget-selftest.py`(15 条,新门禁):用 `FL_BUDGET` 指向**临时账本**,
|
|
188
|
+
端到端跑真实的 `perf-gate.py` —— 退出码、失败文案、`--grant` / `--ratchet` 写盘、
|
|
189
|
+
账本缺失、以及「跑完真账本 sha 未变」。
|
|
190
|
+
- 变异探针:把五个分支逐一改成恒假、让 `main()` 绕过 `verdict`、去掉授予次数上限
|
|
191
|
+
⇒ **5/5 全部被 `budget-selftest.py` 判红**,每次源码逐字节还原(sha 核对)。
|
|
192
|
+
|
|
193
|
+
### 冻结声明:`docs/STABILITY.md`
|
|
194
|
+
|
|
195
|
+
「别动了」有两种翻译,后果相反:不再修(下游一年后不敢升级)/不再**破坏**(修复继续到货)。
|
|
196
|
+
这份文件选第二种,并把「冻结」写成一组**可被机器检查**的承诺:令牌名、类名、JS 成员、
|
|
197
|
+
CLI 子命令与 `--json` 形状、契约字段、文件路径 —— 每条都指着一个正在跑的门禁;
|
|
198
|
+
再写清**没冻的**(选择器写法、注释、demo、文档、内部实现)、版本语义(迁移窗口 ≥ 一个次版本)、
|
|
199
|
+
解冻的三个条件,以及**覆盖不到的地方**(事件名没有运行时探针、`data-*` 只是人写的承诺、
|
|
200
|
+
视觉基线只覆盖我们自己拍的页面)。
|
|
201
|
+
|
|
202
|
+
### timeline(第 40 个组件)
|
|
203
|
+
|
|
204
|
+
被旧预算挡住的那一个:草稿 681 B,而旧模型只剩 419 B 余量。新模型下它只吃掉
|
|
205
|
+
储备 1.8 KB,**没有碰任何数字** —— 这正是新预算存在的意义。
|
|
206
|
+
|
|
207
|
+
几何契约(三条线必须交于一点):**圆心 / 连接线中心 / 首行行盒中心**。
|
|
208
|
+
三者由同一组量派生(`--tl-dot` / `--tl-axis`),改圆点直径时它们一起走。
|
|
209
|
+
连接线从**自己的圆心**画到下一个的圆心(不是 `inset-block: 0` —— 那样第一条上方
|
|
210
|
+
会多出一小截悬空的线,看起来像「上面还有事件没加载」)。
|
|
211
|
+
「最新一条」用**尺寸 + 字重**两条非颜色线索,不用颜色。
|
|
212
|
+
|
|
213
|
+
`timeline-check.js` 13 条(A 语义 / B 几何 / C 反向 + D 强制色),含三条反向控制:
|
|
214
|
+
只把线挪 2px ⇒ 线心与圆心分离;去掉 `:not(:last-child)` ⇒ 末条长出线;
|
|
215
|
+
时间行高改 1.1 ⇒ 圆心不再对首行中心。另有一条**正向**派生检查:
|
|
216
|
+
改 `--tl-axis` 后圆点与线必须一起平移(证明它们同源,不是各写一遍)。
|
|
217
|
+
|
|
218
|
+
组件故意不给状态配色(已披露 / 待披露交给 `badge`):颜色在强制色、色盲、打印
|
|
219
|
+
三种情况下会同时失效,而状态文字不会。
|
|
220
|
+
|
|
221
|
+
### 回归
|
|
222
|
+
|
|
223
|
+
- 全量门禁 **220 道**(218 + 新增 `budget-self` 与 `timeline` 两道)
|
|
224
|
+
- 组件 40(stable 40)· demo 页 43 · 契约 40
|
|
225
|
+
- 体积 **144.45 / 172 KB**(储备用了 1.8 / 10 KB;单文件最大仍是 `tokens.css` 14.5 KB)
|
|
226
|
+
- `timeline.css` 生效 1803 B(原始 7286 B,75% 是注释)
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## [0.22.1] - 2026-10-31
|
|
231
|
+
|
|
232
|
+
### 强制色第 7 类缺陷:侧栏「当前项」变成五项全带竖线
|
|
233
|
+
|
|
234
|
+
0.20.0 给 `app-shell` 的强制色块写的是:
|
|
235
|
+
|
|
236
|
+
```css
|
|
237
|
+
.shell__link { border-inline-start: 3px solid transparent; } /* 以为"免费预留" */
|
|
238
|
+
.shell__link[aria-current="page"] { border-inline-start-color: CanvasText; }
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
实拍(CDP + `forced-colors: active`)读到的不是这样:**五项全都长出 3px 左竖线**。
|
|
242
|
+
原因是强制色会把透明边框解析成实线 —— 链接上是 `LinkText`(实测 `rgb(0,0,159)`),
|
|
243
|
+
当前项是 `CanvasText`(`rgb(0,0,0)`)。等于当前项与其它项**只差一个色相**,
|
|
244
|
+
正是本库在别处一直反对的"只靠颜色"。
|
|
245
|
+
|
|
246
|
+
修法:不预留边框(零位移),当前项改用两条**非颜色**线索 —— 字重 700 +
|
|
247
|
+
行首一个系统色圆点(`content: ""` + `background: CanvasText`,绝对定位不参与布局)。
|
|
248
|
+
|
|
249
|
+
⚠️ 第一版圆点写成 `content: "•"`,实测**可访问名被污染**成「圆点 总览」
|
|
250
|
+
(CSS 生成内容会计入链接名)。改成空内容 + 系统色背景后,名字回到「总览」。
|
|
251
|
+
`Select` 层用系统色是安全的:进度条的 `Highlight` 就是这么活的,
|
|
252
|
+
真正会被替换掉的是**作者色**,不是系统色关键字。
|
|
253
|
+
|
|
254
|
+
### 门禁
|
|
255
|
+
|
|
256
|
+
`forced-colors-check.js` 新增 7 条(28 → 35):
|
|
257
|
+
|
|
258
|
+
- **A7** 只有当前项带非颜色标记(字重 ≥700 + 圆点画出且非透明),其它项两者皆无
|
|
259
|
+
- **A7b** 所有项左竖线为 0(守住"透明边框预留"这条回归路径)
|
|
260
|
+
- **A7c** 圆点不进可访问名(问浏览器要 `accessibility.snapshot` 的名字,不数 DOM)
|
|
261
|
+
- **A8** 拖放区 `dragover` 与 idle 的差别不止颜色:强制色下两者边框色**完全相同**
|
|
262
|
+
(都是 CanvasText),只有线型 dashed → solid 在替读者回答"现在能松手吗"
|
|
263
|
+
- **C7 / C7b / C8** 反向:剥掉 media 块 ⇒ 圆点消失;把**旧写法注回去** ⇒ 三项全都
|
|
264
|
+
长出 3px 竖线;剥掉 `border-style: solid` ⇒ 拖放区两态重新同线型
|
|
265
|
+
|
|
266
|
+
这几条与 0.17.0 的 A1–A6 不同:那些判据问"图形还在不在",
|
|
267
|
+
A7 问的是**两个状态之间的差别还在不在** —— 只测"当前项有标记"会全绿,
|
|
268
|
+
因为五项都有。
|
|
269
|
+
|
|
270
|
+
### 体积
|
|
271
|
+
|
|
272
|
+
`app-shell.css` +174 B(剥注释口径),全库 **142.7 / 143 KB**,没有抬上限。
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## [0.22.0] - 2026-10-31
|
|
277
|
+
|
|
278
|
+
### 翻车点按「读者是谁」拆成两份文件
|
|
279
|
+
|
|
280
|
+
0.21.0 的 `ai/pitfalls.json` 混装了三类东西:组件缺陷、版式问题、交付过程。
|
|
281
|
+
在只收 UI 相关条目时这是对的,但它漏掉的恰好是**写出来给人看的那一类**
|
|
282
|
+
(演示要证明什么、引证要不要回源、页面进公众号会不会被剥成空白)。
|
|
283
|
+
|
|
284
|
+
新增 `ai/pitfalls-writing.json`,12 条,`scope` 恒为 `writing`、`components` 恒为空:
|
|
285
|
+
|
|
286
|
+
| 文件 | scope | 挂组件 | 谁会被它绊到 |
|
|
287
|
+
|---|---|---|---|
|
|
288
|
+
| `pitfalls.json` | component / layout / process | 是(`components`) | 写页面的人 |
|
|
289
|
+
| `pitfalls-writing.json` | writing | 否 | 写文案、做演示、发页面的人 |
|
|
290
|
+
|
|
291
|
+
**为什么不合成一份**:`agentcss ai components --json` 会把每条坑挂到组件上。
|
|
292
|
+
写作类没有组件可挂,混进去之后 `pitfalls: []` 就不再等于「这个组件没有坑」——
|
|
293
|
+
它还得表达「这条根本不属于组件」。字段一旦要同时表达两件事,读的人就分不清了。
|
|
294
|
+
|
|
295
|
+
CLI 侧:
|
|
296
|
+
|
|
297
|
+
- `ai pitfalls` 合并两份,JSON 多一个 `files` 字段(**覆盖面**:只加载了一份时,
|
|
298
|
+
`count` 小不代表这类没有坑)
|
|
299
|
+
- `--scope=writing` 直接可用;`components --json` 仍然只挂核心文件里的条目
|
|
300
|
+
- 读不到某一份时往 stderr 写 `[WARN] … 本次未提供`,**不静默跳过** ——
|
|
301
|
+
静默会让 `--scope=writing` 返回 0 条,而 0 条读起来是「这类没有翻车点」
|
|
302
|
+
|
|
303
|
+
### MCP:把「不做」写成可复查的决定,而不是一句口头结论
|
|
304
|
+
|
|
305
|
+
`ai/START-HERE.md` 新增 §7:三条理由(只读查询 / 第二份真值 / 多一个要信任的常驻进程)、
|
|
306
|
+
**什么条件下该重新考虑**(跑不了子进程、需要跨调用状态),以及真要做时的形状
|
|
307
|
+
(给 `cli.js` 套 stdio 适配,不在 server 里重新实现筛选 —— 那些判据在 CLI 里有门禁守)。
|
|
308
|
+
|
|
309
|
+
`llms.txt` 加一行"No MCP server",免得 agent 去找一个不存在的东西。
|
|
310
|
+
|
|
311
|
+
### 门禁
|
|
312
|
+
|
|
313
|
+
`ai-layer-gate.js` 新增 ⑦ 系列 7 条(27 → 34):
|
|
314
|
+
|
|
315
|
+
- 两份文件字段齐全、写作类不挂组件、跨文件 id 不重复
|
|
316
|
+
- CLI 合并条数 = 两份之和;`files` 里两份都在(少一份 = 静默丢了一半)
|
|
317
|
+
- `--scope=writing` 只返回写作类
|
|
318
|
+
- `components --json` 里挂着的 id 必须全部来自核心文件(写作类越界会点名)
|
|
319
|
+
|
|
320
|
+
反向控制:给写作条目挂一个 `components: ['table']` 并把两条 id 改成一样
|
|
321
|
+
⇒ ⑦′ / ⑦″ / ⑦⁶ 三条精确变红(34 → 31);还原后回到 34/0。
|
|
322
|
+
另做一次人工反向控制:把 `pitfalls-writing.json` 移走 ⇒ stderr 出现
|
|
323
|
+
`[WARN] 读不到 ai/pitfalls-writing.json`,JSON 里 `files` 只剩一份、`writing count=0`。
|
|
324
|
+
|
|
325
|
+
`llms-full.txt` 由生成器合并两份(`Writing / publishing (12)` 一节),
|
|
326
|
+
仍是逐字节受 `docs-site-gate [11]` 守。
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## [0.21.0] - 2026-10-31
|
|
331
|
+
|
|
332
|
+
### 发现层:让外面的 AI 找得到这个库
|
|
333
|
+
|
|
334
|
+
差异化的卖点是"机器可读契约",但在此之前**没有任何自动发现入口**:
|
|
335
|
+
没有 `llms.txt`、没有 `AGENTS.md`,`ai/components.json` 是一份密封的文件 ——
|
|
336
|
+
不知道它存在的人不会去读,知道的模型也只能靠猜类名。
|
|
337
|
+
|
|
338
|
+
新增三份文件,各管一件事:
|
|
339
|
+
|
|
340
|
+
| 文件 | 谁写 | 作用 |
|
|
341
|
+
|---|---|---|
|
|
342
|
+
| `llms.txt`(2.2 KB / ≈550 token) | 手写 | 定位声明 + `npx agentcss ai …` 用法 + 五条硬规矩 |
|
|
343
|
+
| `llms-full.txt`(10.2 KB / ≈2.5k token) | **生成**(`05-audit/gen-llms.py`) | 一页看全 39 个组件(根类名 / tier / 成熟度 / 体积 / 用途 / 翻车点)+ 令牌 + 行为核 |
|
|
344
|
+
| `AGENTS.md`(7 行) | 手写 | 契约在哪、怎么查、怎么验 |
|
|
345
|
+
|
|
346
|
+
**为什么不把 52 KB 的 `components.json` 塞进 `llms-full.txt`**:
|
|
347
|
+
约 13k token,比整份文档大五倍,而其中 slots / variants / aria 这些字段
|
|
348
|
+
在"选组件"阶段用不上。`llms-full.txt` 只内联**决策需要**的字段,
|
|
349
|
+
其余用一条命令代替:`npx agentcss ai components --json`。
|
|
350
|
+
|
|
351
|
+
三份文件都进 `package.json` 的 `files`(`npm pack --dry-run` 可验)。
|
|
352
|
+
|
|
353
|
+
### CLI:`--json` 覆盖全部 11 个子命令
|
|
354
|
+
|
|
355
|
+
审计起点是"只有 components / tokens 支持 `--json`",实测**7/10 支持**,
|
|
356
|
+
缺的是 `validate`(完全没有)与 `patch`(只在没写 `-o` 时裸打文档)。
|
|
357
|
+
`diff` 有,但四个命令各写一份 `flag('json') === true` 判定 —— 形状不统一。
|
|
358
|
+
|
|
359
|
+
现在:
|
|
360
|
+
|
|
361
|
+
- 11 个子命令(含新增 `pitfalls`)全部接受 `--json`
|
|
362
|
+
- 判定收敛到一个 `isJson()`(`--json=false` 不再被当成真)
|
|
363
|
+
- **失败也是 JSON**:`{ok:false, command, error:{kind,message}}` + 非 0 退出
|
|
364
|
+
(旧版失败只有 stderr 一行中文,调用方拿到的是"空 stdout + 退出码 1")
|
|
365
|
+
- `patch --json` 恒输出 `{ok, applied, deterministic, out, doc}`
|
|
366
|
+
|
|
367
|
+
判据在 `ai-layer-gate.js` ⑥′/⑥″:逐个子命令跑一遍,`JSON.parse(stdout)`;
|
|
368
|
+
再把参数改错,要求输出 `ok:false`。反向控制:把 `isJson()` 改成恒 `false`,
|
|
369
|
+
两条必红(实测 27 → 25)。
|
|
370
|
+
|
|
371
|
+
### 已知翻车点:`ai/pitfalls.json` + 挂到组件上
|
|
372
|
+
|
|
373
|
+
从内部教训库(41 条)整理出 **19 条**可复用的失效模式,按 `scope` 分三类
|
|
374
|
+
(component / layout / process),能对上组件的写进 `components`。
|
|
375
|
+
|
|
376
|
+
- `npx agentcss ai pitfalls --json --component=table`
|
|
377
|
+
- `npx agentcss ai components --json` 里每个组件多一个 `pitfalls: [{id,title}]`(**空数组也写**,形状恒定)
|
|
378
|
+
- 组件名拼错会**吵**:列出全部可用 id,而不是静默返回 0 条
|
|
379
|
+
|
|
380
|
+
⚠️ 教训库原文含内部项目名与日期,**不直接搬运**:入库版本逐条改写,
|
|
381
|
+
只保留失效模式与改法。原文 41 条不是 134 条 —— 以实际文件为准。
|
|
382
|
+
|
|
383
|
+
### 门禁
|
|
384
|
+
|
|
385
|
+
- `docs-site-gate` 新增 [11]:`llms-full.txt` 必须与现算逐字节一致;
|
|
386
|
+
`llms.txt` 声明的组件数必须等于契约(实测把它改成 38 ⇒ 红);
|
|
387
|
+
`AGENTS.md` ≤10 行且必须指向 `ai/components.json`。
|
|
388
|
+
- 门禁总数不变(218):新判据挂在既有两道门上,没有新增脚本。
|
|
389
|
+
|
|
390
|
+
### 语言
|
|
391
|
+
|
|
392
|
+
`llms.txt` / `AGENTS.md` 用**英文**,与 `README.md` 一致(英文在门口、中文在深处);
|
|
393
|
+
JSON 的键与 CLI 开关本来就是英文。组件文档仍是中文,且在 `llms.txt` 里写明。
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## [0.20.0] - 2026-10-31
|
|
398
|
+
|
|
399
|
+
### `.shell--rail` 接抽屉:图标在桌面,文字在抽屉
|
|
400
|
+
|
|
401
|
+
0.19.0 把 `shell-nav.js` 接上了,但图标态(`.shell--rail`)没验过。
|
|
402
|
+
一验就发现:**抽屉里只剩一排图标**。原因不在脚本 ——
|
|
403
|
+
脚本克隆的是侧栏节点,而侧栏里**本来就没有文字**(第 4 节的示范是 `aria-label` + 纯 SVG)。
|
|
404
|
+
|
|
405
|
+
真正的修法不是"在克隆时补文字"(那要第二份标记,或者让 JS 认识你的数据结构),
|
|
406
|
+
而是**文字从一开始就留在 DOM 里,只做视觉隐藏**:
|
|
407
|
+
|
|
408
|
+
```html
|
|
409
|
+
<a class="shell__link" href="#" title="总览">
|
|
410
|
+
<svg … aria-hidden="true">…</svg>
|
|
411
|
+
<span class="shell__link-label">总览</span>
|
|
412
|
+
</a>
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
抽屉把克隆挂到 `<body>` 下 ⇒ 克隆**离开了 `.shell--rail` 的作用域**
|
|
416
|
+
⇒ 隐藏规则不再命中 ⇒ 抽屉里自动变成「图标 + 文字」。没有第二份标记,也没有 JS 分支。
|
|
417
|
+
|
|
418
|
+
| 写法 | rail 里 | 可访问名 | 抽屉里 |
|
|
419
|
+
|---|---|---|---|
|
|
420
|
+
| 删掉文字 + `aria-label` | 图标 | 有(第二份真值) | **只有图标** |
|
|
421
|
+
| `display: none` | 图标 | **没有**("链接、链接、链接") | 有字(克隆不受影响,但 rail 已经废了) |
|
|
422
|
+
| `.shell__link-label` + 视觉隐藏 | 图标 | 有(来自可见文字) | **图标 + 文字** |
|
|
423
|
+
|
|
424
|
+
⚠️ `title` 只是鼠标提示。它还会**兜出**一个可访问名 —— 所以门禁 fixture 里故意不写
|
|
425
|
+
`title`,否则"把标签改成 `display:none`"这种破坏会被它盖住,判据看着还是绿的。
|
|
426
|
+
|
|
427
|
+
### 门禁 `app-shell-check.js` +5 条(F12–F14 / G0d / G4)
|
|
428
|
+
|
|
429
|
+
```
|
|
430
|
+
F12 rail:文字 1×1、不是 display:none,可访问名仍是「总览」
|
|
431
|
+
F13 rail:图标仍在 44px 命中区里居中(文字没参与布局)
|
|
432
|
+
F14 抽屉:克隆离开 .shell--rail 后文字自动回来(链接宽 312、标签 28×24)
|
|
433
|
+
G4 反向:把视觉隐藏改成 display:none ⇒ 字还在 DOM 里,可访问名变成空
|
|
434
|
+
G0d 前提:G4 的替换真的改到了文件
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
可访问名不是数 DOM 里的字,是问浏览器:`Accessibility.getPartialAXTree` 返回的
|
|
438
|
+
`name.value`。**`display:none` 的文字照样有 `textContent`** —— G4 证的正是这个差别,
|
|
439
|
+
也是"为什么不许图省事换成 `display:none`"的唯一硬证据。
|
|
440
|
+
|
|
441
|
+
### 仓库卫生:私有代号清单移出公开树
|
|
442
|
+
|
|
443
|
+
`05-audit` 里有两处写着内部项目代号:`outsider-audit.py` 是**明文**正则,
|
|
444
|
+
`publish-guard.py` 是字符串拼接。拼接只骗机器 —— 人读一眼就能拼回来,
|
|
445
|
+
而这两个文件随公开仓库发给每个人。同一类问题还有 `terms.USER_RE`:
|
|
446
|
+
它把本机用户名的前三字符写死在源码里。
|
|
447
|
+
|
|
448
|
+
现在:
|
|
449
|
+
|
|
450
|
+
- 词表统一走 `terms`(原来 outsider-audit 自己抄了一份,与 leak-scan 口径不同)
|
|
451
|
+
- 真实代号与用户名模式搬进 `internal/private-terms.txt`
|
|
452
|
+
(`.gitignore` 里,也不在 npm `files` 里 ⇒ 不进包)
|
|
453
|
+
- 读不到时**不假装扫过**:`terms.private_status()` 由三道扫描器打印
|
|
454
|
+
「私有词表未加载 ⇒ 本次未覆盖」
|
|
455
|
+
- 公开源码只留一个假代号给自检夹具(`terms.DEMO_TERMS`),不指向任何真实项目
|
|
456
|
+
- **工具目录不再整目录豁免**:`05-audit/` 以前被两道扫描一起跳过
|
|
457
|
+
("工具必须含它检测的词"),代号正好躺在那个盲区里。现在按类别豁免 ——
|
|
458
|
+
通用词(场景 / 流程 / 协作称谓)放过,**身份类照抓**
|
|
459
|
+
(真实代号 / 用户名 / 本机绝对路径,见 `terms.identity_hits`)
|
|
460
|
+
- 顺带清掉工具注释里的 5 处本机绝对路径(盘符、临时目录)
|
|
461
|
+
|
|
462
|
+
**改动前后的同一份远端快照**(`19e8623`,那时还没推):
|
|
463
|
+
|
|
464
|
+
| | 结论 |
|
|
465
|
+
|---|---|
|
|
466
|
+
| 改之前 | ✅ 零泄漏 |
|
|
467
|
+
| 改之后 | 🔴 内部代号 5 处 / 2 个文件 · 绝对路径 5 处 / 3 个文件 |
|
|
468
|
+
|
|
469
|
+
差的这 10 处全在 `05-audit/` 里 —— 也就是说,**旧的"零泄漏"是靠不看那里得到的**。
|
|
470
|
+
本地注入反向验证:往仓库里放一个真实代号 ⇒ 报「内部代号 1 处」,删掉即清零;
|
|
471
|
+
夹具 12 / 12(新增两条:工具目录里的代号必须抓到、工具目录里的场景词不许误报)。
|
|
472
|
+
|
|
473
|
+
⚠️ 历史提交里仍有旧版本。清历史要重写全部 SHA 并强推,会让已发布版本的
|
|
474
|
+
provenance 指向不存在的提交 —— 这一步留给发布者决定,本版**没有**自动执行。
|
|
475
|
+
|
|
476
|
+
### 回归
|
|
477
|
+
|
|
478
|
+
- `app-shell-check` 46 条全绿(本次 +5)
|
|
479
|
+
- axe 0 违规(34 条规则通过,含新一节的 3 个链接)- 可访问树快照:新增 1 个标题 + 3 个**有名字**的链接("总览 / 实体 / 勾稽校验")
|
|
480
|
+
- 视觉基线 45 / 46 页像素一致;`app-shell` 页高度 5027 → 5853(多了一节,有意)
|
|
481
|
+
- CSS 142.5 / 143 KB(本次只加了隐藏规则,**没有**再抬预算)
|
|
482
|
+
|
|
483
|
+
### 文档
|
|
484
|
+
|
|
485
|
+
- `app-shell` 的「图标态」从一句"必须有 aria-label 或 title"改成可执行的
|
|
486
|
+
`.shell__link-label` 契约,并写清为什么不能用 `aria-label` 顶替。
|
|
487
|
+
- `README.zh-CN.md` 的注释占比 67% → 68%(459.0 KB 原始里 312.1 KB)——
|
|
488
|
+
源码又长了一轮,数字跟着实测走。
|
|
489
|
+
- `ai/START-HERE.md` **保持中文**,顶部加了一段英文指引(英文读者走 `README.md`,
|
|
490
|
+
契约走语言中立的 `ai/components.json`)。理由与边界写进 `CONTRIBUTING.md`
|
|
491
|
+
的「语言」一节:它指向的文档全是中文,单翻这一页只会把人送到中文墙前。
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
## [0.19.0] - 2026-10-31
|
|
496
|
+
|
|
497
|
+
### 新文件 · `04-recipes/app-shell/shell-nav.js` —— 补上 0.18.0 留下的那个缺口
|
|
498
|
+
|
|
499
|
+
0.18.0 的 app-shell 文档写着"要抽屉就把 `.shell__side` 搬进 drawer 实例"。
|
|
500
|
+
那句话把一个六十行的活儿丢给了使用者:建按钮、克隆列表、断点判断、宽屏复位、
|
|
501
|
+
缺依赖时别装死。每个项目抄一遍,抄出来的版本各有各的漏 ——
|
|
502
|
+
这正是当年 `nav.js` 被做出来的同一个理由,所以这次做成一层。
|
|
503
|
+
|
|
504
|
+
| | 做法 | 为什么 |
|
|
505
|
+
|---|---|---|
|
|
506
|
+
| 复用谁 | `03-patterns/drawer` | Esc / 焦点陷阱 / 焦点归位 / 背景 `inert` / 锁滚动五件事全在它那儿 |
|
|
507
|
+
| 不复用谁 | `03-patterns/nav` | 它的列表**常驻在文档里**(桌面就是那条横排导航),抽屉与列表是同一节点;而这里的侧栏在窄屏是 `display: none`,需要"另建一份"。生命周期相反(常驻 vs 即用即毁) |
|
|
508
|
+
| 断点 | 不写在 JS 里 | 判据是"菜单按钮还在不在"(`offsetParent === null`)。写死 47.99rem 的话,CSS 改一个值 JS 就静默错位 |
|
|
509
|
+
| 缺依赖 | 拒绝初始化 + warn | 宁可没有按钮,也不要一个点了没反应的按钮 —— 窄屏下用户会反复点,然后认为整页坏了 |
|
|
510
|
+
|
|
511
|
+
### 修 `drawer.js` 一个"模态关不掉"的真缺陷
|
|
512
|
+
|
|
513
|
+
```js
|
|
514
|
+
if (t) t.focus(); else (f[0] || d).focus(); // 旧
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
"找到节点"被当成了"聚焦成功"。可节点存在 ≠ 能聚焦:`display: none`、`disabled`、
|
|
518
|
+
`inert` 都会让 `focus()` **静默无效**。
|
|
519
|
+
|
|
520
|
+
后果不是"焦点位置不理想",是**关不掉**:焦点留在 `<body>` 上,
|
|
521
|
+
而 Esc 的监听挂在抽屉元素上 —— 事件冒泡不过去。背景此时已 `inert`,
|
|
522
|
+
Tab 也无处可去。**鼠标用户点遮罩能关,所以这个 bug 只在键盘下现形。**
|
|
523
|
+
|
|
524
|
+
现在判据改成"焦点**真的到了**"(比对 `document.activeElement`),没到就兜底;
|
|
525
|
+
兜底时也跳过隐藏项。这个缺陷是写 shell-nav 时撞出来的 ——
|
|
526
|
+
克隆来的侧栏继承了窄屏的 `display: none`,于是 `initialFocus` 命中一个看不见的链接。
|
|
527
|
+
|
|
528
|
+
### 同一个根因的第二个面:克隆会继承"隐藏"那条规则
|
|
529
|
+
|
|
530
|
+
`.shell__side` 在 ≤47.99rem 是 `display: none`,克隆出来的节点带着同一个类,
|
|
531
|
+
于是在抽屉里**也是隐藏的** —— 抽屉能打开、遮罩在、关闭按钮在,内容是空的。
|
|
532
|
+
加 `.shell__side--drawer` 修饰类,用 0,2,0 压过 0,1,0 那条,
|
|
533
|
+
不靠源码顺序也不靠 `!important`。
|
|
534
|
+
|
|
535
|
+
### 分类纠正:它是「必需」,不是「可选增强」
|
|
536
|
+
|
|
537
|
+
我第一版把它写成"可选增强"("宽屏没有它一切照旧")。那条理由只对桌面成立:
|
|
538
|
+
CSS 在窄屏无条件收走侧栏,没有这层脚本就是**一个入口都没有**。
|
|
539
|
+
START-HERE 的分类判据(aria 计数 11 ≥ 5)与我的初判相反,而判据是对的 ——
|
|
540
|
+
它数的是"这层为实现契约动了多少东西"。两个问题不等价,我当初混了。
|
|
541
|
+
|
|
542
|
+
顺带修 `.shell__topbar-end` 被压扁:窄屏上它会被标题挤到一半宽,
|
|
543
|
+
里面的文字折成两行,顶栏从 56px 长到 76px(`flex: none` 让标题先截断)。
|
|
544
|
+
|
|
545
|
+
### 门禁 `app-shell-check.js` +19 条(F / G 组)
|
|
546
|
+
|
|
547
|
+
```
|
|
548
|
+
F1 宽屏:opt-in 后按钮已注入(顶栏首位 / aria-haspopup / 名字取自 data-shell-nav),但不可见
|
|
549
|
+
F2 窄屏:按钮出现且热区 44×44,侧栏已收走
|
|
550
|
+
F3 点开:role=dialog + aria-modal + 有名字
|
|
551
|
+
F4 抽屉里是侧栏的**可见**克隆 ← 0.19.0 的真缺陷就在这条上
|
|
552
|
+
F5 克隆保留 aria-current
|
|
553
|
+
F6 克隆里没有与文档重复的 id
|
|
554
|
+
F7 初始焦点落在「当前页」,背景 inert + 已锁滚动
|
|
555
|
+
F8 Esc ⇒ 焦点回按钮、锁滚动与 inert 复位
|
|
556
|
+
F9 点链接先关抽屉
|
|
557
|
+
F10 开着抽屉拉宽 ⇒ 自动关
|
|
558
|
+
F11 缺 drawer.js ⇒ 不注入按钮 + 点名警告
|
|
559
|
+
G1/G3/G2 剥掉对应规则/监听 ⇒ 上面三条各自变红
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
三条反向控制都用**改文本 + 重渲染**,不是改运行时状态:
|
|
563
|
+
判据要证的是"这段代码有用",不是"我能把它掰弯"。
|
|
564
|
+
每条前面还有一条 G0「替换真的改到了文件吗」—— 否则整套反向控制可能是空转。
|
|
565
|
+
|
|
566
|
+
### 文档纠错
|
|
567
|
+
|
|
568
|
+
`app-shell` 有三处写着"`drawer` 含 … 变宽自关"。**它没有** ——
|
|
569
|
+
"变宽自关"是 `03-patterns/nav` 的能力(`nav.js` 契约 ③)。
|
|
570
|
+
`drawer.js` 里连 `matchMedia` 都没有。三处已改;宽屏复位的责任现在明确由
|
|
571
|
+
`shell-nav.js` 自己承担(它也确实实现了,F10 守着)。
|
|
572
|
+
|
|
573
|
+
### 回归
|
|
574
|
+
|
|
575
|
+
- `app-shell-check.js` 20 → 39 条,全绿
|
|
576
|
+
- `drawer-check.js` / `nav-drawer.js`(44 条)全绿 —— 改 `drawer.js` 后复跑
|
|
577
|
+
- axe:**0 类违规**(42 页);`landmark-unique` 是 demo 页六个外壳重名,已改名
|
|
578
|
+
- 可访问树 42 页 0 失败 · RTL 40 页 0 失败 · 视觉基线 46 页
|
|
579
|
+
- `gen-behavior.py` 扫描范围扩到 `04-recipes`(否则注入块会变成没人校验的死文本)
|
|
580
|
+
- 门禁总数 218(沿用同一道文件,未新增)
|
|
581
|
+
|
|
582
|
+
## [0.18.0] - 2026-10-31
|
|
583
|
+
|
|
584
|
+
新增两个组件:`upload`(表单控件最后一块)与 `app-shell`(工具型页面骨架)。
|
|
585
|
+
|
|
586
|
+
### 新组件 · app-shell(应用外壳)—— 两个"开发者的屏幕上绿、用户的屏幕上红"的坑
|
|
587
|
+
|
|
588
|
+
本库已有的四个 recipe 都是**内容型**版式(读 / 查 / 写 / 比),
|
|
589
|
+
缺的是**工具型**:左边一列常驻导航、上面一条工具栏、右边是主区。
|
|
590
|
+
每个后台项目都在手写它,而每一版都会踩同样的两个坑:
|
|
591
|
+
|
|
592
|
+
| 坑 | 桌面上的样子 | 用户看到的样子 |
|
|
593
|
+
|---|---|---|
|
|
594
|
+
| 主区少 `min-width: 0` | 1280px 屏幕一切正常 | 数据表多两列就**整页横滚**,侧栏顶栏一起被推走 |
|
|
595
|
+
| 高度用 `100vh` | **永远看不出来** | 手机地址栏展开时底部被裁一截,而且不出现滚动条 |
|
|
596
|
+
| 侧栏 `position: fixed` | 视觉上完全一样 | RTL 下侧栏跑到右边、主区仍在右边留白 |
|
|
597
|
+
|
|
598
|
+
所以门禁必须在"塞宽内容 / 缩到 375px / 打开强制色"的条件下量,量默认状态什么都测不出。
|
|
599
|
+
|
|
600
|
+
#### 布局决策
|
|
601
|
+
|
|
602
|
+
- **侧栏 `sticky` + `align-self: start`**,不是 `fixed`(留在 grid 流里,RTL 自动正确)
|
|
603
|
+
- **底色铺在 `.shell` 上,主区盖回纸色** —— 侧栏是 `sticky`,高度只到内容为止,
|
|
604
|
+
底色画在它身上会出现"侧栏背景只到一半"
|
|
605
|
+
- **分界线画在 `.shell__main` 的 `border-inline-start` 上**,同理:它通高,侧栏不通高
|
|
606
|
+
- **窄屏把侧栏整列收走,不做抽屉** —— 抽屉已有 `03-patterns/drawer`
|
|
607
|
+
(Esc / 焦点归位 / 背景锁 / 变宽自关),再实现一遍就是两份焦点行为
|
|
608
|
+
|
|
609
|
+
#### 门禁 `05-audit/app-shell-check.js`(20 条)
|
|
610
|
+
|
|
611
|
+
A 结构 / B 布局契约 / C 窄屏退化 / D 强制色 / E 反向控制。
|
|
612
|
+
|
|
613
|
+
E 组三条是这个组件最值钱的部分,它把"两个保险"拆开验:
|
|
614
|
+
|
|
615
|
+
```
|
|
616
|
+
E1 只去掉 .shell__scroll 的 overflow ⇒ 页面横滚(B1b 红),主区宽度不变(B1 仍绿)
|
|
617
|
+
E1b 轨道换成 `1fr`(去掉 minmax) ⇒ min-width: 0 单独兜住,主区仍不被撑破
|
|
618
|
+
E1c 再把 min-width 也去掉 ⇒ 主区从 1040 被撑到 1318(B1 红)
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
`minmax(0, 1fr)` 与 `min-width: 0` 是**两层**保险,各自独立生效 —— E1b/E1c
|
|
622
|
+
证明的不是"代码现在是绿的",是"少了任何一层都会坏"。
|
|
623
|
+
|
|
624
|
+
#### 三个自己踩的坑
|
|
625
|
+
|
|
626
|
+
1. **契约是半截的。** 第一版只写了"宽内容要在自己的 overflow 容器里滚",
|
|
627
|
+
却没给那个容器。门禁 C2 在 375px 视口下量到 `docScrollWidth: 748` ——
|
|
628
|
+
于是补了 `.shell__scroll`,并且它还缺 `tabindex="0"` + `role="region"`
|
|
629
|
+
(axe 报 `scrollable-region-focusable`:鼠标能拖,键盘滚不动)。
|
|
630
|
+
2. **顶栏被页面骨架的 `.page h2` 撑高了 43px。**
|
|
631
|
+
`.shell__title` 是 (0,1,0),`.page h2` 是 (0,1,1) ⇒ 标题的 margin 赢。
|
|
632
|
+
顶栏从 56px 变成 99px,而截图里只像"留白多了点"。
|
|
633
|
+
修法是选择器加 `.shell__topbar` 前缀,不是 `!important`。
|
|
634
|
+
3. **反例被右栏的底色盖住了。** 反例的"撑破"是横向溢出,但 `.cmp__pane` 没有
|
|
635
|
+
`overflow: hidden` 时,溢出部分会被**后渲染的右栏背景**盖住 —— 看起来两栏一样。
|
|
636
|
+
给反例栏加 `position: relative; z-index: 1` 才让"越界"可见。
|
|
637
|
+
|
|
638
|
+
#### 回归
|
|
639
|
+
|
|
640
|
+
- axe:**0 类违规 / 1117 条规则通过**(42 页)
|
|
641
|
+
- RTL:40 页 0 失败
|
|
642
|
+
- 可访问树:42 页转录,0 失败
|
|
643
|
+
- 视觉基线:46 页(新增 1 页)
|
|
644
|
+
|
|
645
|
+
### 新组件 · upload(文件上传)—— 补齐表单控件最后一块
|
|
646
|
+
|
|
647
|
+
37 个组件里,表单控件有 input / select / combobox / choice / switch,
|
|
648
|
+
**唯独没有"选文件"**。于是每个项目自己糊一个 `<input type=file>`:
|
|
649
|
+
长得和全站不一样,而拖放、多文件、失败原因全缺。
|
|
650
|
+
|
|
651
|
+
#### 两层形态(同一条 CSS 的升级路径,不是两个组件)
|
|
652
|
+
|
|
653
|
+
| 形态 | 需要 JS | 得到什么 |
|
|
654
|
+
|---|---|---|
|
|
655
|
+
| `.upload` | **不需要** | 原生 `<input type=file>` + `::file-selector-button` 美化;原生文件名回显、表单提交、`accept` 过滤全走浏览器 |
|
|
656
|
+
| `.upload--drop` | 需要 | 加一层拖放区 + 文件列表 + 大小/类型/数量校验 + 移除 |
|
|
657
|
+
|
|
658
|
+
保留基础形态不是"顺手多做一层":拖放区一旦是唯一入口,**JS 没加载就等于没有上传功能**,
|
|
659
|
+
而且用户看不到自己选了什么(原生文件名回显被视觉隐藏吃掉)。
|
|
660
|
+
|
|
661
|
+
#### 三条硬约定(都写进了 README 与门禁)
|
|
662
|
+
|
|
663
|
+
1. **`input.files` 是唯一真值,列表是镜像。** 拖放不是"第二条通道",
|
|
664
|
+
它把文件塞进 `input.files` 再重画。代价是再次选择/拖入会**替换**列表
|
|
665
|
+
(与原生 `<input multiple>` 一致);收益是 `<form>` 序列化、`FormData`、
|
|
666
|
+
原生校验全都不用重写,也就不会出现"列表里有、提交时丢了"。
|
|
667
|
+
2. **校验只有一份实现**(`_validate`),拖放与原生选择都调它。
|
|
668
|
+
写成两遍不会"难看",会**漂移** —— 某天给拖放加了大小限制忘了给原生选择加,
|
|
669
|
+
结果同一页面上拖进去的被拒、点选进去的通过。这种 bug 只有用户会碰到。
|
|
670
|
+
3. **`DataTransfer` 不存在时明确拒绝。** 老 Safari(< 14.1)没有构造器 ⇒
|
|
671
|
+
`input.files` 不可写。此时照样渲染列表、照样派发事件,用户会看到"文件进去了",
|
|
672
|
+
点提交却什么都没有,**且不报错**。本组件提示"当前浏览器不支持拖放"并**不接收**。
|
|
673
|
+
|
|
674
|
+
#### 门禁 `05-audit/upload-check.js`(23 条,四组)
|
|
675
|
+
|
|
676
|
+
- **A 结构**:基础形态可见、增强形态是 1px 视觉隐藏(**不是** `display:none`)、
|
|
677
|
+
仍在 Tab 序列、dropzone 是 `<label for>`、播报区不在 `<ul>` 内、空列表不占位
|
|
678
|
+
- **B 行为**:真值方向、镜像一致、超限/类型/数量三种拒绝都不进 `files`、
|
|
679
|
+
删除后索引不错位、**两条入口对同一文件给出同样结论**、
|
|
680
|
+
`DataTransfer` 缺失时不假成功、进度按需插入
|
|
681
|
+
- **C 强制色**:边框仍在且与背景不同色、`currentColor` 图标没变白
|
|
682
|
+
- **D 反向控制**三条:`display:none` / 只清空 `files` 不动 DOM / 去掉 `border`
|
|
683
|
+
|
|
684
|
+
#### 三个自己踩的坑
|
|
685
|
+
|
|
686
|
+
1. **`<label aria-hidden="true">` 会让 input 失去可访问名。**
|
|
687
|
+
第一版给 dropzone 加了 `aria-hidden`(理由是"名字由 `.field__label` 提供"),
|
|
688
|
+
axe 在对比块里直接报 `label` **critical** —— 那里没有 `.field__label`。
|
|
689
|
+
改成让 label 自己说话:读屏会念两遍(啰嗦),但**任何用法都不会没有名字**。
|
|
690
|
+
干净的写法要求使用者不漏写 label,而没有任何门禁拦得住这件事。
|
|
691
|
+
2. **空的进度槽会占一整行。** `.upload__progress` 显式跨满整行,
|
|
692
|
+
常驻的话 grid 就多一条隐式轨道 ⇒ "待上传"的项比"被拒"的项高 12px。
|
|
693
|
+
改成**按需插入**(`setProgress` 时现建)。
|
|
694
|
+
3. **`min-height: 50px` 抹平行高,是个假象。** 本库的 `box-sizing: border-box`
|
|
695
|
+
挂在 `.page *` 上、是选配(组件库不改宿主盒模型)——
|
|
696
|
+
同一句 min-height 在有骨架的页面里算出 50,在没有骨架的页面里算出 68。
|
|
697
|
+
改成给缺失的那一格补一个同尺寸的 `::after` 占位,两种盒模型下都是 50。
|
|
698
|
+
|
|
699
|
+
#### 回归
|
|
700
|
+
|
|
701
|
+
- axe:**0 类违规 / 1083 条规则通过**(40 页)
|
|
702
|
+
- RTL:40 页 0 失败(含新页面)
|
|
703
|
+
- 命中区:`.upload__remove` 32×32 ——**仅 AA**,有意为之(列表项只隔 4px,
|
|
704
|
+
44px 会溢出到相邻项的可行点区域上)。已登记进 `tap-target.js`,
|
|
705
|
+
让它**被量出来**而不是藏在"没登记所以没被测"里。
|
|
706
|
+
- 视觉基线:45 页(新增 1 页)
|
|
707
|
+
- 门禁总数 216 → 217
|
|
708
|
+
|
|
709
|
+
## [0.17.0] - 2026-10-31
|
|
710
|
+
|
|
711
|
+
### 能力 · 强制色模式(Windows 高对比度):从"3 个 CSS 有 media 块"到"逐页验证 + 门禁"
|
|
712
|
+
|
|
713
|
+
**起因**:`docs/ACCESSIBILITY.md` 里一直挂着一条缺口 ——「强制色板只覆盖 6 个 CSS,
|
|
714
|
+
未逐组件验证」。这句话是诚实的,但它同时意味着一件事:**其余 34 个 CSS
|
|
715
|
+
在高对比度下是什么样,没人知道**。
|
|
716
|
+
|
|
717
|
+
用 CDP 的 `Emulation.setEmulatedMedia` 把 `forced-colors` 打开逐页实拍,
|
|
718
|
+
结果不是"样式略糙",是**信息直接消失**:
|
|
719
|
+
|
|
720
|
+
| 组件 | 强制色下实际发生的事 |
|
|
721
|
+
|---|---|
|
|
722
|
+
| progress(线形) | 735×6 **整块空白**,轨道与填充同色,进度归零 |
|
|
723
|
+
| progress(环形) | `conic-gradient` 被整条移除,只剩一个漂着的 `72%` |
|
|
724
|
+
| switch | 滑块**完全消失**(白底 + 阴影被丢弃 + 无边框),开关只剩空心胶囊,**开与关看不出区别**;修好滑块后还发现禁用态与可用关态**完全一样** |
|
|
725
|
+
| table 排序箭头 | `mask` 还在,但 `background-color: currentColor` 被换成 Canvas ⇒ **白底白箭头** |
|
|
726
|
+
| input 错误图标 | 同上,叹号圆标变白 |
|
|
727
|
+
| skeleton | 整块变白 —— 看起来像"加载完了,内容为空" |
|
|
728
|
+
| choice 单选圆点 / indeterminate 横线 | 同上模式,圆点与横线消失(**checkbox 的勾反而没事**,因为它用 `border` 画) |
|
|
729
|
+
|
|
730
|
+
这些问题在正常截图里一个都看不见,在 axe / 对比度检查里也**全绿**:
|
|
731
|
+
强制色不是"对比度不够",是"颜色被换掉之后语义丢失"。所以它必须单独一道门禁。
|
|
732
|
+
|
|
733
|
+
### 加了什么
|
|
734
|
+
|
|
735
|
+
- **`05-audit/forced-colors-check.js`**(新)—— 三组判据:
|
|
736
|
+
- **A 规则层**(fixture + 真实 CSS):6 类缺陷逐条断言"前景 ≠ 背景"
|
|
737
|
+
- **B 页面层**:同样的模式扫过 37 个真实 demo,防止"修完这几个,别处还有同款"
|
|
738
|
+
(B3 就是这么抓出 `choice` 的 —— 我原本没打算改它)
|
|
739
|
+
- **C 反向控制**:把修复剥掉再跑一遍,**必须变红** —— 否则 A 组的绿只证明
|
|
740
|
+
"代码现在是绿的",不证明判据有牙
|
|
741
|
+
- 已接进 `check-all.sh`(浏览器门禁段,紧随 `container-query`)。
|
|
742
|
+
|
|
743
|
+
### 改了什么(6 个 CSS)
|
|
744
|
+
|
|
745
|
+
- `progress.css`:轨道加 `outline: 1px solid transparent`;填充条
|
|
746
|
+
`@media (forced-colors: active) { background: Highlight }`;环形补 2px 轮廓
|
|
747
|
+
- `switch.css`:滑块 `border: 0` → `1px solid transparent`;开态轨道用 `Highlight`;
|
|
748
|
+
禁用态三处用 `GrayText`;顺手修掉一条**从未生效过**的
|
|
749
|
+
`border-color: var(--accent)`(border 为 0 时它是死代码,加了边框才突然显形)
|
|
750
|
+
- `table.css` / `input.css`:mask 图标的 `background-color` 在强制色下改 `CanvasText`
|
|
751
|
+
- `skeleton.css`:填充改 `GrayText`(系统语义正是"占位/非正文")
|
|
752
|
+
- `choice.css`:radio 圆点与 indeterminate 横线改 `CanvasText`
|
|
753
|
+
|
|
754
|
+
### 三条实测规律(省得下个人再试一遍)
|
|
755
|
+
|
|
756
|
+
1. `border: 1px solid transparent` 与 `outline: Npx solid transparent`
|
|
757
|
+
在强制色下**自动变 CanvasText 实线** ⇒ 边界预留不需要 media query。
|
|
758
|
+
但 `box-shadow: 0 0 0 1px transparent` **无效** —— 阴影整个被丢弃。
|
|
759
|
+
2. **用 `background-color` 画"前景图形"必死,用 `border`/`color` 画则活。**
|
|
760
|
+
强制色按属性换色:背景换背景色、前景换前景色。把图形画在背景属性上,
|
|
761
|
+
等于把它声明成了背景。
|
|
762
|
+
3. **不能把 `rgb(0,0,0)` 当成 Canvas。** 黑既可能是亮色主题的 CanvasText,
|
|
763
|
+
也可能是暗色主题的 Canvas —— 同一个 RGB 在两种主题下含义相反。
|
|
764
|
+
判据只能问"这个图形和它所在的那块底,颜色一样吗"。
|
|
765
|
+
|
|
766
|
+
### 两个判据自身的坑(都已修,记在这里)
|
|
767
|
+
|
|
768
|
+
- **切媒体特性 ≠ 样式已重算完**:`.switch__track` 有
|
|
769
|
+
`transition: background var(--dur-fast)`,切到 `forced-colors` 后读它拿到的是
|
|
770
|
+
**过渡起点**(纯白),等 250ms 才是终态。而 progress / table / choice 没有
|
|
771
|
+
对 `background` 做过渡 ⇒ 立刻就对,于是同一个门禁里"有的过有的不过"。
|
|
772
|
+
修法是 `document.getAnimations().forEach(a => a.finish())` 把动画推到终态,
|
|
773
|
+
不是加 sleep。(和 0.16.1 那次「`networkidle0` ≠ JS 就绪」是同一类错误。)
|
|
774
|
+
- **模拟色板与真机不是同一套**:`GrayText` 在 Chromium 模拟里解析成
|
|
775
|
+
`rgb(96,0,0)` 深红,真实 Windows 高对比度主题下是灰。所以本文档与门禁
|
|
776
|
+
都不写具体 RGB,只判"是否等于背景"。
|
|
777
|
+
|
|
778
|
+
### 视觉基线
|
|
779
|
+
|
|
780
|
+
Windows 三套(light / dark / RTL)已重录。像素比对结果:**43/44 页零差异**,
|
|
781
|
+
只有 `switch` 有 112 个像素(0.0133%)变化,集中在滑块圆角的抗锯齿边缘;
|
|
782
|
+
`switch-geometry` 复验滑块仍是 20×20、上下间隙对称 ⇒ 尺寸未变。
|
|
783
|
+
Linux 三套由 CI 的 `shoot_baseline` 重录。
|
|
784
|
+
|
|
785
|
+
### 顺带修掉一个"环境假装成代码"的假红
|
|
786
|
+
|
|
787
|
+
全量 4 路并发跑完,`tap-target` 与 `rtl-render` 同时红,而且数字都像代码缺陷:
|
|
788
|
+
|
|
789
|
+
```
|
|
790
|
+
ios 验证页 单选/复选 79×22 ❌(单独跑 360×44 ✅)
|
|
791
|
+
analysis-report 希伯来语膨胀溢出 1464px(单独跑 0)
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
归因错了两次(先怀疑 CSS,再怀疑"并发下量得早"),最后靠
|
|
795
|
+
**把脚本退回 git 原版、只换环境**才查清:8000 上跑的是
|
|
796
|
+
`05-audit/serve.py` —— **单线程** HTTPServer(给"看 demo"用的),
|
|
797
|
+
而门禁该用 `http-serve.py`(256 backlog + 守护线程)。
|
|
798
|
+
4 路浏览器并发拉 CSS/字体时单线程排队 ⇒ **样式还没落地就被量**。
|
|
799
|
+
|
|
800
|
+
`check-all.sh` 的 canary 当时只比对 `index.html` 的**字节**:
|
|
801
|
+
它验"是不是同一棵树",不验"这服务撑不撑得住并发" ⇒ 同一棵树 + 单线程服务器,
|
|
802
|
+
canary 愉快放行。
|
|
803
|
+
|
|
804
|
+
- `http-serve.py`:每个响应加 `X-FL-Gate-Server: threaded-256` 身份戳
|
|
805
|
+
- `check-all.sh`:canary 升级为**字节一致 + 身份戳**两条都要;
|
|
806
|
+
缺戳时直接说"很可能是 serve.py,会假红",而不是笼统报错
|
|
807
|
+
- `tap-target` / `rtl-render`:顺手补了 `document.fonts.ready` + 两个动画帧
|
|
808
|
+
(换文本后确实要重等字体,是真稳健性)—— 但注释写明**这不是那次假红的解释**,
|
|
809
|
+
不冒领功劳
|
|
810
|
+
|
|
811
|
+
全量复跑:**216 道 · 0 失败**(1 道 SKIP 是 `benchmark-records`,真实模型记录 0 份,
|
|
812
|
+
与本次无关)。
|
|
813
|
+
|
|
19
814
|
## [0.16.1] - 2026-10-31
|
|
20
815
|
|
|
21
816
|
### 判据 · `a11y-ax` 在并发下量到过"组件还没初始化完"的树
|