mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-12 16:02:48 +08:00
✨ feat(mac-window): 支持切换 macOS 原生窗口控制与原生全屏行为
This commit is contained in:
23
frontend/src/utils/appearance.test.ts
Normal file
23
frontend/src/utils/appearance.test.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { blurToFilter, normalizeBlurForPlatform, normalizeOpacityForPlatform, resolveAppearanceValues } from './appearance';
|
||||
|
||||
describe('appearance helpers', () => {
|
||||
it('falls back to opaque non-blurred appearance when disabled', () => {
|
||||
expect(resolveAppearanceValues({ enabled: false, opacity: 0.3, blur: 12 })).toEqual({ opacity: 1, blur: 0 });
|
||||
});
|
||||
|
||||
it('preserves configured values when appearance is enabled', () => {
|
||||
expect(resolveAppearanceValues({ enabled: true, opacity: 0.72, blur: 9 })).toEqual({ opacity: 0.72, blur: 9 });
|
||||
});
|
||||
|
||||
it('caps opacity at full opacity upper bound', () => {
|
||||
expect(normalizeOpacityForPlatform(2)).toBe(1);
|
||||
});
|
||||
|
||||
it('never returns negative blur and formats blur filter correctly', () => {
|
||||
expect(normalizeBlurForPlatform(-4)).toBe(0);
|
||||
expect(blurToFilter(0)).toBeUndefined();
|
||||
expect(blurToFilter(8)).toBe('blur(8px)');
|
||||
});
|
||||
});
|
||||
47
frontend/src/utils/macWindow.test.ts
Normal file
47
frontend/src/utils/macWindow.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
getMacNativeTitlebarPaddingLeft,
|
||||
getMacNativeTitlebarPaddingRight,
|
||||
shouldHandleMacNativeFullscreenShortcut,
|
||||
shouldSuppressMacNativeEscapeExit,
|
||||
} from './macWindow';
|
||||
|
||||
describe('macWindow helpers', () => {
|
||||
it('uses compact padding when native controls are disabled', () => {
|
||||
expect(getMacNativeTitlebarPaddingLeft(1, false)).toBe(16);
|
||||
expect(getMacNativeTitlebarPaddingRight(1, false)).toBe(0);
|
||||
});
|
||||
|
||||
it('reserves traffic-light safe area when native controls are enabled', () => {
|
||||
expect(getMacNativeTitlebarPaddingLeft(1, true)).toBe(96);
|
||||
expect(getMacNativeTitlebarPaddingRight(1, true)).toBe(16);
|
||||
});
|
||||
|
||||
it('keeps minimum safe area under small ui scales', () => {
|
||||
expect(getMacNativeTitlebarPaddingLeft(0.5, true)).toBe(88);
|
||||
expect(getMacNativeTitlebarPaddingRight(0.5, true)).toBe(12);
|
||||
});
|
||||
|
||||
it('matches Control+Command+F only for mac native mode', () => {
|
||||
expect(shouldHandleMacNativeFullscreenShortcut(true, true, { ctrlKey: true, metaKey: true, altKey: false, key: 'f' })).toBe(true);
|
||||
expect(shouldHandleMacNativeFullscreenShortcut(true, true, { ctrlKey: true, metaKey: true, altKey: false, key: 'F' })).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects conflicting modifiers and non-target keys', () => {
|
||||
expect(shouldHandleMacNativeFullscreenShortcut(true, true, { ctrlKey: true, metaKey: true, altKey: true, key: 'f' })).toBe(false);
|
||||
expect(shouldHandleMacNativeFullscreenShortcut(true, true, { ctrlKey: true, metaKey: false, altKey: false, key: 'f' })).toBe(false);
|
||||
expect(shouldHandleMacNativeFullscreenShortcut(false, true, { ctrlKey: true, metaKey: true, altKey: false, key: 'f' })).toBe(false);
|
||||
expect(shouldHandleMacNativeFullscreenShortcut(true, false, { ctrlKey: true, metaKey: true, altKey: false, key: 'f' })).toBe(false);
|
||||
expect(shouldHandleMacNativeFullscreenShortcut(true, true, { ctrlKey: true, metaKey: true, altKey: false, key: 'g' })).toBe(false);
|
||||
});
|
||||
|
||||
it('suppresses Escape only in mac native fullscreen mode', () => {
|
||||
expect(shouldSuppressMacNativeEscapeExit(true, true, true, { key: 'Escape', defaultPrevented: false })).toBe(true);
|
||||
expect(shouldSuppressMacNativeEscapeExit(true, true, false, { key: 'Escape', defaultPrevented: false })).toBe(false);
|
||||
expect(shouldSuppressMacNativeEscapeExit(true, false, true, { key: 'Escape', defaultPrevented: false })).toBe(false);
|
||||
expect(shouldSuppressMacNativeEscapeExit(false, true, true, { key: 'Escape', defaultPrevented: false })).toBe(false);
|
||||
expect(shouldSuppressMacNativeEscapeExit(true, true, true, { key: 'Enter', defaultPrevented: false })).toBe(false);
|
||||
expect(shouldSuppressMacNativeEscapeExit(true, true, true, { key: 'Escape', defaultPrevented: true })).toBe(false);
|
||||
});
|
||||
});
|
||||
42
frontend/src/utils/macWindow.ts
Normal file
42
frontend/src/utils/macWindow.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
export const getMacNativeTitlebarPaddingLeft = (uiScale: number, enabled: boolean): number => {
|
||||
if (!enabled) {
|
||||
return Math.max(12, Math.round(16 * uiScale));
|
||||
}
|
||||
return Math.max(88, Math.round(96 * uiScale));
|
||||
};
|
||||
|
||||
export const getMacNativeTitlebarPaddingRight = (uiScale: number, enabled: boolean): number => {
|
||||
if (!enabled) {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(12, Math.round(16 * uiScale));
|
||||
};
|
||||
|
||||
export const shouldHandleMacNativeFullscreenShortcut = (
|
||||
isMacRuntime: boolean,
|
||||
useNativeMacWindowControls: boolean,
|
||||
event: Pick<KeyboardEvent, 'ctrlKey' | 'metaKey' | 'altKey' | 'key'>,
|
||||
): boolean => {
|
||||
if (!isMacRuntime || !useNativeMacWindowControls) {
|
||||
return false;
|
||||
}
|
||||
if (!event.ctrlKey || !event.metaKey || event.altKey) {
|
||||
return false;
|
||||
}
|
||||
return String(event.key || '').toLowerCase() === 'f';
|
||||
};
|
||||
|
||||
export const shouldSuppressMacNativeEscapeExit = (
|
||||
isMacRuntime: boolean,
|
||||
useNativeMacWindowControls: boolean,
|
||||
isFullscreen: boolean,
|
||||
event: Pick<KeyboardEvent, 'key' | 'defaultPrevented'>,
|
||||
): boolean => {
|
||||
if (!isMacRuntime || !useNativeMacWindowControls || !isFullscreen) {
|
||||
return false;
|
||||
}
|
||||
if (event.defaultPrevented) {
|
||||
return false;
|
||||
}
|
||||
return String(event.key || '') === 'Escape';
|
||||
};
|
||||
@@ -1,27 +1,21 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { buildOverlayWorkbenchTheme } from './overlayWorkbenchTheme';
|
||||
|
||||
const assertEqual = (actual: unknown, expected: unknown, message: string) => {
|
||||
if (actual !== expected) {
|
||||
throw new Error(`${message}\nactual: ${String(actual)}\nexpected: ${String(expected)}`);
|
||||
}
|
||||
};
|
||||
describe('buildOverlayWorkbenchTheme', () => {
|
||||
it('builds dark theme tokens', () => {
|
||||
const darkTheme = buildOverlayWorkbenchTheme(true);
|
||||
expect(darkTheme.isDark).toBe(true);
|
||||
expect(darkTheme.shellBg).toMatch(/rgba\(15, 15, 17,/);
|
||||
expect(darkTheme.sectionBg).toMatch(/rgba\(255,?\s*255,?\s*255,?\s*0\.03\)/);
|
||||
expect(darkTheme.iconColor).toBe('#ffd666');
|
||||
});
|
||||
|
||||
const assertMatch = (value: string, pattern: RegExp, message: string) => {
|
||||
if (!pattern.test(value)) {
|
||||
throw new Error(`${message}\nactual: ${value}\npattern: ${String(pattern)}`);
|
||||
}
|
||||
};
|
||||
|
||||
const darkTheme = buildOverlayWorkbenchTheme(true);
|
||||
assertEqual(darkTheme.isDark, true, 'dark 主题标记应为 true');
|
||||
assertMatch(darkTheme.shellBg, /rgba\(15, 15, 17,/, 'dark 弹层背景应保持中性黑');
|
||||
assertMatch(darkTheme.sectionBg, /rgba\(255,?\s*255,?\s*255,?\s*0\.03\)/, 'dark section 背景透明度应匹配');
|
||||
assertEqual(darkTheme.iconColor, '#ffd666', 'dark 图标色应为金色强调');
|
||||
|
||||
const lightTheme = buildOverlayWorkbenchTheme(false);
|
||||
assertEqual(lightTheme.isDark, false, 'light 主题标记应为 false');
|
||||
assertMatch(lightTheme.shellBg, /rgba\(255,255,255,0\.98\)/, 'light 弹层背景透明度应匹配');
|
||||
assertMatch(lightTheme.sectionBg, /rgba\(255,?\s*255,?\s*255,?\s*0\.84\)/, 'light section 背景透明度应匹配');
|
||||
assertEqual(lightTheme.iconColor, '#1677ff', 'light 图标色应为蓝色强调');
|
||||
|
||||
console.log('overlayWorkbenchTheme tests passed');
|
||||
it('builds light theme tokens', () => {
|
||||
const lightTheme = buildOverlayWorkbenchTheme(false);
|
||||
expect(lightTheme.isDark).toBe(false);
|
||||
expect(lightTheme.shellBg).toMatch(/rgba\(255,255,255,0\.98\)/);
|
||||
expect(lightTheme.sectionBg).toMatch(/rgba\(255,?\s*255,?\s*255,?\s*0\.84\)/);
|
||||
expect(lightTheme.iconColor).toBe('#1677ff');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user