agentcss 0.25.5 → 0.26.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.
Files changed (59) hide show
  1. package/01-tokens/behavior/labels.js +44 -0
  2. package/02-primitives/combobox/combobox.js +26 -3
  3. package/02-primitives/date-range/date-range.js +27 -5
  4. package/02-primitives/popover/popover.js +22 -2
  5. package/02-primitives/select/select.js +23 -2
  6. package/02-primitives/upload/upload.js +50 -17
  7. package/03-patterns/content/content.css +13 -1
  8. package/03-patterns/content/demo.html +1 -1
  9. package/03-patterns/drawer/drawer.js +44 -6
  10. package/03-patterns/dropdown/dropdown.js +21 -1
  11. package/03-patterns/overlay/overlay.js +39 -2
  12. package/03-patterns/pagination/pagination.js +37 -10
  13. package/03-patterns/tooltip/tooltip.js +17 -2
  14. package/03-patterns/tree/tree.js +21 -1
  15. package/04-recipes/analysis-report/demo.html +1 -1
  16. package/04-recipes/data-showcase/demo.html +2 -2
  17. package/04-recipes/longform/content.html +989 -74
  18. package/04-recipes/longform/longform.html +3 -3
  19. package/04-recipes/report/demo.html +1 -1
  20. package/04-recipes/table/README.md +22 -0
  21. package/04-recipes/table/demo.html +6 -6
  22. package/09-assets/bar/bar.js +28 -4
  23. package/09-assets/model-viewer/model-viewer.js +44 -12
  24. package/09-assets/scientific-plot/demo.html +6 -6
  25. package/API.md +32 -0
  26. package/CHANGELOG.md +129 -0
  27. package/CONTRIBUTING.md +4 -3
  28. package/README.md +19 -2
  29. package/README.zh-CN.md +18 -2
  30. package/SECURITY.md +16 -4
  31. package/START-HERE.md +14 -14
  32. package/SUPPORT.md +2 -2
  33. package/ai/components.json +9 -4
  34. package/ai/pdf.js +50 -8
  35. package/dist/01-tokens/behavior/labels.js +11 -0
  36. package/dist/02-primitives/combobox/combobox.js +25 -3
  37. package/dist/02-primitives/date-range/date-range.js +26 -5
  38. package/dist/02-primitives/popover/popover.js +14 -1
  39. package/dist/02-primitives/select/select.js +22 -2
  40. package/dist/02-primitives/upload/upload.js +47 -17
  41. package/dist/03-patterns/content/content.css +1 -1
  42. package/dist/03-patterns/drawer/drawer.js +32 -4
  43. package/dist/03-patterns/dropdown/dropdown.js +20 -1
  44. package/dist/03-patterns/overlay/overlay.js +25 -1
  45. package/dist/03-patterns/pagination/pagination.js +34 -10
  46. package/dist/03-patterns/tooltip/tooltip.js +14 -2
  47. package/dist/03-patterns/tree/tree.js +20 -1
  48. package/dist/09-assets/bar/bar.js +23 -4
  49. package/dist/09-assets/model-viewer/model-viewer.js +38 -12
  50. package/dist/COSTS.md +14 -14
  51. package/docs/ACCESSIBILITY.md +2 -2
  52. package/docs/L10N.md +92 -0
  53. package/docs/LAYER-BASELINE.md +2 -2
  54. package/docs/RELEASE-PROCESS.md +7 -1
  55. package/docs/VERIFY-RELEASE.md +7 -2
  56. package/index.html +1 -1
  57. package/llms-full.txt +2 -2
  58. package/package.json +17 -6
  59. package/types/index.d.ts +218 -13
