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 @@
22
33import type { ScopedThreadRef } from "@t3tools/contracts";
44import { 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
77import { BrowserSurfaceSlot } from "~/browser/BrowserSurfaceSlot";
88import { previewRuntimeTabId } from "~/browser/previewRuntimeTabId";
......@@ -17,6 +17,7 @@ import {
1717 clampPreviewMiniPlayerPosition,
1818 clampPreviewMiniPlayerSize,
1919 PREVIEW_MINI_PLAYER_DEFAULT_SIZE,
20 PREVIEW_MINI_PLAYER_EDGE_GAP,
2021} from "./previewMiniPlayerLayout";
2122
2223interface DragState {
......@@ -31,6 +32,8 @@ interface ResizeState {
3132 readonly pointerId: number;
3233 readonly pointerX: number;
3334 readonly pointerY: number;
35 readonly playerX: number;
36 readonly playerY: number;
3437 readonly width: number;
3538 readonly height: number;
3639}
......@@ -45,6 +48,7 @@ export function ThreadPreviewMiniPlayer({ threadRef, tabId, bottomInset }: Props
4548 const rootRef = useRef<HTMLElement | null>(null);
4649 const dragRef = useRef<DragState | null>(null);
4750 const resizeRef = useRef<ResizeState | null>(null);
51 const [defaultLayoutVersion, setDefaultLayoutVersion] = useState("");
4852 const miniPlayer = usePreviewMiniPlayerStore((state) =>
4953 selectThreadPreviewMiniPlayer(state.byThreadKey, threadRef),
5054 );
......@@ -91,8 +95,12 @@ export function ThreadPreviewMiniPlayer({ threadRef, tabId, bottomInset }: Props
9195 bottomInset,
9296 );
9397 usePreviewMiniPlayerStore.getState().resize(threadRef, tabId, nextSize);
98 if (!position) {
99 setDefaultLayoutVersion(`${parent.clientWidth}:${parent.clientHeight}`);
100 return;
101 }
94102 const next = clampPreviewMiniPlayerPosition(
95 position ?? { x: root.offsetLeft, y: root.offsetTop },
103 position,
96104 { width: parent.clientWidth, height: parent.clientHeight },
97105 nextSize,
98106 bottomInset,
......@@ -159,11 +167,16 @@ export function ThreadPreviewMiniPlayer({ threadRef, tabId, bottomInset }: Props
159167 const handleResizePointerDown = (event: ReactPointerEvent<HTMLButtonElement>) => {
160168 if (event.button !== 0) return;
161169 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();
163174 resizeRef.current = {
164175 pointerId: event.pointerId,
165176 pointerX: event.clientX,
166177 pointerY: event.clientY,
178 playerX: rootRect.left - parentRect.left,
179 playerY: rootRect.top - parentRect.top,
167180 width: root.offsetWidth,
168181 height: root.offsetHeight,
169182 };
......@@ -194,7 +207,7 @@ export function ThreadPreviewMiniPlayer({ threadRef, tabId, bottomInset }: Props
194207 );
195208 usePreviewMiniPlayerStore.getState().resize(threadRef, tabId, nextSize);
196209 const nextPosition = clampPreviewMiniPlayerPosition(
197 position ?? { x: root.offsetLeft, y: root.offsetTop },
210 { x: resize.playerX, y: resize.playerY },
198211 { width: parent.clientWidth, height: parent.clientHeight },
199212 nextSize,
200213 bottomInset,
......@@ -222,8 +235,8 @@ export function ThreadPreviewMiniPlayer({ threadRef, tabId, bottomInset }: Props
222235 position
223236 ? { left: position.x, top: position.y, width: size.width, height: size.height }
224237 : {
225 right: 16,
226 top: 16,
238 right: PREVIEW_MINI_PLAYER_EDGE_GAP,
239 top: PREVIEW_MINI_PLAYER_EDGE_GAP,
227240 width: size.width,
228241 height: size.height,
229242 }
......@@ -290,7 +303,11 @@ export function ThreadPreviewMiniPlayer({ threadRef, tabId, bottomInset }: Props
290303 visible={Boolean(desktopOverlay?.hasWebContents)}
291304 cornerRadius={12}
292305 fitSourceContent
293 layoutVersion={position ? `${position.x}:${position.y}` : `initial:${bottomInset}`}
306 layoutVersion={
307 position
308 ? `${position.x}:${position.y}`
309 : `initial:${bottomInset}:${defaultLayoutVersion}`
310 }
294311 className="absolute inset-0"
295312 />
296313 <div className="pointer-events-none absolute inset-0 z-[31] rounded-xl ring-1 ring-inset ring-border/80" />