@x33025/sveltely 0.1.23 → 0.1.25

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 (107) hide show
  1. package/dist/actions/LoaderOverlay.svelte +10 -3
  2. package/dist/components/Library/Accordion/Accordion.demo.svelte +21 -0
  3. package/dist/components/Library/Accordion/Accordion.demo.svelte.d.ts +9 -0
  4. package/dist/components/Library/Accordion/Accordion.svelte +78 -0
  5. package/dist/components/Library/Accordion/Accordion.svelte.d.ts +14 -0
  6. package/dist/components/Library/Accordion/Content.svelte +57 -0
  7. package/dist/components/Library/Accordion/Content.svelte.d.ts +8 -0
  8. package/dist/components/Library/Accordion/Header.svelte +98 -0
  9. package/dist/components/Library/Accordion/Header.svelte.d.ts +10 -0
  10. package/dist/components/Library/Accordion/context.d.ts +9 -0
  11. package/dist/components/Library/Accordion/context.js +6 -0
  12. package/dist/components/Library/Accordion/index.d.ts +9 -0
  13. package/dist/components/Library/Accordion/index.js +7 -0
  14. package/dist/components/Library/AnimatedNumber/AnimatedNumber.demo.svelte +3 -2
  15. package/dist/components/Library/ArticleEditor/ArticleEditor.svelte +1 -2
  16. package/dist/components/Library/ArticleEditor/Blocks/Table.svelte +133 -172
  17. package/dist/components/Library/Checkbox/Checkbox.demo.svelte +5 -4
  18. package/dist/components/Library/Checkbox/Checkbox.svelte +6 -5
  19. package/dist/components/Library/Checkbox/Checkbox.svelte.d.ts +2 -1
  20. package/dist/components/Library/ChipInput/ChipInput.demo.svelte +3 -2
  21. package/dist/components/Library/Dropdown/Dropdown.demo.svelte +20 -15
  22. package/dist/components/Library/Floating/Floating.svelte +5 -6
  23. package/dist/components/Library/Grid/Grid.demo.svelte +58 -0
  24. package/dist/components/Library/Grid/Grid.demo.svelte.d.ts +25 -0
  25. package/dist/components/Library/Grid/Grid.svelte +128 -25
  26. package/dist/components/Library/Grid/Grid.svelte.d.ts +38 -9
  27. package/dist/components/Library/Grid/GridItem.svelte +18 -14
  28. package/dist/components/Library/Grid/GridItem.svelte.d.ts +2 -1
  29. package/dist/components/Library/HStack/HStack.svelte +4 -4
  30. package/dist/components/Library/HStack/HStack.svelte.d.ts +2 -1
  31. package/dist/components/Library/Image/Image.demo.svelte +3 -1
  32. package/dist/components/Library/Image/Image.demo.svelte.d.ts +2 -0
  33. package/dist/components/Library/ImageMask/ImageMask.demo.svelte +8 -6
  34. package/dist/components/Library/Label/Label.demo.svelte +5 -5
  35. package/dist/components/Library/Label/Label.svelte +10 -26
  36. package/dist/components/Library/NavigationStack/Link.svelte +1 -4
  37. package/dist/components/Library/Notifications/Notifications.demo.svelte +63 -0
  38. package/dist/components/Library/Notifications/Notifications.demo.svelte.d.ts +9 -0
  39. package/dist/components/Library/Notifications/Notifications.svelte +155 -0
  40. package/dist/components/Library/Notifications/Notifications.svelte.d.ts +35 -0
  41. package/dist/components/Library/Notifications/index.d.ts +2 -0
  42. package/dist/components/Library/Notifications/index.js +1 -0
  43. package/dist/components/Library/Notifications/types.d.ts +8 -0
  44. package/dist/components/Library/Notifications/types.js +1 -0
  45. package/dist/components/Library/NumberField/NumberField.svelte +25 -19
  46. package/dist/components/Library/Pagination/Pagination.demo.svelte +3 -2
  47. package/dist/components/Library/Pagination/Pagination.svelte +6 -18
  48. package/dist/components/Library/Popover/PopoverDebugOverlay.svelte +3 -3
  49. package/dist/components/Library/Portal/Content.svelte +20 -0
  50. package/dist/components/Library/Portal/Content.svelte.d.ts +10 -0
  51. package/dist/components/Library/Portal/Portal.svelte +4 -0
  52. package/dist/components/Library/Portal/Portal.svelte.d.ts +1 -0
  53. package/dist/components/Library/Portal/index.d.ts +1 -0
  54. package/dist/components/Library/Portal/index.js +1 -0
  55. package/dist/components/Library/ScrollView/ScrollView.svelte +88 -9
  56. package/dist/components/Library/ScrollView/ScrollView.svelte.d.ts +9 -2
  57. package/dist/components/Library/ScrollView/index.d.ts +1 -1
  58. package/dist/components/Library/SearchField/SearchField.demo.svelte +3 -2
  59. package/dist/components/Library/SearchField/SearchField.svelte +5 -5
  60. package/dist/components/Library/SearchField/SearchField.svelte.d.ts +2 -1
  61. package/dist/components/Library/SegmentedPicker/SegmentedPicker.demo.svelte +3 -2
  62. package/dist/components/Library/Sheet/Sheet.demo.svelte +3 -2
  63. package/dist/components/Library/Sheet/Sheet.svelte +3 -3
  64. package/dist/components/Library/Slider/Slider.demo.svelte +3 -2
  65. package/dist/components/Library/Spinner/Spinner.demo.svelte +3 -2
  66. package/dist/components/Library/Switch/Switch.demo.svelte +5 -4
  67. package/dist/components/Library/Switch/Switch.svelte +6 -5
  68. package/dist/components/Library/Switch/Switch.svelte.d.ts +2 -1
  69. package/dist/components/Library/Table/Column.svelte +3 -0
  70. package/dist/components/Library/Table/Column.svelte.d.ts +1 -0
  71. package/dist/components/Library/Table/Table.demo.svelte +230 -17
  72. package/dist/components/Library/Table/Table.svelte +322 -78
  73. package/dist/components/Library/Table/Table.svelte.d.ts +5 -0
  74. package/dist/components/Library/Table/types.d.ts +1 -0
  75. package/dist/components/Library/TextField/TextField.svelte +20 -14
  76. package/dist/components/Library/TextShimmer/TextShimmer.demo.svelte +3 -2
  77. package/dist/components/Library/TimePicker/TimePicker.demo.svelte +3 -10
  78. package/dist/components/Library/TokenSearchField/TokenSearchField.demo.svelte +3 -2
  79. package/dist/components/Library/VStack/VStack.svelte +4 -4
  80. package/dist/components/Library/VStack/VStack.svelte.d.ts +2 -1
  81. package/dist/components/Local/ColorStyleControls.svelte +25 -72
  82. package/dist/components/Local/ComponentGrid.svelte +99 -27
  83. package/dist/components/Local/ComponentGrid.svelte.d.ts +2 -1
  84. package/dist/components/Local/ComponentPage.svelte +74 -0
  85. package/dist/components/Local/ComponentPage.svelte.d.ts +13 -0
  86. package/dist/components/Local/HeroCard.svelte +10 -6
  87. package/dist/components/Local/LayoutStyleControls.svelte +33 -25
  88. package/dist/components/Local/StyleControls.svelte +1 -1
  89. package/dist/index.d.ts +8 -3
  90. package/dist/index.js +4 -1
  91. package/dist/style/index.css +3 -4
  92. package/dist/style/layout.d.ts +15 -36
  93. package/dist/style/layout.js +35 -83
  94. package/dist/style/surface.d.ts +1 -0
  95. package/dist/style/surface.js +10 -0
  96. package/dist/style.css +3 -51
  97. package/dist/viewport/geometry.d.ts +8 -0
  98. package/dist/viewport/geometry.js +43 -0
  99. package/dist/viewport/index.d.ts +4 -0
  100. package/dist/viewport/index.js +4 -0
  101. package/dist/viewport/layout.d.ts +4 -0
  102. package/dist/viewport/layout.js +138 -0
  103. package/dist/viewport/placement.d.ts +4 -0
  104. package/dist/viewport/placement.js +14 -0
  105. package/dist/viewport/types.d.ts +81 -0
  106. package/dist/viewport/types.js +1 -0
  107. package/package.json +1 -1
