@svgrid/grid 1.2.33 → 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,255 @@
1
+ <script lang="ts" module>
2
+ export type { GridSelectColumn } from './ui-app.types'
3
+ </script>
4
+
5
+ <script lang="ts">
6
+ /**
7
+ * SvGridSelect - a "grid in a dropdown" single-select: the panel shows the
8
+ * options as a compact multi-column table (header row + optional search), so
9
+ * you pick by more than a label. The open / position / dismiss / roving /
10
+ * keyboard / ARIA mechanics come from the shared `createPopoverSelect` core;
11
+ * this component adds the multi-column table. It does NOT embed the full
12
+ * SvGrid. Emits the chosen row's id. Parity: Smart multicolumn combobox.
13
+ */
14
+ import type { GridSelectColumn } from './ui-app.types'
15
+ import SvField from './SvField.svelte'
16
+ import { editorAria, nextEditorId, type SvEditorProps } from './editor-contract'
17
+ import { portalToBody, popIn } from './popover'
18
+ import { createPopoverSelect } from './createPopoverSelect.svelte'
19
+ import { debounce } from './debounce'
20
+ import { filterGridRows, type Row } from './grid-select'
21
+
22
+ type Props = Pick<
23
+ SvEditorProps,
24
+ 'disabled' | 'label' | 'hint' | 'error' | 'required' | 'invalid' | 'size' | 'id' | 'name' | 'dir' | 'ariaLabel'
25
+ > & {
26
+ columns: ReadonlyArray<GridSelectColumn>
27
+ options: ReadonlyArray<Row>
28
+ idField?: string
29
+ labelField?: string
30
+ value?: string | number | null
31
+ onChange?: (id: string | number, row: Row) => void
32
+ onCommit?: (id: string | number, row: Row) => void
33
+ onCancel?: () => void
34
+ placeholder?: string
35
+ searchable?: boolean
36
+ /** Async/remote rows: called (debounced) with the search query; its result
37
+ * replaces the list. When set, `options` is only the initial list. */
38
+ loadOptions?: (query: string) => Promise<Row[]>
39
+ debounceMs?: number
40
+ loadingText?: string
41
+ }
42
+
43
+ let {
44
+ columns,
45
+ options,
46
+ idField = 'id',
47
+ labelField,
48
+ value = $bindable<string | number | null>(null),
49
+ onChange,
50
+ onCommit,
51
+ onCancel,
52
+ placeholder = 'Select…',
53
+ searchable = true,
54
+ loadOptions,
55
+ debounceMs = 250,
56
+ loadingText = 'Loading…',
57
+ disabled = false,
58
+ label,
59
+ hint,
60
+ error,
61
+ required = false,
62
+ invalid = false,
63
+ size = 'md',
64
+ id,
65
+ name,
66
+ dir,
67
+ ariaLabel,
68
+ }: Props = $props()
69
+
70
+ const autoId = nextEditorId('sv-gs')
71
+ const uid = $derived(id ?? autoId)
72
+ const labelKey = $derived(labelField ?? columns[0]?.field ?? idField)
73
+ const template = $derived(columns.map((c) => c.width ?? '1fr').join(' '))
74
+
75
+ let search = $state('')
76
+ let triggerEl = $state<HTMLButtonElement | null>(null)
77
+ let panelEl = $state<HTMLDivElement | null>(null)
78
+ let searchEl = $state<HTMLInputElement | null>(null)
79
+ let gridEl = $state<HTMLDivElement | null>(null)
80
+
81
+ // Remote/async rows: `asyncRows` holds the last server result; a stale response
82
+ // is dropped by comparing the request sequence.
83
+ let asyncRows = $state<Row[]>([])
84
+ let loading = $state(false)
85
+ let reqSeq = 0
86
+ function doLoad(q: string) {
87
+ if (!loadOptions) return
88
+ const seq = ++reqSeq
89
+ loading = true
90
+ loadOptions(q)
91
+ .then((rows) => { if (seq === reqSeq) { asyncRows = rows; loading = false } })
92
+ .catch(() => { if (seq === reqSeq) loading = false })
93
+ }
94
+ const runLoad = debounce((q: string) => doLoad(q), debounceMs)
95
+
96
+ const filtered = $derived(
97
+ loadOptions ? asyncRows : filterGridRows(options, columns.map((c) => c.field), search),
98
+ )
99
+ // Pool for resolving the selected row's trigger label (a remote-selected id may
100
+ // not be in the current result page).
101
+ const pool = $derived(
102
+ loadOptions ? [...new Map([...options, ...asyncRows].map((r) => [r[idField], r])).values()] : options,
103
+ )
104
+ const selectedRow = $derived(pool.find((r) => r[idField] === value) ?? null)
105
+ const triggerText = $derived(selectedRow ? String(selectedRow[labelKey] ?? '') : '')
106
+
107
+ const gs = createPopoverSelect({
108
+ itemCount: () => filtered.length,
109
+ onSelect: (i) => {
110
+ const r = filtered[i]
111
+ if (r) { value = r[idField] as string | number; onChange?.(r[idField] as string | number, r) }
112
+ },
113
+ onOpenChange: (o) => { if (o) { search = ''; if (loadOptions) doLoad('') } },
114
+ getTrigger: () => triggerEl,
115
+ getPanel: () => panelEl,
116
+ getInitialFocus: () => (searchable ? searchEl : gridEl),
117
+ closeOnSelect: () => true,
118
+ initialActive: () => Math.max(0, filtered.findIndex((r) => r[idField] === value)),
119
+ minWidth: () => 320,
120
+ estimatedHeight: () => 320,
121
+ idPrefix: 'sv-gs',
122
+ })
123
+ </script>
124
+
125
+ <SvField id={uid} {label} {hint} {error} {required} {dir}>
126
+ <div class="sv-gs sv-gs--{size}" class:is-disabled={disabled}>
127
+ <button
128
+ bind:this={triggerEl}
129
+ type="button"
130
+ class="sv-gs__trigger"
131
+ class:is-invalid={invalid}
132
+ {disabled}
133
+ onclick={() => gs.toggle()}
134
+ {...gs.triggerProps()}
135
+ {...editorAria({ id: uid, invalid, required, error, hint, ariaLabel })}
136
+ >
137
+ <span class="sv-gs__value" class:is-placeholder={!triggerText}>{triggerText || placeholder}</span>
138
+ <span class="sv-gs__arrow" aria-hidden="true">▾</span>
139
+ </button>
140
+ {#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
141
+ </div>
142
+ </SvField>
143
+
144
+ {#if gs.open}
145
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
146
+ <div
147
+ bind:this={panelEl}
148
+ class="sv-gs__panel"
149
+ dir={dir}
150
+ use:portalToBody
151
+ use:popIn={{ up: gs.rect.openUpward }}
152
+ style:position="fixed"
153
+ style:top={`${gs.rect.top}px`}
154
+ style:left={`${gs.rect.left}px`}
155
+ style:min-width={`${Math.max(gs.rect.width, 320)}px`}
156
+ onkeydown={gs.onPanelKeydown}
157
+ >
158
+ {#if searchable}
159
+ <!-- svelte-ignore a11y_autofocus -->
160
+ <input
161
+ bind:this={searchEl}
162
+ class="sv-gs__search"
163
+ role="combobox"
164
+ aria-expanded="true"
165
+ aria-controls={gs.panelId}
166
+ aria-activedescendant={gs.activeDescendant()}
167
+ aria-label="Search options"
168
+ placeholder="Search…"
169
+ value={search}
170
+ oninput={(e) => { search = e.currentTarget.value; gs.active = 0; if (loadOptions) runLoad(search) }}
171
+ />
172
+ {/if}
173
+ <div
174
+ bind:this={gridEl}
175
+ id={gs.panelId}
176
+ class="sv-gs__grid"
177
+ role="grid"
178
+ aria-label={label ?? ariaLabel ?? 'Options'}
179
+ aria-activedescendant={searchable ? undefined : gs.activeDescendant()}
180
+ tabindex={searchable ? -1 : 0}
181
+ >
182
+ <div class="sv-gs__head" role="row" style:grid-template-columns={template}>
183
+ {#each columns as col (col.field)}
184
+ <span class="sv-gs__hcell" role="columnheader">{col.header}</span>
185
+ {/each}
186
+ </div>
187
+ <div class="sv-gs__body">
188
+ {#each filtered as row, i (row[idField])}
189
+ <div
190
+ class="sv-gs__row"
191
+ class:is-active={gs.isActive(i)}
192
+ class:is-selected={row[idField] === value}
193
+ role="row"
194
+ aria-selected={row[idField] === value}
195
+ style:grid-template-columns={template}
196
+ onclick={() => { gs.active = i; gs.selectActive() }}
197
+ {...gs.itemProps(i)}
198
+ >
199
+ {#each columns as col (col.field)}
200
+ <span class="sv-gs__cell" role="gridcell">{String(row[col.field] ?? '')}</span>
201
+ {/each}
202
+ </div>
203
+ {:else}
204
+ <div class="sv-gs__empty" role="row">{loading ? loadingText : 'No matches'}</div>
205
+ {/each}
206
+ {#if loading && filtered.length}
207
+ <div class="sv-gs__empty" role="row" aria-live="polite">{loadingText}</div>
208
+ {/if}
209
+ </div>
210
+ </div>
211
+ </div>
212
+ {/if}
213
+
214
+ <style>
215
+ .sv-gs { --_accent: var(--sg-accent, #2563eb); display: inline-flex; width: 260px; }
216
+ .sv-gs.is-disabled { opacity: 0.6; }
217
+ .sv-gs__trigger {
218
+ flex: 1; min-width: 0; display: flex; align-items: center; gap: 6px; text-align: start;
219
+ background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
220
+ border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
221
+ font: inherit; cursor: pointer; padding: 0 9px;
222
+ }
223
+ .sv-gs--sm .sv-gs__trigger { height: 28px; font-size: 12px; }
224
+ .sv-gs--md .sv-gs__trigger { height: 34px; font-size: 13px; }
225
+ .sv-gs--lg .sv-gs__trigger { height: 40px; font-size: 15px; }
226
+ .sv-gs__trigger:focus-visible { outline: none; border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
227
+ .sv-gs__trigger.is-invalid { border-color: var(--sg-danger, #dc2626); }
228
+ .sv-gs__value { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
229
+ .sv-gs__value.is-placeholder { color: var(--sg-muted, #94a3b8); }
230
+ .sv-gs__arrow { color: var(--sg-muted, #64748b); font-size: 10px; }
231
+
232
+ :global(.sv-gs__panel) {
233
+ z-index: 2147483646; display: flex; flex-direction: column; max-height: 340px; max-width: 92vw; overflow: hidden;
234
+ background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
235
+ border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px;
236
+ box-shadow: 0 16px 48px -12px rgba(15, 23, 42, 0.35); font-size: 13px;
237
+ }
238
+ :global(.sv-gs__search) {
239
+ margin: 8px; padding: 6px 9px; font: inherit; font-size: 13px;
240
+ border: 1px solid var(--sg-input-border, #cbd5e1); border-radius: 7px;
241
+ background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a); outline: none;
242
+ }
243
+ :global(.sv-gs__grid) { display: flex; flex-direction: column; overflow: hidden; outline: none; }
244
+ :global(.sv-gs__head) {
245
+ display: grid; gap: 10px; padding: 6px 10px; border-bottom: 1px solid var(--sg-border, #e2e8f0);
246
+ background: var(--sg-header-bg, #f8fafc); color: var(--sg-header-fg, var(--sg-muted, #64748b));
247
+ font-size: 11px; font-weight: 650; text-transform: uppercase; letter-spacing: 0.02em;
248
+ }
249
+ :global(.sv-gs__body) { overflow: auto; }
250
+ :global(.sv-gs__row) { display: grid; gap: 10px; padding: 7px 10px; cursor: pointer; align-items: center; }
251
+ :global(.sv-gs__row.is-active) { background: var(--sg-row-hover-bg, #f1f5f9); }
252
+ :global(.sv-gs__row.is-selected) { color: var(--sg-accent, #2563eb); font-weight: 600; }
253
+ :global(.sv-gs__cell) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
254
+ :global(.sv-gs__empty) { padding: 12px; text-align: center; color: var(--sg-muted, #94a3b8); }
255
+ </style>
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Component tests for SvGridSelect: trigger label, portalled multi-column panel
3
+ * (header + rows), search filter, and row-click select (emits id + row, closes).
4
+ */
5
+ import { afterEach, describe, expect, it, vi } from 'vitest'
6
+ import { mount, unmount, flushSync } from 'svelte'
7
+ import SvGridSelect from './SvGridSelect.svelte'
8
+
9
+ const columns = [
10
+ { field: 'name', header: 'Name' },
11
+ { field: 'email', header: 'Email' },
12
+ { field: 'role', header: 'Role' },
13
+ ]
14
+ const people = [
15
+ { id: 1, name: 'Ada Lovelace', email: 'ada@x.io', role: 'Eng' },
16
+ { id: 2, name: 'Alan Turing', email: 'alan@x.io', role: 'Research' },
17
+ { id: 3, name: 'Grace Hopper', email: 'grace@x.io', role: 'Compilers' },
18
+ ]
19
+
20
+ function mountGs(props: Record<string, unknown>) {
21
+ const target = document.createElement('div')
22
+ document.body.appendChild(target)
23
+ const app = mount(SvGridSelect, { target, props: { columns, options: people, ...props } as any })
24
+ flushSync()
25
+ return { target, destroy: () => { unmount(app); target.remove() } }
26
+ }
27
+
28
+ const bodyRows = () => document.body.querySelectorAll<HTMLElement>('.sv-gs__row')
29
+
30
+ afterEach(() => { document.body.innerHTML = '' })
31
+
32
+ describe('SvGridSelect', () => {
33
+ it('shows the placeholder when nothing is selected', () => {
34
+ const { target, destroy } = mountGs({ value: null, placeholder: 'Pick a person' })
35
+ try {
36
+ expect(target.querySelector('.sv-gs__value')!.textContent).toBe('Pick a person')
37
+ } finally { destroy() }
38
+ })
39
+
40
+ it('shows the selected row label (default = first column)', () => {
41
+ const { target, destroy } = mountGs({ value: 2 })
42
+ try {
43
+ expect(target.querySelector('.sv-gs__value')!.textContent).toBe('Alan Turing')
44
+ } finally { destroy() }
45
+ })
46
+
47
+ it('opens a portalled multi-column panel with a header and rows', () => {
48
+ const { target, destroy } = mountGs({ value: null })
49
+ try {
50
+ target.querySelector<HTMLButtonElement>('.sv-gs__trigger')!.click()
51
+ flushSync()
52
+ const heads = [...document.body.querySelectorAll('.sv-gs__hcell')].map((h) => h.textContent)
53
+ expect(heads).toEqual(['Name', 'Email', 'Role'])
54
+ expect(bodyRows()).toHaveLength(3)
55
+ } finally { destroy() }
56
+ })
57
+
58
+ it('filters rows via the search box', () => {
59
+ const { target, destroy } = mountGs({ value: null })
60
+ try {
61
+ target.querySelector<HTMLButtonElement>('.sv-gs__trigger')!.click()
62
+ flushSync()
63
+ const search = document.body.querySelector<HTMLInputElement>('.sv-gs__search')!
64
+ search.value = 'turing'
65
+ search.dispatchEvent(new Event('input', { bubbles: true }))
66
+ flushSync()
67
+ expect(bodyRows()).toHaveLength(1)
68
+ expect(bodyRows()[0]!.textContent).toContain('Alan Turing')
69
+ } finally { destroy() }
70
+ })
71
+
72
+ it('clicking a row emits (id, row) and closes', () => {
73
+ const onChange = vi.fn()
74
+ const { target, destroy } = mountGs({ value: null, onChange })
75
+ try {
76
+ target.querySelector<HTMLButtonElement>('.sv-gs__trigger')!.click()
77
+ flushSync()
78
+ bodyRows()[2]!.click() // Grace
79
+ flushSync()
80
+ expect(onChange).toHaveBeenCalledWith(3, people[2])
81
+ expect(document.body.querySelector('.sv-gs__panel')).toBeNull()
82
+ } finally { destroy() }
83
+ })
84
+
85
+ it('loads remote rows via loadOptions when opened', async () => {
86
+ const loadOptions = vi.fn(async () => [
87
+ { id: 9, name: 'Remote Person', email: 'r@x.io', role: 'Remote' },
88
+ ])
89
+ const { target, destroy } = mountGs({ value: null, loadOptions })
90
+ try {
91
+ target.querySelector<HTMLButtonElement>('.sv-gs__trigger')!.click()
92
+ flushSync()
93
+ expect(loadOptions).toHaveBeenCalledWith('')
94
+ await vi.waitFor(() => {
95
+ flushSync()
96
+ expect([...bodyRows()].map((r) => r.textContent).join()).toContain('Remote Person')
97
+ })
98
+ } finally { destroy() }
99
+ })
100
+ })
@@ -0,0 +1,109 @@
1
+ <!--
2
+ Built-in group cell for the server-side group/tree model. Drop it into the
3
+ grouping column and it draws the expander + indentation for group rows (and an
4
+ indented leaf value) - no hand-written cell recipe. Pair with `serverGroupRows`
5
+ to feed `<SvGrid data>`, and wire `onToggle` to the controller's `toggleGroup`.
6
+
7
+ cell: (ctx) => renderComponent(SvGroupCell, {
8
+ row: ctx.row.original, onToggle: ctl.toggleGroup, leafField: 'name',
9
+ })
10
+ -->
11
+ <script lang="ts" generics="TData">
12
+ import type { ServerDisplayRow } from './server-data-source'
13
+
14
+ type GroupGridRow = { __group?: ServerDisplayRow<TData> } & Record<string, unknown>
15
+ type Props = {
16
+ /** The grid row from `serverGroupRows` (carries the `__group` marker). */
17
+ row: GroupGridRow
18
+ /**
19
+ * Expand / collapse a group, or load more children. Receives the grid row;
20
+ * wire to `serverGroupNav(ctl).onToggle` (which drives clicks and keyboard).
21
+ */
22
+ onToggle: (row: GroupGridRow) => void
23
+ /** Field to show for leaf rows in the group column (blank if omitted). */
24
+ leafField?: string
25
+ /** Pixels of indentation per level. Default 18. */
26
+ indent?: number
27
+ }
28
+
29
+ let { row, onToggle, leafField, indent = 18 }: Props = $props()
30
+ const meta = $derived(row.__group)
31
+ </script>
32
+
33
+ {#if meta && meta.kind === 'group'}
34
+ <button
35
+ type="button"
36
+ class="sv-group-cell"
37
+ style={`padding-left: ${meta.level * indent}px`}
38
+ aria-expanded={meta.expanded}
39
+ aria-label={meta.expanded ? 'Collapse group' : 'Expand group'}
40
+ onclick={() => onToggle(row)}
41
+ >
42
+ <span class="sv-group-chev" class:open={meta.expanded} aria-hidden="true">▸</span>
43
+ <span class="sv-group-key">{meta.key}</span>
44
+ {#if meta.loading}<span class="sv-group-load" aria-hidden="true">…</span>{/if}
45
+ </button>
46
+ {:else if meta && meta.kind === 'more'}
47
+ <button
48
+ type="button"
49
+ class="sv-group-more"
50
+ style={`padding-left: ${meta.level * indent}px`}
51
+ disabled={meta.loading}
52
+ onclick={() => onToggle(row)}
53
+ >
54
+ {meta.loading ? 'Loading...' : `Load ${meta.remaining} more`}
55
+ </button>
56
+ {:else if meta && meta.kind === 'footer'}
57
+ <span class="sv-group-footer" style={`padding-left: ${meta.level * indent}px`}>Total</span>
58
+ {:else if meta && meta.kind === 'skeleton'}
59
+ <span class="sv-group-skeleton" style={`margin-left: ${meta.level * indent}px`} aria-hidden="true"></span>
60
+ {:else if meta}
61
+ <span class="sv-group-leaf" style={`padding-left: ${(meta.level + 1) * indent}px`}>
62
+ {leafField ? (row[leafField] ?? '') : ''}
63
+ </span>
64
+ {/if}
65
+
66
+ <style>
67
+ .sv-group-cell {
68
+ display: inline-flex;
69
+ align-items: center;
70
+ gap: 6px;
71
+ background: none;
72
+ border: 0;
73
+ padding: 0;
74
+ font: inherit;
75
+ color: inherit;
76
+ cursor: pointer;
77
+ }
78
+ .sv-group-chev {
79
+ display: inline-block;
80
+ transition: transform 0.15s;
81
+ color: var(--sg-muted, #64748b);
82
+ }
83
+ .sv-group-chev.open { transform: rotate(90deg); }
84
+ .sv-group-key { font-weight: 600; }
85
+ .sv-group-load { color: var(--sg-muted, #64748b); font-size: 12px; }
86
+ .sv-group-leaf { color: var(--sg-muted, #475569); }
87
+ .sv-group-more {
88
+ background: none;
89
+ border: 0;
90
+ padding: 0;
91
+ font: inherit;
92
+ font-size: 13px;
93
+ color: var(--sg-accent, #2563eb);
94
+ cursor: pointer;
95
+ }
96
+ .sv-group-more:disabled { color: var(--sg-muted, #64748b); cursor: default; }
97
+ .sv-group-footer { font-weight: 600; color: var(--sg-muted, #64748b); font-size: 12px; text-transform: uppercase; letter-spacing: 0.03em; }
98
+ .sv-group-skeleton {
99
+ display: inline-block;
100
+ width: 60%;
101
+ height: 10px;
102
+ border-radius: 5px;
103
+ background: linear-gradient(90deg, var(--sg-border, #e2e8f0) 25%, var(--sg-header-bg, #f1f5f9) 50%, var(--sg-border, #e2e8f0) 75%);
104
+ background-size: 200% 100%;
105
+ animation: sv-group-shimmer 1.2s infinite;
106
+ }
107
+ @keyframes sv-group-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
108
+ @media (prefers-reduced-motion: reduce) { .sv-group-skeleton { animation: none; } }
109
+ </style>
@@ -2,26 +2,55 @@
2
2
  /**
3
3
  * SvListBox - an inline single/multi-select list (WAI-ARIA listbox) with
4
4
  * roving highlight + full keyboard. Parity: Smart `smart-list-box`. Controlled
5
- * via `value` (scalar or array) + `onChange`.
5
+ * via `value` (scalar or array) + `onChange`. Scales to huge option sets with
6
+ * `virtual` (fixed-row windowing), and each row can be an `itemTemplate`.
6
7
  */
7
- import type { ListOption } from './list-option'
8
+ import { flushSync, type Snippet } from 'svelte'
9
+ import { groupOptions, hasGroups, type ListOption } from './list-option'
10
+ import { virtualRange, scrollToIndex } from './virtual'
8
11
  import { createListbox } from './createListbox.svelte'
12
+ import SvField from './SvField.svelte'
13
+ import { nextEditorId, type SvEditorProps } from './editor-contract'
9
14
 
10
- type Props = {
15
+ type Props = SvEditorProps & {
11
16
  options: ReadonlyArray<ListOption>
12
17
  value?: string | number | Array<string | number> | null
13
18
  onChange?: (value: any) => void
14
19
  multiple?: boolean
15
- disabled?: boolean
16
20
  /** Visible height in rows before scrolling. */
17
21
  rows?: number
18
- ariaLabel?: string
19
- /** Form field name; emits a hidden input per selected value so the listbox
20
- * posts in a native <form> (multiple selection -> repeated name). */
21
- name?: string
22
+ /** Window the list (render only visible rows) for large option sets. */
23
+ virtual?: boolean
24
+ /** Fixed row height in px (must match the CSS row height). Default 32. */
25
+ rowHeight?: number
26
+ /** Custom per-option content. Receives the option. */
27
+ itemTemplate?: Snippet<[ListOption]>
22
28
  }
23
29
 
24
- let { options, value = null, onChange, multiple = false, disabled = false, rows = 7, ariaLabel, name }: Props = $props()
30
+ let {
31
+ options,
32
+ value = null,
33
+ onChange,
34
+ multiple = false,
35
+ disabled = false,
36
+ rows = 7,
37
+ virtual = false,
38
+ rowHeight = 32,
39
+ itemTemplate,
40
+ ariaLabel,
41
+ name,
42
+ invalid = false,
43
+ required = false,
44
+ error,
45
+ label,
46
+ hint,
47
+ dir,
48
+ id,
49
+ }: Props = $props()
50
+
51
+ // Stable id anchors the label/hint/error wiring even when no `id` is passed.
52
+ const autoId = nextEditorId('sv-lb')
53
+ const uid = $derived(id ?? autoId)
25
54
 
26
55
  // The styled listbox is just a renderer over the headless core.
27
56
  const lb = createListbox({
@@ -31,40 +60,85 @@
31
60
  multiple: () => multiple,
32
61
  disabled: () => disabled,
33
62
  ariaLabel: () => ariaLabel,
63
+ id: () => uid,
64
+ invalid: () => invalid,
65
+ required: () => required,
66
+ error: () => error,
67
+ hint: () => hint,
34
68
  })
35
69
  const isSel = (o: ListOption) => lb.isSelected(o.value)
36
70
 
37
- let listEl: HTMLUListElement | null = null
38
- // Scroll-into-view is a render concern, so it lives here (not in the core).
71
+ // Virtualization only applies to a flat (ungrouped) list.
72
+ const useVirtual = $derived(virtual && !hasGroups(options))
73
+ let listEl = $state<HTMLUListElement | null>(null)
74
+ let scrollTop = $state(0)
75
+ let viewportH = $state(0)
76
+ const vr = $derived(
77
+ virtualRange({ scrollTop, viewportHeight: viewportH, rowHeight, count: options.length, overscan: 10 }),
78
+ )
79
+ const windowed = $derived(
80
+ useVirtual ? options.slice(vr.start, vr.end).map((o, i) => ({ ...o, index: vr.start + i })) : [],
81
+ )
82
+
83
+ // Keep the active option in view. Virtual mode drives scrollTop directly (the
84
+ // active row may not be in the DOM); otherwise use scrollIntoView.
39
85
  $effect(() => {
40
86
  const i = lb.activeIndex
41
- queueMicrotask(() => listEl?.querySelector<HTMLElement>(`[data-idx="${i}"]`)?.scrollIntoView({ block: 'nearest' }))
87
+ if (useVirtual) {
88
+ if (!listEl) return
89
+ const next = scrollToIndex(i, listEl.scrollTop, listEl.clientHeight, rowHeight)
90
+ if (next !== listEl.scrollTop) listEl.scrollTop = next
91
+ } else {
92
+ queueMicrotask(() => listEl?.querySelector<HTMLElement>(`[data-idx="${i}"]`)?.scrollIntoView({ block: 'nearest' }))
93
+ }
42
94
  })
43
95
  </script>
44
96
 
45
- <div style="display: contents">
46
- <ul
47
- bind:this={listEl}
48
- class="sv-listbox"
49
- style:--sv-rows={rows}
50
- {...lb.rootProps()}
51
- >
52
- {#each options as opt, i (opt.value)}
53
- <!-- svelte-ignore a11y_click_events_have_key_events a11y_interactive_supports_focus -->
54
- <li
55
- class="sv-listbox__opt"
56
- class:is-selected={isSel(opt)}
57
- class:is-active={lb.isActive(i)}
58
- class:is-disabled={opt.disabled}
59
- {...lb.optionProps(i)}
60
- >
61
- {#if multiple}<span class="sv-listbox__check" aria-hidden="true">{isSel(opt) ? '✓' : ''}</span>{/if}
62
- <span class="sv-listbox__label">{opt.label}</span>
63
- </li>
64
- {/each}
65
- </ul>
66
- {#if name}{#each lb.selectedValues as v (v)}<input type="hidden" {name} value={v} />{/each}{/if}
67
- </div>
97
+ <SvField id={uid} {label} {hint} {error} {required} {dir}>
98
+ <ul
99
+ bind:this={listEl}
100
+ class="sv-listbox"
101
+ class:is-invalid={invalid}
102
+ class:is-virtual={useVirtual}
103
+ style:--sv-rows={rows}
104
+ onscroll={(e) => { scrollTop = e.currentTarget.scrollTop; if (useVirtual) flushSync() }}
105
+ bind:clientHeight={viewportH}
106
+ {...lb.rootProps()}
107
+ >
108
+ {#if useVirtual}
109
+ <!-- Fixed-height sizer establishes the scroll range; rows are absolutely
110
+ positioned by transform so scrolling never reflows (no flash). -->
111
+ <li class="sv-listbox__sizer" aria-hidden="true" style:height={`${vr.totalHeight}px`}></li>
112
+ {#each windowed as opt (opt.value)}
113
+ {@render optionLi(opt, opt.index)}
114
+ {/each}
115
+ {:else}
116
+ {#each groupOptions(options) as g (g.group ?? ' ')}
117
+ {#if g.group != null}<li class="sv-listbox__group" role="presentation">{g.group}</li>{/if}
118
+ {#each g.options as opt (opt.value)}
119
+ {@render optionLi(opt, opt.index)}
120
+ {/each}
121
+ {/each}
122
+ {/if}
123
+ </ul>
124
+ {#if name}{#each lb.selectedValues as v (v)}<input type="hidden" {name} value={v} />{/each}{/if}
125
+ </SvField>
126
+
127
+ {#snippet optionLi(opt, index)}
128
+ <!-- svelte-ignore a11y_click_events_have_key_events a11y_interactive_supports_focus -->
129
+ <li
130
+ class="sv-listbox__opt"
131
+ class:is-selected={isSel(opt)}
132
+ class:is-active={lb.isActive(index)}
133
+ class:is-disabled={opt.disabled}
134
+ style:height={`${rowHeight}px`}
135
+ style:transform={useVirtual ? `translateY(${index * rowHeight}px)` : undefined}
136
+ {...lb.optionProps(index)}
137
+ >
138
+ {#if multiple}<span class="sv-listbox__check" aria-hidden="true">{isSel(opt) ? '✓' : ''}</span>{/if}
139
+ {#if itemTemplate}{@render itemTemplate(opt)}{:else}<span class="sv-listbox__label">{opt.label}</span>{/if}
140
+ </li>
141
+ {/snippet}
68
142
 
69
143
  <style>
70
144
  .sv-listbox {
@@ -76,14 +150,27 @@
76
150
  width: 220px; outline: none;
77
151
  }
78
152
  .sv-listbox:focus-visible { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
153
+ .sv-listbox.is-invalid { border-color: var(--sg-danger, #dc2626); }
154
+ .sv-listbox.is-invalid:focus-visible { box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-danger, #dc2626) 22%, transparent); }
155
+ /* Pre-promote the scrolling-contents layer at mount so the FIRST scroll
156
+ doesn't trigger a one-time layer promotion + repaint (the initial flash). */
157
+ .sv-listbox.is-virtual { position: relative; will-change: scroll-position; }
158
+ .sv-listbox__sizer { padding: 0; margin: 0; pointer-events: none; }
159
+ .sv-listbox.is-virtual .sv-listbox__opt {
160
+ position: absolute; inset-inline: 4px; top: 4px;
161
+ }
79
162
  .sv-listbox__opt {
80
- display: flex; align-items: center; gap: 8px; height: 32px; padding: 0 10px;
163
+ display: flex; align-items: center; gap: 8px; box-sizing: border-box; padding: 0 10px;
81
164
  border-radius: 6px; cursor: pointer; font-size: 13px;
82
165
  }
83
166
  .sv-listbox__opt.is-active { background: var(--sg-row-hover-bg, #f1f5f9); }
84
167
  .sv-listbox__opt.is-selected { background: color-mix(in srgb, var(--_accent) 14%, transparent); color: var(--_accent); font-weight: 600; }
85
168
  .sv-listbox__opt.is-selected.is-active { background: color-mix(in srgb, var(--_accent) 22%, transparent); }
86
169
  .sv-listbox__opt.is-disabled { opacity: 0.4; cursor: not-allowed; }
170
+ .sv-listbox__group {
171
+ padding: 8px 10px 3px; font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em;
172
+ color: var(--sg-muted, #94a3b8);
173
+ }
87
174
  .sv-listbox__check { width: 14px; text-align: center; color: var(--_accent); }
88
175
  .sv-listbox__label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
89
176
  </style>