Merge pull request #621 from Open-Less/fix/aura-ui-polish
revert(ui): 回退 aura 皮肤 + 钥匙串/彩虹边框/提示词修复 (1.3.6-3)
This commit is contained in:
37 files changed
+374
-1053
No files matched your search
Generated
+2
-2
@@ -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,7 +1,7 @@
|
||||
{
|
||||
"name": "openless-app",
|
||||
"private": true,
|
||||
"version": "1.3.6-2",
|
||||
"version": "1.3.6-3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
Generated
+1
-1
@@ -3698,7 +3698,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openless"
|
||||
version = "1.3.6-2"
|
||||
version = "1.3.6-3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arboard",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
@@ -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)保持到组件被卸载。 */
|
||||
|
||||
@@ -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)',
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -923,10 +923,6 @@ export const ja: typeof zhCN = {
|
||||
releaseNotes: 'リリースノート',
|
||||
},
|
||||
personalize: {
|
||||
theme: 'テーマ',
|
||||
themeDesc: 'アプリ外観をライトとダークで切り替えます。',
|
||||
themeLight: 'ライト',
|
||||
themeDark: 'ダーク',
|
||||
font: 'フォントサイズ',
|
||||
fontDesc: 'UI のフォントサイズを全体的にスケール。即時反映。',
|
||||
fontSmall: '小',
|
||||
|
||||
@@ -923,10 +923,6 @@ export const ko: typeof zhCN = {
|
||||
releaseNotes: '릴리스 노트',
|
||||
},
|
||||
personalize: {
|
||||
theme: '테마',
|
||||
themeDesc: '앱 외관을 라이트와 다크로 전환합니다.',
|
||||
themeLight: '라이트',
|
||||
themeDark: '다크',
|
||||
font: '글꼴 크기',
|
||||
fontDesc: 'UI 글꼴 크기를 전체 스케일. 즉시 반영.',
|
||||
fontSmall: '소',
|
||||
|
||||
@@ -919,10 +919,6 @@ export const zhCN = {
|
||||
releaseNotes: '发布日志',
|
||||
},
|
||||
personalize: {
|
||||
theme: '主题',
|
||||
themeDesc: '切换应用外观的浅色与深色版本。',
|
||||
themeLight: '浅色',
|
||||
themeDark: '深色',
|
||||
font: '字体大小',
|
||||
fontDesc: '整体缩放界面字号,立即生效。',
|
||||
fontSmall: '小',
|
||||
|
||||
@@ -921,10 +921,6 @@ export const zhTW: typeof zhCN = {
|
||||
releaseNotes: '發佈日誌',
|
||||
},
|
||||
personalize: {
|
||||
theme: '主題',
|
||||
themeDesc: '切換應用外觀的淺色與深色版本。',
|
||||
themeLight: '淺色',
|
||||
themeDark: '深色',
|
||||
font: '字體大小',
|
||||
fontDesc: '整體縮放界面字號,立即生效。',
|
||||
fontSmall: '小',
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
@@ -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,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 = () => {
|
||||
|
||||
@@ -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) });
|
||||
}
|
||||
@@ -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`
|
||||
}
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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%)',
|
||||
};
|
||||
@@ -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();
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user