@svgrid/grid 2.1.21 → 2.1.23

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 (219) hide show
  1. package/README.md +9 -2
  2. package/dist/DockNodeView.svelte +306 -0
  3. package/dist/DockNodeView.svelte.d.ts +7 -0
  4. package/dist/DockPopoutHost.svelte +17 -0
  5. package/dist/DockPopoutHost.svelte.d.ts +14 -0
  6. package/dist/GridMenus.svelte +83 -3
  7. package/dist/SvAutoComplete.svelte +5 -3
  8. package/dist/SvCarousel.svelte +20 -42
  9. package/dist/SvColorInput.svelte +4 -3
  10. package/dist/SvColorInput.svelte.d.ts +1 -1
  11. package/dist/SvComboBox.svelte +7 -3
  12. package/dist/SvCommand.svelte +28 -85
  13. package/dist/SvCountryInput.svelte +1 -1
  14. package/dist/SvDateRangeInput.svelte +1 -1
  15. package/dist/SvDateTimePicker.svelte +9 -3
  16. package/dist/SvDateTimePicker.svelte.d.ts +3 -0
  17. package/dist/SvDockLayout.svelte +244 -0
  18. package/dist/SvDockLayout.svelte.d.ts +77 -0
  19. package/dist/SvDockManager.svelte +922 -0
  20. package/dist/SvDockManager.svelte.d.ts +106 -0
  21. package/dist/SvDrawer.svelte +69 -47
  22. package/dist/SvDrawer.svelte.d.ts +4 -0
  23. package/dist/SvDropDownList.svelte +85 -13
  24. package/dist/SvDropDownList.svelte.d.ts +3 -0
  25. package/dist/SvField.svelte +21 -4
  26. package/dist/SvField.svelte.d.ts +1 -0
  27. package/dist/SvForm.svelte +201 -91
  28. package/dist/SvForm.svelte.d.ts +15 -22
  29. package/dist/SvGrid.controller.svelte.d.ts +32 -1
  30. package/dist/SvGrid.controller.svelte.js +156 -27
  31. package/dist/SvGrid.css +193 -5
  32. package/dist/SvGrid.svelte +357 -12
  33. package/dist/SvGrid.types.d.ts +452 -1
  34. package/dist/SvGridDropdown.svelte +8 -1
  35. package/dist/SvGridSelect.svelte +1 -0
  36. package/dist/SvListBox.svelte +36 -14
  37. package/dist/SvMaskedInput.svelte +4 -3
  38. package/dist/SvMaskedInput.svelte.d.ts +1 -1
  39. package/dist/SvMenu.svelte +1 -1
  40. package/dist/SvMenuList.svelte +23 -71
  41. package/dist/SvMenuList.svelte.d.ts +2 -13
  42. package/dist/SvModal.svelte +12 -29
  43. package/dist/SvMultiSelect.svelte +1 -0
  44. package/dist/SvNumberInput.svelte +4 -3
  45. package/dist/SvNumberInput.svelte.d.ts +1 -1
  46. package/dist/SvPagination.svelte +18 -18
  47. package/dist/SvPasswordInput.svelte +3 -2
  48. package/dist/SvPasswordInput.svelte.d.ts +1 -1
  49. package/dist/SvPhoneInput.svelte +3 -2
  50. package/dist/SvPhoneInput.svelte.d.ts +1 -1
  51. package/dist/SvPopover.svelte +1 -1
  52. package/dist/SvStepper.svelte +6 -9
  53. package/dist/SvTagsInput.svelte +3 -2
  54. package/dist/SvTagsInput.svelte.d.ts +1 -1
  55. package/dist/SvTooltip.svelte +12 -23
  56. package/dist/SvTour.svelte +32 -4
  57. package/dist/SvTreeSelect.svelte +1 -0
  58. package/dist/build-api.js +76 -54
  59. package/dist/cdn/svgrid.js +10907 -6444
  60. package/dist/cdn/svgrid.svelte-external.js +9622 -5475
  61. package/dist/columns.js +6 -0
  62. package/dist/conditional-formatting.d.ts +67 -3
  63. package/dist/conditional-formatting.js +124 -17
  64. package/dist/createCarousel.svelte.d.ts +72 -0
  65. package/dist/createCarousel.svelte.js +80 -0
  66. package/dist/createCommand.svelte.d.ts +62 -0
  67. package/dist/createCommand.svelte.js +109 -0
  68. package/dist/createForm.svelte.d.ts +62 -0
  69. package/dist/createForm.svelte.js +374 -0
  70. package/dist/createMenu.svelte.d.ts +88 -0
  71. package/dist/createMenu.svelte.js +153 -0
  72. package/dist/createOverlay.svelte.d.ts +38 -0
  73. package/dist/createOverlay.svelte.js +68 -0
  74. package/dist/createPagination.d.ts +74 -0
  75. package/dist/createPagination.js +71 -0
  76. package/dist/createPopoverSelect.svelte.js +6 -12
  77. package/dist/createStepper.d.ts +44 -0
  78. package/dist/createStepper.js +26 -0
  79. package/dist/createTooltip.svelte.d.ts +32 -0
  80. package/dist/createTooltip.svelte.js +61 -0
  81. package/dist/datetime/timezone.d.ts +36 -0
  82. package/dist/datetime/timezone.js +110 -0
  83. package/dist/dock-context.d.ts +52 -0
  84. package/dist/dock-context.js +1 -0
  85. package/dist/dock-manager-model.d.ts +136 -0
  86. package/dist/dock-manager-model.js +514 -0
  87. package/dist/dock-model.d.ts +105 -0
  88. package/dist/dock-model.js +288 -0
  89. package/dist/dock-popout.d.ts +30 -0
  90. package/dist/dock-popout.js +81 -0
  91. package/dist/editor-contract.d.ts +2 -0
  92. package/dist/filter-operators.d.ts +6 -0
  93. package/dist/filter-operators.js +35 -2
  94. package/dist/filtering/excel-filters.d.ts +18 -1
  95. package/dist/filtering/excel-filters.js +56 -0
  96. package/dist/form-field.d.ts +73 -0
  97. package/dist/form-field.js +15 -0
  98. package/dist/index.d.ts +25 -5
  99. package/dist/index.js +40 -4
  100. package/dist/list-nav.d.ts +18 -0
  101. package/dist/list-nav.js +30 -0
  102. package/dist/list-option.d.ts +2 -0
  103. package/dist/menu-item.d.ts +20 -0
  104. package/dist/menu-item.js +1 -0
  105. package/dist/menus.d.ts +5 -0
  106. package/dist/menus.js +68 -16
  107. package/dist/panel-resize.d.ts +25 -0
  108. package/dist/panel-resize.js +40 -0
  109. package/dist/popover.d.ts +29 -3
  110. package/dist/popover.js +22 -6
  111. package/dist/recurrence.d.ts +33 -7
  112. package/dist/recurrence.js +151 -13
  113. package/dist/row-drag.d.ts +8 -0
  114. package/dist/row-drag.js +11 -0
  115. package/dist/scheduler-ical.d.ts +30 -0
  116. package/dist/scheduler-ical.js +180 -0
  117. package/dist/scheduler-model.d.ts +269 -0
  118. package/dist/scheduler-model.js +602 -0
  119. package/dist/scheduler-view.svelte.d.ts +22 -0
  120. package/dist/scheduler-view.svelte.js +13 -0
  121. package/dist/spreadsheet.js +20 -12
  122. package/dist/ui-app.types.d.ts +3 -0
  123. package/package.json +5 -1
  124. package/src/DockNodeView.svelte +306 -0
  125. package/src/DockPopoutHost.svelte +17 -0
  126. package/src/GridMenus.svelte +83 -3
  127. package/src/SvAutoComplete.svelte +5 -3
  128. package/src/SvCarousel.svelte +20 -42
  129. package/src/SvCarousel.test.ts +68 -0
  130. package/src/SvColorInput.svelte +4 -3
  131. package/src/SvComboBox.svelte +7 -3
  132. package/src/SvCommand.svelte +28 -85
  133. package/src/SvCommand.test.ts +78 -0
  134. package/src/SvCountryInput.svelte +1 -1
  135. package/src/SvDateRangeInput.svelte +1 -1
  136. package/src/SvDateTimePicker.svelte +9 -3
  137. package/src/SvDockLayout.svelte +244 -0
  138. package/src/SvDockManager.svelte +922 -0
  139. package/src/SvDrawer.svelte +69 -47
  140. package/src/SvDropDownList.svelte +85 -13
  141. package/src/SvField.svelte +21 -4
  142. package/src/SvForm.svelte +201 -91
  143. package/src/SvForm.test.ts +299 -0
  144. package/src/SvGrid.controller.svelte.ts +152 -28
  145. package/src/SvGrid.css +193 -5
  146. package/src/SvGrid.svelte +357 -12
  147. package/src/SvGrid.types.ts +463 -1
  148. package/src/SvGridDropdown.svelte +8 -1
  149. package/src/SvGridSelect.svelte +1 -0
  150. package/src/SvListBox.svelte +36 -14
  151. package/src/SvMaskedInput.svelte +4 -3
  152. package/src/SvMenu.svelte +1 -1
  153. package/src/SvMenu.test.ts +24 -0
  154. package/src/SvMenuList.svelte +23 -71
  155. package/src/SvModal.svelte +12 -29
  156. package/src/SvModal.test.ts +82 -0
  157. package/src/SvMultiSelect.svelte +1 -0
  158. package/src/SvNumberInput.svelte +4 -3
  159. package/src/SvPagination.svelte +18 -18
  160. package/src/SvPasswordInput.svelte +3 -2
  161. package/src/SvPhoneInput.svelte +3 -2
  162. package/src/SvPopover.svelte +1 -1
  163. package/src/SvStepper.svelte +6 -9
  164. package/src/SvTagsInput.svelte +3 -2
  165. package/src/SvTooltip.svelte +12 -23
  166. package/src/SvTooltip.test.ts +73 -0
  167. package/src/SvTour.svelte +32 -4
  168. package/src/SvTreeSelect.svelte +1 -0
  169. package/src/build-api.ts +74 -51
  170. package/src/columns.ts +5 -0
  171. package/src/conditional-formatting.test.ts +104 -0
  172. package/src/conditional-formatting.ts +176 -19
  173. package/src/createCarousel.svelte.ts +104 -0
  174. package/src/createCommand.svelte.ts +125 -0
  175. package/src/createForm.svelte.ts +333 -0
  176. package/src/createMenu.svelte.ts +202 -0
  177. package/src/createOverlay.svelte.ts +101 -0
  178. package/src/createPagination.test.ts +65 -0
  179. package/src/createPagination.ts +84 -0
  180. package/src/createPopoverSelect.svelte.ts +5 -9
  181. package/src/createStepper.test.ts +62 -0
  182. package/src/createStepper.ts +61 -0
  183. package/src/createTooltip.svelte.ts +86 -0
  184. package/src/datetime/timezone.test.ts +69 -0
  185. package/src/datetime/timezone.ts +134 -0
  186. package/src/dock-context.ts +47 -0
  187. package/src/dock-manager-model.test.ts +275 -0
  188. package/src/dock-manager-model.ts +593 -0
  189. package/src/dock-model.test.ts +163 -0
  190. package/src/dock-model.ts +368 -0
  191. package/src/dock-popout.ts +94 -0
  192. package/src/editor-contract.ts +2 -0
  193. package/src/filter-operators.test.ts +59 -0
  194. package/src/filter-operators.ts +35 -2
  195. package/src/filtering/excel-filters.test.ts +86 -1
  196. package/src/filtering/excel-filters.ts +65 -0
  197. package/src/form-field.ts +87 -0
  198. package/src/index.ts +158 -4
  199. package/src/list-nav.test.ts +49 -0
  200. package/src/list-nav.ts +29 -0
  201. package/src/list-option.ts +2 -0
  202. package/src/menu-item.ts +21 -0
  203. package/src/menus.test.ts +48 -0
  204. package/src/menus.ts +73 -16
  205. package/src/panel-resize.ts +52 -0
  206. package/src/popover.bounds.test.ts +93 -0
  207. package/src/popover.ts +51 -7
  208. package/src/recurrence.test.ts +88 -1
  209. package/src/recurrence.ts +166 -15
  210. package/src/row-drag.ts +10 -0
  211. package/src/scheduler-ical.test.ts +68 -0
  212. package/src/scheduler-ical.ts +180 -0
  213. package/src/scheduler-model.test.ts +546 -0
  214. package/src/scheduler-model.ts +853 -0
  215. package/src/scheduler-view.svelte.ts +33 -0
  216. package/src/spreadsheet.ts +20 -12
  217. package/src/svgrid.effect-driven-filter.svelte.test.ts +116 -0
  218. package/src/ui-app.types.ts +3 -0
  219. package/src/virtual.test.ts +6 -4