@@ -8,6 +8,7 @@
8
8
  import { extractLoaderProps, resolveLoaderOptions, type LoaderProps } from '../../../style/loader';
9
9
  import { extractLayoutProps, layoutStyle, type LayoutProps } from '../../../style/layout';
10
10
  import { extractStyleProps, surfaceStyle, type StyleProps } from '../../../style/surface';
11
+ import { tableViewportLayout, type ViewportGeometry, type ViewportOverscan } from '../../../viewport';
11
12
  import ScrollView from '../ScrollView';
12
13
  import { TABLE_CONTEXT } from './context';
13
14
  import type {
@@ -31,6 +32,10 @@
31
32
  columnHeaders?: TableColumnHeaderVisibility;
32
33
  columnCustomization?: TableColumnCustomization;
33
34
  emptyLabel?: string;
35
+ virtualized?: boolean;
36
+ rowHeight?: number;
37
+ headerHeight?: number;
38
+ overscan?: ViewportOverscan;
34
39
  onSortOrderChange?: (sortOrder: TableSortDescriptor<T>[]) => void;
35
40
  onSelectionChange?: (selection: TableSelection) => void;
36
41
  } & LayoutProps &
@@ -48,12 +53,21 @@
48
53
  columnHeaders = 'visible',
49
54
  columnCustomization = $bindable<TableColumnCustomization>({}),
