From 1387578d5622dc4801a3f38306952e92401160fc Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 23:33:29 +0000 Subject: [PATCH] feat(desktop): let people turn off the sidebar hover reveal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The collapsed sidebar slides out whenever the pointer crosses a 24px band at the window edge, and it dims the content behind a scrim while it is out. There was no way to stop it. Add a "Reveal the sidebar on hover" preference under Appearance. It is on by default, so nothing changes for people who like the current behavior. When it is off, both hover triggers stay quiet — the window edge and the title-bar toggle — and the toggle button and Cmd+B remain the ways to open the sidebar. Toggling the preference emits the standard setting-changed event, so we can finally measure how many people want the hover reveal off. Generated-By: PostHog Desktop Task-Id: 3b41770e-dae5-4037-808b-83e6b5e0f2cb --- .../canvas/components/ChannelsSidebar.tsx | 8 +++-- .../settings/sections/GeneralSettings.tsx | 26 +++++++++++++++ .../src/features/settings/settingsSearch.ts | 5 +++ .../features/settings/settingsStore.test.ts | 1 + .../ui/src/features/settings/settingsStore.ts | 9 ++++++ .../hooks/useSidebarEdgeHoverPeek.test.ts | 32 ++++++++++++++++++- .../packages/ui/src/router/routes/__root.tsx | 4 ++- 7 files changed, 80 insertions(+), 5 deletions(-) diff --git a/products/desktop/packages/ui/src/features/canvas/components/ChannelsSidebar.tsx b/products/desktop/packages/ui/src/features/canvas/components/ChannelsSidebar.tsx index a2603abda259..06d333a662f0 100644 --- a/products/desktop/packages/ui/src/features/canvas/components/ChannelsSidebar.tsx +++ b/products/desktop/packages/ui/src/features/canvas/components/ChannelsSidebar.tsx @@ -29,6 +29,7 @@ import { } from "@posthog/ui/features/canvas/stores/channelPaneStore"; import { useCurrentChannelStore } from "@posthog/ui/features/canvas/stores/currentChannelStore"; import { useOnboardingStore } from "@posthog/ui/features/onboarding/onboardingStore"; +import { useSettingsStore } from "@posthog/ui/features/settings/settingsStore"; import { NavResizeTooltip } from "@posthog/ui/features/sidebar/components/NavResizeTooltip"; import { ProjectSwitcher } from "@posthog/ui/features/sidebar/components/ProjectSwitcher"; import { SidebarMenu } from "@posthog/ui/features/sidebar/components/SidebarMenu"; @@ -158,8 +159,9 @@ function ChannelsSidebarImpl() { const channelsLayout = useChannelsLayout(); const peek = useSidebarPeekStore((s) => s.peek); + const revealOnHover = useSettingsStore((s) => s.revealSidebarOnHover); useSidebarEdgeHoverPeek({ - enabled: !open && !isResizing, + enabled: revealOnHover && !open && !isResizing, peeked: peek, side: "left", width, @@ -169,8 +171,8 @@ function ChannelsSidebarImpl() { onClose: () => endSidebarPeek(), }); useEffect(() => { - if (open) cancelSidebarPeek(); - }, [open]); + if (open || !revealOnHover) cancelSidebarPeek(); + }, [open, revealOnHover]); // The peek store is a module-level singleton — if this sidebar unmounts // while peeked (route without it), a stale peek would greet the remount. useEffect(() => () => cancelSidebarPeek(), []); diff --git a/products/desktop/packages/ui/src/features/settings/sections/GeneralSettings.tsx b/products/desktop/packages/ui/src/features/settings/sections/GeneralSettings.tsx index 96f547e0b89a..7f43a152c61c 100644 --- a/products/desktop/packages/ui/src/features/settings/sections/GeneralSettings.tsx +++ b/products/desktop/packages/ui/src/features/settings/sections/GeneralSettings.tsx @@ -138,6 +138,7 @@ export function GeneralSettings() { defaultCloudMessagingMode, defaultReasoningEffort, diffOpenMode, + revealSidebarOnHover, sendMessagesWith, setAutoConvertLongText, setDefaultInitialTaskMode, @@ -145,6 +146,7 @@ export function GeneralSettings() { setDefaultCloudMessagingMode, setDefaultReasoningEffort, setDiffOpenMode, + setRevealSidebarOnHover, setSendMessagesWith, } = useSettingsStore(); @@ -160,6 +162,18 @@ export function GeneralSettings() { [theme, setTheme], ); + const handleRevealSidebarOnHoverChange = useCallback( + (checked: boolean) => { + track(ANALYTICS_EVENTS.SETTING_CHANGED, { + setting_name: "reveal_sidebar_on_hover", + new_value: checked, + old_value: !checked, + }); + setRevealSidebarOnHover(checked); + }, + [setRevealSidebarOnHover], + ); + const handleAutoConvertLongTextChange = useCallback( (value: AutoConvertLongText) => { track(ANALYTICS_EVENTS.SETTING_CHANGED, { @@ -267,6 +281,18 @@ export function GeneralSettings() { )} + + + + + { ["slotMachineMode", false, true], ["dismissibleUpdateBanners", false, true], ["showSidebarWorktrees", false, true], + ["revealSidebarOnHover", true, false], ] as const)("rehydrates %s", async (field, initial, persisted) => { getItem.mockResolvedValue( JSON.stringify({ state: { [field]: persisted }, version: 0 }), diff --git a/products/desktop/packages/ui/src/features/settings/settingsStore.ts b/products/desktop/packages/ui/src/features/settings/settingsStore.ts index 9bf05ee08226..d70710c71e6b 100644 --- a/products/desktop/packages/ui/src/features/settings/settingsStore.ts +++ b/products/desktop/packages/ui/src/features/settings/settingsStore.ts @@ -314,6 +314,11 @@ export interface SettingsStore { // start a task in. Opt-in: off by default to keep the sidebar uncluttered. showSidebarWorktrees: boolean; setShowSidebarWorktrees: (enabled: boolean) => void; + // Slides the collapsed sidebar out when the pointer reaches the window edge + // or the title-bar toggle. On by default; off leaves the toggle and Cmd+B as + // the only ways to open it. + revealSidebarOnHover: boolean; + setRevealSidebarOnHover: (enabled: boolean) => void; // Experimental / misc hedgehogMode: boolean; @@ -588,6 +593,9 @@ export const useSettingsStore = create()( showSidebarWorktrees: false, setShowSidebarWorktrees: (enabled) => set({ showSidebarWorktrees: enabled }), + revealSidebarOnHover: true, + setRevealSidebarOnHover: (enabled) => + set({ revealSidebarOnHover: enabled }), // Experimental / misc hedgehogMode: false, @@ -733,6 +741,7 @@ export const useSettingsStore = create()( // Sidebar showSidebarWorktrees: state.showSidebarWorktrees, + revealSidebarOnHover: state.revealSidebarOnHover, // Experimental / misc hedgehogMode: state.hedgehogMode, diff --git a/products/desktop/packages/ui/src/primitives/hooks/useSidebarEdgeHoverPeek.test.ts b/products/desktop/packages/ui/src/primitives/hooks/useSidebarEdgeHoverPeek.test.ts index 45e61f10f1b5..f1ed234b9717 100644 --- a/products/desktop/packages/ui/src/primitives/hooks/useSidebarEdgeHoverPeek.test.ts +++ b/products/desktop/packages/ui/src/primitives/hooks/useSidebarEdgeHoverPeek.test.ts @@ -3,8 +3,10 @@ import { PEEK_REVEAL_THRESHOLD, shouldCloseOnExit, shouldRevealOnEdge, + useSidebarEdgeHoverPeek, } from "@posthog/ui/primitives/hooks/useSidebarEdgeHoverPeek"; -import { describe, expect, it } from "vitest"; +import { renderHook } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; describe("shouldRevealOnEdge", () => { const threshold = PEEK_REVEAL_THRESHOLD; @@ -39,3 +41,31 @@ describe("shouldCloseOnExit", () => { expect(shouldCloseOnExit({ pointer, width, margin })).toBe(expected); }); }); + +describe("useSidebarEdgeHoverPeek", () => { + const moveTo = (clientX: number): void => { + document.dispatchEvent(new MouseEvent("mousemove", { clientX })); + }; + + it.each([ + ["reveals while enabled", true, 1], + ["stays closed while disabled", false, 0], + ])("%s", (_name, enabled, calls) => { + const onReveal = vi.fn(); + renderHook(() => + useSidebarEdgeHoverPeek({ + enabled, + peeked: false, + side: "left", + width: 240, + onReveal, + onClose: vi.fn(), + }), + ); + + moveTo(400); + moveTo(2); + + expect(onReveal).toHaveBeenCalledTimes(calls); + }); +}); diff --git a/products/desktop/packages/ui/src/router/routes/__root.tsx b/products/desktop/packages/ui/src/router/routes/__root.tsx index 9027ea01a256..93fff1ef990c 100644 --- a/products/desktop/packages/ui/src/router/routes/__root.tsx +++ b/products/desktop/packages/ui/src/router/routes/__root.tsx @@ -46,6 +46,7 @@ import { useInboxDeepLink } from "@posthog/ui/features/inbox/hooks/useInboxDeepL import { useIntegrations } from "@posthog/ui/features/integrations/useIntegrations"; import { useLoopDeepLink } from "@posthog/ui/features/loops/hooks/useLoopDeepLink"; import { useScoutDeepLink } from "@posthog/ui/features/scouts/hooks/useScoutDeepLink"; +import { useSettingsStore } from "@posthog/ui/features/settings/settingsStore"; import { useSetupDiscovery } from "@posthog/ui/features/setup/useSetupDiscovery"; import { NAV_RAIL_WIDTH } from "@posthog/ui/features/sidebar/constants"; import { @@ -218,6 +219,7 @@ function RootLayout() { const toggleSidebar = useSidebarStore((s) => s.toggle); const sidebarPeek = useSidebarPeekStore((s) => s.peek); + const revealSidebarOnHover = useSettingsStore((s) => s.revealSidebarOnHover); // Toggling makes any hover-peek redundant (opening replaces the overlay; // closing must not leave it lingering under the pointer). const handleToggleSidebar = (): void => { @@ -348,7 +350,7 @@ function RootLayout() { aria-label="Toggle sidebar" onClick={handleToggleSidebar} onMouseEnter={() => { - if (!sidebarOpen) beginSidebarPeek(); + if (revealSidebarOnHover && !sidebarOpen) beginSidebarPeek(); }} > {sidebarOpen ? (