@svgrid/grid 1.2.34 → 2.0.0

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