@@ -479,7 +479,7 @@ th {
479
479
  哪怕会覆盖掉别人写的东西。
480
480
  </p>
481
481
 
482
- <div class="table-wrap">
482
+ <div class="table-wrap" tabindex="0" role="region" aria-label="对照表(可横向滚动)">
483
483
  <table>
484
484
  <colgroup>
485
485
  <col style="width: 34%">
@@ -520,7 +520,7 @@ th {
520
520
  这套机制加上"准入控制",让关键任务(姿态、点火)不会被普通计算饿死。
521
521
  </p>
522
522
 
523
- <pre><code><span class="c-cmt"># 不是原文,是按手册重述的伪代码</span>
523
+ <pre tabindex="0"><code><span class="c-cmt"># 不是原文,是按手册重述的伪代码</span>
524
524
  if computer_overload():
525
525
  issue(1202) <span class="c-cmt"># 告警编号写入 AGC 计数区</span>
526
526
  <span class="c-cmt"># 不停机:停机会丢掉姿态信息,地面将失去唯一的实时数据源</span>
@@ -540,7 +540,7 @@ if computer_overload():
540
540
  包括读取真实的操作数、写入真实的寄存器地址。
541
541
  </p>
542
542
 
543
- <pre><code><span class="c-cmt"># 一段能真正跑起来的最小例子</span>
543
+ <pre tabindex="0"><code><span class="c-cmt"># 一段能真正跑起来的最小例子</span>
544
544
  <span class="c-out">$</span> python3 -m agc --help
545
545
  <span class="c-out">$</span> ./yaAGC -t demo.agc <span class="c-cmt"># 载入磁带并执行</span>
546
546
  <span class="c-out">AGC 0000 GO</span></code></pre>
@@ -32,7 +32,7 @@
32
32
  <!-- ======================================================================== -->
33
33
  <h2 class="rule-top">先看取舍:三套版式各自放弃了什么</h2>
34
34
 
35
- <div class="table"><table>
35
+ <div class="table" tabindex="0" role="region" aria-label="三种 profile 的结构差异(可横向滚动)"><table>
36
36
  <caption class="table__cap">表 A · 三种 profile 的结构差异(判据可验证,不是文案差异)</caption>
37
37
  <thead><tr>
38
38
  <th scope="col">profile</th>
@@ -65,6 +65,28 @@ var COLS = [
65
65
 
66
66
  ## 无障碍
67
67
 
68
+ - **滚动容器必须能用键盘进入。** `.table` 是 `overflow:auto` 的二维滚动区域,
69
+ 窄屏下会横向滚动;**纯 CSS 无法让它被 Tab 到**,必须在标记上给入口:
70
+
71
+ ```html
72
+ <div class="table table--financial" tabindex="0" role="region"
73
+ aria-label="A 股样例数据(可横向滚动)">
74
+ <table>…</table>
75
+ </div>
76
+ ```
77
+
78
+ 三个属性缺一不可:`tabindex="0"`(进 Tab 序列)、`role="region"`(成为可命名地标)、
79
+ `aria-label`(读屏要念出"这是什么表")。少了它们的后果是:只用键盘的用户
80
+ 和部分读屏用户**看不到超出可视区的列**(WCAG 2.1.1)。
81
+ 名字里带上 **「(可横向滚动)」**:只说"什么表"不告诉用户这里能滚,
82
+ 焦点停上来时读屏不会提示还有更多列 —— 那正是这条规则要解决的问题。
83
+ 图/图表容器同理,但名字应来自图注(`aria-labelledby` 指向 `<figcaption>`),
84
+ 不要再手写一份同名文案(同一事实不留第二出处)。
85
+ > 这条由两道门禁守着:`table-check.js` ⑮ 在 320px 下逐个量「会滚的容器有没有入口」,
86
+ > `a11y-scan.js` 在同一个页面上补跑 320px 的 `scrollable-region-focusable`。
87
+ > 桌面宽(1280)下表格不溢出,那条 axe 规则根本不会触发 —— 所以窄屏必须单独判。
88
+ > ⚠️ 只有**真的会滚**的容器需要 `tabindex`;不滚的容器加上去只是多一个空的 Tab 停靠点。
89
+
68
90
  - 数据表用 `<th scope="col">` / `<th scope="row">`;多级表头第一层用 `scope="colgroup"`。
69
91
  复杂表格可用 `id` + `headers` 建立显式关联(本示例未用到,但结构支持)。
70
92
  - 排序状态放在 `<th aria-sort="ascending|descending">`,**不是**放在按钮上。
@@ -131,7 +131,7 @@
131
131
  <strong>空格子和 0 是两件事</strong>:空格子读者分不清"没有数据"还是"漏了",
132
132
  0 会被当成真实数值参与心算。
133
133
  </p>
134
- <div class="table table--compact">
134
+ <div class="table table--compact" tabindex="0" role="region" aria-label="银行对账明细(可横向滚动)">
135
135
  <table>
136
136
  <caption class="table__cap">表 G · 银行对账明细(示例,含缺失值)</caption>
137
137
  <thead>
@@ -353,7 +353,7 @@
353
353
  });
354
354
  }
355
355
 
356
- var h = '<div class="table table--financial"><table>' +
356
+ var h = '<div class="table table--financial" tabindex="0" role="region" aria-label="A 股样例数据(可横向滚动)"><table>' +
357
357
  '<caption class="table__cap">A 股样例数据(非真实财报,量级参照公开年报)· ' +
358
358
  '金额单位:万元 · 比率为比例值</caption>' +
359
359
  '<thead><tr>';
@@ -425,7 +425,7 @@
425
425
  { label: '时效 (150 °C)', key: 'h150' }
426
426
  ];
427
427
 
428
- var a = '<div class="table table--academic"><table>' +
428
+ var a = '<div class="table table--academic" tabindex="0" role="region" aria-label="时效温度对铝合金拉伸性能的影响(可横向滚动)"><table>' +
429
429
  '<caption class="table__cap">表 1 · 不同时效温度下铝合金的拉伸性能(示例数据)</caption>' +
430
430
  '<thead><tr><th scope="col" rowspan="2">合金牌号</th>';
431
431
  GROUPS.forEach(function (g) {
@@ -472,7 +472,7 @@
472
472
  return fixed(p, 3).replace(/^0\./, '.');
473
473
  }
474
474
 
475
- var s = '<div class="table table--stats"><table>' +
475
+ var s = '<div class="table table--stats" tabindex="0" role="region" aria-label="OLS 估计结果(可横向滚动)"><table>' +
476
476
  '<caption class="table__cap">表 2 · 处理效应的 OLS 估计(示例数据,n = 412)</caption>' +
477
477
  '<thead><tr>' +
478
478
  '<th scope="col">项</th>' +
@@ -512,7 +512,7 @@
512
512
  why: '缺失 ⇒ 显示 —,不是 0,也不带符号' }
513
513
  ];
514
514
 
515
- var p = '<div class="table"><table>' +
515
+ var p = '<div class="table" tabindex="0" role="region" aria-label="同一个数在不同列定义下的显示(可横向滚动)"><table>' +
516
516
  '<caption class="table__cap">同一个数、不同的列定义 ⇒ 不同的显示</caption>' +
517
517
  '<thead><tr><th scope="col">列定义</th><th scope="col">输入</th>' +
518
518
  '<th scope="col">渲染结果</th><th scope="col">为什么</th></tr></thead><tbody>';
@@ -547,7 +547,7 @@
547
547
  expect: '显示为字面字符' }
548
548
  ];
549
549
 
550
- var e2 = '<div class="table"><table>' +
550
+ var e2 = '<div class="table" tabindex="0" role="region" aria-label="转义演示:动态文本按文本渲染(可横向滚动)"><table>' +
551
551
  '<caption class="table__cap">动态文本按**文本**渲染,不当 HTML 解释</caption>' +
552
552
  '<thead><tr><th scope="col">列定义</th><th scope="col">原始数据</th>' +
553
553
  '<th scope="col">渲染结果</th><th scope="col">验收点</th></tr></thead><tbody>';
