mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-14 13:13:31 +01:00
151 lines
4.9 KiB
TypeScript
151 lines
4.9 KiB
TypeScript
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<HTMLDivElement>(null);
|
|
const activeWorkspace = useAtomValue(activeWorkspaceAtom);
|
|
const [widthRaw, setWidth] = useLocalStorage<number>(
|
|
`${name}_width::${activeWorkspace?.id ?? 'n/a'}`,
|
|
);
|
|
const [heightRaw, setHeight] = useLocalStorage<number>(
|
|
`${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<CSSProperties>(() => {
|
|
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 (
|
|
<div
|
|
ref={containerRef}
|
|
style={styles}
|
|
className={classNames(className, 'grid w-full h-full overflow-hidden')}
|
|
>
|
|
{firstSlot({ style: areaL, orientation: vertical ? 'vertical' : 'horizontal' })}
|
|
{secondSlot && (
|
|
<>
|
|
<ResizeHandle
|
|
style={areaD}
|
|
className={classNames(
|
|
resizeHandleClassName,
|
|
vertical ? '-translate-y-1' : '-translate-x-1',
|
|
)}
|
|
onResizeMove={handleResizeMove}
|
|
onReset={handleReset}
|
|
side={vertical ? 'top' : 'left'}
|
|
justify="center"
|
|
/>
|
|
{secondSlot({ style: areaR, orientation: vertical ? 'vertical' : 'horizontal' })}
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|