Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 24 additions & 18 deletions docs/architecture/theme-token-optimization.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,12 @@ registry。

| 指标 | 当前基线 |
| --- | ---: |
| 扫描的生产前端文件数 | 1535 |
| 忽略的测试文件数 | 221 |
| 扫描的生产前端文件数 | 1536 |
| 忽略的测试文件数 | 222 |
| 忽略的构建生成文件数 | 0 |
| 包含颜色字面量的文件数 | 25 |
| 颜色字面量出现次数 | 780 |
| 唯一颜色字面量数量 | 560 |
| 颜色字面量出现次数 | 778 |
| 唯一颜色字面量数量 | 557 |
| 组件或非 token 文件中的颜色出现次数 | 0 |
| 组件或非 token 唯一颜色数量 | 0 |
| App UI 颜色出现次数 | 0 |
Expand All @@ -124,8 +124,8 @@ registry。
| token-equivalent app literal 唯一颜色数量 | 0 |
| 普通组件肉眼不可区分 near color pair | 0 |
| 普通组件需证据复核的 near color pair | 0 |
| 专用域肉眼不可区分 near color pair | 6 |
| 专用域需证据复核的 near color pair | 377 |
| 专用域肉眼不可区分 near color pair | 3 |
| 专用域需证据复核的 near color pair | 375 |

当前审计未发现 CSS 变量契约层面的硬错误:

Expand Down Expand Up @@ -157,10 +157,10 @@ registry。
| active surface token rename key | 0 | 防止 `--primary-color`、`--operation-color`、`--delay`、`--um-*` 等旧局部 key 回流 |
| active surface token rename occurrences | 0 | 防止旧 key 在 SCSS、CSS 或 TSX inline style 中被重新定义或读取 |
| surface token rename missing canonical | 0 | 防止 rename registry 指向不存在的 canonical key |
| generated widget payload key | 326 | widget iframe 对外主题变量 allowlist,作为外部边界单独预算,不计入内部 alias 读取 |
| generated widget payload compatibility alias | 64 | payload 仍暴露已登记 legacy alias,防止已生成或第三方内容读取失败 |
| generated widget payload key | 307 | widget iframe 对外主题变量 allowlist,作为外部边界单独预算,不计入内部 alias 读取 |
| generated widget payload compatibility alias | 45 | payload 仍暴露已登记 legacy alias,防止已生成或第三方内容读取失败 |
| generated widget payload compatibility family key | 17 | payload 仍暴露 `--radius-*`、`--spacing-*` 具体 key,同时暴露 canonical family |
| generated widget payload external-only compatibility key | 81 | payload 中仍保留且内部产品代码不再读取的 legacy key,是后续外部兼容评估和收缩队列 |
| generated widget payload external-only compatibility key | 62 | payload 中仍保留且内部产品代码不再读取的 legacy key,是后续外部兼容评估和收缩队列 |
| generated widget payload undefined key | 0 | 防止 payload 引入没有静态、运行时或动态 family 定义的游离 key |
| generated widget payload missing compatibility canonical | 0 | 防止 payload 暴露 legacy key 但 canonical 目标不存在 |
| generated widget payload unexported compatibility canonical | 0 | 防止 payload 暴露 legacy key 但遗漏对应 canonical key |
Expand All @@ -177,7 +177,7 @@ registry。
| Theme presets | 231 | 205 | 主题个性与 palette 映射,不作为普通 app literal 直接合并;git staged 仅在与 added 视觉语义相同时由 preset helper 复用 |
| Token contracts | 176 | 157 | `tokens.scss` 等静态契约根 |
| Editor | 56 | 53 | Monaco/editor 专用域,不能直接泛化到 app token;组件装饰色已迁出 raw literal |
| Mermaid | 105 | 95 | Mermaid 专用渲染域 |
| Mermaid | 103 | 92 | Mermaid 专用渲染域 |
| Theme runtime | 45 | 45 | `ThemeService.ts` 运行时注入 |
| Language identity | 52 | 50 | 语言身份色,已集中到 identity registry |
| Terminal | 37 | 29 | terminal/ANSI 专用域;工具命令空状态已复用 `--tool-command-empty-rgb`,不再保留独立 raw 色 |
Expand All @@ -198,7 +198,7 @@ theme preset 或 boundary fallback 误算成普通 app UI 债务。当前队列
| Theme runtime | 0 | 50 | 先与静态 token 和 payload contract 对齐,避免 early render 或 system theme 回退 |
| Token contracts | 0 | 90 | 优先 alias 精确等价值;状态、层级和 alpha ramp 不按数值强合并 |
| Boundary fallback | 0 | 6 | 只在 iframe/截图 first paint 验证后收缩 |
| Mermaid | 3 | 32 | 需检查节点、边、文本、错误态和 light/dark Mermaid 输出 |
| Mermaid | 0 | 30 | 需检查节点、边、文本、错误态和 light/dark Mermaid 输出 |
| Editor | 0 | 7 | 需检查 Monaco selection、diff、inline highlight 和 light/dark editor 可读性 |
| Syntax / Terminal / Generated widget / Debug overlay / UI exception / Language identity / Visual effects | 0 | 0 | 当前无 near 队列;新增会被单域 baseline 拦截 |

