From 0d9bda12e620883efa12578b947e5f1539184c71 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 30 Mar 2026 15:17:27 +0700 Subject: [PATCH] fix(app): centralize window controls padding into useWindowControlsPadding hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces useTrafficLightPadding with a role-based useWindowControlsPadding hook that absorbs all layout-conditional logic (sidebar state, focus mode, explorer state). Consumers no longer make platform or layout decisions — they just apply the resolved padding values. Fixes Windows title bar overlay buttons overlapping source control icons when the left sidebar is open, and right padding not shifting to the explorer sidebar header when it's open. Also fixes borderless header using borderBottomWidth:0 (layout shift) instead of transparent, and double-click-to-maximize bounce on the open project screen caused by nested drag handlers. --- .../app/src/components/explorer-sidebar.tsx | 4 +- .../src/components/headers/screen-header.tsx | 16 ++--- packages/app/src/components/left-sidebar.tsx | 8 +-- .../app/src/components/split-container.tsx | 11 +-- packages/app/src/constants/layout.ts | 11 --- .../app/src/screens/open-project-screen.tsx | 4 +- packages/app/src/utils/desktop-window.ts | 68 +++++++++++++++---- 7 files changed, 69 insertions(+), 53 deletions(-) diff --git a/packages/app/src/components/explorer-sidebar.tsx b/packages/app/src/components/explorer-sidebar.tsx index 1499bfe03..50bbfda5d 100644 --- a/packages/app/src/components/explorer-sidebar.tsx +++ b/packages/app/src/components/explorer-sidebar.tsx @@ -17,6 +17,7 @@ import { HEADER_INNER_HEIGHT } from "@/constants/layout"; import { GitDiffPane } from "./git-diff-pane"; import { FileExplorerPane } from "./file-explorer-pane"; import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style"; +import { useWindowControlsPadding } from "@/utils/desktop-window"; const MIN_CHAT_WIDTH = 400; function logExplorerSidebar(_event: string, _details: Record): void {} @@ -336,12 +337,13 @@ function SidebarContent({ onOpenFile, }: SidebarContentProps) { const { theme } = useUnistyles(); + const padding = useWindowControlsPadding("explorerSidebar"); const resolvedTab: ExplorerTab = !isGit && activeTab === "changes" ? "files" : activeTab; return ( {/* Header with tabs and close button */} - + {isGit && ( state.desktop.agentListOpen); - const trafficLightPadding = useTrafficLightPadding(); + const padding = useWindowControlsPadding("header"); // Only add extra padding on mobile for better touch targets; on desktop, only use safe area insets const topPadding = isMobile ? HEADER_TOP_PADDING_MOBILE : 0; const baseHorizontalPadding = theme.spacing[2]; - const collapsedSidebarInset = - !isMobile && !desktopAgentListOpen && trafficLightPadding.side - ? trafficLightPadding - : { left: 0, right: 0 }; const dragHandlers = useDesktopDragHandlers(); @@ -45,8 +39,8 @@ export function ScreenHeader({ left, right, leftStyle, rightStyle, borderless }: style={[ styles.row, { - paddingLeft: baseHorizontalPadding + collapsedSidebarInset.left, - paddingRight: baseHorizontalPadding + collapsedSidebarInset.right, + paddingLeft: baseHorizontalPadding + padding.left, + paddingRight: baseHorizontalPadding + padding.right, }, borderless && styles.borderless, ]} @@ -90,6 +84,6 @@ const styles = StyleSheet.create((theme) => ({ gap: theme.spacing[2], }, borderless: { - borderBottomWidth: 0, + borderBottomColor: "transparent", }, })); diff --git a/packages/app/src/components/left-sidebar.tsx b/packages/app/src/components/left-sidebar.tsx index bce074c7b..718575dda 100644 --- a/packages/app/src/components/left-sidebar.tsx +++ b/packages/app/src/components/left-sidebar.tsx @@ -35,7 +35,7 @@ import { type SidebarProjectEntry, } from "@/hooks/use-sidebar-workspaces-list"; import { useSidebarAnimation } from "@/contexts/sidebar-animation-context"; -import { useDesktopDragHandlers, useTrafficLightPadding } from "@/utils/desktop-window"; +import { useDesktopDragHandlers, useWindowControlsPadding } from "@/utils/desktop-window"; import { Combobox } from "@/components/ui/combobox"; import { getHostRuntimeStore, useHosts } from "@/runtime/host-runtime"; import { formatConnectionStatus } from "@/utils/daemons"; @@ -632,7 +632,7 @@ function DesktopSidebar({ }: DesktopSidebarProps) { const newAgentKeys = useShortcutKeys("new-agent"); const dragHandlers = useDesktopDragHandlers(); - const trafficLightPadding = useTrafficLightPadding(); + const padding = useWindowControlsPadding("sidebar"); const sidebarWidth = usePanelStore((state) => state.sidebarWidth); const setSidebarWidth = usePanelStore((state) => state.setSidebarWidth); const { width: viewportWidth } = useWindowDimensions(); @@ -682,9 +682,7 @@ function DesktopSidebar({ return ( - {trafficLightPadding.side === 'left' ? ( - - ) : null} + {padding.top > 0 ? : null} diff --git a/packages/app/src/components/split-container.tsx b/packages/app/src/components/split-container.tsx index 88f458407..b7c192f78 100644 --- a/packages/app/src/components/split-container.tsx +++ b/packages/app/src/components/split-container.tsx @@ -32,7 +32,7 @@ import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { ResizeHandle } from "@/components/resize-handle"; import { shouldFocusPaneFromEventTarget } from "@/components/split-container-pane-focus"; import { usePanelStore } from "@/stores/panel-store"; -import { useTrafficLightPadding } from "@/utils/desktop-window"; +import { useWindowControlsPadding } from "@/utils/desktop-window"; import { computeTabDropPreview, type TabDropPreview, @@ -809,8 +809,7 @@ function SplitPaneView({ const { theme } = useUnistyles(); const paneRef = useRef(null); const stableOnFocusPane = useStableEvent(onFocusPane); - const isFocusModeEnabled = usePanelStore((s) => s.desktop.focusModeEnabled); - const trafficLightPadding = useTrafficLightPadding(); + const padding = useWindowControlsPadding("tabRow"); const paneState = useMemo( () => deriveWorkspacePaneState({ @@ -885,11 +884,7 @@ function SplitPaneView({ + - + diff --git a/packages/app/src/utils/desktop-window.ts b/packages/app/src/utils/desktop-window.ts index 5098ce2dd..15eeb4e3a 100644 --- a/packages/app/src/utils/desktop-window.ts +++ b/packages/app/src/utils/desktop-window.ts @@ -10,6 +10,7 @@ import { } from "@/constants/layout"; import { getDesktopWindow } from "@/desktop/electron/window"; import { isDesktop } from "@/desktop/host"; +import { usePanelStore } from "@/stores/panel-store"; import { readFiniteScreenPoint } from "./desktop-window-drag-coordinates"; export async function toggleMaximize() { @@ -123,7 +124,15 @@ export function useDesktopDragHandlers(): DesktopDragViewProps { }, [isActive]); } -export function useTrafficLightPadding(): { left: number; right: number; top: number; side: 'left' | 'right' | null } { +type RawWindowControlsPadding = { + left: number; + right: number; + top: number; +}; + +type WindowControlsPaddingRole = "sidebar" | "header" | "tabRow" | "explorerSidebar"; + +function useRawWindowControlsPadding(): RawWindowControlsPadding { const [isFullscreen, setIsFullscreen] = useState(false); useEffect(() => { @@ -178,23 +187,52 @@ export function useTrafficLightPadding(): { left: number; right: number; top: nu }; }, []); - if (!getIsDesktop() || isFullscreen) { - return { left: 0, right: 0, top: 0, side: null }; - } + return useMemo((): RawWindowControlsPadding => { + if (!getIsDesktop() || isFullscreen) { + return { left: 0, right: 0, top: 0 }; + } + + if (getIsDesktopMac()) { + return { + left: DESKTOP_TRAFFIC_LIGHT_WIDTH, + right: 0, + top: DESKTOP_TRAFFIC_LIGHT_HEIGHT, + }; + } - if (getIsDesktopMac()) { return { - left: DESKTOP_TRAFFIC_LIGHT_WIDTH, - right: 0, - top: DESKTOP_TRAFFIC_LIGHT_HEIGHT, - side: 'left', + left: 0, + right: DESKTOP_WINDOW_CONTROLS_WIDTH, + top: DESKTOP_WINDOW_CONTROLS_HEIGHT, }; + }, [isFullscreen]); +} + +export function useWindowControlsPadding( + role: WindowControlsPaddingRole, +): { left: number; right: number; top: number } { + const sidebarOpen = usePanelStore((state) => state.desktop.agentListOpen); + const explorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen); + const focusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled); + const rawPadding = useRawWindowControlsPadding(); + const sidebarClosed = !sidebarOpen; + + let left = 0; + let right = 0; + let top = 0; + + if (role === "sidebar") { + left = rawPadding.left; + top = rawPadding.top; + } else if (role === "header") { + left = sidebarClosed ? rawPadding.left : 0; + right = explorerOpen ? 0 : rawPadding.right; + } else if (role === "tabRow") { + left = sidebarClosed && focusModeEnabled ? rawPadding.left : 0; + right = focusModeEnabled && !explorerOpen ? rawPadding.right : 0; + } else if (role === "explorerSidebar") { + right = rawPadding.right; } - return { - left: 0, - right: DESKTOP_WINDOW_CONTROLS_WIDTH, - top: DESKTOP_WINDOW_CONTROLS_HEIGHT, - side: 'right', - }; + return useMemo(() => ({ left, right, top }), [left, right, top]); }