@@ -34,6 +34,7 @@ import "./sv-grid-scrollbar";
34
34
  import {
35
35
  computeColumnStat,
36
36
  formatsNeedingStats,
37
+ formatNeedsStats,
37
38
  type ColumnStat,
38
39
  } from "./conditional-formatting";
39
40
  import SvGridDropdown from "./SvGridDropdown.svelte";
@@ -143,9 +144,12 @@ const MAX_DOM_SCROLL_HEIGHT_FALLBACK = 8_000_000;
143
144
  * matters because mobile WebKit/Blink can report a generous `offsetHeight` yet
144
145
  * expose a smaller scrollable range - trusting the layout height alone is what
145
146
  * stranded the last rows on phones. Using a real scroll container also folds in
146
- * DPR clamping for free. Cached for the page lifetime; constant per browser.
147
+ * DPR clamping for free. Cached, but re-probed when the device pixel ratio
148
+ * changes (browser zoom) since the physical-pixel cap moves with it (#85).
147
149
  */
148
150
  let detectedMaxDomHeight: number | null = null;
151
+ let detectedAtDpr = 0;
152
+ const currentDpr = () => (typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1);
149
153
  /**
150
154
  * Seed the `hiddenColumns` map from any column def marked `visible: false`.
151
155
  * Walks groups so a hidden group hides all of its leaf columns. Keyed by the
@@ -190,7 +194,7 @@ function getMaxDomScrollHeight(): number {
190
194
  return forced;
191
195
  }
192
196
  }
193
- if (detectedMaxDomHeight != null) return detectedMaxDomHeight;
197
+ if (detectedMaxDomHeight != null && detectedAtDpr === currentDpr()) return detectedMaxDomHeight;
194
198
  if (typeof document === "undefined" || !document.body) {
195
199
  return MAX_DOM_SCROLL_HEIGHT_FALLBACK;
196
200
  }
@@ -218,6 +222,7 @@ function getMaxDomScrollHeight(): number {
218
222
  } catch {
219
223
  detectedMaxDomHeight = MAX_DOM_SCROLL_HEIGHT_FALLBACK;
220
224
  }
225
+ detectedAtDpr = currentDpr();
221
226
  return detectedMaxDomHeight;
222
227
  }
223
228
 
@@ -429,6 +434,12 @@ export function createSvGridController<
429
434
  let filterMenuPos = $state<MenuPosition>({ x: 0, y: 0 });
430
435
  let operatorMenuFor = $state<string | null>(null);
431
436
  let operatorMenuPos = $state<MenuPosition>({ x: 0, y: 0 });
437
+ // `in` / `notIn` value-suggestions dropdown: the column whose chip input is
438
+ // active, its anchor position, and the current typed query (for filtering
439
+ // the distinct-value list as the user types).
440
+ let inSuggestFor = $state<string | null>(null);
441
+ let inSuggestPos = $state<MenuPosition>({ x: 0, y: 0 });
442
+ let inSuggestQuery = $state("");
432
443
  let chooseColumnsPos = $state<MenuPosition | null>(null);
433
444
  let contextMenuFor = $state<ContextMenuTarget<TData> | null>(null);
434
445
  let contextMenuPos = $state<MenuPosition>({ x: 0, y: 0 });
@@ -563,6 +574,10 @@ export function createSvGridController<
563
574
  let hiddenColumns = $state<Record<string, boolean>>(
564
575
  initialHiddenColumns(props.columns),
565
576
  );
577
+ // The columns declared `visible: false` in the ColumnDefs. Kept separately so
578
+ // applying a saved view (setState) can re-seed from these and never un-hide a
579
+ // column the app declared hidden (#54).
580
+ const declaredHiddenColumns = $derived(initialHiddenColumns(props.columns));
566
581
 
567
582
  // Collapsible column groups (columnGroupShow). Meta is derived from the tree;
568
583
  // `collapsedColumnGroups` is the live set of collapsed group ids, seeded once
@@ -940,6 +955,10 @@ export function createSvGridController<
940
955
  const left: Record<string, number> = {};
941
956
  let leftAcc = rowNumberWidth + selectionWidth;
942
957
  for (const id of effectivePinning.left) {
958
+ // Hidden pinned columns don't render, so they must contribute neither an
959
+ // offset entry nor width - otherwise the next visible pinned column is
960
+ // pushed out by a phantom gap (#75).
961
+ if (hiddenColumns[id] || hiddenByGroupCollapse[id]) continue;
943
962
  left[id] = leftAcc;
944
963
  leftAcc += getColumnWidth(id);
945
964
  }
@@ -948,6 +967,7 @@ export function createSvGridController<
948
967
  for (let i = effectivePinning.right.length - 1; i >= 0; i -= 1) {
949
968
  const id = effectivePinning.right[i];
950
969
  if (!id) continue;
970
+ if (hiddenColumns[id] || hiddenByGroupCollapse[id]) continue;
951
971
  right[id] = rightAcc;
952
972
  rightAcc += getColumnWidth(id);
953
973
  }
@@ -990,26 +1010,35 @@ export function createSvGridController<
990
1010
  const map = new Map<string, ColumnStat>();
991
1011
  const formats = props.conditionalFormats;
992
1012
  if (!formats?.length || !formatsNeedingStats(formats)) return map;
1013
+ // `visible` (default): the currently displayed rows (filtered + paged),
1014
+ // so the heat map reflects what the user is looking at. `all`: the full
1015
+ // unfiltered dataset, for a stable scale independent of filter/paging.
1016
+ const scanAll = props.conditionalStatScope === "all";
993
1017
  for (const column of allColumns) {
994
1018
  const needs = formats.some(
995
- (f) =>
996
- (f.type === "colorScale" || f.type === "dataBar") &&
997
- (!f.columns || f.columns.includes(column.id)),
1019
+ (f) => formatNeedsStats(f) && (!f.columns || f.columns.includes(column.id)),
998
1020
  );
999
1021
  if (!needs) continue;
1000
1022
  const def = column.columnDef;
1001
1023
  const fieldFn = def.fieldFn;
1002
1024
  const field = def.field;
1003
1025
  const stat = computeColumnStat(
1004
- (function* () {
1005
- for (const row of allRows) {
1006
- yield fieldFn
1007
- ? fieldFn(row.original)
1008
- : field
1009
- ? (row.original as Record<string, unknown>)[field]
1010
- : row.getCellValueByColumnId(column.id);
1011
- }
1012
- })(),
1026
+ scanAll
1027
+ ? (function* () {
1028
+ // Raw data objects (unfiltered); field/fieldFn only - no Row wrapper.
1029
+ for (const obj of internalData as ReadonlyArray<Record<string, unknown>>) {
1030
+ yield fieldFn ? fieldFn(obj as any) : field ? obj[field] : undefined;
1031
+ }
1032
+ })()
1033
+ : (function* () {
1034
+ for (const row of allRows) {
1035
+ yield fieldFn
1036
+ ? fieldFn(row.original)
1037
+ : field
1038
+ ? (row.original as Record<string, unknown>)[field]
1039
+ : row.getCellValueByColumnId(column.id);
1040
+ }
1041
+ })(),
1013
1042
  );
1014
1043
  if (stat) map.set(column.id, stat);
1015
1044
  }
@@ -1076,7 +1105,7 @@ export function createSvGridController<
1076
1105
 
1077
1106
  // A single condition is "active" if it has the value(s) it needs.
1078
1107
  const condActive = (op: FilterOperator, value: string, valueTo?: string): boolean => {
1079
- if (op === "isBlank") return true;
1108
+ if (op === "isBlank" || op === "isNotBlank") return true;
1080
1109
  if (op === "between") return value.trim().length > 0 && (valueTo ?? "").trim().length > 0;
1081
1110
  return value.trim().length > 0;
1082
1111
  };
@@ -1198,10 +1227,13 @@ export function createSvGridController<
1198
1227
  lastSelectionSerialized = serialized;
1199
1228
  const callback = props.onRowSelectionChange;
1200
1229
  if (!callback) return;
1201
- const data = internalData;
1230
+ // Resolve selected rows by row id (not numeric index) so this is correct
1231
+ // with a custom getRowId and after sorting (#45). Iterate the full filtered
1232
+ // model - not the paginated slice - so selections on other pages are kept.
1202
1233
  const selectedRows: TData[] = [];
1203
- for (let i = 0; i < data.length; i++) {
1204
- if (rowSelectionState[String(i)]) selectedRows.push(data[i] as TData);
1234
+ for (const row of allRowsBeforePagination) {
1235
+ if (isGroupRow(row)) continue;
1236
+ if (rowSelectionState[row.id]) selectedRows.push(row.original as TData);
1205
1237
  }
1206
1238
  callback(rowSelectionState, selectedRows);
1207
1239
  });
@@ -1683,7 +1715,7 @@ export function createSvGridController<
1683
1715
  if (!props.onFiltersChange) return;
1684
1716
  const menuEntries = Object.entries(filterMenuValues)
1685
1717
  .filter(([, f]) => {
1686
- if (f.operator === "isBlank") return true;
1718
+ if (f.operator === "isBlank" || f.operator === "isNotBlank") return true;
1687
1719
  if (f.operator === "between") {
1688
1720
  return f.value.trim().length > 0 && (f.valueTo ?? "").trim().length > 0;
1689
1721
  }
@@ -1912,6 +1944,43 @@ export function createSvGridController<
1912
1944
  Math.max(totalColumnWidth - columnWindowEnd, 0),
1913
1945
  );
1914
1946
 
1947
+ /**
1948
+ * `groupHeaderRows` clipped to the rendered column window. The multi-level
1949
+ * group-header band must virtualize in lock-step with the leaf-header row
1950
+ * and the body: otherwise a group cell spanning off-window leaves renders a
1951
+ * header over empty space (its cells were dropped by body virtualization),
1952
+ * so wide grouped/pivot grids show group headers with no cells beneath.
1953
+ *
1954
+ * We reuse each cell's `firstLeafIndex` + `colSpan` (leaf-index range) to
1955
+ * clip against the rendered leaf window, dropping fully-off-window cells and
1956
+ * shrinking partially-visible ones to their visible leaves. The template
1957
+ * frames the result with the SAME left/right spacers the leaf row uses, so
1958
+ * everything stays pixel-aligned. When virtualization is off this is a
1959
+ * pass-through.
1960
+ */
1961
+ const groupHeaderRowsWindowed = $derived.by(() => {
1962
+ const base = groupHeaderRows;
1963
+ if (!columnVirtualizationEnabled || base.length === 0) return base;
1964
+ const items = renderedColumnItems;
1965
+ if (items.length === 0) return base;
1966
+ const winFirst = items[0]!.index;
1967
+ const winLast = items[items.length - 1]!.index; // inclusive
1968
+ return base.map((row) => {
1969
+ const cells: GroupHeaderCell[] = [];
1970
+ for (const cell of row.cells) {
1971
+ const start = cell.firstLeafIndex;
1972
+ const end = start + cell.colSpan; // exclusive
1973
+ const from = Math.max(start, winFirst);
1974
+ const to = Math.min(end, winLast + 1);
1975
+ if (to <= from) continue; // cell is entirely outside the window
1976
+ let widthPx = 0;
1977
+ for (let i = from; i < to; i += 1) widthPx += getColumnWidth(allColumns[i]!.id);
1978
+ cells.push({ ...cell, colSpan: to - from, widthPx, firstLeafIndex: from });
1979
+ }
1980
+ return { ...row, cells };
1981
+ });
1982
+ });
1983
+
1915
1984
  const activeCell = $derived.by(() => {
1916
1985
  gridStateVersion;
1917
1986
  return (
@@ -2212,7 +2281,9 @@ export function createSvGridController<
2212
2281
  viewportVersion;
2213
2282
  // Narrow responsive mode pans instead of scaling, so skip fit scaling.
2214
2283
  if (!props.fitColumns || isNarrowResponsive) return null;
2215
- const cols = grid.getAllColumns().filter((c) => !hiddenColumns[c.id]);
2284
+ // Exclude columns hidden by a collapsed group too (#57) - otherwise their
2285
+ // widths stay in the fit distribution and leave a blank gap at the edge.
2286
+ const cols = grid.getAllColumns().filter((c) => !hiddenColumns[c.id] && !hiddenByGroupCollapse[c.id]);
2216
2287
  if (!cols.length) return null;
2217
2288
  const rowNumberWidth = showRowNumbersEffective ? rowNumberColumnWidth : 0;
2218
2289
  const selectionWidth = showRowSelectionEffective ? selectionColumnWidth : 0;
@@ -2425,15 +2496,19 @@ export function createSvGridController<
2425
2496
  };
2426
2497
  });
2427
2498
 
2428
- const columnMenuFacetValues = $derived.by(() => {
2429
- // The funnel popover drives via `filterMenuFor`; the column menu's Filter
2430
- // tab drives via `columnMenuFor`. Support whichever is open.
2431
- const columnId = filterMenuFor ?? columnMenuFor;
2432
- if (!columnId) return [] as Array<string>;
2499
+ /**
2500
+ * Distinct values for a column, for the set-filter checklist and the
2501
+ * `in` / `notIn` suggestions dropdown. Server-provided values win; numeric /
2502
+ * date columns with many values collapse into range buckets; otherwise the
2503
+ * distinct raw values are collected and sorted naturally. Callable for any
2504
+ * column id (not just the open menu) so the filter row can drive it too.
2505
+ */
2506
+ function facetValuesForColumn(columnId: string): Array<string> {
2507
+ if (!columnId) return [];
2433
2508
  // Server-provided distinct values win (fetched + cached above).
2434
2509
  if (props.serverFilterValues) return serverFacetValues[columnId] ?? [];
2435
2510
  const column = allColumns.find((entry) => entry.id === columnId);
2436
- if (!column) return [] as Array<string>;
2511
+ if (!column) return [];
2437
2512
  // Range-bucketed facets for numeric / date columns with many values.
2438
2513
  const buckets = facetBucketsByColumn.get(columnId);
2439
2514
  if (buckets) return buckets.map((b) => b.label);
@@ -2445,6 +2520,14 @@ export function createSvGridController<
2445
2520
  return Array.from(seen).sort((a, b) =>
2446
2521
  a.localeCompare(b, undefined, { numeric: true }),
2447
2522
  );
2523
+ }
2524
+
2525
+ const columnMenuFacetValues = $derived.by(() => {
2526
+ // The funnel popover drives via `filterMenuFor`; the column menu's Filter
2527
+ // tab drives via `columnMenuFor`. Support whichever is open.
2528
+ const columnId = filterMenuFor ?? columnMenuFor;
2529
+ if (!columnId) return [] as Array<string>;
2530
+ return facetValuesForColumn(columnId);
2448
2531
  });
2449
2532
 
2450
2533
  const columnMenuVisibleFacets = $derived.by(() => {
@@ -2455,6 +2538,20 @@ export function createSvGridController<
2455
2538
  );
2456
2539
  });
2457
2540
 
2541
+ // Distinct values offered in the `in` / `notIn` suggestions dropdown for the
2542
+ // active chip input, narrowed by whatever the user has typed. Capped so a
2543
+ // high-cardinality column can't render thousands of rows into the popover.
2544
+ const IN_SUGGEST_LIMIT = 200;
2545
+ const inSuggestValues = $derived.by(() => {
2546
+ if (!inSuggestFor) return [] as Array<string>;
2547
+ const query = inSuggestQuery.trim().toLowerCase();
2548
+ const all = facetValuesForColumn(inSuggestFor);
2549
+ const matched = query
2550
+ ? all.filter((value) => value.toLowerCase().includes(query))
2551
+ : all;
2552
+ return matched.slice(0, IN_SUGGEST_LIMIT);
2553
+ });
2554
+
2458
2555
 
2459
2556
 
2460
2557
 
@@ -2591,6 +2688,14 @@ export function createSvGridController<
2591
2688
  set operatorMenuFor(v) { operatorMenuFor = v as never; },
2592
2689
  get operatorMenuPos() { return operatorMenuPos; },
2593
2690
  set operatorMenuPos(v) { operatorMenuPos = v as never; },
2691
+ get inSuggestFor() { return inSuggestFor; },
2692
+ set inSuggestFor(v) { inSuggestFor = v as never; },
2693
+ get inSuggestPos() { return inSuggestPos; },
2694
+ set inSuggestPos(v) { inSuggestPos = v as never; },
2695
+ get inSuggestQuery() { return inSuggestQuery; },
2696
+ set inSuggestQuery(v) { inSuggestQuery = v as never; },
2697
+ get inSuggestValues() { return inSuggestValues; },
2698
+ get facetValuesForColumn() { return facetValuesForColumn; },
2594
2699
  get chooseColumnsPos() { return chooseColumnsPos; },
2595
2700
  set chooseColumnsPos(v) { chooseColumnsPos = v as never; },
2596
2701
  get contextMenuFor() { return contextMenuFor; },
@@ -2623,6 +2728,7 @@ export function createSvGridController<
2623
2728
  set internalColumns(v) { internalColumns = v as never; },
2624
2729
  get hiddenColumns() { return hiddenColumns; },
2625
2730
  set hiddenColumns(v) { hiddenColumns = v as never; },
2731
+ get declaredHiddenColumns() { return declaredHiddenColumns; },
2626
2732
  get toggleColumnGroup() { return toggleColumnGroup; },
2627
2733
  get isColumnGroupCollapsed() { return isColumnGroupCollapsed; },
2628
2734
  get externalSortEnabled() { return externalSortEnabled; },
@@ -2637,6 +2743,7 @@ export function createSvGridController<
2637
2743
  get allColumns() { return allColumns; },
2638
2744
  get headerGroups() { return headerGroups; },
2639
2745
  get groupHeaderRows() { return groupHeaderRows; },
2746
+ get groupHeaderRowsWindowed() { return groupHeaderRowsWindowed; },
2640
2747
  get pinnedOffsets() { return pinnedOffsets; },
2641
2748
  get cellPinStyle() { return cellPinStyle; },
2642
2749
  get isColumnPinned() { return isColumnPinned; },
@@ -2981,6 +3088,11 @@ export function createSvGridController<
2981
3088
  get openColumnMenu() { return openColumnMenu; },
2982
3089
  get openFilterMenu() { return openFilterMenu; },
2983
3090
  get openOperatorMenu() { return openOperatorMenu; },
3091
+ get openInSuggest() { return openInSuggest; },
3092
+ get closeInSuggest() { return closeInSuggest; },
3093
+ get addFilterToken() { return addFilterToken; },
3094
+ get removeFilterToken() { return removeFilterToken; },
3095
+ get toggleFilterToken() { return toggleFilterToken; },
2984
3096
  get sortColumnFromMenu() { return sortColumnFromMenu; },
2985
3097
  get clearColumnSort() { return clearColumnSort; },
2986
3098
  get groupByColumnFromMenu() { return groupByColumnFromMenu; },
@@ -3007,12 +3119,12 @@ export function createSvGridController<
3007
3119
  const { showTooltipFor, hideTooltip, flushScheduledScrollSync, scheduleScrollSync, onBodyScroll } = createScrollSync<TFeatures, TData>(ctx);
3008
3120
  const { onGridKeyDown, onWindowKeydown, onHeaderSortClick } = createKeyboard<TFeatures, TData>(ctx);
3009
3121
  const { computeSummaries, hasRenderedColumn } = createSummaries<TFeatures, TData>(ctx);
3010
- const { updateFilterRow, updateFilterOperator, updateFilterMenuValue, updateFilterMenuValueTo, toggleCheckboxWithKeyboard, isColumnFiltered, closeMenus, openChooseColumns, openColumnMenu, openFilterMenu, openOperatorMenu, sortColumnFromMenu, clearColumnSort, groupByColumnFromMenu, clearGroupingFromMenu, isFacetChecked, toggleFacetValue, isAllFacetsChecked, toggleAllFacets, clearColumnFilter, changePage, goToPage, setPageSize, openContextMenu, closeContextMenu, contextMenuItems, saveComment, removeComment, closeCommentEditor } = createMenus<TFeatures, TData>(ctx);
3122
+ const { updateFilterRow, updateFilterOperator, updateFilterMenuValue, updateFilterMenuValueTo, addFilterToken, removeFilterToken, toggleFilterToken, toggleCheckboxWithKeyboard, isColumnFiltered, closeMenus, openInSuggest, closeInSuggest, openChooseColumns, openColumnMenu, openFilterMenu, openOperatorMenu, sortColumnFromMenu, clearColumnSort, groupByColumnFromMenu, clearGroupingFromMenu, isFacetChecked, toggleFacetValue, isAllFacetsChecked, toggleAllFacets, clearColumnFilter, changePage, goToPage, setPageSize, openContextMenu, closeContextMenu, contextMenuItems, saveComment, removeComment, closeCommentEditor } = createMenus<TFeatures, TData>(ctx);
3011
3123
  const { cellConditionalFormat, computeRowClass, computeCellClass, computeCellTooltip, computeCellValidity, computeCellNote, getColumnEditorOptions, formatListCellValue, formatCellValue, formatPinnedValue, computePinnedCellClass } = createCellRender<TFeatures, TData>(ctx);
3012
3124
  const { isCellEditable, isCellEditableAt, getRowColumnValue, getCellDisplayValue, startEditingWithChar, startEditing, stopEditing, startFullRowEdit, setFullRowDraft, commitFullRowEdit, cancelFullRowEdit, saveEditingCell, applyHistoryStep, updateEditingCellValue, onEditorKeyDown, focusOnMount, onCellDoubleClick, pasteFromClipboard, onGridPaste } = createEditing<TFeatures, TData>(ctx);
3013
3125
  const { isRowSelected, toggleRowSelectionById, toggleSelectAllRows, setActiveCell, scrollActiveCellIntoView, setSelection, extendSelection, isCellInSelectedRange, getCellRangeEdges, getSelectionRects, isInFillPreview, fillMarqueeEdges, findColumnById, onCellPointerDown, onCellPointerEnter, endDragSelection, onWindowPointerMove, onCellClick, emitCellDoubleClick } = createSelection<TFeatures, TData>(ctx);
3014
3126
  const { cellPinStyle, isColumnPinned, getCurrentColumnOrder, emitColumnOrder, setColumnOrderInternal, applyColumnDrop, onColumnHeaderDragStart, onColumnHeaderDragOver, onColumnHeaderDragLeave, onColumnHeaderDrop, onColumnHeaderDragEnd, pinColumnLeft, pinColumnRight, unpinColumn, toggleColumnVisibleInPanel, moveColumnInPanel, toggleGroupInPanel, getColumnBaseWidth, getColumnWidth, startColumnResize, onColumnResizeMove, endColumnResize, measureText, autosizeColumn, autosizeAllColumns, resetColumns } = createColumns<TFeatures, TData>(ctx);
3015
- const { onRowDragStart, onRowDragOver, onRowDragLeave, onRowDrop, onRowsContainerDragOver, onRowsContainerDrop, onRowDragEnd } = createRowDrag<TFeatures, TData>(ctx);
3127
+ const { onRowDragStart, onRowDragOver, onRowDragLeave, onRowDrop, onRowsContainerDragOver, onRowsContainerDrop, onRowDragEnd, destroyRowDrag } = createRowDrag<TFeatures, TData>(ctx);
3016
3128
  const { register: registerAlignedGrid, broadcastScroll: broadcastAlignedScroll, broadcastWidths: broadcastAlignedWidths } = createAlignedGrids<TFeatures, TData>(ctx);
3017
3129
  const { buildApi } = createGridApi<TFeatures, TData>(ctx);
3018
3130
  const { readCellRaw, writeCellRaw, applyFillPattern, clearSelectedCellValues, startFillDrag, onFillPointerMove, onFillPointerUp, toggleBooleanCell, copySelectionToClipboard, clearSelectedCells, cutSelectionToClipboard } = createClipboard(ctx);
@@ -3030,5 +3142,17 @@ export function createSvGridController<
3030
3142
  broadcastAlignedWidths();
3031
3143
  });
