@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
@@ -7,23 +7,57 @@
7
7
  * One styled renderer over the headless `createDropdownList` core (state +
8
8
  * keyboard + ARIA); only portal/measure/scroll render concerns live here.
9
9
  */
10
- import { anchoredRect, portalToBody, type AnchoredRect } from './popover'
11
- import type { ListOption } from './list-option'
10
+ import { flushSync } from 'svelte'
11
+ import { anchoredRect, portalToBody, popIn, type AnchoredRect } from './popover'
12
+ import { createDismissableLayer } from './a11y/dismissable'
13
+ import { groupOptions, hasGroups, type ListOption } from './list-option'
14
+ import { virtualRange, scrollToIndex } from './virtual'
15
+ import SvField from './SvField.svelte'
16
+ import { nextEditorId, type SvEditorProps } from './editor-contract'
12
17
  import { createDropdownList } from './createDropdownList.svelte'
13
18
 
14
- type Props = {
19
+ type Props = SvEditorProps & {
15
20
  options: ReadonlyArray<ListOption>
16
21
  value?: string | number | null
17
22
  onChange?: (value: string | number) => void
18
23
  placeholder?: string
19
- disabled?: boolean
20
- name?: string
21
- size?: 'sm' | 'md' | 'lg'
22
- ariaLabel?: string
23
24
  autoOpen?: boolean
25
+ /** Window the option list (render only visible) for large sets. */
26
+ virtual?: boolean
27
+ /** Fixed option height in px (must match the CSS). Default 34. */
28
+ rowHeight?: number
24
29
  }
25
30
 
26
- let { options, value = null, onChange, placeholder = 'Select…', disabled = false, name, size = 'md', ariaLabel, autoOpen = false }: Props = $props()
31
+ let {
32
+ options,
33
+ value = null,
34
+ onChange,
35
+ placeholder = 'Select…',
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
+ autoOpen = false,
48
+ virtual = false,
49
+ rowHeight = 34,
50
+ }: Props = $props()
51
+
52
+ const autoId = nextEditorId('sv-ddl')
53
+ const uid = $derived(id ?? autoId)
54
+
55
+ // Virtualization applies to a flat (ungrouped) list.
56
+ const useVirtual = $derived(virtual && !hasGroups(options))
57
+ let scrollTop = $state(0)
58
+ let viewportH = $state(0)
59
+ const vr = $derived(virtualRange({ scrollTop, viewportHeight: viewportH, rowHeight, count: options.length, overscan: 10 }))
60
+ const windowed = $derived(useVirtual ? options.slice(vr.start, vr.end).map((o, i) => ({ ...o, index: vr.start + i })) : [])
27
61
 
28
62
  let triggerEl = $state<HTMLButtonElement | null>(null)
29
63
  let panelEl = $state<HTMLDivElement | null>(null)
@@ -36,6 +70,11 @@
36
70
  disabled: () => disabled,
37
71
  ariaLabel: () => ariaLabel,
38
72
  focusTrigger: () => triggerEl?.focus(),
73
+ id: () => uid,
74
+ invalid: () => invalid,
75
+ required: () => required,
76
+ error: () => error,
77
+ hint: () => hint,
39
78
  })
40
79
 
41
80
  const selected = $derived(ddl.selected)
@@ -50,43 +89,74 @@
50
89
  updatePos()
51
90
  const rp = () => updatePos()
52
91
  window.addEventListener('scroll', rp, true); window.addEventListener('resize', rp)
