runeforge 0.0.62 → 0.0.63

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 (66) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +1410 -1407
  3. package/dist/components/Avatar.svelte +31 -31
  4. package/dist/components/IconRenderer.svelte +73 -73
  5. package/dist/components/Modal.svelte +75 -75
  6. package/dist/components/common/Header.svelte +40 -40
  7. package/dist/components/crud/EmbeddedField.svelte +203 -203
  8. package/dist/components/crud/Field.svelte +122 -122
  9. package/dist/components/crud/GenericCRUD.svelte +426 -426
  10. package/dist/components/crud/SearchInput.svelte +53 -53
  11. package/dist/components/crud/columns/Avatar.svelte +15 -15
  12. package/dist/components/crud/columns/Icon.svelte +8 -8
  13. package/dist/components/crud/fields/BooleanField.svelte +38 -38
  14. package/dist/components/crud/fields/DatetimeField.svelte +250 -250
  15. package/dist/components/crud/fields/DefaultField.svelte +67 -67
  16. package/dist/components/crud/fields/FileField.svelte +70 -70
  17. package/dist/components/crud/fields/MultiSelectField.svelte +70 -70
  18. package/dist/components/crud/fields/SelectField.svelte +67 -67
  19. package/dist/components/crud/fields/TextareaField.svelte +55 -55
  20. package/dist/components/crud/fields/TreeField.svelte +1 -0
  21. package/dist/components/crud/utils/resolution.js +1 -0
  22. package/dist/components/crud/utils/tree.d.ts +5 -0
  23. package/dist/components/crud/utils/tree.js +29 -0
  24. package/dist/components/crud/views/Create.svelte +239 -239
  25. package/dist/components/crud/views/Read.svelte +131 -131
  26. package/dist/components/crud/views/Update.svelte +215 -215
  27. package/dist/components/crud/views/list/List.svelte +291 -291
  28. package/dist/components/crud/views/list/Modals.svelte +56 -56
  29. package/dist/components/crud/views/list/Table.svelte +173 -173
  30. package/dist/components/crud/views/list/Toolbar.svelte +340 -340
  31. package/dist/components/form/Button.svelte +27 -27
  32. package/dist/components/form/Label.svelte +37 -37
  33. package/dist/components/form/MultiSelect.svelte +248 -248
  34. package/dist/components/form/PasswordInput.svelte +68 -68
  35. package/dist/components/form/Required.svelte +1 -1
  36. package/dist/components/form/Select.svelte +209 -209
  37. package/dist/components/form/Tree.svelte +43 -4
  38. package/dist/components/form/Tree.svelte.d.ts +1 -0
  39. package/dist/components/form/TreeNode.svelte +31 -2
  40. package/dist/components/form/TreeNode.svelte.d.ts +2 -0
  41. package/dist/components/navigation/Breadcrumbs.svelte +111 -111
  42. package/dist/components/table/ColumnFilter.svelte +168 -168
  43. package/dist/components/table/PaginatedTable.svelte +542 -542
  44. package/dist/components/table/Paginator.svelte +113 -113
  45. package/dist/components/table/SortHeader.svelte +43 -43
  46. package/dist/components/table/TableBody.svelte +150 -150
  47. package/dist/components/table/TableHeader.svelte +88 -88
  48. package/dist/icons/defaults/Clear.svelte +6 -6
  49. package/dist/icons/defaults/Create.svelte +6 -6
  50. package/dist/icons/defaults/Delete.svelte +6 -6
  51. package/dist/icons/defaults/Download.svelte +7 -7
  52. package/dist/icons/defaults/Edit.svelte +7 -7
  53. package/dist/icons/defaults/Filter.svelte +6 -6
  54. package/dist/icons/defaults/FilterActive.svelte +6 -6
  55. package/dist/icons/defaults/Folder.svelte +6 -6
  56. package/dist/icons/defaults/Grip.svelte +7 -7
  57. package/dist/icons/defaults/Home.svelte +6 -6
  58. package/dist/icons/defaults/PasswordHide.svelte +9 -9
  59. package/dist/icons/defaults/PasswordShow.svelte +7 -7
  60. package/dist/icons/defaults/SortAsc.svelte +6 -6
  61. package/dist/icons/defaults/SortDesc.svelte +6 -6
  62. package/dist/icons/defaults/SortNone.svelte +6 -6
  63. package/dist/icons/defaults/View.svelte +7 -7
  64. package/dist/types/attribute.d.ts +3 -0
  65. package/dist/types/crud/fields.d.ts +3 -0
  66. package/package.json +1 -1
