Add separate MobileTitleBar component for mobile layout
- Create MobileTitleBar component with fixed top position - Split title bar from tabs bar on mobile (title always on top) - Add data-has-actions attribute for action button detection - Track --tab-pill-height CSS variable for tab buttons - Remove extra padding from mobile content padding-top - Hide tabs container when no tabs or actions present 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
171
frontend/src/components/MobileTitleBar.tsx
Normal file
171
frontend/src/components/MobileTitleBar.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
|
||||
type MobileTitleBarProps = {
|
||||
title: string;
|
||||
menuLabel: string;
|
||||
onMenuClick: () => void;
|
||||
variant?: 'page' | 'admin';
|
||||
};
|
||||
|
||||
const MobileTitleBar = ({
|
||||
title,
|
||||
menuLabel,
|
||||
onMenuClick,
|
||||
variant = 'page'
|
||||
}: MobileTitleBarProps) => {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const menuButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const mainContentRef = useRef<HTMLElement | null>(null);
|
||||
const heightRef = useRef(0);
|
||||
const [isHidden, setIsHidden] = useState(false);
|
||||
const hiddenRef = useRef(false);
|
||||
const lastScrollYRef = useRef(0);
|
||||
const lastDirectionRef = useRef<'up' | 'down'>('up');
|
||||
const rafRef = useRef(0);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const node = containerRef.current;
|
||||
if (!node) return undefined;
|
||||
const mainContent = node.closest('.main-content');
|
||||
mainContentRef.current = mainContent instanceof HTMLElement ? mainContent : null;
|
||||
|
||||
const applyOffsets = (height: number, hidden: boolean) => {
|
||||
const offset = hidden ? 0 : height;
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.style.setProperty('--title-bar-height', `${height}px`);
|
||||
document.documentElement.style.setProperty('--title-bar-offset', `${offset}px`);
|
||||
}
|
||||
if (mainContentRef.current) {
|
||||
mainContentRef.current.style.setProperty('--title-bar-height', `${height}px`);
|
||||
mainContentRef.current.style.setProperty('--title-bar-offset', `${offset}px`);
|
||||
}
|
||||
};
|
||||
|
||||
const updateMetrics = () => {
|
||||
const height = Math.ceil(node.getBoundingClientRect().height);
|
||||
if (height && height !== heightRef.current) {
|
||||
heightRef.current = height;
|
||||
applyOffsets(height, hiddenRef.current);
|
||||
}
|
||||
const menuButton = menuButtonRef.current;
|
||||
if (menuButton) {
|
||||
const buttonWidth = Math.ceil(menuButton.getBoundingClientRect().width);
|
||||
if (buttonWidth) {
|
||||
node.style.setProperty('--title-action-width', `${buttonWidth}px`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
updateMetrics();
|
||||
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
resizeObserver = new ResizeObserver(updateMetrics);
|
||||
resizeObserver.observe(node);
|
||||
if (menuButtonRef.current) {
|
||||
resizeObserver.observe(menuButtonRef.current);
|
||||
}
|
||||
} else if (typeof window !== 'undefined') {
|
||||
window.addEventListener('resize', updateMetrics);
|
||||
}
|
||||
|
||||
return () => {
|
||||
resizeObserver?.disconnect();
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('resize', updateMetrics);
|
||||
}
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.style.removeProperty('--title-bar-height');
|
||||
document.documentElement.style.removeProperty('--title-bar-offset');
|
||||
}
|
||||
if (mainContentRef.current) {
|
||||
mainContentRef.current.style.removeProperty('--title-bar-height');
|
||||
mainContentRef.current.style.removeProperty('--title-bar-offset');
|
||||
}
|
||||
node.style.removeProperty('--title-action-width');
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const height = heightRef.current;
|
||||
const offset = isHidden ? 0 : height;
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.style.setProperty('--title-bar-offset', `${offset}px`);
|
||||
}
|
||||
if (mainContentRef.current) {
|
||||
mainContentRef.current.style.setProperty('--title-bar-offset', `${offset}px`);
|
||||
}
|
||||
hiddenRef.current = isHidden;
|
||||
}, [isHidden]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return undefined;
|
||||
|
||||
lastScrollYRef.current = window.scrollY || 0;
|
||||
const handleScroll = () => {
|
||||
if (rafRef.current) return;
|
||||
rafRef.current = window.requestAnimationFrame(() => {
|
||||
rafRef.current = 0;
|
||||
const currentY = window.scrollY || document.documentElement.scrollTop || 0;
|
||||
const delta = currentY - lastScrollYRef.current;
|
||||
|
||||
if (currentY <= 0) {
|
||||
if (hiddenRef.current) {
|
||||
hiddenRef.current = false;
|
||||
setIsHidden(false);
|
||||
}
|
||||
lastDirectionRef.current = 'up';
|
||||
lastScrollYRef.current = currentY;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Math.abs(delta) >= 8) {
|
||||
const direction = delta > 0 ? 'down' : 'up';
|
||||
if (direction !== lastDirectionRef.current) {
|
||||
lastDirectionRef.current = direction;
|
||||
const shouldHide = direction === 'down';
|
||||
if (shouldHide !== hiddenRef.current) {
|
||||
hiddenRef.current = shouldHide;
|
||||
setIsHidden(shouldHide);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lastScrollYRef.current = currentY;
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
if (rafRef.current) {
|
||||
cancelAnimationFrame(rafRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const containerClass = variant === 'admin' ? 'admin-title-container' : 'page-title-container';
|
||||
const sliderClass = variant === 'admin' ? 'admin-tabs-slider' : 'page-tabs-slider';
|
||||
const titleSectionClass = variant === 'admin' ? 'admin-title-section' : 'page-title-section';
|
||||
const titleTextClass = variant === 'admin' ? 'admin-title-text' : 'page-title-text';
|
||||
|
||||
return (
|
||||
<div className={`${containerClass} ${isHidden ? 'mobile-title-container--hidden' : ''}`} ref={containerRef}>
|
||||
<div className={`${sliderClass} mobile-title-slider`}>
|
||||
<button
|
||||
ref={menuButtonRef}
|
||||
className="mobile-menu-btn"
|
||||
onClick={onMenuClick}
|
||||
aria-label={menuLabel}
|
||||
>
|
||||
<span className="material-symbols-outlined">menu</span>
|
||||
</button>
|
||||
<div className={titleSectionClass}>
|
||||
<span className={titleTextClass}>{title}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileTitleBar;
|
||||
@@ -41,6 +41,7 @@ const TabsScroller = forwardRef<HTMLDivElement, TabsScrollerProps>(
|
||||
const [showLeft, setShowLeft] = useState(false);
|
||||
const [showRight, setShowRight] = useState(false);
|
||||
const heightRef = useRef(0);
|
||||
const pillHeightRef = useRef(0);
|
||||
|
||||
const dragRef = useRef({
|
||||
pointerId: null as number | null,
|
||||
@@ -68,15 +69,32 @@ const TabsScroller = forwardRef<HTMLDivElement, TabsScrollerProps>(
|
||||
const node = sliderRef.current;
|
||||
if (!node) return;
|
||||
const hasTabButtons = Boolean(node.querySelector('.page-tab-btn, .admin-tab-btn'));
|
||||
const hasActionButtons = Boolean(node.querySelector('.btn-primary'));
|
||||
const hasTabsValue = hasTabButtons ? 'true' : 'false';
|
||||
const hasActionsValue = hasActionButtons ? 'true' : 'false';
|
||||
node.dataset.hasTabs = hasTabsValue;
|
||||
node.dataset.hasActions = hasActionsValue;
|
||||
const container = node.closest('.page-tabs-container, .admin-tabs-container');
|
||||
const mainContent = node.closest('.main-content');
|
||||
let containerHeight = 0;
|
||||
if (container instanceof HTMLElement) {
|
||||
container.dataset.hasTabs = hasTabsValue;
|
||||
container.dataset.hasActions = hasActionsValue;
|
||||
containerHeight = Math.ceil(container.getBoundingClientRect().height);
|
||||
}
|
||||
const firstTabButton = node.querySelector('.page-tab-btn, .admin-tab-btn');
|
||||
if (firstTabButton instanceof HTMLElement) {
|
||||
const pillHeight = Math.ceil(firstTabButton.getBoundingClientRect().height);
|
||||
if (pillHeight && pillHeight !== pillHeightRef.current) {
|
||||
pillHeightRef.current = pillHeight;
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.style.setProperty('--tab-pill-height', `${pillHeight}px`);
|
||||
}
|
||||
if (mainContent instanceof HTMLElement) {
|
||||
mainContent.style.setProperty('--tab-pill-height', `${pillHeight}px`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (containerHeight && containerHeight !== heightRef.current) {
|
||||
heightRef.current = containerHeight;
|
||||
if (typeof document !== 'undefined') {
|
||||
@@ -88,6 +106,7 @@ const TabsScroller = forwardRef<HTMLDivElement, TabsScrollerProps>(
|
||||
}
|
||||
if (mainContent instanceof HTMLElement) {
|
||||
mainContent.dataset.hasTabs = hasTabsValue;
|
||||
mainContent.dataset.hasActions = hasActionsValue;
|
||||
}
|
||||
if (!showArrows) {
|
||||
setShowLeft(false);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from '../contexts/LanguageContext';
|
||||
import { useSidebar } from '../contexts/SidebarContext';
|
||||
import MobileTitleBar from '../components/MobileTitleBar';
|
||||
import TabsScroller from '../components/TabsScroller';
|
||||
import { SwipeableContent } from '../components/SwipeableContent';
|
||||
import { apiKeysAPI } from '../api/client';
|
||||
@@ -90,11 +91,13 @@ export default function APIKeys() {
|
||||
|
||||
return (
|
||||
<SwipeableContent className="main-content api-keys-root">
|
||||
<MobileTitleBar
|
||||
title={t.apiKeysPage.title}
|
||||
menuLabel={t.theme.toggleMenu}
|
||||
onMenuClick={toggleMobileMenu}
|
||||
/>
|
||||
<div className="page-tabs-container">
|
||||
<TabsScroller className="page-tabs-slider">
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
<span className="material-symbols-outlined">menu</span>
|
||||
</button>
|
||||
<div className="page-title-section">
|
||||
<span className="page-title-text">{t.apiKeysPage.title}</span>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, useCallback } from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useTranslation } from '../contexts/LanguageContext';
|
||||
import { useSidebar } from '../contexts/SidebarContext';
|
||||
import MobileTitleBar from '../components/MobileTitleBar';
|
||||
import TabsScroller from '../components/TabsScroller';
|
||||
import GeneralTab from '../components/admin/GeneralTab';
|
||||
import UsersTab from '../components/admin/UsersTab';
|
||||
@@ -40,11 +41,13 @@ export default function AdminPanel({ initialTab = 'general' }: { initialTab?: Ta
|
||||
|
||||
return (
|
||||
<main className="main-content admin-panel-root">
|
||||
<MobileTitleBar
|
||||
title={t.admin.panel}
|
||||
menuLabel={t.theme.toggleMenu}
|
||||
onMenuClick={toggleMobileMenu}
|
||||
/>
|
||||
<div className="page-tabs-container">
|
||||
<TabsScroller className="page-tabs-slider" showArrows>
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
<span className="material-symbols-outlined">menu</span>
|
||||
</button>
|
||||
<div className="page-title-section">
|
||||
<span className="page-title-text">{t.admin.panel}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useTranslation } from '../contexts/LanguageContext';
|
||||
import { useSidebar } from '../contexts/SidebarContext';
|
||||
import MobileTitleBar from '../components/MobileTitleBar';
|
||||
import TabsScroller from '../components/TabsScroller';
|
||||
import { SwipeableContent } from '../components/SwipeableContent';
|
||||
|
||||
@@ -9,11 +10,13 @@ export default function Dashboard() {
|
||||
|
||||
return (
|
||||
<SwipeableContent className="main-content">
|
||||
<MobileTitleBar
|
||||
title={t.dashboard.title}
|
||||
menuLabel={t.theme.toggleMenu}
|
||||
onMenuClick={toggleMobileMenu}
|
||||
/>
|
||||
<div className="page-tabs-container">
|
||||
<TabsScroller className="page-tabs-slider">
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
<span className="material-symbols-outlined">menu</span>
|
||||
</button>
|
||||
<div className="page-title-section">
|
||||
<span className="page-title-text">{t.dashboard.title}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useTranslation } from '../contexts/LanguageContext';
|
||||
import { useSidebar } from '../contexts/SidebarContext';
|
||||
import MobileTitleBar from '../components/MobileTitleBar';
|
||||
import TabsScroller from '../components/TabsScroller';
|
||||
import { SwipeableContent } from '../components/SwipeableContent';
|
||||
import '../styles/AdminPanel.css';
|
||||
@@ -10,11 +11,13 @@ export default function Feature1() {
|
||||
|
||||
return (
|
||||
<SwipeableContent className="main-content">
|
||||
<MobileTitleBar
|
||||
title={t.feature1.title}
|
||||
menuLabel={t.theme.toggleMenu}
|
||||
onMenuClick={toggleMobileMenu}
|
||||
/>
|
||||
<div className="page-tabs-container">
|
||||
<TabsScroller className="page-tabs-slider">
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
<span className="material-symbols-outlined">menu</span>
|
||||
</button>
|
||||
<div className="page-title-section">
|
||||
<span className="page-title-text">{t.feature1.title}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useTranslation } from '../contexts/LanguageContext';
|
||||
import { useSidebar } from '../contexts/SidebarContext';
|
||||
import MobileTitleBar from '../components/MobileTitleBar';
|
||||
import TabsScroller from '../components/TabsScroller';
|
||||
import { SwipeableContent } from '../components/SwipeableContent';
|
||||
import '../styles/AdminPanel.css';
|
||||
@@ -10,11 +11,13 @@ export default function Feature2() {
|
||||
|
||||
return (
|
||||
<SwipeableContent className="main-content">
|
||||
<MobileTitleBar
|
||||
title={t.features.feature2}
|
||||
menuLabel={t.theme.toggleMenu}
|
||||
onMenuClick={toggleMobileMenu}
|
||||
/>
|
||||
<div className="page-tabs-container">
|
||||
<TabsScroller className="page-tabs-slider">
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
<span className="material-symbols-outlined">menu</span>
|
||||
</button>
|
||||
<div className="page-title-section">
|
||||
<span className="page-title-text">{t.features.feature2}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useTranslation } from '../contexts/LanguageContext';
|
||||
import { useSidebar } from '../contexts/SidebarContext';
|
||||
import MobileTitleBar from '../components/MobileTitleBar';
|
||||
import TabsScroller from '../components/TabsScroller';
|
||||
import { SwipeableContent } from '../components/SwipeableContent';
|
||||
import '../styles/AdminPanel.css';
|
||||
@@ -10,11 +11,13 @@ export default function Feature3() {
|
||||
|
||||
return (
|
||||
<SwipeableContent className="main-content">
|
||||
<MobileTitleBar
|
||||
title={t.features.feature3}
|
||||
menuLabel={t.theme.toggleMenu}
|
||||
onMenuClick={toggleMobileMenu}
|
||||
/>
|
||||
<div className="page-tabs-container">
|
||||
<TabsScroller className="page-tabs-slider">
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
<span className="material-symbols-outlined">menu</span>
|
||||
</button>
|
||||
<div className="page-title-section">
|
||||
<span className="page-title-text">{t.features.feature3}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from '../contexts/LanguageContext';
|
||||
import { useSidebar } from '../contexts/SidebarContext';
|
||||
import MobileTitleBar from '../components/MobileTitleBar';
|
||||
import TabsScroller from '../components/TabsScroller';
|
||||
import { SwipeableContent } from '../components/SwipeableContent';
|
||||
import { useNotifications } from '../contexts/NotificationsContext';
|
||||
@@ -96,11 +97,13 @@ export default function Notifications() {
|
||||
|
||||
return (
|
||||
<SwipeableContent className="main-content notifications-root">
|
||||
<MobileTitleBar
|
||||
title={t.notificationsPage.title}
|
||||
menuLabel={t.theme.toggleMenu}
|
||||
onMenuClick={toggleMobileMenu}
|
||||
/>
|
||||
<div className="page-tabs-container">
|
||||
<TabsScroller className="page-tabs-slider">
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
<span className="material-symbols-outlined">menu</span>
|
||||
</button>
|
||||
<div className="page-title-section">
|
||||
<span className="page-title-text">{t.notificationsPage.title}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from '../contexts/LanguageContext';
|
||||
import { useSidebar } from '../contexts/SidebarContext';
|
||||
import MobileTitleBar from '../components/MobileTitleBar';
|
||||
import TabsScroller from '../components/TabsScroller';
|
||||
import { SwipeableContent } from '../components/SwipeableContent';
|
||||
import '../styles/Search.css';
|
||||
@@ -78,11 +79,13 @@ export default function Search() {
|
||||
|
||||
return (
|
||||
<SwipeableContent className="main-content search-root">
|
||||
<MobileTitleBar
|
||||
title={t.searchPage.title}
|
||||
menuLabel={t.theme.toggleMenu}
|
||||
onMenuClick={toggleMobileMenu}
|
||||
/>
|
||||
<div className="page-tabs-container">
|
||||
<TabsScroller className="page-tabs-slider">
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
<span className="material-symbols-outlined">menu</span>
|
||||
</button>
|
||||
<div className="page-title-section">
|
||||
<span className="page-title-text">{t.searchPage.title}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from '../contexts/LanguageContext';
|
||||
import { useSidebar } from '../contexts/SidebarContext';
|
||||
import MobileTitleBar from '../components/MobileTitleBar';
|
||||
import TabsScroller from '../components/TabsScroller';
|
||||
import { SwipeableContent } from '../components/SwipeableContent';
|
||||
import { sessionsAPI, twoFactorAPI } from '../api/client';
|
||||
@@ -153,11 +154,13 @@ export default function Settings() {
|
||||
|
||||
return (
|
||||
<SwipeableContent className="main-content settings-page-root">
|
||||
<MobileTitleBar
|
||||
title={t.settings.title}
|
||||
menuLabel={t.theme.toggleMenu}
|
||||
onMenuClick={toggleMobileMenu}
|
||||
/>
|
||||
<div className="page-tabs-container">
|
||||
<TabsScroller className="page-tabs-slider">
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
<span className="material-symbols-outlined">menu</span>
|
||||
</button>
|
||||
<div className="page-title-section">
|
||||
<span className="page-title-text">{t.settings.title}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import type { FormEvent } from 'react';
|
||||
import Sidebar from '../components/Sidebar';
|
||||
import MobileTitleBar from '../components/MobileTitleBar';
|
||||
import TabsScroller from '../components/TabsScroller';
|
||||
import { SwipeableContent } from '../components/SwipeableContent';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
@@ -182,11 +183,13 @@ export default function Users() {
|
||||
<div className="app-layout">
|
||||
<Sidebar />
|
||||
<SwipeableContent className="main-content users-root">
|
||||
<MobileTitleBar
|
||||
title={t.admin.userManagement}
|
||||
menuLabel={t.theme.toggleMenu}
|
||||
onMenuClick={toggleMobileMenu}
|
||||
/>
|
||||
<div className="page-tabs-container">
|
||||
<TabsScroller className="page-tabs-slider">
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
<span className="material-symbols-outlined">menu</span>
|
||||
</button>
|
||||
<div className="page-title-section">
|
||||
<span className="page-title-text">{t.admin.userManagement}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from '../../contexts/LanguageContext';
|
||||
import { useSidebar } from '../../contexts/SidebarContext';
|
||||
import MobileTitleBar from '../../components/MobileTitleBar';
|
||||
import TabsScroller from '../../components/TabsScroller';
|
||||
import { SwipeableContent } from '../../components/SwipeableContent';
|
||||
import { analyticsAPI } from '../../api/client';
|
||||
@@ -47,11 +48,13 @@ export default function Analytics() {
|
||||
|
||||
return (
|
||||
<SwipeableContent className="main-content admin-analytics-root">
|
||||
<MobileTitleBar
|
||||
title={t.analyticsPage.title}
|
||||
menuLabel={t.theme.toggleMenu}
|
||||
onMenuClick={toggleMobileMenu}
|
||||
/>
|
||||
<div className="page-tabs-container">
|
||||
<TabsScroller className="page-tabs-slider">
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
<span className="material-symbols-outlined">menu</span>
|
||||
</button>
|
||||
<div className="page-title-section">
|
||||
<span className="page-title-text">{t.analyticsPage.title}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from '../../contexts/LanguageContext';
|
||||
import { useSidebar } from '../../contexts/SidebarContext';
|
||||
import MobileTitleBar from '../../components/MobileTitleBar';
|
||||
import TabsScroller from '../../components/TabsScroller';
|
||||
import { SwipeableContent } from '../../components/SwipeableContent';
|
||||
import { auditAPI } from '../../api/client';
|
||||
@@ -60,11 +61,13 @@ export default function AuditLogs() {
|
||||
|
||||
return (
|
||||
<SwipeableContent className="main-content admin-audit-root">
|
||||
<MobileTitleBar
|
||||
title={t.auditPage.title}
|
||||
menuLabel={t.theme.toggleMenu}
|
||||
onMenuClick={toggleMobileMenu}
|
||||
/>
|
||||
<div className="page-tabs-container">
|
||||
<TabsScroller className="page-tabs-slider">
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
<span className="material-symbols-outlined">menu</span>
|
||||
</button>
|
||||
<div className="page-title-section">
|
||||
<span className="page-title-text">{t.auditPage.title}</span>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useSidebar } from '../../contexts/SidebarContext';
|
||||
import { useModules, TOGGLEABLE_MODULES } from '../../contexts/ModulesContext';
|
||||
import type { ModuleId } from '../../contexts/ModulesContext';
|
||||
import Feature1Tab from '../../components/admin/Feature1Tab';
|
||||
import MobileTitleBar from '../../components/MobileTitleBar';
|
||||
import { SwipeTabs } from '../../components/SwipeTabs';
|
||||
import TabsScroller from '../../components/TabsScroller';
|
||||
import '../../styles/AdminPanel.css';
|
||||
@@ -780,11 +781,13 @@ export default function Features() {
|
||||
|
||||
return (
|
||||
<main className="main-content admin-panel-root">
|
||||
<MobileTitleBar
|
||||
title={t.featuresPage.title}
|
||||
menuLabel={t.theme.toggleMenu}
|
||||
onMenuClick={toggleMobileMenu}
|
||||
/>
|
||||
<div className="page-tabs-container">
|
||||
<TabsScroller className="page-tabs-slider" ref={tabsContainerRef} showArrows>
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
<span className="material-symbols-outlined">menu</span>
|
||||
</button>
|
||||
<div className="page-title-section">
|
||||
<span className="page-title-text">{t.featuresPage.title}</span>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from '../../contexts/LanguageContext';
|
||||
import { useSidebar } from '../../contexts/SidebarContext';
|
||||
import Sidebar from '../../components/Sidebar';
|
||||
import MobileTitleBar from '../../components/MobileTitleBar';
|
||||
import TabsScroller from '../../components/TabsScroller';
|
||||
import { SwipeableContent } from '../../components/SwipeableContent';
|
||||
import { settingsAPI } from '../../api/client';
|
||||
@@ -61,11 +62,13 @@ export default function Settings() {
|
||||
<div className="app-layout">
|
||||
<Sidebar />
|
||||
<SwipeableContent className="main-content settings-root">
|
||||
<MobileTitleBar
|
||||
title={t.settings.title}
|
||||
menuLabel={t.theme.toggleMenu}
|
||||
onMenuClick={toggleMobileMenu}
|
||||
/>
|
||||
<div className="page-tabs-container">
|
||||
<TabsScroller className="page-tabs-slider">
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
<span className="material-symbols-outlined">menu</span>
|
||||
</button>
|
||||
<div className="page-title-section">
|
||||
<span className="page-title-text">{t.settings.title}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { useTranslation } from '../../contexts/LanguageContext';
|
||||
import { useSidebar } from '../../contexts/SidebarContext';
|
||||
import MobileTitleBar from '../../components/MobileTitleBar';
|
||||
import TabsScroller from '../../components/TabsScroller';
|
||||
import { SwipeableContent } from '../../components/SwipeableContent';
|
||||
import '../../styles/AdminPanel.css';
|
||||
@@ -16,11 +17,14 @@ export default function Sources() {
|
||||
|
||||
return (
|
||||
<SwipeableContent className="main-content admin-panel-root">
|
||||
<MobileTitleBar
|
||||
title={t.sourcesPage.title}
|
||||
menuLabel={t.theme.toggleMenu}
|
||||
onMenuClick={toggleMobileMenu}
|
||||
variant="admin"
|
||||
/>
|
||||
<div className="admin-tabs-container">
|
||||
<TabsScroller className="admin-tabs-slider">
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
<span className="material-symbols-outlined">menu</span>
|
||||
</button>
|
||||
<div className="admin-title-section">
|
||||
<span className="admin-title-text">{t.sourcesPage.title}</span>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useAuth } from '../../contexts/AuthContext';
|
||||
import { useSidebar } from '../../contexts/SidebarContext';
|
||||
import type { SidebarMode } from '../../contexts/SidebarContext';
|
||||
import { ChromePicker, HuePicker } from 'react-color';
|
||||
import MobileTitleBar from '../../components/MobileTitleBar';
|
||||
import { SwipeTabs } from '../../components/SwipeTabs';
|
||||
import TabsScroller from '../../components/TabsScroller';
|
||||
import '../../styles/ThemeSettings.css';
|
||||
@@ -778,12 +779,14 @@ export default function ThemeSettings() {
|
||||
{tooltip.text}
|
||||
</div>
|
||||
)}
|
||||
<MobileTitleBar
|
||||
title={t.theme.title}
|
||||
menuLabel={t.theme.toggleMenu}
|
||||
onMenuClick={toggleMobileMenu}
|
||||
/>
|
||||
{/* Modern Tab Navigation */}
|
||||
<div className="page-tabs-container">
|
||||
<TabsScroller className="page-tabs-slider" showArrows>
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
<span className="material-symbols-outlined">menu</span>
|
||||
</button>
|
||||
<div className="page-title-section">
|
||||
<span className="page-title-text">{t.theme.title}</span>
|
||||
</div>
|
||||
|
||||
@@ -37,6 +37,15 @@
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.page-title-container,
|
||||
.admin-title-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-title-slider {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Ensure no extra margin from body */
|
||||
body {
|
||||
margin: 0;
|
||||
@@ -442,14 +451,78 @@ label,
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.page-title-container,
|
||||
.admin-title-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
z-index: 110;
|
||||
transition: transform 0.25s ease, opacity 0.25s ease;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.mobile-title-container--hidden {
|
||||
transform: translateY(calc(-100% - 12px));
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mobile-title-slider {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 48px minmax(0, 1fr) 48px;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 4px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.mobile-title-slider .mobile-menu-btn {
|
||||
grid-column: 1;
|
||||
justify-self: start;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
min-width: 40px;
|
||||
margin-left: 6px;
|
||||
margin-right: 6px;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
border-radius: var(--radius-lg);
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
background-size: 26px 26px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.mobile-title-slider .page-title-section,
|
||||
.mobile-title-slider .admin-title-section {
|
||||
grid-column: 2;
|
||||
justify-content: center;
|
||||
justify-self: center;
|
||||
max-width: 100%;
|
||||
height: 40px;
|
||||
padding: 0 0.75rem;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mobile-title-slider .page-title-text,
|
||||
.mobile-title-slider .admin-title-text {
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Show mobile menu button with logo */
|
||||
.mobile-menu-btn {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 1;
|
||||
background-image: url('/logo_black.svg');
|
||||
background-size: 28px 28px;
|
||||
background-repeat: no-repeat;
|
||||
@@ -473,40 +546,16 @@ label,
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.page-tabs-slider,
|
||||
.admin-tabs-slider {
|
||||
width: 100%;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
gap: 4px;
|
||||
position: relative;
|
||||
min-height: 48px;
|
||||
.page-tabs-container .page-title-section,
|
||||
.page-tabs-container .admin-title-section,
|
||||
.admin-tabs-container .page-title-section,
|
||||
.admin-tabs-container .admin-title-section {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tabs-scroll-shell {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-title-section,
|
||||
.admin-title-section {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: flex-start;
|
||||
padding: 0.5rem 0.75rem;
|
||||
padding-left: 72px;
|
||||
font-size: 1rem;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-title-section .material-symbols-outlined,
|
||||
.admin-title-section .material-symbols-outlined {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.page-title-text,
|
||||
.admin-title-text {
|
||||
font-size: 0.95rem;
|
||||
.page-tabs-container[data-has-tabs='false']:not([data-has-actions='true']),
|
||||
.admin-tabs-container[data-has-tabs='false']:not([data-has-actions='true']) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide divider on mobile */
|
||||
@@ -515,103 +564,27 @@ label,
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide title section when tabs are present on mobile */
|
||||
.page-tabs-slider[data-has-tabs='true'] .page-title-section,
|
||||
.page-tabs-slider[data-has-tabs='true'] .admin-title-section,
|
||||
.admin-tabs-slider[data-has-tabs='true'] .page-title-section,
|
||||
.admin-tabs-slider[data-has-tabs='true'] .admin-title-section {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Add padding-left when tabs are present to avoid logo overlap */
|
||||
.page-tabs-slider[data-has-tabs='true'],
|
||||
.admin-tabs-slider[data-has-tabs='true'] {
|
||||
padding-left: 72px;
|
||||
}
|
||||
|
||||
/* Center title section absolutely when no tabs are present on mobile */
|
||||
.page-tabs-slider[data-has-tabs='false'],
|
||||
.admin-tabs-slider[data-has-tabs='false'] {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.page-tabs-slider[data-has-tabs='false'] .page-title-section,
|
||||
.page-tabs-slider[data-has-tabs='false'] .admin-title-section,
|
||||
.admin-tabs-slider[data-has-tabs='false'] .page-title-section,
|
||||
.admin-tabs-slider[data-has-tabs='false'] .admin-title-section {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 0.5rem 0.75rem;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
/* Lighter icon color in dark theme when only title is shown */
|
||||
.page-tabs-slider[data-has-tabs='false'] .page-title-section .material-symbols-outlined,
|
||||
.page-tabs-slider[data-has-tabs='false'] .admin-title-section .material-symbols-outlined,
|
||||
.admin-tabs-slider[data-has-tabs='false'] .page-title-section .material-symbols-outlined,
|
||||
.admin-tabs-slider[data-has-tabs='false'] .admin-title-section .material-symbols-outlined {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* Tabs - expand to fill, but scrollable when overflow */
|
||||
.page-tab-btn,
|
||||
.admin-tab-btn {
|
||||
flex: 1 0 auto;
|
||||
justify-content: center;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.9rem;
|
||||
min-width: 44px;
|
||||
}
|
||||
|
||||
/* Hide text on mobile, show only icons */
|
||||
.page-tab-btn span:not(.material-symbols-outlined),
|
||||
.admin-tab-btn span:not(.material-symbols-outlined) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-tab-btn .material-symbols-outlined,
|
||||
.admin-tab-btn .material-symbols-outlined {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
/* Action buttons in slider - icon only on mobile */
|
||||
.page-tabs-slider .btn-primary,
|
||||
.admin-tabs-slider .btn-primary {
|
||||
padding: 0.5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.page-tabs-slider .btn-primary span:not(.material-symbols-outlined),
|
||||
.admin-tabs-slider .btn-primary span:not(.material-symbols-outlined) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
padding: var(--page-padding-y-mobile) var(--page-padding-x-mobile);
|
||||
padding-top: var(--title-bar-offset, var(--title-bar-height, 0px));
|
||||
}
|
||||
|
||||
.admin-tab-content {
|
||||
padding: var(--page-padding-y-mobile) var(--page-padding-x-mobile);
|
||||
padding-top: var(--title-bar-offset, var(--title-bar-height, 0px));
|
||||
}
|
||||
}
|
||||
|
||||
/* Small mobile - further reduce spacing */
|
||||
@media (max-width: 480px) {
|
||||
|
||||
.page-tabs-container,
|
||||
.admin-tabs-container {
|
||||
padding: var(--page-padding-y-mobile) var(--page-padding-x-mobile);
|
||||
}
|
||||
|
||||
.page-content {
|
||||
padding: var(--page-padding-y-mobile) var(--page-padding-x-mobile);
|
||||
padding-top: var(--title-bar-offset, var(--title-bar-height, 0px));
|
||||
}
|
||||
|
||||
.admin-tab-content {
|
||||
padding: var(--page-padding-y-mobile) var(--page-padding-x-mobile);
|
||||
padding-top: var(--title-bar-offset, var(--title-bar-height, 0px));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -849,9 +822,11 @@ label,
|
||||
@media (max-width: 768px) {
|
||||
[data-tab-position='top'] .page-tabs-container,
|
||||
[data-tab-position='top'] .admin-tabs-container {
|
||||
top: var(--title-bar-offset, var(--title-bar-height, 0px));
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
[data-tab-position='top'][data-sidebar-collapsed='true'] .page-tabs-container,
|
||||
@@ -860,9 +835,14 @@ label,
|
||||
right: 0;
|
||||
}
|
||||
|
||||
[data-tab-position='top'] .page-content,
|
||||
[data-tab-position='top'] .admin-tab-content {
|
||||
padding-top: calc(var(--page-padding-y-mobile) + var(--tabs-bar-height, 72px));
|
||||
[data-tab-position='top'] .main-content[data-has-tabs='true'] .page-content,
|
||||
[data-tab-position='top'] .main-content[data-has-actions='true'] .page-content,
|
||||
[data-tab-position='top'] .main-content[data-has-tabs='true'] .admin-tab-content,
|
||||
[data-tab-position='top'] .main-content[data-has-actions='true'] .admin-tab-content {
|
||||
padding-top: calc(
|
||||
var(--title-bar-offset, var(--title-bar-height, 0px)) +
|
||||
var(--tabs-bar-height, 72px)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -898,8 +878,8 @@ label,
|
||||
left: var(--sidebar-width);
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
padding: 0.5rem;
|
||||
padding-bottom: calc(0.5rem + env(safe-area-inset-bottom, 0px));
|
||||
padding: 0.75rem;
|
||||
padding-bottom: calc(0.75rem + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
/* Handle collapsed sidebar */
|
||||
@@ -908,13 +888,6 @@ label,
|
||||
left: var(--sidebar-width-collapsed);
|
||||
}
|
||||
|
||||
/* Adjust slider styling for bottom position */
|
||||
[data-tab-position='bottom'] .page-tabs-slider[data-has-tabs='true'],
|
||||
[data-tab-position='bottom'] .admin-tabs-slider[data-has-tabs='true'] {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
/* Add bottom padding and remove top padding when bar is at bottom */
|
||||
[data-tab-position='bottom'] .main-content[data-has-tabs='true'] {
|
||||
padding-bottom: 0;
|
||||
@@ -958,7 +931,7 @@ label,
|
||||
/* Mobile padding for bottom position */
|
||||
[data-tab-position='bottom'] .main-content[data-has-tabs='true'] .page-content,
|
||||
[data-tab-position='bottom'] .main-content[data-has-tabs='true'] .admin-tab-swipe {
|
||||
padding-top: var(--page-padding-y-mobile);
|
||||
padding-top: var(--title-bar-offset, var(--title-bar-height, 0px));
|
||||
}
|
||||
|
||||
[data-tab-position='bottom'] .main-content[data-has-tabs='true'] .page-content,
|
||||
@@ -981,23 +954,8 @@ label,
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
padding: 0.5rem;
|
||||
padding-bottom: calc(0.5rem + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
[data-tab-position='responsive'] .page-tabs-slider[data-has-tabs='true'],
|
||||
[data-tab-position='responsive'] .admin-tabs-slider[data-has-tabs='true'] {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
/* padding-left: 72px inherited from mobile rules for menu button spacing */
|
||||
}
|
||||
|
||||
/* Hide title section when bar is at bottom (only on pages with tabs) */
|
||||
[data-tab-position='responsive'] .page-tabs-slider[data-has-tabs='true'] .page-title-section,
|
||||
[data-tab-position='responsive'] .page-tabs-slider[data-has-tabs='true'] .admin-title-section,
|
||||
[data-tab-position='responsive'] .admin-tabs-slider[data-has-tabs='true'] .page-title-section,
|
||||
[data-tab-position='responsive'] .admin-tabs-slider[data-has-tabs='true'] .admin-title-section {
|
||||
display: none;
|
||||
padding: 0.75rem;
|
||||
padding-bottom: calc(0.75rem + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
/* Hide divider when bar is at bottom */
|
||||
@@ -1015,7 +973,7 @@ label,
|
||||
|
||||
[data-tab-position='responsive'] .main-content[data-has-tabs='true'] .page-content,
|
||||
[data-tab-position='responsive'] .main-content[data-has-tabs='true'] .admin-tab-swipe {
|
||||
padding-top: var(--page-padding-y-mobile);
|
||||
padding-top: var(--title-bar-offset, var(--title-bar-height, 0px));
|
||||
}
|
||||
|
||||
[data-tab-position='responsive'] .main-content[data-has-tabs='true'] .page-content,
|
||||
|
||||
@@ -139,9 +139,12 @@
|
||||
/* Kept same */
|
||||
|
||||
/* Tab Sizes */
|
||||
--tab-padding: 0.625rem 1rem;
|
||||
--tab-padding-y: 0.625rem;
|
||||
--tab-padding-x: 1rem;
|
||||
--tab-padding: var(--tab-padding-y) var(--tab-padding-x);
|
||||
/* Reduced from 0.75rem 1.25rem */
|
||||
--tab-font-size: 0.95rem;
|
||||
--tab-pill-height: calc((var(--tab-font-size) * 1.2) + (var(--tab-padding-y) * 2));
|
||||
|
||||
/* Input Sizes */
|
||||
--input-padding: 0.625rem 0.875rem;
|
||||
|
||||
Reference in New Issue
Block a user