polish: 集成 review 反馈 + 用户新 surface 的 Windows 视觉修复
review 阶段标的 4 个 High 全部处理,加上用户在 v1.3.1-4 装机后新报的 2 个 Windows 视觉问题,本 commit 一次性收: [review-capsule High] - Capsule.tsx 抽 EXIT_ANIM_MS / INITIAL_VISIBLE_STATE 两个 const,去掉 `240` magic number + `isTauri ? 'idle' : 'recording'` 字面量重复 - 修正退出动画调度的 race 描述注释(cleanup clearTimeout 阻止 fire, 不是"定时器仍会触发") [review-qwen3 High] - LocalAsr.tsx Windows Qwen3 disabled wrapper 加 `inert` HTML5 属性, 让键盘 Tab 也跳过 disabled 区域(仅靠 pointerEvents:none 不挡键盘 + aria-disabled 不传给后代)。两个 wrapper 都加。@ts-expect-error 注释解释为何这里需要绕过 TS lib.dom 旧版未收录 inert 属性。 [review-icons 2 High] - Icon.tsx `overview`: 在三个 dashboard tile 顶部加 sparkline 数据线 区分 layout icon(之前几乎一样,14px nav 视觉无法分辨) - Icon.tsx `selectionAsk`: 气泡尾巴 y=24 → y≈23,避免 strokeLinecap round + strokeWidth 1.5 出 viewBox 底边被裁 [用户新 surface — Vocab 词条大椭圆] - Vocab.tsx VocabChip 加 `alignSelf: 'flex-start'`。真根因(之前 PR #422 误判为 button user-agent appearance):父 flex 容器 minHeight: 80 让 flex item 默认 align-self: stretch 把单个 chip 拉伸到 80px 高, borderRadius:999 渲染成超大椭圆。alignSelf:flex-start 阻止拉伸。 [用户新 surface — 顶栏 vs 玻璃色差] - lib.rs DWMWA_CAPTION_COLOR 从纯白 0xFFFFFF → 0x00F7F5F5 (rgb 245,245,247) 跟 WindowChrome glass linear-gradient 起始色一致 - WindowChrome.tsx glass alpha 0.78 → 0.92(PR #422 为加强玻璃感降的, 现在为压缩色差升回去,玻璃感由 backdrop-filter blur+saturate 提供) 后续:用户希望调研"完全取消 Windows 原生顶栏(像微信那样自绘)"—— 属于 v1.3.1-6 级别 large rewrite,单独追。
This commit is contained in:
1 parent
f44eb5958c
commit
35d53b8be2
6 files changed
+39
-17
No files matched your search
@@ -627,14 +627,16 @@ fn apply_windows_caption_color<R: Runtime>(window: &tauri::WebviewWindow<R>) {
|
||||
};
|
||||
let hwnd = HWND(handle.hwnd.get() as *mut core::ffi::c_void);
|
||||
|
||||
// COLORREF 0x00BBGGRR 编码——白色就是 0x00FFFFFF。
|
||||
let white: u32 = 0x00FFFFFF;
|
||||
// COLORREF 0x00BBGGRR 编码——选用 rgb(245,245,247) 跟 WindowChrome 的 glass linear-gradient
|
||||
// 起始色一致,减小原生 caption bar 跟应用磨砂玻璃的色差(用户反馈:纯白 caption + 半透灰 glass
|
||||
// 色差很丑)。R=0xF5 G=0xF5 B=0xF7 → COLORREF = 0x00F7F5F5。
|
||||
let glass_match: u32 = 0x00F7F5F5;
|
||||
unsafe {
|
||||
if let Err(e) = DwmSetWindowAttribute(
|
||||
hwnd,
|
||||
DWMWA_CAPTION_COLOR,
|
||||
&white as *const _ as *const core::ffi::c_void,
|
||||
std::mem::size_of_val(&white) as u32,
|
||||
&glass_match as *const _ as *const core::ffi::c_void,
|
||||
std::mem::size_of_val(&glass_match) as u32,
|
||||
) {
|
||||
log::warn!("[main] set caption color failed (likely pre-22H2 Win): {e}");
|
||||
}
|
||||
|
||||
@@ -285,11 +285,18 @@ function Pill({ os, state, level, insertedChars, message, onCancel, onConfirm }:
|
||||
);
|
||||
}
|
||||
|
||||
// 与 @keyframes capsule-out 的 0.24s 时长一致——必须同步,否则定时器先于
|
||||
// 动画结束就 unmount → 用户看到半截动画被截断。
|
||||
const EXIT_ANIM_MS = 240;
|
||||
// 初始可见 state:Tauri 内运行从 idle 开始(等后端 capsule:state 事件),
|
||||
// 浏览器 dev 模式从 recording 开始以便直接看到胶囊。
|
||||
const INITIAL_VISIBLE_STATE: CapsuleState = isTauri ? 'idle' : 'recording';
|
||||
|
||||
export function Capsule() {
|
||||
const { t } = useTranslation();
|
||||
const os = detectOS();
|
||||
const metrics = getCapsulePillMetrics(os);
|
||||
const [state, setState] = useState<CapsuleState>(isTauri ? 'idle' : 'recording');
|
||||
const [state, setState] = useState<CapsuleState>(INITIAL_VISIBLE_STATE);
|
||||
const [level, setLevel] = useState<number>(isTauri ? 0 : 0.6);
|
||||
const [insertedChars, setInsertedChars] = useState<number>(0);
|
||||
const [message, setMessage] = useState<string | undefined>();
|
||||
@@ -297,10 +304,10 @@ export function Capsule() {
|
||||
// `leaving` 与 `lastVisibleState` 协同实现「退出动画」:
|
||||
// - 当 state 从非 idle 变成 idle 时,不立即卸载,而是把 leaving 置为 true 并保留
|
||||
// 最后一帧的可见 state(lastVisibleState),让胶囊用 capsule-out 动画收缩淡出。
|
||||
// - 动画结束(~240ms)后再把 leaving 置回 false,组件回到「真正未挂载」分支。
|
||||
// - 动画结束(EXIT_ANIM_MS)后再把 leaving 置回 false,组件回到「真正未挂载」分支。
|
||||
// - 若期间 state 又切回非 idle(例如用户连按热键),立刻中止 leaving 并恢复显示。
|
||||
const [leaving, setLeaving] = useState<boolean>(false);
|
||||
const [lastVisibleState, setLastVisibleState] = useState<CapsuleState>(isTauri ? 'idle' : 'recording');
|
||||
const [lastVisibleState, setLastVisibleState] = useState<CapsuleState>(INITIAL_VISIBLE_STATE);
|
||||
// Windows 端 host 在翻译模式从 84 长到 118;macOS / Linux 上 capsuleLayout 已固定 42 忽略此参数。
|
||||
const hostMetrics = getCapsuleHostMetrics(os, translation);
|
||||
|
||||
@@ -327,12 +334,12 @@ export function Capsule() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 退出动画调度:在 state 真正进入 idle 时,先用 capsule-out 播放 ~240ms,再卸载。
|
||||
// 退出动画调度:在 state 真正进入 idle 时,先用 capsule-out 播放 EXIT_ANIM_MS,再卸载。
|
||||
// 设计要点:
|
||||
// 1. 进入非 idle:清掉 leaving,记录最新可见 state;
|
||||
// 2. 进入 idle 且之前可见:开启 leaving 并启动定时器;
|
||||
// 3. 期间又被打回非 idle:定时器仍会触发,但 effect 内会读到 state !== idle,
|
||||
// 所以 cleanup 时直接 clearTimeout,避免错误地把可见状态切到 idle。
|
||||
// 3. 期间又被打回非 idle:cleanup 直接 clearTimeout,定时器不会触发,
|
||||
// 新一轮 effect 会立即恢复可见态,避免错误地把可见状态切到 idle。
|
||||
useEffect(() => {
|
||||
if (state !== 'idle') {
|
||||
// 立即恢复可见,并取消上一轮可能挂着的离场。
|
||||
@@ -346,7 +353,7 @@ export function Capsule() {
|
||||
const timer = setTimeout(() => {
|
||||
setLeaving(false);
|
||||
setLastVisibleState('idle');
|
||||
}, 240);
|
||||
}, EXIT_ANIM_MS);
|
||||
return () => clearTimeout(timer);
|
||||
// 故意只依赖 state —— lastVisibleState / leaving 是内部派生量,
|
||||
// 把它们加进依赖会让定时器被反复重建。
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
import type { CSSProperties } from 'react';
|
||||
|
||||
export const ICONS: Record<string, string> = {
|
||||
// overview — 仪表盘式分屏布局(顶部摘要条 + 左右两块小卡),传达"概览/总览"
|
||||
overview: 'M4 5h16v5H4zM4 13h7v6H4zM13 13h7v6h-7z',
|
||||
// overview — 仪表盘三卡 + 顶卡内 sparkline 数据线,区分纯三块拼图(layout)
|
||||
overview: 'M4 5h16v5H4zM4 13h7v6H4zM13 13h7v6h-7zM6 7.8l2 1 2-1.5 2 1',
|
||||
// history — 时钟表盘 + 左上角逆时针回拨箭头,强调"过去/回看"
|
||||
history: 'M12 8v4l3 2M3.5 12a8.5 8.5 0 1 0 2.8-6.3L3 8M3 4v4h4',
|
||||
// vocab — Feather 风格 open-book(书脊居中 + 左右两页),相比旧的"带书签的合上书"在 14px 下更易辨识
|
||||
@@ -13,8 +13,8 @@ export const ICONS: Record<string, string> = {
|
||||
style: 'M12 3a9 9 0 1 0 0 18 3 3 0 0 0 3-3v-1a2 2 0 0 1 2-2h1a3 3 0 0 0 3-3 9 9 0 0 0-9-9z',
|
||||
// translate — 地球仪(圆 + 赤道 + 经线椭圆),通用的"语言/国际化"符号,比旧版"A+文+三角"在 14px 下更清晰
|
||||
translate:'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM3 12h18M12 3c2.5 2.5 4 5.5 4 9s-1.5 6.5-4 9c-2.5-2.5-4-5.5-4-9s1.5-6.5 4-9',
|
||||
// selectionAsk — 三行文本("被选中的内容")+ 右下角对话气泡("追问"),直白表达"对选中文字提问"
|
||||
selectionAsk:'M3 5h12M3 9h12M3 13h7M14 14h6a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3l-3 3v-3a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2z',
|
||||
// selectionAsk — 三行文本 + 右下角对话气泡(尾巴拉到 y≈23 防 viewBox 24 底边 stroke-cap 裁切)
|
||||
selectionAsk:'M3 5h12M3 9h12M3 13h7M14 14h6a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3.5l-2.5 2v-2a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2z',
|
||||
settings:'M12 9.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1A2 2 0 1 1 7 4.9l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z',
|
||||
help: 'M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3M12 17h.01M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0z',
|
||||
mic: 'M12 2a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3zM19 11a7 7 0 0 1-14 0M12 18v3M8 21h8',
|
||||
|
||||
@@ -39,11 +39,12 @@ export function WindowChrome({
|
||||
// 两个平台用同一份半透明玻璃 background + backdropFilter,让 sidebar 透明地坐在
|
||||
// 磨砂底板上时有可见的玻璃感。
|
||||
// Windows: Tauri transparent:true + lib.rs apply_mica 提供 Win11 Mica 透出来;
|
||||
// macOS: NSVisualEffectView 提供材质。alpha 0.78 比之前的 0.92 更明显但不过透。
|
||||
// macOS: NSVisualEffectView 提供材质。
|
||||
// alpha 0.92:和原生 Win11 caption(lib.rs 设为 rgb(245,245,247))色差最小,玻璃感仍可见。
|
||||
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.78) 0%, rgba(232,232,236,0.78) 100%)
|
||||
linear-gradient(180deg, rgba(245,245,247,0.92) 0%, rgba(232,232,236,0.92) 100%)
|
||||
`;
|
||||
|
||||
return (
|
||||
|
||||
@@ -772,6 +772,10 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) {
|
||||
)}
|
||||
<div
|
||||
aria-disabled={IS_WINDOWS || undefined}
|
||||
// @ts-expect-error — `inert` 是 HTML5 标准属性(React 19+ 一类型,TS lib.dom 旧版未收录)。
|
||||
// 跟 pointerEvents:none 配合让键盘 Tab 也跳过区域内所有 focusable 控件,避免 AT 用户
|
||||
// 激活无效后端的 IPC(review-qwen3 High)。
|
||||
inert={IS_WINDOWS || undefined}
|
||||
style={
|
||||
IS_WINDOWS
|
||||
? { opacity: 0.5, filter: 'grayscale(0.4)', pointerEvents: 'none' as const }
|
||||
@@ -877,6 +881,10 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) {
|
||||
{(IS_MAC || IS_WINDOWS) && (
|
||||
<div
|
||||
aria-disabled={IS_WINDOWS || undefined}
|
||||
// @ts-expect-error — `inert` 是 HTML5 标准属性(React 19+ 一类型,TS lib.dom 旧版未收录)。
|
||||
// 跟 pointerEvents:none 配合让键盘 Tab 也跳过区域内所有 focusable 控件,避免 AT 用户
|
||||
// 激活无效后端的 IPC(review-qwen3 High)。
|
||||
inert={IS_WINDOWS || undefined}
|
||||
style={
|
||||
IS_WINDOWS
|
||||
? { opacity: 0.5, filter: 'grayscale(0.4)', pointerEvents: 'none' as const }
|
||||
|
||||
@@ -450,6 +450,10 @@ function VocabChip({ entry, onRemove, onToggle }: VocabChipProps) {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
// 父 flex 容器 minHeight: 80 会让 flex item 在 align-self 默认 stretch 下被拉到
|
||||
// 80px 高,chip borderRadius: 999 把高度变大渲染成"超大椭圆"。alignSelf:flex-start
|
||||
// 阻止拉伸,chip 始终保持 content 高度。
|
||||
alignSelf: 'flex-start',
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
padding: '5px 10px 5px 12px',
|
||||
borderRadius: 999,
|
||||
|
||||
Reference in new issue
Block a user