Expand All @@ -207,7 +207,7 @@ theme preset 或 boundary fallback 误算成普通 app UI 债务。当前队列
| 文件 | 颜色出现次数 | 后续处理策略 |
| --- | ---: | --- |
| `src/web-ui/src/component-library/styles/tokens.scss` | 151 | 根 token 契约;优先处理同语义 alias,避免把状态/层级 ramp 按数值强合并 |
| `src/web-ui/src/tools/mermaid-editor/theme/mermaidThemeFallbacks.ts` | 74 | Mermaid 专用渲染兜底;需以节点、边、文本、错误态截图为依据 |
| `src/web-ui/src/tools/mermaid-editor/theme/mermaidThemeFallbacks.ts` | 72 | Mermaid 专用渲染兜底;需以节点、边、文本、错误态截图为依据 |
| `src/web-ui/src/shared/theme/languageIdentityAccents.ts` | 52 | 内置 language/file identity registry;调用方复用常量 |
| `src/web-ui/src/tools/editor/themes/bitfun-dark.theme.ts` | 47 | Monaco theme palette;不拆散到普通 app token |
| `src/web-ui/src/infrastructure/theme/core/ThemeService.ts` | 45 | 运行时注入;需保持 early render、system theme 和 payload 导出兼容 |
Expand Down Expand Up @@ -253,6 +253,9 @@ Phase 5 决策记录:
| `#ffdd00` -> `#ffd700` | merge | `StreamText.scss` fire yellow;editor/reference yellow | RGB distance = 6,均为亮黄强调色,调用点不相邻,不承担不同业务状态 |
| `#7dd3fc` -> `#7DCFFF` | merge | `GenerativeWidgetToolCard.scss`;`bitfun-dark.theme.ts` editor link | RGB distance = 5,均为非状态 sky/cyan 强调,调用点跨 surface 且不相邻 |
| `#00b4d8` -> `#00add8` | merge | `StreamText.scss` ocean mid;Go language identity | RGB distance = 7,同为 cyan/blue identity/visual-effect 色,非错误/警告/状态强度 |
| Mermaid `#dfe2e8` -> `#e0e2e8` | merge | light `nodeFillHover`;dark `nodeText` | RGB distance = 1,跨 light/dark fallback 角色,不在同一主题视口中承担相邻状态区分;合并后保留 `nodeFillHover` 和 `nodeText` 语义 key |
| Mermaid `#5a5e68` -> `#5a5e6a` | merge | dark `edgeLabelBorderHover`;dark `nodeStroke`/`edgeStroke` | RGB distance = 2,均为深色 Mermaid neutral stroke/border;edge label hover border 不表达独立状态严重程度,合并后仍通过 key 区分角色 |
| Mermaid `#6a6e78` -> `#6a6e7a` | merge | dark `textMuted`;dark `nodeStrokeHover` | RGB distance = 2,均为深色 Mermaid subdued neutral;不是 success/warning/error 或数据类别色,合并后保留 muted text 与 hover stroke 语义 |
| `#141414` vs `#121214` | preserve | `LanguageRegistry.ts` reStructuredText identity;Flow Chat capture/editor fallback | RGB distance = 2.83,但 `#141414` 是已存在的 language identity,迁移到 registry 时保持原值;`#121214` 仅作为截图/边界兜底 |
| `#b8c6ff` vs `#b8c4ff` | preserve | Slate theme purple alpha ramp;Slate purple solid stop | RGB distance = 2,但前者是原 alpha ramp 的 RGB channel,后者是 solid 500 stop;合并会改变半透明层级的实际输出 |
| remaining near pairs | none in ordinary components | 无 | 审计口径下普通组件 near pair 已清零;后续只在专用 palette 自身重设计时处理 Monaco/terminal/Mermaid/syntax 内部近似色 |
Expand All @@ -265,8 +268,8 @@ Phase 6 防回退约束:
| --- | ---: | ---: | --- |
| `nearPairs.indistinguishableTotal` | 0 | 0 | 阻止新增普通组件肉眼不可区分 pair 未被合并或记录 |
| `nearPairs.nearTotal` | 0 | 0 | 阻止新增普通组件 near color 债务;新增必须合并、归类或记录理由 |
| `colorDomainNearPairs.indistinguishableTotal` | 6 | 6 | 控制专用域肉眼不可区分 pair 不继续增长,后续只能逐步降低或补充证据 |
| `colorDomainNearPairs.nearTotal` | 377 | 377 | 控制 theme preset/runtime/token/editor/Mermaid 等专用域 near 队列规模 |
| `colorDomainNearPairs.indistinguishableTotal` | 3 | 3 | 控制专用域肉眼不可区分 pair 不继续增长,后续只能逐步降低或补充证据 |
| `colorDomainNearPairs.nearTotal` | 375 | 375 | 控制 theme preset/runtime/token/editor/Mermaid 等专用域 near 队列规模 |
| `colorScopes.appUi.uniqueColors` | 0 | 0 | 阻止普通组件 raw color 唯一色回涨 |
| `colorScopes.appUi.occurrences` | 0 | 0 | 阻止普通组件 raw color 出现次数回涨 |
| `tokenAliasLiterals.occurrences` | 0 | 0 | 阻止重新出现可映射到 token 的 app literal |
Expand All @@ -282,10 +285,10 @@ Phase 6 防回退约束:
| `surfaceTokenRenames.activeUnique` | 0 | 0 | 防止已迁移的 surface-local 旧 key 重新出现 |
| `surfaceTokenRenames.activeOccurrences` | 0 | 0 | 防止旧 key 在定义和读取两侧回流 |
| `surfaceTokenRenames.missingCanonicalUnique` | 0 | 0 | 防止 surface rename contract 指向不存在的 canonical key |
| `generatedWidgetPayload.varUnique` | 326 | 326 | 控制 widget 对外主题 payload allowlist 不继续膨胀 |
| `generatedWidgetPayload.compatibilityAliasUnique` | 64 | 64 | 控制 payload 中显式 legacy alias 数量,后续只能降低或经复审调整 |
| `generatedWidgetPayload.varUnique` | 307 | 307 | 控制 widget 对外主题 payload allowlist 不继续膨胀 |
| `generatedWidgetPayload.compatibilityAliasUnique` | 45 | 45 | 控制 payload 中显式 legacy alias 数量,后续只能降低或经复审调整 |
| `generatedWidgetPayload.compatibilityAliasFamilyUnique` | 17 | 17 | 控制 payload 中 legacy size family 具体 key 数量 |
| `generatedWidgetPayload.externalOnlyCompatibilityUnique` | 81 | 81 | 标记 payload 中内部代码已不读取、仅因外部兼容保留的收缩候选 |
| `generatedWidgetPayload.externalOnlyCompatibilityUnique` | 62 | 62 | 标记 payload 中内部代码已不读取、仅因外部兼容保留的收缩候选 |
| `generatedWidgetPayload.undefinedUnique` | 0 | 0 | 防止 payload 导出未定义主题 key |
| `generatedWidgetPayload.missingCompatibilityCanonicalUnique` | 0 | 0 | 防止 payload 兼容 alias 缺失 canonical 目标 |
| `generatedWidgetPayload.unexportedCompatibilityCanonicalUnique` | 0 | 0 | 防止 payload 兼容 alias 有 canonical 定义但未导出到 iframe |
Expand Down Expand Up @@ -765,7 +768,7 @@ semantic token 描述产品级语义,应作为共享 UI 的默认使用层。
| git/diff 色被当作普通 success/error | added/deleted/changed/conflict 扫描效率下降。 | 使用专用 git/diff token,只有复核后才 alias 到 app intent。 |
| 主题个性被抹平 | 用户选择主题的价值下降。 | theme preset 保留自己的 primitive/accent 映射。 |
| fallback 先删、alias 后补 | embedded 或 early render surface 样式丢失。 | 先加 alias,再删除 fallback。 |
| 兼容 alias 读点清零时误删定义 | 旧主题、生成式 widget、host payload 或外部自定义内容读取旧 key 时样式丢失。 | 只迁移内部 `var()` 读取;`tokens.scss`、runtime 注入和 widget shell 继续暴露 legacy key。 |
| 兼容 alias 读点清零时误删定义 | 旧主题、生成式 widget、host payload 或外部自定义内容读取旧 key 时样式丢失。 | 只迁移内部 `var()` 读取;`tokens.scss` 和 runtime 注入继续保留兼容定义,widget payload 只暴露仍有消费风险的 legacy key。 |
| 静态 token 与运行时 token 不一致 | widget、SCSS、runtime theme 注入结果不一致。 | `tokens.scss`、`ThemeService.ts`、`themePayload.ts` 同阶段对齐。 |
| 动态 CSS 变量 key 被误判为未定义 | inline style 或数据驱动变量失去兜底,导致特定卡片、标签或分组颜色缺失。 | 对动态 key 建立运行时设置清单;删除 fallback 前补组件根默认值或保留边界 fallback。 |
| contrast 验证不可信 | 可访问性回归可能漏掉。 | 先实现真实 contrast 检查,再声称可访问性改善。 |
Expand Down Expand Up @@ -941,6 +944,9 @@ alpha 差异经常承担 elevation 和交互状态,不应全部压成一个值
accent scale 或组件 action token,只有 primary action 与 accent 明确分化时才重新建模。
- `--color-danger` 当前映射到 `--color-error`,但保留 destructive action 语义;只有破坏性动作
明确选择 error token 或迁入专用 action token 后才删除。
- generated widget payload 已停止导出低风险的 `--color-accent*` legacy、`--color-primary*`、
`--accent-primary*` 和 `--color-danger*` 旧拼写;根 token 与 runtime 注入仍保留这些兼容定义,
避免影响 app 内部旧主题和历史 CSS。
- 尺寸 canonical family 是 `--size-radius-*` / `--size-gap-*`;`--radius-*` /
`--spacing-*` 只作为 legacy source 和 generated widget payload 兼容面保留。
- 迁移期 CI 采用严格 baseline:普通 app raw color、内部 compatibility alias 读取、fallback、
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"copy-icons": "copyfiles -f \"src/apps/desktop/icons/Logo-ICON.png\" \"src/web-ui/public/\"",
"copy-assets": "pnpm run copy-monaco && pnpm run copy-icons",
"generate-version": "node scripts/generate-version.cjs",
"generate-all": "pnpm run generate-version",
"generate-startup-theme-bootstrap": "node scripts/generate-startup-theme-bootstrap.mjs",
"generate-all": "pnpm run generate-version && pnpm run generate-startup-theme-bootstrap",
"postinstall": "pnpm run copy-assets",
"dev": "node scripts/dev.cjs web",
"dev:web": "pnpm --dir src/web-ui dev",
Expand Down
54 changes: 54 additions & 0 deletions scripts/generate-startup-theme-bootstrap.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

