import { useCallback, useEffect, useRef, useState } from 'react' const KEY = 'superbill.split' /** * Draggable two-pane split, PDF on the left and values on the right. * * The ratio is remembered: how much page a reviewer needs beside the values * depends on their screen and their document, and re-dragging it every session * would be a small tax paid hundreds of times. * * The drag listeners live on window, not the handle, so the pointer can outrun * the 6px handle mid-drag without dropping it. * * Below `md`, side-by-side stops being usable at all — squeezing both panes * into a 375px screen leaves the PDF at ~165px wide, unreadable. Below that * breakpoint this becomes two full-width tabs instead of a shrinking split; * both panes stay mounted (toggling `hidden`/`flex`, not unmounting) so * switching tabs never resets the PDF viewer's scroll or zoom. */ export function SplitPane({ left, right, min = 25, max = 75, ratio, }: { left: React.ReactNode right: React.ReactNode min?: number max?: number /** * Force a split for as long as it is set, without overwriting the reviewer's * remembered one — fullscreen widens the page and docks the values to the * right, and leaving it must restore the ratio they chose, not this one. */ ratio?: number }) { const [saved, setSaved] = useState(() => { const stored = Number(localStorage.getItem(KEY)) // The scan is the thing being read; the values are short. The page gets // the larger share by default. return stored >= min && stored <= max ? stored : 60 }) const pct = ratio ?? saved const setPct = setSaved const [dragging, setDragging] = useState(false) const [mobileTab, setMobileTab] = useState<'left' | 'right'>('left') const box = useRef(null) useEffect(() => { localStorage.setItem(KEY, String(saved)) }, [saved]) const onMove = useCallback( (e: MouseEvent) => { const rect = box.current?.getBoundingClientRect() if (!rect) return const next = ((e.clientX - rect.left) / rect.width) * 100 setPct(Math.min(max, Math.max(min, next))) }, [min, max], ) useEffect(() => { if (!dragging) return const stop = () => setDragging(false) window.addEventListener('mousemove', onMove) window.addEventListener('mouseup', stop) // The PDF pane has its own drag-to-pan handler; without this it would // interpret the divider drag as a pan and text would select across both // panes on the way. document.body.style.userSelect = 'none' document.body.style.cursor = 'col-resize' return () => { window.removeEventListener('mousemove', onMove) window.removeEventListener('mouseup', stop) document.body.style.userSelect = '' document.body.style.cursor = '' } }, [dragging, onMove]) return (
{/* Tab bar: mobile only. The drag handle is the equivalent affordance on desktop, so this disappears at md instead of coexisting with it. */}
{left}
setDragging(true)} onDoubleClick={() => setPct(52)} title="Drag to resize — double-click to reset" className={`hidden w-1.5 shrink-0 cursor-col-resize transition-colors md:block ${ dragging ? 'bg-accent-1' : 'bg-line hover:bg-muted' }`} /> {/* Pointer events off during the drag, so neither pane steals the move. */}
{right}
) }