@@ -95,9 +95,35 @@
95
95
  * 并且只画(画布内的那一部分),不能画到坐标系外面去
96
96
  * ③ **标注跟着堆叠顶端走**:标签写的是**该列合计**,不是第一个系列的值
97
97
  */
98
+ /* 可见文案默认值(F2 / 0.26.0)。
99
+ ⚠️ 这里用 `opt.text` 而不是 `labels` —— `bar.draw` 的 `labels`
100
+ 早就是「X 轴标签」这个**数据**参数,改名是破坏性变更。
101
+ 同类情况全库只此一处,已在 docs/L10N.md 写明。 */
102
+ var DEFAULT_TEXT = {
103
+ series: function (i) { return '系列 ' + i; },
104
+ clipped: function (n, isStack, max) {
105
+ return '有 ' + n + ' 个' + (isStack ? '类目的合计' : '数据')
106
+ + '超出量程(上限 ' + max + '),已按上限截断显示。';
107
+ }
108
+ };
109
+
110
+ /* ==== BEHAVIOR INJECT BEGIN: labels ==== */
111
+ var flLabels = function (defaults, src) {
112
+ src = src || {};
113
+ var out = {};
114
+ var k;
115
+ for (k in defaults) {
116
+ if (Object.prototype.hasOwnProperty.call(defaults, k)) {
117
+ out[k] = (src[k] === undefined || src[k] === null) ? defaults[k] : src[k];
118
+ }
119
+ }
120
+ return out;
121
+ };
122
+ /* ==== BEHAVIOR INJECT END: labels ==== */
98
123
  function draw(opt) {
99
124
  opt = opt || {};
100
125
  var host = opt.host;
126
+ var T = flLabels(DEFAULT_TEXT, opt && opt.text);
101
127
  if (!host) throw new Error('bar.draw: 缺少 host');
102
128
  if (opt.max === undefined || opt.max === null) {
103
129
  /* 🔴 铁律三:不替调用方决定量程。
@@ -286,7 +312,7 @@
286
312
  /* 🔴 用 textContent,不用 innerHTML —— 系列名来自调用方的数据 */
287
313
  li.appendChild(sw);
288
314
  var tx = document.createElement('span');
289
- tx.textContent = se.name || ('系列 ' + (si + 1));
315
+ tx.textContent = se.name || T.series(si + 1);
290
316
  li.appendChild(tx);
291
317
  legendEl.appendChild(li);
292
318
  });
@@ -302,9 +328,7 @@
302
328
  } else if (clipped.length) {
303
329
  var p2 = document.createElement('p');
304
330
  p2.className = 'bar__note';
305
- p2.textContent = '有 ' + clipped.length + ' 个' +
306
- (mode === 'stack' ? '类目的合计' : '数据') +
307
- '超出量程(上限 ' + max + '),已按上限截断显示。';
331
+ p2.textContent = T.clipped(clipped.length, mode === 'stack', max);
308
332
  host.appendChild(p2);
309
333
  }
310
334
 
@@ -110,7 +110,7 @@
110
110
  n.appendChild(this.ui);
111
111
 
112
112
  // 加载按钮
113
- this.btn = el('button', 'mv__btn', '加载 3D 模型');
113
+ this.btn = el('button', 'mv__btn', LABELS.load);
114
114
  this.btn.type = 'button';
115
115
  this.btn.addEventListener('click', function () { self.load(); });
116
116
  this.ui.appendChild(this.btn);
@@ -120,23 +120,23 @@
120
120
  原来用 innerHTML 直接拼 ⇒ `<img onerror>` 这类值会被解析成节点。
121
121
  改成 textContent 组装:`<code>` 与 `<b>` 仍然存在,但文本永远只是文本。 */
122
122
  var fname = el('code', null, this.src.split('/').pop() || '—');
123
- var strong = el('b', null, '点击后才下载');
124
- this.hint.appendChild(document.createTextNode('模型 '));
123
+ var strong = el('b', null, LABELS.hintStrong);
124
+ this.hint.appendChild(document.createTextNode(LABELS.hintBefore));
125
125
  this.hint.appendChild(fname);
126
- this.hint.appendChild(document.createTextNode(' 会'));
126
+ this.hint.appendChild(document.createTextNode(LABELS.hintBetween));
127
127
  this.hint.appendChild(strong);
128
- this.hint.appendChild(document.createTextNode(',首屏不占流量。'));
128
+ this.hint.appendChild(document.createTextNode(LABELS.hintTail));
129
129
  this.ui.appendChild(this.hint);
130
130
 
131
131
  if (!hasWebGL()) {
132
- this.fail('这台设备的浏览器不支持 WebGL,无法显示 3D 模型。');
132
+ this.fail(LABELS.noWebGL);
133
133
  this.btn.disabled = true;
134
134
  }
135
135
  };
136
136
 
137
137
  Viewer.prototype.makeIdle = function () {
138
138
  var box = el('div', 'mv__stage');
139
- box.appendChild(el('div', 'mv__ph', '3D 模型'));
139
+ box.appendChild(el('div', 'mv__ph', LABELS.placeholder));
140
140
  return box;
141
141
  };
142
142
 
@@ -146,14 +146,14 @@
146
146
  this.state = 'error';
147
147
  this.ui.innerHTML = '';
148
148
  var s = el('div', 'mv__stage mv__stage--err');
149
- s.appendChild(el('div', 'mv__ph', '3D 模型'));
149
+ s.appendChild(el('div', 'mv__ph', LABELS.placeholder));
150
150
  var p = el('p', 'mv__err', msg);
151
151
  s.appendChild(p);
152
152
  this.ui.appendChild(s);
153
153
  if (this.btn) this.ui.appendChild(this.btn);
154
154
  if (this.btn) {
155
155
  this.btn.disabled = false;
156
- this.btn.textContent = '重试';
156
+ this.btn.textContent = LABELS.retry;
157
157
  }
158
158
  };
159
159
 
