@elabs-ai/components-data 4.1.0 → 5.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.
Files changed (139) hide show
  1. package/README.md +1 -1
  2. package/dist/chunk-3OVN7MMO.js +2 -0
  3. package/dist/chunk-3OVN7MMO.js.map +1 -0
  4. package/dist/chunk-3UFWYVDS.js +2 -0
  5. package/dist/chunk-3UFWYVDS.js.map +1 -0
  6. package/dist/chunk-47ZBQZYS.js +47 -0
  7. package/dist/chunk-47ZBQZYS.js.map +1 -0
  8. package/dist/chunk-5K2IFOV2.js +2 -0
  9. package/dist/chunk-5K2IFOV2.js.map +1 -0
  10. package/dist/chunk-677ORWI6.js +17 -0
  11. package/dist/chunk-677ORWI6.js.map +1 -0
  12. package/dist/chunk-7ZYUY3UL.js +2 -0
  13. package/dist/chunk-7ZYUY3UL.js.map +1 -0
  14. package/dist/chunk-BEYHV3ZX.js +37 -0
  15. package/dist/chunk-BEYHV3ZX.js.map +1 -0
  16. package/dist/chunk-DBPJ7LVM.js +57 -0
  17. package/dist/chunk-DBPJ7LVM.js.map +1 -0
  18. package/dist/chunk-E3V3SYY7.js +87 -0
  19. package/dist/chunk-E3V3SYY7.js.map +1 -0
  20. package/dist/chunk-GBO56756.js +81 -0
  21. package/dist/chunk-GBO56756.js.map +1 -0
  22. package/dist/chunk-HG2YMSRM.js +95 -0
  23. package/dist/chunk-HG2YMSRM.js.map +1 -0
  24. package/dist/chunk-K2GHSRCP.js +1823 -0
  25. package/dist/chunk-K2GHSRCP.js.map +1 -0
  26. package/dist/chunk-KCB33PD3.js +43 -0
  27. package/dist/chunk-KCB33PD3.js.map +1 -0
  28. package/dist/chunk-MV2YVXEZ.js +67 -0
  29. package/dist/chunk-MV2YVXEZ.js.map +1 -0
  30. package/dist/chunk-NKVJUQYW.js +82 -0
  31. package/dist/chunk-NKVJUQYW.js.map +1 -0
  32. package/dist/chunk-OASM53LL.js +29 -0
  33. package/dist/chunk-OASM53LL.js.map +1 -0
  34. package/dist/chunk-RZ5MYZOA.js +2 -0
  35. package/dist/chunk-RZ5MYZOA.js.map +1 -0
  36. package/dist/chunk-SK6347WB.js +100 -0
  37. package/dist/chunk-SK6347WB.js.map +1 -0
  38. package/dist/chunk-TBB33PHQ.js +58 -0
  39. package/dist/chunk-TBB33PHQ.js.map +1 -0
  40. package/dist/chunk-VCLA45IC.js +150 -0
  41. package/dist/chunk-VCLA45IC.js.map +1 -0
  42. package/dist/chunk-XN6O6Z2N.js +58 -0
  43. package/dist/chunk-XN6O6Z2N.js.map +1 -0
  44. package/dist/chunk-ZFEW36RG.js +77 -0
  45. package/dist/chunk-ZFEW36RG.js.map +1 -0
  46. package/dist/chunk-ZQXJ34VJ.js +34 -0
  47. package/dist/chunk-ZQXJ34VJ.js.map +1 -0
  48. package/dist/chunk-ZUCGV2XD.js +42 -0
  49. package/dist/chunk-ZUCGV2XD.js.map +1 -0
  50. package/dist/column-picker/column-picker.js +8 -0
  51. package/dist/column-picker/column-picker.js.map +1 -0
  52. package/dist/column-picker/index.js +9 -0
  53. package/dist/column-picker/index.js.map +1 -0
  54. package/dist/data-table/card-layout.js +11 -0
  55. package/dist/data-table/card-layout.js.map +1 -0
  56. package/dist/data-table/cell-scales.js +26 -0
  57. package/dist/data-table/cell-scales.js.map +1 -0
  58. package/dist/data-table/cells/bar-cell.js +14 -0
  59. package/dist/data-table/cells/bar-cell.js.map +1 -0
  60. package/dist/data-table/cells/category-legend.js +9 -0
  61. package/dist/data-table/cells/category-legend.js.map +1 -0
  62. package/dist/data-table/cells/columns-cell.js +13 -0
  63. package/dist/data-table/cells/columns-cell.js.map +1 -0
  64. package/dist/data-table/cells/heatmap-cell.js +13 -0
  65. package/dist/data-table/cells/heatmap-cell.js.map +1 -0
  66. package/dist/data-table/cells/markdown-cell.js +13 -0
  67. package/dist/data-table/cells/markdown-cell.js.map +1 -0
  68. package/dist/data-table/cells/sparkline-cell.js +13 -0
  69. package/dist/data-table/cells/sparkline-cell.js.map +1 -0
  70. package/dist/data-table/column-meta.js +12 -0
  71. package/dist/data-table/column-meta.js.map +1 -0
  72. package/dist/data-table/data-table.js +23 -0
  73. package/dist/data-table/data-table.js.map +1 -0
  74. package/dist/data-table/index.js +23 -0
  75. package/dist/data-table/index.js.map +1 -0
  76. package/dist/data-table/ranks-column.js +13 -0
  77. package/dist/data-table/ranks-column.js.map +1 -0
  78. package/dist/data-table/sticky-rows.js +10 -0
  79. package/dist/data-table/sticky-rows.js.map +1 -0
  80. package/dist/data-table/use-table-breakpoint.js +13 -0
  81. package/dist/data-table/use-table-breakpoint.js.map +1 -0
  82. package/dist/facet-filter/facet-filter.js +8 -0
  83. package/dist/facet-filter/facet-filter.js.map +1 -0
  84. package/dist/facet-filter/index.js +9 -0
  85. package/dist/facet-filter/index.js.map +1 -0
  86. package/dist/filter-bar/filter-bar.js +8 -0
  87. package/dist/filter-bar/filter-bar.js.map +1 -0
  88. package/dist/filter-bar/filter-chip.js +9 -0
  89. package/dist/filter-bar/filter-chip.js.map +1 -0
  90. package/dist/filter-bar/index.js +13 -0
  91. package/dist/filter-bar/index.js.map +1 -0
  92. package/dist/index.d.ts +554 -10
  93. package/dist/index.js +102 -1494
  94. package/dist/index.js.map +1 -1
  95. package/dist/search-input/index.js +9 -0
  96. package/dist/search-input/index.js.map +1 -0
  97. package/dist/search-input/search-input.js +9 -0
  98. package/dist/search-input/search-input.js.map +1 -0
  99. package/dist/to-csv.js +10 -0
  100. package/dist/to-csv.js.map +1 -0
  101. package/package.json +7 -7
  102. package/src/__contract__/filter-chip.contract.test.tsx +49 -0
  103. package/src/column-picker/column-picker.tsx +3 -2
  104. package/src/data-table/card-layout.test.tsx +26 -0
  105. package/src/data-table/card-layout.tsx +74 -0
  106. package/src/data-table/cell-scales.test.ts +118 -0
  107. package/src/data-table/cell-scales.ts +246 -0
  108. package/src/data-table/cells/bar-cell.test.tsx +158 -0
  109. package/src/data-table/cells/bar-cell.tsx +135 -0
  110. package/src/data-table/cells/category-legend.test.tsx +37 -0
  111. package/src/data-table/cells/category-legend.tsx +58 -0
  112. package/src/data-table/cells/columns-cell.test.tsx +27 -0
  113. package/src/data-table/cells/columns-cell.tsx +79 -0
  114. package/src/data-table/cells/heatmap-cell.test.tsx +31 -0
  115. package/src/data-table/cells/heatmap-cell.tsx +116 -0
  116. package/src/data-table/cells/markdown-cell.test.tsx +36 -0
  117. package/src/data-table/cells/markdown-cell.tsx +91 -0
  118. package/src/data-table/cells/sparkline-cell.test.tsx +43 -0
  119. package/src/data-table/cells/sparkline-cell.tsx +113 -0
  120. package/src/data-table/column-meta.test.ts +42 -0
  121. package/src/data-table/column-meta.ts +282 -0
  122. package/src/data-table/data-table.stories.tsx +748 -1
  123. package/src/data-table/data-table.test.tsx +547 -11
  124. package/src/data-table/data-table.tsx +1014 -175
  125. package/src/data-table/ranks-column.test.tsx +65 -0
  126. package/src/data-table/ranks-column.tsx +91 -0
  127. package/src/data-table/sticky-rows.test.ts +16 -0
  128. package/src/data-table/sticky-rows.ts +77 -0
  129. package/src/data-table/use-table-breakpoint.test.ts +14 -0
  130. package/src/data-table/use-table-breakpoint.ts +67 -0
  131. package/src/facet-filter/facet-filter.stories.tsx +4 -1
  132. package/src/facet-filter/facet-filter.test.tsx +3 -3
  133. package/src/filter-bar/filter-chip.stories.tsx +1 -0
  134. package/src/index.ts +64 -0
  135. package/src/search-input/search-input.test.tsx +35 -0
  136. package/src/search-input/search-input.tsx +44 -19
  137. package/src/templates-data-app.stories.tsx +7 -2
  138. package/src/to-csv.test.ts +13 -0
  139. package/src/to-csv.ts +7 -30
