Skip to content

Commit d3357bd

Browse files
committed
feat(ui): 补规格 §8.4 剩余三个动效时长 token
1 parent 64822cd commit d3357bd

2 files changed

Lines changed: 62 additions & 9 deletions

File tree

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

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,39 @@
88
*
99
* ② 本文件是**全仓第一条 `prefers-reduced-motion` 块**(实测改造前 0 处,recon §8.1)。
1010
* 名单一次写全 11 个 `.ed-*` 基类(含本批后面才实现的选择器 —— 未实现的选择器无害),
11-
* 换来「后续任务永不改本文件」;Task 14 的样式守卫断言每个 `.ed-*` 类都在名单里。
11+
* 换来「后续任务永不改本文件」;Task 14 的样式守卫断言每个 `.ed-*` **基类**都在名单里
12+
* (**T14 判据 = 基类名单,不含修饰类** —— `.ed-text--s4` 这类修饰类与基类同在一个元素上,
13+
* 已被同一条规则覆盖,字面枚举修饰类会假红。控制方 2026-09-11 拍定)。
1214
* 无此项时 reduced-motion 用户仍会看到全部动效(§8.6.1 第 4 条:无障碍优先于「活」)。
1315
*
14-
* 边界:本文件**不得出现任何颜色字面量**(颜色只在 ui/tokens.css 与生成器里);
15-
* 也不得写 `z-index`(层级是 TS 标尺 `ui/zIndex.ts` 的职责,规格 §4.2①)。
16+
* ③ 边界:本文件**不得出现任何颜色字面量**(颜色只在 ui/tokens.css 与生成器里);
17+
* 也不得写 `z-index`(层级是 TS 标尺 `ui/zIndex.ts` 的职责,规格 §4.2①)。
18+
*
19+
* ④ **位移上限不变量(规格 §8.4 末句:位移上限 8px)**:原语层任何动效的位移都 ≤ 8px,
20+
* 批 6 的 GSAP 时间线同样不得越过该上限(编排层的「远」靠时长与错开表达,不靠位移)。
21+
* 这条**故意不做成 CSS 变量**:位移的落点是 CSS `translate*()` 的字面量与批 6 的 JS 数值,
22+
* 没有消费者读它,做成变量就是个死变量(还会给人「已被强制」的错觉)。强制手段是
23+
* `style-seams.test.ts` 的机器判据:扫 `primitives/*.css` 的 `translate*()` 数值实参,>8px 即红。
1624
*/
1725