@@ -169,12 +169,12 @@
169
169
 
170
170
  this.ui.innerHTML = '';
171
171
  var stage = el('div', 'mv__stage');
172
- this.statusNode = el('p', 'mv__status', '正在准备…');
172
+ this.statusNode = el('p', 'mv__status', LABELS.preparing);
173
173
  stage.appendChild(this.statusNode);
174
174
  this.ui.appendChild(stage);
175
175
 
176
176
  var t0 = (window.performance && performance.now) ? performance.now() : Date.now();
177
- this.status('正在准备 3D 引擎…');
177
+ this.status(LABELS.preparingEngine);
178
178
  this.boot(null, t0, 0);
179
179
 
180
180
  /* 🔴🔴 **超时兜底**(实测:服务器上会卡死在 loading)
@@ -296,7 +296,7 @@
296
296
  xhr.onprogress = function (e) {
297
297
  if (e.lengthComputable && e.total) {
298
298
  self1.lastBytes = e.total;
299
- self1.status('正在下载模型 ' + Math.round(e.loaded / e.total * 100) + '%');
299
+ self1.status(LABELS.downloading(Math.round(e.loaded / e.total * 100)));
300
300
  }
301
301
  };
302
302
  xhr.onload = function () {
@@ -396,6 +396,36 @@
396
396
 
397
397
  /* ---------- 对外 API ---------- */
398
398
 
399
+ /* 可见 / 读屏文案(F2 / 0.26.0)。
400
+ 本组件由 `[data-mv]` 自动初始化、没有逐实例 opts ⇒ 用**模块级**设置:
401
+ `ModelViewer.setLabels({ load: 'Load 3D model', ... })`
402
+ ⚠️ 已知仍未覆盖的(列在 docs/L10N.md,不假装已做完):
403
+ `mv__cap` 的说明句、`fail()` 的几条错误句 ——
404
+ 它们含 `<b>` 结构与多个动态量,要改成节点拼装才能安全本地化。 */
405
+ var LABELS = {
406
+ load: '加载 3D 模型',
407
+ placeholder: '3D 模型',
408
+ retry: '重试',
409
+ preparing: '正在准备…',
410
+ preparingEngine: '正在准备 3D 引擎…',
411
+ hintBefore: '模型 ',
412
+ hintBetween: ' 会',
413
+ hintStrong: '点击后才下载',
414
+ hintTail: ',首屏不占流量。',
415
+ downloading: function (pct) { return '正在下载模型 ' + pct + '%'; },
416
+ noWebGL: '这台设备的浏览器不支持 WebGL,无法显示 3D 模型。'
417
+ };
418
+
419
+ function setLabels(patch) {
420
+ if (!patch) return;
421
+ for (var k in LABELS) {
422
+ if (Object.prototype.hasOwnProperty.call(LABELS, k)
423
+ && patch[k] !== undefined && patch[k] !== null) {
424
+ LABELS[k] = patch[k];
425
+ }
426
+ }
427
+ }
428
+
399
429
  var ModelViewer = {
400
430
  /** 扫描并初始化页面里的所有查看器 */
401
431
  update: function (root) {
@@ -421,6 +451,8 @@
421
451
  },
422
452
  };
423
453
 
454
+ ModelViewer.setLabels = setLabels;
455
+
424
456
  if (typeof window !== 'undefined') window.ModelViewer = ModelViewer;
425
457
 
426
458
  // @autoinit: [data-mv] —— 扫属性自动 update()(详见 API.md「自动初始化」)
@@ -39,8 +39,8 @@
39
39
  本模块<strong>不做表达式求值</strong>。
40
40
  </p>
41
41
  <figure class="fig">
42
- <div class="fig__box" id="fig1"></div>
43
- <figcaption class="fig__cap">
42
+ <div class="fig__box" id="fig1" tabindex="0" role="region" aria-labelledby="fig1-cap"></div>
43
+ <figcaption class="fig__cap" id="fig1-cap">
44
44
  图 1 · 阻尼振荡位移随时间的变化。A = 1.00 m,λ = 0.35 s⁻¹,f = 1.20 Hz。
45
45
  </figcaption>
46
46
  </figure>
@@ -52,8 +52,8 @@
52
52
  也不据此推断显著性 —— 这两件事必须由测量者说清楚。
53
53
  </p>
54
54
  <figure class="fig">
55
- <div class="fig__box" id="fig2"></div>
56
- <figcaption class="fig__cap">
55
+ <div class="fig__box" id="fig2" tabindex="0" role="region" aria-labelledby="fig2-cap"></div>
56
+ <figcaption class="fig__cap" id="fig2-cap">
57
57
  图 2 · 五种催化剂在不同温度下的转化率(示例数据,误差棒为 ±1 个测量标准差)。
58
58
  </figcaption>
59
59
  </figure>
@@ -105,11 +105,11 @@
105
105
  后者画出的是一段<strong>不存在的趋势</strong>。
106
106
  </p>
107
107
  <figure class="fig">
108
- <div class="fig__box" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:var(--sp-3)">
108
+ <div class="fig__box" tabindex="0" role="region" aria-labelledby="figgap-cap" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:var(--sp-3)">
109
109
  <div id="figGap"></div>
110
110
  <div id="figZero"></div>
111
111
  </div>
112
- <figcaption class="fig__cap">
112
+ <figcaption class="fig__cap" id="figgap-cap">
113
113
  图 5 · 同一组数据,两种缺失处理。左:缺失断开(正确)。右:缺失当 0(错误,仅用于对照)。
114
114
  </figcaption>
115
115
  </figure>
package/API.md CHANGED
@@ -376,6 +376,38 @@ python3 05-audit/prefix-build.py --prefix fl
376
376
 
377
377
  ---
378
378
 
