| ... | ... | @@ -1,43 +1,41 @@ |
| 1 | 1 | "use client"; |
| 2 | 2 | |
| 3 | | import type { ScopedThreadRef } from "@t3tools/contracts"; |
| 3 | import { FILL_PREVIEW_VIEWPORT, type ScopedThreadRef } from "@t3tools/contracts"; |
| 4 | 4 | import { PanelRightIcon, PictureInPicture2, XIcon } from "lucide-react"; |
| 5 | 5 | import { type PointerEvent as ReactPointerEvent, useLayoutEffect, useRef, useState } from "react"; |
| 6 | 6 | |
| 7 | 7 | import { BrowserSurfaceSlot } from "~/browser/BrowserSurfaceSlot"; |
| 8 | import { useBrowserSurfaceStore } from "~/browser/browserSurfaceStore"; |
| 9 | import type { BrowserViewportResizeDirection } from "~/browser/browserViewportLayout"; |
| 8 | 10 | import { previewRuntimeTabId } from "~/browser/previewRuntimeTabId"; |
| 9 | 11 | import { Button } from "~/components/ui/button"; |
| 10 | 12 | import { toastManager } from "~/components/ui/toast"; |
| 11 | 13 | import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip"; |
| 14 | import { cn } from "~/lib/utils"; |
| 12 | 15 | import { useThreadPreviewState } from "~/previewStateStore"; |
| 13 | | import { selectThreadPreviewMiniPlayer, usePreviewMiniPlayerStore } from "~/previewMiniPlayerStore"; |
| 16 | import { |
| 17 | type PreviewMiniPlayerSize, |
| 18 | selectThreadPreviewMiniPlayer, |
| 19 | usePreviewMiniPlayerStore, |
| 20 | } from "~/previewMiniPlayerStore"; |
| 14 | 21 | import { useRightPanelStore } from "~/rightPanelStore"; |
| 15 | 22 | |
| 16 | 23 | import { previewBridge } from "./previewBridge"; |
| 17 | 24 | import { |
| 18 | 25 | clampPreviewMiniPlayerPosition, |
| 19 | | clampPreviewMiniPlayerSize, |
| 20 | | PREVIEW_MINI_PLAYER_DEFAULT_SIZE, |
| 21 | | PREVIEW_MINI_PLAYER_EDGE_GAP, |
| 22 | 26 | PREVIEW_MINI_PLAYER_WEBVIEW_Z_INDEX, |
| 27 | type PreviewMiniPlayerFrame, |
| 28 | resizePreviewMiniPlayer, |
| 29 | resolvePreviewMiniPlayerFrame, |
| 30 | resolvePreviewMiniPlayerSourceSize, |
| 23 | 31 | } from "./previewMiniPlayerLayout"; |
| 24 | 32 | |
| 25 | | interface DragState { |
| 26 | | readonly pointerId: number; |
| 27 | | readonly pointerX: number; |
| 28 | | readonly pointerY: number; |
| 29 | | readonly playerX: number; |
| 30 | | readonly playerY: number; |
| 31 | | } |
| 32 | | |
| 33 | | interface ResizeState { |
| 33 | interface PointerGesture { |
| 34 | 34 | readonly pointerId: number; |
| 35 | 35 | readonly pointerX: number; |
| 36 | 36 | readonly pointerY: number; |
| 37 | | readonly playerX: number; |
| 38 | | readonly playerY: number; |
| 39 | | readonly width: number; |
| 40 | | readonly height: number; |
| 37 | readonly frame: PreviewMiniPlayerFrame; |
| 38 | readonly direction: BrowserViewportResizeDirection | null; |
| 41 | 39 | } |
| 42 | 40 | |
| 43 | 41 | interface Props { |
| ... | ... | @@ -46,11 +44,25 @@ interface Props { |
| 46 | 44 | readonly bottomInset: number; |
| 47 | 45 | } |
| 48 | 46 | |
| 47 | // Invisible grab zones straddling each edge; the cursor is the only affordance. |
| 48 | const RESIZE_HANDLES: ReadonlyArray<{ |
| 49 | readonly direction: BrowserViewportResizeDirection; |
| 50 | readonly className: string; |
| 51 | }> = [ |
| 52 | { direction: "north", className: "inset-x-0 -top-1 h-2 cursor-ns-resize" }, |
| 53 | { direction: "south", className: "inset-x-0 -bottom-1 h-2 cursor-ns-resize" }, |
| 54 | { direction: "west", className: "inset-y-0 -left-1 w-2 cursor-ew-resize" }, |
| 55 | { direction: "east", className: "inset-y-0 -right-1 w-2 cursor-ew-resize" }, |
| 56 | { direction: "northwest", className: "-left-2 -top-2 size-4 cursor-nwse-resize" }, |
| 57 | { direction: "northeast", className: "-right-2 -top-2 size-4 cursor-nesw-resize" }, |
| 58 | { direction: "southwest", className: "-bottom-2 -left-2 size-4 cursor-nesw-resize" }, |
| 59 | { direction: "southeast", className: "-bottom-2 -right-2 size-4 cursor-nwse-resize" }, |
| 60 | ]; |
| 61 | |
| 49 | 62 | export function ThreadPreviewMiniPlayer({ threadRef, tabId, bottomInset }: Props) { |
| 50 | | const rootRef = useRef<HTMLElement | null>(null); |
| 51 | | const dragRef = useRef<DragState | null>(null); |
| 52 | | const resizeRef = useRef<ResizeState | null>(null); |
| 53 | | const [defaultLayoutVersion, setDefaultLayoutVersion] = useState(""); |
| 63 | const containerRef = useRef<HTMLDivElement | null>(null); |
| 64 | const gestureRef = useRef<PointerGesture | null>(null); |
| 65 | const [container, setContainer] = useState<PreviewMiniPlayerSize | null>(null); |
| 54 | 66 | const miniPlayer = usePreviewMiniPlayerStore((state) => |
| 55 | 67 | selectThreadPreviewMiniPlayer(state.byThreadKey, threadRef), |
| 56 | 68 | ); |
| ... | ... | @@ -58,11 +70,25 @@ export function ThreadPreviewMiniPlayer({ threadRef, tabId, bottomInset }: Props |
| 58 | 70 | const snapshot = previewState.sessions[tabId] ?? null; |
| 59 | 71 | const runtimeTabId = previewRuntimeTabId(threadRef, previewState.serverEpoch, tabId); |
| 60 | 72 | const desktopOverlay = previewState.desktopByTabId[tabId] ?? null; |
| 61 | | const position = miniPlayer?.tabId === tabId ? miniPlayer.position : null; |
| 62 | | const size = |
| 63 | | miniPlayer?.tabId === tabId && miniPlayer.size |
| 64 | | ? miniPlayer.size |
| 65 | | : PREVIEW_MINI_PLAYER_DEFAULT_SIZE; |
| 73 | const fittedSourceContent = useBrowserSurfaceStore( |
| 74 | (state) => state.byTabId[runtimeTabId]?.fittedSourceContent ?? null, |
| 75 | ); |
| 76 | const source = resolvePreviewMiniPlayerSourceSize( |
| 77 | snapshot?.viewport ?? FILL_PREVIEW_VIEWPORT, |
| 78 | fittedSourceContent, |
| 79 | desktopOverlay?.zoomFactor ?? 1, |
| 80 | ); |
| 81 | const frame = |
| 82 | container && miniPlayer?.tabId === tabId |
| 83 | ? resolvePreviewMiniPlayerFrame({ |
| 84 | width: miniPlayer.width, |
| 85 | position: miniPlayer.position, |
| 86 | source, |
| 87 | container, |
| 88 | bottomInset, |
| 89 | }) |
| 90 | : null; |
| 91 | |
| 66 | 92 | const close = () => { |
| 67 | 93 | usePreviewMiniPlayerStore.getState().close(threadRef); |
| 68 | 94 | }; |
| ... | ... | @@ -87,139 +113,72 @@ export function ThreadPreviewMiniPlayer({ threadRef, tabId, bottomInset }: Props |
| 87 | 113 | }; |
| 88 | 114 | |
| 89 | 115 | useLayoutEffect(() => { |
| 90 | | const clampAndMove = () => { |
| 91 | | const root = rootRef.current; |
| 92 | | const parent = root?.offsetParent; |
| 93 | | if (!root || !(parent instanceof HTMLElement)) return; |
| 94 | | const nextSize = clampPreviewMiniPlayerSize( |
| 95 | | { width: root.offsetWidth, height: root.offsetHeight }, |
| 96 | | { width: parent.clientWidth, height: parent.clientHeight }, |
| 97 | | bottomInset, |
| 116 | const element = containerRef.current; |
| 117 | if (!element) return; |
| 118 | const measure = () => { |
| 119 | setContainer((current) => |
| 120 | current?.width === element.clientWidth && current.height === element.clientHeight |
| 121 | ? current |
| 122 | : { width: element.clientWidth, height: element.clientHeight }, |
| 98 | 123 | ); |
| 99 | | usePreviewMiniPlayerStore.getState().resize(threadRef, tabId, nextSize); |
| 100 | | if (!position) { |
| 101 | | setDefaultLayoutVersion(`${parent.clientWidth}:${parent.clientHeight}`); |
| 102 | | return; |
| 103 | | } |
| 104 | | const next = clampPreviewMiniPlayerPosition( |
| 105 | | position, |
| 106 | | { width: parent.clientWidth, height: parent.clientHeight }, |
| 107 | | nextSize, |
| 108 | | bottomInset, |
| 109 | | ); |
| 110 | | usePreviewMiniPlayerStore.getState().move(threadRef, tabId, next); |
| 111 | 124 | }; |
| 112 | | clampAndMove(); |
| 113 | | const root = rootRef.current; |
| 114 | | const parent = root?.offsetParent; |
| 115 | | if (!root || !(parent instanceof HTMLElement) || typeof ResizeObserver === "undefined") { |
| 116 | | return; |
| 117 | | } |
| 118 | | const observer = new ResizeObserver(clampAndMove); |
| 119 | | observer.observe(root); |
| 120 | | observer.observe(parent); |
| 125 | measure(); |
| 126 | if (typeof ResizeObserver === "undefined") return; |
| 127 | const observer = new ResizeObserver(measure); |
| 128 | observer.observe(element); |
| 121 | 129 | return () => observer.disconnect(); |
| 122 | | }, [bottomInset, position, tabId, threadRef]); |
| 123 | | |
| 124 | | const handlePointerDown = (event: ReactPointerEvent<HTMLDivElement>) => { |
| 125 | | if (event.button !== 0) return; |
| 126 | | const root = rootRef.current; |
| 127 | | const parent = root?.offsetParent; |
| 128 | | if (!root || !(parent instanceof HTMLElement)) return; |
| 129 | | const rootRect = root.getBoundingClientRect(); |
| 130 | | const parentRect = parent.getBoundingClientRect(); |
| 131 | | dragRef.current = { |
| 132 | | pointerId: event.pointerId, |
| 133 | | pointerX: event.clientX, |
| 134 | | pointerY: event.clientY, |
| 135 | | playerX: rootRect.left - parentRect.left, |
| 136 | | playerY: rootRect.top - parentRect.top, |
| 137 | | }; |
| 138 | | event.currentTarget.setPointerCapture(event.pointerId); |
| 139 | | event.preventDefault(); |
| 140 | | }; |
| 141 | | |
| 142 | | const handlePointerMove = (event: ReactPointerEvent<HTMLDivElement>) => { |
| 143 | | const drag = dragRef.current; |
| 144 | | const root = rootRef.current; |
| 145 | | const parent = root?.offsetParent; |
| 146 | | if (!drag || drag.pointerId !== event.pointerId || !root || !(parent instanceof HTMLElement)) { |
| 147 | | return; |
| 148 | | } |
| 149 | | const next = clampPreviewMiniPlayerPosition( |
| 150 | | { |
| 151 | | x: drag.playerX + event.clientX - drag.pointerX, |
| 152 | | y: drag.playerY + event.clientY - drag.pointerY, |
| 153 | | }, |
| 154 | | { width: parent.clientWidth, height: parent.clientHeight }, |
| 155 | | { width: root.offsetWidth, height: root.offsetHeight }, |
| 156 | | bottomInset, |
| 157 | | ); |
| 158 | | usePreviewMiniPlayerStore.getState().move(threadRef, tabId, next); |
| 159 | | }; |
| 160 | | |
| 161 | | const endDrag = (event: ReactPointerEvent<HTMLDivElement>) => { |
| 162 | | if (dragRef.current?.pointerId !== event.pointerId) return; |
| 163 | | dragRef.current = null; |
| 164 | | if (event.currentTarget.hasPointerCapture(event.pointerId)) { |
| 165 | | event.currentTarget.releasePointerCapture(event.pointerId); |
| 166 | | } |
| 167 | | }; |
| 130 | }, []); |
| 168 | 131 | |
| 169 | | const handleResizePointerDown = (event: ReactPointerEvent<HTMLButtonElement>) => { |
| 170 | | if (event.button !== 0) return; |
| 171 | | const root = rootRef.current; |
| 172 | | const parent = root?.offsetParent; |
| 173 | | if (!root || !(parent instanceof HTMLElement)) return; |
| 174 | | const rootRect = root.getBoundingClientRect(); |
| 175 | | const parentRect = parent.getBoundingClientRect(); |
| 176 | | resizeRef.current = { |
| 132 | const beginGesture = ( |
| 133 | event: ReactPointerEvent<HTMLElement>, |
| 134 | direction: BrowserViewportResizeDirection | null, |
| 135 | ) => { |
| 136 | if (event.button !== 0 || !frame) return; |
| 137 | gestureRef.current = { |
| 177 | 138 | pointerId: event.pointerId, |
| 178 | 139 | pointerX: event.clientX, |
| 179 | 140 | pointerY: event.clientY, |
| 180 | | playerX: rootRect.left - parentRect.left, |
| 181 | | playerY: rootRect.top - parentRect.top, |
| 182 | | width: root.offsetWidth, |
| 183 | | height: root.offsetHeight, |
| 141 | frame, |
| 142 | direction, |
| 184 | 143 | }; |
| 185 | 144 | event.currentTarget.setPointerCapture(event.pointerId); |
| 186 | 145 | event.preventDefault(); |
| 187 | 146 | event.stopPropagation(); |
| 188 | 147 | }; |
| 189 | 148 | |
| 190 | | const handleResizePointerMove = (event: ReactPointerEvent<HTMLButtonElement>) => { |
| 191 | | const resize = resizeRef.current; |
| 192 | | const root = rootRef.current; |
| 193 | | const parent = root?.offsetParent; |
| 194 | | if ( |
| 195 | | !resize || |
| 196 | | resize.pointerId !== event.pointerId || |
| 197 | | !root || |
| 198 | | !(parent instanceof HTMLElement) |
| 199 | | ) { |
| 149 | const handlePointerMove = (event: ReactPointerEvent<HTMLElement>) => { |
| 150 | const gesture = gestureRef.current; |
| 151 | if (!gesture || gesture.pointerId !== event.pointerId || !container) return; |
| 152 | const delta = { x: event.clientX - gesture.pointerX, y: event.clientY - gesture.pointerY }; |
| 153 | const store = usePreviewMiniPlayerStore.getState(); |
| 154 | if (gesture.direction === null) { |
| 155 | store.move( |
| 156 | threadRef, |
| 157 | tabId, |
| 158 | clampPreviewMiniPlayerPosition( |
| 159 | { x: gesture.frame.x + delta.x, y: gesture.frame.y + delta.y }, |
| 160 | container, |
| 161 | gesture.frame, |
| 162 | bottomInset, |
| 163 | ), |
| 164 | ); |
| 200 | 165 | return; |
| 201 | 166 | } |
| 202 | | const nextSize = clampPreviewMiniPlayerSize( |
| 203 | | { |
| 204 | | width: resize.width + event.clientX - resize.pointerX, |
| 205 | | height: resize.height + event.clientY - resize.pointerY, |
| 206 | | }, |
| 207 | | { width: parent.clientWidth, height: parent.clientHeight }, |
| 208 | | bottomInset, |
| 209 | | ); |
| 210 | | usePreviewMiniPlayerStore.getState().resize(threadRef, tabId, nextSize); |
| 211 | | const nextPosition = clampPreviewMiniPlayerPosition( |
| 212 | | { x: resize.playerX, y: resize.playerY }, |
| 213 | | { width: parent.clientWidth, height: parent.clientHeight }, |
| 214 | | nextSize, |
| 167 | const next = resizePreviewMiniPlayer({ |
| 168 | start: gesture.frame, |
| 169 | direction: gesture.direction, |
| 170 | delta, |
| 171 | source, |
| 172 | container, |
| 215 | 173 | bottomInset, |
| 216 | | ); |
| 217 | | usePreviewMiniPlayerStore.getState().move(threadRef, tabId, nextPosition); |
| 174 | }); |
| 175 | store.resize(threadRef, tabId, next.width); |
| 176 | store.move(threadRef, tabId, { x: next.x, y: next.y }); |
| 218 | 177 | }; |
| 219 | 178 | |
| 220 | | const endResize = (event: ReactPointerEvent<HTMLButtonElement>) => { |
| 221 | | if (resizeRef.current?.pointerId !== event.pointerId) return; |
| 222 | | resizeRef.current = null; |
| 179 | const endGesture = (event: ReactPointerEvent<HTMLElement>) => { |
| 180 | if (gestureRef.current?.pointerId !== event.pointerId) return; |
| 181 | gestureRef.current = null; |
| 223 | 182 | if (event.currentTarget.hasPointerCapture(event.pointerId)) { |
| 224 | 183 | event.currentTarget.releasePointerCapture(event.pointerId); |
| 225 | 184 | } |
| ... | ... | @@ -228,125 +187,116 @@ export function ThreadPreviewMiniPlayer({ threadRef, tabId, bottomInset }: Props |
| 228 | 187 | if (!snapshot || miniPlayer?.tabId !== tabId) return null; |
| 229 | 188 | |
| 230 | 189 | return ( |
| 231 | | <section |
| 232 | | ref={rootRef} |
| 233 | | aria-label="Floating browser preview" |
| 234 | | data-preview-mini-player={tabId} |
| 235 | | className="pointer-events-none absolute select-none" |
| 236 | | style={ |
| 237 | | position |
| 238 | | ? { left: position.x, top: position.y, width: size.width, height: size.height } |
| 239 | | : { |
| 240 | | right: PREVIEW_MINI_PLAYER_EDGE_GAP, |
| 241 | | top: PREVIEW_MINI_PLAYER_EDGE_GAP, |
| 242 | | width: size.width, |
| 243 | | height: size.height, |
| 244 | | } |
| 245 | | } |
| 246 | | > |
| 247 | | <div className="group pointer-events-auto absolute right-2 top-2 z-[49] size-3"> |
| 248 | | <div |
| 249 | | aria-hidden="true" |
| 250 | | className="absolute right-0 top-0 size-2 rounded-full bg-foreground/25 shadow-sm ring-1 ring-background/70 transition-opacity group-hover:opacity-0 group-focus-within:opacity-0" |
| 251 | | /> |
| 252 | | <div |
| 253 | | className="pointer-events-none absolute right-0 top-0 flex h-8 cursor-grab items-center gap-0.5 rounded-lg border border-border/80 bg-popover/92 p-0.5 opacity-0 shadow-lg/20 backdrop-blur-xl transition-opacity group-hover:pointer-events-auto group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:opacity-100 active:cursor-grabbing" |
| 254 | | onPointerDown={handlePointerDown} |
| 255 | | onPointerMove={handlePointerMove} |
| 256 | | onPointerUp={endDrag} |
| 257 | | onPointerCancel={endDrag} |
| 190 | <div ref={containerRef} className="pointer-events-none absolute inset-0"> |
| 191 | {frame ? ( |
| 192 | <section |
| 193 | aria-label="Floating browser preview" |
| 194 | data-preview-mini-player={tabId} |
| 195 | className="pointer-events-none absolute select-none" |
| 196 | style={{ left: frame.x, top: frame.y, width: frame.width, height: frame.height }} |
| 258 | 197 | > |
| 259 | | <Tooltip> |
| 260 | | <TooltipTrigger |
| 261 | | render={ |
| 262 | | <Button |
| 263 | | variant="ghost" |
| 264 | | size="icon-xs" |
| 265 | | aria-label="Open preview in right panel" |
| 266 | | onPointerDown={(event) => event.stopPropagation()} |
| 267 | | onClick={openInPanel} |
| 268 | | /> |
| 269 | | } |
| 198 | <div className="group pointer-events-auto absolute right-2 top-2 z-[49] size-3"> |
| 199 | <div |
| 200 | aria-hidden="true" |
| 201 | className="absolute right-0 top-0 size-2 rounded-full bg-foreground/25 shadow-sm ring-1 ring-background/70 transition-opacity group-hover:opacity-0 group-focus-within:opacity-0" |
| 202 | /> |
| 203 | <div |
| 204 | className="pointer-events-none absolute right-0 top-0 flex h-8 cursor-grab items-center gap-0.5 rounded-lg border border-border/80 bg-popover/92 p-0.5 opacity-0 shadow-lg/20 backdrop-blur-xl transition-opacity group-hover:pointer-events-auto group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:opacity-100 active:cursor-grabbing" |
| 205 | onPointerDown={(event) => beginGesture(event, null)} |
| 206 | onPointerMove={handlePointerMove} |
| 207 | onPointerUp={endGesture} |
| 208 | onPointerCancel={endGesture} |
| 270 | 209 | > |
| 271 | | <PanelRightIcon /> |
| 272 | | </TooltipTrigger> |
| 273 | | <TooltipPopup side="top">Open in right panel</TooltipPopup> |
| 274 | | </Tooltip> |
| 275 | | <Tooltip> |
| 276 | | <TooltipTrigger |
| 277 | | render={ |
| 278 | | <Button |
| 279 | | variant={desktopOverlay?.pictureInPicture ? "secondary" : "ghost"} |
| 280 | | size="icon-xs" |
| 281 | | aria-label={ |
| 282 | | desktopOverlay?.pictureInPicture |
| 283 | | ? "Close popped-out preview" |
| 284 | | : "Pop preview into separate window" |
| 210 | <Tooltip> |
| 211 | <TooltipTrigger |
| 212 | render={ |
| 213 | <Button |
| 214 | variant="ghost" |
| 215 | size="icon-xs" |
| 216 | aria-label="Open preview in right panel" |
| 217 | onPointerDown={(event) => event.stopPropagation()} |
| 218 | onClick={openInPanel} |
| 219 | /> |
| 285 | 220 | } |
| 286 | | disabled={!desktopOverlay?.hasWebContents} |
| 287 | | onPointerDown={(event) => event.stopPropagation()} |
| 288 | | onClick={toggleNativePictureInPicture} |
| 289 | | /> |
| 290 | | } |
| 291 | | > |
| 292 | | <PictureInPicture2 /> |
| 293 | | </TooltipTrigger> |
| 294 | | <TooltipPopup side="top"> |
| 295 | | {desktopOverlay?.pictureInPicture |
| 296 | | ? "Close separate window" |
| 297 | | : "Pop into separate window"} |
| 298 | | </TooltipPopup> |
| 299 | | </Tooltip> |
| 300 | | <Tooltip> |
| 301 | | <TooltipTrigger |
| 302 | | render={ |
| 303 | | <Button |
| 304 | | variant="ghost" |
| 305 | | size="icon-xs" |
| 306 | | aria-label="Close floating preview" |
| 307 | | onPointerDown={(event) => event.stopPropagation()} |
| 308 | | onClick={close} |
| 309 | | /> |
| 310 | | } |
| 311 | | > |
| 312 | | <XIcon /> |
| 313 | | </TooltipTrigger> |
| 314 | | <TooltipPopup side="top">Close floating preview</TooltipPopup> |
| 315 | | </Tooltip> |
| 316 | | </div> |
| 317 | | </div> |
| 318 | | |
| 319 | | <div className="relative h-full min-h-0"> |
| 320 | | <div className="absolute inset-0 z-[47] rounded-xl bg-muted shadow-2xl/35" /> |
| 321 | | <BrowserSurfaceSlot |
| 322 | | tabId={runtimeTabId} |
| 323 | | visible={Boolean(desktopOverlay?.hasWebContents)} |
| 324 | | cornerRadius={12} |
| 325 | | zIndex={PREVIEW_MINI_PLAYER_WEBVIEW_Z_INDEX} |
| 326 | | fitSourceContent |
| 327 | | layoutVersion={ |
| 328 | | position |
| 329 | | ? `${position.x}:${position.y}` |
| 330 | | : `initial:${bottomInset}:${defaultLayoutVersion}` |
| 331 | | } |
| 332 | | className="absolute inset-0" |
| 333 | | /> |
| 334 | | <div className="pointer-events-none absolute inset-0 z-[49] rounded-xl ring-1 ring-inset ring-border/80" /> |
| 335 | | {!desktopOverlay?.hasWebContents ? ( |
| 336 | | <div className="pointer-events-none absolute inset-0 z-[49] flex items-center justify-center rounded-xl bg-muted text-xs text-muted-foreground"> |
| 337 | | Reconnecting preview… |
| 221 | > |
| 222 | <PanelRightIcon /> |
| 223 | </TooltipTrigger> |
| 224 | <TooltipPopup side="top">Open in right panel</TooltipPopup> |
| 225 | </Tooltip> |
| 226 | <Tooltip> |
| 227 | <TooltipTrigger |
| 228 | render={ |
| 229 | <Button |
| 230 | variant={desktopOverlay?.pictureInPicture ? "secondary" : "ghost"} |
| 231 | size="icon-xs" |
| 232 | aria-label={ |
| 233 | desktopOverlay?.pictureInPicture |
| 234 | ? "Close popped-out preview" |
| 235 | : "Pop preview into separate window" |
| 236 | } |
| 237 | disabled={!desktopOverlay?.hasWebContents} |
| 238 | onPointerDown={(event) => event.stopPropagation()} |
| 239 | onClick={toggleNativePictureInPicture} |
| 240 | /> |
| 241 | } |
| 242 | > |
| 243 | <PictureInPicture2 /> |
| 244 | </TooltipTrigger> |
| 245 | <TooltipPopup side="top"> |
| 246 | {desktopOverlay?.pictureInPicture |
| 247 | ? "Close separate window" |
| 248 | : "Pop into separate window"} |
| 249 | </TooltipPopup> |
| 250 | </Tooltip> |
| 251 | <Tooltip> |
| 252 | <TooltipTrigger |
| 253 | render={ |
| 254 | <Button |
| 255 | variant="ghost" |
| 256 | size="icon-xs" |
| 257 | aria-label="Close floating preview" |
| 258 | onPointerDown={(event) => event.stopPropagation()} |
| 259 | onClick={close} |
| 260 | /> |
| 261 | } |
| 262 | > |
| 263 | <XIcon /> |
| 264 | </TooltipTrigger> |
| 265 | <TooltipPopup side="top">Close floating preview</TooltipPopup> |
| 266 | </Tooltip> |
| 267 | </div> |
| 338 | 268 | </div> |
| 339 | | ) : null} |
| 340 | | <button |
| 341 | | type="button" |
| 342 | | aria-label="Resize floating preview" |
| 343 | | className="pointer-events-auto absolute bottom-0 right-0 z-[49] size-5 cursor-nwse-resize rounded-br-xl after:absolute after:bottom-1 after:right-1 after:size-2 after:border-b after:border-r after:border-foreground/45" |
| 344 | | onPointerDown={handleResizePointerDown} |
| 345 | | onPointerMove={handleResizePointerMove} |
| 346 | | onPointerUp={endResize} |
| 347 | | onPointerCancel={endResize} |
| 348 | | /> |
| 349 | | </div> |
| 350 | | </section> |
| 269 | |
| 270 | <div className="absolute inset-0 z-[47] rounded-xl bg-muted shadow-2xl/35" /> |
| 271 | <BrowserSurfaceSlot |
| 272 | tabId={runtimeTabId} |
| 273 | visible={Boolean(desktopOverlay?.hasWebContents)} |
| 274 | cornerRadius={12} |
| 275 | zIndex={PREVIEW_MINI_PLAYER_WEBVIEW_Z_INDEX} |
| 276 | fitSourceContent |
| 277 | layoutVersion={`${frame.x}:${frame.y}`} |
| 278 | className="absolute inset-0" |
| 279 | /> |
| 280 | <div className="pointer-events-none absolute inset-0 z-[49] rounded-xl ring-1 ring-inset ring-border/80" /> |
| 281 | {!desktopOverlay?.hasWebContents ? ( |
| 282 | <div className="pointer-events-none absolute inset-0 z-[49] flex items-center justify-center rounded-xl bg-muted text-xs text-muted-foreground"> |
| 283 | Reconnecting preview… |
| 284 | </div> |
| 285 | ) : null} |
| 286 | {RESIZE_HANDLES.map(({ direction, className }) => ( |
| 287 | <div |
| 288 | key={direction} |
| 289 | role="presentation" |
| 290 | data-preview-mini-player-resize={direction} |
| 291 | className={cn("pointer-events-auto absolute z-[49] touch-none", className)} |
| 292 | onPointerDown={(event) => beginGesture(event, direction)} |
| 293 | onPointerMove={handlePointerMove} |
| 294 | onPointerUp={endGesture} |
| 295 | onPointerCancel={endGesture} |
| 296 | /> |
| 297 | ))} |
| 298 | </section> |
| 299 | ) : null} |
| 300 | </div> |
| 351 | 301 | ); |
| 352 | 302 | } |