diff --git a/src/components/stories/StoryViewer.tsx b/src/components/stories/StoryViewer.tsx index 61b1493..6831746 100644 --- a/src/components/stories/StoryViewer.tsx +++ b/src/components/stories/StoryViewer.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect, useCallback, useRef } from "react"; +import { createPortal } from "react-dom"; import dynamic from "next/dynamic"; import { X, Eye, Volume2, VolumeX, Loader2 } from "lucide-react"; import { useTranslations } from "next-intl"; @@ -65,6 +66,12 @@ export function StoryViewer({ const [isMuted, setIsMuted] = useState(true); const advancedRef = useRef(false); + // Portal target must wait for mount (document isn't available during SSR). + const [mounted, setMounted] = useState(false); + useEffect(() => { + setMounted(true); + }, []); + const { data: stories = [] } = useQuery( trpc.stories.listActive.queryOptions({ petId }) ); @@ -183,8 +190,14 @@ export function StoryViewer({ return ( <> - {/* Full-screen overlay */} - {open && ( + {/* Full-screen overlay — portaled to document.body so `position: fixed` + escapes the (app) layout's
ancestor. + A fixed descendant of an overflow-auto container can, on some mobile + browsers (confirmed on Android Chrome), size itself against that + container's full scrollable content instead of the real viewport — + the story then renders many times taller than the screen and only + a small top slice is visible, looking like a heavily zoomed crop. */} + {open && mounted && createPortal(
{viewers.length} )} -
+ , + document.body )} {/* Viewer list sheet — owner only */}