379
+ ## 文案本地化(0.26.0 起)
380
+
381
+ **库默认输出中文;`labels` 可覆盖用户可见 / 读屏会念到的文案。**
382
+
383
+ ```js
384
+ Pagination.create(node, { labels: { prev: 'Previous', next: 'Next' } });
385
+ Overlay.toast({ title: 'Saved', labels: { closeToast: 'Dismiss' } });
386
+ Tooltip.setLabels({ region: 'Tooltip' }); // 模块级(属性初始化,无逐实例 opts)
387
+ ```
388
+
389
+ - 值可以是字符串或函数(动态量用函数:`page(p, isCurrent)`、`uploading(pct)`)。
390
+ - **不传时输出与 0.25.x 逐字相同** —— 升级补丁版本不会换掉界面语言。
391
+ - `bar.draw` 是唯一例外:`labels` 已是「X 轴标签」这个**数据**参数,
392
+ 它的文案覆盖叫 `text`。
393
+ - 开发期错误信息(`throw new Error`)**不在覆盖范围**,它们不经过 UI。
394
+
395
+ 逐组件的键名、以及**已知仍未覆盖**的几处,见 `docs/L10N.md`。
396
+ 门禁:`05-audit/i18n-labels-gate.js`(静态禁硬编码 + 真浏览器验证覆盖生效、默认值不变)。
397
+
398
+ ### HTML 净化钩子
399
+
400
+ `popover` / `drawer` 的字符串 HTML 入口可以接净化器:
401
+
402
+ ```js
403
+ Popover.setSanitizer(function (html) { return DOMPurify.sanitize(html); });
404
+ Drawer.setSanitizer(function (html) { return DOMPurify.sanitize(html); });
405
+ ```
406
+
407
+ 默认**不改变行为**(仍按"调用方已净化"处理),理由与责任边界见 `SECURITY.md`。
408
+
409
+ ---
410
+
379
411
  ## 破坏性变更怎么走
380
412
 
381
413
  公开**名字**(令牌名 / 类名 / 全局与类型成员 / 事件名)的移除走**登记制**:
package/CHANGELOG.md CHANGED
@@ -16,6 +16,135 @@
16
16
 
17
17
  (无)
18
18
 
