@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
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@beeblock/svelar-datatable",
3
+ "version": "0.1.0",
4
+ "description": "Full-featured DataTable plugin for Svelar — sorting, searching, pagination, inline editing, export, and server-side processing",
5
+ "type": "module",
6
+ "keywords": ["svelar-plugin", "datatable", "svelte", "sveltekit", "data-grid"],
7
+ "license": "MIT",
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "exports": {
11
+ "./package.json": "./package.json",
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "./server": {
17
+ "types": "./dist/server/index.d.ts",
18
+ "default": "./dist/server/index.js"
19
+ },
20
+ "./types": {
21
+ "types": "./dist/types.d.ts",
22
+ "default": "./dist/types.js"
23
+ },
24
+ "./ui": "./src/ui/index.ts",
25
+ "./ui/*": "./src/ui/*"
26
+ },
27
+ "files": ["dist", "src/ui", "LICENSE"],
28
+ "scripts": {
29
+ "build": "tsup && (tsc --emitDeclarationOnly || echo 'Warning: tsc declaration emit had errors')",
30
+ "dev": "tsup --watch"
31
+ },
32
+ "peerDependencies": {
33
+ "@beeblock/svelar": ">=0.4.0",
34
+ "svelte": "^5.0.0"
35
+ },
36
+ "peerDependenciesMeta": {
37
+ "exceljs": { "optional": true },
38
+ "@beeblock/svelar": { "optional": false }
39
+ },
40
+ "devDependencies": {
41
+ "tsup": "^8.5.0",
42
+ "typescript": "^5.7.0",
43
+ "svelte": "^5.0.0"
44
+ }
45
+ }
@@ -0,0 +1,385 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { DataTableConfig, DataTableClassNames, ColumnDef, EditorFieldDef } from '../types.js';
4
+ import { DataTableStore } from '../state/DataTableStore.js';
5
+ import { ServerDataTableStore } from '../state/ServerDataTableStore.js';
6
+ import DataTableToolbar from './DataTableToolbar.svelte';
7
+ import DataTableHead from './DataTableHead.svelte';
8
+ import DataTableBody from './DataTableBody.svelte';
9
+ import DataTableFooter from './DataTableFooter.svelte';
10
+ import DataTablePagination from './DataTablePagination.svelte';
11
+ import DataTableEditor from './DataTableEditor.svelte';
12
+ import { onMount } from 'svelte';
13
+
14
+ interface Props extends DataTableConfig {
15
+ // Snippet overrides
16
+ customCell?: Snippet<[{ row: any; column: ColumnDef; value: any }]>;
17
+ expandContent?: Snippet<[{ row: any }]>;
18
+ // Expose store for external control (e.g. opening editor)
19
+ storeRef?: DataTableStore;
20
+ }
21
+
22
+ let {
23
+ // Data source
24
+ data,
25
+ serverUrl,
26
+ serverMethod,
27
+ // Columns
28
+ columns,
29
+ // Features
30
+ sortable = true,
31
+ searchable = true,
32
+ paginate = true,
33
+ selectable = 'none',
34
+ // Pagination
35
+ perPage = 15,
36
+ perPageOptions = [10, 15, 25, 50, 100],
37
+ // Search
38
+ searchDebounceMs = 300,
39
+ // State persistence
40
+ stateSaveKey,
41
+ // Row identity
42
+ rowId = 'id',
43
+ // Row appearance
44
+ rowClass,
45
+ // Buttons
46
+ buttons,
47
+ // Editor
48
+ editorMode,
49
+ editorFields,
50
+ // Callbacks
51
+ onSort,
52
+ onFilter,
53
+ onPageChange,
54
+ onSelect,
55
+ onRowClick,
56
+ onEdit,
57
+ onCellEdit,
58
+ onCreate,
59
+ onDelete,
60
+ // Virtual scroll
61
+ virtualScroll = false,
62
+ virtualRowHeight = 48,
63
+ // Features
64
+ responsive = true,
65
+ groupBy,
66
+ expandable = false,
67
+ // Text
68
+ emptyText = 'No data available',
69
+ loadingText = 'Loading...',
70
+ // CSS
71
+ className = '',
72
+ compact = false,
73
+ striped = true,
74
+ hover = true,
75
+ bordered = false,
76
+ classNames = {} as DataTableClassNames,
77
+ unstyled = false,
78
+ // Snippets
79
+ customCell,
80
+ expandContent,
81
+ // Store ref
82
+ storeRef = $bindable(),
83
+ }: Props = $props();
84
+
85
+ // Build config object
86
+ let config: DataTableConfig = $derived({
87
+ data, serverUrl, serverMethod, columns, sortable, searchable, paginate,
88
+ selectable, perPage, perPageOptions, searchDebounceMs, stateSaveKey,
89
+ rowId, rowClass, buttons, editorMode, editorFields,
90
+ onSort, onFilter, onPageChange, onSelect, onRowClick, onEdit, onCellEdit, onCreate, onDelete,
91
+ virtualScroll, virtualRowHeight, responsive, groupBy, expandable,
92
+ emptyText, loadingText, className, compact, striped, hover, bordered, classNames, unstyled,
93
+ });
94
+
95
+ // Create store
96
+ let store: DataTableStore = $state(
97
+ serverUrl
98
+ ? new ServerDataTableStore(config)
99
+ : new DataTableStore(config)
100
+ );
101
+
102
+ // Expose store ref for external control
103
+ $effect(() => {
104
+ storeRef = store;
105
+ });
106
+
107
+ let state = $state(store.getState());
108
+
109
+ // Subscribe to store changes
110
+ $effect(() => {
111
+ return store.subscribe(() => {
112
+ state = store.getState();
113
+
114
+ // Fire callbacks
115
+ if (onSort) onSort(state.sort);
116
+ if (onFilter) onFilter(state.filters);
117
+ if (onSelect) onSelect(store.getSelectedRows());
118
+ if (onPageChange) onPageChange(state.pagination.page, state.pagination.perPage);
119
+ });
120
+ });
121
+
122
+ // Update data when prop changes (client-side only)
123
+ $effect(() => {
124
+ if (data && !serverUrl) {
125
+ store.setData(data);
126
+ }
127
+ });
128
+
129
+ // Initial server fetch
130
+ onMount(() => {
131
+ if (serverUrl && store instanceof ServerDataTableStore) {
132
+ store.initialFetch();
133
+ }
134
+ return () => {
135
+ if (store instanceof ServerDataTableStore) {
136
+ store.destroy();
137
+ }
138
+ };
139
+ });
140
+
141
+ // Editor submit handler
142
+ async function handleEditorSubmit(formData: Record<string, any>) {
143
+ const isCreate = state.editingRowId === null;
144
+ try {
145
+ if (isCreate && onCreate) {
146
+ await onCreate(formData);
147
+ } else if (!isCreate && onEdit) {
148
+ const row = state.allRows.find((r) => store.getRowId(r) === state.editingRowId);
149
+ if (row) await onEdit(row, formData);
150
+ }
151
+ store.closeEditor();
152
+ } catch (err: any) {
153
+ if (err.errors) {
154
+ store.setValidationErrors(err.errors);
155
+ }
156
+ }
157
+ }
158
+
159
+ // Inline mode: handle cell edit commit
160
+ async function handleInlineCommit(row: any, columnKey: string, newValue: any, oldValue: any) {
161
+ try {
162
+ if (onCellEdit) {
163
+ await onCellEdit(row, columnKey, newValue, oldValue);
164
+ }
165
+ // Update local data
166
+ (row as any)[columnKey] = newValue;
167
+ } catch (err: any) {
168
+ // Revert on error
169
+ (row as any)[columnKey] = oldValue;
170
+ store.setError(err.message ?? 'Failed to save cell');
171
+ }
172
+ }
173
+
174
+ // Excel mode: commit cell edit and fire callback
175
+ async function handleExcelCommit() {
176
+ const result = store.commitCellEdit();
177
+ if (!result) return;
178
+ try {
179
+ if (onCellEdit) {
180
+ await onCellEdit(result.row, result.columnKey, result.newValue, result.oldValue);
181
+ }
182
+ } catch (err: any) {
183
+ // Revert on error
184
+ (result.row as any)[result.columnKey] = result.oldValue;
185
+ store.setError(err.message ?? 'Failed to save cell');
186
+ }
187
+ }
188
+
189
+ // Excel mode: navigate with cross-page support
190
+ function handleExcelNavigate(direction: 'up' | 'down' | 'left' | 'right') {
191
+ // Commit any pending edit first
192
+ const result = store.commitCellEdit();
193
+ if (result && onCellEdit) {
194
+ onCellEdit(result.row, result.columnKey, result.newValue, result.oldValue).catch(() => {
195
+ (result.row as any)[result.columnKey] = result.oldValue;
196
+ });
197
+ }
198
+
199
+ const pageResult = store.navigateCell(direction);
200
+ if (!pageResult) return;
201
+
202
+ const pagination = store.getState().pagination;
203
+ const visibleCols = store.getVisibleColumns();
204
+ const firstEditableCol = visibleCols.find((c) => c.editable !== false);
205
+ if (!firstEditableCol) return;
206
+
207
+ if (pageResult === 'page-next' && pagination.page < pagination.lastPage) {
208
+ store.setPage(pagination.page + 1);
209
+ // Focus first row of new page after recompute
210
+ setTimeout(() => store.focusCell(0, store.getState().excelFocusedCell?.columnKey ?? firstEditableCol.key), 0);
211
+ } else if (pageResult === 'page-prev' && pagination.page > 1) {
212
+ store.setPage(pagination.page - 1);
213
+ // Focus last row of previous page after recompute
214
+ setTimeout(() => {
215
+ const rows = store.getState().paginatedRows;
216
+ const colKey = store.getState().excelFocusedCell?.columnKey ?? firstEditableCol.key;
217
+ store.focusCell(rows.length - 1, colKey);
218
+ }, 0);
219
+ }
220
+ }
221
+
222
+ // Bubble editor anchor tracking
223
+ let bubbleAnchor: HTMLElement | null = $state(null);
224
+
225
+ // Virtual scroll tracking
226
+ let wrapperEl: HTMLDivElement | undefined = $state();
227
+ let scrollTop = $state(0);
228
+ let wrapperHeight = $state(400);
229
+
230
+ function handleWrapperScroll(e: Event) {
231
+ scrollTop = (e.target as HTMLDivElement).scrollTop;
232
+ }
233
+
234
+ // Table CSS classes
235
+ let tableClasses = $derived([
236
+ 'sdt-table',
237
+ compact ? 'sdt-compact' : '',
238
+ striped ? 'sdt-striped' : '',
239
+ hover ? 'sdt-hover' : '',
240
+ bordered ? 'sdt-bordered' : '',
241
+ responsive ? 'sdt-responsive' : '',
242
+ editorMode === 'excel' ? 'sdt-excel' : '',
243
+ className,
244
+ ].filter(Boolean).join(' '));
245
+
246
+
247
+ </script>
248
+
249
+ <div class="{unstyled ? '' : 'sdt-container'} {classNames.container ?? ''} {className}">
250
+ <DataTableToolbar
251
+ {store}
252
+ {columns}
253
+ {searchable}
254
+ {searchDebounceMs}
255
+ {buttons}
256
+ {editorMode}
257
+ {classNames}
258
+ onDeleteClick={onDelete}
259
+ />
260
+
261
+ {#if state.error}
262
+ <div class="sdt-error">
263
+ <span>{state.error}</span>
264
+ <button type="button" onclick={() => store.setError(null)}>Dismiss</button>
265
+ </div>
266
+ {/if}
267
+
268
+ <div
269
+ bind:this={wrapperEl}
270
+ class="{unstyled ? '' : 'sdt-table-wrapper'} {classNames.table ?? ''}"
271
+ class:sdt-responsive={!unstyled && responsive}
272
+ class:sdt-virtual-wrapper={virtualScroll}
273
+ style:max-height={virtualScroll ? '500px' : undefined}
274
+ onscroll={virtualScroll ? handleWrapperScroll : undefined}
275
+ >
276
+ <table class="{unstyled ? '' : tableClasses}">
277
+ <DataTableHead {columns} {store} {selectable} {sortable} {expandable} {classNames} />
278
+ <DataTableBody
279
+ {columns}
280
+ {store}
281
+ {selectable}
282
+ {expandable}
283
+ {editorMode}
284
+ {emptyText}
285
+ {loadingText}
286
+ {rowClass}
287
+ {groupBy}
288
+ {classNames}
289
+ {onRowClick}
290
+ {customCell}
291
+ {expandContent}
292
+ onExcelNavigate={editorMode === 'excel' ? handleExcelNavigate : undefined}
293
+ onExcelCommit={editorMode === 'excel' ? handleExcelCommit : undefined}
294
+ onInlineCommit={editorMode === 'inline' ? handleInlineCommit : undefined}
295
+ onBubbleAnchor={editorMode === 'bubble' ? (el) => { bubbleAnchor = el; } : undefined}
296
+ {virtualScroll}
297
+ {virtualRowHeight}
298
+ {scrollTop}
299
+ containerHeight={500}
300
+ />
301
+ <DataTableFooter {columns} {store} {selectable} {expandable} {classNames} />
302
+ </table>
303
+ </div>
304
+
305
+ {#if paginate}
306
+ <DataTablePagination {store} {perPageOptions} {classNames} />
307
+ {/if}
308
+
309
+ {#if editorMode && editorMode !== 'excel' && editorFields}
310
+ <DataTableEditor
311
+ fields={editorFields}
312
+ {store}
313
+ {classNames}
314
+ onsubmit={handleEditorSubmit}
315
+ anchorEl={bubbleAnchor}
316
+ />
317
+ {/if}
318
+ </div>
319
+
320
+ <style>
321
+ .sdt-container {
322
+ background: var(--sdt-bg, #fff);
323
+ border: 1px solid var(--sdt-border, #e5e7eb);
324
+ border-radius: 0.5rem;
325
+ font-family: var(--sdt-font, inherit);
326
+ }
327
+
328
+ .sdt-table-wrapper {
329
+ overflow-x: auto;
330
+ }
331
+ .sdt-table-wrapper.sdt-responsive {
332
+ overflow-x: auto;
333
+ -webkit-overflow-scrolling: touch;
334
+ }
335
+ .sdt-virtual-wrapper {
336
+ overflow-y: auto;
337
+ }
338
+
339
+ .sdt-table {
340
+ width: 100%;
341
+ border-collapse: collapse;
342
+ table-layout: auto;
343
+ }
344
+
345
+ .sdt-table :global(.sdt-compact .sdt-cell),
346
+ .sdt-table :global(.sdt-compact .sdt-th) {
347
+ padding: 0.375rem 0.5rem;
348
+ }
349
+
350
+ .sdt-error {
351
+ display: flex;
352
+ align-items: center;
353
+ justify-content: space-between;
354
+ padding: 0.625rem 1rem;
355
+ background: #fef2f2;
356
+ border-bottom: 1px solid #fecaca;
357
+ color: #dc2626;
358
+ font-size: 0.8125rem;
359
+ }
360
+ .sdt-error button {
361
+ background: none;
362
+ border: none;
363
+ color: #dc2626;
364
+ cursor: pointer;
365
+ text-decoration: underline;
366
+ font-size: 0.8125rem;
367
+ }
368
+
369
+ /* Stripe via CSS variables so DataTableRow can use them */
370
+ :global(.sdt-striped .sdt-row-even) {
371
+ --sdt-row-stripe: rgba(0, 0, 0, 0.02);
372
+ background-color: var(--sdt-row-stripe);
373
+ }
374
+
375
+ :global(.sdt-hover .sdt-row:hover) {
376
+ --sdt-row-hover: #f9fafb;
377
+ background-color: var(--sdt-row-hover) !important;
378
+ }
379
+
380
+ :global(.sdt-bordered .sdt-cell),
381
+ :global(.sdt-bordered .sdt-th),
382
+ :global(.sdt-bordered .sdt-tf) {
383
+ border: 1px solid var(--sdt-border, #e5e7eb);
384
+ }
385
+ </style>
@@ -0,0 +1,180 @@
1
+ <script lang="ts">
2
+ import type { ColumnDef, DataTableStore, SelectionMode, EditorMode, DataTableClassNames } from '../index.js';
3
+ import type { Snippet } from 'svelte';
4
+ import DataTableRow from './DataTableRow.svelte';
5
+ import DataTableLoading from './DataTableLoading.svelte';
6
+ import DataTableEmpty from './DataTableEmpty.svelte';
7
+
8
+ interface Props {
9
+ columns: ColumnDef[];
10
+ store: DataTableStore;
11
+ selectable?: SelectionMode;
12
+ expandable?: boolean;
13
+ editorMode?: EditorMode;
14
+ emptyText?: string;
15
+ loadingText?: string;
16
+ rowClass?: string | ((row: any, index: number) => string);
17
+ onRowClick?: (row: any, event: MouseEvent) => void;
18
+ customCell?: Snippet<[{ row: any; column: ColumnDef; value: any }]>;
19
+ expandContent?: Snippet<[{ row: any }]>;
20
+ onExcelNavigate?: (direction: 'up' | 'down' | 'left' | 'right') => void;
21
+ onExcelCommit?: () => void;
22
+ onInlineCommit?: (row: any, columnKey: string, newValue: any, oldValue: any) => void;
23
+ groupBy?: string;
24
+ onBubbleAnchor?: (el: HTMLElement) => void;
25
+ classNames?: DataTableClassNames;
26
+ virtualScroll?: boolean;
27
+ virtualRowHeight?: number;
28
+ scrollTop?: number;
29
+ containerHeight?: number;
30
+ }
31
+ let {
32
+ columns,
33
+ store,
34
+ selectable = 'none',
35
+ expandable = false,
36
+ editorMode,
37
+ emptyText = 'No data available',
38
+ loadingText = 'Loading...',
39
+ rowClass,
40
+ onRowClick,
41
+ customCell,
42
+ expandContent,
43
+ onExcelNavigate,
44
+ onExcelCommit,
45
+ onInlineCommit,
46
+ groupBy,
47
+ onBubbleAnchor,
48
+ classNames = {},
49
+ virtualScroll = false,
50
+ virtualRowHeight = 48,
51
+ scrollTop = 0,
52
+ containerHeight = 500,
53
+ }: Props = $props();
54
+
55
+ let state = $state(store.getState());
56
+ $effect(() => {
57
+ return store.subscribe(() => {
58
+ state = store.getState();
59
+ });
60
+ });
61
+
62
+ let visibleColCount = $derived(
63
+ state.columnOrder.filter((key) => state.columnVisibility[key] !== false).length
64
+ + (selectable !== 'none' ? 1 : 0)
65
+ + (expandable ? 1 : 0)
66
+ );
67
+
68
+ // Group rows by column value when groupBy is set
69
+ let groupedRows = $derived(() => {
70
+ if (!groupBy) return null;
71
+ const groups: { key: string; rows: any[] }[] = [];
72
+ const map = new Map<string, any[]>();
73
+ for (const row of state.paginatedRows) {
74
+ const key = String((row as any)[groupBy] ?? 'Other');
75
+ if (!map.has(key)) {
76
+ const arr: any[] = [];
77
+ map.set(key, arr);
78
+ groups.push({ key, rows: arr });
79
+ }
80
+ map.get(key)!.push(row);
81
+ }
82
+ return groups;
83
+ });
84
+
85
+ // Virtual scroll: compute visible row range
86
+ let overscan = 5;
87
+ let virtualVisibleStart = $derived(
88
+ virtualScroll ? Math.max(0, Math.floor(scrollTop / virtualRowHeight) - overscan) : 0
89
+ );
90
+ let virtualVisibleEnd = $derived(
91
+ virtualScroll
92
+ ? Math.min(state.paginatedRows.length, Math.ceil((scrollTop + containerHeight) / virtualRowHeight) + overscan)
93
+ : state.paginatedRows.length
94
+ );
95
+ let virtualTopPad = $derived(virtualScroll ? virtualVisibleStart * virtualRowHeight : 0);
96
+ let virtualBottomPad = $derived(
97
+ virtualScroll ? Math.max(0, (state.paginatedRows.length - virtualVisibleEnd) * virtualRowHeight) : 0
98
+ );
99
+ let virtualRows = $derived(
100
+ virtualScroll
101
+ ? state.paginatedRows.slice(virtualVisibleStart, virtualVisibleEnd)
102
+ : state.paginatedRows
103
+ );
104
+ </script>
105
+
106
+ <tbody class="sdt-tbody {classNames.tbody ?? ''}">
107
+ {#if state.loading}
108
+ <DataTableLoading text={loadingText} colSpan={visibleColCount} />
109
+ {:else if state.paginatedRows.length === 0}
110
+ <DataTableEmpty text={emptyText} colSpan={visibleColCount} />
111
+ {:else if groupBy && groupedRows()}
112
+ {#each groupedRows()! as group}
113
+ <tr class="sdt-group-header">
114
+ <td colspan={visibleColCount} class="sdt-group-cell">{group.key}</td>
115
+ </tr>
116
+ {#each group.rows as row, index (store.getRowId(row))}
117
+ <DataTableRow
118
+ {row}
119
+ {index}
120
+ {columns}
121
+ {store}
122
+ {selectable}
123
+ {expandable}
124
+ {editorMode}
125
+ {rowClass}
126
+ {classNames}
127
+ {onRowClick}
128
+ {customCell}
129
+ {expandContent}
130
+ {onExcelNavigate}
131
+ {onExcelCommit}
132
+ {onInlineCommit}
133
+ {onBubbleAnchor}
134
+ />
135
+ {/each}
136
+ {/each}
137
+ {:else}
138
+ {#if virtualScroll && virtualTopPad > 0}
139
+ <tr style="height: {virtualTopPad}px;"><td colspan={visibleColCount}></td></tr>
140
+ {/if}
141
+ {#each virtualRows as row, i (store.getRowId(row))}
142
+ {@const index = virtualScroll ? virtualVisibleStart + i : i}
143
+ <DataTableRow
144
+ {row}
145
+ {index}
146
+ {columns}
147
+ {store}
148
+ {selectable}
149
+ {expandable}
150
+ {editorMode}
151
+ {rowClass}
152
+ {classNames}
153
+ {onRowClick}
154
+ {customCell}
155
+ {expandContent}
156
+ {onExcelNavigate}
157
+ {onExcelCommit}
158
+ {onInlineCommit}
159
+ />
160
+ {/each}
161
+ {#if virtualScroll && virtualBottomPad > 0}
162
+ <tr style="height: {virtualBottomPad}px;"><td colspan={visibleColCount}></td></tr>
163
+ {/if}
164
+ {/if}
165
+ </tbody>
166
+
167
+ <style>
168
+ .sdt-group-header {
169
+ background: var(--sdt-group-bg, #f3f4f6);
170
+ }
171
+ .sdt-group-cell {
172
+ padding: 0.5rem 0.75rem;
173
+ font-weight: 600;
174
+ font-size: 0.8125rem;
175
+ color: var(--sdt-text-muted, #6b7280);
176
+ text-transform: uppercase;
177
+ letter-spacing: 0.03em;
178
+ border-bottom: 1px solid var(--sdt-border, #e5e7eb);
179
+ }
180
+ </style>
@@ -0,0 +1,93 @@
1
+ <script lang="ts">
2
+ import type { EditorFieldDef, DataTableStore, DataTableClassNames } from '../index.js';
3
+ import DataTableEditorForm from './DataTableEditorForm.svelte';
4
+ import { tick } from 'svelte';
5
+
6
+ interface Props {
7
+ fields: EditorFieldDef[];
8
+ store: DataTableStore;
9
+ anchorEl?: HTMLElement | null;
10
+ classNames?: DataTableClassNames;
11
+ onsubmit: (formData: Record<string, any>) => void | Promise<void>;
12
+ }
13
+ let { fields, store, anchorEl = null, classNames = {}, onsubmit }: Props = $props();
14
+
15
+ let state = $state(store.getState());
16
+ $effect(() => {
17
+ return store.subscribe(() => {
18
+ state = store.getState();
19
+ });
20
+ });
21
+
22
+ let isOpen = $derived(state.editorMode === 'bubble');
23
+ let popoverStyle = $state('');
24
+
25
+ // Get only the field for the column being edited
26
+ let activeFields = $derived(() => {
27
+ if (state.editingColumn) {
28
+ const field = fields.find((f) => f.name === state.editingColumn);
29
+ return field ? [field] : fields;
30
+ }
31
+ return fields;
32
+ });
33
+
34
+ // Position the bubble editor relative to the editing row
35
+ $effect(() => {
36
+ if (!isOpen) return;
37
+
38
+ // Use anchorEl if provided, otherwise find the selected row in the DOM
39
+ async function position() {
40
+ await tick();
41
+ let el = anchorEl;
42
+ if (!el) {
43
+ el = document.querySelector('.sdt-row-selected') as HTMLElement | null;
44
+ }
45
+ if (el) {
46
+ const rect = el.getBoundingClientRect();
47
+ popoverStyle = `top: ${rect.bottom + 4}px; left: ${rect.left}px;`;
48
+ }
49
+ }
50
+ position();
51
+ });
52
+
53
+ function close() {
54
+ store.closeEditor();
55
+ }
56
+ </script>
57
+
58
+ {#if isOpen}
59
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
60
+ <div class="sdt-bubble-backdrop" onclick={close}></div>
61
+ <div class="sdt-bubble-editor" style={popoverStyle}>
62
+ <DataTableEditorForm
63
+ fields={activeFields()}
64
+ {store}
65
+ {onsubmit}
66
+ oncancel={close}
67
+ />
68
+ </div>
69
+ {/if}
70
+
71
+ <style>
72
+ .sdt-bubble-backdrop {
73
+ position: fixed;
74
+ inset: 0;
75
+ z-index: 40;
76
+ }
77
+ .sdt-bubble-editor {
78
+ position: fixed;
79
+ z-index: 41;
80
+ background: var(--sdt-bg, #fff);
81
+ border: 1px solid var(--sdt-border, #e5e7eb);
82
+ border-radius: 0.375rem;
83
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
84
+ padding: 0.75rem;
85
+ min-width: 220px;
86
+ max-width: 320px;
87
+ animation: sdt-pop-in 0.15s ease;
88
+ }
89
+ @keyframes sdt-pop-in {
90
+ from { opacity: 0; transform: translateY(-4px); }
91
+ to { opacity: 1; transform: translateY(0); }
92
+ }
93
+ </style>