@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
@@ -2,26 +2,55 @@
2
2
  /**
3
3
  * SvListBox - an inline single/multi-select list (WAI-ARIA listbox) with
4
4
  * roving highlight + full keyboard. Parity: Smart `smart-list-box`. Controlled
5
- * via `value` (scalar or array) + `onChange`.
5
+ * via `value` (scalar or array) + `onChange`. Scales to huge option sets with
6
+ * `virtual` (fixed-row windowing), and each row can be an `itemTemplate`.
6
7
  */
7
- import type { ListOption } from './list-option'
8
+ import { flushSync, type Snippet } from 'svelte'
9
+ import { groupOptions, hasGroups, type ListOption } from './list-option'
10
+ import { virtualRange, scrollToIndex } from './virtual'
8
11
  import { createListbox } from './createListbox.svelte'
12
+ import SvField from './SvField.svelte'
13
+ import { nextEditorId, type SvEditorProps } from './editor-contract'
9
14
 
10
- type Props = {
15
+ type Props = SvEditorProps & {
11
16
  options: ReadonlyArray<ListOption>
12
17
  value?: string | number | Array<string | number> | null
13
18
  onChange?: (value: any) => void
14
19
  multiple?: boolean
15
- disabled?: boolean
16
20
  /** Visible height in rows before scrolling. */
17
21
  rows?: number
18
- ariaLabel?: string
19
- /** Form field name; emits a hidden input per selected value so the listbox
20
- * posts in a native <form> (multiple selection -> repeated name). */
21
- name?: string
22
+ /** Window the list (render only visible rows) for large option sets. */
23
+ virtual?: boolean
24
+ /** Fixed row height in px (must match the CSS row height). Default 32. */
25
+ rowHeight?: number
26
+ /** Custom per-option content. Receives the option. */
27
+ itemTemplate?: Snippet<[ListOption]>
22
28
  }
23
29
 
24
- let { options, value = null, onChange, multiple = false, disabled = false, rows = 7, ariaLabel, name }: Props = $props()
30
+ let {
31
+ options,
32
+ value = null,
33
+ onChange,
34
+ multiple = false,
35
+ disabled = false,
36
+ rows = 7,
37
+ virtual = false,
38
+ rowHeight = 32,
39
+ itemTemplate,
40
+ ariaLabel,
41
+ name,
42
+ invalid = false,
43
+ required = false,
44
+ error,
45
+ label,
46
+ hint,
47
+ dir,
48
+ id,
49
+ }: Props = $props()
50
+
51
+ // Stable id anchors the label/hint/error wiring even when no `id` is passed.
52
+ const autoId = nextEditorId('sv-lb')
53
+ const uid = $derived(id ?? autoId)
25
54
 
26
55
  // The styled listbox is just a renderer over the headless core.
27
56
  const lb = createListbox({
@@ -31,40 +60,85 @@
31
60
  multiple: () => multiple,
32
61
  disabled: () => disabled,
33
62
  ariaLabel: () => ariaLabel,
63
+ id: () => uid,
64
+ invalid: () => invalid,
65
+ required: () => required,
66
+ error: () => error,
67
+ hint: () => hint,
34
68
  })
35
69
  const isSel = (o: ListOption) => lb.isSelected(o.value)
36
70
 
37
- let listEl: HTMLUListElement | null = null
38
- // Scroll-into-view is a render concern, so it lives here (not in the core).
71
+ // Virtualization only applies to a flat (ungrouped) list.
72
+ const useVirtual = $derived(virtual && !hasGroups(options))
73
+ let listEl = $state<HTMLUListElement | null>(null)
74
+ let scrollTop = $state(0)
75
+ let viewportH = $state(0)
76
+ const vr = $derived(
77
+ virtualRange({ scrollTop, viewportHeight: viewportH, rowHeight, count: options.length, overscan: 10 }),
78
+ )
79
+ const windowed = $derived(
80
+ useVirtual ? options.slice(vr.start, vr.end).map((o, i) => ({ ...o, index: vr.start + i })) : [],
81
+ )
82
+
83
+ // Keep the active option in view. Virtual mode drives scrollTop directly (the
84
+ // active row may not be in the DOM); otherwise use scrollIntoView.
39
85
  $effect(() => {
40
86
  const i = lb.activeIndex
41
- queueMicrotask(() => listEl?.querySelector<HTMLElement>(`[data-idx="${i}"]`)?.scrollIntoView({ block: 'nearest' }))
87
+ if (useVirtual) {
88
+ if (!listEl) return
89
+ const next = scrollToIndex(i, listEl.scrollTop, listEl.clientHeight, rowHeight)
90
+ if (next !== listEl.scrollTop) listEl.scrollTop = next
91
+ } else {
92
+ queueMicrotask(() => listEl?.querySelector<HTMLElement>(`[data-idx="${i}"]`)?.scrollIntoView({ block: 'nearest' }))
93
+ }
42
94
  })
43
95
  </script>
44
96
 
45
- <div style="display: contents">
46
- <ul
47
- bind:this={listEl}
48
- class="sv-listbox"
49
- style:--sv-rows={rows}
50
- {...lb.rootProps()}
51
- >
52
- {#each options as opt, i (opt.value)}
53
- <!-- svelte-ignore a11y_click_events_have_key_events a11y_interactive_supports_focus -->
54
- <li
55
- class="sv-listbox__opt"
56
- class:is-selected={isSel(opt)}
57
- class:is-active={lb.isActive(i)}
58
- class:is-disabled={opt.disabled}
59
- {...lb.optionProps(i)}
60
- >
61
- {#if multiple}<span class="sv-listbox__check" aria-hidden="true">{isSel(opt) ? '✓' : ''}</span>{/if}
62
- <span class="sv-listbox__label">{opt.label}</span>
63
- </li>
64
- {/each}
65
- </ul>
66
- {#if name}{#each lb.selectedValues as v (v)}<input type="hidden" {name} value={v} />{/each}{/if}
67
- </div>
97
+ <SvField id={uid} {label} {hint} {error} {required} {dir}>
98
+ <ul
99
+ bind:this={listEl}
100
+ class="sv-listbox"
101
+ class:is-invalid={invalid}
102
+ class:is-virtual={useVirtual}
103
+ style:--sv-rows={rows}
104
+ onscroll={(e) => { scrollTop = e.currentTarget.scrollTop; if (useVirtual) flushSync() }}
105
+ bind:clientHeight={viewportH}
106
+ {...lb.rootProps()}
107
+ >
108
+ {#if useVirtual}
109
+ <!-- Fixed-height sizer establishes the scroll range; rows are absolutely
110
+ positioned by transform so scrolling never reflows (no flash). -->
111
+ <li class="sv-listbox__sizer" aria-hidden="true" style:height={`${vr.totalHeight}px`}></li>
112
+ {#each windowed as opt (opt.value)}
113
+ {@render optionLi(opt, opt.index)}
114
+ {/each}
115
+ {:else}
116
+ {#each groupOptions(options) as g (g.group ?? ' ')}
117
+ {#if g.group != null}<li class="sv-listbox__group" role="presentation">{g.group}</li>{/if}
118
+ {#each g.options as opt (opt.value)}
119
+ {@render optionLi(opt, opt.index)}
120
+ {/each}
121
+ {/each}
122
+ {/if}
123
+ </ul>
124
+ {#if name}{#each lb.selectedValues as v (v)}<input type="hidden" {name} value={v} />{/each}{/if}
125
+ </SvField>
126
+
127
+ {#snippet optionLi(opt, index)}
128
+ <!-- svelte-ignore a11y_click_events_have_key_events a11y_interactive_supports_focus -->
129
+ <li
130
+ class="sv-listbox__opt"
131
+ class:is-selected={isSel(opt)}
132
+ class:is-active={lb.isActive(index)}
133
+ class:is-disabled={opt.disabled}
134
+ style:height={`${rowHeight}px`}
135
+ style:transform={useVirtual ? `translateY(${index * rowHeight}px)` : undefined}
136
+ {...lb.optionProps(index)}
137
+ >
138
+ {#if multiple}<span class="sv-listbox__check" aria-hidden="true">{isSel(opt) ? '✓' : ''}</span>{/if}
139
+ {#if itemTemplate}{@render itemTemplate(opt)}{:else}<span class="sv-listbox__label">{opt.label}</span>{/if}
140
+ </li>
141
+ {/snippet}
68
142
 
69
143
  <style>
70
144
  .sv-listbox {
@@ -76,14 +150,27 @@
76
150
  width: 220px; outline: none;
77
151
  }
78
152
  .sv-listbox:focus-visible { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
153
+ .sv-listbox.is-invalid { border-color: var(--sg-danger, #dc2626); }
154
+ .sv-listbox.is-invalid:focus-visible { box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-danger, #dc2626) 22%, transparent); }
155
+ /* Pre-promote the scrolling-contents layer at mount so the FIRST scroll
156
+ doesn't trigger a one-time layer promotion + repaint (the initial flash). */
157
+ .sv-listbox.is-virtual { position: relative; will-change: scroll-position; }
158
+ .sv-listbox__sizer { padding: 0; margin: 0; pointer-events: none; }
159
+ .sv-listbox.is-virtual .sv-listbox__opt {
160
+ position: absolute; inset-inline: 4px; top: 4px;
161
+ }
79
162
  .sv-listbox__opt {
80
- display: flex; align-items: center; gap: 8px; height: 32px; padding: 0 10px;
163
+ display: flex; align-items: center; gap: 8px; box-sizing: border-box; padding: 0 10px;
81
164
  border-radius: 6px; cursor: pointer; font-size: 13px;
82
165
  }
83
166
  .sv-listbox__opt.is-active { background: var(--sg-row-hover-bg, #f1f5f9); }
84
167
  .sv-listbox__opt.is-selected { background: color-mix(in srgb, var(--_accent) 14%, transparent); color: var(--_accent); font-weight: 600; }
85
168
  .sv-listbox__opt.is-selected.is-active { background: color-mix(in srgb, var(--_accent) 22%, transparent); }
86
169
  .sv-listbox__opt.is-disabled { opacity: 0.4; cursor: not-allowed; }
170
+ .sv-listbox__group {
171
+ padding: 8px 10px 3px; font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em;
172
+ color: var(--sg-muted, #94a3b8);
173
+ }
87
174
  .sv-listbox__check { width: 14px; text-align: center; color: var(--_accent); }
88
175
  .sv-listbox__label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
89
176
  </style>
@@ -1,21 +1,25 @@
1
1
  /**
2
2
  * SvListBox - an inline single/multi-select list (WAI-ARIA listbox) with
3
3
  * roving highlight + full keyboard. Parity: Smart `smart-list-box`. Controlled
4
- * via `value` (scalar or array) + `onChange`.
4
+ * via `value` (scalar or array) + `onChange`. Scales to huge option sets with
5
+ * `virtual` (fixed-row windowing), and each row can be an `itemTemplate`.
5
6
  */
6
- import type { ListOption } from './list-option';
7
- type Props = {
7
+ import { type Snippet } from 'svelte';
8
+ import { type ListOption } from './list-option';
9
+ import { type SvEditorProps } from './editor-contract';
10
+ type Props = SvEditorProps & {
8
11
  options: ReadonlyArray<ListOption>;
9
12
  value?: string | number | Array<string | number> | null;
10
13
  onChange?: (value: any) => void;
11
14
  multiple?: boolean;
12
- disabled?: boolean;
13
15
  /** Visible height in rows before scrolling. */
14
16
  rows?: number;
15
- ariaLabel?: string;
16
- /** Form field name; emits a hidden input per selected value so the listbox
17
- * posts in a native <form> (multiple selection -> repeated name). */
18
- name?: string;
17
+ /** Window the list (render only visible rows) for large option sets. */
18
+ virtual?: boolean;
19
+ /** Fixed row height in px (must match the CSS row height). Default 32. */
20
+ rowHeight?: number;
21
+ /** Custom per-option content. Receives the option. */
22
+ itemTemplate?: Snippet<[ListOption]>;
19
23
  };
20
24
  declare const SvListBox: import("svelte").Component<Props, {}, "">;
21
25
  type SvListBox = ReturnType<typeof SvListBox>;
@@ -7,20 +7,23 @@
7
7
  * The styled renderer over the headless `createMaskedInput` core; the mask
8
8
  * formatting + state live in the core, spread here via prop-getters.
9
9
  */
10
+ import type { Snippet } from 'svelte'
10
11
  import { unmask } from './datetime/mask'
12
+ import SvField from './SvField.svelte'
13
+ import { nextEditorId, type SvEditorProps } from './editor-contract'
11
14
  import { createMaskedInput } from './createMaskedInput.svelte'
12
15
 
13
- type Props = {
16
+ type Props = SvEditorProps & {
14
17
  value?: string
15
18
  /** Called with (masked, raw, complete). */
16
19
  onChange?: (masked: string, raw: string, complete: boolean) => void
17
20
  mask?: string
18
21
  placeholder?: string
19
- disabled?: boolean
20
- readonly?: boolean
21
- name?: string
22
- size?: 'sm' | 'md' | 'lg'
23
- ariaLabel?: string
22
+ /** Show a clear (x) button when there is a value. */
23
+ clearable?: boolean
24
+ /** Leading / trailing icon adornments. */
25
+ prefixIcon?: Snippet
26
+ suffixIcon?: Snippet
24
27
  }
25
28
 
26
29
  let {
@@ -33,8 +36,21 @@
33
36
  name,
34
37
  size = 'md',
35
38
  ariaLabel,
39
+ invalid = false,
40
+ required = false,
41
+ error,
42
+ label,
43
+ hint,
44
+ dir,
45
+ id,
46
+ clearable = false,
47
+ prefixIcon,
48
+ suffixIcon,
36
49
  }: Props = $props()
37
50
 
51
+ const autoId = nextEditorId('sv-mask')
52
+ const uid = $derived(id ?? autoId)
53
+
38
54
  const mi = createMaskedInput({
39
55
  value: () => value,
40
56
  onChange: (m, r, c) => onChange?.(m, r, c),
@@ -42,25 +58,49 @@
42
58
  placeholder: () => placeholder,
43
59
  disabled: () => disabled,
44
60
  readonly: () => readonly,
61
+ id: () => uid,
62
+ invalid: () => invalid,
63
+ required: () => required,
64
+ error: () => error,
65
+ hint: () => hint,
45
66
  ariaLabel: () => ariaLabel,
46
67
  })
47
68
  </script>
48
69
 
49
- <input class="sv-masked sv-masked--{size}" {...mi.inputProps()} />
50
- {#if name}<input type="hidden" {name} value={mask ? unmask(mi.masked, mask) : mi.masked} />{/if}
70
+ <SvField id={uid} {label} {hint} {error} {required} {dir}>
71
+ <div class="sv-masked sv-masked--{size}" class:is-invalid={invalid} class:is-disabled={disabled}>
72
+ {#if prefixIcon}<span class="sv-masked__adorn">{@render prefixIcon()}</span>{/if}
73
+ <input class="sv-masked__input" {...mi.inputProps()} />
74
+ {#if clearable && mi.masked && !disabled && !readonly}
75
+ <button type="button" class="sv-masked__clear" aria-label="Clear" tabindex="-1" onclick={() => onChange?.('', '', false)}>
76
+ <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18" /></svg>
77
+ </button>
78
+ {/if}
79
+ {#if suffixIcon}<span class="sv-masked__adorn">{@render suffixIcon()}</span>{/if}
80
+ {#if name}<input type="hidden" {name} value={mask ? unmask(mi.masked, mask) : mi.masked} />{/if}
81
+ </div>
82
+ </SvField>
51
83
 
52
84
  <style>
53
85
  .sv-masked {
54
86
  --_accent: var(--sg-accent, #2563eb);
55
- box-sizing: border-box; width: 200px;
87
+ box-sizing: border-box; width: 200px; display: inline-flex; align-items: center; gap: 6px;
56
88
  background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
57
89
  border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
58
- font: inherit; padding: 0 10px; outline: none;
59
- font-variant-numeric: tabular-nums; letter-spacing: 0.02em;
90
+ font: inherit; padding: 0 8px 0 10px;
60
91
  }
61
92
  .sv-masked--sm { height: 28px; font-size: 12px; }
62
93
  .sv-masked--md { height: 34px; font-size: 13px; }
63
94
  .sv-masked--lg { height: 40px; font-size: 15px; }
64
- .sv-masked:focus { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
65
- .sv-masked:disabled { opacity: 0.6; }
95
+ .sv-masked:focus-within { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
96
+ .sv-masked.is-invalid { border-color: var(--sg-danger, #dc2626); }
97
+ .sv-masked.is-invalid:focus-within { box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-danger, #dc2626) 22%, transparent); }
98
+ .sv-masked.is-disabled { opacity: 0.6; }
99
+ .sv-masked__input {
100
+ flex: 1; min-width: 0; border: 0; background: none; outline: none; color: inherit; font: inherit;
101
+ font-variant-numeric: tabular-nums; letter-spacing: 0.02em;
102
+ }
103
+ .sv-masked__adorn { display: inline-flex; flex: none; color: var(--sg-muted, #64748b); }
104
+ .sv-masked__clear { display: grid; place-items: center; width: 20px; flex: none; background: none; border: 0; color: var(--sg-muted, #64748b); cursor: pointer; border-radius: 4px; }
105
+ .sv-masked__clear:hover { color: var(--sg-danger, #dc2626); }
66
106
  </style>
@@ -1,14 +1,24 @@
1
- type Props = {
1
+ /**
2
+ * SvMaskedInput - a pattern-masked text input (#=digit, A=letter, *=alnum;
3
+ * other chars are literals). Parity: Smart `smart-masked-text-box`. Emits both
4
+ * the masked display value and the raw (unmasked) value.
5
+ *
6
+ * The styled renderer over the headless `createMaskedInput` core; the mask
7
+ * formatting + state live in the core, spread here via prop-getters.
8
+ */
9
+ import type { Snippet } from 'svelte';
10
+ import { type SvEditorProps } from './editor-contract';
11
+ type Props = SvEditorProps & {
2
12
  value?: string;
3
13
  /** Called with (masked, raw, complete). */
4
14
  onChange?: (masked: string, raw: string, complete: boolean) => void;
5
15
  mask?: string;
6
16
  placeholder?: string;
7
- disabled?: boolean;
8
- readonly?: boolean;
9
- name?: string;
10
- size?: 'sm' | 'md' | 'lg';
11
- ariaLabel?: string;
17
+ /** Show a clear (x) button when there is a value. */
18
+ clearable?: boolean;
19
+ /** Leading / trailing icon adornments. */
20
+ prefixIcon?: Snippet;
21
+ suffixIcon?: Snippet;
12
22
  };
13
23
  declare const SvMaskedInput: import("svelte").Component<Props, {}, "">;
14
24
  type SvMaskedInput = ReturnType<typeof SvMaskedInput>;
@@ -0,0 +1,101 @@
1
+ <script lang="ts" module>
2
+ export type { MenuItem } from './SvMenuList.svelte'
3
+ </script>
4
+
5
+ <script lang="ts">
6
+ /**
7
+ * SvMenu - a dropdown / context menu: a trigger opens a portalled, animated
8
+ * menu surface with submenus, separators, icons, shortcuts and full keyboard
9
+ * (arrows, Enter, Escape, ArrowRight/Left for submenus). Portals to <body> so it
10
+ * escapes any clipping. Parity: Smart menu / context-menu.
11
+ *
12
+ * ```svelte
13
+ * <SvMenu items={items} onSelect={(i) => run(i)}>
14
+ * {#snippet anchor()}<button>Actions</button>{/snippet}
15
+ * </SvMenu>
16
+ * ```
17
+ */
18
+ import type { Snippet } from 'svelte'
19
+ import { anchoredRect, portalToBody, popIn, type AnchoredRect } from './popover'
20
+ import { createDismissableLayer } from './a11y/dismissable'
21
+ import SvMenuList, { type MenuItem } from './SvMenuList.svelte'
22
+
23
+ let {
24
+ items,
25
+ open = $bindable(false),
26
+ onOpenChange,
27
+ onSelect,
28
+ ariaLabel,
29
+ anchor,
30
+ }: {
31
+ items: ReadonlyArray<MenuItem>
32
+ open?: boolean
33
+ onOpenChange?: (open: boolean) => void
34
+ onSelect?: (item: MenuItem) => void
35
+ ariaLabel?: string
36
+ anchor?: Snippet
37
+ } = $props()
38
+
39
+ let anchorEl = $state<HTMLSpanElement | null>(null)
40
+ let panelEl = $state<HTMLDivElement | null>(null)
41
+ let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false })
42
+
43
+ function setOpen(v: boolean) { if (v !== open) { open = v; onOpenChange?.(v) } }
44
+ function toggle() { setOpen(!open) }
45
+ function updatePos() {
46
+ if (!anchorEl) return
47
+ rect = anchoredRect(anchorEl.getBoundingClientRect(), { estimatedHeight: Math.min(items.length, 10) * 34 + 8, minWidth: 180 })
48
+ }
49
+
50
+ $effect(() => {
51
+ if (!open) return
52
+ updatePos()
53
+ queueMicrotask(() => panelEl?.querySelector<HTMLElement>('[role="menuitem"]:not([disabled])')?.focus())
54
+ const rp = () => updatePos()
55
+ window.addEventListener('scroll', rp, true)
56
+ window.addEventListener('resize', rp)
57
+ // Outside-click dismissal via the shared layer stack (Escape stays owned by
58
+ // SvMenuList's keyboard handling, so closeOnEscape is off here).
59
+ const layer = createDismissableLayer({
60
+ element: () => [anchorEl, panelEl],
61
+ onDismiss: () => setOpen(false),
62
+ closeOnEscape: false,
63
+ })
64
+ layer.activate()
65
+ return () => {
66
+ window.removeEventListener('scroll', rp, true)
67
+ window.removeEventListener('resize', rp)
68
+ layer.release()
69
+ }
70
+ })
71
+ </script>
72
+
73
+ <!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
74
+ <span bind:this={anchorEl} class="sv-menu__anchor" onclick={toggle}>
75
+ {@render anchor?.()}
76
+ </span>
77
+
78
+ {#if open}
79
+ <div
80
+ bind:this={panelEl}
81
+ class="sv-menu"
82
+ use:portalToBody
83
+ use:popIn={{ up: rect.openUpward }}
84
+ style:position="fixed"
85
+ style:top={`${rect.top}px`}
86
+ style:left={`${rect.left}px`}
87
+ aria-label={ariaLabel}
88
+ >
89
+ <SvMenuList {items} onclose={() => setOpen(false)} onselect={(i) => onSelect?.(i)} />
90
+ </div>
91
+ {/if}
92
+
93
+ <style>
94
+ .sv-menu__anchor { display: inline-flex; }
95
+ :global(.sv-menu) {
96
+ z-index: 2147483646; min-width: 180px;
97
+ background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
98
+ border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px;
99
+ box-shadow: 0 16px 48px -12px rgba(15, 23, 42, 0.35); font-size: 13px;
100
+ }
101
+ </style>
@@ -0,0 +1,26 @@
1
+ export type { MenuItem } from './SvMenuList.svelte';
2
+ /**
3
+ * SvMenu - a dropdown / context menu: a trigger opens a portalled, animated
4
+ * menu surface with submenus, separators, icons, shortcuts and full keyboard
5
+ * (arrows, Enter, Escape, ArrowRight/Left for submenus). Portals to <body> so it
6
+ * escapes any clipping. Parity: Smart menu / context-menu.
7
+ *
8
+ * ```svelte
9
+ * <SvMenu items={items} onSelect={(i) => run(i)}>
10
+ * {#snippet anchor()}<button>Actions</button>{/snippet}
11
+ * </SvMenu>
12
+ * ```
13
+ */
14
+ import type { Snippet } from 'svelte';
15
+ import { type MenuItem } from './SvMenuList.svelte';
16
+ type $$ComponentProps = {
17
+ items: ReadonlyArray<MenuItem>;
18
+ open?: boolean;
19
+ onOpenChange?: (open: boolean) => void;
20
+ onSelect?: (item: MenuItem) => void;
21
+ ariaLabel?: string;
22
+ anchor?: Snippet;
23
+ };
24
+ declare const SvMenu: import("svelte").Component<$$ComponentProps, {}, "open">;
25
+ type SvMenu = ReturnType<typeof SvMenu>;
26
+ export default SvMenu;
@@ -0,0 +1,143 @@
1
+ <script lang="ts" module>
2
+ import type { Snippet } from 'svelte'
3
+ export type MenuItem = {
4
+ /** Item text. Omit + set `separator` for a divider. */
5
+ label?: string
6
+ separator?: boolean
7
+ icon?: Snippet
8
+ /** Right-aligned hint, e.g. a keyboard shortcut. */
9
+ shortcut?: string
10
+ disabled?: boolean
11
+ /** Nested submenu items (renders a flyout). */
12
+ children?: MenuItem[]
13
+ onSelect?: () => void
14
+ }
15
+ </script>
16
+
17
+ <script lang="ts">
18
+ /**
19
+ * SvMenuList - the recursive menu surface behind SvMenu: a WAI-ARIA `menu` with
20
+ * roving focus, submenus (hover + ArrowRight), separators, icons and shortcuts.
21
+ * Rendered inside a portalled panel by SvMenu; it self-imports for submenus.
22
+ */
23
+ import Self from './SvMenuList.svelte'
24
+
25
+ let {
26
+ items,
27
+ onclose,
28
+ onselect,
29
+ submenu = false,
30
+ }: {
31
+ items: ReadonlyArray<MenuItem>
32
+ /** Close the WHOLE menu (a leaf was chosen or Escape at the root). */
33
+ onclose: () => void
34
+ /** Report the chosen leaf item. */
35
+ onselect: (item: MenuItem) => void
36
+ submenu?: boolean
37
+ } = $props()
38
+
39
+ // Indexes of real (non-separator) items, for roving focus.
40
+ const focusable = $derived(items.map((it, i) => (it.separator || it.disabled ? -1 : i)).filter((i) => i >= 0))
41
+ let active = $state(-1)
42
+ let openSub = $state(-1)
43
+ let listEl = $state<HTMLDivElement | null>(null)
44
+
45
+ function focusItem(i: number) {
46
+ active = i
47
+ queueMicrotask(() => listEl?.querySelector<HTMLElement>(`[data-mi="${i}"]`)?.focus())
48
+ }
49
+ function move(delta: number) {
50
+ if (!focusable.length) return
51
+ const pos = focusable.indexOf(active)
52
+ const next = focusable[(pos + delta + focusable.length) % focusable.length]
53
+ if (next != null) { openSub = -1; focusItem(next) }
54
+ }
55
+ function choose(item: MenuItem) {
56
+ if (item.disabled || item.separator) return
57
+ if (item.children?.length) { openSub = items.indexOf(item); return }
58
+ item.onSelect?.()
59
+ onselect(item)
60
+ onclose()
61
+ }
62
+ function onKeydown(e: KeyboardEvent, i: number) {
63
+ const item = items[i]
64
+ switch (e.key) {
65
+ case 'ArrowDown': e.preventDefault(); move(1); break
66
+ case 'ArrowUp': e.preventDefault(); move(-1); break
67
+ case 'Home': e.preventDefault(); focusItem(focusable[0] ?? 0); break
68
+ case 'End': e.preventDefault(); focusItem(focusable.at(-1) ?? 0); break
69
+ case 'ArrowRight':
70
+ if (item?.children?.length) { e.preventDefault(); openSub = i }
71
+ break
72
+ case 'ArrowLeft':
73
+ if (submenu) { e.preventDefault(); onclose() } // parent listens via its own onclose wiring
74
+ break
75
+ case 'Enter':
76
+ case ' ': e.preventDefault(); if (item) choose(item); break
77
+ case 'Escape': e.preventDefault(); onclose(); break
78
+ }
79
+ }
80
+ </script>
81
+
82
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
83
+ <div bind:this={listEl} class="sv-menu__list" class:is-submenu={submenu} role="menu">
84
+ {#each items as item, i (i)}
85
+ {#if item.separator}
86
+ <div class="sv-menu__sep" role="separator"></div>
87
+ {:else}
88
+ <div class="sv-menu__itemwrap">
89
+ <button
90
+ type="button"
91
+ class="sv-menu__item"
92
+ class:is-active={active === i}
93
+ data-mi={i}
94
+ role="menuitem"
95
+ aria-haspopup={item.children?.length ? 'menu' : undefined}
96
+ aria-expanded={item.children?.length ? openSub === i : undefined}
97
+ aria-disabled={item.disabled || undefined}
98
+ disabled={item.disabled}
99
+ tabindex={active === i || (active === -1 && i === focusable[0]) ? 0 : -1}
100
+ onpointerenter={() => { if (!item.disabled) { active = i; openSub = item.children?.length ? i : -1 } }}
101
+ onkeydown={(e) => onKeydown(e, i)}
102
+ onclick={() => choose(item)}
103
+ >
104
+ <span class="sv-menu__icon">{#if item.icon}{@render item.icon()}{/if}</span>
105
+ <span class="sv-menu__label">{item.label}</span>
106
+ {#if item.shortcut}<span class="sv-menu__shortcut">{item.shortcut}</span>{/if}
107
+ {#if item.children?.length}<svg class="sv-menu__chev" viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg>{/if}
108
+ </button>
109
+ {#if item.children?.length && openSub === i}
110
+ <div class="sv-menu__flyout">
111
+ <Self items={item.children} onclose={onclose} onselect={onselect} submenu />
112
+ </div>
113
+ {/if}
114
+ </div>
115
+ {/if}
116
+ {/each}
117
+ </div>
118
+
119
+ <style>
120
+ .sv-menu__list { display: flex; flex-direction: column; min-width: 180px; padding: 4px; }
121
+ .sv-menu__sep { height: 1px; background: var(--sg-border, #e2e8f0); margin: 4px 6px; }
122
+ .sv-menu__itemwrap { position: relative; }
123
+ .sv-menu__item {
124
+ display: flex; align-items: center; gap: 9px; width: 100%; box-sizing: border-box;
125
+ padding: 7px 10px; background: none; border: 0; border-radius: 6px; cursor: pointer;
126
+ font: inherit; font-size: 13px; color: var(--sg-fg, #0f172a); text-align: start;
127
+ transition: background 0.1s;
128
+ }
129
+ .sv-menu__item.is-active:not([disabled]), .sv-menu__item:hover:not([disabled]) { background: var(--sg-row-hover-bg, #f1f5f9); }
130
+ .sv-menu__item:focus-visible { outline: 2px solid var(--sg-focus-ring, var(--sg-accent, #2563eb)); outline-offset: -2px; }
131
+ .sv-menu__item[disabled] { opacity: 0.45; cursor: default; }
132
+ .sv-menu__icon { display: inline-flex; width: 16px; flex: none; color: var(--sg-muted, #64748b); }
133
+ .sv-menu__icon:empty { width: 0; }
134
+ .sv-menu__label { flex: 1; white-space: nowrap; }
135
+ .sv-menu__shortcut { color: var(--sg-muted, #94a3b8); font-size: 11.5px; margin-inline-start: 12px; }
136
+ .sv-menu__chev { color: var(--sg-muted, #94a3b8); flex: none; }
137
+ .sv-menu[dir='rtl'] .sv-menu__chev, .sv-menu__list.is-submenu .sv-menu__chev { }
138
+ .sv-menu__flyout {
139
+ position: absolute; top: -5px; inset-inline-start: 100%; z-index: 1;
140
+ background: var(--sg-bg, #fff); border: 1px solid var(--sg-border, #e2e8f0);
141
+ border-radius: 10px; box-shadow: 0 16px 40px -12px rgba(15,23,42,0.32); margin-inline-start: 2px;
142
+ }
143
+ </style>