@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,80 @@
1
+ /**
2
+ * Component tests for SvDateRangeInput: the field renders the formatted range,
3
+ * the two-month range calendar portals to <body>, picking a range emits
4
+ * [start, end], and the shared editor contract (label / error) is wired.
5
+ */
6
+ import { describe, expect, it, afterEach } from 'vitest'
7
+ import { mount, unmount, flushSync } from 'svelte'
8
+ import SvDateRangeInput from './SvDateRangeInput.svelte'
9
+
10
+ function mountDri(props: Record<string, unknown>) {
11
+ const target = document.createElement('div')
12
+ document.body.appendChild(target)
13
+ const app = mount(SvDateRangeInput, { target, props: props as any })
14
+ flushSync()
15
+ return { target, destroy: () => { unmount(app); target.remove() } }
16
+ }
17
+
18
+ // The calendar panel portals to <body>, so query the document.
19
+ const panel = () => document.querySelector<HTMLElement>('.sv-dri__panel')
20
+
21
+ afterEach(() => {
22
+ document.querySelectorAll('.sv-dri__panel').forEach((n) => n.remove())
23
+ })
24
+
25
+ describe('SvDateRangeInput', () => {
26
+ it('formats the initial [start, end] range with the "to" separator', () => {
27
+ const { target, destroy } = mountDri({
28
+ value: [new Date(2026, 5, 1), new Date(2026, 5, 7)],
29
+ formatString: 'yyyy-MM-dd',
30
+ })
31
+ try {
32
+ const input = target.querySelector<HTMLInputElement>('.sv-dri__input')!
33
+ expect(input.value).toBe('2026-06-01 to 2026-06-07')
34
+ } finally { destroy() }
35
+ })
36
+
37
+ it('shows the placeholder when empty and opens the calendar on click', () => {
38
+ const { target, destroy } = mountDri({ value: null, placeholder: 'Pick a range' })
39
+ try {
40
+ const input = target.querySelector<HTMLInputElement>('.sv-dri__input')!
41
+ expect(input.value).toBe('')
42
+ expect(input.placeholder).toBe('Pick a range')
43
+ expect(panel()).toBeNull()
44
+ input.click()
45
+ flushSync()
46
+ expect(panel()).not.toBeNull()
47
+ // The portalled panel embeds a range-mode SvCalendar.
48
+ expect(panel()!.querySelector('.sv-cal')).not.toBeNull()
49
+ } finally { destroy() }
50
+ })
51
+
52
+ it('wires the shared editor contract (label + aria-invalid + error text)', () => {
53
+ const { target, destroy } = mountDri({
54
+ value: null, label: 'Period', required: true, invalid: true, error: 'Required',
55
+ })
56
+ try {
57
+ const input = target.querySelector<HTMLInputElement>('.sv-dri__input')!
58
+ const labelEl = target.querySelector<HTMLLabelElement>('.sv-field__label')!
59
+ expect(labelEl.textContent).toContain('Period')
60
+ expect(labelEl.getAttribute('for')).toBe(input.id)
61
+ expect(input.getAttribute('aria-invalid')).toBe('true')
62
+ expect(input.getAttribute('aria-required')).toBe('true')
63
+ const err = target.querySelector<HTMLElement>('.sv-field__error')!
64
+ expect(err.textContent).toBe('Required')
65
+ expect(err.id).toBe(input.getAttribute('aria-describedby'))
66
+ } finally { destroy() }
67
+ })
68
+
69
+ it('localizes the "to" separator via messages', () => {
70
+ const { target, destroy } = mountDri({
71
+ value: [new Date(2026, 5, 1), new Date(2026, 5, 7)],
72
+ formatString: 'dd/MM/yyyy',
73
+ messages: { to: 'au' },
74
+ })
75
+ try {
76
+ const input = target.querySelector<HTMLInputElement>('.sv-dri__input')!
77
+ expect(input.value).toBe('01/06/2026 au 07/06/2026')
78
+ } finally { destroy() }
79
+ })
80
+ })
@@ -15,11 +15,20 @@
15
15
  */
16
16
  import SvCalendar from './SvCalendar.svelte'
17
17
  import SvTimePicker from './SvTimePicker.svelte'
