Merge pull request #656 from Open-Less/revert/aura-ui-to-beta3-keep-features
revert(ui): 回退 AURA 界面大改到 beta-3 外观,保留功能与今日修复
This commit is contained in:
44 files changed
+581
-1547
No files matched your search
@@ -200,14 +200,14 @@ export function UpdateDialog({
|
||||
const installing = status === 'installing';
|
||||
return (
|
||||
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.18)', display: 'grid', placeItems: 'center', zIndex: 40 }}>
|
||||
<div style={{ width: 360, borderRadius: 'var(--ol-modal-radius)', background: 'var(--ol-surface)', border: '0.5px solid var(--ol-line-strong)', boxShadow: '0 18px 42px rgba(0,0,0,0.18)', padding: 18 }}>
|
||||
<div style={{ width: 360, borderRadius: 16, background: 'var(--ol-surface)', border: '0.5px solid var(--ol-line-strong)', boxShadow: '0 18px 42px rgba(0,0,0,0.18)', padding: 18 }}>
|
||||
<div style={{ fontSize: 15, fontWeight: 650, marginBottom: 8 }}>{t(`settings.about.updateDialog.${status}.title`)}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--ol-ink-3)', lineHeight: 1.6, marginBottom: 14 }}>
|
||||
{t(`settings.about.updateDialog.${status}.desc`, { version })}
|
||||
</div>
|
||||
{(downloading || installing || status === 'downloaded') && (
|
||||
<div style={{ marginBottom: 14 }}>
|
||||
<div style={{ height: 8, borderRadius: 'var(--ol-pill-radius)', background: 'var(--ol-surface-2)', overflow: 'hidden', border: '0.5px solid var(--ol-line)' }}>
|
||||
<div style={{ height: 8, borderRadius: 999, background: 'var(--ol-surface-2)', overflow: 'hidden', border: '0.5px solid var(--ol-line)' }}>
|
||||
<div style={{ height: '100%', width: `${status === 'downloaded' || installing ? 100 : progress ?? 8}%`, background: 'var(--ol-blue)', transition: 'width 0.18s var(--ol-motion-soft)' }} />
|
||||
</div>
|
||||
<div style={{ marginTop: 6, fontSize: 11, color: 'var(--ol-ink-4)' }}>
|
||||
|
||||
@@ -41,7 +41,7 @@ function AudioBars({ level }: AudioBarsProps) {
|
||||
display: 'inline-block',
|
||||
width: 3,
|
||||
height: base + (max - base) * visualVoice * env,
|
||||
borderRadius: 'var(--ol-pill-radius)',
|
||||
borderRadius: 999,
|
||||
background: 'var(--ol-blue)',
|
||||
opacity: 0.82,
|
||||
transformOrigin: 'center',
|
||||
@@ -113,12 +113,12 @@ function CircleButton({ variant, enabled, onClick }: CircleButtonProps) {
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 'var(--ol-pill-radius)',
|
||||
background: isCancel ? 'var(--ol-capsule-cancel-bg)' : 'var(--ol-capsule-confirm-bg)',
|
||||
borderRadius: 999,
|
||||
background: isCancel ? 'rgba(255, 255, 255, 0.55)' : 'rgba(255, 255, 255, 0.92)',
|
||||
backdropFilter: useBackdrop ? 'blur(12px) saturate(160%)' : 'none',
|
||||
WebkitBackdropFilter: useBackdrop ? 'blur(12px) saturate(160%)' : 'none',
|
||||
color: isCancel ? 'var(--ol-capsule-cancel-ink)' : 'var(--ol-capsule-confirm-ink)',
|
||||
border: '0.8px solid var(--ol-glass-border)',
|
||||
color: 'var(--ol-ink)',
|
||||
border: '0.8px solid rgba(0, 0, 0, 0.08)',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -265,11 +265,11 @@ function Pill({ os, state, level, insertedChars, message, operating, onCancel, o
|
||||
width: metrics.width,
|
||||
height: metrics.height,
|
||||
boxSizing: metrics.boxSizing,
|
||||
borderRadius: 'var(--ol-pill-radius)',
|
||||
border: '1px solid var(--ol-glass-border)',
|
||||
borderRadius: 999,
|
||||
border: '1px solid rgba(255, 255, 255, 0.55)',
|
||||
boxShadow: os === 'win'
|
||||
? `0 10px 24px -14px rgba(0, 0, 0, ${(0.24 + ambient * 0.06).toFixed(3)}), var(--ol-pill-shadow)`
|
||||
: `0 18px 50px -10px rgba(0, 0, 0, ${shadowAlpha.toFixed(3)}), var(--ol-pill-shadow)`,
|
||||
? `0 10px 24px -14px rgba(0, 0, 0, ${(0.24 + ambient * 0.06).toFixed(3)}), 0 0 0 0.5px rgba(0, 0, 0, 0.08), inset 0 1px 0 0 rgba(255, 255, 255, 0.8)`
|
||||
: `0 18px 50px -10px rgba(0, 0, 0, ${shadowAlpha.toFixed(3)}), 0 0 0 0.5px rgba(0, 0, 0, 0.08), inset 0 1px 0 0 rgba(255, 255, 255, 0.8)`,
|
||||
color: 'var(--ol-ink)',
|
||||
fontFamily: 'var(--ol-font-sans)',
|
||||
transform: `scale(${scale.toFixed(4)})`,
|
||||
@@ -446,11 +446,11 @@ export function Capsule() {
|
||||
alignItems: 'center',
|
||||
gap: 5,
|
||||
padding: '3px 10px',
|
||||
borderRadius: 'var(--ol-pill-radius)',
|
||||
borderRadius: 999,
|
||||
fontSize: 10.5,
|
||||
fontWeight: 600,
|
||||
color: 'var(--ol-blue)',
|
||||
background: 'var(--ol-glass-bg-strong)',
|
||||
background: 'rgba(255, 255, 255, 0.78)',
|
||||
backdropFilter: 'blur(20px) saturate(180%)',
|
||||
WebkitBackdropFilter: 'blur(20px) saturate(180%)',
|
||||
border: '0.5px solid rgba(37, 99, 235, 0.25)',
|
||||
@@ -465,7 +465,7 @@ export function Capsule() {
|
||||
willChange: 'opacity, transform',
|
||||
}}
|
||||
>
|
||||
<span style={{ width: 5, height: 5, borderRadius: 'var(--ol-pill-radius)', background: 'var(--ol-blue)' }} />
|
||||
<span style={{ width: 5, height: 5, borderRadius: 999, background: 'var(--ol-blue)' }} />
|
||||
{t('capsule.translating')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//
|
||||
// Ported verbatim from design_handoff_openless/variants.jsx::FloatingShell.
|
||||
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState, type ComponentType, type CSSProperties } from 'react';
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState, type ComponentType } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Icon } from './Icon';
|
||||
import { WindowChrome, detectOS, type OS } from './WindowChrome';
|
||||
@@ -26,14 +26,13 @@ import {
|
||||
shouldShowHotkeyModeMigrationPrompt,
|
||||
} from '../lib/hotkeyMigration';
|
||||
import { applyFontScale, readFontScale } from '../lib/fontScale';
|
||||
import { getCredentials, getPlatformCapabilities } from '../lib/ipc';
|
||||
import { getCredentials } from '../lib/ipc';
|
||||
import {
|
||||
PROVIDER_SETUP_PROMPT_DEFERRED_KEY,
|
||||
shouldShowProviderSetupPrompt,
|
||||
} from '../lib/providerSetup';
|
||||
import { type SettingsSectionId } from './SettingsModal';
|
||||
import { useAppState, type AppTab } from '../state/useAppState';
|
||||
import { useMobileLayout } from '../lib/useMobileLayout';
|
||||
|
||||
interface NavItem {
|
||||
id: AppTab;
|
||||
@@ -72,7 +71,6 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
|
||||
const [settingsInitialSection, setSettingsInitialSection] = useState<SettingsSectionId | undefined>();
|
||||
const [providerPromptOpen, setProviderPromptOpen] = useState(false);
|
||||
const [hotkeyModePromptOpen, setHotkeyModePromptOpen] = useState(false);
|
||||
const mobileLayout = useMobileLayout();
|
||||
|
||||
// tab 切换的 cross-fade:旧页 blur+fade out(180ms),结束后挂载新页(走 ol-page-slide enter)。
|
||||
// displayTab 是实际渲染的 tab,currentTab 是用户点中的目标 tab。
|
||||
@@ -98,17 +96,12 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
|
||||
[t],
|
||||
);
|
||||
const Page = (NAV.find((n) => n.id === displayTab) ?? NAV[0]).cmp;
|
||||
const activeNav = NAV.find(n => n.id === currentTab) ?? NAV[0];
|
||||
|
||||
// sidebar nav 滑动指示器:测量当前 active button 的 offsetTop / height,
|
||||
// 用一个 absolute pill 平滑滑过去,而不是每个按钮各自瞬切背景色。
|
||||
const navItemRefs = useRef<Array<HTMLButtonElement | null>>([]);
|
||||
const [pillRect, setPillRect] = useState<{ top: number; height: number } | null>(null);
|
||||
useLayoutEffect(() => {
|
||||
if (mobileLayout) {
|
||||
setPillRect(null);
|
||||
return;
|
||||
}
|
||||
if (settingsOpen) {
|
||||
setPillRect(null);
|
||||
return;
|
||||
@@ -121,13 +114,11 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
|
||||
const el = navItemRefs.current[idx];
|
||||
if (!el) return;
|
||||
setPillRect({ top: el.offsetTop, height: el.offsetHeight });
|
||||
}, [currentTab, settingsOpen, NAV, mobileLayout]);
|
||||
}, [currentTab, settingsOpen, NAV]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
const caps = await getPlatformCapabilities();
|
||||
if (cancelled || caps.platform === 'android') return;
|
||||
const credentials = await getCredentials();
|
||||
const promptDeferredValue = window.sessionStorage.getItem(PROVIDER_SETUP_PROMPT_DEFERRED_KEY);
|
||||
if (!cancelled && shouldShowProviderSetupPrompt(credentials, promptDeferredValue)) {
|
||||
@@ -140,18 +131,11 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void getPlatformCapabilities().then((caps) => {
|
||||
if (cancelled || caps.platform === 'android') return;
|
||||
const acknowledgedValue = window.localStorage.getItem(HOTKEY_MODE_MIGRATION_ACK_KEY);
|
||||
const deferredValue = window.sessionStorage.getItem(HOTKEY_MODE_MIGRATION_DEFERRED_KEY);
|
||||
if (shouldShowHotkeyModeMigrationPrompt(acknowledgedValue, deferredValue)) {
|
||||
setHotkeyModePromptOpen(true);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
const acknowledgedValue = window.localStorage.getItem(HOTKEY_MODE_MIGRATION_ACK_KEY);
|
||||
const deferredValue = window.sessionStorage.getItem(HOTKEY_MODE_MIGRATION_DEFERRED_KEY);
|
||||
if (shouldShowHotkeyModeMigrationPrompt(acknowledgedValue, deferredValue)) {
|
||||
setHotkeyModePromptOpen(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 之前监听的 NAVIGATE_LOCAL_ASR_EVENT 已无意义——「模型设置」独立 tab 已下线,
|
||||
@@ -197,69 +181,37 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="ol-app-shell-bg"
|
||||
data-ol-mobile={mobileLayout ? 'true' : 'false'}
|
||||
style={{ flex: 1, position: 'relative', display: 'flex', flexDirection: 'column', minHeight: 0, paddingTop: os === 'mac' ? 28 : 0 }}
|
||||
>
|
||||
<div style={{ flex: 1, position: 'relative', display: 'flex', flexDirection: 'column', minHeight: 0, paddingTop: os === 'mac' ? 28 : 0 }}>
|
||||
|
||||
{/* Main shell — flush with the frosted backplate (no separate float). */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1, minHeight: 0,
|
||||
display: 'flex',
|
||||
flexDirection: mobileLayout ? 'column' : 'row',
|
||||
background: 'transparent',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
}}>
|
||||
|
||||
{mobileLayout ? (
|
||||
<div className="ol-aura-mobile-topbar">
|
||||
<div className="ol-aura-mobile-brand">
|
||||
<img
|
||||
className="ol-aura-mobile-brand-mark"
|
||||
src="AppIcon.png"
|
||||
alt="OpenLess"
|
||||
/>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div className="ol-aura-mobile-brand-title">OpenLess</div>
|
||||
<div className="ol-aura-mobile-brand-section">{activeNav.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openSettings()}
|
||||
className={settingsOpen ? 'ol-aura-mobile-settings ol-aura-mobile-settings-active' : 'ol-aura-mobile-settings'}
|
||||
aria-label={t('shell.footer.settings')}
|
||||
>
|
||||
<Icon name="settings" size={17} />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
/* Sidebar — 透明地坐在外层磨砂底板上,让 LOGO/导航/快捷键/BETA/footer 共用同一片磨砂玻璃 */
|
||||
<aside
|
||||
className="ol-aura-sidebar"
|
||||
style={{
|
||||
width: 196,
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}>
|
||||
{/* Sidebar — 透明地坐在外层磨砂底板上,让 LOGO/导航/快捷键/BETA/footer 共用同一片磨砂玻璃 */}
|
||||
<aside
|
||||
style={{
|
||||
width: 188,
|
||||
flexShrink: 0,
|
||||
display: 'flex', flexDirection: 'column',
|
||||
background: 'transparent',
|
||||
padding: '10px 10px 12px',
|
||||
}}>
|
||||
|
||||
{/* brand */}
|
||||
<div className="ol-aura-sidebar-brand">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 9, padding: '2px 8px 12px' }}>
|
||||
<img
|
||||
className="ol-aura-sidebar-brand-mark"
|
||||
src="AppIcon.png"
|
||||
alt="OpenLess"
|
||||
/>
|
||||
style={{ width: 22, height: 22, borderRadius: 5, boxShadow: '0 1px 2px rgba(0,0,0,.1), 0 0 0 0.5px rgba(0,0,0,.06)' }} />
|
||||
|
||||
<div>
|
||||
<div className="ol-aura-sidebar-brand-title">OpenLess</div>
|
||||
<div className="ol-aura-sidebar-brand-kicker">VOICE CONSOLE</div>
|
||||
</div>
|
||||
<div style={{ fontSize: 13.5, fontWeight: 600, letterSpacing: '-0.01em', color: 'var(--ol-ink)' }}>OpenLess</div>
|
||||
</div>
|
||||
|
||||
{/* nav — 滑动指示器:active pill 是 absolute 元素,currentTab 改变时 top/height
|
||||
@@ -268,7 +220,6 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
|
||||
<nav style={{ position: 'relative', display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
{pillRect && (
|
||||
<div
|
||||
className="ol-aura-sidebar-pill"
|
||||
aria-hidden
|
||||
style={{
|
||||
position: 'absolute',
|
||||
@@ -276,6 +227,9 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
|
||||
right: 0,
|
||||
top: pillRect.top,
|
||||
height: pillRect.height,
|
||||
background: 'var(--ol-surface)',
|
||||
borderRadius: 8,
|
||||
boxShadow: '0 1px 2px rgba(0,0,0,.05), 0 0 0 0.5px rgba(0,0,0,.06)',
|
||||
transition: 'top 0.36s var(--ol-motion-spring), height 0.36s var(--ol-motion-spring)',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 0,
|
||||
@@ -289,9 +243,18 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
|
||||
key={n.id}
|
||||
ref={el => { navItemRefs.current[i] = el; }}
|
||||
onClick={() => setCurrentTab(n.id)}
|
||||
className={active ? 'ol-nav-btn ol-nav-btn-active ol-aura-sidebar-nav-btn' : 'ol-nav-btn ol-aura-sidebar-nav-btn'}
|
||||
className={active ? 'ol-nav-btn ol-nav-btn-active' : 'ol-nav-btn'}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
padding: '7px 10px',
|
||||
borderRadius: 8, border: 0,
|
||||
background: 'transparent',
|
||||
fontFamily: 'inherit', fontSize: 13,
|
||||
cursor: 'default',
|
||||
transition: 'color 0.16s var(--ol-motion-quick), background 0.16s var(--ol-motion-quick)',
|
||||
textAlign: 'left',
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
}}>
|
||||
|
||||
<Icon name={n.icon} size={14} />
|
||||
@@ -306,10 +269,31 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
|
||||
{/* 底部两行:上行 = 版本 chip(含 BETA 标),下行 = 设置按钮。
|
||||
单行布局在窄 sidebar 下会把「设置」挤成两行竖字 + 版本糊一起;
|
||||
翻回两行同时把顺序反过来:设置真正落到最底,版本在它上面。 */}
|
||||
<div className="ol-aura-sidebar-footer">
|
||||
<div className="ol-aura-sidebar-version">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, paddingTop: 10 }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
flexWrap: 'wrap',
|
||||
padding: '0 10px',
|
||||
fontFamily: 'var(--ol-font-sans)',
|
||||
fontSize: 11,
|
||||
color: 'var(--ol-ink-4)',
|
||||
}}
|
||||
>
|
||||
{IS_BETA_BUILD && (
|
||||
<span className="ol-aura-beta-tag">{t('shell.betaTag')}</span>
|
||||
<span style={{
|
||||
display: 'inline-block',
|
||||
padding: '2px 8px',
|
||||
fontSize: 10,
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.04em',
|
||||
textTransform: 'uppercase',
|
||||
color: 'var(--ol-blue)',
|
||||
background: 'rgba(37,99,235,0.10)',
|
||||
borderRadius: 999,
|
||||
}}>{t('shell.betaTag')}</span>
|
||||
)}
|
||||
|
||||
<span>{t('shell.footer.version', { version: APP_VERSION_LABEL })}</span>
|
||||
@@ -317,33 +301,39 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
|
||||
|
||||
<button
|
||||
onClick={() => openSettings()}
|
||||
className={settingsOpen ? 'ol-nav-btn ol-nav-btn-active ol-aura-sidebar-settings' : 'ol-nav-btn ol-aura-sidebar-settings'}
|
||||
className={settingsOpen ? 'ol-nav-btn ol-nav-btn-active' : 'ol-nav-btn'}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
padding: '7px 10px',
|
||||
borderRadius: 8, border: 0,
|
||||
background: settingsOpen ? 'var(--ol-surface)' : 'transparent',
|
||||
boxShadow: settingsOpen ? '0 1px 2px rgba(0,0,0,.05), 0 0 0 0.5px rgba(0,0,0,.06)' : 'none',
|
||||
fontFamily: 'inherit', fontSize: 13,
|
||||
cursor: 'default',
|
||||
transition: 'color 0.16s var(--ol-motion-quick), background 0.16s var(--ol-motion-quick)',
|
||||
textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
<Icon name="settings" size={14} />
|
||||
<span style={{ flex: 1 }}>{t('shell.footer.settings')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
)}
|
||||
</aside>
|
||||
|
||||
{/* Main content — Linux 禁用透明窗口后使用不透明面;其他平台保留玻璃层。 */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
padding: mobileLayout ? '0' : '4px 8px 6px 0',
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
{/* Main content — Linux 禁用透明窗口后使用不透明面;其他平台保留玻璃层。
|
||||
悬浮台到右边 / 下边的间距相等(都 8px),左侧贴 sidebar(0)。 */}
|
||||
<div style={{ flex: 1, minWidth: 0, padding: '4px 8px 8px 0', display: 'flex' }}>
|
||||
<main
|
||||
className="ol-console-main ol-aura-panel ol-aura-console-main"
|
||||
className="ol-console-main"
|
||||
style={{
|
||||
flex: 1, minWidth: 0,
|
||||
overflow: 'hidden',
|
||||
background: os === 'linux' ? 'var(--ol-surface)' : 'rgba(255, 255, 255, 0.62)',
|
||||
backdropFilter: os === 'linux' ? 'none' : 'blur(18px) saturate(170%)',
|
||||
WebkitBackdropFilter: os === 'linux' ? 'none' : 'blur(18px) saturate(170%)',
|
||||
borderRadius: 'var(--ol-window-console-radius)',
|
||||
border: '0.5px solid rgba(0,0,0,0.06)',
|
||||
boxShadow: '0 1px 0 rgba(255,255,255,0.8) inset, 0 8px 24px -12px rgba(15,17,22,0.10), 0 2px 6px -2px rgba(15,17,22,0.06)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
@@ -368,9 +358,7 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
|
||||
style={{
|
||||
flex: 1, minHeight: 0,
|
||||
overflow: 'auto',
|
||||
padding: mobileLayout
|
||||
? '16px 14px 18px'
|
||||
: '24px 28px 32px',
|
||||
padding: '24px 28px 32px',
|
||||
// position:relative 让页面里的"已保存"toast 用 absolute top:16 right:16
|
||||
// 锚到这块控制台卡的右上角,而不是横在页头变成长横幅。
|
||||
position: 'relative',
|
||||
@@ -391,29 +379,6 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{mobileLayout && (
|
||||
<nav
|
||||
className="ol-aura-mobile-nav"
|
||||
aria-label="OpenLess"
|
||||
style={{ '--ol-nav-count': NAV.length } as CSSProperties}
|
||||
>
|
||||
{NAV.map(n => {
|
||||
const active = !settingsOpen && currentTab === n.id;
|
||||
return (
|
||||
<button
|
||||
key={n.id}
|
||||
type="button"
|
||||
onClick={() => setCurrentTab(n.id)}
|
||||
className={active ? 'ol-aura-mobile-nav-btn ol-aura-mobile-nav-btn-active' : 'ol-aura-mobile-nav-btn'}
|
||||
>
|
||||
<Icon name={n.icon} size={18} />
|
||||
<span>{n.name}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Settings modal — rendered inside this window */}
|
||||
@@ -438,6 +403,44 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
|
||||
/>
|
||||
) : null}
|
||||
<AudioCueListener />
|
||||
|
||||
{/* tab 切换 + provider prompt + footer popover 公用的入场关键帧 */}
|
||||
<style>{`
|
||||
/* nav 三段视觉层次:
|
||||
基础态 → ink-3(中灰文字 + 透明底)
|
||||
hover → ink(深色文字 + 浅灰底) ← 让"翻译"等字词在悬停时高亮,跟基础/选中都拉开差距
|
||||
选中 → ink(深色文字 + 白色 pill 底,由 absolute pill 提供)
|
||||
inline color/fontWeight 留给 active 项写最高优先级;非 active 走 class,
|
||||
这样 :hover 能正确覆盖(CSS 不能盖 inline style)。 */
|
||||
.ol-nav-btn {
|
||||
color: var(--ol-ink-3);
|
||||
font-weight: 500;
|
||||
}
|
||||
.ol-nav-btn.ol-nav-btn-active {
|
||||
color: var(--ol-ink);
|
||||
font-weight: 600;
|
||||
}
|
||||
.ol-nav-btn:not(.ol-nav-btn-active):hover {
|
||||
background: rgba(0,0,0,0.04);
|
||||
color: var(--ol-ink);
|
||||
}
|
||||
@keyframes ol-page-slide {
|
||||
from { opacity: 0; transform: translate3d(10px, 0, 0) scale(.996); filter: blur(6px); }
|
||||
to { opacity: 1; transform: translate3d(0, 0, 0) scale(1); filter: blur(0); }
|
||||
}
|
||||
@keyframes ol-page-fadeout {
|
||||
from { opacity: 1; filter: blur(0); }
|
||||
to { opacity: 0; filter: blur(8px); }
|
||||
}
|
||||
@keyframes ol-prompt-fade {
|
||||
from { opacity: 0; backdrop-filter: blur(0); -webkit-backdrop-filter: blur(0); }
|
||||
to { opacity: 1; backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); }
|
||||
}
|
||||
@keyframes ol-prompt-pop {
|
||||
from { opacity: 0; transform: translateY(6px) scale(.97); filter: blur(6px); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -454,7 +457,7 @@ function ProviderSetupPrompt({ onLater, onOpenSettings }: { onLater: () => void;
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 28,
|
||||
background: 'var(--ol-overlay-bg)',
|
||||
background: 'rgba(15,17,22,0.28)',
|
||||
backdropFilter: 'blur(6px) saturate(140%)',
|
||||
WebkitBackdropFilter: 'blur(6px) saturate(140%)',
|
||||
animation: 'ol-prompt-fade 0.2s var(--ol-motion-soft)',
|
||||
@@ -463,10 +466,10 @@ function ProviderSetupPrompt({ onLater, onOpenSettings }: { onLater: () => void;
|
||||
<div
|
||||
style={{
|
||||
width: 360,
|
||||
borderRadius: 'var(--ol-r-lg)',
|
||||
borderRadius: 12,
|
||||
background: 'var(--ol-surface)',
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
boxShadow: 'var(--ol-shadow-lg)',
|
||||
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.26s var(--ol-motion-spring)',
|
||||
}}
|
||||
@@ -476,8 +479,8 @@ function ProviderSetupPrompt({ onLater, onOpenSettings }: { onLater: () => void;
|
||||
style={{
|
||||
width: 34,
|
||||
height: 34,
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
background: 'var(--ol-blue-soft)',
|
||||
borderRadius: 8,
|
||||
background: 'rgba(37,99,235,0.10)',
|
||||
color: 'var(--ol-blue)',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
@@ -498,7 +501,7 @@ function ProviderSetupPrompt({ onLater, onOpenSettings }: { onLater: () => void;
|
||||
style={{
|
||||
height: 32,
|
||||
padding: '0 13px',
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
borderRadius: 8,
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
background: 'var(--ol-surface)',
|
||||
color: 'var(--ol-ink-3)',
|
||||
@@ -516,10 +519,10 @@ function ProviderSetupPrompt({ onLater, onOpenSettings }: { onLater: () => void;
|
||||
style={{
|
||||
height: 32,
|
||||
padding: '0 14px',
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
borderRadius: 8,
|
||||
border: 0,
|
||||
background: 'var(--ol-primary-solid-bg)',
|
||||
color: 'var(--ol-primary-solid-ink)',
|
||||
background: 'var(--ol-ink)',
|
||||
color: '#fff',
|
||||
fontFamily: 'inherit',
|
||||
fontSize: 12.5,
|
||||
fontWeight: 500,
|
||||
@@ -547,7 +550,7 @@ function HotkeyModeMigrationPrompt({ onLater, onOpenSettings }: { onLater: () =>
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 28,
|
||||
background: 'var(--ol-overlay-bg)',
|
||||
background: 'rgba(15,17,22,0.28)',
|
||||
backdropFilter: 'blur(6px) saturate(140%)',
|
||||
WebkitBackdropFilter: 'blur(6px) saturate(140%)',
|
||||
animation: 'ol-prompt-fade 0.2s var(--ol-motion-soft)',
|
||||
@@ -556,10 +559,10 @@ function HotkeyModeMigrationPrompt({ onLater, onOpenSettings }: { onLater: () =>
|
||||
<div
|
||||
style={{
|
||||
width: 380,
|
||||
borderRadius: 'var(--ol-r-lg)',
|
||||
borderRadius: 12,
|
||||
background: 'var(--ol-surface)',
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
boxShadow: 'var(--ol-shadow-lg)',
|
||||
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.26s var(--ol-motion-spring)',
|
||||
}}
|
||||
@@ -569,8 +572,8 @@ function HotkeyModeMigrationPrompt({ onLater, onOpenSettings }: { onLater: () =>
|
||||
style={{
|
||||
width: 34,
|
||||
height: 34,
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
background: 'var(--ol-blue-soft)',
|
||||
borderRadius: 8,
|
||||
background: 'rgba(37,99,235,0.10)',
|
||||
color: 'var(--ol-blue)',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
@@ -591,7 +594,7 @@ function HotkeyModeMigrationPrompt({ onLater, onOpenSettings }: { onLater: () =>
|
||||
style={{
|
||||
height: 32,
|
||||
padding: '0 13px',
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
borderRadius: 8,
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
background: 'var(--ol-surface)',
|
||||
color: 'var(--ol-ink-3)',
|
||||
@@ -609,10 +612,10 @@ function HotkeyModeMigrationPrompt({ onLater, onOpenSettings }: { onLater: () =>
|
||||
style={{
|
||||
height: 32,
|
||||
padding: '0 14px',
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
borderRadius: 8,
|
||||
border: 0,
|
||||
background: 'var(--ol-primary-solid-bg)',
|
||||
color: 'var(--ol-primary-solid-ink)',
|
||||
background: 'var(--ol-ink)',
|
||||
color: '#fff',
|
||||
fontFamily: 'inherit',
|
||||
fontSize: 12.5,
|
||||
fontWeight: 500,
|
||||
|
||||
@@ -124,7 +124,7 @@ export function GithubLoginModal({ onClose, onSuccess }: GithubLoginModalProps)
|
||||
title={t('common.close')}
|
||||
onClick={close}
|
||||
style={{
|
||||
width: 28, height: 28, borderRadius: 'var(--ol-control-radius)',
|
||||
width: 28, height: 28, borderRadius: 8,
|
||||
display: 'inline-grid', placeItems: 'center',
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
background: 'var(--ol-surface)',
|
||||
@@ -150,7 +150,7 @@ export function GithubLoginModal({ onClose, onSuccess }: GithubLoginModalProps)
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 12,
|
||||
padding: 18, borderRadius: 'var(--ol-r-lg)',
|
||||
padding: 18, borderRadius: 12,
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
background: 'var(--ol-surface-2)',
|
||||
}}>
|
||||
@@ -174,7 +174,7 @@ export function GithubLoginModal({ onClose, onSuccess }: GithubLoginModalProps)
|
||||
</div>
|
||||
<div style={{ fontSize: 11.5, color: 'var(--ol-ink-4)', textAlign: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}>
|
||||
<span style={{
|
||||
display: 'inline-block', width: 8, height: 8, borderRadius: 'var(--ol-pill-radius)',
|
||||
display: 'inline-block', width: 8, height: 8, borderRadius: 999,
|
||||
background: 'var(--ol-blue)', animation: 'ol-pulse 1.4s ease-in-out infinite',
|
||||
}} />
|
||||
{t('marketplace.oauth.waiting')}
|
||||
@@ -197,7 +197,7 @@ export function GithubLoginModal({ onClose, onSuccess }: GithubLoginModalProps)
|
||||
{phase.kind === 'error' && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<div style={{
|
||||
padding: 12, borderRadius: 'var(--ol-r-md)',
|
||||
padding: 12, borderRadius: 10,
|
||||
border: '0.5px solid rgba(239,68,68,0.3)',
|
||||
background: 'rgba(239,68,68,0.06)',
|
||||
color: '#b91c1c',
|
||||
|
||||
@@ -49,7 +49,7 @@ export function MarketplaceModal({ onClose }: MarketplaceModalProps) {
|
||||
style={{
|
||||
width: '100%', maxWidth: 1080, height: '100%', maxHeight: 720,
|
||||
background: 'var(--ol-surface)',
|
||||
borderRadius: 'var(--ol-bubble-radius)',
|
||||
borderRadius: 14,
|
||||
border: '0.5px solid rgba(0,0,0,.08)',
|
||||
boxShadow: '0 30px 80px -20px rgba(15,17,22,.35), 0 0 0 0.5px rgba(0,0,0,.06)',
|
||||
overflow: 'hidden',
|
||||
@@ -70,9 +70,9 @@ export function MarketplaceModal({ onClose }: MarketplaceModalProps) {
|
||||
title={loggedIn ? t('marketplace.modal.loggedIn') : t('marketplace.modal.notLoggedIn')}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
padding: '4px 10px', borderRadius: 'var(--ol-pill-radius)',
|
||||
padding: '4px 10px', borderRadius: 999,
|
||||
border: loggedIn ? '0.5px solid var(--ol-line)' : '0.5px solid rgba(239,68,68,0.32)',
|
||||
background: loggedIn ? 'var(--ol-glass-bg-strong)' : 'rgba(239,68,68,0.08)',
|
||||
background: loggedIn ? 'rgba(255,255,255,0.85)' : 'rgba(239,68,68,0.08)',
|
||||
color: loggedIn ? 'var(--ol-ink-2)' : 'var(--ol-red, #ef4444)',
|
||||
fontSize: 11.5, fontWeight: 500,
|
||||
cursor: 'default',
|
||||
@@ -92,8 +92,8 @@ export function MarketplaceModal({ onClose }: MarketplaceModalProps) {
|
||||
position: 'absolute', top: 16, right: 16, zIndex: 3,
|
||||
width: 32, height: 32,
|
||||
border: '0.5px solid var(--ol-line)',
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
background: 'var(--ol-glass-bg-strong)',
|
||||
borderRadius: 8,
|
||||
background: 'rgba(255,255,255,0.85)',
|
||||
color: 'var(--ol-ink-2)',
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
cursor: 'default',
|
||||
@@ -107,7 +107,7 @@ export function MarketplaceModal({ onClose }: MarketplaceModalProps) {
|
||||
(e.currentTarget as HTMLButtonElement).style.color = 'var(--ol-ink)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.currentTarget as HTMLButtonElement).style.background = 'var(--ol-glass-bg-strong)';
|
||||
(e.currentTarget as HTMLButtonElement).style.background = 'rgba(255,255,255,0.85)';
|
||||
(e.currentTarget as HTMLButtonElement).style.color = 'var(--ol-ink-2)';
|
||||
}}
|
||||
aria-label={t('common.close')}
|
||||
|
||||
@@ -132,7 +132,7 @@ function AndroidOnboarding({ onComplete }: OnboardingProps) {
|
||||
key={step.id}
|
||||
style={{
|
||||
height: 4,
|
||||
borderRadius: 'var(--ol-pill-radius)',
|
||||
borderRadius: 999,
|
||||
background: index <= stepIndex ? 'var(--ol-blue)' : 'var(--ol-line-soft)',
|
||||
}}
|
||||
/>
|
||||
@@ -143,7 +143,7 @@ function AndroidOnboarding({ onComplete }: OnboardingProps) {
|
||||
style={{
|
||||
background: 'var(--ol-surface)',
|
||||
border: '0.5px solid var(--ol-line)',
|
||||
borderRadius: 'var(--ol-bubble-radius)',
|
||||
borderRadius: 14,
|
||||
boxShadow: 'var(--ol-shadow-lg)',
|
||||
padding: 18,
|
||||
display: 'flex',
|
||||
@@ -347,7 +347,7 @@ function DesktopOnboarding({
|
||||
padding: 32,
|
||||
boxSizing: 'border-box',
|
||||
background: 'var(--ol-surface)',
|
||||
borderRadius: 'var(--ol-bubble-radius)',
|
||||
borderRadius: 14,
|
||||
border: '0.5px solid var(--ol-line)',
|
||||
boxShadow: 'var(--ol-shadow-lg)',
|
||||
}}
|
||||
@@ -470,7 +470,7 @@ function AndroidStepCard({ children }: { children: ReactNode }) {
|
||||
flexDirection: 'column',
|
||||
gap: 12,
|
||||
padding: 14,
|
||||
borderRadius: 'var(--ol-r-md)',
|
||||
borderRadius: 10,
|
||||
background: 'var(--ol-surface-2)',
|
||||
border: '0.5px solid var(--ol-line-soft)',
|
||||
minWidth: 0,
|
||||
@@ -488,7 +488,7 @@ function StatusBadge({ granted, label }: { granted: boolean; label: string }) {
|
||||
flexShrink: 0,
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
borderRadius: 'var(--ol-pill-radius)',
|
||||
borderRadius: 999,
|
||||
padding: '4px 8px',
|
||||
color: granted ? 'var(--ol-ok)' : 'var(--ol-ink-4)',
|
||||
background: granted ? 'rgba(40, 160, 90, 0.12)' : 'rgba(0,0,0,0.06)',
|
||||
@@ -526,9 +526,9 @@ function PermissionStep({ index, title, desc, status, actionLabel, onAction, dis
|
||||
style={{
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: 'var(--ol-pill-radius)',
|
||||
background: granted ? 'var(--ol-accent-solid-bg)' : 'rgba(0,0,0,0.06)',
|
||||
color: granted ? 'var(--ol-accent-solid-ink)' : 'var(--ol-ink-3)',
|
||||
borderRadius: 999,
|
||||
background: granted ? 'var(--ol-blue)' : 'rgba(0,0,0,0.06)',
|
||||
color: granted ? '#fff' : 'var(--ol-ink-3)',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -558,9 +558,9 @@ function PermissionStep({ index, title, desc, status, actionLabel, onAction, dis
|
||||
fontWeight: 500,
|
||||
fontFamily: 'inherit',
|
||||
border: 0,
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
background: granted ? 'var(--ol-surface-2)' : 'var(--ol-primary-solid-bg)',
|
||||
color: granted ? 'var(--ol-ink-3)' : 'var(--ol-primary-solid-ink)',
|
||||
borderRadius: 8,
|
||||
background: granted ? 'var(--ol-surface-2)' : 'var(--ol-ink)',
|
||||
color: granted ? 'var(--ol-ink-3)' : '#fff',
|
||||
cursor: disabled ? 'not-allowed' : 'default',
|
||||
opacity: disabled && !granted ? 0.6 : 1,
|
||||
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)',
|
||||
@@ -580,9 +580,9 @@ const primaryButtonStyle = {
|
||||
fontWeight: 600,
|
||||
fontFamily: 'inherit',
|
||||
border: 0,
|
||||
borderRadius: 'var(--ol-r-md)',
|
||||
background: 'var(--ol-primary-solid-bg)',
|
||||
color: 'var(--ol-primary-solid-ink)',
|
||||
borderRadius: 10,
|
||||
background: 'var(--ol-ink)',
|
||||
color: '#fff',
|
||||
cursor: 'default',
|
||||
} as const;
|
||||
|
||||
@@ -594,7 +594,7 @@ const secondaryButtonStyle = {
|
||||
fontWeight: 600,
|
||||
fontFamily: 'inherit',
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
borderRadius: 'var(--ol-r-md)',
|
||||
borderRadius: 10,
|
||||
background: 'var(--ol-surface)',
|
||||
color: 'var(--ol-ink-2)',
|
||||
cursor: 'default',
|
||||
@@ -607,7 +607,7 @@ const plainButtonStyle = {
|
||||
fontWeight: 500,
|
||||
fontFamily: 'inherit',
|
||||
border: 0,
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
borderRadius: 8,
|
||||
background: 'transparent',
|
||||
color: 'var(--ol-ink-4)',
|
||||
cursor: 'default',
|
||||
@@ -616,7 +616,7 @@ const plainButtonStyle = {
|
||||
const footerHintStyle = {
|
||||
marginTop: 18,
|
||||
padding: '12px 14px',
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
borderRadius: 8,
|
||||
background: 'var(--ol-surface-2)',
|
||||
fontSize: 11.5,
|
||||
color: 'var(--ol-ink-3)',
|
||||
|
||||
@@ -2,19 +2,20 @@
|
||||
//
|
||||
// 重构(2026-05):原本是「外层弹窗侧栏 + 设置页内层侧栏」双层嵌套,用户点
|
||||
// 「设置」还要再面对第二个侧栏。现在拍平成单层 —— 通用 / 服务 / 隐私 / 高级 /
|
||||
// 个性化 / 关于 六个 tab。每个 tab 的内容见 pages/settings/。
|
||||
// 个性化 / 关于 六个 tab + 帮助外链组。每个 tab 的内容见 pages/settings/。
|
||||
//
|
||||
// 设计原则:每个可见控件都必须可用。没有后端支撑的占位(如账号)不在此弹窗出现。
|
||||
// 设计原则:每个可见控件都必须可用。没有后端支撑的占位(账号 / 主题切换 等)
|
||||
// 不在此弹窗出现。
|
||||
|
||||
import { useLayoutEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Icon } from './Icon';
|
||||
import { SavedToast } from './SavedToast';
|
||||
import { useSavedToastListener } from '../lib/savedEvent';
|
||||
import { openExternal } from '../lib/ipc';
|
||||
import type { OS } from './WindowChrome';
|
||||
import { GeneralTab, ServicesTab, PrivacyTab, AdvancedTab } from '../pages/settings/tabs';
|
||||
import { AboutSection } from '../pages/settings/AboutSection';
|
||||
import { useMobileLayout } from '../lib/useMobileLayout';
|
||||
|
||||
// 稳定 tab ID(与 i18n key `modal.sections.*` 一致)。
|
||||
export type SettingsSectionId =
|
||||
@@ -33,8 +34,14 @@ interface SettingsModalProps {
|
||||
interface ModalNavItem {
|
||||
id: string;
|
||||
icon: string;
|
||||
external?: boolean;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
const HELP_URL = 'https://github.com/appergb/openless#readme';
|
||||
const RELEASE_NOTES_URL = 'https://github.com/appergb/openless/releases';
|
||||
|
||||
// 第一组:可选中的 tab;第二组:外部链接(永远不 active)。
|
||||
const TAB_ITEMS: ModalNavItem[] = [
|
||||
{ id: 'general', icon: 'settings' },
|
||||
{ id: 'services', icon: 'cloud' },
|
||||
@@ -42,83 +49,67 @@ const TAB_ITEMS: ModalNavItem[] = [
|
||||
{ id: 'advanced', icon: 'bolt' },
|
||||
{ id: 'about', icon: 'info' },
|
||||
];
|
||||
const LINK_ITEMS: ModalNavItem[] = [
|
||||
{ id: 'helpCenter', icon: 'help', external: true, href: HELP_URL },
|
||||
{ id: 'releaseNotes', icon: 'doc', external: true, href: RELEASE_NOTES_URL },
|
||||
];
|
||||
|
||||
export function SettingsModal({ os: _os, onClose, initialSettingsSection }: SettingsModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [section, setSection] = useState<SettingsSectionId>(initialSettingsSection ?? 'general');
|
||||
const savedToast = useSavedToastListener();
|
||||
const mobile = useMobileLayout();
|
||||
|
||||
// 与 sidebar nav 一致的滑动指示器:仅 tab 组有 pill;外链组永远不画 pill。
|
||||
const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);
|
||||
const [pillRect, setPillRect] = useState<{ top: number; height: number } | null>(null);
|
||||
useLayoutEffect(() => {
|
||||
if (mobile) {
|
||||
setPillRect(null);
|
||||
return;
|
||||
}
|
||||
const idx = TAB_ITEMS.findIndex(it => it.id === section);
|
||||
const el = tabRefs.current[idx];
|
||||
if (!el) return;
|
||||
setPillRect({ top: el.offsetTop, height: el.offsetHeight });
|
||||
}, [section, mobile]);
|
||||
}, [section]);
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: 'absolute', inset: 0,
|
||||
background: 'var(--ol-overlay-bg)',
|
||||
background: 'rgba(15,17,22,0.32)',
|
||||
backdropFilter: 'blur(8px) saturate(140%)',
|
||||
WebkitBackdropFilter: 'blur(8px) saturate(140%)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
padding: mobile ? 0 : 28,
|
||||
padding: 28,
|
||||
zIndex: 50,
|
||||
animation: 'ol-modal-backdrop-in 0.18s var(--ol-motion-soft)',
|
||||
}}>
|
||||
|
||||
<div
|
||||
className="ol-aura-settings"
|
||||
data-ol-mobile={mobile ? 'true' : 'false'}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: 920,
|
||||
height: '100%',
|
||||
maxHeight: mobile ? 'none' : 620,
|
||||
display: 'flex',
|
||||
flexDirection: mobile ? 'column' : 'row',
|
||||
overflow: 'hidden',
|
||||
width: '100%', maxWidth: 880, height: '100%', maxHeight: 600,
|
||||
background: 'var(--ol-surface)',
|
||||
borderRadius: 14,
|
||||
border: '0.5px solid rgba(0,0,0,.08)',
|
||||
boxShadow: '0 30px 80px -20px rgba(15,17,22,.35), 0 0 0 0.5px rgba(0,0,0,.06)',
|
||||
display: 'flex', overflow: 'hidden',
|
||||
animation: 'ol-modal-card-in 0.24s var(--ol-motion-spring)',
|
||||
position: 'relative',
|
||||
}}>
|
||||
|
||||
{/* ─── 单层侧栏 ────────────────────────────────────────────── */}
|
||||
<aside
|
||||
className="ol-aura-settings-rail"
|
||||
style={{
|
||||
width: mobile ? '100%' : 214,
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
flexDirection: mobile ? 'row' : 'column',
|
||||
width: 200, flexShrink: 0,
|
||||
background: 'rgba(247,247,250,0.7)',
|
||||
borderRight: '0.5px solid var(--ol-line-soft)',
|
||||
padding: '18px 12px',
|
||||
display: 'flex', flexDirection: 'column', gap: 14,
|
||||
}}>
|
||||
|
||||
{/* tab 组 */}
|
||||
<div
|
||||
className="ol-thinscroll"
|
||||
style={{
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: mobile ? 'row' : 'column',
|
||||
gap: 1,
|
||||
minWidth: 0,
|
||||
flex: mobile ? '1 1 auto' : undefined,
|
||||
overflowX: mobile ? 'auto' : undefined,
|
||||
}}
|
||||
>
|
||||
<div style={{ position: 'relative', display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
{pillRect && (
|
||||
<div
|
||||
className="ol-aura-settings-pill"
|
||||
aria-hidden
|
||||
style={{
|
||||
position: 'absolute',
|
||||
@@ -126,6 +117,9 @@ export function SettingsModal({ os: _os, onClose, initialSettingsSection }: Sett
|
||||
right: 0,
|
||||
top: pillRect.top,
|
||||
height: pillRect.height,
|
||||
background: '#fff',
|
||||
borderRadius: 8,
|
||||
boxShadow: '0 1px 2px rgba(0,0,0,.05), 0 0 0 0.5px rgba(0,0,0,.06)',
|
||||
transition: 'top 0.36s var(--ol-motion-spring), height 0.36s var(--ol-motion-spring)',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 0,
|
||||
@@ -139,30 +133,35 @@ export function SettingsModal({ os: _os, onClose, initialSettingsSection }: Sett
|
||||
key={it.id}
|
||||
ref={el => { tabRefs.current[idx] = el; }}
|
||||
onClick={() => setSection(it.id as SettingsSectionId)}
|
||||
className={active ? 'ol-nav-btn ol-nav-btn-active ol-aura-settings-nav-btn' : 'ol-nav-btn ol-aura-settings-nav-btn'}
|
||||
style={{ ...navBtnStyle, flexShrink: 0 }}>
|
||||
className={active ? 'ol-nav-btn ol-nav-btn-active' : 'ol-nav-btn'}
|
||||
style={navBtnStyle}>
|
||||
<Icon name={it.icon} size={14} />
|
||||
<span style={{ flex: 1 }}>{t(`modal.sections.${it.id}`)}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 外链组 */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 1, paddingTop: 8, borderTop: '0.5px solid var(--ol-line-soft)' }}>
|
||||
{LINK_ITEMS.map(it => (
|
||||
<button
|
||||
key={it.id}
|
||||
onClick={() => { if (it.href) void openExternal(it.href); }}
|
||||
className="ol-nav-btn"
|
||||
style={navBtnStyle}>
|
||||
<Icon name={it.icon} size={14} />
|
||||
<span style={{ flex: 1 }}>{t(`modal.sections.${it.id}`)}</span>
|
||||
<Icon name="external" size={11} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* ─── 内容区 ──────────────────────────────────────────────
|
||||
父容器 overflow:hidden + 列向 flex;关闭按钮、section 标题固定在头部,
|
||||
只有最里层的 scroll wrapper 真正滚动。 */}
|
||||
<div
|
||||
className="ol-aura-settings-content"
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 1, minWidth: 0, overflow: 'hidden', position: 'relative', display: 'flex', flexDirection: 'column' }}>
|
||||
{/* "已保存" toast:right:54 避开 28×28 关闭按钮 + 12px gap。 */}
|
||||
<SavedToast
|
||||
saveState={savedToast.state}
|
||||
@@ -171,32 +170,28 @@ export function SettingsModal({ os: _os, onClose, initialSettingsSection }: Sett
|
||||
offsetStyle={{ position: 'absolute', top: 16, right: 54 }}
|
||||
/>
|
||||
<button
|
||||
className="ol-aura-settings-close"
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: 'absolute', top: 14, right: 14, zIndex: 2,
|
||||
width: 28, height: 28, border: 0, borderRadius: 'var(--ol-pill-radius)',
|
||||
width: 28, height: 28, border: 0, borderRadius: 999,
|
||||
background: 'transparent', color: 'var(--ol-ink-3)',
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
cursor: 'default',
|
||||
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')}
|
||||
title={t('common.close')}>
|
||||
<Icon name="close" size={14} />
|
||||
</button>
|
||||
|
||||
<h2 className="ol-aura-settings-title" style={{ margin: 0, flexShrink: 0 }}>
|
||||
<h2 style={{ margin: 0, padding: '22px 28px 8px', fontSize: 22, fontWeight: 600, letterSpacing: '-0.02em', flexShrink: 0 }}>
|
||||
{t(`modal.sections.${section}`)}
|
||||
</h2>
|
||||
|
||||
<div
|
||||
className="ol-thinscroll"
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
overflow: 'auto',
|
||||
padding: mobile
|
||||
? '8px 14px calc(18px + env(safe-area-inset-bottom, 0px))'
|
||||
: '10px 28px 28px',
|
||||
}}>
|
||||
style={{ flex: 1, minHeight: 0, overflow: 'auto', padding: '10px 28px 28px' }}>
|
||||
{/* key=section 让切 tab 时整块重挂载,ol-tab-fade 轻微淡入。 */}
|
||||
<div
|
||||
key={section}
|
||||
@@ -216,4 +211,12 @@ export function SettingsModal({ os: _os, onClose, initialSettingsSection }: Sett
|
||||
|
||||
const navBtnStyle = {
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
padding: '7px 10px',
|
||||
borderRadius: 8, border: 0,
|
||||
background: 'transparent',
|
||||
fontFamily: 'inherit', fontSize: 13,
|
||||
cursor: 'default', textAlign: 'left' as const,
|
||||
position: 'relative' as const,
|
||||
zIndex: 1,
|
||||
transition: 'color 0.16s var(--ol-motion-quick), background 0.16s var(--ol-motion-quick)',
|
||||
};
|
||||
@@ -128,10 +128,10 @@ export function ShortcutRecorder({
|
||||
const recordButtonStyle: CSSProperties = {
|
||||
fontSize: 12,
|
||||
padding: '5px 14px',
|
||||
background: recording ? 'rgba(37,99,235,0.12)' : 'var(--ol-accent-solid-bg)',
|
||||
color: recording ? 'var(--ol-blue)' : 'var(--ol-accent-solid-ink)',
|
||||
background: recording ? 'rgba(37,99,235,0.12)' : 'var(--ol-blue)',
|
||||
color: recording ? 'var(--ol-blue)' : '#fff',
|
||||
border: 0,
|
||||
borderRadius: 'var(--ol-r-sm)',
|
||||
borderRadius: 6,
|
||||
fontFamily: 'inherit',
|
||||
fontWeight: 500,
|
||||
cursor: recording || disabled ? 'default' : 'pointer',
|
||||
@@ -144,7 +144,7 @@ export function ShortcutRecorder({
|
||||
background: 'transparent',
|
||||
color: 'var(--ol-ink-4)',
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
borderRadius: 'var(--ol-r-sm)',
|
||||
borderRadius: 6,
|
||||
fontFamily: 'inherit',
|
||||
fontWeight: 500,
|
||||
cursor: recording ? 'default' : 'pointer',
|
||||
@@ -160,7 +160,7 @@ export function ShortcutRecorder({
|
||||
return (
|
||||
<div style={rootStyle}>
|
||||
<div style={recorderRowStyle}>
|
||||
<span style={{ padding: '4px 10px', borderRadius: 'var(--ol-r-sm)', background: 'rgba(0,0,0,0.06)', fontSize: 13, fontFamily: 'var(--ol-font-mono)', fontWeight: 500, color: 'var(--ol-ink)' }}>
|
||||
<span style={{ padding: '4px 10px', borderRadius: 6, background: 'rgba(0,0,0,0.06)', fontSize: 13, fontFamily: 'var(--ol-font-mono)', fontWeight: 500, color: 'var(--ol-ink)' }}>
|
||||
{formatComboLabel(value)}
|
||||
</span>
|
||||
<div style={controlsGroupStyle}>
|
||||
@@ -195,7 +195,7 @@ export function ShortcutRecorder({
|
||||
tabIndex={-1}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
style={{ padding: '8px 12px', borderRadius: 'var(--ol-control-radius)', background: 'rgba(37,99,235,0.06)', border: '1px solid rgba(37,99,235,0.2)', fontSize: 12, color: 'var(--ol-blue)', outline: 'none' }}
|
||||
style={{ padding: '8px 12px', borderRadius: 8, background: 'rgba(37,99,235,0.06)', border: '1px solid rgba(37,99,235,0.2)', fontSize: 12, color: 'var(--ol-blue)', outline: 'none' }}
|
||||
ref={el => el?.focus()}
|
||||
>
|
||||
{t('settings.recording.comboRecordHint')}
|
||||
|
||||
@@ -22,7 +22,7 @@ export function Modal({ children, onClose, zIndex = 50, width = 'min(560px, 100%
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
background: 'var(--ol-overlay-bg)',
|
||||
background: 'rgba(0,0,0,0.22)',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
zIndex,
|
||||
@@ -36,10 +36,10 @@ export function Modal({ children, onClose, zIndex = 50, width = 'min(560px, 100%
|
||||
width,
|
||||
maxHeight: '85vh',
|
||||
overflow: 'auto',
|
||||
borderRadius: 'var(--ol-modal-radius)',
|
||||
background: 'var(--ol-card-bg)',
|
||||
border: '0.5px solid var(--ol-card-border)',
|
||||
boxShadow: 'var(--ol-shadow-lg)',
|
||||
borderRadius: 16,
|
||||
background: 'var(--ol-surface)',
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
boxShadow: '0 18px 42px rgba(0,0,0,0.18)',
|
||||
padding: 22,
|
||||
animation: 'ol-modal-card-in 0.24s var(--ol-motion-spring)',
|
||||
}}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Row — two-column row used in the Settings modal sub-sections.
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import { useMobileLayout } from '../../lib/useMobileLayout';
|
||||
|
||||
interface RowProps {
|
||||
label: string;
|
||||
@@ -10,14 +9,13 @@ interface RowProps {
|
||||
}
|
||||
|
||||
export function Row({ label, desc, children }: RowProps) {
|
||||
const mobile = useMobileLayout();
|
||||
return (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: mobile ? 'minmax(0, 1fr)' : '180px 1fr', gap: mobile ? 8 : 16, padding: '12px 0', borderTop: '0.5px solid var(--ol-line-soft)' }}>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '180px 1fr', gap: 16, padding: '12px 0', borderTop: '0.5px solid var(--ol-line-soft)' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, fontWeight: 500, color: 'var(--ol-ink)' }}>{label}</div>
|
||||
{desc && <div style={{ fontSize: 11.5, color: 'var(--ol-ink-4)', marginTop: 4, lineHeight: 1.5 }}>{desc}</div>}
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', minWidth: 0, width: '100%', maxWidth: '100%', flexWrap: mobile ? 'wrap' : 'nowrap', gap: mobile ? 6 : undefined }}>{children}</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,41 +1,32 @@
|
||||
// SegSimple — segmented control used in the Settings modal sub-sections.
|
||||
|
||||
export type SegOption = { value: string; label: string };
|
||||
import { useState } from 'react';
|
||||
|
||||
interface SegSimpleProps {
|
||||
options: SegOption[];
|
||||
value: string;
|
||||
onChange?: (value: string) => void;
|
||||
options: string[];
|
||||
active: string;
|
||||
}
|
||||
|
||||
export function SegSimple({ options, value, onChange }: SegSimpleProps) {
|
||||
export function SegSimple({ options, active }: SegSimpleProps) {
|
||||
const [v, setV] = useState(active);
|
||||
return (
|
||||
<div
|
||||
role="radiogroup"
|
||||
style={{ display: 'inline-flex', padding: 2, borderRadius: 'var(--ol-control-radius)', background: 'var(--ol-segmented-bg)' }}
|
||||
>
|
||||
{options.map((o) => {
|
||||
const selected = value === o.value;
|
||||
return (
|
||||
<div style={{ display: 'inline-flex', padding: 2, borderRadius: 8, background: 'rgba(0,0,0,0.05)' }}>
|
||||
{options.map((o) => (
|
||||
<button
|
||||
key={o.value}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={selected}
|
||||
onClick={() => onChange?.(o.value)}
|
||||
key={o}
|
||||
onClick={() => setV(o)}
|
||||
style={{
|
||||
padding: '5px 12px', fontSize: 12, fontWeight: 500, border: 0, borderRadius: 'var(--ol-r-sm)',
|
||||
padding: '5px 12px', fontSize: 12, fontWeight: 500, border: 0, borderRadius: 6,
|
||||
fontFamily: 'inherit',
|
||||
background: selected ? 'var(--ol-segmented-active-bg)' : 'transparent',
|
||||
color: selected ? 'var(--ol-ink)' : 'var(--ol-ink-3)',
|
||||
boxShadow: selected ? 'var(--ol-segmented-active-shadow)' : 'none',
|
||||
background: v === o ? '#fff' : 'transparent',
|
||||
color: v === o ? 'var(--ol-ink)' : 'var(--ol-ink-3)',
|
||||
boxShadow: v === o ? '0 1px 2px rgba(0,0,0,.08)' : 'none',
|
||||
cursor: 'default',
|
||||
}}
|
||||
>
|
||||
{o.label}
|
||||
{o}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
} from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Icon } from '../Icon';
|
||||
import { useMobileLayout } from '../../lib/useMobileLayout';
|
||||
|
||||
export interface SelectOption {
|
||||
value: string;
|
||||
@@ -60,7 +59,7 @@ const DEFAULT_TRIGGER_STYLE: CSSProperties = {
|
||||
height: 32,
|
||||
fontSize: 12.5,
|
||||
fontFamily: 'inherit',
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
borderRadius: 8,
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
background: 'var(--ol-surface-2)',
|
||||
color: 'var(--ol-ink)',
|
||||
@@ -82,7 +81,6 @@ export function SelectLite({
|
||||
ariaLabel,
|
||||
onOpenChange,
|
||||
}: SelectLiteProps) {
|
||||
const mobile = useMobileLayout();
|
||||
const [open, setOpen] = useState(false);
|
||||
// leaving 让 popover 在卸载前播完 exit keyframe(用户报"没有收缩动画"——之前直接 unmount)
|
||||
const [leaving, setLeaving] = useState(false);
|
||||
@@ -266,14 +264,6 @@ export function SelectLite({
|
||||
const triggerStyle: CSSProperties = {
|
||||
...DEFAULT_TRIGGER_STYLE,
|
||||
...style,
|
||||
boxSizing: 'border-box',
|
||||
...(mobile
|
||||
? {
|
||||
width: style?.width ?? '100%',
|
||||
minWidth: 0,
|
||||
maxWidth: '100%',
|
||||
}
|
||||
: null),
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
cursor: disabled ? 'not-allowed' : 'default',
|
||||
};
|
||||
@@ -322,12 +312,12 @@ export function SelectLite({
|
||||
maxHeight: 280,
|
||||
overflowY: 'auto',
|
||||
padding: 4,
|
||||
borderRadius: 'var(--ol-r-md)',
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
background: 'var(--ol-glass-bg-strong)',
|
||||
backdropFilter: 'blur(var(--ol-glass-blur)) saturate(180%)',
|
||||
WebkitBackdropFilter: 'blur(var(--ol-glass-blur)) saturate(180%)',
|
||||
boxShadow: 'var(--ol-shadow-md)',
|
||||
borderRadius: 10,
|
||||
border: '0.5px solid rgba(0, 0, 0, 0.10)',
|
||||
background: 'rgba(252, 252, 254, 0.94)',
|
||||
backdropFilter: 'blur(20px) saturate(180%)',
|
||||
WebkitBackdropFilter: 'blur(20px) saturate(180%)',
|
||||
boxShadow: '0 12px 30px -10px rgba(15, 17, 22, 0.25), 0 0 0 0.5px rgba(0, 0, 0, 0.06)',
|
||||
zIndex: 9999,
|
||||
fontFamily: 'inherit',
|
||||
fontSize: 12.5,
|
||||
@@ -357,11 +347,11 @@ export function SelectLite({
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
padding: '7px 10px',
|
||||
borderRadius: 'var(--ol-r-sm)',
|
||||
borderRadius: 6,
|
||||
cursor: option.disabled ? 'not-allowed' : 'default',
|
||||
opacity: option.disabled ? 0.45 : 1,
|
||||
background: isHighlighted && !option.disabled
|
||||
? 'var(--ol-blue-soft)'
|
||||
? 'rgba(37, 99, 235, 0.10)'
|
||||
: 'transparent',
|
||||
color: isSelected ? 'var(--ol-blue)' : 'var(--ol-ink)',
|
||||
fontWeight: isSelected ? 600 : 500,
|
||||
|
||||
@@ -14,8 +14,8 @@ export function SwitchLite({ on: initial = false }: SwitchLiteProps) {
|
||||
className="ol-focus-ring"
|
||||
onClick={() => setOn(!on)}
|
||||
style={{
|
||||
position: 'relative', width: 32, height: 18, borderRadius: 'var(--ol-pill-radius)', border: 0,
|
||||
background: on ? 'var(--ol-blue)' : 'var(--ol-toggle-off-bg)',
|
||||
position: 'relative', width: 32, height: 18, borderRadius: 999, border: 0,
|
||||
background: on ? 'var(--ol-blue)' : 'rgba(0,0,0,0.18)',
|
||||
cursor: 'default',
|
||||
outline: 'none',
|
||||
}}
|
||||
@@ -23,7 +23,7 @@ export function SwitchLite({ on: initial = false }: SwitchLiteProps) {
|
||||
<span
|
||||
style={{
|
||||
position: 'absolute', top: 2, left: on ? 16 : 2,
|
||||
width: 14, height: 14, borderRadius: 'var(--ol-pill-radius)', background: 'var(--ol-toggle-knob)',
|
||||
width: 14, height: 14, borderRadius: 999, background: '#fff',
|
||||
boxShadow: '0 1px 2px rgba(0,0,0,.25)', transition: 'left .16s var(--ol-motion-spring)',
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -750,7 +750,7 @@ export const en: typeof zhCN = {
|
||||
volcengineMappingNote: 'Secret Key is not required right now. Resource ID defaults to volc.seedasr.sauc.duration.',
|
||||
localAsrActiveNotice: 'Local ASR ({{name}}) is currently active. Switch or disable it from the Advanced tab.',
|
||||
localAsrTakeoverHint: 'Once "{{name}}" is enabled, the ASR provider will be taken over.',
|
||||
asrProviderTakenOver: 'ASR provider taken over',
|
||||
asrProviderTakenOver: 'A local engine is active. Pick another provider in the dropdown above to switch (the local engine stops automatically); manage local models under Advanced → Local models.',
|
||||
localAsrHint: 'Runs on this machine, no API key needed. Download the model from HuggingFace.',
|
||||
foundryLocalAsrHint: 'Runs on this device, no ASR API key needed. First use downloads runtime components and model.',
|
||||
localAsrPerformanceWarning: 'Local inference is slower than cloud ASR with potentially lower Chinese accuracy. Best for offline or privacy-sensitive use.',
|
||||
@@ -958,14 +958,6 @@ export const en: typeof zhCN = {
|
||||
ko: '한국어 (Beta)',
|
||||
restartHint: 'Some native menus (system tray, etc.) may require an app restart to fully switch.',
|
||||
},
|
||||
appearance: {
|
||||
title: 'Appearance',
|
||||
desc: 'Choose light or dark mode, or follow your system setting.',
|
||||
label: 'Theme',
|
||||
followSystem: 'Follow system',
|
||||
light: 'Light',
|
||||
dark: 'Dark',
|
||||
},
|
||||
remoteInput: {
|
||||
title: 'Remote Input',
|
||||
enableLabel: 'Enable remote input',
|
||||
|
||||
@@ -926,14 +926,6 @@ export const ja: typeof zhCN = {
|
||||
ko: '한국어 (Beta)',
|
||||
restartHint: '一部のネイティブメニュー(トレイ等)は再起動後に反映されます。',
|
||||
},
|
||||
appearance: {
|
||||
title: '外観',
|
||||
desc: 'ライト/ダークモードを選ぶか、システム設定に従います。',
|
||||
label: 'テーマ',
|
||||
followSystem: 'システムに従う',
|
||||
light: 'ライト',
|
||||
dark: 'ダーク',
|
||||
},
|
||||
remoteInput: {
|
||||
title: 'リモート入力',
|
||||
enableLabel: 'リモート入力を有効化',
|
||||
|
||||
@@ -926,14 +926,6 @@ export const ko: typeof zhCN = {
|
||||
ko: '한국어 (Beta)',
|
||||
restartHint: '일부 네이티브 메뉴(트레이 등)는 앱 재시작 후 반영될 수 있습니다.',
|
||||
},
|
||||
appearance: {
|
||||
title: '외관',
|
||||
desc: '라이트/다크 모드를 선택하거나 시스템 설정을 따릅니다.',
|
||||
label: '테마',
|
||||
followSystem: '시스템 따라가기',
|
||||
light: '라이트',
|
||||
dark: '다크',
|
||||
},
|
||||
remoteInput: {
|
||||
title: '원격 입력',
|
||||
enableLabel: '원격 입력 활성화',
|
||||
|
||||
@@ -748,7 +748,7 @@ export const zhCN = {
|
||||
volcengineMappingNote: 'Secret Key 当前无需填写。Resource ID 默认使用 volc.seedasr.sauc.duration。',
|
||||
localAsrActiveNotice: '当前已启用「{{name}}」,可在「高级」中切换或禁用。',
|
||||
localAsrTakeoverHint: '启动「{{name}}」后,ASR 提供商将被接管。',
|
||||
asrProviderTakenOver: 'ASR 提供商已被接管',
|
||||
asrProviderTakenOver: '当前用的是本地引擎,在上方下拉直接选其它供应商即可切换(本地引擎会自动停用);本地模型在「高级 → 本地模型」里管理。',
|
||||
localAsrHint: '在本机运行,无需 API Key。从 HuggingFace 下载模型即可使用。',
|
||||
foundryLocalAsrHint: '在本机运行,无需 ASR API Key。首次使用需下载运行组件和模型。',
|
||||
localAsrPerformanceWarning: '本地推理比云端慢,中文准确率可能更低。适合离线或隐私敏感场景。',
|
||||
@@ -956,14 +956,6 @@ export const zhCN = {
|
||||
ko: '한국어 (Beta)',
|
||||
restartHint: '部分原生菜单(系统托盘等)可能需要重启 App 才会切换。',
|
||||
},
|
||||
appearance: {
|
||||
title: '外观',
|
||||
desc: '选择浅色或深色模式,或跟随系统设置。',
|
||||
label: '主题',
|
||||
followSystem: '跟随系统',
|
||||
light: '浅色',
|
||||
dark: '深色',
|
||||
},
|
||||
remoteInput: {
|
||||
title: '远程输入',
|
||||
enableLabel: '启用远程输入',
|
||||
|
||||
@@ -924,14 +924,6 @@ export const zhTW: typeof zhCN = {
|
||||
ko: '한국어 (Beta)',
|
||||
restartHint: '部分原生菜單(系統托盤等)可能需要重啓 App 纔會切換。',
|
||||
},
|
||||
appearance: {
|
||||
title: '外觀',
|
||||
desc: '選擇淺色或深色模式,或跟隨系統設置。',
|
||||
label: '主題',
|
||||
followSystem: '跟隨系統',
|
||||
light: '淺色',
|
||||
dark: '深色',
|
||||
},
|
||||
remoteInput: {
|
||||
title: '遠端輸入',
|
||||
enableLabel: '啟用遠端輸入',
|
||||
|
||||
@@ -120,21 +120,3 @@ export async function getPlatformCapabilities(): Promise<PlatformCapabilities> {
|
||||
export function getCachedPlatformCapabilities(): PlatformCapabilities | null {
|
||||
return cachedCapabilities;
|
||||
}
|
||||
|
||||
/** Win11 原生标题栏暗色同步;非 Windows / 非 Tauri 为 no-op。 */
|
||||
export async function syncWindowsCaptionTheme(dark: boolean): Promise<void> {
|
||||
if (typeof window === 'undefined') return;
|
||||
if (detectOS() !== 'win') return;
|
||||
|
||||
const isTauri =
|
||||
globalThis.window !== undefined &&
|
||||
'__TAURI_INTERNALS__' in globalThis.window;
|
||||
if (!isTauri) return;
|
||||
|
||||
try {
|
||||
const { invoke } = await import('@tauri-apps/api/core');
|
||||
await invoke('set_windows_caption_theme', { dark });
|
||||
} catch (err) {
|
||||
console.warn('[platform] set_windows_caption_theme failed', err);
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
// 主题模式 — localStorage 持久化,通过 html[data-ol-theme] 切换暗色 token 集。
|
||||
|
||||
import { syncWindowsCaptionTheme } from './platform';
|
||||
|
||||
export type ThemeMode = 'system' | 'light' | 'dark';
|
||||
export type ResolvedThemeMode = 'light' | 'dark';
|
||||
|
||||
const THEME_MODE_KEY = 'ol-theme-mode';
|
||||
|
||||
function systemPrefersDark(): boolean {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
|
||||
export function resolveThemeMode(mode: ThemeMode): ResolvedThemeMode {
|
||||
if (mode === 'dark') return 'dark';
|
||||
if (mode === 'light') return 'light';
|
||||
return systemPrefersDark() ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
export function readThemeMode(): ThemeMode {
|
||||
try {
|
||||
const v = window.localStorage.getItem(THEME_MODE_KEY);
|
||||
if (v === 'system' || v === 'light' || v === 'dark') return v;
|
||||
} catch { /* localStorage 不可用:忽略,落回默认 */ }
|
||||
return 'system';
|
||||
}
|
||||
|
||||
export function applyThemeMode(mode: ThemeMode): void {
|
||||
const resolved = resolveThemeMode(mode);
|
||||
const root = document.documentElement;
|
||||
if (resolved === 'dark') {
|
||||
root.dataset.olTheme = 'dark';
|
||||
} else {
|
||||
delete root.dataset.olTheme;
|
||||
}
|
||||
void syncWindowsCaptionTheme(resolved === 'dark');
|
||||
}
|
||||
|
||||
export function setThemeMode(mode: ThemeMode): ThemeMode {
|
||||
try { window.localStorage.setItem(THEME_MODE_KEY, mode); } catch { /* 忽略 */ }
|
||||
applyThemeMode(mode);
|
||||
return mode;
|
||||
}
|
||||
|
||||
export function subscribeThemeMode(): () => void {
|
||||
const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const onChange = () => {
|
||||
if (readThemeMode() === 'system') applyThemeMode('system');
|
||||
};
|
||||
mq.addEventListener('change', onChange);
|
||||
return () => mq.removeEventListener('change', onChange);
|
||||
}
|
||||
@@ -3,14 +3,9 @@ import ReactDOM from "react-dom/client";
|
||||
import { App } from "./App";
|
||||
import { detectOS } from "./components/WindowChrome";
|
||||
import i18n from "./i18n"; // 副作用:触发 i18next init
|
||||
import { applyThemeMode, readThemeMode, subscribeThemeMode } from "./lib/themeMode";
|
||||
import "./styles/tokens.css";
|
||||
import "./styles/global.css";
|
||||
|
||||
// 首帧前应用主题,避免暗色偏好下先闪一帧亮色。
|
||||
applyThemeMode(readThemeMode());
|
||||
subscribeThemeMode();
|
||||
|
||||
import type { OS } from "./components/WindowChrome";
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
// History.tsx — 接 Tauri 后端 list_history / delete_history_entry / clear_history。
|
||||
// 真实数据来自 ~/Library/Application Support/OpenLess/history.json。
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Icon } from '../components/Icon';
|
||||
import { detectOS } from '../components/WindowChrome';
|
||||
import { formatComboLabel } from '../lib/hotkey';
|
||||
import { clearHistory, deleteHistoryEntry, listHistory, readAudioRecording, retranscribeRecording } from '../lib/ipc';
|
||||
import { clearHistory, deleteHistoryEntry, listHistory, readAudioRecording } from '../lib/ipc';
|
||||
import type { DictationSession, PolishMode } from '../lib/types';
|
||||
import { useHotkeySettings } from '../state/HotkeySettingsContext';
|
||||
import { Btn, Card, PageHeader, Pill } from './_atoms';
|
||||
import { useMobileLayout } from '../lib/useMobileLayout';
|
||||
|
||||
function useFilters(): Array<{ id: 'all' | PolishMode; label: string }> {
|
||||
const { t } = useTranslation();
|
||||
@@ -36,21 +35,15 @@ function useModeLabel(): Record<PolishMode, string> {
|
||||
export function History() {
|
||||
const { t } = useTranslation();
|
||||
const os = detectOS();
|
||||
const mobile = useMobileLayout();
|
||||
const FILTERS = useFilters();
|
||||
const MODE_LABEL = useModeLabel();
|
||||
const [filter, setFilter] = useState<'all' | PolishMode>('all');
|
||||
// issue #612:历史页顶部原为静态 div,只显示统计、不可输入。改为真实搜索框。
|
||||
const [query, setQuery] = useState('');
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
const [items, setItems] = useState<DictationSession[]>([]);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
const [actionError, setActionError] = useState<string | null>(null);
|
||||
const [justCopied, setJustCopied] = useState(false);
|
||||
// issue #613:「重新转录」进行中的条目 id(按钮转 loading、防重复点击)。
|
||||
const [retranscribingId, setRetranscribingId] = useState<string | null>(null);
|
||||
// 录音文件 lazily-detected missing 状态:retention / 条数 cap 清理后磁盘上 wav
|
||||
// 可能已被删,但 history 条目 hasAudioRecording 仍写 true。任一组件
|
||||
// (播放 / 导出)首次 IPC 拿到 'recording not found' 时把 id 加进来,
|
||||
@@ -87,29 +80,10 @@ export function History() {
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
|
||||
// ⌘K / Ctrl+K 聚焦搜索框(issue #612 验收可选项)。
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if ((e.metaKey || e.ctrlKey) && (e.key === 'k' || e.key === 'K')) {
|
||||
e.preventDefault();
|
||||
searchRef.current?.focus();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, []);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const byMode = filter === 'all' ? items : items.filter(s => s.mode === filter);
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) return byMode;
|
||||
return byMode.filter(
|
||||
s =>
|
||||
s.finalText.toLowerCase().includes(q) ||
|
||||
s.rawTranscript.toLowerCase().includes(q) ||
|
||||
(s.appName ?? '').toLowerCase().includes(q),
|
||||
);
|
||||
}, [items, filter, query]);
|
||||
const filtered = useMemo(
|
||||
() => (filter === 'all' ? items : items.filter(s => s.mode === filter)),
|
||||
[items, filter],
|
||||
);
|
||||
const item = useMemo(
|
||||
() => filtered.find(s => s.id === selectedId) || filtered[0],
|
||||
[filtered, selectedId],
|
||||
@@ -189,28 +163,6 @@ export function History() {
|
||||
}
|
||||
};
|
||||
|
||||
// issue #613:对失败条目的归档录音用当前 provider 重新转录,成功后局部刷新该条。
|
||||
const onRetranscribe = async () => {
|
||||
if (!item || retranscribingId) return;
|
||||
const targetId = item.id;
|
||||
setRetranscribingId(targetId);
|
||||
setActionError(null);
|
||||
try {
|
||||
const updated = await retranscribeRecording(targetId);
|
||||
setItems(prev => prev.map(s => (s.id === targetId ? updated : s)));
|
||||
} catch (error) {
|
||||
console.error('[history] failed to retranscribe recording', error);
|
||||
const msg = errorMessage(error);
|
||||
// wav 已被清理:隐藏录音相关操作,不当作用户错误。
|
||||
if (msg.includes('recording not found') || msg.includes('not found')) {
|
||||
markAudioMissing(targetId);
|
||||
}
|
||||
setActionError(t('history.retranscribeFailed', { err: msg }));
|
||||
} finally {
|
||||
setRetranscribingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0 }}>
|
||||
<PageHeader
|
||||
@@ -224,39 +176,17 @@ export function History() {
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: mobile ? '1fr' : '300px 1fr',
|
||||
gap: 14,
|
||||
flex: mobile ? '0 0 auto' : 1,
|
||||
minHeight: mobile ? undefined : 0,
|
||||
}}
|
||||
>
|
||||
<Card padding={0} style={{ display: 'flex', flexDirection: 'column', overflow: 'hidden', maxHeight: mobile ? 360 : undefined }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '300px 1fr', gap: 14, flex: 1, minHeight: 0 }}>
|
||||
<Card padding={0} style={{ display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||
<div style={{ padding: '12px 14px', borderBottom: '0.5px solid var(--ol-line)' }}>
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '6px 10px',
|
||||
border: '0.5px solid var(--ol-line-strong)', borderRadius: 'var(--ol-control-radius)',
|
||||
padding: '6px 10px', fontSize: 12,
|
||||
border: '0.5px solid var(--ol-line-strong)', borderRadius: 8,
|
||||
background: 'var(--ol-surface-2)', color: 'var(--ol-ink-3)',
|
||||
}}>
|
||||
<Icon name="search" size={12} />
|
||||
<input
|
||||
ref={searchRef}
|
||||
type="search"
|
||||
value={query}
|
||||
onChange={e => setQuery(e.target.value)}
|
||||
placeholder={t('history.searchPlaceholder', { shortcut: os === 'mac' ? '⌘K' : 'Ctrl+K' })}
|
||||
style={{
|
||||
flex: 1, minWidth: 0, outline: 'none', border: 0,
|
||||
background: 'transparent', fontSize: 12, color: 'var(--ol-ink-2)',
|
||||
fontFamily: 'inherit',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginTop: 8, fontSize: 11, color: 'var(--ol-ink-4)' }}>
|
||||
{t('history.summary', { total: items.length, shown: filtered.length })}
|
||||
<span style={{ flex: 1 }}>{t('history.summary', { total: items.length, shown: filtered.length })}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap', marginTop: 10 }}>
|
||||
{FILTERS.map(f => (
|
||||
@@ -264,10 +194,10 @@ export function History() {
|
||||
key={f.id}
|
||||
onClick={() => setFilter(f.id)}
|
||||
style={{
|
||||
padding: '3px 9px', fontSize: 11, borderRadius: 'var(--ol-pill-radius)',
|
||||
border: '0.5px solid ' + (filter === f.id ? 'var(--ol-pill-selected-border)' : 'var(--ol-line-strong)'),
|
||||
background: filter === f.id ? 'var(--ol-pill-selected-bg)' : 'transparent',
|
||||
color: filter === f.id ? 'var(--ol-pill-selected-ink)' : 'var(--ol-ink-3)',
|
||||
padding: '3px 9px', fontSize: 11, borderRadius: 999,
|
||||
border: '0.5px solid ' + (filter === f.id ? 'var(--ol-ink)' : 'var(--ol-line-strong)'),
|
||||
background: filter === f.id ? 'var(--ol-ink)' : 'transparent',
|
||||
color: filter === f.id ? '#fff' : 'var(--ol-ink-3)',
|
||||
cursor: 'default', fontFamily: 'inherit', fontWeight: 500,
|
||||
transition: 'background 0.16s var(--ol-motion-quick), color 0.16s var(--ol-motion-quick), border-color 0.16s var(--ol-motion-quick)',
|
||||
}}
|
||||
@@ -277,7 +207,7 @@ export function History() {
|
||||
</div>
|
||||
<div className="ol-thinscroll" style={{ flex: 1, overflow: 'auto', padding: 6 }}>
|
||||
{actionError && (
|
||||
<div style={{ margin: 8, padding: '9px 10px', borderRadius: 'var(--ol-control-radius)', background: 'rgba(239,68,68,0.08)', color: 'var(--ol-red, #ef4444)', fontSize: 12, lineHeight: 1.45 }}>
|
||||
<div style={{ margin: 8, padding: '9px 10px', borderRadius: 8, background: 'rgba(239,68,68,0.08)', color: 'var(--ol-red, #ef4444)', fontSize: 12, lineHeight: 1.45 }}>
|
||||
{actionError}
|
||||
</div>
|
||||
)}
|
||||
@@ -290,9 +220,7 @@ export function History() {
|
||||
)}
|
||||
{!loading && !loadError && filtered.length === 0 && (
|
||||
<div style={{ padding: 16, fontSize: 12, color: 'var(--ol-ink-4)' }}>
|
||||
{query.trim()
|
||||
? t('history.searchNoMatch', { query: query.trim() })
|
||||
: t('history.empty', { trigger: prefs ? formatComboLabel(prefs.dictationHotkey) : '' })}
|
||||
{t('history.empty', { trigger: prefs ? formatComboLabel(prefs.dictationHotkey) : '' })}
|
||||
</div>
|
||||
)}
|
||||
{!loadError && filtered.map(s => (
|
||||
@@ -302,7 +230,7 @@ export function History() {
|
||||
style={{
|
||||
width: '100%', padding: '10px 12px', textAlign: 'left',
|
||||
display: 'flex', flexDirection: 'column', gap: 4,
|
||||
border: 0, borderRadius: 'var(--ol-control-radius)',
|
||||
border: 0, borderRadius: 8,
|
||||
background: selectedId === s.id ? 'rgba(37,99,235,0.06)' : 'transparent',
|
||||
boxShadow: selectedId === s.id ? 'inset 2px 0 0 var(--ol-blue)' : 'none',
|
||||
cursor: 'default', fontFamily: 'inherit', marginBottom: 1,
|
||||
@@ -326,29 +254,17 @@ export function History() {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card padding={mobile ? 16 : 20} className="ol-thinscroll" style={{ overflow: mobile ? 'visible' : 'auto' }}>
|
||||
<Card padding={20} className="ol-thinscroll" style={{ overflow: 'auto' }}>
|
||||
{item ? (
|
||||
<>
|
||||
<div style={{ display: 'flex', alignItems: mobile ? 'flex-start' : 'center', justifyContent: 'space-between', marginBottom: 14, gap: 10, flexDirection: mobile ? 'column' : 'row' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 14 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<span style={{ fontSize: 13, fontFamily: 'var(--ol-font-mono)', color: 'var(--ol-ink-3)' }}>{formatTime(item.createdAt)}</span>
|
||||
<Pill size="sm" tone="default">{MODE_LABEL[item.mode]}</Pill>
|
||||
<span style={{ fontSize: 11, color: 'var(--ol-ink-4)' }}>{formatDuration(item.durationMs, t)}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
<Btn icon={justCopied ? 'check' : 'copy'} variant="ghost" size="sm" onClick={() => void onCopy()}>{justCopied ? t('common.copied') : t('common.copy')}</Btn>
|
||||
{/* issue #613:失败条目(有错误码)且录音仍在时,提供「重新转录」。 */}
|
||||
{item.errorCode && item.hasAudioRecording && !audioMissingIds.has(item.id) && (
|
||||
<Btn
|
||||
icon="refresh"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={retranscribingId === item.id}
|
||||
onClick={() => void onRetranscribe()}
|
||||
>
|
||||
{retranscribingId === item.id ? t('history.retranscribing') : t('history.retranscribe')}
|
||||
</Btn>
|
||||
)}
|
||||
{item.hasAudioRecording && !audioMissingIds.has(item.id) && (
|
||||
<Btn icon="download" variant="ghost" size="sm" onClick={() => void onExportAudio()}>{t('history.exportRecording')}</Btn>
|
||||
)}
|
||||
@@ -362,14 +278,14 @@ export function History() {
|
||||
key={item.id}
|
||||
/>
|
||||
)}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: mobile ? '1fr' : '1fr 1fr', gap: 12 }}>
|
||||
<div style={{ padding: 14, border: '0.5px solid var(--ol-line)', borderRadius: 'var(--ol-r-md)', background: 'var(--ol-surface-2)' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
|
||||
<div style={{ padding: 14, border: '0.5px solid var(--ol-line)', borderRadius: 10, background: 'var(--ol-surface-2)' }}>
|
||||
<Pill size="sm" tone="outline" style={{ marginBottom: 10 }}>{t('history.rawLabel')}</Pill>
|
||||
<p style={{ margin: 0, fontSize: 13, lineHeight: 1.7, color: 'var(--ol-ink-2)', whiteSpace: 'pre-wrap' }}>
|
||||
{item.rawTranscript || t('history.rawEmpty')}
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ padding: 14, border: '0.5px solid var(--ol-blue)', borderRadius: 'var(--ol-r-md)', background: 'var(--ol-blue-soft)' }}>
|
||||
<div style={{ padding: 14, border: '0.5px solid var(--ol-blue)', borderRadius: 10, background: 'var(--ol-blue-soft)' }}>
|
||||
<Pill size="sm" tone="blue" style={{ marginBottom: 10 }}>{MODE_LABEL[item.mode]}</Pill>
|
||||
<p style={{ margin: 0, fontSize: 13, lineHeight: 1.7, color: 'var(--ol-ink)', whiteSpace: 'pre-line' }}>
|
||||
{item.finalText}
|
||||
|
||||
@@ -414,11 +414,11 @@ const shellStyle: CSSProperties = {
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
borderRadius: 'var(--ol-bubble-radius)',
|
||||
borderRadius: 14,
|
||||
overflow: 'hidden',
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
background: 'var(--ol-panel-bg)',
|
||||
boxShadow: 'var(--ol-shadow-lg)',
|
||||
border: '0.5px solid rgba(0, 0, 0, 0.12)',
|
||||
background: 'rgba(246, 247, 250, 0.88)',
|
||||
boxShadow: '0 18px 44px -18px rgba(15,17,22,.28), 0 0 0 0.5px rgba(255,255,255,.7) inset',
|
||||
fontFamily: 'var(--ol-font-sans)',
|
||||
color: 'var(--ol-ink)',
|
||||
isolation: 'isolate',
|
||||
@@ -430,8 +430,9 @@ const toolbarStyle: CSSProperties = {
|
||||
alignItems: 'center',
|
||||
padding: '0 8px',
|
||||
borderBottom: '0.5px solid rgba(0, 0, 0, 0.08)',
|
||||
background: 'var(--ol-panel-bg)',
|
||||
boxShadow: 'var(--ol-shadow-sm)',
|
||||
background:
|
||||
'linear-gradient(180deg, rgba(255,255,255,0.74), rgba(238,240,245,0.58))',
|
||||
boxShadow: '0 1px 0 rgba(255,255,255,.55) inset',
|
||||
backdropFilter: 'blur(18px) saturate(150%)',
|
||||
WebkitBackdropFilter: 'blur(18px) saturate(150%)',
|
||||
flexShrink: 0,
|
||||
@@ -443,7 +444,7 @@ const closeBtnStyle: CSSProperties = {
|
||||
width: 22,
|
||||
height: 22,
|
||||
border: 0,
|
||||
borderRadius: 'var(--ol-r-sm)',
|
||||
borderRadius: 6,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -478,10 +479,10 @@ const roleLabelStyle: CSSProperties = {
|
||||
const userBubbleStyle: CSSProperties = {
|
||||
maxWidth: '85%',
|
||||
padding: '8px 12px',
|
||||
borderRadius: 'var(--ol-bubble-radius)',
|
||||
borderRadius: 14,
|
||||
borderBottomRightRadius: 4,
|
||||
background: 'var(--ol-accent-solid-bg)',
|
||||
color: 'var(--ol-accent-solid-ink)',
|
||||
background: 'var(--ol-blue)',
|
||||
color: '#fff',
|
||||
fontSize: 13,
|
||||
lineHeight: 1.55,
|
||||
wordBreak: 'break-word',
|
||||
@@ -490,7 +491,7 @@ const userBubbleStyle: CSSProperties = {
|
||||
const assistantBubbleStyle: CSSProperties = {
|
||||
maxWidth: '92%',
|
||||
padding: '8px 12px',
|
||||
borderRadius: 'var(--ol-bubble-radius)',
|
||||
borderRadius: 14,
|
||||
borderBottomLeftRadius: 4,
|
||||
background: 'rgba(0,0,0,0.04)',
|
||||
fontSize: 13,
|
||||
@@ -518,7 +519,7 @@ const toolChipStyle: CSSProperties = {
|
||||
color: 'var(--ol-ink-2)',
|
||||
background: 'rgba(0,0,0,0.045)',
|
||||
border: '0.5px solid rgba(0,0,0,0.06)',
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
borderRadius: 8,
|
||||
padding: '4px 8px',
|
||||
};
|
||||
|
||||
@@ -542,7 +543,7 @@ const approvalCardStyle: CSSProperties = {
|
||||
flexDirection: 'column',
|
||||
gap: 6,
|
||||
padding: '10px 12px',
|
||||
borderRadius: 'var(--ol-r-lg)',
|
||||
borderRadius: 12,
|
||||
background: 'rgba(220,38,38,0.05)',
|
||||
border: '0.5px solid rgba(220,38,38,0.20)',
|
||||
};
|
||||
@@ -552,7 +553,7 @@ const approvalCmdStyle: CSSProperties = {
|
||||
fontSize: 11.5,
|
||||
color: 'var(--ol-ink)',
|
||||
background: 'rgba(0,0,0,0.05)',
|
||||
borderRadius: 'var(--ol-r-sm)',
|
||||
borderRadius: 6,
|
||||
padding: '5px 8px',
|
||||
wordBreak: 'break-all',
|
||||
};
|
||||
@@ -563,25 +564,25 @@ const approvalRerunWarningStyle: CSSProperties = {
|
||||
color: 'rgb(180,83,9)',
|
||||
background: 'rgba(245,158,11,0.10)',
|
||||
border: '0.5px solid rgba(245,158,11,0.25)',
|
||||
borderRadius: 'var(--ol-r-sm)',
|
||||
borderRadius: 6,
|
||||
padding: '5px 8px',
|
||||
};
|
||||
|
||||
const approveBtnStyle: CSSProperties = {
|
||||
flex: 1,
|
||||
border: 0,
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
borderRadius: 8,
|
||||
padding: '6px 10px',
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
cursor: 'default',
|
||||
background: 'var(--ol-accent-solid-bg)',
|
||||
color: 'var(--ol-accent-solid-ink)',
|
||||
background: 'var(--ol-blue)',
|
||||
color: '#fff',
|
||||
};
|
||||
|
||||
const denyBtnStyle: CSSProperties = {
|
||||
flex: 1,
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
borderRadius: 8,
|
||||
padding: '6px 10px',
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
@@ -593,7 +594,7 @@ const denyBtnStyle: CSSProperties = {
|
||||
|
||||
const errorRowStyle: CSSProperties = {
|
||||
padding: '8px 12px',
|
||||
borderRadius: 'var(--ol-r-md)',
|
||||
borderRadius: 10,
|
||||
background: 'rgba(220,38,38,0.06)',
|
||||
border: '0.5px solid rgba(220,38,38,0.18)',
|
||||
};
|
||||
@@ -632,7 +633,7 @@ const globalCss = `
|
||||
padding: 1px 5px; border-radius: 4px;
|
||||
background: rgba(0,0,0,0.05); }
|
||||
.lc-answer pre { margin: 0 0 6px; padding: 8px 10px;
|
||||
border-radius: var(--ol-control-radius); background: rgba(0,0,0,0.05);
|
||||
border-radius: 8px; background: rgba(0,0,0,0.05);
|
||||
overflow-x: auto; }
|
||||
.lc-answer pre code { padding: 0; background: transparent; }
|
||||
.lc-answer a { color: var(--ol-blue); text-decoration: none; }
|
||||
|
||||
@@ -1842,7 +1842,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) {
|
||||
style={{
|
||||
fontSize: 13,
|
||||
padding: "6px 10px",
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
borderRadius: 8,
|
||||
border: "0.5px solid rgba(0,0,0,0.12)",
|
||||
background: "var(--ol-surface)",
|
||||
color: "var(--ol-ink)",
|
||||
@@ -1903,7 +1903,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) {
|
||||
style={{
|
||||
fontSize: 13,
|
||||
padding: "6px 10px",
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
borderRadius: 8,
|
||||
border: "0.5px solid rgba(0,0,0,0.12)",
|
||||
background: "var(--ol-surface)",
|
||||
color: "var(--ol-ink)",
|
||||
@@ -1954,7 +1954,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) {
|
||||
style={{
|
||||
fontSize: 13,
|
||||
padding: "6px 10px",
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
borderRadius: 8,
|
||||
border: "0.5px solid rgba(0,0,0,0.12)",
|
||||
background: "var(--ol-surface)",
|
||||
color: "var(--ol-ink)",
|
||||
@@ -2266,7 +2266,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) {
|
||||
fontSize: 13,
|
||||
height: 31,
|
||||
padding: "0 10px",
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
borderRadius: 8,
|
||||
border: "0.5px solid rgba(0,0,0,0.12)",
|
||||
background: "var(--ol-surface)",
|
||||
color: "var(--ol-ink)",
|
||||
@@ -2301,7 +2301,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) {
|
||||
fontSize: 13,
|
||||
height: 31,
|
||||
padding: "0 10px",
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
borderRadius: 8,
|
||||
border: "0.5px solid rgba(0,0,0,0.12)",
|
||||
background: "var(--ol-surface)",
|
||||
color: "var(--ol-ink)",
|
||||
@@ -2334,7 +2334,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) {
|
||||
fontSize: 13,
|
||||
height: 31,
|
||||
padding: "0 10px",
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
borderRadius: 8,
|
||||
border: "0.5px solid rgba(0,0,0,0.12)",
|
||||
background: "var(--ol-surface)",
|
||||
color: "var(--ol-ink)",
|
||||
@@ -2613,7 +2613,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) {
|
||||
style={{
|
||||
fontSize: 13,
|
||||
padding: "6px 10px",
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
borderRadius: 8,
|
||||
border: "0.5px solid rgba(0,0,0,0.12)",
|
||||
background: "var(--ol-surface)",
|
||||
color: "var(--ol-ink)",
|
||||
@@ -2741,7 +2741,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) {
|
||||
style={{
|
||||
fontSize: 13,
|
||||
padding: "6px 10px",
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
borderRadius: 8,
|
||||
border: "0.5px solid rgba(0,0,0,0.12)",
|
||||
background: "var(--ol-surface)",
|
||||
color: "var(--ol-ink)",
|
||||
@@ -2905,7 +2905,7 @@ function FoundryPrepareProgressBlock({
|
||||
<div
|
||||
style={{
|
||||
padding: "10px 12px",
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
borderRadius: 8,
|
||||
background: "rgba(0,0,0,0.035)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
@@ -3040,7 +3040,7 @@ function DownloadProgressBlock({
|
||||
<div
|
||||
style={{
|
||||
padding: "10px 12px",
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
borderRadius: 8,
|
||||
background: "rgba(0,0,0,0.035)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
@@ -3367,7 +3367,7 @@ function TestResultBlock({
|
||||
background: hasError
|
||||
? "rgba(255, 220, 220, 0.5)"
|
||||
: "rgba(0, 0, 0, 0.04)",
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
borderRadius: 8,
|
||||
fontSize: 12.5,
|
||||
color: hasError ? "#9b2c2c" : "var(--ol-ink-2)",
|
||||
lineHeight: 1.6,
|
||||
|
||||
@@ -426,7 +426,7 @@ export function Marketplace() {
|
||||
title={currentLogin ? t('marketplace.myPacks.buttonTitle', { login: currentLogin }) : t('marketplace.myPacks.buttonTitleEmpty')}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 8,
|
||||
height: 30, padding: '0 12px', borderRadius: 'var(--ol-control-radius)',
|
||||
height: 30, padding: '0 12px', borderRadius: 9,
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
background: 'var(--ol-surface)',
|
||||
color: 'var(--ol-ink-2)',
|
||||
@@ -436,7 +436,7 @@ export function Marketplace() {
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
width: 18, height: 18, borderRadius: 'var(--ol-pill-radius)',
|
||||
width: 18, height: 18, borderRadius: 999,
|
||||
display: 'inline-grid', placeItems: 'center',
|
||||
background: 'rgba(15,23,42,0.06)',
|
||||
fontSize: 10, fontWeight: 750,
|
||||
@@ -469,7 +469,7 @@ export function Marketplace() {
|
||||
gap: 6,
|
||||
padding: '6px 10px',
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
borderRadius: 'var(--ol-r-md)',
|
||||
borderRadius: 10,
|
||||
background: 'var(--ol-surface)',
|
||||
}}
|
||||
>
|
||||
@@ -498,7 +498,7 @@ export function Marketplace() {
|
||||
padding: '6px 10px',
|
||||
fontSize: 12,
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
borderRadius: 8,
|
||||
cursor: 'pointer',
|
||||
background: sort === p.id ? 'var(--ol-blue-soft)' : 'var(--ol-surface)',
|
||||
color: sort === p.id ? 'var(--ol-blue)' : 'var(--ol-ink-2)',
|
||||
@@ -567,7 +567,7 @@ export function Marketplace() {
|
||||
style={{
|
||||
textAlign: 'left',
|
||||
padding: 14,
|
||||
borderRadius: 'var(--ol-r-lg)',
|
||||
borderRadius: 12,
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
background: 'var(--ol-surface)',
|
||||
cursor: 'pointer',
|
||||
@@ -644,7 +644,7 @@ export function Marketplace() {
|
||||
style={{
|
||||
padding: 12,
|
||||
border: '0.5px solid var(--ol-line)',
|
||||
borderRadius: 'var(--ol-r-md)',
|
||||
borderRadius: 10,
|
||||
background: 'var(--ol-surface-2)',
|
||||
marginBottom: 14,
|
||||
maxHeight: 280,
|
||||
@@ -679,7 +679,7 @@ export function Marketplace() {
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
padding: '4px 8px',
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
borderRadius: 8,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
color: 'var(--ol-ink-2)'
|
||||
@@ -744,7 +744,7 @@ export function Marketplace() {
|
||||
textAlign: 'left',
|
||||
padding: 10,
|
||||
border: selected ? '1px solid var(--ol-blue)' : '0.5px solid var(--ol-line-strong)',
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
borderRadius: 8,
|
||||
background: selected ? 'var(--ol-blue-soft)' : 'var(--ol-surface)',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
@@ -755,11 +755,11 @@ export function Marketplace() {
|
||||
{/* 选中圈:未选空圆,选中蓝实心 + 白勾 */}
|
||||
<span style={{
|
||||
flexShrink: 0,
|
||||
width: 18, height: 18, borderRadius: 'var(--ol-pill-radius)',
|
||||
border: selected ? '1px solid var(--ol-accent-solid-bg)' : '1px solid var(--ol-line-strong)',
|
||||
background: selected ? 'var(--ol-accent-solid-bg)' : 'transparent',
|
||||
width: 18, height: 18, borderRadius: 999,
|
||||
border: selected ? '1px solid var(--ol-blue)' : '1px solid var(--ol-line-strong)',
|
||||
background: selected ? 'var(--ol-blue)' : 'transparent',
|
||||
display: 'inline-grid', placeItems: 'center',
|
||||
color: 'var(--ol-accent-solid-ink)', fontSize: 11, fontWeight: 700,
|
||||
color: '#fff', fontSize: 11, fontWeight: 700,
|
||||
transition: 'background 0.12s, border-color 0.12s',
|
||||
}}>
|
||||
{selected && '✓'}
|
||||
@@ -814,7 +814,7 @@ export function Marketplace() {
|
||||
gap: 6,
|
||||
padding: '6px 10px',
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
borderRadius: 'var(--ol-r-md)',
|
||||
borderRadius: 10,
|
||||
background: 'var(--ol-surface)',
|
||||
}}
|
||||
>
|
||||
@@ -843,7 +843,7 @@ export function Marketplace() {
|
||||
onClick={() => setShowLogin(true)}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
padding: '5px 10px', borderRadius: 'var(--ol-control-radius)',
|
||||
padding: '5px 10px', borderRadius: 9,
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
background: currentLogin ? 'var(--ol-blue-soft)' : 'var(--ol-surface)',
|
||||
color: currentLogin ? 'var(--ol-blue)' : 'var(--ol-ink-3)',
|
||||
@@ -853,7 +853,7 @@ export function Marketplace() {
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
width: 18, height: 18, borderRadius: 'var(--ol-pill-radius)',
|
||||
width: 18, height: 18, borderRadius: 999,
|
||||
display: 'inline-grid', placeItems: 'center',
|
||||
background: currentLogin ? 'rgba(37,99,235,0.14)' : 'rgba(15,23,42,0.06)',
|
||||
fontSize: 10, fontWeight: 750,
|
||||
@@ -869,7 +869,7 @@ export function Marketplace() {
|
||||
title={t('common.close')}
|
||||
onClick={() => setShowMyPacks(false)}
|
||||
style={{
|
||||
width: 30, height: 30, borderRadius: 'var(--ol-control-radius)',
|
||||
width: 30, height: 30, borderRadius: 9,
|
||||
display: 'inline-grid', placeItems: 'center',
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
background: 'var(--ol-surface)',
|
||||
@@ -965,7 +965,7 @@ export function Marketplace() {
|
||||
key={pack.id}
|
||||
style={{
|
||||
padding: 14,
|
||||
borderRadius: 'var(--ol-r-lg)',
|
||||
borderRadius: 12,
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
background: 'var(--ol-surface)',
|
||||
display: 'flex',
|
||||
|
||||
@@ -4,27 +4,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Icon } from '../components/Icon';
|
||||
import { formatComboLabel } from '../lib/hotkey';
|
||||
import {
|
||||
checkMicrophonePermission,
|
||||
getCredentials,
|
||||
getPlatformCapabilities,
|
||||
listHistory,
|
||||
startDictation,
|
||||
stopDictation,
|
||||
} from '../lib/ipc';
|
||||
import type {
|
||||
CapsulePayload,
|
||||
CapsuleState,
|
||||
CredentialsStatus,
|
||||
DictationSession,
|
||||
PermissionStatus,
|
||||
PlatformCapabilities,
|
||||
PolishMode,
|
||||
} from '../lib/types';
|
||||
import { getCredentials, listHistory } from '../lib/ipc';
|
||||
import type { CredentialsStatus, DictationSession, PolishMode } from '../lib/types';
|
||||
import { useHotkeySettings } from '../state/HotkeySettingsContext';
|
||||
import { Btn, Card, PageHeader, Pill } from './_atoms';
|
||||
import { useMobileLayout } from '../lib/useMobileLayout';
|
||||
import { checkAndroidMicrophoneAccess, requestAndroidMicrophoneAccess } from '../lib/androidMicrophonePermission';
|
||||
|
||||
function useModeLabels(): Record<PolishMode, string> {
|
||||
const { t } = useTranslation();
|
||||
@@ -70,7 +53,6 @@ const LLM_NAME_KEY_BY_ID: Record<string, string> = {
|
||||
|
||||
export function Overview({ onOpenHistory }: OverviewProps) {
|
||||
const { t } = useTranslation();
|
||||
const mobile = useMobileLayout();
|
||||
const modeLabel = useModeLabels();
|
||||
const [history, setHistory] = useState<DictationSession[]>([]);
|
||||
const [historyError, setHistoryError] = useState(false);
|
||||
@@ -188,19 +170,9 @@ export function Overview({ onOpenHistory }: OverviewProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
kicker="AURA OVERVIEW"
|
||||
title={t('overview.title')}
|
||||
desc={t('overview.metricTotalTrend')}
|
||||
/>
|
||||
<PageHeader title={t('overview.title')} />
|
||||
|
||||
<AndroidMicGrantBanner />
|
||||
<InAppDictationControl />
|
||||
|
||||
<div
|
||||
className="ol-overview-hero"
|
||||
style={{ display: 'grid', gridTemplateColumns: mobile ? '1fr' : '1fr 1fr', gap: 14, marginBottom: mobile ? 14 : 20 }}
|
||||
>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 18 }}>
|
||||
<ProviderCard
|
||||
kind={t('overview.asrKind')}
|
||||
name={asrProviderName}
|
||||
@@ -215,7 +187,7 @@ export function Overview({ onOpenHistory }: OverviewProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: mobile ? 'repeat(2, minmax(0, 1fr))' : 'repeat(4, 1fr)', gap: mobile ? 10 : 14, marginBottom: mobile ? 14 : 20 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12, marginBottom: 18 }}>
|
||||
<Metric icon="hash" label={t('overview.metricChars')} value={historyError ? '—' : metrics.charsToday.toLocaleString()} trend={historyError ? t('overview.historyLoadError') : t('overview.metricSegments', { count: metrics.segmentsToday })} />
|
||||
<Metric icon="mic" label={t('overview.metricDuration')} value={historyError ? '—' : formatDuration(metrics.totalDurationMs, t)} trend={historyError ? t('overview.historyLoadError') : ''} />
|
||||
<Metric icon="clock" label={t('overview.metricAvg')} value={historyError ? '—' : formatDuration(metrics.avgLatencyMs, t)} trend={historyError ? t('overview.historyLoadError') : metrics.segmentsToday > 0 ? t('overview.metricAvgTrend') : t('overview.metricNoData')} />
|
||||
@@ -225,8 +197,8 @@ export function Overview({ onOpenHistory }: OverviewProps) {
|
||||
{/* 底部一行 = flex:1 撑满剩余高度(父 wrapper 是 display:flex/column)。
|
||||
只有「最近识别」内部允许滚动;其他卡片按内容自然高度,不破裂底部圆角。
|
||||
issue #243 follow-up:去掉外层 overflow 后底部圆角被裁的视觉问题。 */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: mobile ? '1fr' : '1fr 1.4fr', gap: 14, flex: mobile ? '0 0 auto' : 1, minHeight: mobile ? undefined : 0 }}>
|
||||
<Card className="ol-overview-hero" padding={mobile ? 16 : 20} style={{ display: 'flex', flexDirection: 'column', minHeight: mobile ? undefined : 0 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1.4fr', gap: 12, flex: 1, minHeight: 0 }}>
|
||||
<Card padding={18} style={{ display: 'flex', flexDirection: 'column', minHeight: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 14 }}>
|
||||
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--ol-ink-2)' }}>{t('overview.weekTitle')}</span>
|
||||
<span style={{ fontSize: 11, color: 'var(--ol-ink-4)' }}>{t('overview.weekUnit')}</span>
|
||||
@@ -243,12 +215,12 @@ export function Overview({ onOpenHistory }: OverviewProps) {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="ol-overview-hero" padding={0} style={{ display: 'flex', flexDirection: 'column', minHeight: mobile ? undefined : 0, overflow: 'hidden' }}>
|
||||
<Card padding={0} style={{ display: 'flex', flexDirection: 'column', minHeight: 0, overflow: 'hidden' }}>
|
||||
<div style={{ padding: '14px 18px', borderBottom: '0.5px solid var(--ol-line)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexShrink: 0 }}>
|
||||
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--ol-ink-2)' }}>{t('overview.recentTitle')}</span>
|
||||
<Btn size="sm" variant="ghost" onClick={onOpenHistory}>{t('overview.recentAll')}</Btn>
|
||||
</div>
|
||||
<div className="ol-thinscroll" style={{ flex: mobile ? '0 0 auto' : 1, minHeight: mobile ? undefined : 0, overflow: mobile ? 'visible' : 'auto' }}>
|
||||
<div className="ol-thinscroll" style={{ flex: 1, minHeight: 0, overflow: 'auto' }}>
|
||||
{historyError ? (
|
||||
<div style={{ padding: 24, textAlign: 'center', fontSize: 12, color: 'var(--ol-ink-4)', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10 }}>
|
||||
<span>{t('overview.recentLoadFailed')}</span>
|
||||
@@ -282,14 +254,13 @@ interface ProviderCardProps {
|
||||
|
||||
function ProviderCard({ kind, name, subname, status }: ProviderCardProps) {
|
||||
const { t } = useTranslation();
|
||||
const mobile = useMobileLayout();
|
||||
// ASR 卡用 mic 图标,其他用 sparkle —— 通过比较译文判断会随语言改变,故改用本地化无关的字面量比较。
|
||||
const isAsr = kind === t('overview.asrKind');
|
||||
return (
|
||||
<Card padding={mobile ? 14 : 16} style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<Card padding={16} style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 38, height: 38, borderRadius: 'var(--ol-r-md)',
|
||||
width: 38, height: 38, borderRadius: 10,
|
||||
background: 'var(--ol-blue-soft)',
|
||||
color: 'var(--ol-blue)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
@@ -298,11 +269,11 @@ function ProviderCard({ kind, name, subname, status }: ProviderCardProps) {
|
||||
<Icon name={isAsr ? 'mic' : 'sparkle'} size={18} />
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 2, flexWrap: 'wrap' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 2 }}>
|
||||
<span style={{ fontSize: 11, color: 'var(--ol-ink-4)', fontWeight: 600, letterSpacing: '.06em', textTransform: 'uppercase' }}>{kind}</span>
|
||||
{status === 'configured' && (
|
||||
<Pill tone="ok" size="sm">
|
||||
<span style={{ width: 5, height: 5, borderRadius: 'var(--ol-pill-radius)', background: 'var(--ol-ok)' }} />
|
||||
<span style={{ width: 5, height: 5, borderRadius: 999, background: 'var(--ol-ok)' }} />
|
||||
{t('overview.statusConfigured')}
|
||||
</Pill>
|
||||
)}
|
||||
@@ -331,14 +302,13 @@ interface MetricProps {
|
||||
}
|
||||
|
||||
function Metric({ icon, label, value, trend, accent }: MetricProps) {
|
||||
const mobile = useMobileLayout();
|
||||
return (
|
||||
<Card padding={mobile ? 14 : 16}>
|
||||
<Card padding={16}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8, color: 'var(--ol-ink-3)' }}>
|
||||
<Icon name={icon} size={13} />
|
||||
<span style={{ fontSize: 11.5 }}>{label}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: mobile ? 24 : 26, fontWeight: 600, letterSpacing: '-0.02em', color: accent ? 'var(--ol-blue)' : 'var(--ol-ink)', lineHeight: 1.1 }}>{value}</div>
|
||||
<div style={{ fontSize: 26, fontWeight: 600, letterSpacing: '-0.02em', color: accent ? 'var(--ol-blue)' : 'var(--ol-ink)', lineHeight: 1.1 }}>{value}</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--ol-ink-4)', marginTop: 6 }}>{trend || ' '}</div>
|
||||
</Card>
|
||||
);
|
||||
@@ -373,10 +343,9 @@ function WeekChart({ data }: { data: number[] }) {
|
||||
|
||||
function RecentRow({ session, modeLabel }: { session: DictationSession; modeLabel: Record<PolishMode, string> }) {
|
||||
const { t } = useTranslation();
|
||||
const mobile = useMobileLayout();
|
||||
return (
|
||||
<div style={{ padding: mobile ? '12px 14px' : '12px 18px', borderBottom: '0.5px solid var(--ol-line-soft)', display: 'flex', gap: 12, alignItems: 'flex-start' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 4, minWidth: mobile ? 52 : 60 }}>
|
||||
<div style={{ padding: '12px 18px', borderBottom: '0.5px solid var(--ol-line-soft)', display: 'flex', gap: 12, alignItems: 'flex-start' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 4, minWidth: 60 }}>
|
||||
<span style={{ fontSize: 11, fontFamily: 'var(--ol-font-mono)', color: 'var(--ol-ink-3)' }}>
|
||||
{formatTime(session.createdAt)}
|
||||
</span>
|
||||
@@ -417,189 +386,3 @@ function weekDayLabels(names: string[]): string[] {
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function AndroidMicGrantBanner() {
|
||||
const { t } = useTranslation();
|
||||
const mobile = useMobileLayout();
|
||||
const [platformCaps, setPlatformCaps] = useState<PlatformCapabilities | null>(null);
|
||||
const [microphone, setMicrophone] = useState<PermissionStatus | 'loading'>('loading');
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
void getPlatformCapabilities().then(setPlatformCaps);
|
||||
}, []);
|
||||
|
||||
const refreshMic = useCallback(async () => {
|
||||
if (platformCaps?.platform === 'android') {
|
||||
setMicrophone(await checkAndroidMicrophoneAccess());
|
||||
return;
|
||||
}
|
||||
setMicrophone(await checkMicrophonePermission());
|
||||
}, [platformCaps?.platform]);
|
||||
|
||||
useEffect(() => {
|
||||
if (platformCaps?.platform !== 'android') return;
|
||||
void refreshMic();
|
||||
const onFocus = () => { void refreshMic(); };
|
||||
window.addEventListener('focus', onFocus);
|
||||
return () => window.removeEventListener('focus', onFocus);
|
||||
}, [platformCaps?.platform, refreshMic]);
|
||||
|
||||
if (platformCaps?.platform !== 'android') return null;
|
||||
if (microphone === 'loading' || microphone === 'granted' || microphone === 'notApplicable') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onGrant = async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
setMicrophone(await requestAndroidMicrophoneAccess());
|
||||
} finally {
|
||||
setBusy(false);
|
||||
void refreshMic();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
padding={14}
|
||||
style={{
|
||||
marginBottom: mobile ? 12 : 14,
|
||||
display: 'flex',
|
||||
flexDirection: mobile ? 'column' : 'row',
|
||||
alignItems: mobile ? 'stretch' : 'center',
|
||||
gap: 12,
|
||||
background: 'var(--ol-blue-soft)',
|
||||
border: '0.5px solid rgba(37,99,235,0.18)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, minWidth: 0 }}>
|
||||
<Icon name="mic" size={18} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--ol-ink)' }}>
|
||||
{t('overview.androidMicBanner.title')}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--ol-ink-3)', marginTop: 2, lineHeight: 1.5 }}>
|
||||
{t('overview.androidMicBanner.desc')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Btn variant="primary" size="sm" onClick={() => void onGrant()} disabled={busy} style={{ justifyContent: 'center', width: mobile ? '100%' : undefined }}>
|
||||
{t('overview.androidMicBanner.grant')}
|
||||
</Btn>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function InAppDictationControl() {
|
||||
const { t } = useTranslation();
|
||||
const mobile = useMobileLayout();
|
||||
const [platformCaps, setPlatformCaps] = useState<PlatformCapabilities | null>(null);
|
||||
const [capsuleState, setCapsuleState] = useState<CapsuleState>('idle');
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
void getPlatformCapabilities().then(setPlatformCaps);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!platformCaps?.supportsInAppDictation) return;
|
||||
let cancelled = false;
|
||||
let unlisten: (() => void) | undefined;
|
||||
void (async () => {
|
||||
try {
|
||||
const { listen } = await import('@tauri-apps/api/event');
|
||||
const handle = await listen<CapsulePayload>('capsule:state', (event) => {
|
||||
if (cancelled) return;
|
||||
setCapsuleState(event.payload.state);
|
||||
});
|
||||
if (cancelled) {
|
||||
handle();
|
||||
} else {
|
||||
unlisten = handle;
|
||||
}
|
||||
} catch {
|
||||
// browser dev mock
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
unlisten?.();
|
||||
};
|
||||
}, [platformCaps?.supportsInAppDictation]);
|
||||
|
||||
if (!platformCaps?.supportsInAppDictation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const recording = capsuleState === 'recording';
|
||||
const processing = capsuleState === 'transcribing' || capsuleState === 'polishing';
|
||||
const statusLabel = recording
|
||||
? t('overview.inAppDictation.recording')
|
||||
: processing
|
||||
? t('overview.inAppDictation.processing')
|
||||
: t('overview.inAppDictation.idle');
|
||||
|
||||
const onToggle = async () => {
|
||||
if (busy || processing) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
if (recording) {
|
||||
await stopDictation();
|
||||
} else {
|
||||
if (platformCaps?.platform === 'android') {
|
||||
const current = await checkAndroidMicrophoneAccess();
|
||||
const status = current === 'granted'
|
||||
? current
|
||||
: await requestAndroidMicrophoneAccess();
|
||||
if (status !== 'granted') return;
|
||||
}
|
||||
await startDictation();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[overview] in-app dictation toggle failed', error);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card padding={mobile ? 14 : 16} style={{ marginBottom: mobile ? 14 : 20, display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void onToggle()}
|
||||
disabled={busy || processing}
|
||||
aria-label={recording ? t('overview.inAppDictation.stop') : t('overview.inAppDictation.start')}
|
||||
style={{
|
||||
width: 52,
|
||||
height: 52,
|
||||
borderRadius: 'var(--ol-pill-radius)',
|
||||
border: 0,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: recording ? 'var(--ol-danger-solid-bg)' : 'var(--ol-accent-solid-bg)',
|
||||
color: recording ? 'var(--ol-danger-solid-ink)' : 'var(--ol-accent-solid-ink)',
|
||||
cursor: busy || processing ? 'not-allowed' : 'default',
|
||||
opacity: busy || processing ? 0.7 : 1,
|
||||
transition: 'background 0.16s var(--ol-motion-quick), opacity 0.16s var(--ol-motion-quick)',
|
||||
}}
|
||||
>
|
||||
{recording ? (
|
||||
<span style={{ width: 16, height: 16, borderRadius: 3, background: 'var(--ol-control-solid)', display: 'block' }} />
|
||||
) : (
|
||||
<Icon name="mic" size={20} />
|
||||
)}
|
||||
</button>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 600, color: 'var(--ol-ink)' }}>
|
||||
{t('overview.inAppDictation.title')}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--ol-ink-3)', marginTop: 2 }}>
|
||||
{statusLabel}
|
||||
</div>
|
||||
</div>
|
||||
{!mobile && <Pill tone={recording ? 'outline' : 'ok'} size="sm">{statusLabel}</Pill>}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -100,7 +100,7 @@ export function SelectionAsk() {
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
padding: '8px 14px',
|
||||
borderRadius: 'var(--ol-r-md)',
|
||||
borderRadius: 10,
|
||||
background: 'rgba(0,0,0,0.04)',
|
||||
border: '0.5px solid var(--ol-line)',
|
||||
}}
|
||||
@@ -116,7 +116,7 @@ export function SelectionAsk() {
|
||||
position: 'relative',
|
||||
width: 36,
|
||||
height: 20,
|
||||
borderRadius: 'var(--ol-pill-radius)',
|
||||
borderRadius: 999,
|
||||
border: 0,
|
||||
background: prefs.qaSaveHistory ? 'var(--ol-blue)' : 'rgba(0,0,0,0.18)',
|
||||
cursor: saving ? 'not-allowed' : 'default',
|
||||
@@ -132,8 +132,8 @@ export function SelectionAsk() {
|
||||
left: prefs.qaSaveHistory ? 18 : 2,
|
||||
width: 16,
|
||||
height: 16,
|
||||
borderRadius: 'var(--ol-pill-radius)',
|
||||
background: 'var(--ol-control-solid)',
|
||||
borderRadius: 999,
|
||||
background: '#fff',
|
||||
boxShadow: '0 1px 2px rgba(0,0,0,.2)',
|
||||
transition: 'left .16s var(--ol-motion-spring)',
|
||||
}}
|
||||
|
||||
@@ -127,6 +127,10 @@ export function Style() {
|
||||
const [packs, setPacks] = useState<StylePack[]>([]);
|
||||
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
// prefs:changed 监听器用它读「当前选中」,避免把 selectedId 放进 effect 依赖
|
||||
// 导致每次切换风格包都 unlisten + 重新 listen(两次 IPC/次点击 → 卡顿)。
|
||||
const selectedIdRef = useRef(selectedId);
|
||||
selectedIdRef.current = selectedId;
|
||||
const [draft, setDraft] = useState<StylePack | null>(null);
|
||||
const [busy, setBusy] = useState<BusyAction>('loading');
|
||||
const [saveState, setSaveState] = useState<SaveToastState>('idle');
|
||||
@@ -192,7 +196,7 @@ export function Style() {
|
||||
try {
|
||||
const { listen } = await import('@tauri-apps/api/event');
|
||||
unlisten = await listen('prefs:changed', () => {
|
||||
void loadPacks(selectedId);
|
||||
void loadPacks(selectedIdRef.current);
|
||||
});
|
||||
if (cancelled && unlisten) unlisten();
|
||||
} catch {
|
||||
@@ -203,7 +207,7 @@ export function Style() {
|
||||
cancelled = true;
|
||||
unlisten?.();
|
||||
};
|
||||
}, [selectedId]);
|
||||
}, []);
|
||||
|
||||
const selectedPack = packs.find(pack => pack.id === selectedId) ?? null;
|
||||
const activePack = packs.find(pack => pack.active) ?? null;
|
||||
@@ -570,7 +574,7 @@ export function Style() {
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
padding: '6px 12px',
|
||||
borderRadius: 'var(--ol-pill-radius)',
|
||||
borderRadius: 999,
|
||||
border: '0.5px solid',
|
||||
borderColor: rawPack.active ? 'var(--ol-blue)' : 'var(--ol-line-strong)',
|
||||
background: rawPack.active ? 'var(--ol-blue-soft)' : 'transparent',
|
||||
@@ -599,6 +603,9 @@ export function Style() {
|
||||
<motion.div
|
||||
key={pack.id}
|
||||
layout
|
||||
// 仅当包列表增减时才重算 layout;切换 active(pack.active 变化)
|
||||
// 不再触发整列卡片的 layout 重测,消除切换风格包时的卡顿。
|
||||
layoutDependency={bodyPacks.length}
|
||||
initial={{ opacity: 0, scale: 0.85 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.85 }}
|
||||
@@ -613,11 +620,15 @@ export function Style() {
|
||||
textAlign: 'left',
|
||||
position: 'relative',
|
||||
border: '0.5px solid',
|
||||
borderColor: pack.active ? 'var(--ol-style-card-border-active)' : 'var(--ol-style-card-border)',
|
||||
background: pack.active ? 'var(--ol-style-card-bg-active)' : 'var(--ol-style-card-bg)',
|
||||
borderRadius: 'var(--ol-modal-radius)',
|
||||
borderColor: pack.active ? 'var(--ol-blue)' : 'var(--ol-line)',
|
||||
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))',
|
||||
borderRadius: 18,
|
||||
padding: 16,
|
||||
boxShadow: pack.active ? 'var(--ol-style-card-shadow-active)' : 'var(--ol-style-card-shadow)',
|
||||
boxShadow: pack.active ? '0 0 0 3px var(--ol-blue-ring)' : 'none',
|
||||
cursor: 'default',
|
||||
minHeight: 204,
|
||||
}}
|
||||
@@ -625,7 +636,7 @@ export function Style() {
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 10, marginBottom: 10 }}>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 600, color: isBuiltin && !pack.active ? 'var(--ol-style-card-ink-2)' : 'var(--ol-style-card-ink)' }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 600, color: isBuiltin && !pack.active ? 'var(--ol-ink-2)' : 'var(--ol-ink)' }}>
|
||||
{pack.name}
|
||||
</div>
|
||||
<Pill tone={isBuiltin ? 'outline' : 'blue'} size="sm">
|
||||
@@ -642,7 +653,7 @@ export function Style() {
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12.5,
|
||||
color: 'var(--ol-style-card-ink-3)',
|
||||
color: 'var(--ol-ink-3)',
|
||||
lineHeight: 1.6,
|
||||
display: '-webkit-box',
|
||||
WebkitBoxOrient: 'vertical',
|
||||
@@ -659,10 +670,10 @@ export function Style() {
|
||||
<div
|
||||
aria-hidden
|
||||
style={{
|
||||
width: 36, height: 36, borderRadius: 'var(--ol-r-lg)',
|
||||
width: 36, height: 36, borderRadius: 12,
|
||||
display: 'grid', placeItems: 'center',
|
||||
background: pack.active ? 'var(--ol-style-card-icon-bg-active)' : 'var(--ol-style-card-icon-bg)',
|
||||
color: pack.active ? 'var(--ol-blue)' : 'var(--ol-style-card-ink-3)',
|
||||
background: pack.active ? 'rgba(37,99,235,0.12)' : 'rgba(15,23,42,0.05)',
|
||||
color: pack.active ? 'var(--ol-blue)' : 'var(--ol-ink-3)',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
@@ -676,11 +687,11 @@ export function Style() {
|
||||
aria-label={t('style.pack.deleteImported')}
|
||||
title={t('style.pack.deleteImported')}
|
||||
style={{
|
||||
width: 36, height: 36, borderRadius: 'var(--ol-r-lg)',
|
||||
width: 36, height: 36, borderRadius: 12,
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
border: '0.5px solid var(--ol-style-delete-border)',
|
||||
background: 'var(--ol-style-delete-bg)',
|
||||
border: '0.5px solid rgba(239,68,68,0.32)',
|
||||
background: 'rgba(254,242,242,0.6)',
|
||||
color: 'var(--ol-red, #ef4444)',
|
||||
cursor: busy === 'deleting' ? 'wait' : 'pointer',
|
||||
opacity: busy === 'deleting' ? 0.55 : 1,
|
||||
@@ -751,11 +762,11 @@ export function Style() {
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
textAlign: 'center',
|
||||
border: '0.5px dashed var(--ol-style-add-tile-border)',
|
||||
borderRadius: 'var(--ol-modal-radius)',
|
||||
border: '0.5px dashed var(--ol-line-strong)',
|
||||
borderRadius: 18,
|
||||
padding: 16,
|
||||
background: 'var(--ol-style-add-tile-bg)',
|
||||
color: 'var(--ol-style-card-ink-3)',
|
||||
background: 'transparent',
|
||||
color: 'var(--ol-ink-3)',
|
||||
cursor: busy === 'creating' ? 'wait' : 'pointer',
|
||||
opacity: busy === 'creating' ? 0.55 : 1,
|
||||
minHeight: 204,
|
||||
@@ -764,16 +775,16 @@ export function Style() {
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 44, height: 44, borderRadius: 'var(--ol-pill-radius)',
|
||||
width: 44, height: 44, borderRadius: 999,
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
background: 'var(--ol-style-add-tile-icon-bg)',
|
||||
color: 'var(--ol-style-card-ink-2)',
|
||||
background: 'rgba(15,23,42,0.04)',
|
||||
color: 'var(--ol-ink-2)',
|
||||
}}
|
||||
>
|
||||
<Icon name="plus" size={22} />
|
||||
</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 600, color: 'var(--ol-style-card-ink-2)' }}>{t('style.pack.addPackTileTitle')}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--ol-style-card-ink-3)', lineHeight: 1.55, maxWidth: 220 }}>{t('style.pack.addPackTileHint')}</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 600, color: 'var(--ol-ink-2)' }}>{t('style.pack.addPackTileTitle')}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--ol-ink-4)', lineHeight: 1.55, maxWidth: 220 }}>{t('style.pack.addPackTileHint')}</div>
|
||||
</motion.button>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
@@ -839,7 +850,7 @@ export function Style() {
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 'var(--ol-pill-radius)',
|
||||
borderRadius: 999,
|
||||
border: 0,
|
||||
background: 'transparent',
|
||||
color: 'var(--ol-ink-3)',
|
||||
@@ -986,9 +997,8 @@ export function Style() {
|
||||
<Card
|
||||
padding={16}
|
||||
style={{
|
||||
background: 'var(--ol-style-subtle-bg)',
|
||||
border: '0.5px solid var(--ol-style-subtle-border)',
|
||||
boxShadow: 'var(--ol-style-subtle-shadow)',
|
||||
background: 'linear-gradient(180deg, rgba(255,255,255,0.98), rgba(246,248,252,0.95))',
|
||||
border: '0.5px solid rgba(148,163,184,0.24)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap', marginBottom: 12 }}>
|
||||
@@ -1054,10 +1064,9 @@ export function Style() {
|
||||
<div
|
||||
style={{
|
||||
padding: 14,
|
||||
borderRadius: 'var(--ol-bubble-radius)',
|
||||
background: 'var(--ol-style-subtle-bg)',
|
||||
border: '0.5px solid var(--ol-style-subtle-border)',
|
||||
boxShadow: 'var(--ol-style-subtle-shadow)',
|
||||
borderRadius: 14,
|
||||
background: 'linear-gradient(180deg, rgba(248,250,252,0.98), rgba(241,245,249,0.95))',
|
||||
border: '0.5px solid var(--ol-line)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
|
||||
@@ -1093,9 +1102,7 @@ export function Style() {
|
||||
key={`${draft.id}-example-${index}`}
|
||||
padding={16}
|
||||
style={{
|
||||
background: 'var(--ol-style-card-bg)',
|
||||
border: '0.5px solid var(--ol-style-card-border)',
|
||||
boxShadow: 'var(--ol-style-card-shadow)',
|
||||
background: 'linear-gradient(180deg, rgba(255,255,255,0.98), rgba(248,250,252,0.98))',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, marginBottom: 12 }}>
|
||||
@@ -1113,7 +1120,7 @@ export function Style() {
|
||||
width: 32, height: 32,
|
||||
flexShrink: 0,
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
borderRadius: 8,
|
||||
background: 'transparent',
|
||||
color: 'var(--ol-ink-2)',
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
@@ -1128,9 +1135,9 @@ export function Style() {
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))', gap: 12 }}>
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 'var(--ol-bubble-radius)',
|
||||
border: '0.5px solid var(--ol-style-subtle-border)',
|
||||
background: 'var(--ol-style-subtle-bg)',
|
||||
borderRadius: 14,
|
||||
border: '0.5px solid rgba(148,163,184,0.22)',
|
||||
background: 'rgba(248,250,252,0.9)',
|
||||
padding: 14,
|
||||
}}
|
||||
>
|
||||
@@ -1140,15 +1147,15 @@ export function Style() {
|
||||
<textarea
|
||||
value={example.input}
|
||||
onChange={event => patchExample(index, { input: event.target.value })}
|
||||
style={{ ...textareaStyle, minHeight: 120, background: 'var(--ol-control-solid)' }}
|
||||
style={{ ...textareaStyle, minHeight: 120, background: 'var(--ol-style-input-bg)' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 'var(--ol-bubble-radius)',
|
||||
border: '0.5px solid var(--ol-style-card-border-active)',
|
||||
background: 'var(--ol-style-subtle-bg)',
|
||||
borderRadius: 14,
|
||||
border: '0.5px solid rgba(37,99,235,0.16)',
|
||||
background: 'rgba(239,246,255,0.86)',
|
||||
padding: 14,
|
||||
}}
|
||||
>
|
||||
@@ -1158,7 +1165,7 @@ export function Style() {
|
||||
<textarea
|
||||
value={example.output}
|
||||
onChange={event => patchExample(index, { output: event.target.value })}
|
||||
style={{ ...textareaStyle, minHeight: 120, background: 'var(--ol-control-solid)' }}
|
||||
style={{ ...textareaStyle, minHeight: 120, background: 'var(--ol-style-input-bg)' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1180,9 +1187,9 @@ function MetaItem({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 'var(--ol-r-lg)',
|
||||
border: '0.5px solid var(--ol-style-subtle-border)',
|
||||
background: 'var(--ol-style-card-bg)',
|
||||
borderRadius: 12,
|
||||
border: '0.5px solid rgba(148,163,184,0.2)',
|
||||
background: 'rgba(255,255,255,0.92)',
|
||||
padding: '10px 12px',
|
||||
}}
|
||||
>
|
||||
@@ -1217,9 +1224,9 @@ function DirectiveRow({
|
||||
justifyContent: 'space-between',
|
||||
gap: 12,
|
||||
padding: '10px 12px',
|
||||
borderRadius: 'var(--ol-r-lg)',
|
||||
border: '0.5px solid var(--ol-style-subtle-border)',
|
||||
background: 'var(--ol-style-card-bg)',
|
||||
borderRadius: 12,
|
||||
border: '0.5px solid rgba(148,163,184,0.2)',
|
||||
background: 'rgba(255,255,255,0.92)',
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
@@ -1238,10 +1245,9 @@ const inputStyle: CSSProperties = {
|
||||
boxSizing: 'border-box',
|
||||
minHeight: 38,
|
||||
padding: '9px 11px',
|
||||
borderRadius: 'var(--ol-r-md)',
|
||||
border: '0.5px solid var(--ol-style-input-border)',
|
||||
borderRadius: 10,
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
background: 'var(--ol-style-input-bg)',
|
||||
boxShadow: 'var(--ol-style-input-shadow)',
|
||||
color: 'var(--ol-ink)',
|
||||
font: 'inherit',
|
||||
fontSize: 12.5,
|
||||
@@ -1251,10 +1257,9 @@ const textareaStyle: CSSProperties = {
|
||||
width: '100%',
|
||||
boxSizing: 'border-box',
|
||||
padding: '11px 12px',
|
||||
borderRadius: 'var(--ol-r-lg)',
|
||||
border: '0.5px solid var(--ol-style-input-border)',
|
||||
borderRadius: 12,
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
background: 'var(--ol-style-input-bg)',
|
||||
boxShadow: 'var(--ol-style-input-shadow)',
|
||||
color: 'var(--ol-ink)',
|
||||
font: 'inherit',
|
||||
fontSize: 12.5,
|
||||
|
||||
@@ -77,10 +77,10 @@ export function Translation() {
|
||||
style={{
|
||||
alignSelf: 'flex-start',
|
||||
padding: '6px 12px',
|
||||
borderRadius: 'var(--ol-pill-radius)',
|
||||
borderRadius: 999,
|
||||
border: 0,
|
||||
background: 'var(--ol-accent-solid-bg)',
|
||||
color: 'var(--ol-accent-solid-ink)',
|
||||
background: 'var(--ol-blue)',
|
||||
color: '#fff',
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
cursor: loading ? 'not-allowed' : 'default',
|
||||
@@ -140,7 +140,7 @@ export function Translation() {
|
||||
justifyContent: 'space-between',
|
||||
gap: 10,
|
||||
padding: '8px 12px',
|
||||
borderRadius: 'var(--ol-r-md)',
|
||||
borderRadius: 10,
|
||||
border: '0.5px solid rgba(239,68,68,0.22)',
|
||||
background: 'rgba(239,68,68,0.07)',
|
||||
color: 'var(--ol-red, #ef4444)',
|
||||
@@ -156,7 +156,7 @@ export function Translation() {
|
||||
style={{
|
||||
flex: '0 0 auto',
|
||||
border: 0,
|
||||
borderRadius: 'var(--ol-pill-radius)',
|
||||
borderRadius: 999,
|
||||
background: 'rgba(239,68,68,0.12)',
|
||||
color: 'inherit',
|
||||
padding: '4px 10px',
|
||||
@@ -188,9 +188,9 @@ export function Translation() {
|
||||
fontSize: 12.5,
|
||||
fontWeight: checked ? 600 : 500,
|
||||
border: 0,
|
||||
borderRadius: 'var(--ol-pill-radius)',
|
||||
background: checked ? 'var(--ol-accent-solid-bg)' : 'rgba(0,0,0,0.05)',
|
||||
color: checked ? 'var(--ol-accent-solid-ink)' : 'var(--ol-ink-2)',
|
||||
borderRadius: 999,
|
||||
background: checked ? 'var(--ol-blue)' : 'rgba(0,0,0,0.05)',
|
||||
color: checked ? '#fff' : 'var(--ol-ink-2)',
|
||||
cursor: 'default',
|
||||
fontFamily: 'inherit',
|
||||
transition: 'background 0.12s ease-out, color 0.12s ease-out',
|
||||
@@ -213,7 +213,7 @@ export function Translation() {
|
||||
fontSize: 10.5,
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.04em',
|
||||
borderRadius: 'var(--ol-pill-radius)',
|
||||
borderRadius: 999,
|
||||
background: enabled ? 'rgba(37,99,235,0.10)' : 'rgba(0,0,0,0.05)',
|
||||
color: enabled ? 'var(--ol-blue)' : 'var(--ol-ink-4)',
|
||||
textTransform: 'uppercase',
|
||||
@@ -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: 'var(--ol-control-solid)' }}
|
||||
style={{ width: '100%', maxWidth: 360, fontSize: 13, background: '#fff' }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -279,7 +279,7 @@ export function Vocab() {
|
||||
onClick={() => togglePreset(p.id)}
|
||||
style={{
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
borderRadius: 'var(--ol-pill-radius)',
|
||||
borderRadius: 999,
|
||||
padding: '4px 10px',
|
||||
fontSize: 12,
|
||||
background: selectedPresetIds.includes(p.id) ? 'var(--ol-blue-soft)' : 'var(--ol-surface-2)',
|
||||
@@ -322,14 +322,14 @@ export function Vocab() {
|
||||
value={rulePatternDraft}
|
||||
onChange={e => setRulePatternDraft(e.target.value)}
|
||||
placeholder={t('vocab.corrections.patternPlaceholder')}
|
||||
style={{ height: 32, padding: '0 10px', border: '0.5px solid var(--ol-line-strong)', borderRadius: 'var(--ol-control-radius)', background: 'var(--ol-surface-2)' }}
|
||||
style={{ height: 32, padding: '0 10px', border: '0.5px solid var(--ol-line-strong)', borderRadius: 8, background: 'var(--ol-surface-2)' }}
|
||||
/>
|
||||
<span style={{ color: 'var(--ol-ink-4)', fontSize: 12 }}>→</span>
|
||||
<input
|
||||
value={ruleReplacementDraft}
|
||||
onChange={e => setRuleReplacementDraft(e.target.value)}
|
||||
placeholder={t('vocab.corrections.replacementPlaceholder')}
|
||||
style={{ height: 32, padding: '0 10px', border: '0.5px solid var(--ol-line-strong)', borderRadius: 'var(--ol-control-radius)', background: 'var(--ol-surface-2)' }}
|
||||
style={{ height: 32, padding: '0 10px', border: '0.5px solid var(--ol-line-strong)', borderRadius: 8, background: 'var(--ol-surface-2)' }}
|
||||
/>
|
||||
<Btn size="sm" variant="primary" onClick={() => void onAddCorrectionRule()}>{t('common.add')}</Btn>
|
||||
</div>
|
||||
@@ -362,7 +362,7 @@ export function Vocab() {
|
||||
style={{
|
||||
flex: 1, height: 36, padding: '0 12px',
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
borderRadius: 'var(--ol-control-radius)', fontSize: 13,
|
||||
borderRadius: 8, fontSize: 13,
|
||||
fontFamily: 'inherit', outline: 'none',
|
||||
background: 'var(--ol-surface-2)',
|
||||
transition: 'border-color 0.16s var(--ol-motion-quick), box-shadow 0.18s var(--ol-motion-soft), background 0.16s var(--ol-motion-quick)',
|
||||
@@ -412,7 +412,7 @@ function CorrectionRuleChip({ rule, onToggle, onRemove }: CorrectionRuleChipProp
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
padding: '5px 8px 5px 10px',
|
||||
borderRadius: 'var(--ol-pill-radius)',
|
||||
borderRadius: 999,
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
background: enabled ? 'var(--ol-surface)' : 'var(--ol-surface-2)',
|
||||
opacity: enabled ? 1 : 0.55,
|
||||
@@ -430,7 +430,7 @@ function CorrectionRuleChip({ rule, onToggle, onRemove }: CorrectionRuleChipProp
|
||||
<button
|
||||
onClick={onRemove}
|
||||
aria-label={t('vocab.corrections.removeAria')}
|
||||
style={{ width: 18, height: 18, borderRadius: 'var(--ol-pill-radius)', border: 0, background: 'rgba(0,0,0,0.06)', color: 'var(--ol-ink-4)', cursor: 'default' }}
|
||||
style={{ width: 18, height: 18, borderRadius: 999, border: 0, background: 'rgba(0,0,0,0.06)', color: 'var(--ol-ink-4)', cursor: 'default' }}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@@ -451,12 +451,12 @@ function VocabChip({ entry, onRemove, onToggle }: VocabChipProps) {
|
||||
<span
|
||||
style={{
|
||||
// 父 flex 容器 minHeight: 80 会让 flex item 在 align-self 默认 stretch 下被拉到
|
||||
// 80px 高,chip borderRadius: 'var(--ol-pill-radius)' 把高度变大渲染成"超大椭圆"。alignSelf:flex-start
|
||||
// 80px 高,chip borderRadius: 999 把高度变大渲染成"超大椭圆"。alignSelf:flex-start
|
||||
// 阻止拉伸,chip 始终保持 content 高度。
|
||||
alignSelf: 'flex-start',
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
padding: '5px 10px 5px 12px',
|
||||
borderRadius: 'var(--ol-pill-radius)',
|
||||
borderRadius: 999,
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
background: enabled ? (entry.hits > 0 ? 'var(--ol-blue-soft)' : 'var(--ol-surface)') : 'var(--ol-surface-2)',
|
||||
opacity: enabled ? 1 : 0.55,
|
||||
@@ -476,10 +476,10 @@ function VocabChip({ entry, onRemove, onToggle }: VocabChipProps) {
|
||||
<span
|
||||
style={{
|
||||
minWidth: 18, height: 18, padding: '0 5px',
|
||||
borderRadius: 'var(--ol-pill-radius)', fontSize: 10, fontWeight: 600,
|
||||
borderRadius: 999, fontSize: 10, fontWeight: 600,
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
background: entry.hits > 0 && enabled ? 'var(--ol-accent-solid-bg)' : 'rgba(0,0,0,0.06)',
|
||||
color: entry.hits > 0 && enabled ? 'var(--ol-accent-solid-ink)' : 'var(--ol-ink-4)',
|
||||
background: entry.hits > 0 && enabled ? 'var(--ol-blue)' : 'rgba(0,0,0,0.06)',
|
||||
color: entry.hits > 0 && enabled ? '#fff' : 'var(--ol-ink-4)',
|
||||
fontFamily: 'var(--ol-font-sans)',
|
||||
}}
|
||||
>{entry.hits}</span>
|
||||
@@ -487,7 +487,7 @@ function VocabChip({ entry, onRemove, onToggle }: VocabChipProps) {
|
||||
onClick={onRemove}
|
||||
aria-label={t('vocab.removeAria')}
|
||||
style={{
|
||||
width: 14, height: 14, padding: 0, border: 0, borderRadius: 'var(--ol-pill-radius)',
|
||||
width: 14, height: 14, padding: 0, border: 0, borderRadius: 999,
|
||||
background: 'transparent', color: 'var(--ol-ink-4)',
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'default',
|
||||
}}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
import { useState, type CSSProperties, type ReactNode } from 'react';
|
||||
import { Icon } from '../components/Icon';
|
||||
import { useMobileLayout } from '../lib/useMobileLayout';
|
||||
|
||||
interface PageHeaderProps {
|
||||
kicker?: string;
|
||||
@@ -15,57 +14,19 @@ interface PageHeaderProps {
|
||||
}
|
||||
|
||||
export function PageHeader({ kicker, title, desc, right, titleRight }: PageHeaderProps) {
|
||||
const mobile = useMobileLayout();
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: mobile ? 'column' : 'row',
|
||||
alignItems: mobile ? 'stretch' : 'flex-start',
|
||||
justifyContent: 'space-between',
|
||||
gap: mobile ? 12 : 24,
|
||||
marginBottom: mobile ? 18 : 28,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 24, marginBottom: 24 }}>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
{kicker && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: mobile ? 10 : 11,
|
||||
fontWeight: 600,
|
||||
letterSpacing: '.14em',
|
||||
textTransform: 'uppercase',
|
||||
color: 'var(--ol-ink-4)',
|
||||
marginBottom: 10,
|
||||
fontFamily: 'var(--ol-font-mono)',
|
||||
}}
|
||||
>
|
||||
{kicker}
|
||||
</div>
|
||||
<div style={{ fontSize: 11, fontWeight: 600, letterSpacing: '.08em', textTransform: 'uppercase', color: 'var(--ol-ink-4)', marginBottom: 8 }}>{kicker}</div>
|
||||
)}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
|
||||
<h1
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: mobile ? 28 : 34,
|
||||
fontWeight: 600,
|
||||
letterSpacing: '-0.03em',
|
||||
color: 'var(--ol-ink)',
|
||||
fontFamily: 'var(--ol-font-display)',
|
||||
lineHeight: mobile ? 1.08 : undefined,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
<h1 style={{ margin: 0, fontSize: 26, fontWeight: 600, letterSpacing: '-0.02em', color: 'var(--ol-ink)' }}>{title}</h1>
|
||||
{titleRight}
|
||||
</div>
|
||||
{desc && <p style={{ margin: mobile ? '8px 0 0' : '10px 0 0', fontSize: mobile ? 12.5 : 13.5, color: 'var(--ol-ink-3)', maxWidth: 680, lineHeight: 1.6 }}>{desc}</p>}
|
||||
{desc && <p style={{ margin: '8px 0 0', fontSize: 13, color: 'var(--ol-ink-3)', maxWidth: 640, lineHeight: 1.55 }}>{desc}</p>}
|
||||
</div>
|
||||
{right && (
|
||||
<div style={{ display: 'flex', justifyContent: mobile ? 'flex-start' : 'flex-end', flexWrap: 'wrap', gap: 8 }}>
|
||||
{right}
|
||||
</div>
|
||||
)}
|
||||
{right}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -81,16 +42,15 @@ interface CardProps {
|
||||
export function Card({ children, style, padding = 18, glassy = false, className }: CardProps) {
|
||||
return (
|
||||
<div
|
||||
className={['ol-aura-card', className].filter(Boolean).join(' ')}
|
||||
className={className}
|
||||
style={{
|
||||
...(glassy ? {
|
||||
background: 'var(--ol-panel-bg)',
|
||||
backdropFilter: 'blur(var(--ol-aura-glass-blur)) saturate(150%)',
|
||||
WebkitBackdropFilter: 'blur(var(--ol-aura-glass-blur)) saturate(150%)',
|
||||
border: '1px solid var(--ol-panel-border)',
|
||||
boxShadow: 'var(--ol-panel-shadow)',
|
||||
} : undefined),
|
||||
background: glassy ? 'rgba(255,255,255,0.55)' : 'var(--ol-surface)',
|
||||
backdropFilter: glassy ? 'blur(20px) saturate(160%)' : undefined,
|
||||
WebkitBackdropFilter: glassy ? 'blur(20px) saturate(160%)' : undefined,
|
||||
border: '0.5px solid var(--ol-line)',
|
||||
borderRadius: 'var(--ol-r-lg)',
|
||||
padding,
|
||||
boxShadow: 'var(--ol-shadow-sm)',
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
@@ -111,16 +71,16 @@ interface PillProps {
|
||||
|
||||
export function Pill({ children, tone = 'default', size = 'md', style }: PillProps) {
|
||||
const tones: Record<PillTone, { bg: string; color: string; bd: string }> = {
|
||||
default: { bg: 'var(--ol-pill-bg)', color: 'var(--ol-ink-2)', bd: 'var(--ol-pill-border)' },
|
||||
blue: { bg: 'var(--ol-pill-blue-bg)', color: 'var(--ol-blue)', bd: 'var(--ol-pill-blue-border)' },
|
||||
ok: { bg: 'var(--ol-pill-ok-bg)', color: 'var(--ol-ok)', bd: 'var(--ol-pill-ok-border)' },
|
||||
outline: { bg: 'var(--ol-pill-bg)', color: 'var(--ol-ink-3)', bd: 'var(--ol-pill-border)' },
|
||||
dark: { bg: 'var(--ol-pill-dark-bg)', color: 'var(--ol-on-accent)', bd: 'var(--ol-pill-dark-border)' },
|
||||
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' },
|
||||
};
|
||||
const t = tones[tone];
|
||||
const sz = size === 'sm'
|
||||
? { padding: '3px 10px', fontSize: 11 }
|
||||
: { padding: '5px 12px', fontSize: 11.5 };
|
||||
? { padding: '2px 8px', fontSize: 10.5 }
|
||||
: { padding: '4px 10px', fontSize: 11.5 };
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
@@ -128,13 +88,8 @@ export function Pill({ children, tone = 'default', size = 'md', style }: PillPro
|
||||
borderRadius: 999,
|
||||
background: t.bg,
|
||||
color: t.color,
|
||||
border: `1px solid ${t.bd}`,
|
||||
boxShadow: tone === 'dark' ? 'var(--ol-pill-dark-shadow)' : 'var(--ol-pill-shadow)',
|
||||
backdropFilter: 'blur(16px) saturate(145%)',
|
||||
WebkitBackdropFilter: 'blur(16px) saturate(145%)',
|
||||
fontWeight: 600,
|
||||
letterSpacing: '.01em',
|
||||
lineHeight: 1.2,
|
||||
border: t.bd === 'transparent' ? '0.5px solid transparent' : `0.5px solid ${t.bd}`,
|
||||
fontWeight: 500,
|
||||
whiteSpace: 'nowrap',
|
||||
flexShrink: 0,
|
||||
...sz,
|
||||
@@ -161,10 +116,10 @@ interface BtnProps {
|
||||
|
||||
export function Btn({ children, variant = 'ghost', size = 'md', icon, style, onClick, disabled = false }: BtnProps) {
|
||||
const variants: Record<BtnVariant, { bg: string; color: string; bd: string; sh: string }> = {
|
||||
primary: { bg: 'linear-gradient(180deg, #141821, #0d1016)', color: 'var(--ol-on-accent)', bd: 'transparent', sh: '0 14px 32px -22px rgba(15,23,42,0.55)' },
|
||||
blue: { bg: 'linear-gradient(180deg, var(--ol-accent-solid-bg), var(--ol-accent-solid-bg-hover))', color: 'var(--ol-accent-solid-ink)', bd: 'transparent', sh: '0 14px 32px -22px rgba(37,99,235,.45)' },
|
||||
ghost: { bg: 'var(--ol-control-muted)', color: 'var(--ol-ink-2)', bd: 'var(--ol-control-border)', sh: 'var(--ol-control-shadow)' },
|
||||
soft: { bg: 'var(--ol-control-muted-strong)', color: 'var(--ol-ink-2)', bd: 'transparent', sh: 'var(--ol-control-shadow)' },
|
||||
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' },
|
||||
};
|
||||
const v = variants[variant];
|
||||
const sizes: Record<BtnSize, { padding: string; fontSize: number }> = {
|
||||
@@ -179,7 +134,7 @@ export function Btn({ children, variant = 'ghost', size = 'md', icon, style, onC
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
background: v.bg, color: v.color,
|
||||
border: v.bd === 'transparent' ? '0.5px solid transparent' : `0.5px solid ${v.bd}`,
|
||||
borderRadius: 999,
|
||||
borderRadius: 8,
|
||||
boxShadow: v.sh,
|
||||
fontFamily: 'inherit', fontWeight: 500,
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
@@ -222,10 +177,10 @@ export function Collapsible({ title, desc, defaultOpen = false, embedded = false
|
||||
<div
|
||||
style={{
|
||||
borderBottom: embedded ? '0.5px solid var(--ol-line)' : undefined,
|
||||
border: embedded ? undefined : '1px solid var(--ol-card-border)',
|
||||
borderRadius: embedded ? 0 : 'var(--ol-card-radius)',
|
||||
background: embedded ? 'transparent' : 'var(--ol-card-bg)',
|
||||
boxShadow: embedded ? 'none' : 'var(--ol-card-shadow)',
|
||||
border: embedded ? undefined : '0.5px solid var(--ol-line)',
|
||||
borderRadius: embedded ? 0 : 'var(--ol-r-lg)',
|
||||
background: embedded ? 'transparent' : 'var(--ol-surface)',
|
||||
boxShadow: embedded ? 'none' : 'var(--ol-shadow-sm)',
|
||||
overflow: 'hidden',
|
||||
// 父级 flex column 带 minHeight:0 + overflow:auto 时,所有 flex 子项默认
|
||||
// shrink:1,会把 header 按钮也压成一条线。锁住不压缩,溢出走父容器滚动。
|
||||
|
||||
@@ -95,7 +95,7 @@ export function AboutSection() {
|
||||
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||
<kbd style={{
|
||||
padding: '4px 10px', fontSize: 12, fontFamily: 'var(--ol-font-mono)',
|
||||
borderRadius: 'var(--ol-r-sm)', background: 'var(--ol-surface-2)',
|
||||
borderRadius: 6, background: 'var(--ol-surface-2)',
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
boxShadow: '0 1px 0 rgba(0,0,0,0.04)',
|
||||
color: 'var(--ol-ink-2)',
|
||||
@@ -112,9 +112,9 @@ export function AboutSection() {
|
||||
}
|
||||
|
||||
const btnGhost: CSSProperties = {
|
||||
padding: '5px 10px', fontSize: 12, borderRadius: 'var(--ol-r-sm)',
|
||||
padding: '5px 10px', fontSize: 12, borderRadius: 6,
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
background: 'var(--ol-surface-solid)', color: 'var(--ol-ink-2)',
|
||||
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)',
|
||||
|
||||
@@ -70,9 +70,9 @@ export function CheckUpdateButton({ channel }: { channel: UpdateChannel }) {
|
||||
}
|
||||
|
||||
const checkBtnStyle: CSSProperties = {
|
||||
padding: '5px 10px', fontSize: 12, borderRadius: 'var(--ol-r-sm)',
|
||||
padding: '5px 10px', fontSize: 12, borderRadius: 6,
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
background: 'var(--ol-surface-solid)',
|
||||
background: '#fff',
|
||||
cursor: 'default', fontFamily: 'inherit',
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 6,
|
||||
minWidth: 84,
|
||||
|
||||
@@ -31,7 +31,7 @@ const consoleStyle: CSSProperties = {
|
||||
minHeight: 96,
|
||||
maxHeight: 260,
|
||||
overflow: 'auto',
|
||||
borderRadius: 'var(--ol-r-md)',
|
||||
borderRadius: 10,
|
||||
background: '#0f1117',
|
||||
color: '#d7dbe0',
|
||||
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
|
||||
|
||||
@@ -44,7 +44,7 @@ export function MarketplaceSection() {
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
padding: '10px 12px',
|
||||
borderRadius: 'var(--ol-r-md)',
|
||||
borderRadius: 10,
|
||||
background: 'var(--ol-surface-2)',
|
||||
border: '0.5px solid var(--ol-line)',
|
||||
}}
|
||||
@@ -54,8 +54,8 @@ export function MarketplaceSection() {
|
||||
width: 34,
|
||||
height: 34,
|
||||
borderRadius: 999,
|
||||
background: 'var(--ol-primary-solid-bg)',
|
||||
color: 'var(--ol-primary-solid-ink)',
|
||||
background: 'var(--ol-ink)',
|
||||
color: '#fff',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
|
||||
@@ -200,13 +200,14 @@ export function ProvidersSection({ kind = 'all' }: ProvidersSectionProps = {}) {
|
||||
const [llmModelRevision, setLlmModelRevision] = useState(0);
|
||||
const [asrModelRevision, setAsrModelRevision] = useState(0);
|
||||
const os = detectOS();
|
||||
// 主 ASR 下拉只列云端选项;本地推理(local-qwen3 / foundry-local-whisper /
|
||||
// sherpa-onnx-local / apple-speech)移到「高级 → 本地模型」,防止新手误开 CPU 推理。
|
||||
// 本地重引擎(qwen3 / sherpa / foundry)仍只在「高级 → 本地模型」里启用,
|
||||
// 防止新手在主下拉误开 CPU 推理。Apple 语音是系统自带、零凭据、轻量,
|
||||
// 在 macOS 上直接作为常规选项放进主下拉,方便随时选用 / 切走。
|
||||
const visibleAsrPresets = ASR_PRESETS.filter(
|
||||
p => p.id !== 'foundry-local-whisper'
|
||||
&& p.id !== 'local-qwen3'
|
||||
&& p.id !== 'sherpa-onnx-local'
|
||||
&& p.id !== 'apple-speech',
|
||||
&& (p.id !== 'apple-speech' || os === 'mac'),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -399,50 +400,44 @@ export function ProvidersSection({ kind = 'all' }: ProvidersSectionProps = {}) {
|
||||
未激活时不显示提示。 */}
|
||||
<SettingRow label={t('settings.providers.providerLabel')}>
|
||||
{(() => {
|
||||
const isLocked =
|
||||
committedAsrProvider === 'local-qwen3' ||
|
||||
committedAsrProvider === 'foundry-local-whisper' ||
|
||||
committedAsrProvider === 'sherpa-onnx-local' ||
|
||||
committedAsrProvider === 'apple-speech';
|
||||
const selectedValue: AsrPresetId = isLocked ? committedAsrProvider : asrProvider;
|
||||
// 跨机器同步异常兜底:committed 是本地但不在 visibleAsrPresets 里时,受控
|
||||
// select 会回退到首项造成假象 —— 补一个 disabled option 让 select 找到当前值。
|
||||
const anomalousLocal: AsrPresetId | null =
|
||||
isLocked && !visibleAsrPresets.some(p => p.id === committedAsrProvider)
|
||||
// 本地引擎激活时不再「接管 / 锁死」下拉——下拉始终可用,用户在本页就能直接
|
||||
// 切到其它供应商;切走后端 active 即自动停用本地引擎,不必再进「高级」手动关。
|
||||
// 重引擎(qwen3 / sherpa / foundry)当前激活但不在主下拉里时,补一个可选 option
|
||||
// 让 select 显示当前值并允许切走。Apple 语音在 macOS 已是常规可选项。
|
||||
const hiddenLocalActive: AsrPresetId | null =
|
||||
!visibleAsrPresets.some(p => p.id === committedAsrProvider)
|
||||
? committedAsrProvider
|
||||
: null;
|
||||
const anomalousNameKey = anomalousLocal === 'local-qwen3'
|
||||
const hiddenLocalNameKey = hiddenLocalActive === 'local-qwen3'
|
||||
? 'asrLocalQwen3'
|
||||
: anomalousLocal === 'foundry-local-whisper'
|
||||
: hiddenLocalActive === 'foundry-local-whisper'
|
||||
? 'asrFoundryLocalWhisper'
|
||||
: anomalousLocal === 'sherpa-onnx-local'
|
||||
: hiddenLocalActive === 'sherpa-onnx-local'
|
||||
? 'asrSherpaOnnxLocal'
|
||||
: anomalousLocal === 'apple-speech'
|
||||
: hiddenLocalActive === 'apple-speech'
|
||||
? 'asrAppleSpeech'
|
||||
: null;
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, alignItems: mobile ? 'stretch' : 'flex-start', minWidth: 0, width: '100%', maxWidth: '100%' }}>
|
||||
<SelectLite
|
||||
value={selectedValue}
|
||||
disabled={isLocked}
|
||||
value={asrProvider}
|
||||
onChange={next => onAsrProviderChange(next as AsrPresetId)}
|
||||
options={[
|
||||
...visibleAsrPresets.map(p => ({
|
||||
value: p.id,
|
||||
label: t(`settings.providers.presets.${p.nameKey}`),
|
||||
})),
|
||||
...(anomalousLocal && anomalousNameKey
|
||||
...(hiddenLocalActive && hiddenLocalNameKey
|
||||
? [{
|
||||
value: anomalousLocal,
|
||||
label: t(`settings.providers.presets.${anomalousNameKey}`),
|
||||
disabled: true,
|
||||
value: hiddenLocalActive,
|
||||
label: t(`settings.providers.presets.${hiddenLocalNameKey}`),
|
||||
}]
|
||||
: []),
|
||||
]}
|
||||
ariaLabel={t('settings.providers.providerLabel')}
|
||||
style={{ ...inputStyle, width: '100%', maxWidth: mobile ? '100%' : 200 }}
|
||||
/>
|
||||
{isLocked && (
|
||||
{hiddenLocalActive && (
|
||||
<div style={{ fontSize: 11, color: 'var(--ol-ink-4)', lineHeight: 1.5 }}>
|
||||
{t('settings.providers.asrProviderTakenOver')}
|
||||
</div>
|
||||
@@ -834,7 +829,7 @@ function CredentialField({ label, account, placeholder, mono, mask, defaultValue
|
||||
const miniBtnStyle: CSSProperties = {
|
||||
height: 32, padding: '0 12px',
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
borderRadius: 'var(--ol-control-radius)', background: 'var(--ol-surface)',
|
||||
borderRadius: 8, background: 'var(--ol-surface)',
|
||||
boxShadow: '0 1px 2px rgba(0,0,0,0.04), 0 0 0 0.5px rgba(255,255,255,0.2) inset',
|
||||
color: 'var(--ol-ink-2)', cursor: 'default', flexShrink: 0,
|
||||
fontSize: 12.5, fontWeight: 500, letterSpacing: '0.01em',
|
||||
@@ -844,7 +839,7 @@ const miniBtnStyle: CSSProperties = {
|
||||
const iconBtnStyle: CSSProperties = {
|
||||
width: 32, height: 32,
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
borderRadius: 'var(--ol-control-radius)', background: 'var(--ol-surface)',
|
||||
borderRadius: 8, background: 'var(--ol-surface)',
|
||||
boxShadow: '0 1px 2px rgba(0,0,0,0.04), 0 0 0 0.5px rgba(255,255,255,0.2) inset',
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: 'var(--ol-ink-3)', cursor: 'default', flexShrink: 0,
|
||||
|
||||
@@ -163,7 +163,7 @@ export function RecordingInputSection() {
|
||||
marginTop: 4,
|
||||
marginBottom: 8,
|
||||
padding: '12px 14px',
|
||||
borderRadius: 'var(--ol-r-md)',
|
||||
borderRadius: 10,
|
||||
background: 'rgba(37,99,235,0.08)',
|
||||
border: '0.5px solid rgba(37,99,235,0.18)',
|
||||
}}
|
||||
@@ -189,15 +189,15 @@ export function RecordingInputSection() {
|
||||
)}
|
||||
{showDesktopHotkey && (
|
||||
<SettingRow label={t('settings.recording.modeLabel')}>
|
||||
<div style={{ display: 'inline-flex', padding: 2, borderRadius: 'var(--ol-control-radius)', background: 'rgba(0,0,0,0.05)' }}>
|
||||
<div style={{ display: 'inline-flex', padding: 2, borderRadius: 8, background: 'rgba(0,0,0,0.05)' }}>
|
||||
{choices.map(([v, l]) => (
|
||||
<button
|
||||
key={v}
|
||||
onClick={() => onModeChange(v)}
|
||||
style={{
|
||||
padding: '5px 14px', fontSize: 12, fontWeight: 500,
|
||||
border: 0, borderRadius: 'var(--ol-r-sm)', fontFamily: 'inherit',
|
||||
background: prefs.hotkey.mode === v ? 'var(--ol-surface-solid)' : 'transparent',
|
||||
border: 0, borderRadius: 6, fontFamily: 'inherit',
|
||||
background: prefs.hotkey.mode === v ? '#fff' : 'transparent',
|
||||
color: prefs.hotkey.mode === v ? 'var(--ol-ink)' : 'var(--ol-ink-3)',
|
||||
boxShadow: prefs.hotkey.mode === v ? '0 1px 2px rgba(0,0,0,.08)' : 'none',
|
||||
cursor: 'default',
|
||||
@@ -248,7 +248,7 @@ export function RecordingInputSection() {
|
||||
fontWeight: 500,
|
||||
fontFamily: 'inherit',
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
borderRadius: 8,
|
||||
background: 'var(--ol-surface-2)',
|
||||
color: 'var(--ol-ink-2)',
|
||||
cursor: 'default',
|
||||
|
||||
@@ -115,7 +115,7 @@ export function RemoteInputSection() {
|
||||
|
||||
const smallBtn: CSSProperties = {
|
||||
padding: '4px 10px',
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
borderRadius: 8,
|
||||
fontSize: 12,
|
||||
cursor: 'pointer',
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
@@ -161,13 +161,13 @@ export function RemoteInputSection() {
|
||||
}
|
||||
style={{
|
||||
padding: '5px 12px',
|
||||
borderRadius: 'var(--ol-control-radius)',
|
||||
borderRadius: 8,
|
||||
fontSize: 12.5,
|
||||
cursor: 'pointer',
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
background:
|
||||
mode === m ? 'var(--ol-accent-solid-bg)' : 'var(--ol-surface-2)',
|
||||
color: mode === m ? 'var(--ol-accent-solid-ink)' : 'var(--ol-ink)',
|
||||
mode === m ? 'var(--ol-blue)' : 'var(--ol-surface-2)',
|
||||
color: mode === m ? '#fff' : 'var(--ol-ink)',
|
||||
}}
|
||||
>
|
||||
{t(
|
||||
|
||||
@@ -23,10 +23,10 @@ const enableBtnStyle: CSSProperties = {
|
||||
alignSelf: 'flex-start',
|
||||
fontSize: 12,
|
||||
padding: '5px 14px',
|
||||
background: 'var(--ol-accent-solid-bg)',
|
||||
color: 'var(--ol-accent-solid-ink)',
|
||||
background: 'var(--ol-blue)',
|
||||
color: '#fff',
|
||||
border: 0,
|
||||
borderRadius: 'var(--ol-r-sm)',
|
||||
borderRadius: 6,
|
||||
fontFamily: 'inherit',
|
||||
fontWeight: 500,
|
||||
cursor: 'pointer',
|
||||
@@ -107,7 +107,7 @@ export function ShortcutsSection() {
|
||||
await setQaHotkey(binding);
|
||||
await savePrefs({ ...prefs, qaHotkey: binding });
|
||||
}}
|
||||
style={{ fontSize: 12, padding: '5px 14px', background: 'var(--ol-accent-solid-bg)', color: 'var(--ol-accent-solid-ink)', border: 0, borderRadius: 'var(--ol-r-sm)', fontFamily: 'inherit', fontWeight: 500, cursor: 'default' }}
|
||||
style={{ fontSize: 12, padding: '5px 14px', background: 'var(--ol-blue)', color: '#fff', border: 0, borderRadius: 6, fontFamily: 'inherit', fontWeight: 500, cursor: 'default' }}
|
||||
>
|
||||
{t('selectionAsk.hotkey.enable', 'Enable')}
|
||||
</button>
|
||||
@@ -201,7 +201,7 @@ export function ShortcutsSection() {
|
||||
<kbd style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 4,
|
||||
padding: '4px 10px', fontSize: 12, fontFamily: 'var(--ol-font-mono)',
|
||||
borderRadius: 'var(--ol-r-sm)', background: 'var(--ol-surface-2)',
|
||||
borderRadius: 6, background: 'var(--ol-surface-2)',
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
boxShadow: '0 1px 0 rgba(0,0,0,0.04)',
|
||||
color: 'var(--ol-ink-2)',
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SegSimple } from '../../components/ui/SegSimple';
|
||||
import { readThemeMode, setThemeMode, type ThemeMode } from '../../lib/themeMode';
|
||||
import { Card } from '../_atoms';
|
||||
import { SettingRow } from './shared';
|
||||
|
||||
const MODES: ThemeMode[] = ['system', 'light', 'dark'];
|
||||
|
||||
export function ThemeSection() {
|
||||
const { t } = useTranslation();
|
||||
const [mode, setMode] = useState<ThemeMode>(readThemeMode());
|
||||
|
||||
const options = useMemo(() => MODES.map((value) => ({
|
||||
value,
|
||||
label: t(`settings.appearance.${value === 'system' ? 'followSystem' : value}`),
|
||||
})), [t]);
|
||||
|
||||
const apply = (next: ThemeMode) => {
|
||||
setMode(next);
|
||||
setThemeMode(next);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: 6 }}>{t('settings.appearance.title')}</div>
|
||||
<SettingRow label={t('settings.appearance.label')}>
|
||||
<SegSimple
|
||||
options={options}
|
||||
value={mode}
|
||||
onChange={next => apply(next as ThemeMode)}
|
||||
/>
|
||||
</SettingRow>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -102,9 +102,9 @@ export function Toggle({
|
||||
position: "relative",
|
||||
width: 36,
|
||||
height: 20,
|
||||
borderRadius: "var(--ol-pill-radius)",
|
||||
borderRadius: 999,
|
||||
border: 0,
|
||||
background: on ? "var(--ol-blue)" : "var(--ol-toggle-off-bg)",
|
||||
background: on ? "var(--ol-blue)" : "rgba(0,0,0,0.15)",
|
||||
boxShadow: "inset 0 1px 2px rgba(0,0,0,0.06)",
|
||||
cursor: "default",
|
||||
transition: "background 0.16s var(--ol-motion-quick)",
|
||||
@@ -117,8 +117,8 @@ export function Toggle({
|
||||
left: on ? 18 : 2,
|
||||
width: 16,
|
||||
height: 16,
|
||||
borderRadius: "var(--ol-pill-radius)",
|
||||
background: "var(--ol-toggle-knob)",
|
||||
borderRadius: 999,
|
||||
background: "#fff",
|
||||
boxShadow:
|
||||
"0 1px 2px rgba(0,0,0,.25), 0 0 0 0.5px rgba(0,0,0,.04)",
|
||||
transition: "left .16s var(--ol-motion-spring)",
|
||||
@@ -133,13 +133,11 @@ export const inputStyle: CSSProperties = {
|
||||
height: 32,
|
||||
padding: "0 10px",
|
||||
border: "0.5px solid var(--ol-line-strong)",
|
||||
borderRadius: "var(--ol-control-radius)",
|
||||
borderRadius: 8,
|
||||
fontSize: 12.5,
|
||||
fontFamily: "inherit",
|
||||
outline: "none",
|
||||
background: "var(--ol-input-bg)",
|
||||
color: "var(--ol-input-ink)",
|
||||
caretColor: "var(--ol-input-ink)",
|
||||
background: "var(--ol-surface-2)",
|
||||
width: "100%",
|
||||
maxWidth: 360,
|
||||
transition:
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useEffect, useState } from 'react';
|
||||
import { RecordingInputSection } from './RecordingInputSection';
|
||||
import { ShortcutsSection } from './ShortcutsSection';
|
||||
import { LanguageSection } from './LanguageSection';
|
||||
import { ThemeSection } from './ThemeSection';
|
||||
import { ProvidersSection } from './ProvidersSection';
|
||||
import { MarketplaceSection } from './MarketplaceSection';
|
||||
import { PermissionsSection } from './PermissionsSection';
|
||||
@@ -34,7 +33,6 @@ export function GeneralTab() {
|
||||
<>
|
||||
<RecordingInputSection />
|
||||
{showDesktopShortcuts && <ShortcutsSection />}
|
||||
<ThemeSection />
|
||||
<LanguageSection />
|
||||
</>
|
||||
);
|
||||
@@ -61,14 +59,14 @@ export function PrivacyTab() {
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
padding: '10px 12px',
|
||||
borderRadius: 'var(--ol-r-md)',
|
||||
borderRadius: 10,
|
||||
background: 'var(--ol-blue-soft)',
|
||||
marginBottom: 2,
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
fontSize: 11, padding: '3px 8px', borderRadius: 999,
|
||||
background: 'var(--ol-surface-solid)', color: 'var(--ol-blue)', fontWeight: 600, flexShrink: 0,
|
||||
background: '#fff', color: 'var(--ol-blue)', fontWeight: 600, flexShrink: 0,
|
||||
}}>
|
||||
{t('modal.about.localFirst')}
|
||||
</span>
|
||||
|
||||
@@ -66,13 +66,6 @@ input, textarea {
|
||||
font-family: inherit;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
color: var(--ol-input-ink);
|
||||
caret-color: var(--ol-input-ink);
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
color: var(--ol-input-placeholder);
|
||||
}
|
||||
|
||||
a { color: inherit; text-decoration: none; }
|
||||
@@ -152,7 +145,14 @@ a { color: inherit; text-decoration: none; }
|
||||
.ol-frost {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
background: var(--ol-frost-bg);
|
||||
/* 体渐变 + 左上高光扫面 —— 半透明白底(~0.9),给磨砂面体积感而不是死平。 */
|
||||
background:
|
||||
radial-gradient(135% 86% at 15% -10%,
|
||||
rgba(255, 255, 255, 0.97) 0%,
|
||||
rgba(255, 255, 255, 0) 56%),
|
||||
linear-gradient(161deg,
|
||||
rgba(255, 255, 255, 0.93) 0%,
|
||||
rgba(244, 247, 252, 0.88) 100%);
|
||||
}
|
||||
|
||||
.ol-frost::before {
|
||||
@@ -161,12 +161,12 @@ a { color: inherit; text-decoration: none; }
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
border-radius: inherit;
|
||||
/* 不透明灰度噪点,normal 混合 —— 这是"磨砂颗粒"的来源。opacity 由
|
||||
--ol-frost-grain-opacity 控制颗粒强度。 */
|
||||
/* 不透明灰度噪点,normal 混合 —— 这是"磨砂颗粒"的来源。opacity 直接等于颗粒
|
||||
强度:0.1 几乎看不见,0.32 能明显看出磨砂质感又不脏。要更糙往 0.45 调。 */
|
||||
background-image: var(--ol-frost-grain);
|
||||
/* tile 与 SVG 视口同尺寸(100),stitchTiles 的无缝拼接才成立 */
|
||||
background-size: 100px 100px;
|
||||
opacity: var(--ol-frost-grain-opacity);
|
||||
opacity: 0.32;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -177,402 +177,3 @@ html[data-ol-platform="linux"] .ol-frost {
|
||||
html[data-ol-platform="linux"] .ol-frost::before {
|
||||
content: none;
|
||||
}
|
||||
|
||||
/* ── App shell + Aura surfaces ─────────────────────────────────────────── */
|
||||
|
||||
.ol-app-shell-bg {
|
||||
background: var(--ol-app-shell-bg);
|
||||
}
|
||||
|
||||
.ol-aura-panel {
|
||||
background: var(--ol-panel-bg);
|
||||
border: 1px solid var(--ol-panel-border);
|
||||
box-shadow: var(--ol-panel-shadow);
|
||||
backdrop-filter: blur(var(--ol-aura-glass-blur)) saturate(150%);
|
||||
-webkit-backdrop-filter: blur(var(--ol-aura-glass-blur)) saturate(150%);
|
||||
}
|
||||
|
||||
.ol-aura-card {
|
||||
background: var(--ol-card-bg);
|
||||
border: 1px solid var(--ol-card-border);
|
||||
border-radius: var(--ol-card-radius);
|
||||
box-shadow: var(--ol-card-shadow);
|
||||
}
|
||||
|
||||
html[data-ol-platform="linux"] .ol-aura-panel,
|
||||
html[data-ol-platform="linux"] .ol-aura-card {
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
background: var(--ol-surface);
|
||||
}
|
||||
|
||||
/* ── FloatingShell nav + sidebar ───────────────────────────────────────── */
|
||||
|
||||
.ol-nav-btn {
|
||||
color: var(--ol-ink-3);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ol-nav-btn.ol-nav-btn-active {
|
||||
color: var(--ol-ink);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ol-nav-btn:not(.ol-nav-btn-active):hover {
|
||||
background: var(--ol-nav-hover-bg);
|
||||
color: var(--ol-ink);
|
||||
}
|
||||
|
||||
.ol-aura-sidebar {
|
||||
padding: 14px 12px 14px;
|
||||
background: var(--ol-sidebar-bg);
|
||||
border-right: 1px solid var(--ol-sidebar-border);
|
||||
}
|
||||
|
||||
.ol-aura-sidebar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 10px 16px;
|
||||
margin-bottom: 6px;
|
||||
border-radius: var(--ol-r-lg);
|
||||
background: var(--ol-sidebar-brand-bg);
|
||||
border: 1px solid var(--ol-sidebar-brand-border);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.ol-aura-sidebar-brand-mark {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: var(--ol-control-radius);
|
||||
box-shadow: none;
|
||||
box-sizing: border-box;
|
||||
padding: 3px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.ol-aura-sidebar-brand-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
font-family: var(--ol-font-display);
|
||||
color: var(--ol-ink);
|
||||
}
|
||||
|
||||
.ol-aura-sidebar-brand-kicker {
|
||||
font-size: 10.5px;
|
||||
color: var(--ol-ink-4);
|
||||
font-family: var(--ol-font-mono);
|
||||
letter-spacing: .08em;
|
||||
}
|
||||
|
||||
.ol-aura-sidebar-pill {
|
||||
background: var(--ol-sidebar-pill-bg);
|
||||
border-radius: var(--ol-r-lg);
|
||||
border: 1px solid var(--ol-sidebar-pill-border);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.ol-aura-sidebar-nav-btn {
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--ol-r-lg);
|
||||
border: 0;
|
||||
background: transparent;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
cursor: default;
|
||||
transition: color 0.16s var(--ol-motion-quick), background 0.16s var(--ol-motion-quick);
|
||||
text-align: left;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.ol-aura-sidebar-footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 12px 10px 0;
|
||||
margin-top: 10px;
|
||||
border-top: 1px solid var(--ol-sidebar-footer-border);
|
||||
}
|
||||
|
||||
.ol-aura-sidebar-version {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
padding: 10px 12px;
|
||||
font-family: var(--ol-font-sans);
|
||||
font-size: 11px;
|
||||
color: var(--ol-ink-4);
|
||||
background: var(--ol-sidebar-version-bg);
|
||||
border: 1px solid var(--ol-sidebar-version-border);
|
||||
border-radius: var(--ol-pill-radius);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.ol-aura-beta-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ol-blue);
|
||||
background: rgba(37,99,235,0.10);
|
||||
border-radius: var(--ol-pill-radius);
|
||||
}
|
||||
|
||||
.ol-aura-sidebar-settings {
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--ol-r-lg);
|
||||
border: 1px solid var(--ol-sidebar-settings-border);
|
||||
background: var(--ol-sidebar-settings-bg);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.ol-aura-sidebar-settings.ol-nav-btn-active {
|
||||
background: var(--ol-sidebar-settings-active-bg);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.ol-aura-console-main {
|
||||
border-radius: var(--ol-panel-radius);
|
||||
}
|
||||
|
||||
[data-ol-mobile="true"] .ol-aura-console-main {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.ol-aura-mobile-topbar {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: calc(10px + env(safe-area-inset-top, 0px)) 14px 10px;
|
||||
border-bottom: 1px solid var(--ol-sidebar-border);
|
||||
background: var(--ol-sidebar-bg);
|
||||
}
|
||||
|
||||
.ol-aura-mobile-brand {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.ol-aura-mobile-brand-mark {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: var(--ol-control-radius);
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 3px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.ol-aura-mobile-brand-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--ol-ink);
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.ol-aura-mobile-brand-section {
|
||||
margin-top: 2px;
|
||||
font-size: 11px;
|
||||
color: var(--ol-ink-4);
|
||||
font-family: var(--ol-font-mono);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ol-aura-mobile-settings {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--ol-r-lg);
|
||||
color: var(--ol-ink-3);
|
||||
background: var(--ol-sidebar-settings-bg);
|
||||
border: 1px solid var(--ol-sidebar-settings-border);
|
||||
}
|
||||
|
||||
.ol-aura-mobile-settings-active {
|
||||
color: var(--ol-ink);
|
||||
background: var(--ol-sidebar-settings-active-bg);
|
||||
}
|
||||
|
||||
.ol-aura-mobile-nav {
|
||||
flex-shrink: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--ol-nav-count, 6), minmax(0, 1fr));
|
||||
gap: 2px;
|
||||
padding: 7px 8px calc(7px + env(safe-area-inset-bottom, 0px));
|
||||
border-top: 1px solid var(--ol-sidebar-border);
|
||||
background: var(--ol-sidebar-bg);
|
||||
}
|
||||
|
||||
.ol-aura-mobile-nav-btn {
|
||||
min-width: 0;
|
||||
height: 50px;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
border-radius: var(--ol-r-lg);
|
||||
color: var(--ol-ink-4);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.ol-aura-mobile-nav-btn span {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ol-aura-mobile-nav-btn-active {
|
||||
color: var(--ol-ink);
|
||||
background: var(--ol-sidebar-pill-bg);
|
||||
border: 1px solid var(--ol-sidebar-pill-border);
|
||||
}
|
||||
|
||||
/* ── Settings modal ────────────────────────────────────────────────────── */
|
||||
|
||||
.ol-aura-settings {
|
||||
background: var(--ol-panel-bg);
|
||||
border-radius: var(--ol-shell-radius);
|
||||
border: 1px solid var(--ol-panel-border);
|
||||
box-shadow: var(--ol-panel-shadow);
|
||||
}
|
||||
|
||||
.ol-aura-settings[data-ol-mobile="true"] {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.ol-aura-settings-rail {
|
||||
padding: 20px 14px;
|
||||
gap: 16px;
|
||||
background: var(--ol-settings-rail-bg);
|
||||
border-right: 1px solid var(--ol-settings-rail-border);
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.ol-aura-settings[data-ol-mobile="true"] .ol-aura-settings-rail {
|
||||
padding: calc(10px + env(safe-area-inset-top, 0px)) 10px 8px;
|
||||
gap: 8px;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid var(--ol-settings-rail-border);
|
||||
}
|
||||
|
||||
.ol-aura-settings-pill {
|
||||
background: var(--ol-sidebar-pill-bg);
|
||||
border-radius: var(--ol-r-lg);
|
||||
border: 1px solid var(--ol-sidebar-pill-border);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.ol-aura-settings-nav-btn {
|
||||
padding: 7px 10px;
|
||||
border-radius: var(--ol-r-lg);
|
||||
border: 0;
|
||||
background: transparent;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
cursor: default;
|
||||
text-align: left;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
transition: color 0.16s var(--ol-motion-quick), background 0.16s var(--ol-motion-quick);
|
||||
}
|
||||
|
||||
.ol-aura-settings[data-ol-mobile="true"] .ol-aura-settings-nav-btn {
|
||||
padding: 8px 11px;
|
||||
}
|
||||
|
||||
.ol-aura-settings-nav-btn.ol-nav-btn-active {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.ol-aura-settings[data-ol-mobile="true"] .ol-aura-settings-nav-btn.ol-nav-btn-active {
|
||||
background: var(--ol-sidebar-pill-bg);
|
||||
border: 1px solid var(--ol-sidebar-pill-border);
|
||||
}
|
||||
|
||||
.ol-aura-settings-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
padding-top: 10px;
|
||||
padding-left: 0;
|
||||
border-top: 1px solid var(--ol-settings-links-border);
|
||||
border-left: 0;
|
||||
overflow-x: visible;
|
||||
}
|
||||
|
||||
.ol-aura-settings[data-ol-mobile="true"] .ol-aura-settings-links {
|
||||
flex-direction: row;
|
||||
padding-top: 0;
|
||||
padding-left: 8px;
|
||||
border-top: 0;
|
||||
border-left: 1px solid var(--ol-settings-links-border);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.ol-aura-settings-content {
|
||||
background: var(--ol-settings-content-bg);
|
||||
}
|
||||
|
||||
.ol-aura-settings-close {
|
||||
background: var(--ol-settings-close-bg);
|
||||
color: var(--ol-ink-3);
|
||||
transition: background 0.16s var(--ol-motion-quick);
|
||||
}
|
||||
|
||||
.ol-aura-settings-close:hover {
|
||||
background: var(--ol-settings-close-hover-bg);
|
||||
}
|
||||
|
||||
.ol-aura-settings-title {
|
||||
padding: 24px 28px 10px;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
font-family: var(--ol-font-display);
|
||||
color: var(--ol-ink);
|
||||
}
|
||||
|
||||
.ol-aura-settings[data-ol-mobile="true"] .ol-aura-settings-title {
|
||||
padding: 16px 48px 8px 16px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/* ── Page switch + prompt keyframes ──────────────────────────────────── */
|
||||
|
||||
@keyframes ol-page-slide {
|
||||
from { opacity: 0; transform: translate3d(10px, 0, 0) scale(.996); filter: blur(6px); }
|
||||
to { opacity: 1; transform: translate3d(0, 0, 0) scale(1); filter: blur(0); }
|
||||
}
|
||||
|
||||
@keyframes ol-page-fadeout {
|
||||
from { opacity: 1; filter: blur(0); }
|
||||
to { opacity: 0; filter: blur(8px); }
|
||||
}
|
||||
|
||||
@keyframes ol-prompt-fade {
|
||||
from { opacity: 0; backdrop-filter: blur(0); -webkit-backdrop-filter: blur(0); }
|
||||
to { opacity: 1; backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); }
|
||||
}
|
||||
|
||||
@keyframes ol-prompt-pop {
|
||||
from { opacity: 0; transform: translateY(6px) scale(.97); filter: blur(6px); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }
|
||||
}
|
||||
@@ -1,6 +1,14 @@
|
||||
/* OpenLess design tokens with Aura-specific extensions. */
|
||||
|
||||
:root {
|
||||
/* QaPanel(划词追问)保留新代码引用的 6 个 solid/radius token —— 旧 UI 回退后
|
||||
补回这几个,避免该页元素丢色。用 var() 间接引用基础色,深浅模式自动跟随。 */
|
||||
--ol-accent-solid-bg: var(--ol-blue);
|
||||
--ol-accent-solid-ink: var(--ol-on-accent);
|
||||
--ol-danger-solid-bg: var(--ol-err);
|
||||
--ol-danger-solid-ink: #ffffff;
|
||||
--ol-control-radius: 8px;
|
||||
--ol-bubble-radius: var(--ol-r-lg);
|
||||
/* Shared palette */
|
||||
--ol-white: #ffffff;
|
||||
--ol-canvas: #f7f7f8;
|
||||
@@ -25,11 +33,8 @@
|
||||
--ol-ink: #0a0a0b;
|
||||
--ol-ink-2: #2a2a2d;
|
||||
--ol-ink-3: rgba(10, 10, 11, 0.62);
|
||||
--ol-ink-4: rgba(10, 10, 11, 0.58);
|
||||
--ol-ink-4: rgba(10, 10, 11, 0.42);
|
||||
--ol-ink-5: rgba(10, 10, 11, 0.24);
|
||||
--ol-ink-muted: var(--ol-ink-3);
|
||||
--ol-ink-subtle: var(--ol-ink-4);
|
||||
--ol-placeholder: var(--ol-ink-4);
|
||||
|
||||
/* Blue accent */
|
||||
--ol-blue: #2563eb;
|
||||
@@ -37,11 +42,6 @@
|
||||
--ol-blue-soft: #eff4ff;
|
||||
--ol-blue-ring: rgba(37, 99, 235, 0.22);
|
||||
--ol-on-accent: #ffffff;
|
||||
--ol-accent-solid-bg: var(--ol-blue);
|
||||
--ol-accent-solid-bg-hover: var(--ol-blue-hover);
|
||||
--ol-accent-solid-ink: var(--ol-on-accent);
|
||||
--ol-primary-solid-bg: #111827;
|
||||
--ol-primary-solid-ink: var(--ol-on-accent);
|
||||
|
||||
/* Shared glass */
|
||||
--ol-glass-bg: rgba(255, 255, 255, 0.62);
|
||||
@@ -56,25 +56,6 @@
|
||||
|
||||
/* Frost grain texture used by the .ol-frost helpers in global.css. */
|
||||
--ol-frost-grain: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='100'%20height='100'%3E%3Cfilter%20id='g'%3E%3CfeTurbulence%20type='fractalNoise'%20baseFrequency='0.95'%20numOctaves='1'%20stitchTiles='stitch'/%3E%3CfeColorMatrix%20type='matrix'%20values='0.34%200.34%200.34%200%200%200.34%200.34%200.34%200%200%200.34%200.34%200.34%200%200%200%200%200%200%201'/%3E%3C/filter%3E%3Crect%20width='100'%20height='100'%20filter='url(%23g)'/%3E%3C/svg%3E");
|
||||
--ol-frost-bg:
|
||||
radial-gradient(135% 86% at 15% -10%,
|
||||
rgba(255, 255, 255, 0.97) 0%,
|
||||
rgba(255, 255, 255, 0) 56%),
|
||||
linear-gradient(161deg,
|
||||
rgba(255, 255, 255, 0.93) 0%,
|
||||
rgba(244, 247, 252, 0.88) 100%);
|
||||
--ol-frost-grain-opacity: 0.32;
|
||||
|
||||
/* Capsule circle buttons */
|
||||
--ol-capsule-cancel-bg: var(--ol-control-muted-strong);
|
||||
--ol-capsule-cancel-ink: var(--ol-ink);
|
||||
--ol-capsule-confirm-bg: var(--ol-control-solid);
|
||||
--ol-capsule-confirm-ink: var(--ol-ink);
|
||||
|
||||
/* Settings / form inputs */
|
||||
--ol-input-bg: var(--ol-surface-2);
|
||||
--ol-input-ink: var(--ol-ink);
|
||||
--ol-input-placeholder: var(--ol-placeholder);
|
||||
|
||||
/* Motion */
|
||||
--ol-motion-spring: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
@@ -100,9 +81,6 @@
|
||||
--ol-r-xl: 18px;
|
||||
--ol-r-2xl: var(--ol-card-radius);
|
||||
--ol-r-pill: var(--ol-pill-radius);
|
||||
--ol-control-radius: 8px;
|
||||
--ol-bubble-radius: var(--ol-r-lg);
|
||||
--ol-modal-radius: var(--ol-r-xl);
|
||||
|
||||
/* Typography */
|
||||
--ol-font-display: "Aptos", "Segoe UI Variable Display", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
@@ -115,8 +93,6 @@
|
||||
--ol-warn: #d97706;
|
||||
--ol-warn-soft: #fff7ed;
|
||||
--ol-err: #dc2626;
|
||||
--ol-danger-solid-bg: var(--ol-err);
|
||||
--ol-danger-solid-ink: #ffffff;
|
||||
|
||||
/* Theme-aware shell and control tokens */
|
||||
--ol-app-shell-bg:
|
||||
@@ -134,8 +110,8 @@
|
||||
--ol-panel-bg: var(--ol-aura-surface);
|
||||
--ol-panel-border: var(--ol-aura-glass-border);
|
||||
--ol-panel-shadow: var(--ol-aura-shadow);
|
||||
--ol-card-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.94), rgba(250, 250, 251, 0.86));
|
||||
--ol-card-border: rgba(255, 255, 255, 0.78);
|
||||
--ol-card-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(252, 252, 253, 0.82));
|
||||
--ol-card-border: rgba(255, 255, 255, 0.74);
|
||||
--ol-card-shadow: var(--ol-aura-shadow-soft);
|
||||
|
||||
--ol-sidebar-bg: linear-gradient(180deg, rgba(255,255,255,0.34), rgba(255,255,255,0.12));
|
||||
@@ -179,8 +155,8 @@
|
||||
--ol-pill-selected-ink: #ffffff;
|
||||
--ol-pill-selected-border: #111827;
|
||||
--ol-segmented-bg: rgba(0,0,0,0.04);
|
||||
--ol-segmented-active-bg: var(--ol-control-elevated);
|
||||
--ol-segmented-active-shadow: 0 1px 3px rgba(0,0,0,.08), 0 0 0 0.5px rgba(0,0,0,.08);
|
||||
--ol-segmented-active-bg: #ffffff;
|
||||
--ol-segmented-active-shadow: 0 1px 2px rgba(0,0,0,.06), 0 0 0 0.5px rgba(0,0,0,.06);
|
||||
--ol-toggle-off-bg: rgba(0,0,0,0.15);
|
||||
--ol-toggle-knob: #ffffff;
|
||||
--ol-overlay-bg: rgba(15,17,22,0.32);
|
||||
@@ -237,46 +213,20 @@
|
||||
--ol-ink: #f4f7fb;
|
||||
--ol-ink-2: #d8dfeb;
|
||||
--ol-ink-3: rgba(244, 247, 251, 0.74);
|
||||
--ol-ink-4: rgba(244, 247, 251, 0.54);
|
||||
--ol-ink-4: rgba(244, 247, 251, 0.50);
|
||||
--ol-ink-5: rgba(244, 247, 251, 0.26);
|
||||
--ol-ink-muted: var(--ol-ink-3);
|
||||
--ol-ink-subtle: var(--ol-ink-4);
|
||||
--ol-placeholder: var(--ol-ink-4);
|
||||
|
||||
--ol-blue: #60a5fa;
|
||||
--ol-blue-hover: #3b82f6;
|
||||
--ol-blue-soft: rgba(96, 165, 250, 0.14);
|
||||
--ol-blue-ring: rgba(96, 165, 250, 0.30);
|
||||
--ol-on-accent: #f8fbff;
|
||||
--ol-accent-solid-bg: #2563eb;
|
||||
--ol-accent-solid-bg-hover: #1d4ed8;
|
||||
--ol-accent-solid-ink: #ffffff;
|
||||
--ol-primary-solid-bg: var(--ol-accent-solid-bg);
|
||||
--ol-primary-solid-ink: var(--ol-accent-solid-ink);
|
||||
|
||||
--ol-glass-bg: rgba(15, 19, 27, 0.68);
|
||||
--ol-glass-bg-strong: rgba(17, 22, 30, 0.84);
|
||||
--ol-glass-border: rgba(255, 255, 255, 0.10);
|
||||
--ol-aura-glass-border: rgba(255, 255, 255, 0.10);
|
||||
|
||||
--ol-frost-bg:
|
||||
radial-gradient(135% 86% at 15% -10%,
|
||||
rgba(255, 255, 255, 0.10) 0%,
|
||||
rgba(255, 255, 255, 0) 56%),
|
||||
linear-gradient(161deg,
|
||||
rgba(22, 28, 38, 0.88) 0%,
|
||||
rgba(14, 18, 26, 0.82) 100%);
|
||||
--ol-frost-grain-opacity: 0.28;
|
||||
|
||||
--ol-capsule-cancel-bg: rgba(255, 255, 255, 0.10);
|
||||
--ol-capsule-cancel-ink: var(--ol-ink-2);
|
||||
--ol-capsule-confirm-bg: var(--ol-accent-solid-bg);
|
||||
--ol-capsule-confirm-ink: var(--ol-accent-solid-ink);
|
||||
|
||||
--ol-input-bg: rgba(255, 255, 255, 0.06);
|
||||
--ol-input-ink: var(--ol-ink);
|
||||
--ol-input-placeholder: var(--ol-ink-4);
|
||||
|
||||
--ol-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.34), 0 0 0 0.5px rgba(255, 255, 255, 0.04);
|
||||
--ol-shadow-md: 0 10px 30px -20px rgba(0, 0, 0, 0.42), 0 0 0 0.5px rgba(255, 255, 255, 0.05);
|
||||
--ol-shadow-lg: 0 24px 72px -32px rgba(0, 0, 0, 0.58), 0 0 0 0.5px rgba(255, 255, 255, 0.05);
|
||||
@@ -289,8 +239,6 @@
|
||||
--ol-warn: #f59e0b;
|
||||
--ol-warn-soft: rgba(245, 158, 11, 0.14);
|
||||
--ol-err: #f87171;
|
||||
--ol-danger-solid-bg: #b91c1c;
|
||||
--ol-danger-solid-ink: #ffffff;
|
||||
|
||||
--ol-app-shell-bg: linear-gradient(180deg, #050607 0%, #090a0c 100%);
|
||||
--ol-window-bg:
|
||||
@@ -303,9 +251,9 @@
|
||||
--ol-panel-bg: linear-gradient(180deg, rgba(33,33,36,0.96), rgba(28,28,31,0.94));
|
||||
--ol-panel-border: rgba(255,255,255,0.07);
|
||||
--ol-panel-shadow: 0 18px 44px -34px rgba(0,0,0,0.62);
|
||||
--ol-card-bg: linear-gradient(180deg, rgba(36,36,40,0.98), rgba(30,30,34,0.96));
|
||||
--ol-card-border: rgba(255,255,255,0.08);
|
||||
--ol-card-shadow: 0 12px 32px -24px rgba(0,0,0,0.55);
|
||||
--ol-card-bg: linear-gradient(180deg, rgba(34,34,37,0.96), rgba(29,29,32,0.94));
|
||||
--ol-card-border: rgba(255,255,255,0.06);
|
||||
--ol-card-shadow: none;
|
||||
|
||||
--ol-sidebar-bg: transparent;
|
||||
--ol-sidebar-border: rgba(255,255,255,0.06);
|
||||
@@ -348,8 +296,8 @@
|
||||
--ol-pill-selected-ink: #f4f7fb;
|
||||
--ol-pill-selected-border: rgba(96,165,250,0.40);
|
||||
--ol-segmented-bg: rgba(255,255,255,0.06);
|
||||
--ol-segmented-active-bg: var(--ol-control-elevated);
|
||||
--ol-segmented-active-shadow: 0 0 0 0.5px rgba(255,255,255,0.10);
|
||||
--ol-segmented-active-bg: rgba(255,255,255,0.12);
|
||||
--ol-segmented-active-shadow: none;
|
||||
--ol-toggle-off-bg: rgba(255,255,255,0.16);
|
||||
--ol-toggle-knob: #f8fbff;
|
||||
--ol-overlay-bg: rgba(4, 8, 14, 0.58);
|
||||
@@ -373,11 +321,11 @@
|
||||
--ol-style-add-tile-bg: rgba(255,255,255,0.02);
|
||||
--ol-style-add-tile-icon-bg: rgba(255,255,255,0.10);
|
||||
--ol-style-add-tile-border: rgba(255,255,255,0.10);
|
||||
--ol-style-input-bg: rgba(255,255,255,0.06);
|
||||
--ol-style-input-border: rgba(255,255,255,0.10);
|
||||
--ol-style-input-bg: rgba(255,255,255,0.04);
|
||||
--ol-style-input-border: rgba(255,255,255,0.08);
|
||||
--ol-style-input-shadow: none;
|
||||
--ol-style-subtle-bg: rgba(255,255,255,0.05);
|
||||
--ol-style-subtle-border: rgba(255,255,255,0.09);
|
||||
--ol-style-subtle-bg: rgba(255,255,255,0.03);
|
||||
--ol-style-subtle-border: rgba(255,255,255,0.06);
|
||||
--ol-style-subtle-shadow: none;
|
||||
--ol-style-editor-bg: linear-gradient(180deg, rgba(27,27,30,0.96), rgba(23,23,26,0.94));
|
||||
--ol-style-editor-border: rgba(255,255,255,0.07);
|
||||
|
||||
Reference in new issue
Block a user