"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 ? (