agentcss 0.11.16 → 0.12.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.
@@ -170,12 +170,11 @@ node ai/cli.js tree [--set=light|dark] [--cat=color] # 第二种
170
170
  ## 库里没引用、但承诺给你用的令牌
171
171
 
172
172
  下面这些令牌**本库的组件一个都没用**(`token-audience-gate.py` 会一直盯着这个数:
173
- 现在 5 个,只能降不能升)。它们不是死代码 —— 是**阶梯的档位**和**给脚本读的常量**,
173
+ 现在 3 个,只能降不能升)。它们不是死代码 —— 是**阶梯的档位**和**给脚本读的常量**,
174
174
  你写自己的页面时可以直接引用。
175
175
 
176
176
  | 令牌 | 值 | 为什么库里没人用 |
177
177
  |---|---|---|
178
- | `--tracking-normal` | `0` | 字距阶梯的"常规"档:它是**默认值**,写了等于没写 |
179
178
  | `--bp-sm` / `--bp-lg` / `--bp-xl` | 见上表 | 断点**物理上不可能**被 `var()` 引用(进不了 media query 条件) |
180
179
 
181
180
  > 2026-10-10(第二次):新增的 `04-recipes/report` 在本层内把边注重定义为 14em,
@@ -188,6 +187,10 @@ node ai/cli.js tree [--set=light|dark] [--cat=color] # 第二种
188
187
  > `04-recipes/data-showcase`(KPI 数值字号)开始引用它们 ⇒ 它们**不再**是孤儿。
189
188
  > 按双向校验规则从名单移除,棘轮数 7 → 5(棘轮只能降,本次是降)。
190
189
  >
190
+ > 2026-10-29:`02-primitives/metric` 的标签用上了字距阶梯的"常规"档
191
+ > (12px 中文给一点字距更清)⇒ 它不再是无引用项,从名单移除,棘轮 4 → 3。
192
+ > 这是**降**,不是放水 —— 门禁自己会复核"名单里的令牌是不是真的没人引用"。
193
+ >
191
194
  > ⚠️ 这一节里的**任何** `--` 开头字符串都会被 `token-audience-gate.py` 当作名单项,
192
195
  > 所以上面这句只能用中文描述、不能直接写令牌名。
193
196
 
@@ -223,9 +226,57 @@ node ai/cli.js tree [--set=light|dark] [--cat=color] # 第二种
223
226
  **不会报错但也不会生效**(被 `.page` 前缀的定义以更高特异性静默盖掉,
224
227
  成为死声明)⇒ `05-audit/shell-gate.py` 会拦住它。
225
228
 
229
+ ## 涨跌口径 `market.css`(选配,只在用 `delta` 时需要)
230
+
231
+ `tokens.css` 里的 `--dir-up` / `--dir-down` 是**A 股口径**(红涨绿跌)。
232
+ 要支持国际口径(绿涨红跌),**再引一个文件**:
233
+
234
+ ```html
235
+ <link rel="stylesheet" href="01-tokens/tokens.css">
236
+ <link rel="stylesheet" href="01-tokens/market.css"> <!-- 必须在 tokens.css 之后 -->
237
+ ```
238
+
239
+ 然后在任意祖先上加属性,该范围内整体对调:
240
+
241
+ ```html
242
+ <body data-market="intl">
243
+ <div data-market="intl">…</div> <!-- 也可以只切局部 -->
244
+ ```
245
+
246
+ | 引了 `market.css` | 不引 |
247
+ |---|---|
248
+ | `[data-market="intl"]` 生效 | `data-market` 被**完全忽略**,永远是 A 股口径 |
249
+
250
+ 交换只改**令牌的指向**(`--dir-up: var(--raw-dir-down)`),组件 CSS 里零分支 ——
251
+ `delta` 只写 `color: var(--dir-up)`,两栏 HTML 逐字相同。
252
+
253
+ ⚠️ 三条硬要求(都有判据挡着,见 `05-audit/delta-check.js`):
254
+
255
+ 1. **必须排在 `tokens.css` 之后** —— 同权重靠顺序定胜负。
256
+ 2. **不许写进 `tokens.css`** —— 那会让六个靠词法读 CSS 的工具静默读错
257
+ (`contrast` / `gen-ai-contract` / `gen-tokens-tree` / `token-audience` /
258
+ `theme-sync`)。真实事故见 `internal/ERRORS.md` 的 E137。
259
+ 3. 选择器是 `:root[data-market="intl"]` (0,2,0),**不是** `:where(...)` (0) ——
260
+ `:where()` 抢不过 `:root` 的 (0,1,0),写了等于没写(E135)。
261
+
262
+ > 精简档(`tierA-tokens.css`)**不含**这段交换:它是一份 `<style>` 内的
263
+ > 令牌快照,没有配套文件可引。要国际口径就自己补一条规则 ——
264
+ > 注意精简档**丢掉了 L1 原语**(`--raw-dir-*` 不存在,值已内联),所以不能
265
+ > `var(--raw-dir-down)` 互换,只能写直接色值:
266
+ >
267
+ > ```css
268
+ > [data-market="intl"] { --dir-up: #2f6b3a; --dir-down: #c0392b; }
269
+ > @media (prefers-color-scheme: dark) {
270
+ > [data-market="intl"] { --dir-up: #6FAA7A; --dir-down: #E07A6E; }
271
+ > }
272
+ > ```
273
+ >
274
+ > 这四条色值是**重复**的,所以要跟着 `tokens.css` 一起改 —— 这是精简档的
275
+ > 固有代价(它本来就是"抄一份进去"的形态)。
276
+
226
277
  ## 精简档 的使用方式
