@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,197 @@
1
+ <script lang="ts" module>
2
+ export type { CommandItem } from './ui-app.types'
3
+ </script>
4
+
5
+ <script lang="ts">
6
+ /**
7
+ * SvCommand - a ⌘K command palette: a fuzzy-searchable list of actions in a
8
+ * centered, focus-trapped dialog. Built on the shared primitives (portal +
9
+ * focus trap + scroll lock + dismissable layer) and the pure `fuzzyScore`
10
+ * matcher. Optionally binds a global hotkey to toggle itself. Emits WAI-ARIA
11
+ * combobox markup (activedescendant).
12
+ *
13
+ * ```svelte
14
+ * <SvCommand commands={cmds} onRun={(c) => run(c)} /> // Cmd/Ctrl+K opens it
15
+ * ```
16
+ */
17
+ import type { CommandItem } from './ui-app.types'
18
+ import { portalToBody, popIn } from './popover'
19
+ import { createFocusTrap } from './a11y/focus-trap'
20
+ import { lockScroll } from './a11y/scroll-lock'
21
+ import { createDismissableLayer } from './a11y/dismissable'
22
+ import { fuzzyScore } from './fuzzy'
23
+
24
+ type Props = {
25
+ open?: boolean
26
+ onClose?: () => void
27
+ commands: ReadonlyArray<CommandItem>
28
+ onRun?: (cmd: CommandItem) => void
29
+ placeholder?: string
30
+ emptyText?: string
31
+ /** Global toggle hotkey (Cmd/Ctrl + key). `false` disables it. Default 'mod+k'. */
32
+ hotkey?: 'mod+k' | 'mod+p' | false
33
+ }
34
+
35
+ let {
36
+ open = $bindable(false),
37
+ onClose,
38
+ commands,
39
+ onRun,
40
+ placeholder = 'Type a command or search…',
41
+ emptyText = 'No results',
42
+ hotkey = 'mod+k',
43
+ }: Props = $props()
44
+
45
+ let query = $state('')
46
+ let active = $state(0)
47
+ let panelEl = $state<HTMLDivElement | null>(null)
48
+ let inputEl = $state<HTMLInputElement | null>(null)
49
+
50
+ const results = $derived.by(() => {
51
+ const q = query.trim()
52
+ const list = commands.filter((c) => !c.disabled)
53
+ if (!q) return list
54
+ return list
55
+ .map((c) => ({ c, s: fuzzyScore(`${c.label} ${c.keywords ?? ''} ${c.group ?? ''}`, q) }))
56
+ .filter((x) => x.s !== null)
57
+ .sort((a, b) => (b.s as number) - (a.s as number))
58
+ .map((x) => x.c)
59
+ })
60
+
61
+ function close() { open = false; onClose?.() }
62
+ function run(cmd: CommandItem | undefined) {
63
+ if (!cmd || cmd.disabled) return
64
+ close()
65
+ onRun?.(cmd)
66
+ cmd.onRun?.()
67
+ }
68
+ function move(delta: number) {
69
+ const n = results.length
70
+ if (!n) return
71
+ active = (active + delta + n) % n
72
+ queueMicrotask(() =>
73
+ panelEl?.querySelector(`[data-idx="${active}"]`)?.scrollIntoView({ block: 'nearest' }),
74
+ )
75
+ }
76
+ function onKeydown(e: KeyboardEvent) {
77
+ if (e.key === 'ArrowDown') { e.preventDefault(); move(1) }
78
+ else if (e.key === 'ArrowUp') { e.preventDefault(); move(-1) }
79
+ else if (e.key === 'Enter') { e.preventDefault(); run(results[active]) }
80
+ // Escape + outside-click are owned by the dismissable layer.
81
+ }
82
+
83
+ // Reset active whenever the query changes.
84
+ $effect(() => { void query; active = 0 })
85
+
86
+ // Focus + scroll lock + Escape/outside dismissal while open.
87
+ $effect(() => {
88
+ if (!open || !panelEl) return
89
+ query = ''
90
+ active = 0
91
+ const trap = createFocusTrap(panelEl, { initialFocus: () => inputEl })
92
+ trap.activate()
93
+ const unlock = lockScroll()
94
+ const layer = createDismissableLayer({ element: () => panelEl, onDismiss: () => close() })
95
+ layer.activate()
96
+ return () => { layer.release(); unlock(); trap.release() }
97
+ })
98
+
99
+ // Global toggle hotkey.
100
+ $effect(() => {
101
+ if (!hotkey) return
102
+ const key = hotkey === 'mod+p' ? 'p' : 'k'
103
+ const onKey = (e: KeyboardEvent) => {
104
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === key) {
105
+ e.preventDefault()
106
+ open = !open
107
+ }
108
+ }
109
+ window.addEventListener('keydown', onKey)
110
+ return () => window.removeEventListener('keydown', onKey)
111
+ })
112
+ </script>
113
+
114
+ {#if open}
115
+ <div class="sv-cmd__backdrop" use:portalToBody>
116
+ <div
117
+ bind:this={panelEl}
118
+ class="sv-cmd"
119
+ use:popIn={{}}
120
+ role="dialog"
121
+ aria-modal="true"
122
+ aria-label="Command palette"
123
+ tabindex="-1"
124
+ onkeydown={onKeydown}
125
+ >
126
+ <div class="sv-cmd__search">
127
+ <span class="sv-cmd__searchicon" aria-hidden="true">⌕</span>
128
+ <input
129
+ bind:this={inputEl}
130
+ class="sv-cmd__input"
131
+ type="text"
132
+ role="combobox"
133
+ aria-expanded="true"
134
+ aria-controls="sv-cmd-list"
135
+ aria-activedescendant={results[active] ? `sv-cmd-opt-${active}` : undefined}
136
+ {placeholder}
137
+ value={query}
138
+ oninput={(e) => (query = e.currentTarget.value)}
139
+ />
140
+ </div>
141
+ <ul class="sv-cmd__list" id="sv-cmd-list" role="listbox" aria-label="Commands">
142
+ {#each results as c, i (c.id)}
143
+ {#if !query.trim() && c.group && c.group !== results[i - 1]?.group}
144
+ <li class="sv-cmd__group" role="presentation">{c.group}</li>
145
+ {/if}
146
+ <li
147
+ id={`sv-cmd-opt-${i}`}
148
+ data-idx={i}
149
+ class="sv-cmd__opt"
150
+ class:is-active={i === active}
151
+ role="option"
152
+ aria-selected={i === active}
153
+ onpointermove={() => (active = i)}
154
+ onclick={() => run(c)}
155
+ >
156
+ {#if c.icon}<span class="sv-cmd__icon" aria-hidden="true">{c.icon}</span>{/if}
157
+ <span class="sv-cmd__label">{c.label}</span>
158
+ {#if c.hint}<span class="sv-cmd__hint">{c.hint}</span>{/if}
159
+ {#if c.shortcut}<span class="sv-cmd__kbd">{c.shortcut}</span>{/if}
160
+ </li>
161
+ {:else}
162
+ <li class="sv-cmd__empty" aria-disabled="true">{emptyText}</li>
163
+ {/each}
164
+ </ul>
165
+ </div>
166
+ </div>
167
+ {/if}
168
+
169
+ <style>
170
+ :global(.sv-cmd__backdrop) {
171
+ position: fixed; inset: 0; z-index: 2147483000; display: flex; justify-content: center;
172
+ align-items: flex-start; padding: 12vh 16px 16px;
173
+ background: rgba(15, 23, 42, 0.42); backdrop-filter: blur(2px);
174
+ animation: sv-cmd-fade 0.14s ease both;
175
+ }
176
+ @keyframes sv-cmd-fade { from { opacity: 0 } to { opacity: 1 } }
177
+ :global(.sv-cmd) {
178
+ width: min(560px, 100%); max-height: 62vh; display: flex; flex-direction: column; overflow: hidden;
179
+ background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
180
+ border: 1px solid var(--sg-border, #e2e8f0); border-radius: 14px;
181
+ box-shadow: 0 24px 64px -16px rgba(15, 23, 42, 0.5);
182
+ }
183
+ :global(.sv-cmd__search) { display: flex; align-items: center; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--sg-border, #e2e8f0); }
184
+ :global(.sv-cmd__searchicon) { color: var(--sg-muted, #64748b); font-size: 18px; }
185
+ :global(.sv-cmd__input) { flex: 1; min-width: 0; border: 0; background: none; outline: none; font: inherit; font-size: 15px; color: var(--sg-fg, #0f172a); }
186
+ :global(.sv-cmd__list) { list-style: none; margin: 0; padding: 6px; overflow: auto; }
187
+ :global(.sv-cmd__group) { padding: 8px 10px 4px; font-size: 11px; font-weight: 650; text-transform: uppercase; letter-spacing: 0.03em; color: var(--sg-muted, #94a3b8); }
188
+ :global(.sv-cmd__opt) { display: flex; align-items: center; gap: 10px; padding: 9px 10px; border-radius: 8px; cursor: pointer; font-size: 13.5px; }
189
+ :global(.sv-cmd__opt.is-active) { background: var(--sg-accent, #2563eb); color: var(--sg-on-accent, #fff); }
190
+ :global(.sv-cmd__icon) { flex: none; width: 20px; text-align: center; }
191
+ :global(.sv-cmd__label) { flex: none; }
192
+ :global(.sv-cmd__hint) { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--sg-muted, #94a3b8); font-size: 12.5px; }
193
+ :global(.sv-cmd__opt.is-active .sv-cmd__hint) { color: color-mix(in srgb, var(--sg-on-accent, #fff) 80%, transparent); }
194
+ :global(.sv-cmd__kbd) { flex: none; margin-inline-start: auto; font-size: 11px; padding: 1px 6px; border-radius: 5px; background: color-mix(in srgb, var(--sg-fg, #0f172a) 8%, transparent); color: var(--sg-muted, #64748b); }
195
+ :global(.sv-cmd__opt.is-active .sv-cmd__kbd) { background: color-mix(in srgb, var(--sg-on-accent, #fff) 22%, transparent); color: var(--sg-on-accent, #fff); }
196
+ :global(.sv-cmd__empty) { padding: 22px; text-align: center; color: var(--sg-muted, #94a3b8); font-size: 13px; }
197
+ </style>
@@ -0,0 +1,96 @@
1
+ <script lang="ts" module>
2
+ export type { MenuItem } from './SvMenuList.svelte'
3
+ </script>
4
+
5
+ <script lang="ts">
6
+ /**
7
+ * SvContextMenu - wraps a region and opens a menu at the pointer on right-click
8
+ * (or long-press). Reuses SvMenuList (submenus, keyboard, icons, shortcuts),
9
+ * portals to <body>, and closes via the shared dismissable layer stack.
10
+ * Parity: Smart context-menu.
11
+ *
12
+ * ```svelte
13
+ * <SvContextMenu items={items} onSelect={(i) => run(i)}>
14
+ * <div class="drop-zone">Right-click me</div>
15
+ * </SvContextMenu>
16
+ * ```
17
+ */
18
+ import type { Snippet } from 'svelte'
19
+ import { portalToBody, popIn } from './popover'
20
+ import { createDismissableLayer } from './a11y/dismissable'
21
+ import SvMenuList, { type MenuItem } from './SvMenuList.svelte'
22
+
23
+ type Props = {
24
+ items: ReadonlyArray<MenuItem>
25
+ onSelect?: (item: MenuItem) => void
26
+ disabled?: boolean
27
+ ariaLabel?: string
28
+ children?: Snippet
29
+ }
30
+
31
+ let { items, onSelect, disabled = false, ariaLabel, children }: Props = $props()
32
+
33
+ let open = $state(false)
34
+ let pos = $state({ x: 0, y: 0 })
35
+ let panelEl = $state<HTMLDivElement | null>(null)
36
+
37
+ const MENU_W = 200
38
+ function openAt(clientX: number, clientY: number) {
39
+ // Clamp so the menu stays on-screen (estimate height from item count).
40
+ const estH = Math.min(items.length, 12) * 34 + 8
41
+ const x = Math.min(clientX, window.innerWidth - MENU_W - 6)
42
+ const y = Math.min(clientY, window.innerHeight - estH - 6)
43
+ pos = { x: Math.max(6, x), y: Math.max(6, y) }
44
+ open = true
45
+ }
46
+
47
+ function onContextMenu(e: MouseEvent) {
48
+ if (disabled) return
49
+ e.preventDefault()
50
+ openAt(e.clientX, e.clientY)
51
+ }
52
+
53
+ $effect(() => {
54
+ if (!open) return
55
+ queueMicrotask(() => panelEl?.querySelector<HTMLElement>('[role="menuitem"]:not([disabled])')?.focus())
56
+ const layer = createDismissableLayer({
57
+ element: () => panelEl,
58
+ onDismiss: () => (open = false),
59
+ })
60
+ layer.activate()
61
+ // Reposition-close on scroll (a moved anchor would leave the menu stranded).
62
+ const onScroll = () => (open = false)
63
+ window.addEventListener('scroll', onScroll, true)
64
+ return () => { layer.release(); window.removeEventListener('scroll', onScroll, true) }
65
+ })
66
+ </script>
67
+
68
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
69
+ <div class="sv-ctx__zone" oncontextmenu={onContextMenu}>
70
+ {@render children?.()}
71
+ </div>
72
+
73
+ {#if open}
74
+ <div
75
+ bind:this={panelEl}
76
+ class="sv-ctx"
77
+ use:portalToBody
78
+ use:popIn={{}}
79
+ style:position="fixed"
80
+ style:top={`${pos.y}px`}
81
+ style:left={`${pos.x}px`}
82
+ aria-label={ariaLabel}
83
+ >
84
+ <SvMenuList {items} onclose={() => (open = false)} onselect={(i) => onSelect?.(i)} />
85
+ </div>
86
+ {/if}
87
+
88
+ <style>
89
+ .sv-ctx__zone { display: contents; }
90
+ :global(.sv-ctx) {
91
+ z-index: 2147483646; min-width: 200px;
92
+ background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
93
+ border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px;
94
+ box-shadow: 0 16px 48px -12px rgba(15, 23, 42, 0.35); font-size: 13px;
95
+ }
96
+ </style>
@@ -8,23 +8,48 @@
8
8
  * roving active index + keyboard); only portal/measure/focus render concerns
9
9
  * live here.
10
10
  */
11
- import { anchoredRect, portalToBody, type AnchoredRect } from './popover'
11
+ import { anchoredRect, portalToBody, popIn, type AnchoredRect } from './popover'
12
12
  import { flagEmoji } from './countries'
13
+ import SvField from './SvField.svelte'
14
+ import { nextEditorId, resolveMessages, type SvEditorProps } from './editor-contract'
13
15
  import { createCountryInput } from './createCountryInput.svelte'
14
16
 
15
- type Props = {
17
+ /** User-facing strings (localizable via `messages`). */
18
+ type CountryMessages = { search: string; noResults: string }
19
+ const DEFAULT_MESSAGES: CountryMessages = { search: 'Search countries', noResults: 'No matches' }
20
+
21
+ type Props = SvEditorProps & {
16
22
  value?: string | null
17
23
  onChange?: (code: string) => void
18
24
  /** Show the dial code beside each country. */
19
25
  showDial?: boolean
20
26
  placeholder?: string
21
- disabled?: boolean
22
- name?: string
23
- size?: 'sm' | 'md' | 'lg'
24
- ariaLabel?: string
27
+ /** Override the built-in strings (search + empty-state). */
28
+ messages?: Partial<CountryMessages>
25
29
  }
26
30
 
27
- let { value = null, onChange, showDial = false, placeholder = 'Select country…', disabled = false, name, size = 'md', ariaLabel }: Props = $props()
31
+ let {
32
+ value = null,
33
+ onChange,
34
+ showDial = false,
35
+ placeholder = 'Select country…',
36
+ disabled = false,
37
+ name,
38
+ size = 'md',
39
+ ariaLabel,
40
+ invalid = false,
41
+ required = false,
42
+ error,
43
+ label,
44
+ hint,
45
+ dir,
46
+ id,
47
+ messages,
48
+ }: Props = $props()
49
+
50
+ const autoId = nextEditorId('sv-country')
51
+ const uid = $derived(id ?? autoId)
52
+ const M = $derived(resolveMessages(DEFAULT_MESSAGES, messages))
28
53
 
29
54
  let triggerEl = $state<HTMLButtonElement | null>(null)
30
55
  let searchEl = $state<HTMLInputElement | null>(null)
@@ -36,7 +61,13 @@
36
61
  onChange: (c) => onChange?.(c),
37
62
  disabled: () => disabled,
38
63
  ariaLabel: () => ariaLabel,
64
+ searchLabel: () => M.search,
39
65
  focusTrigger: () => triggerEl?.focus(),
66
+ id: () => uid,
67
+ invalid: () => invalid,
68
+ required: () => required,
69
+ error: () => error,
70
+ hint: () => hint,
40
71
  })
41
72
 
42
73
  const selected = $derived(ci.selected)
@@ -61,20 +92,22 @@
61
92
  })
62
93
  </script>
63
94
 
64
- <button bind:this={triggerEl} class="sv-country sv-country--{size}" class:is-open={ci.open} class:is-disabled={disabled} {...ci.triggerProps()}>
65
- {#if selected}
66
- <span class="sv-country__flag" aria-hidden="true">{flagEmoji(selected.code)}</span>
67
- <span class="sv-country__name">{selected.name}</span>
68
- {#if showDial}<span class="sv-country__dial">{selected.dial}</span>{/if}
69
- {:else}
70
- <span class="sv-country__ph">{placeholder}</span>
71
- {/if}
72
- <svg class="sv-country__chev" viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
73
- </button>
95
+ <SvField id={uid} {label} {hint} {error} {required} {dir}>
96
+ <button bind:this={triggerEl} class="sv-country sv-country--{size}" class:is-open={ci.open} class:is-disabled={disabled} class:is-invalid={invalid} {...ci.triggerProps()}>
97
+ {#if selected}
98
+ <span class="sv-country__flag" aria-hidden="true">{flagEmoji(selected.code)}</span>
99
+ <span class="sv-country__name">{selected.name}</span>
100
+ {#if showDial}<span class="sv-country__dial">{selected.dial}</span>{/if}
101
+ {:else}
102
+ <span class="sv-country__ph">{placeholder}</span>
103
+ {/if}
104
+ <svg class="sv-country__chev" viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
105
+ </button>
106
+ </SvField>
74
107
 
75
108
  {#if ci.open}
76
- <div bind:this={panelEl} class="sv-country__panel" use:portalToBody style:position="fixed" style:top={`${rect.top}px`} style:left={`${rect.left}px`} style:min-width={`${rect.width}px`} role="dialog">
77
- <input bind:this={searchEl} class="sv-country__search" placeholder="Search countries…" {...ci.searchProps()} />
109
+ <div bind:this={panelEl} class="sv-country__panel" use:portalToBody use:popIn={{ up: rect.openUpward }} style:position="fixed" style:top={`${rect.top}px`} style:left={`${rect.left}px`} style:min-width={`${rect.width}px`} role="dialog">
110
+ <input bind:this={searchEl} class="sv-country__search" placeholder={`${M.search}…`} {...ci.searchProps()} />
78
111
  <div class="sv-country__list" {...ci.listboxProps()}>
79
112
  {#each ci.filtered as c, i (c.code)}
80
113
  <!-- svelte-ignore a11y_click_events_have_key_events a11y_interactive_supports_focus -->
@@ -84,7 +117,7 @@
84
117
  <span class="sv-country__dial">{c.dial}</span>
85
118
  </div>
86
119
  {:else}
87
- <div class="sv-ddl__empty">No matches</div>
120
+ <div class="sv-ddl__empty">{M.noResults}</div>
88
121
  {/each}
89
122
  </div>
90
123
  </div>
@@ -95,13 +128,15 @@
95
128
  .sv-country {
96
129
  --_accent: var(--sg-accent, #2563eb);
97
130
  display: inline-flex; align-items: center; gap: 8px; width: 220px;
98
- background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a); font: inherit; text-align: left;
131
+ background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a); font: inherit; text-align: start;
99
132
  border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px); padding: 0 10px; cursor: pointer;
100
133
  }
101
134
  .sv-country--sm { height: 28px; font-size: 12px; }
102
135
  .sv-country--md { height: 34px; font-size: 13px; }
103
136
  .sv-country--lg { height: 40px; font-size: 15px; }
104
137
  .sv-country.is-open, .sv-country:focus-visible { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); outline: none; }
138
+ .sv-country.is-invalid { border-color: var(--sg-danger, #dc2626); }
139
+ .sv-country.is-invalid.is-open, .sv-country.is-invalid:focus-visible { box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-danger, #dc2626) 22%, transparent); }
105
140
  .sv-country.is-disabled { opacity: 0.6; cursor: not-allowed; }
106
141
  .sv-country__flag { font-size: 16px; }
107
142
  .sv-country__name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@ -0,0 +1,256 @@
1
+ <script lang="ts" module>
2
+ export type DateRangeValue = [Date, Date] | null
3
+ </script>
4
+
5
+ <script lang="ts">
6
+ /**
7
+ * SvDateRangeInput - a start/end date-range field with a portalled two-month
8
+ * range calendar and one-click presets (Today, Last 7 days, This month, ...).
9
+ * Parity target: Smart date-range pickers. Emits `[start, end]` (inclusive) or
10
+ * null. It composes the existing headless range engine: the popover is just a
11
+ * `<SvCalendar selectionMode="range">`, so hover-preview, min/max, restricted /
12
+ * important dates and keyboard all come for free.
13
+ *
14
+ * Part of the SvGrid editor kit: carries the shared editor contract (label,
15
+ * hint, error/validation a11y, size, `dir`/RTL and localizable `messages`) via
16
+ * `<SvField>`, exactly like the other field editors.
17
+ */
18
+ import SvCalendar from './SvCalendar.svelte'
19
+ import SvField from './SvField.svelte'
20
+ import { anchoredRect, portalToBody, popIn, type AnchoredRect } from './popover'
21
+ import { editorAria, nextEditorId, resolveMessages, type SvEditorProps } from './editor-contract'
22
+ import { formatDate } from './datetime/date-format'
23
+ import { toDate, startOfDay, type DateLike } from './datetime/date-core'
24
+ import { rangeDays } from './datetime/date-selection'
25
+ import type { CalendarPreset } from './createCalendar.svelte'
26
+
27
+ /** User-facing strings (localizable via `messages`). */
28
+ type RangeMessages = { dialog: string; clear: string; open: string; to: string }
29
+ const DEFAULT_MESSAGES: RangeMessages = {
30
+ dialog: 'Choose a date range', clear: 'Clear', open: 'Open calendar', to: 'to',
31
+ }
32
+
33
+ type Props = SvEditorProps & {
34
+ /** Inclusive [start, end] range, or null. */
35
+ value?: DateRangeValue
36
+ onChange?: (value: DateRangeValue) => void
37
+ /** Display format for each end (token engine, e.g. 'yyyy-MM-dd'). */
38
+ formatString?: string
39
+ min?: DateLike | null
40
+ max?: DateLike | null
41
+ /** Number of month panels shown side by side. Default 2. */
42
+ months?: number
43
+ firstDayOfWeek?: number
44
+ weekNumbers?: boolean
45
+ locale?: string
46
+ placeholder?: string
47
+ /** Quick shortcuts shown in the calendar's side rail. */
48
+ presets?: ReadonlyArray<CalendarPreset>
49
+ /** Animate the calendar's month navigation. */
50
+ animate?: boolean | 'slide' | 'fade'
51
+ /** Open the popover as soon as the field is focused. */
52
+ autoOpen?: boolean
53
+ /** Override the built-in strings. */
54
+ messages?: Partial<RangeMessages>
55
+ }
56
+
57
+ let {
58
+ value = null,
59
+ onChange,
60
+ formatString = 'yyyy-MM-dd',
61
+ min = null,
62
+ max = null,
63
+ months = 2,
64
+ firstDayOfWeek = 0,
65
+ weekNumbers = false,
66
+ locale = typeof navigator !== 'undefined' ? navigator.language : 'en-US',
67
+ placeholder = 'Select date range',
68
+ presets,
69
+ animate = false,
70
+ autoOpen = false,
71
+ disabled = false,
72
+ readonly = false,
73
+ name,
74
+ size = 'md',
75
+ ariaLabel,
76
+ invalid = false,
77
+ required = false,
78
+ error,
79
+ label,
80
+ hint,
81
+ dir,
82
+ id,
83
+ messages,
84
+ }: Props = $props()
85
+
86
+ const autoId = nextEditorId('sv-daterange')
87
+ const uid = $derived(id ?? autoId)
88
+ const M = $derived(resolveMessages(DEFAULT_MESSAGES, messages))
89
+ const resolvedDir = $derived(dir === 'ltr' || dir === 'rtl' ? dir : undefined)
90
+ const isInteractive = $derived(!disabled && !readonly)
91
+
92
+ const fmt = (d: Date) => formatDate(d, formatString, locale)
93
+ // The label shown in the field: "start to end", one end, or the placeholder.
94
+ const display = $derived.by(() => {
95
+ if (!value) return ''
96
+ const [a, b] = value
97
+ return a && b ? `${fmt(a)} ${M.to} ${fmt(b)}` : a ? fmt(a) : ''
98
+ })
99
+ // The calendar wants the FULL inclusive span as its value (every day renders
100
+ // selected); we only keep the two endpoints.
101
+ const calValue = $derived(value ? rangeDays(value[0], value[1]) : null)
102
+
103
+ let open = $state(false)
104
+ function openPanel() { if (isInteractive && !open) open = true }
105
+ function closePanel() { open = false }
106
+ function toggle() { open ? closePanel() : openPanel() }
107
+ function clear() { onChange?.(null) }
108
+
109
+ function onCalendarChange(dates: Date[]) {
110
+ if (!dates.length) return
111
+ const start = dates[0]!
112
+ const end = dates[dates.length - 1]!
113
+ onChange?.([startOfDay(start), startOfDay(end)])
114
+ // A completed range (2+ distinct days, or a single-day range) closes the panel.
115
+ if (dates.length >= 1 && start !== end) closePanel()
116
+ }
117
+
118
+ // --- Portalled popover positioning (DOM-bound; stays in the component) -------
119
+ let triggerEl = $state<HTMLDivElement | null>(null)
120
+ let panelEl = $state<HTMLDivElement | null>(null)
121
+ let panelRect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false })
122
+
123
+ function updatePos() {
124
+ if (!triggerEl) return
125
+ panelRect = anchoredRect(triggerEl.getBoundingClientRect(), { estimatedHeight: 360, minWidth: 300 })
126
+ }
127
+
128
+ $effect(() => {
129
+ if (!open) return
130
+ updatePos()
131
+ const reposition = () => updatePos()
132
+ window.addEventListener('scroll', reposition, true)
133
+ window.addEventListener('resize', reposition)
134
+ const onDown = (e: PointerEvent) => {
135
+ const t = e.target as Node | null
136
+ if (!t) return
137
+ if (triggerEl?.contains(t) || panelEl?.contains(t)) return
138
+ closePanel()
139
+ }
140
+ document.addEventListener('pointerdown', onDown, true)
141
+ return () => {
142
+ window.removeEventListener('scroll', reposition, true)
143
+ window.removeEventListener('resize', reposition)
144
+ document.removeEventListener('pointerdown', onDown, true)
145
+ }
146
+ })
147
+
148
+ function onFieldKeydown(e: KeyboardEvent) {
149
+ if (e.key === 'Escape') { closePanel() }
150
+ else if (e.key === 'ArrowDown' && (e.altKey || !value)) { e.preventDefault(); openPanel() }
151
+ }
152
+ function focusOpen(node: HTMLInputElement) {
153
+ if (!autoOpen) return
154
+ node.focus()
155
+ openPanel()
156
+ }
157
+ </script>
158
+
159
+ <SvField id={uid} {label} {hint} {error} {required} {dir}>
160
+ <div class="sv-dri" class:sv-dri--disabled={disabled}>
161
+ <div class="sv-dri__field sv-dri__field--{size}" class:is-invalid={invalid} bind:this={triggerEl} dir={resolvedDir}>
162
+ <svg class="sv-dri__icon" viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2" /><path d="M16 2v4M8 2v4M3 10h18" /></svg>
163
+ <input
164
+ id={uid}
165
+ class="sv-dri__input"
166
+ type="text"
167
+ readonly
168
+ {placeholder}
169
+ value={display}
170
+ {disabled}
171
+ {...editorAria({ id: uid, invalid, required, error, hint, ariaLabel })}
172
+ aria-haspopup="dialog"
173
+ aria-expanded={open}
174
+ onclick={() => isInteractive && toggle()}
175
+ onkeydown={onFieldKeydown}
176
+ use:focusOpen
177
+ />
178
+ {#if value && isInteractive}
179
+ <button type="button" class="sv-dri__clear" aria-label={M.clear} onclick={(e) => { e.stopPropagation(); clear() }}>&times;</button>
180
+ {/if}
181
+ </div>
182
+
183
+ {#if open}
184
+ <div
185
+ class="sv-dri__panel"
186
+ bind:this={panelEl}
187
+ use:portalToBody
188
+ use:popIn={{ up: panelRect.openUpward }}
189
+ style:position="fixed"
190
+ style:top={`${panelRect.top}px`}
191
+ style:left={`${panelRect.left}px`}
192
+ role="dialog"
193
+ aria-label={M.dialog}
194
+ dir={resolvedDir}
195
+ >
196
+ <SvCalendar
197
+ value={calValue}
198
+ selectionMode="range"
199
+ {min}
200
+ {max}
201
+ {months}
202
+ {firstDayOfWeek}
203
+ {weekNumbers}
204
+ {locale}
205
+ {presets}
206
+ {animate}
207
+ dir={resolvedDir}
208
+ onChange={onCalendarChange}
209
+ />
210
+ </div>
211
+ {/if}
212
+
213
+ {#if name}
214
+ <input type="hidden" name={`${name}_start`} value={value?.[0] ? value[0].toISOString() : ''} />
215
+ <input type="hidden" name={`${name}_end`} value={value?.[1] ? value[1].toISOString() : ''} />
216
+ {/if}
217
+ </div>
218
+ </SvField>
219
+
220
+ <style>
221
+ .sv-dri {
222
+ --_accent: var(--sg-accent, #2563eb);
223
+ --_border: var(--sg-input-border, var(--sg-border, #cbd5e1));
224
+ --_radius: var(--sg-radius, 8px);
225
+ display: inline-block; position: relative; color: var(--sg-fg, #0f172a);
226
+ }
227
+ .sv-dri--disabled { opacity: 0.6; }
228
+ .sv-dri__field {
229
+ display: flex; align-items: center; gap: 6px; width: 260px;
230
+ background: var(--sg-input-bg, #fff); border: 1px solid var(--_border); border-radius: var(--_radius);
231
+ padding-inline: 10px 4px;
232
+ }
233
+ .sv-dri__field:focus-within { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
234
+ .sv-dri__field.is-invalid { border-color: var(--sg-danger, #dc2626); }
235
+ .sv-dri__field.is-invalid:focus-within { box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-danger, #dc2626) 22%, transparent); }
236
+ .sv-dri__field--sm { height: 28px; font-size: 12px; }
237
+ .sv-dri__field--md { height: 34px; font-size: 13px; }
238
+ .sv-dri__field--lg { height: 40px; font-size: 15px; }
239
+ .sv-dri__icon { color: var(--sg-muted, #64748b); flex: none; }
240
+ .sv-dri__input {
241
+ flex: 1; min-width: 0; border: 0; background: none; outline: none; color: inherit; font: inherit;
242
+ cursor: pointer; text-overflow: ellipsis;
243
+ }
244
+ .sv-dri__input::placeholder { color: var(--sg-muted, #94a3b8); }
245
+ .sv-dri__clear {
246
+ display: grid; place-items: center; width: 24px; height: 24px; flex: none;
247
+ background: none; border: 0; color: var(--sg-muted, #64748b); cursor: pointer; border-radius: 6px; font-size: 17px; line-height: 1;
248
+ }
249
+ .sv-dri__clear:hover { color: var(--_accent); background: color-mix(in srgb, var(--_accent) 10%, transparent); }
250
+
251
+ :global(.sv-dri__panel) {
252
+ z-index: 2147483647;
253
+ background: var(--sg-bg, #fff); border: 1px solid var(--sg-border, #e2e8f0);
254
+ border-radius: 12px; box-shadow: 0 16px 48px -12px rgba(15, 23, 42, 0.35);
255
+ }
256
+ </style>