diff --git a/docs/architecture/theme-token-optimization.md b/docs/architecture/theme-token-optimization.md index 82546b80e5..5e176e2486 100644 --- a/docs/architecture/theme-token-optimization.md +++ b/docs/architecture/theme-token-optimization.md @@ -158,14 +158,14 @@ prompt snapshot 投影;CLI/TUI 颜色作为独立终端产品 surface 单独 | 治理契约指标 | 当前值 | 说明 | | --- | ---: | --- | -| compatibility alias contracts | 0 | 显式 root/runtime 兼容别名已退役;历史 generated widget 内容通过 iframe fallback 兼容,尺寸旧 family 由 family contract 单独治理 | +| compatibility alias contracts | 0 | 显式 root/runtime 兼容别名已退役;历史 generated widget 内容通过 iframe fallback 兼容,尺寸旧 family 仅由 iframe fallback 和 family contract 识别 | | compatibility alias 直接使用 key | 0 | 产品代码不再通过 `var(--legacy-alias)` 读取旧 key;新增直接读取会被 baseline 拦截 | | compatibility alias 直接使用次数 | 0 | 显式旧 key 不再在 root/runtime 定义,内部样式读取统一使用 canonical token | | stale compatibility alias contracts | 0 | 防止 registry 重新保留没有必要的静态/runtime 兼容定义 | | compatibility alias family contracts | 2 | `--radius-* -> --size-radius-*`、`--spacing-* -> --size-gap-*` | | compatibility alias family 直接使用 key | 0 | `--radius-*`、`--spacing-*` 旧 family 不再被内部 `var()` 读取 | | compatibility alias family 直接使用次数 | 0 | generated widget payload 只暴露 canonical family,iframe shell 通过 alias fallback 兼容 legacy family | -| stale compatibility alias family contracts | 0 | 防止动态 family 或 canonical family 失配 | +| stale compatibility alias family contracts | 0 | 防止 family contract 指向缺失的 canonical family | | missing compatibility alias family canonicals | 0 | 防止新增 `--radius-x` / `--spacing-x` 但缺少对应 canonical key | | surface token rename contracts | 9 | 显式记录已迁移的 surface-local 旧 key、canonical 目标、owner 和命名边界 | | active surface token rename key | 0 | 防止 `--primary-color`、`--operation-color`、`--delay`、`--um-*` 等旧局部 key 回流 | @@ -188,10 +188,10 @@ prompt snapshot 投影;CLI/TUI 颜色作为独立终端产品 surface 单独 | 区域 | 当前出现次数 | 当前唯一色数 | 说明 | | --- | ---: | ---: | --- | -| Theme presets | 190 | 162 | 主题个性与 palette 映射,不作为普通 app literal 直接合并;仅在同一主题内角色可证明等价时由 preset helper 复用 | -| Token contracts | 128 | 114 | `tokens.scss` 等静态契约根 | +| Theme presets | 189 | 161 | 主题个性与 palette 映射,不作为普通 app literal 直接合并;仅在同一主题内角色可证明等价时由 preset helper 复用 | +| Token contracts | 127 | 113 | `tokens.scss` 等静态契约根 | | Editor | 55 | 48 | Monaco/editor 专用域,不能直接泛化到 app token;组件装饰色已迁出 raw literal | -| Mermaid | 93 | 74 | Mermaid 专用渲染域 | +| Mermaid | 91 | 73 | Mermaid 专用渲染域 | | Theme runtime | 35 | 34 | `ThemeService.ts` 运行时注入;暗色 card alpha fallback 和 scrollbar fallback 已收敛到现有 overlay stop | | Language identity | 52 | 50 | 语言身份色,已集中到 identity registry | | Terminal | 37 | 29 | terminal/ANSI 专用域;工具命令空状态已复用 `--tool-command-empty-rgb`,不再保留独立 raw 色 | @@ -220,14 +220,14 @@ theme preset 或 boundary fallback 误算成普通 app UI 债务。当前队列 | 文件 | 颜色出现次数 | 后续处理策略 | | --- | ---: | --- | -| `src/web-ui/src/component-library/styles/tokens.scss` | 115 | 根 token 契约;优先处理同语义 alias,避免把状态/层级 ramp 按数值强合并 | +| `src/web-ui/src/component-library/styles/tokens.scss` | 114 | 根 token 契约;优先处理同语义 alias,避免把状态/层级 ramp 按数值强合并 | | `src/web-ui/src/tools/mermaid-editor/theme/mermaidThemeFallbacks.ts` | 64 | 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` | 46 | Monaco theme palette;不拆散到普通 app token | | `src/web-ui/src/infrastructure/theme/core/ThemeService.ts` | 35 | 运行时注入;需保持 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` | 30 | theme preset palette;保留主题个性和 alpha ramp 边界 | +| `src/web-ui/src/infrastructure/theme/presets/slate-theme.ts` | 25 | theme preset palette;保留主题个性和 alpha ramp 边界 | 组件级 `var(--token, fallback)` 已收敛到 0;原先的 7 个 fallback token 不再需要 fallback contract registry 保留。 @@ -308,7 +308,7 @@ Phase 6 防回退约束: | `compatibilityAliases.familyUsedUnique` | 0 | 0 | 阻止 `--radius-*`、`--spacing-*` 旧 family 重新成为内部读取面 | | `compatibilityAliases.familyOccurrences` | 0 | 0 | 阻止旧 family 读取次数回涨 | | `compatibilityAliases.staleRegisteredUnique` | 0 | 0 | 防止兼容 alias registry 保留没有定义或 canonical 目标缺失的 key | -| `compatibilityAliases.staleRegisteredFamilyUnique` | 0 | 0 | 防止 `--radius-*`、`--spacing-*` 这类动态 family 与 canonical family 失配 | +| `compatibilityAliases.staleRegisteredFamilyUnique` | 0 | 0 | 防止 `--radius-*`、`--spacing-*` family contract 指向缺失的 canonical family | | `compatibilityAliases.missingCanonicalUnique` | 0 | 0 | 防止 family alias 具体 key 缺失对应 canonical key | | `surfaceTokenRenames.activeUnique` | 0 | 0 | 防止已迁移的 surface-local 旧 key 重新出现 | | `surfaceTokenRenames.activeOccurrences` | 0 | 0 | 防止旧 key 在定义和读取两侧回流 | @@ -541,8 +541,8 @@ semantic token 描述产品级语义,应作为共享 UI 的默认使用层。 | `--color-border-subtle` | `--border-subtle` | root/runtime alias 已退役;历史 generated widget 内容仅通过 iframe fallback 映射。 | | `--color-danger` | `--color-error` | root/runtime alias 已退役;历史 generated widget 内容仅通过 iframe fallback 映射到 error palette。 | | `--color-bg-hover` | `--element-bg-hover` | root/runtime alias 已退役;泛化 hover 收敛到 element interaction layer。 | -| `--radius-*` | `--size-radius-*` | canonical family 为 `--size-radius-*`,旧 family 只作 legacy/widget iframe fallback 兼容。 | -| `--spacing-*` | `--size-gap-*` | canonical family 为 `--size-gap-*`,旧 family 只作 legacy/widget iframe fallback 兼容。 | +| `--radius-*` | `--size-radius-*` | canonical family 为 `--size-radius-*`;旧 family 已从 root/runtime 退役,仅作为 generated widget iframe fallback 兼容和内部回读拦截规则。 | +| `--spacing-*` | `--size-gap-*` | canonical family 为 `--size-gap-*`;旧 family 已从 root/runtime 退役,仅作为 generated widget iframe fallback 兼容和内部回读拦截规则。 | ## 近似色合并规则 @@ -986,8 +986,15 @@ alpha 差异经常承担 elevation 和交互状态,不应全部压成一个值 stacking context,只通过 canonical color、spacing、shape、button 和 tool-card key 获取必要主题信息。 - 根 CSS var export 只保留被 `var()`、运行时主题注入或外部兼容边界实际消费的 key;组件内部仍可继续使用 SCSS token/mixin,避免把未消费的 badge、legacy effect、z-index、git/status 和局部布局 key 扩散为运行时 contract。 +- `colors.purple` 是次级强调色契约,不再等同完整 `AccentColors`。运行时只导出 + `50/100/200/400/500/600/800`,`300/700` 因无内部读取和 payload 消费已退役。 +- ThemeService 的 runtime dynamic family 采用显式白名单导出,包括 accent、purple、shadow、 + blur、radius、spacing、motion、easing、font weight、font size 和 line-height;custom theme + 中的额外字段不得自动外溢为 root key。FontPreference 也不再生成 `5xl`。 +- `effects.shadow` 运行时主题 scale 到 `xl` 为止;`$shadow-2xl` 仅作为 SCSS 编译期局部变量保留, + 不再进入 runtime theme contract。 - 尺寸 canonical family 是 `--size-radius-*` / `--size-gap-*`;`--radius-*` / - `--spacing-*` 只作为 legacy source 和 generated widget iframe alias fallback 保留。 + `--spacing-*` 已从 root/runtime 和内部 source 退役,只作为 generated widget iframe alias fallback 保留。 - 迁移期 CI 采用严格 baseline:普通 app raw color、内部 compatibility alias 读取、fallback、 未定义 CSS var、payload 未定义 key、payload 缺失 canonical 和 payload 未导出 canonical 均为 0; payload 兼容 alias 数量必须保持为 0,不能无依据增长。 diff --git a/scripts/audit-theme-colors.mjs b/scripts/audit-theme-colors.mjs index ccdd302451..bf526e5d8d 100644 --- a/scripts/audit-theme-colors.mjs +++ b/scripts/audit-theme-colors.mjs @@ -1154,7 +1154,7 @@ function audit(options) { : []; const staleCompatibilityAliasFamilyEntries = checksFullThemeSourceRoot ? compatibilityAliasFamilyEntries - .filter(entry => !entry.defined || !entry.canonicalDefined) + .filter(entry => !entry.canonicalDefined) .map(entry => ({ key: entry.key, canonical: entry.canonical })) : []; const uncontractedFallbackVars = fallbackVars diff --git a/scripts/audit-theme-colors.test.mjs b/scripts/audit-theme-colors.test.mjs index 11bb4a7e3b..f119db6f8e 100644 --- a/scripts/audit-theme-colors.test.mjs +++ b/scripts/audit-theme-colors.test.mjs @@ -891,8 +891,8 @@ test('theme color audit requires dynamic CSS var families to be registered', (t) test('theme color audit accepts registered dynamic CSS var families', (t) => { const { dir, sourceRoot } = createFixture({ 'infrastructure/theme/core/ThemeService.ts': [ - "for (const [key, value] of Object.entries(theme.effects.spacing)) {", - " document.documentElement.style.setProperty(`--spacing-${key}`, value);", + "for (const [key, value] of Object.entries(theme.motion.duration)) {", + " document.documentElement.style.setProperty(`--motion-${key}`, value);", '}', '', ].join('\n'), diff --git a/scripts/theme-color-governance-baseline.json b/scripts/theme-color-governance-baseline.json index bc3b2224a0..8771898b5d 100644 --- a/scripts/theme-color-governance-baseline.json +++ b/scripts/theme-color-governance-baseline.json @@ -105,10 +105,10 @@ "max": 0 }, "colorScopes.token.uniqueColors": { - "max": 257 + "max": 255 }, "colorScopes.exception.uniqueColors": { - "max": 246 + "max": 245 }, "cssVarDefinitions.unresolvedRequiredUnique": { "max": 0 @@ -237,16 +237,16 @@ "max": 0 }, "colorDomainScopes.themePreset.occurrences": { - "max": 190 + "max": 189 }, "colorDomainScopes.themePreset.uniqueColors": { - "max": 162 + "max": 161 }, "colorDomainScopes.themeRuntime.occurrences": { "max": 35 }, "colorDomainScopes.tokenContract.occurrences": { - "max": 128 + "max": 127 }, "colorDomainScopes.generatedWidget.occurrences": { "max": 0 @@ -294,10 +294,10 @@ "max": 0 }, "colorDomainScopes.mermaid.occurrences": { - "max": 93 + "max": 91 }, "colorDomainScopes.mermaid.uniqueColors": { - "max": 74 + "max": 73 } } } diff --git a/scripts/theme-css-var-contract.mjs b/scripts/theme-css-var-contract.mjs index e5b9126e79..c82f384292 100644 --- a/scripts/theme-css-var-contract.mjs +++ b/scripts/theme-css-var-contract.mjs @@ -212,16 +212,16 @@ export const TOKEN_COMPATIBILITY_ALIAS_FAMILY_CONTRACTS = [ { prefix: '--radius-', canonicalPrefix: '--size-radius-', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Radius aliases keep older selectors and generated widget iframe fallback working while size-radius is the canonical shape scale.', - removal: 'Retire after all source and generated widget iframe consumers migrate to --size-radius-*.', + owner: 'src/web-ui/src/tools/generative-widget/themePayloadCompatibility.ts', + reason: 'Radius aliases are retired from root/runtime but remain recognized so old generated widget iframe content maps to the canonical shape scale.', + removal: 'Retire after generated widget iframe compatibility no longer needs --radius-* fallbacks.', }, { prefix: '--spacing-', canonicalPrefix: '--size-gap-', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Spacing aliases keep older selectors and generated widget iframe fallback working while size-gap is the canonical spacing scale.', - removal: 'Retire after all source and generated widget iframe consumers migrate to --size-gap-*.', + owner: 'src/web-ui/src/tools/generative-widget/themePayloadCompatibility.ts', + reason: 'Spacing aliases are retired from root/runtime but remain recognized so old generated widget iframe content maps to the canonical spacing scale.', + removal: 'Retire after generated widget iframe compatibility no longer needs --spacing-* fallbacks.', }, ]; @@ -335,12 +335,6 @@ export const DYNAMIC_VAR_FAMILY_CONTRACTS = [ owner: 'src/web-ui/src/infrastructure/font-preference/core/FontPreferenceService.ts', reason: 'Font preference runtime exports navigation font-size aliases from the adjusted typography scale.', }, - { - prefix: '--radius-', - owner: 'src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - canonicalPrefix: '--size-radius-', - reason: 'Theme runtime exports configurable radius entries.', - }, { prefix: '--shadow-', owner: 'src/web-ui/src/infrastructure/theme/core/ThemeService.ts', @@ -356,12 +350,6 @@ export const DYNAMIC_VAR_FAMILY_CONTRACTS = [ owner: 'src/web-ui/src/infrastructure/theme/core/ThemeService.ts', reason: 'Size radius aliases are derived from theme radius entries.', }, - { - prefix: '--spacing-', - owner: 'src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - canonicalPrefix: '--size-gap-', - reason: 'Theme runtime exports configurable spacing entries.', - }, ]; export const REGISTERED_DYNAMIC_VAR_PREFIXES = new Set( diff --git a/src/web-ui/src/component-library/styles/nav-panel-font-scope.scss b/src/web-ui/src/component-library/styles/nav-panel-font-scope.scss index 10ca397e34..bdf9813ed6 100644 --- a/src/web-ui/src/component-library/styles/nav-panel-font-scope.scss +++ b/src/web-ui/src/component-library/styles/nav-panel-font-scope.scss @@ -14,5 +14,4 @@ --font-size-2xl: var(--nav-font-size-2xl); --font-size-3xl: var(--nav-font-size-3xl); --font-size-4xl: var(--nav-font-size-4xl); - --font-size-5xl: var(--nav-font-size-5xl); } diff --git a/src/web-ui/src/component-library/styles/tokens.scss b/src/web-ui/src/component-library/styles/tokens.scss index 53219683a4..9f0c4dface 100644 --- a/src/web-ui/src/component-library/styles/tokens.scss +++ b/src/web-ui/src/component-library/styles/tokens.scss @@ -108,7 +108,6 @@ $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); $color-purple-800: rgba(124, 58, 237, 0.9); // ==================== Semantic colors ==================== @@ -175,9 +174,6 @@ $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: $color-purple-500-a15; -$border-purple: $color-purple-500-a40; - // ==================== Shadow system ==================== $shadow-xs: 0 1px 2px $overlay-black-90; $shadow-sm: 0 2px 4px $overlay-black-80; @@ -360,7 +356,6 @@ $font-size-xl: 16px; $font-size-2xl: 18px; $font-size-3xl: 22px; $font-size-4xl: 26px; -$font-size-5xl: 32px; $line-height-tight: 1.2; $line-height-base: 1.5; @@ -384,7 +379,6 @@ $sidebar-width: 240px; $opacity-disabled: 0.6; $opacity-hover: 0.8; $opacity-focus: 0.9; -$opacity-overlay: 0.4; // ==================== Component-specific tokens ==================== @@ -865,11 +859,9 @@ $badge-info-text: $color-info; --color-purple-50: #{$color-purple-50}; --color-purple-100: #{$color-purple-100}; --color-purple-200: #{$color-purple-200}; - --color-purple-300: #{$color-purple-300}; --color-purple-400: #{$color-purple-400}; --color-purple-500: #{$color-purple-500}; --color-purple-600: #{$color-purple-600}; - --color-purple-700: #{$color-purple-700}; --color-purple-800: #{$color-purple-800}; --color-purple-soft: #a78bfa; --color-indigo-500: #6366f1; @@ -913,8 +905,6 @@ $badge-info-text: $color-info; --border-accent-subtle: #{$border-accent-subtle}; --border-accent: #{$border-accent}; --border-accent-strong: #{$border-accent-strong}; - --border-purple-subtle: #{$border-purple-subtle}; - --border-purple: #{$border-purple}; --z-base: #{$z-base}; --z-decoration: #{$z-decoration}; @@ -1201,9 +1191,6 @@ $badge-info-text: $color-info; --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}; @@ -1212,13 +1199,6 @@ $badge-info-text: $color-info; --size-radius-xl: #{$size-radius-xl}; --size-radius-2xl: #{$size-radius-2xl}; --size-radius-full: #{$size-radius-full}; - --radius-sm: var(--size-radius-sm); - --radius-base: var(--size-radius-base); - --radius-md: var(--size-radius-base); - --radius-lg: var(--size-radius-lg); - --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}; @@ -1230,16 +1210,6 @@ $badge-info-text: $color-info; --size-gap-10: #{$size-gap-10}; --size-gap-12: #{$size-gap-12}; --size-gap-16: #{$size-gap-16}; - --spacing-1: var(--size-gap-1); - --spacing-2: var(--size-gap-2); - --spacing-3: var(--size-gap-3); - --spacing-4: var(--size-gap-4); - --spacing-5: var(--size-gap-5); - --spacing-6: var(--size-gap-6); - --spacing-8: var(--size-gap-8); - --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}; @@ -1278,7 +1248,6 @@ $badge-info-text: $color-info; --font-size-2xl: #{$font-size-2xl}; --font-size-3xl: #{$font-size-3xl}; --font-size-4xl: #{$font-size-4xl}; - --font-size-5xl: #{$font-size-5xl}; /* Flow chat: mirrors UI scale until font preference overwrites (independent mode). */ --flowchat-font-size-xxs: var(--font-size-xxs); @@ -1356,7 +1325,6 @@ $badge-info-text: $color-info; --nav-font-size-2xl: var(--font-size-2xl); --nav-font-size-3xl: var(--font-size-3xl); --nav-font-size-4xl: var(--font-size-4xl); - --nav-font-size-5xl: var(--font-size-5xl); --bitfun-nav-row-action-size: 20px; --bitfun-nav-row-action-icon-size: 13px; --bitfun-nav-row-action-offset: 4px; @@ -1372,7 +1340,6 @@ $badge-info-text: $color-info; --opacity-disabled: #{$opacity-disabled}; --opacity-hover: #{$opacity-hover}; --opacity-focus: #{$opacity-focus}; - --opacity-overlay: #{$opacity-overlay}; --badge-font-size: #{$badge-font-size}; diff --git a/src/web-ui/src/infrastructure/font-preference/types/fontPreference.test.ts b/src/web-ui/src/infrastructure/font-preference/types/fontPreference.test.ts index 6c059b0b97..5b3760e637 100644 --- a/src/web-ui/src/infrastructure/font-preference/types/fontPreference.test.ts +++ b/src/web-ui/src/infrastructure/font-preference/types/fontPreference.test.ts @@ -16,6 +16,7 @@ describe('deriveFontSizeTokens', () => { expect(tokens.lg).toBe('15px'); expect(tokens.xl).toBe('16px'); expect(tokens['2xl']).toBe('18px'); + expect('5xl' in tokens).toBe(false); }); it('clamps below minimum (12px)', () => { diff --git a/src/web-ui/src/infrastructure/font-preference/types/index.ts b/src/web-ui/src/infrastructure/font-preference/types/index.ts index 440f8b76b0..f563531f4d 100644 --- a/src/web-ui/src/infrastructure/font-preference/types/index.ts +++ b/src/web-ui/src/infrastructure/font-preference/types/index.ts @@ -36,7 +36,6 @@ export interface FontSizeTokens { '2xl': string; '3xl': string; '4xl': string; - '5xl': string; } export type FontSizeLevelPresets = Record, FontSizeTokens>; @@ -65,7 +64,6 @@ export function deriveFontSizeTokens(basePx: number): FontSizeTokens { '2xl': `${b + 4}px`, '3xl': `${b + 8}px`, '4xl': `${b + 12}px`, - '5xl': `${b + 18}px`, }; } diff --git a/src/web-ui/src/infrastructure/theme/core/ThemeService.test.ts b/src/web-ui/src/infrastructure/theme/core/ThemeService.test.ts index 0a9aa7c3bc..a6c2c2e699 100644 --- a/src/web-ui/src/infrastructure/theme/core/ThemeService.test.ts +++ b/src/web-ui/src/infrastructure/theme/core/ThemeService.test.ts @@ -343,6 +343,87 @@ describe('ThemeService runtime theme tokens', () => { expect(configAPI.setConfig).not.toHaveBeenCalledWith('themes.custom', expect.anything()); }); + it('does not export non-contract dynamic keys from custom themes', () => { + const service = new ThemeService(); + const customTheme = { + ...bitfunLightTheme, + id: 'custom-extra-keys', + colors: { + ...bitfunLightTheme.colors, + accent: { + ...bitfunLightTheme.colors.accent, + 950: '#111111', + }, + purple: { + ...bitfunLightTheme.colors.purple, + 300: '#222222', + 700: '#333333', + }, + }, + effects: { + ...bitfunLightTheme.effects, + shadow: { + ...bitfunLightTheme.effects.shadow, + '2xl': '0 0 0 #111111', + }, + blur: { + ...bitfunLightTheme.effects.blur, + intense: 'blur(99px)', + }, + radius: { + ...bitfunLightTheme.effects.radius, + huge: '99px', + }, + spacing: { + ...bitfunLightTheme.effects.spacing, + 99: '99px', + }, + }, + motion: { + ...bitfunLightTheme.motion, + duration: { + ...bitfunLightTheme.motion.duration, + lazy: '99s', + }, + easing: { + ...bitfunLightTheme.motion.easing, + bounce: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)', + }, + }, + typography: { + ...bitfunLightTheme.typography, + weight: { + ...bitfunLightTheme.typography.weight, + black: 900, + }, + size: { + ...bitfunLightTheme.typography.size, + '5xl': '99px', + }, + lineHeight: { + ...bitfunLightTheme.typography.lineHeight, + loose: 2, + }, + }, + } as unknown as ThemeConfig; + + (service as unknown as { injectCSSVariables(theme: ThemeConfig): void }).injectCSSVariables(customTheme); + + const rootStyle = document.documentElement.style; + expect(rootStyle.getPropertyValue('--color-accent-950')).toBe(''); + expect(rootStyle.getPropertyValue('--color-purple-300')).toBe(''); + expect(rootStyle.getPropertyValue('--color-purple-700')).toBe(''); + expect(rootStyle.getPropertyValue('--shadow-2xl')).toBe(''); + expect(rootStyle.getPropertyValue('--blur-intense')).toBe(''); + expect(rootStyle.getPropertyValue('--size-radius-huge')).toBe(''); + expect(rootStyle.getPropertyValue('--size-gap-99')).toBe(''); + expect(rootStyle.getPropertyValue('--motion-lazy')).toBe(''); + expect(rootStyle.getPropertyValue('--easing-bounce')).toBe(''); + expect(rootStyle.getPropertyValue('--font-weight-black')).toBe(''); + expect(rootStyle.getPropertyValue('--font-size-5xl')).toBe(''); + expect(rootStyle.getPropertyValue('--line-height-loose')).toBe(''); + }); + it('skips invalid persisted custom themes before they reach preview or runtime injection', async () => { const invalidCustomTheme = { ...bitfunLightTheme, diff --git a/src/web-ui/src/infrastructure/theme/core/ThemeService.ts b/src/web-ui/src/infrastructure/theme/core/ThemeService.ts index 16b109f426..21767a5b59 100644 --- a/src/web-ui/src/infrastructure/theme/core/ThemeService.ts +++ b/src/web-ui/src/infrastructure/theme/core/ThemeService.ts @@ -37,6 +37,18 @@ const THEME_STATIC_COLORS = { black: '#000000', } as const; +const ACCENT_STOPS = [50, 100, 200, 300, 400, 500, 600, 700, 800] as const; +const SECONDARY_ACCENT_STOPS = [50, 100, 200, 400, 500, 600, 800] as const; +const SHADOW_TOKENS = ['xs', 'sm', 'base', 'lg', 'xl'] as const; +const BLUR_TOKENS = ['subtle', 'base'] as const; +const RADIUS_TOKENS = ['sm', 'base', 'lg', 'xl', '2xl', 'full'] as const; +const SPACING_TOKENS = [1, 2, 3, 4, 5, 6, 8, 10, 12, 16] as const; +const MOTION_DURATION_TOKENS = ['instant', 'fast', 'base', 'slow'] as const; +const EASING_TOKENS = ['standard', 'decelerate', 'smooth'] as const; +const FONT_WEIGHT_TOKENS = ['normal', 'medium', 'semibold', 'bold'] as const; +const FONT_SIZE_TOKENS = ['xs', 'sm', 'base', 'lg', 'xl', '2xl', '3xl', '4xl'] as const; +const LINE_HEIGHT_TOKENS = ['tight', 'base', 'relaxed'] as const; + const THEME_OVERLAYS = { white02: 'rgba(255, 255, 255, 0.02)', white04: 'rgba(255, 255, 255, 0.04)', @@ -556,16 +568,14 @@ export class ThemeService { root.style.setProperty('--color-bg-tooltip', colors.background.tooltip); } - root.style.setProperty('--color-overlay', theme.type === 'dark' ? THEME_OVERLAYS.black50 : THEME_OVERLAYS.black30); - - root.style.setProperty('--color-text-primary', colors.text.primary); root.style.setProperty('--color-text-secondary', colors.text.secondary); root.style.setProperty('--color-text-muted', colors.text.muted); root.style.setProperty('--color-text-disabled', colors.text.disabled); - Object.entries(colors.accent).forEach(([key, value]) => { + ACCENT_STOPS.forEach((key) => { + const value = colors.accent[key]; root.style.setProperty(`--color-accent-${key}`, value); }); @@ -581,8 +591,10 @@ export class ThemeService { } - if (colors.purple) { - Object.entries(colors.purple).forEach(([key, value]) => { + const secondaryAccent = colors.purple; + if (secondaryAccent) { + SECONDARY_ACCENT_STOPS.forEach((key) => { + const value = secondaryAccent[key]; root.style.setProperty(`--color-purple-${key}`, value); }); } @@ -649,48 +661,46 @@ export class ThemeService { root.style.setProperty('--color-scrollbar', scrollbarThumb); - if (effects?.shadow) { - Object.entries(effects.shadow).forEach(([key, value]) => { + const shadows = effects?.shadow; + if (shadows) { + SHADOW_TOKENS.forEach((key) => { + const value = shadows[key]; root.style.setProperty(`--shadow-${key}`, value); }); - root.style.setProperty('--glass-shadow-sm', effects.shadow.sm); - root.style.setProperty('--glass-shadow-base', effects.shadow.base); - root.style.setProperty('--glass-shadow-lg', effects.shadow.lg); - root.style.setProperty('--glass-shadow-xl', effects.shadow.xl); - } - - - if (effects?.glow) { - root.style.setProperty('--glow-blue', effects.glow.blue); - root.style.setProperty('--glow-purple', effects.glow.purple); - root.style.setProperty('--glow-mixed', effects.glow.mixed); + root.style.setProperty('--glass-shadow-sm', shadows.sm); + root.style.setProperty('--glass-shadow-base', shadows.base); + root.style.setProperty('--glass-shadow-lg', shadows.lg); + root.style.setProperty('--glass-shadow-xl', shadows.xl); } - if (effects?.blur) { - Object.entries(effects.blur).forEach(([key, value]) => { + const blurs = effects?.blur; + if (blurs) { + BLUR_TOKENS.forEach((key) => { + const value = blurs[key]; root.style.setProperty(`--blur-${key}`, value); }); - root.style.setProperty('--glass-blur-sm', effects.blur.subtle); - root.style.setProperty('--glass-blur-base', effects.blur.base); + root.style.setProperty('--glass-blur-sm', blurs.subtle); + root.style.setProperty('--glass-blur-base', blurs.base); } - if (effects?.radius) { - Object.entries(effects.radius).forEach(([key, value]) => { - root.style.setProperty(`--radius-${key}`, value); + const radii = effects?.radius; + if (radii) { + RADIUS_TOKENS.forEach((key) => { + const value = radii[key]; root.style.setProperty(`--size-radius-${key}`, value); }); - if (effects.radius.base) { - root.style.setProperty('--radius-md', effects.radius.base); - root.style.setProperty('--size-radius-md', effects.radius.base); + if (radii.base) { + root.style.setProperty('--size-radius-md', radii.base); } } - if (effects?.spacing) { - Object.entries(effects.spacing).forEach(([key, value]) => { - root.style.setProperty(`--spacing-${key}`, value); + const spacing = effects?.spacing; + if (spacing) { + SPACING_TOKENS.forEach((key) => { + const value = spacing[key]; root.style.setProperty(`--size-gap-${key}`, value); }); } @@ -700,19 +710,22 @@ export class ThemeService { root.style.setProperty('--opacity-disabled', String(effects.opacity.disabled)); root.style.setProperty('--opacity-hover', String(effects.opacity.hover)); root.style.setProperty('--opacity-focus', String(effects.opacity.focus)); - root.style.setProperty('--opacity-overlay', String(effects.opacity.overlay)); } - if (motion?.duration) { - Object.entries(motion.duration).forEach(([key, value]) => { + const motionDuration = motion?.duration; + if (motionDuration) { + MOTION_DURATION_TOKENS.forEach((key) => { + const value = motionDuration[key]; root.style.setProperty(`--motion-${key}`, value); }); } - if (motion?.easing) { - Object.entries(motion.easing).forEach(([key, value]) => { + const motionEasing = motion?.easing; + if (motionEasing) { + EASING_TOKENS.forEach((key) => { + const value = motionEasing[key]; root.style.setProperty(`--easing-${key}`, value); }); } @@ -724,22 +737,28 @@ export class ThemeService { } - if (typography?.weight) { - Object.entries(typography.weight).forEach(([key, value]) => { + const fontWeights = typography?.weight; + if (fontWeights) { + FONT_WEIGHT_TOKENS.forEach((key) => { + const value = fontWeights[key]; root.style.setProperty(`--font-weight-${key}`, String(value)); }); } - if (typography?.size) { - Object.entries(typography.size).forEach(([key, value]) => { + const typographySize = typography?.size; + if (typographySize) { + FONT_SIZE_TOKENS.forEach((key) => { + const value = typographySize[key]; root.style.setProperty(`--font-size-${key}`, value); }); } - if (typography?.lineHeight) { - Object.entries(typography.lineHeight).forEach(([key, value]) => { + const lineHeights = typography?.lineHeight; + if (lineHeights) { + LINE_HEIGHT_TOKENS.forEach((key) => { + const value = lineHeights[key]; root.style.setProperty(`--line-height-${key}`, String(value)); }); } @@ -837,26 +856,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' ? THEME_OVERLAYS.black70 : THEME_OVERLAYS.black50); - - - root.style.setProperty('--nav-bg', colors.background.secondary); - root.style.setProperty('--nav-item-bg-hover', colors.element.base); - root.style.setProperty('--nav-item-bg-active', colors.element.medium); - root.style.setProperty('--nav-item-text', colors.text.secondary); - root.style.setProperty('--nav-item-text-active', colors.text.primary); - - root.style.setProperty('--panel-bg', colors.background.primary); - root.style.setProperty('--panel-header-bg', colors.background.secondary); - root.style.setProperty('--panel-border', colors.border.base); - - - root.style.setProperty('--tooltip-bg', colors.background.elevated); - root.style.setProperty('--tooltip-border', colors.border.medium); - root.style.setProperty('--tooltip-text', colors.text.primary); root.setAttribute('data-theme', theme.id); 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 48717d0ecb..134165df8f 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 @@ -79,11 +79,9 @@ export const bitfunChinaNightTheme: ThemeConfig = { 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), }, @@ -144,21 +142,11 @@ export const bitfunChinaNightTheme: ThemeConfig = { 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 ${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)', - medium: 'blur(12px) saturate(1.2)', - strong: 'blur(16px) saturate(1.25) brightness(1.05)', - intense: 'blur(20px) saturate(1.3) brightness(1.08)', }, radius: createCompactRadius(), @@ -169,7 +157,6 @@ export const bitfunChinaNightTheme: ThemeConfig = { disabled: 0.45, hover: 0.75, focus: 0.9, - overlay: 0.5, }, }, @@ -180,7 +167,6 @@ export const bitfunChinaNightTheme: ThemeConfig = { fast: '0.2s', base: '0.35s', slow: '0.7s', - lazy: '1.2s', }, easing: createStandardEasing('cubic-bezier(0.25, 0.1, 0.25, 1)'), 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 8111c50d7a..2dea55d0e6 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 @@ -81,11 +81,9 @@ export const bitfunChinaStyleTheme: ThemeConfig = { 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), }, @@ -144,21 +142,11 @@ export const bitfunChinaStyleTheme: ThemeConfig = { 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 ${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)', - medium: 'blur(12px) saturate(1.08)', - strong: 'blur(16px) saturate(1.1) brightness(1.02)', - intense: 'blur(20px) saturate(1.12) brightness(1.03)', }, radius: createCompactRadius(), @@ -169,7 +157,6 @@ export const bitfunChinaStyleTheme: ThemeConfig = { disabled: 0.5, hover: 0.75, focus: 0.9, - overlay: 0.35, }, }, @@ -180,7 +167,6 @@ export const bitfunChinaStyleTheme: ThemeConfig = { fast: '0.2s', base: '0.35s', slow: '0.7s', - lazy: '1.2s', }, easing: createStandardEasing('cubic-bezier(0.25, 0.1, 0.25, 1)'), 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 7bc0662845..f04e826c05 100644 --- a/src/web-ui/src/infrastructure/theme/presets/cyber-theme.ts +++ b/src/web-ui/src/infrastructure/theme/presets/cyber-theme.ts @@ -80,11 +80,9 @@ export const bitfunCyberTheme: ThemeConfig = { 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), }, @@ -145,24 +143,11 @@ export const bitfunCyberTheme: ThemeConfig = { base: '0 4px 12px rgba(0, 0, 0, 0.8)', lg: '0 8px 20px rgba(0, 0, 0, 0.75)', 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 ${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)', - medium: 'blur(12px) saturate(1.4)', - strong: 'blur(16px) saturate(1.5) brightness(1.15)', - intense: 'blur(20px) saturate(1.6) brightness(1.2)', }, radius: createCompactRadius(), @@ -173,7 +158,6 @@ export const bitfunCyberTheme: ThemeConfig = { disabled: 0.5, hover: 0.85, focus: 0.95, - overlay: 0.5, }, }, @@ -184,7 +168,6 @@ export const bitfunCyberTheme: ThemeConfig = { fast: '0.12s', base: '0.25s', slow: '0.5s', - lazy: '0.8s', }, easing: createStandardEasing('cubic-bezier(0.25, 0.46, 0.45, 0.94)'), 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 35d6943656..f60ab09d48 100644 --- a/src/web-ui/src/infrastructure/theme/presets/dark-theme.ts +++ b/src/web-ui/src/infrastructure/theme/presets/dark-theme.ts @@ -83,11 +83,9 @@ export const bitfunDarkTheme: ThemeConfig = { 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), }, @@ -137,21 +135,11 @@ export const bitfunDarkTheme: ThemeConfig = { 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 ${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)', - medium: 'blur(12px) saturate(1.2)', - strong: 'blur(16px) saturate(1.3) brightness(1.1)', - intense: 'blur(20px) saturate(1.4) brightness(1.15)', }, radius: createStandardRadius(), @@ -162,7 +150,6 @@ export const bitfunDarkTheme: ThemeConfig = { disabled: 0.6, hover: 0.8, focus: 0.9, - overlay: 0.4, }, }, @@ -173,7 +160,6 @@ export const bitfunDarkTheme: ThemeConfig = { fast: '0.15s', base: '0.3s', slow: '0.6s', - lazy: '1s', }, easing: createStandardEasing(), 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 82f1793877..8ca2d71a72 100644 --- a/src/web-ui/src/infrastructure/theme/presets/light-theme.ts +++ b/src/web-ui/src/infrastructure/theme/presets/light-theme.ts @@ -85,11 +85,9 @@ export const bitfunLightTheme: ThemeConfig = { 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), }, @@ -155,22 +153,12 @@ export const bitfunLightTheme: ThemeConfig = { 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 ${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)', - medium: 'blur(12px) saturate(1.08)', - strong: 'blur(16px) saturate(1.10) brightness(1.02)', - intense: 'blur(20px) saturate(1.12) brightness(1.03)', }, radius: createStandardRadius(), @@ -181,7 +169,6 @@ export const bitfunLightTheme: ThemeConfig = { disabled: 0.55, hover: 0.75, focus: 0.9, - overlay: 0.35, }, }, @@ -192,7 +179,6 @@ export const bitfunLightTheme: ThemeConfig = { fast: '0.15s', base: '0.3s', slow: '0.6s', - lazy: '1s', }, easing: createStandardEasing(), 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 d928bcb9a1..cdb9c5f379 100644 --- a/src/web-ui/src/infrastructure/theme/presets/midnight-theme.ts +++ b/src/web-ui/src/infrastructure/theme/presets/midnight-theme.ts @@ -81,11 +81,9 @@ export const bitfunMidnightTheme: ThemeConfig = { 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), }, @@ -146,21 +144,11 @@ export const bitfunMidnightTheme: ThemeConfig = { 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 ${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)', - medium: 'blur(12px) saturate(1.25)', - strong: 'blur(16px) saturate(1.3) brightness(1.05)', - intense: 'blur(20px) saturate(1.4) brightness(1.1)', }, radius: createStandardRadius(), @@ -171,7 +159,6 @@ export const bitfunMidnightTheme: ThemeConfig = { disabled: 0.5, hover: 0.8, focus: 0.9, - overlay: 0.4, }, }, @@ -182,7 +169,6 @@ export const bitfunMidnightTheme: ThemeConfig = { fast: '0.15s', base: '0.3s', slow: '0.6s', - lazy: '1s', }, easing: createStandardEasing(), diff --git a/src/web-ui/src/infrastructure/theme/presets/shared.ts b/src/web-ui/src/infrastructure/theme/presets/shared.ts index ae3743f454..54d8437fb4 100644 --- a/src/web-ui/src/infrastructure/theme/presets/shared.ts +++ b/src/web-ui/src/infrastructure/theme/presets/shared.ts @@ -72,7 +72,6 @@ export function createStandardTypography(): ThemeConfig['typography'] { '2xl': '18px', '3xl': '22px', '4xl': '26px', - '5xl': '32px', }, lineHeight: { tight: 1.2, @@ -114,7 +113,6 @@ export function createChinaTypography(): ThemeConfig['typography'] { '2xl': '18px', '3xl': '22px', '4xl': '26px', - '5xl': '32px', }, lineHeight: { tight: 1.3, @@ -176,8 +174,6 @@ export function createStandardEasing(smooth = 'cubic-bezier(0.4, 0, 0.2, 1)'): T return { standard: 'cubic-bezier(0.4, 0, 0.2, 1)', decelerate: 'cubic-bezier(0, 0, 0.2, 1)', - accelerate: 'cubic-bezier(0.4, 0, 1, 1)', - bounce: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)', smooth, }; } 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 2ae8f37553..d6a0f2f6c6 100644 --- a/src/web-ui/src/infrastructure/theme/presets/slate-theme.ts +++ b/src/web-ui/src/infrastructure/theme/presets/slate-theme.ts @@ -32,7 +32,6 @@ 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); const slatePurpleHover = (alpha: number | string) => rgbaFromHex(SLATE_PURPLE_HOVER, alpha); @@ -91,11 +90,9 @@ export const bitfunSlateTheme: ThemeConfig = { 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), }, @@ -145,21 +142,11 @@ export const bitfunSlateTheme: ThemeConfig = { 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 ${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)', - medium: 'blur(12px) saturate(1.12) brightness(0.97)', - strong: 'blur(16px) saturate(1.15) brightness(0.97)', - intense: 'blur(20px) saturate(1.18) brightness(0.96)', }, radius: createSlateRadius(), @@ -170,7 +157,6 @@ export const bitfunSlateTheme: ThemeConfig = { disabled: 0.5, hover: 0.75, focus: 0.85, - overlay: 0.5, }, }, @@ -181,7 +167,6 @@ export const bitfunSlateTheme: ThemeConfig = { fast: '0.12s', base: '0.25s', slow: '0.5s', - lazy: '0.8s', }, easing: createStandardEasing(), 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 537532d079..e32e79694e 100644 --- a/src/web-ui/src/infrastructure/theme/presets/themePresetOutput.test.ts +++ b/src/web-ui/src/infrastructure/theme/presets/themePresetOutput.test.ts @@ -67,42 +67,42 @@ describe('builtin theme preset output', () => { }))).toMatchInlineSnapshot(` [ { - "hash": "44a6f6daeceecfb0166c667b3252f5766f98de7811599079d58d485605ba5857", + "hash": "f1c87b4cbe320d7f174a272aa08bd1df8baf02683d2275bac65c8ab4c46795bd", "id": "bitfun-light", "type": "light", }, { - "hash": "7bfb47bdfd3658b51c5c3a126aff9fafba00cf83a79d6ee148cfe3cecf9095d0", + "hash": "b629c2e63fa570ea4e43fbe5cf3878eca55111fca31715545be8d04ca07aeb56", "id": "bitfun-slate", "type": "dark", }, { - "hash": "2033449df1da52308c856bc9fa0bb006ca9416545c7b3971ee0c1700fd3954d2", + "hash": "235f2c1d67c65426031fb5f6c7f47b37e9a29e102adb501194a6cb87dbbb6880", "id": "bitfun-dark", "type": "dark", }, { - "hash": "2b5f34aa379e97bbea554fa8b376d951c8927bd2d46a25e04cef39d16742ba8d", + "hash": "17c6a0cec1878808c88b87b8a09e3bbc0d536ceb87de4d5b745cd4409e859101", "id": "bitfun-midnight", "type": "dark", }, { - "hash": "e1b7b1ad6ef0bd0b9cb7bc5b020da2112a43367a989837d19856179a24da529d", + "hash": "701165cbf33a44d92547024f1c93c735e57a4479934ac82f913deccebc5c3c40", "id": "bitfun-china-style", "type": "light", }, { - "hash": "ad731afd2ca6dd1cf75d286a14ffc82bc6cc074cdadff68e1daf382e3c445f4a", + "hash": "dd58088bd1558fcf17962e37cda63704c846a723d7408689c93142b78d12acf3", "id": "bitfun-china-night", "type": "dark", }, { - "hash": "b37c0cb6d539a703d1b6c0eca6c374d555ff74c0de0e2ec357bd56241a848fd4", + "hash": "c40b87616669c17552c46e158dc6a6e245c3f450daf9c00c4b5c4b1fcf0801c2", "id": "bitfun-cyber", "type": "dark", }, { - "hash": "beae87b070e7b95463d50f25e715d7e8268a25aa43b8c8b6775de0ff728732c6", + "hash": "c276008fcdbac289f0893cbcb3cdc4227e6dade9870b923d7b606f4eb8701873", "id": "bitfun-tokyo-night", "type": "dark", }, diff --git a/src/web-ui/src/infrastructure/theme/presets/tokyo-night-theme.ts b/src/web-ui/src/infrastructure/theme/presets/tokyo-night-theme.ts index e3a6a309df..91122c6b10 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 @@ -90,11 +90,9 @@ export const bitfunTokyoNightTheme: ThemeConfig = { 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), }, @@ -160,24 +158,11 @@ export const bitfunTokyoNightTheme: ThemeConfig = { 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 ${tokyoAccent(0.35)}, 0 0 24px ${tokyoAccent(0.2)}, 0 4px 16px ${overlayBlack(0.35)}`, - purple: - `0 0 12px ${tokyoPurple(0.32)}, 0 0 24px ${tokyoPurple(0.18)}, 0 4px 16px ${overlayBlack(0.35)}`, - mixed: - `0 0 16px ${tokyoAccent(0.3)}, 0 0 28px ${tokyoPurple(0.2)}, 0 4px 20px ${overlayBlack(0.35)}`, }, blur: { subtle: 'blur(4px) saturate(1.15)', base: 'blur(8px) saturate(1.2)', - medium: 'blur(12px) saturate(1.25)', - strong: 'blur(16px) saturate(1.3) brightness(1.08)', - intense: 'blur(20px) saturate(1.35) brightness(1.1)', }, radius: createCompactRadius(), @@ -188,7 +173,6 @@ export const bitfunTokyoNightTheme: ThemeConfig = { disabled: 0.5, hover: 0.88, focus: 0.96, - overlay: 0.52, }, }, @@ -198,7 +182,6 @@ export const bitfunTokyoNightTheme: ThemeConfig = { fast: '0.12s', base: '0.25s', slow: '0.5s', - lazy: '0.8s', }, easing: createStandardEasing('cubic-bezier(0.25, 0.46, 0.45, 0.94)'), diff --git a/src/web-ui/src/infrastructure/theme/types/index.ts b/src/web-ui/src/infrastructure/theme/types/index.ts index c52ac8d905..be1750a8ca 100644 --- a/src/web-ui/src/infrastructure/theme/types/index.ts +++ b/src/web-ui/src/infrastructure/theme/types/index.ts @@ -51,7 +51,10 @@ export interface AccentColors { 800: ColorValue; } - +export type SecondaryAccentStop = 50 | 100 | 200 | 400 | 500 | 600 | 800; +export type SecondaryAccentColors = Pick; + + export interface SemanticColors { success: ColorValue; successBg: ColorValue; @@ -118,23 +121,12 @@ export interface ShadowConfig { base: string; lg: string; xl: string; - '2xl': string; -} - - -export interface GlowConfig { - blue: string; - purple: string; - mixed: string; } export interface BlurConfig { subtle: string; base: string; - medium: string; - strong: string; - intense: string; } @@ -166,7 +158,6 @@ export interface OpacityConfig { disabled: number; hover: number; focus: number; - overlay: number; } @@ -224,15 +215,12 @@ export interface MotionConfig { fast: string; base: string; slow: string; - lazy: string; } export interface EasingConfig { standard: string; decelerate: string; - accelerate: string; - bounce: string; smooth: string; } @@ -262,7 +250,6 @@ export interface FontSizeConfig { '2xl': string; '3xl': string; '4xl': string; - '5xl': string; } @@ -317,7 +304,7 @@ export interface ThemeConfig { background: BackgroundColors; text: TextColors; accent: AccentColors; - purple?: AccentColors; + purple?: SecondaryAccentColors; semantic: SemanticColors; border: BorderColors; element: ElementBackgrounds; @@ -328,7 +315,6 @@ export interface ThemeConfig { effects: { shadow: ShadowConfig; - glow: GlowConfig; blur: BlurConfig; radius: RadiusConfig; spacing: SpacingConfig; 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 cd7bf179df..67a1701201 100644 --- a/src/web-ui/src/tools/mermaid-editor/theme/_tokens.scss +++ b/src/web-ui/src/tools/mermaid-editor/theme/_tokens.scss @@ -32,13 +32,11 @@ $mermaid-light-highlight-stroke: #334155; --mermaid-node-stroke-hover: #{$border-strong}; --mermaid-node-stroke-width: 1.5px; --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}; - --mermaid-edge-stroke-width: 1.5px; --mermaid-edge-label-bg: #{$color-bg-secondary}; --mermaid-edge-label-border: #{$border-subtle}; --mermaid-edge-label-text: #{$color-text-secondary}; @@ -54,18 +52,14 @@ $mermaid-light-highlight-stroke: #334155; // ==================== Status colors ==================== --mermaid-success: #{$color-success}; - --mermaid-success-bg: #{$color-success-bg}; --mermaid-error: #{$color-error}; --mermaid-error-bg: #{$color-error-bg}; --mermaid-warning: #{$color-warning}; - --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}; @@ -111,18 +105,7 @@ $mermaid-light-highlight-stroke: #334155; --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}; } // ==================== Light theme overrides ==================== @@ -153,7 +136,6 @@ $mermaid-light-highlight-stroke: #334155; // 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); @@ -189,18 +171,7 @@ $mermaid-light-highlight-stroke: #334155; --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.2); } // ==================== Mixins ====================