@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
@@ -15,6 +15,7 @@
15
15
  *
16
16
  * The styled <SvPasswordInput> is just one renderer over this core.
17
17
  */
18
+ import { editorAria, type EditorAriaState } from './editor-contract'
18
19
 
19
20
  export const STRENGTH_LABELS = ['', 'Weak', 'Fair', 'Good', 'Strong']
20
21
 
@@ -36,8 +37,21 @@ export type PasswordInputConfig = {
36
37
  placeholder?: () => string | undefined
37
38
  disabled?: () => boolean
38
39
  readonly?: () => boolean
39
- ariaLabel?: () => string | undefined
40
40
  autocomplete?: () => string | undefined
41
+ // Localizable strings (fall back to the built-in English defaults).
42
+ /** Accessible label for the reveal toggle when the value is hidden. */
43
+ showLabel?: () => string | undefined
44
+ /** Accessible label for the reveal toggle when the value is shown. */
45
+ hideLabel?: () => string | undefined
46
+ /** Strength labels indexed 0-4 (index 0 unused; 1..4 = weak..strong). */
47
+ strengthLabels?: () => readonly string[]
48
+ // Editor contract (ARIA + validation) - folded into inputProps().
49
+ id?: () => string | undefined
50
+ invalid?: () => boolean
51
+ required?: () => boolean
52
+ error?: () => string | undefined
53
+ hint?: () => string | undefined
54
+ ariaLabel?: () => string | undefined
41
55
  }
42
56
 
