import React, { useCallback, useEffect, useRef, useState } from "react"; import { cls } from "../util"; import { defaultBorderMixin } from "../styles"; export type ResizablePanelsProps = { firstPanel: React.ReactNode; secondPanel: React.ReactNode; /** Whether the first panel is visible (e.g. Sidebar) */ showFirstPanel?: boolean; /** Whether the second panel is visible (e.g. Results) */ showSecondPanel?: boolean; /** 0-100 representing the width/height of the first panel */ panelSizePercent: number; onPanelSizeChange: (sizePercent: number) => void; minPanelSizePx?: number; orientation?: "horizontal" | "vertical"; animateLayout?: boolean; className?: string; stacked?: boolean; }; export function ResizablePanels({ firstPanel, secondPanel, showFirstPanel = true, showSecondPanel = true, panelSizePercent, onPanelSizeChange, minPanelSizePx = 200, orientation = "horizontal", animateLayout = true, className, stacked = false }: ResizablePanelsProps) { const containerRef = useRef(null); const isResizingRef = useRef(false); // For local layout tracking without triggering React rerenders during drag const firstPanelRef = useRef(null); const startPosRef = useRef(0); const startSizeRef = useRef(0); const [isResizing, setIsResizing] = useState(false); const isHorizontal = orientation === "horizontal"; const handleResizeStart = useCallback((e: React.MouseEvent) => { if (!showFirstPanel || !showSecondPanel) return; e.preventDefault(); isResizingRef.current = true; setIsResizing(true); startPosRef.current = isHorizontal ? e.clientX : e.clientY; if (firstPanelRef.current) { const rect = firstPanelRef.current.getBoundingClientRect(); startSizeRef.current = isHorizontal ? rect.width : rect.height; } document.body.style.cursor = isHorizontal ? "col-resize" : "row-resize"; document.body.style.userSelect = "none"; }, [isHorizontal, showFirstPanel, showSecondPanel]); useEffect(() => { const handleMouseMove = (e: MouseEvent) => { if (!isResizingRef.current || !containerRef.current) return; const currentPos = isHorizontal ? e.clientX : e.clientY; const delta = currentPos - startPosRef.current; // Dragging right/down increases first panel size let newSize = startSizeRef.current + delta; const containerRect = containerRef.current.getBoundingClientRect(); const containerTotal = isHorizontal ? containerRect.width : containerRect.height; // Limit the maximum size to prevent pushing the second panel offscreen const maxSize = containerTotal - minPanelSizePx; newSize = Math.max(minPanelSizePx, Math.min(newSize, maxSize)); // Directly update the DOM for performance while dragging if (firstPanelRef.current) { firstPanelRef.current.style.flexBasis = `${newSize}px`; } }; const handleMouseUp = () => { if (isResizingRef.current && containerRef.current && firstPanelRef.current) { // Calculate the final percentage const containerRect = containerRef.current.getBoundingClientRect(); const firstPanelRect = firstPanelRef.current.getBoundingClientRect(); const containerSize = isHorizontal ? containerRect.width : containerRect.height; const finalSize = isHorizontal ? firstPanelRect.width : firstPanelRect.height; // Pre-set the DOM to the exact % value BEFORE re-enabling transitions. // This prevents the visual "snap" that occurs when switching from px → % // with the CSS transition active. if (containerSize > 0) { const newPercent = (finalSize / containerSize) * 100; firstPanelRef.current.style.flexBasis = `${newPercent}%`; onPanelSizeChange(Math.max(0, Math.min(100, newPercent))); } isResizingRef.current = false; setIsResizing(false); document.body.style.cursor = ""; document.body.style.userSelect = ""; } }; window.addEventListener("mousemove", handleMouseMove); window.addEventListener("mouseup", handleMouseUp); return () => { window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseup", handleMouseUp); }; }, [onPanelSizeChange, isHorizontal, minPanelSizePx]); // Calculate applied size const appliedBasis = !showFirstPanel ? "0%" : (showSecondPanel ? `${panelSizePercent}%` : "100%"); return (
{/* First Panel */}
{firstPanel}
{/* Divider */} {!stacked && showFirstPanel && showSecondPanel && (
{/* Transparent Hit Area with Pill inside */}
)} {/* Second Panel */}
{secondPanel}
); }