@svgrid/grid 2.6.22 → 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 (115) hide show
  1. package/dist/GridMenus.svelte +184 -6
  2. package/dist/SvAutoComplete.svelte +4 -0
  3. package/dist/SvComboBox.svelte +5 -1
  4. package/dist/SvDateRangeInput.svelte +25 -3
  5. package/dist/SvDropDownList.svelte +4 -0
  6. package/dist/SvForm.svelte +8 -8
  7. package/dist/SvGrid.controller.svelte.d.ts +48 -1
  8. package/dist/SvGrid.controller.svelte.js +214 -52
  9. package/dist/SvGrid.css +221 -0
  10. package/dist/SvGrid.helpers.d.ts +55 -0
  11. package/dist/SvGrid.helpers.js +83 -0
  12. package/dist/SvGrid.svelte +145 -0
  13. package/dist/SvGrid.types.d.ts +105 -0
  14. package/dist/SvGridCellEditor.svelte +99 -17
  15. package/dist/SvGridSelect.svelte +6 -2
  16. package/dist/SvGridSelect.svelte.d.ts +1 -1
  17. package/dist/SvListBox.svelte +8 -0
  18. package/dist/SvMultiSelect.svelte +6 -2
  19. package/dist/SvMultiSelect.svelte.d.ts +1 -1
  20. package/dist/SvNumberInput.svelte +4 -0
  21. package/dist/SvRichCell.svelte +125 -0
  22. package/dist/SvRichCell.svelte.d.ts +15 -0
  23. package/dist/SvTextArea.svelte +5 -1
  24. package/dist/SvTreeSelect.svelte +6 -2
  25. package/dist/SvTreeSelect.svelte.d.ts +1 -1
  26. package/dist/builtin-editors.d.ts +2 -2
  27. package/dist/builtin-editors.js +19 -3
  28. package/dist/cdn/GridMenus-BzWXQjv3.js +600 -0
  29. package/dist/cdn/GridMenus-xr_rHx8F.js +601 -0
  30. package/dist/cdn/SvDateRangeInput-CaOuMs8O.js +198 -0
  31. package/dist/cdn/SvDateRangeInput-DMLKmGEc.js +199 -0
  32. package/dist/cdn/SvDateTimePicker-CCbDZNZB.js +816 -0
  33. package/dist/cdn/SvDateTimePicker-sonaH0oh.js +812 -0
  34. package/dist/cdn/SvGridCellEditor-BAPSC7EL.js +550 -0
  35. package/dist/cdn/SvGridCellEditor-BwzmUWtL.js +549 -0
  36. package/dist/cdn/date-format-BNii4zeD.js +1393 -0
  37. package/dist/cdn/date-format-BnnHlqGw.js +1395 -0
  38. package/dist/cdn/{editor-registry-CiI0xlKg.js → editor-registry-BhgE3S84.js} +51 -25
  39. package/dist/cdn/{src-D1lXwq1l.js → src-DGo7IHug.js} +5730 -5198
  40. package/dist/cdn/{src-C9Hihx1W.js → src-skQN5eqh.js} +7216 -6684
  41. package/dist/cdn/svgrid.js +14 -12
  42. package/dist/cdn/svgrid.svelte-external.js +14 -12
  43. package/dist/cell-values.d.ts +16 -0
  44. package/dist/cell-values.js +28 -0
  45. package/dist/clipboard.d.ts +22 -0
  46. package/dist/clipboard.js +405 -27
  47. package/dist/editing.js +12 -7
  48. package/dist/editors/cell-editors.d.ts +11 -0
  49. package/dist/editors/cell-editors.js +18 -1
  50. package/dist/grid-messages.d.ts +15 -0
  51. package/dist/grid-messages.js +15 -0
  52. package/dist/index.d.ts +4 -0
  53. package/dist/index.js +4 -0
  54. package/dist/list-option.d.ts +6 -0
  55. package/dist/menus.js +40 -7
  56. package/dist/sanitize-html.d.ts +37 -0
  57. package/dist/sanitize-html.js +234 -0
  58. package/dist/selection-bar-view.svelte.d.ts +25 -0
  59. package/dist/selection-bar-view.svelte.js +13 -0
  60. package/dist/selection.d.ts +2 -1
  61. package/dist/selection.js +129 -13
  62. package/package.json +1 -1
  63. package/src/GridMenus.svelte +184 -6
  64. package/src/SvAutoComplete.svelte +4 -0
  65. package/src/SvComboBox.svelte +5 -1
  66. package/src/SvDateRangeInput.svelte +25 -3
  67. package/src/SvDropDownList.svelte +4 -0
  68. package/src/SvForm.svelte +8 -8
  69. package/src/SvGrid.controller.svelte.ts +251 -61
  70. package/src/SvGrid.css +221 -0
  71. package/src/SvGrid.helpers.ts +87 -0
  72. package/src/SvGrid.svelte +145 -0
  73. package/src/SvGrid.types.ts +112 -0
  74. package/src/SvGridCellEditor.svelte +99 -17
  75. package/src/SvGridSelect.svelte +6 -2
  76. package/src/SvListBox.svelte +8 -0
  77. package/src/SvMultiSelect.svelte +6 -2
  78. package/src/SvNumberInput.svelte +4 -0
  79. package/src/SvRichCell.svelte +125 -0
  80. package/src/SvTextArea.svelte +5 -1
  81. package/src/SvTreeSelect.svelte +6 -2
  82. package/src/builtin-editors.test.ts +18 -1
  83. package/src/builtin-editors.ts +19 -3
  84. package/src/cell-values.ts +30 -0
  85. package/src/clipboard.test.ts +93 -20
  86. package/src/clipboard.ts +433 -33
  87. package/src/date-string-column.test.ts +87 -0
  88. package/src/editing.test.ts +25 -0
  89. package/src/editing.ts +12 -9
  90. package/src/editor-block-prop.test.ts +132 -0
  91. package/src/editors/cell-editors.ts +27 -1
  92. package/src/grid-messages.ts +34 -0
  93. package/src/index.ts +13 -0
  94. package/src/list-option.ts +6 -0
  95. package/src/menus.test.ts +43 -0
  96. package/src/menus.ts +37 -7
  97. package/src/move-cells.test.ts +850 -0
  98. package/src/number-editor-literal.test.ts +84 -0
  99. package/src/number-editor.grid.test.ts +160 -0
  100. package/src/sanitize-html.test.ts +182 -0
  101. package/src/sanitize-html.ts +235 -0
  102. package/src/selection-bar-view.svelte.ts +36 -0
  103. package/src/selection.test.ts +157 -0
  104. package/src/selection.ts +131 -13
  105. package/src/svgrid.filter-menu-listbox.svelte.test.ts +56 -10
  106. package/src/svgrid.in-suggest-snapshot.svelte.test.ts +91 -0
  107. package/src/svgrid.menu-scroll-close.test.ts +204 -0
  108. package/src/svgrid.selection-bar-seam.test.ts +185 -0
  109. package/src/svgrid.upsell-license.test.ts +117 -0
  110. package/dist/cdn/GridMenus-BuoBPqxx.js +0 -493
  111. package/dist/cdn/GridMenus-n4llxoOI.js +0 -494
  112. package/dist/cdn/SvDateTimePicker-CHnUkWcH.js +0 -2206
  113. package/dist/cdn/SvDateTimePicker-CRQH_U7E.js +0 -2201
  114. package/dist/cdn/SvGridCellEditor-BGUCzuPB.js +0 -523
  115. package/dist/cdn/SvGridCellEditor-G8IMWIws.js +0 -522
