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:
2025-12-23 21:48:19 +01:00
parent 9e3556322f
commit 500d038ed0
22 changed files with 493 additions and 63 deletions

View File

@@ -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 { NavLink, useNavigate } from 'react-router-dom';
import { useTranslation } from '../contexts/LanguageContext'; import { useTranslation } from '../contexts/LanguageContext';
import { useSidebar } from '../contexts/SidebarContext'; import { useSidebar } from '../contexts/SidebarContext';
@@ -12,6 +12,9 @@ import { appModules } from '../modules';
import UserMenu from './UserMenu'; import UserMenu from './UserMenu';
import '../styles/Sidebar.css'; import '../styles/Sidebar.css';
const SIDEBAR_WIDTH = 280;
const DRAG_START_DISTANCE = 3;
export default function Sidebar() { export default function Sidebar() {
const { t, language, setLanguage } = useTranslation(); const { t, language, setLanguage } = useTranslation();
const { config } = useSiteConfig(); const { config } = useSiteConfig();
@@ -24,7 +27,11 @@ export default function Sidebar() {
closeMobileMenu, closeMobileMenu,
isHovered, isHovered,
setIsHovered, setIsHovered,
showLogo: showLogoContext showLogo: showLogoContext,
sidebarDragProgress,
setSidebarDragProgress,
isSidebarDragging,
setIsSidebarDragging
} = useSidebar(); } = useSidebar();
const { viewMode, toggleViewMode, isUserModeEnabled } = useViewMode(); const { viewMode, toggleViewMode, isUserModeEnabled } = useViewMode();
const { user } = useAuth(); const { user } = useAuth();
@@ -167,10 +174,137 @@ export default function Sidebar() {
setTooltip((prev) => prev.visible ? { ...prev, text } : prev); 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 ( return (
<> <>
{/* Mobile overlay */} {/* 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 */} {/* Sidebar Tooltip */}
{tooltip.visible && ( {tooltip.visible && (
@@ -186,11 +320,19 @@ export default function Sidebar() {
)} )}
<aside <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} data-collapsed={isCollapsed}
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
onClick={handleSidebarClick} 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">
<div className="sidebar-header-content"> <div className="sidebar-header-content">

View File

@@ -23,6 +23,11 @@ type SwipeTabsProps<T extends string | number> = {
renderWindow?: number; renderWindow?: number;
scrollToTopOnChange?: boolean; scrollToTopOnChange?: boolean;
onSwipePastStart?: () => void; 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; const DRAG_START_DISTANCE = 3;
@@ -59,7 +64,11 @@ export function SwipeTabs<T extends string | number>({
threshold = DEFAULT_THRESHOLD, threshold = DEFAULT_THRESHOLD,
renderWindow = DEFAULT_RENDER_WINDOW, renderWindow = DEFAULT_RENDER_WINDOW,
scrollToTopOnChange = true, scrollToTopOnChange = true,
onSwipePastStart onSwipePastStart,
onSwipePastStartProgress,
onSwipePastStartDragStart,
onSwipePastStartDragEnd,
sidebarWidth = 280
}: SwipeTabsProps<T>) { }: SwipeTabsProps<T>) {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const trackRef = useRef<HTMLDivElement>(null); const trackRef = useRef<HTMLDivElement>(null);
@@ -271,6 +280,9 @@ export function SwipeTabs<T extends string | number>({
[cancelAnimationForDrag, measureWidth, swipeDisabled, tabs.length] [cancelAnimationForDrag, measureWidth, swipeDisabled, tabs.length]
); );
const sidebarDragStartedRef = useRef(false);
const sidebarDragProgressRef = useRef(0);
const updateDrag = useCallback( const updateDrag = useCallback(
(x: number, y: number, preventDefault: () => void, pointerType?: string) => { (x: number, y: number, preventDefault: () => void, pointerType?: string) => {
const state = dragRef.current; const state = dragRef.current;
@@ -305,10 +317,27 @@ export function SwipeTabs<T extends string | number>({
const atStart = state.startIndex <= 0; const atStart = state.startIndex <= 0;
const atEnd = state.startIndex >= tabs.length - 1; const atEnd = state.startIndex >= tabs.length - 1;
// Don't apply resistance at start if onSwipePastStart is defined (sidebar can open) // Don't apply resistance at start if onSwipePastStart is defined (sidebar can open)
const hasVirtualTabAtStart = !!onSwipePastStart; const hasVirtualTabAtStart = !!onSwipePastStart || !!onSwipePastStartProgress;
if ((atStart && dx > 0 && !hasVirtualTabAtStart) || (atEnd && dx < 0)) {
// 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 = dx * EDGE_RESISTANCE;
} }
offset = Math.max(Math.min(offset, width), -width); offset = Math.max(Math.min(offset, width), -width);
dragOffsetRef.current = offset; dragOffsetRef.current = offset;
scheduleDragUpdate(); scheduleDragUpdate();
@@ -317,7 +346,7 @@ export function SwipeTabs<T extends string | number>({
preventDefault(); preventDefault();
} }
}, },
[onSwipePastStart, scheduleDragUpdate, tabs.length] [onSwipePastStart, onSwipePastStartProgress, onSwipePastStartDragStart, scheduleDragUpdate, sidebarWidth, tabs.length]
); );
const finishDrag = useCallback( const finishDrag = useCallback(
@@ -334,6 +363,12 @@ export function SwipeTabs<T extends string | number>({
isDraggingRef.current = false; isDraggingRef.current = false;
setDraggingClass(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 (!wasDragging) return;
if (rafRef.current) { if (rafRef.current) {
@@ -346,12 +381,23 @@ export function SwipeTabs<T extends string | number>({
const thresholdPx = width * threshold; const thresholdPx = width * threshold;
let targetIndex = state.startIndex; 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; const direction = dx < 0 ? 1 : -1;
targetIndex = Math.min(Math.max(state.startIndex + direction, 0), tabs.length - 1); targetIndex = Math.min(Math.max(state.startIndex + direction, 0), tabs.length - 1);
// If swiping right on first tab, call onSwipePastStart // If swiping right on first tab without new callbacks, call onSwipePastStart
if (state.startIndex === 0 && dx > 0 && onSwipePastStart) { if (state.startIndex === 0 && dx > 0 && onSwipePastStart && !onSwipePastStartDragEnd) {
onSwipePastStart(); 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>) => { const handlePointerDown = (event: PointerEvent<HTMLDivElement>) => {

View 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>
);
}

View File

@@ -19,6 +19,11 @@ interface SidebarContextType {
setIsHovered: (isHovered: boolean) => void; setIsHovered: (isHovered: boolean) => void;
showLogo: boolean; showLogo: boolean;
setShowLogo: (show: boolean) => void; 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); const SidebarContext = createContext<SidebarContextType | undefined>(undefined);
@@ -45,6 +50,8 @@ export function SidebarProvider({ children }: { children: ReactNode }) {
const [sidebarMode, setSidebarModeState] = useState<SidebarMode>('toggle'); const [sidebarMode, setSidebarModeState] = useState<SidebarMode>('toggle');
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const [showLogo, setShowLogo] = useState(false); const [showLogo, setShowLogo] = useState(false);
const [sidebarDragProgress, setSidebarDragProgress] = useState(0);
const [isSidebarDragging, setIsSidebarDragging] = useState(false);
// Load sidebar mode from backend // Load sidebar mode from backend
useEffect(() => { useEffect(() => {
@@ -140,6 +147,10 @@ export function SidebarProvider({ children }: { children: ReactNode }) {
setIsHovered: (value: boolean) => setIsHovered(value), setIsHovered: (value: boolean) => setIsHovered(value),
showLogo, showLogo,
setShowLogo, setShowLogo,
sidebarDragProgress,
setSidebarDragProgress,
isSidebarDragging,
setIsSidebarDragging,
}} }}
> >
{children} {children}

View File

@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import { useTranslation } from '../contexts/LanguageContext'; import { useTranslation } from '../contexts/LanguageContext';
import { useSidebar } from '../contexts/SidebarContext'; import { useSidebar } from '../contexts/SidebarContext';
import TabsScroller from '../components/TabsScroller'; import TabsScroller from '../components/TabsScroller';
import { SwipeableContent } from '../components/SwipeableContent';
import { apiKeysAPI } from '../api/client'; import { apiKeysAPI } from '../api/client';
import type { ApiKeyItem } from '../api/client'; import type { ApiKeyItem } from '../api/client';
import '../styles/APIKeys.css'; import '../styles/APIKeys.css';
@@ -88,7 +89,7 @@ export default function APIKeys() {
}; };
return ( return (
<main className="main-content api-keys-root"> <SwipeableContent className="main-content api-keys-root">
<div className="page-tabs-container"> <div className="page-tabs-container">
<TabsScroller className="page-tabs-slider"> <TabsScroller className="page-tabs-slider">
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}> <button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
@@ -183,6 +184,6 @@ export default function APIKeys() {
)} )}
</div> </div>
</div> </div>
</main> </SwipeableContent>
); );
} }

View File

@@ -1,4 +1,4 @@
import { useState } from 'react'; import { useState, useCallback } from 'react';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import { useTranslation } from '../contexts/LanguageContext'; import { useTranslation } from '../contexts/LanguageContext';
import { useSidebar } from '../contexts/SidebarContext'; import { useSidebar } from '../contexts/SidebarContext';
@@ -13,7 +13,23 @@ type TabId = 'general' | 'users';
export default function AdminPanel({ initialTab = 'general' }: { initialTab?: TabId } = {}) { export default function AdminPanel({ initialTab = 'general' }: { initialTab?: TabId } = {}) {
const { user: currentUser } = useAuth(); const { user: currentUser } = useAuth();
const { t } = useTranslation(); 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 [activeTab, setActiveTab] = useState<TabId>(initialTab);
const tabs: TabId[] = ['general', 'users']; const tabs: TabId[] = ['general', 'users'];
const renderPanel = (tab: TabId) => (tab === 'general' ? <GeneralTab /> : <UsersTab />); const renderPanel = (tab: TabId) => (tab === 'general' ? <GeneralTab /> : <UsersTab />);
@@ -57,7 +73,9 @@ export default function AdminPanel({ initialTab = 'general' }: { initialTab?: Ta
onTabChange={setActiveTab} onTabChange={setActiveTab}
renderPanel={renderPanel} renderPanel={renderPanel}
panelClassName="admin-tab-content swipe-panel-content" panelClassName="admin-tab-content swipe-panel-content"
onSwipePastStart={openMobileMenu} onSwipePastStartDragStart={handleSidebarDragStart}
onSwipePastStartProgress={handleSidebarDragProgress}
onSwipePastStartDragEnd={handleSidebarDragEnd}
/> />
</main> </main>
); );

View File

@@ -1,13 +1,14 @@
import { useTranslation } from '../contexts/LanguageContext'; import { useTranslation } from '../contexts/LanguageContext';
import { useSidebar } from '../contexts/SidebarContext'; import { useSidebar } from '../contexts/SidebarContext';
import TabsScroller from '../components/TabsScroller'; import TabsScroller from '../components/TabsScroller';
import { SwipeableContent } from '../components/SwipeableContent';
export default function Dashboard() { export default function Dashboard() {
const { t } = useTranslation(); const { t } = useTranslation();
const { toggleMobileMenu } = useSidebar(); const { toggleMobileMenu } = useSidebar();
return ( return (
<main className="main-content"> <SwipeableContent className="main-content">
<div className="page-tabs-container"> <div className="page-tabs-container">
<TabsScroller className="page-tabs-slider"> <TabsScroller className="page-tabs-slider">
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}> <button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
@@ -22,6 +23,6 @@ export default function Dashboard() {
<div className="page-content"> <div className="page-content">
{/* Dashboard content */} {/* Dashboard content */}
</div> </div>
</main> </SwipeableContent>
); );
} }

View File

@@ -1,6 +1,7 @@
import { useTranslation } from '../contexts/LanguageContext'; import { useTranslation } from '../contexts/LanguageContext';
import { useSidebar } from '../contexts/SidebarContext'; import { useSidebar } from '../contexts/SidebarContext';
import TabsScroller from '../components/TabsScroller'; import TabsScroller from '../components/TabsScroller';
import { SwipeableContent } from '../components/SwipeableContent';
import '../styles/AdminPanel.css'; import '../styles/AdminPanel.css';
export default function Feature1() { export default function Feature1() {
@@ -8,7 +9,7 @@ export default function Feature1() {
const { toggleMobileMenu } = useSidebar(); const { toggleMobileMenu } = useSidebar();
return ( return (
<main className="main-content"> <SwipeableContent className="main-content">
<div className="page-tabs-container"> <div className="page-tabs-container">
<TabsScroller className="page-tabs-slider"> <TabsScroller className="page-tabs-slider">
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}> <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> <p>{t.feature1.comingSoon}</p>
</div> </div>
</div> </div>
</main> </SwipeableContent>
); );
} }

View File

@@ -1,6 +1,7 @@
import { useTranslation } from '../contexts/LanguageContext'; import { useTranslation } from '../contexts/LanguageContext';
import { useSidebar } from '../contexts/SidebarContext'; import { useSidebar } from '../contexts/SidebarContext';
import TabsScroller from '../components/TabsScroller'; import TabsScroller from '../components/TabsScroller';
import { SwipeableContent } from '../components/SwipeableContent';
import '../styles/AdminPanel.css'; import '../styles/AdminPanel.css';
export default function Feature2() { export default function Feature2() {
@@ -8,7 +9,7 @@ export default function Feature2() {
const { toggleMobileMenu } = useSidebar(); const { toggleMobileMenu } = useSidebar();
return ( return (
<main className="main-content"> <SwipeableContent className="main-content">
<div className="page-tabs-container"> <div className="page-tabs-container">
<TabsScroller className="page-tabs-slider"> <TabsScroller className="page-tabs-slider">
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}> <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> <p>{t.features.comingSoon}</p>
</div> </div>
</div> </div>
</main> </SwipeableContent>
); );
} }

View File

@@ -1,6 +1,7 @@
import { useTranslation } from '../contexts/LanguageContext'; import { useTranslation } from '../contexts/LanguageContext';
import { useSidebar } from '../contexts/SidebarContext'; import { useSidebar } from '../contexts/SidebarContext';
import TabsScroller from '../components/TabsScroller'; import TabsScroller from '../components/TabsScroller';
import { SwipeableContent } from '../components/SwipeableContent';
import '../styles/AdminPanel.css'; import '../styles/AdminPanel.css';
export default function Feature3() { export default function Feature3() {
@@ -8,7 +9,7 @@ export default function Feature3() {
const { toggleMobileMenu } = useSidebar(); const { toggleMobileMenu } = useSidebar();
return ( return (
<main className="main-content"> <SwipeableContent className="main-content">
<div className="page-tabs-container"> <div className="page-tabs-container">
<TabsScroller className="page-tabs-slider"> <TabsScroller className="page-tabs-slider">
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}> <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> <p>{t.features.comingSoon}</p>
</div> </div>
</div> </div>
</main> </SwipeableContent>
); );
} }

View File

@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import { useTranslation } from '../contexts/LanguageContext'; import { useTranslation } from '../contexts/LanguageContext';
import { useSidebar } from '../contexts/SidebarContext'; import { useSidebar } from '../contexts/SidebarContext';
import TabsScroller from '../components/TabsScroller'; import TabsScroller from '../components/TabsScroller';
import { SwipeableContent } from '../components/SwipeableContent';
import { useNotifications } from '../contexts/NotificationsContext'; import { useNotifications } from '../contexts/NotificationsContext';
import { notificationsAPI } from '../api/client'; import { notificationsAPI } from '../api/client';
import type { NotificationItem } from '../api/client'; import type { NotificationItem } from '../api/client';
@@ -94,7 +95,7 @@ export default function Notifications() {
}; };
return ( return (
<main className="main-content notifications-root"> <SwipeableContent className="main-content notifications-root">
<div className="page-tabs-container"> <div className="page-tabs-container">
<TabsScroller className="page-tabs-slider"> <TabsScroller className="page-tabs-slider">
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}> <button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
@@ -161,6 +162,6 @@ export default function Notifications() {
</div> </div>
)} )}
</div> </div>
</main> </SwipeableContent>
); );
} }

View File

@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from '../contexts/LanguageContext'; import { useTranslation } from '../contexts/LanguageContext';
import { useSidebar } from '../contexts/SidebarContext'; import { useSidebar } from '../contexts/SidebarContext';
import TabsScroller from '../components/TabsScroller'; import TabsScroller from '../components/TabsScroller';
import { SwipeableContent } from '../components/SwipeableContent';
import '../styles/Search.css'; import '../styles/Search.css';
type SearchResult = { type SearchResult = {
@@ -76,7 +77,7 @@ export default function Search() {
: t.searchPage.hint; : t.searchPage.hint;
return ( return (
<main className="main-content search-root"> <SwipeableContent className="main-content search-root">
<div className="page-tabs-container"> <div className="page-tabs-container">
<TabsScroller className="page-tabs-slider"> <TabsScroller className="page-tabs-slider">
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}> <button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
@@ -136,6 +137,6 @@ export default function Search() {
</div> </div>
</div> </div>
</div> </div>
</main> </SwipeableContent>
); );
} }

View File

@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import { useTranslation } from '../contexts/LanguageContext'; import { useTranslation } from '../contexts/LanguageContext';
import { useSidebar } from '../contexts/SidebarContext'; import { useSidebar } from '../contexts/SidebarContext';
import TabsScroller from '../components/TabsScroller'; import TabsScroller from '../components/TabsScroller';
import { SwipeableContent } from '../components/SwipeableContent';
import { sessionsAPI, twoFactorAPI } from '../api/client'; import { sessionsAPI, twoFactorAPI } from '../api/client';
import type { UserSession } from '../api/client'; import type { UserSession } from '../api/client';
import '../styles/SettingsPage.css'; import '../styles/SettingsPage.css';
@@ -151,7 +152,7 @@ export default function Settings() {
}; };
return ( return (
<main className="main-content settings-page-root"> <SwipeableContent className="main-content settings-page-root">
<div className="page-tabs-container"> <div className="page-tabs-container">
<TabsScroller className="page-tabs-slider"> <TabsScroller className="page-tabs-slider">
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}> <button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
@@ -399,6 +400,6 @@ export default function Settings() {
)} )}
</div> </div>
</div> </div>
</main> </SwipeableContent>
); );
} }

