From 6075e96bd05aee2f5fd5cec22d4af17a38442bd6 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sat, 18 Jul 2026 19:11:38 +0200 Subject: [PATCH] refactor(overlay): single overlay-root convention via Overlay helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add components/Overlay.tsx (portals to ) and route the in-tree full-screen/ floating holdouts through it — ChatDock, OnboardingWizard, GlassTuner — so they escape any ancestor stacking context (mask/transform/backdrop-filter) instead of relying on a bare high z-index. Toaster stays in-tree (it's content-column-scoped by design, outside the masked scroller); Welcome is a page, not an overlay. --- frontend/src/components/ChatDock.tsx | 15 +++++++++------ frontend/src/components/GlassTuner.tsx | 5 ++++- frontend/src/components/OnboardingWizard.tsx | 5 ++++- frontend/src/components/Overlay.tsx | 14 ++++++++++++++ 4 files changed, 31 insertions(+), 8 deletions(-) create mode 100644 frontend/src/components/Overlay.tsx diff --git a/frontend/src/components/ChatDock.tsx b/frontend/src/components/ChatDock.tsx index 6304563..21e6c6e 100644 --- a/frontend/src/components/ChatDock.tsx +++ b/frontend/src/components/ChatDock.tsx @@ -7,6 +7,7 @@ import { onMessage, onUnread } from "../lib/messagesSocket"; import * as e2ee from "../lib/e2ee"; import Avatar from "./Avatar"; import ChatThread from "./ChatThread"; +import Overlay from "./Overlay"; // The Messenger-style floating chat dock: open conversations live here, bottom-right, across // page navigation (state persisted per user). Always mounted (for human users) so it also owns @@ -51,11 +52,13 @@ export default function ChatDock({ meId }: { meId: number }) { if (chats.length === 0) return null; return ( -
- {chats.map((c) => ( - - ))} -
+ +
+ {chats.map((c) => ( + + ))} +
+
); } @@ -65,7 +68,7 @@ function ChatWindow({ chat, meId }: { chat: DockChat; meId: number }) {
{/* One-shot attention flash on a new message; keyed by the counter so it replays each time. */} {chat.flash > 0 && ( -
+
)}
)}
+ ); } diff --git a/frontend/src/components/OnboardingWizard.tsx b/frontend/src/components/OnboardingWizard.tsx index 2a7a8d7..e8ef79e 100644 --- a/frontend/src/components/OnboardingWizard.tsx +++ b/frontend/src/components/OnboardingWizard.tsx @@ -4,6 +4,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { AlertTriangle, Check, Loader2, RefreshCw, ShieldCheck, X, Youtube } from "lucide-react"; import { api, type Me } from "../lib/api"; import { beginGrant, endOnboarding } from "../lib/onboarding"; +import Overlay from "./Overlay"; // 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 @@ -122,7 +123,8 @@ export default function OnboardingWizard({ me, onClose }: { me: Me; onClose: () const stepIndex = step === "read" ? 0 : step === "write" ? 1 : 2; return ( -
+ +
+ ); } diff --git a/frontend/src/components/Overlay.tsx b/frontend/src/components/Overlay.tsx new file mode 100644 index 0000000..41730ab --- /dev/null +++ b/frontend/src/components/Overlay.tsx @@ -0,0 +1,14 @@ +import { type ReactNode } from "react"; +import { createPortal } from "react-dom"; + +// The app's overlay-root convention. Any full-screen or floating overlay renders THROUGH this so it +// escapes whatever ancestor stacking context it was declared in — a `mask-image`/`transform`/`filter` +// on the page scroller (or a rail's `backdrop-filter`) would otherwise trap a `fixed` child and let +// the chrome paint over it. That is the exact class of bug that buried the players in E3; the +// modal/player family already dodges it by portaling to , and this makes that the one blessed +// way. Stacking among overlays is then decided by DOM order + the z-index token scale (tailwind +// `zIndex`), never by an ad-hoc magic number. Portals to (the existing de-facto target). +export default function Overlay({ children }: { children: ReactNode }) { + if (typeof document === "undefined") return null; + return createPortal(children, document.body); +}