diff --git a/openless-all/app/index.html b/openless-all/app/index.html index 02569c7f..76241550 100644 --- a/openless-all/app/index.html +++ b/openless-all/app/index.html @@ -2,7 +2,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 5228a23e..ddd60047 100644 --- a/openless-all/app/scripts/aura-skin-contract.test.mjs +++ b/openless-all/app/scripts/aura-skin-contract.test.mjs @@ -186,7 +186,7 @@ assertUsesClassName( 'sample should accept className usage', ); -const [tokens, globalCss, shell, settingsModal, overview, settingsTabs, themeMode, stylePage, sourceFiles, remoteStyle] = await Promise.all([ +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'), @@ -197,6 +197,8 @@ const [tokens, globalCss, shell, settingsModal, overview, settingsTabs, themeMod 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'), ]); assert.match(tokens, /--ol-shell-radius:/, 'tokens.css must define --ol-shell-radius'); @@ -363,6 +365,28 @@ assert.match( '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'); @@ -433,7 +457,8 @@ const illegalCssStringPatterns = [ /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\)/; 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 8794081e..a8c4fb31 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 bb0bd822..be84665a 100644 --- a/openless-all/app/src/components/Capsule.tsx +++ b/openless-all/app/src/components/Capsule.tsx @@ -63,7 +63,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 ( @@ -99,8 +99,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 ( - - - - @@ -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]) => (