"use client" /** * FinderPanelView — Miller-style 3-column split for list-page hubs. * * Visual shell matches Library panel (`ListPageTreeColumnHeader`, `LIST_PAGE_SPLIT_MILLER_COLUMN_PANEL_CLASS`, * shared resizable handles) — see `list-page-split-hub-tokens.ts`. */ import * as React from "react" import { cn } from "../../lib/utils" import { ResizableHandle, ResizablePanel, ResizablePanelGroup, } from "../ui/resizable" import { useIsMobile } from "../../hooks/use-mobile" import { useSidebarReflowZoom } from "../../hooks/use-sidebar-reflow-zoom" import { ListPageTreeColumnHeader } from "./list-page-tree-column-header" import { ListPageSplitDetailsPlaceholder } from "./list-page-split-details-placeholder" import { LIST_PAGE_SPLIT_MILLER_COLUMN_PANEL_CLASS, LIST_PAGE_SPLIT_MILLER_DETAIL_PANEL_CLASS, LIST_PAGE_SPLIT_MILLER_SCROLL_BODY_CLASS, LIST_PAGE_SPLIT_RESIZABLE_GROUP_CLASS, LIST_PAGE_SPLIT_RESIZABLE_GROUP_STYLE, LIST_PAGE_SPLIT_RESIZABLE_HANDLE_CLASS, LIST_PAGE_SPLIT_RESIZABLE_PANEL_STYLE, } from "./list-page-split-hub-tokens" export interface FinderGroup { id: string label: string icon?: string accent?: string count: number } export interface FinderPanelViewProps { groups: FinderGroup[] rows: T[] getRowId: (row: T) => string | number getRowGroupId: (row: T) => string renderListRow: (row: T, isSelected: boolean) => React.ReactNode renderDetail: (row: T) => React.ReactNode emptyDetail?: React.ReactNode emptyList?: React.ReactNode defaultGroupId?: string ariaLabel?: string autoSaveId?: string className?: string style?: React.CSSProperties onAddItem?: () => void /** * When true, omit outer margin, border, and card fill so the grid fits inside * `ListPageSplitHubChrome` (shared split surface across hubs). */ embedded?: boolean /** Left column title (Library: “Categories”). */ groupsColumnTitle?: string /** Middle column title; defaults from the active group label. */ getListColumnTitle?: (activeGroup: FinderGroup | undefined) => string } /** * Roving tabindex for a `role="listbox"` column — one tab stop for the whole list, * Arrow / Home / End move focus. Same keyboard model as `ViewSegmentedControl`, * except focus does not change selection (selection stays on click / Enter / Space). */ function useListboxRovingFocus(count: number, selectedIndex: number) { const optionRefs = React.useRef<(HTMLButtonElement | null)[]>([]) const [focusedIndex, setFocusedIndex] = React.useState(null) const activeIndex = Math.min( Math.max(focusedIndex ?? selectedIndex, 0), Math.max(count - 1, 0), ) const moveActiveOption = React.useCallback( (index: number) => { if (count === 0) return const i = ((index % count) + count) % count setFocusedIndex(i) requestAnimationFrame(() => { optionRefs.current[i]?.focus() }) }, [count], ) const onOptionKeyDown = React.useCallback( (e: React.KeyboardEvent, index: number) => { if (e.key === "ArrowDown") { e.preventDefault() moveActiveOption(index + 1) } else if (e.key === "ArrowUp") { e.preventDefault() moveActiveOption(index - 1) } else if (e.key === "Home") { e.preventDefault() moveActiveOption(0) } else if (e.key === "End") { e.preventDefault() moveActiveOption(count - 1) } }, [count, moveActiveOption], ) return { optionRefs, activeIndex, onOptionKeyDown } } export function GroupsColumn({ groups, selectedGroupId, onSelect, columnTitle, }: { groups: FinderGroup[] selectedGroupId: string onSelect: (id: string) => void columnTitle: string }) { const { optionRefs, activeIndex, onOptionKeyDown } = useListboxRovingFocus( groups.length, groups.findIndex(g => g.id === selectedGroupId), ) return (
{groups.map((group, index) => { const isSelected = group.id === selectedGroupId return (