// src/layout/ResizeHandle.tsx import { useCallback, useRef, useEffect, type KeyboardEvent, type PointerEvent } from 'react' import { GripVertical } from 'lucide-react' import './ResizeHandle.css' interface ResizeHandleProps { /** Current width of the panel being resized. */ currentWidth: number /** Minimum width constraint. */ minWidth: number /** Maximum width constraint. */ maxWidth: number /** Direction of resize: 'left' means dragging right increases width (sidebar), 'right' means dragging left increases width (right panel). */ direction: 'left' | 'right' /** Called with new width as user drags. */ onResize: (width: number) => void /** Accessible label for the resize handle. */ ariaLabel: string } const KEYBOARD_STEP = 10 const KEYBOARD_LARGE_STEP = 50 /** * A draggable resize handle for panel width adjustment. * * Supports both mouse/touch drag and keyboard (arrow keys). */ export function ResizeHandle({ currentWidth, minWidth, maxWidth, direction, onResize, ariaLabel, }: ResizeHandleProps) { const isResizing = useRef(false) const activePointerId = useRef(null) const startX = useRef(0) const startWidth = useRef(0) const handleRef = useRef(null) const handlePointerDown = useCallback((e: PointerEvent) => { if (!e.isPrimary || (e.pointerType === 'mouse' && e.button !== 0)) return e.preventDefault() isResizing.current = true activePointerId.current = e.pointerId startX.current = e.clientX startWidth.current = currentWidth e.currentTarget.setPointerCapture?.(e.pointerId) if (handleRef.current) { handleRef.current.dataset.resizing = 'true' } }, [currentWidth]) useEffect(() => { const handlePointerMove = (e: globalThis.PointerEvent) => { if (!isResizing.current || e.pointerId !== activePointerId.current) return const delta = e.clientX - startX.current // For sidebar (left side), dragging right increases width // For right panel (right side), dragging left increases width const multiplier = direction === 'left' ? 1 : -1 const newWidth = startWidth.current + delta * multiplier const clamped = Math.max(minWidth, Math.min(maxWidth, newWidth)) onResize(clamped) } const stopResizing = () => { if (!isResizing.current) return isResizing.current = false if (activePointerId.current !== null && handleRef.current?.hasPointerCapture?.(activePointerId.current)) { handleRef.current.releasePointerCapture(activePointerId.current) } activePointerId.current = null if (handleRef.current) { handleRef.current.dataset.resizing = 'false' } } const handlePointerEnd = (e: globalThis.PointerEvent) => { if (e.pointerId === activePointerId.current) stopResizing() } document.addEventListener('pointermove', handlePointerMove) document.addEventListener('pointerup', handlePointerEnd) document.addEventListener('pointercancel', handlePointerEnd) window.addEventListener('blur', stopResizing) return () => { document.removeEventListener('pointermove', handlePointerMove) document.removeEventListener('pointerup', handlePointerEnd) document.removeEventListener('pointercancel', handlePointerEnd) window.removeEventListener('blur', stopResizing) stopResizing() } }, [direction, minWidth, maxWidth, onResize]) const handleKeyDown = useCallback((e: KeyboardEvent) => { let delta = 0 const step = e.shiftKey ? KEYBOARD_LARGE_STEP : KEYBOARD_STEP if (direction === 'left') { // Sidebar: Right arrow increases, Left arrow decreases if (e.key === 'ArrowRight') delta = step else if (e.key === 'ArrowLeft') delta = -step } else { // Right panel: Left arrow increases, Right arrow decreases if (e.key === 'ArrowLeft') delta = step else if (e.key === 'ArrowRight') delta = -step } if (delta !== 0) { e.preventDefault() const newWidth = Math.max(minWidth, Math.min(maxWidth, currentWidth + delta)) onResize(newWidth) } }, [currentWidth, direction, minWidth, maxWidth, onResize]) return (
) }