Move split layout

This commit is contained in:
Gregory Schier
2026-03-12 14:00:29 -07:00
parent 0b7705d915
commit 7e7faa69df
25 changed files with 113 additions and 84 deletions

View File

@@ -8,7 +8,7 @@ import { AutoScroller } from './AutoScroller';
import { Banner } from './Banner';
import { Button } from './Button';
import { Separator } from './Separator';
import { SplitLayout } from './SplitLayout';
import { SplitLayout } from '@yaakapp-internal/ui';
import { HStack } from './Stacks';
import { IconButton } from './IconButton';
import classNames from 'classnames';
@@ -37,8 +37,8 @@ interface EventViewerProps<T> {
/** Error message to display as a banner */
error?: string | null;
/** Name for SplitLayout state persistence */
splitLayoutName: string;
/** Key for SplitLayout state persistence */
splitLayoutStorageKey: string;
/** Default ratio for the split (0.0 - 1.0) */
defaultRatio?: number;
@@ -66,7 +66,7 @@ export function EventViewer<T>({
renderDetail,
header,
error,
splitLayoutName,
splitLayoutStorageKey,
defaultRatio = 0.4,
enableKeyboardNav = true,
isLoading = false,
@@ -151,7 +151,7 @@ export function EventViewer<T>({
<div ref={containerRef} className="h-full">
<SplitLayout
layout="vertical"
name={splitLayoutName}
storageKey={splitLayoutStorageKey}
defaultRatio={defaultRatio}
minHeightPx={10}
firstSlot={({ style }) => (

View File

@@ -1,150 +0,0 @@
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>
);
}