From 757764d0467ac78a249a48cf369cd152fa1d477e Mon Sep 17 00:00:00 2001 From: Maurice Date: Tue, 21 Apr 2026 22:30:50 +0200 Subject: [PATCH] hotfix: offline banner as bottom pill instead of full-width top bar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The top bar still blocked the trip planner's top nav on mobile even after #808's padding trick — nav layouts that position their own sticky headers were ignoring the --offline-banner-h offset, and the bar looked alarming for what is usually a 2s blip. Redesign as a small floating pill anchored bottom-center, hovering above the mobile bottom nav (calc(var(--bottom-nav-h) + 16px)). No layout shift anywhere, nothing ever covers the nav, and the pill looks like a passing status chip rather than an error banner. Reverts the body padding-top / navbar top offset introduced in #808 since they're no longer needed with the pill positioning. --- client/src/components/Layout/Navbar.tsx | 1 - .../src/components/Layout/OfflineBanner.tsx | 62 ++++++++----------- client/src/index.css | 6 +- 3 files changed, 27 insertions(+), 42 deletions(-) diff --git a/client/src/components/Layout/Navbar.tsx b/client/src/components/Layout/Navbar.tsx index 36d543c1..5eb7e6b0 100644 --- a/client/src/components/Layout/Navbar.tsx +++ b/client/src/components/Layout/Navbar.tsx @@ -103,7 +103,6 @@ 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 5c3b8010..34b57e18 100644 --- a/client/src/components/Layout/OfflineBanner.tsx +++ b/client/src/components/Layout/OfflineBanner.tsx @@ -1,11 +1,15 @@ /** - * OfflineBanner — persistent top bar indicating connectivity + sync state. + * OfflineBanner — connectivity + sync state indicator. * * States: - * offline + N queued → amber bar "Offline — N changes queued" - * offline + 0 queued → amber bar "Offline" - * online + N pending → blue bar "Syncing N changes…" + * offline + N queued → amber pill "Offline · N queued" + * offline + 0 queued → amber pill "Offline" + * online + N pending → blue pill "Syncing N…" * online + 0 pending → hidden + * + * Rendered as a small floating pill anchored to the bottom-center of the + * viewport so it never competes with top navigation or sticky modal + * headers. On mobile it hovers just above the bottom tab bar. */ import React, { useState, useEffect } from 'react' import { WifiOff, RefreshCw } from 'lucide-react' @@ -40,22 +44,6 @@ 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 @@ -64,9 +52,9 @@ export default function OfflineBanner(): React.ReactElement | null { const label = offline ? pendingCount > 0 - ? `Offline — ${pendingCount} change${pendingCount !== 1 ? 's' : ''} queued` + ? `Offline · ${pendingCount} queued` : 'Offline' - : `Syncing ${pendingCount} change${pendingCount !== 1 ? 's' : ''}…` + : `Syncing ${pendingCount}…` return (
{offline - ? - : + ? + : } {label}
diff --git a/client/src/index.css b/client/src/index.css index e67665c8..4332ffdc 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -431,8 +431,6 @@ 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; @@ -538,9 +536,7 @@ body { font-family: var(--font-system); background-color: var(--bg-primary); color: var(--text-primary); - 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); + transition: background-color 0.2s, color 0.2s; } /* ── Marker cluster custom styling ────────────── */