100 lines
3.4 KiB
TypeScript
100 lines
3.4 KiB
TypeScript
/**
|
|
* TransitionPreviewOverlay Component
|
|
*
|
|
* Full-screen overlay for transition video preview.
|
|
* Designed to work with useTransitionPlayback hook which manages
|
|
* video src and playback externally via the videoRef.
|
|
*
|
|
* Supports letterbox mode to constrain transitions within canvas bounds,
|
|
* matching the behavior of background images and UI elements.
|
|
*/
|
|
|
|
import React from 'react';
|
|
import CanvasLoadingSpinner from '../CanvasLoadingSpinner';
|
|
|
|
interface TransitionPreviewOverlayProps {
|
|
/** Reference to the video element - useTransitionPlayback manages src and playback */
|
|
videoRef: React.RefObject<HTMLVideoElement | null>;
|
|
/** Whether the overlay is visible */
|
|
isActive: boolean;
|
|
/** Whether the video is currently buffering (used to hide video during load) */
|
|
isBuffering?: boolean;
|
|
/** Show loading spinner during buffering (default: false for backward compat) */
|
|
showSpinner?: boolean;
|
|
/** Loading message for spinner */
|
|
spinnerMessage?: string;
|
|
/** Letterbox styles from useCanvasScale - positions overlay within canvas bounds */
|
|
letterboxStyles?: React.CSSProperties;
|
|
/** Video object-fit mode (default: 'contain' to match backgrounds) */
|
|
videoFit?: 'contain' | 'cover';
|
|
/** Additional opacity value for fade-out effects (0-1) */
|
|
opacity?: number;
|
|
/** Forces video element remount when changed - prevents decoder state issues with pre-created blob URLs */
|
|
videoKey?: string;
|
|
}
|
|
|
|
const TransitionPreviewOverlay: React.FC<TransitionPreviewOverlayProps> = ({
|
|
videoRef,
|
|
isActive,
|
|
isBuffering = false,
|
|
showSpinner = false,
|
|
spinnerMessage = 'Preparing transition...',
|
|
letterboxStyles,
|
|
videoFit = 'contain',
|
|
opacity,
|
|
videoKey,
|
|
}) => {
|
|
if (!isActive) return null;
|
|
|
|
// Container opacity: 0 while buffering to prevent black flash
|
|
// Video first frame = old page background, so we hide everything until ready
|
|
const containerOpacity = isBuffering ? 0 : (opacity ?? 1);
|
|
|
|
return (
|
|
// Outer: full viewport, transparent background
|
|
// Transparent ensures if Safari clears video frame when paused,
|
|
// the new page background shows through instead of black flash
|
|
<div className='fixed inset-0 z-50 overflow-hidden pointer-events-none'>
|
|
{/* Loading spinner during buffering - provides user feedback */}
|
|
{isBuffering && showSpinner && (
|
|
<CanvasLoadingSpinner
|
|
isVisible={true}
|
|
message={spinnerMessage}
|
|
size='lg'
|
|
zIndex={60}
|
|
/>
|
|
)}
|
|
|
|
{/* Video container - hidden while buffering */}
|
|
<div
|
|
style={{
|
|
opacity: containerOpacity,
|
|
transition: 'opacity 150ms ease-out',
|
|
}}
|
|
>
|
|
{/* Inner: respects letterbox dimensions when provided */}
|
|
<div
|
|
className='overflow-hidden'
|
|
style={letterboxStyles || { position: 'absolute', inset: 0 }}
|
|
>
|
|
{/* Video element - container handles visibility, video is always opaque */}
|
|
{/* key forces React to remount the video element when URL changes, clearing decoder state */}
|
|
<video
|
|
key={videoKey}
|
|
ref={videoRef}
|
|
className={`absolute inset-0 h-full w-full ${
|
|
videoFit === 'cover' ? 'object-cover' : 'object-contain'
|
|
}`}
|
|
muted
|
|
playsInline
|
|
preload='auto'
|
|
disablePictureInPicture
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default TransitionPreviewOverlay;
|