Skip to content

Commit e5969f0

Browse files
committed
fix(ui): Surface 基类补 box-sizing: border-box
1 parent 4eb5c46 commit e5969f0

3 files changed

Lines changed: 58 additions & 6 deletions

File tree

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,24 @@
77
*
88
* 边界:① 颜色 / 圆角 / 阴影 / 间距一律 `var(--ed-*)`,**本文件零颜色字面量**(`var()` 的兜底
99
* 值也不许是色值 —— 色值只在 ui/tokens.css 与生成器里);② 不得写 `z-index`(层级走
10-
* `ui/zIndex.ts` 六档标尺,规格 §4.2①);③ **不写 `box-sizing`**:尺寸口径属调用点排版,
11-
* 本原语只负责「底 / 边框 / 圆角 / 阴影」四件事(批 4 逐处迁移时按各处实际需要决定,
12-
* 避免原语替调用点改掉盒模型而让迁移期的观感对不上)。
10+
* `ui/zIndex.ts` 六档标尺,规格 §4.2①);③ **基类声明 `box-sizing: border-box`**(控制方
11+
* 2026-09-11 裁决,理由见下方 `.ed-surface` 规则上方的注释);档位修饰符一律**不重复声明**
12+
* 它,由 `style-seams.test.ts` 的「Surface 盒子契约」机器守。
1313
*
1414
* 暗档策略(继承 token 层,本文件不写第二条规则):`--ed-shadow-1` 在 `[data-theme="dark"]` 里
1515
* 是 `0 0 0 1px rgba(255,255,255,.06)` 的**反相白描边**(暗底上黑色投影不可见)⇒ hover 的
1616
* 「升起」在暗档自动退化为**边框墨度**的推进(`--ed-border` → `--ed-border-strong`)。
1717
*/
1818

19+
/* 基类:底 + 圆角 + 盒模型口径。
20+
⚠️ `box-sizing: border-box` **必须在基类声明**(控制方 2026-09-11 裁决):`padded`(12px)+
21+
`bordered`(1px)+ 调用点写 `width:100%` 时,content-box 下会**溢出 26px**(12×2 + 1×2)
22+
—— 这是真缺陷,不是「等批 4 再说」的风格偏好。本仓 `app/src` 只有 `tokens.css` /
23+
`note-mark.css`,**没有全局 CSS reset** ⇒ 「盒子原语自己声明盒模型」是唯一可靠做法;否则
24+
批 4 迁移 20 个弹层时才发现溢出,会变成逐处补样式的返工。档位修饰符(`--<level>` / `--r-*` /
25+
布尔开关)**不再重复声明**它(各档各自声明会漂移,让「一处改对所有面」失效)。 */
1926
.ed-surface {
27+
box-sizing: border-box;
2028
background: var(--ed-bg-surface);
2129
border-radius: var(--ed-radius-panel, 8px);
2230
}

‎app/src/ui/primitives/Surface.tsx‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@
1818
* 逐处改);`style` 只是透传,供批 6 的接缝(直接驱动 `transform` / `boxShadow`)与调用点
1919
* 布局使用;② `interactive` 只给视觉,**不加 `tabIndex` / `role`** —— 键盘可达性由消费方
2020
* 用真实 `<button>` / `<a>` 承载(§8.6.1 第 4 条:键盘路径优先于「活」);③ `bordered`
21-
* 默认 `true`,`bordered={false}` 用于「只有底色、不要分隔线」的阅读面;④ 不写 `box-sizing`
22-
* (尺寸口径属调用点排版,见 `Surface.css` 的边界注释)。
21+
* 默认 `true`,`bordered={false}` 用于「只有底色、不要分隔线」的阅读面;④ 盒模型口径由**基类
22+
* 自带**(`box-sizing: border-box`,控制方 2026-09-11 裁决 —— 本仓无全局 CSS reset,
23+
* `padded` + `bordered` + `width:100%` 在 content-box 下会溢出 26px;理由详见
24+
* `Surface.css` 的基类注释),调用点不必再自己补。
2325
*/
2426

