import { type ReactNode, type RefObject, useLayoutEffect, useRef, useState } from "react"; import { isElectron } from "~/env"; import { getPreviewPanelMaxWidth, type PreviewPanelInlineSize, usePreviewPanelInlineSize, } from "~/hooks/usePreviewPanelInlineSize"; export { getPreviewPanelMaxWidth }; import { cn } from "~/lib/utils"; import { RightPanelResizeHandle } from "./RightPanelResizeHandle"; export type PreviewPanelMode = "inline" | "sheet" | "sidebar" | "embedded"; /** * Shell for the preview panel. In inline mode the panel is user-resizable * via a drag handle on the left edge; width persists per browser. In * sheet/sidebar modes the parent owns the size. */ interface PreviewPanelShellProps { mode: PreviewPanelMode; maximized?: boolean; inlineSize?: PreviewPanelInlineSize; open?: boolean; /** * Overrides the localStorage key used to persist the panel width. Callers * embedding this shell for a different surface (e.g. the pull requests * page) should pass their own key so resizing one panel doesn't clobber * the other's remembered width. */ widthStorageKey?: string; /** Overrides the initial width (px) before the user has resized the panel. */ defaultWidth?: number; children: ReactNode; } export function PreviewPanelShell(props: PreviewPanelShellProps) { if (props.inlineSize) { return ; } return ; } function ResizablePreviewPanelShell(props: PreviewPanelShellProps) { const hostRef = useRef(null); const inlineSize = usePreviewPanelInlineSize(hostRef, { enabled: props.mode === "inline" && !props.maximized, widthStorageKey: props.widthStorageKey, defaultWidth: props.defaultWidth, }); return ; } function PreviewPanelShellFrame( props: PreviewPanelShellProps & { inlineSize: PreviewPanelInlineSize; hostRef?: RefObject; }, ) { const useDragRegion = isElectron && props.mode !== "sheet" && props.mode !== "embedded"; const isInline = props.mode === "inline"; const collapsible = isInline && props.open !== undefined; const open = props.open ?? true; const maximized = props.maximized ?? false; const localHostRef = useRef(null); const hostRef = props.hostRef ?? localHostRef; const { width, handlers } = props.inlineSize; // Derive suppression before the layout commits so the browser never creates // a width transition for resize or maximize changes. const [layoutTransition, setLayoutTransition] = useState(() => ({ open, width, maximized, suppressed: false, })); if ( layoutTransition.open !== open || layoutTransition.width !== width || layoutTransition.maximized !== maximized ) { setLayoutTransition({ open, width, maximized, suppressed: collapsible && layoutTransition.open === open && (layoutTransition.width !== width || layoutTransition.maximized !== maximized), }); } const suppressWidthTransition = layoutTransition.suppressed; useLayoutEffect(() => { if (!suppressWidthTransition) return; let restoreFrame = 0; const paintFrame = window.requestAnimationFrame(() => { restoreFrame = window.requestAnimationFrame(() => { setLayoutTransition((current) => ({ ...current, suppressed: false })); }); }); return () => { window.cancelAnimationFrame(paintFrame); window.cancelAnimationFrame(restoreFrame); }; }, [suppressWidthTransition]); return (
{isInline && !maximized ? : null}
{useDragRegion ?
: null} {props.children}
); }