19
+ ## [0.26.0] - 2026-11-04
20
+
21
+ 这一版有两类改动:**修外部审计确认的缺陷**,以及**新增三个公开能力**
22
+ (文案覆盖、HTML 净化钩子、toast 上限)。升级不需要改代码 ——
23
+ 新增能力都有默认值,且默认值与 0.25.6 行为一致。
24
+
25
+ ### Added
26
+
27
+ - **文案本地化**:`labels` 可以覆盖用户可见 / 读屏会念到的文案。
28
+ 逐实例入口 `Overlay.toast({ labels })`、`Drawer.open({ labels })`、
29
+ `Pagination.create(node, { labels })`、`new Upload(root, { labels })`、
30
+ `Select/Combobox/Dropdown/Tree/DateRange.create(root, { labels })`;
31
+ 属性自动初始化的 `Tooltip` / `ModelViewer` 用模块级 `setLabels()`。
32
+ **不传时输出与 0.25.6 逐字相同**。`bar.draw` 的文案覆盖叫 `text`
33
+ (它的 `labels` 早就是 X 轴标签这个数据参数)。清单与已知剩余项见 `docs/L10N.md`。
34
+ - **HTML 净化钩子**:`Popover.setSanitizer(fn)` / `Drawer.setSanitizer(fn)`,
35
+ 也可逐次传 `sanitize`。默认**不改变行为**(仍按"调用方已净化"处理)——
36
+ 静默自动净化会让既有页面里的 `<b>` / `<svg>` 突然消失。边界见 `SECURITY.md`。
37
+ - **toast 同屏上限**:`Overlay.toast({ maxVisible })`,默认 4,`0` = 不限。
38
+ 超出时摘掉**最旧的**一条(此前无上限,20 条并发提示会把屏幕铺满)。
39
+ - `types/index.d.ts`:新增上列文案类型与 `ToastOptions` / `DialogOptions` /
40
+ `PopoverOptions` / `DrawerOpenOptions` / `UploadOptions` / `DropdownOptions` /
41
+ `NavOptions`;把 `ModelViewer` 从"构造函数"改正为 `{ update, refresh, setLabels }`
42
+ (按旧声明写 `new window.ModelViewer(el)` 会在运行时抛 TypeError)。
43
+
44
+ ### Fixed
45
+
46
+ - `longform` 配方页:`.prose > img` 是**直接子代**选择器,而 Markdown 渲染出的图片
47
+ 在 `<p>` 里 ⇒ `max-width:100%` 从未生效,600px 宽的图在 375px 视口横向溢出
48
+ 246px、320px 溢出 301px。改为后代选择器(`> a` 在 0.9 踩过同一个坑,这次同一个坑换了标签)。
49
+ - 可滚动的 `.table` / `.table-wrap` / `<pre>` / 图框在窄屏下**键盘进不去**
50
+ (WCAG 2.1.1)。补 `tabindex="0"` + `role="region"` + 「内容名(可横向滚动)」为名的
51
+ 可访问名;图类用 `aria-labelledby` 指向 `<figcaption>`,不手写第二份文案。
52
+ - 浏览器自动发现:Playwright 1.6x 的缓存目录名(`chrome-headless-shell-win64/` 等)
53
+ 与代码里的候选**一个都不匹配**,此前靠系统 Chrome 兜底才跑得通。改为按可执行文件名
54
+ 有界递归 + 读 `chromium.executablePath()`,并补 `%LOCALAPPDATA%` 路径。
55
+ - 三处判据把"文本里出现过的字样"当成"当前生效的现状":`contrast.py` 把注释里的旧色值
56
+ 当真值、把 `@media dark` 块之后的浅色定义卷进暗色范围;`wiring-check.py` 把
57
+ `<style>` 里的 CSS 选择器当接线;`delta-check.js` 把 `<pre>` 里的用法示例当成
58
+ 页面真的用了 `data-market`。三处都改成只看**会渲染 / 会执行**的结构。
59
+
60
+ ### Changed
61
+
62
+ - 开发依赖 `puppeteer-core` 23 → 25(`npm audit` 从 9 high 到 0),CI 增加
63
+ `npm audit --omit=dev --audit-level=high`。
64
+ - 根导出补显式 `import` 条件;README 讲清"运行时是 ESM、`engines>=18` 只代表语法可用,
65
+ `require()` 需要 Node ≥ 22.12"。
66
+
67
+ ### 新增 / 加强的门禁
68
+
69
+ - `i18n-labels`:静态禁止「中文字面量直接进 `aria-label` / `textContent` / `el()` 文本位」,
70
+ 并在真浏览器里传 `labels` 断言 DOM 真的变了、不传时仍是中文。
71
+ - `md-inline-gate`:`md-render.py` 生成的单文件产物会把 `tokens.css` / `content.css`
72
+ 整份内联成第二份副本,此前没有任何门禁守着它漂移(实测已落后 586 字符)。
73
+ - `chrome-discovery-gate`:七种 Playwright 目录布局 × 两个入口 + 空目录不回落 + 旧写法元反向。
74
+ - `a11y-scan` 增加 320px 的 `scrollable-region-focusable` 补扫 —— 一上线就抓出
75
+ `content` / `longform` / `analysis-report` / `scientific-plot` 四页真实违规。
76
+ - `zoom-reflow` 的页面清单改为从文件系统枚举 `04-recipes/**/*.html`(43 → 49 页)——
77
+ 上面那个 `.prose` 缺陷就活在不属于任何组件的配方页里。
78
+ - `gen-behavior` 的扫描范围补上 `09-assets`:`bar` 的注入块否则是空的,
79
+ `flLabels is not defined` 只在浏览器门禁才炸。
80
+
81
+ ### 关于外部审计报告里的三条
82
+
83
+ F8(demo 对比度)、F10(model-viewer 小溢出)、F12(缺 SemVer/废弃政策)经复核
84
+ **不成立**:`.bad-err` / `.btn--naive` 都在 `.cmp__bad` 豁免容器内;本机 320px
85
+ 实测溢出 0;`STABILITY.md` / `DEPRECATIONS.md` / `deprecation-gate` 早已存在。
86
+ 没有为"关闭报告项"去改这三处。
87
+
88
+ ---
89
+
90
+ ## [0.25.6] - 2026-11-04
91
+
92
+ 这一版**没有改组件、令牌、CSS 或运行时 JS**。改的是「装到手里的包可不可信」:
93
+ 文档链接、依赖来源、包里不该有的临时文件,以及几道"报告通过却什么都没检查"
94
+ 的发布门禁。从 `0.25.5` 升级不需要改代码。
95
+
96
+ ### Security
97
+
98
+ - 依赖锁文件里有一处 `yaml` 仍指向第三方镜像 `registry.npmmirror.com`,
99
+ 而 `npm ci` 按 `resolved` 直取,会绕开仓库钉住的官方源。已改回
100
+ `registry.npmjs.org`,并核对官方 tarball 的 sha512 与锁文件 `integrity` 一致。
101
+ 新增 `lockfile-source-gate`:每个 `resolved` 的 host 都要在白名单里,
102
+ 镜像源、缺 integrity、非 https 都会红。
103
+ - `package.json` 的 `files` 排除项补上临时文件后缀与门禁探针名。
104
+ 之前反向控制留在源码目录里的 `.bak` 备份会被目录级白名单一起收进包;
105
+ 实测加排除前探针进包、加后不进。
106
+ - HTML 注入点的盘点门禁原本只扫五个目录,现在扫描面由 `files` 白名单推导,
107
+ 新增目录自动纳入;已复核 `esm/`、`adapters/` 里没有注入点。
108
+ 两处把 `src` / `value` 拼进 `innerHTML` 的组件改成 DOM 节点组装。
109
+
110
+ ### Fixed
111
+
112
+ - 包内四份文档(README、README.zh-CN、SUPPORT、docs/LAYER-BASELINE)里
113
+ 有八个相对链接指向仓库文件,而这些文件不在发布包里 —— 装了包的人点开就是死链。
114
+ 改为指向公开仓库的同一份文件;新增包视角链接门禁,用
115
+ `npm pack --dry-run --json` 的真实清单逐条验,不再只从仓库视角看。
116
+ - 打印门禁在并发下偶发误报:活标本 iframe 的样式表已挂上、框高却还是初始值,
117
+ 打印版面被裁掉一半,判据把环境抖动报成内容回归。现在要求文档、样式、字体
118
+ 都就绪且**框高装得下内容**,并在切到打印媒体后重新判定。
119
+ - 文档事实门禁抓到 `CONTRIBUTING.md` 写死了门禁条数;改为让条数只由门禁实算。
120
+ `package.json` 里三条 scripts 缩进错乱,以及 README 里几处历史措辞,一并修正。
121
+ - `release-gate` 曾把新加的通配排除判成「指向不存在的东西」;
122
+ 现在字面排除继续查存在,glob 排除只分类、不按当前有没有文件判。
123
+ - 一条写盘的反向控制漏标 `@serial`,与 `dist-fresh` 并发时报出「孤儿产物」。
124
+ 已补标记,并把那个临时探针也挡在包外。
125
+
126
+ ### Changed · 门禁自证(不进 npm 包)
127
+
128
+ 这些是仓库里的门禁与内部台账,装包的人拿不到,但决定了「全绿」值不值得信:
129
+
130
+ - `doc-imports.js` 原来读**仓库**的文档、却在**消费者**侧解析,验错了对象;
131
+ 而且一份文档都没扫到、或抽出来的全是占位符时照样通过。改为读已安装包里的
132
+ 文档,补零视野守卫、显式承诺清单与显式豁免(`CHANGELOG.md` 里记着历史坏路径,
133
+ 不能当成给读者照抄的入口)、未登记文档自动报红,并补了反向控制。
134
+ 实测:往包内 README 注入一条坏路径,旧判据绿、新判据红。
135
+ - `entry-resolve.js` 在 `exports` 不产生任何说明符时打印 `0/0` 却返回通过,
136
+ 还输出「两套条件都能解析到真实文件」这句假结论。现在零输入即红,
137
+ 并补了覆盖传递闭包、import 条件、通配展开的反向控制。
138
+ - `pack-smoke.sh` 把门禁输出用 `tail` 截成两行,扫描规模那行看不见了;
139
+ 现在保留统计行,看到 0 就能怀疑。
140
+ - 内部路线图与待办台账里几处仍写着「尚未发版 / 待远端验证」的活跃状态,
141
+ 按真值更正,历史段落只加注记、不改写。
142
+
143
+ 上述每一项都跑了正向、反向控制与真实发布包验证;发布前全量门禁 0 FAIL。
144
+ 新增的失误与纠正规则登记在内部 `ERRORS.md`(不随包分发)。
145
+
146
+ ---
147
+
19
148
  ## [0.25.5] - 2026-11-04
