agentcss 0.11.8 → 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.
- package/02-primitives/button/button.css +7 -3
- package/03-patterns/tree/demo.html +9 -1
- package/03-patterns/tree/tree.css +9 -0
- package/03-patterns/tree/tree.js +12 -4
- package/CHANGELOG.md +83 -0
- package/CONTRIBUTING.md +37 -4
- package/README.zh-CN.md +3 -1
- package/START-HERE.md +2 -2
- package/SUPPORT.md +2 -1
- package/ai/components.json +3 -2
- package/dist/02-primitives/button/button.css +1 -1
- package/dist/03-patterns/tree/tree.css +1 -1
- package/dist/03-patterns/tree/tree.js +3 -3
- package/dist/COSTS.md +3 -3
- package/docs/A11Y-MANUAL-PROTOCOL.md +86 -0
- package/docs/ACCESSIBILITY.md +143 -66
- package/index.html +1 -1
- package/package.json +1 -1
|
@@ -58,9 +58,13 @@
|
|
|
58
58
|
user-select: none;
|
|
59
59
|
text-decoration: none;
|
|
60
60
|
|
|
61
|
-
/* 🔴
|
|
62
|
-
|
|
63
|
-
|
|
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.
|
|
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);
|
package/03-patterns/tree/tree.js
CHANGED
|
@@ -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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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,89 @@
|
|
|
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
|
+
|
|
19
102
|
## [0.11.8] - 2026-10-29
|
|
20
103
|
|
|
21
104
|
**补上文档站最大的真空**:令牌用法、焦点环、动效时长、暗色切换、逻辑属性这些
|
package/CONTRIBUTING.md
CHANGED
|
@@ -8,14 +8,47 @@ npm run gate # 全量:含浏览器类
|
|
|
8
8
|
npm run gate:fast # 只跑静态层,不用装浏览器
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
全量是 **
|
|
11
|
+
全量是 **190 道门禁**(本机约 12 分钟)。
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
环境依赖只有一套(Node + Chromium),缺了对应的门禁会**跳过**(跳过不算通过):
|
|
14
14
|
|
|
15
15
|
| 依赖 | 少了会怎样 | 装法 |
|
|
16
16
|
|---|---|---|
|
|
17
|
-
| Node + Chromium | `unit` / `responsive` / `firefox`
|
|
18
|
-
|
|
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` 只查物理属性,两者都看不见"镜像错了"。
|
|
19
52
|
|
|
20
53
|
⚠️ 上面的数字写在**代码块外**是有意的:`doc-facts-gate` 会核对
|
|
21
54
|
「文档里的门禁数」与 `check-all.sh` 的真实注册数,而**代码块内的内容被豁免**
|
package/README.zh-CN.md
CHANGED
|
@@ -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
|
|
391
|
+
- 视觉基线在 `10-review/shots/_baseline-<平台>/`(本库字体走系统栈,
|
|
392
|
+
渲染结果跟操作系统绑定,所以**按平台各存一套**),由 `shot-baseline` 门禁逐张比对。
|
|
393
|
+
|
|
392
394
|
- 每个组件的键盘路径 / ARIA / 对比度都有契约守着(`05-audit/` 下百余道检查)。
|
|
393
395
|
- 数据语义有机械断言:例如科学绘图的「缺失值不被当作零」、
|
|
394
396
|
表格的「不按数值大小猜百分比」,都有**反例对照**证明判据有效。
|
package/START-HERE.md
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
| 组件 | 文件 | 行数 | JS | 什么时候要 |
|
|
36
36
|
|---|---|---|---|---|
|
|
37
|
-
| 按钮 | `02-primitives/button/button.css` |
|
|
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` |
|
|
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
|
-
|
|
27
|
+
下面这张表由 `05-audit/engine-gate.js` 在本机跑出来:`node 05-audit/engine-gate.js`。
|
|
28
|
+
产物是**你这台机器**的结果,不入库,也不该入库 —— 别人的环境不是你的环境。
|
|
28
29
|
|
|
29
30
|
| 平台 | 引擎 | 状态 | 覆盖内容 |
|
|
30
31
|
|---|---|---|---|
|
package/ai/components.json
CHANGED
|
@@ -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.
|
|
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;
|
|
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
|
-
|
|
301
|
-
var
|
|
302
|
-
if (
|
|
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.
|
|
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** |
|
|
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:
|
|
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
|
+
它记录的不是进度,而是**边界**:本库目前能承诺的东西到哪为止。
|
package/docs/ACCESSIBILITY.md
CHANGED
|
@@ -1,76 +1,153 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
|
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
|
+
| 手段 | 覆盖 | 最近结果 |
|
|
11
82
|
|---|---|---|
|
|
12
|
-
|
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
|
17
|
-
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
+
## 四 · 自己验一遍
|
|
47
128
|
|
|
48
129
|
```bash
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
#
|
|
53
|
-
node 05-audit/
|
|
54
|
-
|
|
55
|
-
#
|
|
56
|
-
node 05-audit/
|
|
57
|
-
|
|
58
|
-
#
|
|
59
|
-
python3 05-audit/contrast.py
|
|
60
|
-
node 05-audit/dark-contrast.js
|
|
61
|
-
|
|
62
|
-
# 键盘与焦点
|
|
63
|
-
node 05-audit/keyboard-nav.js
|
|
64
|
-
node 05-audit/focus-ring-check.js
|
|
65
|
-
node 05-audit/focus-return-check.js
|
|
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 视觉基线
|
|
66
140
|
```
|
|
67
141
|
|
|
68
142
|
`python3 05-audit/a11y.py --dir .` 会连 `node_modules` 一起扫,
|
|
69
143
|
那里面的第三方 HTML 会报一堆与你无关的问题 —— 门禁里用的是逐目录调用,别用 `--dir .`。
|
|
70
144
|
|
|
71
|
-
|
|
145
|
+
---
|
|
72
146
|
|
|
73
|
-
|
|
147
|
+
## 五 · 报告问题
|
|
148
|
+
|
|
149
|
+
无障碍问题按**缺陷**处理,不按"改进建议"处理。开 issue 写清三件事:
|
|
74
150
|
**控件 + 输入方式(键盘 / 读屏 / 触摸)+ 期望行为**。
|
|
75
|
-
|
|
76
|
-
|
|
151
|
+
|
|
152
|
+
如果你能提供自己机器上的读屏结果(读到了什么、和预期差在哪),
|
|
153
|
+
它的价值高于本页任何一条自动化结论 —— 那正是第 2 节里空着的那一块。
|
package/index.html
CHANGED