From 0ed9224d0e22da91da66b5967c15c301002cbc12 Mon Sep 17 00:00:00 2001 From: Fernando Gomes Date: Tue, 23 Jun 2026 17:14:50 -0300 Subject: [PATCH 1/2] fix(sidebar): clean up collapsed header panel layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #2872 clipped the sidebar body when collapsed but its companion HeaderRow change (shipped "not run locally") left the header's left panel — the strip holding the Bluebird button and sidebar toggle — in a broken state when collapsed: - On macOS the traffic-light clearance was added to the panel's width while the buttons stayed right-aligned, so the wider Bluebird+toggle cluster drifted back under the window controls. - The panel kept its right border when collapsed, leaving a divider dangling in the header with no sidebar body beneath it — reading as a leftover sidebar stub floating over the page content. Fix: extract the panel layout into a pure, testable `getHeaderSidebarPanelLayout` helper. When collapsed it now reserves the macOS traffic-light strip as real left padding and left-aligns the buttons (so a wide label grows rightward into empty space, never leftward under the controls), and drops the divider (which only belongs while the sidebar body is visible below it). Open-state behavior is unchanged. Verified: typecheck, biome check, and 6 new unit tests covering all open/collapsed × macOS/non-macOS states pass. Generated-By: PostHog Code Task-Id: f4b5e35b-f65b-4c17-a78a-f7076a8a271c --- packages/ui/src/shell/HeaderRow.tsx | 23 +++-- .../ui/src/shell/headerSidebarPanel.test.ts | 83 +++++++++++++++++++ packages/ui/src/shell/headerSidebarPanel.ts | 56 +++++++++++++ 3 files changed, 154 insertions(+), 8 deletions(-) create mode 100644 packages/ui/src/shell/headerSidebarPanel.test.ts create mode 100644 packages/ui/src/shell/headerSidebarPanel.ts diff --git a/packages/ui/src/shell/HeaderRow.tsx b/packages/ui/src/shell/HeaderRow.tsx index cc43d53f4..66d88c34e 100644 --- a/packages/ui/src/shell/HeaderRow.tsx +++ b/packages/ui/src/shell/HeaderRow.tsx @@ -26,6 +26,7 @@ import { useWorkspace } from "@posthog/ui/features/workspace/useWorkspace"; import { Tooltip } from "@posthog/ui/primitives/Tooltip"; import { useAppView } from "@posthog/ui/router/useAppView"; import { track } from "@posthog/ui/shell/analytics"; +import { getHeaderSidebarPanelLayout } from "@posthog/ui/shell/headerSidebarPanel"; import { useHeaderStore } from "@posthog/ui/shell/headerStore"; import { isMac, isWindows } from "@posthog/ui/utils/platform"; import { Box, Flex } from "@radix-ui/themes"; @@ -162,9 +163,7 @@ function BluebirdButton() { } export const HEADER_HEIGHT = 36; -const COLLAPSED_WIDTH = 110; const WINDOWS_TITLEBAR_INSET = 140; -const MACOS_TRAFFIC_LIGHT_INSET = 70; export function HeaderRow() { const content = useHeaderStore((state) => state.content); @@ -174,6 +173,11 @@ export function HeaderRow() { const sidebarWidth = useSidebarStore((state) => state.width); const isResizing = useSidebarStore((state) => state.isResizing); const setIsResizing = useSidebarStore((state) => state.setIsResizing); + const panel = getHeaderSidebarPanelLayout({ + sidebarOpen, + sidebarWidth, + isMac, + }); const activeTaskId = view.type === "task-detail" ? view.taskId : undefined; // Read the live task from the list cache instead of a stale snapshot off the @@ -206,17 +210,20 @@ export function HeaderRow() { > diff --git a/packages/ui/src/shell/headerSidebarPanel.test.ts b/packages/ui/src/shell/headerSidebarPanel.test.ts new file mode 100644 index 000000000..1d89cc325 --- /dev/null +++ b/packages/ui/src/shell/headerSidebarPanel.test.ts @@ -0,0 +1,83 @@ +import { + getHeaderSidebarPanelLayout, + type HeaderSidebarPanelLayout, +} from "@posthog/ui/shell/headerSidebarPanel"; +import { describe, expect, it } from "vitest"; + +describe("getHeaderSidebarPanelLayout", () => { + it.each<{ + name: string; + input: { sidebarOpen: boolean; sidebarWidth: number; isMac: boolean }; + expected: HeaderSidebarPanelLayout; + }>([ + { + name: "open on macOS tracks the sidebar width and keeps the divider", + input: { sidebarOpen: true, sidebarWidth: 256, isMac: true }, + expected: { + width: "256px", + minWidth: "180px", + paddingLeft: undefined, + justify: "end", + showBorder: true, + }, + }, + { + name: "open off macOS tracks the sidebar width", + input: { sidebarOpen: true, sidebarWidth: 300, isMac: false }, + expected: { + width: "300px", + minWidth: "110px", + paddingLeft: undefined, + justify: "end", + showBorder: true, + }, + }, + { + name: "collapsed on macOS reserves the traffic-light strip as left padding", + input: { sidebarOpen: false, sidebarWidth: 256, isMac: true }, + expected: { + width: "180px", + minWidth: "180px", + paddingLeft: "70px", + justify: "start", + showBorder: false, + }, + }, + { + name: "collapsed off macOS needs no traffic-light padding", + input: { sidebarOpen: false, sidebarWidth: 256, isMac: false }, + expected: { + width: "110px", + minWidth: "110px", + paddingLeft: undefined, + justify: "start", + showBorder: false, + }, + }, + ])("$name", ({ input, expected }) => { + expect(getHeaderSidebarPanelLayout(input)).toEqual(expected); + }); + + // Regression guards for the bugs this helper fixes. + it("never reserves traffic-light padding while the sidebar is open", () => { + expect( + getHeaderSidebarPanelLayout({ + sidebarOpen: true, + sidebarWidth: 256, + isMac: true, + }).paddingLeft, + ).toBeUndefined(); + }); + + it("drops the divider when collapsed so it can't dangle over the page", () => { + for (const isMac of [true, false]) { + expect( + getHeaderSidebarPanelLayout({ + sidebarOpen: false, + sidebarWidth: 256, + isMac, + }).showBorder, + ).toBe(false); + } + }); +}); diff --git a/packages/ui/src/shell/headerSidebarPanel.ts b/packages/ui/src/shell/headerSidebarPanel.ts new file mode 100644 index 000000000..15278099f --- /dev/null +++ b/packages/ui/src/shell/headerSidebarPanel.ts @@ -0,0 +1,56 @@ +// The header's left region mirrors the sidebar: it holds the app-rail buttons +// (Bluebird) plus the sidebar toggle and lines up with the sidebar body below +// it. This helper derives its layout so the collapsed state stays clean — it +// clears the macOS traffic lights and drops the divider that would otherwise +// dangle as a stray edge over the page once the sidebar body has collapsed to +// zero width. + +/** Width of the collapsed panel, sized to hold the app-rail + toggle buttons. */ +const COLLAPSED_WIDTH = 110; +/** + * Width of the macOS traffic-light strip (close / minimize / zoom). Reserved as + * real left padding when collapsed so the buttons can never render underneath + * the window controls. + */ +const MACOS_TRAFFIC_LIGHT_INSET = 70; + +export interface HeaderSidebarPanelLayout { + /** Inline width / minWidth for the panel (animates between the two states). */ + width: string; + minWidth: string; + /** + * Left padding reserving the macOS traffic-light strip, or `undefined` when no + * reservation is needed (open, or non-macOS). + */ + paddingLeft: string | undefined; + /** + * Buttons hug the sidebar's right edge when open (matching the divider); when + * collapsed they hug the left, after the traffic-light inset, so a wide + * Bluebird label grows rightward into empty space rather than leftward under + * the window controls. + */ + justify: "start" | "end"; + /** The divider only belongs while the sidebar body is visible beneath it. */ + showBorder: boolean; +} + +export function getHeaderSidebarPanelLayout({ + sidebarOpen, + sidebarWidth, + isMac, +}: { + sidebarOpen: boolean; + sidebarWidth: number; + isMac: boolean; +}): HeaderSidebarPanelLayout { + const collapsedWidth = + COLLAPSED_WIDTH + (isMac ? MACOS_TRAFFIC_LIGHT_INSET : 0); + return { + width: sidebarOpen ? `${sidebarWidth}px` : `${collapsedWidth}px`, + minWidth: `${collapsedWidth}px`, + paddingLeft: + !sidebarOpen && isMac ? `${MACOS_TRAFFIC_LIGHT_INSET}px` : undefined, + justify: sidebarOpen ? "end" : "start", + showBorder: sidebarOpen, + }; +} From 10d372536098fcace583b105b403857b8496d108 Mon Sep 17 00:00:00 2001 From: Fernando Gomes Date: Tue, 23 Jun 2026 17:32:20 -0300 Subject: [PATCH 2/2] test(sidebar): drop redundant header-panel layout guards The two standalone it() guards duplicated assertions already covered by the it.each block (which asserts the full layout object via toEqual for every state), and one used a for-loop where the team prefers it.each. Remove them and fold the regression intent (traffic-light clearance, dropped divider) into the parameterized case names instead. Per Greptile review feedback on #2878. Generated-By: PostHog Code Task-Id: f4b5e35b-f65b-4c17-a78a-f7076a8a271c --- .../ui/src/shell/headerSidebarPanel.test.ts | 27 ++----------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/packages/ui/src/shell/headerSidebarPanel.test.ts b/packages/ui/src/shell/headerSidebarPanel.test.ts index 1d89cc325..86ff9f456 100644 --- a/packages/ui/src/shell/headerSidebarPanel.test.ts +++ b/packages/ui/src/shell/headerSidebarPanel.test.ts @@ -33,7 +33,7 @@ describe("getHeaderSidebarPanelLayout", () => { }, }, { - name: "collapsed on macOS reserves the traffic-light strip as left padding", + name: "collapsed on macOS reserves the traffic-light strip as left padding and drops the divider", input: { sidebarOpen: false, sidebarWidth: 256, isMac: true }, expected: { width: "180px", @@ -44,7 +44,7 @@ describe("getHeaderSidebarPanelLayout", () => { }, }, { - name: "collapsed off macOS needs no traffic-light padding", + name: "collapsed off macOS drops the divider and needs no traffic-light padding", input: { sidebarOpen: false, sidebarWidth: 256, isMac: false }, expected: { width: "110px", @@ -57,27 +57,4 @@ describe("getHeaderSidebarPanelLayout", () => { ])("$name", ({ input, expected }) => { expect(getHeaderSidebarPanelLayout(input)).toEqual(expected); }); - - // Regression guards for the bugs this helper fixes. - it("never reserves traffic-light padding while the sidebar is open", () => { - expect( - getHeaderSidebarPanelLayout({ - sidebarOpen: true, - sidebarWidth: 256, - isMac: true, - }).paddingLeft, - ).toBeUndefined(); - }); - - it("drops the divider when collapsed so it can't dangle over the page", () => { - for (const isMac of [true, false]) { - expect( - getHeaderSidebarPanelLayout({ - sidebarOpen: false, - sidebarWidth: 256, - isMac, - }).showBorder, - ).toBe(false); - } - }); });