@svgrid/grid 2.2.35 → 2.5.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 (380) hide show
  1. package/README.md +199 -199
  2. package/dist/FlexRender.svelte +96 -96
  3. package/dist/GridFooter.svelte +178 -178
  4. package/dist/GridMenus.svelte +705 -705
  5. package/dist/SvAutoComplete.svelte +165 -165
  6. package/dist/SvAvatar.svelte +75 -75
  7. package/dist/SvCalendar.svelte +503 -503
  8. package/dist/SvCarousel.svelte +141 -141
  9. package/dist/SvCheckBox.svelte +102 -102
  10. package/dist/SvCircularProgress.svelte +109 -109
  11. package/dist/SvColorInput.svelte +181 -181
  12. package/dist/SvComboBox.svelte +275 -275
  13. package/dist/SvContextMenu.svelte +116 -116
  14. package/dist/SvCountryInput.svelte +160 -160
  15. package/dist/SvDrawer.svelte +254 -254
  16. package/dist/SvDropDownList.svelte +374 -374
  17. package/dist/SvDurationInput.svelte +126 -126
  18. package/dist/SvField.svelte +293 -293
  19. package/dist/SvForm.svelte +437 -437
  20. package/dist/SvGrid.css +2814 -2814
  21. package/dist/SvGrid.svelte +3490 -3490
  22. package/dist/SvGridChart.svelte +1716 -1716
  23. package/dist/SvGridChartPanel.svelte +485 -485
  24. package/dist/SvGridChartView.svelte +70 -70
  25. package/dist/SvGridDropdown.svelte +728 -728
  26. package/dist/SvGridSelect.svelte +266 -266
  27. package/dist/SvGroupCell.svelte +116 -116
  28. package/dist/SvListBox.svelte +326 -326
  29. package/dist/SvMaskedInput.svelte +122 -122
  30. package/dist/SvMenu.svelte +124 -124
  31. package/dist/SvMenuList.svelte +146 -146
  32. package/dist/SvMultiSelect.svelte +289 -289
  33. package/dist/SvNumberInput.svelte +168 -168
  34. package/dist/SvOtpInput.svelte +158 -158
  35. package/dist/SvPasswordInput.svelte +151 -151
  36. package/dist/SvPhoneInput.svelte +133 -133
  37. package/dist/SvPopover.svelte +197 -197
  38. package/dist/SvProgress.svelte +116 -116
  39. package/dist/SvRadioGroup.svelte +107 -107
  40. package/dist/SvRating.svelte +112 -112
  41. package/dist/SvResult.svelte +73 -73
  42. package/dist/SvRichText.svelte +211 -211
  43. package/dist/SvRowGroupPanel.svelte +170 -170
  44. package/dist/SvScrollArea.svelte +61 -61
  45. package/dist/SvSlider.svelte +200 -200
  46. package/dist/SvSwitchButton.svelte +108 -108
  47. package/dist/SvTagsInput.svelte +112 -112
  48. package/dist/SvTextInput.svelte +147 -147
  49. package/dist/SvTimePicker.svelte +245 -245
  50. package/dist/SvToaster.svelte +159 -159
  51. package/dist/SvToggleButton.svelte +85 -85
  52. package/dist/SvTooltip.svelte +161 -161
  53. package/dist/SvTour.svelte +208 -208
  54. package/dist/SvTree.svelte +444 -444
  55. package/dist/SvTreeSelect.svelte +235 -235
  56. package/dist/cdn/{GridMenus-oQYElQll.js → GridMenus-BpVcKyUr.js} +2 -2
  57. package/dist/cdn/{GridMenus-T-bZNprc.js → GridMenus-CeXL5IW4.js} +2 -2
  58. package/dist/cdn/{SvDateTimePicker-CHnUkWcH.js → SvDateTimePicker-DSUhIta8.js} +2 -2
  59. package/dist/cdn/{SvDateTimePicker-CRQH_U7E.js → SvDateTimePicker-h9kPyszT.js} +2 -2
  60. package/dist/cdn/{SvGridChart-BwVos976.js → SvGridChart-Bs2GIR2Q.js} +1 -1
  61. package/dist/cdn/{SvGridChart-BEJmNNx9.js → SvGridChart-CwhFz7GV.js} +1 -1
  62. package/dist/cdn/{SvGridChartPanel-BOknOkrP.js → SvGridChartPanel-1WXuStzM.js} +1 -1
  63. package/dist/cdn/{SvGridChartPanel-D2PjII4O.js → SvGridChartPanel-e5_nqNP9.js} +1 -1
  64. package/dist/cdn/{SvGridChartView-00LPUHL1.js → SvGridChartView-SmPW10dI.js} +1 -1
  65. package/dist/cdn/{SvGridChartView-BhUEJ5ki.js → SvGridChartView-eSPuJE6g.js} +1 -1
  66. package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-B1ZcLpgs.js} +1 -1
  67. package/dist/cdn/{SvGridDropdown-D13MtJ2j.js → SvGridDropdown-CtdbHcIS.js} +1 -1
  68. package/dist/cdn/{src-Cm7C_WOZ.js → src-DbeN1ReZ.js} +24 -24
  69. package/dist/cdn/{src-72QTH8lh.js → src-DmEQFW5_.js} +23 -23
  70. package/dist/cdn/svgrid.js +5 -5
  71. package/dist/cdn/svgrid.svelte-external.js +5 -5
  72. package/dist/chart-export.js +8 -8
  73. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/_svelte_metadata.json +1 -0
  74. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/_metadata.json +178 -0
  75. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/attachments-qzRy9DX4.js +3843 -0
  76. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/attachments-qzRy9DX4.js.map +1 -0
  77. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/constants-RLNOXCpz.js +23 -0
  78. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/constants-RLNOXCpz.js.map +1 -0
  79. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/dev-DPd12uEU.js +1925 -0
  80. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/dev-DPd12uEU.js.map +1 -0
  81. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/events-CDC4DKuQ.js +206 -0
  82. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/events-CDC4DKuQ.js.map +1 -0
  83. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/flags-BD-lYX7s.js +20 -0
  84. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/flags-BD-lYX7s.js.map +1 -0
  85. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/hydration-D3UBU2t8.js +264 -0
  86. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/hydration-D3UBU2t8.js.map +1 -0
  87. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/legacy-client-B0ZUlbSQ.js +698 -0
  88. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/legacy-client-B0ZUlbSQ.js.map +1 -0
  89. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/package.json +3 -0
  90. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte.js +135 -0
  91. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte.js.map +1 -0
  92. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_animate.js +57 -0
  93. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_animate.js.map +1 -0
  94. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_attachments.js +2 -0
  95. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_compiler.js +46691 -0
  96. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_compiler.js.map +1 -0
  97. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_easing.js +239 -0
  98. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_easing.js.map +1 -0
  99. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_events.js +2 -0
  100. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal.js +6 -0
  101. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal.js.map +1 -0
  102. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_client.js +7 -0
  103. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_disclose-version.js +6 -0
  104. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_disclose-version.js.map +1 -0
  105. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_async.js +7 -0
  106. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_async.js.map +1 -0
  107. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_legacy.js +7 -0
  108. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_legacy.js.map +1 -0
  109. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_tracing.js +7 -0
  110. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_tracing.js.map +1 -0
  111. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_server.js +6 -0
  112. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_legacy.js +85 -0
  113. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_legacy.js.map +1 -0
  114. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_motion.js +561 -0
  115. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_motion.js.map +1 -0
  116. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity.js +26 -0
  117. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity.js.map +1 -0
  118. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity_window.js +124 -0
  119. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity_window.js.map +1 -0
  120. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_server.js +2 -0
  121. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_store.js +91 -0
  122. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_store.js.map +1 -0
  123. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_transition.js +242 -0
  124. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_transition.js.map +1 -0
  125. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-lObbumRX.js +497 -0
  126. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-lObbumRX.js.map +1 -0
  127. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-mR9mAWyK.js +4929 -0
  128. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-mR9mAWyK.js.map +1 -0
  129. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/validate-CezNmRkM.js +192 -0
  130. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/validate-CezNmRkM.js.map +1 -0
  131. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/version-BHHo0WE-.js +10 -0
  132. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/version-BHHo0WE-.js.map +1 -0
  133. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/warnings-yiExAB06.js +140 -0
  134. package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/warnings-yiExAB06.js.map +1 -0
  135. package/dist/themes/index.js +28 -9
  136. package/package.json +11 -11
  137. package/src/FlexRender.svelte +96 -96
  138. package/src/GridFooter.svelte +178 -178
  139. package/src/GridMenus.svelte +705 -705
  140. package/src/SvAutoComplete.svelte +165 -165
  141. package/src/SvAvatar.svelte +75 -75
  142. package/src/SvCalendar.svelte +503 -503
  143. package/src/SvCalendar.test.ts +226 -226
  144. package/src/SvCarousel.svelte +141 -141
  145. package/src/SvCheckBox.svelte +102 -102
  146. package/src/SvCircularProgress.svelte +109 -109
  147. package/src/SvColorInput.svelte +181 -181
  148. package/src/SvComboBox.svelte +275 -275
  149. package/src/SvContextMenu.svelte +116 -116
  150. package/src/SvCountryInput.svelte +160 -160
  151. package/src/SvDrawer.svelte +254 -254
  152. package/src/SvDropDownList.svelte +374 -374
  153. package/src/SvDurationInput.svelte +126 -126
  154. package/src/SvField.svelte +293 -293
  155. package/src/SvForm.svelte +437 -437
  156. package/src/SvForm.test.ts +411 -411
  157. package/src/SvGrid.controller.svelte.ts +3680 -3680
  158. package/src/SvGrid.css +2814 -2814
  159. package/src/SvGrid.svelte +3490 -3490
  160. package/src/SvGrid.types.ts +1708 -1708
  161. package/src/SvGridChart.svelte +1716 -1716
  162. package/src/SvGridChartPanel.svelte +485 -485
  163. package/src/SvGridChartView.svelte +70 -70
  164. package/src/SvGridDropdown.svelte +728 -728
  165. package/src/SvGridSelect.svelte +266 -266
  166. package/src/SvGroupCell.svelte +116 -116
  167. package/src/SvListBox.svelte +326 -326
  168. package/src/SvMaskedInput.svelte +122 -122
  169. package/src/SvMenu.svelte +124 -124
  170. package/src/SvMenu.test.ts +97 -97
  171. package/src/SvMenuList.svelte +146 -146
  172. package/src/SvMultiSelect.svelte +289 -289
  173. package/src/SvNumberInput.svelte +168 -168
  174. package/src/SvOtpInput.svelte +158 -158
  175. package/src/SvPasswordInput.svelte +151 -151
  176. package/src/SvPhoneInput.svelte +133 -133
  177. package/src/SvPopover.svelte +197 -197
  178. package/src/SvProgress.svelte +116 -116
  179. package/src/SvRadioGroup.svelte +107 -107
  180. package/src/SvRating.svelte +112 -112
  181. package/src/SvResult.svelte +73 -73
  182. package/src/SvRichText.svelte +211 -211
  183. package/src/SvRowGroupPanel.svelte +170 -170
  184. package/src/SvScrollArea.svelte +61 -61
  185. package/src/SvSlider.svelte +200 -200
  186. package/src/SvSwitchButton.svelte +108 -108
  187. package/src/SvTagsInput.svelte +112 -112
  188. package/src/SvTextInput.svelte +147 -147
  189. package/src/SvTimePicker.svelte +245 -245
  190. package/src/SvToaster.svelte +159 -159
  191. package/src/SvToaster.test.ts +95 -95
  192. package/src/SvToggleButton.svelte +85 -85
  193. package/src/SvTooltip.svelte +161 -161
  194. package/src/SvTour.svelte +208 -208
  195. package/src/SvTree.svelte +444 -444
  196. package/src/SvTreeSelect.svelte +235 -235
  197. package/src/a11y/dismissable.test.ts +119 -119
  198. package/src/a11y/dismissable.ts +114 -114
  199. package/src/a11y.contract.test.ts +49 -49
  200. package/src/a11y.test.ts +59 -59
  201. package/src/a11y.ts +61 -61
  202. package/src/ai.test.ts +502 -502
  203. package/src/ai.ts +1391 -1391
  204. package/src/build-api.coverage.test.ts +633 -633
  205. package/src/build-api.ts +805 -805
  206. package/src/builtin-editors.grid.test.ts +85 -85
  207. package/src/cell-formatting.ts +169 -169
  208. package/src/cell-render.test.ts +513 -513
  209. package/src/cell-render.ts +496 -496
  210. package/src/cell-values.ts +118 -118
  211. package/src/chart-export.ts +201 -201
  212. package/src/chart-view.svelte.ts +36 -36
  213. package/src/chart.ts +2295 -2295
  214. package/src/collaboration.test.ts +104 -104
  215. package/src/collaboration.ts +167 -167
  216. package/src/core.performance.test.ts +30 -30
  217. package/src/core.ts +1261 -1261
  218. package/src/createAutocomplete.svelte.ts +132 -132
  219. package/src/createCombobox.svelte.ts +191 -191
  220. package/src/createCountryInput.svelte.ts +157 -157
  221. package/src/createDropdownList.svelte.ts +168 -168
  222. package/src/createForm.svelte.ts +386 -386
  223. package/src/createGrid.svelte.ts +42 -42
  224. package/src/createGrid.test.ts +10 -10
  225. package/src/createGridState.svelte.ts +17 -17
  226. package/src/createListbox.svelte.ts +250 -250
  227. package/src/createMenu.svelte.ts +224 -224
  228. package/src/createPopoverSelect.svelte.ts +213 -213
  229. package/src/createSlider.svelte.ts +191 -191
  230. package/src/createTooltip.svelte.ts +144 -144
  231. package/src/createTree.svelte.ts +319 -319
  232. package/src/editing.test.ts +949 -949
  233. package/src/editing.ts +606 -606
  234. package/src/editor-contract.ts +164 -164
  235. package/src/editor-registry.grid.test.ts +144 -144
  236. package/src/editor-registry.ts +122 -122
  237. package/src/export-data-api.test.ts +126 -126
  238. package/src/export-format.test.ts +107 -107
  239. package/src/export-format.ts +601 -601
  240. package/src/filter-operators.ts +157 -157
  241. package/src/flex-render.ts +3 -3
  242. package/src/form-field.ts +127 -127
  243. package/src/group-display.test.ts +167 -167
  244. package/src/group-display.ts +200 -200
  245. package/src/index.ts +724 -724
  246. package/src/js-scroller.svelte.ts +173 -173
  247. package/src/keyboard-handlers.ts +270 -270
  248. package/src/keyboard.test.ts +59 -59
  249. package/src/keyboard.ts +97 -97
  250. package/src/list-nav.test.ts +49 -49
  251. package/src/list-nav.ts +29 -29
  252. package/src/list-option.test.ts +56 -56
  253. package/src/list-option.ts +172 -172
  254. package/src/menus.ts +567 -567
  255. package/src/merge-objects.ts +48 -48
  256. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/_svelte_metadata.json +1 -0
  257. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/_metadata.json +178 -0
  258. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/attachments-qzRy9DX4.js +3843 -0
  259. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/attachments-qzRy9DX4.js.map +1 -0
  260. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/constants-RLNOXCpz.js +23 -0
  261. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/constants-RLNOXCpz.js.map +1 -0
  262. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/dev-DPd12uEU.js +1925 -0
  263. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/dev-DPd12uEU.js.map +1 -0
  264. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/events-CDC4DKuQ.js +206 -0
  265. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/events-CDC4DKuQ.js.map +1 -0
  266. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/flags-BD-lYX7s.js +20 -0
  267. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/flags-BD-lYX7s.js.map +1 -0
  268. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/hydration-D3UBU2t8.js +264 -0
  269. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/hydration-D3UBU2t8.js.map +1 -0
  270. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/legacy-client-B0ZUlbSQ.js +698 -0
  271. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/legacy-client-B0ZUlbSQ.js.map +1 -0
  272. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/package.json +3 -0
  273. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte.js +135 -0
  274. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte.js.map +1 -0
  275. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_animate.js +57 -0
  276. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_animate.js.map +1 -0
  277. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_attachments.js +2 -0
  278. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_compiler.js +46691 -0
  279. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_compiler.js.map +1 -0
  280. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_easing.js +239 -0
  281. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_easing.js.map +1 -0
  282. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_events.js +2 -0
  283. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal.js +6 -0
  284. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal.js.map +1 -0
  285. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_client.js +7 -0
  286. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_disclose-version.js +6 -0
  287. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_disclose-version.js.map +1 -0
  288. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_async.js +7 -0
  289. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_async.js.map +1 -0
  290. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_legacy.js +7 -0
  291. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_legacy.js.map +1 -0
  292. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_tracing.js +7 -0
  293. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_tracing.js.map +1 -0
  294. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_server.js +6 -0
  295. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_legacy.js +85 -0
  296. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_legacy.js.map +1 -0
  297. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_motion.js +561 -0
  298. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_motion.js.map +1 -0
  299. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity.js +26 -0
  300. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity.js.map +1 -0
  301. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity_window.js +124 -0
  302. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity_window.js.map +1 -0
  303. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_server.js +2 -0
  304. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_store.js +91 -0
  305. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_store.js.map +1 -0
  306. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_transition.js +242 -0
  307. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_transition.js.map +1 -0
  308. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-lObbumRX.js +497 -0
  309. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-lObbumRX.js.map +1 -0
  310. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-mR9mAWyK.js +4929 -0
  311. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-mR9mAWyK.js.map +1 -0
  312. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/validate-CezNmRkM.js +192 -0
  313. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/validate-CezNmRkM.js.map +1 -0
  314. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/version-BHHo0WE-.js +10 -0
  315. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/version-BHHo0WE-.js.map +1 -0
  316. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/warnings-yiExAB06.js +140 -0
  317. package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/warnings-yiExAB06.js.map +1 -0
  318. package/src/overlays.test.ts +90 -90
  319. package/src/render-component.ts +28 -28
  320. package/src/row-resize.ts +210 -210
  321. package/src/scheduler-model.test.ts +562 -562
  322. package/src/scheduler-model.ts +872 -872
  323. package/src/selection.test.ts +754 -754
  324. package/src/selection.ts +491 -491
  325. package/src/server-data-source.test.ts +289 -289
  326. package/src/server-data-source.ts +413 -413
  327. package/src/sparkline.test.ts +68 -68
  328. package/src/sparkline.ts +169 -169
  329. package/src/spreadsheet.test.ts +488 -488
  330. package/src/spreadsheet.ts +312 -312
  331. package/src/static-functions.ts +11 -11
  332. package/src/subscribe.ts +38 -38
  333. package/src/svgrid-wrapper.types.ts +526 -526
  334. package/src/svgrid.async-editor-options.test.ts +271 -271
  335. package/src/svgrid.auto-row-height.test.ts +204 -204
  336. package/src/svgrid.behavior.test.ts +705 -705
  337. package/src/svgrid.charting.test.ts +534 -534
  338. package/src/svgrid.comments-autocomplete.test.ts +123 -123
  339. package/src/svgrid.context-menu.test.ts +147 -147
  340. package/src/svgrid.features.test.ts +157 -157
  341. package/src/svgrid.filter-depth.test.ts +163 -163
  342. package/src/svgrid.filter-menu-listbox.svelte.test.ts +331 -322
  343. package/src/svgrid.filter-menu-scroll.test.ts +112 -112
  344. package/src/svgrid.grand-total.test.ts +188 -188
  345. package/src/svgrid.group-display-mode.test.ts +171 -171
  346. package/src/svgrid.group-footers.test.ts +121 -121
  347. package/src/svgrid.group-pagination.test.ts +153 -153
  348. package/src/svgrid.new-features.wrapper.test.ts +251 -251
  349. package/src/svgrid.tree-data.test.ts +186 -186
  350. package/src/svgrid.wrapper.test.ts +40 -40
  351. package/src/svgriddropdown.async-panel.svelte.test.ts +195 -195
  352. package/src/test-setup.ts +62 -62
  353. package/src/themes/index.ts +278 -257
  354. package/src/toast-store.svelte.ts +246 -246
  355. package/src/toast-store.test.ts +147 -147
  356. package/src/tree-row-model.test.ts +168 -168
  357. package/src/ui-buttons.test.ts +144 -144
  358. package/src/ui-inputs.test.ts +118 -118
  359. package/src/ui-localization.test.ts +113 -113
  360. package/src/ui-range.test.ts +70 -70
  361. package/src/ui-selection.test.ts +155 -155
  362. package/src/ui-tier1.test.ts +142 -142
  363. package/src/virtual.test.ts +88 -88
  364. package/src/virtualization/column-virtualizer.test.ts +27 -27
  365. package/src/virtualization/column-virtualizer.ts +30 -30
  366. package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
  367. package/src/virtualization/types.ts +30 -30
  368. package/src/virtualization/virtualizer.test.ts +47 -47
  369. package/src/virtualization/virtualizer.ts +296 -296
  370. package/themes/ag-alpine.css +2 -2
  371. package/themes/antd.css +4 -4
  372. package/themes/atlassian.css +1 -1
  373. package/themes/ember.css +2 -2
  374. package/themes/fluent.css +1 -1
  375. package/themes/github.css +1 -1
  376. package/themes/nord.css +2 -2
  377. package/themes/notion.css +2 -2
  378. package/themes/salesforce.css +1 -1
  379. package/themes/tailwind.css +1 -1
  380. package/themes/vercel.css +1 -1
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
+ }