runeforge 0.0.53 → 0.0.55

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 (78) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +1342 -1222
  3. package/dist/components/Avatar.svelte +31 -31
  4. package/dist/components/IconRenderer.svelte +22 -22
  5. package/dist/components/Modal.svelte +75 -75
  6. package/dist/components/common/Header.svelte +40 -40
  7. package/dist/components/crud/EmbeddedField.svelte +175 -175
  8. package/dist/components/crud/Field.svelte +376 -376
  9. package/dist/components/crud/GenericCRUD.svelte +426 -435
  10. package/dist/components/crud/GenericCRUD.svelte.d.ts +6 -9
  11. package/dist/components/crud/SearchInput.svelte +53 -53
  12. package/dist/components/crud/columns/Avatar.svelte +15 -15
  13. package/dist/components/crud/columns/Icon.svelte +8 -8
  14. package/dist/components/crud/utils/reorder.d.ts +4 -0
  15. package/dist/components/crud/utils/reorder.js +13 -0
  16. package/dist/components/crud/views/Create.svelte +232 -232
  17. package/dist/components/crud/views/Read.svelte +124 -124
  18. package/dist/components/crud/views/Update.svelte +208 -208
  19. package/dist/components/crud/views/list/List.svelte +291 -0
  20. package/dist/components/crud/views/{List.svelte.d.ts → list/List.svelte.d.ts} +4 -14
  21. package/dist/components/crud/views/list/Modals.svelte +56 -0
  22. package/dist/components/crud/views/list/Modals.svelte.d.ts +36 -0
  23. package/dist/components/crud/views/list/Table.svelte +176 -0
  24. package/dist/components/crud/views/list/Table.svelte.d.ts +59 -0
  25. package/dist/components/crud/views/list/Toolbar.svelte +341 -0
  26. package/dist/components/crud/views/list/Toolbar.svelte.d.ts +46 -0
  27. package/dist/components/form/Button.svelte +27 -27
  28. package/dist/components/form/Label.svelte +37 -37
  29. package/dist/components/form/MultiSelect.svelte +248 -248
  30. package/dist/components/form/PasswordInput.svelte +68 -68
  31. package/dist/components/form/Required.svelte +1 -1
  32. package/dist/components/form/Select.svelte +209 -209
  33. package/dist/components/form/Tree.svelte +62 -62
  34. package/dist/components/form/TreeNode.svelte +66 -66
  35. package/dist/components/navigation/Breadcrumbs.svelte +111 -111
  36. package/dist/components/table/ColumnFilter.svelte +168 -168
  37. package/dist/components/table/PaginatedTable.svelte +536 -215
  38. package/dist/components/table/PaginatedTable.svelte.d.ts +11 -2
  39. package/dist/components/table/Paginator.svelte +113 -113
  40. package/dist/components/table/SortHeader.svelte +43 -43
  41. package/dist/components/table/TableBody.svelte +150 -70
  42. package/dist/components/table/TableBody.svelte.d.ts +14 -1
  43. package/dist/components/table/TableHeader.svelte +88 -83
  44. package/dist/components/table/TableHeader.svelte.d.ts +1 -0
  45. package/dist/components/table/sortable.d.ts +27 -0
  46. package/dist/components/table/sortable.js +55 -0
  47. package/dist/components/table/utils.d.ts +28 -1
  48. package/dist/components/table/utils.js +75 -0
  49. package/dist/i18n/en.js +2 -0
  50. package/dist/i18n/es.js +2 -0
  51. package/dist/i18n/types.d.ts +2 -0
  52. package/dist/icons/defaults/Clear.svelte +6 -6
  53. package/dist/icons/defaults/Create.svelte +6 -6
  54. package/dist/icons/defaults/Delete.svelte +6 -6
  55. package/dist/icons/defaults/Download.svelte +7 -7
  56. package/dist/icons/defaults/Edit.svelte +7 -7
  57. package/dist/icons/defaults/Filter.svelte +6 -6
  58. package/dist/icons/defaults/FilterActive.svelte +6 -6
  59. package/dist/icons/defaults/Folder.svelte +6 -6
  60. package/dist/icons/defaults/Grip.svelte +7 -0
  61. package/dist/icons/defaults/Grip.svelte.d.ts +7 -0
  62. package/dist/icons/defaults/Home.svelte +6 -6
  63. package/dist/icons/defaults/PasswordHide.svelte +9 -9
  64. package/dist/icons/defaults/PasswordShow.svelte +7 -7
  65. package/dist/icons/defaults/SortAsc.svelte +6 -6
  66. package/dist/icons/defaults/SortDesc.svelte +6 -6
  67. package/dist/icons/defaults/SortNone.svelte +6 -6
  68. package/dist/icons/defaults/View.svelte +7 -7
  69. package/dist/icons/sets/bootstrap.js +2 -1
  70. package/dist/icons/sets/default.js +2 -0
  71. package/dist/icons/types.d.ts +3 -0
  72. package/dist/index.d.ts +5 -4
  73. package/dist/index.js +3 -2
  74. package/dist/types/attribute.d.ts +9 -0
  75. package/dist/types/crud.d.ts +72 -1
  76. package/dist/types/table.d.ts +56 -0
  77. package/package.json +3 -1
  78. package/dist/components/crud/views/List.svelte +0 -584
