@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
@@ -4,14 +4,19 @@
4
4
 
5
5
  <script lang="ts">
6
6
  /**
7
- * SvTree - a WAI-ARIA tree view. Expand/collapse, single-select highlight, and
8
- * optional cascading tri-state checkboxes. Parity: Smart `smart-tree`. Keyboard:
9
- * up/down move, left collapse/parent, right expand/child, Enter/Space select.
7
+ * SvTree - a WAI-ARIA tree view. Expand/collapse, single-select highlight,
8
+ * cascading tri-state checkboxes, keyboard, and - for large or remote data -
9
+ * fixed-row `virtual` windowing (scales to tens of thousands of nodes) and
10
+ * `loadChildren` lazy-loading. Parity: Smart `smart-tree`.
10
11
  *
11
12
  * The behavior (flattening, cascade math, keyboard, ARIA) lives in the headless
12
- * `createTree` core; this component is just one styled renderer over it.
13
+ * `createTree` core; this component renders it, and owns the DOM concerns
14
+ * (windowing, focus, and merging lazily-loaded children into the node tree).
13
15
  */
14
- import { createTree, type TreeNode } from './createTree.svelte'
16
+ import { tick, flushSync } from 'svelte'
17
+ import { type EditorDir } from './editor-contract'
18
+ import { virtualRange, scrollToIndex } from './virtual'
19
+ import { createTree, sortTreeNodes, type TreeNode, type TreeDropPosition } from './createTree.svelte'
15
20
 
16
21
  type Props = {
17
22
  nodes: ReadonlyArray<TreeNode>
@@ -26,6 +31,30 @@
26
31
  checked?: string[]
27
32
  onCheck?: (ids: string[]) => void
28
33
  ariaLabel?: string
34
+ /** Text direction (rtl mirrors indentation + flips Left/Right keys). */
35
+ dir?: EditorDir
36
+ /** Window rows (render only visible) - for large trees. Needs `height`. */
37
+ virtual?: boolean
38
+ /** Fixed row height in px (must match the CSS). Default 30. */
39
+ rowHeight?: number
40
+ /** Scroll-viewport height in px. Enables scrolling (required for `virtual`). */
41
+ height?: number
42
+ /** Load a lazy node's children on first expand (node has `lazy: true`). */
43
+ loadChildren?: (node: TreeNode) => Promise<TreeNode[]>
44
+ /** Filter query - show only matching nodes + ancestors, auto-expanded. */
45
+ filter?: string
46
+ /** Render a built-in search box that drives the filter. */
47
+ searchable?: boolean
48
+ searchPlaceholder?: string
49
+ /** Sort siblings by label ('asc'/'desc') or a custom comparator. */
50
+ sort?: 'asc' | 'desc' | ((a: TreeNode, b: TreeNode) => number)
51
+ /** Allow inline rename (double-click or F2). Fires onRename. */
52
+ editable?: boolean
53
+ onRename?: (id: string, label: string) => void
54
+ /** Allow drag-drop reordering; fires onMove(dragId, targetId, position).
55
+ * Apply it with the exported `moveTreeNode(nodes, ...)` helper. */
56
+ reorderable?: boolean
57
+ onMove?: (dragId: string, targetId: string, position: TreeDropPosition) => void
29
58
  }
30
59
 
31
60
  let {
@@ -38,67 +67,257 @@
38
67
  checked = [],
39
68
  onCheck,
40
69
  ariaLabel,
70
+ dir,
71
+ virtual = false,
72
+ rowHeight = 30,
73
+ height,
74
+ loadChildren,
75
+ filter,
76
+ searchable = false,
77
+ searchPlaceholder = 'Search...',
78
+ sort,
79
+ editable = false,
80
+ onRename,
81
+ reorderable = false,
82
+ onMove,
41
83
  }: Props = $props()
42
84
 
