agentcss 0.11.16 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,93 @@
1
+ # metric · 指标块
2
+
3
+ 报表里**“一个数”的最小完整单元**:一个标签、一个值、一句上下文。
4
+
5
+ ```html
6
+ <link rel="stylesheet" href="02-primitives/metric/metric.css">
7
+
8
+ <dl class="metric">
9
+ <dt class="metric__label">营业总收入</dt>
10
+ <dd class="metric__value">1,294.06<span class="metric__unit">亿元</span></dd>
11
+ <dd class="metric__foot">
12
+ <span class="delta delta--up">12.40%</span>
13
+ <span class="metric__note">同比</span>
14
+ </dd>
15
+ </dl>
16
+ ```
17
+
18
+ ## 类
19
+
20
+ | 类 | 作用 |
21
+ |---|---|
22
+ | `.metric` | 基础块(推荐 `<dl>`,用 `<div>` 也能渲染) |
23
+ | `.metric__label` | 标签:12px、次要色 |
24
+ | `.metric__value` | 值:等宽字族(`--mono`)+ 默认不换行(装不下才断) |
25
+ | `.metric__unit` | 单位:与数字同基线,小一号 |
26
+ | `.metric__foot` | 脚下那行:通常放 `delta` + 说明词 |
27
+ | `.metric__note` | 说明词(“同比” / “较上季”) |
28
+ | `.metric__spark` | 迷你走势槽位,放不放都行 |
29
+ | `.metric--lg` | 主指标档 |
30
+ | `.metric--sm` | 紧凑档(密集网格) |
31
+ | `.metric--null` | 缺数档:值降为 tertiary、去字重 |
32
+ | `.metric-grid` | KPI 行的自适应网格 |
33
+
34
+ ## 三条硬规矩
35
+
36
+ 1. **缺数写 `—`(U+2014)并说明原因**,不填 0、不留空、不只把字变淡。
37
+ 2. **单位放 `metric__unit`**,不要拼进值字符串。
38
+ 3. **变化量放 `metric__foot`**,不要混进 `metric__value`。
39
+
40
+ ## 反例
41
+
42
+ - ❌ **缺数填 0** —— `0.00` 与 `—` 在报表上是两个不同结论。填 0 会让读者把「未披露」读成「现金流为零」,而它其实可能只是没出报表
43
+ - ❌ **把单位拼进值字符串** —— `1,294.06亿元` 让单位跟着 30px 字号走、和数字一样重,读者第一眼会把“亿元”当成数值的一部分;而且没法单选单位调样式
44
+ - ❌ **把 `delta` 塞进 `metric__value`** —— 值里混进变化量之后等宽对齐失效(两段数字位数不同),读屏也会念成一串。变化量属于 `metric__foot`
45
+ - ❌ **用 `opacity` 表达缺数** —— `--text-tertiary` 的 5.33:1 已经是文字色地板,再乘一次透明度就跌破 4.5。缺数靠“不给数字”表达,不靠让它看不清
46
+ - ❌ **把 `metric` 当卡片用** —— 它没有边框、没有内边距、不承载操作区。要“一块带边框的内容”用 `card`;`metric` 只负责一个数
47
+
48
+ ## 为什么推荐 `<dl>`
49
+
50
+ “标签—值”在 HTML 里有现成语义:术语 + 描述。读屏会念
51
+ 「营业总收入 1,294.06 亿元 +12.40% 同比」;用 `<div>` 只会念一串没有关系的词。
52
+ CSS 只认类名,所以 `<div>` 也能渲染(兼容既有页面),但文档一律示范 `<dl>`。
53
+
54
+ ## 网格
55
+
56
+ ```html
57
+ <div class="metric-grid">
58
+ <dl class="metric"> … </dl>
59
+ <dl class="metric"> … </dl>
60
+ </div>
61
+ ```
62
+
63
+ 内部是 `repeat(auto-fit, minmax(min(100%, 12rem), 1fr))`。
64
+ 那层 `min()` 不是冗余:窄容器下 12rem 会比可用宽度还大,没有它就会横向溢出
65
+ (`05-audit/metric-check.js` 判据 ⑧ 在 375px 视口下直接量 `scrollWidth`)。
66
+
67
+ ## 缺数是一等状态
68
+
69
+ ```html
70
+ <dl class="metric metric--null">
71
+ <dt class="metric__label">经营活动现金流净额</dt>
72
+ <dd class="metric__value">—</dd>
73
+ <dd class="metric__foot"><span class="metric__note">本期未披露</span></dd>
74
+ </dl>
75
+ ```
76
+
77
+ `metric--null` 只把值的颜色降到 tertiary、去掉字重,**不隐藏这一块** ——
78
+ 版面塌掉比显示“没有”更糟。这是本库从 `04-recipes/detail-page` 的 null 变体
79
+ 继承下来的同一条纪律。
80
+
81
+ ## 对本组件不做的事
82
+
83
+ - **不做数值格式化**。千分位、小数位、单位换算由数据侧决定;CSS 不知道
84
+ `1,294.06` 与 `1294.06` 哪个对。库保证的是等宽对齐与不换行。
85
+ - **不判断方向**。涨跌颜色交给 `delta`,本组件不含任何方向令牌引用
86
+ (`metric-check` 判据 ⑦ 静态检查这件事)。
87
+
88
+ ## 预览
89
+
90
+ ```bash
91
+ python -m http.server 8000
92
+ # http://127.0.0.1:8000/02-primitives/metric/demo.html
93
+ ```
@@ -0,0 +1,176 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <link rel="icon" href="data:,">
7
+ <title>metric · 指标块</title>
8
+ <link rel="stylesheet" href="../../01-tokens/tokens.css">
9
+ <link rel="stylesheet" href="../../01-tokens/typography.css">
10
+ <link rel="stylesheet" href="../../01-tokens/page.css">
11
+ <link rel="stylesheet" href="../../02-primitives/metric/metric.css">
12
+ <link rel="stylesheet" href="../../02-primitives/delta/delta.css">
13
+ <link rel="stylesheet" href="../../09-assets/sparkline/sparkline.css">
14
+ <style>
15
+ .pad { padding: var(--sp-4) 0; }
16
+ .frame { border: 1px solid var(--border-decor); border-radius: var(--r-md);
17
+ padding: var(--sp-5); background: var(--surface); }
18
+ </style>
19
+ </head>
20
+ <body class="page page">
21
+ <main class="wrap">
22
+ <h1>metric · 指标块</h1>
23
+ <p class="lede">
24
+ 报表里<strong>“一个数”的最小完整单元</strong>:一个标签、一个值、一句上下文。
25
+ KPI 行、详情页顶部、监控面板,都由它排出来。
26
+ </p>
27
+
28
+ <h2 class="rule-top">一个完整的 metric</h2>
29
+ <div class="frame">
30
+ <dl class="metric" style="max-width: 18rem">
31
+ <dt class="metric__label">营业总收入</dt>
32
+ <dd class="metric__value">1,294.06<span class="metric__unit">亿元</span></dd>
33
+ <dd class="metric__foot">
34
+ <span class="delta delta--up">12.40%</span>
35
+ <span class="metric__note">同比</span>
36
+ </dd>
37
+ </dl>
38
+ </div>
39
+ <p class="small">
40
+ 四件必须一起做对的事:<strong>数字等宽</strong>(上下两行数位对齐,竖着扫能比大小)、
41
+ <strong>单位分离成槽位</strong>(拼进字符串就没法单独调样式,也没法让数字右对齐时把单位甩在外面)、
42
+ <strong>缺数是一等状态</strong>(见下)、<strong>方向交给 delta</strong>(本组件不管颜色)。
43
+ <br>
44
+ 结构用 <code>&lt;dl&gt;</code> 是因为“标签—值”在 HTML 里有现成语义:
45
+ 读屏会念「营业总收入 1,294.06 亿元 +12.40% 同比」。用 <code>&lt;div&gt;</code>
46
+ 只会念一串没有关系的词(CSS 只认类名,用 div 也能渲染,但不推荐)。
47
+ </p>
48
+
49
+ <h2 class="rule-top">一行四个 KPI</h2>
50
+ <div class="metric-grid">
51
+ <dl class="metric">
52
+ <dt class="metric__label">营业总收入</dt>
53
+ <dd class="metric__value">1,294.06<span class="metric__unit">亿元</span></dd>
54
+ <dd class="metric__foot"><span class="delta delta--up">12.40%</span><span class="metric__note">同比</span></dd>
55
+ </dl>
56
+ <dl class="metric">
57
+ <dt class="metric__label">归母净利润</dt>
58
+ <dd class="metric__value">318.72<span class="metric__unit">亿元</span></dd>
59
+ <dd class="metric__foot"><span class="delta delta--up">8.60%</span><span class="metric__note">同比</span></dd>
60
+ </dl>
61
+ <dl class="metric">
62
+ <dt class="metric__label">销售毛利率</dt>
63
+ <dd class="metric__value">41.28<span class="metric__unit">%</span></dd>
64
+ <dd class="metric__foot"><span class="delta delta--down">1.34%</span><span class="metric__note">同比</span></dd>
65
+ </dl>
66
+ <dl class="metric metric--null">
67
+ <dt class="metric__label">经营活动现金流净额</dt>
68
+ <dd class="metric__value">—</dd>
69
+ <dd class="metric__foot"><span class="metric__note">本期未披露</span></dd>
70
+ </dl>
71
+ </div>
72
+ <p class="small">
73
+ 第四块是<strong>缺数</strong>:值写 <code>—</code>(U+2014),并在脚下说明原因。
74
+ 它<strong>不隐藏、不变灰到看不见、也不填 0</strong> —— 版面塌掉比显示“没有”更糟,
75
+ 而填 0 是伪造数据:读者会把“未披露”读成“现金流为零”。
76
+ <br>
77
+ 网格用 <code>metric-grid</code>,它内部是
78
+ <code>minmax(min(100%, 12rem), 1fr)</code> —— 那层 <code>min()</code> 不是冗余:
79
+ 窄容器下 12rem 会比可用宽度还大,没有它就会横向溢出。
80
+ </p>
81
+
82
+ <h2 class="rule-top">三档字号</h2>
83
+ <div class="frame">
84
+ <div class="metric-grid">
85
+ <dl class="metric metric--lg">
86
+ <dt class="metric__label">主指标(lg)</dt>
87
+ <dd class="metric__value">318.72<span class="metric__unit">亿元</span></dd>
88
+ </dl>
89
+ <dl class="metric">
90
+ <dt class="metric__label">默认</dt>
91
+ <dd class="metric__value">318.72<span class="metric__unit">亿元</span></dd>
92
+ </dl>
93
+ <dl class="metric metric--sm">
94
+ <dt class="metric__label">紧凑档(sm)</dt>
95
+ <dd class="metric__value">318.72<span class="metric__unit">亿元</span></dd>
96
+ </dl>
97
+ </div>
98
+ </div>
99
+ <p class="small">
100
+ 三档够了。出现第四档说明 KPI 行的层级没规划好(与 <code>badge</code> / <code>card</code> 同一判断标准)。
101
+ </p>
102
+
103
+ <h2 class="rule-top">迷你走势槽位</h2>
104
+ <div class="frame">
105
+ <div class="metric-grid">
106
+ <dl class="metric">
107
+ <dt class="metric__label">近九个交易日收盘</dt>
108
+ <dd class="metric__value">1,684.00</dd>
109
+ <dd class="metric__foot"><span class="delta delta--up">5.12%</span><span class="metric__note">五日</span></dd>
110
+ <dd class="metric__spark">
111
+ <span class="sparkline" data-spark="1602,1618,1609,1631,1625,1648,1662,1655,1684"></span>
112
+ </dd>
113
+ </dl>
114
+ <dl class="metric">
115
+ <dt class="metric__label">近九个交易日成交量</dt>
116
+ <dd class="metric__value">3.42<span class="metric__unit">亿手</span></dd>
117
+ <dd class="metric__foot"><span class="delta delta--down">2.08%</span><span class="metric__note">五日</span></dd>
118
+ <dd class="metric__spark">
119
+ <span class="sparkline" data-spark="3.9,3.7,3.8,3.6,3.5,3.55,3.48,3.5,3.42"></span>
120
+ </dd>
121
+ </dl>
122
+ </div>
123
+ </div>
124
+ <p class="small">
125
+ <code>metric__spark</code> 只是一个<strong>槽位</strong> —— 它不关心里面放什么。
126
+ 上面放的是 <code>sparkline</code>,也可以什么都不放(前三块就没有)。
127
+ 槽位存在但为空时不留空白高度,所以同一个网格里有没有走势图都能对齐。
128
+ </p>
129
+
130
+ <h2 class="rule-top">反例:缺数填 0</h2>
131
+ <div class="cmp">
132
+ <div class="cmp__pane cmp__bad" data-bad=".metric__value|glyphs|=4">
133
+ <h3>错:拿 0 冒充“没有”</h3>
134
+ <dl class="metric">
135
+ <dt class="metric__label">经营活动现金流净额</dt>
136
+ <dd class="metric__value">0.00</dd>
137
+ <dd class="metric__foot"><span class="metric__note">本期未披露</span></dd>
138
+ </dl>
139
+ <p class="small">
140
+ 值有 4 个字形。读者读到的是“现金流为零”—— 一个具体的、错误的结论。
141
+ </p>
142
+ </div>
143
+ <div class="cmp__pane cmp__good" data-good=".metric__value|glyphs|=1">
144
+ <h3>对:留 <code>—</code> 并说明原因</h3>
145
+ <dl class="metric metric--null">
146
+ <dt class="metric__label">经营活动现金流净额</dt>
147
+ <dd class="metric__value">—</dd>
148
+ <dd class="metric__foot"><span class="metric__note">本期未披露</span></dd>
149
+ </dl>
150
+ <p class="small">
151
+ 值只有 1 个字形(U+2014),且 <code>metric--null</code> 把颜色降到 tertiary。
152
+ </p>
153
+ </div>
154
+ </div>
155
+ <p class="small">
156
+ <strong>为什么不能靠“变淡”表达缺数</strong>:颜色淡了但字还在,读者仍会读那个数字。
157
+ 缺数的正确表达是<strong>不给出数字</strong>,再加一句为什么。
158
+ <br>
159
+ ⚠️ 也不要用 <code>opacity</code> 压淡 —— <code>--text-tertiary</code> 的 5.33:1
160
+ 已经是地板,再乘一次透明度就跌破 4.5。改令牌才是对的做法。
161
+ </p>
162
+
163
+ <h2 class="rule-top">两条边界</h2>
164
+ <p class="small">
165
+ ① <strong>本组件不做格式化</strong>。千分位、小数位、单位换算都由数据侧决定 ——
166
+ CSS 不知道“1,294.06”和“1294.06”哪个对。库能保证的是
167
+ <strong>等宽对齐</strong>与<strong>不换行</strong>。
168
+ <br>
169
+ ② <strong>不要把 delta 写进 <code>metric__value</code></strong>。值里混进变化量之后,
170
+ 等宽对齐就失效了(数字位数不同),而且读屏会念成一串。
171
+ 变化量放 <code>metric__foot</code>。
172
+ </p>
173
+ </main>
174
+ <script src="../../09-assets/sparkline/sparkline.js"></script>
175
+ </body>
176
+ </html>
@@ -0,0 +1,157 @@
1
+ /* ============================================================================
2
+ Metric — 指标块(一个标签 + 一个值 + 一句上下文)
3
+ ============================================================================
4
+ 一句话:**报表里“一个数”的最小完整单元**。
5
+ 财务页、监控页、详情页顶部的 KPI 行,都是由它排出来的。
6
+
7
+ 🔴 为什么它是组件,而不是几个工具类
8
+ ----------------------------------------------------------------------------
9
+ 因为“一个数”有四件事必须一起做对,少一件就会在真实报表里出错:
10
+
11
+ ① 数字要**等宽**(用等宽字族,不靠比例字体的默认)—— 否则上下两行的数位对不齐,
12
+ 竖着扫一眼比大小这件事就废了;这是「财务表格」与「装饰表格」的分界
13
+ ② 单位要**分离**成槽位 —— 拼进字符串后("1,294.06亿元")既不能单独调样式,
14
+ 也没法让数字右对齐时把单位甩在外面
15
+ ③ **缺数是一等状态** —— 不能渲染成 0、不能留空、不能只把字变淡;
16
+ 必须给出“为什么没有”(本库的既有纪律,见 04-recipes/detail-page 的 null 变体)
17
+ ④ 变化的**方向**要能表达 —— 所以它给 delta 留了位置,但它自己不管颜色
18
+
19
+ 这四件事里任意一件交给调用方“记得做”,就会漏。本库在同一条路上栽过:
20
+ `table__unit` 的括号是组件加的,不是要求每个调用方记得加。
21
+
22
+ 🔴 为什么推荐 `<dl>`
23
+ ----------------------------------------------------------------------------
24
+ “标签—值”在 HTML 里有现成语义:术语 + 描述。用 `<dl>` 读屏会念
25
+ 「营业总收入 1,294.06 亿元 +12.4% 同比」,用 `<div>` 只会念一串没有关系的词。
26
+
27
+ 本文件的 CSS **只认类名,不认标签** —— 用 `<div>` 也能正常渲染
28
+ (兼容既有页面),但 demo 与文档一律示范 `<dl>`。
29
+
30
+ 🔴 缺数怎么写
31
+ ----------------------------------------------------------------------------
32
+ <dl class="metric metric--null">
33
+ <dt class="metric__label">经营现金流</dt>
34
+ <dd class="metric__value">—</dd> <!-- U+2014,不是 0、不是空 -->
35
+ <dd class="metric__foot"><span class="metric__note">本期未披露</span></dd>
36
+ </dl>
37
+
38
+ `metric--null` 只做一件事:把值的颜色降到 tertiary、去掉字重。
39
+ 它**不隐藏**这块 —— 版面塌掉比显示“没有”更糟。
40
+ ========================================================================= */
41
+
42
+ .metric {
43
+ display: flex;
44
+ flex-direction: column;
45
+ gap: var(--sp-1);
46
+ margin: 0; /* dl 自带外边距,会破坏网格对齐 */
47
+ min-width: 0; /* flex/grid 子项默认不缩,长标签会撑破列 */
48
+ }
49
+
50
+ /* dl 的 dd 默认有 40px 起始缩进 —— 只在结构与类名同时出现时清掉 */
51
+ .metric > dd {
52
+ margin-inline-start: 0;
53
+ }
54
+
55
+ .metric__label {
56
+ margin: 0;
57
+ font-size: var(--fs-xs);
58
+ font-weight: 500;
59
+ line-height: 1.5;
60
+ color: var(--text-secondary);
61
+ letter-spacing: var(--tracking-normal); /* 小字号中文给一点字距更清;令牌在暗色段有另一档 */
62
+ }
63
+
64
+ /* 值:这一块的全部意义所在 */
65
+ .metric__value {
66
+ margin: 0;
67
+ font-family: var(--mono); /* 等宽字族 ⇒ 数字天然对齐,不需要 tabular-nums */
68
+ font-size: var(--fs-xl);
69
+ font-weight: 600;
70
+ line-height: var(--lh-tight);
71
+ color: var(--text-primary);
72
+ /* 🔴 数字**默认不换行**,但极端情况下必须能断,否则顶破视口。
73
+ 第一版写的是 `white-space: nowrap`,RTL 下阿语膨胀 1.3× 后
74
+ 整个值变成一串装不下的长词,横向溢出 212px(实测)。
75
+ `overflow-wrap: anywhere` 的语义正好是「只有当一个词装不下整行时才断」——
76
+ 正常列宽下 `1,294.06` 绝不会被拆开,只有真的放不下才断。 */
77
+ white-space: normal;
78
+ overflow-wrap: anywhere;
79
+ /* 值 + 单位按 flex 排,**允许整体换行**(单位可以整块掉到下一行)。 */
80
+ display: flex;
81
+ flex-wrap: wrap;
82
+ align-items: baseline;
83
+ }
84
+
85
+ /* 单位:与数字同基线、小一号、次要色
86
+ ⚠️ 用 margin-inline-start 而不是空格字符:
87
+ 空格在等宽字族里也占一格,而 4 位数 × 十几行就会看出错位。 */
88
+ .metric__unit {
89
+ margin-inline-start: 0.3em;
90
+ font-size: var(--fs-sm);
91
+ font-weight: 400;
92
+ color: var(--text-secondary);
93
+ /* 🔴 单位**允许换行**。第一版给了 `nowrap`,结果 RTL 下阿语膨胀 1.3×
94
+ 直接顶破列宽(实测溢出 379px):`nowrap` 会把单位变成列的**最小内容宽度**,
95
+ 网格再想收也收不了。
96
+ `overflow-wrap: anywhere` 是给「一个词长过整列」的兜底 ——
97
+ 阿/希的单位词本来就比中文长,不加它还是会溢出。 */
98
+ white-space: normal;
99
+ overflow-wrap: anywhere;
100
+ min-width: 0; /* flex 子项默认不缩 —— 不写这句上面的换行等于没写 */
101
+ }
102
+
103
+ /* 脚下那行:通常放一个 delta + 一个说明词("同比" / "较上季")*/
104
+ .metric__foot {
105
+ margin: 0;
106
+ display: flex;
107
+ align-items: baseline;
108
+ flex-wrap: wrap;
109
+ gap: var(--sp-2);
110
+ font-size: var(--fs-xs);
111
+ line-height: 1.5;
112
+ color: var(--text-tertiary);
113
+ }
114
+
115
+ .metric__note {
116
+ color: var(--text-tertiary);
117
+ }
118
+
119
+ /* 迷你图槽位:放 sparkline 之类。给它一点上间距,避免贴着数字 */
120
+ .metric__spark {
121
+ margin-block-start: var(--sp-1);
122
+ color: var(--text-tertiary);
123
+ }
124
+
125
+ /* 缺数:值降到 tertiary、去字重,但**仍然占位**
126
+ ⚠️ 不要用 opacity —— 它会让对比度跌破 4.5(--text-tertiary 已经是 5.33 的地板,
127
+ 再乘一次透明度就没了)。改令牌才是对的做法。 */
128
+ .metric--null .metric__value {
129
+ color: var(--text-tertiary);
130
+ font-weight: 400;
131
+ }
132
+
133
+ /* ---------------------------------------------------------------- 字号三档
134
+ 三档够了。出现第四档说明 KPI 行的层级没规划好
135
+ (与 badge / card 的判断标准一致)。 */
136
+ .metric--lg .metric__value {
137
+ font-size: var(--fs-2xl);
138
+ }
139
+
140
+ .metric--sm .metric__value {
141
+ font-size: var(--fs-md);
142
+ font-weight: 500;
143
+ }
144
+
145
+ /* ---------------------------------------------------------------- 网格容器
146
+ KPI 行几乎总是成排出现,把重复的栅格收进来一次。
147
+ ⚠️ `minmax(min(100%, 12rem), 1fr)` 里的 `min(100%, …)` 不是冗余:
148
+ 窄容器(手机单列、侧栏)下 12rem 会比可用宽度还大,
149
+ 没有这层 min() 就会横向溢出 —— 这是 auto-fit 最常见的翻车方式。 */
150
+ .metric-grid {
151
+ display: grid;
152
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
153
+ gap: var(--sp-5) var(--sp-5);
154
+ margin: 0;
155
+ padding: 0;
156
+ list-style: none; /* 用 <ul> 包 metric 时不想看到圆点 */
157
+ }
@@ -438,10 +438,16 @@
438
438
  box.addEventListener('keydown', function (e) {
439
439
  if (e.key === 'Escape') {
440
440
  e.preventDefault();
441
- /* 🔴 必须 stopPropagation:弹层可嵌套。
442
- * 不拦住的话,内层的 Esc 会**冒泡到外层**,
443
- * 一次按键把两层全关掉 —— 键盘用户会以为应用崩了。
444
- * (本库在 popover 上踩过同一个坑,这里一并修好。) */
441
+ /* ⚠️ 防御性 stopPropagation —— 保留,但**当前结构下它不改变任何行为**。
442
+ * 等价突变体实测:删掉这一行,一次 Esc 仍然只关最内层。
443
+ * 原因:两个 `.dialog-backdrop` 是 `body` 的**兄弟**节点,外层 box 不是
444
+ * 内层的祖先,所以内层按钮的 keydown 冒泡路径是
445
+ * button → .dialog → .dialog-backdrop → body
446
+ * —— 根本到不了外层的 `.dialog`(实测 `outer.contains(inner) === false`)。
447
+ * 原注释写「不拦住会一次关两层」,那是**假设了 DOM 嵌套**,与实现不符。
448
+ * 保留理由:若将来改成「内层放进外层 box」的真嵌套,这行就必需。
449
+ * 判据 `pattern-behavior-check.js ⑬` 验证的是**行为**(只关最内层),
450
+ * 它抓不到删除本行的突变 —— 那是等价突变体,不是判据失效。 */
445
451
  e.stopPropagation();
446
452
  close();
447
453
  return;
@@ -102,7 +102,7 @@ python -m http.server 8000
102
102
  # http://127.0.0.1:8000/04-recipes/table/demo.html
103
103
  ```
104
104
 
105
- ## 配色:与本库 `list` 的约定**不同**(已知,未统一)
105
+ ## 配色:与本库 `list` 的约定**不同**(故意的,不是笔误)
106
106
 
107
107
  | 组件 | 类名 | 约定 |
108
108
  |---|---|---|
@@ -114,14 +114,49 @@ python -m http.server 8000
114
114
  - 表格里多是**量**(营收、毛利率、净利润)—— 负数是"红字",会计惯例;
115
115
  - 列表里多是**涨跌**(涨跌幅、日变动)—— A 股惯例是涨红跌绿。
116
116
 
117
- ⚠️ **真正的麻烦在这里**:把「涨跌幅」这一列塞进 `table` 时,两种约定会打架 ——
118
- 负的涨跌幅被渲染成红色,读者第一眼读成"涨"。目前**没有解**:
117
+ ### 涨跌幅放进表格:解题方式(0.12.1 已定)
119
118
 
120
- - 加一套 `num--up` / `num--down`(市场约定)是候选,但那会让 `table` 有四组颜色类;
121
- - 或者由调用方显式指定语义(`data-polarity="market"`),但那要改契约。
119
+ 把「涨跌幅」这一列塞进 `table` 时,两种约定会打架 ——
120
+ 负的涨跌幅被渲染成红色,读者第一眼读成"涨"。这个问题曾经挂在 README 里写「目前没有解」,
121
+ 现在有了,而且**不是**当初列的两个候选之一:
122
122
 
123
- 在决定之前,这一条就写在这儿 —— 一个库里有两种配色约定而不说明,
124
- 比约定本身选错更糟。
123
+ > **市场涨跌不进 `table` 的颜色体系,而是嵌 `delta` 原子。**
124
+
125
+ ```html
126
+ <td class="num"><span class="delta delta--up">3.21%</span></td>
127
+ <td class="num"><span class="delta delta--down">1.05%</span></td>
128
+ <td class="num"><span class="delta delta--flat">0.00%</span></td>
129
+ ```
130
+
131
+ | | `num--pos` / `num--neg` | 嵌 `delta` |
132
+ |---|---|---|
133
+ | 语义 | 会计正负(多了 / 少了) | 市场涨跌(涨了 / 跌了) |
134
+ | 颜色来源 | `--success` / `--danger` | `--dir-up` / `--dir-down` |
135
+ | 随 `[data-market]` 切换 | **不切** | **切** |
136
+ | 符号 | `::before` 的 `+` / `−` | `delta` 自己的 `+` / `−` / 无 |
137
+
138
+ 为什么否掉当初那两个候选:
139
+
140
+ - `num--up` / `num--down` 会让 `table` 有**四组**颜色类,调用方每次都要记"这列用哪组";
141
+ - `data-polarity="market"` 是把语义塞进一个属性,表格得替数据判断"这列是涨跌",
142
+ 而它并不知道 —— 调用方才知道。
143
+
144
+ ⇒ 方向是**独立语义**,所以它就该是独立原子。表格只负责对齐(`td.num`),不负责判色。
145
+
146
+ ⚠️ **不要两套一起用**:`<td class="num num--pos"><span class="delta delta--up">`
147
+ 会让同一格同时受两套颜色支配,谁赢取决于选择器权重 —— 那是最难查的一类 bug。
148
+ `table-check` 的判据 ⑬ 会挡住给 `table.css` 添 `num--up` / `num--down` 的做法。
149
+
150
+ ### 零值:两边都不许有符号(0.12.1 修,E145)
151
+
152
+ `format: 'signed-percent'` 的正号由类名触发,而旧实现判的是 `v < 0` ⇒
153
+ `v === 0` 落进 else 分支得到 `num--pos`,渲染成「**+0.0%**」——
154
+ 一个没变化的数被念成"涨了"。四舍五入处同样复现:`0.0004` 在 1 位精度下也印成「+0.0%」。
155
+
156
+ ⇒ 规则只有一条:**符号必须跟打印出来的数字同号**。
157
+ 先算出将要打印的数值,再按它定符号;等于 0 既不加 `+` 也不加 `−`
158
+ (这与 `delta--flat` 是同一个契约)。`table-check` 判据 ⑫ 守着它,
159
+ 并且要求页面上**必须存在**零值样本 —— 删掉零值行不能算通过。
125
160
 
126
161
 
127
162
  ## 反例
@@ -137,5 +172,8 @@ python -m http.server 8000
137
172
  - ❌ **粘性表头配 `border-collapse: collapse`** —— `separate` 是 sticky 生效的前提,
138
173
  用 collapse 表头不会粘住
139
174
  - ❌ **拿它当虚拟滚动表格用** —— 本组件不做虚拟滚动;超过 200 行请配分页
175
+ - ❌ **给表格再加一套「市场涨跌」的颜色类** —— 那是第三组方向类,
176
+ 加上会计的正负就是四组;市场涨跌嵌 `delta`(判据 ⑬ 会红)
177
+ - ❌ **零值带 `+`** —— 「+0.0%」= 把持平念成上涨;先算打印值再定符号(判据 ⑫ 会红)
140
178
 
141
179
  ---
@@ -9,7 +9,11 @@
9
9
  <link rel="stylesheet" href="../../01-tokens/typography.css">
10
10
  <link rel="stylesheet" href="../../01-tokens/focus-ring.css">
11
11
  <link rel="stylesheet" href="../../01-tokens/page.css">
12
+ <!-- 市场口径切换:只有引了它,[data-market="intl"] 才生效。
13
+ 不引则属性被完全忽略(可选文件,见 01-tokens/market.css 的文件头)。 -->
14
+ <link rel="stylesheet" href="../../01-tokens/market.css">
12
15
  <link rel="stylesheet" href="../../02-primitives/button/button.css">
16
+ <link rel="stylesheet" href="../../02-primitives/delta/delta.css">
13
17
  <link rel="stylesheet" href="table.css">
14
18
  <style>
15
19
  .toolbar { display: flex; gap: var(--sp-2); margin: 0 0 var(--sp-3); flex-wrap: wrap; }
@@ -92,6 +96,30 @@
92
96
  </p>
93
97
  <div id="escproof"></div>
94
98
 
99
+ <!-- ========================================================================
100
+ 两种方向语义:会计正负 vs 市场涨跌(0.12.1)
101
+ ===================================================================== -->
102
+ <h2 class="rule-top">6 · 同一张表里的两种方向语义</h2>
103
+ <p class="note">
104
+ 「同比 +16.3%」和「近 20 日 +3.21%」看起来都是"涨",但它们是<strong>两件事</strong>:
105
+ 前者是会计口径(正负就是加减),后者是市场口径(红涨绿跌,国际口径正好相反)。
106
+ <br>⇒ 会计正负用 <code>num--pos</code> / <code>num--neg</code>;
107
+ 市场涨跌用嵌进单元格的 <code>delta</code>,
108
+ <strong>不给表格加第三组方向类</strong>(<code>num--up</code> / <code>num--down</code> 故意不存在)。
109
+ <br>下面两块数据完全相同,只有外层 <code>data-market</code> 不同 ——
110
+ <strong>会计那一列一个字都不变,市场那一列整体翻转</strong>。
111
+ <br>⚠️ 在同一张表里,会计的「+16.3%」是绿、市场的「+3.21%」是红 ——
112
+ 看起来互相矛盾,<strong>这恰恰是它们必须分开命名的原因</strong>:
113
+ 一个说"多了多少",一个说"涨了多少",共用一个颜色令牌就会给出矛盾信号。
114
+ </p>
115
+ <div id="polarity"></div>
116
+ <p class="table__foot">
117
+ 注:<strong>零值两边都不带符号</strong> —— 会计列不写 <code>+</code>,
118
+ 市场列用 <code>delta--flat</code>。
119
+ 「0.0%」被读成「涨了 0.0%」是本库明确反对的一类错误
120
+ (四舍五入到 0 也算 0,所以 0.0004 在 1 位精度下同样不带符号)。
121
+ </p>
122
+
95
123
 
96
124
 
97
125
  <h2 class="rule-bottom">对比:错写法 vs 本库写法</h2>
@@ -187,17 +215,29 @@
187
215
  }
188
216
  if (f === 'text') return { text: String(v), cls: '' };
189
217
 
190
- var abs = Math.abs(Number(v));
218
+ var n = Number(v);
219
+ var abs = Math.abs(n);
220
+ var isPct = (f === 'percent' || f === 'signed-percent');
191
221
  var text;
192
222
  if (f === 'currency' || f === 'integer') text = grouped(abs, col.precision);
193
- else if (f === 'percent' || f === 'signed-percent') text = fixed(abs * 100, col.precision) + '%';
223
+ else if (isPct) text = fixed(abs * 100, col.precision) + '%';
194
224
  else text = fixed(abs, col.precision);
195
225
 
196
226
  /* 符号:负数总是有 −(数值语义 + 无障碍第二线索);
197
- 正数只有 signed-* 才带 + —— 这就是"按列语义处理" */
227
+ 正数只有 signed-* 才带 + —— 这就是"按列语义处理"。
228
+
229
+ 🔴 但符号必须**跟打印出来的数字同号**(0.12.1 修,E145):
230
+ 旧实现判的是 `v < 0`,于是 `v === 0` 掉进 else 分支拿到 `num--pos`,
231
+ 渲染成「+0.0%」—— 一个没变化的数被念成"涨了"。
232
+ 同一条错还会在**四舍五入**处复现:0.0004 在 1 位精度下打印成
233
+ `0.0%`,若仍按原始值判正负,同样得到「+0.0%」。
234
+ ⇒ 唯一的正确判法:先算出**将要打印的数值**,再按它定符号。
235
+ `= 0` 既不加 + 也不加 −(与 delta 的 `delta--flat` 同一个契约)。 */
236
+ var shown = Number(fixed(isPct ? abs * 100 : abs, col.precision));
198
237
  var cls = 'num';
199
- if (Number(v) < 0) cls += ' num--neg';
200
- else if (f === 'signed-percent' || f === 'signed-decimal') cls += ' num--pos';
238
+ if (shown !== 0 && n < 0) cls += ' num--neg';
239
+ else if (shown !== 0 && n > 0 &&
240
+ (f === 'signed-percent' || f === 'signed-decimal')) cls += ' num--pos';
201
241
 
202
242
  return { text: text, cls: cls };
203
243
  }
@@ -488,6 +528,53 @@
488
528
  '<p class="table__foot">复查方式:用浏览器看这一节的单元格 —— ' +
489
529
  '第二、三列应当<b>原样显示</b>尖括号文本,而不是把它们变成样式。</p>';
490
530
  document.getElementById('escproof').innerHTML = e2;
531
+
532
+ /* --------------------------------------- 预设六:两种方向语义(0.12.1) */
533
+
534
+ /* 市场涨跌:方向交给 delta,表格自己不判色。
535
+ 🔴 为什么不给 table 加 `num--up` / `num--down`:
536
+ 那会让表格有四组颜色类(pos / neg / up / down),调用方每次都要记
537
+ 「这一列用哪组」。方向该由**数据语义**决定,
538
+ 不该由「它被放进了表格」决定 —— 所以它是独立原子,不是表格的变体。
539
+
540
+ 🔴 符号规则与 cell() 完全一致:按**打印出来的数字**判方向。
541
+ 四舍五入到 0 的一律算持平,否则 0.00004 会印成「+0.00%」——
542
+ 同一类错误在会计列与市场列各犯一次,就等于没修。 */
543
+ function deltaCell(v) {
544
+ var n = Number(v);
545
+ var shown = Number(fixed(Math.abs(n) * 100, 2));
546
+ var dir = shown === 0 ? 'flat' : (n > 0 ? 'up' : 'down');
547
+ return '<span class="delta delta--' + dir + '">' +
548
+ esc(fixed(Math.abs(n) * 100, 2)) + '%</span>';
549
+ }
550
+
551
+ var POL_DATA = [
552
+ { name: '营业收入', acct: 0.163, mkt: 0.0321 },
553
+ { name: '营业成本', acct: -0.008, mkt: -0.0105 },
554
+ { name: '其他收益', acct: 0.000, mkt: 0.0000 } /* 零值:两边都不许出符号 */
555
+ ];
556
+
557
+ function polarityTable() {
558
+ var h = '<div class="table"><table>' +
559
+ '<caption class="table__cap">同一份数据 · 会计口径不随市场切换,市场口径整体翻转</caption>' +
560
+ '<thead><tr><th scope="col">科目</th>' +
561
+ '<th scope="col">同比<span class="table__unit">(会计)</span></th>' +
562
+ '<th scope="col">涨跌<span class="table__unit">(市场)</span></th>' +
563
+ '</tr></thead><tbody>';
564
+ POL_DATA.forEach(function (r) {
565
+ var a = cell(r.acct, { format: 'signed-percent', precision: 1 });
566
+ h += '<tr><th scope="row">' + esc(r.name) + '</th>' +
567
+ '<td class="' + a.cls + '">' + esc(a.text) + '</td>' +
568
+ '<td class="num">' + deltaCell(r.mkt) + '</td></tr>';
569
+ });
570
+ return h + '</tbody></table></div>';
571
+ }
572
+
573
+ document.getElementById('polarity').innerHTML =
574
+ '<p class="note">A 股口径 <code>[data-market="cn"]</code>(默认,红涨绿跌)</p>' +
575
+ '<div data-market="cn">' + polarityTable() + '</div>' +
576
+ '<p class="note">国际口径 <code>[data-market="intl"]</code> —— 只有市场那一列翻转</p>' +
577
+ '<div data-market="intl">' + polarityTable() + '</div>';
491
578
  })();
492
579
  </script>
493
580
  </body>