Re-add edge swipe to open sidebar on mobile

Swipe from left edge (50px) to open sidebar menu

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-23 02:28:02 +01:00
parent acebfb7adb
commit 18c4760b5d

View File

@@ -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;