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' })} )}
); }