runeforge 0.0.17 → 0.0.19

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.
@@ -1,236 +1,394 @@
1
1
  <script lang="ts" generics="T extends object = Record<string, unknown>">
2
- import { SvelteSet } from 'svelte/reactivity';
3
- import { invalidateAll } from '$app/navigation';
4
- import Button from '../../form/Button.svelte';
5
- import Header from '../../common/Header.svelte';
6
- import Modal from '../../Modal.svelte';
7
- import PaginatedTable from '../../table/PaginatedTable.svelte';
8
- import { getIconSet } from '../../../icons/context.js';
9
- import { defaultIconSet } from '../../../icons/sets/default.js';
10
- import type {
11
- ActionConfiguration,
12
- ColumnDefinition,
13
- CustomAction,
14
- RowAction
15
- } from '../../../types/crud.js';
16
- import type { FilterSnapshot, ServerPagination, SortDirection, TableQuery } from '../../../types/table.js';
17
- import { getStrings } from '../../../i18n/context.js';
18
-
19
- const strings = getStrings();
20
-
21
- let {
22
- data = [] as T[],
23
- labelOne = '',
24
- labelMany = '',
25
- icon,
26
- pageSize = 10,
27
- idKey = '_id',
28
- creation = {} as ActionConfiguration<T>,
29
- update = {} as ActionConfiguration<T>,
30
- read = {} as ActionConfiguration<T>,
31
- deletion = {} as ActionConfiguration<T>,
32
- actions = [] as CustomAction<T>[],
33
- columns = [] as ColumnDefinition<T>[],
34
- pagination = undefined as ServerPagination | undefined,
35
- initialSort = undefined as { column: string; direction: SortDirection } | undefined,
36
- initialFilters = undefined as Partial<FilterSnapshot> | undefined,
37
- onPaginationChange = undefined as ((query: TableQuery) => void) | undefined,
38
- onCreate,
39
- onEdit,
40
- onView,
41
- onAction,
42
- }: {
43
- data?: T[];
44
- labelOne?: string;
45
- labelMany?: string;
46
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
47
- icon?: any;
48
- pageSize?: number;
49
- idKey?: string;
50
- creation?: ActionConfiguration<T>;
51
- update?: ActionConfiguration<T>;
52
- read?: ActionConfiguration<T>;
53
- deletion?: ActionConfiguration<T>;
54
- actions?: CustomAction<T>[];
55
- columns?: ColumnDefinition<T>[];
56
- pagination?: ServerPagination;
57
- initialSort?: { column: string; direction: SortDirection };
58
- initialFilters?: Partial<FilterSnapshot>;
59
- onPaginationChange?: (query: TableQuery) => void;
60
- onCreate?: () => void;
61
- onEdit?: (item: T) => void;
62
- onView?: (item: T) => void;
63
- onAction?: (action: CustomAction<T>, item: T) => void;
64
- } = $props();
65
-
66
- const icons = $derived(getIconSet() ?? defaultIconSet);
67
- const entityIcon = $derived(icon ?? icons.folder);
68
-
69
- const allowRead = $derived(read.enabled ?? true);
70
- const allowUpdate = $derived(update.enabled ?? true);
71
- const allowDelete = $derived(deletion.enabled ?? true);
72
- const deleteLabel = $derived(deletion.label ?? strings.delete);
73
- const updateLabel = $derived(update.label ?? strings.edit);
74
- const readLabel = $derived(read.label ?? strings.view);
75
- const showRowActions = $derived(allowRead || allowUpdate || allowDelete || actions.length > 0);
76
-
77
- let selected = new SvelteSet<number>();
78
- let pendingDeletion = $state<T[] | null>(null);
79
-
80
- // `selected` holds indices into `data`; if `data` is swapped for a different
81
- // slice (page/sort/filter change in server mode) stale indices could point
82
- // at unrelated rows, so clear on any data reference change.
83
- let lastData: T[] | undefined;
84
- $effect(() => {
85
- if (data !== lastData) {
86
- selected.clear();
87
- lastData = data;
88
- }
89
- });
90
-
91
- async function runEndpointAction(endpoint: string, items: T[]) {
92
- await Promise.all(items.map((item) => {
93
- const fd = new FormData();
94
- fd.set('id', String((item as Record<string, unknown>)[idKey] ?? ''));
95
- return fetch(endpoint, { method: 'POST', body: fd });
96
- }));
97
- await invalidateAll();
98
- }
99
-
100
- async function runDeletion(items: T[]) {
101
- if (deletion.endpoint) {
102
- await runEndpointAction(deletion.endpoint, items);
103
- } else {
104
- await deletion.callback?.(items);
105
- }
106
- }
107
-
108
- function requestDeletion(items: T[]) {
109
- if (deletion.confirm) {
110
- pendingDeletion = items;
111
- } else {
112
- runDeletion(items);
113
- }
114
- }
115
-
116
- function handleCreate() { onCreate?.(); }
117
-
118
- function handleDelete() {
119
- const items = [...selected].map(i => data[i]);
120
- selected.clear();
121
- requestDeletion(items);
122
- }
123
-
124
- async function confirmDeletion() {
125
- if (pendingDeletion) {
126
- await runDeletion(pendingDeletion);
127
- pendingDeletion = null;
128
- }
129
- }
130
-
131
- const rowActions = $derived<RowAction<T>[]>([
132
- ...actions.map((action) => ({
133
- label: action.label,
134
- icon: action.icon,
135
- condition: action.condition,
136
- run: (item: T) => onAction?.(action, item),
137
- })),
138
- ...(allowRead ? [{ label: readLabel, icon: icons.view, run: (item: T) => onView?.(item) }] : []),
139
- ...(allowUpdate ? [{ label: updateLabel, icon: icons.edit, run: (item: T) => onEdit?.(item) }] : []),
140
- ...(allowDelete ? [{ label: deleteLabel, icon: icons.delete, class: 'text-error', run: (item: T) => requestDeletion([item]) }] : []),
141
- ]);
2
+ import { SvelteSet } from 'svelte/reactivity';
3
+ import { invalidateAll } from '$app/navigation';
4
+ import Button from '../../form/Button.svelte';
5
+ import Header from '../../common/Header.svelte';
6
+ import Modal from '../../Modal.svelte';
7
+ import PaginatedTable from '../../table/PaginatedTable.svelte';
8
+ import SearchInput from '../SearchInput.svelte';
9
+ import { downloadCsv, downloadXlsx, type XlsxModule } from '../../table/export.js';
10
+ import { getIconSet } from '../../../icons/context.js';
11
+ import { defaultIconSet } from '../../../icons/sets/default.js';
12
+ import type {
13
+ ActionConfiguration,
14
+ ColumnDefinition,
15
+ CustomAction,
16
+ CustomBulkAction,
17
+ RowAction,
18
+ SearchConfiguration
19
+ } from '../../../types/crud.js';
20
+ import type {
21
+ FilterSnapshot,
22
+ ServerPagination,
23
+ SortDirection,
24
+ TableQuery
25
+ } from '../../../types/table.js';
26
+ import { getStrings } from '../../../i18n/context.js';
27
+
28
+ const strings = getStrings();
29
+
30
+ let {
31
+ data = [] as T[],
32
+ labelOne = '',
33
+ labelMany = '',
34
+ icon,
35
+ pageSize = 10,
36
+ idKey = '_id',
37
+ creation = {} as ActionConfiguration<T>,
38
+ update = {} as ActionConfiguration<T>,
39
+ read = {} as ActionConfiguration<T>,
40
+ deletion = {} as ActionConfiguration<T>,
41
+ actions = [] as CustomAction<T>[],
42
+ customBulkActions = [] as CustomBulkAction<T>[],
43
+ search = undefined as SearchConfiguration | undefined,
44
+ columns = [] as ColumnDefinition<T>[],
45
+ pagination = undefined as ServerPagination | undefined,
46
+ initialSort = undefined as { column: string; direction: SortDirection } | undefined,
47
+ initialFilters = undefined as Partial<FilterSnapshot> | undefined,
48
+ onPaginationChange = undefined as ((query: TableQuery) => void) | undefined,
49
+ enableExport = false,
50
+ onExport = undefined as ((query: TableQuery) => Promise<T[]>) | undefined,
51
+ xlsx = undefined as XlsxModule | undefined,
52
+ onCreate,
53
+ onEdit,
54
+ onView,
55
+ onAction
56
+ }: {
57
+ data?: T[];
58
+ labelOne?: string;
59
+ labelMany?: string;
60
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
61
+ icon?: any;
62
+ pageSize?: number;
63
+ idKey?: string;
64
+ creation?: ActionConfiguration<T>;
65
+ update?: ActionConfiguration<T>;
66
+ read?: ActionConfiguration<T>;
67
+ deletion?: ActionConfiguration<T>;
68
+ actions?: CustomAction<T>[];
69
+ customBulkActions?: CustomBulkAction<T>[];
70
+ search?: SearchConfiguration;
71
+ columns?: ColumnDefinition<T>[];
72
+ pagination?: ServerPagination;
73
+ initialSort?: { column: string; direction: SortDirection };
74
+ initialFilters?: Partial<FilterSnapshot>;
75
+ onPaginationChange?: (query: TableQuery) => void;
76
+ /** Shows an icon-only export button (CSV, and Excel if `xlsx` is provided). */
77
+ enableExport?: boolean;
78
+ /** Server-pagination mode only: fetch all rows matching the current query
79
+ * (unpaginated) for export. Without it, export falls back to the loaded page. */
80
+ onExport?: (query: TableQuery) => Promise<T[]>;
81
+ /** Resolved `xlsx` (SheetJS) module, e.g. `import * as xlsx from 'xlsx'`.
82
+ * Enables the "Export as Excel" option; omit to only offer CSV. */
83
+ xlsx?: XlsxModule;
84
+ onCreate?: () => void;
85
+ onEdit?: (item: T) => void;
86
+ onView?: (item: T) => void;
87
+ onAction?: (action: CustomAction<T>, item: T) => void;
88
+ } = $props();
89
+
90
+ const icons = $derived(getIconSet() ?? defaultIconSet);
91
+ const entityIcon = $derived(icon ?? icons.folder);
92
+
93
+ const allowRead = $derived(read.enabled ?? true);
94
+ const allowUpdate = $derived(update.enabled ?? true);
95
+ const allowDelete = $derived(deletion.enabled ?? true);
96
+ const deleteLabel = $derived(deletion.label ?? strings.delete);
97
+ const updateLabel = $derived(update.label ?? strings.edit);
98
+ const readLabel = $derived(read.label ?? strings.view);
99
+ const showRowActions = $derived(allowRead || allowUpdate || allowDelete || actions.length > 0);
100
+ const allowSelection = $derived(allowDelete || customBulkActions.length > 0);
101
+
102
+ let selected = new SvelteSet<number>();
103
+ let pendingDeletion = $state<T[] | null>(null);
104
+ let pendingBulkAction = $state<{ action: CustomBulkAction<T>; items: T[] } | null>(null);
105
+ const selectedItems = $derived([...selected].map((i) => data[i]));
106
+
107
+ let visibleRows = $state<T[]>([]);
108
+ let exportQuery = $state<TableQuery | undefined>(undefined);
109
+ let exportPopoverEl: HTMLElement | undefined = $state();
110
+ const exportId = $props.id();
111
+
112
+ async function resolveExportRows(): Promise<T[]> {
113
+ if (!pagination) return visibleRows;
114
+ if (!onExport || !exportQuery) return data;
115
+ return onExport(exportQuery);
116
+ }
117
+
118
+ async function handleExport(format: 'csv' | 'xlsx') {
119
+ exportPopoverEl?.hidePopover();
120
+ const rows = await resolveExportRows();
121
+ const filename = `${labelMany || 'export'}-${new Date().toISOString().slice(0, 10)}`;
122
+ if (format === 'csv') downloadCsv(rows, columns, filename);
123
+ else if (xlsx) downloadXlsx(rows, columns, filename, xlsx);
124
+ }
125
+
126
+ // `selected` holds indices into `data`; if `data` is swapped for a different
127
+ // slice (page/sort/filter change in server mode) stale indices could point
128
+ // at unrelated rows, so clear on any data reference change.
129
+ let lastData: T[] | undefined;
130
+ $effect(() => {
131
+ if (data !== lastData) {
132
+ selected.clear();
133
+ lastData = data;
134
+ }
135
+ });
136
+
137
+ async function runEndpointAction(endpoint: string, items: T[]) {
138
+ await Promise.all(
139
+ items.map((item) => {
140
+ const fd = new FormData();
141
+ fd.set('id', String((item as Record<string, unknown>)[idKey] ?? ''));
142
+ return fetch(endpoint, { method: 'POST', body: fd });
143
+ })
144
+ );
145
+ await invalidateAll();
146
+ }
147
+
148
+ async function runDeletion(items: T[]) {
149
+ if (deletion.endpoint) {
150
+ await runEndpointAction(deletion.endpoint, items);
151
+ } else {
152
+ await deletion.callback?.(items);
153
+ }
154
+ }
155
+
156
+ function requestDeletion(items: T[]) {
157
+ if (deletion.confirm) {
158
+ pendingDeletion = items;
159
+ } else {
160
+ runDeletion(items);
161
+ }
162
+ }
163
+
164
+ function handleCreate() {
165
+ onCreate?.();
166
+ }
167
+
168
+ function handleDelete() {
169
+ const items = [...selected].map((i) => data[i]);
170
+ selected.clear();
171
+ requestDeletion(items);
172
+ }
173
+
174
+ async function confirmDeletion() {
175
+ if (pendingDeletion) {
176
+ await runDeletion(pendingDeletion);
177
+ pendingDeletion = null;
178
+ }
179
+ }
180
+
181
+ async function runBulkAction(action: CustomBulkAction<T>, items: T[]) {
182
+ await runEndpointAction(action.endpoint, items);
183
+ }
184
+
185
+ function requestBulkAction(action: CustomBulkAction<T>, items: T[]) {
186
+ if (action.confirm) {
187
+ pendingBulkAction = { action, items };
188
+ } else {
189
+ runBulkAction(action, items);
190
+ }
191
+ }
192
+
193
+ function handleBulkAction(action: CustomBulkAction<T>) {
194
+ const items = selectedItems;
195
+ selected.clear();
196
+ requestBulkAction(action, items);
197
+ }
198
+
199
+ async function confirmBulkAction() {
200
+ if (pendingBulkAction) {
201
+ await runBulkAction(pendingBulkAction.action, pendingBulkAction.items);
202
+ pendingBulkAction = null;
203
+ }
204
+ }
205
+
206
+ const rowActions = $derived<RowAction<T>[]>([
207
+ ...actions.map((action) => ({
208
+ label: action.label,
209
+ icon: action.icon,
210
+ condition: action.condition,
211
+ run: (item: T) => onAction?.(action, item)
212
+ })),
213
+ ...(allowRead
214
+ ? [{ label: readLabel, icon: icons.view, run: (item: T) => onView?.(item) }]
215
+ : []),
216
+ ...(allowUpdate
217
+ ? [{ label: updateLabel, icon: icons.edit, run: (item: T) => onEdit?.(item) }]
218
+ : []),
219
+ ...(allowDelete
220
+ ? [
221
+ {
222
+ label: deleteLabel,
223
+ icon: icons.delete,
224
+ class: 'text-error',
225
+ run: (item: T) => requestDeletion([item])
226
+ }
227
+ ]
228
+ : [])
229
+ ]);
142
230
  </script>
