diff --git a/src/client/Sidebar.tsx b/src/client/Sidebar.tsx index 07035d0..5773559 100644 --- a/src/client/Sidebar.tsx +++ b/src/client/Sidebar.tsx @@ -535,6 +535,26 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) { pendingDrag.current = null } + /** Set once a drag's pointerup handler commits — premature capture loss + * (pointercancel / lostpointercapture without pointerup) must then be told + * apart from a normal release. */ + const dragCommitted = useRef(false) + /** Abort a drag whose pointer stream was interrupted (pointercancel, or + * capture lost before pointerup): no pointerup will arrive, so without + * this the dragging state would stick true and center-column measurement + * would stay paused forever — the bottom panel freezes at stale edges and + * stops tracking sidebar/app-rail layout changes. Reverts the DOM and the + * layout variables to the store's committed sizes. */ + const abortDrag = (reset: () => void): void => { + if (dragCommitted.current) return + stopDragScheduling() + applyDrag( + !narrow && state?.panelOpen === true ? Math.min(state?.width ?? 0, window.innerWidth) : 0, + !narrow && state?.bottomOpen === true ? Math.min(state.bottomHeight, window.innerHeight) : 0, + ) + reset() + } + // Layout push: the app shell gives up the panel's width/height while the // panels are open (0 while collapsed), so the conversation and input bar // are squeezed instead of covered. The margins are capped at the viewport @@ -784,6 +804,7 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) { onPointerDown={(event) => { event.preventDefault() event.currentTarget.setPointerCapture(event.pointerId) + dragCommitted.current = false widthDrag.current = { startX: event.clientX, startWidth: state.width } setDraggingWidth(true) }} @@ -796,12 +817,15 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) { }} onPointerUp={(event) => { if (!event.currentTarget.hasPointerCapture(event.pointerId)) return + dragCommitted.current = true event.currentTarget.releasePointerCapture(event.pointerId) const { startX, startWidth } = widthDrag.current stopDragScheduling() store.reduce(s => setWidth(s, startWidth + (startX - event.clientX))) setDraggingWidth(false) }} + onPointerCancel={() => { abortDrag(() => setDraggingWidth(false)) }} + onLostPointerCapture={() => { abortDrag(() => setDraggingWidth(false)) }} /> )}