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,113 +1,113 @@
1
- <script lang="ts">
2
- import Button from '../form/Button.svelte';
3
- import { getStrings } from '../../i18n/context.js';
4
-
5
- const strings = getStrings();
6
-
7
- let {
8
- page = $bindable(1),
9
- totalPages,
10
- pageStart,
11
- pageSize,
12
- total
13
- }: {
14
- page?: number;
15
- totalPages: number;
16
- pageStart: number;
17
- pageSize: number;
18
- total: number;
19
- } = $props();
20
-
21
- function prev() {
22
- if (page > 1) page--;
23
- }
24
- function next() {
25
- if (page < totalPages) page++;
26
- }
27
-
28
- function buildPages(current: number, total: number): number[] {
29
- if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
30
-
31
- // Local, immediately-discarded dedup set for this one calculation, not
32
- // reactive state — a plain Set is correct here.
33
- // eslint-disable-next-line svelte/prefer-svelte-reactivity
34
- const visible = new Set<number>();
35
- visible.add(1);
36
- visible.add(total);
37
- for (let i = Math.max(1, current - 2); i <= Math.min(total, current + 2); i++) {
38
- visible.add(i);
39
- }
40
-
41
- const sorted = Array.from(visible).sort((a, b) => a - b);
42
- const result: number[] = [];
43
- for (let i = 0; i < sorted.length; i++) {
44
- if (i > 0 && sorted[i] - sorted[i - 1] > 1) result.push(0);
45
- result.push(sorted[i]);
46
- }
47
- return result;
48
- }
49
-
50
- const pages = $derived(buildPages(page, totalPages));
51
- const inputWidth = $derived(`${String(totalPages).length + 4}ch`);
52
-
53
- function handlePageInput(e: KeyboardEvent) {
54
- if (e.key !== 'Enter') return;
55
- const val = parseInt((e.currentTarget as HTMLInputElement).value);
56
- if (!isNaN(val) && val >= 1 && val <= totalPages) {
57
- page = val;
58
- } else {
59
- (e.currentTarget as HTMLInputElement).value = String(page);
60
- }
61
- }
62
-
63
- function resetInput(e: FocusEvent) {
64
- (e.currentTarget as HTMLInputElement).value = String(page);
65
- }
66
- </script>
67
-
68
- {#if totalPages > 1}
69
- <div class="flex items-center justify-between">
70
- <span class="text-sm text-base-content/60">
71
- {strings.showing(pageStart + 1, Math.min(pageStart + pageSize, total), total)}
72
- </span>
73
-
74
- <div class="join">
75
- <Button class="join-item btn-sm" disabled={page === 1} onclick={prev}>«</Button>
76
-
77
- {#each pages as p, i (i)}
78
- {#if p === 0}
79
- <Button class="join-item btn-sm" disabled>…</Button>
80
- {:else if p === page}
81
- <input
82
- type="number"
83
- class="join-item btn btn-sm no-spinner text-center"
84
- style="background-color: var(--color-base-100); width: {inputWidth};"
85
- min="1"
86
- max={totalPages}
87
- value={page}
88
- onkeydown={handlePageInput}
89
- onblur={resetInput}
90
- />
91
- {:else}
92
- <Button class="join-item btn-sm" onclick={() => (page = p)}>{p}</Button>
93
- {/if}
94
- {/each}
95
-
96
- <Button class="join-item btn-sm" disabled={page === totalPages} onclick={next}>»</Button>
97
- </div>
98
- </div>
99
- {/if}
100
-
101
- <style>
102
- .no-spinner {
103
- appearance: none;
104
- -moz-appearance: textfield;
105
- }
106
-
107
- .no-spinner::-webkit-outer-spin-button,
108
- .no-spinner::-webkit-inner-spin-button {
109
- -webkit-appearance: none;
110
- margin: 0;
111
- }
112
- </style>
113
-
1
+ <script lang="ts">
2
+ import Button from '../form/Button.svelte';
3
+ import { getStrings } from '../../i18n/context.js';
4
+
5
+ const strings = getStrings();
6
+
7
+ let {
8
+ page = $bindable(1),
9
+ totalPages,
10
+ pageStart,
11
+ pageSize,
12
+ total
13
+ }: {
14
+ page?: number;
15
+ totalPages: number;
16
+ pageStart: number;
17
+ pageSize: number;
18
+ total: number;
19
+ } = $props();
20
+
21
+ function prev() {
22
+ if (page > 1) page--;
23
+ }
24
+ function next() {
25
+ if (page < totalPages) page++;
26
+ }
27
+
28
+ function buildPages(current: number, total: number): number[] {
29
+ if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
30
+
31
+ // Local, immediately-discarded dedup set for this one calculation, not
32
+ // reactive state — a plain Set is correct here.
33
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
34
+ const visible = new Set<number>();
35
+ visible.add(1);
36
+ visible.add(total);
37
+ for (let i = Math.max(1, current - 2); i <= Math.min(total, current + 2); i++) {
38
+ visible.add(i);
39
+ }
40
+
41
+ const sorted = Array.from(visible).sort((a, b) => a - b);
42
+ const result: number[] = [];
43
+ for (let i = 0; i < sorted.length; i++) {
44
+ if (i > 0 && sorted[i] - sorted[i - 1] > 1) result.push(0);
45
+ result.push(sorted[i]);
46
+ }
47
+ return result;
48
+ }
49
+
50
+ const pages = $derived(buildPages(page, totalPages));
51
+ const inputWidth = $derived(`${String(totalPages).length + 4}ch`);
52
+
53
+ function handlePageInput(e: KeyboardEvent) {
54
+ if (e.key !== 'Enter') return;
55
+ const val = parseInt((e.currentTarget as HTMLInputElement).value);
56
+ if (!isNaN(val) && val >= 1 && val <= totalPages) {
57
+ page = val;
58
+ } else {
59
+ (e.currentTarget as HTMLInputElement).value = String(page);
60
+ }
61
+ }
62
+
63
+ function resetInput(e: FocusEvent) {
64
+ (e.currentTarget as HTMLInputElement).value = String(page);
65
+ }
66
+ </script>
67
+
68
+ {#if totalPages > 1}
69
+ <div class="flex items-center justify-between">
70
+ <span class="text-sm text-base-content/60">
71
+ {strings.showing(pageStart + 1, Math.min(pageStart + pageSize, total), total)}
72
+ </span>
73
+
74
+ <div class="join">
75
+ <Button class="join-item btn-sm" disabled={page === 1} onclick={prev}>«</Button>
76
+
77
+ {#each pages as p, i (i)}
78
+ {#if p === 0}
79
+ <Button class="join-item btn-sm" disabled>…</Button>
80
+ {:else if p === page}
81
+ <input
82
+ type="number"
83
+ class="join-item btn btn-sm no-spinner text-center"
84
+ style="background-color: var(--color-base-100); width: {inputWidth};"
85
+ min="1"
86
+ max={totalPages}
87
+ value={page}
88
+ onkeydown={handlePageInput}
89
+ onblur={resetInput}
90
+ />
91
+ {:else}
92
+ <Button class="join-item btn-sm" onclick={() => (page = p)}>{p}</Button>
93
+ {/if}
94
+ {/each}
95
+
96
+ <Button class="join-item btn-sm" disabled={page === totalPages} onclick={next}>»</Button>
97
+ </div>
98
+ </div>
99
+ {/if}
100
+
101
+ <style>
102
+ .no-spinner {
103
+ appearance: none;
104
+ -moz-appearance: textfield;
105
+ }
106
+
107
+ .no-spinner::-webkit-outer-spin-button,
108
+ .no-spinner::-webkit-inner-spin-button {
109
+ -webkit-appearance: none;
110
+ margin: 0;
111
+ }
112
+ </style>
113
+
@@ -1,43 +1,43 @@
1
- <script lang="ts">
2
- import Button from '../form/Button.svelte';
3
- import { getIconSet } from '../../icons/context.js';
4
- import { defaultIconSet } from '../../icons/sets/default.js';
5
- import type { SortDirection } from '../../types/table.js';
6
-
7
- let {
8
- title,
9
- sortable = true,
10
- direction = null,
11
- onsort,
12
- }: {
13
- title: string;
14
- sortable?: boolean;
15
- direction?: SortDirection | null;
16
- onsort?: () => void;
17
- } = $props();
18
-
19
- const icons = $derived(getIconSet() ?? defaultIconSet);
20
- </script>
21
-
22
- {#if sortable}
23
- <Button
24
- btn={false}
25
- class="flex cursor-pointer items-center gap-1 capitalize hover:text-primary"
26
- onclick={onsort}
27
- title="Ordenar"
28
- >
29
- <span>{title}</span>
30
- {#if direction === 'desc'}
31
- {@const Icon = icons.sortDesc}
32
- <Icon class="size-3" />
33
- {:else if direction === 'asc'}
34
- {@const Icon = icons.sortAsc}
35
- <Icon class="size-3" />
36
- {:else}
37
- {@const Icon = icons.sortNone}
38
- <Icon class="size-3 opacity-30" />
39
- {/if}
40
- </Button>
41
- {:else}
42
- <span class="capitalize">{title}</span>
43
- {/if}
1
+ <script lang="ts">
2
+ import Button from '../form/Button.svelte';
3
+ import { getIconSet } from '../../icons/context.js';
4
+ import { defaultIconSet } from '../../icons/sets/default.js';
5
+ import type { SortDirection } from '../../types/table.js';
6
+
7
+ let {
8
+ title,
9
+ sortable = true,
10
+ direction = null,
11
+ onsort,
12
+ }: {
13
+ title: string;
14
+ sortable?: boolean;
15
+ direction?: SortDirection | null;
16
+ onsort?: () => void;
17
+ } = $props();
18
+
19
+ const icons = $derived(getIconSet() ?? defaultIconSet);
20
+ </script>
21
+
22
+ {#if sortable}
23
+ <Button
24
+ btn={false}
25
+ class="flex cursor-pointer items-center gap-1 capitalize hover:text-primary"
26
+ onclick={onsort}
27
+ title="Ordenar"
28
+ >
29
+ <span>{title}</span>
30
+ {#if direction === 'desc'}
31
+ {@const Icon = icons.sortDesc}
32
+ <Icon class="size-3" />
33
+ {:else if direction === 'asc'}
34
+ {@const Icon = icons.sortAsc}
35
+ <Icon class="size-3" />
36
+ {:else}
37
+ {@const Icon = icons.sortNone}
38
+ <Icon class="size-3 opacity-30" />
39
+ {/if}
40
+ </Button>
41
+ {:else}
42
+ <span class="capitalize">{title}</span>
43
+ {/if}
@@ -1,150 +1,150 @@
1
- <script lang="ts" generics="T extends object">
2
- import type { Snippet } from 'svelte';
3
- import type { SvelteSet } from 'svelte/reactivity';
4
- import type { ColumnDefinition } from '../../types/crud.js';
5
- import type { IndexedRow, ReorderOptions } from '../../types/table.js';
6
- import { sortableRows, type SortEndIndices } from './sortable.js';
7
- import { moveIndexedRows } from './utils.js';
8
- import Button from '../form/Button.svelte';
9
- import { getIconSet } from '../../icons/context.js';
10
- import { defaultIconSet } from '../../icons/sets/default.js';
11
- import { getStrings } from '../../i18n/context.js';
12
-
13
- const strings = getStrings();
14
-
15
- let {
16
- columns,
17
- rows,
18
- selectable,
19
- selected,
20
- onToggle,
21
- colCount,
22
- rowActions,
23
- reorder,
24
- visibleRange,
25
- onDragStart,
26
- onReorder,
27
- }: {
28
- columns: ColumnDefinition<T>[];
29
- rows: IndexedRow<T>[];
30
- selectable: boolean;
31
- selected: SvelteSet<number>;
32
- onToggle: (index: number) => void;
33
- colCount: number;
34
- rowActions?: Snippet<[T]>;
35
- reorder?: ReorderOptions<T>;
36
- /** Reorder mode only: positions within `rows` (not the `index` field)
37
- * that are actually on screen — the rest render `hidden` so they stay in
38
- * the DOM (and reachable by SortableJS) across a page flip mid-drag. */
39
- visibleRange?: { start: number; end: number };
40
- onDragStart?: () => void;
41
- /** Fires once a drag settles, with the complete row list in its new
42
- * order — computed from SortableJS's own before/after indices, correct
43
- * uniformly for a single-row drag and a `multiDrag` group move alike. */
44
- onReorder?: (rows: IndexedRow<T>[]) => void;
45
- } = $props();
46
-
47
- const icons = $derived(getIconSet() ?? defaultIconSet);
48
-
49
- let tbodyEl: HTMLElement | undefined = $state();
50
-
51
- function handleDragEnd(indices: SortEndIndices) {
52
- const fromIndices =
53
- indices.oldIndicies.length > 0 ? indices.oldIndicies : [indices.oldIndex ?? -1];
54
- const toIndex =
55
- indices.newIndicies.length > 0 ? Math.min(...indices.newIndicies) : (indices.newIndex ?? -1);
56
- onReorder?.(moveIndexedRows(rows, fromIndices, toIndex));
57
- }
58
-
59
- // Mirrors the checkbox selection into SortableJS's own MultiDrag selection
60
- // registry, so a `multiDrag` drag moves whatever's currently checked
61
- // instead of relying on the plugin's own click/modifier-key selection UX.
62
- $effect(() => {
63
- const utils = reorder?.multiDrag ? reorder.sortable.utils : undefined;
64
- if (!utils || !tbodyEl) return;
65
- for (const { index } of rows) {
66
- const el = tbodyEl.querySelector(`[data-row-key="${index}"]`);
67
- if (!(el instanceof HTMLElement)) continue;
68
- if (selected.has(index)) utils.select(el);
69
- else utils.deselect(el);
70
- }
71
- });
72
- </script>
73
-
74
- <tbody
75
- data-testid="paginated-table-body"
76
- bind:this={tbodyEl}
77
- use:sortableRows={{
78
- enabled: !!reorder,
79
- sortable: reorder?.sortable,
80
- multiDrag: reorder?.multiDrag,
81
- onStart: () => onDragStart?.(),
82
- onEnd: handleDragEnd,
83
- }}
84
- >
85
- {#if rows.length === 0}
86
- <tr>
87
- <td colspan={colCount} class="py-10 text-center text-base-content/50">
88
- Sin registros
89
- </td>
90
- </tr>
91
- {:else}
92
- {#each rows as { row, index }, position (index)}
93
- {@const isHidden = !!visibleRange && (position < visibleRange.start || position >= visibleRange.end)}
94
- <tr
95
- class="hover"
96
- class:group={!!reorder}
97
- class:cursor-pointer={selectable}
98
- data-row-key={index}
99
- style:display={isHidden ? 'none' : null}
100
- onclick={selectable ? () => onToggle(index) : undefined}
101
- >
102
- {#if reorder}
103
- {@const GripIcon = reorder.icon ?? icons.grip}
104
- <td class="w-10 border-l-4 border-base-content/10 p-0 group-hover:border-primary/50">
105
- <Button
106
- type="button"
107
- btn={false}
108
- data-reorder-handle
109
- class="flex h-full w-full cursor-grab items-center justify-center py-2 text-base-content/40 active:cursor-grabbing"
110
- title={strings.reorder}
111
- aria-label={strings.reorder}
112
- onclick={(e) => e.stopPropagation()}
113
- >
114
- {#if GripIcon}
115
- <GripIcon class="size-4" />
116
- {/if}
117
- </Button>
118
- </td>
119
- {/if}
120
- {#if selectable}
121
- <td>
122
- <input
123
- type="checkbox"
124
- class="checkbox checkbox-sm"
125
- checked={selected.has(index)}
126
- onchange={() => onToggle(index)}
127
- onclick={(e) => e.stopPropagation()}
128
- />
129
- </td>
130
- {/if}
131
- {#each columns as col (col.attribute)}
132
- <td>
133
- {#if col.component}
134
- {@const Cell = col.component}
135
- <Cell value={row[col.attribute]} row={row} />
136
- {:else if col.formatter}
137
- <!-- eslint-disable-next-line svelte/no-at-html-tags -->
138
- {@html col.formatter(row[col.attribute], row)}
139
- {:else}
140
- {String(row[col.attribute] ?? '')}
141
- {/if}
142
- </td>
143
- {/each}
144
- {#if rowActions}
145
- <td class="text-right">{@render rowActions(row)}</td>
146
- {/if}
147
- </tr>
148
- {/each}
149
- {/if}
150
- </tbody>
1
+ <script lang="ts" generics="T extends object">
2
+ import type { Snippet } from 'svelte';
3
+ import type { SvelteSet } from 'svelte/reactivity';
4
+ import type { ColumnDefinition } from '../../types/crud.js';
5
+ import type { IndexedRow, ReorderOptions } from '../../types/table.js';
6
+ import { sortableRows, type SortEndIndices } from './sortable.js';
7
+ import { moveIndexedRows } from './utils.js';
8
+ import Button from '../form/Button.svelte';
9
+ import { getIconSet } from '../../icons/context.js';
10
+ import { defaultIconSet } from '../../icons/sets/default.js';
11
+ import { getStrings } from '../../i18n/context.js';
12
+
13
+ const strings = getStrings();
14
+
15
+ let {
16
+ columns,
17
+ rows,
18
+ selectable,
19
+ selected,
20
+ onToggle,
21
+ colCount,
22
+ rowActions,
23
+ reorder,
24
+ visibleRange,
25
+ onDragStart,
26
+ onReorder,
27
+ }: {
28
+ columns: ColumnDefinition<T>[];
29
+ rows: IndexedRow<T>[];
30
+ selectable: boolean;
31
+ selected: SvelteSet<number>;
32
+ onToggle: (index: number) => void;
33
+ colCount: number;
34
+ rowActions?: Snippet<[T]>;
35
+ reorder?: ReorderOptions<T>;
36
+ /** Reorder mode only: positions within `rows` (not the `index` field)
37
+ * that are actually on screen — the rest render `hidden` so they stay in
38
+ * the DOM (and reachable by SortableJS) across a page flip mid-drag. */
39
+ visibleRange?: { start: number; end: number };
40
+ onDragStart?: () => void;
41
+ /** Fires once a drag settles, with the complete row list in its new
42
+ * order — computed from SortableJS's own before/after indices, correct
43
+ * uniformly for a single-row drag and a `multiDrag` group move alike. */
44
+ onReorder?: (rows: IndexedRow<T>[]) => void;
45
+ } = $props();
46
+
47
+ const icons = $derived(getIconSet() ?? defaultIconSet);
48
+
49
+ let tbodyEl: HTMLElement | undefined = $state();
50
+
51
+ function handleDragEnd(indices: SortEndIndices) {
52
+ const fromIndices =
53
+ indices.oldIndicies.length > 0 ? indices.oldIndicies : [indices.oldIndex ?? -1];
54
+ const toIndex =
55
+ indices.newIndicies.length > 0 ? Math.min(...indices.newIndicies) : (indices.newIndex ?? -1);
56
+ onReorder?.(moveIndexedRows(rows, fromIndices, toIndex));
57
+ }
58
+
59
+ // Mirrors the checkbox selection into SortableJS's own MultiDrag selection
60
+ // registry, so a `multiDrag` drag moves whatever's currently checked
61
+ // instead of relying on the plugin's own click/modifier-key selection UX.
62
+ $effect(() => {
63
+ const utils = reorder?.multiDrag ? reorder.sortable.utils : undefined;
64
+ if (!utils || !tbodyEl) return;
65
+ for (const { index } of rows) {
66
+ const el = tbodyEl.querySelector(`[data-row-key="${index}"]`);
67
+ if (!(el instanceof HTMLElement)) continue;
68
+ if (selected.has(index)) utils.select(el);
69
+ else utils.deselect(el);
70
+ }
71
+ });
72
+ </script>
73
+
74
+ <tbody
75
+ data-testid="paginated-table-body"
76
+ bind:this={tbodyEl}
77
+ use:sortableRows={{
78
+ enabled: !!reorder,
79
+ sortable: reorder?.sortable,
80
+ multiDrag: reorder?.multiDrag,
81
+ onStart: () => onDragStart?.(),
82
+ onEnd: handleDragEnd,
83
+ }}
84
+ >
85
+ {#if rows.length === 0}
86
+ <tr>
87
+ <td colspan={colCount} class="py-10 text-center text-base-content/50">
88
+ Sin registros
89
+ </td>
90
+ </tr>
91
+ {:else}
92
+ {#each rows as { row, index }, position (index)}
93
+ {@const isHidden = !!visibleRange && (position < visibleRange.start || position >= visibleRange.end)}
94
+ <tr
95
+ class="hover"
96
+ class:group={!!reorder}
97
+ class:cursor-pointer={selectable}
98
+ data-row-key={index}
99
+ style:display={isHidden ? 'none' : null}
100
+ onclick={selectable ? () => onToggle(index) : undefined}
101
+ >
102
+ {#if reorder}
103
+ {@const GripIcon = reorder.icon ?? icons.grip}
104
+ <td class="w-10 border-l-4 border-base-content/10 p-0 group-hover:border-primary/50">
105
+ <Button
106
+ type="button"
107
+ btn={false}
108
+ data-reorder-handle
109
+ class="flex h-full w-full cursor-grab items-center justify-center py-2 text-base-content/40 active:cursor-grabbing"
110
+ title={strings.reorder}
111
+ aria-label={strings.reorder}
112
+ onclick={(e) => e.stopPropagation()}
113
+ >
114
+ {#if GripIcon}
115
+ <GripIcon class="size-4" />
116
+ {/if}
117
+ </Button>
118
+ </td>
119
+ {/if}
120
+ {#if selectable}
121
+ <td>
122
+ <input
123
+ type="checkbox"
124
+ class="checkbox checkbox-sm"
125
+ checked={selected.has(index)}
126
+ onchange={() => onToggle(index)}
127
+ onclick={(e) => e.stopPropagation()}
128
+ />
129
+ </td>
130
+ {/if}
131
+ {#each columns as col (col.attribute)}
132
+ <td>
133
+ {#if col.component}
134
+ {@const Cell = col.component}
135
+ <Cell value={row[col.attribute]} row={row} />
136
+ {:else if col.formatter}
137
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -->
138
+ {@html col.formatter(row[col.attribute], row)}
139
+ {:else}
140
+ {String(row[col.attribute] ?? '')}
141
+ {/if}
142
+ </td>
143
+ {/each}
144
+ {#if rowActions}
145
+ <td class="text-right">{@render rowActions(row)}</td>
146
+ {/if}
147
+ </tr>
148
+ {/each}
149
+ {/if}
150
+ </tbody>