@svgrid/grid 3.0.5 → 3.0.6

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/README.md +28 -7
  2. package/dist/GridMenus.svelte +34 -4
  3. package/dist/SvGrid.controller.svelte.d.ts +21 -3
  4. package/dist/SvGrid.controller.svelte.js +205 -50
  5. package/dist/SvGrid.css +127 -2
  6. package/dist/SvGrid.svelte +223 -38
  7. package/dist/SvGrid.types.d.ts +77 -4
  8. package/dist/SvGridDropdown.svelte +20 -0
  9. package/dist/SvGridDropdown.svelte.d.ts +12 -0
  10. package/dist/SvModal.svelte +22 -0
  11. package/dist/SvModal.svelte.d.ts +7 -0
  12. package/dist/a11y/dismissable.d.ts +10 -0
  13. package/dist/a11y/dismissable.js +24 -2
  14. package/dist/build-api.js +81 -31
  15. package/dist/cdn/{GridMenus-CID62rlI.js → GridMenus-C-NtjlrJ.js} +150 -141
  16. package/dist/cdn/GridMenus-TssHej3q.js +644 -0
  17. package/dist/cdn/{SvChartMenu-FBSMINA6.js → SvChartMenu-BFSTpvB-.js} +1 -1
  18. package/dist/cdn/{SvChartMenu-Bl6PBbkT.js → SvChartMenu-BZ1Iwv0y.js} +1 -1
  19. package/dist/cdn/{SvDateTimePicker-DQwt4UAs.js → SvDateTimePicker-D9u_cOf8.js} +1 -1
  20. package/dist/cdn/{SvDateTimePicker-vNU6bZ-q.js → SvDateTimePicker-s_6IUpI7.js} +1 -1
  21. package/dist/cdn/{SvGridCellEditor-B1p-vCK5.js → SvGridCellEditor-BlyCSp0q.js} +1 -1
  22. package/dist/cdn/{SvGridCellEditor-D_0q4xAu.js → SvGridCellEditor-ClRRA9lW.js} +1 -1
  23. package/dist/cdn/{SvGridChart-DzLkSwCH.js → SvGridChart-B4EV8U6t.js} +4 -4
  24. package/dist/cdn/{SvGridChart-C3EWAZaM.js → SvGridChart-DsmwTyZF.js} +4 -4
  25. package/dist/cdn/{SvGridChartBuilder-CfII62sZ.js → SvGridChartBuilder-cHloHobN.js} +4 -4
  26. package/dist/cdn/{SvGridChartBuilder-BE2T1ykB.js → SvGridChartBuilder-vOl7C0Za.js} +4 -4
  27. package/dist/cdn/{SvGridChartPanel-xf4sWVTo.js → SvGridChartPanel-AcqF-Ll5.js} +5 -5
  28. package/dist/cdn/{SvGridChartPanel-CCX5_Wgd.js → SvGridChartPanel-biMtSBPA.js} +5 -5
  29. package/dist/cdn/{SvGridChartView-DYwabQWj.js → SvGridChartView-Dx_GmZuL.js} +2 -2
  30. package/dist/cdn/{SvGridChartView-CfuXmY5I.js → SvGridChartView-I3rDHXAq.js} +2 -2
  31. package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-BWeM3lMe.js} +46 -46
  32. package/dist/cdn/{SvGridDropdown-D13MtJ2j.js → SvGridDropdown-CJ6QqTxu.js} +67 -67
  33. package/dist/cdn/{SvModal-CIZWCcad.js → SvModal-ClxGmpAc.js} +187 -186
  34. package/dist/cdn/{SvModal-BAE-pjZX.js → SvModal-DhB_VMFR.js} +148 -147
  35. package/dist/cdn/{cell-formatting-C2Hf5gqd.js → cell-formatting-DVVKOSI9.js} +1 -1
  36. package/dist/cdn/{chart-Dz7SqMXH.js → chart-DEZxwC0i.js} +1 -1
  37. package/dist/cdn/{chart-panel-messages-CUbf2R4i.js → chart-panel-messages-D12XcTZJ.js} +1 -1
  38. package/dist/cdn/{chart-panel-messages-CmNrMdsr.js → chart-panel-messages-DguS7gC5.js} +1 -1
  39. package/dist/cdn/{chart-summary-BJW_tg_X.js → chart-summary-CMAXBGY-.js} +1 -1
  40. package/dist/cdn/{chart-trend-CaN9mDEV.js → chart-trend-BkEp61Gj.js} +1 -1
  41. package/dist/cdn/{dismissable-DAHetSNk.js → dismissable-Brc4EZU1.js} +5 -1
  42. package/dist/cdn/{export-format-CUDLy2yn.js → export-format-YzdwBww2.js} +1 -1
  43. package/dist/cdn/{row-resize-niQCp040.js → row-resize-DtOS1KG6.js} +41 -32
  44. package/dist/cdn/{server-block-cache-CGoWz-87.js → server-block-cache-DIqb3VZn.js} +4 -0
  45. package/dist/cdn/{src-jcFMkBnN.js → src-CgoeSM8k.js} +4697 -4131
  46. package/dist/cdn/{src-Bklofsad.js → src-DfwxB1TH.js} +4727 -4161
  47. package/dist/cdn/svgrid.js +17 -17
  48. package/dist/cdn/svgrid.svelte-external.js +17 -17
  49. package/dist/cdn/{validate-C7Rn_Bns.js → validate-AZoD8BoR.js} +6 -2
  50. package/dist/cell-formatting.js +18 -1
  51. package/dist/cell-render.js +25 -6
  52. package/dist/clipboard.d.ts +2 -2
  53. package/dist/clipboard.js +72 -23
  54. package/dist/columns.js +7 -2
  55. package/dist/conditional-formatting.d.ts +2 -0
  56. package/dist/conditional-formatting.js +2 -0
  57. package/dist/core.d.ts +22 -6
  58. package/dist/core.js +365 -25
  59. package/dist/editing.js +24 -0
  60. package/dist/grid-messages.d.ts +4 -0
  61. package/dist/grid-messages.js +4 -0
  62. package/dist/headless.d.ts +5 -0
  63. package/dist/headless.js +12 -0
  64. package/dist/history.d.ts +21 -0
  65. package/dist/history.js +34 -4
  66. package/dist/index.d.ts +1 -1
  67. package/dist/keyboard-handlers.js +24 -1
  68. package/dist/keyboard.js +4 -0
  69. package/dist/row-drag-touch.d.ts +1 -1
  70. package/dist/row-drag.js +35 -1
  71. package/dist/row-model.d.ts +6 -0
  72. package/dist/row-resize.js +22 -1
  73. package/dist/selection.js +1 -1
  74. package/dist/server-block-cache.d.ts +5 -0
  75. package/dist/server-block-cache.js +8 -0
  76. package/dist/server-data-source.d.ts +35 -3
  77. package/dist/server-data-source.js +11 -2
  78. package/dist/server.d.ts +1 -1
  79. package/dist/sparkline.js +7 -5
  80. package/dist/svgrid-wrapper.types.d.ts +33 -0
  81. package/dist/validate.js +9 -1
  82. package/package.json +6 -1
  83. package/src/GridMenus.svelte +34 -4
  84. package/src/SvGrid.controller.svelte.ts +206 -53
  85. package/src/SvGrid.css +127 -2
  86. package/src/SvGrid.svelte +223 -38
  87. package/src/SvGrid.types.ts +77 -4
  88. package/src/SvGridDropdown.svelte +20 -0
  89. package/src/SvModal.svelte +22 -0
  90. package/src/SvModal.test.ts +30 -1
  91. package/src/a11y/dismissable.test.ts +10 -1
  92. package/src/a11y/dismissable.ts +22 -2
  93. package/src/build-api.ts +85 -27
  94. package/src/cell-formatting.test.ts +17 -0
  95. package/src/cell-formatting.ts +18 -1
  96. package/src/cell-render.test.ts +24 -2
  97. package/src/cell-render.ts +24 -8
  98. package/src/clipboard.test.ts +107 -0
  99. package/src/clipboard.ts +62 -23
  100. package/src/columns.test.ts +4 -4
  101. package/src/columns.ts +7 -2
  102. package/src/conditional-formatting.test.ts +8 -0
  103. package/src/conditional-formatting.ts +3 -0
  104. package/src/core.tick-repair.test.ts +287 -0
  105. package/src/core.ts +374 -26
  106. package/src/editing.test.ts +52 -0
  107. package/src/editing.ts +25 -0
  108. package/src/grid-messages.test.ts +7 -0
  109. package/src/grid-messages.ts +10 -0
  110. package/src/headless.ts +24 -0
  111. package/src/history.test.ts +49 -1
  112. package/src/history.ts +48 -2
  113. package/src/index.ts +2 -0
  114. package/src/keyboard-handlers.coverage.test.ts +65 -0
  115. package/src/keyboard-handlers.ts +26 -1
  116. package/src/keyboard.ts +3 -0
  117. package/src/row-drag-touch.ts +2 -2
  118. package/src/row-drag.test.ts +51 -0
  119. package/src/row-drag.ts +35 -2
  120. package/src/row-model.ts +6 -0
  121. package/src/row-resize.ts +24 -1
  122. package/src/selection.ts +1 -1
  123. package/src/server-block-cache.test.ts +27 -0
  124. package/src/server-block-cache.ts +13 -0
  125. package/src/server-data-source.infinite.test.ts +62 -0
  126. package/src/server-data-source.test.ts +19 -0
  127. package/src/server-data-source.ts +48 -4
  128. package/src/server.ts +2 -0
  129. package/src/sparkline.test.ts +7 -0
  130. package/src/sparkline.ts +6 -4
  131. package/src/svgrid-wrapper.types.ts +32 -0
  132. package/src/svgrid.api.test.ts +21 -0
  133. package/src/svgrid.charting.test.ts +12 -4
  134. package/src/svgrid.context-menu.test.ts +12 -0
  135. package/src/svgrid.detail-rows.svelte.test.ts +187 -0
  136. package/src/svgrid.filter-menu-scroll.test.ts +9 -0
  137. package/src/svgrid.gantt-seam.test.ts +1 -1
  138. package/src/svgrid.live-update-paths.svelte.test.ts +184 -0
  139. package/src/svgrid.menu-scroll-close.test.ts +10 -0
  140. package/src/svgrid.row-model-seam.svelte.test.ts +16 -0
  141. package/src/svgrid.row-pinning.test.ts +15 -1
  142. package/src/svgrid.selection-bar-seam.test.ts +1 -1
  143. package/src/svgrid.sticky-groups.svelte.test.ts +145 -0
  144. package/src/svgrid.upsell-license.test.ts +1 -1
  145. package/src/test-setup.ts +12 -0
  146. package/src/transaction.test.ts +58 -0
  147. package/src/validate.ts +7 -1
  148. package/dist/cdn/GridMenus-DLL-qZeu.js +0 -635
