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',
|
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 */}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 ────────────── */
|
||||||
|
|||||||
Reference in New Issue
Block a user