Skip to content

Commit d5b7b64

Browse files
committed
feat(motion): 落环境层四件与 keyframes 桶边界
1 parent 5467a91 commit d5b7b64

8 files changed

Lines changed: 430 additions & 18 deletions

File tree

‎app/src/components/structuredBlocks.test.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ describe("renderMarkdownTable", () => {
8181
describe("lowConfidenceClass", () => {
8282
it("低置信(<0.5)返回标记类名,其余返回空串", () => {
8383
// Assert
84-
expect(lowConfidenceClass(0.3)).toBe("ed-low-confidence");
84+
// 批 6 T13(R12.1):类名由 `ed-low-confidence` 改为 `<既有基类>--<修饰>` 形状
85+
// (`ed-text` 基类的修饰类)—— 旧名字会被 motion-coverage.test.ts 的「未登记基类」判据拦下。
86+
expect(lowConfidenceClass(0.3)).toBe("ed-text--low-confidence");
8587
expect(lowConfidenceClass(0.5)).toBe("");
8688
expect(lowConfidenceClass(null)).toBe("");
8789
expect(lowConfidenceClass(undefined)).toBe("");

‎app/src/components/structuredBlocks.ts‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,17 @@ export function renderMarkdownTable(md: string): string {
5353
return `<table>${thead}${tbody}</table>`;
5454
}
5555

56-
/** 低置信样式类名(黄色虚线下划线;低置信/AI 占位渲染统一入口) */
56+
/**
57+
* 低置信样式类名(黄色虚线下划线;低置信/AI 占位渲染统一入口)。
58+
*
59+
* @ai-context 类名形状受 `ui/primitives/motion-coverage.test.ts` 的**选择器域约束**(批 6 T13 · 裁决 R12.1):
60+
* 逐字取 `<既有基类>--<修饰>` 形状(此处 = `Text` 原语的基类 `ed-text`)—— 该修饰类与基类同元素,
61+
* 于是被 reduced-motion 名单的基类条目覆盖,且不必新增基类名。
62+
* 🔴 **不得**改回 `ed-low-confidence`:那个名字落在任何基类域之外,会被上面那条守卫的「未登记基类」
63+
* 判据(`:113-124`)拦下;放行它要改三条既有断言(= G16 候选),控制方已裁定**不启用**。
64+
* 样式落点 = `ui/primitives/Text.css` 的 `.ed-text--low-confidence`(环境层第 ③ 件:墨度极缓慢起伏,
65+
* 幅度真源见 `motion/env.ts`);触发判据 = `SessionSegment.confidence` 的低置信段(阈值 `< 0.5`,不改)。
66+
*/
5767
export function lowConfidenceClass(confidence: number | null | undefined): string {
58-
return confidence != null && confidence < 0.5 ? "ed-low-confidence" : "";
68+
return confidence != null && confidence < 0.5 ? "ed-text--low-confidence" : "";
5969
}

‎app/src/motion/env.test.ts‎

Lines changed: 243 additions & 0 deletions
Large diffs are not rendered by default.

‎app/src/motion/env.ts‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* env.ts —— **环境层**(规格 §8.1「2–6s 循环|应用在呼吸」)的幅度真源与落点登记表(批 6 T13)。
3+
*
4+
* @ai-context 业务背景:四层纲领里,环境层是「闲置时也要有生命感」的载体(用户 2026-09-11 二次确认逐字
5+
* 「交互时最重要,**但闲置时也要有生命感(环境层别退太多)**」),四件逐字 = **探针摆动 · 采集脉冲 ·
6+
* 未确认段落墨度极缓慢起伏(幅度 2%)· 到期刻度微光**。引擎分界按 §8.2:循环环境动效一律走
7+
* **CSS keyframes**(不占 JS 主线程,reduced-motion 一条媒体查询即静态)⇒ 本模块**不产生运动**、
8+
* 不 import GSAP、不碰 DOM,只提供两样判据侧的事实:
9+
* ① **幅度真源**:`LOW_CONFIDENCE_INK_AMPLITUDE`(规格 §8.1 逐字 2%)。CSS 侧只写 keyframes 的**两端
10+
* 极值**(`1` 与 `0.98`),**不写** `0.02` —— 两处各写一个 0.02 就是第二个真源(R1.1);对拍由
11+
* `env.test.ts` 的 V4 负责(幅度只许有一个落点)。
12+
* ② **落点登记表**:keyframes 名 ↔ 落点选择器(逐序数组,供守卫比对)。落点一律取
13+
* `<既有基类>--<修饰>` 形状 —— `ui/primitives/motion-coverage.test.ts:113-124` 的选择器域只放行
14+
* 「修饰类 `--` / BEM 子元素 `__` / 既有基类」三种名字,新起一个 `.ed-*` 基类名会当场撞上那条既有守卫。
15+
*
16+
* 副作用:无 —— 纯数据常量,零 I/O、零 DOM、零 GSAP(可被首屏静态引用,不进 `vendor-gsap` 懒 chunk)。
17+
* 边界:① 只登记四件里的**三件新落点**;第 ① 件「探针摆动」复用 `Loading.css` 的既有 `.ed-probe` +
18+
* `ed-probe-swing`(G5:`Loading.css` 本批零改动)⇒ 不进本表;
19+
* ② **时长不在这里**:唯一真源 = `app/scripts/gen-tokens.mjs`。本层三条循环的周期从 `--ed-dur-skeleton`
20+
* (1200ms)以 `calc()` 倍数派生(故无新时长字面量);唯二例外是 `Loading.css:71` 那个未 token 化的
21+
* `2.4s`,已逐字登记在 `docs/standards/motion.md`;
22+
* ③ 档位节拍(eco 停 / rich 只提高频率)与 reduced-motion 名单都落在 `ui/primitives/motion.css`;
23+
* ④ 三条落点今天都**没有生产调用点**(LIVE 仪表 / 到期刻度 / 低置信段正文分别由波 B 的 T18、T21 与
24+
* T25/T28 接线)⇒ 本模块登记的是 **seam**,不得被读成「环境层已交付」(R11.3 / R12.4)。
25+
*/
26+
27+
/**
28+
* 未确认段落(低置信段)墨度起伏的**幅度** —— 规格 §8.1 逐字「幅度 2%」。
29+
*
30+
* Why 具名:motion.md 的「四层纲领」逐字要求「环境层的幅度上限(含 2% 的墨度起伏)**必须写成具名常量**,
31+
* 不许散落字面量」;`ui/primitives/motion.css` 的 `@keyframes ed-unconfirmed-breathe` 只写两端极值
32+
* (`0.98` / `1`),差值由 `env.test.ts` 与本常量对拍 —— 于是「2%」全仓只有一个数值落点。
33+
*/
34+
export const LOW_CONFIDENCE_INK_AMPLITUDE = 0.02;
35+
36+
/**
37+
* 未确认段落(低置信段)的落点类名 —— `Text` 原语基类 `ed-text` 的**修饰类**。
38+
*
39+
* 🔴 形状受 `ui/primitives/motion-coverage.test.ts` 的选择器域约束,**不得**改回旧的 `ed-low-confidence`
40+
* (那个名字不在任何基类域内,会被「未登记基类」判据拦下;要放行它就得改三条既有断言 = G16,
41+
* 控制方 R12.1 已裁定**不启用**)。生产调用点 = `components/structuredBlocks.ts` 的
42+
* `lowConfidenceClass()`(`SessionSegment.confidence < 0.5` 的段);样式落点 = `ui/primitives/Text.css`。
43+
*/
44+
export const LOW_CONFIDENCE_CLASS = "ed-text--low-confidence";
45+
46+
/** 环境层的一个循环落点:`@keyframes` 名 ↔ 声明它的选择器(选择器原文,含修饰类) */
47+
export interface AmbientItem {
48+
/** `@keyframes` 名(定义只许在 `ui/primitives/motion.css` —— 桶边界) */
49+
readonly keyframes: string;
50+
/** 声明 `animation:` 的选择器原文(必须逐字进 reduced-motion 名单) */
51+
readonly selector: string;
52+
}
53+
54+
/** 环境层 ②③④ 三件新落点(**逐序数组**:顺序 = 规格 §8.1 四件的顺序去掉第 ① 件)。
55+
* ⚠️ `selector` 一律是**选择器原文**(带前导 `.`);`LOW_CONFIDENCE_CLASS` 是类名本身(不带点)
56+
* —— 它是 `lowConfidenceClass()` 的返回值,两处口径不同、不可互相代入。 */
57+
export const AMBIENT_ITEMS: readonly AmbientItem[] = [
58+
{ keyframes: "ed-capture-pulse", selector: ".ed-status--live-pulse" },
59+
{ keyframes: "ed-unconfirmed-breathe", selector: `.${LOW_CONFIDENCE_CLASS}` },
60+
{ keyframes: "ed-due-glow", selector: ".ed-surface--due-glow" },
61+
];

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,15 @@
4444
.ed-text--font-mono { font-family: var(--ed-font-mono, "JetBrains Mono", Consolas, ui-monospace, monospace); font-variant-numeric: tabular-nums; }
4545

4646
.ed-text--truncate { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
47+
48+
/* 未确认段落(低置信段)的墨度极缓慢起伏 —— 环境层第 ③ 件(规格 §8.1 逐字「幅度 2%」· 裁决 R11.3)。
49+
为什么落在 `Text.css`:类名是 `ed-text` 基类的**修饰类**(`<既有基类>--<修饰>` 形状,
50+
🔴 受 `motion-coverage.test.ts` 的选择器域约束,**不得**改回 `ed-low-confidence`),与基类同元素
51+
⇒ 不进基类名单、只按「`animation` 声明逐字进名单」那条加进 `motion.css` 的 reduced-motion 块。
52+
`@keyframes` 在 `motion.css`(本层零 keyframes);幅度真源 = `app/src/motion/env.ts` 的
53+
`LOW_CONFIDENCE_INK_AMPLITUDE`(本文件只写两端极值,不写 0.02)。
54+
生产调用点 = `components/structuredBlocks.ts` 的 `lowConfidenceClass()`(`confidence < 0.5`)。 */
55+
.ed-text--low-confidence {
56+
animation: ed-unconfirmed-breathe calc(var(--ed-dur-skeleton, 1200ms) * 4)
57+
var(--ed-ease-paper, cubic-bezier(0.215, 0.61, 0.355, 1)) infinite;
58+
}

‎app/src/ui/primitives/motion-coverage.test.ts‎

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,11 @@ const BASE_CLASSES: ReadonlyArray<readonly [file: string, base: string]> = [
5858
/**
5959
* 5 个**现存非原语**的 `.ed-*` 名(T3 评审 M-4 实测)。它们不是 CSS 类 —— 是既有标识符里的子串:
6060
* `ed-desc`←`"updated-desc"` · `ed-label`←`note-link-linked-label` · `ed-note`←`…saved-note` ·
61-
* `ed-milestone-note`←`data-testid="degraded-milestone-note"` · `ed-low-confidence`←`structuredBlocks.ts:58`
62-
* 产出的类名串(其 `App.css` 规则已随 Task 13 删除)。任何「全树裸扫 `.ed-*`」的写法都会在这 5 个上假红。
61+
* `ed-milestone-note`←`data-testid="degraded-milestone-note"` · `ed-low-confidence`←**已退役**的名字:
62+
* 它是 `structuredBlocks.ts` 的 `lowConfidenceClass()` 在批 6 T13 之前产出的旧类名(其 `App.css` 规则
63+
* 已随批 0-D Task 13 删除),T13 按 R12.1 改成了 `ed-text--low-confidence`。本条**仍保留**这个旧名
64+
* (控制方 R12.1:不启用 G16 ⇒ 不改这几条既有断言)—— 它的作用从「锚住一个真存在的标识符」变成
65+
* 「锚住一个**绝不许再出现**在 CSS 选择器域里的名字」。任何「全树裸扫 `.ed-*`」的写法都会在这 5 个上假红。
6366
*/
6467
const NON_PRIMITIVE_ED_NAMES: readonly string[] = [
6568
"ed-desc",
@@ -149,12 +152,15 @@ describe("★ 每一处 `animation` 声明的选择器都在名单里(`animati
149152

150153
/* ── T6 追加段(R3.1):三档强度通道的**源序**与**值域闭合**。本段**只追加** —— 上面两条 describe
151154
* 与既有 import 一行未动;`:142` 的「每个 `animation` 选择器逐字进名单」是只读铁判据(G7)。
152-
* 为什么源序是判据而不是注释:§8.5 逐字要求「系统 `prefers-reduced-motion` 优先于档位」,而在同一张
153-
* 样式表里这**只能**靠源序实现(档位块先写、reduced-motion 块后写 ⇒ 后者的 `!important` 覆盖赢);
154-
* 把两块顺序对调,用户选的档位就会盖掉系统无障碍设置,且**没有任何其它判据会报错**。
155+
* 为什么源序是判据而不是注释:§8.5 逐字要求「系统 `prefers-reduced-motion` 优先于档位」。🔴 **机理订正
156+
* (R18.1,T6 评审 I-2 实测)**:真正承重的是 **reduced-motion 块的每条声明都带 `!important`**(档位块
157+
* 不带)—— 即使把两块顺序对调,reduced 仍然赢。所以本段守的**不是**优先级本身,而是一条**防御性钦定**:
158+
* 把「reduced 块带 `!important`、档位块不带」这两条前提的**相对位置**也钉住,于是将来有人拿掉
159+
* `!important`、或给档位块加上 `!important`(那会**反转**无障碍优先级),至少还有一道信号。
160+
* 真实机理自身的守卫在 `../../motion/responseSeams.test.ts` 的 I-2(逐条声明必须带 `!important`)。
155161
* 口径:注释先**就地掩码**(换行保留 ⇒ 报告里的行号与真实文件一致;`stripComments` 会连注释里的换行
156162
* 一起删掉、行号漂移);掩码后取「档位块首个选择器」与「reduced-motion 块起始」的**行号**比对。 */
157-
describe("★ 三档强度通道:源序(档位块在 reduced-motion 块之前)与值域闭合", () => {
163+
describe("★ 三档强度通道:源序(防御性钦定 —— 档位块写在 reduced-motion 块之前)与值域闭合", () => {
158164
const masked = read("motion.css").replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "));
159165
const tierAt = masked.search(/\[data-motion="(?:eco|standard|rich)"\]/);
160166
const reducedAt = masked.indexOf("@media (prefers-reduced-motion");
@@ -165,11 +171,11 @@ describe("★ 三档强度通道:源序(档位块在 reduced-motion 块之
165171
expect(reducedAt, "motion.css 里找不到 reduced-motion 块").toBeGreaterThanOrEqual(0);
166172
});
167173

168-
it("档位块行号 < reduced-motion 块行号(§8.5:系统设置永远赢过用户选的档)", () => {
174+
it("档位块行号 < reduced-motion 块行号(§8.5 的**防御性钦定**:真实承重机理是 reduced 块的 `!important`,见 R18.1)", () => {
169175
expect(tierAt >= 0 && reducedAt >= 0, "两块必须都在,否则本条是空真").toBe(true);
170176
expect(
171177
lineAt(tierAt),
172-
`档位块在第 ${lineAt(tierAt)} 行、reduced-motion 在第 ${lineAt(reducedAt)} 行 —— 顺序写反 = 用户选的档位盖掉系统无障碍设置`,
178+
`档位块在第 ${lineAt(tierAt)} 行、reduced-motion 在第 ${lineAt(reducedAt)} 行 —— 顺序写反 = 这条防御性钦定失效(优先级本身仍由 reduced 块的 !important 承担)`,
173179
).toBeLessThan(lineAt(reducedAt));
174180
});
175181

0 commit comments

Comments
 (0)