@elabs-ai/components-data 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +74 -0
- package/dist/index.d.ts +293 -0
- package/dist/index.js +933 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
- package/src/column-picker/column-picker.stories.tsx +65 -0
- package/src/column-picker/column-picker.test.tsx +134 -0
- package/src/column-picker/column-picker.tsx +75 -0
- package/src/column-picker/index.ts +1 -0
- package/src/data-table/data-table.stories.tsx +804 -0
- package/src/data-table/data-table.test.tsx +1513 -0
- package/src/data-table/data-table.tsx +1375 -0
- package/src/data-table/index.ts +7 -0
- package/src/facet-filter/facet-filter.stories.tsx +121 -0
- package/src/facet-filter/facet-filter.test.tsx +175 -0
- package/src/facet-filter/facet-filter.tsx +104 -0
- package/src/facet-filter/index.ts +1 -0
- package/src/filter-bar/filter-bar.stories.tsx +58 -0
- package/src/filter-bar/filter-bar.test.tsx +60 -0
- package/src/filter-bar/filter-bar.tsx +20 -0
- package/src/filter-bar/index.ts +1 -0
- package/src/index.ts +18 -0
- package/src/search-input/index.ts +1 -0
- package/src/search-input/search-input.stories.tsx +45 -0
- package/src/search-input/search-input.test.tsx +99 -0
- package/src/search-input/search-input.tsx +81 -0
- package/src/templates-data-app.stories.tsx +160 -0
- package/src/to-csv.test.ts +147 -0
- package/src/to-csv.ts +102 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/data-table/data-table.tsx","../src/search-input/search-input.tsx","../src/filter-bar/filter-bar.tsx","../src/facet-filter/facet-filter.tsx","../src/column-picker/column-picker.tsx","../src/to-csv.ts"],"sourcesContent":["\"use client\";\n\nimport { forwardRef, useCallback, useEffect, useRef, useState, type ReactNode } from \"react\";\nimport {\n flexRender,\n getCoreRowModel,\n getFilteredRowModel,\n getPaginationRowModel,\n getSortedRowModel,\n useReactTable,\n type Column,\n type ColumnDef,\n type ColumnFiltersState,\n type ColumnPinningState,\n type OnChangeFn,\n type PaginationState,\n type Row,\n type SortingState,\n type Table as TanstackTable,\n type VisibilityState,\n} from \"@tanstack/react-table\";\nimport { useVirtualizer } from \"@tanstack/react-virtual\";\nimport { ArrowDown, ArrowUp, ArrowUpDown } from \"lucide-react\";\nimport { Button, Skeleton, Spinner, useLocale } from \"@elabs-ai/components-ui\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\n\n// ─── Public types ─────────────────────────────────────────────────────────────\n\n/** Snapshot of table slice state — used for saved-view serialise/rehydrate. */\nexport interface DataTableViewState {\n sorting: SortingState;\n columnVisibility: VisibilityState;\n columnFilters: ColumnFiltersState;\n globalFilter?: string;\n pagination?: PaginationState;\n /**\n * Which columns are frozen to the left/right edge (#333). OPTIONAL on purpose:\n * the other members predate it, and a required key would break every consumer\n * that already constructs a `DataTableViewState` literal.\n */\n columnPinning?: ColumnPinningState;\n}\n\n/**\n * Argument object fired by `onServerChange` whenever a manual slice changes.\n * The consuming app should re-fetch with these params and update `data`.\n */\nexport interface DataTableServerArgs {\n pagination: PaginationState;\n sorting: SortingState;\n columnFilters: ColumnFiltersState;\n globalFilter: string;\n}\n\n/**\n * Fires when a row is activated (#337).\n *\n * Both activation paths deliver a `click`: a pointer click on the row body, and\n * a keyboard Enter/Space on the row's hidden activation `<button>` (which the\n * browser dispatches as a click). So the handler takes ONE event type — there is\n * nothing for the caller to branch on.\n */\nexport type DataTableRowClickHandler<TData> = (\n row: Row<TData>,\n event: React.MouseEvent<HTMLElement>,\n) => void;\n\n// ─── Props ────────────────────────────────────────────────────────────────────\n\nexport interface DataTableProps<TData, TValue> extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n \"children\"\n> {\n columns: ColumnDef<TData, TValue>[];\n data: TData[];\n /** Render a toolbar above the table; receives the table instance. */\n toolbar?: (table: TanstackTable<TData>) => ReactNode;\n /** Enable client-side pagination. */\n enablePagination?: boolean;\n pageSize?: number;\n /**\n * Hide the pager once there's genuinely only one page\n * (`table.getPageCount() <= 1`). Default `true`. When `manualPagination` is\n * set without `rowCount`/`pageCount`, the page count isn't knowable (TanStack\n * falls back to the current page's row count) — in that ambiguous case the\n * pager still renders regardless of this flag, so the existing dev warning\n * (#227) stays the diagnostic instead of a silently-hidden pager. Set to\n * `false` to always show the pager (e.g. while a server total is still\n * loading and you'd rather show a disabled pager than none).\n */\n hidePaginationWhenSingle?: boolean;\n\n /**\n * Controlled global filter value. When provided, the table reflects this\n * value and the component manages no internal filter state. Keep the source\n * of truth in the app and pass it down — never mutate the filter during\n * render (e.g. `table.setGlobalFilter()` in `toolbar`), which loops.\n */\n globalFilter?: string;\n /** Fires when the table requests a global-filter change (e.g. from typeahead). */\n onGlobalFilterChange?: (value: string) => void;\n\n // ── Controlled slices for saved views ─────────────────────────────────────\n /** Controlled sorting state. When provided the component is sorted-controlled. */\n sorting?: SortingState;\n onSortingChange?: OnChangeFn<SortingState>;\n\n /** Controlled column-visibility state. */\n columnVisibility?: VisibilityState;\n onColumnVisibilityChange?: OnChangeFn<VisibilityState>;\n\n /** Controlled column-filters state. */\n columnFilters?: ColumnFiltersState;\n onColumnFiltersChange?: OnChangeFn<ColumnFiltersState>;\n\n /** Controlled pagination state. */\n pagination?: PaginationState;\n onPaginationChange?: OnChangeFn<PaginationState>;\n\n /**\n * Controlled column-pinning state (#333) — the columns frozen against the\n * left and/or right edge while the rest of the table scrolls horizontally.\n * When provided the component is pinning-controlled; otherwise it manages the\n * slice internally and can be seeded once via `initialView.columnPinning`.\n *\n * A pinned column MUST declare an explicit `size` in its `ColumnDef`: the\n * sticky offset is computed from TanStack's `column.getStart(\"left\")` /\n * `getAfter(\"right\")`, which sum the DECLARED sizes, so an auto-width column\n * would render at a width that doesn't match its own offset. A dev-only\n * warning fires for a pinned column with no `size`.\n *\n * Pinning is a LAYOUT concern, not a query concern — it is client-only and\n * never joins `DataTableServerArgs` / `onServerChange`.\n */\n columnPinning?: ColumnPinningState;\n onColumnPinningChange?: OnChangeFn<ColumnPinningState>;\n\n /**\n * One-shot rehydrate for uncontrolled slices only (ignored for any slice\n * whose corresponding controlled prop is set). Maps to `useReactTable`'s\n * `initialState`.\n */\n initialView?: Partial<DataTableViewState>;\n\n // ── Server-side data model ──────────────────────────────────────────────────\n /**\n * When true, sorting is handled by the server. Pass `sorting` (controlled)\n * and handle `onServerChange` to re-fetch with the new sort params.\n * NOTE: controlled ≠ manual — a controlled `sorting` with `manualSorting:false`\n * still sorts locally.\n */\n manualSorting?: boolean;\n /**\n * When true, filtering is handled by the server.\n * NOTE: a controlled `columnFilters` with `manualFiltering:false` still\n * filters locally.\n */\n manualFiltering?: boolean;\n /** When true, pagination is handled by the server. */\n manualPagination?: boolean;\n\n /**\n * Total row count — used by the server model so TanStack can derive\n * page count. Required when `manualPagination` is true and `pageCount` is\n * not provided.\n */\n rowCount?: number;\n /**\n * Total page count — alternative to `rowCount` for server pagination. When\n * both are provided, `pageCount` wins.\n */\n pageCount?: number;\n\n /**\n * Fired after any manual-slice change with the current {pagination, sorting,\n * columnFilters, globalFilter}. The component never fetches; the app must\n * re-fetch and update `data`.\n */\n onServerChange?: (args: DataTableServerArgs) => void;\n\n /** When true: overlay spinner; on empty+loading show skeleton rows instead of empty message. */\n loading?: boolean;\n\n // ── Virtualization ─────────────────────────────────────────────────────────\n /**\n * Opt-in to row virtualization (for very large lists). Mutually exclusive\n * with enablePagination in practice — if both are set, virtualization wins\n * and pagination is silently ignored.\n */\n enableRowVirtualization?: boolean;\n /** Estimated row height in px (used by the virtualizer). Default: 40. */\n estimateRowHeight?: number;\n /** Virtualizer overscan (rows rendered above/below the visible window). Default: 8. */\n overscan?: number;\n /** CSS max-height of the scroll container in virtualized mode. Default: \"32rem\". */\n maxBodyHeight?: string;\n\n /**\n * Number of skeleton placeholder rows to render while loading.\n * Defaults to `pageSize` (non-virtualized) or `min(10, pageSize)` (virtualized).\n */\n loadingRows?: number;\n\n /**\n * Gentle alternating row stripes (\"zebra\") as the row-separation cue, instead\n * of a hairline divider between every row. Default `true` — the stripe is the\n * single separation gesture, so rows carry no divider (a divider on a striped\n * row would be a redundant boundary). Set `false` for the classic line model\n * (a `border-border-strong` divider between rows, no stripes).\n */\n zebra?: boolean;\n\n /**\n * Fires when a row is activated (#337). Setting it adds ONE activation\n * target per row: a visually-hidden `<button>` rendered inside the row's\n * first cell. That button is the row's keyboard tab stop and its accessible\n * name; a pointer click anywhere else in the row resolves to the same\n * handler, so mouse and keyboard converge on one control instead of two\n * competing ones (a focusable `<tr>` cannot carry an activation role without\n * destroying `row` table semantics).\n *\n * Guarded: a click that originates on a nested interactive control\n * (button/link/input/checkbox/…) or is the tail end of a text-selection drag\n * does NOT fire it. Optional; omitting it renders rows exactly as before.\n */\n onRowClick?: DataTableRowClickHandler<TData>;\n /**\n * Accessible name for the row's hidden activation button (#337). Only read\n * when `onRowClick` is set. Defaults to the row's first visible cell value\n * when that is a string/number (the row's primary identifier — the same\n * naming a link in that cell would get), else the localized\n * `data.table.rowAction` fallback. Supply it whenever the first cell isn't a\n * good name for the row.\n */\n rowActionLabel?: (row: Row<TData>) => string;\n /**\n * Per-row className, merged alongside the existing zebra/line/hover/selected\n * classes via `cn()` (so it can't accidentally clobber them) (#337).\n */\n rowClassName?: (row: Row<TData>) => string;\n\n /**\n * Accessible name for the table, rendered as a visually-hidden (`sr-only`)\n * `<caption>` — the first child of `<table>`. Screen readers announce it as\n * the table's name and it makes column-header navigation meaningful.\n * Optional; omit it only when the surrounding page already labels the table\n * unambiguously (e.g. an adjacent heading) (#338).\n */\n caption?: ReactNode;\n\n /** Message shown when there are no rows and not loading. */\n emptyMessage?: ReactNode;\n className?: string;\n}\n\n// ─── Row-click guards (module-level — shared by every renderRow call) ────────\n\n/**\n * CSS selector for anything inside a row that owns its own click/keyboard\n * behavior. A row click must not fire when the user actually meant to\n * activate one of these — the row is the activation target for everything\n * ELSE in the row, not a second competing target (#337).\n */\nconst ROW_CLICK_GUARD_SELECTOR =\n 'button, a[href], input, select, textarea, label, summary, [role=\"button\"], [role=\"link\"], [role=\"menuitem\"], [role=\"checkbox\"], [role=\"radio\"], [role=\"switch\"], [role=\"tab\"], [contenteditable=\"true\"]';\n\nfunction isInteractiveEventTarget(target: EventTarget | null): boolean {\n return target instanceof Element && target.closest(ROW_CLICK_GUARD_SELECTOR) !== null;\n}\n\n/**\n * True while the user is completing a text-selection drag — a row click must\n * not fire for the mouseup/click that ends a selection (#337).\n */\nfunction isActiveTextSelection(): boolean {\n if (typeof window === \"undefined\" || typeof window.getSelection !== \"function\") return false;\n return window.getSelection()?.type === \"Range\";\n}\n\n// ─── Pinning helpers (module-level) ──────────────────────────────────────────\n\n/**\n * The 1px seam between the frozen block and the scrolling block (#333), minus\n * the side — `pinnedCellGeometry` appends `after:end-0` or `after:start-0`.\n *\n * A pseudo-element rather than a `border-e`/`border-s` on purpose: see the note\n * in `pinnedCellGeometry`. Token-backed (`bg-border-strong`, the strong rung per\n * ADR 0010) and no shadow, so a shadowless surface (\n * `data-decoration=\"8|9|10\"`) cannot delete it.\n */\nconst PINNED_SEAM_CLASS =\n \"after:pointer-events-none after:absolute after:inset-y-0 after:w-px after:bg-border-strong after:content-['']\";\n\n/**\n * Ids of leaf columns whose ORIGINAL `ColumnDef` declares no `size` (#333).\n *\n * Deliberately reads the raw `columns` prop rather than `column.columnDef`:\n * TanStack merges its `defaultColumnSizing` (`size: 150`) into every resolved\n * column def, so the resolved def can never distinguish \"the author sized this\"\n * from \"the author left it to the default\" — and the whole point of the pinned\n * `size` warning is to catch the second case.\n *\n * Mirrors TanStack's own id resolution: `columnDef.id`, else the `accessorKey`\n * with `.` → `_`, else a string `header`.\n */\nfunction unsizedColumnIds<TData, TValue>(defs: readonly ColumnDef<TData, TValue>[]): Set<string> {\n const out = new Set<string>();\n const walk = (list: readonly ColumnDef<TData, TValue>[]) => {\n for (const def of list) {\n const group = def as { columns?: ColumnDef<TData, TValue>[] };\n if (group.columns) {\n walk(group.columns);\n continue;\n }\n if (def.size !== undefined) continue;\n const accessorKey = (def as { accessorKey?: string | number }).accessorKey;\n const id =\n def.id ??\n (accessorKey !== undefined\n ? String(accessorKey).replace(/\\./gu, \"_\")\n : typeof def.header === \"string\"\n ? def.header\n : undefined);\n if (id) out.add(id);\n }\n };\n walk(defs);\n return out;\n}\n\n// ─── Component (inner, generic) ───────────────────────────────────────────────\n\n/**\n * Branded TanStack Table wrapper with sorting, global filtering, column\n * visibility and optional pagination. The toolbar render-prop hands you the\n * table instance so SearchInput / FacetFilter / ColumnPicker can drive it.\n *\n * Every slice (sorting / columnVisibility / columnFilters / pagination) is\n * independently controllable. Uncontrolled slices are managed internally.\n * Pass `manualSorting` / `manualFiltering` / `manualPagination` to opt into\n * server-driven data; `onServerChange` fires after each slice change so the\n * app can re-fetch.\n *\n * Accepts a forwarded `ref` to the outermost wrapper `<div>` and spreads any\n * additional HTML div props (e.g. `id`, `aria-*`, `data-*`) onto that element.\n */\nfunction DataTableInner<TData, TValue>(\n {\n columns,\n data,\n toolbar,\n enablePagination = false,\n pageSize = 10,\n hidePaginationWhenSingle = true,\n\n // Global filter\n globalFilter: globalFilterProp,\n onGlobalFilterChange,\n\n // Controlled slices\n sorting: sortingProp,\n onSortingChange: onSortingChangeProp,\n columnVisibility: columnVisibilityProp,\n onColumnVisibilityChange: onColumnVisibilityChangeProp,\n columnFilters: columnFiltersProp,\n onColumnFiltersChange: onColumnFiltersChangeProp,\n pagination: paginationProp,\n onPaginationChange: onPaginationChangeProp,\n columnPinning: columnPinningProp,\n onColumnPinningChange: onColumnPinningChangeProp,\n\n // Saved views rehydration\n initialView,\n\n // Server-side model\n manualSorting = false,\n manualFiltering = false,\n manualPagination = false,\n rowCount,\n pageCount,\n onServerChange,\n\n // Loading\n loading = false,\n loadingRows,\n\n // Virtualization\n enableRowVirtualization = false,\n estimateRowHeight = 40,\n overscan = 8,\n maxBodyHeight = \"32rem\",\n\n zebra = true,\n onRowClick,\n rowActionLabel,\n rowClassName,\n caption,\n emptyMessage = \"No results.\",\n className,\n ...rest\n }: DataTableProps<TData, TValue>,\n ref: React.Ref<HTMLDivElement>,\n) {\n // Component microcopy goes through the locale seam (ADR 0017) — a screen-reader\n // user in a non-English locale has no workaround for a hardcoded accessible name.\n const { t } = useLocale();\n\n // ── Controlled/uncontrolled detection ────────────────────────────────────\n const isSortingControlled = sortingProp !== undefined;\n const isColumnVisibilityControlled = columnVisibilityProp !== undefined;\n const isColumnFiltersControlled = columnFiltersProp !== undefined;\n const isPaginationControlled = paginationProp !== undefined;\n const isFilterControlled = globalFilterProp !== undefined;\n const isColumnPinningControlled = columnPinningProp !== undefined;\n\n // ── Internal state (only drives a slice when uncontrolled) ───────────────\n const [internalSorting, setInternalSorting] = useState<SortingState>(\n () => initialView?.sorting ?? [],\n );\n const [internalColumnVisibility, setInternalColumnVisibility] = useState<VisibilityState>(\n () => initialView?.columnVisibility ?? {},\n );\n const [internalColumnFilters, setInternalColumnFilters] = useState<ColumnFiltersState>(\n () => initialView?.columnFilters ?? [],\n );\n const [internalPagination, setInternalPagination] = useState<PaginationState>(\n () =>\n initialView?.pagination ?? {\n pageIndex: 0,\n pageSize,\n },\n );\n const [internalGlobalFilter, setInternalGlobalFilter] = useState<string>(\n () => initialView?.globalFilter ?? \"\",\n );\n const [internalColumnPinning, setInternalColumnPinning] = useState<ColumnPinningState>(\n () => initialView?.columnPinning ?? { left: [], right: [] },\n );\n\n // ── Resolved state (controlled wins over internal) ───────────────────────\n const sorting = isSortingControlled ? sortingProp : internalSorting;\n const columnVisibility = isColumnVisibilityControlled\n ? columnVisibilityProp\n : internalColumnVisibility;\n const columnFilters = isColumnFiltersControlled ? columnFiltersProp : internalColumnFilters;\n const pagination = isPaginationControlled ? paginationProp : internalPagination;\n const globalFilter = isFilterControlled ? globalFilterProp : internalGlobalFilter;\n const columnPinning = isColumnPinningControlled ? columnPinningProp : internalColumnPinning;\n\n // ── Refs for post-change server callback ─────────────────────────────────\n // We need the current values of ALL slices when any one fires; use refs to\n // avoid stale closures without adding them as deps.\n const sortingRef = useRef(sorting);\n sortingRef.current = sorting;\n const columnFiltersRef = useRef(columnFilters);\n columnFiltersRef.current = columnFilters;\n const paginationRef = useRef(pagination);\n paginationRef.current = pagination;\n const globalFilterRef = useRef(globalFilter);\n globalFilterRef.current = globalFilter;\n const columnVisibilityRef = useRef(columnVisibility);\n columnVisibilityRef.current = columnVisibility;\n const columnPinningRef = useRef(columnPinning);\n columnPinningRef.current = columnPinning;\n\n // ── Dev-only guard: manualPagination needs a total to compute page count ──\n // Without `rowCount` (or `pageCount`), TanStack's `getPageCount()` falls back\n // to the CURRENT PAGE's row count (manual mode has no full row model), so the\n // pager silently reads \"Page 1 of 1\" with Next permanently disabled. Warn\n // once per mount so the missing prop is diagnosable instead of silent (#227).\n const warnedMissingRowCountRef = useRef(false);\n useEffect(() => {\n if (\n process.env.NODE_ENV !== \"production\" &&\n manualPagination &&\n rowCount === undefined &&\n pageCount === undefined &&\n !warnedMissingRowCountRef.current\n ) {\n warnedMissingRowCountRef.current = true;\n console.warn(\n \"[DataTable] `manualPagination` is true but neither `rowCount` nor `pageCount` was \" +\n 'provided — the pager will appear stuck (\"Page 1 of 1\", Next disabled). Pass ' +\n \"`rowCount` (or `pageCount`) so the pager can compute the total.\",\n );\n }\n }, [manualPagination, rowCount, pageCount]);\n\n /** Fire onServerChange with the LATEST slice values (post-update). */\n function fireServerChange(overrides: Partial<DataTableServerArgs> = {}) {\n if (!onServerChange) return;\n onServerChange({\n pagination: paginationRef.current,\n sorting: sortingRef.current,\n columnFilters: columnFiltersRef.current,\n globalFilter: globalFilterRef.current,\n ...overrides,\n });\n }\n\n // ── Updater helpers — all five slices resolve a functional updater against\n // their *Ref.current (the post-update value), never the render-closure\n // variable, so the resolution stays correct once these callbacks are\n // memoized (a useCallback wrap or the React Compiler) ──────────────────────\n function resolveSorting(updater: Parameters<OnChangeFn<SortingState>>[0]): SortingState {\n return typeof updater === \"function\" ? updater(sortingRef.current) : updater;\n }\n function resolveColumnVisibility(\n updater: Parameters<OnChangeFn<VisibilityState>>[0],\n ): VisibilityState {\n return typeof updater === \"function\" ? updater(columnVisibilityRef.current) : updater;\n }\n function resolveColumnFilters(\n updater: Parameters<OnChangeFn<ColumnFiltersState>>[0],\n ): ColumnFiltersState {\n return typeof updater === \"function\" ? updater(columnFiltersRef.current) : updater;\n }\n function resolvePagination(updater: Parameters<OnChangeFn<PaginationState>>[0]): PaginationState {\n return typeof updater === \"function\" ? updater(paginationRef.current) : updater;\n }\n function resolveGlobalFilter(updater: Parameters<OnChangeFn<string>>[0]): string {\n return typeof updater === \"function\" ? updater(globalFilterRef.current) : updater;\n }\n function resolveColumnPinning(\n updater: Parameters<OnChangeFn<ColumnPinningState>>[0],\n ): ColumnPinningState {\n return typeof updater === \"function\" ? updater(columnPinningRef.current) : updater;\n }\n\n // ── Row models — omit client model for manual slices ─────────────────────\n const sortedRowModel = manualSorting ? {} : { getSortedRowModel: getSortedRowModel() };\n const filteredRowModel = manualFiltering ? {} : { getFilteredRowModel: getFilteredRowModel() };\n // Only attach the client pagination row model when we actually paginate locally.\n // Under `manualPagination`, TanStack ignores a supplied `getPaginationRowModel`\n // (it returns the pre-pagination rows — i.e. the page the app already fetched),\n // so attaching it there is dead per-render work. `(A && !B) || B === A || B`,\n // but the honest single-branch form documents that manual mode needs no model.\n const paginationRowModel =\n enablePagination && !manualPagination ? { getPaginationRowModel: getPaginationRowModel() } : {};\n\n // ── Table instance ────────────────────────────────────────────────────────\n const table = useReactTable({\n data,\n columns,\n state: { sorting, columnVisibility, columnFilters, globalFilter, pagination, columnPinning },\n\n // Sorting\n onSortingChange: (updater) => {\n const next = resolveSorting(updater);\n if (!isSortingControlled) setInternalSorting(next);\n onSortingChangeProp?.(updater);\n if (manualSorting) {\n sortingRef.current = next;\n fireServerChange({ sorting: next });\n }\n },\n\n // Column visibility\n onColumnVisibilityChange: (updater) => {\n const next = resolveColumnVisibility(updater);\n if (!isColumnVisibilityControlled) setInternalColumnVisibility(next);\n onColumnVisibilityChangeProp?.(updater);\n // column visibility is never a \"manual\" server concern\n },\n\n // Column filters\n onColumnFiltersChange: (updater) => {\n const next = resolveColumnFilters(updater);\n if (!isColumnFiltersControlled) setInternalColumnFilters(next);\n onColumnFiltersChangeProp?.(updater);\n if (manualFiltering) {\n columnFiltersRef.current = next;\n fireServerChange({ columnFilters: next });\n }\n },\n\n // Global filter\n onGlobalFilterChange: (updater) => {\n const next = resolveGlobalFilter(updater);\n if (!isFilterControlled) setInternalGlobalFilter(next);\n onGlobalFilterChange?.(next);\n if (manualFiltering) {\n globalFilterRef.current = next;\n fireServerChange({ globalFilter: next });\n }\n },\n\n // Pagination\n onPaginationChange: (updater) => {\n const next = resolvePagination(updater);\n if (!isPaginationControlled) setInternalPagination(next);\n onPaginationChangeProp?.(updater);\n if (manualPagination) {\n paginationRef.current = next;\n fireServerChange({ pagination: next });\n }\n },\n\n // Column pinning — a LAYOUT slice, so unlike sorting/filtering/pagination it\n // never fires `onServerChange`: freezing a column changes nothing the server\n // would need to re-query.\n onColumnPinningChange: (updater) => {\n const next = resolveColumnPinning(updater);\n if (!isColumnPinningControlled) setInternalColumnPinning(next);\n onColumnPinningChangeProp?.(updater);\n },\n\n getCoreRowModel: getCoreRowModel(),\n ...sortedRowModel,\n ...filteredRowModel,\n ...paginationRowModel,\n\n // Server-side options\n manualSorting,\n manualFiltering,\n manualPagination,\n ...(rowCount !== undefined ? { rowCount } : {}),\n ...(pageCount !== undefined ? { pageCount } : {}),\n // No `initialState`: every slice is driven explicitly via `state` above\n // (internal slices are seeded from `initialView` at useState init), so a\n // TanStack `initialState` would be dead/misleading.\n });\n\n const rows = table.getRowModel().rows;\n // colSpan for spacer / empty / skeleton cells must match the number of cells a\n // real data row renders (`row.getVisibleCells()`) — use VISIBLE leaf columns so a\n // hidden column (a first-class slice here via columnVisibility + ColumnPicker)\n // doesn't make those rows over-span.\n const colCount = table.getVisibleLeafColumns().length;\n // Virtualized-table ARIA: only a window of rows is mounted, so assistive tech\n // can't infer the true size from the DOM. aria-rowcount counts the header row(s)\n // plus every data row; rendered data rows carry an absolute 1-based aria-rowindex\n // (header rows occupy 1..headerRowCount). Falls back to rows.length for the\n // client path; uses the server `rowCount` total when provided.\n const headerRowCount = table.getHeaderGroups().length;\n const ariaRowCount = (rowCount ?? rows.length) + headerRowCount;\n\n // ── Pinning (#333) ────────────────────────────────────────────────────────\n // Are there any pinned columns at all? Everything pinning-related is gated on\n // this so a table with no pinning renders byte-identical markup to before.\n const hasLeftPinned = (columnPinning.left?.length ?? 0) > 0;\n const hasRightPinned = (columnPinning.right?.length ?? 0) > 0;\n\n // Keep keyboard focus out from UNDER the frozen block (WCAG 2.2 SC 2.4.11,\n // \"Focus Not Obscured\"). Tabbing to a control in a centre column that is\n // currently scrolled under the frozen columns makes the browser scroll it to\n // the SCROLLPORT edge — and the browser has no idea a sticky column is parked\n // there, so the focused control lands behind it, invisibly. Measured on\n // `PinnedColumns`: at scrollLeft 295 the \"Latency (ms)\" / p50 / p95 sort\n // buttons focused at viewport x 15 / 100 / 183, all inside the 17…297 frozen\n // block. `scroll-padding` is the platform's answer — it is exactly the \"don't\n // scroll content to here\" inset that `scrollIntoView` honours. Emitted only\n // when something IS pinned, so an unpinned table keeps its previous DOM.\n const pinnedScrollPadding: React.CSSProperties = {\n ...(hasLeftPinned ? { scrollPaddingInlineStart: table.getLeftTotalSize() } : {}),\n ...(hasRightPinned ? { scrollPaddingInlineEnd: table.getRightTotalSize() } : {}),\n };\n\n // Dev-only guard: a pinned column's sticky offset is `getStart(\"left\")` /\n // `getAfter(\"right\")`, i.e. the SUM OF DECLARED SIZES of the columns beside\n // it. The table is auto-layout, so a pinned column with no `size` renders at\n // whatever width its content wants while its neighbours are offset by\n // TanStack's 150px default — the pinned block then overlaps or gaps. Warn\n // once per mount so that mismatch is diagnosable instead of silent (same\n // idiom as the #227 warning above).\n //\n // Read off the RAW `columns` prop, not `column.columnDef`: TanStack merges a\n // default `size: 150` into every resolved column def, so the merged def can\n // never tell us whether the author actually declared one.\n const warnedUnsizedPinnedRef = useRef(false);\n const pinnedIds = [...(columnPinning.left ?? []), ...(columnPinning.right ?? [])];\n const unsizedIds =\n process.env.NODE_ENV === \"production\" || pinnedIds.length === 0\n ? null\n : unsizedColumnIds(columns);\n const pinnedWithoutSizeKey = unsizedIds\n ? pinnedIds.filter((id) => unsizedIds.has(id)).join(\",\")\n : \"\";\n useEffect(() => {\n if (\n process.env.NODE_ENV !== \"production\" &&\n pinnedWithoutSizeKey !== \"\" &&\n !warnedUnsizedPinnedRef.current\n ) {\n warnedUnsizedPinnedRef.current = true;\n console.warn(\n \"[DataTable] Pinned column(s) without an explicit `size` in their `ColumnDef`: \" +\n `${pinnedWithoutSizeKey}. Sticky offsets are computed from the declared sizes, so an ` +\n \"auto-width pinned column will render at a width that doesn't match its own offset. \" +\n \"Give every pinned column a `size`.\",\n );\n }\n }, [pinnedWithoutSizeKey]);\n\n /**\n * Sticky positioning for one pinned header/body cell (#333).\n *\n * Returns `null` for an unpinned column so the caller emits no `style`, no\n * `data-pinned` and no extra classes — that is what keeps a table with no\n * pinning identical to how it rendered before this feature existed.\n *\n * The offset comes from TanStack (`getStart(\"left\")` sums the widths of the\n * left-pinned columns before this one; `getAfter(\"right\")` sums the\n * right-pinned columns after it), and the same declared `size` is forced onto\n * the cell as `width`/`min`/`max` so the rendered width and the offset agree\n * under the table's auto layout.\n */\n function pinnedCellGeometry(column: Column<TData, unknown>) {\n const pinned = column.getIsPinned();\n if (pinned === false) return null;\n const size = column.getSize();\n const style: React.CSSProperties = {\n width: size,\n minWidth: size,\n maxWidth: size,\n ...(pinned === \"left\"\n ? { left: column.getStart(\"left\") }\n : { right: column.getAfter(\"right\") }),\n };\n return {\n pinned,\n style,\n // The seam between the frozen block and the scrolling block is the SOLE\n // structural cue between two regions that share one row fill and one\n // zebra stripe — delete it and a sighted user cannot tell them apart — so\n // it takes the strong rung (ADR 0010 decision test). No shadow: ADR 0020's\n // `--shadow-strength: 0` (`data-decoration=\"8|9|10\"`) would\n // erase a shadow-only cue entirely.\n //\n // It is drawn as a 1px `::after` INSIDE the cell, NOT as `border-e` /\n // `border-s`. A real border cannot work here: Tailwind's Preflight puts\n // the table in the COLLAPSED border model, and a collapsed border is\n // painted by the <table> at the cell's STATIC position — it does not\n // travel with a `position: sticky` cell, and the cell's own opaque fill\n // (which it needs, see `pinnedCellFillClass`) then paints over it. Measured\n // in Chromium on `Data/DataTable → PinnedColumns`: with `border-e` the\n // seam pixel read `143,143,143` (light `--border-strong`) at\n // scrollLeft 0 and `245,245,245` (the plain cell fill — i.e. GONE) once\n // scrolled, in all three themes and on both edges. So the one cue vanished\n // exactly when the freeze was doing something. The `::after` lives in the\n // sticky cell's own stacking context, so it moves with it.\n edgeClass:\n pinned === \"left\"\n ? column.getIsLastColumn(\"left\")\n ? PINNED_SEAM_CLASS + \" after:end-0\"\n : \"\"\n : column.getIsFirstColumn(\"right\")\n ? PINNED_SEAM_CLASS + \" after:start-0\"\n : \"\",\n };\n }\n\n // ── Scroll container ref for virtualizer ─────────────────────────────────\n const scrollRef = useRef<HTMLDivElement>(null);\n\n // ── Virtualizer (only active in virtualized branch) ───────────────────────\n const virtualizer = useVirtualizer({\n count: enableRowVirtualization ? rows.length : 0,\n getScrollElement: () => (enableRowVirtualization ? scrollRef.current : null),\n estimateSize: () => estimateRowHeight,\n overscan,\n enabled: enableRowVirtualization,\n });\n\n const virtualItems = enableRowVirtualization ? virtualizer.getVirtualItems() : [];\n const totalSize = enableRowVirtualization ? virtualizer.getTotalSize() : 0;\n const paddingTop = virtualItems.length > 0 ? (virtualItems[0]?.start ?? 0) : 0;\n const paddingBottom =\n totalSize > 0 ? totalSize - (virtualItems[virtualItems.length - 1]?.end ?? 0) : 0;\n\n // ── Plain-branch scroll container: overflow measurement ────────────────────\n // #330: the non-virtualized branch's scroll box is `overflow-auto` (it used to\n // clip). Everything that box exposes is gated on MEASURED overflow, because a\n // table that fits must stay exactly as it was:\n // - the keyboard tab stop + its accessible name (WCAG 2.1.1 / axe\n // `scrollable-region-focusable`) — a table that doesn't scroll must NOT\n // gain a focus stop that does nothing and announces \"scrollable\" falsely;\n // - the edge fades, which only make sense when content continues off-edge.\n // So a desktop-width table is a total no-op: no tab stop, no label, no fade.\n const plainScrollRef = useRef<HTMLDivElement>(null);\n const [scrollOverflows, setScrollOverflows] = useState(false);\n const [canScrollLeft, setCanScrollLeft] = useState(false);\n const [canScrollRight, setCanScrollRight] = useState(false);\n\n const updateScrollAffordance = useCallback(() => {\n const el = plainScrollRef.current;\n if (!el) return;\n // 1px tolerance absorbs sub-pixel layout rounding, which would otherwise\n // report a permanent 0.5px overflow on a table that visually fits.\n setScrollOverflows(\n el.scrollWidth > el.clientWidth + 1 || el.scrollHeight > el.clientHeight + 1,\n );\n setCanScrollLeft(el.scrollLeft > 0);\n setCanScrollRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 1);\n }, []);\n\n useEffect(() => {\n const el = plainScrollRef.current;\n if (!el) return;\n updateScrollAffordance();\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(updateScrollAffordance);\n // Observe the CONTAINER (viewport changes) and the <table> inside it\n // (content changes its intrinsic width without resizing the container).\n observer.observe(el);\n if (el.firstElementChild) observer.observe(el.firstElementChild);\n return () => observer.disconnect();\n // Column/row-count changes can also change the table's intrinsic width.\n }, [updateScrollAffordance, colCount, rows.length]);\n\n // ─── Empty / loading state ───────────────────────────────────────────────\n const showEmpty = !loading && rows.length === 0;\n const showSkeletons = loading && rows.length === 0;\n\n // Number of skeleton rows to show — caller can override via `loadingRows`.\n const skeletonRowCount = loadingRows ?? pageSize;\n\n // ─── Render helpers ───────────────────────────────────────────────────────\n\n /**\n * thead — sticky in virtualized mode, normal otherwise.\n * `withRowIndex` (virtualized only) sets the header row's `aria-rowindex` so the\n * windowed `aria-rowcount` on the table stays internally consistent with the\n * absolute indices on the data rows.\n */\n function renderThead(sticky: boolean, withRowIndex = false) {\n return (\n <thead\n className={cn(\n // #173: header bottom is the only cue between header and first data row → border-strong\n \"border-b border-border-strong\",\n // A sticky header scrolls OVER the body, so its fill must be opaque or data\n // rows bleed through the labels; the non-sticky header keeps the /60 wash.\n // z-20 (raised from z-10 for #333) puts the header row above the pinned\n // body cells (z-10) and below the pinned header corner (z-30). No visual\n // delta: nothing else in the table sits between those rungs.\n sticky ? \"sticky top-0 z-20 bg-surface-muted\" : \"bg-surface-muted/60\",\n )}\n >\n {table.getHeaderGroups().map((headerGroup, groupIndex) => (\n <tr key={headerGroup.id} aria-rowindex={withRowIndex ? groupIndex + 1 : undefined}>\n {headerGroup.headers.map((header) => {\n const geometry = pinnedCellGeometry(header.column);\n const canSort = header.column.getCanSort();\n const sorted = header.column.getIsSorted();\n // String-header fallback (`column.id`) so an icon-only / non-text\n // header still yields a named button (#230).\n const headerLabel =\n typeof header.column.columnDef.header === \"string\"\n ? header.column.columnDef.header\n : header.column.id;\n const sortStateLabel =\n sorted === \"asc\" ? \"ascending\" : sorted === \"desc\" ? \"descending\" : \"not sorted\";\n const SortIcon =\n sorted === \"asc\" ? ArrowUp : sorted === \"desc\" ? ArrowDown : ArrowUpDown;\n return (\n <th\n key={header.id}\n scope=\"col\"\n aria-sort={\n canSort\n ? sorted === \"asc\"\n ? \"ascending\"\n : sorted === \"desc\"\n ? \"descending\"\n : \"none\"\n : undefined\n }\n data-pinned={geometry?.pinned ?? undefined}\n style={geometry?.style}\n className={cn(\n \"h-10 px-3 text-start align-middle font-medium text-muted-foreground\",\n // A pinned HEADER cell is the corner where both freezes meet,\n // so it stacks above the sticky header row (z-20) which is\n // above the pinned body cells (z-10). It needs an OPAQUE\n // fill (scrolled header cells pass underneath it), and that\n // fill has to composite to exactly what its unpinned\n // neighbours show — same problem, same two-layer answer as\n // `pinnedCellFillClass`:\n // sticky branch → the row is already opaque `surface-muted`, so match it.\n // plain branch → the row is `surface-muted/60` over the\n // container's `card`, so paint `card` and\n // re-apply the /60 wash on `::before`.\n // Painting the plain branch's corner solid `surface-muted`\n // read 4-5/255 darker than the header beside it in every\n // theme (measured: 242 vs 247 light, 43 vs 40\n // dark) — the same \"floating pill\"\n // artefact #333 was filed about, moved into the header.\n geometry && \"sticky z-30\",\n geometry &&\n (sticky\n ? \"bg-surface-muted\"\n : \"bg-card before:pointer-events-none before:absolute before:inset-0 before:-z-10 before:bg-surface-muted/60 before:content-['']\"),\n // Separate cn() argument on purpose: the seam is the sole\n // structural cue between the frozen and scrolling blocks, so\n // it must not read as a \"boundary + fill in one class string\"\n // redundancy (separation:check).\n geometry?.edgeClass,\n )}\n >\n {header.isPlaceholder ? null : canSort ? (\n <button\n type=\"button\"\n onClick={header.column.getToggleSortingHandler()}\n aria-label={`Sort by ${headerLabel}, ${sortStateLabel}`}\n className=\"inline-flex items-center gap-1 rounded-sm transition-colors duration-fast ease-standard hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n {flexRender(header.column.columnDef.header, header.getContext())}\n <SortIcon\n aria-hidden=\"true\"\n className=\"size-3 shrink-0 transition-colors duration-fast ease-standard\"\n />\n </button>\n ) : (\n flexRender(header.column.columnDef.header, header.getContext())\n )}\n </th>\n );\n })}\n </tr>\n ))}\n </thead>\n );\n }\n\n /**\n * Row separation cue, keyed off the absolute row index so it stays stable\n * under virtualization (a CSS `even:`/`odd:` variant would \"swim\" as the\n * windowed `<tr>`s recycle).\n *\n * - zebra (default): a gentle `foreground/5` wash on alternate rows is the ONE\n * separation gesture; rows carry NO divider (#173's strong divider was the cue\n * only because nothing else was — the stripe replaces it, so a border would now\n * be redundant per the surface-separation rule).\n * - lines (`zebra={false}`): the classic `border-border-strong` divider between\n * rows; `last:border-b-0` so the final divider doesn't double with the\n * container's own bottom border (which reads as a heavy edge / shadow).\n */\n function rowSeparationClass(rowIndex: number): string {\n if (!zebra) return \"border-b border-border-strong last:border-b-0\";\n return rowIndex % 2 === 1 ? \"bg-foreground/5\" : \"\";\n }\n\n /**\n * Fill for a PINNED body cell (#333) — the twin of `rowSeparationClass` above,\n * and the fix for the bug this issue reports.\n *\n * A pinned cell sits above horizontally-scrolling content, so it needs an\n * OPAQUE paint or the scrolled columns read straight through its text. But the\n * row's own cues — the zebra stripe, hover, selected — are TRANSLUCENT washes\n * that live on the `<tr>`, and a single opaque `background-color` on the\n * `<td>` hides all three: that is the \"seam / floating pill\" the issue\n * describes.\n *\n * So the cell paints the opaque `bg-card` base and re-applies the row's wash on\n * a decorative `::before` layer at a NEGATIVE stack level. Inside the cell's own\n * stacking context (it has one — `sticky` + a `z-` rung) that layer paints\n * ABOVE the cell's background and BELOW its text, which is exactly the order an\n * unpinned cell gets from the `<tr>`'s translucent background.\n *\n * The wash must NOT be a background-IMAGE gradient on the cell itself: under\n * `[data-decoration]`, `decoration.css` gives every\n * `.bg-card` element the ambient grid AS a `background-image`, so a gradient\n * would overwrite it and punch a flat, ungridded rectangle into the sheet\n * exactly where the frozen column is.\n *\n * Hover and selected stay in CSS (`group-hover/row:` / `group-data-…/row:`\n * against the `group/row` on the `<tr>`) because only the browser knows the\n * pointer is over a SIBLING cell of the same row.\n *\n * Keep this in sync with `rowSeparationClass`. Known limit: a caller's own\n * `rowClassName` background is NOT mirrored here — the component can't know\n * which part of an arbitrary class string is a fill.\n */\n function pinnedCellFillClass(rowIndex: number): string {\n return cn(\n \"bg-card\",\n \"before:pointer-events-none before:absolute before:inset-0 before:-z-10 before:content-['']\",\n zebra && rowIndex % 2 === 1 && \"before:bg-foreground/5\",\n \"group-hover/row:before:bg-foreground/10\",\n \"group-data-[state=selected]/row:before:bg-accent\",\n );\n }\n\n /**\n * Accessible name for a row's hidden activation button (#337). Prefers the\n * caller's `rowActionLabel`, then the first visible cell's primitive value\n * (the row's primary identifier — the same name a link in that cell would\n * get, so screen-reader users hear \"billing, button\", not five identically\n * named buttons), then the localized generic fallback.\n */\n function rowActionName(row: (typeof rows)[number]): string {\n const explicit = rowActionLabel?.(row);\n if (explicit) return explicit;\n const firstValue = row.getVisibleCells()[0]?.getValue();\n if (typeof firstValue === \"string\" && firstValue.trim() !== \"\") return firstValue;\n if (typeof firstValue === \"number\") return String(firstValue);\n return t(\"data.table.rowAction\");\n }\n\n /** A single data row */\n function renderRow(\n row: (typeof rows)[number],\n rowIndex: number,\n extras?: React.HTMLAttributes<HTMLTableRowElement>,\n ) {\n // #337: `onRowClick` adds exactly ONE activation target per row — a\n // visually-hidden <button> in the first cell. The <tr> stays a plain `row`\n // (a focusable <tr> would be a tab stop with no activation semantics: it\n // can't take role=\"button\" without breaking the table's row/rowgroup\n // structure, so AT would announce a row and never that Enter does anything).\n const clickable = Boolean(onRowClick);\n\n function handleRowClick(event: React.MouseEvent<HTMLTableRowElement>) {\n // The hidden activation button matches this guard too, so a keyboard\n // Enter/Space — which the browser dispatches as a click that bubbles to\n // the row — is handled once, by the button, not twice.\n if (isInteractiveEventTarget(event.target)) return;\n if (isActiveTextSelection()) return;\n onRowClick?.(row, event);\n }\n\n return (\n <tr\n key={row.id}\n data-state={row.getIsSelected() ? \"selected\" : undefined}\n onClick={clickable ? handleRowClick : undefined}\n // Hover/selected are foreground-tint washes so they read more prominent than\n // the zebra stripe in the SAME direction across light/dark themes (the old\n // surface-muted/50 hover went the wrong way over a striped row).\n className={cn(\n // Color-only feedback (no transform/movement) → per\n // docs/MOTION_GUIDELINES.md item 3 this stays under OS reduced-motion\n // (only movement is neutralized); the gated duration-fast/ease-standard\n // pair already collapses toward ~0ms via --motion-factor when the user\n // or OS asks for reduced motion, matching the header sort button.\n \"transition-colors duration-fast ease-standard hover:bg-foreground/10 data-[state=selected]:bg-accent\",\n // Named group (#333) so a PINNED cell can re-apply the row's hover /\n // selected wash on top of its own opaque fill — only CSS knows the\n // pointer is over a sibling cell. Purely a selector hook: `group/row`\n // emits no style of its own.\n \"group/row\",\n rowSeparationClass(rowIndex),\n // `<tr>` isn't in the global auto-cursor-pointer role list (button/\n // menuitem/tab/…), so a clickable row needs its own cursor. The focus\n // ring is driven off the hidden button's `:focus-visible` (same\n // `has-[[data-slot=…]:focus-visible]` pattern as InputGroup) so the\n // ring paints on the ROW the user is about to activate, even though\n // focus lives on the sr-only control inside it.\n clickable &&\n \"cursor-pointer has-[[data-slot=data-table-row-action]:focus-visible]:outline-2 has-[[data-slot=data-table-row-action]:focus-visible]:-outline-offset-2 has-[[data-slot=data-table-row-action]:focus-visible]:outline-ring\",\n rowClassName?.(row),\n )}\n {...extras}\n >\n {row.getVisibleCells().map((cell, cellIndex) => {\n const geometry = pinnedCellGeometry(cell.column);\n return (\n <td\n key={cell.id}\n data-pinned={geometry?.pinned ?? undefined}\n style={geometry?.style}\n className={cn(\n \"px-3 py-2 align-middle\",\n // z-10: above the normal (unpositioned) cells it scrolls over,\n // below the sticky header row (z-20) and the pinned corner (z-30).\n geometry && \"sticky z-10\",\n geometry && pinnedCellFillClass(rowIndex),\n // Separate cn() argument — see pinnedCellGeometry's edgeClass.\n geometry?.edgeClass,\n )}\n >\n {clickable && cellIndex === 0 && (\n <button\n type=\"button\"\n data-slot=\"data-table-row-action\"\n className=\"sr-only\"\n onClick={(event) => onRowClick?.(row, event)}\n >\n {rowActionName(row)}\n </button>\n )}\n {flexRender(cell.column.columnDef.cell, cell.getContext())}\n </td>\n );\n })}\n </tr>\n );\n }\n\n /**\n * Skeleton placeholder `<tr>`s — shared by the normal and virtualized tbody\n * renderers so a markup/token/a11y fix only needs to be made once (#231).\n */\n function renderSkeletonBody(count: number) {\n return Array.from({ length: count }).map((_, i) => (\n <tr key={`skeleton-${i}`} aria-hidden=\"true\" className={rowSeparationClass(i)}>\n {Array.from({ length: colCount }).map((_, j) => (\n <td key={j} className=\"px-3 py-2 align-middle\">\n <Skeleton className=\"h-4 w-full\" />\n </td>\n ))}\n </tr>\n ));\n }\n\n /**\n * Empty-state `<tr>` — shared by the normal and virtualized tbody renderers\n * (#231).\n */\n function renderEmptyBody() {\n return (\n <tr>\n <td colSpan={colCount} className=\"h-24 px-3 text-center text-muted-foreground\">\n {emptyMessage}\n </td>\n </tr>\n );\n }\n\n // ─── Non-virtualized tbody ────────────────────────────────────────────────\n function renderTbodyNormal() {\n if (showSkeletons) {\n return <tbody>{renderSkeletonBody(skeletonRowCount)}</tbody>;\n }\n\n return <tbody>{showEmpty ? renderEmptyBody() : rows.map((row, i) => renderRow(row, i))}</tbody>;\n }\n\n // ─── Virtualized tbody ────────────────────────────────────────────────────\n function renderTbodyVirtualized() {\n if (showSkeletons) {\n // For virtualized mode, cap the visible skeleton rows at 10 unless caller\n // has explicitly set loadingRows.\n const virtualSkeletonCount = loadingRows ?? Math.min(10, pageSize);\n return <tbody>{renderSkeletonBody(virtualSkeletonCount)}</tbody>;\n }\n\n return (\n <tbody>\n {showEmpty ? (\n renderEmptyBody()\n ) : (\n <>\n {/* Top spacer — real <tr> so table layout is preserved */}\n {paddingTop > 0 && (\n <tr aria-hidden=\"true\">\n <td style={{ height: paddingTop }} colSpan={colCount} />\n </tr>\n )}\n {virtualItems.map((virtualRow) => {\n const row = rows[virtualRow.index];\n // row is guaranteed present because virtualizer.count === rows.length,\n // but TypeScript doesn't know array indexing is safe here.\n if (!row) return null;\n return renderRow(row, virtualRow.index, {\n ref: virtualizer.measureElement as React.Ref<HTMLTableRowElement>,\n \"data-index\": virtualRow.index,\n // Absolute 1-based row position; header row(s) occupy 1..headerRowCount.\n \"aria-rowindex\": headerRowCount + virtualRow.index + 1,\n } as React.HTMLAttributes<HTMLTableRowElement>);\n })}\n {/* Bottom spacer */}\n {paddingBottom > 0 && (\n <tr aria-hidden=\"true\">\n <td style={{ height: paddingBottom }} colSpan={colCount} />\n </tr>\n )}\n </>\n )}\n </tbody>\n );\n }\n\n // ─── Pagination controls ──────────────────────────────────────────────────\n function renderPagination() {\n // Virtualization wins over pagination per spec — don't render controls\n if (enableRowVirtualization) return null;\n if (!enablePagination && !manualPagination) return null;\n\n // #342: a genuinely single-page table renders a permanently-disabled\n // pager (\"Page 1 of 1\", both buttons disabled) — hide it, UNLESS the page\n // count isn't actually knowable: under `manualPagination` without a\n // `rowCount`/`pageCount`, TanStack's `getPageCount()` falls back to the\n // CURRENT page's row count, so \"<= 1\" there is a false positive for\n // \"really one page\" — the #227 dev warning above stays the diagnostic for\n // exactly that ambiguous case, so this flag doesn't also mask it.\n const pageCountUnknown = manualPagination && rowCount === undefined && pageCount === undefined;\n if (hidePaginationWhenSingle && !pageCountUnknown && table.getPageCount() <= 1) return null;\n\n return (\n <div className=\"flex items-center justify-between\">\n <p className=\"text-body text-muted-foreground\">\n Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount() || 1}\n </p>\n <div className=\"flex gap-2\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => table.previousPage()}\n disabled={!table.getCanPreviousPage()}\n >\n Previous\n </Button>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => table.nextPage()}\n disabled={!table.getCanNextPage()}\n >\n Next\n </Button>\n </div>\n </div>\n );\n }\n\n // ─── Render ───────────────────────────────────────────────────────────────\n\n // #338: visually-hidden accessible name for the table. Must be the FIRST\n // child of <table> per the HTML spec (caption immediately follows the\n // opening tag) — both branches place it before their thead.\n const captionElement = caption != null ? <caption className=\"sr-only\">{caption}</caption> : null;\n\n if (enableRowVirtualization) {\n // Virtualized branch: scroll container wraps the whole table\n // If both enablePagination and enableRowVirtualization are set,\n // virtualization wins; pagination controls are silently suppressed.\n return (\n <div ref={ref} className={cn(\"space-y-3\", className)} {...rest}>\n {toolbar ? toolbar(table) : null}\n {/* Outer border is redundant (surface change) → plain border per #173 spec.\n tabIndex={0} makes the windowed scroll region keyboard-operable — the rows\n themselves aren't focusable, so without it the off-screen rows are\n unreachable by keyboard (WCAG 2.1.1 / axe `scrollable-region-focusable`). */}\n <div\n ref={scrollRef}\n tabIndex={0}\n // Names the focus stop (WCAG 4.1.2) without a landmark role — a `role=\"region\"`\n // here would add a redundant landmark over the inner real <table>.\n aria-label={t(\"data.table.scrollRegion\")}\n aria-busy={loading || undefined}\n className=\"relative overflow-auto rounded-lg border bg-card focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n style={{ maxHeight: maxBodyHeight, ...pinnedScrollPadding }}\n >\n {/* Loading overlay */}\n {loading && rows.length > 0 && (\n <div\n role=\"status\"\n aria-live=\"polite\"\n // z-40 (raised from z-20 for #333): the overlay covers the WHOLE\n // table, so it has to sit above the pinned-column ladder (body z-10,\n // sticky header z-20, pinned header corner z-30) or a frozen column\n // would punch through the \"loading\" scrim.\n className=\"absolute inset-0 z-40 flex items-center justify-center rounded-lg bg-card/80\"\n >\n <Spinner aria-hidden=\"true\" className=\"text-foreground\" />\n <span className=\"sr-only\">Loading table data…</span>\n </div>\n )}\n <table\n aria-busy={loading || undefined}\n aria-rowcount={ariaRowCount}\n className=\"w-full caption-bottom text-body\"\n >\n {captionElement}\n {renderThead(true, true)}\n {renderTbodyVirtualized()}\n </table>\n </div>\n </div>\n );\n }\n\n // Non-virtualized branch.\n // #330: the scroll box is `overflow-auto` (was `overflow-hidden`, silently\n // clipping columns that didn't fit instead of letting them scroll) and\n // keyboard-focusable, parity with the virtualized branch above. Split into\n // an OUTER non-scrolling wrapper (keeps the rounded/border/bg chrome +\n // clip, and is the positioning context for the loading overlay + edge\n // fades) and an INNER scrolling div (the focusable, `overflow-auto` scroll\n // region) so the edge-fade affordance can stay pinned to the visible edges\n // instead of scrolling away with the table content.\n return (\n <div ref={ref} className={cn(\"space-y-3\", className)} {...rest}>\n {toolbar ? toolbar(table) : null}\n {/* Outer border is redundant (surface change) → plain border per #173 spec */}\n <div\n aria-busy={loading || undefined}\n className=\"relative overflow-hidden rounded-lg border bg-card\"\n >\n {/* Loading overlay */}\n {loading && rows.length > 0 && (\n <div\n role=\"status\"\n aria-live=\"polite\"\n // z-40 (raised from z-20 for #333): the overlay covers the WHOLE\n // table, so it has to sit above the pinned-column ladder (body z-10,\n // sticky header z-20, pinned header corner z-30) or a frozen column\n // would punch through the \"loading\" scrim.\n className=\"absolute inset-0 z-40 flex items-center justify-center rounded-lg bg-card/80\"\n >\n <Spinner aria-hidden=\"true\" className=\"text-foreground\" />\n <span className=\"sr-only\">Loading table data…</span>\n </div>\n )}\n {/* The tab stop exists ONLY while the region measurably overflows: without\n it, columns beyond the viewport are unreachable by keyboard (WCAG 2.1.1 /\n axe `scrollable-region-focusable`) — but adding it unconditionally would\n give every table that FITS a focus stop that does nothing and announces\n \"scrollable\" when it isn't. `aria-label` moves with it (WCAG 4.1.2:\n a name for a stop that exists, none for one that doesn't). No\n `role=\"region\"` — that would add a redundant landmark over the real\n <table> inside it. */}\n <div\n ref={plainScrollRef}\n data-slot=\"data-table-scroll-region\"\n tabIndex={scrollOverflows ? 0 : undefined}\n aria-label={scrollOverflows ? t(\"data.table.scrollRegion\") : undefined}\n onScroll={updateScrollAffordance}\n className=\"overflow-auto rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring\"\n style={hasLeftPinned || hasRightPinned ? pinnedScrollPadding : undefined}\n >\n <table aria-busy={loading || undefined} className=\"w-full caption-bottom text-body\">\n {captionElement}\n {renderThead(false)}\n {renderTbodyNormal()}\n </table>\n </div>\n {/* Horizontal-scroll edge fade — a token-driven affordance that only\n appears once the table actually overflows its container in that\n direction, so a desktop/wide table renders neither (visual no-op).\n\n #333: an edge with a PINNED column renders no fade. The fade lives\n outside the scroll region and would paint a 32px wash straight over\n the frozen column's own text; and the affordance is already carried\n there by the pinned block's `border-border-strong` seam, which is\n what a frozen column means (\"content slides under this edge\"). So\n the fade stays the cue for a FREE edge only. */}\n {canScrollLeft && !hasLeftPinned && (\n <div\n aria-hidden=\"true\"\n data-slot=\"data-table-scroll-fade-left\"\n className=\"pointer-events-none absolute inset-y-0 left-0 z-10 w-8 rounded-lg bg-gradient-to-r from-card to-transparent\"\n />\n )}\n {canScrollRight && !hasRightPinned && (\n <div\n aria-hidden=\"true\"\n data-slot=\"data-table-scroll-fade-right\"\n className=\"pointer-events-none absolute inset-y-0 right-0 z-10 w-8 rounded-lg bg-gradient-to-l from-card to-transparent\"\n />\n )}\n </div>\n\n {renderPagination()}\n </div>\n );\n}\n\n// ─── Public export with forwardRef + generic cast ─────────────────────────────\n//\n// React.forwardRef strips the generic parameter. The cast below restores it so\n// callers get full type inference on `columns` / `data` while still being able\n// to forward a ref to the root <div>.\n//\n// The ref prop is already declared in DataTableProps (optional) so existing\n// consumers are backward-compatible; the forwardRef call means passing a ref\n// object also works.\n\nconst DataTableWithRef = forwardRef(DataTableInner) as <TData, TValue>(\n props: DataTableProps<TData, TValue> & { ref?: React.Ref<HTMLDivElement> },\n) => React.ReactElement | null;\n\nexport { DataTableWithRef as DataTable };\n","\"use client\";\n\nimport { useId, type InputHTMLAttributes } from \"react\";\nimport { Input } from \"@elabs-ai/components-ui\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\nimport { SearchIcon } from \"@elabs-ai/components-icons\";\n\nexport interface SearchInputProps extends Omit<\n InputHTMLAttributes<HTMLInputElement>,\n \"onChange\" | \"value\"\n> {\n value: string;\n onValueChange: (value: string) => void;\n /** Visually-hidden accessible label. Defaults to \"Search\". */\n label?: string;\n containerClassName?: string;\n}\n\n/**\n * Search field with a leading icon and a clear button. Controlled.\n *\n * `disabled` (available via the extended `InputHTMLAttributes`) is how a\n * consumer signals a pending fetch (D5 — the app owns fetch state, this\n * control just reflects it; see loading-states.md). It is forwarded to the\n * `<Input>` explicitly AND gates the clear button — while disabled the clear\n * affordance is hidden so it can't mutate the filter mid-request (#269/#8).\n */\nexport function SearchInput({\n value,\n onValueChange,\n label = \"Search\",\n placeholder = \"Search…\",\n className,\n containerClassName,\n disabled,\n ...props\n}: SearchInputProps) {\n const id = useId();\n return (\n <div className={cn(\"relative w-full max-w-xs\", containerClassName)}>\n <label htmlFor={id} className=\"sr-only\">\n {label}\n </label>\n <SearchIcon\n size={16}\n className=\"pointer-events-none absolute start-2.5 top-1/2 -translate-y-1/2 text-muted-foreground\"\n />\n <Input\n id={id}\n value={value}\n onChange={(e) => onValueChange(e.target.value)}\n placeholder={placeholder}\n disabled={disabled}\n className={cn(\"ps-8\", value && \"pe-8\", className)}\n {...props}\n />\n {value && !disabled ? (\n <button\n type=\"button\"\n onClick={() => onValueChange(\"\")}\n aria-label=\"Clear search\"\n className=\"absolute end-2 top-1/2 -translate-y-1/2 rounded-sm p-0.5 text-muted-foreground transition-colors duration-fast ease-standard hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring animate-in fade-in zoom-in-95 duration-fast ease-entrance\"\n >\n <svg\n width=\"14\"\n height=\"14\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M18 6 6 18M6 6l12 12\" />\n </svg>\n </button>\n ) : null}\n </div>\n );\n}\n","import { type ReactNode } from \"react\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\n\nexport interface FilterBarProps {\n /** Left cluster: search + facet filters. */\n children: ReactNode;\n /** Right cluster: column picker, export, primary actions. */\n actions?: ReactNode;\n className?: string;\n}\n\n/** Horizontal toolbar that groups table filters and actions. */\nexport function FilterBar({ children, actions, className }: FilterBarProps) {\n return (\n <div className={cn(\"flex flex-wrap items-center justify-between gap-2\", className)}>\n <div className=\"flex flex-wrap items-center gap-2\">{children}</div>\n {actions ? <div className=\"flex items-center gap-2\">{actions}</div> : null}\n </div>\n );\n}\n","import type { ButtonHTMLAttributes } from \"react\";\nimport { forwardRef } from \"react\";\nimport {\n Badge,\n Button,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"@elabs-ai/components-ui\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\n\nexport interface FacetOption {\n label: string;\n value: string;\n}\n\nexport interface FacetFilterProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, \"title\"> {\n title: string;\n options: FacetOption[];\n /** Currently selected values (controlled). */\n selected: string[];\n onSelectedChange: (values: string[]) => void;\n}\n\n/**\n * Multi-select faceted filter rendered as a dropdown of toggles.\n *\n * `disabled` (forwarded to the trigger `Button`) is how a consumer signals a\n * pending fetch (D5 — the app owns fetch state, this control just reflects\n * it; see loading-states.md).\n *\n * The trigger takes `Button`'s DEFAULT size (`h-9`), not `sm` (#346): a facet\n * filter lives in a toolbar beside `Select` / `Input` / `DatePicker`, all of\n * which land on `h-9` (Select's own default rung, Input hardcoded, DatePicker\n * via this same Button default). An `sm` trigger was the lone `h-8` outlier in\n * that row, so the top and bottom edges of a filter bar didn't line up.\n */\nexport const FacetFilter = forwardRef<HTMLButtonElement, FacetFilterProps>(function FacetFilter(\n { title, options, selected, onSelectedChange, className, ...props },\n ref,\n) {\n const selectedSet = new Set(selected);\n const toggle = (value: string) => {\n const next = new Set(selectedSet);\n if (next.has(value)) next.delete(value);\n else next.add(value);\n onSelectedChange([...next]);\n };\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button ref={ref} variant=\"outline\" className={cn(\"border-dashed\", className)} {...props}>\n {title}\n {selected.length > 0 ? (\n <Badge\n variant=\"secondary\"\n className=\"ms-1 rounded px-1.5 animate-in fade-in zoom-in-95 duration-fast ease-entrance\"\n >\n {selected.length}\n </Badge>\n ) : null}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent className=\"min-w-[12rem]\">\n <DropdownMenuLabel>{title}</DropdownMenuLabel>\n {options.map((opt) => {\n const checked = selectedSet.has(opt.value);\n return (\n <DropdownMenuItem\n key={opt.value}\n onSelect={(e) => {\n e.preventDefault();\n toggle(opt.value);\n }}\n >\n <span\n aria-hidden=\"true\"\n className={\n \"flex size-4 items-center justify-center rounded border transition-colors duration-fast ease-standard \" +\n (checked ? \"border-primary bg-primary text-primary-foreground\" : \"border-input\")\n }\n >\n {checked ? \"✓\" : \"\"}\n </span>\n {opt.label}\n </DropdownMenuItem>\n );\n })}\n {selected.length > 0 ? (\n <>\n <DropdownMenuSeparator />\n <DropdownMenuItem onSelect={() => onSelectedChange([])}>Clear filters</DropdownMenuItem>\n </>\n ) : null}\n </DropdownMenuContent>\n </DropdownMenu>\n );\n});\n\nFacetFilter.displayName = \"FacetFilter\";\n","import { type Table } from \"@tanstack/react-table\";\nimport type { ButtonHTMLAttributes, ReactElement, Ref } from \"react\";\nimport { forwardRef } from \"react\";\nimport {\n Button,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"@elabs-ai/components-ui\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\n\nexport interface ColumnPickerProps<TData> extends ButtonHTMLAttributes<HTMLButtonElement> {\n table: Table<TData>;\n /** Trigger label. Defaults to \"Columns\". */\n label?: string;\n}\n\nfunction ColumnPickerInner<TData>(\n { table, label = \"Columns\", className, ...props }: ColumnPickerProps<TData>,\n ref: Ref<HTMLButtonElement>,\n) {\n const columns = table.getAllColumns().filter((c) => c.getCanHide());\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button ref={ref} variant=\"outline\" size=\"sm\" className={cn(className)} {...props}>\n {label}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"min-w-[12rem]\">\n <DropdownMenuLabel>Toggle columns</DropdownMenuLabel>\n <DropdownMenuSeparator />\n {columns.map((column) => (\n <DropdownMenuItem\n key={column.id}\n onSelect={(e) => {\n e.preventDefault();\n column.toggleVisibility(!column.getIsVisible());\n }}\n >\n <span\n aria-hidden=\"true\"\n className={\n \"flex size-4 items-center justify-center rounded border transition-colors duration-fast ease-standard \" +\n (column.getIsVisible()\n ? \"border-primary bg-primary text-primary-foreground\"\n : \"border-input\")\n }\n >\n {column.getIsVisible() ? \"✓\" : \"\"}\n </span>\n <span className=\"capitalize\">{column.id}</span>\n </DropdownMenuItem>\n ))}\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n\nColumnPickerInner.displayName = \"ColumnPicker\";\n\n// React.forwardRef strips the generic parameter — the cast below restores it\n// (same pattern as DataTable's public export) so callers get full type\n// inference on `table` while still being able to forward a ref to the\n// trigger `Button`.\n//\n// `disabled` (available via the extended `ButtonHTMLAttributes`, forwarded to\n// the trigger) is how a consumer signals a pending fetch (D5 — the app owns\n// fetch state; see loading-states.md).\nexport const ColumnPicker = forwardRef(ColumnPickerInner) as <TData>(\n props: ColumnPickerProps<TData> & { ref?: Ref<HTMLButtonElement> },\n) => ReactElement | null;\n","/**\n * Minimal, dependency-free CSV serializer (RFC 4180).\n *\n * `toCsv` is pure + SSR-safe (no DOM, no deps). `downloadCsv` delegates the\n * browser save mechanics to `@elabs-ai/components-ui`'s shared `downloadBlob` (one home for\n * the Blob → `<a download>` dance; @elabs-ai/components-ui is already a peer dep here).\n * ChartFrame uses its own local copy of `toCsv` in @elabs-ai/components-charts to avoid a\n * cross-sibling dependency (charts → data is not allowed per the one-way rule).\n */\nimport { downloadBlob } from \"@elabs-ai/components-ui\";\n\nexport type CsvColumn<TData> = { key: keyof TData & string; header?: string };\n\nexport interface ToCsvOptions<TData> {\n /** Subset/reorder of columns. Omitted → all keys from rows[0]. */\n columns?: CsvColumn<TData>[];\n /** Emit header row. Default true. */\n header?: boolean;\n /** Field delimiter. Default \",\". */\n delimiter?: string;\n}\n\nexport interface DownloadCsvOptions<TData> extends ToCsvOptions<TData> {\n /** File name without extension. Default \"download\". */\n filename?: string;\n}\n\n/** RFC 4180 injection guard prefixes. */\nconst INJECTION_PREFIXES = [\"=\", \"+\", \"-\", \"@\"];\n\nfunction stringifyValue(value: unknown): string {\n if (value === null || value === undefined) return \"\";\n if (value instanceof Date) return value.toISOString();\n if (typeof value === \"object\") return JSON.stringify(value);\n return String(value);\n}\n\nfunction quoteField(field: string, delimiter: string): string {\n // CSV-injection guard: prefix with a single quote if the field starts with a\n // formula trigger character.\n if (INJECTION_PREFIXES.some((p) => field.startsWith(p))) {\n field = \"'\" + field;\n }\n // RFC 4180: quote iff the field contains delimiter, double-quote, CR, or LF.\n if (\n field.includes(delimiter) ||\n field.includes('\"') ||\n field.includes(\"\\n\") ||\n field.includes(\"\\r\")\n ) {\n return '\"' + field.replaceAll('\"', '\"\"') + '\"';\n }\n return field;\n}\n\n/**\n * Serialize rows to a CSV string (no DOM access — safe for SSR / jsdom).\n */\nexport function toCsv<TData extends Record<string, unknown>>(\n rows: TData[],\n opts?: ToCsvOptions<TData>,\n): string {\n const delimiter = opts?.delimiter ?? \",\";\n const includeHeader = opts?.header !== false;\n\n // Derive columns from first row when not provided.\n const firstRow = rows[0];\n const cols: CsvColumn<TData>[] =\n opts?.columns ??\n (firstRow !== undefined\n ? (Object.keys(firstRow) as (keyof TData & string)[]).map((k) => ({ key: k }))\n : []);\n\n const lines: string[] = [];\n\n if (includeHeader && cols.length > 0) {\n const headerRow = cols.map((c) => quoteField(c.header ?? c.key, delimiter)).join(delimiter);\n lines.push(headerRow);\n }\n\n for (const row of rows) {\n const line = cols.map((c) => quoteField(stringifyValue(row[c.key]), delimiter)).join(delimiter);\n lines.push(line);\n }\n\n // RFC 4180: CRLF line terminator, trailing newline.\n return lines.join(\"\\r\\n\") + (lines.length > 0 ? \"\\r\\n\" : \"\");\n}\n\n/**\n * Trigger a CSV file download in the browser. No-op in SSR environments.\n */\nexport function downloadCsv<TData extends Record<string, unknown>>(\n rows: TData[],\n opts?: DownloadCsvOptions<TData>,\n): void {\n if (typeof document === \"undefined\") return;\n\n const csv = toCsv(rows, opts);\n const blob = new Blob([csv], { type: \"text/csv;charset=utf-8;\" });\n downloadBlob(blob, (opts?.filename ?? \"download\") + \".csv\");\n}\n"],"mappings":"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","names":["_","cn","jsx","jsxs","cn","jsx","jsxs","forwardRef","Button","cn","Fragment","jsx","jsxs","FacetFilter","forwardRef","Button","DropdownMenu","DropdownMenuContent","DropdownMenuItem","DropdownMenuLabel","DropdownMenuSeparator","DropdownMenuTrigger","cn","jsx","jsxs"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elabs-ai/components-data",
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/mreimitz/elabs-components.git",
|
|
8
|
+
"directory": "packages/data"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"src"
|
|
14
|
+
],
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public",
|
|
24
|
+
"registry": "https://registry.npmjs.org/"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@tanstack/react-table": "^8.20.6",
|
|
28
|
+
"@tanstack/react-virtual": "^3.14.2",
|
|
29
|
+
"lucide-react": "^0.577.0"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"react": "^18.2.0 || ^19.0.0",
|
|
33
|
+
"react-dom": "^18.2.0 || ^19.0.0",
|
|
34
|
+
"@elabs-ai/components-icons": "4.0.0",
|
|
35
|
+
"@elabs-ai/components-ui": "4.0.0",
|
|
36
|
+
"@elabs-ai/components-tokens": "4.0.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
40
|
+
"@testing-library/react": "^16.1.0",
|
|
41
|
+
"@types/react": "^19.0.2",
|
|
42
|
+
"@types/react-dom": "^19.0.2",
|
|
43
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
44
|
+
"eslint": "^9.17.0",
|
|
45
|
+
"jsdom": "^25.0.1",
|
|
46
|
+
"react": "^19.0.0",
|
|
47
|
+
"react-dom": "^19.0.0",
|
|
48
|
+
"tsup": "^8.3.5",
|
|
49
|
+
"typescript": "^5.7.3",
|
|
50
|
+
"vitest": "^3.0.2",
|
|
51
|
+
"@elabs-ai/components-eslint-config": "0.1.0",
|
|
52
|
+
"@elabs-ai/components-icons": "4.0.0",
|
|
53
|
+
"@elabs-ai/components-tokens": "4.0.0",
|
|
54
|
+
"@elabs-ai/components-typescript-config": "0.1.0",
|
|
55
|
+
"@elabs-ai/components-ui": "4.0.0"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsup",
|
|
59
|
+
"typecheck": "tsc --noEmit",
|
|
60
|
+
"lint": "eslint .",
|
|
61
|
+
"test": "vitest run",
|
|
62
|
+
"clean": "rm -rf dist .turbo coverage"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import type { ColumnDef } from "@tanstack/react-table";
|
|
3
|
+
import { DataTable } from "../data-table";
|
|
4
|
+
import { ColumnPicker } from "./column-picker";
|
|
5
|
+
|
|
6
|
+
interface Deployment {
|
|
7
|
+
service: string;
|
|
8
|
+
env: string;
|
|
9
|
+
status: string;
|
|
10
|
+
latencyMs: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const rows: Deployment[] = [
|
|
14
|
+
{ service: "api-gateway", env: "prod", status: "healthy", latencyMs: 82 },
|
|
15
|
+
{ service: "billing", env: "prod", status: "degraded", latencyMs: 240 },
|
|
16
|
+
{ service: "search", env: "staging", status: "healthy", latencyMs: 120 },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const columns: ColumnDef<Deployment>[] = [
|
|
20
|
+
{ accessorKey: "service", header: "Service", enableHiding: false },
|
|
21
|
+
{ accessorKey: "env", header: "Environment" },
|
|
22
|
+
{ accessorKey: "status", header: "Status" },
|
|
23
|
+
{ accessorKey: "latencyMs", header: "Latency (ms)" },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const meta = {
|
|
27
|
+
title: "Data/ColumnPicker",
|
|
28
|
+
component: ColumnPicker,
|
|
29
|
+
parameters: {
|
|
30
|
+
layout: "padded",
|
|
31
|
+
docs: {
|
|
32
|
+
description: {
|
|
33
|
+
component:
|
|
34
|
+
"Column-visibility toggle for a live TanStack table. Unlike the other filter " +
|
|
35
|
+
"primitives it holds no value of its own — it takes the `table` instance the " +
|
|
36
|
+
"DataTable `toolbar` render-prop hands over and drives its visibility slice " +
|
|
37
|
+
"directly. Columns with `enableHiding: false` are omitted from the menu.",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
tags: ["autodocs"],
|
|
42
|
+
} satisfies Meta<typeof ColumnPicker<Deployment>>;
|
|
43
|
+
export default meta;
|
|
44
|
+
type Story = StoryObj<typeof meta>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Rendered over a real DataTable — toggling a column here hides it in the grid
|
|
48
|
+
* below, which is the only way to see the integration actually work.
|
|
49
|
+
*/
|
|
50
|
+
export const Default: Story = {
|
|
51
|
+
render: () => (
|
|
52
|
+
<DataTable columns={columns} data={rows} toolbar={(table) => <ColumnPicker table={table} />} />
|
|
53
|
+
),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** The trigger label is overridable when "Columns" is the wrong word for the domain. */
|
|
57
|
+
export const CustomLabel: Story = {
|
|
58
|
+
render: () => (
|
|
59
|
+
<DataTable
|
|
60
|
+
columns={columns}
|
|
61
|
+
data={rows}
|
|
62
|
+
toolbar={(table) => <ColumnPicker table={table} label="Fields" />}
|
|
63
|
+
/>
|
|
64
|
+
),
|
|
65
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* column-picker.test.tsx — smoke + table-integration lock for the column toggle (#59).
|
|
3
|
+
*
|
|
4
|
+
* ColumnPicker is the one filter primitive that does NOT take its own state: it
|
|
5
|
+
* drives a live TanStack `Table` instance. So the test drives a REAL table (the
|
|
6
|
+
* same `useReactTable` the DataTable toolbar render-prop hands over) instead of
|
|
7
|
+
* a stub — a hand-rolled fake `Table` would prove nothing about the integration
|
|
8
|
+
* that actually breaks (`getCanHide` filtering, `toggleVisibility`).
|
|
9
|
+
*/
|
|
10
|
+
import { describe, expect, it, vi } from "vitest";
|
|
11
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
12
|
+
import {
|
|
13
|
+
useReactTable,
|
|
14
|
+
getCoreRowModel,
|
|
15
|
+
type ColumnDef,
|
|
16
|
+
type VisibilityState,
|
|
17
|
+
} from "@tanstack/react-table";
|
|
18
|
+
import { ColumnPicker } from "./column-picker";
|
|
19
|
+
|
|
20
|
+
interface Row {
|
|
21
|
+
service: string;
|
|
22
|
+
env: string;
|
|
23
|
+
latencyMs: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const data: Row[] = [{ service: "billing", env: "prod", latencyMs: 240 }];
|
|
27
|
+
|
|
28
|
+
/** Renders a real table instance and exposes the picker over it. */
|
|
29
|
+
function Harness({
|
|
30
|
+
columns,
|
|
31
|
+
visibility = {},
|
|
32
|
+
onVisibilityChange,
|
|
33
|
+
label,
|
|
34
|
+
}: {
|
|
35
|
+
columns: ColumnDef<Row>[];
|
|
36
|
+
visibility?: VisibilityState;
|
|
37
|
+
onVisibilityChange?: (next: VisibilityState) => void;
|
|
38
|
+
label?: string;
|
|
39
|
+
}) {
|
|
40
|
+
const table = useReactTable({
|
|
41
|
+
data,
|
|
42
|
+
columns,
|
|
43
|
+
getCoreRowModel: getCoreRowModel(),
|
|
44
|
+
state: { columnVisibility: visibility },
|
|
45
|
+
onColumnVisibilityChange: (updater) => {
|
|
46
|
+
const next = typeof updater === "function" ? updater(visibility) : updater;
|
|
47
|
+
onVisibilityChange?.(next);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
return <ColumnPicker table={table} label={label} />;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const columns: ColumnDef<Row>[] = [
|
|
54
|
+
{ accessorKey: "service", header: "Service" },
|
|
55
|
+
{ accessorKey: "env", header: "Environment" },
|
|
56
|
+
{ accessorKey: "latencyMs", header: "Latency (ms)" },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/** Open the dropdown from the keyboard (the real keyboard path, WCAG 2.1.1). */
|
|
60
|
+
function open(name: string | RegExp = "Columns") {
|
|
61
|
+
fireEvent.keyDown(screen.getByRole("button", { name }), { key: "Enter" });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe("ColumnPicker — trigger", () => {
|
|
65
|
+
it('defaults the trigger label to "Columns"', () => {
|
|
66
|
+
render(<Harness columns={columns} />);
|
|
67
|
+
expect(screen.getByRole("button", { name: "Columns" })).toBeInTheDocument();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("uses a custom label when supplied", () => {
|
|
71
|
+
render(<Harness columns={columns} label="Fields" />);
|
|
72
|
+
expect(screen.getByRole("button", { name: "Fields" })).toBeInTheDocument();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("ColumnPicker — menu contents", () => {
|
|
77
|
+
it("lists every hideable column", () => {
|
|
78
|
+
render(<Harness columns={columns} />);
|
|
79
|
+
open();
|
|
80
|
+
expect(screen.getByRole("menuitem", { name: "service" })).toBeInTheDocument();
|
|
81
|
+
expect(screen.getByRole("menuitem", { name: "env" })).toBeInTheDocument();
|
|
82
|
+
expect(screen.getByRole("menuitem", { name: "latencyMs" })).toBeInTheDocument();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("omits a column that opts out of hiding (enableHiding: false)", () => {
|
|
86
|
+
const pinned: ColumnDef<Row>[] = [
|
|
87
|
+
{ accessorKey: "service", header: "Service", enableHiding: false },
|
|
88
|
+
{ accessorKey: "env", header: "Environment" },
|
|
89
|
+
];
|
|
90
|
+
render(<Harness columns={pinned} />);
|
|
91
|
+
open();
|
|
92
|
+
expect(screen.queryByRole("menuitem", { name: "service" })).toBeNull();
|
|
93
|
+
expect(screen.getByRole("menuitem", { name: "env" })).toBeInTheDocument();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("ColumnPicker — visibility toggling", () => {
|
|
98
|
+
it("hides a visible column through the table's visibility channel", () => {
|
|
99
|
+
const onVisibilityChange = vi.fn<(next: VisibilityState) => void>();
|
|
100
|
+
render(<Harness columns={columns} onVisibilityChange={onVisibilityChange} />);
|
|
101
|
+
open();
|
|
102
|
+
fireEvent.click(screen.getByRole("menuitem", { name: "env" }));
|
|
103
|
+
expect(onVisibilityChange).toHaveBeenCalledWith(expect.objectContaining({ env: false }));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("re-shows a hidden column", () => {
|
|
107
|
+
const onVisibilityChange = vi.fn<(next: VisibilityState) => void>();
|
|
108
|
+
render(
|
|
109
|
+
<Harness
|
|
110
|
+
columns={columns}
|
|
111
|
+
visibility={{ env: false }}
|
|
112
|
+
onVisibilityChange={onVisibilityChange}
|
|
113
|
+
/>,
|
|
114
|
+
);
|
|
115
|
+
open();
|
|
116
|
+
fireEvent.click(screen.getByRole("menuitem", { name: "env" }));
|
|
117
|
+
expect(onVisibilityChange).toHaveBeenCalledWith(expect.objectContaining({ env: true }));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("stays open after a toggle so several columns can be changed in one pass", () => {
|
|
121
|
+
render(<Harness columns={columns} onVisibilityChange={vi.fn()} />);
|
|
122
|
+
open();
|
|
123
|
+
fireEvent.click(screen.getByRole("menuitem", { name: "env" }));
|
|
124
|
+
expect(screen.getByRole("menuitem", { name: "service" })).toBeInTheDocument();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("hides the decorative check swatch from assistive tech", () => {
|
|
128
|
+
render(<Harness columns={columns} />);
|
|
129
|
+
open();
|
|
130
|
+
expect(
|
|
131
|
+
screen.getByRole("menuitem", { name: "service" }).querySelector("[aria-hidden='true']"),
|
|
132
|
+
).not.toBeNull();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { type Table } from "@tanstack/react-table";
|
|
2
|
+
import type { ButtonHTMLAttributes, ReactElement, Ref } from "react";
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Button,
|
|
6
|
+
DropdownMenu,
|
|
7
|
+
DropdownMenuContent,
|
|
8
|
+
DropdownMenuItem,
|
|
9
|
+
DropdownMenuLabel,
|
|
10
|
+
DropdownMenuSeparator,
|
|
11
|
+
DropdownMenuTrigger,
|
|
12
|
+
} from "@elabs-ai/components-ui";
|
|
13
|
+
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
14
|
+
|
|
15
|
+
export interface ColumnPickerProps<TData> extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
16
|
+
table: Table<TData>;
|
|
17
|
+
/** Trigger label. Defaults to "Columns". */
|
|
18
|
+
label?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function ColumnPickerInner<TData>(
|
|
22
|
+
{ table, label = "Columns", className, ...props }: ColumnPickerProps<TData>,
|
|
23
|
+
ref: Ref<HTMLButtonElement>,
|
|
24
|
+
) {
|
|
25
|
+
const columns = table.getAllColumns().filter((c) => c.getCanHide());
|
|
26
|
+
return (
|
|
27
|
+
<DropdownMenu>
|
|
28
|
+
<DropdownMenuTrigger asChild>
|
|
29
|
+
<Button ref={ref} variant="outline" size="sm" className={cn(className)} {...props}>
|
|
30
|
+
{label}
|
|
31
|
+
</Button>
|
|
32
|
+
</DropdownMenuTrigger>
|
|
33
|
+
<DropdownMenuContent align="end" className="min-w-[12rem]">
|
|
34
|
+
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
|
|
35
|
+
<DropdownMenuSeparator />
|
|
36
|
+
{columns.map((column) => (
|
|
37
|
+
<DropdownMenuItem
|
|
38
|
+
key={column.id}
|
|
39
|
+
onSelect={(e) => {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
column.toggleVisibility(!column.getIsVisible());
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
<span
|
|
45
|
+
aria-hidden="true"
|
|
46
|
+
className={
|
|
47
|
+
"flex size-4 items-center justify-center rounded border transition-colors duration-fast ease-standard " +
|
|
48
|
+
(column.getIsVisible()
|
|
49
|
+
? "border-primary bg-primary text-primary-foreground"
|
|
50
|
+
: "border-input")
|
|
51
|
+
}
|
|
52
|
+
>
|
|
53
|
+
{column.getIsVisible() ? "✓" : ""}
|
|
54
|
+
</span>
|
|
55
|
+
<span className="capitalize">{column.id}</span>
|
|
56
|
+
</DropdownMenuItem>
|
|
57
|
+
))}
|
|
58
|
+
</DropdownMenuContent>
|
|
59
|
+
</DropdownMenu>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
ColumnPickerInner.displayName = "ColumnPicker";
|
|
64
|
+
|
|
65
|
+
// React.forwardRef strips the generic parameter — the cast below restores it
|
|
66
|
+
// (same pattern as DataTable's public export) so callers get full type
|
|
67
|
+
// inference on `table` while still being able to forward a ref to the
|
|
68
|
+
// trigger `Button`.
|
|
69
|
+
//
|
|
70
|
+
// `disabled` (available via the extended `ButtonHTMLAttributes`, forwarded to
|
|
71
|
+
// the trigger) is how a consumer signals a pending fetch (D5 — the app owns
|
|
72
|
+
// fetch state; see loading-states.md).
|
|
73
|
+
export const ColumnPicker = forwardRef(ColumnPickerInner) as <TData>(
|
|
74
|
+
props: ColumnPickerProps<TData> & { ref?: Ref<HTMLButtonElement> },
|
|
75
|
+
) => ReactElement | null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ColumnPicker, type ColumnPickerProps } from "./column-picker";
|