Merge pull request #621 from Open-Less/fix/aura-ui-polish

revert(ui): 回退 aura 皮肤 + 钥匙串/彩虹边框/提示词修复 (1.3.6-3)
This commit is contained in:
TRIP authored and GitHub committed 2026-06-08 17:43:24 +08:00
commit 19ae8641d6
37 files changed
+374 -1053

No files matched your search

+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "openless-app",
"version": "1.3.6-2",
"version": "1.3.6-3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openless-app",
"version": "1.3.6-2",
"version": "1.3.6-3",
"dependencies": {
"@formkit/auto-animate": "^0.9.0",
"@tauri-apps/api": "^2.1.1",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "openless-app",
"private": true,
"version": "1.3.6-2",
"version": "1.3.6-3",
"type": "module",
"scripts": {
"dev": "vite",
+1 -1
View File
@@ -3698,7 +3698,7 @@ dependencies = [
[[package]]
name = "openless"
version = "1.3.6-2"
version = "1.3.6-3"
dependencies = [
"anyhow",
"arboard",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "openless"
version = "1.3.6-2"
version = "1.3.6-3"
description = "OpenLess — local voice input that types where your cursor is"
authors = ["OpenLess"]
edition = "2021"
+20 -8
View File
@@ -1483,18 +1483,22 @@ pub(crate) fn show_less_computer_glow<R: tauri::Runtime>(app: &AppHandle<R>) {
.ok()
.flatten()
.or_else(|| app.primary_monitor().ok().flatten());
if let Some(monitor) = monitor {
let scale = monitor.scale_factor();
let size = monitor.size();
let pos = monitor.position();
let _ = window.set_position(tauri::LogicalPosition::new(
// 逻辑坐标的「铺满整屏」矩形 (x, y, w, h)。f64 元组可 Copy:既在 show 前先铺一次,
// 也在主线程 realize(orderFront)后再铺一次(见下,修首次半屏 bug)。
let bounds: Option<(f64, f64, f64, f64)> = monitor.map(|m| {
let scale = m.scale_factor();
let size = m.size();
let pos = m.position();
(
pos.x as f64 / scale,
pos.y as f64 / scale,
));
let _ = window.set_size(tauri::LogicalSize::new(
size.width as f64 / scale,
size.height as f64 / scale,
));
)
});
if let Some((x, y, w, h)) = bounds {
let _ = window.set_position(tauri::LogicalPosition::new(x, y));
let _ = window.set_size(tauri::LogicalSize::new(w, h));
}
// 点击穿透:纯视觉浮层,绝不拦截鼠标。
let _ = window.set_ignore_cursor_events(true);
@@ -1522,6 +1526,14 @@ pub(crate) fn show_less_computer_glow<R: tauri::Runtime>(app: &AppHandle<R>) {
let _ = window_clone.show();
}
}
// 首次使用彩虹边框只画半屏并卡住:glow 窗口 conf 初始 800×600 且 visible:false,
// 首次 show 前从未 realize —— current_monitor() 取不到 / show 前的 set_size 没贴住整屏,
// webview 首帧按 800×600 画出半屏描边。这里在 realize(orderFront)之后**再铺满一次**,
// 强制 webview 按整屏重排重绘。后续使用窗口已 realize,show 前那次就够、不闪。
if let Some((x, y, w, h)) = bounds {
let _ = window_clone.set_position(tauri::LogicalPosition::new(x, y));
let _ = window_clone.set_size(tauri::LogicalSize::new(w, h));
}
});
}
+55 -50
View File
@@ -48,15 +48,6 @@ const LEGACY_CREDS_FILE: &str = "credentials.json";
const KEYRING_CREDENTIALS_ACCOUNT: &str = "credentials.v1";
const KEYRING_CREDENTIALS_CHUNK_PREFIX: &str = "credentials.v1.chunk.";
/// issue #609 F-03:history.json HMAC 完整性密钥的 keyring account。和凭据共用
/// `CredentialsVault::SERVICE_NAME`,但走独立 account,不掺进 chunked CredsRoot。
const KEYRING_HISTORY_HMAC_ACCOUNT: &str = "history.hmac_key.v1";
/// issue #609 C-01:「已启用 HMAC」标志的 keyring account(值固定为 "1")。
/// 一旦置位,sidecar 缺失就判定为攻击者删除(而非 legacy),fail-safe 拒绝。
/// 没有这个标志,攻击者只需删掉 `history.json.hmac` 就能把任意篡改历史伪装成
/// legacy 文件骗过 HMAC 校验。标志放 keyring(而非文件)正是因为它本身要抗删除。
const KEYRING_HISTORY_HMAC_ENROLLED_ACCOUNT: &str = "history.hmac_enrolled.v1";
const KEYRING_HISTORY_HMAC_ENROLLED_VALUE: &str = "1";
/// HMAC 密钥长度(字节)。
const HISTORY_HMAC_KEY_LEN: usize = 32;
/// history HMAC sidecar 文件后缀:`history.json.hmac`。
@@ -828,23 +819,49 @@ fn history_hmac_key() -> Option<Vec<u8>> {
.clone()
}
/// history HMAC 密钥 / enrolled 标志的 0o600 文件路径。
///
/// **为什么从 keyring 迁到文件**(原 #609 F-03 放 keychain):macOS 上每个钥匙串条目各自 ACL,
/// 且 ad-hoc 签名下「始终允许」不持久、条目删建即清空 ACL —— 导致用户**每次听写后反复弹钥匙串
/// 授权**(凭据 3 次之外又多出 HMAC 密钥 + enrolled 共 2 次)。HMAC 密钥与明文 history 同目录、
/// at-rest 防护同为 OS 文件权限(M-01),放文件与放 keychain 安全**收益对等**,却彻底消除钥匙串
/// 弹窗。真正的 API 密钥仍留在 keychain(CredentialsVault)。
fn history_hmac_key_path() -> Result<PathBuf> {
Ok(data_dir()?.join("history_hmac.key"))
}
fn history_hmac_enrolled_path() -> Result<PathBuf> {
Ok(data_dir()?.join(".history_hmac_enrolled.v1"))
}
fn load_or_create_history_hmac_key() -> Result<Vec<u8>> {
let _guard = credentials_lock().lock();
if let Some(hex_key) = get_keyring_password(KEYRING_HISTORY_HMAC_ACCOUNT)? {
let key_path = history_hmac_key_path()?;
if let Ok(hex_key) = fs::read_to_string(&key_path) {
let trimmed = hex_key.trim();
if !trimmed.is_empty() {
let key =
decode_hex(&hex_key).with_context(|| "history HMAC key in keyring is not valid hex")?;
decode_hex(trimmed).with_context(|| "history HMAC key file is not valid hex")?;
if key.len() == HISTORY_HMAC_KEY_LEN {
return Ok(key);
}
log::warn!("[history] keyring 里的 HMAC 密钥长度异常,重新生成");
log::warn!("[history] HMAC 密钥文件长度异常,重新生成");
}
// 生成 32 字节随机密钥并落盘 keyring。
}
// 密钥文件不存在 = 首次启用文件存储(含从旧 keychain 版升级)。生成新 key;旧 history 可能
// 是用「旧 keychain key」签的 —— 新 key 会让其 HMAC 不匹配而被误判篡改清空。迁移:删掉旧
// sidecar 与 enrolled 文件,让旧 history 走 legacy 路径被重新接受 + 用新 key 补签。全程不读
// 旧 keychain,零钥匙串弹窗、不丢历史。
let mut key = vec![0u8; HISTORY_HMAC_KEY_LEN];
getrandom::fill(&mut key).map_err(|e| anyhow!("OS CSPRNG 生成 HMAC 密钥失败:{e}"))?;
let hex_key = encode_hex(&key);
keyring_entry_for(KEYRING_HISTORY_HMAC_ACCOUNT)?
.set_password(&hex_key)
.context("写入 history HMAC 密钥到系统凭据库失败")?;
atomic_write_private(&key_path, encode_hex(&key).as_bytes())
.context("写入 history HMAC 密钥文件失败")?;
if let Ok(dir) = data_dir() {
let _ = fs::remove_file(history_hmac_sidecar_path(&dir.join(HISTORY_FILE)));
}
if let Ok(enrolled) = history_hmac_enrolled_path() {
let _ = fs::remove_file(enrolled);
}
Ok(key)
}
@@ -860,36 +877,28 @@ trait HmacEnrollment {
fn set_enrolled(&self);
}
/// 生产实现:标志落 keyring。读不到/写不了都按「未启用」处理(与 M-03 的退化一致)。
struct KeyringEnrollment;
/// 生产实现:enrolled 标志落 0o600 文件(不再用 keychain,原因见 history_hmac_key_path 注释:
/// 避免 ad-hoc 签名下每次听写反复弹钥匙串)。标志文件存在 = 已启用(曾写过 sidecar)。
/// 读不到 / 写不了都按「未启用」处理(退化与 M-03 一致)。
struct FileEnrollment;
impl HmacEnrollment for KeyringEnrollment {
impl HmacEnrollment for FileEnrollment {
fn is_enrolled(&self) -> bool {
let _guard = credentials_lock().lock();
match get_keyring_password(KEYRING_HISTORY_HMAC_ENROLLED_ACCOUNT) {
Ok(Some(v)) => v == KEYRING_HISTORY_HMAC_ENROLLED_VALUE,
Ok(None) => false,
Err(e) => {
log::warn!("[history] 读取 HMAC enrolled 标志失败,按未启用处理:{e}");
false
}
}
history_hmac_enrolled_path()
.map(|p| p.exists())
.unwrap_or(false)
}
fn set_enrolled(&self) {
let _guard = credentials_lock().lock();
// 已置位则不重复写,避免 macOS Keychain 反复弹 ACL 授权。
if let Ok(Some(v)) = get_keyring_password(KEYRING_HISTORY_HMAC_ENROLLED_ACCOUNT) {
if v == KEYRING_HISTORY_HMAC_ENROLLED_VALUE {
let Ok(path) = history_hmac_enrolled_path() else {
return;
};
// 已置位则不重复写(幂等)。
if path.exists() {
return;
}
}
match keyring_entry_for(KEYRING_HISTORY_HMAC_ENROLLED_ACCOUNT).and_then(|e| {
e.set_password(KEYRING_HISTORY_HMAC_ENROLLED_VALUE)
.map_err(Into::into)
}) {
Ok(()) => {}
Err(e) => log::warn!("[history] 置位 HMAC enrolled 标志失败:{e}"),
if let Err(e) = atomic_write_private(&path, b"1") {
log::warn!("[history] 置位 HMAC enrolled 标志文件失败:{e}");
}
}
}
@@ -1238,10 +1247,10 @@ fn write_account(root: &mut CredsRoot, account: CredentialAccount, value: Option
/// - 内容以**明文 JSON** 落盘(`DictationSession[]`),便于用户导出/审阅。
/// - 机密性靠 **OS 文件系统权限**:unix 下写入后收紧到 `0o600`(仅属主可读写);
/// Windows 走 `%APPDATA%` 的 per-user ACL。
/// - 完整性靠 **HMAC-SHA256**(F-03):密钥 32 字节随机、存系统凭据库
/// (`KEYRING_HISTORY_HMAC_ACCOUNT`);每次写入算 HMAC 写 sidecar
/// `history.json.hmac`;读取时校验,不匹配则 fail-safe 返回空历史,绝不把被篡改
/// 的历史喂给下游 LLM。
/// - 完整性靠 **HMAC-SHA256**(F-03):密钥 32 字节随机、存**同目录 0o600 文件**
/// (`history_hmac.key`,原 keyring 版因 ad-hoc 签名反复弹钥匙串而迁出,密钥与明文
/// history 同目录、安全收益对等);每次写入算 HMAC 写 sidecar `history.json.hmac`;
/// 读取时校验,不匹配则 fail-safe 返回空历史,绝不把被篡改的历史喂给下游 LLM。
///
/// **已知残留**:尚未做**完整静态加密(at-rest encryption)**——本地能读文件的
/// 攻击者仍可读到明文历史(但无法在不被发现的情况下篡改)。完整加密(用 keyring
@@ -1350,11 +1359,7 @@ impl HistoryStore {
/// 置位 enrolled 标志完成迁移(issue #609 C-01)。
/// - sidecar 缺失但**标志已置位** → 攻击者删了 sidecar 想伪装 legacy:fail-safe 返回空。
fn read_locked(&self) -> Result<Vec<DictationSession>> {
read_history_with_key(
&self.path,
history_hmac_key().as_deref(),
&KeyringEnrollment,
)
read_history_with_key(&self.path, history_hmac_key().as_deref(), &FileEnrollment)
}
/// issue #609 F-03/F-04/C-01:写 history 后算 HMAC 写 sidecar;unix 下把两文件都设
@@ -1365,7 +1370,7 @@ impl HistoryStore {
&self.path,
&json,
history_hmac_key().as_deref(),
&KeyringEnrollment,
&FileEnrollment,
)
}
}
+9 -1
View File
@@ -1195,7 +1195,15 @@ const OUTPUT_BLOCK: &str = "# 输出\n\
/// 自带 # 角色 + {{HOTWORDS}} + 八节主体(结构化判断、双层格式、首行收尾、ASR 纠错、
/// 原样保留、禁止事项、输出),因此 Structured 模式跳过标准 ROLE_BLOCK / COMMON_RULES /
/// OUTPUT_BLOCK wrapper,避免与 v2 内的同名段落重复。
const STRUCTURED_BUILTIN_PROMPT: &str = r#"# 角色
const STRUCTURED_BUILTIN_PROMPT: &str = r#"# ⚡ 第一指令(高于一切,先执行再看细则)
先数原文里有几件「可区分的事项」:
- **≥2 件 → 必须输出编号清单**(行首 1. 2. 3.),**禁止**把多件事揉成一整段。≥3 件还要按主题归类、子项另起一行用 (a) (b)。
- 恰好 1 件 → 才输出连贯段落。
判断依据是「事项数」,**不是**原文有没有标点 / 换行 / 已经编号。只要有 2 件以上事项却揉进一段话 = 直接失败。最终形态照本文末尾「# 示例」里的样子输出。
# 角色
你是「清晰结构」整理器。用户输入来自语音识别(ASR),常带错别字、同音字、英文术语音译、断句缺失、语序混乱、口语化表达等问题。
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "OpenLess",
"version": "1.3.6-2",
"version": "1.3.6-3",
"identifier": "com.openless.app",
"build": {
"beforeDevCommand": "npm run dev",
-11
View File
@@ -20,7 +20,6 @@ import { QaPanel } from './pages/QaPanel';
import { LessComputerPanel } from './pages/LessComputerPanel';
import { LessComputerGlow } from './pages/LessComputerGlow';
import { HotkeySettingsProvider } from './state/HotkeySettingsContext';
import { APP_THEME_KEY, applyAppTheme, readAppTheme } from './lib/appTheme';
interface AppProps {
isCapsule: boolean;
@@ -50,16 +49,6 @@ export function App({ isCapsule, isQa, isLessComputer, isLessComputerGlow, force
// Windows 启动不应被权限探测阻塞首屏。
const [gate, setGate] = useState<Gate>('ready');
useEffect(() => {
applyAppTheme(readAppTheme());
const syncTheme = (event: StorageEvent) => {
if (event.key !== APP_THEME_KEY) return;
applyAppTheme(readAppTheme());
};
window.addEventListener('storage', syncTheme);
return () => window.removeEventListener('storage', syncTheme);
}, []);
useEffect(() => {
if (!isTauri) return;
let cancelled = false;
@@ -12,7 +12,6 @@ import { invoke } from '@tauri-apps/api/core';
import type { DownloadEvent } from '@tauri-apps/plugin-updater';
import { Update } from '@tauri-apps/plugin-updater';
import { useTranslation } from 'react-i18next';
import { formatBytes as formatBytesShared } from '../lib/format';
import { isTauri, restartApp, type UpdateChannel } from '../lib/ipc';
import { Btn } from '../pages/_atoms';
@@ -241,7 +240,9 @@ export function UpdateDialog({
);
}
// 下载进度尺寸:封顶 MB、对非有限/<=0 值兜底成 0 B。
function formatBytes(value: number) {
return formatBytesShared(value, { maxUnit: 'MB', guard: true });
if (!Number.isFinite(value) || value <= 0) return '0 B';
if (value < 1024) return `${value} B`;
if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`;
return `${(value / 1024 / 1024).toFixed(1)} MB`;
}
+4 -3
View File
@@ -417,7 +417,8 @@ export function Capsule() {
// - 出场 .24s → .36s(前面 EXIT_ANIM_MS 也同步到 360),曲线改成 ease-in-out 平滑
// 收缩 + 下移 + 淡出三段同步进行
? 'capsule-out .36s cubic-bezier(.55,.06,.68,.19) forwards'
: 'capsule-in .38s cubic-bezier(.16,.86,.32,1.18) both',
// 入场改成弹性"弹出":整体 scale 从 .78 弹到 1,back-out 曲线带回弹 overshoot。
: 'capsule-in .46s cubic-bezier(.34,1.56,.64,1) both',
transformOrigin: 'center',
willChange: 'transform, opacity',
}}
@@ -483,8 +484,8 @@ export function Capsule() {
长出到 scaleX 1 / scaleY 1 / 不透明。配合 wrapper 的 transformOrigin:center,
视觉上是「从中心向左右展开」。 */
@keyframes capsule-in {
from { opacity: 0; transform: scaleX(.18) scaleY(.95); }
to { opacity: 1; transform: scaleX(1) scaleY(1); }
from { opacity: 0; transform: scale(.78) translateY(8px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
/* 离场:scaleX 由 1 收回 0.18 + 整体向下偏移 8px + 淡出。
forwards 让最终帧(opacity:0、scaleX:.18)保持到组件被卸载。 */
+69 -129
View File
@@ -181,10 +181,7 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
};
return (
<div
className="ol-app-shell-bg"
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
@@ -199,26 +196,22 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
{/* Sidebar — 透明地坐在外层磨砂底板上,让 LOGO/导航/快捷键/BETA/footer 共用同一片磨砂玻璃 */}
<aside
className="ol-aura-sidebar"
style={{
width: 196,
width: 188,
flexShrink: 0,
display: 'flex',
flexDirection: 'column',
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
@@ -227,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',
@@ -235,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,
@@ -248,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} />
@@ -265,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>
@@ -276,9 +301,17 @@ 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} />
@@ -287,13 +320,20 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
</div>
</aside>
{/* Main content — Linux 禁用透明窗口后使用不透明面;其他平台保留玻璃层。 */}
<div style={{ flex: 1, minWidth: 0, padding: '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',
}}
@@ -376,112 +416,12 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
color: var(--ol-ink-3);
font-weight: 500;
}
.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: 0;
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: 8px;
box-shadow: none;
}
.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: 12px;
border: 1px solid var(--ol-sidebar-pill-border);
box-shadow: none;
}
.ol-aura-sidebar-nav-btn {
padding: 8px 12px;
border-radius: 12px;
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: 999px;
}
.ol-aura-sidebar-settings {
padding: 10px 12px;
border-radius: 12px;
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);
}
.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);
background: rgba(0,0,0,0.04);
color: var(--ol-ink);
}
@keyframes ol-page-slide {
@@ -517,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)',
@@ -582,7 +522,7 @@ function ProviderSetupPrompt({ onLater, onOpenSettings }: { onLater: () => void;
borderRadius: 8,
border: 0,
background: 'var(--ol-ink)',
color: 'var(--ol-on-accent)',
color: '#fff',
fontFamily: 'inherit',
fontSize: 12.5,
fontWeight: 500,
@@ -610,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)',
@@ -675,7 +615,7 @@ function HotkeyModeMigrationPrompt({ onLater, onOpenSettings }: { onLater: () =>
borderRadius: 8,
border: 0,
background: 'var(--ol-ink)',
color: 'var(--ol-on-accent)',
color: '#fff',
fontFamily: 'inherit',
fontSize: 12.5,
fontWeight: 500,
@@ -35,7 +35,7 @@ export function MarketplaceModal({ onClose }: MarketplaceModalProps) {
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',
@@ -48,10 +48,10 @@ export function MarketplaceModal({ onClose }: MarketplaceModalProps) {
onClick={(e) => e.stopPropagation()}
style={{
width: '100%', maxWidth: 1080, height: '100%', maxHeight: 720,
background: 'var(--ol-style-editor-bg)',
borderRadius: 24,
border: '1px solid var(--ol-style-editor-border)',
boxShadow: 'var(--ol-style-editor-shadow)',
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)',
overflow: 'hidden',
position: 'relative',
display: 'flex', flexDirection: 'column',
@@ -71,12 +71,12 @@ export function MarketplaceModal({ onClose }: MarketplaceModalProps) {
style={{
display: 'inline-flex', alignItems: 'center', gap: 6,
padding: '4px 10px', borderRadius: 999,
border: loggedIn ? '1px solid var(--ol-style-subtle-border)' : '1px solid rgba(239,68,68,0.32)',
background: loggedIn ? 'var(--ol-style-subtle-bg)' : 'rgba(239,68,68,0.10)',
border: loggedIn ? '0.5px solid var(--ol-line)' : '0.5px solid rgba(239,68,68,0.32)',
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',
boxShadow: 'var(--ol-style-subtle-shadow)',
boxShadow: '0 1px 2px rgba(15,17,22,0.05)',
backdropFilter: 'blur(8px) saturate(140%)',
WebkitBackdropFilter: 'blur(8px) saturate(140%)',
}}
@@ -91,23 +91,23 @@ export function MarketplaceModal({ onClose }: MarketplaceModalProps) {
style={{
position: 'absolute', top: 16, right: 16, zIndex: 3,
width: 32, height: 32,
border: '1px solid var(--ol-style-subtle-border)',
border: '0.5px solid var(--ol-line)',
borderRadius: 8,
background: 'var(--ol-style-subtle-bg)',
background: 'rgba(255,255,255,0.85)',
color: 'var(--ol-ink-2)',
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
cursor: 'default',
boxShadow: 'var(--ol-style-subtle-shadow)',
boxShadow: '0 1px 2px rgba(15,17,22,0.05)',
backdropFilter: 'blur(8px) saturate(140%)',
WebkitBackdropFilter: 'blur(8px) saturate(140%)',
transition: 'background 0.16s var(--ol-motion-quick), color 0.16s var(--ol-motion-quick)',
}}
onMouseEnter={(e) => {
(e.currentTarget as HTMLButtonElement).style.background = 'var(--ol-control-solid)';
(e.currentTarget as HTMLButtonElement).style.background = 'var(--ol-surface)';
(e.currentTarget as HTMLButtonElement).style.color = 'var(--ol-ink)';
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLButtonElement).style.background = 'var(--ol-style-subtle-bg)';
(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')}
@@ -74,7 +74,7 @@ export function SettingsModal({ os: _os, onClose, initialSettingsSection }: Sett
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',
@@ -84,34 +84,32 @@ export function SettingsModal({ os: _os, onClose, initialSettingsSection }: Sett
}}>
<div
className="ol-aura-settings"
onClick={(e) => e.stopPropagation()}
style={{
width: '100%',
maxWidth: 920,
height: '100%',
maxHeight: 620,
display: 'flex',
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: 214,
flexShrink: 0,
display: 'flex',
flexDirection: '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 style={{ position: 'relative', display: 'flex', flexDirection: 'column', gap: 1 }}>
{pillRect && (
<div
className="ol-aura-settings-pill"
aria-hidden
style={{
position: 'absolute',
@@ -119,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,
@@ -132,7 +133,7 @@ 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'}
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>
@@ -142,12 +143,12 @@ export function SettingsModal({ os: _os, onClose, initialSettingsSection }: Sett
</div>
{/* 外链组 */}
<div className="ol-aura-settings-links">
<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 ol-aura-settings-nav-btn"
className="ol-nav-btn"
style={navBtnStyle}>
<Icon name={it.icon} size={14} />
<span style={{ flex: 1 }}>{t(`modal.sections.${it.id}`)}</span>
@@ -160,17 +161,7 @@ export function SettingsModal({ os: _os, onClose, initialSettingsSection }: Sett
{/* ─── 内容区 ──────────────────────────────────────────────
父容器 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}
@@ -179,19 +170,22 @@ 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: 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>
@@ -211,69 +205,18 @@ export function SettingsModal({ os: _os, onClose, initialSettingsSection }: Sett
</div>
</div>
</div>
<style>{`
.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-rail {
padding: 20px 14px;
gap: 16px;
background: var(--ol-settings-rail-bg);
border-right: 1px solid var(--ol-settings-rail-border);
}
.ol-aura-settings-pill {
background: var(--ol-sidebar-pill-bg);
border-radius: 12px;
border: 1px solid var(--ol-sidebar-pill-border);
box-shadow: none;
}
.ol-aura-settings-nav-btn {
padding: 7px 10px;
border-radius: 12px;
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-links {
display: flex;
flex-direction: column;
gap: 1px;
padding-top: 10px;
border-top: 1px solid var(--ol-settings-links-border);
}
.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);
}
`}</style>
</div>
);
}
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)',
};
@@ -39,11 +39,13 @@ export function WindowChrome({
// macOS / Windows 共用半透明玻璃 background + backdropFilter。
// macOS: NSVisualEffectView 提供材质;Windows: Tauri apply_mica 提供 Mica;
// Linux: decorations:false 后 CSS 磨砂玻璃自成背景。
//
// 注意:三层渐变的参数与 global.css 中 [data-ol-no-compositing] .ol-winchrome
// 的回退 background 同步(只 opacity 从 0.92 提到 0.96)。改这里时请同步更新 CSS。
const background = 'var(--ol-window-bg)';
// Linux: 透明窗口 / WebKitGTK 合成层不稳定,走不透明 surface。
const background = `
radial-gradient(120% 80% at 0% 0%, rgba(255,255,255,0.55) 0%, rgba(255,255,255,0) 60%),
radial-gradient(100% 70% at 100% 100%, rgba(37,99,235,0.07) 0%, rgba(37,99,235,0) 55%),
linear-gradient(180deg, rgba(245,245,247,0.92) 0%, rgba(232,232,236,0.92) 100%)
`;
const useSolidSurface = os === 'linux';
return (
<div
@@ -60,10 +62,10 @@ export function WindowChrome({
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
border: os === 'win' ? 'none' : os === 'mac' ? 'none' : '0.5px solid var(--ol-window-border)',
background,
backdropFilter: 'blur(var(--ol-glass-blur-strong)) saturate(190%)',
WebkitBackdropFilter: 'blur(var(--ol-glass-blur-strong)) saturate(190%)',
border: os === 'win' ? 'none' : os === 'mac' ? 'none' : '0.5px solid rgba(0,0,0,.10)',
background: useSolidSurface ? 'var(--ol-surface)' : background,
backdropFilter: useSolidSurface ? 'none' : 'blur(var(--ol-glass-blur-strong)) saturate(190%)',
WebkitBackdropFilter: useSolidSurface ? 'none' : 'blur(var(--ol-glass-blur-strong)) saturate(190%)',
animation: os === 'win' ? undefined : 'ol-window-enter 0.42s var(--ol-motion-spring) both',
transition: 'box-shadow 0.28s var(--ol-motion-soft), border-color 0.28s var(--ol-motion-soft), backdrop-filter 0.28s var(--ol-motion-soft)',
willChange: 'opacity, transform, filter',
@@ -156,10 +158,10 @@ function LinuxTitlebar() {
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 6px 0 14px',
background: 'var(--ol-linux-titlebar-bg)',
backdropFilter: 'blur(12px) saturate(180%)',
WebkitBackdropFilter: 'blur(12px) saturate(180%)',
borderBottom: '0.5px solid var(--ol-linux-titlebar-border)',
background: 'var(--ol-surface)',
backdropFilter: 'none',
WebkitBackdropFilter: 'none',
borderBottom: '0.5px solid rgba(0,0,0,0.08)',
color: 'var(--ol-ink-3)',
fontSize: 13,
fontWeight: 500,
@@ -10,7 +10,7 @@ interface SegSimpleProps {
export function SegSimple({ options, active }: SegSimpleProps) {
const [v, setV] = useState(active);
return (
<div style={{ display: 'inline-flex', padding: 2, borderRadius: 8, background: 'var(--ol-segmented-bg)' }}>
<div style={{ display: 'inline-flex', padding: 2, borderRadius: 8, background: 'rgba(0,0,0,0.05)' }}>
{options.map((o) => (
<button
key={o}
@@ -18,9 +18,9 @@ export function SegSimple({ options, active }: SegSimpleProps) {
style={{
padding: '5px 12px', fontSize: 12, fontWeight: 500, border: 0, borderRadius: 6,
fontFamily: 'inherit',
background: v === o ? 'var(--ol-segmented-active-bg)' : 'transparent',
background: v === o ? '#fff' : 'transparent',
color: v === o ? 'var(--ol-ink)' : 'var(--ol-ink-3)',
boxShadow: v === o ? 'var(--ol-segmented-active-shadow)' : 'none',
boxShadow: v === o ? '0 1px 2px rgba(0,0,0,.08)' : 'none',
cursor: 'default',
}}
>
@@ -15,7 +15,7 @@ export function SwitchLite({ on: initial = false }: SwitchLiteProps) {
onClick={() => setOn(!on)}
style={{
position: 'relative', width: 32, height: 18, borderRadius: 999, border: 0,
background: on ? 'var(--ol-blue)' : 'var(--ol-toggle-off-bg)',
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: 999, 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)',
}}
/>
-4
View File
@@ -921,10 +921,6 @@ export const en: typeof zhCN = {
releaseNotes: 'Release notes',
},
personalize: {
theme: 'Theme',
themeDesc: 'Switch the app shell between light and dark appearance.',
themeLight: 'Light',
themeDark: 'Dark',
font: 'Font size',
fontDesc: 'Scale the entire UI font size — applies instantly.',
fontSmall: 'Small',
-4
View File
@@ -923,10 +923,6 @@ export const ja: typeof zhCN = {
releaseNotes: 'リリースノート',
},
personalize: {
theme: 'テーマ',
themeDesc: 'アプリ外観をライトとダークで切り替えます。',
themeLight: 'ライト',
themeDark: 'ダーク',
font: 'フォントサイズ',
fontDesc: 'UI のフォントサイズを全体的にスケール。即時反映。',
fontSmall: '小',
-4
View File
@@ -923,10 +923,6 @@ export const ko: typeof zhCN = {
releaseNotes: '릴리스 노트',
},
personalize: {
theme: '테마',
themeDesc: '앱 외관을 라이트와 다크로 전환합니다.',
themeLight: '라이트',
themeDark: '다크',
font: '글꼴 크기',
fontDesc: 'UI 글꼴 크기를 전체 스케일. 즉시 반영.',
fontSmall: '소',
-4
View File
@@ -919,10 +919,6 @@ export const zhCN = {
releaseNotes: '发布日志',
},
personalize: {
theme: '主题',
themeDesc: '切换应用外观的浅色与深色版本。',
themeLight: '浅色',
themeDark: '深色',
font: '字体大小',
fontDesc: '整体缩放界面字号,立即生效。',
fontSmall: '小',
-4
View File
@@ -921,10 +921,6 @@ export const zhTW: typeof zhCN = {
releaseNotes: '發佈日誌',
},
personalize: {
theme: '主題',
themeDesc: '切換應用外觀的淺色與深色版本。',
themeLight: '淺色',
themeDark: '深色',
font: '字體大小',
fontDesc: '整體縮放界面字號,立即生效。',
fontSmall: '小',
-26
View File
@@ -1,26 +0,0 @@
export type AppThemeId = 'light' | 'dark';
export const APP_THEME_KEY = 'ol-app-theme';
export function readAppTheme(): AppThemeId {
try {
const value = window.localStorage.getItem(APP_THEME_KEY);
if (value === 'light' || value === 'dark') return value;
} catch {
// Ignore storage access failures and fall back to the default theme.
}
return 'light';
}
export function applyAppTheme(theme: AppThemeId): void {
document.documentElement.dataset.olTheme = theme;
}
export function setAppTheme(theme: AppThemeId): void {
applyAppTheme(theme);
try {
window.localStorage.setItem(APP_THEME_KEY, theme);
} catch {
// Ignore storage write failures.
}
}
-71
View File
@@ -1,71 +0,0 @@
// 跨页面共享的格式化工具。把原本散落在 Overview/History/LocalAsr/AutoUpdate/
// settings 各处重复(且行为略有差异)的 clamp / formatTime / formatDuration /
// formatBytes 收口到一处,调用方用参数区分各自需要的分支,行为保持不变。
import type { useTranslation } from 'react-i18next';
type TFn = ReturnType<typeof useTranslation>['t'];
/** 把 n 夹到 [min, max]。 */
export function clamp(n: number, min: number, max: number): number {
return Math.max(min, Math.min(max, n));
}
/**
* 把 ISO 时间格式化为简短显示。
* - 当天:`HH:MM`
* - 非当天:默认 `M/D`;`withTimeOnOtherDays` 为 true 时为 `M/D HH:MM`。
* 无效时间原样返回输入串。
*/
export function formatTime(iso: string, opts?: { withTimeOnOtherDays?: boolean }): string {
const d = new Date(iso);
if (isNaN(d.getTime())) return iso;
const now = new Date();
const sameDay = d.toDateString() === now.toDateString();
const pad = (n: number) => String(n).padStart(2, '0');
if (sameDay) return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
if (opts?.withTimeOnOtherDays) {
return `${d.getMonth() + 1}/${d.getDate()} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
return `${d.getMonth() + 1}/${d.getDate()}`;
}
/**
* 把毫秒时长格式化。<=0 / null 返回占位符 `—`。
* <60s 用 i18n `common.durationSeconds`;>=60s 默认用 i18n `common.durationMinutes`,
* `minutesAsClock` 为 true 时改用 `M:SS` 时钟格式。
*/
export function formatDuration(
ms: number | null,
t: TFn,
opts?: { minutesAsClock?: boolean }
): string {
if (ms == null || ms <= 0) return '—';
const sec = ms / 1000;
if (sec < 60) return t('common.durationSeconds', { value: sec.toFixed(1) });
if (opts?.minutesAsClock) {
return `${Math.floor(sec / 60)}:${String(Math.floor(sec % 60)).padStart(2, '0')}`;
}
return t('common.durationMinutes', { value: (sec / 60).toFixed(1) });
}
/**
* 把字节数格式化为人类可读尺寸。
* - `maxUnit`:封顶单位(默认 GB);'MB' 时不再升到 GB。
* - `mbDigits`:MB 档小数位(AutoUpdate 用 1,LocalAsr 用 0)。
* - `guard`:非有限或 <=0 时返回 `0 B`(AutoUpdate 下载进度需要)。
*/
export function formatBytes(
n: number,
opts?: { maxUnit?: 'MB' | 'GB'; mbDigits?: number; guard?: boolean }
): string {
const maxUnit = opts?.maxUnit ?? 'GB';
const mbDigits = opts?.mbDigits ?? 1;
if (opts?.guard && (!Number.isFinite(n) || n <= 0)) return '0 B';
if (n < 1024) return `${n} B`;
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
if (maxUnit === 'MB' || n < 1024 * 1024 * 1024) {
return `${(n / 1024 / 1024).toFixed(mbDigits)} MB`;
}
return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
}
-3
View File
@@ -3,7 +3,6 @@ import ReactDOM from "react-dom/client";
import { App } from "./App";
import { detectOS } from "./components/WindowChrome";
import i18n from "./i18n"; // 副作用:触发 i18next init
import { applyAppTheme, readAppTheme } from "./lib/appTheme";
import "./styles/tokens.css";
import "./styles/global.css";
@@ -19,8 +18,6 @@ const osQuery = params.get("os") as OS | null;
const os = osQuery ?? detectOS();
document.documentElement.dataset.olPlatform = os;
applyAppTheme(readAppTheme());
const root = ReactDOM.createRoot(document.getElementById("root")!);
const renderApp = () => {
+14 -3
View File
@@ -5,7 +5,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Icon } from '../components/Icon';
import { detectOS } from '../components/WindowChrome';
import { formatDuration, formatTime as formatTimeShared } from '../lib/format';
import { formatComboLabel } from '../lib/hotkey';
import { clearHistory, deleteHistoryEntry, listHistory, readAudioRecording } from '../lib/ipc';
import type { DictationSession, PolishMode } from '../lib/types';
@@ -400,7 +399,19 @@ function AudioRecordingPlayer({
);
}
// History 非当天显示带时分(与 Overview 只显示 M/D 不同)。
function formatTime(iso: string): string {
return formatTimeShared(iso, { withTimeOnOtherDays: true });
const d = new Date(iso);
if (isNaN(d.getTime())) return iso;
const now = new Date();
const sameDay = d.toDateString() === now.toDateString();
const pad = (n: number) => String(n).padStart(2, '0');
if (sameDay) return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
return `${d.getMonth() + 1}/${d.getDate()} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
function formatDuration(ms: number | null, t: ReturnType<typeof useTranslation>['t']): string {
if (ms == null || ms <= 0) return '—';
const sec = ms / 1000;
if (sec < 60) return t('common.durationSeconds', { value: sec.toFixed(1) });
return t('common.durationMinutes', { value: (sec / 60).toFixed(1) });
}
+4 -3
View File
@@ -78,7 +78,6 @@ import {
type SherpaOnnxModelAlias,
type SherpaPrepareProgress,
} from "../lib/localAsr"
import { formatBytes as formatBytesShared } from "../lib/format"
import { useHotkeySettings } from "../state/HotkeySettingsContext"
import { detectOS } from "../components/WindowChrome"
import { SelectLite } from "../components/ui/SelectLite"
@@ -3389,7 +3388,9 @@ function formatFoundrySizeMb(
return Math.round(fileSizeMb).toLocaleString()
}
// 模型尺寸:升到 GB、MB 档取整(与 AutoUpdate 的 1 位小数下载进度不同)。
function formatBytes(n: number): string {
return formatBytesShared(n, { maxUnit: "GB", mbDigits: 0 })
if (n < 1024) return `${n} B`
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`
if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(0)} MB`
return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`
}
+20 -16
View File
@@ -3,7 +3,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Icon } from '../components/Icon';
import { formatDuration as formatDurationShared, formatTime } from '../lib/format';
import { formatComboLabel } from '../lib/hotkey';
import { getCredentials, listHistory } from '../lib/ipc';
import type { CredentialsStatus, DictationSession, PolishMode } from '../lib/types';
@@ -171,16 +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')} />
<div
className="ol-overview-hero"
style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14, marginBottom: 20 }}
>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 18 }}>
<ProviderCard
kind={t('overview.asrKind')}
name={asrProviderName}
@@ -195,7 +187,7 @@ export function Overview({ onOpenHistory }: OverviewProps) {
/>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 14, marginBottom: 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')} />
@@ -205,8 +197,8 @@ export function Overview({ onOpenHistory }: OverviewProps) {
{/* 底部一行 = flex:1 撑满剩余高度(父 wrapper 是 display:flex/column)。
只有「最近识别」内部允许滚动;其他卡片按内容自然高度,不破裂底部圆角。
issue #243 follow-up:去掉外层 overflow 后底部圆角被裁的视觉问题。 */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1.4fr', gap: 14, flex: 1, minHeight: 0 }}>
<Card className="ol-overview-hero" padding={20} style={{ display: 'flex', flexDirection: 'column', minHeight: 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>
@@ -223,7 +215,7 @@ export function Overview({ onOpenHistory }: OverviewProps) {
</div>
</Card>
<Card className="ol-overview-hero" padding={0} style={{ display: 'flex', flexDirection: 'column', minHeight: 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>
@@ -369,9 +361,21 @@ function RecentRow({ session, modeLabel }: { session: DictationSession; modeLabe
);
}
// Overview 用 M:SS 时钟格式展示时长(与 History 的「分钟」i18n 文案不同)。
function formatTime(iso: string): string {
const d = new Date(iso);
if (isNaN(d.getTime())) return iso;
const now = new Date();
const sameDay = d.toDateString() === now.toDateString();
const pad = (n: number) => String(n).padStart(2, '0');
if (sameDay) return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
return `${d.getMonth() + 1}/${d.getDate()}`;
}
function formatDuration(ms: number, t: ReturnType<typeof useTranslation>['t']): string {
return formatDurationShared(ms, t, { minutesAsClock: true });
if (ms <= 0) return '—';
const sec = ms / 1000;
if (sec < 60) return t('common.durationSeconds', { value: sec.toFixed(1) });
return `${Math.floor(sec / 60)}:${String(Math.floor(sec % 60)).padStart(2, '0')}`;
}
function weekDayLabels(names: string[]): string[] {
+48 -101
View File
@@ -551,20 +551,7 @@ export function Style() {
/>
)}
<Card
padding={0}
glassy
style={{
overflow: 'hidden',
flex: '1 1 0',
minHeight: 0,
display: 'flex',
flexDirection: 'column',
background: 'var(--ol-style-shell-bg)',
border: '1px solid var(--ol-style-shell-border)',
boxShadow: 'var(--ol-style-shell-shadow)',
}}
>
<Card padding={0} style={{ overflow: 'hidden', flex: '1 1 0', minHeight: 0, display: 'flex', flexDirection: 'column' }}>
<div style={{ padding: 18, borderBottom: '0.5px solid var(--ol-line)', flexShrink: 0 }}>
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap', minWidth: 0 }}>
@@ -585,16 +572,13 @@ export function Style() {
padding: '6px 12px',
borderRadius: 999,
border: '0.5px solid',
borderColor: rawPack.active ? 'var(--ol-blue)' : 'var(--ol-style-subtle-border)',
background: rawPack.active ? 'rgba(96,165,250,0.18)' : 'var(--ol-style-subtle-bg)',
borderColor: rawPack.active ? 'var(--ol-blue)' : 'var(--ol-line-strong)',
background: rawPack.active ? 'var(--ol-blue-soft)' : 'transparent',
color: rawPack.active ? 'var(--ol-blue)' : 'var(--ol-ink-2)',
fontSize: 12.5,
fontWeight: rawPack.active ? 600 : 500,
whiteSpace: 'nowrap',
cursor: rawPack.active ? 'default' : 'pointer',
backdropFilter: 'blur(18px) saturate(150%)',
WebkitBackdropFilter: 'blur(18px) saturate(150%)',
boxShadow: 'var(--ol-style-subtle-shadow)',
transition: 'border-color 0.16s var(--ol-motion-quick), background 0.16s var(--ol-motion-quick), color 0.16s var(--ol-motion-quick)',
}}
>
@@ -607,7 +591,7 @@ export function Style() {
</div>
</div>
<div className="ol-thinscroll" style={{ padding: 18, overflow: 'auto', flex: '1 1 0', minHeight: 0 }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(220px, 100%), 1fr))', gap: 12 }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))', gap: 12 }}>
<AnimatePresence mode="sync">
{bodyPacks.map(pack => {
const isBuiltin = pack.kind === 'builtin';
@@ -628,23 +612,24 @@ export function Style() {
flexDirection: 'column',
textAlign: 'left',
position: 'relative',
border: '1px 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)',
backdropFilter: 'blur(28px) saturate(165%)',
WebkitBackdropFilter: 'blur(28px) saturate(165%)',
border: '0.5px solid',
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,
overflow: 'hidden',
}}
>
<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: 700, color: '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">
@@ -661,7 +646,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',
@@ -680,11 +665,9 @@ export function Style() {
style={{
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,
backdropFilter: 'blur(14px)',
WebkitBackdropFilter: 'blur(14px)',
}}
>
<Icon name="sparkle" size={16} />
@@ -700,10 +683,8 @@ export function Style() {
width: 36, height: 36, borderRadius: 12,
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
flexShrink: 0,
border: '1px solid var(--ol-style-delete-border)',
background: 'var(--ol-style-delete-bg)',
backdropFilter: 'blur(14px)',
WebkitBackdropFilter: 'blur(14px)',
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,
@@ -774,12 +755,10 @@ export function Style() {
justifyContent: 'center',
gap: 8,
textAlign: 'center',
border: '1px dashed var(--ol-style-add-tile-border)',
border: '0.5px dashed var(--ol-line-strong)',
borderRadius: 18,
padding: 16,
background: 'var(--ol-style-add-tile-bg)',
backdropFilter: 'blur(24px) saturate(150%)',
WebkitBackdropFilter: 'blur(24px) saturate(150%)',
background: 'transparent',
color: 'var(--ol-ink-3)',
cursor: busy === 'creating' ? 'wait' : 'pointer',
opacity: busy === 'creating' ? 0.55 : 1,
@@ -791,10 +770,8 @@ export function Style() {
style={{
width: 44, height: 44, borderRadius: 999,
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
background: 'var(--ol-style-add-tile-icon-bg)',
background: 'rgba(15,23,42,0.04)',
color: 'var(--ol-ink-2)',
backdropFilter: 'blur(14px)',
WebkitBackdropFilter: 'blur(14px)',
}}
>
<Icon name="plus" size={22} />
@@ -850,9 +827,7 @@ export function Style() {
display: 'grid',
gridTemplateRows: 'auto minmax(0, 1fr)',
overflow: 'hidden',
background: 'var(--ol-style-editor-bg)',
border: '1px solid var(--ol-style-editor-border)',
boxShadow: 'var(--ol-style-editor-shadow)',
boxShadow: '0 24px 80px rgba(15,23,42,0.22)',
}}
>
<div style={{ padding: 18, borderBottom: '0.5px solid var(--ol-line)' }}>
@@ -1015,11 +990,8 @@ export function Style() {
<Card
padding={16}
style={{
background: 'var(--ol-style-subtle-bg)',
border: '1px solid var(--ol-style-subtle-border)',
boxShadow: 'var(--ol-style-subtle-shadow)',
backdropFilter: 'blur(20px) saturate(150%)',
WebkitBackdropFilter: 'blur(20px) saturate(150%)',
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 }}>
@@ -1085,12 +1057,9 @@ export function Style() {
<div
style={{
padding: 14,
borderRadius: 16,
background: 'var(--ol-style-subtle-bg)',
border: '1px solid var(--ol-style-subtle-border)',
boxShadow: 'var(--ol-style-subtle-shadow)',
backdropFilter: 'blur(18px) saturate(145%)',
WebkitBackdropFilter: 'blur(18px) saturate(145%)',
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 }}>
@@ -1126,11 +1095,7 @@ export function Style() {
key={`${draft.id}-example-${index}`}
padding={16}
style={{
background: 'var(--ol-style-subtle-bg)',
border: '1px solid var(--ol-style-subtle-border)',
boxShadow: 'var(--ol-style-subtle-shadow)',
backdropFilter: 'blur(18px) saturate(145%)',
WebkitBackdropFilter: 'blur(18px) saturate(145%)',
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 }}>
@@ -1163,13 +1128,10 @@ export function Style() {
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))', gap: 12 }}>
<div
style={{
borderRadius: 16,
border: '1px 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,
boxShadow: 'var(--ol-style-subtle-shadow)',
backdropFilter: 'blur(16px) saturate(145%)',
WebkitBackdropFilter: 'blur(16px) saturate(145%)',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
@@ -1178,19 +1140,16 @@ export function Style() {
<textarea
value={example.input}
onChange={event => patchExample(index, { input: event.target.value })}
style={{ ...textareaStyle, minHeight: 120 }}
style={{ ...textareaStyle, minHeight: 120, background: '#fff' }}
/>
</div>
<div
style={{
borderRadius: 16,
border: '1px solid rgba(96,165,250,0.24)',
background: 'color-mix(in srgb, var(--ol-style-subtle-bg) 82%, rgba(96,165,250,0.16) 18%)',
borderRadius: 14,
border: '0.5px solid rgba(37,99,235,0.16)',
background: 'rgba(239,246,255,0.86)',
padding: 14,
boxShadow: 'var(--ol-style-subtle-shadow)',
backdropFilter: 'blur(16px) saturate(145%)',
WebkitBackdropFilter: 'blur(16px) saturate(145%)',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
@@ -1199,7 +1158,7 @@ export function Style() {
<textarea
value={example.output}
onChange={event => patchExample(index, { output: event.target.value })}
style={{ ...textareaStyle, minHeight: 120 }}
style={{ ...textareaStyle, minHeight: 120, background: '#fff' }}
/>
</div>
</div>
@@ -1221,13 +1180,10 @@ function MetaItem({ label, value }: { label: string; value: string }) {
return (
<div
style={{
borderRadius: 14,
border: '1px solid var(--ol-style-subtle-border)',
background: 'var(--ol-style-subtle-bg)',
borderRadius: 12,
border: '0.5px solid rgba(148,163,184,0.2)',
background: 'rgba(255,255,255,0.92)',
padding: '10px 12px',
boxShadow: 'var(--ol-style-subtle-shadow)',
backdropFilter: 'blur(16px) saturate(145%)',
WebkitBackdropFilter: 'blur(16px) saturate(145%)',
}}
>
<div style={{ fontSize: 11, textTransform: 'uppercase', letterSpacing: '.08em', color: 'var(--ol-ink-4)', marginBottom: 6 }}>
@@ -1261,12 +1217,9 @@ function DirectiveRow({
justifyContent: 'space-between',
gap: 12,
padding: '10px 12px',
borderRadius: 14,
border: '1px solid var(--ol-style-subtle-border)',
background: 'var(--ol-style-subtle-bg)',
boxShadow: 'var(--ol-style-subtle-shadow)',
backdropFilter: 'blur(16px) saturate(145%)',
WebkitBackdropFilter: 'blur(16px) saturate(145%)',
borderRadius: 12,
border: '0.5px solid rgba(148,163,184,0.2)',
background: 'rgba(255,255,255,0.92)',
}}
>
<div style={{ minWidth: 0 }}>
@@ -1285,30 +1238,24 @@ const inputStyle: CSSProperties = {
boxSizing: 'border-box',
minHeight: 38,
padding: '9px 11px',
borderRadius: 12,
border: '1px solid var(--ol-style-input-border)',
background: 'var(--ol-style-input-bg)',
borderRadius: 10,
border: '0.5px solid var(--ol-line-strong)',
background: '#fff',
color: 'var(--ol-ink)',
font: 'inherit',
fontSize: 12.5,
boxShadow: 'var(--ol-style-input-shadow)',
backdropFilter: 'blur(16px) saturate(145%)',
WebkitBackdropFilter: 'blur(16px) saturate(145%)',
};
const textareaStyle: CSSProperties = {
width: '100%',
boxSizing: 'border-box',
padding: '11px 12px',
borderRadius: 14,
border: '1px solid var(--ol-style-input-border)',
background: 'var(--ol-style-input-bg)',
borderRadius: 12,
border: '0.5px solid var(--ol-line-strong)',
background: '#fff',
color: 'var(--ol-ink)',
font: 'inherit',
fontSize: 12.5,
lineHeight: 1.65,
resize: 'vertical',
boxShadow: 'var(--ol-style-input-shadow)',
backdropFilter: 'blur(16px) saturate(145%)',
WebkitBackdropFilter: 'blur(16px) saturate(145%)',
};
+25 -53
View File
@@ -15,39 +15,16 @@ interface PageHeaderProps {
export function PageHeader({ kicker, title, desc, right, titleRight }: PageHeaderProps) {
return (
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 24, marginBottom: 28 }}>
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 24, marginBottom: 24 }}>
<div style={{ minWidth: 0 }}>
{kicker && (
<div
style={{
fontSize: 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: 34,
fontWeight: 600,
letterSpacing: '-0.04em',
color: 'var(--ol-ink)',
fontFamily: 'var(--ol-font-display)',
}}
>
{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: '10px 0 0', fontSize: 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>
@@ -65,15 +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={{
background: glassy ? 'var(--ol-panel-bg)' : 'var(--ol-card-bg)',
backdropFilter: glassy ? 'blur(var(--ol-aura-glass-blur)) saturate(150%)' : undefined,
WebkitBackdropFilter: glassy ? 'blur(var(--ol-aura-glass-blur)) saturate(150%)' : undefined,
border: `1px solid ${glassy ? 'var(--ol-panel-border)' : 'var(--ol-card-border)'}`,
borderRadius: 'var(--ol-card-radius)',
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: glassy ? 'var(--ol-panel-shadow)' : 'var(--ol-card-shadow)',
boxShadow: 'var(--ol-shadow-sm)',
...style,
}}
>
@@ -94,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={{
@@ -111,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,
@@ -144,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-blue), var(--ol-blue-hover))', color: 'var(--ol-on-accent)', 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 }> = {
@@ -162,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',
@@ -9,8 +9,6 @@ import { Icon } from '../../components/Icon';
import { Row } from '../../components/ui/Row';
import { openExternal } from '../../lib/ipc';
import { APP_VERSION_LABEL } from '../../lib/appVersion';
import { readAppTheme, setAppTheme, type AppThemeId } from '../../lib/appTheme';
import { readFontScale, setFontScale, type FontScaleId } from '../../lib/fontScale';
import { Card } from '../_atoms';
import { SectionTitle } from './shared';
import { CheckUpdateButton } from './CheckUpdateButton';
@@ -52,12 +50,7 @@ export function AboutSection() {
</div>
</Card>
{/* ─── 个性化(字体大小)—— 原 personalize tab 并入此处 ──────────── */}
<Card>
<SectionTitle>{t('modal.sections.personalize')}</SectionTitle>
<ThemeRow />
<FontSizeRow />
</Card>
{/* 个性化(字体大小)已按需求移除(页面瘦身)。 */}
{/* ─── 文档链接 ─────────────────────────────────────────────── */}
<Card>
@@ -97,102 +90,10 @@ export function AboutSection() {
);
}
function ThemeRow() {
const { t } = useTranslation();
const [theme, setThemeState] = useState<AppThemeId>(() => readAppTheme());
const themeOptions: Array<[AppThemeId, string]> = [
['light', t('modal.personalize.themeLight')],
['dark', t('modal.personalize.themeDark')],
];
return (
<Row label={t('modal.personalize.theme')} desc={t('modal.personalize.themeDesc')}>
<div style={{ display: 'flex', gap: 4, padding: 2, background: 'var(--ol-segmented-bg)', borderRadius: 10 }}>
{themeOptions.map(([id, label]) => {
const selected = theme === id;
return (
<button
key={id}
onClick={() => {
setThemeState(id);
setAppTheme(id);
}}
style={{
minWidth: 74,
height: 30,
border: 0,
borderRadius: 8,
background: selected ? 'var(--ol-segmented-active-bg)' : 'transparent',
color: selected ? 'var(--ol-ink)' : 'var(--ol-ink-3)',
fontFamily: 'inherit',
fontSize: 12,
fontWeight: selected ? 600 : 500,
cursor: 'default',
boxShadow: selected ? 'var(--ol-segmented-active-shadow)' : 'none',
transition: 'background 0.16s var(--ol-motion-quick), color 0.16s var(--ol-motion-quick), box-shadow 0.18s var(--ol-motion-soft)',
padding: '0 12px',
}}
>
{label}
</button>
);
})}
</div>
</Row>
);
}
// 字体大小 —— 整体缩放界面字号,立即生效(fontScale.ts 走 html.style.zoom)。
function FontSizeRow() {
const { t } = useTranslation();
const [fontScale, setFontScaleState] = useState<FontScaleId>(() => readFontScale());
const applyFontScaleChoice = (next: FontScaleId) => {
setFontScaleState(next);
setFontScale(next);
};
const fontOptions: Array<[FontScaleId, string]> = [
['small', t('modal.personalize.fontSmall')],
['medium', t('modal.personalize.fontMedium')],
['large', t('modal.personalize.fontLarge')],
];
return (
<Row label={t('modal.personalize.font')} desc={t('modal.personalize.fontDesc')}>
<div style={{ display: 'flex', gap: 4, padding: 2, background: 'var(--ol-segmented-bg)', borderRadius: 10 }}>
{fontOptions.map(([id, label]) => {
const selected = fontScale === id;
return (
<button
key={id}
onClick={() => applyFontScaleChoice(id)}
style={{
minWidth: 64,
height: 28,
border: 0,
borderRadius: 8,
background: selected ? 'var(--ol-segmented-active-bg)' : 'transparent',
color: selected ? 'var(--ol-ink)' : 'var(--ol-ink-3)',
fontFamily: 'inherit',
fontSize: 12,
fontWeight: selected ? 600 : 500,
cursor: 'default',
boxShadow: selected ? 'var(--ol-segmented-active-shadow)' : 'none',
transition: 'background 0.16s var(--ol-motion-quick), color 0.16s var(--ol-motion-quick), box-shadow 0.18s var(--ol-motion-soft)',
padding: '0 12px',
}}
>
{label}
</button>
);
})}
</div>
</Row>
);
}
const btnGhost: CSSProperties = {
padding: '5px 10px', fontSize: 12, borderRadius: 6,
border: '0.5px solid var(--ol-line-strong)',
background: 'var(--ol-control-solid)', color: 'var(--ol-ink-2)',
background: '#fff', color: 'var(--ol-ink-2)',
cursor: 'default', fontFamily: 'inherit',
transition: 'background 0.16s var(--ol-motion-quick), border-color 0.16s var(--ol-motion-quick)',
};
@@ -72,7 +72,7 @@ export function CheckUpdateButton({ channel }: { channel: UpdateChannel }) {
const checkBtnStyle: CSSProperties = {
padding: '5px 10px', fontSize: 12, borderRadius: 6,
border: '0.5px solid var(--ol-line-strong)',
background: 'var(--ol-control-solid)',
background: '#fff',
cursor: 'default', fontFamily: 'inherit',
display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 6,
minWidth: 84,
@@ -2,11 +2,13 @@
// 自 Settings.tsx 的 RecordingSection「历史与上下文」折叠组拆出,逻辑零改动。
import { useTranslation } from 'react-i18next';
import { clamp } from '../../lib/format';
import { useHotkeySettings } from '../../state/HotkeySettingsContext';
import { Card } from '../_atoms';
import { SettingRow, SectionTitle, inputStyle } from './shared';
// 范围限制:retention 0-365 天,context window 0-60 分钟(再大对实际对话场景没意义且白烧 token)。
const clamp = (n: number, min: number, max: number) => Math.max(min, Math.min(max, n));
export function DataStorageSection() {
const { t } = useTranslation();
const { prefs, updatePrefs: savePrefs } = useHotkeySettings();
@@ -4,12 +4,13 @@
import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { clamp } from '../../lib/format';
import { exportErrorLog } from '../../lib/ipc';
import { useHotkeySettings } from '../../state/HotkeySettingsContext';
import { Btn, Card } from '../_atoms';
import { SettingRow, Toggle, SectionTitle, inputStyle } from './shared';
const clamp = (n: number, min: number, max: number) => Math.max(min, Math.min(max, n));
export function DebugToolsSection() {
const { t } = useTranslation();
const { prefs, updatePrefs: savePrefs } = useHotkeySettings();
+9 -40
View File
@@ -26,26 +26,12 @@ export function SectionTitle({
)
}
export function SectionDesc({
children,
style,
}: {
// 页面瘦身:设置页描述文案全部隐藏(保留组件签名 + 调用点,便于需要时恢复)。
export function SectionDesc(_props: {
children: ReactNode
style?: CSSProperties
}) {
return (
<div
style={{
fontSize: 12.5,
color: "var(--ol-ink-3)",
lineHeight: 1.6,
marginBottom: 16,
...style,
}}
>
{children}
</div>
)
return null
}
interface SettingRowProps {
@@ -55,9 +41,9 @@ interface SettingRowProps {
controlWidth?: number | string
}
// 页面瘦身:不再渲染每行的描述小字(desc 仍保留在 props 里,调用点无需改、便于恢复)。
export function SettingRow({
label,
desc,
children,
controlWidth,
}: SettingRowProps) {
@@ -69,15 +55,10 @@ export function SettingRow({
gap: 16,
padding: "14px 0",
borderTop: "0.5px solid var(--ol-line-soft)",
alignItems: desc ? "flex-start" : "center",
}}
>
<div
style={{
minWidth: 0,
alignSelf: desc ? "flex-start" : "center",
alignItems: "center",
}}
>
<div style={{ minWidth: 0, alignSelf: "center" }}>
<div
style={{
fontSize: 13,
@@ -87,18 +68,6 @@ export function SettingRow({
>
{label}
</div>
{desc && (
<div
style={{
fontSize: 11.5,
color: "var(--ol-ink-4)",
marginTop: 4,
lineHeight: 1.5,
}}
>
{desc}
</div>
)}
</div>
<div
style={{
@@ -130,7 +99,7 @@ export function Toggle({
height: 20,
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)",
@@ -144,7 +113,7 @@ export function Toggle({
width: 16,
height: 16,
borderRadius: 999,
background: "var(--ol-toggle-knob)",
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)",
@@ -163,7 +132,7 @@ export const inputStyle: CSSProperties = {
fontSize: 12.5,
fontFamily: "inherit",
outline: "none",
background: "var(--ol-control-elevated)",
background: "var(--ol-surface-2)",
width: "100%",
maxWidth: 360,
transition:
+1 -19
View File
@@ -14,9 +14,9 @@ html[data-ol-platform="linux"] #root {
}
body {
background: transparent;
user-select: none;
-webkit-user-select: none;
background: var(--ol-bg-base);
}
#root {
@@ -70,24 +70,6 @@ input, textarea {
a { color: inherit; text-decoration: none; }
.ol-app-shell-bg {
background: var(--ol-app-shell-bg);
}
.ol-aura-panel {
background: var(--ol-panel-bg);
backdrop-filter: blur(var(--ol-aura-glass-blur)) saturate(150%);
-webkit-backdrop-filter: blur(var(--ol-aura-glass-blur)) saturate(150%);
border: 1px solid var(--ol-panel-border);
box-shadow: var(--ol-panel-shadow);
}
.ol-aura-card {
background: var(--ol-card-bg);
border: 1px solid var(--ol-card-border);
box-shadow: var(--ol-card-shadow);
}
@keyframes ol-window-enter {
from {
opacity: 0;
+13 -263
View File
@@ -1,26 +1,15 @@
/* OpenLess design tokens with Aura-specific extensions. */
/* OpenLess design tokens — black + white + electric blue accent, glassy */
:root {
/* Shared palette */
/* Palette — neutrals */
--ol-white: #ffffff;
--ol-canvas: #f7f7f8;
--ol-canvas: #f7f7f8; /* outer chrome wash */
--ol-surface: #ffffff;
--ol-surface-2: #fafafa;
--ol-surface-solid: #ffffff;
--ol-line: rgba(0, 0, 0, 0.08);
--ol-line-strong: rgba(0, 0, 0, 0.14);
--ol-line-soft: rgba(0, 0, 0, 0.05);
/* Aura palette */
--ol-bg-base: #f3f4f6;
--ol-bg-elevated: #fbfbfc;
--ol-aura-surface: rgba(255, 255, 255, 0.72);
--ol-aura-surface-2: rgba(255, 255, 255, 0.88);
--ol-aura-surface-solid: #ffffff;
--ol-aura-line: rgba(15, 23, 42, 0.08);
--ol-aura-line-strong: rgba(15, 23, 42, 0.14);
--ol-aura-line-soft: rgba(255, 255, 255, 0.55);
/* Ink */
--ol-ink: #0a0a0b;
--ol-ink-2: #2a2a2d;
@@ -33,20 +22,19 @@
--ol-blue-hover: #1d4ed8;
--ol-blue-soft: #eff4ff;
--ol-blue-ring: rgba(37, 99, 235, 0.22);
--ol-on-accent: #ffffff;
/* Shared glass */
/* Glass */
--ol-glass-bg: rgba(255, 255, 255, 0.62);
--ol-glass-bg-strong: rgba(255, 255, 255, 0.78);
--ol-glass-border: rgba(255, 255, 255, 0.7);
--ol-glass-blur: 20px;
--ol-glass-blur-strong: 36px;
/* Aura glass */
--ol-aura-glass-border: rgba(255, 255, 255, 0.58);
--ol-aura-glass-blur: 24px;
/* Frost grain texture used by the .ol-frost helpers in global.css. */
/* 假毛玻璃噪点 —— qa 追问框 / capsule 语音胶囊等透明浮窗用。详见 global.css .ol-frost。
关键点:feColorMatrix 用 matrix 把分形噪点**去色 + 强制 alpha=1**(不透明灰度噪点)。
早期版本用 saturate=0 保留了 feTurbulence 的随机 alpha,噪点本身就半透明,再叠
::before 的低 opacity,等于洗没了 —— 看不出磨砂颗粒。现在用不透明灰度噪点,由
::before 的 opacity 单独、可控地决定颗粒强度。 */
--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");
/* Motion */
@@ -59,263 +47,25 @@
--ol-shadow-md: 0 1px 2px rgba(15, 17, 22, 0.05), 0 6px 24px -12px rgba(15, 17, 22, 0.10), 0 0 0 0.5px rgba(0, 0, 0, 0.04);
--ol-shadow-lg: 0 20px 60px -20px rgba(15, 17, 22, 0.18), 0 8px 32px -16px rgba(15, 17, 22, 0.10), 0 0 0 0.5px rgba(0, 0, 0, 0.06);
--ol-shadow-xl: 0 40px 120px -40px rgba(15, 17, 22, 0.30), 0 24px 60px -24px rgba(15, 17, 22, 0.15), 0 0 0 0.5px rgba(0, 0, 0, 0.06);
--ol-aura-shadow: 0 24px 80px -32px rgba(15, 23, 42, 0.24), 0 12px 36px -18px rgba(15, 23, 42, 0.12);
--ol-aura-shadow-soft: 0 10px 30px -18px rgba(15, 23, 42, 0.16), 0 0 0 0.5px rgba(255, 255, 255, 0.55) inset;
/* Radii */
--ol-shell-radius: 32px;
--ol-panel-radius: 28px;
--ol-card-radius: 22px;
--ol-pill-radius: 999px;
--ol-r-sm: 6px;
--ol-r-md: 10px;
--ol-r-lg: 14px;
--ol-r-xl: 18px;
--ol-r-2xl: var(--ol-card-radius);
--ol-r-pill: var(--ol-pill-radius);
--ol-r-2xl: 22px;
--ol-r-pill: 999px;
/* Typography */
--ol-font-display: "Aptos", "Segoe UI Variable Display", "PingFang SC", "Microsoft YaHei", sans-serif;
--ol-font-sans: system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
--ol-font-mono: "SF Mono", "Cascadia Code", Consolas, monospace;
/* Status colors */
/* Status colors (sparse use only) */
--ol-ok: #16a34a;
--ol-ok-soft: #ecfdf5;
--ol-warn: #d97706;
--ol-warn-soft: #fff7ed;
--ol-err: #dc2626;
/* Theme-aware shell and control tokens */
--ol-app-shell-bg:
radial-gradient(circle at top left, rgba(255, 255, 255, 0.68), rgba(255, 255, 255, 0) 38%),
radial-gradient(circle at 86% 18%, rgba(47, 109, 246, 0.08), rgba(47, 109, 246, 0) 30%),
linear-gradient(180deg, var(--ol-bg-elevated) 0%, var(--ol-bg-base) 100%);
--ol-window-bg:
radial-gradient(120% 80% at 0% 0%, rgba(255,255,255,0.55) 0%, rgba(255,255,255,0) 60%),
radial-gradient(100% 70% at 100% 100%, rgba(37,99,235,0.07) 0%, rgba(37,99,235,0) 55%),
linear-gradient(180deg, rgba(245,245,247,0.92) 0%, rgba(232,232,236,0.92) 100%);
--ol-window-border: rgba(0, 0, 0, 0.10);
--ol-linux-titlebar-bg: rgba(245, 245, 247, 0.85);
--ol-linux-titlebar-border: rgba(0, 0, 0, 0.08);
--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.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));
--ol-sidebar-border: rgba(255,255,255,0.38);
--ol-sidebar-brand-bg: linear-gradient(180deg, rgba(255,255,255,0.72), rgba(255,255,255,0.36));
--ol-sidebar-brand-border: rgba(255,255,255,0.50);
--ol-sidebar-pill-bg: linear-gradient(180deg, rgba(255,255,255,0.84), rgba(255,255,255,0.58));
--ol-sidebar-pill-border: rgba(255,255,255,0.62);
--ol-sidebar-footer-border: rgba(255,255,255,0.36);
--ol-sidebar-version-bg: linear-gradient(180deg, rgba(255,255,255,0.68), rgba(255,255,255,0.34));
--ol-sidebar-version-border: rgba(255,255,255,0.56);
--ol-sidebar-settings-bg: linear-gradient(180deg, rgba(255,255,255,0.48), rgba(255,255,255,0.24));
--ol-sidebar-settings-border: rgba(255,255,255,0.50);
--ol-sidebar-settings-active-bg: linear-gradient(180deg, rgba(255,255,255,0.82), rgba(255,255,255,0.48));
--ol-nav-hover-bg: rgba(0, 0, 0, 0.04);
--ol-settings-rail-bg: linear-gradient(180deg, rgba(255,255,255,0.58), rgba(246,248,252,0.72));
--ol-settings-rail-border: rgba(255,255,255,0.52);
--ol-settings-links-border: rgba(255,255,255,0.46);
--ol-settings-content-bg: linear-gradient(180deg, rgba(255,255,255,0.56), rgba(249,250,252,0.74));
--ol-settings-close-bg: rgba(255,255,255,0.38);
--ol-settings-close-hover-bg: rgba(255,255,255,0.72);
--ol-control-muted: rgba(255,255,255,0.42);
--ol-control-muted-strong: rgba(255,255,255,0.58);
--ol-control-border: rgba(255,255,255,0.60);
--ol-control-solid: var(--ol-surface);
--ol-control-elevated: var(--ol-surface-2);
--ol-control-shadow: 0 10px 20px -18px rgba(15,23,42,0.2);
--ol-pill-bg: linear-gradient(180deg, rgba(255,255,255,0.84), rgba(245,247,250,0.68));
--ol-pill-border: rgba(148,163,184,0.22);
--ol-pill-shadow: inset 0 1px 0 0 rgba(255,255,255,0.72), 0 10px 20px -18px rgba(15,23,42,0.18);
--ol-pill-blue-bg: rgba(96,165,250,0.14);
--ol-pill-blue-border: rgba(96,165,250,0.24);
--ol-pill-ok-bg: rgba(74,222,128,0.14);
--ol-pill-ok-border: rgba(74,222,128,0.22);
--ol-pill-dark-bg: linear-gradient(180deg, rgba(28,34,45,0.92), rgba(18,23,31,0.88));
--ol-pill-dark-border: rgba(15,23,42,0.10);
--ol-pill-dark-shadow: 0 12px 20px -18px rgba(15,23,42,0.22);
--ol-segmented-bg: rgba(0,0,0,0.04);
--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);
--ol-style-shell-bg: linear-gradient(180deg, rgba(255,255,255,0.84), rgba(249,250,252,0.72));
--ol-style-shell-border: rgba(255,255,255,0.72);
--ol-style-shell-shadow: 0 32px 80px -40px rgba(15, 23, 42, 0.26);
--ol-style-card-bg: linear-gradient(180deg, rgba(255,255,255,0.96), rgba(247,249,252,0.90));
--ol-style-card-bg-active: linear-gradient(180deg, rgba(255,255,255,0.98), rgba(244,248,255,0.94));
--ol-style-card-border: rgba(255,255,255,0.82);
--ol-style-card-border-active: rgba(96,165,250,0.82);
--ol-style-card-shadow: 0 24px 48px -34px rgba(15, 23, 42, 0.18), inset 0 1px 0 0 rgba(255,255,255,0.65);
--ol-style-card-shadow-active: 0 24px 56px -28px rgba(37,99,235,0.28), 0 0 0 1px rgba(96,165,250,0.78), 0 0 0 4px rgba(96,165,250,0.16), inset 0 1px 0 0 rgba(255,255,255,0.72);
--ol-style-card-ink: #0f172a;
--ol-style-card-ink-2: rgba(15, 23, 42, 0.82);
--ol-style-card-ink-3: rgba(15, 23, 42, 0.56);
--ol-style-card-icon-bg: rgba(226,232,240,0.62);
--ol-style-card-icon-bg-active: rgba(96,165,250,0.14);
--ol-style-delete-bg: rgba(255,255,255,0.56);
--ol-style-delete-border: rgba(248,113,113,0.36);
--ol-style-add-tile-bg: rgba(255,255,255,0.12);
--ol-style-add-tile-icon-bg: rgba(255,255,255,0.48);
--ol-style-add-tile-border: rgba(255,255,255,0.32);
--ol-style-input-bg: rgba(255,255,255,0.86);
--ol-style-input-border: rgba(255,255,255,0.74);
--ol-style-input-shadow: inset 0 1px 0 0 rgba(255,255,255,0.84), 0 10px 24px -22px rgba(15,23,42,0.16);
--ol-style-subtle-bg: rgba(255,255,255,0.74);
--ol-style-subtle-border: rgba(255,255,255,0.62);
--ol-style-subtle-shadow: inset 0 1px 0 0 rgba(255,255,255,0.74);
--ol-style-editor-bg: linear-gradient(180deg, rgba(255,255,255,0.92), rgba(247,249,252,0.82));
--ol-style-editor-border: rgba(255,255,255,0.74);
--ol-style-editor-shadow: 0 32px 96px -32px rgba(15,23,42,0.34);
}
[data-ol-theme='dark'] {
--ol-white: #11141a;
--ol-canvas: #0b0e13;
--ol-surface: #141922;
--ol-surface-2: #1a202b;
--ol-surface-solid: #171d27;
--ol-line: rgba(255, 255, 255, 0.09);
--ol-line-strong: rgba(255, 255, 255, 0.16);
--ol-line-soft: rgba(255, 255, 255, 0.07);
--ol-bg-base: #0a0d12;
--ol-bg-elevated: #10151d;
--ol-aura-surface: rgba(17, 23, 31, 0.76);
--ol-aura-surface-2: rgba(22, 29, 39, 0.88);
--ol-aura-surface-solid: #171d26;
--ol-aura-line: rgba(255, 255, 255, 0.10);
--ol-aura-line-strong: rgba(255, 255, 255, 0.16);
--ol-aura-line-soft: rgba(255, 255, 255, 0.08);
--ol-ink: #f4f7fb;
--ol-ink-2: #d8dfeb;
--ol-ink-3: rgba(244, 247, 251, 0.74);
--ol-ink-4: rgba(244, 247, 251, 0.50);
--ol-ink-5: rgba(244, 247, 251, 0.26);
--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-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-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);
--ol-shadow-xl: 0 48px 120px -52px rgba(0, 0, 0, 0.72), 0 0 0 0.5px rgba(255, 255, 255, 0.06);
--ol-aura-shadow: 0 28px 90px -44px rgba(0, 0, 0, 0.72), 0 12px 36px -24px rgba(2, 8, 20, 0.48);
--ol-aura-shadow-soft: 0 18px 40px -28px rgba(0, 0, 0, 0.62), 0 0 0 0.5px rgba(255, 255, 255, 0.06) inset;
--ol-ok: #4ade80;
--ol-ok-soft: rgba(74, 222, 128, 0.14);
--ol-warn: #f59e0b;
--ol-warn-soft: rgba(245, 158, 11, 0.14);
--ol-err: #f87171;
--ol-app-shell-bg: linear-gradient(180deg, #050607 0%, #090a0c 100%);
--ol-window-bg:
radial-gradient(120% 80% at 0% 0%, rgba(255,255,255,0.04) 0%, rgba(255,255,255,0) 42%),
linear-gradient(180deg, rgba(31,31,34,0.98) 0%, rgba(23,23,25,0.98) 100%);
--ol-window-border: rgba(255,255,255,0.08);
--ol-linux-titlebar-bg: rgba(15, 20, 28, 0.90);
--ol-linux-titlebar-border: rgba(255,255,255,0.08);
--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(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);
--ol-sidebar-brand-bg: transparent;
--ol-sidebar-brand-border: transparent;
--ol-sidebar-pill-bg: rgba(255,255,255,0.06);
--ol-sidebar-pill-border: rgba(255,255,255,0.07);
--ol-sidebar-footer-border: rgba(255,255,255,0.07);
--ol-sidebar-version-bg: transparent;
--ol-sidebar-version-border: transparent;
--ol-sidebar-settings-bg: transparent;
--ol-sidebar-settings-border: transparent;
--ol-sidebar-settings-active-bg: rgba(255,255,255,0.08);
--ol-nav-hover-bg: rgba(255,255,255,0.05);
--ol-settings-rail-bg: rgba(255,255,255,0.02);
--ol-settings-rail-border: rgba(255,255,255,0.06);
--ol-settings-links-border: rgba(255,255,255,0.06);
--ol-settings-content-bg: linear-gradient(180deg, rgba(29,29,32,0.96), rgba(24,24,27,0.94));
--ol-settings-close-bg: rgba(255,255,255,0.06);
--ol-settings-close-hover-bg: rgba(255,255,255,0.14);
--ol-control-muted: rgba(255,255,255,0.05);
--ol-control-muted-strong: rgba(255,255,255,0.08);
--ol-control-border: rgba(255,255,255,0.08);
--ol-control-solid: rgba(24,31,41,0.96);
--ol-control-elevated: rgba(21,27,36,0.98);
--ol-control-shadow: none;
--ol-pill-bg: rgba(255,255,255,0.05);
--ol-pill-border: rgba(255,255,255,0.08);
--ol-pill-shadow: none;
--ol-pill-blue-bg: rgba(59,130,246,0.14);
--ol-pill-blue-border: rgba(96,165,250,0.28);
--ol-pill-ok-bg: rgba(74,222,128,0.16);
--ol-pill-ok-border: rgba(74,222,128,0.24);
--ol-pill-dark-bg: rgba(255,255,255,0.08);
--ol-pill-dark-border: rgba(255,255,255,0.12);
--ol-pill-dark-shadow: none;
--ol-segmented-bg: rgba(255,255,255,0.06);
--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);
--ol-style-shell-bg: linear-gradient(180deg, rgba(29,29,32,0.94), rgba(24,24,27,0.92));
--ol-style-shell-border: rgba(255,255,255,0.07);
--ol-style-shell-shadow: none;
--ol-style-card-bg: rgba(34,34,37,0.94);
--ol-style-card-bg-active: rgba(39,39,43,0.96);
--ol-style-card-border: rgba(255,255,255,0.07);
--ol-style-card-border-active: rgba(96,165,250,0.72);
--ol-style-card-shadow: none;
--ol-style-card-shadow-active: 0 0 0 1px rgba(96,165,250,0.72);
--ol-style-card-ink: #f4f7fb;
--ol-style-card-ink-2: rgba(244,247,251,0.86);
--ol-style-card-ink-3: rgba(244,247,251,0.64);
--ol-style-card-icon-bg: rgba(255,255,255,0.06);
--ol-style-card-icon-bg-active: rgba(96,165,250,0.16);
--ol-style-delete-bg: rgba(255,255,255,0.05);
--ol-style-delete-border: rgba(248,113,113,0.34);
--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.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.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);
--ol-style-editor-shadow: none;
}
* { box-sizing: border-box; }
@@ -333,7 +83,7 @@ body {
.ol-noscrollbar::-webkit-scrollbar { display: none; }
.ol-noscrollbar { scrollbar-width: none; }
/* Thin scrollbar */
/* Thin scrollbar — 细而可见,保留"可以滚"的可见性,但不抢眼。issue #243 */
.ol-thinscroll::-webkit-scrollbar { width: 6px; height: 6px; }
.ol-thinscroll::-webkit-scrollbar-track { background: transparent; }
.ol-thinscroll::-webkit-scrollbar-thumb {