@svgrid/grid 1.2.22 → 1.2.24

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 (263) hide show
  1. package/dist/GridFooter.svelte +34 -20
  2. package/dist/GridFooter.svelte.d.ts +8 -0
  3. package/dist/SvAutoComplete.svelte +89 -0
  4. package/dist/SvAutoComplete.svelte.d.ts +16 -0
  5. package/dist/SvButton.svelte +97 -0
  6. package/dist/SvButton.svelte.d.ts +23 -0
  7. package/dist/SvCalendar.svelte +409 -0
  8. package/dist/SvCalendar.svelte.d.ts +52 -0
  9. package/dist/SvCheckBox.svelte +82 -0
  10. package/dist/SvCheckBox.svelte.d.ts +19 -0
  11. package/dist/SvColorInput.svelte +160 -0
  12. package/dist/SvColorInput.svelte.d.ts +15 -0
  13. package/dist/SvComboBox.svelte +108 -0
  14. package/dist/SvComboBox.svelte.d.ts +15 -0
  15. package/dist/SvCountryInput.svelte +125 -0
  16. package/dist/SvCountryInput.svelte.d.ts +14 -0
  17. package/dist/SvDateTimePicker.svelte +265 -0
  18. package/dist/SvDateTimePicker.svelte.d.ts +38 -0
  19. package/dist/SvDropDownList.svelte +120 -0
  20. package/dist/SvDropDownList.svelte.d.ts +15 -0
  21. package/dist/SvForm.svelte +147 -0
  22. package/dist/SvForm.svelte.d.ts +29 -0
  23. package/dist/SvGauge.svelte +97 -0
  24. package/dist/SvGauge.svelte.d.ts +26 -0
  25. package/dist/SvGrid.controller.svelte.d.ts +16 -2
  26. package/dist/SvGrid.controller.svelte.js +63 -8
  27. package/dist/SvGrid.css +84 -31
  28. package/dist/SvGrid.helpers.d.ts +4 -0
  29. package/dist/SvGrid.helpers.js +40 -12
  30. package/dist/SvGrid.svelte +84 -5
  31. package/dist/SvGrid.types.d.ts +36 -0
  32. package/dist/SvGridDropdown.svelte +16 -30
  33. package/dist/SvGridDropdown.svelte.d.ts +4 -0
  34. package/dist/SvListBox.svelte +89 -0
  35. package/dist/SvListBox.svelte.d.ts +22 -0
  36. package/dist/SvMaskedInput.svelte +66 -0
  37. package/dist/SvMaskedInput.svelte.d.ts +15 -0
  38. package/dist/SvNumberInput.svelte +116 -0
  39. package/dist/SvNumberInput.svelte.d.ts +18 -0
  40. package/dist/SvPasswordInput.svelte +100 -0
  41. package/dist/SvPasswordInput.svelte.d.ts +18 -0
  42. package/dist/SvPhoneInput.svelte +87 -0
  43. package/dist/SvPhoneInput.svelte.d.ts +19 -0
  44. package/dist/SvRadioGroup.svelte +85 -0
  45. package/dist/SvRadioGroup.svelte.d.ts +20 -0
  46. package/dist/SvRating.svelte +91 -0
  47. package/dist/SvRating.svelte.d.ts +18 -0
  48. package/dist/SvRepeatButton.svelte +89 -0
  49. package/dist/SvRepeatButton.svelte.d.ts +21 -0
  50. package/dist/SvSlider.svelte +155 -0
  51. package/dist/SvSlider.svelte.d.ts +20 -0
  52. package/dist/SvSwitchButton.svelte +87 -0
  53. package/dist/SvSwitchButton.svelte.d.ts +14 -0
  54. package/dist/SvTabs.svelte +109 -0
  55. package/dist/SvTabs.svelte.d.ts +26 -0
  56. package/dist/SvTagsInput.svelte +70 -0
  57. package/dist/SvTagsInput.svelte.d.ts +14 -0
  58. package/dist/SvTimePicker.svelte +201 -0
  59. package/dist/SvTimePicker.svelte.d.ts +30 -0
  60. package/dist/SvToggleButton.svelte +57 -0
  61. package/dist/SvToggleButton.svelte.d.ts +16 -0
  62. package/dist/SvTree.svelte +131 -0
  63. package/dist/SvTree.svelte.d.ts +27 -0
  64. package/dist/build-api.js +78 -1
  65. package/dist/cdn/svgrid.js +11674 -5907
  66. package/dist/cdn/svgrid.svelte-external.js +10436 -4671
  67. package/dist/cell-render.d.ts +4 -0
  68. package/dist/cell-render.js +35 -1
  69. package/dist/clipboard.js +19 -1
  70. package/dist/collaboration.d.ts +2 -0
  71. package/dist/collaboration.js +4 -0
  72. package/dist/core.d.ts +22 -1
  73. package/dist/countries.d.ts +15 -0
  74. package/dist/countries.js +61 -0
  75. package/dist/createAutocomplete.svelte.d.ts +77 -0
  76. package/dist/createAutocomplete.svelte.js +100 -0
  77. package/dist/createCalendar.svelte.d.ts +192 -0
  78. package/dist/createCalendar.svelte.js +402 -0
  79. package/dist/createCheckbox.svelte.d.ts +45 -0
  80. package/dist/createCheckbox.svelte.js +26 -0
  81. package/dist/createColorInput.svelte.d.ts +67 -0
  82. package/dist/createColorInput.svelte.js +100 -0
  83. package/dist/createCombobox.svelte.d.ts +102 -0
  84. package/dist/createCombobox.svelte.js +171 -0
  85. package/dist/createCountryInput.svelte.d.ts +86 -0
  86. package/dist/createCountryInput.svelte.js +126 -0
  87. package/dist/createDateTimePicker.svelte.d.ts +83 -0
  88. package/dist/createDateTimePicker.svelte.js +202 -0
  89. package/dist/createDropdownList.svelte.d.ts +78 -0
  90. package/dist/createDropdownList.svelte.js +117 -0
  91. package/dist/createGauge.svelte.d.ts +56 -0
  92. package/dist/createGauge.svelte.js +49 -0
  93. package/dist/createListbox.svelte.d.ts +79 -0
  94. package/dist/createListbox.svelte.js +117 -0
  95. package/dist/createMaskedInput.svelte.d.ts +30 -0
  96. package/dist/createMaskedInput.svelte.js +62 -0
  97. package/dist/createNumberInput.svelte.d.ts +72 -0
  98. package/dist/createNumberInput.svelte.js +167 -0
  99. package/dist/createPasswordInput.svelte.d.ts +60 -0
  100. package/dist/createPasswordInput.svelte.js +72 -0
  101. package/dist/createPhoneInput.svelte.d.ts +46 -0
  102. package/dist/createPhoneInput.svelte.js +105 -0
  103. package/dist/createRadioGroup.svelte.d.ts +64 -0
  104. package/dist/createRadioGroup.svelte.js +73 -0
  105. package/dist/createRating.svelte.d.ts +70 -0
  106. package/dist/createRating.svelte.js +85 -0
  107. package/dist/createSlider.svelte.d.ts +71 -0
  108. package/dist/createSlider.svelte.js +123 -0
  109. package/dist/createSwitch.svelte.d.ts +43 -0
  110. package/dist/createSwitch.svelte.js +41 -0
  111. package/dist/createTabs.svelte.d.ts +78 -0
  112. package/dist/createTabs.svelte.js +92 -0
  113. package/dist/createTagsInput.svelte.d.ts +65 -0
  114. package/dist/createTagsInput.svelte.js +93 -0
  115. package/dist/createTimePicker.svelte.d.ts +106 -0
  116. package/dist/createTimePicker.svelte.js +248 -0
  117. package/dist/createToggle.svelte.d.ts +43 -0
  118. package/dist/createToggle.svelte.js +39 -0
  119. package/dist/createTree.svelte.d.ts +96 -0
  120. package/dist/createTree.svelte.js +194 -0
  121. package/dist/datetime/date-core.d.ts +88 -0
  122. package/dist/datetime/date-core.js +165 -0
  123. package/dist/datetime/date-format.d.ts +39 -0
  124. package/dist/datetime/date-format.js +371 -0
  125. package/dist/datetime/date-restrict.d.ts +23 -0
  126. package/dist/datetime/date-restrict.js +50 -0
  127. package/dist/datetime/date-selection.d.ts +25 -0
  128. package/dist/datetime/date-selection.js +94 -0
  129. package/dist/datetime/mask.d.ts +21 -0
  130. package/dist/datetime/mask.js +90 -0
  131. package/dist/editing.d.ts +1 -1
  132. package/dist/editing.js +7 -0
  133. package/dist/editor-contract.d.ts +48 -0
  134. package/dist/editor-contract.js +26 -0
  135. package/dist/editors/cell-editors.d.ts +2 -2
  136. package/dist/editors/cell-editors.js +3 -1
  137. package/dist/export-format.d.ts +178 -0
  138. package/dist/export-format.js +419 -0
  139. package/dist/hyperformula-adapter.d.ts +4 -0
  140. package/dist/hyperformula-adapter.js +15 -6
  141. package/dist/index.d.ts +60 -0
  142. package/dist/index.js +72 -0
  143. package/dist/list-option.d.ts +10 -0
  144. package/dist/list-option.js +7 -0
  145. package/dist/menus.js +18 -2
  146. package/dist/popover.d.ts +48 -0
  147. package/dist/popover.js +65 -0
  148. package/dist/row-resize.d.ts +4 -4
  149. package/dist/row-resize.js +8 -5
  150. package/dist/selection.d.ts +6 -0
  151. package/dist/selection.js +31 -5
  152. package/dist/server-data-source.d.ts +43 -0
  153. package/dist/server-data-source.js +100 -0
  154. package/dist/sparkline.js +8 -4
  155. package/dist/svgrid-wrapper.types.d.ts +26 -1
  156. package/package.json +6 -1
  157. package/src/GridFooter.svelte +34 -20
  158. package/src/SvAutoComplete.svelte +89 -0
  159. package/src/SvButton.svelte +97 -0
  160. package/src/SvCalendar.svelte +409 -0
  161. package/src/SvCalendar.test.ts +211 -0
  162. package/src/SvCheckBox.svelte +82 -0
  163. package/src/SvColorInput.svelte +160 -0
  164. package/src/SvComboBox.svelte +108 -0
  165. package/src/SvCountryInput.svelte +125 -0
  166. package/src/SvDateTimePicker.svelte +265 -0
  167. package/src/SvDateTimePicker.test.ts +136 -0
  168. package/src/SvDropDownList.svelte +120 -0
  169. package/src/SvForm.svelte +147 -0
  170. package/src/SvGauge.svelte +97 -0
  171. package/src/SvGrid.controller.svelte.ts +74 -8
  172. package/src/SvGrid.css +84 -31
  173. package/src/SvGrid.helpers.ts +41 -12
  174. package/src/SvGrid.svelte +84 -5
  175. package/src/SvGrid.types.ts +33 -0
  176. package/src/SvGridDropdown.svelte +16 -30
  177. package/src/SvListBox.svelte +89 -0
  178. package/src/SvMaskedInput.svelte +66 -0
  179. package/src/SvNumberInput.svelte +116 -0
  180. package/src/SvPasswordInput.svelte +100 -0
  181. package/src/SvPhoneInput.svelte +87 -0
  182. package/src/SvRadioGroup.svelte +85 -0
  183. package/src/SvRating.svelte +91 -0
  184. package/src/SvRepeatButton.svelte +89 -0
  185. package/src/SvSlider.svelte +155 -0
  186. package/src/SvSwitchButton.svelte +87 -0
  187. package/src/SvTabs.svelte +109 -0
  188. package/src/SvTagsInput.svelte +70 -0
  189. package/src/SvTimePicker.svelte +201 -0
  190. package/src/SvTimePicker.test.ts +98 -0
  191. package/src/SvToggleButton.svelte +57 -0
  192. package/src/SvTree.svelte +131 -0
  193. package/src/build-api.ts +115 -0
  194. package/src/cell-render.test.ts +38 -0
  195. package/src/cell-render.ts +43 -0
  196. package/src/clipboard.test.ts +18 -0
  197. package/src/clipboard.ts +24 -1
  198. package/src/collaboration.test.ts +30 -0
  199. package/src/collaboration.ts +6 -0
  200. package/src/core.ts +27 -3
  201. package/src/countries.ts +71 -0
  202. package/src/createAutocomplete.svelte.ts +104 -0
  203. package/src/createCalendar.svelte.ts +488 -0
  204. package/src/createCheckbox.svelte.ts +74 -0
  205. package/src/createColorInput.svelte.ts +115 -0
  206. package/src/createCombobox.svelte.ts +158 -0
  207. package/src/createCountryInput.svelte.ts +126 -0
  208. package/src/createDateTimePicker.svelte.ts +216 -0
  209. package/src/createDropdownList.svelte.ts +125 -0
  210. package/src/createGauge.svelte.ts +86 -0
  211. package/src/createListbox.svelte.ts +186 -0
  212. package/src/createMaskedInput.svelte.ts +81 -0
  213. package/src/createNumberInput.svelte.ts +193 -0
  214. package/src/createPasswordInput.svelte.ts +87 -0
  215. package/src/createPhoneInput.svelte.ts +119 -0
  216. package/src/createRadioGroup.svelte.ts +132 -0
  217. package/src/createRating.svelte.ts +145 -0
  218. package/src/createSlider.svelte.ts +160 -0
  219. package/src/createSwitch.svelte.ts +78 -0
  220. package/src/createTabs.svelte.ts +129 -0
  221. package/src/createTagsInput.svelte.ts +98 -0
  222. package/src/createTimePicker.svelte.ts +289 -0
  223. package/src/createToggle.svelte.ts +69 -0
  224. package/src/createTree.svelte.ts +216 -0
  225. package/src/datetime/date-core.test.ts +217 -0
  226. package/src/datetime/date-core.ts +204 -0
  227. package/src/datetime/date-format.test.ts +121 -0
  228. package/src/datetime/date-format.ts +317 -0
  229. package/src/datetime/date-restrict.ts +60 -0
  230. package/src/datetime/date-selection.test.ts +129 -0
  231. package/src/datetime/date-selection.ts +137 -0
  232. package/src/datetime/mask.test.ts +36 -0
  233. package/src/datetime/mask.ts +84 -0
  234. package/src/editing.test.ts +22 -0
  235. package/src/editing.ts +7 -1
  236. package/src/editor-contract.test.ts +32 -0
  237. package/src/editor-contract.ts +60 -0
  238. package/src/editors/cell-editors.ts +7 -1
  239. package/src/export-data-api.test.ts +126 -0
  240. package/src/export-format.test.ts +107 -0
  241. package/src/export-format.ts +598 -0
  242. package/src/hyperformula-adapter.test.ts +35 -1
  243. package/src/hyperformula-adapter.ts +18 -6
  244. package/src/index.ts +110 -0
  245. package/src/list-option.ts +15 -0
  246. package/src/menus.ts +21 -2
  247. package/src/popover.ts +79 -0
  248. package/src/row-resize.test.ts +25 -0
  249. package/src/row-resize.ts +8 -5
  250. package/src/selection.test.ts +36 -4
  251. package/src/selection.ts +30 -9
  252. package/src/server-data-source.test.ts +201 -0
  253. package/src/server-data-source.ts +148 -0
  254. package/src/sparkline.test.ts +9 -0
  255. package/src/sparkline.ts +9 -4
  256. package/src/svgrid-wrapper.types.ts +28 -1
  257. package/src/svgrid.behavior.test.ts +27 -0
  258. package/src/svgrid.new-features.wrapper.test.ts +27 -1
  259. package/src/ui-buttons.test.ts +105 -0
  260. package/src/ui-composite.test.ts +89 -0
  261. package/src/ui-inputs.test.ts +118 -0
  262. package/src/ui-range.test.ts +61 -0
  263. package/src/ui-selection.test.ts +119 -0