50
55
  emptyLabel = 'No rows',
56
+ virtualized = false,
57
+ rowHeight = 42,
58
+ headerHeight = 38,
59
+ overscan = 160,
51
60
  onSortOrderChange,
52
61
  onSelectionChange,
53
62
  ...restProps
54
63
  }: Props = $props();
55
64
 
56
65
  let tableSortOrder = $state<TableSortDescriptor<T>[]>(sortOrder);
66
+ let scrollOffsetX = $state(0);
67
+ let viewport = $state<ViewportGeometry>({
68
+ offset: { x: 0, y: 0 },
69
+ viewport: { width: 0, height: 0 }
70
+ });
57
71
  let dragSelection = $state<{
58
72
  active: boolean;
59
73
  pointerId: number;
@@ -88,18 +102,7 @@
88
102
  typeof value === 'number' ? `${value}px` : value;
89
103
 
90
104
  const tableShellStyle = $derived.by(() => {
91
- const hasExplicitHeight = layoutProps.height !== undefined || layoutProps.size !== undefined;
92
- const fallbackHeight = hasExplicitHeight ? undefined : (layoutProps.maxHeight ?? '100%');
93
-
94
- return [
95
- layoutStyle(layoutProps),
96
- rootStyle,
97
- layoutProps.height === undefined ? '' : `height: ${toCssSize(layoutProps.height)};`,
98
- layoutProps.maxHeight === undefined ? '' : `max-height: ${toCssSize(layoutProps.maxHeight)};`,
99
- fallbackHeight === undefined ? '' : `height: ${toCssSize(fallbackHeight)};`
100
- ]
101
- .filter(Boolean)
102
- .join(' ');
105
+ return [layoutStyle(layoutProps), rootStyle].filter(Boolean).join(' ');
103
106
  });
104
107
 
105
108
  const headerSpan = (value: TableColumn<T>['colspan'] | TableColumn<T>['rowspan']) =>
@@ -214,7 +217,18 @@
214
217
  (column, index) => !column.hidden && !hidden.has(columnKey(column, index))
215
218
  );
216
219
  });
220
+ const tableMinWidth = $derived.by(() => {
221
+ const widths = visibleColumns
222
+ .map((column) => toCssSize(column.width ?? column.minWidth))
223
+ .filter((width): width is string => Boolean(width));
217
224
 
225
+ if (widths.length === 0) return '100%';
226
+ return `max(100%, calc(${widths.join(' + ')}))`;
227
+ });
228
+ const tableWidthStyle = $derived(`width: ${tableMinWidth};`);
229
+ const headerStyle = $derived(
230
+ `${tableWidthStyle} transform: translate3d(${-scrollOffsetX}px, 0, 0);`
231
+ );
218
232
  const sortedRows = $derived.by(() => {
219
233
  if (tableSortOrder.length === 0) return data;
220
234
 
@@ -243,6 +257,32 @@
243
257
  return 0;
244
258
  });
245
259
  });
260
+ const engineColumns = $derived.by(() =>
261
+ visibleColumns.map((column, index) => ({
262
+ key: String(columnKey(column, index)),
263
+ width:
264
+ typeof column.width === 'number'
265
+ ? column.width
266
+ : typeof column.minWidth === 'number'
267
+ ? column.minWidth
268
+ : 160
269
+ }))
270
+ );
271
+ const engineLayout = $derived.by(() =>
272
+ tableViewportLayout({
273
+ rows: sortedRows,
274
+ columns: engineColumns,
275
+ viewport,
276
+ rowHeight,
277
+ headerHeight: 0,
278
+ overscan,
279
+ rowKey
280
+ })
281
+ );
282
+ const engineHeaderStyle = $derived.by(() => {
283
+ const width = `${engineLayout.content.width}px`;
284
+ return `width: ${width}; min-width: ${width}; height: ${headerHeight}px; transform: translate3d(${-scrollOffsetX}px, 0, 0);`;
285
+ });
246
286
 
