From faad03530c33625997934a576e9c52003cd768b7 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sun, 7 Jun 2026 20:22:52 -0500 Subject: [PATCH] feat(desktop): hover-reveal collapsed chat sidebar as a fixed overlay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the sessions sidebar is collapsed, hovering the left edge now floats it back in as a fixed overlay over the main content instead of just being hidden. The collapsed grid track stays at 0px so the panel never reserves space — it slides over whatever's underneath and retracts on pointer-leave. - PaneShell: new hoverReveal prop. When a pane is collapsed + hoverReveal, render an edge hot-zone + a side-anchored floating panel (absolute, full height, honors any persisted resize width) that slides in on hover/focus. - ChatSidebar: force the (otherwise opacity-0 when collapsed) sidebar fully visible + interactive while the overlay is revealed, via an in-data-[pane-hover-reveal=open] variant. - desktop-controller: opt the chat-sidebar pane into hoverReveal. --- apps/desktop/src/app/chat/sidebar/index.tsx | 7 +- apps/desktop/src/app/desktop-controller.tsx | 1 + .../src/components/pane-shell/pane-shell.tsx | 89 ++++++++++++++++++- 3 files changed, 94 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/app/chat/sidebar/index.tsx b/apps/desktop/src/app/chat/sidebar/index.tsx index ef1832837f3..e8a2c3827e3 100644 --- a/apps/desktop/src/app/chat/sidebar/index.tsx +++ b/apps/desktop/src/app/chat/sidebar/index.tsx @@ -580,7 +580,12 @@ export function ChatSidebar({ panesFlipped ? 'border-l border-r-0' : 'border-r border-l-0', sidebarOpen ? 'border-(--sidebar-edge-border) bg-(--ui-sidebar-surface-background) opacity-100' - : 'pointer-events-none border-transparent bg-transparent opacity-0' + : 'pointer-events-none border-transparent bg-transparent opacity-0', + // Hover-reveal overlay: when collapsed, the PaneShell floats this + // sidebar over the content and marks the wrapper `data-pane-hover-reveal`. + // Force it fully visible + interactive while revealed, regardless of the + // collapsed (sidebarOpen=false) styling above. + 'in-data-[pane-hover-reveal=open]:pointer-events-auto in-data-[pane-hover-reveal=open]:border-(--sidebar-edge-border) in-data-[pane-hover-reveal=open]:bg-(--ui-sidebar-surface-background) in-data-[pane-hover-reveal=open]:opacity-100' )} collapsible="none" > diff --git a/apps/desktop/src/app/desktop-controller.tsx b/apps/desktop/src/app/desktop-controller.tsx index 15466d20950..c0a8d4809c0 100644 --- a/apps/desktop/src/app/desktop-controller.tsx +++ b/apps/desktop/src/app/desktop-controller.tsx @@ -873,6 +873,7 @@ export function DesktopController() { > (null) + const [hoverRevealed, setHoverRevealed] = useState(false) useEffect(() => { if (registered.current) { @@ -218,6 +230,24 @@ export function Pane({ const hi = widthToPx(maxWidth) ?? Number.POSITIVE_INFINITY const side = slot?.side ?? 'left' + // Collapsed + hoverReveal: float the pane contents over the main column on + // hover/focus instead of leaving them fully hidden. Honors any persisted + // resize width so the overlay matches the pane's expanded size. + const overlayActive = !open && hoverReveal && !disabled + + const overlayWidth = + resizable && paneStates[id]?.widthOverride !== undefined + ? `${paneStates[id]?.widthOverride}px` + : widthToCss(width, DEFAULT_WIDTH) + + // Collapse the reveal whenever the pane reopens or gets disabled so it never + // sticks "open" underneath the now-expanded track. + useEffect(() => { + if (!overlayActive && hoverRevealed) { + setHoverRevealed(false) + } + }, [overlayActive, hoverRevealed]) + const startResize = useCallback( (event: ReactPointerEvent) => { const paneWidth = paneRef.current?.getBoundingClientRect().width ?? 0 @@ -273,6 +303,61 @@ export function Pane({ return null } + // Collapsed hover-reveal track: keep the grid cell at 0px (no reserved space) + // but don't clip — the trigger strip and the floating overlay both escape the + // zero-width box via absolute positioning, so the panel renders over the main + // content rather than pushing it. + if (overlayActive) { + const revealEdge = slot.side === 'left' ? { left: 0 } : { right: 0 } + + return ( +
+ {/* Edge hot-zone: hovering it (or moving onto the revealed overlay) + keeps the panel open. Sits above main content but is invisible. */} +
+ ) + } + return (