/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ /** * Pop-out workspace-panel windows (#1208). * * Tears a panel off into a separate OS window the user can drag onto another * screen or keep as a detached tab. Prefers the **Document Picture-in-Picture** * API (Chrome / Edge — borderless, always-on-top, ideal for a second monitor) * and falls back to **`window.open`** everywhere else. * * The panel is NOT re-implemented in the child window: {@link PanelWindowHost} * `createPortal`s the same component into the child document, so it keeps * running in *this* tab's React tree and reads/writes the *same* Zustand store * — state stays live across windows with zero sync code. We only have to copy * the stylesheets + theme class into the child document. * * This module is a framework-agnostic singleton (mirrors `analysis-extensions`) * so the pop-out action can run synchronously inside the click handler — which * the PiP / `window.open` user-activation requirement demands — while the host * subscribes via `useSyncExternalStore` and owns the portals. */ import { getViewerStoreApi } from '@/store'; import { getPanelDef, type WorkspacePanelId } from '@/lib/panels/registry'; interface DocumentPictureInPictureApi { requestWindow(options?: { width?: number; height?: number }): Promise; readonly window: Window | null; } declare global { interface Window { documentPictureInPicture?: DocumentPictureInPictureApi; } } export type PanelWindowKind = 'pip' | 'popup'; export interface PanelWindowEntry { id: WorkspacePanelId; win: Window; kind: PanelWindowKind; } const open = new Map(); const listeners = new Set<() => void>(); let snapshot: PanelWindowEntry[] = []; function rebuildSnapshot(): void { snapshot = [...open.values()]; } function emit(): void { rebuildSnapshot(); for (const l of listeners) l(); } export function subscribePanelWindows(listener: () => void): () => void { listeners.add(listener); return () => listeners.delete(listener); } export function getPanelWindowsSnapshot(): PanelWindowEntry[] { return snapshot; } /** Copy the app's stylesheets + theme class into a popped-out document so the * portalled panel looks identical (Tailwind v4 emits one stylesheet; theme * vars live on `:root` / `.dark` / `.colorful`). */ function bridgeStyles(target: Document): void { const head = target.head ?? target.documentElement.appendChild(target.createElement('head')); // Clone every author stylesheet (dev: