@beeblock/svelar-datatable 0.1.0

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 (44) hide show
  1. package/README.md +149 -0
  2. package/dist/SvelarDatatablePlugin.d.ts +13 -0
  3. package/dist/export/ExportManager.d.ts +4 -0
  4. package/dist/export/clipboard.d.ts +2 -0
  5. package/dist/export/csv.d.ts +4 -0
  6. package/dist/export/excel.d.ts +2 -0
  7. package/dist/export/index.d.ts +6 -0
  8. package/dist/export/pdf.d.ts +2 -0
  9. package/dist/export/print.d.ts +2 -0
  10. package/dist/index.d.ts +5 -0
  11. package/dist/index.js +19 -0
  12. package/dist/server/DataTableController.d.ts +4 -0
  13. package/dist/server/DataTableRequest.d.ts +3 -0
  14. package/dist/server/DataTableService.d.ts +25 -0
  15. package/dist/server/index.d.ts +3 -0
  16. package/dist/server/index.js +1 -0
  17. package/dist/state/DataTableStore.d.ts +64 -0
  18. package/dist/state/ServerDataTableStore.d.ts +23 -0
  19. package/dist/state/index.d.ts +2 -0
  20. package/dist/types.d.ts +208 -0
  21. package/dist/types.js +0 -0
  22. package/dist/ui/index.d.ts +20 -0
  23. package/package.json +45 -0
  24. package/src/ui/DataTable.svelte +385 -0
  25. package/src/ui/DataTableBody.svelte +180 -0
  26. package/src/ui/DataTableBubbleEditor.svelte +93 -0
  27. package/src/ui/DataTableButtons.svelte +139 -0
  28. package/src/ui/DataTableCell.svelte +381 -0
  29. package/src/ui/DataTableColumnToggle.svelte +111 -0
  30. package/src/ui/DataTableEditor.svelte +27 -0
  31. package/src/ui/DataTableEditorField.svelte +190 -0
  32. package/src/ui/DataTableEditorForm.svelte +94 -0
  33. package/src/ui/DataTableEmpty.svelte +40 -0
  34. package/src/ui/DataTableExpandedRow.svelte +37 -0
  35. package/src/ui/DataTableFooter.svelte +65 -0
  36. package/src/ui/DataTableHead.svelte +169 -0
  37. package/src/ui/DataTableLoading.svelte +44 -0
  38. package/src/ui/DataTableModalEditor.svelte +126 -0
  39. package/src/ui/DataTablePagination.svelte +205 -0
  40. package/src/ui/DataTableRow.svelte +192 -0
  41. package/src/ui/DataTableSearch.svelte +95 -0
  42. package/src/ui/DataTableToolbar.svelte +164 -0
  43. package/src/ui/index.ts +20 -0
  44. package/src/ui/virtual/VirtualScroller.svelte +62 -0