3032
3144
 
3145
+ // Unmount teardown. Reads no reactive state, so the cleanup runs once on
3146
+ // destroy - tearing down an in-flight column resize (its document pointer
3147
+ // listeners, #58), a pending tooltip timer (#64), and any managed row drag
3148
+ // this grid still owns (#68). Each of these is a no-op when idle.
3149
+ $effect(() => {
3150
+ return () => {
3151
+ endColumnResize();
3152
+ hideTooltip();
3153
+ destroyRowDrag();
3154
+ };
3155
+ });
3156
+
3033
3157
  return ctx;
3034
3158
  }
package/src/SvGrid.css CHANGED
@@ -485,13 +485,28 @@
485
485
  justify-content: flex-end;
486
486
  }
487
487
  /* The custom vertical scrollbar overlays the right 16px of the viewport
488
- (absolute, z-index 40). Nudge the last column's right-aligned content in by
489
- that width when the scrollbar is visible, so numbers/values aren't hidden
490
- underneath it. Left/center content already clears the right edge. */
491
- .sv-grid-has-vscroll .sv-grid-table tr > :last-child.sv-grid-cell[data-align="right"],
492
- .sv-grid-has-vscroll .sv-grid-table tr > :last-child.sv-grid-column[data-align="right"] {
488
+ (absolute, z-index 40) and does NOT shrink the scroll container's
489
+ clientWidth. Two cases keep the last column clear of it:
490
+
491
+ 1. No horizontal overflow (content fits, nothing to scroll): nudge the last
492
+ column's right-aligned content in by the scrollbar width so numbers
493
+ aren't hidden underneath it. Left/center content already clears it.
494
+ 2. Horizontal overflow: there's nothing to nudge (you scroll to the end and
495
+ the last 16px sit under the bar with no room left). Instead reserve a
496
+ trailing gutter on the scroll container so you can scroll the last column
497
+ fully out from under the overlay. See `.sv-grid-has-vscroll.sv-grid-has-hscroll`. */
498
+ .sv-grid-has-vscroll:not(.sv-grid-has-hscroll) .sv-grid-table tr > :last-child.sv-grid-cell[data-align="right"],
499
+ .sv-grid-has-vscroll:not(.sv-grid-has-hscroll) .sv-grid-table tr > :last-child.sv-grid-column[data-align="right"] {
493
500
  padding-right: calc(var(--sg-cell-px, 7px) + 16px);
494
501
  }
