From 97494679ec61c157accad1ae120ec249557b543f Mon Sep 17 00:00:00 2001 From: matteoscrugli Date: Mon, 5 Jan 2026 16:52:34 +0100 Subject: [PATCH] Add separate MobileTitleBar component for mobile layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- frontend/src/components/MobileTitleBar.tsx | 171 ++++++++++++++ frontend/src/components/TabsScroller.tsx | 19 ++ frontend/src/pages/APIKeys.tsx | 9 +- frontend/src/pages/AdminPanel.tsx | 9 +- frontend/src/pages/Dashboard.tsx | 9 +- frontend/src/pages/Feature1.tsx | 9 +- frontend/src/pages/Feature2.tsx | 9 +- frontend/src/pages/Feature3.tsx | 9 +- frontend/src/pages/Notifications.tsx | 9 +- frontend/src/pages/Search.tsx | 9 +- frontend/src/pages/Settings.tsx | 9 +- frontend/src/pages/Users.tsx | 9 +- frontend/src/pages/admin/Analytics.tsx | 9 +- frontend/src/pages/admin/AuditLogs.tsx | 9 +- frontend/src/pages/admin/Features.tsx | 9 +- frontend/src/pages/admin/Settings.tsx | 9 +- frontend/src/pages/admin/Sources.tsx | 10 +- frontend/src/pages/admin/ThemeSettings.tsx | 9 +- frontend/src/styles/Layout.css | 254 +++++++++------------ frontend/src/styles/theme/dimensions.css | 5 +- 20 files changed, 397 insertions(+), 197 deletions(-) create mode 100644 frontend/src/components/MobileTitleBar.tsx diff --git a/frontend/src/components/MobileTitleBar.tsx b/frontend/src/components/MobileTitleBar.tsx new file mode 100644 index 0000000..caa8db7 --- /dev/null +++ b/frontend/src/components/MobileTitleBar.tsx @@ -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(null); + const menuButtonRef = useRef(null); + const mainContentRef = useRef(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 ( +
+
+ +
+ {title} +
+
+
+ ); +}; + +export default MobileTitleBar; diff --git a/frontend/src/components/TabsScroller.tsx b/frontend/src/components/TabsScroller.tsx index 58e1063..3b545cd 100644 --- a/frontend/src/components/TabsScroller.tsx +++ b/frontend/src/components/TabsScroller.tsx @@ -41,6 +41,7 @@ const TabsScroller = forwardRef( 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( 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( } if (mainContent instanceof HTMLElement) { mainContent.dataset.hasTabs = hasTabsValue; + mainContent.dataset.hasActions = hasActionsValue; } if (!showArrows) { setShowLeft(false); diff --git a/frontend/src/pages/APIKeys.tsx b/frontend/src/pages/APIKeys.tsx index da106f9..ce6ca89 100644 --- a/frontend/src/pages/APIKeys.tsx +++ b/frontend/src/pages/APIKeys.tsx @@ -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 ( +
-
{t.apiKeysPage.title}
diff --git a/frontend/src/pages/AdminPanel.tsx b/frontend/src/pages/AdminPanel.tsx index 61ab03f..c09ba55 100644 --- a/frontend/src/pages/AdminPanel.tsx +++ b/frontend/src/pages/AdminPanel.tsx @@ -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 (
+
-
{t.admin.panel}
diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index e900107..6c8e608 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -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 ( +
-
{t.dashboard.title}
diff --git a/frontend/src/pages/Feature1.tsx b/frontend/src/pages/Feature1.tsx index 6dbbdf1..d294e21 100644 --- a/frontend/src/pages/Feature1.tsx +++ b/frontend/src/pages/Feature1.tsx @@ -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 ( +
-
{t.feature1.title}
diff --git a/frontend/src/pages/Feature2.tsx b/frontend/src/pages/Feature2.tsx index ede4210..c3878ce 100644 --- a/frontend/src/pages/Feature2.tsx +++ b/frontend/src/pages/Feature2.tsx @@ -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 ( +
-
{t.features.feature2}
diff --git a/frontend/src/pages/Feature3.tsx b/frontend/src/pages/Feature3.tsx index 4f04356..95e7cad 100644 --- a/frontend/src/pages/Feature3.tsx +++ b/frontend/src/pages/Feature3.tsx @@ -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 ( +
-
{t.features.feature3}
diff --git a/frontend/src/pages/Notifications.tsx b/frontend/src/pages/Notifications.tsx index 4cd8e3f..a396503 100644 --- a/frontend/src/pages/Notifications.tsx +++ b/frontend/src/pages/Notifications.tsx @@ -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 ( +
-
{t.notificationsPage.title}
diff --git a/frontend/src/pages/Search.tsx b/frontend/src/pages/Search.tsx index 3f1e77e..fae9802 100644 --- a/frontend/src/pages/Search.tsx +++ b/frontend/src/pages/Search.tsx @@ -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 ( +
-
{t.searchPage.title}
diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index 55183a6..8fe30d9 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -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 ( +
-
{t.settings.title}
diff --git a/frontend/src/pages/Users.tsx b/frontend/src/pages/Users.tsx index df414c5..c0c4d44 100644 --- a/frontend/src/pages/Users.tsx +++ b/frontend/src/pages/Users.tsx @@ -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() {
+
-
{t.admin.userManagement}
diff --git a/frontend/src/pages/admin/Analytics.tsx b/frontend/src/pages/admin/Analytics.tsx index 0022438..4eb52a7 100644 --- a/frontend/src/pages/admin/Analytics.tsx +++ b/frontend/src/pages/admin/Analytics.tsx @@ -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 ( +
-
{t.analyticsPage.title}
diff --git a/frontend/src/pages/admin/AuditLogs.tsx b/frontend/src/pages/admin/AuditLogs.tsx index 32a43fd..993aed1 100644 --- a/frontend/src/pages/admin/AuditLogs.tsx +++ b/frontend/src/pages/admin/AuditLogs.tsx @@ -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 ( +
-
{t.auditPage.title}
diff --git a/frontend/src/pages/admin/Features.tsx b/frontend/src/pages/admin/Features.tsx index 6a84fb4..1de965a 100644 --- a/frontend/src/pages/admin/Features.tsx +++ b/frontend/src/pages/admin/Features.tsx @@ -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 (
+
-
{t.featuresPage.title}
diff --git a/frontend/src/pages/admin/Settings.tsx b/frontend/src/pages/admin/Settings.tsx index cf55e15..7122d8c 100644 --- a/frontend/src/pages/admin/Settings.tsx +++ b/frontend/src/pages/admin/Settings.tsx @@ -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() {
+
-
{t.settings.title}
diff --git a/frontend/src/pages/admin/Sources.tsx b/frontend/src/pages/admin/Sources.tsx index 8606bd1..19e3835 100644 --- a/frontend/src/pages/admin/Sources.tsx +++ b/frontend/src/pages/admin/Sources.tsx @@ -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 ( +
-
{t.sourcesPage.title}
diff --git a/frontend/src/pages/admin/ThemeSettings.tsx b/frontend/src/pages/admin/ThemeSettings.tsx index dfd4d1c..9407ace 100644 --- a/frontend/src/pages/admin/ThemeSettings.tsx +++ b/frontend/src/pages/admin/ThemeSettings.tsx @@ -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}
)} + {/* Modern Tab Navigation */}
-
{t.theme.title}
diff --git a/frontend/src/styles/Layout.css b/frontend/src/styles/Layout.css index 4d23263..ceaa6c7 100644 --- a/frontend/src/styles/Layout.css +++ b/frontend/src/styles/Layout.css @@ -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, diff --git a/frontend/src/styles/theme/dimensions.css b/frontend/src/styles/theme/dimensions.css index 4e668f7..5b6826c 100644 --- a/frontend/src/styles/theme/dimensions.css +++ b/frontend/src/styles/theme/dimensions.css @@ -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;