runeforge 0.0.55 → 0.0.56

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