authorgravatar for 123344143+AksharP5@users.noreply.github.comAkshar Patel <123344143+AksharP5@users.noreply.github.com> 2026-08-15 08:00:53-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-15 12:00:53+00:00
log6e6d1b49412d064ccbde7daae2e287f9b62efd7d
tree5978a4cbc451cee8da9793bff69a64fc06805dd7
parent9bdd91293316038d6a50f8bb19afcd73c25ad102
signature Signed by PGP key B5690EEEBB952194

fix(web): keep floating preview anchored after panel closes (#6547)


1 files changed, 24 insertions(+), 7 deletions(-)

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