package/dist/SvGrid.css CHANGED
@@ -14,6 +14,34 @@
14
14
  position: relative;
15
15
  font-family: var(--sg-font, inherit);
16
16
  }
17
+
18
+ /*
19
+ * Room for the selection bar, INSIDE the scroll area.
20
+ *
21
+ * The bar floats - it must not resize the grid, or every layout holding one
22
+ * jumps the moment a checkbox is ticked. So the grid keeps its exact size and
23
+ * the SCROLLER gains bottom padding instead: scrollable space under the last
24
+ * row, the height of the bar. The bar then floats over padding rather than
25
+ * over data, and every row can still be scrolled into clear view.
26
+ *
27
+ * Shrinking the grid instead (padding on the root) was the first attempt. It
28
+ * kept rows visible but made the table resize under the user, which is worse.
29
+ *
30
+ * Only while the bar is up, so an unselected grid scrolls exactly as before.
31
+ */
32
+ .sv-grid-root[data-selbar] {
33
+ --sg-selbar-reserve: 68px;
34
+ }
35
+ /* The upsell note is prose, so it is taller than the bar it stands in for. */
36
+ .sv-grid-root[data-selbar^='upsell'] {
37
+ --sg-selbar-reserve: 116px;
38
+ }
39
+ .sv-grid-root[data-selbar$='bottom'] .sv-grid-container {
40
+ padding-bottom: var(--sg-selbar-reserve);
41
+ }
42
+ .sv-grid-root[data-selbar$='top'] .sv-grid-container {
43
+ padding-top: var(--sg-selbar-reserve);
44
+ }
17
45
  .sv-grid-sr-only {
18
46
  position: absolute;
19
47
  width: 1px;
@@ -928,6 +956,18 @@
928
956
  padding: 5px var(--sg-filter-cell-px, var(--sg-cell-px, 7px));
929
957
  }