@@ -1,340 +1,340 @@
1
- <script lang="ts" generics="T extends object = Record<string, unknown>">
2
- import Button from '../../../form/Button.svelte';
3
- import Header from '../../../common/Header.svelte';
4
- import { downloadCsv, downloadXlsx } from '../../../table/export.js';
5
- import { getIconSet } from '../../../../icons/context.js';
6
- import { defaultIconSet } from '../../../../icons/sets/default.js';
7
- import type {
8
- ActionConfiguration,
9
- CustomBulkAction,
10
- ViewBasedCustomBulkAction
11
- } from '../../../../types/crud/actions.js';
12
- import type { ColumnDefinition } from '../../../../types/crud/columns.js';
13
- import type { ExportConfiguration, SearchConfiguration } from '../../../../types/crud/list-config.js';
14
- import type { ServerPagination, TableQuery } from '../../../../types/table.js';
15
- import { getStrings } from '../../../../i18n/context.js';
16
- import SearchInput from '../../SearchInput.svelte';
17
-
18
- const strings = getStrings();
19
-
20
- function isViewBasedBulkAction<U extends object>(
21
- action: CustomBulkAction<U>
22
- ): action is ViewBasedCustomBulkAction<U> {
23
- return action.kind === 'view';
24
- }
25
-
26
- let {
27
- labelOne = '',
28
- labelMany = '',
29
- icon,
30
- creation = {} as ActionConfiguration<T>,
31
- allowCreate,
32
- allowDelete,
33
- deleteLabel,
34
- search = undefined as SearchConfiguration | undefined,
35
- exportConfig = undefined as ExportConfiguration<T> | undefined,
36
- bulkActions = [] as CustomBulkAction<T>[],
37
- columns = [] as ColumnDefinition<T>[],
38
- pagination = undefined as ServerPagination | undefined,
39
- visibleRows = [] as T[],
40
- exportQuery = undefined as TableQuery | undefined,
41
- selectedCount = 0,
42
- selectedItems = [] as T[],
43
- onCreate,
44
- onDeleteSelected,
45
- onBulkAction
46
- }: {
47
- labelOne?: string;
48
- labelMany?: string;
49
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
50
- icon?: any;
51
- creation?: ActionConfiguration<T>;
52
- allowCreate: boolean;
53
- allowDelete: boolean;
54
- deleteLabel: string;
55
- search?: SearchConfiguration;
56
- exportConfig?: ExportConfiguration<T>;
57
- bulkActions?: CustomBulkAction<T>[];
58
- columns?: ColumnDefinition<T>[];
59
- pagination?: ServerPagination;
60
- visibleRows?: T[];
61
- exportQuery?: TableQuery;
62
- selectedCount?: number;
63
- selectedItems?: T[];
64
- onCreate?: () => void;
65
- onDeleteSelected?: () => void;
66
- onBulkAction?: (action: CustomBulkAction<T>) => void;
67
- } = $props();
68
-
69
- const icons = $derived(getIconSet() ?? defaultIconSet);
70
- const entityIcon = $derived(icon ?? icons.folder);
71
- const enableExport = $derived(!!exportConfig);
72
-
73
- let exportPopoverEl: HTMLElement | undefined = $state();
74
- let actionsPopoverEl: HTMLElement | undefined = $state();
75
- const exportId = $props.id();
76
- const actionsMenuId = `${exportId}-actions`;
77
-
78
- // Collapses export/bulk-actions/delete into a single "Acciones" dropdown
79
- // once the toolbar no longer fits its available width (small monitor, a
80
- // browser window not maximized, many bulkActions, ...) — measured against
81
- // an identical but off-flow, always-uncollapsed clone rather than the
82
- // visible row itself, since the visible row's own width changes depending
83
- // on whether it's currently collapsed.
84
- let toolbarEl: HTMLElement | undefined = $state();
85
- let toolbarMeasureEl: HTMLElement | undefined = $state();
86
- let toolbarCollapsed = $state(false);
87
-
88
- $effect(() => {
89
- if (!toolbarEl || !toolbarMeasureEl) return;
90
- const el = toolbarEl;
91
- const measureEl = toolbarMeasureEl;
92
- const check = () => {
93
- toolbarCollapsed = measureEl.scrollWidth > el.clientWidth + 1;
94
- };
95
- const observer = new ResizeObserver(check);
96
- observer.observe(el);
97
- observer.observe(measureEl);
98
- check();
99
- return () => observer.disconnect();
100
- });
101
-
102
- async function resolveExportRows(): Promise<T[]> {
103
- if (!pagination) return visibleRows;
104
- if (!exportConfig?.callback || !exportQuery) return visibleRows;
105
- return exportConfig.callback(exportQuery);
106
- }
107
-
108
- async function handleExport(format: 'csv' | 'xlsx') {
109
- exportPopoverEl?.hidePopover();
110
- const rows = await resolveExportRows();
111
- const filename = `${labelMany || 'export'}-${new Date().toISOString().slice(0, 10)}`;
112
- if (format === 'csv') downloadCsv(rows, columns, filename);
113
- else if (exportConfig?.xlsx) downloadXlsx(rows, columns, filename, exportConfig.xlsx);
114
- }
115
-
116
- function handleCreate() {
117
- onCreate?.();
118
- }
119
-
120
- function handleDelete() {
121
- onDeleteSelected?.();
122
- }
123
-
124
- function handleBulkAction(action: CustomBulkAction<T>) {
125
- onBulkAction?.(action);
126
- }
127
- </script>
128
-
129
- {#snippet toolbarButtons(measuring: boolean)}
130
- {#if search}
131
- <SearchInput config={search} />
132
- {/if}
133
-
134
- {#if enableExport}
135
- {@const ExportIcon = icons.download}
136
- <Button
137
- variant="ghost"
138
- class="btn-square"
139
- popovertarget={measuring ? undefined : `export-menu-${exportId}`}
140
- style={measuring ? undefined : `anchor-name:--export-anchor-${exportId}`}
141
- aria-label={strings.export}
142
- title={strings.export}
143
- >
144
- <ExportIcon class="size-4" />
145
- </Button>
146
- {#if !measuring}
147
- <div
148
- popover="auto"
149
- id="export-menu-{exportId}"
150
- style="position-anchor:--export-anchor-{exportId}"
151
- class="dropdown dropdown-end w-40 rounded-box border border-base-content/10 bg-base-100 p-1 shadow-lg"
152
- bind:this={exportPopoverEl}
153
- >
154
- <Button
155
- variant="ghost"
156
- class="btn-sm w-full justify-start"
157
- onclick={() => handleExport('csv')}
158
- >
159
- {strings.exportCsv}
160
- </Button>
161
- {#if exportConfig?.xlsx}
162
- <Button
163
- variant="ghost"
164
- class="btn-sm w-full justify-start"
165
- onclick={() => handleExport('xlsx')}
166
- >
167
- {strings.exportExcel}
168
- </Button>
169
- {/if}
170
- </div>
171
- {/if}
172
- {/if}
173
-
174
- {#each bulkActions as bulkAction, i (i)}
175
- {#if bulkAction.condition?.(selectedItems) ?? true}
176
- {@const BulkIcon = bulkAction.icon}
177
- {@const isView = isViewBasedBulkAction(bulkAction)}
178
- <Button
179
- variant={bulkAction.variant ?? 'ghost'}
180
- class="btn-outline"
181
- disabled={!isView && selectedCount === 0}
182
- title={bulkAction.tooltip ?? bulkAction.label}
183
- aria-label={bulkAction.label ? undefined : bulkAction.tooltip}
184
- onclick={measuring ? undefined : () => handleBulkAction(bulkAction)}
185
- >
186
- <BulkIcon class="size-4" />
187
- {#if bulkAction.label}
188
- {bulkAction.label}{#if !isView}
189
- &nbsp;({selectedCount})
190
- {/if}
191
- {/if}
192
- </Button>
193
- {/if}
194
- {/each}
195
-
196
- {#if allowDelete}
197
- {@const DeleteIcon = icons.delete}
198
- <Button
199
- variant="error"
200
- class="btn-outline"
201
- disabled={selectedCount === 0}
202
- onclick={measuring ? undefined : handleDelete}
203
- >
204
- <DeleteIcon class="size-4" />
205
- {deleteLabel} ({selectedCount})
206
- </Button>
207
- {/if}
208
-
209
- {#if allowCreate}
210
- {@const CreateIcon = icons.create}
211
- <Button variant="primary" onclick={measuring ? undefined : handleCreate}>
212
- <CreateIcon class="size-5" />
213
- {#if creation.label}
214
- <span>{creation.label}</span>
215
- {:else}
216
- <span>{strings.create}<span class="hidden sm:inline">&nbsp;{labelOne}</span></span>
217
- {/if}
218
- </Button>
219
- {/if}
220
- {/snippet}
221
-
222
- <Header
223
- title={labelMany}
224
- breadcrumbs={[{ label: labelMany, icon: entityIcon, link: { href: '#' }, prominent: true }]}
225
- >
226
- {#snippet buttons()}
227
- <div bind:this={toolbarEl} class="flex min-w-0 flex-wrap items-center gap-2 overflow-hidden">
228
- {#if toolbarCollapsed}
229
- {#if search}
230
- <SearchInput config={search} />
231
- {/if}
232
-
233
- <Button
234
- variant="ghost"
235
- class="btn-square"
236
- popovertarget="actions-menu-{actionsMenuId}"
237
- style="anchor-name:--actions-anchor-{actionsMenuId}"
238
- aria-label={strings.actions}
239
- title={strings.actions}
240
- >
241
- <span class="text-lg leading-none" aria-hidden="true">⋯</span>
242
- </Button>
243
- <div
244
- popover="auto"
245
- id="actions-menu-{actionsMenuId}"
246
- style="position-anchor:--actions-anchor-{actionsMenuId}"
247
- class="dropdown dropdown-end w-56 rounded-box border border-base-content/10 bg-base-100 p-1 shadow-lg"
248
- bind:this={actionsPopoverEl}
249
- >
250
- {#if allowCreate}
251
- {@const CreateIcon = icons.create}
252
- <Button
253
- variant="ghost"
254
- class="btn-sm w-full justify-start"
255
- onclick={() => {
256
- actionsPopoverEl?.hidePopover();
257
- handleCreate();
258
- }}
259
- >
260
- <CreateIcon class="size-4" />
261
- {creation.label || `${strings.create} ${labelOne}`}
262
- </Button>
263
- {/if}
264
-
265
- {#if allowDelete}
266
- <Button
267
- variant="ghost"
268
- class="btn-sm w-full justify-start text-error"
269
- disabled={selectedCount === 0}
270
- onclick={() => {
271
- actionsPopoverEl?.hidePopover();
272
- handleDelete();
273
- }}
274
- >
275
- {deleteLabel} ({selectedCount})
276
- </Button>
277
- {/if}
278
-
279
- {#if enableExport}
280
- <Button
281
- variant="ghost"
282
- class="btn-sm w-full justify-start"
283
- onclick={() => {
284
- actionsPopoverEl?.hidePopover();
285
- handleExport('csv');
286
- }}
287
- >
288
- {strings.exportCsv}
289
- </Button>
290
- {#if exportConfig?.xlsx}
291
- <Button
292
- variant="ghost"
293
- class="btn-sm w-full justify-start"
294
- onclick={() => {
295
- actionsPopoverEl?.hidePopover();
296
- handleExport('xlsx');
297
- }}
298
- >
299
- {strings.exportExcel}
300
- </Button>
301
- {/if}
302
- {/if}
303
-
304
- {#each bulkActions as bulkAction, i (i)}
305
- {#if bulkAction.condition?.(selectedItems) ?? true}
306
- {@const BulkIcon = bulkAction.icon}
307
- {@const isView = isViewBasedBulkAction(bulkAction)}
308
- <Button
309
- variant="ghost"
310
- class="btn-sm w-full justify-start"
311
- disabled={!isView && selectedCount === 0}
312
- title={bulkAction.tooltip}
313
- onclick={() => {
314
- actionsPopoverEl?.hidePopover();
315
- handleBulkAction(bulkAction);
316
- }}
317
- >
318
- <BulkIcon class="size-4" />
319
- {bulkAction.label ?? bulkAction.tooltip}{#if !isView}
320
- &nbsp;({selectedCount})
321
- {/if}
322
- </Button>
323
- {/if}
324
- {/each}
325
- </div>
326
- {:else}
327
- {@render toolbarButtons(false)}
328
- {/if}
329
- </div>
330
-
331
- <div
332
- bind:this={toolbarMeasureEl}
333
- class="pointer-events-none invisible fixed flex items-center gap-2 whitespace-nowrap"
334
- style="left:-9999px; top:-9999px; width:max-content;"
335
- aria-hidden="true"
336
- >
337
- {@render toolbarButtons(true)}
338
- </div>
339
- {/snippet}
340
- </Header>
1
+ <script lang="ts" generics="T extends object = Record<string, unknown>">
2
+ import Button from '../../../form/Button.svelte';
3
+ import Header from '../../../common/Header.svelte';
4
+ import { downloadCsv, downloadXlsx } from '../../../table/export.js';
5
+ import { getIconSet } from '../../../../icons/context.js';
6
+ import { defaultIconSet } from '../../../../icons/sets/default.js';
7
+ import type {
8
+ ActionConfiguration,
9
+ CustomBulkAction,
10
+ ViewBasedCustomBulkAction
11
+ } from '../../../../types/crud/actions.js';
12
+ import type { ColumnDefinition } from '../../../../types/crud/columns.js';
13
+ import type { ExportConfiguration, SearchConfiguration } from '../../../../types/crud/list-config.js';
14
+ import type { ServerPagination, TableQuery } from '../../../../types/table.js';
15
+ import { getStrings } from '../../../../i18n/context.js';
16
+ import SearchInput from '../../SearchInput.svelte';
17
+
18
+ const strings = getStrings();
19
+
20
+ function isViewBasedBulkAction<U extends object>(
21
+ action: CustomBulkAction<U>
22
+ ): action is ViewBasedCustomBulkAction<U> {
23
+ return action.kind === 'view';
24
+ }
25
+
26
+ let {
27
+ labelOne = '',
28
+ labelMany = '',
29
+ icon,
30
+ creation = {} as ActionConfiguration<T>,
31
+ allowCreate,
32
+ allowDelete,
33
+ deleteLabel,
34
+ search = undefined as SearchConfiguration | undefined,
35
+ exportConfig = undefined as ExportConfiguration<T> | undefined,
36
+ bulkActions = [] as CustomBulkAction<T>[],
37
+ columns = [] as ColumnDefinition<T>[],
38
+ pagination = undefined as ServerPagination | undefined,
39
+ visibleRows = [] as T[],
40
+ exportQuery = undefined as TableQuery | undefined,
41
+ selectedCount = 0,
42
+ selectedItems = [] as T[],
43
+ onCreate,
44
+ onDeleteSelected,
45
+ onBulkAction
46
+ }: {
47
+ labelOne?: string;
48
+ labelMany?: string;
49
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
50
+ icon?: any;
51
+ creation?: ActionConfiguration<T>;
52
+ allowCreate: boolean;
53
+ allowDelete: boolean;
54
+ deleteLabel: string;
55
+ search?: SearchConfiguration;
56
+ exportConfig?: ExportConfiguration<T>;
57
+ bulkActions?: CustomBulkAction<T>[];
58
+ columns?: ColumnDefinition<T>[];
59
+ pagination?: ServerPagination;
60
+ visibleRows?: T[];
61
+ exportQuery?: TableQuery;
62
+ selectedCount?: number;
63
+ selectedItems?: T[];
64
+ onCreate?: () => void;
65
+ onDeleteSelected?: () => void;
66
+ onBulkAction?: (action: CustomBulkAction<T>) => void;
67
+ } = $props();
68
+
69
+ const icons = $derived(getIconSet() ?? defaultIconSet);
70
+ const entityIcon = $derived(icon ?? icons.folder);
71
+ const enableExport = $derived(!!exportConfig);
72
+
73
+ let exportPopoverEl: HTMLElement | undefined = $state();
74
+ let actionsPopoverEl: HTMLElement | undefined = $state();
75
+ const exportId = $props.id();
76
+ const actionsMenuId = `${exportId}-actions`;
77
+
78
+ // Collapses export/bulk-actions/delete into a single "Acciones" dropdown
79
+ // once the toolbar no longer fits its available width (small monitor, a
80
+ // browser window not maximized, many bulkActions, ...) — measured against
81
+ // an identical but off-flow, always-uncollapsed clone rather than the
82
+ // visible row itself, since the visible row's own width changes depending
83
+ // on whether it's currently collapsed.
84
+ let toolbarEl: HTMLElement | undefined = $state();
85
+ let toolbarMeasureEl: HTMLElement | undefined = $state();
86
+ let toolbarCollapsed = $state(false);
87
+
88
+ $effect(() => {
89
+ if (!toolbarEl || !toolbarMeasureEl) return;
90
+ const el = toolbarEl;
91
+ const measureEl = toolbarMeasureEl;
92
+ const check = () => {
93
+ toolbarCollapsed = measureEl.scrollWidth > el.clientWidth + 1;
94
+ };
95
+ const observer = new ResizeObserver(check);
96
+ observer.observe(el);
97
+ observer.observe(measureEl);
98
+ check();
99
+ return () => observer.disconnect();
100
+ });
101
+
102
+ async function resolveExportRows(): Promise<T[]> {
103
+ if (!pagination) return visibleRows;
104
+ if (!exportConfig?.callback || !exportQuery) return visibleRows;
105
+ return exportConfig.callback(exportQuery);
106
+ }
107
+
108
+ async function handleExport(format: 'csv' | 'xlsx') {
109
+ exportPopoverEl?.hidePopover();
110
+ const rows = await resolveExportRows();
111
+ const filename = `${labelMany || 'export'}-${new Date().toISOString().slice(0, 10)}`;
112
+ if (format === 'csv') downloadCsv(rows, columns, filename);
113
+ else if (exportConfig?.xlsx) downloadXlsx(rows, columns, filename, exportConfig.xlsx);
114
+ }
115
+
116
+ function handleCreate() {
117
+ onCreate?.();
118
+ }
119
+
120
+ function handleDelete() {
121
+ onDeleteSelected?.();
122
+ }
123
+
124
+ function handleBulkAction(action: CustomBulkAction<T>) {
125
+ onBulkAction?.(action);
126
+ }
127
+ </script>
128
+
129
+ {#snippet toolbarButtons(measuring: boolean)}
130
+ {#if search}
131
+ <SearchInput config={search} />
132
+ {/if}
133
+
134
+ {#if enableExport}
135
+ {@const ExportIcon = icons.download}
136
+ <Button
137
+ variant="ghost"
138
+ class="btn-square"
139
+ popovertarget={measuring ? undefined : `export-menu-${exportId}`}
140
+ style={measuring ? undefined : `anchor-name:--export-anchor-${exportId}`}
141
+ aria-label={strings.export}
142
+ title={strings.export}
143
+ >
144
+ <ExportIcon class="size-4" />
145
+ </Button>
146
+ {#if !measuring}
147
+ <div
148
+ popover="auto"
149
+ id="export-menu-{exportId}"
150
+ style="position-anchor:--export-anchor-{exportId}"
151
+ class="dropdown dropdown-end w-40 rounded-box border border-base-content/10 bg-base-100 p-1 shadow-lg"
152
+ bind:this={exportPopoverEl}
153
+ >
154
+ <Button
155
+ variant="ghost"
156
+ class="btn-sm w-full justify-start"
157
+ onclick={() => handleExport('csv')}
158
+ >
159
+ {strings.exportCsv}
160
+ </Button>
161
+ {#if exportConfig?.xlsx}
162
+ <Button
163
+ variant="ghost"
164
+ class="btn-sm w-full justify-start"
165
+ onclick={() => handleExport('xlsx')}
166
+ >
167
+ {strings.exportExcel}
168
+ </Button>
169
+ {/if}
170
+ </div>
171
+ {/if}
172
+ {/if}
173
+
174
+ {#each bulkActions as bulkAction, i (i)}
175
+ {#if bulkAction.condition?.(selectedItems) ?? true}
176
+ {@const BulkIcon = bulkAction.icon}
177
+ {@const isView = isViewBasedBulkAction(bulkAction)}
178
+ <Button
179
+ variant={bulkAction.variant ?? 'ghost'}
180
+ class="btn-outline"
181
+ disabled={!isView && selectedCount === 0}
182
+ title={bulkAction.tooltip ?? bulkAction.label}
183
+ aria-label={bulkAction.label ? undefined : bulkAction.tooltip}
184
+ onclick={measuring ? undefined : () => handleBulkAction(bulkAction)}
185
+ >
186
+ <BulkIcon class="size-4" />
187
+ {#if bulkAction.label}
188
+ {bulkAction.label}{#if !isView}
189
+ &nbsp;({selectedCount})
190
+ {/if}
191
+ {/if}
192
+ </Button>
193
+ {/if}
194
+ {/each}
195
+
196
+ {#if allowDelete}
197
+ {@const DeleteIcon = icons.delete}
198
+ <Button
199
+ variant="error"
200
+ class="btn-outline"
201
+ disabled={selectedCount === 0}
202
+ onclick={measuring ? undefined : handleDelete}
203
+ >
204
+ <DeleteIcon class="size-4" />
205
+ {deleteLabel} ({selectedCount})
206
+ </Button>
207
+ {/if}
208
+
209
+ {#if allowCreate}
210
+ {@const CreateIcon = icons.create}
211
+ <Button variant="primary" onclick={measuring ? undefined : handleCreate}>
212
+ <CreateIcon class="size-5" />
213
+ {#if creation.label}
214
+ <span>{creation.label}</span>
215
+ {:else}
216
+ <span>{strings.create}<span class="hidden sm:inline">&nbsp;{labelOne}</span></span>
217
+ {/if}
218
+ </Button>
219
+ {/if}
220
+ {/snippet}
221
+
222
+ <Header
223
+ title={labelMany}
224
+ breadcrumbs={[{ label: labelMany, icon: entityIcon, link: { href: '#' }, prominent: true }]}
225
+ >
226
+ {#snippet buttons()}
227
+ <div bind:this={toolbarEl} class="flex min-w-0 flex-wrap items-center gap-2 overflow-hidden">
228
+ {#if toolbarCollapsed}
229
+ {#if search}
230
+ <SearchInput config={search} />
231
+ {/if}
232
+
233
+ <Button
234
+ variant="ghost"
235
+ class="btn-square"
236
+ popovertarget="actions-menu-{actionsMenuId}"
237
+ style="anchor-name:--actions-anchor-{actionsMenuId}"
238
+ aria-label={strings.actions}
239
+ title={strings.actions}
240
+ >
241
+ <span class="text-lg leading-none" aria-hidden="true">⋯</span>
242
+ </Button>
243
+ <div
244
+ popover="auto"
245
+ id="actions-menu-{actionsMenuId}"
246
+ style="position-anchor:--actions-anchor-{actionsMenuId}"
247
+ class="dropdown dropdown-end w-56 rounded-box border border-base-content/10 bg-base-100 p-1 shadow-lg"
248
+ bind:this={actionsPopoverEl}
249
+ >
250
+ {#if allowCreate}
251
+ {@const CreateIcon = icons.create}
252
+ <Button
253
+ variant="ghost"
254
+ class="btn-sm w-full justify-start"
255
+ onclick={() => {
256
+ actionsPopoverEl?.hidePopover();
257
+ handleCreate();
258
+ }}
259
+ >
260
+ <CreateIcon class="size-4" />
261
+ {creation.label || `${strings.create} ${labelOne}`}
262
+ </Button>
263
+ {/if}
264
+
265
+ {#if allowDelete}
266
+ <Button
267
+ variant="ghost"
268
+ class="btn-sm w-full justify-start text-error"
269
+ disabled={selectedCount === 0}
270
+ onclick={() => {
271
+ actionsPopoverEl?.hidePopover();
272
+ handleDelete();
273
+ }}
274
+ >
275
+ {deleteLabel} ({selectedCount})
276
+ </Button>
277
+ {/if}
278
+
279
+ {#if enableExport}
280
+ <Button
281
+ variant="ghost"
282
+ class="btn-sm w-full justify-start"
283
+ onclick={() => {
284
+ actionsPopoverEl?.hidePopover();
285
+ handleExport('csv');
286
+ }}
287
+ >
288
+ {strings.exportCsv}
289
+ </Button>
290
+ {#if exportConfig?.xlsx}
291
+ <Button
292
+ variant="ghost"
293
+ class="btn-sm w-full justify-start"
294
+ onclick={() => {
295
+ actionsPopoverEl?.hidePopover();
296
+ handleExport('xlsx');
297
+ }}
298
+ >
299
+ {strings.exportExcel}
300
+ </Button>
301
+ {/if}
302
+ {/if}
303
+
304
+ {#each bulkActions as bulkAction, i (i)}
305
+ {#if bulkAction.condition?.(selectedItems) ?? true}
306
+ {@const BulkIcon = bulkAction.icon}
307
+ {@const isView = isViewBasedBulkAction(bulkAction)}
308
+ <Button
309
+ variant="ghost"
310
+ class="btn-sm w-full justify-start"
311
+ disabled={!isView && selectedCount === 0}
312
+ title={bulkAction.tooltip}
313
+ onclick={() => {
314
+ actionsPopoverEl?.hidePopover();
315
+ handleBulkAction(bulkAction);
316
+ }}
317
+ >
318
+ <BulkIcon class="size-4" />
319
+ {bulkAction.label ?? bulkAction.tooltip}{#if !isView}
320
+ &nbsp;({selectedCount})
321
+ {/if}
322
+ </Button>
323
+ {/if}
324
+ {/each}
325
+ </div>
326
+ {:else}
327
+ {@render toolbarButtons(false)}
328
+ {/if}
329
+ </div>
330
+
331
+ <div
332
+ bind:this={toolbarMeasureEl}
333
+ class="pointer-events-none invisible fixed flex items-center gap-2 whitespace-nowrap"
334
+ style="left:-9999px; top:-9999px; width:max-content;"
335
+ aria-hidden="true"
336
+ >
337
+ {@render toolbarButtons(true)}
338
+ </div>
339
+ {/snippet}
340
+ </Header>