agentcss 0.25.1 → 0.25.3

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 (52) hide show
  1. package/01-tokens/tokens.css +1 -1
  2. package/02-primitives/button/README.md +7 -0
  3. package/02-primitives/button/demo.html +52 -0
  4. package/02-primitives/card/README.md +6 -1
  5. package/02-primitives/card/demo.html +13 -0
  6. package/02-primitives/choice/choice.css +10 -0
  7. package/02-primitives/choice/demo.html +38 -0
  8. package/02-primitives/date-range/README.md +1 -1
  9. package/02-primitives/date-range/demo.html +41 -1
  10. package/02-primitives/input/README.md +3 -2
  11. package/02-primitives/input/demo.html +42 -0
  12. package/03-patterns/accordion/demo.html +113 -0
  13. package/03-patterns/content/demo.html +17 -0
  14. package/03-patterns/drawer/README.md +11 -1
  15. package/03-patterns/drawer/drawer.css +22 -6
  16. package/03-patterns/drawer/drawer.js +16 -1
  17. package/03-patterns/dropdown/README.md +10 -8
  18. package/03-patterns/dropdown/demo.html +5 -0
  19. package/03-patterns/dropdown/dropdown.js +8 -0
  20. package/03-patterns/form-validation/demo.html +20 -0
  21. package/03-patterns/list/demo.html +10 -0
  22. package/03-patterns/overlay/overlay.js +14 -1
  23. package/03-patterns/pagination/demo.html +23 -0
  24. package/03-patterns/states/demo.html +39 -1
  25. package/03-patterns/tabs/demo.html +34 -0
  26. package/03-patterns/tooltip/tooltip.js +8 -4
  27. package/04-recipes/app-shell/demo.html +27 -0
  28. package/04-recipes/table/demo.html +34 -0
  29. package/09-assets/bar/bar.js +8 -4
  30. package/09-assets/bar/demo.html +27 -0
  31. package/API.md +13 -5
  32. package/CHANGELOG.md +211 -1
  33. package/CONTRIBUTING.md +5 -5
  34. package/README.zh-CN.md +3 -3
  35. package/START-HERE.md +6 -6
  36. package/ai/components.json +6 -3
  37. package/ai/tokens.json +100 -99
  38. package/ai/tokens.schema.json +96 -50
  39. package/dist/02-primitives/choice/choice.css +1 -1
  40. package/dist/03-patterns/drawer/drawer.css +1 -1
  41. package/dist/03-patterns/drawer/drawer.js +10 -1
  42. package/dist/03-patterns/dropdown/dropdown.js +4 -0
  43. package/dist/03-patterns/overlay/overlay.js +7 -1
  44. package/dist/03-patterns/tooltip/tooltip.js +3 -3
  45. package/dist/09-assets/bar/bar.js +4 -3
  46. package/dist/COSTS.md +13 -13
  47. package/docs/ACCESSIBILITY.md +1 -1
  48. package/docs/GUIDANCE.md +82 -0
  49. package/docs/LESSONS.md +182 -0
  50. package/index.html +1 -1
  51. package/llms-full.txt +3 -3
  52. package/package.json +4 -2
@@ -61,6 +61,11 @@
61
61
  </ul>
62
62
  </div>
63
63
  <div class="log" id="log">(选中项会显示在这里)</div>
64
+ <p class="small">
65
+ 菜单里的那条<strong>分隔线</strong>只写 <code>data-dd-sep</code> 就行:
66
+ <code>role="separator"</code> 与 <code>.dd__sep</code> 由组件补。
67
+ 方向键会跳过它 —— 分隔线不是可选项,让它能被"选中"是错的。
68
+ </p>
64
69
  </div>
65
70
 
66
71
  <h2>2 · 带勾选(选中性不只靠颜色)</h2>
@@ -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/API.md CHANGED
@@ -29,23 +29,31 @@
29
29
  除了 `<script src>`,16 个交互组件的脚本还有一个 ESM 入口:
30
30
 
31
31
  ```js
32
- // 只要一个
32
+ // 只要一个(推荐:只引入这一个脚本,体积最小)
33
33
  import { Accordion } from 'agentcss/esm/accordion.mjs';
34
34
 
35
- // 或者一次拿全(体积更大,Vite/Rollup 会按需摇树)
35
+ // 或者一次拿全 —— ⚠️ 不会摇树,17 个行为脚本全部进产物
36
36
  import { Accordion, Tabs, Select } from 'agentcss/esm';
37
37
  ```
38
38
 
39
- 三条容易踩的:
39
+ 四条容易踩的:
40
40
 
41
41
  1. **入口是包装,不是第二份实现**。`esm/*.mjs` 只做
42
42
  「导入副作用 + 把全局上那个对象 re-export」,所以
43
43
  `Accordion === window.Accordion` 恒成立(门禁 `behavior-api` 断言这一点)。
44
44
  它由 `05-audit/gen-esm.py` 生成,改源码后要重新生成,`esm-gen` 门禁会拦住漂移。
45
- 2. **无 DOM 环境可以直接 import**(SSR 安全)。但自动初始化会被跳过 ——
45
+ 2. **聚合入口不会摇树**(0.25.3 更正)。`esm/*.mjs` 的形式是
46
+ 「先 `import` 目标脚本(副作用:把对象挂到全局),再把它 re-export」——
47
+ 模块体本身就有副作用,打包器无法判断哪个导出没被用到,只能整包收下。
48
+ 实测(esbuild):`import { Select } from 'agentcss/esm'` 产物 **190 KB**,
49
+ 而 `import { Select } from 'agentcss/esm/select'` 只有 **20.8 KB**(约 9 倍)。
50
+ `package.json` 的 `sideEffects` 把 `**/*.js` / `**/*.mjs` 标成有副作用,
51
+ **这是按事实写的、是对的** —— 错的是 0.25.2 之前本文档那句"会按需摇树"。
52
+ ⇒ 要体积就逐个 `import 'agentcss/esm/<组件>'`。
53
+ 3. **无 DOM 环境可以直接 import**(SSR 安全)。但自动初始化会被跳过 ——
46
54
  服务端没有 DOM 可扫,到浏览器里再调 `init()`/构造函数。16 个脚本
47
55
  在 Node 里 `require` 全部成功(门禁的判据 A)。
48
- 3. **重复初始化是安全的**。`init()` 与构造函数都做了幂等处理:
56
+ 4. **重复初始化是安全的**。`init()` 与构造函数都做了幂等处理:
49
57
  同一 root 只会有一个实例,不会出现「点一次切换两次」。
50
58
 
51
59
  ### 四种接入形态