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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 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 */}