diff --git a/openless-all/app/index.html b/openless-all/app/index.html index 02569c7f..146eb78d 100644 --- a/openless-all/app/index.html +++ b/openless-all/app/index.html @@ -3,6 +3,7 @@ + OpenLess diff --git a/openless-all/app/scripts/aura-skin-contract.test.mjs b/openless-all/app/scripts/aura-skin-contract.test.mjs index 72104e37..ddd60047 100644 --- a/openless-all/app/scripts/aura-skin-contract.test.mjs +++ b/openless-all/app/scripts/aura-skin-contract.test.mjs @@ -3,12 +3,6 @@ import { fileURLToPath } from 'node:url'; import path from 'node:path'; import assert from 'node:assert/strict'; -// AURA 界面大改已回退到 beta-3 外观(commit 5671805),随之删除了 themeMode.ts / -// ThemeSection、solid-button token(--ol-*-solid-*)与 .ol-aura-* / .ol-app-shell-bg -// 等 AURA 专属皮肤类。本契约相应精简:只保留回退后仍存在、且仍在守护的护栏 —— -// WCAG 对比度、Style 页主题 token,以及「禁止硬编码白底 / 坏配色组合 / 引号包裹的 -// CSS 变量」的全量源码扫描(commit 760f662 修复暗色白底就依赖这条)。 - const root = new URL('../', import.meta.url); async function read(relPath) { @@ -22,7 +16,7 @@ function escapeRegExp(value) { function extractCssBlock(css, selector) { const escapedSelector = escapeRegExp(selector); const match = css.match(new RegExp(`${escapedSelector}\\s*\\{([\\s\\S]*?)\\n\\}`)); - assert.ok(match, `${selector} block must exist`); + assert.ok(match, `tokens.css must contain ${selector} block`); return match[1]; } @@ -123,6 +117,17 @@ function contrastRatioOverBackground(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); @@ -134,6 +139,17 @@ function assertMutedContrast(tokens, label, bgToken, inkToken, minRatio = 4.5) { return ratio; } +function assertUsesClassName(source, className, message) { + const escapedClassName = escapeRegExp(className); + const patterns = [ + new RegExp(`className\\s*=\\s*(?:\\{\\s*)?"(?:[^"]*\\s)?${escapedClassName}(?:\\s[^"]*)?"(?:\\s*\\})?`), + new RegExp(`className\\s*=\\s*(?:\\{\\s*)?'(?:[^']*\\s)?${escapedClassName}(?:\\s[^']*)?'(?:\\s*\\})?`), + new RegExp(`className\\s*=\\s*(?:\\{\\s*)?\`(?:[^\`]*\\s)?${escapedClassName}(?:\\s[^\`]*)?\`(?:\\s*\\})?`), + ]; + + assert.ok(patterns.some((pattern) => pattern.test(source)), message); +} + const srcRoot = fileURLToPath(new URL('src/', root)); async function walkSourceFiles(dirPath, files = []) { @@ -151,56 +167,267 @@ async function walkSourceFiles(dirPath, files = []) { return files; } -const [tokens, globalCss, stylePage, sourceFiles, remoteStyle] = await Promise.all([ +assert.throws( + () => assertUsesClassName('
ol-app-shell-bg
', 'ol-app-shell-bg', 'sample must require className usage'), + /sample must require className usage/, +); +assert.throws( + () => + assertUsesClassName( + '
', + 'ol-app-shell-bg', + 'sample must require an exact class token', + ), + /sample must require an exact class token/, +); +assertUsesClassName( + '
', + 'ol-app-shell-bg', + 'sample should accept className usage', +); + +const [tokens, globalCss, shell, settingsModal, overview, settingsTabs, themeMode, stylePage, sourceFiles, remoteStyle, selectLite, translationPage] = 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'), + read('src/components/ui/SelectLite.tsx'), + read('src/pages/Translation.tsx'), ]); -// 回退后仍保留的设计 token。 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); +} -// 弱化文字(ink-4)在浅色 surface 上仍要满足 WCAG AA。 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' }, + { + 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' }, + { + 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); } -// 回退后保持静态磨砂:不得引入动画 halo。 -// 注:原 “.ol-frost 不得硬编码白底” 是 dark-mode 护栏,但 AURA 回退删掉了主题切换 -// (themeMode.ts),当前应用是 light-only、dark token 休眠,beta-3 的白色磨砂是预期外观, -// 故该护栏暂时移除;若日后恢复 dark-mode 切换,应连同 --ol-frost-bg token 一起加回。 +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(tokens, /--ol-select-trigger-bg:/, 'tokens.css must define --ol-select-trigger-bg'); +assert.match(tokens, /--ol-select-popover-bg:/, 'tokens.css must define --ol-select-popover-bg'); +assert.match( + tokens, + /\[data-ol-theme='dark'\][\s\S]*--ol-select-popover-bg:/, + 'tokens.css must define --ol-select-popover-bg in dark theme', +); + +assert.match(selectLite, /--ol-select-trigger-bg/, 'SelectLite must use --ol-select-trigger-bg'); +assert.match(selectLite, /--ol-select-popover-bg/, 'SelectLite must use --ol-select-popover-bg'); +assert.match(selectLite, /--ol-select-option-hover-bg/, 'SelectLite must use --ol-select-option-hover-bg'); +assert.doesNotMatch( + selectLite, + /rgba\(\s*252\s*,/, + 'SelectLite must not hardcode light popover rgba backgrounds', +); +assert.doesNotMatch( + translationPage, + /background:\s*'#fff'/, + 'Translation.tsx must not override SelectLite with hardcoded #fff background', +); + +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'); +assertUsesClassName(shell, 'ol-aura-panel', 'FloatingShell must expose an Aura panel hook'); + +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', +); -// Style 页必须走主题 token、不得硬编码浅色卡片底色(回退后暗色白底的根源,commit 760f662)。 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, @@ -208,18 +435,30 @@ for (const pattern of forbiddenStyleCardLightBackgrounds) { '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'); -// 全量源码扫描:注入 CSS 字符串里禁止用引号包裹的 var();禁止把 --ol-ink 当实心底色; -// 禁止 --ol-blue / --ol-err 与 --ol-on-accent / --ol-accent-solid-ink 的低对比组合。 +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 forbiddenInlineInkBackground = + /background:\s*'var\(--ol-ink\)'|background:\s*[^,\n;{]+?\?\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\)/; @@ -255,7 +494,13 @@ for (const relPath of sourceFiles) { } } -console.log('Aura skin contract (trimmed post-revert) OK'); +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( 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 3c11db75..3b3ab4fd 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 @@ -55,6 +55,26 @@ --safe-bottom: env(safe-area-inset-bottom, 0px); } +[data-ol-theme='dark'] { + --bg: #0b0e13; + --surface: #141922; + --surface-2: #1a202b; + --line: rgba(255, 255, 255, 0.09); + --line-strong: rgba(255, 255, 255, 0.16); + --ink: #f4f7fb; + --ink-2: #d8dfeb; + --ink-3: rgba(244, 247, 251, 0.74); + --ink-4: rgba(244, 247, 251, 0.58); + --blue: #60a5fa; + --blue-hover: #3b82f6; + --blue-soft: rgba(96, 165, 250, 0.14); + --blue-ring: rgba(96, 165, 250, 0.30); + --on-accent: #f8fbff; + --accent-solid-bg: #2563eb; + --accent-solid-bg-hover: #3b82f6; + --accent-solid-ink: #f8fbff; +} + * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; diff --git a/openless-all/app/src-tauri/src/types.rs b/openless-all/app/src-tauri/src/types.rs index 847bcef5..7bb50435 100644 --- a/openless-all/app/src-tauri/src/types.rs +++ b/openless-all/app/src-tauri/src/types.rs @@ -92,6 +92,15 @@ pub enum UpdateChannel { Beta, } +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(rename_all = "camelCase")] +pub enum ThemeMode { + #[default] + System, + Light, + Dark, +} + #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub enum InsertStatus { @@ -721,6 +730,9 @@ pub struct UserPreferences { /// 用户改用托盘菜单访问主窗口。默认 false 跟历史行为一致。 #[serde(default)] pub start_minimized: bool, + /// UI theme: follow OS, force light, or force dark. Frontend applies via data-ol-theme. + #[serde(default)] + pub theme_mode: ThemeMode, /// 流式输入:润色 SSE 一边到达一边逐字模拟键盘事件输出到当前焦点。开启后用户感知到 /// 的处理时延显著降低(润色 LLM 第一个 token 即开始落字)。 /// @@ -942,6 +954,8 @@ struct UserPreferencesWire { polish_context_window_minutes: u32, #[serde(default)] start_minimized: bool, + #[serde(default)] + theme_mode: ThemeMode, #[serde(default = "default_true")] streaming_insert: bool, #[serde(default)] @@ -1034,6 +1048,7 @@ impl Default for UserPreferencesWire { history_retention_days: prefs.history_retention_days, polish_context_window_minutes: prefs.polish_context_window_minutes, start_minimized: prefs.start_minimized, + theme_mode: prefs.theme_mode, streaming_insert: prefs.streaming_insert, streaming_insert_default_migrated: prefs.streaming_insert_default_migrated, streaming_insert_save_clipboard: prefs.streaming_insert_save_clipboard, @@ -1140,6 +1155,7 @@ impl<'de> Deserialize<'de> for UserPreferences { history_retention_days: wire.history_retention_days, polish_context_window_minutes: wire.polish_context_window_minutes, start_minimized: wire.start_minimized, + theme_mode: wire.theme_mode, streaming_insert, streaming_insert_default_migrated: true, streaming_insert_save_clipboard: wire.streaming_insert_save_clipboard, @@ -1874,6 +1890,7 @@ impl Default for UserPreferences { history_retention_days: default_history_retention_days(), polish_context_window_minutes: default_polish_context_window_minutes(), start_minimized: false, + theme_mode: ThemeMode::default(), streaming_insert: true, streaming_insert_default_migrated: true, streaming_insert_save_clipboard: true, diff --git a/openless-all/app/src/components/Capsule.tsx b/openless-all/app/src/components/Capsule.tsx index f47bcb5d..9494f8a8 100644 --- a/openless-all/app/src/components/Capsule.tsx +++ b/openless-all/app/src/components/Capsule.tsx @@ -97,7 +97,7 @@ interface CenterTextProps { color?: string; } -function CenterText({ os, kind, text, color = 'var(--ol-ink-3)' }: CenterTextProps) { +function CenterText({ os, kind, text, color = 'var(--ol-capsule-center-ink)' }: CenterTextProps) { const metrics = getCapsulePillMetrics(os); const layout = getCapsuleMessageLayout(os, kind); return ( @@ -133,8 +133,6 @@ interface CircleButtonProps { function CircleButton({ variant, enabled, onClick }: CircleButtonProps) { const { t } = useTranslation(); const isCancel = variant === 'cancel'; - // confirm 是主操作锚点,纯白;cancel 半透 + 自带 backdrop blur 跟 pill 拉开层级。 - const useBackdrop = isCancel; return (
-
+
0 ? t('overview.metricAvgTrend') : t('overview.metricNoData')} /> @@ -331,7 +331,7 @@ function WeekChart({ data }: { data: number[] }) { height: `${(v / max) * 80}px`, minHeight: 2, borderRadius: 4, - background: isToday ? 'var(--ol-blue)' : 'var(--ol-ink)', + background: isToday ? 'var(--ol-blue)' : 'var(--ol-ink-4)', opacity: v === 0 ? 0.15 : isToday ? 1 : 0.85, transition: 'height 0.18s var(--ol-motion-soft), opacity 0.18s var(--ol-motion-soft)', }} diff --git a/openless-all/app/src/pages/Style.tsx b/openless-all/app/src/pages/Style.tsx index fd6ebe01..951bb767 100644 --- a/openless-all/app/src/pages/Style.tsx +++ b/openless-all/app/src/pages/Style.tsx @@ -620,12 +620,10 @@ export function Style() { textAlign: 'left', position: 'relative', border: '0.5px solid', - borderColor: pack.active ? 'var(--ol-blue)' : 'var(--ol-line)', + borderColor: pack.active ? 'var(--ol-style-card-border-active)' : 'var(--ol-style-card-border)', 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))', + ? 'var(--ol-style-card-bg-active)' + : 'var(--ol-style-card-bg)', borderRadius: 18, padding: 16, boxShadow: pack.active ? '0 0 0 3px var(--ol-blue-ring)' : 'none', @@ -636,7 +634,7 @@ export function Style() {
-
+
{pack.name}
@@ -997,7 +995,7 @@ export function Style() { @@ -1065,7 +1063,7 @@ export function Style() { style={{ padding: 14, borderRadius: 14, - background: 'linear-gradient(180deg, rgba(248,250,252,0.98), rgba(241,245,249,0.95))', + background: 'var(--ol-style-subtle-bg)', border: '0.5px solid var(--ol-line)', }} > @@ -1102,7 +1100,7 @@ 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-editor-bg)', }} >
@@ -1137,7 +1135,7 @@ export function Style() { style={{ borderRadius: 14, border: '0.5px solid rgba(148,163,184,0.22)', - background: 'rgba(248,250,252,0.9)', + background: 'var(--ol-style-subtle-bg)', padding: 14, }} > @@ -1155,7 +1153,7 @@ export function Style() { style={{ borderRadius: 14, border: '0.5px solid rgba(37,99,235,0.16)', - background: 'rgba(239,246,255,0.86)', + background: 'var(--ol-style-subtle-bg)', padding: 14, }} > @@ -1189,7 +1187,7 @@ function MetaItem({ label, value }: { label: string; value: string }) { style={{ borderRadius: 12, border: '0.5px solid rgba(148,163,184,0.2)', - background: 'rgba(255,255,255,0.92)', + background: 'var(--ol-style-input-bg)', padding: '10px 12px', }} > @@ -1226,7 +1224,7 @@ function DirectiveRow({ padding: '10px 12px', borderRadius: 12, border: '0.5px solid rgba(148,163,184,0.2)', - background: 'rgba(255,255,255,0.92)', + background: 'var(--ol-style-input-bg)', }} >
diff --git a/openless-all/app/src/pages/Translation.tsx b/openless-all/app/src/pages/Translation.tsx index f54c563c..cb54afa7 100644 --- a/openless-all/app/src/pages/Translation.tsx +++ b/openless-all/app/src/pages/Translation.tsx @@ -228,7 +228,7 @@ export function Translation() { options={targetOptions} placeholder={t('translation.target.disabled')} ariaLabel={t('translation.target.title')} - style={{ width: '100%', maxWidth: 360, fontSize: 13, background: '#fff' }} + style={{ width: '100%', maxWidth: 360, fontSize: 13 }} /> diff --git a/openless-all/app/src/pages/_atoms.tsx b/openless-all/app/src/pages/_atoms.tsx index d98ce7cb..89644c6b 100644 --- a/openless-all/app/src/pages/_atoms.tsx +++ b/openless-all/app/src/pages/_atoms.tsx @@ -53,7 +53,7 @@ export function Card({ children, style, padding = 18, glassy = false, className
= { - default: { bg: 'rgba(0,0,0,0.05)', color: 'var(--ol-ink-2)', bd: 'transparent' }, - blue: { bg: 'var(--ol-blue-soft)',color: 'var(--ol-blue)', bd: 'transparent' }, - ok: { bg: 'var(--ol-ok-soft)', color: 'var(--ol-ok)', bd: 'transparent' }, - outline: { bg: 'transparent', color: 'var(--ol-ink-3)', bd: 'var(--ol-line-strong)' }, - dark: { bg: 'var(--ol-ink)', color: '#fff', bd: 'transparent' }, + default: { bg: 'var(--ol-pill-bg)', color: 'var(--ol-ink-2)', bd: 'transparent' }, + blue: { bg: 'var(--ol-pill-blue-bg)', color: 'var(--ol-blue)', bd: 'transparent' }, + ok: { bg: 'var(--ol-pill-ok-bg)', color: 'var(--ol-ok)', bd: 'transparent' }, + outline: { bg: 'transparent', color: 'var(--ol-ink-3)', bd: 'var(--ol-line-strong)' }, + dark: { bg: 'var(--ol-pill-selected-bg)', color: 'var(--ol-pill-selected-ink)', bd: 'transparent' }, }; const t = tones[tone]; const sz = size === 'sm' @@ -125,10 +125,10 @@ interface BtnProps { export function Btn({ children, variant = 'ghost', size = 'md', icon, style, onClick, disabled = false }: BtnProps) { const variants: Record = { - primary: { bg: 'var(--ol-ink)', color: '#fff', bd: 'transparent', sh: '0 1px 2px rgba(0,0,0,.08)' }, - blue: { bg: 'var(--ol-blue)', color: '#fff', bd: 'transparent', sh: '0 1px 2px rgba(37,99,235,.18)' }, - ghost: { bg: 'transparent', color: 'var(--ol-ink-2)', bd: 'var(--ol-line-strong)', sh: 'none' }, - soft: { bg: 'rgba(0,0,0,0.04)', color: 'var(--ol-ink-2)', bd: 'transparent', sh: 'none' }, + primary: { bg: 'var(--ol-primary-solid-bg)', color: 'var(--ol-primary-solid-ink)', bd: 'transparent', sh: 'var(--ol-shadow-sm)' }, + blue: { bg: 'var(--ol-accent-solid-bg)', color: 'var(--ol-accent-solid-ink)', bd: 'transparent', sh: 'var(--ol-shadow-sm)' }, + ghost: { bg: 'transparent', color: 'var(--ol-ink-2)', bd: 'var(--ol-line-strong)', sh: 'none' }, + soft: { bg: 'var(--ol-control-muted)', color: 'var(--ol-ink-2)', bd: 'transparent', sh: 'none' }, }; const v = variants[variant]; const sizes: Record = { diff --git a/openless-all/app/src/pages/settings/AboutSection.tsx b/openless-all/app/src/pages/settings/AboutSection.tsx index 5d9b4cb3..5915a7e6 100644 --- a/openless-all/app/src/pages/settings/AboutSection.tsx +++ b/openless-all/app/src/pages/settings/AboutSection.tsx @@ -3,7 +3,7 @@ // 「加入 Beta 渠道」已挪到「高级」页底部(见 BetaChannelSection),这里图标旁 // 只保留查正式版的「检查更新」按钮。 -import { useEffect, useRef, useState, type CSSProperties } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Icon } from '../../components/Icon'; import { Row } from '../../components/ui/Row'; @@ -11,7 +11,7 @@ import { getPlatformCapabilities, openExternal } from '../../lib/ipc'; import type { PlatformCapabilities } from '../../lib/types'; import { APP_VERSION_LABEL } from '../../lib/appVersion'; import { Card } from '../_atoms'; -import { SectionTitle } from './shared'; +import { btnGhostStyle, SectionTitle } from './shared'; import { CheckUpdateButton } from './CheckUpdateButton'; const HELP_URL = 'https://github.com/appergb/openless#readme'; @@ -67,27 +67,27 @@ export function AboutSection() { {t('settings.about.linksTitle')} - - - - - @@ -100,7 +100,7 @@ export function AboutSection() { boxShadow: '0 1px 0 rgba(0,0,0,0.04)', color: 'var(--ol-ink-2)', }}>1078960553 - {qqCopied && {t('common.copied')}} @@ -111,11 +111,3 @@ export function AboutSection() { ); } -const btnGhost: CSSProperties = { - padding: '5px 10px', fontSize: 12, borderRadius: 6, - border: '0.5px solid var(--ol-line-strong)', - background: '#fff', color: 'var(--ol-ink-2)', - cursor: 'default', fontFamily: 'inherit', - maxWidth: '100%', - transition: 'background 0.16s var(--ol-motion-quick), border-color 0.16s var(--ol-motion-quick)', -}; diff --git a/openless-all/app/src/pages/settings/CheckUpdateButton.tsx b/openless-all/app/src/pages/settings/CheckUpdateButton.tsx index 23ab8ec5..19106fd2 100644 --- a/openless-all/app/src/pages/settings/CheckUpdateButton.tsx +++ b/openless-all/app/src/pages/settings/CheckUpdateButton.tsx @@ -5,7 +5,8 @@ // 呈现 2.5s 后自动回到 idle,绝不另起文字块、不改变所在卡片高度 —— 杜绝 // 「渲染框突然变大 / 抽搐」。发现新版则弹出固定定位的 UpdateDialog。 -import { useEffect, type CSSProperties } from 'react'; +import { useEffect } from 'react'; +import { btnGhostStyle } from './shared'; import { useTranslation } from 'react-i18next'; import { Icon } from '../../components/Icon'; import { isDialogStatus, UpdateDialog, useAutoUpdate } from '../../components/AutoUpdate'; @@ -45,7 +46,7 @@ export function CheckUpdateButton({ channel }: { channel: UpdateChannel }) { ? t('settings.about.upToDate') : undefined } - style={{ ...checkBtnStyle, color, opacity: checking || busy ? 0.7 : 1 }} + style={{ ...btnGhostStyle, color, opacity: checking || busy ? 0.7 : 1, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 6, minWidth: 84 }} > -
+
{choices.map(([v, l]) => (