From e330edef14baf9a247a80e1e1d3eadd4e9b73177 Mon Sep 17 00:00:00 2001 From: HKLHaoBin Date: Fri, 12 Jun 2026 21:14:16 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E6=A0=B7=E5=BC=8F=E4=B8=8E?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E4=BB=A5=E6=8F=90=E9=AB=98=E4=B8=80=E8=87=B4?= =?UTF-8?q?=E6=80=A7=E5=B9=B6=E4=BC=98=E5=8C=96=E8=AE=BE=E8=AE=A1=20?= =?UTF-8?q?=E2=80=A2=20=E6=9B=B4=E6=96=B0=E4=BA=86=20Card=20=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E6=A0=B7=E5=BC=8F=EF=BC=8C=E9=87=87=E7=94=A8=E6=96=B0?= =?UTF-8?q?=E7=9A=84=E5=8F=98=E9=87=8F=E6=9D=A5=E5=AE=9E=E7=8E=B0=E7=8E=BB?= =?UTF-8?q?=E7=92=83=E6=8B=9F=E6=80=81=E6=95=88=E6=9E=9C=E5=92=8C=E9=98=B4?= =?UTF-8?q?=E5=BD=B1=E3=80=82=20=E2=80=A2=20=E4=BF=AE=E6=94=B9=E4=BA=86=20?= =?UTF-8?q?Btn=20=E7=BB=84=E4=BB=B6=E4=B8=AD=E7=9A=84=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=EF=BC=8C=E4=BB=A5=E7=AC=A6=E5=90=88=E6=96=B0?= =?UTF-8?q?=E7=9A=84=E9=A2=9C=E8=89=B2=E4=BB=A4=E7=89=8C=EF=BC=88tokens?= =?UTF-8?q?=EF=BC=89=E3=80=82=20=E2=80=A2=20=E8=B0=83=E6=95=B4=E4=BA=86=20?= =?UTF-8?q?Collapsible=20=E7=BB=84=E4=BB=B6=E6=A0=B7=E5=BC=8F=EF=BC=8C?= =?UTF-8?q?=E4=BB=A5=E8=8E=B7=E5=BE=97=E6=9B=B4=E5=A5=BD=E7=9A=84=E8=BE=B9?= =?UTF-8?q?=E6=A1=86=E5=92=8C=E8=83=8C=E6=99=AF=E4=B8=80=E8=87=B4=E6=80=A7?= =?UTF-8?q?=E3=80=82=20=E2=80=A2=20=E7=BB=9F=E4=B8=80=E4=BA=86=E5=90=84?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E7=BB=84=E4=BB=B6=E7=9A=84=E8=BE=B9=E6=A1=86?= =?UTF-8?q?=E5=9C=86=E8=A7=92=E5=92=8C=E8=83=8C=E6=99=AF=E9=A2=9C=E8=89=B2?= =?UTF-8?q?=E3=80=82=20=E2=80=A2=20=E5=A2=9E=E5=BC=BA=E4=BA=86=20AboutSect?= =?UTF-8?q?ion=E3=80=81CheckUpdateButton=20=E5=8F=8A=E5=85=B6=E4=BB=96?= =?UTF-8?q?=E6=9D=BF=E5=9D=97=EF=BC=8C=E4=BB=A5=E5=88=A9=E7=94=A8=E6=96=B0?= =?UTF-8?q?=E7=9A=84=E8=AE=BE=E8=AE=A1=E4=BB=A4=E7=89=8C=E3=80=82=20?= =?UTF-8?q?=E2=80=A2=20=E4=BC=98=E5=8C=96=E4=BA=86=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E5=A4=96=E5=A3=B3=EF=BC=88app=20shell=EF=BC=89=E5=92=8C=20aura?= =?UTF-8?q?=20=E8=A1=A8=E9=9D=A2=E7=9A=84=E5=85=A8=E5=B1=80=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F=EF=BC=8C=E7=A1=AE=E4=BF=9D=E8=A7=86=E8=A7=89=E6=95=88?= =?UTF-8?q?=E6=9E=9C=E7=9A=84=E4=B8=80=E8=87=B4=E6=80=A7=E3=80=82=20?= =?UTF-8?q?=E2=80=A2=20=E5=BC=95=E5=85=A5=E4=BA=86=E6=96=B0=E7=9A=84?= =?UTF-8?q?=E9=A2=9C=E8=89=B2=E4=BB=A4=E7=89=8C=EF=BC=8C=E4=BB=A5=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E6=9B=B4=E5=A5=BD=E7=9A=84=E4=B8=BB=E9=A2=98=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=92=8C=E6=97=A0=E9=9A=9C=E7=A2=8D=E6=80=A7=E3=80=82?= =?UTF-8?q?=20=E2=80=A2=20=E6=9B=B4=E6=96=B0=E4=BA=86=E5=A4=9A=E4=B8=AA?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E7=9A=84=20CSS=EF=BC=8C=E4=BB=A5=E5=8F=8D?= =?UTF-8?q?=E6=98=A0=E8=AE=BE=E8=AE=A1=E7=B3=BB=E7=BB=9F=E7=9A=84=E5=8F=98?= =?UTF-8?q?=E6=9B=B4=EF=BC=8C=E5=8C=85=E6=8B=AC=E6=8C=89=E9=92=AE=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E5=92=8C=E8=83=8C=E6=99=AF=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/scripts/aura-skin-contract.test.mjs | 348 ++++++++++++++- .../src/remote_server/assets/style.css | 45 +- .../app/src/components/AutoUpdate.tsx | 4 +- openless-all/app/src/components/Capsule.tsx | 22 +- .../app/src/components/FloatingShell.tsx | 276 ++---------- .../app/src/components/GithubLoginModal.tsx | 8 +- .../app/src/components/MarketplaceModal.tsx | 12 +- .../app/src/components/Onboarding.tsx | 34 +- .../app/src/components/SettingsModal.tsx | 70 +-- .../app/src/components/ShortcutRecorder.tsx | 12 +- openless-all/app/src/components/ui/Modal.tsx | 10 +- .../app/src/components/ui/SegSimple.tsx | 8 +- .../app/src/components/ui/SelectLite.tsx | 18 +- .../app/src/components/ui/SwitchLite.tsx | 6 +- openless-all/app/src/pages/History.tsx | 12 +- .../app/src/pages/LessComputerPanel.tsx | 43 +- openless-all/app/src/pages/LocalAsr.tsx | 22 +- openless-all/app/src/pages/Marketplace.tsx | 34 +- openless-all/app/src/pages/Overview.tsx | 12 +- openless-all/app/src/pages/QaPanel.tsx | 45 +- openless-all/app/src/pages/SelectionAsk.tsx | 8 +- openless-all/app/src/pages/Style.tsx | 42 +- openless-all/app/src/pages/Translation.tsx | 20 +- openless-all/app/src/pages/Vocab.tsx | 24 +- openless-all/app/src/pages/_atoms.tsx | 23 +- .../app/src/pages/settings/AboutSection.tsx | 6 +- .../src/pages/settings/CheckUpdateButton.tsx | 4 +- .../pages/settings/ClaudeConsoleSection.tsx | 2 +- .../src/pages/settings/MarketplaceSection.tsx | 6 +- .../src/pages/settings/ProvidersSection.tsx | 4 +- .../pages/settings/RecordingInputSection.tsx | 10 +- .../src/pages/settings/RemoteInputSection.tsx | 8 +- .../src/pages/settings/ShortcutsSection.tsx | 10 +- .../app/src/pages/settings/shared.tsx | 10 +- openless-all/app/src/pages/settings/tabs.tsx | 4 +- openless-all/app/src/styles/global.css | 399 ++++++++++++++++++ openless-all/app/src/styles/tokens.css | 45 +- 37 files changed, 1075 insertions(+), 591 deletions(-) diff --git a/openless-all/app/scripts/aura-skin-contract.test.mjs b/openless-all/app/scripts/aura-skin-contract.test.mjs index 015d9c3e..22b5baab 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,165 @@ assertUsesClassName( 'sample should accept className usage', ); -const [tokens, globalCss, shell, settingsModal, overview] = await Promise.all([ +const [tokens, globalCss, shell, settingsModal, overview, 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'), + 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); +} 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 +353,60 @@ 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'); +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/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..f2b7323a 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-control-muted-strong)' : 'var(--ol-control-solid)', 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)', + 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..8a8f902e 100644 --- a/openless-all/app/src/components/SettingsModal.tsx +++ b/openless-all/app/src/components/SettingsModal.tsx @@ -80,6 +80,7 @@ export function SettingsModal({ os: _os, onClose, initialSettingsSection }: Sett
e.stopPropagation()} style={{ width: '100%', @@ -175,7 +176,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 +211,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..20a9e0ec 100644 --- a/openless-all/app/src/components/ui/SegSimple.tsx +++ b/openless-all/app/src/components/ui/SegSimple.tsx @@ -10,17 +10,17 @@ interface SegSimpleProps { export function SegSimple({ options, active }: SegSimpleProps) { const [v, setV] = useState(active); return ( -
+
{options.map((o) => ( @@ -451,12 +451,12 @@ function VocabChip({ entry, onRemove, onToggle }: VocabChipProps) { 0 ? 'var(--ol-blue-soft)' : 'var(--ol-surface)') : 'var(--ol-surface-2)', opacity: enabled ? 1 : 0.55, @@ -476,10 +476,10 @@ function VocabChip({ entry, onRemove, onToggle }: VocabChipProps) { 0 && enabled ? 'var(--ol-blue)' : 'rgba(0,0,0,0.06)', - color: entry.hits > 0 && enabled ? '#fff' : 'var(--ol-ink-4)', + background: entry.hits > 0 && enabled ? 'var(--ol-accent-solid-bg)' : 'rgba(0,0,0,0.06)', + color: entry.hits > 0 && enabled ? 'var(--ol-accent-solid-ink)' : 'var(--ol-ink-4)', fontFamily: 'var(--ol-font-sans)', }} >{entry.hits} @@ -487,7 +487,7 @@ function VocabChip({ entry, onRemove, onToggle }: VocabChipProps) { onClick={onRemove} aria-label={t('vocab.removeAria')} style={{ - width: 14, height: 14, padding: 0, border: 0, borderRadius: 999, + width: 14, height: 14, padding: 0, border: 0, borderRadius: 'var(--ol-pill-radius)', background: 'transparent', color: 'var(--ol-ink-4)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'default', }} diff --git a/openless-all/app/src/pages/_atoms.tsx b/openless-all/app/src/pages/_atoms.tsx index 1017f648..695d2103 100644 --- a/openless-all/app/src/pages/_atoms.tsx +++ b/openless-all/app/src/pages/_atoms.tsx @@ -83,13 +83,14 @@ export function Card({ children, style, padding = 18, glassy = false, className
@@ -161,7 +162,7 @@ interface BtnProps { export function Btn({ children, variant = 'ghost', size = 'md', icon, style, onClick, disabled = false }: BtnProps) { const variants: Record = { primary: { bg: 'linear-gradient(180deg, #141821, #0d1016)', color: 'var(--ol-on-accent)', bd: 'transparent', sh: '0 14px 32px -22px rgba(15,23,42,0.55)' }, - blue: { bg: 'linear-gradient(180deg, var(--ol-blue), var(--ol-blue-hover))', color: 'var(--ol-on-accent)', bd: 'transparent', sh: '0 14px 32px -22px rgba(37,99,235,.45)' }, + blue: { bg: 'linear-gradient(180deg, var(--ol-accent-solid-bg), var(--ol-accent-solid-bg-hover))', color: 'var(--ol-accent-solid-ink)', bd: 'transparent', sh: '0 14px 32px -22px rgba(37,99,235,.45)' }, ghost: { bg: 'var(--ol-control-muted)', color: 'var(--ol-ink-2)', bd: 'var(--ol-control-border)', sh: 'var(--ol-control-shadow)' }, soft: { bg: 'var(--ol-control-muted-strong)', color: 'var(--ol-ink-2)', bd: 'transparent', sh: 'var(--ol-control-shadow)' }, }; @@ -221,10 +222,10 @@ export function Collapsible({ title, desc, defaultOpen = false, embedded = false
-
+
{choices.map(([v, l]) => ( @@ -201,7 +201,7 @@ export function ShortcutsSection() { {t('modal.about.localFirst')} diff --git a/openless-all/app/src/styles/global.css b/openless-all/app/src/styles/global.css index 30fdfd61..3d975d52 100644 --- a/openless-all/app/src/styles/global.css +++ b/openless-all/app/src/styles/global.css @@ -177,3 +177,402 @@ html[data-ol-platform="linux"] .ol-frost { html[data-ol-platform="linux"] .ol-frost::before { content: none; } + +/* ── App shell + Aura surfaces ─────────────────────────────────────────── */ + +.ol-app-shell-bg { + background: var(--ol-app-shell-bg); +} + +.ol-aura-panel { + background: var(--ol-panel-bg); + border: 1px solid var(--ol-panel-border); + box-shadow: var(--ol-panel-shadow); + backdrop-filter: blur(var(--ol-aura-glass-blur)) saturate(150%); + -webkit-backdrop-filter: blur(var(--ol-aura-glass-blur)) saturate(150%); +} + +.ol-aura-card { + background: var(--ol-card-bg); + border: 1px solid var(--ol-card-border); + border-radius: var(--ol-card-radius); + box-shadow: var(--ol-card-shadow); +} + +html[data-ol-platform="linux"] .ol-aura-panel, +html[data-ol-platform="linux"] .ol-aura-card { + backdrop-filter: none; + -webkit-backdrop-filter: none; + background: var(--ol-surface); +} + +/* ── FloatingShell nav + sidebar ───────────────────────────────────────── */ + +.ol-nav-btn { + color: var(--ol-ink-3); + font-weight: 500; +} + +.ol-nav-btn.ol-nav-btn-active { + color: var(--ol-ink); + font-weight: 600; +} + +.ol-nav-btn:not(.ol-nav-btn-active):hover { + background: var(--ol-nav-hover-bg); + color: var(--ol-ink); +} + +.ol-aura-sidebar { + padding: 14px 12px 14px; + background: var(--ol-sidebar-bg); + border-right: 1px solid var(--ol-sidebar-border); +} + +.ol-aura-sidebar-brand { + display: flex; + align-items: center; + gap: 10px; + padding: 6px 10px 16px; + margin-bottom: 6px; + border-radius: var(--ol-r-lg); + background: var(--ol-sidebar-brand-bg); + border: 1px solid var(--ol-sidebar-brand-border); + box-shadow: none; +} + +.ol-aura-sidebar-brand-mark { + width: 26px; + height: 26px; + border-radius: var(--ol-control-radius); + box-shadow: none; + box-sizing: border-box; + padding: 3px; + object-fit: contain; +} + +.ol-aura-sidebar-brand-title { + font-size: 14px; + font-weight: 600; + font-family: var(--ol-font-display); + color: var(--ol-ink); +} + +.ol-aura-sidebar-brand-kicker { + font-size: 10.5px; + color: var(--ol-ink-4); + font-family: var(--ol-font-mono); + letter-spacing: .08em; +} + +.ol-aura-sidebar-pill { + background: var(--ol-sidebar-pill-bg); + border-radius: var(--ol-r-lg); + border: 1px solid var(--ol-sidebar-pill-border); + box-shadow: none; +} + +.ol-aura-sidebar-nav-btn { + padding: 8px 12px; + border-radius: var(--ol-r-lg); + border: 0; + background: transparent; + font-family: inherit; + font-size: 13px; + cursor: default; + transition: color 0.16s var(--ol-motion-quick), background 0.16s var(--ol-motion-quick); + text-align: left; + position: relative; + z-index: 1; +} + +.ol-aura-sidebar-footer { + display: flex; + flex-direction: column; + gap: 10px; + padding: 12px 10px 0; + margin-top: 10px; + border-top: 1px solid var(--ol-sidebar-footer-border); +} + +.ol-aura-sidebar-version { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + padding: 10px 12px; + font-family: var(--ol-font-sans); + font-size: 11px; + color: var(--ol-ink-4); + background: var(--ol-sidebar-version-bg); + border: 1px solid var(--ol-sidebar-version-border); + border-radius: var(--ol-pill-radius); + box-shadow: none; +} + +.ol-aura-beta-tag { + display: inline-block; + padding: 2px 8px; + font-size: 10px; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--ol-blue); + background: rgba(37,99,235,0.10); + border-radius: var(--ol-pill-radius); +} + +.ol-aura-sidebar-settings { + padding: 10px 12px; + border-radius: var(--ol-r-lg); + border: 1px solid var(--ol-sidebar-settings-border); + background: var(--ol-sidebar-settings-bg); + box-shadow: none; +} + +.ol-aura-sidebar-settings.ol-nav-btn-active { + background: var(--ol-sidebar-settings-active-bg); + box-shadow: none; +} + +.ol-aura-console-main { + border-radius: var(--ol-panel-radius); +} + +[data-ol-mobile="true"] .ol-aura-console-main { + border-radius: 0; +} + +.ol-aura-mobile-topbar { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: calc(10px + env(safe-area-inset-top, 0px)) 14px 10px; + border-bottom: 1px solid var(--ol-sidebar-border); + background: var(--ol-sidebar-bg); +} + +.ol-aura-mobile-brand { + min-width: 0; + display: flex; + align-items: center; + gap: 10px; +} + +.ol-aura-mobile-brand-mark { + width: 30px; + height: 30px; + border-radius: var(--ol-control-radius); + flex-shrink: 0; + box-sizing: border-box; + padding: 3px; + object-fit: contain; +} + +.ol-aura-mobile-brand-title { + font-size: 14px; + font-weight: 700; + color: var(--ol-ink); + line-height: 1.15; +} + +.ol-aura-mobile-brand-section { + margin-top: 2px; + font-size: 11px; + color: var(--ol-ink-4); + font-family: var(--ol-font-mono); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.ol-aura-mobile-settings { + width: 36px; + height: 36px; + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: var(--ol-r-lg); + color: var(--ol-ink-3); + background: var(--ol-sidebar-settings-bg); + border: 1px solid var(--ol-sidebar-settings-border); +} + +.ol-aura-mobile-settings-active { + color: var(--ol-ink); + background: var(--ol-sidebar-settings-active-bg); +} + +.ol-aura-mobile-nav { + flex-shrink: 0; + display: grid; + grid-template-columns: repeat(var(--ol-nav-count, 6), minmax(0, 1fr)); + gap: 2px; + padding: 7px 8px calc(7px + env(safe-area-inset-bottom, 0px)); + border-top: 1px solid var(--ol-sidebar-border); + background: var(--ol-sidebar-bg); +} + +.ol-aura-mobile-nav-btn { + min-width: 0; + height: 50px; + display: inline-flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 4px; + border-radius: var(--ol-r-lg); + color: var(--ol-ink-4); + font-size: 10px; + font-weight: 600; + line-height: 1.1; +} + +.ol-aura-mobile-nav-btn span { + max-width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.ol-aura-mobile-nav-btn-active { + color: var(--ol-ink); + background: var(--ol-sidebar-pill-bg); + border: 1px solid var(--ol-sidebar-pill-border); +} + +/* ── Settings modal ────────────────────────────────────────────────────── */ + +.ol-aura-settings { + background: var(--ol-panel-bg); + border-radius: var(--ol-shell-radius); + border: 1px solid var(--ol-panel-border); + box-shadow: var(--ol-panel-shadow); +} + +.ol-aura-settings[data-ol-mobile="true"] { + border-radius: 0; +} + +.ol-aura-settings-rail { + padding: 20px 14px; + gap: 16px; + background: var(--ol-settings-rail-bg); + border-right: 1px solid var(--ol-settings-rail-border); + border-bottom: 0; +} + +.ol-aura-settings[data-ol-mobile="true"] .ol-aura-settings-rail { + padding: calc(10px + env(safe-area-inset-top, 0px)) 10px 8px; + gap: 8px; + border-right: 0; + border-bottom: 1px solid var(--ol-settings-rail-border); +} + +.ol-aura-settings-pill { + background: var(--ol-sidebar-pill-bg); + border-radius: var(--ol-r-lg); + border: 1px solid var(--ol-sidebar-pill-border); + box-shadow: none; +} + +.ol-aura-settings-nav-btn { + padding: 7px 10px; + border-radius: var(--ol-r-lg); + border: 0; + background: transparent; + font-family: inherit; + font-size: 13px; + cursor: default; + text-align: left; + position: relative; + z-index: 1; + transition: color 0.16s var(--ol-motion-quick), background 0.16s var(--ol-motion-quick); +} + +.ol-aura-settings[data-ol-mobile="true"] .ol-aura-settings-nav-btn { + padding: 8px 11px; +} + +.ol-aura-settings-nav-btn.ol-nav-btn-active { + background: transparent; + border: 0; +} + +.ol-aura-settings[data-ol-mobile="true"] .ol-aura-settings-nav-btn.ol-nav-btn-active { + background: var(--ol-sidebar-pill-bg); + border: 1px solid var(--ol-sidebar-pill-border); +} + +.ol-aura-settings-links { + display: flex; + flex-direction: column; + gap: 1px; + padding-top: 10px; + padding-left: 0; + border-top: 1px solid var(--ol-settings-links-border); + border-left: 0; + overflow-x: visible; +} + +.ol-aura-settings[data-ol-mobile="true"] .ol-aura-settings-links { + flex-direction: row; + padding-top: 0; + padding-left: 8px; + border-top: 0; + border-left: 1px solid var(--ol-settings-links-border); + overflow-x: auto; +} + +.ol-aura-settings-content { + background: var(--ol-settings-content-bg); +} + +.ol-aura-settings-close { + background: var(--ol-settings-close-bg); + color: var(--ol-ink-3); + transition: background 0.16s var(--ol-motion-quick); +} + +.ol-aura-settings-close:hover { + background: var(--ol-settings-close-hover-bg); +} + +.ol-aura-settings-title { + padding: 24px 28px 10px; + font-size: 22px; + font-weight: 600; + letter-spacing: -0.02em; + font-family: var(--ol-font-display); + color: var(--ol-ink); +} + +.ol-aura-settings[data-ol-mobile="true"] .ol-aura-settings-title { + padding: 16px 48px 8px 16px; + font-size: 20px; +} + +/* ── Page switch + prompt keyframes ──────────────────────────────────── */ + +@keyframes ol-page-slide { + from { opacity: 0; transform: translate3d(10px, 0, 0) scale(.996); filter: blur(6px); } + to { opacity: 1; transform: translate3d(0, 0, 0) scale(1); filter: blur(0); } +} + +@keyframes ol-page-fadeout { + from { opacity: 1; filter: blur(0); } + to { opacity: 0; filter: blur(8px); } +} + +@keyframes ol-prompt-fade { + from { opacity: 0; backdrop-filter: blur(0); -webkit-backdrop-filter: blur(0); } + to { opacity: 1; backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); } +} + +@keyframes ol-prompt-pop { + from { opacity: 0; transform: translateY(6px) scale(.97); filter: blur(6px); } + to { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); } +} diff --git a/openless-all/app/src/styles/tokens.css b/openless-all/app/src/styles/tokens.css index a905b340..a19679e6 100755 --- a/openless-all/app/src/styles/tokens.css +++ b/openless-all/app/src/styles/tokens.css @@ -25,8 +25,11 @@ --ol-ink: #0a0a0b; --ol-ink-2: #2a2a2d; --ol-ink-3: rgba(10, 10, 11, 0.62); - --ol-ink-4: rgba(10, 10, 11, 0.42); + --ol-ink-4: rgba(10, 10, 11, 0.58); --ol-ink-5: rgba(10, 10, 11, 0.24); + --ol-ink-muted: var(--ol-ink-3); + --ol-ink-subtle: var(--ol-ink-4); + --ol-placeholder: var(--ol-ink-4); /* Blue accent */ --ol-blue: #2563eb; @@ -34,6 +37,11 @@ --ol-blue-soft: #eff4ff; --ol-blue-ring: rgba(37, 99, 235, 0.22); --ol-on-accent: #ffffff; + --ol-accent-solid-bg: var(--ol-blue); + --ol-accent-solid-bg-hover: var(--ol-blue-hover); + --ol-accent-solid-ink: var(--ol-on-accent); + --ol-primary-solid-bg: #111827; + --ol-primary-solid-ink: var(--ol-on-accent); /* Shared glass */ --ol-glass-bg: rgba(255, 255, 255, 0.62); @@ -73,6 +81,9 @@ --ol-r-xl: 18px; --ol-r-2xl: var(--ol-card-radius); --ol-r-pill: var(--ol-pill-radius); + --ol-control-radius: 8px; + --ol-bubble-radius: var(--ol-r-lg); + --ol-modal-radius: var(--ol-r-xl); /* Typography */ --ol-font-display: "Aptos", "Segoe UI Variable Display", "PingFang SC", "Microsoft YaHei", sans-serif; @@ -85,6 +96,8 @@ --ol-warn: #d97706; --ol-warn-soft: #fff7ed; --ol-err: #dc2626; + --ol-danger-solid-bg: var(--ol-err); + --ol-danger-solid-ink: #ffffff; /* Theme-aware shell and control tokens */ --ol-app-shell-bg: @@ -102,8 +115,8 @@ --ol-panel-bg: var(--ol-aura-surface); --ol-panel-border: var(--ol-aura-glass-border); --ol-panel-shadow: var(--ol-aura-shadow); - --ol-card-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(252, 252, 253, 0.82)); - --ol-card-border: rgba(255, 255, 255, 0.74); + --ol-card-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.94), rgba(250, 250, 251, 0.86)); + --ol-card-border: rgba(255, 255, 255, 0.78); --ol-card-shadow: var(--ol-aura-shadow-soft); --ol-sidebar-bg: linear-gradient(180deg, rgba(255,255,255,0.34), rgba(255,255,255,0.12)); @@ -147,8 +160,8 @@ --ol-pill-selected-ink: #ffffff; --ol-pill-selected-border: #111827; --ol-segmented-bg: rgba(0,0,0,0.04); - --ol-segmented-active-bg: #ffffff; - --ol-segmented-active-shadow: 0 1px 2px rgba(0,0,0,.06), 0 0 0 0.5px rgba(0,0,0,.06); + --ol-segmented-active-bg: var(--ol-control-elevated); + --ol-segmented-active-shadow: 0 1px 3px rgba(0,0,0,.08), 0 0 0 0.5px rgba(0,0,0,.08); --ol-toggle-off-bg: rgba(0,0,0,0.15); --ol-toggle-knob: #ffffff; --ol-overlay-bg: rgba(15,17,22,0.32); @@ -205,14 +218,22 @@ --ol-ink: #f4f7fb; --ol-ink-2: #d8dfeb; --ol-ink-3: rgba(244, 247, 251, 0.74); - --ol-ink-4: rgba(244, 247, 251, 0.50); + --ol-ink-4: rgba(244, 247, 251, 0.54); --ol-ink-5: rgba(244, 247, 251, 0.26); + --ol-ink-muted: var(--ol-ink-3); + --ol-ink-subtle: var(--ol-ink-4); + --ol-placeholder: var(--ol-ink-4); --ol-blue: #60a5fa; --ol-blue-hover: #3b82f6; --ol-blue-soft: rgba(96, 165, 250, 0.14); --ol-blue-ring: rgba(96, 165, 250, 0.30); --ol-on-accent: #f8fbff; + --ol-accent-solid-bg: #2563eb; + --ol-accent-solid-bg-hover: #1d4ed8; + --ol-accent-solid-ink: #ffffff; + --ol-primary-solid-bg: var(--ol-accent-solid-bg); + --ol-primary-solid-ink: var(--ol-accent-solid-ink); --ol-glass-bg: rgba(15, 19, 27, 0.68); --ol-glass-bg-strong: rgba(17, 22, 30, 0.84); @@ -231,6 +252,8 @@ --ol-warn: #f59e0b; --ol-warn-soft: rgba(245, 158, 11, 0.14); --ol-err: #f87171; + --ol-danger-solid-bg: #b91c1c; + --ol-danger-solid-ink: #ffffff; --ol-app-shell-bg: linear-gradient(180deg, #050607 0%, #090a0c 100%); --ol-window-bg: @@ -243,9 +266,9 @@ --ol-panel-bg: linear-gradient(180deg, rgba(33,33,36,0.96), rgba(28,28,31,0.94)); --ol-panel-border: rgba(255,255,255,0.07); --ol-panel-shadow: 0 18px 44px -34px rgba(0,0,0,0.62); - --ol-card-bg: linear-gradient(180deg, rgba(34,34,37,0.96), rgba(29,29,32,0.94)); - --ol-card-border: rgba(255,255,255,0.06); - --ol-card-shadow: none; + --ol-card-bg: linear-gradient(180deg, rgba(36,36,40,0.98), rgba(30,30,34,0.96)); + --ol-card-border: rgba(255,255,255,0.08); + --ol-card-shadow: 0 12px 32px -24px rgba(0,0,0,0.55); --ol-sidebar-bg: transparent; --ol-sidebar-border: rgba(255,255,255,0.06); @@ -288,8 +311,8 @@ --ol-pill-selected-ink: #f4f7fb; --ol-pill-selected-border: rgba(96,165,250,0.40); --ol-segmented-bg: rgba(255,255,255,0.06); - --ol-segmented-active-bg: rgba(255,255,255,0.12); - --ol-segmented-active-shadow: none; + --ol-segmented-active-bg: var(--ol-control-elevated); + --ol-segmented-active-shadow: 0 0 0 0.5px rgba(255,255,255,0.10); --ol-toggle-off-bg: rgba(255,255,255,0.16); --ol-toggle-knob: #f8fbff; --ol-overlay-bg: rgba(4, 8, 14, 0.58);