@svgrid/grid 2.6.21 → 2.6.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +22 -0
  3. package/dist/GridMenus.svelte +201 -18
  4. package/dist/SvAutoComplete.svelte +4 -0
  5. package/dist/SvComboBox.svelte +5 -1
  6. package/dist/SvDateRangeInput.svelte +25 -3
  7. package/dist/SvDropDownList.svelte +4 -0
  8. package/dist/SvForm.svelte +8 -8
  9. package/dist/SvGrid.controller.svelte.d.ts +61 -9
  10. package/dist/SvGrid.controller.svelte.js +363 -123
  11. package/dist/SvGrid.css +222 -1
  12. package/dist/SvGrid.helpers.d.ts +55 -0
  13. package/dist/SvGrid.helpers.js +83 -0
  14. package/dist/SvGrid.svelte +255 -56
  15. package/dist/SvGrid.types.d.ts +146 -1
  16. package/dist/SvGridCellEditor.svelte +99 -17
  17. package/dist/SvGridSelect.svelte +6 -2
  18. package/dist/SvGridSelect.svelte.d.ts +1 -1
  19. package/dist/SvListBox.svelte +8 -0
  20. package/dist/SvMultiSelect.svelte +6 -2
  21. package/dist/SvMultiSelect.svelte.d.ts +1 -1
  22. package/dist/SvNumberInput.svelte +4 -0
  23. package/dist/SvRichCell.svelte +125 -0
  24. package/dist/SvRichCell.svelte.d.ts +15 -0
  25. package/dist/SvTextArea.svelte +5 -1
  26. package/dist/SvTreeSelect.svelte +6 -2
  27. package/dist/SvTreeSelect.svelte.d.ts +1 -1
  28. package/dist/builtin-editors.d.ts +2 -2
  29. package/dist/builtin-editors.js +19 -3
  30. package/dist/cdn/GridMenus-BzWXQjv3.js +600 -0
  31. package/dist/cdn/GridMenus-xr_rHx8F.js +601 -0
  32. package/dist/cdn/SvDateRangeInput-CaOuMs8O.js +198 -0
  33. package/dist/cdn/SvDateRangeInput-DMLKmGEc.js +199 -0
  34. package/dist/cdn/SvDateTimePicker-CCbDZNZB.js +816 -0
  35. package/dist/cdn/SvDateTimePicker-sonaH0oh.js +812 -0
  36. package/dist/cdn/SvGridCellEditor-BAPSC7EL.js +550 -0
  37. package/dist/cdn/SvGridCellEditor-BwzmUWtL.js +549 -0
  38. package/dist/cdn/column-resize-DsfNXMom.js +102 -0
  39. package/dist/cdn/date-format-BNii4zeD.js +1393 -0
  40. package/dist/cdn/date-format-BnnHlqGw.js +1395 -0
  41. package/dist/cdn/{editor-registry-CiI0xlKg.js → editor-registry-BhgE3S84.js} +51 -25
  42. package/dist/cdn/row-resize-BRcimkUT.js +95 -0
  43. package/dist/cdn/{src-DBel9wRZ.js → src-DGo7IHug.js} +10017 -9488
  44. package/dist/cdn/{src-BYq-qyrp.js → src-skQN5eqh.js} +7829 -7300
  45. package/dist/cdn/svgrid.js +14 -10
  46. package/dist/cdn/svgrid.svelte-external.js +14 -10
  47. package/dist/cell-values.d.ts +16 -0
  48. package/dist/cell-values.js +28 -0
  49. package/dist/clipboard.d.ts +22 -0
  50. package/dist/clipboard.js +405 -27
  51. package/dist/column-groups.js +1 -1
  52. package/dist/column-resize.d.ts +46 -0
  53. package/dist/column-resize.js +205 -0
  54. package/dist/columns.d.ts +0 -3
  55. package/dist/columns.js +0 -57
  56. package/dist/core.d.ts +19 -4
  57. package/dist/core.js +460 -119
  58. package/dist/editing.js +12 -7
  59. package/dist/editors/cell-editors.d.ts +11 -0
  60. package/dist/editors/cell-editors.js +18 -1
  61. package/dist/filtering/excel-filters.js +28 -0
  62. package/dist/grid-messages.d.ts +15 -0
  63. package/dist/grid-messages.js +15 -0
  64. package/dist/group-display.d.ts +1 -1
  65. package/dist/index.d.ts +6 -1
  66. package/dist/index.js +10 -0
  67. package/dist/list-option.d.ts +6 -0
  68. package/dist/menus.js +41 -8
  69. package/dist/row-resize.d.ts +11 -0
  70. package/dist/row-resize.js +7 -1
  71. package/dist/sanitize-html.d.ts +37 -0
  72. package/dist/sanitize-html.js +234 -0
  73. package/dist/selection-bar-view.svelte.d.ts +25 -0
  74. package/dist/selection-bar-view.svelte.js +13 -0
  75. package/dist/selection.d.ts +2 -1
  76. package/dist/selection.js +133 -8
  77. package/dist/spreadsheet.d.ts +1 -1
  78. package/dist/spreadsheet.js +1 -1
  79. package/package.json +1 -1
  80. package/src/GridMenus.svelte +201 -18
  81. package/src/SvAutoComplete.svelte +4 -0
  82. package/src/SvComboBox.svelte +5 -1
  83. package/src/SvDateRangeInput.svelte +25 -3
  84. package/src/SvDropDownList.svelte +4 -0
  85. package/src/SvForm.svelte +8 -8
  86. package/src/SvGrid.controller.svelte.ts +404 -133
  87. package/src/SvGrid.css +222 -1
  88. package/src/SvGrid.helpers.ts +87 -0
  89. package/src/SvGrid.svelte +255 -56
  90. package/src/SvGrid.types.ts +153 -1
  91. package/src/SvGridCellEditor.svelte +99 -17
  92. package/src/SvGridSelect.svelte +6 -2
  93. package/src/SvListBox.svelte +8 -0
  94. package/src/SvMultiSelect.svelte +6 -2
  95. package/src/SvNumberInput.svelte +4 -0
  96. package/src/SvRichCell.svelte +125 -0
  97. package/src/SvTextArea.svelte +5 -1
  98. package/src/SvTreeSelect.svelte +6 -2
  99. package/src/builtin-editors.test.ts +18 -1
  100. package/src/builtin-editors.ts +19 -3
  101. package/src/cell-values.ts +30 -0
  102. package/src/clipboard.test.ts +93 -20
  103. package/src/clipboard.ts +433 -33
  104. package/src/column-groups.ts +1 -1
  105. package/src/column-resize.test.ts +381 -0
  106. package/src/column-resize.ts +227 -0
  107. package/src/columns.test.ts +0 -103
  108. package/src/columns.ts +0 -58
  109. package/src/core.aggregate.test.ts +134 -0
  110. package/src/core.filter.test.ts +156 -0
  111. package/src/core.grouping.test.ts +146 -0
  112. package/src/core.row-shape.test.ts +119 -0
  113. package/src/core.rowmodel-cache.test.ts +121 -0
  114. package/src/core.sort.test.ts +293 -0
  115. package/src/core.ts +516 -119
  116. package/src/date-string-column.test.ts +87 -0
  117. package/src/editing.test.ts +25 -0
  118. package/src/editing.ts +12 -9
  119. package/src/editor-block-prop.test.ts +132 -0
  120. package/src/editors/cell-editors.ts +27 -1
  121. package/src/filtering/excel-filters.ts +30 -0
  122. package/src/filtering/normalize-fast-path.test.ts +104 -0
  123. package/src/grid-messages.ts +34 -0
  124. package/src/group-display.ts +1 -1
  125. package/src/index.ts +25 -1
  126. package/src/list-option.ts +6 -0
  127. package/src/menus.test.ts +43 -0
  128. package/src/menus.ts +38 -8
  129. package/src/move-cells.test.ts +850 -0
  130. package/src/number-editor-literal.test.ts +84 -0
  131. package/src/number-editor.grid.test.ts +160 -0
  132. package/src/resize-props.test.ts +361 -0
  133. package/src/row-resize.test.ts +31 -0
  134. package/src/row-resize.ts +21 -3
  135. package/src/sanitize-html.test.ts +182 -0
  136. package/src/sanitize-html.ts +235 -0
  137. package/src/selection-bar-view.svelte.ts +36 -0
  138. package/src/selection.test.ts +157 -0
  139. package/src/selection.ts +135 -8
  140. package/src/spreadsheet.ts +1 -1
  141. package/src/svgrid.filter-menu-listbox.svelte.test.ts +56 -10
  142. package/src/svgrid.in-suggest-snapshot.svelte.test.ts +91 -0
  143. package/src/svgrid.menu-scroll-close.test.ts +204 -0
  144. package/src/svgrid.selection-bar-seam.test.ts +185 -0
  145. package/src/svgrid.upsell-license.test.ts +117 -0
  146. package/dist/cdn/GridMenus-BfTAKn84.js +0 -488
  147. package/dist/cdn/GridMenus-C3bJd7w8.js +0 -489
  148. package/dist/cdn/SvDateTimePicker-CHnUkWcH.js +0 -2206
  149. package/dist/cdn/SvDateTimePicker-CRQH_U7E.js +0 -2201
  150. package/dist/cdn/SvGridCellEditor-BGUCzuPB.js +0 -523
  151. package/dist/cdn/SvGridCellEditor-G8IMWIws.js +0 -522
