151 lines · 6.0 KB · TypeScript
RawDownload
import {  type ReactNode,  type RefObject,  useEffect,  useLayoutEffect,  useRef,  useState,} from "react"; import { isElectron } from "~/env";import { useResizableWidth } from "~/hooks/useResizableWidth";import { cn } from "~/lib/utils"; import { RightPanelResizeHandle } from "./RightPanelResizeHandle"; export type PreviewPanelMode = "inline" | "sheet" | "sidebar" | "embedded"; const PREVIEW_PANEL_WIDTH_STORAGE_KEY = "t3code:preview-panel-width";const PREVIEW_PANEL_MIN_WIDTH = 360;/** * Upper bound as a fraction of the viewport; only binds on wide screens. * On narrow windows the container clamp below is what preserves the * sibling column's space. */const PREVIEW_PANEL_MAX_WIDTH_FRACTION = 0.7;const PREVIEW_PANEL_DEFAULT_WIDTH = 540;/** * Width reserved for the sibling column (chat, pull-request list) sharing the * panel's flex row. The viewport fraction alone is not enough: the app * sidebar sits outside the row, so on narrow windows (any MacBook, even * fullscreen) the remaining 30% of the viewport minus the sidebar left the * sibling below its usable width and the composer overflowed. */const SIBLING_COLUMN_MIN_WIDTH = 360; export function getPreviewPanelMaxWidth(viewportWidth: number, containerWidth?: number): number {  const fractionCap = Math.floor(viewportWidth * PREVIEW_PANEL_MAX_WIDTH_FRACTION);  const containerCap =    containerWidth === undefined ? Infinity : Math.floor(containerWidth) - SIBLING_COLUMN_MIN_WIDTH;  // Never below the panel's own minimum: when the row cannot fit both  // columns' minimums the sibling yields, and useResizableWidth's clamp  // must not see max < min (it would resolve the inversion to min and,  // via drag-end persistence, overwrite the user's stored width).  return Math.max(PREVIEW_PANEL_MIN_WIDTH, Math.min(fractionCap, containerCap));} /** * 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. */export function PreviewPanelShell(props: {  mode: PreviewPanelMode;  maximized?: 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;}) {  const useDragRegion = isElectron && props.mode !== "sheet" && props.mode !== "embedded";  const isInline = props.mode === "inline";  const hostRef = useRef<HTMLDivElement | null>(null);  // Only inline non-maximized mode applies `width`/`maxWidth`; skip the  // container measurement (and its re-renders) everywhere else.  const maxWidth = useClampedMaxWidth(hostRef, isInline && !props.maximized);  const { width, handlers } = useResizableWidth({    storageKey: props.widthStorageKey ?? PREVIEW_PANEL_WIDTH_STORAGE_KEY,    defaultWidth: props.defaultWidth ?? PREVIEW_PANEL_DEFAULT_WIDTH,    minWidth: PREVIEW_PANEL_MIN_WIDTH,    maxWidth,    edge: "left",  });   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          ? props.maximized            ? "flex-1 border-l border-border"            : "shrink-0 border-l border-border"          : "w-full",      )}      style={isInline && !props.maximized ? { width: `${width}px` } : undefined}      data-preview-panel-mode={props.mode}      data-preview-panel-maximized={props.maximized ? "true" : "false"}    >      {isInline && !props.maximized ? <RightPanelResizeHandle handlers={handlers} /> : null}      {useDragRegion ? <div className="electron-drag-region h-0 w-full" aria-hidden /> : null}      {props.children}    </div>  );} /** * Track viewport and flex-row widths to derive an upper bound for the panel. * Resize-aware so dragging the OS window narrower (or expanding the app * sidebar) re-clamps the stored width on the next render (the hook's clamp * picks this up automatically). The row is observed rather than the panel * itself because the panel competes with its sibling column for row space. * Row measurement only runs when `enabled`; modes without a resize handle * never apply the resulting width, so they skip the observer entirely. */function useClampedMaxWidth(hostRef: RefObject<HTMLDivElement | null>, enabled: boolean): number {  const [vw, setVw] = useState(() => (typeof window === "undefined" ? 1280 : window.innerWidth));  const [containerWidth, setContainerWidth] = useState<number | undefined>(undefined);  useEffect(() => {    if (typeof window === "undefined") return;    let frame = 0;    const onResize = () => {      // Coalesce rapid resize events into one rAF tick.      if (frame !== 0) return;      frame = window.requestAnimationFrame(() => {        frame = 0;        setVw(window.innerWidth);      });    };    window.addEventListener("resize", onResize);    return () => {      window.removeEventListener("resize", onResize);      if (frame !== 0) window.cancelAnimationFrame(frame);    };  }, []);  useLayoutEffect(() => {    if (!enabled) return;    const parent = hostRef.current?.parentElement;    if (!parent) return;    // Measure before first paint: the persisted width must be clamped    // against the row on the initial render, not one observer tick later    // (the panel would flash over-wide on every mount). clientWidth is    // integral, so sub-pixel resize deltas bail out of re-rendering.    const measure = () => {      setContainerWidth(parent.clientWidth);    };    measure();    if (typeof ResizeObserver === "undefined") return;    const observer = new ResizeObserver(measure);    observer.observe(parent);    return () => {      observer.disconnect();    };  }, [hostRef, enabled]);  return getPreviewPanelMaxWidth(vw, containerWidth);}