227
278
 
228
- 复制 **`04-recipes/tierA-tokens.css`** 进 `<style>`(**5.1 KB**,含浅色 102 + 暗色 33 个令牌,零外链零构建)。
279
+ 复制 **`04-recipes/tierA-tokens.css`** 进 `<style>`(**5.2 KB**,含浅色 104 + 暗色 35 个令牌,零外链零构建)。
229
280
 
230
281
  那一份是由 `tokens.css` **自动抽取**的(`python 05-audit/make-tiersnippet.py`),
231
282
  所以不会与本文件漂移;改了这个文件请重新生成。
@@ -0,0 +1,48 @@
1
+ /* ============================================================================
2
+ market.css — 涨跌口径切换(可选文件)
3
+ ============================================================================
4
+ 引了它:`[data-market="intl"]` 会把涨跌配色整体对调(绿涨红跌)。
5
+ 不引它:永远是 A 股口径(红涨绿跌),`data-market` 属性会被**完全忽略**。
6
+
7
+ ⇒ 它是一档**可选增强**,不是核心令牌的一部分。默认交付不带国际口径。
8
+
9
+ ============================================================================
10
+ 🔴 为什么它必须是**单独一个文件**,不能写进 tokens.css
11
+ ----------------------------------------------------------------------------
12
+ 本库有五六个工具(contrast / gen-ai-contract / gen-tokens-tree /
13
+ token-audience / theme-sync)都靠 `05-audit/_css.py` 的
14
+ `declared_tokens()` 读 tokens.css。那个函数是**纯词法**的:
15
+
16
+ 遍历所有 `--x: y;`,后出现的覆盖先出现的 —— 不看选择器。
17
+
18
+ 这是刻意的(它要能处理任意 CSS 片段),但它有一条隐含契约:
19
+ **tokens.css 里只能有「无条件声明」和「暗色媒体段」**。
20
+ 暗色段之所以安全,是因为消费方知道「浅色截断到 @media 之前、暗色读整份」。
21
+
22
+ 第一版我把口径交换写进了 tokens.css 的末尾。后果不是报错,是**静默读错**:
23
+ · `contrast.py` 的报告里「暗色 涨」显示成了绿色(它把交换当默认值叠上去了);
24
+ · `gen-ai-contract --check` 报「现有 #E07A6E 应为 #6FAA7A」,看起来像随机错位。
25
+ 而真实浏览器里的值是**对的**(`dir-gate` 读 computed color 实测
26
+ 暗色 up = #E07A6E)。⇒ 门禁在冤枉代码,比漏判更难查。
27
+
28
+ ============================================================================
29
+ 🔴 为什么选择器不能用 `:where([data-market="intl"])`
30
+ ----------------------------------------------------------------------------
31
+ `:where()` 的特异性是 0,而 `:root` 是 (0,1,0)。两者都匹配 `<html>`,
32
+ **`:root` 赢** —— 交换会被它完全盖掉(实测:写了等于没写)。
33
+ 特异性压得越低,越抢不过 `:root`。
34
+
35
+ 所以写两个选择器,各管一件事:
36
+ `:root[data-market="intl"]` (0,2,0) —— 压过 `:root`,管整页口径
37
+ `[data-market="intl"]` (0,1,0) —— 管局部容器(那里没有 `:root` 竞争)
38
+ 使用者在更深元素上写 `--dir-up` 仍能覆盖,因为**继承来的值权重最低**。
39
+
40
+ ⚠️ 这个文件**必须**在 tokens.css 之后引入(它要覆盖令牌)。
41
+ 放在 `<link>` 顺序后面即可,不需要更特殊的写法。
42
+ ========================================================================= */
43
+
44
+ :root[data-market="intl"],
45
+ [data-market="intl"] {
46
+ --dir-up: var(--raw-dir-down);
47
+ --dir-down: var(--raw-dir-up);
48
+ }
@@ -54,6 +54,11 @@
54
54
  --raw-success-bg: #eaf2e6;
55
55
  --raw-warning-bg: #fbf1dd;
56
56
  --raw-info-bg: #e8f1f8;
