agentcss 0.11.7 → 0.11.9

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.
@@ -58,9 +58,13 @@
58
58
  user-select: none;
59
59
  text-decoration: none;
60
60
 
61
- /* 🔴 兜底:即使内容异常长也不许溢出边界 */
62
- overflow: hidden;
63
- max-width: 100%;
61
+ /* 🔴 0.11.9 改:原来是 `overflow: hidden; max-width: 100%`("内容再长也不
62
+ 许溢出")。副作用:`overflow: hidden` 把 flex/grid 项的**自动最小尺寸**
63
+ 变成 0 ⇒ 窄容器里按钮被压得比自己的文字还窄 ⇒ 文字被裁。
64
+ 实测 200% 字号下按钮 200px、文字 232px,裁掉 32px(WCAG 1.4.4 违规)。
65
+ 而它要防的"label 与 spinner 重叠"已由 grid 叠放解决(同一格不会重叠)。
66
+ ⇒ 换成 `min-width: max-content`:按钮不窄于自己的文字。 */
67
+ min-width: max-content;
64
68
 
65
69
  /* 动效只用 token 里的那一组 —— 一源原则 */
66
70
  transition: background-color var(--dur-fast) var(--ease-out),
@@ -107,8 +107,16 @@
107
107
  '</ul>';
108
108
 
