100 lines · 3.8 KB · TypeScript
RawDownload
import { type RefObject, useEffect, useLayoutEffect, useState } from "react"; import { type ResizableWidthHandlers, useResizableWidth } from "./useResizableWidth"; export interface PreviewPanelInlineSize {  readonly width: number;  readonly handlers: ResizableWidthHandlers;} 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 usable width. */const PREVIEW_PANEL_MAX_WIDTH_FRACTION = 0.7;/** * Floor for the column beside the panel: below this the chat column's * composer overflows, so the panel yields instead. */const SIBLING_COLUMN_MIN_WIDTH = 360;const PREVIEW_PANEL_DEFAULT_WIDTH = 540; export function usePreviewPanelInlineSize(  hostRef?: RefObject<HTMLElement | null>,  options: {    readonly enabled?: boolean | undefined;    readonly widthStorageKey?: string | undefined;    readonly defaultWidth?: number | undefined;  } = {},): PreviewPanelInlineSize {  const maxWidth = useViewportClampedMaxWidth(hostRef, options.enabled ?? true);  return useResizableWidth({    storageKey: options.widthStorageKey ?? PREVIEW_PANEL_WIDTH_STORAGE_KEY,    defaultWidth: options.defaultWidth ?? PREVIEW_PANEL_DEFAULT_WIDTH,    minWidth: PREVIEW_PANEL_MIN_WIDTH,    maxWidth,    edge: "left",  });} /** * Keep the resizable panel's upper bound in sync with the window AND the row * it sits in. Resize-aware so dragging the OS window narrower (or expanding * the app sidebar) re-clamps the stored width on the next render. */function useViewportClampedMaxWidth(  hostRef: RefObject<HTMLElement | null> | undefined,  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 = () => {      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);}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));}