Add tab bar position setting with edge swipe sidebar
- Add theme_tab_bar_position setting (top/bottom/responsive) - Tab bar is now fixed at top, stays visible during scroll - Bottom position uses fixed positioning with safe-area-inset - Add edge swipe gesture to open sidebar on mobile - Remove backdrop-filter for better scroll performance - Simplify TabsScroller by removing inline style manipulation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 = 30; // pixels from left edge to start swipe
|
||||
const SWIPE_MIN_DISTANCE = 50; // minimum swipe distance to trigger
|
||||
|
||||
export type SidebarMode = 'collapsed' | 'expanded' | 'toggle' | 'dynamic';
|
||||
|
||||
interface SidebarContextType {
|
||||
@@ -85,6 +88,12 @@ export function SidebarProvider({ children }: { children: ReactNode }) {
|
||||
// Can only toggle if mode is 'toggle'
|
||||
const canToggle = sidebarMode === 'toggle';
|
||||
|
||||
// Sync collapsed state to document root for CSS selectors
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
root.setAttribute('data-sidebar-collapsed', String(isCollapsed));
|
||||
}, [isCollapsed]);
|
||||
|
||||
const toggleCollapse = () => {
|
||||
if (canToggle) {
|
||||
setUserCollapsed((prev) => {
|
||||
@@ -103,6 +112,67 @@ 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,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const isMobile = () => window.innerWidth <= 768;
|
||||
|
||||
const handleTouchStart = (e: TouchEvent) => {
|
||||
if (!isMobile()) return;
|
||||
const touch = e.touches[0];
|
||||
if (!touch) return;
|
||||
|
||||
// Check if touch started near left edge
|
||||
if (touch.clientX <= EDGE_SWIPE_THRESHOLD) {
|
||||
swipeRef.current = {
|
||||
startX: touch.clientX,
|
||||
startY: touch.clientY,
|
||||
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 horizontal movement is greater than vertical and exceeds threshold, open sidebar
|
||||
if (dx > SWIPE_MIN_DISTANCE && dx > dy * 2) {
|
||||
openMobileMenu();
|
||||
swipeRef.current.isEdgeSwipe = false; // Prevent multiple triggers
|
||||
}
|
||||
};
|
||||
|
||||
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 () => {
|
||||
document.removeEventListener('touchstart', handleTouchStart);
|
||||
document.removeEventListener('touchmove', handleTouchMove);
|
||||
document.removeEventListener('touchend', handleTouchEnd);
|
||||
};
|
||||
}, [openMobileMenu]);
|
||||
|
||||
const setSidebarMode = useCallback(async (mode: SidebarMode) => {
|
||||
try {
|
||||
if (!token) return;
|
||||
|
||||
Reference in New Issue
Block a user