agentcss 0.25.0 → 0.25.2

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 (46) hide show
  1. package/02-primitives/button/README.md +7 -0
  2. package/02-primitives/button/demo.html +52 -0
  3. package/02-primitives/card/README.md +6 -1
  4. package/02-primitives/card/demo.html +13 -0
  5. package/02-primitives/choice/choice.css +10 -0
  6. package/02-primitives/choice/demo.html +38 -0
  7. package/02-primitives/date-range/README.md +1 -1
  8. package/02-primitives/date-range/demo.html +41 -1
  9. package/02-primitives/input/README.md +3 -2
  10. package/02-primitives/input/demo.html +42 -0
  11. package/03-patterns/accordion/demo.html +113 -0
  12. package/03-patterns/content/demo.html +17 -0
  13. package/03-patterns/drawer/README.md +11 -1
  14. package/03-patterns/drawer/drawer.css +22 -6
  15. package/03-patterns/drawer/drawer.js +16 -1
  16. package/03-patterns/dropdown/README.md +10 -8
  17. package/03-patterns/dropdown/demo.html +5 -0
  18. package/03-patterns/dropdown/dropdown.js +8 -0
  19. package/03-patterns/form-validation/demo.html +20 -0
  20. package/03-patterns/list/demo.html +10 -0
  21. package/03-patterns/overlay/overlay.js +14 -1
  22. package/03-patterns/pagination/demo.html +23 -0
  23. package/03-patterns/states/demo.html +39 -1
  24. package/03-patterns/tabs/demo.html +34 -0
  25. package/03-patterns/tooltip/tooltip.js +8 -4
  26. package/04-recipes/app-shell/demo.html +27 -0
  27. package/04-recipes/table/demo.html +34 -0
  28. package/09-assets/bar/bar.js +8 -4
  29. package/09-assets/bar/demo.html +27 -0
  30. package/CHANGELOG.md +127 -0
  31. package/CONTRIBUTING.md +1 -1
  32. package/START-HERE.md +6 -6
  33. package/ai/components.json +6 -3
  34. package/ai/pdf-pages.js +31 -3
  35. package/dist/02-primitives/choice/choice.css +1 -1
  36. package/dist/03-patterns/drawer/drawer.css +1 -1
  37. package/dist/03-patterns/drawer/drawer.js +10 -1
  38. package/dist/03-patterns/dropdown/dropdown.js +4 -0
  39. package/dist/03-patterns/overlay/overlay.js +7 -1
  40. package/dist/03-patterns/tooltip/tooltip.js +3 -3
  41. package/dist/09-assets/bar/bar.js +4 -3
  42. package/dist/COSTS.md +7 -7
  43. package/docs/ACCESSIBILITY.md +2 -2
  44. package/index.html +1 -1
  45. package/llms-full.txt +3 -3
  46. package/package.json +1 -1
@@ -359,6 +359,14 @@
359
359
  li.className = li.className ? (li.className + ' dd__item') : 'dd__item';
360
360
  }