@@ -0,0 +1,1823 @@
1
+ "use client";
2
+ import {
3
+ useTableBreakpoint
4
+ } from "./chunk-BEYHV3ZX.js";
5
+ import {
6
+ HeatmapCell,
7
+ HeatmapLegend,
8
+ heatmapCellStyle
9
+ } from "./chunk-NKVJUQYW.js";
10
+ import {
11
+ MarkdownCell
12
+ } from "./chunk-MV2YVXEZ.js";
13
+ import {
14
+ SparklineCell
15
+ } from "./chunk-HG2YMSRM.js";
16
+ import {
17
+ columnSizeStyle,
18
+ formatCellValue,
19
+ resolveShowAt
20
+ } from "./chunk-TBB33PHQ.js";
21
+ import {
22
+ DataTableRankCell,
23
+ DataTableRankHeader,
24
+ computeRowRanks
25
+ } from "./chunk-XN6O6Z2N.js";
26
+ import {
27
+ stickyRowPinning,
28
+ withoutStickyRows
29
+ } from "./chunk-47ZBQZYS.js";
30
+ import {
31
+ DataTableCard,
32
+ DataTableCardList
33
+ } from "./chunk-KCB33PD3.js";
34
+ import {
35
+ CategoryLegend
36
+ } from "./chunk-ZUCGV2XD.js";
37
+ import {
38
+ ColumnsCell
39
+ } from "./chunk-GBO56756.js";
40
+ import {
41
+ BarCell
42
+ } from "./chunk-SK6347WB.js";
43
+ import {
44
+ computeColumnScales,
45
+ extentOf,
46
+ labelBoxCh,
47
+ seriesEnds,
48
+ seriesValues
49
+ } from "./chunk-VCLA45IC.js";
50
+
51
+ // src/data-table/data-table.tsx
52
+ import {
53
+ forwardRef,
54
+ useCallback,
55
+ useEffect,
56
+ useId,
57
+ useMemo,
58
+ useRef,
59
+ useState
60
+ } from "react";
61
+ import {
62
+ flexRender,
63
+ getCoreRowModel,
64
+ getFilteredRowModel,
65
+ getPaginationRowModel,
66
+ getSortedRowModel,
67
+ useReactTable
68
+ } from "@tanstack/react-table";
69
+ import { useVirtualizer } from "@tanstack/react-virtual";
70
+ import {
71
+ DndContext,
72
+ KeyboardSensor,
73
+ PointerSensor,
74
+ closestCenter,
75
+ useSensor,
76
+ useSensors
77
+ } from "@dnd-kit/core";
78
+ import {
79
+ SortableContext,
80
+ sortableKeyboardCoordinates,
81
+ useSortable,
82
+ verticalListSortingStrategy
83
+ } from "@dnd-kit/sortable";
84
+ import { CSS } from "@dnd-kit/utilities";
85
+ import { ArrowDown, ArrowUp, ArrowUpDown, GripVertical } from "lucide-react";
86
+ import {
87
+ Button,
88
+ Checkbox,
89
+ Skeleton,
90
+ Spinner,
91
+ StatePanel,
92
+ useLocale
93
+ } from "@elabs-ai/components-ui";
94
+ import { cn } from "@elabs-ai/components-ui/lib/cn";
95
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
96
+ function numericColumnClasses(meta) {
97
+ if (!meta?.numeric && !meta?.align) return void 0;
98
+ const alignClass = meta?.align === "start" ? "text-start" : meta?.align === "center" ? "text-center" : meta?.align === "end" ? "text-end" : meta?.numeric ? "text-end" : void 0;
99
+ return cn(alignClass, meta?.numeric && "tabular-nums");
100
+ }
101
+ function rowKeyValue(original, key) {
102
+ const v = (original ?? {})[key];
103
+ return typeof v === "string" || typeof v === "number" && Number.isFinite(v) ? v : null;
104
+ }
105
+ function colorByStyle(target, color) {
106
+ if (!color) return void 0;
107
+ return target === "background" ? { backgroundColor: `color-mix(in oklab, ${color} 22%, transparent)` } : { color: `color-mix(in oklab, ${color} 35%, var(--foreground))` };
108
+ }
109
+ var ROW_ACTION_CLASS = "sr-only focus-visible:outline-none";
110
+ function exactSearchMatch(value, query) {
111
+ const q = String(query ?? "").trim().toLowerCase();
112
+ if (q === "") return true;
113
+ return value !== null && value !== void 0 && String(value).trim().toLowerCase() === q;
114
+ }
115
+ var ROW_CLICK_GUARD_SELECTOR = 'button, a[href], input, select, textarea, label, summary, [role="button"], [role="link"], [role="menuitem"], [role="checkbox"], [role="radio"], [role="switch"], [role="tab"], [contenteditable="true"]';
116
+ function isInteractiveEventTarget(target) {
117
+ return target instanceof Element && target.closest(ROW_CLICK_GUARD_SELECTOR) !== null;
118
+ }
119
+ function isActiveTextSelection() {
120
+ if (typeof window === "undefined" || typeof window.getSelection !== "function") return false;
121
+ return window.getSelection()?.type === "Range";
122
+ }
123
+ var PINNED_SEAM_CLASS = "after:pointer-events-none after:absolute after:inset-y-0 after:w-px after:bg-border-strong after:content-['']";
124
+ var COLUMN_DIVIDER_CLASS = "border-e border-rule last:border-e-0";
125
+ function unsizedColumnIds(defs) {
126
+ const out = /* @__PURE__ */ new Set();
127
+ const walk = (list) => {
128
+ for (const def of list) {
129
+ const group = def;
130
+ if (group.columns) {
131
+ walk(group.columns);
132
+ continue;
133
+ }
134
+ if (def.size !== void 0) continue;
135
+ const accessorKey = def.accessorKey;
136
+ const id = def.id ?? (accessorKey !== void 0 ? String(accessorKey).replace(/\./gu, "_") : typeof def.header === "string" ? def.header : void 0);
137
+ if (id) out.add(id);
138
+ }
139
+ };
140
+ walk(defs);
141
+ return out;
142
+ }
143
+ function resizeWidthStyle(size) {
144
+ return { width: size, minWidth: size, maxWidth: size };
145
+ }
146
+ function firstDataCellValue(row) {
147
+ for (const cell of row.getVisibleCells()) {
148
+ if (!cell.column.accessorFn) continue;
149
+ const value = cell.getValue();
150
+ if (typeof value === "string" && value.trim() !== "") return value;
151
+ if (typeof value === "number") return String(value);
152
+ }
153
+ return void 0;
154
+ }
155
+ function SelectAllHeaderCell({ table }) {
156
+ const { t } = useLocale();
157
+ const allSelected = table.getIsAllPageRowsSelected();
158
+ const someSelected = table.getIsSomePageRowsSelected();
159
+ return /* @__PURE__ */ jsx(
160
+ Checkbox,
161
+ {
162
+ "data-slot": "data-table-select-all",
163
+ checked: allSelected ? true : someSelected ? "indeterminate" : false,
164
+ onCheckedChange: (checked) => table.toggleAllPageRowsSelected(checked === true),
165
+ "aria-label": t("data.table.selectAllRows")
166
+ }
167
+ );
168
+ }
169
+ function SelectRowCell({ row }) {
170
+ const { t } = useLocale();
171
+ const name = firstDataCellValue(row);
172
+ return /* @__PURE__ */ jsx(
173
+ Checkbox,
174
+ {
175
+ "data-slot": "data-table-select-cell",
176
+ checked: row.getIsSelected(),
177
+ disabled: !row.getCanSelect(),
178
+ onCheckedChange: (checked) => row.toggleSelected(checked === true),
179
+ "aria-label": name ? t("data.table.selectRowNamed", { name }) : t("data.table.selectRow")
180
+ }
181
+ );
182
+ }
183
+ function createSelectionColumn() {
184
+ return {
185
+ id: "select",
186
+ size: 40,
187
+ enableSorting: false,
188
+ enableHiding: false,
189
+ header: ({ table }) => (
190
+ // #11 C1: `toggleAllPageRowsSelected` wipes-then-sets on every row when
191
+ // `enableMultiRowSelection` is off (TanStack's `mutateRowIsSelected`), so
192
+ // a select-all header under single-select leaves only the LAST row
193
+ // selected and pins the header at indeterminate forever. Suppress it.
194
+ table.options.enableMultiRowSelection === false ? null : /* @__PURE__ */ jsx(SelectAllHeaderCell, { table })
195
+ ),
196
+ cell: ({ row }) => /* @__PURE__ */ jsx(SelectRowCell, { row })
197
+ };
198
+ }
199
+ function SortableDataRow({
200
+ id,
201
+ disabled,
202
+ attributesOverride,
203
+ children
204
+ }) {
205
+ const { attributes, listeners, setNodeRef, setActivatorNodeRef, transform, isDragging } = useSortable({ id, disabled, transition: null, attributes: attributesOverride });
206
+ return /* @__PURE__ */ jsx(Fragment, { children: children({
207
+ setNodeRef,
208
+ setActivatorNodeRef,
209
+ attributes,
210
+ listeners,
211
+ isDragging,
212
+ style: { transform: CSS.Transform.toString(transform) }
213
+ }) });
214
+ }
215
+ function DataTableInner({
216
+ columns,
217
+ data,
218
+ toolbar,
219
+ enablePagination = false,
220
+ pageSize = 10,
221
+ hidePaginationWhenSingle = true,
222
+ // Global filter
223
+ globalFilter: globalFilterProp,
224
+ onGlobalFilterChange,
225
+ // Controlled slices
226
+ sorting: sortingProp,
227
+ onSortingChange: onSortingChangeProp,
228
+ columnVisibility: columnVisibilityProp,
229
+ onColumnVisibilityChange: onColumnVisibilityChangeProp,
230
+ columnFilters: columnFiltersProp,
231
+ onColumnFiltersChange: onColumnFiltersChangeProp,
232
+ pagination: paginationProp,
233
+ onPaginationChange: onPaginationChangeProp,
234
+ columnPinning: columnPinningProp,
235
+ onColumnPinningChange: onColumnPinningChangeProp,
236
+ enableColumnResizing = false,
237
+ columnResizeMode = "onChange",
238
+ columnSizing: columnSizingProp,
239
+ onColumnSizingChange: onColumnSizingChangeProp,
240
+ rowSelection: rowSelectionProp,
241
+ onRowSelectionChange: onRowSelectionChangeProp,
242
+ enableRowSelection,
243
+ enableMultiRowSelection,
244
+ getRowId,
245
+ // Saved views rehydration
246
+ initialView,
247
+ // Server-side model
248
+ manualSorting = false,
249
+ manualFiltering = false,
250
+ manualPagination = false,
251
+ rowCount,
252
+ pageCount,
253
+ onServerChange,
254
+ // Loading
255
+ loading = false,
256
+ loadingRows,
257
+ // Virtualization
258
+ enableRowVirtualization = false,
259
+ estimateRowHeight = 40,
260
+ overscan = 8,
261
+ maxBodyHeight = "32rem",
262
+ zebra = true,
263
+ columnDividers = false,
264
+ // Presentation (RM-123)
265
+ layout = "table",
266
+ stickyRows,
267
+ showRanks = false,
268
+ rankLabel,
269
+ density = "default",
270
+ mergeEmptyHeaders = false,
271
+ searchMode = "contains",
272
+ hideHeader = false,
273
+ // Row drag-reorder (#13)
274
+ enableRowReorder = false,
275
+ onRowReorder,
276
+ rowReorderHandle = "cell",
277
+ onRowClick,
278
+ rowActionLabel,
279
+ rowClassName,
280
+ caption,
281
+ emptyMessage = "No results.",
282
+ className,
283
+ ...rest
284
+ }, ref) {
285
+ const { t, dir, formatNumber } = useLocale();
286
+ const isSortingControlled = sortingProp !== void 0;
287
+ const isColumnVisibilityControlled = columnVisibilityProp !== void 0;
288
+ const isColumnFiltersControlled = columnFiltersProp !== void 0;
289
+ const isPaginationControlled = paginationProp !== void 0;
290
+ const isFilterControlled = globalFilterProp !== void 0;
291
+ const isColumnPinningControlled = columnPinningProp !== void 0;
292
+ const isColumnSizingControlled = columnSizingProp !== void 0;
293
+ const isRowSelectionControlled = rowSelectionProp !== void 0;
294
+ const [internalSorting, setInternalSorting] = useState(
295
+ () => initialView?.sorting ?? []
296
+ );
297
+ const [internalColumnVisibility, setInternalColumnVisibility] = useState(
298
+ () => initialView?.columnVisibility ?? {}
299
+ );
300
+ const [internalColumnFilters, setInternalColumnFilters] = useState(
301
+ () => initialView?.columnFilters ?? []
302
+ );
303
+ const [internalPagination, setInternalPagination] = useState(
304
+ () => initialView?.pagination ?? {
305
+ pageIndex: 0,
306
+ pageSize
307
+ }
308
+ );
309
+ const [internalGlobalFilter, setInternalGlobalFilter] = useState(
310
+ () => initialView?.globalFilter ?? ""
311
+ );
312
+ const [internalColumnPinning, setInternalColumnPinning] = useState(
313
+ () => initialView?.columnPinning ?? { left: [], right: [] }
314
+ );
315
+ const [internalColumnSizing, setInternalColumnSizing] = useState(
316
+ () => initialView?.columnSizing ?? {}
317
+ );
318
+ const [internalRowSelection, setInternalRowSelection] = useState(
319
+ () => initialView?.rowSelection ?? {}
320
+ );
321
+ const sorting = isSortingControlled ? sortingProp : internalSorting;
322
+ const columnVisibility = isColumnVisibilityControlled ? columnVisibilityProp : internalColumnVisibility;
323
+ const columnFilters = isColumnFiltersControlled ? columnFiltersProp : internalColumnFilters;
324
+ const pagination = isPaginationControlled ? paginationProp : internalPagination;
325
+ const globalFilter = isFilterControlled ? globalFilterProp : internalGlobalFilter;
326
+ const columnPinning = isColumnPinningControlled ? columnPinningProp : internalColumnPinning;
327
+ const columnSizing = isColumnSizingControlled ? columnSizingProp : internalColumnSizing;
328
+ const rowSelection = isRowSelectionControlled ? rowSelectionProp : internalRowSelection;
329
+ const sortingRef = useRef(sorting);
330
+ sortingRef.current = sorting;
331
+ const columnFiltersRef = useRef(columnFilters);
332
+ columnFiltersRef.current = columnFilters;
333
+ const paginationRef = useRef(pagination);
334
+ paginationRef.current = pagination;
335
+ const globalFilterRef = useRef(globalFilter);
336
+ globalFilterRef.current = globalFilter;
337
+ const columnVisibilityRef = useRef(columnVisibility);
338
+ columnVisibilityRef.current = columnVisibility;
339
+ const columnPinningRef = useRef(columnPinning);
340
+ columnPinningRef.current = columnPinning;
341
+ const columnSizingRef = useRef(columnSizing);
342
+ columnSizingRef.current = columnSizing;
343
+ const rowSelectionRef = useRef(rowSelection);
344
+ rowSelectionRef.current = rowSelection;
345
+ const warnedMissingRowCountRef = useRef(false);
346
+ useEffect(() => {
347
+ if (process.env.NODE_ENV !== "production" && manualPagination && rowCount === void 0 && pageCount === void 0 && !warnedMissingRowCountRef.current) {
348
+ warnedMissingRowCountRef.current = true;
349
+ console.warn(
350
+ '[DataTable] `manualPagination` is true but neither `rowCount` nor `pageCount` was provided \u2014 the pager will appear stuck ("Page 1 of 1", Next disabled). Pass `rowCount` (or `pageCount`) so the pager can compute the total.'
351
+ );
352
+ }
353
+ }, [manualPagination, rowCount, pageCount]);
354
+ const warnedManualSelectionRef = useRef(false);
355
+ useEffect(() => {
356
+ if (process.env.NODE_ENV !== "production" && manualPagination && getRowId === void 0 && (isRowSelectionControlled || onRowSelectionChangeProp !== void 0) && !warnedManualSelectionRef.current) {
357
+ warnedManualSelectionRef.current = true;
358
+ console.warn(
359
+ '[DataTable] `rowSelection` is wired up under `manualPagination` with no `getRowId` \u2014 each page is a fresh `data` array, so the default index-based id restarts at "0" per page and a selection made on one page can silently apply to a different record on the next. Pass `getRowId` so selection is keyed to a stable identity instead of position.'
360
+ );
361
+ }
362
+ }, [manualPagination, getRowId, isRowSelectionControlled, onRowSelectionChangeProp]);
363
+ const warnedReorderSortingRef = useRef(false);
364
+ useEffect(() => {
365
+ if (process.env.NODE_ENV !== "production" && enableRowReorder && sorting.length > 0 && !warnedReorderSortingRef.current) {
366
+ warnedReorderSortingRef.current = true;
367
+ console.warn(
368
+ "[DataTable] `enableRowReorder` is set while a column is sorted \u2014 the sort will keep re-ordering rows out from under a manual drag. Clear `sorting` (or avoid enabling both at once) so a drag's new order stays stable."
369
+ );
370
+ }
371
+ }, [enableRowReorder, sorting.length]);
372
+ const warnedReorderVirtualizedRef = useRef(false);
373
+ useEffect(() => {
374
+ if (process.env.NODE_ENV !== "production" && enableRowReorder && enableRowVirtualization && !warnedReorderVirtualizedRef.current) {
375
+ warnedReorderVirtualizedRef.current = true;
376
+ console.warn(
377
+ "[DataTable] `enableRowReorder` has no effect while `enableRowVirtualization` is set \u2014 the two are mutually exclusive. Virtualization wins; row reorder is disabled."
378
+ );
379
+ }
380
+ }, [enableRowReorder, enableRowVirtualization]);
381
+ const rowReorderActive = enableRowReorder && !enableRowVirtualization;
382
+ const hasGripColumn = rowReorderActive && rowReorderHandle === "cell";
383
+ const reorderSensors = useSensors(
384
+ useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
385
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
386
+ );
387
+ const reorderIdentityMapRef = useRef(/* @__PURE__ */ new WeakMap());
388
+ const reorderIdentityCounterRef = useRef(0);
389
+ const reorderRepeatedPositions = useMemo(() => {
390
+ const repeats = /* @__PURE__ */ new Set();
391
+ if (!rowReorderActive) return repeats;
392
+ const seen = /* @__PURE__ */ new Set();
393
+ data.forEach((record, index) => {
394
+ if (record === null || typeof record !== "object") return;
395
+ if (seen.has(record)) repeats.add(index);
396
+ else seen.add(record);
397
+ });
398
+ return repeats;
399
+ }, [data, rowReorderActive]);
400
+ const [reorderLiveMessage, setReorderLiveMessage] = useState("");
401
+ const reorderLastAnnouncedPositionRef = useRef(null);
402
+ function fireServerChange(overrides = {}) {
403
+ if (!onServerChange) return;
404
+ onServerChange({
405
+ pagination: paginationRef.current,
406
+ sorting: sortingRef.current,
407
+ columnFilters: columnFiltersRef.current,
408
+ globalFilter: globalFilterRef.current,
409
+ ...overrides
410
+ });
411
+ }
412
+ function resolveSorting(updater) {
413
+ return typeof updater === "function" ? updater(sortingRef.current) : updater;
414
+ }
415
+ function resolveColumnVisibility(updater) {
416
+ return typeof updater === "function" ? updater(columnVisibilityRef.current) : updater;
417
+ }
418
+ function resolveColumnFilters(updater) {
419
+ return typeof updater === "function" ? updater(columnFiltersRef.current) : updater;
420
+ }
421
+ function resolvePagination(updater) {
422
+ return typeof updater === "function" ? updater(paginationRef.current) : updater;
423
+ }
424
+ function resolveGlobalFilter(updater) {
425
+ return typeof updater === "function" ? updater(globalFilterRef.current) : updater;
426
+ }
427
+ function resolveColumnPinning(updater) {
428
+ return typeof updater === "function" ? updater(columnPinningRef.current) : updater;
429
+ }
430
+ function resolveColumnSizing(updater) {
431
+ return typeof updater === "function" ? updater(columnSizingRef.current) : updater;
432
+ }
433
+ function resolveRowSelection(updater) {
434
+ return typeof updater === "function" ? updater(rowSelectionRef.current) : updater;
435
+ }
436
+ const sortedRowModel = manualSorting ? {} : { getSortedRowModel: getSortedRowModel() };
437
+ const filteredRowModel = manualFiltering ? {} : {
438
+ getFilteredRowModel: stickyRows ? withoutStickyRows(getFilteredRowModel()) : getFilteredRowModel()
439
+ };
440
+ const paginationRowModel = enablePagination && !manualPagination ? { getPaginationRowModel: getPaginationRowModel() } : {};
441
+ const rowPinning = useMemo(
442
+ () => stickyRowPinning(data, stickyRows, getRowId),
443
+ [data, stickyRows, getRowId]
444
+ );
445
+ const stickyActive = (rowPinning.top?.length ?? 0) + (rowPinning.bottom?.length ?? 0) > 0;
446
+ const table = useReactTable({
447
+ data,
448
+ columns,
449
+ state: {
450
+ sorting,
451
+ columnVisibility,
452
+ columnFilters,
453
+ globalFilter,
454
+ pagination,
455
+ columnPinning,
456
+ columnSizing,
457
+ rowSelection,
458
+ ...stickyActive ? { rowPinning } : {}
459
+ },
460
+ ...stickyActive ? { enableRowPinning: true, keepPinnedRows: true } : {},
461
+ // RM-123 `searchMode="exact"`: a row matches when one cell EQUALS the query.
462
+ ...searchMode === "exact" ? {
463
+ globalFilterFn: (row, columnId, filterValue) => exactSearchMatch(row.getValue(columnId), filterValue)
464
+ } : {},
465
+ // Sorting
466
+ onSortingChange: (updater) => {
467
+ const next = resolveSorting(updater);
468
+ if (!isSortingControlled) setInternalSorting(next);
469
+ onSortingChangeProp?.(updater);
470
+ if (manualSorting) {
471
+ sortingRef.current = next;
472
+ fireServerChange({ sorting: next });
473
+ }
474
+ },
475
+ // Column visibility
476
+ onColumnVisibilityChange: (updater) => {
477
+ const next = resolveColumnVisibility(updater);
478
+ if (!isColumnVisibilityControlled) setInternalColumnVisibility(next);
479
+ onColumnVisibilityChangeProp?.(updater);
480
+ },
481
+ // Column filters
482
+ onColumnFiltersChange: (updater) => {
483
+ const next = resolveColumnFilters(updater);
484
+ if (!isColumnFiltersControlled) setInternalColumnFilters(next);
485
+ onColumnFiltersChangeProp?.(updater);
486
+ if (manualFiltering) {
487
+ columnFiltersRef.current = next;
488
+ fireServerChange({ columnFilters: next });
489
+ }
490
+ },
491
+ // Global filter
492
+ onGlobalFilterChange: (updater) => {
493
+ const next = resolveGlobalFilter(updater);
494
+ if (!isFilterControlled) setInternalGlobalFilter(next);
495
+ onGlobalFilterChange?.(next);
496
+ if (manualFiltering) {
497
+ globalFilterRef.current = next;
498
+ fireServerChange({ globalFilter: next });
499
+ }
500
+ },
501
+ // Pagination
502
+ onPaginationChange: (updater) => {
503
+ const next = resolvePagination(updater);
504
+ if (!isPaginationControlled) setInternalPagination(next);
505
+ onPaginationChangeProp?.(updater);
506
+ if (manualPagination) {
507
+ paginationRef.current = next;
508
+ fireServerChange({ pagination: next });
509
+ }
510
+ },
511
+ // Column pinning — a LAYOUT slice, so unlike sorting/filtering/pagination it
512
+ // never fires `onServerChange`: freezing a column changes nothing the server
513
+ // would need to re-query.
514
+ onColumnPinningChange: (updater) => {
515
+ const next = resolveColumnPinning(updater);
516
+ if (!isColumnPinningControlled) setInternalColumnPinning(next);
517
+ onColumnPinningChangeProp?.(updater);
518
+ },
519
+ // Column resizing (#12) — a LAYOUT slice, like column pinning: a column's
520
+ // width changes nothing the server would need to re-query, so this never
521
+ // fires onServerChange either. Routed through by BOTH the pointer path
522
+ // (TanStack's own `header.getResizeHandler()`, wired below) and the
523
+ // keyboard path (`handleResizeKeyDown`, via `table.setColumnSizing`) so
524
+ // the two input modes can never diverge in controlled/uncontrolled
525
+ // behaviour.
526
+ columnResizeMode,
527
+ // RTL fix (#12 review, P1): TanStack's pointer-drag math hardcodes LTR
528
+ // unless told otherwise — `deltaDirection = columnResizeDirection ===
529
+ // 'rtl' ? -1 : 1` internally — so under `dir="rtl"` (the resize handle's
530
+ // own edge already flips via `end-0`, see the `useLocale()` call above)
531
+ // dragging would otherwise move the column's width opposite the visible
532
+ // boundary. `handleResizeKeyDown` below mirrors this for the keyboard path.
533
+ columnResizeDirection: dir,
534
+ enableColumnResizing,
535
+ onColumnSizingChange: (updater) => {
536
+ const next = resolveColumnSizing(updater);
537
+ if (!isColumnSizingControlled) setInternalColumnSizing(next);
538
+ onColumnSizingChangeProp?.(updater);
539
+ },
540
+ // Row selection (#11) — also a LAYOUT/UI slice, so it never fires
541
+ // onServerChange: which rows are checked changes nothing the server
542
+ // would need to re-query.
543
+ onRowSelectionChange: (updater) => {
544
+ const next = resolveRowSelection(updater);
545
+ if (!isRowSelectionControlled) setInternalRowSelection(next);
546
+ onRowSelectionChangeProp?.(updater);
547
+ },
548
+ enableRowSelection,
549
+ enableMultiRowSelection,
550
+ getRowId,
551
+ getCoreRowModel: getCoreRowModel(),
552
+ ...sortedRowModel,
553
+ ...filteredRowModel,
554
+ ...paginationRowModel,
555
+ // Server-side options
556
+ manualSorting,
557
+ manualFiltering,
558
+ manualPagination,
559
+ ...rowCount !== void 0 ? { rowCount } : {},
560
+ ...pageCount !== void 0 ? { pageCount } : {}
561
+ // No `initialState`: every slice is driven explicitly via `state` above
562
+ // (internal slices are seeded from `initialView` at useState init), so a
563
+ // TanStack `initialState` would be dead/misleading.
564
+ });
565
+ const rows = stickyActive ? table.getCenterRows() : table.getRowModel().rows;
566
+ const topRows = stickyActive ? table.getTopRows() : [];
567
+ const bottomRows = stickyActive ? table.getBottomRows() : [];
568
+ const leafColumns = table.getAllLeafColumns();
569
+ const coreRows = table.getCoreRowModel().rows;
570
+ const needsScales = leafColumns.some(
571
+ (c) => c.columnDef.meta?.visual !== void 0 || c.columnDef.meta?.colorBy !== void 0
572
+ );
573
+ const columnScales = useMemo(
574
+ () => needsScales ? computeColumnScales(
575
+ leafColumns.map((c) => ({ id: c.id, meta: c.columnDef.meta })),
576
+ coreRows
577
+ ) : null,
578
+ [needsScales, leafColumns, coreRows]
579
+ );
580
+ const labelBoxes = useMemo(() => {
581
+ const boxes = /* @__PURE__ */ new Map();
582
+ for (const column of leafColumns) {
583
+ const meta = column.columnDef.meta;
584
+ const visual = meta?.visual;
585
+ const labelOf = (value) => formatCellValue(value, meta?.format, formatNumber);
586
+ if (visual?.kind === "bar" && visual.style !== "slim") {
587
+ boxes.set(column.id, {
588
+ value: labelBoxCh(coreRows.map((row) => labelOf(row.getValue(column.id))))
589
+ });
590
+ }
591
+ if (visual?.kind === "sparkline" && visual.labels === "ends") {
592
+ const ends = coreRows.map((row) => seriesEnds(seriesValues(row.original, visual.keys))).filter((pair) => pair !== null);
593
+ boxes.set(column.id, {
594
+ ends: [
595
+ labelBoxCh(ends.map(([first]) => labelOf(first))),
596
+ labelBoxCh(ends.map(([, last]) => labelOf(last)))
597
+ ]
598
+ });
599
+ }
600
+ }
601
+ return boxes;
602
+ }, [leafColumns, coreRows, formatNumber]);
603
+ const rowRanks = useMemo(
604
+ () => showRanks ? computeRowRanks(
605
+ coreRows.map((r) => r.id),
606
+ /* @__PURE__ */ new Set([...rowPinning.top ?? [], ...rowPinning.bottom ?? []])
607
+ ) : null,
608
+ [showRanks, coreRows, rowPinning]
609
+ );
610
+ const hasShowAt = leafColumns.some((c) => c.columnDef.meta?.showAt !== void 0);
611
+ const { ref: breakpointRef, breakpoint } = useTableBreakpoint(
612
+ layout === "auto" || hasShowAt
613
+ );
614
+ const cardsActive = layout === "cards" || layout === "auto" && breakpoint === "narrow";
615
+ const warnedCardReorderRef = useRef(false);
616
+ useEffect(() => {
617
+ if (process.env.NODE_ENV !== "production" && cardsActive && enableRowReorder && !warnedCardReorderRef.current) {
618
+ warnedCardReorderRef.current = true;
619
+ console.warn(
620
+ '[DataTable] `enableRowReorder` is ignored in the card layout \u2014 a card list has no grip column and no drop target, so `onRowReorder` will never fire. Keep `layout="table"` for reordering, or offer the move as a row action in cards.'
621
+ );
622
+ }
623
+ }, [cardsActive, enableRowReorder]);
624
+ const presentationAttrs = layout !== "table" || hasShowAt ? {
625
+ "data-layout": cardsActive ? "cards" : "table",
626
+ "data-breakpoint": layout === "auto" || hasShowAt ? breakpoint : void 0
627
+ } : null;
628
+ const isColumnShown = (column) => resolveShowAt(column.columnDef.meta?.showAt, breakpoint);
629
+ const rootRef = useCallback(
630
+ (node) => {
631
+ breakpointRef(node);
632
+ if (typeof ref === "function") ref(node);
633
+ else if (ref) ref.current = node;
634
+ },
635
+ [breakpointRef, ref]
636
+ );
637
+ const captionId = useId();
638
+ const headerGroupsForCards = table.getHeaderGroups();
639
+ const leadingColCount = (hasGripColumn ? 1 : 0) + (showRanks ? 1 : 0);
640
+ const colCount = table.getVisibleLeafColumns().filter(isColumnShown).length;
641
+ const headerRowCount = table.getHeaderGroups().length;
642
+ const centreRowCount = rowCount ?? rows.length;
643
+ const ariaRowCount = centreRowCount + topRows.length + bottomRows.length + headerRowCount;
644
+ const firstCentreRowIndex = headerRowCount + topRows.length + 1;
645
+ function reorderRowName(id) {
646
+ const row = rows.find((r) => getReorderRowId(r) === id);
647
+ return row ? rowActionName(row) : id;
648
+ }
649
+ function reorderPosition(id) {
650
+ return rows.findIndex((r) => getReorderRowId(r) === id) + 1;
651
+ }
652
+ function getReorderRowId(row) {
653
+ if (getRowId) return row.id;
654
+ const original = row.original;
655
+ if (original !== null && typeof original === "object") {
656
+ const map = reorderIdentityMapRef.current;
657
+ let id = map.get(original);
658
+ if (id === void 0) {
659
+ id = `__reorder-${reorderIdentityCounterRef.current++}`;
660
+ map.set(original, id);
661
+ }
662
+ return reorderRepeatedPositions.has(row.index) ? `${id}__${row.index}` : id;
663
+ }
664
+ return row.id;
665
+ }
666
+ const silentDragAnnouncements = {
667
+ onDragStart: () => void 0,
668
+ onDragOver: () => void 0,
669
+ onDragEnd: () => void 0,
670
+ onDragCancel: () => void 0
671
+ };
672
+ function handleRowDragStart(event) {
673
+ const activeRowId = String(event.active.id);
674
+ reorderLastAnnouncedPositionRef.current = reorderPosition(activeRowId);
675
+ setReorderLiveMessage(t("data.table.reorderPickedUp", { name: reorderRowName(activeRowId) }));
676
+ }
677
+ function handleRowDragOver(event) {
678
+ const { active, over } = event;
679
+ if (!over) return;
680
+ const position = reorderPosition(String(over.id));
681
+ if (position === reorderLastAnnouncedPositionRef.current) return;
682
+ reorderLastAnnouncedPositionRef.current = position;
683
+ setReorderLiveMessage(
684
+ t("data.table.reorderMoved", {
685
+ name: reorderRowName(String(active.id)),
686
+ position,
687
+ total: rows.length
688
+ })
689
+ );
690
+ }
691
+ function handleRowDragCancel(event) {
692
+ const activeRowId = String(event.active.id);
693
+ setReorderLiveMessage(
694
+ t("data.table.reorderCancelled", {
695
+ name: reorderRowName(activeRowId),
696
+ position: reorderPosition(activeRowId),
697
+ total: rows.length
698
+ })
699
+ );
700
+ reorderLastAnnouncedPositionRef.current = null;
701
+ }
702
+ function handleRowDragEnd(event) {
703
+ const { active, over } = event;
704
+ const activeRowId = String(active.id);
705
+ setReorderLiveMessage(
706
+ t("data.table.reorderDropped", {
707
+ name: reorderRowName(activeRowId),
708
+ position: reorderPosition(String(over ? over.id : active.id)),
709
+ total: rows.length
710
+ })
711
+ );
712
+ reorderLastAnnouncedPositionRef.current = null;
713
+ if (!over || active.id === over.id) return;
714
+ const movedRow = rows.find((r) => getReorderRowId(r) === activeRowId);
715
+ const targetRow = rows.find((r) => getReorderRowId(r) === String(over.id));
716
+ if (!movedRow || !targetRow) return;
717
+ const from = movedRow.index;
718
+ const to = targetRow.index;
719
+ if (from < 0 || from >= data.length || to < 0 || to >= data.length) return;
720
+ onRowReorder?.(from, to, movedRow.original);
721
+ }
722
+ const hasLeftPinned = (columnPinning.left?.length ?? 0) > 0;
723
+ const hasRightPinned = (columnPinning.right?.length ?? 0) > 0;
724
+ const pinnedScrollPadding = {
725
+ ...hasLeftPinned ? { scrollPaddingInlineStart: table.getLeftTotalSize() } : {},
726
+ ...hasRightPinned ? { scrollPaddingInlineEnd: table.getRightTotalSize() } : {}
727
+ };
728
+ const warnedUnsizedPinnedRef = useRef(false);
729
+ const pinnedIds = [...columnPinning.left ?? [], ...columnPinning.right ?? []];
730
+ const unsizedIds = process.env.NODE_ENV === "production" || pinnedIds.length === 0 ? null : unsizedColumnIds(columns);
731
+ const pinnedWithoutSizeKey = unsizedIds ? pinnedIds.filter((id) => unsizedIds.has(id)).join(",") : "";
732
+ useEffect(() => {
733
+ if (process.env.NODE_ENV !== "production" && pinnedWithoutSizeKey !== "" && !warnedUnsizedPinnedRef.current) {
734
+ warnedUnsizedPinnedRef.current = true;
735
+ console.warn(
736
+ `[DataTable] Pinned column(s) without an explicit \`size\` in their \`ColumnDef\`: ${pinnedWithoutSizeKey}. Sticky offsets are computed from the declared sizes, so an auto-width pinned column will render at a width that doesn't match its own offset. Give every pinned column a \`size\`.`
737
+ );
738
+ }
739
+ }, [pinnedWithoutSizeKey]);
740
+ function pinnedCellGeometry(column) {
741
+ const pinned = column.getIsPinned();
742
+ if (pinned === false) return null;
743
+ const size = column.getSize();
744
+ const style = {
745
+ width: size,
746
+ minWidth: size,
747
+ maxWidth: size,
748
+ ...pinned === "left" ? { left: column.getStart("left") } : { right: column.getAfter("right") }
749
+ };
750
+ return {
751
+ pinned,
752
+ style,
753
+ // The seam between the frozen block and the scrolling block is the SOLE
754
+ // structural cue between two regions that share one row fill and one
755
+ // zebra stripe — delete it and a sighted user cannot tell them apart — so
756
+ // it takes the strong rung (ADR 0010 decision test). No shadow: ADR 0020's
757
+ // `--shadow-strength: 0` (`data-decoration="8|9|10"`) would
758
+ // erase a shadow-only cue entirely.
759
+ //
760
+ // It is drawn as a 1px `::after` INSIDE the cell, NOT as `border-e` /
761
+ // `border-s`. A real border cannot work here: Tailwind's Preflight puts
762
+ // the table in the COLLAPSED border model, and a collapsed border is
763
+ // painted by the <table> at the cell's STATIC position — it does not
764
+ // travel with a `position: sticky` cell, and the cell's own opaque fill
765
+ // (which it needs, see `pinnedCellFillClass`) then paints over it. Measured
766
+ // in Chromium on `Data/DataTable → PinnedColumns`: with `border-e` the
767
+ // seam pixel read `143,143,143` (light `--border-strong`) at
768
+ // scrollLeft 0 and `245,245,245` (the plain cell fill — i.e. GONE) once
769
+ // scrolled, in every theme and on both edges. So the one cue vanished
770
+ // exactly when the freeze was doing something. The `::after` lives in the
771
+ // sticky cell's own stacking context, so it moves with it.
772
+ edgeClass: pinned === "left" ? column.getIsLastColumn("left") ? PINNED_SEAM_CLASS + " after:end-0" : "" : column.getIsFirstColumn("right") ? PINNED_SEAM_CLASS + " after:start-0" : ""
773
+ };
774
+ }
775
+ const RESIZE_STEP = 10;
776
+ const RESIZE_UNBOUNDED_ARIA_MAX = 2e3;
777
+ function handleResizeKeyDown(event, column) {
778
+ let delta = 0;
779
+ if (event.key === "ArrowRight") delta = RESIZE_STEP;
780
+ else if (event.key === "ArrowLeft") delta = -RESIZE_STEP;
781
+ else return;
782
+ event.preventDefault();
783
+ if (dir === "rtl") delta = -delta;
784
+ const minSize = column.columnDef.minSize ?? 20;
785
+ const maxSize = column.columnDef.maxSize ?? Number.MAX_SAFE_INTEGER;
786
+ const nextSize = Math.min(maxSize, Math.max(minSize, column.getSize() + delta));
787
+ table.setColumnSizing((old) => ({ ...old, [column.id]: nextSize }));
788
+ }
789
+ function handleResizeDoubleClick(column) {
790
+ table.setColumnSizing((old) => {
791
+ if (!(column.id in old)) return old;
792
+ const { [column.id]: _removed, ...rest2 } = old;
793
+ return rest2;
794
+ });
795
+ }
796
+ const scrollRef = useRef(null);
797
+ const virtualizer = useVirtualizer({
798
+ count: enableRowVirtualization ? rows.length : 0,
799
+ getScrollElement: () => enableRowVirtualization ? scrollRef.current : null,
800
+ estimateSize: () => estimateRowHeight,
801
+ overscan,
802
+ enabled: enableRowVirtualization
803
+ });
804
+ const virtualItems = enableRowVirtualization ? virtualizer.getVirtualItems() : [];
805
+ const totalSize = enableRowVirtualization ? virtualizer.getTotalSize() : 0;
806
+ const paddingTop = virtualItems.length > 0 ? virtualItems[0]?.start ?? 0 : 0;
807
+ const paddingBottom = totalSize > 0 ? totalSize - (virtualItems[virtualItems.length - 1]?.end ?? 0) : 0;
808
+ const plainScrollRef = useRef(null);
809
+ const [scrollOverflows, setScrollOverflows] = useState(false);
810
+ const [canScrollLeft, setCanScrollLeft] = useState(false);
811
+ const [canScrollRight, setCanScrollRight] = useState(false);
812
+ const updateScrollAffordance = useCallback(() => {
813
+ const el = plainScrollRef.current;
814
+ if (!el) return;
815
+ setScrollOverflows(
816
+ el.scrollWidth > el.clientWidth + 1 || el.scrollHeight > el.clientHeight + 1
817
+ );
818
+ setCanScrollLeft(el.scrollLeft > 0);
819
+ setCanScrollRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 1);
820
+ }, []);
821
+ useEffect(() => {
822
+ const el = plainScrollRef.current;
823
+ if (!el) return;
824
+ updateScrollAffordance();
825
+ if (typeof ResizeObserver === "undefined") return;
826
+ const observer = new ResizeObserver(updateScrollAffordance);
827
+ observer.observe(el);
828
+ if (el.firstElementChild) observer.observe(el.firstElementChild);
829
+ return () => observer.disconnect();
830
+ }, [updateScrollAffordance, colCount, rows.length, cardsActive]);
831
+ const showEmpty = !loading && rows.length === 0;
832
+ const showSkeletons = loading && rows.length === 0;
833
+ const skeletonRowCount = loadingRows ?? pageSize;
834
+ const headerHeightClass = density === "compact" ? "h-8" : "h-10";
835
+ const cellPadYClass = density === "compact" ? "py-1" : "py-2";
836
+ const defaultCellRenderer = table._getDefaultColumnDef().cell;
837
+ const rowColorColumns = leafColumns.filter((c) => c.columnDef.meta?.colorBy?.scope === "row");
838
+ function cellLabel(value, meta) {
839
+ return formatCellValue(value, meta?.format, formatNumber);
840
+ }
841
+ function renderCellContent(cell) {
842
+ const meta = cell.column.columnDef.meta;
843
+ const visual = meta?.visual;
844
+ const value = cell.getValue();
845
+ const scale = columnScales?.get(cell.column.id);
846
+ if (visual?.kind === "bar") {
847
+ return /* @__PURE__ */ jsx(
848
+ BarCell,
849
+ {
850
+ value: typeof value === "number" ? value : null,
851
+ label: cellLabel(value, meta),
852
+ domain: scale?.barDomain ?? [0, 0],
853
+ variant: visual.style,
854
+ track: visual.track,
855
+ fillColor: visual.colorBy ? scale?.barCategory?.colorOf(rowKeyValue(cell.row.original, visual.colorBy)) : void 0,
856
+ negativeColor: visual.negative !== false,
857
+ labelWidth: labelBoxes.get(cell.column.id)?.value
858
+ }
859
+ );
860
+ }
861
+ if (visual?.kind === "sparkline" || visual?.kind === "columns") {
862
+ const values = seriesValues(cell.row.original, visual.keys);
863
+ const label = values.map((v) => v === null ? "\u2013" : cellLabel(v, meta)).join(", ");
864
+ const domain = visual.range === "column" ? scale?.seriesExtent ?? null : extentOf(values);
865
+ if (visual.kind === "columns") {
866
+ return /* @__PURE__ */ jsx(
867
+ ColumnsCell,
868
+ {
869
+ values: visual.keys.map((key, i) => ({ key, value: values[i] ?? null })),
870
+ domain,
871
+ label,
872
+ height: visual.height
873
+ }
874
+ );
875
+ }
876
+ const ends = seriesEnds(values);
877
+ return /* @__PURE__ */ jsx(
878
+ SparklineCell,
879
+ {
880
+ values,
881
+ domain,
882
+ label,
883
+ fill: visual.fill,
884
+ height: visual.height,
885
+ ends: visual.labels === "ends" && ends ? [cellLabel(ends[0], meta), cellLabel(ends[1], meta)] : void 0,
886
+ endsWidth: labelBoxes.get(cell.column.id)?.ends
887
+ }
888
+ );
889
+ }
890
+ if (visual?.kind === "heatmap") {
891
+ return /* @__PURE__ */ jsx(HeatmapCell, { label: cellLabel(value, meta), hideValue: visual.hideValue });
892
+ }
893
+ if (meta?.markdown && typeof value === "string") {
894
+ const images = typeof meta.markdown === "object" && meta.markdown.images === true;
895
+ return /* @__PURE__ */ jsx(MarkdownCell, { text: value, images });
896
+ }
897
+ if (meta?.format && cell.column.columnDef.cell === defaultCellRenderer) {
898
+ return cellLabel(value, meta);
899
+ }
900
+ return flexRender(cell.column.columnDef.cell, cell.getContext());
901
+ }
902
+ function cellPresentation(cell, includeSizing = true) {
903
+ const meta = cell.column.columnDef.meta;
904
+ if (!meta) return {};
905
+ const scale = columnScales?.get(cell.column.id);
906
+ let style = includeSizing ? columnSizeStyle(meta) : void 0;
907
+ let className2;
908
+ if (meta.visual?.kind === "heatmap") {
909
+ const value = cell.getValue();
910
+ const color = scale?.heatmap?.colorOf(typeof value === "number" ? value : null) ?? null;
911
+ style = { ...style, ...heatmapCellStyle(color) };
912
+ className2 = meta.visual.hideValue ? "h-6 px-0 text-center" : "text-center";
913
+ }
914
+ if (meta.colorBy && (meta.colorBy.scope ?? "cell") === "cell") {
915
+ const color = scale?.category?.colorOf(rowKeyValue(cell.row.original, meta.colorBy.key)) ?? null;
916
+ const colorStyle = colorByStyle(meta.colorBy.target, color);
917
+ if (colorStyle) style = { ...style, ...colorStyle };
918
+ }
919
+ return { className: className2, style };
920
+ }
921
+ function rowColorStyle(row) {
922
+ for (const column of rowColorColumns) {
923
+ const colorBy = column.columnDef.meta?.colorBy;
924
+ if (!colorBy) continue;
925
+ const color = columnScales?.get(column.id)?.category?.colorOf(rowKeyValue(row.original, colorBy.key)) ?? null;
926
+ const style = colorByStyle(colorBy.target, color);
927
+ if (style) return style;
928
+ }
929
+ return void 0;
930
+ }
931
+ function rankOf(row) {
932
+ const rank = rowRanks?.get(row.id);
933
+ return rank === void 0 ? void 0 : formatNumber(rank);
934
+ }
935
+ function renderSortButton(header) {
936
+ const sorted = header.column.getIsSorted();
937
+ const headerLabel = typeof header.column.columnDef.header === "string" ? header.column.columnDef.header : header.column.id;
938
+ const sortStateLabel = sorted === "asc" ? "ascending" : sorted === "desc" ? "descending" : "not sorted";
939
+ const SortIcon = sorted === "asc" ? ArrowUp : sorted === "desc" ? ArrowDown : ArrowUpDown;
940
+ return /* @__PURE__ */ jsxs(
941
+ "button",
942
+ {
943
+ type: "button",
944
+ onClick: header.column.getToggleSortingHandler(),
945
+ "aria-label": `Sort by ${headerLabel}, ${sortStateLabel}`,
946
+ className: "relative z-10 inline-flex min-h-6 items-center gap-1 rounded-sm transition-colors duration-fast ease-standard hover:text-foreground focus-ring",
947
+ children: [
948
+ flexRender(header.column.columnDef.header, header.getContext()),
949
+ /* @__PURE__ */ jsx(
950
+ SortIcon,
951
+ {
952
+ "aria-hidden": "true",
953
+ className: "size-3 shrink-0 transition-colors duration-fast ease-standard"
954
+ }
955
+ )
956
+ ]
957
+ }
958
+ );
959
+ }
960
+ function renderThead(sticky, withRowIndex = false) {
961
+ const headerGroups = table.getHeaderGroups();
962
+ const mergedHeaderColumns = /* @__PURE__ */ new Set();
963
+ return /* @__PURE__ */ jsx(
964
+ "thead",
965
+ {
966
+ className: cn(
967
+ // #173: header bottom is the only cue between header and first data row → border-strong
968
+ // RM-123 `hideHeader`: the header row collapses to zero height (its
969
+ // labels stay for screen readers), so it draws no rule and no wash.
970
+ // A hidden header must not reserve width either — its own padding
971
+ // would otherwise floor every column (a 24 px minimum per cell).
972
+ hideHeader ? "[&_th]:h-0 [&_th]:p-0" : "border-b border-border-strong",
973
+ // A sticky header scrolls OVER the body, so its fill must be opaque or data
974
+ // rows bleed through the labels; the non-sticky header keeps the /60 wash.
975
+ // z-20 (raised from z-10 for #333) puts the header row above the pinned
976
+ // body cells (z-10) and below the pinned header corner (z-30). No visual
977
+ // delta: nothing else in the table sits between those rungs.
978
+ !hideHeader && (sticky ? "sticky top-0 z-20 bg-surface-muted" : "bg-surface-muted/60")
979
+ ),
980
+ children: headerGroups.map((headerGroup, groupIndex) => /* @__PURE__ */ jsxs("tr", { "aria-rowindex": withRowIndex ? groupIndex + 1 : void 0, children: [
981
+ hasGripColumn && /* @__PURE__ */ jsx(
982
+ "th",
983
+ {
984
+ scope: "col",
985
+ className: "h-10 w-10 px-3 align-middle bg-table-header-background",
986
+ children: /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("data.table.reorderColumnHeader") })
987
+ },
988
+ "__reorder"
989
+ ),
990
+ showRanks && groupIndex === 0 && /* @__PURE__ */ jsx(
991
+ DataTableRankHeader,
992
+ {
993
+ label: rankLabel,
994
+ rowSpan: headerGroups.length > 1 ? headerGroups.length : void 0,
995
+ className: cn(
996
+ headerHeightClass,
997
+ "font-table-header bg-table-header-background text-table-header-foreground text-table-header"
998
+ )
999
+ },
1000
+ "__rank"
1001
+ ),
1002
+ headerGroup.headers.map((header) => {
1003
+ const shownLeaves = header.getLeafHeaders().filter((h) => h.subHeaders.length === 0 && isColumnShown(h.column)).length;
1004
+ if (shownLeaves === 0) return null;
1005
+ let mergedRowSpan;
1006
+ if (mergeEmptyHeaders && mergedHeaderColumns.has(header.column.id)) return null;
1007
+ if (mergeEmptyHeaders && header.isPlaceholder) {
1008
+ mergedHeaderColumns.add(header.column.id);
1009
+ mergedRowSpan = headerGroups.length - groupIndex;
1010
+ }
1011
+ const merged = mergedRowSpan !== void 0;
1012
+ const geometry = pinnedCellGeometry(header.column);
1013
+ const canSort = header.column.getCanSort();
1014
+ const sorted = header.column.getIsSorted();
1015
+ const headerLabel = typeof header.column.columnDef.header === "string" ? header.column.columnDef.header : header.column.id;
1016
+ const resizeStyle = enableColumnResizing ? resizeWidthStyle(header.getSize()) : void 0;
1017
+ const canResize = enableColumnResizing && !header.isPlaceholder && header.column.getCanResize();
1018
+ const resizeMax = header.column.columnDef.maxSize;
1019
+ const content = header.isPlaceholder && !merged ? null : canSort ? renderSortButton(header) : flexRender(header.column.columnDef.header, header.getContext());
1020
+ return /* @__PURE__ */ jsxs(
1021
+ "th",
1022
+ {
1023
+ scope: "col",
1024
+ colSpan: shownLeaves > 1 ? shownLeaves : void 0,
1025
+ rowSpan: mergedRowSpan !== void 0 && mergedRowSpan > 1 ? mergedRowSpan : void 0,
1026
+ "aria-sort": canSort ? sorted === "asc" ? "ascending" : sorted === "desc" ? "descending" : "none" : void 0,
1027
+ "data-pinned": geometry?.pinned ?? void 0,
1028
+ style: geometry?.style ?? resizeStyle ?? columnSizeStyle(header.column.columnDef.meta),
1029
+ className: cn(
1030
+ // Same `px-3` the body `<td>` uses (below) — deliberately
1031
+ // NOT split into `ps-3`/`pe-3` for a resize-handle
1032
+ // override (round-1 briefly did this, see the round-2
1033
+ // note on `numericColumnClasses`): the header's padding
1034
+ // must stay byte-identical to the body's so an
1035
+ // end-aligned numeric column's header lines up with its
1036
+ // own values.
1037
+ // Table-header seams (fidelity review #4): a theme dials
1038
+ // background/foreground/size/transform/tracking via the
1039
+ // `--table-header-*` contract; every default equals
1040
+ // today's byte-identical rendering (transparent bg,
1041
+ // `--muted-foreground` ink, 1em size = the table's own
1042
+ // body size, no transform, body tracking).
1043
+ headerHeightClass,
1044
+ "px-3 text-start align-middle font-table-header bg-table-header-background text-table-header-foreground text-table-header tracking-(--table-header-tracking) [text-transform:var(--table-header-transform)]",
1045
+ // #69: a numeric column's `meta` overrides the default
1046
+ // `text-start` — placed right after the base string so
1047
+ // tailwind-merge lets it win over that default.
1048
+ numericColumnClasses(header.column.columnDef.meta),
1049
+ // `sticky`/pinned already establishes a positioning context
1050
+ // for the resize handle's `absolute`; an unpinned resizable
1051
+ // header needs its own.
1052
+ !geometry && canResize && "relative",
1053
+ // A pinned HEADER cell is the corner where both freezes meet,
1054
+ // so it stacks above the sticky header row (z-20) which is
1055
+ // above the pinned body cells (z-10). It needs an OPAQUE
1056
+ // fill (scrolled header cells pass underneath it), and that
1057
+ // fill has to composite to exactly what its unpinned
1058
+ // neighbours show — same problem, same two-layer answer as
1059
+ // `pinnedCellFillClass`:
1060
+ // sticky branch → the row is already opaque `surface-muted`, so match it.
1061
+ // plain branch → the row is `surface-muted/60` over the
1062
+ // container's `card`, so paint `card` and
1063
+ // re-apply the /60 wash on `::before`.
1064
+ // Painting the plain branch's corner solid `surface-muted`
1065
+ // read 4-5/255 darker than the header beside it in every
1066
+ // theme (measured: 242 vs 247 light, 43 vs 40
1067
+ // dark) — the same "floating pill"
1068
+ // artefact #333 was filed about, moved into the header.
1069
+ geometry && "sticky z-30",
1070
+ geometry && (sticky ? "bg-surface-muted" : "bg-card before:pointer-events-none before:absolute before:inset-0 before:-z-10 before:bg-surface-muted/60 before:content-['']"),
1071
+ // Pinned corner keeps its OPAQUE ground above (unchanged
1072
+ // default `transparent` on `--table-header-background`
1073
+ // paints nothing here, byte-identical). A theme that fills
1074
+ // the header instead layers that fill on an `after:`
1075
+ // pseudo ABOVE the ground/wash (source order after
1076
+ // `before:` at the same `-z-10` rung, below the cell's own
1077
+ // text) so the corner stays opaque either way — it never
1078
+ // replaces the ground the way overwriting `background-color`
1079
+ // directly would.
1080
+ geometry && "after:pointer-events-none after:absolute after:inset-0 after:-z-10 after:bg-table-header-background after:content-['']",
1081
+ // Separate cn() argument on purpose: the seam is the sole
1082
+ // structural cue between the frozen and scrolling blocks, so
1083
+ // it must not read as a "boundary + fill in one class string"
1084
+ // redundancy (separation:check).
1085
+ geometry?.edgeClass,
1086
+ columnDividers && !geometry && COLUMN_DIVIDER_CLASS
1087
+ ),
1088
+ children: [
1089
+ hideHeader && content !== null ? (
1090
+ // A focused sort button un-hides its label (skip-link idiom).
1091
+ /* @__PURE__ */ jsx("span", { className: "sr-only focus-within:not-sr-only", children: content })
1092
+ ) : content,
1093
+ canResize && /* @__PURE__ */ jsx(
1094
+ "div",
1095
+ {
1096
+ role: "separator",
1097
+ "aria-orientation": "vertical",
1098
+ "aria-valuenow": Math.round(header.getSize()),
1099
+ "aria-valuemin": header.column.columnDef.minSize,
1100
+ "aria-valuemax": resizeMax !== void 0 && resizeMax < Number.MAX_SAFE_INTEGER ? resizeMax : Math.max(header.getSize(), RESIZE_UNBOUNDED_ARIA_MAX),
1101
+ "aria-valuetext": t("data.table.resizeColumnValue", {
1102
+ count: Math.round(header.getSize()),
1103
+ size: formatNumber(Math.round(header.getSize()))
1104
+ }),
1105
+ "aria-label": t("data.table.resizeColumn", { name: headerLabel }),
1106
+ tabIndex: 0,
1107
+ "data-slot": "data-table-resize-handle",
1108
+ onMouseDown: header.getResizeHandler(),
1109
+ onTouchStart: header.getResizeHandler(),
1110
+ onKeyDown: (event) => handleResizeKeyDown(event, header.column),
1111
+ onDoubleClick: () => handleResizeDoubleClick(header.column),
1112
+ className: cn(
1113
+ // #51: the hit box is a literal 24px (clamped to half
1114
+ // the header cell so it can never overlap a neighbour,
1115
+ // even at `minSize=20`) rather than the `w-2` Tailwind
1116
+ // spacing-scale utility. `w-2` compiles to
1117
+ // `calc(var(--spacing) * 2)`, and `--spacing` is what
1118
+ // `data-density="compact"` rescales — so the old 8px
1119
+ // hit box shrank further under compact density
1120
+ // (~7.1px). A literal px value is density-independent
1121
+ // by construction, which is the actual defect the
1122
+ // maintainer's review corrected (NOT `--type-factor`,
1123
+ // which this handle never used). Do not widen via
1124
+ // overhang into the neighbouring cell instead — on the
1125
+ // last column that lands inside the `overflow-auto`
1126
+ // box (#330 false positive) and a pinned neighbour
1127
+ // paints over/hit-tests away the extra area.
1128
+ "absolute inset-y-0 end-0 w-[min(24px,50%)] cursor-col-resize touch-none select-none",
1129
+ // #51: the focus ring moves to the `after:` pseudo-
1130
+ // element (the drawn seam) rather than the box itself
1131
+ // — the box is now a 24px hit target, and a 24px focus
1132
+ // rectangle would replace the deliberately slim ring
1133
+ // already reviewed/approved as the #12 a11y fix
1134
+ // (da9b29e). `focus-visible:after:*` targets the
1135
+ // pseudo-element the same way `hover:after:w-2` /
1136
+ // `focus-visible:after:w-2` below already do.
1137
+ "focus-visible:outline-none",
1138
+ // a11y fix (#12 review, blocking): this handle is the
1139
+ // SOLE boundary between two adjacent header cells once
1140
+ // resizing is on — no fill/elevation change separates
1141
+ // them otherwise — so per the border/border-strong
1142
+ // decision test (styling-and-tokens.md) it needs a
1143
+ // rung that clears WCAG 1.4.11's 3:1 on its OWN, in
1144
+ // EVERY state, including rest (a control with no
1145
+ // affordance until hover is unusable without a
1146
+ // pointer). `border-strong` measures only 2.86-2.96:1
1147
+ // against this `bg-surface-muted` header — that rung
1148
+ // is guaranteed only vs `--card`/`--background`, not a
1149
+ // same-tone surface, which is the exact trap the rule
1150
+ // warns about. `muted-foreground` is guaranteed AA
1151
+ // text contrast against `--surface-muted`
1152
+ // (TEXT_SURFACES), so it clears the 3:1 non-text
1153
+ // minimum with wide margin (measured ~5.3-6.4:1 in
1154
+ // both themes, unaffected by density) and is already
1155
+ // the header's own label color. A slim persistent
1156
+ // `after:` seam (not just a hover reveal) gives the
1157
+ // real resting boundary; hover/focus widen the drawn
1158
+ // seam to 8px (`after:w-2`) using the same compliant
1159
+ // color — a separate width from the 24px pointer hit
1160
+ // box below (#51), which the seam does not fill.
1161
+ // Dragging keeps the pre-existing full-fill
1162
+ // `bg-primary` treatment — that is a drag AFFORDANCE,
1163
+ // not a focus indicator, and it is redundant with the
1164
+ // pointer capture, so it is out of scope here. The
1165
+ // keyboard focus indicator on both branches is the
1166
+ // shared compound one (#67), applied to the drawn seam
1167
+ // via `focus-visible:after:focus-ring-static`: the
1168
+ // element itself is a 24px transparent hit box, so
1169
+ // ringing IT would ring nothing a user can see.
1170
+ header.column.getIsResizing() ? "after:absolute after:inset-y-0 after:end-0 after:w-2 after:bg-primary after:content-[''] focus-visible:after:focus-ring-static" : "after:absolute after:inset-y-0 after:end-0 after:w-px after:bg-muted-foreground after:content-[''] hover:after:w-2 focus-visible:after:w-2 focus-visible:after:focus-ring-static"
1171
+ )
1172
+ }
1173
+ )
1174
+ ]
1175
+ },
1176
+ header.id
1177
+ );
1178
+ })
1179
+ ] }, headerGroup.id))
1180
+ }
1181
+ );
1182
+ }
1183
+ function rowSeparationClass(rowIndex) {
1184
+ if (!zebra) return "border-b border-border-strong last:border-b-0";
1185
+ return cn(
1186
+ "border-b-(length:--table-row-rule-width) border-border-strong last:border-b-0",
1187
+ // Separate cn() argument: the stripe and the (theme-gated) rule are
1188
+ // alternative cues, never both at once — see the jsdoc above.
1189
+ rowIndex % 2 === 1 && "bg-table-stripe"
1190
+ );
1191
+ }
1192
+ function pinnedCellFillClass(rowIndex) {
1193
+ return cn(
1194
+ "bg-card",
1195
+ "before:pointer-events-none before:absolute before:inset-0 before:-z-10 before:content-['']",
1196
+ zebra && rowIndex % 2 === 1 && "before:bg-table-stripe",
1197
+ "group-hover/row:before:bg-table-row-hover",
1198
+ "group-data-[state=selected]/row:before:bg-selection"
1199
+ );
1200
+ }
1201
+ function rowActionName(row) {
1202
+ const explicit = rowActionLabel?.(row);
1203
+ if (explicit) return explicit;
1204
+ const name = firstDataCellValue(row);
1205
+ if (name !== void 0) return name;
1206
+ return t("data.table.rowAction");
1207
+ }
1208
+ function renderRow(row, rowIndex, extras, dragHandle) {
1209
+ const clickable = Boolean(onRowClick);
1210
+ function handleRowClick(event) {
1211
+ if (isInteractiveEventTarget(event.target)) return;
1212
+ if (isActiveTextSelection()) return;
1213
+ onRowClick?.(row, event);
1214
+ }
1215
+ const sticky = stickyActive ? row.getIsPinned() || void 0 : void 0;
1216
+ const rowStyle = rowColorStyle(row);
1217
+ return /* @__PURE__ */ jsxs(
1218
+ "tr",
1219
+ {
1220
+ "data-state": row.getIsSelected() ? "selected" : void 0,
1221
+ "data-sticky": sticky,
1222
+ onClick: clickable ? handleRowClick : void 0,
1223
+ className: cn(
1224
+ // Color-only feedback (no transform/movement) → per
1225
+ // docs/MOTION_GUIDELINES.md item 3 this stays under OS reduced-motion
1226
+ // (only movement is neutralized); the gated duration-fast/ease-standard
1227
+ // pair already collapses toward ~0ms via --motion-factor when the user
1228
+ // or OS asks for reduced motion, matching the header sort button.
1229
+ "transition-colors duration-fast ease-standard hover:bg-table-row-hover data-[state=selected]:bg-selection",
1230
+ // #13: the dragged row's live `transform` (set inline via `extras.style`,
1231
+ // see `SortableDataRow`) is what actually MOVES it — this class only
1232
+ // makes that movement glide instead of snapping, through the gated
1233
+ // duration/ease utilities (never a raw ms/ease value —
1234
+ // quality-gates.md "Motion-tokened") with a reduced-motion
1235
+ // neutralizer. Raising the dragged row's stacking + opacity is a
1236
+ // colour/composite-only cue, so it isn't gated by the same rule.
1237
+ dragHandle && "relative transition-transform duration-base ease-standard motion-reduce:transition-none",
1238
+ dragHandle?.isDragging && "z-20 opacity-90 shadow-md",
1239
+ // Named group (#333) so a PINNED cell can re-apply the row's hover /
1240
+ // selected wash on top of its own opaque fill — only CSS knows the
1241
+ // pointer is over a sibling cell. Purely a selector hook: `group/row`
1242
+ // emits no style of its own.
1243
+ "group/row",
1244
+ rowSeparationClass(rowIndex),
1245
+ // RM-123 sticky rows: a quiet header-tone wash + medium weight mark
1246
+ // the "average" / "total" rows that repeat on every page.
1247
+ sticky && "font-medium",
1248
+ sticky && "bg-surface-muted/60",
1249
+ // `<tr>` isn't in the global auto-cursor-pointer role list (button/
1250
+ // menuitem/tab/…), so a clickable row needs its own cursor. The focus
1251
+ // ring is driven off the hidden button's `:focus-visible` (same
1252
+ // `has-[[data-slot=…]:focus-visible]` pattern as InputGroup) so the
1253
+ // ring paints on the ROW the user is about to activate, even though
1254
+ // focus lives on the sr-only control inside it.
1255
+ clickable && "cursor-pointer has-[[data-slot=data-table-row-action]:focus-visible]:focus-ring-static-inset",
1256
+ rowClassName?.(row)
1257
+ ),
1258
+ ...extras,
1259
+ style: rowStyle || extras?.style ? { ...rowStyle, ...extras?.style } : void 0,
1260
+ children: [
1261
+ dragHandle?.activator && /* @__PURE__ */ jsx("td", { className: "w-10 px-3 py-2 align-middle", children: /* @__PURE__ */ jsx(
1262
+ "button",
1263
+ {
1264
+ type: "button",
1265
+ ref: dragHandle.activator.setActivatorNodeRef,
1266
+ "data-slot": "data-table-row-drag-handle",
1267
+ "aria-label": t("data.table.reorderHandle", { name: rowActionName(row) }),
1268
+ className: cn(
1269
+ "inline-flex size-7 cursor-grab items-center justify-center rounded-sm text-muted-foreground transition-colors duration-fast ease-standard hover:bg-foreground/10 hover:text-foreground focus-ring active:cursor-grabbing",
1270
+ dragHandle.isDragging && "text-foreground"
1271
+ ),
1272
+ ...dragHandle.activator.attributes,
1273
+ ...dragHandle.activator.listeners,
1274
+ children: /* @__PURE__ */ jsx(GripVertical, { "aria-hidden": "true", className: "size-4" })
1275
+ }
1276
+ ) }),
1277
+ showRanks && /* @__PURE__ */ jsx(DataTableRankCell, { rank: rankOf(row), className: cellPadYClass }),
1278
+ row.getVisibleCells().filter((cell) => isColumnShown(cell.column)).map((cell, cellIndex) => {
1279
+ const geometry = pinnedCellGeometry(cell.column);
1280
+ const resizeStyle = enableColumnResizing ? resizeWidthStyle(cell.column.getSize()) : void 0;
1281
+ const presentation = cellPresentation(cell);
1282
+ const baseStyle = geometry?.style ?? resizeStyle;
1283
+ return /* @__PURE__ */ jsxs(
1284
+ "td",
1285
+ {
1286
+ "data-pinned": geometry?.pinned ?? void 0,
1287
+ style: presentation.style ? { ...baseStyle, ...presentation.style } : baseStyle,
1288
+ className: cn(
1289
+ "px-3 align-middle",
1290
+ cellPadYClass,
1291
+ // #69: same numeric-column seam as the header — see
1292
+ // `numericColumnClasses`.
1293
+ numericColumnClasses(cell.column.columnDef.meta),
1294
+ // z-10: above the normal (unpositioned) cells it scrolls over,
1295
+ // below the sticky header row (z-20) and the pinned corner (z-30).
1296
+ geometry && "sticky z-10",
1297
+ geometry && pinnedCellFillClass(rowIndex),
1298
+ // Separate cn() argument — see pinnedCellGeometry's edgeClass.
1299
+ geometry?.edgeClass,
1300
+ columnDividers && !geometry && COLUMN_DIVIDER_CLASS,
1301
+ presentation.className
1302
+ ),
1303
+ children: [
1304
+ clickable && cellIndex === 0 && /* @__PURE__ */ jsx(
1305
+ "button",
1306
+ {
1307
+ type: "button",
1308
+ "data-slot": "data-table-row-action",
1309
+ className: ROW_ACTION_CLASS,
1310
+ onClick: (event) => onRowClick?.(row, event),
1311
+ children: rowActionName(row)
1312
+ }
1313
+ ),
1314
+ renderCellContent(cell)
1315
+ ]
1316
+ },
1317
+ cell.id
1318
+ );
1319
+ })
1320
+ ]
1321
+ },
1322
+ row.id
1323
+ );
1324
+ }
1325
+ function renderSkeletonBody(count) {
1326
+ const visibleColumns = table.getVisibleLeafColumns().filter(isColumnShown);
1327
+ return Array.from({ length: count }).map((_, i) => /* @__PURE__ */ jsxs("tr", { "aria-hidden": "true", className: rowSeparationClass(i), children: [
1328
+ hasGripColumn && /* @__PURE__ */ jsx("td", { className: "w-10 px-3 py-2 align-middle", children: /* @__PURE__ */ jsx(Skeleton, { className: "size-4" }) }),
1329
+ showRanks && /* @__PURE__ */ jsx("td", { className: cn("w-10 px-3 align-middle", cellPadYClass), children: /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" }) }),
1330
+ visibleColumns.map((column) => /* @__PURE__ */ jsx(
1331
+ "td",
1332
+ {
1333
+ className: cn(
1334
+ "px-3 align-middle",
1335
+ cellPadYClass,
1336
+ numericColumnClasses(column.columnDef.meta),
1337
+ columnDividers && COLUMN_DIVIDER_CLASS
1338
+ ),
1339
+ children: /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" })
1340
+ },
1341
+ column.id
1342
+ ))
1343
+ ] }, `skeleton-${i}`));
1344
+ }
1345
+ function renderEmptyBody() {
1346
+ return /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
1347
+ "td",
1348
+ {
1349
+ colSpan: colCount + leadingColCount,
1350
+ className: "h-24 px-3 text-center text-muted-foreground",
1351
+ children: emptyMessage
1352
+ }
1353
+ ) });
1354
+ }
1355
+ function renderTbodyNormal() {
1356
+ if (showSkeletons) {
1357
+ return /* @__PURE__ */ jsx("tbody", { children: renderSkeletonBody(skeletonRowCount) });
1358
+ }
1359
+ if (showEmpty) {
1360
+ return /* @__PURE__ */ jsx("tbody", { children: renderEmptyBody() });
1361
+ }
1362
+ if (!rowReorderActive) {
1363
+ return /* @__PURE__ */ jsxs("tbody", { children: [
1364
+ topRows.map((row, i) => renderRow(row, i)),
1365
+ rows.map((row, i) => renderRow(row, i)),
1366
+ bottomRows.map((row, i) => renderRow(row, i))
1367
+ ] });
1368
+ }
1369
+ return /* @__PURE__ */ jsx(
1370
+ SortableContext,
1371
+ {
1372
+ items: rows.map((r) => getReorderRowId(r)),
1373
+ strategy: verticalListSortingStrategy,
1374
+ children: /* @__PURE__ */ jsxs("tbody", { children: [
1375
+ topRows.map((row, i) => renderRow(row, i)),
1376
+ rows.map((row, i) => /* @__PURE__ */ jsx(
1377
+ SortableDataRow,
1378
+ {
1379
+ id: getReorderRowId(row),
1380
+ attributesOverride: {
1381
+ // #98: dnd-kit's own `roleDescription: 'sortable'` default is
1382
+ // hardcoded English; override it with the localized value in
1383
+ // BOTH handle modes — `role` stays row-mode-only (see the
1384
+ // `attributesOverride` prop doc above).
1385
+ roleDescription: t("data.table.reorderRoleDescription"),
1386
+ ...rowReorderHandle === "row" ? { role: "row" } : null
1387
+ },
1388
+ children: ({ setNodeRef, setActivatorNodeRef, attributes, listeners, isDragging, style }) => renderRow(
1389
+ row,
1390
+ i,
1391
+ {
1392
+ ref: setNodeRef,
1393
+ style,
1394
+ // `aria-pressed` is a `DraggableAttributes` field meant for a
1395
+ // real `<button>` activator; spread onto a `<tr role="row">`
1396
+ // (row-handle mode) it fails axe's `aria-allowed-attr` (that
1397
+ // ARIA state is not permitted on the `row` role), so strip it
1398
+ // here rather than exempt it downstream.
1399
+ ...rowReorderHandle === "row" ? (() => {
1400
+ const { "aria-pressed": _ariaPressed, ...rowAttributes } = attributes;
1401
+ return { ...rowAttributes, ...listeners };
1402
+ })() : {}
1403
+ },
1404
+ {
1405
+ isDragging,
1406
+ activator: rowReorderHandle === "cell" ? { setActivatorNodeRef, attributes, listeners } : void 0
1407
+ }
1408
+ )
1409
+ },
1410
+ getReorderRowId(row)
1411
+ )),
1412
+ bottomRows.map((row, i) => renderRow(row, i))
1413
+ ] })
1414
+ }
1415
+ );
1416
+ }
1417
+ function renderTbodyVirtualized() {
1418
+ if (showSkeletons) {
1419
+ const virtualSkeletonCount = loadingRows ?? Math.min(10, pageSize);
1420
+ return /* @__PURE__ */ jsx("tbody", { children: renderSkeletonBody(virtualSkeletonCount) });
1421
+ }
1422
+ return /* @__PURE__ */ jsx("tbody", { children: showEmpty ? renderEmptyBody() : /* @__PURE__ */ jsxs(Fragment, { children: [
1423
+ topRows.map(
1424
+ (row, i) => renderRow(row, i, {
1425
+ "aria-rowindex": headerRowCount + i + 1
1426
+ })
1427
+ ),
1428
+ paddingTop > 0 && /* @__PURE__ */ jsx("tr", { "aria-hidden": "true", children: /* @__PURE__ */ jsx("td", { style: { height: paddingTop }, colSpan: colCount + leadingColCount }) }),
1429
+ virtualItems.map((virtualRow) => {
1430
+ const row = rows[virtualRow.index];
1431
+ if (!row) return null;
1432
+ return renderRow(row, virtualRow.index, {
1433
+ ref: virtualizer.measureElement,
1434
+ "data-index": virtualRow.index,
1435
+ // Absolute 1-based row position; header row(s) occupy
1436
+ // 1..headerRowCount and any top-pinned rows the slots after them.
1437
+ "aria-rowindex": firstCentreRowIndex + virtualRow.index
1438
+ });
1439
+ }),
1440
+ paddingBottom > 0 && /* @__PURE__ */ jsx("tr", { "aria-hidden": "true", children: /* @__PURE__ */ jsx("td", { style: { height: paddingBottom }, colSpan: colCount + leadingColCount }) }),
1441
+ bottomRows.map(
1442
+ (row, i) => renderRow(row, i, {
1443
+ "aria-rowindex": firstCentreRowIndex + centreRowCount + i
1444
+ })
1445
+ )
1446
+ ] }) });
1447
+ }
1448
+ function leafHeadersById() {
1449
+ const bottom = headerGroupsForCards[headerGroupsForCards.length - 1];
1450
+ return new Map((bottom?.headers ?? []).map((h) => [h.column.id, h]));
1451
+ }
1452
+ function renderCardSortBar() {
1453
+ const sortable = [...leafHeadersById().values()].filter(
1454
+ (h) => !h.isPlaceholder && isColumnShown(h.column) && h.column.getCanSort()
1455
+ );
1456
+ const selectHeader = leafHeadersById().get("select");
1457
+ if (sortable.length === 0 && !selectHeader) return null;
1458
+ return /* @__PURE__ */ jsxs(
1459
+ "div",
1460
+ {
1461
+ "data-slot": "data-table-card-sort",
1462
+ className: "flex flex-wrap items-center gap-x-4 gap-y-1 border-b border-border-strong px-3 py-2 text-meta text-muted-foreground",
1463
+ children: [
1464
+ selectHeader && !selectHeader.isPlaceholder ? /* @__PURE__ */ jsx("span", { className: "inline-flex", children: flexRender(selectHeader.column.columnDef.header, selectHeader.getContext()) }) : null,
1465
+ sortable.map((header) => /* @__PURE__ */ jsx("span", { className: "inline-flex", children: renderSortButton(header) }, header.id))
1466
+ ]
1467
+ }
1468
+ );
1469
+ }
1470
+ function renderCard(row, headers, extras) {
1471
+ const cells = row.getVisibleCells().filter((cell) => isColumnShown(cell.column));
1472
+ const selectCell = cells.find((cell) => cell.column.id === "select");
1473
+ const clickable = Boolean(onRowClick);
1474
+ const sticky = stickyActive ? row.getIsPinned() || void 0 : void 0;
1475
+ const fields = [];
1476
+ const rank = rankOf(row);
1477
+ if (showRanks && rank !== void 0) {
1478
+ fields.push({
1479
+ id: "__rank",
1480
+ term: rankLabel ?? t("data.table.rankHeader"),
1481
+ value: rank,
1482
+ className: "tabular-nums"
1483
+ });
1484
+ }
1485
+ for (const cell of cells) {
1486
+ if (cell === selectCell) continue;
1487
+ const def = cell.column.columnDef.header;
1488
+ const header = headers.get(cell.column.id);
1489
+ const presentation = cellPresentation(cell, false);
1490
+ fields.push({
1491
+ id: cell.column.id,
1492
+ term: typeof def === "string" ? def : header ? flexRender(def, header.getContext()) : cell.column.id,
1493
+ value: renderCellContent(cell),
1494
+ className: cn(
1495
+ cell.column.columnDef.meta?.numeric && "tabular-nums",
1496
+ presentation.style?.backgroundColor !== void 0 && "min-h-5 rounded-sm px-1"
1497
+ ),
1498
+ style: presentation.style
1499
+ });
1500
+ }
1501
+ return /* @__PURE__ */ jsx(
1502
+ DataTableCard,
1503
+ {
1504
+ ref: extras?.ref,
1505
+ "data-index": extras?.["data-index"],
1506
+ fields,
1507
+ density,
1508
+ "data-state": row.getIsSelected() ? "selected" : void 0,
1509
+ "data-sticky": sticky,
1510
+ onClick: clickable ? (event) => {
1511
+ if (isInteractiveEventTarget(event.target)) return;
1512
+ if (isActiveTextSelection()) return;
1513
+ onRowClick?.(row, event);
1514
+ } : void 0,
1515
+ style: rowColorStyle(row),
1516
+ className: cn(
1517
+ "transition-colors duration-fast ease-standard data-[state=selected]:bg-selection",
1518
+ clickable && "cursor-pointer hover:bg-table-row-hover has-[[data-slot=data-table-row-action]:focus-visible]:focus-ring-static-inset",
1519
+ sticky && "font-medium",
1520
+ sticky && "bg-surface-muted/60"
1521
+ ),
1522
+ lead: selectCell || clickable ? /* @__PURE__ */ jsxs(Fragment, { children: [
1523
+ selectCell && /* @__PURE__ */ jsx("div", { className: "mb-1.5", children: flexRender(selectCell.column.columnDef.cell, selectCell.getContext()) }),
1524
+ clickable && /* @__PURE__ */ jsx(
1525
+ "button",
1526
+ {
1527
+ type: "button",
1528
+ "data-slot": "data-table-row-action",
1529
+ className: ROW_ACTION_CLASS,
1530
+ onClick: (event) => onRowClick?.(row, event),
1531
+ children: rowActionName(row)
1532
+ }
1533
+ )
1534
+ ] }) : void 0
1535
+ },
1536
+ row.id
1537
+ );
1538
+ }
1539
+ function renderCardList(virtualized) {
1540
+ if (showSkeletons) {
1541
+ const count = virtualized ? loadingRows ?? Math.min(10, pageSize) : skeletonRowCount;
1542
+ return /* @__PURE__ */ jsx(DataTableCardList, { "aria-hidden": "true", children: Array.from({ length: count }).map((_, i) => /* @__PURE__ */ jsxs("li", { className: "space-y-2 px-3 py-3", children: [
1543
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-2/3" }),
1544
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-1/2" })
1545
+ ] }, `skeleton-${i}`)) });
1546
+ }
1547
+ if (showEmpty) {
1548
+ return /* @__PURE__ */ jsx(StatePanel, { kind: "empty", title: emptyMessage });
1549
+ }
1550
+ const headers = leafHeadersById();
1551
+ return /* @__PURE__ */ jsxs(DataTableCardList, { "aria-labelledby": caption != null ? captionId : void 0, children: [
1552
+ topRows.map((row) => renderCard(row, headers)),
1553
+ virtualized && paddingTop > 0 && /* @__PURE__ */ jsx("li", { "aria-hidden": "true", style: { height: paddingTop } }),
1554
+ virtualized ? virtualItems.map((virtualRow) => {
1555
+ const row = rows[virtualRow.index];
1556
+ if (!row) return null;
1557
+ return renderCard(row, headers, {
1558
+ ref: virtualizer.measureElement,
1559
+ "data-index": virtualRow.index
1560
+ });
1561
+ }) : rows.map((row) => renderCard(row, headers)),
1562
+ virtualized && paddingBottom > 0 && /* @__PURE__ */ jsx("li", { "aria-hidden": "true", style: { height: paddingBottom } }),
1563
+ bottomRows.map((row) => renderCard(row, headers))
1564
+ ] });
1565
+ }
1566
+ const cardsCaption = caption != null ? /* @__PURE__ */ jsx("p", { id: captionId, className: "sr-only", children: caption }) : null;
1567
+ function renderLegends() {
1568
+ const seen = /* @__PURE__ */ new Set();
1569
+ const legends = [];
1570
+ for (const column of table.getVisibleLeafColumns()) {
1571
+ const visual = column.columnDef.meta?.visual;
1572
+ if (visual?.kind !== "heatmap" || !visual.legend || !isColumnShown(column)) continue;
1573
+ const scale = columnScales?.get(column.id);
1574
+ if (!scale?.heatmap || scale.heatmapGroup === void 0 || seen.has(scale.heatmapGroup)) {
1575
+ continue;
1576
+ }
1577
+ seen.add(scale.heatmapGroup);
1578
+ const header = column.columnDef.header;
1579
+ legends.push(
1580
+ /* @__PURE__ */ jsx(
1581
+ HeatmapLegend,
1582
+ {
1583
+ scale: scale.heatmap,
1584
+ title: typeof visual.legend === "string" ? visual.legend : typeof header === "string" ? header : void 0,
1585
+ formatValue: (v) => cellLabel(v, column.columnDef.meta)
1586
+ },
1587
+ column.id
1588
+ )
1589
+ );
1590
+ }
1591
+ const seenCategoryKeys = /* @__PURE__ */ new Set();
1592
+ for (const column of table.getVisibleLeafColumns()) {
1593
+ if (!isColumnShown(column)) continue;
1594
+ const meta = column.columnDef.meta;
1595
+ const scale = columnScales?.get(column.id);
1596
+ const bar = meta?.visual?.kind === "bar" ? meta.visual : void 0;
1597
+ const keys = [];
1598
+ if (bar?.colorBy && scale?.barCategory) {
1599
+ keys.push({ key: bar.colorBy, scale: scale.barCategory, legend: bar.legend });
1600
+ }
1601
+ if (meta?.colorBy && scale?.category) {
1602
+ keys.push({ key: meta.colorBy.key, scale: scale.category, legend: meta.colorBy.legend });
1603
+ }
1604
+ for (const entry of keys) {
1605
+ if (entry.legend === false || seenCategoryKeys.has(entry.key)) continue;
1606
+ seenCategoryKeys.add(entry.key);
1607
+ legends.push(
1608
+ /* @__PURE__ */ jsx(
1609
+ CategoryLegend,
1610
+ {
1611
+ scale: entry.scale,
1612
+ title: typeof entry.legend === "string" ? entry.legend : entry.key
1613
+ },
1614
+ `category-${entry.key}`
1615
+ )
1616
+ );
1617
+ }
1618
+ }
1619
+ const rankKey = showRanks ? /* @__PURE__ */ jsx(
1620
+ "p",
1621
+ {
1622
+ "data-slot": "data-table-rank-key",
1623
+ className: "text-meta text-muted-foreground",
1624
+ children: rankLabel != null ? `# \u2014 ${rankLabel}` : t("data.table.rankKey")
1625
+ },
1626
+ "__rank-key"
1627
+ ) : null;
1628
+ if (legends.length === 0 && rankKey === null) return null;
1629
+ return /* @__PURE__ */ jsxs("div", { "data-slot": "data-table-legends", className: "flex flex-wrap gap-x-6 gap-y-2", children: [
1630
+ legends,
1631
+ rankKey
1632
+ ] });
1633
+ }
1634
+ function renderPagination() {
1635
+ if (enableRowVirtualization) return null;
1636
+ if (!enablePagination && !manualPagination) return null;
1637
+ const pageCountUnknown = manualPagination && rowCount === void 0 && pageCount === void 0;
1638
+ if (hidePaginationWhenSingle && !pageCountUnknown && table.getPageCount() <= 1) return null;
1639
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
1640
+ /* @__PURE__ */ jsxs("p", { className: "text-body text-muted-foreground", children: [
1641
+ "Page ",
1642
+ table.getState().pagination.pageIndex + 1,
1643
+ " of ",
1644
+ table.getPageCount() || 1
1645
+ ] }),
1646
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
1647
+ /* @__PURE__ */ jsx(
1648
+ Button,
1649
+ {
1650
+ variant: "outline",
1651
+ size: "sm",
1652
+ onClick: () => table.previousPage(),
1653
+ disabled: !table.getCanPreviousPage(),
1654
+ children: "Previous"
1655
+ }
1656
+ ),
1657
+ /* @__PURE__ */ jsx(
1658
+ Button,
1659
+ {
1660
+ variant: "outline",
1661
+ size: "sm",
1662
+ onClick: () => table.nextPage(),
1663
+ disabled: !table.getCanNextPage(),
1664
+ children: "Next"
1665
+ }
1666
+ )
1667
+ ] })
1668
+ ] });
1669
+ }
1670
+ const captionElement = caption != null ? /* @__PURE__ */ jsx("caption", { className: "sr-only", children: caption }) : null;
1671
+ if (enableRowVirtualization) {
1672
+ return /* @__PURE__ */ jsxs("div", { ref: rootRef, ...presentationAttrs, className: cn("space-y-3", className), ...rest, children: [
1673
+ toolbar ? toolbar(table) : null,
1674
+ renderLegends(),
1675
+ /* @__PURE__ */ jsxs(
1676
+ "div",
1677
+ {
1678
+ ref: scrollRef,
1679
+ tabIndex: 0,
1680
+ role: "group",
1681
+ "aria-label": t("data.table.scrollRegion"),
1682
+ "aria-busy": loading || void 0,
1683
+ className: "relative overflow-auto rounded-lg border bg-card focus-ring",
1684
+ style: { maxHeight: maxBodyHeight, ...pinnedScrollPadding },
1685
+ children: [
1686
+ loading && rows.length > 0 && /* @__PURE__ */ jsxs(
1687
+ "div",
1688
+ {
1689
+ role: "status",
1690
+ "aria-live": "polite",
1691
+ className: "absolute inset-0 z-40 flex items-center justify-center rounded-lg bg-card/80",
1692
+ children: [
1693
+ /* @__PURE__ */ jsx(Spinner, { "aria-hidden": "true", className: "text-foreground" }),
1694
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("data.table.loading") })
1695
+ ]
1696
+ }
1697
+ ),
1698
+ cardsActive ? /* @__PURE__ */ jsxs("div", { "data-slot": "data-table-card-region", className: "text-body", children: [
1699
+ cardsCaption,
1700
+ renderCardSortBar(),
1701
+ renderCardList(true)
1702
+ ] }) : /* @__PURE__ */ jsxs(
1703
+ "table",
1704
+ {
1705
+ "aria-busy": loading || void 0,
1706
+ "aria-rowcount": ariaRowCount,
1707
+ className: "w-full caption-bottom text-body",
1708
+ children: [
1709
+ captionElement,
1710
+ renderThead(true, true),
1711
+ renderTbodyVirtualized()
1712
+ ]
1713
+ }
1714
+ )
1715
+ ]
1716
+ }
1717
+ )
1718
+ ] });
1719
+ }
1720
+ const nonVirtualizedContent = /* @__PURE__ */ jsxs("div", { ref: rootRef, ...presentationAttrs, className: cn("space-y-3", className), ...rest, children: [
1721
+ toolbar ? toolbar(table) : null,
1722
+ renderLegends(),
1723
+ /* @__PURE__ */ jsxs(
1724
+ "div",
1725
+ {
1726
+ "aria-busy": loading || void 0,
1727
+ className: "relative overflow-hidden rounded-lg border bg-card",
1728
+ children: [
1729
+ loading && rows.length > 0 && /* @__PURE__ */ jsxs(
1730
+ "div",
1731
+ {
1732
+ role: "status",
1733
+ "aria-live": "polite",
1734
+ className: "absolute inset-0 z-40 flex items-center justify-center rounded-lg bg-card/80",
1735
+ children: [
1736
+ /* @__PURE__ */ jsx(Spinner, { "aria-hidden": "true", className: "text-foreground" }),
1737
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("data.table.loading") })
1738
+ ]
1739
+ }
1740
+ ),
1741
+ cardsActive ? /* @__PURE__ */ jsxs("div", { "data-slot": "data-table-card-region", children: [
1742
+ cardsCaption,
1743
+ renderCardSortBar(),
1744
+ renderCardList(false)
1745
+ ] }) : /* @__PURE__ */ jsx(
1746
+ "div",
1747
+ {
1748
+ ref: plainScrollRef,
1749
+ "data-slot": "data-table-scroll-region",
1750
+ tabIndex: scrollOverflows ? 0 : void 0,
1751
+ role: scrollOverflows ? "group" : void 0,
1752
+ "aria-label": scrollOverflows ? t("data.table.scrollRegion") : void 0,
1753
+ onScroll: updateScrollAffordance,
1754
+ className: "overflow-auto rounded-lg focus-ring-inset",
1755
+ style: hasLeftPinned || hasRightPinned ? pinnedScrollPadding : void 0,
1756
+ children: /* @__PURE__ */ jsxs("table", { "aria-busy": loading || void 0, className: "w-full caption-bottom text-body", children: [
1757
+ captionElement,
1758
+ renderThead(false),
1759
+ renderTbodyNormal()
1760
+ ] })
1761
+ }
1762
+ ),
1763
+ !cardsActive && canScrollLeft && !hasLeftPinned && /* @__PURE__ */ jsx(
1764
+ "div",
1765
+ {
1766
+ "aria-hidden": "true",
1767
+ "data-slot": "data-table-scroll-fade-left",
1768
+ className: "pointer-events-none absolute inset-y-0 left-0 z-10 w-8 rounded-lg bg-gradient-to-r from-card to-transparent"
1769
+ }
1770
+ ),
1771
+ !cardsActive && canScrollRight && !hasRightPinned && /* @__PURE__ */ jsx(
1772
+ "div",
1773
+ {
1774
+ "aria-hidden": "true",
1775
+ "data-slot": "data-table-scroll-fade-right",
1776
+ className: "pointer-events-none absolute inset-y-0 right-0 z-10 w-8 rounded-lg bg-gradient-to-l from-card to-transparent"
1777
+ }
1778
+ )
1779
+ ]
1780
+ }
1781
+ ),
1782
+ renderPagination()
1783
+ ] });
1784
+ if (!rowReorderActive) return nonVirtualizedContent;
1785
+ return /* @__PURE__ */ jsxs(
1786
+ DndContext,
1787
+ {
1788
+ sensors: reorderSensors,
1789
+ collisionDetection: closestCenter,
1790
+ onDragStart: handleRowDragStart,
1791
+ onDragOver: handleRowDragOver,
1792
+ onDragEnd: handleRowDragEnd,
1793
+ onDragCancel: handleRowDragCancel,
1794
+ accessibility: {
1795
+ announcements: silentDragAnnouncements,
1796
+ // #98: dnd-kit's own hidden keyboard-instructions node is hardcoded
1797
+ // English (`defaultScreenReaderInstructions`) unless overridden here.
1798
+ screenReaderInstructions: { draggable: t("data.table.reorderInstructions") }
1799
+ },
1800
+ children: [
1801
+ nonVirtualizedContent,
1802
+ /* @__PURE__ */ jsx(
1803
+ "div",
1804
+ {
1805
+ role: "status",
1806
+ "aria-live": "polite",
1807
+ "aria-atomic": "true",
1808
+ "data-slot": "data-table-reorder-live-region",
1809
+ className: "sr-only",
1810
+ children: reorderLiveMessage
1811
+ }
1812
+ )
1813
+ ]
1814
+ }
1815
+ );
1816
+ }
1817
+ var DataTableWithRef = forwardRef(DataTableInner);
1818
+
1819
+ export {
1820
+ createSelectionColumn,
1821
+ DataTableWithRef
1822
+ };
1823
+ //# sourceMappingURL=chunk-K2GHSRCP.js.map