1"use client";
2
3import { useLayoutEffect, useRef } from "react";
4
5import { acquireBrowserSurface } from "./browserSurfaceStore";
6import { observeResize } from "../lib/observeResize";
7
8export 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}