diff --git a/docs/architecture/theme-token-optimization.md b/docs/architecture/theme-token-optimization.md index 5c0640609a..bbea7f82fb 100644 --- a/docs/architecture/theme-token-optimization.md +++ b/docs/architecture/theme-token-optimization.md @@ -82,8 +82,8 @@ BitFun 后续适配插件体系时也必须遵循同一边界:插件消费 TS | 事项 | Rust/desktop 侧 | TS/web-ui 侧 | | --- | --- | --- | -| 主题选择持久化 | 启动时只读取 `themes.current`,解析 `system`、内置主题 id 和未知值回退;不校验完整主题 schema。 | 读写 `themes.current`,处理 `system`、内置主题和 custom theme 选择。 | -| 完整主题契约 | 不维护完整 `ThemeConfig`、semantic token、component token 或专用 palette。 | 拥有 `ThemeConfig`、主题预设、validator、import/export、runtime CSS 变量注入和审计 registry。 | +| 主题选择持久化 | 启动时只读取 `themes.current`,解析 `system`、内置主题 id 和未知值回退;历史 `theme.id` 只在配置加载/导入时一次性归一到 `themes.current`,不能作为新扩展入口。 | 读写 `themes.current`,处理 `system`、内置主题和 custom theme 选择。 | +| 完整主题契约 | 不维护完整 `ThemeConfig`、semantic token、component token 或专用 palette;旧 Rust GUI theme struct/provider 已退役,默认导出不得出现顶层 `theme`。 | 拥有 `ThemeConfig`、主题预设、validator、import/export、runtime CSS 变量注入和审计 registry。 | | 首屏 bootstrap | 只注入 WebView 首屏所需最小投影:`data-theme`、`data-theme-type`、核心背景/文本 CSS 变量和 `--bitfun-startup-bg`。 | JS 启动后必须重新应用完整主题,覆盖 bootstrap 投影并恢复 Monaco、Mermaid、terminal、widget payload 等专用域。 | | 生成式 UI 主题提示 | 只读取 TS 预设生成的 prompt snapshot manifest,用于模型提示;不得手写内置主题 palette。 | 拥有 prompt snapshot 投影规则;新内置主题加入 `builtinThemes` 后由生成器同步到 Rust 只读 manifest。 | | 插件主题色扩展 | 不维护插件色彩 schema,不把 plugin color 解析放到 Rust/desktop。 | 拥有 `pluginThemeProjection.ts`,把完整主题映射到 OpenCode-compatible 的 7 个语义色 key;插件不得直接依赖内部 CSS var 全量表。 | @@ -111,6 +111,9 @@ contract,并纳入审计基线。 `src/apps/desktop/src/generated/startup_theme_bootstrap.json`。Rust 侧只持有 `id`、`bgPrimary`、`bgSecondary`、`bgScene`、`isLight`、`textPrimary`、 `textMuted`、`accentColor` 这类首屏字段,不维护完整主题 schema 或 palette。 +Rust 配置层只保留一个独立的历史兼容 fallback:加载、导入或旧调用路径遇到 +`theme.id` 时映射到 `themes.current`,并在保存时移除顶层 `theme`;其他旧 GUI theme +字段不再解析、不再校验,也不再作为 public theme contract 暴露。 `GenerativeUITool` 的内置主题提示也必须读取同一生成链路产出的 `src/crates/assembly/core/src/agentic/tools/implementations/generated/theme_prompt_snapshots.json`, 避免 Rust 再手写一套 web-ui 主题色。两个 manifest 都只是 TS 主题预设的只读投影: @@ -152,7 +155,7 @@ Mermaid、Monaco/editor、Prism syntax、terminal ANSI、language identity 和 U registry。Mobile Web 和 Installer 已纳入同一审计口径,但各自使用独立 baseline, 避免移动端或安装器的独立 token 被误算为 web-ui 游离 key。 -`345` 个 web-ui 唯一颜色是前端生产文件的全域审计数,不是普通 app UI 的色值预算。 +`342` 个 web-ui 唯一颜色是前端生产文件的全域审计数,不是普通 app UI 的色值预算。 其中包含主题 preset、token contract、Mermaid、Monaco/editor、terminal、syntax、 language identity 和 UI exception 等专用 palette。language identity 已收敛为 8 个大类身份色, 不再按每种语言或文件类型保留独立色值。真正需要继续压缩的是这些专用域 @@ -178,8 +181,8 @@ label、icon、扩展名或文件名之一,不允许构建只靠颜色区分 | 忽略的测试文件数 | 224 | | 忽略的构建生成文件数 | 0 | | 包含颜色字面量的文件数 | 25 | -| 颜色字面量出现次数 | 536 | -| 唯一颜色字面量数量 | 345 | +| 颜色字面量出现次数 | 535 | +| 唯一颜色字面量数量 | 342 | | 组件或非 token 文件中的颜色出现次数 | 0 | | 组件或非 token 唯一颜色数量 | 0 | | App UI 颜色出现次数 | 0 | @@ -191,13 +194,13 @@ label、icon、扩展名或文件名之一,不允许构建只靠颜色区分 | 普通组件肉眼不可区分 near color pair | 0 | | 普通组件需证据复核的 near color pair | 0 | | 专用域肉眼不可区分 near color pair | 0 | -| 专用域需证据复核的 near color pair | 13 | +| 专用域需证据复核的 near color pair | 9 | 跨 root 基线必须分别看待;这些数字不是互相累加后的单一上限: | root | 颜色出现次数 | 唯一颜色 | App UI raw | fallback var | unresolved / non-contract key | dynamic family | 说明 | | --- | ---: | ---: | ---: | ---: | ---: | ---: | --- | -| `src/web-ui/src` | 536 | 345 | 0 | 0 | 0 | 13 | 主应用完整主题、专用 palette、widget payload 和 editor/terminal/Mermaid 域;UI exception、syntax、language identity 与 boundary fallback 已收敛到小型语义 palette,未读取的 git 同义 runtime/static key 和未消费 legacy mixin 已退役。 | +| `src/web-ui/src` | 535 | 342 | 0 | 0 | 0 | 13 | 主应用完整主题、专用 palette、widget payload 和 editor/terminal/Mermaid 域;UI exception、syntax、language identity 与 boundary fallback 已收敛到小型语义 palette,未读取的 git 同义 runtime/static key 和未消费 legacy mixin 已退役。 | | `src/mobile-web/src` | 35 | 30 | 0 | 0 | 0 | 3 | mobile-web 图片缩略图关闭按钮已读取 `--color-static-white`,普通 app UI raw 归零。 | | `BitFun-Installer/src` | 76 | 62 | 0 | 0 | 0 | 1 | 安装器主题数据保留主题卡可见的 primary/secondary background、accent、text 和状态反馈;runtime 只导出实际消费的 accent family、基础 text/border/element/status key,不复制主应用 purple/info/tooltip 等完整主题面。 | @@ -276,7 +279,7 @@ PR 通过而放宽 `appUi`、fallback、unresolved、non-contract 或 dynamic fa | 区域 | 当前出现次数 | 当前唯一色数 | 说明 | | --- | ---: | ---: | --- | -| Theme presets | 171 | 119 | 主题个性与 palette 映射;跨主题深色 neutral、弱文本、非状态浅色背景和同概念 success 色已收敛;相邻 surface、主题识别主背景、状态色和 editor lineHighlight 继续保留 | +| Theme presets | 170 | 116 | 主题个性与 palette 映射;跨主题深色 neutral、弱文本、非状态浅色背景和同概念 success 色已收敛;相邻 surface、主题识别主背景、状态色和 editor lineHighlight 继续保留 | | Token contracts | 108 | 97 | `tokens.scss` 等静态契约根;黑白 overlay alpha stop 继续保留相邻状态层级,未消费 legacy mixin 和自引用别名已移除,不按数值相近强行合并 | | Editor | 52 | 48 | Monaco/editor 专用域,不能直接泛化到 app token;被动 selection/word highlight 已收敛,但 diff text/line/gutter 继续保留用户可见层级 | | Mermaid | 82 | 65 | Mermaid 专用渲染域;light status fallback 已改读 app semantic status token,critical/error 背景弱重复已收敛,note 文本保留专用高对比值 | @@ -297,7 +300,7 @@ root 的当前队列,不是自动合并指令, | 专用域 | 肉眼不可区分 pair | 需证据复核 pair | 后续处理原则 | | --- | ---: | ---: | --- | -| Theme presets | 0 | 9 | 只剩 light/paper 相邻 surface、Ink Night/Tokyo Night 主题识别 surface 和 Monaco lineHighlight 等需视觉证据的 near pair;不按数值继续强合并 | +| Theme presets | 0 | 5 | 保留 Tokyo Night 主题识别主背景、Ink Night secondary/lineHighlight、Monaco light lineHighlight、Midnight lineHighlight/elevation 等需视觉证据的 near pair;不按数值继续强合并 | | Theme runtime | 0 | 2 | 黑白 overlay runtime stop 虽然数值接近,但直接服务 card、markdown、shadow 等相邻状态;后续只能在有视觉证据时继续压缩 | | Token contracts | 0 | 2 | 静态 token 黑白 overlay stop 保留与 runtime 同步的状态层级;不能为了清零 near pair 破坏 hover/active/elevated 区分 | | Boundary fallback | 0 | 0 | first-paint 兜底已收敛到较粗 overlay stop;后续新增必须重新说明边界理由 | @@ -417,6 +420,9 @@ Phase 5 决策记录: | generated widget derived payload keys | move derived keys to iframe fallback/static shell | `themePayload.ts`、`themePayloadCompatibility.ts`、`themePayload.test.ts` | accent 细分 stop、status border、strong/prominent border、strong element 和 `--size-radius-md` 不再从 host payload 读取;iframe fallback/static shell 继续解析这些派生 key,payload allowlist 94 -> 80 | | CLI preset/runtime near cleanup | merge non-semantic close surfaces | `src/apps/cli/themes/presets/*.json` | Dark/Ink/Midnight/Cyber 的非状态 surface、input、border 和极近前景值复用已有 preset stops;CLI preset unique 122 -> 118、total unique 152 -> 148,preset/runtime near pair 均降为 0。OpenCode compatibility 字段单独计数,不冒充 runtime 收益 | | mobile static overlay foreground | tokenized static color | `src/mobile-web/src/theme/presets/shared.ts`、`chat-input.scss`、`light.ts` | 图片缩略图关闭按钮必须在黑色 overlay 上保持白色前景;新增 mobile `--color-static-white` 后组件 raw `#fff` 归零,light secondary 改读该静态 token,mobile 总颜色出现次数不增加且唯一色 31 -> 30 | +| Rust GUI theme config fallback | retire legacy schema | `src/crates/assembly/core/src/service/config/{types,manager,providers,global,service}.rs` | 删除旧顶层 GUI `theme` struct/provider/default 导出,只保留 `theme.id` -> `themes.current` 的加载、导入和旧调用路径 fallback;导入路径保留 raw config JSON 到 manager 归一化后再反序列化,避免旧 `theme.id` 被提前丢弃。Rust 不再拥有完整 UI theme schema,终端 `terminal.theme` 仍作为 ANSI palette 历史字段保留 | +| web theme preset near cleanup | merge non-critical preset surfaces | `src/web-ui/src/infrastructure/theme/presets/{china-style,light,slate,tokyo-night}-theme.ts` | Light quaternary 复用浅灰背景、Slate button text 复用 primary text、China Style elevated 复用 tertiary、Tokyo border 复用既有深 slate;产品复审后保留 Ink Night secondary,因为它也承担 Monaco lineHighlight 和相邻 ink surface 层级。Theme preset unique 119 -> 116,near pair 9 -> 5 | +| installer dark preview background cleanup | preserve adjacent theme-card identity | `BitFun-Installer/src/theme/installerThemesData.ts` | 产品复审确认 ThemeSetup 会相邻展示主题卡并直接渲染 primary/secondary background,因此 Ink Night/Cyber/Tokyo Night primary 属于用户可见主题识别,不再合并。Installer theme preset unique 保持 53,near pair 保持 3;后续若要继续压缩,需要先新增非背景 identity swatch 或截图证明不会削弱选择识别。 | | CLI warm removed surface | defer | `src/apps/cli/themes/presets/*.json`、`src/apps/cli/src/ui/theme.rs` | 复审发现 `diffRemovedLineNumberBg` 当前未被 CLI renderer 消费,因此不把该类 preset 字段当作真实渲染收益压 baseline;本轮撤回 CLI preset 改动,后续应先明确 schema 消费边界再压缩 | | 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 | @@ -431,7 +437,7 @@ Phase 6 防回退约束: | `nearPairs.indistinguishableTotal` | 0 | 0 | 阻止新增普通组件肉眼不可区分 pair 未被合并或记录 | | `nearPairs.nearTotal` | 0 | 0 | 阻止新增普通组件 near color 债务;新增必须合并、归类或记录理由 | | `colorDomainNearPairs.indistinguishableTotal` | 0 | 0 | 控制专用域肉眼不可区分 pair 不继续增长,后续只能逐步降低或补充证据 | -| `colorDomainNearPairs.nearTotal` | 13 | 13 | 控制 theme preset/runtime/token/editor/Mermaid 等专用域 near 队列规模 | +| `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.exception.uniqueColors` | 182 | 182 | 控制专用域/例外域总体规模;UI exception、syntax 和 language identity 已收敛,但 Mermaid/editor/terminal 仍按各自 owner 单独治理 | @@ -507,6 +513,8 @@ editor、syntax、terminal、language identity、boundary fallback 等专用域 同时补充了一批 app 级别别名和覆盖值。 - `src/web-ui/src/infrastructure/theme/presets/*.ts` 定义完整主题预设色板。 - `src/apps/desktop/src/theme.rs` 只维护 WebView 首屏最小 bootstrap 投影,不能扩展为完整主题 schema。 +- `src/crates/assembly/core/src/service/config/manager.rs` 只保留旧 `theme.id` 到 + `themes.current` 的兼容归一化;`GlobalConfig` 不再包含顶层 GUI `theme`。 - `src/web-ui/src/tools/generative-widget/themePayload.ts` 向 generative widget payload 暴露部分主题变量。 - `src/mobile-web/src/theme/presets` 定义 mobile-web 的独立运行时 token;移动端组件只读这些 @@ -1110,8 +1118,8 @@ alpha 差异经常承担 elevation 和交互状态,不应全部压成一个值 2. 插件/主题扩展投影治理:插件侧只消费 `pluginThemeProjection.ts` 暴露的 7 个 OpenCode-compatible 语义色 key; 新增插件 UI 需求必须先证明无法复用这 7 个 key,再新增小型 surface projection。不得把 generated widget payload 或完整 runtime CSS var 表作为插件主题 schema。 -3. 专用域近似色复核:mobile-web theme preset near pair 已清零;installer 保留 3 个承担主题选择预览识别的 dark primary near pair;web-ui 已先处理不可感知的跨主题 neutral、tooltip 派生、runtime/static panel 值、极近 overlay fallback,以及 theme preset 的非状态 neutral/弱文本/scene/workbench 值。 - 后续继续处理 theme preset 时必须避开按钮 hover/active、light/paper 相邻 surface ramp、Monaco lineHighlight、Mermaid、terminal、syntax 等相邻状态色,除非有截图证据证明不会损害层级。 +3. 专用域近似色复核:mobile-web theme preset near pair 已清零;installer 保留 3 个相邻主题卡可见的 named dark primary preview pair;web-ui 已先处理不可感知的跨主题 neutral、tooltip 派生、runtime/static panel 值、极近 overlay fallback,以及 theme preset 的非状态 neutral/弱文本/scene/workbench 值。 + 后续继续处理 theme preset 时必须避开按钮 hover/active、Tokyo Night 主题识别主背景、Ink Night secondary/lineHighlight、Monaco lineHighlight、Midnight lineHighlight/elevation、Mermaid、terminal、syntax 等相邻状态色,除非有截图证据证明不会损害层级。 4. CLI/TUI palette 压缩:CLI/TUI 已纳入 `theme:color-audit:cli`,Rust fallback、preset 和 runtime-consumed preset near pair 均为 0;后续只处理 preset 内能证明同一终端 surface 语义且已被 CLI renderer 消费的近似色, 不能让 Rust/CLI 复制 web-ui palette。若要跨 surface 共享,只能先定义共享语义投影或生成链路。 5. 自定义主题扩展后续体验优化:custom theme 校验、加载、注册、导出和 preview 输入已绑定到 TS schema; diff --git a/scripts/theme-color-governance-baseline.json b/scripts/theme-color-governance-baseline.json index 7d54eb57bc..4162df32c6 100644 --- a/scripts/theme-color-governance-baseline.json +++ b/scripts/theme-color-governance-baseline.json @@ -105,7 +105,7 @@ "max": 0 }, "colorScopes.token.uniqueColors": { - "max": 200 + "max": 197 }, "colorScopes.exception.uniqueColors": { "max": 182 @@ -153,13 +153,13 @@ "max": 0 }, "colorDomainNearPairs.nearTotal": { - "max": 13 + "max": 9 }, "colorDomainNearPairs.themePreset.indistinguishableTotal": { "max": 0 }, "colorDomainNearPairs.themePreset.nearTotal": { - "max": 9 + "max": 5 }, "colorDomainNearPairs.themeRuntime.indistinguishableTotal": { "max": 0 @@ -240,10 +240,10 @@ "max": 0 }, "colorDomainScopes.themePreset.occurrences": { - "max": 171 + "max": 170 }, "colorDomainScopes.themePreset.uniqueColors": { - "max": 119 + "max": 116 }, "colorDomainScopes.themeRuntime.occurrences": { "max": 27 diff --git a/scripts/theme-color-near-pair-decisions.json b/scripts/theme-color-near-pair-decisions.json index 1a1043bee6..f5cfa7c611 100644 --- a/scripts/theme-color-near-pair-decisions.json +++ b/scripts/theme-color-near-pair-decisions.json @@ -17,26 +17,8 @@ "key": "#1c1c1f <-> #212019", "decision": "keep", "owner": "src/web-ui/src/infrastructure/theme/presets", - "reason": "China Night secondary background keeps a warmer ink tint next to the shared dark neutral; it is a theme-identity surface, not a stray duplicate.", - "reevaluateWhen": "Only if China Night background surfaces are redesigned around the shared neutral scale." - }, - { - "root": "src/web-ui/src", - "domain": "themePreset", - "key": "#e0e3e8 <-> #e8e8e8", - "decision": "keep", - "owner": "src/web-ui/src/infrastructure/theme/presets", - "reason": "Light quaternary surface and slate button text sit near primary text/background stops; they can appear as adjacent surface/text roles and should not be collapsed without focused screenshots.", - "reevaluateWhen": "Only after button text and light surface-ramp screenshots show no loss of hierarchy." - }, - { - "root": "src/web-ui/src", - "domain": "themePreset", - "key": "#e8e8e8 <-> #ebe9e3", - "decision": "keep", - "owner": "src/web-ui/src/infrastructure/theme/presets", - "reason": "China Style elevated paper keeps a warm paper tint while #e8e8e8 is a shared cool neutral/text stop. The distinction is a theme personality cue.", - "reevaluateWhen": "Only if China Style paper surfaces are intentionally moved to the shared neutral ramp." + "reason": "China Night secondary background also feeds Monaco lineHighlight through the theme projection. Merging it into the primary background removes the visible current-line cue and flattens adjacent ink surfaces.", + "reevaluateWhen": "Only if China Night gets a separate editor lineHighlight token or visual review proves the current-line cue remains visible." }, { "root": "src/web-ui/src", @@ -65,24 +47,6 @@ "reason": "China Style paper uses a warm first-paint surface while #f3f3f5 is a cool light neutral and button state stop. This is cross-theme identity, not a duplicate role.", "reevaluateWhen": "Only after China Style paper identity is intentionally neutralized." }, - { - "root": "src/web-ui/src", - "domain": "themePreset", - "key": "#334155 <-> #363b54", - "decision": "keep", - "owner": "src/web-ui/src/infrastructure/theme/presets", - "reason": "Light strong text and Tokyo border encode different theme-local roles. They are not the same semantic token even though their RGB distance is low.", - "reevaluateWhen": "Only if both roles are remapped through a shared semantic border/text projection." - }, - { - "root": "src/web-ui/src", - "domain": "themePreset", - "key": "#ebe9e3 <-> #f0ede0", - "decision": "keep", - "owner": "src/web-ui/src/infrastructure/theme/presets", - "reason": "China Style tertiary and elevated paper surfaces are adjacent paper-ramp stops. Collapsing them would reduce section/elevation separation.", - "reevaluateWhen": "Only after China Style paper-ramp screenshots prove the two stops are not adjacent in product UI." - }, { "root": "src/web-ui/src", "domain": "themeRuntime", @@ -125,8 +89,8 @@ "key": "#0e0e10 <-> #121214", "decision": "keep", "owner": "BitFun-Installer/src/theme/installerThemesData.ts", - "reason": "Cyber and default dark installer cards keep distinct theme preview backgrounds; the values appear as separate theme cards rather than adjacent layers inside one installer surface.", - "reevaluateWhen": "Only after installer theme preview screenshots show Cyber and default dark cards no longer need separate preview identity." + "reason": "ThemeSetup renders dark theme cards side by side with primary and secondary preview backgrounds. Cyber needs its own darker primary preview seed so the named theme is not distinguishable only by label and accent.", + "reevaluateWhen": "Only if installer theme selection adds a separate identity swatch or screenshots prove the primary preview no longer carries theme identity." }, { "root": "BitFun-Installer/src", @@ -134,8 +98,8 @@ "key": "#1a1b26 <-> #1a1c1e", "decision": "keep", "owner": "BitFun-Installer/src/theme/installerThemesData.ts", - "reason": "Tokyo Night primary background intentionally remains close to the web Tokyo hue while the shared installer card surface stays neutral for preview contrast.", - "reevaluateWhen": "Only if the installer Tokyo Night preview is redesigned to inherit the shared dark card surface." + "reason": "Tokyo Night appears as an adjacent installer preview card, and its primary background should preserve the Tokyo base hue while the shared installer card surface stays neutral.", + "reevaluateWhen": "Only after installer preview cards are redesigned to use a dedicated non-background theme identity affordance." }, { "root": "BitFun-Installer/src", @@ -143,8 +107,8 @@ "key": "#121214 <-> #1a1814", "decision": "keep", "owner": "BitFun-Installer/src/theme/installerThemesData.ts", - "reason": "Ink Night keeps a warm dark preview background near the default dark installer card background; this communicates theme identity across separate theme cards.", - "reevaluateWhen": "Only after installer preview screenshots prove the warm Ink Night identity is no longer visually meaningful." + "reason": "Ink Night uses a warmer dark primary background in adjacent installer preview cards. Collapsing it into the canonical dark seed makes multiple dark choices visually flatter before the app is installed.", + "reevaluateWhen": "Only if visual review shows installer dark theme cards no longer rely on primary background identity." } ] } diff --git a/src/apps/desktop/src/api/config_api.rs b/src/apps/desktop/src/api/config_api.rs index db2e817492..8af0adae46 100644 --- a/src/apps/desktop/src/api/config_api.rs +++ b/src/apps/desktop/src/api/config_api.rs @@ -37,6 +37,12 @@ pub struct ResetConfigRequest { pub path: Option, } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ImportConfigRequest { + pub config_data: Value, +} + #[derive(Debug, Deserialize, Default)] pub struct GetRuntimeLoggingInfoRequest {} @@ -262,13 +268,18 @@ pub async fn export_config(state: State<'_, AppState>) -> Result } #[tauri::command] -pub async fn import_config(state: State<'_, AppState>, config: Value) -> Result { +pub async fn import_config( + state: State<'_, AppState>, + request: ImportConfigRequest, +) -> Result { let config_service = &state.config_service; + let config_data = request + .config_data + .get("config") + .cloned() + .unwrap_or(request.config_data); - let export_data: bitfun_core::service::config::ConfigExport = - serde_json::from_value(config).map_err(|e| format!("Invalid config format: {}", e))?; - - match config_service.import_config(export_data).await { + match config_service.import_config_data(config_data).await { Ok(result) => { state.ai_client_factory.invalidate_cache(); info!("Config imported, AI client cache invalidated"); diff --git a/src/crates/assembly/core/src/agentic/tools/implementations/generated/theme_prompt_snapshots.json b/src/crates/assembly/core/src/agentic/tools/implementations/generated/theme_prompt_snapshots.json index 592aee7121..0c8f5315da 100644 --- a/src/crates/assembly/core/src/agentic/tools/implementations/generated/theme_prompt_snapshots.json +++ b/src/crates/assembly/core/src/agentic/tools/implementations/generated/theme_prompt_snapshots.json @@ -132,7 +132,7 @@ "textMuted": "#787c99", "accent500": "#7aa2f7", "accent600": "#6183bb", - "borderBase": "rgba(54, 59, 84, 0.6)", + "borderBase": "rgba(51, 65, 85, 0.6)", "elementBase": "rgba(122, 162, 247, 0.11)", "radiusBase": "6px", "spacing4": "16px", diff --git a/src/crates/assembly/core/src/service/config/global.rs b/src/crates/assembly/core/src/service/config/global.rs index 162a91d15c..ddffab44c1 100644 --- a/src/crates/assembly/core/src/service/config/global.rs +++ b/src/crates/assembly/core/src/service/config/global.rs @@ -211,9 +211,10 @@ impl GlobalConfigManager { pub async fn update_theme(&self, theme_id: &str) -> BitFunResult<()> { let service = Self::get_service().await?; service.set_config("theme.id", theme_id).await?; + let stored_theme_id: String = service.get_config(Some("themes.current")).await?; Self::broadcast_update(ConfigUpdateEvent::ThemeUpdated { - theme_id: theme_id.to_string(), + theme_id: stored_theme_id, }) .await; diff --git a/src/crates/assembly/core/src/service/config/manager.rs b/src/crates/assembly/core/src/service/config/manager.rs index 49e26ab6e9..2e5a55e16d 100644 --- a/src/crates/assembly/core/src/service/config/manager.rs +++ b/src/crates/assembly/core/src/service/config/manager.rs @@ -21,10 +21,68 @@ fn canonical_config_path(path: &str) -> &str { match path { "ai.review_teams.rate_limit_status" => "ai.review_team_rate_limit_status", "ai.review_teams.project_strategy_overrides" => "ai.review_team_project_strategy_overrides", + "theme.id" => "themes.current", _ => path, } } +fn normalize_legacy_theme_id(theme_id: &str) -> String { + match theme_id.trim() { + "dark" => "bitfun-dark".to_string(), + "light" => "bitfun-light".to_string(), + normalized => normalized.to_string(), + } +} + +fn normalize_legacy_theme_value(value: Value) -> Value { + match value { + Value::String(theme_id) => Value::String(normalize_legacy_theme_id(&theme_id)), + value => value, + } +} + +pub(crate) fn normalize_legacy_theme_config_value(mut config: Value) -> Value { + let legacy_theme_id = config + .get("theme") + .and_then(|theme| theme.get("id")) + .and_then(Value::as_str) + .map(str::trim) + .filter(|theme_id| !theme_id.is_empty()) + .map(normalize_legacy_theme_id); + + let Some(config_object) = config.as_object_mut() else { + return config; + }; + + config_object.remove("theme"); + + let Some(legacy_theme_id) = legacy_theme_id else { + return config; + }; + + let themes_value = config_object + .entry("themes".to_string()) + .or_insert_with(|| serde_json::json!({})); + + if !themes_value.is_object() { + *themes_value = serde_json::json!({}); + } + + if let Some(themes_object) = themes_value.as_object_mut() { + let has_current = themes_object + .get("current") + .and_then(Value::as_str) + .map(str::trim) + .is_some_and(|current| !current.is_empty()); + + if !has_current { + themes_object.insert("current".to_string(), Value::String(legacy_theme_id)); + } + } + + config +} + /// Configuration manager. pub struct ConfigManager { config_dir: PathBuf, @@ -122,6 +180,9 @@ impl ConfigManager { let mut config_value: Value = serde_json::from_str(&content).map_err(|e| { BitFunError::config(format!("Failed to parse config file as JSON: {}", e)) })?; + let normalized_config_value = normalize_legacy_theme_config_value(config_value.clone()); + let legacy_theme_normalized = normalized_config_value != config_value; + config_value = normalized_config_value; let file_version = config_value .get("version") @@ -157,10 +218,10 @@ impl ConfigManager { self.config = config; - if needs_migration { + if needs_migration || legacy_theme_normalized { self.config.version = current_version; self.save_config().await?; - info!("Config migrated and saved"); + info!("Config normalized and saved"); } else { debug!("Loaded config from file"); } @@ -180,6 +241,7 @@ impl ConfigManager { /// Performs a smart merge from a JSON value. async fn smart_merge_config_from_value(&mut self, user_value: Value) -> BitFunResult<()> { + let user_value = normalize_legacy_theme_config_value(user_value); let base_config = self.providers.get_default_config(); let base_value = serde_json::to_value(&base_config).map_err(|e| { @@ -312,10 +374,14 @@ impl ConfigManager { T: serde::Serialize, { let old_config = self.config.clone(); - let json_value = serde_json::to_value(value) + let mut json_value = serde_json::to_value(value) .map_err(|e| BitFunError::config(format!("Failed to serialize config value: {}", e)))?; + let original_path = path; let path = canonical_config_path(path); + if original_path == "theme.id" { + json_value = normalize_legacy_theme_value(json_value); + } self.set_value_by_path(path, json_value)?; self.config.last_modified = chrono::Utc::now(); @@ -336,6 +402,7 @@ impl ConfigManager { let old_config = self.config.clone(); if let Some(path) = path { + let path = canonical_config_path(path); let default_config = self.providers.get_default_config(); let default_value = self.get_value_by_path_from_config(&default_config, path)?; self.set_value_by_path(path, default_value)?; @@ -346,6 +413,7 @@ impl ConfigManager { self.config.last_modified = chrono::Utc::now(); if let Some(path) = path { + let path = canonical_config_path(path); self.notify_config_changed(path, &old_config).await?; } else { for provider_name in self.providers.get_provider_names() { @@ -378,6 +446,7 @@ impl ConfigManager { /// Imports configuration. pub async fn import_config(&mut self, config_data: serde_json::Value) -> BitFunResult<()> { let old_config = self.config.clone(); + let config_data = normalize_legacy_theme_config_value(config_data); let imported_config: GlobalConfig = serde_json::from_value(config_data) .map_err(|e| BitFunError::config(format!("Failed to parse imported config: {}", e)))?; @@ -627,7 +696,7 @@ pub struct ConfigStatistics { #[cfg(test)] mod tests { - use super::canonical_config_path; + use super::{canonical_config_path, normalize_legacy_theme_config_value}; #[test] fn canonicalizes_legacy_review_team_auxiliary_paths() { @@ -643,6 +712,55 @@ mod tests { canonical_config_path("ai.review_teams.default"), "ai.review_teams.default" ); + assert_eq!(canonical_config_path("theme.id"), "themes.current"); + } + + #[test] + fn legacy_theme_id_moves_to_themes_current_when_missing() { + let normalized = normalize_legacy_theme_config_value(serde_json::json!({ + "theme": { + "id": "dark", + "colors": { + "background": "#1e1e1e" + } + } + })); + + assert_eq!(normalized["themes"]["current"], "bitfun-dark"); + assert!( + normalized.get("theme").is_none(), + "legacy GUI theme payload should not survive normalization" + ); + } + + #[test] + fn legacy_theme_id_does_not_override_existing_theme_selection() { + let normalized = normalize_legacy_theme_config_value(serde_json::json!({ + "theme": { + "id": "bitfun-dark" + }, + "themes": { + "current": "bitfun-cyber" + } + })); + + assert_eq!(normalized["themes"]["current"], "bitfun-cyber"); + assert!(normalized.get("theme").is_none()); + } + + #[test] + fn legacy_theme_id_fills_empty_existing_theme_selection() { + let normalized = normalize_legacy_theme_config_value(serde_json::json!({ + "theme": { + "id": "light" + }, + "themes": { + "current": "" + } + })); + + assert_eq!(normalized["themes"]["current"], "bitfun-light"); + assert!(normalized.get("theme").is_none()); } } diff --git a/src/crates/assembly/core/src/service/config/providers.rs b/src/crates/assembly/core/src/service/config/providers.rs index 4f2b46cfcd..03a04f7e36 100644 --- a/src/crates/assembly/core/src/service/config/providers.rs +++ b/src/crates/assembly/core/src/service/config/providers.rs @@ -172,73 +172,6 @@ impl ConfigProvider for AIConfigProvider { } } -/// Theme configuration provider (legacy, for a single theme). -pub struct ThemeConfigProvider; - -#[async_trait] -impl ConfigProvider for ThemeConfigProvider { - fn name(&self) -> &str { - "theme" - } - - fn get_default_config(&self) -> serde_json::Value { - serialize_default_config("theme", ThemeConfig::default()) - } - - async fn validate_config(&self, config: &serde_json::Value) -> BitFunResult> { - let warnings = Vec::new(); - - if let Ok(theme_config) = serde_json::from_value::(config.clone()) { - let colors = [ - &theme_config.colors.primary, - &theme_config.colors.secondary, - &theme_config.colors.background, - &theme_config.colors.surface, - &theme_config.colors.text, - &theme_config.colors.accent, - &theme_config.colors.success, - &theme_config.colors.warning, - &theme_config.colors.error, - ]; - - for color in colors { - if !color.starts_with("#") && !color.starts_with("rgb") && !color.starts_with("hsl") - { - return Err(BitFunError::validation(format!( - "Invalid color format: {}", - color - ))); - } - } - } else { - return Err(BitFunError::validation( - "Invalid theme config format".to_string(), - )); - } - - Ok(warnings) - } - - async fn on_config_changed( - &self, - _old_config: &serde_json::Value, - new_config: &serde_json::Value, - ) -> BitFunResult<()> { - if let Ok(theme_config) = serde_json::from_value::(new_config.clone()) { - info!("Theme config changed to: {}", theme_config.display_name); - } - Ok(()) - } - - async fn migrate_config( - &self, - _version: &str, - config: serde_json::Value, - ) -> BitFunResult { - Ok(config) - } -} - /// Theme system configuration provider (new, supports theme management). pub struct ThemesConfigProvider; @@ -550,7 +483,6 @@ impl ConfigProviderRegistry { }; registry.register(Box::new(AIConfigProvider)); - registry.register(Box::new(ThemeConfigProvider)); registry.register(Box::new(ThemesConfigProvider)); registry.register(Box::new(EditorConfigProvider)); registry.register(Box::new(TerminalConfigProvider)); @@ -645,7 +577,6 @@ impl ConfigProviderRegistry { ) -> BitFunResult { match section { "app" => Ok(serde_json::to_value(&config.app)?), - "theme" => Ok(serde_json::to_value(&config.theme)?), "themes" => Ok(serde_json::to_value(&config.themes)?), "editor" => Ok(serde_json::to_value(&config.editor)?), "terminal" => Ok(serde_json::to_value(&config.terminal)?), diff --git a/src/crates/assembly/core/src/service/config/service.rs b/src/crates/assembly/core/src/service/config/service.rs index 8d5afa2208..67309f5fa6 100644 --- a/src/crates/assembly/core/src/service/config/service.rs +++ b/src/crates/assembly/core/src/service/config/service.rs @@ -170,11 +170,19 @@ impl ConfigService { /// imported config that references missing / disabled models is brought /// back into a self-consistent state. pub async fn import_config(&self, export: ConfigExport) -> BitFunResult { + self.import_config_data(serde_json::to_value(export.config)?) + .await + } + + /// Imports raw configuration JSON. Keeping this boundary raw preserves + /// legacy fields that are intentionally normalized before deserialization. + pub async fn import_config_data( + &self, + config_data: serde_json::Value, + ) -> BitFunResult { let import_result = { let mut manager = self.manager.write().await; - manager - .import_config(serde_json::to_value(export.config)?) - .await + manager.import_config(config_data).await }; match import_result { @@ -703,4 +711,71 @@ mod tests { "vision default must not fall back to a text-only model" ); } + + #[tokio::test] + async fn legacy_theme_id_path_writes_themes_current_only() { + let (service, _dir) = test_service("legacy-theme-id-path").await; + + service + .set_config("theme.id", "dark") + .await + .expect("legacy theme path should remain a thin compatibility alias"); + + let current: String = service + .get_config(Some("themes.current")) + .await + .expect("theme selection should be readable from the TS-owned path"); + assert_eq!(current, "bitfun-dark"); + + let export: GlobalConfig = service + .get_config(None) + .await + .expect("full config should load"); + let serialized = serde_json::to_value(export).expect("config should serialize"); + assert!( + serialized.get("theme").is_none(), + "legacy path must not recreate the removed Rust GUI theme schema" + ); + } + + #[tokio::test] + async fn raw_import_preserves_legacy_theme_id_before_deserialization() { + let (service, _dir) = test_service("legacy-theme-raw-import").await; + let mut raw_config = + serde_json::to_value(GlobalConfig::default()).expect("default config should serialize"); + let raw_object = raw_config + .as_object_mut() + .expect("default config should serialize as an object"); + raw_object.remove("themes"); + raw_object.insert( + "theme".to_string(), + serde_json::json!({ + "id": "dark", + "colors": { + "background": "#1e1e1e" + } + }), + ); + + service + .import_config_data(raw_config) + .await + .expect("raw legacy config should import before old fields are dropped"); + + let current: String = service + .get_config(Some("themes.current")) + .await + .expect("legacy theme id should migrate into themes.current"); + assert_eq!(current, "bitfun-dark"); + + let export: GlobalConfig = service + .get_config(None) + .await + .expect("full config should load after import"); + let serialized = serde_json::to_value(export).expect("config should serialize"); + assert!( + serialized.get("theme").is_none(), + "legacy theme payload should not be exported after import" + ); + } } diff --git a/src/crates/assembly/core/src/service/config/types.rs b/src/crates/assembly/core/src/service/config/types.rs index 11569bafcf..b30eae0256 100644 --- a/src/crates/assembly/core/src/service/config/types.rs +++ b/src/crates/assembly/core/src/service/config/types.rs @@ -50,7 +50,6 @@ pub struct FlowChatFontSnapshot { #[serde(default)] pub struct GlobalConfig { pub app: AppConfig, - pub theme: ThemeConfig, pub editor: EditorConfig, pub terminal: TerminalConfig, pub workspace: WorkspaceConfig, @@ -255,86 +254,8 @@ pub struct NotificationConfig { pub enable_startup_tips: bool, } -/// Theme configuration. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(default)] -pub struct ThemeConfig { - pub id: String, - pub name: String, - pub display_name: String, - #[serde(rename = "type")] - pub theme_type: String, - pub colors: ThemeColors, - pub fonts: ThemeFonts, - pub spacing: ThemeSpacing, - pub border_radius: ThemeBorderRadius, - pub shadows: ThemeShadows, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(default)] -pub struct ThemeColors { - pub primary: String, - pub secondary: String, - pub background: String, - pub surface: String, - pub text: String, - pub text_secondary: String, - pub border: String, - pub accent: String, - pub success: String, - pub warning: String, - pub error: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(default)] -pub struct ThemeFonts { - pub primary: String, - pub code: String, - pub sizes: FontSizes, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(default)] -pub struct FontSizes { - pub xs: String, - pub sm: String, - pub base: String, - pub lg: String, - pub xl: String, - #[serde(rename = "2xl")] - pub xxl: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(default)] -pub struct ThemeSpacing { - pub xs: String, - pub sm: String, - pub md: String, - pub lg: String, - pub xl: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(default)] -pub struct ThemeBorderRadius { - pub sm: String, - pub md: String, - pub lg: String, - pub full: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(default)] -pub struct ThemeShadows { - pub sm: String, - pub md: String, - pub lg: String, -} - -/// Theme system configuration (new). +/// Theme system configuration. The full GUI theme contract is owned by TS/web-ui; +/// Rust stores only the selected theme id and opaque custom-theme payloads. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default)] pub struct ThemesConfig { @@ -395,6 +316,8 @@ pub struct TerminalConfig { pub cursor_blink: bool, pub cursor_style: String, pub scrollback: u32, + /// Terminal ANSI palette/defaults. This keeps the historical + /// `terminal.theme` config key but is not the GUI theme contract. pub theme: TerminalThemeConfig, } @@ -1323,7 +1246,6 @@ impl Default for GlobalConfig { fn default() -> Self { Self { app: AppConfig::default(), - theme: ThemeConfig::default(), editor: EditorConfig::default(), terminal: TerminalConfig::default(), workspace: WorkspaceConfig::default(), @@ -1407,97 +1329,6 @@ impl Default for AIExperienceConfig { } } -impl Default for ThemeConfig { - fn default() -> Self { - Self { - id: "dark".to_string(), - name: "dark".to_string(), - display_name: "Dark Theme".to_string(), - theme_type: "dark".to_string(), - colors: ThemeColors::default(), - fonts: ThemeFonts::default(), - spacing: ThemeSpacing::default(), - border_radius: ThemeBorderRadius::default(), - shadows: ThemeShadows::default(), - } - } -} - -impl Default for ThemeColors { - fn default() -> Self { - Self { - primary: "#007acc".to_string(), - secondary: "#6c757d".to_string(), - background: "#1e1e1e".to_string(), - surface: "#2d2d30".to_string(), - text: "#cccccc".to_string(), - text_secondary: "#969696".to_string(), - border: "#3e3e42".to_string(), - accent: "#007acc".to_string(), - success: "#28a745".to_string(), - warning: "#ffc107".to_string(), - error: "#dc3545".to_string(), - } - } -} - -impl Default for ThemeFonts { - fn default() -> Self { - Self { - primary: "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif" - .to_string(), - code: "Consolas, \"Courier New\", monospace".to_string(), - sizes: FontSizes::default(), - } - } -} - -impl Default for FontSizes { - fn default() -> Self { - Self { - xs: "0.75rem".to_string(), - sm: "0.875rem".to_string(), - base: "1rem".to_string(), - lg: "1.125rem".to_string(), - xl: "1.25rem".to_string(), - xxl: "1.5rem".to_string(), - } - } -} - -impl Default for ThemeSpacing { - fn default() -> Self { - Self { - xs: "0.25rem".to_string(), - sm: "0.5rem".to_string(), - md: "1rem".to_string(), - lg: "1.5rem".to_string(), - xl: "2rem".to_string(), - } - } -} - -impl Default for ThemeBorderRadius { - fn default() -> Self { - Self { - sm: "0.125rem".to_string(), - md: "0.25rem".to_string(), - lg: "0.5rem".to_string(), - full: "9999px".to_string(), - } - } -} - -impl Default for ThemeShadows { - fn default() -> Self { - Self { - sm: "0 1px 2px 0 rgba(0, 0, 0, 0.05)".to_string(), - md: "0 4px 6px -1px rgba(0, 0, 0, 0.1)".to_string(), - lg: "0 10px 15px -3px rgba(0, 0, 0, 0.1)".to_string(), - } - } -} - impl Default for EditorConfig { fn default() -> Self { Self { @@ -1896,6 +1727,21 @@ mod tests { assert_eq!(serialized["terminal"]["terminal_panel_position"], "bottom"); } + #[test] + fn global_config_serialization_omits_legacy_theme_section() { + let serialized = + serde_json::to_value(GlobalConfig::default()).expect("config should serialize"); + + assert!( + serialized.get("theme").is_none(), + "Rust config must not export the removed GUI theme schema" + ); + assert_eq!( + serialized["themes"]["current"], "bitfun-light", + "theme selection remains in the TS-owned themes contract" + ); + } + #[test] fn defaults_agent_companion_pet_to_bitfun() { let config: AIExperienceConfig = 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 8580026022..24a8088db1 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 @@ -52,7 +52,7 @@ export const bitfunChinaStyleTheme: ThemeConfig = { secondary: '#f5f3e8', tertiary: '#f0ede0', quaternary: '#ebe8d8', - elevated: '#ebe9e3', + elevated: '#f0ede0', workbench: CHINA_STYLE_PAPER, scene: CHINA_STYLE_PAPER, tooltip: chinaStylePaper(0.96), 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 fae0e016d7..87edadbb4e 100644 --- a/src/web-ui/src/infrastructure/theme/presets/light-theme.ts +++ b/src/web-ui/src/infrastructure/theme/presets/light-theme.ts @@ -54,7 +54,7 @@ export const bitfunLightTheme: ThemeConfig = { primary: '#f3f3f5', secondary: STATIC_WHITE, tertiary: '#e8e8e8', - quaternary: '#e0e3e8', + quaternary: '#e8e8e8', elevated: STATIC_WHITE, workbench: '#e8e8e8', scene: STATIC_WHITE, 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 7ab54c7260..cbb475d2be 100644 --- a/src/web-ui/src/infrastructure/theme/presets/slate-theme.ts +++ b/src/web-ui/src/infrastructure/theme/presets/slate-theme.ts @@ -25,7 +25,7 @@ const SLATE_BACKGROUND_PRIMARY = '#1c1c1f'; const SLATE_BACKGROUND_SECONDARY = '#262626'; const SLATE_TEXT_PRIMARY = '#e8e8e8'; const SLATE_TEXT_MUTED = '#a8b0bd'; -const SLATE_BUTTON_TEXT = '#e0e3e8'; +const SLATE_BUTTON_TEXT = SLATE_TEXT_PRIMARY; const SLATE_ACCENT_SOFT = '#e8e8e8'; const SLATE_ACCENT = '#94a3b8'; const SLATE_ACCENT_HOVER = '#64748b'; 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 a18eaf93ff..c37c83096e 100644 --- a/src/web-ui/src/infrastructure/theme/presets/themePresetOutput.test.ts +++ b/src/web-ui/src/infrastructure/theme/presets/themePresetOutput.test.ts @@ -149,12 +149,12 @@ describe('builtin theme preset output', () => { }))).toMatchInlineSnapshot(` [ { - "hash": "44f230d2565fcdfed0e47fc5ea7d3253fc0bdcf0550c6bdcde16fb18f580467a", + "hash": "146da4fe962477dfef3ebcf275aec128019dee60dd1fde16b22e995b7264e377", "id": "bitfun-light", "type": "light", }, { - "hash": "c1f6fb0895d7e08ab1b5b035ea2db83690ba5eb490ed28ea59d731e9524aaa73", + "hash": "9272e35f09967ad85bf5ad48eeabed079a0d8025f13342a1480aaa6a4d256dc3", "id": "bitfun-slate", "type": "dark", }, @@ -169,7 +169,7 @@ describe('builtin theme preset output', () => { "type": "dark", }, { - "hash": "53d25127d6bcb4c2ef2cbf2246b6a7d37e57cb17df7a3f517d5aedddaf73d6e4", + "hash": "a00254950833b446f6547c3753da862c3818e6801028dab905ec6939f078f86e", "id": "bitfun-china-style", "type": "light", }, @@ -184,7 +184,7 @@ describe('builtin theme preset output', () => { "type": "dark", }, { - "hash": "e79f3ef7f271262755657175f5f4f005d0a3c5e4f0c999b0e5e207b6a3bec6be", + "hash": "5b2b2064a339c59e93d0a3b4f6d29a1e2c0096f1828063bbb2e258e2d48f4cb3", "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 d4a5a0b5d5..28fd35ecc8 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 @@ -30,7 +30,7 @@ const TOKYO_SUCCESS = '#9ece6a'; const TOKYO_WARNING = '#e0af68'; const TOKYO_ERROR = '#f7768e'; const TOKYO_INFO = '#7dcfff'; -const TOKYO_BORDER = '#363b54'; +const TOKYO_BORDER = '#334155'; const TOKYO_SCROLLBAR = '#868bc4'; const TOKYO_GIT_ADDED = '#41a6b5'; const TOKYO_PRIMARY_BUTTON = '#3d59a1';