@classic-homes/theme-svelte 1.25.1 → 1.26.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 (69) hide show
  1. package/dist/lib/components/AlertDialog.svelte +14 -6
  2. package/dist/lib/components/AlertDialog.svelte.d.ts +8 -2
  3. package/dist/lib/components/Badge.svelte +6 -2
  4. package/dist/lib/components/CodeField.svelte +24 -8
  5. package/dist/lib/components/CodeField.svelte.d.ts +8 -0
  6. package/dist/lib/components/Combobox.svelte +25 -111
  7. package/dist/lib/components/DataPanel.svelte +31 -24
  8. package/dist/lib/components/DataPanelHeader.svelte +66 -26
  9. package/dist/lib/components/DataTable.svelte +303 -138
  10. package/dist/lib/components/DataTable.svelte.d.ts +37 -13
  11. package/dist/lib/components/DateTimePicker.svelte +159 -34
  12. package/dist/lib/components/Dialog.svelte +14 -6
  13. package/dist/lib/components/Dialog.svelte.d.ts +8 -2
  14. package/dist/lib/components/FileUpload.svelte +23 -6
  15. package/dist/lib/components/FileUpload.svelte.d.ts +7 -2
  16. package/dist/lib/components/MultiSelect.svelte +28 -155
  17. package/dist/lib/components/NumberInput.svelte +28 -110
  18. package/dist/lib/components/OTPInput.svelte +73 -85
  19. package/dist/lib/components/Select.svelte +24 -29
  20. package/dist/lib/components/Signature.svelte +80 -48
  21. package/dist/lib/components/Signature.svelte.d.ts +6 -1
  22. package/dist/lib/components/SkipLink.svelte +7 -2
  23. package/dist/lib/components/SkipLink.svelte.d.ts +2 -0
  24. package/dist/lib/components/Slider.svelte +4 -4
  25. package/dist/lib/components/Toast.svelte +11 -1
  26. package/dist/lib/components/ToastContainer.svelte +38 -24
  27. package/dist/lib/components/Tooltip.svelte +19 -6
  28. package/dist/lib/components/Tooltip.svelte.d.ts +8 -2
  29. package/dist/lib/components/data-table/DataTableColumnFilter.svelte +3 -1
  30. package/dist/lib/components/data-table/DataTableEditableCell.svelte +25 -3
  31. package/dist/lib/components/data-table/DataTableFacetedFilter.svelte +50 -40
  32. package/dist/lib/components/data-table/DataTableFacetedFilter.svelte.d.ts +0 -10
  33. package/dist/lib/components/data-table/DataTableVirtual.svelte +109 -57
  34. package/dist/lib/components/data-table/DataTableVirtual.svelte.d.ts +2 -0
  35. package/dist/lib/components/data-table/createDataTable.svelte.d.ts +26 -4
  36. package/dist/lib/components/data-table/createDataTable.svelte.js +97 -40
  37. package/dist/lib/components/data-table/index.d.ts +1 -1
  38. package/dist/lib/components/data-table/types.d.ts +14 -4
  39. package/dist/lib/components/data-table/types.js +5 -1
  40. package/dist/lib/components/internal/TriggerChild.svelte +60 -0
  41. package/dist/lib/components/internal/TriggerChild.svelte.d.ts +32 -0
  42. package/dist/lib/components/layout/Sidebar.svelte +33 -3
  43. package/dist/lib/components/layout/Sidebar.svelte.d.ts +1 -1
  44. package/dist/lib/components/layout/sidebar/SidebarFlyout.svelte +6 -1
  45. package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte +6 -1
  46. package/dist/lib/components/layout/sidebar/SidebarSection.svelte +6 -1
  47. package/dist/lib/components/media/Audio.svelte +16 -0
  48. package/dist/lib/components/media/Lightbox.svelte +29 -3
  49. package/dist/lib/components/media/MediaCarousel.svelte +19 -0
  50. package/dist/lib/components/media/MediaEmbed.svelte +32 -1
  51. package/dist/lib/components/media/Video.svelte +16 -0
  52. package/dist/lib/components/media/internal/MediaControls.svelte +9 -1
  53. package/dist/lib/components/notifications/NotificationBadge.svelte +1 -1
  54. package/dist/lib/components/notifications/NotificationItem.svelte +19 -28
  55. package/dist/lib/composables/useEscapeKey.svelte.d.ts +4 -0
  56. package/dist/lib/composables/useEscapeKey.svelte.js +15 -0
  57. package/dist/lib/composables/useHlsSource.svelte.js +31 -4
  58. package/dist/lib/composables/useMediaPlayer.svelte.js +38 -9
  59. package/dist/lib/composables/useMediaZoom.svelte.js +32 -0
  60. package/dist/lib/composables/usePersistedForm.svelte.d.ts +6 -2
  61. package/dist/lib/composables/usePersistedForm.svelte.js +7 -7
  62. package/dist/lib/composables/useReducedMotion.svelte.js +6 -1
  63. package/dist/lib/composables/useRovingTabindex.svelte.js +12 -0
  64. package/dist/lib/index.d.ts +1 -1
  65. package/dist/lib/stores/toast.svelte.d.ts +13 -0
  66. package/dist/lib/stores/toast.svelte.js +36 -5
  67. package/dist/lib/utils/media.d.ts +7 -0
  68. package/dist/lib/utils/media.js +18 -3
  69. package/package.json +1 -1