package/src/SvGrid.svelte CHANGED
@@ -15,6 +15,8 @@
15
15
  } from "./index";
16
16
  import "./sv-grid-scrollbar";
17
17
  import "./SvGrid.css";
18
+ import type { RowResizeOptions } from "./row-resize";
19
+ import type { ColumnResizeOptions } from "./column-resize";
18
20
  import {
19
21
  RenderSnippetConfig,
20
22
  RenderComponentConfig,
@@ -31,6 +33,9 @@
31
33
  import {
32
34
  cfTextStyle,
33
35
  getEditorInputType,
36
+ usesRichDateFilter,
37
+ toIsoDateLocal,
38
+ fromIsoDateLocal,
34
39
  getOptionLabel,
35
40
  getOptionColor,
36
41
  colorfulChipStyle,
@@ -46,6 +51,7 @@
46
51
  // SvGridChartPanel (charting) and SvGridBoard (Kanban) are heavy, prop-gated
47
52
  // views - lazy-loaded below so they stay out of the base <SvGrid> bundle.
48
53
  import { getSchedulerView } from "./scheduler-view.svelte";
54
+ import { getSelectionBarView } from "./selection-bar-view.svelte";
49
55
  import { getBoardView } from "./board-view.svelte";
50
56
  import { getChartView } from "./chart-view.svelte";
51
57
  let props: Props<TFeatures, TData> = $props();
@@ -97,6 +103,37 @@
97
103
  import("./SvGridChartView.svelte").then((m) => (ChartView = m.default));
98
104
  });
