/** Resizable file navigation; width belongs to the mounted tab and survives hiding the pane. */ import clsx from 'clsx' import changesCss from './ChangesTab.module.css' import { useLayoutEffect, useRef, useState, type ReactNode } from 'react' import { ChangedFiles, type ChangedFilesProps } from './ChangedFiles.tsx' import css from './FileTreePane.module.css' const MIN_WIDTH = 200 const MAX_WIDTH = 600 const DIFF_MIN_WIDTH = 320 /** Placement of a tab's file navigator; hiding it retains its viewing choices. */ export type FileTreePosition = 'right' | 'left' | 'hidden' /** * Render a keyboard- and pointer-resizable tree beside the diff. * @param props - tree rows, pane position, and the toggle's controlled element id. * @returns the bounded pane and its resize separator. */ export function FileTreePane({ position, id, ...props }: ChangedFilesProps & { readonly position: FileTreePosition readonly id: string }): ReactNode { const pane = useRef(null) const drag = useRef<{ pointer: number; x: number; width: number } | null>(null) const [width, setWidth] = useState(240) const [maximum, setMaximum] = useState(MAX_WIDTH) useLayoutEffect(() => { const parent = pane.current?.parentElement if (parent === undefined || parent === null) return const measure = (): void => { const available = parent.clientWidth // A retained tab has no measurable width while its session or pane is hidden. if (available === 0) return setMaximum(Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, available - DIFF_MIN_WIDTH))) } measure() const observer = new ResizeObserver(measure) observer.observe(parent) return () => { observer.disconnect() } }, []) const shown = Math.min(width, maximum) const direction = position === 'left' ? 1 : -1 const resize = (next: number): void => { setWidth(Math.round(Math.max(MIN_WIDTH, Math.min(maximum, next)))) } return