View File

@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
import type { FormEvent } from 'react'; import type { FormEvent } from 'react';
import Sidebar from '../components/Sidebar'; import Sidebar from '../components/Sidebar';
import TabsScroller from '../components/TabsScroller'; import TabsScroller from '../components/TabsScroller';
import { SwipeableContent } from '../components/SwipeableContent';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import { useTranslation } from '../contexts/LanguageContext'; import { useTranslation } from '../contexts/LanguageContext';
import { useSidebar } from '../contexts/SidebarContext'; import { useSidebar } from '../contexts/SidebarContext';
@@ -180,7 +181,7 @@ export default function Users() {
return ( return (
<div className="app-layout"> <div className="app-layout">
<Sidebar /> <Sidebar />
<main className="main-content users-root"> <SwipeableContent className="main-content users-root">
<div className="page-tabs-container"> <div className="page-tabs-container">
<TabsScroller className="page-tabs-slider"> <TabsScroller className="page-tabs-slider">
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}> <button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
@@ -297,7 +298,7 @@ export default function Users() {
</div> </div>
)} )}
</div> </div>
</main> </SwipeableContent>
{isModalOpen && ( {isModalOpen && (
<div className="users-modal-backdrop" onClick={closeModal}> <div className="users-modal-backdrop" onClick={closeModal}>

View File

@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from '../../contexts/LanguageContext'; import { useTranslation } from '../../contexts/LanguageContext';
import { useSidebar } from '../../contexts/SidebarContext'; import { useSidebar } from '../../contexts/SidebarContext';
import TabsScroller from '../../components/TabsScroller'; import TabsScroller from '../../components/TabsScroller';
import { SwipeableContent } from '../../components/SwipeableContent';
import { analyticsAPI } from '../../api/client'; import { analyticsAPI } from '../../api/client';
import type { AnalyticsOverview } from '../../api/client'; import type { AnalyticsOverview } from '../../api/client';
import '../../styles/AdminAnalytics.css'; import '../../styles/AdminAnalytics.css';
@@ -45,7 +46,7 @@ export default function Analytics() {
const maxActionCount = useMemo(() => Math.max(1, ...actions.map((a) => a.count)), [actions]); const maxActionCount = useMemo(() => Math.max(1, ...actions.map((a) => a.count)), [actions]);
return ( return (
<main className="main-content admin-analytics-root"> <SwipeableContent className="main-content admin-analytics-root">
<div className="page-tabs-container"> <div className="page-tabs-container">
<TabsScroller className="page-tabs-slider"> <TabsScroller className="page-tabs-slider">
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}> <button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
@@ -149,6 +150,6 @@ export default function Analytics() {
</> </>
)} )}
</div> </div>
</main> </SwipeableContent>
); );
} }