53
- const od = (e: PointerEvent) => { const t = e.target as Node | null; if (t && (triggerEl?.contains(t) || panelEl?.contains(t))) return; ddl.close() }
54
- document.addEventListener('pointerdown', od, true)
55
- return () => { window.removeEventListener('scroll', rp, true); window.removeEventListener('resize', rp); document.removeEventListener('pointerdown', od, true) }
92
+ // Outside-pointer dismissal via the shared layer stack (Escape stays with
93
+ // the core keyboard handler). Because this layer is pushed ON TOP, a
94
+ // dropdown opened inside a dialog/drawer closes itself first and never
95
+ // dismisses the dialog underneath it.
96
+ const layer = createDismissableLayer({
97
+ element: () => [triggerEl, panelEl],
98
+ onDismiss: () => ddl.close(),
99
+ closeOnEscape: false,
100
+ })
101
+ layer.activate()
102
+ return () => { window.removeEventListener('scroll', rp, true); window.removeEventListener('resize', rp); layer.release() }
56
103
  })
57
104
  // Keep the active option scrolled into view (render concern).
58
105
  $effect(() => {
59
106
  if (!ddl.open) return
60
107
  const i = ddl.activeIndex
61
- queueMicrotask(() => panelEl?.querySelector<HTMLElement>(`[data-idx="${i}"]`)?.scrollIntoView({ block: 'nearest' }))
108
+ if (useVirtual) {
109
+ if (!panelEl) return
110
+ const next = scrollToIndex(i, panelEl.scrollTop, panelEl.clientHeight, rowHeight)
111
+ if (next !== panelEl.scrollTop) { panelEl.scrollTop = next; scrollTop = next }
112
+ } else {
113
+ queueMicrotask(() => panelEl?.querySelector<HTMLElement>(`[data-idx="${i}"]`)?.scrollIntoView({ block: 'nearest' }))
114
+ }
62
115
  })
63
116
  function focusOpen(node: HTMLButtonElement) { if (autoOpen) { node.focus(); ddl.openPanel() } }
64
117
  </script>
65
118
 
66
- <button
67
- bind:this={triggerEl}
68
- class="sv-ddl sv-ddl--{size}"
69
- class:is-open={ddl.open}
70
- class:is-disabled={disabled}
71
- {...ddl.triggerProps()}
72
- use:focusOpen
73
- >
74
- <span class="sv-ddl__value" class:is-placeholder={!selected}>{selected?.label ?? placeholder}</span>
75
- <svg class="sv-ddl__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>
76
- </button>
119
+ <SvField id={uid} {label} {hint} {error} {required} {dir}>
120
+ <button
121
+ bind:this={triggerEl}
122
+ class="sv-ddl sv-ddl--{size}"
123
+ class:is-open={ddl.open}
124
+ class:is-disabled={disabled}
125
+ class:is-invalid={invalid}
126
+ {...ddl.triggerProps()}
127
+ use:focusOpen
128
+ >
129
+ <span class="sv-ddl__value" class:is-placeholder={!selected}>{selected?.label ?? placeholder}</span>
130
+ <svg class="sv-ddl__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>
131
+ </button>
132
+ </SvField>
77
133
 
