@svgrid/grid 1.1.0 → 1.1.2

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 (148) hide show
  1. package/dist/FlexRender.svelte +96 -96
  2. package/dist/GridMenus.svelte +205 -127
  3. package/dist/SvGrid.controller.svelte.d.ts +44 -1
  4. package/dist/SvGrid.controller.svelte.js +240 -44
  5. package/dist/SvGrid.css +325 -22
  6. package/dist/SvGrid.helpers.test.d.ts +1 -0
  7. package/dist/SvGrid.helpers.test.js +298 -0
  8. package/dist/SvGrid.svelte +729 -203
  9. package/dist/SvGrid.types.d.ts +91 -4
  10. package/dist/aligned-grids.d.ts +6 -0
  11. package/dist/aligned-grids.js +84 -0
  12. package/dist/aligned-grids.test.d.ts +1 -0
  13. package/dist/aligned-grids.test.js +75 -0
  14. package/dist/build-api.coverage.test.d.ts +20 -0
  15. package/dist/build-api.coverage.test.js +505 -0
  16. package/dist/build-api.js +61 -31
  17. package/dist/cell-render.test.d.ts +1 -0
  18. package/dist/cell-render.test.js +338 -0
  19. package/dist/cell-values.d.ts +1 -1
  20. package/dist/cell-values.js +7 -7
  21. package/dist/chart-export.test.d.ts +1 -0
  22. package/dist/chart-export.test.js +302 -0
  23. package/dist/chart.coverage.test.d.ts +1 -0
  24. package/dist/chart.coverage.test.js +748 -0
  25. package/dist/clipboard.js +88 -24
  26. package/dist/clipboard.test.d.ts +1 -0
  27. package/dist/clipboard.test.js +700 -0
  28. package/dist/collaboration.coverage.test.d.ts +1 -0
  29. package/dist/collaboration.coverage.test.js +200 -0
  30. package/dist/column-groups.d.ts +19 -0
  31. package/dist/column-groups.js +62 -0
  32. package/dist/column-groups.test.d.ts +1 -0
  33. package/dist/column-groups.test.js +56 -0
  34. package/dist/column-types.d.ts +10 -0
  35. package/dist/column-types.js +63 -0
  36. package/dist/column-types.test.d.ts +1 -0
  37. package/dist/column-types.test.js +62 -0
  38. package/dist/columns.test.d.ts +1 -0
  39. package/dist/columns.test.js +625 -0
  40. package/dist/core.d.ts +86 -1
  41. package/dist/core.js +2 -2
  42. package/dist/editing.d.ts +7 -0
  43. package/dist/editing.js +191 -5
  44. package/dist/editing.test.d.ts +1 -0
  45. package/dist/editing.test.js +732 -0
  46. package/dist/editors/cell-editors.coverage.test.d.ts +1 -0
  47. package/dist/editors/cell-editors.coverage.test.js +139 -0
  48. package/dist/facet-buckets.test.d.ts +1 -0
  49. package/dist/facet-buckets.test.js +296 -0
  50. package/dist/filter-operators.test.d.ts +1 -0
  51. package/dist/filter-operators.test.js +135 -0
  52. package/dist/hyperformula-adapter.test.d.ts +1 -0
  53. package/dist/hyperformula-adapter.test.js +205 -0
  54. package/dist/index.d.ts +5 -2
  55. package/dist/index.js +4 -1
  56. package/dist/keyboard-handlers.coverage.test.d.ts +1 -0
  57. package/dist/keyboard-handlers.coverage.test.js +495 -0
  58. package/dist/keyboard-handlers.js +7 -2
  59. package/dist/menus.js +1 -0
  60. package/dist/menus.test.d.ts +1 -0
  61. package/dist/menus.test.js +560 -0
  62. package/dist/named-views.coverage.test.d.ts +1 -0
  63. package/dist/named-views.coverage.test.js +180 -0
  64. package/dist/row-drag.d.ts +49 -0
  65. package/dist/row-drag.js +221 -0
  66. package/dist/row-drag.test.d.ts +1 -0
  67. package/dist/row-drag.test.js +142 -0
  68. package/dist/row-resize.test.d.ts +1 -0
  69. package/dist/row-resize.test.js +329 -0
  70. package/dist/scroll-sync.js +3 -0
  71. package/dist/scroll-sync.test.d.ts +1 -0
  72. package/dist/scroll-sync.test.js +290 -0
  73. package/dist/selection.d.ts +7 -1
  74. package/dist/selection.js +76 -36
  75. package/dist/selection.multi-range.test.d.ts +1 -0
  76. package/dist/selection.multi-range.test.js +55 -0
  77. package/dist/selection.test.d.ts +1 -0
  78. package/dist/selection.test.js +647 -0
  79. package/dist/server-data-source.coverage.test.d.ts +1 -0
  80. package/dist/server-data-source.coverage.test.js +154 -0
  81. package/dist/spreadsheet.d.ts +30 -0
  82. package/dist/spreadsheet.js +48 -0
  83. package/dist/spreadsheet.test.d.ts +1 -0
  84. package/dist/spreadsheet.test.js +446 -0
  85. package/dist/summaries.js +4 -4
  86. package/dist/sv-grid-scrollbar.js +13 -1
  87. package/dist/svgrid-wrapper.types.d.ts +19 -0
  88. package/dist/svgrid.behavior.test.js +20 -0
  89. package/dist/svgrid.interaction.test.js +31 -0
  90. package/dist/svgrid.new-features.wrapper.test.js +34 -2
  91. package/dist/test-setup.js +9 -3
  92. package/dist/virtualization/scroll-scaling.d.ts +17 -0
  93. package/dist/virtualization/scroll-scaling.js +35 -0
  94. package/dist/virtualization/scroll-scaling.test.js +42 -1
  95. package/package.json +2 -1
  96. package/src/FlexRender.svelte +96 -96
  97. package/src/GridMenus.svelte +205 -127
  98. package/src/SvGrid.controller.svelte.ts +204 -36
  99. package/src/SvGrid.css +277 -12
  100. package/src/SvGrid.svelte +727 -205
  101. package/src/SvGrid.types.ts +85 -4
  102. package/src/a11y.contract.test.ts +49 -49
  103. package/src/a11y.test.ts +59 -59
  104. package/src/a11y.ts +59 -59
  105. package/src/aligned-grids.test.ts +80 -0
  106. package/src/aligned-grids.ts +87 -0
  107. package/src/build-api.ts +43 -23
  108. package/src/cell-formatting.ts +169 -169
  109. package/src/cell-values.ts +4 -4
  110. package/src/clipboard.test.ts +49 -0
  111. package/src/clipboard.ts +51 -23
  112. package/src/column-groups.test.ts +59 -0
  113. package/src/column-groups.ts +80 -0
  114. package/src/column-types.test.ts +68 -0
  115. package/src/column-types.ts +82 -0
  116. package/src/core.performance.test.ts +30 -30
  117. package/src/core.ts +1077 -999
  118. package/src/createGrid.svelte.ts +42 -42
  119. package/src/createGrid.test.ts +10 -10
  120. package/src/createGridState.svelte.ts +17 -17
  121. package/src/editing.ts +161 -5
  122. package/src/flex-render.ts +3 -3
  123. package/src/index.ts +208 -196
  124. package/src/keyboard.test.ts +59 -59
  125. package/src/keyboard.ts +97 -97
  126. package/src/menus.ts +1 -0
  127. package/src/merge-objects.ts +48 -48
  128. package/src/render-component.ts +28 -28
  129. package/src/row-drag.test.ts +168 -0
  130. package/src/row-drag.ts +255 -0
  131. package/src/scroll-sync.ts +2 -0
  132. package/src/selection.multi-range.test.ts +61 -0
  133. package/src/selection.ts +71 -37
  134. package/src/spreadsheet.test.ts +46 -2
  135. package/src/spreadsheet.ts +58 -0
  136. package/src/static-functions.ts +11 -11
  137. package/src/subscribe.ts +38 -38
  138. package/src/summaries.ts +4 -4
  139. package/src/svgrid-wrapper.types.ts +412 -393
  140. package/src/svgrid.features.test.ts +157 -157
  141. package/src/svgrid.new-features.wrapper.test.ts +2 -2
  142. package/src/svgrid.wrapper.test.ts +40 -40
  143. package/src/virtualization/column-virtualizer.test.ts +27 -27
  144. package/src/virtualization/column-virtualizer.ts +30 -30
  145. package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
  146. package/src/virtualization/types.ts +30 -30
  147. package/src/virtualization/virtualizer.test.ts +47 -47
  148. package/src/virtualization/virtualizer.ts +296 -296
