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 <PreviewPanelShellFrame {...props} inlineSize={props.inlineSize} />; } return <ResizablePreviewPanelShell {...props} />;} function ResizablePreviewPanelShell(props: PreviewPanelShellProps) { const hostRef = useRef<HTMLDivElement | null>(null); const inlineSize = usePreviewPanelInlineSize(hostRef, { enabled: props.mode === "inline" && !props.maximized, widthStorageKey: props.widthStorageKey, defaultWidth: props.defaultWidth, }); return <PreviewPanelShellFrame {...props} inlineSize={inlineSize} hostRef={hostRef} />;} function PreviewPanelShellFrame( props: PreviewPanelShellProps & { inlineSize: PreviewPanelInlineSize; hostRef?: RefObject<HTMLDivElement | null>; },) { 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<HTMLDivElement | null>(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 ( <div ref={hostRef} className={cn( "relative flex h-full min-h-0 min-w-0 max-w-full flex-col self-stretch bg-background", isInline ? maximized ? "flex-1 border-l border-border" : "shrink-0 border-l border-border" : "w-full", collapsible && "[[data-panel-animations=true]_&]:transition-[width] [[data-panel-animations=true]_&]:[transition-duration:var(--panel-animation-duration)] [[data-panel-animations=true]_&]:ease-out", collapsible && open && "[[data-panel-animations=true]_&]:starting:w-0!", collapsible && !open && "pointer-events-none", )} style={ isInline ? { width: maximized ? "100%" : collapsible && !open ? "0px" : `${width}px`, transitionDuration: suppressWidthTransition ? "0ms" : undefined, } : undefined } data-preview-panel-mode={props.mode} data-preview-panel-maximized={maximized ? "true" : "false"} > {isInline && !maximized ? <RightPanelResizeHandle handlers={handlers} /> : null} <div className={cn("h-full min-h-0 w-full", collapsible && "overflow-clip")}> <div className="flex h-full min-h-0 min-w-0 flex-col" style={collapsible && !maximized ? { width: `calc(${width}px - 1px)` } : undefined} > {useDragRegion ? <div className="electron-drag-region h-0 w-full" aria-hidden /> : null} {props.children} </div> </div> </div> );}
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 <PreviewPanelShellFrame {...props} inlineSize={props.inlineSize} />;
}
return <ResizablePreviewPanelShell {...props} />;
}
function ResizablePreviewPanelShell(props: PreviewPanelShellProps) {
const hostRef = useRef<HTMLDivElement | null>(null);
const inlineSize = usePreviewPanelInlineSize(hostRef, {
enabled: props.mode === "inline" && !props.maximized,
widthStorageKey: props.widthStorageKey,
defaultWidth: props.defaultWidth,
});
return <PreviewPanelShellFrame {...props} inlineSize={inlineSize} hostRef={hostRef} />;
}
function PreviewPanelShellFrame(
props: PreviewPanelShellProps & {
inlineSize: PreviewPanelInlineSize;
hostRef?: RefObject<HTMLDivElement | null>;
},
) {
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<HTMLDivElement | null>(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 (
<div
ref={hostRef}
className={cn(
"relative flex h-full min-h-0 min-w-0 max-w-full flex-col self-stretch bg-background",
isInline
? maximized
? "flex-1 border-l border-border"
: "shrink-0 border-l border-border"
: "w-full",
collapsible &&
"[[data-panel-animations=true]_&]:transition-[width] [[data-panel-animations=true]_&]:[transition-duration:var(--panel-animation-duration)] [[data-panel-animations=true]_&]:ease-out",
collapsible && open && "[[data-panel-animations=true]_&]:starting:w-0!",
collapsible && !open && "pointer-events-none",
)}
style={
isInline
? {
width: maximized ? "100%" : collapsible && !open ? "0px" : `${width}px`,
transitionDuration: suppressWidthTransition ? "0ms" : undefined,
}
: undefined
}
data-preview-panel-mode={props.mode}
data-preview-panel-maximized={maximized ? "true" : "false"}
>
{isInline && !maximized ? <RightPanelResizeHandle handlers={handlers} /> : null}
<div className={cn("h-full min-h-0 w-full", collapsible && "overflow-clip")}>
<div
className="flex h-full min-h-0 min-w-0 flex-col"
style={collapsible && !maximized ? { width: `calc(${width}px - 1px)` } : undefined}
>
{useDragRegion ? <div className="electron-drag-region h-0 w-full" aria-hidden /> : null}
{props.children}
</div>
</div>
</div>
);
}