247
287
  const selectedKeys = $derived.by(() => {
248
288
  if (selection instanceof Set) return new Set<TableRowKey>(selection);
@@ -391,6 +431,11 @@
391
431
  if (selectionMode === 'multiple' || isInteractiveTarget(event.target)) return;
392
432
  updateSelection(key);
393
433
  };
434
+
435
+ const handleScroll = (geometry: ViewportGeometry) => {
436
+ viewport = geometry;
437
+ scrollOffsetX = geometry.offset.x;
438
+ };
394
439
  </script>
395
440
 
396
441
  {#if children}
@@ -403,22 +448,27 @@
403
448
  onpointercancel={endDragSelection}
404
449
  />
405
450
 
406
- <div class="table-shell" style={tableShellStyle} {...forwardedProps}>
451
+ <div
452
+ class="table-shell"
453
+ class:table-shell-engine={virtualized}
454
+ style={tableShellStyle}
455
+ {...forwardedProps}
456
+ >
407
457
  {#if columnHeaders === 'visible'}
408
- <table
409
- class="table-header-table table"
410
- role={selectionMode === 'none' ? undefined : 'grid'}
411
- aria-multiselectable={selectionMode === 'multiple' ? 'true' : undefined}
412
- >
413
- <thead class="table-header">
414
- <tr>
458
+ <div class="table-header-frame">
459
+ {#if virtualized}
460
+ <div
461
+ class="table-header-table table-engine-header"
462
+ style={engineHeaderStyle}
463
+ role={selectionMode === 'none' ? undefined : 'row'}
464
+ >
415
465
  {#each visibleColumns as column, index (columnKey(column, index))}
416
- <th
417
- class={`table-heading table-align-${column.alignment ?? 'leading'}`}
418
- colspan={headerSpan(column.colspan)}
419
- rowspan={headerSpan(column.rowspan)}
420
- style:min-width={toCssSize(column.minWidth)}
421
- style:width={toCssSize(column.width)}
466
+ {@const layoutColumn = engineColumns[index]}
467
+ <div
468
+ class={`table-heading table-engine-heading table-align-${column.alignment ?? 'leading'}`}
469
+ style:width={`${layoutColumn.width}px`}
470
+ style:height={`${headerHeight}px`}
471
+ role={selectionMode === 'none' ? 'columnheader' : 'gridcell'}
422
472
  aria-sort={tableSortOrder[0]?.key === columnSortKey(column, index)
423
473
  ? tableSortOrder[0].direction === 'ascending'
424
474
  ? 'ascending'
@@ -432,7 +482,13 @@
432
482
  class="table-sort-button"
433
483
  onclick={() => updateSort(column, index)}
434
484
  >
435
- <span>{column.label}</span>
485
+ <span>
486
+ {#if column.header}
487
+ {@render column.header(column, index)}
488
+ {:else}
489
+ {column.label}
490
+ {/if}
491
+ </span>
436
492
  {#if sortDirection === 'ascending'}
437
493
  <ArrowUpIcon class="table-sort-icon" size={12} strokeWidth={2.25} />
438
494
  {:else if sortDirection === 'descending'}
@@ -445,61 +501,190 @@
445
501
  />
446
502
  {/if}
447
503
  </button>
504
+ {:else if column.header}
505
+ {@render column.header(column, index)}
448
506
  {:else}
449
507
  {column.label}
450
508
  {/if}
451
- </th>
509
+ </div>
452
510
  {/each}
453
- </tr>
454
- </thead>
455
- </table>
511
+ </div>
512
+ {:else}
513
+ <table
514
+ class="table-header-table table"
515
+ style={headerStyle}
516
+ role={selectionMode === 'none' ? undefined : 'grid'}
517
+ aria-multiselectable={selectionMode === 'multiple' ? 'true' : undefined}
518
+ >
519
+ <thead class="table-header">
520
+ <tr>
521
+ {#each visibleColumns as column, index (columnKey(column, index))}
522
+ <th
523
+ class={`table-heading table-align-${column.alignment ?? 'leading'}`}
524
+ colspan={headerSpan(column.colspan)}
525
+ rowspan={headerSpan(column.rowspan)}
526
+ style:min-width={toCssSize(column.minWidth)}
527
+ style:width={toCssSize(column.width)}
528
+ aria-sort={tableSortOrder[0]?.key === columnSortKey(column, index)
529
+ ? tableSortOrder[0].direction === 'ascending'
530
+ ? 'ascending'
531
+ : 'descending'
532
+ : undefined}
533
+ >
534
+ {#if column.sortable}
535
+ {@const sortDirection = activeSortDirection(column, index)}
536
+ <button
537
+ type="button"
538
+ class="table-sort-button"
539
+ onclick={() => updateSort(column, index)}
540
+ >
541
+ <span>
542
+ {#if column.header}
543
+ {@render column.header(column, index)}
544
+ {:else}
545
+ {column.label}
546
+ {/if}
547
+ </span>
548
+ {#if sortDirection === 'ascending'}
549
+ <ArrowUpIcon class="table-sort-icon" size={12} strokeWidth={2.25} />
550
+ {:else if sortDirection === 'descending'}
551
+ <ArrowDownIcon class="table-sort-icon" size={12} strokeWidth={2.25} />
552
+ {:else}
553
+ <ArrowUpDownIcon
554
+ class="table-sort-icon table-sort-icon-muted"
555
+ size={12}
556
+ strokeWidth={2.25}
557
+ />
558
+ {/if}
559
+ </button>
560
+ {:else if column.header}
561
+ {@render column.header(column, index)}
562
+ {:else}
563
+ {column.label}
564
+ {/if}
565
+ </th>
566
+ {/each}
567
+ </tr>
568
+ </thead>
569
+ </table>
570
+ {/if}
571
+ </div>
456
572
  {/if}
457
573
 
458
- <ScrollView axis="both" loader={bodyLoaderOptions} contentStyles={{ paddingX: 0, paddingY: 0 }}>
459
- <table
460
- class="table-body-table table"
461
- role={selectionMode === 'none' ? undefined : 'grid'}
462
- aria-multiselectable={selectionMode === 'multiple' ? 'true' : undefined}
574
+ {#if virtualized}
575
+ {#snippet layout()}
576
+ {#each engineLayout.rows as placedRow (placedRow.key)}
577
+ {@const key = rowKey(placedRow.item, placedRow.index)}
578
+ <div
579
+ data-table-row-key={String(key)}
580
+ class="table-engine-row table-row"
581
+ class:table-row-selectable={selectionMode !== 'none'}
582
+ class:table-row-selected={isSelected(key)}
583
+ style:width={`${placedRow.rect.width}px`}
584
+ style:height={`${placedRow.rect.height}px`}
585
+ style:transform={`translate3d(${placedRow.rect.x}px, ${placedRow.rect.y}px, 0)`}
586
+ aria-selected={selectionMode === 'none' ? undefined : isSelected(key)}
587
+ role={selectionMode === 'none' ? 'row' : 'row'}
588
+ onclick={(event) => handleRowClick(event, key)}
589
+ onpointerdown={(event) => startDragSelection(event, key)}
590
+ onpointerenter={(event) => continueDragSelection(event, key)}
591
+ onpointerup={endDragSelection}
592
+ onpointercancel={endDragSelection}
593
+ ></div>
594
+ {/each}
595
+ {#each engineLayout.cells as placedCell (placedCell.key)}
596
+ {@const column = visibleColumns[placedCell.item.columnIndex]}
597
+ {@const row = placedCell.item.row}
598
+ {@const index = placedCell.item.rowIndex}
599
+ {@const key = rowKey(row, index)}
600
+ <div
601
+ data-table-row-key={String(key)}
602
+ class={`table-engine-cell table-cell table-align-${column.alignment ?? 'leading'}`}
603
+ class:table-row-selectable={selectionMode !== 'none'}
604
+ style:width={`${placedCell.rect.width}px`}
605
+ style:height={`${placedCell.rect.height}px`}
606
+ style:transform={`translate3d(${placedCell.rect.x}px, ${placedCell.rect.y}px, 0)`}
607
+ aria-selected={selectionMode === 'none' ? undefined : isSelected(key)}
608
+ role={selectionMode === 'none' ? 'cell' : 'gridcell'}
609
+ onclick={(event) => handleRowClick(event, key)}
610
+ onpointerdown={(event) => startDragSelection(event, key)}
611
+ onpointerenter={(event) => continueDragSelection(event, key)}
612
+ onpointerup={endDragSelection}
613
+ onpointercancel={endDragSelection}
614
+ >
615
+ {#if column.cell}
616
+ {@render column.cell(row, index)}
617
+ {:else}
618
+ {displayValue(columnValue(column, row))}
619
+ {/if}
620
+ </div>
621
+ {/each}
622
+ {#if sortedRows.length === 0}
623
+ <div class="table-empty table-engine-empty">{emptyLabel}</div>
624
+ {/if}
625
+ {/snippet}
626
+
627
+ <ScrollView
628
+ axis="both"
629
+ loader={bodyLoaderOptions}
630
+ contentSize={engineLayout.content}
631
+ contentStyles={{ paddingX: 0, paddingY: 0 }}
632
+ onScroll={handleScroll}
633
+ {layout}
634
+ />
635
+ {:else}
636
+ <ScrollView
637
+ axis="both"
638
+ loader={bodyLoaderOptions}
639
+ contentStyles={{ paddingX: 0, paddingY: 0 }}
640
+ onScroll={handleScroll}
463
641
  >
464
- <tbody>
465
- {#each sortedRows as row, index (rowKey(row, index))}
466
- {@const key = rowKey(row, index)}
467
- <tr
468
- data-table-row-key={String(key)}
469
- class="table-row"
470
- class:table-row-selectable={selectionMode !== 'none'}
471
- class:table-row-selected={isSelected(key)}
472
- aria-selected={selectionMode === 'none' ? undefined : isSelected(key)}
473
- onclick={(event) => handleRowClick(event, key)}
474
- onpointerdown={(event) => startDragSelection(event, key)}
475
- onpointerenter={(event) => continueDragSelection(event, key)}
476
- onpointerup={endDragSelection}
477
- onpointercancel={endDragSelection}
478
- >
479
- {#each visibleColumns as column, columnIndex (columnKey(column, columnIndex))}
480
- <td
481
- class={`table-cell table-align-${column.alignment ?? 'leading'}`}
482
- colspan={bodySpan(column.colspan, row, index)}
483
- rowspan={bodySpan(column.rowspan, row, index)}
484
- style:min-width={toCssSize(column.minWidth)}
485
- style:width={toCssSize(column.width)}
486
- >
487
- {#if column.cell}
488
- {@render column.cell(row, index)}
489
- {:else}
490
- {displayValue(columnValue(column, row))}
491
- {/if}
492
- </td>
493
- {/each}
494
- </tr>
495
- {:else}
496
- <tr>
497
- <td class="table-empty" colspan={Math.max(visibleColumns.length, 1)}>{emptyLabel}</td>
498
- </tr>
499
- {/each}
500
- </tbody>
501
- </table>
502
- </ScrollView>
642
+ <table
643
+ class="table-body-table table"
644
+ style={tableWidthStyle}
645
+ role={selectionMode === 'none' ? undefined : 'grid'}
646
+ aria-multiselectable={selectionMode === 'multiple' ? 'true' : undefined}
647
+ >
648
+ <tbody>
649
+ {#each sortedRows as row, index (rowKey(row, index))}
650
+ {@const key = rowKey(row, index)}
651
+ <tr
652
+ data-table-row-key={String(key)}
653
+ class="table-row"
654
+ class:table-row-selectable={selectionMode !== 'none'}
655
+ class:table-row-selected={isSelected(key)}
656
+ aria-selected={selectionMode === 'none' ? undefined : isSelected(key)}
657
+ onclick={(event) => handleRowClick(event, key)}
658
+ onpointerdown={(event) => startDragSelection(event, key)}
659
+ onpointerenter={(event) => continueDragSelection(event, key)}
660
+ onpointerup={endDragSelection}
661
+ onpointercancel={endDragSelection}
662
+ >
663
+ {#each visibleColumns as column, columnIndex (columnKey(column, columnIndex))}
664
+ <td
665
+ class={`table-cell table-align-${column.alignment ?? 'leading'}`}
666
+ colspan={bodySpan(column.colspan, row, index)}
667
+ rowspan={bodySpan(column.rowspan, row, index)}
668
+ style:min-width={toCssSize(column.minWidth)}
669
+ style:width={toCssSize(column.width)}
670
+ >
671
+ {#if column.cell}
672
+ {@render column.cell(row, index)}
673
+ {:else}
674
+ {displayValue(columnValue(column, row))}
675
+ {/if}
676
+ </td>
677
+ {/each}
678
+ </tr>
679
+ {:else}
680
+ <tr>
681
+ <td class="table-empty" colspan={Math.max(visibleColumns.length, 1)}>{emptyLabel}</td>
682
+ </tr>
683
+ {/each}
684
+ </tbody>
685
+ </table>
686
+ </ScrollView>
687
+ {/if}
503
688
  </div>
504
689
 
505
690
  <style>
@@ -512,8 +697,10 @@
512
697
  flex-direction: column;
513
698
  flex: 1 1 auto;
514
699
  align-self: stretch;
700
+ min-width: 0;
515
701
  min-height: 0;
516
702
  width: 100%;
703
+ max-width: 100%;
517
704
  overflow: hidden;
518
705
  border: 1px solid var(--sveltely-border-color);
519
706
  border-radius: var(--sveltely-border-radius);
@@ -524,7 +711,7 @@
524
711
 
525
712
  .table {
526
713
  width: 100%;
527
- min-width: max-content;
714
+ min-width: 100%;
528
715
  border-collapse: separate;
529
716
  border-spacing: 0;
530
717
  }
@@ -537,19 +724,37 @@
537
724
  );
538
725
  }
539
726
 
727
+ .table-header-frame {
728
+ width: 100%;
729
+ min-width: 0;
730
+ overflow: hidden;
731
+ border-bottom: 1px solid var(--sveltely-border-color);
732
+ }
733
+
540
734
  .table-header-table {
541
735
  flex: 0 0 auto;
542
- border-bottom: 1px solid var(--sveltely-border-color);
736
+ will-change: transform;
737
+ }
738
+
739
+ .table-engine-header {
740
+ display: flex;
741
+ background: color-mix(
742
+ in oklab,
743
+ var(--sveltely-background-color) 92%,
744
+ var(--sveltely-control-active-color)
745
+ );
543
746
  }
544
747
 
545
- .table-header-table + :global(.scroll-view) {
748
+ .table-header-frame + :global(.scroll-view) {
546
749
  border-top-left-radius: 0;
547
750
  border-top-right-radius: 0;
548
751
  }
549
752
 
550
753
  .table-shell > :global(.scroll-view) {
551
754
  flex: 1 1 auto;
755
+ min-width: 0;
552
756
  min-height: 0;
757
+ max-width: 100%;
553
758
  }
554
759
 
555
760
  .table-heading,
@@ -571,6 +776,45 @@
571
776
  color: inherit;
572
777
  }
573
778
 
779
+ .table-engine-heading,
780
+ .table-engine-cell {
781
+ display: flex;
782
+ align-items: center;
783
+ min-width: 0;
784
+ overflow: hidden;
785
+ text-overflow: ellipsis;
786
+ }
787
+
788
+ .table-engine-cell {
789
+ position: absolute;
790
+ left: 0;
791
+ top: 0;
792
+ z-index: 1;
793
+ background: transparent;
794
+ }
795
+
796
+ .table-engine-row {
797
+ position: absolute;
798
+ left: 0;
799
+ top: 0;
800
+ z-index: 0;
801
+ border-bottom: 1px solid var(--sveltely-border-color);
802
+ }
803
+
804
+ .table-engine-row.table-row-selectable {
805
+ cursor: default;
806
+ }
807
+
808
+ .table-engine-row.table-row-selectable:hover {
809
+ background: color-mix(in oklab, var(--sveltely-control-active-color) 8%, transparent);
810
+ }
811
+
812
+ .table-engine-empty {
813
+ position: absolute;
814
+ inset: 0 auto auto 0;
815
+ width: 100%;
816
+ }
817
+
574
818
  .table-row:last-child .table-cell {
575
819
  border-bottom: 0;
576
820
  }
@@ -3,6 +3,7 @@ import { type Snippet } from 'svelte';
3
3
  import { type LoaderProps } from '../../../style/loader';
4
4
  import { type LayoutProps } from '../../../style/layout';
5
5
  import { type StyleProps } from '../../../style/surface';
6
+ import { type ViewportOverscan } from '../../../viewport';
6
7
  import type { TableColumn, TableColumnCustomization, TableColumnHeaderVisibility, TableSelection, TableSelectionMode, TableSortDescriptor } from './types';
7
8
  declare function $$render<T extends Record<string, unknown>>(): {
8
9
  props: {
@@ -15,6 +16,10 @@ declare function $$render<T extends Record<string, unknown>>(): {
15
16
  columnHeaders?: TableColumnHeaderVisibility;
16
17
  columnCustomization?: TableColumnCustomization;
17
18
  emptyLabel?: string;
19
+ virtualized?: boolean;
20
+ rowHeight?: number;
21
+ headerHeight?: number;
22
+ overscan?: ViewportOverscan;
18
23
  onSortOrderChange?: (sortOrder: TableSortDescriptor<T>[]) => void;
19
24
  onSelectionChange?: (selection: TableSelection) => void;
20
25
  } & LayoutProps & StyleProps & LoaderProps & Record<string, unknown>;
@@ -18,6 +18,7 @@ export type TableColumnCustomization = {
18
18
  export type TableColumn<T> = {
19
19
  key?: string;
20
20
  label: string;
21
+ header?: Snippet<[column: TableColumn<T>, index: number]>;
21
22
  value?: keyof T | ((row: T) => unknown);
22
23
  cell?: Snippet<[row: T, index: number]>;
23
24
  width?: number | string;
@@ -76,20 +76,22 @@
76
76
  data-disabled={disabled ? 'true' : 'false'}
77
77
  data-error={error ? 'true' : 'false'}
78
78
  >
79
- <input
80
- bind:value
81
- {type}
82
- {disabled}
83
- {name}
84
- {autocomplete}
85
- {required}
86
- {readonly}
87
- {inputmode}
88
- {placeholder}
89
- aria-invalid={error ? 'true' : undefined}
90
- aria-describedby={describedBy}
91
- class="text-field-input"
92
- />
79
+ <div class="text-field-control">
80
+ <input
81
+ bind:value
82
+ {type}
83
+ {disabled}
84
+ {name}
85
+ {autocomplete}
86
+ {required}
87
+ {readonly}
88
+ {inputmode}
89
+ {placeholder}
90
+ aria-invalid={error ? 'true' : undefined}
91
+ aria-describedby={describedBy}
92
+ class="text-field-input"
93
+ />
94
+ </div>
93
95
 
94
96
  {#if help}
95
97
  <span id="text-field-message" class="text-field-message">{help}</span>
@@ -107,6 +109,10 @@
107
109
  font-size: var(--text-field-font-size, var(--sveltely-font-size));
108
110
  }
109
111
 
112
+ .text-field-control {
113
+ display: contents;
114
+ }
115
+
110
116
  .text-field-input {
111
117
  width: 100%;
112
118
  min-width: 0;
@@ -6,9 +6,10 @@
6
6
  </script>
7
7
 
8
8
  <script lang="ts">
9
+ import VStack from '../VStack';
9
10
  import TextShimmer from './TextShimmer.svelte';
10
11
  </script>
11
12
 
12
- <div class="vstack gap-2">
13
+ <VStack gap={5}>
13
14
  <TextShimmer as="h2" text="Sveltely shimmer" />
14
- </div>
15
+ </VStack>
@@ -8,19 +8,12 @@
8
8
  </script>
9
9
 
10
10
  <script lang="ts">
11
+ import VStack from '../VStack';
11
12
  import TimePicker from './TimePicker.svelte';
12
13
 
13
14
  let value = $state<Date | null>(new Date());
14
15
  </script>
15
16
 
16
- <div class="time-picker-demo vstack gap-4">
17
+ <VStack width="100%" height="100%" gap={10}>
17
18
  <TimePicker bind:value />
18
- </div>
19
-
20
- <style>
21
- .time-picker-demo {
22
- width: 100%;
23
- height: 100%;
24
- min-height: 0;
25
- }
26
- </style>
19
+ </VStack>
@@ -7,15 +7,16 @@
7
7
  </script>
8
8
 
9
9
  <script lang="ts">
10
+ import VStack from '../VStack';
10
11
  import TokenSearchField from './TokenSearchField.svelte';
11
12
 
12
13
  let tokens = $state(['svelte', 'ui']);
13
14
  let value = $state('');
14
15
  </script>
15
16
 
16
- <div class="vstack w-full max-w-md gap-2">
17
+ <VStack width="28rem" gap={5}>
17
18
  <TokenSearchField bind:tokens bind:value placeholder="Search keywords" />
18
19
  <p class="text-xs text-[var(--sveltely-text-secondary-color)]">
19
20
  Tokens: {tokens.join(', ') || 'empty'}
20
21
  </p>
21
- </div>
22
+ </VStack>