78
134
  {#if ddl.open}
79
- <div bind:this={panelEl} class="sv-ddl__panel" use:portalToBody style:position="fixed" style:top={`${rect.top}px`} style:left={`${rect.left}px`} style:min-width={`${rect.width}px`} {...ddl.listboxProps()}>
80
- {#each options as opt, i (opt.value)}
81
- <!-- svelte-ignore a11y_click_events_have_key_events a11y_interactive_supports_focus -->
82
- <div
83
- class="sv-ddl__opt"
84
- class:is-active={ddl.isActive(i)}
85
- class:is-selected={ddl.isSelected(opt)}
86
- class:is-disabled={opt.disabled}
87
- {...ddl.optionProps(i)}
88
- >{opt.label}</div>
135
+ <div bind:this={panelEl} class="sv-ddl__panel" class:is-virtual={useVirtual} 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`} onscroll={(e) => { scrollTop = e.currentTarget.scrollTop; if (useVirtual) flushSync() }} bind:clientHeight={viewportH} {...ddl.listboxProps()}>
136
+ {#if useVirtual}
137
+ <div class="sv-ddl__sizer" aria-hidden="true" style:height={`${vr.totalHeight}px`}></div>
138
+ {#each windowed as opt (opt.value)}
139
+ <!-- svelte-ignore a11y_click_events_have_key_events a11y_interactive_supports_focus -->
140
+ <div class="sv-ddl__opt sv-ddl__opt--abs" style:height={`${rowHeight}px`} style:transform={`translateY(${opt.index * rowHeight}px)`} class:is-active={ddl.isActive(opt.index)} class:is-selected={ddl.isSelected(opt)} class:is-disabled={opt.disabled} {...ddl.optionProps(opt.index)}>{opt.label}</div>
141
+ {/each}
142
+ {:else}
143
+ {#each groupOptions(options) as g (g.group ?? '')}
144
+ {#if g.group != null}
145
+ <div class="sv-ddl__group" role="group" aria-label={g.group}>
146
+ <div class="sv-ddl__group-label" aria-hidden="true">{g.group}</div>
147
+ {#each g.options as opt (opt.value)}
148
+ <!-- svelte-ignore a11y_click_events_have_key_events a11y_interactive_supports_focus -->
149
+ <div class="sv-ddl__opt" class:is-active={ddl.isActive(opt.index)} class:is-selected={ddl.isSelected(opt)} class:is-disabled={opt.disabled} {...ddl.optionProps(opt.index)}>{opt.label}</div>
150
+ {/each}
151
+ </div>
152
+ {:else}
153
+ {#each g.options as opt (opt.value)}
154
+ <!-- svelte-ignore a11y_click_events_have_key_events a11y_interactive_supports_focus -->
155
+ <div class="sv-ddl__opt" class:is-active={ddl.isActive(opt.index)} class:is-selected={ddl.isSelected(opt)} class:is-disabled={opt.disabled} {...ddl.optionProps(opt.index)}>{opt.label}</div>
156
+ {/each}
157
+ {/if}
89
158
  {/each}
159
+ {/if}
90
160
  </div>
91
161
  {/if}
92
162
  {#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
@@ -95,7 +165,7 @@
95
165
  .sv-ddl {
96
166
  --_accent: var(--sg-accent, #2563eb);
97
167
  display: inline-flex; align-items: center; justify-content: space-between; gap: 8px; width: 200px;
98
- background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a); font: inherit; text-align: left;
168
+ background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a); font: inherit; text-align: start;
99
169
  border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
100
170
  padding: 0 10px; cursor: pointer;
101
171
  }
@@ -103,6 +173,8 @@
103
173
  .sv-ddl--md { height: 34px; font-size: 13px; }
104
174
  .sv-ddl--lg { height: 40px; font-size: 15px; }
105
175
  .sv-ddl.is-open, .sv-ddl:focus-visible { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); outline: none; }
176
+ .sv-ddl.is-invalid { border-color: var(--sg-danger, #dc2626); }
177
+ .sv-ddl.is-invalid.is-open, .sv-ddl.is-invalid:focus-visible { box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-danger, #dc2626) 22%, transparent); }
106
178
  .sv-ddl.is-disabled { opacity: 0.6; cursor: not-allowed; }
107
179
  .sv-ddl__value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
108
180
  .sv-ddl__value.is-placeholder { color: var(--sg-muted, #94a3b8); }
@@ -113,8 +185,15 @@
113
185
  background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
114
186
  border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px; box-shadow: 0 16px 48px -12px rgba(15,23,42,0.35);
115
187
  }
116
- :global(.sv-ddl__opt) { padding: 7px 10px; border-radius: 6px; cursor: pointer; font-size: 13px; }
188
+ :global(.sv-ddl__opt) { box-sizing: border-box; display: flex; align-items: center; padding: 7px 10px; border-radius: 6px; cursor: pointer; font-size: 13px; }
189
+ :global(.sv-ddl__panel.is-virtual) { position: fixed; will-change: scroll-position; }
190
+ :global(.sv-ddl__sizer) { padding: 0; margin: 0; pointer-events: none; }
191
+ :global(.sv-ddl__opt--abs) { position: absolute; inset-inline: 4px; top: 4px; }
117
192
  :global(.sv-ddl__opt.is-active) { background: var(--sg-row-hover-bg, #f1f5f9); }
118
193
  :global(.sv-ddl__opt.is-selected) { color: var(--sg-accent, #2563eb); font-weight: 600; }
119
194
  :global(.sv-ddl__opt.is-disabled) { opacity: 0.4; cursor: not-allowed; }
195
+ :global(.sv-ddl__group-label) {
196
+ padding: 8px 10px 3px; font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em;
197
+ color: var(--sg-muted, #94a3b8);
198
+ }
120
199
  </style>
@@ -0,0 +1,108 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvDurationInput - a duration editor whose value is a number of MINUTES but
4
+ * which accepts the human forms people type ("1h 30m", "1:30", "90"). It shows
5
+ * a formatted value when unfocused and re-parses on blur / Enter. As a grid
6
+ * cell editor: Enter commits, Escape cancels.
7
+ */
8
+ import SvField from './SvField.svelte'
9
+ import { editorAria, nextEditorId, type SvEditorProps } from './editor-contract'
10
+ import { parseDuration, formatDuration } from './duration'
11
+
12
+ type Props = SvEditorProps & {
13
+ /** Value in minutes (or null when empty). */
14
+ value?: number | null
15
+ onChange?: (minutes: number | null) => void
16
+ onCommit?: (minutes: number | null) => void
17
+ onCancel?: () => void
18
+ /** Display style when unfocused: `1:30` (colon) or `1h 30m` (units). */
19
+ style?: 'colon' | 'units'
20
+ placeholder?: string
21
+ autofocus?: boolean
22
+ }
23
+
24
+ let {
25
+ value = $bindable<number | null>(null),
26
+ onChange,
27
+ onCommit,
28
+ onCancel,
29
+ style = 'colon',
30
+ placeholder = 'e.g. 1h 30m',
31
+ disabled = false,
32
+ readonly = false,
33
+ name,
34
+ size = 'md',
35
+ ariaLabel,
36
+ invalid = false,
37
+ required = false,
38
+ error,
39
+ label,
40
+ hint,
41
+ dir,
42
+ id,
43
+ autofocus = false,
44
+ }: Props = $props()
45
+
46
+ const autoId = nextEditorId('sv-dur')
47
+ const uid = $derived(id ?? autoId)
48
+
49
+ let text = $state('')
50
+ let focused = $state(false)
51
+ // Reflect the external value into the field whenever the user isn't typing.
52
+ $effect(() => {
53
+ if (!focused) text = value == null ? '' : formatDuration(value, style)
54
+ })
55
+
56
+ function commit(): number | null {
57
+ const mins = parseDuration(text)
58
+ value = mins // enables bind:value
59
+ onChange?.(mins)
60
+ if (mins != null) text = formatDuration(mins, style)
61
+ return mins
62
+ }
63
+ function mountFocus(node: HTMLInputElement) {
64
+ if (autofocus) { node.focus(); node.select() }
65
+ }
66
+ </script>
67
+
68
+ <SvField id={uid} {label} {hint} {error} {required} {dir}>
69
+ <div class="sv-dur sv-dur--{size}" class:is-disabled={disabled}>
70
+ <div class="sv-dur__field" class:is-invalid={invalid}>
71
+ <input
72
+ use:mountFocus
73
+ class="sv-dur__input"
74
+ type="text"
75
+ inputmode="numeric"
76
+ value={text}
77
+ {placeholder}
78
+ {disabled}
79
+ {readonly}
80
+ oninput={(e) => (text = e.currentTarget.value)}
81
+ onfocus={() => (focused = true)}
82
+ onblur={() => { focused = false; commit() }}
83
+ onkeydown={(e) => {
84
+ if (e.key === 'Enter') { onCommit?.(commit()) }
85
+ else if (e.key === 'Escape') onCancel?.()
86
+ }}
87
+ {...editorAria({ id: uid, invalid, required, error, hint, ariaLabel })}
88
+ />
89
+ </div>
90
+ {#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
91
+ </div>
92
+ </SvField>
93
+
94
+ <style>
95
+ .sv-dur { --_accent: var(--sg-accent, #2563eb); display: inline-flex; flex-direction: column; width: 160px; }
96
+ .sv-dur.is-disabled { opacity: 0.6; }
97
+ .sv-dur__field {
98
+ display: flex; align-items: center;
99
+ background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
100
+ border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
101
+ }
102
+ .sv-dur__field:focus-within { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
103
+ .sv-dur__field.is-invalid { border-color: var(--sg-danger, #dc2626); }
104
+ .sv-dur__input { flex: 1; min-width: 0; border: 0; background: none; outline: none; color: inherit; font: inherit; padding: 0 10px; }
105
+ .sv-dur--sm .sv-dur__field { height: 28px; font-size: 12px; }
106
+ .sv-dur--md .sv-dur__field { height: 34px; font-size: 13px; }
107
+ .sv-dur--lg .sv-dur__field { height: 40px; font-size: 15px; }
108
+ </style>
@@ -0,0 +1,59 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvEmptyState - a centered placeholder for empty lists, no-results, and blank
4
+ * screens: an icon/illustration, a title, a description, and an action slot.
5
+ * Theme-driven. Parity: Smart empty / no-data states.
6
+ *
7
+ * ```svelte
8
+ * <SvEmptyState title="No orders yet" description="Create your first order to get going.">
9
+ * <SvButton>New order</SvButton>
10
+ * </SvEmptyState>
11
+ * ```
12
+ */
13
+ import type { Snippet } from 'svelte'
14
+
15
+ type Props = {
16
+ title?: string
17
+ description?: string
18
+ /** Tighter padding (for empty dropdowns / small panels). */
19
+ compact?: boolean
20
+ /** Custom icon / illustration (overrides the default). */
21
+ icon?: Snippet
22
+ /** Action buttons / links. */
23
+ children?: Snippet
24
+ }
25
+
26
+ let { title, description, compact = false, icon, children }: Props = $props()
27
+ </script>
28
+
29
+ <div class="sv-empty" class:is-compact={compact}>
30
+ <div class="sv-empty__icon" aria-hidden="true">
31
+ {#if icon}
32
+ {@render icon()}
33
+ {:else}
34
+ <svg viewBox="0 0 48 48" width="44" height="44" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
35
+ <rect x="8" y="12" width="32" height="26" rx="3" /><path d="M8 20h32" /><path d="M16 28h10" />
36
+ </svg>
37
+ {/if}
38
+ </div>
39
+ {#if title}<div class="sv-empty__title">{title}</div>{/if}
40
+ {#if description}<p class="sv-empty__desc">{description}</p>{/if}
41
+ {#if children}<div class="sv-empty__actions">{@render children()}</div>{/if}
42
+ </div>
43
+
44
+ <style>
45
+ .sv-empty {
46
+ display: flex; flex-direction: column; align-items: center; text-align: center;
47
+ gap: 6px; padding: 44px 24px; color: var(--sg-fg, #0f172a);
48
+ }
49
+ .sv-empty.is-compact { padding: 22px 16px; }
50
+ .sv-empty__icon {
51
+ color: var(--sg-muted, #94a3b8); margin-bottom: 4px;
52
+ display: grid; place-items: center; width: 66px; height: 66px; border-radius: 16px;
53
+ background: var(--sg-row-hover-bg, #f1f5f9);
54
+ }
55
+ .sv-empty.is-compact .sv-empty__icon { width: 48px; height: 48px; }
56
+ .sv-empty__title { font-size: 15px; font-weight: 650; }
57
+ .sv-empty__desc { margin: 0; max-width: 42ch; color: var(--sg-muted, #64748b); font-size: 13px; line-height: 1.55; }
58
+ .sv-empty__actions { display: flex; gap: 8px; margin-top: 8px; }
59
+ </style>
@@ -0,0 +1,85 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvField - the shared field chrome for every value-bearing editor in the kit.
4
+ * It renders an optional `label` (wired to the control via `for`/`id`), the
5
+ * control itself (passed as children), and an optional `hint` / `error` line
6
+ * whose ids match `editorHintId` / `editorErrorId` for `aria-describedby`.
7
+ *
8
+ * Editors wrap their control box in it so label/hint/error/RTL behave
9
+ * IDENTICALLY across the kit:
10
+ *
11
+ * ```svelte
12
+ * <SvField {id} {label} {hint} {error} {required} {dir}>
13
+ * <div class="sv-num">...control...</div>
14
+ * </SvField>
15
+ * ```
16
+ */
17
+ import type { Snippet } from 'svelte'
18
+ import { editorErrorId, editorHintId, type EditorDir } from './editor-contract'
19
+
20
+ let {
21
+ id,
22
+ label,
23
+ hint,
24
+ error,
25
+ required = false,
26
+ dir,
27
+ /** Stretch the field (and its control) to the container width. */
28
+ block = false,
29
+ children,
30
+ }: {
31
+ id?: string
32
+ label?: string
33
+ hint?: string
34
+ error?: string
35
+ required?: boolean
36
+ dir?: EditorDir
37
+ block?: boolean
38
+ children: Snippet
39
+ } = $props()
40
+
41
+ // 'auto'/undefined -> inherit from the document (no dir attribute).
42
+ const resolvedDir = $derived(dir === 'ltr' || dir === 'rtl' ? dir : undefined)
43
+ </script>
44
+
45
+ <div class="sv-field" class:sv-field--block={block} dir={resolvedDir}>
46
+ {#if label}
47
+ <label class="sv-field__label" for={id}>
48
+ {label}{#if required}<span class="sv-field__req" aria-hidden="true">*</span>{/if}
49
+ </label>
50
+ {/if}
51
+ {@render children()}
52
+ {#if error}
53
+ <span class="sv-field__error" id={editorErrorId(id)} role="alert">{error}</span>
54
+ {:else if hint}
55
+ <span class="sv-field__hint" id={editorHintId(id)}>{hint}</span>
56
+ {/if}
57
+ </div>
58
+
59
+ <style>
60
+ .sv-field {
61
+ display: inline-flex;
62
+ flex-direction: column;
63
+ gap: 3px;
64
+ text-align: start;
65
+ }
66
+ .sv-field--block { display: flex; width: 100%; }
67
+ .sv-field__label {
68
+ font-size: 12.5px;
69
+ font-weight: 550;
70
+ color: var(--sg-fg, #0f172a);
71
+ line-height: 1.3;
72
+ }
73
+ .sv-field__req { color: var(--sg-danger, #dc2626); margin-inline-start: 2px; }
74
+ .sv-field__hint {
75
+ font-size: 11.5px;
76
+ color: var(--sg-muted, #64748b);
77
+ line-height: 1.35;
78
+ }
79
+ .sv-field__error {
80
+ font-size: 11.5px;
81
+ font-weight: 500;
82
+ color: var(--sg-danger, #dc2626);
83
+ line-height: 1.35;
84
+ }
85
+ </style>
@@ -0,0 +1,169 @@
1
+ <script lang="ts" module>
2
+ export type { FileRejection, FileRejectReason } from './createFileUpload.svelte'
3
+ </script>
4
+
5
+ <script lang="ts">
6
+ /**
7
+ * SvFileUpload - a drag-and-drop file field with a click-to-browse fallback,
8
+ * accept / size / count validation and a selected-files list. Parity: Smart
9
+ * `smart-file-upload`. Controlled via `files` + `onChange`.
10
+ *
11
+ * Part of the editor kit: carries the shared contract (label, hint, error
12
+ * validation, `dir`/RTL and localizable `messages`) via `<SvField>`. The
13
+ * drag/validation/state live in the headless `createFileUpload` core.
14
+ */
15
+ import SvField from './SvField.svelte'
16
+ import { nextEditorId, resolveMessages, type SvEditorProps } from './editor-contract'
17
+ import { createFileUpload, type FileRejection } from './createFileUpload.svelte'
18
+
19
+ /** User-facing strings (localizable via `messages`). */
20
+ type FileMessages = { prompt: string; browse: string; remove: string; hintTypes: string }
21
+ const DEFAULT_MESSAGES: FileMessages = {
22
+ prompt: 'Drag files here or', browse: 'browse', remove: 'Remove', hintTypes: 'Accepted:',
23
+ }
24
+
25
+ type Props = SvEditorProps & {
26
+ files?: File[]
27
+ onChange?: (files: File[]) => void
28
+ onReject?: (rejections: FileRejection[]) => void
29
+ accept?: string
30
+ multiple?: boolean
31
+ /** Max size per file in bytes. */
32
+ maxSize?: number
33
+ maxFiles?: number
34
+ messages?: Partial<FileMessages>
35
+ }
36
+
37
+ let {
38
+ files = $bindable([]),
39
+ onChange,
40
+ onReject,
41
+ accept,
42
+ multiple = false,
43
+ maxSize,
44
+ maxFiles,
45
+ disabled = false,
46
+ readonly = false,
47
+ ariaLabel,
48
+ invalid = false,
49
+ required = false,
50
+ error,
51
+ label,
52
+ hint,
53
+ dir,
54
+ id,
55
+ messages,
56
+ }: Props = $props()
57
+
58
+ const autoId = nextEditorId('sv-file')
59
+ const uid = $derived(id ?? autoId)
60
+ const M = $derived(resolveMessages(DEFAULT_MESSAGES, messages))
61
+
62
+ const fu = createFileUpload({
63
+ files: () => files,
64
+ onChange: (f) => { files = f; onChange?.(f) },
65
+ onReject: (r) => onReject?.(r),
66
+ accept: () => accept,
67
+ multiple: () => multiple,
68
+ maxSize: () => maxSize,
69
+ maxFiles: () => maxFiles,
70
+ disabled: () => disabled,
71
+ readonly: () => readonly,
72
+ id: () => uid,
73
+ invalid: () => invalid,
74
+ required: () => required,
75
+ error: () => error,
76
+ hint: () => hint,
77
+ ariaLabel: () => ariaLabel,
78
+ })
79
+
80
+ let inputEl = $state<HTMLInputElement | null>(null)
81
+
82
+ function openPicker() { if (fu.isInteractive) inputEl?.click() }
83
+ function onDropzoneKey(e: KeyboardEvent) {
84
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openPicker() }
85
+ }
86
+ function onInputChange(e: Event) {
87
+ const list = (e.currentTarget as HTMLInputElement).files
88
+ if (list) fu.addFiles([...list])
89
+ ;(e.currentTarget as HTMLInputElement).value = '' // allow re-selecting the same file
90
+ }
91
+ function onDrop(e: DragEvent) {
92
+ e.preventDefault()
93
+ fu.setDragging(false)
94
+ if (e.dataTransfer?.files) fu.addFiles([...e.dataTransfer.files])
95
+ }
96
+
97
+ const fmtSize = (n: number) => (n < 1024 ? `${n} B` : n < 1048576 ? `${(n / 1024).toFixed(0)} KB` : `${(n / 1048576).toFixed(1)} MB`)
98
+ </script>
99
+
100
+ <SvField id={uid} {label} {hint} {error} {required} {dir}>
101
+ <div class="sv-file" class:is-disabled={disabled}>
102
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
103
+ <div
104
+ class="sv-file__zone"
105
+ class:is-dragging={fu.dragging}
106
+ class:is-invalid={invalid}
107
+ {...fu.dropzoneProps()}
108
+ onclick={openPicker}
109
+ onkeydown={onDropzoneKey}
110
+ ondragenter={(e) => { e.preventDefault(); fu.setDragging(true) }}
111
+ ondragover={(e) => { e.preventDefault(); fu.setDragging(true) }}
112
+ ondragleave={() => fu.setDragging(false)}
113
+ ondrop={onDrop}
114
+ >
115
+ <svg class="sv-file__icon" viewBox="0 0 24 24" width="26" height="26" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12" /></svg>
116
+ <p class="sv-file__prompt">{M.prompt} <span class="sv-file__browse">{M.browse}</span></p>
117
+ {#if accept}<p class="sv-file__types">{M.hintTypes} {accept}</p>{/if}
118
+ <input bind:this={inputEl} class="sv-file__input" {...fu.inputProps()} onchange={onInputChange} />
119
+ </div>
120
+
121
+ {#if files.length}
122
+ <ul class="sv-file__list">
123
+ {#each files as file, i (file.name + file.size + i)}
124
+ <li class="sv-file__item">
125
+ <span class="sv-file__name" title={file.name}>{file.name}</span>
126
+ <span class="sv-file__size">{fmtSize(file.size)}</span>
127
+ {#if fu.isInteractive}
128
+ <button class="sv-file__remove" aria-label={`${M.remove} ${file.name}`} {...fu.removeProps(i)}>&times;</button>
129
+ {/if}
130
+ </li>
131
+ {/each}
132
+ </ul>
133
+ {/if}
134
+ </div>
135
+ </SvField>
136
+
137
+ <style>
138
+ .sv-file {
139
+ --_accent: var(--sg-accent, #2563eb);
140
+ --_border: var(--sg-input-border, var(--sg-border, #cbd5e1));
141
+ --_radius: var(--sg-radius, 8px);
142
+ display: flex; flex-direction: column; gap: 8px; width: 320px; color: var(--sg-fg, #0f172a);
143
+ }
144
+ .sv-file.is-disabled { opacity: 0.6; }
145
+ .sv-file__zone {
146
+ display: flex; flex-direction: column; align-items: center; gap: 4px;
147
+ padding: 22px 16px; text-align: center; cursor: pointer;
148
+ background: var(--sg-input-bg, #fff); color: var(--sg-muted, #64748b);
149
+ border: 1.5px dashed var(--_border); border-radius: var(--_radius); transition: border-color 0.15s, background 0.15s;
150
+ }
151
+ .sv-file__zone:hover, .sv-file__zone:focus-visible { border-color: var(--_accent); outline: none; }
152
+ .sv-file__zone.is-dragging { border-color: var(--_accent); background: color-mix(in srgb, var(--_accent) 8%, transparent); color: var(--_accent); }
153
+ .sv-file__zone.is-invalid { border-color: var(--sg-danger, #dc2626); }
154
+ .sv-file__icon { color: var(--_accent); }
155
+ .sv-file__prompt { margin: 0; font-size: 13px; }
156
+ .sv-file__browse { color: var(--_accent); font-weight: 650; text-decoration: underline; }
157
+ .sv-file__types { margin: 0; font-size: 11.5px; opacity: 0.8; }
158
+ .sv-file__input { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); border: 0; }
159
+
160
+ .sv-file__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
161
+ .sv-file__item {
162
+ display: flex; align-items: center; gap: 8px; padding: 6px 8px; font-size: 12.5px;
163
+ background: var(--sg-header-bg, #f8fafc); border: 1px solid var(--sg-border, #e2e8f0); border-radius: 6px;
164
+ }
165
+ .sv-file__name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 550; }
166
+ .sv-file__size { color: var(--sg-muted, #64748b); flex: none; font-variant-numeric: tabular-nums; }
167
+ .sv-file__remove { flex: none; background: none; border: 0; color: var(--sg-muted, #64748b); cursor: pointer; font-size: 16px; line-height: 1; padding: 0 2px; }
168
+ .sv-file__remove:hover { color: var(--sg-danger, #dc2626); }
169
+ </style>