@elabs-ai/components-data 4.0.0 → 4.2.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/README.md +7 -8
- package/dist/index.d.ts +199 -5
- package/dist/index.js +764 -166
- package/dist/index.js.map +1 -1
- package/package.json +10 -7
- package/src/__contract__/filter-chip.contract.test.tsx +49 -0
- package/src/column-picker/column-picker.tsx +6 -3
- package/src/data-table/data-table.stories.tsx +496 -2
- package/src/data-table/data-table.test.tsx +1431 -17
- package/src/data-table/data-table.tsx +1284 -39
- package/src/data-table/index.ts +10 -0
- package/src/facet-filter/facet-filter.stories.tsx +4 -1
- package/src/facet-filter/facet-filter.test.tsx +3 -3
- package/src/facet-filter/facet-filter.tsx +5 -1
- package/src/filter-bar/filter-chip.stories.tsx +144 -0
- package/src/filter-bar/filter-chip.test.tsx +137 -0
- package/src/filter-bar/filter-chip.tsx +92 -0
- package/src/filter-bar/index.ts +1 -0
- package/src/index.ts +10 -1
- package/src/search-input/search-input.stories.tsx +3 -0
- package/src/search-input/search-input.test.tsx +35 -0
- package/src/search-input/search-input.tsx +45 -20
- package/src/templates-data-app.stories.tsx +6 -4
- package/src/to-csv.test.ts +13 -0
- package/src/to-csv.ts +7 -30
package/dist/index.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
// src/data-table/data-table.tsx
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
forwardRef,
|
|
6
|
+
useCallback,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef,
|
|
10
|
+
useState
|
|
11
|
+
} from "react";
|
|
5
12
|
import {
|
|
6
13
|
flexRender,
|
|
7
14
|
getCoreRowModel,
|
|
@@ -11,10 +18,30 @@ import {
|
|
|
11
18
|
useReactTable
|
|
12
19
|
} from "@tanstack/react-table";
|
|
13
20
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
14
|
-
import {
|
|
15
|
-
|
|
21
|
+
import {
|
|
22
|
+
DndContext,
|
|
23
|
+
KeyboardSensor,
|
|
24
|
+
PointerSensor,
|
|
25
|
+
closestCenter,
|
|
26
|
+
useSensor,
|
|
27
|
+
useSensors
|
|
28
|
+
} from "@dnd-kit/core";
|
|
29
|
+
import {
|
|
30
|
+
SortableContext,
|
|
31
|
+
sortableKeyboardCoordinates,
|
|
32
|
+
useSortable,
|
|
33
|
+
verticalListSortingStrategy
|
|
34
|
+
} from "@dnd-kit/sortable";
|
|
35
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
36
|
+
import { ArrowDown, ArrowUp, ArrowUpDown, GripVertical } from "lucide-react";
|
|
37
|
+
import { Button, Checkbox, Skeleton, Spinner, useLocale } from "@elabs-ai/components-ui";
|
|
16
38
|
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
17
39
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
40
|
+
function numericColumnClasses(meta) {
|
|
41
|
+
if (!meta?.numeric && !meta?.align) return void 0;
|
|
42
|
+
const alignClass = meta?.align === "start" ? "text-start" : meta?.align === "center" ? "text-center" : meta?.align === "end" ? "text-end" : meta?.numeric ? "text-end" : void 0;
|
|
43
|
+
return cn(alignClass, meta?.numeric && "tabular-nums");
|
|
44
|
+
}
|
|
18
45
|
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"]';
|
|
19
46
|
function isInteractiveEventTarget(target) {
|
|
20
47
|
return target instanceof Element && target.closest(ROW_CLICK_GUARD_SELECTOR) !== null;
|
|
@@ -24,6 +51,7 @@ function isActiveTextSelection() {
|
|
|
24
51
|
return window.getSelection()?.type === "Range";
|
|
25
52
|
}
|
|
26
53
|
var PINNED_SEAM_CLASS = "after:pointer-events-none after:absolute after:inset-y-0 after:w-px after:bg-border-strong after:content-['']";
|
|
54
|
+
var COLUMN_DIVIDER_CLASS = "border-e border-rule last:border-e-0";
|
|
27
55
|
function unsizedColumnIds(defs) {
|
|
28
56
|
const out = /* @__PURE__ */ new Set();
|
|
29
57
|
const walk = (list) => {
|
|
@@ -42,6 +70,78 @@ function unsizedColumnIds(defs) {
|
|
|
42
70
|
walk(defs);
|
|
43
71
|
return out;
|
|
44
72
|
}
|
|
73
|
+
function resizeWidthStyle(size) {
|
|
74
|
+
return { width: size, minWidth: size, maxWidth: size };
|
|
75
|
+
}
|
|
76
|
+
function firstDataCellValue(row) {
|
|
77
|
+
for (const cell of row.getVisibleCells()) {
|
|
78
|
+
if (!cell.column.accessorFn) continue;
|
|
79
|
+
const value = cell.getValue();
|
|
80
|
+
if (typeof value === "string" && value.trim() !== "") return value;
|
|
81
|
+
if (typeof value === "number") return String(value);
|
|
82
|
+
}
|
|
83
|
+
return void 0;
|
|
84
|
+
}
|
|
85
|
+
function SelectAllHeaderCell({ table }) {
|
|
86
|
+
const { t } = useLocale();
|
|
87
|
+
const allSelected = table.getIsAllPageRowsSelected();
|
|
88
|
+
const someSelected = table.getIsSomePageRowsSelected();
|
|
89
|
+
return /* @__PURE__ */ jsx(
|
|
90
|
+
Checkbox,
|
|
91
|
+
{
|
|
92
|
+
"data-slot": "data-table-select-all",
|
|
93
|
+
checked: allSelected ? true : someSelected ? "indeterminate" : false,
|
|
94
|
+
onCheckedChange: (checked) => table.toggleAllPageRowsSelected(checked === true),
|
|
95
|
+
"aria-label": t("data.table.selectAllRows")
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
function SelectRowCell({ row }) {
|
|
100
|
+
const { t } = useLocale();
|
|
101
|
+
const name = firstDataCellValue(row);
|
|
102
|
+
return /* @__PURE__ */ jsx(
|
|
103
|
+
Checkbox,
|
|
104
|
+
{
|
|
105
|
+
"data-slot": "data-table-select-cell",
|
|
106
|
+
checked: row.getIsSelected(),
|
|
107
|
+
disabled: !row.getCanSelect(),
|
|
108
|
+
onCheckedChange: (checked) => row.toggleSelected(checked === true),
|
|
109
|
+
"aria-label": name ? t("data.table.selectRowNamed", { name }) : t("data.table.selectRow")
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
function createSelectionColumn() {
|
|
114
|
+
return {
|
|
115
|
+
id: "select",
|
|
116
|
+
size: 40,
|
|
117
|
+
enableSorting: false,
|
|
118
|
+
enableHiding: false,
|
|
119
|
+
header: ({ table }) => (
|
|
120
|
+
// #11 C1: `toggleAllPageRowsSelected` wipes-then-sets on every row when
|
|
121
|
+
// `enableMultiRowSelection` is off (TanStack's `mutateRowIsSelected`), so
|
|
122
|
+
// a select-all header under single-select leaves only the LAST row
|
|
123
|
+
// selected and pins the header at indeterminate forever. Suppress it.
|
|
124
|
+
table.options.enableMultiRowSelection === false ? null : /* @__PURE__ */ jsx(SelectAllHeaderCell, { table })
|
|
125
|
+
),
|
|
126
|
+
cell: ({ row }) => /* @__PURE__ */ jsx(SelectRowCell, { row })
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function SortableDataRow({
|
|
130
|
+
id,
|
|
131
|
+
disabled,
|
|
132
|
+
attributesOverride,
|
|
133
|
+
children
|
|
134
|
+
}) {
|
|
135
|
+
const { attributes, listeners, setNodeRef, setActivatorNodeRef, transform, isDragging } = useSortable({ id, disabled, transition: null, attributes: attributesOverride });
|
|
136
|
+
return /* @__PURE__ */ jsx(Fragment, { children: children({
|
|
137
|
+
setNodeRef,
|
|
138
|
+
setActivatorNodeRef,
|
|
139
|
+
attributes,
|
|
140
|
+
listeners,
|
|
141
|
+
isDragging,
|
|
142
|
+
style: { transform: CSS.Transform.toString(transform) }
|
|
143
|
+
}) });
|
|
144
|
+
}
|
|
45
145
|
function DataTableInner({
|
|
46
146
|
columns,
|
|
47
147
|
data,
|
|
@@ -63,6 +163,15 @@ function DataTableInner({
|
|
|
63
163
|
onPaginationChange: onPaginationChangeProp,
|
|
64
164
|
columnPinning: columnPinningProp,
|
|
65
165
|
onColumnPinningChange: onColumnPinningChangeProp,
|
|
166
|
+
enableColumnResizing = false,
|
|
167
|
+
columnResizeMode = "onChange",
|
|
168
|
+
columnSizing: columnSizingProp,
|
|
169
|
+
onColumnSizingChange: onColumnSizingChangeProp,
|
|
170
|
+
rowSelection: rowSelectionProp,
|
|
171
|
+
onRowSelectionChange: onRowSelectionChangeProp,
|
|
172
|
+
enableRowSelection,
|
|
173
|
+
enableMultiRowSelection,
|
|
174
|
+
getRowId,
|
|
66
175
|
// Saved views rehydration
|
|
67
176
|
initialView,
|
|
68
177
|
// Server-side model
|
|
@@ -81,6 +190,11 @@ function DataTableInner({
|
|
|
81
190
|
overscan = 8,
|
|
82
191
|
maxBodyHeight = "32rem",
|
|
83
192
|
zebra = true,
|
|
193
|
+
columnDividers = false,
|
|
194
|
+
// Row drag-reorder (#13)
|
|
195
|
+
enableRowReorder = false,
|
|
196
|
+
onRowReorder,
|
|
197
|
+
rowReorderHandle = "cell",
|
|
84
198
|
onRowClick,
|
|
85
199
|
rowActionLabel,
|
|
86
200
|
rowClassName,
|
|
@@ -89,13 +203,15 @@ function DataTableInner({
|
|
|
89
203
|
className,
|
|
90
204
|
...rest
|
|
91
205
|
}, ref) {
|
|
92
|
-
const { t } = useLocale();
|
|
206
|
+
const { t, dir, formatNumber } = useLocale();
|
|
93
207
|
const isSortingControlled = sortingProp !== void 0;
|
|
94
208
|
const isColumnVisibilityControlled = columnVisibilityProp !== void 0;
|
|
95
209
|
const isColumnFiltersControlled = columnFiltersProp !== void 0;
|
|
96
210
|
const isPaginationControlled = paginationProp !== void 0;
|
|
97
211
|
const isFilterControlled = globalFilterProp !== void 0;
|
|
98
212
|
const isColumnPinningControlled = columnPinningProp !== void 0;
|
|
213
|
+
const isColumnSizingControlled = columnSizingProp !== void 0;
|
|
214
|
+
const isRowSelectionControlled = rowSelectionProp !== void 0;
|
|
99
215
|
const [internalSorting, setInternalSorting] = useState(
|
|
100
216
|
() => initialView?.sorting ?? []
|
|
101
217
|
);
|
|
@@ -117,12 +233,20 @@ function DataTableInner({
|
|
|
117
233
|
const [internalColumnPinning, setInternalColumnPinning] = useState(
|
|
118
234
|
() => initialView?.columnPinning ?? { left: [], right: [] }
|
|
119
235
|
);
|
|
236
|
+
const [internalColumnSizing, setInternalColumnSizing] = useState(
|
|
237
|
+
() => initialView?.columnSizing ?? {}
|
|
238
|
+
);
|
|
239
|
+
const [internalRowSelection, setInternalRowSelection] = useState(
|
|
240
|
+
() => initialView?.rowSelection ?? {}
|
|
241
|
+
);
|
|
120
242
|
const sorting = isSortingControlled ? sortingProp : internalSorting;
|
|
121
243
|
const columnVisibility = isColumnVisibilityControlled ? columnVisibilityProp : internalColumnVisibility;
|
|
122
244
|
const columnFilters = isColumnFiltersControlled ? columnFiltersProp : internalColumnFilters;
|
|
123
245
|
const pagination = isPaginationControlled ? paginationProp : internalPagination;
|
|
124
246
|
const globalFilter = isFilterControlled ? globalFilterProp : internalGlobalFilter;
|
|
125
247
|
const columnPinning = isColumnPinningControlled ? columnPinningProp : internalColumnPinning;
|
|
248
|
+
const columnSizing = isColumnSizingControlled ? columnSizingProp : internalColumnSizing;
|
|
249
|
+
const rowSelection = isRowSelectionControlled ? rowSelectionProp : internalRowSelection;
|
|
126
250
|
const sortingRef = useRef(sorting);
|
|
127
251
|
sortingRef.current = sorting;
|
|
128
252
|
const columnFiltersRef = useRef(columnFilters);
|
|
@@ -135,6 +259,10 @@ function DataTableInner({
|
|
|
135
259
|
columnVisibilityRef.current = columnVisibility;
|
|
136
260
|
const columnPinningRef = useRef(columnPinning);
|
|
137
261
|
columnPinningRef.current = columnPinning;
|
|
262
|
+
const columnSizingRef = useRef(columnSizing);
|
|
263
|
+
columnSizingRef.current = columnSizing;
|
|
264
|
+
const rowSelectionRef = useRef(rowSelection);
|
|
265
|
+
rowSelectionRef.current = rowSelection;
|
|
138
266
|
const warnedMissingRowCountRef = useRef(false);
|
|
139
267
|
useEffect(() => {
|
|
140
268
|
if (process.env.NODE_ENV !== "production" && manualPagination && rowCount === void 0 && pageCount === void 0 && !warnedMissingRowCountRef.current) {
|
|
@@ -144,6 +272,54 @@ function DataTableInner({
|
|
|
144
272
|
);
|
|
145
273
|
}
|
|
146
274
|
}, [manualPagination, rowCount, pageCount]);
|
|
275
|
+
const warnedManualSelectionRef = useRef(false);
|
|
276
|
+
useEffect(() => {
|
|
277
|
+
if (process.env.NODE_ENV !== "production" && manualPagination && getRowId === void 0 && (isRowSelectionControlled || onRowSelectionChangeProp !== void 0) && !warnedManualSelectionRef.current) {
|
|
278
|
+
warnedManualSelectionRef.current = true;
|
|
279
|
+
console.warn(
|
|
280
|
+
'[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.'
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}, [manualPagination, getRowId, isRowSelectionControlled, onRowSelectionChangeProp]);
|
|
284
|
+
const warnedReorderSortingRef = useRef(false);
|
|
285
|
+
useEffect(() => {
|
|
286
|
+
if (process.env.NODE_ENV !== "production" && enableRowReorder && sorting.length > 0 && !warnedReorderSortingRef.current) {
|
|
287
|
+
warnedReorderSortingRef.current = true;
|
|
288
|
+
console.warn(
|
|
289
|
+
"[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."
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
}, [enableRowReorder, sorting.length]);
|
|
293
|
+
const warnedReorderVirtualizedRef = useRef(false);
|
|
294
|
+
useEffect(() => {
|
|
295
|
+
if (process.env.NODE_ENV !== "production" && enableRowReorder && enableRowVirtualization && !warnedReorderVirtualizedRef.current) {
|
|
296
|
+
warnedReorderVirtualizedRef.current = true;
|
|
297
|
+
console.warn(
|
|
298
|
+
"[DataTable] `enableRowReorder` has no effect while `enableRowVirtualization` is set \u2014 the two are mutually exclusive. Virtualization wins; row reorder is disabled."
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}, [enableRowReorder, enableRowVirtualization]);
|
|
302
|
+
const rowReorderActive = enableRowReorder && !enableRowVirtualization;
|
|
303
|
+
const hasGripColumn = rowReorderActive && rowReorderHandle === "cell";
|
|
304
|
+
const reorderSensors = useSensors(
|
|
305
|
+
useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
|
|
306
|
+
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
|
|
307
|
+
);
|
|
308
|
+
const reorderIdentityMapRef = useRef(/* @__PURE__ */ new WeakMap());
|
|
309
|
+
const reorderIdentityCounterRef = useRef(0);
|
|
310
|
+
const reorderRepeatedPositions = useMemo(() => {
|
|
311
|
+
const repeats = /* @__PURE__ */ new Set();
|
|
312
|
+
if (!rowReorderActive) return repeats;
|
|
313
|
+
const seen = /* @__PURE__ */ new Set();
|
|
314
|
+
data.forEach((record, index) => {
|
|
315
|
+
if (record === null || typeof record !== "object") return;
|
|
316
|
+
if (seen.has(record)) repeats.add(index);
|
|
317
|
+
else seen.add(record);
|
|
318
|
+
});
|
|
319
|
+
return repeats;
|
|
320
|
+
}, [data, rowReorderActive]);
|
|
321
|
+
const [reorderLiveMessage, setReorderLiveMessage] = useState("");
|
|
322
|
+
const reorderLastAnnouncedPositionRef = useRef(null);
|
|
147
323
|
function fireServerChange(overrides = {}) {
|
|
148
324
|
if (!onServerChange) return;
|
|
149
325
|
onServerChange({
|
|
@@ -172,13 +348,28 @@ function DataTableInner({
|
|
|
172
348
|
function resolveColumnPinning(updater) {
|
|
173
349
|
return typeof updater === "function" ? updater(columnPinningRef.current) : updater;
|
|
174
350
|
}
|
|
351
|
+
function resolveColumnSizing(updater) {
|
|
352
|
+
return typeof updater === "function" ? updater(columnSizingRef.current) : updater;
|
|
353
|
+
}
|
|
354
|
+
function resolveRowSelection(updater) {
|
|
355
|
+
return typeof updater === "function" ? updater(rowSelectionRef.current) : updater;
|
|
356
|
+
}
|
|
175
357
|
const sortedRowModel = manualSorting ? {} : { getSortedRowModel: getSortedRowModel() };
|
|
176
358
|
const filteredRowModel = manualFiltering ? {} : { getFilteredRowModel: getFilteredRowModel() };
|
|
177
359
|
const paginationRowModel = enablePagination && !manualPagination ? { getPaginationRowModel: getPaginationRowModel() } : {};
|
|
178
360
|
const table = useReactTable({
|
|
179
361
|
data,
|
|
180
362
|
columns,
|
|
181
|
-
state: {
|
|
363
|
+
state: {
|
|
364
|
+
sorting,
|
|
365
|
+
columnVisibility,
|
|
366
|
+
columnFilters,
|
|
367
|
+
globalFilter,
|
|
368
|
+
pagination,
|
|
369
|
+
columnPinning,
|
|
370
|
+
columnSizing,
|
|
371
|
+
rowSelection
|
|
372
|
+
},
|
|
182
373
|
// Sorting
|
|
183
374
|
onSortingChange: (updater) => {
|
|
184
375
|
const next = resolveSorting(updater);
|
|
@@ -233,6 +424,38 @@ function DataTableInner({
|
|
|
233
424
|
if (!isColumnPinningControlled) setInternalColumnPinning(next);
|
|
234
425
|
onColumnPinningChangeProp?.(updater);
|
|
235
426
|
},
|
|
427
|
+
// Column resizing (#12) — a LAYOUT slice, like column pinning: a column's
|
|
428
|
+
// width changes nothing the server would need to re-query, so this never
|
|
429
|
+
// fires onServerChange either. Routed through by BOTH the pointer path
|
|
430
|
+
// (TanStack's own `header.getResizeHandler()`, wired below) and the
|
|
431
|
+
// keyboard path (`handleResizeKeyDown`, via `table.setColumnSizing`) so
|
|
432
|
+
// the two input modes can never diverge in controlled/uncontrolled
|
|
433
|
+
// behaviour.
|
|
434
|
+
columnResizeMode,
|
|
435
|
+
// RTL fix (#12 review, P1): TanStack's pointer-drag math hardcodes LTR
|
|
436
|
+
// unless told otherwise — `deltaDirection = columnResizeDirection ===
|
|
437
|
+
// 'rtl' ? -1 : 1` internally — so under `dir="rtl"` (the resize handle's
|
|
438
|
+
// own edge already flips via `end-0`, see the `useLocale()` call above)
|
|
439
|
+
// dragging would otherwise move the column's width opposite the visible
|
|
440
|
+
// boundary. `handleResizeKeyDown` below mirrors this for the keyboard path.
|
|
441
|
+
columnResizeDirection: dir,
|
|
442
|
+
enableColumnResizing,
|
|
443
|
+
onColumnSizingChange: (updater) => {
|
|
444
|
+
const next = resolveColumnSizing(updater);
|
|
445
|
+
if (!isColumnSizingControlled) setInternalColumnSizing(next);
|
|
446
|
+
onColumnSizingChangeProp?.(updater);
|
|
447
|
+
},
|
|
448
|
+
// Row selection (#11) — also a LAYOUT/UI slice, so it never fires
|
|
449
|
+
// onServerChange: which rows are checked changes nothing the server
|
|
450
|
+
// would need to re-query.
|
|
451
|
+
onRowSelectionChange: (updater) => {
|
|
452
|
+
const next = resolveRowSelection(updater);
|
|
453
|
+
if (!isRowSelectionControlled) setInternalRowSelection(next);
|
|
454
|
+
onRowSelectionChangeProp?.(updater);
|
|
455
|
+
},
|
|
456
|
+
enableRowSelection,
|
|
457
|
+
enableMultiRowSelection,
|
|
458
|
+
getRowId,
|
|
236
459
|
getCoreRowModel: getCoreRowModel(),
|
|
237
460
|
...sortedRowModel,
|
|
238
461
|
...filteredRowModel,
|
|
@@ -251,6 +474,83 @@ function DataTableInner({
|
|
|
251
474
|
const colCount = table.getVisibleLeafColumns().length;
|
|
252
475
|
const headerRowCount = table.getHeaderGroups().length;
|
|
253
476
|
const ariaRowCount = (rowCount ?? rows.length) + headerRowCount;
|
|
477
|
+
function reorderRowName(id) {
|
|
478
|
+
const row = rows.find((r) => getReorderRowId(r) === id);
|
|
479
|
+
return row ? rowActionName(row) : id;
|
|
480
|
+
}
|
|
481
|
+
function reorderPosition(id) {
|
|
482
|
+
return rows.findIndex((r) => getReorderRowId(r) === id) + 1;
|
|
483
|
+
}
|
|
484
|
+
function getReorderRowId(row) {
|
|
485
|
+
if (getRowId) return row.id;
|
|
486
|
+
const original = row.original;
|
|
487
|
+
if (original !== null && typeof original === "object") {
|
|
488
|
+
const map = reorderIdentityMapRef.current;
|
|
489
|
+
let id = map.get(original);
|
|
490
|
+
if (id === void 0) {
|
|
491
|
+
id = `__reorder-${reorderIdentityCounterRef.current++}`;
|
|
492
|
+
map.set(original, id);
|
|
493
|
+
}
|
|
494
|
+
return reorderRepeatedPositions.has(row.index) ? `${id}__${row.index}` : id;
|
|
495
|
+
}
|
|
496
|
+
return row.id;
|
|
497
|
+
}
|
|
498
|
+
const silentDragAnnouncements = {
|
|
499
|
+
onDragStart: () => void 0,
|
|
500
|
+
onDragOver: () => void 0,
|
|
501
|
+
onDragEnd: () => void 0,
|
|
502
|
+
onDragCancel: () => void 0
|
|
503
|
+
};
|
|
504
|
+
function handleRowDragStart(event) {
|
|
505
|
+
const activeRowId = String(event.active.id);
|
|
506
|
+
reorderLastAnnouncedPositionRef.current = reorderPosition(activeRowId);
|
|
507
|
+
setReorderLiveMessage(t("data.table.reorderPickedUp", { name: reorderRowName(activeRowId) }));
|
|
508
|
+
}
|
|
509
|
+
function handleRowDragOver(event) {
|
|
510
|
+
const { active, over } = event;
|
|
511
|
+
if (!over) return;
|
|
512
|
+
const position = reorderPosition(String(over.id));
|
|
513
|
+
if (position === reorderLastAnnouncedPositionRef.current) return;
|
|
514
|
+
reorderLastAnnouncedPositionRef.current = position;
|
|
515
|
+
setReorderLiveMessage(
|
|
516
|
+
t("data.table.reorderMoved", {
|
|
517
|
+
name: reorderRowName(String(active.id)),
|
|
518
|
+
position,
|
|
519
|
+
total: rows.length
|
|
520
|
+
})
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
function handleRowDragCancel(event) {
|
|
524
|
+
const activeRowId = String(event.active.id);
|
|
525
|
+
setReorderLiveMessage(
|
|
526
|
+
t("data.table.reorderCancelled", {
|
|
527
|
+
name: reorderRowName(activeRowId),
|
|
528
|
+
position: reorderPosition(activeRowId),
|
|
529
|
+
total: rows.length
|
|
530
|
+
})
|
|
531
|
+
);
|
|
532
|
+
reorderLastAnnouncedPositionRef.current = null;
|
|
533
|
+
}
|
|
534
|
+
function handleRowDragEnd(event) {
|
|
535
|
+
const { active, over } = event;
|
|
536
|
+
const activeRowId = String(active.id);
|
|
537
|
+
setReorderLiveMessage(
|
|
538
|
+
t("data.table.reorderDropped", {
|
|
539
|
+
name: reorderRowName(activeRowId),
|
|
540
|
+
position: reorderPosition(String(over ? over.id : active.id)),
|
|
541
|
+
total: rows.length
|
|
542
|
+
})
|
|
543
|
+
);
|
|
544
|
+
reorderLastAnnouncedPositionRef.current = null;
|
|
545
|
+
if (!over || active.id === over.id) return;
|
|
546
|
+
const movedRow = rows.find((r) => getReorderRowId(r) === activeRowId);
|
|
547
|
+
const targetRow = rows.find((r) => getReorderRowId(r) === String(over.id));
|
|
548
|
+
if (!movedRow || !targetRow) return;
|
|
549
|
+
const from = movedRow.index;
|
|
550
|
+
const to = targetRow.index;
|
|
551
|
+
if (from < 0 || from >= data.length || to < 0 || to >= data.length) return;
|
|
552
|
+
onRowReorder?.(from, to, movedRow.original);
|
|
553
|
+
}
|
|
254
554
|
const hasLeftPinned = (columnPinning.left?.length ?? 0) > 0;
|
|
255
555
|
const hasRightPinned = (columnPinning.right?.length ?? 0) > 0;
|
|
256
556
|
const pinnedScrollPadding = {
|
|
@@ -298,12 +598,33 @@ function DataTableInner({
|
|
|
298
598
|
// in Chromium on `Data/DataTable → PinnedColumns`: with `border-e` the
|
|
299
599
|
// seam pixel read `143,143,143` (light `--border-strong`) at
|
|
300
600
|
// scrollLeft 0 and `245,245,245` (the plain cell fill — i.e. GONE) once
|
|
301
|
-
// scrolled, in
|
|
601
|
+
// scrolled, in every theme and on both edges. So the one cue vanished
|
|
302
602
|
// exactly when the freeze was doing something. The `::after` lives in the
|
|
303
603
|
// sticky cell's own stacking context, so it moves with it.
|
|
304
604
|
edgeClass: pinned === "left" ? column.getIsLastColumn("left") ? PINNED_SEAM_CLASS + " after:end-0" : "" : column.getIsFirstColumn("right") ? PINNED_SEAM_CLASS + " after:start-0" : ""
|
|
305
605
|
};
|
|
306
606
|
}
|
|
607
|
+
const RESIZE_STEP = 10;
|
|
608
|
+
const RESIZE_UNBOUNDED_ARIA_MAX = 2e3;
|
|
609
|
+
function handleResizeKeyDown(event, column) {
|
|
610
|
+
let delta = 0;
|
|
611
|
+
if (event.key === "ArrowRight") delta = RESIZE_STEP;
|
|
612
|
+
else if (event.key === "ArrowLeft") delta = -RESIZE_STEP;
|
|
613
|
+
else return;
|
|
614
|
+
event.preventDefault();
|
|
615
|
+
if (dir === "rtl") delta = -delta;
|
|
616
|
+
const minSize = column.columnDef.minSize ?? 20;
|
|
617
|
+
const maxSize = column.columnDef.maxSize ?? Number.MAX_SAFE_INTEGER;
|
|
618
|
+
const nextSize = Math.min(maxSize, Math.max(minSize, column.getSize() + delta));
|
|
619
|
+
table.setColumnSizing((old) => ({ ...old, [column.id]: nextSize }));
|
|
620
|
+
}
|
|
621
|
+
function handleResizeDoubleClick(column) {
|
|
622
|
+
table.setColumnSizing((old) => {
|
|
623
|
+
if (!(column.id in old)) return old;
|
|
624
|
+
const { [column.id]: _removed, ...rest2 } = old;
|
|
625
|
+
return rest2;
|
|
626
|
+
});
|
|
627
|
+
}
|
|
307
628
|
const scrollRef = useRef(null);
|
|
308
629
|
const virtualizer = useVirtualizer({
|
|
309
630
|
count: enableRowVirtualization ? rows.length : 0,
|
|
@@ -356,101 +677,210 @@ function DataTableInner({
|
|
|
356
677
|
// delta: nothing else in the table sits between those rungs.
|
|
357
678
|
sticky ? "sticky top-0 z-20 bg-surface-muted" : "bg-surface-muted/60"
|
|
358
679
|
),
|
|
359
|
-
children: table.getHeaderGroups().map((headerGroup, groupIndex) => /* @__PURE__ */
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
"
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
680
|
+
children: table.getHeaderGroups().map((headerGroup, groupIndex) => /* @__PURE__ */ jsxs("tr", { "aria-rowindex": withRowIndex ? groupIndex + 1 : void 0, children: [
|
|
681
|
+
hasGripColumn && /* @__PURE__ */ jsx("th", { scope: "col", className: "h-10 w-10 px-3 align-middle", children: /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("data.table.reorderColumnHeader") }) }, "__reorder"),
|
|
682
|
+
headerGroup.headers.map((header) => {
|
|
683
|
+
const geometry = pinnedCellGeometry(header.column);
|
|
684
|
+
const canSort = header.column.getCanSort();
|
|
685
|
+
const sorted = header.column.getIsSorted();
|
|
686
|
+
const headerLabel = typeof header.column.columnDef.header === "string" ? header.column.columnDef.header : header.column.id;
|
|
687
|
+
const sortStateLabel = sorted === "asc" ? "ascending" : sorted === "desc" ? "descending" : "not sorted";
|
|
688
|
+
const SortIcon = sorted === "asc" ? ArrowUp : sorted === "desc" ? ArrowDown : ArrowUpDown;
|
|
689
|
+
const resizeStyle = enableColumnResizing ? resizeWidthStyle(header.getSize()) : void 0;
|
|
690
|
+
const canResize = enableColumnResizing && !header.isPlaceholder && header.column.getCanResize();
|
|
691
|
+
const resizeMax = header.column.columnDef.maxSize;
|
|
692
|
+
return /* @__PURE__ */ jsxs(
|
|
693
|
+
"th",
|
|
694
|
+
{
|
|
695
|
+
scope: "col",
|
|
696
|
+
"aria-sort": canSort ? sorted === "asc" ? "ascending" : sorted === "desc" ? "descending" : "none" : void 0,
|
|
697
|
+
"data-pinned": geometry?.pinned ?? void 0,
|
|
698
|
+
style: geometry?.style ?? resizeStyle,
|
|
699
|
+
className: cn(
|
|
700
|
+
// Same `px-3` the body `<td>` uses (below) — deliberately
|
|
701
|
+
// NOT split into `ps-3`/`pe-3` for a resize-handle
|
|
702
|
+
// override (round-1 briefly did this, see the round-2
|
|
703
|
+
// note on `numericColumnClasses`): the header's padding
|
|
704
|
+
// must stay byte-identical to the body's so an
|
|
705
|
+
// end-aligned numeric column's header lines up with its
|
|
706
|
+
// own values.
|
|
707
|
+
"h-10 px-3 text-start align-middle font-table-header text-muted-foreground",
|
|
708
|
+
// #69: a numeric column's `meta` overrides the default
|
|
709
|
+
// `text-start` — placed right after the base string so
|
|
710
|
+
// tailwind-merge lets it win over that default.
|
|
711
|
+
numericColumnClasses(header.column.columnDef.meta),
|
|
712
|
+
// `sticky`/pinned already establishes a positioning context
|
|
713
|
+
// for the resize handle's `absolute`; an unpinned resizable
|
|
714
|
+
// header needs its own.
|
|
715
|
+
!geometry && canResize && "relative",
|
|
716
|
+
// A pinned HEADER cell is the corner where both freezes meet,
|
|
717
|
+
// so it stacks above the sticky header row (z-20) which is
|
|
718
|
+
// above the pinned body cells (z-10). It needs an OPAQUE
|
|
719
|
+
// fill (scrolled header cells pass underneath it), and that
|
|
720
|
+
// fill has to composite to exactly what its unpinned
|
|
721
|
+
// neighbours show — same problem, same two-layer answer as
|
|
722
|
+
// `pinnedCellFillClass`:
|
|
723
|
+
// sticky branch → the row is already opaque `surface-muted`, so match it.
|
|
724
|
+
// plain branch → the row is `surface-muted/60` over the
|
|
725
|
+
// container's `card`, so paint `card` and
|
|
726
|
+
// re-apply the /60 wash on `::before`.
|
|
727
|
+
// Painting the plain branch's corner solid `surface-muted`
|
|
728
|
+
// read 4-5/255 darker than the header beside it in every
|
|
729
|
+
// theme (measured: 242 vs 247 light, 43 vs 40
|
|
730
|
+
// dark) — the same "floating pill"
|
|
731
|
+
// artefact #333 was filed about, moved into the header.
|
|
732
|
+
geometry && "sticky z-30",
|
|
733
|
+
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-['']"),
|
|
734
|
+
// Separate cn() argument on purpose: the seam is the sole
|
|
735
|
+
// structural cue between the frozen and scrolling blocks, so
|
|
736
|
+
// it must not read as a "boundary + fill in one class string"
|
|
737
|
+
// redundancy (separation:check).
|
|
738
|
+
geometry?.edgeClass,
|
|
739
|
+
columnDividers && !geometry && COLUMN_DIVIDER_CLASS
|
|
740
|
+
),
|
|
741
|
+
children: [
|
|
742
|
+
header.isPlaceholder ? null : canSort ? /* @__PURE__ */ jsxs(
|
|
743
|
+
"button",
|
|
744
|
+
{
|
|
745
|
+
type: "button",
|
|
746
|
+
onClick: header.column.getToggleSortingHandler(),
|
|
747
|
+
"aria-label": `Sort by ${headerLabel}, ${sortStateLabel}`,
|
|
748
|
+
className: "relative z-10 inline-flex items-center gap-1 rounded-sm transition-colors duration-fast ease-standard hover:text-foreground focus-ring",
|
|
749
|
+
children: [
|
|
750
|
+
flexRender(header.column.columnDef.header, header.getContext()),
|
|
751
|
+
/* @__PURE__ */ jsx(
|
|
752
|
+
SortIcon,
|
|
753
|
+
{
|
|
754
|
+
"aria-hidden": "true",
|
|
755
|
+
className: "size-3 shrink-0 transition-colors duration-fast ease-standard"
|
|
756
|
+
}
|
|
757
|
+
)
|
|
758
|
+
]
|
|
759
|
+
}
|
|
760
|
+
) : flexRender(header.column.columnDef.header, header.getContext()),
|
|
761
|
+
canResize && /* @__PURE__ */ jsx(
|
|
762
|
+
"div",
|
|
763
|
+
{
|
|
764
|
+
role: "separator",
|
|
765
|
+
"aria-orientation": "vertical",
|
|
766
|
+
"aria-valuenow": Math.round(header.getSize()),
|
|
767
|
+
"aria-valuemin": header.column.columnDef.minSize,
|
|
768
|
+
"aria-valuemax": resizeMax !== void 0 && resizeMax < Number.MAX_SAFE_INTEGER ? resizeMax : Math.max(header.getSize(), RESIZE_UNBOUNDED_ARIA_MAX),
|
|
769
|
+
"aria-valuetext": t("data.table.resizeColumnValue", {
|
|
770
|
+
count: Math.round(header.getSize()),
|
|
771
|
+
size: formatNumber(Math.round(header.getSize()))
|
|
772
|
+
}),
|
|
773
|
+
"aria-label": t("data.table.resizeColumn", { name: headerLabel }),
|
|
774
|
+
tabIndex: 0,
|
|
775
|
+
"data-slot": "data-table-resize-handle",
|
|
776
|
+
onMouseDown: header.getResizeHandler(),
|
|
777
|
+
onTouchStart: header.getResizeHandler(),
|
|
778
|
+
onKeyDown: (event) => handleResizeKeyDown(event, header.column),
|
|
779
|
+
onDoubleClick: () => handleResizeDoubleClick(header.column),
|
|
780
|
+
className: cn(
|
|
781
|
+
// #51: the hit box is a literal 24px (clamped to half
|
|
782
|
+
// the header cell so it can never overlap a neighbour,
|
|
783
|
+
// even at `minSize=20`) rather than the `w-2` Tailwind
|
|
784
|
+
// spacing-scale utility. `w-2` compiles to
|
|
785
|
+
// `calc(var(--spacing) * 2)`, and `--spacing` is what
|
|
786
|
+
// `data-density="compact"` rescales — so the old 8px
|
|
787
|
+
// hit box shrank further under compact density
|
|
788
|
+
// (~7.1px). A literal px value is density-independent
|
|
789
|
+
// by construction, which is the actual defect the
|
|
790
|
+
// maintainer's review corrected (NOT `--type-factor`,
|
|
791
|
+
// which this handle never used). Do not widen via
|
|
792
|
+
// overhang into the neighbouring cell instead — on the
|
|
793
|
+
// last column that lands inside the `overflow-auto`
|
|
794
|
+
// box (#330 false positive) and a pinned neighbour
|
|
795
|
+
// paints over/hit-tests away the extra area.
|
|
796
|
+
"absolute inset-y-0 end-0 w-[min(24px,50%)] cursor-col-resize touch-none select-none",
|
|
797
|
+
// #51: the focus ring moves to the `after:` pseudo-
|
|
798
|
+
// element (the drawn seam) rather than the box itself
|
|
799
|
+
// — the box is now a 24px hit target, and a 24px focus
|
|
800
|
+
// rectangle would replace the deliberately slim ring
|
|
801
|
+
// already reviewed/approved as the #12 a11y fix
|
|
802
|
+
// (da9b29e). `focus-visible:after:*` targets the
|
|
803
|
+
// pseudo-element the same way `hover:after:w-2` /
|
|
804
|
+
// `focus-visible:after:w-2` below already do.
|
|
805
|
+
"focus-visible:outline-none",
|
|
806
|
+
// a11y fix (#12 review, blocking): this handle is the
|
|
807
|
+
// SOLE boundary between two adjacent header cells once
|
|
808
|
+
// resizing is on — no fill/elevation change separates
|
|
809
|
+
// them otherwise — so per the border/border-strong
|
|
810
|
+
// decision test (styling-and-tokens.md) it needs a
|
|
811
|
+
// rung that clears WCAG 1.4.11's 3:1 on its OWN, in
|
|
812
|
+
// EVERY state, including rest (a control with no
|
|
813
|
+
// affordance until hover is unusable without a
|
|
814
|
+
// pointer). `border-strong` measures only 2.86-2.96:1
|
|
815
|
+
// against this `bg-surface-muted` header — that rung
|
|
816
|
+
// is guaranteed only vs `--card`/`--background`, not a
|
|
817
|
+
// same-tone surface, which is the exact trap the rule
|
|
818
|
+
// warns about. `muted-foreground` is guaranteed AA
|
|
819
|
+
// text contrast against `--surface-muted`
|
|
820
|
+
// (TEXT_SURFACES), so it clears the 3:1 non-text
|
|
821
|
+
// minimum with wide margin (measured ~5.3-6.4:1 in
|
|
822
|
+
// both themes, unaffected by density) and is already
|
|
823
|
+
// the header's own label color. A slim persistent
|
|
824
|
+
// `after:` seam (not just a hover reveal) gives the
|
|
825
|
+
// real resting boundary; hover/focus widen the drawn
|
|
826
|
+
// seam to 8px (`after:w-2`) using the same compliant
|
|
827
|
+
// color — a separate width from the 24px pointer hit
|
|
828
|
+
// box below (#51), which the seam does not fill.
|
|
829
|
+
// Dragging keeps the pre-existing full-fill
|
|
830
|
+
// `bg-primary` treatment — that is a drag AFFORDANCE,
|
|
831
|
+
// not a focus indicator, and it is redundant with the
|
|
832
|
+
// pointer capture, so it is out of scope here. The
|
|
833
|
+
// keyboard focus indicator on both branches is the
|
|
834
|
+
// shared compound one (#67), applied to the drawn seam
|
|
835
|
+
// via `focus-visible:after:focus-ring-static`: the
|
|
836
|
+
// element itself is a 24px transparent hit box, so
|
|
837
|
+
// ringing IT would ring nothing a user can see.
|
|
838
|
+
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"
|
|
839
|
+
)
|
|
840
|
+
}
|
|
841
|
+
)
|
|
842
|
+
]
|
|
843
|
+
},
|
|
844
|
+
header.id
|
|
845
|
+
);
|
|
846
|
+
})
|
|
847
|
+
] }, headerGroup.id))
|
|
422
848
|
}
|
|
423
849
|
);
|
|
424
850
|
}
|
|
425
851
|
function rowSeparationClass(rowIndex) {
|
|
426
852
|
if (!zebra) return "border-b border-border-strong last:border-b-0";
|
|
427
|
-
return
|
|
853
|
+
return cn(
|
|
854
|
+
"border-b-(length:--table-row-rule-width) border-border-strong last:border-b-0",
|
|
855
|
+
// Separate cn() argument: the stripe and the (theme-gated) rule are
|
|
856
|
+
// alternative cues, never both at once — see the jsdoc above.
|
|
857
|
+
rowIndex % 2 === 1 && "bg-table-stripe"
|
|
858
|
+
);
|
|
428
859
|
}
|
|
429
860
|
function pinnedCellFillClass(rowIndex) {
|
|
430
861
|
return cn(
|
|
431
862
|
"bg-card",
|
|
432
863
|
"before:pointer-events-none before:absolute before:inset-0 before:-z-10 before:content-['']",
|
|
433
|
-
zebra && rowIndex % 2 === 1 && "before:bg-
|
|
434
|
-
"group-hover/row:before:bg-
|
|
864
|
+
zebra && rowIndex % 2 === 1 && "before:bg-table-stripe",
|
|
865
|
+
"group-hover/row:before:bg-table-row-hover",
|
|
435
866
|
"group-data-[state=selected]/row:before:bg-accent"
|
|
436
867
|
);
|
|
437
868
|
}
|
|
438
869
|
function rowActionName(row) {
|
|
439
870
|
const explicit = rowActionLabel?.(row);
|
|
440
871
|
if (explicit) return explicit;
|
|
441
|
-
const
|
|
442
|
-
if (
|
|
443
|
-
if (typeof firstValue === "number") return String(firstValue);
|
|
872
|
+
const name = firstDataCellValue(row);
|
|
873
|
+
if (name !== void 0) return name;
|
|
444
874
|
return t("data.table.rowAction");
|
|
445
875
|
}
|
|
446
|
-
function renderRow(row, rowIndex, extras) {
|
|
876
|
+
function renderRow(row, rowIndex, extras, dragHandle) {
|
|
447
877
|
const clickable = Boolean(onRowClick);
|
|
448
878
|
function handleRowClick(event) {
|
|
449
879
|
if (isInteractiveEventTarget(event.target)) return;
|
|
450
880
|
if (isActiveTextSelection()) return;
|
|
451
881
|
onRowClick?.(row, event);
|
|
452
882
|
}
|
|
453
|
-
return /* @__PURE__ */
|
|
883
|
+
return /* @__PURE__ */ jsxs(
|
|
454
884
|
"tr",
|
|
455
885
|
{
|
|
456
886
|
"data-state": row.getIsSelected() ? "selected" : void 0,
|
|
@@ -461,7 +891,16 @@ function DataTableInner({
|
|
|
461
891
|
// (only movement is neutralized); the gated duration-fast/ease-standard
|
|
462
892
|
// pair already collapses toward ~0ms via --motion-factor when the user
|
|
463
893
|
// or OS asks for reduced motion, matching the header sort button.
|
|
464
|
-
"transition-colors duration-fast ease-standard hover:bg-
|
|
894
|
+
"transition-colors duration-fast ease-standard hover:bg-table-row-hover data-[state=selected]:bg-accent",
|
|
895
|
+
// #13: the dragged row's live `transform` (set inline via `extras.style`,
|
|
896
|
+
// see `SortableDataRow`) is what actually MOVES it — this class only
|
|
897
|
+
// makes that movement glide instead of snapping, through the gated
|
|
898
|
+
// duration/ease utilities (never a raw ms/ease value —
|
|
899
|
+
// quality-gates.md "Motion-tokened") with a reduced-motion
|
|
900
|
+
// neutralizer. Raising the dragged row's stacking + opacity is a
|
|
901
|
+
// colour/composite-only cue, so it isn't gated by the same rule.
|
|
902
|
+
dragHandle && "relative transition-transform duration-base ease-standard motion-reduce:transition-none",
|
|
903
|
+
dragHandle?.isDragging && "z-20 opacity-90 shadow-md",
|
|
465
904
|
// Named group (#333) so a PINNED cell can re-apply the row's hover /
|
|
466
905
|
// selected wash on top of its own opaque fill — only CSS knows the
|
|
467
906
|
// pointer is over a sibling cell. Purely a selector hook: `group/row`
|
|
@@ -474,58 +913,151 @@ function DataTableInner({
|
|
|
474
913
|
// `has-[[data-slot=…]:focus-visible]` pattern as InputGroup) so the
|
|
475
914
|
// ring paints on the ROW the user is about to activate, even though
|
|
476
915
|
// focus lives on the sr-only control inside it.
|
|
477
|
-
clickable && "cursor-pointer has-[[data-slot=data-table-row-action]:focus-visible]:
|
|
916
|
+
clickable && "cursor-pointer has-[[data-slot=data-table-row-action]:focus-visible]:focus-ring-static-inset",
|
|
478
917
|
rowClassName?.(row)
|
|
479
918
|
),
|
|
480
919
|
...extras,
|
|
481
|
-
children:
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
"td",
|
|
920
|
+
children: [
|
|
921
|
+
dragHandle?.activator && /* @__PURE__ */ jsx("td", { className: "w-10 px-3 py-2 align-middle", children: /* @__PURE__ */ jsx(
|
|
922
|
+
"button",
|
|
485
923
|
{
|
|
486
|
-
"
|
|
487
|
-
|
|
924
|
+
type: "button",
|
|
925
|
+
ref: dragHandle.activator.setActivatorNodeRef,
|
|
926
|
+
"data-slot": "data-table-row-drag-handle",
|
|
927
|
+
"aria-label": t("data.table.reorderHandle", { name: rowActionName(row) }),
|
|
488
928
|
className: cn(
|
|
489
|
-
"
|
|
490
|
-
|
|
491
|
-
// below the sticky header row (z-20) and the pinned corner (z-30).
|
|
492
|
-
geometry && "sticky z-10",
|
|
493
|
-
geometry && pinnedCellFillClass(rowIndex),
|
|
494
|
-
// Separate cn() argument — see pinnedCellGeometry's edgeClass.
|
|
495
|
-
geometry?.edgeClass
|
|
929
|
+
"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",
|
|
930
|
+
dragHandle.isDragging && "text-foreground"
|
|
496
931
|
),
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
932
|
+
...dragHandle.activator.attributes,
|
|
933
|
+
...dragHandle.activator.listeners,
|
|
934
|
+
children: /* @__PURE__ */ jsx(GripVertical, { "aria-hidden": "true", className: "size-4" })
|
|
935
|
+
}
|
|
936
|
+
) }),
|
|
937
|
+
row.getVisibleCells().map((cell, cellIndex) => {
|
|
938
|
+
const geometry = pinnedCellGeometry(cell.column);
|
|
939
|
+
const resizeStyle = enableColumnResizing ? resizeWidthStyle(cell.column.getSize()) : void 0;
|
|
940
|
+
return /* @__PURE__ */ jsxs(
|
|
941
|
+
"td",
|
|
942
|
+
{
|
|
943
|
+
"data-pinned": geometry?.pinned ?? void 0,
|
|
944
|
+
style: geometry?.style ?? resizeStyle,
|
|
945
|
+
className: cn(
|
|
946
|
+
"px-3 py-2 align-middle",
|
|
947
|
+
// #69: same numeric-column seam as the header — see
|
|
948
|
+
// `numericColumnClasses`.
|
|
949
|
+
numericColumnClasses(cell.column.columnDef.meta),
|
|
950
|
+
// z-10: above the normal (unpositioned) cells it scrolls over,
|
|
951
|
+
// below the sticky header row (z-20) and the pinned corner (z-30).
|
|
952
|
+
geometry && "sticky z-10",
|
|
953
|
+
geometry && pinnedCellFillClass(rowIndex),
|
|
954
|
+
// Separate cn() argument — see pinnedCellGeometry's edgeClass.
|
|
955
|
+
geometry?.edgeClass,
|
|
956
|
+
columnDividers && !geometry && COLUMN_DIVIDER_CLASS
|
|
507
957
|
),
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
958
|
+
children: [
|
|
959
|
+
clickable && cellIndex === 0 && /* @__PURE__ */ jsx(
|
|
960
|
+
"button",
|
|
961
|
+
{
|
|
962
|
+
type: "button",
|
|
963
|
+
"data-slot": "data-table-row-action",
|
|
964
|
+
className: "sr-only focus-visible:outline-none",
|
|
965
|
+
onClick: (event) => onRowClick?.(row, event),
|
|
966
|
+
children: rowActionName(row)
|
|
967
|
+
}
|
|
968
|
+
),
|
|
969
|
+
flexRender(cell.column.columnDef.cell, cell.getContext())
|
|
970
|
+
]
|
|
971
|
+
},
|
|
972
|
+
cell.id
|
|
973
|
+
);
|
|
974
|
+
})
|
|
975
|
+
]
|
|
514
976
|
},
|
|
515
977
|
row.id
|
|
516
978
|
);
|
|
517
979
|
}
|
|
518
980
|
function renderSkeletonBody(count) {
|
|
519
|
-
|
|
981
|
+
const visibleColumns = table.getVisibleLeafColumns();
|
|
982
|
+
return Array.from({ length: count }).map((_, i) => /* @__PURE__ */ jsxs("tr", { "aria-hidden": "true", className: rowSeparationClass(i), children: [
|
|
983
|
+
hasGripColumn && /* @__PURE__ */ jsx("td", { className: "w-10 px-3 py-2 align-middle", children: /* @__PURE__ */ jsx(Skeleton, { className: "size-4" }) }),
|
|
984
|
+
visibleColumns.map((column) => /* @__PURE__ */ jsx(
|
|
985
|
+
"td",
|
|
986
|
+
{
|
|
987
|
+
className: cn(
|
|
988
|
+
"px-3 py-2 align-middle",
|
|
989
|
+
numericColumnClasses(column.columnDef.meta),
|
|
990
|
+
columnDividers && COLUMN_DIVIDER_CLASS
|
|
991
|
+
),
|
|
992
|
+
children: /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" })
|
|
993
|
+
},
|
|
994
|
+
column.id
|
|
995
|
+
))
|
|
996
|
+
] }, `skeleton-${i}`));
|
|
520
997
|
}
|
|
521
998
|
function renderEmptyBody() {
|
|
522
|
-
return /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
|
|
999
|
+
return /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
|
|
1000
|
+
"td",
|
|
1001
|
+
{
|
|
1002
|
+
colSpan: colCount + (hasGripColumn ? 1 : 0),
|
|
1003
|
+
className: "h-24 px-3 text-center text-muted-foreground",
|
|
1004
|
+
children: emptyMessage
|
|
1005
|
+
}
|
|
1006
|
+
) });
|
|
523
1007
|
}
|
|
524
1008
|
function renderTbodyNormal() {
|
|
525
1009
|
if (showSkeletons) {
|
|
526
1010
|
return /* @__PURE__ */ jsx("tbody", { children: renderSkeletonBody(skeletonRowCount) });
|
|
527
1011
|
}
|
|
528
|
-
|
|
1012
|
+
if (showEmpty) {
|
|
1013
|
+
return /* @__PURE__ */ jsx("tbody", { children: renderEmptyBody() });
|
|
1014
|
+
}
|
|
1015
|
+
if (!rowReorderActive) {
|
|
1016
|
+
return /* @__PURE__ */ jsx("tbody", { children: rows.map((row, i) => renderRow(row, i)) });
|
|
1017
|
+
}
|
|
1018
|
+
return /* @__PURE__ */ jsx(
|
|
1019
|
+
SortableContext,
|
|
1020
|
+
{
|
|
1021
|
+
items: rows.map((r) => getReorderRowId(r)),
|
|
1022
|
+
strategy: verticalListSortingStrategy,
|
|
1023
|
+
children: /* @__PURE__ */ jsx("tbody", { children: rows.map((row, i) => /* @__PURE__ */ jsx(
|
|
1024
|
+
SortableDataRow,
|
|
1025
|
+
{
|
|
1026
|
+
id: getReorderRowId(row),
|
|
1027
|
+
attributesOverride: {
|
|
1028
|
+
// #98: dnd-kit's own `roleDescription: 'sortable'` default is
|
|
1029
|
+
// hardcoded English; override it with the localized value in
|
|
1030
|
+
// BOTH handle modes — `role` stays row-mode-only (see the
|
|
1031
|
+
// `attributesOverride` prop doc above).
|
|
1032
|
+
roleDescription: t("data.table.reorderRoleDescription"),
|
|
1033
|
+
...rowReorderHandle === "row" ? { role: "row" } : null
|
|
1034
|
+
},
|
|
1035
|
+
children: ({ setNodeRef, setActivatorNodeRef, attributes, listeners, isDragging, style }) => renderRow(
|
|
1036
|
+
row,
|
|
1037
|
+
i,
|
|
1038
|
+
{
|
|
1039
|
+
ref: setNodeRef,
|
|
1040
|
+
style,
|
|
1041
|
+
// `aria-pressed` is a `DraggableAttributes` field meant for a
|
|
1042
|
+
// real `<button>` activator; spread onto a `<tr role="row">`
|
|
1043
|
+
// (row-handle mode) it fails axe's `aria-allowed-attr` (that
|
|
1044
|
+
// ARIA state is not permitted on the `row` role), so strip it
|
|
1045
|
+
// here rather than exempt it downstream.
|
|
1046
|
+
...rowReorderHandle === "row" ? (() => {
|
|
1047
|
+
const { "aria-pressed": _ariaPressed, ...rowAttributes } = attributes;
|
|
1048
|
+
return { ...rowAttributes, ...listeners };
|
|
1049
|
+
})() : {}
|
|
1050
|
+
},
|
|
1051
|
+
{
|
|
1052
|
+
isDragging,
|
|
1053
|
+
activator: rowReorderHandle === "cell" ? { setActivatorNodeRef, attributes, listeners } : void 0
|
|
1054
|
+
}
|
|
1055
|
+
)
|
|
1056
|
+
},
|
|
1057
|
+
getReorderRowId(row)
|
|
1058
|
+
)) })
|
|
1059
|
+
}
|
|
1060
|
+
);
|
|
529
1061
|
}
|
|
530
1062
|
function renderTbodyVirtualized() {
|
|
531
1063
|
if (showSkeletons) {
|
|
@@ -592,9 +1124,10 @@ function DataTableInner({
|
|
|
592
1124
|
{
|
|
593
1125
|
ref: scrollRef,
|
|
594
1126
|
tabIndex: 0,
|
|
1127
|
+
role: "group",
|
|
595
1128
|
"aria-label": t("data.table.scrollRegion"),
|
|
596
1129
|
"aria-busy": loading || void 0,
|
|
597
|
-
className: "relative overflow-auto rounded-lg border bg-card focus-
|
|
1130
|
+
className: "relative overflow-auto rounded-lg border bg-card focus-ring",
|
|
598
1131
|
style: { maxHeight: maxBodyHeight, ...pinnedScrollPadding },
|
|
599
1132
|
children: [
|
|
600
1133
|
loading && rows.length > 0 && /* @__PURE__ */ jsxs(
|
|
@@ -605,7 +1138,7 @@ function DataTableInner({
|
|
|
605
1138
|
className: "absolute inset-0 z-40 flex items-center justify-center rounded-lg bg-card/80",
|
|
606
1139
|
children: [
|
|
607
1140
|
/* @__PURE__ */ jsx(Spinner, { "aria-hidden": "true", className: "text-foreground" }),
|
|
608
|
-
/* @__PURE__ */ jsx("span", { className: "sr-only", children: "
|
|
1141
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: t("data.table.loading") })
|
|
609
1142
|
]
|
|
610
1143
|
}
|
|
611
1144
|
),
|
|
@@ -627,7 +1160,7 @@ function DataTableInner({
|
|
|
627
1160
|
)
|
|
628
1161
|
] });
|
|
629
1162
|
}
|
|
630
|
-
|
|
1163
|
+
const nonVirtualizedContent = /* @__PURE__ */ jsxs("div", { ref, className: cn("space-y-3", className), ...rest, children: [
|
|
631
1164
|
toolbar ? toolbar(table) : null,
|
|
632
1165
|
/* @__PURE__ */ jsxs(
|
|
633
1166
|
"div",
|
|
@@ -643,7 +1176,7 @@ function DataTableInner({
|
|
|
643
1176
|
className: "absolute inset-0 z-40 flex items-center justify-center rounded-lg bg-card/80",
|
|
644
1177
|
children: [
|
|
645
1178
|
/* @__PURE__ */ jsx(Spinner, { "aria-hidden": "true", className: "text-foreground" }),
|
|
646
|
-
/* @__PURE__ */ jsx("span", { className: "sr-only", children: "
|
|
1179
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: t("data.table.loading") })
|
|
647
1180
|
]
|
|
648
1181
|
}
|
|
649
1182
|
),
|
|
@@ -653,9 +1186,10 @@ function DataTableInner({
|
|
|
653
1186
|
ref: plainScrollRef,
|
|
654
1187
|
"data-slot": "data-table-scroll-region",
|
|
655
1188
|
tabIndex: scrollOverflows ? 0 : void 0,
|
|
1189
|
+
role: scrollOverflows ? "group" : void 0,
|
|
656
1190
|
"aria-label": scrollOverflows ? t("data.table.scrollRegion") : void 0,
|
|
657
1191
|
onScroll: updateScrollAffordance,
|
|
658
|
-
className: "overflow-auto rounded-lg focus-
|
|
1192
|
+
className: "overflow-auto rounded-lg focus-ring-inset",
|
|
659
1193
|
style: hasLeftPinned || hasRightPinned ? pinnedScrollPadding : void 0,
|
|
660
1194
|
children: /* @__PURE__ */ jsxs("table", { "aria-busy": loading || void 0, className: "w-full caption-bottom text-body", children: [
|
|
661
1195
|
captionElement,
|
|
@@ -685,28 +1219,75 @@ function DataTableInner({
|
|
|
685
1219
|
),
|
|
686
1220
|
renderPagination()
|
|
687
1221
|
] });
|
|
1222
|
+
if (!rowReorderActive) return nonVirtualizedContent;
|
|
1223
|
+
return /* @__PURE__ */ jsxs(
|
|
1224
|
+
DndContext,
|
|
1225
|
+
{
|
|
1226
|
+
sensors: reorderSensors,
|
|
1227
|
+
collisionDetection: closestCenter,
|
|
1228
|
+
onDragStart: handleRowDragStart,
|
|
1229
|
+
onDragOver: handleRowDragOver,
|
|
1230
|
+
onDragEnd: handleRowDragEnd,
|
|
1231
|
+
onDragCancel: handleRowDragCancel,
|
|
1232
|
+
accessibility: {
|
|
1233
|
+
announcements: silentDragAnnouncements,
|
|
1234
|
+
// #98: dnd-kit's own hidden keyboard-instructions node is hardcoded
|
|
1235
|
+
// English (`defaultScreenReaderInstructions`) unless overridden here.
|
|
1236
|
+
screenReaderInstructions: { draggable: t("data.table.reorderInstructions") }
|
|
1237
|
+
},
|
|
1238
|
+
children: [
|
|
1239
|
+
nonVirtualizedContent,
|
|
1240
|
+
/* @__PURE__ */ jsx(
|
|
1241
|
+
"div",
|
|
1242
|
+
{
|
|
1243
|
+
role: "status",
|
|
1244
|
+
"aria-live": "polite",
|
|
1245
|
+
"aria-atomic": "true",
|
|
1246
|
+
"data-slot": "data-table-reorder-live-region",
|
|
1247
|
+
className: "sr-only",
|
|
1248
|
+
children: reorderLiveMessage
|
|
1249
|
+
}
|
|
1250
|
+
)
|
|
1251
|
+
]
|
|
1252
|
+
}
|
|
1253
|
+
);
|
|
688
1254
|
}
|
|
689
1255
|
var DataTableWithRef = forwardRef(DataTableInner);
|
|
690
1256
|
|
|
691
1257
|
// src/search-input/search-input.tsx
|
|
692
|
-
import { useId } from "react";
|
|
693
|
-
import { Input } from "@elabs-ai/components-ui";
|
|
1258
|
+
import { forwardRef as forwardRef2, useId, useRef as useRef2 } from "react";
|
|
1259
|
+
import { Input, useLocale as useLocale2 } from "@elabs-ai/components-ui";
|
|
694
1260
|
import { cn as cn2 } from "@elabs-ai/components-ui/lib/cn";
|
|
695
1261
|
import { SearchIcon } from "@elabs-ai/components-icons";
|
|
696
1262
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
697
|
-
|
|
1263
|
+
var SearchInput = forwardRef2(function SearchInput2({
|
|
698
1264
|
value,
|
|
699
1265
|
onValueChange,
|
|
700
|
-
label
|
|
701
|
-
placeholder
|
|
1266
|
+
label,
|
|
1267
|
+
placeholder,
|
|
702
1268
|
className,
|
|
703
1269
|
containerClassName,
|
|
704
1270
|
disabled,
|
|
1271
|
+
id: idProp,
|
|
705
1272
|
...props
|
|
706
|
-
}) {
|
|
707
|
-
const
|
|
1273
|
+
}, forwardedRef) {
|
|
1274
|
+
const { t } = useLocale2();
|
|
1275
|
+
const resolvedLabel = label ?? t("data.searchInput.label");
|
|
1276
|
+
const resolvedPlaceholder = placeholder ?? t("data.searchInput.placeholder");
|
|
1277
|
+
const generatedId = useId();
|
|
1278
|
+
const id = idProp ?? generatedId;
|
|
1279
|
+
const inputRef = useRef2(null);
|
|
1280
|
+
const setRefs = (node) => {
|
|
1281
|
+
inputRef.current = node;
|
|
1282
|
+
if (typeof forwardedRef === "function") forwardedRef(node);
|
|
1283
|
+
else if (forwardedRef) forwardedRef.current = node;
|
|
1284
|
+
};
|
|
1285
|
+
const handleClear = () => {
|
|
1286
|
+
onValueChange("");
|
|
1287
|
+
inputRef.current?.focus();
|
|
1288
|
+
};
|
|
708
1289
|
return /* @__PURE__ */ jsxs2("div", { className: cn2("relative w-full max-w-xs", containerClassName), children: [
|
|
709
|
-
/* @__PURE__ */ jsx2("label", { htmlFor: id, className: "sr-only", children:
|
|
1290
|
+
/* @__PURE__ */ jsx2("label", { htmlFor: id, className: "sr-only", children: resolvedLabel }),
|
|
710
1291
|
/* @__PURE__ */ jsx2(
|
|
711
1292
|
SearchIcon,
|
|
712
1293
|
{
|
|
@@ -717,10 +1298,11 @@ function SearchInput({
|
|
|
717
1298
|
/* @__PURE__ */ jsx2(
|
|
718
1299
|
Input,
|
|
719
1300
|
{
|
|
1301
|
+
ref: setRefs,
|
|
720
1302
|
id,
|
|
721
1303
|
value,
|
|
722
1304
|
onChange: (e) => onValueChange(e.target.value),
|
|
723
|
-
placeholder,
|
|
1305
|
+
placeholder: resolvedPlaceholder,
|
|
724
1306
|
disabled,
|
|
725
1307
|
className: cn2("ps-8", value && "pe-8", className),
|
|
726
1308
|
...props
|
|
@@ -730,9 +1312,9 @@ function SearchInput({
|
|
|
730
1312
|
"button",
|
|
731
1313
|
{
|
|
732
1314
|
type: "button",
|
|
733
|
-
onClick:
|
|
734
|
-
"aria-label": "
|
|
735
|
-
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-
|
|
1315
|
+
onClick: handleClear,
|
|
1316
|
+
"aria-label": t("data.searchInput.clear"),
|
|
1317
|
+
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-ring animate-in fade-in zoom-in-95 duration-fast ease-entrance",
|
|
736
1318
|
children: /* @__PURE__ */ jsx2(
|
|
737
1319
|
"svg",
|
|
738
1320
|
{
|
|
@@ -751,7 +1333,7 @@ function SearchInput({
|
|
|
751
1333
|
}
|
|
752
1334
|
) : null
|
|
753
1335
|
] });
|
|
754
|
-
}
|
|
1336
|
+
});
|
|
755
1337
|
|
|
756
1338
|
// src/filter-bar/filter-bar.tsx
|
|
757
1339
|
import "react";
|
|
@@ -764,8 +1346,31 @@ function FilterBar({ children, actions, className }) {
|
|
|
764
1346
|
] });
|
|
765
1347
|
}
|
|
766
1348
|
|
|
1349
|
+
// src/filter-bar/filter-chip.tsx
|
|
1350
|
+
import { forwardRef as forwardRef3 } from "react";
|
|
1351
|
+
import {
|
|
1352
|
+
FilterChip as BaseFilterChip,
|
|
1353
|
+
useLocale as useLocale3
|
|
1354
|
+
} from "@elabs-ai/components-ui";
|
|
1355
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
1356
|
+
var FilterChip = forwardRef3(function FilterChip2({ label, count, countLabel, ...props }, ref) {
|
|
1357
|
+
const { formatNumber } = useLocale3();
|
|
1358
|
+
const countText = count === void 0 ? void 0 : countLabel ? `${countLabel} ${formatNumber(count)}` : formatNumber(count);
|
|
1359
|
+
const { trailing: _ignoredTrailing, ...restProps } = props;
|
|
1360
|
+
return /* @__PURE__ */ jsx4(
|
|
1361
|
+
BaseFilterChip,
|
|
1362
|
+
{
|
|
1363
|
+
ref,
|
|
1364
|
+
"data-slot": "filter-chip",
|
|
1365
|
+
label,
|
|
1366
|
+
...restProps,
|
|
1367
|
+
trailing: countText
|
|
1368
|
+
}
|
|
1369
|
+
);
|
|
1370
|
+
});
|
|
1371
|
+
|
|
767
1372
|
// src/facet-filter/facet-filter.tsx
|
|
768
|
-
import { forwardRef as
|
|
1373
|
+
import { forwardRef as forwardRef4 } from "react";
|
|
769
1374
|
import {
|
|
770
1375
|
Badge,
|
|
771
1376
|
Button as Button2,
|
|
@@ -774,11 +1379,13 @@ import {
|
|
|
774
1379
|
DropdownMenuItem,
|
|
775
1380
|
DropdownMenuLabel,
|
|
776
1381
|
DropdownMenuSeparator,
|
|
777
|
-
DropdownMenuTrigger
|
|
1382
|
+
DropdownMenuTrigger,
|
|
1383
|
+
useLocale as useLocale4
|
|
778
1384
|
} from "@elabs-ai/components-ui";
|
|
779
1385
|
import { cn as cn4 } from "@elabs-ai/components-ui/lib/cn";
|
|
780
|
-
import { Fragment as Fragment2, jsx as
|
|
781
|
-
var FacetFilter =
|
|
1386
|
+
import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1387
|
+
var FacetFilter = forwardRef4(function FacetFilter2({ title, options, selected, onSelectedChange, className, ...props }, ref) {
|
|
1388
|
+
const { t } = useLocale4();
|
|
782
1389
|
const selectedSet = new Set(selected);
|
|
783
1390
|
const toggle = (value) => {
|
|
784
1391
|
const next = new Set(selectedSet);
|
|
@@ -787,9 +1394,9 @@ var FacetFilter = forwardRef2(function FacetFilter2({ title, options, selected,
|
|
|
787
1394
|
onSelectedChange([...next]);
|
|
788
1395
|
};
|
|
789
1396
|
return /* @__PURE__ */ jsxs4(DropdownMenu, { children: [
|
|
790
|
-
/* @__PURE__ */
|
|
1397
|
+
/* @__PURE__ */ jsx5(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs4(Button2, { ref, variant: "outline", className: cn4("border-dashed", className), ...props, children: [
|
|
791
1398
|
title,
|
|
792
|
-
selected.length > 0 ? /* @__PURE__ */
|
|
1399
|
+
selected.length > 0 ? /* @__PURE__ */ jsx5(
|
|
793
1400
|
Badge,
|
|
794
1401
|
{
|
|
795
1402
|
variant: "secondary",
|
|
@@ -799,7 +1406,7 @@ var FacetFilter = forwardRef2(function FacetFilter2({ title, options, selected,
|
|
|
799
1406
|
) : null
|
|
800
1407
|
] }) }),
|
|
801
1408
|
/* @__PURE__ */ jsxs4(DropdownMenuContent, { className: "min-w-[12rem]", children: [
|
|
802
|
-
/* @__PURE__ */
|
|
1409
|
+
/* @__PURE__ */ jsx5(DropdownMenuLabel, { children: title }),
|
|
803
1410
|
options.map((opt) => {
|
|
804
1411
|
const checked = selectedSet.has(opt.value);
|
|
805
1412
|
return /* @__PURE__ */ jsxs4(
|
|
@@ -810,7 +1417,7 @@ var FacetFilter = forwardRef2(function FacetFilter2({ title, options, selected,
|
|
|
810
1417
|
toggle(opt.value);
|
|
811
1418
|
},
|
|
812
1419
|
children: [
|
|
813
|
-
/* @__PURE__ */
|
|
1420
|
+
/* @__PURE__ */ jsx5(
|
|
814
1421
|
"span",
|
|
815
1422
|
{
|
|
816
1423
|
"aria-hidden": "true",
|
|
@@ -825,8 +1432,8 @@ var FacetFilter = forwardRef2(function FacetFilter2({ title, options, selected,
|
|
|
825
1432
|
);
|
|
826
1433
|
}),
|
|
827
1434
|
selected.length > 0 ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
|
|
828
|
-
/* @__PURE__ */
|
|
829
|
-
/* @__PURE__ */
|
|
1435
|
+
/* @__PURE__ */ jsx5(DropdownMenuSeparator, {}),
|
|
1436
|
+
/* @__PURE__ */ jsx5(DropdownMenuItem, { onSelect: () => onSelectedChange([]), children: t("data.facetFilter.clearFilters") })
|
|
830
1437
|
] }) : null
|
|
831
1438
|
] })
|
|
832
1439
|
] });
|
|
@@ -835,7 +1442,7 @@ FacetFilter.displayName = "FacetFilter";
|
|
|
835
1442
|
|
|
836
1443
|
// src/column-picker/column-picker.tsx
|
|
837
1444
|
import "@tanstack/react-table";
|
|
838
|
-
import { forwardRef as
|
|
1445
|
+
import { forwardRef as forwardRef5 } from "react";
|
|
839
1446
|
import {
|
|
840
1447
|
Button as Button3,
|
|
841
1448
|
DropdownMenu as DropdownMenu2,
|
|
@@ -843,17 +1450,20 @@ import {
|
|
|
843
1450
|
DropdownMenuItem as DropdownMenuItem2,
|
|
844
1451
|
DropdownMenuLabel as DropdownMenuLabel2,
|
|
845
1452
|
DropdownMenuSeparator as DropdownMenuSeparator2,
|
|
846
|
-
DropdownMenuTrigger as DropdownMenuTrigger2
|
|
1453
|
+
DropdownMenuTrigger as DropdownMenuTrigger2,
|
|
1454
|
+
useLocale as useLocale5
|
|
847
1455
|
} from "@elabs-ai/components-ui";
|
|
848
1456
|
import { cn as cn5 } from "@elabs-ai/components-ui/lib/cn";
|
|
849
|
-
import { jsx as
|
|
850
|
-
function ColumnPickerInner({ table, label
|
|
1457
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1458
|
+
function ColumnPickerInner({ table, label, className, ...props }, ref) {
|
|
1459
|
+
const { t } = useLocale5();
|
|
1460
|
+
const resolvedLabel = label ?? t("data.columnPicker.label");
|
|
851
1461
|
const columns = table.getAllColumns().filter((c) => c.getCanHide());
|
|
852
1462
|
return /* @__PURE__ */ jsxs5(DropdownMenu2, { children: [
|
|
853
|
-
/* @__PURE__ */
|
|
1463
|
+
/* @__PURE__ */ jsx6(DropdownMenuTrigger2, { asChild: true, children: /* @__PURE__ */ jsx6(Button3, { ref, variant: "outline", size: "sm", className: cn5(className), ...props, children: resolvedLabel }) }),
|
|
854
1464
|
/* @__PURE__ */ jsxs5(DropdownMenuContent2, { align: "end", className: "min-w-[12rem]", children: [
|
|
855
|
-
/* @__PURE__ */
|
|
856
|
-
/* @__PURE__ */
|
|
1465
|
+
/* @__PURE__ */ jsx6(DropdownMenuLabel2, { children: t("data.columnPicker.toggleColumns") }),
|
|
1466
|
+
/* @__PURE__ */ jsx6(DropdownMenuSeparator2, {}),
|
|
857
1467
|
columns.map((column) => /* @__PURE__ */ jsxs5(
|
|
858
1468
|
DropdownMenuItem2,
|
|
859
1469
|
{
|
|
@@ -862,7 +1472,7 @@ function ColumnPickerInner({ table, label = "Columns", className, ...props }, re
|
|
|
862
1472
|
column.toggleVisibility(!column.getIsVisible());
|
|
863
1473
|
},
|
|
864
1474
|
children: [
|
|
865
|
-
/* @__PURE__ */
|
|
1475
|
+
/* @__PURE__ */ jsx6(
|
|
866
1476
|
"span",
|
|
867
1477
|
{
|
|
868
1478
|
"aria-hidden": "true",
|
|
@@ -870,7 +1480,7 @@ function ColumnPickerInner({ table, label = "Columns", className, ...props }, re
|
|
|
870
1480
|
children: column.getIsVisible() ? "\u2713" : ""
|
|
871
1481
|
}
|
|
872
1482
|
),
|
|
873
|
-
/* @__PURE__ */
|
|
1483
|
+
/* @__PURE__ */ jsx6("span", { className: "capitalize", children: column.id })
|
|
874
1484
|
]
|
|
875
1485
|
},
|
|
876
1486
|
column.id
|
|
@@ -879,26 +1489,12 @@ function ColumnPickerInner({ table, label = "Columns", className, ...props }, re
|
|
|
879
1489
|
] });
|
|
880
1490
|
}
|
|
881
1491
|
ColumnPickerInner.displayName = "ColumnPicker";
|
|
882
|
-
var ColumnPicker =
|
|
1492
|
+
var ColumnPicker = forwardRef5(ColumnPickerInner);
|
|
883
1493
|
|
|
884
1494
|
// src/to-csv.ts
|
|
885
|
-
import { downloadBlob } from "@elabs-ai/components-ui";
|
|
886
|
-
var
|
|
887
|
-
|
|
888
|
-
if (value === null || value === void 0) return "";
|
|
889
|
-
if (value instanceof Date) return value.toISOString();
|
|
890
|
-
if (typeof value === "object") return JSON.stringify(value);
|
|
891
|
-
return String(value);
|
|
892
|
-
}
|
|
893
|
-
function quoteField(field, delimiter) {
|
|
894
|
-
if (INJECTION_PREFIXES.some((p) => field.startsWith(p))) {
|
|
895
|
-
field = "'" + field;
|
|
896
|
-
}
|
|
897
|
-
if (field.includes(delimiter) || field.includes('"') || field.includes("\n") || field.includes("\r")) {
|
|
898
|
-
return '"' + field.replaceAll('"', '""') + '"';
|
|
899
|
-
}
|
|
900
|
-
return field;
|
|
901
|
-
}
|
|
1495
|
+
import { csvQuoteField, csvStringifyValue, downloadBlob } from "@elabs-ai/components-ui";
|
|
1496
|
+
var stringifyValue = csvStringifyValue;
|
|
1497
|
+
var quoteField = csvQuoteField;
|
|
902
1498
|
function toCsv(rows, opts) {
|
|
903
1499
|
const delimiter = opts?.delimiter ?? ",";
|
|
904
1500
|
const includeHeader = opts?.header !== false;
|
|
@@ -926,7 +1522,9 @@ export {
|
|
|
926
1522
|
DataTableWithRef as DataTable,
|
|
927
1523
|
FacetFilter,
|
|
928
1524
|
FilterBar,
|
|
1525
|
+
FilterChip,
|
|
929
1526
|
SearchInput,
|
|
1527
|
+
createSelectionColumn,
|
|
930
1528
|
downloadCsv,
|
|
931
1529
|
toCsv
|
|
932
1530
|
};
|