Add swipe-to-open sidebar on all pages + fix bottom bar styling
- Create SwipeableContent component for sidebar swipe on non-tab pages - Add swipe-to-close sidebar from overlay - Make swipe work from entire page (ignoring interactive elements) - Show title and divider on desktop when tab bar is at bottom - Hide title/divider only on mobile for bottom position 🤖 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,4 +1,4 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { useState, useRef, useCallback, type TouchEvent } from 'react';
|
||||
import { NavLink, useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from '../contexts/LanguageContext';
|
||||
import { useSidebar } from '../contexts/SidebarContext';
|
||||
@@ -12,6 +12,9 @@ import { appModules } from '../modules';
|
||||
import UserMenu from './UserMenu';
|
||||
import '../styles/Sidebar.css';
|
||||
|
||||
const SIDEBAR_WIDTH = 280;
|
||||
const DRAG_START_DISTANCE = 3;
|
||||
|
||||
export default function Sidebar() {
|
||||
const { t, language, setLanguage } = useTranslation();
|
||||
const { config } = useSiteConfig();
|
||||
@@ -24,7 +27,11 @@ export default function Sidebar() {
|
||||
closeMobileMenu,
|
||||
isHovered,
|
||||
setIsHovered,
|
||||
showLogo: showLogoContext
|
||||
showLogo: showLogoContext,
|
||||
sidebarDragProgress,
|
||||
setSidebarDragProgress,
|
||||
isSidebarDragging,
|
||||
setIsSidebarDragging
|
||||
} = useSidebar();
|
||||
const { viewMode, toggleViewMode, isUserModeEnabled } = useViewMode();
|
||||
const { user } = useAuth();
|
||||
@@ -167,10 +174,137 @@ export default function Sidebar() {
|
||||
setTooltip((prev) => prev.visible ? { ...prev, text } : prev);
|
||||
};
|
||||
|
||||
// Swipe-to-close handlers for mobile sidebar
|
||||
const closeDragRef = useRef({
|
||||
touchId: null as number | null,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
isActive: false,
|
||||
isDragging: false
|
||||
});
|
||||
|
||||
const handleSidebarTouchStart = useCallback((event: TouchEvent<HTMLElement>) => {
|
||||
// Only handle when sidebar is open on mobile
|
||||
if (!isMobileOpen) return;
|
||||
if (closeDragRef.current.isActive) return;
|
||||
|
||||
const touch = event.touches[0];
|
||||
if (!touch) return;
|
||||
|
||||
closeDragRef.current = {
|
||||
touchId: touch.identifier,
|
||||
startX: touch.clientX,
|
||||
startY: touch.clientY,
|
||||
isActive: true,
|
||||
isDragging: false
|
||||
};
|
||||
}, [isMobileOpen]);
|
||||
|
||||
const handleSidebarTouchMove = useCallback((event: TouchEvent<HTMLElement>) => {
|
||||
const state = closeDragRef.current;
|
||||
if (!state.isActive || state.touchId === null) return;
|
||||
|
||||
const touch = Array.from(event.touches).find(t => t.identifier === state.touchId);
|
||||
if (!touch) return;
|
||||
|
||||
const dx = touch.clientX - state.startX;
|
||||
const dy = touch.clientY - state.startY;
|
||||
|
||||
// Check if we should start dragging
|
||||
if (!state.isDragging) {
|
||||
if (Math.abs(dx) < DRAG_START_DISTANCE && Math.abs(dy) < DRAG_START_DISTANCE) {
|
||||
return;
|
||||
}
|
||||
// Only start horizontal drags going left (negative dx)
|
||||
if (dx >= 0 || Math.abs(dx) <= Math.abs(dy)) {
|
||||
state.isActive = false;
|
||||
return;
|
||||
}
|
||||
state.isDragging = true;
|
||||
// Initialize progress to 1 (fully open) before setting dragging state
|
||||
setSidebarDragProgress(1);
|
||||
setIsSidebarDragging(true);
|
||||
}
|
||||
|
||||
// Calculate progress (1 = fully open, 0 = closed)
|
||||
// dx is negative when swiping left
|
||||
const progress = Math.max(0, Math.min(1, 1 + dx / SIDEBAR_WIDTH));
|
||||
setSidebarDragProgress(progress);
|
||||
}, [setSidebarDragProgress, setIsSidebarDragging]);
|
||||
|
||||
const handleSidebarTouchEnd = useCallback((event: TouchEvent<HTMLElement>) => {
|
||||
const state = closeDragRef.current;
|
||||
if (!state.isActive || state.touchId === null) return;
|
||||
|
||||
const touch = Array.from(event.changedTouches).find(t => t.identifier === state.touchId);
|
||||
if (!touch) return;
|
||||
|
||||
const wasDragging = state.isDragging;
|
||||
const dx = touch.clientX - state.startX;
|
||||
|
||||
// Reset state
|
||||
closeDragRef.current = {
|
||||
touchId: null,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
isActive: false,
|
||||
isDragging: false
|
||||
};
|
||||
|
||||
if (!wasDragging) return;
|
||||
|
||||
setIsSidebarDragging(false);
|
||||
setSidebarDragProgress(0);
|
||||
|
||||
// Close sidebar if dragged past 30% threshold (dx is negative)
|
||||
if (dx < -SIDEBAR_WIDTH * 0.3) {
|
||||
closeMobileMenu();
|
||||
}
|
||||
}, [closeMobileMenu, setSidebarDragProgress, setIsSidebarDragging]);
|
||||
|
||||
const handleSidebarTouchCancel = useCallback(() => {
|
||||
if (closeDragRef.current.isDragging) {
|
||||
setIsSidebarDragging(false);
|
||||
setSidebarDragProgress(0);
|
||||
}
|
||||
closeDragRef.current = {
|
||||
touchId: null,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
isActive: false,
|
||||
isDragging: false
|
||||
};
|
||||
}, [setSidebarDragProgress, setIsSidebarDragging]);
|
||||
|
||||
// Calculate sidebar transform for close gesture (when dragging to close)
|
||||
const getSidebarCloseTransform = () => {
|
||||
if (!isSidebarDragging || !isMobileOpen) return undefined;
|
||||
// When closing, progress goes from 1 (open) to 0 (closed)
|
||||
// Transform: 0% when open (progress=1), -110% when closed (progress=0)
|
||||
const translatePercent = (1 - sidebarDragProgress) * -110;
|
||||
return `translateX(${translatePercent}%)`;
|
||||
};
|
||||
|
||||
const sidebarTransform = isSidebarDragging
|
||||
? (isMobileOpen ? getSidebarCloseTransform() : `translateX(calc(-110% + ${sidebarDragProgress * 110}%))`)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile overlay */}
|
||||
<div className={`sidebar-overlay ${isMobileOpen ? 'visible' : ''}`} onClick={closeMobileMenu} />
|
||||
<div
|
||||
className={`sidebar-overlay ${isMobileOpen ? 'visible' : ''} ${isSidebarDragging ? 'dragging' : ''}`}
|
||||
onClick={closeDragRef.current.isDragging ? undefined : closeMobileMenu}
|
||||
onTouchStart={handleSidebarTouchStart}
|
||||
onTouchMove={handleSidebarTouchMove}
|
||||
onTouchEnd={handleSidebarTouchEnd}
|
||||
onTouchCancel={handleSidebarTouchCancel}
|
||||
style={isSidebarDragging ? {
|
||||
opacity: sidebarDragProgress,
|
||||
visibility: sidebarDragProgress > 0 ? 'visible' : 'hidden',
|
||||
transition: 'none'
|
||||
} : undefined}
|
||||
/>
|
||||
|
||||
{/* Sidebar Tooltip */}
|
||||
{tooltip.visible && (
|
||||
@@ -186,11 +320,19 @@ export default function Sidebar() {
|
||||
)}
|
||||
|
||||
<aside
|
||||
className={`sidebar ${isCollapsed && !isMobileOpen ? 'collapsed' : ''} ${isMobileOpen ? 'open' : ''} ${sidebarMode === 'dynamic' ? 'dynamic' : ''} ${isDynamicExpanded ? 'expanded-force' : ''} ${sidebarMode === 'toggle' ? 'clickable' : ''}`}
|
||||
className={`sidebar ${isCollapsed && !isMobileOpen ? 'collapsed' : ''} ${isMobileOpen ? 'open' : ''} ${sidebarMode === 'dynamic' ? 'dynamic' : ''} ${isDynamicExpanded ? 'expanded-force' : ''} ${sidebarMode === 'toggle' ? 'clickable' : ''} ${isSidebarDragging ? 'dragging' : ''}`}
|
||||
data-collapsed={isCollapsed}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={handleSidebarClick}
|
||||
onTouchStart={handleSidebarTouchStart}
|
||||
onTouchMove={handleSidebarTouchMove}
|
||||
onTouchEnd={handleSidebarTouchEnd}
|
||||
onTouchCancel={handleSidebarTouchCancel}
|
||||
style={isSidebarDragging ? {
|
||||
transform: sidebarTransform,
|
||||
transition: 'none'
|
||||
} : undefined}
|
||||
>
|
||||
<div className="sidebar-header">
|
||||
<div className="sidebar-header-content">
|
||||
|
||||
@@ -23,6 +23,11 @@ type SwipeTabsProps<T extends string | number> = {
|
||||
renderWindow?: number;
|
||||
scrollToTopOnChange?: boolean;
|
||||
onSwipePastStart?: () => void;
|
||||
// For 1:1 sidebar gesture tracking
|
||||
onSwipePastStartProgress?: (progress: number) => void;
|
||||
onSwipePastStartDragStart?: () => void;
|
||||
onSwipePastStartDragEnd?: (shouldOpen: boolean) => void;
|
||||
sidebarWidth?: number; // Width of sidebar for progress calculation
|
||||
};
|
||||
|
||||
const DRAG_START_DISTANCE = 3;
|
||||
@@ -59,7 +64,11 @@ export function SwipeTabs<T extends string | number>({
|
||||
threshold = DEFAULT_THRESHOLD,
|
||||
renderWindow = DEFAULT_RENDER_WINDOW,
|
||||
scrollToTopOnChange = true,
|
||||
onSwipePastStart
|
||||
onSwipePastStart,
|
||||
onSwipePastStartProgress,
|
||||
onSwipePastStartDragStart,
|
||||
onSwipePastStartDragEnd,
|
||||
sidebarWidth = 280
|
||||
}: SwipeTabsProps<T>) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const trackRef = useRef<HTMLDivElement>(null);
|
||||
@@ -271,6 +280,9 @@ export function SwipeTabs<T extends string | number>({
|
||||
[cancelAnimationForDrag, measureWidth, swipeDisabled, tabs.length]
|
||||
);
|
||||
|
||||
const sidebarDragStartedRef = useRef(false);
|
||||
const sidebarDragProgressRef = useRef(0);
|
||||
|
||||
const updateDrag = useCallback(
|
||||
(x: number, y: number, preventDefault: () => void, pointerType?: string) => {
|
||||
const state = dragRef.current;
|
||||
@@ -305,10 +317,27 @@ export function SwipeTabs<T extends string | number>({
|
||||
const atStart = state.startIndex <= 0;
|
||||
const atEnd = state.startIndex >= tabs.length - 1;
|
||||
// Don't apply resistance at start if onSwipePastStart is defined (sidebar can open)
|
||||
const hasVirtualTabAtStart = !!onSwipePastStart;
|
||||
if ((atStart && dx > 0 && !hasVirtualTabAtStart) || (atEnd && dx < 0)) {
|
||||
const hasVirtualTabAtStart = !!onSwipePastStart || !!onSwipePastStartProgress;
|
||||
|
||||
// Calculate sidebar drag progress when swiping right on first tab
|
||||
const isSidebarSwipe = atStart && dx > 0 && hasVirtualTabAtStart;
|
||||
if (isSidebarSwipe) {
|
||||
// Notify drag start
|
||||
if (!sidebarDragStartedRef.current) {
|
||||
sidebarDragStartedRef.current = true;
|
||||
onSwipePastStartDragStart?.();
|
||||
}
|
||||
// Calculate progress (0 to 1) based on sidebar width
|
||||
const progress = Math.min(1, dx / sidebarWidth);
|
||||
sidebarDragProgressRef.current = progress;
|
||||
onSwipePastStartProgress?.(progress);
|
||||
// Don't move the page - only the sidebar moves
|
||||
offset = 0;
|
||||
} else if ((atStart && dx > 0) || (atEnd && dx < 0)) {
|
||||
// Apply resistance at edges when not doing sidebar swipe
|
||||
offset = dx * EDGE_RESISTANCE;
|
||||
}
|
||||
|
||||
offset = Math.max(Math.min(offset, width), -width);
|
||||
dragOffsetRef.current = offset;
|
||||
scheduleDragUpdate();
|
||||
@@ -317,7 +346,7 @@ export function SwipeTabs<T extends string | number>({
|
||||
preventDefault();
|
||||
}
|
||||
},
|
||||
[onSwipePastStart, scheduleDragUpdate, tabs.length]
|
||||
[onSwipePastStart, onSwipePastStartProgress, onSwipePastStartDragStart, scheduleDragUpdate, sidebarWidth, tabs.length]
|
||||
);
|
||||
|
||||
const finishDrag = useCallback(
|
||||
@@ -334,6 +363,12 @@ export function SwipeTabs<T extends string | number>({
|
||||
isDraggingRef.current = false;
|
||||
setDraggingClass(false);
|
||||
|
||||
// Handle sidebar drag end
|
||||
const wasSidebarDragging = sidebarDragStartedRef.current;
|
||||
const sidebarProgress = sidebarDragProgressRef.current;
|
||||
sidebarDragStartedRef.current = false;
|
||||
sidebarDragProgressRef.current = 0;
|
||||
|
||||
if (!wasDragging) return;
|
||||
|
||||
if (rafRef.current) {
|
||||
@@ -346,12 +381,23 @@ export function SwipeTabs<T extends string | number>({
|
||||
const thresholdPx = width * threshold;
|
||||
let targetIndex = state.startIndex;
|
||||
|
||||
if (!cancelled && Math.abs(dx) > thresholdPx) {
|
||||
// Check if this was a sidebar gesture (using the flag, not dx which is 0 for sidebar swipes)
|
||||
const shouldOpenSidebar = !cancelled && sidebarProgress > 0.3; // 30% threshold
|
||||
|
||||
if (wasSidebarDragging) {
|
||||
// Call the new drag end callback with whether to open
|
||||
onSwipePastStartDragEnd?.(shouldOpenSidebar);
|
||||
|
||||
// Also call legacy callback if sidebar should open
|
||||
if (shouldOpenSidebar && onSwipePastStart) {
|
||||
onSwipePastStart();
|
||||
}
|
||||
} else 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) {
|
||||
// If swiping right on first tab without new callbacks, call onSwipePastStart
|
||||
if (state.startIndex === 0 && dx > 0 && onSwipePastStart && !onSwipePastStartDragEnd) {
|
||||
onSwipePastStart();
|
||||
}
|
||||
}
|
||||
@@ -372,7 +418,7 @@ export function SwipeTabs<T extends string | number>({
|
||||
}
|
||||
}
|
||||
},
|
||||
[activeTab, applyTransform, onTabChange, onSwipePastStart, setDraggingClass, tabs, threshold]
|
||||
[activeTab, applyTransform, onTabChange, onSwipePastStart, onSwipePastStartDragEnd, setDraggingClass, sidebarWidth, tabs, threshold]
|
||||
);
|
||||
|
||||
const handlePointerDown = (event: PointerEvent<HTMLDivElement>) => {
|
||||
|
||||
164
frontend/src/components/SwipeableContent.tsx
Normal file
164
frontend/src/components/SwipeableContent.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import { useCallback, useRef, type ReactNode, type TouchEvent } from 'react';
|
||||
import { useSidebar } from '../contexts/SidebarContext';
|
||||
|
||||
interface SwipeableContentProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
/** Sidebar width for progress calculation (default: 280px) */
|
||||
sidebarWidth?: number;
|
||||
/** Render as main element instead of div */
|
||||
as?: 'main' | 'div';
|
||||
}
|
||||
|
||||
const DRAG_START_DISTANCE = 10;
|
||||
|
||||
// Elements that should block the swipe gesture
|
||||
const shouldIgnoreSwipe = (target: EventTarget | null): boolean => {
|
||||
if (!target) return false;
|
||||
const element = target as Element;
|
||||
if (typeof element.closest !== 'function') return false;
|
||||
|
||||
// Ignore swipe on interactive/scrollable elements
|
||||
if (element.closest(
|
||||
'[data-swipe-ignore="true"], .swipe-tabs, [data-radix-scroll-area-viewport], ' +
|
||||
'[style*="overflow: auto"], [style*="overflow-x: auto"], [style*="overflow: scroll"], ' +
|
||||
'[style*="overflow-x: scroll"], input[type="range"], .slider, .carousel'
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export function SwipeableContent({
|
||||
children,
|
||||
className,
|
||||
sidebarWidth = 280,
|
||||
as: Component = 'main'
|
||||
}: SwipeableContentProps) {
|
||||
const {
|
||||
isMobileOpen,
|
||||
openMobileMenu,
|
||||
setSidebarDragProgress,
|
||||
setIsSidebarDragging
|
||||
} = useSidebar();
|
||||
|
||||
const dragRef = useRef({
|
||||
touchId: null as number | null,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
isActive: false,
|
||||
isDragging: false
|
||||
});
|
||||
|
||||
const handleTouchStart = useCallback((event: TouchEvent<HTMLDivElement>) => {
|
||||
// Don't handle if sidebar is already open
|
||||
if (isMobileOpen) return;
|
||||
if (dragRef.current.isActive) return;
|
||||
|
||||
// Don't handle if touch is on an element that should block swipe
|
||||
if (shouldIgnoreSwipe(event.target)) return;
|
||||
|
||||
const touch = event.touches[0];
|
||||
if (!touch) return;
|
||||
|
||||
dragRef.current = {
|
||||
touchId: touch.identifier,
|
||||
startX: touch.clientX,
|
||||
startY: touch.clientY,
|
||||
isActive: true,
|
||||
isDragging: false
|
||||
};
|
||||
}, [isMobileOpen]);
|
||||
|
||||
const handleTouchMove = useCallback((event: TouchEvent<HTMLDivElement>) => {
|
||||
const state = dragRef.current;
|
||||
if (!state.isActive || state.touchId === null) return;
|
||||
|
||||
const touch = Array.from(event.touches).find(t => t.identifier === state.touchId);
|
||||
if (!touch) return;
|
||||
|
||||
const dx = touch.clientX - state.startX;
|
||||
const dy = touch.clientY - state.startY;
|
||||
|
||||
// Check if we should start dragging
|
||||
if (!state.isDragging) {
|
||||
if (Math.abs(dx) < DRAG_START_DISTANCE && Math.abs(dy) < DRAG_START_DISTANCE) {
|
||||
return;
|
||||
}
|
||||
// Only start horizontal drags going right
|
||||
if (dx <= 0 || Math.abs(dx) <= Math.abs(dy)) {
|
||||
state.isActive = false;
|
||||
return;
|
||||
}
|
||||
state.isDragging = true;
|
||||
setIsSidebarDragging(true);
|
||||
}
|
||||
|
||||
// Calculate progress (0 to 1)
|
||||
const progress = Math.min(1, Math.max(0, dx / sidebarWidth));
|
||||
setSidebarDragProgress(progress);
|
||||
|
||||
// Prevent scrolling while dragging
|
||||
if (event.cancelable) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}, [sidebarWidth, setSidebarDragProgress, setIsSidebarDragging]);
|
||||
|
||||
const handleTouchEnd = useCallback((event: TouchEvent<HTMLDivElement>) => {
|
||||
const state = dragRef.current;
|
||||
if (!state.isActive || state.touchId === null) return;
|
||||
|
||||
const touch = Array.from(event.changedTouches).find(t => t.identifier === state.touchId);
|
||||
if (!touch) return;
|
||||
|
||||
const wasDragging = state.isDragging;
|
||||
const dx = touch.clientX - state.startX;
|
||||
|
||||
// Reset state
|
||||
dragRef.current = {
|
||||
touchId: null,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
isActive: false,
|
||||
isDragging: false
|
||||
};
|
||||
|
||||
if (!wasDragging) return;
|
||||
|
||||
setIsSidebarDragging(false);
|
||||
setSidebarDragProgress(0);
|
||||
|
||||
// Open sidebar if dragged past 30% threshold
|
||||
if (dx > sidebarWidth * 0.3) {
|
||||
openMobileMenu();
|
||||
}
|
||||
}, [sidebarWidth, openMobileMenu, setSidebarDragProgress, setIsSidebarDragging]);
|
||||
|
||||
const handleTouchCancel = useCallback(() => {
|
||||
if (dragRef.current.isDragging) {
|
||||
setIsSidebarDragging(false);
|
||||
setSidebarDragProgress(0);
|
||||
}
|
||||
dragRef.current = {
|
||||
touchId: null,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
isActive: false,
|
||||
isDragging: false
|
||||
};
|
||||
}, [setSidebarDragProgress, setIsSidebarDragging]);
|
||||
|
||||
return (
|
||||
<Component
|
||||
className={className}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
onTouchCancel={handleTouchCancel}
|
||||
style={{ touchAction: 'pan-y' }}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
@@ -19,6 +19,11 @@ interface SidebarContextType {
|
||||
setIsHovered: (isHovered: boolean) => void;
|
||||
showLogo: boolean;
|
||||
setShowLogo: (show: boolean) => void;
|
||||
// Mobile sidebar drag state for 1:1 gesture tracking
|
||||
sidebarDragProgress: number; // 0 = closed, 1 = fully open
|
||||
setSidebarDragProgress: (progress: number) => void;
|
||||
isSidebarDragging: boolean;
|
||||
setIsSidebarDragging: (dragging: boolean) => void;
|
||||
}
|
||||
|
||||
const SidebarContext = createContext<SidebarContextType | undefined>(undefined);
|
||||
@@ -45,6 +50,8 @@ export function SidebarProvider({ children }: { children: ReactNode }) {
|
||||
const [sidebarMode, setSidebarModeState] = useState<SidebarMode>('toggle');
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [showLogo, setShowLogo] = useState(false);
|
||||
const [sidebarDragProgress, setSidebarDragProgress] = useState(0);
|
||||
const [isSidebarDragging, setIsSidebarDragging] = useState(false);
|
||||
|
||||
// Load sidebar mode from backend
|
||||
useEffect(() => {
|
||||
@@ -140,6 +147,10 @@ export function SidebarProvider({ children }: { children: ReactNode }) {
|
||||
setIsHovered: (value: boolean) => setIsHovered(value),
|
||||
showLogo,
|
||||
setShowLogo,
|
||||
sidebarDragProgress,
|
||||
setSidebarDragProgress,
|
||||
isSidebarDragging,
|
||||
setIsSidebarDragging,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from '../contexts/LanguageContext';
|
||||
import { useSidebar } from '../contexts/SidebarContext';
|
||||
import TabsScroller from '../components/TabsScroller';
|
||||
import { SwipeableContent } from '../components/SwipeableContent';
|
||||
import { apiKeysAPI } from '../api/client';
|
||||
import type { ApiKeyItem } from '../api/client';
|
||||
import '../styles/APIKeys.css';
|
||||
@@ -88,7 +89,7 @@ export default function APIKeys() {
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="main-content api-keys-root">
|
||||
<SwipeableContent className="main-content api-keys-root">
|
||||
<div className="page-tabs-container">
|
||||
<TabsScroller className="page-tabs-slider">
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
@@ -183,6 +184,6 @@ export default function APIKeys() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</SwipeableContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useTranslation } from '../contexts/LanguageContext';
|
||||
import { useSidebar } from '../contexts/SidebarContext';
|
||||
@@ -13,7 +13,23 @@ type TabId = 'general' | 'users';
|
||||
export default function AdminPanel({ initialTab = 'general' }: { initialTab?: TabId } = {}) {
|
||||
const { user: currentUser } = useAuth();
|
||||
const { t } = useTranslation();
|
||||
const { toggleMobileMenu, openMobileMenu } = useSidebar();
|
||||
const { toggleMobileMenu, openMobileMenu, setSidebarDragProgress, setIsSidebarDragging } = useSidebar();
|
||||
|
||||
const handleSidebarDragStart = useCallback(() => {
|
||||
setIsSidebarDragging(true);
|
||||
}, [setIsSidebarDragging]);
|
||||
|
||||
const handleSidebarDragProgress = useCallback((progress: number) => {
|
||||
setSidebarDragProgress(progress);
|
||||
}, [setSidebarDragProgress]);
|
||||
|
||||
const handleSidebarDragEnd = useCallback((shouldOpen: boolean) => {
|
||||
setIsSidebarDragging(false);
|
||||
setSidebarDragProgress(0);
|
||||
if (shouldOpen) {
|
||||
openMobileMenu();
|
||||
}
|
||||
}, [openMobileMenu, setIsSidebarDragging, setSidebarDragProgress]);
|
||||
const [activeTab, setActiveTab] = useState<TabId>(initialTab);
|
||||
const tabs: TabId[] = ['general', 'users'];
|
||||
const renderPanel = (tab: TabId) => (tab === 'general' ? <GeneralTab /> : <UsersTab />);
|
||||
@@ -57,7 +73,9 @@ export default function AdminPanel({ initialTab = 'general' }: { initialTab?: Ta
|
||||
onTabChange={setActiveTab}
|
||||
renderPanel={renderPanel}
|
||||
panelClassName="admin-tab-content swipe-panel-content"
|
||||
onSwipePastStart={openMobileMenu}
|
||||
onSwipePastStartDragStart={handleSidebarDragStart}
|
||||
onSwipePastStartProgress={handleSidebarDragProgress}
|
||||
onSwipePastStartDragEnd={handleSidebarDragEnd}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { useTranslation } from '../contexts/LanguageContext';
|
||||
import { useSidebar } from '../contexts/SidebarContext';
|
||||
import TabsScroller from '../components/TabsScroller';
|
||||
import { SwipeableContent } from '../components/SwipeableContent';
|
||||
|
||||
export default function Dashboard() {
|
||||
const { t } = useTranslation();
|
||||
const { toggleMobileMenu } = useSidebar();
|
||||
|
||||
return (
|
||||
<main className="main-content">
|
||||
<SwipeableContent className="main-content">
|
||||
<div className="page-tabs-container">
|
||||
<TabsScroller className="page-tabs-slider">
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
@@ -22,6 +23,6 @@ export default function Dashboard() {
|
||||
<div className="page-content">
|
||||
{/* Dashboard content */}
|
||||
</div>
|
||||
</main>
|
||||
</SwipeableContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTranslation } from '../contexts/LanguageContext';
|
||||
import { useSidebar } from '../contexts/SidebarContext';
|
||||
import TabsScroller from '../components/TabsScroller';
|
||||
import { SwipeableContent } from '../components/SwipeableContent';
|
||||
import '../styles/AdminPanel.css';
|
||||
|
||||
export default function Feature1() {
|
||||
@@ -8,7 +9,7 @@ export default function Feature1() {
|
||||
const { toggleMobileMenu } = useSidebar();
|
||||
|
||||
return (
|
||||
<main className="main-content">
|
||||
<SwipeableContent className="main-content">
|
||||
<div className="page-tabs-container">
|
||||
<TabsScroller className="page-tabs-slider">
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
@@ -30,6 +31,6 @@ export default function Feature1() {
|
||||
<p>{t.feature1.comingSoon}</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</SwipeableContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTranslation } from '../contexts/LanguageContext';
|
||||
import { useSidebar } from '../contexts/SidebarContext';
|
||||
import TabsScroller from '../components/TabsScroller';
|
||||
import { SwipeableContent } from '../components/SwipeableContent';
|
||||
import '../styles/AdminPanel.css';
|
||||
|
||||
export default function Feature2() {
|
||||
@@ -8,7 +9,7 @@ export default function Feature2() {
|
||||
const { toggleMobileMenu } = useSidebar();
|
||||
|
||||
return (
|
||||
<main className="main-content">
|
||||
<SwipeableContent className="main-content">
|
||||
<div className="page-tabs-container">
|
||||
<TabsScroller className="page-tabs-slider">
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
@@ -30,6 +31,6 @@ export default function Feature2() {
|
||||
<p>{t.features.comingSoon}</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</SwipeableContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTranslation } from '../contexts/LanguageContext';
|
||||
import { useSidebar } from '../contexts/SidebarContext';
|
||||
import TabsScroller from '../components/TabsScroller';
|
||||
import { SwipeableContent } from '../components/SwipeableContent';
|
||||
import '../styles/AdminPanel.css';
|
||||
|
||||
export default function Feature3() {
|
||||
@@ -8,7 +9,7 @@ export default function Feature3() {
|
||||
const { toggleMobileMenu } = useSidebar();
|
||||
|
||||
return (
|
||||
<main className="main-content">
|
||||
<SwipeableContent className="main-content">
|
||||
<div className="page-tabs-container">
|
||||
<TabsScroller className="page-tabs-slider">
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
@@ -30,6 +31,6 @@ export default function Feature3() {
|
||||
<p>{t.features.comingSoon}</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</SwipeableContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from '../contexts/LanguageContext';
|
||||
import { useSidebar } from '../contexts/SidebarContext';
|
||||
import TabsScroller from '../components/TabsScroller';
|
||||
import { SwipeableContent } from '../components/SwipeableContent';
|
||||
import { useNotifications } from '../contexts/NotificationsContext';
|
||||
import { notificationsAPI } from '../api/client';
|
||||
import type { NotificationItem } from '../api/client';
|
||||
@@ -94,7 +95,7 @@ export default function Notifications() {
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="main-content notifications-root">
|
||||
<SwipeableContent className="main-content notifications-root">
|
||||
<div className="page-tabs-container">
|
||||
<TabsScroller className="page-tabs-slider">
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
@@ -161,6 +162,6 @@ export default function Notifications() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</SwipeableContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from '../contexts/LanguageContext';
|
||||
import { useSidebar } from '../contexts/SidebarContext';
|
||||
import TabsScroller from '../components/TabsScroller';
|
||||
import { SwipeableContent } from '../components/SwipeableContent';
|
||||
import '../styles/Search.css';
|
||||
|
||||
type SearchResult = {
|
||||
@@ -76,7 +77,7 @@ export default function Search() {
|
||||
: t.searchPage.hint;
|
||||
|
||||
return (
|
||||
<main className="main-content search-root">
|
||||
<SwipeableContent className="main-content search-root">
|
||||
<div className="page-tabs-container">
|
||||
<TabsScroller className="page-tabs-slider">
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
@@ -136,6 +137,6 @@ export default function Search() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</SwipeableContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from '../contexts/LanguageContext';
|
||||
import { useSidebar } from '../contexts/SidebarContext';
|
||||
import TabsScroller from '../components/TabsScroller';
|
||||
import { SwipeableContent } from '../components/SwipeableContent';
|
||||
import { sessionsAPI, twoFactorAPI } from '../api/client';
|
||||
import type { UserSession } from '../api/client';
|
||||
import '../styles/SettingsPage.css';
|
||||
@@ -151,7 +152,7 @@ export default function Settings() {
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="main-content settings-page-root">
|
||||
<SwipeableContent className="main-content settings-page-root">
|
||||
<div className="page-tabs-container">
|
||||
<TabsScroller className="page-tabs-slider">
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
@@ -399,6 +400,6 @@ export default function Settings() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</SwipeableContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
import type { FormEvent } from 'react';
|
||||
import Sidebar from '../components/Sidebar';
|
||||
import TabsScroller from '../components/TabsScroller';
|
||||
import { SwipeableContent } from '../components/SwipeableContent';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useTranslation } from '../contexts/LanguageContext';
|
||||
import { useSidebar } from '../contexts/SidebarContext';
|
||||
@@ -180,7 +181,7 @@ export default function Users() {
|
||||
return (
|
||||
<div className="app-layout">
|
||||
<Sidebar />
|
||||
<main className="main-content users-root">
|
||||
<SwipeableContent className="main-content users-root">
|
||||
<div className="page-tabs-container">
|
||||
<TabsScroller className="page-tabs-slider">
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
@@ -297,7 +298,7 @@ export default function Users() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</SwipeableContent>
|
||||
|
||||
{isModalOpen && (
|
||||
<div className="users-modal-backdrop" onClick={closeModal}>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from '../../contexts/LanguageContext';
|
||||
import { useSidebar } from '../../contexts/SidebarContext';
|
||||
import TabsScroller from '../../components/TabsScroller';
|
||||
import { SwipeableContent } from '../../components/SwipeableContent';
|
||||
import { analyticsAPI } from '../../api/client';
|
||||
import type { AnalyticsOverview } from '../../api/client';
|
||||
import '../../styles/AdminAnalytics.css';
|
||||
@@ -45,7 +46,7 @@ export default function Analytics() {
|
||||
const maxActionCount = useMemo(() => Math.max(1, ...actions.map((a) => a.count)), [actions]);
|
||||
|
||||
return (
|
||||
<main className="main-content admin-analytics-root">
|
||||
<SwipeableContent className="main-content admin-analytics-root">
|
||||
<div className="page-tabs-container">
|
||||
<TabsScroller className="page-tabs-slider">
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
@@ -149,6 +150,6 @@ export default function Analytics() {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</SwipeableContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from '../../contexts/LanguageContext';
|
||||
import { useSidebar } from '../../contexts/SidebarContext';
|
||||
import TabsScroller from '../../components/TabsScroller';
|
||||
import { SwipeableContent } from '../../components/SwipeableContent';
|
||||
import { auditAPI } from '../../api/client';
|
||||
import type { AuditLogItem } from '../../api/client';
|
||||
import '../../styles/AdminAudit.css';
|
||||
@@ -58,7 +59,7 @@ export default function AuditLogs() {
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="main-content admin-audit-root">
|
||||
<SwipeableContent className="main-content admin-audit-root">
|
||||
<div className="page-tabs-container">
|
||||
<TabsScroller className="page-tabs-slider">
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
@@ -161,6 +162,6 @@ export default function AuditLogs() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</SwipeableContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,23 @@ const findTouchById = (touches: TouchList, id: number) => {
|
||||
export default function Features() {
|
||||
const { user: currentUser } = useAuth();
|
||||
const { t } = useTranslation();
|
||||
const { toggleMobileMenu, openMobileMenu } = useSidebar();
|
||||
const { toggleMobileMenu, openMobileMenu, setSidebarDragProgress, setIsSidebarDragging } = useSidebar();
|
||||
|
||||
const handleSidebarDragStart = useCallback(() => {
|
||||
setIsSidebarDragging(true);
|
||||
}, [setIsSidebarDragging]);
|
||||
|
||||
const handleSidebarDragProgress = useCallback((progress: number) => {
|
||||
setSidebarDragProgress(progress);
|
||||
}, [setSidebarDragProgress]);
|
||||
|
||||
const handleSidebarDragEnd = useCallback((shouldOpen: boolean) => {
|
||||
setIsSidebarDragging(false);
|
||||
setSidebarDragProgress(0);
|
||||
if (shouldOpen) {
|
||||
openMobileMenu();
|
||||
}
|
||||
}, [openMobileMenu, setIsSidebarDragging, setSidebarDragProgress]);
|
||||
const { moduleStates, moduleOrder, setModuleEnabled, setModulePosition, setModuleOrder, saveModulesToBackend, saveModuleOrder, hasInitialized, isLoading } = useModules();
|
||||
const [activeTab, setActiveTab] = useState<TabId>('config');
|
||||
const hasUserMadeChanges = useRef(false);
|
||||
@@ -806,7 +822,9 @@ export default function Features() {
|
||||
onTabChange={handleTabChange}
|
||||
renderPanel={renderTabContent}
|
||||
panelClassName="admin-tab-content swipe-panel-content"
|
||||
onSwipePastStart={openMobileMenu}
|
||||
onSwipePastStartDragStart={handleSidebarDragStart}
|
||||
onSwipePastStartProgress={handleSidebarDragProgress}
|
||||
onSwipePastStartDragEnd={handleSidebarDragEnd}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useTranslation } from '../../contexts/LanguageContext';
|
||||
import { useSidebar } from '../../contexts/SidebarContext';
|
||||
import Sidebar from '../../components/Sidebar';
|
||||
import TabsScroller from '../../components/TabsScroller';
|
||||
import { SwipeableContent } from '../../components/SwipeableContent';
|
||||
import { settingsAPI } from '../../api/client';
|
||||
import '../../styles/Settings.css';
|
||||
|
||||
@@ -59,7 +60,7 @@ export default function Settings() {
|
||||
return (
|
||||
<div className="app-layout">
|
||||
<Sidebar />
|
||||
<main className="main-content settings-root">
|
||||
<SwipeableContent className="main-content settings-root">
|
||||
<div className="page-tabs-container">
|
||||
<TabsScroller className="page-tabs-slider">
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
@@ -110,7 +111,7 @@ export default function Settings() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</SwipeableContent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useAuth } from '../../contexts/AuthContext';
|
||||
import { useTranslation } from '../../contexts/LanguageContext';
|
||||
import { useSidebar } from '../../contexts/SidebarContext';
|
||||
import TabsScroller from '../../components/TabsScroller';
|
||||
import { SwipeableContent } from '../../components/SwipeableContent';
|
||||
import '../../styles/AdminPanel.css';
|
||||
|
||||
export default function Sources() {
|
||||
@@ -14,7 +15,7 @@ export default function Sources() {
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="main-content admin-panel-root">
|
||||
<SwipeableContent className="main-content admin-panel-root">
|
||||
<div className="admin-tabs-container">
|
||||
<TabsScroller className="admin-tabs-slider">
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
@@ -35,6 +36,6 @@ export default function Sources() {
|
||||
<p>{t.sourcesPage.comingSoon}</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</SwipeableContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { useTheme, COLOR_PALETTES } from '../../contexts/ThemeContext';
|
||||
import type { AccentColor, BorderRadius, SidebarStyle, Density, FontFamily, ColorPalette, TabBarPosition } from '../../contexts/ThemeContext';
|
||||
import { useTranslation } from '../../contexts/LanguageContext';
|
||||
@@ -45,8 +45,24 @@ export default function ThemeSettings() {
|
||||
} = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAuth();
|
||||
const { toggleMobileMenu, openMobileMenu, sidebarMode, setSidebarMode } = useSidebar();
|
||||
const { toggleMobileMenu, openMobileMenu, sidebarMode, setSidebarMode, setSidebarDragProgress, setIsSidebarDragging } = useSidebar();
|
||||
const isAdmin = user?.is_superuser || false;
|
||||
|
||||
const handleSidebarDragStart = useCallback(() => {
|
||||
setIsSidebarDragging(true);
|
||||
}, [setIsSidebarDragging]);
|
||||
|
||||
const handleSidebarDragProgress = useCallback((progress: number) => {
|
||||
setSidebarDragProgress(progress);
|
||||
}, [setSidebarDragProgress]);
|
||||
|
||||
const handleSidebarDragEnd = useCallback((shouldOpen: boolean) => {
|
||||
setIsSidebarDragging(false);
|
||||
setSidebarDragProgress(0);
|
||||
if (shouldOpen) {
|
||||
openMobileMenu();
|
||||
}
|
||||
}, [openMobileMenu, setIsSidebarDragging, setSidebarDragProgress]);
|
||||
const tabs: ThemeTab[] = isAdmin ? ['colors', 'appearance', 'preview', 'advanced'] : ['colors', 'appearance', 'preview'];
|
||||
const [tooltip, setTooltip] = useState<{ text: string; left: number; visible: boolean }>({
|
||||
text: '',
|
||||
@@ -820,7 +836,9 @@ export default function ThemeSettings() {
|
||||
onTabChange={setActiveTab}
|
||||
renderPanel={renderPanel}
|
||||
panelClassName="admin-tab-content swipe-panel-content"
|
||||
onSwipePastStart={openMobileMenu}
|
||||
onSwipePastStartDragStart={handleSidebarDragStart}
|
||||
onSwipePastStartProgress={handleSidebarDragProgress}
|
||||
onSwipePastStartDragEnd={handleSidebarDragEnd}
|
||||
/>
|
||||
|
||||
{/* Color Picker Popup */}
|
||||
|
||||
@@ -887,22 +887,6 @@ label,
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
/* Hide title section when bar is at bottom (only on pages with tabs) */
|
||||
[data-tab-position='bottom'] .page-tabs-slider[data-has-tabs='true'] .page-title-section,
|
||||
[data-tab-position='bottom'] .page-tabs-slider[data-has-tabs='true'] .admin-title-section,
|
||||
[data-tab-position='bottom'] .admin-tabs-slider[data-has-tabs='true'] .page-title-section,
|
||||
[data-tab-position='bottom'] .admin-tabs-slider[data-has-tabs='true'] .admin-title-section {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide divider when bar is at bottom */
|
||||
[data-tab-position='bottom'] .page-tabs-slider .page-tabs-divider,
|
||||
[data-tab-position='bottom'] .page-tabs-slider .admin-tabs-divider,
|
||||
[data-tab-position='bottom'] .admin-tabs-slider .page-tabs-divider,
|
||||
[data-tab-position='bottom'] .admin-tabs-slider .admin-tabs-divider {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Add bottom padding and remove top padding when bar is at bottom */
|
||||
[data-tab-position='bottom'] .main-content {
|
||||
padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px));
|
||||
@@ -926,7 +910,13 @@ label,
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* Keep proper spacing for mobile menu button - inherits padding-left: 72px from mobile rules */
|
||||
/* Hide divider on mobile when bar is at bottom */
|
||||
[data-tab-position='bottom'] .page-tabs-slider .page-tabs-divider,
|
||||
[data-tab-position='bottom'] .page-tabs-slider .admin-tabs-divider,
|
||||
[data-tab-position='bottom'] .admin-tabs-slider .page-tabs-divider,
|
||||
[data-tab-position='bottom'] .admin-tabs-slider .admin-tabs-divider {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive position - top on desktop, bottom on mobile */
|
||||
|
||||
@@ -678,6 +678,16 @@ button.nav-item {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
/* During drag, disable transitions for 1:1 gesture tracking */
|
||||
.sidebar.dragging {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.sidebar-overlay.dragging {
|
||||
transition: none !important;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Reset ALL collapsed styles on mobile - sidebar should always be fully expanded when open */
|
||||
.sidebar.collapsed {
|
||||
width: 280px;
|
||||
|
||||
Reference in New Issue
Block a user