agentcss 0.24.2 → 0.25.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 (76) hide show
  1. package/01-tokens/README.md +1 -1
  2. package/01-tokens/tokens.css +67 -1
  3. package/01-tokens/typography.css +1 -1
  4. package/02-primitives/button/demo.html +8 -0
  5. package/02-primitives/card/README.md +15 -0
  6. package/02-primitives/choice/README.md +30 -0
  7. package/02-primitives/choice/choice.css +1 -1
  8. package/02-primitives/input/README.md +1 -0
  9. package/02-primitives/select/README.md +42 -2
  10. package/02-primitives/select/demo.html +11 -6
  11. package/02-primitives/select/select.js +184 -94
  12. package/02-primitives/timeline/demo.html +2 -2
  13. package/02-primitives/timeline/timeline.css +1 -1
  14. package/03-patterns/accordion/README.md +15 -0
  15. package/03-patterns/content/README.md +11 -0
  16. package/03-patterns/drawer/README.md +1 -1
  17. package/03-patterns/dropdown/demo.html +1 -1
  18. package/03-patterns/form-validation/README.md +18 -0
  19. package/03-patterns/form-validation/form-validation.css +12 -1
  20. package/03-patterns/list/README.md +11 -0
  21. package/03-patterns/overlay/README.md +26 -6
  22. package/03-patterns/overlay/overlay.css +22 -3
  23. package/03-patterns/overlay/overlay.js +63 -17
  24. package/03-patterns/states/README.md +15 -0
  25. package/03-patterns/states/states.css +18 -1
  26. package/03-patterns/tooltip/README.md +12 -0
  27. package/04-recipes/longform/longform.html +3 -3
  28. package/04-recipes/report/README.md +9 -0
  29. package/04-recipes/report/report.css +5 -0
  30. package/04-recipes/table/README.md +16 -0
  31. package/04-recipes/tierA-tokens.css +3 -3
  32. package/09-assets/README.md +18 -0
  33. package/09-assets/bar/bar.css +25 -10
  34. package/09-assets/echarts-adapter/demo.html +3 -1
  35. package/09-assets/echarts-adapter/echarts-adapter.js +107 -34
  36. package/09-assets/scientific-plot/README.md +2 -1
  37. package/09-assets/scientific-plot/scientific-plot.js +0 -7
  38. package/AGENTS.md +2 -1
  39. package/CHANGELOG.md +317 -18
  40. package/CONTRIBUTING.md +6 -5
  41. package/README.md +6 -2
  42. package/README.zh-CN.md +19 -7
  43. package/SECURITY.md +5 -2
  44. package/START-HERE.md +7 -7
  45. package/SUPPORT.md +86 -24
  46. package/ai/START-HERE.md +3 -2
  47. package/ai/cli.js +126 -6
  48. package/ai/components.json +258 -2
  49. package/ai/components.meta.json +257 -40
  50. package/ai/components.schema.json +96 -0
  51. package/ai/contract.schema.json +11 -0
  52. package/ai/pdf-pages.js +79 -0
  53. package/ai/pdf.js +354 -0
  54. package/ai/tokens.json +2 -2
  55. package/ai/validate.js +48 -1
  56. package/dist/01-tokens/tokens.css +1 -1
  57. package/dist/02-primitives/select/select.js +158 -71
  58. package/dist/03-patterns/form-validation/form-validation.css +1 -1
  59. package/dist/03-patterns/overlay/overlay.css +1 -1
  60. package/dist/03-patterns/overlay/overlay.js +40 -11
  61. package/dist/03-patterns/states/states.css +1 -1
  62. package/dist/04-recipes/report/report.css +1 -1
  63. package/dist/04-recipes/tierA-tokens.css +1 -1
  64. package/dist/09-assets/bar/bar.css +1 -1
  65. package/dist/09-assets/echarts-adapter/echarts-adapter.js +75 -33
  66. package/dist/09-assets/scientific-plot/scientific-plot.js +0 -7
  67. package/dist/COSTS.md +37 -37
  68. package/docs/ACCESSIBILITY.md +3 -3
  69. package/docs/FACTS.md +1 -1
  70. package/docs/LAYER-BASELINE.md +28 -3
  71. package/docs/PRINT.md +450 -0
  72. package/docs/RELEASE-PROCESS.md +78 -3
  73. package/index.html +1 -1
  74. package/llms-full.txt +7 -7
  75. package/llms.txt +5 -0
  76. package/package.json +6 -1
@@ -116,17 +116,19 @@
116
116
  for (var i = 0; i < inertRefs.length; i++) {
117
117
  if (inertRefs[i].node === node) { inertRefs[i].count++; return; }
118
118
  }
119
- inertRefs.push({ node: node, count: 1 });
119
+ /* 🔴 记下**打开之前**的状态:页面可能本来就把某块设为 inert
120
+ (比如另一个弹层、或宿主自己的锁屏)。固定写回 false 会把它误解锁。 */
121
+ inertRefs.push({ node: node, count: 1, prev: node.inert === true });
120
122
  node.inert = true;
121
123
  }
122
124
 