@@ -25,6 +25,10 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
25
25
  operator: FilterOperator;
26
26
  value: string;
27
27
  valueTo?: string;
28
+ operator2?: FilterOperator;
29
+ value2?: string;
30
+ valueTo2?: string;
31
+ join?: "AND" | "OR";
28
32
  }>;
29
33
  verticalScrollbarEl: HTMLElement | null;
30
34
  horizontalScrollbarEl: HTMLElement | null;
@@ -35,6 +39,7 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
35
39
  pendingScrollLeft: number;
36
40
  scrollSyncRaf: number | null;
37
41
  selectionRange: SelectionRange;
42
+ selectionRanges: SelectionRange[];
38
43
  isDraggingSelection: boolean;
39
44
  fillDrag: {
40
45
  sourceMinRow: number;
@@ -49,6 +54,10 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
49
54
  colIndex: number;
50
55
  } | null;
51
56
  editingCell: CellEditState;
57
+ fullRowEdit: {
58
+ rowId: string;
59
+ draft: Record<string, unknown>;
60
+ } | null;
52
61
  editedCellValues: Record<string, unknown>;
53
62
  readonly UNDO_LIMIT: number;
54
63
  history: {
@@ -100,6 +109,7 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
100
109
  readonly DATE_OPERATORS: FilterOperator[];
101
110
  readonly CHECKBOX_OPERATORS: FilterOperator[];
102
111
  columnMenuFor: string | null;
112
+ columnMenuTab: "filter" | "columns" | "general";
103
113
  columnMenuPos: MenuPosition;
104
114
  columnMenuSearch: string;
105
115
  filterMenuFor: string | null;
@@ -140,6 +150,8 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
140
150
  internalData: readonly TData[];
141
151
  internalColumns: ColumnDef<TFeatures, TData>[];
142
152
  hiddenColumns: Record<string, boolean>;
153
+ readonly toggleColumnGroup: (groupId: string) => void;
154
+ readonly isColumnGroupCollapsed: (groupId: string) => boolean;
143
155
  readonly externalSortEnabled: boolean;
144
156
  readonly externalFilterEnabled: boolean;
145
157
  readonly passthroughSortedRowModel: ({ rows }: {
@@ -165,6 +177,10 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
165
177
  * multi-level value tree). They render as empty cells so the
166
178
  * layout stays aligned without showing duplicate labels. */
167
179
  isPlaceholder: boolean;
180
+ /** Set when this group cell has a collapse toggle. */
181
+ groupId?: string;
182
+ collapsible: boolean;
183
+ collapsed: boolean;
168
184
  }[];
169
185
  }[];
170
186
  readonly pinnedOffsets: {
@@ -176,6 +192,17 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
176
192
  colDragId: string | null;
177
193
  colDropOnId: string | null;
178
194
  colDropSide: "before" | "after" | null;
195
+ rowDragActive: boolean;
196
+ rowDropIndex: number | null;
197
+ rowDropSide: "before" | "after" | null;
198
+ readonly onRowDragStart: (e: DragEvent, rowIndex: number) => void;
199
+ readonly onRowDragOver: (e: DragEvent, rowIndex: number) => void;
200
+ readonly onRowDragLeave: (rowIndex: number) => void;
201
+ readonly onRowDrop: (e: DragEvent, rowIndex: number) => void;
202
+ readonly onRowsContainerDragOver: (e: DragEvent) => void;
203
+ readonly onRowsContainerDrop: (e: DragEvent) => void;
204
+ readonly onRowDragEnd: () => void;
205
+ readonly broadcastAlignedScroll: (left: number) => void;
179
206
  readonly getCurrentColumnOrder: () => string[];
180
207
  readonly emitColumnOrder: () => void;
181
208
  readonly setColumnOrderInternal: (ids: ReadonlyArray<string>) => void;
@@ -215,6 +242,7 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
215
242
  max: number;
216
243
  } | null;