View File

@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from '../../contexts/LanguageContext'; import { useTranslation } from '../../contexts/LanguageContext';
import { useSidebar } from '../../contexts/SidebarContext'; import { useSidebar } from '../../contexts/SidebarContext';
import TabsScroller from '../../components/TabsScroller'; import TabsScroller from '../../components/TabsScroller';
import { SwipeableContent } from '../../components/SwipeableContent';
import { auditAPI } from '../../api/client'; import { auditAPI } from '../../api/client';
import type { AuditLogItem } from '../../api/client'; import type { AuditLogItem } from '../../api/client';
import '../../styles/AdminAudit.css'; import '../../styles/AdminAudit.css';
@@ -58,7 +59,7 @@ export default function AuditLogs() {
}; };
return ( return (
<main className="main-content admin-audit-root"> <SwipeableContent className="main-content admin-audit-root">
<div className="page-tabs-container"> <div className="page-tabs-container">
<TabsScroller className="page-tabs-slider"> <TabsScroller className="page-tabs-slider">
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}> <button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
@@ -161,6 +162,6 @@ export default function AuditLogs() {
</button> </button>
</div> </div>
</div> </div>
</main> </SwipeableContent>
); );
} }

View File

@@ -25,7 +25,23 @@ const findTouchById = (touches: TouchList, id: number) => {
export default function Features() { export default function Features() {
const { user: currentUser } = useAuth(); const { user: currentUser } = useAuth();
const { t } = useTranslation(); 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 { moduleStates, moduleOrder, setModuleEnabled, setModulePosition, setModuleOrder, saveModulesToBackend, saveModuleOrder, hasInitialized, isLoading } = useModules();
const [activeTab, setActiveTab] = useState<TabId>('config'); const [activeTab, setActiveTab] = useState<TabId>('config');
const hasUserMadeChanges = useRef(false); const hasUserMadeChanges = useRef(false);
@@ -806,7 +822,9 @@ export default function Features() {
onTabChange={handleTabChange} onTabChange={handleTabChange}
renderPanel={renderTabContent} renderPanel={renderTabContent}
panelClassName="admin-tab-content swipe-panel-content" panelClassName="admin-tab-content swipe-panel-content"
onSwipePastStart={openMobileMenu} onSwipePastStartDragStart={handleSidebarDragStart}
onSwipePastStartProgress={handleSidebarDragProgress}
onSwipePastStartDragEnd={handleSidebarDragEnd}
/> />
</main> </main>
); );

View File

@@ -3,6 +3,7 @@ import { useTranslation } from '../../contexts/LanguageContext';
import { useSidebar } from '../../contexts/SidebarContext'; import { useSidebar } from '../../contexts/SidebarContext';
import Sidebar from '../../components/Sidebar'; import Sidebar from '../../components/Sidebar';
import TabsScroller from '../../components/TabsScroller'; import TabsScroller from '../../components/TabsScroller';
import { SwipeableContent } from '../../components/SwipeableContent';
import { settingsAPI } from '../../api/client'; import { settingsAPI } from '../../api/client';
import '../../styles/Settings.css'; import '../../styles/Settings.css';
@@ -59,7 +60,7 @@ export default function Settings() {
return ( return (
<div className="app-layout"> <div className="app-layout">
<Sidebar /> <Sidebar />
<main className="main-content settings-root"> <SwipeableContent className="main-content settings-root">
<div className="page-tabs-container"> <div className="page-tabs-container">
<TabsScroller className="page-tabs-slider"> <TabsScroller className="page-tabs-slider">
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}> <button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
@@ -110,7 +111,7 @@ export default function Settings() {
</div> </div>
</div> </div>
</div> </div>
</main> </SwipeableContent>
</div> </div>
); );
} }

View File

@@ -2,6 +2,7 @@ import { useAuth } from '../../contexts/AuthContext';
import { useTranslation } from '../../contexts/LanguageContext'; import { useTranslation } from '../../contexts/LanguageContext';
import { useSidebar } from '../../contexts/SidebarContext'; import { useSidebar } from '../../contexts/SidebarContext';
import TabsScroller from '../../components/TabsScroller'; import TabsScroller from '../../components/TabsScroller';
import { SwipeableContent } from '../../components/SwipeableContent';
import '../../styles/AdminPanel.css'; import '../../styles/AdminPanel.css';
export default function Sources() { export default function Sources() {
@@ -14,7 +15,7 @@ export default function Sources() {
} }
return ( return (
<main className="main-content admin-panel-root"> <SwipeableContent className="main-content admin-panel-root">
<div className="admin-tabs-container"> <div className="admin-tabs-container">
<TabsScroller className="admin-tabs-slider"> <TabsScroller className="admin-tabs-slider">
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}> <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> <p>{t.sourcesPage.comingSoon}</p>
</div> </div>
</div> </div>
</main> </SwipeableContent>
); );
} }

