From 75e54bb82e8db3e9ef789a985cac0a155957e361 Mon Sep 17 00:00:00 2001 From: Forgotten Date: Wed, 25 Feb 2026 21:43:49 -0600 Subject: [PATCH] feat(ui): add swipe gesture to open/close sidebar on mobile Swipe right from the left edge (30px zone) opens the sidebar, swipe left when open closes it. Ignores vertical scrolling. Co-Authored-By: Claude Opus 4.6 --- ui/src/components/Layout.tsx | 45 ++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) 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;