85
+ const resolvedDir = $derived(dir === 'ltr' || dir === 'rtl' ? dir : undefined)
86
+ let query = $state('')
87
+ const effectiveFilter = $derived(filter ?? (searchable ? query : undefined))
88
+
89
+ // Sort siblings (recursively) before anything else.
90
+ const sortedNodes = $derived.by(() => {
91
+ if (!sort) return nodes
92
+ const cmp = typeof sort === 'function' ? sort : (a: TreeNode, b: TreeNode) => (sort === 'asc' ? 1 : -1) * a.label.localeCompare(b.label)
93
+ return sortTreeNodes(nodes, cmp)
94
+ })
95
+
96
+ // --- Lazy loading: merge loaded children into the (controlled) node tree -----
97
+ let loaded = $state(new Map<string, TreeNode[]>())
98
+ let loadingIds = $state(new Set<string>())
99
+
100
+ function inject(list: ReadonlyArray<TreeNode>): TreeNode[] {
101
+ return list.map((n) => {
102
+ if (n.lazy && loaded.has(n.id)) return { ...n, children: inject(loaded.get(n.id)!), lazy: false }
103
+ if (n.children?.length) return { ...n, children: inject(n.children) }
104
+ return n
105
+ })
106
+ }
107
+ const effectiveNodes = $derived(inject(sortedNodes))
108
+
109
+ // --- Inline rename ----------------------------------------------------------
110
+ let editingId = $state<string | null>(null)
111
+ let draftLabel = $state('')
112
+ function startEdit(node: TreeNode) {
113
+ if (!editable || node.disabled) return
114
+ editingId = node.id
115
+ draftLabel = node.label
116
+ tick().then(() => treeEl?.querySelector<HTMLInputElement>('.sv-tree__edit')?.select())
117
+ }
118
+ function commitEdit() {
119
+ if (editingId == null) return
120
+ const id = editingId
121
+ const label = draftLabel.trim()
122
+ editingId = null
123
+ if (label) onRename?.(id, label)
124
+ }
125
+ function cancelEdit() { editingId = null }
126
+
127
+ // --- Drag-drop reorder ------------------------------------------------------
128
+ let dragId = $state<string | null>(null)
129
+ let dropId = $state<string | null>(null)
130
+ let dropPos = $state<TreeDropPosition | null>(null)
131
+ function computeDropPos(e: DragEvent, node: TreeNode): TreeDropPosition {
132
+ const el = e.currentTarget as HTMLElement
133
+ const r = el.getBoundingClientRect()
134
+ const y = (e.clientY - r.top) / r.height
135
+ const canNest = !!node.children?.length || !!node.lazy
136
+ if (y < 0.3) return 'before'
137
+ if (y > 0.7 || !canNest) return y > 0.5 ? 'after' : (canNest ? 'inside' : 'after')
138
+ return 'inside'
139
+ }
140
+ function onRowDragStart(e: DragEvent, node: TreeNode) {
141
+ dragId = node.id
142
+ if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move'
143
+ }
144
+ function onRowDragOver(e: DragEvent, node: TreeNode) {
145
+ if (!dragId || dragId === node.id) return
146
+ e.preventDefault()
147
+ dropId = node.id
148
+ dropPos = computeDropPos(e, node)
149
+ }
150
+ function onRowDrop(e: DragEvent, node: TreeNode) {
151
+ e.preventDefault()
152
+ if (dragId && dragId !== node.id && dropPos) onMove?.(dragId, node.id, dropPos)
153
+ dragId = dropId = dropPos = null
154
+ }
155
+ function endDrag() { dragId = dropId = dropPos = null }
156
+
157
+ function findNode(list: ReadonlyArray<TreeNode>, id: string): TreeNode | null {
158
+ for (const n of list) {
159
+ if (n.id === id) return n
160
+ if (n.children) { const f = findNode(n.children, id); if (f) return f }
161
+ }
162
+ return null
163
+ }
164
+
165
+ async function handleToggle(id: string, expanded: boolean) {
166
+ onToggle?.(id, expanded)
167
+ if (!expanded || !loadChildren) return
168
+ // Search the merged tree, not the raw prop: a nested lazy node (revealed by
169
+ // a parent's earlier load) lives inside `loaded`, never in `nodes`.
170
+ const node = findNode(effectiveNodes, id)
171
+ if (!node?.lazy || loaded.has(id) || loadingIds.has(id)) return
172
+ loadingIds = new Set(loadingIds).add(id)
173
+ // A placeholder row shows immediately while the promise resolves.
174
+ loaded = new Map(loaded).set(id, [{ id: `${id}__loading`, label: 'Loading...', disabled: true }])
175
+ try {
176
+ const kids = await loadChildren(node)
177
+ loaded = new Map(loaded).set(id, kids)
178
+ } catch {
179
+ loaded = new Map(loaded).set(id, [])
180
+ } finally {
181
+ const next = new Set(loadingIds); next.delete(id); loadingIds = next
182
+ }
183
+ }
184
+
43
185
  const tree = createTree({
44
- nodes: () => nodes,
186
+ nodes: () => effectiveNodes,
45
187
  selected: () => selected,
46
188
  onSelect: (id) => onSelect?.(id),
47
189
  expandedIds: () => expandedIds,
48
- onToggle: (id, exp) => onToggle?.(id, exp),
190
+ onToggle: handleToggle,
49
191
  checkable: () => checkable,
50
192
  checked: () => checked,
51
193
  onCheck: (ids) => onCheck?.(ids),
52
194
  ariaLabel: () => ariaLabel,
195
+ dir: () => dir,
196
+ filter: () => effectiveFilter,
53
197
  })
