diff --git a/frontend/src/contexts/SidebarContext.tsx b/frontend/src/contexts/SidebarContext.tsx index cbdf5ee..ded48d1 100644 --- a/frontend/src/contexts/SidebarContext.tsx +++ b/frontend/src/contexts/SidebarContext.tsx @@ -1,8 +1,11 @@ -import { createContext, useContext, useState, useEffect, useCallback } from 'react'; +import { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react'; import type { ReactNode } from 'react'; import { useAuth } from './AuthContext'; import { settingsAPI } from '../api/client'; +const EDGE_SWIPE_THRESHOLD = 50; // pixels from left edge to start swipe +const SWIPE_MIN_DISTANCE = 40; // minimum swipe distance to trigger + export type SidebarMode = 'collapsed' | 'expanded' | 'toggle' | 'dynamic'; interface SidebarContextType { @@ -109,6 +112,69 @@ export function SidebarProvider({ children }: { children: ReactNode }) { setIsMobileOpen(false); }; + const openMobileMenu = useCallback(() => { + setIsMobileOpen(true); + }, []); + + // Edge swipe detection for mobile + const swipeRef = useRef({ + startX: 0, + startY: 0, + isEdgeSwipe: false, + isMobile: false, + }); + + useEffect(() => { + const updateMobile = () => { + swipeRef.current.isMobile = window.innerWidth <= 768; + }; + updateMobile(); + window.addEventListener('resize', updateMobile, { passive: true }); + + const handleTouchStart = (e: TouchEvent) => { + if (!swipeRef.current.isMobile) return; + const touch = e.touches[0]; + if (!touch) return; + + if (touch.clientX <= EDGE_SWIPE_THRESHOLD) { + swipeRef.current.startX = touch.clientX; + swipeRef.current.startY = touch.clientY; + swipeRef.current.isEdgeSwipe = true; + } else { + swipeRef.current.isEdgeSwipe = false; + } + }; + + const handleTouchMove = (e: TouchEvent) => { + if (!swipeRef.current.isEdgeSwipe) return; + const touch = e.touches[0]; + if (!touch) return; + + const dx = touch.clientX - swipeRef.current.startX; + const dy = Math.abs(touch.clientY - swipeRef.current.startY); + + if (dx > SWIPE_MIN_DISTANCE && dx > dy * 2) { + openMobileMenu(); + swipeRef.current.isEdgeSwipe = false; + } + }; + + const handleTouchEnd = () => { + swipeRef.current.isEdgeSwipe = false; + }; + + document.addEventListener('touchstart', handleTouchStart, { passive: true }); + document.addEventListener('touchmove', handleTouchMove, { passive: true }); + document.addEventListener('touchend', handleTouchEnd, { passive: true }); + + return () => { + window.removeEventListener('resize', updateMobile); + document.removeEventListener('touchstart', handleTouchStart); + document.removeEventListener('touchmove', handleTouchMove); + document.removeEventListener('touchend', handleTouchEnd); + }; + }, [openMobileMenu]); + const setSidebarMode = useCallback(async (mode: SidebarMode) => { try { if (!token) return;