fix(ui): every full-screen overlay hosts its own toasts, above the players' controls, without re-announcing them

Deep-review round 3 of bug hunt 2026-09 (the round-2 overlay toast host, finished).

- Inside an overlay the toast stack sits above the bottom band (bottom-24, the UpNextCard rung) and
  lets clicks between cards through: at the app's own spot it covered the Plex transport row and
  PlayerModal's docked bar and took their clicks while a toast was up (measured: play/pause, ±10,
  stop, prev/next, mute all hit the toast).
- ui/Modal and the onboarding wizard mount an overlay host too, so a toast raised under any dialog
  is readable and a click on it no longer closes the dialog; the Modal's Tab trap spans the whole
  dialog, so a toast's action and dismiss are keyboard-reachable. A test now enforces the
  convention: a component that covers the viewport at z-overlay must mount <Toaster overlay />.
- PlayerModal's host moves into the stage in true fullscreen (outside the element holding the lock it
  was painted under it).
- A host that takes over shows the toasts it inherits silently (no second role=alert, its region
  aria-live=off until they are in), and each countdown bar starts as far in as its toast already is
  instead of restarting while the real dismiss timer does not.
- The overlay-host registry uses lib/store instead of a hand-rolled listener triad.
This commit is contained in:
2026-09-11 18:04:54 +02:00
parent 5c5cabbe5a
commit 33fe895539
9 changed files with 228 additions and 41 deletions
@@ -7,6 +7,7 @@ import { api, type Me } from "../lib/api";
import { beginGrant, endOnboarding } from "../lib/onboarding";
import { Button } from "./ui/Button";
import Overlay from "./ui/Overlay";
import Toaster from "./ui/Toaster";
// A first-login wizard that walks the user through granting YouTube access one scope at a
// time, each with a plain explanation and an up-front heads-up about Google's "unverified
@@ -262,6 +263,9 @@ export default function OnboardingWizard({ me, onClose }: { me: Me; onClose: ()
<Dots index={stepIndex} total={3} />
</div>
</div>
{/* Toasts raised while the wizard is up (the connection notice among them) show above its
scrim, not under it — see ui/Toaster. */}
<Toaster overlay />
</div>
</Overlay>
);