Centre the offline banner over the content column

This commit is contained in:
2026-08-22 12:09:18 +02:00
parent 09e2e5e133
commit 3674099758
2 changed files with 25 additions and 25 deletions
@@ -1,7 +1,6 @@
import { WifiOff } from "lucide-react"
import { AnimatePresence, motion } from "motion/react"
import { useSyncExternalStore } from "react"
import { createPortal } from "react-dom"
import { ago } from "@/components/Health/queries"
import { connectionStore } from "@/lib/connectionStore"
@@ -17,10 +16,11 @@ import { isPortal } from "@/lib/portal"
* stale readings with a timestamp are more use than an empty page, which is
* why the banner leads with when we last heard anything.
*
* Drawn on `document.body` rather than where it is mounted. `position: fixed`
* resolves against the nearest ancestor carrying a transform, a filter or a
* `backdrop-filter`, and the shell floats several of those — inside them the
* banner centres on the content column instead of the viewport.
* Centred over the content column rather than the window: it belongs to what
* is being read, not to the shell around it. That is what `sticky` inside the
* content column buys — no sidebar width to track and nothing to recompute
* when the sidebar collapses — and `h-0` keeps it out of the flow, so nothing
* below it moves when the banner appears.
*/
export function ConnectionBanner() {
const connection = useSyncExternalStore(
@@ -30,19 +30,19 @@ export function ConnectionBanner() {
)
if (!isPortal()) return null
return createPortal(
<AnimatePresence>
{connection.offline && (
<motion.div
variants={fadeIn}
initial="hidden"
animate="visible"
exit="hidden"
role="status"
aria-live="polite"
className="pointer-events-none fixed inset-x-0 top-4 z-50 flex justify-center px-4"
>
<div className="flex items-center gap-2 rounded-full border border-border bg-card/80 px-4 py-2 text-sm shadow-e2 backdrop-blur-md">
return (
<div className="pointer-events-none sticky top-4 z-50 flex h-0 justify-center px-4">
<AnimatePresence>
{connection.offline && (
<motion.div
variants={fadeIn}
initial="hidden"
animate="visible"
exit="hidden"
role="status"
aria-live="polite"
className="flex items-center gap-2 rounded-full border border-border bg-card/80 px-4 py-2 text-sm shadow-e2 backdrop-blur-md"
>
<WifiOff className="size-4 shrink-0 text-muted-foreground" />
<span className="font-medium">Installation offline</span>
<span className="text-muted-foreground">
@@ -50,10 +50,9 @@ export function ConnectionBanner() {
? `last seen ${ago(connection.lastSeen / 1000)} — reconnecting…`
: "reconnecting…"}
</span>
</div>
</motion.div>
)}
</AnimatePresence>,
document.body,
</motion.div>
)}
</AnimatePresence>
</div>
)
}
+3 -2
View File
@@ -29,10 +29,11 @@ function Layout() {
return (
<SidebarProvider className="bg-card">
{/* Renders nothing unless this page is served through a portal. */}
<ConnectionBanner />
<AppSidebar />
<SidebarInset className="bg-card">
{/* Renders nothing unless this page is served through a portal.
Inside the inset so it centres over the content, not the window. */}
<ConnectionBanner />
{/* The sidebar carries its own collapse control; a phone has no
sidebar on screen to carry it. */}
<header className="sticky top-0 z-10 flex h-16 shrink-0 items-center gap-2 px-4 md:hidden">