From ad8eeca6e28f5bf6f6c6be46a012b622e08663c1 Mon Sep 17 00:00:00 2001 From: HKLHaoBin Date: Sun, 14 Jun 2026 00:41:27 +0800 Subject: [PATCH 1/3] Add dark mode with CSS tokens, settings UI, and desktop capsule styling. Wire themeMode runtime with localStorage boot cache and Rust theme_mode prefs sync, complete Aura token contract, and keep capsule pill/buttons dark with light icons across app themes. Co-authored-by: Cursor --- openless-all/app/index.html | 2 +- .../src/remote_server/assets/style.css | 20 +++ openless-all/app/src-tauri/src/types.rs | 17 +++ openless-all/app/src/components/Capsule.tsx | 22 ++-- .../app/src/components/FloatingShell.tsx | 24 ++-- .../app/src/components/Onboarding.tsx | 8 +- .../app/src/components/SettingsModal.tsx | 15 +-- .../app/src/components/WindowChrome.tsx | 12 +- openless-all/app/src/i18n/en.ts | 7 ++ openless-all/app/src/i18n/ja.ts | 7 ++ openless-all/app/src/i18n/ko.ts | 7 ++ openless-all/app/src/i18n/zh-CN.ts | 7 ++ openless-all/app/src/i18n/zh-TW.ts | 7 ++ openless-all/app/src/lib/ipc.ts | 1 + openless-all/app/src/lib/platform.ts | 17 +++ openless-all/app/src/lib/stylePrefs.test.ts | 1 + openless-all/app/src/lib/themeMode.ts | 114 ++++++++++++++++++ openless-all/app/src/lib/types.ts | 4 + openless-all/app/src/main.tsx | 2 + openless-all/app/src/pages/Overview.tsx | 2 +- openless-all/app/src/pages/Style.tsx | 24 ++-- openless-all/app/src/pages/_atoms.tsx | 20 +-- .../src/pages/settings/MarketplaceSection.tsx | 4 +- .../app/src/pages/settings/ThemeSection.tsx | 58 +++++++++ openless-all/app/src/pages/settings/tabs.tsx | 5 +- .../app/src/state/HotkeySettingsContext.tsx | 3 + openless-all/app/src/styles/global.css | 66 ++++++++-- openless-all/app/src/styles/tokens.css | 64 +++++++++- 28 files changed, 456 insertions(+), 84 deletions(-) create mode 100644 openless-all/app/src/lib/themeMode.ts create mode 100644 openless-all/app/src/pages/settings/ThemeSection.tsx 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/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..93070b83 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]) => (