package/README.md CHANGED
@@ -17,7 +17,7 @@
17
17
  <p align="center">
18
18
  <a href="https://svgrid.com">Website</a> ·
19
19
  <a href="https://svgrid.com/docs/">Docs</a> ·
20
- <a href="https://svgrid.com/demos/">370+ Demos</a> ·
20
+ <a href="https://svgrid.com/demos/">400+ Demos</a> ·
21
21
  <a href="https://svgrid.com/pricing/">Pricing</a> ·
22
22
  <a href="https://svgrid.com/roadmap/">Roadmap</a>
23
23
  </p>
@@ -78,7 +78,7 @@ all wire up the moment you turn on the matching prop.
78
78
  |---|---|
79
79
  | **License** | MIT, free for commercial use |
80
80
  | **Requires** | `svelte@^5` (peer dependency), Node 16+ |
81
- | **Bundle (gzip)** | ~2 KB headless core, ~80 KB full `<SvGrid>` + ~9 KB CSS |
81
+ | **Bundle (gzip)** | ~2.7 KB headless core, ~98 KB full `<SvGrid>` + ~10 KB CSS; charts, date pickers, menus and export load as separate chunks on first use |
82
82
  | **Types** | Bundled, no `@types/` package needed |
83
83
  | **Module format** | ESM, plus a CDN build at `@svgrid/grid/cdn` |