18
- import { anchoredRect, portalToBody, type AnchoredRect } from './popover'
18
+ import SvField from './SvField.svelte'
19
+ import { anchoredRect, portalToBody, popIn, type AnchoredRect } from './popover'
20
+ import { createDismissableLayer } from './a11y/dismissable'
21
+ import { nextEditorId, resolveMessages, type SvEditorProps } from './editor-contract'
19
22
  import { createDateTimePicker, type DateTimeValue, type DropDownDisplayMode } from './createDateTimePicker.svelte'
20
23
  import type { DateLike } from './datetime/date-core'
21
24
 
22
- type Props = {
25
+ /** User-facing strings (localizable via `messages`). */
26
+ type DateTimeMessages = { dialog: string; date: string; time: string; clear: string; open: string }
27
+ const DEFAULT_MESSAGES: DateTimeMessages = {
28
+ dialog: 'Choose date and time', date: 'DATE', time: 'TIME', clear: 'Clear', open: 'Open picker',
29
+ }
30
+
31
+ type Props = SvEditorProps & {
23
32
  value?: DateTimeValue
24
33
  onChange?: (value: Date | null) => void
25
34
  /** Fired when the value is finalized (Enter, blur, or a single-date pick).
@@ -33,9 +42,6 @@
33
42
  max?: DateLike | null
34
43
  nullable?: boolean
35
44
  placeholder?: string
36
- disabled?: boolean
37
- readonly?: boolean
38
- name?: string
39
45
  locale?: string
40
46
  firstDayOfWeek?: number
41
47
  weekNumbers?: boolean
@@ -50,6 +56,8 @@
50
56
  autoOpen?: boolean
51
57
  /** Animate the calendar's month/drill navigation (honors reduced-motion). */
52
58
  animate?: boolean | 'slide' | 'fade'
59
+ /** Override the built-in strings (tabs, dialog, clear/open aria-labels). */
60
+ messages?: Partial<DateTimeMessages>
53
61
  }
54
62
 
55
63
  let {
@@ -75,8 +83,22 @@
75
83
  stepMinutes = 1,
76
84
  autoOpen = false,
77
85
  animate = false,
86
+ ariaLabel,
87
+ invalid = false,
88
+ required = false,
89
+ error,
90
+ label,
91
+ hint,
92
+ dir,
93
+ id,
94
+ messages,
78
95
  }: Props = $props()
79
96
 
97
+ const autoId = nextEditorId('sv-dtp')
98
+ const uid = $derived(id ?? autoId)
99
+ const M = $derived(resolveMessages(DEFAULT_MESSAGES, messages))
100
+ const resolvedDir = $derived(dir === 'ltr' || dir === 'rtl' ? dir : undefined)
101
+
80
102
  // The headless core owns value math, parse/format, clamping, the dropdown
81
103
  // open/tab state and the sub-picker wiring. Reactive inputs are getters;
82
104
  // callbacks are closures.
@@ -95,6 +117,14 @@
95
117
  stepMinutes: () => stepMinutes,
96
118
  disabled: () => disabled,
97
119
  readonly: () => readonly,
120
+ toggleLabel: () => M.open,
121
+ clearLabel: () => M.clear,
122
+ id: () => uid,
123
+ invalid: () => invalid,
124
+ required: () => required,
125
+ error: () => error,
126
+ hint: () => hint,
127
+ ariaLabel: () => ariaLabel,
98
128
  })
99
129
 
100
130
  // --- Portalled dropdown positioning (DOM-bound; stays in the component) -----
@@ -114,17 +144,16 @@
114
144
  const reposition = () => updatePos()
115
145
  window.addEventListener('scroll', reposition, true)
116
146
  window.addEventListener('resize', reposition)
117
- const onDown = (e: PointerEvent) => {
118
- const t = e.target as Node | null
119
- if (!t) return
120
- if (triggerEl?.contains(t) || panelEl?.contains(t)) return
121
- dtp.closePanel()
122
- }
123
- document.addEventListener('pointerdown', onDown, true)
147
+ const layer = createDismissableLayer({
148
+ element: () => [triggerEl, panelEl],
149
+ onDismiss: () => dtp.closePanel(),
150
+ closeOnEscape: false,
151
+ })
152
+ layer.activate()
124
153
  return () => {
125
154
  window.removeEventListener('scroll', reposition, true)
126
155
  window.removeEventListener('resize', reposition)
127
- document.removeEventListener('pointerdown', onDown, true)
156
+ layer.release()
128
157
  }
