Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143// src/layout/ResizeHandle.tsximport { 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 = 10const 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<number | null>(null) const startX = useRef(0) const startWidth = useRef(0) const handleRef = useRef<HTMLDivElement>(null)
const handlePointerDown = useCallback((e: PointerEvent<HTMLDivElement>) => { 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 ( <div ref={handleRef} className="resize-handle" role="separator" aria-orientation="vertical" aria-valuenow={currentWidth} aria-valuemin={minWidth} aria-valuemax={maxWidth} aria-label={ariaLabel} tabIndex={0} onPointerDown={handlePointerDown} onKeyDown={handleKeyDown} data-resizing="false" > <span className="resize-handle-grip" aria-hidden="true"> <GripVertical size={12} strokeWidth={2} /> </span> </div> )}