runeforge 0.0.53 → 0.0.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +1342 -1222
  3. package/dist/components/Avatar.svelte +31 -31
  4. package/dist/components/IconRenderer.svelte +22 -22
  5. package/dist/components/Modal.svelte +75 -75
  6. package/dist/components/common/Header.svelte +40 -40
  7. package/dist/components/crud/EmbeddedField.svelte +175 -175
  8. package/dist/components/crud/Field.svelte +376 -376
  9. package/dist/components/crud/GenericCRUD.svelte +426 -435
  10. package/dist/components/crud/GenericCRUD.svelte.d.ts +6 -9
  11. package/dist/components/crud/SearchInput.svelte +53 -53
  12. package/dist/components/crud/columns/Avatar.svelte +15 -15
  13. package/dist/components/crud/columns/Icon.svelte +8 -8
  14. package/dist/components/crud/utils/reorder.d.ts +4 -0
  15. package/dist/components/crud/utils/reorder.js +13 -0
  16. package/dist/components/crud/views/Create.svelte +232 -232
  17. package/dist/components/crud/views/Read.svelte +124 -124
  18. package/dist/components/crud/views/Update.svelte +208 -208
  19. package/dist/components/crud/views/list/List.svelte +291 -0
  20. package/dist/components/crud/views/{List.svelte.d.ts → list/List.svelte.d.ts} +4 -14
  21. package/dist/components/crud/views/list/Modals.svelte +56 -0
  22. package/dist/components/crud/views/list/Modals.svelte.d.ts +36 -0
  23. package/dist/components/crud/views/list/Table.svelte +176 -0
  24. package/dist/components/crud/views/list/Table.svelte.d.ts +59 -0
  25. package/dist/components/crud/views/list/Toolbar.svelte +341 -0
  26. package/dist/components/crud/views/list/Toolbar.svelte.d.ts +46 -0
  27. package/dist/components/form/Button.svelte +27 -27
  28. package/dist/components/form/Label.svelte +37 -37
  29. package/dist/components/form/MultiSelect.svelte +248 -248
  30. package/dist/components/form/PasswordInput.svelte +68 -68
  31. package/dist/components/form/Required.svelte +1 -1
  32. package/dist/components/form/Select.svelte +209 -209
  33. package/dist/components/form/Tree.svelte +62 -62
  34. package/dist/components/form/TreeNode.svelte +66 -66
  35. package/dist/components/navigation/Breadcrumbs.svelte +111 -111
  36. package/dist/components/table/ColumnFilter.svelte +168 -168
  37. package/dist/components/table/PaginatedTable.svelte +536 -215
  38. package/dist/components/table/PaginatedTable.svelte.d.ts +11 -2
  39. package/dist/components/table/Paginator.svelte +113 -113
  40. package/dist/components/table/SortHeader.svelte +43 -43
  41. package/dist/components/table/TableBody.svelte +150 -70
  42. package/dist/components/table/TableBody.svelte.d.ts +14 -1
  43. package/dist/components/table/TableHeader.svelte +88 -83
  44. package/dist/components/table/TableHeader.svelte.d.ts +1 -0
  45. package/dist/components/table/sortable.d.ts +27 -0
  46. package/dist/components/table/sortable.js +55 -0
  47. package/dist/components/table/utils.d.ts +28 -1
  48. package/dist/components/table/utils.js +75 -0
  49. package/dist/i18n/en.js +2 -0
  50. package/dist/i18n/es.js +2 -0
  51. package/dist/i18n/types.d.ts +2 -0
  52. package/dist/icons/defaults/Clear.svelte +6 -6
  53. package/dist/icons/defaults/Create.svelte +6 -6
  54. package/dist/icons/defaults/Delete.svelte +6 -6
  55. package/dist/icons/defaults/Download.svelte +7 -7
  56. package/dist/icons/defaults/Edit.svelte +7 -7
  57. package/dist/icons/defaults/Filter.svelte +6 -6
  58. package/dist/icons/defaults/FilterActive.svelte +6 -6
  59. package/dist/icons/defaults/Folder.svelte +6 -6
  60. package/dist/icons/defaults/Grip.svelte +7 -0
  61. package/dist/icons/defaults/Grip.svelte.d.ts +7 -0
  62. package/dist/icons/defaults/Home.svelte +6 -6
  63. package/dist/icons/defaults/PasswordHide.svelte +9 -9
  64. package/dist/icons/defaults/PasswordShow.svelte +7 -7
  65. package/dist/icons/defaults/SortAsc.svelte +6 -6
  66. package/dist/icons/defaults/SortDesc.svelte +6 -6
  67. package/dist/icons/defaults/SortNone.svelte +6 -6
  68. package/dist/icons/defaults/View.svelte +7 -7
  69. package/dist/icons/sets/bootstrap.js +2 -1
  70. package/dist/icons/sets/default.js +2 -0
  71. package/dist/icons/types.d.ts +3 -0
  72. package/dist/index.d.ts +5 -4
  73. package/dist/index.js +3 -2
  74. package/dist/types/attribute.d.ts +9 -0
  75. package/dist/types/crud.d.ts +72 -1
  76. package/dist/types/table.d.ts +56 -0
  77. package/package.json +3 -1
  78. package/dist/components/crud/views/List.svelte +0 -584