@@ -1,584 +0,0 @@
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 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
- ViewBasedCustomBulkAction
20
- } from '../../../types/crud.js';
21
- import type {
22
- FilterSnapshot,
23
- ServerPagination,
24
- SortDirection,
25
- TableQuery
26
- } from '../../../types/table.js';
27
- import { getStrings } from '../../../i18n/context.js';
28
-
29
- const strings = getStrings();
30
-
31
- function isViewBasedBulkAction(
32
- action: CustomBulkAction<T>
33
- ): action is ViewBasedCustomBulkAction<T> {
34
- return action.kind === 'view';
35
- }
36
-
37
- let {
38
- data = [] as T[],
39
- labelOne = '',
40
- labelMany = '',
41
- icon,
42
- pageSize = 10,
43
- idKey = '_id',
44
- creation = {} as ActionConfiguration<T>,
45
- update = {} as ActionConfiguration<T>,
46
- read = {} as ActionConfiguration<T>,
47
- deletion = {} as ActionConfiguration<T>,
48
- actions = [] as CustomAction<T>[],
49
- customBulkActions = [] as CustomBulkAction<T>[],
50
- search = undefined as SearchConfiguration | undefined,
51
- columns = [] as ColumnDefinition<T>[],
52
- pagination = undefined as ServerPagination | undefined,
53
- initialSort = undefined as { column: string; direction: SortDirection } | undefined,
54
- initialFilters = undefined as Partial<FilterSnapshot> | undefined,
55
- onPaginationChange = undefined as ((query: TableQuery) => void) | undefined,
56
- enableExport = false,
57
- onExport = undefined as ((query: TableQuery) => Promise<T[]>) | undefined,
58
- xlsx = undefined as XlsxModule | undefined,
59
- onCreate,
60
- onEdit,
61
- onView,
62
- onAction,
63
- onBulkAction
64
- }: {
65
- data?: T[];
66
- labelOne?: string;
67
- labelMany?: string;
68
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
69
- icon?: any;
70
- pageSize?: number;
71
- idKey?: string;
72
- creation?: ActionConfiguration<T>;
73
- update?: ActionConfiguration<T>;
74
- read?: ActionConfiguration<T>;
75
- deletion?: ActionConfiguration<T>;
76
- actions?: CustomAction<T>[];
77
- customBulkActions?: CustomBulkAction<T>[];
78
- search?: SearchConfiguration;
79
- columns?: ColumnDefinition<T>[];
80
- pagination?: ServerPagination;
81
- initialSort?: { column: string; direction: SortDirection };
82
- initialFilters?: Partial<FilterSnapshot>;
83
- onPaginationChange?: (query: TableQuery) => void;
84
- /** Shows an icon-only export button (CSV, and Excel if `xlsx` is provided). */
85
- enableExport?: boolean;
86
- /** Server-pagination mode only: fetch all rows matching the current query
87
- * (unpaginated) for export. Without it, export falls back to the loaded page. */
88
- onExport?: (query: TableQuery) => Promise<T[]>;
89
- /** Resolved `xlsx` (SheetJS) module, e.g. `import * as xlsx from 'xlsx'`.
90
- * Enables the "Export as Excel" option; omit to only offer CSV. */
91
- xlsx?: XlsxModule;
92
- onCreate?: () => void;
93
- onEdit?: (item: T) => void;
94
- onView?: (item: T) => void;
95
- onAction?: (action: CustomAction<T>, item: T) => void;
96
- onBulkAction?: (action: ViewBasedCustomBulkAction<T>, items: T[]) => void;
97
- } = $props();
98
-
99
- const icons = $derived(getIconSet() ?? defaultIconSet);
100
- const entityIcon = $derived(icon ?? icons.folder);
101
-
102
- const allowRead = $derived(read.enabled ?? true);
103
- const allowUpdate = $derived(update.enabled ?? true);
104
- const allowDelete = $derived(deletion.enabled ?? true);
105
- const allowCreate = $derived(creation.enabled ?? true);
106
- const deleteLabel = $derived(deletion.label ?? strings.delete);
107
- const updateLabel = $derived(update.label ?? strings.edit);
108
- const readLabel = $derived(read.label ?? strings.view);
109
- const showRowActions = $derived(allowRead || allowUpdate || allowDelete || actions.length > 0);
110
- const allowSelection = $derived(
111
- allowDelete || customBulkActions.some((action) => !isViewBasedBulkAction(action))
112
- );
113
-
114
- let selected = new SvelteSet<number>();
115
- let pendingDeletion = $state<T[] | null>(null);
116
- let pendingBulkAction = $state<{ action: CustomBulkAction<T>; items: T[] } | null>(null);
117
- const selectedItems = $derived([...selected].map((i) => data[i]));
118
-
119
- let visibleRows = $state<T[]>([]);
120
- let exportQuery = $state<TableQuery | undefined>(undefined);
121
- let exportPopoverEl: HTMLElement | undefined = $state();
122
- let actionsPopoverEl: HTMLElement | undefined = $state();
123
- const exportId = $props.id();
124
- const actionsMenuId = `${exportId}-actions`;
125
-
126
- // Collapses export/bulk-actions/delete into a single "Acciones" dropdown
127
- // once the toolbar no longer fits its available width (small monitor, a
128
- // browser window not maximized, many customBulkActions, ...) — measured
129
- // against an identical but off-flow, always-uncollapsed clone rather than
130
- // the visible row itself, since the visible row's own width changes
131
- // depending on whether it's currently collapsed.
132
- let toolbarEl: HTMLElement | undefined = $state();
133
- let toolbarMeasureEl: HTMLElement | undefined = $state();
134
- let toolbarCollapsed = $state(false);
135
-
136
- $effect(() => {
137
- if (!toolbarEl || !toolbarMeasureEl) return;
138
- const el = toolbarEl;
139
- const measureEl = toolbarMeasureEl;
140
- const check = () => {
141
- toolbarCollapsed = measureEl.scrollWidth > el.clientWidth + 1;
142
- };
143
- const observer = new ResizeObserver(check);
144
- observer.observe(el);
145
- observer.observe(measureEl);
146
- check();
147
- return () => observer.disconnect();
148
- });
149
-
150
- async function resolveExportRows(): Promise<T[]> {
151
- if (!pagination) return visibleRows;
152
- if (!onExport || !exportQuery) return data;
153
- return onExport(exportQuery);
154
- }
155
-
156
- async function handleExport(format: 'csv' | 'xlsx') {
157
- exportPopoverEl?.hidePopover();
158
- const rows = await resolveExportRows();
159
- const filename = `${labelMany || 'export'}-${new Date().toISOString().slice(0, 10)}`;
160
- if (format === 'csv') downloadCsv(rows, columns, filename);
161
- else if (xlsx) downloadXlsx(rows, columns, filename, xlsx);
162
- }
163
-
164
- // `selected` holds indices into `data`; if `data` is swapped for a different
165
- // slice (page/sort/filter change in server mode) stale indices could point
166
- // at unrelated rows, so clear on any data reference change.
167
- let lastData: T[] | undefined;
168
- $effect(() => {
169
- if (data !== lastData) {
170
- selected.clear();
171
- lastData = data;
172
- }
173
- });
174
-
175
- async function runEndpointAction(endpoint: string, items: T[]) {
176
- await Promise.all(
177
- items.map((item) => {
178
- const fd = new FormData();
179
- fd.set('id', String((item as Record<string, unknown>)[idKey] ?? ''));
180
- return fetch(endpoint, { method: 'POST', body: fd });
181
- })
182
- );
183
- await invalidateAll();
184
- }
185
-
186
- async function runDeletion(items: T[]) {
187
- if (deletion.endpoint) {
188
- await runEndpointAction(deletion.endpoint, items);
189
- } else {
190
- await deletion.callback?.(items);
191
- }
192
- }
193
-
194
- function requestDeletion(items: T[]) {
195
- if (deletion.confirm) {
196
- pendingDeletion = items;
197
- } else {
198
- runDeletion(items);
199
- }
200
- }
201
-
202
- function handleCreate() {
203
- onCreate?.();
204
- }
205
-
206
- function handleDelete() {
207
- const items = [...selected].map((i) => data[i]);
208
- selected.clear();
209
- requestDeletion(items);
210
- }
211
-
212
- async function confirmDeletion() {
213
- if (pendingDeletion) {
214
- await runDeletion(pendingDeletion);
215
- pendingDeletion = null;
216
- }
217
- }
218
-
219
- async function runBulkAction(action: CustomBulkAction<T>, items: T[]) {
220
- if (isViewBasedBulkAction(action)) return;
221
- await runEndpointAction(action.endpoint, items);
222
- }
223
-
224
- function requestBulkAction(action: CustomBulkAction<T>, items: T[]) {
225
- if (!isViewBasedBulkAction(action) && action.confirm) {
226
- pendingBulkAction = { action, items };
227
- } else {
228
- runBulkAction(action, items);
229
- }
230
- }
231
-
232
- function handleBulkAction(action: CustomBulkAction<T>) {
233
- const items = selectedItems;
234
- if (isViewBasedBulkAction(action)) {
235
- onBulkAction?.(action, items);
236
- return;
237
- }
238
- selected.clear();
239
- requestBulkAction(action, items);
240
- }
241
-
242
- async function confirmBulkAction() {
243
- if (pendingBulkAction) {
244
- await runBulkAction(pendingBulkAction.action, pendingBulkAction.items);
245
- pendingBulkAction = null;
246
- }
247
- }
248
-
249
- const rowActions = $derived<RowAction<T>[]>([
250
- ...actions.map((action) => ({
251
- label: action.label,
252
- icon: action.icon,
253
- toggle: action.toggle,
254
- class: action.class,
255
- condition: action.condition,
256
- run: (item: T) => onAction?.(action, item)
257
- })),
258
- ...(allowRead
259
- ? [{ label: readLabel, icon: icons.view, run: (item: T) => onView?.(item) }]
260
- : []),
261
- ...(allowUpdate
262
- ? [{ label: updateLabel, icon: icons.edit, run: (item: T) => onEdit?.(item) }]
263
- : []),
264
- ...(allowDelete
265
- ? [
266
- {
267
- label: deleteLabel,
268
- icon: icons.delete,
269
- class: 'text-error',
270
- run: (item: T) => requestDeletion([item])
271
- }
272
- ]
273
- : [])
274
- ]);
275
- </script>
276
-
277
- {#snippet toolbarButtons(measuring: boolean)}
278
- {#if search}
279
- <SearchInput config={search} />
280
- {/if}
281
-
282
- {#if enableExport}
283
- {@const ExportIcon = icons.download}
284
- <Button
285
- variant="ghost"
286
- class="btn-square"
287
- popovertarget={measuring ? undefined : `export-menu-${exportId}`}
288
- style={measuring ? undefined : `anchor-name:--export-anchor-${exportId}`}
289
- aria-label={strings.export}
290
- title={strings.export}
291
- >
292
- <ExportIcon class="size-4" />
293
- </Button>
294
- {#if !measuring}
295
- <div
296
- popover="auto"
297
- id="export-menu-{exportId}"
298
- style="position-anchor:--export-anchor-{exportId}"
299
- class="dropdown dropdown-end w-40 rounded-box border border-base-content/10 bg-base-100 p-1 shadow-lg"
300
- bind:this={exportPopoverEl}
301
- >
302
- <Button
303
- variant="ghost"
304
- class="btn-sm w-full justify-start"
305
- onclick={() => handleExport('csv')}
306
- >
307
- {strings.exportCsv}
308
- </Button>
309
- {#if xlsx}
310
- <Button
311
- variant="ghost"
312
- class="btn-sm w-full justify-start"
313
- onclick={() => handleExport('xlsx')}
314
- >
315
- {strings.exportExcel}
316
- </Button>
317
- {/if}
318
- </div>
319
- {/if}
320
- {/if}
321
-
322
- {#each customBulkActions as bulkAction, i (i)}
323
- {#if bulkAction.condition?.(selectedItems) ?? true}
324
- {@const BulkIcon = bulkAction.icon}
325
- {@const isView = isViewBasedBulkAction(bulkAction)}
326
- <Button
327
- variant={bulkAction.variant ?? 'ghost'}
328
- class="btn-outline"
329
- disabled={!isView && selected.size === 0}
330
- title={bulkAction.tooltip ?? bulkAction.label}
331
- aria-label={bulkAction.label ? undefined : bulkAction.tooltip}
332
- onclick={measuring ? undefined : () => handleBulkAction(bulkAction)}
333
- >
334
- <BulkIcon class="size-4" />
335
- {#if bulkAction.label}
336
- {bulkAction.label}{#if !isView}
337
- &nbsp;({selected.size})
338
- {/if}
339
- {/if}
340
- </Button>
341
- {/if}
342
- {/each}
343
-
344
- {#if allowDelete}
345
- {@const DeleteIcon = icons.delete}
346
- <Button
347
- variant="error"
348
- class="btn-outline"
349
- disabled={selected.size === 0}
350
- onclick={measuring ? undefined : handleDelete}
351
- >
352
- <DeleteIcon class="size-4" />
353
- {deleteLabel} ({selected.size})
354
- </Button>
355
- {/if}
356
-
357
- {#if allowCreate}
358
- {@const CreateIcon = icons.create}
359
- <Button variant="primary" onclick={measuring ? undefined : handleCreate}>
360
- <CreateIcon class="size-5" />
361
- {#if creation.label}
362
- <span>{creation.label}</span>
363
- {:else}
364
- <span>{strings.create}<span class="hidden sm:inline">&nbsp;{labelOne}</span></span>
365
- {/if}
366
- </Button>
367
- {/if}
368
- {/snippet}
369
-
370
- {#snippet actionsCell(item: T)}
371
- <div class="flex justify-end items-center gap-1">
372
- {#each rowActions as action (action.label)}
373
- {#if action.condition?.(item) ?? true}
374
- {@const resolvedClass =
375
- typeof action.class === 'function' ? action.class(item) : action.class}
376
- {#if action.toggle}
377
- <input
378
- type="checkbox"
379
- class={['toggle toggle-sm', resolvedClass]}
380
- title={action.label}
381
- aria-label={action.label}
382
- checked={action.toggle(item)}
383
- onclick={(e) => {
384
- e.preventDefault();
385
- e.stopPropagation();
386
- action.run(item);
387
- }}
388
- />
389
- {:else}
390
- {@const Icon = action.icon}
391
- <Button
392
- variant="ghost"
393
- class={['btn-xs', resolvedClass]}
394
- title={action.label}
395
- aria-label={action.label}
396
- onclick={(e) => {
397
- e.stopPropagation();
398
- action.run(item);
399
- }}
400
- >
401
- <Icon class="size-4" />
402
- </Button>
403
- {/if}
404
- {/if}
405
- {/each}
406
- </div>
407
- {/snippet}
408
-
409
- <div class="flex flex-col gap-6">
410
- <Header
411
- title={labelMany}
412
- breadcrumbs={[{ label: labelMany, icon: entityIcon, link: { href: '#' }, prominent: true }]}
413
- >
414
- {#snippet buttons()}
415
- <div bind:this={toolbarEl} class="flex min-w-0 flex-wrap items-center gap-2 overflow-hidden">
416
- {#if toolbarCollapsed}
417
- {#if search}
418
- <SearchInput config={search} />
419
- {/if}
420
-
421
- <Button
422
- variant="ghost"
423
- class="btn-square"
424
- popovertarget="actions-menu-{actionsMenuId}"
425
- style="anchor-name:--actions-anchor-{actionsMenuId}"
426
- aria-label={strings.actions}
427
- title={strings.actions}
428
- >
429
- <span class="text-lg leading-none" aria-hidden="true">⋯</span>
430
- </Button>
431
- <div
432
- popover="auto"
433
- id="actions-menu-{actionsMenuId}"
434
- style="position-anchor:--actions-anchor-{actionsMenuId}"
435
- class="dropdown dropdown-end w-56 rounded-box border border-base-content/10 bg-base-100 p-1 shadow-lg"
436
- bind:this={actionsPopoverEl}
437
- >
438
- {#if allowCreate}
439
- {@const CreateIcon = icons.create}
440
- <Button
441
- variant="ghost"
442
- class="btn-sm w-full justify-start"
443
- onclick={() => {
444
- actionsPopoverEl?.hidePopover();
445
- handleCreate();
446
- }}
447
- >
448
- <CreateIcon class="size-4" />
449
- {creation.label || `${strings.create} ${labelOne}`}
450
- </Button>
451
- {/if}
452
-
453
- {#if allowDelete}
454
- <Button
455
- variant="ghost"
456
- class="btn-sm w-full justify-start text-error"
457
- disabled={selected.size === 0}
458
- onclick={() => {
459
- actionsPopoverEl?.hidePopover();
460
- handleDelete();
461
- }}
462
- >
463
- {deleteLabel} ({selected.size})
464
- </Button>
465
- {/if}
466
-
467
- {#if enableExport}
468
- <Button
469
- variant="ghost"
470
- class="btn-sm w-full justify-start"
471
- onclick={() => {
472
- actionsPopoverEl?.hidePopover();
473
- handleExport('csv');
474
- }}
475
- >
476
- {strings.exportCsv}
477
- </Button>
478
- {#if xlsx}
479
- <Button
480
- variant="ghost"
481
- class="btn-sm w-full justify-start"
482
- onclick={() => {
483
- actionsPopoverEl?.hidePopover();
484
- handleExport('xlsx');
485
- }}
486
- >
487
- {strings.exportExcel}
488
- </Button>
489
- {/if}
490
- {/if}
491
-
492
- {#each customBulkActions as bulkAction, i (i)}
493
- {#if bulkAction.condition?.(selectedItems) ?? true}
494
- {@const BulkIcon = bulkAction.icon}
495
- {@const isView = isViewBasedBulkAction(bulkAction)}
496
- <Button
497
- variant="ghost"
498
- class="btn-sm w-full justify-start"
499
- disabled={!isView && selected.size === 0}
500
- title={bulkAction.tooltip}
501
- onclick={() => {
502
- actionsPopoverEl?.hidePopover();
503
- handleBulkAction(bulkAction);
504
- }}
505
- >
506
- <BulkIcon class="size-4" />
507
- {bulkAction.label ?? bulkAction.tooltip}{#if !isView}
508
- &nbsp;({selected.size})
509
- {/if}
510
- </Button>
511
- {/if}
512
- {/each}
513
- </div>
514
- {:else}
515
- {@render toolbarButtons(false)}
516
- {/if}
517
- </div>
518
-
519
- <div
520
- bind:this={toolbarMeasureEl}
521
- class="pointer-events-none invisible fixed flex items-center gap-2 whitespace-nowrap"
522
- style="left:-9999px; top:-9999px; width:max-content;"
523
- aria-hidden="true"
524
- >
525
- {@render toolbarButtons(true)}
526
- </div>
527
- {/snippet}
528
- </Header>
529
-
530
- <div class="table-wrapper">
531
- <PaginatedTable
532
- {data}
533
- {columns}
534
- {pageSize}
535
- selectable={allowSelection}
536
- {selected}
537
- rowActions={showRowActions ? actionsCell : undefined}
538
- {pagination}
539
- {initialSort}
540
- {initialFilters}
541
- {onPaginationChange}
542
- bind:visibleRows
543
- bind:query={exportQuery}
544
- />
545
- </div>
546
- </div>
547
-
548
- {#if pendingDeletion !== null}
549
- <Modal title={deleteLabel} onClose={() => (pendingDeletion = null)}>
550
- <p>{strings.deleteConfirm(pendingDeletion.length, deleteLabel)}</p>
551
- <div class="flex justify-end gap-2 mt-4">
552
- <Button variant="ghost" onclick={() => (pendingDeletion = null)}>
553
- {strings.cancel}
554
- </Button>
555
- <Button variant="error" onclick={confirmDeletion}>
556
- {strings.confirm}
557
- </Button>
558
- </div>
559
- </Modal>
560
- {/if}
561
-
562
- {#if pendingBulkAction !== null}
563
- {@const pendingActionLabel =
564
- pendingBulkAction.action.label ?? pendingBulkAction.action.tooltip ?? strings.actions}
565
- <Modal title={pendingActionLabel} onClose={() => (pendingBulkAction = null)}>
566
- <p>{strings.deleteConfirm(pendingBulkAction.items.length, pendingActionLabel)}</p>
567
- <div class="flex justify-end gap-2 mt-4">
568
- <Button variant="ghost" onclick={() => (pendingBulkAction = null)}>
569
- {strings.cancel}
570
- </Button>
571
- <Button variant={pendingBulkAction.action.variant ?? 'primary'} onclick={confirmBulkAction}>
572
- {strings.confirm}
573
- </Button>
574
- </div>
575
- </Modal>
576
- {/if}
577
-
578
- <style>
579
- .table-wrapper {
580
- width: 100%;
581
- max-width: var(--runeforge-crud-max-width);
582
- margin-inline: auto;
583
- }
584
- </style>