diff --git a/openless-all/app/scripts/aura-skin-contract.test.mjs b/openless-all/app/scripts/aura-skin-contract.test.mjs index 015d9c3e..5228a23e 100644 --- a/openless-all/app/scripts/aura-skin-contract.test.mjs +++ b/openless-all/app/scripts/aura-skin-contract.test.mjs @@ -1,4 +1,6 @@ -import { readFile } from 'node:fs/promises'; +import { readFile, readdir } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; import assert from 'node:assert/strict'; const root = new URL('../', import.meta.url); @@ -11,6 +13,132 @@ function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } +function extractCssBlock(css, selector) { + const escapedSelector = escapeRegExp(selector); + const match = css.match(new RegExp(`${escapedSelector}\\s*\\{([\\s\\S]*?)\\n\\}`)); + assert.ok(match, `tokens.css must contain ${selector} block`); + return match[1]; +} + +function parseCustomProperties(block) { + const tokens = new Map(); + const re = /^\s*(--[\w-]+)\s*:\s*([^;]+);/gm; + let match; + while ((match = re.exec(block)) !== null) { + tokens.set(match[1], match[2].trim()); + } + return tokens; +} + +function resolveTokenValue(name, tokens, stack = new Set()) { + const raw = tokens.get(name); + assert.ok(raw, `missing token ${name}`); + if (!raw.startsWith('var(')) { + return raw; + } + const inner = raw.slice(4, -1).trim(); + const refName = inner.split(',')[0].trim(); + assert.ok(refName.startsWith('--'), `unsupported var() reference in ${name}: ${raw}`); + if (stack.has(refName)) { + throw new Error(`circular var() reference: ${[...stack, refName].join(' -> ')}`); + } + stack.add(refName); + return resolveTokenValue(refName, tokens, stack); +} + +function parseCssColor(value) { + const hexMatch = value.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); + if (hexMatch) { + let hex = hexMatch[1]; + if (hex.length === 3) { + hex = hex + .split('') + .map((ch) => ch + ch) + .join(''); + } + return { + r: Number.parseInt(hex.slice(0, 2), 16), + g: Number.parseInt(hex.slice(2, 4), 16), + b: Number.parseInt(hex.slice(4, 6), 16), + a: 1, + }; + } + + const rgbaMatch = value.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)$/i); + if (rgbaMatch) { + return { + r: Number(rgbaMatch[1]), + g: Number(rgbaMatch[2]), + b: Number(rgbaMatch[3]), + a: rgbaMatch[4] === undefined ? 1 : Number(rgbaMatch[4]), + }; + } + + throw new Error(`unsupported color format: ${value}`); +} + +function srgbChannel(value) { + const normalized = value / 255; + return normalized <= 0.03928 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4; +} + +function relativeLuminance({ r, g, b }) { + const R = srgbChannel(r); + const G = srgbChannel(g); + const B = srgbChannel(b); + return 0.2126 * R + 0.7152 * G + 0.0722 * B; +} + +function contrastRatio(foreground, background) { + const fg = parseCssColor(foreground); + const bg = parseCssColor(background); + assert.equal(fg.a, 1, `foreground must be opaque for contrast checks: ${foreground}`); + assert.equal(bg.a, 1, `background must be opaque for contrast checks: ${background}`); + const lighter = Math.max(relativeLuminance(fg), relativeLuminance(bg)); + const darker = Math.min(relativeLuminance(fg), relativeLuminance(bg)); + return (lighter + 0.05) / (darker + 0.05); +} + +function compositeOverBackground(fgValue, bgValue) { + const fg = parseCssColor(fgValue); + const bg = parseCssColor(bgValue); + if (fg.a === 1) { + return fgValue; + } + const alpha = fg.a; + const r = Math.round(fg.r * alpha + bg.r * (1 - alpha)); + const g = Math.round(fg.g * alpha + bg.g * (1 - alpha)); + const b = Math.round(fg.b * alpha + bg.b * (1 - alpha)); + return `rgb(${r}, ${g}, ${b})`; +} + +function contrastRatioOverBackground(foreground, background) { + const effectiveFg = compositeOverBackground(foreground, background); + return contrastRatio(effectiveFg, background); +} + +function assertSolidContrast(tokens, label, bgToken, inkToken, minRatio = 4.5) { + const bg = resolveTokenValue(bgToken, tokens); + const ink = resolveTokenValue(inkToken, tokens); + const ratio = contrastRatio(ink, bg); + assert.ok( + ratio >= minRatio, + `${label}: ${inkToken} on ${bgToken} must meet WCAG AA (${ratio.toFixed(2)}:1 < ${minRatio}:1)`, + ); + return ratio; +} + +function assertMutedContrast(tokens, label, bgToken, inkToken, minRatio = 4.5) { + const bg = resolveTokenValue(bgToken, tokens); + const ink = resolveTokenValue(inkToken, tokens); + const ratio = contrastRatioOverBackground(ink, bg); + assert.ok( + ratio >= minRatio, + `${label}: ${inkToken} on ${bgToken} must meet WCAG AA (${ratio.toFixed(2)}:1 < ${minRatio}:1)`, + ); + return ratio; +} + function assertUsesClassName(source, className, message) { const escapedClassName = escapeRegExp(className); const patterns = [ @@ -22,6 +150,23 @@ function assertUsesClassName(source, className, message) { assert.ok(patterns.some((pattern) => pattern.test(source)), message); } +const srcRoot = fileURLToPath(new URL('src/', root)); + +async function walkSourceFiles(dirPath, files = []) { + const entries = await readdir(dirPath, { withFileTypes: true }); + for (const entry of entries) { + const entryPath = path.join(dirPath, entry.name); + if (entry.isDirectory()) { + await walkSourceFiles(entryPath, files); + continue; + } + if (/\.(tsx?|css)$/.test(entry.name)) { + files.push(path.relative(srcRoot, entryPath).replace(/\\/g, '/')); + } + } + return files; +} + assert.throws( () => assertUsesClassName('
ol-app-shell-bg
', 'ol-app-shell-bg', 'sample must require className usage'), /sample must require className usage/, @@ -41,22 +186,192 @@ assertUsesClassName( 'sample should accept className usage', ); -const [tokens, globalCss, shell, settingsModal, overview] = await Promise.all([ +const [tokens, globalCss, shell, settingsModal, overview, settingsTabs, themeMode, stylePage, sourceFiles, remoteStyle] = await Promise.all([ read('src/styles/tokens.css'), read('src/styles/global.css'), read('src/components/FloatingShell.tsx'), read('src/components/SettingsModal.tsx'), read('src/pages/Overview.tsx'), + read('src/pages/settings/tabs.tsx'), + read('src/lib/themeMode.ts'), + read('src/pages/Style.tsx'), + walkSourceFiles(srcRoot), + read('src-tauri/src/remote_server/assets/style.css'), ]); assert.match(tokens, /--ol-shell-radius:/, 'tokens.css must define --ol-shell-radius'); assert.match(tokens, /--ol-panel-radius:/, 'tokens.css must define --ol-panel-radius'); assert.match(tokens, /--ol-aura-shadow:/, 'tokens.css must define --ol-aura-shadow'); assert.match(tokens, /--ol-font-display:/, 'tokens.css must define --ol-font-display'); +assert.match(tokens, /--ol-on-accent:/, 'tokens.css must define --ol-on-accent'); +assert.match(tokens, /--ol-primary-solid-bg:/, 'tokens.css must define --ol-primary-solid-bg'); +assert.match(tokens, /--ol-primary-solid-ink:/, 'tokens.css must define --ol-primary-solid-ink'); +assert.match(tokens, /--ol-control-radius:/, 'tokens.css must define --ol-control-radius'); +assert.match(tokens, /--ol-accent-solid-bg:/, 'tokens.css must define --ol-accent-solid-bg'); +assert.match(tokens, /--ol-accent-solid-bg-hover:/, 'tokens.css must define --ol-accent-solid-bg-hover'); +assert.match(tokens, /--ol-accent-solid-ink:/, 'tokens.css must define --ol-accent-solid-ink'); +assert.match(tokens, /--ol-danger-solid-bg:/, 'tokens.css must define --ol-danger-solid-bg'); +assert.match(tokens, /--ol-danger-solid-ink:/, 'tokens.css must define --ol-danger-solid-ink'); + +assert.match( + tokens, + /\[data-ol-theme='dark'\][\s\S]*--ol-accent-solid-bg:/, + 'tokens.css must define --ol-accent-solid-bg in dark theme', +); +assert.match( + tokens, + /\[data-ol-theme='dark'\][\s\S]*--ol-accent-solid-bg-hover:/, + 'tokens.css must define --ol-accent-solid-bg-hover in dark theme', +); +assert.match( + tokens, + /\[data-ol-theme='dark'\][\s\S]*--ol-accent-solid-ink:/, + 'tokens.css must define --ol-accent-solid-ink in dark theme', +); +assert.match( + tokens, + /\[data-ol-theme='dark'\][\s\S]*--ol-danger-solid-bg:/, + 'tokens.css must define --ol-danger-solid-bg in dark theme', +); +assert.match( + tokens, + /\[data-ol-theme='dark'\][\s\S]*--ol-danger-solid-ink:/, + 'tokens.css must define --ol-danger-solid-ink in dark theme', +); + +assert.match( + tokens, + /\[data-ol-theme='dark'\][\s\S]*--ol-primary-solid-bg:/, + 'tokens.css must define --ol-primary-solid-bg in dark theme', +); +assert.match( + tokens, + /\[data-ol-theme='dark'\][\s\S]*--ol-primary-solid-ink:/, + 'tokens.css must define --ol-primary-solid-ink in dark theme', +); + +const lightTokens = parseCustomProperties(extractCssBlock(tokens, ':root')); +const darkTokens = parseCustomProperties(extractCssBlock(tokens, "[data-ol-theme='dark']")); + +const contrastPairs = [ + { + label: 'light accent-solid', + tokens: lightTokens, + bg: '--ol-accent-solid-bg', + ink: '--ol-accent-solid-ink', + }, + { + label: 'light primary-solid', + tokens: lightTokens, + bg: '--ol-primary-solid-bg', + ink: '--ol-primary-solid-ink', + }, + { + label: 'dark accent-solid', + tokens: darkTokens, + bg: '--ol-accent-solid-bg', + ink: '--ol-accent-solid-ink', + }, + { + label: 'dark primary-solid', + tokens: darkTokens, + bg: '--ol-primary-solid-bg', + ink: '--ol-primary-solid-ink', + }, + { + label: 'light danger-solid', + tokens: lightTokens, + bg: '--ol-danger-solid-bg', + ink: '--ol-danger-solid-ink', + }, + { + label: 'dark danger-solid', + tokens: darkTokens, + bg: '--ol-danger-solid-bg', + ink: '--ol-danger-solid-ink', + }, +]; + +const contrastRatios = {}; +for (const pair of contrastPairs) { + contrastRatios[pair.label] = assertSolidContrast(pair.tokens, pair.label, pair.bg, pair.ink); +} + +const mutedContrastPairs = [ + { + label: 'light ink-4 on surface', + tokens: lightTokens, + bg: '--ol-surface', + ink: '--ol-ink-4', + }, + { + label: 'light ink-4 on surface-2', + tokens: lightTokens, + bg: '--ol-surface-2', + ink: '--ol-ink-4', + }, +]; + +const mutedContrastRatios = {}; +for (const pair of mutedContrastPairs) { + mutedContrastRatios[pair.label] = assertMutedContrast(pair.tokens, pair.label, pair.bg, pair.ink); +} + +const remoteTokens = parseCustomProperties(extractCssBlock(remoteStyle, ':root')); + +const remoteMutedContrastPairs = [ + { + label: 'remote ink-4 on surface', + tokens: remoteTokens, + bg: '--surface', + ink: '--ink-4', + }, + { + label: 'remote ink-4 on surface-2', + tokens: remoteTokens, + bg: '--surface-2', + ink: '--ink-4', + }, +]; + +const remoteMutedContrastRatios = {}; +for (const pair of remoteMutedContrastPairs) { + remoteMutedContrastRatios[pair.label] = assertMutedContrast(pair.tokens, pair.label, pair.bg, pair.ink); +} + +const olFrostBlock = extractCssBlock(globalCss, '.ol-frost'); +assert.doesNotMatch( + olFrostBlock, + /rgba\(\s*255\s*,\s*255\s*,\s*255/i, + '.ol-frost in global.css must not hardcode white rgba background gradients', +); +assert.match(globalCss, /background:\s*var\(--ol-frost-bg\)/, '.ol-frost must use --ol-frost-bg token'); + +assert.match( + tokens, + /\[data-ol-theme='dark'\][\s\S]*--ol-input-ink:/, + 'tokens.css must define --ol-input-ink in dark theme', +); +assert.match( + tokens, + /\[data-ol-theme='dark'\][\s\S]*--ol-capsule-confirm-bg:/, + 'tokens.css must define --ol-capsule-confirm-bg in dark theme', +); +assert.match( + tokens, + /\[data-ol-theme='dark'\][\s\S]*--ol-capsule-confirm-ink:/, + 'tokens.css must define --ol-capsule-confirm-ink in dark theme', +); assert.match(globalCss, /\.ol-app-shell-bg\b/, 'global.css must expose .ol-app-shell-bg'); assert.match(globalCss, /\.ol-aura-panel\b/, 'global.css must expose .ol-aura-panel'); assert.doesNotMatch(globalCss, /@keyframes ol-aura-halo/, 'global.css must not add an animated halo'); +assert.match(globalCss, /\.ol-aura-card\b/, 'global.css must expose .ol-aura-card'); +assert.match( + globalCss, + /\.ol-aura-settings\[data-ol-mobile="true"\]/, + 'global.css must scope mobile settings radius to the settings element itself', +); assertUsesClassName(shell, 'ol-app-shell-bg', 'FloatingShell must use the app shell background class'); assertUsesClassName(shell, 'ol-aura-sidebar', 'FloatingShell must expose an Aura sidebar hook'); @@ -65,4 +380,108 @@ assertUsesClassName(shell, 'ol-aura-panel', 'FloatingShell must expose an Aura p assertUsesClassName(settingsModal, 'ol-aura-settings', 'SettingsModal must expose an Aura settings wrapper'); assertUsesClassName(overview, 'ol-overview-hero', 'Overview must expose a high-visibility overview surface hook'); +assert.match( + settingsTabs, + /import\s+\{[^}]*ThemeSection[^}]*\}\s+from\s+['"]\.\/ThemeSection['"]/, + 'tabs.tsx GeneralTab must import ThemeSection', +); +assert.match(settingsTabs, //, 'tabs.tsx GeneralTab must render ThemeSection'); + +assert.match( + themeMode, + /prefers-color-scheme:\s*dark/, + 'themeMode.ts must listen for prefers-color-scheme: dark', +); +assert.match( + themeMode, + /data-ol-theme|olTheme|dataset\.olTheme/, + 'themeMode.ts must apply theme via data-ol-theme / dataset.olTheme', +); + +const forbiddenStyleCardLightBackgrounds = [ + /rgba\(\s*255\s*,\s*255\s*,\s*255/i, + /rgba\(\s*248\s*,\s*250\s*,\s*252/i, + /rgba\(\s*239\s*,\s*246\s*,\s*255/i, +]; + +for (const pattern of forbiddenStyleCardLightBackgrounds) { + assert.doesNotMatch( + stylePage, + pattern, + 'Style.tsx must not hardcode light style-card backgrounds (use --ol-style-* tokens)', + ); +} + +assert.match( + stylePage, + /--ol-style-card-bg/, + 'Style.tsx must reference --ol-style-card-bg for style pack surfaces', +); +assert.match( + stylePage, + /--ol-style-card-ink/, + 'Style.tsx must reference --ol-style-card-ink for style pack text', +); +assert.match( + stylePage, + /--ol-style-subtle-bg/, + 'Style.tsx must reference --ol-style-subtle-bg for editor subtle surfaces', +); + +const illegalCssStringPatterns = [ + /color:\s*'var\([^)]+\)';/, + /background:\s*'var\([^)]+\)';/, +]; + +const forbiddenInlineInkBackground = /background:\s*'var\(--ol-ink\)'/; + +const forbiddenBlueOnAccentCombo = + /background:[\s\S]{0,200}var\(--ol-blue\)[\s\S]{0,500}?color:[\s\S]{0,200}var\(--ol-on-accent\)|color:[\s\S]{0,200}var\(--ol-on-accent\)[\s\S]{0,500}?background:[\s\S]{0,200}var\(--ol-blue\)/; + +const forbiddenErrOnAccentCombo = + /background:[\s\S]{0,200}var\(--ol-err[^)]*\)[\s\S]{0,500}?color:[\s\S]{0,200}var\(--ol-on-accent\)|background:[\s\S]{0,200}var\(--ol-err[^)]*\)[\s\S]{0,500}?color:[\s\S]{0,200}var\(--ol-accent-solid-ink\)|color:[\s\S]{0,200}var\(--ol-on-accent\)[\s\S]{0,500}?background:[\s\S]{0,200}var\(--ol-err[^)]*\)|color:[\s\S]{0,200}var\(--ol-accent-solid-ink\)[\s\S]{0,500}?background:[\s\S]{0,200}var\(--ol-err[^)]*\)/; + +for (const relPath of sourceFiles) { + const source = await read(`src/${relPath}`); + for (const pattern of illegalCssStringPatterns) { + assert.doesNotMatch( + source, + pattern, + `src/${relPath} must not use quoted CSS custom properties inside injected CSS strings`, + ); + } + if (relPath.endsWith('.tsx')) { + assert.doesNotMatch( + source, + forbiddenInlineInkBackground, + `src/${relPath} must not use --ol-ink as a button/solid background`, + ); + assert.doesNotMatch( + source, + forbiddenBlueOnAccentCombo, + `src/${relPath} must not pair background var(--ol-blue) with color var(--ol-on-accent) (use --ol-accent-solid-* tokens)`, + ); + assert.doesNotMatch( + source, + forbiddenErrOnAccentCombo, + `src/${relPath} must not pair background var(--ol-err) with color var(--ol-on-accent) or var(--ol-accent-solid-ink) (use --ol-danger-solid-* tokens)`, + ); + } +} + console.log('Aura skin contract OK'); +console.log( + 'Solid contrast ratios:', + Object.fromEntries( + Object.entries(contrastRatios).map(([label, ratio]) => [label, `${ratio.toFixed(2)}:1`]), + ), +); +console.log( + 'Muted contrast ratios:', + Object.fromEntries( + Object.entries({ ...mutedContrastRatios, ...remoteMutedContrastRatios }).map(([label, ratio]) => [ + label, + `${ratio.toFixed(2)}:1`, + ]), + ), +); diff --git a/openless-all/app/scripts/windows-ui-config.test.mjs b/openless-all/app/scripts/windows-ui-config.test.mjs index 4d9ddb61..c83f64ad 100644 --- a/openless-all/app/scripts/windows-ui-config.test.mjs +++ b/openless-all/app/scripts/windows-ui-config.test.mjs @@ -22,7 +22,8 @@ const capsuleTsx = await readFile(new URL('../src/components/Capsule.tsx', impor const capsuleLayoutTs = await readFile(new URL('../src/lib/capsuleLayout.ts', import.meta.url), 'utf-8'); const windowChromeTsx = await readFile(new URL('../src/components/WindowChrome.tsx', import.meta.url), 'utf-8'); const floatingShellTsx = await readFile(new URL('../src/components/FloatingShell.tsx', import.meta.url), 'utf-8'); -const tokensCss = await readFile(new URL('../src/styles/tokens.css', import.meta.url), 'utf-8'); +const themeModeTs = await readFile(new URL('../src/lib/themeMode.ts', import.meta.url), 'utf-8'); +const platformTs = await readFile(new URL('../src/lib/platform.ts', import.meta.url), 'utf-8'); if (!capsuleWindow) { throw new Error('capsule window config missing'); @@ -34,13 +35,31 @@ assertEqual(capsuleWindow.width, 220, 'windows capsule config keeps translation- assertEqual(capsuleWindow.height, 110, 'windows capsule config keeps translation-capable height baseline'); assertEqual(capsuleWindow.transparent, true, 'capsule window should keep transparent visuals'); assertEqual(capsuleWindow.alwaysOnTop, true, 'capsule window should stay above the focused app while recording'); -assertEqual(mainWindow.decorations, true, 'shared main window config should keep native macOS traffic lights'); +assertEqual(mainWindow.decorations, true, 'windows main window should keep native decorations'); assertEqual(mainWindow.visible, false, 'windows main window should stay hidden until the intended first show point'); assertMatch( libRs, - /#\[cfg\(target_os = "windows"\)\][\s\S]*?main\.set_decorations\(false\)/, - 'windows runtime should disable native chrome before the first show', + /fn apply_windows_caption_theme[\s\S]*?DWMWA_USE_IMMERSIVE_DARK_MODE[\s\S]*?DWMWA_CAPTION_COLOR[\s\S]*?DWMWA_TEXT_COLOR[\s\S]*?DWMWA_BORDER_COLOR/, + 'windows runtime should sync immersive dark mode and caption/text/border colors', +); + +assertMatch( + libRs, + /#\[tauri::command\][\s\S]*?fn set_windows_caption_theme/, + 'windows caption theme should be exposed as a Tauri command', +); + +assertMatch( + themeModeTs, + /export function applyThemeMode[\s\S]*?syncWindowsCaptionTheme/, + 'applyThemeMode should sync Windows native caption theme', +); + +assertMatch( + platformTs, + /export async function syncWindowsCaptionTheme[\s\S]*?set_windows_caption_theme/, + 'platform IPC wrapper should invoke set_windows_caption_theme', ); assertMatch( @@ -49,10 +68,18 @@ assertMatch( 'macOS capsule should show without taking the key window', ); -if (!/function WindowsResizeHandles\(\)/.test(windowChromeTsx)) { - throw new Error('windows frameless shell should expose explicit resize handles'); +const tokensCss = await readFile(new URL('../src/styles/tokens.css', import.meta.url), 'utf-8'); + +if (!/os === 'win' \|\| os === 'android' \? 0 : 14/.test(windowChromeTsx)) { + throw new Error('windows main shell should rely on native decorations instead of a frameless chrome shell'); } +assertMatch( + windowChromeTsx, + /\/\/ Windows: decorations:true 时外层不画圆角/, + 'windows WindowChrome should defer chrome to native decorations', +); + assertMatch( windowChromeTsx, /const MAC_TITLEBAR_HEIGHT = 28;/, @@ -66,8 +93,8 @@ assertMatch( if (/standardWindowButton|setFrameOrigin: origin|tune_macos_main_window_controls/.test(libRs)) { throw new Error('macOS traffic lights should not be manually repositioned; keep native AppKit button frames visible'); } -if (!/action=\"close\"/.test(windowChromeTsx) || !/tone=\"danger\"/.test(windowChromeTsx)) { - throw new Error('windows titlebar should keep the close button and danger hover treatment'); +if (!/className=\"ol-linux-close-btn\"/.test(windowChromeTsx)) { + throw new Error('linux titlebar should keep the close button treatment'); } assertMatch( tokensCss, @@ -75,12 +102,14 @@ assertMatch( 'shared motion tokens should drive shell animations and transitions', ); -if (!/startResizeDragging\(direction\)/.test(windowChromeTsx)) { - throw new Error('windows resize handles should delegate edge dragging to Tauri'); -} +assertMatch( + windowChromeTsx, + /function LinuxTitlebar\(\)/, + 'linux should keep the custom ol-linux-titlebar shell', +); -if (!/borderRadius:\s*'var\(--ol-window-console-radius\)'/.test(floatingShellTsx)) { - throw new Error('floating shell should consume the shared window-console radius'); +if (!/borderRadius:\s*'var\(--ol-r-lg\)'/.test(floatingShellTsx)) { + throw new Error('floating shell should consume the shared radius token'); } assertMatch( diff --git a/openless-all/app/src-tauri/src/lib.rs b/openless-all/app/src-tauri/src/lib.rs index 021ee9fa..b08e0f2e 100644 --- a/openless-all/app/src-tauri/src/lib.rs +++ b/openless-all/app/src-tauri/src/lib.rs @@ -281,6 +281,7 @@ macro_rules! app_invoke_handler_desktop { commands::sherpa_onnx_asr_reveal_model_dir, commands::export_error_log, restart_app, + set_windows_caption_theme, ] }; } @@ -507,9 +508,9 @@ fn run_desktop() { if let Err(e) = apply_mica(&main, None) { log::warn!("[main] mica failed: {e}"); } - // Win11 22H2+: 把原生标题栏底色调成白色,与应用 sidebar 视觉统一。 + // Win11 22H2+: 同步原生标题栏主题;前端就绪后会再调 set_windows_caption_theme。 // 老版 Windows 静默失败,不阻塞。 - apply_windows_caption_color(&main); + apply_windows_caption_theme(&main, false); } // 静默启动开关:prefs.start_minimized = true → 不弹主窗口, // 用户从菜单栏 / 托盘点击访问。开机自启时尤其有用,避免每次 @@ -957,41 +958,84 @@ pub(crate) fn refresh_tray_microphone_menu(_app: &AppHandle) -> tauri::Result<() Ok(()) } -/// 把 Win11 原生标题栏底色刷成白色,与应用 sidebar 视觉统一。需要 Win11 22H2+ -/// (Build 22621+) 才支持 `DWMWA_CAPTION_COLOR`(35);老 Windows 上 DwmSetWindowAttribute -/// 返回错误,仅打 warn 不阻塞启动。 +/// Win11 22H2+ (Build 22621+) 同步原生标题栏沉浸式暗色 / caption / text / border 色。 +/// 老 Windows 上 DwmSetWindowAttribute 返回错误,仅打 warn 不阻塞启动。 #[cfg(target_os = "windows")] -fn apply_windows_caption_color(window: &tauri::WebviewWindow) { +fn apply_windows_caption_theme(window: &tauri::WebviewWindow, dark: bool) { use raw_window_handle::{HasWindowHandle, RawWindowHandle}; use windows::Win32::Foundation::HWND; - use windows::Win32::Graphics::Dwm::{DwmSetWindowAttribute, DWMWA_CAPTION_COLOR}; + use windows::Win32::Graphics::Dwm::{ + DwmSetWindowAttribute, DWMWA_BORDER_COLOR, DWMWA_CAPTION_COLOR, DWMWA_TEXT_COLOR, + DWMWA_USE_IMMERSIVE_DARK_MODE, + }; let handle = match window.window_handle().map(|h| h.as_raw()) { Ok(RawWindowHandle::Win32(handle)) => handle, Ok(other) => { - log::warn!("[main] unexpected raw window handle for caption color: {other:?}"); + log::warn!("[main] unexpected raw window handle for caption theme: {other:?}"); return; } Err(e) => { - log::warn!("[main] read raw window handle for caption color failed: {e}"); + log::warn!("[main] read raw window handle for caption theme failed: {e}"); return; } }; let hwnd = HWND(handle.hwnd.get() as *mut core::ffi::c_void); - // COLORREF 0x00BBGGRR 编码——选用 rgb(245,245,247) 跟 WindowChrome 的 glass linear-gradient - // 起始色一致,减小原生 caption bar 跟应用磨砂玻璃的色差(用户反馈:纯白 caption + 半透灰 glass - // 色差很丑)。R=0xF5 G=0xF5 B=0xF7 → COLORREF = 0x00F7F5F5。 - let glass_match: u32 = 0x00F7F5F5; + // COLORREF 0x00BBGGRR — light 对齐 WindowChrome glass 起始色 rgb(245,245,247); + // dark 对齐 tokens.css --ol-surface (#141922) / --ol-ink (#f4f7fb) / --ol-surface-2 (#1a202b)。 + let immersive_dark: i32 = i32::from(dark); + let caption_color: u32 = if dark { 0x0022_1914 } else { 0x00F7_F5F5 }; + let text_color: u32 = if dark { 0x00FB_F7F4 } else { 0x002A_170F }; + let border_color: u32 = if dark { 0x002B_201A } else { 0x00E8_E8E8 }; + unsafe { - if let Err(e) = DwmSetWindowAttribute( + set_dwm_window_attribute( + hwnd, + DWMWA_USE_IMMERSIVE_DARK_MODE, + &immersive_dark, + "immersive dark mode", + ); + set_dwm_window_attribute( hwnd, DWMWA_CAPTION_COLOR, - &glass_match as *const _ as *const core::ffi::c_void, - std::mem::size_of_val(&glass_match) as u32, - ) { - log::warn!("[main] set caption color failed (likely pre-22H2 Win): {e}"); - } + &caption_color, + "caption color", + ); + set_dwm_window_attribute(hwnd, DWMWA_TEXT_COLOR, &text_color, "text color"); + set_dwm_window_attribute(hwnd, DWMWA_BORDER_COLOR, &border_color, "border color"); + } +} + +#[cfg(target_os = "windows")] +unsafe fn set_dwm_window_attribute( + hwnd: windows::Win32::Foundation::HWND, + attribute: windows::Win32::Graphics::Dwm::DWMWINDOWATTRIBUTE, + value: &T, + label: &str, +) { + use windows::Win32::Graphics::Dwm::DwmSetWindowAttribute; + + if let Err(e) = DwmSetWindowAttribute( + hwnd, + attribute, + value as *const _ as *const core::ffi::c_void, + std::mem::size_of_val(value) as u32, + ) { + log::warn!("[main] set {label} failed (likely pre-22H2 Win): {e}"); + } +} + +/// 前端主题切换时同步主窗口原生标题栏;非 Windows 为 no-op。 +#[tauri::command] +fn set_windows_caption_theme(app: AppHandle, dark: bool) { + #[cfg(target_os = "windows")] + if let Some(main) = app.get_webview_window("main") { + apply_windows_caption_theme(&main, dark); + } + #[cfg(not(target_os = "windows"))] + { + let _ = (app, dark); } } diff --git a/openless-all/app/src-tauri/src/remote_server/assets/style.css b/openless-all/app/src-tauri/src/remote_server/assets/style.css index 026c3625..3c11db75 100644 --- a/openless-all/app/src-tauri/src/remote_server/assets/style.css +++ b/openless-all/app/src-tauri/src/remote_server/assets/style.css @@ -15,13 +15,17 @@ --ink: #0a0a0b; --ink-2: #2a2a2d; --ink-3: rgba(10, 10, 11, 0.62); - --ink-4: rgba(10, 10, 11, 0.42); + --ink-4: rgba(10, 10, 11, 0.58); /* 蓝色强调 */ --blue: #2563eb; --blue-hover: #1d4ed8; --blue-soft: #eff4ff; --blue-ring: rgba(37, 99, 235, 0.22); + --on-accent: #ffffff; + --accent-solid-bg: var(--blue); + --accent-solid-bg-hover: var(--blue-hover); + --accent-solid-ink: var(--on-accent); /* 状态色 */ --ok: #16a34a; @@ -35,8 +39,13 @@ --shadow-lg: 0 20px 60px -20px rgba(15, 17, 22, 0.18), 0 8px 32px -16px rgba(15, 17, 22, 0.10), 0 0 0 0.5px rgba(0, 0, 0, 0.06); /* 圆角 */ + --control-radius: 8px; + --r-sm: 6px; + --r-md: 10px; --r-lg: 14px; --r-xl: 18px; + --bubble-radius: var(--r-lg); + --modal-radius: var(--r-xl); --r-2xl: 22px; --r-pill: 999px; @@ -179,7 +188,7 @@ body { padding: 15px 18px; font-size: 17px; font-weight: 600; - color: #fff; + color: var(--on-accent); border: none; border-radius: var(--r-lg); cursor: pointer; @@ -189,10 +198,10 @@ body { .btn:disabled { opacity: .5; cursor: default; } .btn-primary { - background: var(--blue); + background: var(--accent-solid-bg); box-shadow: 0 6px 18px -6px var(--blue-ring); } -.btn-primary:active { background: var(--blue-hover); } +.btn-primary:active { background: var(--accent-solid-bg-hover); } .hint-error { color: var(--danger); @@ -230,10 +239,10 @@ body { .help-link { display: inline-block; padding: 9px 16px; - border-radius: var(--r-md); + border-radius: var(--control-radius); border: none; - background: var(--blue); - color: #fff; + background: var(--accent-solid-bg); + color: var(--accent-solid-ink); font-size: 13px; font-weight: 600; font-family: inherit; @@ -242,7 +251,7 @@ body { -webkit-appearance: none; appearance: none; } -.help-link:active { background: var(--blue-hover); } +.help-link:active { background: var(--accent-solid-bg-hover); } .help-link-ghost { background: var(--surface); color: var(--blue); @@ -260,7 +269,7 @@ body { .app-icon { width: 26px; height: 26px; - border-radius: 7px; + border-radius: var(--control-radius); flex: none; box-shadow: var(--shadow-sm); } @@ -275,7 +284,7 @@ body { display: inline-flex; background: var(--surface-2); border: 0.5px solid var(--line); - border-radius: 12px; + border-radius: var(--r-lg); padding: 3px; gap: 2px; } @@ -288,13 +297,13 @@ body { font-size: 13px; font-weight: 600; padding: 7px 14px; - border-radius: 9px; + border-radius: var(--control-radius); cursor: pointer; transition: background .15s ease, color .15s ease; } .mode-btn.active { - background: var(--blue); - color: #fff; + background: var(--accent-solid-bg); + color: var(--accent-solid-ink); } /* ===== 录音主区 ===== */ @@ -315,8 +324,8 @@ body { border-radius: 50%; border: none; cursor: pointer; - color: #fff; - background: linear-gradient(180deg, #3b82f6 0%, #2563eb 100%); + color: var(--accent-solid-ink); + background: linear-gradient(180deg, var(--accent-solid-bg-hover) 0%, var(--accent-solid-bg) 100%); box-shadow: 0 16px 36px -10px rgba(37, 99, 235, .5), inset 0 1px 0 rgba(255, 255, 255, .25); @@ -359,7 +368,7 @@ body { box-shadow: 0 16px 36px -10px rgba(220, 38, 38, .5); animation: breathe 1.6s ease-in-out infinite; } -.record-btn.recording .record-btn-label { color: #fff; } +.record-btn.recording .record-btn-label { color: var(--on-accent); } .record-btn.recording .record-btn-ring { opacity: 1; border-color: rgba(220, 38, 38, .4); @@ -498,7 +507,7 @@ body { cursor: pointer; transition: background .15s ease, color .15s ease; } -.result-copy:active { background: var(--blue); color: #fff; } +.result-copy:active { background: var(--accent-solid-bg); color: var(--accent-solid-ink); } .result-copy.copied { background: var(--ok-soft); border-color: var(--ok); color: var(--ok); } /* ===== 提示文字 ===== */ @@ -570,7 +579,7 @@ body { font-size: 12px; line-height: 1.5; color: var(--ink-4); - background: rgba(255, 255, 255, .85); + background: rgba(255, 255, 255, .92); backdrop-filter: blur(12px) saturate(160%); -webkit-backdrop-filter: blur(12px) saturate(160%); border-top: 0.5px solid var(--line); diff --git a/openless-all/app/src/components/AutoUpdate.tsx b/openless-all/app/src/components/AutoUpdate.tsx index 0d0cfa0e..d59b9332 100644 --- a/openless-all/app/src/components/AutoUpdate.tsx +++ b/openless-all/app/src/components/AutoUpdate.tsx @@ -200,14 +200,14 @@ export function UpdateDialog({ const installing = status === 'installing'; return (
-
+
{t(`settings.about.updateDialog.${status}.title`)}
{t(`settings.about.updateDialog.${status}.desc`, { version })}
{(downloading || installing || status === 'downloaded') && (
-
+
diff --git a/openless-all/app/src/components/Capsule.tsx b/openless-all/app/src/components/Capsule.tsx index bb0bd822..091dc104 100644 --- a/openless-all/app/src/components/Capsule.tsx +++ b/openless-all/app/src/components/Capsule.tsx @@ -41,7 +41,7 @@ function AudioBars({ level }: AudioBarsProps) { display: 'inline-block', width: 3, height: base + (max - base) * visualVoice * env, - borderRadius: 999, + borderRadius: 'var(--ol-pill-radius)', background: 'var(--ol-blue)', opacity: 0.82, transformOrigin: 'center', @@ -113,12 +113,12 @@ function CircleButton({ variant, enabled, onClick }: CircleButtonProps) { style={{ width: 28, height: 28, - borderRadius: 999, - background: isCancel ? 'rgba(255, 255, 255, 0.55)' : 'rgba(255, 255, 255, 0.92)', + borderRadius: 'var(--ol-pill-radius)', + background: isCancel ? 'var(--ol-capsule-cancel-bg)' : 'var(--ol-capsule-confirm-bg)', backdropFilter: useBackdrop ? 'blur(12px) saturate(160%)' : 'none', WebkitBackdropFilter: useBackdrop ? 'blur(12px) saturate(160%)' : 'none', - color: 'var(--ol-ink)', - border: '0.8px solid rgba(0, 0, 0, 0.08)', + color: isCancel ? 'var(--ol-capsule-cancel-ink)' : 'var(--ol-capsule-confirm-ink)', + border: '0.8px solid var(--ol-glass-border)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', @@ -265,11 +265,11 @@ function Pill({ os, state, level, insertedChars, message, operating, onCancel, o width: metrics.width, height: metrics.height, boxSizing: metrics.boxSizing, - borderRadius: 999, - border: '1px solid rgba(255, 255, 255, 0.55)', + borderRadius: 'var(--ol-pill-radius)', + border: '1px solid var(--ol-glass-border)', boxShadow: os === 'win' - ? `0 10px 24px -14px rgba(0, 0, 0, ${(0.24 + ambient * 0.06).toFixed(3)}), 0 0 0 0.5px rgba(0, 0, 0, 0.08), inset 0 1px 0 0 rgba(255, 255, 255, 0.8)` - : `0 18px 50px -10px rgba(0, 0, 0, ${shadowAlpha.toFixed(3)}), 0 0 0 0.5px rgba(0, 0, 0, 0.08), inset 0 1px 0 0 rgba(255, 255, 255, 0.8)`, + ? `0 10px 24px -14px rgba(0, 0, 0, ${(0.24 + ambient * 0.06).toFixed(3)}), var(--ol-pill-shadow)` + : `0 18px 50px -10px rgba(0, 0, 0, ${shadowAlpha.toFixed(3)}), var(--ol-pill-shadow)`, color: 'var(--ol-ink)', fontFamily: 'var(--ol-font-sans)', transform: `scale(${scale.toFixed(4)})`, @@ -446,11 +446,11 @@ export function Capsule() { alignItems: 'center', gap: 5, padding: '3px 10px', - borderRadius: 999, + borderRadius: 'var(--ol-pill-radius)', fontSize: 10.5, fontWeight: 600, color: 'var(--ol-blue)', - background: 'rgba(255, 255, 255, 0.78)', + background: 'var(--ol-glass-bg-strong)', backdropFilter: 'blur(20px) saturate(180%)', WebkitBackdropFilter: 'blur(20px) saturate(180%)', border: '0.5px solid rgba(37, 99, 235, 0.25)', @@ -465,7 +465,7 @@ export function Capsule() { willChange: 'opacity, transform', }} > - + {t('capsule.translating')}
diff --git a/openless-all/app/src/components/FloatingShell.tsx b/openless-all/app/src/components/FloatingShell.tsx index ba22c926..266cd3c5 100644 --- a/openless-all/app/src/components/FloatingShell.tsx +++ b/openless-all/app/src/components/FloatingShell.tsx @@ -4,7 +4,7 @@ // // Ported verbatim from design_handoff_openless/variants.jsx::FloatingShell. -import { useEffect, useLayoutEffect, useMemo, useRef, useState, type ComponentType } from 'react'; +import { useEffect, useLayoutEffect, useMemo, useRef, useState, type ComponentType, type CSSProperties } from 'react'; import { useTranslation } from 'react-i18next'; import { Icon } from './Icon'; import { WindowChrome, detectOS, type OS } from './WindowChrome'; @@ -199,6 +199,7 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia return (
@@ -392,7 +393,11 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
{mobileLayout && ( -
); } @@ -689,10 +463,10 @@ function ProviderSetupPrompt({ onLater, onOpenSettings }: { onLater: () => void;
void; style={{ width: 34, height: 34, - borderRadius: 8, - background: 'rgba(37,99,235,0.10)', + borderRadius: 'var(--ol-control-radius)', + background: 'var(--ol-blue-soft)', color: 'var(--ol-blue)', display: 'inline-flex', alignItems: 'center', @@ -724,7 +498,7 @@ function ProviderSetupPrompt({ onLater, onOpenSettings }: { onLater: () => void; style={{ height: 32, padding: '0 13px', - borderRadius: 8, + borderRadius: 'var(--ol-control-radius)', border: '0.5px solid var(--ol-line-strong)', background: 'var(--ol-surface)', color: 'var(--ol-ink-3)', @@ -742,10 +516,10 @@ function ProviderSetupPrompt({ onLater, onOpenSettings }: { onLater: () => void; style={{ height: 32, padding: '0 14px', - borderRadius: 8, + borderRadius: 'var(--ol-control-radius)', border: 0, - background: 'var(--ol-ink)', - color: 'var(--ol-on-accent)', + background: 'var(--ol-primary-solid-bg)', + color: 'var(--ol-primary-solid-ink)', fontFamily: 'inherit', fontSize: 12.5, fontWeight: 500, @@ -782,10 +556,10 @@ function HotkeyModeMigrationPrompt({ onLater, onOpenSettings }: { onLater: () =>
style={{ width: 34, height: 34, - borderRadius: 8, - background: 'rgba(37,99,235,0.10)', + borderRadius: 'var(--ol-control-radius)', + background: 'var(--ol-blue-soft)', color: 'var(--ol-blue)', display: 'inline-flex', alignItems: 'center', @@ -817,7 +591,7 @@ function HotkeyModeMigrationPrompt({ onLater, onOpenSettings }: { onLater: () => style={{ height: 32, padding: '0 13px', - borderRadius: 8, + borderRadius: 'var(--ol-control-radius)', border: '0.5px solid var(--ol-line-strong)', background: 'var(--ol-surface)', color: 'var(--ol-ink-3)', @@ -835,10 +609,10 @@ function HotkeyModeMigrationPrompt({ onLater, onOpenSettings }: { onLater: () => style={{ height: 32, padding: '0 14px', - borderRadius: 8, + borderRadius: 'var(--ol-control-radius)', border: 0, - background: 'var(--ol-ink)', - color: 'var(--ol-on-accent)', + background: 'var(--ol-primary-solid-bg)', + color: 'var(--ol-primary-solid-ink)', fontFamily: 'inherit', fontSize: 12.5, fontWeight: 500, diff --git a/openless-all/app/src/components/GithubLoginModal.tsx b/openless-all/app/src/components/GithubLoginModal.tsx index 8dc4c5c3..e2850c49 100644 --- a/openless-all/app/src/components/GithubLoginModal.tsx +++ b/openless-all/app/src/components/GithubLoginModal.tsx @@ -124,7 +124,7 @@ export function GithubLoginModal({ onClose, onSuccess }: GithubLoginModalProps) title={t('common.close')} onClick={close} style={{ - width: 28, height: 28, borderRadius: 8, + width: 28, height: 28, borderRadius: 'var(--ol-control-radius)', display: 'inline-grid', placeItems: 'center', border: '0.5px solid var(--ol-line-strong)', background: 'var(--ol-surface)', @@ -150,7 +150,7 @@ export function GithubLoginModal({ onClose, onSuccess }: GithubLoginModalProps)
@@ -174,7 +174,7 @@ export function GithubLoginModal({ onClose, onSuccess }: GithubLoginModalProps)
{t('marketplace.oauth.waiting')} @@ -197,7 +197,7 @@ export function GithubLoginModal({ onClose, onSuccess }: GithubLoginModalProps) {phase.kind === 'error' && (
{ - (e.currentTarget as HTMLButtonElement).style.background = 'rgba(255,255,255,0.85)'; + (e.currentTarget as HTMLButtonElement).style.background = 'var(--ol-glass-bg-strong)'; (e.currentTarget as HTMLButtonElement).style.color = 'var(--ol-ink-2)'; }} aria-label={t('common.close')} diff --git a/openless-all/app/src/components/Onboarding.tsx b/openless-all/app/src/components/Onboarding.tsx index bb518005..8fff0b04 100644 --- a/openless-all/app/src/components/Onboarding.tsx +++ b/openless-all/app/src/components/Onboarding.tsx @@ -132,7 +132,7 @@ function AndroidOnboarding({ onComplete }: OnboardingProps) { key={step.id} style={{ height: 4, - borderRadius: 999, + borderRadius: 'var(--ol-pill-radius)', background: index <= stepIndex ? 'var(--ol-blue)' : 'var(--ol-line-soft)', }} /> @@ -143,7 +143,7 @@ function AndroidOnboarding({ onComplete }: OnboardingProps) { style={{ background: 'var(--ol-surface)', border: '0.5px solid var(--ol-line)', - borderRadius: 14, + borderRadius: 'var(--ol-bubble-radius)', boxShadow: 'var(--ol-shadow-lg)', padding: 18, display: 'flex', @@ -347,7 +347,7 @@ function DesktopOnboarding({ padding: 32, boxSizing: 'border-box', background: 'var(--ol-surface)', - borderRadius: 14, + borderRadius: 'var(--ol-bubble-radius)', border: '0.5px solid var(--ol-line)', boxShadow: 'var(--ol-shadow-lg)', }} @@ -470,7 +470,7 @@ function AndroidStepCard({ children }: { children: ReactNode }) { flexDirection: 'column', gap: 12, padding: 14, - borderRadius: 10, + borderRadius: 'var(--ol-r-md)', background: 'var(--ol-surface-2)', border: '0.5px solid var(--ol-line-soft)', minWidth: 0, @@ -488,7 +488,7 @@ function StatusBadge({ granted, label }: { granted: boolean; label: string }) { flexShrink: 0, fontSize: 11, fontWeight: 600, - borderRadius: 999, + borderRadius: 'var(--ol-pill-radius)', padding: '4px 8px', color: granted ? 'var(--ol-ok)' : 'var(--ol-ink-4)', background: granted ? 'rgba(40, 160, 90, 0.12)' : 'rgba(0,0,0,0.06)', @@ -526,9 +526,9 @@ function PermissionStep({ index, title, desc, status, actionLabel, onAction, dis style={{ width: 22, height: 22, - borderRadius: 999, - background: granted ? 'var(--ol-blue)' : 'rgba(0,0,0,0.06)', - color: granted ? '#fff' : 'var(--ol-ink-3)', + borderRadius: 'var(--ol-pill-radius)', + background: granted ? 'var(--ol-accent-solid-bg)' : 'rgba(0,0,0,0.06)', + color: granted ? 'var(--ol-accent-solid-ink)' : 'var(--ol-ink-3)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', @@ -558,9 +558,9 @@ function PermissionStep({ index, title, desc, status, actionLabel, onAction, dis fontWeight: 500, fontFamily: 'inherit', border: 0, - borderRadius: 8, - background: granted ? 'var(--ol-surface-2)' : 'var(--ol-ink)', - color: granted ? 'var(--ol-ink-3)' : '#fff', + borderRadius: 'var(--ol-control-radius)', + background: granted ? 'var(--ol-surface-2)' : 'var(--ol-primary-solid-bg)', + color: granted ? 'var(--ol-ink-3)' : 'var(--ol-primary-solid-ink)', cursor: disabled ? 'not-allowed' : 'default', opacity: disabled && !granted ? 0.6 : 1, transition: 'background 0.16s var(--ol-motion-quick), color 0.16s var(--ol-motion-quick), opacity 0.18s var(--ol-motion-soft), transform 0.12s var(--ol-motion-quick)', @@ -580,9 +580,9 @@ const primaryButtonStyle = { fontWeight: 600, fontFamily: 'inherit', border: 0, - borderRadius: 10, - background: 'var(--ol-ink)', - color: '#fff', + borderRadius: 'var(--ol-r-md)', + background: 'var(--ol-primary-solid-bg)', + color: 'var(--ol-primary-solid-ink)', cursor: 'default', } as const; @@ -594,7 +594,7 @@ const secondaryButtonStyle = { fontWeight: 600, fontFamily: 'inherit', border: '0.5px solid var(--ol-line-strong)', - borderRadius: 10, + borderRadius: 'var(--ol-r-md)', background: 'var(--ol-surface)', color: 'var(--ol-ink-2)', cursor: 'default', @@ -607,7 +607,7 @@ const plainButtonStyle = { fontWeight: 500, fontFamily: 'inherit', border: 0, - borderRadius: 8, + borderRadius: 'var(--ol-control-radius)', background: 'transparent', color: 'var(--ol-ink-4)', cursor: 'default', @@ -616,7 +616,7 @@ const plainButtonStyle = { const footerHintStyle = { marginTop: 18, padding: '12px 14px', - borderRadius: 8, + borderRadius: 'var(--ol-control-radius)', background: 'var(--ol-surface-2)', fontSize: 11.5, color: 'var(--ol-ink-3)', diff --git a/openless-all/app/src/components/SettingsModal.tsx b/openless-all/app/src/components/SettingsModal.tsx index f55a682f..9b202a93 100644 --- a/openless-all/app/src/components/SettingsModal.tsx +++ b/openless-all/app/src/components/SettingsModal.tsx @@ -4,8 +4,7 @@ // 「设置」还要再面对第二个侧栏。现在拍平成单层 —— 通用 / 服务 / 隐私 / 高级 / // 个性化 / 关于 六个 tab。每个 tab 的内容见 pages/settings/。 // -// 设计原则:每个可见控件都必须可用。没有后端支撑的占位(账号 / 主题切换 等) -// 不在此弹窗出现。 +// 设计原则:每个可见控件都必须可用。没有后端支撑的占位(如账号)不在此弹窗出现。 import { useLayoutEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -80,6 +79,7 @@ export function SettingsModal({ os: _os, onClose, initialSettingsSection }: Sett
e.stopPropagation()} style={{ width: '100%', @@ -175,7 +175,7 @@ export function SettingsModal({ os: _os, onClose, initialSettingsSection }: Sett onClick={onClose} style={{ position: 'absolute', top: 14, right: 14, zIndex: 2, - width: 28, height: 28, border: 0, borderRadius: 999, + width: 28, height: 28, border: 0, borderRadius: 'var(--ol-pill-radius)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'default', }} @@ -210,73 +210,6 @@ export function SettingsModal({ os: _os, onClose, initialSettingsSection }: Sett
-
); } diff --git a/openless-all/app/src/components/ShortcutRecorder.tsx b/openless-all/app/src/components/ShortcutRecorder.tsx index fad6e257..5106222d 100644 --- a/openless-all/app/src/components/ShortcutRecorder.tsx +++ b/openless-all/app/src/components/ShortcutRecorder.tsx @@ -128,10 +128,10 @@ export function ShortcutRecorder({ const recordButtonStyle: CSSProperties = { fontSize: 12, padding: '5px 14px', - background: recording ? 'rgba(37,99,235,0.12)' : 'var(--ol-blue)', - color: recording ? 'var(--ol-blue)' : '#fff', + background: recording ? 'rgba(37,99,235,0.12)' : 'var(--ol-accent-solid-bg)', + color: recording ? 'var(--ol-blue)' : 'var(--ol-accent-solid-ink)', border: 0, - borderRadius: 6, + borderRadius: 'var(--ol-r-sm)', fontFamily: 'inherit', fontWeight: 500, cursor: recording || disabled ? 'default' : 'pointer', @@ -144,7 +144,7 @@ export function ShortcutRecorder({ background: 'transparent', color: 'var(--ol-ink-4)', border: '0.5px solid var(--ol-line-strong)', - borderRadius: 6, + borderRadius: 'var(--ol-r-sm)', fontFamily: 'inherit', fontWeight: 500, cursor: recording ? 'default' : 'pointer', @@ -160,7 +160,7 @@ export function ShortcutRecorder({ return (
- + {formatComboLabel(value)}
@@ -195,7 +195,7 @@ export function ShortcutRecorder({ tabIndex={-1} onKeyDown={onKeyDown} onKeyUp={onKeyUp} - style={{ padding: '8px 12px', borderRadius: 8, background: 'rgba(37,99,235,0.06)', border: '1px solid rgba(37,99,235,0.2)', fontSize: 12, color: 'var(--ol-blue)', outline: 'none' }} + style={{ padding: '8px 12px', borderRadius: 'var(--ol-control-radius)', background: 'rgba(37,99,235,0.06)', border: '1px solid rgba(37,99,235,0.2)', fontSize: 12, color: 'var(--ol-blue)', outline: 'none' }} ref={el => el?.focus()} > {t('settings.recording.comboRecordHint')} diff --git a/openless-all/app/src/components/ui/Modal.tsx b/openless-all/app/src/components/ui/Modal.tsx index e84d376a..91594cab 100644 --- a/openless-all/app/src/components/ui/Modal.tsx +++ b/openless-all/app/src/components/ui/Modal.tsx @@ -22,7 +22,7 @@ export function Modal({ children, onClose, zIndex = 50, width = 'min(560px, 100% style={{ position: 'fixed', inset: 0, - background: 'rgba(0,0,0,0.22)', + background: 'var(--ol-overlay-bg)', display: 'grid', placeItems: 'center', zIndex, @@ -36,10 +36,10 @@ export function Modal({ children, onClose, zIndex = 50, width = 'min(560px, 100% width, maxHeight: '85vh', overflow: 'auto', - borderRadius: 16, - background: 'var(--ol-surface)', - border: '0.5px solid var(--ol-line-strong)', - boxShadow: '0 18px 42px rgba(0,0,0,0.18)', + borderRadius: 'var(--ol-modal-radius)', + background: 'var(--ol-card-bg)', + border: '0.5px solid var(--ol-card-border)', + boxShadow: 'var(--ol-shadow-lg)', padding: 22, animation: 'ol-modal-card-in 0.24s var(--ol-motion-spring)', }} diff --git a/openless-all/app/src/components/ui/SegSimple.tsx b/openless-all/app/src/components/ui/SegSimple.tsx index 802df27a..bb97df07 100644 --- a/openless-all/app/src/components/ui/SegSimple.tsx +++ b/openless-all/app/src/components/ui/SegSimple.tsx @@ -1,32 +1,41 @@ // SegSimple — segmented control used in the Settings modal sub-sections. -import { useState } from 'react'; +export type SegOption = { value: string; label: string }; interface SegSimpleProps { - options: string[]; - active: string; + options: SegOption[]; + value: string; + onChange?: (value: string) => void; } -export function SegSimple({ options, active }: SegSimpleProps) { - const [v, setV] = useState(active); +export function SegSimple({ options, value, onChange }: SegSimpleProps) { return ( -
- {options.map((o) => ( +
+ {options.map((o) => { + const selected = value === o.value; + return ( - ))} + ); + })}
); } diff --git a/openless-all/app/src/components/ui/SelectLite.tsx b/openless-all/app/src/components/ui/SelectLite.tsx index ec3ebebe..45f87e7f 100644 --- a/openless-all/app/src/components/ui/SelectLite.tsx +++ b/openless-all/app/src/components/ui/SelectLite.tsx @@ -60,7 +60,7 @@ const DEFAULT_TRIGGER_STYLE: CSSProperties = { height: 32, fontSize: 12.5, fontFamily: 'inherit', - borderRadius: 8, + borderRadius: 'var(--ol-control-radius)', border: '0.5px solid var(--ol-line-strong)', background: 'var(--ol-surface-2)', color: 'var(--ol-ink)', @@ -322,12 +322,12 @@ export function SelectLite({ maxHeight: 280, overflowY: 'auto', padding: 4, - borderRadius: 10, - border: '0.5px solid rgba(0, 0, 0, 0.10)', - background: 'rgba(252, 252, 254, 0.94)', - backdropFilter: 'blur(20px) saturate(180%)', - WebkitBackdropFilter: 'blur(20px) saturate(180%)', - boxShadow: '0 12px 30px -10px rgba(15, 17, 22, 0.25), 0 0 0 0.5px rgba(0, 0, 0, 0.06)', + borderRadius: 'var(--ol-r-md)', + border: '0.5px solid var(--ol-line-strong)', + background: 'var(--ol-glass-bg-strong)', + backdropFilter: 'blur(var(--ol-glass-blur)) saturate(180%)', + WebkitBackdropFilter: 'blur(var(--ol-glass-blur)) saturate(180%)', + boxShadow: 'var(--ol-shadow-md)', zIndex: 9999, fontFamily: 'inherit', fontSize: 12.5, @@ -357,11 +357,11 @@ export function SelectLite({ alignItems: 'center', gap: 8, padding: '7px 10px', - borderRadius: 6, + borderRadius: 'var(--ol-r-sm)', cursor: option.disabled ? 'not-allowed' : 'default', opacity: option.disabled ? 0.45 : 1, background: isHighlighted && !option.disabled - ? 'rgba(37, 99, 235, 0.10)' + ? 'var(--ol-blue-soft)' : 'transparent', color: isSelected ? 'var(--ol-blue)' : 'var(--ol-ink)', fontWeight: isSelected ? 600 : 500, diff --git a/openless-all/app/src/components/ui/SwitchLite.tsx b/openless-all/app/src/components/ui/SwitchLite.tsx index 56181127..691fb1c7 100644 --- a/openless-all/app/src/components/ui/SwitchLite.tsx +++ b/openless-all/app/src/components/ui/SwitchLite.tsx @@ -14,8 +14,8 @@ export function SwitchLite({ on: initial = false }: SwitchLiteProps) { className="ol-focus-ring" onClick={() => setOn(!on)} style={{ - position: 'relative', width: 32, height: 18, borderRadius: 999, border: 0, - background: on ? 'var(--ol-blue)' : 'rgba(0,0,0,0.18)', + position: 'relative', width: 32, height: 18, borderRadius: 'var(--ol-pill-radius)', border: 0, + background: on ? 'var(--ol-blue)' : 'var(--ol-toggle-off-bg)', cursor: 'default', outline: 'none', }} @@ -23,7 +23,7 @@ export function SwitchLite({ on: initial = false }: SwitchLiteProps) { diff --git a/openless-all/app/src/i18n/en.ts b/openless-all/app/src/i18n/en.ts index fbbc2fc5..4b98efed 100644 --- a/openless-all/app/src/i18n/en.ts +++ b/openless-all/app/src/i18n/en.ts @@ -958,6 +958,14 @@ export const en: typeof zhCN = { ko: '한국어 (Beta)', restartHint: 'Some native menus (system tray, etc.) may require an app restart to fully switch.', }, + appearance: { + title: 'Appearance', + desc: 'Choose light or dark mode, or follow your system setting.', + label: 'Theme', + followSystem: 'Follow system', + light: 'Light', + dark: 'Dark', + }, remoteInput: { title: 'Remote Input', enableLabel: 'Enable remote input', diff --git a/openless-all/app/src/i18n/ja.ts b/openless-all/app/src/i18n/ja.ts index 5942e5a6..445a9068 100644 --- a/openless-all/app/src/i18n/ja.ts +++ b/openless-all/app/src/i18n/ja.ts @@ -926,6 +926,14 @@ export const ja: typeof zhCN = { ko: '한국어 (Beta)', restartHint: '一部のネイティブメニュー(トレイ等)は再起動後に反映されます。', }, + appearance: { + title: '外観', + desc: 'ライト/ダークモードを選ぶか、システム設定に従います。', + label: 'テーマ', + followSystem: 'システムに従う', + light: 'ライト', + dark: 'ダーク', + }, remoteInput: { title: 'リモート入力', enableLabel: 'リモート入力を有効化', diff --git a/openless-all/app/src/i18n/ko.ts b/openless-all/app/src/i18n/ko.ts index 07030b0c..d80bd63f 100644 --- a/openless-all/app/src/i18n/ko.ts +++ b/openless-all/app/src/i18n/ko.ts @@ -926,6 +926,14 @@ export const ko: typeof zhCN = { ko: '한국어 (Beta)', restartHint: '일부 네이티브 메뉴(트레이 등)는 앱 재시작 후 반영될 수 있습니다.', }, + appearance: { + title: '외관', + desc: '라이트/다크 모드를 선택하거나 시스템 설정을 따릅니다.', + label: '테마', + followSystem: '시스템 따라가기', + light: '라이트', + dark: '다크', + }, remoteInput: { title: '원격 입력', enableLabel: '원격 입력 활성화', diff --git a/openless-all/app/src/i18n/zh-CN.ts b/openless-all/app/src/i18n/zh-CN.ts index db8f3c3a..e26457ab 100644 --- a/openless-all/app/src/i18n/zh-CN.ts +++ b/openless-all/app/src/i18n/zh-CN.ts @@ -956,6 +956,14 @@ export const zhCN = { ko: '한국어 (Beta)', restartHint: '部分原生菜单(系统托盘等)可能需要重启 App 才会切换。', }, + appearance: { + title: '外观', + desc: '选择浅色或深色模式,或跟随系统设置。', + label: '主题', + followSystem: '跟随系统', + light: '浅色', + dark: '深色', + }, remoteInput: { title: '远程输入', enableLabel: '启用远程输入', diff --git a/openless-all/app/src/i18n/zh-TW.ts b/openless-all/app/src/i18n/zh-TW.ts index 523716fd..55aac730 100644 --- a/openless-all/app/src/i18n/zh-TW.ts +++ b/openless-all/app/src/i18n/zh-TW.ts @@ -924,6 +924,14 @@ export const zhTW: typeof zhCN = { ko: '한국어 (Beta)', restartHint: '部分原生菜單(系統托盤等)可能需要重啓 App 纔會切換。', }, + appearance: { + title: '外觀', + desc: '選擇淺色或深色模式,或跟隨系統設置。', + label: '主題', + followSystem: '跟隨系統', + light: '淺色', + dark: '深色', + }, remoteInput: { title: '遠端輸入', enableLabel: '啟用遠端輸入', diff --git a/openless-all/app/src/lib/platform.ts b/openless-all/app/src/lib/platform.ts index 03badcdb..3e91c6d2 100644 --- a/openless-all/app/src/lib/platform.ts +++ b/openless-all/app/src/lib/platform.ts @@ -120,3 +120,21 @@ export async function getPlatformCapabilities(): Promise { export function getCachedPlatformCapabilities(): PlatformCapabilities | null { return cachedCapabilities; } + +/** Win11 原生标题栏暗色同步;非 Windows / 非 Tauri 为 no-op。 */ +export async function syncWindowsCaptionTheme(dark: boolean): Promise { + if (typeof window === 'undefined') return; + if (detectOS() !== 'win') return; + + const isTauri = + globalThis.window !== undefined && + '__TAURI_INTERNALS__' in globalThis.window; + if (!isTauri) return; + + try { + const { invoke } = await import('@tauri-apps/api/core'); + await invoke('set_windows_caption_theme', { dark }); + } catch (err) { + console.warn('[platform] set_windows_caption_theme failed', err); + } +} diff --git a/openless-all/app/src/lib/themeMode.ts b/openless-all/app/src/lib/themeMode.ts new file mode 100644 index 00000000..5c1138a5 --- /dev/null +++ b/openless-all/app/src/lib/themeMode.ts @@ -0,0 +1,52 @@ +// 主题模式 — localStorage 持久化,通过 html[data-ol-theme] 切换暗色 token 集。 + +import { syncWindowsCaptionTheme } from './platform'; + +export type ThemeMode = 'system' | 'light' | 'dark'; +export type ResolvedThemeMode = 'light' | 'dark'; + +const THEME_MODE_KEY = 'ol-theme-mode'; + +function systemPrefersDark(): boolean { + return window.matchMedia('(prefers-color-scheme: dark)').matches; +} + +export function resolveThemeMode(mode: ThemeMode): ResolvedThemeMode { + if (mode === 'dark') return 'dark'; + if (mode === 'light') return 'light'; + return systemPrefersDark() ? 'dark' : 'light'; +} + +export function readThemeMode(): ThemeMode { + try { + const v = window.localStorage.getItem(THEME_MODE_KEY); + if (v === 'system' || v === 'light' || v === 'dark') return v; + } catch { /* localStorage 不可用:忽略,落回默认 */ } + return 'system'; +} + +export function applyThemeMode(mode: ThemeMode): void { + const resolved = resolveThemeMode(mode); + const root = document.documentElement; + if (resolved === 'dark') { + root.dataset.olTheme = 'dark'; + } else { + delete root.dataset.olTheme; + } + void syncWindowsCaptionTheme(resolved === 'dark'); +} + +export function setThemeMode(mode: ThemeMode): ThemeMode { + try { window.localStorage.setItem(THEME_MODE_KEY, mode); } catch { /* 忽略 */ } + applyThemeMode(mode); + return mode; +} + +export function subscribeThemeMode(): () => void { + const mq = window.matchMedia('(prefers-color-scheme: dark)'); + const onChange = () => { + if (readThemeMode() === 'system') applyThemeMode('system'); + }; + mq.addEventListener('change', onChange); + return () => mq.removeEventListener('change', onChange); +} diff --git a/openless-all/app/src/main.tsx b/openless-all/app/src/main.tsx index 73111798..3fff5d83 100644 --- a/openless-all/app/src/main.tsx +++ b/openless-all/app/src/main.tsx @@ -3,9 +3,14 @@ import ReactDOM from "react-dom/client"; import { App } from "./App"; import { detectOS } from "./components/WindowChrome"; import i18n from "./i18n"; // 副作用:触发 i18next init +import { applyThemeMode, readThemeMode, subscribeThemeMode } from "./lib/themeMode"; import "./styles/tokens.css"; import "./styles/global.css"; +// 首帧前应用主题,避免暗色偏好下先闪一帧亮色。 +applyThemeMode(readThemeMode()); +subscribeThemeMode(); + import type { OS } from "./components/WindowChrome"; const params = new URLSearchParams(window.location.search); diff --git a/openless-all/app/src/pages/History.tsx b/openless-all/app/src/pages/History.tsx index 371c7d42..f51c7ad6 100644 --- a/openless-all/app/src/pages/History.tsx +++ b/openless-all/app/src/pages/History.tsx @@ -238,7 +238,7 @@ export function History() {
@@ -264,7 +264,7 @@ export function History() { key={f.id} onClick={() => setFilter(f.id)} style={{ - padding: '3px 9px', fontSize: 11, borderRadius: 999, + padding: '3px 9px', fontSize: 11, borderRadius: 'var(--ol-pill-radius)', border: '0.5px solid ' + (filter === f.id ? 'var(--ol-pill-selected-border)' : 'var(--ol-line-strong)'), background: filter === f.id ? 'var(--ol-pill-selected-bg)' : 'transparent', color: filter === f.id ? 'var(--ol-pill-selected-ink)' : 'var(--ol-ink-3)', @@ -277,7 +277,7 @@ export function History() {
{actionError && ( -
+
{actionError}
)} @@ -302,7 +302,7 @@ export function History() { style={{ width: '100%', padding: '10px 12px', textAlign: 'left', display: 'flex', flexDirection: 'column', gap: 4, - border: 0, borderRadius: 8, + border: 0, borderRadius: 'var(--ol-control-radius)', background: selectedId === s.id ? 'rgba(37,99,235,0.06)' : 'transparent', boxShadow: selectedId === s.id ? 'inset 2px 0 0 var(--ol-blue)' : 'none', cursor: 'default', fontFamily: 'inherit', marginBottom: 1, @@ -363,13 +363,13 @@ export function History() { /> )}
-
+
{t('history.rawLabel')}

{item.rawTranscript || t('history.rawEmpty')}

-
+
{MODE_LABEL[item.mode]}

{item.finalText} diff --git a/openless-all/app/src/pages/LessComputerPanel.tsx b/openless-all/app/src/pages/LessComputerPanel.tsx index 206c0e85..fe729b5e 100644 --- a/openless-all/app/src/pages/LessComputerPanel.tsx +++ b/openless-all/app/src/pages/LessComputerPanel.tsx @@ -414,11 +414,11 @@ const shellStyle: CSSProperties = { height: '100vh', display: 'flex', flexDirection: 'column', - borderRadius: 14, + borderRadius: 'var(--ol-bubble-radius)', overflow: 'hidden', - border: '0.5px solid rgba(0, 0, 0, 0.12)', - background: 'rgba(246, 247, 250, 0.88)', - boxShadow: '0 18px 44px -18px rgba(15,17,22,.28), 0 0 0 0.5px rgba(255,255,255,.7) inset', + border: '0.5px solid var(--ol-line-strong)', + background: 'var(--ol-panel-bg)', + boxShadow: 'var(--ol-shadow-lg)', fontFamily: 'var(--ol-font-sans)', color: 'var(--ol-ink)', isolation: 'isolate', @@ -430,9 +430,8 @@ const toolbarStyle: CSSProperties = { alignItems: 'center', padding: '0 8px', borderBottom: '0.5px solid rgba(0, 0, 0, 0.08)', - background: - 'linear-gradient(180deg, rgba(255,255,255,0.74), rgba(238,240,245,0.58))', - boxShadow: '0 1px 0 rgba(255,255,255,.55) inset', + background: 'var(--ol-panel-bg)', + boxShadow: 'var(--ol-shadow-sm)', backdropFilter: 'blur(18px) saturate(150%)', WebkitBackdropFilter: 'blur(18px) saturate(150%)', flexShrink: 0, @@ -444,7 +443,7 @@ const closeBtnStyle: CSSProperties = { width: 22, height: 22, border: 0, - borderRadius: 6, + borderRadius: 'var(--ol-r-sm)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', @@ -479,10 +478,10 @@ const roleLabelStyle: CSSProperties = { const userBubbleStyle: CSSProperties = { maxWidth: '85%', padding: '8px 12px', - borderRadius: 14, + borderRadius: 'var(--ol-bubble-radius)', borderBottomRightRadius: 4, - background: 'var(--ol-blue)', - color: '#fff', + background: 'var(--ol-accent-solid-bg)', + color: 'var(--ol-accent-solid-ink)', fontSize: 13, lineHeight: 1.55, wordBreak: 'break-word', @@ -491,7 +490,7 @@ const userBubbleStyle: CSSProperties = { const assistantBubbleStyle: CSSProperties = { maxWidth: '92%', padding: '8px 12px', - borderRadius: 14, + borderRadius: 'var(--ol-bubble-radius)', borderBottomLeftRadius: 4, background: 'rgba(0,0,0,0.04)', fontSize: 13, @@ -519,7 +518,7 @@ const toolChipStyle: CSSProperties = { color: 'var(--ol-ink-2)', background: 'rgba(0,0,0,0.045)', border: '0.5px solid rgba(0,0,0,0.06)', - borderRadius: 8, + borderRadius: 'var(--ol-control-radius)', padding: '4px 8px', }; @@ -543,7 +542,7 @@ const approvalCardStyle: CSSProperties = { flexDirection: 'column', gap: 6, padding: '10px 12px', - borderRadius: 12, + borderRadius: 'var(--ol-r-lg)', background: 'rgba(220,38,38,0.05)', border: '0.5px solid rgba(220,38,38,0.20)', }; @@ -553,7 +552,7 @@ const approvalCmdStyle: CSSProperties = { fontSize: 11.5, color: 'var(--ol-ink)', background: 'rgba(0,0,0,0.05)', - borderRadius: 6, + borderRadius: 'var(--ol-r-sm)', padding: '5px 8px', wordBreak: 'break-all', }; @@ -564,25 +563,25 @@ const approvalRerunWarningStyle: CSSProperties = { color: 'rgb(180,83,9)', background: 'rgba(245,158,11,0.10)', border: '0.5px solid rgba(245,158,11,0.25)', - borderRadius: 6, + borderRadius: 'var(--ol-r-sm)', padding: '5px 8px', }; const approveBtnStyle: CSSProperties = { flex: 1, border: 0, - borderRadius: 8, + borderRadius: 'var(--ol-control-radius)', padding: '6px 10px', fontSize: 12, fontWeight: 600, cursor: 'default', - background: 'var(--ol-blue)', - color: '#fff', + background: 'var(--ol-accent-solid-bg)', + color: 'var(--ol-accent-solid-ink)', }; const denyBtnStyle: CSSProperties = { flex: 1, - borderRadius: 8, + borderRadius: 'var(--ol-control-radius)', padding: '6px 10px', fontSize: 12, fontWeight: 600, @@ -594,7 +593,7 @@ const denyBtnStyle: CSSProperties = { const errorRowStyle: CSSProperties = { padding: '8px 12px', - borderRadius: 10, + borderRadius: 'var(--ol-r-md)', background: 'rgba(220,38,38,0.06)', border: '0.5px solid rgba(220,38,38,0.18)', }; @@ -633,7 +632,7 @@ const globalCss = ` padding: 1px 5px; border-radius: 4px; background: rgba(0,0,0,0.05); } .lc-answer pre { margin: 0 0 6px; padding: 8px 10px; - border-radius: 8px; background: rgba(0,0,0,0.05); + border-radius: var(--ol-control-radius); background: rgba(0,0,0,0.05); overflow-x: auto; } .lc-answer pre code { padding: 0; background: transparent; } .lc-answer a { color: var(--ol-blue); text-decoration: none; } diff --git a/openless-all/app/src/pages/LocalAsr.tsx b/openless-all/app/src/pages/LocalAsr.tsx index 5c5624a8..62589498 100644 --- a/openless-all/app/src/pages/LocalAsr.tsx +++ b/openless-all/app/src/pages/LocalAsr.tsx @@ -1842,7 +1842,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) { style={{ fontSize: 13, padding: "6px 10px", - borderRadius: 8, + borderRadius: 'var(--ol-control-radius)', border: "0.5px solid rgba(0,0,0,0.12)", background: "var(--ol-surface)", color: "var(--ol-ink)", @@ -1903,7 +1903,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) { style={{ fontSize: 13, padding: "6px 10px", - borderRadius: 8, + borderRadius: 'var(--ol-control-radius)', border: "0.5px solid rgba(0,0,0,0.12)", background: "var(--ol-surface)", color: "var(--ol-ink)", @@ -1954,7 +1954,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) { style={{ fontSize: 13, padding: "6px 10px", - borderRadius: 8, + borderRadius: 'var(--ol-control-radius)', border: "0.5px solid rgba(0,0,0,0.12)", background: "var(--ol-surface)", color: "var(--ol-ink)", @@ -2266,7 +2266,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) { fontSize: 13, height: 31, padding: "0 10px", - borderRadius: 8, + borderRadius: 'var(--ol-control-radius)', border: "0.5px solid rgba(0,0,0,0.12)", background: "var(--ol-surface)", color: "var(--ol-ink)", @@ -2301,7 +2301,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) { fontSize: 13, height: 31, padding: "0 10px", - borderRadius: 8, + borderRadius: 'var(--ol-control-radius)', border: "0.5px solid rgba(0,0,0,0.12)", background: "var(--ol-surface)", color: "var(--ol-ink)", @@ -2334,7 +2334,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) { fontSize: 13, height: 31, padding: "0 10px", - borderRadius: 8, + borderRadius: 'var(--ol-control-radius)', border: "0.5px solid rgba(0,0,0,0.12)", background: "var(--ol-surface)", color: "var(--ol-ink)", @@ -2613,7 +2613,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) { style={{ fontSize: 13, padding: "6px 10px", - borderRadius: 8, + borderRadius: 'var(--ol-control-radius)', border: "0.5px solid rgba(0,0,0,0.12)", background: "var(--ol-surface)", color: "var(--ol-ink)", @@ -2741,7 +2741,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) { style={{ fontSize: 13, padding: "6px 10px", - borderRadius: 8, + borderRadius: 'var(--ol-control-radius)', border: "0.5px solid rgba(0,0,0,0.12)", background: "var(--ol-surface)", color: "var(--ol-ink)", @@ -2905,7 +2905,7 @@ function FoundryPrepareProgressBlock({

@@ -498,7 +498,7 @@ export function Marketplace() { padding: '6px 10px', fontSize: 12, border: '0.5px solid var(--ol-line-strong)', - borderRadius: 8, + borderRadius: 'var(--ol-control-radius)', cursor: 'pointer', background: sort === p.id ? 'var(--ol-blue-soft)' : 'var(--ol-surface)', color: sort === p.id ? 'var(--ol-blue)' : 'var(--ol-ink-2)', @@ -567,7 +567,7 @@ export function Marketplace() { style={{ textAlign: 'left', padding: 14, - borderRadius: 12, + borderRadius: 'var(--ol-r-lg)', border: '0.5px solid var(--ol-line-strong)', background: 'var(--ol-surface)', cursor: 'pointer', @@ -644,7 +644,7 @@ export function Marketplace() { style={{ padding: 12, border: '0.5px solid var(--ol-line)', - borderRadius: 10, + borderRadius: 'var(--ol-r-md)', background: 'var(--ol-surface-2)', marginBottom: 14, maxHeight: 280, @@ -679,7 +679,7 @@ export function Marketplace() { border: 'none', cursor: 'pointer', padding: '4px 8px', - borderRadius: 8, + borderRadius: 'var(--ol-control-radius)', fontSize: 12, fontWeight: 500, color: 'var(--ol-ink-2)' @@ -744,7 +744,7 @@ export function Marketplace() { textAlign: 'left', padding: 10, border: selected ? '1px solid var(--ol-blue)' : '0.5px solid var(--ol-line-strong)', - borderRadius: 8, + borderRadius: 'var(--ol-control-radius)', background: selected ? 'var(--ol-blue-soft)' : 'var(--ol-surface)', cursor: 'pointer', display: 'flex', @@ -755,11 +755,11 @@ export function Marketplace() { {/* 选中圈:未选空圆,选中蓝实心 + 白勾 */} {selected && '✓'} @@ -814,7 +814,7 @@ export function Marketplace() { gap: 6, padding: '6px 10px', border: '0.5px solid var(--ol-line-strong)', - borderRadius: 10, + borderRadius: 'var(--ol-r-md)', background: 'var(--ol-surface)', }} > @@ -843,7 +843,7 @@ export function Marketplace() { onClick={() => setShowLogin(true)} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, - padding: '5px 10px', borderRadius: 9, + padding: '5px 10px', borderRadius: 'var(--ol-control-radius)', border: '0.5px solid var(--ol-line-strong)', background: currentLogin ? 'var(--ol-blue-soft)' : 'var(--ol-surface)', color: currentLogin ? 'var(--ol-blue)' : 'var(--ol-ink-3)', @@ -853,7 +853,7 @@ export function Marketplace() { }} > setShowMyPacks(false)} style={{ - width: 30, height: 30, borderRadius: 9, + width: 30, height: 30, borderRadius: 'var(--ol-control-radius)', display: 'inline-grid', placeItems: 'center', border: '0.5px solid var(--ol-line-strong)', background: 'var(--ol-surface)', @@ -965,7 +965,7 @@ export function Marketplace() { key={pack.id} style={{ padding: 14, - borderRadius: 12, + borderRadius: 'var(--ol-r-lg)', border: '0.5px solid var(--ol-line-strong)', background: 'var(--ol-surface)', display: 'flex', diff --git a/openless-all/app/src/pages/Overview.tsx b/openless-all/app/src/pages/Overview.tsx index 8dadbc82..84d5bdcd 100644 --- a/openless-all/app/src/pages/Overview.tsx +++ b/openless-all/app/src/pages/Overview.tsx @@ -289,7 +289,7 @@ function ProviderCard({ kind, name, subname, status }: ProviderCardProps) {
{kind} {status === 'configured' && ( - + {t('overview.statusConfigured')} )} @@ -573,20 +573,20 @@ function InAppDictationControl() { style={{ width: 52, height: 52, - borderRadius: 999, + borderRadius: 'var(--ol-pill-radius)', border: 0, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', - background: recording ? 'var(--ol-err, #ef4444)' : 'var(--ol-blue)', - color: '#fff', + background: recording ? 'var(--ol-danger-solid-bg)' : 'var(--ol-accent-solid-bg)', + color: recording ? 'var(--ol-danger-solid-ink)' : 'var(--ol-accent-solid-ink)', cursor: busy || processing ? 'not-allowed' : 'default', opacity: busy || processing ? 0.7 : 1, transition: 'background 0.16s var(--ol-motion-quick), opacity 0.16s var(--ol-motion-quick)', }} > {recording ? ( - + ) : ( )} diff --git a/openless-all/app/src/pages/QaPanel.tsx b/openless-all/app/src/pages/QaPanel.tsx index 847581ac..f1df23e1 100644 --- a/openless-all/app/src/pages/QaPanel.tsx +++ b/openless-all/app/src/pages/QaPanel.tsx @@ -685,7 +685,8 @@ function MobileRecordButton({ disabled={disabled} style={{ ...mobileRecordButtonStyle, - background: recording ? 'var(--ol-err)' : 'var(--ol-blue)', + background: recording ? 'var(--ol-danger-solid-bg)' : 'var(--ol-accent-solid-bg)', + color: recording ? 'var(--ol-danger-solid-ink)' : 'var(--ol-accent-solid-ink)', opacity: disabled ? 0.48 : 1, }} > @@ -773,10 +774,10 @@ const shellStyle: CSSProperties = { height: '100vh', display: 'flex', flexDirection: 'column', - borderRadius: 14, + borderRadius: 'var(--ol-bubble-radius)', overflow: 'hidden', - border: '0.5px solid rgba(0, 0, 0, 0.08)', - boxShadow: 'var(--ol-shadow-lg), inset 0 1px 0 0 rgba(255, 255, 255, 0.9)', + border: '0.5px solid var(--ol-line)', + boxShadow: 'var(--ol-shadow-lg)', fontFamily: 'var(--ol-font-sans)', color: 'var(--ol-ink)', }; @@ -804,7 +805,7 @@ const iconBtnBaseStyle: CSSProperties = { width: 22, height: 22, border: 0, - borderRadius: 6, + borderRadius: 'var(--ol-r-sm)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', @@ -839,7 +840,7 @@ const previewStyle: CSSProperties = { fontSize: 11.5, lineHeight: 1.5, padding: '8px 10px', - borderRadius: 8, + borderRadius: 'var(--ol-control-radius)', background: 'rgba(0, 0, 0, 0.035)', border: '0.5px solid rgba(0, 0, 0, 0.06)', }; @@ -858,10 +859,10 @@ const turnIndicatorStyle: CSSProperties = { const userBubbleStyle: CSSProperties = { maxWidth: '80%', padding: '8px 12px', - borderRadius: 14, + borderRadius: 'var(--ol-bubble-radius)', borderBottomRightRadius: 4, - background: 'var(--ol-blue)', - color: '#fff', + background: 'var(--ol-accent-solid-bg)', + color: 'var(--ol-accent-solid-ink)', fontSize: 13, lineHeight: 1.55, wordBreak: 'break-word', @@ -870,7 +871,7 @@ const userBubbleStyle: CSSProperties = { const selectionQuoteStyle: CSSProperties = { maxWidth: '80%', padding: '6px 10px', - borderRadius: 10, + borderRadius: 'var(--ol-r-md)', background: 'rgba(0,0,0,0.04)', border: '0.5px solid rgba(0,0,0,0.06)', fontSize: 11.5, @@ -882,7 +883,7 @@ const selectionQuoteStyle: CSSProperties = { const assistantBubbleStyle: CSSProperties = { maxWidth: '92%', padding: '8px 12px', - borderRadius: 14, + borderRadius: 'var(--ol-bubble-radius)', borderBottomLeftRadius: 4, background: 'rgba(0,0,0,0.04)', fontSize: 13, @@ -897,7 +898,7 @@ const errorRowStyle: CSSProperties = { flexDirection: 'column', gap: 4, padding: '8px 12px', - borderRadius: 10, + borderRadius: 'var(--ol-r-md)', background: 'rgba(220,38,38,0.06)', border: '0.5px solid rgba(220,38,38,0.18)', }; @@ -918,7 +919,7 @@ const statusBarStyle: CSSProperties = { gap: 8, padding: '0 16px', borderTop: '0.5px solid rgba(0, 0, 0, 0.06)', - background: 'rgba(255,255,255,0.4)', + background: 'var(--ol-control-muted)', }; const embeddedToolbarStyle: CSSProperties = { @@ -938,8 +939,8 @@ const mobileRecordButtonStyle: CSSProperties = { width: '100%', minHeight: 42, border: 0, - borderRadius: 10, - color: '#fff', + borderRadius: 'var(--ol-r-md)', + color: 'var(--ol-accent-solid-ink)', fontSize: 14, fontWeight: 700, fontFamily: 'var(--ol-font-sans)', @@ -953,7 +954,7 @@ const composerStyle: CSSProperties = { gap: 8, padding: '10px 12px', borderTop: '0.5px solid rgba(0, 0, 0, 0.06)', - background: 'rgba(255,255,255,0.48)', + background: 'var(--ol-control-muted-strong)', }; const composerTextareaStyle: CSSProperties = { @@ -962,13 +963,13 @@ const composerTextareaStyle: CSSProperties = { maxHeight: 88, resize: 'vertical', border: '0.5px solid rgba(0,0,0,0.12)', - borderRadius: 8, + borderRadius: 'var(--ol-control-radius)', padding: '8px 9px', fontFamily: 'var(--ol-font-sans)', fontSize: 13, lineHeight: 1.45, color: 'var(--ol-ink)', - background: 'rgba(255,255,255,0.78)', + background: 'var(--ol-control-solid)', outline: 'none', }; @@ -976,10 +977,10 @@ const composerSendStyle: CSSProperties = { minHeight: 40, minWidth: 58, border: 0, - borderRadius: 8, + borderRadius: 'var(--ol-control-radius)', padding: '0 12px', - background: 'var(--ol-blue)', - color: '#fff', + background: 'var(--ol-accent-solid-bg)', + color: 'var(--ol-accent-solid-ink)', fontFamily: 'var(--ol-font-sans)', fontSize: 13, fontWeight: 700, @@ -1010,7 +1011,7 @@ const globalCss = ` padding: 1px 5px; border-radius: 4px; background: rgba(0,0,0,0.05); } .qa-answer pre { margin: 0 0 6px; padding: 8px 10px; - border-radius: 8px; background: rgba(0,0,0,0.05); + border-radius: var(--ol-control-radius); background: rgba(0,0,0,0.05); overflow-x: auto; } .qa-answer pre code { padding: 0; background: transparent; } .qa-answer a { color: var(--ol-blue); text-decoration: none; } diff --git a/openless-all/app/src/pages/SelectionAsk.tsx b/openless-all/app/src/pages/SelectionAsk.tsx index 980fad77..04cc8dae 100644 --- a/openless-all/app/src/pages/SelectionAsk.tsx +++ b/openless-all/app/src/pages/SelectionAsk.tsx @@ -100,7 +100,7 @@ export function SelectionAsk() { alignItems: 'center', gap: 12, padding: '8px 14px', - borderRadius: 10, + borderRadius: 'var(--ol-r-md)', background: 'rgba(0,0,0,0.04)', border: '0.5px solid var(--ol-line)', }} @@ -116,7 +116,7 @@ export function SelectionAsk() { position: 'relative', width: 36, height: 20, - borderRadius: 999, + borderRadius: 'var(--ol-pill-radius)', border: 0, background: prefs.qaSaveHistory ? 'var(--ol-blue)' : 'rgba(0,0,0,0.18)', cursor: saving ? 'not-allowed' : 'default', @@ -132,8 +132,8 @@ export function SelectionAsk() { left: prefs.qaSaveHistory ? 18 : 2, width: 16, height: 16, - borderRadius: 999, - background: '#fff', + borderRadius: 'var(--ol-pill-radius)', + background: 'var(--ol-control-solid)', boxShadow: '0 1px 2px rgba(0,0,0,.2)', transition: 'left .16s var(--ol-motion-spring)', }} diff --git a/openless-all/app/src/pages/Style.tsx b/openless-all/app/src/pages/Style.tsx index b6baf7d3..43cabc5c 100644 --- a/openless-all/app/src/pages/Style.tsx +++ b/openless-all/app/src/pages/Style.tsx @@ -570,7 +570,7 @@ export function Style() { alignItems: 'center', gap: 6, padding: '6px 12px', - borderRadius: 999, + borderRadius: 'var(--ol-pill-radius)', border: '0.5px solid', borderColor: rawPack.active ? 'var(--ol-blue)' : 'var(--ol-line-strong)', background: rawPack.active ? 'var(--ol-blue-soft)' : 'transparent', @@ -613,15 +613,11 @@ export function Style() { textAlign: 'left', position: 'relative', border: '0.5px solid', - borderColor: pack.active ? 'var(--ol-blue)' : 'var(--ol-line)', - background: pack.active - ? 'linear-gradient(180deg, rgba(239,246,255,0.92), rgba(255,255,255,0.98))' - : isBuiltin - ? 'linear-gradient(180deg, rgba(248,250,252,0.92), rgba(241,245,249,0.85))' - : 'linear-gradient(180deg, rgba(255,255,255,0.98), rgba(248,250,252,0.92))', - borderRadius: 18, + borderColor: pack.active ? 'var(--ol-style-card-border-active)' : 'var(--ol-style-card-border)', + background: pack.active ? 'var(--ol-style-card-bg-active)' : 'var(--ol-style-card-bg)', + borderRadius: 'var(--ol-modal-radius)', padding: 16, - boxShadow: pack.active ? '0 0 0 3px var(--ol-blue-ring)' : 'none', + boxShadow: pack.active ? 'var(--ol-style-card-shadow-active)' : 'var(--ol-style-card-shadow)', cursor: 'default', minHeight: 204, }} @@ -629,7 +625,7 @@ export function Style() {
-
+
{pack.name}
@@ -646,7 +642,7 @@ export function Style() {
@@ -680,11 +676,11 @@ export function Style() { aria-label={t('style.pack.deleteImported')} title={t('style.pack.deleteImported')} style={{ - width: 36, height: 36, borderRadius: 12, + width: 36, height: 36, borderRadius: 'var(--ol-r-lg)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, - border: '0.5px solid rgba(239,68,68,0.32)', - background: 'rgba(254,242,242,0.6)', + border: '0.5px solid var(--ol-style-delete-border)', + background: 'var(--ol-style-delete-bg)', color: 'var(--ol-red, #ef4444)', cursor: busy === 'deleting' ? 'wait' : 'pointer', opacity: busy === 'deleting' ? 0.55 : 1, @@ -755,11 +751,11 @@ export function Style() { justifyContent: 'center', gap: 8, textAlign: 'center', - border: '0.5px dashed var(--ol-line-strong)', - borderRadius: 18, + border: '0.5px dashed var(--ol-style-add-tile-border)', + borderRadius: 'var(--ol-modal-radius)', padding: 16, - background: 'transparent', - color: 'var(--ol-ink-3)', + background: 'var(--ol-style-add-tile-bg)', + color: 'var(--ol-style-card-ink-3)', cursor: busy === 'creating' ? 'wait' : 'pointer', opacity: busy === 'creating' ? 0.55 : 1, minHeight: 204, @@ -768,16 +764,16 @@ export function Style() { >
-
{t('style.pack.addPackTileTitle')}
-
{t('style.pack.addPackTileHint')}
+
{t('style.pack.addPackTileTitle')}
+
{t('style.pack.addPackTileHint')}
@@ -843,7 +839,7 @@ export function Style() { style={{ width: 28, height: 28, - borderRadius: 999, + borderRadius: 'var(--ol-pill-radius)', border: 0, background: 'transparent', color: 'var(--ol-ink-3)', @@ -990,8 +986,9 @@ export function Style() {
@@ -1057,9 +1054,10 @@ export function Style() {
@@ -1095,7 +1093,9 @@ export function Style() { key={`${draft.id}-example-${index}`} padding={16} style={{ - background: 'linear-gradient(180deg, rgba(255,255,255,0.98), rgba(248,250,252,0.98))', + background: 'var(--ol-style-card-bg)', + border: '0.5px solid var(--ol-style-card-border)', + boxShadow: 'var(--ol-style-card-shadow)', }} >
@@ -1113,7 +1113,7 @@ export function Style() { width: 32, height: 32, flexShrink: 0, border: '0.5px solid var(--ol-line-strong)', - borderRadius: 8, + borderRadius: 'var(--ol-control-radius)', background: 'transparent', color: 'var(--ol-ink-2)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', @@ -1128,9 +1128,9 @@ export function Style() {
@@ -1140,15 +1140,15 @@ export function Style() {