Shared sidebar layout

This commit is contained in:
Gregory Schier
2026-03-12 14:19:29 -07:00
parent 87e60372fe
commit 47f0daabff
6 changed files with 200 additions and 141 deletions

View File

@@ -0,0 +1,105 @@
import classNames from 'classnames';
import type { CSSProperties, ReactNode } from 'react';
import { useCallback, useMemo, useRef, useState } from 'react';
import type { ResizeHandleEvent } from './ResizeHandle';
import { ResizeHandle } from './ResizeHandle';
const side = { gridArea: 'side', minWidth: 0 };
const drag = { gridArea: 'drag' };
const body = { gridArea: 'body', minWidth: 0 };
interface Props {
width: number;
onWidthChange: (width: number) => void;
hidden?: boolean;
onHiddenChange?: (hidden: boolean) => void;
defaultWidth?: number;
minWidth?: number;
className?: string;
sidebar: ReactNode;
children: ReactNode;
}
export function SidebarLayout({
width,
onWidthChange,
hidden = false,
onHiddenChange,
defaultWidth = 250,
minWidth = 50,
className,
sidebar,
children,
}: Props) {
const [isResizing, setIsResizing] = useState(false);
const startWidth = useRef<number | null>(null);
const sideWidth = hidden ? 0 : width;
const styles = useMemo<CSSProperties>(
() => ({
gridTemplate: `
' ${side.gridArea} ${drag.gridArea} ${body.gridArea}' minmax(0,1fr)
/ ${sideWidth}px 0 1fr`,
}),
[sideWidth],
);
const handleResizeStart = useCallback(() => {
startWidth.current = width;
setIsResizing(true);
}, [width]);
const handleResizeEnd = useCallback(() => {
setIsResizing(false);
startWidth.current = null;
}, []);
const handleResizeMove = useCallback(
({ x, xStart }: ResizeHandleEvent) => {
if (startWidth.current == null) return;
const newWidth = startWidth.current + (x - xStart);
if (newWidth < minWidth) {
onHiddenChange?.(true);
onWidthChange(defaultWidth);
} else {
if (hidden) onHiddenChange?.(false);
onWidthChange(newWidth);
}
},
[minWidth, hidden, onHiddenChange, onWidthChange, defaultWidth],
);
const handleReset = useCallback(() => {
onWidthChange(defaultWidth);
}, [onWidthChange, defaultWidth]);
return (
<div
style={styles}
className={classNames(
className,
'grid w-full h-full',
!isResizing && 'transition-grid',
)}
>
<div style={side} className="overflow-hidden">
{sidebar}
</div>
<ResizeHandle
style={drag}
className="-translate-x-[1px]"
justify="end"
side="right"
onResizeStart={handleResizeStart}
onResizeEnd={handleResizeEnd}
onResizeMove={handleResizeMove}
onReset={handleReset}
/>
<div style={body} className="min-w-0">
{children}
</div>
</div>
);
}

View File

@@ -21,6 +21,7 @@ export type { TreeItemProps } from "./components/tree/TreeItem";
export { isSelectedFamily, selectedIdsFamily } from "./components/tree/atoms";
export { minPromiseMillis } from "./lib/minPromiseMillis";
export { ResizeHandle } from "./components/ResizeHandle";
export { SidebarLayout } from "./components/SidebarLayout";
export type { ResizeHandleEvent } from "./components/ResizeHandle";
export { SplitLayout } from "./components/SplitLayout";
export type { SplitLayoutLayout, SlotProps } from "./components/SplitLayout";