02 · 样式管线实测图(OneGUI 接管前的基线)¶
历史记录 / Historical record:本文保留当时的计划、研究或验收证据,不代表 0.11 当前能力全集。现行说明见 文档入口;英文入口见 English overview。
侦察范围:
packages/ui/src/*.css、apps/desktop/src/styles/*.css、apps/desktop/src/{state,lib,components}、tools/*.mjs。 排除node_modules/vendor/._*。所有计数为 2026-10-14 实测(grep -ro/node tools/*.mjs),非估计。
0. 总量与文件清单¶
| 文件 | 行数 | 规则块 | 备注 |
|---|---|---|---|
apps/desktop/src/styles/global.css |
8616 | 1753 | 单体巨石,占全仓 CSS 的 65% |
apps/desktop/src/styles/explorer-home.css |
1003 | 179 | 由 components/Layout.tsx:53 引入 |
apps/desktop/src/styles/explorer-ui.css |
941 | 195 | components/Layout.tsx:52 |
apps/desktop/src/styles/motion.css |
912 | 96 | main.tsx:55 |
apps/desktop/src/styles/explorer.css |
593 | 97 | components/Layout.tsx:51 |
apps/desktop/src/styles/explorer-oh.css |
484 | 72 | plugins/ChatDock.tsx:44 |
packages/ui/src/tokens.css |
263 | — | System + Compat 两层 |
packages/ui/src/palette.css |
221 | — | Reference 层,生成物 |
apps/desktop/src/styles/glass-levels.css |
112 | 6 | 玻璃四档 |
packages/ui/src/base.css |
76 | — | reset + 滚动条 |
| 合计 | 13221 | 2398 |
第三方 CSS 也进同一张表:quill.snow.css(RichEditor.tsx:2 + YktSubjectiveEditor.tsx:18)、leaflet.css(Trace.tsx:14)。
1. 令牌层:Reference / System / Compat¶
1.1 三层各自定义了什么¶
| 层 | 文件 | 前缀 | 定义数 | 谁消费 |
|---|---|---|---|---|
| Reference | palette.css:7-158 |
--md-ref-palette-* |
144(6 族 × 24 档:primary / secondary / tertiary / neutral / neutral-variant / error,档位 0,4,6,10,12,17,20,22,24,30,40,50,60,70,80,87,90,92,94,95,96,98,99,100) | 0 处。全仓无一处 var(--md-ref-palette-*) 真引用,只有 tokens.css:13,53,54 的注释示例。token-guard 把这 144 个单独归类输出「Reference 层预留 144 个 tone」 |
| System | tokens.css:18-101(亮)+ :217-263(暗) |
--md-sys-color-* / --md-sys-shape-* / --md-sys-typescale-* / --md-sys-elevation-* / --md-sys-state-opacity-disabled / --md-sys-focus-ring |
79(亮 45 + 暗 34) | 316 处(见 1.3) |
| Compat | tokens.css:108-206 |
--bg --surface* --border* --text-* --primary* --accent* --red/amber/green* --hover/--active --elev-* --shadow-* --ring --font-* --text-{xxs,xs,sm,base,md,lg,xl} --gap-{1..6} --r-* --sidebar-w |
56 个唯一名 / 59 条声明 | 1919 处(见 1.3) |
| 别名 | tokens.css:112-120 |
--dsw-alias-* |
9(bg-base bg-layer-1/2 border-l1/l2 label-primary/secondary brand-primary state-error-primary) |
样式表 34 处 + TSX 43 处(见 §8 风险 7) |
1.2 层的纪律(tokens.css:9-14)¶
① 组件只准用 Compat 旧名或 System 名,不准写死颜色字面量;② 改外观改令牌文件,改结构改组件; ③ System 层初值 = 「改造前实测值」基线,因此重写不改变观感。
1.3 应用到底吃哪一层(实测引用数)¶
| 消费面 | var() 总数 |
Compat | System(--md-sys-*) |
Reference | 玻璃令牌(--glass-*/--lg-*) |
动效令牌(--dur-*/--ease-*/…) |
|---|---|---|---|---|---|---|
apps/desktop/src/styles/*.css |
3079 | 1919 (62%) | 316 (10%) | 0 | 443 (14%) | 266 |
apps/desktop/src/**/*.{ts,tsx} |
713 | ~560 (79%) | 18 (3%) | 0 | ~30 | ~60 |
packages/ui/src/*.css |
内部互引(--surface → --glass-card-fill 等) |
— | — | 0 | — | 0 |
结论:管道事实上的主干是 Compat 层,System 层只在迁移过的组件里(按钮/chip/开关/弹窗/滚动条)小范围生效。 分文件看迁移梯度:
| 文件 | System 引用 | Compat 引用 | 玻璃引用 | 迁移度 |
|---|---|---|---|---|
global.css |
263 | 1561 | 191 | 新组件走 System,老区块仍 Compat |
explorer-ui.css |
18 | 75 | 57 | 半迁 |
explorer.css |
11 | 41 | 42 | 半迁 |
explorer-home.css |
6 | 169 | 97 | 几乎全 Compat |
explorer-oh.css |
2 | 58 | 52 | 几乎全 Compat |
motion.css |
16 | 15 | 1 | 只引动效 System 名 |
Compat 层最热的名字(styles/*.css 内):--text-3 244、--border 140、--accent 146、--text-1 195、
--r-pill 67、--border-soft 59、--surface-2 51、--r-md 50、--surface-3 49、--surface 46、
--accent-soft 46、--red 46、--accent-border 43。
零引用(僵尸): --elev-1/2/3、--gap-5、--gap-6、--primary-hover、--r-xs(1)、--text-dim(5)。
1.4 已知的令牌层缺陷(OneGUI 必须先修)¶
| 缺陷 | 位置 | 后果 |
|---|---|---|
--surface / --surface-2 / --surface-3 在同一个 :root 里各声明两次,后声明覆盖前者 |
tokens.css:126-128 指向 --glass-card-fill*,tokens.css:130-132 又指回 --md-sys-color-surface-container-* |
「Compat 表面 = 玻璃 tint」这条 2026-10-13 的设计语言换代实际上没有生效。玻璃效果靠 global.css:8196-8199 把 System 四个 container 名重写成 rgba 才勉强成立 |
表面填充有三个管理方:tokens.css、global.css:8196-8199、glass-levels.css:5-19 |
— | state/theme.ts:270-272 只好用 SURFACE_FAMILY 黑名单禁止主题注入这 5 个名字。谁都不是唯一真源 |
--md-sys-typescale-* 只有 5 个无后缀名(display/headline/title/body/label),消费端却写 --md-sys-typescale-headline-medium-size / -label-medium-size |
定义 tokens.css:97-101;引用 global.css(3+2 处) |
全靠 var(--x, 28px) 的 fallback 兜住,token-guard 归入「可选(带 fallback) 92 个」 |
--danger 从未定义 |
tokens.css:120 --dsw-alias-state-error-primary: var(--danger, #e5484d) |
靠 fallback 活着 |
本地私有令牌 98 个(--ctl-* --fog-core --ws-color --home-* --explorer-bar-h --glass-*)散落在 global.css / explorer.css:7-13,不属于任何层 |
— | token-guard 只保证「有定义」,不保证「有归属」 |
2. 玻璃档位:styles/glass-levels.css 全解剖¶
2.1 档位与选择器¶
档名来自 lib/glassLevel.ts:10:clear | balanced | rich | deep(中文「清透 / 均衡 / 浓郁 / 极致」)。
balanced 是基座,不落属性(glassLevel.ts:47-52:level === "balanced" 时 delete root.dataset.glass),
所以 glass-levels.css 只有 3 个档 × 明暗 2 = 6 个选择器块:
| 选择器 | 行 | 生效条件 |
|---|---|---|
html[data-glass="clear"] |
:4 |
亮色 |
html[data-glass="clear"][data-scheme="dark"] |
:23 |
暗色 |
html[data-glass="rich"] |
:41 |
亮色 |
html[data-glass="rich"][data-scheme="dark"] |
:59 |
暗色 |
html[data-glass="deep"] |
:76 |
亮色 |
html[data-glass="deep"][data-scheme="dark"] |
:95 |
暗色 |
双轴:data-glass(程度,由 lib/glassLevel.ts 独占写)+ data-scheme(明暗,由 state/theme.ts:488 与
lib/dynamicColor.ts:syncScheme() 写)。data-wallpaper-lum 不参与玻璃档,它只驱动 explorer.css:295-412
的栏体文字色分支。
2.2 每档定义的全部令牌(共 21 个名字)¶
clear 亮色块定义 16 条:--surface --surface-2 --surface-3 --dsw-alias-bg-base --glass-{fill,fill-regular,tint,menu-fill,menu-blur,chip-blur,panel-blur,card-fill,card-fill-2,card-fill-3,shadow}
--lg-blur-clear。rich/deep 亮色块与三档暗色块额外写 4 个 --md-sys-color-surface-container-*
(:53-56 :89-92 :35-38 :71-74 :108-111)。
| 令牌 | clear 亮/暗 | rich 亮/暗 | deep 亮/暗 | 语义 |
|---|---|---|---|---|
--surface |
.30 / .06 | .60 / .14 | .20 / .20 | 三级面(→ --glass-card-fill 家族) |
--surface-2 |
.30 / .06 | .60 / .14 | .74 / .20 | 注意 deep 亮色 .74 是反向跳档 |
--surface-3 |
.30 / .06 | .60 / .14 | .74 / .20 | — |
--glass-card-fill |
.30 / .06 | .60 / .14 | .74 / .20 | 卡片面 |
--glass-card-fill-2 |
.30 / .06 | .60 / .14 | .20 / .20 | 二级卡面(deep 亮色贴底) |
--glass-card-fill-3 |
.30 / .06 | .60 / .14 | .74 / .20 | 样式表 0 消费者(僵尸) |
--glass-menu-fill |
.30 / .06 | .60 / .14 | .74 / .20 | 菜单/浮层 |
--glass-fill |
.30 / .06 | .60 / .14 | .20 / .20 | 通用填充 |
--glass-fill-regular |
.30 / .06 | .60 / .14 | .74 / .20 | 常规填充 |
--glass-tint |
.26 / .04 | .40 / .08 | .64 / .10 | 叠色 tint |
--glass-shadow |
有 / 有 | 无(沿用基座)/ 无 | 有 / 有 | 只有 clear/deep 覆盖 |
--glass-menu-blur --glass-chip-blur --glass-panel-blur |
只在 clear 覆写(22/20/20px) | — | — | 其余档沿用 global.css:8165,8170,8171 |
--lg-blur-clear |
clear 覆写 | — | — | 命名与档位错位(clear 名却给 clear 档用) |
--md-sys-color-surface-container{,-low,-high,-lowest} |
暗 .06 | 双档 .60/.14 | 双档 .74/.20 | System 名被档位劫持 |
2.3 每个令牌的消费者(按文件,var() 引用数)¶
| 令牌 | global.css | explorer-home | explorer-ui | explorer | explorer-oh | motion | 合计 |
|---|---|---|---|---|---|---|---|
--glass-rim-top |
23 | 11 | 9 | 4 | 6 | — | 53 |
--glass-menu-blur |
24 | 10 | 10 | 8 | — | — | 52 |
--glass-chip-blur |
19 | 12 | 4 | 2 | 8 | — | 45 |
--glass-border |
11 | 9 | 4 | 3 | 8 | — | 35 |
--glass-shadow |
13 | 8 | 6 | 5 | 3 | — | 35 |
--glass-panel-blur |
14 | 2 | — | — | 4 | — | 20 |
--glass-menu-fill |
6 | 5 | 5 | 4 | — | — | 20 |
--glass-card-fill-2 |
5 | 2 | — | 5 | — | — | 12 |
--lg-blur-clear |
4 | 4 | — | 2 | — | — | 10 |
--glass-fill-regular |
3 | 2 | 1 | — | 3 | — | 9 |
--glass-refract |
4 | — | — | — | — | — | 4(+ 定义 3 处) |
--glass-tint |
1 | — | 1 | 1 | — | — | 3 |
--glass-fill |
— | — | — | — | 1 | — | 1 |
--glass-card-fill |
— | — | — | 1 | — | — | 1 |
--glass-card-fill-3 |
— | — | — | — | — | — | 0 |
--surface(=档位) |
22 | 9 | 7 | 1 | 6 | 1 | 46 |
--surface-2 |
49 | 2 | — | — | — | — | 51 |
--surface-3 |
39 | 7 | 1 | 2 | — | — | 49 |
--md-sys-color-surface-container-lowest |
65 | — | 5 | — | 1 | — | 71 |
--dsw-alias-bg-base |
— | — | — | — | — | — | 0(只在档位块里被写,没人读) |
2.4 冲突源¶
explorer.css 另有一个 :root 块(:7,定义 --explorer-bar-h: 48px 等)加 6 个按 [data-wallpaper-lum]
分支的作用域令牌块(:295 :325 :356 :375 :557 :565)+ 2 个 [data-scheme] 分支(:574 :584),
重新定义 --glass-tint --glass-border --glass-rim-* --text-* --md-sys-color-* --border* --hover。
它们作用域挂在 .explorer-shell 上,特异性高于 html[data-glass] 与 :root,所以栏体/程序坞那一片
永远不跟档位走。explorer.css:376-378 的注释承认过一次同型事故:「此前写死 .44 导致程序坞在 clear/rich/deep 档下不跟档」。
3. 主题管线:state/theme.ts(706 行)¶
3.1 形态¶
主题 = 插件(manifest.category === "theme"),模块导出 ThemeDef(theme.ts:17-39)。
契约:只做令牌覆盖,不碰结构布局(theme.ts:5-7)。
| 字段 | 作用 |
|---|---|
vars |
Compat 令牌覆盖,允许任意 --x 键(只校验 /^--[\w-]+$/,theme.ts:462) |
fonts.{ui,mono} |
追加 --font-ui / --font-mono(:469-470) |
css |
受信任意 CSS,原样拼在注入样式尾部(:474) |
dark |
声明暗色:color-scheme: dark + data-scheme="dark"(:485-489) |
logo |
内联 SVG,只存快照给 UI 用(:490) |
source / owner |
builtin / plugin,插件卸载时按 owner 回收(removePluginThemes, :656-666) |
3.2 注入路径¶
bootstrap() :350-400 localStorage("onethu.theme.v1") → 播种/升级内置主题 → applyActive()
applyActive():495-501 followSystem ? (systemDark ? night : day) : activeId → applyTheme(def)
applyTheme() :440-492 ① data-dynamic="on" → 直接 return(取色优先) ② <html> 挂 .theme-anim(420ms)
③ 拼 CSS 文本 → <style id="onethu-theme-style"> ④ head.appendChild(style) ← 每次重挪末尾(:481)
⑤ root.dataset.theme = def.id / root.style.colorScheme
⑥ def.dark ? root.dataset.scheme="dark" : delete root.dataset.scheme
注入选择器::root[data-theme="<id>"](特异性 0,2,0),theme.ts:471-473 明确注释:靠特异性稳压
tokens.css 的 :root(0,1,0),因为「平级时输赢取决于文档顺序,vite 样式注入顺序不可依赖」。
3.3 Compat → System 镜像¶
COMPAT_TO_SYSTEM(theme.ts:274-316)把每条 Compat 覆盖同时写两份(:465-467):--text-1: X 与
--md-sys-color-on-surface: X。表里 39 条映射,由 tools/theme-system-test.mjs 从 tokens.css 反解核对,
漂移即红。但镜像对 --surface 家族被禁用(SURFACE_FAMILY, :270-272)——表面填充归玻璃档独占。
3.4 内置主题 7 个¶
| id | 名字 | dark | vars 条数 | 附加 css |
|---|---|---|---|---|
onethu.theme.ivory |
象牙·默认 | – | 0 | – |
onethu.theme.violet |
紫水晶 | – | 15 | – |
onethu.theme.tsinghua |
清华紫 | – | 24 | –(也是取色降级目标,dynamicColor.ts:26) |
onethu.theme.celadon |
青瓷 | – | 15 | – |
onethu.theme.warmsand |
暖沙 | – | 8 | – |
onethu.theme.midnight |
墨蓝夜航 | – | 15 | – |
onethu.theme.night |
凝夜 | ✔ | 26 | 3 条定点补丁(:242-245) |
内置升级通道(:382-398):版本号或令牌集不一致就整体刷新为随包定义。
另有用户生成主题走完全不同的路:state/userext.ts:349-373 自建 <style id="onethu.usergen-theme">,
选择器是裸 :root(没有 [data-theme] 加成),靠 head.appendChild 压轴 + :383 监听 data-theme 变化重挂。
3.5 谁决定 data-scheme¶
| 写入方 | 位置 | 条件 |
|---|---|---|
state/theme.ts:488-489 |
主题应用时 | def.dark === true → "dark",否则删属性 |
lib/dynamicColor.ts:syncScheme() |
取色模式 | 取色开启期间由 prefers-color-scheme 驱动(主题已让位) |
tokens.css:217 |
消费方 | :root[data-scheme="dark"] 提供 34 条 System 暗色基线 |
lib/textContrast.ts:386 |
观察方 | MutationObserver 监听 ["data-scheme","data-glass","data-wallpaper-lum"] |
state/userext.ts:384 |
观察方 | 监听 ["data-theme","data-scheme"] |
components/exthw/ProblemBody.tsx:252 |
观察方 | 监听 ["data-theme","style","class"] + #onethu-theme-style 的 characterData |
昼夜调度(:494-501 :504-517 :568-582):followSystem 为真时按 matchMedia("(prefers-color-scheme: dark)")
在 dayThemeId/nightThemeId 之间切;这是主题级昼夜,与 data-scheme 的 System 暗色套是两条并行通道。
4. 对比度引擎:lib/textContrast.ts(390 行)¶
4.1 机制¶
| 项 | 值 | 位置 |
|---|---|---|
| 壁纸离屏采样 | canvas,8px 网格(CELL=8),瓦片亮度缓存 96px(TILE) |
:17-18 :102-115 |
| 单轮新增叶子预算 | 4000(NODE_BUDGET) |
:19 |
| 达标线 | 对比率 ≥ 3.0(MIN_CONTRAST) |
:20 |
| 候选色 | 白 rgba(255,255,255,.95) / 黑 rgba(20,22,28,.94) |
:196-197 |
| 触发 | 首次进入视口(IntersectionObserver,threshold: 0.01) |
:342-353 |
| 重取 | 仅 wall / theme / glass-level 三类显式环境变更 | :355-367 :233-234 |
4.2 它改什么 DOM¶
写内联 style="color: …",不写 data 属性选择器,然后用 data-* 做幂等标记:
| 标记 | 语义 | 位置 |
|---|---|---|
el.style.setProperty("color", …) + data-tx="lock" |
文本叶子已定格 | :228-229 |
el.style.setProperty("color", …) + data-txi="lock" |
图标宿主已定格(SVG 继承 currentColor) |
:281-282 |
[data-tx="keep"] |
逃生舱:子树永不被碰 | :206 :252 :297 |
重取时 style.removeProperty("color") |
摘掉旧内联色再重算 | :218-221 :271-275 :302-309 |
图标单独分流在 settleIcon()(:248-284)::262 把 cr>240 || cr<24 判为「近极端色 = 隐形笔画本身」,
即使对比度达标也强制重算。这是 2026-10-14 用户点名「很多 icon 没有颜色变深导致看不见」的补丁。
4.3 环境耦合(最脆弱的部分)¶
| 硬编码依赖 | 行 | 说明 |
|---|---|---|
.explorer-shell |
:179 :287 :321 |
只扫这个根,其他根(portal 到 body 的浮层)拿不到 |
.explorer-shell[data-wallpaper-lum] |
:180-185 |
纱的提白/压黑比例 0.34 / 0.36 写死 |
.explorer-content + 排除 .explorer-topbar, .ex-rail, .ex-bbar, .ex-mobile-strip, .home-chrome-layer |
:184 |
页类名单写进引擎 |
.wall-video-veil;沿祖先链最多 40 跳(hop < 40) |
:153 :141 |
视频纱硬编码 rgba(0,0,0,0.2);超深树漏算 |
4.4 已知失败模式(注释里写明的历史事故)¶
| 事故 | 注释位置 |
|---|---|
用 bg < 0.5 当黑白分界是错的(0.5 是中灰,交叉点 L≈0.179)→ 首页时钟/OneGO! 白字压 L=0.352 底只有 2.33:1 |
:224-226 |
旧实现从 parentElement 起收集玻璃层 → 漏掉元素自身的玻璃底 → 误判「底很亮」→ 写白字 |
:142-143 |
只匹配 rgba(),hex 停靠点整片漏掉(gel token 的 tint 渐变里就有) |
:150-151 |
| 首版滚动触发重算 = 「乱闪的根源」,已删 | :11-12 :234 |
只监听 style 属性会自触发死循环 → attributeFilter 收窄到 3 个 data 属性 |
:385-386 |
4.5 测试工具¶
tools/*contrast* 不存在。相近的只有 dynamic-color-test.mjs(测 rolesFromPalette 映射)、
theme-scheme-test.mjs、theme-vars-test.mjs、dynamic-flow-test.mjs。textContrast.ts 本身没有任何守卫。
5. 动效层¶
5.1 styles/motion.css(912 行,96 规则,25 个 @keyframes)¶
| 区块 | 内容 |
|---|---|
| §1 令牌 | :16-51,22 个 |
| §2 reduced-motion | :56-66,全局 * 强制 animation-duration: 1ms !important 等 5 条 !important |
| §3 keyframes | m-grid-in m-rise m-fade m-pop m-spring-in m-sheet-up/down m-slide-right/left m-toast-in/out m-page-in m-check-draw m-breathe m-stripes m-knob m-focus-ring m-page-in-back m-tab-in m-tab-in-back m-reveal-in m-expand-in m-fade-out m-spring-out 等 25 个 |
| §4-§16 | 微交互 / hover 悬浮 / stagger / 页面转场 / 弹层 / toast / 骨架 / 数字滚动 / 工具类 / theme-anim / 滚动条 / 预览 / 揭示 |
5.2 令牌集¶
System(:20-30,11 个) |
Compat(:34-51,11 个) |
|---|---|
--md-sys-motion-easing-{emphasized, emphasized-decelerate, emphasized-accelerate, standard, standard-decelerate, standard-accelerate} |
--ease-out(→ emphasized-decelerate)、--ease-in-out(→ standard)、--ease-ios(→ emphasized)、--ease-spring = cubic-bezier(0.34,1.34,0.5,1)(品牌抖动,非 MD3) |
--md-sys-motion-duration-{short-2:100, short-4:200, medium-2:300, medium-4:400, long-2:500} |
--dur-1: 140ms(不接 MD3,注释:按压反馈要「立刻」)、--dur-2→short-4、--dur-3→medium-2、--dur-4→long-2 |
| — | --rise-1/2/3 = 4/8/14px、--stagger: 26ms |
消费数(styles/*.css / TSX):--dur-2 83/22、--ease-out 75/23、--dur-3 37/12、--dur-1 28/0、
--stagger 15/0、--ease-spring 14/1、--ease-ios 10/2、--dur-4 2/0、--rise-2 1/0、
--rise-1 --rise-3 0(僵尸)。
5.3 类名词汇(m-* 是 keyframes 名,不是 utility 类)¶
| 家族 | 形态 | 实例数 | 用途 |
|---|---|---|---|
m-* |
keyframes 名,不是 utility 类 | 25 个定义;CSS 侧 22 处 animation: m-* 引用,TSX 侧 38 处字符串引用(m-spring-in 10、m-fade 9、m-fade-out 6、m-pop-out 4、m-rise 3…) |
组件用内联 style={{animation: "m-pop .2s …"}} 或类名挂 |
anim-* |
utility 类(.anim-rise .anim-fade .anim-pop .anim-spring .anim-slide-right .anim-delay-1..4) |
0 处 TSX 使用 | 死的工具类;TSX 只出现 3 次字符串 anim-delight(是分支名,不是类) |
| 状态类 | .is-closing / .is-open / .is-scrolled / .is-ready |
大量 | 退场相位由 lib/useExitPhase.ts 打 .is-closing |
5.4 reduced-motion 与 JS 侧¶
- CSS:
motion.css:56-66全局!important降级(1ms +scroll-behavior: auto)。 - JS:
lib/motion.ts:9-11prefersReducedMotion();:88installScrollReveal()在html挂has-reveal类(motion.css:678,685依赖它),减弱动态时不挂。components/Ripple.tsx同检查。 lib/motion.ts还持有导航指示器物理常量:NAV_DUR_MIN/MAX/SLOPE、NAV_APEX、NAV_BOUNCE_MS、NAV_EASE、useNavIndicator()(:297)、useBottomNavPill()(:457),底栏胶囊与侧栏蓝条共用;nav-shell-test.mjs钉复用。- 另有一个未被
pnpm guard调用的tools/motion-test.mjs。
6. 样式如何进入应用¶
6.1 import 顺序(唯一的确定性来源是 main.tsx)¶
| 序 | 语句 | 行 |
|---|---|---|
| 1 | import "@onethu/ui/tokens.css" |
main.tsx:39 |
| 2 | import { initGlassLevel } → initGlassLevel()(写 data-glass) |
:48-49 |
| 3 | import "@onethu/ui/base.css" |
:50 |
| 4 | import "./styles/global.css" |
:51 |
| 5 | import "./styles/glass-levels.css" |
:52 |
| 6 | import "./styles/motion.css" |
:55(注释:必须在 global 之后,否则同选择器的 transition 被覆盖) |
| 7 | installScrollReveal(); installTextContrast(); installRipple(); |
:31-35(函数在 import 之前就被调用,靠 ESM 提升 + 模块求值序) |
| 8 | initDynamicColor(); watchDynamicColor(); |
:83-84(注释:放在 CSS 导入之后,注入 <style> 要插在令牌之后) |
其余 4 个样式文件不在入口,由组件引入,因此落位依赖模块图求值顺序:
| 文件 | 引入点 |
|---|---|
explorer.css |
components/Layout.tsx:51 |
explorer-ui.css |
components/Layout.tsx:52(文件头注释自称「本文件在 explorer.css 之后加载」) |
explorer-home.css |
components/Layout.tsx:53 |
explorer-oh.css |
plugins/ChatDock.tsx:44(注释:悬浮球窗口同 bundle,一处引入两端生效) |
quill.snow.css |
components/RichEditor.tsx:2、components/exthw/YktSubjectiveEditor.tsx:18 |
leaflet.css |
pages/Trace.tsx:14 |
packages/ui/src/tokens.css:16 是全仓唯一的 CSS @import(引 palette.css)。
6.2 运行时 <style> 注入站点¶
| id | 写入方 | 选择器 / 内容 | 特异性 |
|---|---|---|---|
onethu-theme-style |
state/theme.ts:450-481 |
:root[data-theme="<id>"]{…} + 主题附加 CSS |
(0,2,0) |
onethu-dynamic-color |
lib/dynamicColor.ts:93-101 |
html:root[data-dynamic="on"]{…} + [data-scheme="dark"]{…} |
(0,2,1) ← 最高 |
onethu.usergen-theme |
state/userext.ts:362-368 |
裸 :root{…},appendChild 压轴 |
(0,1,0) 但文档序最后 |
| (插件 CSS) | plugins/loader.ts:233-236 |
插件注入的全局样式,停用即移除 | 未限定 |
<style>{DOCX_CSS}</style> |
components/FilePreview.tsx:1435 |
组件内联 | 文档序最末 |
<style>{ORB_KEYFRAMES}</style> |
components/GlobalOrb.tsx:371,431 |
关键帧 | — |
<style>{SHEET_CSS}</style> |
components/AttachActionsSheet.tsx:432 |
弹层 CSS | — |
<style>{TRAY_CSS}</style> |
components/AttachTray.tsx:152 |
— | — |
<style>{SHEET_CSS}</style> |
components/ImageFilterSheet.tsx:200 |
— | — |
<style>{BODY_STYLE}</style> |
pages/ThubookPage.tsx:223 |
— | — |
<style>(srcDoc 内联) |
pages/MailPage.tsx:133 |
iframe 内滚动条 + 正文样式(父页样式进不了独立文档) | — |
<style>{latexBundle.inlineCss}</style> |
components/exthw/YktSubjectiveEditor.tsx:391 |
— | — |
srcdoc 拼 <style> |
lib/yktBody.ts:664 |
作业正文独立文档(:461 有净化说明) |
— |
index.html:13-22 另有一个全局 SVG #dsh-liquid-refract 滤镜(feTurbulence 位移图),被
global.css:8164 的 --glass-refract: url(#dsh-liquid-refract) 引用 —— 这是运行时的玻璃折射真源,却住在 HTML 里。
7. 护栏工具逐个体检¶
14 个全部在 pnpm guard 里(guard 脚本按固定顺序串跑 49 个 mjs + desktop typecheck);pnpm lint:style 只跑 style-scan.mjs。
| 工具 | 锁的不变量 | 找目标的方式 | 在 guard | 组件库化改造难度 |
|---|---|---|---|---|
tools/token-guard.mjs(98 行) |
① 所有 var(--x) 引用都有定义;② Compat 旧名不得漏迁;③ CSS 注释不得嵌套(曾让 vite 报 Unknown word) |
遍历 apps/desktop/src + packages/ui/src 的 .css/.tsx/.ts,正则扫 (--x)\s*: 建定义集、var(--x[,]) 建引用集;TSX 里 "--x": 与模板串 --x: 记为「运行时注入」 |
✔ 第 1 位 | 低。组件库只是把「组件目录」换个位置,正则不变即可继续用 |
tools/style-scan.mjs(127 行) |
4pt 间距网格 + 字阶只准 12/14/16/20/24;按 文件+违规值→数量 记 ratchet 基线,只拦新增 |
正则遍历 packages/ui/src + apps/desktop/src/styles 的 padding/margin/gap/*-{top,right,…} 与 font-size 声明,跳过 var()/calc()/%/em |
✔ 第 4 位(--write-baseline 可写基线) |
中。基线是「按 CSS 文件 + 字面值」,组件库落地会整体置换文件名:老基线全失效 → 需要一次性的「基线重播种 + 人工过一遍」 |
tools/card-layers-test.mjs(108 行) |
① base.css 的 body 必须是 container-low、不得等于 --bg;② 9 类卡片底色=色块+形状令牌、无可见描边、无阴影、不借 --elev-3;③ hover 必须走 .btn::after 共享状态层且不得改描边/底色;④ 4 个「未迁移」类必须真实存在 |
纯正则读 global.css 与 base.css 文本,block(src, selector) 用大括号配对切规则体 |
✔ | 高。9 类卡片全是页级类名(.home-card .plg-stat .app-card .week-course .mail-list…)。组件库会把这些名字换成 .og-card 之类 → 断言全红且必须重写为「按组件 API 断言」而非按类名断言 |
tools/controls-test.mjs(84 行) |
胶囊一套几何(.chip-btn 高 24px、禁字面 999px、禁 Compat 底色)、开关两实现归一(.switch/.plg-switch 同 36×22、同走 surface-container-high+primary on 态)、进度两条配方归一、骨架屏只许一条规则、空状态 4pt 网格 |
正则读 global.css |
✔ | 高。断言直接钉在 .chip-btn .switch .plg-switch .thos-progress-bar .cloud-quota-bar .skeleton .empty 这些业务类名上 |
tools/list-row-test.mjs(42 行) |
6 类行(.mail-row .cloud-row .svc-row .task-row .wb-row .home-modal-row)不得有字面圆角 / --border-soft 分隔线 / 字面色值;5 类交互行必须有 hover/active |
正则读 global.css |
✔ | 中。规则本身通用,只依赖类名清单 |
tools/button-variants-test.mjs(126 行) |
读 5 个文件(global.css motion.css tokens.css Ripple.tsx main.tsx):状态层 8%/12%/8%/38% 明暗两套、焦点环唯一、禁用单一档、变体不各写 hover 底色、涟漪只动 transform/opacity 且 reduced-motion 不生成、宿主 position: relative、FAB 与两个野路子按钮(.task-stat .today-quick-chip)接入同一模型 |
正则跨文件读文本 + block() 切块 |
✔ | 高。这是最接近「组件契约测试」的一个(已经跨 CSS/TSX),但仍是字符串匹配:组件库化后要改成「读组件源码 + 断言 props/variant 表」 |
tools/input-system-test.mjs(82 行) |
3 类常规输入高度 38/40、padding 0 12px、圆角 medium、底色 container、描边必须保留;3 条聚焦规则统一走 --md-sys-focus-ring;3 项紧凑清单须存活;手机 40px 档 |
正则读 global.css,选择器含 \n.field input { 这类带换行的字面串 |
✔ | 高。NORMAL/FOCUS 数组用带 \n 的字面选择器串做 indexOf,任何空白/格式变动即失效 |
tools/modal-shell-test.mjs(162 行) |
读 6 个文件(global.css tokens.css motion.css 3 个 TSX):遮罩走 --md-sys-color-scrim 明暗两档、桌面弹窗形状+elev-3+保留描边、手机 bottom sheet(贴底/满宽/92dvh/只圆上两角/安全区/把手/上滑)、抽屉内卡片灰阶去描边、退场动画全族存在、is-closing 相位、portal 到 body、内联 animation 压掉 class 退场、JS 延时与 CSS 时长同源(200ms)、useExitPhase 全覆盖 |
正则 + block() + 逐文件读 TSX 字符串 |
✔ | 高。它同时钉 CSS、DOM 结构、hook 用法、延时数值——组件库化必须拆成 3~4 个正交测试 |
tools/nav-shell-test.mjs(105 行) |
底栏 5 等分 / 无描边 / 安全区 / 64×32 胶囊 / secondary-container / is-ready 才点亮;侧栏 ≤240px、整行胶囊、竖条指示器关闭、分区标题令牌化、折叠钮无描边;手机大标题走 headline-medium-size;motion.ts 的导航常量必须复用 |
读 5 个文件;pick(selector, feature) 用内容特征正则在多个同名 .bottom-nav { 里挑真身 |
✔ | 高。已在注释里承认「.bottom-nav 的第一处是桌面 display:none」这种靠特征选真身的脆弱性,且断言里夹着已废弃结构(bottom-nav-indicator 必须不存在) |
tools/scrollbar-test.mjs(110 行) |
① 全库不得声明 scrollbar-width(除 none)/scrollbar-color(否则 Chromium ≥121 整体忽略 ::-webkit-scrollbar);② base.css 的令牌化细滚动条(混色 ≤22%、透明边+content-box、hover/active 逐级加深);③ 颜色只走令牌;④ 元素级覆盖只许改宽度;⑤ html 必须 scrollbar-gutter: stable + overflow-y: scroll;⑥ 邮件 iframe 的 srcDoc 要自带滚动条样式 |
遍历 apps/desktop/src + packages/ui/src 全部 .css(walk());TSX 只查 MailPage.tsx |
✔ | 低。几乎全是全局不变量(*:: 伪元素 + html),组件库化后仍然成立且应该升级为组件库的滚动容器基线 |
tools/icon-test.mjs(76 行) |
基座 18×18 / viewBox 24 / strokeWidth 1.6 / currentColor / aria-hidden;16px 档 1.4 且画布只有 1 处;除白名单外禁止手写 <svg>;禁 strokeWidth=2 |
遍历 apps/desktop/src/**/*.tsx,数 <svg 出现次数并与 ALLOW 映射比对;断言 Icons.tsx 的正则 |
✔ | 低-中。语义上是「图标组件库」的契约,几乎可以原样搬进 OneGUI,只需把 SRC 与白名单路径改成库路径 |
tools/density-test.mjs(47 行) |
html.is-phone 首个规则块 = 密度刻度:--text-base: 15px、--text-sm: 14px、--gap-1..6 = 4/8/12/16/20/24;html.is-phone body { line-height: 1.6 };手机层不得出现字面 13/14px 字号;5 处功能性分隔线必须在、2 处装饰性必须删 |
正则读 global.css,CSS.indexOf("html.is-phone {") 定位首个规则块 |
✔ | 中。密度刻度是 OneGUI 必须接管的「第二套令牌」,但断言靠 indexOf 取第一个 html.is-phone { 块,加一条规则即失效 |
tools/token-compat-diff.mjs(93 行) |
「令牌重写零观感变化」:git show HEAD:packages/ui/src/tokens.css(+ palette)里每条非 --md-* 变量的解析值与工作区新值逐条比对;6 条白名单「有意变化」单独列出 |
execFileSync("git", ["show", REF:path]) + 自写 var() 链解析(深度 10)+ stripSchemeBlocks() 剔暗色块 |
✔ 第 2 位 | 中。它把「与 git HEAD 的旧值比对」当成不变量——OneGUI 一旦真的改了令牌值,这个测试必须同步维护 INTENTIONAL 白名单,否则每次提交都红 |
tools/css-parse-test.mjs(82 行) |
每个 CSS 文件括号配平、注释闭合;且不允许规则被嵌进非 @ 规则(少写 } 的典型后果;历史事故:27 项护栏一条没拦住,按钮全变默认样式) |
逐字符扫描 + 括号栈,stack[top] 不以 @ 开头即报错 |
✔ 第 3 位 | 低。纯结构校验,与类名/组件无关,可原样保留 |
注:
tools/*contrast*-test系列不存在;tools/dom-stub.mjs只被dynamic-flow-test.mjs与theme-system-test.mjs使用, 其余 12 个目标工具全是纯文本正则,不建 DOM。
7.1 tools/style-scan-baseline.json(108 行)¶
| 指标 | 值 |
|---|---|
| 覆盖文件 | 6 |
| 间距违规基线 | 690 |
| 字号违规基线 | 123 |
| 合计 | 813 |
| 当前实测 | 间距 688(比基线少 2,已清了一点)、字号 123(持平) |
| 文件 | 间距 | 字号 |
|---|---|---|
apps/desktop/src/styles/global.css |
590 | 94 |
explorer-ui.css |
57 | 19 |
explorer-oh.css |
29 | 9 |
explorer.css |
8 | 1 |
explorer-home.css |
5 | 0 |
motion.css |
1 | 0 |
Top 间距违规值:10px ×189、6px ×138、2px ×117、14px ×62、1px ×37、7px ×34、9px ×33。
Top 字号违规值:13px ×34、11px ×28、10px ×22、10.5px ×10、15px ×9、12.5px ×7。
813 处「规范债」就是 OneGUI 组件库的隐含需求清单——组件库给出档位后这些数字才能真的降。
8. 风险清单:组件库接管样式时最可能反打的地方¶
| # | 风险 | 证据(grep 计数) | 为什么会反打 |
|---|---|---|---|
| 1 | !important 军备竞赛 |
styles/*.css 104 处(explorer-ui 24 / global 36 / explorer 13 / motion 6 / explorer-home 3 / explorer-oh 1);TS/TSX 6 处 |
组件库若靠「普通特异性」上新样式,会被这 110 处直接压掉。global.css 的注释自己写着「!important 压过内联样式」(.xk-panel 一族) |
| 2 | 内联样式压过 class | TSX 中 style={{ 1481 处,其中含 color/background/border 的 397 处;el.style.setProperty 11 处(对比度引擎独占其中 2 处) |
组件库用 class 表达外观,但凡宿主传了 style={{color}} 就赢。modal-shell-test.mjs 已经踩过同型坑:「内联 animation 优先级更高 → 退场永远不生效」 |
| 3 | 每页 :has() 选择器 |
:has( 30 处(global.css 27 + motion.css 1 + 其余 2);其中 :has(.tasks-body) 13 处、:has(> .page-anim[data-page="…"]) 8 处、:has(.sidebar.is-collapsed) 1 处 |
这些是「根据页面内容反推布局」的补丁(待办页没有 body 就换排布)。组件库一旦改变 DOM 形状,这些选择器静默失效——不报错、不报红,只是布局悄悄变样 |
| 4 | CSS 与 TSX 的类名耦合是双向的 | card-layers-test 绑 9 类页级卡 + 4 类未迁清单;controls-test 绑 7 个业务类;list-row-test 绑 6 类行;input-system-test 用带换行的字面选择器做 indexOf |
护栏本身是「组件库的最大阻力」:改类名 → 护栏红 → 改护栏 → 又得保证语义。explorer-ui.css:21 已经出现「global.css 的 .tabstrip{flex-wrap:wrap} 会撞车」这类跨文件类名冲突 |
| 5 | 表面填充的三个管理方(tokens.css 双声明覆盖 + global.css:8196-8199 劫持 System 名 + glass-levels.css 六块覆盖) |
--surface + --surface-2 + --surface-3 合计 146 处消费;--md-sys-color-surface-container-* 合计 100 处消费;玻璃令牌 443 处 |
组件库要「认领表面」,必须先把这 3 个写入方收敛成 1 个。theme.ts:264-272 的 SURFACE_FAMILY 黑名单是现状的补丁而非解法 |
| 6 | html.is-phone 密度层 |
html.is-phone 规则 139 处(global 64 / explorer-ui 29 / explorer-home 26 / explorer-oh 12 / explorer 4);首个块重写 13 个令牌(7 字号 + 6 间距) |
这是第二套令牌体系(手机正文 15px、--gap-5/6 换成 20/24),与桌面档并行。组件库必须内建「密度」维度,否则 139 处手机覆写会全部躲到库外面 |
| 7 | --dsw-alias-* / --dsw-radius-* 幽灵令牌 |
样式表引用 --dsw-* 34 处(--dsw-radius-lg 8、--dsw-alias-bg-layer-1 6、--dsw-radius-md 5、--dsw-radius-xs/sm 2);TSX 43 处;tokens.css:112-120 只定义了 9 个,--dsw-radius-* 全部未定义 |
tokens.css:109-111 的注释承认过一次事故:「应用内从未定义 → 深夜模式标签栏/侧栏恒回退 #fff 发白」。组件库化会扩大这个面 |
| 8 | 字面值残留 | app CSS 中 hex 139 处、rgba()/hsla() 328 处、border-radius: 999px 17 处、字面 font-size: Npx 232 处;TSX 侧 var(--…) 只有 713 处 |
组件库若声称「组件不写颜色」,这 467 处色值是必须先清的地基;explorer.css:303-328 的 color: #1c1f28 连文字色都写死了 |
| 9 | 选择器特异性天花板已经很高 | global.css 1753 条规则、最深选择器带 6 层类限定;explorer.css 有 6 个按 [data-wallpaper-lum] 分支的作用域令牌块;html.has-reveal :is(.list,.stats,.today-grid,.market-grid,.icon-grid,.setting-group) > *[data-reveal-in](motion.css:685)已到 (0,4,0)+ |
组件库想用低特异性 class 覆盖,必须走 @layer 或 :where();但现有代码没有 @layer,一次性引入会让 2398 条规则的胜出关系整体重排 |
| 10 | <style> 注入站点 13 处,且有一个在 HTML 里 |
onethu-theme-style / onethu-dynamic-color / onethu.usergen-theme + 插件 loader + 9 处组件内联 <style>;index.html:13 的 #dsh-liquid-refract SVG 滤镜被 global.css:8164 引用;exthw/ProblemBody.tsx:236-252 用 MutationObserver 监听 #onethu-theme-style 的 characterData |
「一个样式命令」在运行时是五条不同的注入路径,其中两条靠 head.appendChild 抢文档序。组件库若也 inject,就是第六条 |
| 11 | CSS 导入顺序不是静态可读的 | 入口只引 4 个;explorer*.css 由 components/Layout.tsx:51-53、plugins/ChatDock.tsx:44 引入;quill.snow.css 引 2 次;leaflet.css 1 次 |
Vite dev 用 JS 注入 <style>,顺序 = 模块求值顺序;组件懒加载/代码分割一改,级联次序就变。explorer-ui.css:6 的「本文件在 explorer.css 之后加载」是一句没有被任何工具保证的约定 |
| 12 | AppleDouble ._* 与 exFAT 卷 |
仓库全量 ._* 3706 个(其中 plugins/OneTHU-Harness/target/debug/deps 2298);源码目录内(apps/packages/tools/docs)仍有 388 个,packages/ui/src 与 apps/desktop/src/styles 每个 CSS 都有对应的 ._xxx.css(4096 字节) |
新增一个 packages/ui/src/onegui/*.css 就会立刻多一个 ._onegui.css。凡是「遍历目录里 *.css」的护栏(style-scan scrollbar-test css-parse-test token-guard)都可能读到 ._ 文件——本次侦察全程用 grep -v '\._' 手工规避,现有脚本没有一处排除 ._*,属于共存缺口 |
| 13 | 对比度引擎没有护栏 | tools/*contrast* 不存在;textContrast.ts 390 行无任何测试;引擎硬编码 .explorer-shell / .explorer-content / .ex-rail / .ex-bbar / .ex-mobile-strip / .home-chrome-layer / .wall-video-veil 7 个类名 |
组件库换 DOM 形状 → 引擎的 closest() 判定失效 → 纱不再提白 → 「字看不见」类事故回归,且没有任何测试会红 |
| 14 | motion.css 与 global.css 的同选择器争夺 |
motion.css 96 条规则里大量 .btn .input .row-caret .switch-knob .seg-item .nav-item .skeleton .toast-host 等业务类;main.tsx:53-54 靠注释保证 motion 在 global 之后 |
组件库接管 .btn 后,motion.css 的微交互规则会默默作用在新组件上(有时是想要的,有时是灾难)。需要显式决定 motion 的所有权 |
9. 接管建议的落点(供 03 文档展开)¶
| 目标 | 现状真源 | OneGUI 应接管为 |
|---|---|---|
| 令牌 | packages/ui/src/tokens.css(3 层 + 56 Compat 名,但表面被三方劫持) |
单层语义令牌 + 显式的 @layer onegui.tokens |
| 玻璃 | glass-levels.css(6 选择器块,被 explorer.css 用更高特异性旁路) |
玻璃度作为组件级 prop,档位只写一处 |
| 主题 | state/theme.ts(<style id> + :root[data-theme] + Compat→System 镜像表 39 条) |
CSSOM 变量写入 or 单一 @layer onegui.theme,镜像表自动化 |
| 明暗 | data-scheme(2 个写入方 + 3 个观察方) |
单一 scheme service,组件只订阅 |
| 对比度 | lib/textContrast.ts(390 行,写内联色,7 个硬编码类名,0 测试) |
组件层的 tone="auto" 意图,引擎内部化 |
| 动效 | motion.css(25 keyframes + 22 令牌)+ lib/motion.ts(27 KB,含导航物理常量) |
统一 og-motion 服务 + 令牌;reduced-motion 由库保证 |
| 密度 | html.is-phone 139 条覆写 + 13 个令牌重写 |
密度维度进令牌层(data-density),页级覆写清零 |
| 护栏 | 14 个正则文本测试,绑 50+ 业务类名 | 组件契约测试(按 variant/props 断言)+ 保留 3 个纯结构测试(css-parse token-guard scrollbar) |
只读侦察产物,未修改任何其他文件。