502
+ /* Case 2: reserve the overlay scrollbar's width as end-of-scroll space so the
503
+ final column can be scrolled clear of the vertical scrollbar. Modern browsers
504
+ include a scroll container's inline-end padding in its scrollable overflow;
505
+ the custom scrollbars are siblings of this container, so their layout is
506
+ unaffected. */
507
+ .sv-grid-container.sv-grid-has-vscroll.sv-grid-has-hscroll {
508
+ padding-right: 16px;
509
+ }
495
510
  .sv-grid-column[data-align="center"] .sv-grid-header-sort {
496
511
  justify-content: center;
497
512
  }
@@ -857,6 +872,11 @@
857
872
  flex-direction: row;
858
873
  align-items: center;
859
874
  gap: 4px;
875
+ box-sizing: border-box;
876
+ /* Inset the operator button + input from the cell edges so the filter row
877
+ lines up with the header label / body text (same --sg-cell-px gutter) and
878
+ isn't cramped against the top/side borders. */
879
+ padding: 5px var(--sg-filter-cell-px, var(--sg-cell-px, 7px));
860
880
  }
861
881
 
862
882
  .sv-grid-filter-operator {
@@ -871,6 +891,100 @@
871
891
  box-sizing: border-box;
872
892
  }
873
893
 