123
- /** 还一个引用;降到 0 才真正解锁。 */
125
+ /** 还一个引用;降到 0 才真正还原。 */
124
126
  function inertRelease(node) {
125
127
  for (var i = 0; i < inertRefs.length; i++) {
126
128
  if (inertRefs[i].node !== node) continue;
127
129
  inertRefs[i].count--;
128
130
  if (inertRefs[i].count <= 0) {
129
- node.inert = false;
131
+ node.inert = inertRefs[i].prev === true;
130
132
  inertRefs.splice(i, 1);
131
133
  }
132
134
  return;
@@ -578,17 +580,58 @@
578
580
  var nodes = Array.prototype.slice.call(
579
581
  document.body.querySelectorAll('body > *')
580
582
  );
581
- nodes.forEach(function (n) {
582
- /* 任何已有的 backdrop(外层)都跳过 —— 它们自己管自己的 */
583
- if (n.classList.contains('dialog-backdrop')) return;
584
- if (useInert) {
585
- inerted.push(n);
586
- inertAcquire(n);
587
- } else {
588
- ariaed.push(n);
589
- ariaAcquire(n);
590
- }
591
- });
583
+ /* 🔴 以前这里 `if (n.classList.contains('dialog-backdrop')) return;`
584
+ —— 把所有已存在的弹层(外层 dialog)当成"它自己管自己"跳过了。
585
+ 实际后果:开 A 再开 B,A 既没 inert 也没 aria-hidden,
586
+ **两个 dialog 同时暴露给读屏**,Tab 也能从 B 走回 A(有 inert 时
587
+ 虽然背景被锁,但 A 和 B 都是"前景",只是在同一个 body 下并列)。
588
+ ⇒ 正解:只跳过**自己**这一层 backdrop,别的弹层照常隔离。 */
589
+ function isolate(n) {
590
+ if (!n || n === backdrop) return;
591
+ if (useInert) { inerted.push(n); inertAcquire(n); }
592
+ else { ariaed.push(n); ariaAcquire(n); }
593
+ }
594
+ nodes.forEach(isolate);
595
+
596
+ /* 🔴 只在 open 时拍一次快照是不够的:弹层开着的时候,
597
+ 宿主页面完全可能再往 body 里插东西(toast、埋点 iframe、框架的 portal)。
598
+ 那些节点不在快照里 ⇒ 不受隔离 ⇒ 读屏和 Tab 都能进。
599
+ ⇒ 用 MutationObserver 补:**只盯 body 的直接子节点**(与快照口径一致),
600
+ 且不隔离新出现的 backdrop —— 那是"又开了一层",它会自己隔离别人。 */
601
+ var mo = null;
602
+ if (typeof window.MutationObserver === 'function') {
603
+ mo = new window.MutationObserver(function (records) {
604
+ /* 🔴 自愈:如果这一层的 backdrop 已经被**外部**从 DOM 里摘掉
605
+ (框架卸载、测试清理、别人 innerHTML='' 都会这么干),
606
+ 那 close() 永远不会跑 ⇒ 观察器成了幽灵:
607
+ 它会继续把之后插入 body 的节点一个个设成 inert,且没人释放。
608
+ 症状:后续任何 dialog 都抢不回焦点(目标节点被幽灵锁着)。
609
+ ⇒ 一旦发现自己已不在文档里,就地回收。 */
610
+ if (!document.documentElement.contains(backdrop)) {
611
+ if (mo) { mo.disconnect(); mo = null; }
612
+ if (closed) return; /* 正常 close 已还过,别还第二次 */
613
+ closed = true;
614
+ inerted.forEach(inertRelease);
615
+ ariaed.forEach(ariaRelease);
616
+ inerted = [];
617
+ ariaed = [];
618
+ if (uninstallIsolation) { uninstallIsolation(); uninstallIsolation = null; }
619
+ unlockScroll();
620
+ return;
621
+ }
622
+ for (var r = 0; r < records.length; r++) {
623
+ var added = records[r].addedNodes;
624
+ for (var a = 0; a < added.length; a++) {
625
+ var n = added[a];
626
+ if (!n || n.nodeType !== 1) continue;
627
+ if (n.classList && n.classList.contains('dialog-backdrop')) continue;
628
+ if (n.inert === true || n.getAttribute('aria-hidden') === 'true') continue;
629
+ isolate(n);
630
+ }
631
+ }
632
+ });
633
+ mo.observe(document.body, { childList: true });
634
+ }
592
635
 
593
636
  function close() {
594
637
  if (closed) return;
@@ -616,6 +659,7 @@
616
659
  if (openStack[si] === stackEntry) { openStack.splice(si, 1); break; }
617
660
  }
618
661
  if (uninstallIsolation) { uninstallIsolation(); uninstallIsolation = null; }
662
+ if (mo) { mo.disconnect(); mo = null; }
619
663
 
620
664
  /* ⭐ 引用计数:还一个引用,降到 0 才真的解锁 ⇒ 与关闭顺序无关 */
621
665
  inerted.forEach(inertRelease);
@@ -623,9 +667,11 @@
623
667
  ariaed.forEach(ariaRelease);
624
668
  unlockScroll();
625
669
 
626
- /* 🔴 焦点必须回到**触发它的那个元素**。
627
- 不还的话,键盘用户要重新 Tab 一遍才能回到原处。 */
628
- fr.restore();
670
+ /* 🔴 焦点必须回到**触发它的那个元素**,但**只在焦点还是我的时候**。
671
+ `focusIsMine` 这个判断以前算了却没用(死变量)⇒ 关外层时会把
672
+ 焦点从内层手里抢走。现在真的拿它把关。 */
673
+ if (focusIsMine) fr.restore();
674
+ else fr.clear();
629
675
  /* ⚠️ 不写 `document.body.focus()`:body 默认不可聚焦,
630
676
  那行从来没起过作用,只会让人以为"有兜底"。 */
631
677
  }
@@ -36,6 +36,21 @@
36
36
 
37
37
  骨架屏必须标 `aria-hidden="true"`,否则屏幕阅读器会把它读成一堆无意义色块。
38
38
 
39
+ ### 形状变体(本文件自带一份,注意与 primitive 重复)
40
+
41
+ | 类 | 形状 |
42
+ |---|---|
43
+ | `.skeleton` | 底块(底色 `--surface-sunken`,圆角 `--r-sm`,带微光扫过) |
44
+ | `.skeleton--text` | 一行文字(`height:1em`,下边距 `0.35em`);`:last-child` 收窄到 70%,看起来像段落结尾 |
45
+ | `.skeleton--title` | 标题行(`height:1.4em`,宽度 60%) |
46
+ | `.skeleton--avatar` | 头像(`40×40`,`--r-pill` 全圆),配 `.skeleton--text` 做列表项占位 |
47
+ | `.skeleton--block` | 大块内容区(`height:120px`) |
48
+
49
+ > ⚠️ **`02-primitives/skeleton/skeleton.css` 里另有一份实现**,两者已分叉
50
+ > (底色令牌、高度写法、变体集合都不同:那边有 `--circle` 没有 `--avatar`/`--title`)。
51
+ > 谁后加载谁生效。已登记在 `05-audit/css-ownership.json`,去重计划在 0.26.0。
52
+ > **本文件只用 `.skeleton` 时,请引 primitive 那份,别两边都引。**
53
+
39
54
  ---
40
55
 
41
56
  ## 300ms 延迟(需要 JS,约 10 行)
@@ -129,7 +129,24 @@
129
129
  background: linear-gradient(90deg,
130
130
  transparent 0%, var(--sk-hi) 50%, transparent 100%);
131
131
  transform: translateX(-100%);
132
- animation: skeleton-sweep 1.4s ease-in-out infinite;
132
+ }
133
+ /* 🔴 动画必须包在 `prefers-reduced-motion: no-preference` 里(WCAG 2.3.3)。
134
+ 以前这里是**无条件** `animation: skeleton-sweep 1.4s infinite`:
135
+
136
+ · 本文件先加载(states/demo.html 就是这种情况,它没引 skeleton.css)
137
+ ⇒ 减少动态效果的用户照样看到闪烁的微光;
138
+ · 与 `02-primitives/skeleton/skeleton.css` 同特异性 ⇒ 谁后加载谁定生死。
139
+
140
+ 即"要不要尊重减少动态效果"取决于 CSS 顺序。与 E254 同类。
141
+ ⚠️ 这里与 skeleton.css 是**两份实现**(已登记在 css-ownership.json),
142
+ 修的时候两边都要看。 */
143
+ @media (prefers-reduced-motion: no-preference) {
144
+ .skeleton::after {
145
+ animation: skeleton-sweep 1.4s ease-in-out infinite;
146
+ }
147
+ }
148
+ @media (prefers-reduced-motion: reduce) {
149
+ .skeleton::after { animation: none; opacity: 0; }
133
150
  }