@@ -30,7 +30,7 @@
30
30
  * />
31
31
  * ```
32
32
  */
33
- import type { Snippet } from 'svelte';
33
+ import { untrack, type Snippet } from 'svelte';
34
34
  import type { DataTableColumn } from '../types/components.js';
35
35
  import { cn } from '../utils.js';
36
36
  import { validateNonEmptyArray } from '../validation.js';
@@ -41,7 +41,12 @@
41
41
  getColumnWidthStyle,
42
42
  getAriaSortValue,
43
43
  } from './data-table/utils.js';
44
- import type { PaginationState } from './data-table/types.js';
44
+ import type { ColumnFiltersState, PaginationState } from './data-table/types.js';
45
+ import DataTableColumnFilter from './data-table/DataTableColumnFilter.svelte';
46
+ import DataTableColumnVisibility from './data-table/DataTableColumnVisibility.svelte';
47
+ import DataTableEditableCell from './data-table/DataTableEditableCell.svelte';
48
+ import DataTableFacetedFilter from './data-table/DataTableFacetedFilter.svelte';
49
+ import DataTableVirtual from './data-table/DataTableVirtual.svelte';
45
50
  import Skeleton from './Skeleton.svelte';
46
51
  import Checkbox from './Checkbox.svelte';
47
52
 
@@ -69,8 +74,11 @@
69
74
  sortColumn?: string;
70
75
  /** Current sort direction */
71
76
  sortDirection?: 'asc' | 'desc';
72
- /** Callback when sort changes - when provided, enables server-side sorting */
73
- onSort?: (column: string, direction: 'asc' | 'desc') => void;
77
+ /**
78
+ * Callback when sort changes - when provided, enables server-side sorting.
79
+ * Called with `column: null` when the sort is cleared.
80
+ */
81
+ onSort?: (column: string | null, direction: 'asc' | 'desc') => void;
74
82
 
75
83
  // === Interaction Props ===
76
84
  /** Callback when row is clicked */
@@ -79,7 +87,10 @@
79
87
  // === Selection Props ===
80
88
  /** Enable row selection - can be boolean or function for per-row control */
81
89
  enableRowSelection?: boolean | ((row: T) => boolean);
82
- /** Enable multi-row selection (default: true when selection enabled) */
90
+ /**
91
+ * Allow more than one selected row (default: true). When false, selecting
92
+ * a row replaces the previous selection and there is no select-all box.
93
+ */
83
94
  enableMultiRowSelection?: boolean;
84
95
  /** Controlled row selection state (Set of row IDs) */
85
96
  rowSelection?: Set<string>;
@@ -89,7 +100,7 @@
89
100
  getRowId?: (row: T, index: number) => string;
90
101
 
91
102
  // === Visibility Props ===
92
- /** Enable column visibility toggling */
103
+ /** Show a "Columns" menu above the table for hiding and showing columns */
93
104
  enableColumnVisibility?: boolean;
94
105
  /** Controlled visibility state (Set of hidden column IDs) */
95
106
  columnVisibility?: Set<string>;
@@ -97,18 +108,27 @@
97
108
  onColumnVisibilityChange?: (hiddenColumns: Set<string>) => void;
98
109
 
99
110
  // === Filtering Props ===
100
- /** Enable filtering features */
111
+ /**
112
+ * Show a filter row under the header: a text filter per column, or a
113
+ * faceted (multi-select) filter for columns that define `filterOptions`
114
+ */
101
115
  enableFiltering?: boolean;
102
- /** Enable global filter input */
116
+ /**
117
+ * Apply `globalFilter` to the rows. No search input is rendered; bind
118
+ * `globalFilter` to your own input.
119
+ */
103
120
  enableGlobalFilter?: boolean;
104
121
  /** Controlled global filter value */
105
122
  globalFilter?: string;
106
123
  /** Callback when global filter changes */
107
124
  onGlobalFilterChange?: (filter: string) => void;
108
- /** Controlled column filters */
109
- columnFilters?: Map<string, string>;
125
+ /**
126
+ * Controlled column filters. A string value is a substring filter; a
127
+ * string[] value (what the faceted filter sets) matches those values exactly.
128
+ */
129
+ columnFilters?: ColumnFiltersState;
110
130
  /** Callback when column filters change */
111
- onColumnFiltersChange?: (filters: Map<string, string>) => void;
131
+ onColumnFiltersChange?: (filters: ColumnFiltersState) => void;
112
132
  /** Use server-side filtering (manual mode) */
113
133
  manualFiltering?: boolean;
114
134
 
@@ -127,15 +147,24 @@
127
147
  pageCount?: number;
128
148
 
129
149
  // === Virtualization Props ===
130
- /** Enable row virtualization for large datasets (1000+ rows recommended) */
150
+ /**
151
+ * Render rows with DataTableVirtual (only the visible rows are in the DOM)
152
+ * for large datasets (1000+ rows recommended). Pagination, `row` snippets,
153
+ * `hideBelow`, and inline editing do not apply in this mode.
154
+ */
131
155
  enableVirtualization?: boolean;
156
+ /** Height of the scrolling body when virtualized (CSS value, default: 500px) */
157
+ virtualHeight?: string;
132
158
  /** Height of each row in pixels for virtualization (default: 48) */
133
159
  virtualRowHeight?: number;
134
160
  /** Number of rows to render outside the visible viewport (default: 10) */
135
161
  virtualOverscan?: number;
136
162
 
137
163
  // === Editing Props ===
138
- /** Enable inline cell editing */
164
+ /**
165
+ * Enable inline cell editing (DataTableEditableCell) for columns that set
166
+ * `enableEditing: true`. Requires `onCellEdit`.
167
+ */
139
168
  enableEditing?: boolean;
140
169
  /** Callback when a cell is edited */
141
170
  onCellEdit?: (rowId: string, columnId: string, value: unknown, row: T) => void;
@@ -177,7 +206,7 @@
177
206
  enableGlobalFilter = false,
178
207
  globalFilter = $bindable(''),
179
208
  onGlobalFilterChange,
180
- columnFilters = $bindable(new Map<string, string>()),
209
+ columnFilters = $bindable(new Map() as ColumnFiltersState),
181
210
  onColumnFiltersChange,
182
211
  manualFiltering = false,
183
212
 
@@ -191,6 +220,7 @@
191
220
 
192
221
  // Virtualization props
193
222
  enableVirtualization = false,
223
+ virtualHeight = '500px',
194
224
  virtualRowHeight = 48,
195
225
  virtualOverscan = 10,
196
226
 
@@ -245,18 +275,22 @@
245
275
  initialPageSize: pageSize,
246
276
  initialPageIndex: pageIndex,
247
277
  initialGlobalFilter: globalFilter,
278
+ initialHiddenColumns: [...columnVisibility],
279
+ initialSelectedRows: [...rowSelection],
280
+ initialColumnFilters: columnFilters,
281
+ enableMultiRowSelection,
248
282
  manualSorting: !!onSort,
249
283
  manualFiltering,
250
284
  manualPagination,
251
285
  // Without the pager every row must render, not just the first page.
252
- enablePagination,
286
+ // The virtual body windows the full row list itself.
287
+ enablePagination: enablePagination && !enableVirtualization,
253
288
  pageCount,
254
289
  onSortChange: (col, dir) => {
255
290
  sortColumn = col ?? undefined;
256
291
  sortDirection = dir;
257
- if (col && onSort) {
258
- onSort(col, dir);
259
- }
292
+ // A cleared sort is reported too, or a server would stay sorted
293
+ onSort?.(col, dir);
260
294
  },
261
295
  onGlobalFilterChange: (filter) => {
262
296
  globalFilter = filter;
@@ -281,10 +315,87 @@
281
315
  },
282
316
  }));
283
317
 
284
- // Sync external state changes to table
318
+ // Mirror controlled props into the table. Each effect tracks only its prop
319
+ // (the comparison runs untracked), so a table-side change such as a filter
320
+ // resetting the page is never undone by a stale prop value.
321
+ function sameSet(a: Set<string>, b: Set<string>) {
322
+ return a.size === b.size && [...a].every((v) => b.has(v));
323
+ }
324
+
325
+ function sameFilters(a: ColumnFiltersState, b: ColumnFiltersState) {
326
+ if (a.size !== b.size) return false;
327
+ for (const [key, value] of a) {
328
+ const other = b.get(key);
329
+ if (Array.isArray(value) && Array.isArray(other)) {
330
+ if (value.length !== other.length || value.some((v, i) => v !== other[i])) return false;
331
+ } else if (value !== other) {
332
+ return false;
333
+ }
334
+ }
335
+ return true;
336
+ }
337
+
338
+ $effect.pre(() => {
339
+ const value = globalFilter;
340
+ untrack(() => {
341
+ if (value !== table.globalFilter) table.setGlobalFilter(value);
342
+ });
343
+ });
344
+
345
+ $effect.pre(() => {
346
+ const column = sortColumn ?? null;
347
+ const direction = sortDirection;
348
+ untrack(() => {
349
+ if (column !== table.sortColumn || direction !== table.sortDirection) {
350
+ table.syncState({ sortColumn: column, sortDirection: direction });
351
+ }
352
+ });
353
+ });
354
+
355
+ $effect.pre(() => {
356
+ const index = pageIndex;
357
+ const size = pageSize;
358
+ untrack(() => {
359
+ if (index !== table.pageIndex || size !== table.pageSize) {
360
+ table.syncState({ pageIndex: index, pageSize: size });
361
+ }
362
+ });
363
+ });
364
+
365
+ $effect.pre(() => {
366
+ const selection = rowSelection;
367
+ untrack(() => {
368
+ if (!sameSet(selection, table.selectedRows)) table.syncState({ selectedRows: selection });
369
+ });
370
+ });
371
+
372
+ $effect.pre(() => {
373
+ const hidden = columnVisibility;
374
+ untrack(() => {
375
+ if (!sameSet(hidden, table.hiddenColumns)) table.syncState({ hiddenColumns: hidden });
376
+ });
377
+ });
378
+
379
+ $effect.pre(() => {
380
+ const filters = columnFilters;
381
+ untrack(() => {
382
+ if (!sameFilters(filters, table.columnFilters)) table.syncState({ columnFilters: filters });
383
+ });
384
+ });
385
+
386
+ const showSelectAll = $derived(!!enableRowSelection && enableMultiRowSelection);
387
+ const canEdit = $derived(enableEditing && !!onCellEdit);
388
+ const columnCount = $derived(table.columns.length + (enableRowSelection ? 1 : 0));
389
+
390
+ function handleCellEdit(rowId: string, columnId: string, value: unknown, row: T) {
391
+ onCellEdit?.(rowId, columnId, value, row);
392
+ }
393
+
394
+ // Editing needs somewhere to send the value; say so rather than silently
395
+ // rendering read-only cells.
285
396
  $effect(() => {
286
- if (globalFilter !== table.globalFilter) {
287
- table.setGlobalFilter(globalFilter);
397
+ if (enableEditing && !onCellEdit) {
398
+ console.warn('[DataTable] `enableEditing` has no effect without an `onCellEdit` callback.');
288
399
  }
289
400
  });
290
401
 
@@ -352,141 +463,195 @@
352
463
  }
353
464
  </script>
354
465
 
355
- <div class={cn('overflow-x-auto rounded-lg border', className)}>
356
- <table class="w-full text-sm">
357
- {#if caption}
358
- <caption class="sr-only">{caption}</caption>
359
- {/if}
360
-
361
- <thead class="border-b bg-muted/50">
362
- <tr>
363
- {#if enableRowSelection}
364
- <th scope="col" class="w-10 px-4 py-3">
365
- <Checkbox
366
- checked={table.isAllSelected}
367
- indeterminate={table.isSomeSelected}
368
- onCheckedChange={handleSelectAll}
369
- aria-label="Select all rows"
370
- />
371
- </th>
372
- {/if}
466
+ {#if enableColumnVisibility}
467
+ <div class="flex justify-end pb-2">
468
+ <DataTableColumnVisibility {table} />
469
+ </div>
470
+ {/if}
373
471
 
374
- {#each table.columns as column (column.id)}
375
- {@const isSorted = table.sortColumn === column.id}
376
- <th
377
- scope="col"
378
- class={cn(
379
- 'px-4 py-3 font-medium text-muted-foreground',
380
- getAlignClass(column.align),
381
- column.hideBelow && hideBelowClasses[column.hideBelow],
382
- column.sortable && 'cursor-pointer select-none hover:bg-muted-hover'
383
- )}
384
- style={getColumnWidthStyle(column)}
385
- aria-sort={getAriaSortValue(isSorted, table.sortDirection)}
386
- >
387
- {#if column.sortable}
388
- <button
389
- type="button"
390
- class="inline-flex w-full items-center justify-between gap-2"
391
- onclick={() => handleHeaderClick(column.id, true)}
392
- >
393
- <span>{column.header}</span>
394
- <svg
395
- class={cn(
396
- 'h-4 w-4 transition-transform',
397
- !isSorted && 'opacity-0',
398
- isSorted && table.sortDirection === 'desc' && 'rotate-180'
399
- )}
400
- fill="none"
401
- viewBox="0 0 24 24"
402
- stroke="currentColor"
403
- stroke-width="2"
472
+ {#if enableVirtualization && !loading}
473
+ <DataTableVirtual
474
+ {table}
475
+ rowHeight={virtualRowHeight}
476
+ overscan={virtualOverscan}
477
+ height={virtualHeight}
478
+ {onRowClick}
479
+ enableRowSelection={!!enableRowSelection}
480
+ enableSelectAll={enableMultiRowSelection}
481
+ class={cn('text-sm', className)}
482
+ />
483
+ {:else}
484
+ <div class={cn('overflow-x-auto rounded-lg border', className)}>
485
+ <table class="w-full text-sm">
486
+ {#if caption}
487
+ <caption class="sr-only">{caption}</caption>
488
+ {/if}
489
+
490
+ <thead class="border-b bg-muted/50">
491
+ <tr>
492
+ {#if enableRowSelection}
493
+ <th scope="col" class="w-10 px-4 py-3">
494
+ {#if showSelectAll}
495
+ <Checkbox
496
+ checked={table.isAllSelected}
497
+ indeterminate={table.isSomeSelected}
498
+ onCheckedChange={handleSelectAll}
499
+ aria-label="Select all rows"
500
+ />
501
+ {:else}
502
+ <span class="sr-only">Select</span>
503
+ {/if}
504
+ </th>
505
+ {/if}
506
+
507
+ {#each table.columns as column (column.id)}
508
+ {@const isSorted = table.sortColumn === column.id}
509
+ <th
510
+ scope="col"
511
+ class={cn(
512
+ 'px-4 py-3 font-medium text-muted-foreground',
513
+ getAlignClass(column.align),
514
+ column.hideBelow && hideBelowClasses[column.hideBelow],
515
+ column.sortable && 'cursor-pointer select-none hover:bg-muted-hover'
516
+ )}
517
+ style={getColumnWidthStyle(column)}
518
+ aria-sort={getAriaSortValue(isSorted, table.sortDirection)}
519
+ >
520
+ {#if column.sortable}
521
+ <button
522
+ type="button"
523
+ class="inline-flex w-full items-center justify-between gap-2"
524
+ onclick={() => handleHeaderClick(column.id, true)}
404
525
  >
405
- <path stroke-linecap="round" stroke-linejoin="round" d="M5 15l7-7 7 7" />
406
- </svg>
407
- </button>
408
- {:else}
409
- {column.header}
410
- {/if}
411
- </th>
412
- {/each}
413
- </tr>
414
- </thead>
415
-
416
- <tbody class="divide-y">
417
- {#if loading}
418
- {#each Array(loadingRows) as _, i (i)}
526
+ <span>{column.header}</span>
527
+ <svg
528
+ class={cn(
529
+ 'h-4 w-4 transition-transform',
530
+ !isSorted && 'opacity-0',
531
+ isSorted && table.sortDirection === 'desc' && 'rotate-180'
532
+ )}
533
+ fill="none"
534
+ viewBox="0 0 24 24"
535
+ stroke="currentColor"
536
+ stroke-width="2"
537
+ >
538
+ <path stroke-linecap="round" stroke-linejoin="round" d="M5 15l7-7 7 7" />
539
+ </svg>
540
+ </button>
541
+ {:else}
542
+ {column.header}
543
+ {/if}
544
+ </th>
545
+ {/each}
546
+ </tr>
547
+ {#if enableFiltering}
419
548
  <tr>
420
549
  {#if enableRowSelection}
421
- <td class="px-4 py-3">
422
- <Skeleton class="h-5 w-5" />
423
- </td>
550
+ <td class="px-4 pb-3"></td>
424
551
  {/if}
425
552
  {#each table.columns as column (column.id)}
426
- <td class={cn('px-4 py-3', column.hideBelow && hideBelowClasses[column.hideBelow])}>
427
- <Skeleton class="h-4 w-3/4" />
553
+ <td
554
+ class={cn(
555
+ 'px-4 pb-3 font-normal',
556
+ column.hideBelow && hideBelowClasses[column.hideBelow]
557
+ )}
558
+ >
559
+ {#if column.filterOptions?.length}
560
+ <DataTableFacetedFilter
561
+ {table}
562
+ columnId={column.id}
563
+ title={column.header}
564
+ options={column.filterOptions}
565
+ />
566
+ {:else}
567
+ <DataTableColumnFilter
568
+ {table}
569
+ columnId={column.id}
570
+ placeholder={column.filterPlaceholder}
571
+ />
572
+ {/if}
428
573
  </td>
429
574
  {/each}
430
575
  </tr>
431
- {/each}
432
- {:else if table.rows.length === 0}
433
- <tr>
434
- <td
435
- colspan={enableRowSelection ? table.columns.length + 1 : table.columns.length}
436
- class="px-4 py-8 text-center text-muted-foreground"
437
- >
438
- {emptyMessage}
439
- </td>
440
- </tr>
441
- {:else}
442
- {#each table.rows as row, index (row.id)}
443
- {#if rowSnippet}
444
- {@render rowSnippet(row.original, index)}
445
- {:else}
446
- <tr
447
- class={cn(
448
- 'bg-card',
449
- onRowClick && 'cursor-pointer hover:bg-muted-hover',
450
- row.isSelected && 'bg-primary/5'
451
- )}
452
- tabindex={onRowClick ? 0 : undefined}
453
- onclick={() => handleRowClick(row.original)}
454
- onkeydown={onRowClick ? (e) => handleRowKeydown(e, row.original) : undefined}
455
- >
576
+ {/if}
577
+ </thead>
578
+
579
+ <tbody class="divide-y">
580
+ {#if loading}
581
+ {#each Array(loadingRows) as _, i (i)}
582
+ <tr>
456
583
  {#if enableRowSelection}
457
584
  <td class="px-4 py-3">
458
- {#if canSelectRow(row.original)}
459
- <Checkbox
460
- checked={row.isSelected}
461
- onCheckedChange={() => table.toggleRowSelection(row.id)}
462
- aria-label={`Select row ${index + 1}`}
463
- onclick={(e: MouseEvent) => e.stopPropagation()}
464
- />
465
- {/if}
585
+ <Skeleton class="h-5 w-5" />
466
586
  </td>
467
587
  {/if}
468
-
469
588
  {#each table.columns as column (column.id)}
470
- {@const value = getColumnValue(row.original, column)}
471
- <td
472
- class={cn(
473
- 'px-4 py-3',
474
- getAlignClass(column.align),
475
- column.hideBelow && hideBelowClasses[column.hideBelow]
476
- )}
477
- >
478
- {table.getFormattedCellValue(row.original, column)}
589
+ <td class={cn('px-4 py-3', column.hideBelow && hideBelowClasses[column.hideBelow])}>
590
+ <Skeleton class="h-4 w-3/4" />
479
591
  </td>
480
592
  {/each}
481
593
  </tr>
482
- {/if}
483
- {/each}
484
- {/if}
485
- </tbody>
486
- </table>
487
- </div>
594
+ {/each}
595
+ {:else if table.rows.length === 0}
596
+ <tr>
597
+ <td colspan={columnCount} class="px-4 py-8 text-center text-muted-foreground">
598
+ {emptyMessage}
599
+ </td>
600
+ </tr>
601
+ {:else}
602
+ {#each table.rows as row, index (row.id)}
603
+ {#if rowSnippet}
604
+ {@render rowSnippet(row.original, index)}
605
+ {:else}
606
+ <tr
607
+ class={cn(
608
+ 'bg-card',
609
+ onRowClick && 'cursor-pointer hover:bg-muted-hover',
610
+ row.isSelected && 'bg-primary/5'
611
+ )}
612
+ tabindex={onRowClick ? 0 : undefined}
613
+ onclick={() => handleRowClick(row.original)}
614
+ onkeydown={onRowClick ? (e) => handleRowKeydown(e, row.original) : undefined}
615
+ >
616
+ {#if enableRowSelection}
617
+ <td class="px-4 py-3">
618
+ {#if canSelectRow(row.original)}
619
+ <Checkbox
620
+ checked={row.isSelected}
621
+ onCheckedChange={() => table.toggleRowSelection(row.id)}
622
+ aria-label={`Select row ${index + 1}`}
623
+ onclick={(e: MouseEvent) => e.stopPropagation()}
624
+ />
625
+ {/if}
626
+ </td>
627
+ {/if}
628
+
629
+ {#each table.columns as column (column.id)}
630
+ {@const value = getColumnValue(row.original, column)}
631
+ <td
632
+ class={cn(
633
+ 'px-4 py-3',
634
+ getAlignClass(column.align),
635
+ column.hideBelow && hideBelowClasses[column.hideBelow]
636
+ )}
637
+ >
638
+ {#if canEdit && column.enableEditing}
639
+ <DataTableEditableCell {row} {column} onSave={handleCellEdit} />
640
+ {:else}
641
+ {table.getFormattedCellValue(row.original, column)}
642
+ {/if}
643
+ </td>
644
+ {/each}
645
+ </tr>
646
+ {/if}
647
+ {/each}
648
+ {/if}
649
+ </tbody>
650
+ </table>
651
+ </div>
652
+ {/if}
488
653
 
489
- {#if enablePagination && !loading && table.rows.length > 0}
654
+ {#if enablePagination && !enableVirtualization && !loading && table.rows.length > 0}
490
655
  <!-- Pagination controls -->
491
656
  <div class="flex flex-wrap items-center justify-between gap-2 border-t px-4 py-3">
492
657
  <div class="text-sm text-muted-foreground">
@@ -29,9 +29,9 @@
29
29
  * />
30
30
  * ```