57
+ /* 方向色(涨跌)。当前取值与 danger / success 相同 —— 但那是**巧合**,
58
+ 不是同一件事,所以单独命名(理由见语义层那一段)。
59
+ 浅色:红 4.89–5.44 · 绿 5.75–5.96,四组底全部 AA。 */
60
+ --raw-dir-up: #c0392b;
61
+ --raw-dir-down: #2f6b3a;
57
62
  --raw-accent: #1b4d8f;
58
63
  --raw-switch-on: #1b4d8f;
59
64
  --raw-accent-hover: #163a6d;
@@ -195,6 +200,45 @@
195
200
  --warning-bg: var(--raw-warning-bg);
196
201
  --info-bg: var(--raw-info-bg);
197
202
 
203
+ /* ---------------------------------------------------------------- 方向色
204
+ 🔴 为什么方向要独立成令牌,而不是复用 --danger / --success
205
+ ----------------------------------------------------------------
206
+ 这两组读起来都是"红/绿",但它们是**两件事**:
207
+
208
+ --danger / --success = 好恶(错误/成功)。这是**评价**。
209
+ --dir-up / --dir-down = 涨跌(方向)。这是**事实**。
210
+
211
+ 把两件事共用一个名字,会在这个场景里立刻暴露:
212
+ 「研发费用同比 +38%」—— 是"涨",但那是**坏消息**。
213
+ 用 --danger 表示它,颜色说着"错了",语义说着"涨了",读者收到矛盾信号。
214
+
215
+ 所以方向色单独命名,取值可以与语义色相同(当前就相同),
216
+ 但**允许将来分开调**:金融终端普遍用比 --danger 更饱和的红。
217
+
218
+ 🔴 口径可整体切换(不要在每个组件里判断)
219
+ ----------------------------------------------------------------
220
+ 默认是 A 股 / 东亚口径:**红涨绿跌**。
221
+ 给 `<html data-market="intl">`(或任意祖先)就切成国际口径:绿涨红跌。
222
+
223
+ 实现方式与暗色同构 —— **换掉底层值,语义名不动**:
224
+ --dir-up 永远表示"涨",组件的 CSS 只写 `color: var(--dir-up)`。
225
+ 口径切换只改这两条令牌的指向,组件一行都不用改。
226
+ 组件里若出现 `[data-market]` 分支,那是把全局约定抄进了组件(判据会拦)。
227
+
228
+ 实测对比度(05-audit/contrast.py,四组底各算一遍):
229
+ 浅色:红 4.89–5.44 · 绿 5.75–5.96 (全部 AA)
230
+ 暗色:红 5.60–6.15 · 绿 6.01–6.60 (全部 AA)
231
+ 最差的一格是红点在 --surface-sunken 上 4.89 —— 仍 ≥ 4.5。
232
+ ---------------------------------------------------------------- */
233
+ --dir-up: var(--raw-dir-up); /* 涨:默认红 */
234
+ --dir-down: var(--raw-dir-down); /* 跌:默认绿 */
235
+
236
+ /* ⚠️ 口径切换的规则在 `:root` **外面**(见本块结束后的「国际口径」)。
237
+ 第一版写在里面 —— `:root { }` 只能装声明,不能装规则块。
238
+ theme-sync.py 按「声明序列」解析这个块,遇到嵌套规则会把选择器切碎,
239
+ 生成一段无效 CSS(实测:`:where(...)` 被截成 `where(...)`、第二条声明丢失)。
240
+ 这类嵌套即使浏览器支持 CSS Nesting 也不该写:本库要兼容 Chromium 97。 */
241
+
198
242
  /* ---------------------------------------------------------------- 强调色
199
243
  🔴 一源原则:一个项目只选一个 accent,不要混用。
200
244
  accent 决定"可点击"的语义,混用等于取消了这层语义。
@@ -642,6 +686,10 @@
642
686
  --raw-danger-bg: #341D1A;
643
687
  --raw-success: #6FAA7A;
644
688
  --raw-success-bg: #16241A;
689
+ /* 方向色(暗色):与 success / danger 同值,理由见浅色段。
690
+ 暗色下红 5.60–6.15 · 绿 6.01–6.60,四组底全部 AA。 */
691
+ --raw-dir-up: #E07A6E;
692
+ --raw-dir-down: #6FAA7A;
645
693
  --raw-warning: #C99A3F;