View File

@@ -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 { useTheme, COLOR_PALETTES } from '../../contexts/ThemeContext';
import type { AccentColor, BorderRadius, SidebarStyle, Density, FontFamily, ColorPalette, TabBarPosition } from '../../contexts/ThemeContext'; import type { AccentColor, BorderRadius, SidebarStyle, Density, FontFamily, ColorPalette, TabBarPosition } from '../../contexts/ThemeContext';
import { useTranslation } from '../../contexts/LanguageContext'; import { useTranslation } from '../../contexts/LanguageContext';
@@ -45,8 +45,24 @@ export default function ThemeSettings() {
} = useTheme(); } = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
const { user } = useAuth(); const { user } = useAuth();
const { toggleMobileMenu, openMobileMenu, sidebarMode, setSidebarMode } = useSidebar(); const { toggleMobileMenu, openMobileMenu, sidebarMode, setSidebarMode, setSidebarDragProgress, setIsSidebarDragging } = useSidebar();
const isAdmin = user?.is_superuser || false; 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 tabs: ThemeTab[] = isAdmin ? ['colors', 'appearance', 'preview', 'advanced'] : ['colors', 'appearance', 'preview'];
const [tooltip, setTooltip] = useState<{ text: string; left: number; visible: boolean }>({ const [tooltip, setTooltip] = useState<{ text: string; left: number; visible: boolean }>({
text: '', text: '',
@@ -820,7 +836,9 @@ export default function ThemeSettings() {
onTabChange={setActiveTab} onTabChange={setActiveTab}
renderPanel={renderPanel} renderPanel={renderPanel}
panelClassName="admin-tab-content swipe-panel-content" panelClassName="admin-tab-content swipe-panel-content"
onSwipePastStart={openMobileMenu} onSwipePastStartDragStart={handleSidebarDragStart}
onSwipePastStartProgress={handleSidebarDragProgress}
onSwipePastStartDragEnd={handleSidebarDragEnd}
/> />
{/* Color Picker Popup */} {/* Color Picker Popup */}

View File

@@ -887,22 +887,6 @@ label,
justify-content: flex-start; 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 */ /* Add bottom padding and remove top padding when bar is at bottom */
[data-tab-position='bottom'] .main-content { [data-tab-position='bottom'] .main-content {
padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px)); padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px));
@@ -926,7 +910,13 @@ label,
left: 0; 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 */ /* Responsive position - top on desktop, bottom on mobile */

View File

@@ -678,6 +678,16 @@ button.nav-item {
transform: translateX(0); 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 */ /* Reset ALL collapsed styles on mobile - sidebar should always be fully expanded when open */
.sidebar.collapsed { .sidebar.collapsed {
width: 280px; width: 280px;