@gradio/dataframe 0.22.0 → 0.23.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 (128) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/Dataframe.test.ts +697 -32
  3. package/Index.svelte +107 -106
  4. package/dist/Index.svelte +107 -106
  5. package/dist/Index.svelte.d.ts +0 -1
  6. package/dist/shared/BooleanCell.svelte +12 -6
  7. package/dist/shared/BooleanCell.svelte.d.ts +4 -18
  8. package/dist/shared/CellMenu.svelte +74 -49
  9. package/dist/shared/CellMenu.svelte.d.ts +5 -19
  10. package/dist/shared/CellMenuButton.svelte +3 -3
  11. package/dist/shared/CellMenuButton.svelte.d.ts +4 -18
  12. package/dist/shared/CellMenuIcons.svelte +1 -1
  13. package/dist/shared/CellMenuIcons.svelte.d.ts +4 -18
  14. package/dist/shared/DataCell.svelte +189 -0
  15. package/dist/shared/DataCell.svelte.d.ts +44 -0
  16. package/dist/shared/EditableCell.svelte +88 -64
  17. package/dist/shared/EditableCell.svelte.d.ts +8 -26
  18. package/dist/shared/EmptyRowButton.svelte +2 -2
  19. package/dist/shared/EmptyRowButton.svelte.d.ts +4 -18
  20. package/dist/shared/FilterMenu.svelte +26 -18
  21. package/dist/shared/FilterMenu.svelte.d.ts +5 -21
  22. package/dist/shared/HeaderCell.svelte +218 -0
  23. package/dist/shared/HeaderCell.svelte.d.ts +30 -0
  24. package/dist/shared/RowNumber.svelte +4 -2
  25. package/dist/shared/RowNumber.svelte.d.ts +4 -18
  26. package/dist/shared/Table.svelte +1037 -949
  27. package/dist/shared/Table.svelte.d.ts +12 -30
  28. package/dist/shared/Toolbar.svelte +32 -22
  29. package/dist/shared/Toolbar.svelte.d.ts +6 -21
  30. package/dist/shared/column_measurement.svelte.d.ts +20 -0
  31. package/dist/shared/column_measurement.svelte.js +79 -0
  32. package/dist/shared/icons/FilterIcon.svelte +14 -6
  33. package/dist/shared/icons/FilterIcon.svelte.d.ts +14 -6
  34. package/dist/shared/icons/Padlock.svelte +6 -2
  35. package/dist/shared/icons/Padlock.svelte.d.ts +4 -25
  36. package/dist/shared/icons/SelectionButtons.svelte +23 -8
  37. package/dist/shared/icons/SelectionButtons.svelte.d.ts +4 -18
  38. package/dist/shared/icons/SortButtonDown.svelte +15 -4
  39. package/dist/shared/icons/SortButtonDown.svelte.d.ts +4 -25
  40. package/dist/shared/icons/SortButtonUp.svelte +15 -5
  41. package/dist/shared/icons/SortButtonUp.svelte.d.ts +4 -25
  42. package/dist/shared/icons/SortIcon.svelte +13 -7
  43. package/dist/shared/icons/SortIcon.svelte.d.ts +7 -21
  44. package/dist/shared/tanstack/index.d.ts +3 -0
  45. package/dist/shared/tanstack/index.js +3 -0
  46. package/dist/shared/tanstack/table.svelte.d.ts +26 -0
  47. package/dist/shared/tanstack/table.svelte.js +102 -0
  48. package/dist/shared/tanstack/virtual.svelte.d.ts +14 -0
  49. package/dist/shared/tanstack/virtual.svelte.js +76 -0
  50. package/dist/shared/types.d.ts +2 -0
  51. package/dist/shared/utils/filter.d.ts +5 -0
  52. package/dist/shared/utils/filter.js +62 -0
  53. package/dist/shared/utils/selection_utils.js +1 -1
  54. package/dist/shared/utils/table_utils.d.ts +1 -14
  55. package/dist/shared/utils/table_utils.js +0 -61
  56. package/dist/standalone/Index.svelte +71 -46
  57. package/dist/standalone/Index.svelte.d.ts +4 -23
  58. package/dist/types.d.ts +32 -0
  59. package/dist/types.js +1 -0
  60. package/package.json +10 -9
  61. package/shared/BooleanCell.svelte +12 -6
  62. package/shared/CellMenu.svelte +74 -49
  63. package/shared/CellMenuButton.svelte +3 -3
  64. package/shared/CellMenuIcons.svelte +1 -1
  65. package/shared/DataCell.svelte +189 -0
  66. package/shared/EditableCell.svelte +88 -64
  67. package/shared/EmptyRowButton.svelte +2 -2
  68. package/shared/FilterMenu.svelte +26 -18
  69. package/shared/HeaderCell.svelte +218 -0
  70. package/shared/RowNumber.svelte +4 -2
  71. package/shared/Table.svelte +1037 -949
  72. package/shared/Toolbar.svelte +32 -22
  73. package/shared/column_measurement.svelte.ts +103 -0
  74. package/shared/icons/FilterIcon.svelte +14 -6
  75. package/shared/icons/Padlock.svelte +6 -2
  76. package/shared/icons/SelectionButtons.svelte +23 -8
  77. package/shared/icons/SortButtonDown.svelte +15 -4
  78. package/shared/icons/SortButtonUp.svelte +15 -5
  79. package/shared/icons/SortIcon.svelte +13 -7
  80. package/shared/tanstack/index.ts +23 -0
  81. package/shared/tanstack/table.svelte.ts +117 -0
  82. package/shared/tanstack/virtual.svelte.ts +105 -0
  83. package/shared/types.ts +3 -0
  84. package/shared/utils/filter.ts +66 -0
  85. package/shared/utils/selection_utils.ts +1 -1
  86. package/shared/utils/table_utils.ts +1 -100
  87. package/standalone/Index.svelte +71 -46
  88. package/test/filter.test.ts +350 -0
  89. package/test/selection_utils.test.ts +416 -0
  90. package/test/table_utils.test.ts +45 -112
  91. package/types.ts +34 -0
  92. package/dist/shared/TableCell.svelte +0 -330
  93. package/dist/shared/TableCell.svelte.d.ts +0 -66
  94. package/dist/shared/TableHeader.svelte +0 -344
  95. package/dist/shared/TableHeader.svelte.d.ts +0 -61
  96. package/dist/shared/VirtualTable.svelte +0 -379
  97. package/dist/shared/VirtualTable.svelte.d.ts +0 -42
  98. package/dist/shared/context/dataframe_context.d.ts +0 -180
  99. package/dist/shared/context/dataframe_context.js +0 -412
  100. package/dist/shared/icons/SortArrowDown.svelte +0 -25
  101. package/dist/shared/icons/SortArrowDown.svelte.d.ts +0 -20
  102. package/dist/shared/icons/SortArrowUp.svelte +0 -25
  103. package/dist/shared/icons/SortArrowUp.svelte.d.ts +0 -20
  104. package/dist/shared/utils/data_processing.d.ts +0 -14
  105. package/dist/shared/utils/data_processing.js +0 -47
  106. package/dist/shared/utils/drag_utils.d.ts +0 -15
  107. package/dist/shared/utils/drag_utils.js +0 -67
  108. package/dist/shared/utils/filter_utils.d.ts +0 -29
  109. package/dist/shared/utils/filter_utils.js +0 -123
  110. package/dist/shared/utils/keyboard_utils.d.ts +0 -3
  111. package/dist/shared/utils/keyboard_utils.js +0 -236
  112. package/dist/shared/utils/menu_utils.d.ts +0 -42
  113. package/dist/shared/utils/menu_utils.js +0 -58
  114. package/dist/shared/utils/sort_utils.d.ts +0 -26
  115. package/dist/shared/utils/sort_utils.js +0 -59
  116. package/shared/TableCell.svelte +0 -330
  117. package/shared/TableHeader.svelte +0 -344
  118. package/shared/VirtualTable.svelte +0 -379
  119. package/shared/context/dataframe_context.ts +0 -701
  120. package/shared/icons/SortArrowDown.svelte +0 -25
  121. package/shared/icons/SortArrowUp.svelte +0 -25
  122. package/shared/utils/data_processing.ts +0 -77
  123. package/shared/utils/drag_utils.ts +0 -105
  124. package/shared/utils/filter_utils.ts +0 -208
  125. package/shared/utils/keyboard_utils.ts +0 -309
  126. package/shared/utils/menu_utils.ts +0 -115
  127. package/shared/utils/sort_utils.ts +0 -91
  128. package/test/sort_utils.test.ts +0 -323