20
149
 
21
150
  ### 新增:`tokens.core.css`(只有变量)与 `base.reset.css`(只有底座)
package/CONTRIBUTING.md CHANGED
@@ -29,9 +29,10 @@ npm run gate # 全量:含浏览器类
29
29
  npm run gate:fast # 只跑静态层,不用装浏览器
30
30
  ```
31
31
 
32
- 全量是 **239 道门禁**(默认 `min(核数, 6)` 路并发,实测 583 秒 / 退出码 0;
33
- 单道耗时累加 1658 秒,并发效率约 2.8×。组件层要起浏览器,还要逐张比 **109 张**视觉截图 ——
34
- 47 浅色 + 47 暗色 + 15 RTL,按平台分集,只比本平台那套)。
32
+ 全量的门禁条数由 `05-audit/check-all.sh` 实算,**不在文档里写死** —— 新增一道门禁
33
+ 就会变,写下来的第二天就是第二个会漂的真值(I-7)。默认 `min(核数, 6)` 路并发;
34
+ 组件层要起浏览器,还要逐张比浅色 / 暗色 / RTL 三套视觉基线(按平台分集,
35
+ 只比本平台那套)。条数、耗时、并发效率一律以**本轮门禁打印的结果**为准。
35
36
  只想确认没改坏静态契约的话用 `gate:fast`,几秒到几分钟。
36
37
 
37
38
  ### 全量为什么**不**在 CI 上跑(这不是缺口,是结论)
package/README.md CHANGED
@@ -10,6 +10,23 @@ npm install agentcss
10
10
 
11
11
  The npm package is `agentcss`; the repository is `frontend-lib`. They are different things: the package name is what you install, the repository name is where the source lives.
12
12
 
13
+ ### Module format: ESM-only
14
+
15
+ The JS entry points (`esm/*.mjs`) are **ESM only** — `exports` has no `require` condition.
16
+
17
+ | You write | Node **≥ 20.19 / ≥ 22.12** | Node 18 – 20.18 |
18
+ |---|---|---|
19
+ | `import 'agentcss'` | ✅ | ✅ |
20
+ | `require('agentcss')` | ✅ (Node's built-in require(ESM)) | ❌ `ERR_REQUIRE_ESM` |
21
+
22
+ - On older runtimes use `await import('agentcss')`, or consume the CSS entry points only
23
+ (`agentcss/tokens.css` etc. are not JS and work on any Node version).
24
+ - The shipped `*.js` files under `02-primitives/**` and `03-patterns/**` are **classic
25
+ scripts** for `<script>` tags, not CommonJS modules — they attach to `window.*`.
26
+ - `"type": "commonjs"` is deliberate: it describes how those classic scripts are
27
+ *parsed*. Flipping it to `module` would make bundlers re-interpret them as ESM,
28
+ which is a breaking change.
29
+
13
30
  ## Docs and live demos
14
31
 
15
32
  - **Every demo page, one index** — https://owain323.github.io/frontend-lib/
@@ -269,7 +286,7 @@ Two builds ship:
269
286
  The source tree keeps every comment — that is documentation, not payload.
270
287
 
271
288
  **One frozen ceiling.** The whole library is held to a single budget recorded in
272
- [`05-audit/budget.json`](05-audit/budget.json): a hard ceiling, a reserve that absorbs
289
+ [`05-audit/budget.json`](https://github.com/owain323/frontend-lib/blob/master/05-audit/budget.json): a hard ceiling, a reserve that absorbs
273
290
  normal additions *without anyone editing a number*, and a ratchet that tightens the
274
291
  budget when the library shrinks. The rules — and what would have to be true to raise
275
292
  the ceiling — are in [`docs/BUDGET.md`](docs/BUDGET.md).
@@ -290,7 +307,7 @@ not — is in [`docs/STABILITY.md`](docs/STABILITY.md).
290
307
  - **Tested once** (`🔍`) means exactly that: one manual run on a real device, with a full UA and a per-capability result, but **no automated check watching it**. A browser update will not turn anything red. It never counts as verified, and the two never add up.
291
308
  - **Not claimed** means exactly that. A row that has not been tested is not written as supported.
292
309
 
293
- Mobile WebView support is where that distinction bites. Seven entry points have been probed by hand — per-capability results in [`10-review/webview/RESULTS.md`](10-review/webview/RESULTS.md) — while Android System WebView still has **no data at all**. None of the seven was checked for soft-keyboard, safe-area, scroll-bounce or 200 % text behaviour: "15/15 capabilities" is not "the page works on a phone".
310
+ Mobile WebView support is where that distinction bites. Seven entry points have been probed by hand — per-capability results in [`10-review/webview/RESULTS.md`](https://github.com/owain323/frontend-lib/blob/master/10-review/webview/RESULTS.md) — while Android System WebView still has **no data at all**. None of the seven was checked for soft-keyboard, safe-area, scroll-bounce or 200 % text behaviour: "15/15 capabilities" is not "the page works on a phone".
294
311
 
295
312
  ## Theming
296
313
 
package/README.zh-CN.md CHANGED
@@ -60,6 +60,22 @@ npm install agentcss
60
60
 
61
61
  装出来的目录名就是 `agentcss`,`dist-tags.latest` 指向当前版本。
62
62
 
63
+ ### 模块格式:ESM-only(请先读这一条)
64
+
65
+ `agentcss` 的 JS 入口(`esm/*.mjs`)**只有 ESM**,`exports` 里没有 `require` 条件。
66
+
67
+ | 你的写法 | Node **≥ 20.19 / ≥ 22.12** | Node 18 ~ 20.18 |
68
+ |---|---|---|
69
+ | `import 'agentcss'` | ✅ | ✅ |
70
+ | `require('agentcss')` | ✅(Node 自带的 require(ESM)) | ❌ `ERR_REQUIRE_ESM` |
71
+
72
+ - 走 `require` 的旧环境请改用 `await import('agentcss')`,或只用 CSS 入口
73
+ (`agentcss/tokens.css` 这类**不是 JS**,任何 Node 版本都能直接引用)。
74
+ - 包里的 `*.js`(`02-primitives/**`、`03-patterns/**`)是给 `<script>` 用的**经典脚本**,
75
+ 不是 CommonJS 模块 —— 它们挂 `window.*`,不需要 bundler。
76
+ - `type` 字段保持 `commonjs` 是**有意**的:它描述的是那批经典脚本的解析方式。
77
+ 把它改成 `module` 会让 bundler 用 ESM 语义重新解析它们,属于破坏性变更。
78
+
63
79
  另外两条**同样真实可用**的路(内容与 registry 上那一份同源):
64
80
 
65
81
  ```bash
@@ -282,7 +298,7 @@ dist/01-tokens/tokens.css ← 拷这个
282
298
  那些注释是写给你看的取舍理由(为什么这么定、踩过什么坑),不是写给你浏览器看的。
283
299
  **注释让你看代码时处处撞墙;从源码目录引入的话,构建工具还会把那部分字节原样打进产物。**
284
300
 
285
- **一个冻结的上限。** 整套库受一份账本约束([`05-audit/budget.json`](05-audit/budget.json)):
301
+ **一个冻结的上限。** 整套库受一份账本约束([`05-audit/budget.json`](https://github.com/owain323/frontend-lib/blob/master/05-audit/budget.json)):
286
302
  硬天花板 + 一份「常规新增组件不用改任何数字」的储备 + 一个只在体积下降时收紧的棘轮。
287
303
  规则、以及「什么情况才允许抬天花板」,在 [`docs/BUDGET.md`](docs/BUDGET.md)。
288
304
 
@@ -456,7 +472,7 @@ bash 05-audit/check-all.sh # 全量门禁约 130 项,本机约 8 分钟
456
472
  `🔍` 是「在真实设备上人工试过一次,之后没人再看」——
457
473
  浏览器升级只会被前者发现。逐项结果、完整 UA 与复核方法见
458
474
  [`docs/LAYER-BASELINE.md`](docs/LAYER-BASELINE.md) 与
459
- [`10-review/webview/RESULTS.md`](10-review/webview/RESULTS.md)。
475
+ [`10-review/webview/RESULTS.md`](https://github.com/owain323/frontend-lib/blob/master/10-review/webview/RESULTS.md)。
460
476
 
461
477
  ⚠️ 定位里写「移动 Web」指的是**设计取向**(触控优先、命中区不小于 44px),
462
478
  不代表已被自动化覆盖,更不代表页面在真机上就能用:
package/SECURITY.md CHANGED
@@ -44,10 +44,22 @@
44
44
  下面两处会把字符串**当 HTML 解析**。它们是**有意的 API**,不是疏漏 ——
45
45
  `popover` 要能放任意块级内容,`drawer` 要能放整页内容,只收文本就废了。
46
46
 
47
- | 文件 | 入口 | 谁负责净化 |
48
- |---|---|---|
49
- | `02-primitives/popover/popover.js` | `opts.html` | 调用方 |
50
- | `03-patterns/drawer/drawer.js` | `opt.content` | 调用方 |
47
+ | 文件 | 入口 | 净化钩子 | 谁负责净化 |
48
+ |---|---|---|---|
49
+ | `02-primitives/popover/popover.js` | `opts.html` | `opts.sanitize` / `Popover.setSanitizer(fn)` | 调用方(或接钩子) |
50
+ | `03-patterns/drawer/drawer.js` | `opt.content`(字符串) | `opt.sanitize` / `Drawer.setSanitizer(fn)` | 调用方(或接钩子) |
51
+
52
+ **0.26.0 起有钩子,但默认不改变行为**:不传 `sanitize`、也不调 `setSanitizer()`
53
+ 时,两处仍然直接把字符串当 HTML 解析 —— 与 0.25.x 完全一致(静默"自动净化"
54
+ 会让既有页面里的 `<b>` / `<svg>` 突然消失,那也是一种破坏)。
55
+ 接 DOMPurify 只需一行:
56
+
57
+ ```js
58
+ Popover.setSanitizer(function (html) { return DOMPurify.sanitize(html); });
59
+ Drawer.setSanitizer(function (html) { return DOMPurify.sanitize(html); });
60
+ ```
61
+
62
+ 逐次覆盖优先于模块级:`Popover.attach(el, { html: s, sanitize: fn })`。
51
63
 
52
64
  **为什么本库不做净化**:净化需要一个 HTML 解析器或一份允许列表实现,而本库的目标是
53
65
  零运行时依赖 + Chromium 97 可用。给一个"看起来安全"的半吊子净化,比明确说