@svgrid/grid 1.2.34 → 2.0.1

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 (444) hide show
  1. package/README.md +6 -5
  2. package/dist/SvAccordion.svelte +109 -0
  3. package/dist/SvAccordion.svelte.d.ts +28 -0
  4. package/dist/SvAlert.svelte +92 -0
  5. package/dist/SvAlert.svelte.d.ts +27 -0
  6. package/dist/SvAutoComplete.svelte +47 -18
  7. package/dist/SvAutoComplete.svelte.d.ts +2 -5
  8. package/dist/SvAvatar.svelte +75 -0
  9. package/dist/SvAvatar.svelte.d.ts +14 -0
  10. package/dist/SvAvatarGroup.svelte +48 -0
  11. package/dist/SvAvatarGroup.svelte.d.ts +16 -0
  12. package/dist/SvBadge.svelte +54 -0
  13. package/dist/SvBadge.svelte.d.ts +23 -0
  14. package/dist/SvBreadcrumb.svelte +88 -0
  15. package/dist/SvBreadcrumb.svelte.d.ts +23 -0
  16. package/dist/SvButtonGroup.svelte +121 -0
  17. package/dist/SvButtonGroup.svelte.d.ts +16 -0
  18. package/dist/SvCalendar.svelte +72 -9
  19. package/dist/SvCalendar.svelte.d.ts +34 -1
  20. package/dist/SvCard.svelte +71 -0
  21. package/dist/SvCard.svelte.d.ts +28 -0
  22. package/dist/SvCarousel.svelte +134 -0
  23. package/dist/SvCarousel.svelte.d.ts +28 -0
  24. package/dist/SvCheckBox.svelte +36 -21
  25. package/dist/SvCheckBox.svelte.d.ts +2 -5
  26. package/dist/SvChip.svelte +88 -0
  27. package/dist/SvChip.svelte.d.ts +31 -0
  28. package/dist/SvCircularProgress.svelte +109 -0
  29. package/dist/SvCircularProgress.svelte.d.ts +31 -0
  30. package/dist/SvColorInput.svelte +54 -27
  31. package/dist/SvColorInput.svelte.d.ts +10 -6
  32. package/dist/SvComboBox.svelte +119 -33
  33. package/dist/SvComboBox.svelte.d.ts +18 -6
  34. package/dist/SvCommand.svelte +197 -0
  35. package/dist/SvCommand.svelte.d.ts +26 -0
  36. package/dist/SvContextMenu.svelte +96 -0
  37. package/dist/SvContextMenu.svelte.d.ts +25 -0
  38. package/dist/SvCountryInput.svelte +56 -21
  39. package/dist/SvCountryInput.svelte.d.ts +9 -5
  40. package/dist/SvDateRangeInput.svelte +256 -0
  41. package/dist/SvDateRangeInput.svelte.d.ts +37 -0
  42. package/dist/SvDateTimePicker.svelte +57 -20
  43. package/dist/SvDateTimePicker.svelte.d.ts +12 -4
  44. package/dist/SvDivider.svelte +61 -0
  45. package/dist/SvDivider.svelte.d.ts +22 -0
  46. package/dist/SvDrawer.svelte +226 -0
  47. package/dist/SvDrawer.svelte.d.ts +43 -0
  48. package/dist/SvDropDownList.svelte +114 -35
  49. package/dist/SvDropDownList.svelte.d.ts +7 -6
  50. package/dist/SvDurationInput.svelte +108 -0
  51. package/dist/SvDurationInput.svelte.d.ts +15 -0
  52. package/dist/SvEmptyState.svelte +59 -0
  53. package/dist/SvEmptyState.svelte.d.ts +25 -0
  54. package/dist/SvField.svelte +85 -0
  55. package/dist/SvField.svelte.d.ts +30 -0
  56. package/dist/SvFileUpload.svelte +169 -0
  57. package/dist/SvFileUpload.svelte.d.ts +24 -0
  58. package/dist/SvForm.svelte +7 -1
  59. package/dist/SvForm.svelte.d.ts +3 -0
  60. package/dist/SvGauge.svelte +7 -1
  61. package/dist/SvGauge.svelte.d.ts +11 -0
  62. package/dist/SvGrid.controller.svelte.d.ts +6 -0
  63. package/dist/SvGrid.controller.svelte.js +64 -9
  64. package/dist/SvGrid.css +33 -0
  65. package/dist/SvGrid.svelte +116 -3
  66. package/dist/SvGrid.types.d.ts +366 -0
  67. package/dist/SvGridBoard.svelte +2281 -0
  68. package/dist/SvGridBoard.svelte.d.ts +31 -0
  69. package/dist/SvGridDropdown.svelte +2 -1
  70. package/dist/SvGridSelect.svelte +255 -0
  71. package/dist/SvGridSelect.svelte.d.ts +32 -0
  72. package/dist/SvGroupCell.svelte +109 -0
  73. package/dist/SvGroupCell.svelte.d.ts +41 -0
  74. package/dist/SvListBox.svelte +123 -36
  75. package/dist/SvListBox.svelte.d.ts +12 -8
  76. package/dist/SvMaskedInput.svelte +53 -13
  77. package/dist/SvMaskedInput.svelte.d.ts +16 -6
  78. package/dist/SvMenu.svelte +101 -0
  79. package/dist/SvMenu.svelte.d.ts +26 -0
  80. package/dist/SvMenuList.svelte +143 -0
  81. package/dist/SvMenuList.svelte.d.ts +24 -0
  82. package/dist/SvModal.svelte +220 -0
  83. package/dist/SvModal.svelte.d.ts +31 -0
  84. package/dist/SvMultiSelect.svelte +277 -0
  85. package/dist/SvMultiSelect.svelte.d.ts +31 -0
  86. package/dist/SvNavPane.svelte +314 -0
  87. package/dist/SvNavPane.svelte.d.ts +59 -0
  88. package/dist/SvNumberInput.svelte +23 -9
  89. package/dist/SvNumberInput.svelte.d.ts +2 -0
  90. package/dist/SvOtpInput.svelte +157 -0
  91. package/dist/SvOtpInput.svelte.d.ts +16 -0
  92. package/dist/SvPagination.svelte +100 -0
  93. package/dist/SvPagination.svelte.d.ts +16 -0
  94. package/dist/SvPasswordInput.svelte +42 -12
  95. package/dist/SvPasswordInput.svelte.d.ts +13 -6
  96. package/dist/SvPhoneInput.svelte +43 -11
  97. package/dist/SvPhoneInput.svelte.d.ts +10 -11
  98. package/dist/SvPopover.svelte +127 -0
  99. package/dist/SvPopover.svelte.d.ts +35 -0
  100. package/dist/SvProgress.svelte +116 -0
  101. package/dist/SvProgress.svelte.d.ts +32 -0
  102. package/dist/SvRadioGroup.svelte +40 -22
  103. package/dist/SvRadioGroup.svelte.d.ts +2 -10
  104. package/dist/SvRating.svelte +51 -30
  105. package/dist/SvRating.svelte.d.ts +10 -7
  106. package/dist/SvRichText.svelte +192 -0
  107. package/dist/SvRichText.svelte.d.ts +16 -0
  108. package/dist/SvRowGroupPanel.svelte +147 -0
  109. package/dist/SvRowGroupPanel.svelte.d.ts +17 -0
  110. package/dist/SvScrollArea.svelte +50 -0
  111. package/dist/SvScrollArea.svelte.d.ts +22 -0
  112. package/dist/SvSkeleton.svelte +76 -0
  113. package/dist/SvSkeleton.svelte.d.ts +25 -0
  114. package/dist/SvSlider.svelte +41 -9
  115. package/dist/SvSlider.svelte.d.ts +4 -4
  116. package/dist/SvSparkline.svelte +77 -0
  117. package/dist/SvSparkline.svelte.d.ts +28 -0
  118. package/dist/SvSplitter.svelte +138 -0
  119. package/dist/SvSplitter.svelte.d.ts +32 -0
  120. package/dist/SvStat.svelte +86 -0
  121. package/dist/SvStat.svelte.d.ts +30 -0
  122. package/dist/SvStepper.svelte +91 -0
  123. package/dist/SvStepper.svelte.d.ts +24 -0
  124. package/dist/SvSwitchButton.svelte +39 -24
  125. package/dist/SvSwitchButton.svelte.d.ts +2 -5
  126. package/dist/SvTabs.svelte +147 -28
  127. package/dist/SvTabs.svelte.d.ts +16 -4
  128. package/dist/SvTagsInput.svelte +59 -22
  129. package/dist/SvTagsInput.svelte.d.ts +9 -4
  130. package/dist/SvTextArea.svelte +113 -0
  131. package/dist/SvTextArea.svelte.d.ts +18 -0
  132. package/dist/SvTextInput.svelte +115 -0
  133. package/dist/SvTextInput.svelte.d.ts +20 -0
  134. package/dist/SvTimePicker.svelte +19 -5
  135. package/dist/SvTimePicker.svelte.d.ts +12 -0
  136. package/dist/SvTimeline.svelte +75 -0
  137. package/dist/SvTimeline.svelte.d.ts +24 -0
  138. package/dist/SvToaster.svelte +124 -0
  139. package/dist/SvToaster.svelte.d.ts +10 -0
  140. package/dist/SvToggleButton.svelte +37 -12
  141. package/dist/SvToggleButton.svelte.d.ts +2 -4
  142. package/dist/SvTooltip.svelte +113 -0
  143. package/dist/SvTooltip.svelte.d.ts +22 -0
  144. package/dist/SvTour.svelte +151 -0
  145. package/dist/SvTour.svelte.d.ts +25 -0
  146. package/dist/SvTree.svelte +286 -44
  147. package/dist/SvTree.svelte.d.ts +26 -9
  148. package/dist/SvTreeSelect.svelte +222 -0
  149. package/dist/SvTreeSelect.svelte.d.ts +24 -0
  150. package/dist/a11y/dismissable.d.ts +32 -0
  151. package/dist/a11y/dismissable.js +54 -0
  152. package/dist/a11y/focus-trap.d.ts +39 -0
  153. package/dist/a11y/focus-trap.js +142 -0
  154. package/dist/a11y/live-region.d.ts +8 -0
  155. package/dist/a11y/live-region.js +60 -0
  156. package/dist/a11y/scroll-lock.d.ts +4 -0
  157. package/dist/a11y/scroll-lock.js +41 -0
  158. package/dist/a11y.d.ts +3 -1
  159. package/dist/a11y.js +1 -1
  160. package/dist/avatar.d.ts +10 -0
  161. package/dist/avatar.js +22 -0
  162. package/dist/builtin-editors.d.ts +5 -0
  163. package/dist/builtin-editors.js +55 -0
  164. package/dist/cdn/svgrid.js +16021 -8234
  165. package/dist/cdn/svgrid.svelte-external.js +14542 -6764
  166. package/dist/columns.js +3 -2
  167. package/dist/core.d.ts +7 -1
  168. package/dist/countries.d.ts +12 -0
  169. package/dist/countries.js +24 -0
  170. package/dist/createAccordion.svelte.d.ts +71 -0
  171. package/dist/createAccordion.svelte.js +99 -0
  172. package/dist/createAutocomplete.svelte.d.ts +13 -4
  173. package/dist/createAutocomplete.svelte.js +11 -1
  174. package/dist/createButtonGroup.svelte.d.ts +62 -0
  175. package/dist/createButtonGroup.svelte.js +154 -0
  176. package/dist/createCalendar.svelte.d.ts +5 -0
  177. package/dist/createCalendar.svelte.js +2 -0
  178. package/dist/createCheckbox.svelte.d.ts +9 -17
  179. package/dist/createCheckbox.svelte.js +28 -1
  180. package/dist/createColorInput.svelte.d.ts +12 -22
  181. package/dist/createColorInput.svelte.js +13 -0
  182. package/dist/createCombobox.svelte.d.ts +16 -5
  183. package/dist/createCombobox.svelte.js +13 -2
  184. package/dist/createCountryInput.svelte.d.ts +14 -3
  185. package/dist/createCountryInput.svelte.js +12 -2
  186. package/dist/createDateTimePicker.svelte.d.ts +17 -2
  187. package/dist/createDateTimePicker.svelte.js +12 -2
  188. package/dist/createDropdownList.svelte.d.ts +14 -5
  189. package/dist/createDropdownList.svelte.js +50 -1
  190. package/dist/createFileUpload.svelte.d.ts +68 -0
  191. package/dist/createFileUpload.svelte.js +122 -0
  192. package/dist/createListbox.svelte.d.ts +10 -1
  193. package/dist/createListbox.svelte.js +48 -1
  194. package/dist/createMaskedInput.svelte.d.ts +11 -2
  195. package/dist/createMaskedInput.svelte.js +11 -1
  196. package/dist/createNumberInput.svelte.d.ts +1 -0
  197. package/dist/createNumberInput.svelte.js +1 -0
  198. package/dist/createPasswordInput.svelte.d.ts +19 -21
  199. package/dist/createPasswordInput.svelte.js +16 -4
  200. package/dist/createPhoneInput.svelte.d.ts +26 -7
  201. package/dist/createPhoneInput.svelte.js +24 -3
  202. package/dist/createPopoverSelect.svelte.d.ts +86 -0
  203. package/dist/createPopoverSelect.svelte.js +196 -0
  204. package/dist/createRadioGroup.svelte.d.ts +9 -0
  205. package/dist/createRadioGroup.svelte.js +11 -1
  206. package/dist/createRating.svelte.d.ts +11 -0
  207. package/dist/createRating.svelte.js +12 -1
  208. package/dist/createSlider.svelte.d.ts +2 -0
  209. package/dist/createSlider.svelte.js +16 -5
  210. package/dist/createSplitter.svelte.d.ts +67 -0
  211. package/dist/createSplitter.svelte.js +99 -0
  212. package/dist/createSwitch.svelte.d.ts +9 -16
  213. package/dist/createSwitch.svelte.js +28 -1
  214. package/dist/createTabs.svelte.d.ts +7 -0
  215. package/dist/createTabs.svelte.js +17 -2
  216. package/dist/createTagsInput.svelte.d.ts +13 -1
  217. package/dist/createTagsInput.svelte.js +12 -3
  218. package/dist/createToggle.svelte.d.ts +9 -0
  219. package/dist/createToggle.svelte.js +12 -1
  220. package/dist/createTree.svelte.d.ts +19 -0
  221. package/dist/createTree.svelte.js +108 -15
  222. package/dist/debounce.d.ts +11 -0
  223. package/dist/debounce.js +30 -0
  224. package/dist/duration.d.ts +15 -0
  225. package/dist/duration.js +51 -0
  226. package/dist/editor-contract.d.ts +62 -7
  227. package/dist/editor-contract.js +45 -5
  228. package/dist/editor-registry.d.ts +67 -0
  229. package/dist/editor-registry.js +38 -0
  230. package/dist/editors/cell-editors.d.ts +1 -1
  231. package/dist/editors/cell-editors.js +5 -0
  232. package/dist/fuzzy.d.ts +3 -0
  233. package/dist/fuzzy.js +41 -0
  234. package/dist/grid-select.d.ts +9 -0
  235. package/dist/grid-select.js +8 -0
  236. package/dist/index.d.ts +79 -6
  237. package/dist/index.js +78 -4
  238. package/dist/keyboard-handlers.js +19 -0
  239. package/dist/list-option.d.ts +36 -0
  240. package/dist/list-option.js +68 -0
  241. package/dist/otp.d.ts +16 -0
  242. package/dist/otp.js +26 -0
  243. package/dist/paginate.d.ts +20 -0
  244. package/dist/paginate.js +40 -0
  245. package/dist/popover.d.ts +15 -1
  246. package/dist/popover.js +29 -3
  247. package/dist/recurrence.d.ts +36 -0
  248. package/dist/recurrence.js +90 -0
  249. package/dist/server-data-source.d.ts +89 -0
  250. package/dist/server-data-source.js +4 -0
  251. package/dist/server-group-model.d.ts +98 -0
  252. package/dist/server-group-model.js +263 -0
  253. package/dist/toast-store.svelte.d.ts +41 -0
  254. package/dist/toast-store.svelte.js +113 -0
  255. package/dist/tree-select.d.ts +19 -0
  256. package/dist/tree-select.js +40 -0
  257. package/dist/ui-app.types.d.ts +80 -0
  258. package/dist/ui-app.types.js +7 -0
  259. package/dist/validators.d.ts +43 -0
  260. package/dist/validators.js +53 -0
  261. package/dist/virtual.d.ts +35 -0
  262. package/dist/virtual.js +34 -0
  263. package/package.json +1 -1
  264. package/src/SvAccordion.svelte +109 -0
  265. package/src/SvAccordion.test.ts +66 -0
  266. package/src/SvAlert.svelte +92 -0
  267. package/src/SvAutoComplete.svelte +47 -18
  268. package/src/SvAvatar.svelte +75 -0
  269. package/src/SvAvatarGroup.svelte +48 -0
  270. package/src/SvBadge.svelte +54 -0
  271. package/src/SvBreadcrumb.svelte +88 -0
  272. package/src/SvButtonGroup.svelte +121 -0
  273. package/src/SvButtonGroup.test.ts +72 -0
  274. package/src/SvCalendar.svelte +72 -9
  275. package/src/SvCard.svelte +71 -0
  276. package/src/SvCarousel.svelte +134 -0
  277. package/src/SvCheckBox.svelte +36 -21
  278. package/src/SvChip.svelte +88 -0
  279. package/src/SvCircularProgress.svelte +109 -0
  280. package/src/SvColorInput.svelte +54 -27
  281. package/src/SvComboBox.svelte +119 -33
  282. package/src/SvCommand.svelte +197 -0
  283. package/src/SvContextMenu.svelte +96 -0
  284. package/src/SvCountryInput.svelte +56 -21
  285. package/src/SvDateRangeInput.svelte +256 -0
  286. package/src/SvDateRangeInput.test.ts +80 -0
  287. package/src/SvDateTimePicker.svelte +57 -20
  288. package/src/SvDivider.svelte +61 -0
  289. package/src/SvDrawer.svelte +226 -0
  290. package/src/SvDrawer.test.ts +125 -0
  291. package/src/SvDropDownList.svelte +114 -35
  292. package/src/SvDurationInput.svelte +108 -0
  293. package/src/SvEmptyState.svelte +59 -0
  294. package/src/SvField.svelte +85 -0
  295. package/src/SvFileUpload.svelte +169 -0
  296. package/src/SvFileUpload.test.ts +59 -0
  297. package/src/SvForm.svelte +7 -1
  298. package/src/SvGauge.svelte +7 -1
  299. package/src/SvGrid.controller.svelte.ts +68 -9
  300. package/src/SvGrid.css +33 -0
  301. package/src/SvGrid.svelte +116 -3
  302. package/src/SvGrid.types.ts +374 -0
  303. package/src/SvGridBoard.svelte +2281 -0
  304. package/src/SvGridDropdown.svelte +2 -1
  305. package/src/SvGridSelect.svelte +255 -0
  306. package/src/SvGridSelect.test.ts +100 -0
  307. package/src/SvGroupCell.svelte +109 -0
  308. package/src/SvListBox.svelte +123 -36
  309. package/src/SvMaskedInput.svelte +53 -13
  310. package/src/SvMenu.svelte +101 -0
  311. package/src/SvMenu.test.ts +73 -0
  312. package/src/SvMenuList.svelte +143 -0
  313. package/src/SvModal.svelte +220 -0
  314. package/src/SvMultiSelect.svelte +277 -0
  315. package/src/SvMultiSelect.test.ts +167 -0
  316. package/src/SvNavPane.svelte +314 -0
  317. package/src/SvNavPane.test.ts +70 -0
  318. package/src/SvNumberInput.svelte +23 -9
  319. package/src/SvOtpInput.svelte +157 -0
  320. package/src/SvPagination.svelte +100 -0
  321. package/src/SvPasswordInput.svelte +42 -12
  322. package/src/SvPhoneInput.svelte +43 -11
  323. package/src/SvPopover.svelte +127 -0
  324. package/src/SvProgress.svelte +116 -0
  325. package/src/SvRadioGroup.svelte +40 -22
  326. package/src/SvRating.svelte +51 -30
  327. package/src/SvRichText.svelte +192 -0
  328. package/src/SvRowGroupPanel.svelte +147 -0
  329. package/src/SvScrollArea.svelte +50 -0
  330. package/src/SvSkeleton.svelte +76 -0
  331. package/src/SvSlider.svelte +41 -9
  332. package/src/SvSparkline.svelte +77 -0
  333. package/src/SvSplitter.svelte +138 -0
  334. package/src/SvSplitter.test.ts +55 -0
  335. package/src/SvStat.svelte +86 -0
  336. package/src/SvStepper.svelte +91 -0
  337. package/src/SvSwitchButton.svelte +39 -24
  338. package/src/SvTabs.svelte +147 -28
  339. package/src/SvTagsInput.svelte +59 -22
  340. package/src/SvTextArea.svelte +113 -0
  341. package/src/SvTextInput.svelte +115 -0
  342. package/src/SvTimePicker.svelte +19 -5
  343. package/src/SvTimeline.svelte +75 -0
  344. package/src/SvToaster.svelte +124 -0
  345. package/src/SvToaster.test.ts +63 -0
  346. package/src/SvToggleButton.svelte +37 -12
  347. package/src/SvTooltip.svelte +113 -0
  348. package/src/SvTour.svelte +151 -0
  349. package/src/SvTree.svelte +286 -44
  350. package/src/SvTreeSelect.svelte +222 -0
  351. package/src/SvTreeSelect.test.ts +87 -0
  352. package/src/a11y/dismissable.test.ts +95 -0
  353. package/src/a11y/dismissable.ts +94 -0
  354. package/src/a11y/focus-trap.test.ts +110 -0
  355. package/src/a11y/focus-trap.ts +134 -0
  356. package/src/a11y/live-region.test.ts +40 -0
  357. package/src/a11y/live-region.ts +65 -0
  358. package/src/a11y/scroll-lock.test.ts +38 -0
  359. package/src/a11y/scroll-lock.ts +41 -0
  360. package/src/a11y.ts +3 -1
  361. package/src/avatar.test.ts +32 -0
  362. package/src/avatar.ts +21 -0
  363. package/src/board.test.ts +927 -0
  364. package/src/builtin-editors.grid.test.ts +85 -0
  365. package/src/builtin-editors.test.ts +58 -0
  366. package/src/builtin-editors.ts +56 -0
  367. package/src/columns.ts +3 -2
  368. package/src/core.ts +10 -0
  369. package/src/countries.ts +25 -0
  370. package/src/createAccordion.svelte.ts +126 -0
  371. package/src/createAutocomplete.svelte.ts +18 -1
  372. package/src/createButtonGroup.svelte.ts +166 -0
  373. package/src/createCalendar.svelte.ts +6 -0
  374. package/src/createCheckbox.svelte.ts +23 -1
  375. package/src/createColorInput.svelte.ts +20 -0
  376. package/src/createCombobox.svelte.ts +22 -2
  377. package/src/createCountryInput.svelte.ts +21 -2
  378. package/src/createDateTimePicker.svelte.ts +23 -2
  379. package/src/createDropdownList.svelte.ts +31 -2
  380. package/src/createFileUpload.svelte.ts +144 -0
  381. package/src/createListbox.svelte.ts +35 -3
  382. package/src/createMaskedInput.svelte.ts +18 -1
  383. package/src/createNumberInput.svelte.ts +2 -0
  384. package/src/createPasswordInput.svelte.ts +31 -5
  385. package/src/createPhoneInput.svelte.ts +38 -4
  386. package/src/createPopoverSelect.svelte.ts +208 -0
  387. package/src/createRadioGroup.svelte.ts +23 -1
  388. package/src/createRating.svelte.ts +26 -1
  389. package/src/createSlider.svelte.ts +14 -3
  390. package/src/createSplitter.svelte.ts +119 -0
  391. package/src/createSwitch.svelte.ts +26 -3
  392. package/src/createTabs.svelte.ts +20 -3
  393. package/src/createTagsInput.svelte.ts +24 -3
  394. package/src/createToggle.svelte.ts +25 -2
  395. package/src/createTree.svelte.ts +104 -14
  396. package/src/debounce.test.ts +50 -0
  397. package/src/debounce.ts +41 -0
  398. package/src/duration.test.ts +41 -0
  399. package/src/duration.ts +51 -0
  400. package/src/editor-contract.ts +87 -8
  401. package/src/editor-registry.grid.test.ts +98 -0
  402. package/src/editor-registry.test.ts +86 -0
  403. package/src/editor-registry.ts +106 -0
  404. package/src/editors/cell-editors.ts +10 -1
  405. package/src/fuzzy.test.ts +40 -0
  406. package/src/fuzzy.ts +39 -0
  407. package/src/grid-select.test.ts +28 -0
  408. package/src/grid-select.ts +20 -0
  409. package/src/index.ts +150 -5
  410. package/src/keyboard-handlers.ts +20 -0
  411. package/src/list-option.ts +74 -0
  412. package/src/list-power.test.ts +71 -0
  413. package/src/otp.test.ts +28 -0
  414. package/src/otp.ts +28 -0
  415. package/src/overlays.test.ts +85 -0
  416. package/src/paginate.test.ts +41 -0
  417. package/src/paginate.ts +61 -0
  418. package/src/phone-valid.test.ts +26 -0
  419. package/src/popover.ts +31 -3
  420. package/src/progress.test.ts +55 -0
  421. package/src/recurrence.test.ts +91 -0
  422. package/src/recurrence.ts +111 -0
  423. package/src/server-data-source.ts +102 -0
  424. package/src/server-group-model.test.ts +294 -0
  425. package/src/server-group-model.ts +370 -0
  426. package/src/svgrid.responsive.test.ts +72 -0
  427. package/src/toast-store.svelte.ts +148 -0
  428. package/src/toast-store.test.ts +86 -0
  429. package/src/tree-edit.test.ts +49 -0
  430. package/src/tree-filter.test.ts +56 -0
  431. package/src/tree-select.test.ts +48 -0
  432. package/src/tree-select.ts +62 -0
  433. package/src/ui-app.test.ts +130 -0
  434. package/src/ui-app.types.ts +88 -0
  435. package/src/ui-display.test.ts +79 -0
  436. package/src/ui-inputs.test.ts +1 -1
  437. package/src/ui-status.test.ts +141 -0
  438. package/src/ui-tier1.test.ts +142 -0
  439. package/src/ui-tier2.test.ts +124 -0
  440. package/src/ui-tier3.test.ts +139 -0
  441. package/src/validators.test.ts +58 -0
  442. package/src/validators.ts +105 -0
  443. package/src/virtual.test.ts +72 -0
  444. package/src/virtual.ts +67 -0
