@@ -12,6 +12,14 @@ interface PreviewViewportProps {
1212 isLoading ?: boolean ;
1313 loadingText ?: string ;
1414 fluidLayout ?: boolean ; // For content-based sizing (code snippet)
15+ onViewportMetricsChange ?: ( metrics : {
16+ scale : number ;
17+ containerWidth : number ;
18+ containerHeight : number ;
19+ scaledWidth : number ;
20+ scaledHeight : number ;
21+ bottomWhitespace : number ;
22+ } ) => void ;
1523}
1624
1725const DEFAULT_SURFACE = {
@@ -27,12 +35,31 @@ export function PreviewViewport({
2735 isLoading = false ,
2836 loadingText = "Loading..." ,
2937 fluidLayout = false ,
38+ onViewportMetricsChange,
3039} : PreviewViewportProps ) {
3140 const containerRef = useRef < HTMLDivElement | null > ( null ) ;
3241 const [ scale , setScale ] = useState ( 1 ) ;
3342 const [ hasMeasured , setHasMeasured ] = useState ( false ) ;
3443 const rafRef = useRef < number | undefined > ( undefined ) ;
3544
45+ const reportMetrics = useCallback (
46+ ( nextScale : number , containerWidth : number , containerHeight : number ) => {
47+ const scaledWidth = surfaceWidth * nextScale ;
48+ const scaledHeight = surfaceHeight * nextScale ;
49+ const bottomWhitespace = Math . max ( 0 , containerHeight - scaledHeight ) ;
50+
51+ onViewportMetricsChange ?.( {
52+ scale : nextScale ,
53+ containerWidth,
54+ containerHeight,
55+ scaledWidth,
56+ scaledHeight,
57+ bottomWhitespace,
58+ } ) ;
59+ } ,
60+ [ onViewportMetricsChange , surfaceHeight , surfaceWidth ]
61+ ) ;
62+
3663 // Throttled scale update using requestAnimationFrame
3764 const updateScale = useCallback ( ( ) => {
3865 if ( rafRef . current ) return ; // Already scheduled
@@ -42,17 +69,18 @@ export function PreviewViewport({
4269 if ( ! containerRef . current ) return ;
4370 const containerWidth = containerRef . current . clientWidth ;
4471 const containerHeight = containerRef . current . clientHeight ;
45- if ( ! containerWidth || ! containerHeight || ! surfaceWidth || ! surfaceHeight ) return ;
72+ if ( ! containerWidth || ! surfaceWidth || ! surfaceHeight ) return ;
4673
4774 // Scale to fit both width and height, ensuring entire canvas is visible without scrolling
4875 const scaleX = containerWidth / surfaceWidth ;
49- const scaleY = containerHeight / surfaceHeight ;
76+ const scaleY = containerHeight ? containerHeight / surfaceHeight : Number . POSITIVE_INFINITY ;
5077 const nextScale = Math . min ( scaleX , scaleY , 1 ) ;
5178
5279 setScale ( nextScale ) ;
5380 setHasMeasured ( true ) ;
81+ reportMetrics ( nextScale , containerWidth , containerHeight ) ;
5482 } ) ;
55- } , [ surfaceWidth , surfaceHeight ] ) ;
83+ } , [ surfaceWidth , surfaceHeight , reportMetrics ] ) ;
5684
5785 useLayoutEffect ( ( ) => {
5886 if ( fluidLayout ) {
@@ -66,13 +94,14 @@ export function PreviewViewport({
6694 // Initial scale calculation - fit both width and height
6795 const containerWidth = node . clientWidth ;
6896 const containerHeight = node . clientHeight ;
69- if ( containerWidth && containerHeight && surfaceWidth && surfaceHeight ) {
97+ if ( containerWidth && surfaceWidth && surfaceHeight ) {
7098 const scaleX = containerWidth / surfaceWidth ;
71- const scaleY = containerHeight / surfaceHeight ;
99+ const scaleY = containerHeight ? containerHeight / surfaceHeight : Number . POSITIVE_INFINITY ;
72100 const nextScale = Math . min ( scaleX , scaleY , 1 ) ;
73101
74102 setScale ( nextScale ) ;
75103 setHasMeasured ( true ) ;
104+ reportMetrics ( nextScale , containerWidth , containerHeight ) ;
76105 }
77106
78107 const observer = new ResizeObserver ( updateScale ) ;
@@ -82,7 +111,7 @@ export function PreviewViewport({
82111 observer . disconnect ( ) ;
83112 if ( rafRef . current ) cancelAnimationFrame ( rafRef . current ) ;
84113 } ;
85- } , [ surfaceWidth , surfaceHeight , updateScale , fluidLayout ] ) ;
114+ } , [ surfaceWidth , surfaceHeight , updateScale , fluidLayout , reportMetrics ] ) ;
86115
87116 // Fluid layout: content determines size, no fixed canvas
88117 if ( fluidLayout ) {
0 commit comments