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:
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user