@@ -14,17 +14,29 @@
14
14
  *
15
15
  * The styled <SvSwitchButton> is just one renderer over this core.
16
16
  */
17
+ import { editorAria, type EditorAriaState } from './editor-contract'
18
+
17
19
  export type SwitchConfig = {
18
20
  checked: () => boolean
19
21
  onChange?: (checked: boolean) => void
20
22
  disabled?: () => boolean
23
+ // Editor contract (ARIA + validation) - folded into switchProps().
24
+ id?: () => string | undefined
25
+ invalid?: () => boolean
26
+ required?: () => boolean
27
+ error?: () => string | undefined
28
+ hint?: () => string | undefined
21
29
  ariaLabel?: () => string | undefined
22
30
  }
23
31
 
24
32
  export type SwitchProps = {
25
33
  type: 'button'
26
34
  role: 'switch'
35
+ id: string | undefined
27
36
  'aria-checked': boolean
37
+ 'aria-invalid': 'true' | undefined
38
+ 'aria-required': 'true' | undefined
39
+ 'aria-describedby': string | undefined
28
40
  'aria-label': string | undefined
29
41
  disabled: boolean
30
42
  'data-checked': '' | undefined
@@ -59,18 +71,29 @@ export function createSwitch(config: SwitchConfig): Switch {
59
71
  else if (e.key === 'ArrowLeft') { if (checked()) config.onChange?.(false) }
60
72
  }
61
73
 
74
+ // The validation/label ARIA is derived from the editor contract state.
75
+ const ariaState = (): EditorAriaState => ({
76
+ id: config.id?.(),
77
+ invalid: config.invalid?.(),
78
+ required: config.required?.(),
79
+ error: config.error?.(),
80
+ hint: config.hint?.(),
81
+ ariaLabel: config.ariaLabel?.(),
82
+ })
83
+
62
84
  return {
63
85
  get checked() { return checked() },
64
86
  get disabled() { return disabled() },
65
87
  toggle,
66
88
  onKeydown,
67
89
  switchProps: () => ({
68
- type: 'button',
69
- role: 'switch',
90
+ type: 'button' as const,
91
+ role: 'switch' as const,
92
+ id: config.id?.(),
70
93
  'aria-checked': checked(),
71
- 'aria-label': config.ariaLabel?.(),
72
94
  disabled: disabled(),
73
95
  'data-checked': checked() ? '' : undefined,
96
+ ...editorAria(ariaState()),
74
97
  onclick: toggle,
75
98
  onkeydown: onKeydown,
76
99
  }),
@@ -23,7 +23,9 @@
23
23
  * a render concern, so the component watches `focusTick`/`focusId` and calls
24
24
  * `.focus()` itself; the core only decides *which* tab should take focus.
25
25
  */
26
- export type TabItem = { id: string; label: string; disabled?: boolean }
26
+ import type { EditorDir } from './editor-contract'
27
+
28
+ export type TabItem = { id: string; label: string; disabled?: boolean; closable?: boolean }
27
29
 
28
30
  export type TabsOrientation = 'horizontal' | 'vertical'
29
31
  export type TabsActivation = 'automatic' | 'manual'
@@ -33,8 +35,12 @@ export type TabsConfig = {
33
35
  tabs: () => ReadonlyArray<TabItem>
34
36
  value: () => string | undefined
35
37
  onChange?: (id: string) => void
38
+ /** Fired when a closable tab is closed (Delete key, or the renderer's x). */
39
+ onClose?: (id: string) => void
36
40
  orientation?: () => TabsOrientation
37
41
  activation?: () => TabsActivation
42
+ /** Text direction; under `'rtl'` horizontal Left/Right arrows flip. */
43
+ dir?: () => EditorDir | undefined
38
44
  ariaLabel?: () => string | undefined
39
45
  }
40
46
 
@@ -64,18 +70,28 @@ export function createTabs(config: TabsConfig) {
64
70
  config.onChange?.(tid)
65
71
  }
66
72
 
73
+ function close(tid: string) {
74
+ const t = tabs().find((x) => x.id === tid)
75
+ if (t?.closable) config.onClose?.(tid)
76
+ }
77
+
67
78
  function onKeydown(e: KeyboardEvent) {
68
79
  const list = enabled
69
80
  if (!list.length) return
70
81
  const idx = list.findIndex((t) => t.id === active)
71
- const fwd = orientation() === 'horizontal' ? 'ArrowRight' : 'ArrowDown'
72
- const back = orientation() === 'horizontal' ? 'ArrowLeft' : 'ArrowUp'
82
+ const horiz = orientation() === 'horizontal'
83
+ // Under horizontal RTL the Left/Right arrows swap so focus follows the
84
+ // visual order (Right moves to the previous tab).
85
+ const rtlH = horiz && config.dir?.() === 'rtl'
86
+ const fwd = horiz ? (rtlH ? 'ArrowLeft' : 'ArrowRight') : 'ArrowDown'
87
+ const back = horiz ? (rtlH ? 'ArrowRight' : 'ArrowLeft') : 'ArrowUp'
73
88
  let next: string | null = null
74
89
  if (e.key === fwd) next = list[(idx + 1) % list.length]?.id ?? null
75
90
  else if (e.key === back) next = list[(idx - 1 + list.length) % list.length]?.id ?? null
76
91
  else if (e.key === 'Home') next = list[0]?.id ?? null
77
92
  else if (e.key === 'End') next = list.at(-1)?.id ?? null
78
93
  else if ((e.key === 'Enter' || e.key === ' ') && activation() === 'manual') { e.preventDefault(); select(active); return }
94
+ else if (e.key === 'Delete') { e.preventDefault(); close(active); return }
79
95
  else return
80
96
  if (next) {
81
97
  e.preventDefault()
@@ -93,6 +109,7 @@ export function createTabs(config: TabsConfig) {
93
109
  get focusTick() { return focusTick },
94
110
  isActive: (tid: string) => tid === active,
95
111
  select,
112
+ close,
96
113
  onKeydown,
97
114
  /** Spread onto the tablist container element. */
98
115
  tablistProps: () => ({
@@ -19,6 +19,8 @@
19
19
  * ```
20
20
  */
21
21
 
22
+ import { editorAria, type EditorAriaState } from './editor-contract'
23
+
22
24
  /** Reactive inputs are passed as getters so the core tracks live prop changes. */
23
25
  export type TagsInputConfig = {
24
26
  value: () => string[]
@@ -28,6 +30,16 @@ export type TagsInputConfig = {
28
30
  unique?: () => boolean
29
31
  max?: () => number
30
32
  ariaLabel?: () => string | undefined
33
+ /** Accessible label for the draft input (localizable). */
34
+ addLabel?: () => string | undefined
35
+ /** Accessible label prefix for a chip's remove button (localizable). */
36
+ removeLabel?: () => string | undefined
37
+ // Editor contract (ARIA + validation) - folded into rootProps().
38
+ id?: () => string | undefined
39
+ invalid?: () => boolean
40
+ required?: () => boolean
41
+ error?: () => string | undefined
42
+ hint?: () => string | undefined
31
43
  }
32
44
 
33
45
  export function createTagsInput(config: TagsInputConfig) {
@@ -36,6 +48,15 @@ export function createTagsInput(config: TagsInputConfig) {
36
48
  const max = () => config.max?.() ?? Infinity
37
49
  const tags = () => config.value()
38
50
 
51
+ const ariaState = (): EditorAriaState => ({
52
+ id: config.id?.(),
53
+ invalid: config.invalid?.(),
54
+ required: config.required?.(),
55
+ error: config.error?.(),
56
+ hint: config.hint?.(),
57
+ ariaLabel: config.ariaLabel?.(),
58
+ })
59
+
39
60
  let draft = $state('')
40
61
 
41
62
  function add(raw: string) {
@@ -70,14 +91,14 @@ export function createTagsInput(config: TagsInputConfig) {
70
91
  /** Spread onto the container element. */
71
92
  rootProps: () => ({
72
93
  role: 'group' as const,
73
- 'aria-label': config.ariaLabel?.(),
94
+ ...editorAria(ariaState()),
74
95
  }),
75
96
  /** Spread onto the text <input>. */
76
97
  inputProps: () => ({
77
98
  type: 'text' as const,
78
99
  value: draft,
79
100
  disabled: disabled(),
80
- 'aria-label': config.ariaLabel?.() ?? 'Add tag',
101
+ 'aria-label': config.addLabel?.() ?? config.ariaLabel?.() ?? 'Add tag',
81
102
  oninput: onInput,
82
103
  onkeydown: onKeydown,
83
104
  onblur: onBlur,
@@ -89,7 +110,7 @@ export function createTagsInput(config: TagsInputConfig) {
89
110
  /** Spread onto a chip's remove <button> at `index`. */
90
111
  removeProps: (index: number) => ({
91
112
  type: 'button' as const,
92
- 'aria-label': `Remove ${tags()[index] ?? ''}`,
113
+ 'aria-label': `${config.removeLabel?.() ?? 'Remove'} ${tags()[index] ?? ''}`.trim(),
93
114
  onclick: () => removeAt(index),
94
115
  }),
95
116
  }
@@ -16,18 +16,30 @@
16
16
  * The styled <SvToggleButton> is just one renderer over this core.
17
17
  */
18
18
 
19
+ import { editorAria, type EditorAriaState } from './editor-contract'
20
+
19
21
  /** Reactive inputs are passed as getters so the core tracks live prop changes
20
22
  * (the same controlled pattern used across the SvGrid UI kit). */
21
23
  export type ToggleConfig = {
22
24
  pressed: () => boolean
23
25
  onChange?: (pressed: boolean) => void
24
26
  disabled?: () => boolean
27
+ // Editor contract (ARIA + validation) - folded into buttonProps().
28
+ id?: () => string | undefined
29
+ invalid?: () => boolean
30
+ required?: () => boolean
31
+ error?: () => string | undefined
32
+ hint?: () => string | undefined
25
33
  ariaLabel?: () => string | undefined
26
34
  }
27
35
 
28
36
  export type ToggleButtonProps = {
29
37
  type: 'button'
38
+ id: string | undefined
30
39
  'aria-pressed': boolean
40
+ 'aria-invalid': 'true' | undefined
41
+ 'aria-required': 'true' | undefined
42
+ 'aria-describedby': string | undefined
31
43
  'aria-label': string | undefined
32
44
  disabled: boolean
33
45
  'data-pressed': '' | undefined
@@ -53,16 +65,27 @@ export function createToggle(config: ToggleConfig): Toggle {
53
65
  config.onChange?.(!pressed())
54
66
  }
55
67
 
68
+ // The validation/label ARIA is derived from the editor contract state.
69
+ const ariaState = (): EditorAriaState => ({
70
+ id: config.id?.(),
71
+ invalid: config.invalid?.(),
72
+ required: config.required?.(),
73
+ error: config.error?.(),
74
+ hint: config.hint?.(),
75
+ ariaLabel: config.ariaLabel?.(),
76
+ })
77
+
56
78
  return {
57
79
  get pressed() { return pressed() },
58
80
  get disabled() { return disabled() },
59
81
  toggle,
60
82
  buttonProps: () => ({
61
- type: 'button',
83
+ type: 'button' as const,
84
+ id: config.id?.(),
62
85
  'aria-pressed': pressed(),
63
- 'aria-label': config.ariaLabel?.(),
64
86
  disabled: disabled(),
65
87
  'data-pressed': pressed() ? '' : undefined,
88
+ ...editorAria(ariaState()),
66
89
  onclick: toggle,
67
90
  }),
68
91
  }
@@ -26,6 +26,9 @@ export type TreeNode = {
26
26
  label: string
27
27
  children?: TreeNode[]
28
28
  disabled?: boolean
29
+ /** Marks a node whose children are loaded on demand (shows an expand arrow
30
+ * even with no children yet; the renderer loads them on first expand). */
31
+ lazy?: boolean
29
32
  }
30
33
 
31
34
  export type CheckState = 'checked' | 'indeterminate' | 'unchecked'
@@ -66,6 +69,64 @@ function findNode(list: ReadonlyArray<TreeNode>, id: string): TreeNode | null {
66
69
  return null
67
70
  }
68
71
 
72
+ /** Where a dragged node lands relative to the drop target. */
73
+ export type TreeDropPosition = 'before' | 'after' | 'inside'
74
+
75
+ /** True if `id` is inside `node`'s subtree (used to block invalid drops). */
76
+ export function treeContains(node: TreeNode, id: string): boolean {
77
+ return node.children?.some((c) => c.id === id || treeContains(c, id)) ?? false
78
+ }
79
+
80
+ /**
81
+ * Return a NEW node tree with `dragId` moved before/after/inside `targetId`
82
+ * (pure - the caller sets it back as the controlled `nodes`). No-ops on invalid
83
+ * moves (self, or into own descendant).
84
+ */
85
+ export function moveTreeNode(
86
+ nodes: ReadonlyArray<TreeNode>,
87
+ dragId: string,
88
+ targetId: string,
89
+ position: TreeDropPosition,
90
+ ): TreeNode[] {
91
+ if (dragId === targetId) return [...nodes]
92
+ const dragged = findNode(nodes, dragId)
93
+ if (!dragged) return [...nodes]
94
+ if (dragId === targetId || treeContains(dragged, targetId)) return [...nodes]
95
+
96
+ const without = (list: ReadonlyArray<TreeNode>): TreeNode[] => {
97
+ const out: TreeNode[] = []
98
+ for (const n of list) {
99
+ if (n.id === dragId) continue
100
+ out.push(n.children ? { ...n, children: without(n.children) } : n)
101
+ }
102
+ return out
103
+ }
104
+ const insert = (list: ReadonlyArray<TreeNode>): TreeNode[] => {
105
+ const out: TreeNode[] = []
106
+ for (const n of list) {
107
+ if (n.id === targetId) {
108
+ if (position === 'before') { out.push(dragged, n) }
109
+ else if (position === 'after') { out.push(n, dragged) }
110
+ else out.push({ ...n, children: [...(n.children ?? []), dragged] })
111
+ } else if (n.children) {
112
+ out.push({ ...n, children: insert(n.children) })
113
+ } else out.push(n)
114
+ }
115
+ return out
116
+ }
117
+ return insert(without(nodes))
118
+ }
119
+
120
+ /** Sort siblings (recursively) by a comparator (pure). */
121
+ export function sortTreeNodes(
122
+ nodes: ReadonlyArray<TreeNode>,
123
+ compare: (a: TreeNode, b: TreeNode) => number,
124
+ ): TreeNode[] {
125
+ return [...nodes]
126
+ .sort(compare)
127
+ .map((n) => (n.children ? { ...n, children: sortTreeNodes(n.children, compare) } : n))
128
+ }
129
+
69
130
  /** Reactive inputs are passed as getters so the core tracks live prop changes. */
70
131
  export type TreeConfig = {
71
132
  nodes: () => ReadonlyArray<TreeNode>
@@ -78,6 +139,10 @@ export type TreeConfig = {
78
139
  checked?: () => string[]
79
140
  onCheck?: (ids: string[]) => void
80
141
  ariaLabel?: () => string | undefined
142
+ /** Text direction; under `'rtl'` the Left/Right expand/collapse keys swap. */
143
+ dir?: () => import('./editor-contract').EditorDir | undefined
144
+ /** Filter query: show only matching nodes + their ancestors, auto-expanded. */
145
+ filter?: () => string | undefined
81
146
  }
82
147
 
83
148
  export function createTree(config: TreeConfig) {
@@ -92,14 +157,34 @@ export function createTree(config: TreeConfig) {
92
157
  if (!seeded) { seeded = true; const ids = config.expandedIds?.(); if (ids) expanded = new Set(ids) }
93
158
  })
94
159
 
160
+ // When filtering, precompute which nodes to show (a match, or an ancestor of a
161
+ // match) and which to force-open so matches are revealed.
162
+ const filterInfo = $derived.by(() => {
163
+ const q = config.filter?.()?.trim().toLowerCase()
164
+ if (!q) return null
165
+ const visible = new Set<string>()
166
+ const forceOpen = new Set<string>()
167
+ const walk = (node: TreeNode): boolean => {
168
+ let childMatch = false
169
+ for (const c of node.children ?? []) childMatch = walk(c) || childMatch
170
+ const selfMatch = node.label.toLowerCase().includes(q)
171
+ if (selfMatch || childMatch) { visible.add(node.id); if (childMatch) forceOpen.add(node.id) }
172
+ return selfMatch || childMatch
173
+ }
174
+ for (const n of nodes()) walk(n)
175
+ return { visible, forceOpen }
176
+ })
177
+
95
178
  const rows = $derived.by<TreeRow[]>(() => {
96
179
  const out: TreeRow[] = []
180
+ const fi = filterInfo
97
181
  const walk = (list: ReadonlyArray<TreeNode>, depth: number, parentId: string | null) => {
98
182
  for (const node of list) {
99
- const hasChildren = !!node.children?.length
100
- const open = expanded.has(node.id)
183
+ if (fi && !fi.visible.has(node.id)) continue
184
+ const hasChildren = !!node.children?.length || !!node.lazy
185
+ const open = fi ? fi.forceOpen.has(node.id) || expanded.has(node.id) : expanded.has(node.id)
101
186
  out.push({ node, depth, hasChildren, open, parentId, index: out.length })
102
- if (hasChildren && open) walk(node.children!, depth + 1, node.id)
187
+ if (open && node.children?.length) walk(node.children, depth + 1, node.id)
103
188
  }
104
189
  }
105
190
  walk(nodes(), 0, null)
@@ -120,7 +205,9 @@ export function createTree(config: TreeConfig) {
120
205
  const isSelected = (id: string) => id === selected()
121
206
 
122
207
  function toggleExpand(node: TreeNode) {
123
- if (!node.children?.length) return
208
+ // Lazy nodes have no children *yet* - they must still be expandable so the
209
+ // toggle fires onToggle and the host can fetch children on demand.
210
+ if (!node.children?.length && !node.lazy) return
124
211
  const next = new Set(expanded)
125
212
  const willOpen = !next.has(node.id)
126
213
  willOpen ? next.add(node.id) : next.delete(node.id)
@@ -147,19 +234,22 @@ export function createTree(config: TreeConfig) {
147
234
  function onKeydown(e: KeyboardEvent) {
148
235
  const item = rows[active]
149
236
  if (!item) return
237
+ // "expand" opens the node or steps into the first child; "collapse" closes it
238
+ // or jumps to the parent. Under RTL the physical Left/Right keys are swapped.
239
+ const expand = () => {
240
+ if (item.hasChildren && !item.open) toggleExpand(item.node)
241
+ else if (item.hasChildren && item.open) setActive(Math.min(active + 1, rows.length - 1), true)
242
+ }
243
+ const collapse = () => {
244
+ if (item.hasChildren && item.open) toggleExpand(item.node)
245
+ else if (item.parentId) { const pi = rows.findIndex((r) => r.node.id === item.parentId); if (pi >= 0) setActive(pi, true) }
246
+ }
247
+ const rtl = config.dir?.() === 'rtl'
150
248
  switch (e.key) {
151
249
  case 'ArrowDown': e.preventDefault(); setActive(Math.min(active + 1, rows.length - 1), true); break
152
250
  case 'ArrowUp': e.preventDefault(); setActive(Math.max(active - 1, 0), true); break
153
- case 'ArrowRight':
154
- e.preventDefault()
155
- if (item.hasChildren && !item.open) toggleExpand(item.node)
156
- else if (item.hasChildren && item.open) setActive(Math.min(active + 1, rows.length - 1), true)
157
- break
158
- case 'ArrowLeft':
159
- e.preventDefault()
160
- if (item.hasChildren && item.open) toggleExpand(item.node)
161
- else if (item.parentId) { const pi = rows.findIndex((r) => r.node.id === item.parentId); if (pi >= 0) setActive(pi, true) }
162
- break
251
+ case 'ArrowRight': e.preventDefault(); (rtl ? collapse : expand)(); break
252
+ case 'ArrowLeft': e.preventDefault(); (rtl ? expand : collapse)(); break
163
253
  case 'Enter': e.preventDefault(); select(item.node); break
164
254
  case ' ': e.preventDefault(); checkable() ? toggleCheck(item.node) : select(item.node); break
165
255
  case 'Home': e.preventDefault(); setActive(0, true); break
@@ -0,0 +1,50 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { debounce } from './debounce'
3
+
4
+ beforeEach(() => vi.useFakeTimers())
5
+ afterEach(() => vi.useRealTimers())
6
+
7
+ describe('debounce', () => {
8
+ it('runs once after the quiet period with the latest args', () => {
9
+ const fn = vi.fn()
10
+ const d = debounce(fn, 200)
11
+ d('a'); d('b'); d('c')
12
+ expect(fn).not.toHaveBeenCalled()
13
+ vi.advanceTimersByTime(199)
14
+ expect(fn).not.toHaveBeenCalled()
15
+ vi.advanceTimersByTime(1)
16
+ expect(fn).toHaveBeenCalledTimes(1)
17
+ expect(fn).toHaveBeenCalledWith('c')
18
+ })
19
+
20
+ it('resets the timer on each call', () => {
21
+ const fn = vi.fn()
22
+ const d = debounce(fn, 200)
23
+ d('x')
24
+ vi.advanceTimersByTime(150)
25
+ d('y') // restarts the 200ms window
26
+ vi.advanceTimersByTime(150)
27
+ expect(fn).not.toHaveBeenCalled()
28
+ vi.advanceTimersByTime(50)
29
+ expect(fn).toHaveBeenCalledExactlyOnceWith('y')
30
+ })
31
+
32
+ it('cancel() drops the pending call', () => {
33
+ const fn = vi.fn()
34
+ const d = debounce(fn, 200)
35
+ d('a')
36
+ d.cancel()
37
+ vi.advanceTimersByTime(500)
38
+ expect(fn).not.toHaveBeenCalled()
39
+ })
40
+
41
+ it('flush() runs the pending call immediately', () => {
42
+ const fn = vi.fn()
43
+ const d = debounce(fn, 200)
44
+ d('a')
45
+ d.flush()
46
+ expect(fn).toHaveBeenCalledExactlyOnceWith('a')
47
+ vi.advanceTimersByTime(500)
48
+ expect(fn).toHaveBeenCalledTimes(1) // not called again
49
+ })
50
+ })
@@ -0,0 +1,41 @@
1
+ /**
2
+ * debounce - coalesce rapid calls into one trailing call after `ms` of quiet.
3
+ * Used for remote search in the async dropdowns (SvMultiSelect/GridSelect/...),
4
+ * but general-purpose. Returns the debounced function with `cancel()` (drop the
5
+ * pending call) and `flush()` (run it now). Framework-free + pure.
6
+ */
7
+ export type Debounced<A extends unknown[]> = ((...args: A) => void) & {
8
+ cancel: () => void
9
+ flush: () => void
10
+ }
11
+
12
+ export function debounce<A extends unknown[]>(fn: (...args: A) => void, ms: number): Debounced<A> {
13
+ let timer: ReturnType<typeof setTimeout> | null = null
14
+ let pending: A | null = null
15
+
16
+ const run = () => {
17
+ timer = null
18
+ const args = pending
19
+ pending = null
20
+ if (args) fn(...args)
21
+ }
22
+
23
+ const debounced = ((...args: A) => {
24
+ pending = args
25
+ if (timer) clearTimeout(timer)
26
+ timer = setTimeout(run, ms)
27
+ }) as Debounced<A>
28
+
29
+ debounced.cancel = () => {
30
+ if (timer) clearTimeout(timer)
31
+ timer = null
32
+ pending = null
33
+ }
34
+ debounced.flush = () => {
35
+ if (timer) {
36
+ clearTimeout(timer)
37
+ run()
38
+ }
39
+ }
40
+ return debounced
41
+ }
@@ -0,0 +1,41 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { parseDuration, formatDuration } from './duration'
3
+
4
+ describe('parseDuration', () => {
5
+ it('parses h:mm', () => {
6
+ expect(parseDuration('1:30')).toBe(90)
7
+ expect(parseDuration('0:05')).toBe(5)
8
+ })
9
+ it('rejects invalid minutes in h:mm', () => {
10
+ expect(parseDuration('1:75')).toBeNull()
11
+ })
12
+ it('parses unit form incl. decimals', () => {
13
+ expect(parseDuration('1h 30m')).toBe(90)
14
+ expect(parseDuration('1.5h')).toBe(90)
15
+ expect(parseDuration('45m')).toBe(45)
16
+ })
17
+ it('treats a bare number as minutes', () => {
18
+ expect(parseDuration('90')).toBe(90)
19
+ })
20
+ it('returns null for empty / garbage', () => {
21
+ expect(parseDuration('')).toBeNull()
22
+ expect(parseDuration('abc')).toBeNull()
23
+ expect(parseDuration('1h banana')).toBeNull()
24
+ })
25
+ })
26
+
27
+ describe('formatDuration', () => {
28
+ it('formats colon style', () => {
29
+ expect(formatDuration(90)).toBe('1:30')
30
+ expect(formatDuration(5)).toBe('0:05')
31
+ })
32
+ it('formats unit style', () => {
33
+ expect(formatDuration(90, 'units')).toBe('1h 30m')
34
+ expect(formatDuration(60, 'units')).toBe('1h')
35
+ expect(formatDuration(45, 'units')).toBe('45m')
36
+ })
37
+ it('returns empty for invalid input', () => {
38
+ expect(formatDuration(-1)).toBe('')
39
+ expect(formatDuration(NaN)).toBe('')
40
+ })
41
+ })
@@ -0,0 +1,51 @@
1
+ /**
2
+ * duration - pure parse/format helpers behind <SvDurationInput>. The canonical
3
+ * value is a number of MINUTES; these convert to/from the human forms people
4
+ * type ("1h 30m", "1:30", "90"). Framework-free + pure so they are unit-tested
5
+ * directly.
6
+ */
7
+
8
+ /**
9
+ * Parse a human duration to minutes, or `null` if unparseable. Accepts:
10
+ * - `h:mm` e.g. "1:30" -> 90
11
+ * - unit form e.g. "1h 30m", "1.5h", "45m" -> 90 / 90 / 45
12
+ * - a bare number e.g. "90" -> 90 (treated as minutes)
13
+ */
14
+ export function parseDuration(input: string): number | null {
15
+ const s = input.trim().toLowerCase()
16
+ if (!s) return null
17
+
18
+ const colon = s.match(/^(\d+):(\d{1,2})$/)
19
+ if (colon) {
20
+ const mm = parseInt(colon[2]!, 10)
21
+ if (mm > 59) return null
22
+ return parseInt(colon[1]!, 10) * 60 + mm
23
+ }
24
+
25
+ const units = s.match(/\d+(?:\.\d+)?\s*[hm]/g)
26
+ if (units && units.join('').replace(/\s/g, '').length === s.replace(/\s/g, '').length) {
27
+ let total = 0
28
+ for (const part of units) {
29
+ const m = part.match(/(\d+(?:\.\d+)?)\s*([hm])/)!
30
+ const n = parseFloat(m[1]!)
31
+ total += m[2] === 'h' ? n * 60 : n
32
+ }
33
+ return Math.round(total)
34
+ }
35
+
36
+ const num = Number(s)
37
+ return Number.isFinite(num) ? Math.round(num) : null
38
+ }
39
+
40
+ /** Format minutes as `h:mm` (colon) or `Nh Nm` (units). Empty for invalid input. */
41
+ export function formatDuration(minutes: number, style: 'colon' | 'units' = 'colon'): string {
42
+ if (!Number.isFinite(minutes) || minutes < 0) return ''
43
+ const h = Math.floor(minutes / 60)
44
+ const m = Math.round(minutes % 60)
45
+ if (style === 'units') {
46
+ if (h && m) return `${h}h ${m}m`
47
+ if (h) return `${h}h`
48
+ return `${m}m`
49
+ }
50
+ return `${h}:${String(m).padStart(2, '0')}`
51
+ }