fix(windows): 收紧主窗口 shell 外观契约

This commit is contained in:
Cooper-X-Oak authored and baiqing committed 2026-05-02 12:12:46 +08:00
1 parent 946d842023
commit 802ee653c9
4 files changed
+54 -32

No files matched your search

@@ -15,18 +15,39 @@ function assertMatch(source, pattern, name) {
const raw = await readFile(new URL('../src-tauri/tauri.conf.json', import.meta.url), 'utf-8');
const config = JSON.parse(raw);
const capsuleWindow = config.app.windows.find((window) => window.label === 'capsule');
const mainWindow = config.app.windows.find((window) => window.label === 'main');
const libRs = await readFile(new URL('../src-tauri/src/lib.rs', import.meta.url), 'utf-8');
const coordinatorRs = await readFile(new URL('../src-tauri/src/coordinator.rs', import.meta.url), 'utf-8');
const capsuleTsx = await readFile(new URL('../src/components/Capsule.tsx', import.meta.url), 'utf-8');
const capsuleLayoutTs = await readFile(new URL('../src/lib/capsuleLayout.ts', import.meta.url), 'utf-8');
const windowChromeTsx = await readFile(new URL('../src/components/WindowChrome.tsx', import.meta.url), 'utf-8');
const floatingShellTsx = await readFile(new URL('../src/components/FloatingShell.tsx', import.meta.url), 'utf-8');
if (!capsuleWindow) {
throw new Error('capsule window config missing');
}
if (!mainWindow) {
throw new Error('main window config missing');
}
assertEqual(capsuleWindow.width, 220, 'windows capsule config keeps translation-capable width baseline');
assertEqual(capsuleWindow.height, 110, 'windows capsule config keeps translation-capable height baseline');
assertEqual(capsuleWindow.transparent, true, 'capsule window should keep transparent visuals');
assertEqual(capsuleWindow.alwaysOnTop, true, 'capsule window should stay above the focused app while recording');
assertEqual(mainWindow.decorations, false, 'windows main window should use only custom titlebar');
assertEqual(mainWindow.visible, false, 'windows main window should stay hidden until the intended first show point');
if (!/function WindowsResizeHandles\(\)/.test(windowChromeTsx)) {
throw new Error('windows frameless shell should expose explicit resize handles');
}
if (!/startResizeDragging\(direction\)/.test(windowChromeTsx)) {
throw new Error('windows resize handles should delegate edge dragging to Tauri');
}
if (!/borderRadius:\s*'var\(--ol-window-console-radius\)'/.test(floatingShellTsx)) {
throw new Error('floating shell should consume the shared window-console radius');
}
assertMatch(
coordinatorRs,
/let visible = !matches!\(state,\s*CapsuleState::Idle\);/,
+1 -1
View File
@@ -21,7 +21,7 @@
"minWidth": 980,
"minHeight": 640,
"resizable": true,
"decorations": true,
"decorations": false,
"transparent": true,
"shadow": true,
"hiddenTitle": true,
@@ -255,7 +255,7 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
flex: 1, minWidth: 0,
overflow: 'hidden',
background: 'var(--ol-surface)',
borderRadius: os === 'mac' ? 20 : 10,
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',
@@ -1,16 +1,3 @@
// WindowChrome.tsx — frosted outer frame + raised inner console pattern.
// The OUTER frame is a translucent shell with a tinted backdrop showing through.
// The INNER content lives in a single raised card that floats above it.
//
// Layout per window:
// ┌─ frosted outer ───────────────────────────────┐
// │ [titlebar] │
// │ ┌─ raised console (white, shadow) ─┐ │
// │ │ sidebar │ main │ │
// │ └──────────────────────────────────┘ │
// │ [icon footer] │
// └───────────────────────────────────────────────┘
import { useState, type CSSProperties, type ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import { getCurrentWindow } from '@tauri-apps/api/window';
@@ -31,8 +18,9 @@ export function detectOS(): OS {
const MAC_TITLEBAR_HEIGHT = 36;
const MAC_SYSTEM_CONTROLS_RESERVED_WIDTH = 80;
const MAC_WINDOW_RADIUS = 20;
const WIN_WINDOW_RADIUS = 0;
export const WIN_TITLEBAR_HEIGHT = 36;
export const WIN_WINDOW_RADIUS = 10;
export const WIN_CONSOLE_RADIUS = 10;
const WIN_RESIZE_EDGE = 6;
const WIN_RESIZE_CORNER = 14;
@@ -53,19 +41,32 @@ interface WindowChromeProps {
height?: number | string;
}
export function WindowChrome({ os = 'mac', title = 'OpenLess', children, height = 800 }: WindowChromeProps) {
export function WindowChrome({
os = 'mac',
title = 'OpenLess',
children,
height = 800,
}: WindowChromeProps) {
const shellRadius = os === 'mac' ? 20 : os === 'win' ? WIN_WINDOW_RADIUS : 14;
const consoleRadius = os === 'mac' ? 20 : os === 'win' ? WIN_CONSOLE_RADIUS : 14;
return (
<div
style={{
'--ol-window-shell-radius': `${shellRadius}px`,
'--ol-window-console-radius': `${consoleRadius}px`,
'--ol-window-titlebar-height': `${os === 'mac' ? MAC_TITLEBAR_HEIGHT : WIN_TITLEBAR_HEIGHT}px`,
width: '100%',
height,
position: 'relative',
borderRadius: os === 'mac' ? MAC_WINDOW_RADIUS : WIN_WINDOW_RADIUS,
boxShadow: 'var(--ol-shadow-xl)',
borderRadius: 'var(--ol-window-shell-radius)',
boxShadow: os === 'win'
? '0 22px 54px -28px rgba(15, 17, 22, 0.38), 0 10px 30px -20px rgba(15, 17, 22, 0.22), 0 0 0 0.5px rgba(0, 0, 0, 0.10)'
: 'var(--ol-shadow-xl)',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
border: '0.5px solid rgba(0,0,0,.10)',
border: os === 'win' ? '1px solid rgba(255,255,255,0.18)' : '0.5px solid rgba(0,0,0,.10)',
background: `
radial-gradient(120% 80% at 0% 0%, rgba(255,255,255,0.7) 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%),
@@ -73,12 +74,10 @@ export function WindowChrome({ os = 'mac', title = 'OpenLess', children, height
`,
backdropFilter: 'blur(40px) saturate(180%)',
WebkitBackdropFilter: 'blur(40px) saturate(180%)',
}}
} as CSSProperties}
>
{os === 'win' && <WinTitleBar title={title} />}
{os === 'win' && <WindowsResizeHandles />}
{/* macOS:三色窗口按钮交给系统绘制和定位。这里只保留顶部拖动区,
并避开系统按钮热区,防止拖动层吞掉 close/minimize/zoom 点击。 */}
{os === 'mac' && (
<div
data-tauri-drag-region
@@ -108,12 +107,14 @@ function WinTitleBar({ title }: WinTitleBarProps) {
return (
<div
style={{
height: 36,
height: WIN_TITLEBAR_HEIGHT,
flexShrink: 0,
display: 'flex',
alignItems: 'stretch',
position: 'relative',
zIndex: 70,
borderBottom: '0.5px solid rgba(0, 0, 0, 0.08)',
background: 'linear-gradient(180deg, rgba(255,255,255,0.20) 0%, rgba(255,255,255,0.08) 100%)',
}}
>
<div
@@ -163,7 +164,7 @@ function WindowsResizeHandles() {
if (event.button !== 0) return;
event.preventDefault();
event.stopPropagation();
void startWindowsResize(handle.direction);
void startResizeDragging(handle.direction);
}}
style={{
position: 'absolute',
@@ -180,17 +181,17 @@ function WindowsResizeHandles() {
interface WinTitleButtonProps {
title: string;
action: 'minimize' | 'toggleMaximize' | 'close';
children: ReactNode;
tone?: 'default' | 'danger';
children: ReactNode;
}
function WinTitleButton({ title, action, children, tone = 'default' }: WinTitleButtonProps) {
function WinTitleButton({ title, action, tone = 'default', children }: WinTitleButtonProps) {
const [hovered, setHovered] = useState(false);
const [pressed, setPressed] = useState(false);
const danger = tone === 'danger';
const background = pressed
? danger ? '#c42b1c' : 'rgba(0,0,0,0.12)'
: hovered ? danger ? '#e81123' : 'rgba(0,0,0,0.08)'
? danger ? '#c42b1c' : 'rgba(0, 0, 0, 0.12)'
: hovered ? danger ? '#e81123' : 'rgba(0, 0, 0, 0.08)'
: 'transparent';
const color = danger && (hovered || pressed) ? '#fff' : 'var(--ol-ink-3)';
@@ -198,6 +199,7 @@ function WinTitleButton({ title, action, children, tone = 'default' }: WinTitleB
<button
style={{ ...winBtnStyle, background, color }}
title={title}
onClick={() => runWindowsWindowAction(action)}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => {
setHovered(false);
@@ -205,14 +207,13 @@ function WinTitleButton({ title, action, children, tone = 'default' }: WinTitleB
}}
onMouseDown={() => setPressed(true)}
onMouseUp={() => setPressed(false)}
onClick={() => runWindowsWindowAction(action)}
>
{children}
</button>
);
}
async function startWindowsResize(direction: ResizeDirection) {
async function startResizeDragging(direction: ResizeDirection) {
try {
await getCurrentWindow().startResizeDragging(direction);
} catch (error) {