import { createServer } from 'vite';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const repoRoot = path.resolve(__dirname, '..');
const webUiRoot = path.join(repoRoot, 'src/web-ui');
const outputPath = path.join(
repoRoot,
'src/apps/desktop/src/generated/startup_theme_bootstrap.json',
);
const checkOnly = process.argv.includes('--check');

const server = await createServer({
root: webUiRoot,
logLevel: 'error',
appType: 'custom',
server: { middlewareMode: true },
optimizeDeps: {
entries: [],
noDiscovery: true,
},
});

try {
const [{ builtinThemes }, { createStartupThemeBootstrapManifest }] = await Promise.all([
server.ssrLoadModule('/src/infrastructure/theme/presets/index.ts'),
server.ssrLoadModule('/src/infrastructure/theme/presets/startupThemeBootstrap.ts'),
]);

const manifest = createStartupThemeBootstrapManifest(builtinThemes);
const nextContent = `${JSON.stringify(manifest, null, 2)}\n`;
const currentContent = fs.existsSync(outputPath)
? fs.readFileSync(outputPath, 'utf8')
: null;

if (checkOnly) {
if (currentContent !== nextContent) {
console.error(
'Startup theme bootstrap manifest is stale. Run `pnpm run generate-startup-theme-bootstrap`.',
);
process.exitCode = 1;
}
} else {
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, nextContent, 'utf8');
console.log(`Generated ${path.relative(repoRoot, outputPath).replace(/\\/g, '/')}`);
}
} finally {
await server.close();
}
26 changes: 13 additions & 13 deletions scripts/theme-color-governance-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,19 +54,19 @@
"max": 0
},
"generatedWidgetPayload.varUnique": {
"max": 326
"max": 307
},
"generatedWidgetPayload.occurrences": {
"max": 326
"max": 307
},
"generatedWidgetPayload.undefinedUnique": {
"max": 0
},
"generatedWidgetPayload.compatibilityAliasUnique": {
"max": 64
"max": 45
},
"generatedWidgetPayload.compatibilityAliasOccurrences": {
"max": 64
"max": 45
},
"generatedWidgetPayload.compatibilityAliasFamilyUnique": {
"max": 17
Expand All @@ -75,10 +75,10 @@
"max": 17
},
"generatedWidgetPayload.externalOnlyCompatibilityUnique": {
"max": 81
"max": 62
},
"generatedWidgetPayload.externalOnlyCompatibilityOccurrences": {
"max": 81
"max": 62
},
"generatedWidgetPayload.missingCompatibilityCanonicalUnique": {
"max": 0
Expand Down Expand Up @@ -108,7 +108,7 @@
"max": 340
},
"colorScopes.exception.uniqueColors": {
"max": 273
"max": 270
},
"cssVarDefinitions.unresolvedRequiredUnique": {
"max": 0
Expand Down Expand Up @@ -147,10 +147,10 @@
"max": 0
},
"colorDomainNearPairs.indistinguishableTotal": {
"max": 6
"max": 3
},
"colorDomainNearPairs.nearTotal": {
"max": 377
"max": 375
},
"colorDomainNearPairs.themePreset.indistinguishableTotal": {
"max": 3
Expand Down Expand Up @@ -183,10 +183,10 @@
"max": 6
},
"colorDomainNearPairs.mermaid.indistinguishableTotal": {
"max": 3
"max": 0
},
"colorDomainNearPairs.mermaid.nearTotal": {
"max": 32
"max": 30
},
"colorDomainNearPairs.editor.indistinguishableTotal": {
"max": 0
Expand Down Expand Up @@ -294,10 +294,10 @@
"max": 0
},
"colorDomainScopes.mermaid.occurrences": {
"max": 105
"max": 103
},
"colorDomainScopes.mermaid.uniqueColors": {
"max": 95
"max": 92
}
}
}
87 changes: 87 additions & 0 deletions src/apps/desktop/src/generated/startup_theme_bootstrap.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{
"version": 1,
"defaultLightThemeId": "bitfun-light",
"defaultDarkThemeId": "bitfun-dark",
"themes": [
{
"id": "bitfun-light",
"bgPrimary": "#f3f3f5",
"bgSecondary": "#ffffff",
"bgScene": "#ffffff",
"isLight": true,
"textPrimary": "#1e293b",
"textMuted": "#64748b",
"accentColor": "#64748b"
},
{
"id": "bitfun-slate",
"bgPrimary": "#14161a",
"bgSecondary": "#22262c",
"bgScene": "#22262c",
"isLight": false,
"textPrimary": "#eef0f3",
"textMuted": "#9ea4ab",
"accentColor": "#94a3b8"
},
{
"id": "bitfun-dark",
"bgPrimary": "#0e0e10",
"bgSecondary": "#1c1c1f",
"bgScene": "#1c1c1f",
"isLight": false,
"textPrimary": "#e8e8e8",
"textMuted": "#858585",
"accentColor": "#60a5fa"
},
{
"id": "bitfun-midnight",
"bgPrimary": "#2b2d30",
"bgSecondary": "#1e1f22",
"bgScene": "#27292c",
"isLight": false,
"textPrimary": "#bcbec4",
"textMuted": "#6f737a",
"accentColor": "#58a6ff"
},
{
"id": "bitfun-china-style",
"bgPrimary": "#faf8f0",
"bgSecondary": "#f5f3e8",
"bgScene": "#fdfcf6",
"isLight": true,
"textPrimary": "#1a1a1a",
"textMuted": "#6a6a6a",
"accentColor": "#2e5e8a"
},
{
"id": "bitfun-china-night",
"bgPrimary": "#1a1814",
"bgSecondary": "#212019",
"bgScene": "#1e1c17",
"isLight": false,
"textPrimary": "#e8e6e1",
"textMuted": "#928f89",
"accentColor": "#73a5cc"
},
{
"id": "bitfun-cyber",
"bgPrimary": "#101010",
"bgSecondary": "#151515",
"bgScene": "#141414",
"isLight": false,
"textPrimary": "#e0f2ff",
"textMuted": "#7fadcc",
"accentColor": "#00e6ff"
},
{
"id": "bitfun-tokyo-night",
"bgPrimary": "#1a1b26",
"bgSecondary": "#16161e",
"bgScene": "#1a1b26",
"isLight": false,
"textPrimary": "#c0caf5",
"textMuted": "#787c99",
"accentColor": "#7aa2f7"
}
]
}
Loading
Loading