agentcss 0.13.1 → 0.14.0
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/behavior/dismissable.js +31 -15
- package/02-primitives/combobox/combobox.js +6 -14
- package/02-primitives/popover/popover.js +6 -14
- package/02-primitives/select/README.md +5 -0
- package/02-primitives/select/select.js +6 -14
- package/03-patterns/drawer/drawer.js +86 -8
- package/03-patterns/dropdown/dropdown.js +13 -16
- package/03-patterns/nav/nav.js +6 -14
- package/03-patterns/overlay/overlay.js +85 -16
- package/03-patterns/tooltip/tooltip.js +6 -14
- package/04-recipes/table/table.css +19 -4
- package/CHANGELOG.md +149 -0
- package/CONTRIBUTING.md +1 -1
- package/README.zh-CN.md +1 -1
- package/START-HERE.md +10 -10
- package/SUPPORT.md +7 -1
- package/ai/START-HERE.md +1 -1
- package/ai/behaviors.json +8 -8
- package/ai/components.json +11 -17
- package/dist/01-tokens/behavior/dismissable.js +3 -1
- package/dist/02-primitives/combobox/combobox.js +3 -1
- package/dist/02-primitives/popover/popover.js +3 -1
- package/dist/02-primitives/select/select.js +3 -1
- package/dist/03-patterns/drawer/drawer.js +73 -7
- package/dist/03-patterns/dropdown/dropdown.js +5 -2
- package/dist/03-patterns/nav/nav.js +3 -1
- package/dist/03-patterns/overlay/overlay.js +72 -7
- package/dist/03-patterns/tooltip/tooltip.js +3 -1
- package/dist/04-recipes/table/table.css +1 -1
- package/dist/COSTS.md +9 -9
- package/docs/A11Y-MANUAL-PROTOCOL.md +104 -20
- package/docs/ACCESSIBILITY.md +26 -12
- package/docs/CONVENTIONS.md +9 -0
- package/index.html +1 -1
- package/package.json +1 -1
|
@@ -251,6 +251,69 @@ clear: function () { saved = null; },
|
|
|
251
251
|
};
|
|
252
252
|
};
|
|
253
253
|
|
|
254
|
+
var flDismissable = function (opts) {
|
|
255
|
+
var opt = opts || {};
|
|
256
|
+
var escOn = opt.escOn || [];
|
|
257
|
+
var inside = opt.inside || [];
|
|
258
|
+
var outside = opt.outside !== false;
|
|
259
|
+
var when = opt.when || function () { return true; };
|
|
260
|
+
var onDismiss = opt.onDismiss || function () {};
|
|
261
|
+
|
|
262
|
+
var shared = window.__flDismiss || (window.__flDismiss = { stack: [], bound: false });
|
|
263
|
+
|
|
264
|
+
function isInside(node) {
|
|
265
|
+
if (!node) return false;
|
|
266
|
+
for (var i = 0; i < inside.length; i++) {
|
|
267
|
+
var el = inside[i];
|
|
268
|
+
if (!el || !el.contains) continue;
|
|
269
|
+
if (el === node || el.contains(node)) return true;
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function onKey(e) {
|
|
275
|
+
if (!when()) return;
|
|
276
|
+
var k = e.key;
|
|
277
|
+
if (k !== 'Escape' && k !== 'Esc' && e.keyCode !== 27) return;
|
|
278
|
+
|
|
279
|
+
var closed = onDismiss(e, 'esc');
|
|
280
|
+
if (closed === false) return;
|
|
281
|
+
if (e.preventDefault) e.preventDefault();
|
|
282
|
+
if (e.stopPropagation) e.stopPropagation();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function onDocClick(e) {
|
|
286
|
+
var top = null;
|
|
287
|
+
for (var i = shared.stack.length - 1; i >= 0; i--) {
|
|
288
|
+
if (shared.stack[i].when()) { top = shared.stack[i]; break; }
|
|
289
|
+
}
|
|
290
|
+
if (!top || top.isInside(e.target)) return;
|
|
291
|
+
top.onDismiss(e, 'outside');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
var rec = { when: when, isInside: isInside, onDismiss: onDismiss };
|
|
295
|
+
|
|
296
|
+
for (var j = 0; j < escOn.length; j++) {
|
|
297
|
+
if (escOn[j]) escOn[j].addEventListener('keydown', onKey);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (outside) shared.stack.push(rec);
|
|
301
|
+
if (!shared.bound) {
|
|
302
|
+
shared.bound = true;
|
|
303
|
+
document.addEventListener('click', onDocClick, true);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
destroy: function () {
|
|
308
|
+
for (var j = 0; j < escOn.length; j++) {
|
|
309
|
+
if (escOn[j]) escOn[j].removeEventListener('keydown', onKey);
|
|
310
|
+
}
|
|
311
|
+
var k = shared.stack.indexOf(rec);
|
|
312
|
+
if (k >= 0) shared.stack.splice(k, 1);
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
};
|
|
316
|
+
|
|
254
317
|
function dialog(opts) {
|
|
255
318
|
opts = opts || {};
|
|
256
319
|
|
|
@@ -310,16 +373,16 @@ var uninstallIsolation = null;
|
|
|
310
373
|
openStack.push(stackEntry);
|
|
311
374
|
if (!useInert) uninstallIsolation = installIsolation(stackEntry);
|
|
312
375
|
|
|
376
|
+
var dm = flDismissable({
|
|
377
|
+
escOn: [box],
|
|
378
|
+
outside: false,
|
|
379
|
+
when: function () { return !closed; },
|
|
380
|
+
onDismiss: function () { close(); },
|
|
381
|
+
});
|
|
382
|
+
|
|
313
383
|
function focusables() { return focusableIn(box); }
|
|
314
384
|
|
|
315
385
|
box.addEventListener('keydown', function (e) {
|
|
316
|
-
if (e.key === 'Escape') {
|
|
317
|
-
e.preventDefault();
|
|
318
|
-
|
|
319
|
-
e.stopPropagation();
|
|
320
|
-
close();
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
386
|
if (e.key !== 'Tab') return;
|
|
324
387
|
|
|
325
388
|
var f = focusables();
|
|
@@ -370,6 +433,8 @@ function close() {
|
|
|
370
433
|
if (closed) return;
|
|
371
434
|
closed = true;
|
|
372
435
|
|
|
436
|
+
dm.destroy();
|
|
437
|
+
|
|
373
438
|
var ae = document.activeElement;
|
|
374
439
|
var focusIsMine = !ae || ae === document.body ||
|
|
375
440
|
box.contains(ae) || backdrop.contains(ae);
|
|
@@ -36,6 +36,7 @@ var flDismissable = function (opts) {
|
|
|
36
36
|
var opt = opts || {};
|
|
37
37
|
var escOn = opt.escOn || [];
|
|
38
38
|
var inside = opt.inside || [];
|
|
39
|
+
var outside = opt.outside !== false;
|
|
39
40
|
var when = opt.when || function () { return true; };
|
|
40
41
|
var onDismiss = opt.onDismiss || function () {};
|
|
41
42
|
|
|
@@ -76,7 +77,8 @@ var rec = { when: when, isInside: isInside, onDismiss: onDismiss };
|
|
|
76
77
|
for (var j = 0; j < escOn.length; j++) {
|
|
77
78
|
if (escOn[j]) escOn[j].addEventListener('keydown', onKey);
|
|
78
79
|
}
|
|
79
|
-
|
|
80
|
+
|
|
81
|
+
if (outside) shared.stack.push(rec);
|
|
80
82
|
if (!shared.bound) {
|
|
81
83
|
shared.bound = true;
|
|
82
84
|
document.addEventListener('click', onDocClick, true);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
.table {position:relative;overflow:auto;max-height:var(--table-max-h, 70vh);background:var(--surface);border:1px solid var(--border-decor);border-radius:var(--r-md);}.table table {width:100%;border-collapse:separate;border-spacing:0;font-size:var(--fs-sm);}.table thead th {position:sticky;top:0;z-index:var(--z-sticky);background:var(--surface-raised);color:var(--text-secondary);font-weight:600;text-align:start;white-space:nowrap;padding:var(--sp-2) var(--sp-3);border-bottom:1px solid var(--border-decor);}.table__sort {display:inline-flex;align-items:center;gap:var(--sp-1);min-height:32px;padding:0 var(--sp-1);margin:0 calc(-1 * var(--sp-1));background:none;border:0;border-radius:var(--r-xs, 3px);color:inherit;font:inherit;cursor:pointer;}.table__sort:focus-visible {outline:var(--focus-ring-width) solid var(--focus-ring-color);outline-offset:var(--focus-ring-offset);}.table__sort::after {content:"
|
|
1
|
+
.table {position:relative;overflow:auto;max-height:var(--table-max-h, 70vh);background:var(--surface);border:1px solid var(--border-decor);border-radius:var(--r-md);}.table table {width:100%;border-collapse:separate;border-spacing:0;font-size:var(--fs-sm);}.table thead th {position:sticky;top:0;z-index:var(--z-sticky);background:var(--surface-raised);color:var(--text-secondary);font-weight:600;text-align:start;white-space:nowrap;padding:var(--sp-2) var(--sp-3);border-bottom:1px solid var(--border-decor);}.table__sort {display:inline-flex;align-items:center;gap:var(--sp-1);min-height:32px;padding:0 var(--sp-1);margin:0 calc(-1 * var(--sp-1));background:none;border:0;border-radius:var(--r-xs, 3px);color:inherit;font:inherit;cursor:pointer;}.table__sort:focus-visible {outline:var(--focus-ring-width) solid var(--focus-ring-color);outline-offset:var(--focus-ring-offset);}.table__sort::after {content:"";inline-size:.85em;block-size:.85em;flex:none;background-color:currentColor;opacity:.45;-webkit-mask:var(--sort-glyph) center / contain no-repeat;mask:var(--sort-glyph) center / contain no-repeat;--sort-glyph:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 1.4 5.2 4.6h1.7v2.6h2.2V4.6h1.7z'/%3E%3Cpath d='M8 14.6 10.8 11.4h-1.7V8.8H6.9v2.6H5.2z'/%3E%3C/svg%3E");}.table th[aria-sort="ascending"] .table__sort::after, .table__sort[aria-sort="ascending"]::after {--sort-glyph:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 1.6 4.6 6h2.1v8h2.6V6h2.1z'/%3E%3C/svg%3E");opacity:1;}.table th[aria-sort="descending"] .table__sort::after, .table__sort[aria-sort="descending"]::after {--sort-glyph:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 14.4 11.4 10h-2.1V2H6.7v8H4.6z'/%3E%3C/svg%3E");opacity:1;}.table tbody td, .table tbody th {padding:var(--sp-2) var(--sp-3);text-align:start;border-bottom:1px solid var(--border-decor);vertical-align:top;}.table tbody tr:last-child td {border-bottom:0;}@media (hover:hover) and (pointer:fine) {.table tbody tr:hover {background:var(--surface-sunken);}}.table .num {text-align:end;font-variant-numeric:tabular-nums;font-family:var(--mono);white-space:nowrap;}.table .num--pos {color:var(--success);}.table .num--neg {color:var(--danger);}.table .num--pos::before {content:"+";}.table .num--neg::before {content:"−";}.table__empty, .table__error, .table__loading {padding:var(--sp-7) var(--sp-4);text-align:center;color:var(--text-secondary);font-size:var(--fs-sm);}.table__empty {background:var(--surface-sunken);}.table__error {background:var(--surface-sunken);color:var(--danger);}.table__loading {color:var(--text-tertiary);}.table__bar {height:8px;margin:var(--sp-2) auto;max-width:220px;background:var(--surface-sunken);border-radius:999px;overflow:hidden;}@media (prefers-reduced-motion:no-preference) {.table__bar::after {content:"";display:block;height:100%;width:40%;background:var(--accent-soft);border-radius:999px;animation:table-shimmer 1.2s var(--ease-in-out, ease-in-out) infinite;}@keyframes table-shimmer {0% {transform:translateX(-100%);}100% {transform:translateX(250%);}}}.table__empty .btn, .table__error .btn {margin-top:var(--sp-3);}.table--compact {font-size:var(--fs-xs);}.table--compact thead th, .table--compact tbody td {padding:var(--sp-1) var(--sp-2);}.table__cap {caption-side:top;text-align:start;padding:var(--sp-2) var(--sp-3);font-size:var(--fs-xs);color:var(--text-tertiary);}.table__foot {margin:var(--sp-2) 0 0;padding:0 var(--sp-1);font-size:var(--fs-xs);color:var(--text-tertiary);line-height:1.6;}.table__foot ol {margin:0;padding-inline-start:var(--sp-4);}.table thead tr + tr th {top:2.4em;font-weight:500;font-size:var(--fs-xs);color:var(--text-tertiary);border-bottom:1px solid var(--border-decor);}.table thead th[colspan] {text-align:center;border-inline-start:1px solid var(--border-decor);}.table thead th[colspan]:first-child {border-inline-start:0;}.table--financial thead th {white-space:nowrap;}.table--financial tbody th {white-space:nowrap;}.table--financial thead th:not(:first-child) {text-align:end;}.table__unit {font-weight:400;font-size:inherit;color:var(--text-secondary);margin-inline-start:0.35em;white-space:nowrap;}.table .num--na, .table .cell--na {color:var(--text-tertiary);text-align:center;}.table--academic {border:0;border-radius:0;background:transparent;max-height:none;}.table--academic table {background:transparent;}.table--academic thead th {background:transparent;color:var(--text-primary);font-weight:600;border-top:2px solid var(--border-decor-str);border-bottom:1px solid var(--border-decor);position:static;}.table--academic thead tr + tr th {font-weight:400;border-top:0;border-bottom:1px solid var(--border-decor);}.table--academic tbody td, .table--academic tbody th {border-bottom:0;}.table--academic tbody tr:last-child td, .table--academic tbody tr:last-child th {border-bottom:1px solid var(--border-decor);}@media (hover:hover) and (pointer:fine) {.table--academic tbody tr:hover {background:transparent;}}.table--stats .stat__se {color:var(--text-tertiary);font-size:var(--fs-xs);}.table--stats .stat__p {font-family:var(--mono);font-variant-numeric:tabular-nums;}.table--stats .stat__p--sig {font-weight:700;color:var(--text-primary);}.table--stats .stat__ci {font-family:var(--mono);font-variant-numeric:tabular-nums;white-space:nowrap;}@media print {.table {max-height:none;overflow:visible;}.table thead th {position:static;}.table--academic thead th {border-top:2px solid var(--text-primary);}}
|
package/dist/COSTS.md
CHANGED
|
@@ -22,36 +22,36 @@
|
|
|
22
22
|
| primitive | `button` | 2.7 KB | 1.0 KB | — | **3.7 KB** | 26.0 KB | −22.4 KB |
|
|
23
23
|
| primitive | `card` | 2.7 KB | 0.9 KB | — | **3.6 KB** | 23.1 KB | −19.5 KB |
|
|
24
24
|
| primitive | `choice` | 2.7 KB | 1.1 KB | — | **3.8 KB** | 25.0 KB | −21.2 KB |
|
|
25
|
-
| primitive | `combobox` | 2.7 KB | 1.0 KB | 5.4 KB | **9.0 KB** | 29.
|
|
25
|
+
| primitive | `combobox` | 2.7 KB | 1.0 KB | 5.4 KB | **9.0 KB** | 29.2 KB | −20.1 KB |
|
|
26
26
|
| primitive | `date-range` | 2.7 KB | 0.8 KB | 2.5 KB | **6.1 KB** | 26.1 KB | −20.1 KB |
|
|
27
27
|
| primitive | `delta` | 2.7 KB | 0.3 KB | — | **3.0 KB** | 22.4 KB | −19.4 KB |
|
|
28
28
|
| primitive | `input` | 2.7 KB | 1.0 KB | — | **3.7 KB** | 23.2 KB | −19.5 KB |
|
|
29
29
|
| primitive | `metric` | 2.7 KB | 0.5 KB | — | **3.2 KB** | 22.8 KB | −19.7 KB |
|
|
30
|
-
| primitive | `popover` | 2.7 KB | 0.8 KB | 2.2 KB | **5.7 KB** |
|
|
30
|
+
| primitive | `popover` | 2.7 KB | 0.8 KB | 2.2 KB | **5.7 KB** | 26.9 KB | −21.2 KB |
|
|
31
31
|
| primitive | `progress` | 2.7 KB | 0.7 KB | — | **3.4 KB** | 22.5 KB | −19.1 KB |
|
|
32
|
-
| primitive | `select` | 2.7 KB | 0.8 KB | 3.5 KB | **7.0 KB** | 29.
|
|
32
|
+
| primitive | `select` | 2.7 KB | 0.8 KB | 3.5 KB | **7.0 KB** | 29.2 KB | −22.2 KB |
|
|
33
33
|
| primitive | `separator` | 2.7 KB | 0.2 KB | — | **2.9 KB** | 20.9 KB | −18.0 KB |
|
|
34
34
|
| primitive | `skeleton` | 2.7 KB | 0.5 KB | — | **3.2 KB** | 21.1 KB | −17.9 KB |
|
|
35
35
|
| primitive | `switch` | 2.7 KB | 0.8 KB | — | **3.4 KB** | 26.8 KB | −23.3 KB |
|
|
36
36
|
| pattern | `accordion` | 2.7 KB | 1.3 KB | 1.8 KB | **5.8 KB** | 34.1 KB | −28.3 KB |
|
|
37
37
|
| pattern | `content` | 2.7 KB | 1.0 KB | — | **3.7 KB** | 24.2 KB | −20.5 KB |
|
|
38
|
-
| pattern | `drawer` | 2.7 KB | 0.8 KB | 2.
|
|
38
|
+
| pattern | `drawer` | 2.7 KB | 0.8 KB | 2.4 KB | **5.9 KB** | 26.1 KB | −20.2 KB |
|
|
39
39
|
| pattern | `dropdown` | 2.7 KB | 0.8 KB | 2.9 KB | **6.4 KB** | 27.9 KB | −21.5 KB |
|
|
40
40
|
| pattern | `form-validation` | 2.7 KB | 0.8 KB | — | **3.5 KB** | 24.1 KB | −20.7 KB |
|
|
41
41
|
| pattern | `list` | 2.7 KB | 0.6 KB | — | **3.3 KB** | 22.0 KB | −18.7 KB |
|
|
42
|
-
| pattern | `nav` | 2.7 KB | 1.2 KB | 2.6 KB | **6.
|
|
43
|
-
| pattern | `overlay` | 2.7 KB | 1.1 KB |
|
|
42
|
+
| pattern | `nav` | 2.7 KB | 1.2 KB | 2.6 KB | **6.6 KB** | 30.7 KB | −24.1 KB |
|
|
43
|
+
| pattern | `overlay` | 2.7 KB | 1.1 KB | 3.3 KB | **7.1 KB** | 34.5 KB | −27.4 KB |
|
|
44
44
|
| pattern | `pagination` | 2.7 KB | 0.8 KB | 2.6 KB | **6.0 KB** | 30.1 KB | −24.1 KB |
|
|
45
45
|
| pattern | `reveal` | 2.7 KB | 0.3 KB | 1.2 KB | **4.2 KB** | 24.2 KB | −20.0 KB |
|
|
46
46
|
| pattern | `states` | 2.7 KB | 0.8 KB | — | **3.5 KB** | 23.6 KB | −20.1 KB |
|
|
47
47
|
| pattern | `tabs` | 2.7 KB | 0.7 KB | 2.0 KB | **5.4 KB** | 26.8 KB | −21.4 KB |
|
|
48
|
-
| pattern | `tooltip` | 2.7 KB | 0.5 KB | 1.9 KB | **5.1 KB** | 26.
|
|
48
|
+
| pattern | `tooltip` | 2.7 KB | 0.5 KB | 1.9 KB | **5.1 KB** | 26.0 KB | −20.9 KB |
|
|
49
49
|
| pattern | `tree` | 2.7 KB | 0.6 KB | 2.9 KB | **6.2 KB** | 27.8 KB | −21.6 KB |
|
|
50
50
|
|
|
51
51
|
## 三、全库
|
|
52
52
|
|
|
53
|
-
- 全部组件都用上(min,含 tokens 一份):**
|
|
54
|
-
- 源码版(带注释)全库 gzip:
|
|
53
|
+
- 全部组件都用上(min,含 tokens 一份):**62.1 KB**
|
|
54
|
+
- 源码版(带注释)全库 gzip:304.3 KB
|
|
55
55
|
|
|
56
56
|
> ⚠️ 源码版那个数字**不是**要付的钱:它包含我们的注释(全库 CSS 有 67% 是注释,字节口径)。
|
|
57
57
|
> 它的用途只有一个 —— 让人看清"dist 到底省了多少"。
|
|
@@ -4,8 +4,14 @@
|
|
|
4
4
|
但证明不了"念出来是否**合理**"。这一页把这件事变成**可执行、可复现、可填表**的动作,
|
|
5
5
|
时长约 20 分钟。
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
**当前状态:已执行两轮(2026-10-30)。** 九个页面都跑过 Tab 遍历,浮层焦点在
|
|
8
|
+
`overlay` / `drawer` 上验到;**标题层级(`H` 键逐级跳)在第二轮补齐**——九个页面
|
|
9
|
+
共 107 个标题,逐条与页面结构对得上。第一轮修掉的缺陷、第二轮的破坏实验,
|
|
10
|
+
以及三趟才拿到干净数据的那三个操作坑,都写在第三节。
|
|
11
|
+
|
|
12
|
+
证据形态要说清楚:**不是人耳旁听**,是让 NVDA 把朗读日志写到文件,逐条取它交给语音
|
|
13
|
+
合成器的原始文本(`IO - speech.speech.speak`)。这比人耳更可复现、可存档,但它证明的
|
|
14
|
+
是"NVDA 念了什么",不覆盖"听起来是否别扭"。
|
|
9
15
|
|
|
10
16
|
---
|
|
11
17
|
|
|
@@ -38,9 +44,17 @@ python3 -m http.server 8000 --bind 127.0.0.1
|
|
|
38
44
|
|
|
39
45
|
## 二 · 每页只做这 5 件事(NVDA 键位在括号里)
|
|
40
46
|
|
|
41
|
-
1.
|
|
42
|
-
|
|
47
|
+
1. **按 Tab 走一遍**(走的是焦点顺序,与浏览光标停在哪无关),听到的每个控件都要有
|
|
48
|
+
**说得通的名字**。听到 "button"、"未标记"、"undefined"、只有图标名 ⇒ ❌ 记下来。
|
|
43
49
|
2. **用读屏的"下一个标题"跳(NVDA `H`)**,标题层级应当是 1 → 2 → 3,不跳级。
|
|
50
|
+
|
|
51
|
+
> **三个实操坑**(2026-10-30 实测,三个都踩过,踩完才拿到干净结果):
|
|
52
|
+
> - **换一个查询串再打开**。NVDA 按文档 URL 记忆浏览光标:同一个 URL 再开一次会恢复
|
|
53
|
+
> 上次的位置,若上次停在文末,按 `H` 只会念「往下没有标题」——看起来像"本页没有标题"。
|
|
54
|
+
> - **`h1` 用 `H` 再 `SHIFT+H` 取**。加载后的光标落点不固定:有时正好在 `h1` 上
|
|
55
|
+
> (按 `↑` 直接念出「标题 1 级 + 页面名」),有时落在紧随其后的导语段落上。
|
|
56
|
+
> 稳妥的做法是先按一次 `H` 走到第一个 `h2`,再按 `SHIFT+H` 退回来 —— 那一条必然是 `h1`。
|
|
57
|
+
> - **末端那句「往下没有标题」是证据,不是失败**。它证明 `H` 已经走过本页最后一个标题。
|
|
44
58
|
3. **把焦点放到一个禁用控件上**,它应当被念成"不可用"(而不是干脆跳过或念成正常按钮)。
|
|
45
59
|
4. **触发表单错误**(页面里带校验的那块),错误文字应当被**紧跟控件**念出,
|
|
46
60
|
不是只在屏幕右上角飘一条。
|
|
@@ -56,25 +70,94 @@ python3 -m http.server 8000 --bind 127.0.0.1
|
|
|
56
70
|
|
|
57
71
|
| 页面 | ① 名字 | ② 标题层级 | ③ 禁用态 | ④ 错误说明 | ⑤ 浮层焦点 | 备注(听到了什么) |
|
|
58
72
|
|---|---|---|---|---|---|---|
|
|
59
|
-
| button |
|
|
60
|
-
| input |
|
|
61
|
-
| choice |
|
|
62
|
-
| tabs |
|
|
63
|
-
| overlay |
|
|
64
|
-
| table |
|
|
65
|
-
| **accordion** |
|
|
66
|
-
| **drawer** |
|
|
67
|
-
| **dropdown** |
|
|
68
|
-
|
|
69
|
-
> **为什么后三行是补的**:`accordion` / `drawer` / `dropdown`
|
|
73
|
+
| button | 通过 | 通过(19/19) | 未执行 | — | — | 12 个按钮全部念出名字;加载态念「忙碌中」。标题从「1 级 button · 七态矩阵」→「2 级 七态」→「3 级 1. default …」,末条念「往下没有标题」 |
|
|
74
|
+
| input | 通过 | 通过(13/13) | 未执行 | 通过 | — | 只读念「只读」;错误念「错误的输入 · 请输入有效金额」,紧跟控件。标题 1 → 2 → 3 → 2 → 3 → 2 → 2,与结构一致 |
|
|
75
|
+
| choice | 通过 | 通过(10/10) | — | — | — | 「全选(下面 3 项)」念「部分选中」;单选念「时间范围 · 分组 · 已选中 · 3 之 1 项」。标题 1 → 2×6 → 3 → 3 → 2 |
|
|
76
|
+
| tabs | 通过 | 通过(10/10) | — | — | — | 念「选项卡组 · 选项卡 · 已选中 · 2 之 1 项」;错例组三个 `tabindex=0` 各停一次,正例组只停一次 —— 对照成立。标题 1 → 2×5 → 3×4 |
|
|
77
|
+
| overlay | 通过 | 通过(11/11,对话框标题念成 2 级) | — | — | 通过 | 打开后焦点进对话框(取消 / 确认标记),Esc 后回到「普通确认」,再 Tab 落到「删除这份报告」。标题 1 → 2×3 → 3×2 → 2×2 → 3×2 → 2 |
|
|
78
|
+
| table | 通过 | 通过(10/10) | — | — | — | 表格念「共 7 行 7 列 · 1 行 1 列」;🔴 **列头按钮把 `↕` 念了出来**(见下,已修)。标题 1 → 2×7 → 3 → 3 |
|
|
79
|
+
| **accordion** | 通过 | 通过(21/21) | 通过 | — | — | 「公司基本信息 · 按钮 · 已展开 · 标题 3 级」→ 回车念「已折叠」→ 再回车念「已展开」;`aria-disabled` 那栏念「不可用」。标题 1 → 2 → 3×3 → 2 → 3×3 → 2 → 3×3 → 2×4 → 3×5 |
|
|
80
|
+
| **drawer** | 通过 | 通过(6/6) | — | — | 通过 | 打开念「从left进来的抽屉 · 对话框」;Tab 在「取消 → 确定 → 关闭」之间循环;Esc 后回到「左侧 · 按钮」。标题 1 → 2×3 → 3×2 |
|
|
81
|
+
| **dropdown** | 通过 | 通过(7/7) | — | — | 通过(修复后) | 按钮念「菜单按钮 · 已折叠 · 子菜单」,展开念「已展开 · 菜单」。**修复前**:按 Enter 打开后焦点留在按钮上(读屏在浏览模式下把 Enter 变成点击,走 click 分支),修复后真机复测焦点落在「✓ 按日期」。方向键仍无法在项间移动 —— 极简 APG 对照页表现相同,属读屏浏览模式限制。标题 1 → 2×4 → 3×2 |
|
|
82
|
+
|
|
83
|
+
> **为什么后三行是补的**:`accordion` / `drawer` / `dropdown` 当时都是 beta 组件,
|
|
70
84
|
> 而它们升 stable 的判据之一就是「行为零欠账」——人工读屏是这件事**唯一的用户侧证据**。
|
|
71
|
-
>
|
|
72
|
-
>
|
|
85
|
+
> (0.14.0 把 drawer / overlay 的 dismissable 收编进核之后,两者已升 stable;
|
|
86
|
+
> accordion 的 `roving:gap` 经核对 APG 原文后判定为**判据写错**(APG Accordion 没有
|
|
87
|
+
> 方向键),已改为 `n/a`。`ai/components.json` 当前记 **1 个 beta**:dropdown
|
|
88
|
+
> —— 它的 `typeahead:gap` 是真的:APG Menu 把"按字符跳项"列为 Optional,本库没做。)
|
|
89
|
+
> 只覆盖 `overlay` 一个浮层,等于对其他浮层的真实表现没有任何记录。
|
|
90
|
+
|
|
91
|
+
**本轮跑出来并已修掉的一条真缺陷**:`table` 的列头按钮被念成「公司 · `↕` · 按钮」——
|
|
92
|
+
那个 `↕` 来自 `table.css` 里 `.table__sort::after` 的 `content`,Chromium 把伪元素文本算进了
|
|
93
|
+
可访问名。**自动检查抓不到它**(可访问名"非空"就算过),当时那 212 道门禁里也没有一道看这个。
|
|
94
|
+
|
|
95
|
+
改法:把字形换成 `mask` 图像(`content` 留空),三态形状仍是 `↕ / ↑ / ↓`,
|
|
96
|
+
颜色靠 `currentColor` 跟着主题走,标记结构一个字符都不用动。
|
|
97
|
+
|
|
98
|
+
**为什么不用 `content: "↕" / ""` 那个一行改法**(2026-10-30 实测更正):
|
|
99
|
+
早先这里写着"Firefox 不支持替代文本语法"——**那是错的**。三个引擎实测下来都被接受,
|
|
100
|
+
而且都能把 `↕` 从可访问名里去掉:
|
|
101
|
+
|
|
102
|
+
| 写法 | Chromium | Firefox | WebKit |
|
|
103
|
+
|---|---|---|---|
|
|
104
|
+
| `content: "↕" / ""` | 显示 · 名=`公司` | 显示 · 名=`公司` | 显示 · 名=`公司` |
|
|
105
|
+
| `content: "↕" / "排序"` | 显示 · 名=`公司 排序` | 显示 · 名=`公司排序` | 显示 · 名=`公司排序` |
|
|
106
|
+
| `mask`(本库) | 显示 · 名=`公司` | 显示 · 名=`公司` | 显示 · 名=`公司` |
|
|
107
|
+
| `content: "↕"`(原写法) | 显示 · 名=`公司↕` | 显示 · 名=`公司↕` | 显示 · 名=`公司↕` |
|
|
108
|
+
|
|
109
|
+
真正拦住它的是**老引擎**:这套语法要 Chromium 108 / Firefox 128 / Safari 17.4 起才认,
|
|
110
|
+
而不认的引擎会**把整条声明丢掉** ⇒ 箭头彻底消失(比念出符号更糟)。
|
|
111
|
+
支持矩阵里有 Android 百度浏览器(Chromium 97,见 [LAYER-BASELINE](LAYER-BASELINE.md)),
|
|
112
|
+
落在门槛之外。🔴 **这一条是按版本门槛推断的,没有在 Chromium 97 上实测过** ——
|
|
113
|
+
说的就是"没测的不算已验证"。
|
|
114
|
+
|
|
115
|
+
改完用 NVDA 复听:表头只剩「公司 · 按钮」;`table-check` 的排序判据也跟着改成
|
|
116
|
+
读 `mask` 的自定义属性(破坏实验验证过:把降序箭头改成升序形状,判据当场红)。
|
|
117
|
+
|
|
118
|
+
**`dropdown` 那行要连着对照读**:同样的操作在极简 APG 对照页上表现一致(↓ 被吃),
|
|
119
|
+
所以"方向键读不出菜单项"不是本库特有。但另一件事是**真缺陷**:用 NVDA 在浏览模式下
|
|
120
|
+
按 Enter,它会把这次按键变成一次点击 ⇒ 走的是 click 分支,而本库原来只在键盘分支里
|
|
121
|
+
聚焦首项 ⇒ 菜单打开了、焦点还在按钮上,菜单项永远进不去。
|
|
122
|
+
自动门禁当时是绿的,因为它只测了 `↓` 这条键盘路径。
|
|
123
|
+
**修法与判据**:把聚焦首项收进 `open()`(两条路径共用),并补一条"点击打开也聚焦首项"。
|
|
124
|
+
|
|
125
|
+
### ② 那一格是怎么填的:两条通道,各测一半
|
|
126
|
+
|
|
127
|
+
标题层级容易被当成"看一眼 DOM 就知道"的事,但**"结构对不对"和"读屏跳得对不对"
|
|
128
|
+
是两件事**。所以这一格用两条互不依赖的通道各测一遍:
|
|
129
|
+
|
|
130
|
+
| 通道 | 覆盖 | 这一轮的结果 |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| 机器 · 运行时 DOM + 可访问性树 | **96 个页面、887 个标题**(`node_modules` / `dist` 之外的全部 HTML,空夹具除外) | 每页恰好一个 `h1`;跳级 0 处、空名 0 个;层级分布 `h1×96 · h2×581 · h3×210`(没有 h4–h6);DOM 里的标题数与无障碍树里的 heading 节点数**逐页相等** |
|
|
133
|
+
| 读屏 · NVDA `H` 键实录 | 协议表里的 **9 页、107 个标题** | **107/107 条**念出的级别与页面结构逐条一致;九页走到底时都念「往下没有标题」 |
|
|
134
|
+
|
|
135
|
+
机器侧不是"跑绿了就算"。这三条 axe 规则(`heading-order` /
|
|
136
|
+
`page-has-heading-one` / `empty-heading`)都做过破坏实验:
|
|
137
|
+
|
|
138
|
+
| 注入的破坏 | `heading-order` | `page-has-heading-one` | `empty-heading` |
|
|
139
|
+
|---|---|---|---|
|
|
140
|
+
| 基线(不动) | 绿 | 绿 | 绿 |
|
|
141
|
+
| `h2` 后插 `h4`(跳级) | **红** | 绿 | 绿 |
|
|
142
|
+
| 删掉唯一的 `h1` | 绿 | **红** | 绿 |
|
|
143
|
+
| 清空一个 `h3` 的文本 | 绿 | 绿 | **红** |
|
|
144
|
+
| 反向对照:`h3` 后插 `h4`(合法) | 绿 | 绿 | 绿 |
|
|
145
|
+
|
|
146
|
+
最后一行是关键:合法层级仍然全绿,说明它不是"见到 `h4` 就报"。
|
|
147
|
+
|
|
148
|
+
> **机器侧可复跑**:`node 05-audit/heading-outline.js`(先起本地静态服务),
|
|
149
|
+
> 末行会打印 `96 页 · 887 个标题(h1×96 · h2×581 · h3×210)· 0 页异常`。
|
|
150
|
+
> 它不是门禁,是取证工具:结构本身已由 axe 那三条规则守着,它负责的是
|
|
151
|
+
> "把整站每个标题都数一遍"这件事,以及第四条判据(DOM 标题数 == 无障碍树 heading 数)。
|
|
152
|
+
>
|
|
153
|
+
> **读屏侧不进仓库**:实录脚本依赖一台装了 NVDA 的 Windows 机。复核方式是按第二节
|
|
154
|
+
> 第 2 条自己走一遍——九页的层级序列在上表每行的备注里都抄了,对不上就是有问题。
|
|
73
155
|
|
|
74
156
|
状态只填三种:**通过** / **失败(附听到的内容)** / **未执行**。
|
|
75
157
|
"大概没问题"不属于这三种,不要写。
|
|
76
158
|
|
|
77
|
-
填写人 / 日期 /
|
|
159
|
+
填写人 / 日期 / 系统与读屏版本:
|
|
160
|
+
`脚本驱动 NVDA(朗读取自 speech 日志原文,非人耳旁听)/ 2026-10-30,两轮 / Windows 10 22H2 19045 · NVDA 2026.3 · Edge`
|
|
78
161
|
|
|
79
162
|
---
|
|
80
163
|
|
|
@@ -90,5 +173,6 @@ python3 -m http.server 8000 --bind 127.0.0.1
|
|
|
90
173
|
## 五 · 为什么这份协议要写进仓库
|
|
91
174
|
|
|
92
175
|
因为"我们打算做读屏测试"和"我们做过读屏测试"在仓库里长得一模一样 ——
|
|
93
|
-
|
|
94
|
-
|
|
176
|
+
除非有一张**带状态列的表**。第一轮填这张表时,九页里有六页的"标题层级"写着"未执行";
|
|
177
|
+
那一列记录的不是进度,而是**边界**。第二轮把那一列填满,边界就移动了一格 ——
|
|
178
|
+
现在还剩 macOS VoiceOver 与 Android TalkBack 两格空白(见第四节第 2 条)。
|
package/docs/ACCESSIBILITY.md
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
| 项目 | 值 |
|
|
7
7
|
|---|---|
|
|
8
|
-
| 声明适用版本 | agentcss `0.
|
|
9
|
-
| 声明日期 | 2026-10-
|
|
8
|
+
| 声明适用版本 | agentcss `0.14.0` |
|
|
9
|
+
| 声明日期 | 2026-10-30 |
|
|
10
10
|
| 复核节奏 | 每次发版前重跑(`npm run gate` 内含全部证据) |
|
|
11
11
|
| 范围 | 本库交付的 CSS / JS、全部 `demo.html`(组件页 + 图表页 + 组合页)、以及文档站生成的页面 |
|
|
12
12
|
| 不在范围 | 使用者拿这些组件拼出来的业务页面(那是你的无障碍责任,不是库的) |
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
| SC | 名称 | 状态 | 证据 / 边界 |
|
|
27
27
|
|---|---|---|---|
|
|
28
28
|
| 1.1.1 | 非文本内容 | ◐ | 图标一律 `aria-hidden`,语义由文字或 `aria-label` 承担;图表类组件要求调用方给替代文本,**库替不了使用者** |
|
|
29
|
-
| 1.3.1 | 信息与关系 | ✅ | `a11y-scan`(axe
|
|
29
|
+
| 1.3.1 | 信息与关系 | ✅ | `a11y-scan`(axe `heading-order` / `page-has-heading-one` / `empty-heading`)+ `a11y.py` 静态层级 + **96 页运行时大纲实测(887 个标题)**:每页恰好一个 `h1`、无跳级、无空名;三条规则都做过破坏实验,确认不是"没查" |
|
|
30
30
|
| 1.3.2 | 有意义的顺序 | ✅ | DOM 顺序 = 视觉顺序,无 `order` / 绝对定位重排 |
|
|
31
31
|
| 1.3.3 | 感官特性 | ✅ | 说明文字不以"左边的按钮""圆形图标"指代 |
|
|
32
32
|
| 1.3.4 | 方向 | ✅ | 无 `orientation` 锁定,横竖屏均可用 |
|
|
@@ -82,30 +82,43 @@
|
|
|
82
82
|
|---|---|---|
|
|
83
83
|
| `axe-core`(`a11y-scan.js`) | 39 页(37 组件 demo + 2 个非组件页) | **0 类违规**,991 条规则通过 |
|
|
84
84
|
| 反向控制(注入 5 类典型违规) | 证明上一条不是"没查" | 5 类全部抓到 |
|
|
85
|
+
| `heading-outline.js`(人工工具) | **全部 HTML:96 页 / 887 个标题** | 0 处跳级 · 每页恰好 1 个 `h1` · DOM 标题数 == 无障碍树 heading 数 |
|
|
85
86
|
| `a11y-ax.js` 可访问性树 | **全部 `demo.html`(现 40 个)**;清单与磁盘不一致即红 | 0 个无名控件;40 份朗读序列快照在 `10-review/a11y-tree/` |
|
|
86
87
|
| `contrast.py` / `dark-contrast.js` | 亮 / 暗两套,逐对实算 | 正文 ≥ 4.5:1 |
|
|
87
88
|
| `zoom-reflow.js` | 40 页 × 3 条 AA 硬项 | 320px 不横滚 · 200% 不裁字 · 焦点不被遮挡 |
|
|
88
89
|
| `focus-ring-check` / `focus-return-check` / `keyboard-nav` | 焦点可见、焦点归还、键盘路径 | 全绿 |
|
|
89
90
|
|
|
90
|
-
###
|
|
91
|
+
### 人工(**已执行一轮,仍缺人耳与第二平台**)
|
|
91
92
|
|
|
92
93
|
自动化的天花板必须说清楚:**axe 只能覆盖约 30–40% 的 WCAG 问题**,而且它听不见。
|
|
93
|
-
以下三件事本库**没有**任何一条真实记录:
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
2026-10-30 用 NVDA 2026.3 + Edge(Windows 10 22H2)跑了两轮:把 NVDA 的朗读日志写到
|
|
96
|
+
文件,逐条取它交给语音合成器的原文。九页结果见
|
|
97
|
+
[无障碍人工测试协议](A11Y-MANUAL-PROTOCOL.md) 第三节。
|
|
98
|
+
|
|
99
|
+
第一轮覆盖 Tab 遍历、禁用态、错误说明与浮层焦点;第二轮补的是**标题层级**——
|
|
100
|
+
用读屏的「下一个标题」跳(`H`),逐页核对它念出的级别与页面结构是否一致。
|
|
101
|
+
同一轮里把机器侧的结构扫描从 9 页扩到全站 96 页(887 个标题)。
|
|
102
|
+
|
|
103
|
+
**它证明的是"NVDA 念了什么",不等于"人听着顺不顺"。** 仍然缺:
|
|
104
|
+
|
|
105
|
+
1. **人耳旁听** —— 证据是日志文本;语气、打断、重复播报这类问题不在其中。
|
|
106
|
+
2. **macOS VoiceOver / Android TalkBack** —— 一条记录都没有。
|
|
98
107
|
3. **9 处「待人工」结论**(例如图表的替代文本是否真的描述了数据)。
|
|
99
108
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
109
|
+
这一轮还确认并修掉了一条真缺陷:`table` 的列头按钮会把排序符号 `↕` 念出来
|
|
110
|
+
(来源是 `table.css` 的 `::after` 伪元素文本,Chromium 把它算进了可访问名)。
|
|
111
|
+
自动检查抓不到它 —— 可访问名"非空"就算过。
|
|
112
|
+
修法是把字形换成 `mask` 图像(`content` 留空),视觉与三态形状不变;
|
|
113
|
+
改完用 NVDA 复听,表头只剩「公司 · 按钮」。详见协议页第三节。
|
|
103
114
|
|
|
104
115
|
---
|
|
105
116
|
|
|
106
117
|
## 三 · 已知缺口(按影响排序)
|
|
107
118
|
|
|
108
|
-
1.
|
|
119
|
+
1. **没有人耳旁听与第二平台记录**:2026-10-30 有一轮 NVDA 脚本实录(见协议页),
|
|
120
|
+
但 VoiceOver / TalkBack 仍为空白,语气与播报时机也没人判断过。
|
|
121
|
+
这是与 Carbon / Spectrum 那类一致性报告差距最大的一处。
|
|
109
122
|
2. **没有 VPAT / ACR 正式报告**。采购流程要的那种文档本库没有出;
|
|
110
123
|
上面那张 SC 表可以当它的替代品,但它不是法律意义上的 ACR。
|
|
111
124
|
3. **强制色板(forced-colors)只覆盖 6 个 CSS**,Windows 高对比度模式未逐组件验证。
|
|
@@ -130,6 +143,7 @@
|
|
|
130
143
|
# 一条命令跑完全部无障碍相关判据(含反向控制)
|
|
131
144
|
npm run gate:fast # 静态层:结构 / 契约 / 文档事实
|
|
132
145
|
node 05-audit/a11y-scan.js # axe(需要本地静态服务 :8000)
|
|
146
|
+
node 05-audit/heading-outline.js # 全站标题大纲:96 页 / 887 个标题(同一服务)
|
|
133
147
|
node 05-audit/a11y-ax.js # 可访问性树 + 名称判据
|
|
134
148
|
node 05-audit/zoom-reflow.js # 200% / 320px / 焦点遮挡
|
|
135
149
|
node 05-audit/rtl-render.js # RTL 渲染 + 阿/希膨胀 + 字形镜像
|
package/docs/CONVENTIONS.md
CHANGED
|
@@ -194,6 +194,11 @@
|
|
|
194
194
|
- **方向必须有非颜色的载体**:`delta` 用 `+` / `−`(U+2212)符号,
|
|
195
195
|
且符号必须真的进可访问性树(CSS `::before` 生成的字符不在元素 name 里,
|
|
196
196
|
但会作为独立 StaticText 出现 —— 换空串/背景图/`visibility:hidden` 就会没)。
|
|
197
|
+
⚠️ 别把这条推广成"伪元素文本永远不影响可访问名":**`name-from-contents` 的元素
|
|
198
|
+
(`<button>` / `<a>`)会把伪元素文本算进名字** —— `table` 的列头就是这样被念出
|
|
199
|
+
「公司 · ↕ · 按钮」的(三个引擎都实测过,见
|
|
200
|
+
[无障碍人工测试协议](A11Y-MANUAL-PROTOCOL.md))。装饰性符号别挂在按钮上,
|
|
201
|
+
或者用 `mask` 画。
|
|
197
202
|
- **容器不许再加一套方向类**。表格里只有会计正负(`num--pos` / `num--neg`),
|
|
198
203
|
市场涨跌一律嵌 `<span class="delta delta--up">`
|
|
199
204
|
—— 否则 `table` 会有四组颜色类,调用方每列都要记"这列用哪组"。
|
|
@@ -213,6 +218,10 @@
|
|
|
213
218
|
| 亮/暗两套对比度达标 | `contrast.py`、`dark-contrast.js`、`component-contrast.py` |
|
|
214
219
|
| 焦点环存在且不是 `outline:none` | `focus-ring-check.js` |
|
|
215
220
|
| 关掉弹层后焦点回到触发点 | `focus-return-check.js` |
|
|
221
|
+
| 弹层**打开那一刻**焦点就进弹层(不是等 Tab 才进) | `overlay-check.js` ③b、`drawer-check.js` ① |
|
|
222
|
+
| **每条**关闭路径都要归还焦点(Esc / 点遮罩 / 关闭按钮) | `overlay-check.js` ⑤b(遮罩)+ ⑤(Esc) |
|
|
223
|
+
| 装饰符号不许进可访问名(伪元素有文本就会被算进 `name-from-contents`) | `table-check.js` ⑨b |
|
|
224
|
+
| 浮层类组件的每条输入路径都要测(键盘 / 点击 / 读屏合成点击) | `opener-path-check.js`(三条路径 × 五个组件一张矩阵,带 `--selftest` 证明分得清路径)+ `dropdown-check` ①b、`overlay-check` ③b、`drawer-check` ① |
|
|
216
225
|
| 键盘可达(Tab 顺序、Esc 关闭) | `keyboard-nav.js` |
|
|
217
226
|
| 减少动效策略双向一致 | `motion.py --reduced-audit` |
|
|
218
227
|
| 逻辑属性全覆盖 | `rtl-check.js` |
|
package/index.html
CHANGED