@svgrid/grid 3.0.5 → 3.0.7

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 (166) hide show
  1. package/README.md +28 -7
  2. package/dist/GridMenus.svelte +34 -4
  3. package/dist/SvGrid.controller.svelte.d.ts +23 -3
  4. package/dist/SvGrid.controller.svelte.js +235 -54
  5. package/dist/SvGrid.css +143 -7
  6. package/dist/SvGrid.helpers.js +2 -4
  7. package/dist/SvGrid.svelte +223 -38
  8. package/dist/SvGrid.types.d.ts +77 -4
  9. package/dist/SvGridDropdown.svelte +20 -0
  10. package/dist/SvGridDropdown.svelte.d.ts +12 -0
  11. package/dist/SvModal.svelte +22 -0
  12. package/dist/SvModal.svelte.d.ts +7 -0
  13. package/dist/a11y/dismissable.d.ts +10 -0
  14. package/dist/a11y/dismissable.js +24 -2
  15. package/dist/build-api.js +81 -31
  16. package/dist/cdn/{GridMenus-CID62rlI.js → GridMenus-BgUS-W3o.js} +151 -142
  17. package/dist/cdn/GridMenus-CcwU8I4t.js +644 -0
  18. package/dist/cdn/{SvChartMenu-FBSMINA6.js → SvChartMenu-BFSTpvB-.js} +1 -1
  19. package/dist/cdn/{SvChartMenu-Bl6PBbkT.js → SvChartMenu-BZ1Iwv0y.js} +1 -1
  20. package/dist/cdn/{SvDateTimePicker-DQwt4UAs.js → SvDateTimePicker-D9u_cOf8.js} +1 -1
  21. package/dist/cdn/{SvDateTimePicker-vNU6bZ-q.js → SvDateTimePicker-s_6IUpI7.js} +1 -1
  22. package/dist/cdn/{SvGridCellEditor-B1p-vCK5.js → SvGridCellEditor-9yz-SbdZ.js} +2 -2
  23. package/dist/cdn/{SvGridCellEditor-D_0q4xAu.js → SvGridCellEditor-D7-ql1Hd.js} +2 -2
  24. package/dist/cdn/{SvGridChart-C3EWAZaM.js → SvGridChart-DMUOFxOe.js} +4 -4
  25. package/dist/cdn/{SvGridChart-DzLkSwCH.js → SvGridChart-N7ik0oWy.js} +4 -4
  26. package/dist/cdn/{SvGridChartBuilder-CfII62sZ.js → SvGridChartBuilder-CB_Jnm2k.js} +4 -4
  27. package/dist/cdn/{SvGridChartBuilder-BE2T1ykB.js → SvGridChartBuilder-Ca7gF8ZK.js} +4 -4
  28. package/dist/cdn/{SvGridChartPanel-xf4sWVTo.js → SvGridChartPanel-9GOJRHP7.js} +5 -5
  29. package/dist/cdn/{SvGridChartPanel-CCX5_Wgd.js → SvGridChartPanel-Dl-FujN8.js} +5 -5
  30. package/dist/cdn/{SvGridChartView-CfuXmY5I.js → SvGridChartView-B1LFgoPP.js} +2 -2
  31. package/dist/cdn/{SvGridChartView-DYwabQWj.js → SvGridChartView-By3n7DGZ.js} +2 -2
  32. package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-BWeM3lMe.js} +46 -46
  33. package/dist/cdn/{SvGridDropdown-D13MtJ2j.js → SvGridDropdown-CJ6QqTxu.js} +67 -67
  34. package/dist/cdn/{SvModal-CIZWCcad.js → SvModal-ClxGmpAc.js} +187 -186
  35. package/dist/cdn/{SvModal-BAE-pjZX.js → SvModal-DhB_VMFR.js} +148 -147
  36. package/dist/cdn/{cell-formatting-C2Hf5gqd.js → cell-formatting-Mnv84zoE.js} +28 -6
  37. package/dist/cdn/{chart-Dz7SqMXH.js → chart-By7h2Guv.js} +1 -1
  38. package/dist/cdn/{chart-panel-messages-CUbf2R4i.js → chart-panel-messages-BC-tOgFY.js} +1 -1
  39. package/dist/cdn/{chart-panel-messages-CmNrMdsr.js → chart-panel-messages-DUaXr-sb.js} +1 -1
  40. package/dist/cdn/{chart-summary-BJW_tg_X.js → chart-summary-BDtrNEiA.js} +1 -1
  41. package/dist/cdn/{chart-trend-CaN9mDEV.js → chart-trend-C1aOAPBZ.js} +1 -1
  42. package/dist/cdn/{dismissable-DAHetSNk.js → dismissable-Brc4EZU1.js} +5 -1
  43. package/dist/cdn/{editor-registry-BhgE3S84.js → editor-registry-BVeHvlZR.js} +49 -50
  44. package/dist/cdn/{export-format-CUDLy2yn.js → export-format-y1wdyCcv.js} +93 -97
  45. package/dist/cdn/{row-resize-niQCp040.js → row-resize-DtOS1KG6.js} +41 -32
  46. package/dist/cdn/{server-block-cache-CGoWz-87.js → server-block-cache-DIqb3VZn.js} +4 -0
  47. package/dist/cdn/{src-jcFMkBnN.js → src-25JmJ_7g.js} +4768 -4149
  48. package/dist/cdn/{src-Bklofsad.js → src-kE9B98r6.js} +4796 -4177
  49. package/dist/cdn/svgrid.js +18 -18
  50. package/dist/cdn/svgrid.svelte-external.js +18 -18
  51. package/dist/cdn/{validate-C7Rn_Bns.js → validate-AZoD8BoR.js} +6 -2
  52. package/dist/cell-formatting.d.ts +9 -0
  53. package/dist/cell-formatting.js +52 -1
  54. package/dist/cell-render.js +25 -6
  55. package/dist/clipboard.d.ts +2 -2
  56. package/dist/clipboard.js +72 -23
  57. package/dist/columns.js +7 -2
  58. package/dist/conditional-formatting.d.ts +2 -0
  59. package/dist/conditional-formatting.js +2 -0
  60. package/dist/core.d.ts +22 -6
  61. package/dist/core.js +478 -40
  62. package/dist/editing.js +24 -0
  63. package/dist/export-format.d.ts +8 -0
  64. package/dist/export-format.js +23 -16
  65. package/dist/grid-messages.d.ts +4 -0
  66. package/dist/grid-messages.js +4 -0
  67. package/dist/headless.d.ts +5 -0
  68. package/dist/headless.js +12 -0
  69. package/dist/history.d.ts +21 -0
  70. package/dist/history.js +34 -4
  71. package/dist/index.d.ts +1 -1
  72. package/dist/keyboard-handlers.js +24 -1
  73. package/dist/keyboard.js +4 -0
  74. package/dist/row-drag-touch.d.ts +1 -1
  75. package/dist/row-drag.js +35 -1
  76. package/dist/row-model.d.ts +6 -0
  77. package/dist/row-resize.js +22 -1
  78. package/dist/selection.js +1 -1
  79. package/dist/server-block-cache.d.ts +5 -0
  80. package/dist/server-block-cache.js +8 -0
  81. package/dist/server-data-source.d.ts +35 -3
  82. package/dist/server-data-source.js +11 -2
  83. package/dist/server.d.ts +1 -1
  84. package/dist/sparkline.js +7 -5
  85. package/dist/svgrid-wrapper.types.d.ts +33 -0
  86. package/dist/validate.js +9 -1
  87. package/dist/virtualization/column-virtualizer.d.ts +1 -0
  88. package/dist/virtualization/svelte-virtualizer.svelte.d.ts +1 -0
  89. package/dist/virtualization/svelte-virtualizer.svelte.js +1 -0
  90. package/dist/virtualization/virtualizer.d.ts +3 -0
  91. package/dist/virtualization/virtualizer.js +14 -1
  92. package/package.json +6 -1
  93. package/src/GridMenus.svelte +34 -4
  94. package/src/SvGrid.controller.svelte.ts +236 -57
  95. package/src/SvGrid.css +143 -7
  96. package/src/SvGrid.helpers.ts +2 -4
  97. package/src/SvGrid.svelte +223 -38
  98. package/src/SvGrid.types.ts +77 -4
  99. package/src/SvGridDropdown.svelte +20 -0
  100. package/src/SvModal.svelte +22 -0
  101. package/src/SvModal.test.ts +30 -1
  102. package/src/a11y/dismissable.test.ts +10 -1
  103. package/src/a11y/dismissable.ts +22 -2
  104. package/src/build-api.ts +85 -27
  105. package/src/cell-formatting.test.ts +17 -0
  106. package/src/cell-formatting.ts +49 -1
  107. package/src/cell-render.test.ts +24 -2
  108. package/src/cell-render.ts +24 -8
  109. package/src/clipboard.test.ts +107 -0
  110. package/src/clipboard.ts +62 -23
  111. package/src/columns.test.ts +4 -4
  112. package/src/columns.ts +7 -2
  113. package/src/conditional-formatting.test.ts +8 -0
  114. package/src/conditional-formatting.ts +3 -0
  115. package/src/core.sort.test.ts +86 -0
  116. package/src/core.tick-repair.test.ts +287 -0
  117. package/src/core.ts +478 -40
  118. package/src/editing.test.ts +52 -0
  119. package/src/editing.ts +25 -0
  120. package/src/export-dates.test.ts +114 -0
  121. package/src/export-format.ts +26 -14
  122. package/src/grid-messages.test.ts +7 -0
  123. package/src/grid-messages.ts +10 -0
  124. package/src/headless.ts +24 -0
  125. package/src/history.test.ts +49 -1
  126. package/src/history.ts +48 -2
  127. package/src/index.ts +2 -0
  128. package/src/keyboard-handlers.coverage.test.ts +65 -0
  129. package/src/keyboard-handlers.ts +26 -1
  130. package/src/keyboard.ts +3 -0
  131. package/src/row-drag-touch.ts +2 -2
  132. package/src/row-drag.test.ts +51 -0
  133. package/src/row-drag.ts +35 -2
  134. package/src/row-model.ts +6 -0
  135. package/src/row-resize.ts +24 -1
  136. package/src/selection.ts +1 -1
  137. package/src/server-block-cache.test.ts +27 -0
  138. package/src/server-block-cache.ts +13 -0
  139. package/src/server-data-source.infinite.test.ts +62 -0
  140. package/src/server-data-source.test.ts +19 -0
  141. package/src/server-data-source.ts +48 -4
  142. package/src/server.ts +2 -0
  143. package/src/sparkline.test.ts +7 -0
  144. package/src/sparkline.ts +6 -4
  145. package/src/svgrid-wrapper.types.ts +32 -0
  146. package/src/svgrid.api.test.ts +21 -0
  147. package/src/svgrid.charting.test.ts +12 -4
  148. package/src/svgrid.context-menu.test.ts +12 -0
  149. package/src/svgrid.data-push.svelte.test.ts +94 -0
  150. package/src/svgrid.detail-rows.svelte.test.ts +187 -0
  151. package/src/svgrid.filter-menu-scroll.test.ts +9 -0
  152. package/src/svgrid.gantt-seam.test.ts +1 -1
  153. package/src/svgrid.live-update-paths.svelte.test.ts +186 -0
  154. package/src/svgrid.menu-scroll-close.test.ts +10 -0
  155. package/src/svgrid.row-model-seam.svelte.test.ts +16 -0
  156. package/src/svgrid.row-pinning.test.ts +15 -1
  157. package/src/svgrid.selection-bar-seam.test.ts +1 -1
  158. package/src/svgrid.sticky-groups.svelte.test.ts +145 -0
  159. package/src/svgrid.upsell-license.test.ts +1 -1
  160. package/src/test-setup.ts +12 -0
  161. package/src/transaction.test.ts +58 -0
  162. package/src/validate.ts +7 -1
  163. package/src/virtualization/svelte-virtualizer.svelte.ts +1 -0
  164. package/src/virtualization/virtualizer.test.ts +44 -0
  165. package/src/virtualization/virtualizer.ts +13 -1
  166. package/dist/cdn/GridMenus-DLL-qZeu.js +0 -635
