| ... | ... | @@ -2,7 +2,7 @@ |
| 2 | 2 | |
| 3 | 3 | import type { ScopedThreadRef } from "@t3tools/contracts"; |
| 4 | 4 | import { PanelRightIcon, PictureInPicture2, XIcon } from "lucide-react"; |
| 5 | | import { type PointerEvent as ReactPointerEvent, useLayoutEffect, useRef } from "react"; |
| 5 | import { type PointerEvent as ReactPointerEvent, useLayoutEffect, useRef, useState } from "react"; |
| 6 | 6 | |
| 7 | 7 | import { BrowserSurfaceSlot } from "~/browser/BrowserSurfaceSlot"; |
| 8 | 8 | import { previewRuntimeTabId } from "~/browser/previewRuntimeTabId"; |
| ... | ... | @@ -17,6 +17,7 @@ import { |
| 17 | 17 | clampPreviewMiniPlayerPosition, |
| 18 | 18 | clampPreviewMiniPlayerSize, |
| 19 | 19 | PREVIEW_MINI_PLAYER_DEFAULT_SIZE, |
| 20 | PREVIEW_MINI_PLAYER_EDGE_GAP, |
| 20 | 21 | } from "./previewMiniPlayerLayout"; |
| 21 | 22 | |
| 22 | 23 | interface DragState { |
| ... | ... | @@ -31,6 +32,8 @@ interface ResizeState { |
| 31 | 32 | readonly pointerId: number; |
| 32 | 33 | readonly pointerX: number; |
| 33 | 34 | readonly pointerY: number; |
| 35 | readonly playerX: number; |
| 36 | readonly playerY: number; |
| 34 | 37 | readonly width: number; |
| 35 | 38 | readonly height: number; |
| 36 | 39 | } |
| ... | ... | @@ -45,6 +48,7 @@ export function ThreadPreviewMiniPlayer({ threadRef, tabId, bottomInset }: Props |
| 45 | 48 | const rootRef = useRef<HTMLElement | null>(null); |
| 46 | 49 | const dragRef = useRef<DragState | null>(null); |
| 47 | 50 | const resizeRef = useRef<ResizeState | null>(null); |
| 51 | const [defaultLayoutVersion, setDefaultLayoutVersion] = useState(""); |
| 48 | 52 | const miniPlayer = usePreviewMiniPlayerStore((state) => |
| 49 | 53 | selectThreadPreviewMiniPlayer(state.byThreadKey, threadRef), |
| 50 | 54 | ); |
| ... | ... | @@ -91,8 +95,12 @@ export function ThreadPreviewMiniPlayer({ threadRef, tabId, bottomInset }: Props |
| 91 | 95 | bottomInset, |
| 92 | 96 | ); |
| 93 | 97 | usePreviewMiniPlayerStore.getState().resize(threadRef, tabId, nextSize); |
| 98 | if (!position) { |
| 99 | setDefaultLayoutVersion(`${parent.clientWidth}:${parent.clientHeight}`); |
| 100 | return; |
| 101 | } |
| 94 | 102 | const next = clampPreviewMiniPlayerPosition( |
| 95 | | position ?? { x: root.offsetLeft, y: root.offsetTop }, |
| 103 | position, |
| 96 | 104 | { width: parent.clientWidth, height: parent.clientHeight }, |
| 97 | 105 | nextSize, |
| 98 | 106 | bottomInset, |
| ... | ... | @@ -159,11 +167,16 @@ export function ThreadPreviewMiniPlayer({ threadRef, tabId, bottomInset }: Props |
| 159 | 167 | const handleResizePointerDown = (event: ReactPointerEvent<HTMLButtonElement>) => { |
| 160 | 168 | if (event.button !== 0) return; |
| 161 | 169 | const root = rootRef.current; |
| 162 | | if (!root) return; |
| 170 | const parent = root?.offsetParent; |
| 171 | if (!root || !(parent instanceof HTMLElement)) return; |
| 172 | const rootRect = root.getBoundingClientRect(); |
| 173 | const parentRect = parent.getBoundingClientRect(); |
| 163 | 174 | resizeRef.current = { |
| 164 | 175 | pointerId: event.pointerId, |
| 165 | 176 | pointerX: event.clientX, |
| 166 | 177 | pointerY: event.clientY, |
| 178 | playerX: rootRect.left - parentRect.left, |
| 179 | playerY: rootRect.top - parentRect.top, |
| 167 | 180 | width: root.offsetWidth, |
| 168 | 181 | height: root.offsetHeight, |
| 169 | 182 | }; |
| ... | ... | @@ -194,7 +207,7 @@ export function ThreadPreviewMiniPlayer({ threadRef, tabId, bottomInset }: Props |
| 194 | 207 | ); |
| 195 | 208 | usePreviewMiniPlayerStore.getState().resize(threadRef, tabId, nextSize); |
| 196 | 209 | const nextPosition = clampPreviewMiniPlayerPosition( |
| 197 | | position ?? { x: root.offsetLeft, y: root.offsetTop }, |
| 210 | { x: resize.playerX, y: resize.playerY }, |
| 198 | 211 | { width: parent.clientWidth, height: parent.clientHeight }, |
| 199 | 212 | nextSize, |
| 200 | 213 | bottomInset, |
| ... | ... | @@ -222,8 +235,8 @@ export function ThreadPreviewMiniPlayer({ threadRef, tabId, bottomInset }: Props |
| 222 | 235 | position |
| 223 | 236 | ? { left: position.x, top: position.y, width: size.width, height: size.height } |
| 224 | 237 | : { |
| 225 | | right: 16, |
| 226 | | top: 16, |
| 238 | right: PREVIEW_MINI_PLAYER_EDGE_GAP, |
| 239 | top: PREVIEW_MINI_PLAYER_EDGE_GAP, |
| 227 | 240 | width: size.width, |
| 228 | 241 | height: size.height, |
| 229 | 242 | } |
| ... | ... | @@ -290,7 +303,11 @@ export function ThreadPreviewMiniPlayer({ threadRef, tabId, bottomInset }: Props |
| 290 | 303 | visible={Boolean(desktopOverlay?.hasWebContents)} |
| 291 | 304 | cornerRadius={12} |
| 292 | 305 | fitSourceContent |
| 293 | | layoutVersion={position ? `${position.x}:${position.y}` : `initial:${bottomInset}`} |
| 306 | layoutVersion={ |
| 307 | position |
| 308 | ? `${position.x}:${position.y}` |
| 309 | : `initial:${bottomInset}:${defaultLayoutVersion}` |
| 310 | } |
| 294 | 311 | className="absolute inset-0" |
| 295 | 312 | /> |
| 296 | 313 | <div className="pointer-events-none absolute inset-0 z-[31] rounded-xl ring-1 ring-inset ring-border/80" /> |