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));}
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));
}