runeforge 0.0.61 → 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 (67) 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/components/table/state.svelte.js +7 -2
  49. package/dist/icons/defaults/Clear.svelte +6 -6
  50. package/dist/icons/defaults/Create.svelte +6 -6
  51. package/dist/icons/defaults/Delete.svelte +6 -6
  52. package/dist/icons/defaults/Download.svelte +7 -7
  53. package/dist/icons/defaults/Edit.svelte +7 -7
  54. package/dist/icons/defaults/Filter.svelte +6 -6
  55. package/dist/icons/defaults/FilterActive.svelte +6 -6
  56. package/dist/icons/defaults/Folder.svelte +6 -6
  57. package/dist/icons/defaults/Grip.svelte +7 -7
  58. package/dist/icons/defaults/Home.svelte +6 -6
  59. package/dist/icons/defaults/PasswordHide.svelte +9 -9
  60. package/dist/icons/defaults/PasswordShow.svelte +7 -7
  61. package/dist/icons/defaults/SortAsc.svelte +6 -6
  62. package/dist/icons/defaults/SortDesc.svelte +6 -6
  63. package/dist/icons/defaults/SortNone.svelte +6 -6
  64. package/dist/icons/defaults/View.svelte +7 -7
  65. package/dist/types/attribute.d.ts +3 -0
  66. package/dist/types/crud/fields.d.ts +3 -0
  67. 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/columns.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/columns.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>