From b6afc661318bc692ba2ccfbb347410f1e6944196 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 22 Apr 2026 16:19:31 +0700 Subject: [PATCH] fix(app): avoid settings header offset --- .../src/components/headers/screen-header.tsx | 4 +- packages/app/src/screens/settings-screen.tsx | 1 + packages/app/src/utils/desktop-window.test.ts | 42 ++++++++++ packages/app/src/utils/desktop-window.ts | 76 ++++++++++++++----- 4 files changed, 105 insertions(+), 18 deletions(-) create mode 100644 packages/app/src/utils/desktop-window.test.ts diff --git a/packages/app/src/components/headers/screen-header.tsx b/packages/app/src/components/headers/screen-header.tsx index cc5b3b58b..96feadc30 100644 --- a/packages/app/src/components/headers/screen-header.tsx +++ b/packages/app/src/components/headers/screen-header.tsx @@ -18,6 +18,7 @@ interface ScreenHeaderProps { leftStyle?: StyleProp; rightStyle?: StyleProp; borderless?: boolean; + windowControlsPaddingRole?: "header" | "detailHeader"; onRowLayout?: (event: LayoutChangeEvent) => void; } @@ -31,12 +32,13 @@ export function ScreenHeader({ leftStyle, rightStyle, borderless, + windowControlsPaddingRole = "header", onRowLayout, }: ScreenHeaderProps) { const { theme } = useUnistyles(); const insets = useSafeAreaInsets(); const isMobile = useIsCompactFormFactor(); - const padding = useWindowControlsPadding("header"); + const padding = useWindowControlsPadding(windowControlsPaddingRole); // 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]; diff --git a/packages/app/src/screens/settings-screen.tsx b/packages/app/src/screens/settings-screen.tsx index a1fd2ca14..a7a7cefae 100644 --- a/packages/app/src/screens/settings-screen.tsx +++ b/packages/app/src/screens/settings-screen.tsx @@ -898,6 +898,7 @@ export default function SettingsScreen({ view }: SettingsScreenProps) { diff --git a/packages/app/src/utils/desktop-window.test.ts b/packages/app/src/utils/desktop-window.test.ts new file mode 100644 index 000000000..cdc0ce071 --- /dev/null +++ b/packages/app/src/utils/desktop-window.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from "vitest"; +import { resolveWindowControlsPadding } from "@/utils/desktop-window"; + +const rawPadding = { + left: 80, + right: 48, + top: 28, +}; + +describe("resolveWindowControlsPadding", () => { + it("pads the main header for window controls when the app sidebar is closed", () => { + expect( + resolveWindowControlsPadding({ + role: "header", + rawPadding, + sidebarClosed: true, + explorerOpen: false, + focusModeEnabled: false, + }), + ).toEqual({ + left: 80, + right: 48, + top: 0, + }); + }); + + it("does not add left padding to detail headers with their own sidebar", () => { + expect( + resolveWindowControlsPadding({ + role: "detailHeader", + rawPadding, + sidebarClosed: true, + explorerOpen: false, + focusModeEnabled: false, + }), + ).toEqual({ + left: 0, + right: 48, + top: 0, + }); + }); +}); diff --git a/packages/app/src/utils/desktop-window.ts b/packages/app/src/utils/desktop-window.ts index 4634463a0..346092ca6 100644 --- a/packages/app/src/utils/desktop-window.ts +++ b/packages/app/src/utils/desktop-window.ts @@ -17,7 +17,12 @@ type RawWindowControlsPadding = { top: number; }; -type WindowControlsPaddingRole = "sidebar" | "header" | "tabRow" | "explorerSidebar"; +type WindowControlsPaddingRole = + | "sidebar" + | "header" + | "detailHeader" + | "tabRow" + | "explorerSidebar"; // Module-level cache so hook remounts (e.g., on navigation) don't briefly // fall back to the default `false` while the async fullscreen check resolves. @@ -114,22 +119,59 @@ export function useWindowControlsPadding(role: WindowControlsPaddingRole): { 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; - } + const { left, right, top } = resolveWindowControlsPadding({ + role, + rawPadding, + sidebarClosed, + explorerOpen, + focusModeEnabled, + }); return useMemo(() => ({ left, right, top }), [left, right, top]); } + +export function resolveWindowControlsPadding(input: { + role: WindowControlsPaddingRole; + rawPadding: RawWindowControlsPadding; + sidebarClosed: boolean; + explorerOpen: boolean; + focusModeEnabled: boolean; +}): RawWindowControlsPadding { + if (input.role === "sidebar") { + return { + left: input.rawPadding.left, + right: 0, + top: input.rawPadding.top, + }; + } + + if (input.role === "header") { + return { + left: input.sidebarClosed ? input.rawPadding.left : 0, + right: input.explorerOpen ? 0 : input.rawPadding.right, + top: 0, + }; + } + + if (input.role === "detailHeader") { + return { + left: 0, + right: input.rawPadding.right, + top: 0, + }; + } + + if (input.role === "tabRow") { + return { + left: input.sidebarClosed && input.focusModeEnabled ? input.rawPadding.left : 0, + right: input.focusModeEnabled && !input.explorerOpen ? input.rawPadding.right : 0, + top: 0, + }; + } + + return { + left: 0, + right: input.rawPadding.right, + top: 0, + }; +}