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