import classNames from "classnames"; import { useAtomValue } from "jotai"; import type { CSSProperties, ReactNode } from "react"; import { useCallback, useMemo, useRef } from "react"; import { useLocalStorage } from "react-use"; import { activeWorkspaceAtom } from "../../hooks/useActiveWorkspace"; import { useContainerSize } from "../../hooks/useContainerQuery"; import { clamp } from "../../lib/clamp"; import type { ResizeHandleEvent } from "../ResizeHandle"; import { ResizeHandle } from "../ResizeHandle"; export type SplitLayoutLayout = "responsive" | "horizontal" | "vertical"; export interface SlotProps { orientation: "horizontal" | "vertical"; style: CSSProperties; } interface Props { name: string; firstSlot: (props: SlotProps) => ReactNode; secondSlot: null | ((props: SlotProps) => ReactNode); style?: CSSProperties; className?: string; defaultRatio?: number; minHeightPx?: number; minWidthPx?: number; layout?: SplitLayoutLayout; resizeHandleClassName?: string; } const baseProperties = { minWidth: 0 }; const areaL = { ...baseProperties, gridArea: "left" }; const areaR = { ...baseProperties, gridArea: "right" }; const areaD = { ...baseProperties, gridArea: "drag" }; const STACK_VERTICAL_WIDTH = 500; export function SplitLayout({ style, firstSlot, secondSlot, className, name, layout = "responsive", resizeHandleClassName, defaultRatio = 0.5, minHeightPx = 10, minWidthPx = 10, }: Props) { const containerRef = useRef(null); const activeWorkspace = useAtomValue(activeWorkspaceAtom); const [widthRaw, setWidth] = useLocalStorage( `${name}_width::${activeWorkspace?.id ?? "n/a"}`, ); const [heightRaw, setHeight] = useLocalStorage( `${name}_height::${activeWorkspace?.id ?? "n/a"}`, ); const width = widthRaw ?? defaultRatio; let height = heightRaw ?? defaultRatio; if (!secondSlot) { height = 0; minHeightPx = 0; } const size = useContainerSize(containerRef); const verticalBasedOnSize = size.width !== 0 && size.width < STACK_VERTICAL_WIDTH; const vertical = layout !== "horizontal" && (layout === "vertical" || verticalBasedOnSize); const styles = useMemo(() => { return { ...style, gridTemplate: vertical ? ` ' ${areaL.gridArea}' minmax(0,${1 - height}fr) ' ${areaD.gridArea}' 0 ' ${areaR.gridArea}' minmax(${minHeightPx}px,${height}fr) / 1fr ` : ` ' ${areaL.gridArea} ${areaD.gridArea} ${areaR.gridArea}' minmax(0,1fr) / ${1 - width}fr 0 ${width}fr `, }; }, [style, vertical, height, minHeightPx, width]); const handleReset = useCallback(() => { if (vertical) setHeight(defaultRatio); else setWidth(defaultRatio); }, [vertical, setHeight, defaultRatio, setWidth]); const handleResizeMove = useCallback( (e: ResizeHandleEvent) => { if (containerRef.current === null) return; // const containerRect = containerRef.current.getBoundingClientRect(); const { paddingLeft, paddingRight, paddingTop, paddingBottom } = getComputedStyle( containerRef.current, ); const $c = containerRef.current; const containerWidth = $c.clientWidth - Number.parseFloat(paddingLeft) - Number.parseFloat(paddingRight); const containerHeight = $c.clientHeight - Number.parseFloat(paddingTop) - Number.parseFloat(paddingBottom); const mouseStartX = e.xStart; const mouseStartY = e.yStart; const startWidth = containerWidth * width; const startHeight = containerHeight * height; if (vertical) { const maxHeightPx = containerHeight - minHeightPx; const newHeightPx = clamp(startHeight - (e.y - mouseStartY), minHeightPx, maxHeightPx); setHeight(newHeightPx / containerHeight); } else { const maxWidthPx = containerWidth - minWidthPx; const newWidthPx = clamp(startWidth - (e.x - mouseStartX), minWidthPx, maxWidthPx); setWidth(newWidthPx / containerWidth); } }, [width, height, vertical, minHeightPx, setHeight, minWidthPx, setWidth], ); return (
{firstSlot({ style: areaL, orientation: vertical ? "vertical" : "horizontal" })} {secondSlot && ( <> {secondSlot({ style: areaR, orientation: vertical ? "vertical" : "horizontal" })} )}
); }