Skip to content

Commit 2ebac5e

Browse files
committed
refactor(motion): 动效 token 迁入生成器真源
1 parent bffed92 commit 2ebac5e

8 files changed

Lines changed: 212 additions & 38 deletions

File tree

‎app/scripts/gen-tokens.d.mts‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
*
44
* Why:生成器是 `.mjs`(**纯 node 必须能跑**,故不能是 `.ts`),而 `tsc` 默认对 `.mjs` 报 TS7016
55
* (「implicitly has an 'any' type」)—— 该错误会随 `npm run build`(`tsc && vite build`)使构建失败。
6-
* `src/ui/tokens.drift.test.ts` 需要 `renderAll()` / `CONTRAST_BASELINE` / `normalizeEol()` 来守住产物。
6+
* `src/ui/tokens.drift.test.ts` 需要 `renderAll()` / `CONTRAST_BASELINE` / `normalizeEol()` 来守住产物;
7+
* `src/ui/primitives/style-seams.test.ts`(批 6 T5 起)还需要 `DURATION_TOKENS` / `EASING_TOKENS`。
78
*
89
* 纪律:本文件是**类型契约**,不含运行时值 —— 声明与生成器实现不可能在运行时分叉。
910
*
@@ -59,6 +60,29 @@ export declare const SCALE_SOURCE: {
5960
readonly iconSizes: readonly number[];
6061
};
6162

63+
/** 动效时长源数据(规格 §8.4);`name` 不含 `--ed-dur-` 前缀,`ms` 为纯数字 */
64+
export interface DurationToken {
65+
readonly name: string;
66+
readonly ms: number;
67+
readonly usage: string;
68+
}
69+
70+
/** 缓动源数据(规格 §8.4);`name` 不含 `--ed-` 前缀 */
71+
export interface EasingToken {
72+
readonly name: string;
73+
readonly value: string;
74+
readonly usage: string;
75+
}
76+
77+
/** 批 6 T5 起:动效时长真源(9 条)—— 迁移自 primitives/motion.css 的临时接缝 */
78+
export declare const DURATION_TOKENS: readonly DurationToken[];
79+
80+
/** 批 6 T5 起:缓动真源(今日 1 条;T8 追加双基调两条) */
81+
export declare const EASING_TOKENS: readonly EasingToken[];
82+
83+
/* 注:合并名册 `MOTION_TOKENS` 只存在于**产物** `src/ui/tokens.gen.ts`(由 `renderTs()` 生成),
84+
生成器本身不导出它 —— 故此处**不得**声明,否则 import 方在编译期可通过、运行期取到 undefined。 */
85+
6286
/** 纯渲染:返回两份产物的完整文本,不触磁盘 */
6387
export declare function renderAll(): { css: string; ts: string };
6488