894
+ /* Invalid regex pattern: flag the input so the user knows the half-typed
895
+ pattern isn't being applied (it matches nothing until it compiles). */
896
+ .sv-grid-filter-value.sv-grid-filter-value-invalid,
897
+ .sv-grid-tp-filter-input.sv-grid-filter-value-invalid {
898
+ border-color: var(--sg-danger, #e5484d) !important;
899
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--sg-danger, #e5484d) 45%, transparent);
900
+ }
901
+
902
+ /* In / Not-in chip (token) input: chips + a flex-growing text box share one
903
+ bordered control that reads like a single field. */
904
+ .sv-grid-filter-chips {
905
+ flex: 3 1 0;
906
+ min-width: 0;
907
+ box-sizing: border-box;
908
+ display: flex;
909
+ flex-wrap: nowrap;
910
+ align-items: center;
911
+ gap: 3px;
912
+ padding: 2px 4px;
913
+ border: 1px solid var(--sg-input-border, #8794a8);
914
+ border-radius: 4px;
915
+ background: var(--sg-input-bg, #fff);
916
+ overflow: hidden;
917
+ }
918
+
919
+ /* A few chips stay visible; the rest collapse into a non-wrapping "+N" pill
920
+ that opens the value dropdown (select / unselect). */
921
+ .sv-grid-filter-chip .sv-grid-filter-chip-label {
922
+ max-width: 84px;
923
+ }
924
+ .sv-grid-filter-chip-more {
925
+ flex: none;
926
+ border: 0;
927
+ cursor: pointer;
928
+ font: inherit;
929
+ font-size: 11px;
930
+ line-height: 16px;
931
+ color: var(--sg-text, inherit);
932
+ }
933
+ .sv-grid-filter-chip-more:hover {
934
+ background: color-mix(in srgb, var(--sg-accent, #0b63f3) 26%, transparent);
935
+ }
936
+ .sv-grid-filter-chips:focus-within {
937
+ border-color: var(--sg-accent, #0b63f3);
938
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--sg-accent, #0b63f3) 45%, transparent);
939
+ }
940
+ .sv-grid-filter-chip {
941
+ display: inline-flex;
942
+ align-items: center;
943
+ gap: 2px;
944
+ max-width: 120px;
945
+ padding: 0 2px 0 6px;
946
+ border-radius: 3px;
947
+ font-size: 11px;
948
+ line-height: 16px;
949
+ background: color-mix(in srgb, var(--sg-accent, #0b63f3) 16%, transparent);
950
+ color: var(--sg-text, inherit);
951
+ }
952
+ .sv-grid-filter-chip-label {
953
+ overflow: hidden;
954
+ text-overflow: ellipsis;
955
+ white-space: nowrap;
956
+ }
957
+ .sv-grid-filter-chip-x {
958
+ flex: none;
959
+ display: inline-flex;
960
+ align-items: center;
961
+ justify-content: center;
962
+ width: 14px;
963
+ height: 14px;
964
+ padding: 0;
965
+ border: 0;
966
+ border-radius: 3px;
967
+ background: transparent;
968
+ color: inherit;
969
+ font-size: 13px;
970
+ line-height: 1;
971
+ cursor: pointer;
972
+ opacity: 0.7;
973
+ }
974
+ .sv-grid-filter-chip-x:hover {
975
+ opacity: 1;
976
+ background: color-mix(in srgb, var(--sg-accent, #0b63f3) 28%, transparent);
977
+ }
978
+ .sv-grid-filter-chips .sv-grid-filter-chip-input {
979
+ flex: 1 1 14px;
980
+ min-width: 14px;
981
+ border: 0 !important;
982
+ padding: 0 2px;
983
+ background: transparent;
984
+ box-shadow: none !important;
985
+ outline: none;
986
+ }
987
+
874
988
  /* Brief glow used when the user clicks the funnel icon while the grid
875
989
  is in row-filter mode - draws attention to where the input is so
876
990
  the click isn't a no-op. */
@@ -1220,6 +1334,22 @@ select.sv-grid-fr-editor {
1220
1334
  width: 100%;
1221
1335
  font-size: 0.8125rem;
1222
1336
  }
1337
+ /* The global "Filter all rows" search: a labelled input bar above the grid. Ships
1338
+ styled so it never renders as a bare, unpositioned text label + input. */
1339
+ .sv-grid-global-filter {
1340
+ display: inline-flex;
1341
+ align-items: center;
1342
+ gap: 8px;
1343
+ margin-bottom: 8px;
1344
+ font-size: 0.8125rem;
1345
+ font-weight: 550;
1346
+ color: var(--sg-muted, #64748b);
1347
+ }
1348
+ .sv-grid-global-filter input {
1349
+ height: 30px;
1350
+ min-width: 220px;
1351
+ font-size: 0.8125rem;
1352
+ }
1223
1353
  .sv-grid-column-filter::placeholder,
1224
1354
  .sv-grid-filter-value::placeholder,
1225
1355
  .sv-grid-menu-search::placeholder,
@@ -1752,6 +1882,38 @@ select.sv-grid-fr-editor {
1752
1882
  background: var(--sg-row-hover-bg, #eef2f8);
1753
1883
  }
1754
1884
 
1885
+ /* In / Not-in value-suggestions dropdown (anchored under the chip input).
1886
+ Defined AFTER .sv-grid-menu so its tighter sizing wins - the element
1887
+ carries both classes. Compact rows keep a long value list scannable. */
1888
+ .sv-grid-menu.sv-grid-in-suggest {
1889
+ min-width: 140px;
1890
+ max-width: 260px;
1891
+ max-height: 220px;
1892
+ overflow-y: auto;
1893
+ font-size: 12px;
1894
+ }
1895
+ .sv-grid-in-suggest-item {
1896
+ gap: 6px;
1897
+ padding: 3px 8px;
1898
+ border-radius: 4px;
1899
+ line-height: 1.4;
1900
+ }
1901
+ .sv-grid-in-suggest-item[aria-selected="true"] {
1902
+ color: var(--sg-accent, #0b63f3);
1903
+ font-weight: 600;
1904
+ }
1905
+ .sv-grid-in-suggest-check {
1906
+ flex: none;
1907
+ width: 12px;
1908
+ text-align: center;
1909
+ color: var(--sg-accent, #0b63f3);
1910
+ }
1911
+ .sv-grid-in-suggest-label {
1912
+ overflow: hidden;
1913
+ text-overflow: ellipsis;
1914
+ white-space: nowrap;
1915
+ }
1916
+
1755
1917
  .sv-grid-menu-item[aria-checked="true"] {
1756
1918
  background: var(--sg-selection-bg, #eaf2ff);
1757
1919
  color: var(--sg-accent, #0b63f3);
@@ -2376,6 +2538,32 @@ select.sv-grid-fr-editor {
2376
2538
  color: var(--sg-fg, #101828);
2377
2539
  }
2378
2540
 
2541
+ /* Placeholder shown when `scheduler` is set but the enterprise renderer that
2542
+ powers the calendar view has not been registered. */
2543
+ .sv-grid-scheduler-upsell {
2544
+ display: flex;
2545
+ flex-direction: column;
2546
+ gap: 6px;
2547
+ align-items: flex-start;
2548
+ justify-content: center;
2549
+ height: 100%;
2550
+ padding: 24px;
2551
+ text-align: left;
2552
+ border: 1px dashed var(--sg-border, #e4e7ec);
2553
+ border-radius: var(--sg-radius, 8px);
2554
+ background: color-mix(in srgb, var(--sg-fg, #1f2937) 5%, transparent);
2555
+ color: color-mix(in srgb, var(--sg-fg, #101828) 70%, transparent);
2556
+ }
2557
+ .sv-grid-scheduler-upsell strong { font-size: 0.95rem; }
2558
+ .sv-grid-scheduler-upsell p { margin: 0; font-size: 0.85rem; max-width: 42ch; }
2559
+ .sv-grid-scheduler-upsell code {
2560
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
2561
+ font-size: 0.82em;
2562
+ padding: 1px 4px;
2563
+ border-radius: 4px;
2564
+ background: color-mix(in srgb, var(--sg-fg, #101828) 8%, transparent);
2565
+ }
2566
+
2379
2567
  /* ===================================================================
2380
2568
  Integrated chart panel (the `charting` prop)
2381
2569
  =================================================================== */