2527
import type { CSSProperties, ReactElement, ReactNode } from "react";

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

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
* (生成器改档后不重跑本层,兜底值会静默退回旧档 —— 未定义变量不报错);
88
* ② `Text.tsx` 产出的每个类,`Text.css` 里必须有对应规则(类名拼错 = 静默无样式,编译期全绿);
99
* ③ `motion.css` 的 10 个动效变量、**全仓唯一一条** reduced-motion 块,以及规格 §8.4 的
10-
* 「位移上限 8px」—— 批 6 要靠前两者删块接管,靠第三者不越界。
10+
* 「位移上限 8px」—— 批 6 要靠前两者删块接管,靠第三者不越界;
11+
* ④ `Surface.css` 的**盒子口径**(控制方 2026-09-11 裁决):基类必须自带
12+
* `box-sizing: border-box` 且**只声明一次** —— `padded` + `bordered` + `width:100%` 在
13+
* content-box 下溢出 26px,而本仓**没有全局 CSS reset**,盒子原语不自己声明就没人声明。
1114
*
1215
* 副作用:只读磁盘(同目录 + `primitives/` 下的 `.css`),不修改任何文件。
1316
* 边界:口径与 `tokens.drift.test.ts` 一致 —— **必须归一 EOL**(本仓无 `.gitattributes` 且
@@ -28,6 +31,7 @@ const stripComments = (s: string): string => s.replace(/\/\*[\s\S]*?\*\//g, "");
2831

2932
const TEXT_CSS = read("Text.css");
3033
const MOTION_CSS = read("motion.css");
34+
const SURFACE_CSS = read("Surface.css");
3135
const INDEX_TS = read("index.ts");
3236

3337
/** `Text.tsx` 产出的类名契约(与 `Text.test.tsx` 的名单同源;改名单必须同时改 CSS 与两份测试) */
@@ -81,6 +85,44 @@ describe("Text 类名 ↔ CSS 规则一致(类名拼错 = 静默无样式)",
8185
});
8286
});
8387

88+
/** `Surface.tsx` 产出的 12 个类名(与 `Surface.test.tsx` 的名单同源) */
89+
const SURFACE_CLASSES: readonly string[] = [
90+
"ed-surface",
91+
...["sunken", "canvas", "surface", "raised"].map((l) => `ed-surface--${l}`),
92+
...["stamp", "control", "panel", "overlay"].map((r) => `ed-surface--r-${r}`),
93+
"ed-surface--bordered",
94+
"ed-surface--interactive",
95+
"ed-surface--padded",
96+
];
97+
98+
describe("Surface 盒子契约(控制方 2026-09-11 裁决 · 类名 ↔ CSS 一致)", () => {
99+
it("基类必须自带 `box-sizing: border-box`(padded+bordered+width:100% 在 content-box 下溢出 26px)", () => {
100+
const clean = stripComments(SURFACE_CSS);
101+
const start = clean.indexOf(".ed-surface {");
102+
expect(start, "Surface.css 缺少 `.ed-surface` 基类规则").toBeGreaterThanOrEqual(0);
103+
const base = clean.slice(start, clean.indexOf("}", start));
104+
expect(
105+
base,
106+
"`.ed-surface` 基类必须声明盒模型口径(本仓无全局 CSS reset,原语不声明就没人声明;批 4 迁移时才溢出 = 20 个弹层逐个补样式)",
107+
).toContain("box-sizing: border-box");
108+
});
109+
110+
it("盒模型口径只声明一次(档位修饰符不得各自声明 —— 各档漂移会让「一处改对所有面」失效)", () => {
111+
const clean = stripComments(SURFACE_CSS);
112+
expect(clean.match(/box-sizing\s*:/g)).toHaveLength(1);
113+
});
114+
115+
it("Surface.tsx 产出的每个类在 Surface.css 里都有规则;且零颜色字面量、无 z-index", () => {
116+
const clean = stripComments(SURFACE_CSS);
117+
expect(SURFACE_CLASSES).toHaveLength(12);
118+
for (const cls of SURFACE_CLASSES) {
119+
expect(clean, `Surface.css 缺少 .${cls} 的规则`).toContain(`.${cls} {`);
120+
}
121+
expect(clean, "Surface.css 出现颜色字面量(色值只在 ui/tokens.css 与生成器里)").not.toMatch(COLOR_LITERAL);
122+
expect(clean, "Surface.css 写了 z-index(层级是 ui/zIndex.ts 标尺的职责)").not.toMatch(/z-index/);
123+
});
124+
});
125+
84126
describe("token 兜底字面量 == 真源(生成器改档后不重跑本层会静默漂移)", () => {
85127
it("18 个 `--ed-type-N-{size,line,weight}` 的兜底值逐条等于 SCALE_TOKENS.typeScaleVars", () => {
86128
expect(SCALE_TOKENS.typeScaleVars).toHaveLength(6);

0 commit comments

Comments
 (0)