From be5d6141f493980a4a98f496d6b0b371b9d27a29 Mon Sep 17 00:00:00 2001 From: matteoscrugli Date: Tue, 23 Dec 2025 02:33:31 +0100 Subject: [PATCH] Add swipe-to-open sidebar on first tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When swiping right on the first tab, opens the mobile sidebar menu. Added onSwipePastStart prop to SwipeTabs component. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- frontend/src/components/SwipeTabs.tsx | 11 +++- frontend/src/contexts/SidebarContext.tsx | 66 +--------------------- frontend/src/pages/AdminPanel.tsx | 3 +- frontend/src/pages/admin/Features.tsx | 3 +- frontend/src/pages/admin/ThemeSettings.tsx | 3 +- 5 files changed, 18 insertions(+), 68 deletions(-) diff --git a/frontend/src/components/SwipeTabs.tsx b/frontend/src/components/SwipeTabs.tsx index 631c76e..39bce3e 100644 --- a/frontend/src/components/SwipeTabs.tsx +++ b/frontend/src/components/SwipeTabs.tsx @@ -22,6 +22,7 @@ type SwipeTabsProps = { threshold?: number; renderWindow?: number; scrollToTopOnChange?: boolean; + onSwipePastStart?: () => void; }; const DRAG_START_DISTANCE = 3; @@ -57,7 +58,8 @@ export function SwipeTabs({ swipeDisabled = false, threshold = DEFAULT_THRESHOLD, renderWindow = DEFAULT_RENDER_WINDOW, - scrollToTopOnChange = true + scrollToTopOnChange = true, + onSwipePastStart }: SwipeTabsProps) { const containerRef = useRef(null); const trackRef = useRef(null); @@ -336,6 +338,11 @@ export function SwipeTabs({ if (!cancelled && Math.abs(dx) > thresholdPx) { const direction = dx < 0 ? 1 : -1; targetIndex = Math.min(Math.max(state.startIndex + direction, 0), tabs.length - 1); + + // If swiping right on first tab, call onSwipePastStart + if (state.startIndex === 0 && dx > 0 && onSwipePastStart) { + onSwipePastStart(); + } } dragOffsetRef.current = 0; @@ -354,7 +361,7 @@ export function SwipeTabs({ } } }, - [activeTab, applyTransform, onTabChange, setDraggingClass, tabs, threshold] + [activeTab, applyTransform, onTabChange, onSwipePastStart, setDraggingClass, tabs, threshold] ); const handlePointerDown = (event: PointerEvent) => { diff --git a/frontend/src/contexts/SidebarContext.tsx b/frontend/src/contexts/SidebarContext.tsx index ded48d1..9c4b378 100644 --- a/frontend/src/contexts/SidebarContext.tsx +++ b/frontend/src/contexts/SidebarContext.tsx @@ -1,11 +1,8 @@ -import { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react'; +import { createContext, useContext, useState, useEffect, useCallback } 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 { @@ -15,6 +12,7 @@ interface SidebarContextType { canToggle: boolean; toggleCollapse: () => void; toggleMobileMenu: () => void; + openMobileMenu: () => void; closeMobileMenu: () => void; setSidebarMode: (mode: SidebarMode) => Promise; isHovered: boolean; @@ -116,65 +114,6 @@ export function SidebarProvider({ children }: { children: ReactNode }) { 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; @@ -194,6 +133,7 @@ export function SidebarProvider({ children }: { children: ReactNode }) { canToggle, toggleCollapse, toggleMobileMenu, + openMobileMenu, closeMobileMenu, setSidebarMode, isHovered, diff --git a/frontend/src/pages/AdminPanel.tsx b/frontend/src/pages/AdminPanel.tsx index 4263bf5..7dc397f 100644 --- a/frontend/src/pages/AdminPanel.tsx +++ b/frontend/src/pages/AdminPanel.tsx @@ -13,7 +13,7 @@ type TabId = 'general' | 'users'; export default function AdminPanel({ initialTab = 'general' }: { initialTab?: TabId } = {}) { const { user: currentUser } = useAuth(); const { t } = useTranslation(); - const { toggleMobileMenu } = useSidebar(); + const { toggleMobileMenu, openMobileMenu } = useSidebar(); const [activeTab, setActiveTab] = useState(initialTab); const tabs: TabId[] = ['general', 'users']; const renderPanel = (tab: TabId) => (tab === 'general' ? : ); @@ -57,6 +57,7 @@ export default function AdminPanel({ initialTab = 'general' }: { initialTab?: Ta onTabChange={setActiveTab} renderPanel={renderPanel} panelClassName="admin-tab-content swipe-panel-content" + onSwipePastStart={openMobileMenu} /> ); diff --git a/frontend/src/pages/admin/Features.tsx b/frontend/src/pages/admin/Features.tsx index 37c7610..35f2c0b 100644 --- a/frontend/src/pages/admin/Features.tsx +++ b/frontend/src/pages/admin/Features.tsx @@ -25,7 +25,7 @@ const findTouchById = (touches: TouchList, id: number) => { export default function Features() { const { user: currentUser } = useAuth(); const { t } = useTranslation(); - const { toggleMobileMenu } = useSidebar(); + const { toggleMobileMenu, openMobileMenu } = useSidebar(); const { moduleStates, moduleOrder, setModuleEnabled, setModulePosition, setModuleOrder, saveModulesToBackend, saveModuleOrder, hasInitialized, isLoading } = useModules(); const [activeTab, setActiveTab] = useState('config'); const hasUserMadeChanges = useRef(false); @@ -785,6 +785,7 @@ export default function Features() { onTabChange={handleTabChange} renderPanel={renderTabContent} panelClassName="admin-tab-content swipe-panel-content" + onSwipePastStart={openMobileMenu} /> ); diff --git a/frontend/src/pages/admin/ThemeSettings.tsx b/frontend/src/pages/admin/ThemeSettings.tsx index a497716..10f694a 100644 --- a/frontend/src/pages/admin/ThemeSettings.tsx +++ b/frontend/src/pages/admin/ThemeSettings.tsx @@ -45,7 +45,7 @@ export default function ThemeSettings() { } = useTheme(); const { t } = useTranslation(); const { user } = useAuth(); - const { toggleMobileMenu, sidebarMode, setSidebarMode } = useSidebar(); + const { toggleMobileMenu, openMobileMenu, sidebarMode, setSidebarMode } = useSidebar(); const isAdmin = user?.is_superuser || false; const tabs: ThemeTab[] = isAdmin ? ['colors', 'appearance', 'preview', 'advanced'] : ['colors', 'appearance', 'preview']; const [tooltip, setTooltip] = useState<{ text: string; left: number; visible: boolean }>({ @@ -764,6 +764,7 @@ export default function ThemeSettings() { onTabChange={setActiveTab} renderPanel={renderPanel} panelClassName="admin-tab-content swipe-panel-content" + onSwipePastStart={openMobileMenu} /> {/* Color Picker Popup */}