diff --git a/openless-all/app/scripts/windows-ui-config.test.mjs b/openless-all/app/scripts/windows-ui-config.test.mjs index 633a7583..873afdbb 100644 --- a/openless-all/app/scripts/windows-ui-config.test.mjs +++ b/openless-all/app/scripts/windows-ui-config.test.mjs @@ -22,6 +22,7 @@ const capsuleTsx = await readFile(new URL('../src/components/Capsule.tsx', impor const capsuleLayoutTs = await readFile(new URL('../src/lib/capsuleLayout.ts', import.meta.url), 'utf-8'); const windowChromeTsx = await readFile(new URL('../src/components/WindowChrome.tsx', import.meta.url), 'utf-8'); const floatingShellTsx = await readFile(new URL('../src/components/FloatingShell.tsx', import.meta.url), 'utf-8'); +const tokensCss = await readFile(new URL('../src/styles/tokens.css', import.meta.url), 'utf-8'); if (!capsuleWindow) { throw new Error('capsule window config missing'); @@ -33,13 +34,47 @@ assertEqual(capsuleWindow.width, 220, 'windows capsule config keeps translation- assertEqual(capsuleWindow.height, 110, 'windows capsule config keeps translation-capable height baseline'); assertEqual(capsuleWindow.transparent, true, 'capsule window should keep transparent visuals'); assertEqual(capsuleWindow.alwaysOnTop, true, 'capsule window should stay above the focused app while recording'); -assertEqual(mainWindow.decorations, false, 'windows main window should use only custom titlebar'); +assertEqual(mainWindow.decorations, true, 'shared main window config should keep native macOS traffic lights'); assertEqual(mainWindow.visible, false, 'windows main window should stay hidden until the intended first show point'); +assertMatch( + libRs, + /#\[cfg\(target_os = "windows"\)\][\s\S]*?main\.set_decorations\(false\)/, + 'windows runtime should disable native chrome before the first show', +); + +assertMatch( + coordinatorRs, + /#\[cfg\(target_os = "macos"\)\][\s\S]*?orderFrontRegardless/, + 'macOS capsule should show without taking the key window', +); + if (!/function WindowsResizeHandles\(\)/.test(windowChromeTsx)) { throw new Error('windows frameless shell should expose explicit resize handles'); } +assertMatch( + windowChromeTsx, + /const MAC_TITLEBAR_HEIGHT = 30;/, + 'macOS titlebar spacer should stay visually compact around the native traffic lights', +); +assertMatch( + libRs, + /show_main_window[\s\S]*?set_focus\(\)/, + 'macOS main window should rely on native traffic lights instead of manually moving standardWindowButton frames', +); +if (/standardWindowButton|setFrameOrigin: origin|tune_macos_main_window_controls/.test(libRs)) { + throw new Error('macOS traffic lights should not be manually repositioned; keep native AppKit button frames visible'); +} +if (!/action=\"close\"/.test(windowChromeTsx) || !/tone=\"danger\"/.test(windowChromeTsx)) { + throw new Error('windows titlebar should keep the close button and danger hover treatment'); +} +assertMatch( + tokensCss, + /--ol-motion-spring:[\s\S]*?--ol-motion-soft:[\s\S]*?--ol-motion-quick:/, + 'shared motion tokens should drive shell animations and transitions', +); + if (!/startResizeDragging\(direction\)/.test(windowChromeTsx)) { throw new Error('windows resize handles should delegate edge dragging to Tauri'); } diff --git a/openless-all/app/src-tauri/src/coordinator.rs b/openless-all/app/src-tauri/src/coordinator.rs index c588ec4d..2dab885a 100644 --- a/openless-all/app/src-tauri/src/coordinator.rs +++ b/openless-all/app/src-tauri/src/coordinator.rs @@ -2532,7 +2532,7 @@ fn restore_focus_target_if_possible(target: Option) -> bool { let _ = unsafe { ShowWindow(hwnd, SW_RESTORE) }; } let _ = unsafe { SetForegroundWindow(hwnd) }; - std::thread::sleep(Duration::from_millis(60)); + std::thread::sleep(std::time::Duration::from_millis(60)); let foreground = unsafe { GetForegroundWindow() }; if foreground != hwnd { @@ -2548,23 +2548,24 @@ fn restore_focus_target_if_possible(_target: Option) -> bool { } #[cfg(target_os = "windows")] -fn show_capsule_window_no_activate() -> bool { - use std::iter::once; - use windows::core::PCWSTR; +fn show_capsule_window_no_activate( + _app: &AppHandle, + window: &tauri::WebviewWindow, +) -> bool { + use raw_window_handle::{HasWindowHandle, RawWindowHandle}; use windows::Win32::Foundation::HWND; use windows::Win32::UI::WindowsAndMessaging::{ - FindWindowW, SetWindowPos, ShowWindow, HWND_TOPMOST, SWP_NOACTIVATE, SWP_NOMOVE, - SWP_NOSIZE, SWP_SHOWWINDOW, SW_SHOWNOACTIVATE, + SetWindowPos, ShowWindow, HWND_TOPMOST, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, + SWP_SHOWWINDOW, SW_SHOWNOACTIVATE, }; - let title: Vec = "OpenLess Capsule".encode_utf16().chain(once(0)).collect(); - let hwnd = match unsafe { FindWindowW(PCWSTR::null(), PCWSTR(title.as_ptr())) } { - Ok(hwnd) => hwnd, - Err(_) => return false, + let Ok(handle) = window.window_handle() else { + return false; }; - if hwnd == HWND::default() || hwnd.0.is_null() { + let RawWindowHandle::Win32(raw) = handle.as_raw() else { return false; - } + }; + let hwnd = HWND(raw.hwnd.get() as *mut _); let _ = unsafe { ShowWindow(hwnd, SW_SHOWNOACTIVATE) }; let _ = unsafe { @@ -2581,8 +2582,15 @@ fn show_capsule_window_no_activate() -> bool { true } +// macOS / Linux 上不走 no-activate 路径:胶囊由 emit_capsule 的 fallback +// `window.show()` 直接显示,再用 restore_main_window_key_if_active 把焦点还给 +// 主窗口。这是 1.2.11 的实现 — 单独走 orderFrontRegardless 会让胶囊在 webview +// 未完整初始化时偶发不可见。 #[cfg(not(target_os = "windows"))] -fn show_capsule_window_no_activate() -> bool { +fn show_capsule_window_no_activate( + _app: &AppHandle, + _window: &tauri::WebviewWindow, +) -> bool { false } @@ -2651,15 +2659,11 @@ fn emit_capsule( let visible = !matches!(state, CapsuleState::Idle); maybe_position_capsule_bottom_center(inner, &window, payload.translation); if show_capsule && visible { - if cfg!(target_os = "windows") { - if !show_capsule_window_no_activate() { - let _ = window.show(); - } - } else { + if !show_capsule_window_no_activate(&app, &window) { let _ = window.show(); } - // 胶囊 show() 在 macOS 会调 makeKeyAndOrderFront: 抢走主窗口焦点。 - // 若 OpenLess 已是前台 app,用 makeKeyWindow 还原主窗口(不激活 NSApp)。 + // macOS/Windows 优先走 no-activate show,避免录音胶囊抢走主窗口点击焦点。 + // 若 fallback 到 show(),OpenLess 已是前台 app 时再把 key window 还给 main。 #[cfg(target_os = "macos")] crate::restore_main_window_key_if_active(&app); } else { diff --git a/openless-all/app/src-tauri/src/lib.rs b/openless-all/app/src-tauri/src/lib.rs index 13cd0b36..539f224d 100644 --- a/openless-all/app/src-tauri/src/lib.rs +++ b/openless-all/app/src-tauri/src/lib.rs @@ -486,7 +486,8 @@ fn activate_app(_app: &AppHandle) {} /// 不调 NSApp.activate,不抢其他 app 焦点,符合 CLAUDE.md 约束。 #[cfg(target_os = "macos")] pub(crate) fn restore_main_window_key_if_active(app: &AppHandle) { - let _ = app.run_on_main_thread(|| { + let main = app.get_webview_window("main"); + let _ = app.run_on_main_thread(move || { use objc2::msg_send; use objc2::runtime::{AnyClass, AnyObject, Bool}; unsafe { @@ -501,11 +502,18 @@ pub(crate) fn restore_main_window_key_if_active(app: &AppHandle) if !is_active.as_bool() { return; } - let main_win: *mut AnyObject = msg_send![ns_app, mainWindow]; - if main_win.is_null() { + let Some(main) = main else { return; - } - let _: () = msg_send![main_win, makeKeyWindow]; + }; + match main.ns_window() { + Ok(handle) => { + let main_win = handle as *mut AnyObject; + if !main_win.is_null() { + let _: () = msg_send![main_win, makeKeyWindow]; + } + } + Err(e) => log::warn!("[main] ns_window unavailable for key restore: {e}"), + }; } }); } @@ -753,9 +761,12 @@ fn capsule_window_bounds(translation_active: bool) -> CapsuleWindowBounds { #[cfg(not(target_os = "windows"))] { + // macOS / Linux:固定 220×110,与 1.2.11 行为一致 — 录音 / 翻译徽章 + // 共用同一个窗口尺寸,避免按 Shift 后窗口高度变化导致胶囊整体下移。 + let _ = translation_active; CapsuleWindowBounds { - width: 176.0, - height: if translation_active { 110.0 } else { 42.0 }, + width: 220.0, + height: 110.0, bottom_inset: 0.0, } } @@ -769,7 +780,7 @@ fn capsule_visual_height(_translation_active: bool) -> f64 { #[cfg(not(target_os = "windows"))] { - 42.0 + 96.0 } } @@ -788,7 +799,7 @@ mod tests { assert_eq!((bounds.width, bounds.height, bounds.bottom_inset), (220.0, 84.0, 12.0)); #[cfg(not(target_os = "windows"))] - assert_eq!((bounds.width, bounds.height, bounds.bottom_inset), (176.0, 42.0, 0.0)); + assert_eq!((bounds.width, bounds.height, bounds.bottom_inset), (220.0, 110.0, 0.0)); } #[test] @@ -798,7 +809,7 @@ mod tests { assert_eq!((bounds.width, bounds.height, bounds.bottom_inset), (220.0, 118.0, 12.0)); #[cfg(not(target_os = "windows"))] - assert_eq!((bounds.width, bounds.height, bounds.bottom_inset), (176.0, 110.0, 0.0)); + assert_eq!((bounds.width, bounds.height, bounds.bottom_inset), (220.0, 110.0, 0.0)); } #[test] @@ -807,7 +818,7 @@ mod tests { assert_eq!(capsule_visual_height(true), 52.0); #[cfg(not(target_os = "windows"))] - assert_eq!(capsule_visual_height(true), 42.0); + assert_eq!(capsule_visual_height(true), 96.0); } #[test] @@ -816,6 +827,6 @@ mod tests { assert_eq!(capsule_height_for_qa(), 52.0); #[cfg(not(target_os = "windows"))] - assert_eq!(capsule_height_for_qa(), 42.0); + assert_eq!(capsule_height_for_qa(), 96.0); } } diff --git a/openless-all/app/src-tauri/src/polish.rs b/openless-all/app/src-tauri/src/polish.rs index f394eb4e..082fb4a7 100644 --- a/openless-all/app/src-tauri/src/polish.rs +++ b/openless-all/app/src-tauri/src/polish.rs @@ -645,8 +645,12 @@ pub mod prompts { const COMMON_RULES: &str = "# 通用规则\n\ 1) \u{4E0D}确定 / 转写明显不完整 / 断句在半截 \u{2192} 保留原话,\u{4E0D}要替用户补全或猜测。\n\ 2) 中英混输、专有名词、产品名、代码 / 命令 / 路径 / URL、数字与单位、emoji \u{2192} 原样保留。\n\ - 3) \u{4E0D}引入用户没说过的事实;中途改口以最终版本为准。\n\ - 4) 如果原始转写本身是在\u{201C}询问 / 要求别人做某事\u{201D},只整理为清楚的问题或请求,\u{4E0D}代替对方回答。"; + 3) \u{4E0D}引入用户没说过的事实;中途改口以最终版本为准。在保留原意和语气的前提下,按用户的整体意图把零碎口语组织成协调、自然的书面表达。\n\ + 4) 如果原始转写本身是在\u{201C}询问 / 要求别人做某事\u{201D},只整理为清楚的问题或请求,\u{4E0D}代替对方回答。\n\ + 5) 自动纠错:明显的 ASR 同音 / 形近错字按上下文纠回正确字面,常见模式包括\ + \u{201C}跟目录 / 根木鹿\u{201D}\u{2192}\u{201C}根目录\u{201D}、\u{201C}代码厂\u{201D}\u{2192}\u{201C}代码仓\u{201D}、\ + \u{201C}编一编\u{201D}\u{2192}\u{201C}编译\u{201D}、\u{201C}的 / 得 / 地\u{201D}用法、\u{201C}做 / 作\u{201D} 等常见错别字。\ + 专有名词(见 # 热词)、人名、品牌名、不在常见中文词典里的词原样保留,\u{4E0D}强行改字;改了之后含义会发生变化的不改。"; const OUTPUT_BLOCK: &str = "# 输出\n\ 直接输出最终文本正文。需要结构化时直接从标题 / 段落 / 编号开始。\n\ @@ -912,6 +916,32 @@ mod tests { assert!(prompt.contains("- OpenLess")); } + #[test] + fn common_rules_include_auto_correction_and_natural_organization() { + // 所有 mode 都要带上"自动纠错"(规则 5)和"按整体意图组织成自然书面表达" + // 的扩展(规则 3)。任一缺失说明 COMMON_RULES 被回退掉了。 + for mode in [ + PolishMode::Raw, + PolishMode::Light, + PolishMode::Structured, + PolishMode::Formal, + ] { + let prompt = prompts::system_prompt(mode); + assert!( + prompt.contains("5) 自动纠错"), + "{mode:?} prompt 缺少自动纠错规则" + ); + assert!( + prompt.contains("根目录"), + "{mode:?} prompt 缺少根目录纠错示例" + ); + assert!( + prompt.contains("按用户的整体意图把零碎口语组织成协调、自然的书面表达"), + "{mode:?} prompt 缺少自然组织扩展" + ); + } + } + #[tokio::test] async fn chat_completion_omits_authorization_when_api_key_is_empty() { let listener = TcpListener::bind("127.0.0.1:0").unwrap(); diff --git a/openless-all/app/src-tauri/tauri.conf.json b/openless-all/app/src-tauri/tauri.conf.json index fa959dd5..fead7540 100644 --- a/openless-all/app/src-tauri/tauri.conf.json +++ b/openless-all/app/src-tauri/tauri.conf.json @@ -21,11 +21,12 @@ "minWidth": 980, "minHeight": 640, "resizable": true, - "decorations": false, + "decorations": true, "transparent": true, "shadow": true, "hiddenTitle": true, "titleBarStyle": "Overlay", + "trafficLightPosition": { "x": 14, "y": 20 }, "visible": false, "acceptFirstMouse": true }, diff --git a/openless-all/app/src/components/AutoUpdate.tsx b/openless-all/app/src/components/AutoUpdate.tsx index 167103a1..40f96d85 100644 --- a/openless-all/app/src/components/AutoUpdate.tsx +++ b/openless-all/app/src/components/AutoUpdate.tsx @@ -173,7 +173,7 @@ export function UpdateDialog({ {(downloading || installing || status === 'downloaded') && (
-
+
{installing diff --git a/openless-all/app/src/components/Capsule.tsx b/openless-all/app/src/components/Capsule.tsx index 03120f28..618e4681 100644 --- a/openless-all/app/src/components/Capsule.tsx +++ b/openless-all/app/src/components/Capsule.tsx @@ -39,7 +39,7 @@ function AudioBars({ level }: AudioBarsProps) { borderRadius: 999, background: 'var(--ol-blue)', opacity: 0.82, - transition: 'height 0.06s linear', + transition: 'height 0.08s var(--ol-motion-quick)', }} /> ))} @@ -59,7 +59,7 @@ function ProcessingDots() { borderRadius: 999, background: 'var(--ol-blue)', opacity: 0.85, - animation: `cap-dot 0.9s linear ${i * 0.3}s infinite`, + animation: `cap-dot 0.9s var(--ol-motion-soft) ${i * 0.3}s infinite`, }} /> ))} @@ -130,7 +130,7 @@ function CircleButton({ variant, enabled, onClick }: CircleButtonProps) { flexShrink: 0, padding: 0, boxShadow: '0 1px 2px rgba(0, 0, 0, 0.06)', - transition: 'opacity 0.15s ease-out, background 0.12s ease-out, transform 0.08s ease-out', + transition: 'opacity 0.18s var(--ol-motion-soft), background 0.16s var(--ol-motion-quick), transform 0.12s var(--ol-motion-quick)', }} > {isCancel ? ( @@ -243,7 +243,7 @@ function Pill({ os, state, level, insertedChars, message, onCancel, onConfirm }: fontFamily: 'var(--ol-font-sans)', transform: `scale(${scale.toFixed(4)})`, transformOrigin: 'center', - transition: 'transform 0.06s linear, box-shadow 0.06s linear', + transition: 'transform 0.08s var(--ol-motion-quick), box-shadow 0.08s var(--ol-motion-quick)', willChange: 'transform, box-shadow', filter: dropShadow, }} @@ -261,12 +261,13 @@ export function Capsule() { const { t } = useTranslation(); const os = detectOS(); const metrics = getCapsulePillMetrics(os); - const [translation, setTranslation] = useState(false); - const hostMetrics = getCapsuleHostMetrics(os, translation); const [state, setState] = useState(isTauri ? 'idle' : 'recording'); const [level, setLevel] = useState(isTauri ? 0 : 0.6); const [insertedChars, setInsertedChars] = useState(0); const [message, setMessage] = useState(); + const [translation, setTranslation] = useState(false); + // Windows 端 host 在翻译模式从 84 长到 118;macOS / Linux 上 capsuleLayout 已固定 42 忽略此参数。 + const hostMetrics = getCapsuleHostMetrics(os, translation); useEffect(() => { if (!isTauri) return; @@ -329,9 +330,11 @@ export function Capsule() { style={{ position: 'absolute', left: '50%', - // bottom = 50%(pill 中线)+ pill 半高 21px(capsuleLayout mac=42)+ 8px 间隔。 - // 只有翻译徽章可见时才需要额外高度;普通录音/转写状态由后端缩到 pill 本体,避免透明死区。 - bottom: `${hostMetrics.bottomInset + metrics.height + hostMetrics.badgeGap}px`, + // macOS / Linux:胶囊窗口 220×110、pill 居中,badge 锚到 pill 中线上方 21+8。 + // Windows:pill 不居中(带 12pt 阴影 inset),用 hostMetrics 量到底部 inset + pill 高 + gap。 + bottom: os === 'win' + ? `${hostMetrics.bottomInset + metrics.height + hostMetrics.badgeGap}px` + : 'calc(50% + 21px + 8px)', transform: 'translateX(-50%)', pointerEvents: 'none', }} diff --git a/openless-all/app/src/components/FloatingShell.tsx b/openless-all/app/src/components/FloatingShell.tsx index 6e0de024..5537bfc2 100644 --- a/openless-all/app/src/components/FloatingShell.tsx +++ b/openless-all/app/src/components/FloatingShell.tsx @@ -22,16 +22,13 @@ import { HOTKEY_MODE_MIGRATION_DEFERRED_KEY, shouldShowHotkeyModeMigrationPrompt, } from '../lib/hotkeyMigration'; -import { getHotkeyTriggerLabel } from '../lib/hotkey'; import { applyFontScale, readFontScale } from '../lib/fontScale'; import { getCredentials, openExternal } from '../lib/ipc'; -import { OL_DATA } from '../lib/mockData'; import { PROVIDER_SETUP_PROMPT_DEFERRED_KEY, shouldShowProviderSetupPrompt, } from '../lib/providerSetup'; import type { SettingsSectionId } from '../pages/Settings'; -import { useHotkeySettings } from '../state/HotkeySettingsContext'; import { useAppState, type AppTab } from '../state/useAppState'; interface NavItem { @@ -75,7 +72,20 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia const [providerPromptOpen, setProviderPromptOpen] = useState(false); const [hotkeyModePromptOpen, setHotkeyModePromptOpen] = useState(false); const [helpPopoverOpen, setHelpPopoverOpen] = useState(false); - const { hotkey } = useHotkeySettings(); + + // tab 切换的 cross-fade:旧页 blur+fade out(180ms),结束后挂载新页(走 ol-page-slide enter)。 + // displayTab 是实际渲染的 tab,currentTab 是用户点中的目标 tab。 + const [displayTab, setDisplayTab] = useState(initialTab); + const [tabPhase, setTabPhase] = useState<'idle' | 'exiting'>('idle'); + useEffect(() => { + if (currentTab === displayTab) return; + setTabPhase('exiting'); + const id = window.setTimeout(() => { + setDisplayTab(currentTab); + setTabPhase('idle'); + }, 180); + return () => window.clearTimeout(id); + }, [currentTab, displayTab]); // 字体档位 — 启动时按 localStorage 应用一次;之后改动来自 Settings 的"个性化"section。 useEffect(() => { @@ -100,7 +110,7 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia () => NAV_BASE.map(b => ({ ...b, name: t(`nav.${b.id}`) })), [t], ); - const Page = (NAV.find((n) => n.id === currentTab) ?? NAV[0]).cmp; + const Page = (NAV.find((n) => n.id === displayTab) ?? NAV[0]).cmp; useEffect(() => { let cancelled = false; @@ -151,7 +161,7 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia }; return ( -
+
{/* Main shell — flush with the frosted backplate (no separate float). */}
{/* brand */} -
+
OpenLess {n.name} - {n.id === 'history' && - {OL_DATA.history.length} - } ); })} @@ -225,21 +229,6 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
- {/* shortcut hint — 不要 dashed 边框,否则会切断"整片磨砂玻璃"的视觉 */} -
-
{t('shell.shortcutLabel')}
-
- {getHotkeyTriggerLabel(hotkey?.trigger)} - {t('shell.shortcutHint')} -
-
- {/* BETA 区域 — 去掉描边和实色背景,让它和底部 footer 一起浮在磨砂玻璃上 */}
{t('shell.betaTag')}
@@ -249,7 +238,7 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia {/* Main content — inset white card sitting on the frosted backplate. 内卡圆角与外层窗口(WindowChrome 20/14)对齐,避免视觉上"两个不一致的圆角"。 */} -
+
- {/* key={currentTab} 让每次切换重挂这棵子树 → ol-page-slide keyframe 重新触发。 + {/* key={displayTab} 让每次切换重挂这棵子树 → ol-page-slide keyframe 重新触发。 + 旧 tab 退出时不立刻 unmount,而是先播 ol-page-fadeout(blur+淡出), + 180ms 后再切到新 tab 并播入场动画。详见 displayTab/tabPhase 的 effect。 padding + overflow:auto 直接挂在这棵 wrapper 上: - 自然高度的页(Overview / Vocab / Style)—— 整页内容超出时 wrapper 出现滚动条 - 用 height:100% 撑满的页(History 左右双列)—— 100% 能解析到 wrapper 的固定高度, 两列内部各自的 overflow:auto 才能独立滚动 */}
- {currentTab === 'overview' ? ( + {displayTab === 'overview' ? ( setCurrentTab('history')} /> ) : ( @@ -349,17 +342,24 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia {/* tab 切换 + provider prompt + footer popover 公用的入场关键帧 */}
@@ -379,9 +379,9 @@ function ProviderSetupPrompt({ onLater, onOpenSettings }: { onLater: () => void; justifyContent: 'center', padding: 28, background: 'rgba(15,17,22,0.28)', - backdropFilter: 'blur(2px)', - WebkitBackdropFilter: 'blur(2px)', - animation: 'ol-prompt-fade 0.18s ease-out', + backdropFilter: 'blur(6px) saturate(140%)', + WebkitBackdropFilter: 'blur(6px) saturate(140%)', + animation: 'ol-prompt-fade 0.2s var(--ol-motion-soft)', }} >
void; border: '0.5px solid rgba(0,0,0,.08)', boxShadow: '0 24px 70px -24px rgba(15,17,22,.38), 0 0 0 0.5px rgba(0,0,0,.06)', padding: 20, - animation: 'ol-prompt-pop 0.22s cubic-bezier(.2,.9,.3,1.1)', + animation: 'ol-prompt-pop 0.26s var(--ol-motion-spring)', }} >
@@ -430,7 +430,7 @@ function ProviderSetupPrompt({ onLater, onOpenSettings }: { onLater: () => void; fontSize: 12.5, fontWeight: 500, cursor: 'default', - transition: 'background 0.12s ease-out, border-color 0.12s ease-out', + transition: 'background 0.16s var(--ol-motion-quick), border-color 0.16s var(--ol-motion-quick)', }} > {t('shell.providerPrompt.later')} @@ -448,7 +448,7 @@ function ProviderSetupPrompt({ onLater, onOpenSettings }: { onLater: () => void; fontSize: 12.5, fontWeight: 500, cursor: 'default', - transition: 'background 0.12s ease-out, transform 0.08s ease-out', + transition: 'background 0.16s var(--ol-motion-quick), transform 0.12s var(--ol-motion-quick)', }} > {t('shell.providerPrompt.openSettings')} @@ -472,9 +472,9 @@ function HotkeyModeMigrationPrompt({ onLater, onOpenSettings }: { onLater: () => justifyContent: 'center', padding: 28, background: 'rgba(15,17,22,0.28)', - backdropFilter: 'blur(2px)', - WebkitBackdropFilter: 'blur(2px)', - animation: 'ol-prompt-fade 0.18s ease-out', + backdropFilter: 'blur(6px) saturate(140%)', + WebkitBackdropFilter: 'blur(6px) saturate(140%)', + animation: 'ol-prompt-fade 0.2s var(--ol-motion-soft)', }} >
border: '0.5px solid rgba(0,0,0,.08)', boxShadow: '0 24px 70px -24px rgba(15,17,22,.38), 0 0 0 0.5px rgba(0,0,0,.06)', padding: 20, - animation: 'ol-prompt-pop 0.22s cubic-bezier(.2,.9,.3,1.1)', + animation: 'ol-prompt-pop 0.26s var(--ol-motion-spring)', }} >
@@ -523,7 +523,7 @@ function HotkeyModeMigrationPrompt({ onLater, onOpenSettings }: { onLater: () => fontSize: 12.5, fontWeight: 500, cursor: 'default', - transition: 'background 0.12s ease-out, border-color 0.12s ease-out', + transition: 'background 0.16s var(--ol-motion-quick), border-color 0.16s var(--ol-motion-quick)', }} > {t('shell.hotkeyModePrompt.later')} @@ -541,7 +541,7 @@ function HotkeyModeMigrationPrompt({ onLater, onOpenSettings }: { onLater: () => fontSize: 12.5, fontWeight: 500, cursor: 'default', - transition: 'background 0.12s ease-out, transform 0.08s ease-out', + transition: 'background 0.16s var(--ol-motion-quick), transform 0.12s var(--ol-motion-quick)', }} > {t('shell.hotkeyModePrompt.openSettings')} @@ -579,7 +579,7 @@ function FooterIcon({ name, tip, active, onClick }: FooterIconProps) { color: active ? 'var(--ol-ink)' : hover ? 'var(--ol-ink-2)' : 'var(--ol-ink-4)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'default', - transition: 'background 0.12s ease-out, color 0.12s ease-out', + transition: 'background 0.16s var(--ol-motion-quick), color 0.16s var(--ol-motion-quick)', }}> @@ -610,11 +610,11 @@ function FooterIconWithPopover({ padding: 12, borderRadius: 12, background: 'rgba(255,255,255,0.96)', - backdropFilter: 'blur(20px) saturate(180%)', - WebkitBackdropFilter: 'blur(20px) saturate(180%)', + backdropFilter: 'blur(var(--ol-glass-blur)) saturate(180%)', + WebkitBackdropFilter: 'blur(var(--ol-glass-blur)) saturate(180%)', border: '0.5px solid rgba(0,0,0,0.08)', boxShadow: '0 18px 50px -22px rgba(15,17,22,0.32), 0 0 0 0.5px rgba(0,0,0,0.05)', - animation: 'ol-popover-pop 0.18s cubic-bezier(0.32, 0.72, 0, 1) both', + animation: 'ol-popover-pop 0.22s var(--ol-motion-spring) both', transformOrigin: 'bottom left', }} > @@ -693,7 +693,7 @@ function FooterAutoUpdateButton() { cursor: 'default', padding: 0, opacity: u.checking || u.busy ? 0.7 : 1, - transition: 'opacity 0.12s ease-out', + transition: 'opacity 0.16s var(--ol-motion-soft)', }} > {u.checking ? t('settings.about.checkingUpdate') : t('settings.about.checkUpdateBtn')} diff --git a/openless-all/app/src/components/Onboarding.tsx b/openless-all/app/src/components/Onboarding.tsx index 1f272707..978c3033 100644 --- a/openless-all/app/src/components/Onboarding.tsx +++ b/openless-all/app/src/components/Onboarding.tsx @@ -247,7 +247,7 @@ function PermissionStep({ index, title, desc, status, actionLabel, onAction, dis color: granted ? 'var(--ol-ink-3)' : '#fff', cursor: disabled ? 'not-allowed' : 'default', opacity: disabled && !granted ? 0.6 : 1, - transition: 'background 0.15s ease-out, color 0.15s ease-out, opacity 0.15s ease-out, transform 0.08s ease-out', + 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)', }} > {actionLabel} diff --git a/openless-all/app/src/components/SettingsModal.tsx b/openless-all/app/src/components/SettingsModal.tsx index 20911c5d..dcf04656 100644 --- a/openless-all/app/src/components/SettingsModal.tsx +++ b/openless-all/app/src/components/SettingsModal.tsx @@ -67,12 +67,12 @@ export function SettingsModal({ os: _os, onClose, initialSettingsSection }: Sett style={{ position: 'absolute', inset: 0, background: 'rgba(15,17,22,0.32)', - backdropFilter: 'blur(2px)', - WebkitBackdropFilter: 'blur(2px)', + backdropFilter: 'blur(8px) saturate(140%)', + WebkitBackdropFilter: 'blur(8px) saturate(140%)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 28, zIndex: 50, - animation: 'ol-modal-fade .18s ease-out', + animation: 'ol-modal-fade .2s var(--ol-motion-soft)', }}>
@@ -121,7 +121,7 @@ export function SettingsModal({ os: _os, onClose, initialSettingsSection }: Sett fontFamily: 'inherit', fontSize: 13, fontWeight: active ? 600 : 500, boxShadow: active ? '0 1px 2px rgba(0,0,0,.05), 0 0 0 0.5px rgba(0,0,0,.06)' : 'none', cursor: 'default', textAlign: 'left', - transition: 'background 0.12s ease-out, color 0.12s ease-out', + transition: 'background 0.16s var(--ol-motion-quick), color 0.16s var(--ol-motion-quick)', }}> @@ -146,7 +146,7 @@ export function SettingsModal({ os: _os, onClose, initialSettingsSection }: Sett background: 'transparent', color: 'var(--ol-ink-3)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'default', - transition: 'background 0.12s ease-out', + transition: 'background 0.16s var(--ol-motion-quick)', }} onMouseEnter={e => (e.currentTarget.style.background = 'rgba(0,0,0,0.05)')} onMouseLeave={e => (e.currentTarget.style.background = 'transparent')} @@ -173,10 +173,13 @@ export function SettingsModal({ os: _os, onClose, initialSettingsSection }: Sett
@@ -234,7 +237,7 @@ function PersonalizeSection() { fontWeight: selected ? 600 : 500, cursor: 'default', boxShadow: selected ? '0 1px 2px rgba(0,0,0,.06), 0 0 0 0.5px rgba(0,0,0,.06)' : 'none', - transition: 'background 0.12s ease-out, color 0.12s ease-out, box-shadow 0.12s ease-out', + transition: 'background 0.16s var(--ol-motion-quick), color 0.16s var(--ol-motion-quick), box-shadow 0.18s var(--ol-motion-soft)', padding: '0 12px', }} > @@ -302,7 +305,7 @@ const btnGhost: CSSProperties = { border: '0.5px solid var(--ol-line-strong)', background: '#fff', color: 'var(--ol-ink-2)', cursor: 'default', fontFamily: 'inherit', - transition: 'background 0.12s ease-out, border-color 0.12s ease-out', + transition: 'background 0.16s var(--ol-motion-quick), border-color 0.16s var(--ol-motion-quick)', }; // 真正可用的语言切换器 —— 用原生