@@ -1,25 +1,33 @@
1
1
  <script lang="ts">
2
2
  import { Copy, Check } from "@gradio/icons";
3
3
  import { FullscreenButton } from "@gradio/atoms";
4
- import { onDestroy } from "svelte";
5
- import { createEventDispatcher } from "svelte";
6
4
  import { IconButton } from "@gradio/atoms";
7
5
 
8
- export let show_fullscreen_button = false;
9
- export let show_copy_button = false;
10
- export let show_search: "none" | "search" | "filter" = "none";
11
- export let fullscreen = false;
12
- export let on_copy: () => Promise<void>;
13
- export let on_commit_filter: () => void;
14
-
15
- const dispatch = createEventDispatcher<{
16
- search: string | null;
17
- }>();
18
-
19
- let copied = false;
6
+ let {
7
+ show_fullscreen_button = false,
8
+ show_copy_button = false,
9
+ show_search = "none",
10
+ fullscreen = false,
11
+ on_copy,
12
+ on_commit_filter,
13
+ current_search_query = null,
14
+ onsearch,
15
+ onfullscreen
16
+ }: {
17
+ show_fullscreen_button?: boolean;
18
+ show_copy_button?: boolean;
19
+ show_search?: "none" | "search" | "filter";
20
+ fullscreen?: boolean;
21
+ on_copy: () => Promise<void>;
22
+ on_commit_filter: () => void;
23
+ current_search_query?: string | null;
24
+ onsearch?: (query: string | null) => void;
25
+ onfullscreen?: () => void;
26
+ } = $props();
27
+
28
+ let copied = $state(false);
20
29
  let timer: ReturnType<typeof setTimeout>;
