fix(app): centralize window controls padding into useWindowControlsPadding hook

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.
This commit is contained in:
Mohamed Boudra
2026-03-30 15:17:27 +07:00
parent a94bc0720c
commit 0d9bda12e6
7 changed files with 69 additions and 53 deletions
@@ -17,6 +17,7 @@ import { HEADER_INNER_HEIGHT } from "@/constants/layout";
import { GitDiffPane } from "./git-diff-pane"; import { GitDiffPane } from "./git-diff-pane";
import { FileExplorerPane } from "./file-explorer-pane"; import { FileExplorerPane } from "./file-explorer-pane";
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style"; import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
import { useWindowControlsPadding } from "@/utils/desktop-window";
const MIN_CHAT_WIDTH = 400; const MIN_CHAT_WIDTH = 400;
function logExplorerSidebar(_event: string, _details: Record<string, unknown>): void {} function logExplorerSidebar(_event: string, _details: Record<string, unknown>): void {}
@@ -336,12 +337,13 @@ function SidebarContent({
onOpenFile, onOpenFile,
}: SidebarContentProps) { }: SidebarContentProps) {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const padding = useWindowControlsPadding("explorerSidebar");
const resolvedTab: ExplorerTab = !isGit && activeTab === "changes" ? "files" : activeTab; const resolvedTab: ExplorerTab = !isGit && activeTab === "changes" ? "files" : activeTab;
return ( return (
<View style={styles.sidebarContent} pointerEvents="auto"> <View style={styles.sidebarContent} pointerEvents="auto">
{/* Header with tabs and close button */} {/* Header with tabs and close button */}
<View style={styles.header} testID="explorer-header"> <View style={[styles.header, { paddingRight: padding.right }]} testID="explorer-header">
<View style={styles.tabsContainer}> <View style={styles.tabsContainer}>
{isGit && ( {isGit && (
<Pressable <Pressable
@@ -7,8 +7,7 @@ import {
HEADER_INNER_HEIGHT_MOBILE, HEADER_INNER_HEIGHT_MOBILE,
HEADER_TOP_PADDING_MOBILE, HEADER_TOP_PADDING_MOBILE,
} from "@/constants/layout"; } from "@/constants/layout";
import { useDesktopDragHandlers, useTrafficLightPadding } from "@/utils/desktop-window"; import { useDesktopDragHandlers, useWindowControlsPadding } from "@/utils/desktop-window";
import { usePanelStore } from "@/stores/panel-store";
interface ScreenHeaderProps { interface ScreenHeaderProps {
left?: ReactNode; left?: ReactNode;
@@ -26,15 +25,10 @@ export function ScreenHeader({ left, right, leftStyle, rightStyle, borderless }:
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const desktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen); const padding = useWindowControlsPadding("header");
const trafficLightPadding = useTrafficLightPadding();
// Only add extra padding on mobile for better touch targets; on desktop, only use safe area insets // 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 topPadding = isMobile ? HEADER_TOP_PADDING_MOBILE : 0;
const baseHorizontalPadding = theme.spacing[2]; const baseHorizontalPadding = theme.spacing[2];
const collapsedSidebarInset =
!isMobile && !desktopAgentListOpen && trafficLightPadding.side
? trafficLightPadding
: { left: 0, right: 0 };
const dragHandlers = useDesktopDragHandlers(); const dragHandlers = useDesktopDragHandlers();
@@ -45,8 +39,8 @@ export function ScreenHeader({ left, right, leftStyle, rightStyle, borderless }:
style={[ style={[
styles.row, styles.row,
{ {
paddingLeft: baseHorizontalPadding + collapsedSidebarInset.left, paddingLeft: baseHorizontalPadding + padding.left,
paddingRight: baseHorizontalPadding + collapsedSidebarInset.right, paddingRight: baseHorizontalPadding + padding.right,
}, },
borderless && styles.borderless, borderless && styles.borderless,
]} ]}
@@ -90,6 +84,6 @@ const styles = StyleSheet.create((theme) => ({
gap: theme.spacing[2], gap: theme.spacing[2],
}, },
borderless: { borderless: {
borderBottomWidth: 0, borderBottomColor: "transparent",
}, },
})); }));
+3 -5
View File
@@ -35,7 +35,7 @@ import {
type SidebarProjectEntry, type SidebarProjectEntry,
} from "@/hooks/use-sidebar-workspaces-list"; } from "@/hooks/use-sidebar-workspaces-list";
import { useSidebarAnimation } from "@/contexts/sidebar-animation-context"; 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 { Combobox } from "@/components/ui/combobox";
import { getHostRuntimeStore, useHosts } from "@/runtime/host-runtime"; import { getHostRuntimeStore, useHosts } from "@/runtime/host-runtime";
import { formatConnectionStatus } from "@/utils/daemons"; import { formatConnectionStatus } from "@/utils/daemons";
@@ -632,7 +632,7 @@ function DesktopSidebar({
}: DesktopSidebarProps) { }: DesktopSidebarProps) {
const newAgentKeys = useShortcutKeys("new-agent"); const newAgentKeys = useShortcutKeys("new-agent");
const dragHandlers = useDesktopDragHandlers(); const dragHandlers = useDesktopDragHandlers();
const trafficLightPadding = useTrafficLightPadding(); const padding = useWindowControlsPadding("sidebar");
const sidebarWidth = usePanelStore((state) => state.sidebarWidth); const sidebarWidth = usePanelStore((state) => state.sidebarWidth);
const setSidebarWidth = usePanelStore((state) => state.setSidebarWidth); const setSidebarWidth = usePanelStore((state) => state.setSidebarWidth);
const { width: viewportWidth } = useWindowDimensions(); const { width: viewportWidth } = useWindowDimensions();
@@ -682,9 +682,7 @@ function DesktopSidebar({
return ( return (
<Animated.View style={[styles.desktopSidebar, resizeAnimatedStyle]}> <Animated.View style={[styles.desktopSidebar, resizeAnimatedStyle]}>
{trafficLightPadding.side === 'left' ? ( {padding.top > 0 ? <View style={{ height: padding.top }} {...dragHandlers} /> : null}
<View style={{ height: trafficLightPadding.top }} {...dragHandlers} />
) : null}
<View style={styles.sidebarHeader} {...dragHandlers}> <View style={styles.sidebarHeader} {...dragHandlers}>
<View style={styles.sidebarHeaderRow}> <View style={styles.sidebarHeaderRow}>
<SessionsButton onPress={handleViewMore} /> <SessionsButton onPress={handleViewMore} />
@@ -32,7 +32,7 @@ import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { ResizeHandle } from "@/components/resize-handle"; import { ResizeHandle } from "@/components/resize-handle";
import { shouldFocusPaneFromEventTarget } from "@/components/split-container-pane-focus"; import { shouldFocusPaneFromEventTarget } from "@/components/split-container-pane-focus";
import { usePanelStore } from "@/stores/panel-store"; import { usePanelStore } from "@/stores/panel-store";
import { useTrafficLightPadding } from "@/utils/desktop-window"; import { useWindowControlsPadding } from "@/utils/desktop-window";
import { import {
computeTabDropPreview, computeTabDropPreview,
type TabDropPreview, type TabDropPreview,
@@ -809,8 +809,7 @@ function SplitPaneView({
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const paneRef = useRef<View | null>(null); const paneRef = useRef<View | null>(null);
const stableOnFocusPane = useStableEvent(onFocusPane); const stableOnFocusPane = useStableEvent(onFocusPane);
const isFocusModeEnabled = usePanelStore((s) => s.desktop.focusModeEnabled); const padding = useWindowControlsPadding("tabRow");
const trafficLightPadding = useTrafficLightPadding();
const paneState = useMemo( const paneState = useMemo(
() => () =>
deriveWorkspacePaneState({ deriveWorkspacePaneState({
@@ -885,11 +884,7 @@ function SplitPaneView({
<View <View
style={[ style={[
styles.paneTabs, styles.paneTabs,
isFocusModeEnabled && { paddingLeft: padding.left, paddingRight: padding.right },
trafficLightPadding.side && {
paddingLeft: trafficLightPadding.left,
paddingRight: trafficLightPadding.right,
},
]} ]}
> >
<WorkspaceDesktopTabsRow <WorkspaceDesktopTabsRow
-11
View File
@@ -60,14 +60,3 @@ export function getIsDesktop(): boolean {
} }
return result; return result;
} }
// Get traffic light padding values (only non-zero on desktop macOS)
export function getTrafficLightPadding(): { left: number; top: number } {
if (!getIsDesktopMac()) {
return { left: 0, top: 0 };
}
return {
left: DESKTOP_TRAFFIC_LIGHT_WIDTH,
top: DESKTOP_TRAFFIC_LIGHT_HEIGHT,
};
}
@@ -26,9 +26,9 @@ export function OpenProjectScreen({ serverId }: { serverId: string }) {
}, [isMobile, openAgentList]); }, [isMobile, openAgentList]);
return ( return (
<View style={styles.container} {...dragHandlers}> <View style={styles.container}>
<MenuHeader borderless /> <MenuHeader borderless />
<View style={styles.content}> <View style={styles.content} {...dragHandlers}>
<View style={styles.logo}> <View style={styles.logo}>
<PaseoLogo size={56} /> <PaseoLogo size={56} />
</View> </View>
+53 -15
View File
@@ -10,6 +10,7 @@ import {
} from "@/constants/layout"; } from "@/constants/layout";
import { getDesktopWindow } from "@/desktop/electron/window"; import { getDesktopWindow } from "@/desktop/electron/window";
import { isDesktop } from "@/desktop/host"; import { isDesktop } from "@/desktop/host";
import { usePanelStore } from "@/stores/panel-store";
import { readFiniteScreenPoint } from "./desktop-window-drag-coordinates"; import { readFiniteScreenPoint } from "./desktop-window-drag-coordinates";
export async function toggleMaximize() { export async function toggleMaximize() {
@@ -123,7 +124,15 @@ export function useDesktopDragHandlers(): DesktopDragViewProps {
}, [isActive]); }, [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); const [isFullscreen, setIsFullscreen] = useState(false);
useEffect(() => { useEffect(() => {
@@ -178,23 +187,52 @@ export function useTrafficLightPadding(): { left: number; right: number; top: nu
}; };
}, []); }, []);
if (!getIsDesktop() || isFullscreen) { return useMemo((): RawWindowControlsPadding => {
return { left: 0, right: 0, top: 0, side: null }; 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 { return {
left: DESKTOP_TRAFFIC_LIGHT_WIDTH, left: 0,
right: 0, right: DESKTOP_WINDOW_CONTROLS_WIDTH,
top: DESKTOP_TRAFFIC_LIGHT_HEIGHT, top: DESKTOP_WINDOW_CONTROLS_HEIGHT,
side: 'left',
}; };
}, [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 { return useMemo(() => ({ left, right, top }), [left, right, top]);
left: 0,
right: DESKTOP_WINDOW_CONTROLS_WIDTH,
top: DESKTOP_WINDOW_CONTROLS_HEIGHT,
side: 'right',
};
} }