@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
@@ -0,0 +1,294 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { createServerGroupModel } from './server-group-model'
3
+ import type { ServerDataSource, ServerDisplayRow, ServerGroupRow } from './server-data-source'
4
+
5
+ type Row = { country: string; city: string; amount: number }
6
+
7
+ const DATA: Row[] = [
8
+ { country: 'DE', city: 'Berlin', amount: 10 },
9
+ { country: 'DE', city: 'Berlin', amount: 5 },
10
+ { country: 'DE', city: 'Munich', amount: 7 },
11
+ { country: 'FR', city: 'Paris', amount: 3 },
12
+ ]
13
+
14
+ /**
15
+ * A fake backend that groups + aggregates DATA per the request's groupBy /
16
+ * groupKeys, exactly as a real SQL GROUP BY would - returns group rows for a
17
+ * group level and leaf rows at the bottom.
18
+ */
19
+ function makeGroupSource(data: Row[], delay = 0): ServerDataSource<Row> & { calls: number } {
20
+ return {
21
+ calls: 0,
22
+ async getRows(req) {
23
+ this.calls += 1
24
+ if (delay) await new Promise((r) => setTimeout(r, delay))
25
+ const subset = data.filter((r) =>
26
+ req.groupKeys.every((k, i) => String((r as Record<string, unknown>)[req.groupBy[i]!]) === k),
27
+ )
28
+ const level = req.groupKeys.length
29
+ if (level < req.groupBy.length) {
30
+ const field = req.groupBy[level]!
31
+ const map = new Map<string, Record<string, unknown>>()
32
+ for (const r of subset) {
33
+ const key = String((r as Record<string, unknown>)[field])
34
+ const agg = map.get(key) ?? { [field]: (r as Record<string, unknown>)[field] }
35
+ for (const a of req.aggregations) {
36
+ const cur = (agg[a.col] as number) ?? 0
37
+ agg[a.col] = a.fn === 'count' ? cur + 1 : cur + ((r as Record<string, unknown>)[a.col] as number)
38
+ }
39
+ map.set(key, agg)
40
+ }
41
+ const groups = [...map.values()]
42
+ return { rows: groups.slice(req.startRow, req.endRow) as unknown as Row[], rowCount: groups.length }
43
+ }
44
+ return { rows: subset.slice(req.startRow, req.endRow), rowCount: subset.length }
45
+ },
46
+ }
47
+ }
48
+
49
+ const flush = () => new Promise((r) => setTimeout(r, 0))
50
+ const groups = (rows: ServerDisplayRow<Row>[]) => rows.filter((r): r is ServerGroupRow<Row> => r.kind === 'group')
51
+
52
+ describe('createServerGroupModel', () => {
53
+ it('refresh() loads the top-level groups with aggregates', async () => {
54
+ const src = makeGroupSource(DATA)
55
+ let state = createServerGroupModel<Row>(src, {
56
+ groupBy: ['country', 'city'],
57
+ aggregations: [{ col: 'amount', fn: 'sum' }],
58
+ onChange: (s) => (state = s as never),
59
+ }).getState()
60
+ const ctl = createServerGroupModel<Row>(src, {
61
+ groupBy: ['country', 'city'],
62
+ aggregations: [{ col: 'amount', fn: 'sum' }],
63
+ onChange: (s) => (state = s as never),
64
+ })
65
+ ctl.refresh()
66
+ await flush()
67
+ const g = groups(state.displayRows)
68
+ expect(g.map((r) => r.key)).toEqual(['DE', 'FR'])
69
+ expect(g.every((r) => r.level === 0 && !r.expanded)).toBe(true)
70
+ expect(g[0]!.aggregates.amount).toBe(22) // 10 + 5 + 7
71
+ expect(g[1]!.aggregates.amount).toBe(3)
72
+ })
73
+
74
+ it('expanding a group lazily fetches and splices its child groups', async () => {
75
+ const src = makeGroupSource(DATA)
76
+ let state = { displayRows: [] as ServerDisplayRow<Row>[] } as never
77
+ const ctl = createServerGroupModel<Row>(src, {
78
+ groupBy: ['country', 'city'],
79
+ aggregations: [{ col: 'amount', fn: 'sum' }],
80
+ onChange: (s) => (state = s as never),
81
+ })
82
+ ctl.refresh()
83
+ await flush()
84
+ const callsBefore = src.calls
85
+ ctl.expandGroup(['DE'])
86
+ await flush()
87
+ expect(src.calls).toBe(callsBefore + 1) // one fetch for DE's children
88
+ const rows = (state as { displayRows: ServerDisplayRow<Row>[] }).displayRows
89
+ // DE (expanded), Berlin, Munich, FR
90
+ expect(rows.map((r) => (r.kind === 'group' ? r.key : 'leaf'))).toEqual(['DE', 'Berlin', 'Munich', 'FR'])
91
+ const berlin = rows.find((r) => r.kind === 'group' && r.key === 'Berlin') as ServerGroupRow<Row>
92
+ expect(berlin.level).toBe(1)
93
+ expect(berlin.aggregates.amount).toBe(15) // 10 + 5
94
+ })
95
+
96
+ it('expanding a bottom-level group returns leaf rows', async () => {
97
+ const src = makeGroupSource(DATA)
98
+ let state = { displayRows: [] as ServerDisplayRow<Row>[] } as never
99
+ const ctl = createServerGroupModel<Row>(src, {
100
+ groupBy: ['country', 'city'],
101
+ onChange: (s) => (state = s as never),
102
+ })
103
+ ctl.refresh()
104
+ await flush()
105
+ ctl.expandGroup(['DE'])
106
+ await flush()
107
+ ctl.expandGroup(['DE', 'Berlin'])
108
+ await flush()
109
+ const rows = (state as { displayRows: ServerDisplayRow<Row>[] }).displayRows
110
+ const leaves = rows.filter((r) => r.kind === 'leaf')
111
+ expect(leaves).toHaveLength(2) // the two Berlin rows
112
+ expect(leaves.every((r) => r.level === 2)).toBe(true)
113
+ })
114
+
115
+ it('collapse removes the children again', async () => {
116
+ const src = makeGroupSource(DATA)
117
+ let state = { displayRows: [] as ServerDisplayRow<Row>[] } as never
118
+ const ctl = createServerGroupModel<Row>(src, {
119
+ groupBy: ['country', 'city'],
120
+ onChange: (s) => (state = s as never),
121
+ })
122
+ ctl.refresh()
123
+ await flush()
124
+ ctl.expandGroup(['DE'])
125
+ await flush()
126
+ ctl.collapseGroup(['DE'])
127
+ const rows = ctl.getState().displayRows
128
+ expect(rows.map((r) => (r.kind === 'group' ? r.key : 'leaf'))).toEqual(['DE', 'FR'])
129
+ expect(ctl.isExpanded(['DE'])).toBe(false)
130
+ })
131
+
132
+ it('setFilter resets the tree and refetches from the top', async () => {
133
+ const src = makeGroupSource(DATA)
134
+ const ctl = createServerGroupModel<Row>(src, { groupBy: ['country'], onChange: () => {} })
135
+ ctl.refresh()
136
+ await flush()
137
+ const before = src.calls
138
+ ctl.setFilter({ global: 'x' })
139
+ await flush()
140
+ expect(src.calls).toBeGreaterThan(before)
141
+ expect(ctl.getState().filterModel).toEqual({ global: 'x' })
142
+ })
143
+
144
+ it('a stale expand response is dropped after a reset (race safety)', async () => {
145
+ const src = makeGroupSource(DATA, 20) // slow
146
+ let state = { displayRows: [] as ServerDisplayRow<Row>[] } as never
147
+ const ctl = createServerGroupModel<Row>(src, {
148
+ groupBy: ['country', 'city'],
149
+ onChange: (s) => (state = s as never),
150
+ })
151
+ ctl.refresh()
152
+ await new Promise((r) => setTimeout(r, 30))
153
+ ctl.expandGroup(['DE']) // in-flight, slow
154
+ ctl.setGroupBy(['city']) // reset (bumps generation) before DE's children land
155
+ await new Promise((r) => setTimeout(r, 60))
156
+ const rows = (state as { displayRows: ServerDisplayRow<Row>[] }).displayRows
157
+ // Regrouped by city at the top; no stale DE subtree leaked in.
158
+ expect(rows.every((r) => r.level === 0)).toBe(true)
159
+ expect(rows.some((r) => r.kind === 'group' && r.key === 'DE')).toBe(false)
160
+ })
161
+
162
+ it('pages a group\'s children in blocks (load more)', async () => {
163
+ const data: Row[] = [
164
+ { country: 'DE', city: 'B', amount: 1 },
165
+ { country: 'DE', city: 'M', amount: 2 },
166
+ { country: 'DE', city: 'H', amount: 3 },
167
+ { country: 'DE', city: 'K', amount: 4 },
168
+ ]
169
+ const src = makeGroupSource(data)
170
+ let state = { displayRows: [] as ServerDisplayRow<Row>[] } as never
171
+ const ctl = createServerGroupModel<Row>(src, {
172
+ groupBy: ['country'],
173
+ pageSize: 2,
174
+ onChange: (s) => (state = s as never),
175
+ })
176
+ ctl.refresh()
177
+ await flush()
178
+ ctl.expandGroup(['DE'])
179
+ await flush()
180
+ let rows = (state as { displayRows: ServerDisplayRow<Row>[] }).displayRows
181
+ expect(rows.filter((r) => r.kind === 'leaf')).toHaveLength(2) // first block
182
+ const more = rows.find((r) => r.kind === 'more')
183
+ expect(more && more.kind === 'more' && more.remaining).toBe(2)
184
+
185
+ ctl.loadMoreChildren(['DE'])
186
+ await flush()
187
+ rows = (state as { displayRows: ServerDisplayRow<Row>[] }).displayRows
188
+ expect(rows.filter((r) => r.kind === 'leaf')).toHaveLength(4) // all loaded
189
+ expect(rows.some((r) => r.kind === 'more')).toBe(false) // no more affordance
190
+ })
191
+
192
+ it('emits a subtotal footer row after an expanded group when groupFooters is on', async () => {
193
+ const src = makeGroupSource(DATA)
194
+ let state = { displayRows: [] as ServerDisplayRow<Row>[] } as never
195
+ const ctl = createServerGroupModel<Row>(src, {
196
+ groupBy: ['country', 'city'],
197
+ aggregations: [{ col: 'amount', fn: 'sum' }],
198
+ groupFooters: true,
199
+ onChange: (s) => (state = s as never),
200
+ })
201
+ ctl.refresh()
202
+ await flush()
203
+ ctl.expandGroup(['DE'])
204
+ await flush()
205
+ const rows = (state as { displayRows: ServerDisplayRow<Row>[] }).displayRows
206
+ const footers = rows.filter((r) => r.kind === 'footer')
207
+ expect(footers).toHaveLength(1)
208
+ expect(footers[0]!.kind === 'footer' && footers[0]!.key).toBe('DE')
209
+ })
210
+
211
+ it('shows skeleton rows while a group\'s first block is loading', async () => {
212
+ const src = makeGroupSource(DATA, 20) // slow
213
+ let state = { displayRows: [] as ServerDisplayRow<Row>[] } as never
214
+ const ctl = createServerGroupModel<Row>(src, {
215
+ groupBy: ['country', 'city'],
216
+ skeletonRows: 3,
217
+ onChange: (s) => (state = s as never),
218
+ })
219
+ ctl.refresh()
220
+ await new Promise((r) => setTimeout(r, 35)) // root loaded
221
+ ctl.expandGroup(['DE']) // fetch in flight -> synchronous emit with skeletons
222
+ const loading = (state as { displayRows: ServerDisplayRow<Row>[] }).displayRows
223
+ expect(loading.filter((r) => r.kind === 'skeleton')).toHaveLength(3)
224
+ await new Promise((r) => setTimeout(r, 40)) // DE loads
225
+ expect((state as { displayRows: ServerDisplayRow<Row>[] }).displayRows.some((r) => r.kind === 'skeleton')).toBe(false)
226
+ })
227
+ })
228
+
229
+ type Node = { id: string; parentId: string | null; name: string; kids: boolean }
230
+ const TREE: Node[] = [
231
+ { id: '1', parentId: null, name: 'Root A', kids: true },
232
+ { id: '2', parentId: null, name: 'Root B', kids: false },
233
+ { id: '1a', parentId: '1', name: 'Child A1', kids: true },
234
+ { id: '1b', parentId: '1', name: 'Child A2', kids: false },
235
+ { id: '1a1', parentId: '1a', name: 'Leaf A1a', kids: false },
236
+ ]
237
+
238
+ /** A fake self-referential tree backend: return the children of the node at the end of groupKeys. */
239
+ function makeTreeSource(nodes: Node[]): ServerDataSource<Node> & { calls: number } {
240
+ return {
241
+ calls: 0,
242
+ async getRows(req) {
243
+ this.calls += 1
244
+ const parent = req.groupKeys.length ? req.groupKeys[req.groupKeys.length - 1] : null
245
+ const children = nodes.filter((n) => (n.parentId ?? null) === (parent ?? null))
246
+ return { rows: children, rowCount: children.length }
247
+ },
248
+ }
249
+ }
250
+
251
+ describe('createServerGroupModel - treeData', () => {
252
+ const opts = (onChange: (s: unknown) => void) => ({
253
+ treeData: true,
254
+ getRowId: (n: Node) => n.id,
255
+ hasChildren: (n: Node) => n.kids,
256
+ onChange: onChange as never,
257
+ })
258
+ const nameOf = (r: ServerDisplayRow<Node>) => ('data' in r ? r.data.name : `(${r.kind})`)
259
+
260
+ it('throws without getRowId/hasChildren', () => {
261
+ expect(() => createServerGroupModel<Node>(makeTreeSource(TREE), { treeData: true, onChange: () => {} })).toThrow()
262
+ })
263
+
264
+ it('loads roots, marking expandable nodes as groups and leaves as leaves', async () => {
265
+ const src = makeTreeSource(TREE)
266
+ let state = { displayRows: [] as ServerDisplayRow<Node>[] } as never
267
+ const ctl = createServerGroupModel<Node>(src, opts((s) => (state = s as never)))
268
+ ctl.refresh()
269
+ await flush()
270
+ const rows = (state as { displayRows: ServerDisplayRow<Node>[] }).displayRows
271
+ expect(rows.map((r) => [r.kind, nameOf(r)])).toEqual([
272
+ ['group', 'Root A'], // kids: true
273
+ ['leaf', 'Root B'], // kids: false
274
+ ])
275
+ })
276
+
277
+ it('expanding a node fetches and splices its children by id path', async () => {
278
+ const src = makeTreeSource(TREE)
279
+ let state = { displayRows: [] as ServerDisplayRow<Node>[] } as never
280
+ const ctl = createServerGroupModel<Node>(src, opts((s) => (state = s as never)))
281
+ ctl.refresh()
282
+ await flush()
283
+ ctl.expandGroup(['1'])
284
+ await flush()
285
+ let rows = (state as { displayRows: ServerDisplayRow<Node>[] }).displayRows
286
+ expect(rows.map(nameOf)).toEqual(['Root A', 'Child A1', 'Child A2', 'Root B'])
287
+ // drill one more level: Child A1 -> Leaf A1a
288
+ ctl.expandGroup(['1', '1a'])
289
+ await flush()
290
+ rows = (state as { displayRows: ServerDisplayRow<Node>[] }).displayRows
291
+ expect(rows.map(nameOf)).toEqual(['Root A', 'Child A1', 'Leaf A1a', 'Child A2', 'Root B'])
292
+ expect(rows.find((r) => nameOf(r) === 'Leaf A1a')!.level).toBe(2)
293
+ })
294
+ })
@@ -0,0 +1,370 @@
1
+ /**
2
+ * Server-side GROUP row model (SSRM grouping) - the first-class
3
+ * companion to `createServerDataSource`. Grouping, aggregation, and lazy
4
+ * expansion all flow through the SAME `ServerDataSource.getRows` contract:
5
+ *
6
+ * - The request carries `groupBy` (the columns grouped on) and `groupKeys`
7
+ * (the path of the group being expanded). While `groupKeys.length <
8
+ * groupBy.length` the server returns GROUP rows (one per distinct key at
9
+ * that level, carrying the key + aggregate values); at the bottom level it
10
+ * returns the leaf rows under that path.
11
+ * - Expanding a group is just another `getRows` with `groupKeys` set to that
12
+ * group's path. This controller owns the group tree: expand / collapse,
13
+ * per-node lazy fetch + cache, race-safety, and flattening to a display
14
+ * list the grid renders (group rows with an expander, leaves underneath).
15
+ *
16
+ * Headless and framework-agnostic on purpose: render from `state.displayRows`
17
+ * and call `toggleGroup` from the group cell's expander.
18
+ */
19
+ import type {
20
+ ServerAggregation,
21
+ ServerDataSource,
22
+ ServerDisplayRow,
23
+ ServerFilterModel,
24
+ ServerGroupRow,
25
+ ServerSortModel,
26
+ } from './server-data-source'
27
+
28
+ export type ServerGroupState<TData> = {
29
+ /** The flattened tree: top-level groups, with expanded groups' children spliced in. */
30
+ displayRows: ServerDisplayRow<TData>[]
31
+ groupBy: string[]
32
+ /** True during the initial top-level fetch (before the root has ever loaded). */
33
+ loading: boolean
34
+ /** The rejection from the last failed top-level fetch, else null. */
35
+ error: unknown
36
+ sortModel: ServerSortModel
37
+ filterModel: ServerFilterModel
38
+ /** Ids (JSON of the path) of the currently expanded groups. */
39
+ expandedGroups: string[]
40
+ }
41
+
42
+ export type ServerGroupControllerOptions<TData> = {
43
+ /** Columns to group on, outer to inner. Empty in `treeData` mode. */
44
+ groupBy?: string[]
45
+ /** Value columns to roll up per group. */
46
+ aggregations?: ServerAggregation[]
47
+ /** Max children fetched per group in one request. Default 200. */
48
+ pageSize?: number
49
+ /**
50
+ * Self-referential tree mode: instead of grouping flat rows by columns, each
51
+ * row is a node whose children are fetched on expand. Requires `getRowId`
52
+ * (the node id, which builds the `groupKeys` path) and `hasChildren` (whether
53
+ * a node is expandable). `getRows` receives `groupKeys` = the path to the
54
+ * node being expanded, and returns that node's direct children.
55
+ */
56
+ treeData?: boolean
57
+ /** Node id, used to build the `groupKeys` path. Required in `treeData` mode. */
58
+ getRowId?: (row: TData) => string
59
+ /** Whether a node can be expanded. Required in `treeData` mode. */
60
+ hasChildren?: (row: TData) => boolean
61
+ /** Emit a subtotal "footer" row after each expanded group's children. */
62
+ groupFooters?: boolean
63
+ /** Placeholder rows shown while a group's first block is loading. Default 3. */
64
+ skeletonRows?: number
65
+ onChange: (state: ServerGroupState<TData>) => void
66
+ }
67
+
68
+ export type ServerGroupController<TData> = {
69
+ /** (Re-)fetch the tree from the top. Call once to load the first level. */
70
+ refresh(): void
71
+ setSort(sortModel: ServerSortModel): void
72
+ setFilter(filterModel: ServerFilterModel): void
73
+ /** Change the grouping columns. Collapses everything and reloads the top level. */
74
+ setGroupBy(groupBy: string[]): void
75
+ /** Expand or collapse a group row (from its expander). */
76
+ toggleGroup(row: ServerGroupRow<TData>): void
77
+ expandGroup(path: string[]): void
78
+ collapseGroup(path: string[]): void
79
+ /** Fetch the next block of a group's children (intra-group paging). */
80
+ loadMoreChildren(path: string[]): void
81
+ isExpanded(path: string[]): boolean
82
+ getState(): ServerGroupState<TData>
83
+ /** Stop accepting in-flight responses. Call on unmount. */
84
+ dispose(): void
85
+ }
86
+
87
+ /**
88
+ * Map a `ServerGroupState.displayRows` to grid rows: each row's data spread at
89
+ * the top level (so normal `field` columns show group subtotals on group rows
90
+ * and cell values on leaves) plus a `__group` marker the built-in `SvGroupCell`
91
+ * reads to draw the expander + indentation. Feed the result to `<SvGrid data>`.
92
+ */
93
+ export type ServerGroupGridRow<TData> = TData & { __group: ServerDisplayRow<TData> }
94
+ export function serverGroupRows<TData>(
95
+ state: ServerGroupState<TData> | undefined | null,
96
+ ): ServerGroupGridRow<TData>[] {
97
+ return (state?.displayRows ?? []).map(
98
+ (r) => ({ ...('data' in r ? (r.data as object) : {}), __group: r }) as ServerGroupGridRow<TData>,
99
+ )
100
+ }
101
+
102
+ /**
103
+ * Build the `serverGroup` prop for `<SvGrid>` from a controller, so the grid's
104
+ * built-in tree keyboard (ArrowRight / ArrowLeft expand / collapse) and treegrid
105
+ * a11y work with no app-level key handling. Reads the `__group` marker that
106
+ * `serverGroupRows` attaches. Usage: `<SvGrid ... serverGroup={serverGroupNav(ctl)} />`.
107
+ */
108
+ export function serverGroupNav<TData>(ctl: ServerGroupController<TData>) {
109
+ return {
110
+ isGroup: (row: ServerGroupGridRow<TData>) => row.__group?.kind === 'group',
111
+ level: (row: ServerGroupGridRow<TData>) => row.__group?.level ?? 0,
112
+ expanded: (row: ServerGroupGridRow<TData>) =>
113
+ row.__group?.kind === 'group' ? row.__group.expanded : false,
114
+ onToggle: (row: ServerGroupGridRow<TData>) => {
115
+ if (row.__group?.kind === 'group') ctl.toggleGroup(row.__group)
116
+ else if (row.__group?.kind === 'more') ctl.loadMoreChildren(row.__group.path)
117
+ },
118
+ }
119
+ }
120
+
121
+ const keyOf = (path: string[]): string => JSON.stringify(path)
122
+
123
+ type ChildDescriptor<TData> =
124
+ | { kind: 'group'; path: string[]; field: string; key: string; level: number; aggregates: Record<string, unknown>; data: TData }
125
+ | { kind: 'leaf'; id: string; level: number; data: TData }
126
+
127
+ type GroupNode<TData> = {
128
+ loaded: boolean
129
+ loading: boolean
130
+ error: unknown
131
+ count: number
132
+ children: ChildDescriptor<TData>[]
133
+ }
134
+
135
+ export function createServerGroupModel<TData>(
136
+ source: ServerDataSource<TData>,
137
+ options: ServerGroupControllerOptions<TData>,
138
+ ): ServerGroupController<TData> {
139
+ let groupBy = [...(options.groupBy ?? [])]
140
+ const aggregations = options.aggregations ?? []
141
+ const pageSize = options.pageSize ?? 200
142
+ const treeData = !!options.treeData
143
+ if (treeData && (!options.getRowId || !options.hasChildren)) {
144
+ throw new Error('createServerGroupModel: treeData requires getRowId and hasChildren')
145
+ }
146
+ const getRowId = options.getRowId
147
+ const hasChildren = options.hasChildren
148
+ const groupFooters = !!options.groupFooters
149
+ const skeletonRows = Math.max(0, options.skeletonRows ?? 3)
150
+
151
+ let sortModel: ServerSortModel = []
152
+ let filterModel: ServerFilterModel = {}
153
+ const nodes = new Map<string, GroupNode<TData>>()
154
+ const expanded = new Set<string>() // path keys
155
+ // Generation guard: bumped on any tree reset so every fetch in flight for the
156
+ // previous shape is discarded when it lands (the classic stale-response bug).
157
+ let generation = 0
158
+ let disposed = false
159
+
160
+ const rootKey = keyOf([])
161
+
162
+ function ensureNode(key: string): GroupNode<TData> {
163
+ let node = nodes.get(key)
164
+ if (!node) {
165
+ node = { loaded: false, loading: false, error: null, count: 0, children: [] }
166
+ nodes.set(key, node)
167
+ }
168
+ return node
169
+ }
170
+
171
+ // Walk the loaded tree from the root, splicing expanded groups' children in.
172
+ function flatten(): ServerDisplayRow<TData>[] {
173
+ const out: ServerDisplayRow<TData>[] = []
174
+ const walk = (path: string[]): void => {
175
+ const node = nodes.get(keyOf(path))
176
+ if (!node?.loaded) return
177
+ for (const child of node.children) {
178
+ if (child.kind === 'leaf') {
179
+ out.push({ kind: 'leaf', id: child.id, level: child.level, data: child.data })
180
+ continue
181
+ }
182
+ const cid = keyOf(child.path)
183
+ const cnode = nodes.get(cid)
184
+ const isExp = expanded.has(cid)
185
+ out.push({
186
+ kind: 'group',
187
+ id: cid,
188
+ path: child.path,
189
+ field: child.field,
190
+ key: child.key,
191
+ level: child.level,
192
+ expanded: isExp,
193
+ loading: !!cnode?.loading,
194
+ aggregates: child.aggregates,
195
+ data: child.data,
196
+ })
197
+ if (isExp) {
198
+ if (cnode?.loaded) {
199
+ walk(child.path)
200
+ // Subtotal row after the group's children.
201
+ if (groupFooters) {
202
+ out.push({
203
+ kind: 'footer',
204
+ id: cid + 'footer',
205
+ level: child.level + 1,
206
+ path: child.path,
207
+ key: child.key,
208
+ aggregates: child.aggregates,
209
+ data: child.data,
210
+ })
211
+ }
212
+ } else if (cnode?.loading) {
213
+ // First block in flight: placeholder skeleton rows.
214
+ for (let i = 0; i < skeletonRows; i += 1) {
215
+ out.push({ kind: 'skeleton', id: cid + 'sk' + i, level: child.level + 1 })
216
+ }
217
+ }
218
+ }
219
+ }
220
+ // Intra-group paging: emit a "load more" affordance when only part of this
221
+ // node's children have been fetched.
222
+ if (node.children.length < node.count) {
223
+ out.push({
224
+ kind: 'more',
225
+ id: keyOf([...path, 'more']),
226
+ level: path.length,
227
+ path,
228
+ remaining: node.count - node.children.length,
229
+ loading: node.loading,
230
+ })
231
+ }
232
+ }
233
+ walk([])
234
+ return out
235
+ }
236
+
237
+ function emit(): void {
238
+ if (disposed) return
239
+ const root = nodes.get(rootKey)
240
+ options.onChange({
241
+ displayRows: flatten(),
242
+ groupBy: [...groupBy],
243
+ loading: !!root?.loading && !root?.loaded,
244
+ error: root?.error ?? null,
245
+ sortModel,
246
+ filterModel,
247
+ expandedGroups: [...expanded],
248
+ })
249
+ }
250
+
251
+ // append=false loads the first block of a node's children (on expand);
252
+ // append=true fetches the NEXT block and concatenates (intra-group paging).
253
+ async function fetchNode(path: string[], append = false): Promise<void> {
254
+ if (disposed) return
255
+ const gen = generation
256
+ const key = keyOf(path)
257
+ const node = ensureNode(key)
258
+ if (node.loading) return // a fetch for this node is already in flight
259
+ node.loading = true
260
+ node.error = null
261
+ emit()
262
+ const level = path.length
263
+ const groupField = groupBy[level]
264
+ const startRow = append ? node.children.length : 0
265
+ try {
266
+ const res = await source.getRows({
267
+ startRow,
268
+ endRow: startRow + pageSize,
269
+ pageIndex: Math.floor(startRow / pageSize),
270
+ pageSize,
271
+ sortModel,
272
+ filterModel,
273
+ groupBy,
274
+ groupKeys: path,
275
+ aggregations,
276
+ })
277
+ if (disposed || gen !== generation) return // stale: the tree was reset
278
+ node.count = res.rowCount
279
+ const block = res.rows.map((data, i): ChildDescriptor<TData> => {
280
+ // A row is an expandable "group" when: (tree mode) the source says it has
281
+ // children, or (grouping mode) we are above the innermost groupBy level.
282
+ const isGroup = treeData ? hasChildren!(data) : level < groupBy.length
283
+ if (isGroup) {
284
+ const gk = treeData
285
+ ? getRowId!(data)
286
+ : ((v) => (v == null ? '' : String(v)))((data as Record<string, unknown>)[groupField!])
287
+ const aggregates: Record<string, unknown> = {}
288
+ for (const a of aggregations) aggregates[a.col] = (data as Record<string, unknown>)[a.col]
289
+ return { kind: 'group', path: [...path, gk], field: treeData ? '' : groupField!, key: gk, level, aggregates, data }
290
+ }
291
+ const id = treeData && getRowId ? `${key}:${getRowId(data)}` : `${key}:${startRow + i}`
292
+ return { kind: 'leaf', id, level, data }
293
+ })
294
+ node.children = append ? [...node.children, ...block] : block
295
+ node.loaded = true
296
+ node.loading = false
297
+ emit()
298
+ } catch (err) {
299
+ if (disposed || gen !== generation) return
300
+ node.error = err
301
+ if (!append) node.loaded = false
302
+ node.loading = false
303
+ emit()
304
+ }
305
+ }
306
+
307
+ // Clear the cache and reload the root plus any still-expanded paths (in
308
+ // parallel; flatten only descends into loaded nodes, so children appear as
309
+ // each level lands).
310
+ function resetTree(): void {
311
+ generation += 1
312
+ nodes.clear()
313
+ void fetchNode([])
314
+ for (const k of expanded) void fetchNode(JSON.parse(k) as string[])
315
+ }
316
+
317
+ function expandGroup(path: string[]): void {
318
+ const key = keyOf(path)
319
+ if (expanded.has(key)) return
320
+ expanded.add(key)
321
+ const node = nodes.get(key)
322
+ if (!node?.loaded && !node?.loading) void fetchNode(path)
323
+ else emit()
324
+ }
325
+
326
+ function collapseGroup(path: string[]): void {
327
+ if (expanded.delete(keyOf(path))) emit()
328
+ }
329
+
330
+ return {
331
+ refresh: resetTree,
332
+ setSort(next) {
333
+ sortModel = next
334
+ resetTree()
335
+ },
336
+ setFilter(next) {
337
+ filterModel = next
338
+ resetTree()
339
+ },
340
+ setGroupBy(next) {
341
+ groupBy = [...next]
342
+ expanded.clear()
343
+ resetTree()
344
+ },
345
+ toggleGroup(row) {
346
+ if (expanded.has(row.id)) collapseGroup(row.path)
347
+ else expandGroup(row.path)
348
+ },
349
+ expandGroup,
350
+ collapseGroup,
351
+ loadMoreChildren: (path) => void fetchNode(path, true),
352
+ isExpanded: (path) => expanded.has(keyOf(path)),
353
+ getState() {
354
+ const root = nodes.get(rootKey)
355
+ return {
356
+ displayRows: flatten(),
357
+ groupBy: [...groupBy],
358
+ loading: !!root?.loading && !root?.loaded,
359
+ error: root?.error ?? null,
360
+ sortModel,
361
+ filterModel,
362
+ expandedGroups: [...expanded],
363
+ }
364
+ },
365
+ dispose() {
366
+ disposed = true
367
+ generation += 1
368
+ },
369
+ }
370
+ }