@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
@@ -0,0 +1,314 @@
1
+ <script lang="ts" module>
2
+ import type { Snippet } from 'svelte'
3
+ export type NavItem = {
4
+ id: string
5
+ label: string
6
+ icon?: Snippet
7
+ /** Right-aligned count/badge (e.g. unread). */
8
+ badge?: string | number
9
+ disabled?: boolean
10
+ /** Nested items (one level of sub-items, indented). */
11
+ children?: NavItem[]
12
+ }
13
+ export type NavSection = {
14
+ id: string
15
+ label?: string
16
+ items: NavItem[]
17
+ /** Show a collapse chevron on the section header. Default true when labelled. */
18
+ collapsible?: boolean
19
+ }
20
+ /** A bottom "module" button (Outlook-style: Mail / Calendar / People / Tasks). */
21
+ export type NavModule = { id: string; label: string; icon?: Snippet; badge?: string | number }
22
+ </script>
23
+
24
+ <script lang="ts">
25
+ /**
26
+ * SvNavPane - an Outlook-style navigation pane: collapsible sections, items with
27
+ * icons and badge counts, one level of nested sub-items, single-select
28
+ * highlight, arrow-key navigation, and an icon-only `collapsed` rail. A common
29
+ * app shell primitive (mail folders, workspace nav, settings).
30
+ */
31
+ import { type EditorDir } from './editor-contract'
32
+
33
+ let {
34
+ sections,
35
+ value = $bindable<string | null>(null),
36
+ onSelect,
37
+ expandedSections = $bindable<string[] | undefined>(undefined),
38
+ expandedItems = $bindable<string[]>([]),
39
+ collapsed = false,
40
+ ariaLabel = 'Navigation',
41
+ dir,
42
+ modules,
43
+ moduleValue = $bindable<string | null>(null),
44
+ onModuleSelect,
45
+ moduleRows = $bindable<number | undefined>(undefined),
46
+ height,
47
+ }: {
48
+ sections: ReadonlyArray<NavSection>
49
+ value?: string | null
50
+ onSelect?: (id: string) => void
51
+ /** Open section ids (uncontrolled default = all open). */
52
+ expandedSections?: string[]
53
+ /** Open nested-item ids. */
54
+ expandedItems?: string[]
55
+ /** Icon-only rail (labels hidden). */
56
+ collapsed?: boolean
57
+ ariaLabel?: string
58
+ dir?: EditorDir
59
+ /** Outlook-style bottom module buttons (Mail / Calendar / ...). */
60
+ modules?: ReadonlyArray<NavModule>
61
+ /** Selected module id. */
62
+ moduleValue?: string | null
63
+ onModuleSelect?: (id: string) => void
64
+ /** How many modules show as full labelled rows; the rest collapse to an icon
65
+ * rail. Draggable via the splitter. Default: all. */
66
+ moduleRows?: number
67
+ /** Pane height in px - lets the splitter redistribute space (recommended
68
+ * when `modules` is set). */
69
+ height?: number
70
+ } = $props()
71
+
72
+ const resolvedDir = $derived(dir === 'ltr' || dir === 'rtl' ? dir : undefined)
73
+ // Default: every section open.
74
+ const openSections = $derived(new Set(expandedSections ?? sections.map((s) => s.id)))
75
+ const openItems = $derived(new Set(expandedItems))
76
+
77
+ function toggleSection(id: string) {
78
+ const next = new Set(openSections)
79
+ next.has(id) ? next.delete(id) : next.add(id)
80
+ expandedSections = [...next]
81
+ }
82
+ function toggleItem(id: string) {
83
+ const next = new Set(openItems)
84
+ next.has(id) ? next.delete(id) : next.add(id)
85
+ expandedItems = [...next]
86
+ }
87
+ function select(item: NavItem) {
88
+ if (item.disabled) return
89
+ if (item.children?.length) { toggleItem(item.id); return }
90
+ value = item.id
91
+ onSelect?.(item.id)
92
+ }
93
+
94
+ let rootEl = $state<HTMLElement | null>(null)
95
+ function onItemKey(e: KeyboardEvent) {
96
+ if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return
97
+ e.preventDefault()
98
+ const focusables = [...(rootEl?.querySelectorAll<HTMLElement>('[data-nav]:not([disabled])') ?? [])]
99
+ const i = focusables.indexOf(document.activeElement as HTMLElement)
100
+ const next = focusables[(i + (e.key === 'ArrowDown' ? 1 : -1) + focusables.length) % focusables.length]
101
+ next?.focus()
102
+ }
103
+
104
+ // --- Bottom module strip + Outlook-style resize splitter -------------------
105
+ const moduleCount = $derived(modules?.length ?? 0)
106
+ const visibleModules = $derived(Math.max(0, Math.min(moduleCount, moduleRows ?? moduleCount)))
107
+ const fullModules = $derived(modules?.slice(0, visibleModules) ?? [])
108
+ const railModules = $derived(modules?.slice(visibleModules) ?? [])
109
+ const MODULE_ROW = 40
110
+
111
+ let splitActive = false
112
+ let splitStart = { y: 0, count: 0 }
113
+ function setRows(n: number) { moduleRows = Math.max(0, Math.min(moduleCount, n)) }
114
+ function onSplitDown(e: PointerEvent) {
115
+ splitActive = true
116
+ splitStart = { y: e.clientY, count: visibleModules }
117
+ ;(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId)
118
+ }
119
+ function onSplitMove(e: PointerEvent) {
120
+ if (!splitActive) return
121
+ // Outlook behaviour: dragging the splitter UP enlarges the module strip
122
+ // (more full labelled rows); dragging DOWN collapses rows into the icon rail.
123
+ setRows(splitStart.count + Math.round((splitStart.y - e.clientY) / MODULE_ROW))
124
+ }
125
+ function onSplitUp(e: PointerEvent) {
126
+ if (!splitActive) return
127
+ splitActive = false
128
+ ;(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId)
129
+ }
130
+ function onSplitKey(e: KeyboardEvent) {
131
+ if (e.key === 'ArrowUp') { e.preventDefault(); setRows(visibleModules + 1) }
132
+ else if (e.key === 'ArrowDown') { e.preventDefault(); setRows(visibleModules - 1) }
133
+ }
134
+ function selectModule(id: string) { moduleValue = id; onModuleSelect?.(id) }
135
+ </script>
136
+
137
+ <nav
138
+ bind:this={rootEl}
139
+ class="sv-nav"
140
+ class:is-collapsed={collapsed}
141
+ dir={resolvedDir}
142
+ aria-label={ariaLabel}
143
+ style:height={height != null ? `${height}px` : undefined}
144
+ >
145
+ <div class="sv-nav__scroll" onkeydown={onItemKey}>
146
+ {#each sections as section (section.id)}
147
+ {@const secOpen = openSections.has(section.id)}
148
+ {@const collapsible = section.collapsible ?? !!section.label}
149
+ <div class="sv-nav__section">
150
+ {#if section.label}
151
+ <button
152
+ type="button"
153
+ class="sv-nav__head"
154
+ class:is-open={secOpen}
155
+ aria-expanded={collapsible ? secOpen : undefined}
156
+ onclick={() => collapsible && toggleSection(section.id)}
157
+ >
158
+ {#if collapsible}<svg class="sv-nav__caret" viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg>{/if}
159
+ <span class="sv-nav__head-label">{section.label}</span>
160
+ </button>
161
+ {/if}
162
+ {#if secOpen || !collapsible}
163
+ <ul class="sv-nav__list" role="list">
164
+ {#each section.items as item (item.id)}
165
+ {@const itemOpen = openItems.has(item.id)}
166
+ <li>
167
+ <button
168
+ type="button"
169
+ class="sv-nav__item"
170
+ class:is-active={value === item.id}
171
+ data-nav
172
+ aria-current={value === item.id ? 'page' : undefined}
173
+ aria-expanded={item.children?.length ? itemOpen : undefined}
174
+ disabled={item.disabled}
175
+ title={collapsed ? item.label : undefined}
176
+ onclick={() => select(item)}
177
+ >
178
+ <span class="sv-nav__icon">{#if item.icon}{@render item.icon()}{:else}<span class="sv-nav__dot"></span>{/if}</span>
179
+ <span class="sv-nav__label">{item.label}</span>
180
+ {#if item.children?.length}<svg class="sv-nav__caret sv-nav__caret--item" class:is-open={itemOpen} viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg>
181
+ {:else if item.badge != null}<span class="sv-nav__badge">{item.badge}</span>{/if}
182
+ </button>
183
+ {#if item.children?.length && itemOpen && !collapsed}
184
+ <ul class="sv-nav__sublist" role="list">
185
+ {#each item.children as sub (sub.id)}
186
+ <li>
187
+ <button type="button" class="sv-nav__item sv-nav__item--sub" class:is-active={value === sub.id} data-nav aria-current={value === sub.id ? 'page' : undefined} disabled={sub.disabled} onclick={() => select(sub)}>
188
+ <span class="sv-nav__label">{sub.label}</span>
189
+ {#if sub.badge != null}<span class="sv-nav__badge">{sub.badge}</span>{/if}
190
+ </button>
191
+ </li>
192
+ {/each}
193
+ </ul>
194
+ {/if}
195
+ </li>
196
+ {/each}
197
+ </ul>
198
+ {/if}
199
+ </div>
200
+ {/each}
201
+ </div>
202
+
203
+ {#if moduleCount}
204
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
205
+ <div
206
+ class="sv-nav__msplit"
207
+ role="separator"
208
+ aria-orientation="horizontal"
209
+ aria-label="Resize module buttons"
210
+ aria-valuemin="0"
211
+ aria-valuemax={moduleCount}
212
+ aria-valuenow={visibleModules}
213
+ tabindex="0"
214
+ onpointerdown={onSplitDown}
215
+ onpointermove={onSplitMove}
216
+ onpointerup={onSplitUp}
217
+ onpointercancel={onSplitUp}
218
+ onkeydown={onSplitKey}
219
+ ><span class="sv-nav__mgrip"></span></div>
220
+
221
+ <div class="sv-nav__modules">
222
+ {#each fullModules as m (m.id)}
223
+ <button type="button" class="sv-nav__mod" class:is-active={moduleValue === m.id} aria-current={moduleValue === m.id ? 'page' : undefined} onclick={() => selectModule(m.id)}>
224
+ <span class="sv-nav__micon">{#if m.icon}{@render m.icon()}{/if}</span>
225
+ <span class="sv-nav__label">{m.label}</span>
226
+ {#if m.badge != null}<span class="sv-nav__badge">{m.badge}</span>{/if}
227
+ </button>
228
+ {/each}
229
+ {#if railModules.length}
230
+ <div class="sv-nav__mrail">
231
+ {#each railModules as m (m.id)}
232
+ <button type="button" class="sv-nav__modicon" class:is-active={moduleValue === m.id} title={m.label} aria-label={m.label} aria-current={moduleValue === m.id ? 'page' : undefined} onclick={() => selectModule(m.id)}>
233
+ {#if m.icon}{@render m.icon()}{:else}{m.label.slice(0, 1)}{/if}
234
+ </button>
235
+ {/each}
236
+ </div>
237
+ {/if}
238
+ </div>
239
+ {/if}
240
+ </nav>
241
+
242
+ <style>
243
+ .sv-nav {
244
+ --_accent: var(--sg-accent, #2563eb);
245
+ width: 240px; padding: 8px; box-sizing: border-box; user-select: none;
246
+ background: var(--sg-header-bg, #f8fafc); color: var(--sg-fg, #0f172a);
247
+ border: 1px solid var(--sg-border, #e2e8f0); border-radius: var(--sg-radius, 10px);
248
+ font-size: 13px; display: flex; flex-direction: column; gap: 4px;
249
+ }
250
+ .sv-nav.is-collapsed { width: 56px; }
251
+ .sv-nav__section { display: flex; flex-direction: column; }
252
+ .sv-nav__head {
253
+ display: flex; align-items: center; gap: 6px; padding: 6px 8px; margin-top: 4px;
254
+ background: none; border: 0; cursor: pointer; font: inherit; font-size: 11px; font-weight: 700;
255
+ text-transform: uppercase; letter-spacing: 0.04em; color: var(--sg-muted, #64748b); text-align: start;
256
+ }
257
+ .is-collapsed .sv-nav__head-label { display: none; }
258
+ .sv-nav__caret { color: var(--sg-muted, #94a3b8); transition: transform 0.12s; flex: none; }
259
+ .sv-nav__head.is-open .sv-nav__caret { transform: rotate(90deg); }
260
+ .sv-nav__caret--item.is-open { transform: rotate(90deg); }
261
+ .sv-nav[dir='rtl'] .sv-nav__caret:not(.is-open) { transform: scaleX(-1); }
262
+ .sv-nav__list, .sv-nav__sublist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 1px; }
263
+ .sv-nav__sublist { padding-inline-start: 26px; }
264
+ .sv-nav__item {
265
+ display: flex; align-items: center; gap: 9px; width: 100%; box-sizing: border-box;
266
+ padding: 7px 9px; background: none; border: 0; border-radius: 7px; cursor: pointer;
267
+ font: inherit; font-size: 13px; color: var(--sg-fg, #0f172a); text-align: start;
268
+ transition: background 0.12s, color 0.12s;
269
+ }
270
+ .sv-nav__item:hover:not(:disabled):not(.is-active) { background: var(--sg-row-hover-bg, #eef1f5); }
271
+ .sv-nav__item:focus-visible { outline: 2px solid var(--sg-focus-ring, var(--_accent)); outline-offset: -2px; }
272
+ .sv-nav__item:disabled { opacity: 0.45; cursor: not-allowed; }
273
+ .sv-nav__item.is-active { background: color-mix(in srgb, var(--_accent) 15%, transparent); color: var(--_accent); font-weight: 600; }
274
+ .sv-nav__item--sub { padding-block: 5px; font-size: 12.5px; }
275
+ .sv-nav__icon { display: inline-flex; width: 18px; flex: none; color: var(--sg-muted, #64748b); justify-content: center; }
276
+ .sv-nav__item.is-active .sv-nav__icon { color: var(--_accent); }
277
+ .sv-nav__dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; opacity: 0.5; }
278
+ .is-collapsed .sv-nav__label, .is-collapsed .sv-nav__badge, .is-collapsed .sv-nav__caret--item { display: none; }
279
+ .is-collapsed .sv-nav__item { justify-content: center; padding: 8px 0; }
280
+ .sv-nav__label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
281
+ .sv-nav__badge {
282
+ flex: none; min-width: 18px; height: 18px; padding: 0 6px; box-sizing: border-box;
283
+ display: inline-flex; align-items: center; justify-content: center;
284
+ background: color-mix(in srgb, var(--_accent) 16%, transparent); color: var(--_accent);
285
+ border-radius: 999px; font-size: 11px; font-weight: 700;
286
+ }
287
+ .sv-nav__caret--item { color: var(--sg-muted, #94a3b8); flex: none; }
288
+
289
+ /* Scroll area holds the sections; the module strip is pinned at the bottom. */
290
+ .sv-nav__scroll { flex: 1 1 0; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 4px; }
291
+ /* Outlook-style resize splitter: drag down to collapse modules into the icon rail. */
292
+ .sv-nav__msplit { flex: none; height: 11px; display: grid; place-items: center; cursor: row-resize; touch-action: none; }
293
+ .sv-nav__mgrip { width: 30px; height: 3px; border-radius: 999px; background: var(--sg-border, #cbd5e1); transition: background 0.12s; }
294
+ .sv-nav__msplit:hover .sv-nav__mgrip, .sv-nav__msplit:focus-visible .sv-nav__mgrip { background: var(--_accent); }
295
+ .sv-nav__msplit:focus-visible { outline: none; }
296
+ .sv-nav__modules { flex: none; display: flex; flex-direction: column; gap: 1px; border-top: 1px solid var(--sg-border, #e2e8f0); padding-top: 6px; }
297
+ .sv-nav__mod {
298
+ display: flex; align-items: center; gap: 10px; padding: 8px 9px; background: none; border: 0; border-radius: 7px;
299
+ cursor: pointer; font: inherit; font-size: 13px; font-weight: 600; color: var(--sg-fg, #0f172a); text-align: start;
300
+ }
301
+ .sv-nav__mod:hover:not(.is-active) { background: var(--sg-row-hover-bg, #eef1f5); }
302
+ .sv-nav__mod.is-active { background: color-mix(in srgb, var(--_accent) 15%, transparent); color: var(--_accent); }
303
+ .sv-nav__micon { display: inline-flex; width: 18px; flex: none; color: var(--sg-muted, #64748b); justify-content: center; }
304
+ .sv-nav__mod.is-active .sv-nav__micon { color: var(--_accent); }
305
+ .sv-nav__mrail { display: flex; gap: 4px; padding: 4px 2px 0; flex-wrap: wrap; }
306
+ .sv-nav__modicon {
307
+ width: 34px; height: 34px; display: grid; place-items: center; flex: none;
308
+ background: none; border: 0; border-radius: 7px; cursor: pointer; color: var(--sg-muted, #64748b); font-weight: 700; font-size: 13px;
309
+ }
310
+ .sv-nav__modicon:hover:not(.is-active) { background: var(--sg-row-hover-bg, #eef1f5); color: var(--sg-fg, #0f172a); }
311
+ .sv-nav__modicon.is-active { background: color-mix(in srgb, var(--_accent) 15%, transparent); color: var(--_accent); }
312
+ .is-collapsed .sv-nav__mod .sv-nav__label, .is-collapsed .sv-nav__mod .sv-nav__badge { display: none; }
313
+ .is-collapsed .sv-nav__mod { justify-content: center; padding: 8px 0; }
314
+ </style>
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Component tests for SvNavPane: sections + items render with selection, and the
3
+ * bottom module strip splits into full rows + an icon rail that the resize
4
+ * splitter (keyboard) adjusts.
5
+ */
6
+ import { describe, expect, it } from 'vitest'
7
+ import { mount, unmount, flushSync } from 'svelte'
8
+ import SvNavPane, { type NavSection, type NavModule } from './SvNavPane.svelte'
9
+
10
+ const sections: NavSection[] = [
11
+ { id: 's1', label: 'Favorites', items: [
12
+ { id: 'inbox', label: 'Inbox', badge: 3 },
13
+ { id: 'sent', label: 'Sent' },
14
+ ] },
15
+ ]
16
+ const modules: NavModule[] = [
17
+ { id: 'mail', label: 'Mail' },
18
+ { id: 'calendar', label: 'Calendar' },
19
+ { id: 'people', label: 'People' },
20
+ { id: 'tasks', label: 'Tasks' },
21
+ ]
22
+
23
+ function mountNav(props: Record<string, unknown>) {
24
+ const target = document.createElement('div')
25
+ document.body.appendChild(target)
26
+ const app = mount(SvNavPane, { target, props: props as any })
27
+ flushSync()
28
+ return { target, destroy: () => { unmount(app); target.remove() } }
29
+ }
30
+
31
+ describe('SvNavPane', () => {
32
+ it('renders sections + items, highlights the selected, shows badges', () => {
33
+ const { target, destroy } = mountNav({ sections, value: 'inbox' })
34
+ try {
35
+ expect(target.querySelector('nav')!.getAttribute('aria-label')).toBe('Navigation')
36
+ const active = target.querySelector<HTMLElement>('.sv-nav__item.is-active')!
37
+ expect(active.textContent).toContain('Inbox')
38
+ expect(active.getAttribute('aria-current')).toBe('page')
39
+ expect(target.querySelector('.sv-nav__badge')!.textContent).toBe('3')
40
+ } finally { destroy() }
41
+ })
42
+
43
+ it('splits modules into full rows + an icon rail per moduleRows', () => {
44
+ const { target, destroy } = mountNav({ sections, modules, moduleRows: 2, height: 400 })
45
+ try {
46
+ expect(target.querySelectorAll('.sv-nav__mod').length).toBe(2) // full rows
47
+ const rail = target.querySelector('.sv-nav__mrail')!
48
+ expect(rail.querySelectorAll('.sv-nav__modicon').length).toBe(2) // remaining as icons
49
+ } finally { destroy() }
50
+ })
51
+
52
+ it('the resize splitter (ArrowUp) collapses one module row into the rail', () => {
53
+ let rows: number | undefined
54
+ const { target, destroy } = mountNav({
55
+ sections, modules, moduleRows: 3, height: 400,
56
+ // bindable moduleRows reported via a spy is awkward; assert DOM instead.
57
+ })
58
+ try {
59
+ expect(target.querySelectorAll('.sv-nav__mod').length).toBe(3)
60
+ const split = target.querySelector<HTMLElement>('.sv-nav__msplit')!
61
+ expect(split.getAttribute('role')).toBe('separator')
62
+ expect(split.getAttribute('aria-valuenow')).toBe('3')
63
+ split.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }))
64
+ flushSync()
65
+ expect(target.querySelectorAll('.sv-nav__mod').length).toBe(2)
66
+ expect(target.querySelector('.sv-nav__msplit')!.getAttribute('aria-valuenow')).toBe('2')
67
+ void rows
68
+ } finally { destroy() }
69
+ })
70
+ })
@@ -9,7 +9,8 @@
9
9
  * spinner + keyboard and ARIA all come from the core via prop-getters.
10
10
  */
11
11
  import SvRepeatButton from './SvRepeatButton.svelte'
12
- import { editorErrorId, type SvEditorProps } from './editor-contract'
12
+ import SvField from './SvField.svelte'
13
+ import { nextEditorId, type SvEditorProps } from './editor-contract'
13
14
  import { createNumberInput } from './createNumberInput.svelte'
14
15
 
15
16
  type Props = SvEditorProps & {
@@ -25,6 +26,8 @@
25
26
  suffix?: string
26
27
  placeholder?: string
27
28
  spinButtons?: boolean
29
+ /** Show a clear (x) button when there is a value. */
30
+ clearable?: boolean
28
31
  }
29
32
 
30
33
  let {
@@ -41,15 +44,22 @@
41
44
  disabled = false,
42
45
  readonly = false,
43
46
  spinButtons = true,
47
+ clearable = false,
44
48
  name,
45
49
  size = 'md',
46
50
  ariaLabel,
47
51
  invalid = false,
48
52
  required = false,
49
53
  error,
54
+ label,
55
+ hint,
56
+ dir,
50
57
  id,
51
58
  }: Props = $props()
52
59
 
60
+ const autoId = nextEditorId('sv-num')
61
+ const uid = $derived(id ?? autoId)
62
+
53
63
  const num = createNumberInput({
54
64
  value: () => value,
55
65
  onChange: (v) => onChange?.(v),
@@ -63,17 +73,23 @@
63
73
  placeholder: () => placeholder,
64
74
  disabled: () => disabled,
65
75
  readonly: () => readonly,
66
- id: () => id,
76
+ id: () => uid,
67
77
  invalid: () => invalid,
68
78
  required: () => required,
69
79
  error: () => error,
80
+ hint: () => hint,
70
81
  ariaLabel: () => ariaLabel,
71
82
  })
72
83
  </script>
73
84
 
74
- <div class="sv-field">
85
+ <SvField id={uid} {label} {hint} {error} {required} {dir}>
75
86
  <div class="sv-num sv-num--{size}" class:is-disabled={disabled} class:is-invalid={invalid}>
76
87
  <input class="sv-num__input" {...num.inputProps()} />
88
+ {#if clearable && value != null && !disabled && !readonly}
89
+ <button type="button" class="sv-num__clear" aria-label="Clear" tabindex="-1" onclick={() => onChange?.(null)}>
90
+ <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18" /></svg>
91
+ </button>
92
+ {/if}
77
93
  {#if spinButtons}
78
94
  <div class="sv-num__spin">
79
95
  <SvRepeatButton size="sm" variant="ghost" ariaLabel="Increment" onclick={num.increment}>
@@ -86,13 +102,9 @@
86
102
  {/if}
87
103
  {#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
88
104
  </div>
89
- {#if error}<span class="sv-field__error" id={editorErrorId(id)} role="alert">{error}</span>{/if}
90
- </div>
105
+ </SvField>
91
106
 
92
107
  <style>
93
- /* Shared field wrapper: stacks the control box above an optional error line. */
94
- .sv-field { display: inline-flex; flex-direction: column; gap: 3px; }
95
- .sv-field__error { font-size: 11.5px; font-weight: 500; color: var(--sg-danger, #dc2626); line-height: 1.35; }
96
108
  .sv-num {
97
109
  --_accent: var(--sg-accent, #2563eb);
98
110
  display: inline-flex; align-items: stretch; width: 150px;
@@ -111,6 +123,8 @@
111
123
  .sv-num--sm { height: 28px; font-size: 12px; }
112
124
  .sv-num--md { height: 34px; font-size: 13px; }
113
125
  .sv-num--lg { height: 40px; font-size: 15px; }
114
- .sv-num__spin { display: flex; flex-direction: column; border-left: 1px solid var(--sg-border, #e2e8f0); }
126
+ .sv-num__clear { display: grid; place-items: center; width: 22px; align-self: center; flex: none; background: none; border: 0; color: var(--sg-muted, #64748b); cursor: pointer; border-radius: 4px; }
127
+ .sv-num__clear:hover { color: var(--sg-danger, #dc2626); }
128
+ .sv-num__spin { display: flex; flex-direction: column; border-inline-start: 1px solid var(--sg-border, #e2e8f0); }
115
129
  .sv-num__spin :global(.sv-repeat) { flex: 1; padding: 0 6px; border-radius: 0; }
116
130
  </style>
@@ -0,0 +1,157 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvOtpInput - a segmented one-time-code / PIN entry: N single-char cells with
4
+ * auto-advance, Backspace-to-previous, arrow navigation, and paste that
5
+ * distributes across the cells. Emits the joined string. Parity: Smart OTP.
6
+ *
7
+ * ```svelte
8
+ * <SvOtpInput length={6} value={code} onChange={(v) => (code = v)}
9
+ * onComplete={(v) => verify(v)} />
10
+ * ```
11
+ */
12
+ import SvField from './SvField.svelte'
13
+ import { nextEditorId, type SvEditorProps } from './editor-contract'
14
+ import { sanitizeOtp, otpCells, isOtpComplete } from './otp'
15
+
16
+ type Props = Pick<
17
+ SvEditorProps,
18
+ 'disabled' | 'label' | 'hint' | 'error' | 'required' | 'dir' | 'size' | 'id' | 'name' | 'invalid'
19
+ > & {
20
+ value?: string
21
+ onChange?: (value: string) => void
22
+ /** Fired once every cell is filled. */
23
+ onComplete?: (value: string) => void
24
+ length?: number
25
+ /** Digits only (default) vs any character. */
26
+ numeric?: boolean
27
+ /** Mask the characters (password dots). */
28
+ mask?: boolean
29
+ autofocus?: boolean
30
+ }
31
+
32
+ let {
33
+ value = $bindable(''),
34
+ onChange,
35
+ onComplete,
36
+ length = 6,
37
+ numeric = true,
38
+ mask = false,
39
+ disabled = false,
40
+ label,
41
+ hint,
42
+ error,
43
+ required = false,
44
+ dir,
45
+ size = 'md',
46
+ id,
47
+ name,
48
+ invalid = false,
49
+ autofocus = false,
50
+ }: Props = $props()
51
+
52
+ const autoId = nextEditorId('sv-otp')
53
+ const uid = $derived(id ?? autoId)
54
+ const cells = $derived(otpCells(sanitizeOtp(value, length, numeric), length))
55
+ let inputs = $state<HTMLInputElement[]>([])
56
+
57
+ function commit(next: string) {
58
+ const clean = sanitizeOtp(next, length, numeric)
59
+ value = clean // enables bind:value
60
+ onChange?.(clean)
61
+ if (isOtpComplete(clean, length)) onComplete?.(clean)
62
+ }
63
+ function focusCell(i: number) {
64
+ const el = inputs[Math.max(0, Math.min(length - 1, i))]
65
+ el?.focus()
66
+ el?.select()
67
+ }
68
+ function currentCells(): string[] {
69
+ return otpCells(sanitizeOtp(value, length, numeric), length)
70
+ }
71
+
72
+ function onCellInput(i: number, e: Event) {
73
+ const ch = sanitizeOtp((e.currentTarget as HTMLInputElement).value, 1, numeric).slice(-1)
74
+ const arr = currentCells()
75
+ arr[i] = ch
76
+ commit(arr.join(''))
77
+ if (ch) focusCell(i + 1)
78
+ }
79
+ function onCellKeydown(i: number, e: KeyboardEvent) {
80
+ if (e.key === 'Backspace') {
81
+ e.preventDefault()
82
+ const arr = currentCells()
83
+ if (arr[i]) {
84
+ arr[i] = ''
85
+ commit(arr.join(''))
86
+ } else if (i > 0) {
87
+ arr[i - 1] = ''
88
+ commit(arr.join(''))
89
+ focusCell(i - 1)
90
+ }
91
+ } else if (e.key === 'ArrowLeft') {
92
+ e.preventDefault()
93
+ focusCell(i - 1)
94
+ } else if (e.key === 'ArrowRight') {
95
+ e.preventDefault()
96
+ focusCell(i + 1)
97
+ }
98
+ }
99
+ function onPaste(i: number, e: ClipboardEvent) {
100
+ e.preventDefault()
101
+ const text = e.clipboardData?.getData('text') ?? ''
102
+ const arr = currentCells()
103
+ const pasted = sanitizeOtp(text, length - i, numeric)
104
+ for (let k = 0; k < pasted.length; k++) arr[i + k] = pasted[k]!
105
+ const joined = arr.join('')
106
+ commit(joined)
107
+ focusCell(Math.min(i + pasted.length, length - 1))
108
+ }
109
+ function mountFocus(node: HTMLInputElement, i: number) {
110
+ if (autofocus && i === 0) node.focus()
111
+ }
112
+ </script>
113
+
114
+ <SvField id={uid} {label} {hint} {error} {required} {dir}>
115
+ <div
116
+ class="sv-otp sv-otp--{size}"
117
+ class:is-disabled={disabled}
118
+ class:is-invalid={invalid}
119
+ role="group"
120
+ aria-label={label ?? 'One-time code'}
121
+ >
122
+ {#each cells as cell, i (i)}
123
+ <input
124
+ bind:this={inputs[i]}
125
+ use:mountFocus={i}
126
+ class="sv-otp__cell"
127
+ type={mask ? 'password' : 'text'}
128
+ inputmode={numeric ? 'numeric' : 'text'}
129
+ autocomplete={i === 0 ? 'one-time-code' : 'off'}
130
+ maxlength="1"
131
+ value={cell}
132
+ {disabled}
133
+ aria-label={`Character ${i + 1} of ${length}`}
134
+ oninput={(e) => onCellInput(i, e)}
135
+ onkeydown={(e) => onCellKeydown(i, e)}
136
+ onpaste={(e) => onPaste(i, e)}
137
+ onfocus={(e) => (e.currentTarget as HTMLInputElement).select()}
138
+ />
139
+ {/each}
140
+ {#if name}<input type="hidden" {name} value={value} />{/if}
141
+ </div>
142
+ </SvField>
143
+
144
+ <style>
145
+ .sv-otp { --_accent: var(--sg-accent, #2563eb); display: inline-flex; gap: 8px; }
146
+ .sv-otp.is-disabled { opacity: 0.6; }
147
+ .sv-otp__cell {
148
+ width: 40px; height: 46px; text-align: center; font: inherit; font-weight: 650; font-size: 18px;
149
+ background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
150
+ border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
151
+ outline: none; caret-color: var(--_accent);
152
+ }
153
+ .sv-otp--sm .sv-otp__cell { width: 34px; height: 40px; font-size: 16px; }
154
+ .sv-otp--lg .sv-otp__cell { width: 46px; height: 54px; font-size: 20px; }
155
+ .sv-otp__cell:focus { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
156
+ .sv-otp.is-invalid .sv-otp__cell { border-color: var(--sg-danger, #dc2626); }
157
+ </style>