From e47fce4ae416eaf49f273c91703895f0ed9ab2bb Mon Sep 17 00:00:00 2001 From: paisley <8197966+su8su@users.noreply.github.com> Date: Tue, 2 Jun 2026 17:57:27 +0800 Subject: [PATCH] fix(mac): align traffic lights in sidebar chrome (#1092) --- electron/main/index.ts | 13 ++++- electron/main/ipc-handlers.ts | 8 +++ electron/main/traffic-light-layout.ts | 40 +++++++++++++++ electron/preload/index.ts | 1 + package.json | 3 +- pnpm-lock.yaml | 9 ++-- shared/sidebar-layout.ts | 58 ++++++++++++++++++++++ src/components/layout/MainLayout.tsx | 4 +- src/components/layout/Sidebar.tsx | 22 ++++++-- tests/e2e/mac-titlebar-drag-region.spec.ts | 18 ++++++- tests/unit/sidebar-layout.test.ts | 48 ++++++++++++++++++ 11 files changed, 210 insertions(+), 14 deletions(-) create mode 100644 electron/main/traffic-light-layout.ts create mode 100644 shared/sidebar-layout.ts create mode 100644 tests/unit/sidebar-layout.test.ts diff --git a/electron/main/index.ts b/electron/main/index.ts index bb17dd1..72d25fe 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -28,6 +28,8 @@ import { } from '../utils/openclaw-workspace'; import { autoInstallCliIfNeeded, generateCompletionCache, installCompletionToProfile } from '../utils/openclaw-cli'; import { isQuitting, setQuitting } from './app-state'; +import { getMacTrafficLightPosition, syncMacTrafficLightPosition } from './traffic-light-layout'; +import { getSetting } from '../utils/store'; import { applyProxySettings } from './proxy'; import { syncLaunchAtStartupSettingFromStore } from './launch-at-startup'; import { @@ -43,7 +45,6 @@ import { } from './quit-lifecycle'; import { createSignalQuitHandler } from './signal-quit'; import { acquireProcessInstanceFileLock } from './process-instance-lock'; -import { getSetting } from '../utils/store'; import { ensureBuiltinSkillsInstalled, ensurePreinstalledSkillsInstalled, trimBundledOpenClawSkillsAndConfigs } from '../utils/skill-config'; import { startHostApiServer } from '../api/server'; @@ -187,7 +188,9 @@ function createWindow(): BrowserWindow { webviewTag: true, // Enable for embedding OpenClaw Control UI }, titleBarStyle: isMac ? 'hiddenInset' : useCustomTitleBar ? 'hidden' : 'default', - trafficLightPosition: isMac ? { x: 16, y: 16 } : undefined, + trafficLightPosition: isMac + ? getMacTrafficLightPosition(false) + : undefined, frame: isMac || !useCustomTitleBar, show: false, }); @@ -261,6 +264,12 @@ function createMainWindow(): BrowserWindow { return; } + if (process.platform === 'darwin') { + void getSetting('sidebarCollapsed').then((sidebarCollapsed) => { + syncMacTrafficLightPosition(win, sidebarCollapsed); + }); + } + const action = consumeMainWindowReady(mainWindowFocusState); if (action === 'focus') { focusWindow(win); diff --git a/electron/main/ipc-handlers.ts b/electron/main/ipc-handlers.ts index d72bc51..0917b9c 100644 --- a/electron/main/ipc-handlers.ts +++ b/electron/main/ipc-handlers.ts @@ -7,6 +7,7 @@ import { existsSync } from 'node:fs'; import { homedir } from 'node:os'; import { join, extname, basename, resolve, sep, relative } from 'node:path'; import crypto from 'node:crypto'; +import { syncMacTrafficLightPosition } from './traffic-light-layout'; import { GatewayManager } from '../gateway/manager'; import { ClawHubService, ClawHubSearchParams, ClawHubInstallParams, ClawHubUninstallParams } from '../gateway/clawhub'; import { @@ -2305,6 +2306,13 @@ function registerUsageHandlers(): void { * Window control handlers (for custom title bar on Windows) */ function registerWindowHandlers(mainWindow: BrowserWindow): void { + ipcMain.handle('window:syncTrafficLightPosition', (_, sidebarCollapsed: unknown) => { + if (typeof sidebarCollapsed !== 'boolean') { + return; + } + syncMacTrafficLightPosition(mainWindow, sidebarCollapsed); + }); + ipcMain.handle('window:minimize', () => { mainWindow.minimize(); }); diff --git a/electron/main/traffic-light-layout.ts b/electron/main/traffic-light-layout.ts new file mode 100644 index 0000000..e4f635f --- /dev/null +++ b/electron/main/traffic-light-layout.ts @@ -0,0 +1,40 @@ +import { release } from 'node:os'; +import type { BrowserWindow } from 'electron'; + +const MAC_SIDEBAR_CHROME_HEIGHT = 28; +const MAC_TRAFFIC_LIGHT_GAP = 8; +const MAC_TRAFFIC_LIGHT_FRAME_HEIGHT = 16; +const MAC_TRAFFIC_LIGHT_FRAME_HEIGHT_TAHOE = 14; + +function getMacTrafficLightFrameHeight(darwinMajor: number): number { + return darwinMajor >= 25 + ? MAC_TRAFFIC_LIGHT_FRAME_HEIGHT_TAHOE + : MAC_TRAFFIC_LIGHT_FRAME_HEIGHT; +} + +function getMacTrafficLightChromeOffset(buttonFrameHeight: number): number { + return Math.floor((MAC_SIDEBAR_CHROME_HEIGHT - buttonFrameHeight) / 2); +} + +export function getMacTrafficLightPosition(sidebarCollapsed: boolean): { x: number; y: number } { + const darwinMajor = Number.parseInt(release().split('.')[0] ?? '0', 10); + const buttonFrameHeight = getMacTrafficLightFrameHeight(darwinMajor); + const offset = getMacTrafficLightChromeOffset(buttonFrameHeight); + + if (sidebarCollapsed) { + return { x: MAC_TRAFFIC_LIGHT_GAP, y: Math.max(MAC_TRAFFIC_LIGHT_GAP, offset) }; + } + + return { x: offset + 1, y: offset }; +} + +export function syncMacTrafficLightPosition( + win: BrowserWindow, + sidebarCollapsed: boolean, +): void { + if (process.platform !== 'darwin' || win.isDestroyed()) { + return; + } + + win.setWindowButtonPosition(getMacTrafficLightPosition(sidebarCollapsed)); +} diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 6558c96..1213b33 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -50,6 +50,7 @@ const electronAPI = { 'window:maximize', 'window:close', 'window:isMaximized', + 'window:syncTrafficLightPosition', // Settings 'settings:get', 'settings:set', diff --git a/package.json b/package.json index bd607e3..88dd0b1 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,8 @@ "node-machine-id": "^1.1.12", "posthog-node": "^5.28.0", "tar": "^6.2.1", - "ws": "^8.19.0" + "ws": "^8.19.0", + "yaml": "^2.9.0" }, "devDependencies": { "@buape/carbon": "0.16.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0dd8e17..0824ba7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: ws: specifier: ^8.19.0 version: 8.20.0 + yaml: + specifier: ^2.9.0 + version: 2.9.0 devDependencies: '@buape/carbon': specifier: 0.16.0 @@ -4018,10 +4021,6 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-east-asian-width@1.5.0: - resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} - engines: {node: '>=18'} - get-east-asian-width@1.6.0: resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} engines: {node: '>=18'} @@ -10820,8 +10819,6 @@ snapshots: get-caller-file@2.0.5: {} - get-east-asian-width@1.5.0: {} - get-east-asian-width@1.6.0: {} get-intrinsic@1.3.0: diff --git a/shared/sidebar-layout.ts b/shared/sidebar-layout.ts new file mode 100644 index 0000000..679b193 --- /dev/null +++ b/shared/sidebar-layout.ts @@ -0,0 +1,58 @@ +/** macOS sidebar title strip height — matches VS Code compact title bar (px). */ +export const MAC_SIDEBAR_CHROME_HEIGHT = 28; + +/** Visible traffic-light diameter on macOS (px). */ +export const MAC_TRAFFIC_LIGHT_BUTTON_SIZE = 12; + +/** Native edge-to-edge gap between traffic light buttons (px). */ +export const MAC_TRAFFIC_LIGHT_GAP = 8; + +/** Frame height VS Code uses when vertically centering traffic lights (pre-Tahoe). */ +export const MAC_TRAFFIC_LIGHT_FRAME_HEIGHT = 16; + +/** Frame height on macOS Tahoe and newer. */ +export const MAC_TRAFFIC_LIGHT_FRAME_HEIGHT_TAHOE = 14; + +/** macOS close + minimize + zoom button group width (px). */ +export const MAC_TRAFFIC_LIGHT_GROUP_WIDTH = + 3 * MAC_TRAFFIC_LIGHT_BUTTON_SIZE + 2 * MAC_TRAFFIC_LIGHT_GAP; + +/** + * Collapsed sidebar rail width (px): four spacing units plus three buttons. + * 4 × 8 + 3 × 12 = 68 + */ +export const SIDEBAR_COLLAPSED_WIDTH = + 4 * MAC_TRAFFIC_LIGHT_GAP + 3 * MAC_TRAFFIC_LIGHT_BUTTON_SIZE; + +export function getMacTrafficLightFrameHeight(darwinMajor: number): number { + return darwinMajor >= 25 + ? MAC_TRAFFIC_LIGHT_FRAME_HEIGHT_TAHOE + : MAC_TRAFFIC_LIGHT_FRAME_HEIGHT; +} + +/** VS Code-style inset that vertically centers traffic lights in the chrome strip. */ +export function getMacTrafficLightChromeOffset( + chromeHeight = MAC_SIDEBAR_CHROME_HEIGHT, + buttonFrameHeight = MAC_TRAFFIC_LIGHT_FRAME_HEIGHT, +): number { + return Math.floor((chromeHeight - buttonFrameHeight) / 2); +} + +export function getMacTrafficLightPosition(options: { + sidebarCollapsed: boolean; + chromeHeight?: number; + buttonFrameHeight?: number; +}): { x: number; y: number } { + const chromeHeight = options.chromeHeight ?? MAC_SIDEBAR_CHROME_HEIGHT; + const buttonFrameHeight = options.buttonFrameHeight ?? MAC_TRAFFIC_LIGHT_FRAME_HEIGHT; + const offset = getMacTrafficLightChromeOffset(chromeHeight, buttonFrameHeight); + + if (options.sidebarCollapsed) { + // Collapsed rail: keep uniform 8px grid on left / inter-dot / right. + const spacing = MAC_TRAFFIC_LIGHT_GAP; + return { x: spacing, y: Math.max(spacing, offset) }; + } + + // Expanded: VS Code aligns left inset with vertical inset (x = offset + 1). + return { x: offset + 1, y: offset }; +} diff --git a/src/components/layout/MainLayout.tsx b/src/components/layout/MainLayout.tsx index 5f86131..3092672 100644 --- a/src/components/layout/MainLayout.tsx +++ b/src/components/layout/MainLayout.tsx @@ -5,6 +5,7 @@ import { Outlet } from 'react-router-dom'; import { Sidebar } from './Sidebar'; import { TitleBar } from './TitleBar'; +import { MAC_SIDEBAR_CHROME_HEIGHT } from '../../../shared/sidebar-layout'; import { cn } from '@/lib/utils'; export function MainLayout() { @@ -37,7 +38,8 @@ export function MainLayout() {