31
31
  */
32
- import type { Snippet } from 'svelte';
32
+ import { type Snippet } from 'svelte';
33
33
  import type { DataTableColumn } from '../types/components.js';
34
- import type { PaginationState } from './data-table/types.js';
34
+ import type { ColumnFiltersState, PaginationState } from './data-table/types.js';
35
35
  declare function $$render<T extends object>(): {
36
36
  props: {
37
37
  /** Array of data rows */
@@ -54,13 +54,19 @@ declare function $$render<T extends object>(): {
54
54
  sortColumn?: string;
55
55
  /** Current sort direction */
56
56
  sortDirection?: "asc" | "desc";
57
- /** Callback when sort changes - when provided, enables server-side sorting */
58
- onSort?: (column: string, direction: "asc" | "desc") => void;
57
+ /**
58
+ * Callback when sort changes - when provided, enables server-side sorting.
59
+ * Called with `column: null` when the sort is cleared.
60
+ */
61
+ onSort?: (column: string | null, direction: "asc" | "desc") => void;
59
62
  /** Callback when row is clicked */
60
63
  onRowClick?: (row: T) => void;
61
64
  /** Enable row selection - can be boolean or function for per-row control */
62
65
  enableRowSelection?: boolean | ((row: T) => boolean);
63
- /** Enable multi-row selection (default: true when selection enabled) */
66
+ /**
67
+ * Allow more than one selected row (default: true). When false, selecting
68
+ * a row replaces the previous selection and there is no select-all box.
69
+ */
64
70
  enableMultiRowSelection?: boolean;
65
71
  /** Controlled row selection state (Set of row IDs) */
66
72
  rowSelection?: Set<string>;
@@ -68,24 +74,33 @@ declare function $$render<T extends object>(): {
68
74
  onRowSelectionChange?: (selection: Set<string>) => void;
69
75
  /** Function to get unique row ID (defaults to the row's index in `data`) */
70
76
  getRowId?: (row: T, index: number) => string;
71
- /** Enable column visibility toggling */
77
+ /** Show a "Columns" menu above the table for hiding and showing columns */
72
78
  enableColumnVisibility?: boolean;
73
79
  /** Controlled visibility state (Set of hidden column IDs) */
74
80
  columnVisibility?: Set<string>;
75
81
  /** Callback when visibility changes */
76
82
  onColumnVisibilityChange?: (hiddenColumns: Set<string>) => void;
77
- /** Enable filtering features */
83
+ /**
84
+ * Show a filter row under the header: a text filter per column, or a
85
+ * faceted (multi-select) filter for columns that define `filterOptions`
86
+ */
78
87
  enableFiltering?: boolean;
79
- /** Enable global filter input */
88
+ /**
89
+ * Apply `globalFilter` to the rows. No search input is rendered; bind
90
+ * `globalFilter` to your own input.
91
+ */
80
92
  enableGlobalFilter?: boolean;
81
93
  /** Controlled global filter value */
82
94
  globalFilter?: string;
83
95
  /** Callback when global filter changes */
84
96
  onGlobalFilterChange?: (filter: string) => void;
85
- /** Controlled column filters */
86
- columnFilters?: Map<string, string>;
97
+ /**
98
+ * Controlled column filters. A string value is a substring filter; a
99
+ * string[] value (what the faceted filter sets) matches those values exactly.
100
+ */
101
+ columnFilters?: ColumnFiltersState;
87
102
  /** Callback when column filters change */
88
- onColumnFiltersChange?: (filters: Map<string, string>) => void;
103
+ onColumnFiltersChange?: (filters: ColumnFiltersState) => void;
89
104
  /** Use server-side filtering (manual mode) */
90
105
  manualFiltering?: boolean;
91
106
  /** Enable built-in pagination */
@@ -100,13 +115,22 @@ declare function $$render<T extends object>(): {
100
115
  manualPagination?: boolean;
101
116
  /** Total page count (required for server-side pagination) */
102
117
  pageCount?: number;
103
- /** Enable row virtualization for large datasets (1000+ rows recommended) */
118
+ /**
119
+ * Render rows with DataTableVirtual (only the visible rows are in the DOM)
120
+ * for large datasets (1000+ rows recommended). Pagination, `row` snippets,
121
+ * `hideBelow`, and inline editing do not apply in this mode.
122
+ */
104
123
  enableVirtualization?: boolean;
124
+ /** Height of the scrolling body when virtualized (CSS value, default: 500px) */
125
+ virtualHeight?: string;
105
126
  /** Height of each row in pixels for virtualization (default: 48) */
106
127
  virtualRowHeight?: number;
107
128
  /** Number of rows to render outside the visible viewport (default: 10) */
108
129
  virtualOverscan?: number;
109
- /** Enable inline cell editing */
130
+ /**
131
+ * Enable inline cell editing (DataTableEditableCell) for columns that set
132
+ * `enableEditing: true`. Requires `onCellEdit`.
133
+ */
110
134
  enableEditing?: boolean;
111
135
  /** Callback when a cell is edited */
112
136
  onCellEdit?: (rowId: string, columnId: string, value: unknown, row: T) => void;