1826
:root {
27+
/* 响应层(<200ms):单属性、无时序的交互回执 ⇒ 一律 CSS transition(规格 §8.2 判据) */
1928
--ed-dur-micro: 120ms;
29+
/* 弹层遮罩与面板进出场:出场比进场快(规格 §8.4「进出场:弹层 200/160」) */
2030
--ed-dur-overlay-in: 200ms;
2131
--ed-dur-overlay-out: 160ms;
32+
/* Toast 进出场(规格 §8.4「Toast 180/140」) */
2233
--ed-dur-toast-in: 180ms;
2334
--ed-dur-toast-out: 140ms;
35+
/* 循环环境动效:骨架微光(规格 §8.4「--dur-skeleton 1200ms」) */
2436
--ed-dur-skeleton: 1200ms;
37+
/* 面板 / 视图 / 列折叠(规格 §8.4「--dur-card 220ms」) */
38+
--ed-dur-card: 220ms;
39+
/* 显影 / 编排层(规格 §8.4「--dur-reveal 500ms」) */
40+
--ed-dur-reveal: 500ms;
41+
/* 页面切换(规格 §8.4「--dur-page 150ms」) */
42+
--ed-dur-page: 150ms;
43+
/* 缓动:全站唯一曲线(规格 §8.4「--ease cubic-bezier(0.2,0,0,1)」) */
2544
--ed-ease: cubic-bezier(0.2, 0, 0, 1);
2645
}
2746

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

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@
66
* ① 字阶/字族的 `var(--ed-x, 兜底字面量)` 的兜底值必须等于真源 `SCALE_TOKENS`
77
* (生成器改档后不重跑本层,兜底值会静默退回旧档 —— 未定义变量不报错);
88
* ② `Text.tsx` 产出的每个类,`Text.css` 里必须有对应规则(类名拼错 = 静默无样式,编译期全绿);
9-
* ③ `motion.css` 的 7 个动效变量与**全仓唯一一条** reduced-motion 块 —— 批 6 要靠它们删块接管。
9+
* ③ `motion.css` 的 10 个动效变量、**全仓唯一一条** reduced-motion 块,以及规格 §8.4 的
10+
* 「位移上限 8px」—— 批 6 要靠前两者删块接管,靠第三者不越界。
1011
*
11-
* 副作用:只读磁盘(同目录三份文件),不修改任何文件。
12+
* 副作用:只读磁盘(同目录 + `primitives/` 下的 `.css`),不修改任何文件。
1213
* 边界:口径与 `tokens.drift.test.ts` 一致 —— **必须归一 EOL**(本仓无 `.gitattributes` 且
1314
* `core.autocrlf=true`,逐字节断言会在别人机器上假阳性);判据前先**剥掉注释**(注释里提到
14-
* `--ed-dur-x` / 颜色名不该让守卫误报)。颜色只做「本层不得出现字面量」的反例守门,
15-
* 不重述 token 真源(那是 `contrast.test.ts` / `tokens.drift.test.ts` 的职责)。
15+
* `--ed-dur-x` / 颜色名 / `translateY(12px)` 这类反例不该让守卫误报)。颜色只做「本层不得出现
16+
* 字面量」的反例守门,不重述 token 真源(那是 `contrast.test.ts` / `tokens.drift.test.ts` 的职责)。
1617
*/
17-
import { readFileSync } from "node:fs";
18+
import { readdirSync, readFileSync } from "node:fs";
1819
import { dirname, join } from "node:path";
1920
import { fileURLToPath } from "node:url";
2021
import { describe, expect, it } from "vitest";
@@ -46,6 +47,27 @@ const MOTION_CLASSES: readonly string[] = [
4647

4748
const COLOR_LITERAL = /#[0-9a-fA-F]{3,8}\b|\b(?:rgb|hsl)a?\(/;
4849

50+
/** 规格 §8.4 末句:位移上限 8px(响应层/编排层/环境层一律适用;批 6 的 GSAP 也不得越过) */
51+
const SHIFT_MAX_PX = 8;
52+
53+
/**
54+
* 取一段 CSS 里所有 `translate*()` 的**数值实参**(px/rem)并换算成 px。
55+
* 百分比(`translate(-50%, -50%)` 居中技巧)、`calc()`、`var()` 一律跳过 —— 它们不是「位移量」,
56+
* 而本条判据要守的正是「位移量 ≤ 8px」这一个规范硬数字。
57+
*/
58+
function shiftViolations(css: string, file: string): string[] {
59+
const out: string[] = [];
60+
for (const m of stripComments(css).matchAll(/translate(?:3d|X|Y|Z)?\(([^)]*)\)/g)) {
61+
for (const arg of m[1].split(",")) {
62+
const px = /^\s*(-?\d+(?:\.\d+)?)(px|rem)\s*$/.exec(arg);
63+
if (!px) continue;
64+
const value = px[2] === "rem" ? Math.abs(Number(px[1])) * 16 : Math.abs(Number(px[1]));
65+
if (value > SHIFT_MAX_PX) out.push(`${file}: ${m[0]} = ${value}px > ${SHIFT_MAX_PX}px`);
66+
}
67+
}
68+
return out;
69+
}
70+
4971
describe("Text 类名 ↔ CSS 规则一致(类名拼错 = 静默无样式)", () => {
5072
it("Text.tsx 产出的每个类在 Text.css 里都有规则", () => {
5173
for (const cls of TEXT_CLASSES) {
@@ -95,7 +117,7 @@ describe("token 兜底字面量 == 真源(生成器改档后不重跑本层会
95117
});
96118

97119
describe("motion.css 接缝契约(批 6 删块即接管,故名字与取值必须钉住)", () => {
98-
it("规格 §8.4 的 7 个动效变量落值,一个不多一个不少", () => {
120+
it("规格 §8.4 的 10 个动效变量落值,一个不多一个不少", () => {
99121
const clean = stripComments(MOTION_CSS);
100122
const expected: ReadonlyArray<readonly [string, string]> = [
101123
["--ed-dur-micro", "120ms"],
@@ -104,12 +126,24 @@ describe("motion.css 接缝契约(批 6 删块即接管,故名字与取值
104126
["--ed-dur-toast-in", "180ms"],
105127
["--ed-dur-toast-out", "140ms"],
106128
["--ed-dur-skeleton", "1200ms"],
129+
// 控制方 2026-09-11 裁决补入:批 6 的编排层 / 页面切换不得重新硬编码这三个
130+
["--ed-dur-card", "220ms"],
131+
["--ed-dur-reveal", "500ms"],
132+
["--ed-dur-page", "150ms"],
107133
["--ed-ease", "cubic-bezier(0.2, 0, 0, 1)"],
108134
];
109135
for (const [name, value] of expected) expect(clean).toContain(`${name}: ${value};`);
110136
expect(clean.match(/--ed-[a-z0-9-]+\s*:/g)).toHaveLength(expected.length);
111137
});
112138

139+
it("规格 §8.4:原语 CSS 的位移一律 ≤ 8px(8px 是注释不变量,本条是它的机器判据)", () => {
140+
const files = readdirSync(HERE).filter((f) => f.endsWith(".css"));
141+
// 防空目录把守卫静默关掉(同 zIndex.guard.test.ts 的「名单非空」思路)
142+
expect(files.length, "primitives/ 下应有 Text.css 与 motion.css").toBeGreaterThanOrEqual(2);
143+
const violations = files.flatMap((f) => shiftViolations(read(f), f));
144+
expect(violations, `位移超过规格 §8.4 的 8px 上限:\n${violations.join("\n")}`).toEqual([]);
145+
});
146+
113147
it("全仓唯一一条 reduced-motion 块,覆盖 11 个 `.ed-*` 基类(含 transition 与 animation 两条)", () => {
114148
const clean = stripComments(MOTION_CSS);
115149
expect(clean.match(/@media \(prefers-reduced-motion: reduce\)/g)).toHaveLength(1);

0 commit comments

Comments
 (0)