@svgrid/grid 2.2.32 → 2.4.0

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 (405) hide show
  1. package/README.md +199 -199
  2. package/dist/DockNodeView.svelte +1 -1
  3. package/dist/FlexRender.svelte +96 -96
  4. package/dist/GridFooter.svelte +178 -179
  5. package/dist/GridMenus.svelte +705 -782
  6. package/dist/SvAutoComplete.svelte +165 -165
  7. package/dist/SvAvatar.svelte +75 -75
  8. package/dist/SvCalendar.svelte +503 -503
  9. package/dist/SvCarousel.svelte +141 -141
  10. package/dist/SvCheckBox.svelte +102 -102
  11. package/dist/SvCircularProgress.svelte +109 -109
  12. package/dist/SvColorInput.svelte +181 -181
  13. package/dist/SvComboBox.svelte +275 -275
  14. package/dist/SvContextMenu.svelte +116 -116
  15. package/dist/SvCountryInput.svelte +160 -160
  16. package/dist/SvDateRangeInput.svelte +1 -1
  17. package/dist/SvDrawer.svelte +254 -254
  18. package/dist/SvDropDownList.svelte +374 -374
  19. package/dist/SvDurationInput.svelte +126 -126
  20. package/dist/SvField.svelte +293 -293
  21. package/dist/SvForm.svelte +437 -437
  22. package/dist/SvGrid.css +2814 -2804
  23. package/dist/SvGrid.svelte +3490 -3513
  24. package/dist/SvGridChart.svelte +1716 -1724
  25. package/dist/SvGridChartPanel.svelte +485 -485
  26. package/dist/SvGridChartView.svelte +70 -70
  27. package/dist/SvGridDropdown.svelte +728 -728
  28. package/dist/SvGridSelect.svelte +266 -268
  29. package/dist/SvGroupCell.svelte +116 -116
  30. package/dist/SvKbd.svelte +1 -1
  31. package/dist/SvList.svelte +1 -1
  32. package/dist/SvListBox.svelte +326 -326
  33. package/dist/SvMaskedInput.svelte +122 -122
  34. package/dist/SvMenu.svelte +124 -124
  35. package/dist/SvMenuList.svelte +146 -146
  36. package/dist/SvMenubar.svelte +1 -1
  37. package/dist/SvMultiSelect.svelte +289 -290
  38. package/dist/SvNumberInput.svelte +168 -168
  39. package/dist/SvOtpInput.svelte +158 -158
  40. package/dist/SvPasswordInput.svelte +151 -151
  41. package/dist/SvPhoneInput.svelte +133 -133
  42. package/dist/SvPopover.svelte +197 -197
  43. package/dist/SvProgress.svelte +116 -116
  44. package/dist/SvRadioGroup.svelte +107 -107
  45. package/dist/SvRating.svelte +112 -112
  46. package/dist/SvResult.svelte +73 -73
  47. package/dist/SvRichText.svelte +211 -207
  48. package/dist/SvRowGroupPanel.svelte +170 -170
  49. package/dist/SvScrollArea.svelte +61 -61
  50. package/dist/SvSlider.svelte +200 -200
  51. package/dist/SvSwitchButton.svelte +108 -108
  52. package/dist/SvTagsInput.svelte +112 -112
  53. package/dist/SvTextInput.svelte +147 -147
  54. package/dist/SvTimePicker.svelte +245 -245
  55. package/dist/SvToaster.svelte +159 -159
  56. package/dist/SvToggleButton.svelte +85 -85
  57. package/dist/SvTooltip.svelte +161 -161
  58. package/dist/SvTour.svelte +208 -204
  59. package/dist/SvTree.svelte +444 -444
  60. package/dist/SvTreeSelect.svelte +235 -237
  61. package/dist/cdn/{GridMenus-C6ixSGWW.js → GridMenus-BpVcKyUr.js} +5 -7
  62. package/dist/cdn/{GridMenus-Cyk3qpJ7.js → GridMenus-CeXL5IW4.js} +153 -152
  63. package/dist/cdn/{SvDateTimePicker-ClqtVIj3.js → SvDateTimePicker-DSUhIta8.js} +165 -165
  64. package/dist/cdn/{SvDateTimePicker-CdqaP61H.js → SvDateTimePicker-h9kPyszT.js} +572 -555
  65. package/dist/cdn/{SvGridChart-BcLCYvw9.js → SvGridChart-Bs2GIR2Q.js} +28 -21
  66. package/dist/cdn/{SvGridChart-BhnFdLd0.js → SvGridChart-CwhFz7GV.js} +2 -2
  67. package/dist/cdn/{SvGridChartPanel-Cmx9DHAS.js → SvGridChartPanel-1WXuStzM.js} +2 -2
  68. package/dist/cdn/{SvGridChartPanel-DvjwxLvK.js → SvGridChartPanel-e5_nqNP9.js} +22 -14
  69. package/dist/cdn/{SvGridChartView-PwqI4s02.js → SvGridChartView-SmPW10dI.js} +3 -3
  70. package/dist/cdn/{SvGridChartView-B40DTJSX.js → SvGridChartView-eSPuJE6g.js} +2 -2
  71. package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-B1ZcLpgs.js} +1 -1
  72. package/dist/cdn/{SvGridDropdown-Dh-2ej8j.js → SvGridDropdown-CtdbHcIS.js} +6 -5
  73. package/dist/cdn/{chart-T1usDN0o.js → chart-i4XcXyHJ.js} +12 -10
  74. package/dist/cdn/disclose-version-Dr7rEVv-.js +2866 -0
  75. package/dist/cdn/{export-format-JT5pWVT8.js → export-format-Cv1Dll6k.js} +66 -245
  76. package/dist/cdn/{src-CgD-wiuS.js → src-DbeN1ReZ.js} +9223 -8944
  77. package/dist/cdn/{src-BgHjWF0Q.js → src-DmEQFW5_.js} +9023 -8828
  78. package/dist/cdn/svgrid.js +9 -9
  79. package/dist/cdn/svgrid.svelte-external.js +9 -9
  80. package/dist/chart-export.js +8 -8
  81. package/dist/chart.d.ts +2 -0
  82. package/dist/chart.js +1 -4
  83. package/dist/columns.js +9 -2
  84. package/dist/createMenu.svelte.js +0 -1
  85. package/dist/createPopoverSelect.svelte.d.ts +2 -2
  86. package/dist/editing.js +1 -1
  87. package/dist/export-format.js +3 -0
  88. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/_svelte_metadata.json +1 -0
  89. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/_metadata.json +178 -0
  90. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/attachments-qzRy9DX4.js +3843 -0
  91. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/attachments-qzRy9DX4.js.map +1 -0
  92. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/constants-RLNOXCpz.js +23 -0
  93. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/constants-RLNOXCpz.js.map +1 -0
  94. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/dev-DPd12uEU.js +1925 -0
  95. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/dev-DPd12uEU.js.map +1 -0
  96. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/events-CDC4DKuQ.js +206 -0
  97. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/events-CDC4DKuQ.js.map +1 -0
  98. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/flags-BD-lYX7s.js +20 -0
  99. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/flags-BD-lYX7s.js.map +1 -0
  100. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/hydration-D3UBU2t8.js +264 -0
  101. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/hydration-D3UBU2t8.js.map +1 -0
  102. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/legacy-client-B0ZUlbSQ.js +698 -0
  103. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/legacy-client-B0ZUlbSQ.js.map +1 -0
  104. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/package.json +3 -0
  105. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte.js +135 -0
  106. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte.js.map +1 -0
  107. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_animate.js +57 -0
  108. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_animate.js.map +1 -0
  109. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_attachments.js +2 -0
  110. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_compiler.js +46691 -0
  111. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_compiler.js.map +1 -0
  112. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_easing.js +239 -0
  113. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_easing.js.map +1 -0
  114. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_events.js +2 -0
  115. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal.js +6 -0
  116. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal.js.map +1 -0
  117. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_client.js +7 -0
  118. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_disclose-version.js +6 -0
  119. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_disclose-version.js.map +1 -0
  120. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_async.js +7 -0
  121. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_async.js.map +1 -0
  122. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_legacy.js +7 -0
  123. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_legacy.js.map +1 -0
  124. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_tracing.js +7 -0
  125. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_tracing.js.map +1 -0
  126. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_server.js +6 -0
  127. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_legacy.js +85 -0
  128. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_legacy.js.map +1 -0
  129. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_motion.js +561 -0
  130. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_motion.js.map +1 -0
  131. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity.js +26 -0
  132. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity.js.map +1 -0
  133. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity_window.js +124 -0
  134. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity_window.js.map +1 -0
  135. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_server.js +2 -0
  136. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_store.js +91 -0
  137. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_store.js.map +1 -0
  138. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_transition.js +242 -0
  139. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_transition.js.map +1 -0
  140. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-lObbumRX.js +497 -0
  141. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-lObbumRX.js.map +1 -0
  142. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-mR9mAWyK.js +4929 -0
  143. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-mR9mAWyK.js.map +1 -0
  144. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/validate-CezNmRkM.js +192 -0
  145. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/validate-CezNmRkM.js.map +1 -0
  146. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/version-BHHo0WE-.js +10 -0
  147. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/version-BHHo0WE-.js.map +1 -0
  148. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/warnings-yiExAB06.js +140 -0
  149. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/warnings-yiExAB06.js.map +1 -0
  150. package/dist/selection.js +0 -1
  151. package/package.json +11 -10
  152. package/src/DockNodeView.svelte +1 -1
  153. package/src/FlexRender.svelte +96 -96
  154. package/src/GridFooter.svelte +178 -179
  155. package/src/GridMenus.svelte +705 -782
  156. package/src/SvAutoComplete.svelte +165 -165
  157. package/src/SvAvatar.svelte +75 -75
  158. package/src/SvCalendar.svelte +503 -503
  159. package/src/SvCalendar.test.ts +226 -226
  160. package/src/SvCarousel.svelte +141 -141
  161. package/src/SvCheckBox.svelte +102 -102
  162. package/src/SvCircularProgress.svelte +109 -109
  163. package/src/SvColorInput.svelte +181 -181
  164. package/src/SvComboBox.svelte +275 -275
  165. package/src/SvContextMenu.svelte +116 -116
  166. package/src/SvCountryInput.svelte +160 -160
  167. package/src/SvDateRangeInput.svelte +1 -1
  168. package/src/SvDrawer.svelte +254 -254
  169. package/src/SvDropDownList.svelte +374 -374
  170. package/src/SvDurationInput.svelte +126 -126
  171. package/src/SvField.svelte +293 -293
  172. package/src/SvForm.svelte +437 -437
  173. package/src/SvForm.test.ts +411 -411
  174. package/src/SvGrid.controller.svelte.ts +3680 -3685
  175. package/src/SvGrid.css +2814 -2804
  176. package/src/SvGrid.svelte +3490 -3513
  177. package/src/SvGrid.types.ts +1708 -1709
  178. package/src/SvGridChart.svelte +1716 -1724
  179. package/src/SvGridChartPanel.svelte +485 -485
  180. package/src/SvGridChartView.svelte +70 -70
  181. package/src/SvGridDropdown.svelte +728 -728
  182. package/src/SvGridSelect.svelte +266 -268
  183. package/src/SvGroupCell.svelte +116 -116
  184. package/src/SvKbd.svelte +1 -1
  185. package/src/SvList.svelte +1 -1
  186. package/src/SvListBox.svelte +326 -326
  187. package/src/SvMaskedInput.svelte +122 -122
  188. package/src/SvMenu.svelte +124 -124
  189. package/src/SvMenu.test.ts +97 -97
  190. package/src/SvMenuList.svelte +146 -146
  191. package/src/SvMenubar.svelte +1 -1
  192. package/src/SvMultiSelect.svelte +289 -290
  193. package/src/SvNumberInput.svelte +168 -168
  194. package/src/SvOtpInput.svelte +158 -158
  195. package/src/SvPasswordInput.svelte +151 -151
  196. package/src/SvPhoneInput.svelte +133 -133
  197. package/src/SvPopover.svelte +197 -197
  198. package/src/SvProgress.svelte +116 -116
  199. package/src/SvRadioGroup.svelte +107 -107
  200. package/src/SvRating.svelte +112 -112
  201. package/src/SvResult.svelte +73 -73
  202. package/src/SvRichText.svelte +211 -207
  203. package/src/SvRowGroupPanel.svelte +170 -170
  204. package/src/SvScrollArea.svelte +61 -61
  205. package/src/SvSlider.svelte +200 -200
  206. package/src/SvSwitchButton.svelte +108 -108
  207. package/src/SvTagsInput.svelte +112 -112
  208. package/src/SvTextInput.svelte +147 -147
  209. package/src/SvTimePicker.svelte +245 -245
  210. package/src/SvToaster.svelte +159 -159
  211. package/src/SvToaster.test.ts +95 -95
  212. package/src/SvToggleButton.svelte +85 -85
  213. package/src/SvTooltip.svelte +161 -161
  214. package/src/SvTour.svelte +208 -204
  215. package/src/SvTree.svelte +444 -444
  216. package/src/SvTreeSelect.svelte +235 -237
  217. package/src/a11y/dismissable.test.ts +119 -119
  218. package/src/a11y/dismissable.ts +114 -114
  219. package/src/a11y.contract.test.ts +49 -49
  220. package/src/a11y.test.ts +59 -59
  221. package/src/a11y.ts +61 -61
  222. package/src/ai.test.ts +502 -502
  223. package/src/ai.ts +1391 -1391
  224. package/src/build-api.coverage.test.ts +633 -633
  225. package/src/build-api.ts +805 -910
  226. package/src/builtin-editors.grid.test.ts +85 -85
  227. package/src/cell-formatting.test.ts +1 -1
  228. package/src/cell-formatting.ts +169 -169
  229. package/src/cell-render.test.ts +513 -513
  230. package/src/cell-render.ts +496 -590
  231. package/src/cell-values.ts +118 -118
  232. package/src/chart-export.ts +201 -201
  233. package/src/chart-view.svelte.ts +36 -36
  234. package/src/chart.ts +2295 -2296
  235. package/src/clipboard.ts +0 -102
  236. package/src/collaboration.test.ts +104 -104
  237. package/src/collaboration.ts +167 -167
  238. package/src/columns.ts +7 -117
  239. package/src/core.performance.test.ts +30 -30
  240. package/src/core.ts +1261 -1261
  241. package/src/createAutocomplete.svelte.ts +132 -132
  242. package/src/createCombobox.svelte.ts +191 -191
  243. package/src/createCountryInput.svelte.ts +157 -157
  244. package/src/createDropdownList.svelte.ts +168 -168
  245. package/src/createForm.svelte.ts +386 -386
  246. package/src/createGrid.svelte.ts +42 -42
  247. package/src/createGrid.test.ts +10 -10
  248. package/src/createGridState.svelte.ts +17 -17
  249. package/src/createListbox.svelte.ts +250 -250
  250. package/src/createMenu.svelte.ts +224 -225
  251. package/src/createPopoverSelect.svelte.ts +213 -213
  252. package/src/createSlider.svelte.ts +191 -191
  253. package/src/createTooltip.svelte.ts +144 -144
  254. package/src/createTree.svelte.ts +319 -319
  255. package/src/editing.test.ts +949 -949
  256. package/src/editing.ts +606 -709
  257. package/src/editor-contract.ts +164 -164
  258. package/src/editor-registry.grid.test.ts +144 -146
  259. package/src/editor-registry.ts +122 -122
  260. package/src/export-data-api.test.ts +126 -126
  261. package/src/export-format.test.ts +107 -107
  262. package/src/export-format.ts +601 -598
  263. package/src/features.ts +0 -90
  264. package/src/filter-operators.ts +157 -157
  265. package/src/flex-render.ts +3 -3
  266. package/src/form-field.ts +127 -127
  267. package/src/group-display.test.ts +167 -167
  268. package/src/group-display.ts +200 -200
  269. package/src/index.ts +724 -724
  270. package/src/js-scroller.svelte.ts +173 -173
  271. package/src/keyboard-handlers.coverage.test.ts +1 -1
  272. package/src/keyboard-handlers.ts +270 -390
  273. package/src/keyboard.test.ts +59 -59
  274. package/src/keyboard.ts +97 -97
  275. package/src/list-nav.test.ts +49 -49
  276. package/src/list-nav.ts +29 -29
  277. package/src/list-option.test.ts +56 -56
  278. package/src/list-option.ts +172 -172
  279. package/src/menus.test.ts +0 -1
  280. package/src/menus.ts +567 -679
  281. package/src/merge-objects.ts +48 -48
  282. package/src/new-features.test.ts +3 -1
  283. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/_svelte_metadata.json +1 -0
  284. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/_metadata.json +178 -0
  285. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/attachments-qzRy9DX4.js +3843 -0
  286. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/attachments-qzRy9DX4.js.map +1 -0
  287. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/constants-RLNOXCpz.js +23 -0
  288. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/constants-RLNOXCpz.js.map +1 -0
  289. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/dev-DPd12uEU.js +1925 -0
  290. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/dev-DPd12uEU.js.map +1 -0
  291. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/events-CDC4DKuQ.js +206 -0
  292. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/events-CDC4DKuQ.js.map +1 -0
  293. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/flags-BD-lYX7s.js +20 -0
  294. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/flags-BD-lYX7s.js.map +1 -0
  295. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/hydration-D3UBU2t8.js +264 -0
  296. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/hydration-D3UBU2t8.js.map +1 -0
  297. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/legacy-client-B0ZUlbSQ.js +698 -0
  298. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/legacy-client-B0ZUlbSQ.js.map +1 -0
  299. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/package.json +3 -0
  300. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte.js +135 -0
  301. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte.js.map +1 -0
  302. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_animate.js +57 -0
  303. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_animate.js.map +1 -0
  304. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_attachments.js +2 -0
  305. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_compiler.js +46691 -0
  306. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_compiler.js.map +1 -0
  307. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_easing.js +239 -0
  308. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_easing.js.map +1 -0
  309. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_events.js +2 -0
  310. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal.js +6 -0
  311. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal.js.map +1 -0
  312. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_client.js +7 -0
  313. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_disclose-version.js +6 -0
  314. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_disclose-version.js.map +1 -0
  315. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_async.js +7 -0
  316. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_async.js.map +1 -0
  317. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_legacy.js +7 -0
  318. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_legacy.js.map +1 -0
  319. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_tracing.js +7 -0
  320. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_tracing.js.map +1 -0
  321. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_server.js +6 -0
  322. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_legacy.js +85 -0
  323. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_legacy.js.map +1 -0
  324. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_motion.js +561 -0
  325. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_motion.js.map +1 -0
  326. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity.js +26 -0
  327. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity.js.map +1 -0
  328. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity_window.js +124 -0
  329. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity_window.js.map +1 -0
  330. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_server.js +2 -0
  331. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_store.js +91 -0
  332. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_store.js.map +1 -0
  333. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_transition.js +242 -0
  334. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_transition.js.map +1 -0
  335. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-lObbumRX.js +497 -0
  336. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-lObbumRX.js.map +1 -0
  337. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-mR9mAWyK.js +4929 -0
  338. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-mR9mAWyK.js.map +1 -0
  339. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/validate-CezNmRkM.js +192 -0
  340. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/validate-CezNmRkM.js.map +1 -0
  341. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/version-BHHo0WE-.js +10 -0
  342. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/version-BHHo0WE-.js.map +1 -0
  343. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/warnings-yiExAB06.js +140 -0
  344. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/warnings-yiExAB06.js.map +1 -0
  345. package/src/overlays.test.ts +90 -90
  346. package/src/render-component.ts +28 -28
  347. package/src/row-drag.test.ts +1 -1
  348. package/src/row-resize.test.ts +1 -2
  349. package/src/row-resize.ts +210 -210
  350. package/src/scheduler-model.test.ts +562 -562
  351. package/src/scheduler-model.ts +872 -872
  352. package/src/scroll-sync.test.ts +4 -7
  353. package/src/scroll-sync.ts +0 -122
  354. package/src/selection.test.ts +754 -754
  355. package/src/selection.ts +491 -600
  356. package/src/server-data-source.test.ts +289 -289
  357. package/src/server-data-source.ts +413 -413
  358. package/src/server-group-model.test.ts +2 -2
  359. package/src/sparkline.test.ts +68 -68
  360. package/src/sparkline.ts +169 -169
  361. package/src/spreadsheet.test.ts +488 -488
  362. package/src/spreadsheet.ts +312 -312
  363. package/src/static-functions.ts +11 -11
  364. package/src/subscribe.ts +38 -38
  365. package/src/summaries.ts +0 -112
  366. package/src/svgrid-wrapper.types.ts +526 -526
  367. package/src/svgrid.async-editor-options.test.ts +271 -271
  368. package/src/svgrid.auto-row-height.test.ts +204 -204
  369. package/src/svgrid.behavior.test.ts +705 -706
  370. package/src/svgrid.charting.test.ts +534 -534
  371. package/src/svgrid.column-reorder.test.ts +0 -8
  372. package/src/svgrid.comments-autocomplete.test.ts +123 -123
  373. package/src/svgrid.context-menu.test.ts +147 -147
  374. package/src/svgrid.features.test.ts +157 -157
  375. package/src/svgrid.filter-depth.test.ts +163 -163
  376. package/src/svgrid.filter-menu-listbox.svelte.test.ts +322 -322
  377. package/src/svgrid.filter-menu-scroll.test.ts +112 -112
  378. package/src/svgrid.footer-pagesize.svelte.test.ts +86 -0
  379. package/src/svgrid.grand-total.test.ts +188 -188
  380. package/src/svgrid.group-display-mode.test.ts +171 -171
  381. package/src/svgrid.group-footers.test.ts +121 -121
  382. package/src/svgrid.group-pagination.test.ts +153 -153
  383. package/src/svgrid.new-features.wrapper.test.ts +251 -251
  384. package/src/svgrid.tree-data.test.ts +186 -186
  385. package/src/svgrid.wrapper.test.ts +40 -40
  386. package/src/svgriddropdown.async-panel.svelte.test.ts +195 -195
  387. package/src/test-setup.ts +62 -62
  388. package/src/themes/index.ts +257 -257
  389. package/src/toast-store.svelte.ts +246 -246
  390. package/src/toast-store.test.ts +147 -147
  391. package/src/tree-row-model.test.ts +168 -169
  392. package/src/ui-buttons.test.ts +144 -144
  393. package/src/ui-inputs.test.ts +118 -118
  394. package/src/ui-localization.test.ts +113 -113
  395. package/src/ui-range.test.ts +70 -70
  396. package/src/ui-selection.test.ts +155 -155
  397. package/src/ui-tier1.test.ts +142 -142
  398. package/src/virtual.test.ts +88 -88
  399. package/src/virtualization/column-virtualizer.test.ts +27 -27
  400. package/src/virtualization/column-virtualizer.ts +30 -30
  401. package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
  402. package/src/virtualization/types.ts +30 -30
  403. package/src/virtualization/virtualizer.test.ts +47 -47
  404. package/src/virtualization/virtualizer.ts +296 -296
  405. package/dist/cdn/disclose-version-DoD9AFD_.js +0 -2958
