diff --git a/ui/src/components/Layout.tsx b/ui/src/components/Layout.tsx index 53c8f14c..1e920b6a 100644 --- a/ui/src/components/Layout.tsx +++ b/ui/src/components/Layout.tsx @@ -80,6 +80,51 @@ export function Layout() { setMobileNavVisible(true); }, [isMobile]); + // Swipe gesture to open/close sidebar on mobile + useEffect(() => { + if (!isMobile) return; + + const EDGE_ZONE = 30; // px from left edge to start open-swipe + const MIN_DISTANCE = 50; // minimum horizontal swipe distance + const MAX_VERTICAL = 75; // max vertical drift before we ignore + + let startX = 0; + let startY = 0; + + const onTouchStart = (e: TouchEvent) => { + const t = e.touches[0]!; + startX = t.clientX; + startY = t.clientY; + }; + + const onTouchEnd = (e: TouchEvent) => { + const t = e.changedTouches[0]!; + const dx = t.clientX - startX; + const dy = Math.abs(t.clientY - startY); + + if (dy > MAX_VERTICAL) return; // vertical scroll, ignore + + // Swipe right from left edge → open + if (!sidebarOpen && startX < EDGE_ZONE && dx > MIN_DISTANCE) { + setSidebarOpen(true); + return; + } + + // Swipe left when open → close + if (sidebarOpen && dx < -MIN_DISTANCE) { + setSidebarOpen(false); + } + }; + + document.addEventListener("touchstart", onTouchStart, { passive: true }); + document.addEventListener("touchend", onTouchEnd, { passive: true }); + + return () => { + document.removeEventListener("touchstart", onTouchStart); + document.removeEventListener("touchend", onTouchEnd); + }; + }, [isMobile, sidebarOpen, setSidebarOpen]); + const handleMainScroll = useCallback( (event: UIEvent) => { if (!isMobile) return;