143
231
 
144
232
  {#snippet actionsCell(item: T)}
145
- <div class="flex justify-end gap-1">
146
- {#each rowActions as action (action.label)}
147
- {#if action.condition?.(item) ?? true}
148
- {@const Icon = action.icon}
149
- <Button
150
- variant="ghost"
151
- class={['btn-xs', action.class]}
152
- title={action.label}
153
- aria-label={action.label}
154
- onclick={(e) => { e.stopPropagation(); action.run(item); }}
155
- >
156
- <Icon class="size-4" />
157
- </Button>
158
- {/if}
159
- {/each}
160
- </div>
233
+ <div class="flex justify-end gap-1">
234
+ {#each rowActions as action (action.label)}
235
+ {#if action.condition?.(item) ?? true}
236
+ {@const Icon = action.icon}
237
+ <Button
238
+ variant="ghost"
239
+ class={['btn-xs', action.class]}
240
+ title={action.label}
241
+ aria-label={action.label}
242
+ onclick={(e) => {
243
+ e.stopPropagation();
244
+ action.run(item);
245
+ }}
246
+ >
247
+ <Icon class="size-4" />
248
+ </Button>
249
+ {/if}
250
+ {/each}
251
+ </div>
161
252
  {/snippet}
162
253
 
163
254
  <div class="flex flex-col gap-6">
255
+ <Header
256
+ title={labelMany}
257
+ breadcrumbs={[{ label: labelMany, icon: entityIcon, link: { href: '#' }, prominent: true }]}
258
+ >
259
+ {#snippet buttons()}
260
+ {#if search}
261
+ <SearchInput config={search} />
262
+ {/if}
164
263
 
165
- <Header
166
- title={labelMany}
167
- breadcrumbs={[
168
- { label: labelMany, icon: entityIcon, link: { href: '#' }, prominent: true },
169
- ]}
170
- >
171
- {#snippet buttons()}
172
- {#if allowDelete}
173
- {@const DeleteIcon = icons.delete}
174
- <Button
175
- variant="error"
176
- class="btn-outline"
177
- disabled={selected.size === 0}
178
- onclick={handleDelete}
179
- >
180
- <DeleteIcon class="size-4" />
181
- {deleteLabel} ({selected.size})
182
- </Button>
183
- {/if}
184
-
185
- {@const CreateIcon = icons.create}
186
- <Button
187
- variant="primary"
188
- onclick={handleCreate}
189
- >
190
- <CreateIcon class="size-5" />
191
- {#if creation.label}
192
- <span>{creation.label}</span>
193
- {:else}
194
- <span>{strings.create}<span class="hidden sm:inline">&nbsp;{labelOne}</span></span>
195
- {/if}
196
- </Button>
197
- {/snippet}
198
- </Header>
199
-
200
- <div class="table-wrapper">
201
- <PaginatedTable
202
- {data}
203
- {columns}
204
- {pageSize}
205
- selectable={allowDelete}
206
- {selected}
207
- rowActions={showRowActions ? actionsCell : undefined}
208
- {pagination}
209
- {initialSort}
210
- {initialFilters}
211
- {onPaginationChange}
212
- />
213
- </div>
264
+ {#if enableExport}
265
+ {@const ExportIcon = icons.download}
266
+ <Button
267
+ variant="ghost"
268
+ class="btn-square"
269
+ popovertarget="export-menu-{exportId}"
270
+ style="anchor-name:--export-anchor-{exportId}"
271
+ aria-label={strings.export}
272
+ title={strings.export}
273
+ >
274
+ <ExportIcon class="size-4" />
275
+ </Button>
276
+ <div
277
+ popover="auto"
278
+ id="export-menu-{exportId}"
279
+ style="position-anchor:--export-anchor-{exportId}"
280
+ class="dropdown dropdown-end w-40 rounded-box border border-base-content/10 bg-base-100 p-1 shadow-lg"
281
+ bind:this={exportPopoverEl}
282
+ >
283
+ <Button
284
+ variant="ghost"
285
+ class="btn-sm w-full justify-start"
286
+ onclick={() => handleExport('csv')}
287
+ >
288
+ {strings.exportCsv}
289
+ </Button>
290
+ {#if xlsx}
291
+ <Button
292
+ variant="ghost"
293
+ class="btn-sm w-full justify-start"
294
+ onclick={() => handleExport('xlsx')}
295
+ >
296
+ {strings.exportExcel}
297
+ </Button>
298
+ {/if}
299
+ </div>
300
+ {/if}
301
+
302
+ {#each customBulkActions as bulkAction (bulkAction.label)}
303
+ {#if bulkAction.condition?.(selectedItems) ?? true}
304
+ {@const BulkIcon = bulkAction.icon}
305
+ <Button
306
+ variant={bulkAction.variant ?? 'ghost'}
307
+ class="btn-outline"
308
+ disabled={selected.size === 0}
309
+ onclick={() => handleBulkAction(bulkAction)}
310
+ >
311
+ <BulkIcon class="size-4" />
312
+ {bulkAction.label} ({selected.size})
313
+ </Button>
314
+ {/if}
315
+ {/each}
316
+
317
+ {#if allowDelete}
318
+ {@const DeleteIcon = icons.delete}
319
+ <Button
320
+ variant="error"
321
+ class="btn-outline"
322
+ disabled={selected.size === 0}
323
+ onclick={handleDelete}
324
+ >
325
+ <DeleteIcon class="size-4" />
326
+ {deleteLabel} ({selected.size})
327
+ </Button>
328
+ {/if}
329
+
330
+ {@const CreateIcon = icons.create}
331
+ <Button variant="primary" onclick={handleCreate}>
332
+ <CreateIcon class="size-5" />
333
+ {#if creation.label}
334
+ <span>{creation.label}</span>
335
+ {:else}
336
+ <span>{strings.create}<span class="hidden sm:inline">&nbsp;{labelOne}</span></span>
337
+ {/if}
338
+ </Button>
339
+ {/snippet}
340
+ </Header>
341
+
342
+ <div class="table-wrapper">
343
+ <PaginatedTable
344
+ {data}
345
+ {columns}
346
+ {pageSize}
347
+ selectable={allowSelection}
348
+ {selected}
349
+ rowActions={showRowActions ? actionsCell : undefined}
350
+ {pagination}
351
+ {initialSort}
352
+ {initialFilters}
353
+ {onPaginationChange}
354
+ bind:visibleRows
355
+ bind:query={exportQuery}
356
+ />
357
+ </div>
214
358
  </div>
215
359
 
216
360
  {#if pendingDeletion !== null}
217
- <Modal title={deleteLabel} onClose={() => (pendingDeletion = null)}>
218
- <p>{strings.deleteConfirm(pendingDeletion.length, deleteLabel)}</p>
219
- <div class="flex justify-end gap-2 mt-4">
220
- <Button variant="ghost" onclick={() => (pendingDeletion = null)}>
221
- {strings.cancel}
222
- </Button>
223
- <Button variant="error" onclick={confirmDeletion}>
224
- {strings.confirm}
225
- </Button>
226
- </div>
227
- </Modal>
361
+ <Modal title={deleteLabel} onClose={() => (pendingDeletion = null)}>
362
+ <p>{strings.deleteConfirm(pendingDeletion.length, deleteLabel)}</p>
363
+ <div class="flex justify-end gap-2 mt-4">
364
+ <Button variant="ghost" onclick={() => (pendingDeletion = null)}>
365
+ {strings.cancel}
366
+ </Button>
367
+ <Button variant="error" onclick={confirmDeletion}>
368
+ {strings.confirm}
369
+ </Button>
370
+ </div>
371
+ </Modal>
372
+ {/if}
373
+
374
+ {#if pendingBulkAction !== null}
375
+ <Modal title={pendingBulkAction.action.label} onClose={() => (pendingBulkAction = null)}>
376
+ <p>{strings.deleteConfirm(pendingBulkAction.items.length, pendingBulkAction.action.label)}</p>
377
+ <div class="flex justify-end gap-2 mt-4">
378
+ <Button variant="ghost" onclick={() => (pendingBulkAction = null)}>
379
+ {strings.cancel}
380
+ </Button>
381
+ <Button variant={pendingBulkAction.action.variant ?? 'primary'} onclick={confirmBulkAction}>
382
+ {strings.confirm}
383
+ </Button>
384
+ </div>
385
+ </Modal>
228
386
  {/if}
229
387
 
230
388
  <style>
231
- .table-wrapper {
232
- width: 100%;
233
- max-width: var(--runeforge-crud-max-width);
234
- margin-inline: auto;
235
- }
389
+ .table-wrapper {
390
+ width: 100%;
391
+ max-width: var(--runeforge-crud-max-width);
392
+ margin-inline: auto;
393
+ }
236
394
  </style>
@@ -1,4 +1,5 @@
1
- import type { ActionConfiguration, ColumnDefinition, CustomAction } from '../../../types/crud.js';
1
+ import { type XlsxModule } from '../../table/export.js';
2
+ import type { ActionConfiguration, ColumnDefinition, CustomAction, CustomBulkAction, SearchConfiguration } from '../../../types/crud.js';
2
3
  import type { FilterSnapshot, ServerPagination, SortDirection, TableQuery } from '../../../types/table.js';
3
4
  declare function $$render<T extends object = Record<string, unknown>>(): {
4
5
  props: {
@@ -13,6 +14,8 @@ declare function $$render<T extends object = Record<string, unknown>>(): {
13
14
  read?: ActionConfiguration<T>;
14
15
  deletion?: ActionConfiguration<T>;
15
16
  actions?: CustomAction<T>[];
17
+ customBulkActions?: CustomBulkAction<T>[];
18
+ search?: SearchConfiguration;
16
19
  columns?: ColumnDefinition<T>[];
17
20
  pagination?: ServerPagination;
18
21
  initialSort?: {
@@ -21,6 +24,14 @@ declare function $$render<T extends object = Record<string, unknown>>(): {
21
24
  };
22
25
  initialFilters?: Partial<FilterSnapshot>;
23
26
  onPaginationChange?: (query: TableQuery) => void;
27
+ /** Shows an icon-only export button (CSV, and Excel if `xlsx` is provided). */
28
+ enableExport?: boolean;
29
+ /** Server-pagination mode only: fetch all rows matching the current query
30
+ * (unpaginated) for export. Without it, export falls back to the loaded page. */
31
+ onExport?: (query: TableQuery) => Promise<T[]>;
32
+ /** Resolved `xlsx` (SheetJS) module, e.g. `import * as xlsx from 'xlsx'`.
33
+ * Enables the "Export as Excel" option; omit to only offer CSV. */
34
+ xlsx?: XlsxModule;
24
35
  onCreate?: () => void;
25
36
  onEdit?: (item: T) => void;
26
37
  onView?: (item: T) => void;