646
694
  /* 🔴 最终定的(第二次修正):
647
695
  「本身**淡黄色的那些框**你就改成**暖灰色**,
@@ -696,6 +744,8 @@
696
744
  --raw-danger: #c0392b;
697
745
  --raw-danger-bg: #fdeeec;
698
746
  --raw-danger-hover: #8f1e17;
747
+ --raw-dir-down: #2f6b3a;
748
+ --raw-dir-up: #c0392b;
699
749
  --raw-info: #1d5f8a;
700
750
  --raw-info-bg: #e8f1f8;
701
751
  --raw-paper: #f6f7f8;
@@ -730,6 +780,8 @@
730
780
  --danger: var(--raw-danger);
731
781
  --danger-bg: var(--raw-danger-bg);
732
782
  --danger-hover: var(--raw-danger-hover);
783
+ --dir-down: var(--raw-dir-down);
784
+ --dir-up: var(--raw-dir-up);
733
785
  --info: var(--raw-info);
734
786
  --info-bg: var(--raw-info-bg);
735
787
  --paper: var(--raw-paper);
@@ -769,6 +821,8 @@
769
821
  --raw-danger: #E07A6E;
770
822
  --raw-danger-bg: #341D1A;
771
823
  --raw-danger-hover: #F09A90;
824
+ --raw-dir-down: #6FAA7A;
825
+ --raw-dir-up: #E07A6E;
772
826
  --raw-info: #5B9BC4;
773
827
  --raw-info-bg: #13242F;
774
828
  --raw-paper: #14171A;
@@ -803,6 +857,8 @@
803
857
  --danger: var(--raw-danger);
804
858
  --danger-bg: var(--raw-danger-bg);
805
859
  --danger-hover: var(--raw-danger-hover);
860
+ --dir-down: var(--raw-dir-down);
861
+ --dir-up: var(--raw-dir-up);
806
862
  --info: var(--raw-info);
807
863
  --info-bg: var(--raw-info-bg);
808
864
  --paper: var(--raw-paper);
@@ -830,6 +886,16 @@
830
886
  }
831
887
  /* === END: dark-via-attr === */
832
888
 
889
+ /* 🔴 涨跌口径切换(`[data-market="intl"]`)**不在本文件里**,在 `01-tokens/market.css`。
890
+ ----------------------------------------------------------------------------
891
+ 原因不是洁癖:读本文件的那几个工具(contrast / gen-ai-contract /
892
+ gen-tokens-tree / token-audience)走 `_css.declared_tokens()`,它是**纯词法**的
893
+ —— 遍历所有声明、后者覆盖前者、**不看选择器**。
894
+ 把一段**条件性**的交换规则放在这里,它们会把它当成默认值叠上去,
895
+ 于是报告里「暗色 涨」显示成绿色,而浏览器里其实是对的 —— 门禁冤枉代码,
896
+ 比漏判更难查(实测,见 E137)。
897
+ ⇒ 本文件只放「无条件声明」与「暗色媒体段」,条件规则一律外置。
898
+ ========================================================================= */
833
899
 
