@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,73 @@
1
+ import type { Column, ColumnDef, RowSelectionState, Table } from '@tanstack/react-table';
2
+ import type { ControlledState } from '@vendure-io/ui/components/molecules/data-table/data-table-types';
3
+
4
+ // Pure, TanStack-shaped logic for the DataTable core. No JSX and no React state,
5
+ // so each function is unit-testable in isolation (the getRange/clampPage
6
+ // precedent from table-pagination.tsx).
7
+
8
+ // biome-ignore lint/suspicious/noExplicitAny: matches TanStack's ColumnDef value-type convention
9
+ type AnyColumnDef<TData> = ColumnDef<TData, any>;
10
+
11
+ /**
12
+ * A capability config is controlled when it carries a `value`. Controlled means
13
+ * the prop is the source of truth on every render; uncontrolled (`defaultValue`
14
+ * or nothing) means the core owns the state.
15
+ */
16
+ export function isControlled<T>(config: ControlledState<T> | undefined): boolean {
17
+ return config?.value !== undefined;
18
+ }
19
+
20
+ /**
21
+ * Prepend a `select` column and append an `actions` column around the
22
+ * consumer's data columns, keeping `columns` about data only. The display
23
+ * columns are built in `data-table.tsx` (they render JSX); this helper owns the
24
+ * ordering so it can be tested without a DOM.
25
+ */
26
+ export function buildDisplayColumns<TData>(
27
+ columns: AnyColumnDef<TData>[],
28
+ display: { select?: AnyColumnDef<TData>; actions?: AnyColumnDef<TData> },
29
+ ): AnyColumnDef<TData>[] {
30
+ return [
31
+ ...(display.select ? [display.select] : []),
32
+ ...columns,
33
+ ...(display.actions ? [display.actions] : []),
34
+ ];
35
+ }
36
+
37
+ /**
38
+ * Hideable data columns for the column-visibility menu. Group columns are not
39
+ * listed because their visibility is derived from leaf columns.
40
+ */
41
+ export function getHideableLeafColumns<TData>(table: Table<TData>): Column<TData, unknown>[] {
42
+ return table.getAllLeafColumns().filter((column) => column.getCanHide());
43
+ }
44
+
45
+ /** Resolve a render-prop-or-node slot against the live table instance. */
46
+ export function resolveSlot<TData, TSlot>(
47
+ slot: TSlot | ((table: Table<TData>) => TSlot) | undefined,
48
+ table: Table<TData>,
49
+ ): TSlot | undefined {
50
+ return typeof slot === 'function' ? (slot as (table: Table<TData>) => TSlot)(table) : slot;
51
+ }
52
+
53
+ /**
54
+ * Materialise the current selection into row originals via the cross-page cache.
55
+ * Selected ids missing from the cache (never-seen pages) are skipped rather than
56
+ * yielding `undefined` holes.
57
+ */
58
+ export function getSelectedOriginals<TData>(
59
+ selection: RowSelectionState,
60
+ cache: Map<string, TData>,
61
+ ): TData[] {
62
+ const originals: TData[] = [];
63
+ for (const id of getSelectedRowIds(selection)) {
64
+ const original = cache.get(id);
65
+ if (original !== undefined) originals.push(original);
66
+ }
67
+ return originals;
68
+ }
69
+
70
+ /** The ids flagged `true` in a `RowSelectionState`. */
71
+ export function getSelectedRowIds(selection: RowSelectionState): string[] {
72
+ return Object.keys(selection).filter((id) => selection[id]);
73
+ }
@@ -0,0 +1,241 @@
1
+ import type {
2
+ CellContext,
3
+ ColumnDef,
4
+ ColumnFiltersState,
5
+ HeaderContext,
6
+ OnChangeFn,
7
+ Row,
8
+ RowSelectionState,
9
+ SortingState,
10
+ Table,
11
+ TableOptions,
12
+ VisibilityState,
13
+ } from '@tanstack/react-table';
14
+ import type * as React from 'react';
15
+
16
+ // Pure type module for the DataTable molecule. Kept runtime-free so it can be
17
+ // imported by both `data-table.tsx` and its sub-components without an import
18
+ // cycle. `data-table.tsx` re-exports everything here (plus the consumer-facing
19
+ // TanStack types) so columns can be authored from a single import.
20
+
21
+ /**
22
+ * Controlled OR uncontrolled, per capability. `value` present → controlled: the
23
+ * prop is the source of truth on every render (no internal seeding). `defaultValue`
24
+ * → uncontrolled: the core seeds its own state and `onChange` (if given) only
25
+ * observes. Never mix `value` with `defaultValue`.
26
+ */
27
+ export type ControlledState<T> =
28
+ | { value: T; onChange: OnChangeFn<T>; defaultValue?: never }
29
+ | { value?: never; onChange?: OnChangeFn<T>; defaultValue?: T };
30
+
31
+ /* --- capability configs; PRESENCE of the config is the only feature switch --- */
32
+
33
+ export type DataTableSorting = ControlledState<SortingState> & {
34
+ /** 'server' (default) → manualSorting, emit only. 'client' → TanStack sorts `rows`. */
35
+ mode?: 'server' | 'client';
36
+ };
37
+
38
+ export type DataTableColumnVisibility = ControlledState<VisibilityState> & {
39
+ /**
40
+ * Defaults to true. Set false when a consumer renders its own column-settings
41
+ * control through `toolbar(table)` while the core still consumes visibility.
42
+ */
43
+ showViewOptions?: boolean;
44
+ };
45
+
46
+ export type DataTableRowSelection<TData> = ControlledState<RowSelectionState> & {
47
+ enableRowSelection?: boolean | ((row: Row<TData>) => boolean);
48
+ /** Display-column id for the generated selection column. Default: 'select'. */
49
+ selectColumnId?: string;
50
+ };
51
+
52
+ /**
53
+ * 1-based, mirrors the Phase-1 `TablePagination` (composed verbatim). Always
54
+ * controlled via `page`/`onPageChange` — no `defaultValue` branch, because page
55
+ * is typically URL-owned.
56
+ */
57
+ export interface DataTablePagination {
58
+ mode?: 'server' | 'client'; // default 'server'
59
+ page: number; // 1-based
60
+ pageSize: number;
61
+ totalItems?: number; // required in server mode
62
+ onPageChange: (page: number) => void;
63
+ onPageSizeChange?: (size: number) => void; // presence renders the size selector
64
+ pageSizeOptions?: number[]; // default [10, 25, 50, 100]
65
+ getPageHref?: (page: number) => string;
66
+ formatRange?: (range: { start: number; end: number; total: number }) => string;
67
+ }
68
+
69
+ /**
70
+ * Open operator vocabulary — the core never interprets it; it only stores it in
71
+ * `ColumnFiltersState` and renders chips. Matches the donor's `{ [operator]: value }`.
72
+ */
73
+ export type DataTableFilterOperator =
74
+ | 'eq'
75
+ | 'notEq'
76
+ | 'contains'
77
+ | 'notContains'
78
+ | 'lt'
79
+ | 'lte'
80
+ | 'gt'
81
+ | 'gte'
82
+ | 'between'
83
+ | 'in'
84
+ | 'notIn'
85
+ | 'regex'
86
+ | 'isNull'
87
+ | (string & {});
88
+
89
+ export type DataTableFilterValue = Partial<Record<DataTableFilterOperator, unknown>>;
90
+
91
+ /** Declares one filterable column to the add-filter menu + chip row. */
92
+ export interface DataTableFilterColumn<TData = unknown> {
93
+ id: string; // must match a ColumnDef.id / accessorKey
94
+ label: React.ReactNode;
95
+ operators?: DataTableFilterOperator[];
96
+ /**
97
+ * Editor for this column's value. Default: a text `Input` emitting
98
+ * `{ [operators?.[0] ?? 'contains']: value }`. The dashboard passes its own
99
+ * typed editors here.
100
+ */
101
+ renderInput?: (ctx: {
102
+ value: DataTableFilterValue | undefined;
103
+ onChange: (next: DataTableFilterValue | undefined) => void;
104
+ column: DataTableFilterColumn<TData>;
105
+ }) => React.ReactNode;
106
+ /**
107
+ * Chip summary. Default: `${label}: ${JSON.stringify(value)}`. The dashboard
108
+ * passes its human-readable-operator formatter here.
109
+ */
110
+ formatChip?: (value: DataTableFilterValue) => React.ReactNode;
111
+ }
112
+
113
+ export type DataTableFilters<TData> = ControlledState<ColumnFiltersState> & {
114
+ mode?: 'server' | 'client'; // default 'server'
115
+ /**
116
+ * Optional. Present → the core renders the add-filter menu. Absent → the core
117
+ * renders ONLY the applied-chip row (the dashboard supplies its own add-filter
118
+ * in `toolbar`).
119
+ */
120
+ columns?: DataTableFilterColumn<TData>[];
121
+ /** Collapse chips past this many into an "N filters" popover. Default Infinity. */
122
+ inlineChipLimit?: number;
123
+ /**
124
+ * Defaults to true when `columns` is present. Set false when a consumer uses
125
+ * `columns` for chip labels/formatting but renders its own add-filter UI.
126
+ */
127
+ showAddFilter?: boolean;
128
+ /** Defaults to true. Set false when a consumer renders its own applied-filter UI. */
129
+ showAppliedFilters?: boolean;
130
+ };
131
+
132
+ export interface DataTableBulkActionContext<TData> {
133
+ /**
134
+ * `row.original` for EVERY selected row across pages (resolved from the core's
135
+ * selection cache). Named `selection` to match the donor's `BulkActionContext`.
136
+ */
137
+ selection: TData[];
138
+ selectedRowIds: string[];
139
+ table: Table<TData>;
140
+ clearSelection: () => void; // table.resetRowSelection()
141
+ }
142
+
143
+ export interface DataTableLabels {
144
+ empty?: string; // 'No results'
145
+ selectAllRows?: string; // 'Select all rows'
146
+ selectRow?: (index: number) => string; // (i) => `Select row ${i + 1}`
147
+ rowActions?: string; // 'Open row actions'
148
+ columnsTrigger?: string; // 'Columns'
149
+ columnsHeading?: string; // 'Toggle columns'
150
+ addFilter?: string; // 'Add filter'
151
+ removeFilter?: (columnLabel: string) => string; // (l) => `Remove ${l} filter`
152
+ sortLabel?: (columnLabel: string) => string; // (c) => `Sort by ${c}`
153
+ filtersCollapsed?: (count: number) => string; // (n) => `${n} filters`
154
+ pagination?: {
155
+ // forwarded to TablePagination
156
+ navLabel?: string;
157
+ previousLabel?: string;
158
+ nextLabel?: string;
159
+ pageSizeLabel?: string;
160
+ };
161
+ }
162
+
163
+ export interface DataTableProps<TData> {
164
+ rows: TData[]; // current page (server) or full set (client). The core never fetches.
165
+ // biome-ignore lint/suspicious/noExplicitAny: matches TanStack's ColumnDef value-type convention
166
+ columns: ColumnDef<TData, any>[]; // `meta` passed through untouched. THE cell/header seam.
167
+ /** TanStack getRowId. Required with server pagination + rowSelection. */
168
+ getRowId?: (row: TData, index: number) => string;
169
+
170
+ isLoading?: boolean; // skeleton rows only while `rows` is empty; never blanks populated rows
171
+ skeletonRowCount?: number; // default 10
172
+
173
+ // capability configs — omit → capability + its UI vanish
174
+ pagination?: DataTablePagination;
175
+ sorting?: DataTableSorting;
176
+ filters?: DataTableFilters<TData>;
177
+ columnVisibility?: DataTableColumnVisibility;
178
+ rowSelection?: DataTableRowSelection<TData>;
179
+
180
+ // slots
181
+ /** First child of the internal `ListHeader` (title zone) — compose `PageHeader` here. */
182
+ header?: React.ReactNode;
183
+ /** Controls row (search, faceted filters, custom buttons). Render-prop gets the live table. */
184
+ toolbar?: React.ReactNode | ((table: Table<TData>) => React.ReactNode);
185
+ /** Overlay content when selection is non-empty. Only meaningful with `rowSelection` wired. */
186
+ bulkActions?: (ctx: DataTableBulkActionContext<TData>) => React.ReactNode;
187
+ /**
188
+ * Per-row actions → appends an actions column. First arg is `row.original`
189
+ * (matches the issue's `rowActions={(row) => …}`); `ctx` gives the TanStack row + table.
190
+ */
191
+ rowActions?: (row: TData, ctx: { row: Row<TData>; table: Table<TData> }) => React.ReactNode;
192
+ /**
193
+ * Right-click accelerator: wraps each row in a context menu whose content is
194
+ * the returned nodes (author with `ContextMenuItem`/`ContextMenuSeparator`
195
+ * from the context-menu atom). Unlike `rowActions`, the trigger is the row
196
+ * itself, so the consumer supplies only the menu items — the core owns the
197
+ * `ContextMenu` chrome. Additive to `rowActions`: keep the visible `...` button
198
+ * as the discoverable, keyboard- and touch-accessible path and use this as a
199
+ * power-user shortcut for the same actions. `ctx` mirrors `rowActions`.
200
+ */
201
+ contextActions?: (row: TData, ctx: { row: Row<TData>; table: Table<TData> }) => React.ReactNode;
202
+ /** Replaces the built-in "No results" cell. */
203
+ emptyState?: React.ReactNode;
204
+
205
+ /**
206
+ * Row-render seam. Overrides how each row's JSX is produced, so consumers can
207
+ * layer content the default cell grid can't express — full-width utility rows
208
+ * (a single `TableCell` with `colSpan={ctx.columnCount}`), or per-row wrappers
209
+ * (draggable / expandable rows). `ctx.defaultRow` is the built-in `<TableRow>`;
210
+ * return it to keep the default rendering for a given row. Called for every row
211
+ * in the current model. Complements `setTableOptions` (which shapes the row
212
+ * MODEL) by owning the row JSX — together they restore the donor's
213
+ * `isUtilityRow`/`renderUtilityRow`/`DraggableRow` seam without the core reading
214
+ * `meta`.
215
+ */
216
+ renderRow?: (
217
+ row: Row<TData>,
218
+ ctx: { table: Table<TData>; columnCount: number; defaultRow: React.ReactNode },
219
+ ) => React.ReactNode;
220
+
221
+ /**
222
+ * Escape hatch (donor's `setTableOptions` name kept for port recognisability):
223
+ * mutate `TableOptions` before `useReactTable` — expanded rows, drag meta,
224
+ * utility rows. Applied last.
225
+ */
226
+ setTableOptions?: (options: TableOptions<TData>) => TableOptions<TData>;
227
+
228
+ labels?: DataTableLabels;
229
+ className?: string; // outer div[data-slot="data-table"]
230
+ }
231
+
232
+ /**
233
+ * Cell/header helper types — the donor's `DataTableDisplayComponent` shape, so
234
+ * registry-resolved display components drop straight into `ColumnDef.cell`.
235
+ */
236
+ export type DataTableCellComponent<TData, TValue = unknown> = (
237
+ ctx: CellContext<TData, TValue>,
238
+ ) => React.ReactNode;
239
+ export type DataTableHeaderComponent<TData, TValue = unknown> = (
240
+ ctx: HeaderContext<TData, TValue>,
241
+ ) => React.ReactNode;
@@ -0,0 +1,73 @@
1
+ 'use client';
2
+
3
+ import type { Column, Table } from '@tanstack/react-table';
4
+ import { Button } from '@vendure-io/ui/components/atoms/button';
5
+ import {
6
+ DropdownMenu,
7
+ DropdownMenuCheckboxItem,
8
+ DropdownMenuContent,
9
+ DropdownMenuGroup,
10
+ DropdownMenuLabel,
11
+ DropdownMenuSeparator,
12
+ DropdownMenuTrigger,
13
+ } from '@vendure-io/ui/components/atoms/dropdown-menu';
14
+ import { getHideableLeafColumns } from '@vendure-io/ui/components/molecules/data-table/data-table-helpers';
15
+ import { SlidersHorizontalIcon } from 'lucide-react';
16
+
17
+ // The column-visibility gear. Lists only hideable columns (`getCanHide()`), so
18
+ // the always-on `select`/`actions` display columns never appear here. Labels
19
+ // come from a string `ColumnDef.header`, falling back to the column id — `meta`
20
+ // is deliberately never read (it belongs to the consumer, make-or-break #7).
21
+
22
+ interface DataTableViewOptionsProps<TData> {
23
+ table: Table<TData>;
24
+ triggerLabel?: string;
25
+ heading?: string;
26
+ }
27
+
28
+ function columnLabel<TData>(column: Column<TData, unknown>): string {
29
+ const header = column.columnDef.header;
30
+ return typeof header === 'string' ? header : column.id;
31
+ }
32
+
33
+ function DataTableViewOptions<TData>({
34
+ table,
35
+ triggerLabel = 'Columns',
36
+ heading = 'Toggle columns',
37
+ }: DataTableViewOptionsProps<TData>) {
38
+ const hideableColumns = getHideableLeafColumns(table);
39
+
40
+ return (
41
+ <DropdownMenu>
42
+ <DropdownMenuTrigger
43
+ data-slot="data-table-view-options"
44
+ render={
45
+ <Button variant="outline" size="sm">
46
+ <SlidersHorizontalIcon />
47
+ {triggerLabel}
48
+ </Button>
49
+ }
50
+ />
51
+ <DropdownMenuContent align="end" className="w-44">
52
+ {/* Base UI ties `GroupLabel` to a `Group` via context: the heading names
53
+ the toggle group and errors if rendered outside one. */}
54
+ <DropdownMenuGroup>
55
+ <DropdownMenuLabel>{heading}</DropdownMenuLabel>
56
+ <DropdownMenuSeparator />
57
+ {hideableColumns.map((column) => (
58
+ <DropdownMenuCheckboxItem
59
+ key={column.id}
60
+ className="capitalize"
61
+ checked={column.getIsVisible()}
62
+ onCheckedChange={(checked) => column.toggleVisibility(checked)}
63
+ >
64
+ {columnLabel(column)}
65
+ </DropdownMenuCheckboxItem>
66
+ ))}
67
+ </DropdownMenuGroup>
68
+ </DropdownMenuContent>
69
+ </DropdownMenu>
70
+ );
71
+ }
72
+
73
+ export { DataTableViewOptions };