@svgrid/grid 1.2.34 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (444) hide show
  1. package/README.md +6 -5
  2. package/dist/SvAccordion.svelte +109 -0
  3. package/dist/SvAccordion.svelte.d.ts +28 -0
  4. package/dist/SvAlert.svelte +92 -0
  5. package/dist/SvAlert.svelte.d.ts +27 -0
  6. package/dist/SvAutoComplete.svelte +47 -18
  7. package/dist/SvAutoComplete.svelte.d.ts +2 -5
  8. package/dist/SvAvatar.svelte +75 -0
  9. package/dist/SvAvatar.svelte.d.ts +14 -0
  10. package/dist/SvAvatarGroup.svelte +48 -0
  11. package/dist/SvAvatarGroup.svelte.d.ts +16 -0
  12. package/dist/SvBadge.svelte +54 -0
  13. package/dist/SvBadge.svelte.d.ts +23 -0
  14. package/dist/SvBreadcrumb.svelte +88 -0
  15. package/dist/SvBreadcrumb.svelte.d.ts +23 -0
  16. package/dist/SvButtonGroup.svelte +121 -0
  17. package/dist/SvButtonGroup.svelte.d.ts +16 -0
  18. package/dist/SvCalendar.svelte +72 -9
  19. package/dist/SvCalendar.svelte.d.ts +34 -1
  20. package/dist/SvCard.svelte +71 -0
  21. package/dist/SvCard.svelte.d.ts +28 -0
  22. package/dist/SvCarousel.svelte +134 -0
  23. package/dist/SvCarousel.svelte.d.ts +28 -0
  24. package/dist/SvCheckBox.svelte +36 -21
  25. package/dist/SvCheckBox.svelte.d.ts +2 -5
  26. package/dist/SvChip.svelte +88 -0
  27. package/dist/SvChip.svelte.d.ts +31 -0
  28. package/dist/SvCircularProgress.svelte +109 -0
  29. package/dist/SvCircularProgress.svelte.d.ts +31 -0
  30. package/dist/SvColorInput.svelte +54 -27
  31. package/dist/SvColorInput.svelte.d.ts +10 -6
  32. package/dist/SvComboBox.svelte +119 -33
  33. package/dist/SvComboBox.svelte.d.ts +18 -6
  34. package/dist/SvCommand.svelte +197 -0
  35. package/dist/SvCommand.svelte.d.ts +26 -0
  36. package/dist/SvContextMenu.svelte +96 -0
  37. package/dist/SvContextMenu.svelte.d.ts +25 -0
  38. package/dist/SvCountryInput.svelte +56 -21
  39. package/dist/SvCountryInput.svelte.d.ts +9 -5
  40. package/dist/SvDateRangeInput.svelte +256 -0
  41. package/dist/SvDateRangeInput.svelte.d.ts +37 -0
  42. package/dist/SvDateTimePicker.svelte +57 -20
  43. package/dist/SvDateTimePicker.svelte.d.ts +12 -4
  44. package/dist/SvDivider.svelte +61 -0
  45. package/dist/SvDivider.svelte.d.ts +22 -0
  46. package/dist/SvDrawer.svelte +226 -0
  47. package/dist/SvDrawer.svelte.d.ts +43 -0
  48. package/dist/SvDropDownList.svelte +114 -35
  49. package/dist/SvDropDownList.svelte.d.ts +7 -6
  50. package/dist/SvDurationInput.svelte +108 -0
  51. package/dist/SvDurationInput.svelte.d.ts +15 -0
  52. package/dist/SvEmptyState.svelte +59 -0
  53. package/dist/SvEmptyState.svelte.d.ts +25 -0
  54. package/dist/SvField.svelte +85 -0
  55. package/dist/SvField.svelte.d.ts +30 -0
  56. package/dist/SvFileUpload.svelte +169 -0
  57. package/dist/SvFileUpload.svelte.d.ts +24 -0
  58. package/dist/SvForm.svelte +7 -1
  59. package/dist/SvForm.svelte.d.ts +3 -0
  60. package/dist/SvGauge.svelte +7 -1
  61. package/dist/SvGauge.svelte.d.ts +11 -0
  62. package/dist/SvGrid.controller.svelte.d.ts +6 -0
  63. package/dist/SvGrid.controller.svelte.js +64 -9
  64. package/dist/SvGrid.css +33 -0
  65. package/dist/SvGrid.svelte +116 -3
  66. package/dist/SvGrid.types.d.ts +366 -0
  67. package/dist/SvGridBoard.svelte +2281 -0
  68. package/dist/SvGridBoard.svelte.d.ts +31 -0
  69. package/dist/SvGridDropdown.svelte +2 -1
  70. package/dist/SvGridSelect.svelte +255 -0
  71. package/dist/SvGridSelect.svelte.d.ts +32 -0
  72. package/dist/SvGroupCell.svelte +109 -0
  73. package/dist/SvGroupCell.svelte.d.ts +41 -0
  74. package/dist/SvListBox.svelte +123 -36
  75. package/dist/SvListBox.svelte.d.ts +12 -8
  76. package/dist/SvMaskedInput.svelte +53 -13
  77. package/dist/SvMaskedInput.svelte.d.ts +16 -6
  78. package/dist/SvMenu.svelte +101 -0
  79. package/dist/SvMenu.svelte.d.ts +26 -0
  80. package/dist/SvMenuList.svelte +143 -0
  81. package/dist/SvMenuList.svelte.d.ts +24 -0
  82. package/dist/SvModal.svelte +220 -0
  83. package/dist/SvModal.svelte.d.ts +31 -0
  84. package/dist/SvMultiSelect.svelte +277 -0
  85. package/dist/SvMultiSelect.svelte.d.ts +31 -0
  86. package/dist/SvNavPane.svelte +314 -0
  87. package/dist/SvNavPane.svelte.d.ts +59 -0
  88. package/dist/SvNumberInput.svelte +23 -9
  89. package/dist/SvNumberInput.svelte.d.ts +2 -0
  90. package/dist/SvOtpInput.svelte +157 -0
  91. package/dist/SvOtpInput.svelte.d.ts +16 -0
  92. package/dist/SvPagination.svelte +100 -0
  93. package/dist/SvPagination.svelte.d.ts +16 -0
  94. package/dist/SvPasswordInput.svelte +42 -12
  95. package/dist/SvPasswordInput.svelte.d.ts +13 -6
  96. package/dist/SvPhoneInput.svelte +43 -11
  97. package/dist/SvPhoneInput.svelte.d.ts +10 -11
  98. package/dist/SvPopover.svelte +127 -0
  99. package/dist/SvPopover.svelte.d.ts +35 -0
  100. package/dist/SvProgress.svelte +116 -0
  101. package/dist/SvProgress.svelte.d.ts +32 -0
  102. package/dist/SvRadioGroup.svelte +40 -22
  103. package/dist/SvRadioGroup.svelte.d.ts +2 -10
  104. package/dist/SvRating.svelte +51 -30
  105. package/dist/SvRating.svelte.d.ts +10 -7
  106. package/dist/SvRichText.svelte +192 -0
  107. package/dist/SvRichText.svelte.d.ts +16 -0
  108. package/dist/SvRowGroupPanel.svelte +147 -0
  109. package/dist/SvRowGroupPanel.svelte.d.ts +17 -0
  110. package/dist/SvScrollArea.svelte +50 -0
  111. package/dist/SvScrollArea.svelte.d.ts +22 -0
  112. package/dist/SvSkeleton.svelte +76 -0
  113. package/dist/SvSkeleton.svelte.d.ts +25 -0
  114. package/dist/SvSlider.svelte +41 -9
  115. package/dist/SvSlider.svelte.d.ts +4 -4
  116. package/dist/SvSparkline.svelte +77 -0
  117. package/dist/SvSparkline.svelte.d.ts +28 -0
  118. package/dist/SvSplitter.svelte +138 -0
  119. package/dist/SvSplitter.svelte.d.ts +32 -0
  120. package/dist/SvStat.svelte +86 -0
  121. package/dist/SvStat.svelte.d.ts +30 -0
  122. package/dist/SvStepper.svelte +91 -0
  123. package/dist/SvStepper.svelte.d.ts +24 -0
  124. package/dist/SvSwitchButton.svelte +39 -24
  125. package/dist/SvSwitchButton.svelte.d.ts +2 -5
  126. package/dist/SvTabs.svelte +147 -28
  127. package/dist/SvTabs.svelte.d.ts +16 -4
  128. package/dist/SvTagsInput.svelte +59 -22
  129. package/dist/SvTagsInput.svelte.d.ts +9 -4
  130. package/dist/SvTextArea.svelte +113 -0
  131. package/dist/SvTextArea.svelte.d.ts +18 -0
  132. package/dist/SvTextInput.svelte +115 -0
  133. package/dist/SvTextInput.svelte.d.ts +20 -0
  134. package/dist/SvTimePicker.svelte +19 -5
  135. package/dist/SvTimePicker.svelte.d.ts +12 -0
  136. package/dist/SvTimeline.svelte +75 -0
  137. package/dist/SvTimeline.svelte.d.ts +24 -0
  138. package/dist/SvToaster.svelte +124 -0
  139. package/dist/SvToaster.svelte.d.ts +10 -0
  140. package/dist/SvToggleButton.svelte +37 -12
  141. package/dist/SvToggleButton.svelte.d.ts +2 -4
  142. package/dist/SvTooltip.svelte +113 -0
  143. package/dist/SvTooltip.svelte.d.ts +22 -0
  144. package/dist/SvTour.svelte +151 -0
  145. package/dist/SvTour.svelte.d.ts +25 -0
  146. package/dist/SvTree.svelte +286 -44
  147. package/dist/SvTree.svelte.d.ts +26 -9
  148. package/dist/SvTreeSelect.svelte +222 -0
  149. package/dist/SvTreeSelect.svelte.d.ts +24 -0
  150. package/dist/a11y/dismissable.d.ts +32 -0
  151. package/dist/a11y/dismissable.js +54 -0
  152. package/dist/a11y/focus-trap.d.ts +39 -0
  153. package/dist/a11y/focus-trap.js +142 -0
  154. package/dist/a11y/live-region.d.ts +8 -0
  155. package/dist/a11y/live-region.js +60 -0
  156. package/dist/a11y/scroll-lock.d.ts +4 -0
  157. package/dist/a11y/scroll-lock.js +41 -0
  158. package/dist/a11y.d.ts +3 -1
  159. package/dist/a11y.js +1 -1
  160. package/dist/avatar.d.ts +10 -0
  161. package/dist/avatar.js +22 -0
  162. package/dist/builtin-editors.d.ts +5 -0
  163. package/dist/builtin-editors.js +55 -0
  164. package/dist/cdn/svgrid.js +16021 -8234
  165. package/dist/cdn/svgrid.svelte-external.js +14542 -6764
  166. package/dist/columns.js +3 -2
  167. package/dist/core.d.ts +7 -1
  168. package/dist/countries.d.ts +12 -0
  169. package/dist/countries.js +24 -0
  170. package/dist/createAccordion.svelte.d.ts +71 -0
  171. package/dist/createAccordion.svelte.js +99 -0
  172. package/dist/createAutocomplete.svelte.d.ts +13 -4
  173. package/dist/createAutocomplete.svelte.js +11 -1
  174. package/dist/createButtonGroup.svelte.d.ts +62 -0
  175. package/dist/createButtonGroup.svelte.js +154 -0
  176. package/dist/createCalendar.svelte.d.ts +5 -0
  177. package/dist/createCalendar.svelte.js +2 -0
  178. package/dist/createCheckbox.svelte.d.ts +9 -17
  179. package/dist/createCheckbox.svelte.js +28 -1
  180. package/dist/createColorInput.svelte.d.ts +12 -22
  181. package/dist/createColorInput.svelte.js +13 -0
  182. package/dist/createCombobox.svelte.d.ts +16 -5
  183. package/dist/createCombobox.svelte.js +13 -2
  184. package/dist/createCountryInput.svelte.d.ts +14 -3
  185. package/dist/createCountryInput.svelte.js +12 -2
  186. package/dist/createDateTimePicker.svelte.d.ts +17 -2
  187. package/dist/createDateTimePicker.svelte.js +12 -2
  188. package/dist/createDropdownList.svelte.d.ts +14 -5
  189. package/dist/createDropdownList.svelte.js +50 -1
  190. package/dist/createFileUpload.svelte.d.ts +68 -0
  191. package/dist/createFileUpload.svelte.js +122 -0
  192. package/dist/createListbox.svelte.d.ts +10 -1
  193. package/dist/createListbox.svelte.js +48 -1
  194. package/dist/createMaskedInput.svelte.d.ts +11 -2
  195. package/dist/createMaskedInput.svelte.js +11 -1
  196. package/dist/createNumberInput.svelte.d.ts +1 -0
  197. package/dist/createNumberInput.svelte.js +1 -0
  198. package/dist/createPasswordInput.svelte.d.ts +19 -21
  199. package/dist/createPasswordInput.svelte.js +16 -4
  200. package/dist/createPhoneInput.svelte.d.ts +26 -7
  201. package/dist/createPhoneInput.svelte.js +24 -3
  202. package/dist/createPopoverSelect.svelte.d.ts +86 -0
  203. package/dist/createPopoverSelect.svelte.js +196 -0
  204. package/dist/createRadioGroup.svelte.d.ts +9 -0
  205. package/dist/createRadioGroup.svelte.js +11 -1
  206. package/dist/createRating.svelte.d.ts +11 -0
  207. package/dist/createRating.svelte.js +12 -1
  208. package/dist/createSlider.svelte.d.ts +2 -0
  209. package/dist/createSlider.svelte.js +16 -5
  210. package/dist/createSplitter.svelte.d.ts +67 -0
  211. package/dist/createSplitter.svelte.js +99 -0
  212. package/dist/createSwitch.svelte.d.ts +9 -16
  213. package/dist/createSwitch.svelte.js +28 -1
  214. package/dist/createTabs.svelte.d.ts +7 -0
  215. package/dist/createTabs.svelte.js +17 -2
  216. package/dist/createTagsInput.svelte.d.ts +13 -1
  217. package/dist/createTagsInput.svelte.js +12 -3
  218. package/dist/createToggle.svelte.d.ts +9 -0
  219. package/dist/createToggle.svelte.js +12 -1
  220. package/dist/createTree.svelte.d.ts +19 -0
  221. package/dist/createTree.svelte.js +108 -15
  222. package/dist/debounce.d.ts +11 -0
  223. package/dist/debounce.js +30 -0
  224. package/dist/duration.d.ts +15 -0
  225. package/dist/duration.js +51 -0
  226. package/dist/editor-contract.d.ts +62 -7
  227. package/dist/editor-contract.js +45 -5
  228. package/dist/editor-registry.d.ts +67 -0
  229. package/dist/editor-registry.js +38 -0
  230. package/dist/editors/cell-editors.d.ts +1 -1
  231. package/dist/editors/cell-editors.js +5 -0
  232. package/dist/fuzzy.d.ts +3 -0
  233. package/dist/fuzzy.js +41 -0
  234. package/dist/grid-select.d.ts +9 -0
  235. package/dist/grid-select.js +8 -0
  236. package/dist/index.d.ts +79 -6
  237. package/dist/index.js +78 -4
  238. package/dist/keyboard-handlers.js +19 -0
  239. package/dist/list-option.d.ts +36 -0
  240. package/dist/list-option.js +68 -0
  241. package/dist/otp.d.ts +16 -0
  242. package/dist/otp.js +26 -0
  243. package/dist/paginate.d.ts +20 -0
  244. package/dist/paginate.js +40 -0
  245. package/dist/popover.d.ts +15 -1
  246. package/dist/popover.js +29 -3
  247. package/dist/recurrence.d.ts +36 -0
  248. package/dist/recurrence.js +90 -0
  249. package/dist/server-data-source.d.ts +89 -0
  250. package/dist/server-data-source.js +4 -0
  251. package/dist/server-group-model.d.ts +98 -0
  252. package/dist/server-group-model.js +263 -0
  253. package/dist/toast-store.svelte.d.ts +41 -0
  254. package/dist/toast-store.svelte.js +113 -0
  255. package/dist/tree-select.d.ts +19 -0
  256. package/dist/tree-select.js +40 -0
  257. package/dist/ui-app.types.d.ts +80 -0
  258. package/dist/ui-app.types.js +7 -0
  259. package/dist/validators.d.ts +43 -0
  260. package/dist/validators.js +53 -0
  261. package/dist/virtual.d.ts +35 -0
  262. package/dist/virtual.js +34 -0
  263. package/package.json +1 -1
  264. package/src/SvAccordion.svelte +109 -0
  265. package/src/SvAccordion.test.ts +66 -0
  266. package/src/SvAlert.svelte +92 -0
  267. package/src/SvAutoComplete.svelte +47 -18
  268. package/src/SvAvatar.svelte +75 -0
  269. package/src/SvAvatarGroup.svelte +48 -0
  270. package/src/SvBadge.svelte +54 -0
  271. package/src/SvBreadcrumb.svelte +88 -0
  272. package/src/SvButtonGroup.svelte +121 -0
  273. package/src/SvButtonGroup.test.ts +72 -0
  274. package/src/SvCalendar.svelte +72 -9
  275. package/src/SvCard.svelte +71 -0
  276. package/src/SvCarousel.svelte +134 -0
  277. package/src/SvCheckBox.svelte +36 -21
  278. package/src/SvChip.svelte +88 -0
  279. package/src/SvCircularProgress.svelte +109 -0
  280. package/src/SvColorInput.svelte +54 -27
  281. package/src/SvComboBox.svelte +119 -33
  282. package/src/SvCommand.svelte +197 -0
  283. package/src/SvContextMenu.svelte +96 -0
  284. package/src/SvCountryInput.svelte +56 -21
  285. package/src/SvDateRangeInput.svelte +256 -0
  286. package/src/SvDateRangeInput.test.ts +80 -0
  287. package/src/SvDateTimePicker.svelte +57 -20
  288. package/src/SvDivider.svelte +61 -0
  289. package/src/SvDrawer.svelte +226 -0
  290. package/src/SvDrawer.test.ts +125 -0
  291. package/src/SvDropDownList.svelte +114 -35
  292. package/src/SvDurationInput.svelte +108 -0
  293. package/src/SvEmptyState.svelte +59 -0
  294. package/src/SvField.svelte +85 -0
  295. package/src/SvFileUpload.svelte +169 -0
  296. package/src/SvFileUpload.test.ts +59 -0
  297. package/src/SvForm.svelte +7 -1
  298. package/src/SvGauge.svelte +7 -1
  299. package/src/SvGrid.controller.svelte.ts +68 -9
  300. package/src/SvGrid.css +33 -0
  301. package/src/SvGrid.svelte +116 -3
  302. package/src/SvGrid.types.ts +374 -0
  303. package/src/SvGridBoard.svelte +2281 -0
  304. package/src/SvGridDropdown.svelte +2 -1
  305. package/src/SvGridSelect.svelte +255 -0
  306. package/src/SvGridSelect.test.ts +100 -0
  307. package/src/SvGroupCell.svelte +109 -0
  308. package/src/SvListBox.svelte +123 -36
  309. package/src/SvMaskedInput.svelte +53 -13
  310. package/src/SvMenu.svelte +101 -0
  311. package/src/SvMenu.test.ts +73 -0
  312. package/src/SvMenuList.svelte +143 -0
  313. package/src/SvModal.svelte +220 -0
  314. package/src/SvMultiSelect.svelte +277 -0
  315. package/src/SvMultiSelect.test.ts +167 -0
  316. package/src/SvNavPane.svelte +314 -0
  317. package/src/SvNavPane.test.ts +70 -0
  318. package/src/SvNumberInput.svelte +23 -9
  319. package/src/SvOtpInput.svelte +157 -0
  320. package/src/SvPagination.svelte +100 -0
  321. package/src/SvPasswordInput.svelte +42 -12
  322. package/src/SvPhoneInput.svelte +43 -11
  323. package/src/SvPopover.svelte +127 -0
  324. package/src/SvProgress.svelte +116 -0
  325. package/src/SvRadioGroup.svelte +40 -22
  326. package/src/SvRating.svelte +51 -30
  327. package/src/SvRichText.svelte +192 -0
  328. package/src/SvRowGroupPanel.svelte +147 -0
  329. package/src/SvScrollArea.svelte +50 -0
  330. package/src/SvSkeleton.svelte +76 -0
  331. package/src/SvSlider.svelte +41 -9
  332. package/src/SvSparkline.svelte +77 -0
  333. package/src/SvSplitter.svelte +138 -0
  334. package/src/SvSplitter.test.ts +55 -0
  335. package/src/SvStat.svelte +86 -0
  336. package/src/SvStepper.svelte +91 -0
  337. package/src/SvSwitchButton.svelte +39 -24
  338. package/src/SvTabs.svelte +147 -28
  339. package/src/SvTagsInput.svelte +59 -22
  340. package/src/SvTextArea.svelte +113 -0
  341. package/src/SvTextInput.svelte +115 -0
  342. package/src/SvTimePicker.svelte +19 -5
  343. package/src/SvTimeline.svelte +75 -0
  344. package/src/SvToaster.svelte +124 -0
  345. package/src/SvToaster.test.ts +63 -0
  346. package/src/SvToggleButton.svelte +37 -12
  347. package/src/SvTooltip.svelte +113 -0
  348. package/src/SvTour.svelte +151 -0
  349. package/src/SvTree.svelte +286 -44
  350. package/src/SvTreeSelect.svelte +222 -0
  351. package/src/SvTreeSelect.test.ts +87 -0
  352. package/src/a11y/dismissable.test.ts +95 -0
  353. package/src/a11y/dismissable.ts +94 -0
  354. package/src/a11y/focus-trap.test.ts +110 -0
  355. package/src/a11y/focus-trap.ts +134 -0
  356. package/src/a11y/live-region.test.ts +40 -0
  357. package/src/a11y/live-region.ts +65 -0
  358. package/src/a11y/scroll-lock.test.ts +38 -0
  359. package/src/a11y/scroll-lock.ts +41 -0
  360. package/src/a11y.ts +3 -1
  361. package/src/avatar.test.ts +32 -0
  362. package/src/avatar.ts +21 -0
  363. package/src/board.test.ts +927 -0
  364. package/src/builtin-editors.grid.test.ts +85 -0
  365. package/src/builtin-editors.test.ts +58 -0
  366. package/src/builtin-editors.ts +56 -0
  367. package/src/columns.ts +3 -2
  368. package/src/core.ts +10 -0
  369. package/src/countries.ts +25 -0
  370. package/src/createAccordion.svelte.ts +126 -0
  371. package/src/createAutocomplete.svelte.ts +18 -1
  372. package/src/createButtonGroup.svelte.ts +166 -0
  373. package/src/createCalendar.svelte.ts +6 -0
  374. package/src/createCheckbox.svelte.ts +23 -1
  375. package/src/createColorInput.svelte.ts +20 -0
  376. package/src/createCombobox.svelte.ts +22 -2
  377. package/src/createCountryInput.svelte.ts +21 -2
  378. package/src/createDateTimePicker.svelte.ts +23 -2
  379. package/src/createDropdownList.svelte.ts +31 -2
  380. package/src/createFileUpload.svelte.ts +144 -0
  381. package/src/createListbox.svelte.ts +35 -3
  382. package/src/createMaskedInput.svelte.ts +18 -1
  383. package/src/createNumberInput.svelte.ts +2 -0
  384. package/src/createPasswordInput.svelte.ts +31 -5
  385. package/src/createPhoneInput.svelte.ts +38 -4
  386. package/src/createPopoverSelect.svelte.ts +208 -0
  387. package/src/createRadioGroup.svelte.ts +23 -1
  388. package/src/createRating.svelte.ts +26 -1
  389. package/src/createSlider.svelte.ts +14 -3
  390. package/src/createSplitter.svelte.ts +119 -0
  391. package/src/createSwitch.svelte.ts +26 -3
  392. package/src/createTabs.svelte.ts +20 -3
  393. package/src/createTagsInput.svelte.ts +24 -3
  394. package/src/createToggle.svelte.ts +25 -2
  395. package/src/createTree.svelte.ts +104 -14
  396. package/src/debounce.test.ts +50 -0
  397. package/src/debounce.ts +41 -0
  398. package/src/duration.test.ts +41 -0
  399. package/src/duration.ts +51 -0
  400. package/src/editor-contract.ts +87 -8
  401. package/src/editor-registry.grid.test.ts +98 -0
  402. package/src/editor-registry.test.ts +86 -0
  403. package/src/editor-registry.ts +106 -0
  404. package/src/editors/cell-editors.ts +10 -1
  405. package/src/fuzzy.test.ts +40 -0
  406. package/src/fuzzy.ts +39 -0
  407. package/src/grid-select.test.ts +28 -0
  408. package/src/grid-select.ts +20 -0
  409. package/src/index.ts +150 -5
  410. package/src/keyboard-handlers.ts +20 -0
  411. package/src/list-option.ts +74 -0
  412. package/src/list-power.test.ts +71 -0
  413. package/src/otp.test.ts +28 -0
  414. package/src/otp.ts +28 -0
  415. package/src/overlays.test.ts +85 -0
  416. package/src/paginate.test.ts +41 -0
  417. package/src/paginate.ts +61 -0
  418. package/src/phone-valid.test.ts +26 -0
  419. package/src/popover.ts +31 -3
  420. package/src/progress.test.ts +55 -0
  421. package/src/recurrence.test.ts +91 -0
  422. package/src/recurrence.ts +111 -0
  423. package/src/server-data-source.ts +102 -0
  424. package/src/server-group-model.test.ts +294 -0
  425. package/src/server-group-model.ts +370 -0
  426. package/src/svgrid.responsive.test.ts +72 -0
  427. package/src/toast-store.svelte.ts +148 -0
  428. package/src/toast-store.test.ts +86 -0
  429. package/src/tree-edit.test.ts +49 -0
  430. package/src/tree-filter.test.ts +56 -0
  431. package/src/tree-select.test.ts +48 -0
  432. package/src/tree-select.ts +62 -0
  433. package/src/ui-app.test.ts +130 -0
  434. package/src/ui-app.types.ts +88 -0
  435. package/src/ui-display.test.ts +79 -0
  436. package/src/ui-inputs.test.ts +1 -1
  437. package/src/ui-status.test.ts +141 -0
  438. package/src/ui-tier1.test.ts +142 -0
  439. package/src/ui-tier2.test.ts +124 -0
  440. package/src/ui-tier3.test.ts +139 -0
  441. package/src/validators.test.ts +58 -0
  442. package/src/validators.ts +105 -0
  443. package/src/virtual.test.ts +72 -0
  444. package/src/virtual.ts +67 -0
