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