@vendure-io/ui 1.2.0 → 2.0.0-beta.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 (112) hide show
  1. package/README.md +5 -5
  2. package/package.json +9 -4
  3. package/src/components/{ui → atoms}/alert-dialog.tsx +4 -4
  4. package/src/components/{ui → atoms}/badge.tsx +6 -3
  5. package/src/components/{ui → atoms}/button-group.tsx +1 -2
  6. package/src/components/{ui → atoms}/button.tsx +8 -2
  7. package/src/components/{ui → atoms}/calendar.tsx +1 -1
  8. package/src/components/{ui → atoms}/card.tsx +2 -3
  9. package/src/components/{ui → atoms}/carousel.tsx +1 -1
  10. package/src/components/atoms/checkbox.tsx +29 -0
  11. package/src/components/{ui → atoms}/combobox.tsx +3 -4
  12. package/src/components/{ui → atoms}/command.tsx +2 -2
  13. package/src/components/{ui → atoms}/context-menu.tsx +1 -1
  14. package/src/components/{ui → atoms}/dialog.tsx +4 -4
  15. package/src/components/{ui → atoms}/drawer.tsx +3 -3
  16. package/src/components/{ui → atoms}/dropdown-menu.tsx +3 -2
  17. package/src/components/{ui → atoms}/empty.tsx +1 -1
  18. package/src/components/{ui → atoms}/field.tsx +2 -2
  19. package/src/components/{ui → atoms}/hover-card.tsx +1 -1
  20. package/src/components/{ui → atoms}/input-group.tsx +3 -4
  21. package/src/components/{ui → atoms}/input-otp.tsx +0 -1
  22. package/src/components/{ui → atoms}/input.tsx +0 -1
  23. package/src/components/{ui → atoms}/item.tsx +2 -2
  24. package/src/components/{ui → atoms}/menubar.tsx +3 -4
  25. package/src/components/{ui → atoms}/native-select.tsx +0 -1
  26. package/src/components/{ui → atoms}/navigation-menu.tsx +7 -4
  27. package/src/components/{ui → atoms}/pagination.tsx +1 -1
  28. package/src/components/{ui → atoms}/popover.tsx +2 -2
  29. package/src/components/{ui → atoms}/select.tsx +22 -4
  30. package/src/components/{ui → atoms}/sheet.tsx +4 -4
  31. package/src/components/{ui → atoms}/sidebar.tsx +6 -6
  32. package/src/components/atoms/skeleton.tsx +16 -0
  33. package/src/components/{ui → atoms}/slider.tsx +0 -1
  34. package/src/components/{ui → atoms}/sonner.tsx +1 -1
  35. package/src/components/{ui → atoms}/switch.tsx +0 -1
  36. package/src/components/{ui → atoms}/tabs.tsx +6 -4
  37. package/src/components/{ui → atoms}/textarea.tsx +0 -1
  38. package/src/components/{ui → atoms}/toggle-group.tsx +3 -3
  39. package/src/components/{ui → atoms}/toggle.tsx +0 -1
  40. package/src/components/{ui → atoms}/tooltip.tsx +2 -1
  41. package/src/components/molecules/anonymized-token.tsx +170 -0
  42. package/src/components/molecules/chip.tsx +85 -0
  43. package/src/components/molecules/combobox-free-text.tsx +160 -0
  44. package/src/components/molecules/confirm-dialog.tsx +108 -0
  45. package/src/components/molecules/copyable-text.tsx +102 -0
  46. package/src/components/molecules/data-table/data-table-bulk-actions.tsx +69 -0
  47. package/src/components/molecules/data-table/data-table-column-header.tsx +61 -0
  48. package/src/components/molecules/data-table/data-table-filters.tsx +198 -0
  49. package/src/components/molecules/data-table/data-table-helpers.tsx +73 -0
  50. package/src/components/molecules/data-table/data-table-types.tsx +241 -0
  51. package/src/components/molecules/data-table/data-table-view-options.tsx +73 -0
  52. package/src/components/molecules/data-table/data-table.tsx +500 -0
  53. package/src/components/molecules/data-table/list-header.tsx +53 -0
  54. package/src/components/molecules/data-table/table-pagination.tsx +263 -0
  55. package/src/components/molecules/date-time.tsx +73 -0
  56. package/src/components/molecules/description-list.tsx +120 -0
  57. package/src/components/molecules/format-provider.tsx +46 -0
  58. package/src/components/molecules/id-chip.tsx +79 -0
  59. package/src/components/molecules/illustrations/access-denied.tsx +55 -0
  60. package/src/components/molecules/illustrations/empty-collection.tsx +66 -0
  61. package/src/components/molecules/illustrations/empty-database.tsx +44 -0
  62. package/src/components/molecules/illustrations/empty-media.tsx +68 -0
  63. package/src/components/molecules/illustrations/error.tsx +64 -0
  64. package/src/components/molecules/illustrations/first-run.tsx +55 -0
  65. package/src/components/molecules/illustrations/illustration-types.tsx +9 -0
  66. package/src/components/molecules/illustrations/no-activity.tsx +49 -0
  67. package/src/components/molecules/illustrations/no-deployments.tsx +49 -0
  68. package/src/components/molecules/illustrations/no-documents.tsx +67 -0
  69. package/src/components/molecules/illustrations/no-keys.tsx +50 -0
  70. package/src/components/molecules/illustrations/no-logs.tsx +53 -0
  71. package/src/components/molecules/illustrations/no-members.tsx +56 -0
  72. package/src/components/molecules/illustrations/no-notifications.tsx +52 -0
  73. package/src/components/molecules/illustrations/no-orders.tsx +48 -0
  74. package/src/components/molecules/illustrations/no-plugins.tsx +72 -0
  75. package/src/components/molecules/illustrations/no-results.tsx +69 -0
  76. package/src/components/molecules/illustrations/not-found.tsx +48 -0
  77. package/src/components/molecules/illustrations/offline.tsx +43 -0
  78. package/src/components/molecules/illustrations/pending-approval.tsx +56 -0
  79. package/src/components/molecules/illustrations/upload-dropzone.tsx +59 -0
  80. package/src/components/molecules/money.tsx +91 -0
  81. package/src/components/{custom → molecules}/multi-select.tsx +2 -2
  82. package/src/components/molecules/page-header.tsx +80 -0
  83. package/src/components/molecules/password-input.tsx +47 -0
  84. package/src/components/molecules/relative-time.tsx +81 -0
  85. package/src/components/molecules/stat-card.tsx +134 -0
  86. package/src/components/molecules/state-views/empty-state.tsx +75 -0
  87. package/src/components/molecules/state-views/error-state.tsx +91 -0
  88. package/src/components/molecules/state-views/loading-state.tsx +80 -0
  89. package/src/components/molecules/status-badge.tsx +100 -0
  90. package/src/hooks/use-copy.ts +51 -0
  91. package/src/lib/base-ui.ts +2 -1
  92. package/src/lib/format-relative.ts +41 -0
  93. package/src/lib/state-dictionary.ts +137 -0
  94. package/src/components/ui/checkbox.tsx +0 -30
  95. package/src/components/ui/skeleton.tsx +0 -13
  96. /package/src/components/{ui → atoms}/accordion.tsx +0 -0
  97. /package/src/components/{ui → atoms}/alert.tsx +0 -0
  98. /package/src/components/{ui → atoms}/aspect-ratio.tsx +0 -0
  99. /package/src/components/{ui → atoms}/avatar.tsx +0 -0
  100. /package/src/components/{ui → atoms}/breadcrumb.tsx +0 -0
  101. /package/src/components/{ui → atoms}/chart.tsx +0 -0
  102. /package/src/components/{ui → atoms}/collapsible.tsx +0 -0
  103. /package/src/components/{ui → atoms}/direction.tsx +0 -0
  104. /package/src/components/{ui → atoms}/kbd.tsx +0 -0
  105. /package/src/components/{ui → atoms}/label.tsx +0 -0
  106. /package/src/components/{ui → atoms}/progress.tsx +0 -0
  107. /package/src/components/{ui → atoms}/radio-group.tsx +0 -0
  108. /package/src/components/{ui → atoms}/resizable.tsx +0 -0
  109. /package/src/components/{ui → atoms}/scroll-area.tsx +0 -0
  110. /package/src/components/{ui → atoms}/separator.tsx +0 -0
  111. /package/src/components/{ui → atoms}/spinner.tsx +0 -0
  112. /package/src/components/{ui → atoms}/table.tsx +0 -0
