Skip to content

Commit 3ca01c8

Browse files
committed
feat(ui): 定阴影两档与字阶 CSS 变量并接线 tokens.css(0-D 硬前置①②)
- gen-tokens.mjs 新增 TYPE_SCALE / SHADOW_TOKENS 真源,人读 typeScale 改为同源派生: 第 3 档无单位行高 1.9 → 29.5px(15.5 × 1.9 = 29.45) - 产物新增 18 个字阶 CSS 变量与两档阴影(亮档投影 / 暗档白色反相描边) - --due 亮档第二次对比度修正:按「三底 ≥4.5 且剪报底 ≥4.55」取最小加深量 ⇒ #9F5E10(f=0.994;剪报底 4.5571 / 纸 4.9309 / 面 5.1436) - 新增剪报底「底 × 墨」组合断言 + ink-4 / 旧值 #A05F10 两条反例守门(ink-4 值不动) - main.tsx 接线 ./ui/tokens.css(排 note-mark.css 之前),drift 测试加接线守卫 - 回写 ui-ux-system.md(阴影改名与剪报前景规则)与规格 §4.1/§4.2/§4.4
1 parent 76d2066 commit 3ca01c8

9 files changed

Lines changed: 252 additions & 20 deletions

File tree

‎app/scripts/gen-tokens.mjs‎

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const COLOR_TOKENS = [
3636
{ name: "bg-sunken", light: "#F1EEE7", dark: "#100F0E", usage: "输入槽 / 骨架 / 内嵌" },
3737
{ name: "bg-canvas", light: "#FBFAF8", dark: "#141312", usage: "窗口底(纸)" },
3838
{ name: "bg-surface", light: "#FFFFFF", dark: "#1C1A18", usage: "卡片 / 列 / 阅读面" },
39-
{ name: "bg-raised", light: "#FFFFFF", dark: "#24211E", usage: "弹层 / 菜单 / 浮窗(亮档另加 --ed-shadow-1(0-D 前定值))" },
39+
{ name: "bg-raised", light: "#FFFFFF", dark: "#24211E", usage: "弹层 / 菜单 / 浮窗(亮档另加 --ed-shadow-1)" },
4040
{ name: "border", light: "#EAE7E0", dark: "#2E2A26", usage: "横格 / 分隔" },
4141
{ name: "border-strong", light: "#C9C4B8", dark: "#423C36", usage: "输入框 / 刻度底 / 引线" },
4242
{ name: "ink-4", light: "#909088", dark: "#6E6A62", usage: "未确认(过渡态,3.22:1,见 ADR-032 第 4 条)" },
@@ -46,18 +46,43 @@ export const COLOR_TOKENS = [
4646
{ name: "mark-clip", light: "#F4F1E9", dark: "#221F1B", usage: "剪报底纹(默认开,仅背景色不加边框)" },
4747
{ name: "stamp", light: "#B3271E", dark: "#E0604F", usage: "状态戳 —— 全站唯一非中性色,绝不用于按钮" },
4848
{ name: "ok", light: "#2F7A4F", dark: "#4FAE74", usage: "掌握 / 已毕业 / 成功回执" },
49-
{ name: "due", light: "#A05F10", dark: "#E0A44B", usage: "到期刻度 / 低置信点线 / 记忆语义文字(亮档原 #B26A12 实测仅 4.06:1,不合格,已改)" },
49+
{ name: "due", light: "#9F5E10", dark: "#E0A44B", usage: "到期刻度 / 低置信点线 / 记忆语义文字(亮档两次对比度修正:#B26A12 4.06:1 → #A05F10 → #9F5E10;最终值以剪报底余量 ≥0.05 为准)" },
5050
{ name: "link", light: "#1F5FBF", dark: "#6E9BE8", usage: "链接 / 时间码 / 引用" },
5151
{ name: "overlay", light: "#1A1A1A", dark: "#1A1A1A", usage: "遮罩基色(配 --ed-overlay-alpha 使用)" },
5252
];
5353

54+
/**
55+
* 字阶结构化真源(规范 §4.2)—— CSS 变量与人读串**同源派生**,不允许各写一份。
56+
* 第 3 档原为无单位行高 `1.9`,其余为 px(两种写法并存会让 CSS 无法统一消费);
57+
* 换算依据:15.5px × 1.9 = 29.45px ⇒ 取一位小数 **29.5px**(0.5px 是可用精度,
58+
* 差 0.05px 不影响任何排版判定,且比 29px 更接近真实行高)。
59+
*/
60+
export const TYPE_SCALE = [
61+
{ size: 25, line: 34, weight: 600 },
62+
{ size: 17, line: 24, weight: 600 },
63+
{ size: 15.5, line: 29.5, weight: 400 },
64+
{ size: 13, line: 20, weight: 400 },
65+
{ size: 12, line: 18, weight: 500 },
66+
{ size: 11.5, line: 16, weight: 500 },
67+
];
68+
69+
/**
70+
* 阴影 token(规范 §4.2② · 2026-09-11 用户裁决「纸感双层暖墨」,提交 `44b6e05a`)。
71+
* **暗档不用投影**,改白色反相描边(沿用 `docs/product/ui-ux-system.md:204` 的既有做法)——
72+
* 暗底上的黑色投影看不见,只会让面板边缘糊成一团。
73+
*/
74+
export const SHADOW_TOKENS = [
75+
{ name: "shadow-1", light: "0 1px 2px rgba(28,25,23,.06), 0 4px 12px rgba(28,25,23,.08)", dark: "0 0 0 1px rgba(255,255,255,.06)", usage: "低层:菜单 / 浮层 / 小卡(亮档投影;暗档反相描边)" },
76+
{ name: "shadow-2", light: "0 2px 4px rgba(28,25,23,.06), 0 12px 32px rgba(28,25,23,.14)", dark: "0 0 0 1px rgba(255,255,255,.06)", usage: "高层:Modal / 浮窗(亮档投影;暗档反相描边)" },
77+
];
78+
5479
/** 非颜色 token:两档共用(规范 §4.2) */
5580
export const SCALE_SOURCE = {
5681
fontFamilyBody: '"Source Han Serif SC", "Songti SC", SimSun, serif',
5782
fontFamilyUi: '"Inter", "Segoe UI Variable", "Microsoft YaHei UI", system-ui, sans-serif',
5883
fontFamilyMono: '"JetBrains Mono", Consolas, ui-monospace, monospace',
59-
/** 字阶:字号/行高·字重(规范 §4.2,下界 12px;`11.5/16 mono` 为唯一点名例外) */
60-
typeScale: ["25px/34px·600", "17px/24px·600", "15.5px/1.9·400", "13px/20px·400", "12px/18px·500", "11.5px/16px·500"],
84+
/** 字阶:字号/行高·字重(规范 §4.2,下界 12px;`11.5/16 mono` 为唯一点名例外)—— 由 TYPE_SCALE 派生 */
85+
typeScale: TYPE_SCALE.map((t) => `${t.size}px/${t.line}px·${t.weight}`),
6186
/** 间距:4 为半档,其余落 8px 网格 */
6287
spaceScale: [4, 8, 12, 16, 24, 32, 48],
6388
/** 圆角:3 印章 · 5 控件与卡 · 8 面板 · 10 浮层 */
@@ -104,12 +129,22 @@ ${light}
104129
--ed-font-ui: ${SCALE_SOURCE.fontFamilyUi};
105130
--ed-font-mono: ${SCALE_SOURCE.fontFamilyMono};
106131
132+
/* 字阶(规范 §4.2):--ed-type-<n>-{size,line,weight},n 与 TYPE_SCALE 下标同序(1 起) */
133+
${TYPE_SCALE.map((t, i) => [
134+
` --ed-type-${i + 1}-size: ${t.size}px;`,
135+
` --ed-type-${i + 1}-line: ${t.line}px;`,
136+
` --ed-type-${i + 1}-weight: ${t.weight};`,
137+
].join("\n")).join("\n")}
138+
107139
/* 间距(4 为半档,仅图标内边距) */
108140
${SCALE_SOURCE.spaceScale.map((n) => ` --ed-space-${n}: ${n}px;`).join("\n")}
109141
110142
/* 圆角 */
111143
${SCALE_SOURCE.radiusScale.map((r) => ` --ed-radius-${r.name}: ${r.px}px;`).join("\n")}
112144
145+
/* 阴影(规范 §4.2②):亮档投影;暗档在 [data-theme="dark"] 里改为反相白描边 */
146+
${SHADOW_TOKENS.map((t) => ` --ed-${t.name}: ${t.light}; /* ${t.usage} */`).join("\n")}
147+
113148
/* 遮罩 */
114149
--ed-overlay-alpha: ${SCALE_SOURCE.overlayAlpha};
115150
@@ -119,6 +154,9 @@ ${SCALE_SOURCE.radiusScale.map((r) => ` --ed-radius-${r.name}: ${r.px}px;`).joi
119154
120155
[data-theme="dark"] {
121156
${dark}
157+
158+
/* 阴影:暗档不用投影,改白色反相描边(暗底上黑色投影不可见) */
159+
${SHADOW_TOKENS.map((t) => ` --ed-${t.name}: ${t.dark};`).join("\n")}
122160
}
123161
`;
124162
}
@@ -153,6 +191,7 @@ export const SCALE_TOKENS = {
153191
fontFamilyUi: ${JSON.stringify(SCALE_SOURCE.fontFamilyUi)},
154192
fontFamilyMono: ${JSON.stringify(SCALE_SOURCE.fontFamilyMono)},
155193
typeScale: ${JSON.stringify(SCALE_SOURCE.typeScale)},
194+
typeScaleVars: ${JSON.stringify(TYPE_SCALE)},
156195
spaceScale: ${JSON.stringify(SCALE_SOURCE.spaceScale)},
157196
radiusScale: ${JSON.stringify(SCALE_SOURCE.radiusScale)},
158197
overlayAlpha: ${SCALE_SOURCE.overlayAlpha},

‎app/scripts/gen-tokens.test.mjs‎

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* 待并入或显式豁免。
1212
*/
1313
import { describe, expect, it } from "vitest";
14-
import { COLOR_TOKENS, CONTRAST_BASELINE, SCALE_SOURCE, renderAll } from "./gen-tokens.mjs";
14+
import { COLOR_TOKENS, CONTRAST_BASELINE, SCALE_SOURCE, SHADOW_TOKENS, TYPE_SCALE, renderAll } from "./gen-tokens.mjs";
1515
import { contrastRatio } from "../src/ui/contrast.ts";
1616

1717
describe("gen-tokens 规范数据", () => {
@@ -126,3 +126,84 @@ describe("renderAll", () => {
126126
expect(sizes.filter((n) => n < 12)).toEqual([11.5]);
127127
});
128128
});
129+
130+
describe("字阶 CSS 变量(0-D 硬前置②)", () => {
131+
it("6 档 × size/line/weight 共 18 个变量都在,且与 TYPE_SCALE 一一对应", () => {
132+
const { css } = renderAll();
133+
expect(TYPE_SCALE).toHaveLength(6);
134+
for (const [i, t] of TYPE_SCALE.entries()) {
135+
const n = i + 1;
136+
expect(css, `第 ${n} 档 size`).toContain(`--ed-type-${n}-size: ${t.size}px;`);
137+
expect(css, `第 ${n} 档 line`).toContain(`--ed-type-${n}-line: ${t.line}px;`);
138+
expect(css, `第 ${n} 档 weight`).toContain(`--ed-type-${n}-weight: ${t.weight};`);
139+
}
140+
// 18 个变量名一个不多一个不少
141+
expect(css.match(/--ed-type-\d-(size|line|weight)/g)?.length).toBe(18);
142+
});
143+
144+
it("无单位行高已消灭(反例守门:/1.9 不得回归)", () => {
145+
expect(SCALE_SOURCE.typeScale.join("|")).not.toContain("/1.9");
146+
expect(SCALE_SOURCE.typeScale[2]).toBe("15.5px/29.5px·400");
147+
expect(SCALE_SOURCE.typeScale[2]).toContain(`${TYPE_SCALE[2].line}px`);
148+
});
149+
150+
it("字阶人读串与结构化真源同源(不允许各写一份)", () => {
151+
expect([...SCALE_SOURCE.typeScale]).toEqual(
152+
TYPE_SCALE.map((t) => `${t.size}px/${t.line}px·${t.weight}`),
153+
);
154+
});
155+
});
156+
157+
describe("阴影 token(规范 §4.2②)", () => {
158+
it("两档齐全,值逐字等于用户裁决", () => {
159+
expect(SHADOW_TOKENS.map((t) => t.name)).toEqual(["shadow-1", "shadow-2"]);
160+
expect(SHADOW_TOKENS[0].light).toBe("0 1px 2px rgba(28,25,23,.06), 0 4px 12px rgba(28,25,23,.08)");
161+
expect(SHADOW_TOKENS[1].light).toBe("0 2px 4px rgba(28,25,23,.06), 0 12px 32px rgba(28,25,23,.14)");
162+
});
163+
164+
it("暗档不是投影而是反相白描边,且两档各写一次", () => {
165+
const { css } = renderAll();
166+
for (const t of SHADOW_TOKENS) {
167+
expect(t.dark).toBe("0 0 0 1px rgba(255,255,255,.06)");
168+
expect(css).toContain(`--ed-${t.name}: ${t.light};`);
169+
expect(css).toContain(`--ed-${t.name}: ${t.dark};`);
170+
}
171+
});
172+
173+
it("usage 串不得含注释终止符(会被写进 CSS 注释)", () => {
174+
for (const t of SHADOW_TOKENS) expect(t.usage).not.toContain("*/");
175+
});
176+
177+
it("产物里不再出现「引用未定义变量」的说明(0-A 交接第 1① 条)", () => {
178+
const { css, ts } = renderAll();
179+
expect(css).not.toContain("0-D 前定值");
180+
expect(ts).not.toContain("0-D 前定值");
181+
});
182+
});
183+
184+
// 求解规则(Task 1 Step 6):保持色相、RGB 三通道按同一系数 f 缩放取整,
185+
// 取满足「纸 ≥4.5 且 面 ≥4.5 且 剪报底 ≥4.55」的**最小加深量**。
186+
// 4.55 = 4.5 底线 + 0.05 余量:`#A05F10` 正是「卡在 4.4950」的反例,
187+
// hex 量化与将来底色微调都会吃掉没有余量的值 —— 故余量本身是硬要求。
188+
describe("--due 亮档第二次对比度修正(剪报底余量规则)", () => {
189+
const CLIP = "#F4F1E9"; // --ed-mark-clip 亮档
190+
const by = (n) => COLOR_TOKENS.find((t) => t.name === n);
191+
192+
it("新值满足全部三条(纸 / 面 / 剪报底),且剪报底留 ≥0.05 余量", () => {
193+
const due = by("due").light;
194+
expect(contrastRatio(due, "#FBFAF8"), "due/纸").toBeGreaterThanOrEqual(4.5);
195+
expect(contrastRatio(due, "#FFFFFF"), "due/面").toBeGreaterThanOrEqual(4.5);
196+
expect(contrastRatio(due, CLIP), "due/剪报底").toBeGreaterThanOrEqual(4.55);
197+
});
198+
199+
it("反例守门:被替换掉的 #A05F10 在剪报底上仍低于 4.5(不得回归)", () => {
200+
expect(contrastRatio("#A05F10", CLIP)).toBeLessThan(4.5);
201+
// 更早的 #B26A12 连纸底都不过
202+
expect(contrastRatio("#B26A12", "#FBFAF8")).toBeLessThan(4.5);
203+
});
204+
205+
it("暗档 #E0A44B 未被牵连改动(暗档剪报底 7.49:1 本就达标)", () => {
206+
expect(by("due").dark).toBe("#E0A44B");
207+
expect(contrastRatio(by("due").dark, "#221F1B")).toBeGreaterThanOrEqual(4.5);
208+
});
209+
});

‎app/src/main.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import React from "react";
22
import ReactDOM from "react-dom/client";
33
import App from "./App";
4+
// 设计系统 token(ADR-032):只定义 :root / [data-theme="dark"] 的自定义属性,无选择器 ⇒ 零视觉变化
5+
import "./ui/tokens.css";
46
// v0.16.1:正文多色荧光笔样式(remarkMarkHighlight 注入类名 note-mark[-{colorId}])
57
import "./note-mark.css";
68

‎app/src/ui/contrast.test.ts‎

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
*
44
* Why:规范 §4.3 的硬数字(11 / 4.5 / 3 / 10.5)是本批验收口径,必须由机器而非人眼守护;
55
* 同时守住 hex 解析的边界(非法输入抛错而非静默回退)与两档不可合并且值不得回归。
6+
* 批 0-D 追加:剪报底纹(`--ed-mark-clip`)作为**第三种底**参与判定 —— §4.3 的阈值是以
7+
* 「阅读面」为基准定的,把文字放到剪报底上会换底,故「底 × 墨」组合必须逐对实测。
68
*/
79
import { describe, expect, it } from "vitest";
810
import { contrastRatio, meetsAA, parseHex, relativeLuminance } from "./contrast";
11+
import { COLOR_TOKENS } from "./tokens";
912

1013
describe("parseHex", () => {
1114
it("解析 6 位 hex", () => {
@@ -83,11 +86,15 @@ describe("contrastRatio", () => {
8386
expect(contrastRatio("#6E6E68", "#FBFAF8")).toBeGreaterThanOrEqual(4.5);
8487
});
8588

86-
it("规范:due 亮档达标(原 #B26A12 仅 4.06:1,低于正文线,已改 #A05F10)", () => {
87-
expect(contrastRatio("#A05F10", "#FBFAF8")).toBeGreaterThanOrEqual(4.5);
88-
expect(contrastRatio("#A05F10", "#FFFFFF")).toBeGreaterThanOrEqual(4.5);
89-
// 反例守门:旧值不得回归
89+
it("规范:due 亮档两次修正后达标(#B26A12 4.06:1 → #A05F10 → 本值)", () => {
90+
expect(contrastRatio("#9F5E10", "#FBFAF8"), "due/纸").toBeGreaterThanOrEqual(4.5);
91+
expect(contrastRatio("#9F5E10", "#FFFFFF"), "due/面").toBeGreaterThanOrEqual(4.5);
92+
// 第二次修正的求解规则:剪报底须留 ≥0.05 余量(4.5 + 0.05 = 4.55),
93+
// 因为 hex 量化与将来底色微调都会吃掉没有余量的值(#A05F10 正是卡在 4.4950 的反例)
94+
expect(contrastRatio("#9F5E10", "#F4F1E9"), "due/剪报底").toBeGreaterThanOrEqual(4.55);
95+
// 反例守门:两个旧值不得回归(各自在不同底上不达标)
9096
expect(contrastRatio("#B26A12", "#FBFAF8")).toBeLessThan(4.5);
97+
expect(contrastRatio("#A05F10", "#F4F1E9")).toBeLessThan(4.5);
9198
});
9299

93100
it("规范:ink-4 对纸底允许低于 4.5 但不低于 3:1", () => {
@@ -121,3 +128,42 @@ describe("meetsAA", () => {
121128
expect(meetsAA(2.99, { large: true })).toBe(false);
122129
});
123130
});
131+
132+
// ---- 剪报底纹(0-D 硬前置 · 规范 §4.4 的防御性规则)------------------------------------
133+
// 裁决(2026-09-11 控制方):剪报底纹**只改背景**,文字仍用所在层级的 `--ed-ink-*`,
134+
// **不新增前景 token**;因此原来按「面」定的阈值在这里要逐对重测一遍。
135+
// 实测(Task 1 报告,亮档底 `#F4F1E9`):ink-1 15.4197 · ink-2 10.1204 · ink-3 4.5454 ·
136+
// stamp 5.7679 · ok 4.6310 · due 4.5571 · link 5.3968;**ink-4 仅 2.8489 ⇒ 禁止组合**。
137+
// 暗档底 `#221F1B`:ink-1 14.5564 · ink-2 10.7931 · ink-3 5.5052 · ink-4 3.0480 ·
138+
// stamp 4.6533 · ok 5.9688 · due 7.4863 · link 5.8732。
139+
describe("剪报底纹上的文字(底 × 墨组合 · 规范 §4.4)", () => {
140+
const by = (n: string) => COLOR_TOKENS.find((t) => t.name === n);
141+
const clipOf = (theme: "light" | "dark") => by("mark-clip")![theme];
142+
const AA: ReadonlyArray<readonly [string, "light" | "dark"]> = [
143+
["ink-1", "light"], ["ink-1", "dark"],
144+
["ink-2", "light"], ["ink-2", "dark"],
145+
["ink-3", "light"], ["ink-3", "dark"],
146+
["stamp", "light"], ["stamp", "dark"],
147+
["ok", "light"], ["ok", "dark"],
148+
["due", "light"], ["due", "dark"],
149+
["link", "light"], ["link", "dark"],
150+
["ink-4", "dark"], // ink-4 是过渡态:只须 ≥3(§4.3);**亮档不在名单里 —— 见下面反例守门**
151+
];
152+
for (const [name, theme] of AA) {
153+
it(`${theme} · ${name} / 剪报底`, () => {
154+
const floor = name === "ink-4" ? 3 : 4.5;
155+
expect(contrastRatio(by(name)![theme], clipOf(theme)), `${theme} ${name}/剪报底`).toBeGreaterThanOrEqual(floor);
156+
});
157+
}
158+
159+
// ★ 反例守门(范式 B,同 `#B26A12` 的写法):把「禁止组合」钉成机器判据 ——
160+
// 将来若有人把 ink-4 的文字放到剪报底纹上,这条断言就是那份"为什么不行"的证据。
161+
it("规范:ink-4 亮档不得用于剪报底纹(实测低于其 3:1 过渡态例外)", () => {
162+
expect(contrastRatio(by("ink-4")!.light, clipOf("light"))).toBeLessThan(3);
163+
});
164+
165+
it("规范:due 亮档第二次修正——刚被替换掉的 #A05F10 在剪报底上仍不达标(防回归)", () => {
166+
expect(contrastRatio("#A05F10", clipOf("light"))).toBeLessThan(4.5);
167+
expect(contrastRatio(by("due")!.light, clipOf("light"))).toBeGreaterThanOrEqual(4.5);
168+
});
169+
});

‎app/src/ui/tokens.css‎

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
--ed-bg-sunken: #F1EEE7; /* 输入槽 / 骨架 / 内嵌 */
99
--ed-bg-canvas: #FBFAF8; /* 窗口底(纸) */
1010
--ed-bg-surface: #FFFFFF; /* 卡片 / 列 / 阅读面 */
11-
--ed-bg-raised: #FFFFFF; /* 弹层 / 菜单 / 浮窗(亮档另加 --ed-shadow-1(0-D 前定值)) */
11+
--ed-bg-raised: #FFFFFF; /* 弹层 / 菜单 / 浮窗(亮档另加 --ed-shadow-1) */
1212
--ed-border: #EAE7E0; /* 横格 / 分隔 */
1313
--ed-border-strong: #C9C4B8; /* 输入框 / 刻度底 / 引线 */
1414
--ed-ink-4: #909088; /* 未确认(过渡态,3.22:1,见 ADR-032 第 4 条) */
@@ -18,7 +18,7 @@
1818
--ed-mark-clip: #F4F1E9; /* 剪报底纹(默认开,仅背景色不加边框) */
1919
--ed-stamp: #B3271E; /* 状态戳 —— 全站唯一非中性色,绝不用于按钮 */
2020
--ed-ok: #2F7A4F; /* 掌握 / 已毕业 / 成功回执 */
21-
--ed-due: #A05F10; /* 到期刻度 / 低置信点线 / 记忆语义文字(亮档原 #B26A12 实测仅 4.06:1,不合格,已改) */
21+
--ed-due: #9F5E10; /* 到期刻度 / 低置信点线 / 记忆语义文字(亮档两次对比度修正:#B26A12 4.06:1 → #A05F10 → #9F5E10;最终值以剪报底余量 ≥0.05 为准) */
2222
--ed-link: #1F5FBF; /* 链接 / 时间码 / 引用 */
2323
--ed-overlay: #1A1A1A; /* 遮罩基色(配 --ed-overlay-alpha 使用) */
2424

@@ -27,6 +27,26 @@
2727
--ed-font-ui: "Inter", "Segoe UI Variable", "Microsoft YaHei UI", system-ui, sans-serif;
2828
--ed-font-mono: "JetBrains Mono", Consolas, ui-monospace, monospace;
2929

30+
/* 字阶(规范 §4.2):--ed-type-<n>-{size,line,weight},n 与 TYPE_SCALE 下标同序(1 起) */
31+
--ed-type-1-size: 25px;
32+
--ed-type-1-line: 34px;
33+
--ed-type-1-weight: 600;
34+
--ed-type-2-size: 17px;
35+
--ed-type-2-line: 24px;
36+
--ed-type-2-weight: 600;
37+
--ed-type-3-size: 15.5px;
38+
--ed-type-3-line: 29.5px;
39+
--ed-type-3-weight: 400;
40+
--ed-type-4-size: 13px;
41+
--ed-type-4-line: 20px;
42+
--ed-type-4-weight: 400;
43+
--ed-type-5-size: 12px;
44+
--ed-type-5-line: 18px;
45+
--ed-type-5-weight: 500;
46+
--ed-type-6-size: 11.5px;
47+
--ed-type-6-line: 16px;
48+
--ed-type-6-weight: 500;
49+
3050
/* 间距(4 为半档,仅图标内边距) */
3151
--ed-space-4: 4px;
3252
--ed-space-8: 8px;
@@ -42,6 +62,10 @@
4262
--ed-radius-panel: 8px;
4363
--ed-radius-overlay: 10px;
4464

65+
/* 阴影(规范 §4.2②):亮档投影;暗档在 [data-theme="dark"] 里改为反相白描边 */
66+
--ed-shadow-1: 0 1px 2px rgba(28,25,23,.06), 0 4px 12px rgba(28,25,23,.08); /* 低层:菜单 / 浮层 / 小卡(亮档投影;暗档反相描边) */
67+
--ed-shadow-2: 0 2px 4px rgba(28,25,23,.06), 0 12px 32px rgba(28,25,23,.14); /* 高层:Modal / 浮窗(亮档投影;暗档反相描边) */
68+
4569
/* 遮罩 */
4670
--ed-overlay-alpha: 0.34;
4771

@@ -66,4 +90,8 @@
6690
--ed-due: #E0A44B;
6791
--ed-link: #6E9BE8;
6892
--ed-overlay: #1A1A1A;
93+
94+
/* 阴影:暗档不用投影,改白色反相描边(暗底上黑色投影不可见) */
95+
--ed-shadow-1: 0 0 0 1px rgba(255,255,255,.06);
96+
--ed-shadow-2: 0 0 0 1px rgba(255,255,255,.06);
6997
}

‎app/src/ui/tokens.drift.test.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,13 @@ describe("token 门面一致性", () => {
7575
}
7676
});
7777
});
78+
79+
describe("token 接线守卫", () => {
80+
// Why:未接线的 `var(--ed-*)` **不会报错**,只会静默取不到值(见 tokens.ts 头注)。
81+
// 批 0-D 的原语全靠这些变量着色/取阴影,漏一次 import 就会让整层原语在运行时失效,
82+
// 而所有单元测试仍全绿 —— 故把这条从纪律变成机器判据。
83+
it("应用入口 import 了 ui/tokens.css(未接线则所有 var(--ed-*) 运行时无值)", () => {
84+
const main = readFileSync(join(HERE, "..", "main.tsx"), "utf8");
85+
expect(main, "main.tsx 必须 import ./ui/tokens.css(见批 0-D 控制方裁决)").toContain('import "./ui/tokens.css";');
86+
});
87+
});

0 commit comments

Comments
 (0)