217
244
  toolPanelOpen: boolean;
245
+ toolPanelTab: "columns" | "filters";
218
246
  readonly toolPanelEnabled: boolean;
219
247
  readonly toolPanelColumns: Column<TData>[];
220
248
  readonly toolPanelHeaderLabel: typeof toolPanelHeaderLabel;
@@ -324,7 +352,7 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
324
352
  readonly startColumnResize: (event: PointerEvent, columnId: string) => void;
325
353
  readonly onColumnResizeMove: (event: PointerEvent) => void;
326
354
  readonly endColumnResize: () => void;
327
- readonly setSelection: (rowIndex: number, colIndex: number) => void;
355
+ readonly setSelection: (rowIndex: number, colIndex: number, additive?: boolean) => void;
328
356
  readonly extendSelection: (rowIndex: number, colIndex: number) => void;
329
357
  readonly isCellInSelectedRange: (rowIndex: number, colIndex: number) => boolean;
330
358
  readonly getCellRangeEdges: (rowIndex: number, colIndex: number) => {
@@ -333,6 +361,12 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
333
361
  left: boolean;
334
362
  right: boolean;
335
363
  } | null;
364
+ readonly getSelectionRects: () => {
365
+ minRow: number;
366
+ maxRow: number;
367
+ minCol: number;
368
+ maxCol: number;
369
+ }[];
336
370
  readonly fillHandleCell: {
337
371
  rowIndex: any;
338
372
  colIndex: any;
@@ -354,9 +388,18 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
354
388
  readonly onCellClick: (rowIndex: number, colIndex: number) => void;
355
389
  readonly emitCellDoubleClick: (rowIndex: number, colIndex: number) => void;
356
390
  readonly copySelectionToClipboard: () => void;
391
+ readonly cutSelectionToClipboard: () => Promise<void>;
392
+ readonly pasteFromClipboard: () => Promise<void>;
393
+ readonly onGridPaste: (event: ClipboardEvent) => void;
357
394
  readonly clearSelectedCells: () => boolean;
358
395
  readonly onCellDoubleClick: (rowIndex: number, colIndex: number) => void;
359
396
  readonly startEditingWithChar: (rowIndex: number, colIndex: number, char: string) => boolean;
397
+ readonly startEditing: (rowIndex: number, columnId: string) => boolean;
398
+ readonly stopEditing: (cancel?: boolean) => boolean;
399
+ readonly startFullRowEdit: (rowIndex: number) => boolean;
400
+ readonly setFullRowDraft: (columnId: string, value: unknown) => void;
401
+ readonly commitFullRowEdit: () => void;
402
+ readonly cancelFullRowEdit: () => void;
360
403
  readonly saveEditingCell: () => void;
361
404
  readonly applyHistoryStep: (step: {
362
405
  rowId: string;
@@ -1,5 +1,5 @@
1
1
  import { applyExcelFilter, normalizeForFilter, createColumnVirtualizer, createCoreRowModel, createExpandedRowModel, createFilteredRowModel, createGroupedRowModel, createSvelteVirtualizer, createSortedRowModel, createSvGrid, getGridCellDomId, sortFns, } from "./index";
2
- import { createRowScrollScaling } from "./virtualization/scroll-scaling";
2
+ import { createRowScrollScaling, resolveMaxDomHeight, } from "./virtualization/scroll-scaling";
3
3
  import "./sv-grid-scrollbar";
4
4
  import { computeColumnStat, formatsNeedingStats, } from "./conditional-formatting";
5
5
  import { rawToNumber, } from "./SvGrid.helpers";
@@ -12,6 +12,10 @@ import { createCellRender, } from "./cell-render";
12
12
  import { createEditing, } from "./editing";
13
13
  import { createSelection, } from "./selection";
14
14
  import { createColumns, } from "./columns";
15
+ import { createRowDrag, } from "./row-drag";
16
+ import { createAlignedGrids, } from "./aligned-grids";
17
+ import { resolveColumnTypes, } from "./column-types";
18
+ import { computeColumnGroupMeta, hiddenLeavesForCollapse, } from "./column-groups";
15
19
  import { createGridApi, } from "./build-api";
16
20
  import { createClipboard, } from "./clipboard";
17
21
  import { filterOperatorOptions, fallbackOperatorOption, TEXT_OPERATORS, NUMBER_OPERATORS, DATE_OPERATORS, CHECKBOX_OPERATORS, operatorOption, operatorsForColumn, defaultOperatorFor, operatorLabelFor, } from "./filter-operators";
@@ -24,35 +28,81 @@ import { getColumnBaseValue, isGroupRow, toolPanelHeaderLabel, formatSummaryNume
24
28
  */
25
29
  const MAX_DOM_SCROLL_HEIGHT_FALLBACK = 8000000;
26
30
  /**
27
- * The browser's actual maximum element height in CSS px. Browsers clamp how
28
- * tall a single element may be (and mobile / high-DPR caps differ), so we
29
- * measure it once by reading the clamped `offsetHeight` of an absurdly tall
30
- * probe kept inside a 1x1 `overflow:hidden` wrapper (so it never affects page
31
- * layout or scroll). Cached for the page lifetime; constant per browser.
31
+ * The browser's actual maximum *scrollable* element height in CSS px. Browsers
32
+ * clamp how tall a single element may be, and the cap is lower on mobile /
33
+ * high-DPR devices (the physical limit is in device px, so a 3x-DPR phone has
34
+ * ~1/3 the CSS-px cap of a 1x desktop). Past that cap a scroll container
35
+ * silently clamps its `scrollHeight` and the tail rows of a huge virtualized
36
+ * grid become unreachable.
37
+ *
38
+ * We measure two signals from one offscreen probe and keep the smaller (see
39
+ * `resolveMaxDomHeight`): the probe's clamped `offsetHeight`, AND the
40
+ * `scrollHeight` a real `overflow:auto` container exposes for it. The second
41
+ * matters because mobile WebKit/Blink can report a generous `offsetHeight` yet
42
+ * expose a smaller scrollable range - trusting the layout height alone is what
43
+ * stranded the last rows on phones. Using a real scroll container also folds in
44
+ * DPR clamping for free. Cached for the page lifetime; constant per browser.
32
45
  */
33
46
  let detectedMaxDomHeight = null;
47
+ /**
48
+ * Seed the `hiddenColumns` map from any column def marked `visible: false`.
49
+ * Walks groups so a hidden group hides all of its leaf columns. Keyed by the
50
+ * same id `setColumnVisible` uses (`id ?? field`), so user toggles afterward
51
+ * stay consistent. Run once at mount; prop changes don't re-apply it.
52
+ */
53
+ function initialHiddenColumns(defs) {
54
+ const hidden = {};
55
+ const walk = (cols, inheritedHidden) => {
56
+ for (const def of cols) {
57
+ const hide = inheritedHidden || def.visible === false;
58
+ if (def.columns?.length) {
59
+ walk(def.columns, hide);
60
+ }
61
+ else if (hide) {
62
+ const id = def.id ?? def.field;
63
+ if (id)
64
+ hidden[id] = true;
65
+ }
66
+ }
67
+ };
68
+ walk(defs, false);
69
+ return hidden;
70
+ }
34
71
  function getMaxDomScrollHeight() {
72
+ // Escape hatch: a page may pin the cap via `window.__svgridMaxDomHeight`.
73
+ // Checked before the cache so it always wins. Two uses: reproducing a
74
+ // phone's lower element-height limit on desktop (and our e2e coverage of
75
+ // the huge-list path), and overriding detection on a device where it reads
76
+ // wrong. A non-positive / non-finite value is ignored.
77
+ if (typeof window !== "undefined") {
78
+ const forced = window
79
+ .__svgridMaxDomHeight;
80
+ if (typeof forced === "number" && Number.isFinite(forced) && forced > 0) {
81
+ return forced;
82
+ }
83
+ }
35
84
  if (detectedMaxDomHeight != null)
36
85
  return detectedMaxDomHeight;
37
86
  if (typeof document === "undefined" || !document.body) {
38
87
  return MAX_DOM_SCROLL_HEIGHT_FALLBACK;
39
88
  }
40
89
  try {
90
+ // The wrapper is itself an `overflow:auto` scroll container (kept tiny and
91
+ // offscreen so it never affects page layout or scroll), so we can read the
92
+ // height it actually exposes as scrollable - not just the probe's layout
93
+ // height. On high-DPR mobile the two diverge and the scrollable one is the
94
+ // limit that matters.
41
95
  const wrap = document.createElement("div");
42
96
  wrap.style.cssText =
43
- "position:fixed;top:0;left:-9999px;width:1px;height:1px;overflow:hidden;visibility:hidden;pointer-events:none;";
97
+ "position:fixed;top:0;left:-9999px;width:1px;height:100px;overflow:auto;visibility:hidden;pointer-events:none;";
44
98
  const probe = document.createElement("div");
45
99
  probe.style.cssText = "width:1px;height:1000000000px;";
46
100
  wrap.appendChild(probe);
47
101
  document.body.appendChild(wrap);
48
- const measured = probe.offsetHeight;
102
+ const layoutCap = probe.offsetHeight;
103
+ const scrollCap = wrap.scrollHeight;
49
104
  document.body.removeChild(wrap);
50
- // A clamped read is a large finite number well under the 1e9 we asked for.
51
- // If the browser did not clamp (returned ~1e9) or returned junk, fall back.
52
- detectedMaxDomHeight =
53
- measured > 100000 && measured < 900000000
54
- ? measured
55
- : MAX_DOM_SCROLL_HEIGHT_FALLBACK;
105
+ detectedMaxDomHeight = resolveMaxDomHeight(layoutCap, scrollCap, MAX_DOM_SCROLL_HEIGHT_FALLBACK);
56
106
  }
57
107
  catch {
58
108
  detectedMaxDomHeight = MAX_DOM_SCROLL_HEIGHT_FALLBACK;
@@ -91,6 +141,10 @@ export function createSvGridController(props) {
91
141
  let pendingScrollLeft = 0;
92
142
  let scrollSyncRaf = null;
93
143
  let selectionRange = $state({ anchor: null, focus: null });
144
+ // Extra committed ranges for multi-range (Ctrl+drag) selection. The
145
+ // `selectionRange` above is always the ACTIVE range being manipulated; these
146
+ // are the finished ones. Full selection = these + the active range.
147
+ let selectionRanges = $state.raw([]);
94
148
  let isDraggingSelection = $state(false);
95
149
  /** Excel-style fill handle drag state. While non-null we paint a "fill
96
150
  * preview" overlay on cells between the source range and the pointer
@@ -98,6 +152,9 @@ export function createSvGridController(props) {
98
152
  let fillDrag = $state(null);
99
153
  let activeAtPointerDown = null;
100
154
  let editingCell = $state(null);
155
+ // Full-row editing: the row currently in whole-row edit + its per-column
156
+ // draft (keyed by column id). Null when not in full-row mode.
157
+ let fullRowEdit = $state(null);
101
158
  let editedCellValues = $state({});
102
159
  const UNDO_LIMIT = 200;
103
160
  let history = $state([]);
@@ -160,6 +217,7 @@ export function createSvGridController(props) {
160
217
  const rowNumberColumnWidth = $derived(props.rowNumberWidth ?? 56);
161
218
  const showRowNumbersEffective = $derived(props.showRowNumbers ?? false);
162
219
  let columnMenuFor = $state(null);
220
+ let columnMenuTab = $state("general");
163
221
  let columnMenuPos = $state({ x: 0, y: 0 });
164
222
  let columnMenuSearch = $state("");
165
223
  let filterMenuFor = $state(null);
@@ -205,14 +263,25 @@ export function createSvGridController(props) {
205
263
  };
206
264
  });
207
265
  /** Vertical overflow from the virtualizer's authoritative total size,
208
- * NOT from `scrollMetrics.scrollHeight`. Reading DOM dimensions
266
+ * NOT from `scrollMetrics.scrollHeight` alone. Reading DOM dimensions
209
267
  * during a Svelte derived runs BEFORE the browser paints - the table
210
268
  * hasn't laid out the new rows yet, so `scrollHeight` is briefly 0
211
269
  * even after data loads. That made the overflow flag return false,
212
- * hid the scrollbar, and broke dragging. */
270
+ * hid the scrollbar, and broke dragging.
271
+ *
272
+ * However, virtualizer.getTotalSize() uses rowHeight * numRows which
273
+ * underestimates when variable-height rows are present (e.g. master-detail
274
+ * expanded rows). We therefore take the MAX of the two sources:
275
+ * - virtualizer.getTotalSize(): correct at initial load (before first paint)
276
+ * - scrollMetrics.scrollHeight: correct after detail rows expand (DOM is live,
277
+ * ResizeObserver on gridRootEl already bumps scrollVersion at that point) */
213
278
  const hasVerticalOverflow = $derived.by(() => {
214
279
  virtualizer.version;
215
- return virtualizer.getTotalSize() > viewportHeight + 1;
280
+ const virtualizerSize = virtualizer.getTotalSize();
281
+ // scrollMetrics.scrollHeight is 0 before initial paint; once the table
282
+ // is in the DOM it reflects the true content height including expanded rows.
283
+ const domSize = scrollMetrics.scrollHeight;
284
+ return Math.max(virtualizerSize, domSize) > viewportHeight + 1;
216
285
  });
217
286
  // Effective filter-UI flags. Each show* prop wins when explicitly set;
218
287
  // otherwise the `filterMode` prop (default 'menu') picks exactly one surface.
@@ -236,9 +305,41 @@ export function createSvGridController(props) {
236
305
  // mutates these so add/remove operations don't need a callback round-trip.
237
306
  // svelte-ignore state_referenced_locally
238
307
  let internalData = $state.raw(props.data);
308
+ // Resolve `cellDataType` / `inferColumnTypes` into concrete editorType +
309
+ // format defaults once, up front, so every downstream reader sees a normal
310
+ // column. Explicit fields on the ColumnDef always win.
311
+ // svelte-ignore state_referenced_locally
312
+ const resolveCols = (cols) => resolveColumnTypes(cols, props.data?.[0], props.inferColumnTypes === true);
239
313
  // svelte-ignore state_referenced_locally
240
- let internalColumns = $state.raw(props.columns);
241
- let hiddenColumns = $state({});
314
+ let internalColumns = $state.raw(resolveCols(props.columns));
315
+ // svelte-ignore state_referenced_locally
316
+ let hiddenColumns = $state(initialHiddenColumns(props.columns));
317
+ // Collapsible column groups (columnGroupShow). Meta is derived from the tree;
318
+ // `collapsedColumnGroups` is the live set of collapsed group ids, seeded once
319
+ // from each collapsible group's `openByDefault` (default: collapsed).
320
+ const columnGroupMeta = $derived(computeColumnGroupMeta(props.columns));
321
+ // svelte-ignore state_referenced_locally
322
+ let collapsedColumnGroups = $state((() => {
323
+ const meta = computeColumnGroupMeta(props.columns);
324
+ const s = new Set();
325
+ for (const id of meta.collapsibleGroupIds)
326
+ if (!meta.defaultOpen.get(id))
327
+ s.add(id);
328
+ return s;
329
+ })());
330
+ // Leaf ids hidden right now because their group is collapsed/expanded.
331
+ const hiddenByGroupCollapse = $derived(hiddenLeavesForCollapse(columnGroupMeta, collapsedColumnGroups));
332
+ function toggleColumnGroup(groupId) {
333
+ const next = new Set(collapsedColumnGroups);
334
+ if (next.has(groupId))
335
+ next.delete(groupId);
336
+ else
337
+ next.add(groupId);
338
+ collapsedColumnGroups = next;
339
+ }
340
+ function isColumnGroupCollapsed(groupId) {
341
+ return collapsedColumnGroups.has(groupId);
342
+ }
242
343
  $effect(() => {
243
344
  // When the consumer replaces `data` (e.g. a "Reset" button), drop any
244
345
  // accumulated cell-edit overrides - otherwise `getCellDisplayValue`
@@ -248,7 +349,7 @@ export function createSvGridController(props) {
248
349
  editedCellValues = {};
249
350
  });
250
351
  $effect(() => {
251
- internalColumns = props.columns;
352
+ internalColumns = resolveCols(props.columns);
252
353
  });
253
354
  // Captured ONCE at mount: `externalSort` is a structural choice (tree vs
254
355
  // flat data) so toggling it after mount is not supported. Reading it here
@@ -334,7 +435,7 @@ export function createSvGridController(props) {
334
435
  const allColumns = $derived.by(() => {
335
436
  let raw = grid
336
437
  .getAllColumns()
337
- .filter((column) => !hiddenColumns[column.id]);
438
+ .filter((column) => !hiddenColumns[column.id] && !hiddenByGroupCollapse[column.id]);
338
439
  // Apply user reorder (if any). Unknown ids in userColumnOrder are
339
440
  // skipped; columns not in userColumnOrder keep their original
340
441
  // relative order after the user-ordered ones.
@@ -403,13 +504,17 @@ export function createSvGridController(props) {
403
504
  function buildId(def, parentId, fallbackIx) {
404
505
  return def.id ?? def.field ?? `${parentId ?? 'col'}_d_${fallbackIx}`;
405
506
  }
507
+ // Leaves hidden by a collapsed/expanded group are skipped everywhere here,
508
+ // so group colSpan + widthPx exclude them and stay aligned with the leaves
509
+ // the body actually renders.
510
+ const hiddenLeaf = hiddenByGroupCollapse;
406
511
  function collectLeaves(defs, parentId, depthHere) {
407
512
  defs.forEach((def, ix) => {
408
513
  const id = buildId(def, parentId, ix);
409
514
  if (def.columns?.length) {
410
515
  collectLeaves(def.columns, id, depthHere + 1);
411
516
  }
412
- else {
517
+ else if (!hiddenLeaf[id]) {
413
518
  leafEntries.push({ id, widthPx: getColumnWidth(id) });
414
519
  }
415
520
  });
@@ -419,6 +524,10 @@ export function createSvGridController(props) {
419
524
  const nodes = [];
420
525
  for (const def of defs) {
421
526
  const id = buildId(def, parentId, nodes.length);
527
+ // Skip leaves the collapse state hides, so leaf indices/colSpans match
528
+ // `leafEntries` (and the body's rendered columns) exactly.
529
+ if (!def.columns?.length && hiddenLeaf[id])
530
+ continue;
422
531
  const leafStart = cursor.leaf;
423
532
  if (def.columns?.length) {
424
533
  indexTree(def.columns, id, cursor);
@@ -464,6 +573,7 @@ export function createSvGridController(props) {
464
573
  const cells = at.map((n) => {
465
574
  const isLeafEarly = !n.def.columns?.length;
466
575
  const headerText = typeof n.def.header === 'string' ? n.def.header : '';
576
+ const collapsible = columnGroupMeta.collapsibleGroupIds.has(n.id);
467
577
  return {
468
578
  key: `${n.id}_d${d}`,
469
579
  label: isLeafEarly ? '' : headerText,
@@ -471,6 +581,9 @@ export function createSvGridController(props) {
471
581
  widthPx: sumLeafWidths(n.leafStart, n.leafEnd),
472
582
  firstLeafIndex: n.leafStart,
473
583
  isPlaceholder: isLeafEarly,
584
+ groupId: collapsible ? n.id : undefined,
585
+ collapsible,
586
+ collapsed: collapsible && collapsedColumnGroups.has(n.id),
474
587
  };
475
588
  });
476
589
  rows.push({ id: `gh_${d}`, cells });
@@ -504,6 +617,12 @@ export function createSvGridController(props) {
504
617
  let colDragId = $state(null);
505
618
  let colDropOnId = $state(null);
506
619
  let colDropSide = $state(null);
620
+ // Live drag state for managed row dragging. Only meaningful while a row is
621
+ // being dragged (`props.rowDragManaged`). `rowDropIndex` is the visible row
622
+ // index currently hovered; `rowDropSide` says which edge the drop line paints.
623
+ let rowDragActive = $state(false);
624
+ let rowDropIndex = $state(null);
625
+ let rowDropSide = $state(null);
507
626
  // ---- Conditional formatting --------------------------------------------
508
627
  // True when the feature is in use; gates the per-cell positioning context
509
628
  // (cells are otherwise non-relative for scroll performance).
@@ -521,12 +640,12 @@ export function createSvGridController(props) {
521
640
  if (!needs)
522
641
  continue;
523
642
  const def = column.columnDef;
524
- const accessorFn = def.accessorFn;
643
+ const fieldFn = def.fieldFn;
525
644
  const field = def.field;
526
645
  const stat = computeColumnStat((function* () {
527
646
  for (const row of allRows) {
528
- yield accessorFn
529
- ? accessorFn(row.original)
647
+ yield fieldFn
648
+ ? fieldFn(row.original)
530
649
  : field
531
650
  ? row.original[field]
532
651
  : row.getCellValueByColumnId(column.id);
@@ -577,23 +696,36 @@ export function createSvGridController(props) {
577
696
  .some((cell) => normalizeForFilter(String(cell.getValue() ?? ""), props.filterLocale)
578
697
  .includes(needle)));
579
698
  }
580
- const menuFilters = Object.entries(filterMenuValues).filter(([_, filter]) => {
581
- if (filter.operator === "isBlank")
699
+ // A single condition is "active" if it has the value(s) it needs.
700
+ const condActive = (op, value, valueTo) => {
701
+ if (op === "isBlank")
582
702
  return true;
583
- // `between` needs both endpoints; otherwise treat as inactive.
584
- if (filter.operator === "between") {
585
- return (filter.value.trim().length > 0 &&
586
- (filter.valueTo ?? "").trim().length > 0);
587
- }
588
- return filter.value.trim().length > 0;
703
+ if (op === "between")
704
+ return value.trim().length > 0 && (valueTo ?? "").trim().length > 0;
705
+ return value.trim().length > 0;
706
+ };
707
+ const evalCond = (cellValue, columnId, op, value, valueTo) => applyExcelFilter(cellValue, { id: columnId, operator: op, value, valueTo: op === "between" ? valueTo : undefined }, { locale: props.filterLocale });
708
+ // A column filter is active if either of its (up to two) conditions is.
709
+ const menuFilters = Object.entries(filterMenuValues).filter(([_, f]) => {
710
+ const a = condActive(f.operator, f.value, f.valueTo);
711
+ const b = !!f.operator2 && condActive(f.operator2, f.value2 ?? "", f.valueTo2);
712
+ return a || b;
589
713
  });
590
714
  if (menuFilters.length) {
591
- rows = rows.filter((row) => menuFilters.every(([columnId, filter]) => applyExcelFilter(getRowColumnValue(row, columnId), {
592
- id: columnId,
593
- operator: filter.operator,
594
- value: filter.value,
595
- valueTo: filter.operator === "between" ? filter.valueTo : undefined,
596
- }, { locale: props.filterLocale })));
715
+ rows = rows.filter((row) => menuFilters.every(([columnId, f]) => {
716
+ const cellValue = getRowColumnValue(row, columnId);
717
+ const aActive = condActive(f.operator, f.value, f.valueTo);
718
+ const bActive = !!f.operator2 && condActive(f.operator2, f.value2 ?? "", f.valueTo2);
719
+ const ra = aActive ? evalCond(cellValue, columnId, f.operator, f.value, f.valueTo) : null;
720
+ const rb = bActive
721
+ ? evalCond(cellValue, columnId, f.operator2, f.value2 ?? "", f.valueTo2)
722
+ : null;
723
+ if (ra === null)
724
+ return rb ?? true;
725
+ if (rb === null)
726
+ return ra;
727
+ return f.join === "OR" ? ra || rb : ra && rb;
728
+ }));
597
729
  }
598
730
  const valueFilterEntries = Object.entries(valueFilters);
599
731
  if (valueFilterEntries.length) {
@@ -739,7 +871,10 @@ export function createSvGridController(props) {
739
871
  };
740
872
  });
741
873
  // ---- Tool panel (docked columns sidebar) -------------------------------
742
- let toolPanelOpen = $state(false);
874
+ // svelte-ignore state_referenced_locally
875
+ let toolPanelOpen = $state(props.toolPanelDefaultOpen === true);
876
+ // svelte-ignore state_referenced_locally
877
+ let toolPanelTab = $state(props.toolPanelDefaultTab ?? "columns");
743
878
  const toolPanelEnabled = $derived(props.toolPanel === true);
744
879
  // Every column (including hidden ones) in the user's current order, so the
745
880
  // panel can toggle/reorder anything. Group columns are flagged live.
@@ -1053,6 +1188,7 @@ export function createSvGridController(props) {
1053
1188
  if (isFirstRender)
1054
1189
  return;
1055
1190
  selectionRange = { anchor: null, focus: null };
1191
+ selectionRanges = [];
1056
1192
  editingCell = null;
1057
1193
  if (scrollContainer) {
1058
1194
  scrollContainer.scrollTop = 0;
@@ -1218,7 +1354,16 @@ export function createSvGridController(props) {
1218
1354
  return null;
1219
1355
  const rowNumberWidth = showRowNumbersEffective ? rowNumberColumnWidth : 0;
1220
1356
  const selectionWidth = showRowSelectionEffective ? selectionColumnWidth : 0;
1221
- const target = (scrollContainer?.clientWidth ?? 0) - rowNumberWidth - selectionWidth;
1357
+ // Reserve the custom vertical scrollbar's width when it's visible. It
1358
+ // overlays the right 16px of the viewport (absolute, z-index 40) and
1359
+ // does NOT shrink clientWidth, so without this the last fitted column
1360
+ // slides under it and its right-aligned content (e.g. a number column)
1361
+ // is hidden behind the opaque scrollbar.
1362
+ const scrollbarWidth = hasVerticalOverflow ? 16 : 0;
1363
+ const target = (scrollContainer?.clientWidth ?? 0) -
1364
+ rowNumberWidth -
1365
+ selectionWidth -
1366
+ scrollbarWidth;
1222
1367
  if (target <= 0)
1223
1368
  return null;
1224
1369
  // Split base widths into pinned (user-resized) and scalable.
@@ -1322,7 +1467,9 @@ export function createSvGridController(props) {
1322
1467
  return map;
1323
1468
  });
1324
1469
  const columnMenuFacetValues = $derived.by(() => {
1325
- const columnId = filterMenuFor;
1470
+ // The funnel popover drives via `filterMenuFor`; the column menu's Filter
1471
+ // tab drives via `columnMenuFor`. Support whichever is open.
1472
+ const columnId = filterMenuFor ?? columnMenuFor;
1326
1473
  if (!columnId)
1327
1474
  return [];
1328
1475
  const column = allColumns.find((entry) => entry.id === columnId);
@@ -1393,6 +1540,8 @@ export function createSvGridController(props) {
1393
1540
  set scrollSyncRaf(v) { scrollSyncRaf = v; },
1394
1541
  get selectionRange() { return selectionRange; },
1395
1542
  set selectionRange(v) { selectionRange = v; },
1543
+ get selectionRanges() { return selectionRanges; },
1544
+ set selectionRanges(v) { selectionRanges = v; },
1396
1545
  get isDraggingSelection() { return isDraggingSelection; },
1397
1546
  set isDraggingSelection(v) { isDraggingSelection = v; },
1398
1547
  get fillDrag() { return fillDrag; },
@@ -1401,6 +1550,8 @@ export function createSvGridController(props) {
1401
1550
  set activeAtPointerDown(v) { activeAtPointerDown = v; },
1402
1551
  get editingCell() { return editingCell; },
1403
1552
  set editingCell(v) { editingCell = v; },
1553
+ get fullRowEdit() { return fullRowEdit; },
1554
+ set fullRowEdit(v) { fullRowEdit = v; },
1404
1555
  get editedCellValues() { return editedCellValues; },
1405
1556
  set editedCellValues(v) { editedCellValues = v; },
1406
1557
  get UNDO_LIMIT() { return UNDO_LIMIT; },
@@ -1453,6 +1604,8 @@ export function createSvGridController(props) {
1453
1604
  get DATE_OPERATORS() { return DATE_OPERATORS; },
1454
1605
  get CHECKBOX_OPERATORS() { return CHECKBOX_OPERATORS; },
1455
1606
  get columnMenuFor() { return columnMenuFor; },
1607
+ get columnMenuTab() { return columnMenuTab; },
1608
+ set columnMenuTab(v) { columnMenuTab = v; },
1456
1609
  set columnMenuFor(v) { columnMenuFor = v; },
1457
1610
  get columnMenuPos() { return columnMenuPos; },
1458
1611
  set columnMenuPos(v) { columnMenuPos = v; },
@@ -1498,6 +1651,8 @@ export function createSvGridController(props) {
1498
1651
  set internalColumns(v) { internalColumns = v; },
1499
1652
  get hiddenColumns() { return hiddenColumns; },
1500
1653
  set hiddenColumns(v) { hiddenColumns = v; },
1654
+ get toggleColumnGroup() { return toggleColumnGroup; },
1655
+ get isColumnGroupCollapsed() { return isColumnGroupCollapsed; },
1501
1656
  get externalSortEnabled() { return externalSortEnabled; },
1502
1657
  get externalFilterEnabled() { return externalFilterEnabled; },
1503
1658
  get passthroughSortedRowModel() { return passthroughSortedRowModel; },
@@ -1519,6 +1674,20 @@ export function createSvGridController(props) {
1519
1674
  set colDropOnId(v) { colDropOnId = v; },
1520
1675
  get colDropSide() { return colDropSide; },
1521
1676
  set colDropSide(v) { colDropSide = v; },
1677
+ get rowDragActive() { return rowDragActive; },
1678
+ set rowDragActive(v) { rowDragActive = v; },
1679
+ get rowDropIndex() { return rowDropIndex; },
1680
+ set rowDropIndex(v) { rowDropIndex = v; },
1681
+ get rowDropSide() { return rowDropSide; },
1682
+ set rowDropSide(v) { rowDropSide = v; },
1683
+ get onRowDragStart() { return onRowDragStart; },
1684
+ get onRowDragOver() { return onRowDragOver; },
1685
+ get onRowDragLeave() { return onRowDragLeave; },
1686
+ get onRowDrop() { return onRowDrop; },
1687
+ get onRowsContainerDragOver() { return onRowsContainerDragOver; },
1688
+ get onRowsContainerDrop() { return onRowsContainerDrop; },
1689
+ get onRowDragEnd() { return onRowDragEnd; },
1690
+ get broadcastAlignedScroll() { return broadcastAlignedScroll; },
1522
1691
  get getCurrentColumnOrder() { return getCurrentColumnOrder; },
1523
1692
  get emitColumnOrder() { return emitColumnOrder; },
1524
1693
  get setColumnOrderInternal() { return setColumnOrderInternal; },
@@ -1554,6 +1723,8 @@ export function createSvGridController(props) {
1554
1723
  get statusBarStats() { return statusBarStats; },
1555
1724
  get toolPanelOpen() { return toolPanelOpen; },
1556
1725
  set toolPanelOpen(v) { toolPanelOpen = v; },
1726
+ get toolPanelTab() { return toolPanelTab; },
1727
+ set toolPanelTab(v) { toolPanelTab = v; },
1557
1728
  get toolPanelEnabled() { return toolPanelEnabled; },
1558
1729
  get toolPanelColumns() { return toolPanelColumns; },
1559
1730
  get toolPanelHeaderLabel() { return toolPanelHeaderLabel; },
@@ -1636,6 +1807,7 @@ export function createSvGridController(props) {
1636
1807
  get extendSelection() { return extendSelection; },
1637
1808
  get isCellInSelectedRange() { return isCellInSelectedRange; },
1638
1809
  get getCellRangeEdges() { return getCellRangeEdges; },
1810
+ get getSelectionRects() { return getSelectionRects; },
1639
1811
  get fillHandleCell() { return fillHandleCell; },
1640
1812
  get isInFillPreview() { return isInFillPreview; },
1641
1813
  get findColumnById() { return findColumnById; },
@@ -1654,9 +1826,18 @@ export function createSvGridController(props) {
1654
1826
  get onCellClick() { return onCellClick; },
1655
1827
  get emitCellDoubleClick() { return emitCellDoubleClick; },
1656
1828
  get copySelectionToClipboard() { return copySelectionToClipboard; },
1829
+ get cutSelectionToClipboard() { return cutSelectionToClipboard; },
1830
+ get pasteFromClipboard() { return pasteFromClipboard; },
1831
+ get onGridPaste() { return onGridPaste; },
1657
1832
  get clearSelectedCells() { return clearSelectedCells; },
1658
1833
  get onCellDoubleClick() { return onCellDoubleClick; },
1659
1834
  get startEditingWithChar() { return startEditingWithChar; },
1835
+ get startEditing() { return startEditing; },
1836
+ get stopEditing() { return stopEditing; },
1837
+ get startFullRowEdit() { return startFullRowEdit; },
1838
+ get setFullRowDraft() { return setFullRowDraft; },
1839
+ get commitFullRowEdit() { return commitFullRowEdit; },
1840
+ get cancelFullRowEdit() { return cancelFullRowEdit; },
1660
1841
  get saveEditingCell() { return saveEditingCell; },
1661
1842
  get applyHistoryStep() { return applyHistoryStep; },
1662
1843
  get updateEditingCellValue() { return updateEditingCellValue; },
@@ -1723,10 +1904,25 @@ export function createSvGridController(props) {
1723
1904
  const { computeSummaries, hasRenderedColumn } = createSummaries(ctx);
1724
1905
  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(ctx);
1725
1906
  const { cellConditionalFormat, computeRowClass, computeCellClass, computeCellTooltip, computeCellNote, getColumnEditorOptions, formatListCellValue, formatCellValue, formatPinnedValue, computePinnedCellClass } = createCellRender(ctx);
1726
- const { isCellEditable, isCellEditableAt, getRowColumnValue, getCellDisplayValue, startEditingWithChar, saveEditingCell, applyHistoryStep, updateEditingCellValue, onEditorKeyDown, focusOnMount, onCellDoubleClick, pasteFromClipboard } = createEditing(ctx);
1727
- const { isRowSelected, toggleRowSelectionById, toggleSelectAllRows, setActiveCell, scrollActiveCellIntoView, setSelection, extendSelection, isCellInSelectedRange, getCellRangeEdges, isInFillPreview, findColumnById, onCellPointerDown, onCellPointerEnter, endDragSelection, onWindowPointerMove, onCellClick, emitCellDoubleClick } = createSelection(ctx);
1907
+ const { isCellEditable, isCellEditableAt, getRowColumnValue, getCellDisplayValue, startEditingWithChar, startEditing, stopEditing, startFullRowEdit, setFullRowDraft, commitFullRowEdit, cancelFullRowEdit, saveEditingCell, applyHistoryStep, updateEditingCellValue, onEditorKeyDown, focusOnMount, onCellDoubleClick, pasteFromClipboard, onGridPaste } = createEditing(ctx);
1908
+ const { isRowSelected, toggleRowSelectionById, toggleSelectAllRows, setActiveCell, scrollActiveCellIntoView, setSelection, extendSelection, isCellInSelectedRange, getCellRangeEdges, getSelectionRects, isInFillPreview, findColumnById, onCellPointerDown, onCellPointerEnter, endDragSelection, onWindowPointerMove, onCellClick, emitCellDoubleClick } = createSelection(ctx);
1728
1909
  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(ctx);
1910
+ const { onRowDragStart, onRowDragOver, onRowDragLeave, onRowDrop, onRowsContainerDragOver, onRowsContainerDrop, onRowDragEnd } = createRowDrag(ctx);
1911
+ const { register: registerAlignedGrid, broadcastScroll: broadcastAlignedScroll, broadcastWidths: broadcastAlignedWidths } = createAlignedGrids(ctx);
1729
1912
  const { buildApi } = createGridApi(ctx);
1730
1913
  const { readCellRaw, writeCellRaw, applyFillPattern, clearSelectedCellValues, startFillDrag, onFillPointerMove, onFillPointerUp, toggleBooleanCell, copySelectionToClipboard, clearSelectedCells, cutSelectionToClipboard } = createClipboard(ctx);
1914
+ // Aligned grids: register in the shared group on mount, and mirror column
1915
+ // resizes to peers whenever columnWidths changes. Horizontal-scroll mirroring
1916
+ // is driven from onBodyScroll (via ctx.broadcastAlignedScroll).
1917
+ $effect(() => {
1918
+ if (props.alignedGridGroup == null)
1919
+ return;
1920
+ return registerAlignedGrid();
1921
+ });
1922
+ $effect(() => {
1923
+ // Track columnWidths reactively, then broadcast to aligned peers.
1924
+ void columnWidths;
1925
+ broadcastAlignedWidths();
1926
+ });
1731
1927
  return ctx;
1732
1928
  }