From cc00e4d067006fc6e332bfdc8139ce70be76b4da Mon Sep 17 00:00:00 2001 From: limityan Date: Tue, 7 Jul 2026 16:30:02 +0800 Subject: [PATCH] refactor(theme): collapse git derived color tokens --- docs/architecture/theme-token-optimization.md | 42 +++--- scripts/theme-color-governance-baseline.json | 18 +-- .../SnapshotCard/SnapshotCard.scss | 6 +- .../src/component-library/styles/tokens.scss | 17 --- .../components/InlineDiffPreview.scss | 18 +-- .../tool-cards/GetFileDiffDisplay.scss | 8 +- .../flow_chat/tool-cards/GitToolDisplay.scss | 6 +- .../theme/core/ThemeService.test.ts | 125 +++++++++++++++++- .../infrastructure/theme/core/ThemeService.ts | 56 +++++--- .../theme/presets/china-night-theme.ts | 6 - .../theme/presets/china-style-theme.ts | 6 - .../theme/presets/cyber-theme.ts | 6 - .../theme/presets/dark-theme.ts | 6 - .../theme/presets/light-theme.ts | 6 - .../theme/presets/midnight-theme.ts | 6 - .../infrastructure/theme/presets/shared.ts | 3 +- .../theme/presets/slate-theme.ts | 7 - .../theme/presets/themePresetOutput.test.ts | 25 ++-- .../theme/presets/tokyo-night-theme.ts | 8 -- .../src/infrastructure/theme/types/index.ts | 4 - .../components/GitDiffView/GitDiffView.scss | 14 +- 21 files changed, 220 insertions(+), 173 deletions(-) diff --git a/docs/architecture/theme-token-optimization.md b/docs/architecture/theme-token-optimization.md index c9b8e69982..78345bb55e 100644 --- a/docs/architecture/theme-token-optimization.md +++ b/docs/architecture/theme-token-optimization.md @@ -155,7 +155,7 @@ Mermaid、Monaco/editor、Prism syntax、terminal ANSI、language identity 和 U registry。Mobile Web 和 Installer 已纳入同一审计口径,但各自使用独立 baseline, 避免移动端或安装器的独立 token 被误算为 web-ui 游离 key。 -`306` 个 web-ui 唯一颜色是前端生产文件的全域审计数,不是普通 app UI 的色值预算。 +`302` 个 web-ui 唯一颜色是前端生产文件的全域审计数,不是普通 app UI 的色值预算。 其中包含主题 preset、token contract、Mermaid、Monaco/editor、terminal、syntax、 language identity 和 UI exception 等专用 palette。language identity 已收敛为 8 个大类身份色, 不再按每种语言或文件类型保留独立色值。真正需要继续压缩的是这些专用域 @@ -178,15 +178,15 @@ label、icon、扩展名或文件名之一,不允许构建只靠颜色区分 | 指标 | 当前基线 | | --- | ---: | | 扫描的生产前端文件数 | 1567 | -| 忽略的测试文件数 | 238 | +| 忽略的测试文件数 | 239 | | 忽略的构建生成文件数 | 1 | | 包含颜色字面量的文件数 | 24 | -| 颜色字面量出现次数 | 486 | -| 唯一颜色字面量数量 | 306 | +| 颜色字面量出现次数 | 479 | +| 唯一颜色字面量数量 | 302 | | 组件或非 token 文件中的颜色出现次数 | 0 | | 组件或非 token 唯一颜色数量 | 0 | -| token 文件颜色出现次数 | 291 | -| token 文件唯一颜色数量 | 181 | +| token 文件颜色出现次数 | 284 | +| token 文件唯一颜色数量 | 177 | | App UI 颜色出现次数 | 0 | | App UI 唯一颜色数量 | 0 | | `var(--token, fallback)` 出现次数 | 0 | @@ -202,7 +202,7 @@ label、icon、扩展名或文件名之一,不允许构建只靠颜色区分 | root | 颜色出现次数 | 唯一颜色 | App UI raw | fallback var | unresolved / non-contract key | dynamic family | 说明 | | --- | ---: | ---: | ---: | ---: | ---: | ---: | --- | -| `src/web-ui/src` | 486 | 306 | 0 | 0 | 0 | 13 | 主应用完整主题、专用 palette、widget payload 和 editor/terminal/Mermaid 域;UI exception、syntax、language identity 与 boundary fallback 已收敛到小型语义 palette,Mermaid status/pie fallback 复用既有语义/类别色,未接入的 Mermaid SCSS token 路径已退役;未读取的 git 同义 runtime/static key、未消费 legacy mixin、文件图标一扩展一色 root key、局部预览派生 key、nav 字体 root family、GitGraph/tool/search/action/inline-tag/windowControls、z-index/glass shadow/opacity utility helper、跨 surface 动画序号 root 默认等低复用 root helper 已退役。 | +| `src/web-ui/src` | 479 | 302 | 0 | 0 | 0 | 13 | 主应用完整主题、专用 palette、widget payload 和 editor/terminal/Mermaid 域;UI exception、syntax、language identity 与 boundary fallback 已收敛到小型语义 palette,Mermaid status/pie fallback 复用既有语义/类别色,未接入的 Mermaid SCSS token 路径已退役;未读取的 git 同义 runtime/static key、git/diff 派生背景与 hover key、未消费 legacy mixin、文件图标一扩展一色 root key、局部预览派生 key、nav 字体 root family、GitGraph/tool/search/action/inline-tag/windowControls、z-index/glass shadow/opacity utility helper、跨 surface 动画序号 root 默认等低复用 root helper 已退役。 | | `src/mobile-web/src` | 35 | 30 | 0 | 0 | 0 | 3 | mobile-web 图片缩略图关闭按钮已读取 `--color-static-white`,普通 app UI raw 归零。 | | `BitFun-Installer/src` | 61 | 49 | 0 | 0 | 0 | 0 | 安装器主题数据保留主题卡可见的 primary/secondary background、单一 accent、text 和状态反馈;弱强调背景由 accent 主色局部派生,runtime 只导出实际消费的基础 text/border/element/status key,不复制主应用 purple/info/tooltip 或多级 accent ramp。 | @@ -238,10 +238,10 @@ CLI/TUI 使用独立审计,不参与 CSS var root 计数: | non-contract dynamic inputs | 0 | | non-contract component-private vars | 0 | | runtime-only legacy required vars | 1 | -| static root contract key | 250 | -| static root contract external usage key | 250 | +| static root contract key | 242 | +| static root contract external usage key | 242 | | static root contract internal-only key | 0 | -| static root contract low external usage key | 51 | +| static root contract low external usage key | 44 | 审计补充了机器可校验的治理契约,用于把“可删除债务”和“必须保留的兼容/边界” 分开: @@ -286,8 +286,8 @@ PR 通过而放宽 `appUi`、fallback、unresolved、non-contract 或 dynamic fa | 区域 | 当前出现次数 | 当前唯一色数 | 说明 | | --- | ---: | ---: | --- | -| Theme presets | 168 | 115 | 主题个性与 palette 映射;跨主题深色 neutral、弱文本、非状态浅色背景和同概念 success 色已收敛;相邻 surface、主题识别主背景、状态色和 editor lineHighlight 继续保留 | -| Token contracts | 90 | 80 | `tokens.scss` 等静态契约根;黑白 overlay alpha stop 继续保留相邻状态层级,未消费 legacy mixin、自引用别名、死 Sass helper 和低复用单 surface helper 已移除,不按数值相近强行合并 | +| Theme presets | 167 | 114 | 主题个性与 palette 映射;跨主题深色 neutral、弱文本、非状态浅色背景和同概念 success 色已收敛;相邻 surface、主题识别主背景、状态色和 editor lineHighlight 继续保留 | +| Token contracts | 84 | 76 | `tokens.scss` 等静态契约根;git/diff 派生背景和 hover 不再作为主题输入或 root/runtime contract,消费侧从 git 语义文本色局部派生;黑白 overlay alpha stop 继续保留相邻状态层级,未消费 legacy mixin、自引用别名、死 Sass helper 和低复用单 surface helper 已移除,不按数值相近强行合并 | | Editor | 52 | 48 | Monaco/editor 专用域,不能直接泛化到 app token;被动 selection/word highlight 已收敛,但 diff text/line/gutter 继续保留用户可见层级 | | Mermaid | 53 | 48 | Mermaid 专用渲染域;status fallback 复用 app semantic status 默认值,pie 5-8 复用紧凑类别色,dark info/activation 恢复 accent 类别感;节点、边、cluster、note 文本和 light 高亮仍保留相邻层级差异。未接入当前 Markdown Mermaid 渲染路径的 SCSS token 文件已删除 | | Theme runtime | 27 | 26 | `ThemeService.ts` 运行时注入;黑白 overlay alpha 与静态 token、payload shell 保持相同 stop,避免 early render 与 runtime 状态层级漂移 | @@ -324,7 +324,7 @@ root 的当前队列,不是自动合并指令, | 文件 | 颜色出现次数 | 后续处理策略 | | --- | ---: | --- | -| `src/web-ui/src/component-library/styles/tokens.scss` | 81 | 根 token 契约;优先处理同语义 alias,避免把状态/层级 ramp 按数值强合并 | +| `src/web-ui/src/component-library/styles/tokens.scss` | 71 | 根 token 契约;优先处理同语义 alias,避免把状态/层级 ramp 按数值强合并 | | `src/web-ui/src/tools/mermaid-editor/theme/mermaidThemeFallbacks.ts` | 53 | Mermaid 专用渲染兜底;需以节点、边、文本、错误态截图为依据 | | `src/web-ui/src/tools/editor/themes/bitfun-dark.theme.ts` | 46 | Monaco theme palette;不拆散到普通 app token | | `src/web-ui/src/infrastructure/theme/core/ThemeService.ts` | 27 | 运行时注入;需保持 early render、system theme 和 payload 导出兼容 | @@ -436,7 +436,7 @@ Phase 5 决策记录: | remaining near pairs | none in ordinary components | 无 | 审计口径下普通组件 near pair 已清零;后续只在专用 palette 自身重设计时处理 Monaco/terminal/Mermaid/syntax 内部近似色 | | Monaco theme palette | classify as exception | `tools/editor/themes/bitfun-dark.theme.ts` | 该文件是 Monaco theme 完整色板,不是普通 app UI;归入 editor/exception 后不再被误计为 component raw color | | Flow Chat capture fallback | boundary fallback | `ExportImageButton.tsx`、`captureElementToDownloadsPng.tsx` -> `themeBoundaryFallbacks.ts` | `#121214` 只在 root theme 变量不可用时兜底截图背景,集中 owner 后避免截图工具重复携带 raw fallback | -| git runtime alias surface | retire unused keys | `tokens.scss`、`ThemeService.ts` | `--git-color-pull*`、`--git-color-push*`、`--git-color-branch-border`、`--git-color-added-border`、`--git-color-changes-border`、`--git-color-changes-bg-hover` 和 `--git-color-deleted-border` 没有产品读取,且分别只是 branch/staged/status 派生同义 key;删除后实际渲染仍读取 canonical branch、staged、added、changes、deleted key,web-ui 定义 key 956 -> 945 | +| git runtime alias and derived background surface | retire unused and derived keys | `tokens.scss`、`ThemeService.ts`、theme presets、git/diff 消费侧样式 | `--git-color-pull*`、`--git-color-push*`、`--git-color-branch-border`、`--git-color-added-border`、`--git-color-changes-border`、`--git-color-changes-bg-hover` 和 `--git-color-deleted-border` 没有产品读取,且分别只是 branch/staged/status 派生同义 key;`changesBg`、`addedBg`、`deletedBg`、`stagedBg` 及对应 hover/border root key 不再作为主题扩展输入,diff/status 背景在消费侧从 canonical `changes`、`added`、`deleted`、`staged` 文本色用固定 alpha 派生。旧 custom theme 中的多余字段会在归一化、持久化和导出时剥离,不恢复为 public contract。static root contract 250 -> 242,runtime contract 102 -> 94,web unique colors 306 -> 302。 | | installer minimal runtime projection | retire unused installer keys | `BitFun-Installer/src/theme/installerThemesData.ts`、`installerThemeRuntime.ts`、`variables.css`、`ThemeSetup.tsx` | 安装器是简单首启/安装 surface,不消费完整 app 主题 schema;删除未读取的 background tertiary/quaternary/elevated/workbench/flowchat/tooltip、text disabled、purple family、info/highlight、border strong/prominent、element base/elevated 和额外 radius key。ThemeSetup 预览仍保留 primary/secondary background、element soft、muted text 和 accent;安装器定义 key 54 -> 30,唯一色 72 -> 66 | | web root export compression | retire derived and extension-specific root keys | `tokens.scss`、`FileExplorer.scss`、`markdown-preview.css`、`SnapshotRollbackButton.scss`、`registerDefaultTypes.ts` | 删除未提供独立主题语义的 `--color-purple-soft`、`--color-cyan-400`、`--color-error-soft`、diff fullscreen panel RGB、preview/markdown preview RGB、miniapp/app card gradient/action RGB 和一扩展一色 file explorer icon RGB。FlowChat inline tag 的保留 root helper 改为 canonical accent/status token,但当前可见 tag 主路径仍由 `config.tagColor` 驱动;miniapp/card gradient 改从 canonical accent/status token 派生;miniapp card action 背景改为组件局部 static-white overlay;preview demo 与 diff fullscreen 因内部仍使用白色 overlay 文本与边框,保留由 static black/white 和品牌 accent 派生的固定暗色 surface;markdown light code 文字读取当前主题 text token。文件树图标按 code/markup/media/config/text 类别复用现有语义 token,文件名、扩展名和图标继续承担主识别。static root contract 566 -> 508,external usage key 515 -> 493,internal-only key 51 -> 15,tokenContract 颜色出现次数 108 -> 102,token 唯一色 197 -> 192。剩余 15 个 internal-only key 主要是 nav font dynamic family、blur/glass/motion 静态族,不在本轮硬删;generated widget payload 中公开给 iframe 的 key 已按外部消费计入审计。 | | internal-only static root cleanup | derive locally or runtime-only | `tokens.scss`、`app/styles/nav-panel-font-scope.scss`、`FontPreferenceService.ts`、`ThemeService.ts`、`theme-css-var-contract.mjs` | 删除剩余 internal-only static root key:`--nav-font-size-*` 不再作为公共 root family,由 app nav 私有 scope 从 `--flowchat-font-size-*` 通过 CSS `calc()`/`max()` 派生完整 `xxs` 到 `4xl` 字体阶梯,保持“比 FlowChat 小一个 baseline step”的体验并减少运行时注入;root `--font-size-4xl` 不再作为静态 contract 暴露,`--flowchat-font-size-4xl` 继续镜像 runtime `--font-size-4xl`;`--glass-base`、`--blur-subtle`、`--blur-base`、`--motion-slow` 不再作为 static root helper,相关 public key 直接写入 SCSS fallback 或由 runtime owner 同步注入,运行时主题仍继续注入 `--blur-*`、`--motion-*`、`--font-size-*` 等真正可扩展 family。static root contract 508 -> 493,internal-only key 15 -> 0,dynamic family 14 -> 13;没有新增 unresolved、fallback-only、non-contract 或 dynamic-family export 错误。 | @@ -460,18 +460,18 @@ Phase 6 防回退约束: | `colorDomainNearPairs.nearTotal` | 9 | 9 | 控制 theme preset/runtime/token/editor/Mermaid 等专用域 near 队列规模 | | `colorScopes.appUi.uniqueColors` | 0 | 0 | 阻止普通组件 raw color 唯一色回涨 | | `colorScopes.appUi.occurrences` | 0 | 0 | 阻止普通组件 raw color 出现次数回涨 | -| `colorScopes.token.occurrences` | 291 | 291 | 阻止 token 层重新写回已归并的派生色、扩展名色或 preview RGB 字面量 | -| `colorScopes.token.uniqueColors` | 181 | 181 | 控制 root/token 层唯一色数量,后续只允许在债务减少时下调 | +| `colorScopes.token.occurrences` | 284 | 284 | 阻止 token 层重新写回已归并的派生色、扩展名色或 preview RGB 字面量 | +| `colorScopes.token.uniqueColors` | 177 | 177 | 控制 root/token 层唯一色数量,后续只允许在债务减少时下调 | | `colorScopes.exception.uniqueColors` | 162 | 162 | 控制专用域/例外域总体规模;UI exception、syntax 和 language identity 已收敛,Mermaid status/pie fallback 已压缩且未接入 SCSS token 路径已退役,editor/terminal 仍按各自 owner 单独治理 | -| `cssVarDefinitions.staticContractDefinedUnique` | 250 | 250 | 控制静态 root contract key 总量,避免新增主题时需要维护不可扩展的大型 CSS var 表 | -| `cssVarDefinitions.staticContractExternalUsageUnique` | 250 | 250 | 跟踪真正被 root 外消费的 static contract key,防止删除 key 后遗漏调用点;generated widget payload 暴露给 iframe 的 key 也按外部消费计数 | +| `cssVarDefinitions.staticContractDefinedUnique` | 242 | 242 | 控制静态 root contract key 总量,避免新增主题时需要维护不可扩展的大型 CSS var 表 | +| `cssVarDefinitions.staticContractExternalUsageUnique` | 242 | 242 | 跟踪真正被 root 外消费的 static contract key,防止删除 key 后遗漏调用点;generated widget payload 暴露给 iframe 的 key 也按外部消费计数 | | `cssVarDefinitions.staticContractInternalOnlyUnique` | 0 | 0 | 暴露仅定义或内部派生的 root key;新增项必须删除、局部派生或证明是外部消费 contract | -| `cssVarDefinitions.staticContractLowExternalUsageUnique` | 51 | 51 | 暴露低外部消费 key,作为后续继续压缩 root contract 的候选队列 | +| `cssVarDefinitions.staticContractLowExternalUsageUnique` | 44 | 44 | 暴露低外部消费 key,作为后续继续压缩 root contract 的候选队列 | | `cssVarDefinitions.runtimeOnlyRequiredContractUnique` | 1 | 1 | 仅允许 `--window-control-close-hover-color` 作为 deprecated window close hover 覆盖,且由 baseline allowlist 锁定名称;默认路径不读取该 var,旧 custom theme 通过 `components.windowControls.close.hoverColor` 和 runtime attribute 激活 | | `colorDomainScopes.syntax.occurrences` | 16 | 16 | 阻止 Prism syntax palette 回到一 token class 一色的不可扩展模式,同时保留相邻 token 可读性边界 | | `colorDomainScopes.languageIdentity.uniqueColors` | 8 | 8 | 阻止 language/file identity 回到一语言一色或一扩展一色的不可扩展模式 | -| `colorDomainScopes.tokenContract.occurrences` | 90 | 90 | 控制 token contract 域 raw color 出现次数,防止 root 派生色回流 | -| `colorDomainScopes.tokenContract.uniqueColors` | 80 | 80 | 控制 token contract 域唯一色,确保新主题扩展不依赖额外静态色表 | +| `colorDomainScopes.tokenContract.occurrences` | 84 | 84 | 控制 token contract 域 raw color 出现次数,防止 root 派生色回流 | +| `colorDomainScopes.tokenContract.uniqueColors` | 76 | 76 | 控制 token contract 域唯一色,确保新主题扩展不依赖额外静态色表 | | `colorDomainScopes.boundaryFallback.occurrences` | 18 | 18 | 防止 iframe/mini app/截图兜底色重新散写;导出 key 可保留语义,实际字面值必须回到 boundary fallback palette | | `colorDomainScopes.mermaid.occurrences` | 53 | 53 | 控制 Mermaid 专用域 raw fallback 规模;status/pie 已压缩,未接入 SCSS token 路径已退役,节点/边/cluster/note 文本等相邻层级不能无证据合并 | | `colorDomainScopes.mermaid.uniqueColors` | 48 | 48 | 控制 Mermaid 专用域唯一色数量;新增类别色或状态色必须先复用现有 compact fallback,确有相邻可读性需求才新增 | diff --git a/scripts/theme-color-governance-baseline.json b/scripts/theme-color-governance-baseline.json index 1992a9ced7..cbd6f03499 100644 --- a/scripts/theme-color-governance-baseline.json +++ b/scripts/theme-color-governance-baseline.json @@ -105,10 +105,10 @@ "max": 0 }, "colorScopes.token.occurrences": { - "max": 291 + "max": 284 }, "colorScopes.token.uniqueColors": { - "max": 181 + "max": 177 }, "colorScopes.exception.uniqueColors": { "max": 162 @@ -144,16 +144,16 @@ "max": 0 }, "cssVarDefinitions.staticContractDefinedUnique": { - "max": 250 + "max": 242 }, "cssVarDefinitions.staticContractExternalUsageUnique": { - "max": 250 + "max": 242 }, "cssVarDefinitions.staticContractInternalOnlyUnique": { "max": 0 }, "cssVarDefinitions.staticContractLowExternalUsageUnique": { - "max": 51 + "max": 44 }, "tokenAliasLiterals.occurrences": { "max": 0 @@ -264,19 +264,19 @@ "max": 0 }, "colorDomainScopes.themePreset.occurrences": { - "max": 168 + "max": 167 }, "colorDomainScopes.themePreset.uniqueColors": { - "max": 115 + "max": 114 }, "colorDomainScopes.themeRuntime.occurrences": { "max": 27 }, "colorDomainScopes.tokenContract.occurrences": { - "max": 90 + "max": 84 }, "colorDomainScopes.tokenContract.uniqueColors": { - "max": 80 + "max": 76 }, "colorDomainScopes.generatedWidget.occurrences": { "max": 0 diff --git a/src/web-ui/src/component-library/components/FlowChatCards/SnapshotCard/SnapshotCard.scss b/src/web-ui/src/component-library/components/FlowChatCards/SnapshotCard/SnapshotCard.scss index 473b4d059b..407bcb0b52 100644 --- a/src/web-ui/src/component-library/components/FlowChatCards/SnapshotCard/SnapshotCard.scss +++ b/src/web-ui/src/component-library/components/FlowChatCards/SnapshotCard/SnapshotCard.scss @@ -201,10 +201,10 @@ .snapshot-card__action-btn--accept { color: var(--color-success); - border: 1px solid var(--git-color-staged-border); + border: 1px solid color-mix(in srgb, var(--git-color-staged) 30%, transparent); &:hover:not(:disabled) { - background: var(--git-color-staged-bg-hover); + background: color-mix(in srgb, var(--git-color-staged) 15%, transparent); border-color: color-mix(in srgb, var(--git-color-staged) 50%, transparent); } } @@ -214,7 +214,7 @@ border: 1px solid var(--color-error-border); &:hover:not(:disabled) { - background: var(--git-color-deleted-bg-hover); + background: color-mix(in srgb, var(--git-color-deleted) 15%, transparent); border-color: color-mix(in srgb, var(--git-color-deleted) 50%, transparent); } } diff --git a/src/web-ui/src/component-library/styles/tokens.scss b/src/web-ui/src/component-library/styles/tokens.scss index c9aa2b5b40..dc5025af39 100644 --- a/src/web-ui/src/component-library/styles/tokens.scss +++ b/src/web-ui/src/component-library/styles/tokens.scss @@ -117,21 +117,12 @@ $git-color-branch-bg: $overlay-white-08; $git-color-branch-bg-hover: $overlay-white-12; $git-color-changes: $color-warning; -$git-color-changes-bg: rgba(245, 158, 11, 0.1); -$git-color-changes-bg-hover: rgba(245, 158, 11, 0.15); $git-color-staged: rgb(34, 197, 94); -$git-color-staged-bg: rgba(34, 197, 94, 0.1); -$git-color-staged-bg-hover: rgba(34, 197, 94, 0.15); -$git-color-staged-border: rgba(34, 197, 94, 0.3); $git-color-added: $git-color-staged; -$git-color-added-bg: $git-color-staged-bg; -$git-color-added-bg-hover: $git-color-staged-bg-hover; $git-color-deleted: $color-error; -$git-color-deleted-bg: $color-error-bg; -$git-color-deleted-bg-hover: rgba(239, 68, 68, 0.15); // ==================== Border system ==================== $border-subtle: $overlay-white-12; @@ -428,17 +419,9 @@ $badge-info-text: $color-info; --git-color-branch-bg: #{$git-color-branch-bg}; --git-color-branch-bg-hover: #{$git-color-branch-bg-hover}; --git-color-changes: #{$git-color-changes}; - --git-color-changes-bg: #{$git-color-changes-bg}; --git-color-staged: #{$git-color-staged}; - --git-color-staged-bg: #{$git-color-staged-bg}; - --git-color-staged-bg-hover: #{$git-color-staged-bg-hover}; - --git-color-staged-border: #{$git-color-staged-border}; --git-color-added: #{$git-color-added}; - --git-color-added-bg: #{$git-color-added-bg}; - --git-color-added-bg-hover: #{$git-color-added-bg-hover}; --git-color-deleted: #{$git-color-deleted}; - --git-color-deleted-bg: #{$git-color-deleted-bg}; - --git-color-deleted-bg-hover: #{$git-color-deleted-bg-hover}; --border-subtle: #{$border-subtle}; --border-base: #{$border-base}; diff --git a/src/web-ui/src/flow_chat/components/InlineDiffPreview.scss b/src/web-ui/src/flow_chat/components/InlineDiffPreview.scss index 2dbef73ac3..d5c500f269 100644 --- a/src/web-ui/src/flow_chat/components/InlineDiffPreview.scss +++ b/src/web-ui/src/flow_chat/components/InlineDiffPreview.scss @@ -81,23 +81,23 @@ } &--added { - background: var(--git-color-staged-bg); + background: color-mix(in srgb, var(--git-color-added) 10%, transparent); .diff-line__gutter { - background: var(--git-color-staged-bg-hover); + background: color-mix(in srgb, var(--git-color-added) 15%, transparent); } .diff-line__prefix { - color: var(--git-color-staged); + color: var(--git-color-added); font-weight: 500; } } &--removed { - background: var(--git-color-deleted-bg); + background: color-mix(in srgb, var(--git-color-deleted) 10%, transparent); .diff-line__gutter { - background: var(--git-color-deleted-bg-hover); + background: color-mix(in srgb, var(--git-color-deleted) 15%, transparent); } .diff-line__prefix { @@ -225,19 +225,19 @@ .light .inline-diff-preview { .diff-line { &--added { - background: var(--git-color-staged-bg); + background: color-mix(in srgb, var(--git-color-added) 10%, transparent); .diff-line__gutter { - background: color-mix(in srgb, var(--git-color-staged) 18%, transparent); + background: color-mix(in srgb, var(--git-color-added) 18%, transparent); } .diff-line__prefix { - color: var(--git-color-staged); + color: var(--git-color-added); } } &--removed { - background: var(--git-color-deleted-bg); + background: color-mix(in srgb, var(--git-color-deleted) 10%, transparent); .diff-line__gutter { background: color-mix(in srgb, var(--git-color-deleted) 18%, transparent); diff --git a/src/web-ui/src/flow_chat/tool-cards/GetFileDiffDisplay.scss b/src/web-ui/src/flow_chat/tool-cards/GetFileDiffDisplay.scss index ae03cc6056..0289257504 100644 --- a/src/web-ui/src/flow_chat/tool-cards/GetFileDiffDisplay.scss +++ b/src/web-ui/src/flow_chat/tool-cards/GetFileDiffDisplay.scss @@ -79,13 +79,13 @@ word-break: break-all; .diff-add { - background: var(--git-color-added-bg-hover); - color: var(--color-success); + background: color-mix(in srgb, var(--git-color-added) 15%, transparent); + color: var(--git-color-added); } .diff-del { - background: var(--git-color-deleted-bg-hover); - color: var(--color-error); + background: color-mix(in srgb, var(--git-color-deleted) 15%, transparent); + color: var(--git-color-deleted); } } } diff --git a/src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.scss b/src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.scss index 63458bfc90..b217956997 100644 --- a/src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.scss +++ b/src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.scss @@ -337,7 +337,7 @@ color: color-mix(in srgb, var(--color-success) 80%, transparent); &:hover:not(:disabled) { - background: var(--git-color-staged-bg-hover); + background: color-mix(in srgb, var(--git-color-staged) 15%, transparent); color: var(--git-color-staged); } @@ -350,8 +350,8 @@ color: color-mix(in srgb, var(--color-error) 80%, transparent); &:hover:not(:disabled) { - background: var(--git-color-deleted-bg-hover); - color: var(--color-error); + background: color-mix(in srgb, var(--git-color-deleted) 15%, transparent); + color: var(--git-color-deleted); } &:active:not(:disabled) { diff --git a/src/web-ui/src/infrastructure/theme/core/ThemeService.test.ts b/src/web-ui/src/infrastructure/theme/core/ThemeService.test.ts index d7b3ac947c..4f82b3595e 100644 --- a/src/web-ui/src/infrastructure/theme/core/ThemeService.test.ts +++ b/src/web-ui/src/infrastructure/theme/core/ThemeService.test.ts @@ -137,13 +137,15 @@ describe('ThemeService runtime theme tokens', () => { expect(rootStyle.getPropertyValue('--git-color-branch')).toBe('#a1a1aa'); expect(rootStyle.getPropertyValue('--git-color-branch-bg')).toBe('rgba(255, 255, 255, 0.06)'); expect(rootStyle.getPropertyValue('--git-color-branch-bg-hover')).toBe('rgba(255, 255, 255, 0.12)'); + expect(rootStyle.getPropertyValue('--git-color-changes')).toBe('rgb(245, 158, 11)'); expect(rootStyle.getPropertyValue('--git-color-added')).toBe('rgb(34, 197, 94)'); - expect(rootStyle.getPropertyValue('--git-color-added-bg')).toBe('rgba(34, 197, 94, 0.1)'); - expect(rootStyle.getPropertyValue('--git-color-added-bg-hover')).toBe('rgba(34, 197, 94, 0.15)'); - expect(rootStyle.getPropertyValue('--git-color-changes-bg')).toBe('rgba(245, 158, 11, 0.1)'); - expect(rootStyle.getPropertyValue('--git-color-deleted-bg-hover')).toBe('rgba(239, 68, 68, 0.15)'); - expect(rootStyle.getPropertyValue('--git-color-staged-bg-hover')).toBe('rgba(34, 197, 94, 0.15)'); - expect(rootStyle.getPropertyValue('--git-color-staged-border')).toBe('rgba(34, 197, 94, 0.3)'); + expect(rootStyle.getPropertyValue('--git-color-deleted')).toBe('rgb(239, 68, 68)'); + expect(rootStyle.getPropertyValue('--git-color-staged')).toBe('rgb(34, 197, 94)'); + expect(rootStyle.getPropertyValue('--git-color-changes-bg')).toBe(''); + expect(rootStyle.getPropertyValue('--git-color-added-bg')).toBe(''); + expect(rootStyle.getPropertyValue('--git-color-deleted-bg')).toBe(''); + expect(rootStyle.getPropertyValue('--git-color-staged-bg')).toBe(''); + expect(rootStyle.getPropertyValue('--git-color-staged-border')).toBe(''); expect(rootStyle.getPropertyValue('--git-color-pull')).toBe(''); expect(rootStyle.getPropertyValue('--git-color-push')).toBe(''); }); @@ -388,7 +390,7 @@ describe('ThemeService runtime theme tokens', () => { expect(configAPI.setConfig).not.toHaveBeenCalledWith('themes.custom', expect.anything()); }); - it('does not export non-contract dynamic keys from custom themes', () => { + it('does not inject non-contract dynamic keys from custom themes', () => { const service = new ThemeService(); const customTheme = { ...bitfunLightTheme, @@ -616,6 +618,115 @@ describe('ThemeService runtime theme tokens', () => { ).rejects.toThrow(/reserved for a built-in theme/); }); + it('strips non-contract git color keys from registered custom themes', async () => { + const nonContractGitColorKeys = [ + 'changesBg', + 'addedBg', + 'deletedBg', + 'stagedBg', + 'addedBgHover', + 'stagedBorder', + 'pull', + ] as const; + const expectNoNonContractGitColorKeys = (gitColors: ThemeConfig['colors']['git']) => { + const gitRecord = gitColors as unknown as Record; + nonContractGitColorKeys.forEach(key => { + expect(gitRecord).not.toHaveProperty(key); + }); + }; + const service = new ThemeService(); + const legacyTheme = { + ...bitfunDarkTheme, + id: 'custom-legacy-git-bg', + name: 'Legacy Git Backgrounds', + colors: { + ...bitfunDarkTheme.colors, + git: { + ...bitfunDarkTheme.colors.git, + changesBg: 'rgba(245, 158, 11, 0.1)', + addedBg: 'rgba(34, 197, 94, 0.1)', + deletedBg: 'rgba(239, 68, 68, 0.1)', + stagedBg: 'rgba(16, 185, 129, 0.1)', + addedBgHover: 'rgba(34, 197, 94, 0.2)', + stagedBorder: 'rgba(16, 185, 129, 0.4)', + pull: '#60a5fa', + }, + }, + } as unknown as ThemeConfig; + + await service.registerTheme(legacyTheme); + + const normalized = service.getTheme('custom-legacy-git-bg'); + expect(normalized).toBeDefined(); + if (!normalized) { + throw new Error('Expected custom legacy git theme to be registered'); + } + expect(normalized.colors.git.added).toBe(bitfunDarkTheme.colors.git.added); + expectNoNonContractGitColorKeys(normalized.colors.git); + + const persistedThemes = vi.mocked(configAPI.setConfig).mock.calls.find(([key]) => key === 'themes.custom')?.[1] as + | ThemeConfig[] + | undefined; + const persistedTheme = persistedThemes?.find(theme => theme.id === 'custom-legacy-git-bg'); + expect(persistedTheme).toBeDefined(); + if (!persistedTheme) { + throw new Error('Expected custom legacy git theme to be persisted'); + } + expectNoNonContractGitColorKeys(persistedTheme.colors.git); + + const exported = service.exportTheme('custom-legacy-git-bg'); + expect(exported).not.toBeNull(); + if (!exported) { + throw new Error('Expected custom legacy git theme to be exported'); + } + expectNoNonContractGitColorKeys(exported.theme.colors.git); + }); + + it('migrates persisted custom themes with non-contract git color keys on load', async () => { + const legacyTheme = { + ...bitfunDarkTheme, + id: 'custom-loaded-legacy-git', + name: 'Loaded Legacy Git', + colors: { + ...bitfunDarkTheme.colors, + git: { + ...bitfunDarkTheme.colors.git, + changesBg: 'rgba(245, 158, 11, 0.1)', + addedBgHover: 'rgba(34, 197, 94, 0.2)', + stagedBorder: 'rgba(16, 185, 129, 0.4)', + }, + }, + } as unknown as ThemeConfig; + vi.mocked(configAPI.getConfig).mockResolvedValue({ custom: [legacyTheme] }); + const service = new ThemeService(); + + await service.ensureUserThemesLoaded(); + + const normalized = service.getTheme('custom-loaded-legacy-git'); + expect(normalized).toBeDefined(); + if (!normalized) { + throw new Error('Expected legacy custom theme to load'); + } + expect(normalized.colors.git.added).toBe(bitfunDarkTheme.colors.git.added); + expect(normalized.colors.git.staged).toBe(bitfunDarkTheme.colors.git.staged); + expect(normalized.colors.git as unknown as Record).not.toHaveProperty('changesBg'); + expect(normalized.colors.git as unknown as Record).not.toHaveProperty('addedBgHover'); + expect(normalized.colors.git as unknown as Record).not.toHaveProperty('stagedBorder'); + + const migratedThemes = vi.mocked(configAPI.setConfig).mock.calls.find(([key]) => key === 'themes.custom')?.[1] as + | ThemeConfig[] + | undefined; + expect(migratedThemes).toHaveLength(1); + const migratedGitColors = migratedThemes?.[0]?.colors.git as unknown as Record | undefined; + expect(migratedGitColors).toBeDefined(); + if (!migratedGitColors) { + throw new Error('Expected migrated theme to keep git colors'); + } + expect(migratedGitColors).not.toHaveProperty('changesBg'); + expect(migratedGitColors).not.toHaveProperty('addedBgHover'); + expect(migratedGitColors).not.toHaveProperty('stagedBorder'); + }); + it('projects normalized custom themes through the compact plugin color boundary', async () => { const service = new ThemeService(); const partialCustomTheme = { diff --git a/src/web-ui/src/infrastructure/theme/core/ThemeService.ts b/src/web-ui/src/infrastructure/theme/core/ThemeService.ts index 8d7d1165e6..a0783c55a1 100644 --- a/src/web-ui/src/infrastructure/theme/core/ThemeService.ts +++ b/src/web-ui/src/infrastructure/theme/core/ThemeService.ts @@ -32,6 +32,9 @@ const FLOW_CHAT_LINK_COLORS = { }, } as const; +const GIT_COLOR_CONTRACT_KEYS = ['branch', 'branchBg', 'changes', 'added', 'deleted', 'staged'] as const; +const GIT_COLOR_CONTRACT_KEY_SET = new Set(GIT_COLOR_CONTRACT_KEYS); + const THEME_STATIC_COLORS = { white: '#ffffff', black: '#000000', @@ -107,19 +110,29 @@ function accentColorToRgbChannels(accent: string): string | null { return null; } -function colorWithAlpha(color: string, alpha: number): string { - const channels = accentColorToRgbChannels(color); - if (channels) { - return `rgba(${channels.replace(/\s+/g, ', ')}, ${alpha})`; - } - const percent = `${Math.round(alpha * 1000) / 10}%`; - return `color-mix(in srgb, ${color} ${percent}, transparent)`; -} - function cloneThemeConfig(theme: ThemeConfig): ThemeConfig { return JSON.parse(JSON.stringify(theme)) as ThemeConfig; } +function hasNonContractGitColorKeys(theme: Partial): boolean { + const gitColors = theme.colors?.git as unknown as Record | undefined; + return Boolean( + gitColors && + Object.keys(gitColors).some(key => !GIT_COLOR_CONTRACT_KEY_SET.has(key)), + ); +} + +function stripNonContractGitColorKeys(theme: ThemeConfig): ThemeConfig { + const sanitized = cloneThemeConfig(theme); + const gitColors = sanitized.colors?.git as unknown as Record | undefined; + Object.keys(gitColors ?? {}).forEach(key => { + if (!GIT_COLOR_CONTRACT_KEY_SET.has(key)) { + delete gitColors?.[key]; + } + }); + return sanitized; +} + function mergeThemeConfig(base: ThemeConfig, override: Partial): ThemeConfig { const mergeValue = (baseValue: unknown, overrideValue: unknown): unknown => { if (overrideValue === undefined || overrideValue === null) { @@ -278,11 +291,17 @@ export class ThemeService { if (Array.isArray(themes) && themes.length > 0) { let loadedCount = 0; - themes.forEach(theme => { + let migratedGitColorKeys = false; + const persistedThemes = [...themes]; + themes.forEach((theme, index) => { try { const normalizedTheme = this.normalizeCustomTheme(theme); this.themes.set(normalizedTheme.id, normalizedTheme); loadedCount += 1; + if (hasNonContractGitColorKeys(theme)) { + persistedThemes[index] = normalizedTheme; + migratedGitColorKeys = true; + } } catch (error) { log.warn('Skipped invalid user theme', { id: theme?.id, @@ -291,6 +310,13 @@ export class ThemeService { } }); log.info('Loaded user themes', { count: loadedCount, skipped: themes.length - loadedCount }); + if (migratedGitColorKeys) { + try { + await configAPI.setConfig('themes.custom', persistedThemes); + } catch (error) { + log.warn('Failed to migrate custom theme Git color keys', error); + } + } } } catch (_error) { @@ -342,7 +368,7 @@ export class ThemeService { const baseTheme = theme.type === 'light' ? builtinThemes.find(item => item.type === 'light') || builtinThemes[0] : builtinThemes.find(item => item.id === 'bitfun-dark') || builtinThemes.find(item => item.type === 'dark') || builtinThemes[0]; - const normalized = mergeThemeConfig(baseTheme, theme); + const normalized = stripNonContractGitColorKeys(mergeThemeConfig(baseTheme, theme)); const validation = this.validateTheme(normalized); if (!validation.valid) { @@ -633,17 +659,9 @@ export class ThemeService { root.style.setProperty('--git-color-branch-bg', colors.git.branchBg); root.style.setProperty('--git-color-branch-bg-hover', colors.element.medium); root.style.setProperty('--git-color-changes', colors.git.changes); - root.style.setProperty('--git-color-changes-bg', colors.git.changesBg); root.style.setProperty('--git-color-added', colors.git.added); - root.style.setProperty('--git-color-added-bg', colors.git.addedBg); - root.style.setProperty('--git-color-added-bg-hover', colorWithAlpha(colors.git.added, 0.15)); root.style.setProperty('--git-color-deleted', colors.git.deleted); - root.style.setProperty('--git-color-deleted-bg', colors.git.deletedBg); - root.style.setProperty('--git-color-deleted-bg-hover', colorWithAlpha(colors.git.deleted, 0.15)); root.style.setProperty('--git-color-staged', colors.git.staged); - root.style.setProperty('--git-color-staged-bg', colors.git.stagedBg); - root.style.setProperty('--git-color-staged-bg-hover', colorWithAlpha(colors.git.staged, 0.15)); - root.style.setProperty('--git-color-staged-border', colorWithAlpha(colors.git.staged, 0.3)); diff --git a/src/web-ui/src/infrastructure/theme/presets/china-night-theme.ts b/src/web-ui/src/infrastructure/theme/presets/china-night-theme.ts index 2e5b9fd11c..76e6ab1368 100644 --- a/src/web-ui/src/infrastructure/theme/presets/china-night-theme.ts +++ b/src/web-ui/src/infrastructure/theme/presets/china-night-theme.ts @@ -30,9 +30,6 @@ const CHINA_NIGHT_ERROR = '#e85555'; const chinaNightBackground = (alpha: number | string) => rgbaFromHex(CHINA_NIGHT_BACKGROUND, alpha); const chinaNightText = (alpha: number | string) => rgbaFromHex(CHINA_NIGHT_TEXT_PRIMARY, alpha); const chinaNightAccent = (alpha: number | string) => rgbaFromHex(CHINA_NIGHT_ACCENT, alpha); -const chinaNightSuccess = (alpha: number | string) => rgbaFromHex(CHINA_NIGHT_SUCCESS, alpha); -const chinaNightWarning = (alpha: number | string) => rgbaFromHex(CHINA_NIGHT_WARNING, alpha); -const chinaNightError = (alpha: number | string) => rgbaFromHex(CHINA_NIGHT_ERROR, alpha); export const bitfunChinaNightTheme: ThemeConfig = { @@ -96,11 +93,8 @@ export const bitfunChinaNightTheme: ThemeConfig = { branch: rgbFromHex(CHINA_NIGHT_ACCENT), branchBg: chinaNightAccent(0.12), changes: rgbFromHex(CHINA_NIGHT_WARNING), - changesBg: chinaNightWarning(0.12), added: rgbFromHex(CHINA_NIGHT_SUCCESS), - addedBg: chinaNightSuccess(0.12), deleted: rgbFromHex(CHINA_NIGHT_ERROR), - deletedBg: chinaNightError(0.12), }), }, diff --git a/src/web-ui/src/infrastructure/theme/presets/china-style-theme.ts b/src/web-ui/src/infrastructure/theme/presets/china-style-theme.ts index cb1c2def73..128bedf29e 100644 --- a/src/web-ui/src/infrastructure/theme/presets/china-style-theme.ts +++ b/src/web-ui/src/infrastructure/theme/presets/china-style-theme.ts @@ -30,9 +30,6 @@ const CHINA_STYLE_BORDER = '#6a5c46'; const chinaStylePaper = (alpha: number | string) => rgbaFromHex(CHINA_STYLE_PAPER, alpha); const chinaStyleBlue = (alpha: number | string) => rgbaFromHex(CHINA_STYLE_BLUE, alpha); -const chinaStyleSuccess = (alpha: number | string) => rgbaFromHex(CHINA_STYLE_SUCCESS, alpha); -const chinaStyleWarning = (alpha: number | string) => rgbaFromHex(CHINA_STYLE_WARNING, alpha); -const chinaStyleError = (alpha: number | string) => rgbaFromHex(CHINA_STYLE_ERROR, alpha); const chinaStyleBorder = (alpha: number | string) => rgbaFromHex(CHINA_STYLE_BORDER, alpha); export const bitfunChinaStyleTheme: ThemeConfig = { @@ -98,11 +95,8 @@ export const bitfunChinaStyleTheme: ThemeConfig = { branch: rgbFromHex(CHINA_STYLE_BLUE), branchBg: chinaStyleBlue(0.08), changes: rgbFromHex(CHINA_STYLE_WARNING), - changesBg: chinaStyleWarning(0.08), added: rgbFromHex(CHINA_STYLE_SUCCESS), - addedBg: chinaStyleSuccess(0.08), deleted: rgbFromHex(CHINA_STYLE_ERROR), - deletedBg: chinaStyleError(0.08), }), }, diff --git a/src/web-ui/src/infrastructure/theme/presets/cyber-theme.ts b/src/web-ui/src/infrastructure/theme/presets/cyber-theme.ts index 81a9aa3904..b13890fb06 100644 --- a/src/web-ui/src/infrastructure/theme/presets/cyber-theme.ts +++ b/src/web-ui/src/infrastructure/theme/presets/cyber-theme.ts @@ -30,9 +30,6 @@ const CYBER_ERROR = '#ff0055'; const CYBER_SURFACE_SECONDARY = '#1c1c1f'; const cyberAccent = (alpha: number | string) => rgbaFromHex(CYBER_ACCENT, alpha); -const cyberSuccess = (alpha: number | string) => rgbaFromHex(CYBER_SUCCESS, alpha); -const cyberWarning = (alpha: number | string) => rgbaFromHex(CYBER_WARNING, alpha); -const cyberError = (alpha: number | string) => rgbaFromHex(CYBER_ERROR, alpha); export const bitfunCyberTheme: ThemeConfig = { @@ -105,11 +102,8 @@ export const bitfunCyberTheme: ThemeConfig = { branch: rgbFromHex(CYBER_ACCENT), branchBg: cyberAccent(0.12), changes: rgbFromHex(CYBER_WARNING), - changesBg: cyberWarning(0.12), added: rgbFromHex(CYBER_SUCCESS), - addedBg: cyberSuccess(0.12), deleted: rgbFromHex(CYBER_ERROR), - deletedBg: cyberError(0.12), }), }, diff --git a/src/web-ui/src/infrastructure/theme/presets/dark-theme.ts b/src/web-ui/src/infrastructure/theme/presets/dark-theme.ts index 8c86a2f5a4..e98977de81 100644 --- a/src/web-ui/src/infrastructure/theme/presets/dark-theme.ts +++ b/src/web-ui/src/infrastructure/theme/presets/dark-theme.ts @@ -32,9 +32,6 @@ const DARK_SUCCESS = '#34d399'; const DARK_WARNING = '#f59e0b'; const DARK_ERROR = '#ef4444'; -const darkWarning = (alpha: number | string) => rgbaFromHex(DARK_WARNING, alpha); -const darkError = (alpha: number | string) => rgbaFromHex(DARK_ERROR, alpha); - export const bitfunDarkTheme: ThemeConfig = { id: 'bitfun-dark', @@ -87,11 +84,8 @@ export const bitfunDarkTheme: ThemeConfig = { branch: '#a1a1aa', branchBg: overlayWhite(0.06), changes: rgbFromHex(DARK_WARNING), - changesBg: darkWarning(0.1), added: 'rgb(34, 197, 94)', - addedBg: 'rgba(34, 197, 94, 0.1)', deleted: rgbFromHex(DARK_ERROR), - deletedBg: darkError(0.1), }), scrollbar: createDarkNeutralScrollbar(), diff --git a/src/web-ui/src/infrastructure/theme/presets/light-theme.ts b/src/web-ui/src/infrastructure/theme/presets/light-theme.ts index 08ececcf59..c83510cbde 100644 --- a/src/web-ui/src/infrastructure/theme/presets/light-theme.ts +++ b/src/web-ui/src/infrastructure/theme/presets/light-theme.ts @@ -30,9 +30,6 @@ const LIGHT_ERROR = '#c26565'; const lightInk = (alpha: number | string) => rgbaFromHex(LIGHT_INK, alpha); const lightAccent = (alpha: number | string) => rgbaFromHex(LIGHT_ACCENT, alpha); const lightAccentHover = (alpha: number | string) => rgbaFromHex(LIGHT_ACCENT_HOVER, alpha); -const lightSuccess = (alpha: number | string) => rgbaFromHex(LIGHT_SUCCESS, alpha); -const lightWarning = (alpha: number | string) => rgbaFromHex(LIGHT_WARNING, alpha); -const lightError = (alpha: number | string) => rgbaFromHex(LIGHT_ERROR, alpha); export const bitfunLightTheme: ThemeConfig = { @@ -130,11 +127,8 @@ export const bitfunLightTheme: ThemeConfig = { branch: rgbFromHex(LIGHT_ACCENT_HOVER), branchBg: lightAccentHover(0.1), changes: rgbFromHex(LIGHT_WARNING), - changesBg: lightWarning(0.08), added: rgbFromHex(LIGHT_SUCCESS), - addedBg: lightSuccess(0.08), deleted: rgbFromHex(LIGHT_ERROR), - deletedBg: lightError(0.08), }), }, diff --git a/src/web-ui/src/infrastructure/theme/presets/midnight-theme.ts b/src/web-ui/src/infrastructure/theme/presets/midnight-theme.ts index 1917393163..0f0b061a92 100644 --- a/src/web-ui/src/infrastructure/theme/presets/midnight-theme.ts +++ b/src/web-ui/src/infrastructure/theme/presets/midnight-theme.ts @@ -31,9 +31,6 @@ const MIDNIGHT_ERROR = '#cc7f7a'; const midnightBackground = (alpha: number | string) => rgbaFromHex(MIDNIGHT_BACKGROUND, alpha); const midnightText = (alpha: number | string) => rgbaFromHex(MIDNIGHT_TEXT_PRIMARY, alpha); const midnightAccent = (alpha: number | string) => rgbaFromHex(MIDNIGHT_ACCENT, alpha); -const midnightSuccess = (alpha: number | string) => rgbaFromHex(MIDNIGHT_SUCCESS, alpha); -const midnightWarning = (alpha: number | string) => rgbaFromHex(MIDNIGHT_WARNING, alpha); -const midnightError = (alpha: number | string) => rgbaFromHex(MIDNIGHT_ERROR, alpha); export const bitfunMidnightTheme: ThemeConfig = { @@ -96,11 +93,8 @@ export const bitfunMidnightTheme: ThemeConfig = { branch: rgbFromHex(MIDNIGHT_ACCENT), branchBg: midnightAccent(0.1), changes: rgbFromHex(MIDNIGHT_WARNING), - changesBg: midnightWarning(0.1), added: rgbFromHex(MIDNIGHT_SUCCESS), - addedBg: midnightSuccess(0.1), deleted: rgbFromHex(MIDNIGHT_ERROR), - deletedBg: midnightError(0.1), }), }, diff --git a/src/web-ui/src/infrastructure/theme/presets/shared.ts b/src/web-ui/src/infrastructure/theme/presets/shared.ts index 8184d77f16..aa9b21ce5e 100644 --- a/src/web-ui/src/infrastructure/theme/presets/shared.ts +++ b/src/web-ui/src/infrastructure/theme/presets/shared.ts @@ -291,12 +291,11 @@ export function createDarkNeutralElement(): ElementBackgrounds { } export function createGitColors( - config: Omit & Partial>, + config: Omit & Partial>, ): GitColors { return { ...config, staged: config.staged ?? config.added, - stagedBg: config.stagedBg ?? config.addedBg, }; } diff --git a/src/web-ui/src/infrastructure/theme/presets/slate-theme.ts b/src/web-ui/src/infrastructure/theme/presets/slate-theme.ts index ec7bee09c1..8892aafc61 100644 --- a/src/web-ui/src/infrastructure/theme/presets/slate-theme.ts +++ b/src/web-ui/src/infrastructure/theme/presets/slate-theme.ts @@ -34,10 +34,6 @@ const SLATE_SUCCESS = '#7eb09b'; const SLATE_WARNING = '#f59e0b'; const SLATE_ERROR = '#c9878d'; -const slateSuccess = (alpha: number | string) => rgbaFromHex(SLATE_SUCCESS, alpha); -const slateWarning = (alpha: number | string) => rgbaFromHex(SLATE_WARNING, alpha); -const slateError = (alpha: number | string) => rgbaFromHex(SLATE_ERROR, alpha); - export const bitfunSlateTheme: ThemeConfig = { id: 'bitfun-slate', @@ -108,11 +104,8 @@ export const bitfunSlateTheme: ThemeConfig = { branch: SLATE_ACCENT, branchBg: overlayWhite(0.06), changes: rgbFromHex(SLATE_WARNING), - changesBg: slateWarning(0.1), added: rgbFromHex(SLATE_SUCCESS), - addedBg: slateSuccess(0.1), deleted: rgbFromHex(SLATE_ERROR), - deletedBg: slateError(0.1), }), scrollbar: createDarkNeutralScrollbar(), diff --git a/src/web-ui/src/infrastructure/theme/presets/themePresetOutput.test.ts b/src/web-ui/src/infrastructure/theme/presets/themePresetOutput.test.ts index e742e4aa2d..d0cce95b6b 100644 --- a/src/web-ui/src/infrastructure/theme/presets/themePresetOutput.test.ts +++ b/src/web-ui/src/infrastructure/theme/presets/themePresetOutput.test.ts @@ -37,30 +37,21 @@ describe('builtin theme preset output', () => { branch: '#64748b', branchBg: 'rgba(100, 116, 139, 0.1)', changes: '#f59e0b', - changesBg: 'rgba(245, 158, 11, 0.1)', added: '#22c55e', - addedBg: 'rgba(34, 197, 94, 0.1)', deleted: '#ef4444', - deletedBg: 'rgba(239, 68, 68, 0.1)', })).toMatchObject({ staged: '#22c55e', - stagedBg: 'rgba(34, 197, 94, 0.1)', }); expect(createGitColors({ branch: '#64748b', branchBg: 'rgba(100, 116, 139, 0.1)', changes: '#f59e0b', - changesBg: 'rgba(245, 158, 11, 0.1)', added: '#22c55e', - addedBg: 'rgba(34, 197, 94, 0.1)', deleted: '#ef4444', - deletedBg: 'rgba(239, 68, 68, 0.1)', staged: '#10b981', - stagedBg: 'rgba(16, 185, 129, 0.1)', })).toMatchObject({ staged: '#10b981', - stagedBg: 'rgba(16, 185, 129, 0.1)', }); }); @@ -149,42 +140,42 @@ describe('builtin theme preset output', () => { }))).toMatchInlineSnapshot(` [ { - "hash": "63622f32216bbb5aea27e3ba0aec61f385a43842e57a6f17c814b341b8a54ac7", + "hash": "64235e8fca47462c9943d3015d024dbd9be8df90d25a5e7508f7e5b2ee789c6c", "id": "bitfun-light", "type": "light", }, { - "hash": "a62e5f3c6a5cbf4c7a35a2f51ed3a5f2a1eb82f6421f11e194ebb9a707517311", + "hash": "d6b190bdf145c29e28852d4922e0ee03cfe635ccc42081d82acd9ed75ba9e9c6", "id": "bitfun-slate", "type": "dark", }, { - "hash": "a527410c9bdf5ed396d82d843e19b298f8b5fe280ebc70bfe01237e753b28713", + "hash": "65b55d4d15503949e120e28fa1210449dfec390af69514410cea429f28bd19da", "id": "bitfun-dark", "type": "dark", }, { - "hash": "8b2f11e903371752749b5c7a44337df517dbb2ed9cf0fed29a2341ebb1890f0b", + "hash": "57d5efdaec49f1e4f21f3d095150dd81e810a0dedae4ae690d15b99e042228fa", "id": "bitfun-midnight", "type": "dark", }, { - "hash": "d198bd1a20566689f5bf2660bc47b5aa2da854e5f3dcae9e655a5408b36141d1", + "hash": "9fa44f208108fa973bdded43853ad1de449a2f235b3469350900d3afa3e8a50a", "id": "bitfun-china-style", "type": "light", }, { - "hash": "f93427f5952e5a7fe5882cb114a6969e8fb7564292761029d6ee071233b2bdd5", + "hash": "89461a6629abee37ae1d573b57bce7acf54b05101425c82b65d444573ef66722", "id": "bitfun-china-night", "type": "dark", }, { - "hash": "fffcb8a66e3a10745d155402be32b90df29e162f3b4a589862cccb9bafb215b7", + "hash": "14f6897c94657f1e090107d8ff9d81d7540fad1fca0eaf1b068662b9fa5a518b", "id": "bitfun-cyber", "type": "dark", }, { - "hash": "81ece301a307408b2149141c7b1553bf422245d20278e98442d6c36063acfc3b", + "hash": "80c5e00c474fd929154996d1fc8f0a230c50c5ccaaa966fbe588cf2349e2ede5", "id": "bitfun-tokyo-night", "type": "dark", }, diff --git a/src/web-ui/src/infrastructure/theme/presets/tokyo-night-theme.ts b/src/web-ui/src/infrastructure/theme/presets/tokyo-night-theme.ts index 405ef9fb06..a7149c3837 100644 --- a/src/web-ui/src/infrastructure/theme/presets/tokyo-night-theme.ts +++ b/src/web-ui/src/infrastructure/theme/presets/tokyo-night-theme.ts @@ -35,12 +35,8 @@ const TOKYO_GIT_ADDED = '#41a6b5'; const TOKYO_PRIMARY_BUTTON = '#3d59a1'; const tokyoAccent = (alpha: number | string) => rgbaFromHex(TOKYO_ACCENT, alpha); -const tokyoSuccess = (alpha: number | string) => rgbaFromHex(TOKYO_SUCCESS, alpha); -const tokyoWarning = (alpha: number | string) => rgbaFromHex(TOKYO_WARNING, alpha); -const tokyoError = (alpha: number | string) => rgbaFromHex(TOKYO_ERROR, alpha); const tokyoBorder = (alpha: number | string) => rgbaFromHex(TOKYO_BORDER, alpha); const tokyoScrollbar = (alpha: number | string) => rgbaFromHex(TOKYO_SCROLLBAR, alpha); -const tokyoGitAdded = (alpha: number | string) => rgbaFromHex(TOKYO_GIT_ADDED, alpha); const tokyoPrimaryButton = (alpha: number | string) => rgbaFromHex(TOKYO_PRIMARY_BUTTON, alpha); /** Colors aligned with the Tokyo Night palette (Enkia / VS Code Tokyo Night). */ @@ -114,13 +110,9 @@ export const bitfunTokyoNightTheme: ThemeConfig = { branch: rgbFromHex(TOKYO_ACCENT), branchBg: tokyoAccent(0.12), changes: rgbFromHex(TOKYO_WARNING), - changesBg: tokyoWarning(0.12), added: rgbFromHex(TOKYO_GIT_ADDED), - addedBg: tokyoGitAdded(0.12), deleted: rgbFromHex(TOKYO_ERROR), - deletedBg: tokyoError(0.12), staged: rgbFromHex(TOKYO_SUCCESS), - stagedBg: tokyoSuccess(0.12), }), scrollbar: { diff --git a/src/web-ui/src/infrastructure/theme/types/index.ts b/src/web-ui/src/infrastructure/theme/types/index.ts index a78822c94a..c98543afe1 100644 --- a/src/web-ui/src/infrastructure/theme/types/index.ts +++ b/src/web-ui/src/infrastructure/theme/types/index.ts @@ -97,13 +97,9 @@ export interface GitColors { branch: ColorValue; branchBg: ColorValue; changes: ColorValue; - changesBg: ColorValue; added: ColorValue; - addedBg: ColorValue; deleted: ColorValue; - deletedBg: ColorValue; staged: ColorValue; - stagedBg: ColorValue; } diff --git a/src/web-ui/src/tools/git/components/GitDiffView/GitDiffView.scss b/src/web-ui/src/tools/git/components/GitDiffView/GitDiffView.scss index f08610e371..fbcfe2d880 100644 --- a/src/web-ui/src/tools/git/components/GitDiffView/GitDiffView.scss +++ b/src/web-ui/src/tools/git/components/GitDiffView/GitDiffView.scss @@ -235,17 +235,17 @@ $opacity-high: 0.8 !default; color: var(--color-text-muted); &--added { - background: var(--git-color-added-bg); + background: color-mix(in srgb, var(--git-color-added) 10%, transparent); color: var(--git-color-added); } &--modified { - background: var(--git-color-changes-bg); + background: color-mix(in srgb, var(--git-color-changes) 10%, transparent); color: var(--git-color-changes); } &--deleted { - background: var(--git-color-deleted-bg); + background: color-mix(in srgb, var(--git-color-deleted) 10%, transparent); color: var(--git-color-deleted); } @@ -304,10 +304,10 @@ $opacity-high: 0.8 !default; transition: background $motion-fast $easing-standard; &--added { - background: var(--git-color-added-bg); + background: color-mix(in srgb, var(--git-color-added) 10%, transparent); &:hover { - background: var(--git-color-added-bg-hover); + background: color-mix(in srgb, var(--git-color-added) 15%, transparent); } .bitfun-git-diff-view__line-content { @@ -316,10 +316,10 @@ $opacity-high: 0.8 !default; } &--deleted { - background: var(--git-color-deleted-bg); + background: color-mix(in srgb, var(--git-color-deleted) 10%, transparent); &:hover { - background: var(--git-color-deleted-bg-hover); + background: color-mix(in srgb, var(--git-color-deleted) 15%, transparent); } .bitfun-git-diff-view__line-content {