99
105
  let MenusOverlay = $state<typeof import("./GridMenus.svelte").default | null>(null);
106
+ // The grid's own date picker, for date FILTER inputs. Lazy for the same
107
+ // reason the cell editor's copy is: most grids never show a filter row, and
108
+ // most that do have no date column.
109
+ let FilterDatePicker = $state<typeof import("./SvDateTimePicker.svelte").default | null>(null);
110
+ $effect(() => {
111
+ if (!showFilterRowEffective && !ctrl.filterMenuFor) return;
112
+ if (FilterDatePicker) return;
113
+ const wantsPicker = ctrl.allColumns.some((c: any) =>
114
+ usesRichDateFilter(c.columnDef?.editorType),
115
+ );
116
+ if (wantsPicker)
117
+ import("./SvDateTimePicker.svelte").then((m) => (FilterDatePicker = m.default));
118
+ });
119
+ // `between` on a date column filters with a RANGE control rather than two
120
+ // single-date fields: one field, one popover, both ends picked in a single
121
+ // gesture - and it fits a narrow date column, which two pickers plus their
122
+ // buttons do not. Loaded only once a date column is actually set to Between,
123
+ // so a grid that never uses the operator never pays for the chunk.
124
+ let FilterRangePicker = $state<typeof import("./SvDateRangeInput.svelte").default | null>(null);
125
+ $effect(() => {
126
+ if (!showFilterRowEffective) return;
127
+ if (FilterRangePicker) return;
128
+ const wantsRange = ctrl.allColumns.some(
129
+ (c: any) =>
130
+ usesRichDateFilter(c.columnDef?.editorType) &&
131
+ (ctrl.filterMenuValues[c.id]?.operator ??
132
+ ctrl.defaultOperatorFor(c)) === "between",
133
+ );
134
+ if (wantsRange)
135
+ import("./SvDateRangeInput.svelte").then((m) => (FilterRangePicker = m.default));
136
+ });
100
137
  $effect(() => {
101
138
  if (ctrl.chartingEnabled && ctrl.chartPanelOpen && !ChartPanelView)
102
139
  import("./SvGridChartPanel.svelte").then((m) => (ChartPanelView = m.default));
@@ -167,7 +204,9 @@
167
204
  const hideTooltip = $derived(ctrl.hideTooltip);
168
205
  const findHits = $derived(ctrl.findHits);
169
206
  const headerHeight = $derived(ctrl.headerHeight);
170
- const resizingColumnId = $derived(ctrl.resizingColumnId);
207
+ // Column resizing has always been unconditional; `columnResize={false}` is
208
+ // the opt-out, so the default has to stay on when the prop is absent.
209
+ const columnResizeEnabled = $derived(opt.columnResize === true);
171
210
  const selectionColumnWidth = $derived(ctrl.selectionColumnWidth);
172
211
  const rowNumberColumnWidth = $derived(ctrl.rowNumberColumnWidth);
173
212
  const showRowNumbersEffective = $derived(ctrl.showRowNumbersEffective);
@@ -264,6 +303,23 @@
264
303
  // Row-grouping display modes: group state lives in synthetic columns instead
265
304
  // of a full-width banner row.
266
305
  const groupColumnMode = $derived(ctrl.groupColumnMode);
306
+ /**
307
+ * True when a cell's content needs none of the wrapper snippets.
308
+ *
309
+ * A cell body normally renders through `cellBodyWithFormat` ->
310
+ * `cellBodyFormatted` -> `cellBody`, and with no grouping, no tree data and
311
+ * no conditional formats the first two are pure pass-throughs: three snippet
312
+ * renders and two `{#if}` blocks per cell to reach content that one render
313
+ * produces. Measured by ablation, that chain is 4.2 ms of a 17.4 ms mount at
314
+ * 28 rows x 9 columns - about a quarter of the whole mount, and the largest
315
+ * single per-cell cost.
316
+ *
317
+ * Computed once per render rather than per cell, so the fast path costs one
318
+ * boolean read at each of the ~250 cells instead of two snippet frames.
319
+ */
320
+ const plainCellBody = $derived(
321
+ !groupColumnMode && !treeData && !hasConditionalFormats,
322
+ );
267
323
  const autoGroupCell = $derived(ctrl.autoGroupCell);
268
324
  const isAutoGroupColumn = (id: string): boolean =>
269
325
  id === "__autoGroup" || id.startsWith("__group_");
@@ -280,7 +336,60 @@
280
336
  // The fixed row height, matching what the virtualized path takes from the virtualizer.
281
337
  // The non-virtualized (`virtualization={false}`) body must apply this too, else its rows
282
338
  // fall back to content height and look shorter than a virtualized grid's.
339
+ /**
340
+ * `rowResize` and `columnResize` are both opt-in and both default to OFF, so
341
+ * neither module is imported statically - a grid that leaves them alone must
342
+ * not carry their bytes. This wraps an action so its module is fetched on
343
+ * first enable and never otherwise. The handles appear a microtask after the
344
+ * first paint, which is invisible for a drag affordance.
345
+ *
346
+ * One helper for both: two hand-rolled copies of this bookkeeping would cost
347
+ * more base bundle than the deferral saves.
348
+ */
349
+ type LazyHandle<O> = { update(o: O): void; destroy(): void };
350
+ function lazyAction<O extends { disabled?: boolean }>(
351
+ load: () => Promise<(node: HTMLElement, opts: O) => LazyHandle<O>>,
352
+ ) {
353
+ return (node: HTMLElement, opts: O): LazyHandle<O> => {
354
+ let handle: LazyHandle<O> | null = null;
355
+ let current = opts;
356
+ let destroyed = false;
357
+ const ensure = () => {
358
+ if (handle || destroyed || current.disabled) return;
359
+ void load().then((make) => {
360
+ // The prop can be switched back off, or the grid unmounted, while
361
+ // the chunk is still in flight.
362
+ if (destroyed || current.disabled || handle) return;
363
+ handle = make(node, current);
364
+ });
365
+ };
366
+ ensure();
367
+ return {
368
+ update(next: O) {
369
+ current = next;
370
+ if (handle) handle.update(next);
371
+ else ensure();
372
+ },
373
+ destroy() {
374
+ destroyed = true;
375
+ handle?.destroy();
376
+ },
377
+ };
378
+ };
379
+ }
380
+
381
+ const lazyRowResize = lazyAction<RowResizeOptions>(() =>
382
+ import("./row-resize").then((m) => m.rowResize),
383
+ );
384
+ const lazyColumnResize = lazyAction<ColumnResizeOptions>(() =>
385
+ import("./column-resize").then((m) => m.columnResize),
386
+ );
387
+
283
388
  const rowSizePx = (i: number): number => {
389
+ // A height the user dragged wins over the declared one, so the row stays
390
+ // where they put it across re-renders and virtualization recycling.
391
+ const dragged = ctrl.rowResizeHeightPx(i);
392
+ if (dragged != null) return dragged;
284
393
  const rh = opt.rowHeight;
285
394
  return typeof rh === "function" ? rh(i) : (rh ?? 30);
286
395
  };
@@ -358,26 +467,10 @@
358
467
  return "sv-grid-cell-flash";
359
468
  }
360
469
 
361
- // Keyboard-accessible column resize (#79): focus a resize handle and use the
362
- // arrow keys (Shift = fine 1px step). Complements the pointer-drag resize.
363
- function resizeColumnByKeyboard(e: KeyboardEvent, columnId: string) {
364
- let delta = 0;
365
- const step = e.shiftKey ? 1 : 10;
366
- if (e.key === "ArrowLeft") delta = -step;
367
- else if (e.key === "ArrowRight") delta = step;
368
- else return;
369
- e.preventDefault();
370
- const current = ctrl.getColumnWidth(columnId);
371
- ctrl.columnWidths = {
372
- ...ctrl.columnWidths,
373
- [columnId]: Math.max(40, current + delta),
374
- };
375
- }
376
-
377
470
  // ---- Full-row editing -------------------------------------------------
378
471
  const fullRowEdit = $derived(ctrl.fullRowEdit);
379
472
  // Commit the whole row when the user clicks away from its editors (Excel /
380
- // AG-Grid feel). Clicking within any full-row editor keeps editing.
473
+ // spreadsheet feel). Clicking within any full-row editor keeps editing.
381
474
  // Stays here rather than in SvGridCellEditor: that component is instantiated
382
475
  // once PER editable cell during a full-row edit, so a document listener there
383
476
  // would be attached N times and commit N times.
@@ -429,7 +522,6 @@
429
522
  const setActiveCell = $derived(ctrl.setActiveCell);
430
523
  const scrollActiveCellIntoView = $derived(ctrl.scrollActiveCellIntoView);
431
524
  const getColumnWidth = $derived(ctrl.getColumnWidth);
432
- const startColumnResize = $derived(ctrl.startColumnResize);
433
525
  const getCellRangeEdges = $derived(ctrl.getCellRangeEdges);
434
526
  const fillHandleCell = $derived(ctrl.fillHandleCell);
435
527
  const isInFillPreview = $derived(ctrl.isInFillPreview);
@@ -624,6 +716,24 @@
624
716
  onpointermove={onWindowPointerMove}
625
717
  />
626
718
 
719
+ <!--
720
+ The licensing line every Enterprise upsell note carries. One snippet rather
721
+ than four copies: four gates worded four ways is how a product ends up
722
+ explaining its own licensing inconsistently.
723
+
724
+ Enterprise is SOFT-gated - the feature runs without a key and the grid shows
725
+ an unlicensed watermark - so this says exactly that rather than implying the
726
+ feature is inert without one.
727
+ -->
728
+ {#snippet enterpriseLicenseNote()}
729
+ <p class="sv-grid-upsell-license">
730
+ {messages.upsellLicense}
731
+ <a href="https://svgrid.com/pricing/" target="_blank" rel="noopener noreferrer">
732
+ {messages.upsellLicenseLink}
733
+ </a>
734
+ </p>
735
+ {/snippet}
736
+
627
737
  {#if opt.loading && !opt.loadingOverlay && !hasMeasured}
628
738
  <!-- Full-screen loading state only on the *initial* load, before the grid
629
739
  has ever rendered. Once measured, a `loading` flip (from a server-mode
@@ -680,6 +790,7 @@
680
790
  <code>@svgrid/enterprise</code> and call <code>enableBoardView()</code>
681
791
  to render it.
682
792
  </p>
793
+ {@render enterpriseLicenseNote()}
683
794
  </div>
684
795
  {/if}
685
796
  </div>
@@ -727,6 +838,7 @@
727
838
  <code>@svgrid/enterprise</code> and call <code>enableSchedulerView()</code>
728
839
  to render it.
729
840
  </p>
841
+ {@render enterpriseLicenseNote()}
730
842
  </div>
731
843
  {/if}
732
844
  </div>
@@ -809,6 +921,7 @@
809
921
  <div class="sv-grid-scheduler-upsell sv-grid-pivot-upsell" role="note">
810
922
  <strong>{messages.pivotUpsellTitle}</strong>
811
923
  <p>{messages.pivotUpsellBody}</p>
924
+ {@render enterpriseLicenseNote()}
812
925
  </div>
813
926
  {/if}
814
927
  </div>
@@ -866,6 +979,13 @@
866
979
  <path d="M8 5l9 7-9 7" />
867
980
  {:else if name === "op-lessThan"}
868
981
  <path d="M16 5l-9 7 9 7" />
982
+ {:else if name === "op-between"}
983
+ <!-- Two bounds with the span between them. The catalogue has always
984
+ named this icon; the snippet had no case for it, so the Between
985
+ row in the operator menu drew an empty <svg>. -->
986
+ <path d="M5 5v14" />
987
+ <path d="M19 5v14" />
988
+ <path d="M9 12h6" />
869
989
  {:else if name === "op-isBlank"}
870
990
  <circle cx="12" cy="12" r="8" />
871
991
  <path d="M6.5 6.5l11 11" />
@@ -1294,6 +1414,32 @@
1294
1414
  class="sv-grid-root"
1295
1415
  class:sv-grid-root-fill={opt.containerHeight === "100%"}
1296
1416
  style={chartDockReserveStyle}
1417
+ data-move-grab={ctrl.moveGrabHover || ctrl.moveDrag ? "true" : undefined}
1418
+ data-selbar={
1419
+ ctrl.selectionBarOn && ctrl.selectionBarTarget.ids.length > 0
1420
+ ? (getSelectionBarView() ? ctrl.selectionBarPosition : `upsell-${ctrl.selectionBarPosition}`)
1421
+ : undefined
1422
+ }
1423
+ use:lazyRowResize={{
1424
+ disabled: !ctrl.rowResizeOn,
1425
+ // No gutter column on most grids, so fall back to the first body cell -
1426
+ // otherwise `rowResize` would be a prop that silently does nothing.
1427
+ anchor: "row",
1428
+ onResize: (index, height) => ctrl.setRowResizeHeight(index, height),
1429
+ }}
1430
+ use:lazyColumnResize={{
1431
+ disabled: !columnResizeEnabled,
1432
+ getWidth: (columnId) => ctrl.getColumnWidth(columnId),
1433
+ onResize: (columnId, width) => {
1434
+ ctrl.columnWidths = { ...ctrl.columnWidths, [columnId]: width };
1435
+ },
1436
+ label: (columnId) => {
1437
+ const col = ctrl.findColumnById(columnId);
1438
+ return col ? toolPanelHeaderLabel(col) : columnId;
1439
+ },
1440
+ canResize: (columnId) => ctrl.columnResizable(columnId),
1441
+ onAutosize: (columnId) => ctrl.autosizeColumn(columnId),
1442
+ }}
1297
1443
  >
1298
1444
  {#if showGlobalFilterEffective}
1299
1445
  <label class="sv-grid-global-filter">
@@ -1747,32 +1893,6 @@
1747
1893
  }}
1748
1894
  />
1749
1895
  {/if}
1750
- <div
1751
- class="sv-grid-resize-handle"
1752
- class:is-resizing={resizingColumnId ===
1753
- header.column.id}
1754
- role="separator"
1755
- aria-orientation="vertical"
1756
- aria-label={`Resize ${toolPanelHeaderLabel(header.column)}`}
1757
- tabindex="0"
1758
- {...(() => {
1759
- // A focusable separator is a widget, so ARIA requires
1760
- // aria-valuenow. The value it exposes is the column
1761
- // width the arrow keys change; 40 is the floor
1762
- // enforced in resizeColumnByKeyboard.
1763
- const w = Math.round(ctrl.getColumnWidth(header.column.id));
1764
- return {
1765
- "aria-valuenow": w,
1766
- "aria-valuemin": 40,
1767
- "aria-valuetext": `${w} pixels`,
1768
- };
1769
- })()}
1770
- onpointerdown={(event) =>
1771
- startColumnResize(event, header.column.id)}
1772
- onkeydown={(event) =>
1773
- resizeColumnByKeyboard(event, header.column.id)}
1774
- ondblclick={(event) => event.stopPropagation()}
1775
- ></div>
1776
1896
  {/if}
1777
1897
  </th>
1778
1898
  {/if}
@@ -1900,6 +2020,57 @@
1900
2020
  }}
1901
2021
  />
1902
2022
  </div>
2023
+ {:else if activeOperator !== "isBlank" && activeOperator !== "isNotBlank" && activeOperator !== "regex" && FilterDatePicker && usesRichDateFilter(rendered.column.columnDef.editorType)}
2024
+ <!-- Same picker the cell editor uses, so a date
2025
+ column looks the same wherever you touch it.
2026
+ `regex` keeps the text box - a pattern is not
2027
+ a date. -->
2028
+ {#if activeOperator === "between"}
2029
+ <!-- `between` needs BOTH bounds - `condActive`
2030
+ ignores the filter until `valueTo` is set too -
2031
+ so this is one range field rather than two
2032
+ single-date pickers: both ends come from a
2033
+ single gesture in one popover, and it still
2034
+ fits a narrow date column. It writes the same
2035
+ pair of stores the plain text inputs use. -->
2036
+ {#if FilterRangePicker}
2037
+ {@const RangePicker = FilterRangePicker}
2038
+ {@const from = fromIsoDateLocal(
2039
+ filterRowValues[rendered.column.id],
2040
+ )}
2041
+ {@const to = fromIsoDateLocal(
2042
+ filterMenuValues[rendered.column.id]?.valueTo,
2043
+ )}
2044
+ <RangePicker
2045
+ value={from && to ? [from, to] : null}
2046
+ formatString="yyyy-MM-dd"
2047
+ block
2048
+ placeholder="Range…"
2049
+ onChange={(range) => {
2050
+ updateFilterRow(
2051
+ rendered.column.id,
2052
+ range ? toIsoDateLocal(range[0]) : "",
2053
+ );
2054
+ updateFilterMenuValueTo(
2055
+ rendered.column.id,
2056
+ range ? toIsoDateLocal(range[1]) : "",
2057
+ );
2058
+ }}
2059
+ />
2060
+ {/if}
2061
+ {:else}
2062
+ {@const FilterPicker = FilterDatePicker}
2063
+ <FilterPicker
2064
+ value={(filterRowValues[rendered.column.id] ?? "") || null}
2065
+ formatString="yyyy-MM-dd"
2066
+ dropDownDisplayMode="calendar"
2067
+ block
2068
+ nullable
2069
+ placeholder="Filter…"
2070
+ onChange={(d) =>
2071
+ updateFilterRow(rendered.column.id, toIsoDateLocal(d))}
2072
+ />
2073
+ {/if}
1903
2074
  {:else if activeOperator !== "isBlank" && activeOperator !== "isNotBlank"}
1904
2075
  {@const frType = getEditorInputType(
1905
2076
  rendered.column.columnDef.editorType ?? "text",
@@ -2177,12 +2348,11 @@
2177
2348
  rendered.column.columnDef.cellFlash,
2178
2349
  ),
2179
2350
  }}
2180
- {...getGridCellA11yProps({
2181
- id: getGridCellDomId(ctrl.gridDomId, rowIndex, colIndex),
2182
- rowIndex: rowIndex + 1,
2183
- colIndex: colIndex + 1,
2184
- selected: isRowSelected(row.id),
2185
- })}
2351
+ role="gridcell"
2352
+ id={getGridCellDomId(ctrl.gridDomId, rowIndex, colIndex)}
2353
+ aria-colindex={colIndex + 1}
2354
+ aria-rowindex={rowIndex + 1}
2355
+ aria-selected={isRowSelected(row.id)}
2186
2356
  >
2187
2357
  {#if inRowEdit || isEditing}
2188
2358
  <!-- The editing cell stays empty until the lazy
@@ -2194,11 +2364,15 @@
2194
2364
  <CellEditor {ctrl} column={rendered.column} {row} fullRow={inRowEdit} />
2195
2365
  {/if}
2196
2366
  {:else}
2197
- {@render cellBodyWithFormat(
2198
- row,
2199
- rendered.column,
2200
- cellValue,
2201
- )}
2367
+ {#if plainCellBody}
2368
+ {@render cellBody(row, rendered.column, cellValue)}
2369
+ {:else}
2370
+ {@render cellBodyWithFormat(
2371
+ row,
2372
+ rendered.column,
2373
+ cellValue,
2374
+ )}
2375
+ {/if}
2202
2376
  {/if}
2203
2377
  {#if !isEditing && fillHandleCell && fillHandleCell.rowIndex === rowIndex && fillHandleCell.colIndex === colIndex}
2204
2378
  <!-- Excel-style fill handle: drag down/right to
@@ -2948,6 +3122,31 @@
2948
3122
  {#if ctrl.chartingEnabled && ctrl.chartPanelOpen && ChartPanelView}
2949
3123
  <ChartPanelView {ctrl} />
2950
3124
  {/if}
3125
+
3126
+ <!-- Bulk-action bar. INSIDE the root, unlike GridMenus: it is positioned
3127
+ `absolute` against it, so moving it outside would anchor it to the
3128
+ page. The renderer is Enterprise and arrives through the
3129
+ selection-bar-view seam; without it, a short note in its place. -->
3130
+ {#if ctrl.selectionBarOn && ctrl.selectionBarTarget.ids.length > 0}
3131
+ {#if getSelectionBarView()}
3132
+ {@const SelectionBarView = getSelectionBarView()}
3133
+ <SelectionBarView {ctrl} />
3134
+ {:else}
3135
+ <div
3136
+ class="sv-grid-scheduler-upsell sv-grid-selection-bar-upsell"
3137
+ data-position={ctrl.selectionBarPosition}
3138
+ role="note"
3139
+ >
3140
+ <strong>Bulk-action bar</strong>
3141
+ <p>
3142
+ The selection bar is an Enterprise feature. Install
3143
+ <code>@svgrid/enterprise</code> and call <code>enableSelectionBar()</code>
3144
+ to render it.
3145
+ </p>
3146
+ {@render enterpriseLicenseNote()}
3147
+ </div>
3148
+ {/if}
3149
+ {/if}
2951
3150
  </div>
2952
3151
  <!-- /.sv-grid-root -->
2953
3152
 
@@ -217,6 +217,78 @@ export type ContextMenuItem<TData extends RowData = RowData> =
217
217
  action: (target: ContextMenuTarget<TData>) => void;
218
218
  };
219
219
 
220
+ /** What a {@link SelectionBarAction} is handed when it runs. */
221
+ export type SelectionBarTarget<TData extends RowData = RowData> = {
222
+ /** The selected rows, in the grid's current display order. */
223
+ rows: TData[];
224
+ /** Their row ids, same order as `rows`. */
225
+ ids: string[];
226
+ };
227
+
228
+ /**
229
+ * One button on the selection bar. Same shape as {@link ContextMenuItem}'s
230
+ * object form - key, label, optional hidden/disabled predicates, and an action -
231
+ * except everything here is handed the whole SELECTION rather than one cell.
232
+ */
233
+ export type SelectionBarAction<TData extends RowData = RowData> = {
234
+ key: string;
235
+ label: string;
236
+ /**
237
+ * Inline SVG path data (the `d` of a single 24x24 path) drawn to the left of
238
+ * the label. A string rather than a component so an action array stays plain
239
+ * data that can live in a `.ts` module next to the columns.
240
+ */
241
+ icon?: string;
242
+ /** Render in the destructive style (red). Does not add a confirmation. */
243
+ danger?: boolean;
244
+ /** Hide the button entirely for this selection. */
245
+ hidden?: (target: SelectionBarTarget<TData>) => boolean;
246
+ /** Render greyed-out and non-clickable for this selection. */
247
+ disabled?: (target: SelectionBarTarget<TData>) => boolean;
248
+ /** Invoked on click. The bar stays open; clear the selection yourself if the
249
+ * action should dismiss it. */
250
+ action: (target: SelectionBarTarget<TData>) => void;
251
+ };
252
+
253
+ /**
254
+ * A built-in bar button, named by key - the same convention
255
+ * {@link ContextMenuItem} uses for its string form.
256
+ *
257
+ * - `'selectAll'` selects every row in the current view.
258
+ * - `'editFields'` opens the bulk-edit dialog: pick a field, set one value,
259
+ * apply it to every selected row in a single undo step.
260
+ * - `'separator'` draws a divider.
261
+ */
262
+ export type SelectionBarBuiltin = "selectAll" | "editFields" | "separator";
263
+
264
+ /**
265
+ * Full form of the {@link SvGridProps.selectionBar} prop, for when the array
266
+ * shorthand is not enough.
267
+ */
268
+ export type SelectionBarConfig<TData extends RowData = RowData> = {
269
+ /**
270
+ * The buttons, left to right. Strings are {@link SelectionBarBuiltin} keys,
271
+ * objects are your own actions - so a Jira-shaped bar is
272
+ * `['selectAll', 'editFields', 'separator', myAction]`.
273
+ */
274
+ actions?: ReadonlyArray<SelectionBarAction<TData> | SelectionBarBuiltin>;
275
+ /**
276
+ * Which edge of the grid the bar floats against. Default `'bottom'`, where
277
+ * the eye is already going after ticking a checkbox and where it covers the
278
+ * rows a user is least likely to be reading.
279
+ */
280
+ position?: "bottom" | "top";
281
+ /**
282
+ * How many actions stay on the bar before the rest collapse into an overflow
283
+ * menu. Default 6, which fits a full issue-tracker set (select all, edit,
284
+ * status, watch, delete) without collapsing. A bar wide enough to need
285
+ * horizontal scrolling is a bar whose last button nobody finds.
286
+ */
287
+ maxVisible?: number;
288
+ /** Hide the trailing clear (x) button. Default false. */
289
+ hideClear?: boolean;
290
+ };
291
+
220
292
  /** A single Kanban lane (board column). {@link BoardConfig}. */
221
293
  export type BoardLane = {
222
294
  /** Lane id - equals the `groupBy` field value of the cards it holds. */
@@ -1003,6 +1075,29 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
1003
1075
  * browser menu shows instead).
1004
1076
  */
1005
1077
  contextMenu?: boolean | ReadonlyArray<ContextMenuItem<TData>>;
1078
+ /**
1079
+ * Floating bar that appears over the grid while rows are selected, showing
1080
+ * the count and the actions that apply to the whole selection - the pattern
1081
+ * an issue tracker uses for bulk edit.
1082
+ *
1083
+ * `true` gives the count and a Clear button. An array is the shorthand for
1084
+ * `{ actions }`. Pass {@link SelectionBarConfig} to also set `position`
1085
+ * (`'bottom'` default, or `'top'`), `maxVisible` and `hideClear`. Omitted /
1086
+ * `false` is off.
1087
+ *
1088
+ * Default **off**. This is chrome that floats over your content, not a
1089
+ * gesture: it overlaps a row, and what belongs on it is specific to the app.
1090
+ *
1091
+ * **The prop and its types are free; the bar itself is an Enterprise
1092
+ * feature.** Install `@svgrid/enterprise` and call `enableSelectionBar()`
1093
+ * (or `installEnterprise`, which does it for you) to render it - without
1094
+ * that the grid shows a short upsell note in its place, the same way the
1095
+ * scheduler and board views do.
1096
+ */
1097
+ selectionBar?:
1098
+ | boolean
1099
+ | ReadonlyArray<SelectionBarAction<TData> | SelectionBarBuiltin>
1100
+ | SelectionBarConfig<TData>;
1006
1101
  /**
1007
1102
  * The feature set built with `tableFeatures({ ... })`. Optional - the
1008
1103
  * `sortable` / `filterable` / `groupable` shortcuts below inject the
@@ -1329,6 +1424,28 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
1329
1424
  * supplying per-row heights).
1330
1425
  */
1331
1426
  autoRowHeight?: boolean;
1427
+ /**
1428
+ * Let the user drag a row's bottom edge to change its height. **Off by
1429
+ * default**, because a resizable row needs somewhere to grab and most grids
1430
+ * do not want a drag target on every row.
1431
+ *
1432
+ * The grid remembers the heights itself, so this works as a bare boolean -
1433
+ * no `rowHeight` function required. Drag, or focus the grip and use Up/Down
1434
+ * (Shift for 1px steps).
1435
+ *
1436
+ * Turning this on also turns on the **row header column** ({@link
1437
+ * showRowNumbers}), because that is where the grip lives and where a
1438
+ * spreadsheet puts it - a drag target on the edge of a data cell works but
1439
+ * reads as an accident. An explicit `showRowNumbers={false}` still wins; the
1440
+ * grip then falls back to the row's first cell, and a column of your own
1441
+ * carrying `cellClass: 'sv-row-gutter'` is used ahead of either.
1442
+ *
1443
+ * Ignored under `autoRowHeight`, where the content decides the height and a
1444
+ * manual one would immediately be overwritten. For full control of the
1445
+ * heights - persisting them, sharing them between grids - pass a
1446
+ * function-valued {@link rowHeight} and use the `rowResize` action directly.
1447
+ */
1448
+ rowResize?: boolean;
1332
1449
  /**
1333
1450
  * Height (px) of a single column-header level row. With multi-level
1334
1451
  * (grouped) headers the total header height is `levels * headerHeight`,
@@ -1387,6 +1504,24 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
1387
1504
  * win once they happen.
1388
1505
  */
1389
1506
  fitColumns?: boolean;
1507
+ /**
1508
+ * Let the user drag a column's edge to change its width. **Off by default**,
1509
+ * so `width` means the width you asked for until you say otherwise.
1510
+ *
1511
+ * Turn it on for grids the reader is meant to arrange - a spreadsheet, a
1512
+ * dense report, anything with columns whose content varies in length. Leave
1513
+ * it off for layouts you control, and for columns that must keep their size:
1514
+ * a row-number gutter, a checkbox column, an icon column.
1515
+ *
1516
+ * It is grid-wide rather than per-column: there is no `resizable: false` on a
1517
+ * single column definition. `api.autosizeColumn()`, `fitColumns` and
1518
+ * programmatic width changes work either way - this only governs the drag
1519
+ * handle.
1520
+ *
1521
+ * The handles come from the `columnResize` action, loaded on demand: a grid
1522
+ * that leaves this off never fetches that code.
1523
+ */
1524
+ columnResize?: boolean;
1390
1525
  /**
1391
1526
  * Make the grid usable on narrow screens. When the grid's own width drops
1392
1527
  * below the breakpoint (default `640`px), pinned columns are un-pinned so the
@@ -1415,6 +1550,23 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
1415
1550
  * is the shortcut alias and wins over it.
1416
1551
  */
1417
1552
  enableCellSelection?: boolean;
1553
+ /**
1554
+ * Excel-style drag-and-drop of a selected cell range: grab the range's
1555
+ * border and drag it somewhere else to MOVE the values there, or hold
1556
+ * Ctrl (Cmd on macOS) to COPY them instead. The modifier is read at drop
1557
+ * time, so it can be pressed or released mid-drag.
1558
+ *
1559
+ * On by default whenever cell selection is on, matching the fill handle.
1560
+ * Set `false` to keep a border pointerdown starting a fresh selection, which
1561
+ * is what it did before this existed.
1562
+ *
1563
+ * The grab strip is the outer 4px of the range border, so a pointerdown
1564
+ * anywhere further inside still starts a new selection. A drop is refused
1565
+ * outright - nothing changes - when the destination would fall outside the
1566
+ * grid, or when any source or destination cell is read-only. It does not
1567
+ * move the part that fits and silently drop the rest.
1568
+ */
1569
+ moveCells?: boolean;
1418
1570
  /**
1419
1571
  * Highlight the row under the pointer. Default **false** - the hover tint can
1420
1572
  * compete with the cell selection / fill marquee. Set `true` to opt in; when
@@ -1498,7 +1650,7 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
1498
1650
  charting?: boolean | ChartingConfig<TData>;
1499
1651
  /**
1500
1652
  * Render the header column menu (⋮) as a tabbed popover - **General**,
1501
- * **Filter**, and **Columns** tabs (the AG-Grid layout). Defaults to `false`,
1653
+ * **Filter**, and **Columns** tabs (the tabbed layout). Defaults to `false`,
1502
1654
  * which keeps the flat menu (actions list + "Choose columns" submenu).
1503
1655
  */
1504
1656
  columnMenuTabs?: boolean;