43
57
  export function createPasswordInput(config: PasswordInputConfig) {
@@ -53,29 +67,41 @@ export function createPasswordInput(config: PasswordInputConfig) {
53
67
  }
54
68
  const toggle = () => (revealed = !revealed)
55
69
 
70
+ const ariaState = (): EditorAriaState => ({
71
+ id: config.id?.(),
72
+ invalid: config.invalid?.(),
73
+ required: config.required?.(),
74
+ error: config.error?.(),
75
+ hint: config.hint?.(),
76
+ ariaLabel: config.ariaLabel?.(),
77
+ })
78
+
56
79
  return {
57
80
  /** Whether the value is currently shown as plain text. */
58
81
  get revealed() { return revealed },
59
82
  /** Strength score, 0-4. */
60
83
  get strength() { return strength },
61
- /** Human label for the strength score. */
62
- get strengthLabel() { return STRENGTH_LABELS[strength] },
84
+ /** Human label for the strength score (localizable via `strengthLabels`). */
85
+ get strengthLabel() { return (config.strengthLabels?.() ?? STRENGTH_LABELS)[strength] },
63
86
  toggle,
64
87
  /** Spread onto the text/password input element. */
65
88
  inputProps: () => ({
89
+ id: config.id?.(),
66
90
  value: config.value(),
67
91
  type: (revealed ? 'text' : 'password') as 'text' | 'password',
68
92
  placeholder: config.placeholder?.(),
69
93
  disabled: disabled(),
70
94
  readonly: readonly(),
71
95
  autocomplete: (config.autocomplete?.() ?? 'current-password') as AutoFill,
72
- 'aria-label': config.ariaLabel?.(),
96
+ ...editorAria(ariaState()),
73
97
  oninput: onInput,
74
98
  }),
75
99
  /** Spread onto the reveal (show/hide) toggle button. */
76
100
  toggleProps: () => ({
77
101
  type: 'button' as const,
78
- 'aria-label': revealed ? 'Hide password' : 'Show password',
102
+ 'aria-label': revealed
103
+ ? (config.hideLabel?.() ?? 'Hide password')
104
+ : (config.showLabel?.() ?? 'Show password'),
79
105
  'aria-pressed': revealed,
80
106
  disabled: disabled(),
81
107
  tabindex: -1,
@@ -17,18 +17,30 @@
17
17
  *
18
18
  * The styled <SvPhoneInput> is just one renderer over this core.
19
19
  */
20
- import { COUNTRIES, COUNTRY_BY_CODE, flagEmoji } from './countries'
20
+ import { COUNTRIES, COUNTRY_BY_CODE, flagEmoji, phoneDigitsValid } from './countries'
21
21
  import { applyMask } from './datetime/mask'
22
+ import { editorAria, type EditorAriaState } from './editor-contract'
23
+
24
+ /** Emitted alongside the phone value: parsed parts + per-country validity. */
25
+ export type PhoneParts = { country: string; dial: string; national: string; valid: boolean; complete: boolean }
22
26
 
23
27
  /** Reactive inputs are passed as getters so the core tracks live prop changes. */
24
28
  export type PhoneInputConfig = {
25
29
  value: () => string
26
- onChange?: (value: string, parts: { country: string; dial: string; national: string }) => void
30
+ onChange?: (value: string, parts: PhoneParts) => void
27
31
  /** Default country ISO code. */
28
32
  country?: () => string
29
33
  disabled?: () => boolean
30
34
  readonly?: () => boolean
31
35
  placeholder?: () => string | undefined
36
+ /** Accessible name for the country `<select>` (localizable). */
37
+ countryLabel?: () => string | undefined
38
+ // Editor contract (ARIA + validation) - folded into inputProps().
39
+ id?: () => string | undefined
40
+ invalid?: () => boolean
41
+ required?: () => boolean
42
+ error?: () => string | undefined
43
+ hint?: () => string | undefined
32
44
  ariaLabel?: () => string | undefined
33
45
  }
34
46
 
@@ -69,8 +81,13 @@ export function createPhoneInput(config: PhoneInputConfig) {
69
81
  }
70
82
  })
71
83
 
84
+ // Per-country length validity: empty is valid (required handles emptiness);
85
+ // a non-empty number must match the country's expected national length.
86
+ const complete = $derived(national.length > 0 && phoneDigitsValid(iso, national))
87
+ const valid = $derived(national.length === 0 || phoneDigitsValid(iso, national))
88
+
72
89
  function emit() {
73
- config.onChange?.(`${dial}${national}`, { country: iso, dial, national })
90
+ config.onChange?.(`${dial}${national}`, { country: iso, dial, national, valid, complete })
74
91
  }
75
92
  function onNumberInput(e: Event) {
76
93
  const raw = (e.currentTarget as HTMLInputElement).value.replace(/\D/g, '')
@@ -84,6 +101,15 @@ export function createPhoneInput(config: PhoneInputConfig) {
84
101
  emit()
85
102
  }
86
103
 
104
+ const ariaState = (): EditorAriaState => ({
105
+ id: config.id?.(),
106
+ invalid: config.invalid?.(),
107
+ required: config.required?.(),
108
+ error: config.error?.(),
109
+ hint: config.hint?.(),
110
+ ariaLabel: config.ariaLabel?.(),
111
+ })
112
+
87
113
  return {
88
114
  /** Selected country ISO code. */
89
115
  get iso() { return iso },
@@ -95,21 +121,29 @@ export function createPhoneInput(config: PhoneInputConfig) {
95
121
  get displayNational() { return displayNational },
96
122
  /** The full emitted value (`+<dial><digits>`). */
97
123
  get value() { return `${dial}${national}` },
124
+ /** Whether the national number is a plausible length for the country. */
125
+ get valid() { return valid },
126
+ /** Whether a non-empty number matches the country's expected length. */
127
+ get complete() { return complete },
98
128
  /** Spread onto the country `<select>` element. */
99
129
  selectProps: () => ({
100
130
  value: iso,
101
131
  disabled: disabled(),
102
- 'aria-label': 'Country',
132
+ 'aria-label': config.countryLabel?.() ?? 'Country',
103
133
  onchange: onCountryChange,
104
134
  }),
105
135
  /** Spread onto the national number input element. */
106
136
  inputProps: () => ({
137
+ id: config.id?.(),
107
138
  value: displayNational,
108
139
  type: 'tel' as const,
109
140
  inputmode: 'tel' as const,
110
141
  placeholder: config.placeholder?.() ?? 'Phone number',
111
142
  disabled: disabled(),
112
143
  readonly: readonly(),
144
+ // aria-invalid/required/describedby come from the editor contract; the
145
+ // aria-label default stays 'Phone number' so the field is always named.
146
+ ...editorAria(ariaState()),
113
147
  'aria-label': config.ariaLabel?.() ?? 'Phone number',
114
148
  oninput: onNumberInput,
115
149
  }),
@@ -0,0 +1,208 @@
1
+ /**
2
+ * createPopoverSelect - the shared HEADLESS engine behind the kit's dropdown
3
+ * selects (SvMultiSelect, SvTreeSelect, SvGridSelect and any future one). It
4
+ * owns the parts every anchored-panel select repeats: open/close state, the
5
+ * `anchoredRect` positioning (reposition on scroll/resize), outside/Escape
6
+ * dismissal via the shared layer stack, a roving `active` index over the visible
7
+ * items, the WAI-ARIA combobox wiring (`aria-expanded` / `aria-controls` /
8
+ * `aria-activedescendant` + stable option ids), and focus management (focus
9
+ * moves into the panel on open, back to the trigger on close).
10
+ *
11
+ * The component owns its own `bind:this` refs and item rendering; it passes the
12
+ * refs in as getters and spreads the returned prop-getters onto its trigger,
13
+ * panel and items. Runes-based, like `createListbox`.
14
+ */
15
+ import { anchoredRect, type AnchoredRect } from './popover'
16
+ import { createDismissableLayer } from './a11y/dismissable'
17
+
18
+ let uidSeq = 0
19
+
20
+ export type PopoverSelectConfig = {
21
+ /** Number of currently-visible/navigable items (post-filter). */
22
+ itemCount: () => number
23
+ /** Whether item `i` is disabled (skipped by roving, not selectable). */
24
+ disabled?: (index: number) => boolean
25
+ /** Commit the item at `index` (Enter or a click the component forwards). */
26
+ onSelect: (index: number) => void
27
+ /** Notified whenever the open state changes. */
28
+ onOpenChange?: (open: boolean) => void
29
+ /** The trigger element (component owns `bind:this`, passes a getter). */
30
+ getTrigger: () => HTMLElement | null
31
+ /** The panel element. */
32
+ getPanel: () => HTMLElement | null
33
+ /** Element focused when the panel opens (e.g. a search input). Defaults to the
34
+ * panel itself. Its id-holder also carries `aria-activedescendant`. */
35
+ getInitialFocus?: () => HTMLElement | null
36
+ /** Estimated panel height, for the flip-up decision. Default 280. */
37
+ estimatedHeight?: () => number
38
+ /** Minimum panel width. Default 200. */
39
+ minWidth?: () => number
40
+ /** Close after a selection (single-select). Default true. */
41
+ closeOnSelect?: () => boolean
42
+ /** Active index to start on when opening (e.g. the selected row). Default 0. */
43
+ initialActive?: () => number
44
+ /** Handle a key the core doesn't (e.g. tree ArrowLeft/Right). Return true if
45
+ * handled to stop the core's default handling. */
46
+ onExtraKey?: (event: KeyboardEvent, active: number) => boolean
47
+ idPrefix?: string
48
+ }
49
+
50
+ export function createPopoverSelect(config: PopoverSelectConfig) {
51
+ const id = `${config.idPrefix ?? 'sv-pop'}-${uidSeq++}`
52
+ const panelId = `${id}-panel`
53
+ const optionId = (i: number) => `${id}-opt-${i}`
54
+
55
+ let open = $state(false)
56
+ let active = $state(0)
57
+ let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false })
58
+
59
+ const count = () => config.itemCount()
60
+ const isDisabled = (i: number) => config.disabled?.(i) ?? false
61
+
62
+ function enabledIndices(): number[] {
63
+ const out: number[] = []
64
+ const n = count()
65
+ for (let i = 0; i < n; i++) if (!isDisabled(i)) out.push(i)
66
+ return out
67
+ }
68
+ function clampActive(i: number): number {
69
+ const n = count()
70
+ return n <= 0 ? 0 : Math.max(0, Math.min(i, n - 1))
71
+ }
72
+
73
+ function setOpen(v: boolean) {
74
+ if (v === open) return
75
+ open = v
76
+ config.onOpenChange?.(v)
77
+ }
78
+ function openPanel() {
79
+ active = clampActive(config.initialActive?.() ?? 0)
80
+ setOpen(true)
81
+ }
82
+ function closePanel() {
83
+ setOpen(false)
84
+ config.getTrigger()?.focus()
85
+ }
86
+ function toggle() {
87
+ open ? setOpen(false) : openPanel()
88
+ }
89
+
90
+ function move(delta: number) {
91
+ const list = enabledIndices()
92
+ if (!list.length) return
93
+ const pos = list.indexOf(active)
94
+ active = list[(pos + delta + list.length) % list.length] ?? list[0]!
95
+ }
96
+ function first() { active = enabledIndices()[0] ?? 0 }
97
+ function last() { active = enabledIndices().at(-1) ?? 0 }
98
+ function selectActive() {
99
+ if (count() === 0 || isDisabled(active)) return
100
+ config.onSelect(active)
101
+ if (config.closeOnSelect?.() ?? true) closePanel()
102
+ }
103
+
104
+ function updatePos() {
105
+ const t = config.getTrigger()
106
+ if (!t) return
107
+ rect = anchoredRect(t.getBoundingClientRect(), {
108
+ estimatedHeight: config.estimatedHeight?.() ?? 280,
109
+ minWidth: config.minWidth?.() ?? 200,
110
+ })
111
+ }
112
+
113
+ // While open: position, focus into the panel, reposition on scroll/resize, and
114
+ // register a dismissable layer (Escape / outside-click, top-of-stack first).
115
+ $effect(() => {
116
+ if (!open) return
117
+ updatePos()
118
+ const initial = config.getInitialFocus?.() ?? config.getPanel()
119
+ queueMicrotask(() => initial?.focus())
120
+ const rp = () => updatePos()
121
+ window.addEventListener('scroll', rp, true)
122
+ window.addEventListener('resize', rp)
123
+ const layer = createDismissableLayer({
124
+ element: () => [config.getTrigger(), config.getPanel()],
125
+ onDismiss: () => setOpen(false),
126
+ })
127
+ layer.activate()
128
+ return () => {
129
+ window.removeEventListener('scroll', rp, true)
130
+ window.removeEventListener('resize', rp)
131
+ layer.release()
132
+ }
133
+ })
134
+
135
+ function onTriggerKeydown(e: KeyboardEvent) {
136
+ if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
137
+ e.preventDefault()
138
+ openPanel()
139
+ } else if (e.key === 'ArrowUp') {
140
+ e.preventDefault()
141
+ openPanel()
142
+ last()
143
+ } else if (e.key === 'Escape') {
144
+ setOpen(false)
145
+ }
146
+ }
147
+ function onPanelKeydown(e: KeyboardEvent) {
148
+ if (config.onExtraKey?.(e, active)) return
149
+ switch (e.key) {
150
+ case 'ArrowDown': e.preventDefault(); move(1); break
151
+ case 'ArrowUp': e.preventDefault(); move(-1); break
152
+ case 'Home': e.preventDefault(); first(); break
153
+ case 'End': e.preventDefault(); last(); break
154
+ case 'Enter': e.preventDefault(); selectActive(); break
155
+ case 'Escape': e.preventDefault(); closePanel(); break
156
+ case 'Tab': setOpen(false); break
157
+ }
158
+ }
159
+
160
+ /** The current active-descendant id (for the focused element), or undefined. */
161
+ const activeDescendant = () => (open && count() > 0 ? optionId(active) : undefined)
162
+
163
+ return {
164
+ get open() { return open },
165
+ set open(v: boolean) { setOpen(v) },
166
+ get active() { return active },
167
+ set active(i: number) { active = clampActive(i) },
168
+ get rect() { return rect },
169
+ /** Stable id of the panel element (for aria-controls / labelling). */
170
+ panelId,
171
+ /** Stable id of the option at `i` (for aria-activedescendant + item id). */
172
+ optionId,
173
+ activeDescendant,
174
+ openPanel,
175
+ closePanel,
176
+ toggle,
177
+ move,
178
+ first,
179
+ last,
180
+ selectActive,
181
+ updatePos,
182
+ onPanelKeydown,
183
+ /** True when item `i` is the roving-active one. */
184
+ isActive: (i: number) => i === active,
185
+ /** Spread onto the trigger button. */
186
+ triggerProps: () => ({
187
+ 'aria-haspopup': 'listbox' as const,
188
+ 'aria-expanded': open,
189
+ 'aria-controls': open ? panelId : undefined,
190
+ onkeydown: onTriggerKeydown,
191
+ }),
192
+ /** Attributes for the element that OWNS focus + activedescendant (the panel,
193
+ * or a search input). Pass the ARIA role the panel should expose. */
194
+ focusOwnerProps: (role: 'listbox' | 'tree' | 'grid' = 'listbox') => ({
195
+ role,
196
+ 'aria-activedescendant': activeDescendant(),
197
+ onkeydown: onPanelKeydown,
198
+ }),
199
+ /** Spread onto the item element at `i`. */
200
+ itemProps: (i: number) => ({
201
+ id: optionId(i),
202
+ 'data-active': i === active ? '' : undefined,
203
+ onpointermove: () => { if (!isDisabled(i)) active = i },
204
+ }),
205
+ }
206
+ }
207
+
208
+ export type PopoverSelect = ReturnType<typeof createPopoverSelect>
@@ -21,6 +21,8 @@
21
21
  */
22
22
  export type RadioOption = { value: string | number; label: string; disabled?: boolean }
23
23
 
24
+ import { editorAria, type EditorAriaState } from './editor-contract'
25
+
24
26
  /** Reactive inputs are passed as getters so the core tracks live prop changes. */
25
27
  export type RadioGroupConfig = {
26
28
  options: () => ReadonlyArray<RadioOption>
@@ -28,6 +30,12 @@ export type RadioGroupConfig = {
28
30
  onChange?: (value: string | number) => void
29
31
  disabled?: () => boolean
30
32
  ariaLabel?: () => string | undefined
33
+ // Editor contract (ARIA + validation) - folded into groupProps().
34
+ id?: () => string | undefined
35
+ invalid?: () => boolean
36
+ required?: () => boolean
37
+ error?: () => string | undefined
38
+ hint?: () => string | undefined
31
39
  }
32
40
 
33
41
  export type RadioProps = {
@@ -43,7 +51,11 @@ export type RadioProps = {
43
51
 
44
52
  export type RadioGroupProps = {
45
53
  role: 'radiogroup'
54
+ id: string | undefined
46
55
  'aria-label': string | undefined
56
+ 'aria-invalid': 'true' | undefined
57
+ 'aria-required': 'true' | undefined
58
+ 'aria-describedby': string | undefined
47
59
  'aria-disabled': boolean
48
60
  onkeydown: (e: KeyboardEvent) => void
49
61
  }
@@ -102,6 +114,15 @@ export function createRadioGroup(config: RadioGroupConfig): RadioGroup {
102
114
  container.querySelectorAll<HTMLElement>('[role="radio"]')[target]?.focus()
103
115
  }
104
116
 
117
+ const ariaState = (): EditorAriaState => ({
118
+ id: config.id?.(),
119
+ invalid: config.invalid?.(),
120
+ required: config.required?.(),
121
+ error: config.error?.(),
122
+ hint: config.hint?.(),
123
+ ariaLabel: config.ariaLabel?.(),
124
+ })
125
+
105
126
  return {
106
127
  get value() { return config.value() },
107
128
  get disabled() { return disabled() },
@@ -110,8 +131,9 @@ export function createRadioGroup(config: RadioGroupConfig): RadioGroup {
110
131
  onKeydown,
111
132
  groupProps: () => ({
112
133
  role: 'radiogroup',
113
- 'aria-label': config.ariaLabel?.(),
134
+ id: config.id?.(),
114
135
  'aria-disabled': disabled(),
136
+ ...editorAria(ariaState()),
115
137
  onkeydown: onKeydown,
116
138
  }),
117
139
  radioProps: (option: RadioOption) => {
@@ -19,6 +19,8 @@
19
19
  */
20
20
  export type RatingFill = 'full' | 'half' | 'empty'
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 RatingConfig = {
24
26
  value: () => number
@@ -28,12 +30,24 @@ export type RatingConfig = {
28
30
  readonly?: () => boolean
29
31
  disabled?: () => boolean
30
32
  ariaLabel?: () => string | undefined
33
+ /** Accessible label for a star given its 1-based position (localizable). */
34
+ starLabel?: (position: number) => string | undefined
35
+ // Editor contract (ARIA + validation) - folded into rootProps().
36
+ id?: () => string | undefined
37
+ invalid?: () => boolean
38
+ required?: () => boolean
39
+ error?: () => string | undefined
40
+ hint?: () => string | undefined
31
41
  }
32
42
 
33
43
  export type RatingRootProps = {
34
44
  role: 'slider'
45
+ id: string | undefined
35
46
  tabindex: number
36
47
  'aria-label': string
48
+ 'aria-invalid': 'true' | undefined
49
+ 'aria-required': 'true' | undefined
50
+ 'aria-describedby': string | undefined
37
51
  'aria-valuenow': number
38
52
  'aria-valuemin': number
39
53
  'aria-valuemax': number
@@ -112,6 +126,15 @@ export function createRating(config: RatingConfig): Rating {
112
126
  else if (e.key === 'End') { e.preventDefault(); config.onChange?.(max()) }
113
127
  }
114
128
 
129
+ const ariaState = (): EditorAriaState => ({
130
+ id: config.id?.(),
131
+ invalid: config.invalid?.(),
132
+ required: config.required?.(),
133
+ error: config.error?.(),
134
+ hint: config.hint?.(),
135
+ ariaLabel: config.ariaLabel?.(),
136
+ })
137
+
115
138
  return {
116
139
  get value() { return config.value() },
117
140
  get preview() { return preview },
@@ -123,7 +146,9 @@ export function createRating(config: RatingConfig): Rating {
123
146
  onKeydown,
124
147
  rootProps: () => ({
125
148
  role: 'slider',
149
+ id: config.id?.(),
126
150
  tabindex: interactive() ? 0 : -1,
151
+ ...editorAria(ariaState()),
127
152
  'aria-label': config.ariaLabel?.() ?? 'Rating',
128
153
  'aria-valuenow': config.value(),
129
154
  'aria-valuemin': 0,
@@ -136,7 +161,7 @@ export function createRating(config: RatingConfig): Rating {
136
161
  type: 'button',
137
162
  tabindex: -1,
138
163
  disabled: !interactive(),
139
- 'aria-label': `${index + 1} star${index ? 's' : ''}`,
164
+ 'aria-label': config.starLabel?.(index + 1) ?? `${index + 1} star${index ? 's' : ''}`,
140
165
  'data-fill': fillOf(index),
141
166
  onpointermove: (e: PointerEvent) => { if (interactive()) hover = valueAt(e, index) },
142
167
  onclick: (e: MouseEvent) => { if (interactive()) config.onChange?.(valueAt(e, index)) },
@@ -35,6 +35,8 @@ export type SliderConfig = {
35
35
  orientation?: () => SliderOrientation
36
36
  disabled?: () => boolean
37
37
  ariaLabel?: () => string | undefined
38
+ /** Right-to-left: mirrors horizontal pointer + Left/Right key direction. */
39
+ rtl?: () => boolean
38
40
  }
39
41
 
40
42
  export function createSlider(config: SliderConfig) {
@@ -44,6 +46,7 @@ export function createSlider(config: SliderConfig) {
44
46
  const range = () => config.range?.() ?? false
45
47
  const isVert = () => (config.orientation?.() ?? 'horizontal') === 'vertical'
46
48
  const disabled = () => config.disabled?.() ?? false
49
+ const rtl = () => config.rtl?.() ?? false
47
50
 
48
51
  const lo = $derived(range() ? (config.value() as [number, number])[0] : min())
49
52
  const hi = $derived(range() ? (config.value() as [number, number])[1] : (config.value() as number))
@@ -59,9 +62,10 @@ export function createSlider(config: SliderConfig) {
59
62
 
60
63
  /** Turn a pointer position (client coords) + measured track rect into a value. */
61
64
  function valueFromPointer(pos: SliderPoint, rect: DOMRect): number {
65
+ const horiz = (pos.x - rect.left) / rect.width
62
66
  const frac = isVert()
63
67
  ? 1 - (pos.y - rect.top) / rect.height
64
- : (pos.x - rect.left) / rect.width
68
+ : rtl() ? 1 - horiz : horiz
65
69
  return clampStep(min() + Math.min(1, Math.max(0, frac)) * (max() - min()))
66
70
  }
67
71
 
@@ -104,9 +108,16 @@ export function createSlider(config: SliderConfig) {
104
108
  function onThumbKey(e: KeyboardEvent, which: SliderThumb) {
105
109
  if (disabled()) return
106
110
  const cur = which === 'lo' ? lo : hi
111
+ // Under horizontal RTL, Left increases and Right decreases (Up/Down keep
112
+ // their natural up=more direction regardless of text direction).
113
+ const rtlH = rtl() && !isVert()
114
+ const inc = clampStep(cur + stepSize())
115
+ const dec = clampStep(cur - stepSize())
107
116
  let next = cur
108
- if (e.key === 'ArrowRight' || e.key === 'ArrowUp') next = clampStep(cur + stepSize())
109
- else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') next = clampStep(cur - stepSize())
117
+ if (e.key === 'ArrowUp') next = inc
118
+ else if (e.key === 'ArrowDown') next = dec
119
+ else if (e.key === 'ArrowRight') next = rtlH ? dec : inc
120
+ else if (e.key === 'ArrowLeft') next = rtlH ? inc : dec
110
121
  else if (e.key === 'Home') next = min()
111
122
  else if (e.key === 'End') next = max()
112
123
  else if (e.key === 'PageUp') next = clampStep(cur + stepSize() * 10)
@@ -0,0 +1,119 @@
1
+ /**
2
+ * createSplitter - the HEADLESS core behind <SvSplitter>: a two-pane resizable
3
+ * split with a draggable separator (WAI-ARIA `separator`), keyboard resize and
4
+ * clamping, exposed as **prop-getters** you spread onto YOUR OWN markup. The core
5
+ * never touches the DOM - the renderer measures the container rect and passes the
6
+ * pointer position + rect IN via `setFromPointer` (same split as createSlider).
7
+ * Parity: Smart `smart-splitter`.
8
+ *
9
+ * ```svelte
10
+ * <script lang="ts">
11
+ * import { createSplitter } from '@svgrid/grid'
12
+ * let frac = $state(0.5)
13
+ * const sp = createSplitter({ fraction: () => frac, onChange: (f) => (frac = f) })
14
+ * </script>
15
+ * <div class="row" bind:this={row}>
16
+ * <div style:flex-basis={sp.percent + '%'}>A</div>
17
+ * <div {...sp.separatorProps()} onpointerdown={...}></div>
18
+ * <div style:flex="1">B</div>
19
+ * </div>
20
+ * ```
21
+ */
22
+ export type SplitterOrientation = 'horizontal' | 'vertical'
23
+ export type SplitterPoint = { x: number; y: number }
24
+
25
+ /** Reactive inputs are passed as getters so the core tracks live prop changes. */
26
+ export type SplitterConfig = {
27
+ /** Size of the FIRST pane as a fraction of the container (0..1). */
28
+ fraction: () => number
29
+ onChange?: (fraction: number) => void
30
+ /** Split direction: `horizontal` = a vertical gutter between left/right panes. */
31
+ orientation?: () => SplitterOrientation
32
+ /** Min / max fraction for the first pane (0..1). */
33
+ min?: () => number
34
+ max?: () => number
35
+ /** Keyboard step as a fraction (default 0.02 = 2%). */
36
+ step?: () => number
37
+ disabled?: () => boolean
38
+ dir?: () => 'ltr' | 'rtl' | 'auto' | undefined
39
+ ariaLabel?: () => string | undefined
40
+ }
41
+
42
+ export function createSplitter(config: SplitterConfig) {
43
+ const isVert = () => (config.orientation?.() ?? 'horizontal') === 'vertical'
44
+ const min = () => config.min?.() ?? 0.1
45
+ const max = () => config.max?.() ?? 0.9
46
+ const step = () => config.step?.() ?? 0.02
47
+ const disabled = () => config.disabled?.() ?? false
48
+ const rtl = () => config.dir?.() === 'rtl'
49
+
50
+ const clamp = (f: number) => Math.min(max(), Math.max(min(), f))
51
+ const fraction = $derived(clamp(config.fraction()))
52
+ const percent = $derived(Math.round(fraction * 1000) / 10)
53
+
54
+ let dragging = $state(false)
55
+
56
+ function fromPointer(pos: SplitterPoint, rect: DOMRect): number {
57
+ const raw = isVert()
58
+ ? (pos.y - rect.top) / rect.height
59
+ : (pos.x - rect.left) / rect.width
60
+ // Horizontal RTL: the first pane sits on the right, so invert.
61
+ const f = !isVert() && rtl() ? 1 - raw : raw
62
+ return clamp(f)
63
+ }
64
+
65
+ function pointerDown() { if (!disabled()) dragging = true }
66
+ function setFromPointer(pos: SplitterPoint, rect: DOMRect) {
67
+ if (!dragging || disabled()) return
68
+ config.onChange?.(fromPointer(pos, rect))
69
+ }
70
+ function endDrag() { dragging = false }
71
+
72
+ function nudge(delta: number) {
73
+ if (disabled()) return
74
+ config.onChange?.(clamp(fraction + delta))
75
+ }
76
+
77
+ function onKeydown(e: KeyboardEvent) {
78
+ if (disabled()) return
79
+ const horiz = !isVert()
80
+ const rtlH = horiz && rtl()
81
+ const inc = () => nudge(step())
82
+ const dec = () => nudge(-step())
83
+ switch (e.key) {
84
+ case 'ArrowRight': if (horiz) { e.preventDefault(); rtlH ? dec() : inc() } break
85
+ case 'ArrowLeft': if (horiz) { e.preventDefault(); rtlH ? inc() : dec() } break
86
+ case 'ArrowDown': if (!horiz) { e.preventDefault(); inc() } break
87
+ case 'ArrowUp': if (!horiz) { e.preventDefault(); dec() } break
88
+ case 'Home': e.preventDefault(); config.onChange?.(min()); break
89
+ case 'End': e.preventDefault(); config.onChange?.(max()); break
90
+ }
91
+ }
92
+
93
+ return {
94
+ /** Clamped fraction (0..1) of the first pane. */
95
+ get fraction() { return fraction },
96
+ /** Clamped size of the first pane as a percentage (0..100). */
97
+ get percent() { return percent },
98
+ /** Whether the separator is being dragged. */
99
+ get dragging() { return dragging },
100
+ pointerDown,
101
+ setFromPointer,
102
+ endDrag,
103
+ nudge,
104
+ /** Spread onto the draggable separator element. */
105
+ separatorProps: () => ({
106
+ role: 'separator' as const,
107
+ tabindex: disabled() ? -1 : 0,
108
+ 'aria-orientation': (isVert() ? 'horizontal' : 'vertical') as 'horizontal' | 'vertical',
109
+ 'aria-valuemin': Math.round(min() * 100),
110
+ 'aria-valuemax': Math.round(max() * 100),
111
+ 'aria-valuenow': Math.round(fraction * 100),
112
+ 'aria-label': config.ariaLabel?.() ?? 'Resize panes',
113
+ 'aria-disabled': disabled() || undefined,
114
+ onkeydown: onKeydown,
115
+ }),
116
+ }
117
+ }
118
+
119
+ export type Splitter = ReturnType<typeof createSplitter>