@@ -0,0 +1,97 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvGauge - a radial arc gauge (SVG) that renders a value within [min, max]
4
+ * with optional colored threshold bands, a needle, and a center label. Parity:
5
+ * Smart `smart-gauge` (radial). Display control; theme-driven.
6
+ *
7
+ * The arc/needle geometry + `role="meter"` ARIA live in the headless
8
+ * `createGauge` core; this component is just one styled renderer over it.
9
+ */
10
+ import { createGauge } from './createGauge.svelte'
11
+
12
+ type Band = { from: number; to: number; color: string }
13
+
14
+ type Props = {
15
+ value?: number
16
+ min?: number
17
+ max?: number
18
+ /** Sweep angle in degrees (default 270, a classic dashboard gauge). */
19
+ sweep?: number
20
+ /** Colored threshold bands along the arc. */
21
+ bands?: Band[]
22
+ /** Show the needle. */
23
+ needle?: boolean
24
+ /** Center label (defaults to the formatted value). */
25
+ label?: string
26
+ unit?: string
27
+ size?: number
28
+ thickness?: number
29
+ formatValue?: (v: number) => string
30
+ ariaLabel?: string
31
+ }
32
+
33
+ let {
34
+ value = 0,
35
+ min = 0,
36
+ max = 100,
37
+ sweep = 270,
38
+ bands = [],
39
+ needle = true,
40
+ label,
41
+ unit = '',
42
+ size = 180,
43
+ thickness = 14,
44
+ formatValue,
45
+ ariaLabel,
46
+ }: Props = $props()
47
+
48
+ const gauge = createGauge({
49
+ value: () => value,
50
+ min: () => min,
51
+ max: () => max,
52
+ sweep: () => sweep,
53
+ size: () => size,
54
+ thickness: () => thickness,
55
+ ariaLabel: () => ariaLabel,
56
+ })
57
+ const clamped = $derived(gauge.clamped)
58
+ const cx = $derived(gauge.cx)
59
+ const cy = $derived(gauge.cy)
60
+ const r = $derived(gauge.r)
61
+ const arcPath = (v0: number, v1: number, radius: number) => gauge.arcPath(v0, v1, radius)
62
+ const needleEnd = $derived(gauge.needleEnd)
63
+ const display = $derived(label ?? `${formatValue ? formatValue(clamped) : Math.round(clamped)}${unit}`)
64
+ </script>
65
+
66
+ <div class="sv-gauge" {...gauge.rootProps()}>
67
+ <svg viewBox={`0 0 ${size} ${size}`} width={size} height={size} class="sv-gauge__svg">
68
+ <!-- track -->
69
+ <path d={arcPath(min, max, r)} class="sv-gauge__track" fill="none" stroke-width={thickness} stroke-linecap="round" />
70
+ <!-- bands (or accent fill up to value when no bands) -->
71
+ {#if bands.length}
72
+ {#each bands as band (band.from + '-' + band.to)}
73
+ <path d={arcPath(band.from, band.to, r)} fill="none" stroke={band.color} stroke-width={thickness} stroke-linecap="round" />
74
+ {/each}
75
+ {:else}
76
+ <path d={arcPath(min, clamped, r)} class="sv-gauge__value" fill="none" stroke-width={thickness} stroke-linecap="round" />
77
+ {/if}
78
+ {#if needle}
79
+ <line x1={cx} y1={cy} x2={needleEnd.x} y2={needleEnd.y} class="sv-gauge__needle" stroke-linecap="round" />
80
+ <circle cx={cx} cy={cy} r="5" class="sv-gauge__hub" />
81
+ {/if}
82
+ </svg>
83
+ <div class="sv-gauge__label">{display}</div>
84
+ </div>
85
+
86
+ <style>
87
+ .sv-gauge { position: relative; display: inline-flex; flex-direction: column; align-items: center; color: var(--sg-fg, #0f172a); }
88
+ .sv-gauge__svg { display: block; }
89
+ .sv-gauge__track { stroke: var(--sg-border, #e2e8f0); }
90
+ .sv-gauge__value { stroke: var(--sg-accent, #2563eb); }
91
+ .sv-gauge__needle { stroke: var(--sg-fg, #0f172a); stroke-width: 3; }
92
+ .sv-gauge__hub { fill: var(--sg-fg, #0f172a); }
93
+ .sv-gauge__label {
94
+ position: absolute; left: 0; right: 0; bottom: 22%; text-align: center;
95
+ font-size: 22px; font-weight: 750; font-variant-numeric: tabular-nums;
96
+ }
97
+ </style>
@@ -409,6 +409,10 @@ export function createSvGridController<
409
409
  });
410
410
  let columnVirtualizerVersion = $state(0);
411
411
  let gridStateVersion = $state(0);
412
+ // Bumps only when a row-model-affecting slice changes (see the store
413
+ // subscription below) - the row-model derivation depends on THIS, not the
414
+ // catch-all gridStateVersion, so navigation doesn't rebuild 1M rows.
415
+ let dataStateVersion = $state(0);
412
416
  const selectionColumnWidth = 44;
413
417
  const rowNumberColumnWidth = $derived(props.rowNumberWidth ?? 56);
414
418
  const showRowNumbersEffective = $derived(props.showRowNumbers ?? false);
@@ -582,6 +586,10 @@ export function createSvGridController<
582
586
  // the wrapper records filter state but does not actually filter rows.
583
587
  // svelte-ignore state_referenced_locally
584
588
  const externalFilterEnabled = props.externalFilter === true;
589
+ // Server-side pagination: the footer reads rowCount/pageIndex from props and
590
+ // emits onPaginationChange instead of slicing locally. Controlled by the
591
+ // consumer. Reactive (unlike sort/filter) so pageIndex/rowCount can change.
592
+ const externalPaginationEnabled = $derived(props.externalPagination === true);
585
593
  const passthroughSortedRowModel = ({ rows }: { rows: Array<Row<TData>> }) =>
586
594
  rows;
587
595
 
@@ -629,9 +637,38 @@ export function createSvGridController<
629
637
  },
630
638
  });
631
639
 
640
+ // `gridStateVersion` bumps on EVERY store change (incl. moving the active
641
+ // cell). `dataStateVersion` bumps ONLY when a slice that actually changes the
642
+ // row model changes (sort / filter / pagination / grouping / expansion /
643
+ // selection) - so the O(rows) row-model derivation does NOT re-run on plain
644
+ // keyboard navigation. Without this, arrow-keying a 1,000,000-row grid re-ran
645
+ // the entire core->filter->sort->group pipeline on every keystroke.
646
+ let prevDataSlices:
647
+ | { sorting: unknown; columnFilters: unknown; pagination: unknown; grouping: unknown; expanded: unknown; rowSelection: unknown }
648
+ | null = null;
632
649
  $effect(() => {
633
650
  const unsubscribe = grid.store.subscribe(() => {
634
651
  gridStateVersion += 1;
652
+ const s = grid.getState();
653
+ if (
654
+ !prevDataSlices ||
655
+ prevDataSlices.sorting !== s.sorting ||
656
+ prevDataSlices.columnFilters !== s.columnFilters ||
657
+ prevDataSlices.pagination !== s.pagination ||
658
+ prevDataSlices.grouping !== s.grouping ||
659
+ prevDataSlices.expanded !== s.expanded ||
660
+ prevDataSlices.rowSelection !== s.rowSelection
661
+ ) {
662
+ dataStateVersion += 1;
663
+ prevDataSlices = {
664
+ sorting: s.sorting,
665
+ columnFilters: s.columnFilters,
666
+ pagination: s.pagination,
667
+ grouping: s.grouping,
668
+ expanded: s.expanded,
669
+ rowSelection: s.rowSelection,
670
+ };
671
+ }
635
672
  });
636
673
  return unsubscribe;
637
674
  });
@@ -977,7 +1014,11 @@ export function createSvGridController<
977
1014
  * filters reduce the dataset.
978
1015
  */
979
1016
  const allRowsBeforePagination = $derived.by(() => {
980
- gridStateVersion;
1017
+ // Depend on dataStateVersion (row-model-affecting store changes) NOT
1018
+ // gridStateVersion - so moving the active cell / selection does not force
1019
+ // this O(rows) pipeline to re-run. Filter-input state (globalFilter etc.)
1020
+ // and internalData are read below and tracked as their own dependencies.
1021
+ dataStateVersion;
981
1022
  // Touch internalData + internalColumns so the row model re-derives when
982
1023
  // the consumer replaces the data array (e.g. via a "Reset" button).
983
1024
  void internalData;
@@ -1081,7 +1122,8 @@ export function createSvGridController<
1081
1122
  */
1082
1123
  const allRows = $derived.by(() => {
1083
1124
  const rows = allRowsBeforePagination;
1084
- if (!paginationEnabled) return rows;
1125
+ // External pagination: `data` already IS the current page - never slice.
1126
+ if (!paginationEnabled || externalPaginationEnabled) return rows;
1085
1127
  const { pageIndex, pageSize } = paginationState;
1086
1128
  const start = pageIndex * pageSize;
1087
1129
  return rows.slice(start, start + pageSize);
@@ -1089,14 +1131,27 @@ export function createSvGridController<
1089
1131
 
1090
1132
  // When a filter reduces the dataset, the stored pageIndex can point beyond
1091
1133
  // the last valid page. Reset to page 0 so the grid never shows a blank body.
1134
+ // Skipped for external pagination, where the consumer owns pageIndex.
1092
1135
  $effect(() => {
1093
- if (!paginationEnabled) return;
1136
+ if (!paginationEnabled || externalPaginationEnabled) return;
1094
1137
  const { pageIndex, pageSize } = paginationState;
1095
1138
  const pageCount = Math.ceil(allRowsBeforePagination.length / pageSize);
1096
1139
  if (pageCount > 0 && pageIndex >= pageCount) {
1097
1140
  grid.setPagination({ pageIndex: 0, pageSize });
1098
1141
  }
1099
1142
  });
1143
+
1144
+ // Footer-facing pagination values. In external mode they come from the
1145
+ // consumer-controlled props; otherwise from the local row model + state.
1146
+ const paginationTotalRows = $derived(
1147
+ externalPaginationEnabled ? (props.rowCount ?? 0) : allRowsBeforePagination.length,
1148
+ );
1149
+ const paginationPageIndex = $derived(
1150
+ externalPaginationEnabled ? (props.pageIndex ?? 0) : paginationState.pageIndex,
1151
+ );
1152
+ const paginationPageSize = $derived(
1153
+ externalPaginationEnabled ? (props.pageSize ?? 10) : paginationState.pageSize,
1154
+ );
1100
1155
  const rowSelectionState = $derived.by(() => {
1101
1156
  gridStateVersion;
1102
1157
  return grid.getState().rowSelection ?? {};
@@ -1501,9 +1556,14 @@ export function createSvGridController<
1501
1556
 
1502
1557
  let summaryByColumn = $state<Record<string, string>>({});
1503
1558
  $effect(() => {
1504
- // Re-aggregate whenever the visible data, columns, edits, or any
1505
- // state that reorders/filters rows changes.
1506
- gridStateVersion;
1559
+ // Re-aggregate whenever the data / columns / edits change. We depend on
1560
+ // `allRows` / `allColumns` / `editedCellValues` DIRECTLY - not the
1561
+ // catch-all `gridStateVersion` - because that version bumps on EVERY store
1562
+ // change, including moving the active cell or selection. `allRows` stays
1563
+ // referentially stable across those (sort/filter/paginate produce a new
1564
+ // rows array; navigation does not), so this now skips the
1565
+ // O(rows x cols) aggregation on plain keyboard navigation - which was
1566
+ // making arrow-key movement crawl on huge grids (e.g. 1,000,000 rows).
1507
1567
  void editedCellValues;
1508
1568
  const rows = allRows;
1509
1569
  const columns = allColumns;
@@ -2209,6 +2269,10 @@ export function createSvGridController<
2209
2269
  get sortDirectionByColumn() { return sortDirectionByColumn; },
2210
2270
  get groupingColumns() { return groupingColumns; },
2211
2271
  get paginationState() { return paginationState; },
2272
+ get externalPaginationEnabled() { return externalPaginationEnabled; },
2273
+ get paginationTotalRows() { return paginationTotalRows; },
2274
+ get paginationPageIndex() { return paginationPageIndex; },
2275
+ get paginationPageSize() { return paginationPageSize; },
2212
2276
  get getRowColumnValue() { return getRowColumnValue; },
2213
2277
  get allRowsBeforePagination() { return allRowsBeforePagination; },
2214
2278
  get allRows() { return allRows; },
@@ -2274,6 +2338,7 @@ export function createSvGridController<
2274
2338
  get computeRowClass() { return computeRowClass; },
2275
2339
  get computeCellClass() { return computeCellClass; },
2276
2340
  get computeCellTooltip() { return computeCellTooltip; },
2341
+ get computeCellValidity() { return computeCellValidity; },
2277
2342
  get computeCellNote() { return computeCellNote; },
2278
2343
  get getCellDisplayValue() { return getCellDisplayValue; },
2279
2344
  get getColumnAlign() { return getColumnAlign; },
@@ -2309,6 +2374,7 @@ export function createSvGridController<
2309
2374
  get getSelectionRects() { return getSelectionRects; },
2310
2375
  get fillHandleCell() { return fillHandleCell; },
2311
2376
  get isInFillPreview() { return isInFillPreview; },
2377
+ get fillMarqueeEdges() { return fillMarqueeEdges; },
2312
2378
  get findColumnById() { return findColumnById; },
2313
2379
  get readCellRaw() { return readCellRaw; },
2314
2380
  get writeCellRaw() { return writeCellRaw; },
@@ -2402,9 +2468,9 @@ export function createSvGridController<
2402
2468
  const { onGridKeyDown, onWindowKeydown, onHeaderSortClick } = createKeyboard<TFeatures, TData>(ctx);
2403
2469
  const { computeSummaries, hasRenderedColumn } = createSummaries<TFeatures, TData>(ctx);
2404
2470
  const { updateFilterRow, updateFilterOperator, updateFilterMenuValue, updateFilterMenuValueTo, toggleCheckboxWithKeyboard, isColumnFiltered, closeMenus, openChooseColumns, openColumnMenu, openFilterMenu, openOperatorMenu, sortColumnFromMenu, clearColumnSort, groupByColumnFromMenu, clearGroupingFromMenu, isFacetChecked, toggleFacetValue, isAllFacetsChecked, toggleAllFacets, clearColumnFilter, changePage, goToPage, setPageSize, openContextMenu, closeContextMenu, contextMenuItems, saveComment, removeComment, closeCommentEditor } = createMenus<TFeatures, TData>(ctx);
2405
- const { cellConditionalFormat, computeRowClass, computeCellClass, computeCellTooltip, computeCellNote, getColumnEditorOptions, formatListCellValue, formatCellValue, formatPinnedValue, computePinnedCellClass } = createCellRender<TFeatures, TData>(ctx);
2471
+ const { cellConditionalFormat, computeRowClass, computeCellClass, computeCellTooltip, computeCellValidity, computeCellNote, getColumnEditorOptions, formatListCellValue, formatCellValue, formatPinnedValue, computePinnedCellClass } = createCellRender<TFeatures, TData>(ctx);
2406
2472
  const { isCellEditable, isCellEditableAt, getRowColumnValue, getCellDisplayValue, startEditingWithChar, startEditing, stopEditing, startFullRowEdit, setFullRowDraft, commitFullRowEdit, cancelFullRowEdit, saveEditingCell, applyHistoryStep, updateEditingCellValue, onEditorKeyDown, focusOnMount, onCellDoubleClick, pasteFromClipboard, onGridPaste } = createEditing<TFeatures, TData>(ctx);
2407
- const { isRowSelected, toggleRowSelectionById, toggleSelectAllRows, setActiveCell, scrollActiveCellIntoView, setSelection, extendSelection, isCellInSelectedRange, getCellRangeEdges, getSelectionRects, isInFillPreview, findColumnById, onCellPointerDown, onCellPointerEnter, endDragSelection, onWindowPointerMove, onCellClick, emitCellDoubleClick } = createSelection<TFeatures, TData>(ctx);
2473
+ const { isRowSelected, toggleRowSelectionById, toggleSelectAllRows, setActiveCell, scrollActiveCellIntoView, setSelection, extendSelection, isCellInSelectedRange, getCellRangeEdges, getSelectionRects, isInFillPreview, fillMarqueeEdges, findColumnById, onCellPointerDown, onCellPointerEnter, endDragSelection, onWindowPointerMove, onCellClick, emitCellDoubleClick } = createSelection<TFeatures, TData>(ctx);
2408
2474
  const { cellPinStyle, isColumnPinned, getCurrentColumnOrder, emitColumnOrder, setColumnOrderInternal, applyColumnDrop, onColumnHeaderDragStart, onColumnHeaderDragOver, onColumnHeaderDragLeave, onColumnHeaderDrop, onColumnHeaderDragEnd, pinColumnLeft, pinColumnRight, unpinColumn, toggleColumnVisibleInPanel, moveColumnInPanel, toggleGroupInPanel, getColumnBaseWidth, getColumnWidth, startColumnResize, onColumnResizeMove, endColumnResize, measureText, autosizeColumn, autosizeAllColumns, resetColumns } = createColumns<TFeatures, TData>(ctx);
2409
2475
  const { onRowDragStart, onRowDragOver, onRowDragLeave, onRowDrop, onRowsContainerDragOver, onRowsContainerDrop, onRowDragEnd } = createRowDrag<TFeatures, TData>(ctx);
2410
2476
  const { register: registerAlignedGrid, broadcastScroll: broadcastAlignedScroll, broadcastWidths: broadcastAlignedWidths } = createAlignedGrids<TFeatures, TData>(ctx);
package/src/SvGrid.css CHANGED
@@ -348,9 +348,13 @@
348
348
  .sv-grid-table tr > :last-child.sv-grid-cell {
349
349
  border-right: 0;
350
350
  }
351
- /* Row hover tint. */
352
- .sv-grid-table tbody tr:hover > .sv-grid-cell {
353
- background: var(--sg-row-hover-bg, #eef2ff);
351
+ /* Row hover tint - opt-in via `enableRowHover` (off by default; the class is
352
+ present unless the grid opts in). Uses `background-color` (not the shorthand)
353
+ so it never resets the fill-marquee `background-image`, and skips selected
354
+ cells so the selection stays visible under a hovered row. */
355
+ .sv-grid-table:not(.sv-grid-no-row-hover)
356
+ tbody tr:hover > .sv-grid-cell:not([data-selected-range="true"]) {
357
+ background-color: var(--sg-row-hover-bg, #eef2ff);
354
358
  }
355
359
  /* Only the active cell + the cell hosting the fill handle become a
356
360
  positioning context (so the absolutely-positioned handle anchors
@@ -441,8 +445,10 @@
441
445
  ),
442
446
  var(--sg-pinned-bg, color-mix(in oklab, var(--sg-header-bg, #f1f5f9) 92%, var(--sg-accent, #2563eb) 8%));
443
447
  }
444
- /* Hovered row keeps the pinned tint distinguishable from the hover. */
445
- .sv-grid-row:hover > .sv-grid-cell[data-pinned] {
448
+ /* Hovered row keeps the pinned tint distinguishable from the hover. Selected
449
+ cells are excluded so the selection stays visible under hover. */
450
+ .sv-grid-table:not(.sv-grid-no-row-hover)
451
+ .sv-grid-row:hover > .sv-grid-cell[data-pinned]:not([data-selected-range="true"]) {
446
452
  background:
447
453
  linear-gradient(
448
454
  color-mix(in srgb, var(--sg-row-hover-bg, #eef2ff) 55%, transparent),
@@ -1062,18 +1068,52 @@ select.sv-grid-fr-editor {
1062
1068
  transform-origin: bottom right;
1063
1069
  }
1064
1070
 
1065
- /* Fill-preview overlay paints an accent border on the cells the user is
1066
- about to fill. Source cells (still in the selection range) keep their
1067
- normal range styling. The border + tint derive from the theme accent
1068
- (`--sg-accent`) so they match each theme instead of a fixed blue;
1069
- `--sg-fill-preview-border` / `--sg-fill-preview-bg` let a theme override
1070
- them directly. */
1071
- .sv-grid-cell[data-fill-preview="true"] {
1072
- box-shadow: inset 0 0 0 2px
1073
- var(--sg-fill-preview-border,
1074
- color-mix(in srgb, var(--sg-accent, #0b63f3) 50%, transparent));
1075
- background: var(--sg-fill-preview-bg,
1076
- color-mix(in srgb, var(--sg-accent, #0b63f3) 8%, transparent));
1071
+ /* Excel-style fill-handle drag marquee: ONE thin dashed rectangle around the
1072
+ whole target range (source + fill-into area), not a thick per-cell border or
1073
+ fill tint. Drawn as four background-image layers on the boundary cells - no
1074
+ `border`, so it paints on the compositor with zero reflow while dragging.
1075
+ `--sg-fill-marquee` overrides the colour (defaults to a theme-aware grey). */
1076
+ /* Step 1 (LOW specificity): default every edge layer to `none`, then light up
1077
+ the edges this cell sits on. These are custom properties, which the
1078
+ `background:` shorthands on selected / zebra cells do NOT reset - so they can
1079
+ safely live at low specificity and just compete with each other by source
1080
+ order (gradient rules come after the defaults, so they win). */
1081
+ .sv-grid-cell[data-fill-top="true"],
1082
+ .sv-grid-cell[data-fill-bottom="true"],
1083
+ .sv-grid-cell[data-fill-left="true"],
1084
+ .sv-grid-cell[data-fill-right="true"] {
1085
+ --sv-fill-top: none;
1086
+ --sv-fill-bottom: none;
1087
+ --sv-fill-left: none;
1088
+ --sv-fill-right: none;
1089
+ /* Match the selection colour (the grid accent) by default. */
1090
+ --sv-fill-color: var(--sg-fill-marquee, var(--sg-accent, #2563eb));
1091
+ }
1092
+ .sv-grid-cell[data-fill-top="true"] {
1093
+ --sv-fill-top: repeating-linear-gradient(to right, var(--sv-fill-color) 0 3px, transparent 3px 6px);
1094
+ }
1095
+ .sv-grid-cell[data-fill-bottom="true"] {
1096
+ --sv-fill-bottom: repeating-linear-gradient(to right, var(--sv-fill-color) 0 3px, transparent 3px 6px);
1097
+ }
1098
+ .sv-grid-cell[data-fill-left="true"] {
1099
+ --sv-fill-left: repeating-linear-gradient(to bottom, var(--sv-fill-color) 0 3px, transparent 3px 6px);
1100
+ }
1101
+ .sv-grid-cell[data-fill-right="true"] {
1102
+ --sv-fill-right: repeating-linear-gradient(to bottom, var(--sv-fill-color) 0 3px, transparent 3px 6px);
1103
+ }
1104
+ /* Step 2 (HIGH specificity, `.sv-grid-table` prefix): compose the resolved edge
1105
+ layers into `background-image` so it beats the `background:` shorthands on
1106
+ selected / zebra / hover cells (which would otherwise reset background-image).
1107
+ The cell keeps its background-COLOR; this only adds the dashed lines on top. */
1108
+ .sv-grid-table .sv-grid-cell[data-fill-top="true"],
1109
+ .sv-grid-table .sv-grid-cell[data-fill-bottom="true"],
1110
+ .sv-grid-table .sv-grid-cell[data-fill-left="true"],
1111
+ .sv-grid-table .sv-grid-cell[data-fill-right="true"] {
1112
+ background-image: var(--sv-fill-top), var(--sv-fill-bottom),
1113
+ var(--sv-fill-left), var(--sv-fill-right);
1114
+ background-repeat: no-repeat;
1115
+ background-size: 100% 2px, 100% 2px, 2px 100%, 2px 100%;
1116
+ background-position: top left, bottom left, top left, top right;
1077
1117
  }
1078
1118
 
1079
1119
  .sv-grid-cell-editing {
@@ -1338,6 +1378,17 @@ select.sv-grid-fr-editor {
1338
1378
  .sv-grid-cell-cf {
1339
1379
  position: relative;
1340
1380
  }
1381
+ /* Declarative cell validation (column `validate` hook). Invalid cells get a
1382
+ * soft red wash and a red inset ring - Handsontable's `htInvalid` look. The
1383
+ * value keeps rendering as-is; the message (if any) shows as the tooltip. */
1384
+ .sv-grid-cell-invalid {
1385
+ background-color: var(--sg-invalid-bg, rgba(239, 68, 68, 0.14)) !important;
1386
+ box-shadow: inset 0 0 0 1px var(--sg-invalid-border, rgba(239, 68, 68, 0.45));
1387
+ color: var(--sg-invalid-fg, #b91c1c);
1388
+ }
1389
+ .sv-grid-cell-invalid.sv-grid-cell-active {
1390
+ box-shadow: inset 0 0 0 2px var(--sg-invalid-border, rgba(239, 68, 68, 0.7));
1391
+ }
1341
1392
  .sv-grid-status-bar {
1342
1393
  display: flex;
1343
1394
  flex-wrap: wrap;
@@ -2103,32 +2154,34 @@ select.sv-grid-fr-editor {
2103
2154
  color: var(--sg-fg, #0f172a);
2104
2155
  font-size: 13px;
2105
2156
  }
2157
+ /* Top-positioned pager: rounded + bordered on the top edge instead. */
2158
+ .sv-grid-pagination-top {
2159
+ border-top: 1px solid var(--sg-border, #e2e8f0);
2160
+ border-bottom: 0;
2161
+ border-radius: var(--sg-radius, 6px) var(--sg-radius, 6px) 0 0;
2162
+ }
2106
2163
  .sv-grid-pagination-pagesize {
2107
2164
  display: inline-flex;
2108
2165
  align-items: center;
2109
2166
  gap: 8px;
2110
2167
  color: var(--sg-muted, #64748b);
2111
2168
  }
2112
- .sv-grid-pagination-pagesize select {
2169
+ .sv-grid-pagination-pagesize-dd {
2170
+ position: relative;
2113
2171
  height: 28px;
2114
- padding: 0 22px 0 8px;
2172
+ min-width: 62px;
2115
2173
  border: 1px solid var(--sg-input-border, #cbd5e1);
2116
2174
  border-radius: 5px;
2117
2175
  background: var(--sg-input-bg, #fff);
2118
- color: var(--sg-fg, #0f172a);
2119
- font: inherit;
2176
+ }
2177
+ .sv-grid-pagination-pagesize-dd .sv-grid-dropdown,
2178
+ .sv-grid-pagination-pagesize-dd .sv-grid-dropdown-trigger {
2179
+ background: transparent;
2180
+ border-radius: 5px;
2120
2181
  font-size: 13px;
2121
- cursor: pointer;
2122
- appearance: none;
2123
- -webkit-appearance: none;
2124
- background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
2125
- linear-gradient(135deg, currentColor 50%, transparent 50%);
2126
- background-position: calc(100% - 12px) 12px, calc(100% - 8px) 12px;
2127
- background-size: 4px 4px, 4px 4px;
2128
- background-repeat: no-repeat;
2182
+ color: var(--sg-fg, #0f172a);
2129
2183
  }
2130
- .sv-grid-pagination-pagesize select:focus {
2131
- outline: none;
2184
+ .sv-grid-pagination-pagesize-dd:focus-within {
2132
2185
  border-color: var(--sg-accent, #2563eb);
2133
2186
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-accent, #2563eb) 30%, transparent);
2134
2187
  }
@@ -69,19 +69,47 @@ export function toDateTimeLocalInputValue(value: unknown) {
69
69
  return parsed.toISOString().slice(0, 16);
70
70
  }
71
71
 
72
+ /** Strip the `-native` opt-out suffix so the native-input helpers treat
73
+ * `date-native` exactly like `date` (the rich pickers own the bare types). */
74
+ function baseEditor(editorType: CellEditorType): string {
75
+ return String(editorType).replace(/-native$/, "");
76
+ }
77
+
78
+ /** 'HH:MM[:SS]' string (or Date) to a Date carrying that time (today's date). */
79
+ export function timeStringToDate(value: unknown): Date | null {
80
+ if (value instanceof Date) return value;
81
+ if (typeof value === "string") {
82
+ const m = value.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?/);
83
+ if (m) {
84
+ const d = new Date();
85
+ d.setHours(Math.min(23, +m[1]!), Math.min(59, +m[2]!), m[3] ? +m[3] : 0, 0);
86
+ return d;
87
+ }
88
+ }
89
+ return null;
90
+ }
91
+
92
+ /** A Date to an 'HH:MM' string (the `time` editor's stored form). */
93
+ export function dateToTimeString(d: Date | null): string | null {
94
+ if (!d) return null;
95
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
96
+ }
97
+
72
98
  export function getEditableInputValue(editorType: CellEditorType, value: unknown) {
73
- if (editorType === "date") return toDateInputValue(value);
74
- if (editorType === "datetime") return toDateTimeLocalInputValue(value);
99
+ const base = baseEditor(editorType);
100
+ if (base === "date") return toDateInputValue(value);
101
+ if (base === "datetime") return toDateTimeLocalInputValue(value);
75
102
  return String(value ?? "");
76
103
  }
77
104
 
78
105
  export function getEditorInputType(editorType: CellEditorType) {
79
- if (editorType === "number") return "number";
80
- if (editorType === "date") return "date";
81
- if (editorType === "datetime") return "datetime-local";
82
- if (editorType === "time") return "time";
83
- if (editorType === "password") return "password";
84
- if (editorType === "color") return "color";
106
+ const base = baseEditor(editorType);
107
+ if (base === "number") return "number";
108
+ if (base === "date") return "date";
109
+ if (base === "datetime") return "datetime-local";
110
+ if (base === "time") return "time";
111
+ if (base === "password") return "password";
112
+ if (base === "color") return "color";
85
113
  return "text";
86
114
  }
87
115
 
@@ -130,13 +158,14 @@ export function colorfulChipStyle(color: string | undefined): string {
130
158
  }
131
159
 
132
160
  export function getEditorClass(editorType: CellEditorType) {
133
- if (editorType === "number")
161
+ const base = baseEditor(editorType);
162
+ if (base === "number")
134
163
  return "sv-grid-cell-editor sv-grid-cell-editor-number";
135
- if (editorType === "date")
164
+ if (base === "date")
136
165
  return "sv-grid-cell-editor sv-grid-cell-editor-date";
137
- if (editorType === "datetime")
166
+ if (base === "datetime")
138
167
  return "sv-grid-cell-editor sv-grid-cell-editor-datetime";
139
- if (editorType === "color")
168
+ if (base === "color")
140
169
  return "sv-grid-cell-editor sv-grid-cell-editor-color";
141
170
  return "sv-grid-cell-editor";
142
171
  }