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
2932const TEXT_CSS = read ( "Text.css" ) ;
3033const MOTION_CSS = read ( "motion.css" ) ;
34+ const SURFACE_CSS = read ( "Surface.css" ) ;
3135const 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 ( / b o x - s i z i n g \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 - i n d e x / ) ;
123+ } ) ;
124+ } ) ;
125+
84126describe ( "token 兜底字面量 == 真源(生成器改档后不重跑本层会静默漂移)" , ( ) => {
85127 it ( "18 个 `--ed-type-N-{size,line,weight}` 的兜底值逐条等于 SCALE_TOKENS.typeScaleVars" , ( ) => {
86128 expect ( SCALE_TOKENS . typeScaleVars ) . toHaveLength ( 6 ) ;
0 commit comments