109
109
  if (collapsed) {
110
+ /* 🔴 判据必须查 `.tree__item`(类名),不能查 `role="treeitem"`。
111
+ 原因:这段跑在 `Tree.create()` **之前**,而 `role="treeitem"` 正是
112
+ `create()` 补上去的 —— 上面手写的 `<li>` 只有类名、没有 role。
113
+ ⇒ 按 role 查时,每一个 `g.parentElement.getAttribute('role')` 都是
114
+ `null`,条件恒假,**一个都没收起**,
115
+ 而页面小标题写着「2 · 默认全部收起」⇒ 演示与实物相反。
116
+ (0.11.9 由 rtl-render 的方向性字形探针发现:
117
+ 探针找 `.tree__item[aria-expanded="false"]` 一个都找不到。) */
110
118
  document.querySelectorAll('#' + id + ' [role="group"]').forEach(function (g) {
111
- if (g.parentElement.getAttribute && g.parentElement.getAttribute('role') === 'treeitem') {
119
+ if (g.parentElement.classList.contains('tree__item')) {
112
120
  g.hidden = true;
113
121
  g.parentElement.setAttribute('aria-expanded', 'false');
114
122
  }
@@ -87,6 +87,15 @@
87
87
  content: "\00B7"; /* · 叶子节点 */
88
88
  }
89
89
 
90
+ /* ---------- 书写方向(RTL)----------
91
+ ⚠️ 逻辑属性只能翻「框的位置」,翻不了**字形**:▼/▶ 是文字符号,不是边框
92
+ 画出来的三角(select 的 caret 用 border,天然对称,不用管)。
93
+ ⇒ RTL 里"收起"的箭头必须指向左,否则阿拉伯语用户看到的是一个指错方向的
94
+ 箭头 —— 这种错不报错、也不溢出,只能靠一条显式规则或一张基线图发现。 */
95
+ [dir="rtl"] .tree__item[aria-expanded="false"] > .tree__node .tree__marker::before {
96
+ content: "\25C0"; /* ◀ */
97
+ }
98
+
90
99
  /* ---------- 层级缩进(用左边框引导线)---------- */
91
100
  .tree ul ul {
92
101
  padding-inline-start: var(--sp-5);
@@ -357,10 +357,18 @@
357
357
  if (it.ul && it.li.getAttribute('aria-expanded') === 'true') {
358
358
  setExpanded(it, false); /* 先收起,不动焦点 */
359
359
  } else {
360
- /* 找到父节点并聚焦它(← 在收起状态下要"往回走")*/
361
- for (var i = cur; i > 0; i--) {
362
- var cand = visible()[i];
363
- if (it.li.contains(cand.li)) { focusAt(i); return; }
360
+ /* 找到父节点并聚焦它(← 在收起状态下要"往回走")
361
+ 🔴 0.11.9 修:循环原来从 `cur` **本身**开始,而
362
+ `it.li.contains(it.li)` 恒为 true ⇒ 第一次迭代就 focusAt(cur)
363
+ 然后 return ⇒「已收起时按 ← 回到父节点」**从来没生效过**,
364
+ 焦点原地不动,看起来像"按键没反应"。
365
+
366
+ ⚠️ 它一直没被发现,是因为 tree-check 那条断言在 demo 出现
367
+ 真实的收起节点之前一直走"跳过"分支 —— 跳过不算通过。
368
+ 现在从 cur - 1 往回找第一个包含本节点的可见项 = 最近的祖先。 */
369
+ var vis = visible();
370
+ for (var i = cur - 1; i >= 0; i--) {
371
+ if (vis[i].li.contains(it.li)) { focusAt(i); return; }
364
372
  }
365
373
  }
366
374
  return;
package/CHANGELOG.md CHANGED
@@ -16,6 +16,129 @@
16
16
 
17
17
  (无)
18
18
 
19
+ ## [0.11.9] - 2026-10-29
20
+
21
+ **把「我们说做到的」和「能证明做到的」对齐**。这一版没有新组件,改的全是
22
+ 「话」与「证据」之间的落差 —— 每一处落差在改了之后都能被一条命令复现。
23
+
24
+ ### Added
25
+
26
+ - **无障碍一致性声明**(`docs/ACCESSIBILITY.md` 重写)。此前这一页写的是
27
+ 「我们验了什么」,读者真正要的是「你按哪一版标准、哪几条达标、哪几条不达标」。
28
+ 现在是 **51 行 WCAG 2.2 AA 逐条声明**(✅ 27 · ◐ 7 · ✗ 0 · — 10),
29
+ 四态各有定义:✅ 有自动化判据守着、◐ 部分达成(附缺口)、
30
+ ✗ 未达成(本版为 0)、— 不适用(附不适用的理由)。
31
+ 每一行都给出可复现的命令或判据名 —— 没有证据的行不许写 ✅。
32
+ - **真机读屏协议**(`docs/A11Y-MANUAL-PROTOCOL.md`):20 分钟、两条路径
33
+ (NVDA + Chrome / VoiceOver + Safari)、6 个页面 × 5 件事。结果表**全部写着
34
+ 「未执行」** —— 这一页记录的是边界,不是进度。自动化能给的是可访问性树,
35
+ 给不了「读屏器念出来到底顺不顺」,而后者才是用户的实际体验。
36
+ - **组件文档内嵌可直接操作的示例**(35 页)。此前每页只有 2–4 个链接指向
37
+ `demo.html`:想知道按钮长什么样,得跳出去。现在是 same-origin iframe 活标本 ——
38
+ 自动撑高(多档采样,不靠猜高度)、跟随明暗主题、脚本可交互;
39
+ `file://` 打开时退化为固定高度可滚动(跨源拿不到内部高度,这是浏览器的规矩,
40
+ 不是没做)。
41
+ - **无障碍树快照判据**(`a11y-ax`):10 个页面、575 行朗读序列,逐行 diff;
42
+ 同时硬判 WCAG 4.1.2 —— 每个可交互控件必须有非空 accessible name。
43
+ 这是声明里「名称/角色/值」那一行唯一的证据来源。
44
+ - **缩放 / 重排 / 焦点遮挡判据**(`zoom-reflow`):38 页,覆盖 1.4.4 文本 200%、
45
+ 1.4.10 320px 重排、2.4.11 焦点不被吸顶栏遮挡。
46
+ - **RTL 渲染判据**(`rtl-render`):37 页。判三件事 —— RTL 下页面不横滚、
47
+ 把正文换成阿拉伯语 / 希伯来语并膨胀到 1.3 倍仍不横滚、方向性字形真的镜像
48
+ (收起箭头 ▶→◀ 必须翻;展开箭头 ▼ 与叶子标记 · **必须不翻**)。
49
+ - **RTL 视觉基线** 13 页(`--dir rtl`):覆盖「镜像错了但不溢出不改字形」这一类
50
+ 自动化看不见的坏法(抽屉从错误的一侧滑入、栅格左右互换、上/下一页顺序反了)。
51
+ - **CI 字体安装脚本**(`05-audit/ci-fonts.sh`):ubuntu runner 默认没有 CJK /
52
+ 阿拉伯 / 希伯来字体,不装的话视觉基线会录成一堆方框 —— 而"方框"也是有像素的,
53
+ 比对会"通过",直到真回归也看不出来。脚本装完还会点名验一遍三类字体各至少一条。
54
+ - **失败时上传视觉证据**(两个工作流):`_current-*` 与 `_diff-*` 打成 artifact。
55
+ 没有它,CI 上的 `FAIL … 7.6%` 只能靠猜 —— 本机看不到那台机器的渲染结果。
56
+
57
+ ### Fixed
58
+
59
+ - **按钮在 200% 字号下被裁字**。`overflow: hidden` 会把 flex/grid 项的自动最小尺寸
60
+ 变成 0,而它当初要防的重叠已由 grid 叠放解决。改为 `min-width: max-content`。
61
+ - **`tree` 演示第 2 例「默认全部收起」实际是全部展开**。收起逻辑查
62
+ `role="treeitem"`,而那个 `role` 正是 `Tree.create()` 之后才补上的 ——
63
+ 判据跑在它之前,条件恒假。改为查 `.tree__item` 类名。
64
+ - **`tree` 的折叠箭头在 RTL 下不镜像**。全库此前**没有任何一条 `[dir="rtl"]` 规则**:
65
+ 逻辑属性只翻「框」,翻不了「字形」,而 ▶ 是文字符号不是边框画出来的三角。
66
+ - **视觉回归不再依赖 Pillow**。缺 Pillow 时那道门禁此前直接跳过,而本仓库自己的
67
+ 规矩是「跳过不是通过」。比对改为零依赖 PNG 解码(zlib + 5 种行过滤),
68
+ 并与 Pillow 做过逐字节交叉验证(393×1744 的基线图,解码结果完全相同)。
69
+ - **`404.html` 的站点根路径被判据当成了文件系统路径**。`/frontend-lib/01-tokens/*.css`
70
+ 是 Pages 的站点根写法(404 页会在任意层级被服务,只能写绝对路径),
71
+ 而判据把它交给 `os.path.join` —— 前导 `/` 会让 `join` 丢掉前面的参数,
72
+ 于是变成 `/frontend-lib/...`。这台机器的仓库**正好**在 `E:\frontend-lib`,
73
+ Windows 就把它解析到真实文件上,**本机永远绿**;换到 Linux 立刻红 3 处。
74
+ 改为显式的站点根映射。教训不是"路径写错了",是**环境顺手掩盖了判据缺陷**。
75
+ - **`SUPPORT.md` 指向一个不入库的文件**。原文写「证据文件:`10-review/engine-evidence.json`」,
76
+ 而那一行在 `.gitignore` 里(生成物、每台机器不同)⇒ 读者点开是 404,
77
+ npm 包里也没有。改为指向**生成它的脚本**,并说明产物本就不该入库。
78
+ 本机能过只是因为磁盘上恰好留着上次生成的产物。
79
+ - **门禁失败时的诊断会给出错误结论**。跑批器原来用
80
+ `grep '^\s*\[' | head -6` 显示失败输出:`head -6` 把 13 节的 release 门禁
81
+ 截成 `[1]`…`[6]`,**看起来像前六节全挂了**(实际挂的是第 10 节);
82
+ `grep` 只认节标题,把 `· 文件 原因` 这种明细行全丢掉 ——
83
+ `wiring` 报了 3 处、日志里一处都看不见。改为原样回显失败门禁的完整输出
84
+ (超过 160 行才截断,且保留末尾 40 行 —— 崩溃栈在那里)。
85
+ - **视觉基线改为按平台分集**(`_baseline-<平台>[-dark|-rtl]`)。本库不含
86
+ `@font-face`,字体走系统栈,同一份 HTML 换到另一个系统整页像素差 7~11% ——
87
+ 与回归无关。基线目录名现在带平台,各平台各存一套;某平台没有基线集时
88
+ 门禁**直接判红**并说明去哪弄,而不是拿另一台机器的渲染当标准。
89
+
90
+ ### Changed
91
+
92
+ - **发版改为 tag 触发**(`.github/workflows/release.yml`):`v*` tag → 校验
93
+ tag 与版本一致 → 跑发布前门禁 → `npm publish --provenance` → 从 CHANGELOG
94
+ 对应小节建 Release(小节为空则拒绝)。`--provenance` 让 npm 给包签上
95
+ 「这确实由这个仓库、这条工作流构建」,本地手工发版发不出这个签名。
96
+ - `CONTRIBUTING.md` 的环境依赖改成两份实话:Node + Chromium(行为与渲染类判据)、
97
+ Pillow(仅交叉验证用,不再是运行时依赖)。
98
+ - 发版工作流多了两条实话:发布前**先装 CI 字体**(基线锚在字体上),
99
+ 以及 `workflow_dispatch` 只用于**补录某平台基线**、不会顺手发版。
100
+
101
+
102
+ ## [0.11.8] - 2026-10-29
103
+
104
+ **补上文档站最大的真空**:令牌用法、焦点环、动效时长、暗色切换、逻辑属性这些
105
+ 「不属于任何组件」的规则,此前**没有任何页面承载**。症状不是少一页,是搜
106
+ 「减少动效」「逻辑属性」「强制暗」全站 0 命中 —— 用户问的是意思,而站里没有
107
+ 承载那个意思的地方。
108
+
109
+ ### Added
110
+
111
+ - **全局约定页**(`docs/CONVENTIONS.md` → `guide/conventions.html`)。十节:
112
+ 令牌三层(raw / 语义 / 组件私有,附实测对比度)、间距与圆角与层级的台阶、
113
+ 焦点环(2px + 2px、为什么只用 `:focus-visible`、深底内白环)、动效三档时长
114
+ 与"减少动效"的三条策略(位移关 / 真实进度留 / 颜色可缩短)、暗色三态与两个
115
+ 选择器坑、逻辑属性与绝对定位写法、为什么**不用** `@layer`、`!important`
116
+ 必须可分类、对比度与 44px 触控线、覆盖策略(改令牌而不是改组件),
117
+ 末尾一张「每条规则由哪道判据挡住」的对应表。
118
+ - **无障碍页**(`docs/ACCESSIBILITY.md` → `guide/accessibility.html`):
119
+ 目标等级、**验过的范围与手段**(含反向控制与实算对比度)、
120
+ **六条已知缺口**(无真机读屏记录、RTL 只有静态检查、强制色板只覆盖一部分、
121
+ Android 系统 WebView 未验、无 VPAT、9 处「待人工」未结论)、自测命令。
122
+ - **概念页这一页型**:生成器此前只认组件目录(`ai/components.json` 的 35 个),
123
+ 跨组件的规则没有地方放。现在 `CONCEPT_PAGES` 是一等公民:进索引(排在最前)、
124
+ 进搜索键表、有自己的目录与上下篇。
125
+ - **`md-links` 门禁 + 反向控制**:文档里的路径必须落地。
126
+
127
+ ### Fixed
128
+
129
+ - **9 处文档指着不存在的文件**。`README.zh-CN.md` / `docs/INVARIANT.md` /
130
+ `ai/START-HERE.md` / `CONTRIBUTING.md` 一共 9 处指向 `docs/GUIDANCE.md`、
131
+ `docs/BENCHMARK.md`、`docs/ROADMAP.md`、`docs/RELEASING.md` —— 那四份在 0.11.4
132
+ 被**故意**移进内部目录(含账号二次因子、到期日这类不能出门的内容),
133
+ 公开面于是留下一排悬空指针。现在全部改指向真实存在的文件
134
+ (`docs/CONVENTIONS.md`、`CONTRIBUTING.md`、`API.md`、`CHANGELOG.md`)。
135
+ - 为什么一直没人发现:文档里的路径有**两种写法** —— markdown 链接与行内代码,
136
+ 而既有判据都在看别的东西(pack-closure 判包内 HTML 引用、docs-site 判生成后的
137
+ 页面链接)。`md-links.py` 两种都判,并对 CHANGELOG 显式豁免(那是历史,
138
+ 改它等于篡改版本记录)。
139
+ - 搜索探针从 14 条加到 17 条:新增的三条(减少动效 / 逻辑属性 / 强制暗)
140
+ 在加页面之前**确实是红的**,先红后绿才算判据有效。
141
+
19
142
  ## [0.11.7] - 2026-10-29
20
143
 
21
144
  **交付物零变化,改的是"我们怎么验自己"**:外网泄漏审查此前**一个文件都没读到**
package/CONTRIBUTING.md CHANGED
@@ -8,7 +8,47 @@ npm run gate # 全量:含浏览器类
8
8
  npm run gate:fast # 只跑静态层,不用装浏览器
9
9
  ```
10
10
 
11
- 全量是 **181 道门禁**(本机约 12 分钟)。
11
+ 全量是 **190 道门禁**(本机约 12 分钟)。
12
+
13
+ 环境依赖只有一套(Node + Chromium),缺了对应的门禁会**跳过**(跳过不算通过):
14
+
15
+ | 依赖 | 少了会怎样 | 装法 |
16
+ |---|---|---|
17
+ | **Node ≥ 22 + Chromium** | `unit` / `responsive` / `firefox` 与全部行为、渲染类门禁(无障碍 / RTL / 键盘 / 视觉基线 / 活标本)**跳过** | 按 `package.json` 的 `engines` 装 Node;浏览器由 `05-audit/browser.js` 自己拉起(`puppeteer-core` 已在 devDependencies) |
18
+
19
+ Python 侧**没有第三方依赖**了:视觉比对的 PNG 解码是本仓库自己的
20
+ `05-audit/pngdiff.py`(zlib + 行过滤,与 Pillow 做过逐字节对齐)。
21
+ Pillow 只在你想复核那次对齐时才有用,装了不加分、不装也不跳过。
22
+
23
+ RTL 有**两套**证据,需要分别跑、分别更新:
24
+
25
+ ```bash
26
+ node 05-audit/rtl-render.js # 渲染判据(37 页)
27
+ python 05-audit/shot-baseline.py --check --dir rtl # 视觉基线(13 页)
28
+ ```
29
+
30
+ **视觉基线按平台分集**:目录名是 `_baseline-<平台>[-<scheme>][-rtl]`,
31
+ `<平台>` 取 `sys.platform`(`win32` / `linux` / `darwin`)。原因是本库
32
+ **不含 `@font-face`**,字体走系统栈 —— 同一份 HTML 换到另一个系统,
33
+ 字宽、行高、换行位置全变,整页像素差 7~11%,跟"回归"一点关系都没有。
34
+ 所以:
35
+
36
+ - 本机没有**当前平台**的基线集 ⇒ 这门门禁**直接判红**(不是跳过):
37
+ 没有基线就没人能说"观感没变"。用 `--record` 在你**有权代表的那台机器上**
38
+ 录一套,连同 `_stamp.json` 一起提交;
39
+ - **不要**在 CI 上随手 `--record`:那等于把 CI 的渲染当成标准,
40
+ 以后真回归反而看不出来;
41
+ - Linux 那套的字体由 `05-audit/ci-fonts.sh` 统一安装(两个工作流都调它,
42
+ 不各抄一份 apt 清单)。字体版本升级会让 CI 红而本机绿 —— 确认是字体变化,
43
+ 就重录 Linux 那套;这也是"基线锚在渲染环境上"的固有代价。
44
+
45
+ **加新组件时请注意**:若它含有 **方向性字形**(箭头、指示符)、
46
+ **左右不对称的槽位/边框/滑入方向**、或**顺序有语义**(上一页/下一页、首列),
47
+ 就必须把它的 demo 加进 `shot-baseline.py` 的 `RTL_PAGES`。
48
+ RTL 视觉基线**故意不是全量**(纯文本页在 RTL 下与 LTR 是同一套盒模型,
49
+ 多录不增加判据),但反过来 —— 命中上面三条却没进表的页面,
50
+ 它的 RTL 观感**没有任何门禁看着**:`rtl-render` 只抓溢出与字形,
51
+ `rtl-check` 只查物理属性,两者都看不见"镜像错了"。
12
52
 
13
53
  ⚠️ 上面的数字写在**代码块外**是有意的:`doc-facts-gate` 会核对
14
54
  「文档里的门禁数」与 `check-all.sh` 的真实注册数,而**代码块内的内容被豁免**
@@ -79,7 +119,7 @@ fix: <一句话说清改了什么、为什么>
79
119
  - **JS 必须过 ES5 门禁**:不用 `const` `let`、箭头函数、模板串、`class`、`async`、
80
120
  `?.`、`??`。范围是 `01-tokens` ~ `09-assets`(`es5-gate.py` 守着)。
81
121
  - **状态写在 DOM 属性上**,不写在 class 里(INVARIANT I-8)。
82
- - **CSS 用 `@layer` 吗?** 现在不用,理由与切换判据见 `docs/GUIDANCE.md`。
122
+ - **CSS 用 `@layer` 吗?** 现在不用,理由与切换判据见 `docs/CONVENTIONS.md` 的「不用 `@layer`」一节。
83
123
  - 注释写**为什么**,不写是什么。取舍理由、踩过的坑、放弃的方案 ——
84
124
  这些半年后有用;`.foo { color: red }` 没有。
85
125
 
package/README.zh-CN.md CHANGED
@@ -69,8 +69,8 @@ npm install github:owain323/frontend-lib
69
69
  ```
70
70
 
71
71
  > 历史说明:0.9.0 曾发布成功、两分钟后被误删(`ERRORS.md` E55);此后一段时间
72
- > registry 上没有可安装版本,靠 GitHub Release 附件分发(`docs/RELEASING.md`
73
- > 「发布受阻时」)。自 0.11.2 起这条通路已恢复。
72
+ > registry 上没有可安装版本,靠 GitHub Release 附件分发(当时的分发路径记在
73
+ > `CHANGELOG.md` 的 0.11.2 一节)。自 0.11.2 起这条通路已恢复。
74
74
 
75
75
  **② 源码复制**(不想装依赖):把 `01-tokens/` 和需要的组件目录复制进你的项目。
76
76
  ⚠️ 复制 `dist/` 里的版本,**别复制源码版** —— 源码版有一半以上是给人和 AI 读的注释
@@ -343,7 +343,7 @@ import 'agentcss/dist/02-primitives/button/button.css';
343
343
  | `ai/` | **机器可读契约**:`components.json` / `tokens.json` / schema / 校验器 / 命令行 |
344
344
  | `adapters/` | 场景适配层(如演示文稿),**不进核心** |
345
345
  | `benchmark/` | 兼容性基准(10 类编辑意图,当前无模型实测记录) |
346
- | `docs/` | `INVARIANT.md`(不变式)· `GUIDANCE.md`(建议)· `BENCHMARK.md`(对标) |
346
+ | `docs/` | `INVARIANT.md`(不变式)· `CONVENTIONS.md`(全局约定)· `ACCESSIBILITY.md`(无障碍)· `AI-CONTRACT.md`(与 AI 协作的契约)· `LAYER-BASELINE.md`(引擎能力基线) |
347
347
  | `types/` | TypeScript 类型定义(描述 `window` 上的全局对象) |
348
348
 
349
349
  ## 示例与验证状态
@@ -388,7 +388,9 @@ bash 05-audit/check-all.sh # 全量门禁约 130 项,本机约 8 分钟
388
388
  ### 经过实际验证的
389
389
 
390
390
  - 桌面 Chrome / Edge / Firefox,跑过全量门禁(含真浏览器渲染与无障碍扫描)。
391
- - 视觉基线在 `10-review/shots/_baseline/`,由 `shot-baseline` 门禁逐张比对。
391
+ - 视觉基线在 `10-review/shots/_baseline-<平台>/`(本库字体走系统栈,
392
+ 渲染结果跟操作系统绑定,所以**按平台各存一套**),由 `shot-baseline` 门禁逐张比对。
393
+
392
394
  - 每个组件的键盘路径 / ARIA / 对比度都有契约守着(`05-audit/` 下百余道检查)。
393
395
  - 数据语义有机械断言:例如科学绘图的「缺失值不被当作零」、
394
396
  表格的「不按数值大小猜百分比」,都有**反例对照**证明判据有效。
@@ -474,10 +476,10 @@ node ai/cli.js diff a.json b.json # 两份契约文档按语义 id
474
476
  |---|---|---|
475
477
  | **不变式** | `docs/INVARIANT.md` | 客观事实,永不变。有门禁守边界 |
476
478
  | **契约** | `ai/*.json` + `API.md` | 约定,随版本变 |
477
- | **建议** | `docs/GUIDANCE.md` | 工作建议,可整份推翻 |
479
+ | **建议** | `CONTRIBUTING.md` | 工作方式建议,可整份推翻 |
478
480
 
479
481
  约束要放宽时动的是**建议层**,不变式一个字都不用动。
480
- 对标了谁、故意不学谁,见 `docs/BENCHMARK.md`。
482
+ 对标了谁、故意不学谁,散见 `CHANGELOG.md`(每版一节的取舍)与各组件的 README(每份都写了「来源」一栏)。
481
483
 
482
484
  ## 可访问性
483
485
 
@@ -499,9 +501,9 @@ npm run benchmark # 兼容性基准
499
501
 
500
502
  改代码时跑 `gate:fast` 即可;**提交前**再跑一次 `gate`。
501
503
 
502
- 类型声明在 `types/index.d.ts`。改动它之前先看
503
- [docs/ROADMAP.md](docs/ROADMAP.md) 里的「契约类事项」——
504
- 那几项没做完之前,本库还不适合作为长期依赖引入。
504
+ 类型声明在 `types/index.d.ts`。改动它之前先看 `API.md` 的「稳定程度约定」,
505
+ 以及 `ai/components.json` 里标 `beta` 的那几个组件 ——
506
+ 它们定稿之前,那几部分不适合作为长期依赖引入。
505
507
 
506
508
  ## 许可
507
509
 
package/START-HERE.md CHANGED
@@ -34,7 +34,7 @@
34
34
 
35
35
  | 组件 | 文件 | 行数 | JS | 什么时候要 |
36
36
  |---|---|---|---|---|
37
- | 按钮 | `02-primitives/button/button.css` | 353 | — | 几乎总要 |
37
+ | 按钮 | `02-primitives/button/button.css` | 357 | — | 几乎总要 |
38
38
  | 输入框 | `02-primitives/input/input.css` | 239 | — | 有表单就要 |
39
39
  | 标签 / 徽章 | `02-primitives/badge/badge.css` | 141 | — | 有状态标记就要("已披露""停牌") |
40
40
  | 卡片 | `02-primitives/card/card.css` | 246 | — | 有分组内容就要 |
@@ -52,7 +52,7 @@
52
52
  | 动作菜单(beta) | `03-patterns/dropdown/dropdown.css` | 135 | **必需 451** (`dropdown.js`) | 要一组动作(新建 / 导出 / 删除)就要 |
53
53
  | 文字提示 | `03-patterns/tooltip/tooltip.css` | 89 | **必需 288** (`tooltip.js`) | 图标按钮或缩写需要一句话解释就要 |
54
54
  | 分页 | `03-patterns/pagination/pagination.css` | 195 | **必需 395** (`pagination.js`) | 数据多到要翻页就要 |
55
- | 树形 | `03-patterns/tree/tree.css` | 113 | **必需 456** (`tree.js`) | 层级数据(文件 / 组织 / 分类)要展开就要 |
55
+ | 树形 | `03-patterns/tree/tree.css` | 122 | **必需 464** (`tree.js`) | 层级数据(文件 / 组织 / 分类)要展开就要 |
56
56
  | 侧边抽屉(beta) | `03-patterns/drawer/drawer.css` | 129 | **必需 258** (`drawer.js`) | 要从侧边滑出面板就要 |
57
57
  | 弹窗 / 吐司(beta) | `03-patterns/overlay/overlay.css` | 319 | **必需 563** (`overlay.js`) | 需要弹层(吐司 / 模态)就要 |
58
58
  | 状态(空 / 加载 / 错 / 成功) | `03-patterns/states/states.css` | 252 | — | **凡是会异步取数就要** |
package/SUPPORT.md CHANGED
@@ -24,7 +24,8 @@
24
24
 
25
25
  ## 自动化覆盖的环境
26
26
 
27
- 证据文件:`10-review/engine-evidence.json`(由 `05-audit/engine-gate.js` 生成,可复跑)。
27
+ 下面这张表由 `05-audit/engine-gate.js` 在本机跑出来:`node 05-audit/engine-gate.js`。
28
+ 产物是**你这台机器**的结果,不入库,也不该入库 —— 别人的环境不是你的环境。
28
29
 
29
30
  | 平台 | 引擎 | 状态 | 覆盖内容 |
30
31
  |---|---|---|---|
package/ai/START-HERE.md CHANGED
@@ -92,9 +92,9 @@ node ai/cli.js check <doc.json> # 校验契约文档(读 JSON,不解析原
92
92
  |---|---|---|
93
93
  | 不变式 | `docs/INVARIANT.md` | 客观事实,有门禁守边界 |
94
94
  | 契约 | `ai/*.json` | 版本化约定 |
95
- | 建议 | `docs/GUIDANCE.md` | 工作建议,可整份推翻 |
95
+ | 建议 | `CONTRIBUTING.md` | 工作方式建议,可整份推翻 |
96
96
 
97
- > `docs/GUIDANCE.md` 里「贴进 system prompt 的极简版」的适用范围是
97
+ > 本文件(就是那份贴进 system prompt 的极简版)的适用范围是
98
98
  > **维护 frontend-lib 自身或严格按本库范式写页面**,不是所有 HTML 任务的通用规则。
99
99
 
100
100
  ## 7 · 三句话版本
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
3
  "generatedBy": "05-audit/gen-ai-contract.py",
4
- "libraryVersion": "0.11.7",
4
+ "libraryVersion": "0.11.9",
5
5
  "$comment": "机器可读的组件契约。slots = BEM 元素名(在本库里元素名就是插槽名),states.enum = 组件会在 DOM 上写出的 data-* 取值,states.legacyClass = 仍未收敛到 data 属性的历史状态类(INVARIANT I-8 未达标)。",
6
6
  "maturityCriteria": {
7
7
  "alpha": "缺 demo.html 或缺 05-audit 下的专属门禁 ⇒ 还没被机器验证过,不要用",
@@ -990,7 +990,8 @@
990
990
  },
991
991
  "aria": {
992
992
  "roles": [
993
- "group"
993
+ "group",
994
+ "treeitem"
994
995
  ],
995
996
  "attrs": [
996
997
  "expanded",
@@ -1 +1 @@
1
- .btn {display:inline-grid;grid-template-areas:"stack";place-items:center;font-family:inherit;font-size:var(--fs-sm);font-weight:500;line-height:1;white-space:nowrap;min-height:44px;padding:0 var(--sp-5);border:1px solid transparent;border-radius:var(--r-sm);cursor:pointer;user-select:none;text-decoration:none;overflow:hidden;max-width:100%;transition:background-color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out);}.btn.btn--sm {position:relative;min-height:var(--sz-touch-sm, 32px);padding:0 var(--sp-3);font-size:var(--fs-xs);}.btn.btn--sm::after {content:"";position:absolute;inset-inline-start:0;inset-inline-end:0;top:50%;height:var(--sz-touch, 44px);transform:translateY(-50%);}.btn.btn--md {min-height:44px;padding:0 var(--sp-5);}.btn.btn--lg {min-height:52px;padding:0 var(--sp-6);font-size:var(--fs-base);}.btn--primary {background:var(--accent);color:var(--text-on-accent);}@media (hover:hover) and (pointer:fine) {.btn--primary:hover {background:var(--accent-hover);}}.btn--secondary {background:var(--surface);color:var(--text-primary);border-color:var(--border-control);}@media (hover:hover) and (pointer:fine) {.btn--secondary:hover {background:var(--surface-sunken);border-color:var(--text-tertiary);}}.btn--ghost {background:transparent;color:var(--text-secondary);}@media (hover:hover) and (pointer:fine) {.btn--ghost:hover {background:var(--surface-sunken);color:var(--text-primary);}}.btn--danger {background:var(--danger);color:var(--text-on-accent);}@media (hover:hover) and (pointer:fine) {.btn--danger:hover {background:var(--danger-hover);}}.btn:active {transform:translateY(var(--lift-sm));}.btn:focus-visible {outline:2px solid var(--accent);outline-offset:2px;}.btn:disabled, .btn[aria-disabled="true"] {background:var(--surface-sunken);color:var(--text-tertiary);border-color:var(--border-decor);cursor:not-allowed;}@media (hover:hover) and (pointer:fine) {.btn:disabled:hover, .btn[aria-disabled="true"]:hover {background:var(--surface-sunken);transform:none;}}.btn:disabled:active, .btn[aria-disabled="true"]:active {transform:none;}.btn[aria-busy="true"] {cursor:progress;pointer-events:none;}.btn__label {grid-area:stack;min-width:0;}.btn[aria-busy="true"] .btn__label {opacity:0;}.btn__spinner {grid-area:stack;opacity:0;width:16px;height:16px;border:2px solid currentColor;border-top-color:transparent;border-radius:50%;animation:btn-spin 700ms linear infinite;}.btn[aria-busy="true"] .btn__spinner {opacity:1;}@keyframes btn-spin {to {transform:rotate(360deg);}}@media (prefers-reduced-motion:reduce) {.btn__spinner {animation-duration:1.2s !important;}}.btn[data-state="success"] {background:var(--success);color:var(--text-on-accent);}.btn[data-state="success"] .btn__label {opacity:1;}.btn[data-state="success"] .btn__spinner {opacity:0;}.btn--icon {width:44px;padding:0;}.btn--icon.btn--sm {width:32px;}.btn--icon.btn--lg {width:52px;}.btn--block {display:flex;width:100%;}
1
+ .btn {display:inline-grid;grid-template-areas:"stack";place-items:center;font-family:inherit;font-size:var(--fs-sm);font-weight:500;line-height:1;white-space:nowrap;min-height:44px;padding:0 var(--sp-5);border:1px solid transparent;border-radius:var(--r-sm);cursor:pointer;user-select:none;text-decoration:none;min-width:max-content;transition:background-color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out);}.btn.btn--sm {position:relative;min-height:var(--sz-touch-sm, 32px);padding:0 var(--sp-3);font-size:var(--fs-xs);}.btn.btn--sm::after {content:"";position:absolute;inset-inline-start:0;inset-inline-end:0;top:50%;height:var(--sz-touch, 44px);transform:translateY(-50%);}.btn.btn--md {min-height:44px;padding:0 var(--sp-5);}.btn.btn--lg {min-height:52px;padding:0 var(--sp-6);font-size:var(--fs-base);}.btn--primary {background:var(--accent);color:var(--text-on-accent);}@media (hover:hover) and (pointer:fine) {.btn--primary:hover {background:var(--accent-hover);}}.btn--secondary {background:var(--surface);color:var(--text-primary);border-color:var(--border-control);}@media (hover:hover) and (pointer:fine) {.btn--secondary:hover {background:var(--surface-sunken);border-color:var(--text-tertiary);}}.btn--ghost {background:transparent;color:var(--text-secondary);}@media (hover:hover) and (pointer:fine) {.btn--ghost:hover {background:var(--surface-sunken);color:var(--text-primary);}}.btn--danger {background:var(--danger);color:var(--text-on-accent);}@media (hover:hover) and (pointer:fine) {.btn--danger:hover {background:var(--danger-hover);}}.btn:active {transform:translateY(var(--lift-sm));}.btn:focus-visible {outline:2px solid var(--accent);outline-offset:2px;}.btn:disabled, .btn[aria-disabled="true"] {background:var(--surface-sunken);color:var(--text-tertiary);border-color:var(--border-decor);cursor:not-allowed;}@media (hover:hover) and (pointer:fine) {.btn:disabled:hover, .btn[aria-disabled="true"]:hover {background:var(--surface-sunken);transform:none;}}.btn:disabled:active, .btn[aria-disabled="true"]:active {transform:none;}.btn[aria-busy="true"] {cursor:progress;pointer-events:none;}.btn__label {grid-area:stack;min-width:0;}.btn[aria-busy="true"] .btn__label {opacity:0;}.btn__spinner {grid-area:stack;opacity:0;width:16px;height:16px;border:2px solid currentColor;border-top-color:transparent;border-radius:50%;animation:btn-spin 700ms linear infinite;}.btn[aria-busy="true"] .btn__spinner {opacity:1;}@keyframes btn-spin {to {transform:rotate(360deg);}}@media (prefers-reduced-motion:reduce) {.btn__spinner {animation-duration:1.2s !important;}}.btn[data-state="success"] {background:var(--success);color:var(--text-on-accent);}.btn[data-state="success"] .btn__label {opacity:1;}.btn[data-state="success"] .btn__spinner {opacity:0;}.btn--icon {width:44px;padding:0;}.btn--icon.btn--sm {width:32px;}.btn--icon.btn--lg {width:52px;}.btn--block {display:flex;width:100%;}
@@ -1 +1 @@
1
- .tree {font-size:var(--fs-sm);color:var(--text-primary);outline:none;}.tree ul {margin:0;padding:0;list-style:none;}.tree__item {position:relative;}.tree__node {display:flex;align-items:center;gap:var(--sp-2);min-height:32px;padding:0 var(--sp-2);border-radius:var(--r-xs);cursor:pointer;user-select:none;}.tree__node:focus-visible {outline:var(--focus-ring-width) solid var(--focus-ring-color);outline-offset:calc(-1 * var(--focus-ring-width));}@media (hover:hover) and (pointer:fine) {.tree__node:hover {background:var(--surface-sunken);}}.tree__node[aria-selected="true"] {background:var(--accent-soft);color:var(--text-primary);font-weight:500;}.tree__marker {flex:0 0 auto;width:1em;font-size:.8em;color:var(--text-tertiary);text-align:center;}.tree__item[aria-expanded="true"] > .tree__node .tree__marker::before {content:"\25BC";}.tree__item[aria-expanded="false"] > .tree__node .tree__marker::before {content:"\25B6";}.tree__item:not([aria-expanded]) > .tree__node .tree__marker::before {content:"\00B7";}.tree ul ul {padding-inline-start:var(--sp-5);border-inline-start:1px solid var(--border-decor);margin-inline-start:var(--sp-4);}.tree__label {flex:1 1 auto;}.tree__count {flex:0 0 auto;padding:0 var(--sp-1);font-size:var(--fs-xs);font-family:var(--mono);font-variant-numeric:tabular-nums;color:var(--text-tertiary);background:var(--surface-sunken);border-radius:var(--r-pill);}@media (prefers-reduced-motion:reduce) {.tree__marker {transition:none;}}
1
+ .tree {font-size:var(--fs-sm);color:var(--text-primary);outline:none;}.tree ul {margin:0;padding:0;list-style:none;}.tree__item {position:relative;}.tree__node {display:flex;align-items:center;gap:var(--sp-2);min-height:32px;padding:0 var(--sp-2);border-radius:var(--r-xs);cursor:pointer;user-select:none;}.tree__node:focus-visible {outline:var(--focus-ring-width) solid var(--focus-ring-color);outline-offset:calc(-1 * var(--focus-ring-width));}@media (hover:hover) and (pointer:fine) {.tree__node:hover {background:var(--surface-sunken);}}.tree__node[aria-selected="true"] {background:var(--accent-soft);color:var(--text-primary);font-weight:500;}.tree__marker {flex:0 0 auto;width:1em;font-size:.8em;color:var(--text-tertiary);text-align:center;}.tree__item[aria-expanded="true"] > .tree__node .tree__marker::before {content:"\25BC";}.tree__item[aria-expanded="false"] > .tree__node .tree__marker::before {content:"\25B6";}.tree__item:not([aria-expanded]) > .tree__node .tree__marker::before {content:"\00B7";}[dir="rtl"] .tree__item[aria-expanded="false"] > .tree__node .tree__marker::before {content:"\25C0";}.tree ul ul {padding-inline-start:var(--sp-5);border-inline-start:1px solid var(--border-decor);margin-inline-start:var(--sp-4);}.tree__label {flex:1 1 auto;}.tree__count {flex:0 0 auto;padding:0 var(--sp-1);font-size:var(--fs-xs);font-family:var(--mono);font-variant-numeric:tabular-nums;color:var(--text-tertiary);background:var(--surface-sunken);border-radius:var(--r-pill);}@media (prefers-reduced-motion:reduce) {.tree__marker {transition:none;}}
@@ -297,9 +297,9 @@ if (it.ul && it.li.getAttribute('aria-expanded') === 'true') {
297
297
  setExpanded(it, false);
298
298
  } else {
299
299
 
300
- for (var i = cur; i > 0; i--) {
301
- var cand = visible()[i];
302
- if (it.li.contains(cand.li)) { focusAt(i); return; }
300
+ var vis = visible();
301
+ for (var i = cur - 1; i >= 0; i--) {
302
+ if (vis[i].li.contains(it.li)) { focusAt(i); return; }
303
303
  }
304
304
  }
305
305
  return;
package/dist/COSTS.md CHANGED
@@ -19,7 +19,7 @@
19
19
  | 档 | 组件 | tokens(必付) | CSS | JS | **合计** | 源码版合计 | 差 |
20
20
  |---|---|---:|---:|---:|---:|---:|---:|
21
21
  | primitive | `badge` | 2.6 KB | 0.5 KB | — | **3.1 KB** | 20.8 KB | −17.7 KB |
22
- | primitive | `button` | 2.6 KB | 1.0 KB | — | **3.6 KB** | 24.2 KB | −20.6 KB |
22
+ | primitive | `button` | 2.6 KB | 1.0 KB | — | **3.6 KB** | 24.4 KB | −20.8 KB |
23
23
  | primitive | `card` | 2.6 KB | 0.9 KB | — | **3.6 KB** | 21.6 KB | −18.0 KB |
24
24
  | primitive | `choice` | 2.6 KB | 1.1 KB | — | **3.7 KB** | 23.4 KB | −19.7 KB |
25
25
  | primitive | `combobox` | 2.6 KB | 1.0 KB | 5.4 KB | **9.0 KB** | 27.7 KB | −18.8 KB |
@@ -44,12 +44,12 @@
44
44
  | pattern | `states` | 2.6 KB | 0.8 KB | — | **3.5 KB** | 22.1 KB | −18.6 KB |
45
45
  | pattern | `tabs` | 2.6 KB | 0.7 KB | 2.0 KB | **5.3 KB** | 25.0 KB | −19.7 KB |
46
46
  | pattern | `tooltip` | 2.6 KB | 0.5 KB | 1.9 KB | **5.0 KB** | 24.7 KB | −19.7 KB |
47
- | pattern | `tree` | 2.6 KB | 0.6 KB | 2.9 KB | **6.2 KB** | 25.6 KB | −19.5 KB |
47
+ | pattern | `tree` | 2.6 KB | 0.6 KB | 2.9 KB | **6.2 KB** | 26.2 KB | −20.1 KB |
48
48
 
49
49
  ## 三、全库
50
50
 
51
51
  - 全部组件都用上(min,含 tokens 一份):**60.0 KB**
52
- - 源码版(带注释)全库 gzip:288.2 KB
52
+ - 源码版(带注释)全库 gzip:289.0 KB
53
53
 
54
54
  > ⚠️ 源码版那个数字**不是**要付的钱:它包含我们的注释(全库 CSS 有 66% 是注释,字节口径)。
55
55
  > 它的用途只有一个 —— 让人看清"dist 到底省了多少"。
@@ -0,0 +1,86 @@
1
+ # 无障碍人工测试协议(读屏)
2
+
3
+ **为什么需要这份东西**:自动检查(axe / 可访问性树)能证明"每个控件有名字、顺序稳定",
4
+ 但证明不了"念出来是否**合理**"。这一页把这件事变成**可执行、可复现、可填表**的动作,
5
+ 时长约 20 分钟。
6
+
7
+ **当前状态:未执行。** 下面那张结果表是空的 —— 这不是省略,是事实。
8
+ 跑完并填上之后,[无障碍一致性声明](ACCESSIBILITY.md) 里"没有真机读屏记录"那一条才能划掉。
9
+
10
+ ---
11
+
12
+ ## 一 · 准备
13
+
14
+ | 路径 | 系统 | 读屏 | 浏览器 |
15
+ |---|---|---|---|
16
+ | A | Windows 10/11 | [NVDA](https://www.nvaccess.org/download/)(免费) | Chrome 或 Edge |
17
+ | B | macOS | VoiceOver(系统自带,`Cmd + F5`) | Safari |
18
+
19
+ 被测页面(本地起一个静态服务,仓库根目录执行):
20
+
21
+ ```bash
22
+ python3 -m http.server 8000 --bind 127.0.0.1
23
+ # 然后访问 http://127.0.0.1:8000/02-primitives/button/demo.html
24
+ ```
25
+
26
+ 要测的 6 个页面(一个组件一类交互语义,覆盖主要风险):
27
+
28
+ | # | 页面 | 它想证明什么 |
29
+ |---|---|---|
30
+ | 1 | `02-primitives/button/demo.html` | 按钮的名字念得出、禁用与加载态念得对 |
31
+ | 2 | `02-primitives/input/demo.html` | 标签与错误说明能被一起念出来 |
32
+ | 3 | `02-primitives/choice/demo.html` | 单选 / 复选的分组关系念得对 |
33
+ | 4 | `03-patterns/tabs/demo.html` | 标签页的选中态与面板归属念得对 |
34
+ | 5 | `03-patterns/overlay/demo.html` | 对话框打开时焦点进入、关闭后归还 |
35
+ | 6 | `04-recipes/table/demo.html` | 表头与单元格的关联念得出来 |
36
+
37
+ ---
38
+
39
+ ## 二 · 每页只做这 5 件事(NVDA 键位在括号里)
40
+
41
+ 1. **从页面顶部按 Tab 走一遍**,听到的每个控件都要有**说得通的名字**。
42
+ 听到 "button"、"未标记"、"undefined"、只有图标名 ⇒ ❌ 记下来。
43
+ 2. **用读屏的"下一个标题"跳(NVDA `H`)**,标题层级应当是 1 → 2 → 3,不跳级。
44
+ 3. **把焦点放到一个禁用控件上**,它应当被念成"不可用"(而不是干脆跳过或念成正常按钮)。
45
+ 4. **触发表单错误**(页面里带校验的那块),错误文字应当被**紧跟控件**念出,
46
+ 不是只在屏幕右上角飘一条。
47
+ 5. **打开一个浮层(对话框 / 抽屉)**:焦点应进入浮层内部,`Esc` 关闭后
48
+ 焦点应回到**打开它的那个按钮**。
49
+
50
+ > 判读标准只有一条:**不看屏幕,只听**。听到的内容能不能让你知道该做什么。
51
+ > 一旦你需要看屏幕才能确定发生了什么,那一处就是缺陷。
52
+
53
+ ---
54
+
55
+ ## 三 · 结果表(填这里)
56
+
57
+ | 页面 | ① 名字 | ② 标题层级 | ③ 禁用态 | ④ 错误说明 | ⑤ 浮层焦点 | 备注(听到了什么) |
58
+ |---|---|---|---|---|---|---|
59
+ | button | 未执行 | 未执行 | 未执行 | — | — | |
60
+ | input | 未执行 | 未执行 | 未执行 | 未执行 | — | |
61
+ | choice | 未执行 | 未执行 | 未执行 | — | — | |
62
+ | tabs | 未执行 | 未执行 | — | — | — | |
63
+ | overlay | 未执行 | 未执行 | — | — | 未执行 | |
64
+ | table | 未执行 | 未执行 | — | — | — | |
65
+
66
+ 状态只填三种:**通过** / **失败(附听到的内容)** / **未执行**。
67
+ "大概没问题"不属于这三种,不要写。
68
+
69
+ 填写人 / 日期 / 系统与读屏版本:`____ / ____ / ____`
70
+
71
+ ---
72
+
73
+ ## 四 · 跑完之后
74
+
75
+ 1. 失败项开 issue,标题用 `a11y: <控件> <现象>`,正文贴上"听到的内容"与"期望的内容"。
76
+ 2. 把结果表更新进本页,并在 [无障碍一致性声明](ACCESSIBILITY.md) 第二节把
77
+ "人工(尚未执行)"改成实际结论。
78
+ 3. 声明里第 3 节「已知缺口」第 1 条据此划掉 —— **只有在这一步之后**才允许改。
79
+
80
+ ---
81
+
82
+ ## 五 · 为什么这份协议要写进仓库
83
+
84
+ 因为"我们打算做读屏测试"和"我们做过读屏测试"在仓库里长得一模一样 ——
85
+ 除非有一张**带状态列的表**。这张表现在全部写着"未执行",
86
+ 它记录的不是进度,而是**边界**:本库目前能承诺的东西到哪为止。
@@ -0,0 +1,153 @@
1
+ # 无障碍一致性声明
2
+
3
+ **目标等级:WCAG 2.2 Level AA。** 本文按 **成功准则(Success Criterion)逐条**说明
4
+ 状态、证据来源与缺口 —— 不是"我们很重视无障碍",而是"哪一条过、哪一条没过、凭什么"。
5
+
6
+ | 项目 | 值 |
7
+ |---|---|
8
+ | 声明适用版本 | agentcss `0.11.9` |
9
+ | 声明日期 | 2026-10-29 |
10
+ | 复核节奏 | 每次发版前重跑(`npm run gate` 内含全部证据) |
11
+ | 范围 | 本库交付的 CSS / JS、35 个组件的 `demo.html`、以及文档站生成的页面 |
12
+ | 不在范围 | 使用者拿这些组件拼出来的业务页面(那是你的无障碍责任,不是库的) |
13
+
14
+ 判读方式只有一句:**表里写「通过」的,下面都给出了可复现的命令;写「部分」或「未验」的,
15
+ 就是没做到,不要按"应该没问题"理解。**
16
+
17
+ ---
18
+
19
+ ## 一 · 逐条状态
20
+
21
+ 图例:**✅ 通过**(有判据且为绿)· **◐ 部分**(做了但有明确边界)· **✗ 未验**(没有证据)·
22
+ **— 不适用**(本库不含该内容,理由写在括号里)
23
+
24
+ ### 1. 可感知
25
+
26
+ | SC | 名称 | 状态 | 证据 / 边界 |
27
+ |---|---|---|---|
28
+ | 1.1.1 | 非文本内容 | ◐ | 图标一律 `aria-hidden`,语义由文字或 `aria-label` 承担;图表类组件要求调用方给替代文本,**库替不了使用者** |
29
+ | 1.3.1 | 信息与关系 | ✅ | `a11y-scan`(axe)+ 页面结构判据:单一 `h1`、`main`、`lang`、标题不跳级 |
30
+ | 1.3.2 | 有意义的顺序 | ✅ | DOM 顺序 = 视觉顺序,无 `order` / 绝对定位重排 |
31
+ | 1.3.3 | 感官特性 | ✅ | 说明文字不以"左边的按钮""圆形图标"指代 |
32
+ | 1.3.4 | 方向 | ✅ | 无 `orientation` 锁定,横竖屏均可用 |
33
+ | 1.3.5 | 输入目的识别 | — | 库不生成字段的 `autocomplete` 语义,由调用方按场景决定 |
34
+ | 1.4.1 | 颜色的使用 | ✅ | 状态均带文字或图标,不单靠颜色区分 |
35
+ | 1.4.3 | 对比度(最低) | ✅ | `contrast.py` 从令牌实算:正文 ≥ 4.5:1、控件 ≥ 3:1 |
36
+ | 1.4.4 | 调整文本大小 | ✅ | `zoom-reflow.js`:根字号拉到 200% 后,页面不横滚、文字不被裁 |
37
+ | 1.4.5 | 文本的图像 | — | 不使用文字截图 |
38
+ | 1.4.10 | 重排 | ✅ | `zoom-reflow.js`:320 CSS px 宽下 `<html>` 无横向滚动(数据表格允许内部横滚,属 WCAG 豁免的二维内容) |
39
+ | 1.4.11 | 非文本对比度 | ✅ | 控件边框 / 焦点环 / 图表色阶单独实算 |
40
+ | 1.4.12 | 文本间距 | ◐ | 组件不写死行高与段距(都用令牌),但**未**按"行高 1.5 倍 / 段距 2 倍"的极端注入逐组件验证 |
41
+ | 1.4.13 | 内容悬停或聚焦 | ◐ | tooltip 可 Esc 关闭、可悬停保持;**未**验证"可选中提示内文字"这一分支 |
42
+ | 2.1.1 | 键盘 | ✅ | `keyboard-nav.js`:Tab 顺序、Enter/Space、Esc 关闭 |
43
+ | 2.1.2 | 无键盘陷阱 | ✅ | `keyboard-nav.js`:浮层关闭后焦点可继续移动,不困在层内 |
44
+ | 2.1.4 | 字符快捷键 | — | 库不注册单字符快捷键 |
45
+ | 2.2.1 | 计时可调 | ◐ | toast 自动消失时长可配但有上限;**未**提供"暂停计时"控件 |
46
+ | 2.2.2 | 暂停、停止、隐藏 | ✅ | `motion.py --reduced-audit`:`prefers-reduced-motion` 下位移关闭,spinner 例外已登记 |
47
+ | 2.3.1 | 三次闪烁 | — | 无大于 3Hz 的闪烁内容 |
48
+ | 2.4.1 | 跳过区块 | ✅ | 文档站每页有跳转链接;组件自身不设地标 |
49
+ | 2.4.2 | 页面标题 | ✅ | 文档站逐页判 `<title>` 非空且与 `h1` 一致 |
50
+ | 2.4.3 | 焦点顺序 | ✅ | 同 2.1.1;无 `tabindex > 0` |
51
+ | 2.4.4 | 链接目的 | ✅ | 链接文字自身可读,无"点这里" |
52
+ | 2.4.6 | 标题和标签 | ✅ | 表单控件均有可见 `<label>`;`a11y-ax` 判每个可交互控件有非空名称 |
53
+ | 2.4.7 | 焦点可见 | ✅ | `focus-ring-check.js`:`:focus-visible` 有 2px 焦点环,非 `outline: none` |
54
+ | 2.4.11 | 焦点不被遮挡(2.2 新增 AA) | ✅ | `zoom-reflow.js`:焦点矩形与吸顶 / 吸底元素的重叠 ≥ 90% 即判失败 |
55
+ | 2.4.13 | 焦点外观(AAA,仅记录) | ◐ | 焦点环厚度与对比度达标,但未按 AAA 逐组件出数字 |
56
+ | 2.5.1 | 指针手势 | — | 无多点 / 路径手势 |
57
+ | 2.5.3 | 标签中的名称 | ◐ | 可见文字与 accessible name 一致;**未**做"语音输入可命中"的实测 |
58
+ | 2.5.5 | 目标大小(AAA,仅记录) | ✅ | 44×44 热区(`a11y.py` + 行为脚本) |
59
+ | 2.5.7 | 拖动移动(2.2 AA) | — | 无拖动型交互(滑块用键盘 / 点击调整) |
60
+ | 2.5.8 | 目标大小最小(2.2 AA) | ✅ | 最小 24×24,实测见 `a11y.py` |
61
+ | 3.1.1 | 语言 | ✅ | 文档站与 demo 均有 `lang`,双语页各自声明 |
62
+ | 3.1.2 | 部分语言 | — | 中文页面内无成段外语 |
63
+ | 3.2.1 | 焦点时 | ✅ | 焦点不触发上下文切换(无 focus 即跳转 / 即提交) |
64
+ | 3.2.2 | 输入时 | ✅ | 输入不自动提交;`select` 改变不跳页 |
65
+ | 3.2.6 | 一致的帮助(2.2 A) | — | 库无全局帮助入口 |
66
+ | 3.3.1 | 错误识别 | ✅ | `form-validation` 用文字说明错误,不只变色 |
67
+ | 3.3.2 | 标签或说明 | ✅ | 必填项有可见标记 + `required` |
68
+ | 3.3.7 | 冗余输入(2.2 A) | — | 由业务表单决定,库不承载 |
69
+ | 3.3.8 | 可访问的认证(2.2 AA) | — | 库不含登录流程 |
70
+ | 4.1.2 | 名称、角色、值 | ✅ | `a11y-ax.js`:10 个代表页的可访问性树逐节点判,每个可交互控件有非空名称;朗读序列存快照防回归 |
71
+ | 4.1.3 | 状态消息 | ◐ | toast / 表单错误用 `role="status"` / `alert`;**未**在真实读屏里验证播报时机 |
72
+
73
+ **统计:✅ 27 条 · ◐ 7 条 · ✗ 0 条 · — 12 条。**
74
+
75
+ ---
76
+
77
+ ## 二 · 证据是怎么来的(以及它盖不住什么)
78
+
79
+ ### 自动化(可复跑,命令都在 `05-audit/`)
80
+
81
+ | 手段 | 覆盖 | 最近结果 |
82
+ |---|---|---|
83
+ | `axe-core`(`a11y-scan.js`) | 38 页(35 组件 demo + 文档站) | **0 类违规**,991 条规则通过 |
84
+ | 反向控制(注入 5 类典型违规) | 证明上一条不是"没查" | 5 类全部抓到 |
85
+ | `a11y-ax.js` 可访问性树 | 10 个交互语义各异的页面 | 0 个无名控件;朗读序列存 `10-review/a11y-tree/` |
86
+ | `contrast.py` / `dark-contrast.js` | 亮 / 暗两套,逐对实算 | 正文 ≥ 4.5:1 |
87
+ | `zoom-reflow.js` | 38 页 × 3 条 AA 硬项 | 320px 不横滚 · 200% 不裁字 · 焦点不被遮挡 |
88
+ | `focus-ring-check` / `focus-return-check` / `keyboard-nav` | 焦点可见、焦点归还、键盘路径 | 全绿 |
89
+
90
+ ### 人工(**尚未执行** —— 这是本声明最大的空白)
91
+
92
+ 自动化的天花板必须说清楚:**axe 只能覆盖约 30–40% 的 WCAG 问题**,而且它听不见。
93
+ 以下三件事本库**没有**任何一条真实记录:
94
+
95
+ 1. **真机读屏**(NVDA / VoiceOver / TalkBack)——一条录音都没有。
96
+ 2. **真实键盘 + 读屏组合下的阅读顺序**是否符合预期(`a11y-ax` 只证明"顺序稳定",
97
+ 不证明"顺序正确"——后者需要人判断)。
98
+ 3. **9 处「待人工」结论**(例如图表的替代文本是否真的描述了数据)。
99
+
100
+ 为了把这块从"没有"变成"可执行",协议写在
101
+ [无障碍人工测试协议](A11Y-MANUAL-PROTOCOL.md):两条路径、约 20 分钟、结果直接填表。
102
+ **在你或志愿者跑完之前,本声明只代表"自动检查覆盖的部分是干净的"。**
103
+
104
+ ---
105
+
106
+ ## 三 · 已知缺口(按影响排序)
107
+
108
+ 1. **没有真机读屏记录**(同上)。这是与 Carbon / Spectrum 那类一致性报告差距最大的一处。
109
+ 2. **没有 VPAT / ACR 正式报告**。采购流程要的那种文档本库没有出;
110
+ 上面那张 SC 表可以当它的替代品,但它不是法律意义上的 ACR。
111
+ 3. **强制色板(forced-colors)只覆盖 6 个 CSS**,Windows 高对比度模式未逐组件验证。
112
+ 4. **RTL 已进渲染判据与视觉基线,但没进真机**:
113
+ `rtl-render.js` 判 37 页(RTL 不横滚 / 阿·希文字膨胀 1.3× 不破相 /
114
+ 方向性字形镜像),`shot-baseline.py --dir rtl` 有 13 张视觉基线
115
+ (覆盖"镜像错了但不溢出不改字形"那一类)。
116
+ **仍然缺的是**:母语 RTL 使用者的实际阅读体验、iOS/Android 真机 RTL 截图,
117
+ 以及「页面加载后再切 `dir`」时组件内部状态是否需要重算。
118
+ 最后这条的现状是:CSS 会立刻响应(树形箭头、栅格、滑入方向都靠 CSS),
119
+ 但凡是 JS 在挂载时把方向读进变量的地方都不会重算 —— 本库目前没有这种写法,
120
+ 这一点靠 `rtl-render` 的"挂载后再改 dir"路径间接守着。
121
+ 5. **1.4.12 文本间距**未按极端注入验证;**1.4.13** 未验证"提示内文字可选中"。
122
+ 6. **Android 系统 WebView 未验证**,真机矩阵里它是 ❌(见[浏览器支持矩阵](../SUPPORT.md))。
123
+ 7. **`prefers-contrast: more` 未单独验收**(只做了 `forced-colors`)。
124
+
125
+ ---
126
+
127
+ ## 四 · 自己验一遍
128
+
129
+ ```bash
130
+ # 一条命令跑完全部无障碍相关判据(含反向控制)
131
+ npm run gate:fast # 静态层:结构 / 契约 / 文档事实
132
+ node 05-audit/a11y-scan.js # axe(需要本地静态服务 :8000)
133
+ node 05-audit/a11y-ax.js # 可访问性树 + 名称判据
134
+ node 05-audit/zoom-reflow.js # 200% / 320px / 焦点遮挡
135
+ node 05-audit/rtl-render.js # RTL 渲染 + 阿/希膨胀 + 字形镜像
136
+ node 05-audit/a11y-ax.js --selftest # 证明上面那条判据抓得到问题
137
+ node 05-audit/zoom-reflow.js --selftest # 同上
138
+ node 05-audit/rtl-render.js --selftest # 同上
139
+ python 05-audit/shot-baseline.py --check --dir rtl # RTL 视觉基线
140
+ ```
141
+
142
+ `python3 05-audit/a11y.py --dir .` 会连 `node_modules` 一起扫,
143
+ 那里面的第三方 HTML 会报一堆与你无关的问题 —— 门禁里用的是逐目录调用,别用 `--dir .`。
144
+
145
+ ---
146
+
147
+ ## 五 · 报告问题
148
+
149
+ 无障碍问题按**缺陷**处理,不按"改进建议"处理。开 issue 写清三件事:
150
+ **控件 + 输入方式(键盘 / 读屏 / 触摸)+ 期望行为**。
151
+
152
+ 如果你能提供自己机器上的读屏结果(读到了什么、和预期差在哪),
153
+ 它的价值高于本页任何一条自动化结论 —— 那正是第 2 节里空着的那一块。
@@ -0,0 +1,194 @@
1
+ # 全局约定
2
+
3
+ 这些规则不属于任何一个组件:令牌怎么用、焦点环多大、动效多久、暗色怎么切、
4
+ 能不能写 `margin-left`。它们由 `01-tokens/` 里的几个全局文件实现,由一批门禁守住。
5
+ 本文是它们的**唯一说明书** —— 规则写在这里,判据在仓库里,两边一一对应。
6
+
7
+ 读法:如果你只想知道「我该写哪个变量」,看第一节和第二节;
8
+ 如果你想改这套东西,看第十节,那里列了每条规则由什么挡住。
9
+
10
+ ## 一 · 令牌分三层,你只用中间层
11
+
12
+ | 层 | 长什么样 | 谁写 | 谁用 |
13
+ |---|---|---|---|
14
+ | raw | `--raw-paper`、`--raw-text-primary` | 只有 `01-tokens/tokens.css` | 没人 —— 它是调色板原料 |
15
+ | 语义 | `--paper`、`--surface`、`--text-primary` | 只有 `01-tokens/tokens.css` | **你的代码用这一层** |
16
+ | 组件私有 | `--btn-pad` 之类 | 该组件自己的 CSS | 该组件自己 |
17
+
18
+ **为什么必须隔一层**:暗色不是"把颜色逐条改掉",而是**换掉 raw 的值,语义层不动**。
19
+ 组件里写 `--raw-*`,暗色就会漏掉它 —— 这也是 `dark-contrast.js` 会算两遍对比度的原因。
20
+ 组件与页面里**写死 `#hex`** 一律不许(`hygiene.py` 判)。
21
+
22
+ 常用语义令牌,括号里是对当前纸底 / 表面的**实算对比度**(不是估的):
23
+
24
+ | 令牌 | 用途 | 对比度 |
25
+ |---|---|---|
26
+ | `--paper` | 页面底色 | — |
27
+ | `--surface` / `--surface-raised` / `--surface-sunken` | 卡片 / 浮起 / 凹陷 | — |
28
+ | `--text-primary` | 正文 | 16.60 / 17.81 · AAA |
29
+ | `--text-secondary` | 次要说明 | 7.01 · AAA |
30
+ | `--text-tertiary` | 弱化文字 | 5.33 / 4.89 · AA |
31
+ | `--border-control` | 输入框等**控件**边框 | 3.19 / 3.42 · AA(大字与控件) |
32
+ | `--border-decor` | 纯装饰分隔线 | 1.18 · 不适用(WCAG 不强制装饰) |
33
+ | `--accent` / `--accent-soft` | 强调色 / 浅底与焦点环 | 见 `contrast.py` 输出 |
34
+
35
+ ⚠️ `--accent-soft` **不要拿来当正文背景**:它只保证"能看见有一块色",不保证字读得清。
36
+
37
+ ## 二 · 间距、圆角、层级:只准踩台阶
38
+
39
+ 间距是 8 档,前 3 档固定、后 5 档随视口缩放:
40
+
41
+ | 令牌 | 值 | 说明 |
42
+ |---|---|---|
43
+ | `--sp-1` / `--sp-2` / `--sp-3` | 4 / 8 / 12px | 组件内部微调 |
44
+ | `--sp-4` | `clamp(1rem, …, 1.25rem)` | 16 → 20px |
45
+ | `--sp-5` | `clamp(1.25rem, …, 1.75rem)` | 20 → 28px |
46
+ | `--sp-6` | `clamp(1.5rem, …, 2.25rem)` | 24 → 36px |
47
+ | `--sp-7` | `clamp(2rem, …, 3.5rem)` | 32 → 56px |
48
+ | `--sp-8` | `clamp(2.5rem, …, 4.5rem)` | 40 → 72px |
49
+
50
+ 圆角:`--r-xs 3` / `--r-sm 6` / `--r-md 10` / `--r-lg 14` / `--r-pill 999`(px)。
51
+ 层级:`--z-base 0` → `--z-sticky 10` → `--z-header 20` → `--z-overlay 30` →
52
+ `--z-modal 40` → `--z-toast 50`,另有 `--z-raised 5`(卡片悬停、内层可点元素)。
53
+
54
+ 写 `padding: 13px` 不算错,但它**不会跟着页面框一起变** —— 这就是为什么
55
+ 文档站曾经出现"改 `--sp-6`,35 页 padding-top 一动不动"(那时页面框被写了两份)。
56
+
57
+ ## 三 · 焦点环:`:focus-visible`,2px + 2px
58
+
59
+ ```css
60
+ :focus-visible {
61
+ outline: 2px solid var(--accent);
62
+ outline-offset: 2px;
63
+ border-radius: var(--r-sm);
64
+ }
65
+ ```
66
+
67
+ - **只用 `:focus-visible`,不用 `:focus`**。前者只给键盘用户看,鼠标点击不冒框。
68
+ 用 `:focus` 的后果是"点一下留个框",用户会以为界面卡住了。
69
+ - **禁止 `outline: none` 而不给替代指示** —— 这是无障碍事故,不是审美选择。
70
+ 真要自绘,用 `--focus-ring-width` / `--focus-ring-offset` / `--focus-ring-color`
71
+ 三个令牌,别自己写死像素。
72
+ - 深底上(暗色、实心按钮)加一圈内白环 `--focus-ring-inset`,否则焦点环与背景糊在一起。
73
+ - 打开弹层再关掉,**焦点必须回到触发它的元素**上:`focus-return-check.js` 判这一条。
74
+
75
+ ## 四 · 动效:三档时长,三条规定
76
+
77
+ | 令牌 | 值 | 用在哪 |
78
+ |---|---|---|
79
+ | `--dur-fast` | 120ms | 颜色、透明度、悬停 |
80
+ | `--dur-base` | 180ms | 位移、展开 |
81
+ | `--dur-slow` | 280ms | 大块入场、遮罩 |
82
+
83
+ 曲线三条:`--ease-out`(入场/展开)、`--ease-in-out`(屏上位移/折叠)、
84
+ `--ease-drawer`(抽屉式滑入)。超过 300ms 的 UI 动效会被读成"卡"。
85
+
86
+ 系统开了"减少动效"时,策略是**分档**的,不是一刀切:
87
+
88
+ 1. **位移 / 缩放 / 旋转 —— 关**。前庭不适的主要来源。
89
+ 2. **真实进度(spinner、加载态)—— 留**。停掉它,用户会以为没点上,从而重复提交;
90
+ 这比有动画更糟。目前登记的例外只有三处:`button`、`states`、`form-validation`。
91
+ 3. **颜色 / 透明度 —— 可以缩短到 `--dur-fast` 保留**,这一档不强制、也没有判据
92
+ (写了却没判据的规则,比没有规则更糟)。
93
+
94
+ 例外登记在 `ai/motion-policy.json`。**声明了却没实现 ⇒ 红;实现了却没声明 ⇒ 红。**
95
+ 这条双向判据是它存在的全部意义。
96
+
97
+ ## 五 · 暗色:三种状态,组件一律不许自判
98
+
99
+ | 使用者的意思 | 怎么表达 |
100
+ |---|---|
101
+ | 跟随系统 | 什么都不写(`color-scheme: light dark` 已声明) |
102
+ | 我要亮的 | `<html data-theme="light">` |
103
+ | 我要暗的 | `<html data-theme="dark">` |
104
+
105
+ 实现上有两个必须知道的坑:
106
+
107
+ - 系统暗色那一段的选择器是 `:root:not([data-theme="light"])`,**强制暗**另由
108
+ `:root[data-theme="dark"]` 那一段负责,两段互不干扰。
109
+ **少了 `:not(...)`**,系统开着暗色时使用者点"亮"仍然会看到暗的 ——
110
+ 三态开关里最容易被漏掉的那条路径(这也是为什么"强制亮"能压过系统暗色)。
111
+ - **组件里不许出现 `prefers-color-scheme`**。组件只认语义令牌,主题切换由
112
+ `tokens.css` 一处完成。组件自己判主题,等于给每种组件各开一个"主题分支",
113
+ 加一个主题就要改 35 个文件。
114
+
115
+ 暗色不等于"把浅色取反",三条已经踩过的坑:正文不用纯白(深底上字会发散)、
116
+ 凹陷面在暗色里要**比纸面略亮**(层级靠变亮,不是变暗)、滚动条 thumb 要更亮
117
+ (用浅色那档会变成"黑里有黑")。
118
+
119
+ ## 六 · 逻辑属性:不许写 `margin-left`
120
+
121
+ 交付的 43 个 CSS 里,物理方向属性(`margin-left` / `padding-left` / `left:`)**0 处**,
122
+ 全部用 `margin-inline-start` 这类逻辑属性。`rtl-check.js` 守这一条。
123
+
124
+ 绝对定位是唯一需要动脑的地方:
125
+
126
+ ```css
127
+ /* 错的:RTL 下浮层会跑到另一边 */
128
+ .tip { position: absolute; left: 0; }
129
+ /* 对的:两个方向都说清楚 */
130
+ .tip { position: absolute; inset-inline-start: 0; }
131
+ /* 确实要按物理方向时,显式覆盖 */
132
+ [dir="rtl"] .tip { left: auto; right: 0; }
133
+ ```
134
+
135
+ ## 七 · 不用 `@layer`;`!important` 必须说得清
136
+
137
+ 交付 CSS 里 `@layer` 是 **0 处**,这是决定,不是遗漏:目标环境里有 Chromium 97 的内核
138
+ (见[浏览器支持矩阵](../SUPPORT.md)),它不认识 `@layer` —— **整层样式会直接消失**。
139
+ 所以"未分层"是当前唯一安全的选择。`cascade-layer-gate.py` 把这条变成可判状态:
140
+ 一旦出现第一条 `@layer`,就要求三条前提的实测记录已落档,否则红。
141
+
142
+ `!important` 现有 10 处,**每一处都必须能被归类**:
143
+
144
+ | 类别 | 处数 | 为什么必须赢 |
145
+ |---|---|---|
146
+ | 无障碍偏好(`prefers-reduced-motion`) | 9 | 操作系统级偏好,组件无权推翻 |
147
+ | 正确性(`[hidden] { display: none }`) | 1 | 不赢就会"藏不住" |
148
+
149
+ 分不了类的 `!important` ⇒ 红。判据不是"清零"——清零会逼人删掉无障碍兜底。
150
+
151
+ ## 八 · 对比度与触控的硬线
152
+
153
+ - 正文 **4.5:1**,大字与**控件**边界 **3:1**,纯装饰不强制(但也不许拿来传信息)。
154
+ - 触控目标 **44×44**(WCAG 2.5.5)。视觉尺寸可以更小,热区要补足。
155
+ - **禁用态用明确色值,不用 `opacity`**。半透明会让文字对比度随底色漂移,
156
+ 在暗色下直接不达标;而且"看起来只是淡了点"没法表达"不能点"。
157
+
158
+ ## 九 · 覆盖策略:改令牌,不要改组件
159
+
160
+ ```css
161
+ /* 换品牌色:只改一个语义令牌,全站生效,暗色与焦点环自动跟上 */
162
+ :root { --accent: var(--brand-accent); }
163
+ ```
164
+
165
+ `--brand-accent` 由你自己定义(任意合法颜色值)。之所以不在这里写一个具体的十六进制:
166
+ 本库的对比度判据会把**文档里的色值**当成"内联副本"去核对,
167
+ 它分不清示例与真实用法 —— 与其教它开例外,不如让示例本来就不带字面值。
168
+
169
+ - 优先覆盖 **语义令牌**(第一节那张表)。
170
+ - 其次覆盖组件自己的槽位变量(如按钮的内边距),它们都以组件名为前缀。
171
+ - **不要把 `!important` 当覆盖手段**:赢了这一条,会把无障碍兜底一起压掉。
172
+ - 不要用更深的选择器去压(`.page .btn.btn--primary`)—— 那是把特异性当版本管理,
173
+ 下次改版必然打架。
174
+
175
+ ## 十 · 每条规则由什么挡住
176
+
177
+ 规则只有落到判据上才会被遵守。对应关系如下(文件都在 `05-audit/`):
178
+
179
+ | 规则 | 判据 |
180
+ |---|---|
181
+ | 不写死 `#hex`、不留调试残留 | `hygiene.py` |
182
+ | 组件之间不许 `@import` 互相依赖 | `css-independence.py` |
183
+ | 亮/暗两套对比度达标 | `contrast.py`、`dark-contrast.js`、`component-contrast.py` |
184
+ | 焦点环存在且不是 `outline:none` | `focus-ring-check.js` |
185
+ | 关掉弹层后焦点回到触发点 | `focus-return-check.js` |
186
+ | 键盘可达(Tab 顺序、Esc 关闭) | `keyboard-nav.js` |
187
+ | 减少动效策略双向一致 | `motion.py --reduced-audit` |
188
+ | 逻辑属性全覆盖 | `rtl-check.js` |
189
+ | `@layer` 计数与 `!important` 分类 | `cascade-layer-gate.py` |
190
+ | 触控目标 44px | `a11y.py`(静态) + 行为脚本(运行时) |
191
+ | 页面骨架只有一份(改令牌能牵动全部页面) | `skeleton-lever-check.js` |
192
+
193
+ 想加一条新规则?先在门禁里加判据、确认它会红,再写进本文 ——
194
+ **顺序反了就会得到一条"写在文档里、没人执行"的规则。**
package/docs/INVARIANT.md CHANGED
@@ -44,7 +44,7 @@
44
44
  |---|---|---|---|
45
45
  | **Invariant**(本文件) | 客观事实 | **永不变**(除非 CSS 规范变了) | 门禁 |
46
46
  | **Contract**(`ai/contract.schema.json`) | 版本化的接口约定 | 随版本变,**向后兼容** | 校验器 |
47
- | **Guidance**(`docs/GUIDANCE.md`) | 当前模型下的工作建议 | **随时可变,可整份推翻** | 人 |
47
+ | **Guidance**(`CONTRIBUTING.md`) | 当前模型下的工作建议 | **随时可变,可整份推翻** | 人 |
48
48
 
49
49
  ⇒ **判断一条内容该放哪层的唯一标准**:
50
50
  > 如果明天出现一个无限强的模型,这句话还成立吗?
@@ -93,7 +93,7 @@ a { color: red } /* 命中文档里每一个 <a>,无论它属于谁 */
93
93
  `@layer` 的层序优先于选择器特异性。
94
94
  (这也是为什么 Web Awesome 把全局样式分层——让使用者的 CSS 自动赢,不需要 `!important`。)
95
95
 
96
- - 本库未使用 `@layer`。理由、切换前提与就绪判据见 `docs/BENCHMARK.md` 的「级联分层的决定」一节。
96
+ - 本库未使用 `@layer`。理由、切换前提与就绪判据见 `docs/CONVENTIONS.md` 的「不用 `@layer`」一节。
97
97
 
98
98
  ### I-4 inline style 的优先级高于**一切**样式表
99
99
 
package/index.html CHANGED
@@ -92,7 +92,7 @@ li a:active { background: var(--surface-sunken); }
92
92
  </p>
93
93
  </main>
94
94
  <footer class="wrap">
95
- <p class="note">0.11.7 · 零第三方运行时依赖</p>
95
+ <p class="note">0.11.9 · 零第三方运行时依赖</p>
96
96
  </footer>
97
97
  </body>
98
98
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentcss",
3
- "version": "0.11.7",
3
+ "version": "0.11.9",
4
4
  "description": "移动 Web 组件库:CSS + 可选行为脚本,零第三方运行时依赖。样式用 bundler 引入,交互行为用 script 标签引入。",
5
5
  "keywords": [
6
6
  "css",