84
84
  | **SSR** | Works under SvelteKit SSR and static builds |
@@ -90,7 +90,10 @@ all wire up the moment you turn on the matching prop.
90
90
  - **Filtering** - Excel-style filter menu, inline filter row, set / value-list filter, locale-aware matching, between operators.
91
91
  - **Editing** - 15 built-in `editorType`s (text, number, date, datetime, time, select, rich-select with typeahead, autocomplete, textarea, color, checkbox, list, chips, rating, password), plus `date-native` / `datetime-native` / `time-native` to opt out of the rich pickers, and a `cellEditor` snippet slot for anything custom.
92
92
  - **Selection** - cell-range click+drag and Shift+arrows, copy/paste as TSV, Excel-style fill handle, row selection.
93
- - **Views** - row grouping with aggregation, tree data, master/detail, full-width detail rows, spreadsheet mode with formulas.
93
+ - **Views** - row grouping with aggregation, tree data, master/detail, full-width detail rows, and a
94
+ spreadsheet layout mode (merged cells, per-cell borders). Formula cells here go through an adapter
95
+ seam you point at your own `hyperformula` install; the built-in formula engine and `<SvSheet>` are
96
+ in the Suite edition of `@svgrid/enterprise`.
94
97
  - **Layout** - row + column pinning, sticky header + first column, header drag-to-reorder, autosize, keyboard-accessible column sizing, zebra rows, responsive mode.
95
98
  - **Data operations** - find in grid (Ctrl+F), undo / redo, transaction API, optimistic updates, server-side data with sort / filter / paging / infinite-scroll pushdown (lazy grouping, tree, pivot and transactions on the server are in `@svgrid/enterprise`).
96
99
  - **AI helpers, free** - natural-language filter, smart fill, summarize, classify, anomaly detection, and "chart this". Model-agnostic: register one provider, nothing is bundled.
