Open History to see the last commit
1import { useLayoutEffect, useRef } from "react";
2
3import { acquireBrowserSurface } from "./browserSurfaceStore";
4import { observeResize } from "../lib/observeResize";
5
6export function BrowserSurfaceSlot(props: {
7 readonly tabId: string;
8 readonly visible: boolean;
9 readonly cornerRadius?: number;
10 readonly zIndex?: number;
11 readonly layoutVersion?: string | number;
12 readonly className?: string;
13 readonly fitSourceContent?: boolean;
14}) {
15 const {
16 tabId,
17 visible,
18 cornerRadius = 0,
19 zIndex = 30,
20 layoutVersion,
21 className,
22 fitSourceContent = false,
23 } = props;
24 const presentationRef = useRef({ visible, cornerRadius, zIndex });
25 const updateRef = useRef<(() => void) | null>(null);
26
27 // The lease lives as long as this element for one tab, so the ref callback owns it.
28 const attachSurface = (element: HTMLDivElement) => {
29 let lease = acquireBrowserSurface(tabId, fitSourceContent);
30 const update = () => {
31 const rect = element.getBoundingClientRect();
32 const presentation = presentationRef.current;
33 const presented = lease.present(
34 {
35 x: Math.round(rect.x),
36 y: Math.round(rect.y),
37 width: Math.max(1, Math.round(rect.width)),
38 height: Math.max(1, Math.round(rect.height)),
39 },
40 presentation.visible && rect.width > 0 && rect.height > 0,
41 presentation.cornerRadius,
42 presentation.zIndex,
43 );
44 if (presentation.visible && !presented) {
45 lease.release();
46 lease = acquireBrowserSurface(tabId, fitSourceContent);
47 lease.present(
48 {
49 x: Math.round(rect.x),
50 y: Math.round(rect.y),
51 width: Math.max(1, Math.round(rect.width)),
52 height: Math.max(1, Math.round(rect.height)),
53 },
54 rect.width > 0 && rect.height > 0,
55 presentation.cornerRadius,
56 presentation.zIndex,
57 );
58 }
59 };
60 updateRef.current = update;
61 update();
62 const observed: Element[] = [element];
63 // Inline shells animate without resizing the slot, so observe the shell too.
64 const panel = element.closest('[data-preview-panel-mode="inline"]');
65 if (panel) observed.push(panel);
66 // Sidebar width changes move the slot without changing its size or column offset.
67 observed.push(...document.querySelectorAll('[data-slot="sidebar-gap"]'));
68 const stopObserving = observeResize(observed, update);
69 window.addEventListener("resize", update);
70 window.addEventListener("scroll", update, true);
71 return () => {
72 stopObserving();
73 window.removeEventListener("resize", update);
74 window.removeEventListener("scroll", update, true);
75 if (updateRef.current === update) updateRef.current = null;
76 lease.release();
77 };
78 };
79
80 // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- pushes visibility, radius and stacking to the desktop browser surface after layout
81 useLayoutEffect(() => {
82 presentationRef.current = { visible, cornerRadius, zIndex };
83 updateRef.current?.();
84 }, [
85 cornerRadius,
86 // oxlint-disable-next-line react/exhaustive-effect-dependencies -- A layoutVersion bump means the slot moved without resizing, so it re-presents at the new position.
87 layoutVersion,
88 visible,
89 zIndex,
90 ]);
91
92 return <div ref={attachSurface} className={className} data-browser-surface-slot={tabId} />;
93}