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..86ff9f456 --- /dev/null +++ b/packages/ui/src/shell/headerSidebarPanel.test.ts @@ -0,0 +1,60 @@ +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 and drops the divider", + input: { sidebarOpen: false, sidebarWidth: 256, isMac: true }, + expected: { + width: "180px", + minWidth: "180px", + paddingLeft: "70px", + justify: "start", + showBorder: false, + }, + }, + { + name: "collapsed off macOS drops the divider and 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); + }); +}); 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, + }; +}