chore(design-system): add extraction artifact and token drift gate
Record the design decisions actually in use and add a static gate that blocks new bare colors, bare sizes, important overrides, and wb token definitions outside token sources.
This commit is contained in:
1 parent
98cf32b6e2
commit
3ceae1e983
4 files changed
+4663
No files matched your search
@@ -0,0 +1,467 @@
|
|||||||
|
# YCode 设计系统现状(Extract,2026-10-18)
|
||||||
|
|
||||||
|
> **本文件是现状快照,不是目标方案。** 只记录当前仓库里实际生效的 token、刻度、组件与约定,供后续 Build / Audit 逐条对照。
|
||||||
|
> 目标分层、兼容层边界与迁移顺序见 `docs/设计系统提取与统一方案-2026-10-18.md`(本文件第 6 节只做指向,不复制方案内容)。
|
||||||
|
> 采集范围:`src/renderer/src`(统计时排除第三方资产目录 `workbuddy/artifacts/recovered-pierre/`、`workbuddy/workbench/source-code-assets/`)与构建产物 `out/renderer/`。
|
||||||
|
> 采集方式:`rg` 文本检索 + 正则统计 + PowerShell 逐字节读取文件;未做浏览器运行时取值,未改动任何实现文件。
|
||||||
|
> 工作树快照说明:提取期间同批 P0 工作已落地 `scripts/check-design-tokens.mjs`、`scripts/design-token-baseline.json`(`npm run check:tokens`)并更新 `scripts/verify-ui.mjs`;本文件第 5.5 / 6 节按**当前工作树**(含这些未提交改动)记录。
|
||||||
|
|
||||||
|
## 0. 规模与测量基线
|
||||||
|
|
||||||
|
| 指标 | 实测值 | 说明 |
|
||||||
|
| --- | ---: | --- |
|
||||||
|
| CSS 文件 / TSX 文件 | 109 / 151 | `src/renderer/src` 全量,含恢复层 |
|
||||||
|
| CSS 行数 | 19,790 | 逐文件 `Get-Content` 行数求和 |
|
||||||
|
| `var(--yc-*)` 引用 | 1,372 | 与方案文档记录的 1,372 一致 |
|
||||||
|
| `var(--wb-*)` 引用 | 22,766 | 方案文档记录 22,756(本文件实测多 10 处) |
|
||||||
|
| `!important` | 926 | 方案文档记录 925(本文件实测多 1 处) |
|
||||||
|
| `tokens.css` 中 `--yc-*` 声明 | 271 | `:root` 229 + `html[data-theme='dark']` 18 + `.cb-font-size-fixed` 24 |
|
||||||
|
| `workbuddy/typography/tokens.css` 中 `--wb-*` 声明 | 115 | 唯一的根级 `--wb-*` 家族(排版/间距) |
|
||||||
|
| 原生控件 | `<button>` 362 / `<input>` 48 / `<textarea>` 8 / `<select>` 2 | TSX 内直接书写 |
|
||||||
|
| 内联 `<svg>` | 164(其中 69 个在 `*Icons.tsx` 模块内) | 图标模块共 25 个 |
|
||||||
|
| `wb-button` 类出现(TSX) | 247 次,分布在 18 个文件 | 变体计数见 3.1 |
|
||||||
|
| 本地 `className="btn…"` | 12 处 | 见 3.2 |
|
||||||
|
|
||||||
|
## 1. 权威来源:谁在根级生效,谁只是作用域闭包
|
||||||
|
|
||||||
|
### 1.1 根级生效的层
|
||||||
|
|
||||||
|
| 层 | 位置(选择器) | 内容 | 声明数 |
|
||||||
|
| --- | --- | --- | ---: |
|
||||||
|
| **YCode 权威 token** | `src/renderer/src/styles/tokens.css:17` `:root` | 调色板、语义色、间距/字号/行高/字重、控件尺寸、圆角、字体族、布局、描边、阴影、动效、字号档位 | 229 |
|
||||||
|
| 深色重映射 | `src/renderer/src/styles/tokens.css:352` `html[data-theme='dark']` | 只重映射 text/bg/border/气泡/阴影;未列出的沿用浅色 | 18 |
|
||||||
|
| **根级 WB 排版 token** | `src/renderer/src/workbuddy/typography/tokens.css:5` `:root` | `--wb-spacing-*`、`--wb-font-size-*`、`--wb-font-line-height-*`、`--wb-font-weight-*`、语义体例(body/caption/code/h1-h3/display/spec-*/chatflow-*)、`--wb-button-{height,font-size,line-height,icon-size}-*` | 115 |
|
||||||
|
| 固定字号边界重解析 | `src/renderer/src/styles/tokens.css` 末尾 `.cb-font-size-fixed, .cb-font-size-fixed *` | 在内层把 `--yc-text-*` 重新指回 `--wb-font-*`,避免全局字号档位重复叠加 | 24 |
|
||||||
|
|
||||||
|
依赖方向(实测):
|
||||||
|
|
||||||
|
- `--yc-space-1…10/12` 全部 `var(--wb-spacing-N)`;例外 `--yc-space-11: 56px` 是硬写(WB 无 `--wb-spacing-11`)。
|
||||||
|
- `--yc-font-size-1…11`、`--yc-font-weight-400/500/600/700`、大部分 `--yc-line-height-*` 均指向 `--wb-font-*`;例外 `--yc-line-height-6-2: 26px` 是硬写(WB 无该 token,见 5.3)。
|
||||||
|
- `--yc-font-mono: var(--wb-font-mono)` → `--wb-font-code-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace`。
|
||||||
|
- `--yc-text-body/caption/code/h1/h2/h3/display-*` 全部 `var(--wb-font-*)` 别名,是 YCode 侧唯一一层"语义字号"入口。
|
||||||
|
|
||||||
|
### 1.2 `--wb-*`:除排版外全部是"按作用域复制的兼容闭包"
|
||||||
|
|
||||||
|
根级只定义了 115 个排版/间距 token;**颜色、圆角、控件尺寸、按钮/输入/弹窗等 `--wb-*` 在根上没有定义**,而是各页面/组件在自己的 subtree 里整份复制。实测 `--wb-palette-brand-8:` 在 13 个 CSS 文件里被重复声明:
|
||||||
|
|
||||||
|
| 作用域(已逐行核验) | 文件:行 |
|
||||||
|
| --- | --- |
|
||||||
|
| `.yc-capability-center` | `src/renderer/src/capabilities/capability-center-source.css:2` |
|
||||||
|
| `.models-settings-panel` | `src/renderer/src/components/model-settings.css:146` |
|
||||||
|
| `.source-home` | `src/renderer/src/components/source-home-recovered.css:2` |
|
||||||
|
| `.source-workspace-picker` | `src/renderer/src/components/source-workspace-picker-recovered.css:2` |
|
||||||
|
| `.chat-pane.cr-theme` | `src/renderer/src/workbuddy/conversation/source-styles.css:7` |
|
||||||
|
| (其余 8 个文件同族复制) | `components/settings-shell-source.css`、`components/workspace-source.css`、`components/source-general-settings-recovered.css`、`components/source-settings-navigation-recovered.css`、`components/source-select/source-select-recovered.css`、`projects/source-project-controls.css`、`workbuddy/composer/add-menu/source.css`、`workbuddy/composer/model-selector/source.css`、`workbuddy/subagents/theme.css` |
|
||||||
|
|
||||||
|
闭包缺失的直接后果(可复现):
|
||||||
|
|
||||||
|
- `--wb-size-control-sm/ms/md/lg/xl/2xl`、`--wb-icon-size-sm/ms/md/lg/xl` 只在这些作用域内定义(例:`capability-center-source.css:8`、`model-settings.css:146`、`workbuddy/conversation/source-styles.css:7`),根上没有。
|
||||||
|
- 因此排版 token 里的 `--wb-button-height-small: var(--wb-size-control-sm)`、`--wb-button-icon-size-medium: var(--wb-icon-size-md)` 等,只有在某个闭包作用域内才会解析成功;出作用域即失效。
|
||||||
|
- `--wb-font-line-height-9`、`--wb-font-line-height-6-2` 被 7 个 CSS 文件引用(`var(--wb-font-line-height-9)`、`var(--wb-font-line-height-6-2)`),但**全仓库没有任何定义**;`--wb-font-spec-h2-line-height`、`--wb-font-spec-h5-line-height` 同样指向未定义项。
|
||||||
|
|
||||||
|
恢复层命名家族(用于判断"这段 CSS 来自哪里"):
|
||||||
|
|
||||||
|
| 前缀 / 形态 | 含义 | 唯一选择器数(实测) |
|
||||||
|
| --- | --- | ---: |
|
||||||
|
| `cr-*` | 会话渲染恢复层(chat renderer) | 591 |
|
||||||
|
| `cb-*` | 原版通用组件层(button/checkbox/input/…) | 204 |
|
||||||
|
| `ec-*` | 能力中心/专家中心恢复层 | 227 |
|
||||||
|
| `sm-*` | 本地技能/市场页面 | 274 |
|
||||||
|
| `dc-*` | 抽屉/面板少量恢复规则 | 6 |
|
||||||
|
| `_name_hash_index` | 原版 CSS Module 哈希类名(如 `._about_385qb_250`、`._dialogOverlay_peiew_7`、`._icon_vnue1_21`) | 大量 |
|
||||||
|
|
||||||
|
### 1.3 CSS 加载顺序与冲突点
|
||||||
|
|
||||||
|
声明的顺序(源码为准):
|
||||||
|
|
||||||
|
| 入口 | 顺序 |
|
||||||
|
| --- | --- |
|
||||||
|
| `src/renderer/src/main.tsx:5-10` | `workbuddy/typography/tokens.css` → `styles/tokens.css` → `styles/app.css` → `styles/sidebar.css` → `workbuddy/conversation/source-styles.css` → `workbuddy/conversation/source-adapters.css` |
|
||||||
|
| `src/renderer/src/settings.tsx:4-6` | `workbuddy/typography/tokens.css` → `styles/tokens.css` → `styles/app.css` → `styles/settings-entry.css` |
|
||||||
|
|
||||||
|
组件级追加(在入口之后由组件模块自带,顺序受模块求值影响):
|
||||||
|
|
||||||
|
| 文件:行 | 导入 |
|
||||||
|
| --- | --- |
|
||||||
|
| `src/renderer/src/App.tsx:19-21` | `panel-motion-source.css`、`drawer-motion-source.css`、`panel-layout-adapters.css` |
|
||||||
|
| `src/renderer/src/components/Sidebar.tsx:34-35` | `workspace-source.css`、`sidebar-shell-source.css` |
|
||||||
|
| `src/renderer/src/components/SettingsDialog.tsx:7-11` | `source-select/source-select-recovered.css`、`source-select/source-select.css`、`source-general-settings.css`、`settings-shell-source.css`、`../styles/settings-entry.css` |
|
||||||
|
| `src/renderer/src/components/ChatPane.tsx:8` | `workbuddy/conversation/search-highlight-adapters.css` |
|
||||||
|
| `src/renderer/src/capabilities/CapabilityCenter.tsx:9` | `capability-center-source.css` |
|
||||||
|
| `src/renderer/src/components/SourceHome.tsx:7` | `source-home.css`(其第 1 行 `@import './source-home-recovered.css'`) |
|
||||||
|
| `src/renderer/src/components/ModelSettingsPanel.tsx:9-10` | `model-settings.css`、`custom-model-settings.css` |
|
||||||
|
| `src/renderer/src/projects/ProjectListPage.tsx:11-13` | `source-project-controls.css`、`source-projects-recovered.css`、`project-layout.css` |
|
||||||
|
|
||||||
|
构建产物实测(`out/renderer/`,与"声明顺序"并不一致):
|
||||||
|
|
||||||
|
- `out/renderer/index.html` 只挂 2 个样式表,且顺序为 `app-h0tta3aw.css` → `index-C92zurN-.css`。
|
||||||
|
- `app-h0tta3aw.css` 的**第一条规则**是恢复层 `.source-select-host{--wb-palette-…}`,来自 `SettingsDialog.tsx` 的导入;`--yc-*` 根块出现在约 178.6 KB 处,晚于 `.settings-modal-overlay`(约 152.5 KB)。
|
||||||
|
- `styles/sidebar.css`(`.sidebar__brand-block` 约 2.87 MB)与 `.chat-pane.cr-theme`(约 2.89 MB)落在第二个样式表 `index-C92zurN-.css` 里。
|
||||||
|
|
||||||
|
冲突点清单:
|
||||||
|
|
||||||
|
1. 声明顺序 ≠ 生效顺序:`main.tsx` 的 `import App` / `import { SettingsWindow }` 写在样式导入之前,打包后组件自带 CSS 先于入口样式落地。
|
||||||
|
2. 同特异性并列时由后加载者获胜(本地 `.btn` 与恢复 `wb-button` 在同一元素上并用时需要显式顺序,`styles/app.css` 与 `sidebar-shell-source.css` 都定义过按钮外观)。
|
||||||
|
3. 作用域闭包不可迁移:把元素移出 `.source-home` / `.models-settings-panel` / `.chat-pane.cr-theme` 等宿主后,`--wb-*` 立即失配。
|
||||||
|
4. 深色主题按作用域各写一遍:`html[data-theme='dark'] …scope…` 与组件自带的 `.dark/.vscode-dark` 兼容分支同时存在,优先级取决于加载顺序。
|
||||||
|
|
||||||
|
### 1.4 `--yc-*` 也不是严格单源
|
||||||
|
|
||||||
|
| 位置 | 重新声明的变量 | 原因(文件内注释) |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `src/renderer/src/styles/sidebar.css:14-15` `.sidebar` | `--yc-font-sans`、`--yc-text-primary` | 侧栏保留原版 shell 回退栈 |
|
||||||
|
| `src/renderer/src/styles/sidebar.css:29-36` `html[data-theme='dark'] .sidebar` | `--yc-bg-secondary`、`--yc-bg-hover`、`--yc-bg-active`、`--yc-text-*`、`--yc-border-tertiary` | 暗色侧栏用 `#1f1f1f` 而非全局 `#171717` |
|
||||||
|
| `src/renderer/src/styles/sidebar.css:708-710` `.notif-panel` | `--yc-notif-bg/-border/-title` | 通知面板局部 token |
|
||||||
|
| `src/renderer/src/components/task-menu.css:21-26,41-46` `.conversation-context-menu` | `--yc-task-menu-*` | Portal 不在 `.chat-pane.cr-theme` 内,显式给同值 |
|
||||||
|
| `src/renderer/src/projects/project-menu.css:3-6,21-24` `.yc-project-more` | `--yc-project-menu-*` | Portal 不继承侧栏 token |
|
||||||
|
| `src/renderer/src/App.tsx:396`(内联 style) | `--yc-layout-main` | 运行时计算主列宽度;由 `workbuddy/workbench/panel-layout-adapters.css:2` 消费 |
|
||||||
|
|
||||||
|
## 2. 实际生效的 scales(只列组件真正在用的步骤)
|
||||||
|
|
||||||
|
计数列 = 该 token 在 `src/renderer/src`(CSS/TSX/TS)中的出现次数,**含 `tokens.css` 自身的别名定义**,用于说明"这一步确实被消费"。
|
||||||
|
|
||||||
|
### 2.1 颜色:中性阶(来源 `styles/tokens.css:19-36`)
|
||||||
|
|
||||||
|
| Token | 值 | 计数 |
|
||||||
|
| --- | --- | ---: |
|
||||||
|
| `--yc-black-100` | `#000000` | 5 |
|
||||||
|
| `--yc-black-70 / -50 / -30 / -20 / -15 / -10` | `rgba(0,0,0,.7/.5/.3/.2/.15/.1)` | 各 1–2 |
|
||||||
|
| `--yc-white-100` | `#ffffff` | 9 |
|
||||||
|
| `--yc-gray-1` | `#fafafa` | 2 |
|
||||||
|
| `--yc-gray-2` | `#f7f7f7` | 2 |
|
||||||
|
| `--yc-gray-3` | `#f2f2f2` | 2 |
|
||||||
|
| `--yc-gray-4` | `#ebebeb` | 2 |
|
||||||
|
| `--yc-gray-5` | `#e6e6e6` | 3 |
|
||||||
|
| `--yc-gray-6` | `#dedede` | 2 |
|
||||||
|
|
||||||
|
### 2.2 品牌与状态色
|
||||||
|
|
||||||
|
| Token | 浅色值 | 深色处理 | 计数 |
|
||||||
|
| --- | --- | --- | ---: |
|
||||||
|
| `--yc-brand-8` | `#00c29a` | 不重映射(恢复层深色用 `#4cf0ce`) | 13 |
|
||||||
|
| `--yc-brand-soft` | `#dff7f2` | 不重映射 | 1 |
|
||||||
|
| `--yc-green-8` | `#0cbf5b` | 不重映射 | 1 |
|
||||||
|
| `--yc-orange-8` | `#ff7800` | 不重映射 | 1 |
|
||||||
|
| `--yc-red-8` | `#f64041` | 不重映射 | 3 |
|
||||||
|
| `--yc-blue-8` | `#3370ff` | 不重映射 | 2 |
|
||||||
|
| `--yc-status-success / warning / error / info` | 即 green/orange/red/blue-8 别名 | 同左 | 9 / 7 / 29 / 5 |
|
||||||
|
|
||||||
|
恢复层同一时刻的 palette 值(`capability-center-source.css:2` 与其 dark 分支):`--wb-palette-brand-8` 浅色 `#00c29a`、深色 `#4cf0ce`;`gray-3/4/5` 浅色 `#f2f2f2/#ebebeb/#e6e6e6`、深色 `#1f1f1f/#242424/#3a3a3a`。
|
||||||
|
|
||||||
|
### 2.3 语义色(浅色 → 深色)
|
||||||
|
|
||||||
|
| Token | 浅色 | 深色(`html[data-theme='dark']`) | 计数 |
|
||||||
|
| --- | --- | --- | ---: |
|
||||||
|
| `--yc-text-primary` | `rgba(0,0,0,.9)` | `rgba(255,255,255,.9)` | 99 |
|
||||||
|
| `--yc-text-secondary` | `rgba(0,0,0,.7)` | `rgba(255,255,255,.7)` | 80 |
|
||||||
|
| `--yc-text-tertiary` | `rgba(0,0,0,.5)` | `rgba(255,255,255,.5)` | 74 |
|
||||||
|
| `--yc-text-disabled` | `rgba(0,0,0,.3)` | `rgba(255,255,255,.3)` | 3 |
|
||||||
|
| `--yc-text-inverse` | `#ffffff` | `#1f1f1f` | 2 |
|
||||||
|
| `--yc-bg-primary` | `#ffffff` | `#1f1f1f` | 56 |
|
||||||
|
| `--yc-bg-secondary` | `#f7f7f7` | `#171717` | 27 |
|
||||||
|
| `--yc-bg-tertiary` | `#f2f2f2` | `#242424` | 8 |
|
||||||
|
| `--yc-bg-hover` | `#ebebeb` | `#2a2a2a` | 39 |
|
||||||
|
| `--yc-bg-active` | `#e6e6e6` | `#333333` | 6 |
|
||||||
|
| `--yc-bg-scrim` | `rgba(0,0,0,.35)` | `rgba(0,0,0,.6)` | 2 |
|
||||||
|
| `--yc-border-primary` | `#e6e6e6` | `#333333` | 46 |
|
||||||
|
| `--yc-border-secondary` | `#ebebeb` | `#2a2a2a` | 5 |
|
||||||
|
| `--yc-border-tertiary` | `#f2f2f2` | `#242424` | 4 |
|
||||||
|
| `--yc-border-strong` | `#dedede` | `#3d3d3d` | 3 |
|
||||||
|
| `--yc-bubble-user-bg` | `#f2f2f2` | `#2f2f2f` | 2 |
|
||||||
|
|
||||||
|
### 2.4 间距阶(`--yc-space-*` = `--wb-spacing-*`)
|
||||||
|
|
||||||
|
| 步骤 | 值 | 计数 | 步骤 | 值 | 计数 |
|
||||||
|
| --- | ---: | ---: | --- | ---: | ---: |
|
||||||
|
| `space-1` | 2px | 24 | `space-7` | 24px | 5 |
|
||||||
|
| `space-2` | 4px | 41 | `space-8` | 32px | 2 |
|
||||||
|
| `space-3` | 8px | 60 | `space-9` | 40px | 7 |
|
||||||
|
| `space-4` | 12px | 44 | `space-10` | 48px | 2 |
|
||||||
|
| `space-5` | 16px | 22 | `space-11` | 56px(硬写) | 1 |
|
||||||
|
| `space-6` | 20px | 14 | `space-12` | 64px | 8 |
|
||||||
|
|
||||||
|
恢复层同时高频使用 `--wb-spacing-*` 本体(`-1` 282、`-2` 354、`-3` 508、`-4` 313、`-5` 288、`-6` 99、`-7` 90、`-8` 73、`-9` 26、`-10` 45、`-12` 46,另有 `-0` 15、`-2-5` 53)。
|
||||||
|
|
||||||
|
### 2.5 字号 / 行高 / 字重
|
||||||
|
|
||||||
|
字号阶(`--yc-font-size-N`,计数含定义):
|
||||||
|
|
||||||
|
| N | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|
||||||
|
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||||
|
| px | 8 | 10 | 12 | 13 | 14 | 16 | 18 | 20 | 24 | 28 | 32 |
|
||||||
|
| 计数 | 1 | 10 | 23 | 10 | 15 | 4 | 9 | 2 | 1 | 1 | 1 |
|
||||||
|
|
||||||
|
行高阶(`--yc-line-height-*`):`4:16`、`4-2:18`、`5:20`、`5-2:22`、`6:24`、`6-2:26`(硬写)、`7:28`、`8:32`、`10:40`。
|
||||||
|
|
||||||
|
字重:`400 / 500 / 600 / 700`。
|
||||||
|
|
||||||
|
语义体例(`--yc-text-*` → `--wb-font-*`,size/line-height/weight):
|
||||||
|
|
||||||
|
| 体例 | 值 | 计数(size) |
|
||||||
|
| --- | --- | ---: |
|
||||||
|
| body | 14 / 22 / 400 | 21 |
|
||||||
|
| body-strong | 14 / 22 / 600 | 3 |
|
||||||
|
| caption | 12 / 18 / 400 | 36 |
|
||||||
|
| code | 13 / 20 / 400 | 3 |
|
||||||
|
| h1 | 24 / 32 / 600 | 1 |
|
||||||
|
| h2 | 20 / 28 / 600 | 2 |
|
||||||
|
| h3 | 16 / 24 / 600 | 2 |
|
||||||
|
| display | 32 / 40 / 700 | 1 |
|
||||||
|
|
||||||
|
对话流(chatflow,独立一套,默认 offset=0,实际值 = `calc(基准 + var(--yc-font-offset) × 倍率)`):h1 21/37、h2 19/33、h3 17/30、h4 15/26、p1 15/26、p2 13/23、p3 11/19。
|
||||||
|
|
||||||
|
Markdown 正文:根字号 `--yc-md-root: 17.143px`、行高 `1.75`、段落 `0.875em`、行内代码 `12.857px / 22.5px`。
|
||||||
|
|
||||||
|
字体族:`--yc-font-sans: 'PingFang SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif`;`--yc-font-mono` 继承 `ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace`。
|
||||||
|
|
||||||
|
全局字号档位(`src/renderer/src/fontSize.ts` + `tokens.css` 末尾):
|
||||||
|
|
||||||
|
| 档 | mdRoot | offset |
|
||||||
|
| ---: | ---: | ---: |
|
||||||
|
| 1 | 16px | -1 |
|
||||||
|
| 2(默认) | 17.143px | 0 |
|
||||||
|
| 3 | 18.286px | +1 |
|
||||||
|
| 4 | 20.571px | +2 |
|
||||||
|
| 5 | 22.857px | +3(仅正文放大) |
|
||||||
|
| 6 | 26.286px | +4(仅正文放大) |
|
||||||
|
|
||||||
|
`--yc-font-offset-global` 由 JS 写入,控件侧统一夹取 `--yc-font-offset: clamp(-1, var(--yc-font-offset-global), 2)`。
|
||||||
|
|
||||||
|
### 2.6 控件尺寸阶
|
||||||
|
|
||||||
|
| Token | 值 | 计数 | | Token | 值 | 计数 |
|
||||||
|
| --- | ---: | ---: | --- | --- | ---: | ---: |
|
||||||
|
| `--yc-size-3` | 12px | 3 | | `--yc-size-10` | 40px | 4 |
|
||||||
|
| `--yc-size-3-2` | 14px | 5 | | `--yc-size-11` | 44px | 1 |
|
||||||
|
| `--yc-size-4` | 16px | 9 | | `--yc-size-12` | 48px | 2 |
|
||||||
|
| `--yc-size-5` | 20px | 5 | | `--yc-size-13` | 52px | 1 |
|
||||||
|
| `--yc-size-6` | 24px | 7 | | `--yc-size-14` | 56px | 2 |
|
||||||
|
| `--yc-size-7` | 28px | 11 | | `--yc-size-17` | 68px | 1 |
|
||||||
|
| `--yc-size-8` | 32px | 2 | | `--yc-size-9` | 36px | 1 |
|
||||||
|
|
||||||
|
语义控件高度:`--yc-control-sm:24`、`ms:28`、`md:32`、`lg:40`、`xl:48`、`2xl:56`;实际消费 `control-md` 18 次、`control-xl` 3 次、`control-2xl` 2 次、`control-ms` 2 次、`control-sm/lg` 各 1 次。
|
||||||
|
|
||||||
|
### 2.7 圆角阶
|
||||||
|
|
||||||
|
| Token | 值 | 计数 | Token | 值 | 计数 |
|
||||||
|
| --- | ---: | ---: | --- | ---: | ---: |
|
||||||
|
| `--yc-radius-none` | 0 | 1 | `--yc-radius-xl` | 12px | 7 |
|
||||||
|
| `--yc-radius-xs` | 2px | 4 | `--yc-radius-2xl` | 16px | 6 |
|
||||||
|
| `--yc-radius-sm` | 4px | 8 | `--yc-radius-3xl` | 20px | 1 |
|
||||||
|
| `--yc-radius-md` | 6px | 16 | `--yc-radius-4xl` | 24px | 2 |
|
||||||
|
| `--yc-radius-lg` | 8px | 29 | `--yc-radius-full` | 9999px | 16 |
|
||||||
|
| 语义:`--yc-radius-bubble` | 16px | 3 | `--yc-radius-input` | 24px | 2 |
|
||||||
|
|
||||||
|
恢复层圆角集合与之一致(`capability-center-source.css` 实测出现 0/2/4/6/8/12/16/20/24/9999px)。
|
||||||
|
|
||||||
|
### 2.8 阴影
|
||||||
|
|
||||||
|
| Token | 浅色值 | 深色值 | 计数 |
|
||||||
|
| --- | --- | --- | ---: |
|
||||||
|
| `--yc-shadow-input` | `rgba(0,0,0,.03) 0 16px 32px -8px, rgba(0,0,0,.06) 0 2px 4px -4px` | `0 1px 2px rgba(0,0,0,.3), 0 4px 12px rgba(0,0,0,.35)` | 3 |
|
||||||
|
| `--yc-shadow-popover` | `0 4px 16px rgba(0,0,0,.1)` | 不重映射 | 1 |
|
||||||
|
| `--yc-shadow-dialog` | `0 12px 32px rgba(0,0,0,.16)` | `0 12px 32px rgba(0,0,0,.5)` | 1 |
|
||||||
|
|
||||||
|
### 2.9 动效
|
||||||
|
|
||||||
|
| Token | 值 | 计数 |
|
||||||
|
| --- | --- | ---: |
|
||||||
|
| `--yc-motion-fast` | `0.1s` | 3 |
|
||||||
|
| `--yc-motion-medium` | `0.15s` | 1 |
|
||||||
|
| `--yc-motion-slow` | `0.2s` | 1 |
|
||||||
|
| `--yc-motion-easing` | `cubic-bezier(0.4, 0, 0.2, 1)` | 1 |
|
||||||
|
|
||||||
|
恢复层使用自己的时长/缓动闭包(实测出现值):时长 `.1s / .15s / .2s / .3s / .4s`,缓动 `cubic-bezier(.2,0,0,1)`、`cubic-bezier(.4,0,.2,1)`、`cubic-bezier(.4,0,1,1)`、`cubic-bezier(0,0,.2,1)`、`linear`。
|
||||||
|
|
||||||
|
### 2.10 布局常量
|
||||||
|
|
||||||
|
| Token | 值 | 计数 | 备注(来源文件内实测说明) |
|
||||||
|
| --- | ---: | ---: | --- |
|
||||||
|
| `--yc-layout-sidebar-expanded` | 280px | 2 | 实测 @1334 窗口;@1276 实测为 240px,宽度本身可变 |
|
||||||
|
| `--yc-layout-sidebar` | = expanded | 5 | 当前展开默认 |
|
||||||
|
| `--yc-sidebar-rail-width` | 44px | 1 | 收起后的窄轨 |
|
||||||
|
| `--yc-layout-right` | 647px | 4 | 详情面板实测宽 |
|
||||||
|
| `--yc-layout-right-min` | 320px | 1 | 最小宽 |
|
||||||
|
| `--yc-content-max` | 832px | 4 | `.cr-message-list__content` |
|
||||||
|
| `--yc-content-inline-pad` | 40px | 1 | `--yc-space-9` |
|
||||||
|
| `--yc-input-max` | 784px | 6 | `.cr-input-box` |
|
||||||
|
| `--yc-titlebar-height` | 30px | 6 | 顶部条带 |
|
||||||
|
| `--yc-titlebar-leading` | 92px | 3 | 左侧操作区 |
|
||||||
|
| `--yc-window-control-width` | 46px | 2 | 单钮宽 |
|
||||||
|
| `--yc-titlebar-menu-min-width` | 200px | 2 | 菜单最小宽 |
|
||||||
|
| `--yc-window-control-icon` | 10px | 3 | 窗口控制图标 |
|
||||||
|
| `--yc-dialog-width / -narrow-width` | 560px / 420px | 1 / 1 | 对话框 |
|
||||||
|
|
||||||
|
### 2.11 实测一次性常量(非标准阶,具名后组件不写裸像素)
|
||||||
|
|
||||||
|
选取后续组件最常复用的一组(全部定义在 `styles/tokens.css`「实测常量」段):
|
||||||
|
|
||||||
|
| Token | 值 | Token | 值 |
|
||||||
|
| --- | ---: | --- | ---: |
|
||||||
|
| `--yc-scrollbar-size` | 6px | `--yc-input-min-height` | 96px |
|
||||||
|
| `--yc-scrollbar-thumb-alpha` | 0.2 | `--yc-input-editor-min-height` | 64px |
|
||||||
|
| `--yc-resizer-hit` | 6px | `--yc-input-max-height` | 200px |
|
||||||
|
| `--yc-sidebar-item-height` | 31px | `--yc-input-box-white-height` | 144px |
|
||||||
|
| `--yc-sidebar-section-label-height` | 30px | `--yc-input-editor-home` | 74px |
|
||||||
|
| `--yc-sidebar-section-head-height` | 34px | `--yc-input-footer-row` | 40px |
|
||||||
|
| `--yc-sidebar-tab-height` | 32px | `--yc-prompt-row-height` | 38px |
|
||||||
|
| `--yc-sidebar-footer-height` | 57px | `--yc-prompt-row-step` | 50px |
|
||||||
|
| `--yc-discover-height` | 28px | `--yc-msg-header-height` | 24.5px |
|
||||||
|
| `--yc-user-menu-height` | 40px | `--yc-bubble-max-height` | 310px |
|
||||||
|
| `--yc-user-avatar-size` | 28px | `--yc-tool-body-max-height` | 320px |
|
||||||
|
| `--yc-icon-btn-sm / -md` | 24px / 32px | `--yc-panel-tab-min-width / -max-width` | 80px / 240px |
|
||||||
|
| `--yc-badge-size / -offset` | 14px / 2px | `--yc-home-content-width` | 800px |
|
||||||
|
| `--yc-segmented-height` | 32px | `--yc-home-quick-actions-width` | 680px |
|
||||||
|
| `--yc-segmented-item-width` | 72px | `--yc-home-title-size / -line-height` | 30px / 42px |
|
||||||
|
| `--yc-dialog-width` | 560px | `--yc-empty-desc-max-width` | 324px |
|
||||||
|
| `--yc-line-height-19 / -21 / -245 / -2625` | 19 / 21 / 24.5 / 26.25px | `--yc-letter-spacing-badge` | 0.2px |
|
||||||
|
|
||||||
|
## 3. 组件清单
|
||||||
|
|
||||||
|
层级标记:**[Y]** = YCode 自有实现;**[R]** = REA 恢复组件(生成物,勿手改);**[L]** = 本地旧类/一次性实现(迁移目标见第 6 节)。
|
||||||
|
|
||||||
|
### 3.1 可复用组件矩阵
|
||||||
|
|
||||||
|
| 组件 | 代码 path | 类名 / 变体 | 层级 | 备注(实测) |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| Button / IconButton | `src/renderer/src/components/task-confirm-dialog.tsx`、`components/SourceChatHeader.tsx`、`components/ArchivedTasksDialog.tsx`、`workbuddy/user-menu/AccountMenu.tsx`、`projects/ProjectDetailPage.tsx`、`capabilities/*Page.tsx` | `wb-button` + `--primary(16) / --secondary(23) / --grey(未用) / --ghost(13) / --link(3) / --danger(4) / --icon(9) / --circle(5) / --loading(2) / --disabled(2) / --full(1)`;尺寸 `--small(14) / --medium(41) / --xsmall(1)`;内部 `wb-button__content(24)`、`wb-button__icon(29)` | [R] 样式(各 scope 闭包) + [Y] 调用点 | 共 247 次类名出现 / 18 个 TSX 文件;`<button>` 原生 362 个 |
|
||||||
|
| IconButton(本地) | `src/renderer/src/styles/app.css:934-949`、`components/Sidebar.tsx:346`、`components/NotificationsPanel.tsx:148` | `.btn-icon` | [L] | 与 `wb-button--icon/circle` 并存的第三套图标按钮 |
|
||||||
|
| Button(本地) | `src/renderer/src/styles/app.css:949-981` | `.btn`、`.btn--primary`、`.btn--ghost` | [L] | 12 处 TSX 使用:`components/AboutDialog.tsx:50/53`、`components/ChatPane.tsx:499-526`、`components/SettingsDialog.tsx:80` |
|
||||||
|
| Button(页面级) | `src/renderer/src/automations/automation-page.css` | `.automation-button`、`--primary / --secondary / --danger` | [L] | 自动化页专用 |
|
||||||
|
| Input / Textarea | `components/ModelSettingsPanel.tsx`、`components/EditableModelSelect.tsx`、`components/WorkspaceDialog.tsx`、`components/SourceWorkspacePicker.tsx`、`capabilities/ExpertsPage.tsx`、`projects/ProjectListPage.tsx` | `wb-input`、`wb-input-wrapper`、`wb-input__inner / __prefix / __suffix / __clear / __icon-button / __count / __tips`、`wb-input--filled / --status-error / --with-affix`、`wb-input-wrapper--small/medium/filled/disabled`、`wb-input-group__addon`;文本域 `wb-textarea` | [R] 样式 + [Y] 调用点 | `<input>` 48、`<textarea>` 8 |
|
||||||
|
| Textarea(本地) | `projects/ProjectInstructionDrawer.tsx:25`、`capabilities/ConnectorsPage.tsx:25`、`components/MemorySettingsPanel.tsx:293/472`、`components/PersonalizationSettingsPanel.tsx:336` | `.project-instruction-drawer__textarea`、`.mcp-config-textarea`、`.memory-settings-panel__manage-editor`、`.local-memory-editor` | [L] | 四套局部文本域样式 |
|
||||||
|
| Select | `components/source-select/SourceSelect.tsx`(宿主,渲染 `<span class="source-select-host">`)、`components/source-select/RecoveredSelectCore.tsx`、`components/source-select/SourceSelectPopover.tsx` | `wb-select__panel / __list / __option / __option--selected / __option--single-selected / __option-label / __empty`、`wb-select-popover`;宿主 props:`size: small/medium/large`、`shape`、`placement`、`showSearch`、`allowClear`、`portalRoot` | [R] 核心 + [Y] 包装 | 另有 `components/EditableModelSelect.tsx`、`components/SourceModelPicker.tsx`(`.source-model-picker`) |
|
||||||
|
| Select(原生) | `src/renderer/src/automations/AutomationRecords.tsx:100,112` | `<select>` | [L] | 全仓库仅 2 处 |
|
||||||
|
| Dialog | `components/task-confirm-dialog.tsx` + `components/task-confirm-dialog.css`、`components/ArchivedTasksDialog.tsx` + `.css`、`components/WorkspaceDialog.tsx` + `components/workspace-source.css`、`components/SettingsDialog.tsx` + `components/settings-shell-source.css` | `wb-modal`、`wb-modal__overlay / __overlay--center / __header / __header-row / __title / __title-wrap / __body / __footer / __footer--right / __footer-actions / __close`、`wb-modal-anim-open`;设置页用 `.settings-modal`、`.settings-modal-overlay` | [R] | 另有 `components/AboutDialog.tsx`(本地 `.dialog`)、`workbuddy/composer/permission/SourcePermissionControl.tsx`(确认弹窗) |
|
||||||
|
| Popover / Menu | `components/AppDiscoverPopover.tsx` + `components/app-discover-popover.css`、`workbuddy/user-menu/AccountMenu.tsx` + `account-menu-source.css`、`components/task-menu.tsx` + `task-menu.css`、`projects/ProjectMenu.tsx` + `project-menu.css`、`components/source-select/SourceSelectPopover.tsx` | `wb-popover`、`wb-popover--placement-top`、`wb-dropdown`、`wb-dropdown__item / __list / __label / __check / __icon / __search / __scroll / __footer`、`wb-dropdown--single / --scrollable`;YCode 侧 `.yc-project-more`、`.conversation-context-menu` | [R]+[Y] | 侧栏菜单在 `.chat-pane.cr-theme` 之外,`task-menu.css` 显式给出等价 token |
|
||||||
|
| Tabs / Segmented | `components/SourceSettingsNavigation.tsx` + `components/settings-shell-source.css`(`.settings-navigation__item` 等)、`components/Sidebar.tsx:369-415` + `styles/sidebar.css`(`.conversation-list-tab-button`)、`components/SourceHome.tsx`(`.wb-scene-tabs`,样式在 `source-home-recovered.css`)、`capabilities/ExpertsPage.tsx` | Segmented:`wb-segmented`、`wb-segmented__item / __item--active / __item-label / __thumb / __thumb--measured`;Tabs:`conversation-list-tab-button`、`wb-scene-tabs__pill` | [R] 样式 + [Y] 容器 | 无 `wb-tabs` 类(全仓库 0 次);Segmented 尺寸 token 见 2.11 |
|
||||||
|
| Checkbox | `components/source-select/RecoveredSelectCore.tsx`、`components/ModelSettingsPanel.tsx`、`workbuddy/composer/permission/SourcePermissionControl.tsx` | `wb-checkbox`、`__box / __control / __former / __icon / __input / __label`、`--checked / --disabled / --indeterminate / --readonly` | [R] | 样式闭包在 `model-settings.css`、`source-select-recovered.css`、`source-permission-recovered.css` |
|
||||||
|
| Switch | `components/GeneralSettingsPanel.tsx`、`components/MemorySettingsPanel.tsx`、`components/PersonalizationSettingsPanel.tsx`、`workbuddy/composer/permission/SourcePermissionControl.tsx`、`capabilities/SkillsPage.tsx` | `wb-switch`、`__track / __thumb / __input`、`--small / --large / --danger` | [R] | 主要样式与主题化在 `components/settings-shell-source.css` |
|
||||||
|
| Card | `components/ModelSettingsPanel.tsx`(`.models-settings-panel__card`,`model-settings.css:6`)、`components/AppearanceSettingsPanel.tsx`(`.appearance-card`,`source-appearance-panel-recovered.css`)、`components/Sidebar.tsx`(`.conversation-list-invite-friends__card`,`sidebar-shell-source.css`)、`components/ToolCard.tsx`(`.tool-card*`,`styles/app.css:425+`)、`workbuddy/conversation/MessageQuoteCard.tsx`(`.cr-input-quote-card*`,`message-quote-source.css`)、`capabilities/ExpertsPage.tsx`(`.expert-card`,`expert-source.css`) | 无统一 Card 组件;无 `wb-card` 类(0 次) | 混合 | 6 套独立卡片样式 |
|
||||||
|
| ListItem / NavItem | `components/SourceWorkspacePicker.tsx`(`wb-dropdown__item`)、`components/SourceSettingsNavigation.tsx`(`.settings-navigation__item`)、`components/Sidebar.tsx`(`.sidebar__item*`、`.conversation-item`,`styles/sidebar.css` + `components/workspace-source.css`)、`capabilities/SkillsPage.tsx`(`wb-dropdown__item`) | `wb-dropdown__item--selected / --empty`、`.settings-navigation__item--active`、`.sidebar__item--menu-open` | [R]+[Y] | 侧栏列表项几何:31px 高、`padding 5px 12px`(`tokens.css` 注释实测) |
|
||||||
|
| Badge / Tag | 使用点:`components/Sidebar.tsx:369/415/596`(`.sidebar__version-badge`、`.conversation-list-version-badge`、`.sidebar__tab-beta`、`.msg-center-badge`)、`capabilities/ConnectorsPage.tsx:84`(`.mcp-installed-badge`)、`projects/ProjectDetailPage.tsx:527`(`.task-item__source-badge`)、`components/AppearanceSettingsPanel.tsx:41`(`.appearance-badge--validity`)、`components/ModelSettingsPanel.tsx:224`(`models-settings-panel__connection-failed-badge`) | 无统一 Badge;`wb-tag` 仅在 `capabilities/expert-source.css`、`components/workspace-source.css` 有样式,TSX 无使用 | 混合 | `styles/app.css:1417-1443` 还有无双基 `.badge / .badge--ok / .badge--warn`,TSX 未使用 |
|
||||||
|
| EmptyState | `capabilities/ExpertsPage.tsx`(`.capability-expert-empty`,`expert-source.css`;插图 `capabilities/assets/empty-created-expert.svg`)、`components/Sidebar.tsx`(`.conversation-list-empty`、`.sidebar__empty`,`sidebar-shell-source.css` / `sidebar.css`)、`automations/AutomationPage.tsx`(`.automation-empty-state`)、`library/LibraryPage.tsx`(经 `library.css` `.library-feed-empty`、`.library-local-empty`) | 每页各写一套;恢复层另有 `colleague-chat-empty-profile`(`styles/tokens.css` 中记录其 gap/尺寸实测) | 混合 | 无 `wb-empty` 类(0 次) |
|
||||||
|
| Avatar | `components/SourceAgentAvatar.tsx`(REA 生成的固定 SVG,ev_d6e6920f…)、`workbuddy/user-menu/AccountMenu.tsx`(用户头像,28px,`--yc-user-avatar-size`)、`components/MessageBubble.tsx`(`.assistant-avatar`,`workbuddy/conversation/source-styles.css`)、`workbuddy/subagents/*`(`.yc-member-avatar`,`subagents/adapters.css`) | `.wb-avatar` 样式仅在 `projects/source-project-detail-recovered.css` 与 `styles/tokens.css` 注释中出现;无 `wb-avatar` JSX 使用 | 混合 | `.avatar*`/`avatar-item` 等类名还散落在若干页面 |
|
||||||
|
| Spinner | `components/ArchivedTasksDialog.tsx`(`.archived-tasks-spin`)、`automations/AutomationPage.tsx`(`.automation-running-spinner`)、`workbuddy/artifacts/ArtifactContentRenderer.tsx`、`workbuddy/mermaid/MermaidBlock.tsx`(`.loading-spinner`)、`workbuddy/conversation/MessageEditorIcons.tsx`(`.cr-input-spin`)、`workbuddy/subagents/*`(`.yc-member-spinner`) | 关键帧:`wb-icon-spin`(`components/source-select/source-select-recovered.css`);无 `wb-spinner` 类 | 混合 | 各模块自带旋转样式 |
|
||||||
|
| Skeleton | `library/LibraryPageParity.tsx`(`.library-skeleton`、`.library-feed-skeleton`、`.library-local-skeleton`,`library/library.css`)、`components/sidebar-shell-source.css`(`.conversation-list-skeleton`)、`capabilities/connectors-source.css`(`.connector-skeleton*`)、`components/settings-panel-loader.tsx` + `styles/settings-entry.css`(`.settings-panel-loading`) | 无 `wb-skeleton` 类 | [Y]+[R] | 4 套骨架模式 |
|
||||||
|
| Tooltip | `components/source-select/RecoveredSelectTooltip.tsx`、`workbuddy/composer/context-info/ComposerContextInfo.tsx:189`(`wb-tooltip wb-tooltip--placement-top wb-tooltip--fade-open`、`wb-tooltip__content / __arrow`)、`workbuddy/conversation/UserMessageEditor.tsx:20`(选择器白名单引用 `.wb-tooltip`) | `wb-tooltip--placement-* / --interactive / --no-padding / --fade-*`;token:`--wb-tooltip-z-index:1070`(运行时兜底) | [R] | 样式闭包在 `sidebar-shell-source.css`、`source-select-recovered.css`、`composer/context-info/source.css`、`composer/model-selector/source.css`、`conversation/question-source.css` |
|
||||||
|
| Toast / Notice | `components/ModelSettingsPanel.tsx:219`(`.model-ui-toast`,`model-settings.css:406`,fixed 顶部居中、z-index 1200)、`capabilities/SkillsPage.tsx:107`(`.yc-skill-notice` / `.yc-skill-error`,`skills-host.css`)、`workbuddy/subagents/SubagentDetailPanel.tsx:72-75` 与 `components/RightSidebar.tsx:32`(`.yc-member-notice`,`subagents/adapters.css`)、`workbuddy/message-actions/MessageActionToolbar.tsx`(`.wb-message-actions__pending / __error`) | 无统一 Toast;恢复层只保留 `--wb-message-*` token(`capability-center-source.css` 闭包内),无 JSX 宿主 | 混合 | `styles/app.css:1448-1462` 的 `.notice`(含 dark 规则)在 TSX 中查无使用 |
|
||||||
|
|
||||||
|
### 3.2 有样式、无 JSX / 已废弃的入口
|
||||||
|
|
||||||
|
| 项 | 位置 | 现状 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `.notice` | `styles/app.css:1448`、`:1458`(dark) | TSX 无 `className="notice"` |
|
||||||
|
| `.badge / .badge--ok / .badge--warn` | `styles/app.css:1417-1443` | TSX 无使用 |
|
||||||
|
| `wb-tag` | `capabilities/expert-source.css`、`components/workspace-source.css` | 只有恢复样式,无 TSX 宿主 |
|
||||||
|
| `--wb-message-*`(Toast/Message 组件 token) | `capabilities/capability-center-source.css` | 只有 token 闭包,无组件宿主 |
|
||||||
|
| `.input / .input--mono`、`.dialog / .dialog__actions`、`.menu-dropdown*` | `styles/app.css` | 本地旧类;`.dialog` 由 `components/AboutDialog.tsx`、`components/ChatPane.tsx` 使用,`.menu-dropdown*` 由 `components/TitleBar.tsx` 使用 |
|
||||||
|
|
||||||
|
### 3.3 图标与原生控件现状
|
||||||
|
|
||||||
|
- 图标模块 25 个(`components/icons.tsx`、`components/icons-extra.tsx`、`components/general-settings-icons.tsx`、`components/task-menu-icons.tsx`、`components/SourceChatHeaderIcons.tsx`、`components/source-select/SourceSelectIcons.tsx`、`capabilities/{ConnectorSourceIcons,SkillSourceIcons}.tsx`、`library/{LibraryIcons,SourceLibraryIcons,SourceLibraryChromeIcons}.tsx`、`automations/automation-source-icons.tsx`、`projects/ProjectIcon.tsx`、`workbuddy/**/{*Icons,icons,account-menu-icons}.tsx`)。
|
||||||
|
- 内联 `<svg>` 164 个,其中仅 69 个位于 `*Icons.tsx`,其余内联在页面/组件里。
|
||||||
|
- 原生控件:`<button>` 362 / `<input>` 48 / `<textarea>` 8 / `<select>` 2(`automations/AutomationRecords.tsx`)。
|
||||||
|
- `wb-icon` 类在多个恢复闭包中有独立尺寸规则(例:`source-select-recovered.css` 中 `.wb-icon` 与 `wb-icon--spin` 动画)。
|
||||||
|
|
||||||
|
## 4. 约定(当前实际遵守 / 未遵守的规矩)
|
||||||
|
|
||||||
|
| 主题 | 现状 | 证据 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 类名体系 | 四类并存:`wb-*`(原版组件/恢复层)、`yc-*`(YCode 自有宿主与页面)、恢复家族 `cr-*/cb-*/ec-*/sm-*/dc-*`、原版 CSS Module 哈希 `_name_hash_index` | 见 1.2;`yc-*` 选择器 59 个(唯一) |
|
||||||
|
| 组件实现方式 | 无组件库:全部是"类名 + 手写 JSX";按钮/输入/弹窗在每个页面各自拼类名 | `package.json` 依赖无 UI kit;`wb-button` 247 次、`wb-input*` 多处手拼 |
|
||||||
|
| 无 Tailwind | 仓库无 `tailwind.config.*` / `postcss.config.*`(仅有 postcss 传递依赖),源码无 `@apply` | 根目录文件列表 + `package.json` |
|
||||||
|
| 无 `cn` / `clsx` / classnames 依赖 | `package.json` 无该依赖;全仓库唯一的拼接 helper 是 `components/ModelSettingsPanel.tsx:41` 的 `const cx = (name) => prefix + name`(前缀 `models-settings-panel__`),只做加前缀,不做条件合并 | 代码检索 |
|
||||||
|
| className 处理 | 直接字符串字面量或模板字符串拼接(如 `className={'a ' + b}`、`[..].filter(Boolean).join(' ')`,后者出现在恢复运行时) | TSX 全量检索 |
|
||||||
|
| 主题切换 | `document.documentElement.dataset.theme = 'light' | 'dark'`,由 `src/renderer/src/useSharedTheme.ts` 统一写入(`localStorage['ycode:theme']`,两个窗口共享);组件通过 `html[data-theme='dark']` 重映射 | `useSharedTheme.ts:8-20`、`App.tsx:41`、`SettingsDialog.tsx:95`、`tokens.css:352` |
|
||||||
|
| 恢复模块的并行主题属性 | 宿主还会在子树上打 `data-cr-theme="dark|light"`(以及 `data-theme`),供 `[data-cr-theme=dark]` 恢复规则命中 | `components/ChatPane.tsx:420`、`components/SourceModelPicker.tsx:47`、`workbuddy/composer/add-menu/SourceComposerAddMenu.tsx:29`、`workbuddy/subagents/SubagentDetailPanel.tsx:37` 等 |
|
||||||
|
| 平台/桌面标记 | `body[data-electron-desktop]`、`body[data-platform]` 由入口写入;恢复 CSS 另有 `body.wb-mobile-layout` 分支 | `main.tsx:16-17`、`settings.tsx:11-12`、`settings-shell-source.css` |
|
||||||
|
| 字号档位 | `--yc-font-offset-global` 由 `fontSize.ts` 写入;`.cb-font-size-fixed` 边界内重解析,防止"双重放大" | `fontSize.ts`、`tokens.css` 末尾块 |
|
||||||
|
| 恢复文件不可手改 | `*-source.css` / `*-recovered.css` 带生成头(REA ev id + SHA),由 `scripts/rea/recover-*.mjs` 生成;页面通过 `@import` 或 TSX import 引入(例:`components/source-home.css:1`) | 13 个 CSS 带 `generated by / Recovered from` 头;`scripts/rea/` 下 80+ recovery 脚本 |
|
||||||
|
| 打包入口 | 主窗口与设置窗口共用 `window.ycode` 假桥(`scripts/verify-ui.mjs`),命名空间现覆盖 24 个(与渲染进程使用的 24 个一致) | 见 5.5 |
|
||||||
|
|
||||||
|
## 5. 例外与风险
|
||||||
|
|
||||||
|
### 5.1 重复调色板作用域
|
||||||
|
|
||||||
|
- 13 个文件重复定义 `--wb-palette-brand-8`(含浅色/深色两套),根上没有权威 `--wb-palette-*`。
|
||||||
|
- 组件一旦被移出 `.source-*` / `.models-settings-panel` / `.yc-capability-center` / `.chat-pane.cr-theme` 等宿主,就会掉色、掉圆角、掉主题。
|
||||||
|
- 新组件引用 `--wb-*` 只有在正确作用域内才成立;引用 `--yc-*` 才有全局保证(且仍需注意 1.4 的局部重声明)。
|
||||||
|
|
||||||
|
### 5.2 作用域之外的尺寸/图标 token 未定义
|
||||||
|
|
||||||
|
- `--wb-size-control-*`、`--wb-icon-size-*` 只在闭包内出现(`capability-center-source.css:8`、`model-settings.css:146`、`source-styles.css:7` 等),排版 token 中引用它们的 `--wb-button-height-*`、`--wb-button-icon-size-*` 在根作用域不可解析。
|
||||||
|
|
||||||
|
### 5.3 悬空 token 引用
|
||||||
|
|
||||||
|
- `--wb-font-line-height-9`(7 个文件引用)与 `--wb-font-line-height-6-2`(多个恢复文件引用)全仓库无定义;`--yc-line-height-6-2: 26px` 是同值硬写替代。
|
||||||
|
- 影响 `--wb-font-spec-h2-line-height`、`--wb-font-spec-h5-line-height` 及部分恢复规则的 line-height 声明(部分浏览器会按 invalid-at-computed-value 处理)。
|
||||||
|
|
||||||
|
### 5.4 加载顺序不是声明顺序
|
||||||
|
|
||||||
|
- 入口先 `import App/SettingsWindow`、后导入样式,打包后组件自带 CSS 先落地(实测 `app-h0tta3aw.css` 首条规则来自 `source-select-recovered.css`)。
|
||||||
|
- 结果:`styles/app.css`、`styles/sidebar.css` 与恢复层、settings-shell 的相对顺序依赖构建产物,而非源码书写意图。
|
||||||
|
|
||||||
|
### 5.5 `scripts/verify-ui.mjs` 假桥状态(相对方案文档已变化)
|
||||||
|
|
||||||
|
- 方案文档记录"假桥缺少 `window.ycode.library`";**本次实测该断言已过期**:`scripts/verify-ui.mjs` 的 STUB 现定义 24 个命名空间(`window/edit/app/config/general/personalization/agents/memory/models/workspace/session/chat/experts/skills/mcp/projects/automation/library/attachments/speech/git/browser/terminal/resources`),与渲染进程实际使用的 24 个一致。
|
||||||
|
- 残留风险:假桥是**手工维护的 preload 形状副本**(无类型约束、返回值多为空集合),新增命名空间后必须同步,否则页面静默半挂载;它只能保证"能挂载",不能保证数据态渲染。
|
||||||
|
- 同批新增的静态守护 `scripts/check-design-tokens.mjs` + `scripts/design-token-baseline.json`(入口 `npm run check:tokens`)扫描 `src/renderer/src` 的裸 hex、裸 px、`!important`、`--wb-*` 定义、`var(--yc-*)`/`var(--wb-*)` 使用;门禁分类为 `core / local / adapter`,`source / recovered / vendor` 只记录不阻塞。本次实测 `node scripts/check-design-tokens.mjs --json`(334 个文件):`ycVarUses` 1,372、`wbVarUses` 22,766、`important` 926,与本文件第 0 节手工统计完全一致;对比口径时以该 JSON 为准(它按分类拆分,本文件第 5.6 节只列头部文件)。
|
||||||
|
|
||||||
|
### 5.6 `!important` 与 CRLF
|
||||||
|
|
||||||
|
- `!important` 总量 926,集中在恢复/投影文件:
|
||||||
|
|
||||||
|
| 文件 | 数量 |
|
||||||
|
| --- | ---: |
|
||||||
|
| `projects/source-project-detail-recovered.css` | 287 |
|
||||||
|
| `projects/source-projects-recovered.css` | 272 |
|
||||||
|
| `components/workspace-source.css` | 108 |
|
||||||
|
| `workbuddy/workbench/source-code-assets/editor-gwlzV742.css` | 97 |
|
||||||
|
| `components/sidebar-shell-source.css` | 19 |
|
||||||
|
| `capabilities/created-expert-source.css` | 16 |
|
||||||
|
| `workbuddy/composer/add-menu/source.css`、`components/source-memory-settings-recovered.css` | 各 13 |
|
||||||
|
| `components/source-home-recovered.css`、`workbuddy/user-menu/account-menu-source.css`、`workbuddy/workbench/panel-motion-source.css` | 各 11 |
|
||||||
|
|
||||||
|
本次点名的四个核心文件:`capabilities/capability-center-source.css` 0、`components/model-settings.css` 3、`components/source-home-recovered.css` 11、`workbuddy/conversation/source-styles.css` 3。
|
||||||
|
- 换行符:320 个 CSS/TSX/TS 文件为 LF,**11 个为 CRLF**:`capabilities/ExpertsPage.tsx`、`capabilities/SkillsPage.tsx`、`components/ChatPane.tsx`、`components/MessageBubble.tsx`、`components/model-settings.css`、`components/ModelSettingsPanel.tsx`、`components/PersonalizationSettingsPanel.tsx`、`components/RightSidebar.tsx`、`components/Sidebar.tsx`、`workbuddy/conversation/SourceWriteFileView.tsx`、`workbuddy/conversation/ToolCallRenderer.tsx`。跨文件 diff 会整文件抖动。
|
||||||
|
|
||||||
|
### 5.7 深色主题按作用域重写
|
||||||
|
|
||||||
|
- 除了 `tokens.css` 的 18 条根级重映射,深色还分别写在:`model-settings.css:147`、`source-home-recovered.css:3`、`source-select-recovered.css:3`、`source-workspace-picker-recovered.css:3`、`source-chat-header-recovered.css:3`、`source-model-picker-recovered.css:3`、`source-settings-navigation-recovered.css:3`、`source-general-settings-recovered.css`、`source-agents-panels-recovered.css:7`、`source-about-help-panels-recovered.css:4`、`source-appearance-panel-recovered.css:4`、`source-memory-settings.css:41`、`settings-shell-source.css:6`、`library/library.css:44`、`projects/project-menu.css:20`、`automations/automation-page.css:1625,2730`、`components/app-discover-popover.css:49`、`workbuddy/workbench/workbench-adapters.css:48-50`、`workbuddy/subagents/theme.css` 等,且部分恢复规则同时识别 `body.dark / body.vscode-dark / .vscode-dark`。
|
||||||
|
- 风险:主题值多份拷贝,任一处漏改会出现"局部亮块"。
|
||||||
|
|
||||||
|
### 5.8 已知组件层重复
|
||||||
|
|
||||||
|
- 按钮 3 套(`wb-button` / `.btn+.btn-icon` / `.automation-button`),输入框 ≥4 套,弹窗 2 套(`wb-modal` / `.dialog`),提示 4 套(`model-ui-toast` / `yc-skill-notice` / `yc-member-notice` / `.notice`),空态每页一套,骨架 4 套,图标 25 个模块 + 95 处散落内联 SVG(164 − 69,见 3.3)。
|
||||||
|
|
||||||
|
## 6. 目标层(指向方案,不在此展开)
|
||||||
|
|
||||||
|
目标架构、兼容层桥接规则、组件抽取优先级(Button/Input/Select/Dialog/Popover/Tabs → Checkbox/Switch/Card/ListItem/Badge/EmptyState → Avatar/Spinner/Tooltip/Toast)与 P0→P4 迁移顺序,全部以 `docs/设计系统提取与统一方案-2026-10-18.md` 为准。组件接口契约(命名规范、变体矩阵、禁止事项)以同批产出的 `docs/界面基础组件规范-2026-10-18.md` 为准(如该文件存在);本文件第 5.5 节已回填 P0 两项工具的当前实测状态(`npm run check:tokens` 静态守护、修正后的 `scripts/verify-ui.mjs` 假桥),后续 Build / Audit 以这两个入口 + 本文件第 2 节取值为改动前后的基线。
|
||||||
|
|
||||||
|
## 附录:复核命令
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 权威层与声明数
|
||||||
|
rg -c -- "--yc-[a-z0-9-]+\s*:" src/renderer/src/styles/tokens.css
|
||||||
|
rg -c -- "--wb-[a-z0-9-]+\s*:" src/renderer/src/workbuddy/typography/tokens.css
|
||||||
|
|
||||||
|
# 作用域重复调色板
|
||||||
|
rg -l -- "--wb-palette-brand-8:" src/renderer/src
|
||||||
|
|
||||||
|
# 悬空 token
|
||||||
|
rg -l -- "var\(--wb-font-line-height-9\)|var\(--wb-font-line-height-6-2\)" src/renderer/src
|
||||||
|
rg -n -- "--wb-font-line-height-(9|6-2):" src/renderer/src
|
||||||
|
|
||||||
|
# 加载顺序(源码)
|
||||||
|
Select-String -Path src/renderer/src/main.tsx,src/renderer/src/settings.tsx -Pattern "import '.+\.css'"
|
||||||
|
|
||||||
|
# 加载顺序(产物)
|
||||||
|
Select-String -Path out/renderer/index.html -Pattern '\.css'
|
||||||
|
|
||||||
|
# 假桥覆盖面
|
||||||
|
rg -o -- "window\.ycode\.[a-zA-Z]+" src/renderer/src --glob "*.tsx" --glob "*.ts" | Sort-Object -Unique
|
||||||
|
|
||||||
|
# !important / CRLF 抽查
|
||||||
|
rg -o -F "!important" src/renderer/src --glob "*.css" | Measure-Object
|
||||||
|
```
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
"sandbox:maintain": "node scripts/sandbox-maintain.mjs",
|
"sandbox:maintain": "node scripts/sandbox-maintain.mjs",
|
||||||
"start": "electron-vite preview",
|
"start": "electron-vite preview",
|
||||||
"typecheck": "tsc --noEmit -p tsconfig.node.json && tsc --noEmit -p tsconfig.web.json",
|
"typecheck": "tsc --noEmit -p tsconfig.node.json && tsc --noEmit -p tsconfig.web.json",
|
||||||
|
"check:tokens": "node scripts/check-design-tokens.mjs",
|
||||||
"test:settings-performance": "node scripts/verify-settings-performance.cjs",
|
"test:settings-performance": "node scripts/verify-settings-performance.cjs",
|
||||||
"models:sync-metadata": "node scripts/sync-model-metadata.mjs",
|
"models:sync-metadata": "node scripts/sync-model-metadata.mjs",
|
||||||
"test:custom-models": "esbuild scripts/custom-model-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/custom-model-regression.mjs --log-level=warning && node .tmp/custom-model-regression.mjs",
|
"test:custom-models": "esbuild scripts/custom-model-regression.ts --bundle --platform=node --format=esm --outfile=.tmp/custom-model-regression.mjs --log-level=warning && node .tmp/custom-model-regression.mjs",
|
||||||
|
|||||||
@@ -0,0 +1,501 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* P0 设计 token 静态漂移守护(只读扫描)。
|
||||||
|
*
|
||||||
|
* 扫描范围:src/renderer/src 下的 .css / .tsx / .ts。
|
||||||
|
* 统计:裸 hex、裸 px、!important、--wb-* 定义、var(--yc-*) / var(--wb-*) 使用。
|
||||||
|
*
|
||||||
|
* 用法:
|
||||||
|
* node scripts/check-design-tokens.mjs 对比 baseline,出现回归退出码 1
|
||||||
|
* node scripts/check-design-tokens.mjs --json 输出机器可读 JSON(stdout 仅 JSON)
|
||||||
|
* node scripts/check-design-tokens.mjs --update-baseline 全量重写 baseline(唯一写操作)
|
||||||
|
*
|
||||||
|
* 分类(按顺序匹配):
|
||||||
|
* vendor 路径包含 source-code-assets 目录
|
||||||
|
* recovered 文件名或目录名含 recovered
|
||||||
|
* adapter *-adapter.css / *-adapters.css
|
||||||
|
* source source.css / source-*.css / *-source.css
|
||||||
|
* core 路径包含 styles/ 目录
|
||||||
|
* tokenSource 命中 TOKEN_SOURCE_PATHS 的精确路径(token 源文件,唯一允许写裸值的位置)
|
||||||
|
* local 其余
|
||||||
|
* 门禁分类:core / local / adapter;source / recovered / vendor / tokenSource 只记录与摘要,不阻塞。
|
||||||
|
* 失败条件(仅四项):新增门禁文件含裸 hex / 裸 px / !important / --wb-* 定义;
|
||||||
|
* 存量门禁文件这四项计数高于 baseline。var(--yc-*) / var(--wb-*) 使用数只记录与提示。
|
||||||
|
*/
|
||||||
|
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
||||||
|
import { dirname, extname, join, relative, resolve, sep } from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url))
|
||||||
|
const REPO_ROOT = resolve(SCRIPT_DIR, '..')
|
||||||
|
const SCAN_ROOT = join(REPO_ROOT, 'src', 'renderer', 'src')
|
||||||
|
const SCAN_ROOT_LABEL = 'src/renderer/src'
|
||||||
|
const BASELINE_PATH = join(SCRIPT_DIR, 'design-token-baseline.json')
|
||||||
|
const BASELINE_LABEL = 'scripts/design-token-baseline.json'
|
||||||
|
const BASELINE_VERSION = 1
|
||||||
|
|
||||||
|
const SCAN_EXTENSIONS = new Set(['.css', '.ts', '.tsx'])
|
||||||
|
const CLASS_ORDER = ['vendor', 'recovered', 'adapter', 'source', 'core', 'tokenSource', 'local']
|
||||||
|
const GATED_CLASSES = new Set(['core', 'local', 'adapter'])
|
||||||
|
// token 源文件:唯一允许写裸 hex/px/!important/--wb-* 定义的位置。
|
||||||
|
// 按规范化(小写、/ 分隔)后的精确路径豁免,不做文件名通配,避免页面级 tokens.css 逃逸。
|
||||||
|
const TOKEN_SOURCE_PATHS = new Set([
|
||||||
|
'src/renderer/src/styles/tokens.css',
|
||||||
|
'src/renderer/src/workbuddy/typography/tokens.css',
|
||||||
|
'src/renderer/src/ui/tokens.css'
|
||||||
|
])
|
||||||
|
const METRIC_LABELS = {
|
||||||
|
bareHex: '裸 hex',
|
||||||
|
barePx: '裸 px',
|
||||||
|
important: '!important',
|
||||||
|
wbDefinitions: '--wb-* 定义',
|
||||||
|
ycVarUses: 'var(--yc-*) 使用',
|
||||||
|
wbVarUses: 'var(--wb-*) 使用'
|
||||||
|
}
|
||||||
|
const METRICS = ['bareHex', 'barePx', 'important', 'wbDefinitions', 'ycVarUses', 'wbVarUses']
|
||||||
|
// 门禁计数:新增与存量门禁文件都只对这四个指标的上升失败;var 使用数只提示。
|
||||||
|
const GATE_METRICS = ['bareHex', 'barePx', 'important', 'wbDefinitions']
|
||||||
|
const METRIC_PATTERNS = {
|
||||||
|
bareHex: () => /#[0-9a-fA-F]{3,8}\b/g,
|
||||||
|
barePx: () => /(?<![\w.])\d+(?:\.\d+)?px\b/gi,
|
||||||
|
important: () => /!\s*important/gi,
|
||||||
|
wbDefinitions: () => /--wb-[\w-]+\s*:/g,
|
||||||
|
ycVarUses: () => /var\(\s*--yc-[\w-]*/g,
|
||||||
|
wbVarUses: () => /var\(\s*--wb-[\w-]*/g
|
||||||
|
}
|
||||||
|
const MAX_LISTED = 20
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const options = { updateBaseline: false, json: false, help: false }
|
||||||
|
for (const arg of argv) {
|
||||||
|
if (arg === '--update-baseline') options.updateBaseline = true
|
||||||
|
else if (arg === '--json') options.json = true
|
||||||
|
else if (arg === '--help' || arg === '-h') options.help = true
|
||||||
|
else throw new Error(`未知参数:${arg}`)
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
function printUsage() {
|
||||||
|
console.log(
|
||||||
|
[
|
||||||
|
'用法:node scripts/check-design-tokens.mjs [--json] [--update-baseline]',
|
||||||
|
'',
|
||||||
|
' --json 输出机器可读 JSON(stdout 仅 JSON)',
|
||||||
|
' --update-baseline 全量重写 scripts/design-token-baseline.json',
|
||||||
|
' --help 显示本帮助'
|
||||||
|
].join('\n')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPosix(value) {
|
||||||
|
return value.split(sep).join('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
function zeroCounts() {
|
||||||
|
const counts = {}
|
||||||
|
for (const metric of METRICS) counts[metric] = 0
|
||||||
|
return counts
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCounts(value) {
|
||||||
|
const counts = zeroCounts()
|
||||||
|
if (!value || typeof value !== 'object') return counts
|
||||||
|
for (const metric of METRICS) {
|
||||||
|
const raw = value[metric]
|
||||||
|
counts[metric] = typeof raw === 'number' && Number.isFinite(raw) ? raw : 0
|
||||||
|
}
|
||||||
|
return counts
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSourceCssName(lowerName) {
|
||||||
|
if (!lowerName.endsWith('.css')) return false
|
||||||
|
return lowerName === 'source.css' || lowerName.startsWith('source-') || lowerName.endsWith('-source.css')
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAdapterCssName(lowerName) {
|
||||||
|
return lowerName.endsWith('-adapter.css') || lowerName.endsWith('-adapters.css')
|
||||||
|
}
|
||||||
|
|
||||||
|
function classifyPath(relPath) {
|
||||||
|
if (TOKEN_SOURCE_PATHS.has(relPath.toLowerCase())) return 'tokenSource'
|
||||||
|
const segments = relPath.split('/')
|
||||||
|
const fileName = segments[segments.length - 1]
|
||||||
|
const lowerName = fileName.toLowerCase()
|
||||||
|
const directories = segments.slice(0, -1)
|
||||||
|
if (directories.includes('source-code-assets')) return 'vendor'
|
||||||
|
if (segments.some((segment) => segment.toLowerCase().includes('recovered'))) return 'recovered'
|
||||||
|
if (isAdapterCssName(lowerName)) return 'adapter'
|
||||||
|
if (isSourceCssName(lowerName)) return 'source'
|
||||||
|
if (directories.includes('styles')) return 'core'
|
||||||
|
return 'local'
|
||||||
|
}
|
||||||
|
|
||||||
|
function listScannableFiles(dir) {
|
||||||
|
const results = []
|
||||||
|
const stack = [dir]
|
||||||
|
while (stack.length > 0) {
|
||||||
|
const current = stack.pop()
|
||||||
|
let entries
|
||||||
|
try {
|
||||||
|
entries = readdirSync(current, { withFileTypes: true })
|
||||||
|
} catch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue
|
||||||
|
const fullPath = join(current, entry.name)
|
||||||
|
if (entry.isDirectory()) stack.push(fullPath)
|
||||||
|
else if (entry.isFile() && SCAN_EXTENSIONS.has(extname(entry.name).toLowerCase())) results.push(fullPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results.sort()
|
||||||
|
}
|
||||||
|
|
||||||
|
function analyzeSource(text) {
|
||||||
|
const counts = zeroCounts()
|
||||||
|
for (const metric of METRICS) {
|
||||||
|
const matches = text.match(METRIC_PATTERNS[metric]())
|
||||||
|
counts[metric] = matches ? matches.length : 0
|
||||||
|
}
|
||||||
|
return counts
|
||||||
|
}
|
||||||
|
|
||||||
|
function scanFiles() {
|
||||||
|
const startedAt = Date.now()
|
||||||
|
const files = []
|
||||||
|
const unreadable = []
|
||||||
|
for (const absolutePath of listScannableFiles(SCAN_ROOT)) {
|
||||||
|
const path = toPosix(relative(REPO_ROOT, absolutePath))
|
||||||
|
let text
|
||||||
|
try {
|
||||||
|
text = readFileSync(absolutePath, 'utf8')
|
||||||
|
} catch (error) {
|
||||||
|
unreadable.push({ path, error: String(error && error.message ? error.message : error) })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
files.push({ path, class: classifyPath(path), counts: analyzeSource(text) })
|
||||||
|
}
|
||||||
|
files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
|
||||||
|
return { files, unreadable, durationMs: Date.now() - startedAt }
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBaseline(files) {
|
||||||
|
const entries = {}
|
||||||
|
for (const file of files) {
|
||||||
|
entries[file.path] = { class: file.class, counts: file.counts }
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
version: BASELINE_VERSION,
|
||||||
|
description: '设计 token 静态计数 baseline,由 scripts/check-design-tokens.mjs --update-baseline 生成。不要手改。',
|
||||||
|
scanRoot: SCAN_ROOT_LABEL,
|
||||||
|
metrics: METRICS,
|
||||||
|
gatedClasses: CLASS_ORDER.filter((name) => GATED_CLASSES.has(name)),
|
||||||
|
files: entries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadBaseline() {
|
||||||
|
if (!existsSync(BASELINE_PATH)) return null
|
||||||
|
let parsed
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(readFileSync(BASELINE_PATH, 'utf8'))
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`baseline 不是合法 JSON:${BASELINE_LABEL}(${error.message})`)
|
||||||
|
}
|
||||||
|
if (!parsed || typeof parsed !== 'object' || !parsed.files || typeof parsed.files !== 'object') {
|
||||||
|
throw new Error(`baseline 结构无法识别:${BASELINE_LABEL}`)
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarize(files) {
|
||||||
|
const byClass = {}
|
||||||
|
for (const name of CLASS_ORDER) byClass[name] = { files: 0, counts: zeroCounts() }
|
||||||
|
for (const file of files) {
|
||||||
|
const bucket = byClass[file.class] ?? byClass.local
|
||||||
|
bucket.files += 1
|
||||||
|
for (const metric of METRICS) bucket.counts[metric] += file.counts[metric]
|
||||||
|
}
|
||||||
|
const gated = { files: 0, counts: zeroCounts() }
|
||||||
|
const nonGated = { files: 0, counts: zeroCounts() }
|
||||||
|
for (const name of CLASS_ORDER) {
|
||||||
|
const target = GATED_CLASSES.has(name) ? gated : nonGated
|
||||||
|
target.files += byClass[name].files
|
||||||
|
for (const metric of METRICS) target.counts[metric] += byClass[name].counts[metric]
|
||||||
|
}
|
||||||
|
return { byClass, gated, nonGated }
|
||||||
|
}
|
||||||
|
|
||||||
|
function baselineNonGatedTotals(baselineFiles) {
|
||||||
|
const totals = { files: 0, counts: zeroCounts() }
|
||||||
|
for (const entry of Object.values(baselineFiles)) {
|
||||||
|
if (GATED_CLASSES.has(entry && entry.class)) continue
|
||||||
|
totals.files += 1
|
||||||
|
const counts = normalizeCounts(entry && entry.counts)
|
||||||
|
for (const metric of METRICS) totals.counts[metric] += counts[metric]
|
||||||
|
}
|
||||||
|
return totals
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareWithBaseline(scan, baselineFiles) {
|
||||||
|
const violations = []
|
||||||
|
const notices = { deleted: [], newGatedFilesWithoutCounts: [], usageIncreases: [] }
|
||||||
|
const currentByPath = new Set(scan.files.map((file) => file.path))
|
||||||
|
|
||||||
|
for (const file of scan.files) {
|
||||||
|
const previous = baselineFiles[file.path]
|
||||||
|
if (!previous) {
|
||||||
|
if (!GATED_CLASSES.has(file.class)) continue
|
||||||
|
const hits = GATE_METRICS.filter((metric) => file.counts[metric] > 0)
|
||||||
|
if (hits.length > 0) {
|
||||||
|
violations.push({
|
||||||
|
type: 'new-file',
|
||||||
|
path: file.path,
|
||||||
|
class: file.class,
|
||||||
|
metrics: hits.map((metric) => ({ metric, current: file.counts[metric] }))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
notices.newGatedFilesWithoutCounts.push({ path: file.path, class: file.class })
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (!GATED_CLASSES.has(file.class)) continue
|
||||||
|
const previousCounts = normalizeCounts(previous.counts)
|
||||||
|
const increases = []
|
||||||
|
const usageIncreases = []
|
||||||
|
for (const metric of METRICS) {
|
||||||
|
if (file.counts[metric] > previousCounts[metric]) {
|
||||||
|
const increase = { metric, from: previousCounts[metric], to: file.counts[metric] }
|
||||||
|
if (GATE_METRICS.includes(metric)) increases.push(increase)
|
||||||
|
else usageIncreases.push(increase)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (increases.length > 0) {
|
||||||
|
violations.push({ type: 'regression', path: file.path, class: file.class, increases })
|
||||||
|
}
|
||||||
|
if (usageIncreases.length > 0) {
|
||||||
|
notices.usageIncreases.push({ path: file.path, class: file.class, increases: usageIncreases })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [path, entry] of Object.entries(baselineFiles)) {
|
||||||
|
if (!currentByPath.has(path)) notices.deleted.push({ path, class: (entry && entry.class) ?? 'unknown' })
|
||||||
|
}
|
||||||
|
notices.deleted.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
|
||||||
|
notices.usageIncreases.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
|
||||||
|
|
||||||
|
for (const unreadable of scan.unreadable) {
|
||||||
|
violations.push({ type: 'unreadable', path: unreadable.path, error: unreadable.error })
|
||||||
|
}
|
||||||
|
|
||||||
|
return { violations, notices }
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCounts(counts) {
|
||||||
|
return METRICS.map((metric) => `${METRIC_LABELS[metric]}=${counts[metric]}`).join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDelta(delta) {
|
||||||
|
return METRICS.map((metric) => `${METRIC_LABELS[metric]} ${delta[metric] >= 0 ? '+' : ''}${delta[metric]}`).join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatList(items, limit = MAX_LISTED) {
|
||||||
|
const shown = items.slice(0, limit)
|
||||||
|
const rest = items.length - shown.length
|
||||||
|
return rest > 0 ? `${shown.join(';')};…(还有 ${rest} 项,详见 --json)` : shown.join(';')
|
||||||
|
}
|
||||||
|
|
||||||
|
function describeViolation(violation) {
|
||||||
|
if (violation.type === 'regression') {
|
||||||
|
const detail = violation.increases
|
||||||
|
.map((item) => `${METRIC_LABELS[item.metric]} ${item.from} → ${item.to}(+${item.to - item.from})`)
|
||||||
|
.join(';')
|
||||||
|
return `[回归] ${violation.path}(${violation.class}):${detail}`
|
||||||
|
}
|
||||||
|
if (violation.type === 'new-file') {
|
||||||
|
const detail = violation.metrics.map((item) => `${METRIC_LABELS[item.metric]}=${item.current}`).join(',')
|
||||||
|
return `[新增门禁文件] ${violation.path}(${violation.class}):${detail}`
|
||||||
|
}
|
||||||
|
return `[无法读取] ${violation.path}:${violation.error}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function printHuman(result) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('设计 token 漂移检查(静态)')
|
||||||
|
const fileCount = result.scan.files.length
|
||||||
|
lines.push(` 扫描:${SCAN_ROOT_LABEL} 共 ${fileCount} 个文件(用时 ${result.scan.durationMs} ms)`)
|
||||||
|
if (result.updated) {
|
||||||
|
lines.push(` baseline:已写入 ${BASELINE_LABEL}(${fileCount} 个文件)`)
|
||||||
|
} else {
|
||||||
|
lines.push(` baseline:${BASELINE_LABEL}(${result.baselineFiles} 个文件)`)
|
||||||
|
}
|
||||||
|
const summary = result.summary
|
||||||
|
lines.push(
|
||||||
|
` 门禁分类:${CLASS_ORDER.filter((name) => GATED_CLASSES.has(name))
|
||||||
|
.map((name) => `${name} ${summary.byClass[name].files}`)
|
||||||
|
.join(' | ')}(合计 ${summary.gated.files} 个文件)`
|
||||||
|
)
|
||||||
|
lines.push(` 合计:${formatCounts(summary.gated.counts)}`)
|
||||||
|
lines.push(
|
||||||
|
` 非门禁分类:${CLASS_ORDER.filter((name) => !GATED_CLASSES.has(name))
|
||||||
|
.map((name) => `${name} ${summary.byClass[name].files}`)
|
||||||
|
.join(' | ')}(合计 ${summary.nonGated.files} 个文件,仅记录不阻塞)`
|
||||||
|
)
|
||||||
|
lines.push(` 合计:${formatCounts(summary.nonGated.counts)}`)
|
||||||
|
if (result.nonGatedDelta) {
|
||||||
|
lines.push(` 非门禁相对 baseline:文件 ${result.nonGatedDelta.files >= 0 ? '+' : ''}${result.nonGatedDelta.files};${formatDelta(result.nonGatedDelta.counts)}`)
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
|
||||||
|
if (result.updated) {
|
||||||
|
lines.push('结果:baseline 已更新(退出码 0)。')
|
||||||
|
} else if (result.violations.length === 0) {
|
||||||
|
lines.push('结果:通过,无 token 漂移回归。')
|
||||||
|
} else {
|
||||||
|
lines.push(`结果:失败,共 ${result.violations.length} 项:`)
|
||||||
|
lines.push(...result.violations.map((violation) => ` ${describeViolation(violation)}`))
|
||||||
|
lines.push('')
|
||||||
|
lines.push('提示:确认变更符合预期后运行 `node scripts/check-design-tokens.mjs --update-baseline` 刷新 baseline;')
|
||||||
|
lines.push(' 否则请改用 --yc-* token、复用现有值,或去掉重复的 --wb-* 定义。')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.updated) {
|
||||||
|
const notices = result.notices ?? { deleted: [], newGatedFilesWithoutCounts: [], usageIncreases: [] }
|
||||||
|
if (notices.usageIncreases.length > 0) {
|
||||||
|
lines.push(`提示(不失败,仅记录):${notices.usageIncreases.length} 个门禁文件 token 使用数上升:`)
|
||||||
|
lines.push(
|
||||||
|
...notices.usageIncreases
|
||||||
|
.slice(0, MAX_LISTED)
|
||||||
|
.map(
|
||||||
|
(item) =>
|
||||||
|
` ${item.path}(${item.class}):${item.increases
|
||||||
|
.map((increase) => `${METRIC_LABELS[increase.metric]} ${increase.from} → ${increase.to}(+${increase.to - increase.from})`)
|
||||||
|
.join(';')}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (notices.usageIncreases.length > MAX_LISTED) {
|
||||||
|
lines.push(` …(还有 ${notices.usageIncreases.length - MAX_LISTED} 项,详见 --json)`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (notices.deleted.length > 0) {
|
||||||
|
lines.push(`提示(不失败):baseline 中 ${notices.deleted.length} 个文件已不存在:${formatList(notices.deleted.map((item) => item.path))}`)
|
||||||
|
}
|
||||||
|
if (notices.newGatedFilesWithoutCounts.length > 0) {
|
||||||
|
lines.push(
|
||||||
|
`提示:新增门禁文件 ${notices.newGatedFilesWithoutCounts.length} 个且无受控计数(允许):${formatList(
|
||||||
|
notices.newGatedFilesWithoutCounts.map((item) => `${item.path}(${item.class})`)
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(lines.join('\n'))
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildJsonResult(result) {
|
||||||
|
return {
|
||||||
|
ok: result.violations.length === 0,
|
||||||
|
updated: result.updated,
|
||||||
|
scanRoot: SCAN_ROOT_LABEL,
|
||||||
|
baseline: { path: BASELINE_LABEL, files: result.baselineFiles },
|
||||||
|
metrics: METRICS,
|
||||||
|
gatedClasses: CLASS_ORDER.filter((name) => GATED_CLASSES.has(name)),
|
||||||
|
scan: { files: result.scan.files.length, durationMs: result.scan.durationMs },
|
||||||
|
summary: result.summary,
|
||||||
|
nonGatedDelta: result.nonGatedDelta,
|
||||||
|
violations: result.violations,
|
||||||
|
notices: result.notices
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
let options
|
||||||
|
try {
|
||||||
|
options = parseArgs(process.argv.slice(2))
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error.message)
|
||||||
|
printUsage()
|
||||||
|
process.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (options.help) {
|
||||||
|
printUsage()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const scan = scanFiles()
|
||||||
|
const summary = summarize(scan.files)
|
||||||
|
|
||||||
|
if (options.updateBaseline) {
|
||||||
|
const baseline = buildBaseline(scan.files)
|
||||||
|
writeFileSync(BASELINE_PATH, `${JSON.stringify(baseline, null, 2)}\n`, 'utf8')
|
||||||
|
const result = {
|
||||||
|
updated: true,
|
||||||
|
scan,
|
||||||
|
summary,
|
||||||
|
baselineFiles: scan.files.length,
|
||||||
|
nonGatedDelta: null,
|
||||||
|
violations: [],
|
||||||
|
notices: { deleted: [], newGatedFilesWithoutCounts: [], usageIncreases: [] }
|
||||||
|
}
|
||||||
|
if (options.json) console.log(JSON.stringify(buildJsonResult(result)))
|
||||||
|
else printHuman(result)
|
||||||
|
process.exitCode = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let baseline
|
||||||
|
try {
|
||||||
|
baseline = loadBaseline()
|
||||||
|
} catch (error) {
|
||||||
|
if (options.json) {
|
||||||
|
console.log(JSON.stringify({ ok: false, baselineFound: false, baseline: { path: BASELINE_LABEL }, error: error.message }))
|
||||||
|
} else {
|
||||||
|
console.error(`[check:tokens] ${error.message}`)
|
||||||
|
console.error('[check:tokens] 请运行:node scripts/check-design-tokens.mjs --update-baseline')
|
||||||
|
}
|
||||||
|
process.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!baseline) {
|
||||||
|
if (options.json) {
|
||||||
|
console.log(
|
||||||
|
JSON.stringify({
|
||||||
|
ok: false,
|
||||||
|
baselineFound: false,
|
||||||
|
baseline: { path: BASELINE_LABEL },
|
||||||
|
hint: 'node scripts/check-design-tokens.mjs --update-baseline'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
console.error(`[check:tokens] 未找到 baseline:${BASELINE_LABEL}`)
|
||||||
|
console.error('[check:tokens] 请先运行:node scripts/check-design-tokens.mjs --update-baseline')
|
||||||
|
}
|
||||||
|
process.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const baselineFiles = baseline.files
|
||||||
|
const { violations, notices } = compareWithBaseline(scan, baselineFiles)
|
||||||
|
const previousNonGated = baselineNonGatedTotals(baselineFiles)
|
||||||
|
const nonGatedDelta = { files: summary.nonGated.files - previousNonGated.files, counts: zeroCounts() }
|
||||||
|
for (const metric of METRICS) {
|
||||||
|
nonGatedDelta.counts[metric] = summary.nonGated.counts[metric] - previousNonGated.counts[metric]
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
updated: false,
|
||||||
|
scan,
|
||||||
|
summary,
|
||||||
|
baselineFiles: Object.keys(baselineFiles).length,
|
||||||
|
nonGatedDelta,
|
||||||
|
violations,
|
||||||
|
notices
|
||||||
|
}
|
||||||
|
if (options.json) console.log(JSON.stringify(buildJsonResult(result)))
|
||||||
|
else printHuman(result)
|
||||||
|
process.exitCode = violations.length > 0 ? 1 : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user