129
158
  })
130
159
 
@@ -142,8 +171,9 @@
142
171
  }
143
172
  </script>
144
173
 
145
- <div class="sv-dtp" class:sv-dtp--disabled={disabled} onfocusout={onRootFocusOut}>
146
- <div class="sv-dtp__field" bind:this={triggerEl}>
174
+ <SvField id={uid} {label} {hint} {error} {required} {dir}>
175
+ <div class="sv-dtp" class:sv-dtp--disabled={disabled} dir={resolvedDir} onfocusout={onRootFocusOut}>
176
+ <div class="sv-dtp__field" class:is-invalid={invalid} bind:this={triggerEl}>
147
177
  {#if spinButtons}
148
178
  <div class="sv-dtp__spin">
149
179
  <button {...dtp.spinProps(1)}>▲</button>
@@ -170,16 +200,18 @@
170
200
  class="sv-dtp__panel"
171
201
  bind:this={panelEl}
172
202
  use:portalToBody
203
+ use:popIn={{ up: panelRect.openUpward }}
173
204
  style:position="fixed"
174
205
  style:top={`${panelRect.top}px`}
175
206
  style:left={`${panelRect.left}px`}
176
207
  role="dialog"
177
- aria-label="Choose date and time"
208
+ aria-label={M.dialog}
209
+ dir={resolvedDir}
178
210
  >
179
211
  {#if dtp.showDateTab && dtp.showTimeTab}
180
212
  <div class="sv-dtp__tabs" role="tablist">
181
- <button class:is-active={dtp.tab === 'date'} {...dtp.tabProps('date')}>DATE</button>
182
- <button class:is-active={dtp.tab === 'time'} {...dtp.tabProps('time')}>TIME</button>
213
+ <button class:is-active={dtp.tab === 'date'} {...dtp.tabProps('date')}>{M.date}</button>
214
+ <button class:is-active={dtp.tab === 'time'} {...dtp.tabProps('time')}>{M.time}</button>
183
215
  </div>
184
216
  {/if}
185
217
  <div class="sv-dtp__body">
@@ -193,6 +225,7 @@
193
225
  {weekNumbers}
194
226
  {locale}
195
227
  {animate}
228
+ dir={resolvedDir}
196
229
  onChange={dtp.onCalendarChange}
197
230
  />
198
231
  {:else if dtp.showTimeTab}
@@ -201,6 +234,7 @@
201
234
  format={hourFormat}
202
235
  {minuteInterval}
203
236
  footer
237
+ dir={resolvedDir}
204
238
  onChange={dtp.onTimeChange}
205
239
  />
206
240
  {/if}
@@ -210,6 +244,7 @@
210
244
 
211
245
  {#if name}<input type="hidden" {name} value={dtp.current ? dtp.current.toISOString() : ''} />{/if}
212
246
  </div>
247
+ </SvField>
213
248
 
214
249
  <style>
215
250
  .sv-dtp {
@@ -225,14 +260,16 @@
225
260
  .sv-dtp__field {
226
261
  display: flex; align-items: center; gap: 2px;
227
262
  background: var(--_bg); border: 1px solid var(--_border); border-radius: var(--_radius);
228
- padding: 0 4px 0 0; height: 34px;
263
+ padding-block: 0; padding-inline: 0 4px; height: 34px;
229
264
  }
230
265
  .sv-dtp__field:focus-within { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
266
+ .sv-dtp__field.is-invalid { border-color: var(--sg-danger, #dc2626); }
267
+ .sv-dtp__field.is-invalid:focus-within { box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-danger, #dc2626) 22%, transparent); }
231
268
  .sv-dtp__input {
232
269
  flex: 1; min-width: 0; border: 0; background: none; outline: none; color: inherit;
233
270
  font: inherit; font-size: 13px; padding: 0 8px; height: 100%;
234
271
  }
235
- .sv-dtp__spin { display: flex; flex-direction: column; border-right: 1px solid var(--_border); }
272
+ .sv-dtp__spin { display: flex; flex-direction: column; border-inline-end: 1px solid var(--_border); }
236
273
  .sv-dtp__spin button {
237
274
  flex: 1; width: 20px; border: 0; background: none; color: var(--_muted); cursor: pointer;
238
275
  font-size: 7px; line-height: 1; padding: 0;
@@ -0,0 +1,61 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvDivider - a separator line, horizontal or vertical, optionally with a
4
+ * centered / start / end label. Theme-driven. Parity: Smart divider.
5
+ *
6
+ * ```svelte
7
+ * <SvDivider />
8
+ * <SvDivider label="OR" />
9
+ * <SvDivider orientation="vertical" />
10
+ * ```
11
+ */
12
+ import type { Snippet } from 'svelte'
13
+
14
+ type Props = {
15
+ orientation?: 'horizontal' | 'vertical'
16
+ label?: string
17
+ /** Label position along a horizontal divider. */
18
+ align?: 'start' | 'center' | 'end'
19
+ dashed?: boolean
20
+ children?: Snippet
21
+ }
22
+
23
+ let { orientation = 'horizontal', label, align = 'center', dashed = false, children }: Props = $props()
24
+ const hasLabel = $derived(!!label || !!children)
25
+ </script>
26
+
27
+ {#if orientation === 'vertical'}
28
+ <span class="sv-divider sv-divider--v" class:is-dashed={dashed} role="separator" aria-orientation="vertical"></span>
29
+ {:else}
30
+ <div class="sv-divider sv-divider--h sv-divider--{align}" class:is-dashed={dashed} class:has-label={hasLabel} role="separator">
31
+ {#if hasLabel}<span class="sv-divider__label">{#if children}{@render children()}{:else}{label}{/if}</span>{/if}
32
+ </div>
33
+ {/if}
34
+
35
+ <style>
36
+ .sv-divider--h {
37
+ --_line: var(--sg-border, #e2e8f0);
38
+ height: 1px; background: var(--_line); border: 0; margin: 12px 0;
39
+ }
40
+ .sv-divider--h.is-dashed { height: 0; background: none; border-top: 1px dashed var(--_line); }
41
+ .sv-divider--h.has-label {
42
+ height: auto; background: none; display: flex; align-items: center; gap: 12px;
43
+ color: var(--sg-muted, #64748b); font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em;
44
+ }
45
+ .sv-divider--h.has-label::before, .sv-divider--h.has-label::after {
46
+ content: ''; height: 1px; background: var(--_line);
47
+ }
48
+ .sv-divider--h.has-label.is-dashed::before, .sv-divider--h.has-label.is-dashed::after { height: 0; border-top: 1px dashed var(--_line); }
49
+ .sv-divider--center::before, .sv-divider--center::after { flex: 1; }
50
+ .sv-divider--start::before { width: 16px; flex: none; }
51
+ .sv-divider--start::after { flex: 1; }
52
+ .sv-divider--end::before { flex: 1; }
53
+ .sv-divider--end::after { width: 16px; flex: none; }
54
+ .sv-divider__label { flex: none; }
55
+
56
+ .sv-divider--v {
57
+ display: inline-block; width: 1px; align-self: stretch; min-height: 1em;
58
+ background: var(--sg-border, #e2e8f0); margin: 0 12px; vertical-align: middle;
59
+ }
60
+ .sv-divider--v.is-dashed { width: 0; background: none; border-inline-start: 1px dashed var(--sg-border, #e2e8f0); }
61
+ </style>
@@ -0,0 +1,226 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvDrawer - an edge-anchored side sheet (right / left / top / bottom): a
4
+ * backdrop, a sliding panel, and the full dialog a11y contract. Built entirely
5
+ * on the shared primitives - `createFocusTrap`, `lockScroll` and
6
+ * `createDismissableLayer` - so its focus, scroll-lock and Escape/backdrop
7
+ * behaviour is identical to SvModal and nested overlays close top-first.
8
+ * Portalled to <body>. Controlled via `open`.
9
+ *
10
+ * ```svelte
11
+ * <SvDrawer bind:open side="right" title="Filters">
12
+ * <p>Body</p>
13
+ * {#snippet footer()}<button onclick={() => open = false}>Apply</button>{/snippet}
14
+ * </SvDrawer>
15
+ * ```
16
+ */
17
+ import type { Snippet } from 'svelte'
18
+ import { portalToBody } from './popover'
19
+ import { nextEditorId } from './editor-contract'
20
+ import { createFocusTrap } from './a11y/focus-trap'
21
+ import { lockScroll } from './a11y/scroll-lock'
22
+ import { createDismissableLayer } from './a11y/dismissable'
23
+
24
+ /** Edge the drawer slides from. Kept in sync with the `DrawerSide` export in
25
+ * index.ts (a bare type-only module script trips the Svelte/Vite parser). */
26
+ type DrawerSide = 'right' | 'left' | 'top' | 'bottom'
27
+
28
+ type Props = {
29
+ open?: boolean
30
+ onClose?: () => void
31
+ /** Edge the drawer slides from. Default 'right'. */
32
+ side?: DrawerSide
33
+ /** Panel size along its axis (width for left/right, height for top/bottom).
34
+ * Any CSS length; defaults to a sensible per-axis value. */
35
+ size?: string
36
+ title?: string
37
+ closeOnBackdrop?: boolean
38
+ closeOnEsc?: boolean
39
+ /** Hide the header close (x) button. */
40
+ hideClose?: boolean
41
+ /** Render as a mobile bottom-sheet: bottom-anchored, rounded top, a grab
42
+ * handle, and swipe-down-to-close. Overrides `side`. */
43
+ sheet?: boolean
44
+ /** Accessible name when there is no visible `title`. */
45
+ ariaLabel?: string
46
+ children?: Snippet
47
+ footer?: Snippet
48
+ }
49
+
50
+ let {
51
+ open = $bindable(false),
52
+ onClose,
53
+ side = 'right',
54
+ size,
55
+ title,
56
+ closeOnBackdrop = true,
57
+ closeOnEsc = true,
58
+ hideClose = false,
59
+ sheet = false,
60
+ ariaLabel,
61
+ children,
62
+ footer,
63
+ }: Props = $props()
64
+
65
+ const titleId = nextEditorId('sv-drawer-title')
66
+ let dialogEl = $state<HTMLDivElement | null>(null)
67
+
68
+ // A sheet is a bottom drawer with mobile affordances.
69
+ const effectiveSide = $derived<DrawerSide>(sheet ? 'bottom' : side)
70
+ const horizontal = $derived(effectiveSide === 'left' || effectiveSide === 'right')
71
+ const sizeVar = $derived(size ?? (horizontal ? 'min(400px, 92vw)' : 'min(360px, 85vh)'))
72
+
73
+ function close() { open = false; onClose?.() }
74
+
75
+ // Swipe-down-to-close for the sheet grab handle.
76
+ let dragY = $state(0)
77
+ let dragging = $state(false)
78
+ let dragStartY = 0
79
+ function onGrabDown(e: PointerEvent) {
80
+ dragging = true
81
+ dragStartY = e.clientY
82
+ ;(e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId)
83
+ }
84
+ function onGrabMove(e: PointerEvent) {
85
+ if (!dragging) return
86
+ dragY = Math.max(0, e.clientY - dragStartY)
87
+ }
88
+ function onGrabUp(e: PointerEvent) {
89
+ if (!dragging) return
90
+ dragging = false
91
+ ;(e.currentTarget as HTMLElement).releasePointerCapture?.(e.pointerId)
92
+ if (dragY > 80) close()
93
+ else dragY = 0
94
+ }
95
+
96
+ // Focus trap + scroll lock + Escape/backdrop dismissal, all from the shared
97
+ // primitives - one tested implementation across every dialog-like overlay.
98
+ $effect(() => {
99
+ if (!open || !dialogEl) return
100
+ dragY = 0
101
+ const trap = createFocusTrap(dialogEl)
102
+ trap.activate()
103
+ const unlockScroll = lockScroll()
104
+ const layer = createDismissableLayer({
105
+ element: () => dialogEl,
106
+ onDismiss: (reason) => {
107
+ if (reason === 'escape' ? closeOnEsc : closeOnBackdrop) close()
108
+ },
109
+ })
110
+ layer.activate()
111
+ return () => {
112
+ layer.release()
113
+ unlockScroll()
114
+ trap.release()
115
+ }
116
+ })
117
+ </script>
118
+
119
+ {#if open}
120
+ <div class="sv-drawer__backdrop" use:portalToBody>
121
+ <div
122
+ bind:this={dialogEl}
123
+ class="sv-drawer sv-drawer--{effectiveSide}"
124
+ class:is-sheet={sheet}
125
+ class:is-dragging={dragging}
126
+ role="dialog"
127
+ aria-modal="true"
128
+ aria-label={!title ? ariaLabel : undefined}
129
+ aria-labelledby={title ? titleId : undefined}
130
+ tabindex="-1"
131
+ style:--sv-drawer-size={sizeVar}
132
+ style:transform={sheet && dragY ? `translateY(${dragY}px)` : undefined}
133
+ >
134
+ {#if sheet}
135
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
136
+ <div
137
+ class="sv-drawer__grab"
138
+ aria-hidden="true"
139
+ onpointerdown={onGrabDown}
140
+ onpointermove={onGrabMove}
141
+ onpointerup={onGrabUp}
142
+ onpointercancel={onGrabUp}
143
+ ><span></span></div>
144
+ {/if}
145
+ {#if title || !hideClose}
146
+ <div class="sv-drawer__header">
147
+ <h2 class="sv-drawer__title" id={titleId}>{title ?? ''}</h2>
148
+ {#if !hideClose}
149
+ <button type="button" class="sv-drawer__x" aria-label="Close" onclick={close}>&times;</button>
150
+ {/if}
151
+ </div>
152
+ {/if}
153
+ <div class="sv-drawer__body">{@render children?.()}</div>
154
+ {#if footer}<div class="sv-drawer__footer">{@render footer()}</div>{/if}
155
+ </div>
156
+ </div>
157
+ {/if}
158
+
159
+ <style>
160
+ :global(.sv-drawer__backdrop) {
161
+ position: fixed; inset: 0; z-index: 2147483000;
162
+ background: rgba(15, 23, 42, 0.45); backdrop-filter: blur(1.5px);
163
+ animation: sv-drawer-fade 0.16s ease both;
164
+ }
165
+ @keyframes sv-drawer-fade { from { opacity: 0 } to { opacity: 1 } }
166
+
167
+ :global(.sv-drawer) {
168
+ position: fixed;
169
+ display: flex; flex-direction: column;
170
+ background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
171
+ box-shadow: 0 24px 64px -16px rgba(15, 23, 42, 0.5);
172
+ overflow: hidden;
173
+ }
174
+ /* Left / right: full height, fixed width, slide along X. */
175
+ :global(.sv-drawer--right), :global(.sv-drawer--left) {
176
+ top: 0; bottom: 0; width: var(--sv-drawer-size); max-width: 100vw;
177
+ }
178
+ :global(.sv-drawer--right) { inset-inline-end: 0; border-inline-start: 1px solid var(--sg-border, #e2e8f0); animation: sv-drawer-in-right 0.22s cubic-bezier(0.16, 1, 0.3, 1) both; }
179
+ :global(.sv-drawer--left) { inset-inline-start: 0; border-inline-end: 1px solid var(--sg-border, #e2e8f0); animation: sv-drawer-in-left 0.22s cubic-bezier(0.16, 1, 0.3, 1) both; }
180
+ /* Top / bottom: full width, fixed height, slide along Y. */
181
+ :global(.sv-drawer--top), :global(.sv-drawer--bottom) {
182
+ left: 0; right: 0; height: var(--sv-drawer-size); max-height: 100vh;
183
+ }
184
+ :global(.sv-drawer--top) { top: 0; border-bottom: 1px solid var(--sg-border, #e2e8f0); animation: sv-drawer-in-top 0.22s cubic-bezier(0.16, 1, 0.3, 1) both; }
185
+ :global(.sv-drawer--bottom) { bottom: 0; border-top: 1px solid var(--sg-border, #e2e8f0); animation: sv-drawer-in-bottom 0.22s cubic-bezier(0.16, 1, 0.3, 1) both; }
186
+
187
+ /* Mobile bottom-sheet: centered, fixed width, rounded top, grab handle. */
188
+ :global(.sv-drawer.is-sheet) {
189
+ left: 0; right: 0; margin: 0 auto; top: auto;
190
+ width: min(560px, 100vw); height: auto; max-height: 85vh;
191
+ border: 1px solid var(--sg-border, #e2e8f0); border-bottom: 0;
192
+ border-radius: 16px 16px 0 0;
193
+ transition: transform 0.22s cubic-bezier(0.16, 1, 0.3, 1);
194
+ }
195
+ :global(.sv-drawer.is-sheet.is-dragging) { transition: none; }
196
+ :global(.sv-drawer__grab) {
197
+ flex: none; display: grid; place-items: center; padding: 8px 0 2px; cursor: grab; touch-action: none;
198
+ }
199
+ :global(.sv-drawer__grab span) {
200
+ display: block; width: 40px; height: 4px; border-radius: 999px; background: var(--sg-border, #cbd5e1);
201
+ }
202
+
203
+ @keyframes sv-drawer-in-right { from { transform: translateX(100%) } to { transform: translateX(0) } }
204
+ @keyframes sv-drawer-in-left { from { transform: translateX(-100%) } to { transform: translateX(0) } }
205
+ @keyframes sv-drawer-in-top { from { transform: translateY(-100%) } to { transform: translateY(0) } }
206
+ @keyframes sv-drawer-in-bottom { from { transform: translateY(100%) } to { transform: translateY(0) } }
207
+ @media (prefers-reduced-motion: reduce) {
208
+ :global(.sv-drawer__backdrop), :global(.sv-drawer) { animation: none }
209
+ }
210
+
211
+ :global(.sv-drawer__header) {
212
+ display: flex; align-items: center; gap: 12px; padding: 13px 16px;
213
+ border-bottom: 1px solid var(--sg-border, #e2e8f0);
214
+ }
215
+ :global(.sv-drawer__title) { flex: 1; margin: 0; font-size: 15px; font-weight: 650; }
216
+ :global(.sv-drawer__x) {
217
+ flex: none; width: 28px; height: 28px; display: grid; place-items: center;
218
+ background: none; border: 0; color: var(--sg-muted, #64748b); cursor: pointer; border-radius: 6px; font-size: 20px; line-height: 1;
219
+ }
220
+ :global(.sv-drawer__x:hover) { background: var(--sg-row-hover-bg, #f1f5f9); color: var(--sg-fg, #0f172a); }
221
+ :global(.sv-drawer__body) { padding: 16px; overflow: auto; flex: 1; font-size: 13.5px; line-height: 1.6; }
222
+ :global(.sv-drawer__footer) {
223
+ display: flex; justify-content: flex-end; gap: 8px; padding: 12px 16px;
224
+ border-top: 1px solid var(--sg-border, #e2e8f0);
225
+ }
226
+ </style>
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Component tests for SvDrawer: dialog semantics, side variants, focus trap,
3
+ * scroll lock, and Escape / backdrop dismissal - all inherited from the shared
4
+ * a11y primitives.
5
+ */
6
+ import { describe, expect, it } from 'vitest'
7
+ import { mount, unmount, flushSync } from 'svelte'
8
+ import SvDrawer from './SvDrawer.svelte'
9
+
10
+ function mountDrawer(props: Record<string, unknown>) {
11
+ const target = document.createElement('div')
12
+ document.body.appendChild(target)
13
+ const app = mount(SvDrawer, { target, props: props as any })
14
+ flushSync()
15
+ return { target, destroy: () => { unmount(app); target.remove() } }
16
+ }
17
+
18
+ const dialog = () => document.body.querySelector<HTMLElement>('.sv-drawer')
19
+
20
+ describe('SvDrawer', () => {
21
+ it('renders nothing when closed', () => {
22
+ const { destroy } = mountDrawer({ open: false })
23
+ try {
24
+ expect(dialog()).toBeNull()
25
+ } finally { destroy() }
26
+ })
27
+
28
+ it('renders a modal dialog with the title wired to aria-labelledby', () => {
29
+ const { destroy } = mountDrawer({ open: true, title: 'Filters' })
30
+ try {
31
+ const el = dialog()!
32
+ expect(el.getAttribute('role')).toBe('dialog')
33
+ expect(el.getAttribute('aria-modal')).toBe('true')
34
+ const labelledby = el.getAttribute('aria-labelledby')
35
+ expect(labelledby).toBeTruthy()
36
+ expect(document.getElementById(labelledby!)?.textContent).toBe('Filters')
37
+ } finally { destroy() }
38
+ })
39
+
40
+ it('applies the side modifier class', () => {
41
+ const { destroy } = mountDrawer({ open: true, side: 'left' })
42
+ try {
43
+ expect(dialog()!.classList.contains('sv-drawer--left')).toBe(true)
44
+ } finally { destroy() }
45
+ })
46
+
47
+ it('locks body scroll while open and restores it on close', () => {
48
+ document.body.style.overflow = 'auto'
49
+ const { destroy } = mountDrawer({ open: true })
50
+ try {
51
+ expect(document.body.style.overflow).toBe('hidden')
52
+ } finally {
53
+ destroy()
54
+ flushSync()
55
+ expect(document.body.style.overflow).toBe('auto')
56
+ }
57
+ })
58
+
59
+ it('closes on Escape and fires onClose', () => {
60
+ let closed = 0
61
+ const { destroy } = mountDrawer({ open: true, onClose: () => closed++ })
62
+ try {
63
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
64
+ flushSync()
65
+ expect(closed).toBe(1)
66
+ expect(dialog()).toBeNull()
67
+ } finally { destroy() }
68
+ })
69
+
70
+ it('does not close on Escape when closeOnEsc is false', () => {
71
+ const { destroy } = mountDrawer({ open: true, closeOnEsc: false })
72
+ try {
73
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
74
+ flushSync()
75
+ expect(dialog()).not.toBeNull()
76
+ } finally { destroy() }
77
+ })
78
+
79
+ it('dismisses on outside (backdrop) pointerdown', () => {
80
+ const { destroy } = mountDrawer({ open: true })
81
+ try {
82
+ const backdrop = document.body.querySelector<HTMLElement>('.sv-drawer__backdrop')!
83
+ backdrop.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true }))
84
+ flushSync()
85
+ expect(dialog()).toBeNull()
86
+ } finally { destroy() }
87
+ })
88
+
89
+ // ---- Mobile bottom-sheet (Track D) -------------------------------------
90
+ it('sheet mode renders bottom-anchored with a grab handle', () => {
91
+ const { destroy } = mountDrawer({ open: true, sheet: true, side: 'right' })
92
+ try {
93
+ const el = dialog()!
94
+ expect(el.classList.contains('is-sheet')).toBe(true)
95
+ expect(el.classList.contains('sv-drawer--bottom')).toBe(true) // sheet overrides side
96
+ expect(document.body.querySelector('.sv-drawer__grab')).not.toBeNull()
97
+ } finally { destroy() }
98
+ })
99
+
100
+ it('swiping the grab handle down past the threshold closes the sheet', () => {
101
+ let closed = 0
102
+ const { destroy } = mountDrawer({ open: true, sheet: true, onClose: () => closed++ })
103
+ try {
104
+ const grab = document.body.querySelector<HTMLElement>('.sv-drawer__grab')!
105
+ grab.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true, clientY: 100 }))
106
+ grab.dispatchEvent(new MouseEvent('pointermove', { bubbles: true, clientY: 220 })) // dy = 120 > 80
107
+ grab.dispatchEvent(new MouseEvent('pointerup', { bubbles: true, clientY: 220 }))
108
+ flushSync()
109
+ expect(closed).toBe(1)
110
+ expect(dialog()).toBeNull()
111
+ } finally { destroy() }
112
+ })
113
+
114
+ it('a small swipe snaps back without closing', () => {
115
+ const { destroy } = mountDrawer({ open: true, sheet: true })
116
+ try {
117
+ const grab = document.body.querySelector<HTMLElement>('.sv-drawer__grab')!
118
+ grab.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true, clientY: 100 }))
119
+ grab.dispatchEvent(new MouseEvent('pointermove', { bubbles: true, clientY: 140 })) // dy = 40 < 80
120
+ grab.dispatchEvent(new MouseEvent('pointerup', { bubbles: true, clientY: 140 }))
121
+ flushSync()
122
+ expect(dialog()).not.toBeNull()
123
+ } finally { destroy() }
124
+ })
125
+ })