@@ -0,0 +1,500 @@
1
+ 'use client';
2
+
3
+ import {
4
+ type Column,
5
+ type ColumnDef,
6
+ type ColumnFiltersState,
7
+ flexRender,
8
+ functionalUpdate,
9
+ getCoreRowModel,
10
+ getFilteredRowModel,
11
+ getPaginationRowModel,
12
+ getSortedRowModel,
13
+ type OnChangeFn,
14
+ type PaginationState,
15
+ type RowSelectionState,
16
+ type SortingState,
17
+ type TableOptions,
18
+ type TableState,
19
+ useReactTable,
20
+ type VisibilityState,
21
+ } from '@tanstack/react-table';
22
+ import { Checkbox } from '@vendure-io/ui/components/atoms/checkbox';
23
+ import {
24
+ ContextMenu,
25
+ ContextMenuContent,
26
+ ContextMenuTrigger,
27
+ } from '@vendure-io/ui/components/atoms/context-menu';
28
+ import { Skeleton } from '@vendure-io/ui/components/atoms/skeleton';
29
+ import {
30
+ Table,
31
+ TableBody,
32
+ TableCell,
33
+ TableHead,
34
+ TableHeader,
35
+ TableRow,
36
+ } from '@vendure-io/ui/components/atoms/table';
37
+ import {
38
+ DataTableBulkActions,
39
+ useSelectionCache,
40
+ } from '@vendure-io/ui/components/molecules/data-table/data-table-bulk-actions';
41
+ import { DataTableColumnHeader } from '@vendure-io/ui/components/molecules/data-table/data-table-column-header';
42
+ import {
43
+ DataTableAddFilter,
44
+ DataTableAppliedFilters,
45
+ } from '@vendure-io/ui/components/molecules/data-table/data-table-filters';
46
+ import {
47
+ buildDisplayColumns,
48
+ resolveSlot,
49
+ } from '@vendure-io/ui/components/molecules/data-table/data-table-helpers';
50
+ import type {
51
+ ControlledState,
52
+ DataTableProps,
53
+ } from '@vendure-io/ui/components/molecules/data-table/data-table-types';
54
+ import { DataTableViewOptions } from '@vendure-io/ui/components/molecules/data-table/data-table-view-options';
55
+ import {
56
+ ListHeader,
57
+ ListHeaderChips,
58
+ ListHeaderControls,
59
+ } from '@vendure-io/ui/components/molecules/data-table/list-header';
60
+ import { TablePagination } from '@vendure-io/ui/components/molecules/data-table/table-pagination';
61
+ import { cn } from '@vendure-io/ui/lib/utils';
62
+ import * as React from 'react';
63
+
64
+ // The composition root. Owns the single `useReactTable` instance and the
65
+ // controlled/uncontrolled bridge, then lays out header → controls → chips →
66
+ // bulk overlay → Table → footer. Every capability follows the Phase-1 rule
67
+ // verbatim: a feature renders only if its config is wired — no feature flags, no
68
+ // disabled placeholders. Only this folder imports TanStack; the composed
69
+ // ListHeader/TablePagination/Chip primitives stay TanStack-free.
70
+
71
+ /**
72
+ * Bridge a capability config to a `[value, onChange]` pair TanStack can consume
73
+ * as fully controlled. Controlled configs (`value`) are the source of truth every
74
+ * render; uncontrolled configs (`defaultValue` or nothing) get internal state the
75
+ * core owns, with any `onChange` observing. This fixes the donor's seed-then-emit
76
+ * bug: the prop, not a one-time seed, drives every render.
77
+ */
78
+ function useControlledTableState<T>(
79
+ config: ControlledState<T> | undefined,
80
+ fallback: T,
81
+ ): readonly [T, OnChangeFn<T>] {
82
+ const controlled = config?.value !== undefined;
83
+ const [internal, setInternal] = React.useState<T>(config?.defaultValue ?? fallback);
84
+ const value = controlled ? (config?.value as T) : internal;
85
+ const onChange = React.useCallback<OnChangeFn<T>>(
86
+ (updater) => {
87
+ if (!controlled) setInternal((old) => functionalUpdate(updater, old));
88
+ config?.onChange?.(updater);
89
+ },
90
+ [controlled, config],
91
+ );
92
+ return [value, onChange] as const;
93
+ }
94
+
95
+ /** A string label for a column, from a string `header`, else the column id. */
96
+ function headerLabelText<TData>(column: Column<TData, unknown>): string {
97
+ const header = column.columnDef.header;
98
+ return typeof header === 'string' ? header : column.id;
99
+ }
100
+
101
+ function ariaSort<TData>(
102
+ hasSorting: boolean,
103
+ column: Column<TData, unknown>,
104
+ ): 'ascending' | 'descending' | 'none' | undefined {
105
+ if (!hasSorting || !column.getCanSort()) return undefined;
106
+ const sorted = column.getIsSorted();
107
+ return sorted === 'asc' ? 'ascending' : sorted === 'desc' ? 'descending' : 'none';
108
+ }
109
+
110
+ function DataTable<TData>({
111
+ rows,
112
+ columns,
113
+ getRowId,
114
+ isLoading,
115
+ skeletonRowCount = 10,
116
+ pagination,
117
+ sorting,
118
+ filters,
119
+ columnVisibility,
120
+ rowSelection,
121
+ header,
122
+ toolbar,
123
+ bulkActions,
124
+ rowActions,
125
+ contextActions,
126
+ emptyState,
127
+ renderRow,
128
+ setTableOptions,
129
+ labels,
130
+ className,
131
+ }: DataTableProps<TData>) {
132
+ const warnedMissingPaginatedSelectionRowId = React.useRef(false);
133
+ if (
134
+ process.env.NODE_ENV !== 'production' &&
135
+ rowSelection != null &&
136
+ pagination != null &&
137
+ pagination.mode !== 'client' &&
138
+ getRowId == null &&
139
+ !warnedMissingPaginatedSelectionRowId.current
140
+ ) {
141
+ warnedMissingPaginatedSelectionRowId.current = true;
142
+ console.warn(
143
+ '[DataTable] rowSelection with server pagination requires getRowId. ' +
144
+ "TanStack's default page-index row ids can resolve bulkActions selection to the wrong row after changing pages.",
145
+ );
146
+ }
147
+
148
+ // Resolve labels once per `labels` identity so the derived closures are stable
149
+ // deps for the display-column memo below.
150
+ const l = React.useMemo(
151
+ () => ({
152
+ empty: labels?.empty ?? 'No results',
153
+ selectAllRows: labels?.selectAllRows ?? 'Select all rows',
154
+ selectRow: labels?.selectRow ?? ((index: number) => `Select row ${index + 1}`),
155
+ rowActions: labels?.rowActions ?? 'Open row actions',
156
+ columnsTrigger: labels?.columnsTrigger ?? 'Columns',
157
+ columnsHeading: labels?.columnsHeading ?? 'Toggle columns',
158
+ addFilter: labels?.addFilter ?? 'Add filter',
159
+ removeFilter:
160
+ labels?.removeFilter ?? ((columnLabel: string) => `Remove ${columnLabel} filter`),
161
+ sortLabel: labels?.sortLabel ?? ((columnLabel: string) => `Sort by ${columnLabel}`),
162
+ filtersCollapsed: labels?.filtersCollapsed ?? ((count: number) => `${count} filters`),
163
+ pagination: labels?.pagination,
164
+ }),
165
+ [labels],
166
+ );
167
+
168
+ const [sortingState, onSortingChange] = useControlledTableState<SortingState>(sorting, []);
169
+ const [filtersState, onColumnFiltersChange] = useControlledTableState<ColumnFiltersState>(
170
+ filters,
171
+ [],
172
+ );
173
+ const [visibilityState, onColumnVisibilityChange] = useControlledTableState<VisibilityState>(
174
+ columnVisibility,
175
+ {},
176
+ );
177
+ const [selectionState, onRowSelectionChange] = useControlledTableState<RowSelectionState>(
178
+ rowSelection,
179
+ {},
180
+ );
181
+ const selectColumnId = rowSelection?.selectColumnId ?? 'select';
182
+
183
+ // Inject the select/actions display columns around the data columns. `meta` on
184
+ // the consumer's columns is never touched — it's the dashboard's field-info seam.
185
+ const displayColumns = React.useMemo(() => {
186
+ // biome-ignore lint/suspicious/noExplicitAny: matches TanStack's ColumnDef value-type convention
187
+ const selectColumn: ColumnDef<TData, any> | undefined = rowSelection
188
+ ? {
189
+ id: selectColumnId,
190
+ enableSorting: false,
191
+ enableHiding: false,
192
+ // The checkboxes stay hidden until they're relevant: a header/row
193
+ // checkbox reveals on hover of its row (the enclosing `group/*`), on
194
+ // keyboard focus, or once it carries a checked/indeterminate state —
195
+ // so an idle table reads as content, not a selection grid. They're
196
+ // floated absolutely over the (zero-width) select cell so an empty
197
+ // selection reserves no gutter — no left gap to read as a bug.
198
+ header: ({ table }) => (
199
+ <Checkbox
200
+ checked={table.getIsAllPageRowsSelected()}
201
+ indeterminate={table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected()}
202
+ onCheckedChange={(checked) => table.toggleAllPageRowsSelected(checked)}
203
+ aria-label={l.selectAllRows}
204
+ className="absolute top-1/2 left-2 z-10 -translate-y-1/2 opacity-0 transition-opacity group-hover/header-row:opacity-100 focus-visible:opacity-100 data-checked:opacity-100 data-indeterminate:opacity-100"
205
+ />
206
+ ),
207
+ cell: ({ row }) => (
208
+ <Checkbox
209
+ checked={row.getIsSelected()}
210
+ disabled={!row.getCanSelect()}
211
+ onCheckedChange={(checked) => row.toggleSelected(checked)}
212
+ aria-label={l.selectRow(row.index)}
213
+ className="absolute top-1/2 left-2 z-10 -translate-y-1/2 opacity-0 transition-opacity group-hover/row:opacity-100 focus-visible:opacity-100 data-checked:opacity-100"
214
+ />
215
+ ),
216
+ }
217
+ : undefined;
218
+
219
+ // biome-ignore lint/suspicious/noExplicitAny: matches TanStack's ColumnDef value-type convention
220
+ const actionsColumn: ColumnDef<TData, any> | undefined = rowActions
221
+ ? {
222
+ id: 'actions',
223
+ enableSorting: false,
224
+ enableHiding: false,
225
+ header: () => <span className="sr-only">{l.rowActions}</span>,
226
+ cell: ({ row, table }) => rowActions(row.original, { row, table }),
227
+ }
228
+ : undefined;
229
+
230
+ return buildDisplayColumns(columns, { select: selectColumn, actions: actionsColumn });
231
+ }, [
232
+ columns,
233
+ rowSelection,
234
+ rowActions,
235
+ selectColumnId,
236
+ l.selectAllRows,
237
+ l.selectRow,
238
+ l.rowActions,
239
+ ]);
240
+
241
+ const state: Partial<TableState> = {};
242
+ if (sorting) state.sorting = sortingState;
243
+ if (filters) state.columnFilters = filtersState;
244
+ if (columnVisibility) state.columnVisibility = visibilityState;
245
+ if (rowSelection) state.rowSelection = selectionState;
246
+ if (pagination) {
247
+ state.pagination = { pageIndex: pagination.page - 1, pageSize: pagination.pageSize };
248
+ }
249
+
250
+ const onPaginationChange: OnChangeFn<PaginationState> = (updater) => {
251
+ if (!pagination) return;
252
+ const current: PaginationState = {
253
+ pageIndex: pagination.page - 1,
254
+ pageSize: pagination.pageSize,
255
+ };
256
+ const next = functionalUpdate(updater, current);
257
+ if (next.pageSize !== current.pageSize) pagination.onPageSizeChange?.(next.pageSize);
258
+ if (next.pageIndex !== current.pageIndex) pagination.onPageChange(next.pageIndex + 1);
259
+ };
260
+
261
+ const baseOptions: TableOptions<TData> = {
262
+ data: rows,
263
+ columns: displayColumns,
264
+ state,
265
+ getRowId,
266
+ getCoreRowModel: getCoreRowModel(),
267
+ enableSorting: sorting != null,
268
+ enableFilters: filters != null,
269
+ // Server mode (default) emits only; client mode enables the matching engine.
270
+ manualSorting: sorting ? sorting.mode !== 'client' : undefined,
271
+ manualFiltering: filters ? filters.mode !== 'client' : undefined,
272
+ manualPagination: pagination ? pagination.mode !== 'client' : undefined,
273
+ rowCount: pagination && pagination.mode !== 'client' ? pagination.totalItems : undefined,
274
+ };
275
+
276
+ if (sorting) {
277
+ baseOptions.onSortingChange = onSortingChange;
278
+ if (sorting.mode === 'client') baseOptions.getSortedRowModel = getSortedRowModel();
279
+ }
280
+ if (filters) {
281
+ baseOptions.onColumnFiltersChange = onColumnFiltersChange;
282
+ if (filters.mode === 'client') baseOptions.getFilteredRowModel = getFilteredRowModel();
283
+ }
284
+ if (columnVisibility) baseOptions.onColumnVisibilityChange = onColumnVisibilityChange;
285
+ if (rowSelection) {
286
+ baseOptions.onRowSelectionChange = onRowSelectionChange;
287
+ baseOptions.enableRowSelection = rowSelection.enableRowSelection ?? true;
288
+ }
289
+ if (pagination) {
290
+ baseOptions.onPaginationChange = onPaginationChange;
291
+ if (pagination.mode === 'client') baseOptions.getPaginationRowModel = getPaginationRowModel();
292
+ }
293
+
294
+ const table = useReactTable(setTableOptions ? setTableOptions(baseOptions) : baseOptions);
295
+
296
+ const selectionCache = useSelectionCache(table);
297
+
298
+ const bodyRows = table.getRowModel().rows;
299
+ const columnCount = table.getVisibleLeafColumns().length;
300
+
301
+ // The select checkbox is floated absolutely over a zero-width cell, so the
302
+ // gutter it needs is folded into the leading data column as left padding —
303
+ // an idle table reads as an intentional indent, not an empty checkbox cell,
304
+ // and the revealed checkbox never overlaps the first column's content.
305
+ const leadingColumnId =
306
+ rowSelection != null
307
+ ? table.getVisibleLeafColumns().find((column) => column.id !== selectColumnId)?.id
308
+ : undefined;
309
+ const columnClass = (id: string): string | undefined => {
310
+ if (id === selectColumnId) return 'relative w-0 p-0';
311
+ if (id === leadingColumnId) return 'pl-8';
312
+ // Actions cells drop vertical padding so a row-action button (taller than a
313
+ // text line) sits inside the natural row height rather than inflating every
314
+ // row; `align-middle` on the cell keeps it centred.
315
+ if (id === 'actions') return 'py-0';
316
+ return undefined;
317
+ };
318
+
319
+ const toolbarNode = toolbar != null ? resolveSlot(toolbar, table) : undefined;
320
+ const showViewOptions = columnVisibility != null && columnVisibility.showViewOptions !== false;
321
+ const hasFilterMenu = Boolean(
322
+ filters?.showAddFilter !== false && filters?.columns && filters.columns.length > 0,
323
+ );
324
+ const showControls = toolbar != null || showViewOptions || hasFilterMenu;
325
+ const showChips =
326
+ filters != null &&
327
+ filters.showAppliedFilters !== false &&
328
+ table.getState().columnFilters.length > 0;
329
+ const showHeader = header != null || showControls || showChips;
330
+ const showBulk = rowSelection != null && bulkActions != null;
331
+
332
+ return (
333
+ <div data-slot="data-table" className={cn('flex flex-col gap-4', className)}>
334
+ {showHeader && (
335
+ <ListHeader>
336
+ {header}
337
+ {showControls && (
338
+ <ListHeaderControls>
339
+ {toolbarNode}
340
+ {hasFilterMenu && filters?.columns && (
341
+ <DataTableAddFilter table={table} columns={filters.columns} label={l.addFilter} />
342
+ )}
343
+ {showViewOptions && (
344
+ <DataTableViewOptions
345
+ table={table}
346
+ triggerLabel={l.columnsTrigger}
347
+ heading={l.columnsHeading}
348
+ />
349
+ )}
350
+ </ListHeaderControls>
351
+ )}
352
+ {showChips && (
353
+ <ListHeaderChips>
354
+ <DataTableAppliedFilters
355
+ table={table}
356
+ columns={filters?.columns}
357
+ inlineChipLimit={filters?.inlineChipLimit}
358
+ removeLabel={l.removeFilter}
359
+ collapsedLabel={l.filtersCollapsed}
360
+ />
361
+ </ListHeaderChips>
362
+ )}
363
+ </ListHeader>
364
+ )}
365
+
366
+ {showBulk && bulkActions && (
367
+ <DataTableBulkActions table={table} cache={selectionCache} render={bulkActions} />
368
+ )}
369
+
370
+ <Table>
371
+ <TableHeader>
372
+ {table.getHeaderGroups().map((headerGroup) => (
373
+ <TableRow key={headerGroup.id} className="group/header-row">
374
+ {headerGroup.headers.map((headerCell) => {
375
+ const content = headerCell.isPlaceholder
376
+ ? null
377
+ : flexRender(headerCell.column.columnDef.header, headerCell.getContext());
378
+ return (
379
+ <TableHead
380
+ key={headerCell.id}
381
+ aria-sort={ariaSort(sorting != null, headerCell.column)}
382
+ className={columnClass(headerCell.column.id)}
383
+ >
384
+ {sorting != null && !headerCell.isPlaceholder ? (
385
+ <DataTableColumnHeader
386
+ column={headerCell.column}
387
+ sortLabel={l.sortLabel(headerLabelText(headerCell.column))}
388
+ >
389
+ {content}
390
+ </DataTableColumnHeader>
391
+ ) : (
392
+ content
393
+ )}
394
+ </TableHead>
395
+ );
396
+ })}
397
+ </TableRow>
398
+ ))}
399
+ </TableHeader>
400
+ <TableBody>
401
+ {isLoading && rows.length === 0 ? (
402
+ Array.from({ length: skeletonRowCount }).map((_, rowIndex) => (
403
+ <TableRow key={`skeleton-${rowIndex}`}>
404
+ {Array.from({ length: Math.max(columnCount, 1) }).map((__, cellIndex) => (
405
+ <TableCell key={`skeleton-cell-${cellIndex}`}>
406
+ <Skeleton className="h-4 w-full" />
407
+ </TableCell>
408
+ ))}
409
+ </TableRow>
410
+ ))
411
+ ) : bodyRows.length === 0 ? (
412
+ <TableRow className="hover:bg-transparent">
413
+ <TableCell
414
+ colSpan={Math.max(columnCount, 1)}
415
+ className="text-muted-foreground h-24 text-center"
416
+ >
417
+ {emptyState ?? l.empty}
418
+ </TableCell>
419
+ </TableRow>
420
+ ) : (
421
+ bodyRows.map((row) => {
422
+ const rowNode = (
423
+ <TableRow
424
+ className="group/row"
425
+ data-state={row.getIsSelected() ? 'selected' : undefined}
426
+ >
427
+ {row.getVisibleCells().map((cell) => (
428
+ <TableCell key={cell.id} className={columnClass(cell.column.id)}>
429
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
430
+ </TableCell>
431
+ ))}
432
+ </TableRow>
433
+ );
434
+ // Right-click accelerator: the row itself is the context-menu
435
+ // trigger, so the consumer supplies only the items and the core
436
+ // owns the menu chrome.
437
+ const defaultRow = contextActions ? (
438
+ <ContextMenu>
439
+ <ContextMenuTrigger render={rowNode} />
440
+ <ContextMenuContent>
441
+ {contextActions(row.original, { row, table })}
442
+ </ContextMenuContent>
443
+ </ContextMenu>
444
+ ) : (
445
+ rowNode
446
+ );
447
+ // Row-render seam: consumers can swap the default row for a
448
+ // full-width utility row or a per-row wrapper the cell grid can't
449
+ // express. Returning `defaultRow` keeps the built-in rendering.
450
+ return (
451
+ <React.Fragment key={row.id}>
452
+ {renderRow ? renderRow(row, { table, columnCount, defaultRow }) : defaultRow}
453
+ </React.Fragment>
454
+ );
455
+ })
456
+ )}
457
+ </TableBody>
458
+ </Table>
459
+
460
+ {pagination && (
461
+ <TablePagination
462
+ page={pagination.page}
463
+ pageSize={pagination.pageSize}
464
+ totalItems={pagination.totalItems ?? table.getPrePaginationRowModel().rows.length}
465
+ onPageChange={pagination.onPageChange}
466
+ onPageSizeChange={pagination.onPageSizeChange}
467
+ pageSizeOptions={pagination.pageSizeOptions}
468
+ getPageHref={pagination.getPageHref}
469
+ formatRange={pagination.formatRange}
470
+ navLabel={l.pagination?.navLabel}
471
+ previousLabel={l.pagination?.previousLabel}
472
+ nextLabel={l.pagination?.nextLabel}
473
+ pageSizeLabel={l.pagination?.pageSizeLabel}
474
+ />
475
+ )}
476
+ </div>
477
+ );
478
+ }
479
+
480
+ export { DataTable };
481
+
482
+ // Re-export the consumer-facing TanStack types so columns can be authored from
483
+ // the same import as `DataTable` — no direct `@tanstack/react-table` dependency
484
+ // in consumer code just to type a `ColumnDef`.
485
+ export type {
486
+ CellContext,
487
+ ColumnDef,
488
+ ColumnFiltersState,
489
+ HeaderContext,
490
+ OnChangeFn,
491
+ Row,
492
+ RowSelectionState,
493
+ SortingState,
494
+ Table,
495
+ TableOptions,
496
+ VisibilityState,
497
+ } from '@tanstack/react-table';
498
+ // Re-export the full DataTable type surface so a consumer imports everything
499
+ // (props, capability configs, contexts, labels) from this one module.
500
+ export type * from '@vendure-io/ui/components/molecules/data-table/data-table-types';
@@ -0,0 +1,53 @@
1
+ import { cn } from '@vendure-io/ui/lib/utils';
2
+ import type * as React from 'react';
3
+
4
+ // The three-zone list-page header: title row / control row / chip row. Ships
5
+ // layout, not logic — TanStack-free, fetch-agnostic, router-agnostic.
6
+ //
7
+ // By decision, the title row is NOT re-implemented here: it complements
8
+ // `PageHeader` rather than competing with it, so consumers compose the existing
9
+ // `PageHeader` compound as the first child. The chip row is rendered by the
10
+ // consumer only when filters are active — ListHeaderChips does no
11
+ // `React.Children` introspection and holds no active-filter logic.
12
+
13
+ /**
14
+ * Root of the list-page header: a vertical stack of zones. Render free children
15
+ * top to bottom — typically `PageHeader`, then `ListHeaderControls`, then
16
+ * (only when filters are active) `ListHeaderChips`.
17
+ */
18
+ function ListHeader({ className, ...props }: React.ComponentProps<'div'>) {
19
+ return (
20
+ <div data-slot="list-header" className={cn('flex flex-col gap-4', className)} {...props} />
21
+ );
22
+ }
23
+
24
+ /**
25
+ * The control row: a horizontal flex row for search input, filter triggers, and
26
+ * view options. Layout only — children are free.
27
+ */
28
+ function ListHeaderControls({ className, ...props }: React.ComponentProps<'div'>) {
29
+ return (
30
+ <div
31
+ data-slot="list-header-controls"
32
+ className={cn('flex flex-wrap items-center gap-2', className)}
33
+ {...props}
34
+ />
35
+ );
36
+ }
37
+
38
+ /**
39
+ * The chip row: a horizontal wrapping flex row of applied-filter `Chip`s. Dumb
40
+ * by design — render it only when filters are active; it does not inspect its
41
+ * children or decide when to appear.
42
+ */
43
+ function ListHeaderChips({ className, ...props }: React.ComponentProps<'div'>) {
44
+ return (
45
+ <div
46
+ data-slot="list-header-chips"
47
+ className={cn('flex flex-wrap items-center gap-2', className)}
48
+ {...props}
49
+ />
50
+ );
51
+ }
52
+
53
+ export { ListHeader, ListHeaderControls, ListHeaderChips };