| 1 | "use client"; |
| 2 | |
| 3 | import { useLayoutEffect, useRef } from "react"; |
| 4 | |
| 5 | import { acquireBrowserSurface } from "./browserSurfaceStore"; |
| 6 | import { observeResize } from "../lib/observeResize"; |
| 7 | |
| 8 | export function BrowserSurfaceSlot(props: { |
| 9 | readonly tabId: string; |
| 10 | readonly visible: boolean; |
| 11 | readonly cornerRadius?: number; |
| 12 | readonly zIndex?: number; |
| 13 | readonly layoutVersion?: string | number; |
| 14 | readonly className?: string; |
| 15 | readonly fitSourceContent?: boolean; |
| 16 | }) { |
| 17 | const { |
| 18 | tabId, |
| 19 | visible, |
| 20 | cornerRadius = 0, |
| 21 | zIndex = 30, |
| 22 | layoutVersion, |
| 23 | className, |
| 24 | fitSourceContent = false, |
| 25 | } = props; |
| 26 | const elementRef = useRef<HTMLDivElement | null>(null); |
| 27 | const presentationRef = useRef({ visible, cornerRadius, zIndex }); |
| 28 | const updateRef = useRef<(() => void) | null>(null); |
| 29 | |
| 30 | useLayoutEffect(() => { |
| 31 | const element = elementRef.current; |
| 32 | if (!element) return; |
| 33 | let lease = acquireBrowserSurface(tabId, fitSourceContent); |
| 34 | const update = () => { |
| 35 | const rect = element.getBoundingClientRect(); |
| 36 | const presentation = presentationRef.current; |
| 37 | const sheet = element.closest('[data-slot="sheet-viewport"]'); |
| 38 | const sheetLayer = sheet ? Number(window.getComputedStyle(sheet).zIndex) : 0; |
| 39 | const zIndex = Math.max(presentation.zIndex, sheetLayer + 1); |
| 40 | const presented = lease.present( |
| 41 | { |
| 42 | x: Math.round(rect.x), |
| 43 | y: Math.round(rect.y), |
| 44 | width: Math.max(1, Math.round(rect.width)), |
| 45 | height: Math.max(1, Math.round(rect.height)), |
| 46 | }, |
| 47 | presentation.visible && rect.width > 0 && rect.height > 0, |
| 48 | presentation.cornerRadius, |
| 49 | zIndex, |
| 50 | ); |
| 51 | if (presentation.visible && !presented) { |
| 52 | lease.release(); |
| 53 | lease = acquireBrowserSurface(tabId, fitSourceContent); |
| 54 | lease.present( |
| 55 | { |
| 56 | x: Math.round(rect.x), |
| 57 | y: Math.round(rect.y), |
| 58 | width: Math.max(1, Math.round(rect.width)), |
| 59 | height: Math.max(1, Math.round(rect.height)), |
| 60 | }, |
| 61 | rect.width > 0 && rect.height > 0, |
| 62 | presentation.cornerRadius, |
| 63 | zIndex, |
| 64 | ); |
| 65 | } |
| 66 | }; |
| 67 | updateRef.current = update; |
| 68 | update(); |
| 69 | const observed: Element[] = [element]; |
| 70 | // Inline panels animate their outer width while keeping the content at |
| 71 | // full width. The slot moves without resizing, so measure on shell resizes too. |
| 72 | const panel = element.closest('[data-preview-panel-mode="inline"]'); |
| 73 | if (panel) observed.push(panel); |
| 74 | // A sidebar opening or closing shifts the whole column, so a slot that keeps |
| 75 | // its size and its offset inside the column (the mini player on the left) |
| 76 | // still moves on screen. The gap element carries the sidebar's width. |
| 77 | observed.push(...document.querySelectorAll('[data-slot="sidebar-gap"]')); |
| 78 | const stopObserving = observeResize(observed, update); |
| 79 | window.addEventListener("resize", update); |
| 80 | window.addEventListener("scroll", update, true); |
| 81 | return () => { |
| 82 | stopObserving(); |
| 83 | window.removeEventListener("resize", update); |
| 84 | window.removeEventListener("scroll", update, true); |
| 85 | if (updateRef.current === update) updateRef.current = null; |
| 86 | lease.release(); |
| 87 | }; |
| 88 | }, [fitSourceContent, tabId]); |
| 89 | |
| 90 | useLayoutEffect(() => { |
| 91 | presentationRef.current = { visible, cornerRadius, zIndex }; |
| 92 | updateRef.current?.(); |
| 93 | }, [cornerRadius, layoutVersion, visible, zIndex]); |
| 94 | |
| 95 | return <div ref={elementRef} className={className} data-browser-surface-slot={tabId} />; |
| 96 | } |