‎app/scripts/gen-tokens.mjs‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,37 @@ export const SHADOW_TOKENS = [
7676
{ 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 / 浮窗(亮档投影;暗档反相描边)" },
7777
];
7878

79+
/**
80+
* 动效时长 token(规格 §8.4)—— 批 6 从 `src/ui/primitives/motion.css` 的临时接缝**逐字迁入**(R2.3)。
81+
*
82+
* Why 进真源:`motion.css` 的那段 `:root{}` 是批 0-D 的**临时**落点(当时还没有动效真源),文件头逐字
83+
* 承诺「批 6 的动效 token 真源完成后整块删除」。迁移后本文件是时长/缓动的**唯一真源**(R1.1 单一真源,
84+
* 绝不双写):`motion.css` 里不再允许出现 `--ed-*` 定义,由 `style-seams.test.ts` 的回归封条守。
85+
* 值**逐字照抄**临时块、一个不改;消费点写的是 `var(--ed-dur-x, <同值字面量>)` ⇒ 删块即生效、零规则改动。
86+
*
87+
* 边界:`ms` 是**纯数字**(`renderCss` 负责拼 `ms` 单位);`usage` 只进 TS 产物,不进 CSS。
88+
*/
89+
export const DURATION_TOKENS = [
90+
{ name: "micro", ms: 120, usage: "响应层:单属性、无时序的交互回执(规格 §8.2 判据)" },
91+
{ name: "overlay-in", ms: 200, usage: "弹层遮罩与面板进场(规格 §8.4「进出场:弹层 200/160」)" },
92+
{ name: "overlay-out", ms: 160, usage: "弹层遮罩与面板出场 —— 出场比进场快(规格 §8.4)" },
93+
{ name: "toast-in", ms: 180, usage: "Toast 进场(规格 §8.4「Toast 180/140」)" },
94+
{ name: "toast-out", ms: 140, usage: "Toast 出场 —— 出场比进场快(规格 §8.4)" },
95+
{ name: "skeleton", ms: 1200, usage: "循环环境动效:骨架微光(规格 §8.4「--dur-skeleton 1200ms」)" },
96+
{ name: "card", ms: 220, usage: "面板 / 视图 / 列折叠(规格 §8.4「--dur-card 220ms」);今日 0 生产消费者,波 C 首次消费" },
97+
{ name: "reveal", ms: 500, usage: "显影 / 编排层(规格 §8.4「--dur-reveal 500ms」);今日 0 生产消费者,波 C 首次消费" },
98+
{ name: "page", ms: 150, usage: "页面切换(规格 §8.4「--dur-page 150ms」);今日 0 生产消费者,波 C 首次消费" },
99+
];
100+
101+
/**
102+
* 缓动 token(规格 §8.4「`--ease cubic-bezier(0.2,0,0,1)`」)—— 与时长**分开列**:
103+
* 变量名是 `--ed-ease`(没有 `dur-` 段),而时长恒为 `--ed-dur-<name>`,两者的拼法不同名。
104+
* 批 6 的 T8 会在此追加双基调的两条缓动(`data-tone` 的仪器 / 纸),**只许追加、不许改本条**。
105+
*/
106+
export const EASING_TOKENS = [
107+
{ name: "ease", value: "cubic-bezier(0.2, 0, 0, 1)", usage: "全站唯一曲线(规格 §8.4)" },
108+
];
109+
79110
/** 非颜色 token:两档共用(规范 §4.2) */
80111
export const SCALE_SOURCE = {
81112
fontFamilyBody: '"Source Han Serif SC", "Songti SC", SimSun, serif',
@@ -161,6 +192,10 @@ ${SHADOW_TOKENS.map((t) => ` --ed-${t.name}: ${t.light}; /* ${t.usage} */`).joi
161192
/* 壳层纵向基准(规格 §1 决策 16):顶栏高度 —— 页面用 calc(100vh - var(--ed-nav-h)) 消费 */
162193
--ed-nav-h: ${SCALE_SOURCE.navHeight}px;
163194
195+
/* 动效(规格 §8.4):9 个时长 + 1 个缓动 —— 批 6 从 primitives/motion.css 的临时接缝迁入,值逐字不变 */
196+
${DURATION_TOKENS.map((t) => ` --ed-dur-${t.name}: ${t.ms}ms;`).join("\n")}
197+
${EASING_TOKENS.map((t) => ` --ed-${t.name}: ${t.value};`).join("\n")}
198+
164199
/* 图标 */
165200
--ed-icon-stroke: ${SCALE_SOURCE.iconStroke};
166201
}
@@ -178,6 +213,21 @@ function renderTs() {
178213
const colorRows = COLOR_TOKENS.map(
179214
(t) => ` { name: ${JSON.stringify(t.name)}, light: ${JSON.stringify(t.light)}, dark: ${JSON.stringify(t.dark)}, usage: ${JSON.stringify(t.usage)} },`,
180215
).join("\n");
216+
const durationRows = DURATION_TOKENS.map(
217+
(t) => ` { name: ${JSON.stringify(t.name)}, ms: ${t.ms}, usage: ${JSON.stringify(t.usage)} },`,
218+
).join("\n");
219+
const easingRows = EASING_TOKENS.map(
220+
(t) => ` { name: ${JSON.stringify(t.name)}, value: ${JSON.stringify(t.value)}, usage: ${JSON.stringify(t.usage)} },`,
221+
).join("\n");
222+
// 合并名册:与上面两份同源派生(同一份 DURATION_TOKENS/EASING_TOKENS 数据),不是第二份手写数据
223+
const motionRows = [
224+
...DURATION_TOKENS.map(
225+
(t) => ` { name: ${JSON.stringify(`dur-${t.name}`)}, cssVar: ${JSON.stringify(`--ed-dur-${t.name}`)}, kind: "duration", value: ${JSON.stringify(`${t.ms}ms`)} },`,
226+
),
227+
...EASING_TOKENS.map(
228+
(t) => ` { name: ${JSON.stringify(t.name)}, cssVar: ${JSON.stringify(`--ed-${t.name}`)}, kind: "easing", value: ${JSON.stringify(t.value)} },`,
229+
),
230+
].join("\n");
181231
return `${TS_HEADER}export interface ColorToken {
182232
/** 不含 \`--ed-\` 前缀;完整变量名用 \`cssVar(name)\` 组装 */
183233
readonly name: string;
@@ -214,6 +264,46 @@ export const SCALE_TOKENS = {
214264
iconSizes: ${JSON.stringify(SCALE_SOURCE.iconSizes)},
215265
} as const;
216266
267+
export interface DurationToken {
268+
/** 不含 \`--ed-dur-\` 前缀;完整变量名 = \`--ed-dur-\` + name */
269+
readonly name: string;
270+
readonly ms: number;
271+
readonly usage: string;
272+
}
273+
274+
export interface EasingToken {
275+
/** 不含 \`--ed-\` 前缀(今日只有全站唯一曲线 \`ease\`) */
276+
readonly name: string;
277+
readonly value: string;
278+
readonly usage: string;
279+
}
280+
281+
/** 动效时长(规格 §8.4)—— 值**逐字**来自批 0-D 的临时接缝 primitives/motion.css,批 6 迁入此处 */
282+
export const DURATION_TOKENS = [
283+
${durationRows}
284+
] as const satisfies readonly DurationToken[];
285+
286+
/** 时长短名的字面量联合:门面收窄入参用(拼错必须编译期报错,不是运行期静默取不到值) */
287+
export type DurationTokenName = (typeof DURATION_TOKENS)[number]["name"];
288+
289+
/** 缓动曲线(规格 §8.4)—— 批 6 的 T8 会追加双基调的两条,只许追加 */
290+
export const EASING_TOKENS = [
291+
${easingRows}
292+
] as const satisfies readonly EasingToken[];
293+
294+
/**
295+
* 时长 + 缓动的**合并名册**:\`name\` 是不含前缀的短名(\`dur-micro\` / \`ease\`),
296+
* \`cssVar\` 是 CSS 变量全名,\`value\` 是 CSS 里的字面量(时长带 \`ms\` 单位)。
297+
*
298+
* Why:消费方(守卫 / GSAP / 文档回写)要的正是「变量全名 ↔ 定值」这一层;两份数据同源派生,
299+
* 故合并名册不可能与上面两份分叉。
300+
*/
301+
export const MOTION_TOKENS = [
302+
${motionRows}
303+
] as const;
304+
305+
export type MotionTokenName = (typeof MOTION_TOKENS)[number]["name"];
306+
217307
export const THEMES = ["light", "dark"] as const;
218308
export type ThemeName = (typeof THEMES)[number];
219309
`;

‎app/src/ui/primitives/Toast.style.test.ts‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* —— transition 只许出现在 `opacity` / `transform` 上;高度/内边距/位置过渡会触发重排,
1111
* 也会让多条 toast 的堆叠逐帧抖动。第二个 `it` 用**变异样本**证明该判据有区分度。
1212
* ② 进出场时长逐字取 `--ed-dur-toast-in/out`(180 / 140,**出场比进场快**)且各带同值兜底字面量
13-
* —— 批 6 的动效 token 真源落地后删 `motion.css` 的变量块即生效。
13+
* —— 批 6 T5 已把真源迁进生成器并删掉 `motion.css` 的变量块(兜底字面量使删块即生效)。
1414
* ③ 与其它原语同向的纪律:类名 ↔ CSS 规则一致 · 零颜色字面量 · 零 `z-index`(层级是 TS 标尺的
1515
* 职责)· `--ed-stamp` 不进任何 `background` 声明(规格 §4.1「绝不用于按钮」)。
1616
* ④ **退场时长的两个真源对拍**(T9 评审 I-3):`Toast.tsx` 的 `EXIT_MS` 与 `motion.css` 的
@@ -32,7 +32,8 @@ const stripComments = (s: string): string => s.replace(/\/\*[\s\S]*?\*\//g, "");
3232

3333
const CLEAN = stripComments(read("Toast.css"));
3434
const TSX = read("Toast.tsx");
35-
const MOTION_CSS = read("motion.css");
35+
/** 批 6 T5 起,时长 token 的**唯一真源产物**是 `ui/tokens.css`(`motion.css` 的临时变量块已删) */
36+
const TOKENS_CSS = read("../tokens.css");
3637

3738
/** 颜色字面量(与 `style-seams.test.ts` 同一口径:本层不得出现,颜色只经 `var(--ed-*)`) */
3839
const COLOR_LITERAL = /#[0-9a-fA-F]{3,8}\b|\b(?:rgb|hsl)a?\(/;
@@ -113,20 +114,21 @@ describe("③ 纪律判据(与批 0-D 其它原语同向)", () => {
113114

114115
describe("④ 退场时长的两个真源对拍(T9 评审 I-3)", () => {
115116
/**
116-
* `Toast.tsx` 的 `EXIT_MS` 与 `motion.css` 的 `--ed-dur-toast-out` 是**并列的真源**:前者还是
117+
* `Toast.tsx` 的 `EXIT_MS` 与 `tokens.css` 的 `--ed-dur-toast-out` 是**并列的真源**:前者还是
117118
* `usePresence` 兜底窗口的一半(`EXIT_MS + timeoutSlackMs(80)` = 220ms)。若将来有人只把 token
118119
* 调大(如 260ms)而 `EXIT_MS` 不动 ⇒ 名义过渡比兜底还长,**退场演到一半就被兜底计时器摘掉**,
119120
* 而没有任何断言会红。故把两者钉成同一个数字。
120121
*
121122
* 边界(本条**不覆盖**):① 只判"两个数字相等",不判 CSS 是否真的引用了该变量(那是 ② 的职责);
122123
* ② `usePresence` 的 slack 默认值(80)不在此判据内 —— 改它须连 `usePresence.test.tsx` 一起改;
123-
* ③ 若批 6 把时长搬进 token 真源并删掉 `motion.css` 的变量块,本条会红,届时按新的唯一真源改写。
124+
* ③ 批 6 T5 已把时长搬进 token 真源(`gen-tokens.mjs` ⇒ `tokens.css`)并删掉 `motion.css` 的变量块 ⇒
125+
* 本条的源已随之改读 `tokens.css`(**判据本体的两个数字与不变式一字未改**,只换真源文件)。
124126
*/
125127
it("`const EXIT_MS` == `--ed-dur-toast-out`(否则兜底窗口会把退场中途摘掉)", () => {
126128
const exitMs = /const EXIT_MS = (\d+);/.exec(TSX)?.[1];
127-
const tokenMs = /--ed-dur-toast-out:\s*(\d+)ms;/.exec(MOTION_CSS)?.[1];
129+
const tokenMs = /--ed-dur-toast-out:\s*(\d+)ms;/.exec(TOKENS_CSS)?.[1];
128130
expect(exitMs, "Toast.tsx 缺 `const EXIT_MS = <n>;`").toBeDefined();
129-
expect(tokenMs, "motion.css 缺 `--ed-dur-toast-out: <n>ms;`").toBeDefined();
131+
expect(tokenMs, "生成物 tokens.css 缺 `--ed-dur-toast-out: <n>ms;`").toBeDefined();
130132
expect(Number(exitMs), "退场名义时长必须与 --ed-dur-toast-out 同值(兜底窗口 = EXIT_MS + 80)").toBe(Number(tokenMs));
131133
});
132134
});

‎app/src/ui/primitives/motion.css‎

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -34,26 +34,6 @@
3434
* 必须先把 `style` 摘掉,否则改类不会有观感变化。
3535
*/
3636

37-
:root {
38-
/* 响应层(<200ms):单属性、无时序的交互回执 ⇒ 一律 CSS transition(规格 §8.2 判据) */
39-
--ed-dur-micro: 120ms;
40-
/* 弹层遮罩与面板进出场:出场比进场快(规格 §8.4「进出场:弹层 200/160」) */
41-
--ed-dur-overlay-in: 200ms;
42-
--ed-dur-overlay-out: 160ms;
43-
/* Toast 进出场(规格 §8.4「Toast 180/140」) */
44-
--ed-dur-toast-in: 180ms;
45-
--ed-dur-toast-out: 140ms;
46-
/* 循环环境动效:骨架微光(规格 §8.4「--dur-skeleton 1200ms」) */
47-
--ed-dur-skeleton: 1200ms;
48-
/* 面板 / 视图 / 列折叠(规格 §8.4「--dur-card 220ms」) */
49-
--ed-dur-card: 220ms;
50-
/* 显影 / 编排层(规格 §8.4「--dur-reveal 500ms」) */
51-
--ed-dur-reveal: 500ms;
52-
/* 页面切换(规格 §8.4「--dur-page 150ms」) */
53-
--ed-dur-page: 150ms;
54-
/* 缓动:全站唯一曲线(规格 §8.4「--ease cubic-bezier(0.2,0,0,1)」) */
55-
--ed-ease: cubic-bezier(0.2, 0, 0, 1);
56-
}
5737

5838
/* ★ 名单 = 12 个 `.ed-*` **基类** + **每一处 `animation` 声明的选择器原文**(含伪元素)。
5939
⚠️ 为什么伪元素必须逐字列出(T11 评审用 headless Chromium `--force-prefers-reduced-motion` 实测的 Critical):

‎app/src/ui/primitives/style-contract.test.ts‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
* 本文件用 `Record<Union, …>` 全枚举(**编译期双向**)+ 档数冗余(**运行期**)钉住它。
1010
* ③ **退场时长三方对拍**:JS 兜底窗口与 CSS 过渡时长漂移时不会有任何报错(T9 评审 I-3)。
1111
*
12-
* 副作用:只读磁盘(同目录),不修改任何文件。**只 import 类型**(`import type`)—— 运行时不加载任何
13-
* React 组件 / `.css`,故本文件留在 vitest 的默认 node 环境(无 DOM 也能跑)。
12+
* 副作用:只读磁盘(同目录 + 唯一真源产物 `ui/tokens.css`),不修改任何文件。组件与样式表**只 import
13+
* 类型**(`import type`)—— 运行时不加载它们,故本文件留在 vitest 的默认 node 环境(无 DOM 也能跑)。
1414
*
1515
* 边界:① 判据只覆盖 `app/src/ui/primitives/` 一层(其余 `.ed-*` 名属既有代码,见 `style-seams.test.ts`
1616
* 的 `NON_PRIMITIVE_ED_NAMES`);② 判据前**先剥注释**、先归一 EOL(本仓无 `.gitattributes` 且
@@ -208,16 +208,16 @@ describe("契约完整性锚:类型联合 ↔ CSS 类规则(给联合加一
208208
* 退场时长**三方对拍**(T9 评审 I-3 的通用化 —— 原来只有 Toast 一条无判据):
209209
* JS 侧兜底窗口(`usePresence` 的 `exitMs`)与 CSS 侧过渡时长**必须是同一个数**,否则不会有任何报错:
210210
* JS 早了 = 过渡被截断(看不见出场);JS 晚了 = 节点多残留一截(那段时间还能被点到,正是退场误触面)。
211-
* 三方 = 组件常量 `EXIT_MS` / 组件 CSS 的 `var(--ed-…, <n>ms)` 兜底字面量 / `motion.css` 的定值。
211+
* 三方 = 组件常量 `EXIT_MS` / 组件 CSS 的 `var(--ed-…, <n>ms)` 兜底字面量 / 唯一真源产物 `tokens.css` 的定值。
212212
* 新增原语时在表里加一行即可(表非空断言防"表被清空 = 守卫静默关掉")。
213213
*/
214214
const DURATION_PAIRS: ReadonlyArray<readonly [module: string, css: string]> = [
215215
["Modal.tsx", "Modal.css"],
216216
["Toast.tsx", "Toast.css"],
217217
];
218218

219-
describe("退场时长三方对拍:组件常量 == 组件 CSS 兜底字面量 == motion.css 定值", () => {
220-
const motion = stripComments(read("motion.css"));
219+
describe("退场时长三方对拍:组件常量 == 组件 CSS 兜底字面量 == tokens.css 定值", () => {
220+
const tokens = stripComments(read("../tokens.css"));
221221
it("`const EXIT_MS` 与 `[data-phase=\"exit\"]` 的 `transition-duration` token 逐条相等", () => {
222222
expect(DURATION_PAIRS.length).toBeGreaterThanOrEqual(2);
223223
for (const [mod, css] of DURATION_PAIRS) {
@@ -230,7 +230,7 @@ describe("退场时长三方对拍:组件常量 == 组件 CSS 兜底字面量
230230
expect(decl, `${css} 的退场规则必须写 \`transition-duration: var(--ed-dur-…, <n>ms)\``).not.toBeNull();
231231
const jsMs = Number(constant?.[1]);
232232
expect(jsMs, `${mod} 的 EXIT_MS 与 ${css} 的兜底字面量不一致(漂移不报错,只表现为截断或残留)`).toBe(Number(decl?.[2]));
233-
expect(motion, `motion.css 的 --${decl?.[1]} 定值必须等于 ${jsMs}ms`).toContain(`--${decl?.[1]}: ${jsMs}ms;`);
233+
expect(tokens, `tokens.css 的 --${decl?.[1]} 定值必须等于 ${jsMs}ms`).toContain(`--${decl?.[1]}: ${jsMs}ms;`);
234234
}
235235
});
236236
});

‎app/src/ui/primitives/style-seams.test.ts‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
* ① 字阶/字族的 `var(--ed-x, 兜底字面量)` 的兜底值必须等于真源 `SCALE_TOKENS`
77
* (生成器改档后不重跑本层,兜底值会静默退回旧档 —— 未定义变量不报错);
88
* ② `Text.tsx` 产出的每个类,`Text.css` 里必须有对应规则(类名拼错 = 静默无样式,编译期全绿);
9-
* ③ `motion.css` 的 10 个动效变量、**`app/src` 内唯一一条** reduced-motion 块,以及规格 §8.4 的
10-
* 「位移上限 8px」—— 批 6 要靠前两者删块接管,靠第三者不越界;
9+
* ③ 动效 token 的**唯一真源**(`tokens.css`,由 `scripts/gen-tokens.mjs` 生成;`motion.css` 里恒 0 条
10+
* `--ed-*` 定义 —— 批 6 已把临时接缝迁走)、**`app/src` 内唯一一条** reduced-motion 块,以及规格
11+
* §8.4 的「位移上限 8px」—— 批 6 靠前者钉真源与封回归,靠后两者守住覆盖与不越界;
1112
* ④ `Surface.css` 的**盒子口径**(控制方 2026-09-11 裁决):基类必须自带
1213
* `box-sizing: border-box` 且**只声明一次** —— `padded` + `bordered` + `width:100%` 在
1314
* content-box 下溢出 26px,而本仓**没有全局 CSS reset**,盒子原语不自己声明就没人声明。
@@ -23,6 +24,7 @@ import { dirname, join } from "node:path";
2324
import { fileURLToPath } from "node:url";
2425
import { describe, expect, it } from "vitest";
2526
import { SCALE_TOKENS } from "../tokens";
27+
import { DURATION_TOKENS, EASING_TOKENS, renderAll } from "../../../scripts/gen-tokens.mjs";
2628

2729
const HERE = dirname(fileURLToPath(import.meta.url));
2830
const normalizeEol = (s: string): string => s.replace(/\r\n/g, "\n");
@@ -159,9 +161,13 @@ describe("token 兜底字面量 == 真源(生成器改档后不重跑本层会
159161
});
160162
});
161163

162-
describe("motion.css 接缝契约(批 6 删块即接管,故名字与取值必须钉住)", () => {
164+
describe("动效 token 真源契约(批 6 起临时接缝已迁走 ⇒ 钉唯一真源 + 封回归路径)", () => {
163165
it("规格 §8.4 的 10 个动效变量落值,一个不多一个不少", () => {
164166
const clean = stripComments(MOTION_CSS);
167+
expect(
168+
clean.match(/--ed-[a-z0-9-]+\s*:/g) ?? [],
169+
"批 6 已把真源迁进 scripts/gen-tokens.mjs:motion.css 不得再有任何 --ed-* 定义",
170+
).toEqual([]);
165171
const expected: ReadonlyArray<readonly [string, string]> = [
166172
["--ed-dur-micro", "120ms"],
167173
["--ed-dur-overlay-in", "200ms"],
@@ -175,8 +181,11 @@ describe("motion.css 接缝契约(批 6 删块即接管,故名字与取值
175181
["--ed-dur-page", "150ms"],
176182
["--ed-ease", "cubic-bezier(0.2, 0, 0, 1)"],
177183
];
178-
for (const [name, value] of expected) expect(clean).toContain(`${name}: ${value};`);
179-
expect(clean.match(/--ed-[a-z0-9-]+\s*:/g)).toHaveLength(expected.length);
184+
const source = stripComments(renderAll().css);
185+
for (const [name, value] of expected) expect(source).toContain(`${name}: ${value};`);
186+
// 真源侧计数:把「一个不多一个不少」钉在生成器的名单上(不再钉临时接缝)
187+
expect(DURATION_TOKENS).toHaveLength(9);
188+
expect(EASING_TOKENS).toHaveLength(1);
180189
});
181190

182191
it("规格 §8.4:原语 CSS 的位移一律 ≤ 8px(8px 是注释不变量,本条是它的机器判据)", () => {

0 commit comments

Comments
 (0)