diff --git a/components/album/progressive-image.tsx b/components/album/progressive-image.tsx index 1d54b818..f63cc4e0 100644 --- a/components/album/progressive-image.tsx +++ b/components/album/progressive-image.tsx @@ -2,6 +2,7 @@ import type { ProgressiveImageProps } from '~/types/props.ts' import { useEffect, useState, useRef, Activity } from 'react' +import { createPortal } from 'react-dom' import { useTranslations } from 'next-intl' import { MotionImage } from '~/components/album/motion-image' import { useBlurImageDataUrl } from '~/hooks/use-blurhash' @@ -237,7 +238,14 @@ export default function ProgressiveImage( view, and pointer-events are disabled while hidden so the invisible overlay never blocks the page. Mounted lazily on first open so the engine isn't built until the user actually zooms. */} - {hasOpenedFullScreen && props.keepViewerMounted !== false && ( + {/* Portal the fullscreen overlay to so its `fixed inset-0` + positioning is relative to the viewport. When the detail page is + opened as the intercepted-route modal, an ancestor (Radix + DialogContent) carries a `translate(-50%,-50%)` transform, which + would otherwise become the containing block for `position: fixed` + and trap the viewer inside the dialog box → blank fullscreen. The + guard keeps it inert during SSR. */} + {hasOpenedFullScreen && props.keepViewerMounted !== false && typeof document !== 'undefined' && createPortal( webGLAvailable ?
-
+ , + document.body )} ) : null}