diff --git a/README.md b/README.md index d010c4b..b4be02e 100644 --- a/README.md +++ b/README.md @@ -1369,7 +1369,9 @@ Things I chose while building, with the reasoning: homework wording (no tips — the tips are deck-specific), and the route screens pass a single message with no stages or progress. Cancel stays a 44px touch target and the icon animation respects `prefers-reduced-motion`. Verified by the freeze-loading, review, wizard and mobile - suites. + suites. The link overlay is portalled to ``: inside a glass card `backdrop-filter` makes + the card a containing block for `position: fixed`, so the overlay was squeezed into a 44px + strip of the card instead of covering the viewport (the review suite now measures it). 59. **Adding a deck or a homework syncs itself.** Sync used to be push-on-demand only: a deck frozen on the laptop sat in `dirty` until someone opened Settings and pressed Sync now. The two diff --git a/components/LinkLoadingScreen.tsx b/components/LinkLoadingScreen.tsx index c3d1a51..3837d95 100644 --- a/components/LinkLoadingScreen.tsx +++ b/components/LinkLoadingScreen.tsx @@ -1,6 +1,7 @@ "use client"; import { useLinkStatus } from "next/link"; +import { createPortal } from "react-dom"; import { RouteLoading } from "@/components/RouteLoading"; @@ -9,15 +10,21 @@ import { RouteLoading } from "@/components/RouteLoading"; * `` starts navigating — before any response arrives — so a slow route * never looks like a dead tap. Renders nothing while idle and disappears with * the page that owns it. + * + * Portalled to : the link sits inside a glass card, and `backdrop-filter` + * makes such an ancestor a containing block for `position: fixed` — rendered + * in place, the overlay was confined to a 44px strip of the card instead of the + * viewport. */ export function LinkLoadingScreen({ messageKey }: { messageKey: string }) { const { pending } = useLinkStatus(); - if (!pending) return null; + if (!pending || typeof document === "undefined") return null; - return ( -
+ return createPortal( +
-
+
, + document.body, ); } diff --git a/public/sw.js b/public/sw.js index 289a99f..b083a25 100644 --- a/public/sw.js +++ b/public/sw.js @@ -9,7 +9,7 @@ * * Bumping CACHE_VERSION is how a new deploy invalidates old assets. */ -const CACHE_VERSION = "freezmo-v47"; +const CACHE_VERSION = "freezmo-v48"; const SHELL_CACHE = `${CACHE_VERSION}-shell`; const ASSET_CACHE = `${CACHE_VERSION}-assets`; diff --git a/tests/review-test.mjs b/tests/review-test.mjs index d8d649a..a8cc54a 100644 --- a/tests/review-test.mjs +++ b/tests/review-test.mjs @@ -54,6 +54,19 @@ async function makeReviewPage(locale = "en") { check("loading screen appears immediately on tap", loadingAt !== null && loadingAt - tClick < 1000, `${loadingAt ? loadingAt - tClick : "never"} ms`); check("still on the library while stalled", page.url().replace(BASE, "") === "/", page.url()); check("spinner is visible", (await page.getByRole("status").count()) >= 1); + /* The overlay is portalled to : rendered inside the glass card it was + confined to a 44px strip of that card (backdrop-filter is a containing + block for fixed descendants). */ + const overlay = await page.evaluate(() => { + const status = document.querySelector('[role="status"][aria-busy="true"]'); + const rect = status?.parentElement?.getBoundingClientRect(); + return rect ? { w: Math.round(rect.width), h: Math.round(rect.height) } : null; + }); + check( + "loading overlay covers the viewport", + overlay !== null && overlay.w >= 380 && overlay.h >= 800, + JSON.stringify(overlay), + ); release(); let sessionAt = null;