From ec85e2285e6a0929791864e8100b5ba1eaa4b671 Mon Sep 17 00:00:00 2001 From: limityan Date: Mon, 29 Jun 2026 18:14:46 +0800 Subject: [PATCH] refactor(web-ui): reduce theme color literal duplication --- docs/architecture/theme-token-optimization.md | 148 +++--- scripts/theme-color-governance-baseline.json | 22 +- .../src/component-library/styles/tokens.scss | 448 ++++++++++-------- .../infrastructure/theme/core/ThemeService.ts | 110 +++-- .../theme/presets/china-night-theme.ts | 342 +++++++------ .../theme/presets/china-style-theme.ts | 363 +++++++------- .../theme/presets/cyber-theme.ts | 338 +++++++------ .../theme/presets/dark-theme.ts | 270 ++++++----- .../theme/presets/light-theme.ts | 371 ++++++++------- .../theme/presets/midnight-theme.ts | 310 ++++++------ .../infrastructure/theme/presets/shared.ts | 37 ++ .../theme/presets/slate-theme.ts | 268 ++++++----- .../theme/presets/themePresetOutput.test.ts | 10 +- .../theme/presets/tokyo-night-theme.ts | 283 ++++++----- .../tools/mermaid-editor/theme/_tokens.scss | 52 +- .../theme/mermaidThemeFallbacks.ts | 129 ++--- 16 files changed, 1957 insertions(+), 1544 deletions(-) diff --git a/docs/architecture/theme-token-optimization.md b/docs/architecture/theme-token-optimization.md index 2ce3d85fbf..da3d51820d 100644 --- a/docs/architecture/theme-token-optimization.md +++ b/docs/architecture/theme-token-optimization.md @@ -51,6 +51,48 @@ - 对每个页面做像素级重设计。 - 在所有调用方迁移完成前移除兼容别名。 +## 主题扩展职责边界 + +主题扩展必须把“持久化/首屏引导”和“完整渲染契约”拆开维护,避免 Rust 与 +TS 各自增长一套主题定义。参考经验: + +- [OpenCode theme](https://opencode.ai/docs/themes/) 采用可扩展 JSON schema、内置/用户/项目多层加载、 + 可复用色值定义和 light/dark variant;扩展点贴近 TUI 渲染层,host 只负责加载顺序和选择。 +- [Claude Code theme](https://code.claude.com/docs/en/terminal-config) 通过 `/theme`、theme picker 和 auto + light/dark 匹配终端背景,同时明确不接管终端自身配色;host 只适配环境,不复制完整 palette。 +- [Codex config](https://developers.openai.com/codex/config-basic) 使用分层 config 和明确优先级; + 对 BitFun 的启发是:Rust/host 可以读取偏好与可信配置层,但不应成为 UI token 语义 owner。 + +当前职责边界如下: + +| 事项 | 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。 | +| 首屏 bootstrap | 只注入 WebView 首屏所需最小投影:`data-theme`、`data-theme-type`、核心背景/文本 CSS 变量和 `--bitfun-startup-bg`。 | JS 启动后必须重新应用完整主题,覆盖 bootstrap 投影并恢复 Monaco、Mermaid、terminal、widget payload 等专用域。 | +| 内置主题扩展 | 不手写新增完整 palette。只有新内置主题需要首屏无闪烁时,才更新最小 bootstrap 投影。 | 新主题先进入 TS 预设和 `builtinThemes`;所有语义、组件和专用域 token 以 TS 侧为准。 | +| custom theme | 不解析 `themes.custom`,不复制 custom schema;保存的 custom id 在 Rust 启动阶段不可用时使用系统/默认首屏回退。 | 加载、校验、注册、注销、导入导出 custom theme;custom 加载完成后覆盖 Rust 首屏回退。 | +| CSS 变量和 key 命名 | 只允许新增明确写入启动主题投影 manifest 的首屏 key;不得新增 backend theme service 或平行 alias 表。 | 新 primitive/semantic/component key、兼容 alias、surface rename、dynamic family 和 widget payload key 均在 TS contract/audit 中登记。 | +| 专用渲染域 | 不维护 Monaco、terminal ANSI、Mermaid、Prism、language identity、UI exception 等色板。 | 专用域由各自 TS owner 维护,按 `colorDomainScopes.*` 和 `colorDomainNearPairs.*` 预算治理。 | + +当前 `src/apps/desktop/src/theme.rs::get_builtin_theme` 仍手写了一份内置主题的最小首屏投影, +这是受控技术债,不是新的主题扩展入口。后续只要触碰 Rust 启动主题逻辑,应优先把该投影改为 +由 TS 主题预设生成的 `startup-theme-bootstrap` manifest,再由 Rust `include_str!` 或构建产物读取。 +在生成链路落地前,Rust 侧最多维护 `id`、`selection_id`、`bg_primary`、`bg_secondary`、`bg_scene`、 +`is_light`、`text_primary`、`text_muted`、`accent_color` 这类首屏字段;不得复制完整颜色表。 + +新增或扩展主题时按以下顺序执行: + +1. 在 TS 侧更新 `ThemeConfig`、主题预设、validator 和 runtime 注入;需要外部 iframe 读取时同步 + `themePayload` allowlist 与 CSS var contract。 +2. 对新增色值先走 token 预算:可复用则复用,肉眼不可区分才直接合并,有独立语义才新增 + semantic/component token,专用域进入 exception namespace。 +3. 只有影响 JS 加载前首屏的字段,才更新 Rust bootstrap 投影;不能为了 Rust 读取方便新增第二套主题 schema。 +4. custom theme 不要求 Rust 首屏精确还原。若后续要优化 custom theme 首屏体验,也必须由 TS schema 生成 + 最小 bootstrap cache,而不是让 Rust 直接解析 custom theme。 +5. 变更必须通过主题审计、CSS var contract 和必要的 focused visual review;跨 desktop/web/mobile 或 + light/dark/system 形态的行为由 TS 运行时统一验证。 + ## 当前现状 基于当前审计口径,普通 @@ -70,8 +112,8 @@ registry。 | 忽略的测试文件数 | 221 | | 忽略的构建生成文件数 | 0 | | 包含颜色字面量的文件数 | 25 | -| 颜色字面量出现次数 | 1702 | -| 唯一颜色字面量数量 | 912 | +| 颜色字面量出现次数 | 780 | +| 唯一颜色字面量数量 | 560 | | 组件或非 token 文件中的颜色出现次数 | 0 | | 组件或非 token 唯一颜色数量 | 0 | | App UI 颜色出现次数 | 0 | @@ -82,8 +124,8 @@ registry。 | token-equivalent app literal 唯一颜色数量 | 0 | | 普通组件肉眼不可区分 near color pair | 0 | | 普通组件需证据复核的 near color pair | 0 | -| 专用域肉眼不可区分 near color pair | 35 | -| 专用域需证据复核的 near color pair | 644 | +| 专用域肉眼不可区分 near color pair | 6 | +| 专用域需证据复核的 near color pair | 377 | 当前审计未发现 CSS 变量契约层面的硬错误: @@ -132,11 +174,11 @@ registry。 | 区域 | 当前出现次数 | 当前唯一色数 | 说明 | | --- | ---: | ---: | --- | -| Theme presets | 1019 | 611 | 主题个性与 palette 映射,不作为普通 app literal 直接合并;git staged 仅在与 added 视觉语义相同时由 preset helper 复用 | -| Token contracts | 267 | 159 | `tokens.scss` 等静态契约根 | +| 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 | 139 | 95 | Mermaid 专用渲染域 | -| Theme runtime | 54 | 45 | `ThemeService.ts` 运行时注入 | +| Mermaid | 105 | 95 | Mermaid 专用渲染域 | +| Theme runtime | 45 | 45 | `ThemeService.ts` 运行时注入 | | Language identity | 52 | 50 | 语言身份色,已集中到 identity registry | | Terminal | 37 | 29 | terminal/ANSI 专用域;工具命令空状态已复用 `--tool-command-empty-rgb`,不再保留独立 raw 色 | | Boundary fallback | 22 | 22 | iframe/miniapp/截图兜底值,不作为普通 app token | @@ -152,9 +194,9 @@ theme preset 或 boundary fallback 误算成普通 app UI 债务。当前队列 | 专用域 | 肉眼不可区分 pair | 需证据复核 pair | 后续处理原则 | | --- | ---: | ---: | --- | -| Theme presets | 30 | 459 | 优先处理同一 theme 内 hex/rgb 完全等价和同 alpha 阶重复;保留主题个性 | +| Theme presets | 3 | 192 | 优先处理同一 theme 内 hex/rgb 完全等价和同 alpha 阶重复;保留主题个性 | | Theme runtime | 0 | 50 | 先与静态 token 和 payload contract 对齐,避免 early render 或 system theme 回退 | -| Token contracts | 2 | 90 | 优先 alias 精确等价值;状态、层级和 alpha ramp 不按数值强合并 | +| Token contracts | 0 | 90 | 优先 alias 精确等价值;状态、层级和 alpha ramp 不按数值强合并 | | Boundary fallback | 0 | 6 | 只在 iframe/截图 first paint 验证后收缩 | | Mermaid | 3 | 32 | 需检查节点、边、文本、错误态和 light/dark Mermaid 输出 | | Editor | 0 | 7 | 需检查 Monaco selection、diff、inline highlight 和 light/dark editor 可读性 | @@ -164,21 +206,14 @@ theme preset 或 boundary fallback 误算成普通 app UI 债务。当前队列 | 文件 | 颜色出现次数 | 后续处理策略 | | --- | ---: | --- | -| `src/web-ui/src/component-library/styles/tokens.scss` | 242 | 根 token 契约;优先处理同语义 alias,避免把状态/层级 ramp 按数值强合并 | -| `src/web-ui/src/infrastructure/theme/presets/cyber-theme.ts` | 145 | theme preset palette;保留主题个性,优先继续收敛同一结构下的机械重复 | -| `src/web-ui/src/infrastructure/theme/presets/tokyo-night-theme.ts` | 145 | theme preset palette;Tokyo git staged 与 added 语义不同,保留显式覆盖 | -| `src/web-ui/src/infrastructure/theme/presets/china-style-theme.ts` | 128 | theme preset palette;保留主题个性,优先继续收敛同一结构下的机械重复 | -| `src/web-ui/src/infrastructure/theme/presets/light-theme.ts` | 128 | theme preset palette;保留主题个性,优先继续收敛同一结构下的机械重复 | -| `src/web-ui/src/infrastructure/theme/presets/china-night-theme.ts` | 122 | theme preset palette;保留主题个性,优先继续收敛同一结构下的机械重复 | -| `src/web-ui/src/infrastructure/theme/presets/midnight-theme.ts` | 120 | theme preset palette;保留主题个性,优先继续收敛同一结构下的机械重复 | -| `src/web-ui/src/infrastructure/theme/presets/dark-theme.ts` | 109 | theme preset palette;保留主题个性,优先继续收敛同一结构下的机械重复 | -| `src/web-ui/src/infrastructure/theme/presets/slate-theme.ts` | 109 | theme preset palette;保留主题个性,优先继续收敛同一结构下的机械重复 | -| `src/web-ui/src/tools/mermaid-editor/theme/mermaidThemeFallbacks.ts` | 106 | Mermaid 专用渲染兜底;需以节点、边、文本、错误态截图为依据 | -| `src/web-ui/src/infrastructure/theme/core/ThemeService.ts` | 54 | 运行时注入;需保持 early render、system theme 和 payload 导出兼容 | +| `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/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 导出兼容 | | `src/web-ui/src/shared/theme/uiExceptionAccents.ts` | 38 | 固定 UI 身份/角色色 registry;新增必须说明 owner/role | | `src/web-ui/src/tools/terminal/utils/xtermTheme.ts` | 36 | terminal ANSI palette;不与 app semantic color 合并 | +| `src/web-ui/src/infrastructure/theme/presets/slate-theme.ts` | 35 | theme preset palette;保留主题个性和 alpha ramp 边界 | 组件级 `var(--token, fallback)` 已收敛到 0;原先的 7 个 fallback token 不再需要 fallback contract registry 保留。 @@ -195,17 +230,16 @@ fallback 收敛决策表: | `--primary-color` | 改为明确的 tool-card accent token | `--primary-color` 是历史 tool card 局部入口,不提升为全局 app token;BaseToolCard 现在通过 `--base-tool-card-accent-color` 映射到 `--markdown-primary-color` | 产品代码不再定义或读取旧 key,回流由 `surfaceTokenRenames` 拦截 | | `--scene-viewport-border-width` | 上移默认值 | 静态 token 提供 `1px`,ThemeService 继续按主题 layout 覆盖为 `1px` 或 `0` | 移除 viewport border fallback | -阶段状态: +稳定里程碑: -| 阶段 | 状态 | 当前判断 | -| --- | --- | --- | -| Phase 0:基线与工具 | 已完成主体 | 审计脚本可区分测试文件、fallback token、dynamic families 和 exception domains | -| Phase 1:canonical token 契约 | 已完成调用方迁移 | compatibility alias registry 已记录 64 个显式 alias 和 2 个 alias family;内部 `var()` 读取已清零;widget payload 的 64 个显式 alias 和 17 个 family key 作为外部兼容面单独预算,并补齐 `--size-radius-2xl` / `--size-radius-full` canonical 导出 | -| Phase 2:精确重复合并 | 已完成主体 | token-equivalent app literal 已清零;截图兜底、language identity 和 review/agent/insights 固定色已迁入显式 registry;`tokens.scss` 中同域同语义的 deep-review consent 精确重复已改为 alias | -| Phase 3:legacy fallback 迁移 | 已完成主体 | 组件级 `var(--token, fallback)` 已清零,baseline 降到 0;新增 fallback 会被审计报告和 baseline 拦截 | -| Phase 4:组件 token 抽取 | 已完成主体 | CodeEditor、StreamText、ChatInputPixelPet、ReferencesPanel、AgentCompanion、tool-card、editor、generative-widget 组件装饰色已抽为组件 token 或复用 contract token;Flow Chat 局部动态 key 已改为 surface namespace | -| Phase 5:近似色合并 | 已完成主体并进入专用域证据队列 | 普通组件 near pair 已清零;极近似视觉色只在不相邻或不承担状态差异时合并,Monaco/terminal/Mermaid/syntax/theme preset 等专用 palette 通过 `colorDomainNearPairs.*` 排队复核 | -| Phase 6:防回退约束 | 已强化 | baseline 已同步到 component/non-token=0、appUi=0、token-equivalent=0、ordinary nearPair=0、compatibility alias 读取=0、fallback=0、surface rename debt=0,并保留 generated widget payload、external-only compatibility、domain contract 和专用域 near-pair 防回退指标 | +| 里程碑 | 稳定要求 | +| --- | --- | +| 基线与工具 | 审计脚本必须持续区分测试文件、fallback token、dynamic family、exception domain 和 generated widget 外部兼容面。 | +| canonical token 契约 | 内部调用方只读 canonical token;compatibility alias 仅作为显式外部兼容定义保留,新增或删除都必须进入 registry。 | +| fallback 收敛 | 普通组件不得新增 `var(--token, fallback)` 色值;确需边界兜底时先证明 owner、reason 和 boundary。 | +| 组件 token 抽取 | 复杂 surface 只能在 semantic token 不足以表达契约时新增 component token,并保持 surface namespace。 | +| 近似色治理 | 普通组件 near pair 维持 0;专用域 near pair 只作为证据队列,不能按数值相似自动合并。 | +| 防回退约束 | baseline 必须继续拦截 raw app color、token-equivalent literal、内部 alias 读取、未定义 key、stale registry 和 payload 缺失 canonical。 | Phase 5 决策记录: @@ -220,6 +254,7 @@ Phase 5 决策记录: | `#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 色,非错误/警告/状态强度 | | `#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 内部近似色 | | 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 | @@ -230,8 +265,8 @@ Phase 6 防回退约束: | --- | ---: | ---: | --- | | `nearPairs.indistinguishableTotal` | 0 | 0 | 阻止新增普通组件肉眼不可区分 pair 未被合并或记录 | | `nearPairs.nearTotal` | 0 | 0 | 阻止新增普通组件 near color 债务;新增必须合并、归类或记录理由 | -| `colorDomainNearPairs.indistinguishableTotal` | 35 | 35 | 控制专用域肉眼不可区分 pair 不继续增长,后续只能逐步降低或补充证据 | -| `colorDomainNearPairs.nearTotal` | 644 | 644 | 控制 theme preset/runtime/token/editor/Mermaid 等专用域 near 队列规模 | +| `colorDomainNearPairs.indistinguishableTotal` | 6 | 6 | 控制专用域肉眼不可区分 pair 不继续增长,后续只能逐步降低或补充证据 | +| `colorDomainNearPairs.nearTotal` | 377 | 377 | 控制 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 | @@ -288,7 +323,8 @@ editor、syntax、terminal、language identity、boundary fallback 等专用域 `:root` CSS 变量。 - `src/web-ui/src/infrastructure/theme/core/ThemeService.ts` 根据当前主题在运行时注入 CSS 变量, 同时补充了一批 app 级别别名和覆盖值。 -- `src/web-ui/src/theme/presets/*.ts` 定义主题预设色板。 +- `src/web-ui/src/infrastructure/theme/presets/*.ts` 定义完整主题预设色板。 +- `src/apps/desktop/src/theme.rs` 只维护 WebView 首屏最小 bootstrap 投影,不能扩展为完整主题 schema。 - `src/web-ui/src/tools/generative-widget/themePayload.ts` 向 generative widget payload 暴露部分主题变量。 - 组件 SCSS/CSS/TSX 中存在大量局部颜色字面量和局部 fallback。 @@ -298,6 +334,7 @@ editor、syntax、terminal、language identity、boundary fallback 等专用域 - 静态 token 和运行时 token 没有共享单一注册表。 - 有些 token 只由 `ThemeService.ts` 动态注入,但组件 fallback 假设它们 在所有渲染边界都存在。 +- Rust 启动投影和 TS 主题预设仍有最小色值重复,后续应改为由 TS 生成 manifest。 - 同一个语义角色存在多种历史命名方式。 - 组件 fallback 中的字面量过多,导致 fallback 变成实际上的第二套色板。 - 当前主题验证链路不能作为充分的可访问性证据,contrast 计算需要真实实现 @@ -874,33 +911,22 @@ alpha 差异经常承担 elevation 和交互状态,不应全部压成一个值 - 高风险 surface 是否有截图或 focused visual check。 - PR 描述是否说明了任何用户可见视觉变化。 -## 建议 PR 拆分 - -建议按证据和 surface 拆分,避免一次性大迁移: - -已完成的治理批次覆盖前 9 项,并把第 10/11 项所需的专用域 near 队列、 -widget payload external-only 兼容面和 raw color 防回退指标纳入审计预算: - -1. 审计工具和 baseline report。 -2. canonical token map 与 compatibility alias。 -3. 静态和运行时 token 对齐。 -4. token 文件中的精确重复合并。 -5. component-library fallback 迁移。 -6. Flow Chat surface 迁移。 -7. tool card 和 review panel 迁移。 -8. git/diff surface 迁移。 -9. widget/editor/terminal namespace 清理。 -10. 带截图的近似色合并批次。 -11. 新增 raw app color 的防回退约束。 - -每个 PR 应包含: - -- 范围和影响 surface。 -- before/after 指标。 -- 用户可见 surface 的截图。 -- 命中的 visual governance surface 和对应证据类型。 -- 明确保留的近似色列表。 -- 验证命令和结果。 +## 后续收敛顺序 + +后续不再按历史阶段拆零碎 PR,而是围绕能继续降低色值数量和降低扩展歧义的 +大块工作推进: + +1. 生成 Rust 启动主题投影:由 TS 内置主题预设导出 `startup-theme-bootstrap` manifest, + 替代 `theme.rs` 中手写的最小色值表,消除前后端重复定义。 +2. 收缩 generated widget 外部兼容面:保留仍有消费风险的 legacy key,移除无读取证据的 + external-only key,并用 payload contract 防止遗漏 canonical。 +3. 专用域近似色复核:优先处理同一 theme、同一专用域、同一语义且用户不可区分的 near pair; + Monaco、Mermaid、terminal、syntax 的相邻状态色必须保留或提供截图证据后再合并。 +4. 自定义主题扩展硬化:把 custom theme 校验、import/export 和 preview 回退都绑定到 TS schema; + 如需改善首屏体验,只允许生成最小 bootstrap cache,不允许 Rust 直接拥有 custom theme schema。 + +每个 PR 应包含范围、影响 surface、before/after 指标、命中的 visual governance surface、 +明确保留的近似色列表,以及验证命令和结果。 ## 已审定兼容策略 diff --git a/scripts/theme-color-governance-baseline.json b/scripts/theme-color-governance-baseline.json index ac4570f6da..a47645918b 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": 697 + "max": 340 }, "colorScopes.exception.uniqueColors": { "max": 273 @@ -147,16 +147,16 @@ "max": 0 }, "colorDomainNearPairs.indistinguishableTotal": { - "max": 35 + "max": 6 }, "colorDomainNearPairs.nearTotal": { - "max": 644 + "max": 377 }, "colorDomainNearPairs.themePreset.indistinguishableTotal": { - "max": 30 + "max": 3 }, "colorDomainNearPairs.themePreset.nearTotal": { - "max": 459 + "max": 192 }, "colorDomainNearPairs.themeRuntime.indistinguishableTotal": { "max": 0 @@ -165,7 +165,7 @@ "max": 50 }, "colorDomainNearPairs.tokenContract.indistinguishableTotal": { - "max": 2 + "max": 0 }, "colorDomainNearPairs.tokenContract.nearTotal": { "max": 90 @@ -237,16 +237,16 @@ "max": 0 }, "colorDomainScopes.themePreset.occurrences": { - "max": 1019 + "max": 231 }, "colorDomainScopes.themePreset.uniqueColors": { - "max": 611 + "max": 205 }, "colorDomainScopes.themeRuntime.occurrences": { - "max": 54 + "max": 45 }, "colorDomainScopes.tokenContract.occurrences": { - "max": 267 + "max": 176 }, "colorDomainScopes.generatedWidget.occurrences": { "max": 0 @@ -294,7 +294,7 @@ "max": 0 }, "colorDomainScopes.mermaid.occurrences": { - "max": 139 + "max": 105 }, "colorDomainScopes.mermaid.uniqueColors": { "max": 95 diff --git a/src/web-ui/src/component-library/styles/tokens.scss b/src/web-ui/src/component-library/styles/tokens.scss index 0d48125ea1..73671bb49c 100644 --- a/src/web-ui/src/component-library/styles/tokens.scss +++ b/src/web-ui/src/component-library/styles/tokens.scss @@ -1,16 +1,80 @@ /** * Component library design tokens (SCSS) */ +// ==================== Primitive color literals ==================== +$color-static-white: #ffffff; +$color-static-black: #000000; +$color-static-emerald: #10b981; +$color-static-orange: #f97316; + +$overlay-white-01: rgba(255, 255, 255, 0.01); +$overlay-white-02: rgba(255, 255, 255, 0.02); +$overlay-white-03: rgba(255, 255, 255, 0.03); +$overlay-white-04: rgba(255, 255, 255, 0.04); +$overlay-white-05: rgba(255, 255, 255, 0.05); +$overlay-white-06: rgba(255, 255, 255, 0.06); +$overlay-white-08: rgba(255, 255, 255, 0.08); +$overlay-white-09: rgba(255, 255, 255, 0.09); +$overlay-white-10: rgba(255, 255, 255, 0.1); +$overlay-white-12: rgba(255, 255, 255, 0.12); +$overlay-white-15: rgba(255, 255, 255, 0.15); +$overlay-white-18: rgba(255, 255, 255, 0.18); +$overlay-white-20: rgba(255, 255, 255, 0.2); +$overlay-white-22: rgba(255, 255, 255, 0.22); +$overlay-white-24: rgba(255, 255, 255, 0.24); +$overlay-white-25: rgba(255, 255, 255, 0.25); +$overlay-white-32: rgba(255, 255, 255, 0.32); +$overlay-white-40: rgba(255, 255, 255, 0.4); +$overlay-white-60: rgba(255, 255, 255, 0.6); + +$overlay-black-06: rgba(0, 0, 0, 0.06); +$overlay-black-08: rgba(0, 0, 0, 0.08); +$overlay-black-10: rgba(0, 0, 0, 0.1); +$overlay-black-12: rgba(0, 0, 0, 0.12); +$overlay-black-15: rgba(0, 0, 0, 0.15); +$overlay-black-20: rgba(0, 0, 0, 0.2); +$overlay-black-25: rgba(0, 0, 0, 0.25); +$overlay-black-30: rgba(0, 0, 0, 0.3); +$overlay-black-40: rgba(0, 0, 0, 0.4); +$overlay-black-50: rgba(0, 0, 0, 0.5); +$overlay-black-60: rgba(0, 0, 0, 0.6); +$overlay-black-70: rgba(0, 0, 0, 0.7); +$overlay-black-80: rgba(0, 0, 0, 0.8); +$overlay-black-85: rgba(0, 0, 0, 0.85); +$overlay-black-90: rgba(0, 0, 0, 0.9); + +$color-blue-500-a06: rgba(96, 165, 250, 0.06); +$color-blue-500-a08: rgba(96, 165, 250, 0.08); +$color-blue-500-a12: rgba(96, 165, 250, 0.12); +$color-blue-500-a15: rgba(96, 165, 250, 0.15); +$color-blue-500-a18: rgba(96, 165, 250, 0.18); +$color-blue-600-a08: rgba(59, 130, 246, 0.08); +$color-blue-600-a12: rgba(59, 130, 246, 0.12); +$color-blue-600-a15: rgba(59, 130, 246, 0.15); +$color-blue-600-a18: rgba(59, 130, 246, 0.18); +$color-blue-600-a25: rgba(59, 130, 246, 0.25); +$color-blue-600-a30: rgba(59, 130, 246, 0.3); +$color-blue-600-a40: rgba(59, 130, 246, 0.4); + +$color-purple-500-a05: rgba(139, 92, 246, 0.05); +$color-purple-500-a08: rgba(139, 92, 246, 0.08); +$color-purple-500-a10: rgba(139, 92, 246, 0.1); +$color-purple-500-a12: rgba(139, 92, 246, 0.12); +$color-purple-500-a15: rgba(139, 92, 246, 0.15); +$color-purple-500-a25: rgba(139, 92, 246, 0.25); +$color-purple-500-a30: rgba(139, 92, 246, 0.3); +$color-purple-500-a40: rgba(139, 92, 246, 0.4); + // ==================== Base color system ==================== $color-bg-primary: #0e0e10; $color-bg-secondary: #1c1c1f; -$color-bg-tertiary: #0e0e10; +$color-bg-tertiary: $color-bg-primary; $color-bg-quaternary: #252528; -$color-bg-elevated: #1c1c1f; -$color-bg-workbench: #0e0e10; -$color-bg-flowchat: #0e0e10; -$color-bg-scene: #1c1c1f; +$color-bg-elevated: $color-bg-secondary; +$color-bg-workbench: $color-bg-primary; +$color-bg-flowchat: $color-bg-primary; +$color-bg-scene: $color-bg-secondary; $color-bg-tooltip: rgba(28, 28, 31, 0.96); $color-text-primary: #e8e8e8; @@ -19,7 +83,7 @@ $color-text-muted: #858585; $color-text-disabled: #555555; // ==================== Neutral surface system ==================== -$element-bg-subtle: rgba(255, 255, 255, 0.05); +$element-bg-subtle: $overlay-white-05; $element-bg-soft: rgba(255, 255, 255, 0.07); $element-bg-base: rgba(255, 255, 255, 0.095); $element-bg-medium: rgba(255, 255, 255, 0.125); @@ -28,7 +92,7 @@ $element-bg-elevated: rgba(255, 255, 255, 0.19); // ==================== Blue system ==================== $color-accent-50: rgba(96, 165, 250, 0.04); -$color-accent-100: rgba(96, 165, 250, 0.08); +$color-accent-100: $color-blue-500-a08; $color-accent-200: rgba(96, 165, 250, 0.15); $color-accent-300: rgba(96, 165, 250, 0.25); $color-accent-400: rgba(96, 165, 250, 0.4); @@ -39,10 +103,10 @@ $color-accent-800: rgba(59, 130, 246, 0.9); // ==================== Purple system ==================== $color-purple-50: rgba(139, 92, 246, 0.04); -$color-purple-100: rgba(139, 92, 246, 0.08); -$color-purple-200: rgba(139, 92, 246, 0.15); -$color-purple-300: rgba(139, 92, 246, 0.25); -$color-purple-400: rgba(139, 92, 246, 0.4); +$color-purple-100: $color-purple-500-a08; +$color-purple-200: $color-purple-500-a15; +$color-purple-300: $color-purple-500-a25; +$color-purple-400: $color-purple-500-a40; $color-purple-500: #8b5cf6; $color-purple-600: #7c3aed; $color-purple-700: rgba(124, 58, 237, 0.8); @@ -62,16 +126,16 @@ $color-error-bg: rgba(239, 68, 68, 0.1); $color-error-border: rgba(239, 68, 68, 0.3); $color-info: #a1a1aa; -$color-info-bg: rgba(255, 255, 255, 0.08); -$color-info-border: rgba(255, 255, 255, 0.22); +$color-info-bg: $overlay-white-08; +$color-info-border: $overlay-white-22; // ==================== Git-specific color system ==================== $git-color-branch: #a1a1aa; -$git-color-branch-bg: rgba(255, 255, 255, 0.06); -$git-color-branch-bg-hover: rgba(255, 255, 255, 0.12); -$git-color-branch-border: rgba(255, 255, 255, 0.18); +$git-color-branch-bg: $overlay-white-06; +$git-color-branch-bg-hover: $overlay-white-12; +$git-color-branch-border: $overlay-white-18; -$git-color-changes: rgb(245, 158, 11); +$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-changes-border: rgba(245, 158, 11, 0.3); @@ -85,7 +149,7 @@ $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: rgb(239, 68, 68); +$git-color-deleted: $color-error; $git-color-deleted-bg: rgba(239, 68, 68, 0.1); $git-color-deleted-bg-hover: rgba(239, 68, 68, 0.15); $git-color-deleted-border: rgba(239, 68, 68, 0.3); @@ -99,63 +163,63 @@ $git-color-push-bg: $git-color-staged-bg; $git-color-push-bg-hover: $git-color-staged-bg-hover; // ==================== Border system ==================== -$border-subtle: rgba(255, 255, 255, 0.12); -$border-base: rgba(255, 255, 255, 0.18); -$border-medium: rgba(255, 255, 255, 0.24); -$border-strong: rgba(255, 255, 255, 0.32); -$border-prominent: rgba(255, 255, 255, 0.4); +$border-subtle: $overlay-white-12; +$border-base: $overlay-white-18; +$border-medium: $overlay-white-24; +$border-strong: $overlay-white-32; +$border-prominent: $overlay-white-40; $border-hover: $border-medium; $border-focus: $border-strong; -$border-accent-soft: rgba(59, 130, 246, 0.3); -$border-accent-subtle: rgba(59, 130, 246, 0.15); -$border-accent: rgba(59, 130, 246, 0.4); +$border-accent-soft: $color-blue-600-a30; +$border-accent-subtle: $color-blue-600-a15; +$border-accent: $color-blue-600-a40; $border-accent-strong: rgba(59, 130, 246, 0.6); -$border-purple-subtle: rgba(139, 92, 246, 0.15); -$border-purple: rgba(139, 92, 246, 0.4); +$border-purple-subtle: $color-purple-500-a15; +$border-purple: $color-purple-500-a40; // ==================== Shadow system ==================== -$shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.9); -$shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.8); -$shadow-base: 0 4px 8px rgba(0, 0, 0, 0.7); -$shadow-lg: 0 8px 16px rgba(0, 0, 0, 0.6); -$shadow-xl: 0 12px 24px rgba(0, 0, 0, 0.5); -$shadow-2xl: 0 16px 32px rgba(0, 0, 0, 0.4); - -$button-shadow-base: 0 2px 8px rgba(96, 165, 250, 0.08); -$button-shadow-hover: 0 4px 12px rgba(96, 165, 250, 0.12); -$button-shadow-active: 0 1px 4px rgba(96, 165, 250, 0.06); - -$glow-shadow-blue: - 0 12px 32px rgba(59, 130, 246, 0.25), - 0 6px 16px rgba(96, 165, 250, 0.18), - 0 3px 8px rgba(0, 0, 0, 0.1); - -$glow-shadow-purple: - 0 12px 32px rgba(139, 92, 246, 0.25), +$shadow-xs: 0 1px 2px $overlay-black-90; +$shadow-sm: 0 2px 4px $overlay-black-80; +$shadow-base: 0 4px 8px $overlay-black-70; +$shadow-lg: 0 8px 16px $overlay-black-60; +$shadow-xl: 0 12px 24px $overlay-black-50; +$shadow-2xl: 0 16px 32px $overlay-black-40; + +$button-shadow-base: 0 2px 8px $color-blue-500-a08; +$button-shadow-hover: 0 4px 12px $color-blue-500-a12; +$button-shadow-active: 0 1px 4px $color-blue-500-a06; + +$glow-shadow-blue: + 0 12px 32px $color-blue-600-a25, + 0 6px 16px $color-blue-500-a18, + 0 3px 8px $overlay-black-10; + +$glow-shadow-purple: + 0 12px 32px $color-purple-500-a25, 0 6px 16px rgba(124, 58, 237, 0.18), - 0 3px 8px rgba(0, 0, 0, 0.1); + 0 3px 8px $overlay-black-10; $glow-shadow-red: 0 8px 20px rgba(239, 68, 68, 0.2), - 0 4px 10px rgba(0, 0, 0, 0.08), - inset 0 1px 0 rgba(255, 255, 255, 0.1); + 0 4px 10px $overlay-black-08, + inset 0 1px 0 $overlay-white-10; $glow-shadow-green: 0 8px 20px rgba(52, 211, 153, 0.2), - 0 4px 10px rgba(0, 0, 0, 0.08), - inset 0 1px 0 rgba(255, 255, 255, 0.1); + 0 4px 10px $overlay-black-08, + inset 0 1px 0 $overlay-white-10; -$glow-shadow-mixed: +$glow-shadow-mixed: 0 12px 32px rgba(96, 165, 250, 0.2), - 0 6px 16px rgba(139, 92, 246, 0.15), - 0 3px 8px rgba(0, 0, 0, 0.1); + 0 6px 16px $color-purple-500-a15, + 0 3px 8px $overlay-black-10; -$inner-glow-top: inset 0 1px 0 rgba(255, 255, 255, 0.08); -$inner-glow-top-hover: inset 0 1px 0 rgba(255, 255, 255, 0.25); -$inner-glow-bottom: inset 0 -1px 0 rgba(255, 255, 255, 0.04); -$inner-glow-bottom-blue: inset 0 -1px 0 rgba(59, 130, 246, 0.12); +$inner-glow-top: inset 0 1px 0 $overlay-white-08; +$inner-glow-top-hover: inset 0 1px 0 $overlay-white-25; +$inner-glow-bottom: inset 0 -1px 0 $overlay-white-04; +$inner-glow-bottom-blue: inset 0 -1px 0 $color-blue-600-a12; // ==================== Blur system ==================== @@ -167,34 +231,34 @@ $blur-intense: blur(20px) saturate(1.4) brightness(1.15); // ==================== Glass background system ==================== $glass-base: linear-gradient(135deg, - rgba(255, 255, 255, 0.02) 0%, - rgba(255, 255, 255, 0.01) 50%, - rgba(255, 255, 255, 0.03) 100%); + $overlay-white-02 0%, + $overlay-white-01 50%, + $overlay-white-03 100%); -$glass-disabled: rgba(255, 255, 255, 0.02); +$glass-disabled: $overlay-white-02; -$glass-blue-subtle: rgba(59, 130, 246, 0.08); +$glass-blue-subtle: $color-blue-600-a08; $glass-blue-base: linear-gradient(135deg, - rgba(59, 130, 246, 0.12) 0%, - rgba(59, 130, 246, 0.08) 30%, + $color-blue-600-a12 0%, + $color-blue-600-a08 30%, rgba(59, 130, 246, 0.06) 60%, - rgba(59, 130, 246, 0.15) 100%); + $color-blue-600-a15 100%); $glass-blue-hover: linear-gradient(135deg, - rgba(59, 130, 246, 0.15) 0%, - rgba(59, 130, 246, 0.12) 30%, - rgba(59, 130, 246, 0.08) 60%, - rgba(59, 130, 246, 0.18) 100%); + $color-blue-600-a15 0%, + $color-blue-600-a12 30%, + $color-blue-600-a08 60%, + $color-blue-600-a18 100%); $glass-purple-base: linear-gradient(135deg, - rgba(139, 92, 246, 0.08) 0%, - rgba(139, 92, 246, 0.05) 30%, + $color-purple-500-a08 0%, + $color-purple-500-a05 30%, rgba(139, 92, 246, 0.06) 60%, - rgba(139, 92, 246, 0.1) 100%); + $color-purple-500-a10 100%); $glass-purple-hover: linear-gradient(135deg, - rgba(139, 92, 246, 0.25) 0%, + $color-purple-500-a25 0%, rgba(124, 58, 237, 0.18) 30%, rgba(196, 181, 253, 0.15) 60%, - rgba(139, 92, 246, 0.3) 100%); + $color-purple-500-a30 100%); $glass-red-subtle: rgba(239, 68, 68, 0.05); $glass-red-base: rgba(239, 68, 68, 0.08); @@ -326,27 +390,27 @@ $opacity-overlay: 0.4; // ==================== Component-specific tokens ==================== $button-bg-default: linear-gradient(135deg, - rgba(255, 255, 255, 0.08) 0%, - rgba(255, 255, 255, 0.06) 50%, - rgba(255, 255, 255, 0.1) 100% + $overlay-white-08 0%, + $overlay-white-06 50%, + $overlay-white-10 100% ); -$button-border-default: rgba(255, 255, 255, 0.08); +$button-border-default: $overlay-white-08; $button-bg-hover: linear-gradient(135deg, - rgba(59, 130, 246, 0.15) 0%, - rgba(96, 165, 250, 0.12) 30%, - rgba(59, 130, 246, 0.08) 60%, - rgba(59, 130, 246, 0.18) 100% + $color-blue-600-a15 0%, + $color-blue-500-a12 30%, + $color-blue-600-a08 60%, + $color-blue-600-a18 100% ); -$button-border-hover: rgba(59, 130, 246, 0.4); -$button-text-hover: #ffffff; +$button-border-hover: $color-blue-600-a40; +$button-text-hover: $color-static-white; $button-transform-hover: translateY(-3px) scale(1.03); $button-bg-primary: linear-gradient(135deg, - rgba(255, 255, 255, 0.1) 0%, - rgba(255, 255, 255, 0.08) 30%, - rgba(255, 255, 255, 0.09) 60%, - rgba(255, 255, 255, 0.12) 100% + $overlay-white-10 0%, + $overlay-white-08 30%, + $overlay-white-09 60%, + $overlay-white-12 100% ); $button-bg-primary-hover: $button-bg-hover; $button-bg-primary-active: $color-accent-700; @@ -360,14 +424,14 @@ $panel-bg: $color-bg-secondary; $panel-border: $border-base; $panel-shadow: $shadow-sm; -$card-bg-default: rgba(255, 255, 255, 0.04); -$card-bg-elevated: rgba(255, 255, 255, 0.06); -$card-bg-subtle: rgba(255, 255, 255, 0.02); -$card-bg-hover: rgba(255, 255, 255, 0.06); -$card-bg-active: rgba(255, 255, 255, 0.08); +$card-bg-default: $overlay-white-04; +$card-bg-elevated: $overlay-white-06; +$card-bg-subtle: $overlay-white-02; +$card-bg-hover: $overlay-white-06; +$card-bg-active: $overlay-white-08; $card-bg-accent: rgba(96, 165, 250, 0.1); $card-bg-accent-hover: rgba(96, 165, 250, 0.14); -$card-bg-purple: rgba(139, 92, 246, 0.1); +$card-bg-purple: $color-purple-500-a10; $card-bg-purple-hover: rgba(139, 92, 246, 0.14); $card-bg: $card-bg-default; // Legacy alias $card-border: $border-base; @@ -384,7 +448,7 @@ $nav-border: $border-base; $modal-bg: $color-bg-elevated; $modal-border: $border-medium; $modal-shadow: $shadow-xl; -$modal-backdrop: rgba(0, 0, 0, 0.85); +$modal-backdrop: $overlay-black-85; // ==================== Badge/label tokens ==================== // Badges/labels are borderless by default and use background color to differentiate @@ -420,16 +484,16 @@ $badge-info-text: $color-info; // Legacy: interactive base styles (prefer dashed-interactive-base) @mixin liquid-glass-base { background: linear-gradient(135deg, - rgba(255, 255, 255, 0.08) 0%, - rgba(255, 255, 255, 0.06) 50%, - rgba(255, 255, 255, 0.1) 100% + $overlay-white-08 0%, + $overlay-white-06 50%, + $overlay-white-10 100% ); - border: 1px solid rgba(255, 255, 255, 0.08); + border: 1px solid $overlay-white-08; color: $color-text-secondary; backdrop-filter: $blur-base; -webkit-backdrop-filter: $blur-base; - box-shadow: - 0 2px 4px rgba(0, 0, 0, 0.1), + box-shadow: + 0 2px 4px $overlay-black-10, $inner-glow-top; transition: all $motion-base $easing-standard; position: relative; @@ -440,40 +504,40 @@ $badge-info-text: $color-info; @mixin liquid-glass-hover($color: 'blue') { @if $color == 'blue' { background: linear-gradient(135deg, - rgba(59, 130, 246, 0.15) 0%, - rgba(96, 165, 250, 0.12) 30%, - rgba(59, 130, 246, 0.08) 60%, - rgba(59, 130, 246, 0.18) 100% + $color-blue-600-a15 0%, + $color-blue-500-a12 30%, + $color-blue-600-a08 60%, + $color-blue-600-a18 100% ); - border-color: rgba(59, 130, 246, 0.4); - box-shadow: + border-color: $color-blue-600-a40; + box-shadow: $glow-shadow-blue, $inner-glow-top-hover, $inner-glow-bottom-blue; } @else if $color == 'purple' { background: linear-gradient(135deg, - rgba(139, 92, 246, 0.15) 0%, + $color-purple-500-a15 0%, rgba(168, 85, 247, 0.12) 30%, rgba(124, 58, 237, 0.08) 60%, rgba(139, 92, 246, 0.18) 100% ); - border-color: rgba(139, 92, 246, 0.4); - box-shadow: + border-color: $color-purple-500-a40; + box-shadow: $glow-shadow-purple, $inner-glow-top-hover, - inset 0 -1px 0 rgba(139, 92, 246, 0.12); + inset 0 -1px 0 $color-purple-500-a12; } @else if $color == 'mixed' { background: linear-gradient(135deg, - rgba(59, 130, 246, 0.12) 0%, - rgba(139, 92, 246, 0.12) 100% + $color-blue-600-a12 0%, + $color-purple-500-a12 100% ); - border-color: rgba(59, 130, 246, 0.4); - box-shadow: + border-color: $color-blue-600-a40; + box-shadow: $glow-shadow-mixed, $inner-glow-top-hover, $inner-glow-bottom-blue; } - + color: $color-text-primary; backdrop-filter: $blur-strong; -webkit-backdrop-filter: $blur-strong; @@ -490,36 +554,36 @@ $badge-info-text: $color-info; height: 100%; z-index: 1; pointer-events: none; - + @if $color == 'white' { background: linear-gradient(90deg, transparent, - rgba(255, 255, 255, 0.05), - rgba(255, 255, 255, 0.08), - rgba(255, 255, 255, 0.05), + $overlay-white-05, + $overlay-white-08, + $overlay-white-05, transparent ); } @else if $color == 'blue' { background: linear-gradient(90deg, transparent, - rgba(96, 165, 250, 0.08), + $color-blue-500-a08, rgba(147, 197, 253, 0.15), - rgba(96, 165, 250, 0.08), + $color-blue-500-a08, transparent ); } @else if $color == 'purple' { background: linear-gradient(90deg, transparent, - rgba(139, 92, 246, 0.08), + $color-purple-500-a08, rgba(196, 181, 253, 0.15), - rgba(139, 92, 246, 0.08), + $color-purple-500-a08, transparent ); } - + transition: left $motion-slow $easing-standard; } - + &:hover::before { left: 100%; } @@ -544,7 +608,7 @@ $badge-info-text: $color-info; // Legacy: full hover effect combo @mixin cool-hover-effect($color-variant: 'blue') { transition: all $motion-base $easing-standard; - + &:hover:not(:disabled) { @include liquid-glass-hover($color-variant); } @@ -575,7 +639,7 @@ $badge-info-text: $color-info; background: $element-bg-subtle; border-style: solid; border-color: $border-strong; - + svg { color: $color-accent-500; } @@ -622,18 +686,18 @@ $badge-info-text: $color-info; @mixin card-hover-effect { @include light-flow('white'); - + &:hover { background: linear-gradient(135deg, - rgba(96, 165, 250, 0.08) 0%, - rgba(139, 92, 246, 0.05) 100% + $color-blue-500-a08 0%, + $color-purple-500-a05 100% ); border-color: rgba(96, 165, 250, 0.3); transform: translateY(-4px) scale(1.01); - box-shadow: - 0 12px 32px rgba(96, 165, 250, 0.15), - 0 6px 16px rgba(139, 92, 246, 0.1), - 0 2px 8px rgba(0, 0, 0, 0.1); + box-shadow: + 0 12px 32px $color-blue-500-a15, + 0 6px 16px $color-purple-500-a10, + 0 2px 8px $overlay-black-10; backdrop-filter: blur(12px) saturate(1.2); -webkit-backdrop-filter: blur(12px) saturate(1.2); } @@ -698,7 +762,7 @@ $badge-info-text: $color-info; @mixin badge-variant($variant: 'neutral') { @include badge-base; - + @if $variant == 'neutral' { background: $badge-neutral-bg; color: $badge-neutral-text; @@ -727,38 +791,38 @@ $badge-info-text: $color-info; @mixin apply-design-tokens { --color-bg-primary: #{$color-bg-primary}; - --color-static-white: #ffffff; - --color-static-black: #000000; + --color-static-white: #{$color-static-white}; + --color-static-black: #{$color-static-black}; --color-static-white-rgb: 255, 255, 255; --color-static-black-rgb: 0, 0, 0; - --color-overlay-white-02: rgba(255, 255, 255, 0.02); - --color-overlay-white-03: rgba(255, 255, 255, 0.03); - --color-overlay-white-04: rgba(255, 255, 255, 0.04); - --color-overlay-white-05: rgba(255, 255, 255, 0.05); - --color-overlay-white-06: rgba(255, 255, 255, 0.06); - --color-overlay-white-08: rgba(255, 255, 255, 0.08); - --color-overlay-white-10: rgba(255, 255, 255, 0.1); - --color-overlay-white-12: rgba(255, 255, 255, 0.12); - --color-overlay-white-15: rgba(255, 255, 255, 0.15); - --color-overlay-white-20: rgba(255, 255, 255, 0.2); + --color-overlay-white-02: #{$overlay-white-02}; + --color-overlay-white-03: #{$overlay-white-03}; + --color-overlay-white-04: #{$overlay-white-04}; + --color-overlay-white-05: #{$overlay-white-05}; + --color-overlay-white-06: #{$overlay-white-06}; + --color-overlay-white-08: #{$overlay-white-08}; + --color-overlay-white-10: #{$overlay-white-10}; + --color-overlay-white-12: #{$overlay-white-12}; + --color-overlay-white-15: #{$overlay-white-15}; + --color-overlay-white-20: #{$overlay-white-20}; --color-overlay-white-22: color-mix(in srgb, var(--color-static-white) 22%, transparent); - --color-overlay-white-60: rgba(255, 255, 255, 0.6); + --color-overlay-white-60: #{$overlay-white-60}; --color-overlay-white-70: color-mix(in srgb, var(--color-static-white) 70%, transparent); --color-overlay-white-80: color-mix(in srgb, var(--color-static-white) 80%, transparent); --color-overlay-white-90: color-mix(in srgb, var(--color-static-white) 90%, transparent); --color-overlay-white-92: color-mix(in srgb, var(--color-static-white) 92%, transparent); --color-overlay-white-95: color-mix(in srgb, var(--color-static-white) 95%, transparent); - --color-overlay-black-06: rgba(0, 0, 0, 0.06); - --color-overlay-black-08: rgba(0, 0, 0, 0.08); - --color-overlay-black-10: rgba(0, 0, 0, 0.1); - --color-overlay-black-12: rgba(0, 0, 0, 0.12); - --color-overlay-black-15: rgba(0, 0, 0, 0.15); - --color-overlay-black-20: rgba(0, 0, 0, 0.2); - --color-overlay-black-25: rgba(0, 0, 0, 0.25); - --color-overlay-black-30: rgba(0, 0, 0, 0.3); - --color-overlay-black-40: rgba(0, 0, 0, 0.4); - --color-overlay-black-50: rgba(0, 0, 0, 0.5); - --color-overlay-black-80: rgba(0, 0, 0, 0.8); + --color-overlay-black-06: #{$overlay-black-06}; + --color-overlay-black-08: #{$overlay-black-08}; + --color-overlay-black-10: #{$overlay-black-10}; + --color-overlay-black-12: #{$overlay-black-12}; + --color-overlay-black-15: #{$overlay-black-15}; + --color-overlay-black-20: #{$overlay-black-20}; + --color-overlay-black-25: #{$overlay-black-25}; + --color-overlay-black-30: #{$overlay-black-30}; + --color-overlay-black-40: #{$overlay-black-40}; + --color-overlay-black-50: #{$overlay-black-50}; + --color-overlay-black-80: #{$overlay-black-80}; --color-overlay-slate-rgb: 15, 23, 42; --color-overlay-slate-03: rgba(var(--color-overlay-slate-rgb), 0.03); --color-overlay-slate-04: rgba(var(--color-overlay-slate-rgb), 0.04); @@ -797,7 +861,7 @@ $badge-info-text: $color-info; --background-tertiary: var(--color-bg-tertiary); --color-background-secondary: var(--color-bg-secondary); --color-background-tertiary: var(--color-bg-tertiary); - + --color-text-primary: #{$color-text-primary}; --color-text-secondary: #{$color-text-secondary}; --color-text-tertiary: var(--color-text-muted); @@ -808,7 +872,7 @@ $badge-info-text: $color-info; --text-tertiary: var(--color-text-muted); --text-muted: var(--color-text-muted); --text-disabled: var(--color-text-disabled); - + --element-bg-subtle: #{$element-bg-subtle}; --element-bg-soft: #{$element-bg-soft}; --element-bg-base: #{$element-bg-base}; @@ -817,7 +881,7 @@ $badge-info-text: $color-info; --element-bg-elevated: #{$element-bg-elevated}; --element-bg: var(--element-bg-base); --element-bg-hover: var(--element-bg-medium); - + --color-accent-50: #{$color-accent-50}; --color-accent-100: #{$color-accent-100}; --color-accent-200: #{$color-accent-200}; @@ -841,7 +905,7 @@ $badge-info-text: $color-info; --color-primary-bg: var(--color-accent-100); --color-primary-bg-subtle: var(--color-accent-50); --color-accent-alpha: var(--color-accent-100); - + --color-purple-50: #{$color-purple-50}; --color-purple-100: #{$color-purple-100}; --color-purple-200: #{$color-purple-200}; @@ -856,7 +920,7 @@ $badge-info-text: $color-info; --color-cyan-400: #22d3ee; --color-cyan-500: #06b6d4; --color-error-soft: #f87171; - + --color-success: #{$color-success}; --color-success-bg: #{$color-success-bg}; --color-success-border: #{$color-success-border}; @@ -881,7 +945,7 @@ $badge-info-text: $color-info; --color-info: #{$color-info}; --color-info-bg: #{$color-info-bg}; --color-info-border: #{$color-info-border}; - + --git-color-branch: #{$git-color-branch}; --git-color-branch-bg: #{$git-color-branch-bg}; --git-color-branch-bg-hover: #{$git-color-branch-bg-hover}; @@ -907,7 +971,7 @@ $badge-info-text: $color-info; --git-color-push: #{$git-color-push}; --git-color-push-bg: #{$git-color-push-bg}; --git-color-push-bg-hover: #{$git-color-push-bg-hover}; - + --border-subtle: #{$border-subtle}; --border-color: var(--border-subtle); --border-base: #{$border-base}; @@ -927,7 +991,7 @@ $badge-info-text: $color-info; --border-accent-strong: #{$border-accent-strong}; --border-purple-subtle: #{$border-purple-subtle}; --border-purple: #{$border-purple}; - + --z-base: #{$z-base}; --z-decoration: #{$z-decoration}; --z-content: #{$z-content}; @@ -958,7 +1022,7 @@ $badge-info-text: $color-info; --button-shadow-hover: #{$button-shadow-hover}; --button-shadow-active: #{$button-shadow-active}; --scrollbar-thumb: var(--color-overlay-white-12); - --scrollbar-thumb-hover: rgba(255, 255, 255, 0.22); + --scrollbar-thumb-hover: #{$overlay-white-22}; --color-scrollbar: var(--scrollbar-thumb); --btn-primary-bg: var(--color-accent-200); --btn-primary-color: var(--color-accent-600); @@ -1004,9 +1068,9 @@ $badge-info-text: $color-info; --tool-card-positive-bg: #{$git-color-staged-bg}; --tool-card-positive-bg-hover: #{$git-color-staged-bg-hover}; --tool-card-positive-icon: color-mix(in srgb, var(--tool-card-positive-color) 90%, transparent); - --tool-card-git-color: #f97316; - --tool-card-terminal-color: #10b981; - --tool-card-risk-high-color: #f97316; + --tool-card-git-color: #{$color-static-orange}; + --tool-card-terminal-color: #{$color-static-emerald}; + --tool-card-risk-high-color: #{$color-static-orange}; --tool-card-mcp-color: var(--color-indigo-500); --flowchat-inline-tag-blue: #{$color-accent-500}; --flowchat-inline-tag-purple: var(--color-purple-soft); @@ -1024,19 +1088,19 @@ $badge-info-text: $color-info; --flowchat-copy-success-color: var(--color-success); --markdown-copy-success-color: var(--color-success); --git-graph-lane-blue: #{$color-accent-500}; - --git-graph-lane-green: #10b981; + --git-graph-lane-green: #{$color-static-emerald}; --git-graph-lane-cyan: var(--color-cyan-500); --git-graph-lane-warning: #{$color-warning}; --git-graph-lane-purple: #{$color-purple-500}; --git-graph-lane-error: #{$color-error}; --git-graph-lane-pink: #ec4899; - --git-graph-lane-orange: #f97316; + --git-graph-lane-orange: #{$color-static-orange}; --git-graph-text-primary: #d1d5db; --git-graph-text-muted: #6b7280; --git-graph-text-disabled: #4b5563; --git-graph-static-white: #{$button-text-hover}; --mission-control-group-primary-color: var(--color-accent-600); - --mission-control-group-secondary-color: #10b981; + --mission-control-group-secondary-color: #{$color-static-emerald}; --mission-control-group-tertiary-color: var(--color-warning); --deep-review-consent-accent: #2563eb; --deep-review-consent-accent-strong: #1d4ed8; @@ -1231,18 +1295,18 @@ $badge-info-text: $color-info; --config-page-content-max-width: 600px; --profile-acp-left-inline-pad: var(--size-gap-6); --review-team-member-accent-default: #64748b; - + --glow-shadow-blue: #{$glow-shadow-blue}; --glow-shadow-purple: #{$glow-shadow-purple}; --glow-shadow-red: #{$glow-shadow-red}; --glow-shadow-green: #{$glow-shadow-green}; --glow-shadow-mixed: #{$glow-shadow-mixed}; - + --inner-glow-top: #{$inner-glow-top}; --inner-glow-top-hover: #{$inner-glow-top-hover}; --inner-glow-bottom: #{$inner-glow-bottom}; --inner-glow-bottom-blue: #{$inner-glow-bottom-blue}; - + --glass-base: #{$glass-base}; --glass-disabled: #{$glass-disabled}; --glass-blue-subtle: #{$glass-blue-subtle}; @@ -1267,13 +1331,13 @@ $badge-info-text: $color-info; --glass-shadow-base: var(--shadow-base); --glass-shadow-lg: var(--shadow-lg); --glass-shadow-xl: var(--shadow-xl); - + --blur-subtle: #{$blur-subtle}; --blur-base: #{$blur-base}; --blur-medium: #{$blur-medium}; --blur-strong: #{$blur-strong}; --blur-intense: #{$blur-intense}; - + --size-radius-sm: #{$size-radius-sm}; --size-radius-base: #{$size-radius-base}; --size-radius-md: var(--size-radius-base); @@ -1288,7 +1352,7 @@ $badge-info-text: $color-info; --radius-xl: var(--size-radius-xl); --radius-2xl: var(--size-radius-2xl); --radius-full: var(--size-radius-full); - + --size-gap-1: #{$size-gap-1}; --size-gap-2: #{$size-gap-2}; --size-gap-3: #{$size-gap-3}; @@ -1309,7 +1373,7 @@ $badge-info-text: $color-info; --spacing-10: var(--size-gap-10); --spacing-12: var(--size-gap-12); --spacing-16: var(--size-gap-16); - + --motion-instant: #{$motion-instant}; --motion-fast: #{$motion-fast}; --motion-normal: var(--motion-base); @@ -1317,7 +1381,7 @@ $badge-info-text: $color-info; --motion-slow: #{$motion-slow}; --motion-lazy: #{$motion-lazy}; --smooth-height-collapse-duration: var(--motion-slow); - + --easing-standard: #{$easing-standard}; --easing-decelerate: #{$easing-decelerate}; --easing-accelerate: #{$easing-accelerate}; @@ -1325,7 +1389,7 @@ $badge-info-text: $color-info; --easing-smooth: #{$easing-smooth}; --easing-ease-in: #{$easing-ease-in}; --easing-ease-out: #{$easing-ease-out}; - + --font-family-sans: #{$font-family-sans}; --font-family-mono: #{$font-family-mono}; --font-sans: var(--font-family-sans); @@ -1349,7 +1413,7 @@ $badge-info-text: $color-info; --font-weight-medium: #{$font-weight-medium}; --font-weight-semibold: #{$font-weight-semibold}; --font-weight-bold: #{$font-weight-bold}; - + --font-size-xxs: #{$font-size-xxs}; --font-size-2xs: #{$font-size-2xs}; --font-size-xs: #{$font-size-xs}; @@ -1444,11 +1508,11 @@ $badge-info-text: $color-info; --bitfun-nav-row-action-icon-size: 13px; --bitfun-nav-row-action-offset: 4px; --bitfun-nav-row-action-gap: 4px; - + --line-height-tight: #{$line-height-tight}; --line-height-base: #{$line-height-base}; --line-height-relaxed: #{$line-height-relaxed}; - + --button-height-sm: #{$button-height-sm}; --button-height-base: #{$button-height-base}; --button-height-lg: #{$button-height-lg}; @@ -1457,18 +1521,18 @@ $badge-info-text: $color-info; --input-height-lg: #{$input-height-lg}; --header-height: #{$header-height}; --sidebar-width: #{$sidebar-width}; - + --opacity-disabled: #{$opacity-disabled}; --opacity-hover: #{$opacity-hover}; --opacity-focus: #{$opacity-focus}; --opacity-overlay: #{$opacity-overlay}; - + --badge-border-radius: #{$badge-border-radius}; --badge-padding-x: #{$badge-padding-x}; --badge-padding-y: #{$badge-padding-y}; --badge-font-size: #{$badge-font-size}; --badge-font-weight: #{$badge-font-weight}; - + --badge-neutral-bg: #{$badge-neutral-bg}; --badge-neutral-text: #{$badge-neutral-text}; --badge-accent-bg: #{$badge-accent-bg}; @@ -1483,7 +1547,7 @@ $badge-info-text: $color-info; --badge-error-text: #{$badge-error-text}; --badge-info-bg: #{$badge-info-bg}; --badge-info-text: #{$badge-info-text}; - + --card-bg-default: #{$card-bg-default}; --card-bg-elevated: #{$card-bg-elevated}; --card-bg-subtle: #{$card-bg-subtle}; diff --git a/src/web-ui/src/infrastructure/theme/core/ThemeService.ts b/src/web-ui/src/infrastructure/theme/core/ThemeService.ts index e700c0ada6..4b422b3c4d 100644 --- a/src/web-ui/src/infrastructure/theme/core/ThemeService.ts +++ b/src/web-ui/src/infrastructure/theme/core/ThemeService.ts @@ -31,6 +31,65 @@ const FLOW_CHAT_LINK_COLORS = { }, } as const; +const THEME_STATIC_COLORS = { + white: '#ffffff', + black: '#000000', +} as const; + +const THEME_OVERLAYS = { + white02: 'rgba(255, 255, 255, 0.02)', + white03: 'rgba(255, 255, 255, 0.03)', + white04: 'rgba(255, 255, 255, 0.04)', + white05: 'rgba(255, 255, 255, 0.05)', + white06: 'rgba(255, 255, 255, 0.06)', + white08: 'rgba(255, 255, 255, 0.08)', + white10: 'rgba(255, 255, 255, 0.1)', + white12: 'rgba(255, 255, 255, 0.12)', + white15: 'rgba(255, 255, 255, 0.15)', + white20: 'rgba(255, 255, 255, 0.2)', + white22: 'rgba(255, 255, 255, 0.22)', + white60: 'rgba(255, 255, 255, 0.6)', + black04: 'rgba(0, 0, 0, 0.04)', + black06: 'rgba(0, 0, 0, 0.06)', + black08: 'rgba(0, 0, 0, 0.08)', + black10: 'rgba(0, 0, 0, 0.1)', + black12: 'rgba(0, 0, 0, 0.12)', + black15: 'rgba(0, 0, 0, 0.15)', + black20: 'rgba(0, 0, 0, 0.2)', + black25: 'rgba(0, 0, 0, 0.25)', + black28: 'rgba(0, 0, 0, 0.28)', + black30: 'rgba(0, 0, 0, 0.3)', + black40: 'rgba(0, 0, 0, 0.4)', + black50: 'rgba(0, 0, 0, 0.5)', + black70: 'rgba(0, 0, 0, 0.7)', + black80: 'rgba(0, 0, 0, 0.8)', +} as const; + +const THEME_OVERLAY_TOKEN_VALUES = [ + ['--color-overlay-white-02', THEME_OVERLAYS.white02], + ['--color-overlay-white-03', THEME_OVERLAYS.white03], + ['--color-overlay-white-04', THEME_OVERLAYS.white04], + ['--color-overlay-white-05', THEME_OVERLAYS.white05], + ['--color-overlay-white-06', THEME_OVERLAYS.white06], + ['--color-overlay-white-08', THEME_OVERLAYS.white08], + ['--color-overlay-white-10', THEME_OVERLAYS.white10], + ['--color-overlay-white-12', THEME_OVERLAYS.white12], + ['--color-overlay-white-15', THEME_OVERLAYS.white15], + ['--color-overlay-white-20', THEME_OVERLAYS.white20], + ['--color-overlay-white-60', THEME_OVERLAYS.white60], + ['--color-overlay-black-06', THEME_OVERLAYS.black06], + ['--color-overlay-black-08', THEME_OVERLAYS.black08], + ['--color-overlay-black-10', THEME_OVERLAYS.black10], + ['--color-overlay-black-12', THEME_OVERLAYS.black12], + ['--color-overlay-black-15', THEME_OVERLAYS.black15], + ['--color-overlay-black-20', THEME_OVERLAYS.black20], + ['--color-overlay-black-25', THEME_OVERLAYS.black25], + ['--color-overlay-black-30', THEME_OVERLAYS.black30], + ['--color-overlay-black-40', THEME_OVERLAYS.black40], + ['--color-overlay-black-50', THEME_OVERLAYS.black50], + ['--color-overlay-black-80', THEME_OVERLAYS.black80], +] as const; + declare global { // Injected by the desktop webview initialization script. These values let the // first renderer pass apply the persisted built-in theme without waiting on a @@ -407,34 +466,11 @@ export class ThemeService { root.style.setProperty('--color-bg-primary', colors.background.primary); - root.style.setProperty('--color-static-white', '#ffffff'); - root.style.setProperty('--color-static-black', '#000000'); + root.style.setProperty('--color-static-white', THEME_STATIC_COLORS.white); + root.style.setProperty('--color-static-black', THEME_STATIC_COLORS.black); root.style.setProperty('--color-static-white-rgb', '255, 255, 255'); root.style.setProperty('--color-static-black-rgb', '0, 0, 0'); - [ - ['--color-overlay-white-02', 'rgba(255, 255, 255, 0.02)'], - ['--color-overlay-white-03', 'rgba(255, 255, 255, 0.03)'], - ['--color-overlay-white-04', 'rgba(255, 255, 255, 0.04)'], - ['--color-overlay-white-05', 'rgba(255, 255, 255, 0.05)'], - ['--color-overlay-white-06', 'rgba(255, 255, 255, 0.06)'], - ['--color-overlay-white-08', 'rgba(255, 255, 255, 0.08)'], - ['--color-overlay-white-10', 'rgba(255, 255, 255, 0.1)'], - ['--color-overlay-white-12', 'rgba(255, 255, 255, 0.12)'], - ['--color-overlay-white-15', 'rgba(255, 255, 255, 0.15)'], - ['--color-overlay-white-20', 'rgba(255, 255, 255, 0.2)'], - ['--color-overlay-white-60', 'rgba(255, 255, 255, 0.6)'], - ['--color-overlay-black-06', 'rgba(0, 0, 0, 0.06)'], - ['--color-overlay-black-08', 'rgba(0, 0, 0, 0.08)'], - ['--color-overlay-black-10', 'rgba(0, 0, 0, 0.1)'], - ['--color-overlay-black-12', 'rgba(0, 0, 0, 0.12)'], - ['--color-overlay-black-15', 'rgba(0, 0, 0, 0.15)'], - ['--color-overlay-black-20', 'rgba(0, 0, 0, 0.2)'], - ['--color-overlay-black-25', 'rgba(0, 0, 0, 0.25)'], - ['--color-overlay-black-30', 'rgba(0, 0, 0, 0.3)'], - ['--color-overlay-black-40', 'rgba(0, 0, 0, 0.4)'], - ['--color-overlay-black-50', 'rgba(0, 0, 0, 0.5)'], - ['--color-overlay-black-80', 'rgba(0, 0, 0, 0.8)'], - ].forEach(([name, value]) => { + THEME_OVERLAY_TOKEN_VALUES.forEach(([name, value]) => { root.style.setProperty(name, value); }); root.style.setProperty('--color-bg-secondary', colors.background.secondary); @@ -465,7 +501,7 @@ export class ThemeService { root.style.setProperty('--color-bg-tooltip', colors.background.tooltip); } - root.style.setProperty('--color-overlay', theme.type === 'dark' ? 'rgba(0, 0, 0, 0.5)' : 'rgba(0, 0, 0, 0.3)'); + root.style.setProperty('--color-overlay', theme.type === 'dark' ? THEME_OVERLAYS.black50 : THEME_OVERLAYS.black30); root.style.setProperty('--color-text-primary', colors.text.primary); @@ -592,13 +628,13 @@ export class ThemeService { const scrollbarThumb = colors.scrollbar?.thumb ?? ( theme.type === 'dark' - ? 'rgba(255, 255, 255, 0.12)' - : 'rgba(0, 0, 0, 0.15)' + ? THEME_OVERLAYS.white12 + : THEME_OVERLAYS.black15 ); const scrollbarThumbHover = colors.scrollbar?.thumbHover ?? ( theme.type === 'dark' - ? 'rgba(255, 255, 255, 0.22)' - : 'rgba(0, 0, 0, 0.28)' + ? THEME_OVERLAYS.white22 + : THEME_OVERLAYS.black28 ); root.style.setProperty('--scrollbar-thumb', scrollbarThumb); root.style.setProperty('--scrollbar-thumb-hover', scrollbarThumbHover); @@ -894,17 +930,17 @@ export class ThemeService { root.style.setProperty('--card-bg-default', 'rgba(255, 255, 255, 0.025)'); root.style.setProperty('--card-bg-elevated', 'rgba(255, 255, 255, 0.035)'); root.style.setProperty('--card-bg-subtle', 'rgba(255, 255, 255, 0.015)'); - root.style.setProperty('--card-bg-hover', 'rgba(255, 255, 255, 0.04)'); - root.style.setProperty('--card-bg-active', 'rgba(255, 255, 255, 0.05)'); + root.style.setProperty('--card-bg-hover', THEME_OVERLAYS.white04); + root.style.setProperty('--card-bg-active', THEME_OVERLAYS.white05); root.style.setProperty('--card-bg-accent', 'rgba(255, 255, 255, 0.09)'); root.style.setProperty('--card-bg-accent-hover', 'rgba(255, 255, 255, 0.13)'); root.style.setProperty('--card-bg-purple', 'rgba(139, 92, 246, 0.08)'); root.style.setProperty('--card-bg-purple-hover', 'rgba(139, 92, 246, 0.12)'); } else { - root.style.setProperty('--card-bg-default', 'rgba(0, 0, 0, 0.06)'); - root.style.setProperty('--card-bg-elevated', 'rgba(0, 0, 0, 0.08)'); - root.style.setProperty('--card-bg-subtle', 'rgba(0, 0, 0, 0.04)'); + root.style.setProperty('--card-bg-default', THEME_OVERLAYS.black06); + root.style.setProperty('--card-bg-elevated', THEME_OVERLAYS.black08); + root.style.setProperty('--card-bg-subtle', THEME_OVERLAYS.black04); root.style.setProperty('--card-bg-hover', 'rgba(0, 0, 0, 0.065)'); root.style.setProperty('--card-bg-active', 'rgba(0, 0, 0, 0.09)'); root.style.setProperty('--card-bg-accent', 'rgba(15, 23, 42, 0.08)'); @@ -916,7 +952,7 @@ export class ThemeService { root.style.setProperty('--modal-bg', colors.background.elevated); root.style.setProperty('--modal-border', colors.border.base); - root.style.setProperty('--modal-overlay', theme.type === 'dark' ? 'rgba(0, 0, 0, 0.7)' : 'rgba(0, 0, 0, 0.5)'); + root.style.setProperty('--modal-overlay', theme.type === 'dark' ? THEME_OVERLAYS.black70 : THEME_OVERLAYS.black50); root.style.setProperty('--nav-bg', colors.background.secondary); 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 8989cd1dce..6545e3140e 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 @@ -1,4 +1,4 @@ - + import { ThemeConfig } from '../types'; import { @@ -8,129 +8,155 @@ import { createStandardEasing, createStandardSpacing, createWindowControls, + overlayBlack, + overlayWhite, + rgbFromHex, + rgbaFromHex, } from './shared'; +const CHINA_NIGHT_BACKGROUND = '#1a1814'; +const CHINA_NIGHT_TEXT_PRIMARY = '#e8e6e1'; +const CHINA_NIGHT_BUTTON_TEXT = '#ccc9c4'; +const CHINA_NIGHT_ACCENT = '#73a5cc'; +const CHINA_NIGHT_ACCENT_HOVER = '#5a8bb3'; +const CHINA_NIGHT_GREEN = '#96c6b4'; +const CHINA_NIGHT_GREEN_HOVER = '#7aab98'; +const CHINA_NIGHT_SUCCESS = '#6bc072'; +const CHINA_NIGHT_WARNING = '#f5b555'; +const CHINA_NIGHT_ERROR = '#e85555'; +const CHINA_NIGHT_HIGHLIGHT = '#e6a84a'; + +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 chinaNightAccentHover = (alpha: number | string) => rgbaFromHex(CHINA_NIGHT_ACCENT_HOVER, alpha); +const chinaNightGreen = (alpha: number | string) => rgbaFromHex(CHINA_NIGHT_GREEN, alpha); +const chinaNightGreenHover = (alpha: number | string) => rgbaFromHex(CHINA_NIGHT_GREEN_HOVER, 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 = { - + id: 'bitfun-china-night', name: 'Ink Night', type: 'dark', description: 'Chinese dark theme - Starlit ink night, moonlight like water, serene and elegant', author: 'BitFun Team', version: '1.0.0', - - + + colors: { background: { - primary: '#1a1814', - secondary: '#212019', - tertiary: '#262420', - quaternary: '#2d2926', - elevated: '#2d2926', - workbench: '#1a1814', + primary: CHINA_NIGHT_BACKGROUND, + secondary: '#212019', + tertiary: '#262420', + quaternary: '#2d2926', + elevated: '#2d2926', + workbench: CHINA_NIGHT_BACKGROUND, scene: '#1e1c17', - tooltip: 'rgba(26, 24, 20, 0.95)', + tooltip: chinaNightBackground(0.95), }, - + text: { - primary: '#e8e6e1', - secondary: '#c5c3be', - muted: '#928f89', - disabled: '#5f5d59', + primary: CHINA_NIGHT_TEXT_PRIMARY, + secondary: '#c5c3be', + muted: '#928f89', + disabled: '#5f5d59', }, - + accent: { - 50: 'rgba(115, 165, 204, 0.04)', - 100: 'rgba(115, 165, 204, 0.08)', - 200: 'rgba(115, 165, 204, 0.15)', - 300: 'rgba(115, 165, 204, 0.25)', - 400: 'rgba(115, 165, 204, 0.4)', - 500: '#73a5cc', - 600: '#5a8bb3', - 700: 'rgba(90, 139, 179, 0.8)', - 800: 'rgba(90, 139, 179, 0.9)', + 50: chinaNightAccent(0.04), + 100: chinaNightAccent(0.08), + 200: chinaNightAccent(0.15), + 300: chinaNightAccent(0.25), + 400: chinaNightAccent(0.4), + 500: CHINA_NIGHT_ACCENT, + 600: CHINA_NIGHT_ACCENT_HOVER, + 700: chinaNightAccentHover(0.8), + 800: chinaNightAccentHover(0.9), }, - + purple: { - 50: 'rgba(150, 198, 180, 0.04)', - 100: 'rgba(150, 198, 180, 0.08)', - 200: 'rgba(150, 198, 180, 0.15)', - 300: 'rgba(150, 198, 180, 0.25)', - 400: 'rgba(150, 198, 180, 0.4)', - 500: '#96c6b4', - 600: '#7aab98', - 700: 'rgba(122, 171, 152, 0.8)', - 800: 'rgba(122, 171, 152, 0.9)', + 50: chinaNightGreen(0.04), + 100: chinaNightGreen(0.08), + 200: chinaNightGreen(0.15), + 300: chinaNightGreen(0.25), + 400: chinaNightGreen(0.4), + 500: CHINA_NIGHT_GREEN, + 600: CHINA_NIGHT_GREEN_HOVER, + 700: chinaNightGreenHover(0.8), + 800: chinaNightGreenHover(0.9), }, - + semantic: { - success: '#6bc072', - successBg: 'rgba(107, 192, 114, 0.12)', - successBorder: 'rgba(107, 192, 114, 0.3)', - - warning: '#f5b555', - warningBg: 'rgba(245, 181, 85, 0.12)', - warningBorder: 'rgba(245, 181, 85, 0.3)', - - error: '#e85555', - errorBg: 'rgba(232, 85, 85, 0.12)', - errorBorder: 'rgba(232, 85, 85, 0.3)', - - info: '#73a5cc', - infoBg: 'rgba(115, 165, 204, 0.12)', - infoBorder: 'rgba(115, 165, 204, 0.3)', - - - highlight: '#e6a84a', - highlightBg: 'rgba(230, 168, 74, 0.15)', + success: CHINA_NIGHT_SUCCESS, + successBg: chinaNightSuccess(0.12), + successBorder: chinaNightSuccess(0.3), + + warning: CHINA_NIGHT_WARNING, + warningBg: chinaNightWarning(0.12), + warningBorder: chinaNightWarning(0.3), + + error: CHINA_NIGHT_ERROR, + errorBg: chinaNightError(0.12), + errorBorder: chinaNightError(0.3), + + info: CHINA_NIGHT_ACCENT, + infoBg: chinaNightAccent(0.12), + infoBorder: chinaNightAccent(0.3), + + + highlight: CHINA_NIGHT_HIGHLIGHT, + highlightBg: rgbaFromHex(CHINA_NIGHT_HIGHLIGHT, 0.15), }, - + border: { - subtle: 'rgba(232, 230, 225, 0.1)', - base: 'rgba(232, 230, 225, 0.16)', - medium: 'rgba(232, 230, 225, 0.22)', - strong: 'rgba(232, 230, 225, 0.28)', - prominent: 'rgba(232, 230, 225, 0.38)', + subtle: chinaNightText(0.1), + base: chinaNightText(0.16), + medium: chinaNightText(0.22), + strong: chinaNightText(0.28), + prominent: chinaNightText(0.38), }, - + element: { - subtle: 'rgba(115, 165, 204, 0.06)', - soft: 'rgba(115, 165, 204, 0.09)', - base: 'rgba(115, 165, 204, 0.12)', - medium: 'rgba(115, 165, 204, 0.16)', - strong: 'rgba(115, 165, 204, 0.2)', - elevated: 'rgba(45, 41, 38, 0.95)', + subtle: chinaNightAccent(0.06), + soft: chinaNightAccent(0.09), + base: chinaNightAccent(0.12), + medium: chinaNightAccent(0.16), + strong: chinaNightAccent(0.2), + elevated: rgbaFromHex('#2d2926', 0.95), }, - + git: createGitColors({ - branch: 'rgb(115, 165, 204)', - branchBg: 'rgba(115, 165, 204, 0.12)', - changes: 'rgb(245, 181, 85)', - changesBg: 'rgba(245, 181, 85, 0.12)', - added: 'rgb(107, 192, 114)', - addedBg: 'rgba(107, 192, 114, 0.12)', - deleted: 'rgb(232, 85, 85)', - deletedBg: 'rgba(232, 85, 85, 0.12)', + 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), }), }, - - + + effects: { shadow: { - xs: '0 1px 2px rgba(0, 0, 0, 0.5)', - sm: '0 2px 4px rgba(0, 0, 0, 0.6)', - base: '0 4px 8px rgba(0, 0, 0, 0.65)', - lg: '0 8px 16px rgba(0, 0, 0, 0.7)', - xl: '0 12px 24px rgba(0, 0, 0, 0.75)', - '2xl': '0 16px 32px rgba(0, 0, 0, 0.8)', + xs: `0 1px 2px ${overlayBlack(0.5)}`, + sm: `0 2px 4px ${overlayBlack(0.6)}`, + base: `0 4px 8px ${overlayBlack(0.65)}`, + lg: `0 8px 16px ${overlayBlack(0.7)}`, + xl: `0 12px 24px ${overlayBlack(0.75)}`, + '2xl': `0 16px 32px ${overlayBlack(0.8)}`, }, - + glow: { - blue: '0 8px 24px rgba(115, 165, 204, 0.25), 0 4px 12px rgba(115, 165, 204, 0.18), 0 2px 6px rgba(0, 0, 0, 0.3)', - purple: '0 8px 24px rgba(150, 198, 180, 0.25), 0 4px 12px rgba(150, 198, 180, 0.18), 0 2px 6px rgba(0, 0, 0, 0.3)', - mixed: '0 8px 24px rgba(115, 165, 204, 0.2), 0 4px 12px rgba(150, 198, 180, 0.18), 0 2px 6px rgba(0, 0, 0, 0.3)', + blue: `0 8px 24px ${chinaNightAccent(0.25)}, 0 4px 12px ${chinaNightAccent(0.18)}, 0 2px 6px ${overlayBlack(0.3)}`, + purple: `0 8px 24px ${chinaNightGreen(0.25)}, 0 4px 12px ${chinaNightGreen(0.18)}, 0 2px 6px ${overlayBlack(0.3)}`, + mixed: `0 8px 24px ${chinaNightAccent(0.2)}, 0 4px 12px ${chinaNightGreen(0.18)}, 0 2px 6px ${overlayBlack(0.3)}`, }, - + blur: { subtle: 'blur(4px) saturate(1.1)', base: 'blur(8px) saturate(1.15)', @@ -138,11 +164,11 @@ export const bitfunChinaNightTheme: ThemeConfig = { strong: 'blur(16px) saturate(1.25) brightness(1.05)', intense: 'blur(20px) saturate(1.3) brightness(1.08)', }, - + radius: createCompactRadius(), - + spacing: createStandardSpacing(), - + opacity: { disabled: 0.45, hover: 0.75, @@ -150,8 +176,8 @@ export const bitfunChinaNightTheme: ThemeConfig = { overlay: 0.5, }, }, - - + + motion: { duration: { instant: '0.1s', @@ -160,89 +186,89 @@ export const bitfunChinaNightTheme: ThemeConfig = { slow: '0.7s', lazy: '1.2s', }, - + easing: createStandardEasing('cubic-bezier(0.25, 0.1, 0.25, 1)'), }, - - + + typography: createChinaTypography(), - - + + components: { - + windowControls: createWindowControls({ standard: { - dot: 'rgba(115, 165, 204, 0.45)', - dotShadow: '0 0 4px rgba(115, 165, 204, 0.2)', - hoverBg: 'rgba(115, 165, 204, 0.12)', - hoverColor: '#73a5cc', - hoverBorder: 'rgba(115, 165, 204, 0.2)', - hoverShadow: '0 2px 8px rgba(115, 165, 204, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1)', + dot: chinaNightAccent(0.45), + dotShadow: `0 0 4px ${chinaNightAccent(0.2)}`, + hoverBg: chinaNightAccent(0.12), + hoverColor: CHINA_NIGHT_ACCENT, + hoverBorder: chinaNightAccent(0.2), + hoverShadow: `0 2px 8px ${chinaNightAccent(0.15)}, inset 0 1px 0 ${overlayWhite(0.1)}`, }, close: { - dot: 'rgba(232, 85, 85, 0.45)', - dotShadow: '0 0 4px rgba(232, 85, 85, 0.2)', - hoverBg: 'rgba(232, 85, 85, 0.12)', - hoverColor: '#e85555', - hoverBorder: 'rgba(232, 85, 85, 0.2)', - hoverShadow: '0 2px 8px rgba(232, 85, 85, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1)', + dot: chinaNightError(0.45), + dotShadow: `0 0 4px ${chinaNightError(0.2)}`, + hoverBg: chinaNightError(0.12), + hoverColor: CHINA_NIGHT_ERROR, + hoverBorder: chinaNightError(0.2), + hoverShadow: `0 2px 8px ${chinaNightError(0.15)}, inset 0 1px 0 ${overlayWhite(0.1)}`, }, common: { - defaultColor: 'rgba(255, 255, 255, 0.9)', - defaultDot: 'rgba(255, 255, 255, 0.2)', - disabledDot: 'rgba(255, 255, 255, 0.1)', - flowGradient: 'linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.05), transparent)', + defaultColor: overlayWhite(0.9), + defaultDot: overlayWhite(0.2), + disabledDot: overlayWhite(0.1), + flowGradient: `linear-gradient(90deg, transparent, ${overlayWhite(0.05)}, ${overlayWhite(0.08)}, ${overlayWhite(0.05)}, transparent)`, }, }), - + button: { - + default: { - background: 'rgba(115, 165, 204, 0.11)', + background: chinaNightAccent(0.11), color: '#a29d96', border: 'transparent', shadow: 'none', }, hover: { - background: 'rgba(115, 165, 204, 0.19)', - color: '#ccc9c4', + background: chinaNightAccent(0.19), + color: CHINA_NIGHT_BUTTON_TEXT, border: 'transparent', shadow: 'none', transform: 'none', }, active: { - background: 'rgba(115, 165, 204, 0.15)', - color: '#ccc9c4', + background: chinaNightAccent(0.15), + color: CHINA_NIGHT_BUTTON_TEXT, border: 'transparent', shadow: 'none', transform: 'none', }, - - + + primary: { default: { - background: 'rgba(115, 165, 204, 0.24)', + background: chinaNightAccent(0.24), color: '#88b8d8', border: 'transparent', shadow: 'none', }, hover: { - background: 'rgba(115, 165, 204, 0.34)', + background: chinaNightAccent(0.34), color: '#b0d5ea', border: 'transparent', shadow: 'none', transform: 'none', }, active: { - background: 'rgba(115, 165, 204, 0.28)', + background: chinaNightAccent(0.28), color: '#b0d5ea', border: 'transparent', shadow: 'none', transform: 'none', }, }, - - + + ghost: { default: { background: 'transparent', @@ -251,15 +277,15 @@ export const bitfunChinaNightTheme: ThemeConfig = { shadow: 'none', }, hover: { - background: 'rgba(115, 165, 204, 0.13)', - color: '#ccc9c4', + background: chinaNightAccent(0.13), + color: CHINA_NIGHT_BUTTON_TEXT, border: 'transparent', shadow: 'none', transform: 'none', }, active: { - background: 'rgba(115, 165, 204, 0.11)', - color: '#ccc9c4', + background: chinaNightAccent(0.11), + color: CHINA_NIGHT_BUTTON_TEXT, border: 'transparent', shadow: 'none', transform: 'none', @@ -267,34 +293,34 @@ export const bitfunChinaNightTheme: ThemeConfig = { }, }, }, - - + + monaco: { base: 'vs-dark', inherit: true, rules: [ - { token: 'comment', foreground: '928f89', fontStyle: 'italic' }, - { token: 'keyword', foreground: 'e85555' }, - { token: 'string', foreground: '6bc072' }, - { token: 'number', foreground: 'f5b555' }, - { token: 'type', foreground: '73a5cc' }, - { token: 'class', foreground: '73a5cc' }, - { token: 'function', foreground: '96c6b4' }, - { token: 'variable', foreground: 'c5c3be' }, - { token: 'constant', foreground: 'd4a574' }, - { token: 'operator', foreground: 'e85555' }, - { token: 'tag', foreground: '73a5cc' }, - { token: 'attribute.name', foreground: '96c6b4' }, - { token: 'attribute.value', foreground: '6bc072' }, + { token: 'comment', foreground: '928f89', fontStyle: 'italic' }, + { token: 'keyword', foreground: 'e85555' }, + { token: 'string', foreground: '6bc072' }, + { token: 'number', foreground: 'f5b555' }, + { token: 'type', foreground: '73a5cc' }, + { token: 'class', foreground: '73a5cc' }, + { token: 'function', foreground: '96c6b4' }, + { token: 'variable', foreground: 'c5c3be' }, + { token: 'constant', foreground: 'd4a574' }, + { token: 'operator', foreground: 'e85555' }, + { token: 'tag', foreground: '73a5cc' }, + { token: 'attribute.name', foreground: '96c6b4' }, + { token: 'attribute.value', foreground: '6bc072' }, ], colors: { - background: '#1a1814', - foreground: '#e8e6e1', - lineHighlight: '#212019', - selection: 'rgba(115, 165, 204, 0.25)', - cursor: '#73a5cc', - 'editor.selectionBackground': 'rgba(115, 165, 204, 0.25)', - 'editorCursor.foreground': '#73a5cc', + background: CHINA_NIGHT_BACKGROUND, + foreground: CHINA_NIGHT_TEXT_PRIMARY, + lineHighlight: '#212019', + selection: chinaNightAccent(0.25), + cursor: CHINA_NIGHT_ACCENT, + 'editor.selectionBackground': chinaNightAccent(0.25), + 'editorCursor.foreground': CHINA_NIGHT_ACCENT, }, }, }; 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 dd4917f7e7..c128e98461 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 @@ -1,4 +1,4 @@ - + import { ThemeConfig } from '../types'; import { @@ -8,127 +8,154 @@ import { createStandardEasing, createStandardSpacing, createWindowControls, + rgbFromHex, + rgbaFromHex, + STATIC_BLACK, + STATIC_WHITE, } from './shared'; +const CHINA_STYLE_PAPER = '#faf8f0'; +const CHINA_STYLE_INK = '#1a1a1a'; +const CHINA_STYLE_BUTTON_TEXT = '#3a3a3a'; +const CHINA_STYLE_BLUE = '#2e5e8a'; +const CHINA_STYLE_BLUE_HOVER = '#234a6d'; +const CHINA_STYLE_GREEN = '#7eb09b'; +const CHINA_STYLE_GREEN_HOVER = '#5a9078'; +const CHINA_STYLE_SUCCESS = '#52ad5a'; +const CHINA_STYLE_WARNING = '#f0a020'; +const CHINA_STYLE_ERROR = '#c8102e'; +const CHINA_STYLE_BORDER = '#6a5c46'; + +const chinaStylePaper = (alpha: number | string) => rgbaFromHex(CHINA_STYLE_PAPER, alpha); +const chinaStyleInk = (alpha: number | string) => rgbaFromHex(CHINA_STYLE_INK, alpha); +const chinaStyleBlue = (alpha: number | string) => rgbaFromHex(CHINA_STYLE_BLUE, alpha); +const chinaStyleBlueHover = (alpha: number | string) => rgbaFromHex(CHINA_STYLE_BLUE_HOVER, alpha); +const chinaStyleGreen = (alpha: number | string) => rgbaFromHex(CHINA_STYLE_GREEN, alpha); +const chinaStyleGreenHover = (alpha: number | string) => rgbaFromHex(CHINA_STYLE_GREEN_HOVER, 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 = { - + id: 'bitfun-china-style', name: 'Ink Charm', type: 'light', description: 'Chinese style theme - Rice paper and ink, blue and vermilion, warm and elegant', author: 'BitFun Team', version: '1.0.0', - - + + colors: { background: { - primary: '#faf8f0', - secondary: '#f5f3e8', - tertiary: '#f0ede0', - quaternary: '#ebe8d8', - elevated: '#ebe9e3', - workbench: '#faf8f0', + primary: CHINA_STYLE_PAPER, + secondary: '#f5f3e8', + tertiary: '#f0ede0', + quaternary: '#ebe8d8', + elevated: '#ebe9e3', + workbench: CHINA_STYLE_PAPER, scene: '#fdfcf6', - tooltip: 'rgba(250, 248, 240, 0.96)', + tooltip: chinaStylePaper(0.96), }, - + text: { - primary: '#1a1a1a', - secondary: '#3d3d3d', - muted: '#6a6a6a', - disabled: '#9a9a9a', + primary: CHINA_STYLE_INK, + secondary: '#3d3d3d', + muted: '#6a6a6a', + disabled: '#9a9a9a', }, - + accent: { - 50: 'rgba(46, 94, 138, 0.04)', - 100: 'rgba(46, 94, 138, 0.08)', - 200: 'rgba(46, 94, 138, 0.15)', - 300: 'rgba(46, 94, 138, 0.25)', - 400: 'rgba(46, 94, 138, 0.4)', - 500: '#2e5e8a', - 600: '#234a6d', - 700: 'rgba(35, 74, 109, 0.8)', - 800: 'rgba(35, 74, 109, 0.9)', + 50: chinaStyleBlue(0.04), + 100: chinaStyleBlue(0.08), + 200: chinaStyleBlue(0.15), + 300: chinaStyleBlue(0.25), + 400: chinaStyleBlue(0.4), + 500: CHINA_STYLE_BLUE, + 600: CHINA_STYLE_BLUE_HOVER, + 700: chinaStyleBlueHover(0.8), + 800: chinaStyleBlueHover(0.9), }, - + purple: { - 50: 'rgba(126, 176, 155, 0.04)', - 100: 'rgba(126, 176, 155, 0.08)', - 200: 'rgba(126, 176, 155, 0.15)', - 300: 'rgba(126, 176, 155, 0.25)', - 400: 'rgba(126, 176, 155, 0.4)', - 500: '#7eb09b', - 600: '#5a9078', - 700: 'rgba(90, 144, 120, 0.8)', - 800: 'rgba(90, 144, 120, 0.9)', + 50: chinaStyleGreen(0.04), + 100: chinaStyleGreen(0.08), + 200: chinaStyleGreen(0.15), + 300: chinaStyleGreen(0.25), + 400: chinaStyleGreen(0.4), + 500: CHINA_STYLE_GREEN, + 600: CHINA_STYLE_GREEN_HOVER, + 700: chinaStyleGreenHover(0.8), + 800: chinaStyleGreenHover(0.9), }, - + semantic: { - success: '#52ad5a', - successBg: 'rgba(82, 173, 90, 0.08)', - successBorder: 'rgba(82, 173, 90, 0.25)', - - warning: '#f0a020', - warningBg: 'rgba(240, 160, 32, 0.08)', - warningBorder: 'rgba(240, 160, 32, 0.25)', - - error: '#c8102e', - errorBg: 'rgba(200, 16, 46, 0.08)', - errorBorder: 'rgba(200, 16, 46, 0.25)', - - info: '#2e5e8a', - infoBg: 'rgba(46, 94, 138, 0.08)', - infoBorder: 'rgba(46, 94, 138, 0.25)', - highlight: '#2e5e8a', - highlightBg: 'rgba(46, 94, 138, 0.12)', + success: CHINA_STYLE_SUCCESS, + successBg: chinaStyleSuccess(0.08), + successBorder: chinaStyleSuccess(0.25), + + warning: CHINA_STYLE_WARNING, + warningBg: chinaStyleWarning(0.08), + warningBorder: chinaStyleWarning(0.25), + + error: CHINA_STYLE_ERROR, + errorBg: chinaStyleError(0.08), + errorBorder: chinaStyleError(0.25), + + info: CHINA_STYLE_BLUE, + infoBg: chinaStyleBlue(0.08), + infoBorder: chinaStyleBlue(0.25), + highlight: CHINA_STYLE_BLUE, + highlightBg: chinaStyleBlue(0.12), }, - + border: { - subtle: 'rgba(106, 92, 70, 0.12)', - base: 'rgba(106, 92, 70, 0.2)', - medium: 'rgba(106, 92, 70, 0.28)', - strong: 'rgba(106, 92, 70, 0.36)', - prominent: 'rgba(106, 92, 70, 0.48)', + subtle: chinaStyleBorder(0.12), + base: chinaStyleBorder(0.2), + medium: chinaStyleBorder(0.28), + strong: chinaStyleBorder(0.36), + prominent: chinaStyleBorder(0.48), }, - + element: { - subtle: 'rgba(46, 94, 138, 0.03)', - soft: 'rgba(46, 94, 138, 0.06)', - base: 'rgba(46, 94, 138, 0.1)', - medium: 'rgba(46, 94, 138, 0.14)', - strong: 'rgba(46, 94, 138, 0.18)', - elevated: 'rgba(255, 255, 255, 0.85)', + subtle: chinaStyleBlue(0.03), + soft: chinaStyleBlue(0.06), + base: chinaStyleBlue(0.1), + medium: chinaStyleBlue(0.14), + strong: chinaStyleBlue(0.18), + elevated: rgbaFromHex(STATIC_WHITE, 0.85), }, - + git: createGitColors({ - branch: 'rgb(46, 94, 138)', - branchBg: 'rgba(46, 94, 138, 0.08)', - changes: 'rgb(240, 160, 32)', - changesBg: 'rgba(240, 160, 32, 0.08)', - added: 'rgb(82, 173, 90)', - addedBg: 'rgba(82, 173, 90, 0.08)', - deleted: 'rgb(200, 16, 46)', - deletedBg: 'rgba(200, 16, 46, 0.08)', + 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), }), }, - - + + effects: { shadow: { - xs: '0 1px 2px rgba(106, 92, 70, 0.06)', - sm: '0 2px 4px rgba(106, 92, 70, 0.08)', - base: '0 4px 8px rgba(106, 92, 70, 0.1)', - lg: '0 8px 16px rgba(106, 92, 70, 0.12)', - xl: '0 12px 24px rgba(106, 92, 70, 0.15)', - '2xl': '0 16px 32px rgba(106, 92, 70, 0.18)', + xs: `0 1px 2px ${chinaStyleBorder(0.06)}`, + sm: `0 2px 4px ${chinaStyleBorder(0.08)}`, + base: `0 4px 8px ${chinaStyleBorder(0.1)}`, + lg: `0 8px 16px ${chinaStyleBorder(0.12)}`, + xl: `0 12px 24px ${chinaStyleBorder(0.15)}`, + '2xl': `0 16px 32px ${chinaStyleBorder(0.18)}`, }, - + glow: { - blue: '0 8px 24px rgba(46, 94, 138, 0.18), 0 4px 12px rgba(46, 94, 138, 0.12), 0 2px 6px rgba(106, 92, 70, 0.05)', - purple: '0 8px 24px rgba(126, 176, 155, 0.18), 0 4px 12px rgba(126, 176, 155, 0.12), 0 2px 6px rgba(106, 92, 70, 0.05)', - mixed: '0 8px 24px rgba(46, 94, 138, 0.12), 0 4px 12px rgba(126, 176, 155, 0.1), 0 2px 6px rgba(106, 92, 70, 0.05)', + blue: `0 8px 24px ${chinaStyleBlue(0.18)}, 0 4px 12px ${chinaStyleBlue(0.12)}, 0 2px 6px ${chinaStyleBorder(0.05)}`, + purple: `0 8px 24px ${chinaStyleGreen(0.18)}, 0 4px 12px ${chinaStyleGreen(0.12)}, 0 2px 6px ${chinaStyleBorder(0.05)}`, + mixed: `0 8px 24px ${chinaStyleBlue(0.12)}, 0 4px 12px ${chinaStyleGreen(0.1)}, 0 2px 6px ${chinaStyleBorder(0.05)}`, }, - + blur: { subtle: 'blur(4px) saturate(1.03)', base: 'blur(8px) saturate(1.05)', @@ -136,11 +163,11 @@ export const bitfunChinaStyleTheme: ThemeConfig = { strong: 'blur(16px) saturate(1.1) brightness(1.02)', intense: 'blur(20px) saturate(1.12) brightness(1.03)', }, - + radius: createCompactRadius(), - + spacing: createStandardSpacing(), - + opacity: { disabled: 0.5, hover: 0.75, @@ -148,99 +175,99 @@ export const bitfunChinaStyleTheme: ThemeConfig = { overlay: 0.35, }, }, - - + + motion: { duration: { instant: '0.1s', - fast: '0.2s', - base: '0.35s', + fast: '0.2s', + base: '0.35s', slow: '0.7s', - lazy: '1.2s', + lazy: '1.2s', }, - + easing: createStandardEasing('cubic-bezier(0.25, 0.1, 0.25, 1)'), }, - - + + typography: createChinaTypography(), - - + + components: { - + windowControls: createWindowControls({ standard: { - dot: 'rgba(46, 94, 138, 0.45)', - dotShadow: '0 0 4px rgba(46, 94, 138, 0.2)', - hoverBg: 'rgba(46, 94, 138, 0.12)', - hoverColor: '#2e5e8a', - hoverBorder: 'rgba(46, 94, 138, 0.2)', - hoverShadow: '0 2px 8px rgba(46, 94, 138, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.5)', + dot: chinaStyleBlue(0.45), + dotShadow: `0 0 4px ${chinaStyleBlue(0.2)}`, + hoverBg: chinaStyleBlue(0.12), + hoverColor: CHINA_STYLE_BLUE, + hoverBorder: chinaStyleBlue(0.2), + hoverShadow: `0 2px 8px ${chinaStyleBlue(0.15)}, inset 0 1px 0 ${rgbaFromHex(STATIC_WHITE, 0.5)}`, }, close: { - dot: 'rgba(200, 16, 46, 0.45)', - dotShadow: '0 0 4px rgba(200, 16, 46, 0.2)', - hoverBg: 'rgba(200, 16, 46, 0.12)', - hoverColor: '#c8102e', - hoverBorder: 'rgba(200, 16, 46, 0.2)', - hoverShadow: '0 2px 8px rgba(200, 16, 46, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.5)', + dot: chinaStyleError(0.45), + dotShadow: `0 0 4px ${chinaStyleError(0.2)}`, + hoverBg: chinaStyleError(0.12), + hoverColor: CHINA_STYLE_ERROR, + hoverBorder: chinaStyleError(0.2), + hoverShadow: `0 2px 8px ${chinaStyleError(0.15)}, inset 0 1px 0 ${rgbaFromHex(STATIC_WHITE, 0.5)}`, }, common: { - defaultColor: 'rgba(26, 26, 26, 0.9)', - defaultDot: 'rgba(106, 92, 70, 0.2)', - disabledDot: 'rgba(106, 92, 70, 0.1)', - flowGradient: 'linear-gradient(90deg, transparent, rgba(106, 92, 70, 0.05), rgba(106, 92, 70, 0.08), rgba(106, 92, 70, 0.05), transparent)', + defaultColor: chinaStyleInk(0.9), + defaultDot: chinaStyleBorder(0.2), + disabledDot: chinaStyleBorder(0.1), + flowGradient: `linear-gradient(90deg, transparent, ${chinaStyleBorder(0.05)}, ${chinaStyleBorder(0.08)}, ${chinaStyleBorder(0.05)}, transparent)`, }, }), - + button: { - + default: { - background: 'rgba(46, 94, 138, 0.09)', + background: chinaStyleBlue(0.09), color: '#5a5a5a', border: 'transparent', shadow: 'none', }, hover: { - background: 'rgba(46, 94, 138, 0.16)', - color: '#3a3a3a', + background: chinaStyleBlue(0.16), + color: CHINA_STYLE_BUTTON_TEXT, border: 'transparent', shadow: 'none', transform: 'none', }, active: { - background: 'rgba(46, 94, 138, 0.12)', - color: '#3a3a3a', + background: chinaStyleBlue(0.12), + color: CHINA_STYLE_BUTTON_TEXT, border: 'transparent', shadow: 'none', transform: 'none', }, - - + + primary: { default: { - background: '#000000', - color: '#ffffff', + background: STATIC_BLACK, + color: STATIC_WHITE, border: 'transparent', shadow: 'none', }, hover: { background: '#262626', - color: '#ffffff', + color: STATIC_WHITE, border: 'transparent', shadow: 'none', transform: 'none', }, active: { - background: '#1a1a1a', - color: '#ffffff', + background: CHINA_STYLE_INK, + color: STATIC_WHITE, border: 'transparent', shadow: 'none', transform: 'none', }, }, - - + + ghost: { default: { background: 'transparent', @@ -249,15 +276,15 @@ export const bitfunChinaStyleTheme: ThemeConfig = { shadow: 'none', }, hover: { - background: 'rgba(46, 94, 138, 0.11)', - color: '#3a3a3a', + background: chinaStyleBlue(0.11), + color: CHINA_STYLE_BUTTON_TEXT, border: 'transparent', shadow: 'none', transform: 'none', }, active: { - background: 'rgba(46, 94, 138, 0.08)', - color: '#3a3a3a', + background: chinaStyleBlue(0.08), + color: CHINA_STYLE_BUTTON_TEXT, border: 'transparent', shadow: 'none', transform: 'none', @@ -265,41 +292,41 @@ export const bitfunChinaStyleTheme: ThemeConfig = { }, }, }, - - + + monaco: { base: 'vs', inherit: true, rules: [ - { token: 'comment', foreground: '6a6a6a', fontStyle: 'italic' }, - { token: 'keyword', foreground: 'c8102e' }, - { token: 'string', foreground: '52ad5a' }, - { token: 'number', foreground: 'f0a020' }, - { token: 'type', foreground: '2e5e8a' }, - { token: 'class', foreground: '2e5e8a' }, - { token: 'function', foreground: '7eb09b' }, - { token: 'variable', foreground: '3d3d3d' }, - { token: 'constant', foreground: 'a0522d' }, - { token: 'operator', foreground: 'c8102e' }, - { token: 'tag', foreground: '2e5e8a' }, - { token: 'attribute.name', foreground: '7eb09b' }, - { token: 'attribute.value', foreground: '52ad5a' }, + { token: 'comment', foreground: '6a6a6a', fontStyle: 'italic' }, + { token: 'keyword', foreground: 'c8102e' }, + { token: 'string', foreground: '52ad5a' }, + { token: 'number', foreground: 'f0a020' }, + { token: 'type', foreground: '2e5e8a' }, + { token: 'class', foreground: '2e5e8a' }, + { token: 'function', foreground: '7eb09b' }, + { token: 'variable', foreground: '3d3d3d' }, + { token: 'constant', foreground: 'a0522d' }, + { token: 'operator', foreground: 'c8102e' }, + { token: 'tag', foreground: '2e5e8a' }, + { token: 'attribute.name', foreground: '7eb09b' }, + { token: 'attribute.value', foreground: '52ad5a' }, ], colors: { - background: '#faf8f0', - foreground: '#1a1a1a', - lineHighlight: '#f5f3e8', - selection: 'rgba(46, 94, 138, 0.28)', - cursor: '#2e5e8a', - - 'editor.selectionBackground': 'rgba(46, 94, 138, 0.28)', - 'editor.selectionForeground': '#1a1a1a', - 'editor.inactiveSelectionBackground': 'rgba(46, 94, 138, 0.18)', - 'editor.selectionHighlightBackground': 'rgba(46, 94, 138, 0.2)', - 'editor.selectionHighlightBorder': 'rgba(46, 94, 138, 0.35)', - 'editorCursor.foreground': '#2e5e8a', - 'editor.wordHighlightBackground': 'rgba(46, 94, 138, 0.12)', - 'editor.wordHighlightStrongBackground': 'rgba(46, 94, 138, 0.22)', + background: CHINA_STYLE_PAPER, + foreground: CHINA_STYLE_INK, + lineHighlight: '#f5f3e8', + selection: chinaStyleBlue(0.28), + cursor: CHINA_STYLE_BLUE, + + 'editor.selectionBackground': chinaStyleBlue(0.28), + 'editor.selectionForeground': CHINA_STYLE_INK, + 'editor.inactiveSelectionBackground': chinaStyleBlue(0.18), + 'editor.selectionHighlightBackground': chinaStyleBlue(0.2), + 'editor.selectionHighlightBorder': chinaStyleBlue(0.35), + 'editorCursor.foreground': CHINA_STYLE_BLUE, + 'editor.wordHighlightBackground': chinaStyleBlue(0.12), + 'editor.wordHighlightStrongBackground': chinaStyleBlue(0.22), }, }, }; 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 5537154e77..5e14d66413 100644 --- a/src/web-ui/src/infrastructure/theme/presets/cyber-theme.ts +++ b/src/web-ui/src/infrastructure/theme/presets/cyber-theme.ts @@ -1,4 +1,4 @@ - + import { ThemeConfig } from '../types'; import { @@ -8,113 +8,137 @@ import { createStandardEasing, createStandardSpacing, createWindowControls, + overlayBlack, + rgbFromHex, + rgbaFromHex, + STATIC_WHITE, } from './shared'; +const CYBER_BACKGROUND = '#101010'; +const CYBER_TEXT_PRIMARY = '#e0f2ff'; +const CYBER_TEXT_SECONDARY = '#c7e7ff'; +const CYBER_TEXT_MUTED = '#7fadcc'; +const CYBER_ACCENT = '#00e6ff'; +const CYBER_ACCENT_HOVER = '#00ccff'; +const CYBER_PURPLE = '#8a2be2'; +const CYBER_PURPLE_HOVER = '#7928ca'; +const CYBER_SUCCESS = '#00ff9f'; +const CYBER_WARNING = '#ffcc00'; +const CYBER_ERROR = '#ff0055'; + +const cyberAccent = (alpha: number | string) => rgbaFromHex(CYBER_ACCENT, alpha); +const cyberAccentHover = (alpha: number | string) => rgbaFromHex(CYBER_ACCENT_HOVER, alpha); +const cyberPurple = (alpha: number | string) => rgbaFromHex(CYBER_PURPLE, alpha); +const cyberPurpleHover = (alpha: number | string) => rgbaFromHex(CYBER_PURPLE_HOVER, 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 = { - + id: 'bitfun-cyber', name: 'Cyber', type: 'dark', description: 'Tech-style theme - Deep black hole, neon future, ultimate tech aesthetics', author: 'BitFun Team', version: '1.0.0', - - + + colors: { background: { - primary: '#101010', - secondary: '#151515', - tertiary: '#1a1a1a', - quaternary: '#1f1f1f', - elevated: '#0d0d0d', - workbench: '#101010', + primary: CYBER_BACKGROUND, + secondary: '#151515', + tertiary: '#1a1a1a', + quaternary: '#1f1f1f', + elevated: '#0d0d0d', + workbench: CYBER_BACKGROUND, scene: '#141414', tooltip: 'rgba(16, 16, 16, 0.95)', }, - + text: { - primary: '#e0f2ff', - secondary: '#c7e7ff', - muted: '#7fadcc', - disabled: '#4a5a66', + primary: CYBER_TEXT_PRIMARY, + secondary: CYBER_TEXT_SECONDARY, + muted: CYBER_TEXT_MUTED, + disabled: '#4a5a66', }, - + accent: { - 50: 'rgba(0, 230, 255, 0.05)', - 100: 'rgba(0, 230, 255, 0.1)', - 200: 'rgba(0, 230, 255, 0.18)', - 300: 'rgba(0, 230, 255, 0.3)', - 400: 'rgba(0, 230, 255, 0.45)', - 500: '#00e6ff', - 600: '#00ccff', - 700: 'rgba(0, 204, 255, 0.85)', - 800: 'rgba(0, 204, 255, 0.95)', + 50: cyberAccent(0.05), + 100: cyberAccent(0.1), + 200: cyberAccent(0.18), + 300: cyberAccent(0.3), + 400: cyberAccent(0.45), + 500: CYBER_ACCENT, + 600: CYBER_ACCENT_HOVER, + 700: cyberAccentHover(0.85), + 800: cyberAccentHover(0.95), }, - + purple: { - 50: 'rgba(138, 43, 226, 0.05)', - 100: 'rgba(138, 43, 226, 0.1)', - 200: 'rgba(138, 43, 226, 0.18)', - 300: 'rgba(138, 43, 226, 0.3)', - 400: 'rgba(138, 43, 226, 0.45)', - 500: '#8a2be2', - 600: '#7928ca', - 700: 'rgba(121, 40, 202, 0.85)', - 800: 'rgba(121, 40, 202, 0.95)', + 50: cyberPurple(0.05), + 100: cyberPurple(0.1), + 200: cyberPurple(0.18), + 300: cyberPurple(0.3), + 400: cyberPurple(0.45), + 500: CYBER_PURPLE, + 600: CYBER_PURPLE_HOVER, + 700: cyberPurpleHover(0.85), + 800: cyberPurpleHover(0.95), }, - + semantic: { - success: '#00ff9f', - successBg: 'rgba(0, 255, 159, 0.12)', - successBorder: 'rgba(0, 255, 159, 0.35)', - - warning: '#ffcc00', - warningBg: 'rgba(255, 204, 0, 0.12)', - warningBorder: 'rgba(255, 204, 0, 0.35)', - - error: '#ff0055', - errorBg: 'rgba(255, 0, 85, 0.12)', - errorBorder: 'rgba(255, 0, 85, 0.35)', - - info: '#00e6ff', - infoBg: 'rgba(0, 230, 255, 0.12)', - infoBorder: 'rgba(0, 230, 255, 0.35)', - - + success: CYBER_SUCCESS, + successBg: cyberSuccess(0.12), + successBorder: cyberSuccess(0.35), + + warning: CYBER_WARNING, + warningBg: cyberWarning(0.12), + warningBorder: cyberWarning(0.35), + + error: CYBER_ERROR, + errorBg: cyberError(0.12), + errorBorder: cyberError(0.35), + + info: CYBER_ACCENT, + infoBg: cyberAccent(0.12), + infoBorder: cyberAccent(0.35), + + highlight: '#ffdd44', highlightBg: 'rgba(255, 221, 68, 0.15)', }, - + border: { - subtle: 'rgba(0, 230, 255, 0.14)', - base: 'rgba(0, 230, 255, 0.2)', - medium: 'rgba(0, 230, 255, 0.28)', - strong: 'rgba(0, 230, 255, 0.36)', - prominent: 'rgba(0, 230, 255, 0.5)', + subtle: cyberAccent(0.14), + base: cyberAccent(0.2), + medium: cyberAccent(0.28), + strong: cyberAccent(0.36), + prominent: cyberAccent(0.5), }, - + element: { - subtle: 'rgba(0, 230, 255, 0.06)', - soft: 'rgba(0, 230, 255, 0.09)', - base: 'rgba(0, 230, 255, 0.13)', - medium: 'rgba(0, 230, 255, 0.17)', - strong: 'rgba(0, 230, 255, 0.22)', - elevated: 'rgba(0, 230, 255, 0.27)', + subtle: cyberAccent(0.06), + soft: cyberAccent(0.09), + base: cyberAccent(0.13), + medium: cyberAccent(0.17), + strong: cyberAccent(0.22), + elevated: cyberAccent(0.27), }, - + git: createGitColors({ - branch: 'rgb(0, 230, 255)', - branchBg: 'rgba(0, 230, 255, 0.12)', - changes: 'rgb(255, 204, 0)', - changesBg: 'rgba(255, 204, 0, 0.12)', - added: 'rgb(0, 255, 159)', - addedBg: 'rgba(0, 255, 159, 0.12)', - deleted: 'rgb(255, 0, 85)', - deletedBg: 'rgba(255, 0, 85, 0.12)', + 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), }), }, - - + + effects: { shadow: { xs: '0 1px 3px rgba(0, 0, 0, 0.9)', @@ -124,16 +148,16 @@ export const bitfunCyberTheme: ThemeConfig = { xl: '0 12px 28px rgba(0, 0, 0, 0.7)', '2xl': '0 16px 36px rgba(0, 0, 0, 0.65)', }, - + glow: { - - blue: '0 0 12px rgba(0, 230, 255, 0.4), 0 0 24px rgba(0, 230, 255, 0.25), 0 0 36px rgba(0, 230, 255, 0.15), 0 4px 16px rgba(0, 0, 0, 0.3)', - - purple: '0 0 12px rgba(138, 43, 226, 0.4), 0 0 24px rgba(138, 43, 226, 0.25), 0 0 36px rgba(138, 43, 226, 0.15), 0 4px 16px rgba(0, 0, 0, 0.3)', - - mixed: '0 0 16px rgba(0, 230, 255, 0.35), 0 0 28px rgba(138, 43, 226, 0.25), 0 0 40px rgba(0, 230, 255, 0.12), 0 4px 20px rgba(0, 0, 0, 0.35)', + + blue: `0 0 12px ${cyberAccent(0.4)}, 0 0 24px ${cyberAccent(0.25)}, 0 0 36px ${cyberAccent(0.15)}, 0 4px 16px ${overlayBlack(0.3)}`, + + purple: `0 0 12px ${cyberPurple(0.4)}, 0 0 24px ${cyberPurple(0.25)}, 0 0 36px ${cyberPurple(0.15)}, 0 4px 16px ${overlayBlack(0.3)}`, + + mixed: `0 0 16px ${cyberAccent(0.35)}, 0 0 28px ${cyberPurple(0.25)}, 0 0 40px ${cyberAccent(0.12)}, 0 4px 20px ${overlayBlack(0.35)}`, }, - + blur: { subtle: 'blur(4px) saturate(1.2)', base: 'blur(8px) saturate(1.3)', @@ -141,11 +165,11 @@ export const bitfunCyberTheme: ThemeConfig = { strong: 'blur(16px) saturate(1.5) brightness(1.15)', intense: 'blur(20px) saturate(1.6) brightness(1.2)', }, - + radius: createCompactRadius(), - + spacing: createStandardSpacing(), - + opacity: { disabled: 0.5, hover: 0.85, @@ -153,8 +177,8 @@ export const bitfunCyberTheme: ThemeConfig = { overlay: 0.5, }, }, - - + + motion: { duration: { instant: '0.08s', @@ -163,125 +187,125 @@ export const bitfunCyberTheme: ThemeConfig = { slow: '0.5s', lazy: '0.8s', }, - + easing: createStandardEasing('cubic-bezier(0.25, 0.46, 0.45, 0.94)'), }, - - + + typography: createExpressiveTypography(), - - + + components: { - + windowControls: createWindowControls({ standard: { - dot: 'rgba(0, 230, 255, 0.5)', - dotShadow: '0 0 6px rgba(0, 230, 255, 0.35)', - hoverBg: 'rgba(0, 230, 255, 0.15)', - hoverColor: '#00e6ff', - hoverBorder: 'rgba(0, 230, 255, 0.3)', - hoverShadow: '0 0 12px rgba(0, 230, 255, 0.3), 0 2px 8px rgba(0, 230, 255, 0.2), inset 0 1px 0 rgba(0, 230, 255, 0.2)', + dot: cyberAccent(0.5), + dotShadow: `0 0 6px ${cyberAccent(0.35)}`, + hoverBg: cyberAccent(0.15), + hoverColor: CYBER_ACCENT, + hoverBorder: cyberAccent(0.3), + hoverShadow: `0 0 12px ${cyberAccent(0.3)}, 0 2px 8px ${cyberAccent(0.2)}, inset 0 1px 0 ${cyberAccent(0.2)}`, }, close: { - dot: 'rgba(255, 0, 85, 0.5)', - dotShadow: '0 0 6px rgba(255, 0, 85, 0.35)', - hoverBg: 'rgba(255, 0, 85, 0.15)', - hoverColor: '#ff0055', - hoverBorder: 'rgba(255, 0, 85, 0.3)', - hoverShadow: '0 0 12px rgba(255, 0, 85, 0.3), 0 2px 8px rgba(255, 0, 85, 0.2), inset 0 1px 0 rgba(255, 0, 85, 0.2)', + dot: cyberError(0.5), + dotShadow: `0 0 6px ${cyberError(0.35)}`, + hoverBg: cyberError(0.15), + hoverColor: CYBER_ERROR, + hoverBorder: cyberError(0.3), + hoverShadow: `0 0 12px ${cyberError(0.3)}, 0 2px 8px ${cyberError(0.2)}, inset 0 1px 0 ${cyberError(0.2)}`, }, common: { - defaultColor: 'rgba(224, 242, 255, 0.9)', - defaultDot: 'rgba(0, 230, 255, 0.2)', - disabledDot: 'rgba(0, 230, 255, 0.1)', - flowGradient: 'linear-gradient(90deg, transparent, rgba(0, 230, 255, 0.08), rgba(0, 230, 255, 0.12), rgba(0, 230, 255, 0.08), transparent)', + defaultColor: rgbaFromHex(CYBER_TEXT_PRIMARY, 0.9), + defaultDot: cyberAccent(0.2), + disabledDot: cyberAccent(0.1), + flowGradient: `linear-gradient(90deg, transparent, ${cyberAccent(0.08)}, ${cyberAccent(0.12)}, ${cyberAccent(0.08)}, transparent)`, }, }), - + button: { - + default: { - background: 'rgba(0, 230, 255, 0.08)', - color: '#7fadcc', - border: 'rgba(0, 230, 255, 0.15)', - shadow: '0 0 8px rgba(0, 230, 255, 0.1)', + background: cyberAccent(0.08), + color: CYBER_TEXT_MUTED, + border: cyberAccent(0.15), + shadow: `0 0 8px ${cyberAccent(0.1)}`, }, hover: { - background: 'rgba(0, 230, 255, 0.14)', - color: '#c7e7ff', - border: 'rgba(0, 230, 255, 0.3)', - shadow: '0 0 16px rgba(0, 230, 255, 0.2), 0 2px 8px rgba(0, 0, 0, 0.3)', + background: cyberAccent(0.14), + color: CYBER_TEXT_SECONDARY, + border: cyberAccent(0.3), + shadow: `0 0 16px ${cyberAccent(0.2)}, 0 2px 8px ${overlayBlack(0.3)}`, transform: 'translateY(-1px)', }, active: { - background: 'rgba(0, 230, 255, 0.12)', - color: '#c7e7ff', - border: 'rgba(0, 230, 255, 0.35)', - shadow: '0 0 12px rgba(0, 230, 255, 0.15)', + background: cyberAccent(0.12), + color: CYBER_TEXT_SECONDARY, + border: cyberAccent(0.35), + shadow: `0 0 12px ${cyberAccent(0.15)}`, transform: 'translateY(0)', }, - - + + primary: { default: { - background: 'rgba(0, 230, 255, 0.18)', - color: '#e0f2ff', - border: 'rgba(0, 230, 255, 0.4)', - shadow: '0 0 16px rgba(0, 230, 255, 0.25)', + background: cyberAccent(0.18), + color: CYBER_TEXT_PRIMARY, + border: cyberAccent(0.4), + shadow: `0 0 16px ${cyberAccent(0.25)}`, }, hover: { - background: 'rgba(0, 230, 255, 0.25)', - color: '#ffffff', - border: 'rgba(0, 230, 255, 0.6)', - shadow: '0 0 24px rgba(0, 230, 255, 0.4), 0 0 36px rgba(0, 230, 255, 0.2), 0 4px 12px rgba(0, 0, 0, 0.3)', + background: cyberAccent(0.25), + color: STATIC_WHITE, + border: cyberAccent(0.6), + shadow: `0 0 24px ${cyberAccent(0.4)}, 0 0 36px ${cyberAccent(0.2)}, 0 4px 12px ${overlayBlack(0.3)}`, transform: 'translateY(-2px)', }, active: { - background: 'rgba(0, 230, 255, 0.22)', - color: '#ffffff', - border: 'rgba(0, 230, 255, 0.5)', - shadow: '0 0 20px rgba(0, 230, 255, 0.3)', + background: cyberAccent(0.22), + color: STATIC_WHITE, + border: cyberAccent(0.5), + shadow: `0 0 20px ${cyberAccent(0.3)}`, transform: 'translateY(-1px)', }, }, - - + + ghost: { default: { background: 'transparent', - color: '#7fadcc', - border: 'rgba(0, 230, 255, 0.2)', + color: CYBER_TEXT_MUTED, + border: cyberAccent(0.2), shadow: 'none', }, hover: { - background: 'rgba(0, 230, 255, 0.1)', - color: '#c7e7ff', - border: 'rgba(0, 230, 255, 0.35)', - shadow: '0 0 12px rgba(0, 230, 255, 0.15)', + background: cyberAccent(0.1), + color: CYBER_TEXT_SECONDARY, + border: cyberAccent(0.35), + shadow: `0 0 12px ${cyberAccent(0.15)}`, transform: 'translateY(-1px)', }, active: { - background: 'rgba(0, 230, 255, 0.08)', - color: '#c7e7ff', - border: 'rgba(0, 230, 255, 0.3)', - shadow: '0 0 8px rgba(0, 230, 255, 0.1)', + background: cyberAccent(0.08), + color: CYBER_TEXT_SECONDARY, + border: cyberAccent(0.3), + shadow: `0 0 8px ${cyberAccent(0.1)}`, transform: 'translateY(0)', }, }, }, }, - - + + monaco: { base: 'vs-dark', inherit: true, rules: [], colors: { - background: '#101010', - foreground: '#c7e7ff', + background: CYBER_BACKGROUND, + foreground: CYBER_TEXT_SECONDARY, lineHighlight: '#151515', selection: '#1a4d66', - cursor: '#00e6ff', + cursor: CYBER_ACCENT, }, }, }; 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 34a75da1c5..8a99abab4d 100644 --- a/src/web-ui/src/infrastructure/theme/presets/dark-theme.ts +++ b/src/web-ui/src/infrastructure/theme/presets/dark-theme.ts @@ -1,4 +1,4 @@ - + import { ThemeConfig } from '../types'; import { @@ -11,118 +11,142 @@ import { createStandardSpacing, createStandardTypography, createWindowControls, + overlayBlack, + overlayWhite, + rgbFromHex, + rgbaFromHex, } from './shared'; +const DARK_BACKGROUND_PRIMARY = '#0e0e10'; +const DARK_BACKGROUND_SECONDARY = '#1c1c1f'; +const DARK_TEXT_PRIMARY = '#e8e8e8'; +const DARK_BUTTON_TEXT = '#c8c8c8'; +const DARK_ACCENT = '#60a5fa'; +const DARK_ACCENT_HOVER = '#3b82f6'; +const DARK_PURPLE = '#8b5cf6'; +const DARK_PURPLE_HOVER = '#7c3aed'; +const DARK_SUCCESS = '#34d399'; +const DARK_WARNING = '#f59e0b'; +const DARK_ERROR = '#ef4444'; + +const darkAccent = (alpha: number | string) => rgbaFromHex(DARK_ACCENT, alpha); +const darkAccentHover = (alpha: number | string) => rgbaFromHex(DARK_ACCENT_HOVER, alpha); +const darkPurple = (alpha: number | string) => rgbaFromHex(DARK_PURPLE, alpha); +const darkPurpleHover = (alpha: number | string) => rgbaFromHex(DARK_PURPLE_HOVER, alpha); +const darkSuccess = (alpha: number | string) => rgbaFromHex(DARK_SUCCESS, alpha); +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', name: 'Dark', type: 'dark', description: 'Default dark theme', author: 'BitFun Team', version: '2.1.0', - - + + colors: { background: { - primary: '#0e0e10', - secondary: '#1c1c1f', - tertiary: '#0e0e10', + primary: DARK_BACKGROUND_PRIMARY, + secondary: DARK_BACKGROUND_SECONDARY, + tertiary: DARK_BACKGROUND_PRIMARY, quaternary: '#252528', - elevated: '#1c1c1f', - workbench: '#0e0e10', - scene: '#1c1c1f', + elevated: DARK_BACKGROUND_SECONDARY, + workbench: DARK_BACKGROUND_PRIMARY, + scene: DARK_BACKGROUND_SECONDARY, tooltip: 'rgba(28, 28, 31, 0.96)', }, - + text: { - primary: '#e8e8e8', + primary: DARK_TEXT_PRIMARY, secondary: '#b0b0b0', muted: '#858585', disabled: '#555555', }, - + accent: { - 50: 'rgba(96, 165, 250, 0.04)', - 100: 'rgba(96, 165, 250, 0.08)', - 200: 'rgba(96, 165, 250, 0.15)', - 300: 'rgba(96, 165, 250, 0.25)', - 400: 'rgba(96, 165, 250, 0.4)', - 500: '#60a5fa', - 600: '#3b82f6', - 700: 'rgba(59, 130, 246, 0.8)', - 800: 'rgba(59, 130, 246, 0.9)', + 50: darkAccent(0.04), + 100: darkAccent(0.08), + 200: darkAccent(0.15), + 300: darkAccent(0.25), + 400: darkAccent(0.4), + 500: DARK_ACCENT, + 600: DARK_ACCENT_HOVER, + 700: darkAccentHover(0.8), + 800: darkAccentHover(0.9), }, - + purple: { - 50: 'rgba(139, 92, 246, 0.04)', - 100: 'rgba(139, 92, 246, 0.08)', - 200: 'rgba(139, 92, 246, 0.15)', - 300: 'rgba(139, 92, 246, 0.25)', - 400: 'rgba(139, 92, 246, 0.4)', - 500: '#8b5cf6', - 600: '#7c3aed', - 700: 'rgba(124, 58, 237, 0.8)', - 800: 'rgba(124, 58, 237, 0.9)', + 50: darkPurple(0.04), + 100: darkPurple(0.08), + 200: darkPurple(0.15), + 300: darkPurple(0.25), + 400: darkPurple(0.4), + 500: DARK_PURPLE, + 600: DARK_PURPLE_HOVER, + 700: darkPurpleHover(0.8), + 800: darkPurpleHover(0.9), }, - + semantic: { - success: '#34d399', - successBg: 'rgba(52, 211, 153, 0.1)', - successBorder: 'rgba(52, 211, 153, 0.3)', - - warning: '#f59e0b', - warningBg: 'rgba(245, 158, 11, 0.1)', - warningBorder: 'rgba(245, 158, 11, 0.3)', - - error: '#ef4444', - errorBg: 'rgba(239, 68, 68, 0.1)', - errorBorder: 'rgba(239, 68, 68, 0.3)', - + success: DARK_SUCCESS, + successBg: darkSuccess(0.1), + successBorder: darkSuccess(0.3), + + warning: DARK_WARNING, + warningBg: darkWarning(0.1), + warningBorder: darkWarning(0.3), + + error: DARK_ERROR, + errorBg: darkError(0.1), + errorBorder: darkError(0.3), + info: '#a1a1aa', - infoBg: 'rgba(255, 255, 255, 0.08)', - infoBorder: 'rgba(255, 255, 255, 0.22)', - - + infoBg: overlayWhite(0.08), + infoBorder: overlayWhite(0.22), + + highlight: '#a8a8a8', - highlightBg: 'rgba(255, 255, 255, 0.1)', + highlightBg: overlayWhite(0.1), }, - + border: createDarkNeutralBorder(), - + element: createDarkNeutralElement(), - + git: createGitColors({ branch: '#a1a1aa', - branchBg: 'rgba(255, 255, 255, 0.06)', - changes: 'rgb(245, 158, 11)', - changesBg: 'rgba(245, 158, 11, 0.1)', + 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: 'rgb(239, 68, 68)', - deletedBg: 'rgba(239, 68, 68, 0.1)', + deleted: rgbFromHex(DARK_ERROR), + deletedBg: darkError(0.1), }), - + scrollbar: createDarkNeutralScrollbar(), }, - - + + effects: { shadow: { - xs: '0 1px 2px rgba(0, 0, 0, 0.9)', - sm: '0 2px 4px rgba(0, 0, 0, 0.8)', - base: '0 4px 8px rgba(0, 0, 0, 0.7)', - lg: '0 8px 16px rgba(0, 0, 0, 0.6)', - xl: '0 12px 24px rgba(0, 0, 0, 0.5)', - '2xl': '0 16px 32px rgba(0, 0, 0, 0.4)', + xs: `0 1px 2px ${overlayBlack(0.9)}`, + sm: `0 2px 4px ${overlayBlack(0.8)}`, + base: `0 4px 8px ${overlayBlack(0.7)}`, + lg: `0 8px 16px ${overlayBlack(0.6)}`, + xl: `0 12px 24px ${overlayBlack(0.5)}`, + '2xl': `0 16px 32px ${overlayBlack(0.4)}`, }, - + glow: { - blue: '0 12px 32px rgba(96, 165, 250, 0.2), 0 6px 16px rgba(96, 165, 250, 0.12), 0 3px 8px rgba(0, 0, 0, 0.12)', - purple: '0 12px 32px rgba(139, 92, 246, 0.22), 0 6px 16px rgba(124, 58, 237, 0.14), 0 3px 8px rgba(0, 0, 0, 0.12)', - mixed: '0 12px 32px rgba(255, 255, 255, 0.06), 0 6px 16px rgba(139, 92, 246, 0.12), 0 3px 8px rgba(0, 0, 0, 0.12)', + blue: `0 12px 32px ${darkAccent(0.2)}, 0 6px 16px ${darkAccent(0.12)}, 0 3px 8px ${overlayBlack(0.12)}`, + purple: `0 12px 32px ${darkPurple(0.22)}, 0 6px 16px ${darkPurpleHover(0.14)}, 0 3px 8px ${overlayBlack(0.12)}`, + mixed: `0 12px 32px ${overlayWhite(0.06)}, 0 6px 16px ${darkPurple(0.12)}, 0 3px 8px ${overlayBlack(0.12)}`, }, - + blur: { subtle: 'blur(4px) saturate(1.05)', base: 'blur(8px) saturate(1.1)', @@ -130,11 +154,11 @@ export const bitfunDarkTheme: ThemeConfig = { strong: 'blur(16px) saturate(1.3) brightness(1.1)', intense: 'blur(20px) saturate(1.4) brightness(1.15)', }, - + radius: createStandardRadius(), - + spacing: createStandardSpacing(), - + opacity: { disabled: 0.6, hover: 0.8, @@ -142,8 +166,8 @@ export const bitfunDarkTheme: ThemeConfig = { overlay: 0.4, }, }, - - + + motion: { duration: { instant: '0.1s', @@ -152,89 +176,89 @@ export const bitfunDarkTheme: ThemeConfig = { slow: '0.6s', lazy: '1s', }, - + easing: createStandardEasing(), }, - - + + typography: createStandardTypography(), - - + + components: { - + windowControls: createWindowControls({ standard: { - dot: 'rgba(255, 255, 255, 0.38)', - dotShadow: '0 0 4px rgba(0, 0, 0, 0.35)', - hoverBg: 'rgba(255, 255, 255, 0.1)', + dot: overlayWhite(0.38), + dotShadow: `0 0 4px ${overlayBlack(0.35)}`, + hoverBg: overlayWhite(0.1), hoverColor: '#e4e4e4', - hoverBorder: 'rgba(255, 255, 255, 0.16)', - hoverShadow: '0 2px 8px rgba(0, 0, 0, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.08)', + hoverBorder: overlayWhite(0.16), + hoverShadow: `0 2px 8px ${overlayBlack(0.25)}, inset 0 1px 0 ${overlayWhite(0.08)}`, }, close: { - dot: 'rgba(239, 68, 68, 0.45)', - dotShadow: '0 0 4px rgba(239, 68, 68, 0.2)', - hoverBg: 'rgba(239, 68, 68, 0.12)', - hoverColor: '#ef4444', - hoverBorder: 'rgba(239, 68, 68, 0.2)', - hoverShadow: '0 2px 8px rgba(239, 68, 68, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1)', + dot: darkError(0.45), + dotShadow: `0 0 4px ${darkError(0.2)}`, + hoverBg: darkError(0.12), + hoverColor: DARK_ERROR, + hoverBorder: darkError(0.2), + hoverShadow: `0 2px 8px ${darkError(0.15)}, inset 0 1px 0 ${overlayWhite(0.1)}`, }, common: { - defaultColor: 'rgba(232, 232, 232, 0.9)', - defaultDot: 'rgba(255, 255, 255, 0.2)', - disabledDot: 'rgba(255, 255, 255, 0.1)', - flowGradient: 'linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.05), transparent)', + defaultColor: rgbaFromHex(DARK_TEXT_PRIMARY, 0.9), + defaultDot: overlayWhite(0.2), + disabledDot: overlayWhite(0.1), + flowGradient: `linear-gradient(90deg, transparent, ${overlayWhite(0.05)}, ${overlayWhite(0.08)}, ${overlayWhite(0.05)}, transparent)`, }, }), - + button: { - + default: { - background: 'rgba(255, 255, 255, 0.08)', + background: overlayWhite(0.08), color: '#9a9a9a', border: 'transparent', shadow: 'none', }, hover: { - background: 'rgba(255, 255, 255, 0.14)', - color: '#c8c8c8', + background: overlayWhite(0.14), + color: DARK_BUTTON_TEXT, border: 'transparent', shadow: 'none', transform: 'none', }, active: { - background: 'rgba(255, 255, 255, 0.1)', - color: '#c8c8c8', + background: overlayWhite(0.1), + color: DARK_BUTTON_TEXT, border: 'transparent', shadow: 'none', transform: 'none', }, - - + + primary: { default: { - background: 'rgba(255, 255, 255, 0.16)', + background: overlayWhite(0.16), color: '#f4f4f5', border: 'transparent', shadow: 'none', }, hover: { - background: 'rgba(255, 255, 255, 0.24)', + background: overlayWhite(0.24), color: '#fafafa', border: 'transparent', shadow: 'none', transform: 'none', }, active: { - background: 'rgba(255, 255, 255, 0.2)', + background: overlayWhite(0.2), color: '#fafafa', border: 'transparent', shadow: 'none', transform: 'none', }, }, - - + + ghost: { default: { background: 'transparent', @@ -243,15 +267,15 @@ export const bitfunDarkTheme: ThemeConfig = { shadow: 'none', }, hover: { - background: 'rgba(255, 255, 255, 0.1)', - color: '#c8c8c8', + background: overlayWhite(0.1), + color: DARK_BUTTON_TEXT, border: 'transparent', shadow: 'none', transform: 'none', }, active: { - background: 'rgba(255, 255, 255, 0.07)', - color: '#c8c8c8', + background: overlayWhite(0.07), + color: DARK_BUTTON_TEXT, border: 'transparent', shadow: 'none', transform: 'none', @@ -259,19 +283,19 @@ export const bitfunDarkTheme: ThemeConfig = { }, }, }, - - - - + + + + monaco: { base: 'vs-dark', inherit: true, - rules: [], + rules: [], colors: { background: '#121214', - foreground: '#e8e8e8', + foreground: DARK_TEXT_PRIMARY, lineHighlight: '#18181a', - selection: 'rgba(255, 255, 255, 0.12)', + selection: overlayWhite(0.12), cursor: '#c4c4c4', }, }, 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 f355a75dc5..0409599736 100644 --- a/src/web-ui/src/infrastructure/theme/presets/light-theme.ts +++ b/src/web-ui/src/infrastructure/theme/presets/light-theme.ts @@ -1,4 +1,4 @@ - + import { ThemeConfig } from '../types'; import { @@ -8,10 +8,35 @@ import { createStandardSpacing, createStandardTypography, createWindowControls, + rgbFromHex, + rgbaFromHex, + STATIC_BLACK, + STATIC_WHITE, } from './shared'; +const LIGHT_INK = '#0f172a'; +const LIGHT_TEXT_PRIMARY = '#1e293b'; +const LIGHT_TEXT_STRONG = '#334155'; +const LIGHT_ACCENT = '#64748b'; +const LIGHT_ACCENT_HOVER = '#475569'; +const LIGHT_PURPLE = '#7c6b99'; +const LIGHT_PURPLE_HOVER = '#655680'; +const LIGHT_SUCCESS = '#5b9a6f'; +const LIGHT_WARNING = '#c08c42'; +const LIGHT_ERROR = '#c26565'; +const LIGHT_HIGHLIGHT = '#b8863a'; + +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 lightPurpleBase = (alpha: number | string) => rgbaFromHex('#6b5a89', alpha); +const lightPurpleHover = (alpha: number | string) => rgbaFromHex(LIGHT_PURPLE_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 = { - + id: 'bitfun-light', name: 'Light', type: 'light', @@ -22,127 +47,127 @@ export const bitfunLightTheme: ThemeConfig = { layout: { sceneViewportBorder: false, }, - - + + colors: { background: { primary: '#f3f3f5', - secondary: '#ffffff', - tertiary: '#e8eaee', - quaternary: '#e0e3e8', - elevated: '#ffffff', - workbench: '#eceef1', - scene: '#ffffff', - tooltip: 'rgba(255, 255, 255, 0.98)', + secondary: STATIC_WHITE, + tertiary: '#e8eaee', + quaternary: '#e0e3e8', + elevated: STATIC_WHITE, + workbench: '#eceef1', + scene: STATIC_WHITE, + tooltip: rgbaFromHex(STATIC_WHITE, 0.98), }, - + text: { - primary: '#1e293b', - secondary: '#3d4f66', - muted: '#64748b', - disabled: '#94a3b8', + primary: LIGHT_TEXT_PRIMARY, + secondary: '#3d4f66', + muted: LIGHT_ACCENT, + disabled: '#94a3b8', }, - - + + accent: { - 50: 'rgba(15, 23, 42, 0.04)', - 100: 'rgba(15, 23, 42, 0.07)', - 200: 'rgba(15, 23, 42, 0.1)', - 300: 'rgba(15, 23, 42, 0.16)', - 400: 'rgba(15, 23, 42, 0.26)', - 500: '#64748b', - 600: '#475569', - 700: 'rgba(71, 85, 105, 0.88)', - 800: 'rgba(51, 65, 85, 0.94)', + 50: lightInk(0.04), + 100: lightInk(0.07), + 200: lightInk(0.1), + 300: lightInk(0.16), + 400: lightInk(0.26), + 500: LIGHT_ACCENT, + 600: LIGHT_ACCENT_HOVER, + 700: lightAccentHover(0.88), + 800: rgbaFromHex(LIGHT_TEXT_STRONG, 0.94), }, - - + + purple: { - 50: 'rgba(107, 90, 137, 0.04)', - 100: 'rgba(107, 90, 137, 0.08)', - 200: 'rgba(107, 90, 137, 0.14)', - 300: 'rgba(107, 90, 137, 0.22)', - 400: 'rgba(107, 90, 137, 0.36)', - 500: '#7c6b99', - 600: '#655680', - 700: 'rgba(101, 86, 128, 0.8)', - 800: 'rgba(101, 86, 128, 0.9)', + 50: lightPurpleBase(0.04), + 100: lightPurpleBase(0.08), + 200: lightPurpleBase(0.14), + 300: lightPurpleBase(0.22), + 400: lightPurpleBase(0.36), + 500: LIGHT_PURPLE, + 600: LIGHT_PURPLE_HOVER, + 700: lightPurpleHover(0.8), + 800: lightPurpleHover(0.9), }, - - + + semantic: { - success: '#5b9a6f', - successBg: 'rgba(91, 154, 111, 0.08)', - successBorder: 'rgba(91, 154, 111, 0.25)', - - warning: '#c08c42', - warningBg: 'rgba(192, 140, 66, 0.08)', - warningBorder: 'rgba(192, 140, 66, 0.25)', - - error: '#c26565', - errorBg: 'rgba(194, 101, 101, 0.08)', - errorBorder: 'rgba(194, 101, 101, 0.25)', - - info: '#64748b', - infoBg: 'rgba(100, 116, 139, 0.1)', - infoBorder: 'rgba(100, 116, 139, 0.28)', - - - highlight: '#b8863a', - highlightBg: 'rgba(184, 134, 58, 0.12)', + success: LIGHT_SUCCESS, + successBg: lightSuccess(0.08), + successBorder: lightSuccess(0.25), + + warning: LIGHT_WARNING, + warningBg: lightWarning(0.08), + warningBorder: lightWarning(0.25), + + error: LIGHT_ERROR, + errorBg: lightError(0.08), + errorBorder: lightError(0.25), + + info: LIGHT_ACCENT, + infoBg: lightAccent(0.1), + infoBorder: lightAccent(0.28), + + + highlight: LIGHT_HIGHLIGHT, + highlightBg: rgbaFromHex(LIGHT_HIGHLIGHT, 0.12), }, - - + + border: { - subtle: 'rgba(100, 116, 139, 0.15)', - base: 'rgba(100, 116, 139, 0.22)', - medium: 'rgba(100, 116, 139, 0.32)', - strong: 'rgba(100, 116, 139, 0.42)', - prominent: 'rgba(100, 116, 139, 0.52)', + subtle: lightAccent(0.15), + base: lightAccent(0.22), + medium: lightAccent(0.32), + strong: lightAccent(0.42), + prominent: lightAccent(0.52), }, - - + + element: { - subtle: 'rgba(15, 23, 42, 0.045)', - soft: 'rgba(15, 23, 42, 0.065)', - base: 'rgba(15, 23, 42, 0.09)', - medium: 'rgba(15, 23, 42, 0.12)', - strong: 'rgba(15, 23, 42, 0.16)', - elevated: 'rgba(255, 255, 255, 0.92)', + subtle: lightInk(0.045), + soft: lightInk(0.065), + base: lightInk(0.09), + medium: lightInk(0.12), + strong: lightInk(0.16), + elevated: rgbaFromHex(STATIC_WHITE, 0.92), }, - - + + git: createGitColors({ - branch: 'rgb(71, 85, 105)', - branchBg: 'rgba(71, 85, 105, 0.1)', - changes: 'rgb(192, 140, 66)', - changesBg: 'rgba(192, 140, 66, 0.08)', - added: 'rgb(91, 154, 111)', - addedBg: 'rgba(91, 154, 111, 0.08)', - deleted: 'rgb(194, 101, 101)', - deletedBg: 'rgba(194, 101, 101, 0.08)', + 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), }), }, - - + + effects: { shadow: { - - xs: '0 1px 2px rgba(71, 85, 105, 0.06)', - sm: '0 2px 4px rgba(71, 85, 105, 0.08)', - base: '0 4px 8px rgba(71, 85, 105, 0.1)', - lg: '0 8px 16px rgba(71, 85, 105, 0.12)', - xl: '0 12px 24px rgba(71, 85, 105, 0.14)', - '2xl': '0 16px 32px rgba(71, 85, 105, 0.16)', + + xs: `0 1px 2px ${lightAccentHover(0.06)}`, + sm: `0 2px 4px ${lightAccentHover(0.08)}`, + base: `0 4px 8px ${lightAccentHover(0.1)}`, + lg: `0 8px 16px ${lightAccentHover(0.12)}`, + xl: `0 12px 24px ${lightAccentHover(0.14)}`, + '2xl': `0 16px 32px ${lightAccentHover(0.16)}`, }, - - + + glow: { - blue: '0 8px 24px rgba(15, 23, 42, 0.08), 0 4px 12px rgba(15, 23, 42, 0.05), 0 2px 6px rgba(71, 85, 105, 0.04)', - purple: '0 8px 24px rgba(15, 23, 42, 0.07), 0 4px 12px rgba(100, 116, 139, 0.06), 0 2px 6px rgba(71, 85, 105, 0.04)', - mixed: '0 8px 24px rgba(15, 23, 42, 0.07), 0 4px 12px rgba(15, 23, 42, 0.05), 0 2px 6px rgba(71, 85, 105, 0.04)', + blue: `0 8px 24px ${lightInk(0.08)}, 0 4px 12px ${lightInk(0.05)}, 0 2px 6px ${lightAccentHover(0.04)}`, + purple: `0 8px 24px ${lightInk(0.07)}, 0 4px 12px ${lightAccent(0.06)}, 0 2px 6px ${lightAccentHover(0.04)}`, + mixed: `0 8px 24px ${lightInk(0.07)}, 0 4px 12px ${lightInk(0.05)}, 0 2px 6px ${lightAccentHover(0.04)}`, }, - + blur: { subtle: 'blur(4px) saturate(1.02)', base: 'blur(8px) saturate(1.05)', @@ -150,11 +175,11 @@ export const bitfunLightTheme: ThemeConfig = { strong: 'blur(16px) saturate(1.10) brightness(1.02)', intense: 'blur(20px) saturate(1.12) brightness(1.03)', }, - + radius: createStandardRadius(), - + spacing: createStandardSpacing(), - + opacity: { disabled: 0.55, hover: 0.75, @@ -162,8 +187,8 @@ export const bitfunLightTheme: ThemeConfig = { overlay: 0.35, }, }, - - + + motion: { duration: { instant: '0.1s', @@ -172,106 +197,106 @@ export const bitfunLightTheme: ThemeConfig = { slow: '0.6s', lazy: '1s', }, - + easing: createStandardEasing(), }, - - + + typography: createStandardTypography(), - - + + components: { - + windowControls: createWindowControls({ standard: { - dot: 'rgba(100, 116, 139, 0.5)', - dotShadow: '0 0 4px rgba(15, 23, 42, 0.12)', - hoverBg: 'rgba(15, 23, 42, 0.08)', - hoverColor: '#475569', - hoverBorder: 'rgba(100, 116, 139, 0.28)', - hoverShadow: '0 2px 8px rgba(15, 23, 42, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.6)', + dot: lightAccent(0.5), + dotShadow: `0 0 4px ${lightInk(0.12)}`, + hoverBg: lightInk(0.08), + hoverColor: LIGHT_ACCENT_HOVER, + hoverBorder: lightAccent(0.28), + hoverShadow: `0 2px 8px ${lightInk(0.08)}, inset 0 1px 0 ${rgbaFromHex(STATIC_WHITE, 0.6)}`, }, close: { - dot: 'rgba(194, 101, 101, 0.55)', - dotShadow: '0 0 4px rgba(194, 101, 101, 0.2)', - hoverBg: 'rgba(194, 101, 101, 0.14)', + dot: lightError(0.55), + dotShadow: `0 0 4px ${lightError(0.2)}`, + hoverBg: lightError(0.14), hoverColor: '#a85555', - hoverBorder: 'rgba(194, 101, 101, 0.25)', - hoverShadow: '0 2px 8px rgba(194, 101, 101, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.6)', + hoverBorder: lightError(0.25), + hoverShadow: `0 2px 8px ${lightError(0.15)}, inset 0 1px 0 ${rgbaFromHex(STATIC_WHITE, 0.6)}`, }, common: { - defaultColor: 'rgba(30, 41, 59, 0.95)', - defaultDot: 'rgba(100, 116, 139, 0.28)', - disabledDot: 'rgba(100, 116, 139, 0.15)', - flowGradient: 'linear-gradient(90deg, transparent, rgba(100, 116, 139, 0.06), rgba(100, 116, 139, 0.1), rgba(100, 116, 139, 0.06), transparent)', + defaultColor: rgbaFromHex(LIGHT_TEXT_PRIMARY, 0.95), + defaultDot: lightAccent(0.28), + disabledDot: lightAccent(0.15), + flowGradient: `linear-gradient(90deg, transparent, ${lightAccent(0.06)}, ${lightAccent(0.1)}, ${lightAccent(0.06)}, transparent)`, }, }), - + button: { - + default: { - background: 'rgba(15, 23, 42, 0.07)', - color: '#475569', + background: lightInk(0.07), + color: LIGHT_ACCENT_HOVER, border: 'transparent', shadow: 'none', }, hover: { - background: 'rgba(15, 23, 42, 0.11)', - color: '#334155', + background: lightInk(0.11), + color: LIGHT_TEXT_STRONG, border: 'transparent', shadow: 'none', transform: 'none', }, active: { - background: 'rgba(15, 23, 42, 0.09)', - color: '#334155', + background: lightInk(0.09), + color: LIGHT_TEXT_STRONG, border: 'transparent', shadow: 'none', transform: 'none', }, - - + + primary: { default: { - background: '#000000', - color: '#ffffff', + background: STATIC_BLACK, + color: STATIC_WHITE, border: 'transparent', shadow: 'none', }, hover: { background: '#262626', - color: '#ffffff', + color: STATIC_WHITE, border: 'transparent', shadow: 'none', transform: 'none', }, active: { background: '#1a1a1a', - color: '#ffffff', + color: STATIC_WHITE, border: 'transparent', shadow: 'none', transform: 'none', }, }, - - + + ghost: { default: { background: 'transparent', - color: '#475569', + color: LIGHT_ACCENT_HOVER, border: 'transparent', shadow: 'none', }, hover: { - background: 'rgba(15, 23, 42, 0.08)', - color: '#334155', + background: lightInk(0.08), + color: LIGHT_TEXT_STRONG, border: 'transparent', shadow: 'none', transform: 'none', }, active: { - background: 'rgba(15, 23, 42, 0.055)', - color: '#334155', + background: lightInk(0.055), + color: LIGHT_TEXT_STRONG, border: 'transparent', shadow: 'none', transform: 'none', @@ -279,42 +304,42 @@ export const bitfunLightTheme: ThemeConfig = { }, }, }, - - + + monaco: { base: 'vs', inherit: true, rules: [ - { token: 'comment', foreground: '94a3b8', fontStyle: 'italic' }, - { token: 'keyword', foreground: '6b5a89' }, - { token: 'string', foreground: '5b9a6f' }, - { token: 'number', foreground: 'b8863a' }, + { token: 'comment', foreground: '94a3b8', fontStyle: 'italic' }, + { token: 'keyword', foreground: '6b5a89' }, + { token: 'string', foreground: '5b9a6f' }, + { token: 'number', foreground: 'b8863a' }, { token: 'type', foreground: '475569' }, { token: 'class', foreground: '475569' }, - { token: 'function', foreground: '7c6b99' }, - { token: 'variable', foreground: '475569' }, - { token: 'constant', foreground: 'c08c42' }, - { token: 'operator', foreground: '6b5a89' }, + { token: 'function', foreground: '7c6b99' }, + { token: 'variable', foreground: '475569' }, + { token: 'constant', foreground: 'c08c42' }, + { token: 'operator', foreground: '6b5a89' }, { token: 'tag', foreground: '475569' }, - { token: 'attribute.name', foreground: '7c6b99' }, - { token: 'attribute.value', foreground: '5b9a6f' }, + { token: 'attribute.name', foreground: '7c6b99' }, + { token: 'attribute.value', foreground: '5b9a6f' }, ], colors: { - background: '#f7f8fa', - foreground: '#1e293b', - lineHighlight: '#f0f4f8', - selection: 'rgba(15, 23, 42, 0.14)', - cursor: '#1e293b', - - 'editor.selectionBackground': 'rgba(15, 23, 42, 0.14)', - 'editor.selectionForeground': '#1e293b', - 'editor.inactiveSelectionBackground': 'rgba(15, 23, 42, 0.09)', - 'editor.selectionHighlightBackground': 'rgba(15, 23, 42, 0.1)', - 'editor.selectionHighlightBorder': 'rgba(15, 23, 42, 0.22)', - 'editorCursor.foreground': '#1e293b', - - 'editor.wordHighlightBackground': 'rgba(15, 23, 42, 0.07)', - 'editor.wordHighlightStrongBackground': 'rgba(15, 23, 42, 0.11)', + background: '#f7f8fa', + foreground: LIGHT_TEXT_PRIMARY, + lineHighlight: '#f0f4f8', + selection: lightInk(0.14), + cursor: LIGHT_TEXT_PRIMARY, + + 'editor.selectionBackground': lightInk(0.14), + 'editor.selectionForeground': LIGHT_TEXT_PRIMARY, + 'editor.inactiveSelectionBackground': lightInk(0.09), + 'editor.selectionHighlightBackground': lightInk(0.1), + 'editor.selectionHighlightBorder': lightInk(0.22), + 'editorCursor.foreground': LIGHT_TEXT_PRIMARY, + + 'editor.wordHighlightBackground': lightInk(0.07), + 'editor.wordHighlightStrongBackground': lightInk(0.11), }, }, }; 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 f5a5a6bbb0..cefc6707e6 100644 --- a/src/web-ui/src/infrastructure/theme/presets/midnight-theme.ts +++ b/src/web-ui/src/infrastructure/theme/presets/midnight-theme.ts @@ -1,4 +1,4 @@ - + import { ThemeConfig } from '../types'; import { @@ -8,129 +8,157 @@ import { createStandardSpacing, createStandardTypography, createWindowControls, + overlayBlack, + overlayWhite, + rgbFromHex, + rgbaFromHex, } from './shared'; +const MIDNIGHT_BACKGROUND = '#2b2d30'; +const MIDNIGHT_TEXT_PRIMARY = '#bcbec4'; +const MIDNIGHT_BUTTON_TEXT = '#afb1b5'; +const MIDNIGHT_ACCENT = '#58a6ff'; +const MIDNIGHT_ACCENT_HOVER = '#3b82f6'; +const MIDNIGHT_PURPLE = '#9c78ff'; +const MIDNIGHT_PURPLE_HOVER = '#8b5cf6'; +const MIDNIGHT_SUCCESS = '#6aab73'; +const MIDNIGHT_WARNING = '#e0a055'; +const MIDNIGHT_ERROR = '#cc7f7a'; +const MIDNIGHT_CONTROL_ERROR = '#ef4444'; +const MIDNIGHT_HIGHLIGHT = '#d4a574'; + +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 midnightAccentHover = (alpha: number | string) => rgbaFromHex(MIDNIGHT_ACCENT_HOVER, alpha); +const midnightPurple = (alpha: number | string) => rgbaFromHex(MIDNIGHT_PURPLE, alpha); +const midnightPurpleHover = (alpha: number | string) => rgbaFromHex(MIDNIGHT_PURPLE_HOVER, 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); +const midnightControlError = (alpha: number | string) => rgbaFromHex(MIDNIGHT_CONTROL_ERROR, alpha); + export const bitfunMidnightTheme: ThemeConfig = { - + id: 'bitfun-midnight', name: 'Midnight', type: 'dark', description: 'Midnight gray dark theme - Professional and elegant, inspired by JetBrains IDE', author: 'BitFun Team', version: '1.0.0', - - + + colors: { background: { - primary: '#2b2d30', - secondary: '#1e1f22', - tertiary: '#313335', - quaternary: '#3c3f41', - elevated: '#2b2d30', - workbench: '#212121', + primary: MIDNIGHT_BACKGROUND, + secondary: '#1e1f22', + tertiary: '#313335', + quaternary: '#3c3f41', + elevated: MIDNIGHT_BACKGROUND, + workbench: '#212121', scene: '#27292c', - tooltip: 'rgba(43, 45, 48, 0.94)', + tooltip: midnightBackground(0.94), }, - + text: { - primary: '#bcbec4', - secondary: '#9da0a8', - muted: '#6f737a', - disabled: '#4e5157', + primary: MIDNIGHT_TEXT_PRIMARY, + secondary: '#9da0a8', + muted: '#6f737a', + disabled: '#4e5157', }, - + accent: { - 50: 'rgba(88, 166, 255, 0.04)', - 100: 'rgba(88, 166, 255, 0.08)', - 200: 'rgba(88, 166, 255, 0.15)', - 300: 'rgba(88, 166, 255, 0.25)', - 400: 'rgba(88, 166, 255, 0.4)', - 500: '#58a6ff', - 600: '#3b82f6', - 700: 'rgba(59, 130, 246, 0.8)', - 800: 'rgba(59, 130, 246, 0.9)', + 50: midnightAccent(0.04), + 100: midnightAccent(0.08), + 200: midnightAccent(0.15), + 300: midnightAccent(0.25), + 400: midnightAccent(0.4), + 500: MIDNIGHT_ACCENT, + 600: MIDNIGHT_ACCENT_HOVER, + 700: midnightAccentHover(0.8), + 800: midnightAccentHover(0.9), }, - + purple: { - 50: 'rgba(156, 120, 255, 0.04)', - 100: 'rgba(156, 120, 255, 0.08)', - 200: 'rgba(156, 120, 255, 0.15)', - 300: 'rgba(156, 120, 255, 0.25)', - 400: 'rgba(156, 120, 255, 0.4)', - 500: '#9c78ff', - 600: '#8b5cf6', - 700: 'rgba(139, 92, 246, 0.8)', - 800: 'rgba(139, 92, 246, 0.9)', + 50: midnightPurple(0.04), + 100: midnightPurple(0.08), + 200: midnightPurple(0.15), + 300: midnightPurple(0.25), + 400: midnightPurple(0.4), + 500: MIDNIGHT_PURPLE, + 600: MIDNIGHT_PURPLE_HOVER, + 700: midnightPurpleHover(0.8), + 800: midnightPurpleHover(0.9), }, - + semantic: { - success: '#6aab73', - successBg: 'rgba(106, 171, 115, 0.1)', - successBorder: 'rgba(106, 171, 115, 0.3)', - - warning: '#e0a055', - warningBg: 'rgba(224, 160, 85, 0.1)', - warningBorder: 'rgba(224, 160, 85, 0.3)', - - error: '#cc7f7a', - errorBg: 'rgba(204, 127, 122, 0.1)', - errorBorder: 'rgba(204, 127, 122, 0.3)', - - info: '#58a6ff', - infoBg: 'rgba(88, 166, 255, 0.1)', - infoBorder: 'rgba(88, 166, 255, 0.3)', - - - highlight: '#d4a574', - highlightBg: 'rgba(212, 165, 116, 0.15)', + success: MIDNIGHT_SUCCESS, + successBg: midnightSuccess(0.1), + successBorder: midnightSuccess(0.3), + + warning: MIDNIGHT_WARNING, + warningBg: midnightWarning(0.1), + warningBorder: midnightWarning(0.3), + + error: MIDNIGHT_ERROR, + errorBg: midnightError(0.1), + errorBorder: midnightError(0.3), + + info: MIDNIGHT_ACCENT, + infoBg: midnightAccent(0.1), + infoBorder: midnightAccent(0.3), + + + highlight: MIDNIGHT_HIGHLIGHT, + highlightBg: rgbaFromHex(MIDNIGHT_HIGHLIGHT, 0.15), }, - + border: { - subtle: 'rgba(255, 255, 255, 0.08)', - base: 'rgba(255, 255, 255, 0.14)', - medium: 'rgba(255, 255, 255, 0.2)', - strong: 'rgba(255, 255, 255, 0.26)', - prominent: 'rgba(255, 255, 255, 0.35)', + subtle: overlayWhite(0.08), + base: overlayWhite(0.14), + medium: overlayWhite(0.2), + strong: overlayWhite(0.26), + prominent: overlayWhite(0.35), }, - + element: { - subtle: 'rgba(255, 255, 255, 0.04)', - soft: 'rgba(255, 255, 255, 0.06)', - base: 'rgba(255, 255, 255, 0.09)', - medium: 'rgba(255, 255, 255, 0.12)', - strong: 'rgba(255, 255, 255, 0.15)', - elevated: 'rgba(255, 255, 255, 0.18)', + subtle: overlayWhite(0.04), + soft: overlayWhite(0.06), + base: overlayWhite(0.09), + medium: overlayWhite(0.12), + strong: overlayWhite(0.15), + elevated: overlayWhite(0.18), }, - + git: createGitColors({ - branch: 'rgb(88, 166, 255)', - branchBg: 'rgba(88, 166, 255, 0.1)', - changes: 'rgb(224, 160, 85)', - changesBg: 'rgba(224, 160, 85, 0.1)', - added: 'rgb(106, 171, 115)', - addedBg: 'rgba(106, 171, 115, 0.1)', - deleted: 'rgb(204, 127, 122)', - deletedBg: 'rgba(204, 127, 122, 0.1)', + 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), }), }, - - + + effects: { shadow: { - xs: '0 1px 2px rgba(0, 0, 0, 0.8)', - sm: '0 2px 4px rgba(0, 0, 0, 0.75)', - base: '0 4px 8px rgba(0, 0, 0, 0.7)', - lg: '0 8px 16px rgba(0, 0, 0, 0.65)', - xl: '0 12px 24px rgba(0, 0, 0, 0.8)', - '2xl': '0 16px 32px rgba(0, 0, 0, 0.85)', + xs: `0 1px 2px ${overlayBlack(0.8)}`, + sm: `0 2px 4px ${overlayBlack(0.75)}`, + base: `0 4px 8px ${overlayBlack(0.7)}`, + lg: `0 8px 16px ${overlayBlack(0.65)}`, + xl: `0 12px 24px ${overlayBlack(0.8)}`, + '2xl': `0 16px 32px ${overlayBlack(0.85)}`, }, - + glow: { - blue: '0 12px 32px rgba(88, 166, 255, 0.25), 0 6px 16px rgba(88, 166, 255, 0.18), 0 3px 8px rgba(0, 0, 0, 0.15)', - purple: '0 12px 32px rgba(156, 120, 255, 0.25), 0 6px 16px rgba(156, 120, 255, 0.18), 0 3px 8px rgba(0, 0, 0, 0.15)', - mixed: '0 12px 32px rgba(88, 166, 255, 0.2), 0 6px 16px rgba(156, 120, 255, 0.18), 0 3px 8px rgba(0, 0, 0, 0.15)', + blue: `0 12px 32px ${midnightAccent(0.25)}, 0 6px 16px ${midnightAccent(0.18)}, 0 3px 8px ${overlayBlack(0.15)}`, + purple: `0 12px 32px ${midnightPurple(0.25)}, 0 6px 16px ${midnightPurple(0.18)}, 0 3px 8px ${overlayBlack(0.15)}`, + mixed: `0 12px 32px ${midnightAccent(0.2)}, 0 6px 16px ${midnightPurple(0.18)}, 0 3px 8px ${overlayBlack(0.15)}`, }, - + blur: { subtle: 'blur(4px) saturate(1.1)', base: 'blur(8px) saturate(1.2)', @@ -138,11 +166,11 @@ export const bitfunMidnightTheme: ThemeConfig = { strong: 'blur(16px) saturate(1.3) brightness(1.05)', intense: 'blur(20px) saturate(1.4) brightness(1.1)', }, - + radius: createStandardRadius(), - + spacing: createStandardSpacing(), - + opacity: { disabled: 0.5, hover: 0.8, @@ -150,8 +178,8 @@ export const bitfunMidnightTheme: ThemeConfig = { overlay: 0.4, }, }, - - + + motion: { duration: { instant: '0.1s', @@ -160,89 +188,89 @@ export const bitfunMidnightTheme: ThemeConfig = { slow: '0.6s', lazy: '1s', }, - + easing: createStandardEasing(), }, - - + + typography: createStandardTypography(), - - + + components: { - + windowControls: createWindowControls({ standard: { - dot: 'rgba(88, 166, 255, 0.45)', - dotShadow: '0 0 4px rgba(88, 166, 255, 0.2)', - hoverBg: 'rgba(88, 166, 255, 0.12)', - hoverColor: '#58a6ff', - hoverBorder: 'rgba(88, 166, 255, 0.2)', - hoverShadow: '0 2px 8px rgba(88, 166, 255, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1)', + dot: midnightAccent(0.45), + dotShadow: `0 0 4px ${midnightAccent(0.2)}`, + hoverBg: midnightAccent(0.12), + hoverColor: MIDNIGHT_ACCENT, + hoverBorder: midnightAccent(0.2), + hoverShadow: `0 2px 8px ${midnightAccent(0.15)}, inset 0 1px 0 ${overlayWhite(0.1)}`, }, close: { - dot: 'rgba(239, 68, 68, 0.45)', - dotShadow: '0 0 4px rgba(239, 68, 68, 0.2)', - hoverBg: 'rgba(239, 68, 68, 0.12)', - hoverColor: '#ef4444', - hoverBorder: 'rgba(239, 68, 68, 0.2)', - hoverShadow: '0 2px 8px rgba(239, 68, 68, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1)', + dot: midnightControlError(0.45), + dotShadow: `0 0 4px ${midnightControlError(0.2)}`, + hoverBg: midnightControlError(0.12), + hoverColor: MIDNIGHT_CONTROL_ERROR, + hoverBorder: midnightControlError(0.2), + hoverShadow: `0 2px 8px ${midnightControlError(0.15)}, inset 0 1px 0 ${overlayWhite(0.1)}`, }, common: { - defaultColor: 'rgba(188, 190, 196, 0.9)', - defaultDot: 'rgba(188, 190, 196, 0.2)', - disabledDot: 'rgba(188, 190, 196, 0.1)', - flowGradient: 'linear-gradient(90deg, transparent, rgba(188, 190, 196, 0.05), rgba(188, 190, 196, 0.08), rgba(188, 190, 196, 0.05), transparent)', + defaultColor: midnightText(0.9), + defaultDot: midnightText(0.2), + disabledDot: midnightText(0.1), + flowGradient: `linear-gradient(90deg, transparent, ${midnightText(0.05)}, ${midnightText(0.08)}, ${midnightText(0.05)}, transparent)`, }, }), - + button: { - + default: { - background: 'rgba(188, 190, 196, 0.11)', + background: midnightText(0.11), color: '#949699', border: 'transparent', shadow: 'none', }, hover: { - background: 'rgba(188, 190, 196, 0.17)', - color: '#afb1b5', + background: midnightText(0.17), + color: MIDNIGHT_BUTTON_TEXT, border: 'transparent', shadow: 'none', transform: 'none', }, active: { - background: 'rgba(188, 190, 196, 0.14)', - color: '#afb1b5', + background: midnightText(0.14), + color: MIDNIGHT_BUTTON_TEXT, border: 'transparent', shadow: 'none', transform: 'none', }, - - + + primary: { default: { - background: 'rgba(88, 166, 255, 0.2)', + background: midnightAccent(0.2), color: '#6aa8e8', border: 'transparent', shadow: 'none', }, hover: { - background: 'rgba(88, 166, 255, 0.3)', + background: midnightAccent(0.3), color: '#8fc0f0', border: 'transparent', shadow: 'none', transform: 'none', }, active: { - background: 'rgba(88, 166, 255, 0.24)', + background: midnightAccent(0.24), color: '#8fc0f0', border: 'transparent', shadow: 'none', transform: 'none', }, }, - - + + ghost: { default: { background: 'transparent', @@ -251,15 +279,15 @@ export const bitfunMidnightTheme: ThemeConfig = { shadow: 'none', }, hover: { - background: 'rgba(188, 190, 196, 0.13)', - color: '#afb1b5', + background: midnightText(0.13), + color: MIDNIGHT_BUTTON_TEXT, border: 'transparent', shadow: 'none', transform: 'none', }, active: { - background: 'rgba(188, 190, 196, 0.11)', - color: '#afb1b5', + background: midnightText(0.11), + color: MIDNIGHT_BUTTON_TEXT, border: 'transparent', shadow: 'none', transform: 'none', @@ -267,8 +295,8 @@ export const bitfunMidnightTheme: ThemeConfig = { }, }, }, - - + + monaco: { base: 'vs-dark', inherit: true, @@ -288,11 +316,11 @@ export const bitfunMidnightTheme: ThemeConfig = { { token: 'attribute.value', foreground: 'a5c261' }, ], colors: { - background: '#2b2d30', - foreground: '#bcbec4', + background: MIDNIGHT_BACKGROUND, + foreground: MIDNIGHT_TEXT_PRIMARY, lineHighlight: '#313335', selection: '#3d4752', - cursor: '#58a6ff', + cursor: MIDNIGHT_ACCENT, }, }, }; diff --git a/src/web-ui/src/infrastructure/theme/presets/shared.ts b/src/web-ui/src/infrastructure/theme/presets/shared.ts index 243d453074..6e5d3e7b4f 100644 --- a/src/web-ui/src/infrastructure/theme/presets/shared.ts +++ b/src/web-ui/src/infrastructure/theme/presets/shared.ts @@ -10,6 +10,43 @@ import type { type WindowControlState = WindowControlsConfig['minimize']; +export const STATIC_BLACK = '#000000'; +export const STATIC_WHITE = '#ffffff'; + +function hexToRgbChannels(hex: string): [number, number, number] { + const raw = hex.trim().replace(/^#/, ''); + const expanded = raw.length === 3 + ? raw.split('').map(channel => channel + channel).join('') + : raw; + if (!/^[0-9a-f]{6}$/i.test(expanded)) { + throw new Error(`Invalid hex color: ${hex}`); + } + const value = Number.parseInt(expanded, 16); + return [ + (value >> 16) & 255, + (value >> 8) & 255, + value & 255, + ]; +} + +export function rgbFromHex(hex: string): string { + const [r, g, b] = hexToRgbChannels(hex); + return `rgb(${r}, ${g}, ${b})`; +} + +export function rgbaFromHex(hex: string, alpha: number | string): string { + const [r, g, b] = hexToRgbChannels(hex); + return `rgba(${r}, ${g}, ${b}, ${alpha})`; +} + +export function overlayBlack(alpha: number | string): string { + return rgbaFromHex(STATIC_BLACK, alpha); +} + +export function overlayWhite(alpha: number | string): string { + return rgbaFromHex(STATIC_WHITE, alpha); +} + export function createWindowControls(config: { standard: WindowControlState; close: WindowControlState; 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 da64a18cbb..91e1791ff7 100644 --- a/src/web-ui/src/infrastructure/theme/presets/slate-theme.ts +++ b/src/web-ui/src/infrastructure/theme/presets/slate-theme.ts @@ -1,4 +1,4 @@ - + import { ThemeConfig } from '../types'; import { @@ -11,10 +11,38 @@ import { createStandardSpacing, createStandardTypography, createWindowControls, + overlayBlack, + overlayWhite, + rgbFromHex, + rgbaFromHex, + STATIC_WHITE, } from './shared'; +const SLATE_BACKGROUND_PRIMARY = '#14161a'; +const SLATE_BACKGROUND_SECONDARY = '#22262c'; +const SLATE_TEXT_PRIMARY = '#eef0f3'; +const SLATE_TEXT_MUTED = '#a8b0bd'; +const SLATE_BUTTON_TEXT = '#dce0e6'; +const SLATE_ACCENT = '#94a3b8'; +const SLATE_ACCENT_HOVER = '#64748b'; +// Keep the original alpha ramp channels separate from the solid 500 stop. +const SLATE_PURPLE_ALPHA = '#b8c6ff'; +const SLATE_PURPLE = '#b8c4ff'; +const SLATE_PURPLE_HOVER = '#9dacf5'; +const SLATE_SUCCESS = '#7fb899'; +const SLATE_WARNING = '#f59e0b'; +const SLATE_ERROR = '#c9878d'; + +const slateAccent = (alpha: number | string) => rgbaFromHex(SLATE_ACCENT, alpha); +const slateAccentHover = (alpha: number | string) => rgbaFromHex(SLATE_ACCENT_HOVER, alpha); +const slatePurple = (alpha: number | string) => rgbaFromHex(SLATE_PURPLE_ALPHA, alpha); +const slatePurpleHover = (alpha: number | string) => rgbaFromHex(SLATE_PURPLE_HOVER, alpha); +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', name: 'Slate', type: 'dark', @@ -25,110 +53,110 @@ export const bitfunSlateTheme: ThemeConfig = { layout: { sceneViewportBorder: false, }, - + colors: { background: { - primary: '#14161a', - secondary: '#22262c', - tertiary: '#14161a', + primary: SLATE_BACKGROUND_PRIMARY, + secondary: SLATE_BACKGROUND_SECONDARY, + tertiary: SLATE_BACKGROUND_PRIMARY, quaternary: '#2c3038', - elevated: '#22262c', - workbench: '#14161a', - scene: '#22262c', + elevated: SLATE_BACKGROUND_SECONDARY, + workbench: SLATE_BACKGROUND_PRIMARY, + scene: SLATE_BACKGROUND_SECONDARY, tooltip: 'rgba(34, 38, 44, 0.96)', }, - + text: { - primary: '#eef0f3', + primary: SLATE_TEXT_PRIMARY, secondary: '#c8ccd2', muted: '#9ea4ab', disabled: '#65696f', }, - - + + // Cool gray accent — neutral chrome for slate surfaces (links, focus, nav tints). accent: { - 50: 'rgba(226, 232, 240, 0.05)', - 100: 'rgba(226, 232, 240, 0.09)', + 50: rgbaFromHex('#e2e8f0', 0.05), + 100: rgbaFromHex('#e2e8f0', 0.09), 200: 'rgba(203, 213, 225, 0.14)', 300: 'rgba(203, 213, 225, 0.24)', 400: 'rgba(148, 163, 184, 0.45)', - 500: '#94a3b8', - 600: '#64748b', - 700: 'rgba(100, 116, 139, 0.85)', + 500: SLATE_ACCENT, + 600: SLATE_ACCENT_HOVER, + 700: slateAccentHover(0.85), 800: 'rgba(71, 85, 105, 0.92)', }, - - + + purple: { - 50: 'rgba(184, 198, 255, 0.04)', - 100: 'rgba(184, 198, 255, 0.08)', - 200: 'rgba(184, 198, 255, 0.15)', - 300: 'rgba(184, 198, 255, 0.25)', - 400: 'rgba(184, 198, 255, 0.4)', - 500: '#b8c4ff', - 600: '#9dacf5', - 700: 'rgba(157, 172, 245, 0.8)', - 800: 'rgba(157, 172, 245, 0.9)', + 50: slatePurple(0.04), + 100: slatePurple(0.08), + 200: slatePurple(0.15), + 300: slatePurple(0.25), + 400: slatePurple(0.4), + 500: SLATE_PURPLE, + 600: SLATE_PURPLE_HOVER, + 700: slatePurpleHover(0.8), + 800: slatePurpleHover(0.9), }, - + semantic: { - success: '#7fb899', - successBg: 'rgba(127, 184, 153, 0.1)', - successBorder: 'rgba(127, 184, 153, 0.3)', - - warning: '#f59e0b', - warningBg: 'rgba(245, 158, 11, 0.1)', - warningBorder: 'rgba(245, 158, 11, 0.3)', - - error: '#c9878d', - errorBg: 'rgba(201, 135, 141, 0.1)', - errorBorder: 'rgba(201, 135, 141, 0.3)', - - info: '#a8b0bd', - infoBg: 'rgba(255, 255, 255, 0.07)', - infoBorder: 'rgba(255, 255, 255, 0.2)', - - + success: SLATE_SUCCESS, + successBg: slateSuccess(0.1), + successBorder: slateSuccess(0.3), + + warning: SLATE_WARNING, + warningBg: slateWarning(0.1), + warningBorder: slateWarning(0.3), + + error: SLATE_ERROR, + errorBg: slateError(0.1), + errorBorder: slateError(0.3), + + info: SLATE_TEXT_MUTED, + infoBg: overlayWhite(0.07), + infoBorder: overlayWhite(0.2), + + highlight: '#c8cdd4', - highlightBg: 'rgba(255, 255, 255, 0.1)', + highlightBg: overlayWhite(0.1), }, - + border: createDarkNeutralBorder(), - + element: createDarkNeutralElement(), - + git: createGitColors({ branch: '#9ca6b8', - branchBg: 'rgba(255, 255, 255, 0.06)', - changes: 'rgb(245, 158, 11)', - changesBg: 'rgba(245, 158, 11, 0.1)', - added: 'rgb(127, 184, 153)', - addedBg: 'rgba(127, 184, 153, 0.1)', - deleted: 'rgb(201, 135, 141)', - deletedBg: 'rgba(201, 135, 141, 0.1)', + 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(), }, - - + + effects: { shadow: { - xs: '0 1px 2px rgba(0, 0, 0, 0.85)', - sm: '0 2px 4px rgba(0, 0, 0, 0.8)', - base: '0 4px 8px rgba(0, 0, 0, 0.75)', - lg: '0 8px 16px rgba(0, 0, 0, 0.7)', - xl: '0 12px 24px rgba(0, 0, 0, 0.85)', - '2xl': '0 16px 32px rgba(0, 0, 0, 0.9)', + xs: `0 1px 2px ${overlayBlack(0.85)}`, + sm: `0 2px 4px ${overlayBlack(0.8)}`, + base: `0 4px 8px ${overlayBlack(0.75)}`, + lg: `0 8px 16px ${overlayBlack(0.7)}`, + xl: `0 12px 24px ${overlayBlack(0.85)}`, + '2xl': `0 16px 32px ${overlayBlack(0.9)}`, }, - + glow: { - blue: '0 12px 32px rgba(148, 163, 184, 0.14), 0 6px 16px rgba(148, 163, 184, 0.1), 0 3px 8px rgba(0, 0, 0, 0.2)', - purple: '0 12px 32px rgba(184, 198, 255, 0.2), 0 6px 16px rgba(184, 198, 255, 0.12), 0 3px 8px rgba(0, 0, 0, 0.2)', - mixed: '0 12px 32px rgba(255, 255, 255, 0.05), 0 6px 16px rgba(184, 198, 255, 0.1), 0 3px 8px rgba(0, 0, 0, 0.18)', + blue: `0 12px 32px ${slateAccent(0.14)}, 0 6px 16px ${slateAccent(0.1)}, 0 3px 8px ${overlayBlack(0.2)}`, + purple: `0 12px 32px ${slatePurple(0.2)}, 0 6px 16px ${slatePurple(0.12)}, 0 3px 8px ${overlayBlack(0.2)}`, + mixed: `0 12px 32px ${overlayWhite(0.05)}, 0 6px 16px ${slatePurple(0.1)}, 0 3px 8px ${overlayBlack(0.18)}`, }, - + blur: { subtle: 'blur(4px) saturate(1.05) brightness(0.98)', base: 'blur(8px) saturate(1.08) brightness(0.98)', @@ -136,11 +164,11 @@ export const bitfunSlateTheme: ThemeConfig = { strong: 'blur(16px) saturate(1.15) brightness(0.97)', intense: 'blur(20px) saturate(1.18) brightness(0.96)', }, - + radius: createSlateRadius(), - + spacing: createStandardSpacing(), - + opacity: { disabled: 0.5, hover: 0.75, @@ -148,8 +176,8 @@ export const bitfunSlateTheme: ThemeConfig = { overlay: 0.5, }, }, - - + + motion: { duration: { instant: '0.08s', @@ -158,106 +186,106 @@ export const bitfunSlateTheme: ThemeConfig = { slow: '0.5s', lazy: '0.8s', }, - + easing: createStandardEasing(), }, - - + + typography: createStandardTypography(), - - + + components: { - + windowControls: createWindowControls({ standard: { dot: 'rgba(203, 213, 225, 0.42)', - dotShadow: '0 0 4px rgba(0, 0, 0, 0.35)', - hoverBg: 'rgba(255, 255, 255, 0.09)', + dotShadow: `0 0 4px ${overlayBlack(0.35)}`, + hoverBg: overlayWhite(0.09), hoverColor: '#e2e6eb', - hoverBorder: 'rgba(255, 255, 255, 0.14)', - hoverShadow: '0 2px 8px rgba(0, 0, 0, 0.22), inset 0 1px 0 rgba(255, 255, 255, 0.06)', + hoverBorder: overlayWhite(0.14), + hoverShadow: `0 2px 8px ${overlayBlack(0.22)}, inset 0 1px 0 ${overlayWhite(0.06)}`, }, close: { - dot: 'rgba(201, 135, 141, 0.5)', - dotShadow: '0 0 4px rgba(201, 135, 141, 0.25)', - hoverBg: 'rgba(201, 135, 141, 0.15)', - hoverColor: '#c9878d', - hoverBorder: 'rgba(201, 135, 141, 0.25)', - hoverShadow: '0 2px 8px rgba(201, 135, 141, 0.18), inset 0 1px 0 rgba(255, 255, 255, 0.08)', + dot: slateError(0.5), + dotShadow: `0 0 4px ${slateError(0.25)}`, + hoverBg: slateError(0.15), + hoverColor: SLATE_ERROR, + hoverBorder: slateError(0.25), + hoverShadow: `0 2px 8px ${slateError(0.18)}, inset 0 1px 0 ${overlayWhite(0.08)}`, }, common: { defaultColor: 'rgba(232, 234, 236, 0.92)', defaultDot: 'rgba(198, 202, 208, 0.48)', disabledDot: 'rgba(168, 171, 176, 0.2)', - flowGradient: 'linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.05), transparent)', + flowGradient: `linear-gradient(90deg, transparent, ${overlayWhite(0.05)}, ${overlayWhite(0.08)}, ${overlayWhite(0.05)}, transparent)`, }, }), - + button: { - + default: { - background: 'rgba(255, 255, 255, 0.08)', - color: '#a8b0bd', + background: overlayWhite(0.08), + color: SLATE_TEXT_MUTED, border: 'transparent', shadow: 'none', }, hover: { - background: 'rgba(255, 255, 255, 0.12)', - color: '#dce0e6', + background: overlayWhite(0.12), + color: SLATE_BUTTON_TEXT, border: 'transparent', shadow: 'none', transform: 'none', }, active: { - background: 'rgba(255, 255, 255, 0.1)', - color: '#dce0e6', + background: overlayWhite(0.1), + color: SLATE_BUTTON_TEXT, border: 'transparent', shadow: 'none', transform: 'none', }, - - + + primary: { default: { - background: 'rgba(255, 255, 255, 0.14)', + background: overlayWhite(0.14), color: '#f0f2f5', border: 'transparent', shadow: 'none', }, hover: { - background: 'rgba(255, 255, 255, 0.2)', - color: '#ffffff', + background: overlayWhite(0.2), + color: STATIC_WHITE, border: 'transparent', shadow: 'none', transform: 'none', }, active: { - background: 'rgba(255, 255, 255, 0.17)', - color: '#ffffff', + background: overlayWhite(0.17), + color: STATIC_WHITE, border: 'transparent', shadow: 'none', transform: 'none', }, }, - - + + ghost: { default: { background: 'transparent', - color: '#a8b0bd', + color: SLATE_TEXT_MUTED, border: 'transparent', shadow: 'none', }, hover: { - background: 'rgba(255, 255, 255, 0.08)', - color: '#dce0e6', + background: overlayWhite(0.08), + color: SLATE_BUTTON_TEXT, border: 'transparent', shadow: 'none', transform: 'none', }, active: { - background: 'rgba(255, 255, 255, 0.06)', - color: '#dce0e6', + background: overlayWhite(0.06), + color: SLATE_BUTTON_TEXT, border: 'transparent', shadow: 'none', transform: 'none', @@ -265,8 +293,8 @@ export const bitfunSlateTheme: ThemeConfig = { }, }, }, - - + + monaco: { base: 'vs-dark', inherit: true, @@ -287,9 +315,9 @@ export const bitfunSlateTheme: ThemeConfig = { ], colors: { background: '#1a1c1e', - foreground: '#eef0f3', + foreground: SLATE_TEXT_PRIMARY, lineHighlight: '#22252a', - selection: 'rgba(255, 255, 255, 0.12)', + selection: overlayWhite(0.12), cursor: '#aeb6c3', }, }, 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 fbfca738b4..09e2f2306a 100644 --- a/src/web-ui/src/infrastructure/theme/presets/themePresetOutput.test.ts +++ b/src/web-ui/src/infrastructure/theme/presets/themePresetOutput.test.ts @@ -2,7 +2,7 @@ import { createHash } from 'node:crypto'; import { describe, expect, it } from 'vitest'; import { builtinThemes } from './index'; -import { createGitColors } from './shared'; +import { createGitColors, overlayBlack, overlayWhite, rgbFromHex, rgbaFromHex } from './shared'; function hashTheme(theme: unknown): string { return createHash('sha256') @@ -11,6 +11,14 @@ function hashTheme(theme: unknown): string { } describe('builtin theme preset output', () => { + it('formats hex palette references as stable rgb strings', () => { + expect(rgbFromHex('#00e6ff')).toBe('rgb(0, 230, 255)'); + expect(rgbaFromHex('#00e6ff', 0.12)).toBe('rgba(0, 230, 255, 0.12)'); + expect(rgbaFromHex('#00e6ff', '0.12')).toBe('rgba(0, 230, 255, 0.12)'); + expect(overlayBlack(0.3)).toBe('rgba(0, 0, 0, 0.3)'); + expect(overlayWhite(0.08)).toBe('rgba(255, 255, 255, 0.08)'); + }); + it('aliases staged git colors to added colors unless a theme overrides them', () => { expect(createGitColors({ branch: '#64748b', 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 4f77b43f9d..7fda70aaeb 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 @@ -8,8 +8,43 @@ import { createStandardEasing, createStandardSpacing, createWindowControls, + overlayBlack, + rgbFromHex, + rgbaFromHex, + STATIC_WHITE, } from './shared'; +const TOKYO_BACKGROUND_PRIMARY = '#1a1b26'; +const TOKYO_BACKGROUND_SECONDARY = '#16161e'; +const TOKYO_TEXT_PRIMARY = '#c0caf5'; +const TOKYO_TEXT_SECONDARY = '#a9b1d6'; +const TOKYO_TEXT_MUTED = '#787c99'; +const TOKYO_ACCENT = '#7aa2f7'; +const TOKYO_ACCENT_HOVER = '#6183bb'; +const TOKYO_PURPLE = '#bb9af7'; +const TOKYO_PURPLE_HOVER = '#9d7cd8'; +const TOKYO_SUCCESS = '#9ece6a'; +const TOKYO_WARNING = '#e0af68'; +const TOKYO_ERROR = '#f7768e'; +const TOKYO_INFO = '#7dcfff'; +const TOKYO_BORDER = '#363b54'; +const TOKYO_SCROLLBAR = '#868bc4'; +const TOKYO_GIT_ADDED = '#41a6b5'; +const TOKYO_PRIMARY_BUTTON = '#3d59a1'; + +const tokyoAccent = (alpha: number | string) => rgbaFromHex(TOKYO_ACCENT, alpha); +const tokyoAccentHover = (alpha: number | string) => rgbaFromHex(TOKYO_ACCENT_HOVER, alpha); +const tokyoPurple = (alpha: number | string) => rgbaFromHex(TOKYO_PURPLE, alpha); +const tokyoPurpleHover = (alpha: number | string) => rgbaFromHex(TOKYO_PURPLE_HOVER, 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 tokyoInfo = (alpha: number | string) => rgbaFromHex(TOKYO_INFO, 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). */ export const bitfunTokyoNightTheme: ThemeConfig = { id: 'bitfun-tokyo-night', @@ -22,121 +57,121 @@ export const bitfunTokyoNightTheme: ThemeConfig = { colors: { background: { - primary: '#1a1b26', - secondary: '#16161e', + primary: TOKYO_BACKGROUND_PRIMARY, + secondary: TOKYO_BACKGROUND_SECONDARY, tertiary: '#14141b', quaternary: '#1e202e', elevated: '#20222c', - workbench: '#16161e', - scene: '#1a1b26', + workbench: TOKYO_BACKGROUND_SECONDARY, + scene: TOKYO_BACKGROUND_PRIMARY, tooltip: 'rgba(22, 22, 30, 0.94)', }, text: { - primary: '#c0caf5', - secondary: '#a9b1d6', - muted: '#787c99', + primary: TOKYO_TEXT_PRIMARY, + secondary: TOKYO_TEXT_SECONDARY, + muted: TOKYO_TEXT_MUTED, disabled: '#545c7e', }, accent: { - 50: 'rgba(122, 162, 247, 0.05)', - 100: 'rgba(122, 162, 247, 0.08)', - 200: 'rgba(122, 162, 247, 0.15)', - 300: 'rgba(122, 162, 247, 0.25)', - 400: 'rgba(122, 162, 247, 0.4)', - 500: '#7aa2f7', - 600: '#6183bb', - 700: 'rgba(97, 131, 187, 0.85)', - 800: 'rgba(97, 131, 187, 0.95)', + 50: tokyoAccent(0.05), + 100: tokyoAccent(0.08), + 200: tokyoAccent(0.15), + 300: tokyoAccent(0.25), + 400: tokyoAccent(0.4), + 500: TOKYO_ACCENT, + 600: TOKYO_ACCENT_HOVER, + 700: tokyoAccentHover(0.85), + 800: tokyoAccentHover(0.95), }, purple: { - 50: 'rgba(187, 154, 247, 0.05)', - 100: 'rgba(187, 154, 247, 0.08)', - 200: 'rgba(187, 154, 247, 0.15)', - 300: 'rgba(187, 154, 247, 0.25)', - 400: 'rgba(187, 154, 247, 0.4)', - 500: '#bb9af7', - 600: '#9d7cd8', - 700: 'rgba(157, 124, 216, 0.85)', - 800: 'rgba(157, 124, 216, 0.95)', + 50: tokyoPurple(0.05), + 100: tokyoPurple(0.08), + 200: tokyoPurple(0.15), + 300: tokyoPurple(0.25), + 400: tokyoPurple(0.4), + 500: TOKYO_PURPLE, + 600: TOKYO_PURPLE_HOVER, + 700: tokyoPurpleHover(0.85), + 800: tokyoPurpleHover(0.95), }, semantic: { - success: '#9ece6a', - successBg: 'rgba(158, 206, 106, 0.12)', - successBorder: 'rgba(158, 206, 106, 0.35)', + success: TOKYO_SUCCESS, + successBg: tokyoSuccess(0.12), + successBorder: tokyoSuccess(0.35), - warning: '#e0af68', - warningBg: 'rgba(224, 175, 104, 0.12)', - warningBorder: 'rgba(224, 175, 104, 0.35)', + warning: TOKYO_WARNING, + warningBg: tokyoWarning(0.12), + warningBorder: tokyoWarning(0.35), - error: '#f7768e', - errorBg: 'rgba(247, 118, 142, 0.12)', - errorBorder: 'rgba(247, 118, 142, 0.35)', + error: TOKYO_ERROR, + errorBg: tokyoError(0.12), + errorBorder: tokyoError(0.35), - info: '#7dcfff', - infoBg: 'rgba(125, 207, 255, 0.12)', - infoBorder: 'rgba(125, 207, 255, 0.35)', + info: TOKYO_INFO, + infoBg: tokyoInfo(0.12), + infoBorder: tokyoInfo(0.35), - highlight: '#e0af68', - highlightBg: 'rgba(224, 175, 104, 0.15)', + highlight: TOKYO_WARNING, + highlightBg: tokyoWarning(0.15), }, border: { - subtle: 'rgba(54, 59, 84, 0.45)', - base: 'rgba(54, 59, 84, 0.6)', - medium: 'rgba(54, 59, 84, 0.72)', - strong: 'rgba(54, 59, 84, 0.85)', - prominent: 'rgba(122, 162, 247, 0.45)', + subtle: tokyoBorder(0.45), + base: tokyoBorder(0.6), + medium: tokyoBorder(0.72), + strong: tokyoBorder(0.85), + prominent: tokyoAccent(0.45), }, element: { - subtle: 'rgba(122, 162, 247, 0.06)', - soft: 'rgba(122, 162, 247, 0.08)', - base: 'rgba(122, 162, 247, 0.11)', - medium: 'rgba(122, 162, 247, 0.14)', - strong: 'rgba(122, 162, 247, 0.18)', - elevated: 'rgba(122, 162, 247, 0.22)', + subtle: tokyoAccent(0.06), + soft: tokyoAccent(0.08), + base: tokyoAccent(0.11), + medium: tokyoAccent(0.14), + strong: tokyoAccent(0.18), + elevated: tokyoAccent(0.22), }, git: createGitColors({ - branch: 'rgb(122, 162, 247)', - branchBg: 'rgba(122, 162, 247, 0.12)', - changes: 'rgb(224, 175, 104)', - changesBg: 'rgba(224, 175, 104, 0.12)', - added: 'rgb(65, 166, 181)', - addedBg: 'rgba(65, 166, 181, 0.12)', - deleted: 'rgb(247, 118, 142)', - deletedBg: 'rgba(247, 118, 142, 0.12)', - staged: 'rgb(158, 206, 106)', - stagedBg: 'rgba(158, 206, 106, 0.12)', + 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: { - thumb: 'rgba(134, 139, 196, 0.15)', - thumbHover: 'rgba(134, 139, 196, 0.28)', + thumb: tokyoScrollbar(0.15), + thumbHover: tokyoScrollbar(0.28), }, }, effects: { shadow: { - xs: '0 1px 3px rgba(0, 0, 0, 0.55)', - sm: '0 2px 6px rgba(0, 0, 0, 0.5)', - base: '0 4px 12px rgba(0, 0, 0, 0.48)', - lg: '0 8px 20px rgba(0, 0, 0, 0.45)', - xl: '0 12px 28px rgba(0, 0, 0, 0.42)', - '2xl': '0 16px 36px rgba(0, 0, 0, 0.38)', + xs: `0 1px 3px ${overlayBlack(0.55)}`, + sm: `0 2px 6px ${overlayBlack(0.5)}`, + base: `0 4px 12px ${overlayBlack(0.48)}`, + lg: `0 8px 20px ${overlayBlack(0.45)}`, + xl: `0 12px 28px ${overlayBlack(0.42)}`, + '2xl': `0 16px 36px ${overlayBlack(0.38)}`, }, glow: { blue: - '0 0 12px rgba(122, 162, 247, 0.35), 0 0 24px rgba(122, 162, 247, 0.2), 0 4px 16px rgba(0, 0, 0, 0.35)', + `0 0 12px ${tokyoAccent(0.35)}, 0 0 24px ${tokyoAccent(0.2)}, 0 4px 16px ${overlayBlack(0.35)}`, purple: - '0 0 12px rgba(187, 154, 247, 0.32), 0 0 24px rgba(187, 154, 247, 0.18), 0 4px 16px rgba(0, 0, 0, 0.35)', + `0 0 12px ${tokyoPurple(0.32)}, 0 0 24px ${tokyoPurple(0.18)}, 0 4px 16px ${overlayBlack(0.35)}`, mixed: - '0 0 16px rgba(122, 162, 247, 0.3), 0 0 28px rgba(187, 154, 247, 0.2), 0 4px 20px rgba(0, 0, 0, 0.35)', + `0 0 16px ${tokyoAccent(0.3)}, 0 0 28px ${tokyoPurple(0.2)}, 0 4px 20px ${overlayBlack(0.35)}`, }, blur: { @@ -176,74 +211,74 @@ export const bitfunTokyoNightTheme: ThemeConfig = { components: { windowControls: createWindowControls({ standard: { - dot: 'rgba(122, 162, 247, 0.55)', - dotShadow: '0 0 6px rgba(122, 162, 247, 0.35)', - hoverBg: 'rgba(122, 162, 247, 0.14)', - hoverColor: '#7aa2f7', - hoverBorder: 'rgba(122, 162, 247, 0.35)', + dot: tokyoAccent(0.55), + dotShadow: `0 0 6px ${tokyoAccent(0.35)}`, + hoverBg: tokyoAccent(0.14), + hoverColor: TOKYO_ACCENT, + hoverBorder: tokyoAccent(0.35), hoverShadow: - '0 0 12px rgba(122, 162, 247, 0.28), 0 2px 8px rgba(122, 162, 247, 0.15), inset 0 1px 0 rgba(122, 162, 247, 0.18)', + `0 0 12px ${tokyoAccent(0.28)}, 0 2px 8px ${tokyoAccent(0.15)}, inset 0 1px 0 ${tokyoAccent(0.18)}`, }, close: { - dot: 'rgba(247, 118, 142, 0.55)', - dotShadow: '0 0 6px rgba(247, 118, 142, 0.35)', - hoverBg: 'rgba(247, 118, 142, 0.14)', - hoverColor: '#f7768e', - hoverBorder: 'rgba(247, 118, 142, 0.35)', + dot: tokyoError(0.55), + dotShadow: `0 0 6px ${tokyoError(0.35)}`, + hoverBg: tokyoError(0.14), + hoverColor: TOKYO_ERROR, + hoverBorder: tokyoError(0.35), hoverShadow: - '0 0 12px rgba(247, 118, 142, 0.28), 0 2px 8px rgba(247, 118, 142, 0.15), inset 0 1px 0 rgba(247, 118, 142, 0.18)', + `0 0 12px ${tokyoError(0.28)}, 0 2px 8px ${tokyoError(0.15)}, inset 0 1px 0 ${tokyoError(0.18)}`, }, common: { - defaultColor: 'rgba(192, 202, 245, 0.92)', - defaultDot: 'rgba(122, 162, 247, 0.22)', - disabledDot: 'rgba(122, 162, 247, 0.12)', + defaultColor: rgbaFromHex(TOKYO_TEXT_PRIMARY, 0.92), + defaultDot: tokyoAccent(0.22), + disabledDot: tokyoAccent(0.12), flowGradient: - 'linear-gradient(90deg, transparent, rgba(122, 162, 247, 0.08), rgba(187, 154, 247, 0.1), rgba(122, 162, 247, 0.08), transparent)', + `linear-gradient(90deg, transparent, ${tokyoAccent(0.08)}, ${tokyoPurple(0.1)}, ${tokyoAccent(0.08)}, transparent)`, }, }), button: { default: { - background: 'rgba(122, 162, 247, 0.1)', - color: '#a9b1d6', - border: 'rgba(122, 162, 247, 0.22)', - shadow: '0 0 8px rgba(122, 162, 247, 0.12)', + background: tokyoAccent(0.1), + color: TOKYO_TEXT_SECONDARY, + border: tokyoAccent(0.22), + shadow: `0 0 8px ${tokyoAccent(0.12)}`, }, hover: { - background: 'rgba(122, 162, 247, 0.16)', - color: '#c0caf5', - border: 'rgba(122, 162, 247, 0.38)', - shadow: '0 0 16px rgba(122, 162, 247, 0.22), 0 2px 8px rgba(0, 0, 0, 0.35)', + background: tokyoAccent(0.16), + color: TOKYO_TEXT_PRIMARY, + border: tokyoAccent(0.38), + shadow: `0 0 16px ${tokyoAccent(0.22)}, 0 2px 8px ${overlayBlack(0.35)}`, transform: 'translateY(-1px)', }, active: { - background: 'rgba(122, 162, 247, 0.13)', - color: '#c0caf5', - border: 'rgba(122, 162, 247, 0.42)', - shadow: '0 0 12px rgba(122, 162, 247, 0.18)', + background: tokyoAccent(0.13), + color: TOKYO_TEXT_PRIMARY, + border: tokyoAccent(0.42), + shadow: `0 0 12px ${tokyoAccent(0.18)}`, transform: 'translateY(0)', }, primary: { default: { - background: 'rgba(61, 89, 161, 0.55)', - color: '#c0caf5', - border: 'rgba(122, 162, 247, 0.45)', - shadow: '0 0 14px rgba(61, 89, 161, 0.35)', + background: tokyoPrimaryButton(0.55), + color: TOKYO_TEXT_PRIMARY, + border: tokyoAccent(0.45), + shadow: `0 0 14px ${tokyoPrimaryButton(0.35)}`, }, hover: { - background: 'rgba(61, 89, 161, 0.72)', - color: '#ffffff', - border: 'rgba(122, 162, 247, 0.55)', + background: tokyoPrimaryButton(0.72), + color: STATIC_WHITE, + border: tokyoAccent(0.55), shadow: - '0 0 22px rgba(122, 162, 247, 0.35), 0 4px 12px rgba(0, 0, 0, 0.35)', + `0 0 22px ${tokyoAccent(0.35)}, 0 4px 12px ${overlayBlack(0.35)}`, transform: 'translateY(-2px)', }, active: { - background: 'rgba(61, 89, 161, 0.62)', - color: '#ffffff', - border: 'rgba(122, 162, 247, 0.48)', - shadow: '0 0 18px rgba(122, 162, 247, 0.28)', + background: tokyoPrimaryButton(0.62), + color: STATIC_WHITE, + border: tokyoAccent(0.48), + shadow: `0 0 18px ${tokyoAccent(0.28)}`, transform: 'translateY(-1px)', }, }, @@ -251,22 +286,22 @@ export const bitfunTokyoNightTheme: ThemeConfig = { ghost: { default: { background: 'transparent', - color: '#787c99', - border: 'rgba(54, 59, 84, 0.55)', + color: TOKYO_TEXT_MUTED, + border: tokyoBorder(0.55), shadow: 'none', }, hover: { - background: 'rgba(122, 162, 247, 0.1)', - color: '#c0caf5', - border: 'rgba(122, 162, 247, 0.35)', - shadow: '0 0 12px rgba(122, 162, 247, 0.15)', + background: tokyoAccent(0.1), + color: TOKYO_TEXT_PRIMARY, + border: tokyoAccent(0.35), + shadow: `0 0 12px ${tokyoAccent(0.15)}`, transform: 'translateY(-1px)', }, active: { - background: 'rgba(122, 162, 247, 0.08)', - color: '#a9b1d6', - border: 'rgba(122, 162, 247, 0.3)', - shadow: '0 0 8px rgba(122, 162, 247, 0.12)', + background: tokyoAccent(0.08), + color: TOKYO_TEXT_SECONDARY, + border: tokyoAccent(0.3), + shadow: `0 0 8px ${tokyoAccent(0.12)}`, transform: 'translateY(0)', }, }, @@ -278,11 +313,11 @@ export const bitfunTokyoNightTheme: ThemeConfig = { inherit: true, rules: [], colors: { - background: '#1a1b26', - foreground: '#a9b1d6', + background: TOKYO_BACKGROUND_PRIMARY, + foreground: TOKYO_TEXT_SECONDARY, lineHighlight: '#1e202e', selection: 'rgba(81, 92, 126, 0.35)', - cursor: '#c0caf5', + cursor: TOKYO_TEXT_PRIMARY, }, }, }; diff --git a/src/web-ui/src/tools/mermaid-editor/theme/_tokens.scss b/src/web-ui/src/tools/mermaid-editor/theme/_tokens.scss index 58686dd824..b0a67d7c1f 100644 --- a/src/web-ui/src/tools/mermaid-editor/theme/_tokens.scss +++ b/src/web-ui/src/tools/mermaid-editor/theme/_tokens.scss @@ -12,7 +12,9 @@ @use '../../../component-library/styles/tokens' as *; $mermaid-light-node-fill: rgba(15, 23, 42, 0.06); +$mermaid-light-node-fill-hover: rgba(15, 23, 42, 0.1); $mermaid-light-node-stroke: rgba(100, 116, 139, 0.35); +$mermaid-light-edge-label-border: rgba(100, 116, 139, 0.25); $mermaid-light-text-primary: #1e293b; $mermaid-light-text-secondary: #475569; $mermaid-light-surface: #f7f8fa; @@ -31,7 +33,7 @@ $mermaid-light-highlight-stroke: #334155; --mermaid-node-text: #{$color-text-primary}; --mermaid-node-radius: #{$size-radius-sm}; --mermaid-node-dash-array: 4 2; - + // ==================== Edge styles ==================== --mermaid-edge-stroke: #{$border-medium}; --mermaid-edge-stroke-hover: #{$border-strong}; @@ -40,7 +42,7 @@ $mermaid-light-highlight-stroke: #334155; --mermaid-edge-label-border: #{$border-subtle}; --mermaid-edge-label-text: #{$color-text-secondary}; --mermaid-arrow-color: #{$color-text-muted}; - + // ==================== Subgraph/cluster styles ==================== --mermaid-cluster-fill: #{$element-bg-subtle}; --mermaid-cluster-stroke: #{$border-subtle}; @@ -48,7 +50,7 @@ $mermaid-light-highlight-stroke: #334155; --mermaid-cluster-radius: #{$size-radius-base}; --mermaid-cluster-text: #{$color-text-secondary}; --mermaid-cluster-dash-array: 5 3; - + // ==================== Status colors ==================== --mermaid-success: #{$color-success}; --mermaid-success-bg: #{$color-success-bg}; @@ -58,17 +60,17 @@ $mermaid-light-highlight-stroke: #334155; --mermaid-warning-bg: #{$color-warning-bg}; --mermaid-info: #{$color-info}; --mermaid-info-bg: #{$color-info-bg}; - + // ==================== Highlight styles ==================== --mermaid-highlight-stroke: #{$color-accent-500}; --mermaid-highlight-glow: #{$glow-shadow-blue}; --mermaid-selected-stroke: #{$color-accent-600}; - + // ==================== Interaction effects ==================== --mermaid-transition-duration: #{$motion-base}; --mermaid-transition-easing: #{$easing-standard}; --mermaid-hover-filter: brightness(1.08); - + // ==================== Sequence diagram ==================== --mermaid-actor-fill: #{$element-bg-base}; --mermaid-actor-stroke: #{$border-medium}; @@ -80,7 +82,7 @@ $mermaid-light-highlight-stroke: #334155; --mermaid-note-text: #{$color-text-secondary}; --mermaid-activation-fill: rgba(74, 144, 217, 0.15); --mermaid-activation-stroke: #{$border-medium}; - + // ==================== Gantt chart ==================== --mermaid-section-fill: #{$color-bg-primary}; --mermaid-section-alt-fill: #{$color-bg-secondary}; @@ -92,7 +94,7 @@ $mermaid-light-highlight-stroke: #334155; --mermaid-active-stroke: #{$color-info}; --mermaid-crit-fill: #{$color-error-bg}; --mermaid-crit-stroke: #{$color-error}; - + // ==================== Pie chart ==================== --mermaid-pie-stroke: #{$color-bg-primary}; --mermaid-pie-stroke-width: 2px; @@ -106,17 +108,17 @@ $mermaid-light-highlight-stroke: #334155; --mermaid-pie-6: #d890b8; --mermaid-pie-7: #68c8d8; --mermaid-pie-8: #a8d868; - + // ==================== Class/ER diagram ==================== --mermaid-class-fill: #{$element-bg-base}; --mermaid-class-stroke: #{$border-medium}; --mermaid-class-text: #{$color-text-primary}; --mermaid-class-divider: #{$border-subtle}; - + // ==================== Git diagram ==================== --mermaid-commit-text: #{$color-text-primary}; --mermaid-branch-label: #{$color-text-primary}; - + // ==================== Background and container ==================== --mermaid-bg: var(--color-bg-scene); --mermaid-container-border: #{$border-subtle}; @@ -129,32 +131,32 @@ $mermaid-light-highlight-stroke: #334155; .light { // Node styles --mermaid-node-fill: #{$mermaid-light-node-fill}; - --mermaid-node-fill-hover: rgba(15, 23, 42, 0.1); + --mermaid-node-fill-hover: #{$mermaid-light-node-fill-hover}; --mermaid-node-stroke: #{$mermaid-light-node-stroke}; --mermaid-node-stroke-hover: rgba(51, 65, 85, 0.55); --mermaid-node-text: #{$mermaid-light-text-primary}; - + // Edges --mermaid-edge-stroke: #{$mermaid-light-node-stroke}; --mermaid-edge-stroke-hover: rgba(51, 65, 85, 0.5); --mermaid-edge-label-bg: #{$mermaid-light-surface}; - --mermaid-edge-label-border: rgba(100, 116, 139, 0.25); + --mermaid-edge-label-border: #{$mermaid-light-edge-label-border}; --mermaid-edge-label-text: #{$mermaid-light-text-secondary}; --mermaid-arrow-color: #64748b; - + // Subgraph/cluster --mermaid-cluster-fill: rgba(15, 23, 42, 0.04); - --mermaid-cluster-stroke: rgba(100, 116, 139, 0.25); + --mermaid-cluster-stroke: #{$mermaid-light-edge-label-border}; --mermaid-cluster-text: #{$mermaid-light-text-secondary}; - + // Highlight - slate (matches light theme focus / accent) --mermaid-highlight-stroke: #{$mermaid-light-highlight-stroke}; --mermaid-highlight-glow: drop-shadow(0 0 6px rgba(15, 23, 42, 0.18)); --mermaid-selected-stroke: #{$mermaid-light-text-primary}; - + // Interaction effects --mermaid-hover-filter: brightness(0.97); - + // Sequence diagram --mermaid-actor-fill: #{$mermaid-light-node-fill}; --mermaid-actor-stroke: #{$mermaid-light-node-stroke}; @@ -164,9 +166,9 @@ $mermaid-light-highlight-stroke: #334155; --mermaid-note-fill: rgba(184, 134, 58, 0.12); --mermaid-note-stroke: rgba(192, 140, 66, 0.4); --mermaid-note-text: #92400e; - --mermaid-activation-fill: rgba(15, 23, 42, 0.1); + --mermaid-activation-fill: #{$mermaid-light-node-fill-hover}; --mermaid-activation-stroke: rgba(51, 65, 85, 0.35); - + // Gantt chart --mermaid-section-fill: #{$mermaid-light-surface}; --mermaid-section-alt-fill: #f0f1f3; @@ -178,23 +180,23 @@ $mermaid-light-highlight-stroke: #334155; --mermaid-active-stroke: #{$mermaid-light-highlight-stroke}; --mermaid-crit-fill: rgba(194, 101, 101, 0.15); --mermaid-crit-stroke: #c26565; - + // Pie chart - cohesive colors. --mermaid-pie-stroke: #{$mermaid-light-surface}; --mermaid-pie-title-text: #{$mermaid-light-text-primary}; --mermaid-pie-legend-text: #{$mermaid-light-text-secondary}; --mermaid-pie-1: #{$mermaid-light-text-secondary}; - + // Class diagram --mermaid-class-fill: #{$mermaid-light-node-fill}; --mermaid-class-stroke: #{$mermaid-light-node-stroke}; --mermaid-class-text: #{$mermaid-light-text-primary}; --mermaid-class-divider: rgba(100, 116, 139, 0.2); - + // Git diagram --mermaid-commit-text: #{$mermaid-light-text-primary}; --mermaid-branch-label: #{$mermaid-light-text-primary}; - + // Background --mermaid-bg: var(--color-bg-scene); --mermaid-container-border: rgba(100, 116, 139, 0.18); diff --git a/src/web-ui/src/tools/mermaid-editor/theme/mermaidThemeFallbacks.ts b/src/web-ui/src/tools/mermaid-editor/theme/mermaidThemeFallbacks.ts index cc92d503a2..05dfe2ddd6 100644 --- a/src/web-ui/src/tools/mermaid-editor/theme/mermaidThemeFallbacks.ts +++ b/src/web-ui/src/tools/mermaid-editor/theme/mermaidThemeFallbacks.ts @@ -2,114 +2,137 @@ export const MERMAID_THEME_MODES = ['dark', 'light'] as const; export type MermaidThemeMode = typeof MERMAID_THEME_MODES[number]; +const DARK_NODE_FILL = '#1c1e23'; +const DARK_NODE_FILL_HOVER = '#262830'; +const DARK_NODE_TEXT = '#e0e2e8'; +const DARK_NODE_STROKE = '#5a5e6a'; +const DARK_NODE_BORDER = '#4a4e58'; +const DARK_NODE_STROKE_SUBTLE = '#3a3e48'; +const DARK_CLUSTER_TEXT = '#9a9ea8'; +const DARK_INFO = '#78a8d8'; +const DARK_ERROR = '#e87878'; + +const LIGHT_NODE_FILL = '#e8eaef'; +const LIGHT_NODE_FILL_HOVER = '#dfe2e8'; +const LIGHT_NODE_STROKE = '#9ca3af'; +const LIGHT_NODE_STROKE_MUTED = '#d1d5db'; +const LIGHT_NODE_STROKE_HOVER = '#6b7280'; +const LIGHT_EDGE_LABEL_BG = '#f3f4f6'; +const LIGHT_SECTION_ALT_FILL = '#e5e7eb'; +const LIGHT_TITLE_TEXT = '#111827'; +const LIGHT_HIGHLIGHT_STROKE = '#334155'; +const LIGHT_ERROR = '#dc2626'; +const LIGHT_WARNING = '#f59e0b'; +const LIGHT_CLUSTER_FILL = 'rgba(229, 231, 235, 0.7)'; + export const MERMAID_THEME_FALLBACKS = { dark: { - nodeFill: '#1c1e23', + nodeFill: DARK_NODE_FILL, nodeFillRuntime: 'rgba(28, 30, 35, 0.9)', - nodeFillHover: '#262830', + nodeFillHover: DARK_NODE_FILL_HOVER, nodeFillHoverRuntime: 'rgba(38, 40, 48, 0.95)', - nodeText: '#e0e2e8', + nodeText: DARK_NODE_TEXT, nodeTextStrong: '#f0f2f8', - nodeStroke: '#5a5e6a', - nodeBorder: '#4a4e58', - nodeStrokeMuted: '#4a4e58', - nodeStrokeSubtle: '#3a3e48', + nodeStroke: DARK_NODE_STROKE, + nodeBorder: DARK_NODE_BORDER, + nodeStrokeMuted: DARK_NODE_BORDER, + nodeStrokeSubtle: DARK_NODE_STROKE_SUBTLE, nodeStrokeHover: '#6a6e7a', nodeStrokeHoverStrong: '#8a8e9a', - edgeStroke: '#5a5e6a', + edgeStroke: DARK_NODE_STROKE, edgeLabelBorderHover: '#5a5e68', clusterFill: '#16181c', clusterFillRuntime: 'rgba(24, 26, 30, 0.6)', clusterFillHover: 'rgba(34, 36, 42, 0.7)', - clusterText: '#9a9ea8', + clusterText: DARK_CLUSTER_TEXT, textMuted: '#6a6e78', arrow: '#7a7e8a', edgeLabelBg: '#1a1c20', - edgeLabelBgStrong: '#262830', + edgeLabelBgStrong: DARK_NODE_FILL_HOVER, edgeLabelBgRuntime: 'rgba(26, 28, 32, 0.95)', edgeLabelBgHover: 'rgba(36, 38, 45, 0.98)', noteFill: '#222428', - noteText: '#9a9ea8', - noteStroke: '#4a4e58', + noteText: DARK_CLUSTER_TEXT, + noteStroke: DARK_NODE_BORDER, activationFill: '#2a2c32', - activationStroke: '#5a5e6a', - sectionFill: '#1c1e23', - sectionAltFill: '#262830', - gridStroke: '#3a3e48', + activationStroke: DARK_NODE_STROKE, + sectionFill: DARK_NODE_FILL, + sectionAltFill: DARK_NODE_FILL_HOVER, + gridStroke: DARK_NODE_STROKE_SUBTLE, doneFill: 'rgba(109, 212, 160, 0.15)', doneStroke: '#6dd4a0', activeFill: 'rgba(120, 168, 216, 0.15)', - activeStroke: '#78a8d8', + activeStroke: DARK_INFO, critFill: 'rgba(232, 120, 120, 0.15)', - critStroke: '#e87878', + critStroke: DARK_ERROR, warning: '#e8b060', - info: '#78a8d8', - taskClickableInfo: '#78a8d8', + info: DARK_INFO, + taskClickableInfo: DARK_INFO, pie5: '#a090d8', pie6: '#d890b8', pie7: '#68c8d8', pie8: '#a8d868', - pieTitleText: '#e0e2e8', - pieLegendText: '#9a9ea8', - pieStroke: '#1c1e23', + pieTitleText: DARK_NODE_TEXT, + pieLegendText: DARK_CLUSTER_TEXT, + pieStroke: DARK_NODE_FILL, errorFill: 'rgba(232, 120, 120, 0.12)', - error: '#e87878', + error: DARK_ERROR, highlightStroke: '#a8acb8', highlightGlow: 'drop-shadow(0 0 6px rgba(168, 172, 184, 0.4))', highlightGlowStrong: 'drop-shadow(0 0 10px rgba(168, 172, 184, 0.5))', }, light: { - nodeFill: '#e8eaef', - nodeFillRuntime: '#e8eaef', - nodeFillHover: '#dfe2e8', - nodeFillHoverRuntime: '#dfe2e8', + nodeFill: LIGHT_NODE_FILL, + nodeFillRuntime: LIGHT_NODE_FILL, + nodeFillHover: LIGHT_NODE_FILL_HOVER, + nodeFillHoverRuntime: LIGHT_NODE_FILL_HOVER, nodeText: '#1f2937', - nodeTextStrong: '#111827', - nodeStroke: '#9ca3af', - nodeBorder: '#9ca3af', - nodeStrokeMuted: '#d1d5db', - nodeStrokeSubtle: '#d1d5db', - nodeStrokeHover: '#6b7280', - nodeStrokeHoverStrong: '#6b7280', - edgeStroke: '#9ca3af', - edgeLabelBorderHover: '#9ca3af', - clusterFill: 'rgba(229, 231, 235, 0.7)', - clusterFillRuntime: 'rgba(229, 231, 235, 0.7)', + nodeTextStrong: LIGHT_TITLE_TEXT, + nodeStroke: LIGHT_NODE_STROKE, + nodeBorder: LIGHT_NODE_STROKE, + nodeStrokeMuted: LIGHT_NODE_STROKE_MUTED, + nodeStrokeSubtle: LIGHT_NODE_STROKE_MUTED, + nodeStrokeHover: LIGHT_NODE_STROKE_HOVER, + nodeStrokeHoverStrong: LIGHT_NODE_STROKE_HOVER, + edgeStroke: LIGHT_NODE_STROKE, + edgeLabelBorderHover: LIGHT_NODE_STROKE, + clusterFill: LIGHT_CLUSTER_FILL, + clusterFillRuntime: LIGHT_CLUSTER_FILL, clusterFillHover: 'rgba(209, 213, 219, 0.8)', clusterText: '#4b5563', - textMuted: '#6b7280', - arrow: '#6b7280', - edgeLabelBg: '#f3f4f6', - edgeLabelBgStrong: '#f3f4f6', - edgeLabelBgRuntime: '#f3f4f6', - edgeLabelBgHover: '#e5e7eb', + textMuted: LIGHT_NODE_STROKE_HOVER, + arrow: LIGHT_NODE_STROKE_HOVER, + edgeLabelBg: LIGHT_EDGE_LABEL_BG, + edgeLabelBgStrong: LIGHT_EDGE_LABEL_BG, + edgeLabelBgRuntime: LIGHT_EDGE_LABEL_BG, + edgeLabelBgHover: LIGHT_SECTION_ALT_FILL, noteFill: '#fef3c7', noteText: '#92400e', noteStroke: '#f59e0b', activationFill: 'rgba(147, 197, 253, 0.25)', activationStroke: '#93c5fd', - sectionFill: '#f3f4f6', - sectionAltFill: '#e5e7eb', + sectionFill: LIGHT_EDGE_LABEL_BG, + sectionAltFill: LIGHT_SECTION_ALT_FILL, gridStroke: 'rgba(156, 163, 175, 0.3)', doneFill: 'rgba(34, 197, 94, 0.2)', doneStroke: '#16a34a', activeFill: 'rgba(15, 23, 42, 0.08)', - activeStroke: '#334155', + activeStroke: LIGHT_HIGHLIGHT_STROKE, critFill: 'rgba(239, 68, 68, 0.2)', - critStroke: '#dc2626', - warning: '#f59e0b', + critStroke: LIGHT_ERROR, + warning: LIGHT_WARNING, info: '#64748b', taskClickableInfo: '#475569', pie5: '#8b5cf6', pie6: '#ec4899', pie7: '#06b6d4', pie8: '#84cc16', - pieTitleText: '#111827', + pieTitleText: LIGHT_TITLE_TEXT, pieLegendText: '#374151', pieStroke: '#f9fafb', errorFill: 'rgba(239, 68, 68, 0.15)', - error: '#dc2626', - highlightStroke: '#334155', + error: LIGHT_ERROR, + highlightStroke: LIGHT_HIGHLIGHT_STROKE, highlightGlow: 'drop-shadow(0 0 6px rgba(15, 23, 42, 0.18))', highlightGlowStrong: 'drop-shadow(0 0 10px rgba(15, 23, 42, 0.22))', },