834
900
  /* ============================================================================
835
901
  字体栈令牌
@@ -0,0 +1,92 @@
1
+ # delta · 变化量
2
+
3
+ 一个数相对某个基准**变了多少、往哪边变**。不判断好坏。
4
+
5
+ ```html
6
+ <link rel="stylesheet" href="02-primitives/delta/delta.css">
7
+
8
+ <span class="delta delta--up">3.21%</span> <!-- 渲染 +3.21%,红 -->
9
+ <span class="delta delta--down">1.05%</span> <!-- 渲染 −1.05%,绿 -->
10
+ <span class="delta delta--flat">0.00%</span> <!-- 无符号,中性色 -->
11
+ ```
12
+
13
+ ## 类
14
+
15
+ | 类 | 作用 |
16
+ |---|---|
17
+ | `.delta` | 基础:等宽字族(`--mono`)+ 不换行 |
18
+ | `.delta--up` | 涨:默认红(口径见下) |
19
+ | `.delta--down` | 跌:默认绿 |
20
+ | `.delta--flat` | 持平:中性色,**不带符号** |
21
+ | `.delta--arrow` | 把符号换成 ▲ / ▼(与符号档互斥) |
22
+ | `.delta--sm` | 紧凑档,用于表格行内 |
23
+
24
+ ## 三条硬规矩
25
+
26
+ 1. **HTML 里只写数值本体**,不写 `+` / `−`。符号由 CSS 生成,手写就是两个符号。
27
+ 2. **方向由你给,组件不推断**。本组件没有 JS、不读数值 —— 类名写错就颜色错,且不报错。
28
+ 3. **值为 0 用 `--flat`**,塞进 `--up` / `--down` 等于伪造方向。
29
+
30
+ ## 反例
31
+
32
+ - ❌ **HTML 里手写符号** —— 数据侧照着习惯写 `−1.05%`,组件再补一个减号,渲染成 `−−1.05%`;页面不崩、控制台不响,只是报表看起来像坏了。demo 里有两栏真实对照(字形 7 对 6),`05-audit/demo-claim-gate` 逐栏量
33
+ - ❌ **拿它表达好坏** —— `delta--up` 说的是方向,不是评价。「研发费用 +38%」确实是涨,但那是成本失控;要表达成功/失败/警告用 `badge` 的语义档
34
+ - ❌ **把涨跌幅塞进表格的 `num--pos`** —— 那是**会计约定**(正 = 绿),套在涨跌幅上正好与 A 股习惯相反。表格里的涨跌幅列用 `delta`,不要用 `num--pos`
35
+ - ❌ **方向靠猜** —— 本组件不读数值、也不推断正负。类名写错就颜色错,而且不报错;值为 0 必须用 `delta--flat`,塞进 `--up` 等于伪造方向
36
+ - ❌ **`delta--arrow` 与符号档同时加** —— 两者抢同一个 `::before`。同时加不会双显,但说明你以为它们能叠加,按这个理解去写别的组合就会踩空
37
+
38
+ ## 口径:红涨绿跌是缺省,不是唯一
39
+
40
+ 默认 A 股口径。给任意祖先加 `data-market="intl"` 切成国际口径(绿涨红跌):
41
+
42
+ ```html
43
+ <html data-market="intl">
44
+ <!-- 或只切一块 -->
45
+ <section data-market="intl"> … </section>
46
+ ```
47
+
48
+ 实现方式与暗色同构:`--dir-up` 永远表示“涨”,口径切换只改**令牌的指向**,
49
+ 组件 CSS 里没有任何 `[data-market]` 分支。两栏 HTML 逐字相同,只差那个属性。
50
+
51
+ ## 与另外两组颜色的分工
52
+
53
+ | 想表达 | 用什么 | 语义 |
54
+ |---|---|---|
55
+ | 状态(已停牌 / 审计中) | `badge` 的 `badge--success` 等 | 一个词,没有数值 |
56
+ | 会计正负(营收 -3.2 亿) | `table` 的 `num--pos` / `num--neg` | 正 = 绿,负 = 红(红字) |
57
+ | 变化方向(同比 +12.4%) | `delta` 的 `delta--up` / `--down` | 只描述往哪边变 |
58
+
59
+ 三者不可互换 —— 用错不会报错,只会让报表在某个具体场景里说出相反的结论。
60
+
61
+ ## 读屏拿到的是什么
62
+
63
+ `+` / `−` 由 CSS 生成,所以它们**不在**元素的 `textContent` 里。
64
+ 但实测(Chromium + CDP 可访问性树):它们是**独立的 StaticText 节点**,
65
+ 读屏会按顺序念「加」「3.21%」。也就是说方向对读屏用户是可用的,不是只靠颜色。
66
+
67
+ ⚠️ 这个机制很容易被改坏:把 `content` 换成空串、换成背景图、或让伪元素
68
+ `visibility: hidden`,视觉上可能仍有符号,可访问性树里就没了。
69
+ `05-audit/delta-check.js` 的判据 ⑦ 直接数十字树里的 `+` / `−` 节点(破坏实验证明它会红)。
70
+
71
+ ## 实测对比度
72
+
73
+ `05-audit/contrast.py`,四组底(纸底 / 表面 / 内凹 / 卡片)各算一遍:
74
+
75
+ | 令牌 | 浅色 | 暗色 |
76
+ |---|---|---|
77
+ | `--dir-up`(红) | 4.89 – 5.44 | 5.60 – 6.15 |
78
+ | `--dir-down`(绿) | 5.75 – 5.96 | 6.01 – 6.60 |
79
+
80
+ 全部 ≥ 4.5(AA)。最差的一格是红点在 `--surface-sunken` 上 4.89,12px 字号下仍达标。
81
+
82
+ ## 为什么强制高对比模式不用额外处理
83
+
84
+ `+` / `−` 与配色无关,强制色抹掉红绿之后符号仍在表达方向 —— 1.4.1 要的
85
+ “信息不能只由颜色承载”本来就成立。这里**故意没写** `@media (forced-colors: active)` 块。
86
+
87
+ ## 预览
88
+
89
+ ```bash
90
+ python -m http.server 8000
91
+ # http://127.0.0.1:8000/02-primitives/delta/demo.html
92
+ ```
@@ -0,0 +1,113 @@
1
+ /* ============================================================================
2
+ Delta — 变化量(涨跌 / 增减)
3
+ ============================================================================
4
+ 一句话:**一个数相对某个基准变了多少,带方向**。
5
+
6
+ 🔴 它与 badge、table 的 num--pos/neg 分工在哪
7
+ ----------------------------------------------------------------------------
8
+ · badge 说的是**状态**(已停牌 / 审计中)—— 一个词,没有数值
9
+ · num--pos/neg 说的是**会计正负**(营收 -3.2 亿是"红字")—— 好恶由会计决定
10
+ · delta 说的是**变化方向**(同比 +12.4%)—— 只描述往哪边变了
11
+
12
+ 这不是同一件事的三种叫法。最刺眼的例子:
13
+ 「研发费用 +38%」用 num--pos 渲染成绿 = 说"好消息",
14
+ 而它其实是成本失控。颜色对了语义错了,读者不会怀疑颜色,只会怀疑自己的判断。
15
+
16
+ 🔴 为什么符号由 CSS 生成,而不是写在 HTML 里
17
+ ----------------------------------------------------------------------------
18
+ 与 `table .num--pos::before` 同一理由:让"漏写符号"在结构上不可能发生。
19
+ demo 里留着一组对照 —— 手写 `−1.05%` 再叠 CSS 的 `−`,渲染成 `−−1.05%`。
20
+ 这类错误不会报错、不会崩,只会让报表看起来像坏了。
21
+
22
+ ⇒ 约定:**HTML 里写数值本体(`3.21%`),符号交给本组件。**
23
+
24
+ 🔴 为什么不能只靠颜色(WCAG 1.4.1)
25
+ ----------------------------------------------------------------------------
26
+ 红绿是**冗余线索**,不是唯一线索。默认的 `+` / `−` 就是第二线索;
27
+ 需要更强方向感时加 `.delta--arrow`,把符号换成 ▲ / ▼。
28
+ ⚠️ 箭头是字符伪元素,读屏可能把它念成字符名。要严格可控,
29
+ 请给外层加 `aria-label`(如「同比上涨 12.4%」),或不用这一档。
30
+
31
+ 🔴 方向由**你**给,不由数值推断
32
+ ----------------------------------------------------------------------------
33
+ 本组件没有 JS,也不会读数值。类名写错就颜色错,且不报错。
34
+ ⇒ 数据侧务必按值选类;值恰好为 0 时用 `.delta--flat`,不要塞进 up/down。
35
+
36
+ 口径:默认红涨绿跌(A 股)。`[data-market="intl"]` 下整体对调,
37
+ 本文件**不做任何判断** —— 它只写 `var(--dir-up)`,切换发生在令牌层。
38
+
39
+ 实测对比度(05-audit/contrast.py):
40
+ 浅色 up 4.89–5.44 / down 5.75–5.96 · 暗色 up 5.60–6.15 / down 6.01–6.60
41
+ 全部 ≥ 4.5(AA),12px 小字号下也成立。
42
+ ========================================================================= */
43
+
44
+ .delta {
45
+ display: inline-flex;
46
+ align-items: baseline; /* 与相邻数字基线对齐,不用 center(会错半像素)*/
47
+ gap: 0.15em; /* 符号与数字之间:随字号走,不用 --sp(那是布局间距)*/
48
+ font-family: var(--mono);
49
+ /* 🔴 这里**故意不声明**「等宽数字」那个特性(`tabular-nums`)。
50
+ 等宽字族每个字形同宽(含 `.` 与 `,`),数字天然对齐;
51
+ 那个特性只对**比例字族**有意义,在等宽字族上是无效动作。
52
+ 库里的 `.num` / `.num-lg` 是为「比例字体里的数字」准备的统一手段,
53
+ 而 delta 已经用 `--mono` 从字族层面解决了 —— 再声明一次就是两套机制。 */
54
+ white-space: nowrap; /* 「+12.4%」不许断成两行 */
55
+ font-weight: 500; /* 比正文重半档:它是结论,不是叙述 */
56
+ line-height: 1.4;
57
+ }
58
+
59
+ /* 符号槽:默认正负号。放在 ::before 而不是 ::after —— 符号是"前缀",
60
+ 且 ::before 在 bidi 环境下会跟着方向走(RTL 里自然跑到右边)。 */
61
+ .delta::before {
62
+ font-weight: 400; /* 符号别跟着加粗,否则像"两个 +" */
63
+ font-size: 0.95em;
64
+ }
65
+
66
+ .delta--up {
67
+ color: var(--dir-up);
68
+ }
69
+ .delta--up::before {
70
+ content: "+";
71
+ }
72
+
73
+ .delta--down {
74
+ color: var(--dir-down);
75
+ }
76
+ .delta--down::before {
77
+ content: "\2212"; /* U+2212 真减号。连字符比它窄一半,混排会歪 */
78
+ }
79
+
80
+ /* 持平:无方向 ⇒ 无符号、无彩色。
81
+ ⚠️ 这里**故意不生成符号**:0 本身没有方向,给个 ± 是把"误差"的概念借过来。
82
+ 它不违反 1.4.1,因为中性色本来就不承载"涨/跌"这个信息。 */
83
+ .delta--flat {
84
+ color: var(--text-secondary);
85
+ }
86
+ .delta--flat::before {
87
+ content: none;
88
+ }
89
+
90
+ /* 方向强化档:把符号换成箭头。
91
+ ⚠️ 与符号档**互斥**(同一个 ::before),不要同时加。 */
92
+ .delta--arrow.delta--up::before {
93
+ content: "\25B2"; /* ▲ */
94
+ font-size: 0.8em; /* 三角在同样字号下显得过大 */
95
+ }
96
+ .delta--arrow.delta--down::before {
97
+ content: "\25BC"; /* ▼ */
98
+ font-size: 0.8em;
99
+ }
100
+
101
+ /* 表格行内、密集列表里的紧凑档 */
102
+ .delta--sm {
103
+ font-size: var(--fs-xs);
104
+ font-weight: 400;
105
+ }
106
+
107
+ /* 🔴 这里**故意没有** `@media (forced-colors: active)` 块
108
+ ----------------------------------------------------------------------------
109
+ 第一版写了一个(给 up/down 加下划线,理由是"强制色下红绿都变系统色")。
110
+ 写完发现自己绕回来了:强制色确实会抹掉红绿,但 1.4.1 要的是
111
+ "**信息不能只由颜色承载**" —— 而 `+` / `−` 一直都在,与配色无关。
112
+ ⇒ 颜色被抹掉时,符号仍在表达方向,本来就不需要补偿。
113
+ 加下划线反而更糟:它会让数字看起来像链接。 */
@@ -0,0 +1,162 @@
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>delta · 变化量</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
+ <!-- 口径切换是可选文件;本页有 intl 对照栏,所以必须引它 -->
12
+ <link rel="stylesheet" href="../../01-tokens/market.css">
13
+ <link rel="stylesheet" href="../../02-primitives/delta/delta.css">
14
+ <style>
15
+ .row { display: flex; gap: var(--sp-4); flex-wrap: wrap; align-items: baseline;
16
+ padding: var(--sp-3) 0; }
17
+ .line { display: flex; justify-content: space-between; align-items: baseline;
18
+ gap: var(--sp-3); padding: var(--sp-2) 0;
19
+ border-bottom: 1px solid var(--border-decor); }
20
+ .line:last-child { border-bottom: 0; }
21
+ .line__k { color: var(--text-secondary); }
22
+ /* 口径对照:两栏**都是对的**,只是两种约定 —— 所以不用 .cmp(那是对错对照,
23
+ demo-claim 会要求两栏断言互斥,而这里没有"错"的那一栏)。 */
24
+ .pair { display: grid; gap: var(--sp-4); align-items: start;
25
+ padding: var(--sp-4) 0; }
26
+ @media (min-width: 40rem) { .pair { grid-template-columns: 1fr 1fr; } }
27
+ .pair__pane { padding: var(--sp-4); border: 1px solid var(--border-decor);
28
+ border-radius: var(--r-md); background: var(--surface); }
29
+ .pair__pane > h3 { margin: 0 0 var(--sp-2); font-size: var(--fs-sm);
30
+ color: var(--text-secondary); }
31
+ </style>
32
+ </head>
33
+ <body class="page page">
34
+ <main class="wrap">
35
+ <h1>delta · 变化量</h1>
36
+ <p class="lede">
37
+ delta 只回答一件事:<strong>这个数相对基准往哪边变了、变了多少</strong>。
38
+ 它<strong>不判断好坏</strong> —— 同一个“涨”,对营收是好消息,对费用是坏消息,
39
+ 那是读者的判断,不是颜色的判断。
40
+ </p>
41
+
42
+ <h2 class="rule-top">三个方向</h2>
43
+ <div class="row">
44
+ <span class="delta delta--up">3.21%</span>
45
+ <span class="delta delta--down">1.05%</span>
46
+ <span class="delta delta--flat">0.00%</span>
47
+ <span class="delta delta--sm delta--up">0.42%</span>
48
+ </div>
49
+ <p class="small">
50
+ 最后一个是紧凑档(<code>delta--sm</code>),用于表格行内与密集列表。
51
+ <br>
52
+ <strong>符号是 CSS 生成的</strong>:HTML 里只写 <code>3.21%</code>,
53
+ 渲染成 <code>+3.21%</code>;写 <code>1.05%</code> 配 <code>delta--down</code>,
54
+ 渲染成 <code>−1.05%</code>(减号是 U+2212,不是连字符 —— 连字符窄一半,混排会歪)。
55
+ 持平刻意<strong>不带符号</strong>:0 没有方向,给它一个 ± 是把“误差”的概念借了过来。
56
+ </p>
57
+
58
+ <h2 class="rule-top">符号为什么由 CSS 生成</h2>
59
+ <p class="small">
60
+ 因为“手写符号”是一种<strong>不会报错</strong>的错误。数据侧照着习惯把
61
+ <code>−1.05%</code> 写进去,组件再补一个减号,渲染出来是
62
+ <code>−−1.05%</code> —— 页面不崩、控制台不响,只是报表看起来像坏了。
63
+ 下面是两栏真实渲染(同一段数据,只差手写时带没带符号):
64
+ </p>
65
+ <div class="cmp">
66
+ <div class="cmp__pane cmp__bad" data-bad=".delta|glyphs|=7">
67
+ <h3>错:HTML 里也写了减号</h3>
68
+ <div class="row"><span class="delta delta--down">−1.05%</span></div>
69
+ <p class="small">字形 7 个 —— 两个减号叠在一起。</p>
70
+ </div>
71
+ <div class="cmp__pane cmp__good" data-good=".delta|glyphs|=6">
72
+ <h3>对:HTML 里只写数值本体</h3>
73
+ <div class="row"><span class="delta delta--down">1.05%</span></div>
74
+ <p class="small">字形 6 个 —— 减号只有一个,来自 CSS。</p>
75
+ </div>
76
+ </div>
77
+
78
+ <h2 class="rule-top">口径:默认红涨绿跌,可整体切换</h2>
79
+ <div class="pair">
80
+ <div class="pair__pane">
81
+ <h3>默认:A 股口径(红涨绿跌)</h3>
82
+ <div class="row">
83
+ <span class="delta delta--up">3.21%</span>
84
+ <span class="delta delta--down">1.05%</span>
85
+ </div>
86
+ </div>
87
+ <div class="pair__pane" data-market="intl">
88
+ <h3>国际口径(<code>[data-market="intl"]</code>)</h3>
89
+ <div class="row">
90
+ <span class="delta delta--up">3.21%</span>
91
+ <span class="delta delta--down">1.05%</span>
92
+ </div>
93
+ </div>
94
+ </div>
95
+ <p class="small">
96
+ 两栏的 HTML <strong>逐字相同</strong>,区别只有右栏祖先上多了
97
+ <code>data-market="intl"</code>。组件 CSS 里没有任何
98
+ <code>[data-market]</code> 分支 —— 它只写 <code>color: var(--dir-up)</code>,
99
+ 切换发生在令牌层(跟暗色同一手法:换底层值,语义名不动)。
100
+ <br>
101
+ 也可以只切局部:这个属性加在任何容器上,就只影响那一块。
102
+ </p>
103
+
104
+ <h2 class="rule-top">方向强化:箭头档</h2>
105
+ <div class="row">
106
+ <span class="delta delta--arrow delta--up">3.21%</span>
107
+ <span class="delta delta--arrow delta--down">1.05%</span>
108
+ <span class="delta">+3.21%(默认符号档,对照)</span>
109
+ </div>
110
+ <p class="small">
111
+ <code>delta--arrow</code> 把符号换成 ▲ / ▼。它与符号档<strong>互斥</strong>
112
+ (同一个 ::before),不要同时加。
113
+ <br>
114
+ ⚠️ 箭头是字符伪元素,读屏可能念出字符本身。要严格可控就给它外层加
115
+ <code>aria-label</code>(如「同比上涨 3.21%」),或不用这一档 ——
116
+ 默认的 +/− 已经满足「不能只靠颜色」。
117
+ </p>
118
+
119
+ <h2 class="rule-top">在真语境里</h2>
120
+ <div>
121
+ <div class="line">
122
+ <span class="line__k">营业总收入</span>
123
+ <span><span class="delta delta--up">12.40%</span></span>
124
+ </div>
125
+ <div class="line">
126
+ <span class="line__k">研发费用</span>
127
+ <span><span class="delta delta--up">38.20%</span></span>
128
+ </div>
129
+ <div class="line">
130
+ <span class="line__k">经营活动现金流净额</span>
131
+ <span><span class="delta delta--down">6.10%</span></span>
132
+ </div>
133
+ <div class="line">
134
+ <span class="line__k">基本每股收益</span>
135
+ <span><span class="delta delta--flat">0.00%</span></span>
136
+ </div>
137
+ </div>
138
+ <p class="small">
139
+ <strong>看第二行</strong>:研发费用 +38.20% 是“涨”,但那是成本失控,不是好消息。
140
+ 如果用 <code>table</code> 的 <code>num--pos</code>(会计约定:正 = 绿)来渲染,
141
+ 它会是绿色的 —— 颜色说“好事”,语义说“坏消息”。这正是 delta 存在的理由:
142
+ 它只描述方向,不替读者下结论。
143
+ </p>
144
+
145
+ <h2 class="rule-top">三条边界</h2>
146
+ <p class="small">
147
+ ① <strong>方向由你给,不由数值推断</strong>。本组件没有 JS,不读数值;
148
+ 类名写错就颜色错,而且不报错。数据侧务必按值选类,值为 0 用
149
+ <code>delta--flat</code>。
150
+ <br>
151
+ ② <strong>这不是“好/坏”指示器</strong>。要表达“成功/失败/警告”用
152
+ <code>badge</code> 的语义变体;要表达会计正负用 <code>table</code> 的
153
+ <code>num--pos/neg</code>。三者不可互换。
154
+ <br>
155
+ ③ <strong>涨跌幅塞进表格时用它,不要用 <code>num--pos/neg</code></strong>。
156
+ 那两组是会计约定(正 = 绿),套在涨跌幅上会把“跌”渲染成红、把“涨”渲染成绿,
157
+ 正好与 A 股习惯相反。这一条此前是 <code>table</code> README 里登记的未解问题,
158
+ 现在有解了。
159
+ </p>
160
+ </main>
161
+ </body>
162
+ </html>