From 906d8821a477961af26d2d6b787b00746798a868 Mon Sep 17 00:00:00 2001 From: Maurice Date: Tue, 21 Apr 2026 22:10:11 +0200 Subject: [PATCH] fix: offline banner no longer covers the top of the app (#813) OfflineBanner was fixed at top:0 but the rest of the page had no idea it was visible, so on mobile (and the desktop nav on wider screens) the banner sat on top of the header content. When the banner is visible it now sets --offline-banner-h on ; body reserves that space via padding-top, and the desktop fixed Navbar shifts its top by the same amount. When back online the var is removed and everything snaps back. --- client/src/components/Layout/Navbar.tsx | 1 + client/src/components/Layout/OfflineBanner.tsx | 16 ++++++++++++++++ client/src/index.css | 6 +++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/client/src/components/Layout/Navbar.tsx b/client/src/components/Layout/Navbar.tsx index 1933982c..a8ec8f5c 100644 --- a/client/src/components/Layout/Navbar.tsx +++ b/client/src/components/Layout/Navbar.tsx @@ -89,6 +89,7 @@ export default function Navbar({ tripTitle, tripId, onBack, showBack, onShare }: touchAction: 'manipulation', paddingTop: 'env(safe-area-inset-top, 0px)', height: 'var(--nav-h)', + top: 'var(--offline-banner-h, 0px)', transition: 'background 240ms cubic-bezier(0.23,1,0.32,1), backdrop-filter 240ms cubic-bezier(0.23,1,0.32,1), box-shadow 240ms cubic-bezier(0.23,1,0.32,1)', }} className="hidden md:flex items-center px-4 gap-4 fixed top-0 left-0 right-0 z-[200]"> {/* Left side */} diff --git a/client/src/components/Layout/OfflineBanner.tsx b/client/src/components/Layout/OfflineBanner.tsx index 04b58cf1..5c3b8010 100644 --- a/client/src/components/Layout/OfflineBanner.tsx +++ b/client/src/components/Layout/OfflineBanner.tsx @@ -40,6 +40,22 @@ export default function OfflineBanner(): React.ReactElement | null { }, []) const hidden = isOnline && pendingCount === 0 + + // When the banner is visible, reserve space at the top of the page so it + // doesn't cover the nav/header. Uses a CSS var on so we can offset + // via a global `body` rule instead of rewiring every layout. + useEffect(() => { + const root = document.documentElement + if (hidden) { + root.style.removeProperty('--offline-banner-h') + } else { + // 32px for icon+text row + the top safe-area inset that the banner adds + // in its own padding. Kept in one place so it's easy to tweak. + root.style.setProperty('--offline-banner-h', 'calc(env(safe-area-inset-top, 0px) + 32px)') + } + return () => { root.style.removeProperty('--offline-banner-h') } + }, [hidden]) + if (hidden) return null const offline = !isOnline diff --git a/client/src/index.css b/client/src/index.css index 4332ffdc..e67665c8 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -431,6 +431,8 @@ input[type="number"], input[type="time"], input[type="date"], input[type="dateti --safe-top: env(safe-area-inset-top, 0px); --nav-h: 0px; --bottom-nav-h: 0px; + /* Set by OfflineBanner when it's visible so body can reserve space. */ + --offline-banner-h: 0px; --font-system: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', system-ui, sans-serif; --sp-1: 4px; --sp-2: 8px; @@ -536,7 +538,9 @@ body { font-family: var(--font-system); background-color: var(--bg-primary); color: var(--text-primary); - transition: background-color 0.2s, color 0.2s; + transition: background-color 0.2s, color 0.2s, padding-top 0.15s ease; + /* Reserve space when OfflineBanner is visible; 0 when online. */ + padding-top: var(--offline-banner-h, 0px); } /* ── Marker cluster custom styling ────────────── */