@@ -0,0 +1,190 @@
1
+ <script lang="ts">
2
+ import type { EditorFieldDef, DataTableStore } from '../index.js';
3
+
4
+ interface Props {
5
+ field: EditorFieldDef;
6
+ store: DataTableStore;
7
+ }
8
+ let { field, store }: Props = $props();
9
+
10
+ let state = $state(store.getState());
11
+ $effect(() => {
12
+ return store.subscribe(() => {
13
+ state = store.getState();
14
+ });
15
+ });
16
+
17
+ let value = $derived(state.formData[field.name] ?? field.defaultValue ?? '');
18
+ let error = $derived(state.validationErrors[field.name]);
19
+ let visible = $derived(field.showWhen ? field.showWhen(state.formData) : true);
20
+
21
+ function handleChange(val: any) {
22
+ store.setFormField(field.name, val);
23
+ }
24
+ </script>
25
+
26
+ {#if visible}
27
+ <div class="sdt-editor-field" class:sdt-editor-field-error={!!error}>
28
+ {#if field.type !== 'hidden'}
29
+ <label class="sdt-editor-label" for="sdt-field-{field.name}">
30
+ {field.label}
31
+ {#if field.required}<span class="sdt-required">*</span>{/if}
32
+ </label>
33
+ {/if}
34
+
35
+ {#if field.type === 'text' || field.type === 'number' || field.type === 'date' || field.type === 'datetime'}
36
+ <input
37
+ id="sdt-field-{field.name}"
38
+ type={field.type === 'datetime' ? 'datetime-local' : field.type}
39
+ class="sdt-editor-input {field.className ?? ''}"
40
+ value={value}
41
+ placeholder={field.placeholder}
42
+ disabled={field.disabled}
43
+ required={field.required}
44
+ oninput={(e) => handleChange((e.target as HTMLInputElement).value)}
45
+ />
46
+ {:else if field.type === 'textarea'}
47
+ <textarea
48
+ id="sdt-field-{field.name}"
49
+ class="sdt-editor-input sdt-editor-textarea {field.className ?? ''}"
50
+ value={value}
51
+ placeholder={field.placeholder}
52
+ disabled={field.disabled}
53
+ required={field.required}
54
+ oninput={(e) => handleChange((e.target as HTMLTextAreaElement).value)}
55
+ rows="3"
56
+ ></textarea>
57
+ {:else if field.type === 'select'}
58
+ <select
59
+ id="sdt-field-{field.name}"
60
+ class="sdt-editor-input {field.className ?? ''}"
61
+ value={value}
62
+ disabled={field.disabled}
63
+ required={field.required}
64
+ onchange={(e) => handleChange((e.target as HTMLSelectElement).value)}
65
+ >
66
+ <option value="">{field.placeholder ?? 'Select...'}</option>
67
+ {#each field.options ?? [] as opt}
68
+ <option value={opt.value}>{opt.label}</option>
69
+ {/each}
70
+ </select>
71
+ {:else if field.type === 'multi-select'}
72
+ <select
73
+ id="sdt-field-{field.name}"
74
+ class="sdt-editor-input {field.className ?? ''}"
75
+ multiple
76
+ disabled={field.disabled}
77
+ onchange={(e) => {
78
+ const selected = Array.from((e.target as HTMLSelectElement).selectedOptions).map(o => o.value);
79
+ handleChange(selected);
80
+ }}
81
+ >
82
+ {#each field.options ?? [] as opt}
83
+ <option value={opt.value} selected={Array.isArray(value) && value.includes(opt.value)}>{opt.label}</option>
84
+ {/each}
85
+ </select>
86
+ {:else if field.type === 'checkbox'}
87
+ <label class="sdt-editor-checkbox">
88
+ <input
89
+ id="sdt-field-{field.name}"
90
+ type="checkbox"
91
+ checked={!!value}
92
+ disabled={field.disabled}
93
+ onchange={(e) => handleChange((e.target as HTMLInputElement).checked)}
94
+ />
95
+ <span>{field.placeholder ?? field.label}</span>
96
+ </label>
97
+ {:else if field.type === 'radio'}
98
+ <div class="sdt-editor-radios">
99
+ {#each field.options ?? [] as opt}
100
+ <label class="sdt-editor-radio">
101
+ <input
102
+ type="radio"
103
+ name="sdt-field-{field.name}"
104
+ value={opt.value}
105
+ checked={value === opt.value}
106
+ disabled={field.disabled}
107
+ onchange={() => handleChange(opt.value)}
108
+ />
109
+ <span>{opt.label}</span>
110
+ </label>
111
+ {/each}
112
+ </div>
113
+ {:else if field.type === 'readonly'}
114
+ <div class="sdt-editor-readonly">{value}</div>
115
+ {:else if field.type === 'hidden'}
116
+ <input type="hidden" value={value} />
117
+ {/if}
118
+
119
+ {#if error}
120
+ <span class="sdt-editor-error">{error}</span>
121
+ {/if}
122
+ </div>
123
+ {/if}
124
+
125
+ <style>
126
+ .sdt-editor-field {
127
+ display: flex;
128
+ flex-direction: column;
129
+ gap: 0.25rem;
130
+ margin-bottom: 0.75rem;
131
+ }
132
+ .sdt-editor-label {
133
+ font-size: 0.8125rem;
134
+ font-weight: 500;
135
+ color: var(--sdt-text, #111827);
136
+ }
137
+ .sdt-required {
138
+ color: #ef4444;
139
+ margin-left: 0.125rem;
140
+ }
141
+ .sdt-editor-input {
142
+ padding: 0.5rem 0.625rem;
143
+ border: 1px solid var(--sdt-border, #e5e7eb);
144
+ border-radius: 0.375rem;
145
+ font-size: 0.875rem;
146
+ background: var(--sdt-input-bg, #fff);
147
+ color: var(--sdt-text, #111827);
148
+ outline: none;
149
+ transition: border-color 0.15s;
150
+ }
151
+ .sdt-editor-input:focus {
152
+ border-color: var(--sdt-primary, #3b82f6);
153
+ box-shadow: 0 0 0 2px var(--sdt-primary-ring, rgba(59, 130, 246, 0.2));
154
+ }
155
+ .sdt-editor-input:disabled {
156
+ opacity: 0.6;
157
+ cursor: not-allowed;
158
+ }
159
+ .sdt-editor-textarea {
160
+ resize: vertical;
161
+ min-height: 4rem;
162
+ }
163
+ .sdt-editor-checkbox,
164
+ .sdt-editor-radio {
165
+ display: flex;
166
+ align-items: center;
167
+ gap: 0.5rem;
168
+ font-size: 0.875rem;
169
+ cursor: pointer;
170
+ }
171
+ .sdt-editor-radios {
172
+ display: flex;
173
+ flex-direction: column;
174
+ gap: 0.375rem;
175
+ }
176
+ .sdt-editor-readonly {
177
+ padding: 0.5rem 0.625rem;
178
+ background: var(--sdt-hover, #f3f4f6);
179
+ border-radius: 0.375rem;
180
+ font-size: 0.875rem;
181
+ color: var(--sdt-text-muted, #6b7280);
182
+ }
183
+ .sdt-editor-error {
184
+ font-size: 0.75rem;
185
+ color: #ef4444;
186
+ }
187
+ .sdt-editor-field-error .sdt-editor-input {
188
+ border-color: #ef4444;
189
+ }
190
+ </style>
@@ -0,0 +1,94 @@
1
+ <script lang="ts">
2
+ import type { EditorFieldDef, DataTableStore } from '../index.js';
3
+ import DataTableEditorField from './DataTableEditorField.svelte';
4
+
5
+ interface Props {
6
+ fields: EditorFieldDef[];
7
+ store: DataTableStore;
8
+ onsubmit: (formData: Record<string, any>) => void | Promise<void>;
9
+ oncancel: () => void;
10
+ submitLabel?: string;
11
+ }
12
+ let { fields, store, onsubmit, oncancel, submitLabel = 'Save' }: Props = $props();
13
+
14
+ let submitting = $state(false);
15
+
16
+ async function handleSubmit(e: Event) {
17
+ e.preventDefault();
18
+ submitting = true;
19
+ try {
20
+ const state = store.getState();
21
+ await onsubmit(state.formData);
22
+ } finally {
23
+ submitting = false;
24
+ }
25
+ }
26
+ </script>
27
+
28
+ <form class="sdt-editor-form" onsubmit={handleSubmit}>
29
+ <div class="sdt-editor-fields">
30
+ {#each fields as field}
31
+ <DataTableEditorField {field} {store} />
32
+ {/each}
33
+ </div>
34
+
35
+ <div class="sdt-editor-actions">
36
+ <button type="button" class="sdt-btn sdt-btn-cancel" onclick={oncancel} disabled={submitting}>
37
+ Cancel
38
+ </button>
39
+ <button type="submit" class="sdt-btn sdt-btn-submit" disabled={submitting}>
40
+ {#if submitting}
41
+ Saving...
42
+ {:else}
43
+ {submitLabel}
44
+ {/if}
45
+ </button>
46
+ </div>
47
+ </form>
48
+
49
+ <style>
50
+ .sdt-editor-form {
51
+ display: flex;
52
+ flex-direction: column;
53
+ gap: 0.5rem;
54
+ }
55
+ .sdt-editor-fields {
56
+ display: flex;
57
+ flex-direction: column;
58
+ }
59
+ .sdt-editor-actions {
60
+ display: flex;
61
+ justify-content: flex-end;
62
+ gap: 0.5rem;
63
+ padding-top: 0.5rem;
64
+ border-top: 1px solid var(--sdt-border, #e5e7eb);
65
+ }
66
+ .sdt-btn {
67
+ padding: 0.5rem 1rem;
68
+ border-radius: 0.375rem;
69
+ font-size: 0.8125rem;
70
+ font-weight: 500;
71
+ cursor: pointer;
72
+ border: 1px solid transparent;
73
+ transition: background-color 0.15s;
74
+ }
75
+ .sdt-btn:disabled {
76
+ opacity: 0.5;
77
+ cursor: not-allowed;
78
+ }
79
+ .sdt-btn-cancel {
80
+ background: var(--sdt-input-bg, #fff);
81
+ border-color: var(--sdt-border, #e5e7eb);
82
+ color: var(--sdt-text, #111827);
83
+ }
84
+ .sdt-btn-cancel:hover:not(:disabled) {
85
+ background: var(--sdt-hover, #f3f4f6);
86
+ }
87
+ .sdt-btn-submit {
88
+ background: var(--sdt-primary, #3b82f6);
89
+ color: #fff;
90
+ }
91
+ .sdt-btn-submit:hover:not(:disabled) {
92
+ background: var(--sdt-primary-hover, #2563eb);
93
+ }
94
+ </style>
@@ -0,0 +1,40 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ text?: string;
4
+ colSpan?: number;
5
+ }
6
+ let { text = 'No data available', colSpan = 1 }: Props = $props();
7
+ </script>
8
+
9
+ <tr class="sdt-empty-row">
10
+ <td colspan={colSpan} class="sdt-empty-cell">
11
+ <div class="sdt-empty-content">
12
+ <svg class="sdt-empty-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
13
+ <path d="M20 13V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v7m16 0v5a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-5m16 0h-2.586a1 1 0 0 0-.707.293l-2.414 2.414a1 1 0 0 1-.707.293h-3.172a1 1 0 0 1-.707-.293l-2.414-2.414A1 1 0 0 0 6.586 13H4" />
14
+ </svg>
15
+ <span>{text}</span>
16
+ </div>
17
+ </td>
18
+ </tr>
19
+
20
+ <style>
21
+ .sdt-empty-row {
22
+ height: 120px;
23
+ }
24
+ .sdt-empty-cell {
25
+ text-align: center;
26
+ padding: 2rem;
27
+ color: var(--sdt-text-muted, #6b7280);
28
+ }
29
+ .sdt-empty-content {
30
+ display: flex;
31
+ flex-direction: column;
32
+ align-items: center;
33
+ gap: 0.5rem;
34
+ }
35
+ .sdt-empty-icon {
36
+ width: 2rem;
37
+ height: 2rem;
38
+ opacity: 0.5;
39
+ }
40
+ </style>
@@ -0,0 +1,37 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ interface Props {
5
+ row: any;
6
+ colSpan: number;
7
+ expandContent?: Snippet<[{ row: any }]>;
8
+ }
9
+ let { row, colSpan, expandContent }: Props = $props();
10
+ </script>
11
+
12
+ <tr class="sdt-expanded-row">
13
+ <td colspan={colSpan} class="sdt-expanded-cell">
14
+ {#if expandContent}
15
+ {@render expandContent({ row })}
16
+ {:else}
17
+ <pre class="sdt-expanded-default">{JSON.stringify(row, null, 2)}</pre>
18
+ {/if}
19
+ </td>
20
+ </tr>
21
+
22
+ <style>
23
+ .sdt-expanded-row {
24
+ background: var(--sdt-expanded-bg, #f9fafb);
25
+ }
26
+ .sdt-expanded-cell {
27
+ padding: 1rem;
28
+ border-bottom: 1px solid var(--sdt-border, #e5e7eb);
29
+ }
30
+ .sdt-expanded-default {
31
+ margin: 0;
32
+ font-size: 0.75rem;
33
+ color: var(--sdt-text-muted, #6b7280);
34
+ white-space: pre-wrap;
35
+ word-break: break-all;
36
+ }
37
+ </style>
@@ -0,0 +1,65 @@
1
+ <script lang="ts">
2
+ import type { ColumnDef, DataTableStore, SelectionMode, DataTableClassNames } from '../index.js';
3
+
4
+ interface Props {
5
+ columns: ColumnDef[];
6
+ store: DataTableStore;
7
+ selectable?: SelectionMode;
8
+ expandable?: boolean;
9
+ classNames?: DataTableClassNames;
10
+ }
11
+ let { columns, store, selectable = 'none', expandable = false, classNames = {} }: Props = $props();
12
+
13
+ let state = $state(store.getState());
14
+ $effect(() => {
15
+ return store.subscribe(() => {
16
+ state = store.getState();
17
+ });
18
+ });
19
+
20
+ let visibleColumns = $derived(
21
+ state.columnOrder
22
+ .filter((key) => state.columnVisibility[key] !== false)
23
+ .map((key) => columns.find((c) => c.key === key))
24
+ .filter(Boolean) as ColumnDef[]
25
+ );
26
+
27
+ let hasFooter = $derived(visibleColumns.some((c) => c.footer !== undefined));
28
+
29
+ function getFooterValue(col: ColumnDef): string {
30
+ if (!col.footer) return '';
31
+ if (typeof col.footer === 'function') {
32
+ return String(col.footer(state.filteredRows));
33
+ }
34
+ return col.footer;
35
+ }
36
+ </script>
37
+
38
+ {#if hasFooter}
39
+ <tfoot class="sdt-tfoot {classNames.tfoot ?? ''}">
40
+ <tr>
41
+ {#if selectable !== 'none'}
42
+ <td class="sdt-tf {classNames.tf ?? ''}"></td>
43
+ {/if}
44
+ {#if expandable}
45
+ <td class="sdt-tf {classNames.tf ?? ''}"></td>
46
+ {/if}
47
+ {#each visibleColumns as col}
48
+ <td class="sdt-tf {classNames.tf ?? ''}">{getFooterValue(col)}</td>
49
+ {/each}
50
+ </tr>
51
+ </tfoot>
52
+ {/if}
53
+
54
+ <style>
55
+ .sdt-tfoot {
56
+ background: var(--sdt-head-bg, #f9fafb);
57
+ }
58
+ .sdt-tf {
59
+ padding: var(--sdt-cell-padding, 0.625rem 0.75rem);
60
+ border-top: 2px solid var(--sdt-border, #e5e7eb);
61
+ font-size: 0.8125rem;
62
+ font-weight: 600;
63
+ color: var(--sdt-text, #111827);
64
+ }
65
+ </style>
@@ -0,0 +1,169 @@
1
+ <script lang="ts">
2
+ import type { ColumnDef, DataTableStore, SelectionMode, DataTableClassNames } from '../index.js';
3
+
4
+ interface Props {
5
+ columns: ColumnDef[];
6
+ store: DataTableStore;
7
+ selectable?: SelectionMode;
8
+ sortable?: boolean;
9
+ expandable?: boolean;
10
+ classNames?: DataTableClassNames;
11
+ }
12
+ let { columns, store, selectable = 'none', sortable = true, expandable = false, classNames = {} }: Props = $props();
13
+
14
+ let state = $state(store.getState());
15
+ $effect(() => {
16
+ return store.subscribe(() => {
17
+ state = store.getState();
18
+ });
19
+ });
20
+
21
+ let visibleColumns = $derived(
22
+ state.columnOrder
23
+ .filter((key) => state.columnVisibility[key] !== false)
24
+ .map((key) => columns.find((c) => c.key === key))
25
+ .filter(Boolean) as ColumnDef[]
26
+ );
27
+
28
+ let allSelected = $derived(() => {
29
+ if (state.filteredRows.length === 0) return false;
30
+ return state.filteredRows.every((r) => state.selectedIds.has(store.getRowId(r)));
31
+ });
32
+
33
+ function getSortDirection(key: string): 'asc' | 'desc' | null {
34
+ const s = state.sort.find((s) => s.column === key);
35
+ return s?.direction ?? null;
36
+ }
37
+
38
+ function getSortIndex(key: string): number {
39
+ if (state.sort.length <= 1) return -1;
40
+ return state.sort.findIndex((s) => s.column === key);
41
+ }
42
+
43
+ function handleSort(col: ColumnDef, e: MouseEvent) {
44
+ if (!sortable || col.sortable === false) return;
45
+ store.toggleSort(col.key, e.shiftKey);
46
+ }
47
+
48
+ function handleSelectAll() {
49
+ if (allSelected()) {
50
+ store.deselectAll();
51
+ } else {
52
+ store.selectAll();
53
+ }
54
+ }
55
+ </script>
56
+
57
+ <thead class="sdt-thead {classNames.thead ?? ''}">
58
+ <tr>
59
+ {#if selectable === 'multi'}
60
+ <th class="sdt-th sdt-th-checkbox {classNames.th ?? ''}" style="width: 40px;">
61
+ <input
62
+ type="checkbox"
63
+ checked={allSelected()}
64
+ onchange={handleSelectAll}
65
+ aria-label="Select all"
66
+ />
67
+ </th>
68
+ {:else if selectable === 'single'}
69
+ <th class="sdt-th sdt-th-checkbox {classNames.th ?? ''}" style="width: 40px;"></th>
70
+ {/if}
71
+
72
+ {#if expandable}
73
+ <th class="sdt-th sdt-th-expand {classNames.th ?? ''}" style="width: 32px;"></th>
74
+ {/if}
75
+
76
+ {#each visibleColumns as col}
77
+ <th
78
+ class="sdt-th {col.headerClassName ?? ''} {classNames.th ?? ''}"
79
+ class:sdt-th-sortable={sortable && col.sortable !== false}
80
+ style:width={col.width}
81
+ style:min-width={col.minWidth}
82
+ style:max-width={col.maxWidth}
83
+ >
84
+ {#if sortable && col.sortable !== false}
85
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
86
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
87
+ <div class="sdt-th-sort" onclick={(e) => handleSort(col, e)}>
88
+ <span>{col.header}</span>
89
+ <span class="sdt-sort-indicator">
90
+ {#if getSortDirection(col.key) === 'asc'}
91
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
92
+ <path d="m18 15-6-6-6 6" />
93
+ </svg>
94
+ {:else if getSortDirection(col.key) === 'desc'}
95
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
96
+ <path d="m6 9 6 6 6-6" />
97
+ </svg>
98
+ {:else}
99
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14" class="sdt-sort-idle">
100
+ <path d="m7 15 5 5 5-5M7 9l5-5 5 5" />
101
+ </svg>
102
+ {/if}
103
+ {#if getSortIndex(col.key) >= 0}
104
+ <span class="sdt-sort-index">{getSortIndex(col.key) + 1}</span>
105
+ {/if}
106
+ </span>
107
+ </div>
108
+ {:else}
109
+ <span>{col.header}</span>
110
+ {/if}
111
+ </th>
112
+ {/each}
113
+ </tr>
114
+ </thead>
115
+
116
+ <style>
117
+ .sdt-thead {
118
+ position: sticky;
119
+ top: 0;
120
+ z-index: 10;
121
+ background: var(--sdt-head-bg, #f9fafb);
122
+ }
123
+ .sdt-th {
124
+ padding: var(--sdt-cell-padding, 0.625rem 0.75rem);
125
+ text-align: left;
126
+ font-size: 0.75rem;
127
+ font-weight: 600;
128
+ text-transform: uppercase;
129
+ letter-spacing: 0.05em;
130
+ color: var(--sdt-text-muted, #6b7280);
131
+ border-bottom: 2px solid var(--sdt-border, #e5e7eb);
132
+ white-space: nowrap;
133
+ user-select: none;
134
+ }
135
+ .sdt-th-expand {
136
+ padding: var(--sdt-cell-padding, 0.625rem 0.5rem);
137
+ }
138
+ .sdt-th-checkbox {
139
+ text-align: center;
140
+ }
141
+ .sdt-th-checkbox input {
142
+ accent-color: var(--sdt-primary, #3b82f6);
143
+ }
144
+ .sdt-th-sortable {
145
+ cursor: pointer;
146
+ }
147
+ .sdt-th-sortable:hover {
148
+ color: var(--sdt-text, #111827);
149
+ background: var(--sdt-hover, #f3f4f6);
150
+ }
151
+ .sdt-th-sort {
152
+ display: inline-flex;
153
+ align-items: center;
154
+ gap: 0.25rem;
155
+ }
156
+ .sdt-sort-indicator {
157
+ display: inline-flex;
158
+ align-items: center;
159
+ gap: 0.125rem;
160
+ }
161
+ .sdt-sort-idle {
162
+ opacity: 0.3;
163
+ }
164
+ .sdt-sort-index {
165
+ font-size: 0.625rem;
166
+ color: var(--sdt-primary, #3b82f6);
167
+ font-weight: 700;
168
+ }
169
+ </style>
@@ -0,0 +1,44 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ text?: string;
4
+ colSpan?: number;
5
+ }
6
+ let { text = 'Loading...', colSpan = 1 }: Props = $props();
7
+ </script>
8
+
9
+ <tr class="sdt-loading-row">
10
+ <td colspan={colSpan} class="sdt-loading-cell">
11
+ <div class="sdt-loading-content">
12
+ <svg class="sdt-spinner" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
13
+ <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25" />
14
+ <path d="M12 2a10 10 0 0 1 10 10" stroke="currentColor" stroke-width="3" stroke-linecap="round" />
15
+ </svg>
16
+ <span>{text}</span>
17
+ </div>
18
+ </td>
19
+ </tr>
20
+
21
+ <style>
22
+ .sdt-loading-row {
23
+ height: 120px;
24
+ }
25
+ .sdt-loading-cell {
26
+ text-align: center;
27
+ padding: 2rem;
28
+ color: var(--sdt-text-muted, #6b7280);
29
+ }
30
+ .sdt-loading-content {
31
+ display: flex;
32
+ align-items: center;
33
+ justify-content: center;
34
+ gap: 0.5rem;
35
+ }
36
+ .sdt-spinner {
37
+ width: 1.25rem;
38
+ height: 1.25rem;
39
+ animation: sdt-spin 0.75s linear infinite;
40
+ }
41
+ @keyframes sdt-spin {
42
+ to { transform: rotate(360deg); }
43
+ }
44
+ </style>