54
198
 
55
- // DOM focus movement is a render concern: follow the core's roving focus row.
56
- let treeEl: HTMLDivElement | null = null
199
+ // --- Virtualization ---------------------------------------------------------
200
+ const scrollable = $derived(virtual || height != null)
201
+ let treeEl = $state<HTMLDivElement | null>(null)
202
+ let scrollTop = $state(0)
203
+ let viewportH = $state(0)
204
+ const vr = $derived(
205
+ virtualRange({ scrollTop, viewportHeight: viewportH, rowHeight, count: tree.rows.length, overscan: 10 }),
206
+ )
207
+ const visibleRows = $derived(virtual ? tree.rows.slice(vr.start, vr.end) : tree.rows)
208
+
209
+ // Follow the core's roving focus row; scroll it into view first when windowed.
57
210
  let lastFocusTick = 0
58
211
  $effect(() => {
59
- if (tree.focusTick !== lastFocusTick) {
60
- lastFocusTick = tree.focusTick
61
- const i = tree.activeIndex
62
- queueMicrotask(() => treeEl?.querySelector<HTMLElement>(`[data-row="${i}"]`)?.focus())
212
+ if (tree.focusTick === lastFocusTick) return
213
+ lastFocusTick = tree.focusTick
214
+ const i = tree.activeIndex
215
+ if (virtual && treeEl) {
216
+ const next = scrollToIndex(i, scrollTop, treeEl.clientHeight, rowHeight)
217
+ if (next !== scrollTop) { treeEl.scrollTop = next; scrollTop = next }
63
218
  }
219
+ tick().then(() => treeEl?.querySelector<HTMLElement>(`[data-row="${i}"]`)?.focus())
64
220
  })
65
221
  </script>
66
222
 
67
- <div bind:this={treeEl} class="sv-tree" {...tree.treeProps()}>
68
- {#each tree.rows as item (item.node.id)}
69
- {@const cs = checkable ? tree.checkStateOf(item.node) : 'unchecked'}
70
- <div
71
- class="sv-tree__row"
72
- class:is-selected={item.node.id === selected}
73
- class:is-disabled={item.node.disabled}
74
- style:padding-left={`${item.depth * 18 + 6}px`}
75
- {...tree.itemProps(item)}
223
+ <div class="sv-tree-host" class:has-search={searchable} dir={resolvedDir}>
224
+ {#if searchable}
225
+ <div class="sv-tree__search">
226
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7" /><path d="m21 21-4.3-4.3" /></svg>
227
+ <input class="sv-tree__search-input" type="text" placeholder={searchPlaceholder} bind:value={query} aria-label={searchPlaceholder} />
228
+ {#if query}<button type="button" class="sv-tree__search-clear" aria-label="Clear search" onclick={() => (query = '')}>&times;</button>{/if}
229
+ </div>
230
+ {/if}
231
+ <div
232
+ bind:this={treeEl}
233
+ class="sv-tree"
234
+ class:sv-tree--scroll={scrollable}
235
+ class:is-virtual={virtual}
236
+ dir={resolvedDir}
237
+ style:height={height != null ? `${height}px` : undefined}
238
+ onscroll={scrollable ? (e) => { scrollTop = e.currentTarget.scrollTop; if (virtual) flushSync() } : undefined}
239
+ bind:clientHeight={viewportH}
240
+ {...tree.treeProps()}
241
+ >
242
+ {#if virtual}<div class="sv-tree__sizer" aria-hidden="true" style:height={`${vr.totalHeight}px`}></div>{/if}
243
+ {#each visibleRows as item (item.node.id)}
244
+ {@render treeRow(item)}
245
+ {/each}
246
+ </div>
247
+ </div>
248
+
249
+ {#snippet treeRow(item)}
250
+ {@const cs = checkable ? tree.checkStateOf(item.node) : 'unchecked'}
251
+ {@const isLoading = loadingIds.has(item.node.id)}
252
+ <div
253
+ class="sv-tree__row"
254
+ class:is-selected={item.node.id === selected}
255
+ class:is-disabled={item.node.disabled}
256
+ class:is-dragging={dragId === item.node.id}
257
+ class:drop-before={dropId === item.node.id && dropPos === 'before'}
258
+ class:drop-after={dropId === item.node.id && dropPos === 'after'}
259
+ class:drop-inside={dropId === item.node.id && dropPos === 'inside'}
260
+ style:height={`${rowHeight}px`}
261
+ style:padding-inline-start={`${item.depth * 18 + 6}px`}
262
+ style:transform={virtual ? `translateY(${item.index * rowHeight}px)` : undefined}
263
+ draggable={reorderable && editingId !== item.node.id && !item.node.disabled}
264
+ ondragstart={reorderable ? (e) => onRowDragStart(e, item.node) : undefined}
265
+ ondragover={reorderable ? (e) => onRowDragOver(e, item.node) : undefined}
266
+ ondragleave={reorderable ? () => { if (dropId === item.node.id) { dropId = null; dropPos = null } } : undefined}
267
+ ondrop={reorderable ? (e) => onRowDrop(e, item.node) : undefined}
268
+ ondragend={reorderable ? endDrag : undefined}
269
+ ondblclick={editable ? () => startEdit(item.node) : undefined}
270
+ onkeydown={editable ? (e) => { if (e.key === 'F2') { e.preventDefault(); startEdit(item.node) } } : undefined}
271
+ {...tree.itemProps(item)}
272
+ >
273
+ <button
274
+ type="button"
275
+ class="sv-tree__twist"
276
+ class:is-open={item.open}
277
+ class:is-leaf={!item.hasChildren}
278
+ class:is-loading={isLoading}
279
+ tabindex="-1"
280
+ aria-hidden={!item.hasChildren}
281
+ onclick={(e) => { e.stopPropagation(); tree.toggleExpand(item.node) }}
76
282
  >
77
- <button
78
- type="button"
79
- class="sv-tree__twist"
80
- class:is-open={item.open}
81
- class:is-leaf={!item.hasChildren}
82
- tabindex="-1"
83
- aria-hidden={!item.hasChildren}
84
- onclick={(e) => { e.stopPropagation(); tree.toggleExpand(item.node) }}
85
- >
86
- {#if item.hasChildren}
87
- <svg 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>
88
- {/if}
89
- </button>
90
- {#if checkable}
91
- <button type="button" class="sv-tree__check is-{cs}" tabindex="-1" aria-hidden="true" onclick={(e) => { e.stopPropagation(); tree.toggleCheck(item.node) }}>
92
- {#if cs === 'checked'}<svg viewBox="0 0 16 16"><path d="M3.5 8.5l3 3 6-6.5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" /></svg>
93
- {:else if cs === 'indeterminate'}<svg viewBox="0 0 16 16"><path d="M4 8h8" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" /></svg>{/if}
94
- </button>
283
+ {#if isLoading}
284
+ <svg class="sv-tree__spin" viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M21 12a9 9 0 1 1-6.2-8.6" /></svg>
285
+ {:else if item.hasChildren}
286
+ <svg 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>
95
287
  {/if}
288
+ </button>
289
+ {#if checkable}
290
+ <button type="button" class="sv-tree__check is-{cs}" tabindex="-1" aria-hidden="true" onclick={(e) => { e.stopPropagation(); tree.toggleCheck(item.node) }}>
291
+ {#if cs === 'checked'}<svg viewBox="0 0 16 16"><path d="M3.5 8.5l3 3 6-6.5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" /></svg>
292
+ {:else if cs === 'indeterminate'}<svg viewBox="0 0 16 16"><path d="M4 8h8" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" /></svg>{/if}
293
+ </button>
294
+ {/if}
295
+ {#if editingId === item.node.id}
296
+ <!-- svelte-ignore a11y_autofocus -->
297
+ <input
298
+ class="sv-tree__edit"
299
+ bind:value={draftLabel}
300
+ onclick={(e) => e.stopPropagation()}
301
+ onkeydown={(e) => { if (e.key === 'Enter') { e.preventDefault(); commitEdit() } else if (e.key === 'Escape') { e.preventDefault(); cancelEdit() } }}
302
+ onblur={commitEdit}
303
+ />
304
+ {:else}
96
305
  <span class="sv-tree__label">{item.node.label}</span>
97
- </div>
98
- {/each}
99
- </div>
306
+ {/if}
307
+ </div>
308
+ {/snippet}
100
309
 
101
310
  <style>
311
+ .sv-tree-host { display: contents; }
312
+ .sv-tree-host.has-search { display: inline-flex; flex-direction: column; gap: 6px; }
313
+ .sv-tree__search {
314
+ display: flex; align-items: center; gap: 7px; width: 260px; box-sizing: border-box; padding: 0 10px; height: 32px;
315
+ background: var(--sg-input-bg, #fff); color: var(--sg-muted, #64748b);
316
+ border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
317
+ }
318
+ .sv-tree__search:focus-within { border-color: var(--sg-accent, #2563eb); box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-accent, #2563eb) 22%, transparent); }
319
+ .sv-tree__search-input { flex: 1; min-width: 0; border: 0; background: none; outline: none; color: var(--sg-fg, #0f172a); font: inherit; font-size: 13px; }
320
+ .sv-tree__search-clear { background: none; border: 0; color: var(--sg-muted, #64748b); cursor: pointer; font-size: 16px; line-height: 1; padding: 0 2px; }
102
321
  .sv-tree {
103
322
  --_accent: var(--sg-accent, #2563eb);
104
323
  width: 260px; padding: 4px; user-select: none;
@@ -106,8 +325,12 @@
106
325
  border: 1px solid var(--sg-border, #e2e8f0); border-radius: var(--sg-radius, 8px);
107
326
  font-size: 13px;
108
327
  }
328
+ .sv-tree--scroll { overflow-y: auto; }
329
+ .sv-tree.is-virtual { position: relative; will-change: scroll-position; }
330
+ .sv-tree__sizer { padding: 0; margin: 0; pointer-events: none; }
331
+ .sv-tree.is-virtual .sv-tree__row { position: absolute; inset-inline: 4px; top: 4px; }
109
332
  .sv-tree__row {
110
- display: flex; align-items: center; gap: 4px; height: 30px; padding-right: 8px;
333
+ display: flex; align-items: center; gap: 4px; box-sizing: border-box; padding-inline-end: 8px;
111
334
  border-radius: 6px; cursor: pointer; outline: none;
112
335
  }
113
336
  .sv-tree__row:hover:not(.is-disabled) { background: var(--sg-row-hover-bg, #f1f5f9); }
@@ -119,7 +342,12 @@
119
342
  background: none; border: 0; color: var(--sg-muted, #64748b); cursor: pointer; transition: transform 0.12s;
120
343
  }
121
344
  .sv-tree__twist.is-open { transform: rotate(90deg); }
345
+ .sv-tree[dir='rtl'] .sv-tree__twist:not(.is-open) svg { transform: scaleX(-1); }
122
346
  .sv-tree__twist.is-leaf { visibility: hidden; }
347
+ .sv-tree__twist.is-loading { transform: none; }
348
+ .sv-tree__spin { animation: sv-tree-spin 0.7s linear infinite; }
349
+ @keyframes sv-tree-spin { to { transform: rotate(360deg); } }
350
+ @media (prefers-reduced-motion: reduce) { .sv-tree__spin { animation: none; } }
123
351
  .sv-tree__check {
124
352
  display: grid; place-items: center; width: 16px; height: 16px; flex: none; padding: 0;
125
353
  border: 1.5px solid var(--sg-border, #cbd5e1); border-radius: 4px; background: var(--sg-input-bg, #fff);
@@ -128,4 +356,18 @@
128
356
  .sv-tree__check.is-checked, .sv-tree__check.is-indeterminate { background: var(--_accent); border-color: var(--_accent); }
129
357
  .sv-tree__check svg { width: 100%; height: 100%; }
130
358
  .sv-tree__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
359
+ .sv-tree__edit {
360
+ flex: 1; min-width: 0; height: 22px; padding: 0 6px; margin: 0; font: inherit; font-size: 13px;
361
+ border: 1px solid var(--_accent); border-radius: 5px; background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a); outline: none;
362
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent);
363
+ }
364
+ /* Drag-drop feedback */
365
+ .sv-tree__row.is-dragging { opacity: 0.4; }
366
+ .sv-tree__row.drop-inside { background: color-mix(in srgb, var(--_accent) 18%, transparent); box-shadow: inset 0 0 0 1px var(--_accent); }
367
+ .sv-tree__row.drop-before, .sv-tree__row.drop-after { position: relative; }
368
+ .sv-tree__row.drop-before::before, .sv-tree__row.drop-after::after {
369
+ content: ''; position: absolute; inset-inline: 4px; height: 2px; background: var(--_accent); border-radius: 2px; z-index: 1;
370
+ }
371
+ .sv-tree__row.drop-before::before { top: -1px; }
372
+ .sv-tree__row.drop-after::after { bottom: -1px; }
131
373
  </style>
@@ -1,13 +1,6 @@
1
1
  export type { TreeNode } from './createTree.svelte';
2
- /**
3
- * SvTree - a WAI-ARIA tree view. Expand/collapse, single-select highlight, and
4
- * optional cascading tri-state checkboxes. Parity: Smart `smart-tree`. Keyboard:
5
- * up/down move, left collapse/parent, right expand/child, Enter/Space select.
6
- *
7
- * The behavior (flattening, cascade math, keyboard, ARIA) lives in the headless
8
- * `createTree` core; this component is just one styled renderer over it.
9
- */
10
- import { type TreeNode } from './createTree.svelte';
2
+ import { type EditorDir } from './editor-contract';
3
+ import { type TreeNode, type TreeDropPosition } from './createTree.svelte';
11
4
  type Props = {
12
5
  nodes: ReadonlyArray<TreeNode>;
13
6
  /** Selected node id (single-select highlight). */
@@ -21,6 +14,30 @@ type Props = {
21
14
  checked?: string[];
22
15
  onCheck?: (ids: string[]) => void;
23
16
  ariaLabel?: string;
17
+ /** Text direction (rtl mirrors indentation + flips Left/Right keys). */
18
+ dir?: EditorDir;
19
+ /** Window rows (render only visible) - for large trees. Needs `height`. */
20
+ virtual?: boolean;
21
+ /** Fixed row height in px (must match the CSS). Default 30. */
22
+ rowHeight?: number;
23
+ /** Scroll-viewport height in px. Enables scrolling (required for `virtual`). */
24
+ height?: number;
25
+ /** Load a lazy node's children on first expand (node has `lazy: true`). */
26
+ loadChildren?: (node: TreeNode) => Promise<TreeNode[]>;
27
+ /** Filter query - show only matching nodes + ancestors, auto-expanded. */
28
+ filter?: string;
29
+ /** Render a built-in search box that drives the filter. */
30
+ searchable?: boolean;
31
+ searchPlaceholder?: string;
32
+ /** Sort siblings by label ('asc'/'desc') or a custom comparator. */
33
+ sort?: 'asc' | 'desc' | ((a: TreeNode, b: TreeNode) => number);
34
+ /** Allow inline rename (double-click or F2). Fires onRename. */
35
+ editable?: boolean;
36
+ onRename?: (id: string, label: string) => void;
37
+ /** Allow drag-drop reordering; fires onMove(dragId, targetId, position).
38
+ * Apply it with the exported `moveTreeNode(nodes, ...)` helper. */
39
+ reorderable?: boolean;
40
+ onMove?: (dragId: string, targetId: string, position: TreeDropPosition) => void;
24
41
  };
25
42
  declare const SvTree: import("svelte").Component<Props, {}, "">;
26
43
  type SvTree = ReturnType<typeof SvTree>;
@@ -0,0 +1,222 @@
1
+ <script lang="ts" module>
2
+ export type { TreeSelectNode } from './ui-app.types'
3
+ </script>
4
+
5
+ <script lang="ts">
6
+ /**
7
+ * SvTreeSelect - a single-select dropdown that shows an indented, collapsible
8
+ * tree in its panel (pick a node from a hierarchy). The open / position /
9
+ * dismiss / roving / keyboard / ARIA mechanics come from the shared
10
+ * `createPopoverSelect` core (with ArrowLeft/Right handled via onExtraKey for
11
+ * expand/collapse); this component adds the tree flattening + rendering. Covers
12
+ * the "cascader / tree select" pattern without depending on SvTree.
13
+ */
14
+ import type { TreeSelectNode } from './ui-app.types'
15
+ import SvField from './SvField.svelte'
16
+ import { editorAria, nextEditorId, type SvEditorProps } from './editor-contract'
17
+ import { portalToBody, popIn } from './popover'
18
+ import { createPopoverSelect } from './createPopoverSelect.svelte'
19
+ import { flattenVisible, findNodePath } from './tree-select'
20
+
21
+ type Props = Pick<
22
+ SvEditorProps,
23
+ 'disabled' | 'label' | 'hint' | 'error' | 'required' | 'invalid' | 'size' | 'id' | 'name' | 'dir' | 'ariaLabel'
24
+ > & {
25
+ nodes: ReadonlyArray<TreeSelectNode>
26
+ value?: string | number | null
27
+ onChange?: (value: string | number) => void
28
+ onCommit?: (value: string | number) => void
29
+ onCancel?: () => void
30
+ placeholder?: string
31
+ showPath?: boolean
32
+ expandedIds?: ReadonlyArray<string | number>
33
+ }
34
+
35
+ let {
36
+ nodes,
37
+ value = $bindable<string | number | null>(null),
38
+ onChange,
39
+ onCommit,
40
+ onCancel,
41
+ placeholder = 'Select…',
42
+ showPath = false,
43
+ expandedIds = [],
44
+ disabled = false,
45
+ label,
46
+ hint,
47
+ error,
48
+ required = false,
49
+ invalid = false,
50
+ size = 'md',
51
+ id,
52
+ name,
53
+ dir,
54
+ ariaLabel,
55
+ }: Props = $props()
56
+
57
+ const autoId = nextEditorId('sv-ts')
58
+ const uid = $derived(id ?? autoId)
59
+
60
+ let expanded = $state(new Set<string | number>(expandedIds))
61
+ let triggerEl = $state<HTMLButtonElement | null>(null)
62
+ let panelEl = $state<HTMLDivElement | null>(null)
63
+ let treeEl = $state<HTMLUListElement | null>(null)
64
+
65
+ const rows = $derived(flattenVisible(nodes, expanded))
66
+ const selectedPath = $derived(value == null ? null : findNodePath(nodes, value))
67
+ const triggerText = $derived(
68
+ !selectedPath
69
+ ? ''
70
+ : showPath
71
+ ? selectedPath.map((n) => n.label).join(' / ')
72
+ : selectedPath[selectedPath.length - 1]!.label,
73
+ )
74
+
75
+ function setExpanded(v: string | number, on: boolean) {
76
+ const next = new Set(expanded)
77
+ on ? next.add(v) : next.delete(v)
78
+ expanded = next
79
+ }
80
+ function toggleExpand(v: string | number) {
81
+ setExpanded(v, !expanded.has(v))
82
+ }
83
+ function pick(node: TreeSelectNode) {
84
+ if (node.disabled) return
85
+ value = node.value // enables bind:value
86
+ onChange?.(node.value)
87
+ ts.closePanel()
88
+ }
89
+ function onRowClick(row: { node: TreeSelectNode; hasChildren: boolean }) {
90
+ if (row.hasChildren) toggleExpand(row.node.value)
91
+ else pick(row.node)
92
+ }
93
+
94
+ const ts = createPopoverSelect({
95
+ itemCount: () => rows.length,
96
+ disabled: (i) => !!rows[i]?.node.disabled,
97
+ onSelect: (i) => { const row = rows[i]; if (row) onRowClick(row) },
98
+ getTrigger: () => triggerEl,
99
+ getPanel: () => panelEl,
100
+ getInitialFocus: () => treeEl,
101
+ closeOnSelect: () => false, // onRowClick handles pick + close (branches stay open)
102
+ initialActive: () => Math.max(0, rows.findIndex((r) => r.node.value === value)),
103
+ minWidth: () => 220,
104
+ estimatedHeight: () => 300,
105
+ onExtraKey: (e, act) => {
106
+ const row = rows[act]
107
+ if (e.key === 'ArrowRight') {
108
+ e.preventDefault()
109
+ if (row?.hasChildren && !row.expanded) setExpanded(row.node.value, true)
110
+ return true
111
+ }
112
+ if (e.key === 'ArrowLeft') {
113
+ e.preventDefault()
114
+ if (row?.hasChildren && row.expanded) setExpanded(row.node.value, false)
115
+ return true
116
+ }
117
+ return false
118
+ },
119
+ idPrefix: 'sv-ts',
120
+ })
121
+ </script>
122
+
123
+ <SvField id={uid} {label} {hint} {error} {required} {dir}>
124
+ <div class="sv-ts sv-ts--{size}" class:is-disabled={disabled}>
125
+ <button
126
+ bind:this={triggerEl}
127
+ type="button"
128
+ class="sv-ts__trigger"
129
+ class:is-invalid={invalid}
130
+ {disabled}
131
+ onclick={() => ts.toggle()}
132
+ {...ts.triggerProps()}
133
+ {...editorAria({ id: uid, invalid, required, error, hint, ariaLabel })}
134
+ >
135
+ <span class="sv-ts__value" class:is-placeholder={!triggerText}>{triggerText || placeholder}</span>
136
+ <span class="sv-ts__arrow" aria-hidden="true">▾</span>
137
+ </button>
138
+ {#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
139
+ </div>
140
+ </SvField>
141
+
142
+ {#if ts.open}
143
+ <div
144
+ bind:this={panelEl}
145
+ class="sv-ts__panel"
146
+ dir={dir}
147
+ use:portalToBody
148
+ use:popIn={{ up: ts.rect.openUpward }}
149
+ style:position="fixed"
150
+ style:top={`${ts.rect.top}px`}
151
+ style:left={`${ts.rect.left}px`}
152
+ style:min-width={`${ts.rect.width}px`}
153
+ >
154
+ <ul
155
+ bind:this={treeEl}
156
+ id={ts.panelId}
157
+ class="sv-ts__tree"
158
+ role="tree"
159
+ aria-label={label ?? ariaLabel ?? 'Tree'}
160
+ aria-activedescendant={ts.activeDescendant()}
161
+ tabindex="0"
162
+ onkeydown={ts.onPanelKeydown}
163
+ >
164
+ {#each rows as row, i (row.node.value)}
165
+ <li
166
+ class="sv-ts__row"
167
+ class:is-active={ts.isActive(i)}
168
+ class:is-selected={row.node.value === value}
169
+ class:is-disabled={row.node.disabled}
170
+ role="treeitem"
171
+ aria-selected={row.node.value === value}
172
+ aria-expanded={row.hasChildren ? row.expanded : undefined}
173
+ style:padding-inline-start={`${8 + row.depth * 16}px`}
174
+ onclick={() => onRowClick(row)}
175
+ {...ts.itemProps(i)}
176
+ >
177
+ {#if row.hasChildren}
178
+ <span class="sv-ts__chev" class:is-open={row.expanded} aria-hidden="true">▸</span>
179
+ {:else}
180
+ <span class="sv-ts__chev sv-ts__chev--leaf" aria-hidden="true"></span>
181
+ {/if}
182
+ <span class="sv-ts__label">{row.node.label}</span>
183
+ </li>
184
+ {/each}
185
+ </ul>
186
+ </div>
187
+ {/if}
188
+
189
+ <style>
190
+ .sv-ts { --_accent: var(--sg-accent, #2563eb); display: inline-flex; width: 240px; }
191
+ .sv-ts.is-disabled { opacity: 0.6; }
192
+ .sv-ts__trigger {
193
+ flex: 1; min-width: 0; display: flex; align-items: center; gap: 6px; text-align: start;
194
+ background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
195
+ border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
196
+ font: inherit; cursor: pointer; padding: 0 9px;
197
+ }
198
+ .sv-ts--sm .sv-ts__trigger { height: 28px; font-size: 12px; }
199
+ .sv-ts--md .sv-ts__trigger { height: 34px; font-size: 13px; }
200
+ .sv-ts--lg .sv-ts__trigger { height: 40px; font-size: 15px; }
201
+ .sv-ts__trigger:focus-visible { outline: none; border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
202
+ .sv-ts__trigger.is-invalid { border-color: var(--sg-danger, #dc2626); }
203
+ .sv-ts__value { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
204
+ .sv-ts__value.is-placeholder { color: var(--sg-muted, #94a3b8); }
205
+ .sv-ts__arrow { color: var(--sg-muted, #64748b); font-size: 10px; }
206
+
207
+ :global(.sv-ts__panel) {
208
+ z-index: 2147483646; max-height: 320px; max-width: 92vw; overflow: hidden;
209
+ background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
210
+ border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px;
211
+ box-shadow: 0 16px 48px -12px rgba(15, 23, 42, 0.35); font-size: 13px;
212
+ }
213
+ :global(.sv-ts__tree) { list-style: none; margin: 0; padding: 4px; overflow: auto; max-height: 320px; outline: none; }
214
+ :global(.sv-ts__row) { display: flex; align-items: center; gap: 4px; padding: 6px 8px; border-radius: 6px; cursor: pointer; }
215
+ :global(.sv-ts__row.is-active) { background: var(--sg-row-hover-bg, #f1f5f9); }
216
+ :global(.sv-ts__row.is-selected) { color: var(--sg-accent, #2563eb); font-weight: 600; }
217
+ :global(.sv-ts__row.is-disabled) { opacity: 0.5; cursor: not-allowed; }
218
+ :global(.sv-ts__chev) { width: 14px; flex: none; color: var(--sg-muted, #64748b); transition: transform 0.12s; font-size: 10px; }
219
+ :global(.sv-ts__chev.is-open) { transform: rotate(90deg); }
220
+ :global(.sv-ts__chev--leaf) { visibility: hidden; }
221
+ :global(.sv-ts__label) { flex: 1; white-space: nowrap; }
222
+ </style>