From ec726da79777267546ec8b79ed8ed0578d4d2ae5 Mon Sep 17 00:00:00 2001 From: matteoscrugli Date: Tue, 23 Dec 2025 22:54:22 +0100 Subject: [PATCH] Fix page content padding consistency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Restore bottom padding for .page-content and .admin-tab-content across all breakpoints (was accidentally set to 0) - Change bottom tab bar padding from max() to calc(padding + bar-height) so content has both regular margin AND space for fixed tab bar 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- frontend/src/components/SwipeTabs.tsx | 27 ++++- frontend/src/components/TabsScroller.tsx | 18 +++- frontend/src/styles/Layout.css | 122 ++++++++++------------- 3 files changed, 97 insertions(+), 70 deletions(-) diff --git a/frontend/src/components/SwipeTabs.tsx b/frontend/src/components/SwipeTabs.tsx index 53f9487..ae89b8f 100644 --- a/frontend/src/components/SwipeTabs.tsx +++ b/frontend/src/components/SwipeTabs.tsx @@ -72,6 +72,7 @@ export function SwipeTabs({ }: SwipeTabsProps) { const containerRef = useRef(null); const trackRef = useRef(null); + const activePanelRef = useRef(null); const activeIndex = useMemo(() => { const index = tabs.indexOf(activeTab); return index >= 0 ? index : 0; @@ -79,6 +80,7 @@ export function SwipeTabs({ const [displayIndex, setDisplayIndex] = useState(activeIndex); const displayIndexRef = useRef(displayIndex); + const [containerHeight, setContainerHeight] = useState(null); const widthRef = useRef(0); const baseOffsetRef = useRef(0); const dragOffsetRef = useRef(0); @@ -135,6 +137,13 @@ export function SwipeTabs({ } }, []); + const updateHeight = useCallback(() => { + const node = activePanelRef.current; + if (!node) return; + const nextHeight = Math.ceil(node.getBoundingClientRect().height); + setContainerHeight((prev) => (prev === nextHeight ? prev : nextHeight)); + }, []); + const measureWidth = useCallback(() => { const width = containerRef.current?.getBoundingClientRect().width || 0; if (width && width !== widthRef.current) { @@ -171,6 +180,18 @@ export function SwipeTabs({ applyTransform(0, false); }, [applyTransform, displayIndex]); + useLayoutEffect(() => { + const node = activePanelRef.current; + if (!node) return undefined; + updateHeight(); + if (typeof ResizeObserver === 'undefined') return undefined; + const observer = new ResizeObserver(() => updateHeight()); + observer.observe(node); + return () => { + observer.disconnect(); + }; + }, [displayIndex, updateHeight]); + useEffect(() => { return () => { if (rafRef.current) { @@ -540,6 +561,7 @@ export function SwipeTabs({
({ const key = tab !== null ? String(tab) : `empty-${slotIndex}`; return (
-
+
{tab !== null ? renderPanel(tab) : null}
diff --git a/frontend/src/components/TabsScroller.tsx b/frontend/src/components/TabsScroller.tsx index 8e6a517..58e1063 100644 --- a/frontend/src/components/TabsScroller.tsx +++ b/frontend/src/components/TabsScroller.tsx @@ -40,6 +40,7 @@ const TabsScroller = forwardRef( const rafRef = useRef(0); const [showLeft, setShowLeft] = useState(false); const [showRight, setShowRight] = useState(false); + const heightRef = useRef(0); const dragRef = useRef({ pointerId: null as number | null, @@ -70,10 +71,21 @@ const TabsScroller = forwardRef( const hasTabsValue = hasTabButtons ? 'true' : 'false'; node.dataset.hasTabs = hasTabsValue; 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; + containerHeight = Math.ceil(container.getBoundingClientRect().height); + } + if (containerHeight && containerHeight !== heightRef.current) { + heightRef.current = containerHeight; + if (typeof document !== 'undefined') { + document.documentElement.style.setProperty('--tabs-bar-height', `${containerHeight}px`); + } + if (mainContent instanceof HTMLElement) { + mainContent.style.setProperty('--tabs-bar-height', `${containerHeight}px`); + } } - const mainContent = node.closest('.main-content'); if (mainContent instanceof HTMLElement) { mainContent.dataset.hasTabs = hasTabsValue; } @@ -104,6 +116,7 @@ const TabsScroller = forwardRef( updateOverflow(); const node = sliderRef.current; if (!node) return undefined; + const container = node.closest('.page-tabs-container, .admin-tabs-container'); const handleScroll = () => scheduleOverflowUpdate(); node.addEventListener('scroll', handleScroll, { passive: true }); @@ -111,6 +124,9 @@ const TabsScroller = forwardRef( if (typeof ResizeObserver !== 'undefined') { resizeObserver = new ResizeObserver(() => scheduleOverflowUpdate()); resizeObserver.observe(node); + if (container instanceof HTMLElement) { + resizeObserver.observe(container); + } } else if (typeof window !== 'undefined') { window.addEventListener('resize', scheduleOverflowUpdate); } diff --git a/frontend/src/styles/Layout.css b/frontend/src/styles/Layout.css index c6c443e..26422f8 100644 --- a/frontend/src/styles/Layout.css +++ b/frontend/src/styles/Layout.css @@ -29,31 +29,12 @@ /* ========== PAGE STRUCTURE ========== */ -/* Page header container - fixed at top */ +/* Page header container */ .page-tabs-container, .admin-tabs-container { display: flex; justify-content: center; padding: 0.75rem; - position: fixed; - top: 0; - left: var(--sidebar-width); - right: 0; - z-index: 100; -} - -/* Handle collapsed sidebar for fixed header (desktop only) */ -@media (min-width: 769px) { - [data-sidebar-collapsed='true'] .page-tabs-container, - [data-sidebar-collapsed='true'] .admin-tabs-container { - left: var(--sidebar-width-collapsed); - } -} - -/* Add top padding to content to account for fixed header */ -.page-content, -.admin-tab-swipe { - padding-top: calc(var(--page-padding-y) + 60px); } /* Ensure no extra margin from body */ @@ -394,7 +375,7 @@ label, /* Main content wrapper - with symmetric padding from sidebar edge to window edge */ .page-content { - padding: var(--page-padding-y) var(--page-padding-x) 0; + padding: var(--page-padding-y) var(--page-padding-x); width: 100%; max-width: var(--page-max-width); margin: 0 auto; @@ -409,7 +390,7 @@ label, /* Admin tab content (for tabbed interfaces) */ .admin-tab-content { - padding: var(--page-padding-y) var(--page-padding-x) 0; + padding: var(--page-padding-y) var(--page-padding-x); max-width: var(--page-max-width); margin: 0 auto; width: 100%; @@ -445,13 +426,8 @@ label, padding: var(--page-padding-y-tablet) var(--page-padding-x-tablet); } - .page-content, - .admin-tab-swipe { - padding-top: calc(var(--page-padding-y-tablet) + 60px); - } - .admin-tab-content { - padding: var(--page-padding-y-tablet) var(--page-padding-x-tablet) 0; + padding: var(--page-padding-y-tablet) var(--page-padding-x-tablet); } } @@ -495,12 +471,6 @@ label, .page-tabs-container, .admin-tabs-container { padding: 0.75rem; - left: 0; - } - - .page-content, - .admin-tab-swipe { - padding-top: calc(var(--page-padding-y-mobile) + 60px); } .page-tabs-slider, @@ -620,11 +590,11 @@ label, } .page-content { - padding: var(--page-padding-y-mobile) var(--page-padding-x-mobile) 0; + padding: var(--page-padding-y-mobile) var(--page-padding-x-mobile); } .admin-tab-content { - padding: var(--page-padding-y-mobile) var(--page-padding-x-mobile) 0; + padding: var(--page-padding-y-mobile) var(--page-padding-x-mobile); } } @@ -637,11 +607,11 @@ label, } .page-content { - padding: var(--page-padding-y-mobile) var(--page-padding-x-mobile) 0; + padding: var(--page-padding-y-mobile) var(--page-padding-x-mobile); } .admin-tab-content { - padding: var(--page-padding-y-mobile) var(--page-padding-x-mobile) 0; + padding: var(--page-padding-y-mobile) var(--page-padding-x-mobile); } } @@ -859,8 +829,8 @@ label, /* Bottom position - fixed at bottom (global setting for all pages) */ /* Note: Primary positioning is handled by TabsScroller component with inline styles. These CSS rules serve as fallback/additional styling. */ -[data-tab-position='bottom'] .page-tabs-container, -[data-tab-position='bottom'] .admin-tabs-container { +[data-tab-position='bottom'] .page-tabs-container[data-has-tabs='true'], +[data-tab-position='bottom'] .admin-tabs-container[data-has-tabs='true'] { position: fixed; top: auto; bottom: 0; @@ -872,54 +842,62 @@ label, } /* Handle collapsed sidebar */ -[data-tab-position='bottom'][data-sidebar-collapsed='true'] .page-tabs-container, -[data-tab-position='bottom'][data-sidebar-collapsed='true'] .admin-tabs-container { +[data-tab-position='bottom'][data-sidebar-collapsed='true'] .page-tabs-container[data-has-tabs='true'], +[data-tab-position='bottom'][data-sidebar-collapsed='true'] .admin-tabs-container[data-has-tabs='true'] { left: var(--sidebar-width-collapsed); } /* Adjust slider styling for bottom position */ -[data-tab-position='bottom'] .page-tabs-slider, -[data-tab-position='bottom'] .admin-tabs-slider { +[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 { - padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px)); +[data-tab-position='bottom'] .main-content[data-has-tabs='true'] { + padding-bottom: 0; } -[data-tab-position='bottom'] .page-content, -[data-tab-position='bottom'] .admin-tab-swipe { +[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); } +[data-tab-position='bottom'] .main-content[data-has-tabs='true'] .page-content, +[data-tab-position='bottom'] .main-content[data-has-tabs='true'] .admin-tab-content { + padding-bottom: calc( + var(--page-padding-y) + + var(--tabs-bar-height, calc(72px + env(safe-area-inset-bottom, 0px))) + ); +} + /* Mobile: full width at bottom */ @media (max-width: 768px) { - [data-tab-position='bottom'] .page-tabs-container, - [data-tab-position='bottom'] .admin-tabs-container { + [data-tab-position='bottom'] .page-tabs-container[data-has-tabs='true'], + [data-tab-position='bottom'] .admin-tabs-container[data-has-tabs='true'] { left: 0; } /* Override collapsed state on mobile */ - [data-tab-position='bottom'][data-sidebar-collapsed='true'] .page-tabs-container, - [data-tab-position='bottom'][data-sidebar-collapsed='true'] .admin-tabs-container { + [data-tab-position='bottom'][data-sidebar-collapsed='true'] .page-tabs-container[data-has-tabs='true'], + [data-tab-position='bottom'][data-sidebar-collapsed='true'] .admin-tabs-container[data-has-tabs='true'] { left: 0; } /* Hide divider on mobile when bar is at bottom */ - [data-tab-position='bottom'] .page-tabs-slider .page-tabs-divider, - [data-tab-position='bottom'] .page-tabs-slider .admin-tabs-divider, - [data-tab-position='bottom'] .admin-tabs-slider .page-tabs-divider, - [data-tab-position='bottom'] .admin-tabs-slider .admin-tabs-divider { + [data-tab-position='bottom'] .page-tabs-slider[data-has-tabs='true'] .page-tabs-divider, + [data-tab-position='bottom'] .page-tabs-slider[data-has-tabs='true'] .admin-tabs-divider, + [data-tab-position='bottom'] .admin-tabs-slider[data-has-tabs='true'] .page-tabs-divider, + [data-tab-position='bottom'] .admin-tabs-slider[data-has-tabs='true'] .admin-tabs-divider { display: none; } } /* Responsive position - top on desktop, bottom on mobile */ @media (max-width: 768px) { - [data-tab-position='responsive'] .page-tabs-container, - [data-tab-position='responsive'] .admin-tabs-container { + [data-tab-position='responsive'] .page-tabs-container[data-has-tabs='true'], + [data-tab-position='responsive'] .admin-tabs-container[data-has-tabs='true'] { position: fixed; top: auto; bottom: 0; @@ -930,8 +908,8 @@ label, padding-bottom: calc(0.5rem + env(safe-area-inset-bottom, 0px)); } - [data-tab-position='responsive'] .page-tabs-slider, - [data-tab-position='responsive'] .admin-tabs-slider { + [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 */ @@ -946,20 +924,28 @@ label, } /* Hide divider when bar is at bottom */ - [data-tab-position='responsive'] .page-tabs-slider .page-tabs-divider, - [data-tab-position='responsive'] .page-tabs-slider .admin-tabs-divider, - [data-tab-position='responsive'] .admin-tabs-slider .page-tabs-divider, - [data-tab-position='responsive'] .admin-tabs-slider .admin-tabs-divider { + [data-tab-position='responsive'] .page-tabs-slider[data-has-tabs='true'] .page-tabs-divider, + [data-tab-position='responsive'] .page-tabs-slider[data-has-tabs='true'] .admin-tabs-divider, + [data-tab-position='responsive'] .admin-tabs-slider[data-has-tabs='true'] .page-tabs-divider, + [data-tab-position='responsive'] .admin-tabs-slider[data-has-tabs='true'] .admin-tabs-divider { display: none; } /* Add bottom padding and remove top padding */ - [data-tab-position='responsive'] .main-content { - padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px)); + [data-tab-position='responsive'] .main-content[data-has-tabs='true'] { + padding-bottom: 0; } - [data-tab-position='responsive'] .page-content, - [data-tab-position='responsive'] .admin-tab-swipe { + [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); } + + [data-tab-position='responsive'] .main-content[data-has-tabs='true'] .page-content, + [data-tab-position='responsive'] .main-content[data-has-tabs='true'] .admin-tab-content { + padding-bottom: calc( + var(--page-padding-y-mobile) + + var(--tabs-bar-height, calc(72px + env(safe-area-inset-bottom, 0px))) + ); + } }