mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
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:
@@ -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 */}
|
||||
|
||||
@@ -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 <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
|
||||
|
||||
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 (
|
||||
<div
|
||||
@@ -74,27 +62,29 @@ export default function OfflineBanner(): React.ReactElement | null {
|
||||
aria-live="polite"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
// Hover above the mobile bottom nav; on desktop --bottom-nav-h is 0,
|
||||
// so the pill sits 16px from the bottom.
|
||||
bottom: 'calc(var(--bottom-nav-h) + 16px)',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
zIndex: 9999,
|
||||
background: bg,
|
||||
color: text,
|
||||
display: 'flex',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
paddingTop: 'calc(env(safe-area-inset-top, 0px) + 6px)',
|
||||
paddingBottom: '6px',
|
||||
paddingLeft: '16px',
|
||||
paddingRight: '16px',
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
gap: 6,
|
||||
padding: '6px 14px',
|
||||
borderRadius: 999,
|
||||
boxShadow: '0 4px 16px rgba(0,0,0,0.18), 0 0 0 1px rgba(255,255,255,0.08)',
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
whiteSpace: 'nowrap',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
{offline
|
||||
? <WifiOff size={14} />
|
||||
: <RefreshCw size={14} style={{ animation: 'spin 1s linear infinite' }} />
|
||||
? <WifiOff size={12} />
|
||||
: <RefreshCw size={12} style={{ animation: 'spin 1s linear infinite' }} />
|
||||
}
|
||||
{label}
|
||||
</div>
|
||||
|
||||
@@ -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 ────────────── */
|
||||
|
||||
Reference in New Issue
Block a user