package/src/core.ts CHANGED
@@ -1,1261 +1,1261 @@
1
- import type { SparklineConfig } from './sparkline'
2
- import { resolveColumnId } from './column-id'
3
-
4
- export type RowData = Record<string, unknown>
5
- export type Updater<T> = T | ((prev: T) => T)
6
- export type SortingState = Array<{ id: string; desc: boolean }>
7
- export type ColumnFilter = { id: string; value: unknown; fn?: keyof typeof filterFns }
8
- export type ColumnFiltersState = Array<ColumnFilter>
9
- export type PaginationState = { pageIndex: number; pageSize: number }
10
- export type GroupingState = Array<string>
11
- export type ExpandedState = Record<string, boolean>
12
- export type RowSelectionState = Record<string, boolean>
13
- export type ActiveCellState = {
14
- rowIndex: number
15
- colIndex: number
16
- cellId: string | null
17
- }
18
- export type TableFeatures = Record<string, unknown>
19
-
20
- export type CellData = unknown
21
-
22
- export type HeaderContext<TData extends RowData> = {
23
- header: Header<TData>
24
- column: Column<TData>
25
- table: SvGrid<TData>
26
- }
27
-
28
- export type CellContext<TData extends RowData> = {
29
- cell: Cell<TData>
30
- row: Row<TData>
31
- column: Column<TData>
32
- table: SvGrid<TData>
33
- getValue: () => unknown
34
- }
35
-
36
- /** Params passed to a column's `colSpan(...)` / `rowSpan(...)` callbacks. */
37
- export type CellSpanParams<TData extends RowData = RowData> = {
38
- /** The row's underlying data object. */
39
- data: TData
40
- /** Display-row index in the current (filtered/sorted) row set. */
41
- rowIndex: number
42
- /** The column's id. */
43
- columnId: string
44
- /** The cell's base value for this column. */
45
- value: unknown
46
- }
47
-
48
- /** The raw option list a column's `editorOptions` can supply. */
49
- export type EditorOptionSource = ReadonlyArray<
50
- string | number | { value: string | number; label?: string; color?: string }
51
- >
52
-
53
- /** Params passed to a column's `valueParser(...)` on edit commit. */
54
- export type ValueParserParams<TData extends RowData = RowData> = {
55
- /** The value after built-in per-`editorType` coercion. */
56
- newValue: unknown
57
- /** The cell's previous value. */
58
- oldValue: unknown
59
- /** The raw string the editor produced (pre-coercion). */
60
- rawInput: string
61
- /** The row's underlying data object. */
62
- data: TData
63
- /** The column's id. */
64
- columnId: string
65
- }
66
-
67
- /**
68
- * Context passed to a custom `cellEditor` snippet/component. Three write
69
- * helpers cover the lifecycle:
70
- *
71
- * - `update(next)` - stage `next` as the draft, keep the editor open.
72
- * Use this for live-preview controls (sliders,
73
- * color pickers) so the user can keep adjusting.
74
- * - `commit(next?)` - write the value AND close the editor. The
75
- * argument is optional; when omitted, the most
76
- * recently `update()`d value is saved. Use this
77
- * for "done" gestures (Enter, picking an option).
78
- * - `cancel()` - discard the draft and close the editor.
79
- */
80
- export type EditorContext<TData extends RowData> = CellContext<TData> & {
81
- value: unknown
82
- update: (next: unknown) => void
83
- commit: (next?: unknown) => void
84
- cancel: () => void
85
- }
86
-
87
- export type CellFormatConfig =
88
- | {
89
- type: 'number'
90
- locales?: string | Array<string>
91
- options?: Intl.NumberFormatOptions
92
- }
93
- | {
94
- type: 'currency'
95
- /** ISO 4217 (default USD) */
96
- currency?: string
97
- locales?: string | Array<string>
98
- options?: Omit<Intl.NumberFormatOptions, 'style' | 'currency'>
99
- }
100
- | {
101
- type: 'percent'
102
- locales?: string | Array<string>
103
- options?: Omit<Intl.NumberFormatOptions, 'style'>
104
- /**
105
- * If true, numeric cell values are 0–100 (e.g. 42 → 42%) instead of Intl’s 0–1 fraction (0.42 → 42%).
106
- * Default false.
107
- */
108
- valueIsPercentPoints?: boolean
109
- }
110
- | {
111
- type: 'date' | 'datetime'
112
- locales?: string | Array<string>
113
- /**
114
- * Shortcut patterns merged with `options`:
115
- * `'d'` short numeric date, `'D'` long date, `'y-m-d'` yyyy/mm/dd-style,
116
- * `'short'`|`'medium'`|`'long'` use dateStyle/timeStyle presets.
117
- */
118
- pattern?: string
119
- options?: Intl.DateTimeFormatOptions
120
- }
121
-
122
- export type CellFormatter<TData extends RowData> = (context: {
123
- value: unknown
124
- row: Row<TData>
125
- column: Column<TData>
126
- table: SvGrid<TData>
127
- }) => string
128
-
129
- export type ColumnDefTemplate<TContext> = string | ((context: TContext) => unknown)
130
-
131
- /**
132
- * How a column's value is aggregated for a group row when `columnGrouping`
133
- * is active. Built-in reducers cover the common cases; pass a function for
134
- * anything custom (weighted average, median, percentile, distinct count).
135
- * The function receives the finite numeric values AND the raw leaf rows.
136
- */
137
- export type GroupAggregator<TData = any> =
138
- | 'sum'
139
- | 'avg'
140
- | 'min'
141
- | 'max'
142
- | 'count'
143
- | 'countDistinct'
144
- | 'extent'
145
- | 'first'
146
- | ((values: number[], rows: Array<TData>) => unknown)
147
-
148
- /** Apply a group aggregator over a bucket's leaf rows for one column. */
149
- export function applyGroupAggregate<TData extends RowData>(
150
- agg: GroupAggregator<TData>,
151
- columnId: string,
152
- rows: ReadonlyArray<Row<TData>>,
153
- ): unknown {
154
- const raw = rows.map((r) => r.getCellValueByColumnId(columnId))
155
- if (typeof agg === 'function') {
156
- const nums = raw.map((v) => Number(v)).filter((n) => Number.isFinite(n))
157
- return agg(nums, rows.map((r) => r.original))
158
- }
159
- if (agg === 'count') return rows.length
160
- if (agg === 'countDistinct') return new Set(raw.map((v) => String(v ?? ''))).size
161
- if (agg === 'first') return raw[0]
162
- const nums = raw.map((v) => Number(v)).filter((n) => Number.isFinite(n))
163
- if (!nums.length) return undefined
164
- switch (agg) {
165
- case 'sum':
166
- return nums.reduce((a, b) => a + b, 0)
167
- case 'avg':
168
- return nums.reduce((a, b) => a + b, 0) / nums.length
169
- case 'min':
170
- return Math.min(...nums)
171
- case 'max':
172
- return Math.max(...nums)
173
- case 'extent':
174
- return `${Math.min(...nums)} – ${Math.max(...nums)}`
175
- default:
176
- return undefined
177
- }
178
- }
179
-
180
- /**
181
- * A column definition.
182
- *
183
- * `TFeatures` is a phantom parameter - it is threaded through nested
184
- * `columns` groups but no member depends on it, so `{}`, `TableFeatures` and
185
- * `typeof features` are all interchangeable here. It is deliberately left
186
- * WITHOUT a default: `ColumnDef<Row>` would otherwise bind `Row` to this slot
187
- * and silently type your data as `RowData`, losing every field-name check.
188
- * Prefer {@link GridColumns} / {@link GridColumnDef} for the common case.
189
- */
190
- export type ColumnDef<TFeatures extends TableFeatures, TData extends RowData> = {
191
- id?: string
192
- field?: keyof TData & string
193
- fieldFn?: (row: TData) => unknown
194
- header?: ColumnDefTemplate<HeaderContext<TData>>
195
- footer?: ColumnDefTemplate<HeaderContext<TData>>
196
- cell?: ColumnDefTemplate<CellContext<TData>>
197
- columns?: Array<ColumnDef<TFeatures, TData>>
198
- /**
199
- * Declarative cell spanning (merged cells). Return how many COLUMNS this
200
- * cell spans to the right (1 = no span). Value-driven, AG-Grid-style. Feed
201
- * `spansToMerges(rows, columns)` into `spreadsheetLayout` to apply - it uses
202
- * the same real `colspan`/`rowspan` merge engine (no separate code path).
203
- */
204
- colSpan?: (params: CellSpanParams<TData>) => number
205
- /**
206
- * Declarative cell spanning (merged cells). Return how many ROWS this cell
207
- * spans downward (1 = no span). See `colSpan` for how to apply.
208
- */
209
- rowSpan?: (params: CellSpanParams<TData>) => number
210
- /**
211
- * High-level data type for the column. A convenience that resolves to the
212
- * right `editorType`, alignment, date `format`, and filter operators without
213
- * setting each by hand:
214
- * 'text' → text editor, left-aligned
215
- * 'number' → number editor, right-aligned, numeric filter operators
216
- * 'boolean' → checkbox editor, centered
217
- * 'date' → date editor (Date values), right-aligned, `{ type: 'date' }` format
218
- * 'dateString' → date editor for ISO date STRINGS (e.g. '2026-06-27')
219
- * Anything you set explicitly (`editorType`, `align`, `format`) still wins -
220
- * `cellDataType` only fills the gaps. Grid-level `inferColumnTypes` infers
221
- * this from the first data row for columns that declare neither.
222
- */
223
- cellDataType?: 'text' | 'number' | 'boolean' | 'date' | 'dateString'
224
- /**
225
- * Hide this column when the grid's `responsive` mode is on and the grid is
226
- * narrower than this many pixels - drop low-priority columns on small
227
- * screens. No effect unless the grid has `responsive` set.
228
- */
229
- hideBelow?: number
230
- /**
231
- * For a column INSIDE a collapsible column group: `'open'` shows this column
232
- * only while the group is expanded, `'closed'` only while collapsed. Omit to
233
- * always show it. Setting it on any direct child gives the parent group a
234
- * collapse toggle. Pair with `openByDefault` on the group.
235
- */
236
- columnGroupShow?: 'open' | 'closed'
237
- /**
238
- * For a GROUP column (one with `columns: [...]`): start the group expanded.
239
- * Defaults to `false` (collapsed), matching AG Grid - so only the always-on
240
- * and `columnGroupShow: 'closed'` children show until the user expands it.
241
- */
242
- openByDefault?: boolean
243
- editorType?:
244
- | 'text'
245
- | 'number'
246
- | 'date' // rich SvCalendar popover (opt out with 'date-native')
247
- | 'datetime' // rich SvDateTimePicker (opt out with 'datetime-native')
248
- | 'time' // rich SvTimePicker dial (opt out with 'time-native')
249
- | 'date-native' // plain <input type="date">
250
- | 'datetime-native' // plain <input type="datetime-local">
251
- | 'time-native' // plain <input type="time"> - HH:MM or HH:MM:SS
252
- | 'password' // native <input type="password"> with masked rendering
253
- | 'checkbox'
254
- | 'list'
255
- | 'chips'
256
- | 'select' // custom dropdown - single value, no typeahead
257
- | 'rich-select' // custom dropdown with a typeahead search input
258
- | 'autocomplete' // free-text input with a live-filtered suggestion list (accepts any value)
259
- | 'textarea' // multi-line editor; Tab or Ctrl+Enter commits, plain Enter inserts a newline
260
- | 'color' // native <input type="color"> swatch
261
- | 'rating' // 5-star rating control
262
- // Any other string names a CUSTOM editor registered via `registerCellEditor`
263
- // (or `registerBuiltinEditors`). `(string & {})` keeps the literals above
264
- // autocompleting while allowing arbitrary custom type names.
265
- | (string & {})
266
- /**
267
- * Custom in-cell editor. Receives the cell context PLUS a `commit(value)`
268
- * and `cancel()` helper. Use when none of the built-in `editorType`s fit;
269
- * the snippet's outer element is mounted inside the editing cell and
270
- * inherits keyboard handling (Esc cancels, Enter commits unless your
271
- * snippet preventDefaults it).
272
- *
273
- * Coexists with `editorType`: when both are set, `cellEditor` wins and
274
- * `editorType` is treated as a hint for parsing the saved value.
275
- */
276
- cellEditor?: ColumnDefTemplate<EditorContext<TData>>
277
- /**
278
- * Per-column tooltip. String shows as a native `title=`; `(ctx) => string`
279
- * runs per cell so the tooltip can reflect the value. Returning an empty
280
- * string skips the tooltip.
281
- */
282
- tooltip?: string | ((ctx: CellContext<TData>) => string | null | undefined)
283
- /**
284
- * Declarative per-cell validation (Handsontable-style). Runs for EVERY
285
- * rendered cell - including values already present in `data` on load, not
286
- * just on edit - so bad data is flagged immediately. Invalid cells get the
287
- * `sv-grid-cell-invalid` class (red highlight) and the returned message as
288
- * their tooltip.
289
- *
290
- * Return value:
291
- * - `null` / `undefined` / `true` → valid (no highlight)
292
- * - `false` → invalid, no message
293
- * - a non-empty `string` → invalid, string is the tooltip
294
- *
295
- * The value keeps rendering as-is (the grid does NOT roll it back); pair
296
- * with `onCellValueChange` if you also want to reject the commit.
297
- */
298
- validate?: (params: {
299
- value: unknown
300
- row: TData
301
- rowIndex: number
302
- column: Column<TData>
303
- }) => string | boolean | null | undefined
304
- /**
305
- * Gate editing per column or per cell.
306
- *
307
- * - `true` (or omitted): the column is fully editable.
308
- * - `false`: the column is read-only - double-click, type-to-edit,
309
- * fill-handle drag, Delete, and clipboard paste all skip it.
310
- * - `(ctx) => boolean`: evaluated for each cell, so you can lock
311
- * individual rows (e.g. by role, status, ownership). Returning
312
- * `false` opts the cell out of every editing path, identical to
313
- * setting `editable: false` on the whole column for that row.
314
- *
315
- * The grid-wide `enableInlineEditing` prop still wins when set to
316
- * `false`.
317
- */
318
- editable?: boolean | ((context: CellContext<TData>) => boolean)
319
- /**
320
- * Transform the committed edit value before it is written to the row.
321
- * Runs after the built-in per-`editorType` coercion, so `newValue` is
322
- * already type-parsed; return the final value to store (e.g. round a
323
- * number, uppercase a code, look up an id). AG-Grid-style `valueParser`.
324
- */
325
- valueParser?: (params: ValueParserParams<TData>) => unknown
326
- /**
327
- * Briefly flash / highlight this column's cell when its value changes
328
- * (streaming feeds, edits, server pushes). `true` uses the default flash;
329
- * pass `{ className }` to apply your own animation class instead.
330
- */
331
- cellFlash?: boolean | { className?: string }
332
- /**
333
- * When `false`, this column never shows a sort indicator and clicking
334
- * its header is a no-op - `api.setSort(thisColumn, ...)` is also
335
- * ignored. Defaults to `true` (the column participates in sorting as
336
- * long as `rowSortingFeature` is registered).
337
- */
338
- sortable?: boolean
339
- /**
340
- * When `false`, this column never shows a filter funnel / menu and
341
- * `api.setFilter(thisColumn, ...)` is ignored. Defaults to `true` (the
342
- * column is filterable as long as `columnFilteringFeature` is
343
- * registered).
344
- */
345
- filterable?: boolean
346
- /**
347
- * Options for `editorType: 'list' | 'chips'`. Either bare values (the
348
- * string is both value and label) or `{ value, label }` objects.
349
- * For `chips` this is optional - when omitted, the chips editor becomes
350
- * free-form (user types and presses Enter to commit a chip).
351
- *
352
- * Pass a function `(row) => options` for row-dependent (cascading)
353
- * options - e.g. City options that depend on Country in the same row.
354
- *
355
- * Either form may return a **Promise**, for options that come from the
356
- * server. While it resolves, the editor shows a loading state and the cell
357
- * renders its raw value.
358
- *
359
- * Results are cached so reopening an editor does not refetch: a static source
360
- * per column, a per-row source per row AND per that row's data - so a cascade
361
- * reloads by itself when the cell it depends on is edited. Call
362
- * `api.refreshEditorOptions(columnId?)` when the list changes server-side.
363
- */
364
- editorOptions?:
365
- | EditorOptionSource
366
- | Promise<EditorOptionSource>
367
- | ((row: TData) => EditorOptionSource | Promise<EditorOptionSource>)
368
- /** When true, list/chips allow multiple selections. Cell value becomes an array. */
369
- editorMultiple?: boolean
370
- /** Separator used when joining array values for the readonly cell display. Defaults to ', '. */
371
- editorSeparator?: string
372
- format?: CellFormatConfig
373
- formatter?: CellFormatter<TData>
374
- /**
375
- * Aggregate this column's values into the group row when grouping is
376
- * active. `'sum' | 'avg' | 'min' | 'max' | 'count' | 'countDistinct' |
377
- * 'extent' | 'first'`, or a custom `(values, rows) => unknown`. The result
378
- * is formatted with this column's `format` and shown in the group header.
379
- */
380
- aggregate?: GroupAggregator<TData>
381
- /**
382
- * Render the cell as an in-cell sparkline chart. The cell value should be
383
- * an array of numbers (or a comma/space separated string). Mutually
384
- * exclusive with a custom `cell` renderer (a `cell` wins if both are set).
385
- *
386
- * { sparkline: { type: 'line' } } // default line
387
- * { sparkline: { type: 'bar', color: '#16a34a' } }
388
- * { sparkline: { type: 'winloss' } } // sign-only up/down
389
- *
390
- * See `SparklineConfig` for the full option set (type, color,
391
- * negativeColor, width, height, fixed min/max).
392
- */
393
- sparkline?: SparklineConfig
394
- /** Initial column width in pixels. Falls back to the grid's `columnWidth` prop. */
395
- width?: number
396
- /**
397
- * Initial visibility. Set `false` to start the column hidden while still
398
- * listing it in the Choose Columns UI for the user to re-enable. Applied
399
- * once at mount; after that `api.setColumnVisible` / user toggles win.
400
- * On a group column, `false` hides the whole group's leaf columns.
401
- */
402
- visible?: boolean
403
- /**
404
- * Horizontal alignment for header and body cells. When omitted, the
405
- * default is inferred from `editorType`:
406
- * - `'number' | 'date' | 'datetime'` → `'right'`
407
- * - `'checkbox'` → `'center'`
408
- * - everything else → `'left'`
409
- */
410
- align?: 'left' | 'center' | 'right'
411
- /**
412
- * Per-cell conditional CSS. Two shapes:
413
- *
414
- * - **String** (or array of strings): class name(s) added to the
415
- * cell's `<td>` for every row in this column.
416
- * - **Function**: invoked per cell with the same `CellContext` shape
417
- * the `cell` renderer receives. Return a string, an array of
418
- * strings, or an object mapping class names to booleans.
419
- *
420
- * Use it for status tinting, conditional bold, "negative number"
421
- * coloring - anything that's a function of the row's value. Cells
422
- * still receive their format / cell renderer; the class just
423
- * augments the rendered `<td>`.
424
- */
425
- cellClass?:
426
- | string
427
- | ReadonlyArray<string>
428
- | ((ctx: CellContext<TData>) => string | ReadonlyArray<string> | Record<string, boolean> | undefined | null)
429
- }
430
-
431
- /**
432
- * A column definition keyed only by your row type - the ergonomic form of
433
- * {@link ColumnDef}, whose first parameter is a phantom feature bag that is
434
- * almost always `{}`.
435
- *
436
- * ```ts
437
- * const columns: GridColumns<Person> = [{ field: 'firstName', header: 'Name' }]
438
- * ```
439
- *
440
- * Interchangeable with `ColumnDef<{}, TData>` and `ColumnDef<typeof features,
441
- * TData>` in both directions, so it mixes freely with existing code.
442
- */
443
- export type GridColumnDef<TData extends RowData = RowData> = ColumnDef<TableFeatures, TData>
444
-
445
- /** An array of {@link GridColumnDef} - what you pass to `<SvGrid columns={...}>`. */
446
- export type GridColumns<TData extends RowData = RowData> = Array<GridColumnDef<TData>>
447
-
448
- export type Column<TData extends RowData> = {
449
- id: string
450
- columnDef: ColumnDef<any, TData>
451
- depth: number
452
- parentId?: string
453
- getCanSort: () => boolean
454
- getCanFilter: () => boolean
455
- getIsSorted: () => false | 'asc' | 'desc'
456
- getToggleSortingHandler: () => () => void
457
- }
458
-
459
- export type Header<TData extends RowData> = {
460
- id: string
461
- isPlaceholder: boolean
462
- colSpan: number
463
- column: Column<TData>
464
- getContext: () => HeaderContext<TData>
465
- }
466
-
467
- export type HeaderGroup<TData extends RowData> = {
468
- id: string
469
- headers: Array<Header<TData>>
470
- }
471
-
472
- export type Cell<TData extends RowData> = {
473
- id: string
474
- row: Row<TData>
475
- column: Column<TData>
476
- getValue: () => unknown
477
- getContext: () => CellContext<TData>
478
- }
479
-
480
- export type Row<TData extends RowData> = {
481
- id: string
482
- index: number
483
- original: TData
484
- depth: number
485
- subRows?: Array<Row<TData>>
486
- /** Total leaf (data) rows under this group row. Undefined for data rows. */
487
- leafCount?: number
488
- getCanExpand: () => boolean
489
- getIsExpanded: () => boolean
490
- toggleExpanded: () => void
491
- getIsSelected: () => boolean
492
- toggleSelected: () => void
493
- getAllCells: () => Array<Cell<TData>>
494
- getCellValueByColumnId: (columnId: string) => unknown
495
- }
496
-
497
- export type RowModel<TData extends RowData> = {
498
- rows: Array<Row<TData>>
499
- }
500
-
501
- export type Store<T> = {
502
- readonly state: T
503
- setState: (updater: (prev: T) => T) => void
504
- subscribe: (listener: () => void) => () => void
505
- }
506
-
507
- function createStore<T>(initial: T): Store<T> {
508
- let value = initial
509
- const listeners = new Set<() => void>()
510
- return {
511
- get state() {
512
- return value
513
- },
514
- setState(updater) {
515
- value = updater(value)
516
- listeners.forEach((listener) => listener())
517
- },
518
- subscribe(listener) {
519
- listeners.add(listener)
520
- return () => listeners.delete(listener)
521
- },
522
- }
523
- }
524
-
525
- export const rowSortingFeature = { key: 'rowSortingFeature' }
526
- export const columnFilteringFeature = { key: 'columnFilteringFeature' }
527
- export const rowPaginationFeature = { key: 'rowPaginationFeature' }
528
- export const columnGroupingFeature = { key: 'columnGroupingFeature' }
529
- export const rowSelectionFeature = { key: 'rowSelectionFeature' }
530
- export const rowExpandingFeature = { key: 'rowExpandingFeature' }
531
-
532
- export function tableFeatures<T extends TableFeatures>(features: T): T {
533
- return features
534
- }
535
-
536
- export const sortFns = {
537
- auto: (a: unknown, b: unknown) => String(a).localeCompare(String(b)),
538
- number: (a: unknown, b: unknown) => Number(a ?? 0) - Number(b ?? 0),
539
- date: (a: unknown, b: unknown) => {
540
- const aa = new Date(a as any).getTime()
541
- const bb = new Date(b as any).getTime()
542
- return aa - bb
543
- },
544
- }
545
-
546
- export const filterFns = {
547
- includesString: (value: unknown, query: string) =>
548
- String(value).toLowerCase().includes(query.toLowerCase()),
549
- equals: (value: unknown, query: unknown) => value === query,
550
- }
551
-
552
- export type RowModelFactory<TData extends RowData> = (args: {
553
- table: SvGrid<TData>
554
- rows: Array<Row<TData>>
555
- }) => Array<Row<TData>>
556
-
557
- export function createCoreRowModel<TData extends RowData>(): RowModelFactory<TData> {
558
- return ({ rows }) => rows
559
- }
560
- export function createFilteredRowModel<TData extends RowData>(): RowModelFactory<TData> {
561
- return ({ table, rows }) => {
562
- const filters: ColumnFiltersState = table.getState().columnFilters ?? []
563
- if (!filters.length) return rows
564
- return rows.filter((row) => {
565
- return filters.every((filter) => {
566
- const cellValue = row
567
- .getAllCells()
568
- .find((cell) => cell.column.id === filter.id)
569
- ?.getValue()
570
- const filterFn = filter.fn ? filterFns[filter.fn] : filterFns.includesString
571
- return filterFn(cellValue, filter.value as any)
572
- })
573
- })
574
- }
575
- }
576
- export function createPaginatedRowModel<TData extends RowData>(): RowModelFactory<TData> {
577
- return ({ table, rows }) => {
578
- const pagination = table.getState().pagination ?? { pageIndex: 0, pageSize: rows.length || 10 }
579
- const start = pagination.pageIndex * pagination.pageSize
580
- return rows.slice(start, start + pagination.pageSize)
581
- }
582
- }
583
- export function createGroupedRowModel<TData extends RowData>(): RowModelFactory<TData> {
584
- return ({ table, rows }) => {
585
- const grouping: GroupingState = table.getState().grouping ?? []
586
- if (!grouping.length) return rows
587
- const columns = table.getAllColumns()
588
-
589
- // Recursively bucket rows by each grouping column in turn. At every level a
590
- // group row is built that stands in for its children - a non-group column
591
- // resolves to the value shared by every leaf row, or to undefined when the
592
- // leaves disagree.
593
- function buildGroups(
594
- input: Array<Row<TData>>,
595
- levelIndex: number,
596
- depth: number,
597
- idPrefix: string,
598
- ): Array<Row<TData>> {
599
- if (levelIndex >= grouping.length) {
600
- // Leaves: actual data rows, with their nesting depth recorded.
601
- return input.map((row) => ({ ...row, depth }))
602
- }
603
- const groupKey = grouping[levelIndex]
604
- if (!groupKey) return input
605
-
606
- const buckets = new Map<string, Array<Row<TData>>>()
607
- for (const row of input) {
608
- const value = row.getCellValueByColumnId(groupKey)
609
- const key = String(value ?? '')
610
- const list = buckets.get(key) ?? []
611
- list.push(row)
612
- buckets.set(key, list)
613
- }
614
-
615
- const groupRows: Array<Row<TData>> = []
616
- let index = 0
617
- buckets.forEach((children, key) => {
618
- const id = `${idPrefix}_${groupKey}_${key}`
619
- const subRows = buildGroups(children, levelIndex + 1, depth + 1, id)
620
- const isDeepest = levelIndex + 1 >= grouping.length
621
- const leafCount = isDeepest
622
- ? subRows.length
623
- : subRows.reduce((sum, sub) => sum + (sub.leafCount ?? 0), 0)
624
-
625
- const resolveColumnValue = (columnId: string): unknown => {
626
- if (columnId === groupKey) return key
627
- let resolved: unknown
628
- let hasResolved = false
629
- for (const child of children) {
630
- const childValue = child.getCellValueByColumnId(columnId)
631
- if (!hasResolved) {
632
- resolved = childValue
633
- hasResolved = true
634
- } else if (childValue !== resolved) {
635
- return undefined
636
- }
637
- }
638
- return resolved
639
- }
640
-
641
- const groupOriginal: Record<string, unknown> = {}
642
- columns.forEach((column) => {
643
- const field = column.columnDef.field
644
- if (!field) return
645
- const agg = column.columnDef.aggregate
646
- groupOriginal[field] = agg
647
- ? applyGroupAggregate(agg, column.id, children)
648
- : resolveColumnValue(column.id)
649
- })
650
-
651
- const groupRow: Row<TData> = {
652
- id,
653
- index: index++,
654
- original: groupOriginal as TData,
655
- depth,
656
- subRows,
657
- leafCount,
658
- getCanExpand: () => true,
659
- getIsExpanded: () => Boolean((table.getState().expanded ?? {})[id]),
660
- toggleExpanded: () => {
661
- table.setExpanded((prev) => ({ ...prev, [id]: !prev[id] }))
662
- },
663
- getIsSelected: () => Boolean((table.getState().rowSelection ?? {})[id]),
664
- toggleSelected: () => {
665
- table.setRowSelection((prev) => ({ ...prev, [id]: !prev[id] }))
666
- },
667
- getAllCells: () => [],
668
- // Prefer the precomputed group value (which carries aggregates)
669
- // and fall back to the shared-value resolver for columns without
670
- // a field.
671
- getCellValueByColumnId: (columnId: string) => {
672
- const col = columns.find((c) => c.id === columnId)
673
- const field = col?.columnDef.field
674
- if (field && field in groupOriginal) return groupOriginal[field]
675
- return resolveColumnValue(columnId)
676
- },
677
- }
678
- groupRows.push(groupRow)
679
- })
680
- return groupRows
681
- }
682
-
683
- return buildGroups(rows, 0, 0, 'group')
684
- }
685
- }
686
- export type TreeRowModelOptions = {
687
- /** Field holding each row's parent id. Rows with no parent are roots. */
688
- parentField: string
689
- /** Field holding the row's own id. Defaults to `'id'`. */
690
- idField?: string
691
- }
692
-
693
- /**
694
- * Client-side tree data: nest the grid's own flat rows into a parent/child
695
- * hierarchy that `createExpandedRowModel` then walks.
696
- *
697
- * This works on the rows the grid already built rather than on raw data, so
698
- * tree rows keep their cells, editing, selection and formatting - they are real
699
- * data rows that happen to have children, not synthetic banners like grouping's.
700
- * That is also why the model is parent-id based: nested source arrays never
701
- * become rows (the grid only builds rows for `data`), so nested input is
702
- * flattened first with {@link flattenTreeData}. One code path, no duplicated
703
- * row construction.
704
- *
705
- * Rows are tagged `__treeRow` so `isGroupRow` does not mistake an expandable
706
- * data row for a full-width group banner.
707
- */
708
- export function createTreeRowModel<TData extends RowData>(
709
- options: TreeRowModelOptions,
710
- ): RowModelFactory<TData> {
711
- const { parentField, idField = 'id' } = options
712
- return ({ table, rows }) => {
713
- if (!rows.length) return rows
714
- const keyOf = (row: Row<TData>) => (row.original as any)?.[idField]
715
- const parentOf = (row: Row<TData>) => (row.original as any)?.[parentField]
716
-
717
- const present = new Set<unknown>()
718
- for (const row of rows) present.add(keyOf(row))
719
-
720
- const childrenByParent = new Map<unknown, Array<Row<TData>>>()
721
- const roots: Array<Row<TData>> = []
722
- for (const row of rows) {
723
- const parent = parentOf(row)
724
- // A row whose parent is absent (filtered out, or never existed) becomes a
725
- // root rather than disappearing - silently dropping rows is worse than a
726
- // shallower tree. Self-parenting is treated the same way.
727
- if (parent == null || parent === keyOf(row) || !present.has(parent)) {
728
- roots.push(row)
729
- continue
730
- }
731
- const list = childrenByParent.get(parent) ?? []
732
- list.push(row)
733
- childrenByParent.set(parent, list)
734
- }
735
-
736
- // Guards a cycle in the parent chain from recursing forever.
737
- const seen = new Set<unknown>()
738
- const build = (row: Row<TData>, depth: number): Row<TData> => {
739
- const key = keyOf(row)
740
- const id = row.id
741
- if (seen.has(key)) {
742
- return { ...row, depth, subRows: [], getCanExpand: () => false } as Row<TData>
743
- }
744
- seen.add(key)
745
- const subRows = (childrenByParent.get(key) ?? []).map((child) => build(child, depth + 1))
746
- return {
747
- ...row,
748
- depth,
749
- subRows,
750
- leafCount: subRows.reduce((n, sub) => n + 1 + (sub.leafCount ?? 0), 0),
751
- __treeRow: true,
752
- getCanExpand: () => subRows.length > 0,
753
- getIsExpanded: () => Boolean((table.getState().expanded ?? {})[id]),
754
- toggleExpanded: () => {
755
- table.setExpanded((prev) => ({ ...prev, [id]: !prev[id] }))
756
- },
757
- } as Row<TData>
758
- }
759
-
760
- return roots.map((root) => build(root, 0))
761
- }
762
- }
763
-
764
- export type FlattenTreeOptions = {
765
- /** Field holding an array of child objects. */
766
- childrenField: string
767
- /** Field holding each object's id. Defaults to `'id'`. */
768
- idField?: string
769
- /** Field to WRITE the resolved parent id onto. Defaults to `'__parentId'`. */
770
- parentField?: string
771
- }
772
-
773
- /**
774
- * Flatten nested tree data into the flat parent-id shape `createTreeRowModel`
775
- * consumes, stamping each child with its parent's id.
776
- *
777
- * Children are emitted directly after their parent so the natural order already
778
- * matches the rendered tree. The `childrenField` array is left on the objects
779
- * (harmless, and callers often still want it); only the parent link is added.
780
- */
781
- export function flattenTreeData<T extends RowData>(
782
- data: ReadonlyArray<T>,
783
- options: FlattenTreeOptions,
784
- ): T[] {
785
- const { childrenField, idField = 'id', parentField = '__parentId' } = options
786
- const out: T[] = []
787
- const walk = (nodes: ReadonlyArray<T>, parentId: unknown) => {
788
- for (const node of nodes) {
789
- const flat = { ...node, [parentField]: parentId } as T
790
- out.push(flat)
791
- const kids = (node as any)[childrenField]
792
- if (Array.isArray(kids) && kids.length) walk(kids as ReadonlyArray<T>, (node as any)[idField])
793
- }
794
- }
795
- walk(data, null)
796
- return out
797
- }
798
-
799
- export function createExpandedRowModel<TData extends RowData>(): RowModelFactory<TData> {
800
- return ({ table, rows }) => {
801
- const expanded: ExpandedState = table.getState().expanded ?? {}
802
- const flattened: Array<Row<TData>> = []
803
- const visit = (row: Row<TData>) => {
804
- flattened.push(row)
805
- if (row.subRows?.length && expanded[row.id]) {
806
- for (const sub of row.subRows) visit(sub)
807
- }
808
- }
809
- for (const row of rows) visit(row)
810
- return flattened
811
- }
812
- }
813
- export function createSortedRowModel<TData extends RowData>(
814
- localSortFns: typeof sortFns = sortFns,
815
- ): RowModelFactory<TData> {
816
- return ({ table, rows }) => {
817
- const sorting = table.getState().sorting ?? []
818
- if (!sorting.length) return rows
819
-
820
- const sorted = [...rows].sort((a, b) => {
821
- for (const clause of sorting) {
822
- const column = table.getAllColumns().find((col) => col.id === clause.id)
823
- if (!column) continue
824
- const editorType = column.columnDef.editorType
825
- const comparator =
826
- editorType === 'number'
827
- ? localSortFns.number
828
- : editorType === 'date' || editorType === 'datetime'
829
- ? localSortFns.date
830
- : localSortFns.auto
831
- const result = comparator(
832
- a.getCellValueByColumnId(column.id),
833
- b.getCellValueByColumnId(column.id),
834
- )
835
- if (result !== 0) return clause.desc ? -result : result
836
- }
837
- return 0
838
- })
839
- return sorted
840
- }
841
- }
842
-
843
- export type SvGridOptions<TFeatures extends TableFeatures, TData extends RowData> = {
844
- _features: TFeatures
845
- _rowModels?: {
846
- coreRowModel?: RowModelFactory<TData>
847
- filteredRowModel?: RowModelFactory<TData>
848
- sortedRowModel?: RowModelFactory<TData>
849
- paginatedRowModel?: RowModelFactory<TData>
850
- groupedRowModel?: RowModelFactory<TData>
851
- expandedRowModel?: RowModelFactory<TData>
852
- }
853
- columns: Array<ColumnDef<TFeatures, TData>>
854
- data: ReadonlyArray<TData>
855
- /**
856
- * Optional row-id resolver. When set, the value it returns becomes
857
- * `row.id` (and therefore the selection / expansion / edit key). When
858
- * omitted, ids fall back to the row's array index as a string. Use a
859
- * stable id (database PK, UUID, etc.) so selection survives reorders.
860
- */
861
- getRowId?: (row: TData, index: number) => string
862
- state?: Partial<Record<string, any>>
863
- onSortingChange?: (updater: Updater<SortingState>) => void
864
- onColumnFiltersChange?: (updater: Updater<ColumnFiltersState>) => void
865
- onPaginationChange?: (updater: Updater<PaginationState>) => void
866
- onGroupingChange?: (updater: Updater<GroupingState>) => void
867
- onExpandedChange?: (updater: Updater<ExpandedState>) => void
868
- onRowSelectionChange?: (updater: Updater<RowSelectionState>) => void
869
- onActiveCellChange?: (updater: Updater<ActiveCellState>) => void
870
- }
871
-
872
- export type SvGrid<TData extends RowData> = {
873
- store: Store<Record<string, any>>
874
- optionsStore: Store<Record<string, any>>
875
- state: Record<string, any>
876
- getState: () => Record<string, any>
877
- setOptions: (updater: Updater<Record<string, any>>) => void
878
- setColumnFilters: (updater: Updater<ColumnFiltersState>) => void
879
- setPagination: (updater: Updater<PaginationState>) => void
880
- setGrouping: (updater: Updater<GroupingState>) => void
881
- setExpanded: (updater: Updater<ExpandedState>) => void
882
- setRowSelection: (updater: Updater<RowSelectionState>) => void
883
- setActiveCell: (updater: Updater<ActiveCellState>) => void
884
- moveActiveCell: (next: { rowDelta?: number; colDelta?: number }) => void
885
- getAllColumns: () => Array<Column<TData>>
886
- getHeaderGroups: () => Array<HeaderGroup<TData>>
887
- getFooterGroups: () => Array<HeaderGroup<TData>>
888
- getRowModel: () => RowModel<TData>
889
- }
890
-
891
- type InternalGrid<TData extends RowData> = SvGrid<TData> & {
892
- getAllColumns: () => Array<Column<TData>>
893
- }
894
-
895
- export function createSvGridCore<TFeatures extends TableFeatures, TData extends RowData>(
896
- options: SvGridOptions<TFeatures, TData>,
897
- ): SvGrid<TData> {
898
- const internalState: Record<string, any> = {
899
- sorting: [],
900
- columnFilters: [],
901
- pagination: { pageIndex: 0, pageSize: options.data.length || 10 },
902
- grouping: [],
903
- expanded: {},
904
- rowSelection: {},
905
- activeCell: { rowIndex: 0, colIndex: 0, cellId: null },
906
- ...(options.state ?? {}),
907
- }
908
- const store = createStore(internalState)
909
- const optionsStore = createStore(options as Record<string, any>)
910
- let cachedColumnsInput: Array<ColumnDef<TFeatures, TData>> | null = null
911
- let cachedColumns: Array<Column<TData>> = []
912
- let cachedHeaderGroups: Array<HeaderGroup<TData>> = []
913
- let cachedBaseRowsInput: ReadonlyArray<TData> | null = null
914
- let cachedBaseRowsColumns: Array<Column<TData>> | null = null
915
- let cachedBaseRows: Array<Row<TData>> = []
916
- let cachedRowModel: RowModel<TData> | null = null
917
- let cachedRowModelBaseRows: Array<Row<TData>> | null = null
918
- let cachedPipeline = options._rowModels
919
- let cachedSlices: {
920
- sorting: SortingState | undefined
921
- columnFilters: ColumnFiltersState | undefined
922
- pagination: PaginationState | undefined
923
- grouping: GroupingState | undefined
924
- expanded: ExpandedState | undefined
925
- rowSelection: RowSelectionState | undefined
926
- } | null = null
927
-
928
- const grid = {
929
- store,
930
- optionsStore,
931
- get state() {
932
- return store.state
933
- },
934
- getState() {
935
- return store.state
936
- },
937
- setOptions(updater: Updater<Record<string, any>>) {
938
- optionsStore.setState((prev) =>
939
- typeof updater === 'function' ? (updater as any)(prev) : updater,
940
- )
941
- },
942
- setColumnFilters(updater: Updater<ColumnFiltersState>) {
943
- store.setState((prev) => ({
944
- ...prev,
945
- columnFilters:
946
- typeof updater === 'function' ? (updater as any)(prev.columnFilters ?? []) : updater,
947
- }))
948
- options.onColumnFiltersChange?.(updater)
949
- },
950
- setPagination(updater: Updater<PaginationState>) {
951
- store.setState((prev) => ({
952
- ...prev,
953
- pagination:
954
- typeof updater === 'function'
955
- ? (updater as any)(prev.pagination ?? { pageIndex: 0, pageSize: 10 })
956
- : updater,
957
- }))
958
- options.onPaginationChange?.(updater)
959
- },
960
- setGrouping(updater: Updater<GroupingState>) {
961
- store.setState((prev) => ({
962
- ...prev,
963
- grouping: typeof updater === 'function' ? (updater as any)(prev.grouping ?? []) : updater,
964
- }))
965
- options.onGroupingChange?.(updater)
966
- },
967
- setExpanded(updater: Updater<ExpandedState>) {
968
- store.setState((prev) => ({
969
- ...prev,
970
- expanded: typeof updater === 'function' ? (updater as any)(prev.expanded ?? {}) : updater,
971
- }))
972
- options.onExpandedChange?.(updater)
973
- },
974
- setRowSelection(updater: Updater<RowSelectionState>) {
975
- store.setState((prev) => ({
976
- ...prev,
977
- rowSelection:
978
- typeof updater === 'function' ? (updater as any)(prev.rowSelection ?? {}) : updater,
979
- }))
980
- options.onRowSelectionChange?.(updater)
981
- },
982
- setActiveCell(updater: Updater<ActiveCellState>) {
983
- store.setState((prev) => {
984
- const previous: ActiveCellState = prev.activeCell ?? {
985
- rowIndex: 0,
986
- colIndex: 0,
987
- cellId: null,
988
- }
989
- const nextActive =
990
- typeof updater === 'function' ? updater(previous) : updater
991
- return {
992
- ...prev,
993
- activeCell: nextActive,
994
- }
995
- })
996
- options.onActiveCellChange?.(updater)
997
- },
998
- moveActiveCell(next: { rowDelta?: number; colDelta?: number }) {
999
- const rows = grid.getRowModel().rows
1000
- const columns = grid.getAllColumns()
1001
- const maxRow = Math.max(rows.length - 1, 0)
1002
- const maxCol = Math.max(columns.length - 1, 0)
1003
- const current: ActiveCellState = grid.getState().activeCell ?? {
1004
- rowIndex: 0,
1005
- colIndex: 0,
1006
- cellId: null,
1007
- }
1008
-
1009
- const rowIndex = Math.min(
1010
- Math.max(current.rowIndex + (next.rowDelta ?? 0), 0),
1011
- maxRow,
1012
- )
1013
- const colIndex = Math.min(
1014
- Math.max(current.colIndex + (next.colDelta ?? 0), 0),
1015
- maxCol,
1016
- )
1017
- const columnId = columns[colIndex]?.id ?? 'col_0'
1018
- grid.setActiveCell({
1019
- rowIndex,
1020
- colIndex,
1021
- cellId: `${rowIndex}_${columnId}`,
1022
- })
1023
- },
1024
- getAllColumns() {
1025
- // Cache hit: referentially identical columns array.
1026
- if (cachedColumnsInput === options.columns && cachedColumns.length) {
1027
- return cachedColumns
1028
- }
1029
- // Soft cache hit: consumers commonly recreate the columns array
1030
- // inline on every render (e.g. `columns={[...]}`). If the new
1031
- // array has the same length AND each entry has the same `field` /
1032
- // `id` / `header` (the visibility-affecting structure of a
1033
- // column), trust the previous build. Mutable inner fields like
1034
- // `cell` and `editorOptions` are still picked up on the next real
1035
- // render that bumps an actual data dep - they're read at cell-
1036
- // render time, not at this top-level cache.
1037
- if (
1038
- cachedColumnsInput &&
1039
- options.columns.length === cachedColumnsInput.length &&
1040
- cachedColumns.length === options.columns.length &&
1041
- options.columns.every((c, i) => {
1042
- const prev = cachedColumnsInput![i]!
1043
- return (
1044
- c.field === prev.field &&
1045
- c.id === prev.id &&
1046
- c.header === prev.header &&
1047
- c.editorType === prev.editorType
1048
- )
1049
- })
1050
- ) {
1051
- // Update the stored input reference so the strict check hits
1052
- // next time, but reuse the built column model.
1053
- cachedColumnsInput = options.columns
1054
- return cachedColumns
1055
- }
1056
-
1057
- cachedColumnsInput = options.columns
1058
- cachedHeaderGroups = []
1059
- const build = (
1060
- defs: Array<ColumnDef<TFeatures, TData>>,
1061
- depth: number,
1062
- parentId?: string,
1063
- ): Array<Column<TData>> => {
1064
- const leaves: Array<Column<TData>> = []
1065
- defs.forEach((columnDef, index) => {
1066
- const id = resolveColumnId(columnDef, parentId, depth, index)
1067
- if (columnDef.columns?.length) {
1068
- leaves.push(...build(columnDef.columns, depth + 1, id))
1069
- return
1070
- }
1071
- leaves.push({
1072
- id,
1073
- depth,
1074
- parentId,
1075
- columnDef,
1076
- getCanSort: () =>
1077
- Boolean((options._features as any).rowSortingFeature) &&
1078
- columnDef.sortable !== false,
1079
- getCanFilter: () =>
1080
- Boolean((options._features as any).columnFilteringFeature) &&
1081
- columnDef.filterable !== false,
1082
- getIsSorted: () => {
1083
- const entry = store.state.sorting?.find((s: any) => s.id === id)
1084
- if (!entry) return false
1085
- return entry.desc ? 'desc' : 'asc'
1086
- },
1087
- getToggleSortingHandler: () => () => {
1088
- const clauses: SortingState = store.state.sorting ?? []
1089
- const current = clauses.find((s: any) => s.id === id)
1090
- const nextClause: SortingState = !current
1091
- ? [...clauses, { id, desc: false }]
1092
- : current.desc
1093
- ? clauses.filter((s) => s.id !== id)
1094
- : clauses.map((s) => (s.id === id ? { ...s, desc: true } : s))
1095
- store.setState((prev) => ({ ...prev, sorting: nextClause }))
1096
- options.onSortingChange?.(nextClause)
1097
- },
1098
- })
1099
- })
1100
- return leaves
1101
- }
1102
- cachedColumns = build(options.columns, 0)
1103
- return cachedColumns
1104
- },
1105
- getHeaderGroups() {
1106
- if (cachedHeaderGroups.length) return cachedHeaderGroups
1107
- const headers = grid.getAllColumns().map((column) => {
1108
- const header: Header<TData> = {
1109
- id: column.id,
1110
- isPlaceholder: false,
1111
- colSpan: 1,
1112
- column,
1113
- getContext: () => ({ header, column, table: grid }),
1114
- }
1115
- return header
1116
- })
1117
- cachedHeaderGroups = [{ id: 'header_group_0', headers }]
1118
- return cachedHeaderGroups
1119
- },
1120
- getFooterGroups() {
1121
- return grid.getHeaderGroups()
1122
- },
1123
- getRowModel() {
1124
- const columns = grid.getAllColumns()
1125
- if (cachedBaseRowsInput !== options.data || cachedBaseRowsColumns !== columns) {
1126
- cachedBaseRowsInput = options.data
1127
- cachedBaseRowsColumns = columns
1128
- // O(1) column-id → index lookup so getCellValueByColumnId doesn't do
1129
- // a linear `findIndex` on every cell read (was O(rows × cells × cols)).
1130
- const columnIndexById = new Map<string, number>()
1131
- for (let i = 0; i < columns.length; i++) columnIndexById.set(columns[i]!.id, i)
1132
- const columnCount = columns.length
1133
-
1134
- cachedBaseRows = new Array(options.data.length)
1135
- const getRowId = options.getRowId
1136
- for (let index = 0; index < options.data.length; index++) {
1137
- const original = options.data[index]!
1138
- const id = getRowId ? getRowId(original, index) : String(index)
1139
- // `values` and `cells` are computed lazily - for a 100k-row grid
1140
- // with only ~20 visible rows we don't want to materialise every
1141
- // row's full value array or cell objects up front.
1142
- let cachedValues: Array<unknown> | null = null
1143
- let cachedCells: Array<Cell<TData>> | null = null
1144
-
1145
- function computeValues(): Array<unknown> {
1146
- const values = new Array<unknown>(columnCount)
1147
- for (let i = 0; i < columnCount; i++) {
1148
- const column = columns[i]!
1149
- if (column.columnDef.fieldFn) {
1150
- values[i] = column.columnDef.fieldFn(original)
1151
- } else if (column.columnDef.field) {
1152
- values[i] = (original as any)[column.columnDef.field]
1153
- } else {
1154
- values[i] = undefined
1155
- }
1156
- }
1157
- return values
1158
- }
1159
-
1160
- const row: Row<TData> = {
1161
- id,
1162
- index,
1163
- original,
1164
- depth: 0,
1165
- getCanExpand: () => false,
1166
- getIsExpanded: () => Boolean((store.state.expanded ?? {})[id]),
1167
- toggleExpanded: () => {
1168
- grid.setExpanded((prev) => ({ ...prev, [id]: !prev[id] }))
1169
- },
1170
- getIsSelected: () => Boolean((store.state.rowSelection ?? {})[id]),
1171
- toggleSelected: () => {
1172
- grid.setRowSelection((prev) => ({ ...prev, [id]: !prev[id] }))
1173
- },
1174
- getAllCells: () => {
1175
- if (cachedCells) return cachedCells
1176
- const built = new Array<Cell<TData>>(columnCount)
1177
- for (let i = 0; i < columnCount; i++) {
1178
- const column = columns[i]!
1179
- const colIndex = i
1180
- const cell: Cell<TData> = {
1181
- id: `${id}_${column.id}`,
1182
- row,
1183
- column,
1184
- getValue: () => {
1185
- if (!cachedValues) cachedValues = computeValues()
1186
- return cachedValues[colIndex]
1187
- },
1188
- getContext: () => ({
1189
- cell,
1190
- row,
1191
- column,
1192
- table: grid,
1193
- getValue: () => cell.getValue(),
1194
- }),
1195
- }
1196
- built[i] = cell
1197
- }
1198
- cachedCells = built
1199
- return built
1200
- },
1201
- getCellValueByColumnId: (columnId: string) => {
1202
- const idx = columnIndexById.get(columnId)
1203
- if (idx === undefined) return undefined
1204
- if (!cachedValues) cachedValues = computeValues()
1205
- return cachedValues[idx]
1206
- },
1207
- }
1208
- cachedBaseRows[index] = row
1209
- }
1210
- }
1211
-
1212
- const currentSlices = {
1213
- sorting: store.state.sorting,
1214
- columnFilters: store.state.columnFilters,
1215
- pagination: store.state.pagination,
1216
- grouping: store.state.grouping,
1217
- expanded: store.state.expanded,
1218
- rowSelection: store.state.rowSelection,
1219
- }
1220
- if (
1221
- cachedRowModel &&
1222
- cachedRowModelBaseRows === cachedBaseRows &&
1223
- cachedPipeline === options._rowModels &&
1224
- cachedSlices?.sorting === currentSlices.sorting &&
1225
- cachedSlices?.columnFilters === currentSlices.columnFilters &&
1226
- cachedSlices?.pagination === currentSlices.pagination &&
1227
- cachedSlices?.grouping === currentSlices.grouping &&
1228
- cachedSlices?.expanded === currentSlices.expanded &&
1229
- cachedSlices?.rowSelection === currentSlices.rowSelection
1230
- ) {
1231
- return cachedRowModel
1232
- }
1233
-
1234
- let rows: Array<Row<TData>> = cachedBaseRows
1235
-
1236
- const pipeline = options._rowModels ?? {}
1237
- const ordered: Array<RowModelFactory<TData> | undefined> = [
1238
- pipeline.coreRowModel,
1239
- pipeline.filteredRowModel,
1240
- pipeline.sortedRowModel,
1241
- pipeline.groupedRowModel,
1242
- pipeline.expandedRowModel,
1243
- pipeline.paginatedRowModel,
1244
- ]
1245
- ordered.forEach((fn) => {
1246
- if (fn) rows = fn({ table: grid, rows })
1247
- })
1248
- cachedPipeline = options._rowModels
1249
- cachedSlices = currentSlices
1250
- cachedRowModelBaseRows = cachedBaseRows
1251
- cachedRowModel = { rows }
1252
- return cachedRowModel
1253
- },
1254
- } as InternalGrid<TData>
1255
-
1256
- return grid
1257
- }
1258
-
1259
- export function isFunction(value: unknown): value is (...args: Array<any>) => any {
1260
- return typeof value === 'function'
1261
- }
1
+ import type { SparklineConfig } from './sparkline'
2
+ import { resolveColumnId } from './column-id'
3
+
4
+ export type RowData = Record<string, unknown>
5
+ export type Updater<T> = T | ((prev: T) => T)
6
+ export type SortingState = Array<{ id: string; desc: boolean }>
7
+ export type ColumnFilter = { id: string; value: unknown; fn?: keyof typeof filterFns }
8
+ export type ColumnFiltersState = Array<ColumnFilter>
9
+ export type PaginationState = { pageIndex: number; pageSize: number }
10
+ export type GroupingState = Array<string>
11
+ export type ExpandedState = Record<string, boolean>
12
+ export type RowSelectionState = Record<string, boolean>
13
+ export type ActiveCellState = {
14
+ rowIndex: number
15
+ colIndex: number
16
+ cellId: string | null
17
+ }
18
+ export type TableFeatures = Record<string, unknown>
19
+
20
+ export type CellData = unknown
21
+
22
+ export type HeaderContext<TData extends RowData> = {
23
+ header: Header<TData>
24
+ column: Column<TData>
25
+ table: SvGrid<TData>
26
+ }
27
+
28
+ export type CellContext<TData extends RowData> = {
29
+ cell: Cell<TData>
30
+ row: Row<TData>
31
+ column: Column<TData>
32
+ table: SvGrid<TData>
33
+ getValue: () => unknown
34
+ }
35
+
36
+ /** Params passed to a column's `colSpan(...)` / `rowSpan(...)` callbacks. */
37
+ export type CellSpanParams<TData extends RowData = RowData> = {
38
+ /** The row's underlying data object. */
39
+ data: TData
40
+ /** Display-row index in the current (filtered/sorted) row set. */
41
+ rowIndex: number
42
+ /** The column's id. */
43
+ columnId: string
44
+ /** The cell's base value for this column. */
45
+ value: unknown
46
+ }
47
+
48
+ /** The raw option list a column's `editorOptions` can supply. */
49
+ export type EditorOptionSource = ReadonlyArray<
50
+ string | number | { value: string | number; label?: string; color?: string }
51
+ >
52
+
53
+ /** Params passed to a column's `valueParser(...)` on edit commit. */
54
+ export type ValueParserParams<TData extends RowData = RowData> = {
55
+ /** The value after built-in per-`editorType` coercion. */
56
+ newValue: unknown
57
+ /** The cell's previous value. */
58
+ oldValue: unknown
59
+ /** The raw string the editor produced (pre-coercion). */
60
+ rawInput: string
61
+ /** The row's underlying data object. */
62
+ data: TData
63
+ /** The column's id. */
64
+ columnId: string
65
+ }
66
+
67
+ /**
68
+ * Context passed to a custom `cellEditor` snippet/component. Three write
69
+ * helpers cover the lifecycle:
70
+ *
71
+ * - `update(next)` - stage `next` as the draft, keep the editor open.
72
+ * Use this for live-preview controls (sliders,
73
+ * color pickers) so the user can keep adjusting.
74
+ * - `commit(next?)` - write the value AND close the editor. The
75
+ * argument is optional; when omitted, the most
76
+ * recently `update()`d value is saved. Use this
77
+ * for "done" gestures (Enter, picking an option).
78
+ * - `cancel()` - discard the draft and close the editor.
79
+ */
80
+ export type EditorContext<TData extends RowData> = CellContext<TData> & {
81
+ value: unknown
82
+ update: (next: unknown) => void
83
+ commit: (next?: unknown) => void
84
+ cancel: () => void
85
+ }
86
+
87
+ export type CellFormatConfig =
88
+ | {
89
+ type: 'number'
90
+ locales?: string | Array<string>
91
+ options?: Intl.NumberFormatOptions
92
+ }
93
+ | {
94
+ type: 'currency'
95
+ /** ISO 4217 (default USD) */
96
+ currency?: string
97
+ locales?: string | Array<string>
98
+ options?: Omit<Intl.NumberFormatOptions, 'style' | 'currency'>
99
+ }
100
+ | {
101
+ type: 'percent'
102
+ locales?: string | Array<string>
103
+ options?: Omit<Intl.NumberFormatOptions, 'style'>
104
+ /**
105
+ * If true, numeric cell values are 0–100 (e.g. 42 → 42%) instead of Intl’s 0–1 fraction (0.42 → 42%).
106
+ * Default false.
107
+ */
108
+ valueIsPercentPoints?: boolean
109
+ }
110
+ | {
111
+ type: 'date' | 'datetime'
112
+ locales?: string | Array<string>
113
+ /**
114
+ * Shortcut patterns merged with `options`:
115
+ * `'d'` short numeric date, `'D'` long date, `'y-m-d'` yyyy/mm/dd-style,
116
+ * `'short'`|`'medium'`|`'long'` use dateStyle/timeStyle presets.
117
+ */
118
+ pattern?: string
119
+ options?: Intl.DateTimeFormatOptions
120
+ }
121
+
122
+ export type CellFormatter<TData extends RowData> = (context: {
123
+ value: unknown
124
+ row: Row<TData>
125
+ column: Column<TData>
126
+ table: SvGrid<TData>
127
+ }) => string
128
+
129
+ export type ColumnDefTemplate<TContext> = string | ((context: TContext) => unknown)
130
+
131
+ /**
132
+ * How a column's value is aggregated for a group row when `columnGrouping`
133
+ * is active. Built-in reducers cover the common cases; pass a function for
134
+ * anything custom (weighted average, median, percentile, distinct count).
135
+ * The function receives the finite numeric values AND the raw leaf rows.
136
+ */
137
+ export type GroupAggregator<TData = any> =
138
+ | 'sum'
139
+ | 'avg'
140
+ | 'min'
141
+ | 'max'
142
+ | 'count'
143
+ | 'countDistinct'
144
+ | 'extent'
145
+ | 'first'
146
+ | ((values: number[], rows: Array<TData>) => unknown)
147
+
148
+ /** Apply a group aggregator over a bucket's leaf rows for one column. */
149
+ export function applyGroupAggregate<TData extends RowData>(
150
+ agg: GroupAggregator<TData>,
151
+ columnId: string,
152
+ rows: ReadonlyArray<Row<TData>>,
153
+ ): unknown {
154
+ const raw = rows.map((r) => r.getCellValueByColumnId(columnId))
155
+ if (typeof agg === 'function') {
156
+ const nums = raw.map((v) => Number(v)).filter((n) => Number.isFinite(n))
157
+ return agg(nums, rows.map((r) => r.original))
158
+ }
159
+ if (agg === 'count') return rows.length
160
+ if (agg === 'countDistinct') return new Set(raw.map((v) => String(v ?? ''))).size
161
+ if (agg === 'first') return raw[0]
162
+ const nums = raw.map((v) => Number(v)).filter((n) => Number.isFinite(n))
163
+ if (!nums.length) return undefined
164
+ switch (agg) {
165
+ case 'sum':
166
+ return nums.reduce((a, b) => a + b, 0)
167
+ case 'avg':
168
+ return nums.reduce((a, b) => a + b, 0) / nums.length
169
+ case 'min':
170
+ return Math.min(...nums)
171
+ case 'max':
172
+ return Math.max(...nums)
173
+ case 'extent':
174
+ return `${Math.min(...nums)} – ${Math.max(...nums)}`
175
+ default:
176
+ return undefined
177
+ }
178
+ }
179
+
180
+ /**
181
+ * A column definition.
182
+ *
183
+ * `TFeatures` is a phantom parameter - it is threaded through nested
184
+ * `columns` groups but no member depends on it, so `{}`, `TableFeatures` and
185
+ * `typeof features` are all interchangeable here. It is deliberately left
186
+ * WITHOUT a default: `ColumnDef<Row>` would otherwise bind `Row` to this slot
187
+ * and silently type your data as `RowData`, losing every field-name check.
188
+ * Prefer {@link GridColumns} / {@link GridColumnDef} for the common case.
189
+ */
190
+ export type ColumnDef<TFeatures extends TableFeatures, TData extends RowData> = {
191
+ id?: string
192
+ field?: keyof TData & string
193
+ fieldFn?: (row: TData) => unknown
194
+ header?: ColumnDefTemplate<HeaderContext<TData>>
195
+ footer?: ColumnDefTemplate<HeaderContext<TData>>
196
+ cell?: ColumnDefTemplate<CellContext<TData>>
197
+ columns?: Array<ColumnDef<TFeatures, TData>>
198
+ /**
199
+ * Declarative cell spanning (merged cells). Return how many COLUMNS this
200
+ * cell spans to the right (1 = no span). Value-driven, AG-Grid-style. Feed
201
+ * `spansToMerges(rows, columns)` into `spreadsheetLayout` to apply - it uses
202
+ * the same real `colspan`/`rowspan` merge engine (no separate code path).
203
+ */
204
+ colSpan?: (params: CellSpanParams<TData>) => number
205
+ /**
206
+ * Declarative cell spanning (merged cells). Return how many ROWS this cell
207
+ * spans downward (1 = no span). See `colSpan` for how to apply.
208
+ */
209
+ rowSpan?: (params: CellSpanParams<TData>) => number
210
+ /**
211
+ * High-level data type for the column. A convenience that resolves to the
212
+ * right `editorType`, alignment, date `format`, and filter operators without
213
+ * setting each by hand:
214
+ * 'text' → text editor, left-aligned
215
+ * 'number' → number editor, right-aligned, numeric filter operators
216
+ * 'boolean' → checkbox editor, centered
217
+ * 'date' → date editor (Date values), right-aligned, `{ type: 'date' }` format
218
+ * 'dateString' → date editor for ISO date STRINGS (e.g. '2026-06-27')
219
+ * Anything you set explicitly (`editorType`, `align`, `format`) still wins -
220
+ * `cellDataType` only fills the gaps. Grid-level `inferColumnTypes` infers
221
+ * this from the first data row for columns that declare neither.
222
+ */
223
+ cellDataType?: 'text' | 'number' | 'boolean' | 'date' | 'dateString'
224
+ /**
225
+ * Hide this column when the grid's `responsive` mode is on and the grid is
226
+ * narrower than this many pixels - drop low-priority columns on small
227
+ * screens. No effect unless the grid has `responsive` set.
228
+ */
229
+ hideBelow?: number
230
+ /**
231
+ * For a column INSIDE a collapsible column group: `'open'` shows this column
232
+ * only while the group is expanded, `'closed'` only while collapsed. Omit to
233
+ * always show it. Setting it on any direct child gives the parent group a
234
+ * collapse toggle. Pair with `openByDefault` on the group.
235
+ */
236
+ columnGroupShow?: 'open' | 'closed'
237
+ /**
238
+ * For a GROUP column (one with `columns: [...]`): start the group expanded.
239
+ * Defaults to `false` (collapsed), matching AG Grid - so only the always-on
240
+ * and `columnGroupShow: 'closed'` children show until the user expands it.
241
+ */
242
+ openByDefault?: boolean
243
+ editorType?:
244
+ | 'text'
245
+ | 'number'
246
+ | 'date' // rich SvCalendar popover (opt out with 'date-native')
247
+ | 'datetime' // rich SvDateTimePicker (opt out with 'datetime-native')
248
+ | 'time' // rich SvTimePicker dial (opt out with 'time-native')
249
+ | 'date-native' // plain <input type="date">
250
+ | 'datetime-native' // plain <input type="datetime-local">
251
+ | 'time-native' // plain <input type="time"> - HH:MM or HH:MM:SS
252
+ | 'password' // native <input type="password"> with masked rendering
253
+ | 'checkbox'
254
+ | 'list'
255
+ | 'chips'
256
+ | 'select' // custom dropdown - single value, no typeahead
257
+ | 'rich-select' // custom dropdown with a typeahead search input
258
+ | 'autocomplete' // free-text input with a live-filtered suggestion list (accepts any value)
259
+ | 'textarea' // multi-line editor; Tab or Ctrl+Enter commits, plain Enter inserts a newline
260
+ | 'color' // native <input type="color"> swatch
261
+ | 'rating' // 5-star rating control
262
+ // Any other string names a CUSTOM editor registered via `registerCellEditor`
263
+ // (or `registerBuiltinEditors`). `(string & {})` keeps the literals above
264
+ // autocompleting while allowing arbitrary custom type names.
265
+ | (string & {})
266
+ /**
267
+ * Custom in-cell editor. Receives the cell context PLUS a `commit(value)`
268
+ * and `cancel()` helper. Use when none of the built-in `editorType`s fit;
269
+ * the snippet's outer element is mounted inside the editing cell and
270
+ * inherits keyboard handling (Esc cancels, Enter commits unless your
271
+ * snippet preventDefaults it).
272
+ *
273
+ * Coexists with `editorType`: when both are set, `cellEditor` wins and
274
+ * `editorType` is treated as a hint for parsing the saved value.
275
+ */
276
+ cellEditor?: ColumnDefTemplate<EditorContext<TData>>
277
+ /**
278
+ * Per-column tooltip. String shows as a native `title=`; `(ctx) => string`
279
+ * runs per cell so the tooltip can reflect the value. Returning an empty
280
+ * string skips the tooltip.
281
+ */
282
+ tooltip?: string | ((ctx: CellContext<TData>) => string | null | undefined)
283
+ /**
284
+ * Declarative per-cell validation (Handsontable-style). Runs for EVERY
285
+ * rendered cell - including values already present in `data` on load, not
286
+ * just on edit - so bad data is flagged immediately. Invalid cells get the
287
+ * `sv-grid-cell-invalid` class (red highlight) and the returned message as
288
+ * their tooltip.
289
+ *
290
+ * Return value:
291
+ * - `null` / `undefined` / `true` → valid (no highlight)
292
+ * - `false` → invalid, no message
293
+ * - a non-empty `string` → invalid, string is the tooltip
294
+ *
295
+ * The value keeps rendering as-is (the grid does NOT roll it back); pair
296
+ * with `onCellValueChange` if you also want to reject the commit.
297
+ */
298
+ validate?: (params: {
299
+ value: unknown
300
+ row: TData
301
+ rowIndex: number
302
+ column: Column<TData>
303
+ }) => string | boolean | null | undefined
304
+ /**
305
+ * Gate editing per column or per cell.
306
+ *
307
+ * - `true` (or omitted): the column is fully editable.
308
+ * - `false`: the column is read-only - double-click, type-to-edit,
309
+ * fill-handle drag, Delete, and clipboard paste all skip it.
310
+ * - `(ctx) => boolean`: evaluated for each cell, so you can lock
311
+ * individual rows (e.g. by role, status, ownership). Returning
312
+ * `false` opts the cell out of every editing path, identical to
313
+ * setting `editable: false` on the whole column for that row.
314
+ *
315
+ * The grid-wide `enableInlineEditing` prop still wins when set to
316
+ * `false`.
317
+ */
318
+ editable?: boolean | ((context: CellContext<TData>) => boolean)
319
+ /**
320
+ * Transform the committed edit value before it is written to the row.
321
+ * Runs after the built-in per-`editorType` coercion, so `newValue` is
322
+ * already type-parsed; return the final value to store (e.g. round a
323
+ * number, uppercase a code, look up an id). AG-Grid-style `valueParser`.
324
+ */
325
+ valueParser?: (params: ValueParserParams<TData>) => unknown
326
+ /**
327
+ * Briefly flash / highlight this column's cell when its value changes
328
+ * (streaming feeds, edits, server pushes). `true` uses the default flash;
329
+ * pass `{ className }` to apply your own animation class instead.
330
+ */
331
+ cellFlash?: boolean | { className?: string }
332
+ /**
333
+ * When `false`, this column never shows a sort indicator and clicking
334
+ * its header is a no-op - `api.setSort(thisColumn, ...)` is also
335
+ * ignored. Defaults to `true` (the column participates in sorting as
336
+ * long as `rowSortingFeature` is registered).
337
+ */
338
+ sortable?: boolean
339
+ /**
340
+ * When `false`, this column never shows a filter funnel / menu and
341
+ * `api.setFilter(thisColumn, ...)` is ignored. Defaults to `true` (the
342
+ * column is filterable as long as `columnFilteringFeature` is
343
+ * registered).
344
+ */
345
+ filterable?: boolean
346
+ /**
347
+ * Options for `editorType: 'list' | 'chips'`. Either bare values (the
348
+ * string is both value and label) or `{ value, label }` objects.
349
+ * For `chips` this is optional - when omitted, the chips editor becomes
350
+ * free-form (user types and presses Enter to commit a chip).
351
+ *
352
+ * Pass a function `(row) => options` for row-dependent (cascading)
353
+ * options - e.g. City options that depend on Country in the same row.
354
+ *
355
+ * Either form may return a **Promise**, for options that come from the
356
+ * server. While it resolves, the editor shows a loading state and the cell
357
+ * renders its raw value.
358
+ *
359
+ * Results are cached so reopening an editor does not refetch: a static source
360
+ * per column, a per-row source per row AND per that row's data - so a cascade
361
+ * reloads by itself when the cell it depends on is edited. Call
362
+ * `api.refreshEditorOptions(columnId?)` when the list changes server-side.
363
+ */
364
+ editorOptions?:
365
+ | EditorOptionSource
366
+ | Promise<EditorOptionSource>
367
+ | ((row: TData) => EditorOptionSource | Promise<EditorOptionSource>)
368
+ /** When true, list/chips allow multiple selections. Cell value becomes an array. */
369
+ editorMultiple?: boolean
370
+ /** Separator used when joining array values for the readonly cell display. Defaults to ', '. */
371
+ editorSeparator?: string
372
+ format?: CellFormatConfig
373
+ formatter?: CellFormatter<TData>
374
+ /**
375
+ * Aggregate this column's values into the group row when grouping is
376
+ * active. `'sum' | 'avg' | 'min' | 'max' | 'count' | 'countDistinct' |
377
+ * 'extent' | 'first'`, or a custom `(values, rows) => unknown`. The result
378
+ * is formatted with this column's `format` and shown in the group header.
379
+ */
380
+ aggregate?: GroupAggregator<TData>
381
+ /**
382
+ * Render the cell as an in-cell sparkline chart. The cell value should be
383
+ * an array of numbers (or a comma/space separated string). Mutually
384
+ * exclusive with a custom `cell` renderer (a `cell` wins if both are set).
385
+ *
386
+ * { sparkline: { type: 'line' } } // default line
387
+ * { sparkline: { type: 'bar', color: '#16a34a' } }
388
+ * { sparkline: { type: 'winloss' } } // sign-only up/down
389
+ *
390
+ * See `SparklineConfig` for the full option set (type, color,
391
+ * negativeColor, width, height, fixed min/max).
392
+ */
393
+ sparkline?: SparklineConfig
394
+ /** Initial column width in pixels. Falls back to the grid's `columnWidth` prop. */
395
+ width?: number
396
+ /**
397
+ * Initial visibility. Set `false` to start the column hidden while still
398
+ * listing it in the Choose Columns UI for the user to re-enable. Applied
399
+ * once at mount; after that `api.setColumnVisible` / user toggles win.
400
+ * On a group column, `false` hides the whole group's leaf columns.
401
+ */
402
+ visible?: boolean
403
+ /**
404
+ * Horizontal alignment for header and body cells. When omitted, the
405
+ * default is inferred from `editorType`:
406
+ * - `'number' | 'date' | 'datetime'` → `'right'`
407
+ * - `'checkbox'` → `'center'`
408
+ * - everything else → `'left'`
409
+ */
410
+ align?: 'left' | 'center' | 'right'
411
+ /**
412
+ * Per-cell conditional CSS. Two shapes:
413
+ *
414
+ * - **String** (or array of strings): class name(s) added to the
415
+ * cell's `<td>` for every row in this column.
416
+ * - **Function**: invoked per cell with the same `CellContext` shape
417
+ * the `cell` renderer receives. Return a string, an array of
418
+ * strings, or an object mapping class names to booleans.
419
+ *
420
+ * Use it for status tinting, conditional bold, "negative number"
421
+ * coloring - anything that's a function of the row's value. Cells
422
+ * still receive their format / cell renderer; the class just
423
+ * augments the rendered `<td>`.
424
+ */
425
+ cellClass?:
426
+ | string
427
+ | ReadonlyArray<string>
428
+ | ((ctx: CellContext<TData>) => string | ReadonlyArray<string> | Record<string, boolean> | undefined | null)
429
+ }
430
+
431
+ /**
432
+ * A column definition keyed only by your row type - the ergonomic form of
433
+ * {@link ColumnDef}, whose first parameter is a phantom feature bag that is
434
+ * almost always `{}`.
435
+ *
436
+ * ```ts
437
+ * const columns: GridColumns<Person> = [{ field: 'firstName', header: 'Name' }]
438
+ * ```
439
+ *
440
+ * Interchangeable with `ColumnDef<{}, TData>` and `ColumnDef<typeof features,
441
+ * TData>` in both directions, so it mixes freely with existing code.
442
+ */
443
+ export type GridColumnDef<TData extends RowData = RowData> = ColumnDef<TableFeatures, TData>
444
+
445
+ /** An array of {@link GridColumnDef} - what you pass to `<SvGrid columns={...}>`. */
446
+ export type GridColumns<TData extends RowData = RowData> = Array<GridColumnDef<TData>>
447
+
448
+ export type Column<TData extends RowData> = {
449
+ id: string
450
+ columnDef: ColumnDef<any, TData>
451
+ depth: number
452
+ parentId?: string
453
+ getCanSort: () => boolean
454
+ getCanFilter: () => boolean
455
+ getIsSorted: () => false | 'asc' | 'desc'
456
+ getToggleSortingHandler: () => () => void
457
+ }
458
+
459
+ export type Header<TData extends RowData> = {
460
+ id: string
461
+ isPlaceholder: boolean
462
+ colSpan: number
463
+ column: Column<TData>
464
+ getContext: () => HeaderContext<TData>
465
+ }
466
+
467
+ export type HeaderGroup<TData extends RowData> = {
468
+ id: string
469
+ headers: Array<Header<TData>>
470
+ }
471
+
472
+ export type Cell<TData extends RowData> = {
473
+ id: string
474
+ row: Row<TData>
475
+ column: Column<TData>
476
+ getValue: () => unknown
477
+ getContext: () => CellContext<TData>
478
+ }
479
+
480
+ export type Row<TData extends RowData> = {
481
+ id: string
482
+ index: number
483
+ original: TData
484
+ depth: number
485
+ subRows?: Array<Row<TData>>
486
+ /** Total leaf (data) rows under this group row. Undefined for data rows. */
487
+ leafCount?: number
488
+ getCanExpand: () => boolean
489
+ getIsExpanded: () => boolean
490
+ toggleExpanded: () => void
491
+ getIsSelected: () => boolean
492
+ toggleSelected: () => void
493
+ getAllCells: () => Array<Cell<TData>>
494
+ getCellValueByColumnId: (columnId: string) => unknown
495
+ }
496
+
497
+ export type RowModel<TData extends RowData> = {
498
+ rows: Array<Row<TData>>
499
+ }
500
+
501
+ export type Store<T> = {
502
+ readonly state: T
503
+ setState: (updater: (prev: T) => T) => void
504
+ subscribe: (listener: () => void) => () => void
505
+ }
506
+
507
+ function createStore<T>(initial: T): Store<T> {
508
+ let value = initial
509
+ const listeners = new Set<() => void>()
510
+ return {
511
+ get state() {
512
+ return value
513
+ },
514
+ setState(updater) {
515
+ value = updater(value)
516
+ listeners.forEach((listener) => listener())
517
+ },
518
+ subscribe(listener) {
519
+ listeners.add(listener)
520
+ return () => listeners.delete(listener)
521
+ },
522
+ }
523
+ }
524
+
525
+ export const rowSortingFeature = { key: 'rowSortingFeature' }
526
+ export const columnFilteringFeature = { key: 'columnFilteringFeature' }
527
+ export const rowPaginationFeature = { key: 'rowPaginationFeature' }
528
+ export const columnGroupingFeature = { key: 'columnGroupingFeature' }
529
+ export const rowSelectionFeature = { key: 'rowSelectionFeature' }
530
+ export const rowExpandingFeature = { key: 'rowExpandingFeature' }
531
+
532
+ export function tableFeatures<T extends TableFeatures>(features: T): T {
533
+ return features
534
+ }
535
+
536
+ export const sortFns = {
537
+ auto: (a: unknown, b: unknown) => String(a).localeCompare(String(b)),
538
+ number: (a: unknown, b: unknown) => Number(a ?? 0) - Number(b ?? 0),
539
+ date: (a: unknown, b: unknown) => {
540
+ const aa = new Date(a as any).getTime()
541
+ const bb = new Date(b as any).getTime()
542
+ return aa - bb
543
+ },
544
+ }
545
+
546
+ export const filterFns = {
547
+ includesString: (value: unknown, query: string) =>
548
+ String(value).toLowerCase().includes(query.toLowerCase()),
549
+ equals: (value: unknown, query: unknown) => value === query,
550
+ }
551
+
552
+ export type RowModelFactory<TData extends RowData> = (args: {
553
+ table: SvGrid<TData>
554
+ rows: Array<Row<TData>>
555
+ }) => Array<Row<TData>>
556
+
557
+ export function createCoreRowModel<TData extends RowData>(): RowModelFactory<TData> {
558
+ return ({ rows }) => rows
559
+ }
560
+ export function createFilteredRowModel<TData extends RowData>(): RowModelFactory<TData> {
561
+ return ({ table, rows }) => {
562
+ const filters: ColumnFiltersState = table.getState().columnFilters ?? []
563
+ if (!filters.length) return rows
564
+ return rows.filter((row) => {
565
+ return filters.every((filter) => {
566
+ const cellValue = row
567
+ .getAllCells()
568
+ .find((cell) => cell.column.id === filter.id)
569
+ ?.getValue()
570
+ const filterFn = filter.fn ? filterFns[filter.fn] : filterFns.includesString
571
+ return filterFn(cellValue, filter.value as any)
572
+ })
573
+ })
574
+ }
575
+ }
576
+ export function createPaginatedRowModel<TData extends RowData>(): RowModelFactory<TData> {
577
+ return ({ table, rows }) => {
578
+ const pagination = table.getState().pagination ?? { pageIndex: 0, pageSize: rows.length || 10 }
579
+ const start = pagination.pageIndex * pagination.pageSize
580
+ return rows.slice(start, start + pagination.pageSize)
581
+ }
582
+ }
583
+ export function createGroupedRowModel<TData extends RowData>(): RowModelFactory<TData> {
584
+ return ({ table, rows }) => {
585
+ const grouping: GroupingState = table.getState().grouping ?? []
586
+ if (!grouping.length) return rows
587
+ const columns = table.getAllColumns()
588
+
589
+ // Recursively bucket rows by each grouping column in turn. At every level a
590
+ // group row is built that stands in for its children - a non-group column
591
+ // resolves to the value shared by every leaf row, or to undefined when the
592
+ // leaves disagree.
593
+ function buildGroups(
594
+ input: Array<Row<TData>>,
595
+ levelIndex: number,
596
+ depth: number,
597
+ idPrefix: string,
598
+ ): Array<Row<TData>> {
599
+ if (levelIndex >= grouping.length) {
600
+ // Leaves: actual data rows, with their nesting depth recorded.
601
+ return input.map((row) => ({ ...row, depth }))
602
+ }
603
+ const groupKey = grouping[levelIndex]
604
+ if (!groupKey) return input
605
+
606
+ const buckets = new Map<string, Array<Row<TData>>>()
607
+ for (const row of input) {
608
+ const value = row.getCellValueByColumnId(groupKey)
609
+ const key = String(value ?? '')
610
+ const list = buckets.get(key) ?? []
611
+ list.push(row)
612
+ buckets.set(key, list)
613
+ }
614
+
615
+ const groupRows: Array<Row<TData>> = []
616
+ let index = 0
617
+ buckets.forEach((children, key) => {
618
+ const id = `${idPrefix}_${groupKey}_${key}`
619
+ const subRows = buildGroups(children, levelIndex + 1, depth + 1, id)
620
+ const isDeepest = levelIndex + 1 >= grouping.length
621
+ const leafCount = isDeepest
622
+ ? subRows.length
623
+ : subRows.reduce((sum, sub) => sum + (sub.leafCount ?? 0), 0)
624
+
625
+ const resolveColumnValue = (columnId: string): unknown => {
626
+ if (columnId === groupKey) return key
627
+ let resolved: unknown
628
+ let hasResolved = false
629
+ for (const child of children) {
630
+ const childValue = child.getCellValueByColumnId(columnId)
631
+ if (!hasResolved) {
632
+ resolved = childValue
633
+ hasResolved = true
634
+ } else if (childValue !== resolved) {
635
+ return undefined
636
+ }
637
+ }
638
+ return resolved
639
+ }
640
+
641
+ const groupOriginal: Record<string, unknown> = {}
642
+ columns.forEach((column) => {
643
+ const field = column.columnDef.field
644
+ if (!field) return
645
+ const agg = column.columnDef.aggregate
646
+ groupOriginal[field] = agg
647
+ ? applyGroupAggregate(agg, column.id, children)
648
+ : resolveColumnValue(column.id)
649
+ })
650
+
651
+ const groupRow: Row<TData> = {
652
+ id,
653
+ index: index++,
654
+ original: groupOriginal as TData,
655
+ depth,
656
+ subRows,
657
+ leafCount,
658
+ getCanExpand: () => true,
659
+ getIsExpanded: () => Boolean((table.getState().expanded ?? {})[id]),
660
+ toggleExpanded: () => {
661
+ table.setExpanded((prev) => ({ ...prev, [id]: !prev[id] }))
662
+ },
663
+ getIsSelected: () => Boolean((table.getState().rowSelection ?? {})[id]),
664
+ toggleSelected: () => {
665
+ table.setRowSelection((prev) => ({ ...prev, [id]: !prev[id] }))
666
+ },
667
+ getAllCells: () => [],
668
+ // Prefer the precomputed group value (which carries aggregates)
669
+ // and fall back to the shared-value resolver for columns without
670
+ // a field.
671
+ getCellValueByColumnId: (columnId: string) => {
672
+ const col = columns.find((c) => c.id === columnId)
673
+ const field = col?.columnDef.field
674
+ if (field && field in groupOriginal) return groupOriginal[field]
675
+ return resolveColumnValue(columnId)
676
+ },
677
+ }
678
+ groupRows.push(groupRow)
679
+ })
680
+ return groupRows
681
+ }
682
+
683
+ return buildGroups(rows, 0, 0, 'group')
684
+ }
685
+ }
686
+ export type TreeRowModelOptions = {
687
+ /** Field holding each row's parent id. Rows with no parent are roots. */
688
+ parentField: string
689
+ /** Field holding the row's own id. Defaults to `'id'`. */
690
+ idField?: string
691
+ }
692
+
693
+ /**
694
+ * Client-side tree data: nest the grid's own flat rows into a parent/child
695
+ * hierarchy that `createExpandedRowModel` then walks.
696
+ *
697
+ * This works on the rows the grid already built rather than on raw data, so
698
+ * tree rows keep their cells, editing, selection and formatting - they are real
699
+ * data rows that happen to have children, not synthetic banners like grouping's.
700
+ * That is also why the model is parent-id based: nested source arrays never
701
+ * become rows (the grid only builds rows for `data`), so nested input is
702
+ * flattened first with {@link flattenTreeData}. One code path, no duplicated
703
+ * row construction.
704
+ *
705
+ * Rows are tagged `__treeRow` so `isGroupRow` does not mistake an expandable
706
+ * data row for a full-width group banner.
707
+ */
708
+ export function createTreeRowModel<TData extends RowData>(
709
+ options: TreeRowModelOptions,
710
+ ): RowModelFactory<TData> {
711
+ const { parentField, idField = 'id' } = options
712
+ return ({ table, rows }) => {
713
+ if (!rows.length) return rows
714
+ const keyOf = (row: Row<TData>) => (row.original as any)?.[idField]
715
+ const parentOf = (row: Row<TData>) => (row.original as any)?.[parentField]
716
+
717
+ const present = new Set<unknown>()
718
+ for (const row of rows) present.add(keyOf(row))
719
+
720
+ const childrenByParent = new Map<unknown, Array<Row<TData>>>()
721
+ const roots: Array<Row<TData>> = []
722
+ for (const row of rows) {
723
+ const parent = parentOf(row)
724
+ // A row whose parent is absent (filtered out, or never existed) becomes a
725
+ // root rather than disappearing - silently dropping rows is worse than a
726
+ // shallower tree. Self-parenting is treated the same way.
727
+ if (parent == null || parent === keyOf(row) || !present.has(parent)) {
728
+ roots.push(row)
729
+ continue
730
+ }
731
+ const list = childrenByParent.get(parent) ?? []
732
+ list.push(row)
733
+ childrenByParent.set(parent, list)
734
+ }
735
+
736
+ // Guards a cycle in the parent chain from recursing forever.
737
+ const seen = new Set<unknown>()
738
+ const build = (row: Row<TData>, depth: number): Row<TData> => {
739
+ const key = keyOf(row)
740
+ const id = row.id
741
+ if (seen.has(key)) {
742
+ return { ...row, depth, subRows: [], getCanExpand: () => false } as Row<TData>
743
+ }
744
+ seen.add(key)
745
+ const subRows = (childrenByParent.get(key) ?? []).map((child) => build(child, depth + 1))
746
+ return {
747
+ ...row,
748
+ depth,
749
+ subRows,
750
+ leafCount: subRows.reduce((n, sub) => n + 1 + (sub.leafCount ?? 0), 0),
751
+ __treeRow: true,
752
+ getCanExpand: () => subRows.length > 0,
753
+ getIsExpanded: () => Boolean((table.getState().expanded ?? {})[id]),
754
+ toggleExpanded: () => {
755
+ table.setExpanded((prev) => ({ ...prev, [id]: !prev[id] }))
756
+ },
757
+ } as Row<TData>
758
+ }
759
+
760
+ return roots.map((root) => build(root, 0))
761
+ }
762
+ }
763
+
764
+ export type FlattenTreeOptions = {
765
+ /** Field holding an array of child objects. */
766
+ childrenField: string
767
+ /** Field holding each object's id. Defaults to `'id'`. */
768
+ idField?: string
769
+ /** Field to WRITE the resolved parent id onto. Defaults to `'__parentId'`. */
770
+ parentField?: string
771
+ }
772
+
773
+ /**
774
+ * Flatten nested tree data into the flat parent-id shape `createTreeRowModel`
775
+ * consumes, stamping each child with its parent's id.
776
+ *
777
+ * Children are emitted directly after their parent so the natural order already
778
+ * matches the rendered tree. The `childrenField` array is left on the objects
779
+ * (harmless, and callers often still want it); only the parent link is added.
780
+ */
781
+ export function flattenTreeData<T extends RowData>(
782
+ data: ReadonlyArray<T>,
783
+ options: FlattenTreeOptions,
784
+ ): T[] {
785
+ const { childrenField, idField = 'id', parentField = '__parentId' } = options
786
+ const out: T[] = []
787
+ const walk = (nodes: ReadonlyArray<T>, parentId: unknown) => {
788
+ for (const node of nodes) {
789
+ const flat = { ...node, [parentField]: parentId } as T
790
+ out.push(flat)
791
+ const kids = (node as any)[childrenField]
792
+ if (Array.isArray(kids) && kids.length) walk(kids as ReadonlyArray<T>, (node as any)[idField])
793
+ }
794
+ }
795
+ walk(data, null)
796
+ return out
797
+ }
798
+
799
+ export function createExpandedRowModel<TData extends RowData>(): RowModelFactory<TData> {
800
+ return ({ table, rows }) => {
801
+ const expanded: ExpandedState = table.getState().expanded ?? {}
802
+ const flattened: Array<Row<TData>> = []
803
+ const visit = (row: Row<TData>) => {
804
+ flattened.push(row)
805
+ if (row.subRows?.length && expanded[row.id]) {
806
+ for (const sub of row.subRows) visit(sub)
807
+ }
808
+ }
809
+ for (const row of rows) visit(row)
810
+ return flattened
811
+ }
812
+ }
813
+ export function createSortedRowModel<TData extends RowData>(
814
+ localSortFns: typeof sortFns = sortFns,
815
+ ): RowModelFactory<TData> {
816
+ return ({ table, rows }) => {
817
+ const sorting = table.getState().sorting ?? []
818
+ if (!sorting.length) return rows
819
+
820
+ const sorted = [...rows].sort((a, b) => {
821
+ for (const clause of sorting) {
822
+ const column = table.getAllColumns().find((col) => col.id === clause.id)
823
+ if (!column) continue
824
+ const editorType = column.columnDef.editorType
825
+ const comparator =
826
+ editorType === 'number'
827
+ ? localSortFns.number
828
+ : editorType === 'date' || editorType === 'datetime'
829
+ ? localSortFns.date
830
+ : localSortFns.auto
831
+ const result = comparator(
832
+ a.getCellValueByColumnId(column.id),
833
+ b.getCellValueByColumnId(column.id),
834
+ )
835
+ if (result !== 0) return clause.desc ? -result : result
836
+ }
837
+ return 0
838
+ })
839
+ return sorted
840
+ }
841
+ }
842
+
843
+ export type SvGridOptions<TFeatures extends TableFeatures, TData extends RowData> = {
844
+ _features: TFeatures
845
+ _rowModels?: {
846
+ coreRowModel?: RowModelFactory<TData>
847
+ filteredRowModel?: RowModelFactory<TData>
848
+ sortedRowModel?: RowModelFactory<TData>
849
+ paginatedRowModel?: RowModelFactory<TData>
850
+ groupedRowModel?: RowModelFactory<TData>
851
+ expandedRowModel?: RowModelFactory<TData>
852
+ }
853
+ columns: Array<ColumnDef<TFeatures, TData>>
854
+ data: ReadonlyArray<TData>
855
+ /**
856
+ * Optional row-id resolver. When set, the value it returns becomes
857
+ * `row.id` (and therefore the selection / expansion / edit key). When
858
+ * omitted, ids fall back to the row's array index as a string. Use a
859
+ * stable id (database PK, UUID, etc.) so selection survives reorders.
860
+ */
861
+ getRowId?: (row: TData, index: number) => string
862
+ state?: Partial<Record<string, any>>
863
+ onSortingChange?: (updater: Updater<SortingState>) => void
864
+ onColumnFiltersChange?: (updater: Updater<ColumnFiltersState>) => void
865
+ onPaginationChange?: (updater: Updater<PaginationState>) => void
866
+ onGroupingChange?: (updater: Updater<GroupingState>) => void
867
+ onExpandedChange?: (updater: Updater<ExpandedState>) => void
868
+ onRowSelectionChange?: (updater: Updater<RowSelectionState>) => void
869
+ onActiveCellChange?: (updater: Updater<ActiveCellState>) => void
870
+ }
871
+
872
+ export type SvGrid<TData extends RowData> = {
873
+ store: Store<Record<string, any>>
874
+ optionsStore: Store<Record<string, any>>
875
+ state: Record<string, any>
876
+ getState: () => Record<string, any>
877
+ setOptions: (updater: Updater<Record<string, any>>) => void
878
+ setColumnFilters: (updater: Updater<ColumnFiltersState>) => void
879
+ setPagination: (updater: Updater<PaginationState>) => void
880
+ setGrouping: (updater: Updater<GroupingState>) => void
881
+ setExpanded: (updater: Updater<ExpandedState>) => void
882
+ setRowSelection: (updater: Updater<RowSelectionState>) => void
883
+ setActiveCell: (updater: Updater<ActiveCellState>) => void
884
+ moveActiveCell: (next: { rowDelta?: number; colDelta?: number }) => void
885
+ getAllColumns: () => Array<Column<TData>>
886
+ getHeaderGroups: () => Array<HeaderGroup<TData>>
887
+ getFooterGroups: () => Array<HeaderGroup<TData>>
888
+ getRowModel: () => RowModel<TData>
889
+ }
890
+
891
+ type InternalGrid<TData extends RowData> = SvGrid<TData> & {
892
+ getAllColumns: () => Array<Column<TData>>
893
+ }
894
+
895
+ export function createSvGridCore<TFeatures extends TableFeatures, TData extends RowData>(
896
+ options: SvGridOptions<TFeatures, TData>,
897
+ ): SvGrid<TData> {
898
+ const internalState: Record<string, any> = {
899
+ sorting: [],
900
+ columnFilters: [],
901
+ pagination: { pageIndex: 0, pageSize: options.data.length || 10 },
902
+ grouping: [],
903
+ expanded: {},
904
+ rowSelection: {},
905
+ activeCell: { rowIndex: 0, colIndex: 0, cellId: null },
906
+ ...(options.state ?? {}),
907
+ }
908
+ const store = createStore(internalState)
909
+ const optionsStore = createStore(options as Record<string, any>)
910
+ let cachedColumnsInput: Array<ColumnDef<TFeatures, TData>> | null = null
911
+ let cachedColumns: Array<Column<TData>> = []
912
+ let cachedHeaderGroups: Array<HeaderGroup<TData>> = []
913
+ let cachedBaseRowsInput: ReadonlyArray<TData> | null = null
914
+ let cachedBaseRowsColumns: Array<Column<TData>> | null = null
915
+ let cachedBaseRows: Array<Row<TData>> = []
916
+ let cachedRowModel: RowModel<TData> | null = null
917
+ let cachedRowModelBaseRows: Array<Row<TData>> | null = null
918
+ let cachedPipeline = options._rowModels
919
+ let cachedSlices: {
920
+ sorting: SortingState | undefined
921
+ columnFilters: ColumnFiltersState | undefined
922
+ pagination: PaginationState | undefined
923
+ grouping: GroupingState | undefined
924
+ expanded: ExpandedState | undefined
925
+ rowSelection: RowSelectionState | undefined
926
+ } | null = null
927
+
928
+ const grid = {
929
+ store,
930
+ optionsStore,
931
+ get state() {
932
+ return store.state
933
+ },
934
+ getState() {
935
+ return store.state
936
+ },
937
+ setOptions(updater: Updater<Record<string, any>>) {
938
+ optionsStore.setState((prev) =>
939
+ typeof updater === 'function' ? (updater as any)(prev) : updater,
940
+ )
941
+ },
942
+ setColumnFilters(updater: Updater<ColumnFiltersState>) {
943
+ store.setState((prev) => ({
944
+ ...prev,
945
+ columnFilters:
946
+ typeof updater === 'function' ? (updater as any)(prev.columnFilters ?? []) : updater,
947
+ }))
948
+ options.onColumnFiltersChange?.(updater)
949
+ },
950
+ setPagination(updater: Updater<PaginationState>) {
951
+ store.setState((prev) => ({
952
+ ...prev,
953
+ pagination:
954
+ typeof updater === 'function'
955
+ ? (updater as any)(prev.pagination ?? { pageIndex: 0, pageSize: 10 })
956
+ : updater,
957
+ }))
958
+ options.onPaginationChange?.(updater)
959
+ },
960
+ setGrouping(updater: Updater<GroupingState>) {
961
+ store.setState((prev) => ({
962
+ ...prev,
963
+ grouping: typeof updater === 'function' ? (updater as any)(prev.grouping ?? []) : updater,
964
+ }))
965
+ options.onGroupingChange?.(updater)
966
+ },
967
+ setExpanded(updater: Updater<ExpandedState>) {
968
+ store.setState((prev) => ({
969
+ ...prev,
970
+ expanded: typeof updater === 'function' ? (updater as any)(prev.expanded ?? {}) : updater,
971
+ }))
972
+ options.onExpandedChange?.(updater)
973
+ },
974
+ setRowSelection(updater: Updater<RowSelectionState>) {
975
+ store.setState((prev) => ({
976
+ ...prev,
977
+ rowSelection:
978
+ typeof updater === 'function' ? (updater as any)(prev.rowSelection ?? {}) : updater,
979
+ }))
980
+ options.onRowSelectionChange?.(updater)
981
+ },
982
+ setActiveCell(updater: Updater<ActiveCellState>) {
983
+ store.setState((prev) => {
984
+ const previous: ActiveCellState = prev.activeCell ?? {
985
+ rowIndex: 0,
986
+ colIndex: 0,
987
+ cellId: null,
988
+ }
989
+ const nextActive =
990
+ typeof updater === 'function' ? updater(previous) : updater
991
+ return {
992
+ ...prev,
993
+ activeCell: nextActive,
994
+ }
995
+ })
996
+ options.onActiveCellChange?.(updater)
997
+ },
998
+ moveActiveCell(next: { rowDelta?: number; colDelta?: number }) {
999
+ const rows = grid.getRowModel().rows
1000
+ const columns = grid.getAllColumns()
1001
+ const maxRow = Math.max(rows.length - 1, 0)
1002
+ const maxCol = Math.max(columns.length - 1, 0)
1003
+ const current: ActiveCellState = grid.getState().activeCell ?? {
1004
+ rowIndex: 0,
1005
+ colIndex: 0,
1006
+ cellId: null,
1007
+ }
1008
+
1009
+ const rowIndex = Math.min(
1010
+ Math.max(current.rowIndex + (next.rowDelta ?? 0), 0),
1011
+ maxRow,
1012
+ )
1013
+ const colIndex = Math.min(
1014
+ Math.max(current.colIndex + (next.colDelta ?? 0), 0),
1015
+ maxCol,
1016
+ )
1017
+ const columnId = columns[colIndex]?.id ?? 'col_0'
1018
+ grid.setActiveCell({
1019
+ rowIndex,
1020
+ colIndex,
1021
+ cellId: `${rowIndex}_${columnId}`,
1022
+ })
1023
+ },
1024
+ getAllColumns() {
1025
+ // Cache hit: referentially identical columns array.
1026
+ if (cachedColumnsInput === options.columns && cachedColumns.length) {
1027
+ return cachedColumns
1028
+ }
1029
+ // Soft cache hit: consumers commonly recreate the columns array
1030
+ // inline on every render (e.g. `columns={[...]}`). If the new
1031
+ // array has the same length AND each entry has the same `field` /
1032
+ // `id` / `header` (the visibility-affecting structure of a
1033
+ // column), trust the previous build. Mutable inner fields like
1034
+ // `cell` and `editorOptions` are still picked up on the next real
1035
+ // render that bumps an actual data dep - they're read at cell-
1036
+ // render time, not at this top-level cache.
1037
+ if (
1038
+ cachedColumnsInput &&
1039
+ options.columns.length === cachedColumnsInput.length &&
1040
+ cachedColumns.length === options.columns.length &&
1041
+ options.columns.every((c, i) => {
1042
+ const prev = cachedColumnsInput![i]!
1043
+ return (
1044
+ c.field === prev.field &&
1045
+ c.id === prev.id &&
1046
+ c.header === prev.header &&
1047
+ c.editorType === prev.editorType
1048
+ )
1049
+ })
1050
+ ) {
1051
+ // Update the stored input reference so the strict check hits
1052
+ // next time, but reuse the built column model.
1053
+ cachedColumnsInput = options.columns
1054
+ return cachedColumns
1055
+ }
1056
+
1057
+ cachedColumnsInput = options.columns
1058
+ cachedHeaderGroups = []
1059
+ const build = (
1060
+ defs: Array<ColumnDef<TFeatures, TData>>,
1061
+ depth: number,
1062
+ parentId?: string,
1063
+ ): Array<Column<TData>> => {
1064
+ const leaves: Array<Column<TData>> = []
1065
+ defs.forEach((columnDef, index) => {
1066
+ const id = resolveColumnId(columnDef, parentId, depth, index)
1067
+ if (columnDef.columns?.length) {
1068
+ leaves.push(...build(columnDef.columns, depth + 1, id))
1069
+ return
1070
+ }
1071
+ leaves.push({
1072
+ id,
1073
+ depth,
1074
+ parentId,
1075
+ columnDef,
1076
+ getCanSort: () =>
1077
+ Boolean((options._features as any).rowSortingFeature) &&
1078
+ columnDef.sortable !== false,
1079
+ getCanFilter: () =>
1080
+ Boolean((options._features as any).columnFilteringFeature) &&
1081
+ columnDef.filterable !== false,
1082
+ getIsSorted: () => {
1083
+ const entry = store.state.sorting?.find((s: any) => s.id === id)
1084
+ if (!entry) return false
1085
+ return entry.desc ? 'desc' : 'asc'
1086
+ },
1087
+ getToggleSortingHandler: () => () => {
1088
+ const clauses: SortingState = store.state.sorting ?? []
1089
+ const current = clauses.find((s: any) => s.id === id)
1090
+ const nextClause: SortingState = !current
1091
+ ? [...clauses, { id, desc: false }]
1092
+ : current.desc
1093
+ ? clauses.filter((s) => s.id !== id)
1094
+ : clauses.map((s) => (s.id === id ? { ...s, desc: true } : s))
1095
+ store.setState((prev) => ({ ...prev, sorting: nextClause }))
1096
+ options.onSortingChange?.(nextClause)
1097
+ },
1098
+ })
1099
+ })
1100
+ return leaves
1101
+ }
1102
+ cachedColumns = build(options.columns, 0)
1103
+ return cachedColumns
1104
+ },
1105
+ getHeaderGroups() {
1106
+ if (cachedHeaderGroups.length) return cachedHeaderGroups
1107
+ const headers = grid.getAllColumns().map((column) => {
1108
+ const header: Header<TData> = {
1109
+ id: column.id,
1110
+ isPlaceholder: false,
1111
+ colSpan: 1,
1112
+ column,
1113
+ getContext: () => ({ header, column, table: grid }),
1114
+ }
1115
+ return header
1116
+ })
1117
+ cachedHeaderGroups = [{ id: 'header_group_0', headers }]
1118
+ return cachedHeaderGroups
1119
+ },
1120
+ getFooterGroups() {
1121
+ return grid.getHeaderGroups()
1122
+ },
1123
+ getRowModel() {
1124
+ const columns = grid.getAllColumns()
1125
+ if (cachedBaseRowsInput !== options.data || cachedBaseRowsColumns !== columns) {
1126
+ cachedBaseRowsInput = options.data
1127
+ cachedBaseRowsColumns = columns
1128
+ // O(1) column-id → index lookup so getCellValueByColumnId doesn't do
1129
+ // a linear `findIndex` on every cell read (was O(rows × cells × cols)).
1130
+ const columnIndexById = new Map<string, number>()
1131
+ for (let i = 0; i < columns.length; i++) columnIndexById.set(columns[i]!.id, i)
1132
+ const columnCount = columns.length
1133
+
1134
+ cachedBaseRows = new Array(options.data.length)
1135
+ const getRowId = options.getRowId
1136
+ for (let index = 0; index < options.data.length; index++) {
1137
+ const original = options.data[index]!
1138
+ const id = getRowId ? getRowId(original, index) : String(index)
1139
+ // `values` and `cells` are computed lazily - for a 100k-row grid
1140
+ // with only ~20 visible rows we don't want to materialise every
1141
+ // row's full value array or cell objects up front.
1142
+ let cachedValues: Array<unknown> | null = null
1143
+ let cachedCells: Array<Cell<TData>> | null = null
1144
+
1145
+ function computeValues(): Array<unknown> {
1146
+ const values = new Array<unknown>(columnCount)
1147
+ for (let i = 0; i < columnCount; i++) {
1148
+ const column = columns[i]!
1149
+ if (column.columnDef.fieldFn) {
1150
+ values[i] = column.columnDef.fieldFn(original)
1151
+ } else if (column.columnDef.field) {
1152
+ values[i] = (original as any)[column.columnDef.field]
1153
+ } else {
1154
+ values[i] = undefined
1155
+ }
1156
+ }
1157
+ return values
1158
+ }
1159
+
1160
+ const row: Row<TData> = {
1161
+ id,
1162
+ index,
1163
+ original,
1164
+ depth: 0,
1165
+ getCanExpand: () => false,
1166
+ getIsExpanded: () => Boolean((store.state.expanded ?? {})[id]),
1167
+ toggleExpanded: () => {
1168
+ grid.setExpanded((prev) => ({ ...prev, [id]: !prev[id] }))
1169
+ },
1170
+ getIsSelected: () => Boolean((store.state.rowSelection ?? {})[id]),
1171
+ toggleSelected: () => {
1172
+ grid.setRowSelection((prev) => ({ ...prev, [id]: !prev[id] }))
1173
+ },
1174
+ getAllCells: () => {
1175
+ if (cachedCells) return cachedCells
1176
+ const built = new Array<Cell<TData>>(columnCount)
1177
+ for (let i = 0; i < columnCount; i++) {
1178
+ const column = columns[i]!
1179
+ const colIndex = i
1180
+ const cell: Cell<TData> = {
1181
+ id: `${id}_${column.id}`,
1182
+ row,
1183
+ column,
1184
+ getValue: () => {
1185
+ if (!cachedValues) cachedValues = computeValues()
1186
+ return cachedValues[colIndex]
1187
+ },
1188
+ getContext: () => ({
1189
+ cell,
1190
+ row,
1191
+ column,
1192
+ table: grid,
1193
+ getValue: () => cell.getValue(),
1194
+ }),
1195
+ }
1196
+ built[i] = cell
1197
+ }
1198
+ cachedCells = built
1199
+ return built
1200
+ },
1201
+ getCellValueByColumnId: (columnId: string) => {
1202
+ const idx = columnIndexById.get(columnId)
1203
+ if (idx === undefined) return undefined
1204
+ if (!cachedValues) cachedValues = computeValues()
1205
+ return cachedValues[idx]
1206
+ },
1207
+ }
1208
+ cachedBaseRows[index] = row
1209
+ }
1210
+ }
1211
+
1212
+ const currentSlices = {
1213
+ sorting: store.state.sorting,
1214
+ columnFilters: store.state.columnFilters,
1215
+ pagination: store.state.pagination,
1216
+ grouping: store.state.grouping,
1217
+ expanded: store.state.expanded,
1218
+ rowSelection: store.state.rowSelection,
1219
+ }
1220
+ if (
1221
+ cachedRowModel &&
1222
+ cachedRowModelBaseRows === cachedBaseRows &&
1223
+ cachedPipeline === options._rowModels &&
1224
+ cachedSlices?.sorting === currentSlices.sorting &&
1225
+ cachedSlices?.columnFilters === currentSlices.columnFilters &&
1226
+ cachedSlices?.pagination === currentSlices.pagination &&
1227
+ cachedSlices?.grouping === currentSlices.grouping &&
1228
+ cachedSlices?.expanded === currentSlices.expanded &&
1229
+ cachedSlices?.rowSelection === currentSlices.rowSelection
1230
+ ) {
1231
+ return cachedRowModel
1232
+ }
1233
+
1234
+ let rows: Array<Row<TData>> = cachedBaseRows
1235
+
1236
+ const pipeline = options._rowModels ?? {}
1237
+ const ordered: Array<RowModelFactory<TData> | undefined> = [
1238
+ pipeline.coreRowModel,
1239
+ pipeline.filteredRowModel,
1240
+ pipeline.sortedRowModel,
1241
+ pipeline.groupedRowModel,
1242
+ pipeline.expandedRowModel,
1243
+ pipeline.paginatedRowModel,
1244
+ ]
1245
+ ordered.forEach((fn) => {
1246
+ if (fn) rows = fn({ table: grid, rows })
1247
+ })
1248
+ cachedPipeline = options._rowModels
1249
+ cachedSlices = currentSlices
1250
+ cachedRowModelBaseRows = cachedBaseRows
1251
+ cachedRowModel = { rows }
1252
+ return cachedRowModel
1253
+ },
1254
+ } as InternalGrid<TData>
1255
+
1256
+ return grid
1257
+ }
1258
+
1259
+ export function isFunction(value: unknown): value is (...args: Array<any>) => any {
1260
+ return typeof value === 'function'
1261
+ }