"use client"
import { useLayoutEffect, useRef, useState } from "react"
import { cn } from "@/lib/utils"
export type ScaledPreviewFit = "contain" | "cover"
export type ScaledPreviewIframeProps = {
src: string
title: string
viewportWidth: number
viewportHeight: number
ready?: boolean
active?: boolean
/**
* contain = letterbox si le ratio parent ≠ ratio viewport ;
* cover = remplit (recadrage éventuel, seulement les coins si ratios proches).
* Idéalement le parent porte un aspect-ratio = viewportWidth/viewportHeight,
* auquel cas contain et cover sont équivalents (aucune coupe).
*/
fit?: ScaledPreviewFit
/** Débord de sécurité (px) en mode cover — évite les hairlines aux bords/coins. */
coverEpsilonPx?: number
maxScale?: number
className?: string
placeholderClassName?: string
}
/**
* Iframe à viewport logique fixe (rendu « grand écran »), affichée à la taille
* du parent. Le `transform: scale` est porté par un
wrapper dimensionné
* au viewport ; l'iframe est en `absolute inset-0` et remplit ce wrapper à
* 100 % (WebKit scale mal une iframe directement). Aucun aspect-ratio n'est
* posé sur l'iframe : c'est au(x) parent(s) redimensionnable(s) de le porter.
*/
export function ScaledPreviewIframe({
src,
title,
viewportWidth,
viewportHeight,
ready = true,
active = true,
fit = "cover",
coverEpsilonPx = 1,
maxScale,
className,
placeholderClassName,
}: ScaledPreviewIframeProps) {
const containerRef = useRef
(null)
/** null = pas encore mesuré (évite un iframe à scale par défaut au 1er paint). */
const [scale, setScale] = useState(null)
useLayoutEffect(() => {
if (!ready) {
setScale(null)
return
}
const node = containerRef.current
if (!node) return
const update = () => {
// offsetWidth/Height = taille de layout SANS les transforms des ancêtres.
const width = node.offsetWidth
const height = node.offsetHeight
if (width <= 0 || height <= 0) return
let next: number
if (fit === "cover") {
const eps = coverEpsilonPx
next = Math.max(
(width + eps) / viewportWidth,
(height + eps) / viewportHeight
)
} else {
next = Math.min(width / viewportWidth, height / viewportHeight)
}
if (maxScale !== undefined) next = Math.min(next, maxScale)
setScale(next)
}
update()
const raf1 = requestAnimationFrame(() => {
update()
requestAnimationFrame(update)
})
const observer = new ResizeObserver(update)
observer.observe(node)
window.addEventListener("load", update, { once: true })
return () => {
cancelAnimationFrame(raf1)
observer.disconnect()
window.removeEventListener("load", update)
}
}, [ready, viewportWidth, viewportHeight, fit, coverEpsilonPx, maxScale])
return (
{ready && scale !== null ? (
) : (
Chargement…
)}
)
}