@@ -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'),
@@ -591,7 +625,16 @@ export function createSvGridController(rawProps, domIdBase) {
591
625
  // `data` is optional on the type (a `rowModel` may supply the rows
592
626
  // instead), so every read falls back to empty rather than to undefined.
593
627
  const EMPTY_DATA = [];
594
- let internalData = $state.raw(props.data ?? EMPTY_DATA);
628
+ // Seeded as a copy, for the same reason the sync effect below makes one: the
629
+ // row model tells data apart by array identity, so the grid has to own an
630
+ // array the consumer cannot grow behind its back.
631
+ let internalData = $state.raw(props.data?.length ? props.data.slice() : EMPTY_DATA);
632
+ // What the last sync saw, so the effect can tell a replacement (drop the
633
+ // cell edits) from a push or splice (keep them) and skip a no-op run.
634
+ // svelte-ignore state_referenced_locally
635
+ let lastData = props.data ?? EMPTY_DATA;
636
+ // svelte-ignore state_referenced_locally
637
+ let lastDataLength = props.data?.length ?? 0;
595
638
  // Resolve `cellDataType` / `inferColumnTypes` into concrete editorType +
596
639
  // format defaults once, up front, so every downstream reader sees a normal
597
640
  // column. Explicit fields on the ColumnDef always win.
@@ -632,12 +675,29 @@ export function createSvGridController(rawProps, domIdBase) {
632
675
  return collapsedColumnGroups.has(groupId);
633
676
  }
634
677
  $effect(() => {
678
+ const next = props.data ?? EMPTY_DATA;
679
+ // Reading `length` is what subscribes this effect to a `$state` array's own
680
+ // length signal, so `rows.push(...)` and `rows.splice(...)` re-run it.
681
+ // Without this read nothing in the grid tracks the row count at all:
682
+ // `getRowModel` reads `.length` only inside its cache miss, and the miss is
683
+ // keyed on the array reference, which a mutation does not change.
684
+ const length = next.length;
685
+ const replaced = next !== lastData;
686
+ if (!replaced && length === lastDataLength)
687
+ return;
688
+ lastData = next;
689
+ lastDataLength = length;
690
+ // A fresh array object, because `getRowModel` memoises on identity. The
691
+ // elements are the same objects, so a row that did not move keeps its row
692
+ // object and a field write still reaches its cell through the proxy.
693
+ internalData = length === 0 ? EMPTY_DATA : next.slice();
635
694
  // When the consumer replaces `data` (e.g. a "Reset" button), drop any
636
695
  // accumulated cell-edit overrides - otherwise `getCellDisplayValue`
637
696
  // would keep returning the old edited values from `editedCellValues`
638
- // even though the underlying data has been replaced.
639
- internalData = props.data ?? EMPTY_DATA;
640
- editedCellValues = {};
697
+ // even though the underlying data has been replaced. Growing or shrinking
698
+ // the array is not a reset, so it keeps them.
699
+ if (replaced)
700
+ editedCellValues = {};
641
701
  });
642
702
  $effect(() => {
643
703
  internalColumns = resolveCols(props.columns);
@@ -661,35 +721,41 @@ export function createSvGridController(rawProps, domIdBase) {
661
721
  // the row-model pipeline is read before any later prop change could apply.
662
722
  // svelte-ignore state_referenced_locally
663
723
  const treeDataConfig = props.treeData;
724
+ // Pagination is intentionally NOT in the grid's row-model pipeline. The
725
+ // wrapper applies its own filters (filterMenuValues, globalFilter,
726
+ // valueFilters) on top of `grid.getRowModel().rows`. If pagination ran
727
+ // first, those filters would only see the visible page. Instead the wrapper
728
+ // paginates last - see `allRows` below.
729
+ //
730
+ // Built ONCE. Both inputs are captured at mount, and the stages keep state
731
+ // between runs: the sorted and filtered stages hold their last output so a
732
+ // data tick can repair it instead of recomputing (core.ts, `replacedRowsOf`).
733
+ // This used to be a getter that built fresh stages on every read, which
734
+ // threw that cache away on each run - a live feed re-sorted 100k rows on
735
+ // every tick and the row-model memo's `cachedPipeline` never matched.
736
+ const rowModels = {
737
+ coreRowModel: createCoreRowModel(),
738
+ filteredRowModel: createFilteredRowModel(),
739
+ // External-sort mode: pass the rows through untouched so the consumer
740
+ // controls ordering (e.g. tree data that must preserve hierarchy).
741
+ sortedRowModel: externalSortEnabled
742
+ ? passthroughSortedRowModel
743
+ : createSortedRowModel(sortFns),
744
+ // Tree nesting replaces grouping when `treeData` is set - a row cannot
745
+ // be both a hierarchy node and bucketed under a group banner.
746
+ groupedRowModel: treeDataConfig
747
+ ? createTreeRowModel({
748
+ parentField: treeDataConfig.parentField,
749
+ idField: treeDataConfig.idField,
750
+ })
751
+ : createGroupedRowModel(),
752
+ expandedRowModel: createExpandedRowModel(),
753
+ };
664
754
  const grid = createSvGrid({
665
755
  get _features() {
666
756
  return resolveEffectiveFeatures();
667
757
  },
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
- },
758
+ _rowModels: rowModels,
693
759
  get columns() {
694
760
  return internalColumns;
695
761
  },
@@ -1091,10 +1157,8 @@ export function createSvGridController(rawProps, domIdBase) {
1091
1157
  });
1092
1158
  /** Cumulative pixel offsets for left- and right-pinned columns. */
1093
1159
  const pinnedOffsets = $derived.by(function pinnedOffsets_d() {
1094
- const rowNumberWidth = showRowNumbersEffective ? rowNumberColumnWidth : 0;
1095
- const selectionWidth = showRowSelectionEffective ? selectionColumnWidth : 0;
1096
1160
  const left = {};
1097
- let leftAcc = rowNumberWidth + selectionWidth;
1161
+ let leftAcc = systemColumnsWidth;
1098
1162
  for (const id of effectivePinning.left) {
1099
1163
  // Hidden pinned columns don't render, so they must contribute neither an
1100
1164
  // offset entry nor width - otherwise the next visible pinned column is
@@ -1611,16 +1675,13 @@ export function createSvGridController(rawProps, domIdBase) {
1611
1675
  // so consumers don't see spurious callbacks during re-renders.
1612
1676
  let lastCellRangeSerialized = "";
1613
1677
  $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
- : [];
1678
+ // Every rectangle, the committed ones a Ctrl+click added and the
1679
+ // active one last, the order getSelectionRects gives: a consumer's
1680
+ // status bar and shading cover the whole selection, not only its last
1681
+ // range. Depend on selectionRanges so a change to the committed set
1682
+ // re-runs this too.
1683
+ void selectionRanges;
1684
+ const ranges = getSelectionRects().map((r) => [r.minRow, r.minCol, r.maxRow, r.maxCol]);
1624
1685
  const serialized = JSON.stringify(ranges);
1625
1686
  if (serialized === lastCellRangeSerialized)
1626
1687
  return;
@@ -2651,6 +2712,16 @@ export function createSvGridController(rawProps, domIdBase) {
2651
2712
  return total;
2652
2713
  });
2653
2714
  /** The declared or dragged height of one row; measured under auto height. */
2715
+ /** A detail row's declared height, or null when the row is not one (or none is declared). */
2716
+ function detailRowHeightPx(i) {
2717
+ const dh = props.detailRowHeight;
2718
+ if (dh == null || !props.isDetailRow)
2719
+ return null;
2720
+ const row = allRows[i];
2721
+ if (!row || !props.isDetailRow(row.original, i))
2722
+ return null;
2723
+ return typeof dh === "function" ? dh(row.original) : dh;
2724
+ }
2654
2725
  function rowSizePxOf(i) {
2655
2726
  if (isRowCollapsed(i))
2656
2727
  return 0;
@@ -2659,6 +2730,9 @@ export function createSvGridController(rawProps, domIdBase) {
2659
2730
  const own = rowResizeHeightPx(i);
2660
2731
  if (own != null)
2661
2732
  return own;
2733
+ const detail = detailRowHeightPx(i);
2734
+ if (detail != null)
2735
+ return detail;
2662
2736
  const rh = props.rowHeight;
2663
2737
  return typeof rh === "function" ? rh(i) : (rh ?? 30);
2664
2738
  }
@@ -2669,10 +2743,91 @@ export function createSvGridController(rawProps, domIdBase) {
2669
2743
  top += rowSizePxOf(k);
2670
2744
  return top;
2671
2745
  }
2746
+ const NO_STICKY = [];
2747
+ /** Whether a row is a group and how deep it sits, for any of the three tree sources. */
2748
+ function rowGroupLevel(row) {
2749
+ const sg = props.serverGroup;
2750
+ if (sg) {
2751
+ const data = row.original;
2752
+ return { group: sg.isGroup(data), level: sg.level(data) };
2753
+ }
2754
+ // Client grouping and tree data: an expandable row is the group (a tree
2755
+ // row is a data row too, which is why isGroupRow() excludes it, but for
2756
+ // the band it is the parent its children scroll under).
2757
+ if (typeof row.getCanExpand === "function" && row.getCanExpand())
2758
+ return { group: true, level: row.depth ?? 0 };
2759
+ return { group: false, level: row.depth ?? 0 };
2760
+ }
2761
+ /** The first row whose bottom edge is under the header, in row-index space. */
2762
+ const firstRowUnderHeader = $derived.by(function firstRowUnderHeader_d() {
2763
+ if (allRows.length === 0)
2764
+ return -1;
2765
+ if (rowVirtualizationEnabled) {
2766
+ virtualizer.version;
2767
+ const offset = virtualizer.getState().scrollOffset;
2768
+ const hit = virtualRows.find((it) => it.end > offset);
2769
+ return hit ? hit.index : -1;
2770
+ }
2771
+ rowResizeVersion;
2772
+ autoRowHeightVersion;
2773
+ const top = scrollMetrics.scrollTop;
2774
+ let acc = 0;
2775
+ let i = 0;
2776
+ while (i < allRows.length - 1 && acc + rowSizePxOf(i) <= top) {
2777
+ acc += rowSizePxOf(i);
2778
+ i += 1;
2779
+ }
2780
+ return i;
2781
+ });
2782
+ const stickyGroupRows = $derived.by(function stickyGroupRows_d() {
2783
+ if (!props.stickyGroupRows)
2784
+ return NO_STICKY;
2785
+ const first = firstRowUnderHeader;
2786
+ if (first < 0)
2787
+ return NO_STICKY;
2788
+ const head = allRows[first];
2789
+ if (!head)
2790
+ return NO_STICKY;
2791
+ // A group row at level L, or a leaf at level L, has its ancestors at
2792
+ // levels L-1 .. 0, each the nearest group row above at that level.
2793
+ let want = rowGroupLevel(head).level - 1;
2794
+ if (want < 0)
2795
+ return NO_STICKY;
2796
+ const found = [];
2797
+ for (let i = first - 1; i >= frozenRowCount && want >= 0; i -= 1) {
2798
+ const row = allRows[i];
2799
+ const info = rowGroupLevel(row);
2800
+ if (info.group && info.level === want) {
2801
+ found.push({ row, rowIndex: i });
2802
+ want -= 1;
2803
+ }
2804
+ }
2805
+ if (found.length === 0)
2806
+ return NO_STICKY;
2807
+ found.reverse();
2808
+ rowResizeVersion;
2809
+ autoRowHeightVersion;
2810
+ let top = headerHeight + frozenBandHeight;
2811
+ return found.map((f) => {
2812
+ const at = top;
2813
+ const size = rowSizePxOf(f.rowIndex);
2814
+ top += size;
2815
+ return { ...f, top: at, size };
2816
+ });
2817
+ });
2818
+ /** The band's height under virtualization, taken off the top spacer. */
2819
+ const stickyBandHeight = $derived(rowVirtualizationEnabled ? stickyGroupRows.reduce((sum, s) => sum + s.size, 0) : 0);
2820
+ /** Without virtualization the real row sticks: its top, or undefined. */
2821
+ function stickyTopOfRow(rowIndex) {
2822
+ if (rowVirtualizationEnabled || stickyGroupRows.length === 0)
2823
+ return undefined;
2824
+ return stickyGroupRows.find((s) => s.rowIndex === rowIndex)?.top;
2825
+ }
2672
2826
  // 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));
2827
+ // virtualRowStart / virtualRowBottomSpacer. The frozen band and the sticky
2828
+ // group band are in the flow before the spacer whatever the window shows,
2829
+ // so they come off the spacer.
2830
+ const rowTopSpacer = $derived(Math.max(virtualRowStart - rowOffsetAdjustment - frozenBandHeight - stickyBandHeight, 0));
2676
2831
  const rowBottomSpacer = $derived(Math.max(rowDomTotalSize - (virtualRowEnd - rowOffsetAdjustment), 0));
2677
2832
  const virtualColumns = $derived.by(function virtualColumns_d() {
2678
2833
  columnVirtualizerVersion;
@@ -2757,9 +2912,7 @@ export function createSvGridController(rawProps, domIdBase) {
2757
2912
  * `fittedColumnWidths`, so the overflow decision settles in the same
2758
2913
  * render where fit-scaling lands - no race, no scrollbar flash. */
2759
2914
  const hasHorizontalOverflow = $derived.by(function hasHorizontalOverflow_d() {
2760
- const fixedCols = (showRowNumbersEffective ? rowNumberColumnWidth : 0) +
2761
- (showRowSelectionEffective ? selectionColumnWidth : 0);
2762
- let total = fixedCols;
2915
+ let total = systemColumnsWidth;
2763
2916
  for (const column of allColumns)
2764
2917
  total += getColumnWidth(column.id);
2765
2918
  // +1 to tolerate sub-pixel rounding residue from `fitColumns`.
@@ -3208,8 +3361,12 @@ export function createSvGridController(rawProps, domIdBase) {
3208
3361
  : typeof rh === "function"
3209
3362
  ? rh
3210
3363
  : (rh ?? 30);
3211
- const estimateSize = rowResizeHeights.size > 0 || collapsedRows.size > 0
3364
+ // Detail rows have their own height; only when the props say so does
3365
+ // the per-index lookup replace the uniform fast path.
3366
+ const detailOn = props.detailRowHeight != null && !!props.isDetailRow;
3367
+ const estimateSize = rowResizeHeights.size > 0 || collapsedRows.size > 0 || detailOn
3212
3368
  ? (index) => rowResizeHeightPx(index) ??
3369
+ detailRowHeightPx(index) ??
3213
3370
  (typeof base === "function" ? base(index) : base)
3214
3371
  : base;
3215
3372
  virtualizer.setOptions({
@@ -3320,6 +3477,18 @@ export function createSvGridController(rawProps, domIdBase) {
3320
3477
  // onto a server.
3321
3478
  if (props.rowSelectionModel)
3322
3479
  return props.rowSelectionModel.headerState();
3480
+ // Nothing checked is the common case and needs no walk: this derived
3481
+ // re-runs on every data change, and on a 100k-row live feed the filter
3482
+ // below was 30 ms per tick spent to conclude "none".
3483
+ let anyChecked = false;
3484
+ for (const id in rowSelectionState) {
3485
+ if (rowSelectionState[id]) {
3486
+ anyChecked = true;
3487
+ break;
3488
+ }
3489
+ }
3490
+ if (!anyChecked)
3491
+ return "none";
3323
3492
  const selectable = allRows.filter((row) => !isGroupRow(row) && !placeholderStateOf(row));
3324
3493
  if (!selectable.length)
3325
3494
  return "none";
@@ -3367,8 +3536,6 @@ export function createSvGridController(rawProps, domIdBase) {
3367
3536
  const cols = grid.getAllColumns().filter((c) => !hiddenColumns[c.id] && !hiddenByGroupCollapse[c.id] && !collapsedColumns[c.id]);
3368
3537
  if (!cols.length)
3369
3538
  return null;
3370
- const rowNumberWidth = showRowNumbersEffective ? rowNumberColumnWidth : 0;
3371
- const selectionWidth = showRowSelectionEffective ? selectionColumnWidth : 0;
3372
3539
  // Reserve the custom vertical scrollbar's width when it's visible. It
3373
3540
  // overlays the right 16px of the viewport (absolute, z-index 40) and
3374
3541
  // does NOT shrink clientWidth, so without this the last fitted column
@@ -3376,8 +3543,7 @@ export function createSvGridController(rawProps, domIdBase) {
3376
3543
  // is hidden behind the opaque scrollbar.
3377
3544
  const scrollbarWidth = hasVerticalOverflow ? 16 : 0;
3378
3545
  const target = (scrollContainer?.clientWidth ?? 0) -
3379
- rowNumberWidth -
3380
- selectionWidth -
3546
+ systemColumnsWidth -
3381
3547
  scrollbarWidth;
3382
3548
  if (target <= 0)
3383
3549
  return null;
@@ -3779,6 +3945,8 @@ export function createSvGridController(rawProps, domIdBase) {
3779
3945
  set historyPtr(v) { historyPtr = v; },
3780
3946
  get historyGroupId() { return historyGroupId; },
3781
3947
  set historyGroupId(v) { historyGroupId = v; },
3948
+ get historyTag() { return historyTag; },
3949
+ set historyTag(v) { historyTag = v; },
3782
3950
  get historyVersion() { return historyVersion; },
3783
3951
  set historyVersion(v) { historyVersion = v; },
3784
3952
  get tooltip() { return tooltip; },
@@ -3815,6 +3983,11 @@ export function createSvGridController(rawProps, domIdBase) {
3815
3983
  get gridStateVersion() { return gridStateVersion; },
3816
3984
  set gridStateVersion(v) { gridStateVersion = v; },
3817
3985
  get selectionColumnWidth() { return selectionColumnWidth; },
3986
+ get detailToggleColumnWidth() { return detailToggleColumnWidth; },
3987
+ get detailToggleColumnLeft() { return detailToggleColumnLeft; },
3988
+ get showDetailToggleEffective() { return showDetailToggleEffective; },
3989
+ get systemColumnsWidth() { return systemColumnsWidth; },
3990
+ get detailToggleOf() { return detailToggleOf; },
3818
3991
  get rowNumberColumnWidth() { return rowNumberColumnWidth; },
3819
3992
  get showRowNumbersEffective() { return showRowNumbersEffective; },
3820
3993
  get filterOperatorOptions() { return filterOperatorOptions; },
@@ -4234,6 +4407,8 @@ export function createSvGridController(rawProps, domIdBase) {
4234
4407
  get frozenRowList() { return frozenRowList; },
4235
4408
  get frozenBandHeight() { return frozenBandHeight; },
4236
4409
  get frozenRowTop() { return frozenRowTop; },
4410
+ get stickyGroupRows() { return stickyGroupRows; },
4411
+ get stickyTopOfRow() { return stickyTopOfRow; },
4237
4412
  /** Record the height the user dragged a row to. */
4238
4413
  get setRowResizeHeight() { return setRowResizeHeight; },
4239
4414
  get isRowCollapsed() { return isRowCollapsed; },
@@ -4246,6 +4421,12 @@ export function createSvGridController(rawProps, domIdBase) {
4246
4421
  rowResizeVersion;
4247
4422
  return rowResizeHeightPx;
4248
4423
  },
4424
+ /** The height row i is drawn at: dragged, measured, detail, declared or default. */
4425
+ get rowSizePxOf() {
4426
+ rowResizeVersion;
4427
+ autoRowHeightVersion;
4428
+ return rowSizePxOf;
4429
+ },
4249
4430
  get virtualRowTotalSize() { return virtualRowTotalSize; },
4250
4431
  get virtualRowStart() { return virtualRowStart; },
4251
4432
  get virtualRowEnd() { return virtualRowEnd; },
package/dist/SvGrid.css CHANGED
@@ -535,6 +535,10 @@
535
535
  .sv-grid-placeholder-retry:hover {
536
536
  background: color-mix(in srgb, var(--sg-fg, #0f172a) 6%, transparent);
537
537
  }
538
+ .sv-grid-placeholder-retry:focus-visible {
539
+ outline: 2px solid var(--sg-accent, #2563eb);
540
+ outline-offset: 2px;
541
+ }
538
542
 
539
543
  /* Pinned columns read as "frozen" via three layered cues:
540
544
  1. A distinct fill (slightly stronger than the header) so the
@@ -813,6 +817,12 @@
813
817
  .sv-grid-row-drop-after > td {
814
818
  box-shadow: inset 0 -2px 0 0 var(--sg-accent, #6366f1);
815
819
  }
820
+ /* Landing inside a group or tree row (the row becomes its child): the
821
+ * whole row is the target, so it takes a tint and a ring rather than a line. */
822
+ .sv-grid-row-drop-into > td {
823
+ box-shadow: inset 0 2px 0 0 var(--sg-accent, #6366f1), inset 0 -2px 0 0 var(--sg-accent, #6366f1);
824
+ background: color-mix(in srgb, var(--sg-accent, #6366f1) 10%, transparent);
825
+ }
816
826
 
817
827
  /* Pinned rows (top / bottom). Each cell sticks individually so the
818
828
  * row tracks scroll on every browser. Top rows stick below the sticky
@@ -837,6 +847,27 @@
837
847
  .sv-grid-pinned-row-bottom[data-pinned-index="1"] td { bottom: var(--sg-pinned-row-h, 32px); }
838
848
  .sv-grid-pinned-row-bottom[data-pinned-index="2"] td { bottom: calc(var(--sg-pinned-row-h, 32px) * 2); }
839
849
  .sv-grid-pinned-row-bottom[data-pinned-index="3"] td { bottom: calc(var(--sg-pinned-row-h, 32px) * 3); }
850
+ /* While the selection bar is up the scroller carries bottom padding (see
851
+ --sg-selbar-reserve), and a sticky bottom edge is inset by that padding:
852
+ the lowest pinned row then holds 68px above the viewport's edge, with the
853
+ body rows scrolling through the strip beneath it, in and out from behind
854
+ the bar. That strip is meant to be the bar's own ground, so the lowest
855
+ pinned row paints it: a shadow in the body colour, spread by half the
856
+ reserve and pushed down by the same, fills the strip under each cell; the
857
+ clip cuts the spread's sideways spill at the cell's own edges so a later
858
+ cell's shadow never paints over its neighbour. The cells sit at z-index 4,
859
+ above the rows and under the floating bar. */
860
+ .sv-grid-root[data-selbar$='bottom'] .sv-grid-pinned-row-bottom[data-pinned-index="0"] td {
861
+ box-shadow: 0 calc(var(--sg-selbar-reserve) / 2) 0 calc(var(--sg-selbar-reserve) / 2) var(--sg-bg, #fff);
862
+ clip-path: inset(0 0 calc(-1 * var(--sg-selbar-reserve)) 0);
863
+ }
864
+ /* The same strip above a sticky header when the bar sits at the top: the
865
+ header holds under the reserve and rows scrolled up showed through above
866
+ it. The first header row's cells paint the strip the same way. */
867
+ .sv-grid-root[data-selbar$='top'] .sv-grid-head > tr:first-child > th {
868
+ box-shadow: 0 calc(-1 * var(--sg-selbar-reserve) / 2) 0 calc(var(--sg-selbar-reserve) / 2) var(--sg-bg, #fff);
869
+ clip-path: inset(calc(-1 * var(--sg-selbar-reserve)) 0 0 0);
870
+ }
840
871
  /* Sticky-left columns keep their left position; just override their
841
872
  * z so they sit above non-pinned pinned-row cells. */
842
873
  .sv-grid-pinned-row td[data-pinned] { z-index: 5; }
@@ -1018,13 +1049,58 @@
1018
1049
  z-index: 4;
1019
1050
  }
1020
1051
 
1021
- /* Row-number column: sticks to the left edge of the scroll viewport. */
1052
+ /* Detail-toggle column (`showDetailToggle`): a row-header gutter of chevrons
1053
+ that open and close detail rows. Sticky beside the row numbers and the
1054
+ selection column, on the header band's colour so it reads as chrome, not
1055
+ data; no menu, no resize handle, outside the active cell. */
1056
+ .sv-grid-detail-toggle-column,
1057
+ .sv-grid-detail-toggle-cell {
1058
+ text-align: center;
1059
+ padding: 0;
1060
+ position: sticky;
1061
+ z-index: 4;
1062
+ background: var(--sg-header-bg, #f5f7fb);
1063
+ vertical-align: middle;
1064
+ }
1065
+ .sv-grid-detail-toggle {
1066
+ display: inline-flex;
1067
+ align-items: center;
1068
+ justify-content: center;
1069
+ width: 24px;
1070
+ height: 24px;
1071
+ padding: 0;
1072
+ border: 0;
1073
+ border-radius: 5px;
1074
+ background: transparent;
1075
+ color: var(--sg-muted, #64748b);
1076
+ cursor: pointer;
1077
+ vertical-align: middle;
1078
+ }
1079
+ .sv-grid-detail-toggle:hover {
1080
+ background: color-mix(in srgb, var(--sg-accent, #2563eb) 12%, transparent);
1081
+ color: var(--sg-accent, #2563eb);
1082
+ }
1083
+ .sv-grid-detail-toggle:focus-visible {
1084
+ outline: 2px solid var(--sg-accent, #2563eb);
1085
+ outline-offset: 1px;
1086
+ }
1087
+ .sv-grid-detail-toggle > .sv-grid-icon {
1088
+ width: 12px;
1089
+ height: 12px;
1090
+ transition: transform 0.15s ease;
1091
+ }
1092
+ .sv-grid-detail-toggle[aria-expanded="true"] > .sv-grid-icon {
1093
+ transform: rotate(90deg);
1094
+ }
1095
+
1096
+ /* Row-number column: sticks to the inline-start edge of the scroll viewport,
1097
+ which is the right edge under `dir="rtl"`. */
1022
1098
  .sv-grid-row-number-column,
1023
1099
  .sv-grid-row-number-cell {
1024
1100
  text-align: right;
1025
1101
  padding-inline: 8px;
1026
1102
  position: sticky;
1027
- left: 0;
1103
+ inset-inline-start: 0;
1028
1104
  z-index: 4;
1029
1105
  /* Match the column-header band so the row-number gutter and the column
1030
1106
  headers form one continuous header chrome. */
@@ -1311,9 +1387,6 @@ select.sv-grid-fr-editor {
1311
1387
  .sv-grid-cell-active,
1312
1388
  .sv-grid-cell-editing {
1313
1389
  position: relative;
1314
- /* High enough to sit above pinned-column stacking contexts (z=4) so the
1315
- list/chips popover overlays adjacent cells instead of being clipped. */
1316
- z-index: 20;
1317
1390
  /* `box-shadow: inset` is bound to the cell's own box, so the ring
1318
1391
  clips cleanly with the cell when it's partially scrolled out of
1319
1392
  view (and shows up consistently at the right edge of a virtualized
@@ -1323,10 +1396,33 @@ select.sv-grid-fr-editor {
1323
1396
  inconsistent at scroll boundaries. */
1324
1397
  box-shadow: inset 0 0 0 2px var(--sg-accent, #0b63f3);
1325
1398
  outline: none;
1326
- /* Let list/chips popouts extend past the cell box (default cells have
1327
- overflow: hidden so the chips picker would otherwise be clipped). */
1399
+ }
1400
+ /* Only the EDITING cell escapes its own box and outranks the pinned-column
1401
+ stacking contexts (z=4), so its list/chips popout overlays the neighbours
1402
+ instead of being clipped. The merely-ACTIVE cell used to get the same
1403
+ treatment, and on a grid scrolled sideways that made it paint straight over
1404
+ the sticky selection / row-number column: the checkbox disappeared and the
1405
+ cell's text - unclipped, because overflow was visible - spilled across it.
1406
+ A grid seeds its active cell at (0,0), so this hit the FIRST row of any
1407
+ horizontally scrolled grid with `showRowSelection`, untouched by the user. */
1408
+ .sv-grid-cell-editing {
1409
+ z-index: 20;
1328
1410
  overflow: visible;
1329
1411
  }
1412
+ /* Above ordinary cells so the ring is not overdrawn by a neighbour, below the
1413
+ sticky columns (z=4) so those keep covering it as they should. */
1414
+ .sv-grid-cell-active:not(.sv-grid-cell-editing) {
1415
+ z-index: 3;
1416
+ }
1417
+ /* The active cell a grid mounts with is seeded at (0,0), not chosen: no
1418
+ ring until a click, an arrow key or the api activates a cell (the same
1419
+ gate the fill handle has always had). Once activated it stays through a
1420
+ blur, so a toolbar acting on "the focused row" keeps its cue. A page with
1421
+ six grids on it, or a detail grid inside each open row, used to open
1422
+ with an accent ring in every one of them. */
1423
+ .sv-grid-active-seeded .sv-grid-cell-active {
1424
+ box-shadow: none;
1425
+ }
1330
1426
 
1331
1427
  /* ---- The editing cell's border -------------------------------------------
1332
1428
  One ring for every editor, drawn by the CELL rather than by each editor.
@@ -1625,6 +1721,40 @@ select.sv-grid-fr-editor {
1625
1721
  border-bottom: 1px solid var(--sg-frozen-edge, var(--sg-border, #94a3b8));
1626
1722
  }
1627
1723
 
1724
+ /* A detail row with a declared height (detailRowHeight): the row keeps the
1725
+ height the virtualizer counted on - the auto-height rule above must not
1726
+ win - and the panel's body, a block inside the cell, takes that height
1727
+ and scrolls what does not fit. (A table cell's own height is only a
1728
+ minimum, so the height goes on the block.) */
1729
+ .sv-grid-row.sv-grid-detail-row.sv-grid-detail-row-sized {
1730
+ height: var(--sv-detail-h) !important;
1731
+ }
1732
+ .sv-grid-row.sv-grid-detail-row-sized > .sv-grid-detail-cell {
1733
+ vertical-align: top;
1734
+ box-sizing: border-box;
1735
+ height: var(--sv-detail-h);
1736
+ }
1737
+ .sv-grid-detail-body {
1738
+ box-sizing: border-box;
1739
+ height: 100%;
1740
+ overflow: auto;
1741
+ }
1742
+
1743
+ /* Sticky group rows: the group a row belongs to, held under the header
1744
+ (and under the frozen band) while its rows scroll past. Same layer as
1745
+ the frozen band; the last one carries the band's edge. */
1746
+ .sv-grid-row.sv-grid-row-sticky-group {
1747
+ position: sticky;
1748
+ z-index: 31;
1749
+ background: var(--sg-bg, #fff);
1750
+ }
1751
+ .sv-grid-row.sv-grid-row-sticky-group > .sv-grid-cell {
1752
+ background: var(--sg-bg, #fff);
1753
+ }
1754
+ .sv-grid-row.sv-grid-row-sticky-group-last > .sv-grid-cell {
1755
+ border-bottom: 1px solid var(--sg-frozen-edge, var(--sg-border, #94a3b8));
1756
+ }
1757
+
1628
1758
  /* The multiline text editor sizes itself by its lines and keeps the
1629
1759
  input's single-line look until a break is typed. */
1630
1760
  .sv-grid-cell-editor-multiline {
@@ -3082,6 +3212,12 @@ select.sv-grid-fr-editor {
3082
3212
  .sv-grid-row-number-cell {
3083
3213
  border-right-color: var(--sg-header-bg, #f1f5f9);
3084
3214
  }
3215
+ /* The detail-toggle gutter fuses with the row numbers the same way; only
3216
+ its own right edge keeps a line, between the chrome and the data. */
3217
+ .sv-grid-row-number-cell:has(+ .sv-grid-detail-toggle-cell),
3218
+ .sv-grid-row-number-column:has(+ .sv-grid-detail-toggle-column) {
3219
+ border-right-color: var(--sg-header-bg, #f1f5f9);
3220
+ }
3085
3221
 
3086
3222
  /* Group row label colour. */
3087
3223
  .sv-grid-group-row > .sv-grid-cell {
@@ -1,3 +1,4 @@
1
+ import { parseDateValue } from "./cell-formatting.js";
1
2
  export function cfTextStyle(cf) {
2
3
  let s = "";
3
4
  if (cf.color)
@@ -233,10 +234,7 @@ export function getEditorClass(editorType) {
233
234
  return "sv-grid-cell-editor";
234
235
  }
235
236
  export function asDate(value) {
236
- if (value == null || value === "")
237
- return null;
238
- const parsed = value instanceof Date ? value : new Date(value);
239
- return Number.isNaN(parsed.getTime()) ? null : parsed;
237
+ return parseDateValue(value);
240
238
  }
241
239
  export function clampMenuX(x, width) {
242
240
  return Math.max(8, Math.min(x, window.innerWidth - width - 8));