'use client' import * as React from 'react' import { cn } from '../../lib/utils' export interface SplitPaneProps { /** Content of the first (left / top) pane. */ first: React.ReactNode /** Content of the second (right / bottom) pane. */ second: React.ReactNode /** Split direction. `vertical` = side-by-side (a vertical divider). Default `vertical`. */ orientation?: 'vertical' | 'horizontal' /** Initial size of the first pane, as a percentage (0-100). Default 50. */ initialSize?: number /** Minimum size of the first pane as a percentage. Default 20. */ minSize?: number /** Maximum size of the first pane as a percentage. Default 80. */ maxSize?: number /** Keyboard nudge / step size in percentage points. Default 3. */ step?: number /** Persist the size under this key in localStorage. */ storageKey?: string /** Notified with the new first-pane percentage whenever it changes. */ onResize?: (size: number) => void className?: string 'aria-label'?: string } function clamp(v: number, min: number, max: number) { return Math.min(max, Math.max(min, v)) } /** * Generic, app-agnostic resizable two-pane split. Drag the divider with a * pointer or nudge it with the arrow keys. Nothing domain-specific lives here. */ export function SplitPane({ first, second, orientation = 'vertical', initialSize = 50, minSize = 20, maxSize = 80, step = 3, storageKey, onResize, className, 'aria-label': ariaLabel = 'Resize panes', }: SplitPaneProps) { const isVertical = orientation === 'vertical' const containerRef = React.useRef(null) const draggingRef = React.useRef(false) const [size, setSizeState] = React.useState(() => { if (typeof window !== 'undefined' && storageKey) { const saved = window.localStorage.getItem(storageKey) if (saved != null) { const n = Number(saved) if (!Number.isNaN(n)) return clamp(n, minSize, maxSize) } } return clamp(initialSize, minSize, maxSize) }) const setSize = React.useCallback( (next: number) => { const clamped = clamp(next, minSize, maxSize) setSizeState(clamped) onResize?.(clamped) if (typeof window !== 'undefined' && storageKey) { window.localStorage.setItem(storageKey, String(clamped)) } }, [minSize, maxSize, onResize, storageKey] ) const onKeyDown = (e: React.KeyboardEvent) => { const grow = isVertical ? 'ArrowRight' : 'ArrowDown' const shrink = isVertical ? 'ArrowLeft' : 'ArrowUp' if (e.key === grow) { e.preventDefault() setSize(size + step) } else if (e.key === shrink) { e.preventDefault() setSize(size - step) } else if (e.key === 'Home') { e.preventDefault() setSize(minSize) } else if (e.key === 'End') { e.preventDefault() setSize(maxSize) } } const onPointerMove = React.useCallback( (e: PointerEvent) => { if (!draggingRef.current || !containerRef.current) return const rect = containerRef.current.getBoundingClientRect() const pct = isVertical ? ((e.clientX - rect.left) / rect.width) * 100 : ((e.clientY - rect.top) / rect.height) * 100 setSize(pct) }, [isVertical, setSize] ) const stopDragging = React.useCallback(() => { draggingRef.current = false document.body.style.cursor = '' document.body.style.userSelect = '' }, []) React.useEffect(() => { window.addEventListener('pointermove', onPointerMove) window.addEventListener('pointerup', stopDragging) return () => { window.removeEventListener('pointermove', onPointerMove) window.removeEventListener('pointerup', stopDragging) } }, [onPointerMove, stopDragging]) const startDragging = () => { draggingRef.current = true document.body.style.cursor = isVertical ? 'col-resize' : 'row-resize' document.body.style.userSelect = 'none' } return (
{first}
{second}
) }