134
151
  @keyframes skeleton-sweep {
135
152
  to { transform: translateX(100%); }
@@ -31,6 +31,18 @@ Tooltip.attach(按钮, '说明文字', 'top');
31
31
  | `text` | string | 纯文本(用 `textContent` 写入,**天然免疫注入**)|
32
32
  | `place` | `top`(默认)/ `bottom` / `inset-inline-start` / `inset-inline-end` | 方位 |
33
33
 
34
+ `place` 原样拼进类名(`.tooltip--` + `place`),所以四个方位对应的就是这四个类:
35
+
36
+ | 类 | 出现位置 |
37
+ |---|---|
38
+ | `.tooltip--top` | 在触发元素**下方**,箭头朝上(默认) |
39
+ | `.tooltip--bottom` | 在触发元素**上方**,箭头朝下 |
40
+ | `.tooltip--inset-inline-start` | 在触发元素**右侧**,箭头朝左。用逻辑属性,RTL 下自动镜像到左边 |
41
+ | `.tooltip--inset-inline-end` | 在触发元素**左侧**,箭头朝右。同样随书写方向镜像 |
42
+
43
+ ⚠️ 方位名容易读反:`--top` 指的是**箭头朝上**,所以气泡在下方。
44
+ 换成"气泡在哪边"去想会写错。
45
+
34
46
  公开 API 只有 `Tooltip.attach()`,**没有** `show` / `hide` ——
35
47
  显隐由 hover / focus / click 自动驱动。
36
48
 
@@ -106,7 +106,7 @@ body {
106
106
 
107
107
  /* ---------------------------------------------------------------- 布局
108
108
  ⚠️ 改版:原来容器 1040px + 目录 13rem + 正文 68ch = 896px,
109
- 右边空出 144px,视觉重心整个偏左 —— 评审意见"排版混乱"。
109
+ 右边空出 144px,视觉重心整个偏左 —— 整页看起来是散的。
110
110
 
111
111
  改法不是"匀称",是**把空间用完**:
112
112
  容器 920px = 目录 11rem(176) + gap 40 + 正文 72ch(≈680) + 余量 24
@@ -152,7 +152,7 @@ body {
152
152
 
153
153
  /* 这一页唯一的视觉焦点。
154
154
  🔴 改版要点:标题从衬线改成**无衬线 + 更大 + 紧字距**。
155
- 原来衬线 40px 只比正文大一档,撑不起焦点(评审意见"视觉效果不强")。
155
+ 原来衬线 40px 只比正文大一档,撑不起焦点。
156
156
  现在无衬线 + 最大 3.4rem + letter-spacing -0.02em ≈ 正文的 2.5 倍,
157
157
  而正文保留衬线保证长文可读 —— **两者优势都占**。 */
158
158
  .article__title {
@@ -187,7 +187,7 @@ body {
187
187
  }
188
188
 
189
189
  /* 小标题:无衬线,与主标题同一套语言 —— 层级靠字号和字重,不靠颜色
190
- 🔴 改版第二轮:评审意见"整体尖锐、仍有 AI 痕迹"。
190
+ 🔴 改版第二轮:整体偏尖锐,仍有"模板感"。
191
191
  诊断来源:① 通栏 1px 灰线(硬边)② 字距 -0.022em + weight 600(太紧太重)
192
192
  处理:通栏线换成**短蓝线段**(经典编辑排版手法,软化且增加节奏),
193
193
  字距放宽、字重降一级。锐度降下来,但层级对比反而更清楚。 */
@@ -54,6 +54,15 @@ report.css ← 本系统
54
54
  > ⭐ 为什么钉死六种:报告里每多一种"自创角色",读者的解读负担就多一分。
55
55
  > 发现自己在写第七种时,应该回头用**结构**(列表、表格、引用)而不是字号表达。
56
56
 
57
+ 导语有两个名字,**`.rep__standfirst` 与 `.rep__lede` 是同一条规则**(分组选择器):
58
+
59
+ | 类 | 说明 |
60
+ |---|---|
61
+ | `.rep__standfirst` | 推荐用这个(全库其余地方都这么叫) |
62
+ | `.rep__lede` | 兼容别名,早期版本用过。留着是为了不让老页面掉样式,新代码别用 |
63
+
64
+ 两者都吃 `--rep-lead-fs` / `--lh-loose` / `--measure-lede`,改一个变量两边一起变。
65
+
57
66
  ### 标记骨架
58
67
 
59
68
  ```html
@@ -361,4 +361,9 @@
361
361
  .rep__fig,
362
362
  .rep table { break-inside: avoid; }
363
363
  .rep__pull { border-inline-start-width: 2px; }
364
+ /* 孤行寡行:段首/段尾不要只留一行在另一页。
365
+ ⚠️ 必须带 `.rep` 前缀 —— 裸 `p` / `li` 会改掉宿主页面所有段落,
366
+ 那是 `bleed-gate` 的硬红线(组件只影响自己)。
367
+ 其他配方 / 使用者的页面要这一条,自己写同样的三行即可。 */
368
+ .rep p, .rep li, .rep blockquote { orphans: 2; widows: 2; }
364
369
  }
@@ -19,6 +19,22 @@
19
19
  | (默认 / `.table--financial`) | 业务与财务数据 | 粘性表头、数字右对齐 + 等宽数字、行高亮 |
20
20
  | `.table--academic` | 学术出版 | **三线表**:去掉竖线/斑马纹/行分隔线,只留顶线、表头下线、底线 |
21
21
  | `.table--stats` | 统计与实验结果 | 估计值 / 标准误 / 置信区间 / p 值各有位置与样式 |
22
+ | `.table--compact` | 密度高、一屏要塞很多行 | 字号降到 `--fs-xs`,单元格内边距收到 `--sp-1 --sp-2`。**只用在小字不伤可读性的场合**(数字表、对账表),正文表格别用 |
23
+
24
+ ### 缺失值:`.cell--na`
25
+
26
+ 空单元格放一个"没有值"的标记时用它(`--text-tertiary` + 居中):
27
+
28
+ ```html
29
+ <td class="cell--na">—</td>
30
+ <td class="cell--na">N/A</td>
31
+ ```
32
+
33
+ 为什么要有专门的类:**空着和"没有"不是一回事**。空单元格读者不知道是漏了还是本来就没有;
34
+ 直接写个普通文本的"—"又会继承数字列的右对齐和等宽字体,看起来像一个数据。
35
+ `.cell--na` 把它拉回中性表达。
36
+
37
+ > 只写 `.table .cell--na` 生效(必须在这张表里面)。
22
38
 
23
39
  ## 列定义(示例页里的用法)
24
40
 
@@ -145,7 +145,7 @@
145
145
 
146
146
  }
147
147
 
148
- @media (prefers-color-scheme: dark) {
148
+ @media screen and (prefers-color-scheme: dark) {
149
149
 
150
150
  :root:not([data-theme="light"]) {
151
151
  --paper: #14171A;
@@ -163,8 +163,8 @@
163
163
  --text-primary: #E4E7EA;
164
164
  --text-secondary: #AEB6C0;
165
165
  --text-tertiary: #9BA3AD;
166
- --text-on-accent: #0D1114;
167
- --text-on-solid: #0D1114;
166
+ --text-on-accent: #15181c;
167
+ --text-on-solid: #15181c;
168
168
  --skeleton-bg: #2E343C;
169
169
  --skeleton-shimmer: rgba(255, 255, 255, 0.06);
170
170
 
@@ -25,6 +25,24 @@ python -m http.server 8000
25
25
  # 然后打开 http://127.0.0.1:8000/09-assets/sparkline/demo.html
26
26
  ```
27
27
 
28
+ ## 从 JS 画东西时:主题色要按**作用域**读
29
+
30
+ 本库的主题是容器级的 —— `[data-theme="dark"]` 可以只罩住一块区域。
31
+ 所以 JS 资产**不能**只读 `document.documentElement`:
32
+
33
+ ```js
34
+ // ❌ 暗容器里的图表会拿到浅色令牌(实测 1.01:1,字看不见)
35
+ getComputedStyle(document.documentElement).getPropertyValue('--text-primary')
36
+
37
+ // ✅ 由渲染目标往上找作用域
38
+ getComputedStyle(el.closest('[data-theme]') || document.documentElement)
39
+ .getPropertyValue('--text-primary')
40
+ ```
41
+
42
+ 判"当前是明还是暗"要量 `--paper` 的**亮度**,不要读 `data-theme` 属性、也不要拿令牌值和字面量比
43
+ (`theme-toggle.js` 只把 `data-theme-current` 写在 `:root` 上,容器永远读不到)。
44
+ `theme-scope-gate` 的 ⑥ 会检查这一点:新加的 JS 资产若从根读颜色,门禁直接红。
45
+
28
46
  ## 图标:本库不内置,需要就自己内联
29
47
 
30
48
  要"关闭""警告""箭头"这类图形时,**一律内联 SVG** ——
@@ -130,6 +130,15 @@
130
130
  .bar__label--max { fill: var(--text-primary); font-weight: 700; }
131
131
  .bar__label--clip { fill: var(--danger, #c0392b); font-weight: 700; }
132
132
 
133
+ /* 打印:描边去掉。
134
+ 那圈 `var(--surface)` 光晕在屏幕上是为了把标注从深色柱子上"抠"出来;
135
+ 纸上没有深色柱子(底色被丢弃),光晕反而变成一圈**白色文字轮廓**。
136
+ 实测(0.24.2,`examples/finance-report`):`stroke: none` 之后,
137
+ 产物里 4 段 #FFFFFF 的文字 run 消失,最差对比度从 1.00:1 升到 5.72:1。 */
138
+ @media print {
139
+ .bar__label { stroke: none; }
140
+ }
141
+
133
142
  /* ---------- 越界说明 ---------- */
134
143
  .bar__note {
135
144
  margin: var(--sp-2) 0 0;
@@ -154,13 +163,19 @@
154
163
  }
155
164
  }
156
165
 
157
- /* ---------- 暗色下容器要跟着换 ---------- */
158
- @media (prefers-color-scheme: dark) {
159
- .bar {
160
- background: var(--surface, #1a1c1e);
161
- border-color: var(--border-decor);
162
- }
163
- .bar__label {
164
- stroke: var(--surface, #1a1c1e);
165
- }
166
- }
166
+ /* ---------- 暗色:**不在这里写分支** ----------
167
+ 这里原来有一段针对 `prefers-color-scheme: dark` 的媒体查询块,
168
+ 里面只有两条:`.bar` 的 background 与 `.bar__label` 的 stroke。
169
+ ⚠️ 那段文字在这里**故意不照抄**:多个门禁拿这个字符串当锚点,
170
+ 注释里出现字面量会把它们带偏(E199 踩过一次)。
171
+ 它做的只有一件事:把 `var()` 的**兜底值**从 #fff 换成暗色值。
172
+ 令牌在的时候(本库的正式用法)两条声明解析结果完全相同 ⇒ 纯冗余。
173
+
174
+ 代价却是真的:
175
+ · 它让这个资产自己长出一个主题分支,绕过令牌层
176
+ (`theme-scope-gate` 判据⑤ 只扫 02/03/04,扫不到 09-assets,
177
+ 所以这条分支一直绿着存在);
178
+ · 打印时它照样命中 ⇒ SVG 描边在纸上用暗色值,而底色已被丢弃。
179
+
180
+ ⇒ 主题一律由令牌承担。没有令牌时用的是浅色兜底(#fff),
181
+ 这正是"没装令牌就别指望暗色"的应有行为。 */
@@ -182,7 +182,9 @@ var c = ChartAdapter.create(document.getElementById('chart'), {
182
182
  // ↑ 就这样。主题、y 轴从 0、字号、网格边距都已经合规了。</code></pre>
183
183
  </div>
184
184
  <p class="note">
185
- 换主题时调 <code>ChartAdapter.refresh(c)</code>,图表会跟着变
185
+ 换主题时调 <code>c = ChartAdapter.refresh(c)</code>,图表会跟着变 ——
186
+ <strong>它会返回一个新实例</strong>(ECharts 的主题在 init 时绑定,换主题必须重建),
187
+ 不接返回值就会拿到一个已 dispose 的旧实例
186
188
  (配合 <code>01-tokens/theme-toggle.js</code>)。
187
189
  </p>
188
190
  </div>
@@ -45,32 +45,53 @@
45
45
 
46
46
  /* ---------- 1. 从 CSS 变量取令牌 ---------- */
47
47
 
48
- /** 读一个 CSS 变量(读不到就返回 fallback)。 */
49
- function cssVar(name, fallback) {
48
+ /**
49
+ * 读一个 CSS 变量(读不到就返回 fallback)。
50
+ * @param {string} name
51
+ * @param {string} fallback
52
+ * @param {Element} [ctx] 作用域元素;默认根元素
53
+ *
54
+ * 🔴 为什么必须有 ctx
55
+ * 本库的主题是**容器级**的:`[data-theme="dark"]` 可以只罩住一块区域
56
+ * (tokens.css 里 `[data-theme="dark"]` 与 `:root[data-theme="dark"]` 并列)。
57
+ * 只读 `document.documentElement` 的话,暗容器里的图表会拿到**浅色**令牌:
58
+ * 实测 `--text-primary` = #15181c,而该容器 `--paper` = #14171A ⇒ **1.01:1**。
59
+ * 文字不是「不够清楚」,是根本看不见。
60
+ */
61
+ function cssVar(name, fallback, ctx) {
50
62
  if (typeof window.getComputedStyle !== 'function') return fallback;
51
- var v = getComputedStyle(document.documentElement).getPropertyValue(name);
63
+ var el = ctx || document.documentElement;
64
+ var v = getComputedStyle(el).getPropertyValue(name);
52
65
  return (v && v.trim()) || fallback;
53
66
  }
54
67
 
68
+ /** 图表该跟随谁的主题:最近的一个 `[data-theme]` 祖先,没有就是根。 */
69
+ function scopeOf(el) {
70
+ try {
71
+ if (el && el.closest) return el.closest('[data-theme]') || document.documentElement;
72
+ } catch (e) { /* 老浏览器 / 脱离文档的节点 */ }
73
+ return document.documentElement;
74
+ }
75
+
55
76
  /**
56
77
  * 拿到本库的调色板。
57
78
  * 🔴 全部有 fallback —— 万一页面没引 tokens.css 也能出图
58
79
  * (不因为缺令牌就整个图表画不出来)。
59
80
  */
60
- function palette() {
81
+ function palette(ctx) {
61
82
  return {
62
- text: cssVar('--text-primary', '#15181c'),
63
- textSub: cssVar('--text-secondary', '#5b6472'),
64
- textTertiary: cssVar('--text-tertiary', '#8a94a3'),
65
- surface: cssVar('--surface', '#ffffff'),
66
- paper: cssVar('--paper', '#f6f7f8'),
67
- border: cssVar('--border-decor', '#e2e5e9'),
68
- borderStrong: cssVar('--border-control', '#868b94'),
69
- danger: cssVar('--danger', '#c0392b'), // A 股:涨红
70
- success: cssVar('--success', '#2e7d52'), // 跌绿
71
- warn: cssVar('--warning', '#b8860b'),
72
- info: cssVar('--info', '#2b7a9e'),
73
- accent: cssVar('--accent', '#1b4d8f'),
83
+ text: cssVar('--text-primary', '#15181c', ctx),
84
+ textSub: cssVar('--text-secondary', '#5b6472', ctx),
85
+ textTertiary: cssVar('--text-tertiary', '#8a94a3', ctx),
86
+ surface: cssVar('--surface', '#ffffff', ctx),
87
+ paper: cssVar('--paper', '#f6f7f8', ctx),
88
+ border: cssVar('--border-decor', '#e2e5e9', ctx),
89
+ borderStrong: cssVar('--border-control', '#868b94', ctx),
90
+ danger: cssVar('--danger', '#c0392b', ctx), // A 股:涨红
91
+ success: cssVar('--success', '#2e7d52', ctx), // 跌绿
92
+ warn: cssVar('--warning', '#b8860b', ctx),
93
+ info: cssVar('--info', '#2b7a9e', ctx),
94
+ accent: cssVar('--accent', '#1b4d8f', ctx),
74
95
  };
75
96
  }
76
97
 
@@ -92,22 +113,62 @@
92
113
 
93
114
  /* ---------- 3. 主题对象(喂给 echarts.registerTheme) ---------- */
94
115
 
116
+ /** 把一个 CSS 颜色解析成 [r,g,b](认 #rgb / #rrggbb / rgb() / rgba())。 */
117
+ function parseColor(v) {
118
+ if (!v) return null;
119
+ var t = String(v).trim();
120
+ var m = /^#([0-9a-f]{3})$/i.exec(t);
121
+ if (m) {
122
+ return [parseInt(m[1][0] + m[1][0], 16),
123
+ parseInt(m[1][1] + m[1][1], 16),
124
+ parseInt(m[1][2] + m[1][2], 16)];
125
+ }
126
+ m = /^#([0-9a-f]{6})$/i.exec(t);
127
+ if (m) {
128
+ return [parseInt(m[1].slice(0, 2), 16),
129
+ parseInt(m[1].slice(2, 4), 16),
130
+ parseInt(m[1].slice(4, 6), 16)];
131
+ }
132
+ m = /^rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)/i.exec(t);
133
+ if (m) return [Math.round(+m[1]), Math.round(+m[2]), Math.round(+m[3])];
134
+ return null;
135
+ }
136
+
137
+ /**
138
+ * 这个作用域是不是暗色 —— 读它**实际解析出来**的 `--paper` 算相对亮度。
139
+ *
140
+ * 🔴 为什么不用 `data-theme` / `data-theme-current` 属性
141
+ * · `data-theme-current` 只由 theme-toggle.js 写在 `:root` 上;
142
+ * 容器级暗色永远不会设置它 ⇒ 属性判定在容器里恒为「浅色」。
143
+ * · 就算改用 `data-theme`,也只回答了「声明了什么」,没回答「算出什么色」。
144
+ * 令牌系统允许项目覆盖 `--paper`,只有量出来的亮度才是事实(同 I-10)。
145
+ */
146
+ function isDarkScope(ctx) {
147
+ var c = parseColor(cssVar('--paper', '', ctx));
148
+ if (!c) return false;
149
+ var lin = c.map(function (v) {
150
+ v = v / 255;
151
+ return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
152
+ });
153
+ // 相对亮度 < 0.18 判暗(#14171A ≈ 0.008,#f6f7f8 ≈ 0.90,中间没有歧义)
154
+ return (0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2]) < 0.18;
155
+ }
156
+
95
157
  /**
96
158
  * 生成 ECharts 主题。
97
159
  * @param {string} name 主题名
98
160
  * @param {object} p 调色板(默认自动取)
99
161
  */
100
- function theme(name, p) {
101
- p = p || palette();
102
- var isDark = document.documentElement.getAttribute('data-theme-current') === 'dark'
103
- || cssVar('--paper', '#fff') !== '#f6f7f8';
162
+ function theme(name, p, ctx) {
163
+ p = p || palette(ctx);
164
+ var isDark = isDarkScope(ctx);
104
165
 
105
166
  return {
106
167
  color: [p.accent, p.danger, p.success, p.warn, p.info],
107
168
  backgroundColor: 'transparent', // 交给页面的背景
108
169
  textStyle: {
109
170
  color: p.text,
110
- fontFamily: cssVar('--sans', 'system-ui, sans-serif'),
171
+ fontFamily: cssVar('--sans', 'system-ui, sans-serif', ctx),
111
172
  },
112
173
  title: {
113
174
  textStyle: { color: p.text, fontWeight: 600 },
@@ -115,7 +176,9 @@
115
176
  },
116
177
  legend: { textStyle: { color: p.textSub } },
117
178
  tooltip: {
118
- backgroundColor: isDark ? '#1c2024' : '#ffffff',
179
+ // 令牌即真值:暗档 --surface 实测 = #1C2024(与原来写死的值同色),
180
+ // 亮档 = #ffffff ⇒ 换成令牌后**像素零变化**,但能跟随容器作用域。
181
+ backgroundColor: cssVar('--surface', isDark ? '#1c2024' : '#ffffff', ctx),
119
182
  borderColor: p.border,
120
183
  borderWidth: 1,
121
184
  textStyle: { color: p.text, fontSize: 12 },
@@ -148,13 +211,13 @@
148
211
  * 一组**符合图表规范**的默认 option。
149
212
  * 覆盖 ECharts 的默认值 —— 让你不写就合规。
150
213
  */
151
- function defaults(option) {
152
- var p = palette();
214
+ function defaults(option, ctx) {
215
+ var p = palette(ctx);
153
216
  var o = option || {};
154
217
 
155
218
  // 🔴 数字格式:千分位 + 等宽
156
219
  o.textStyle = merge(o.textStyle, {
157
- fontFamily: cssVar('--mono', 'ui-monospace, monospace'),
220
+ fontFamily: cssVar('--mono', 'ui-monospace, monospace', ctx),
158
221
  });
159
222
 
160
223
  // 网格:留够左边距给 y 轴标签
@@ -214,9 +277,14 @@
214
277
  * ② setOption(option, themeName)
215
278
  * 不需要任何"先注册再重初始化"的 dance。
216
279
  */
217
- function currentThemeName() {
218
- return 'fe-' +
219
- (document.documentElement.getAttribute('data-theme-current') || 'light');
280
+ /**
281
+ * 主题名 = 调色板身份,必须**按作用域**取。
282
+ * 🔴 `registerTheme` 是全局按名字登记的:一个暗容器里的图和一个页外亮色的图
283
+ * 如果都叫 `fe-light`,后登记的那个会把前一个的配色顶掉 —— 同页两个图表
284
+ * 互相污染,而且看起来「某一块莫名其妙变亮了」。
285
+ */
286
+ function currentThemeName(ctx) {
287
+ return 'fe-' + (isDarkScope(ctx) ? 'dark' : 'light');
220
288
  }
221
289
 
222
290
  function create(dom, opt) {
@@ -227,16 +295,17 @@
227
295
  }
228
296
  return null;
229
297
  }
230
- var name = currentThemeName();
298
+ var ctx = scopeOf(dom);
299
+ var name = currentThemeName(ctx);
231
300
  // 🔴 每次都注册:换主题后令牌变了,得重新注册同名主题
232
301
  if (typeof window.echarts.registerTheme === 'function') {
233
- window.echarts.registerTheme(name, theme(name));
302
+ window.echarts.registerTheme(name, theme(name, null, ctx));
234
303
  }
235
304
  var inst = window.echarts.init(dom, name);
236
305
  inst.__feDom = dom; // refresh 要用
237
306
  inst.__feOpts = opt || {};
238
307
  inst.__feTheme = name;
239
- inst.setOption(defaults(inst.__feOpts));
308
+ inst.setOption(defaults(inst.__feOpts, ctx));
240
309
  return inst;
241
310
  }
242
311
 
@@ -254,14 +323,18 @@
254
323
  palette: palette,
255
324
  seriesColors: seriesColors,
256
325
  theme: theme,
326
+ /** 作用域对应的主题名(`fe-light` / `fe-dark`)。registerTheme 全局按名登记,
327
+ * 同页存在两种作用域时,调用方/判据必须能区分它们。 */
328
+ themeName: currentThemeName,
257
329
  defaults: defaults,
258
330
  create: create,
259
331
  /** 主题切换后重设(配合 theme-toggle.js 调用) */
260
332
  refresh: function (inst) {
261
333
  if (!inst) return;
262
- var name = currentThemeName();
334
+ var ctx = scopeOf(inst.__feDom);
335
+ var name = currentThemeName(ctx);
263
336
  if (typeof window.echarts !== 'undefined' && window.echarts.registerTheme) {
264
- window.echarts.registerTheme(name, theme(name));
337
+ window.echarts.registerTheme(name, theme(name, null, ctx));
265
338
  }
266
339
  // 🔴 主题名变了必须重建实例(ECharts 的主题在 init 时绑定)
267
340
  if (inst.__feDom) {
@@ -269,7 +342,7 @@
269
342
  inst.dispose();
270
343
  return create(inst.__feDom, opt);
271
344
  }
272
- inst.setOption(defaults(inst.__feOpts), name);
345
+ inst.setOption(defaults(inst.__feOpts, ctx), name);
273
346
  return inst;
274
347
  },
275
348
  };
@@ -88,7 +88,8 @@ http://127.0.0.1:8000/09-assets/scientific-plot/demo.html
88
88
 
89
89
  ## 已知限制
90
90
 
91
- - 未在 iOS / Android 真机 WebView 上验证(与库整体一致)。
91
+ - 未做过真机整页验收:图表在真实设备上的 DPR、字号、滚动表现都没有验过。
92
+ 能力探针那 7 行只测 CSS 能力,不覆盖画布渲染。
92
93
  - 刻度标签未做"相邻标签重叠时自动抽稀"(对数轴跨大数量级时已按 10 的幂简化)。
93
94
  - 无数据点级别的无障碍替代(图表整体有 `aria-label`,逐点值需调用方在页面上另给文本等价说明,
94
95
  demo.html 第 5 节演示了做法)。
@@ -67,13 +67,6 @@
67
67
  return v.toFixed(p);
68
68
  }
69
69
 
70
- function cssVar(name, fallback) {
71
- if (typeof getComputedStyle !== 'function' || !document.documentElement) return fallback;
72
- var v = getComputedStyle(document.documentElement).getPropertyValue(name);
73
- v = v ? v.trim() : '';
74
- return v || fallback;
75
- }
76
-
77
70
  /* --------------------------------------------------------------- 比例尺 */
78
71
 
79
72
  /**
package/AGENTS.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # agentcss — for coding agents
2
2
 
3
- - **Contract (machine-readable):** `ai/components.json` — 40 components with root class, slots, variants, states, ARIA and file paths. Query it; never guess class names.
3
+ - **Contract (machine-readable):** `ai/components.json` — 40 components with root class, slots, variants, states, ARIA, file paths, and **`printPolicy`** (what happens to this component on paper: `omit` / `expand` / `reveal` / `keep-together` / `allow-break`, plus a `pagination` flag). Query it; never guess class names — and never assume a component prints as it looks on screen.
4
4
  - **Known failure modes:** `ai/pitfalls.json`, or `npx agentcss ai pitfalls --json --component=<id>`.
5
+ - **Print / PDF:** `npx agentcss ai pdf <page.html> --out page.pdf [--paper=a4|letter|a5] [--margin="18mm 15mm"] [--max-pages=N]`. `@page` belongs to the exporter, never to the library. To hold a document to paper standards use `ai check <doc.json> --medium=paper` — it then requires `pageBudget` and refuses `pagination:"server"` components.
5
6
  - **Query:** `npx agentcss ai components --json [--maturity=stable]` · `ai tokens --json [--set=dark]` · `ai tree --json`.
6
7
  - **Validate:** `npx agentcss ai validate <file> --schema=components --json`. Every subcommand accepts `--json`; failures print `{"ok":false,…}` and exit non-zero.
7
8
  - **Markup ground truth:** each component's `demo.html`. Restyle through tokens (CSS variables); never rewrite component selectors.