agentcss 0.12.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/03-patterns/overlay/overlay.js +10 -4
- package/04-recipes/table/README.md +45 -7
- package/04-recipes/table/demo.html +92 -5
- package/CHANGELOG.md +160 -0
- package/CONTRIBUTING.md +54 -2
- package/START-HERE.md +1 -1
- package/ai/cli.js +149 -0
- package/ai/components.json +1 -1
- package/dist/COSTS.md +2 -2
- package/docs/A11Y-MANUAL-PROTOCOL.md +8 -0
- package/docs/ACCESSIBILITY.md +5 -5
- package/docs/CONVENTIONS.md +8 -0
- package/index.html +1 -1
- package/package.json +1 -1
|
@@ -438,10 +438,16 @@
|
|
|
438
438
|
box.addEventListener('keydown', function (e) {
|
|
439
439
|
if (e.key === 'Escape') {
|
|
440
440
|
e.preventDefault();
|
|
441
|
-
/*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
441
|
+
/* ⚠️ 防御性 stopPropagation —— 保留,但**当前结构下它不改变任何行为**。
|
|
442
|
+
* 等价突变体实测:删掉这一行,一次 Esc 仍然只关最内层。
|
|
443
|
+
* 原因:两个 `.dialog-backdrop` 是 `body` 的**兄弟**节点,外层 box 不是
|
|
444
|
+
* 内层的祖先,所以内层按钮的 keydown 冒泡路径是
|
|
445
|
+
* button → .dialog → .dialog-backdrop → body
|
|
446
|
+
* —— 根本到不了外层的 `.dialog`(实测 `outer.contains(inner) === false`)。
|
|
447
|
+
* 原注释写「不拦住会一次关两层」,那是**假设了 DOM 嵌套**,与实现不符。
|
|
448
|
+
* 保留理由:若将来改成「内层放进外层 box」的真嵌套,这行就必需。
|
|
449
|
+
* 判据 `pattern-behavior-check.js ⑬` 验证的是**行为**(只关最内层),
|
|
450
|
+
* 它抓不到删除本行的突变 —— 那是等价突变体,不是判据失效。 */
|
|
445
451
|
e.stopPropagation();
|
|
446
452
|
close();
|
|
447
453
|
return;
|
|
@@ -102,7 +102,7 @@ python -m http.server 8000
|
|
|
102
102
|
# http://127.0.0.1:8000/04-recipes/table/demo.html
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
## 配色:与本库 `list`
|
|
105
|
+
## 配色:与本库 `list` 的约定**不同**(故意的,不是笔误)
|
|
106
106
|
|
|
107
107
|
| 组件 | 类名 | 约定 |
|
|
108
108
|
|---|---|---|
|
|
@@ -114,14 +114,49 @@ python -m http.server 8000
|
|
|
114
114
|
- 表格里多是**量**(营收、毛利率、净利润)—— 负数是"红字",会计惯例;
|
|
115
115
|
- 列表里多是**涨跌**(涨跌幅、日变动)—— A 股惯例是涨红跌绿。
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
负的涨跌幅被渲染成红色,读者第一眼读成"涨"。目前**没有解**:
|
|
117
|
+
### 涨跌幅放进表格:解题方式(0.12.1 已定)
|
|
119
118
|
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
把「涨跌幅」这一列塞进 `table` 时,两种约定会打架 ——
|
|
120
|
+
负的涨跌幅被渲染成红色,读者第一眼读成"涨"。这个问题曾经挂在 README 里写「目前没有解」,
|
|
121
|
+
现在有了,而且**不是**当初列的两个候选之一:
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
> **市场涨跌不进 `table` 的颜色体系,而是嵌 `delta` 原子。**
|
|
124
|
+
|
|
125
|
+
```html
|
|
126
|
+
<td class="num"><span class="delta delta--up">3.21%</span></td>
|
|
127
|
+
<td class="num"><span class="delta delta--down">1.05%</span></td>
|
|
128
|
+
<td class="num"><span class="delta delta--flat">0.00%</span></td>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
| | `num--pos` / `num--neg` | 嵌 `delta` |
|
|
132
|
+
|---|---|---|
|
|
133
|
+
| 语义 | 会计正负(多了 / 少了) | 市场涨跌(涨了 / 跌了) |
|
|
134
|
+
| 颜色来源 | `--success` / `--danger` | `--dir-up` / `--dir-down` |
|
|
135
|
+
| 随 `[data-market]` 切换 | **不切** | **切** |
|
|
136
|
+
| 符号 | `::before` 的 `+` / `−` | `delta` 自己的 `+` / `−` / 无 |
|
|
137
|
+
|
|
138
|
+
为什么否掉当初那两个候选:
|
|
139
|
+
|
|
140
|
+
- `num--up` / `num--down` 会让 `table` 有**四组**颜色类,调用方每次都要记"这列用哪组";
|
|
141
|
+
- `data-polarity="market"` 是把语义塞进一个属性,表格得替数据判断"这列是涨跌",
|
|
142
|
+
而它并不知道 —— 调用方才知道。
|
|
143
|
+
|
|
144
|
+
⇒ 方向是**独立语义**,所以它就该是独立原子。表格只负责对齐(`td.num`),不负责判色。
|
|
145
|
+
|
|
146
|
+
⚠️ **不要两套一起用**:`<td class="num num--pos"><span class="delta delta--up">`
|
|
147
|
+
会让同一格同时受两套颜色支配,谁赢取决于选择器权重 —— 那是最难查的一类 bug。
|
|
148
|
+
`table-check` 的判据 ⑬ 会挡住给 `table.css` 添 `num--up` / `num--down` 的做法。
|
|
149
|
+
|
|
150
|
+
### 零值:两边都不许有符号(0.12.1 修,E145)
|
|
151
|
+
|
|
152
|
+
`format: 'signed-percent'` 的正号由类名触发,而旧实现判的是 `v < 0` ⇒
|
|
153
|
+
`v === 0` 落进 else 分支得到 `num--pos`,渲染成「**+0.0%**」——
|
|
154
|
+
一个没变化的数被念成"涨了"。四舍五入处同样复现:`0.0004` 在 1 位精度下也印成「+0.0%」。
|
|
155
|
+
|
|
156
|
+
⇒ 规则只有一条:**符号必须跟打印出来的数字同号**。
|
|
157
|
+
先算出将要打印的数值,再按它定符号;等于 0 既不加 `+` 也不加 `−`
|
|
158
|
+
(这与 `delta--flat` 是同一个契约)。`table-check` 判据 ⑫ 守着它,
|
|
159
|
+
并且要求页面上**必须存在**零值样本 —— 删掉零值行不能算通过。
|
|
125
160
|
|
|
126
161
|
|
|
127
162
|
## 反例
|
|
@@ -137,5 +172,8 @@ python -m http.server 8000
|
|
|
137
172
|
- ❌ **粘性表头配 `border-collapse: collapse`** —— `separate` 是 sticky 生效的前提,
|
|
138
173
|
用 collapse 表头不会粘住
|
|
139
174
|
- ❌ **拿它当虚拟滚动表格用** —— 本组件不做虚拟滚动;超过 200 行请配分页
|
|
175
|
+
- ❌ **给表格再加一套「市场涨跌」的颜色类** —— 那是第三组方向类,
|
|
176
|
+
加上会计的正负就是四组;市场涨跌嵌 `delta`(判据 ⑬ 会红)
|
|
177
|
+
- ❌ **零值带 `+`** —— 「+0.0%」= 把持平念成上涨;先算打印值再定符号(判据 ⑫ 会红)
|
|
140
178
|
|
|
141
179
|
---
|
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
<link rel="stylesheet" href="../../01-tokens/typography.css">
|
|
10
10
|
<link rel="stylesheet" href="../../01-tokens/focus-ring.css">
|
|
11
11
|
<link rel="stylesheet" href="../../01-tokens/page.css">
|
|
12
|
+
<!-- 市场口径切换:只有引了它,[data-market="intl"] 才生效。
|
|
13
|
+
不引则属性被完全忽略(可选文件,见 01-tokens/market.css 的文件头)。 -->
|
|
14
|
+
<link rel="stylesheet" href="../../01-tokens/market.css">
|
|
12
15
|
<link rel="stylesheet" href="../../02-primitives/button/button.css">
|
|
16
|
+
<link rel="stylesheet" href="../../02-primitives/delta/delta.css">
|
|
13
17
|
<link rel="stylesheet" href="table.css">
|
|
14
18
|
<style>
|
|
15
19
|
.toolbar { display: flex; gap: var(--sp-2); margin: 0 0 var(--sp-3); flex-wrap: wrap; }
|
|
@@ -92,6 +96,30 @@
|
|
|
92
96
|
</p>
|
|
93
97
|
<div id="escproof"></div>
|
|
94
98
|
|
|
99
|
+
<!-- ========================================================================
|
|
100
|
+
两种方向语义:会计正负 vs 市场涨跌(0.12.1)
|
|
101
|
+
===================================================================== -->
|
|
102
|
+
<h2 class="rule-top">6 · 同一张表里的两种方向语义</h2>
|
|
103
|
+
<p class="note">
|
|
104
|
+
「同比 +16.3%」和「近 20 日 +3.21%」看起来都是"涨",但它们是<strong>两件事</strong>:
|
|
105
|
+
前者是会计口径(正负就是加减),后者是市场口径(红涨绿跌,国际口径正好相反)。
|
|
106
|
+
<br>⇒ 会计正负用 <code>num--pos</code> / <code>num--neg</code>;
|
|
107
|
+
市场涨跌用嵌进单元格的 <code>delta</code>,
|
|
108
|
+
<strong>不给表格加第三组方向类</strong>(<code>num--up</code> / <code>num--down</code> 故意不存在)。
|
|
109
|
+
<br>下面两块数据完全相同,只有外层 <code>data-market</code> 不同 ——
|
|
110
|
+
<strong>会计那一列一个字都不变,市场那一列整体翻转</strong>。
|
|
111
|
+
<br>⚠️ 在同一张表里,会计的「+16.3%」是绿、市场的「+3.21%」是红 ——
|
|
112
|
+
看起来互相矛盾,<strong>这恰恰是它们必须分开命名的原因</strong>:
|
|
113
|
+
一个说"多了多少",一个说"涨了多少",共用一个颜色令牌就会给出矛盾信号。
|
|
114
|
+
</p>
|
|
115
|
+
<div id="polarity"></div>
|
|
116
|
+
<p class="table__foot">
|
|
117
|
+
注:<strong>零值两边都不带符号</strong> —— 会计列不写 <code>+</code>,
|
|
118
|
+
市场列用 <code>delta--flat</code>。
|
|
119
|
+
「0.0%」被读成「涨了 0.0%」是本库明确反对的一类错误
|
|
120
|
+
(四舍五入到 0 也算 0,所以 0.0004 在 1 位精度下同样不带符号)。
|
|
121
|
+
</p>
|
|
122
|
+
|
|
95
123
|
|
|
96
124
|
|
|
97
125
|
<h2 class="rule-bottom">对比:错写法 vs 本库写法</h2>
|
|
@@ -187,17 +215,29 @@
|
|
|
187
215
|
}
|
|
188
216
|
if (f === 'text') return { text: String(v), cls: '' };
|
|
189
217
|
|
|
190
|
-
var
|
|
218
|
+
var n = Number(v);
|
|
219
|
+
var abs = Math.abs(n);
|
|
220
|
+
var isPct = (f === 'percent' || f === 'signed-percent');
|
|
191
221
|
var text;
|
|
192
222
|
if (f === 'currency' || f === 'integer') text = grouped(abs, col.precision);
|
|
193
|
-
else if (
|
|
223
|
+
else if (isPct) text = fixed(abs * 100, col.precision) + '%';
|
|
194
224
|
else text = fixed(abs, col.precision);
|
|
195
225
|
|
|
196
226
|
/* 符号:负数总是有 −(数值语义 + 无障碍第二线索);
|
|
197
|
-
正数只有 signed-* 才带 + —— 这就是"按列语义处理"
|
|
227
|
+
正数只有 signed-* 才带 + —— 这就是"按列语义处理"。
|
|
228
|
+
|
|
229
|
+
🔴 但符号必须**跟打印出来的数字同号**(0.12.1 修,E145):
|
|
230
|
+
旧实现判的是 `v < 0`,于是 `v === 0` 掉进 else 分支拿到 `num--pos`,
|
|
231
|
+
渲染成「+0.0%」—— 一个没变化的数被念成"涨了"。
|
|
232
|
+
同一条错还会在**四舍五入**处复现:0.0004 在 1 位精度下打印成
|
|
233
|
+
`0.0%`,若仍按原始值判正负,同样得到「+0.0%」。
|
|
234
|
+
⇒ 唯一的正确判法:先算出**将要打印的数值**,再按它定符号。
|
|
235
|
+
`= 0` 既不加 + 也不加 −(与 delta 的 `delta--flat` 同一个契约)。 */
|
|
236
|
+
var shown = Number(fixed(isPct ? abs * 100 : abs, col.precision));
|
|
198
237
|
var cls = 'num';
|
|
199
|
-
if (
|
|
200
|
-
else if (
|
|
238
|
+
if (shown !== 0 && n < 0) cls += ' num--neg';
|
|
239
|
+
else if (shown !== 0 && n > 0 &&
|
|
240
|
+
(f === 'signed-percent' || f === 'signed-decimal')) cls += ' num--pos';
|
|
201
241
|
|
|
202
242
|
return { text: text, cls: cls };
|
|
203
243
|
}
|
|
@@ -488,6 +528,53 @@
|
|
|
488
528
|
'<p class="table__foot">复查方式:用浏览器看这一节的单元格 —— ' +
|
|
489
529
|
'第二、三列应当<b>原样显示</b>尖括号文本,而不是把它们变成样式。</p>';
|
|
490
530
|
document.getElementById('escproof').innerHTML = e2;
|
|
531
|
+
|
|
532
|
+
/* --------------------------------------- 预设六:两种方向语义(0.12.1) */
|
|
533
|
+
|
|
534
|
+
/* 市场涨跌:方向交给 delta,表格自己不判色。
|
|
535
|
+
🔴 为什么不给 table 加 `num--up` / `num--down`:
|
|
536
|
+
那会让表格有四组颜色类(pos / neg / up / down),调用方每次都要记
|
|
537
|
+
「这一列用哪组」。方向该由**数据语义**决定,
|
|
538
|
+
不该由「它被放进了表格」决定 —— 所以它是独立原子,不是表格的变体。
|
|
539
|
+
|
|
540
|
+
🔴 符号规则与 cell() 完全一致:按**打印出来的数字**判方向。
|
|
541
|
+
四舍五入到 0 的一律算持平,否则 0.00004 会印成「+0.00%」——
|
|
542
|
+
同一类错误在会计列与市场列各犯一次,就等于没修。 */
|
|
543
|
+
function deltaCell(v) {
|
|
544
|
+
var n = Number(v);
|
|
545
|
+
var shown = Number(fixed(Math.abs(n) * 100, 2));
|
|
546
|
+
var dir = shown === 0 ? 'flat' : (n > 0 ? 'up' : 'down');
|
|
547
|
+
return '<span class="delta delta--' + dir + '">' +
|
|
548
|
+
esc(fixed(Math.abs(n) * 100, 2)) + '%</span>';
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
var POL_DATA = [
|
|
552
|
+
{ name: '营业收入', acct: 0.163, mkt: 0.0321 },
|
|
553
|
+
{ name: '营业成本', acct: -0.008, mkt: -0.0105 },
|
|
554
|
+
{ name: '其他收益', acct: 0.000, mkt: 0.0000 } /* 零值:两边都不许出符号 */
|
|
555
|
+
];
|
|
556
|
+
|
|
557
|
+
function polarityTable() {
|
|
558
|
+
var h = '<div class="table"><table>' +
|
|
559
|
+
'<caption class="table__cap">同一份数据 · 会计口径不随市场切换,市场口径整体翻转</caption>' +
|
|
560
|
+
'<thead><tr><th scope="col">科目</th>' +
|
|
561
|
+
'<th scope="col">同比<span class="table__unit">(会计)</span></th>' +
|
|
562
|
+
'<th scope="col">涨跌<span class="table__unit">(市场)</span></th>' +
|
|
563
|
+
'</tr></thead><tbody>';
|
|
564
|
+
POL_DATA.forEach(function (r) {
|
|
565
|
+
var a = cell(r.acct, { format: 'signed-percent', precision: 1 });
|
|
566
|
+
h += '<tr><th scope="row">' + esc(r.name) + '</th>' +
|
|
567
|
+
'<td class="' + a.cls + '">' + esc(a.text) + '</td>' +
|
|
568
|
+
'<td class="num">' + deltaCell(r.mkt) + '</td></tr>';
|
|
569
|
+
});
|
|
570
|
+
return h + '</tbody></table></div>';
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
document.getElementById('polarity').innerHTML =
|
|
574
|
+
'<p class="note">A 股口径 <code>[data-market="cn"]</code>(默认,红涨绿跌)</p>' +
|
|
575
|
+
'<div data-market="cn">' + polarityTable() + '</div>' +
|
|
576
|
+
'<p class="note">国际口径 <code>[data-market="intl"]</code> —— 只有市场那一列翻转</p>' +
|
|
577
|
+
'<div data-market="intl">' + polarityTable() + '</div>';
|
|
491
578
|
})();
|
|
492
579
|
</script>
|
|
493
580
|
</body>
|
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,166 @@
|
|
|
16
16
|
|
|
17
17
|
(无)
|
|
18
18
|
|
|
19
|
+
## [0.13.1] - 2026-10-30
|
|
20
|
+
|
|
21
|
+
**补录 Linux 三套视觉基线。0.13.0 的 tag 因这处过期未通过发布门禁,包没有发出去
|
|
22
|
+
—— npm 上不存在 0.13.0。本版本内容 = 0.13.0 + 基线对齐。**
|
|
23
|
+
|
|
24
|
+
0.12.1 改了 `04-recipes/table/demo.html`(新增第 6 节两栏对照,页面高度
|
|
25
|
+
`4540` → `5653`),当时只重录了 Windows 的三套基线。Linux 三套仍指向旧代码,
|
|
26
|
+
而**基线与指纹戳是按平台分集的** —— 本机的 `visual-stamp` 只核本平台,
|
|
27
|
+
于是「212 道全绿」在这件事上的保证范围**仅限 Windows**,Linux 那三套要到
|
|
28
|
+
CI 上才暴露。
|
|
29
|
+
|
|
30
|
+
重录后逐张核对:三套各**只有** `04-recipes_table.png` 变化(418 KB → 556 KB),
|
|
31
|
+
其余 `43 / 43 / 14` 张**逐字节相同**。这同时证明渲染环境没有漂移,
|
|
32
|
+
变化只来自表格页新增的那一节。
|
|
33
|
+
|
|
34
|
+
`CONTRIBUTING.md` 补了基线纪律:改 demo 的**结构**(页面高度会变)⇒
|
|
35
|
+
所有已有平台的基线都要重录,不只本机;Linux 那套走
|
|
36
|
+
Actions → release → Run workflow → 勾 `shoot_baseline`,
|
|
37
|
+
**不要**在本机 `--record`(那录的是 Windows 的渲染,等于污染基准)。
|
|
38
|
+
|
|
39
|
+
## [0.13.0] - 2026-10-30
|
|
40
|
+
|
|
41
|
+
**门禁体系从「两态」变成「三态」,并补上四道真能红的门禁。
|
|
42
|
+
`SKIP` 从此是一个独立的、不计入通过了的结果 —— 在此之前,
|
|
43
|
+
「我没跑」只能靠 `return 0` 表达,于是它被显示成 PASS。**
|
|
44
|
+
|
|
45
|
+
组件 CSS 产物本轮只动了一处注释,所以对**使用者**而言这是个空版本;
|
|
46
|
+
真正的变化在「你怎么知道它是对的」这条链上。
|
|
47
|
+
|
|
48
|
+
### 新增能力
|
|
49
|
+
|
|
50
|
+
- **`node ai/cli.js assets`** —— 列出**全部**可复用资产(49 项),
|
|
51
|
+
不再只覆盖「有独立 CSS 的组件目录」。此前 `longform`(单文件长文示例页)、
|
|
52
|
+
`data-showcase`、`echarts-adapter`、`01-tokens/page.css`(页面骨架)、
|
|
53
|
+
`01-tokens/behavior/`(三个共享微行为核)对机器不可见 ——
|
|
54
|
+
而 AI 不知道有 `page.css` 就会自己写布局,不知道有 `behavior/` 就会重写
|
|
55
|
+
「Esc 关闭 + 焦点归还」。清单**从目录扫描生成**,不维护第二份手工列表。
|
|
56
|
+
- **`benchmark/REAL-PAGE-TASKS.md`** —— P01–P10 十个页面级生成任务,
|
|
57
|
+
每个给出「需求 → 必须用到的库能力 → 自动判据 → 参考结果」,配 A/B 对照协议、
|
|
58
|
+
记录格式与统计口径。定位是**效果验证**,与既有的 T01–T10(契约层)分开报告。
|
|
59
|
+
- **`benchmark/score-page.js`** —— 对**任意** HTML 打分的 5 条静态判据。
|
|
60
|
+
既有的 `a11y-ax` / `responsive-check` / `zoom-reflow` 页面清单是内置页常量,
|
|
61
|
+
指不到新生成的文件 ⇒ 不能拿它们给生成页打分,这是另写一个的原因。
|
|
62
|
+
库类覆盖率阈值 `0.6` 是拿参考页实测 `0.696` 校准的,反向控制 3 条全绿。
|
|
63
|
+
|
|
64
|
+
### 修掉的真缺陷
|
|
65
|
+
|
|
66
|
+
- **门禁跑完不退出(必然死锁)** `check-all.sh` 的主等待是裸 `wait`,
|
|
67
|
+
而它会等**本 shell 起的全部子进程** —— 包括脚本自己启动的静态服务,
|
|
68
|
+
那个服务永不退出 ⇒ 208 道跑完后进程无限挂起,只能人工 kill。
|
|
69
|
+
实测复现:`sleep 30 & wait` 超时 `124`;加 `disown` 后 **0.076 秒**返回。
|
|
70
|
+
⇒ 起服务后 `disown`。
|
|
71
|
+
- **临时目录只增不减(21.2 GB)** `FL_TMPDIR` 只 `mkdir -p`、从不清理;
|
|
72
|
+
更隐蔽的是第 565 行的 `trap ... EXIT` **覆盖**了第 171 行的清理 trap
|
|
73
|
+
(bash 的 trap 是替换,不是追加)⇒ 浏览器 profile 一个都删不掉。
|
|
74
|
+
⇒ 合并成唯一出口,并且只清理本脚本自己设的默认目录(用户显式指定的不碰)。
|
|
75
|
+
清理时另揪出 8 个孤儿 headless 浏览器仍占着 profile 目录。
|
|
76
|
+
- **两道门禁把「没跑」说成「通过」** `engine-gate` 在可用引擎 < 2 时 `return 0`;
|
|
77
|
+
`benchmark --check-records` 把参考解法数成「模型记录」。两者都被汇总显示为 PASS。
|
|
78
|
+
⇒ `check-all.sh` 引入第三态 **`rc=3` = 未判定**,单独计数;
|
|
79
|
+
有未判定项时**不再打印「已执行的全部通过」**这种容易被读成全绿的措辞。
|
|
80
|
+
(没用 `2`:已有 9 道门禁用 `exit(2)` 表示「参数错 / 前置不满足」,
|
|
81
|
+
把 2 定义成跳过会把这些真错误静默吞掉。)
|
|
82
|
+
- **资产发现的三处遗漏**(详见上节「新增能力」的 `ai assets` 条目)。
|
|
83
|
+
|
|
84
|
+
### 新增门禁(4 道,均带反向控制)
|
|
85
|
+
|
|
86
|
+
| 门禁 | 它守什么 | 反向控制 |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| `assets-catalog` | 磁盘目录 / 契约 / CLI 三方对账 | 注入空目录 ⇒ 红;`_` 前缀目录 ⇒ 不算资产 |
|
|
89
|
+
| `zero-dep` | 「零第三方运行时依赖」这条对外承诺 | 往 `dependencies` 注入 `lodash` ⇒ 红 |
|
|
90
|
+
| `pattern-behavior` | **组件有没有真的接上行为核**(13 条) | 关掉遮罩关闭 / 不归还焦点 ⇒ 红 |
|
|
91
|
+
| `benchmark-score` | 评分器自身的判据合成 | 3 条破坏样本 ⇒ 红 |
|
|
92
|
+
|
|
93
|
+
`zero-dep` 值得单说:README 第一句就是「零依赖」,而此前**没有任何门禁读
|
|
94
|
+
`dependencies`**。注入 `lodash` 后 `doc-facts` / `dist:check` / `npm test`
|
|
95
|
+
**三道全部 RC=0** —— 核心承诺被违反时全库不会发现。
|
|
96
|
+
|
|
97
|
+
`pattern-behavior` 与既有的 `dismissable-check` / `focus-return-check` 分工不同:
|
|
98
|
+
后者测**核本身**(把核注入空白页直接调),前者测**接线**(通过组件真实 API
|
|
99
|
+
开一个抽屉,验焦点是否进入、Esc 是否只关最内层、点遮罩是否符合该组件自己的语义)。
|
|
100
|
+
|
|
101
|
+
### 文档对齐
|
|
102
|
+
|
|
103
|
+
- `CONTRIBUTING.md`:补「全量为什么**不**在 CI 上跑」的完整理由与责任边界;
|
|
104
|
+
补 `rc=3` 三态表;补「失败时先读 `internal/tmp/selftest-fail-*.log`」。
|
|
105
|
+
- `docs/A11Y-MANUAL-PROTOCOL.md`:结果表补 `accordion` / `drawer` / `dropdown`
|
|
106
|
+
三行 —— 三个 beta 组件此前一个都不在表内,而「beta 升 stable」的判据之一
|
|
107
|
+
就是行为零欠账,人工读屏是唯一的用户侧证据。**状态仍写「未执行」**:
|
|
108
|
+
加行不等于做过。
|
|
109
|
+
|
|
110
|
+
### 未改(诚实登记)
|
|
111
|
+
|
|
112
|
+
- **真实模型跑批仍是 0 条**。`benchmark --check-records` 现在会显示 **SKIP**,
|
|
113
|
+
这不是故障,是它在说「还没有真实记录」。转述时不要把它说成通过。
|
|
114
|
+
- **人工读屏仍未执行**:需要一台装读屏软件的机器与人耳,已在协议里标 `NOT_TESTED`。
|
|
115
|
+
- 一处**等价突变体**:`overlay.js` 里删除 `stopPropagation` 抓不到红。实测
|
|
116
|
+
`outer.contains(inner) === false`(两个 backdrop 是兄弟节点),拦不拦行为一样。
|
|
117
|
+
保留该行(未来若改成真嵌套就必需),但注释已改为说明**真实机制**,
|
|
118
|
+
而不是原先那句把假设写成事实的「不拦会一次关两层」。
|
|
119
|
+
|
|
120
|
+
## [0.12.1] - 2026-10-29
|
|
121
|
+
|
|
122
|
+
**把 `table` 与 `delta` 接上:市场涨跌以 `delta` 嵌进单元格,
|
|
123
|
+
表格不再需要第四组颜色类;顺带修掉「零值被印成正号」。**
|
|
124
|
+
|
|
125
|
+
这是 0.12.0 留下的那条自认缺口(`table/README.md` 原话:「目前**没有解**」)。
|
|
126
|
+
解法不是当初列的两个候选(加 `num--up/down` 或 `data-polarity`),而是第三条:
|
|
127
|
+
**方向是独立语义,所以它继续待在独立原子里,表格只负责对齐。**
|
|
128
|
+
|
|
129
|
+
### 修掉的真缺陷
|
|
130
|
+
|
|
131
|
+
- **E145 · 零值被印成正号** `format: 'signed-percent'` 的正号由类名触发,
|
|
132
|
+
而旧实现判的是 `v < 0` ⇒ `v === 0` 掉进 else 得到 `num--pos`,渲染成「**+0.0%**」——
|
|
133
|
+
一个没变化的数被念成"涨了"。四舍五入处同样复现:`0.0004` 在 1 位精度下也印成「+0.0%」。
|
|
134
|
+
⇒ 改成「先算打印值,再按它定符号」,与 `delta--flat` 同一个契约。
|
|
135
|
+
(**不是门禁发现的** —— 门禁只验它写了判据的东西。这条是有人顺着 demo 源码
|
|
136
|
+
读下来才看见的,记在这里提醒自己:判据的边界就是视野的边界。)
|
|
137
|
+
|
|
138
|
+
### 本轮流程上踩的坑(详见 `internal/ERRORS.md` E146–E148)
|
|
139
|
+
|
|
140
|
+
- **E146** 新判据 F7 的豁免条件第一版用 `or` 连接「版本号」与「过去时语汇」,
|
|
141
|
+
结果 `docs/ACCESSIBILITY.md` 表格行里的 WCAG 编号「**4.1.2**」被当成版本号
|
|
142
|
+
⇒ 整行豁免 ⇒ **判据对它自己最该管的那一行完全失效**。破坏实验才发现。
|
|
143
|
+
- **E147** 同一份文档的「适用版本 `0.11.9`」与「38 个 demo 页」跨了三个版本没人改。
|
|
144
|
+
这类数字只有加了 F7/F8 才第一次被机器看见。
|
|
145
|
+
- **E148** 我在本文件初稿里写了一条**没发生过的缺陷**("table demo 写了 `data-market`
|
|
146
|
+
却没引 `market.css`")。核对 `git show HEAD:` 发现改前那个 demo 里 `data-market`
|
|
147
|
+
出现 **0 次** —— 是我把"我要新增的东西"误记成"原来就有的漏洞"。已删。
|
|
148
|
+
|
|
149
|
+
### 新增
|
|
150
|
+
|
|
151
|
+
- `04-recipes/table/demo.html` 第 6 节:**同一张表里的两种方向语义**。
|
|
152
|
+
两块数据逐字相同,只有外层 `data-market` 不同 —— 会计列一个字不变,市场列整体翻转。
|
|
153
|
+
零值那一行两边都不带符号。
|
|
154
|
+
- `table-check.js` 三条判据(该门禁的专属判据 14 → 17 条):
|
|
155
|
+
- ⑫ 符号与打印值同号(**且要求页面上真的存在零值样本**,删掉零值行不能算通过)
|
|
156
|
+
- ⑬ 市场涨跌用 `delta` + `table.css` 不许出现 `num--up/down` + intl 只翻市场列
|
|
157
|
+
- ⑭ 393px 下单元格内的 `delta` 不溢出(量几何,不看声明)
|
|
158
|
+
三次破坏实验确认判别力:退回旧符号逻辑 ⇒ ⑫ 红;给 `table.css` 加 `num--up` ⇒ ⑬ 红;
|
|
159
|
+
把 intl 块改成 cn ⇒ ⑬ 红。
|
|
160
|
+
- `doc-facts-gate` 新增 F7:文档里的「全部 N 个 demo 页」必须等于磁盘份数。
|
|
161
|
+
它第一次跑就抓到 `CONTRIBUTING.md` 的旧数字,也抓到本门禁自己的一处设计错误
|
|
162
|
+
(拿 WCAG 编号「4.1.2」当版本号 ⇒ 整行被豁免 ⇒ 判据对目标文件完全失效)。
|
|
163
|
+
- `doc-facts-gate` 新增 F8:「无障碍声明适用版本」必须等于 `VERSION`。
|
|
164
|
+
|
|
165
|
+
### 文档对齐
|
|
166
|
+
|
|
167
|
+
- `docs/ACCESSIBILITY.md`:适用版本 `0.11.9` → `0.12.1`;
|
|
168
|
+
覆盖数 38 → **40**(a11y-ax)/ 39(axe)/ 40(zoom-reflow)——
|
|
169
|
+
三个工具各自口径不同,原先把它们写成同一个 38 是错的。
|
|
170
|
+
- `04-recipes/table/README.md`:「未来没有解」改写为解法与取舍,并补零值规则。
|
|
171
|
+
|
|
172
|
+
### 未改(诚实登记)
|
|
173
|
+
|
|
174
|
+
- `axe` 的 991 条规则通过数、`zoom-reflow` 的 3 条 AA 硬项结果都是上一轮实测值,
|
|
175
|
+
本版没有重跑这两个工具(只重跑了 `a11y-ax`)。数字沿用,但**不声称本轮复核过**。
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
19
179
|
## [0.12.0] - 2026-10-29
|
|
20
180
|
|
|
21
181
|
**新增两个组件:`delta`(变化量)与 `metric`(指标块)。这是「数值展示层」的第一批。**
|
package/CONTRIBUTING.md
CHANGED
|
@@ -8,10 +8,48 @@ npm run gate # 全量:含浏览器类
|
|
|
8
8
|
npm run gate:fast # 只跑静态层,不用装浏览器
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
全量是 **
|
|
12
|
-
0.11.15 起默认 4
|
|
11
|
+
全量是 **212 道门禁**(4 路并发实测约 9 分钟 / 退出码 0;单道耗时累加 1691 秒,
|
|
12
|
+
组件层要起浏览器,还要逐张比 96 张视觉截图。0.11.15 起默认 4 路并发)。
|
|
13
13
|
只想确认没改坏静态契约的话用 `gate:fast`,几秒到几分钟。
|
|
14
14
|
|
|
15
|
+
### 全量为什么**不**在 CI 上跑(这不是缺口,是结论)
|
|
16
|
+
|
|
17
|
+
`.github/workflows/gate.yml` 分三层:`quick`(`MODE=fast`,静态 + 视觉)、
|
|
18
|
+
`contract`(API 契约,需 Chromium)、`browsers`(nightly,三引擎矩阵)。
|
|
19
|
+
**没有任何一处是 `MODE=full`**,而且不该有 —— 实测在 Linux runner 上跑全量会
|
|
20
|
+
FAIL 10+ 道(字体渲染不同 ⇒ 视觉 diff 必然不一致;路径/编码不同 ⇒ 体积类误报;
|
|
21
|
+
shallow clone ⇒ 依赖 git 状态的判据语义不同;浏览器版本不同 ⇒ 截图基线不匹配)。
|
|
22
|
+
|
|
23
|
+
**一个长期红的 CI 比没有 CI 更糟**:大家会习惯性忽略它,真正的新问题也一起被忽略。
|
|
24
|
+
所以边界是:
|
|
25
|
+
|
|
26
|
+
- **CI 负责**:跨平台可靠的那部分(静态契约、类型、API 形态、单引擎行为)
|
|
27
|
+
- **发版人负责**:发版前在**本机目标平台**跑一次 `bash 05-audit/check-all.sh`
|
|
28
|
+
|
|
29
|
+
`browsers` job 已覆盖三引擎的组件行为契约,但它**不替代**本机全量:
|
|
30
|
+
它不跑视觉基线(基线按平台分集)、不跑 RTL/响应式/无障碍全量。
|
|
31
|
+
|
|
32
|
+
### 失败时先读哪里
|
|
33
|
+
|
|
34
|
+
`selftest-all` 报红时,除屏幕上那 4 行摘要,**完整子输出会落盘**到
|
|
35
|
+
`internal/tmp/selftest-fail-<脚本名>.log`(`internal/` 已被忽略,不进仓库)。
|
|
36
|
+
不可复现的红最需要这个 —— 摘要往往不够定位。
|
|
37
|
+
|
|
38
|
+
只想验一条反向控制时,用 `SKIP_SELFTESTS="a.py,b.py" python3 05-audit/selftest-all.py`
|
|
39
|
+
(列表里没写到的都会跑;被跳过的会在末尾逐条列出理由)。
|
|
40
|
+
|
|
41
|
+
### 门禁退出码的三种含义(写新门禁必读)
|
|
42
|
+
|
|
43
|
+
| rc | 含义 | 汇总里显示 | 计入失败 |
|
|
44
|
+
|---|---|---|---|
|
|
45
|
+
| `0` | 通过 | `PASS` | 否 |
|
|
46
|
+
| `1` | 失败 | `FAIL`,并原样打印门禁自己的话 | 是 |
|
|
47
|
+
| `3` | **跳过 / 未判定**(环境不具备、数据为空,但本身无错) | `SKIP`,末尾单独数「未判定 N 道」 | 否 |
|
|
48
|
+
|
|
49
|
+
**`2` 不是跳过**:已有 9 个门禁用它表示「参数错 / 前置条件不满足」,往 2 上加语义会把真错误
|
|
50
|
+
静默降级成跳过。**更不要 `return 0` 冒充跳过**——`rc=0` 天然等于通过,那正是假绿
|
|
51
|
+
(见 `internal/ERRORS.md` 的 E152 / E153)。有未判定项时,汇总**不会**打印「全部通过」。
|
|
52
|
+
|
|
15
53
|
环境依赖只有一套(Node + Chromium),缺了对应的门禁会**跳过**(跳过不算通过):
|
|
16
54
|
|
|
17
55
|
| 依赖 | 少了会怎样 | 装法 |
|
|
@@ -44,6 +82,20 @@ python 05-audit/shot-baseline.py --check --dir rtl # 视觉基线(13 页)
|
|
|
44
82
|
不各抄一份 apt 清单)。字体版本升级会让 CI 红而本机绿 —— 确认是字体变化,
|
|
45
83
|
就重录 Linux 那套;这也是"基线锚在渲染环境上"的固有代价。
|
|
46
84
|
|
|
85
|
+
**改了 demo 的结构 ⇒ 所有已有平台的基线都要重录,不只是你本机那套。**
|
|
86
|
+
|
|
87
|
+
基线连同 `_stamp.json` 是**按平台分集**的:你重录 Windows 那套,
|
|
88
|
+
本机的 `visual-stamp` 立刻绿;而 Linux 那套的戳仍指向旧 head,
|
|
89
|
+
它在 CI 上红,**本机全量永远看不见**。判据很简单:**页面高度会不会变**——
|
|
90
|
+
增删章节、改布局、改示例数据条数都会变;改文案、改注释不会。
|
|
91
|
+
`visual` 报「尺寸变了 (393, 4540) → (393, 5653)」就是该重录的信号,
|
|
92
|
+
不是"轻微像素差",别当噪音忽略。
|
|
93
|
+
|
|
94
|
+
Linux 那套**不要**在本机 `--record`(录出来的是 Windows 的渲染,等于污染基准)。
|
|
95
|
+
正确通道是 Actions → release → Run workflow → 勾 `shoot_baseline`:
|
|
96
|
+
它在 ubuntu + `ci-fonts.sh` 的同一套环境里录,产物只出 artifact、不落库、不发布。
|
|
97
|
+
下载 `baseline-Linux` 覆盖 `10-review/shots/_baseline-linux*`,**逐张看过**再提交。
|
|
98
|
+
|
|
47
99
|
**加新组件时请注意**:若它含有 **方向性字形**(箭头、指示符)、
|
|
48
100
|
**左右不对称的槽位/边框/滑入方向**、或**顺序有语义**(上一页/下一页、首列),
|
|
49
101
|
就必须把它的 demo 加进 `shot-baseline.py` 的 `RTL_PAGES`。
|
package/START-HERE.md
CHANGED
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
| 分页 | `03-patterns/pagination/pagination.css` | 195 | **必需 401** (`pagination.js`) | 数据多到要翻页就要 |
|
|
55
55
|
| 树形 | `03-patterns/tree/tree.css` | 122 | **必需 464** (`tree.js`) | 层级数据(文件 / 组织 / 分类)要展开就要 |
|
|
56
56
|
| 侧边抽屉(beta) | `03-patterns/drawer/drawer.css` | 129 | **必需 258** (`drawer.js`) | 要从侧边滑出面板就要 |
|
|
57
|
-
| 弹窗 / 吐司(beta) | `03-patterns/overlay/overlay.css` | 319 | **必需
|
|
57
|
+
| 弹窗 / 吐司(beta) | `03-patterns/overlay/overlay.css` | 319 | **必需 569** (`overlay.js`) | 需要弹层(吐司 / 模态)就要 |
|
|
58
58
|
| 状态(空 / 加载 / 错 / 成功) | `03-patterns/states/states.css` | 252 | — | **凡是会异步取数就要** |
|
|
59
59
|
| 表单校验 | `03-patterns/form-validation/form-validation.css` | 263 | — | 提交前要校验就要 |
|
|
60
60
|
| 列表增删 | `03-patterns/list/list.css` | 162 | 可选 201 (`flip.js`,FLIP 增删动画,纯增强) | 列表会动态增删就要 |
|
package/ai/cli.js
CHANGED
|
@@ -256,6 +256,153 @@ function cmdComponents(args) {
|
|
|
256
256
|
process.exit(0);
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
// 🔴 ai assets —— 组件之外那批资产的机器可读目录(WO-03)
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
// 为什么要有这条:
|
|
262
|
+
// `ai components` 只认 `components.json`,而它只收「有独立 CSS 的目录」⇒ 37 个。
|
|
263
|
+
// 磁盘上还有 5 项不是组件、但 AI 不知道就会自己重写的东西:
|
|
264
|
+
// · 01-tokens/page.css 页面骨架与响应式两栏
|
|
265
|
+
// · 01-tokens/behavior/ 微行为核(Esc 关闭 / 焦点归还 / 方向键游走)
|
|
266
|
+
// · 04-recipes/longform 单文件长文示例页(连 demo.html 都没有)
|
|
267
|
+
// · 04-recipes/data-showcase
|
|
268
|
+
// · 09-assets/echarts-adapter 依赖宿主已有 ECharts(本库不含)
|
|
269
|
+
// 后果很具体:不知道 page.css 就自己写布局,不知道 behavior/ 就手写
|
|
270
|
+
// dismissable —— 后者正是 4 个 beta 组件被判 beta 的原因(dismissable:manual)。
|
|
271
|
+
//
|
|
272
|
+
// ⚠️ 本命令的清单**从磁盘扫描生成**,禁止手工维护第二份事实。
|
|
273
|
+
// `05-audit/assets-catalog-gate.py` 校验「磁盘目录数 == 本命令输出数」,
|
|
274
|
+
// 两边一旦漂移就变红。
|
|
275
|
+
var ROOT = path.join(AI, '..');
|
|
276
|
+
var ASSET_LAYERS = ['02-primitives', '03-patterns', '04-recipes', '09-assets'];
|
|
277
|
+
|
|
278
|
+
function readFirstUsefulLine(file) {
|
|
279
|
+
try {
|
|
280
|
+
var s = fs.readFileSync(file, 'utf8').split('\n');
|
|
281
|
+
for (var i = 0; i < s.length && i < 90; i++) {
|
|
282
|
+
var t = s[i].trim();
|
|
283
|
+
if (!t) continue;
|
|
284
|
+
// 块注释边界与装饰线:跳过,不是描述
|
|
285
|
+
if (/^\/\*+[=\s*-]*$/.test(t)) continue;
|
|
286
|
+
if (/^[=\s-]*\*+\/$/.test(t)) continue;
|
|
287
|
+
if (/^\*+[=\s*-]*$/.test(t)) continue;
|
|
288
|
+
if (/^={3,}/.test(t) || /^-{3,}/.test(t)) continue;
|
|
289
|
+
// 注释内的内容行:去掉前导 * 再判断
|
|
290
|
+
if (t.charAt(0) === '*') { t = t.replace(/^\*+\s*/, '').trim(); if (!t) continue; }
|
|
291
|
+
if (t.length > 2 && t.slice(-2) === '*/') { t = t.slice(0, -2).trim(); if (!t) continue; }
|
|
292
|
+
if (t.charAt(0) === '#') { t = t.replace(/^#+\s*/, '').trim(); if (t) return t.slice(0, 72); continue; }
|
|
293
|
+
// Markdown 表格:只认「用途」这类带说明的字段,其余(来源/门禁/文件)跳过
|
|
294
|
+
var m = t.match(/^\|\s*\*\*([^*]+)\*\*\s*\|\s*([^|]+)\|/);
|
|
295
|
+
if (m) {
|
|
296
|
+
if (m[1].indexOf('用途') >= 0 || m[1].indexOf('定位') >= 0) return m[2].trim().slice(0, 72);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (t.charAt(0) === '|') continue;
|
|
300
|
+
// 无中文的「a / b / c」是路径面包屑,不是描述
|
|
301
|
+
if (!/[\u4e00-\u9fa5]/.test(t) && t.indexOf(' / ') >= 0) continue;
|
|
302
|
+
if (!/[\u4e00-\u9fa5]/.test(t) && /\.(css|js|html)$/.test(t)) continue;
|
|
303
|
+
return t.slice(0, 72);
|
|
304
|
+
}
|
|
305
|
+
} catch (e) { /* 读不到就当没描述,不抛 */ }
|
|
306
|
+
return '';
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function describeAsset(dir, name, purpose) {
|
|
310
|
+
// 在契约里的,直接用契约的 purpose(比 README 标题精确)
|
|
311
|
+
if (purpose) return purpose;
|
|
312
|
+
var d = path.join(ROOT, dir, name);
|
|
313
|
+
var rd = path.join(d, 'README.md');
|
|
314
|
+
if (fs.existsSync(rd)) {
|
|
315
|
+
var txt = fs.readFileSync(rd, 'utf8');
|
|
316
|
+
var m = txt.match(/\|\s*\*\*用途\*\*\s*\|\s*([^|\n]+)/);
|
|
317
|
+
if (m) return m[1].trim().slice(0, 72);
|
|
318
|
+
return readFirstUsefulLine(rd);
|
|
319
|
+
}
|
|
320
|
+
// 没有 README:回退到目录内第一个脚本的头部注释
|
|
321
|
+
var f2 = fs.readdirSync(d).filter(function (f) {
|
|
322
|
+
return /\.(js|css)$/.test(f) && f.indexOf('demo') !== 0;
|
|
323
|
+
});
|
|
324
|
+
if (f2.length) return readFirstUsefulLine(path.join(d, f2[0]));
|
|
325
|
+
return '';
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// 类型判定必须机械可判,不查名单:
|
|
329
|
+
// 在组件契约里 → component
|
|
330
|
+
// 09-assets 下的目录 → adapter(都是「消费外部能力」的适配层)
|
|
331
|
+
// 有 CSS 但不在契约 → component(意味着契约漏了它,门禁会报)
|
|
332
|
+
// 无 CSS 有 demo.html → recipe(示例页)
|
|
333
|
+
// 其余 → example(单文件示例)
|
|
334
|
+
function classifyAsset(layer, name, inContract, files) {
|
|
335
|
+
if (inContract[name]) return 'component';
|
|
336
|
+
if (layer === '09-assets') return 'adapter';
|
|
337
|
+
if (files.some(function (f) { return /\.css$/.test(f); })) return 'component';
|
|
338
|
+
if (files.indexOf('demo.html') >= 0) return 'recipe';
|
|
339
|
+
return 'example';
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function collectAssets() {
|
|
343
|
+
var doc = readJSON(path.join(AI, 'components.json'));
|
|
344
|
+
var inContract = {};
|
|
345
|
+
var purposeOf = {};
|
|
346
|
+
doc.components.forEach(function (c) {
|
|
347
|
+
inContract[c.id] = true;
|
|
348
|
+
purposeOf[c.id] = c.purpose || '';
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
var rows = [];
|
|
352
|
+
ASSET_LAYERS.forEach(function (layer) {
|
|
353
|
+
var abs = path.join(ROOT, layer);
|
|
354
|
+
if (!fs.existsSync(abs)) return;
|
|
355
|
+
fs.readdirSync(abs).sort().forEach(function (n) {
|
|
356
|
+
// 下划线/点开头是临时目录与隐藏目录,不是资产(扫描口径,不是白名单)
|
|
357
|
+
if (n.charAt(0) === '_' || n.charAt(0) === '.') return;
|
|
358
|
+
var p = path.join(abs, n);
|
|
359
|
+
if (!fs.statSync(p).isDirectory()) return;
|
|
360
|
+
var files = fs.readdirSync(p);
|
|
361
|
+
rows.push({
|
|
362
|
+
id: n, layer: layer, type: classifyAsset(layer, n, inContract, files),
|
|
363
|
+
demo: files.indexOf('demo.html') >= 0, contract: !!inContract[n],
|
|
364
|
+
desc: describeAsset(layer, n, purposeOf[n])
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// 01-tokens 层:同样靠扫描,不写死名字。
|
|
370
|
+
// behavior/ 目录 → behavior;*.css → base;*.js → behavior;*.html 是 demo,跳过
|
|
371
|
+
var tk = path.join(ROOT, '01-tokens');
|
|
372
|
+
if (fs.existsSync(tk)) {
|
|
373
|
+
fs.readdirSync(tk).sort().forEach(function (n) {
|
|
374
|
+
if (n.charAt(0) === '_' || n.charAt(0) === '.') return;
|
|
375
|
+
var p = path.join(tk, n);
|
|
376
|
+
var st = fs.statSync(p);
|
|
377
|
+
if (st.isDirectory()) {
|
|
378
|
+
rows.push({ id: n, layer: '01-tokens', type: 'behavior', demo: false,
|
|
379
|
+
contract: false, desc: describeAsset('01-tokens', n, '') });
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (/\.html$/.test(n)) return;
|
|
383
|
+
var t = /\.css$/.test(n) ? 'base' : /\.js$/.test(n) ? 'behavior' : null;
|
|
384
|
+
if (!t) return;
|
|
385
|
+
rows.push({ id: n, layer: '01-tokens', type: t, demo: false, contract: false,
|
|
386
|
+
desc: readFirstUsefulLine(p) });
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
return rows;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function cmdAssets(args) {
|
|
393
|
+
var rows = collectAssets();
|
|
394
|
+
if (flag('json', false)) { out(rows); process.exit(0); }
|
|
395
|
+
var miss = rows.filter(function (r) { return !r.contract; });
|
|
396
|
+
process.stdout.write(' === 全部资产(' + rows.length + ' 项)===\n');
|
|
397
|
+
process.stdout.write(' 契约内 ' + (rows.length - miss.length) + ' 项 · 契约外 ' + miss.length + ' 项\n\n');
|
|
398
|
+
rows.forEach(function (r) {
|
|
399
|
+
process.stdout.write(' ' + (r.contract ? '✅' : '○') + ' ' + pad(r.id, 22) +
|
|
400
|
+
'[' + r.type + ']' + (r.demo ? ' demo' : ' ') + ' ' + r.desc + '\n');
|
|
401
|
+
});
|
|
402
|
+
process.stdout.write('\n ✅ 在组件契约内(可 ai components 查详情) ○ 契约外(需看本节)\n');
|
|
403
|
+
process.exit(0);
|
|
404
|
+
}
|
|
405
|
+
|
|
259
406
|
function cmdTokens(args) {
|
|
260
407
|
var doc = readJSON(path.join(AI, 'tokens.json'));
|
|
261
408
|
var set = flag('set', null);
|
|
@@ -351,6 +498,7 @@ var CMDS = {
|
|
|
351
498
|
diff: cmdDiff,
|
|
352
499
|
patch: cmdPatch,
|
|
353
500
|
components: cmdComponents,
|
|
501
|
+
assets: cmdAssets,
|
|
354
502
|
tokens: cmdTokens,
|
|
355
503
|
tree: cmdTree,
|
|
356
504
|
profile: cmdProfile,
|
|
@@ -367,6 +515,7 @@ function main() {
|
|
|
367
515
|
' ai diff <改动前.json> <改动后.json> 按语义 id 比对\n' +
|
|
368
516
|
' ai patch <doc.json> <patch.json> [-o out.json]\n' +
|
|
369
517
|
' ai components [--maturity=stable|beta|alpha]\n' +
|
|
518
|
+
' ai assets [--json] 组件之外的资产:布局 / 微行为核 / 示例页 / 适配器\n' +
|
|
370
519
|
' ai tokens [--set=light|dark]\n' +
|
|
371
520
|
' ai tree [--set=light|dark] [--cat=color]\n' +
|
|
372
521
|
' ai profile 列出三档及其差异\n' +
|
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.
|
|
4
|
+
"libraryVersion": "0.13.1",
|
|
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 下的专属门禁 ⇒ 还没被机器验证过,不要用",
|
package/dist/COSTS.md
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
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
42
|
| pattern | `nav` | 2.7 KB | 1.2 KB | 2.6 KB | **6.5 KB** | 30.9 KB | −24.4 KB |
|
|
43
|
-
| pattern | `overlay` | 2.7 KB | 1.1 KB | 2.8 KB | **6.7 KB** | 33.
|
|
43
|
+
| pattern | `overlay` | 2.7 KB | 1.1 KB | 2.8 KB | **6.7 KB** | 33.9 KB | −27.2 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 |
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
## 三、全库
|
|
52
52
|
|
|
53
53
|
- 全部组件都用上(min,含 tokens 一份):**61.2 KB**
|
|
54
|
-
- 源码版(带注释)全库 gzip:302.
|
|
54
|
+
- 源码版(带注释)全库 gzip:302.3 KB
|
|
55
55
|
|
|
56
56
|
> ⚠️ 源码版那个数字**不是**要付的钱:它包含我们的注释(全库 CSS 有 67% 是注释,字节口径)。
|
|
57
57
|
> 它的用途只有一个 —— 让人看清"dist 到底省了多少"。
|
|
@@ -62,6 +62,14 @@ python3 -m http.server 8000 --bind 127.0.0.1
|
|
|
62
62
|
| tabs | 未执行 | 未执行 | — | — | — | |
|
|
63
63
|
| overlay | 未执行 | 未执行 | — | — | 未执行 | |
|
|
64
64
|
| table | 未执行 | 未执行 | — | — | — | |
|
|
65
|
+
| **accordion** | 未执行 | 未执行 | — | — | — | 重点:展开/收起时 `aria-expanded` 有没有读出来,面板与标题的关联有没有读 |
|
|
66
|
+
| **drawer** | 未执行 | 未执行 | — | — | 未执行 | 重点:抽屉打开时焦点有没有进到里面;Esc 关闭后焦点有没有回到触发按钮 |
|
|
67
|
+
| **dropdown** | 未执行 | 未执行 | — | — | 未执行 | 重点:方向键在菜单项之间的移动能否被读出;Esc 关列表后焦点在哪 |
|
|
68
|
+
|
|
69
|
+
> **为什么后三行是补的**:`accordion` / `drawer` / `dropdown` 是当前仅有的 beta 组件里的三个,
|
|
70
|
+
> 而它们升 stable 的判据之一就是「行为零欠账」——人工读屏是这件事**唯一的用户侧证据**。
|
|
71
|
+
> 只覆盖 `overlay` 一个浮层,等于对另外两个浮层的真实表现没有任何记录。
|
|
72
|
+
> 这三行同样**未执行**,加行不等于做过。
|
|
65
73
|
|
|
66
74
|
状态只填三种:**通过** / **失败(附听到的内容)** / **未执行**。
|
|
67
75
|
"大概没问题"不属于这三种,不要写。
|
package/docs/ACCESSIBILITY.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
| 项目 | 值 |
|
|
7
7
|
|---|---|
|
|
8
|
-
| 声明适用版本 | agentcss `0.
|
|
8
|
+
| 声明适用版本 | agentcss `0.13.1` |
|
|
9
9
|
| 声明日期 | 2026-10-29 |
|
|
10
10
|
| 复核节奏 | 每次发版前重跑(`npm run gate` 内含全部证据) |
|
|
11
11
|
| 范围 | 本库交付的 CSS / JS、全部 `demo.html`(组件页 + 图表页 + 组合页)、以及文档站生成的页面 |
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
| 3.3.2 | 标签或说明 | ✅ | 必填项有可见标记 + `required` |
|
|
68
68
|
| 3.3.7 | 冗余输入(2.2 A) | — | 由业务表单决定,库不承载 |
|
|
69
69
|
| 3.3.8 | 可访问的认证(2.2 AA) | — | 库不含登录流程 |
|
|
70
|
-
| 4.1.2 | 名称、角色、值 | ✅ | `a11y-ax.js`:**全部
|
|
70
|
+
| 4.1.2 | 名称、角色、值 | ✅ | `a11y-ax.js`:**全部 40 个 demo 页**的可访问性树逐节点判,每个可交互控件有非空名称;朗读序列逐页存快照防回归 |
|
|
71
71
|
| 4.1.3 | 状态消息 | ◐ | toast / 表单错误用 `role="status"` / `alert`;**未**在真实读屏里验证播报时机 |
|
|
72
72
|
|
|
73
73
|
**统计:✅ 27 条 · ◐ 7 条 · ✗ 0 条 · — 12 条。**
|
|
@@ -80,11 +80,11 @@
|
|
|
80
80
|
|
|
81
81
|
| 手段 | 覆盖 | 最近结果 |
|
|
82
82
|
|---|---|---|
|
|
83
|
-
| `axe-core`(`a11y-scan.js`) |
|
|
83
|
+
| `axe-core`(`a11y-scan.js`) | 39 页(37 组件 demo + 2 个非组件页) | **0 类违规**,991 条规则通过 |
|
|
84
84
|
| 反向控制(注入 5 类典型违规) | 证明上一条不是"没查" | 5 类全部抓到 |
|
|
85
|
-
| `a11y-ax.js` 可访问性树 | **全部 `demo.html`(现
|
|
85
|
+
| `a11y-ax.js` 可访问性树 | **全部 `demo.html`(现 40 个)**;清单与磁盘不一致即红 | 0 个无名控件;40 份朗读序列快照在 `10-review/a11y-tree/` |
|
|
86
86
|
| `contrast.py` / `dark-contrast.js` | 亮 / 暗两套,逐对实算 | 正文 ≥ 4.5:1 |
|
|
87
|
-
| `zoom-reflow.js` |
|
|
87
|
+
| `zoom-reflow.js` | 40 页 × 3 条 AA 硬项 | 320px 不横滚 · 200% 不裁字 · 焦点不被遮挡 |
|
|
88
88
|
| `focus-ring-check` / `focus-return-check` / `keyboard-nav` | 焦点可见、焦点归还、键盘路径 | 全绿 |
|
|
89
89
|
|
|
90
90
|
### 人工(**尚未执行** —— 这是本声明最大的空白)
|
package/docs/CONVENTIONS.md
CHANGED
|
@@ -194,6 +194,13 @@
|
|
|
194
194
|
- **方向必须有非颜色的载体**:`delta` 用 `+` / `−`(U+2212)符号,
|
|
195
195
|
且符号必须真的进可访问性树(CSS `::before` 生成的字符不在元素 name 里,
|
|
196
196
|
但会作为独立 StaticText 出现 —— 换空串/背景图/`visibility:hidden` 就会没)。
|
|
197
|
+
- **容器不许再加一套方向类**。表格里只有会计正负(`num--pos` / `num--neg`),
|
|
198
|
+
市场涨跌一律嵌 `<span class="delta delta--up">`
|
|
199
|
+
—— 否则 `table` 会有四组颜色类,调用方每列都要记"这列用哪组"。
|
|
200
|
+
表格只管对齐(`td.num`),不管判色。
|
|
201
|
+
- **零值不带符号**:符号必须跟**打印出来的数字**同号。
|
|
202
|
+
`0` 与「四舍五入到 0」(如精度 1 下的 `0.0004`)都不加 `+` / `−`
|
|
203
|
+
—— 「+0.0%」等于把持平念成上涨。会计列与市场列是同一条规则。
|
|
197
204
|
|
|
198
205
|
## 十一 · 每条规则由什么挡住
|
|
199
206
|
|
|
@@ -213,6 +220,7 @@
|
|
|
213
220
|
| 触控目标 44px | `a11y.py`(静态) + 行为脚本(运行时) |
|
|
214
221
|
| 页面骨架只有一份(改令牌能牵动全部页面) | `skeleton-lever-check.js` |
|
|
215
222
|
| 涨跌方向色对、口径可切、符号进可访问性树 | `delta-check.js` |
|
|
223
|
+
| 表格里的方向语义(涨跌嵌 `delta`、零值不带符号、口径只翻市场列) | `table-check.js` ⑫⑬⑭ |
|
|
216
224
|
| 指标缺数是显式状态、数字等宽对齐 | `metric-check.js` |
|
|
217
225
|
| 文档里的数字与实际一致(组件数/门禁数/精简档令牌数) | `doc-facts-gate.py` |
|
|
218
226
|
|
package/index.html
CHANGED