From a81f59ba0f2e0692cd9f94427d0dc6899a89d81a Mon Sep 17 00:00:00 2001 From: baiqing Date: Sat, 9 May 2026 16:09:24 +0800 Subject: [PATCH] feat(ui): consolidate footer/nav, sliding indicator, hover cues, top-right saved toast MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 整体 UI 整理 + 跨页面"已保存"提示统一到右上角小 pill。 ## Footer 简化 - 删 账号 icon(= openSettings('providers') 跟齿轮重复入口) - 删 帮助 popover(version + 2 条外链跟 Settings → 关于 重复) - 删 检查更新 按钮(Settings.about 已有同等控件) - BETA 角标从 sidebar 移到右下角紧贴版本号 - 设置 icon 内嵌 12px 不再贴右边角 ## Sidebar / Settings 三处 nav 统一 - 滑动指示器:用一个 absolute pill,currentTab 切换时 top/height 平滑过渡 到目标 button(cubic-bezier(0.16,1,0.3,1) 360ms,无 overshoot 的 Apple ease-out-quint),不是各按钮瞬切背景。 - Hover 三段视觉层次: 基础态 ink-3 中灰文字 + 透明底 Hover ink 全黑文字 + 浅灰底(图标通过 currentColor 同步加深) 选中 ink 全黑文字 + 白色滑动 pill 底 用 .ol-nav-btn class + :hover 实现,inline color 让位给 CSS(CSS 不能盖 inline style 是关键)。 ## "已保存" toast 改右上角小 pill - 新增 components/SavedToast.tsx + lib/savedEvent.ts(emitSaved 事件总线) - Translation / SelectionAsk 内联 banner 改为锚到 ol-thinscroll 控制台卡 右上角的 absolute pill;scroll wrapper 加 position:relative - Settings → ASR 配置:CredentialField API key 内联"已保存"挪到 SettingsModal 右上角统一 pill(offsetStyle: right:54 避开关闭按钮);只保留持续性的 readError 在原位标识字段不可用 ## Translation / SelectionAsk 简化 - 删跟 Settings.shortcuts 重复的快捷键卡(用户改键统一在 Settings 完成) - SelectionAsk "历史保存"从全宽 Card 改成左对齐的小模块(标签 + toggle 紧贴) - PageHeader 砍 kicker+desc → 单行 title - 使用方法卡去 inset 提示块(保留 5 步 ol) ## Overview 收头 - PageHeader 单行 title(去 kicker / desc / 右侧"按 X 开始"pill) --- .../app/src/components/FloatingShell.tsx | 288 +++++------------- .../app/src/components/SavedToast.tsx | 53 ++++ .../app/src/components/SettingsModal.tsx | 56 +++- openless-all/app/src/lib/savedEvent.ts | 64 ++++ openless-all/app/src/pages/Overview.tsx | 29 +- openless-all/app/src/pages/SelectionAsk.tsx | 222 ++++---------- openless-all/app/src/pages/Settings.tsx | 100 ++++-- openless-all/app/src/pages/Translation.tsx | 102 +------ 8 files changed, 381 insertions(+), 533 deletions(-) create mode 100644 openless-all/app/src/components/SavedToast.tsx create mode 100644 openless-all/app/src/lib/savedEvent.ts diff --git a/openless-all/app/src/components/FloatingShell.tsx b/openless-all/app/src/components/FloatingShell.tsx index 48cbca97..0ff74b2f 100644 --- a/openless-all/app/src/components/FloatingShell.tsx +++ b/openless-all/app/src/components/FloatingShell.tsx @@ -4,9 +4,8 @@ // // Ported verbatim from design_handoff_openless/variants.jsx::FloatingShell. -import { useEffect, useMemo, useState, type CSSProperties, type ComponentType, type ReactNode } from 'react'; +import { useEffect, useLayoutEffect, useMemo, useRef, useState, type ComponentType } from 'react'; import { useTranslation } from 'react-i18next'; -import { isDialogStatus, UpdateDialog, useAutoUpdate } from './AutoUpdate'; import { Icon } from './Icon'; import { WindowChrome, detectOS, type OS } from './WindowChrome'; import { SettingsModal } from './SettingsModal'; @@ -23,15 +22,13 @@ import { HOTKEY_MODE_MIGRATION_DEFERRED_KEY, shouldShowHotkeyModeMigrationPrompt, } from '../lib/hotkeyMigration'; -import { formatComboLabel } from '../lib/hotkey'; import { applyFontScale, readFontScale } from '../lib/fontScale'; -import { getCredentials, openExternal } from '../lib/ipc'; +import { getCredentials } from '../lib/ipc'; import { PROVIDER_SETUP_PROMPT_DEFERRED_KEY, shouldShowProviderSetupPrompt, } from '../lib/providerSetup'; import { NAVIGATE_LOCAL_ASR_EVENT, type SettingsSectionId } from '../pages/Settings'; -import { useHotkeySettings } from '../state/HotkeySettingsContext'; import { useAppState, type AppTab } from '../state/useAppState'; interface NavItem { @@ -51,9 +48,6 @@ const NAV_BASE: Array> = [ { id: 'localAsr', icon: 'archive', cmp: LocalAsr }, ]; -const RELEASE_NOTES_URL = 'https://github.com/appergb/openless/releases'; -const HELP_DOCS_URL = 'https://github.com/appergb/openless#readme'; - interface FloatingShellProps { os?: OS; initialTab?: AppTab; @@ -75,8 +69,6 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia const [settingsInitialSection, setSettingsInitialSection] = useState(); const [providerPromptOpen, setProviderPromptOpen] = useState(false); const [hotkeyModePromptOpen, setHotkeyModePromptOpen] = useState(false); - const [helpPopoverOpen, setHelpPopoverOpen] = useState(false); - const { prefs } = useHotkeySettings(); // tab 切换的 cross-fade:旧页 blur+fade out(180ms),结束后挂载新页(走 ol-page-slide enter)。 // displayTab 是实际渲染的 tab,currentTab 是用户点中的目标 tab。 @@ -97,26 +89,23 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia applyFontScale(readFontScale()); }, []); - // help popover 打开时,点击其他位置自动关闭 - useEffect(() => { - if (!helpPopoverOpen) return; - const onDown = (e: MouseEvent) => { - const target = e.target as Element | null; - if (target && target.closest('[data-ol-footer-popover]')) return; - setHelpPopoverOpen(false); - }; - const id = window.setTimeout(() => document.addEventListener('mousedown', onDown), 0); - return () => { - window.clearTimeout(id); - document.removeEventListener('mousedown', onDown); - }; - }, [helpPopoverOpen]); const NAV = useMemo( () => NAV_BASE.map(b => ({ ...b, name: t(`nav.${b.id}`) })), [t], ); const Page = (NAV.find((n) => n.id === displayTab) ?? NAV[0]).cmp; + // sidebar nav 滑动指示器:测量当前 active button 的 offsetTop / height, + // 用一个 absolute pill 平滑滑过去,而不是每个按钮各自瞬切背景色。 + const navItemRefs = useRef>([]); + const [pillRect, setPillRect] = useState<{ top: number; height: number } | null>(null); + useLayoutEffect(() => { + const idx = NAV.findIndex(n => n.id === currentTab); + const el = navItemRefs.current[idx]; + if (!el) return; + setPillRect({ top: el.offsetTop, height: el.offsetHeight }); + }, [currentTab, NAV]); + useEffect(() => { let cancelled = false; (async () => { @@ -221,25 +210,47 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
OpenLess
- {/* nav */} -