@@ -131,7 +134,7 @@ import {
131
134
  ```
132
135
 
133
136
  - **Headless** - drive `createSvGrid` and the row-model factories, render your own markup.
134
- `getGrid*A11yProps` helpers keep custom markup accessible. About 2 KB gzipped.
137
+ `getGrid*A11yProps` helpers keep custom markup accessible. About 2.7 KB gzipped.
135
138
  - **Render-ready** - `<SvGrid />` is a complete, accessible, themeable grid you configure with props.
136
139
 
137
140
  Compatibility aliases are provided for the framework-neutral names: `createGrid`, `createGridState`,
@@ -141,7 +144,7 @@ Compatibility aliases are provided for the framework-neutral names: `createGrid`
141
144
 
142
145
  SvGrid publishes an [MCP server](https://www.npmjs.com/package/@svgrid/mcp) plus
143
146
  [llms.txt](https://svgrid.com/llms.txt) and [llms-full.txt](https://svgrid.com/llms-full.txt), so Claude,
144
- Cursor, and Zed answer with accurate, version-pinned APIs and all 370+ demo sources as context rather
147
+ Cursor, and Zed answer with accurate, version-pinned APIs and all 400+ demo sources as context rather
145
148
  than hallucinated methods.
146
149
 
147
150
  ```bash
@@ -158,12 +161,30 @@ editing, virtualization, **server-side data** (paging, infinite block scroll, so
158
161
  **AI helpers** all ship in the free package. The optional [`@svgrid/enterprise`](https://www.npmjs.com/package/@svgrid/enterprise) pack layers
159
162
  on the export- and analytics-heavy features teams tend to need last:
160
163
 
164
+ The pack sells in two editions, both covering unlimited production apps.
165
+
166
+ **Grid edition:**
167
+
161
168
  - **Excel / PDF / CSV / TSV / HTML export** plus paginated print.
162
169
  - **Excel / CSV / TSV / JSON import** with auto-mapping and per-row validation.
163
170
  - **Pivot tables** with a drag-and-drop pivot Designer.
164
171
  - **Kanban board, scheduler / calendar and Gantt renderers** for the `board`, `scheduler` and `gantt` props.
172
+ - **Server-side row model** for lazy grouping, tree, pivot and transactions.
173
+
174
+ **Suite edition** adds everything above plus:
175
+
176
+ - **A real spreadsheet.** `<SvSheet>` and the `<sv-sheet>` element over a built-in formula engine
177
+ with cross-sheet references, named ranges, dynamic arrays and spill, iterative calculation, and
178
+ dependency-graph recalculation. No `hyperformula` needed, and no runtime dependency.
179
+ - **Spreadsheet file formats.** Read and write `.xlsx`, `.xls`, `.ods` and `.csv`, with formulas,
180
+ merges, frozen panes, named ranges and charts surviving the round trip.
181
+ - **SvGrid Studio.** The designer, the code generator and the SQL data sources.
182
+
183
+ Both editions come with a 30-day evaluation period: every feature runs without a key, and under a
184
+ key that does not cover it, with a watermark and a console message about the Enterprise package. `licenseCovers('spreadsheet')` tells you which
185
+ side of the line the current key is on, if you would rather hide an entry point than let it nudge.
165
186
 
166
- OSS projects receive an Enterprise key free. See [Pricing](https://svgrid.com/pricing/).
187
+ OSS projects receive a Suite key free. See [Pricing](https://svgrid.com/pricing/).
167
188
 
168
189
  ## Also in the box
169
190
 
@@ -203,7 +224,7 @@ Full detail: [SvGrid vs AG Grid vs TanStack Table](https://svgrid.com/docs/help/
203
224
  ## Documentation
204
225
 
205
226
  - [Getting started](https://svgrid.com/docs/getting-started/) - end-to-end walkthrough.
206
- - [370+ live demos](https://svgrid.com/demos/) - copy-paste recipes for every feature.
227
+ - [400+ live demos](https://svgrid.com/demos/) - copy-paste recipes for every feature.
207
228
  - [Why headless?](https://svgrid.com/docs/why-headless/) - when to compose vs. drop in.
208
229
  - [Theming and Tailwind](https://svgrid.com/docs/help/tailwind/) - re-skin via `--sg-*` tokens and dark mode.
209
230
  - [Accessibility](https://svgrid.com/docs/help/accessibility/) - the WAI-ARIA grid pattern as implemented.
@@ -214,6 +214,19 @@
214
214
  });
215
215
  }
216
216
 
217
+ /**
218
+ * Whether a column's active filter operator is one of the set operators,
219
+ * read at event time from component state: the condition input's focus
220
+ * and blur handlers cannot depend on a template `@const`, because blur
221
+ * also fires while the popover is being torn down, after that const's
222
+ * effect is gone.
223
+ */
224
+ function isSetOperatorFor(colId: string): boolean {
225
+ const column = allColumns.find((c) => c.id === colId);
226
+ const op = filterMenuValues[colId]?.operator ?? defaultOperatorFor(column);
227
+ return op === "in" || op === "notIn";
228
+ }
229
+
217
230
  /** Enter anywhere in the panel means "done" - filtering is already live. */
218
231
  function onFilterPanelKeydown(event: KeyboardEvent) {
219
232
  if (event.key !== "Enter" || event.shiftKey) return;
@@ -286,8 +299,23 @@
286
299
  const saveComment = $derived(ctrl.saveComment);
287
300
  const removeComment = $derived(ctrl.removeComment);
288
301
  const closeCommentEditor = $derived(ctrl.closeCommentEditor);
302
+
303
+ function onContextMenuKeydown(event: KeyboardEvent) {
304
+ // Escape closes the context menu, the way every spreadsheet and desktop
305
+ // app does. The backdrop closes it on a click, but a keyboard user who
306
+ // opened it (Shift+F10, the Menu key) otherwise has no way out, and the
307
+ // menu sits over the sheet swallowing every other key. Handled at the
308
+ // window so it works whatever holds focus while the menu is open.
309
+ if (contextMenuFor && (event.key === "Escape" || event.key === "Esc")) {
310
+ event.preventDefault();
311
+ event.stopPropagation();
312
+ closeContextMenu();
313
+ }
314
+ }
289
315
  </script>
290
316
 
317
+ <svelte:window onkeydown={onContextMenuKeydown} />
318
+
291
319
  {#if tooltip}
292
320
  <!-- Custom hover tooltip. Floats above the grid; positioned by
293
321
  showTooltipFor(). Notes and column tooltips both surface here. -->
@@ -396,10 +424,12 @@
396
424
  : menuActiveOperator === "regex"
397
425
  ? "Pattern..."
398
426
  : "Filter value..."}
399
- onfocus={isSetOp
400
- ? (event) => onSetOpFilterFocus(event.currentTarget as HTMLInputElement, colId)
401
- : undefined}
402
- onblur={isSetOp ? () => closeInSuggest() : undefined}
427
+ onfocus={(event) => {
428
+ if (isSetOperatorFor(colId)) onSetOpFilterFocus(event.currentTarget as HTMLInputElement, colId);
429
+ }}
430
+ onblur={() => {
431
+ if (isSetOperatorFor(colId)) closeInSuggest();
432
+ }}
403
433
  oninput={(event) => {
404
434
  const input = event.currentTarget as HTMLInputElement;
405
435
  updateFilterMenuValue(colId, input.value);
@@ -78,6 +78,7 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
78
78
  history: SharedHistoryStep[];
79
79
  historyPtr: number;
80
80
  historyGroupId: string | undefined;
81
+ historyTag: unknown;
81
82
  historyVersion: number;
82
83
  tooltip: {
83
84
  text: string;
@@ -116,6 +117,14 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
116
117
  columnVirtualizerVersion: number;
117
118
  gridStateVersion: number;
118
119
  readonly selectionColumnWidth: number;
120
+ readonly detailToggleColumnWidth: number;
121
+ readonly detailToggleColumnLeft: number;
122
+ readonly showDetailToggleEffective: boolean;
123
+ readonly systemColumnsWidth: number;
124
+ readonly detailToggleOf: (row: Row<TData>, rowIndex: number) => {
125
+ open: boolean;
126
+ toggle: () => void;
127
+ } | null;
119
128
  readonly rowNumberColumnWidth: number;
120
129
  readonly showRowNumbersEffective: boolean;
121
130
  readonly filterOperatorOptions: import("./SvGrid.types.js").FilterOption[];
@@ -241,7 +250,7 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
241
250
  colDropSide: "before" | "after" | null;
242
251
  rowDragActive: boolean;
243
252
  rowDropIndex: number | null;
244
- rowDropSide: "before" | "after" | null;
253
+ rowDropSide: "before" | "after" | "into" | null;
245
254
  readonly onRowDragStart: (e: DragEvent, rowIndex: number) => void;
246
255
  readonly onRowDragOver: (e: DragEvent, rowIndex: number) => void;
247
256
  readonly onRowDragLeave: (rowIndex: number) => void;
@@ -525,6 +534,13 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
525
534
  readonly frozenRowList: Row<TData>[];
526
535
  readonly frozenBandHeight: number;
527
536
  readonly frozenRowTop: (i: number) => number;
537
+ readonly stickyGroupRows: {
538
+ row: Row<TData>;
539
+ rowIndex: number;
540
+ top: number;
541
+ size: number;
542
+ }[];
543
+ readonly stickyTopOfRow: (rowIndex: number) => number | undefined;
528
544
  /** Record the height the user dragged a row to. */
529
545
  readonly setRowResizeHeight: (index: number, height: number | null) => void;
530
546
  readonly isRowCollapsed: (index: number) => boolean;
@@ -534,6 +550,8 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
534
550
  readonly autosizeColumnByUser: (columnId: string) => void;
535
551
  /** A row's dragged height, or undefined when it has not been resized. */
536
552
  readonly rowResizeHeightPx: (index: number) => number | undefined;
553
+ /** The height row i is drawn at: dragged, measured, detail, declared or default. */
554
+ readonly rowSizePxOf: (i: number) => number;
537
555
  readonly virtualRowTotalSize: number;
538
556
  readonly virtualRowStart: number;
539
557
  readonly virtualRowEnd: number;
@@ -672,11 +690,11 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
672
690
  columnResizable(columnId: string): boolean;
673
691
  readonly readCellRaw: (rowIndex: number, columnId: string) => unknown;
674
692
  readonly writeCellRaw: (rowIndex: number, columnId: string, value: unknown) => void;
675
- readonly applyFillPattern: () => void;
693
+ readonly applyFillPattern: (toggled?: boolean) => void;
676
694
  readonly clearSelectedCellValues: () => void;
677
695
  readonly startFillDrag: (event: PointerEvent, rowIndex: number, colIndex: number) => void;
678
696
  readonly onFillPointerMove: (event: PointerEvent) => void;
679
- readonly onFillPointerUp: () => void;
697
+ readonly onFillPointerUp: (event?: PointerEvent) => void;
680
698
  readonly fillDownToNeighbour: () => void;
681
699
  readonly isOnMoveGrabStrip: (cell: HTMLElement, rowIndex: number, colIndex: number, clientX: number, clientY: number) => boolean;
682
700
  readonly startMoveDrag: (event: PointerEvent, rowIndex: number, colIndex: number) => boolean;
@@ -327,7 +327,11 @@ export function createSvGridController(rawProps, domIdBase) {
327
327
  // grows with rows x columns and a Map lookup per cell per render.
328
328
  let editedCellValues = $state.raw({});
329
329
  const UNDO_LIMIT = 200;
330
- let history = $state([]);
330
+ // Raw: pushHistory always assigns a fresh array and nothing renders from a
331
+ // step, so deep proxies would cost 200 wrapped objects for nothing. It also
332
+ // keeps a consumer's `tag` the object it passed, which a proxy would not be:
333
+ // the sheet shell compares tags by identity.
334
+ let history = $state.raw([]);
331
335
  /** Index in `history` of the LAST applied step. -1 means "nothing applied".
332
336
  * undo() decrements; redo() increments. New edits truncate everything
333
337
  * past the pointer (the classic "you can't redo after editing" rule). */
@@ -340,6 +344,9 @@ export function createSvGridController(rawProps, domIdBase) {
340
344
  * $state: it is written and read synchronously inside a single call and
341
345
  * nothing renders from it. */
342
346
  let historyGroupId = undefined;
347
+ /** The consumer's mark for the steps recorded from now on (`api.setHistoryTag`);
348
+ * not $state for the same reason. */
349
+ let historyTag = undefined;
343
350
  let tooltip = $state(null);
344
351
  let tooltipTimer = null;
345
352
  // ---- Find-in-grid ----------------------------------------------------
@@ -410,7 +417,28 @@ export function createSvGridController(rawProps, domIdBase) {
410
417
  // catch-all gridStateVersion, so navigation doesn't rebuild 1M rows.
411
418
  let dataStateVersion = $state(0);
412
419
  const selectionColumnWidth = 44;
420
+ const detailToggleColumnWidth = 36;
413
421
  const rowNumberColumnWidth = $derived(props.rowNumberWidth ?? 56);
422
+ const showDetailToggleEffective = $derived(props.showDetailToggle === true);
423
+ /**
424
+ * What the chevron on a row does, or null when the row has no detail to
425
+ * open: a detail row itself, a group or placeholder on a server model, a
426
+ * row `hasDetail` rules out, or a grid with nothing to call.
427
+ */
428
+ function detailToggleOf(row, rowIndex) {
429
+ const data = row.original;
430
+ if (props.isDetailRow?.(data, rowIndex))
431
+ return null;
432
+ const sg = props.serverGroup;
433
+ if (sg) {
434
+ if (!sg.toggleDetail || sg.isGroup(data) || (sg.hasDetail && !sg.hasDetail(data)))
435
+ return null;
436
+ return { open: sg.detailOpen?.(data) ?? false, toggle: () => sg.toggleDetail(data) };
437
+ }
438
+ if (!props.onDetailToggle || (props.hasDetail && !props.hasDetail(data)))
439
+ return null;
440
+ return { open: props.isDetailOpen?.(data) ?? false, toggle: () => props.onDetailToggle(data, rowIndex) };
441
+ }
414
442
  // A per-row `rowHeight` function already supplies heights, so auto-measuring
415
443
  // would fight it. Fixed numbers are fine - they become the pre-measure estimate.
416
444
  const autoRowHeightOn = $derived(props.autoRowHeight === true && typeof props.rowHeight !== "function");
@@ -563,6 +591,12 @@ export function createSvGridController(rawProps, domIdBase) {
563
591
  const showRowSelectionEffective = $derived(props.showRowSelection ??
564
592
  ((props.selectionMode ?? "both") === "row" ||
565
593
  (props.selectionMode ?? "both") === "both"));
594
+ /** The system columns to the left of the data: row numbers, selection, the detail toggle. */
595
+ const systemColumnsWidth = $derived((showRowNumbersEffective ? rowNumberColumnWidth : 0) +
596
+ (showRowSelectionEffective ? selectionColumnWidth : 0) +
597
+ (showDetailToggleEffective ? detailToggleColumnWidth : 0));
598
+ /** Where the detail-toggle column sticks: after the row numbers and the selection column. */
599
+ const detailToggleColumnLeft = $derived((showRowNumbersEffective ? rowNumberColumnWidth : 0) + (showRowSelectionEffective ? selectionColumnWidth : 0));
566
600
  // `selectable` is the shortcut alias, so it wins over the fine-grained prop,
567
601
  // which in turn wins over `selectionMode` - the same precedence the other
568
602
  // shortcuts use. Unlike them it starts ON (selectionMode defaults to 'both'),
@@ -661,35 +695,41 @@ export function createSvGridController(rawProps, domIdBase) {
661
695
  // the row-model pipeline is read before any later prop change could apply.
662
696
  // svelte-ignore state_referenced_locally
663
697
  const treeDataConfig = props.treeData;
698
+ // Pagination is intentionally NOT in the grid's row-model pipeline. The
699
+ // wrapper applies its own filters (filterMenuValues, globalFilter,
700
+ // valueFilters) on top of `grid.getRowModel().rows`. If pagination ran
701
+ // first, those filters would only see the visible page. Instead the wrapper
702
+ // paginates last - see `allRows` below.
703
+ //
704
+ // Built ONCE. Both inputs are captured at mount, and the stages keep state
705
+ // between runs: the sorted and filtered stages hold their last output so a
706
+ // data tick can repair it instead of recomputing (core.ts, `replacedRowsOf`).
707
+ // This used to be a getter that built fresh stages on every read, which
708
+ // threw that cache away on each run - a live feed re-sorted 100k rows on
709
+ // every tick and the row-model memo's `cachedPipeline` never matched.
710
+ const rowModels = {
711
+ coreRowModel: createCoreRowModel(),
712
+ filteredRowModel: createFilteredRowModel(),
713
+ // External-sort mode: pass the rows through untouched so the consumer
714
+ // controls ordering (e.g. tree data that must preserve hierarchy).
715
+ sortedRowModel: externalSortEnabled
716
+ ? passthroughSortedRowModel
717
+ : createSortedRowModel(sortFns),
718
+ // Tree nesting replaces grouping when `treeData` is set - a row cannot
719
+ // be both a hierarchy node and bucketed under a group banner.
720
+ groupedRowModel: treeDataConfig
721
+ ? createTreeRowModel({
722
+ parentField: treeDataConfig.parentField,
723
+ idField: treeDataConfig.idField,
724
+ })
725
+ : createGroupedRowModel(),
726
+ expandedRowModel: createExpandedRowModel(),
727
+ };
664
728
  const grid = createSvGrid({
665
729
  get _features() {
666
730
  return resolveEffectiveFeatures();
667
731
  },
668
- get _rowModels() {
669
- // Pagination is intentionally NOT in the grid's row-model pipeline.
670
- // The wrapper applies its own filters (filterMenuValues, globalFilter,
671
- // valueFilters) on top of `grid.getRowModel().rows`. If pagination
672
- // ran first, those filters would only see the visible page. Instead
673
- // the wrapper paginates last - see `allRows` below.
674
- return {
675
- coreRowModel: createCoreRowModel(),
676
- filteredRowModel: createFilteredRowModel(),
677
- // External-sort mode: pass the rows through untouched so the consumer
678
- // controls ordering (e.g. tree data that must preserve hierarchy).
679
- sortedRowModel: externalSortEnabled
680
- ? passthroughSortedRowModel
681
- : createSortedRowModel(sortFns),
682
- // Tree nesting replaces grouping when `treeData` is set - a row cannot
683
- // be both a hierarchy node and bucketed under a group banner.
684
- groupedRowModel: treeDataConfig
685
- ? createTreeRowModel({
686
- parentField: treeDataConfig.parentField,
687
- idField: treeDataConfig.idField,
688
- })
689
- : createGroupedRowModel(),
690
- expandedRowModel: createExpandedRowModel(),
691
- };
692
- },
732
+ _rowModels: rowModels,
693
733
  get columns() {
694
734
  return internalColumns;
695
735
  },
@@ -1091,10 +1131,8 @@ export function createSvGridController(rawProps, domIdBase) {
1091
1131
  });
1092
1132
  /** Cumulative pixel offsets for left- and right-pinned columns. */
1093
1133
  const pinnedOffsets = $derived.by(function pinnedOffsets_d() {
1094
- const rowNumberWidth = showRowNumbersEffective ? rowNumberColumnWidth : 0;
1095
- const selectionWidth = showRowSelectionEffective ? selectionColumnWidth : 0;
1096
1134
  const left = {};
1097
- let leftAcc = rowNumberWidth + selectionWidth;
1135
+ let leftAcc = systemColumnsWidth;
1098
1136
  for (const id of effectivePinning.left) {
1099
1137
  // Hidden pinned columns don't render, so they must contribute neither an
1100
1138
  // offset entry nor width - otherwise the next visible pinned column is
@@ -1611,16 +1649,13 @@ export function createSvGridController(rawProps, domIdBase) {
1611
1649
  // so consumers don't see spurious callbacks during re-renders.
1612
1650
  let lastCellRangeSerialized = "";
1613
1651
  $effect(() => {
1614
- const a = selectionRange.anchor;
1615
- const f = selectionRange.focus;
1616
- const ranges = a && f
1617
- ? [[
1618
- Math.min(a.rowIndex, f.rowIndex),
1619
- Math.min(a.colIndex, f.colIndex),
1620
- Math.max(a.rowIndex, f.rowIndex),
1621
- Math.max(a.colIndex, f.colIndex),
1622
- ]]
1623
- : [];
1652
+ // Every rectangle, the committed ones a Ctrl+click added and the
1653
+ // active one last, the order getSelectionRects gives: a consumer's
1654
+ // status bar and shading cover the whole selection, not only its last
1655
+ // range. Depend on selectionRanges so a change to the committed set
1656
+ // re-runs this too.
1657
+ void selectionRanges;
1658
+ const ranges = getSelectionRects().map((r) => [r.minRow, r.minCol, r.maxRow, r.maxCol]);
1624
1659
  const serialized = JSON.stringify(ranges);
1625
1660
  if (serialized === lastCellRangeSerialized)
1626
1661
  return;
@@ -2651,6 +2686,16 @@ export function createSvGridController(rawProps, domIdBase) {
2651
2686
  return total;
2652
2687
  });
2653
2688
  /** The declared or dragged height of one row; measured under auto height. */
2689
+ /** A detail row's declared height, or null when the row is not one (or none is declared). */
2690
+ function detailRowHeightPx(i) {
2691
+ const dh = props.detailRowHeight;
2692
+ if (dh == null || !props.isDetailRow)
2693
+ return null;
2694
+ const row = allRows[i];
2695
+ if (!row || !props.isDetailRow(row.original, i))
2696
+ return null;
2697
+ return typeof dh === "function" ? dh(row.original) : dh;
2698
+ }
2654
2699
  function rowSizePxOf(i) {
2655
2700
  if (isRowCollapsed(i))
2656
2701
  return 0;
@@ -2659,6 +2704,9 @@ export function createSvGridController(rawProps, domIdBase) {
2659
2704
  const own = rowResizeHeightPx(i);
2660
2705
  if (own != null)
2661
2706
  return own;
2707
+ const detail = detailRowHeightPx(i);
2708
+ if (detail != null)
2709
+ return detail;
2662
2710
  const rh = props.rowHeight;
2663
2711
  return typeof rh === "function" ? rh(i) : (rh ?? 30);
2664
2712
  }
@@ -2669,10 +2717,91 @@ export function createSvGridController(rawProps, domIdBase) {
2669
2717
  top += rowSizePxOf(k);
2670
2718
  return top;
2671
2719
  }
2720
+ const NO_STICKY = [];
2721
+ /** Whether a row is a group and how deep it sits, for any of the three tree sources. */
2722
+ function rowGroupLevel(row) {
2723
+ const sg = props.serverGroup;
2724
+ if (sg) {
2725
+ const data = row.original;
2726
+ return { group: sg.isGroup(data), level: sg.level(data) };
2727
+ }
2728
+ // Client grouping and tree data: an expandable row is the group (a tree
2729
+ // row is a data row too, which is why isGroupRow() excludes it, but for
2730
+ // the band it is the parent its children scroll under).
2731
+ if (typeof row.getCanExpand === "function" && row.getCanExpand())
2732
+ return { group: true, level: row.depth ?? 0 };
2733
+ return { group: false, level: row.depth ?? 0 };
2734
+ }
2735
+ /** The first row whose bottom edge is under the header, in row-index space. */
2736
+ const firstRowUnderHeader = $derived.by(function firstRowUnderHeader_d() {
2737
+ if (allRows.length === 0)
2738
+ return -1;
2739
+ if (rowVirtualizationEnabled) {
2740
+ virtualizer.version;
2741
+ const offset = virtualizer.getState().scrollOffset;
2742
+ const hit = virtualRows.find((it) => it.end > offset);
2743
+ return hit ? hit.index : -1;
2744
+ }
2745
+ rowResizeVersion;
2746
+ autoRowHeightVersion;
2747
+ const top = scrollMetrics.scrollTop;
2748
+ let acc = 0;
2749
+ let i = 0;
2750
+ while (i < allRows.length - 1 && acc + rowSizePxOf(i) <= top) {
2751
+ acc += rowSizePxOf(i);
2752
+ i += 1;
2753
+ }
2754
+ return i;
2755
+ });
2756
+ const stickyGroupRows = $derived.by(function stickyGroupRows_d() {
2757
+ if (!props.stickyGroupRows)
2758
+ return NO_STICKY;
2759
+ const first = firstRowUnderHeader;
2760
+ if (first < 0)
2761
+ return NO_STICKY;
2762
+ const head = allRows[first];
2763
+ if (!head)
2764
+ return NO_STICKY;
2765
+ // A group row at level L, or a leaf at level L, has its ancestors at
2766
+ // levels L-1 .. 0, each the nearest group row above at that level.
2767
+ let want = rowGroupLevel(head).level - 1;
2768
+ if (want < 0)
2769
+ return NO_STICKY;
2770
+ const found = [];
2771
+ for (let i = first - 1; i >= frozenRowCount && want >= 0; i -= 1) {
2772
+ const row = allRows[i];
2773
+ const info = rowGroupLevel(row);
2774
+ if (info.group && info.level === want) {
2775
+ found.push({ row, rowIndex: i });
2776
+ want -= 1;
2777
+ }
2778
+ }
2779
+ if (found.length === 0)
2780
+ return NO_STICKY;
2781
+ found.reverse();
2782
+ rowResizeVersion;
2783
+ autoRowHeightVersion;
2784
+ let top = headerHeight + frozenBandHeight;
2785
+ return found.map((f) => {
2786
+ const at = top;
2787
+ const size = rowSizePxOf(f.rowIndex);
2788
+ top += size;
2789
+ return { ...f, top: at, size };
2790
+ });
2791
+ });
2792
+ /** The band's height under virtualization, taken off the top spacer. */
2793
+ const stickyBandHeight = $derived(rowVirtualizationEnabled ? stickyGroupRows.reduce((sum, s) => sum + s.size, 0) : 0);
2794
+ /** Without virtualization the real row sticks: its top, or undefined. */
2795
+ function stickyTopOfRow(rowIndex) {
2796
+ if (rowVirtualizationEnabled || stickyGroupRows.length === 0)
2797
+ return undefined;
2798
+ return stickyGroupRows.find((s) => s.rowIndex === rowIndex)?.top;
2799
+ }
2672
2800
  // Spacer heights in DOM space. With scaling inert these equal the original
2673
- // virtualRowStart / virtualRowBottomSpacer. The frozen band is in the flow
2674
- // before the spacer whatever the window shows, so it comes off the spacer.
2675
- const rowTopSpacer = $derived(Math.max(virtualRowStart - rowOffsetAdjustment - frozenBandHeight, 0));
2801
+ // virtualRowStart / virtualRowBottomSpacer. The frozen band and the sticky
2802
+ // group band are in the flow before the spacer whatever the window shows,
2803
+ // so they come off the spacer.
2804
+ const rowTopSpacer = $derived(Math.max(virtualRowStart - rowOffsetAdjustment - frozenBandHeight - stickyBandHeight, 0));
2676
2805
  const rowBottomSpacer = $derived(Math.max(rowDomTotalSize - (virtualRowEnd - rowOffsetAdjustment), 0));
2677
2806
  const virtualColumns = $derived.by(function virtualColumns_d() {
2678
2807
  columnVirtualizerVersion;
@@ -2757,9 +2886,7 @@ export function createSvGridController(rawProps, domIdBase) {
2757
2886
  * `fittedColumnWidths`, so the overflow decision settles in the same
2758
2887
  * render where fit-scaling lands - no race, no scrollbar flash. */
2759
2888
  const hasHorizontalOverflow = $derived.by(function hasHorizontalOverflow_d() {
2760
- const fixedCols = (showRowNumbersEffective ? rowNumberColumnWidth : 0) +
2761
- (showRowSelectionEffective ? selectionColumnWidth : 0);
2762
- let total = fixedCols;
2889
+ let total = systemColumnsWidth;
2763
2890
  for (const column of allColumns)
2764
2891
  total += getColumnWidth(column.id);
2765
2892
  // +1 to tolerate sub-pixel rounding residue from `fitColumns`.
@@ -3208,8 +3335,12 @@ export function createSvGridController(rawProps, domIdBase) {
3208
3335
  : typeof rh === "function"
3209
3336
  ? rh
3210
3337
  : (rh ?? 30);
3211
- const estimateSize = rowResizeHeights.size > 0 || collapsedRows.size > 0
3338
+ // Detail rows have their own height; only when the props say so does
3339
+ // the per-index lookup replace the uniform fast path.
3340
+ const detailOn = props.detailRowHeight != null && !!props.isDetailRow;
3341
+ const estimateSize = rowResizeHeights.size > 0 || collapsedRows.size > 0 || detailOn
3212
3342
  ? (index) => rowResizeHeightPx(index) ??
3343
+ detailRowHeightPx(index) ??
3213
3344
  (typeof base === "function" ? base(index) : base)
3214
3345
  : base;
3215
3346
  virtualizer.setOptions({
@@ -3320,6 +3451,18 @@ export function createSvGridController(rawProps, domIdBase) {
3320
3451
  // onto a server.
3321
3452
  if (props.rowSelectionModel)
3322
3453
  return props.rowSelectionModel.headerState();
3454
+ // Nothing checked is the common case and needs no walk: this derived
3455
+ // re-runs on every data change, and on a 100k-row live feed the filter
3456
+ // below was 30 ms per tick spent to conclude "none".
3457
+ let anyChecked = false;
3458
+ for (const id in rowSelectionState) {
3459
+ if (rowSelectionState[id]) {
3460
+ anyChecked = true;
3461
+ break;
3462
+ }
3463
+ }
3464
+ if (!anyChecked)
3465
+ return "none";
3323
3466
  const selectable = allRows.filter((row) => !isGroupRow(row) && !placeholderStateOf(row));
3324
3467
  if (!selectable.length)
3325
3468
  return "none";
@@ -3367,8 +3510,6 @@ export function createSvGridController(rawProps, domIdBase) {
3367
3510
  const cols = grid.getAllColumns().filter((c) => !hiddenColumns[c.id] && !hiddenByGroupCollapse[c.id] && !collapsedColumns[c.id]);
3368
3511
  if (!cols.length)
3369
3512
  return null;
3370
- const rowNumberWidth = showRowNumbersEffective ? rowNumberColumnWidth : 0;
3371
- const selectionWidth = showRowSelectionEffective ? selectionColumnWidth : 0;
3372
3513
  // Reserve the custom vertical scrollbar's width when it's visible. It
3373
3514
  // overlays the right 16px of the viewport (absolute, z-index 40) and
3374
3515
  // does NOT shrink clientWidth, so without this the last fitted column
@@ -3376,8 +3517,7 @@ export function createSvGridController(rawProps, domIdBase) {
3376
3517
  // is hidden behind the opaque scrollbar.
3377
3518
  const scrollbarWidth = hasVerticalOverflow ? 16 : 0;
3378
3519
  const target = (scrollContainer?.clientWidth ?? 0) -
3379
- rowNumberWidth -
3380
- selectionWidth -
3520
+ systemColumnsWidth -
3381
3521
  scrollbarWidth;
3382
3522
  if (target <= 0)
3383
3523
  return null;
@@ -3779,6 +3919,8 @@ export function createSvGridController(rawProps, domIdBase) {
3779
3919
  set historyPtr(v) { historyPtr = v; },
3780
3920
  get historyGroupId() { return historyGroupId; },
3781
3921
  set historyGroupId(v) { historyGroupId = v; },
3922
+ get historyTag() { return historyTag; },
3923
+ set historyTag(v) { historyTag = v; },
3782
3924
  get historyVersion() { return historyVersion; },
3783
3925
  set historyVersion(v) { historyVersion = v; },
3784
3926
  get tooltip() { return tooltip; },
@@ -3815,6 +3957,11 @@ export function createSvGridController(rawProps, domIdBase) {
3815
3957
  get gridStateVersion() { return gridStateVersion; },
3816
3958
  set gridStateVersion(v) { gridStateVersion = v; },
3817
3959
  get selectionColumnWidth() { return selectionColumnWidth; },
3960
+ get detailToggleColumnWidth() { return detailToggleColumnWidth; },
3961
+ get detailToggleColumnLeft() { return detailToggleColumnLeft; },
3962
+ get showDetailToggleEffective() { return showDetailToggleEffective; },
3963
+ get systemColumnsWidth() { return systemColumnsWidth; },
3964
+ get detailToggleOf() { return detailToggleOf; },
3818
3965
  get rowNumberColumnWidth() { return rowNumberColumnWidth; },
3819
3966
  get showRowNumbersEffective() { return showRowNumbersEffective; },
3820
3967
  get filterOperatorOptions() { return filterOperatorOptions; },
@@ -4234,6 +4381,8 @@ export function createSvGridController(rawProps, domIdBase) {
4234
4381
  get frozenRowList() { return frozenRowList; },
4235
4382
  get frozenBandHeight() { return frozenBandHeight; },
4236
4383
  get frozenRowTop() { return frozenRowTop; },
4384
+ get stickyGroupRows() { return stickyGroupRows; },
4385
+ get stickyTopOfRow() { return stickyTopOfRow; },
4237
4386
  /** Record the height the user dragged a row to. */
4238
4387
  get setRowResizeHeight() { return setRowResizeHeight; },
4239
4388
  get isRowCollapsed() { return isRowCollapsed; },
@@ -4246,6 +4395,12 @@ export function createSvGridController(rawProps, domIdBase) {
4246
4395
  rowResizeVersion;
4247
4396
  return rowResizeHeightPx;
4248
4397
  },
4398
+ /** The height row i is drawn at: dragged, measured, detail, declared or default. */
4399
+ get rowSizePxOf() {
4400
+ rowResizeVersion;
4401
+ autoRowHeightVersion;
4402
+ return rowSizePxOf;
4403
+ },
4249
4404
  get virtualRowTotalSize() { return virtualRowTotalSize; },
4250
4405
  get virtualRowStart() { return virtualRowStart; },
4251
4406
  get virtualRowEnd() { return virtualRowEnd; },