# Record List Browse a virtualized data grid with search, multi-column sorting, and grouped filters. ## Install ```sh npx shadcn@4.21.0 add lr-run/dir-ui/record-list#v0.1.3 ``` [Registry item with source and dependencies](https://github.com/lr-run/dir-ui/blob/v0.1.3/registry.json) Requires React 19, Tailwind CSS 4 and shadcn initialized with Base UI. Registry target placeholders resolve through components.json: @ui, @components, @hooks and @lib. Imports below use the conventional @/ prefix for illustration; the CLI rewrites them to your configured aliases. Shared components are installed once through registryDependencies. The installer merges shared theme rules into the stylesheet configured in components.json. Existing files are never forcibly overwritten; review CLI conflicts before accepting changes. No Dir runtime is required. The installer adds the grid stylesheet import to your configured CSS file. Give the list a bounded height. Use stable row IDs. The caller owns data loading and persistence. ## RecordList, useInfiniteRecords, type RecordListProps, type RecordColumn, type TableColumnState, type LoadRecordPage ```tsx import { RecordList, useInfiniteRecords, type RecordListProps, type RecordColumn, type TableColumnState, type LoadRecordPage } from '@/components/record-list/record-list.tsx' ``` | Prop | Type | Default / required | Notes | | --- | --- | --- | --- | | grid.columns | readonly RecordColumn[] | Required | Flat column definitions. Extends react-data-grid Column with type, getValue, icon, required, and format. getValue defaults to row[column.key]. | | grid.columnState / onColumnStateChange | readonly TableColumnState[] / (state: TableColumnState[]) => void | Uncontrolled | Array order defines column order; each item can override hidden, frozen, label, width, and format. Supply both props for controlled layout or persistence. | | grid.sortColumns / onSortColumnsChange | SortColumn[] / (sorts: SortColumn[]) => void | — | Controlled query sorting. Apply sorting to local rows or request a new remote dataset. The grid does not sort loaded pages independently. | | grid.onOpenRecord | (row: R) => void | — | Clicking a record name or record cell, double-clicking a row, or pressing Enter opens the detail page. The icon also calls this unless onPreviewRecord is supplied. Other cells retain selection behavior. | | grid.onPreviewRecord | (row: R) => void | onOpenRecord | Opens the record-cell preview icon separately from name navigation. Use this for a side panel. | | grid.pagination | RecordPagination | — | Set hasMore, loading, error, onLoadMore, and optional total/threshold. Loads within 240px of the end; also fills a short viewport. Error stops automatic requests and exposes Retry. | | useInfiniteRecords | ({ loadPage, queryKey, rowKeyGetter }) => InfiniteRecordState & { loadMore, retry } | — | Keep loader and rowKeyGetter stable. Include every server query parameter in queryKey. The hook aborts and discards old queries, deduplicates IDs and requests, and retries the same cursor. Loaded rows accumulate; react-data-grid virtualizes DOM rendering. | | loadPage | ({ cursor: string \| null, signal: AbortSignal }) => Promise> | — | Return rows, nextCursor (null at the end), and optional total. Pass signal to fetch. Repeated cursors are rejected. Errors preserve loaded rows. The hook loads the first page on mount. | | title | ReactNode | Required | List heading. | | grid | RecordTableProps | Required unless children is provided | react-data-grid props plus typed columns, column layout, record opening, and cursor pagination. Rows are 36px; headers are 40px. Pass either grid or children. | | grid.sort | GridSortOptions | — | Controlled Sort menu: fields, value, onChange, and optional maxSorts. | | grid.filter | GridFilterOptions | — | Controlled Filter menu: fields, value, onChange, and optional limits. Groups have at most 3 levels including the root (maxDepth defaults to 2 and is capped at 2). Valid changes apply immediately. Incomplete edits stay in the local draft until completed. | | grid.toolbar | ReactNode | — | Additional toolbar content, such as a search input or custom query controls. | | actions | ReactNode | — | Header actions such as record creation. Configure column settings through grid.columnSettings. | | grid.columnSettings | boolean \| GridColumnSettings | — | true enables built-in column settings backed by grid.columnState, including required-column protection. A GridColumnSettings object allows custom controlled settings. | | classNames | { header?: string; title?: string; content?: string; footer?: string } | — | Tailwind classes for list slots. Use grid.containerClassName and grid.toolbarClassName to compose the grid layout. | | selection | ReactNode | — | Optional caller-owned selection status. | | footer | ReactNode | — | Filtered record count, pagination, or custom footer content. | | children | ReactNode | Required unless grid is provided | Escape hatch for custom content. Cannot be combined with grid. | | className | string | '' | Set --record-list-height to customize the grid container height (default 320px). | ```ts type RecordListProps = { title: ReactNode actions?: ReactNode selection?: ReactNode footer?: ReactNode className?: string classNames?: { header?: string; title?: string; content?: string; footer?: string } } & ( | { grid: RecordTableProps; children?: never } | { grid?: never; children: ReactNode } ) type RecordColumn = Column & { type?: 'text' | 'record' | 'email' | 'url' | 'number' | 'money' | 'percent' | 'date' | 'datetime' | 'boolean' | 'status' | 'tags' | 'member'; getValue?: (row: R) => unknown; icon?: ReactNode; required?: boolean; format?: ColumnFormat } type ColumnFormat = { grouping?: boolean; decimals?: number; dateStyle?: 'short' | 'medium' | 'long'; currency?: string } type TableColumnState = { key: string; hidden?: boolean; frozen?: boolean; label?: string; width?: number; format?: ColumnFormat } type RecordPagination = { hasMore: boolean; loading: boolean; error?: string | null; onLoadMore: () => void | Promise; total?: number; threshold?: number } type RecordPage = { rows: readonly R[]; nextCursor: string | null; total?: number } ``` Every default column header, including the first record column, opens a menu on click. Column label editing is not offered. Selecting the active direction removes that sort. Other sorts keep their priority after the selected column. Record columns are frozen by default; columnState.frozen overrides that default, including false. Frozen columns appear on the left in their relative order; unfreezing restores their layout order. Header movement stays within the same frozen group; required columns cannot be hidden or moved. renderCell and renderHeaderCell override the defaults. Inline editing is disabled. Formatting affects display and clipboard output, never stored values. Async examples simulate loading entirely in browser JavaScript; sample data/query adapters are separate from the component. The local example evaluates typed comparisons, choice membership, nested groups, and stable sorting. Relative month filters in this sample use 30-day intervals; production adapters define their own calendar semantics.