From ca47077944a58d4321e8f4c274c72df01fef0a9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 8 Oct 2026 21:29:26 +0200 Subject: [PATCH] fix(pwa): lift toasts above the bottom bar wherever the phone shell shows it The toaster's mobile offset applies below 600 px only, while the bottom bar follows the shell up to md and on a touch screen held sideways. In between, the update toast and its reload button sat over the tabs. --- src/__tests__/pwa-mobile-shell-guard.test.ts | 9 +++++++++ src/app/globals.css | 15 +++++++++++++++ src/components/providers.tsx | 5 +++-- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/__tests__/pwa-mobile-shell-guard.test.ts b/src/__tests__/pwa-mobile-shell-guard.test.ts index 22558121e..0daf812ef 100644 --- a/src/__tests__/pwa-mobile-shell-guard.test.ts +++ b/src/__tests__/pwa-mobile-shell-guard.test.ts @@ -60,6 +60,15 @@ describe("the shell owns the safe area", () => { expect(css).toMatch(/@custom-variant pointer-fine/); }); + it("bottom toasts clear the bar under the same query the bar follows", () => { + // Sonner's own mobile offset stops at 600 px; the bar shows up to `md` + // and on a phone held sideways, so the lift keys on `shell-mobile`. + const css = code(read("src/app/globals.css")); + expect(css).toMatch( + /html \[data-sonner-toaster\]\[data-y-position="bottom"\]\s*\{\s*@variant shell-mobile\s*\{\s*bottom:\s*calc\(4rem \+ env\(safe-area-inset-bottom/, + ); + }); + it("the bottom bar and the Coach button step aside for the keyboard", () => { expect(read("src/components/layout/bottom-nav.tsx")).toContain( 'data-keyboard-hide=""', diff --git a/src/app/globals.css b/src/app/globals.css index 9f8b8e3b3..6d230f1fe 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -628,6 +628,21 @@ html[data-keyboard="open"] [data-keyboard-hide] { display: none !important; } +/* + * Toasts clear the bottom bar wherever the phone shell shows it. The + * toaster's `mobileOffset` (providers.tsx) applies below 600 px only, while + * the bar follows the shell (up to `md`, and a phone held sideways): between + * the two the "new version" toast sat over the tabs. Unlayered and one `html` + * deeper than Sonner's injected `[data-sonner-toaster][data-y-position]` + * rule, which is unlayered too and lands after this sheet. The Coach panel's + * rule further down still outranks it while that panel is open. + */ +html [data-sonner-toaster][data-y-position="bottom"] { + @variant shell-mobile { + bottom: calc(4rem + env(safe-area-inset-bottom, 0px) + 0.75rem); + } +} + /* * Side and top sheets run the full height of the window. In an installed * app on a tablet the status bar sits over their first 24 px, and a phone diff --git a/src/components/providers.tsx b/src/components/providers.tsx index 55e3ec559..07d77b6dc 100644 --- a/src/components/providers.tsx +++ b/src/components/providers.tsx @@ -392,8 +392,9 @@ function OfflineMutationToaster() { * Below 600 px Sonner lays toasts across the bottom edge, 16 px up, which on * a phone is on top of the bottom bar: the "new version" hint sat over the * tabs and the add button until it was dismissed. The mobile offset lifts - * them above the bar and the home indicator. From `md` the bar is gone and - * the desktop offset applies. + * them above the bar and the home indicator. Sonner applies it below 600 px + * only, while the bar follows the shell (up to `md`, and a phone held + * sideways), so `globals.css` repeats the lift under `shell-mobile`. */ function AppToaster() { const { resolvedTheme } = useTheme();