@@ -1,6 +1,6 @@
1
1
  import { SvelteSet } from 'svelte/reactivity';
2
- import type { FilterSnapshot, ServerPagination, SortDirection, TableQuery } from '../../types/table.js';
3
- import type { Snippet } from 'svelte';
2
+ import type { FilterSnapshot, ReorderOptions, ServerPagination, SortDirection, TableQuery } from '../../types/table.js';
3
+ import { type Snippet } from 'svelte';
4
4
  import type { ColumnDefinition } from '../../types/crud.js';
5
5
  declare function $$render<T extends object = Record<string, unknown>>(): {
6
6
  props: {
@@ -27,6 +27,15 @@ declare function $$render<T extends object = Record<string, unknown>>(): {
27
27
  /** Current ordering + filters snapshot, kept in sync for callers that
28
28
  * need to replicate the active query (e.g. exporting server-side). */
29
29
  query?: TableQuery;
30
+ /** Turns on drag-to-reorder. Per-column filters are suppressed and row
31
+ * order is fully owned by `reorder.compare`/`reorder.attribute` while
32
+ * it's active — ignored entirely in server-pagination mode
33
+ * (`pagination` set), since the full row set needs to be reachable
34
+ * client-side for drag positions to be meaningful. */
35
+ reorder?: ReorderOptions<T>;
36
+ /** Fires after a drag settles with the complete reordered row list
37
+ * (client mode only). The caller decides how to persist it. */
38
+ onReorder?: (rows: T[]) => void;
30
39
  };
31
40
  exports: {};
32
41
  bindings: "selected" | "visibleRows" | "query";
@@ -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,70 +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 } from '../../types/table.js';
6
-
7
- let {
8
- columns,
9
- rows,
10
- selectable,
11
- selected,
12
- onToggle,
13
- colCount,
14
- rowActions,
15
- }: {
16
- columns: ColumnDefinition<T>[];
17
- rows: IndexedRow<T>[];
18
- selectable: boolean;
19
- selected: SvelteSet<number>;
20
- onToggle: (index: number) => void;
21
- colCount: number;
22
- rowActions?: Snippet<[T]>;
23
- } = $props();
24
- </script>
25
-
26
- <tbody data-testid="paginated-table-body">
27
- {#if rows.length === 0}
28
- <tr>
29
- <td colspan={colCount} class="py-10 text-center text-base-content/50">
30
- Sin registros
31
- </td>
32
- </tr>
33
- {:else}
34
- {#each rows as { row, index } (index)}
35
- <tr
36
- class="hover"
37
- class:cursor-pointer={selectable}
38
- onclick={selectable ? () => onToggle(index) : undefined}
39
- >
40
- {#if selectable}
41
- <td>
42
- <input
43
- type="checkbox"
44
- class="checkbox checkbox-sm"
45
- checked={selected.has(index)}
46
- onchange={() => onToggle(index)}
47
- onclick={(e) => e.stopPropagation()}
48
- />
49
- </td>
50
- {/if}
51
- {#each columns as col (col.attribute)}
52
- <td>
53
- {#if col.component}
54
- {@const Cell = col.component}
55
- <Cell value={row[col.attribute]} row={row} />
56
- {:else if col.formatter}
57
- <!-- eslint-disable-next-line svelte/no-at-html-tags -->
58
- {@html col.formatter(row[col.attribute], row)}
59
- {:else}
60
- {String(row[col.attribute] ?? '')}
61
- {/if}
62
- </td>
63
- {/each}
64
- {#if rowActions}
65
- <td class="text-right">{@render rowActions(row)}</td>
66
- {/if}
67
- </tr>
68
- {/each}
69
- {/if}
70
- </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>
@@ -1,7 +1,7 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { SvelteSet } from 'svelte/reactivity';
3
3
  import type { ColumnDefinition } from '../../types/crud.js';
4
- import type { IndexedRow } from '../../types/table.js';
4
+ import type { IndexedRow, ReorderOptions } from '../../types/table.js';
5
5
  declare function $$render<T extends object>(): {
6
6
  props: {
7
7
  columns: ColumnDefinition<T>[];
@@ -11,6 +11,19 @@ declare function $$render<T extends object>(): {
11
11
  onToggle: (index: number) => void;
12
12
  colCount: number;
13
13
  rowActions?: Snippet<[T]>;
14
+ reorder?: ReorderOptions<T>;
15
+ /** Reorder mode only: positions within `rows` (not the `index` field)
16
+ * that are actually on screen — the rest render `hidden` so they stay in
17
+ * the DOM (and reachable by SortableJS) across a page flip mid-drag. */
18
+ visibleRange?: {
19
+ start: number;
20
+ end: number;
21
+ };
22
+ onDragStart?: () => void;
23
+ /** Fires once a drag settles, with the complete row list in its new
24
+ * order — computed from SortableJS's own before/after indices, correct
25
+ * uniformly for a single-row drag and a `multiDrag` group move alike. */
26
+ onReorder?: (rows: IndexedRow<T>[]) => void;
14
27
  };
15
28
  exports: {};
16
29
  bindings: "";