hotfix: offline banner as bottom pill instead of full-width top bar

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.
This commit is contained in:
Maurice
2026-04-21 22:30:50 +02:00
parent 94e64acc34
commit 757764d046
3 changed files with 27 additions and 42 deletions
-1
View File
@@ -103,7 +103,6 @@ export default function Navbar({ tripTitle, tripId, onBack, showBack, onShare }:
touchAction: 'manipulation', touchAction: 'manipulation',
paddingTop: 'env(safe-area-inset-top, 0px)', paddingTop: 'env(safe-area-inset-top, 0px)',
height: 'var(--nav-h)', 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)', 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]"> }} className="hidden md:flex items-center px-4 gap-4 fixed top-0 left-0 right-0 z-[200]">
{/* Left side */} {/* Left side */}
+26 -36
View File
@@ -1,11 +1,15 @@
/** /**
* OfflineBanner — persistent top bar indicating connectivity + sync state. * OfflineBanner — connectivity + sync state indicator.
* *
* States: * States:
* offline + N queued → amber bar "Offline N changes queued" * offline + N queued → amber pill "Offline · N queued"
* offline + 0 queued → amber bar "Offline" * offline + 0 queued → amber pill "Offline"
* online + N pending → blue bar "Syncing N changes…" * online + N pending → blue pill "Syncing N…"
* online + 0 pending → hidden * 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 React, { useState, useEffect } from 'react'
import { WifiOff, RefreshCw } from 'lucide-react' import { WifiOff, RefreshCw } from 'lucide-react'
@@ -40,22 +44,6 @@ export default function OfflineBanner(): React.ReactElement | null {
}, []) }, [])
const hidden = isOnline && pendingCount === 0 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 <html> 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 if (hidden) return null
const offline = !isOnline const offline = !isOnline
@@ -64,9 +52,9 @@ export default function OfflineBanner(): React.ReactElement | null {
const label = offline const label = offline
? pendingCount > 0 ? pendingCount > 0
? `Offline ${pendingCount} change${pendingCount !== 1 ? 's' : ''} queued` ? `Offline · ${pendingCount} queued`
: 'Offline' : 'Offline'
: `Syncing ${pendingCount} change${pendingCount !== 1 ? 's' : ''}` : `Syncing ${pendingCount}`
return ( return (
<div <div
@@ -74,27 +62,29 @@ export default function OfflineBanner(): React.ReactElement | null {
aria-live="polite" aria-live="polite"
style={{ style={{
position: 'fixed', position: 'fixed',
top: 0, // Hover above the mobile bottom nav; on desktop --bottom-nav-h is 0,
left: 0, // so the pill sits 16px from the bottom.
right: 0, bottom: 'calc(var(--bottom-nav-h) + 16px)',
left: '50%',
transform: 'translateX(-50%)',
zIndex: 9999, zIndex: 9999,
background: bg, background: bg,
color: text, color: text,
display: 'flex', display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', gap: 6,
gap: 8, padding: '6px 14px',
paddingTop: 'calc(env(safe-area-inset-top, 0px) + 6px)', borderRadius: 999,
paddingBottom: '6px', boxShadow: '0 4px 16px rgba(0,0,0,0.18), 0 0 0 1px rgba(255,255,255,0.08)',
paddingLeft: '16px', fontSize: 12,
paddingRight: '16px', fontWeight: 600,
fontSize: 13, whiteSpace: 'nowrap',
fontWeight: 500, pointerEvents: 'none',
}} }}
> >
{offline {offline
? <WifiOff size={14} /> ? <WifiOff size={12} />
: <RefreshCw size={14} style={{ animation: 'spin 1s linear infinite' }} /> : <RefreshCw size={12} style={{ animation: 'spin 1s linear infinite' }} />
} }
{label} {label}
</div> </div>
+1 -5
View File
@@ -431,8 +431,6 @@ input[type="number"], input[type="time"], input[type="date"], input[type="dateti
--safe-top: env(safe-area-inset-top, 0px); --safe-top: env(safe-area-inset-top, 0px);
--nav-h: 0px; --nav-h: 0px;
--bottom-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; --font-system: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', system-ui, sans-serif;
--sp-1: 4px; --sp-1: 4px;
--sp-2: 8px; --sp-2: 8px;
@@ -538,9 +536,7 @@ body {
font-family: var(--font-system); font-family: var(--font-system);
background-color: var(--bg-primary); background-color: var(--bg-primary);
color: var(--text-primary); color: var(--text-primary);
transition: background-color 0.2s, color 0.2s, padding-top 0.15s ease; transition: background-color 0.2s, color 0.2s;
/* Reserve space when OfflineBanner is visible; 0 when online. */
padding-top: var(--offline-banner-h, 0px);
} }
/* ── Marker cluster custom styling ────────────── */ /* ── Marker cluster custom styling ────────────── */