21
- export let current_search_query: string | null = null;
22
- let input_value = "";
30
+ let input_value = $state("");
23
31
 
24
32
  function handle_search_input(e: Event): void {
25
33
  const target = e.target as HTMLTextAreaElement;
@@ -27,7 +35,7 @@
27
35
  const new_query = input_value || null;
28
36
  if (current_search_query !== new_query) {
29
37
  current_search_query = new_query;
30
- dispatch("search", current_search_query);
38
+ onsearch?.(current_search_query);
31
39
  }
32
40
  }
33
41
 
@@ -44,8 +52,10 @@
44
52
  copy_feedback();
45
53
  }
46
54
 
47
- onDestroy(() => {
48
- if (timer) clearTimeout(timer);
55
+ $effect(() => {
56
+ return () => {
57
+ if (timer) clearTimeout(timer);
58
+ };
49
59
  });
50
60
  </script>
51
61
 
@@ -56,7 +66,7 @@
56
66
  <input
57
67
  type="text"
58
68
  value={current_search_query || ""}
59
- on:input={handle_search_input}
69
+ oninput={handle_search_input}
60
70
  placeholder={show_search === "filter" ? "Filter..." : "Search..."}
61
71
  class="search-input"
62
72
  class:filter-mode={show_search === "filter"}
@@ -65,7 +75,7 @@
65
75
  {#if current_search_query && show_search === "filter"}
66
76
  <button
67
77
  class="toolbar-button check-button"
68
- on:click={on_commit_filter}
78
+ onclick={on_commit_filter}
69
79
  aria-label="Apply filter and update dataframe values"
70
80
  title="Apply filter and update dataframe values"
71
81
  >
@@ -82,7 +92,7 @@
82
92
  />
83
93
  {/if}
84
94
  {#if show_fullscreen_button}
85
- <FullscreenButton {fullscreen} on:fullscreen />
95
+ <FullscreenButton {fullscreen} onclick={(_fs) => onfullscreen?.()} />
86
96
  {/if}
87
97
  </div>
88
98
  </div>
@@ -0,0 +1,103 @@
1
+ /**
2
+ * measures header cell positions/widths and provides inline styles
3
+ * for absolutely-positioned body cells to match.
4
+ */
5
+ export function create_column_measurement(opts: {
6
+ header_row_el: () => HTMLTableRowElement | undefined;
7
+ header_table_el: () => HTMLTableElement | undefined;
8
+ resolved_headers: () => string[];
9
+ row_data: () => any[];
10
+ show_row_numbers: () => boolean;
11
+ column_widths: () => string[];
12
+ on_resize?: () => void;
13
+ }): {
14
+ readonly col_widths: number[];
15
+ readonly col_lefts: number[];
16
+ readonly total_header_width: number;
17
+ readonly header_height: number;
18
+ readonly row_num_width: number;
19
+ get_col_style: (index: number) => string;
20
+ } {
21
+ let col_widths: number[] = $state([]);
22
+ let total_header_width: number = $state(0);
23
+ let header_height: number = $state(0);
24
+
25
+ // use offsets (not just widths) avoids accumulated subpixel rounding errors
26
+ let col_lefts: number[] = $state([]);
27
+
28
+ function measure(): void {
29
+ const current_row_el = opts.header_row_el();
30
+ const current_table_el = opts.header_table_el();
31
+ if (!current_row_el) return;
32
+
33
+ const cells = current_row_el.querySelectorAll<HTMLElement>(".header-cell");
34
+ const table_rect = current_table_el?.getBoundingClientRect();
35
+ const table_left = table_rect?.left ?? 0;
36
+
37
+ col_lefts = Array.from(cells).map(
38
+ (c) => c.getBoundingClientRect().left - table_left
39
+ );
40
+ col_widths = Array.from(cells).map((c) => c.getBoundingClientRect().width);
41
+ if (current_table_el) {
42
+ total_header_width = table_rect?.width ?? 0;
43
+ header_height = table_rect?.height ?? 0;
44
+ }
45
+ opts.on_resize?.();
46
+ }
47
+
48
+ $effect(() => {
49
+ const table_el = opts.header_table_el();
50
+ const row_el = opts.header_row_el();
51
+ if (!table_el || !row_el) return;
52
+
53
+ // re-run when columns change so we observe new cells
54
+ opts.resolved_headers();
55
+
56
+ const ro = new ResizeObserver(measure);
57
+ ro.observe(table_el);
58
+
59
+ // observe individual header cells so content-driven column
60
+ // width changes (editing, data updates) trigger a re-measure
61
+ // even when the overall table dimensions stay the same
62
+ const cells = row_el.querySelectorAll<HTMLElement>(".header-cell");
63
+ cells.forEach((cell) => ro.observe(cell));
64
+
65
+ return () => ro.disconnect();
66
+ });
67
+
68
+ let row_num_width = $derived.by(() => {
69
+ if (!opts.show_row_numbers()) return 0;
70
+ const row_el = opts.header_row_el();
71
+ if (!row_el) return 0;
72
+ const el = row_el.querySelector<HTMLElement>(".row-number-header");
73
+ return el?.getBoundingClientRect().width ?? 48;
74
+ });
75
+
76
+ function get_col_style(index: number): string {
77
+ if (col_widths[index] !== undefined) {
78
+ return `width: ${col_widths[index]}px; flex: 0 0 ${col_widths[index]}px;`;
79
+ }
80
+ const user_widths = opts.column_widths();
81
+ if (user_widths[index]) return `width: ${user_widths[index]};`;
82
+ return "width: 150px;";
83
+ }
84
+
85
+ return {
86
+ get col_widths() {
87
+ return col_widths;
88
+ },
89
+ get col_lefts() {
90
+ return col_lefts;
91
+ },
92
+ get total_header_width() {
93
+ return total_header_width;
94
+ },
95
+ get header_height() {
96
+ return header_height;
97
+ },
98
+ get row_num_width() {
99
+ return row_num_width;
100
+ },
101
+ get_col_style
102
+ };
103
+ }
@@ -1,12 +1,20 @@
1
- <script lang="ts">
2
- </script>
3
-
4
- <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1
+ <svg viewBox="0 0 24 24" width="20" height="20">
5
2
  <path
6
- d="M4 4h16v2.67l-6.67 6.67v8L9.33 19v-5.66L2.67 6.67V4h1.33z"
3
+ d="M5 5H19"
4
+ stroke="currentColor"
5
+ stroke-width="2"
6
+ stroke-linecap="round"
7
+ />
8
+ <path
9
+ d="M8 9H16"
10
+ stroke="currentColor"
11
+ stroke-width="2"
12
+ stroke-linecap="round"
13
+ />
14
+ <path
15
+ d="M11 13H13"
7
16
  stroke="currentColor"
8
17
  stroke-width="2"
9
18
  stroke-linecap="round"
10
- stroke-linejoin="round"
11
19
  />
12
20
  </svg>
@@ -1,8 +1,12 @@
1
+ <script lang="ts">
2
+ let { size = 16 } = $props();
3
+ </script>
4
+
1
5
  <div class="wrapper" aria-label="Static column">
2
6
  <svg
3
7
  xmlns="http://www.w3.org/2000/svg"
4
- width="13"
5
- height="13"
8
+ width={size}
9
+ height={size}
6
10
  viewBox="0 0 24 24"
7
11
  fill="none"
8
12
  stroke="currentColor"
@@ -1,25 +1,36 @@
1
1
  <script lang="ts">
2
- export let position: "column" | "row";
3
- export let coords: [number, number];
4
- export let on_click: (() => void) | null = null;
2
+ let {
3
+ position,
4
+ coords,
5
+ on_click = null
6
+ }: {
7
+ position: "column" | "row";
8
+ coords: [number, number];
9
+ on_click?: (() => void) | null;
10
+ } = $props();
5
11
 
6
- $: is_first_position =
7
- position === "column" ? coords[0] === 0 : coords[1] === 0;
8
- $: direction =
12
+ let is_first_position = $derived(
13
+ position === "column" ? coords[0] === 0 : coords[1] === 0
14
+ );
15
+ let direction = $derived(
9
16
  position === "column"
10
17
  ? is_first_position
11
18
  ? "down"
12
19
  : "up"
13
20
  : is_first_position
14
21
  ? "right"
15
- : "left";
22
+ : "left"
23
+ );
16
24
  </script>
17
25
 
18
26
  <button
19
27
  class="selection-button selection-button-{position} {is_first_position
20
28
  ? `move-${direction}`
21
29
  : ''}"
22
- on:click|stopPropagation={() => on_click && on_click()}
30
+ onclick={(e: MouseEvent) => {
31
+ e.stopPropagation();
32
+ on_click && on_click();
33
+ }}
23
34
  aria-label={`Select ${position}`}
24
35
  >
25
36
  <span class={direction}>
@@ -50,6 +61,8 @@
50
61
 
51
62
  .selection-button-row {
52
63
  left: calc(var(--size-2-5) * -1);
64
+ top: 50%;
65
+ transform: translateY(-50%);
53
66
  border-radius: var(--radius-sm) 0 0 var(--radius-sm);
54
67
  }
55
68
 
@@ -62,6 +75,8 @@
62
75
  .move-right {
63
76
  left: auto;
64
77
  right: calc(var(--size-2-5) * -1);
78
+ top: 50%;
79
+ transform: translateY(-50%);
65
80
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
66
81
  }
67
82
 
@@ -1,14 +1,25 @@
1
+ <script lang="ts">
2
+ let { size = 16 } = $props();
3
+ </script>
4
+
1
5
  <svg
2
- viewBox="0 0 24 24"
6
+ width={size}
7
+ height={size}
8
+ viewBox="0 0 16 16"
3
9
  fill="none"
4
10
  xmlns="http://www.w3.org/2000/svg"
5
- focusable="false"
6
11
  >
7
12
  <path
8
- d="M7 10l5 5 5-5"
13
+ d="M4 8L8 12L12 8"
9
14
  stroke="currentColor"
10
- stroke-width="2"
15
+ stroke-width="1.5"
11
16
  stroke-linecap="round"
12
17
  stroke-linejoin="round"
13
18
  />
19
+ <path
20
+ d="M8 12V4"
21
+ stroke="currentColor"
22
+ stroke-width="1.5"
23
+ stroke-linecap="round"
24
+ />
14
25
  </svg>
@@ -1,15 +1,25 @@
1
+ <script lang="ts">
2
+ let { size = 16 } = $props();
3
+ </script>
4
+
1
5
  <svg
2
- viewBox="0 0 24 24"
6
+ width={size}
7
+ height={size}
8
+ viewBox="0 0 16 16"
3
9
  fill="none"
4
10
  xmlns="http://www.w3.org/2000/svg"
5
- aria-hidden="true"
6
- focusable="false"
7
11
  >
8
12
  <path
9
- d="M7 14l5-5 5 5"
13
+ d="M4 8L8 4L12 8"
10
14
  stroke="currentColor"
11
- stroke-width="2"
15
+ stroke-width="1.5"
12
16
  stroke-linecap="round"
13
17
  stroke-linejoin="round"
14
18
  />
19
+ <path
20
+ d="M8 4V12"
21
+ stroke="currentColor"
22
+ stroke-width="1.5"
23
+ stroke-linecap="round"
24
+ />
15
25
  </svg>
@@ -1,15 +1,21 @@
1
1
  <script lang="ts">
2
- import { createEventDispatcher } from "svelte";
3
2
  import type { I18nFormatter } from "@gradio/utils";
4
3
  import SortButtonUp from "./SortButtonUp.svelte";
5
4
  import SortButtonDown from "./SortButtonDown.svelte";
6
5
  import { IconButton } from "@gradio/atoms";
7
6
  type SortDirection = "asc" | "desc";
8
- export let direction: SortDirection | null = null;
9
- export let priority: number | null = null;
10
- export let i18n: I18nFormatter;
11
7
 
12
- const dispatch = createEventDispatcher<{ sort: SortDirection }>();
8
+ let {
9
+ direction = null,
10
+ priority = null,
11
+ i18n,
12
+ onsort
13
+ }: {
14
+ direction?: SortDirection | null;
15
+ priority?: number | null;
16
+ i18n: I18nFormatter;
17
+ onsort?: (direction: SortDirection) => void;
18
+ } = $props();
13
19
  </script>
14
20
 
15
21
  <div class="sort-icons" role="group" aria-label={i18n("dataframe.sort_column")}>
@@ -25,7 +31,7 @@
25
31
  highlight={direction === "asc"}
26
32
  onclick={(event) => {
27
33
  event.stopPropagation();
28
- dispatch("sort", "asc");
34
+ onsort?.("asc");
29
35
  }}
30
36
  ></IconButton>
31
37
  <IconButton
@@ -35,7 +41,7 @@
35
41
  highlight={direction === "desc"}
36
42
  onclick={(event) => {
37
43
  event.stopPropagation();
38
- dispatch("sort", "desc");
44
+ onsort?.("desc");
39
45
  }}
40
46
  ></IconButton>
41
47
  </div>
@@ -0,0 +1,23 @@
1
+ export { createSvelteTable } from "./table.svelte.js";
2
+
3
+ export { createSvelteVirtualizer, type VirtualItem } from "./virtual.svelte.js";
4
+
5
+ export {
6
+ createColumnHelper,
7
+ getCoreRowModel,
8
+ getSortedRowModel,
9
+ getFilteredRowModel,
10
+ type ColumnDef,
11
+ type TableOptions,
12
+ type Table,
13
+ type Header,
14
+ type Cell,
15
+ type Row,
16
+ type SortingState,
17
+ type ColumnFiltersState,
18
+ type ColumnPinningState,
19
+ type FilterFn,
20
+ type SortingFn,
21
+ type CellContext,
22
+ type HeaderContext
23
+ } from "@tanstack/table-core";
@@ -0,0 +1,117 @@
1
+ import {
2
+ createTable,
3
+ type RowData,
4
+ type TableOptions,
5
+ type TableOptionsResolved,
6
+ type TableState
7
+ } from "@tanstack/table-core";
8
+
9
+ /**
10
+ * Merges objects while preserving property getters for lazy evaluation.
11
+ * Properties are defined as getters that look up values from sources in
12
+ * reverse order at access time. This is critical: it means reading a
13
+ * property from the merged result doesn't happen until the property is
14
+ * actually accessed, not when mergeObjects is called.
15
+ */
16
+ export function mergeObjects(...sources: any): any {
17
+ const target: Record<string, any> = {};
18
+ for (let i = 0; i < sources.length; i++) {
19
+ let source = sources[i];
20
+ if (typeof source === "function") source = source();
21
+ if (source) {
22
+ const descriptors = Object.getOwnPropertyDescriptors(source);
23
+ for (const key in descriptors) {
24
+ if (key in target) continue;
25
+ Object.defineProperty(target, key, {
26
+ enumerable: true,
27
+ get() {
28
+ for (let j = sources.length - 1; j >= 0; j--) {
29
+ let s = sources[j];
30
+ if (typeof s === "function") s = s();
31
+ const v = (s || {})[key];
32
+ if (v !== undefined) return v;
33
+ }
34
+ }
35
+ });
36
+ }
37
+ }
38
+ }
39
+ return target;
40
+ }
41
+
42
+ /**
43
+ * Creates a reactive TanStack Table for Svelte 5.
44
+ *
45
+ * The reactivity works through mergeObjects' lazy getters:
46
+ * - $effect.pre calls table.setOptions() with a merged object
47
+ * - The merged object has lazy getters for `data`, `columns`, `state`, etc.
48
+ * - TanStack stores this object but doesn't deep-read all properties immediately
49
+ * - When getRowModel() is called (in $derived), TanStack reads `data` and `columns`
50
+ * through the lazy getters, which read the reactive $state/$derived values
51
+ * - onStateChange fires when TanStack mutates its own state → bumps version
52
+ * - version is read by getRowModel/getHeaderGroups → $derived re-evaluates
53
+ */
54
+ export function createSvelteTable<TData extends RowData>(
55
+ options: TableOptions<TData>
56
+ ) {
57
+ const resolvedOptions: TableOptionsResolved<TData> = mergeObjects(
58
+ {
59
+ state: {},
60
+ onStateChange() {},
61
+ renderFallbackValue: null,
62
+ mergeOptions: (
63
+ defaultOptions: TableOptions<TData>,
64
+ opts: Partial<TableOptions<TData>>
65
+ ) => {
66
+ return mergeObjects(defaultOptions, opts);
67
+ }
68
+ },
69
+ options
70
+ );
71
+
72
+ const table = createTable(resolvedOptions);
73
+ let state = $state<Partial<TableState>>(table.initialState);
74
+ let version = $state(0);
75
+
76
+ function updateOptions(): void {
77
+ table.setOptions((prev) => {
78
+ // mergeObjects creates lazy getters — `state` is NOT read here,
79
+ // only when TanStack accesses the `state` property later.
80
+ return mergeObjects(prev, options, {
81
+ state: mergeObjects(state, options.state || {}),
82
+ onStateChange: (updater: any) => {
83
+ if (updater instanceof Function) state = updater(state);
84
+ else state = mergeObjects(state, updater);
85
+ version += 1;
86
+ options.onStateChange?.(updater);
87
+ }
88
+ });
89
+ });
90
+ }
91
+
92
+ // Initial sync
93
+ updateOptions();
94
+
95
+ // Re-sync when options change. Because mergeObjects uses lazy getters,
96
+ // this effect's tracked dependencies are only the properties that
97
+ // table.setOptions() eagerly reads (which is minimal — mostly just
98
+ // checking if the options object reference changed).
99
+ $effect.pre(() => {
100
+ updateOptions();
101
+ });
102
+
103
+ return {
104
+ getRowModel: () => {
105
+ void version;
106
+ return table.getRowModel();
107
+ },
108
+ getHeaderGroups: () => {
109
+ void version;
110
+ return table.getHeaderGroups();
111
+ },
112
+ getColumn: (id: string) => {
113
+ void version;
114
+ return table.getColumn(id);
115
+ }
116
+ };
117
+ }
@@ -0,0 +1,105 @@
1
+ import {
2
+ Virtualizer,
3
+ elementScroll,
4
+ observeElementOffset,
5
+ observeElementRect,
6
+ type PartialKeys,
7
+ type VirtualizerOptions
8
+ } from "@tanstack/virtual-core";
9
+ import { untrack } from "svelte";
10
+
11
+ export type { VirtualItem } from "@tanstack/virtual-core";
12
+
13
+ /**
14
+ * Creates a reactive TanStack Virtualizer for Svelte 5.
15
+ *
16
+ * Returns a getter function that returns the virtualizer instance.
17
+ * Call the getter inside $derived or template expressions to create
18
+ * reactive dependencies on the virtualizer state.
19
+ */
20
+ export function createSvelteVirtualizer<
21
+ TScrollElement extends Element,
22
+ TItemElement extends Element
23
+ >(
24
+ options: PartialKeys<
25
+ VirtualizerOptions<TScrollElement, TItemElement>,
26
+ "observeElementRect" | "observeElementOffset" | "scrollToFn"
27
+ >
28
+ ): {
29
+ instance: Virtualizer<TScrollElement, TItemElement>;
30
+ virtualItems: () => ReturnType<
31
+ Virtualizer<TScrollElement, TItemElement>["getVirtualItems"]
32
+ >;
33
+ totalSize: () => number;
34
+ } {
35
+ let version = $state(0);
36
+
37
+ const virtualizer = new Virtualizer<TScrollElement, TItemElement>({
38
+ observeElementRect: observeElementRect,
39
+ observeElementOffset: observeElementOffset,
40
+ scrollToFn: elementScroll,
41
+ ...options,
42
+ onChange: (instance, sync) => {
43
+ if (sync) {
44
+ version += 1;
45
+ } else {
46
+ queueMicrotask(() => {
47
+ version += 1;
48
+ });
49
+ }
50
+ options.onChange?.(instance, sync);
51
+ }
52
+ });
53
+
54
+ $effect(() => {
55
+ const cleanup = virtualizer._didMount();
56
+ untrack(() => {
57
+ version += 1;
58
+ });
59
+ return cleanup;
60
+ });
61
+
62
+ let prev_count = 0;
63
+
64
+ $effect(() => {
65
+ const current_count = options.count;
66
+ virtualizer.setOptions({
67
+ observeElementRect: observeElementRect,
68
+ observeElementOffset: observeElementOffset,
69
+ scrollToFn: elementScroll,
70
+ ...options,
71
+ onChange: (instance, sync) => {
72
+ if (sync) {
73
+ version += 1;
74
+ } else {
75
+ queueMicrotask(() => {
76
+ version += 1;
77
+ });
78
+ }
79
+ options.onChange?.(instance, sync);
80
+ }
81
+ });
82
+
83
+ if (prev_count === 0 && current_count > 0) {
84
+ virtualizer.measure();
85
+ }
86
+ prev_count = current_count;
87
+ });
88
+
89
+ $effect.pre(() => {
90
+ void version;
91
+ virtualizer._willUpdate();
92
+ });
93
+
94
+ return {
95
+ instance: virtualizer,
96
+ virtualItems: () => {
97
+ void version;
98
+ return virtualizer.getVirtualItems();
99
+ },
100
+ totalSize: () => {
101
+ void version;
102
+ return virtualizer.getTotalSize();
103
+ }
104
+ };
105
+ }
package/shared/types.ts CHANGED
@@ -29,3 +29,6 @@ export type ElementRefs = Record<
29
29
  >;
30
30
 
31
31
  export type DataBinding = Record<string, TableCell>;
32
+
33
+ export type SortDirection = "asc" | "desc";
34
+ export type FilterDatatype = "string" | "number";