930
958
 
959
+ /* The rich date picker in the filter row. It enters `.sv-grid-filter-row-control`
960
+ as a `.sv-field`, which carries none of the flex sizing every other filter
961
+ control has, so it laid itself out at the full cell width NEXT TO the
962
+ operator button: the box was cut off at the column line and the calendar
963
+ toggle ended up outside the cell, underneath the next column's filter, where
964
+ no click could reach it. Size it like `.sv-grid-filter-value` so it shares
965
+ the row instead of overflowing it. */
966
+ .sv-grid-filter-row-control > .sv-field {
967
+ flex: 3 1 0;
968
+ min-width: 0;
969
+ }
970
+
931
971
  .sv-grid-filter-operator {
932
972
  flex: 2 1 0;
933
973
  min-width: 0;
@@ -1196,6 +1236,57 @@ select.sv-grid-fr-editor {
1196
1236
  overflow: visible;
1197
1237
  }
1198
1238
 
1239
+ /* ---- The editing cell's border -------------------------------------------
1240
+ One ring for every editor, drawn by the CELL rather than by each editor.
1241
+ They used to draw their own and they disagreed: the text/number/date inputs
1242
+ put a 2px accent-at-30% halo OUTSIDE the cell, the dropdown trigger put a
1243
+ solid 2px outline INSIDE it (outline-offset: -2px), and the checkbox drew
1244
+ nothing - so the border was single, double or missing depending on which
1245
+ column you happened to be editing.
1246
+
1247
+ Excel's shape: a 2px band centred on the cell's own grid line, so half of it
1248
+ covers the line and half sits inside the cell. `inset: -1px` with a 2px
1249
+ border straddles exactly that way. It is a pseudo-element, not the cell's
1250
+ `box-shadow`, because every editor fills the cell with an opaque background
1251
+ and would paint over an inset shadow; ::after is the cell's last child, so
1252
+ it lands on top of the editor. `pointer-events: none` keeps it out of the
1253
+ way of clicks on the editor underneath. */
1254
+ .sv-grid-cell.sv-grid-cell-editing::after {
1255
+ content: "";
1256
+ position: absolute;
1257
+ inset: -1px;
1258
+ border: 2px solid var(--sg-accent, #0b63f3);
1259
+ pointer-events: none;
1260
+ }
1261
+ /* The shared active/editing rule paints an inset ring. For an editing cell the
1262
+ ::after above replaces it - keeping both would show two lines through any
1263
+ editor that does not fill the cell (the checkbox). */
1264
+ .sv-grid-cell.sv-grid-cell-editing {
1265
+ box-shadow: none;
1266
+ }
1267
+ /* Each editor's own focus ring, off while it is the in-cell editor. Elsewhere
1268
+ (filter row, standalone use) they keep it. Three class selectors so these win
1269
+ over the dropdown's and the picker's component-scoped `:focus` rules. */
1270
+ .sv-grid-cell.sv-grid-cell-editing .sv-grid-cell-editor:focus,
1271
+ .sv-grid-cell.sv-grid-cell-editing .sv-grid-dropdown-trigger:focus,
1272
+ .sv-grid-cell.sv-grid-cell-editing .sv-dtp__field:focus-within {
1273
+ box-shadow: none;
1274
+ outline: none;
1275
+ }
1276
+ /* The checkbox editor's own focus outline, plus any ring a host page puts on
1277
+ focused buttons (both demo shells do, via a global `button:focus-visible`).
1278
+ Either one is a second border around the only control in the cell, which the
1279
+ cell already frames. Outside an editing cell - the row-selection checkbox,
1280
+ read-only cells - the outline stays.
1281
+
1282
+ The rating stars deliberately keep their focus ring: with five buttons in a
1283
+ radiogroup it is what shows which star the arrow keys are on. */
1284
+ .sv-grid-cell.sv-grid-cell-editing .sv-grid-checkbox:focus-visible {
1285
+ box-shadow: none;
1286
+ outline: none;
1287
+ }
1288
+
1289
+
1199
1290
  /* Excel-style fill handle: a 7×7 blue square anchored to the bottom-
1200
1291
  right corner of the active cell or selection range. The parent
1201
1292
  `.sv-grid-cell` is `position: relative` (sticky cells override
@@ -1246,6 +1337,44 @@ select.sv-grid-fr-editor {
1246
1337
  transform-origin: bottom right;
1247
1338
  }
1248
1339
 
1340
+ /* Placement for the selection-bar upsell note. The BAR itself is an Enterprise
1341
+ component and carries its own scoped styles - this is only the stand-in the
1342
+ free grid shows when no renderer is registered, positioned on the same edge
1343
+ the real bar would use so the layout does not jump once it is installed. */
1344
+ .sv-grid-selection-bar-upsell {
1345
+ position: absolute;
1346
+ left: 50%;
1347
+ transform: translateX(-50%);
1348
+ /* Same slot as the real bar: above the custom scrollbars (40/41/42), below
1349
+ find-in-grid (60) and the menus (900+). Sharing the value matters because
1350
+ installing enterprise must not change where this sits in the stack. */
1351
+ z-index: 45;
1352
+ max-width: calc(100% - 24px);
1353
+ }
1354
+ .sv-grid-selection-bar-upsell[data-position="bottom"] { bottom: 16px; }
1355
+ .sv-grid-selection-bar-upsell[data-position="top"] { top: 16px; }
1356
+
1357
+ /* Excel-style range drag-and-drop (`moveCells`). The grab target is the outer
1358
+ 4px of the selection border, which is PAINTED (an inset box-shadow) rather
1359
+ than an element, so there is nothing to hang `cursor` off per cell. Instead
1360
+ the controller flips one attribute on the root while the pointer is over the
1361
+ strip, and this turns the whole grid's cursor for as long as that lasts -
1362
+ the cursor is on the border anyway, so the effect is identical to a hit
1363
+ target and costs nothing per cell. `!important` because several cell states
1364
+ (editing, checkbox, link) set their own cursor at higher specificity, and
1365
+ during a grab the move cursor has to win over all of them. */
1366
+ .sv-grid-root[data-move-grab="true"],
1367
+ .sv-grid-root[data-move-grab="true"] .sv-grid-cell {
1368
+ cursor: move !important;
1369
+ }
1370
+ /* The fill handle keeps its own crosshair: it sits in the bottom-right corner,
1371
+ inside the grab strip's territory, and it is a real element that captures
1372
+ the pointerdown first. Letting it read as "move" would promise the wrong
1373
+ gesture. */
1374
+ .sv-grid-root[data-move-grab="true"] .sv-grid-fill-handle {
1375
+ cursor: crosshair !important;
1376
+ }
1377
+
1249
1378
  /* Excel-style fill-handle drag marquee: ONE thin dashed rectangle around the
1250
1379
  whole target range (source + fill-into area), not a thick per-cell border or
1251
1380
  fill tint. Drawn as four background-image layers on the boundary cells - no
@@ -1435,6 +1564,76 @@ select.sv-grid-fr-editor {
1435
1564
  height: 100%;
1436
1565
  }
1437
1566
 
1567
+ /* Rich date / datetime / time editor (SvDateTimePicker) inside a cell.
1568
+ The picker is a standalone UI-kit control: a fixed 34px pill with its own
1569
+ 1px border, radius and outer focus ring. Dropped into a cell it stayed in
1570
+ flow, so that pill drew a second, smaller box inside the cell's editing
1571
+ ring, its radius pulled the corners away from the grid lines, and its ring
1572
+ bled past the cell - the borders never lined up with the column. In-cell it
1573
+ gets the same treatment as `.sv-grid-cell-editor`: taken out of flow, pinned
1574
+ to the cell box and stripped of its own chrome - the cell draws the single
1575
+ editing ring for every editor. Scoped to the editing cell, so the filter row
1576
+ and standalone pickers keep their box.
1577
+ Three class selectors (not two) so these win over the component's own
1578
+ scoped rules whatever order the two stylesheets land in. */
1579
+ .sv-grid-cell.sv-grid-cell-editing .sv-dtp {
1580
+ position: absolute;
1581
+ inset: 0;
1582
+ display: block;
1583
+ width: auto;
1584
+ }
1585
+ .sv-grid-cell.sv-grid-cell-editing .sv-dtp__field {
1586
+ height: 100%;
1587
+ border: 0;
1588
+ border-radius: 0;
1589
+ box-shadow: none;
1590
+ /* The compact buttons below carry their own breathing room. */
1591
+ padding-inline: 0;
1592
+ }
1593
+ /* Match the cell's own type instead of the control's fixed 13px, so the value
1594
+ doesn't change size the moment the cell goes into edit. */
1595
+ .sv-grid-cell.sv-grid-cell-editing .sv-dtp__input {
1596
+ font: inherit;
1597
+ }
1598
+ /* Date columns are narrow and the two 26px buttons ate enough of them to clip
1599
+ the value. Trimmed wherever the picker sits inside a column's width - the
1600
+ cell editor and the filter row - but not for standalone pickers. */
1601
+ .sv-grid-cell.sv-grid-cell-editing .sv-dtp__clear,
1602
+ .sv-grid-cell.sv-grid-cell-editing .sv-dtp__toggle,
1603
+ .sv-grid-filter-row-control > .sv-field .sv-dtp__toggle {
1604
+ width: 20px;
1605
+ height: 20px;
1606
+ }
1607
+ /* Safety valve for a very narrow date column: the toggle keeps its 20px while
1608
+ there is room and gives some back rather than spilling over the column line
1609
+ onto the next filter, where no click could reach it. */
1610
+ .sv-grid-filter-row-control > .sv-field .sv-dtp__toggle {
1611
+ flex: 0 1 auto;
1612
+ min-width: 0;
1613
+ }
1614
+ /* The filter row drops the picker's clear button. It costs ~20px of a filter
1615
+ cell that has none to spare, next to the operator button and the calendar
1616
+ toggle, and a date column is usually narrow. Nothing is lost: emptying the
1617
+ text clears the filter, as does Clear filter in the column menu. */
1618
+ .sv-grid-filter-row-control > .sv-field .sv-dtp__clear {
1619
+ display: none;
1620
+ }
1621
+
1622
+ /* The range field `between` uses (SvDateRangeInput). Its stock chrome - a 10px
1623
+ inset, a 15px icon, two 6px gaps and a 24px clear - adds up to more than a
1624
+ narrow date column has, and the clear button hung out past the field's own
1625
+ border. Trimmed to fit; the clear can shrink further before anything spills. */
1626
+ .sv-grid-filter-row-control > .sv-field .sv-dri__field {
1627
+ padding-inline: 4px 2px;
1628
+ gap: 3px;
1629
+ }
1630
+ .sv-grid-filter-row-control > .sv-field .sv-dri__clear {
1631
+ width: 20px;
1632
+ height: 20px;
1633
+ flex: 0 1 auto;
1634
+ min-width: 0;
1635
+ }
1636
+
1438
1637
  /* List editor: native <select>. For multi-select it grows into a
1439
1638
  small popup-style listbox that hangs over the cell. */
1440
1639
  .sv-grid-cell-editor-list {
@@ -1981,6 +2180,16 @@ select.sv-grid-fr-editor {
1981
2180
  font-weight: 600;
1982
2181
  color: var(--sg-muted, #475569);
1983
2182
  }
2183
+ /* Trailing status on a section heading: how much of the list is selected, or
2184
+ that a condition is ANDed with it. Pushed to the far edge and de-emphasised
2185
+ so it reads as state, not as part of the heading. */
2186
+ .sv-grid-menu-head-note {
2187
+ margin-inline-start: auto;
2188
+ font-weight: 500;
2189
+ font-size: 0.85em;
2190
+ color: var(--sg-muted, #64748b);
2191
+ opacity: 0.85;
2192
+ }
1984
2193
 
1985
2194
  .sv-grid-menu-search {
1986
2195
  width: 100%;
@@ -2745,6 +2954,18 @@ select.sv-grid-fr-editor {
2745
2954
  }
2746
2955
  .sv-grid-scheduler-upsell strong { font-size: 0.95rem; }
2747
2956
  .sv-grid-scheduler-upsell p { margin: 0; font-size: 0.85rem; max-width: 42ch; }
2957
+ /* The licensing line sits under the "how to install it" line, quieter than it:
2958
+ the first sentence is what unblocks the developer looking at this, the second
2959
+ is what they need before shipping. */
2960
+ .sv-grid-upsell-license {
2961
+ font-size: 0.8rem;
2962
+ opacity: 0.85;
2963
+ }
2964
+ .sv-grid-upsell-license a {
2965
+ color: inherit;
2966
+ text-decoration: underline;
2967
+ text-underline-offset: 2px;
2968
+ }
2748
2969
  .sv-grid-scheduler-upsell code {
2749
2970
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
2750
2971
  font-size: 0.82em;
@@ -18,6 +18,61 @@ export declare function timeStringToDate(value: unknown): Date | null;
18
18
  export declare function dateToTimeString(d: Date | null): string | null;
19
19
  export declare function getEditableInputValue(editorType: CellEditorType, value: unknown): string;
20
20
  export declare function getEditorInputType(editorType: CellEditorType): "number" | "date" | "text" | "time" | "password" | "color" | "datetime-local";
21
+ /**
22
+ * Input type for the CELL EDITOR specifically, which differs from
23
+ * {@link getEditorInputType} in one place: `number` becomes a text input.
24
+ *
25
+ * `<input type="number">` runs the HTML value-sanitization algorithm, so it
26
+ * only ever reports a value that parses as a valid float. Every intermediate
27
+ * state on the way to `12.5` - `12.` - is not one, and the element reports the
28
+ * empty string instead. The editor reads `event.currentTarget.value` into its
29
+ * draft, so the decimal point the user just typed disappeared. Same for a lone
30
+ * `-` on the way to a negative and `1e` on the way to `1e3`.
31
+ *
32
+ * A text input keeps the raw keystrokes, and the draft is coerced to a number
33
+ * at commit by `parseEditorValue` - which the editor already did. Pair it with
34
+ * `inputmode="decimal"` for the mobile keypad and
35
+ * {@link isNumericEditorInput} for the character filtering that
36
+ * `type="number"` used to provide.
37
+ *
38
+ * The filter-row and filter-menu inputs deliberately still use
39
+ * {@link getEditorInputType}: they feed a different pipeline, and changing
40
+ * them is a separate decision.
41
+ */
42
+ /**
43
+ * Should this column filter with the grid own date picker rather than the
44
+ * browser one?
45
+ *
46
+ * The grid convention is rich-by-default with an explicit `-native` opt-out,
47
+ * which the cell editor follows. The filter row did not: a `date` column
48
+ * edited through SvDateTimePicker was FILTERED through a native
49
+ * `<input type="date">`, so the same column offered two different date UIs.
50
+ */
51
+ /**
52
+ * A Date to the 'YYYY-MM-DD' a date filter compares against, using LOCAL
53
+ * parts. `toISOString()` would convert through UTC, so picking the 1st in a
54
+ * negative-offset zone would filter for the 31st of the month before.
55
+ */
56
+ export declare function toIsoDateLocal(value: Date | null | undefined): string;
57
+ /**
58
+ * The inverse of {@link toIsoDateLocal}: read a 'YYYY-MM-DD' filter value back
59
+ * as that LOCAL calendar day. `new Date('2026-09-10')` is parsed as UTC
60
+ * midnight, which is the 9th anywhere west of Greenwich - so handing the raw
61
+ * string to a date control and writing it back through `toIsoDateLocal` walks
62
+ * the value backwards a day per round trip.
63
+ */
64
+ export declare function fromIsoDateLocal(value: string | null | undefined): Date | null;
65
+ export declare function usesRichDateFilter(editorType: CellEditorType | undefined): boolean;
66
+ export declare function getCellEditorInputType(editorType: CellEditorType): "number" | "date" | "text" | "time" | "password" | "color" | "datetime-local";
67
+ /**
68
+ * Is `value` something the user could still be typing towards a number?
69
+ *
70
+ * Deliberately permissive about INTERMEDIATE states - `''`, `'-'`, `'12.'` and
71
+ * `'1e'` all pass, because rejecting them is the exact bug this replaces. It
72
+ * only rejects text that can never become a number, which is the guarantee
73
+ * `type="number"` used to give.
74
+ */
75
+ export declare function isNumericEditorInput(value: string): boolean;
21
76
  /** Resolve a stored value to an array form (for list/chips multi-select). */
22
77
  export declare function toValueArray(value: unknown): Array<string | number>;
23
78
  /** Look up the display label for a given option value. */
@@ -106,6 +106,89 @@ export function getEditorInputType(editorType) {
106
106
  return "color";
107
107
  return "text";
108
108
  }
109
+ /**
110
+ * Input type for the CELL EDITOR specifically, which differs from
111
+ * {@link getEditorInputType} in one place: `number` becomes a text input.
112
+ *
113
+ * `<input type="number">` runs the HTML value-sanitization algorithm, so it
114
+ * only ever reports a value that parses as a valid float. Every intermediate
115
+ * state on the way to `12.5` - `12.` - is not one, and the element reports the
116
+ * empty string instead. The editor reads `event.currentTarget.value` into its
117
+ * draft, so the decimal point the user just typed disappeared. Same for a lone
118
+ * `-` on the way to a negative and `1e` on the way to `1e3`.
119
+ *
120
+ * A text input keeps the raw keystrokes, and the draft is coerced to a number
121
+ * at commit by `parseEditorValue` - which the editor already did. Pair it with
122
+ * `inputmode="decimal"` for the mobile keypad and
123
+ * {@link isNumericEditorInput} for the character filtering that
124
+ * `type="number"` used to provide.
125
+ *
126
+ * The filter-row and filter-menu inputs deliberately still use
127
+ * {@link getEditorInputType}: they feed a different pipeline, and changing
128
+ * them is a separate decision.
129
+ */
130
+ /**
131
+ * Should this column filter with the grid own date picker rather than the
132
+ * browser one?
133
+ *
134
+ * The grid convention is rich-by-default with an explicit `-native` opt-out,
135
+ * which the cell editor follows. The filter row did not: a `date` column
136
+ * edited through SvDateTimePicker was FILTERED through a native
137
+ * `<input type="date">`, so the same column offered two different date UIs.
138
+ */
139
+ /**
140
+ * A Date to the 'YYYY-MM-DD' a date filter compares against, using LOCAL
141
+ * parts. `toISOString()` would convert through UTC, so picking the 1st in a
142
+ * negative-offset zone would filter for the 31st of the month before.
143
+ */
144
+ export function toIsoDateLocal(value) {
145
+ if (!value || Number.isNaN(value.getTime()))
146
+ return "";
147
+ const pad = (n) => String(n).padStart(2, "0");
148
+ return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}`;
149
+ }
150
+ /**
151
+ * The inverse of {@link toIsoDateLocal}: read a 'YYYY-MM-DD' filter value back
152
+ * as that LOCAL calendar day. `new Date('2026-09-10')` is parsed as UTC
153
+ * midnight, which is the 9th anywhere west of Greenwich - so handing the raw
154
+ * string to a date control and writing it back through `toIsoDateLocal` walks
155
+ * the value backwards a day per round trip.
156
+ */
157
+ export function fromIsoDateLocal(value) {
158
+ if (!value)
159
+ return null;
160
+ const parts = /^(\d{4})-(\d{2})-(\d{2})/.exec(value);
161
+ if (!parts) {
162
+ const loose = new Date(value);
163
+ return Number.isNaN(loose.getTime()) ? null : loose;
164
+ }
165
+ const local = new Date(Number(parts[1]), Number(parts[2]) - 1, Number(parts[3]));
166
+ return Number.isNaN(local.getTime()) ? null : local;
167
+ }
168
+ export function usesRichDateFilter(editorType) {
169
+ if (!editorType)
170
+ return false;
171
+ if (String(editorType).endsWith("-native"))
172
+ return false;
173
+ const base = baseEditor(editorType);
174
+ return base === "date" || base === "datetime" || base === "time";
175
+ }
176
+ export function getCellEditorInputType(editorType) {
177
+ if (baseEditor(editorType) === "number")
178
+ return "text";
179
+ return getEditorInputType(editorType);
180
+ }
181
+ /**
182
+ * Is `value` something the user could still be typing towards a number?
183
+ *
184
+ * Deliberately permissive about INTERMEDIATE states - `''`, `'-'`, `'12.'` and
185
+ * `'1e'` all pass, because rejecting them is the exact bug this replaces. It
186
+ * only rejects text that can never become a number, which is the guarantee
187
+ * `type="number"` used to give.
188
+ */
189
+ export function isNumericEditorInput(value) {
190
+ return /^-?(\d*\.?\d*)(e-?\d*)?$/i.test(value);
191
+ }
109
192
  /** Resolve a stored value to an array form (for list/chips multi-select). */
110
193
  export function toValueArray(value) {
111
194
  if (value == null || value === "")
@@ -33,6 +33,9 @@
33
33
  import {
34
34
  cfTextStyle,
35
35
  getEditorInputType,
36
+ usesRichDateFilter,
37
+ toIsoDateLocal,
38
+ fromIsoDateLocal,
36
39
  getOptionLabel,
37
40
  getOptionColor,
38
41
  colorfulChipStyle,
@@ -48,6 +51,7 @@
48
51
  // SvGridChartPanel (charting) and SvGridBoard (Kanban) are heavy, prop-gated
49
52
  // views - lazy-loaded below so they stay out of the base <SvGrid> bundle.
50
53
  import { getSchedulerView } from "./scheduler-view.svelte";
54
+ import { getSelectionBarView } from "./selection-bar-view.svelte";
51
55
  import { getBoardView } from "./board-view.svelte";
52
56
  import { getChartView } from "./chart-view.svelte";
53
57
  let props: Props<TFeatures, TData> = $props();
@@ -99,6 +103,37 @@
99
103
  import("./SvGridChartView.svelte").then((m) => (ChartView = m.default));
100
104
  });
101
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
+ });
102
137
  $effect(() => {
103
138
  if (ctrl.chartingEnabled && ctrl.chartPanelOpen && !ChartPanelView)
104
139
  import("./SvGridChartPanel.svelte").then((m) => (ChartPanelView = m.default));
@@ -681,6 +716,24 @@
681
716
  onpointermove={onWindowPointerMove}
682
717
  />
683
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
+
684
737
  {#if opt.loading && !opt.loadingOverlay && !hasMeasured}
685
738
  <!-- Full-screen loading state only on the *initial* load, before the grid
686
739
  has ever rendered. Once measured, a `loading` flip (from a server-mode
@@ -737,6 +790,7 @@
737
790
  <code>@svgrid/enterprise</code> and call <code>enableBoardView()</code>
738
791
  to render it.
739
792
  </p>
793
+ {@render enterpriseLicenseNote()}
740
794
  </div>
741
795
  {/if}
742
796
  </div>
@@ -784,6 +838,7 @@
784
838
  <code>@svgrid/enterprise</code> and call <code>enableSchedulerView()</code>
785
839
  to render it.
786
840
  </p>
841
+ {@render enterpriseLicenseNote()}
787
842
  </div>
788
843
  {/if}
789
844
  </div>
@@ -866,6 +921,7 @@
866
921
  <div class="sv-grid-scheduler-upsell sv-grid-pivot-upsell" role="note">
867
922
  <strong>{messages.pivotUpsellTitle}</strong>
868
923
  <p>{messages.pivotUpsellBody}</p>
924
+ {@render enterpriseLicenseNote()}
869
925
  </div>
870
926
  {/if}
871
927
  </div>
@@ -923,6 +979,13 @@
923
979
  <path d="M8 5l9 7-9 7" />
924
980
  {:else if name === "op-lessThan"}
925
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" />
926
989
  {:else if name === "op-isBlank"}
927
990
  <circle cx="12" cy="12" r="8" />
928
991
  <path d="M6.5 6.5l11 11" />
@@ -1351,6 +1414,12 @@
1351
1414
  class="sv-grid-root"
1352
1415
  class:sv-grid-root-fill={opt.containerHeight === "100%"}
1353
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
+ }
1354
1423
  use:lazyRowResize={{
1355
1424
  disabled: !ctrl.rowResizeOn,
1356
1425
  // No gutter column on most grids, so fall back to the first body cell -
@@ -1951,6 +2020,57 @@
1951
2020
  }}
1952
2021
  />
1953
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}
1954
2074
  {:else if activeOperator !== "isBlank" && activeOperator !== "isNotBlank"}
1955
2075
  {@const frType = getEditorInputType(
1956
2076
  rendered.column.columnDef.editorType ?? "text",
@@ -3002,6 +3122,31 @@
3002
3122
  {#if ctrl.chartingEnabled && ctrl.chartPanelOpen && ChartPanelView}
3003
3123
  <ChartPanelView {ctrl} />
3004
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}
3005
3150
  </div>
3006
3151
  <!-- /.sv-grid-root -->
3007
3152