diff --git a/BitFun-Installer/src-tauri/src/installer/types.rs b/BitFun-Installer/src-tauri/src/installer/types.rs index 03c7933e63..0b692ccb48 100644 --- a/BitFun-Installer/src-tauri/src/installer/types.rs +++ b/BitFun-Installer/src-tauri/src/installer/types.rs @@ -70,6 +70,9 @@ impl From for RemoteModelInfo { pub enum ConnectionTestMessageCode { ToolCallsNotDetected, ImageInputCheckFailed, + TlsOrCertificateIssue, + ProxyIssue, + NetworkIssue, } impl From for ConnectionTestMessageCode { @@ -81,6 +84,13 @@ impl From for ConnectionTe bitfun_ai_adapters::types::ConnectionTestMessageCode::ImageInputCheckFailed => { Self::ImageInputCheckFailed } + bitfun_ai_adapters::types::ConnectionTestMessageCode::TlsOrCertificateIssue => { + Self::TlsOrCertificateIssue + } + bitfun_ai_adapters::types::ConnectionTestMessageCode::ProxyIssue => Self::ProxyIssue, + bitfun_ai_adapters::types::ConnectionTestMessageCode::NetworkIssue => { + Self::NetworkIssue + } } } } diff --git a/BitFun-Installer/src/pages/LanguageSelect.tsx b/BitFun-Installer/src/pages/LanguageSelect.tsx index a5b85aeb7e..97e00653d5 100644 --- a/BitFun-Installer/src/pages/LanguageSelect.tsx +++ b/BitFun-Installer/src/pages/LanguageSelect.tsx @@ -44,13 +44,13 @@ export function LanguageSelect({ onSelect }: LanguageSelectProps) { BitFun

BitFun

@@ -106,9 +106,9 @@ export function LanguageSelect({ onSelect }: LanguageSelectProps) { style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '14px 16px', width: '100%', - background: isSelected ? 'rgba(96, 165, 250, 0.14)' : 'rgba(148, 163, 184, 0.08)', + background: isSelected ? 'var(--color-accent-100)' : 'var(--element-bg-subtle)', border: 'none', - borderRadius: 'var(--radius-sm)', + borderRadius: 'var(--size-radius-sm)', cursor: 'pointer', textAlign: 'left', transition: 'all 0.25s ease', outline: 'none', @@ -117,12 +117,12 @@ export function LanguageSelect({ onSelect }: LanguageSelectProps) { }} onMouseEnter={(e) => { if (!isSelected) { - e.currentTarget.style.background = 'rgba(148, 163, 184, 0.14)'; + e.currentTarget.style.background = 'var(--element-bg-soft)'; } }} onMouseLeave={(e) => { if (!isSelected) { - e.currentTarget.style.background = 'rgba(148, 163, 184, 0.08)'; + e.currentTarget.style.background = 'var(--element-bg-subtle)'; } }} > diff --git a/BitFun-Installer/src/pages/Options.tsx b/BitFun-Installer/src/pages/Options.tsx index accb3cad26..36674bc253 100644 --- a/BitFun-Installer/src/pages/Options.tsx +++ b/BitFun-Installer/src/pages/Options.tsx @@ -107,7 +107,7 @@ export function Options({ ) : null} {!existingInstall.mainBinaryPresent ? ( -
+
{t('options.existingInstallBinaryMissing')}
) : null} diff --git a/BitFun-Installer/src/pages/ThemeSetup.tsx b/BitFun-Installer/src/pages/ThemeSetup.tsx index 200ffc25d9..af3ef32666 100644 --- a/BitFun-Installer/src/pages/ThemeSetup.tsx +++ b/BitFun-Installer/src/pages/ThemeSetup.tsx @@ -29,7 +29,7 @@ export function ThemeSetup({ options, setOptions, onLaunch, onClose }: ThemeSetu width: '100%', borderRadius: 12, padding: 8, - background: active ? 'rgba(96, 165, 250, 0.14)' : 'rgba(148, 163, 184, 0.08)', + background: active ? 'var(--color-accent-100)' : 'var(--element-bg-subtle)', border: 'none', cursor: 'pointer', transition: 'background 0.2s ease', diff --git a/BitFun-Installer/src/styles/global.css b/BitFun-Installer/src/styles/global.css index 86ba0d0230..111b92f9ac 100644 --- a/BitFun-Installer/src/styles/global.css +++ b/BitFun-Installer/src/styles/global.css @@ -27,7 +27,7 @@ html, body, #root { display: flex; align-items: center; justify-content: space-between; padding: 0 16px; background: var(--color-bg-primary); - border-bottom: 1px solid rgba(148, 163, 184, 0.12); + border-bottom: 1px solid var(--border-subtle); -webkit-app-region: drag; flex-shrink: 0; position: relative; z-index: 10; } @@ -58,20 +58,20 @@ html, body, #root { width: 32px; height: 32px; padding: 0; background: transparent; border: 1px solid transparent; - border-radius: var(--radius-sm); - color: #8a8a8a; + border-radius: var(--size-radius-sm); + color: var(--color-text-muted); cursor: pointer; outline: none; transition: background var(--motion-fast) ease, color var(--motion-fast) ease; } .window-controls__btn:hover { - background: rgba(255, 255, 255, 0.06); - color: #b8b8b8; + background: var(--element-bg-subtle); + color: var(--color-text-secondary); } .window-controls__btn:active { opacity: 0.8; } .window-controls__btn--close:hover { - background: rgba(255, 255, 255, 0.06); - color: #ef4444; + background: var(--element-bg-subtle); + color: var(--color-error); } .installer-content { @@ -115,7 +115,7 @@ html, body, #root { gap: 10px; flex-wrap: wrap; padding: 10px 22px 12px; - border-top: 1px solid rgba(148, 163, 184, 0.12); + border-top: 1px solid var(--border-subtle); background: var(--color-bg-primary); flex-shrink: 0; } @@ -135,7 +135,7 @@ html, body, #root { min-height: 28px; background: transparent; border: 1px dashed var(--border-base); - border-radius: var(--radius-sm); + border-radius: var(--size-radius-sm); font-family: var(--font-sans); font-size: 13px; font-weight: 500; color: var(--color-text-muted); @@ -204,14 +204,14 @@ html, body, #root { width: 100%; background: transparent; border: 1px dashed var(--border-base); - border-radius: var(--radius-sm); + border-radius: var(--size-radius-sm); cursor: pointer; text-align: left; box-shadow: none; transition: background 0.25s ease, transform 0.2s ease, border-color 0.25s ease, border-style 0.25s ease; position: relative; overflow: hidden; } .card-btn:hover { - background: rgba(148, 163, 184, 0.08); + background: var(--element-bg-subtle); border-style: solid; border-color: var(--border-medium); transform: translateY(-1px); @@ -220,17 +220,17 @@ html, body, #root { .card-btn:active { transform: translateY(0); } .card-btn:focus-visible { outline: none; - background: rgba(148, 163, 184, 0.14); + background: var(--element-bg-soft); box-shadow: none; } -.card-btn:hover .card-btn__icon { background: rgba(148, 163, 184, 0.16); } +.card-btn:hover .card-btn__icon { background: var(--element-bg-soft); } .card-btn:hover .card-btn__icon svg { color: var(--color-accent-600); } .card-btn:hover .card-btn__arrow { opacity: 0.8; transform: translateX(0); } .card-btn__icon { flex-shrink: 0; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; - background: rgba(148, 163, 184, 0.1); + background: var(--element-bg-subtle); border-radius: 10px; transition: all 0.25s ease; } @@ -252,7 +252,7 @@ html, body, #root { flex: 1; padding: 10px 14px; background: var(--element-bg-subtle); border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); + border-radius: var(--size-radius-sm); color: var(--color-text-primary); font-size: 12px; font-family: var(--font-mono); outline: none; transition: all 0.25s ease; @@ -275,7 +275,7 @@ html, body, #root { gap: 8px; background: var(--element-bg-subtle); border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); + border-radius: var(--size-radius-sm); color: var(--color-text-primary); cursor: pointer; text-align: left; @@ -326,8 +326,8 @@ html, body, #root { padding: 4px; background: var(--color-bg-secondary); border: 1px solid var(--border-medium); - border-radius: var(--radius-sm); - box-shadow: 0 8px 18px rgba(0, 0, 0, 0.25); + border-radius: var(--size-radius-sm); + box-shadow: none; } .bf-select-search { @@ -343,7 +343,7 @@ html, body, #root { min-height: 30px; padding: 6px 8px; border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); + border-radius: var(--size-radius-sm); background: var(--element-bg-subtle); color: var(--color-text-primary); font-size: 12px; @@ -366,7 +366,7 @@ html, body, #root { background: transparent; color: var(--color-text-primary); padding: 8px; - border-radius: var(--radius-sm); + border-radius: var(--size-radius-sm); text-align: left; display: flex; flex-direction: column; @@ -538,7 +538,7 @@ html, body, #root { gap: 10px; flex-wrap: wrap; padding: 10px 22px 12px; - border-top: 1px solid rgba(148, 163, 184, 0.12); + border-top: 1px solid var(--border-subtle); background: var(--color-bg-primary); flex-shrink: 0; } @@ -558,7 +558,7 @@ html, body, #root { .checkbox-item { display: flex; align-items: center; gap: 10px; cursor: pointer; padding: 8px 10px; - border-radius: var(--radius-sm); + border-radius: var(--size-radius-sm); transition: background var(--motion-fast) ease; } .checkbox-item:hover { background: var(--element-bg-subtle); } @@ -571,7 +571,7 @@ html, body, #root { transition: all var(--motion-fast) ease; flex-shrink: 0; } .checkbox-box.checked { background: var(--color-accent-500); border-color: var(--color-accent-500); } -.checkbox-box svg { width: 10px; height: 10px; color: white; opacity: 0; transition: opacity var(--motion-fast) ease; } +.checkbox-box svg { width: 10px; height: 10px; color: var(--color-bg-secondary); opacity: 0; transition: opacity var(--motion-fast) ease; } .checkbox-box.checked svg { opacity: 1; } .checkbox-label { font-size: 13px; color: var(--color-text-secondary); transition: color var(--motion-fast) ease; } @@ -590,7 +590,7 @@ html, body, #root { } .progress-bar-fill::after { content: ''; position: absolute; inset: 0; - background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent); + background: linear-gradient(90deg, transparent, var(--color-accent-200), transparent); background-size: 200% 100%; animation: shimmer 2s linear infinite; } diff --git a/BitFun-Installer/src/styles/variables.css b/BitFun-Installer/src/styles/variables.css index ff5e39d44c..871aedaeaa 100644 --- a/BitFun-Installer/src/styles/variables.css +++ b/BitFun-Installer/src/styles/variables.css @@ -20,17 +20,13 @@ --color-accent-400: rgba(96, 165, 250, 0.4); --color-accent-500: #60a5fa; --color-accent-600: #3b82f6; - --color-accent-700: rgba(59, 130, 246, 0.8); - --color-accent-800: rgba(59, 130, 246, 0.9); --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-500: #8b5cf6; --color-purple-600: #7c3aed; - --color-purple-700: rgba(124, 58, 237, 0.8); --color-purple-800: rgba(124, 58, 237, 0.9); --color-success: #34d399; @@ -53,9 +49,9 @@ --border-strong: rgba(255, 255, 255, 0.32); --border-prominent: rgba(225, 171, 128, 0.50); - --radius-sm: 6px; - --radius-base: 8px; - --radius-lg: 12px; + --size-radius-sm: 6px; + --size-radius-base: 8px; + --size-radius-lg: 12px; --font-sans: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'SF Pro Display', diff --git a/BitFun-Installer/src/theme/installerThemeRuntime.ts b/BitFun-Installer/src/theme/installerThemeRuntime.ts index c438c90ccd..538954a5fa 100644 --- a/BitFun-Installer/src/theme/installerThemeRuntime.ts +++ b/BitFun-Installer/src/theme/installerThemeRuntime.ts @@ -3,6 +3,9 @@ import { SYSTEM_THEME_ID, type ThemeId, type ThemePreferenceId } from '../types/ import type { InstallerTheme } from './installerThemesData'; import { findInstallerThemeById } from './installerThemesData'; +const ACCENT_STOPS = ['50', '100', '200', '300', '400', '500', '600'] as const; +const SECONDARY_ACCENT_STOPS = ['50', '100', '200', '400', '500', '600', '800'] as const; + /** Same rule as main app `getSystemPreferredDefaultThemeId`: dark -> bitfun-dark, else bitfun-light. */ export function getSystemPreferredBuiltinThemeId(): ThemeId { if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') { @@ -45,15 +48,13 @@ export function applyInstallerThemeToDocument(theme: InstallerTheme): void { root.style.setProperty('--color-highlight', colors.semantic.highlight); root.style.setProperty('--color-highlight-bg', colors.semantic.highlightBg); - Object.entries(colors.accent).forEach(([key, value]) => { - root.style.setProperty(`--color-accent-${key}`, value); + ACCENT_STOPS.forEach((key) => { + root.style.setProperty(`--color-accent-${key}`, colors.accent[key]); }); - if (colors.purple) { - Object.entries(colors.purple).forEach(([key, value]) => { - root.style.setProperty(`--color-purple-${key}`, value); - }); - } + SECONDARY_ACCENT_STOPS.forEach((key) => { + root.style.setProperty(`--color-purple-${key}`, colors.purple[key]); + }); root.setAttribute('data-theme', theme.id); root.setAttribute('data-theme-type', theme.type); diff --git a/BitFun-Installer/src/theme/installerThemesData.ts b/BitFun-Installer/src/theme/installerThemesData.ts index aae24b61db..d693ecc6f0 100644 --- a/BitFun-Installer/src/theme/installerThemesData.ts +++ b/BitFun-Installer/src/theme/installerThemesData.ts @@ -1,5 +1,11 @@ import type { ThemeId } from '../types/installer'; +type AccentStop = '50' | '100' | '200' | '300' | '400' | '500' | '600'; +type SecondaryAccentStop = Exclude | '800'; +type AccentRamp = Record; +type SecondaryAccentRamp = Record; +type RampAlphas = readonly [string, string, string, string, string]; + export type InstallerTheme = { id: ThemeId; name: string; @@ -21,8 +27,8 @@ export type InstallerTheme = { muted: string; disabled: string; }; - accent: Record<'50' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800', string>; - purple: Record<'50' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800', string>; + accent: AccentRamp; + purple: SecondaryAccentRamp; semantic: { success: string; warning: string; @@ -49,19 +55,68 @@ export type InstallerTheme = { }; }; +const DEFAULT_RAMP_ALPHAS: RampAlphas = ['0.04', '0.08', '0.15', '0.25', '0.4']; +const LIGHT_RAMP_ALPHAS: RampAlphas = ['0.04', '0.08', '0.14', '0.22', '0.36']; +const CYBER_RAMP_ALPHAS: RampAlphas = ['0.05', '0.1', '0.18', '0.3', '0.45']; +const PURPLE_800_ALPHA = { + default: '0.9', + cyber: '0.95', +} as const; + +function alpha(rgb: string, opacity: string): string { + return `rgba(${rgb}, ${opacity})`; +} + +function createAccentRamp( + rgb: string, + solid500: string, + solid600: string, + alphas: RampAlphas = DEFAULT_RAMP_ALPHAS, +): AccentRamp { + return { + '50': alpha(rgb, alphas[0]), + '100': alpha(rgb, alphas[1]), + '200': alpha(rgb, alphas[2]), + '300': alpha(rgb, alphas[3]), + '400': alpha(rgb, alphas[4]), + '500': solid500, + '600': solid600, + }; +} + +function createSecondaryRamp( + rgb: string, + solid500: string, + solid600: string, + strongRgb = rgb, + alphas: RampAlphas = DEFAULT_RAMP_ALPHAS, + highAlpha: string = PURPLE_800_ALPHA.default, +): SecondaryAccentRamp { + const ramp = createAccentRamp(rgb, solid500, solid600, alphas); + return { + '50': ramp['50'], + '100': ramp['100'], + '200': ramp['200'], + '400': ramp['400'], + '500': ramp['500'], + '600': ramp['600'], + '800': alpha(strongRgb, highAlpha), + }; +} + export const THEMES: InstallerTheme[] = [ { id: 'bitfun-dark', name: 'Dark', type: 'dark', colors: { - background: { primary: '#121214', secondary: '#18181a', tertiary: '#121214', quaternary: '#202024', elevated: '#18181a', workbench: '#121214', flowchat: '#121214', tooltip: 'rgba(30, 30, 32, 0.92)' }, + background: { primary: '#121214', secondary: '#18181a', tertiary: '#121214', quaternary: '#202024', elevated: '#18181a', workbench: '#121214', flowchat: '#121214', tooltip: alpha('30, 30, 32', '0.92') }, text: { primary: '#e8e8e8', 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)' }, - 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)' }, - semantic: { success: '#34d399', warning: '#f59e0b', error: '#ef4444', info: '#E1AB80', highlight: '#d4a574', highlightBg: 'rgba(212, 165, 116, 0.15)' }, - border: { subtle: 'rgba(255, 255, 255, 0.12)', base: 'rgba(255, 255, 255, 0.18)', medium: 'rgba(255, 255, 255, 0.24)', strong: 'rgba(255, 255, 255, 0.32)', prominent: 'rgba(225, 171, 128, 0.50)' }, - element: { subtle: 'rgba(255, 255, 255, 0.06)', soft: 'rgba(255, 255, 255, 0.10)', base: 'rgba(255, 255, 255, 0.13)', medium: 'rgba(255, 255, 255, 0.17)', strong: 'rgba(255, 255, 255, 0.21)', elevated: 'rgba(255, 255, 255, 0.25)' }, + accent: createAccentRamp('96, 165, 250', '#60a5fa', '#3b82f6'), + purple: createSecondaryRamp('139, 92, 246', '#8b5cf6', '#7c3aed', '124, 58, 237'), + semantic: { success: '#34d399', warning: '#f59e0b', error: '#ef4444', info: '#E1AB80', highlight: '#d4a574', highlightBg: alpha('212, 165, 116', '0.15') }, + border: { subtle: alpha('255, 255, 255', '0.12'), base: alpha('255, 255, 255', '0.18'), medium: alpha('255, 255, 255', '0.24'), strong: alpha('255, 255, 255', '0.32'), prominent: alpha('225, 171, 128', '0.50') }, + element: { subtle: alpha('255, 255, 255', '0.06'), soft: alpha('255, 255, 255', '0.10'), base: alpha('255, 255, 255', '0.13'), medium: alpha('255, 255, 255', '0.17'), strong: alpha('255, 255, 255', '0.21'), elevated: alpha('255, 255, 255', '0.25') }, }, }, { @@ -69,13 +124,13 @@ export const THEMES: InstallerTheme[] = [ name: 'Light', type: 'light', colors: { - background: { primary: '#f7f8fa', secondary: '#ffffff', tertiary: '#f3f5f8', quaternary: '#ebeef3', elevated: '#ffffff', workbench: '#f7f8fa', flowchat: '#f7f8fa', tooltip: 'rgba(255, 255, 255, 0.98)' }, + background: { primary: '#f7f8fa', secondary: '#ffffff', tertiary: '#f3f5f8', quaternary: '#ebeef3', elevated: '#ffffff', workbench: '#f7f8fa', flowchat: '#f7f8fa', tooltip: alpha('255, 255, 255', '0.98') }, text: { primary: '#1e293b', secondary: '#3d4f66', muted: '#64748b', disabled: '#94a3b8' }, - accent: { '50': 'rgba(71, 102, 143, 0.04)', '100': 'rgba(71, 102, 143, 0.08)', '200': 'rgba(71, 102, 143, 0.14)', '300': 'rgba(71, 102, 143, 0.22)', '400': 'rgba(71, 102, 143, 0.36)', '500': '#5a7bb2', '600': '#4a6694', '700': 'rgba(74, 102, 148, 0.8)', '800': 'rgba(74, 102, 148, 0.9)' }, - 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)' }, - semantic: { success: '#5b9a6f', warning: '#c08c42', error: '#c26565', info: '#5a7bb2', highlight: '#b8863a', highlightBg: 'rgba(184, 134, 58, 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)' }, - element: { subtle: 'rgba(71, 102, 143, 0.05)', soft: 'rgba(71, 102, 143, 0.08)', base: 'rgba(71, 102, 143, 0.11)', medium: 'rgba(71, 102, 143, 0.15)', strong: 'rgba(71, 102, 143, 0.20)', elevated: 'rgba(255, 255, 255, 0.92)' }, + accent: createAccentRamp('71, 102, 143', '#5a7bb2', '#4a6694', LIGHT_RAMP_ALPHAS), + purple: createSecondaryRamp('107, 90, 137', '#7c6b99', '#655680', '101, 86, 128', LIGHT_RAMP_ALPHAS), + semantic: { success: '#5b9a6f', warning: '#c08c42', error: '#c26565', info: '#5a7bb2', highlight: '#b8863a', highlightBg: alpha('184, 134, 58', '0.12') }, + border: { subtle: alpha('100, 116, 139', '0.15'), base: alpha('100, 116, 139', '0.22'), medium: alpha('100, 116, 139', '0.32'), strong: alpha('100, 116, 139', '0.42'), prominent: alpha('100, 116, 139', '0.52') }, + element: { subtle: alpha('71, 102, 143', '0.05'), soft: alpha('71, 102, 143', '0.08'), base: alpha('71, 102, 143', '0.11'), medium: alpha('71, 102, 143', '0.15'), strong: alpha('71, 102, 143', '0.20'), elevated: alpha('255, 255, 255', '0.92') }, }, }, { @@ -83,13 +138,13 @@ export const THEMES: InstallerTheme[] = [ name: 'Midnight', type: 'dark', colors: { - background: { primary: '#2b2d30', secondary: '#1e1f22', tertiary: '#313335', quaternary: '#3c3f41', elevated: '#2b2d30', workbench: '#212121', flowchat: '#2b2d30', tooltip: 'rgba(43, 45, 48, 0.94)' }, + background: { primary: '#2b2d30', secondary: '#1e1f22', tertiary: '#313335', quaternary: '#3c3f41', elevated: '#2b2d30', workbench: '#212121', flowchat: '#2b2d30', tooltip: alpha('43, 45, 48', '0.94') }, text: { primary: '#bcbec4', 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)' }, - 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)' }, - semantic: { success: '#6aab73', warning: '#e0a055', error: '#cc7f7a', info: '#58a6ff', highlight: '#d4a574', highlightBg: 'rgba(212, 165, 116, 0.15)' }, - border: { subtle: 'rgba(255, 255, 255, 0.08)', base: 'rgba(255, 255, 255, 0.14)', medium: 'rgba(255, 255, 255, 0.20)', strong: 'rgba(255, 255, 255, 0.26)', prominent: 'rgba(255, 255, 255, 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)' }, + accent: createAccentRamp('88, 166, 255', '#58a6ff', '#3b82f6'), + purple: createSecondaryRamp('156, 120, 255', '#9c78ff', '#8b5cf6', '139, 92, 246'), + semantic: { success: '#6aab73', warning: '#e0a055', error: '#cc7f7a', info: '#58a6ff', highlight: '#d4a574', highlightBg: alpha('212, 165, 116', '0.15') }, + border: { subtle: alpha('255, 255, 255', '0.08'), base: alpha('255, 255, 255', '0.14'), medium: alpha('255, 255, 255', '0.20'), strong: alpha('255, 255, 255', '0.26'), prominent: alpha('255, 255, 255', '0.35') }, + element: { subtle: alpha('255, 255, 255', '0.04'), soft: alpha('255, 255, 255', '0.06'), base: alpha('255, 255, 255', '0.09'), medium: alpha('255, 255, 255', '0.12'), strong: alpha('255, 255, 255', '0.15'), elevated: alpha('255, 255, 255', '0.18') }, }, }, { @@ -97,13 +152,13 @@ export const THEMES: InstallerTheme[] = [ name: 'Ink Charm', type: 'light', colors: { - background: { primary: '#faf8f0', secondary: '#f5f3e8', tertiary: '#f0ede0', quaternary: '#ebe8d8', elevated: '#ebe9e3', workbench: '#faf8f0', flowchat: '#faf8f0', tooltip: 'rgba(250, 248, 240, 0.96)' }, + background: { primary: '#faf8f0', secondary: '#f5f3e8', tertiary: '#f0ede0', quaternary: '#ebe8d8', elevated: '#ebe9e3', workbench: '#faf8f0', flowchat: '#faf8f0', tooltip: alpha('250, 248, 240', '0.96') }, text: { primary: '#1a1a1a', 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)' }, - 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)' }, - semantic: { success: '#52ad5a', warning: '#f0a020', error: '#c8102e', info: '#2e5e8a', highlight: '#f0a020', highlightBg: 'rgba(240, 160, 32, 0.12)' }, - border: { subtle: 'rgba(106, 92, 70, 0.12)', base: 'rgba(106, 92, 70, 0.20)', medium: 'rgba(106, 92, 70, 0.28)', strong: 'rgba(106, 92, 70, 0.36)', prominent: 'rgba(106, 92, 70, 0.48)' }, - element: { subtle: 'rgba(46, 94, 138, 0.03)', soft: 'rgba(46, 94, 138, 0.06)', base: 'rgba(46, 94, 138, 0.10)', medium: 'rgba(46, 94, 138, 0.14)', strong: 'rgba(46, 94, 138, 0.18)', elevated: 'rgba(255, 255, 255, 0.85)' }, + accent: createAccentRamp('46, 94, 138', '#2e5e8a', '#234a6d'), + purple: createSecondaryRamp('126, 176, 155', '#7eb09b', '#5a9078', '90, 144, 120'), + semantic: { success: '#52ad5a', warning: '#f0a020', error: '#c8102e', info: '#2e5e8a', highlight: '#f0a020', highlightBg: alpha('240, 160, 32', '0.12') }, + border: { subtle: alpha('106, 92, 70', '0.12'), base: alpha('106, 92, 70', '0.20'), medium: alpha('106, 92, 70', '0.28'), strong: alpha('106, 92, 70', '0.36'), prominent: alpha('106, 92, 70', '0.48') }, + element: { subtle: alpha('46, 94, 138', '0.03'), soft: alpha('46, 94, 138', '0.06'), base: alpha('46, 94, 138', '0.10'), medium: alpha('46, 94, 138', '0.14'), strong: alpha('46, 94, 138', '0.18'), elevated: alpha('255, 255, 255', '0.85') }, }, }, { @@ -111,13 +166,13 @@ export const THEMES: InstallerTheme[] = [ name: 'Ink Night', type: 'dark', colors: { - background: { primary: '#1a1814', secondary: '#212019', tertiary: '#262420', quaternary: '#2d2926', elevated: '#2d2926', workbench: '#1a1814', flowchat: '#1a1814', tooltip: 'rgba(26, 24, 20, 0.95)' }, + background: { primary: '#1a1814', secondary: '#212019', tertiary: '#262420', quaternary: '#2d2926', elevated: '#2d2926', workbench: '#1a1814', flowchat: '#1a1814', tooltip: alpha('26, 24, 20', '0.95') }, text: { primary: '#e8e6e1', 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)' }, - 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)' }, - semantic: { success: '#6bc072', warning: '#f5b555', error: '#e85555', info: '#73a5cc', highlight: '#e6a84a', highlightBg: 'rgba(230, 168, 74, 0.15)' }, - border: { subtle: 'rgba(232, 230, 225, 0.10)', 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)' }, - 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.20)', elevated: 'rgba(45, 41, 38, 0.95)' }, + accent: createAccentRamp('115, 165, 204', '#73a5cc', '#5a8bb3'), + purple: createSecondaryRamp('150, 198, 180', '#96c6b4', '#7aab98', '122, 171, 152'), + semantic: { success: '#6bc072', warning: '#f5b555', error: '#e85555', info: '#73a5cc', highlight: '#e6a84a', highlightBg: alpha('230, 168, 74', '0.15') }, + border: { subtle: alpha('232, 230, 225', '0.10'), base: alpha('232, 230, 225', '0.16'), medium: alpha('232, 230, 225', '0.22'), strong: alpha('232, 230, 225', '0.28'), prominent: alpha('232, 230, 225', '0.38') }, + element: { subtle: alpha('115, 165, 204', '0.06'), soft: alpha('115, 165, 204', '0.09'), base: alpha('115, 165, 204', '0.12'), medium: alpha('115, 165, 204', '0.16'), strong: alpha('115, 165, 204', '0.20'), elevated: alpha('45, 41, 38', '0.95') }, }, }, { @@ -125,13 +180,13 @@ export const THEMES: InstallerTheme[] = [ name: 'Cyber', type: 'dark', colors: { - background: { primary: '#101010', secondary: '#151515', tertiary: '#1a1a1a', quaternary: '#1f1f1f', elevated: '#0d0d0d', workbench: '#101010', flowchat: '#101010', tooltip: 'rgba(16, 16, 16, 0.95)' }, + background: { primary: '#101010', secondary: '#151515', tertiary: '#1a1a1a', quaternary: '#1f1f1f', elevated: '#0d0d0d', workbench: '#101010', flowchat: '#101010', tooltip: alpha('16, 16, 16', '0.95') }, text: { primary: '#e0f2ff', secondary: '#c7e7ff', muted: '#7fadcc', 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)' }, - 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)' }, - semantic: { success: '#00ff9f', warning: '#ffcc00', error: '#ff0055', info: '#00e6ff', highlight: '#ffdd44', highlightBg: 'rgba(255, 221, 68, 0.15)' }, - border: { subtle: 'rgba(0, 230, 255, 0.14)', base: 'rgba(0, 230, 255, 0.20)', medium: 'rgba(0, 230, 255, 0.28)', strong: 'rgba(0, 230, 255, 0.36)', prominent: 'rgba(0, 230, 255, 0.50)' }, - 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)' }, + accent: createAccentRamp('0, 230, 255', '#00e6ff', '#00ccff', CYBER_RAMP_ALPHAS), + purple: createSecondaryRamp('138, 43, 226', '#8a2be2', '#7928ca', '121, 40, 202', CYBER_RAMP_ALPHAS, PURPLE_800_ALPHA.cyber), + semantic: { success: '#00ff9f', warning: '#ffcc00', error: '#ff0055', info: '#00e6ff', highlight: '#ffdd44', highlightBg: alpha('255, 221, 68', '0.15') }, + border: { subtle: alpha('0, 230, 255', '0.14'), base: alpha('0, 230, 255', '0.20'), medium: alpha('0, 230, 255', '0.28'), strong: alpha('0, 230, 255', '0.36'), prominent: alpha('0, 230, 255', '0.50') }, + element: { subtle: alpha('0, 230, 255', '0.06'), soft: alpha('0, 230, 255', '0.09'), base: alpha('0, 230, 255', '0.13'), medium: alpha('0, 230, 255', '0.17'), strong: alpha('0, 230, 255', '0.22'), elevated: alpha('0, 230, 255', '0.27') }, }, }, { @@ -139,13 +194,13 @@ export const THEMES: InstallerTheme[] = [ name: 'Tokyo Night', type: 'dark', colors: { - background: { primary: '#1a1b26', secondary: '#16161e', tertiary: '#14141b', quaternary: '#1e202e', elevated: '#20222c', workbench: '#16161e', flowchat: '#1a1b26', tooltip: 'rgba(22, 22, 30, 0.94)' }, + background: { primary: '#1a1b26', secondary: '#16161e', tertiary: '#14141b', quaternary: '#1e202e', elevated: '#20222c', workbench: '#16161e', flowchat: '#1a1b26', tooltip: alpha('22, 22, 30', '0.94') }, text: { primary: '#c0caf5', secondary: '#a9b1d6', muted: '#787c99', 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)' }, - 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)' }, - semantic: { success: '#9ece6a', warning: '#e0af68', error: '#f7768e', info: '#7dcfff', highlight: '#e0af68', highlightBg: 'rgba(224, 175, 104, 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)' }, - 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)' }, + accent: createAccentRamp('122, 162, 247', '#7aa2f7', '#6183bb', ['0.05', '0.08', '0.15', '0.25', '0.4']), + purple: createSecondaryRamp('187, 154, 247', '#bb9af7', '#9d7cd8', '157, 124, 216', ['0.05', '0.08', '0.15', '0.25', '0.4'], PURPLE_800_ALPHA.cyber), + semantic: { success: '#9ece6a', warning: '#e0af68', error: '#f7768e', info: '#7dcfff', highlight: '#e0af68', highlightBg: alpha('224, 175, 104', '0.15') }, + border: { subtle: alpha('54, 59, 84', '0.45'), base: alpha('54, 59, 84', '0.6'), medium: alpha('54, 59, 84', '0.72'), strong: alpha('54, 59, 84', '0.85'), prominent: alpha('122, 162, 247', '0.45') }, + element: { subtle: alpha('122, 162, 247', '0.06'), soft: alpha('122, 162, 247', '0.08'), base: alpha('122, 162, 247', '0.11'), medium: alpha('122, 162, 247', '0.14'), strong: alpha('122, 162, 247', '0.18'), elevated: alpha('122, 162, 247', '0.22') }, }, }, { @@ -153,13 +208,13 @@ export const THEMES: InstallerTheme[] = [ name: 'Slate', type: 'dark', colors: { - background: { primary: '#1a1c1e', secondary: '#1a1c1e', tertiary: '#1a1c1e', quaternary: '#32363a', elevated: '#1a1c1e', workbench: '#1a1c1e', flowchat: '#1a1c1e', tooltip: 'rgba(42, 45, 48, 0.96)' }, + background: { primary: '#1a1c1e', secondary: '#1a1c1e', tertiary: '#1a1c1e', quaternary: '#32363a', elevated: '#1a1c1e', workbench: '#1a1c1e', flowchat: '#1a1c1e', tooltip: alpha('42, 45, 48', '0.96') }, text: { primary: '#eef0f3', secondary: '#c8ccd2', muted: '#9ea4ab', disabled: '#65696f' }, - accent: { '50': 'rgba(122, 176, 238, 0.04)', '100': 'rgba(122, 176, 238, 0.08)', '200': 'rgba(122, 176, 238, 0.15)', '300': 'rgba(122, 176, 238, 0.25)', '400': 'rgba(122, 176, 238, 0.4)', '500': '#7ab0ee', '600': '#689ad8', '700': 'rgba(104, 154, 216, 0.8)', '800': 'rgba(104, 154, 216, 0.9)' }, - 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)' }, - semantic: { success: '#7fb899', warning: '#d4a574', error: '#c9878d', info: '#7ab0ee', highlight: '#e2e4e7', highlightBg: 'rgba(212, 214, 216, 0.12)' }, - border: { subtle: 'rgba(255, 255, 255, 0.12)', base: 'rgba(255, 255, 255, 0.18)', medium: 'rgba(255, 255, 255, 0.24)', strong: 'rgba(255, 255, 255, 0.32)', prominent: 'rgba(255, 255, 255, 0.45)' }, - element: { subtle: 'rgba(255, 255, 255, 0.06)', soft: 'rgba(255, 255, 255, 0.10)', base: 'rgba(255, 255, 255, 0.13)', medium: 'rgba(255, 255, 255, 0.17)', strong: 'rgba(255, 255, 255, 0.21)', elevated: 'rgba(255, 255, 255, 0.25)' }, + accent: createAccentRamp('122, 176, 238', '#7ab0ee', '#689ad8'), + purple: createSecondaryRamp('184, 198, 255', '#b8c4ff', '#9dacf5', '157, 172, 245'), + semantic: { success: '#7fb899', warning: '#d4a574', error: '#c9878d', info: '#7ab0ee', highlight: '#e2e4e7', highlightBg: alpha('212, 214, 216', '0.12') }, + border: { subtle: alpha('255, 255, 255', '0.12'), base: alpha('255, 255, 255', '0.18'), medium: alpha('255, 255, 255', '0.24'), strong: alpha('255, 255, 255', '0.32'), prominent: alpha('255, 255, 255', '0.45') }, + element: { subtle: alpha('255, 255, 255', '0.06'), soft: alpha('255, 255, 255', '0.10'), base: alpha('255, 255, 255', '0.13'), medium: alpha('255, 255, 255', '0.17'), strong: alpha('255, 255, 255', '0.21'), elevated: alpha('255, 255, 255', '0.25') }, }, }, ]; @@ -176,7 +231,7 @@ export const THEME_DISPLAY_ORDER: ThemeId[] = [ ]; export function findInstallerThemeById(id: ThemeId): InstallerTheme { - return THEMES.find((t) => t.id === id) - ?? THEMES.find((t) => t.id === 'bitfun-light') + return THEMES.find((theme) => theme.id === id) + ?? THEMES.find((theme) => theme.id === 'bitfun-light') ?? THEMES[0]; } diff --git a/BitFun-Installer/src/types/installer.ts b/BitFun-Installer/src/types/installer.ts index 07343176c1..646890aa7f 100644 --- a/BitFun-Installer/src/types/installer.ts +++ b/BitFun-Installer/src/types/installer.ts @@ -56,7 +56,12 @@ export interface ModelConfig { } /** Matches backend `ConnectionTestMessageCode` (camelCase JSON). */ -export type ConnectionTestMessageCode = 'toolCallsNotDetected' | 'imageInputCheckFailed'; +export type ConnectionTestMessageCode = + | 'toolCallsNotDetected' + | 'imageInputCheckFailed' + | 'tlsOrCertificateIssue' + | 'proxyIssue' + | 'networkIssue'; export interface ConnectionTestResult { success: boolean; diff --git a/docs/architecture/theme-token-optimization.md b/docs/architecture/theme-token-optimization.md index 5e176e2486..8e7155a50a 100644 --- a/docs/architecture/theme-token-optimization.md +++ b/docs/architecture/theme-token-optimization.md @@ -1,6 +1,8 @@ # 主题与颜色 Token 优化方案 -> 当前治理基线以 `scripts/theme-color-governance-baseline.json` 和审计脚本输出为准。 +> 当前治理基线以 `scripts/theme-color-governance-baseline.json`、 +> `scripts/theme-color-governance-baseline.mobile-web.json`、 +> `scripts/theme-color-governance-baseline.installer.json` 和审计脚本输出为准。 本文档用于梳理 BitFun 前端主题、硬编码颜色、重复 token、近似色冗余、 命名漂移和后续治理方案。目标不是把所有看起来相近的颜色都合并,而是让 @@ -13,6 +15,8 @@ - `src/web-ui` 中的主题预设、运行时 CSS 变量注入和共享样式 token。 - `src/web-ui/src/component-library/styles` 下的 token 定义。 +- `src/mobile-web` 中的独立 mobile theme preset、运行时 CSS 变量和移动端组件样式。 +- `BitFun-Installer/src` 中的安装器主题数据、运行时变量注入、静态变量和安装流程组件样式。 - 组件 SCSS/CSS/TSX 中的硬编码颜色、fallback 色值和局部 token。 - Rust/desktop 侧由 TS 主题预设生成的最小投影 manifest。 - 旧 token 名称到新规范名称的兼容别名。 @@ -77,6 +81,14 @@ TS 各自增长一套主题定义。参考经验: | CSS 变量和 key 命名 | 只允许新增明确写入启动主题投影 manifest 的首屏 key;不得新增 backend theme service 或平行 alias 表。 | 新 primitive/semantic/component key、兼容 alias、surface rename、dynamic family 和 widget payload key 均在 TS contract/audit 中登记。 | | 专用渲染域 | Core/web host 不维护 Monaco、Mermaid、Prism、language identity、UI exception 等 web-ui 色板;CLI/TUI preset 和 terminal ANSI 是独立终端 surface,不能成为 web-ui 主题源头。 | web-ui 专用域由各自 TS owner 维护,按 `colorDomainScopes.*` 和 `colorDomainNearPairs.*` 预算治理。 | +Mobile Web 和 Installer 是独立产品形态,但主题扩展位置仍在各自 TS 侧维护: +`src/mobile-web/src/theme/presets` 拥有 mobile runtime key,`BitFun-Installer/src/theme` +和 `BitFun-Installer/src/styles/variables.css` 拥有安装器运行时与首屏静态变量。 +Rust 侧不得因为安装器需要编译或首屏显示而复制完整 palette;安装器 Rust 只处理安装流程、 +API/DTO 映射和必要的启动壳逻辑。跨 root 动态 key 必须在 `scripts/theme-css-var-contract.mjs` +登记 owner,审计脚本按 root owner 判断 stale dynamic family,避免 web-ui、mobile-web 和 +installer 互相误报或重复定义。 + 当前 Rust 启动主题投影已改为读取 TS 内置主题预设生成的 `src/apps/desktop/src/generated/startup_theme_bootstrap.json`。Rust 侧只持有 `id`、`bgPrimary`、`bgSecondary`、`bgScene`、`isLight`、`textPrimary`、 @@ -107,14 +119,16 @@ app/component 层的 raw color literal、token-equivalent app literal、普通 和内部旧 alias 读取都已收敛到 0。剩余色值全部落在明确 owner 的专用域: theme preset/runtime、token contract、boundary fallback、 Mermaid、Monaco/editor、Prism syntax、terminal ANSI、language identity 和 UI exception -registry。 +registry。Mobile Web 和 Installer 已纳入同一审计口径,但各自使用独立 baseline, +避免移动端或安装器的独立 token 被误算为 web-ui 游离 key。 -`472` 个唯一颜色是前端生产文件的全域审计数,不是普通 app UI 的色值预算。 +`456` 个 web-ui 唯一颜色是前端生产文件的全域审计数,不是普通 app UI 的色值预算。 其中包含主题 preset、token contract、Mermaid、Monaco/editor、terminal、syntax、 language identity 和 UI exception 等专用 palette。真正需要继续压缩的是这些专用域 内部能被证明等价的近似色,而不是把它们直接并入普通 app semantic token。前后端职责边界 已经收敛为:web-ui/TS 侧维护完整主题源;Rust/desktop 侧只读取 TS 生成的首屏和 -prompt snapshot 投影;CLI/TUI 颜色作为独立终端产品 surface 单独治理。 +prompt snapshot 投影;mobile-web 和 installer 颜色在各自 TS root 中维护;CLI/TUI +颜色作为独立终端产品 surface 单独治理。 `colorScopes.exception`、`colorDomainScopes.uiException` 和 `colorDomainScopes.boundaryFallback` 的数值上升不是新增游离色,而是把原先散在 service/component 文件中的身份色、review team @@ -126,8 +140,8 @@ prompt snapshot 投影;CLI/TUI 颜色作为独立终端产品 surface 单独 | 忽略的测试文件数 | 223 | | 忽略的构建生成文件数 | 1 | | 包含颜色字面量的文件数 | 25 | -| 颜色字面量出现次数 | 685 | -| 唯一颜色字面量数量 | 472 | +| 颜色字面量出现次数 | 661 | +| 唯一颜色字面量数量 | 456 | | 组件或非 token 文件中的颜色出现次数 | 0 | | 组件或非 token 唯一颜色数量 | 0 | | App UI 颜色出现次数 | 0 | @@ -141,6 +155,14 @@ prompt snapshot 投影;CLI/TUI 颜色作为独立终端产品 surface 单独 | 专用域肉眼不可区分 near color pair | 0 | | 专用域需证据复核的 near color pair | 119 | +跨 root 基线必须分别看待;这些数字不是互相累加后的单一上限: + +| root | 颜色出现次数 | 唯一颜色 | App UI raw | fallback var | unresolved / non-contract key | dynamic family | 说明 | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | --- | +| `src/web-ui/src` | 661 | 456 | 0 | 0 | 0 | 13 | 主应用完整主题、专用 palette、widget payload 和 editor/terminal/Mermaid 域。 | +| `src/mobile-web/src` | 37 | 35 | 1 | 0 | 0 | 3 | 仅保留图片缩略图黑色 overlay 上的 `#fff` 关闭按钮前景;现有 token 无法同时保证亮暗主题安全。 | +| `BitFun-Installer/src` | 203 | 147 | 0 | 0 | 0 | 2 | 安装器主题数据和静态变量纳入 contract;runtime 只导出实际消费的 accent/purple 色阶。 | + 当前审计未发现 CSS 变量契约层面的硬错误: | 契约指标 | 当前值 | @@ -184,13 +206,19 @@ prompt snapshot 投影;CLI/TUI 颜色作为独立终端产品 surface 单独 | color domain contracts | 13 | 每个专用域都有 owner、reason 和 merge policy | | active uncontracted color domains | 0 | 防止新增专用域但没有 owner/合并策略 | +跨 root 审计必须使用 `pnpm run theme:color-audit:all`。`theme:color-audit` 只覆盖 +`src/web-ui/src`;mobile-web 和 installer 的 raw color、fallback、dynamic family、 +unresolved key 和专用域 near pair 需要分别由 `theme:color-audit:mobile` 与 +`theme:color-audit:installer` 校验。新增 baseline 只能在债务减少时下调;不得为了让 +PR 通过而放宽 `appUi`、fallback、unresolved、non-contract 或 dynamic family 上限。 + 剩余颜色集中在几个专用域;普通 app UI 不再保留 raw color literal: | 区域 | 当前出现次数 | 当前唯一色数 | 说明 | | --- | ---: | ---: | --- | | Theme presets | 189 | 161 | 主题个性与 palette 映射,不作为普通 app literal 直接合并;仅在同一主题内角色可证明等价时由 preset helper 复用 | | Token contracts | 127 | 113 | `tokens.scss` 等静态契约根 | -| Editor | 55 | 48 | Monaco/editor 专用域,不能直接泛化到 app token;组件装饰色已迁出 raw literal | +| Editor | 52 | 48 | Monaco/editor 专用域,不能直接泛化到 app token;被动 selection/word highlight 的重复弱高亮已收敛 | | Mermaid | 91 | 73 | Mermaid 专用渲染域 | | Theme runtime | 35 | 34 | `ThemeService.ts` 运行时注入;暗色 card alpha fallback 和 scrollbar fallback 已收敛到现有 overlay stop | | Language identity | 52 | 50 | 语言身份色,已集中到 identity registry | @@ -320,6 +348,10 @@ Phase 6 防回退约束: | `generatedWidgetPayload.undefinedUnique` | 0 | 0 | 防止 payload 导出未定义主题 key | | `generatedWidgetPayload.missingCompatibilityCanonicalUnique` | 0 | 0 | 防止 payload 兼容 alias 缺失 canonical 目标 | | `generatedWidgetPayload.unexportedCompatibilityCanonicalUnique` | 0 | 0 | 防止 payload 兼容 alias 有 canonical 定义但未导出到 iframe | +| `mobile-web` app UI raw color | 1 | 1 | 仅允许图片缩略图黑色 overlay 上的白色关闭按钮前景;新增必须先转 token 或补充 owner | +| `mobile-web` dynamic families | 3 | 3 | `--color-accent-*`、`--color-purple-*`、`--color-pink-*` 由 mobile preset 拥有 | +| `installer` app UI raw color | 0 | 0 | 安装器组件不得携带 raw app color | +| `installer` dynamic families | 2 | 2 | 安装器只导出实际消费的 accent/purple family,不保留未使用高阶 key | | `fallbackContracts.uncontractedUnique` | 0 | 0 | 防止新增未说明边界的 `var(--token, fallback)` | | `fallbackContracts.staleRegisteredUnique` | 0 | 0 | 防止已删除 fallback 继续留在 registry 中 | | `colorDomainContracts.activeUncontractedUnique` | 0 | 0 | 防止新增专用颜色域但没有 owner 和 merge policy | @@ -345,6 +377,7 @@ editor、syntax、terminal、language identity、boundary fallback 等专用域 | generated-widget | iframe fallback、host payload、loading/error | payload 只导出 canonical key;旧 alias 兼容集中在 iframe fallback | | theme-settings | theme switcher、system/custom theme preview | custom theme preview 可能比普通组件更早暴露 runtime canonical token 缺失 | | mobile-web-shell | mobile-web、mobile/narrow、loading/error/navigation | mobile web 是独立构建目标,不能只依赖 desktop WebView 验证 | +| installer-shell | installer、theme setup、language/options、loading/error | installer 是独立 React/Tauri surface;主题变量由 installer TS root 维护,Rust 不得复制 palette | ## 现有架构地图 @@ -358,18 +391,23 @@ editor、syntax、terminal、language identity、boundary fallback 等专用域 - `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。 +- `src/mobile-web/src/theme/presets` 定义 mobile-web 的独立运行时 token;移动端组件只读这些 + canonical key,不从 web-ui 或 Rust 复制 fallback palette。 +- `BitFun-Installer/src/theme` 和 `BitFun-Installer/src/styles/variables.css` 定义安装器主题数据、 + 运行时注入色阶和首屏静态变量;安装器 Rust 不拥有 UI palette。 +- 普通组件 SCSS/CSS/TSX 的 raw app color 与局部 fallback 已由 baseline 约束为 0; + 新增必须先进入 token/exception 决策。 主要架构问题: -- 静态 token 和运行时 token 没有共享单一注册表。 -- 有些 token 只由 `ThemeService.ts` 动态注入,但组件 fallback 假设它们 - 在所有渲染边界都存在。 -- Rust 启动投影和 TS 主题预设仍有最小色值重复,后续应改为由 TS 生成 manifest。 -- 同一个语义角色存在多种历史命名方式。 -- 组件 fallback 中的字面量过多,导致 fallback 变成实际上的第二套色板。 -- 当前主题验证链路不能作为充分的可访问性证据,contrast 计算需要真实实现 - 后才能支撑大规模颜色合并判断。 +- 静态 token、运行时 token、payload allowlist、mobile preset 和 installer preset 已由审计 + contract 串联,但还不是单一代码生成源;新增 root 或 surface 时必须同步 contract owner。 +- 有些 token 仍由运行时动态注入,必须通过 dynamic family contract 说明 owner,不能在组件内 + 用 fallback literal 暗中兜底。 +- 同一个语义角色的旧命名已退役为 baseline 约束;后续风险是旧 key 通过新 root、inline style + 或生成式 widget 边界回流。 +- 当前主题验证链路仍不能替代可访问性证据,contrast 计算和 focused visual review 仍是大规模 + 近似色合并前的必要条件。 ## 问题分类 @@ -664,7 +702,7 @@ semantic token 描述产品级语义,应作为共享 UI 的默认使用层。 验收标准: -- 脚本可以在 `src/web-ui` 上无副作用运行。 +- 脚本可以在 `src/web-ui/src`、`src/mobile-web/src` 和 `BitFun-Installer/src` 上无副作用运行。 - 报告可以对比 baseline 与当前分支。 - 报告能区分普通 app color 与已知 exception namespace。 @@ -785,7 +823,7 @@ semantic token 描述产品级语义,应作为共享 UI 的默认使用层。 - 历史迁移可以按目录增量推进。 - exception 可见、可审查。 - 兼容 alias 可见、可审查;组件 fallback 保持 0,且 stale contract 为 0。 -- CI 至少运行 `theme:color-audit:test`、`theme:color-audit` 和 +- CI 至少运行 `theme:color-audit:test`、`theme:color-audit:all` 和 `theme:visual-contract`。 ## 风险清单 @@ -883,7 +921,7 @@ alpha 差异经常承担 elevation 和交互状态,不应全部压成一个值 实现类 PR: - `pnpm run theme:color-audit:test` -- `pnpm run theme:color-audit` +- `pnpm run theme:color-audit:all` - `pnpm run theme:visual-contract` - `pnpm run lint:web` - `pnpm run type-check:web` diff --git a/package.json b/package.json index c62235a9cc..12d816c91f 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,9 @@ "i18n:contract:test:ci": "cross-env BITFUN_I18N_CONTRACT_TEST_PROFILE=ci node --test scripts/i18n-contract.test.mjs", "i18n:audit": "node scripts/i18n-audit.mjs", "theme:color-audit": "node scripts/audit-theme-colors.mjs", + "theme:color-audit:mobile": "node scripts/audit-theme-colors.mjs --root src/mobile-web/src --baseline scripts/theme-color-governance-baseline.mobile-web.json", + "theme:color-audit:installer": "node scripts/audit-theme-colors.mjs --root BitFun-Installer/src --baseline scripts/theme-color-governance-baseline.installer.json", + "theme:color-audit:all": "pnpm run theme:color-audit && pnpm run theme:color-audit:mobile && pnpm run theme:color-audit:installer", "theme:color-audit:test": "node --test scripts/audit-theme-colors.test.mjs", "theme:visual-contract": "node scripts/validate-theme-visual-contract.mjs", "check:repo-hygiene": "node scripts/check-repo-hygiene.mjs", diff --git a/scripts/audit-theme-colors.mjs b/scripts/audit-theme-colors.mjs index bf526e5d8d..a7d1094cf0 100644 --- a/scripts/audit-theme-colors.mjs +++ b/scripts/audit-theme-colors.mjs @@ -13,6 +13,7 @@ import { CONTRACT_VAR_DEFINITION_PATH_PARTS, DEFAULT_BASELINE_PATH, DEFAULT_ROOT, + DYNAMIC_VAR_FAMILY_CONTRACTS, EXCEPTION_PATH_PARTS, FALLBACK_VAR_CONTRACTS, REGISTERED_DYNAMIC_VAR_PREFIXES, @@ -35,6 +36,7 @@ const VAR_FALLBACK_PATTERN = /var\(\s*(--[a-zA-Z0-9_-]+)\s*,/g; const CSS_VAR_SET_PROPERTY_PATTERN = /\.setProperty\(\s*['"`](--[a-zA-Z0-9_-]+)/g; const CSS_VAR_INLINE_STYLE_PATTERN = /['"`](--[a-zA-Z0-9_-]+)['"`]\s*:/g; const CSS_VAR_DYNAMIC_SET_PATTERN = /\.setProperty\(\s*`(--[a-zA-Z0-9_-]*)\$\{/g; +const CSS_VAR_COLOR_RAMP_PATTERN = /colorRamp\(\s*['"`](--[a-zA-Z0-9_-]+)['"`]/g; const CSS_VAR_LITERAL_PATTERN = /['"`](--[a-zA-Z0-9_-]+)['"`]/g; const GENERATED_WIDGET_THEME_PAYLOAD_PATH = 'tools/generative-widget/themePayload.ts'; const REPORT_ROW_LIMIT = 100; @@ -242,6 +244,22 @@ function collectMatches(content, pattern) { return Array.from(content.matchAll(pattern)); } +function contractOwnerMatchesRoot(contract, rootRelativePath) { + return String(contract.owner ?? '') + .split(';') + .map(owner => owner.trim()) + .filter(Boolean) + .some(owner => ( + owner === rootRelativePath + || owner.startsWith(`${rootRelativePath}/`) + || rootRelativePath.startsWith(`${owner}/`) + )); +} + +function colorRampPrefix(name) { + return name.endsWith('-') ? name : `${name}-`; +} + function previousNonWhitespace(chars) { for (let index = chars.length - 1; index >= 0; index -= 1) { const char = chars[index]; @@ -727,6 +745,7 @@ function applyBaseline(report, options) { function audit(options) { const root = path.resolve(options.root); const checksFullThemeSourceRoot = root === path.resolve(DEFAULT_ROOT); + const rootRelativePath = normalizePath(path.relative(process.cwd(), root)); const files = walkFiles(root); const cwd = process.cwd(); const fileEntries = files.map(file => ({ @@ -863,6 +882,12 @@ function audit(options) { addToSetMap(dynamicDefinitionFiles, match[1], relativePath); } + for (const match of collectMatches(content, CSS_VAR_COLOR_RAMP_PATTERN)) { + const prefix = colorRampPrefix(match[1]); + dynamicDefinitionPrefixes.add(prefix); + addToSetMap(dynamicDefinitionFiles, prefix, relativePath); + } + if (isGeneratedWidgetThemePayloadFile(relativePath)) { for (const match of collectMatches(content, CSS_VAR_LITERAL_PATTERN)) { incrementMap(generatedWidgetPayloadVarCounts, match[1]); @@ -922,7 +947,9 @@ function audit(options) { files: Array.from(dynamicDefinitionFiles.get(prefix) ?? []).sort().slice(0, 5), })); const staleRegisteredDynamicFamilyEntries = checksFullThemeSourceRoot - ? Array.from(REGISTERED_DYNAMIC_VAR_PREFIXES) + ? DYNAMIC_VAR_FAMILY_CONTRACTS + .filter(contract => contractOwnerMatchesRoot(contract, rootRelativePath)) + .map(contract => contract.prefix) .filter(prefix => !dynamicDefinitionPrefixes.has(prefix)) .sort((a, b) => a.localeCompare(b)) .map(prefix => ({ key: prefix })) diff --git a/scripts/audit-theme-colors.test.mjs b/scripts/audit-theme-colors.test.mjs index f119db6f8e..b4957a30eb 100644 --- a/scripts/audit-theme-colors.test.mjs +++ b/scripts/audit-theme-colors.test.mjs @@ -17,6 +17,34 @@ import { } from './theme-css-var-contract.mjs'; const root = process.cwd(); +const SOURCE_OWNER_ROOTS = [ + 'BitFun-Installer/src', + 'src/mobile-web/src', + 'src/web-ui/src', +]; + +function contractOwnerHasKnownSource(owner) { + return String(owner ?? '') + .split(';') + .map(entry => entry.trim()) + .filter(Boolean) + .some(entry => SOURCE_OWNER_ROOTS.some(sourceRoot => ( + entry === sourceRoot + || entry.startsWith(`${sourceRoot}/`) + ))); +} + +function contractOwnerMatchesRoot(contract, sourceRoot) { + return String(contract.owner ?? '') + .split(';') + .map(entry => entry.trim()) + .filter(Boolean) + .some(entry => ( + entry === sourceRoot + || entry.startsWith(`${sourceRoot}/`) + || sourceRoot.startsWith(`${entry}/`) + )); +} function writeText(filePath, content) { fs.mkdirSync(path.dirname(filePath), { recursive: true }); @@ -65,7 +93,7 @@ test('theme CSS var contract registry is explicit and non-overlapping', () => { ); for (const contract of DYNAMIC_VAR_FAMILY_CONTRACTS) { assert.match(contract.prefix, /^--[a-z0-9-]+-$/); - assert.ok(contract.owner.includes('src/web-ui/src/'), `${contract.prefix} must name a source owner`); + assert.ok(contractOwnerHasKnownSource(contract.owner), `${contract.prefix} must name a source owner`); assert.ok(contract.reason.trim().length >= 20, `${contract.prefix} must explain why it is dynamic`); if (contract.canonicalPrefix !== undefined) { assert.match(contract.canonicalPrefix, /^--[a-z0-9-]+-$/); @@ -144,30 +172,33 @@ test('theme CSS var contract registry is explicit and non-overlapping', () => { }); test('repository dynamic CSS var families match the registered contract', () => { - const result = runAudit(['--json', '--no-baseline']); - assert.equal(result.status, 0, result.stderr || result.stdout); - - const report = JSON.parse(result.stdout); - const registeredPrefixes = DYNAMIC_VAR_FAMILY_CONTRACTS - .map(contract => contract.prefix) - .sort(); - assert.deepEqual(report.cssVarDefinitions.dynamicFamilyPrefixes, registeredPrefixes); - assert.equal(report.cssVarDefinitions.unregisteredDynamicFamilyUnique, 0); - assert.equal(report.cssVarDefinitions.staleRegisteredDynamicFamilyUnique, 0); - assert.equal(report.compatibilityAliases.staleRegisteredUnique, 0); - assert.equal(report.compatibilityAliases.staleRegisteredFamilyUnique, 0); - assert.equal(report.compatibilityAliases.missingCanonicalUnique, 0); - assert.equal(report.generatedWidgetPayload.undefinedUnique, 0); - assert.equal(report.generatedWidgetPayload.missingCompatibilityCanonicalUnique, 0); - assert.equal(report.generatedWidgetPayload.unexportedCompatibilityCanonicalUnique, 0); - assert.equal(report.fallbackContracts.uncontractedUnique, 0); - assert.equal(report.fallbackContracts.staleRegisteredUnique, 0); - assert.equal(report.colorDomainContracts.missingRegisteredUnique, 0); - assert.equal(report.colorDomainContracts.staleRegisteredUnique, 0); - assert.equal(report.colorDomainContracts.activeUncontractedUnique, 0); - assert.equal(report.surfaceTokenRenames.activeUnique, 0); - assert.equal(report.surfaceTokenRenames.activeOccurrences, 0); - assert.equal(report.surfaceTokenRenames.missingCanonicalUnique, 0); + for (const sourceRoot of SOURCE_OWNER_ROOTS) { + const result = runAudit(['--root', sourceRoot, '--json', '--no-baseline']); + assert.equal(result.status, 0, result.stderr || result.stdout); + + const report = JSON.parse(result.stdout); + const registeredPrefixes = DYNAMIC_VAR_FAMILY_CONTRACTS + .filter(contract => contractOwnerMatchesRoot(contract, sourceRoot)) + .map(contract => contract.prefix) + .sort(); + assert.deepEqual(report.cssVarDefinitions.dynamicFamilyPrefixes, registeredPrefixes); + assert.equal(report.cssVarDefinitions.unregisteredDynamicFamilyUnique, 0); + assert.equal(report.cssVarDefinitions.staleRegisteredDynamicFamilyUnique, 0); + assert.equal(report.compatibilityAliases.staleRegisteredUnique, 0); + assert.equal(report.compatibilityAliases.staleRegisteredFamilyUnique, 0); + assert.equal(report.compatibilityAliases.missingCanonicalUnique, 0); + assert.equal(report.generatedWidgetPayload.undefinedUnique, 0); + assert.equal(report.generatedWidgetPayload.missingCompatibilityCanonicalUnique, 0); + assert.equal(report.generatedWidgetPayload.unexportedCompatibilityCanonicalUnique, 0); + assert.equal(report.fallbackContracts.uncontractedUnique, 0); + assert.equal(report.fallbackContracts.staleRegisteredUnique, 0); + assert.equal(report.colorDomainContracts.missingRegisteredUnique, 0); + assert.equal(report.colorDomainContracts.staleRegisteredUnique, 0); + assert.equal(report.colorDomainContracts.activeUncontractedUnique, 0); + assert.equal(report.surfaceTokenRenames.activeUnique, 0); + assert.equal(report.surfaceTokenRenames.activeOccurrences, 0); + assert.equal(report.surfaceTokenRenames.missingCanonicalUnique, 0); + } }); test('generated widget iframe compatibility aliases stay outside root/runtime contracts', () => { diff --git a/scripts/theme-color-governance-baseline.installer.json b/scripts/theme-color-governance-baseline.installer.json new file mode 100644 index 0000000000..6dbd7d0c55 --- /dev/null +++ b/scripts/theme-color-governance-baseline.installer.json @@ -0,0 +1,117 @@ +{ + "version": 1, + "description": "Baseline for installer theme color governance. Lower values when debt is removed; do not raise without a documented review reason.", + "budgets": { + "fallbackOccurrences": { + "max": 0 + }, + "fallbackUniqueTokens": { + "max": 0 + }, + "fallbackContracts.uncontractedUnique": { + "max": 0 + }, + "compatibilityAliases.usedUnique": { + "max": 0 + }, + "compatibilityAliases.occurrences": { + "max": 0 + }, + "compatibilityAliases.familyUsedUnique": { + "max": 0 + }, + "compatibilityAliases.familyOccurrences": { + "max": 0 + }, + "compatibilityAliases.missingCanonicalUnique": { + "max": 0 + }, + "surfaceTokenRenames.activeUnique": { + "max": 0 + }, + "surfaceTokenRenames.activeOccurrences": { + "max": 0 + }, + "surfaceTokenRenames.missingCanonicalUnique": { + "max": 0 + }, + "colorDomainContracts.missingRegisteredUnique": { + "max": 0 + }, + "colorDomainContracts.staleRegisteredUnique": { + "max": 0 + }, + "colorDomainContracts.activeUncontractedUnique": { + "max": 0 + }, + "colorScopes.appUi.occurrences": { + "max": 0 + }, + "colorScopes.appUi.uniqueColors": { + "max": 0 + }, + "cssVarDefinitions.unresolvedRequiredUnique": { + "max": 0 + }, + "cssVarDefinitions.fallbackOnlyUnique": { + "max": 0 + }, + "cssVarDefinitions.nonContractCrossFileUnique": { + "max": 0 + }, + "cssVarDefinitions.nonContractDynamicInputUnique": { + "max": 0 + }, + "cssVarDefinitions.nonContractCssPrivateUnique": { + "max": 0 + }, + "cssVarDefinitions.unregisteredDynamicFamilyUnique": { + "max": 0 + }, + "tokenAliasLiterals.occurrences": { + "max": 0 + }, + "nearPairs.indistinguishableTotal": { + "max": 0 + }, + "nearPairs.nearTotal": { + "max": 0 + }, + "colorDomainNearPairs.themePreset.indistinguishableTotal": { + "max": 0 + }, + "colorDomainNearPairs.themePreset.nearTotal": { + "max": 69 + }, + "colorDomainNearPairs.tokenContract.indistinguishableTotal": { + "max": 0 + }, + "colorDomainNearPairs.tokenContract.nearTotal": { + "max": 7 + }, + "colorDomainNearPairs.appUi.indistinguishableTotal": { + "max": 0 + }, + "colorDomainNearPairs.appUi.nearTotal": { + "max": 0 + }, + "colorDomainScopes.themePreset.occurrences": { + "max": 160 + }, + "colorDomainScopes.themePreset.uniqueColors": { + "max": 124 + }, + "colorDomainScopes.tokenContract.occurrences": { + "max": 43 + }, + "colorDomainScopes.tokenContract.uniqueColors": { + "max": 39 + }, + "colorDomainScopes.appUi.occurrences": { + "max": 0 + }, + "colorDomainScopes.appUi.uniqueColors": { + "max": 0 + } + } +} diff --git a/scripts/theme-color-governance-baseline.json b/scripts/theme-color-governance-baseline.json index 8771898b5d..eadd7d771f 100644 --- a/scripts/theme-color-governance-baseline.json +++ b/scripts/theme-color-governance-baseline.json @@ -261,7 +261,7 @@ "max": 17 }, "colorDomainScopes.editor.occurrences": { - "max": 55 + "max": 52 }, "colorDomainScopes.syntax.occurrences": { "max": 18 diff --git a/scripts/theme-color-governance-baseline.mobile-web.json b/scripts/theme-color-governance-baseline.mobile-web.json new file mode 100644 index 0000000000..04556f9e56 --- /dev/null +++ b/scripts/theme-color-governance-baseline.mobile-web.json @@ -0,0 +1,105 @@ +{ + "version": 1, + "description": "Baseline for Mobile Web theme color governance. Lower values when debt is removed; do not raise without a documented review reason.", + "budgets": { + "fallbackOccurrences": { + "max": 0 + }, + "fallbackUniqueTokens": { + "max": 0 + }, + "fallbackContracts.uncontractedUnique": { + "max": 0 + }, + "compatibilityAliases.usedUnique": { + "max": 0 + }, + "compatibilityAliases.occurrences": { + "max": 0 + }, + "compatibilityAliases.familyUsedUnique": { + "max": 0 + }, + "compatibilityAliases.familyOccurrences": { + "max": 0 + }, + "compatibilityAliases.missingCanonicalUnique": { + "max": 0 + }, + "surfaceTokenRenames.activeUnique": { + "max": 0 + }, + "surfaceTokenRenames.activeOccurrences": { + "max": 0 + }, + "surfaceTokenRenames.missingCanonicalUnique": { + "max": 0 + }, + "colorDomainContracts.missingRegisteredUnique": { + "max": 0 + }, + "colorDomainContracts.staleRegisteredUnique": { + "max": 0 + }, + "colorDomainContracts.activeUncontractedUnique": { + "max": 0 + }, + "colorScopes.appUi.occurrences": { + "max": 1 + }, + "colorScopes.appUi.uniqueColors": { + "max": 1 + }, + "cssVarDefinitions.unresolvedRequiredUnique": { + "max": 0 + }, + "cssVarDefinitions.fallbackOnlyUnique": { + "max": 0 + }, + "cssVarDefinitions.nonContractCrossFileUnique": { + "max": 0 + }, + "cssVarDefinitions.nonContractDynamicInputUnique": { + "max": 0 + }, + "cssVarDefinitions.nonContractCssPrivateUnique": { + "max": 0 + }, + "cssVarDefinitions.unregisteredDynamicFamilyUnique": { + "max": 0 + }, + "tokenAliasLiterals.occurrences": { + "max": 0 + }, + "nearPairs.indistinguishableTotal": { + "max": 0 + }, + "nearPairs.nearTotal": { + "max": 0 + }, + "colorDomainNearPairs.themePreset.indistinguishableTotal": { + "max": 0 + }, + "colorDomainNearPairs.themePreset.nearTotal": { + "max": 7 + }, + "colorDomainNearPairs.appUi.indistinguishableTotal": { + "max": 0 + }, + "colorDomainNearPairs.appUi.nearTotal": { + "max": 0 + }, + "colorDomainScopes.themePreset.occurrences": { + "max": 36 + }, + "colorDomainScopes.themePreset.uniqueColors": { + "max": 34 + }, + "colorDomainScopes.appUi.occurrences": { + "max": 1 + }, + "colorDomainScopes.appUi.uniqueColors": { + "max": 1 + } + } +} diff --git a/scripts/theme-css-var-contract.mjs b/scripts/theme-css-var-contract.mjs index c82f384292..dd21c76c73 100644 --- a/scripts/theme-css-var-contract.mjs +++ b/scripts/theme-css-var-contract.mjs @@ -4,6 +4,8 @@ export const DEFAULT_BASELINE_PATH = 'scripts/theme-color-governance-baseline.js export const COLOR_EXTENSIONS = new Set(['.css', '.scss', '.sass', '.ts', '.tsx', '.js', '.jsx']); export const TOKEN_PATH_PARTS = [ + 'BitFun-Installer/src/styles/variables.css', + 'BitFun-Installer/src/theme', 'component-library/styles', 'infrastructure/theme', 'theme/presets', @@ -14,16 +16,21 @@ export const TOKEN_ALIAS_SOURCE_PATH_PARTS = [ ]; export const CONTRACT_VAR_DEFINITION_PATH_PARTS = [ + 'BitFun-Installer/src/styles/variables.css', + 'BitFun-Installer/src/theme/installerThemeRuntime.ts', 'component-library/styles', 'infrastructure/theme', + 'src/mobile-web/src/theme/presets', 'tools/generative-widget/themePayload.ts', ]; export const STATIC_CONTRACT_VAR_DEFINITION_PATH_PARTS = [ + 'BitFun-Installer/src/styles/variables.css', 'component-library/styles', ]; export const RUNTIME_CONTRACT_VAR_DEFINITION_PATH_PARTS = [ + 'BitFun-Installer/src/theme/installerThemeRuntime.ts', 'infrastructure/theme', ]; @@ -44,7 +51,7 @@ export const COLOR_DOMAIN_RULES = [ { key: 'themePreset', label: 'Theme presets', - pathParts: ['infrastructure/theme/presets', 'theme/presets'], + pathParts: ['BitFun-Installer/src/theme', 'infrastructure/theme/presets', 'theme/presets'], }, { key: 'themeRuntime', @@ -54,7 +61,7 @@ export const COLOR_DOMAIN_RULES = [ { key: 'tokenContract', label: 'Token contracts', - pathParts: ['component-library/styles'], + pathParts: ['BitFun-Installer/src/styles/variables.css', 'component-library/styles'], }, { key: 'generatedWidget', @@ -292,13 +299,18 @@ export const DYNAMIC_VAR_FAMILY_CONTRACTS = [ }, { prefix: '--color-accent-', - owner: 'src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Theme runtime exports the active accent palette scale by numeric stop.', + owner: 'src/web-ui/src/infrastructure/theme/core/ThemeService.ts; src/mobile-web/src/theme/presets; BitFun-Installer/src/theme', + reason: 'Theme runtime, mobile presets, and installer theme data export the active accent palette scale by numeric stop.', }, { prefix: '--color-purple-', - owner: 'src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Theme runtime exports the secondary purple palette scale by numeric stop.', + owner: 'src/web-ui/src/infrastructure/theme/core/ThemeService.ts; src/mobile-web/src/theme/presets; BitFun-Installer/src/theme', + reason: 'Theme runtime, mobile presets, and installer theme data export the secondary purple palette scale by numeric stop.', + }, + { + prefix: '--color-pink-', + owner: 'src/mobile-web/src/theme/presets', + reason: 'Mobile presets export assistant-mode identity accents by numeric stop for session and picker states.', }, { prefix: '--easing-', diff --git a/src/mobile-web/src/components/ErrorBoundary.tsx b/src/mobile-web/src/components/ErrorBoundary.tsx index 39422faf96..73bf398175 100644 --- a/src/mobile-web/src/components/ErrorBoundary.tsx +++ b/src/mobile-web/src/components/ErrorBoundary.tsx @@ -58,7 +58,7 @@ export class ErrorBoundary extends React.Component = ({ path, onGetFileInfo, onDownload }) => { - const { isDark } = useTheme(); const { t } = useI18n(); const [state, setState] = useState({ status: 'loading' }); const onGetFileInfoRef = useRef(onGetFileInfo); @@ -291,16 +290,16 @@ const FileCard: React.FC = ({ path, onGetFileInfo, onDownload }) alignItems: 'center', gap: '10px', padding: '10px 14px', - border: `1px solid ${isDark ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.12)'}`, + border: '1px solid var(--border-subtle)', borderRadius: '10px', - background: isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.03)', + background: 'var(--element-bg-subtle)', cursor: state.status === 'ready' || state.status === 'done' ? 'pointer' : 'default', maxWidth: '300px', verticalAlign: 'middle', transition: 'background 0.15s', }; - const iconColor = isDark ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.5)'; + const iconColor = 'var(--color-text-muted)'; if (state.status === 'loading') { return ( @@ -342,14 +341,14 @@ const FileCard: React.FC = ({ path, onGetFileInfo, onDownload }) overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', - color: isDark ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.85)', + color: 'var(--color-text-primary)', }}> {name} {formatFileSize(size)} @@ -358,9 +357,7 @@ const FileCard: React.FC = ({ path, onGetFileInfo, onDownload }) {isDownloading ? `${Math.round((state as any).progress * 100)}%` : isDone ? '✓' : '↓'} @@ -412,7 +409,7 @@ const MarkdownContent: React.FC = ({ content, onFileDownlo }, }} lineNumberStyle={{ - color: isDark ? '#666' : '#999', + color: 'var(--color-text-muted)', paddingRight: '1em', textAlign: 'right' as const, userSelect: 'none' as const, @@ -449,7 +446,7 @@ const MarkdownContent: React.FC = ({ content, onFileDownlo type="button" style={{ cursor: 'pointer', - color: 'var(--color-accent, #3b82f6)', + color: 'var(--color-accent-500)', textDecoration: 'underline', background: 'none', border: 'none', @@ -487,7 +484,7 @@ const MarkdownContent: React.FC = ({ content, onFileDownlo href={href} target="_blank" rel="noopener noreferrer" - style={{ color: 'var(--color-accent, #3b82f6)', textDecoration: 'underline' }} + style={{ color: 'var(--color-accent-500)', textDecoration: 'underline' }} > {children} diff --git a/src/mobile-web/src/styles/components/chat-input.scss b/src/mobile-web/src/styles/components/chat-input.scss index 8267934056..e4cd78c935 100644 --- a/src/mobile-web/src/styles/components/chat-input.scss +++ b/src/mobile-web/src/styles/components/chat-input.scss @@ -184,7 +184,7 @@ &:active:not(:disabled), &--open { - background: rgba(100, 140, 255, 0.15); + background: var(--color-accent-100); color: var(--color-text-primary); } @@ -205,7 +205,7 @@ .chat-model-selector__icon, .chat-model-selector__chevron { - color: var(--color-text-tertiary); + color: var(--color-text-muted); } .chat-model-selector__thinking { @@ -236,8 +236,8 @@ justify-content: center; padding: 1px 4px; border-radius: 4px; - background: rgba(100, 200, 160, 0.15); - color: rgba(100, 200, 160, 0.95); + background: var(--color-success-bg); + color: var(--color-success); font-size: 7px; font-weight: 600; line-height: 1; @@ -392,7 +392,7 @@ flex-shrink: 0; &:active { - background: var(--element-bg-medium, var(--element-bg-soft)); + background: var(--element-bg-medium); color: var(--color-text-primary); } @@ -402,11 +402,11 @@ } &--Plan { - background: rgba(6, 182, 212, 0.15); + background: var(--color-info-bg); } &--debug { - background: rgba(249, 115, 22, 0.15); + background: var(--color-warning-bg); } } @@ -454,7 +454,7 @@ &:not(:disabled) { background: var(--color-accent-500); border: 1px solid var(--color-accent-600); - color: white; + color: var(--btn-primary-color); box-shadow: var(--shadow-sm); } @@ -474,7 +474,7 @@ &.is-stop { background: var(--color-error) !important; border-color: var(--color-error) !important; - color: white !important; + color: var(--btn-primary-color) !important; opacity: 1 !important; box-shadow: var(--shadow-sm); @@ -520,7 +520,7 @@ width: 16px; height: 16px; border-radius: 50%; - background: rgba(0, 0, 0, 0.6); + background: var(--color-overlay); color: #fff; border: none; font-size: 11px; diff --git a/src/mobile-web/src/styles/components/chat.scss b/src/mobile-web/src/styles/components/chat.scss index 5fe6ca112b..d404ccc0fd 100644 --- a/src/mobile-web/src/styles/components/chat.scss +++ b/src/mobile-web/src/styles/components/chat.scss @@ -14,8 +14,8 @@ border-bottom: 1px solid var(--border-subtle); flex-shrink: 0; background: var(--color-bg-elevated); - backdrop-filter: var(--blur-medium); - -webkit-backdrop-filter: var(--blur-medium); + backdrop-filter: var(--blur-base); + -webkit-backdrop-filter: var(--blur-base); position: relative; z-index: 10; } @@ -154,7 +154,7 @@ border: 1px solid var(--border-subtle); background: var(--color-bg-elevated); color: var(--color-text-primary); - box-shadow: var(--shadow-md); + box-shadow: var(--shadow-base); cursor: pointer; display: flex; align-items: center; @@ -288,7 +288,7 @@ padding: 10px 14px; font-size: var(--font-size-sm); color: var(--color-text-secondary); - background: var(--color-bg-subtle); + background: var(--element-bg-subtle); border-radius: 10px; animation: analyzeGlow 2s ease-in-out infinite; @@ -300,7 +300,7 @@ .chat-msg__image-analyzing-icon { display: flex; align-items: center; - color: var(--color-primary); + color: var(--color-accent-500); animation: analyzeSpin 3s linear infinite; } @@ -321,8 +321,8 @@ right: 16px; padding: 12px 16px; border-radius: 10px; - background: rgba(220, 50, 50, 0.92); - color: #fff; + background: var(--color-error); + color: var(--btn-primary-color); font-size: 13px; line-height: 1.4; z-index: 9999; @@ -331,7 +331,7 @@ backdrop-filter: blur(8px); &--info { - background: rgba(22, 110, 75, 0.94); + background: var(--color-success); } } @@ -351,7 +351,7 @@ left: 0; right: 0; bottom: 0; - background: rgba(0, 0, 0, 0.4); + background: var(--color-overlay); display: flex; align-items: flex-end; justify-content: center; diff --git a/src/mobile-web/src/styles/components/markdown.scss b/src/mobile-web/src/styles/components/markdown.scss index 615a524851..e6fc232352 100644 --- a/src/mobile-web/src/styles/components/markdown.scss +++ b/src/mobile-web/src/styles/components/markdown.scss @@ -100,8 +100,8 @@ } } - ul > li::marker { color: #888; font-size: 1.2em; font-weight: bold; } - ol > li::marker { color: #888; font-weight: bold; } + ul > li::marker { color: var(--color-text-muted); font-size: 1.2em; font-weight: bold; } + ol > li::marker { color: var(--color-text-muted); font-weight: bold; } ul ul, ol ol, ul ol, ol ul { margin-top: 0.1rem; diff --git a/src/mobile-web/src/styles/components/sessions.scss b/src/mobile-web/src/styles/components/sessions.scss index b3c1a09ba3..976d52ec5d 100644 --- a/src/mobile-web/src/styles/components/sessions.scss +++ b/src/mobile-web/src/styles/components/sessions.scss @@ -22,8 +22,8 @@ min-height: 56px; border-bottom: 1px solid var(--border-subtle); background: var(--color-bg-elevated); - backdrop-filter: var(--blur-medium); - -webkit-backdrop-filter: var(--blur-medium); + backdrop-filter: var(--blur-base); + -webkit-backdrop-filter: var(--blur-base); flex-shrink: 0; } @@ -746,7 +746,7 @@ left: 0; right: 0; bottom: 0; - background: rgba(0, 0, 0, 0.5); + background: var(--color-overlay); display: flex; align-items: flex-end; justify-content: center; @@ -981,7 +981,7 @@ left: 0; right: 0; bottom: 0; - background: rgba(0, 0, 0, 0.4); + background: var(--color-overlay); display: flex; align-items: flex-end; justify-content: center; @@ -1148,7 +1148,7 @@ &--save { background: var(--color-accent-500); - color: #fff; + color: var(--btn-primary-color); &:active:not(:disabled) { opacity: 0.85; @@ -1223,7 +1223,7 @@ &--danger { background: var(--color-error); - color: #fff; + color: var(--btn-primary-color); &:active:not(:disabled) { opacity: 0.85; @@ -1244,7 +1244,7 @@ font-size: var(--font-size-sm); font-weight: var(--font-weight-medium); z-index: 9999; - box-shadow: var(--shadow-md); + box-shadow: var(--shadow-base); pointer-events: none; animation: toastIn 0.25s var(--easing-standard); } diff --git a/src/mobile-web/src/styles/components/tool-card.scss b/src/mobile-web/src/styles/components/tool-card.scss index e99110a92e..c437d757de 100644 --- a/src/mobile-web/src/styles/components/tool-card.scss +++ b/src/mobile-web/src/styles/components/tool-card.scss @@ -228,12 +228,12 @@ border-radius: var(--size-radius-sm); border: none; background: var(--color-error); - color: white; + color: var(--btn-primary-color); cursor: pointer; flex-shrink: 0; transition: all var(--motion-fast) var(--easing-standard); margin-left: auto; - box-shadow: 0 1px 4px rgba(239, 68, 68, 0.35); + box-shadow: 0 1px 4px var(--color-error-border); &:active { transform: scale(0.9); @@ -589,7 +589,7 @@ .is-selected & { border-color: var(--color-accent-500); background: var(--color-accent-500); - color: white; + color: var(--btn-primary-color); } &--multi { @@ -773,7 +773,7 @@ .chat-todo-card__item-text { font-size: var(--font-size-xs); color: var(--color-text-primary); - line-height: var(--line-height-normal); + line-height: var(--line-height-base); .chat-todo-card__item--in_progress & { color: var(--color-accent-500); diff --git a/src/mobile-web/src/styles/components/workspace.scss b/src/mobile-web/src/styles/components/workspace.scss index 18837c1967..2ef77500d4 100644 --- a/src/mobile-web/src/styles/components/workspace.scss +++ b/src/mobile-web/src/styles/components/workspace.scss @@ -20,8 +20,8 @@ min-height: 56px; border-bottom: 1px solid var(--border-subtle); background: var(--color-bg-elevated); - backdrop-filter: var(--blur-medium); - -webkit-backdrop-filter: var(--blur-medium); + backdrop-filter: var(--blur-base); + -webkit-backdrop-filter: var(--blur-base); flex-shrink: 0; h1 { @@ -117,7 +117,7 @@ &--primary { background: var(--color-accent-500); - color: #fff; + color: var(--btn-primary-color); &:active { background: var(--color-accent-600); diff --git a/src/mobile-web/src/styles/global.scss b/src/mobile-web/src/styles/global.scss index 7dd48be402..7f5d6f2371 100644 --- a/src/mobile-web/src/styles/global.scss +++ b/src/mobile-web/src/styles/global.scss @@ -188,7 +188,7 @@ html.theme-switching { gap: 8px; padding: 8px 16px; background: var(--color-warning); - color: #fff; + color: var(--btn-primary-color); font-size: var(--font-size-sm); font-weight: var(--font-weight-medium); animation: slideDown var(--motion-fast) var(--easing-standard); @@ -198,8 +198,8 @@ html.theme-switching { display: inline-block; width: 14px; height: 14px; - border: 2px solid rgba(255, 255, 255, 0.3); - border-top-color: #fff; + border: 2px solid color-mix(in srgb, currentColor 30%, transparent); + border-top-color: currentColor; border-radius: 50%; animation: reconnect-spin 0.8s linear infinite; } diff --git a/src/mobile-web/src/theme/presets/dark.ts b/src/mobile-web/src/theme/presets/dark.ts index 72c212df06..64426521cd 100644 --- a/src/mobile-web/src/theme/presets/dark.ts +++ b/src/mobile-web/src/theme/presets/dark.ts @@ -10,13 +10,8 @@ import { const BLACK = '0, 0, 0'; const WHITE = '255, 255, 255'; const ACCENT = '96, 165, 250'; -const ACCENT_STRONG = '59, 130, 246'; const PURPLE = '139, 92, 246'; -const PURPLE_STRONG = '124, 58, 237'; const PINK = '236, 72, 153'; -const PINK_STRONG = '219, 39, 119'; -const ORANGE = '249, 115, 22'; -const ORANGE_STRONG = '234, 88, 12'; const SUCCESS = '52, 211, 153'; const WARNING = '245, 158, 11'; const ERROR = '239, 68, 68'; @@ -36,8 +31,6 @@ const PURPLE_500 = '#8b5cf6'; const PURPLE_600 = '#7c3aed'; const PINK_500 = '#ec4899'; const PINK_600 = '#db2777'; -const ORANGE_500 = '#f97316'; -const ORANGE_600 = '#ea580c'; const SUCCESS_500 = '#34d399'; const WARNING_500 = '#f59e0b'; const ERROR_500 = '#ef4444'; @@ -67,10 +60,9 @@ export const darkTheme: MobileThemeVars = { '--element-bg-strong': alpha(WHITE, '0.19'), '--element-bg-elevated': alpha(WHITE, '0.24'), - ...colorRamp('--color-accent', ACCENT, ACCENT_500, ACCENT_600, ACCENT_STRONG), - ...colorRamp('--color-purple', PURPLE, PURPLE_500, PURPLE_600, PURPLE_STRONG), - ...colorRamp('--color-pink', PINK, PINK_500, PINK_600, PINK_STRONG), - ...colorRamp('--color-orange', ORANGE, ORANGE_500, ORANGE_600, ORANGE_STRONG), + ...colorRamp('--color-accent', ACCENT, ACCENT_500, ACCENT_600), + ...colorRamp('--color-purple', PURPLE, PURPLE_500, PURPLE_600), + ...colorRamp('--color-pink', PINK, PINK_500, PINK_600), '--color-success': SUCCESS_500, '--color-success-bg': alpha(SUCCESS, '0.1'), @@ -95,46 +87,20 @@ export const darkTheme: MobileThemeVars = { '--border-strong': alpha(WHITE, '0.32'), '--border-prominent': alpha(INFO, '0.5'), - '--glow-blue': shadow( - `0 12px 32px ${alpha(INFO, '0.25')}`, - `0 6px 16px ${alpha(INFO, '0.18')}`, - `0 3px 8px ${alpha(BLACK, '0.1')}`, - ), - '--glow-purple': shadow( - `0 12px 32px ${alpha(PURPLE, '0.25')}`, - `0 6px 16px ${alpha(PURPLE, '0.18')}`, - `0 3px 8px ${alpha(BLACK, '0.1')}`, - ), - '--glow-orange': shadow( - `0 12px 32px ${alpha(ORANGE, '0.25')}`, - `0 6px 16px ${alpha(ORANGE, '0.18')}`, - `0 3px 8px ${alpha(BLACK, '0.1')}`, - ), - '--glow-mixed': shadow( - `0 12px 32px ${alpha(INFO, '0.2')}`, - `0 6px 16px ${alpha(PURPLE, '0.15')}`, - `0 3px 8px ${alpha(BLACK, '0.1')}`, - ), - '--shadow-xs': `0 1px 2px ${alpha(BLACK, '0.9')}`, '--shadow-sm': `0 2px 4px ${alpha(BLACK, '0.8')}`, '--shadow-base': `0 4px 8px ${alpha(BLACK, '0.7')}`, '--shadow-lg': `0 8px 16px ${alpha(BLACK, '0.6')}`, '--shadow-xl': `0 12px 24px ${alpha(BLACK, '0.5')}`, - '--shadow-2xl': `0 16px 32px ${alpha(BLACK, '0.4')}`, '--blur-subtle': 'blur(4px) saturate(1.05)', '--blur-base': 'blur(8px) saturate(1.1)', - '--blur-medium': 'blur(12px) saturate(1.2)', - '--blur-strong': 'blur(16px) saturate(1.3) brightness(1.1)', - '--blur-intense': 'blur(20px) saturate(1.4) brightness(1.15)', ...commonMobileThemeVars, '--opacity-disabled': '0.6', '--opacity-hover': '0.8', '--opacity-focus': '0.9', - '--opacity-overlay': '0.4', '--scrollbar-thumb': alpha(WHITE, '0.15'), '--scrollbar-thumb-hover': alpha(WHITE, '0.24'), diff --git a/src/mobile-web/src/theme/presets/light.ts b/src/mobile-web/src/theme/presets/light.ts index 938eaccc06..cb0b95c96a 100644 --- a/src/mobile-web/src/theme/presets/light.ts +++ b/src/mobile-web/src/theme/presets/light.ts @@ -12,13 +12,9 @@ const WHITE = '255, 255, 255'; const IOS_NEUTRAL = '120, 120, 128'; const BORDER = '60, 60, 67'; const ACCENT = '0, 122, 255'; -const ACCENT_STRONG = '0, 102, 204'; const PURPLE = '175, 82, 222'; -const PURPLE_STRONG = '149, 54, 204'; const PINK = '236, 72, 153'; -const PINK_STRONG = '219, 39, 119'; const ORANGE = '255, 149, 0'; -const ORANGE_STRONG = '204, 122, 0'; const SUCCESS = '52, 199, 89'; const ERROR = '255, 59, 48'; @@ -36,7 +32,6 @@ const PURPLE_600 = '#9536cc'; const PINK_500 = '#ec4899'; const PINK_600 = '#db2777'; const ORANGE_500 = '#ff9500'; -const ORANGE_600 = '#cc7a00'; const SUCCESS_500 = '#34c759'; const ERROR_500 = '#ff3b30'; @@ -63,10 +58,9 @@ export const lightTheme: MobileThemeVars = { '--element-bg-strong': alpha(IOS_NEUTRAL, '0.2'), '--element-bg-elevated': alpha(WHITE, '0.94'), - ...colorRamp('--color-accent', ACCENT, ACCENT_500, ACCENT_600, ACCENT_STRONG, ['0.04', '0.08', '0.14', '0.22', '0.36']), - ...colorRamp('--color-purple', PURPLE, PURPLE_500, PURPLE_600, PURPLE_STRONG, ['0.04', '0.08', '0.14', '0.22', '0.36']), - ...colorRamp('--color-pink', PINK, PINK_500, PINK_600, PINK_STRONG, ['0.04', '0.08', '0.14', '0.22', '0.36']), - ...colorRamp('--color-orange', ORANGE, ORANGE_500, ORANGE_600, ORANGE_STRONG, ['0.04', '0.08', '0.14', '0.22', '0.36']), + ...colorRamp('--color-accent', ACCENT, ACCENT_500, ACCENT_600, ['0.04', '0.08', '0.14', '0.22', '0.36']), + ...colorRamp('--color-purple', PURPLE, PURPLE_500, PURPLE_600, ['0.04', '0.08', '0.14', '0.22', '0.36']), + ...colorRamp('--color-pink', PINK, PINK_500, PINK_600, ['0.04', '0.08', '0.14', '0.22', '0.36']), '--color-success': SUCCESS_500, '--color-success-bg': alpha(SUCCESS, '0.08'), @@ -91,14 +85,6 @@ export const lightTheme: MobileThemeVars = { '--border-strong': alpha(BORDER, '0.3'), '--border-prominent': alpha(BORDER, '0.4'), - '--glow-blue': `0 2px 8px ${alpha(ACCENT, '0.08')}`, - '--glow-purple': `0 2px 8px ${alpha(PURPLE, '0.08')}`, - '--glow-orange': `0 2px 8px ${alpha(ORANGE, '0.08')}`, - '--glow-mixed': shadow( - `0 2px 8px ${alpha(ACCENT, '0.06')}`, - `0 2px 8px ${alpha(PURPLE, '0.04')}`, - ), - '--shadow-xs': `0 1px 2px ${alpha(BLACK, '0.04')}`, '--shadow-sm': shadow( `0 1px 3px ${alpha(BLACK, '0.06')}`, @@ -116,23 +102,15 @@ export const lightTheme: MobileThemeVars = { `0 8px 24px ${alpha(BLACK, '0.1')}`, `0 4px 8px ${alpha(BLACK, '0.04')}`, ), - '--shadow-2xl': shadow( - `0 12px 32px ${alpha(BLACK, '0.12')}`, - `0 4px 12px ${alpha(BLACK, '0.06')}`, - ), '--blur-subtle': 'blur(4px) saturate(1.2)', '--blur-base': 'blur(8px) saturate(1.4)', - '--blur-medium': 'blur(12px) saturate(1.6)', - '--blur-strong': 'blur(16px) saturate(1.8)', - '--blur-intense': 'blur(20px) saturate(2.0)', ...commonMobileThemeVars, '--opacity-disabled': '0.55', '--opacity-hover': '0.75', '--opacity-focus': '0.9', - '--opacity-overlay': '0.35', '--scrollbar-thumb': alpha(BLACK, '0.15'), '--scrollbar-thumb-hover': alpha(BLACK, '0.25'), diff --git a/src/mobile-web/src/theme/presets/shared.ts b/src/mobile-web/src/theme/presets/shared.ts index 491985b89c..1e97014d50 100644 --- a/src/mobile-web/src/theme/presets/shared.ts +++ b/src/mobile-web/src/theme/presets/shared.ts @@ -15,7 +15,6 @@ export function colorRamp( rgb: string, solid500: string, solid600: string, - strongRgb: string = rgb, stops: readonly [string, string, string, string, string] = ['0.04', '0.08', '0.15', '0.25', '0.4'], ): MobileThemeVars { return { @@ -26,8 +25,6 @@ export function colorRamp( [`${prefix}-400`]: alpha(rgb, stops[4]), [`${prefix}-500`]: solid500, [`${prefix}-600`]: solid600, - [`${prefix}-700`]: alpha(strongRgb, '0.8'), - [`${prefix}-800`]: alpha(strongRgb, '0.9'), }; } @@ -54,12 +51,9 @@ export const commonMobileThemeVars: MobileThemeVars = { '--motion-fast': '0.15s', '--motion-base': '0.3s', '--motion-slow': '0.6s', - '--motion-lazy': '1s', '--easing-standard': 'cubic-bezier(0.4, 0, 0.2, 1)', '--easing-decelerate': 'cubic-bezier(0, 0, 0.2, 1)', - '--easing-accelerate': 'cubic-bezier(0.4, 0, 1, 1)', - '--easing-bounce': 'cubic-bezier(0.68, -0.55, 0.265, 1.55)', '--easing-smooth': 'cubic-bezier(0.4, 0, 0.2, 1)', '--font-family-sans': "'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'SF Pro Display', Roboto, sans-serif", diff --git a/src/web-ui/src/infrastructure/theme/integrations/MonacoThemeSync.ts b/src/web-ui/src/infrastructure/theme/integrations/MonacoThemeSync.ts index 5c7125ece9..c324c16740 100644 --- a/src/web-ui/src/infrastructure/theme/integrations/MonacoThemeSync.ts +++ b/src/web-ui/src/infrastructure/theme/integrations/MonacoThemeSync.ts @@ -10,6 +10,7 @@ const log = createLogger('MonacoThemeSync'); const SEMANTIC_HIGHLIGHTING_RULES = BitFunDarkTheme.rules; const TRANSPARENT_MONACO_BORDER = '#00000000'; +const LIGHT_MONACO_PASSIVE_SELECTION = 'rgba(15, 23, 42, 0.1)'; const TRANSPARENT_MONACO_BORDER_COLORS = { 'focusBorder': TRANSPARENT_MONACO_BORDER, 'contrastBorder': TRANSPARENT_MONACO_BORDER, @@ -27,11 +28,11 @@ function getBitfunLightMonacoTheme(): Monaco.editor.IStandaloneThemeData { 'editor.selectionBackground': 'rgba(15, 23, 42, 0.14)', 'editor.selectionForeground': '#1e293b', - 'editor.inactiveSelectionBackground': 'rgba(15, 23, 42, 0.1)', - 'editor.selectionHighlightBackground': 'rgba(15, 23, 42, 0.1)', + 'editor.inactiveSelectionBackground': LIGHT_MONACO_PASSIVE_SELECTION, + 'editor.selectionHighlightBackground': LIGHT_MONACO_PASSIVE_SELECTION, 'editor.selectionHighlightBorder': 'rgba(15, 23, 42, 0.22)', - 'editor.wordHighlightBackground': 'rgba(15, 23, 42, 0.1)', - 'editor.wordHighlightStrongBackground': 'rgba(15, 23, 42, 0.1)', + 'editor.wordHighlightBackground': LIGHT_MONACO_PASSIVE_SELECTION, + 'editor.wordHighlightStrongBackground': LIGHT_MONACO_PASSIVE_SELECTION, }), }; }