361
361
  if (li.hasAttribute('data-dd-sep')) {
362
+ /* 🔴 和上面的 dd__item 同一个道理:分隔线那 1px 的高度与背景
363
+ 全在 `.dd__sep` 这条规则里。作者只写 `data-dd-sep` 而不写类名时,
364
+ 量到的渲染高度是 **0px**(实测),两组菜单项直接贴着 ——
365
+ 看上去像"漏了一条分隔线",而且没有任何报错。
366
+ ⇒ 由组件补类,不指望每个使用者记得写。 */
367
+ if (String(li.className).indexOf('dd__sep') < 0) {
368
+ li.className = li.className ? (li.className + ' dd__sep') : 'dd__sep';
369
+ }
362
370
  li.setAttribute('role', 'separator');
363
371
  li.setAttribute('aria-hidden', 'true');
364
372
  } else if (!li.hasAttribute('role')) {
@@ -21,6 +21,8 @@ p.small { font-size: var(--fs-xs); color: var(--text-tertiary); line-height: 1.6
21
21
  .good { background: var(--surface-sunken); }
22
22
  .bad h3 { color: var(--danger); }
23
23
  .good h3 { color: var(--success); }
24
+ /* 校验中示例只放一个字段,别撑满整行 —— 撑满了会让人以为"整页都在查" */
25
+ .checking-box { max-width: 460px; }
24
26
  .btn { display:inline-flex; align-items:center; justify-content:center; height:44px;
25
27
  padding:0 var(--sp-5); border:1px solid transparent; border-radius:var(--r-sm);
26
28
  font-size:var(--fs-sm); font-weight:500; cursor:pointer; font-family:inherit;
@@ -91,6 +93,24 @@ p.small { font-size: var(--fs-xs); color: var(--text-tertiary); line-height: 1.6
91
93
  而且把逻辑硬塞进样式层不可测。控制权交给 JS 加 <code>data-dirty</code> 属性。
92
94
  </div>
93
95
 
96
+ <h3 class="rule-bottom">第三种状态:异步校验中(<code>field--checking</code>)</h3>
97
+ <div class="box checking-box">
98
+ <div class="field field--checking">
99
+ <label class="field__label" for="uname">用户名</label>
100
+ <input class="field__input" id="uname" value="data_team" autocomplete="username"
101
+ aria-describedby="uname-checking">
102
+ <span class="field__checking-hint" id="uname-checking">正在检查是否可用…</span>
103
+ </div>
104
+ <p class="small" style="margin-top:var(--sp-3)">
105
+ 🔴 <strong>校验中绝不能把输入框置灰或禁用</strong> —— 用户会以为"不让打字了",
106
+ 于是停在那里等,而其实他完全可以接着改。
107
+ 正确做法只有一条:<strong>保持可输入</strong>,旁边给一个小的 pending 指示。
108
+ <br>这个指示器是<strong>原地旋转</strong>(12px),不是全屏 spinner:
109
+ 它只说明"这一项在查",不代表"整个页面在忙"。
110
+ <code>prefers-reduced-motion</code> 下放慢到 1.2s 但不停(停了会被当成卡死)。
111
+ </p>
112
+ </div>
113
+
94
114
  <h2 class="rule-bottom">二、完整表单:提交后出错误汇总</h2>
95
115
  <p class="small">什么都不填,直接点提交。焦点会移到汇总上,汇总里每条可点击跳转。</p>
96
116
 
@@ -47,6 +47,9 @@ p.small { font-size: var(--fs-xs); color: var(--text-tertiary); line-height: 1.6
47
47
  </div>
48
48
 
49
49
  <ul class="list" id="list" role="list"></ul>
50
+ <!-- 列表末尾的一行小字:说明"这个列表本身"(条数、排序、口径),
51
+ 不是空状态。空状态用 states 的 .state--empty(见下面)。 -->
52
+ <p class="list__note" id="note" aria-live="polite">共 0 条</p>
50
53
 
51
54
  <!-- 空状态:删光后显示,容器位置不变所以不跳动 -->
52
55
  <div id="empty" hidden>
@@ -165,6 +168,8 @@ var EXTRA = [
165
168
 
166
169
  var list = document.getElementById('list');
167
170
  var emptyBox = document.getElementById('empty');
171
+ var note = document.getElementById('note');
172
+ var mode = '插入顺序';
168
173
  var data = SEED.slice();
169
174
  var seq = 100;
170
175
 
@@ -210,6 +215,7 @@ function paint() {
210
215
  for (var i = 0; i < data.length; i++) { html += rowHtml(data[i]); }
211
216
  list.innerHTML = html;
212
217
  emptyBox.hidden = data.length > 0;
218
+ note.textContent = '共 ' + data.length + ' 条 · ' + mode;
213
219
  }
214
220
 
215
221
  function repaint(mutate) {
@@ -219,6 +225,7 @@ function repaint(mutate) {
219
225
  for (var i = 0; i < data.length; i++) { html += rowHtml(data[i]); }
220
226
  list.innerHTML = html;
221
227
  emptyBox.hidden = data.length > 0;
228
+ note.textContent = '共 ' + data.length + ' 条 · ' + mode;
222
229
  });
223
230
  }
224
231
 
@@ -254,12 +261,14 @@ document.getElementById('add').onclick = function () {
254
261
  };
255
262
 
256
263
  document.getElementById('sort').onclick = function () {
264
+ mode = '按记录数降序';
257
265
  repaint(function () {
258
266
  data.sort(function (a, b) { return b.rec - a.rec; });
259
267
  });
260
268
  };
261
269
 
262
270
  document.getElementById('reverse').onclick = function () {
271
+ mode = '按名称排序';
263
272
  repaint(function () {
264
273
  data.sort(function (a, b) { return a.name.localeCompare(b.name, 'zh-Hans-CN'); });
265
274
  });
@@ -270,6 +279,7 @@ document.getElementById('clear').onclick = function () {
270
279
  };
271
280
 
272
281
  document.getElementById('reload').onclick = function () {
282
+ mode = '插入顺序';
273
283
  data = SEED.slice();
274
284
  for (var i = 0; i < data.length; i++) { data[i].id = 'r' + (++seq); }
275
285
  paint();
@@ -252,6 +252,19 @@
252
252
  🔴 绝不抢焦点。role="status" 会被屏幕阅读器自动播报,
253
253
  focus() 会打断用户当前正在做的事。
254
254
  */
255
+ /* 变体 ⇒ 类名。🔴 两个理由,都不是"为了好看":
256
+ ① **白名单**:`toast({variant:'warn'})` 原本会拼出 `toast--warn`,
257
+ 没有任何 CSS 命中 ⇒ 一条本该是黄色的警示条渲染成普通白条,
258
+ 不报错、控制台干净。现在未知值退回无变体(至少不假装有语义)。
259
+ ② **写完整字面量**:`'toast--' + variant` 在源码里搜不到
260
+ `toast--success`,覆盖率门禁认不出"谁在用这个类"
261
+ (实测 success / warning 因此长期躺在"只被 README 认领"名单里)。 */
262
+ var TOAST_VARIANT = {
263
+ success: 'toast--success',
264
+ error: 'toast--error',
265
+ warning: 'toast--warning'
266
+ };
267
+
255
268
  function toast(opts) {
256
269
  opts = opts || {};
257
270
 
@@ -266,7 +279,7 @@
266
279
  }
267
280
 
268
281
  var el = document.createElement('div');
269
- el.className = 'toast' + (opts.variant ? ' toast--' + opts.variant : '');
282
+ el.className = 'toast' + (TOAST_VARIANT[opts.variant] ? ' ' + TOAST_VARIANT[opts.variant] : '');
270
283
 
271
284
  var body = document.createElement('div');
272
285
  body.className = 'toast__body';
@@ -121,6 +121,23 @@ th { font-size: var(--fs-xs); color: var(--text-secondary); font-weight: 500; }
121
121
 
122
122
 
123
123
 
124
+ <h2 class="rule-top">7 · 表格页脚里的精简分页(不占一行位置)</h2>
125
+ <p class="note">
126
+ 数据表底部通常只有一两行的空间:<b>状态文字可以省,播报区不能省</b>。
127
+ <br>做法是把 <code>data-pg-live</code> 挂在一个 <code>.pagination__sr</code> 上 ——
128
+ <b>视觉上不占位,读屏照常播报</b>(用 <code>clip</code> 裁掉,不是 <code>display:none</code>;
129
+ 后者会把整块移出无障碍树,播报就没了)。
130
+ <br>⚠️ 省略 <code>data-pg-status</code> 是允许的(JS 里有判空);
131
+ 省略 <code>data-pg-live</code> 则等于"翻页了但不告诉读屏"。
132
+ </p>
133
+ <div class="demo-box">
134
+ <nav class="pagination" aria-label="明细表分页"
135
+ data-pagination data-total="248" data-page-size="20" data-page="7" id="pg2">
136
+ <ul class="pagination__list" data-pg-list></ul>
137
+ <p class="pagination__sr" data-pg-live aria-live="polite"></p>
138
+ </nav>
139
+ </div>
140
+
124
141
  <h2 class="rule-bottom">对比:错写法 vs 本库写法</h2>
125
142
  <div class="cmp">
126
143
  <div class="cmp__pane cmp__bad" data-bad="li.pagination__item|count|>7">
@@ -200,6 +217,12 @@ pg.onChange = function (page) {
200
217
  }
201
218
  };
202
219
  load(pg.page);
220
+
221
+ /* 精简分页(页脚用):没有 data-pg-status,播报区挂在 .pagination__sr 上 */
222
+ var pg2 = Pagination.create(document.getElementById('pg2'));
223
+ pg2.onChange = function (page) {
224
+ document.getElementById('pg2').setAttribute('data-page', String(page));
225
+ };
203
226
  </script>
204
227
  </body>
205
228
  </html>
@@ -56,6 +56,12 @@ p.lead { color: var(--text-secondary); }
56
56
  }
57
57
  .lbl { font-size: var(--fs-xs); color: var(--text-tertiary);
58
58
  margin-bottom: var(--sp-2); display: block; }
59
+ /* 列表项骨架:头像 + 两行 —— 结构与真实行一致(.row 的 padding 与 gap 对齐) */
60
+ .sk-list { border: 1px solid var(--border-decor); border-radius: var(--r-sm);
61
+ padding: var(--sp-2) 0; max-width: 420px; }
62
+ .sk-row { display: flex; align-items: center; gap: var(--sp-3);
63
+ padding: var(--sp-3) var(--sp-4); }
64
+ .sk-lines { flex: 1; min-width: 0; }
59
65
  </style>
60
66
  </head>
61
67
  <body class="page">
@@ -140,6 +146,31 @@ p.lead { color: var(--text-secondary); }
140
146
 
141
147
  <hr class="rule">
142
148
 
149
+ <h2>列表项的骨架:头像占位 + 两行文字</h2>
150
+ <p>
151
+ <code>skeleton--avatar</code> 是 40×40 的全圆占位,专门配 <code>skeleton--text</code>
152
+ 做「头像 + 姓名 + 副标题」这种列表项。宽度节奏要跟真内容一致,
153
+ 否则内容到位时头像会把文字挤走 —— 那正是骨架屏要避免的事。
154
+ </p>
155
+ <div class="sk-list" role="status" aria-live="polite" aria-label="正在加载公司列表">
156
+ <div class="sk-row">
157
+ <div class="skeleton skeleton--avatar" aria-hidden="true"></div>
158
+ <div class="sk-lines">
159
+ <div class="skeleton skeleton--text" aria-hidden="true"></div>
160
+ <div class="skeleton skeleton--text" aria-hidden="true"></div>
161
+ </div>
162
+ </div>
163
+ <div class="sk-row">
164
+ <div class="skeleton skeleton--avatar" aria-hidden="true"></div>
165
+ <div class="sk-lines">
166
+ <div class="skeleton skeleton--text" aria-hidden="true"></div>
167
+ <div class="skeleton skeleton--text" aria-hidden="true"></div>
168
+ </div>
169
+ </div>
170
+ </div>
171
+
172
+ <hr class="rule">
173
+
143
174
  <h2>300ms:不显示比闪一下好</h2>
144
175
  <p>
145
176
  请求在 300ms 内返回时<strong>不要显示加载态</strong>。骨架屏闪一下比不显示更糟 ——
@@ -218,7 +249,14 @@ var VIEWS = {
218
249
  '<div class="state__action">' +
219
250
  '<button class="btn btn--sm btn--primary" type="button">重试</button>' +
220
251
  '<button class="btn btn--sm btn--secondary" type="button">复制诊断信息</button>' +
221
- '</div></div>';
252
+ '</div>' +
253
+ /* 技术细节放这里、并且折叠起来:主文案说"我该怎么办",
254
+ 细节留给要把它贴给技术支持的人。默认摊开会把所有人都吓一遍。 */
255
+ '<details class="state__more">' +
256
+ '<summary>诊断信息</summary>' +
257
+ '<div class="state__detail">sqlite3 · SELECT … FROM financial_facts WHERE ws=19 · err: SQLITE_BUSY (database is locked) · 12,480 ms · trace 8f2c41a7</div>' +
258
+ '</details>' +
259
+ '</div>';
222
260
  }
223
261
  };
224
262
 
@@ -13,6 +13,10 @@
13
13
  <style>
14
14
  .log { font-family: var(--mono); font-size: var(--fs-xs);
15
15
  color: var(--text-secondary); min-height: 1.5em; }
16
+ /* 键帽:用真正的 <kbd> 元素,不是 span+class(后者会被 refs 判成"未定义的类") */
17
+ kbd { display: inline-block; padding: 2px 7px; border: 1px solid var(--border-control);
18
+ border-bottom-width: 2px; border-radius: 5px; background: var(--surface);
19
+ font-family: var(--mono); font-size: 11px; color: var(--text-primary); }
16
20
  </style>
17
21
  </head>
18
22
  <body class="page">
@@ -86,6 +90,36 @@
86
90
  </div>
87
91
  </div>
88
92
 
93
+ <h2 class="rule-top">纵向(设置页侧栏那种)</h2>
94
+ <p class="note">
95
+ 加 <code>.tabs--vertical</code>,并把 tablist 的
96
+ <code>aria-orientation</code> 改成 <code>vertical</code> ——
97
+ <strong>两件事必须同时做</strong>:只改 CSS 的话,键盘仍然是
98
+ <code>←</code> <code>→</code> 切换,而屏幕阅读器被告知"横向",
99
+ 于是它提示用户按左右键;用户按上下键,没反应。
100
+ </p>
101
+ <div class="tabs tabs--vertical" data-tabs>
102
+ <div class="tabs__list" role="tablist" aria-orientation="vertical" aria-label="账户设置">
103
+ <button class="tabs__tab" role="tab" id="v1" aria-controls="vp1"
104
+ aria-selected="true">基本资料</button>
105
+ <button class="tabs__tab" role="tab" id="v2" aria-controls="vp2"
106
+ aria-selected="false">安全</button>
107
+ <button class="tabs__tab" role="tab" id="v3" aria-controls="vp3"
108
+ aria-selected="false">通知</button>
109
+ </div>
110
+ <div class="tabs__panel" role="tabpanel" id="vp1" aria-labelledby="v1">
111
+ <p>头像、显示名、所属工作区。</p>
112
+ <p><button class="btn btn--md btn--secondary">修改头像(可 Tab 到)</button></p>
113
+ </div>
114
+ <div class="tabs__panel" role="tabpanel" id="vp2" aria-labelledby="v2" hidden>
115
+ <p>两步验证、登录设备、API 令牌。</p>
116
+ </div>
117
+ <div class="tabs__panel" role="tabpanel" id="vp3" aria-labelledby="v3" hidden>
118
+ <p>周报邮件、异常告警的接收范围。</p>
119
+ </div>
120
+ </div>
121
+ <p class="log">试 <kbd>↓</kbd> <kbd>↑</kbd>:选中线应该在<strong>文字右侧</strong>(RTL 下换到左侧)。</p>
122
+
89
123
  <h2 class="rule-top">键盘契约(APG / Radix)</h2>
90
124
  <div class="box">
91
125
  <p>
@@ -157,10 +157,14 @@ var flDismissable = function (opts) {
157
157
  return r;
158
158
  }
159
159
 
160
- /* 逻辑方位 ⇒ 类名后缀。CSS 里只有 --inset-inline-start / --inset-inline-end */
160
+ /* 逻辑方位 ⇒ 完整类名。CSS 里只有 --inset-inline-start / --inset-inline-end。
161
+ 🔴 为什么写整串而不是 `'tooltip--' + CLS[pos]`:拼出来的名字
162
+ 在源码里 grep 不到,改 CSS 时无法反查谁在用;覆盖率门禁也只能看到
163
+ 一段前缀,于是四个方位长期被记成"只被 README 认领"
164
+ —— 而它们其实每次 hover 都在渲染。 */
161
165
  var CLS = {
162
- top: 'top', bottom: 'bottom',
163
- start: 'inset-inline-start', end: 'inset-inline-end',
166
+ top: 'tooltip--top', bottom: 'tooltip--bottom',
167
+ start: 'tooltip--inset-inline-start', end: 'tooltip--inset-inline-end',
164
168
  };
165
169
 
166
170
  /**
@@ -195,7 +199,7 @@ var flDismissable = function (opts) {
195
199
  var id = 'tt' + (++uid);
196
200
  var tip = document.createElement('div');
197
201
  tip.id = id;
198
- tip.className = 'tooltip tooltip--' + CLS[pos];
202
+ tip.className = 'tooltip ' + CLS[pos];
199
203
  tip.setAttribute('role', 'tooltip');
200
204
  tip.textContent = text;
201
205
 
@@ -316,6 +316,33 @@
316
316
  <p class="note">只做纯图标栏、不接抽屉的话,第 4 节那种写法(图标 + <code>aria-label</code>)
317
317
  也成立;但只要你会用 <code>data-shell-nav</code>,就把文字留在 DOM 里 ——
318
318
  一份标记同时满足两种形态。</p>
319
+ <h2>8 · 平面外壳(<code>shell--flat</code>):融进纸面,不画第二层框</h2>
320
+ <div class="demo" style="padding:0">
321
+ <div class="shell shell--flat shell-demo--short">
322
+ <header class="shell__topbar">
323
+ <h2 class="shell__title">内嵌在报告里的外壳</h2>
324
+ </header>
325
+ <nav class="shell__side" aria-label="主导航(平面外壳)">
326
+ <ul class="shell__side-group">
327
+ <li><a class="shell__link" href="#" aria-current="page">概览</a></li>
328
+ <li><a class="shell__link" href="#">明细</a></li>
329
+ </ul>
330
+ </nav>
331
+ <div class="shell__main">
332
+ <div class="shell__content">
333
+ <div class="ph">外壳底色改回纸色(--paper)</div>
334
+ <div class="ph">主区边线透明 ⇒ 不与外层的卡片边框重影</div>
335
+ </div>
336
+ </div>
337
+ </div>
338
+ </div>
339
+ <p class="note">
340
+ 什么时候用:外壳<strong>本身已经在卡片 / 弹窗 / 报告版式里</strong>。
341
+ 这时它再自带一层底色和左边线,就会和外层边框形成"双重描边" ——
342
+ 两条相距 1px 的线,看上去像渲染错位。
343
+ <br>什么时候别用:外壳是整页容器。那时它需要自己的边界,
344
+ <code>--flat</code> 会让侧栏和主区糊成一片。
345
+ </p>
319
346
 
320
347
  </main>
321
348
  <!-- shell-nav.js 依赖 Drawer(抽屉的行为层)。顺序不能反:
@@ -122,6 +122,40 @@
122
122
 
123
123
 
124
124
 
125
+ <h2 class="rule-top">7 · 缺失值与紧凑密度(对账表这种"一屏二十行"的场合)</h2>
126
+ <p class="note">
127
+ <code>table--compact</code> 把字号降到 <code>--fs-xs</code>、内边距收到
128
+ <code>--sp-1 --sp-2</code> —— <strong>只给数字表、对账表用</strong>,
129
+ 正文表格用了会读不动。
130
+ <br><code>cell--na</code> 是缺失值的统一写法:灰色、居中、不留空。
131
+ <strong>空格子和 0 是两件事</strong>:空格子读者分不清"没有数据"还是"漏了",
132
+ 0 会被当成真实数值参与心算。
133
+ </p>
134
+ <div class="table table--compact">
135
+ <table>
136
+ <caption class="table__cap">表 G · 银行对账明细(示例,含缺失值)</caption>
137
+ <thead>
138
+ <tr>
139
+ <th scope="col">科目</th>
140
+ <th scope="col" class="num">期初</th>
141
+ <th scope="col" class="num">本期发生</th>
142
+ <th scope="col" class="num">期末</th>
143
+ </tr>
144
+ </thead>
145
+ <tbody>
146
+ <tr><th scope="row">银行存款</th><td class="num">1,204,300</td><td class="num">318,200</td><td class="num">1,522,500</td></tr>
147
+ <tr><th scope="row">应收账款</th><td class="num">884,120</td><td class="cell--na">—</td><td class="num">884,120</td></tr>
148
+ <tr><th scope="row">其他应收款</th><td class="cell--na">N/A</td><td class="num">12,400</td><td class="num">12,400</td></tr>
149
+ <tr><th scope="row">预付账款</th><td class="num">46,700</td><td class="num">3,100</td><td class="num">49,800</td></tr>
150
+ </tbody>
151
+ </table>
152
+ </div>
153
+ <p class="table__foot">
154
+ 两处缺失值用了两种写法(<code>—</code> 与 <code>N/A</code>):
155
+ 会计表里 <code>—</code> 表示"本期无发生",<code>N/A</code> 表示"未取到数"。
156
+ 混用会让人以为两者是同一件事 —— <strong>选一种并在表注里说明</strong>。
157
+ </p>
158
+
125
159
  <h2 class="rule-bottom">对比:错写法 vs 本库写法</h2>
126
160
  <div class="cmp">
127
161
  <div class="cmp__pane cmp__bad" data-bad="td.num--pos|glyphs|=5">
@@ -23,8 +23,12 @@
23
23
  var NS = 'http://www.w3.org/2000/svg';
24
24
  var uid = 0;
25
25
  /* 分类色 5 个(bar.css 里的 .bar__s0..s4),与 scientific-plot 的 ramp 对齐。
26
- 超过 5 个系列请由调用方给 `series[i].cls` 自定义 —— 颜色循环会造成误读。 */
27
- var SERIES_STYLES = 5;
26
+ 超过 5 个系列请由调用方给 `series[i].cls` 自定义 —— 颜色循环会造成误读。
27
+ 🔴 写成**完整类名数组**而不是 `'bar__s' + n`:
28
+ 拼串在源码里搜不到 `bar__s4`,改配色时无法反查谁在用;
29
+ 也让 .bar__s4 一直躺在覆盖率门禁的"只被 README 认领"名单里。 */
30
+ var SERIES_CLS = ['bar__s0', 'bar__s1', 'bar__s2', 'bar__s3', 'bar__s4'];
31
+ var SERIES_STYLES = SERIES_CLS.length;
28
32
 
29
33
  /* ==================================================================== *
30
34
  * 铁律二的核心:哪些点值得标数字
@@ -193,7 +197,7 @@
193
197
  yy = padT + plotH - vt;
194
198
 
195
199
  rect = el('rect', {
196
- 'class': 'bar__bar bar__s' + (s % SERIES_STYLES) +
200
+ 'class': 'bar__bar ' + SERIES_CLS[s % SERIES_STYLES] +
197
201
  (series[s].cls ? ' ' + series[s].cls : ''),
198
202
  x: x, y: yy, width: barW, height: vt - vb,
199
203
  });
@@ -277,7 +281,7 @@
277
281
  var li = document.createElement('li');
278
282
  li.className = 'bar__legend-item';
279
283
  var sw = document.createElement('span');
280
- sw.className = 'bar__legend-swatch bar__s' + (si % SERIES_STYLES) +
284
+ sw.className = 'bar__legend-swatch ' + SERIES_CLS[si % SERIES_STYLES] +
281
285
  (se.cls ? ' ' + se.cls : '');
282
286
  /* 🔴 用 textContent,不用 innerHTML —— 系列名来自调用方的数据 */
283
287
  li.appendChild(sw);
@@ -82,6 +82,16 @@
82
82
  <button class="btn" id="try">不给 max,看会发生什么</button>
83
83
  <div id="err"></div>
84
84
 
85
+ <h2>5 · 五个系列 = 五档分类色;第 6 个必须自带 <code>cls</code></h2>
86
+ <p class="note">
87
+ <code>bar.css</code> 只提供 <code>.bar__s0</code>–<code>.bar__s4</code> 五档分类色
88
+ (与 <code>scientific-plot</code> 的 ramp 对齐)。超过五个系列时,本库
89
+ <b>不会循环取色</b>——循环会让两个不同的系列拿到同一个颜色,读者会以为它们是同一类。
90
+ <br>⇒ 第 6 个系列起,请由调用方给 <code>series[i].cls</code>。
91
+ 下面这张图正好用满五档:五根柱子五种颜色,都能在基线截图里逐色核对。
92
+ </p>
93
+ <div class="bar" id="b5"></div>
94
+
85
95
  </main>
86
96
 
87
97
  <script src="bar.js"></script>
@@ -156,6 +166,23 @@
156
166
  ariaLabel: '每日 09:00 前的事项数,含阈值线 1',
157
167
  });
158
168
 
169
+ /* 5 · 五系列 = 五档分类色(.bar__s0..s4 全部真的渲染出来) */
170
+ Bar.draw({
171
+ host: document.getElementById('b5'),
172
+ mode: 'group',
173
+ max: 9,
174
+ height: 190,
175
+ labels: LBL.slice(0, 6),
176
+ series: [
177
+ { name: '受理', values: [3, 4, 5, 2, 6, 4] },
178
+ { name: '审核', values: [2, 3, 4, 5, 3, 6] },
179
+ { name: '放款', values: [4, 2, 3, 4, 5, 3] },
180
+ { name: '对账', values: [5, 5, 2, 3, 4, 2] },
181
+ { name: '归档', values: [1, 6, 6, 1, 2, 5] },
182
+ ],
183
+ ariaLabel: '六天五个环节的并列分组柱状图,五档分类色',
184
+ });
185
+
159
186
  /* 4 · 演示"量程必填" */
160
187
  document.getElementById('try').addEventListener('click', function () {
161
188
  var box = document.getElementById('err');
package/CHANGELOG.md CHANGED
@@ -16,6 +16,133 @@
16
16
 
17
17
  (无)
18
18
 
19
+ ## [0.25.2] - 2026-11-03
20
+
21
+ **修订版:把覆盖率门禁里 41 个「只被 README 认领」的类全部变成真的渲染。
22
+ 量它们的过程本身成了一次体检 —— 查出 4 个静默缺陷(两个方位摆错、分隔线高度 0、
23
+ 说明落在标签右侧),外加 3 条判据在误报。不改类名、不改令牌、不改 API。**
24
+
25
+ ### 清零:41 个未渲染类 → 0
26
+
27
+ `css-coverage-baseline.json` 的棘轮从 41 条降到 **0**(`unrendered: []`)。
28
+ 三类分开处理,不是一把梭往 demo 里塞空 div:
29
+
30
+ | 类型 | 数量 | 做法 |
31
+ |---|---|---|
32
+ | JS 拼接生成、**其实一直在渲染** | 10 | `tooltip--top/bottom/inset-inline-*`、`drawer--left/right/top/bottom`、`toast--success/warning` |
33
+ | 纯 CSS、demo 里一次没用过 | 30 | 逐个补真实用例(徽标/卡片/表格/抽屉/提示…) |
34
+ | README 里的兼容别名 | 1 | `rep__lede`:加判据要求它与 `rep__standfirst` 计算样式**逐项相同** |
35
+
36
+ 第一类是判据自己的盲区:`'tooltip--' + CLS[pos]` 这种拼串在源码里 grep 不到
37
+ `tooltip--inset-inline-start`,于是"渲染了但没人认领"。修法不是放宽判据,
38
+ 而是**改成完整字面量映射**(`{ top: 'tooltip--top', … }`)——顺带把
39
+ `toast({variant:'warn'})`(拼出不存在的类 ⇒ 警示条渲染成白条)也收成了白名单,
40
+ `{place:'tp'}`(拼出 `drawer--tp` ⇒ 静默贴右)现在退回默认值。
41
+
42
+ ### 修复:四个「渲染出来才发现」的缺陷
43
+
44
+ | 缺陷 | 实测症状 | 修法 |
45
+ |---|---|---|
46
+ | 抽屉的左右/上下**根本不动** | 1280×800 下四个方位量矩形:`--left` 的 left=860(该 0)、`--bottom` 的 top=0(该 380)。两个方向摆错,控制台干净、契约全绿 | 容器只留主轴对齐,落位交给各方向的 `margin-inline-end:auto` / `align-self`。四个方位补了逐条判据 |
47
+ | 菜单分隔线**高度 0** | 只写 `data-dd-sep` 不写类名时,量到的高度是 0px,两组菜单项直接贴着,像漏了一条线 | 由 JS 补 `.dd__sep`(和它早就在做的 `dd__item` 同一个理由:组件级类名不该要求使用者手抄) |
48
+ | `.choice__hint` 落在标签**右侧** | README 写"标签下方",实际是第三个 flex item,被 `flex:1` 的 label 挤到最右边 | `.choice` 加 `flex-wrap`,hint 独占一行并缩进到与文字左对齐(20px 勾选框 + 8px gap) |
49
+ | `.drange--disabled` **只管一半** | 文档写"整体禁用",CSS 只把预设与清除置灰;日期输入照样能改 | 文档改成"视觉禁用",demo 补正确组合(类负责观感、原生 `disabled` 负责行为),并说明只做一半的后果 |
50
+
51
+ ### 修复:三条判据在**误报**(比漏报更伤信任)
52
+
53
+ - **form-validation「错误文案具体」**:原来扫的是"所有 `aria-describedby` 指向的文本",
54
+ 于是帮助文字与"校验中"提示也被当错误文案审;判据里还有一条
55
+ `t.length <= 最长含糊词长度 + 2`(=9)—— 等于"凡是 ≤9 字都算含糊",与内容无关。
56
+ 实测把一个 8 字的正常提示判成"太含糊"。现在只审 `.field__error` / `role=alert`
57
+ / `aria-invalid` 指向的文案,且只判"太短(<4 字)"与"只由含糊词构成(≤6 字)"。
58
+ 反向控制:把文案换成「输入有误」⇒ 立刻转红。
59
+ - **report「宽通道」**:参照物取成了 `.rep__sec`(列容器,544px),而行长只施加在
60
+ 内容块上(420px)⇒ "宽通道"永远看着没打开。改成探针式:同一篇正文里插一个块,
61
+ 先量(被行长压住)、加类再量(放开),两个值必须拉开。
62
+ 顺带记下:`--wide` 只在 **finance** 版有意义(magazine 的 42em 比列还宽)。
63
+ - **coverage-gate 的"未渲染"口径**:它不是"没渲染",是"文本里没提到"。
64
+ 文档里补了一句,免得后人把绿当成"视觉回归看得到它"。
65
+
66
+ ### 新增判据(10 条)
67
+
68
+ | 契约 | 新增 |
69
+ |---|---|
70
+ | `drawer-check` | 四个方位各量一次"矩形贴哪条边"(此前只测 `right`) |
71
+ | `tabs-contract` | 纵向变体:`flex-direction:column`、面板在右侧、`↓`/`↑` 真的切换(此前纵向只有 CSS,没有判据) |
72
+ | `report-check` | `rep__lede` 与主名计算样式逐项相同;宽通道探针(with/without 两次测量) |
73
+
74
+ ### 视觉基线
75
+
76
+ 16 页有变化(浅色 16 / 暗色 16 / RTL 8,都是新增区块),`--update` 重录后
77
+ **109 / 109 张逐字节一致**;Linux 基线同批在 CI 上重录(`release.yml` 的
78
+ `shoot_baseline`)。`size-baseline` 重录:drawer.css +34.5%(1.5 KB 全是注释 ——
79
+ 这段"两个方位摆错"的现场记录值得留着)、drawer.js +8.5%、choice.css +5.3%,
80
+ 其余 65 个文件不变。
81
+
82
+ ---
83
+
84
+ ## [0.25.1] - 2026-11-03
85
+
86
+ **修订版:修 `agentcss pdf` 在「长文档」上的一处误判 —— 50 页的文档会被报
87
+ 「无法判定预算」(退出码 4),哪怕预算够、文件也没坏。不改类名、不改令牌、不改 API。**
88
+
89
+ ### 修复:长文档的页树根 `Count` 被读成了叶节点的数
90
+
91
+ `agentcss pdf <page> --max-pages=N` 会交叉核对两个页数:`/Type /Page` 对象数
92
+ 与页树根的 `/Count`。两者不一致时**拒绝判定预算**(这是对的 —— 预算判错的代价是
93
+ "多印 40 页纸,而工具说通过")。
94
+
95
+ 问题出在"页树根"的读法:旧实现取的是**文件里第一个** `/Count`。
96
+ 而 Chromium 的页树是**分层**的,实测一份 50 页产物里有 8 个 `/Count`:
97
+
98
+ ```
99
+ 8, 8, 8, 8, 8, 8, 2, 50 ← 只有最后一个是根
100
+ ```
101
+
102
+ 第一个是叶节点的 8。于是"页对象 50"和"页树 8"打架,工具据此报
103
+ `unreadable` —— **一份完全正常的 50 页文档被读法错误挡在门外**,
104
+ 使用者会以为是自己的 PDF 坏了。
105
+
106
+ 现在顺引用走:`/Type /Catalog` → 它的 `/Pages N 0 R` → 对象 N 的 `/Count`。
107
+ 对象内键序不保证,所以两侧各开一个窗口找;解析不到时返回 `null`
108
+ (= 不做交叉核对),**不退回"第一个匹配"** —— 宁可少一道核对,
109
+ 也不能拿一个错数字去否决用户的文件。
110
+
111
+ 反向控制:把旧读法放回去,同一份样本立刻读成 8,两条端到端断言同时转红。
112
+
113
+ ### 新增:打印门禁的六个对抗夹具(12 条断言)
114
+
115
+ 此前的反向控制喂的是**合成数字**,证明不了"判据拿到真实极端页面还认得出来"。
116
+ 新增 `05-audit/fixtures/print-adversarial/`(生成器 `_gen.py`,改规模只改常量):
117
+
118
+ | 面 | 断言 |
119
+ |---|---|
120
+ | 50 页 | 页数正好 50(多一页 = 尾部空页);文本流与页数**逐个**对上 |
121
+ | 一行表 | 整表落在 1 页(`thead` 被重复到第二页会变 2 页) |
122
+ | 200 列 | 判出横向溢出;配一份压进纸面的对照,证明不是"见大表就红" |
123
+ | 嵌套浮层 | 打印态下两层遮罩都落回正常流、不铺底色,正文与两层弹窗的字都在 |
124
+ | 近空白页 | 一个字符也算字;首屏无字、**三页文档中间一页无字**都必须报出来 |
125
+ | 单块超一页 | 与"同内容可拆分"的对照组比 run 数,少了就是静默丢字 |
126
+
127
+ 12 条都做过变异验证:拆掉 `overlay.css` 的 `@media print` ⇒ ④ 转红;
128
+ 夹具从 50 页改成 51 页 ⇒ ① 转红;夹具目录改名 ⇒ 报"缺 10/10 个"并退出非 0
129
+ (缺测不许算过)。
130
+
131
+ ### 修复:`pagesWithoutText` 的计数会被二进制噪声抵掉
132
+
133
+ ⑫ 算的是 `无字页 = 页数 − 文本流数`,而"文本流"只要求流里出现 `BT` 两个字节 ——
134
+ 字体 / cmap / ICC 的二进制里会**偶然**出现它。实测同一批夹具 10 个里有 6 个多计
135
+ (50 页数出 51 个流、1 页数出 2 个)。而差是**没有位置信息**的:
136
+ 多出来的那一个流,正好抵掉一张真没字的纸 ⇒ ⑫ 报 0,安静地通过。
137
+
138
+ 现在要求同一流里同时有 `BT`(开始文本)+ `Tf`(选字体)+ `Tj`/`TJ`(真的写出来)。
139
+ 实测 10 个夹具逐个精确。副作用是真实页面的 run 数各少了 1–3 个
140
+ (`recipe-report` 1071→1070、`type-wide-table` 1526→1523、`type-form` 578→576、
141
+ `type-grid-media` 517→516,页数一个没变)—— 那些 run 是二进制里的假算子,
142
+ 基线按新口径重录。
143
+
144
+ ---
145
+
19
146
  ## [0.25.0] - 2026-11-03
20
147
 
21
148
  **次版本:新增一个子命令(`agentcss pdf`)+ 新增契约字段(`printPolicy` / `pageBudget`)
package/CONTRIBUTING.md CHANGED
@@ -29,7 +29,7 @@ npm run gate # 全量:含浏览器类
29
29
  npm run gate:fast # 只跑静态层,不用装浏览器
30
30
  ```
31
31
 
32
- 全量是 **232 道门禁**(默认 `min(核数, 6)` 路并发,实测 583 秒 / 退出码 0;
32
+ 全量是 **233 道门禁**(默认 `min(核数, 6)` 路并发,实测 583 秒 / 退出码 0;
33
33
  单道耗时累加 1658 秒,并发效率约 2.8×。组件层要起浏览器,还要逐张比 **109 张**视觉截图 ——
34
34
  47 浅色 + 47 暗色 + 15 RTL,按平台分集,只比本平台那套)。
35
35
  只想确认没改坏静态契约的话用 `gate:fast`,几秒到几分钟。
package/START-HERE.md CHANGED
@@ -38,7 +38,7 @@
38
38
  | 输入框 | `02-primitives/input/input.css` | 253 | — | 有表单就要 |
39
39
  | 标签 / 徽章 | `02-primitives/badge/badge.css` | 141 | — | 有状态标记就要("已披露""停牌") |
40
40
  | 卡片 | `02-primitives/card/card.css` | 283 | — | 有分组内容就要 |
41
- | 单选 / 多选 | `02-primitives/choice/choice.css` | 424 | — | 单选 / 多选就要 |
41
+ | 单选 / 多选 | `02-primitives/choice/choice.css` | 434 | — | 单选 / 多选就要 |
42
42
  | 下拉选择 | `02-primitives/select/select.css` | 151 | **必需 631** (`select.js`) | 原生 select 的外观或交互不够用就要 |
43
43
  | 可搜索选择 | `02-primitives/combobox/combobox.css` | 223 | **必需 477** (`combobox.js`) | 选项多到要搜索,或要选多个并留成标签就要 |
44
44
  | 开关 | `02-primitives/switch/switch.css` | 429 | — | 有"立即生效的设置"就要 |
@@ -49,12 +49,12 @@
49
49
  | 日期区间 | `02-primitives/date-range/date-range.css` | 206 | **必需 301** (`date-range.js`) | 要选起止日期就要 |
50
50
  | 标签页 | `03-patterns/tabs/tabs.css` | 172 | **必需 316** (`tabs.js`) | 同层级内容切换就要 |
51
51
  | 折叠面板 | `03-patterns/accordion/accordion.css` | 453 | **必需 389** (`accordion.js`) | 长表单分段就要 |
52
- | 动作菜单 | `03-patterns/dropdown/dropdown.css` | 135 | **必需 547** (`dropdown.js`) | 要一组动作(新建 / 导出 / 删除)就要 |
53
- | 文字提示 | `03-patterns/tooltip/tooltip.css` | 89 | **必需 280** (`tooltip.js`) | 图标按钮或缩写需要一句话解释就要 |
52
+ | 动作菜单 | `03-patterns/dropdown/dropdown.css` | 135 | **必需 555** (`dropdown.js`) | 要一组动作(新建 / 导出 / 删除)就要 |
53
+ | 文字提示 | `03-patterns/tooltip/tooltip.css` | 89 | **必需 284** (`tooltip.js`) | 图标按钮或缩写需要一句话解释就要 |
54
54
  | 分页 | `03-patterns/pagination/pagination.css` | 195 | **必需 401** (`pagination.js`) | 数据多到要翻页就要 |
55
55
  | 树形 | `03-patterns/tree/tree.css` | 122 | **必需 464** (`tree.js`) | 层级数据(文件 / 组织 / 分类)要展开就要 |
56
- | 侧边抽屉 | `03-patterns/drawer/drawer.css` | 129 | **必需 361** (`drawer.js`) | 要从侧边滑出面板就要 |
57
- | 弹窗 / 吐司 | `03-patterns/overlay/overlay.css` | 338 | **必需 684** (`overlay.js`) | 需要弹层(吐司 / 模态)就要 |
56
+ | 侧边抽屉 | `03-patterns/drawer/drawer.css` | 145 | **必需 376** (`drawer.js`) | 要从侧边滑出面板就要 |
57
+ | 弹窗 / 吐司 | `03-patterns/overlay/overlay.css` | 338 | **必需 697** (`overlay.js`) | 需要弹层(吐司 / 模态)就要 |
58
58
  | 状态(空 / 加载 / 错 / 成功) | `03-patterns/states/states.css` | 269 | — | **凡是会异步取数就要** |
59
59
  | 表单校验 | `03-patterns/form-validation/form-validation.css` | 274 | — | 提交前要校验就要 |
60
60
  | 列表增删 | `03-patterns/list/list.css` | 162 | 可选 201 (`flip.js`,FLIP 增删动画,纯增强) | 列表会动态增删就要 |
@@ -70,7 +70,7 @@
70
70
  | detail-page | `04-recipes/detail-page/detail-page.css` | 190 | — | |
71
71
  | report | `04-recipes/report/report.css` | 369 | — | |
72
72
  | table | `04-recipes/table/table.css` | 409 | — | |
73
- | bar | `09-assets/bar/bar.css` | 181 | **必需 320** (`bar.js`) | |
73
+ | bar | `09-assets/bar/bar.css` | 181 | **必需 324** (`bar.js`) | |
74
74
  | model-viewer | `09-assets/model-viewer/model-viewer.css` | 143 | **必需 423** (`model-viewer.js`) | |
75
75
  | scientific-plot | `09-assets/scientific-plot/scientific-plot.css` | 141 | **必需 603** (`scientific-plot.js`) | |
76
76
  | sparkline | `09-assets/sparkline/sparkline.css` | 158 | **必需 508** (`sparkline.js`) | |