import { useCallback, useEffect, useRef, useState } from "react"; export type PaneSide = "left" | "right"; export type PaneBounds = { initial: number; min: number; max: number; }; const STEP = 16; const LEAP = 64; export function clampPane(value: number, min: number, max: number): number { return Math.min(max, Math.max(min, value)); } /** Translates an on-screen drag into the width change for the pane's edge. */ export function resizeDelta(side: PaneSide, delta: number): number { return side === "left" ? delta : -delta; } export function restorePaneWidth(stored: string | null, { initial, min, max }: PaneBounds): number { let value = Number(stored); return Number.isFinite(value) && value > 0 ? clampPane(value, min, max) : initial; } export function keyboardPaneDelta( side: PaneSide, key: string, shiftKey: boolean, width: number, { min, max }: Pick, ): number | undefined { let step = shiftKey ? LEAP : STEP; if (key === "ArrowRight") return resizeDelta(side, step); if (key === "ArrowLeft") return resizeDelta(side, -step); if (key === "Home") return min - width; if (key === "End") return max - width; } export function ResizeHandle( { label, max, min, onResize, side, width }: { label: string; max: number; min: number; onResize: (delta: number) => void; side: PaneSide; width: number; }, ) { let origin = useRef(0); let [dragging, setDragging] = useState(false); return (
{ let delta = keyboardPaneDelta(side, event.key, event.shiftKey, width, { min, max }); if (delta === undefined) return; onResize(delta); event.preventDefault(); }} onPointerDown={event => { origin.current = event.clientX; setDragging(true); event.currentTarget.setPointerCapture(event.pointerId); }} onPointerMove={event => { if (!event.currentTarget.hasPointerCapture(event.pointerId)) return; let delta = event.clientX - origin.current; origin.current = event.clientX; onResize(resizeDelta(side, delta)); }} // Lost capture covers ordinary release and every form of pointer cancellation. onLostPointerCapture={() => setDragging(false)} role="separator" tabIndex={0} /> ); } export function usePaneWidth( { active, initial, max, min, storageKey }: PaneBounds & { active: boolean; storageKey: string; }, ): readonly [number, (delta: number) => void] { let [width, setWidth] = useState(initial); let loaded = useRef(false); useEffect(() => { if (!active) return; if (!loaded.current) { loaded.current = true; let restored = restorePaneWidth(localStorage.getItem(storageKey), { initial, min, max }); if (restored !== width) { setWidth(restored); return; } } localStorage.setItem(storageKey, String(width)); }, [active, initial, max, min, storageKey, width]); let resize = useCallback( (delta: number) => setWidth(current => clampPane(current + delta, min, max)), [max, min], ); return [width, resize]; }