@@ -0,0 +1,220 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvModal - an accessible modal dialog: backdrop, focus trap, Escape / backdrop
4
+ * close, and a scale-in animation. Optionally draggable (by its header) and
5
+ * resizable (bottom-right handle). Parity: Smart `smart-window` (modal mode).
6
+ * Portalled to <body> so it overlays everything. Controlled via `open`.
7
+ *
8
+ * ```svelte
9
+ * <SvModal bind:open title="Edit row" draggable resizable>
10
+ * <p>Body</p>
11
+ * {#snippet footer()}<button onclick={() => open = false}>Close</button>{/snippet}
12
+ * </SvModal>
13
+ * ```
14
+ */
15
+ import type { Snippet } from 'svelte'
16
+ import { portalToBody } from './popover'
17
+ import { nextEditorId } from './editor-contract'
18
+ import { createFocusTrap } from './a11y/focus-trap'
19
+ import { lockScroll } from './a11y/scroll-lock'
20
+ import { createDismissableLayer } from './a11y/dismissable'
21
+
22
+ type Props = {
23
+ open?: boolean
24
+ onClose?: () => void
25
+ title?: string
26
+ size?: 'sm' | 'md' | 'lg'
27
+ draggable?: boolean
28
+ resizable?: boolean
29
+ closeOnBackdrop?: boolean
30
+ closeOnEsc?: boolean
31
+ /** Hide the header close (x) button. */
32
+ hideClose?: boolean
33
+ children?: Snippet
34
+ footer?: Snippet
35
+ }
36
+
37
+ let {
38
+ open = $bindable(false),
39
+ onClose,
40
+ title,
41
+ size = 'md',
42
+ draggable = false,
43
+ resizable = false,
44
+ closeOnBackdrop = true,
45
+ closeOnEsc = true,
46
+ hideClose = false,
47
+ children,
48
+ footer,
49
+ }: Props = $props()
50
+
51
+ const titleId = nextEditorId('sv-modal-title')
52
+ let dialogEl = $state<HTMLDivElement | null>(null)
53
+ let drag = $state({ x: 0, y: 0 })
54
+ let box = $state<{ w: number; h: number } | null>(null)
55
+
56
+ function close() { open = false; onClose?.() }
57
+
58
+ // Reset transient drag/resize each time it opens, then hand focus, scroll-lock
59
+ // and Escape/backdrop dismissal to the shared a11y primitives (one tested
60
+ // implementation, and nested overlays close top-first via the layer stack).
61
+ $effect(() => {
62
+ if (!open || !dialogEl) return
63
+ drag = { x: 0, y: 0 }
64
+ box = null
65
+ const trap = createFocusTrap(dialogEl)
66
+ trap.activate()
67
+ const unlockScroll = lockScroll()
68
+ const layer = createDismissableLayer({
69
+ element: () => dialogEl,
70
+ onDismiss: (reason) => {
71
+ if (reason === 'escape' ? closeOnEsc : closeOnBackdrop) close()
72
+ },
73
+ })
74
+ layer.activate()
75
+ return () => {
76
+ layer.release()
77
+ unlockScroll()
78
+ trap.release()
79
+ }
80
+ })
81
+
82
+ // --- Drag (header) ---------------------------------------------------------
83
+ let dragging = false
84
+ let dragStart = { x: 0, y: 0, ox: 0, oy: 0 }
85
+ function onHeaderDown(e: PointerEvent) {
86
+ if (!draggable || (e.target as HTMLElement).closest('.sv-modal__x')) return
87
+ dragging = true
88
+ dragStart = { x: e.clientX, y: e.clientY, ox: drag.x, oy: drag.y }
89
+ ;(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId)
90
+ }
91
+ function onHeaderMove(e: PointerEvent) {
92
+ if (!dragging) return
93
+ drag = { x: dragStart.ox + (e.clientX - dragStart.x), y: dragStart.oy + (e.clientY - dragStart.y) }
94
+ }
95
+ function onHeaderUp(e: PointerEvent) {
96
+ if (!dragging) return
97
+ dragging = false
98
+ ;(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId)
99
+ }
100
+
101
+ // --- Resize (bottom-right handle) ------------------------------------------
102
+ let resizing = false
103
+ let resizeStart = { x: 0, y: 0, w: 0, h: 0 }
104
+ function onResizeDown(e: PointerEvent) {
105
+ if (!resizable || !dialogEl) return
106
+ e.stopPropagation()
107
+ resizing = true
108
+ const r = dialogEl.getBoundingClientRect()
109
+ resizeStart = { x: e.clientX, y: e.clientY, w: r.width, h: r.height }
110
+ box = { w: r.width, h: r.height }
111
+ ;(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId)
112
+ }
113
+ function onResizeMove(e: PointerEvent) {
114
+ if (!resizing) return
115
+ box = {
116
+ w: Math.max(240, resizeStart.w + (e.clientX - resizeStart.x)),
117
+ h: Math.max(160, resizeStart.h + (e.clientY - resizeStart.y)),
118
+ }
119
+ }
120
+ function onResizeUp(e: PointerEvent) {
121
+ if (!resizing) return
122
+ resizing = false
123
+ ;(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId)
124
+ }
125
+ </script>
126
+
127
+ {#if open}
128
+ <div class="sv-modal__backdrop" use:portalToBody>
129
+ <div
130
+ bind:this={dialogEl}
131
+ class="sv-modal sv-modal--{size}"
132
+ class:is-resized={!!box}
133
+ role="dialog"
134
+ aria-modal="true"
135
+ aria-labelledby={title ? titleId : undefined}
136
+ tabindex="-1"
137
+ style:transform={`translate(calc(-50% + ${drag.x}px), calc(-50% + ${drag.y}px))`}
138
+ style:width={box ? `${box.w}px` : undefined}
139
+ style:height={box ? `${box.h}px` : undefined}
140
+ >
141
+ {#if title || !hideClose}
142
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
143
+ <div
144
+ class="sv-modal__header"
145
+ class:is-draggable={draggable}
146
+ onpointerdown={onHeaderDown}
147
+ onpointermove={onHeaderMove}
148
+ onpointerup={onHeaderUp}
149
+ onpointercancel={onHeaderUp}
150
+ >
151
+ <h2 class="sv-modal__title" id={titleId}>{title ?? ''}</h2>
152
+ {#if !hideClose}
153
+ <button type="button" class="sv-modal__x" aria-label="Close" onclick={close}>&times;</button>
154
+ {/if}
155
+ </div>
156
+ {/if}
157
+ <div class="sv-modal__body">{@render children?.()}</div>
158
+ {#if footer}<div class="sv-modal__footer">{@render footer()}</div>{/if}
159
+ {#if resizable}
160
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
161
+ <div
162
+ class="sv-modal__resize"
163
+ aria-hidden="true"
164
+ onpointerdown={onResizeDown}
165
+ onpointermove={onResizeMove}
166
+ onpointerup={onResizeUp}
167
+ onpointercancel={onResizeUp}
168
+ ></div>
169
+ {/if}
170
+ </div>
171
+ </div>
172
+ {/if}
173
+
174
+ <style>
175
+ :global(.sv-modal__backdrop) {
176
+ position: fixed; inset: 0; z-index: 2147483000;
177
+ background: rgba(15, 23, 42, 0.45); backdrop-filter: blur(1.5px);
178
+ animation: sv-modal-fade 0.16s ease both;
179
+ }
180
+ @keyframes sv-modal-fade { from { opacity: 0 } to { opacity: 1 } }
181
+ @media (prefers-reduced-motion: reduce) { :global(.sv-modal__backdrop) { animation: none } }
182
+
183
+ :global(.sv-modal) {
184
+ position: fixed; top: 50%; left: 50%;
185
+ display: flex; flex-direction: column; max-width: 94vw; max-height: 88vh; min-width: 240px;
186
+ background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
187
+ border: 1px solid var(--sg-border, #e2e8f0); border-radius: 12px; overflow: hidden;
188
+ box-shadow: 0 24px 64px -16px rgba(15, 23, 42, 0.5);
189
+ animation: sv-modal-in 0.18s cubic-bezier(0.16, 1, 0.3, 1) both;
190
+ }
191
+ @keyframes sv-modal-in { from { opacity: 0; transform: translate(-50%, -50%) scale(0.94) } to { opacity: 1 } }
192
+ @media (prefers-reduced-motion: reduce) { :global(.sv-modal) { animation: none } }
193
+ :global(.sv-modal--sm) { width: 360px; }
194
+ :global(.sv-modal--md) { width: 520px; }
195
+ :global(.sv-modal--lg) { width: 720px; }
196
+ :global(.sv-modal.is-resized) { max-width: none; max-height: none; }
197
+
198
+ :global(.sv-modal__header) {
199
+ display: flex; align-items: center; gap: 12px; padding: 13px 16px;
200
+ border-bottom: 1px solid var(--sg-border, #e2e8f0);
201
+ }
202
+ :global(.sv-modal__header.is-draggable) { cursor: move; touch-action: none; user-select: none; }
203
+ :global(.sv-modal__title) { flex: 1; margin: 0; font-size: 15px; font-weight: 650; }
204
+ :global(.sv-modal__x) {
205
+ flex: none; width: 28px; height: 28px; display: grid; place-items: center;
206
+ background: none; border: 0; color: var(--sg-muted, #64748b); cursor: pointer; border-radius: 6px; font-size: 20px; line-height: 1;
207
+ }
208
+ :global(.sv-modal__x:hover) { background: var(--sg-row-hover-bg, #f1f5f9); color: var(--sg-fg, #0f172a); }
209
+ :global(.sv-modal__body) { padding: 16px; overflow: auto; flex: 1; font-size: 13.5px; line-height: 1.6; }
210
+ :global(.sv-modal__footer) {
211
+ display: flex; justify-content: flex-end; gap: 8px; padding: 12px 16px;
212
+ border-top: 1px solid var(--sg-border, #e2e8f0);
213
+ }
214
+ :global(.sv-modal__resize) {
215
+ position: absolute; inset-inline-end: 0; bottom: 0; width: 16px; height: 16px;
216
+ cursor: nwse-resize; touch-action: none;
217
+ background: linear-gradient(135deg, transparent 50%, var(--sg-muted, #94a3b8) 50%, var(--sg-muted, #94a3b8) 60%, transparent 60%, transparent 72%, var(--sg-muted, #94a3b8) 72%, var(--sg-muted, #94a3b8) 82%, transparent 82%);
218
+ opacity: 0.6;
219
+ }
220
+ </style>
@@ -0,0 +1,277 @@
1
+ <script lang="ts" module>
2
+ export type { MultiSelectOption } from './ui-app.types'
3
+ </script>
4
+
5
+ <script lang="ts">
6
+ /**
7
+ * SvMultiSelect - a multi-select dropdown: a trigger showing the picked items
8
+ * as chips (collapsing to "+N"), and a portalled panel with an optional search
9
+ * box and a checkbox list. The open/position/dismiss/roving/keyboard/ARIA
10
+ * mechanics come from the shared `createPopoverSelect` core; this component adds
11
+ * the multi-select toggle, chips and search UI. Distinct from SvTagsInput
12
+ * (free-form tags) - this picks from a fixed set.
13
+ */
14
+ import type { MultiSelectOption } 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 { debounce } from './debounce'
20
+
21
+ type Props = Pick<
22
+ SvEditorProps,
23
+ 'disabled' | 'label' | 'hint' | 'error' | 'required' | 'invalid' | 'size' | 'id' | 'name' | 'dir' | 'ariaLabel'
24
+ > & {
25
+ options: ReadonlyArray<MultiSelectOption>
26
+ value?: ReadonlyArray<string | number>
27
+ onChange?: (value: Array<string | number>) => void
28
+ onCommit?: (value: Array<string | number>) => void
29
+ onCancel?: () => void
30
+ placeholder?: string
31
+ searchable?: boolean
32
+ maxTagCount?: number
33
+ clearable?: boolean
34
+ /** Async/remote options: called (debounced) with the search query; its
35
+ * result replaces the list. When set, `options` is only the initial list. */
36
+ loadOptions?: (query: string) => Promise<MultiSelectOption[]>
37
+ /** Debounce for remote search. Default 250ms. */
38
+ debounceMs?: number
39
+ loadingText?: string
40
+ }
41
+
42
+ let {
43
+ options,
44
+ value = $bindable<ReadonlyArray<string | number>>([]),
45
+ onChange,
46
+ onCommit,
47
+ onCancel,
48
+ placeholder = 'Select…',
49
+ searchable = true,
50
+ maxTagCount = 3,
51
+ clearable = true,
52
+ loadOptions,
53
+ debounceMs = 250,
54
+ loadingText = 'Loading…',
55
+ disabled = false,
56
+ label,
57
+ hint,
58
+ error,
59
+ required = false,
60
+ invalid = false,
61
+ size = 'md',
62
+ id,
63
+ name,
64
+ dir,
65
+ ariaLabel,
66
+ }: Props = $props()
67
+
68
+ const autoId = nextEditorId('sv-ms')
69
+ const uid = $derived(id ?? autoId)
70
+
71
+ let search = $state('')
72
+ let triggerEl = $state<HTMLButtonElement | null>(null)
73
+ let panelEl = $state<HTMLDivElement | null>(null)
74
+ let searchEl = $state<HTMLInputElement | null>(null)
75
+ let listEl = $state<HTMLUListElement | null>(null)
76
+
77
+ // Remote/async options: `asyncOptions` holds the last server result; a stale
78
+ // response is dropped by comparing the request sequence.
79
+ let asyncOptions = $state<MultiSelectOption[]>([])
80
+ let loading = $state(false)
81
+ let reqSeq = 0
82
+ function doLoad(q: string) {
83
+ if (!loadOptions) return
84
+ const seq = ++reqSeq
85
+ loading = true
86
+ loadOptions(q)
87
+ .then((opts) => { if (seq === reqSeq) { asyncOptions = opts; loading = false } })
88
+ .catch(() => { if (seq === reqSeq) loading = false })
89
+ }
90
+ const runLoad = debounce((q: string) => doLoad(q), debounceMs)
91
+
92
+ const selected = $derived(new Set(value))
93
+ const filtered = $derived(
94
+ loadOptions
95
+ ? asyncOptions
96
+ : search.trim()
97
+ ? options.filter((o) => o.label.toLowerCase().includes(search.trim().toLowerCase()))
98
+ : options,
99
+ )
100
+ // Pool for resolving selected-chip labels (a remote-selected value may not be
101
+ // in the current result page, so merge the seed options with the last result).
102
+ const pool = $derived(
103
+ loadOptions
104
+ ? [...new Map([...options, ...asyncOptions].map((o) => [o.value, o])).values()]
105
+ : options,
106
+ )
107
+ const selectedOptions = $derived(pool.filter((o) => selected.has(o.value)))
108
+
109
+ function emit(next: Array<string | number>) {
110
+ value = next // enables bind:value
111
+ onChange?.(next)
112
+ }
113
+ function toggleOption(opt: MultiSelectOption) {
114
+ if (opt.disabled) return
115
+ const next = new Set(selected)
116
+ next.has(opt.value) ? next.delete(opt.value) : next.add(opt.value)
117
+ emit([...next])
118
+ }
119
+
120
+ const ms = createPopoverSelect({
121
+ itemCount: () => filtered.length,
122
+ disabled: (i) => !!filtered[i]?.disabled,
123
+ onSelect: (i) => { const o = filtered[i]; if (o) toggleOption(o) },
124
+ onOpenChange: (o) => { if (o) { search = ''; if (loadOptions) doLoad('') } },
125
+ getTrigger: () => triggerEl,
126
+ getPanel: () => panelEl,
127
+ getInitialFocus: () => (searchable ? searchEl : listEl),
128
+ closeOnSelect: () => false,
129
+ minWidth: () => 200,
130
+ idPrefix: 'sv-ms',
131
+ })
132
+ </script>
133
+
134
+ <SvField id={uid} {label} {hint} {error} {required} {dir}>
135
+ <div class="sv-ms sv-ms--{size}" class:is-disabled={disabled}>
136
+ <button
137
+ bind:this={triggerEl}
138
+ type="button"
139
+ class="sv-ms__trigger"
140
+ class:is-invalid={invalid}
141
+ {disabled}
142
+ onclick={() => ms.toggle()}
143
+ {...ms.triggerProps()}
144
+ {...editorAria({ id: uid, invalid, required, error, hint, ariaLabel })}
145
+ >
146
+ {#if selectedOptions.length === 0}
147
+ <span class="sv-ms__placeholder">{placeholder}</span>
148
+ {:else}
149
+ <span class="sv-ms__chips">
150
+ {#each selectedOptions.slice(0, maxTagCount) as opt (opt.value)}
151
+ <span class="sv-ms__chip">{opt.label}</span>
152
+ {/each}
153
+ {#if selectedOptions.length > maxTagCount}
154
+ <span class="sv-ms__chip sv-ms__chip--more">+{selectedOptions.length - maxTagCount}</span>
155
+ {/if}
156
+ </span>
157
+ {/if}
158
+ <span class="sv-ms__arrow" aria-hidden="true">▾</span>
159
+ </button>
160
+ {#if clearable && selectedOptions.length && !disabled}
161
+ <button type="button" class="sv-ms__clear" tabindex="-1" aria-label="Clear all" onclick={() => emit([])}>&times;</button>
162
+ {/if}
163
+ {#if name}
164
+ {#each value as v (v)}<input type="hidden" {name} value={v} />{/each}
165
+ {/if}
166
+ </div>
167
+ </SvField>
168
+
169
+ {#if ms.open}
170
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
171
+ <div
172
+ bind:this={panelEl}
173
+ class="sv-ms__panel"
174
+ dir={dir}
175
+ use:portalToBody
176
+ use:popIn={{ up: ms.rect.openUpward }}
177
+ style:position="fixed"
178
+ style:top={`${ms.rect.top}px`}
179
+ style:left={`${ms.rect.left}px`}
180
+ style:min-width={`${ms.rect.width}px`}
181
+ onkeydown={ms.onPanelKeydown}
182
+ >
183
+ {#if searchable}
184
+ <!-- svelte-ignore a11y_autofocus -->
185
+ <input
186
+ bind:this={searchEl}
187
+ class="sv-ms__search"
188
+ role="combobox"
189
+ aria-expanded="true"
190
+ aria-controls={ms.panelId}
191
+ aria-activedescendant={ms.activeDescendant()}
192
+ aria-label="Search options"
193
+ placeholder="Search…"
194
+ value={search}
195
+ oninput={(e) => { search = e.currentTarget.value; ms.active = 0; if (loadOptions) runLoad(search) }}
196
+ />
197
+ {/if}
198
+ <ul
199
+ bind:this={listEl}
200
+ id={ms.panelId}
201
+ class="sv-ms__list"
202
+ role="listbox"
203
+ aria-multiselectable="true"
204
+ aria-label={label ?? ariaLabel ?? 'Options'}
205
+ aria-activedescendant={searchable ? undefined : ms.activeDescendant()}
206
+ tabindex={searchable ? -1 : 0}
207
+ >
208
+ {#each filtered as opt, i (opt.value)}
209
+ <li
210
+ class="sv-ms__opt"
211
+ class:is-active={ms.isActive(i)}
212
+ class:is-disabled={opt.disabled}
213
+ role="option"
214
+ aria-selected={selected.has(opt.value)}
215
+ aria-disabled={opt.disabled}
216
+ onclick={() => toggleOption(opt)}
217
+ {...ms.itemProps(i)}
218
+ >
219
+ <span class="sv-ms__check" aria-hidden="true">{selected.has(opt.value) ? '✓' : ''}</span>
220
+ <span class="sv-ms__label">{opt.label}</span>
221
+ </li>
222
+ {:else}
223
+ <li class="sv-ms__empty" aria-disabled="true">{loading ? loadingText : 'No matches'}</li>
224
+ {/each}
225
+ {#if loading && filtered.length}
226
+ <li class="sv-ms__loading" aria-live="polite">{loadingText}</li>
227
+ {/if}
228
+ </ul>
229
+ <div class="sv-ms__footer">
230
+ <button type="button" class="sv-ms__done" onclick={() => { onCommit?.([...value]); ms.closePanel() }}>Done</button>
231
+ </div>
232
+ </div>
233
+ {/if}
234
+
235
+ <style>
236
+ .sv-ms { --_accent: var(--sg-accent, #2563eb); display: inline-flex; align-items: stretch; position: relative; width: 240px; }
237
+ .sv-ms.is-disabled { opacity: 0.6; }
238
+ .sv-ms__trigger {
239
+ flex: 1; min-width: 0; display: flex; align-items: center; gap: 6px; text-align: start;
240
+ background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
241
+ border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
242
+ font: inherit; cursor: pointer; padding: 0 8px;
243
+ }
244
+ .sv-ms--sm .sv-ms__trigger { min-height: 28px; font-size: 12px; }
245
+ .sv-ms--md .sv-ms__trigger { min-height: 34px; font-size: 13px; }
246
+ .sv-ms--lg .sv-ms__trigger { min-height: 40px; font-size: 15px; }
247
+ .sv-ms__trigger:focus-visible { outline: none; border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
248
+ .sv-ms__trigger.is-invalid { border-color: var(--sg-danger, #dc2626); }
249
+ .sv-ms__placeholder { flex: 1; color: var(--sg-muted, #94a3b8); }
250
+ .sv-ms__chips { flex: 1; display: flex; flex-wrap: wrap; gap: 4px; padding: 4px 0; }
251
+ .sv-ms__chip { display: inline-flex; align-items: center; padding: 1px 7px; border-radius: 999px; font-size: 11.5px; background: color-mix(in srgb, var(--_accent) 14%, transparent); color: var(--_accent); }
252
+ .sv-ms__chip--more { background: var(--sg-row-hover-bg, #f1f5f9); color: var(--sg-muted, #64748b); }
253
+ .sv-ms__arrow { margin-inline-start: auto; color: var(--sg-muted, #64748b); font-size: 10px; }
254
+ .sv-ms__clear { position: absolute; inset-inline-end: 26px; align-self: center; background: none; border: 0; color: var(--sg-muted, #64748b); cursor: pointer; font-size: 15px; }
255
+ .sv-ms__clear:hover { color: var(--sg-fg, #0f172a); }
256
+
257
+ :global(.sv-ms__panel) {
258
+ z-index: 2147483646; display: flex; flex-direction: column; max-height: 300px; max-width: 92vw;
259
+ background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
260
+ border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px; overflow: hidden;
261
+ box-shadow: 0 16px 48px -12px rgba(15, 23, 42, 0.35); font-size: 13px;
262
+ }
263
+ :global(.sv-ms__search) {
264
+ margin: 8px; padding: 6px 9px; font: inherit; font-size: 13px;
265
+ border: 1px solid var(--sg-input-border, #cbd5e1); border-radius: 7px;
266
+ background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a); outline: none;
267
+ }
268
+ :global(.sv-ms__list) { list-style: none; margin: 0; padding: 4px; overflow: auto; flex: 1; outline: none; }
269
+ :global(.sv-ms__opt) { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 6px; cursor: pointer; }
270
+ :global(.sv-ms__opt.is-active) { background: var(--sg-row-hover-bg, #f1f5f9); }
271
+ :global(.sv-ms__opt.is-disabled) { opacity: 0.5; cursor: not-allowed; }
272
+ :global(.sv-ms__check) { width: 14px; color: var(--sg-accent, #2563eb); font-weight: 700; }
273
+ :global(.sv-ms__empty) { padding: 10px; color: var(--sg-muted, #94a3b8); text-align: center; }
274
+ :global(.sv-ms__loading) { padding: 8px 10px; color: var(--sg-muted, #94a3b8); font-size: 12px; text-align: center; }
275
+ :global(.sv-ms__footer) { display: flex; justify-content: flex-end; padding: 6px 8px; border-top: 1px solid var(--sg-border, #e2e8f0); }
276
+ :global(.sv-ms__done) { font: inherit; font-size: 12.5px; padding: 4px 12px; border: 0; border-radius: 6px; background: var(--sg-accent, #2563eb); color: var(--sg-on-accent, #fff); cursor: pointer; }
277
+ </style>
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Component tests for SvMultiSelect: trigger/placeholder, portalled panel open,
3
+ * option toggling, chip display + "+N" collapse, search filter, and clear-all.
4
+ */
5
+ import { afterEach, describe, expect, it, vi } from 'vitest'
6
+ import { mount, unmount, flushSync } from 'svelte'
7
+ import SvMultiSelect from './SvMultiSelect.svelte'
8
+
9
+ const options = [
10
+ { value: 'a', label: 'Apple' },
11
+ { value: 'b', label: 'Banana' },
12
+ { value: 'c', label: 'Cherry' },
13
+ { value: 'd', label: 'Date' },
14
+ ]
15
+
16
+ function mountMs(props: Record<string, unknown>) {
17
+ const target = document.createElement('div')
18
+ document.body.appendChild(target)
19
+ const app = mount(SvMultiSelect, { target, props: { options, ...props } as any })
20
+ flushSync()
21
+ return { target, destroy: () => { unmount(app); target.remove() } }
22
+ }
23
+
24
+ const panel = () => document.body.querySelector('.sv-ms__panel')
25
+
26
+ afterEach(() => { document.body.innerHTML = '' })
27
+
28
+ describe('SvMultiSelect', () => {
29
+ it('shows the placeholder when nothing is selected', () => {
30
+ const { target, destroy } = mountMs({ value: [], placeholder: 'Pick fruit' })
31
+ try {
32
+ expect(target.querySelector('.sv-ms__placeholder')!.textContent).toBe('Pick fruit')
33
+ } finally { destroy() }
34
+ })
35
+
36
+ it('opens a portalled panel on trigger click', () => {
37
+ const { target, destroy } = mountMs({ value: [] })
38
+ try {
39
+ expect(panel()).toBeNull()
40
+ target.querySelector<HTMLButtonElement>('.sv-ms__trigger')!.click()
41
+ flushSync()
42
+ expect(panel()).not.toBeNull()
43
+ expect(document.body.querySelectorAll('.sv-ms__opt')).toHaveLength(4)
44
+ } finally { destroy() }
45
+ })
46
+
47
+ it('toggling an option emits the updated value array', () => {
48
+ const onChange = vi.fn()
49
+ const { target, destroy } = mountMs({ value: ['a'], onChange })
50
+ try {
51
+ target.querySelector<HTMLButtonElement>('.sv-ms__trigger')!.click()
52
+ flushSync()
53
+ const opts = document.body.querySelectorAll<HTMLElement>('.sv-ms__opt')
54
+ opts[1]!.click() // add 'b'
55
+ expect(onChange).toHaveBeenCalledWith(['a', 'b'])
56
+ } finally { destroy() }
57
+ })
58
+
59
+ it('deselects an already-selected option', () => {
60
+ const onChange = vi.fn()
61
+ const { target, destroy } = mountMs({ value: ['a', 'b'], onChange })
62
+ try {
63
+ target.querySelector<HTMLButtonElement>('.sv-ms__trigger')!.click()
64
+ flushSync()
65
+ const opts = document.body.querySelectorAll<HTMLElement>('.sv-ms__opt')
66
+ opts[0]!.click() // remove 'a'
67
+ expect(onChange).toHaveBeenCalledWith(['b'])
68
+ } finally { destroy() }
69
+ })
70
+
71
+ it('renders chips and collapses beyond maxTagCount', () => {
72
+ const { target, destroy } = mountMs({ value: ['a', 'b', 'c', 'd'], maxTagCount: 2 })
73
+ try {
74
+ const chips = target.querySelectorAll('.sv-ms__chip')
75
+ // 2 chips + 1 "+2" pill
76
+ expect(chips).toHaveLength(3)
77
+ expect(target.querySelector('.sv-ms__chip--more')!.textContent).toBe('+2')
78
+ } finally { destroy() }
79
+ })
80
+
81
+ it('filters options by the search box', () => {
82
+ const { target, destroy } = mountMs({ value: [], searchable: true })
83
+ try {
84
+ target.querySelector<HTMLButtonElement>('.sv-ms__trigger')!.click()
85
+ flushSync()
86
+ const search = document.body.querySelector<HTMLInputElement>('.sv-ms__search')!
87
+ search.value = 'an'
88
+ search.dispatchEvent(new Event('input', { bubbles: true }))
89
+ flushSync()
90
+ const labels = [...document.body.querySelectorAll('.sv-ms__label')].map((el) => el.textContent)
91
+ expect(labels).toEqual(['Banana']) // only "Banana" contains "an"
92
+ } finally { destroy() }
93
+ })
94
+
95
+ it('clear-all empties the selection', () => {
96
+ const onChange = vi.fn()
97
+ const { target, destroy } = mountMs({ value: ['a', 'b'], onChange })
98
+ try {
99
+ target.querySelector<HTMLButtonElement>('.sv-ms__clear')!.click()
100
+ expect(onChange).toHaveBeenCalledWith([])
101
+ } finally { destroy() }
102
+ })
103
+
104
+ // ---- WAI-ARIA combobox wiring (Track B) --------------------------------
105
+ it('wires the trigger + search as an APG combobox with aria-activedescendant', () => {
106
+ const { target, destroy } = mountMs({ value: [] })
107
+ try {
108
+ const trigger = target.querySelector<HTMLButtonElement>('.sv-ms__trigger')!
109
+ expect(trigger.getAttribute('aria-haspopup')).toBe('listbox')
110
+ expect(trigger.getAttribute('aria-expanded')).toBe('false')
111
+
112
+ trigger.click()
113
+ flushSync()
114
+ expect(trigger.getAttribute('aria-expanded')).toBe('true')
115
+
116
+ const list = document.body.querySelector('.sv-ms__list')!
117
+ const search = document.body.querySelector<HTMLInputElement>('.sv-ms__search')!
118
+ expect(list.getAttribute('role')).toBe('listbox')
119
+ expect(search.getAttribute('role')).toBe('combobox')
120
+ expect(search.getAttribute('aria-controls')).toBe(list.id)
121
+
122
+ // activedescendant points at the first option, and follows ArrowDown.
123
+ const opts = document.body.querySelectorAll<HTMLElement>('.sv-ms__opt')
124
+ expect(search.getAttribute('aria-activedescendant')).toBe(opts[0]!.id)
125
+ search.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }))
126
+ flushSync()
127
+ expect(search.getAttribute('aria-activedescendant')).toBe(opts[1]!.id)
128
+ } finally { destroy() }
129
+ })
130
+
131
+ it('Enter on the active option toggles it (keyboard select)', () => {
132
+ const onChange = vi.fn()
133
+ const { target, destroy } = mountMs({ value: [], onChange })
134
+ try {
135
+ target.querySelector<HTMLButtonElement>('.sv-ms__trigger')!.click()
136
+ flushSync()
137
+ const search = document.body.querySelector<HTMLInputElement>('.sv-ms__search')!
138
+ search.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
139
+ expect(onChange).toHaveBeenCalledWith(['a']) // first option
140
+ } finally { destroy() }
141
+ })
142
+
143
+ it('carries dir onto the portalled panel (RTL escapes the field scope otherwise)', () => {
144
+ const { target, destroy } = mountMs({ value: [], dir: 'rtl' })
145
+ try {
146
+ target.querySelector<HTMLButtonElement>('.sv-ms__trigger')!.click()
147
+ flushSync()
148
+ expect(document.body.querySelector('.sv-ms__panel')!.getAttribute('dir')).toBe('rtl')
149
+ } finally { destroy() }
150
+ })
151
+
152
+ // ---- Async / remote options (Track C) ----------------------------------
153
+ it('loads remote options via loadOptions when opened', async () => {
154
+ const loadOptions = vi.fn(async () => [{ value: 'z', label: 'Remote Zed' }])
155
+ const { target, destroy } = mountMs({ value: [], loadOptions })
156
+ try {
157
+ target.querySelector<HTMLButtonElement>('.sv-ms__trigger')!.click()
158
+ flushSync()
159
+ expect(loadOptions).toHaveBeenCalledWith('') // initial load on open
160
+ await vi.waitFor(() => {
161
+ flushSync()
162
+ const opts = [...document.body.querySelectorAll('.sv-ms__opt')].map((o) => o.textContent)
163
+ expect(opts.join()).toContain('Remote Zed')
164
+ })
165
+ } finally { destroy() }
166
+ })
167
+ })