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:
2026-01-05 16:52:34 +01:00
parent c85a7f9258
commit 97494679ec
20 changed files with 397 additions and 197 deletions

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

View File

@@ -41,6 +41,7 @@ const TabsScroller = forwardRef<HTMLDivElement, TabsScrollerProps>(
const [showLeft, setShowLeft] = useState(false); const [showLeft, setShowLeft] = useState(false);
const [showRight, setShowRight] = useState(false); const [showRight, setShowRight] = useState(false);
const heightRef = useRef(0); const heightRef = useRef(0);
const pillHeightRef = useRef(0);
const dragRef = useRef({ const dragRef = useRef({
pointerId: null as number | null, pointerId: null as number | null,
@@ -68,15 +69,32 @@ const TabsScroller = forwardRef<HTMLDivElement, TabsScrollerProps>(
const node = sliderRef.current; const node = sliderRef.current;
if (!node) return; if (!node) return;
const hasTabButtons = Boolean(node.querySelector('.page-tab-btn, .admin-tab-btn')); const hasTabButtons = Boolean(node.querySelector('.page-tab-btn, .admin-tab-btn'));
const hasActionButtons = Boolean(node.querySelector('.btn-primary'));
const hasTabsValue = hasTabButtons ? 'true' : 'false'; const hasTabsValue = hasTabButtons ? 'true' : 'false';
const hasActionsValue = hasActionButtons ? 'true' : 'false';
node.dataset.hasTabs = hasTabsValue; node.dataset.hasTabs = hasTabsValue;
node.dataset.hasActions = hasActionsValue;
const container = node.closest('.page-tabs-container, .admin-tabs-container'); const container = node.closest('.page-tabs-container, .admin-tabs-container');
const mainContent = node.closest('.main-content'); const mainContent = node.closest('.main-content');
let containerHeight = 0; let containerHeight = 0;
if (container instanceof HTMLElement) { if (container instanceof HTMLElement) {
container.dataset.hasTabs = hasTabsValue; container.dataset.hasTabs = hasTabsValue;
container.dataset.hasActions = hasActionsValue;
containerHeight = Math.ceil(container.getBoundingClientRect().height); 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) { if (containerHeight && containerHeight !== heightRef.current) {
heightRef.current = containerHeight; heightRef.current = containerHeight;
if (typeof document !== 'undefined') { if (typeof document !== 'undefined') {
@@ -88,6 +106,7 @@ const TabsScroller = forwardRef<HTMLDivElement, TabsScrollerProps>(
} }
if (mainContent instanceof HTMLElement) { if (mainContent instanceof HTMLElement) {
mainContent.dataset.hasTabs = hasTabsValue; mainContent.dataset.hasTabs = hasTabsValue;
mainContent.dataset.hasActions = hasActionsValue;
} }
if (!showArrows) { if (!showArrows) {
setShowLeft(false); setShowLeft(false);

View File

@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react'; 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 MobileTitleBar from '../components/MobileTitleBar';
import TabsScroller from '../components/TabsScroller'; import TabsScroller from '../components/TabsScroller';
import { SwipeableContent } from '../components/SwipeableContent'; import { SwipeableContent } from '../components/SwipeableContent';
import { apiKeysAPI } from '../api/client'; import { apiKeysAPI } from '../api/client';
@@ -90,11 +91,13 @@ export default function APIKeys() {
return ( return (
<SwipeableContent className="main-content api-keys-root"> <SwipeableContent className="main-content api-keys-root">
<MobileTitleBar
title={t.apiKeysPage.title}
menuLabel={t.theme.toggleMenu}
onMenuClick={toggleMobileMenu}
/>
<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}>
<span className="material-symbols-outlined">menu</span>
</button>
<div className="page-title-section"> <div className="page-title-section">
<span className="page-title-text">{t.apiKeysPage.title}</span> <span className="page-title-text">{t.apiKeysPage.title}</span>
</div> </div>

View File

@@ -2,6 +2,7 @@ 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';
import MobileTitleBar from '../components/MobileTitleBar';
import TabsScroller from '../components/TabsScroller'; import TabsScroller from '../components/TabsScroller';
import GeneralTab from '../components/admin/GeneralTab'; import GeneralTab from '../components/admin/GeneralTab';
import UsersTab from '../components/admin/UsersTab'; import UsersTab from '../components/admin/UsersTab';
@@ -40,11 +41,13 @@ export default function AdminPanel({ initialTab = 'general' }: { initialTab?: Ta
return ( return (
<main className="main-content admin-panel-root"> <main className="main-content admin-panel-root">
<MobileTitleBar
title={t.admin.panel}
menuLabel={t.theme.toggleMenu}
onMenuClick={toggleMobileMenu}
/>
<div className="page-tabs-container"> <div className="page-tabs-container">
<TabsScroller className="page-tabs-slider" showArrows> <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"> <div className="page-title-section">
<span className="page-title-text">{t.admin.panel}</span> <span className="page-title-text">{t.admin.panel}</span>
</div> </div>

View File

@@ -1,5 +1,6 @@
import { useTranslation } from '../contexts/LanguageContext'; import { useTranslation } from '../contexts/LanguageContext';
import { useSidebar } from '../contexts/SidebarContext'; import { useSidebar } from '../contexts/SidebarContext';
import MobileTitleBar from '../components/MobileTitleBar';
import TabsScroller from '../components/TabsScroller'; import TabsScroller from '../components/TabsScroller';
import { SwipeableContent } from '../components/SwipeableContent'; import { SwipeableContent } from '../components/SwipeableContent';
@@ -9,11 +10,13 @@ export default function Dashboard() {
return ( return (
<SwipeableContent className="main-content"> <SwipeableContent className="main-content">
<MobileTitleBar
title={t.dashboard.title}
menuLabel={t.theme.toggleMenu}
onMenuClick={toggleMobileMenu}
/>
<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}>
<span className="material-symbols-outlined">menu</span>
</button>
<div className="page-title-section"> <div className="page-title-section">
<span className="page-title-text">{t.dashboard.title}</span> <span className="page-title-text">{t.dashboard.title}</span>
</div> </div>

View File

@@ -1,5 +1,6 @@
import { useTranslation } from '../contexts/LanguageContext'; import { useTranslation } from '../contexts/LanguageContext';
import { useSidebar } from '../contexts/SidebarContext'; import { useSidebar } from '../contexts/SidebarContext';
import MobileTitleBar from '../components/MobileTitleBar';
import TabsScroller from '../components/TabsScroller'; import TabsScroller from '../components/TabsScroller';
import { SwipeableContent } from '../components/SwipeableContent'; import { SwipeableContent } from '../components/SwipeableContent';
import '../styles/AdminPanel.css'; import '../styles/AdminPanel.css';
@@ -10,11 +11,13 @@ export default function Feature1() {
return ( return (
<SwipeableContent className="main-content"> <SwipeableContent className="main-content">
<MobileTitleBar
title={t.feature1.title}
menuLabel={t.theme.toggleMenu}
onMenuClick={toggleMobileMenu}
/>
<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}>
<span className="material-symbols-outlined">menu</span>
</button>
<div className="page-title-section"> <div className="page-title-section">
<span className="page-title-text">{t.feature1.title}</span> <span className="page-title-text">{t.feature1.title}</span>
</div> </div>

View File

@@ -1,5 +1,6 @@
import { useTranslation } from '../contexts/LanguageContext'; import { useTranslation } from '../contexts/LanguageContext';
import { useSidebar } from '../contexts/SidebarContext'; import { useSidebar } from '../contexts/SidebarContext';
import MobileTitleBar from '../components/MobileTitleBar';
import TabsScroller from '../components/TabsScroller'; import TabsScroller from '../components/TabsScroller';
import { SwipeableContent } from '../components/SwipeableContent'; import { SwipeableContent } from '../components/SwipeableContent';
import '../styles/AdminPanel.css'; import '../styles/AdminPanel.css';
@@ -10,11 +11,13 @@ export default function Feature2() {
return ( return (
<SwipeableContent className="main-content"> <SwipeableContent className="main-content">
<MobileTitleBar
title={t.features.feature2}
menuLabel={t.theme.toggleMenu}
onMenuClick={toggleMobileMenu}
/>
<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}>
<span className="material-symbols-outlined">menu</span>
</button>
<div className="page-title-section"> <div className="page-title-section">
<span className="page-title-text">{t.features.feature2}</span> <span className="page-title-text">{t.features.feature2}</span>
</div> </div>

View File

@@ -1,5 +1,6 @@
import { useTranslation } from '../contexts/LanguageContext'; import { useTranslation } from '../contexts/LanguageContext';
import { useSidebar } from '../contexts/SidebarContext'; import { useSidebar } from '../contexts/SidebarContext';
import MobileTitleBar from '../components/MobileTitleBar';
import TabsScroller from '../components/TabsScroller'; import TabsScroller from '../components/TabsScroller';
import { SwipeableContent } from '../components/SwipeableContent'; import { SwipeableContent } from '../components/SwipeableContent';
import '../styles/AdminPanel.css'; import '../styles/AdminPanel.css';
@@ -10,11 +11,13 @@ export default function Feature3() {
return ( return (
<SwipeableContent className="main-content"> <SwipeableContent className="main-content">
<MobileTitleBar
title={t.features.feature3}
menuLabel={t.theme.toggleMenu}
onMenuClick={toggleMobileMenu}
/>
<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}>
<span className="material-symbols-outlined">menu</span>
</button>
<div className="page-title-section"> <div className="page-title-section">
<span className="page-title-text">{t.features.feature3}</span> <span className="page-title-text">{t.features.feature3}</span>
</div> </div>

View File

@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react'; 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 MobileTitleBar from '../components/MobileTitleBar';
import TabsScroller from '../components/TabsScroller'; import TabsScroller from '../components/TabsScroller';
import { SwipeableContent } from '../components/SwipeableContent'; import { SwipeableContent } from '../components/SwipeableContent';
import { useNotifications } from '../contexts/NotificationsContext'; import { useNotifications } from '../contexts/NotificationsContext';
@@ -96,11 +97,13 @@ export default function Notifications() {
return ( return (
<SwipeableContent className="main-content notifications-root"> <SwipeableContent className="main-content notifications-root">
<MobileTitleBar
title={t.notificationsPage.title}
menuLabel={t.theme.toggleMenu}
onMenuClick={toggleMobileMenu}
/>
<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}>
<span className="material-symbols-outlined">menu</span>
</button>
<div className="page-title-section"> <div className="page-title-section">
<span className="page-title-text">{t.notificationsPage.title}</span> <span className="page-title-text">{t.notificationsPage.title}</span>
</div> </div>

View File

@@ -1,6 +1,7 @@
import { useEffect, useMemo, useRef, useState } from 'react'; 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 MobileTitleBar from '../components/MobileTitleBar';
import TabsScroller from '../components/TabsScroller'; import TabsScroller from '../components/TabsScroller';
import { SwipeableContent } from '../components/SwipeableContent'; import { SwipeableContent } from '../components/SwipeableContent';
import '../styles/Search.css'; import '../styles/Search.css';
@@ -78,11 +79,13 @@ export default function Search() {
return ( return (
<SwipeableContent className="main-content search-root"> <SwipeableContent className="main-content search-root">
<MobileTitleBar
title={t.searchPage.title}
menuLabel={t.theme.toggleMenu}
onMenuClick={toggleMobileMenu}
/>
<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}>
<span className="material-symbols-outlined">menu</span>
</button>
<div className="page-title-section"> <div className="page-title-section">
<span className="page-title-text">{t.searchPage.title}</span> <span className="page-title-text">{t.searchPage.title}</span>
</div> </div>

View File

@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react'; 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 MobileTitleBar from '../components/MobileTitleBar';
import TabsScroller from '../components/TabsScroller'; import TabsScroller from '../components/TabsScroller';
import { SwipeableContent } from '../components/SwipeableContent'; import { SwipeableContent } from '../components/SwipeableContent';
import { sessionsAPI, twoFactorAPI } from '../api/client'; import { sessionsAPI, twoFactorAPI } from '../api/client';
@@ -153,11 +154,13 @@ export default function Settings() {
return ( return (
<SwipeableContent className="main-content settings-page-root"> <SwipeableContent className="main-content settings-page-root">
<MobileTitleBar
title={t.settings.title}
menuLabel={t.theme.toggleMenu}
onMenuClick={toggleMobileMenu}
/>
<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}>
<span className="material-symbols-outlined">menu</span>
</button>
<div className="page-title-section"> <div className="page-title-section">
<span className="page-title-text">{t.settings.title}</span> <span className="page-title-text">{t.settings.title}</span>
</div> </div>

View File

@@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from 'react'; 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 MobileTitleBar from '../components/MobileTitleBar';
import TabsScroller from '../components/TabsScroller'; import TabsScroller from '../components/TabsScroller';
import { SwipeableContent } from '../components/SwipeableContent'; import { SwipeableContent } from '../components/SwipeableContent';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
@@ -182,11 +183,13 @@ export default function Users() {
<div className="app-layout"> <div className="app-layout">
<Sidebar /> <Sidebar />
<SwipeableContent className="main-content users-root"> <SwipeableContent className="main-content users-root">
<MobileTitleBar
title={t.admin.userManagement}
menuLabel={t.theme.toggleMenu}
onMenuClick={toggleMobileMenu}
/>
<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}>
<span className="material-symbols-outlined">menu</span>
</button>
<div className="page-title-section"> <div className="page-title-section">
<span className="page-title-text">{t.admin.userManagement}</span> <span className="page-title-text">{t.admin.userManagement}</span>
</div> </div>

View File

@@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from 'react'; 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 MobileTitleBar from '../../components/MobileTitleBar';
import TabsScroller from '../../components/TabsScroller'; import TabsScroller from '../../components/TabsScroller';
import { SwipeableContent } from '../../components/SwipeableContent'; import { SwipeableContent } from '../../components/SwipeableContent';
import { analyticsAPI } from '../../api/client'; import { analyticsAPI } from '../../api/client';
@@ -47,11 +48,13 @@ export default function Analytics() {
return ( return (
<SwipeableContent className="main-content admin-analytics-root"> <SwipeableContent className="main-content admin-analytics-root">
<MobileTitleBar
title={t.analyticsPage.title}
menuLabel={t.theme.toggleMenu}
onMenuClick={toggleMobileMenu}
/>
<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}>
<span className="material-symbols-outlined">menu</span>
</button>
<div className="page-title-section"> <div className="page-title-section">
<span className="page-title-text">{t.analyticsPage.title}</span> <span className="page-title-text">{t.analyticsPage.title}</span>
</div> </div>

View File

@@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from 'react'; 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 MobileTitleBar from '../../components/MobileTitleBar';
import TabsScroller from '../../components/TabsScroller'; import TabsScroller from '../../components/TabsScroller';
import { SwipeableContent } from '../../components/SwipeableContent'; import { SwipeableContent } from '../../components/SwipeableContent';
import { auditAPI } from '../../api/client'; import { auditAPI } from '../../api/client';
@@ -60,11 +61,13 @@ export default function AuditLogs() {
return ( return (
<SwipeableContent className="main-content admin-audit-root"> <SwipeableContent className="main-content admin-audit-root">
<MobileTitleBar
title={t.auditPage.title}
menuLabel={t.theme.toggleMenu}
onMenuClick={toggleMobileMenu}
/>
<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}>
<span className="material-symbols-outlined">menu</span>
</button>
<div className="page-title-section"> <div className="page-title-section">
<span className="page-title-text">{t.auditPage.title}</span> <span className="page-title-text">{t.auditPage.title}</span>
</div> </div>

View File

@@ -5,6 +5,7 @@ import { useSidebar } from '../../contexts/SidebarContext';
import { useModules, TOGGLEABLE_MODULES } from '../../contexts/ModulesContext'; import { useModules, TOGGLEABLE_MODULES } from '../../contexts/ModulesContext';
import type { ModuleId } from '../../contexts/ModulesContext'; import type { ModuleId } from '../../contexts/ModulesContext';
import Feature1Tab from '../../components/admin/Feature1Tab'; import Feature1Tab from '../../components/admin/Feature1Tab';
import MobileTitleBar from '../../components/MobileTitleBar';
import { SwipeTabs } from '../../components/SwipeTabs'; import { SwipeTabs } from '../../components/SwipeTabs';
import TabsScroller from '../../components/TabsScroller'; import TabsScroller from '../../components/TabsScroller';
import '../../styles/AdminPanel.css'; import '../../styles/AdminPanel.css';
@@ -780,11 +781,13 @@ export default function Features() {
return ( return (
<main className="main-content admin-panel-root"> <main className="main-content admin-panel-root">
<MobileTitleBar
title={t.featuresPage.title}
menuLabel={t.theme.toggleMenu}
onMenuClick={toggleMobileMenu}
/>
<div className="page-tabs-container"> <div className="page-tabs-container">
<TabsScroller className="page-tabs-slider" ref={tabsContainerRef} showArrows> <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"> <div className="page-title-section">
<span className="page-title-text">{t.featuresPage.title}</span> <span className="page-title-text">{t.featuresPage.title}</span>
</div> </div>

View File

@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
import { useTranslation } from '../../contexts/LanguageContext'; 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 MobileTitleBar from '../../components/MobileTitleBar';
import TabsScroller from '../../components/TabsScroller'; import TabsScroller from '../../components/TabsScroller';
import { SwipeableContent } from '../../components/SwipeableContent'; import { SwipeableContent } from '../../components/SwipeableContent';
import { settingsAPI } from '../../api/client'; import { settingsAPI } from '../../api/client';
@@ -61,11 +62,13 @@ export default function Settings() {
<div className="app-layout"> <div className="app-layout">
<Sidebar /> <Sidebar />
<SwipeableContent className="main-content settings-root"> <SwipeableContent className="main-content settings-root">
<MobileTitleBar
title={t.settings.title}
menuLabel={t.theme.toggleMenu}
onMenuClick={toggleMobileMenu}
/>
<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}>
<span className="material-symbols-outlined">menu</span>
</button>
<div className="page-title-section"> <div className="page-title-section">
<span className="page-title-text">{t.settings.title}</span> <span className="page-title-text">{t.settings.title}</span>
</div> </div>

View File

@@ -1,6 +1,7 @@
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';
import MobileTitleBar from '../../components/MobileTitleBar';
import TabsScroller from '../../components/TabsScroller'; import TabsScroller from '../../components/TabsScroller';
import { SwipeableContent } from '../../components/SwipeableContent'; import { SwipeableContent } from '../../components/SwipeableContent';
import '../../styles/AdminPanel.css'; import '../../styles/AdminPanel.css';
@@ -16,11 +17,14 @@ export default function Sources() {
return ( return (
<SwipeableContent className="main-content admin-panel-root"> <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"> <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}>
<span className="material-symbols-outlined">menu</span>
</button>
<div className="admin-title-section"> <div className="admin-title-section">
<span className="admin-title-text">{t.sourcesPage.title}</span> <span className="admin-title-text">{t.sourcesPage.title}</span>
</div> </div>

View File

@@ -6,6 +6,7 @@ import { useAuth } from '../../contexts/AuthContext';
import { useSidebar } from '../../contexts/SidebarContext'; import { useSidebar } from '../../contexts/SidebarContext';
import type { SidebarMode } from '../../contexts/SidebarContext'; import type { SidebarMode } from '../../contexts/SidebarContext';
import { ChromePicker, HuePicker } from 'react-color'; import { ChromePicker, HuePicker } from 'react-color';
import MobileTitleBar from '../../components/MobileTitleBar';
import { SwipeTabs } from '../../components/SwipeTabs'; import { SwipeTabs } from '../../components/SwipeTabs';
import TabsScroller from '../../components/TabsScroller'; import TabsScroller from '../../components/TabsScroller';
import '../../styles/ThemeSettings.css'; import '../../styles/ThemeSettings.css';
@@ -778,12 +779,14 @@ export default function ThemeSettings() {
{tooltip.text} {tooltip.text}
</div> </div>
)} )}
<MobileTitleBar
title={t.theme.title}
menuLabel={t.theme.toggleMenu}
onMenuClick={toggleMobileMenu}
/>
{/* Modern Tab Navigation */} {/* Modern Tab Navigation */}
<div className="page-tabs-container"> <div className="page-tabs-container">
<TabsScroller className="page-tabs-slider" showArrows> <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"> <div className="page-title-section">
<span className="page-title-text">{t.theme.title}</span> <span className="page-title-text">{t.theme.title}</span>
</div> </div>

View File

@@ -37,6 +37,15 @@
padding: 0.75rem; padding: 0.75rem;
} }
.page-title-container,
.admin-title-container {
display: none;
}
.mobile-title-slider {
width: 100%;
}
/* Ensure no extra margin from body */ /* Ensure no extra margin from body */
body { body {
margin: 0; margin: 0;
@@ -442,14 +451,78 @@ label,
margin-left: 0; 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 */ /* Show mobile menu button with logo */
.mobile-menu-btn { .mobile-menu-btn {
display: flex; display: flex;
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
z-index: 1;
background-image: url('/logo_black.svg'); background-image: url('/logo_black.svg');
background-size: 28px 28px; background-size: 28px 28px;
background-repeat: no-repeat; background-repeat: no-repeat;
@@ -473,40 +546,16 @@ label,
padding: 0.75rem; padding: 0.75rem;
} }
.page-tabs-slider, .page-tabs-container .page-title-section,
.admin-tabs-slider { .page-tabs-container .admin-title-section,
width: 100%; .admin-tabs-container .page-title-section,
flex-wrap: nowrap; .admin-tabs-container .admin-title-section {
justify-content: flex-start; display: none;
gap: 4px;
position: relative;
min-height: 48px;
} }
.tabs-scroll-shell { .page-tabs-container[data-has-tabs='false']:not([data-has-actions='true']),
width: 100%; .admin-tabs-container[data-has-tabs='false']:not([data-has-actions='true']) {
} display: none;
.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;
} }
/* Hide divider on mobile */ /* Hide divider on mobile */
@@ -515,103 +564,27 @@ label,
display: none; 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 { .page-content {
padding: var(--page-padding-y-mobile) var(--page-padding-x-mobile); 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 { .admin-tab-content {
padding: var(--page-padding-y-mobile) var(--page-padding-x-mobile); 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 */ /* Small mobile - further reduce spacing */
@media (max-width: 480px) { @media (max-width: 480px) {
.page-tabs-container,
.admin-tabs-container {
padding: var(--page-padding-y-mobile) var(--page-padding-x-mobile);
}
.page-content { .page-content {
padding: var(--page-padding-y-mobile) var(--page-padding-x-mobile); 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 { .admin-tab-content {
padding: var(--page-padding-y-mobile) var(--page-padding-x-mobile); 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) { @media (max-width: 768px) {
[data-tab-position='top'] .page-tabs-container, [data-tab-position='top'] .page-tabs-container,
[data-tab-position='top'] .admin-tabs-container { [data-tab-position='top'] .admin-tabs-container {
top: var(--title-bar-offset, var(--title-bar-height, 0px));
left: 0; left: 0;
right: 0; right: 0;
width: 100%; width: 100%;
padding-top: 0;
} }
[data-tab-position='top'][data-sidebar-collapsed='true'] .page-tabs-container, [data-tab-position='top'][data-sidebar-collapsed='true'] .page-tabs-container,
@@ -860,9 +835,14 @@ label,
right: 0; right: 0;
} }
[data-tab-position='top'] .page-content, [data-tab-position='top'] .main-content[data-has-tabs='true'] .page-content,
[data-tab-position='top'] .admin-tab-content { [data-tab-position='top'] .main-content[data-has-actions='true'] .page-content,
padding-top: calc(var(--page-padding-y-mobile) + var(--tabs-bar-height, 72px)); [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); left: var(--sidebar-width);
right: 0; right: 0;
z-index: 100; z-index: 100;
padding: 0.5rem; padding: 0.75rem;
padding-bottom: calc(0.5rem + env(safe-area-inset-bottom, 0px)); padding-bottom: calc(0.75rem + env(safe-area-inset-bottom, 0px));
} }
/* Handle collapsed sidebar */ /* Handle collapsed sidebar */
@@ -908,13 +888,6 @@ label,
left: var(--sidebar-width-collapsed); 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 */ /* Add bottom padding and remove top padding when bar is at bottom */
[data-tab-position='bottom'] .main-content[data-has-tabs='true'] { [data-tab-position='bottom'] .main-content[data-has-tabs='true'] {
padding-bottom: 0; padding-bottom: 0;
@@ -958,7 +931,7 @@ label,
/* Mobile padding for bottom position */ /* 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'] .page-content,
[data-tab-position='bottom'] .main-content[data-has-tabs='true'] .admin-tab-swipe { [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, [data-tab-position='bottom'] .main-content[data-has-tabs='true'] .page-content,
@@ -981,23 +954,8 @@ label,
left: 0; left: 0;
right: 0; right: 0;
z-index: 100; z-index: 100;
padding: 0.5rem; padding: 0.75rem;
padding-bottom: calc(0.5rem + env(safe-area-inset-bottom, 0px)); padding-bottom: calc(0.75rem + 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;
} }
/* Hide divider when bar is at bottom */ /* 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'] .page-content,
[data-tab-position='responsive'] .main-content[data-has-tabs='true'] .admin-tab-swipe { [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, [data-tab-position='responsive'] .main-content[data-has-tabs='true'] .page-content,

View File

@@ -139,9 +139,12 @@
/* Kept same */ /* Kept same */
/* Tab Sizes */ /* 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 */ /* Reduced from 0.75rem 1.25rem */
--tab-font-size: 0.95rem; --tab-font-size: 0.95rem;
--tab-pill-height: calc((var(--tab-font-size) * 1.2) + (var(--tab-padding-y) * 2));
/* Input Sizes */ /* Input Sizes */
--input-padding: 0.625rem 0.875rem; --input-padding: 0.625rem 0.875rem;