From 8d13b25f51be99a588cf872e21d2cc842cf4baf2 Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 8 Jun 2026 22:03:40 +0000 Subject: [PATCH] =?UTF-8?q?fix(ui):=20sidebar=20follow-ups=20=E2=80=94=20s?= =?UTF-8?q?table=20logo,=20symmetric=20rail=20highlight,=20sticky=20peek?= =?UTF-8?q?=20(PAP-10676)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses board feedback on the collapsible sidebar: - Company logo no longer shifts on collapse. The workspace-switcher Button's default size includes `has-[>svg]:px-3`, so with the chevron svg present (expanded) its padding was 12px but in the rail (chevron hidden) it fell back to the `px-2` of 8px — a 4px horizontal jump. Pinning it to `px-3` keeps the logo's left edge in the icon column in both states. - Selected-item highlight is now symmetric in the rail. The icon sits at left=24 (nav px-3 + item px-3); a 56px rail left only 4px of trailing space so the active background looked clipped on the right / heavy on the left. Rail width is now 64 (24 + 16 icon + 24), centering the icon and making the full-width highlight symmetric around it. Icons still don't move (left edge unchanged). The cramped rail also clipped the search/toggle controls, so they are dropped in the rail (expansion stays available via hover-peek + Pin and Cmd/Ctrl+B); they return when expanded or peeking. - Peek no longer collapses on navigation. Removed the close-peek-on-pathname effect and guarded the panel blur handler with a pointer-inside ref: clicking a nav item moves focus to
, but while the pointer is still over the flyout the peek stays open until the user actually mouses off. Verified by rendering the real Sidebar at the 64px rail: company logo and every nav icon align to left=25 in both states (dX=0), the active highlight is centered/symmetric, and the icon-no-move guarantee still holds (max |delta| 1px, rounding). 60 sidebar tests + Layout tests pass; tsc clean. Co-Authored-By: Paperclip --- ui/src/components/Layout.tsx | 35 ++++++++--- ui/src/components/Sidebar.test.tsx | 19 +++--- ui/src/components/Sidebar.tsx | 79 +++++++++++++----------- ui/src/components/SidebarCompanyMenu.tsx | 7 ++- ui/src/components/SidebarShell.tsx | 12 ++-- 5 files changed, 94 insertions(+), 58 deletions(-) diff --git a/ui/src/components/Layout.tsx b/ui/src/components/Layout.tsx index 0144eb6d52b..c4fd8dc5623 100644 --- a/ui/src/components/Layout.tsx +++ b/ui/src/components/Layout.tsx @@ -212,6 +212,11 @@ export function Layout() { // effective `peeking` to desktop + collapsed + hover-capable pointers, so // these handlers are inert otherwise. const peekTimer = useRef(null); + // Whether the pointer is currently over the peek panel. Used to keep the peek + // open across focus changes (e.g. navigation steals focus to
) as long as + // the user is still hovering — it should only close when they actually mouse off + // (PAP-10676). + const pointerInsidePanel = useRef(false); const clearPeekTimer = useCallback(() => { if (peekTimer.current !== null) { window.clearTimeout(peekTimer.current); @@ -230,15 +235,29 @@ export function Layout() { clearPeekTimer(); peekTimer.current = window.setTimeout(() => setPeeking(false), 120); }, [clearPeekTimer, setPeeking]); + const handlePanelPointerEnter = useCallback(() => { + pointerInsidePanel.current = true; + openPeek(); + }, [openPeek]); + const handlePanelPointerLeave = useCallback(() => { + pointerInsidePanel.current = false; + closePeek(); + }, [closePeek]); + // Close on focus leaving the panel only when the pointer isn't hovering it. + // Clicking a rail/peek nav item moves focus to
on navigation; if the + // mouse is still over the flyout we keep it open until the pointer leaves. + const handlePanelBlur = useCallback(() => { + if (pointerInsidePanel.current) return; + closePeek(); + }, [closePeek]); // Tidy up any pending peek timer on unmount. useEffect(() => clearPeekTimer, [clearPeekTimer]); - // Close peek on navigation so it never lingers over a freshly routed page. - useEffect(() => { - clearPeekTimer(); - setPeeking(false); - }, [location.pathname, clearPeekTimer, setPeeking]); + // Intentionally do NOT close the peek on navigation: clicking a nav item means + // the pointer is still over the flyout, so it should stay open until the user + // actually mouses off (handled by onPanelMouseLeave) or blurs out / hits Escape + // (PAP-10676). Auto-closing here made the sidebar collapse on every page change. // Escape closes an open peek without trapping the pointer. useEffect(() => { @@ -435,10 +454,10 @@ export function Layout() { collapsed={collapsed} peeking={peeking} resizable - onPanelMouseEnter={collapsed ? openPeek : undefined} - onPanelMouseLeave={collapsed ? closePeek : undefined} + onPanelMouseEnter={collapsed ? handlePanelPointerEnter : undefined} + onPanelMouseLeave={collapsed ? handlePanelPointerLeave : undefined} onPanelFocusCapture={collapsed ? openPeekImmediate : undefined} - onPanelBlurCapture={collapsed ? closePeek : undefined} + onPanelBlurCapture={collapsed ? handlePanelBlur : undefined} >
{isCompanySettingsRoute ? ( diff --git a/ui/src/components/Sidebar.test.tsx b/ui/src/components/Sidebar.test.tsx index acd8f5c5303..960adfcc31d 100644 --- a/ui/src/components/Sidebar.test.tsx +++ b/ui/src/components/Sidebar.test.tsx @@ -315,19 +315,20 @@ describe("Sidebar", () => { }); }); - it("header toggle expands a collapsed rail", async () => { + it("keeps the collapsed rail top bar to just the company logo (no clipped search/toggle)", async () => { + // In the narrow rail the search/toggle controls don't fit beside the logo and + // would overflow/clip, shoving the logo out of the icon column (PAP-10676), so + // they are dropped in the rail. Expansion stays reachable via hover-peek + Pin + // and Cmd/Ctrl+B. The full controls return as soon as the panel is expanded or + // peeking (covered by the other top-bar tests). mockSidebar.collapsed = true; mockInstanceSettingsApi.getExperimental.mockResolvedValue({ enableIsolatedWorkspaces: false }); const root = await renderSidebar(); - const toggle = container.querySelector('button[aria-label="Expand sidebar"]'); - expect(toggle).not.toBeNull(); - expect(toggle?.getAttribute("aria-expanded")).toBe("false"); - - act(() => { - toggle?.click(); - }); - expect(mockSidebar.toggleCollapsed).toHaveBeenCalledTimes(1); + expect(container.querySelector('button[aria-label="Expand sidebar"]')).toBeNull(); + expect(container.querySelector('a[aria-label="Open search"]')).toBeNull(); + // The company menu (workspace switcher / logo) is still present in the rail. + expect(container.textContent).toContain("Company menu"); flushSync(() => { root.unmount(); diff --git a/ui/src/components/Sidebar.tsx b/ui/src/components/Sidebar.tsx index 04778f264f2..73d08e59837 100644 --- a/ui/src/components/Sidebar.tsx +++ b/ui/src/components/Sidebar.tsx @@ -72,47 +72,56 @@ export function Sidebar() { {/* Top bar: Company name (bold) + Search — aligned with top sections (no visible border) */}
- - {/* Desktop-only collapse/expand affordance. While peeking (hover flyout - over the collapsed rail) it becomes a Pin that promotes the peek to a - pinned-expanded sidebar; otherwise it toggles the pinned rail. Mobile - uses the off-canvas drawer, so this control is hidden there. */} - {!isMobile ? ( - peeking ? ( + {/* In the collapsed rail the search/toggle controls don't fit beside the + logo — keeping them would overflow the 64px rail and squeeze the logo + out of alignment with the icon column below it (PAP-10676). They return + as soon as the panel is expanded (pinned) or peeking. Expansion in the + rail is still reachable via hover-peek + Pin and Cmd/Ctrl+B. */} + {!rail ? ( + <> - ) : ( - - ) + {/* Desktop-only collapse/expand affordance. While peeking (hover flyout + over the collapsed rail) it becomes a Pin that promotes the peek to a + pinned-expanded sidebar; otherwise it toggles the pinned rail. Mobile + uses the off-canvas drawer, so this control is hidden there. */} + {!isMobile ? ( + peeking ? ( + + ) : ( + + ) + ) : null} + ) : null}
diff --git a/ui/src/components/SidebarCompanyMenu.tsx b/ui/src/components/SidebarCompanyMenu.tsx index 7b93c5a2693..c1d77f84534 100644 --- a/ui/src/components/SidebarCompanyMenu.tsx +++ b/ui/src/components/SidebarCompanyMenu.tsx @@ -225,7 +225,12 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb