@svgrid/grid 2.6.4 → 2.6.6

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