From 48623801a1bba8f1c259212d37f56a504812e42d Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Sat, 24 Oct 2026 03:01:19 +0200 Subject: [PATCH] fix(app): keep tablet sidebar controls above system navigation (#7768) The wide sidebar reserved only the top safe area, leaving its footer under Android navigation UI. Apply the existing bottom inset to the same interior and add a native geometry regression check. Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../mobile/sidebar-system-insets/android.mjs | 40 +++++++++++++++++++ packages/app/src/components/left-sidebar.tsx | 7 +++- 2 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 packages/app/e2e/mobile/sidebar-system-insets/android.mjs diff --git a/packages/app/e2e/mobile/sidebar-system-insets/android.mjs b/packages/app/e2e/mobile/sidebar-system-insets/android.mjs new file mode 100644 index 0000000000..cf7c45fbb0 --- /dev/null +++ b/packages/app/e2e/mobile/sidebar-system-insets/android.mjs @@ -0,0 +1,40 @@ +// Open a connected host with the wide sidebar visible, keyboard closed, and +// three-button navigation enabled on an Android tablet. Run against a fresh build: +// node packages/app/e2e/mobile/sidebar-system-insets/android.mjs +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; + +const [, , session, serial] = process.argv; +assert(session && serial, "Pass the agent-device session and adb serial"); +const windowState = execFileSync("adb", ["-s", serial, "shell", "dumpsys", "window"], { + encoding: "utf8", + timeout: 10_000, +}); +const navBar = windowState.match(/type=navigationBars frame=\[0,(\d+)\]\[\d+,\d+\] visible=true/); +assert(navBar, "A visible bottom navigation bar is required"); +assert(!/type=ime .*visible=true/.test(windowState), "Close the keyboard first"); +const navigationTop = Number(navBar[1]); +const { data } = JSON.parse( + execFileSync("agent-device", ["snapshot", "--raw", "--json", "--session", session], { + encoding: "utf8", + timeout: 60_000, + }), +); +const focusedApp = windowState.match(/mCurrentFocus=.*? u\d+ ([^/\s]+)\//)?.[1]; +assert.equal( + focusedApp, + data.appBundleId, + "Snapshot must belong to the focused app on this device", +); +for (const id of [ + "sidebar-add-project", + "sidebar-hosts-trigger", + "sidebar-help", + "sidebar-settings", +]) { + const button = data.nodes.find((node) => node.identifier === id); + assert(button?.rect, `${id} must be visible above the system navigation bar`); + const bottom = button.rect.y + button.rect.height; + assert(bottom <= navigationTop, `${id} ends at ${bottom}, below navigation top ${navigationTop}`); + console.log(`PASS: ${id} bottom=${bottom}, navigationTop=${navigationTop}`); +} diff --git a/packages/app/src/components/left-sidebar.tsx b/packages/app/src/components/left-sidebar.tsx index 3f5d218150..dbc501b5c7 100644 --- a/packages/app/src/components/left-sidebar.tsx +++ b/packages/app/src/components/left-sidebar.tsx @@ -719,8 +719,11 @@ function DesktopSidebar({ [active, resizeAnimatedStyle], ); const desktopSidebarBorderStyle = useMemo( - () => [styles.desktopSidebarBorder, { flex: 1, paddingTop: insetsTop }], - [insetsTop], + () => [ + styles.desktopSidebarBorder, + { flex: 1, paddingTop: insetsTop, paddingBottom: insets.bottom }, + ], + [insetsTop, insets.bottom], ); const sidebarHeaderGroupStyle = useMemo( () => [styles.sidebarHeaderGroup, ownsTopLeft && styles.sidebarHeaderGroupBelowChrome],