zabi-components 8.0.0 → 8.1.0

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 (364) hide show
  1. package/CHANGELOG.md +1924 -0
  2. package/README.md +243 -10
  3. package/THEME.md +149 -332
  4. package/THEMING.md +1192 -266
  5. package/dist/atoms/ActionPanel.svelte +15 -5
  6. package/dist/atoms/Avatar.svelte +104 -0
  7. package/dist/atoms/Avatar.svelte.d.ts +34 -0
  8. package/dist/atoms/Badge.svelte +36 -7
  9. package/dist/atoms/Badge.svelte.d.ts +2 -2
  10. package/dist/atoms/Button.svelte +177 -27
  11. package/dist/atoms/Button.svelte.d.ts +30 -2
  12. package/dist/atoms/Card.svelte +4 -4
  13. package/dist/atoms/CardHeader.svelte +16 -4
  14. package/dist/atoms/Checkbox.svelte +48 -21
  15. package/dist/atoms/Checkbox.svelte.d.ts +4 -2
  16. package/dist/atoms/CodeBlock.svelte +20 -2
  17. package/dist/atoms/CodeBlock.svelte.d.ts +4 -0
  18. package/dist/atoms/ColorPicker.svelte +259 -49
  19. package/dist/atoms/ColorPicker.svelte.d.ts +3 -0
  20. package/dist/atoms/Container.svelte +16 -8
  21. package/dist/atoms/DateField.svelte +107 -0
  22. package/dist/atoms/DateField.svelte.d.ts +53 -0
  23. package/dist/atoms/FieldMessages.svelte +69 -0
  24. package/dist/atoms/FieldMessages.svelte.d.ts +23 -0
  25. package/dist/atoms/FloatingActionButton.svelte +174 -0
  26. package/dist/atoms/FloatingActionButton.svelte.d.ts +51 -0
  27. package/dist/atoms/Heading.svelte +37 -5
  28. package/dist/atoms/Heading.svelte.d.ts +7 -0
  29. package/dist/atoms/IconButton.svelte +220 -24
  30. package/dist/atoms/IconButton.svelte.d.ts +49 -4
  31. package/dist/atoms/Input.svelte +253 -52
  32. package/dist/atoms/Input.svelte.d.ts +32 -2
  33. package/dist/atoms/List.svelte +4 -1
  34. package/dist/atoms/ListItem.svelte +44 -11
  35. package/dist/atoms/ListItem.svelte.d.ts +5 -0
  36. package/dist/atoms/Progress.svelte +27 -8
  37. package/dist/atoms/Progress.svelte.d.ts +4 -0
  38. package/dist/atoms/Radio.svelte +19 -3
  39. package/dist/atoms/Radio.svelte.d.ts +4 -2
  40. package/dist/atoms/Rating.svelte +494 -0
  41. package/dist/atoms/Rating.svelte.d.ts +56 -0
  42. package/dist/atoms/Select.svelte +433 -91
  43. package/dist/atoms/Select.svelte.d.ts +49 -3
  44. package/dist/atoms/SelectionControl.svelte +94 -17
  45. package/dist/atoms/SelectionControl.svelte.d.ts +4 -3
  46. package/dist/atoms/Skeleton.svelte +1 -1
  47. package/dist/atoms/Slider.svelte +309 -0
  48. package/dist/atoms/Slider.svelte.d.ts +40 -0
  49. package/dist/atoms/Spinner.svelte +57 -0
  50. package/dist/atoms/Spinner.svelte.d.ts +19 -0
  51. package/dist/atoms/Table.svelte +151 -6
  52. package/dist/atoms/Table.svelte.d.ts +15 -3
  53. package/dist/atoms/TemporalField.svelte +140 -0
  54. package/dist/atoms/TemporalField.svelte.d.ts +32 -0
  55. package/dist/atoms/Text.svelte +23 -11
  56. package/dist/atoms/Text.svelte.d.ts +6 -3
  57. package/dist/atoms/Textarea.svelte +55 -44
  58. package/dist/atoms/Textarea.svelte.d.ts +14 -2
  59. package/dist/atoms/ThemeToggle.svelte +223 -44
  60. package/dist/atoms/ThemeToggle.svelte.d.ts +26 -3
  61. package/dist/atoms/TimeField.svelte +108 -0
  62. package/dist/atoms/TimeField.svelte.d.ts +54 -0
  63. package/dist/atoms/Toast.svelte +17 -4
  64. package/dist/atoms/Toast.svelte.d.ts +2 -0
  65. package/dist/atoms/Toggle.svelte +41 -8
  66. package/dist/atoms/Toggle.svelte.d.ts +11 -2
  67. package/dist/atoms/Tooltip.svelte +508 -23
  68. package/dist/atoms/Tooltip.svelte.d.ts +11 -0
  69. package/dist/atoms/ZabiStringsProvider.svelte +45 -0
  70. package/dist/atoms/ZabiStringsProvider.svelte.d.ts +27 -0
  71. package/dist/atoms/index.d.ts +17 -0
  72. package/dist/atoms/index.js +12 -0
  73. package/dist/atoms/selection-control.styles.d.ts +36 -5
  74. package/dist/atoms/selection-control.styles.js +40 -9
  75. package/dist/components/atoms/index.d.ts +17 -0
  76. package/dist/components/atoms/index.d.ts.map +1 -1
  77. package/dist/components/atoms/selection-control.styles.d.ts +36 -5
  78. package/dist/components/atoms/selection-control.styles.d.ts.map +1 -1
  79. package/dist/components/index.d.ts +80 -2
  80. package/dist/components/index.d.ts.map +1 -1
  81. package/dist/components/molecules/index.d.ts +46 -0
  82. package/dist/components/molecules/index.d.ts.map +1 -1
  83. package/dist/components/molecules/toast-store.d.ts +53 -7
  84. package/dist/components/molecules/toast-store.d.ts.map +1 -1
  85. package/dist/components/organisms/index.d.ts +5 -0
  86. package/dist/components/organisms/index.d.ts.map +1 -1
  87. package/dist/components/types/variants.d.ts +15 -1
  88. package/dist/components/types/variants.d.ts.map +1 -1
  89. package/dist/components/util/app-shell.d.ts +28 -0
  90. package/dist/components/util/app-shell.d.ts.map +1 -0
  91. package/dist/components/util/avatar.d.ts +36 -0
  92. package/dist/components/util/avatar.d.ts.map +1 -0
  93. package/dist/components/util/bottom-sheet.d.ts +30 -0
  94. package/dist/components/util/bottom-sheet.d.ts.map +1 -0
  95. package/dist/components/util/bottom-tab-bar.d.ts +35 -0
  96. package/dist/components/util/bottom-tab-bar.d.ts.map +1 -0
  97. package/dist/components/util/calendar.d.ts +56 -0
  98. package/dist/components/util/calendar.d.ts.map +1 -0
  99. package/dist/components/util/collapsible.d.ts +58 -0
  100. package/dist/components/util/collapsible.d.ts.map +1 -0
  101. package/dist/components/util/confirm-dialog.d.ts +12 -0
  102. package/dist/components/util/confirm-dialog.d.ts.map +1 -0
  103. package/dist/components/util/date.d.ts +96 -0
  104. package/dist/components/util/date.d.ts.map +1 -0
  105. package/dist/components/util/drawer.d.ts +13 -0
  106. package/dist/components/util/drawer.d.ts.map +1 -0
  107. package/dist/components/util/dropdown.d.ts +47 -0
  108. package/dist/components/util/dropdown.d.ts.map +1 -0
  109. package/dist/components/util/field.d.ts +39 -0
  110. package/dist/components/util/field.d.ts.map +1 -0
  111. package/dist/components/util/fit-in-viewport.d.ts +201 -0
  112. package/dist/components/util/fit-in-viewport.d.ts.map +1 -0
  113. package/dist/components/util/focus-utils.d.ts +75 -2
  114. package/dist/components/util/focus-utils.d.ts.map +1 -1
  115. package/dist/components/util/hydration.d.ts +45 -0
  116. package/dist/components/util/hydration.d.ts.map +1 -0
  117. package/dist/components/util/keyboard-inset.d.ts +18 -0
  118. package/dist/components/util/keyboard-inset.d.ts.map +1 -0
  119. package/dist/components/util/media-grid.d.ts +51 -0
  120. package/dist/components/util/media-grid.d.ts.map +1 -0
  121. package/dist/components/util/media.d.ts +6 -0
  122. package/dist/components/util/media.d.ts.map +1 -0
  123. package/dist/components/util/overlay-chrome.d.ts +53 -0
  124. package/dist/components/util/overlay-chrome.d.ts.map +1 -0
  125. package/dist/components/util/overlay.d.ts +72 -0
  126. package/dist/components/util/overlay.d.ts.map +1 -0
  127. package/dist/components/util/photo.d.ts +215 -0
  128. package/dist/components/util/photo.d.ts.map +1 -0
  129. package/dist/components/util/portal.d.ts +23 -0
  130. package/dist/components/util/portal.d.ts.map +1 -0
  131. package/dist/components/util/pull-to-refresh.d.ts +27 -0
  132. package/dist/components/util/pull-to-refresh.d.ts.map +1 -0
  133. package/dist/components/util/radio-keys.d.ts +25 -0
  134. package/dist/components/util/radio-keys.d.ts.map +1 -0
  135. package/dist/components/util/rating.d.ts +23 -0
  136. package/dist/components/util/rating.d.ts.map +1 -0
  137. package/dist/components/util/ready-made-strings.d.ts +77 -0
  138. package/dist/components/util/ready-made-strings.d.ts.map +1 -0
  139. package/dist/components/util/scroll-padding.d.ts +20 -0
  140. package/dist/components/util/scroll-padding.d.ts.map +1 -0
  141. package/dist/components/util/segmented-control.d.ts +16 -0
  142. package/dist/components/util/segmented-control.d.ts.map +1 -0
  143. package/dist/components/util/select.d.ts +41 -0
  144. package/dist/components/util/select.d.ts.map +1 -0
  145. package/dist/components/util/sheet-drag.d.ts +60 -0
  146. package/dist/components/util/sheet-drag.d.ts.map +1 -0
  147. package/dist/components/util/sidebar.d.ts +63 -0
  148. package/dist/components/util/sidebar.d.ts.map +1 -0
  149. package/dist/components/util/sortable-list.d.ts +65 -0
  150. package/dist/components/util/sortable-list.d.ts.map +1 -0
  151. package/dist/components/util/stepper.d.ts +40 -0
  152. package/dist/components/util/stepper.d.ts.map +1 -0
  153. package/dist/components/util/swipeable-list-item.d.ts +36 -0
  154. package/dist/components/util/swipeable-list-item.d.ts.map +1 -0
  155. package/dist/components/util/theme-mode.d.ts +80 -0
  156. package/dist/components/util/theme-mode.d.ts.map +1 -0
  157. package/dist/components/util/toaster.d.ts +93 -0
  158. package/dist/components/util/toaster.d.ts.map +1 -0
  159. package/dist/components/util/tooltip.d.ts +10 -0
  160. package/dist/components/util/tooltip.d.ts.map +1 -0
  161. package/dist/components/util/top-navbar.d.ts +11 -0
  162. package/dist/components/util/top-navbar.d.ts.map +1 -0
  163. package/dist/components/util/touch-target.d.ts +31 -0
  164. package/dist/components/util/touch-target.d.ts.map +1 -0
  165. package/dist/components/util/zabi-strings.d.ts +133 -0
  166. package/dist/components/util/zabi-strings.d.ts.map +1 -0
  167. package/dist/create-theme/cli.js +169 -0
  168. package/dist/create-theme/index.d.ts +176 -0
  169. package/dist/create-theme/index.js +817 -0
  170. package/dist/create-theme/lib/ramp-math.js +128 -0
  171. package/dist/create-theme/lib/resolve.js +141 -0
  172. package/dist/create-theme/theme-data.js +1116 -0
  173. package/dist/index.d.ts +80 -2
  174. package/dist/index.js +57 -2
  175. package/dist/molecules/Alert.svelte +33 -5
  176. package/dist/molecules/Alert.svelte.d.ts +2 -0
  177. package/dist/molecules/AppBar.svelte +415 -0
  178. package/dist/molecules/AppBar.svelte.d.ts +69 -0
  179. package/dist/molecules/AvatarGroup.svelte +116 -0
  180. package/dist/molecules/AvatarGroup.svelte.d.ts +40 -0
  181. package/dist/molecules/BottomSheet.svelte +540 -0
  182. package/dist/molecules/BottomSheet.svelte.d.ts +93 -0
  183. package/dist/molecules/BottomTabBar.svelte +297 -0
  184. package/dist/molecules/BottomTabBar.svelte.d.ts +64 -0
  185. package/dist/molecules/Calendar.svelte +597 -0
  186. package/dist/molecules/Calendar.svelte.d.ts +72 -0
  187. package/dist/molecules/Collapsible.svelte +243 -0
  188. package/dist/molecules/Collapsible.svelte.d.ts +44 -0
  189. package/dist/molecules/CollapsibleGroup.svelte +126 -0
  190. package/dist/molecules/CollapsibleGroup.svelte.d.ts +14 -0
  191. package/dist/molecules/ComponentDemo.svelte +16 -4
  192. package/dist/molecules/ComponentDemo.svelte.d.ts +3 -0
  193. package/dist/molecules/ConfirmDialog.svelte +253 -0
  194. package/dist/molecules/ConfirmDialog.svelte.d.ts +55 -0
  195. package/dist/molecules/ContactForm.svelte +34 -17
  196. package/dist/molecules/ContactForm.svelte.d.ts +8 -0
  197. package/dist/molecules/Drawer.svelte +394 -0
  198. package/dist/molecules/Drawer.svelte.d.ts +55 -0
  199. package/dist/molecules/Dropdown.svelte +509 -48
  200. package/dist/molecules/Dropdown.svelte.d.ts +45 -6
  201. package/dist/molecules/DropdownItem.svelte +122 -0
  202. package/dist/molecules/DropdownItem.svelte.d.ts +28 -0
  203. package/dist/molecules/EmptyState.svelte +55 -10
  204. package/dist/molecules/EmptyState.svelte.d.ts +8 -0
  205. package/dist/molecules/FormField.svelte +9 -1
  206. package/dist/molecules/FormField.svelte.d.ts +2 -0
  207. package/dist/molecules/Header.svelte +1 -1
  208. package/dist/molecules/ImageUpload.svelte +383 -70
  209. package/dist/molecules/ImageUpload.svelte.d.ts +47 -2
  210. package/dist/molecules/MediaGrid.svelte +527 -0
  211. package/dist/molecules/MediaGrid.svelte.d.ts +77 -0
  212. package/dist/molecules/Modal.svelte +288 -72
  213. package/dist/molecules/Modal.svelte.d.ts +51 -2
  214. package/dist/molecules/NavigationMenuContent.svelte +58 -1
  215. package/dist/molecules/NavigationMenuItem.svelte +0 -1
  216. package/dist/molecules/NavigationMenuLink.svelte +1 -1
  217. package/dist/molecules/NavigationMenuList.svelte +10 -3
  218. package/dist/molecules/NavigationMenuTrigger.svelte +2 -2
  219. package/dist/molecules/Page.svelte +32 -2
  220. package/dist/molecules/Page.svelte.d.ts +11 -0
  221. package/dist/molecules/PhotoGrid.svelte +433 -0
  222. package/dist/molecules/PhotoGrid.svelte.d.ts +62 -0
  223. package/dist/molecules/PhotoViewer.svelte +1116 -0
  224. package/dist/molecules/PhotoViewer.svelte.d.ts +56 -0
  225. package/dist/molecules/PropsTable.svelte +22 -10
  226. package/dist/molecules/PropsTable.svelte.d.ts +3 -0
  227. package/dist/molecules/PullToRefresh.svelte +263 -0
  228. package/dist/molecules/PullToRefresh.svelte.d.ts +52 -0
  229. package/dist/molecules/RadioGroup.svelte +52 -14
  230. package/dist/molecules/RadioGroup.svelte.d.ts +14 -3
  231. package/dist/molecules/Section.svelte +1 -1
  232. package/dist/molecules/SegmentedControl.svelte +323 -0
  233. package/dist/molecules/SegmentedControl.svelte.d.ts +34 -0
  234. package/dist/molecules/SidebarBrandHeader.svelte +11 -1
  235. package/dist/molecules/SidebarBrandHeader.svelte.d.ts +3 -0
  236. package/dist/molecules/SidebarFooter.svelte +29 -6
  237. package/dist/molecules/SidebarFooter.svelte.d.ts +3 -0
  238. package/dist/molecules/SlideUp.svelte +220 -61
  239. package/dist/molecules/SlideUp.svelte.d.ts +26 -0
  240. package/dist/molecules/SortableList.svelte +632 -0
  241. package/dist/molecules/SortableList.svelte.d.ts +69 -0
  242. package/dist/molecules/Stepper.svelte +548 -0
  243. package/dist/molecules/Stepper.svelte.d.ts +39 -0
  244. package/dist/molecules/StickyActionBar.svelte +191 -0
  245. package/dist/molecules/StickyActionBar.svelte.d.ts +65 -0
  246. package/dist/molecules/SwipeableListItem.svelte +334 -0
  247. package/dist/molecules/SwipeableListItem.svelte.d.ts +54 -0
  248. package/dist/molecules/Tabs.svelte +233 -9
  249. package/dist/molecules/Tabs.svelte.d.ts +7 -0
  250. package/dist/molecules/Toaster.svelte +350 -5
  251. package/dist/molecules/Toaster.svelte.d.ts +95 -2
  252. package/dist/molecules/ToasterToast.svelte +253 -67
  253. package/dist/molecules/ToasterToast.svelte.d.ts +9 -0
  254. package/dist/molecules/UnsavedChangesBar.svelte +212 -0
  255. package/dist/molecules/UnsavedChangesBar.svelte.d.ts +43 -0
  256. package/dist/molecules/index.d.ts +46 -0
  257. package/dist/molecules/index.js +27 -0
  258. package/dist/molecules/toast-store.d.ts +53 -7
  259. package/dist/molecules/toast-store.js +21 -0
  260. package/dist/organisms/AppShell.svelte +196 -0
  261. package/dist/organisms/AppShell.svelte.d.ts +61 -0
  262. package/dist/organisms/SidebarAccountPanel.svelte +107 -12
  263. package/dist/organisms/SidebarAccountPanel.svelte.d.ts +19 -0
  264. package/dist/organisms/SidebarNavigation.svelte +96 -15
  265. package/dist/organisms/SidebarNavigation.svelte.d.ts +29 -1
  266. package/dist/organisms/SidebarPanel.svelte +31 -10
  267. package/dist/organisms/SidebarShell.svelte +162 -11
  268. package/dist/organisms/SidebarShell.svelte.d.ts +56 -2
  269. package/dist/organisms/TopNavbar.svelte +256 -31
  270. package/dist/organisms/TopNavbar.svelte.d.ts +26 -0
  271. package/dist/organisms/index.d.ts +5 -0
  272. package/dist/organisms/index.js +3 -0
  273. package/dist/types/components.d.ts +4 -0
  274. package/dist/types/components.d.ts.map +1 -1
  275. package/dist/types/index.d.ts +605 -25
  276. package/dist/types/index.d.ts.map +1 -1
  277. package/dist/types/variants.ts +17 -1
  278. package/dist/util/app-shell.d.ts +27 -0
  279. package/dist/util/app-shell.js +72 -0
  280. package/dist/util/avatar.d.ts +35 -0
  281. package/dist/util/avatar.js +52 -0
  282. package/dist/util/bottom-sheet.d.ts +29 -0
  283. package/dist/util/bottom-sheet.js +43 -0
  284. package/dist/util/bottom-tab-bar.d.ts +34 -0
  285. package/dist/util/bottom-tab-bar.js +54 -0
  286. package/dist/util/calendar.d.ts +55 -0
  287. package/dist/util/calendar.js +74 -0
  288. package/dist/util/collapsible.d.ts +57 -0
  289. package/dist/util/collapsible.js +23 -0
  290. package/dist/util/confirm-dialog.d.ts +11 -0
  291. package/dist/util/confirm-dialog.js +2 -0
  292. package/dist/util/date.d.ts +95 -0
  293. package/dist/util/date.js +237 -0
  294. package/dist/util/drawer.d.ts +12 -0
  295. package/dist/util/drawer.js +7 -0
  296. package/dist/util/dropdown.d.ts +46 -0
  297. package/dist/util/dropdown.js +21 -0
  298. package/dist/util/field.d.ts +38 -0
  299. package/dist/util/field.js +38 -0
  300. package/dist/util/fit-in-viewport.d.ts +200 -0
  301. package/dist/util/fit-in-viewport.js +399 -0
  302. package/dist/util/focus-utils.d.ts +75 -2
  303. package/dist/util/focus-utils.js +182 -9
  304. package/dist/util/hydration.d.ts +44 -0
  305. package/dist/util/hydration.js +58 -0
  306. package/dist/util/keyboard-inset.d.ts +17 -0
  307. package/dist/util/keyboard-inset.js +53 -0
  308. package/dist/util/media-grid.d.ts +50 -0
  309. package/dist/util/media-grid.js +81 -0
  310. package/dist/util/media.d.ts +5 -0
  311. package/dist/util/media.js +10 -0
  312. package/dist/util/overlay-chrome.d.ts +52 -0
  313. package/dist/util/overlay-chrome.js +56 -0
  314. package/dist/util/overlay.d.ts +71 -0
  315. package/dist/util/overlay.js +240 -0
  316. package/dist/util/photo.d.ts +214 -0
  317. package/dist/util/photo.js +206 -0
  318. package/dist/util/portal.d.ts +22 -0
  319. package/dist/util/portal.js +50 -0
  320. package/dist/util/pull-to-refresh.d.ts +26 -0
  321. package/dist/util/pull-to-refresh.js +30 -0
  322. package/dist/util/radio-keys.d.ts +24 -0
  323. package/dist/util/radio-keys.js +49 -0
  324. package/dist/util/rating.d.ts +22 -0
  325. package/dist/util/rating.js +26 -0
  326. package/dist/util/ready-made-strings.d.ts +76 -0
  327. package/dist/util/ready-made-strings.js +67 -0
  328. package/dist/util/scroll-padding.d.ts +19 -0
  329. package/dist/util/scroll-padding.js +44 -0
  330. package/dist/util/segmented-control.d.ts +15 -0
  331. package/dist/util/segmented-control.js +1 -0
  332. package/dist/util/select.d.ts +40 -0
  333. package/dist/util/select.js +63 -0
  334. package/dist/util/sheet-drag.d.ts +59 -0
  335. package/dist/util/sheet-drag.js +241 -0
  336. package/dist/util/sidebar.d.ts +62 -0
  337. package/dist/util/sidebar.js +38 -0
  338. package/dist/util/sortable-list.d.ts +64 -0
  339. package/dist/util/sortable-list.js +53 -0
  340. package/dist/util/stepper.d.ts +39 -0
  341. package/dist/util/stepper.js +38 -0
  342. package/dist/util/swipeable-list-item.d.ts +35 -0
  343. package/dist/util/swipeable-list-item.js +32 -0
  344. package/dist/util/theme-mode.d.ts +79 -0
  345. package/dist/util/theme-mode.js +121 -0
  346. package/dist/util/toaster.d.ts +92 -0
  347. package/dist/util/toaster.js +75 -0
  348. package/dist/util/tooltip.d.ts +9 -0
  349. package/dist/util/tooltip.js +18 -0
  350. package/dist/util/top-navbar.d.ts +10 -0
  351. package/dist/util/top-navbar.js +5 -0
  352. package/dist/util/touch-target.d.ts +30 -0
  353. package/dist/util/touch-target.js +30 -0
  354. package/dist/util/zabi-strings.d.ts +132 -0
  355. package/dist/util/zabi-strings.js +105 -0
  356. package/dist/zabi-components-colors.css +5 -2
  357. package/dist/zabi-components-theme-dark-only.css +543 -241
  358. package/dist/zabi-components-theme-dark.css +543 -241
  359. package/dist/zabi-components-theme-only.css +1584 -367
  360. package/dist/zabi-components-theme.css +1584 -367
  361. package/dist/zabi-components.css +3885 -518
  362. package/docs/lucide-icons.md +40 -17
  363. package/docs/theme-imports.md +81 -11
  364. package/package.json +35 -8
@@ -1,9 +1,12 @@
1
1
  <script lang="ts">
2
- import { onDestroy } from "svelte";
2
+ import { DEFAULT_IMAGE_UPLOAD_TEXTS, zabiStringsFor } from "../util/zabi-strings.js";
3
+ import { onDestroy, tick, untrack, type Snippet } from "svelte";
3
4
  import type { HTMLAttributes } from "svelte/elements";
4
5
  import Button from "../atoms/Button.svelte";
5
- import { Image } from "@lucide/svelte";
6
+ import Image from "@lucide/svelte/icons/image";
6
7
  import { cn } from "../util/cn.js";
8
+ import { isVideoPath, isVideoUrl } from "../util/media.js";
9
+ import { generateId } from "../util/ssr-safe.js";
7
10
 
8
11
  export type ImageUploadFileDetail = {
9
12
  /** Selected file, or `null` when the image is removed. */
@@ -12,54 +15,213 @@
12
15
  url: string | null;
13
16
  };
14
17
 
15
- type Props = Omit<HTMLAttributes<HTMLDivElement>, "onchange" | "onclick"> & {
18
+ export type ImageUploadPreviewType = "image" | "video";
19
+
20
+ export type ImageUploadActionsPlacement = "overlay" | "strip";
21
+
22
+ export type ImageUploadPreviewDetail = {
23
+ /** The current `value`. */
24
+ url: string;
25
+ /** What the built-in preview would have rendered. */
26
+ type: ImageUploadPreviewType;
27
+ alt: string;
28
+ };
29
+
30
+ type Props = Omit<HTMLAttributes<HTMLDivElement>, "onchange" | "onclick" | "id"> & {
16
31
  /** Extra classes for the host element. */
17
32
  class?: string;
33
+ /** Id of the control the label points at; generated when omitted. */
34
+ id?: string;
35
+ /** Visible label, associated with the dropzone. */
36
+ label?: string;
18
37
  /** Preview URL; `bind:value` to read it. */
19
38
  value?: string | null;
20
39
  disabled?: boolean;
21
40
  accept?: string;
22
41
  placeholder?: string;
42
+ /** Second line of the dropzone. */
43
+ browseText?: string;
44
+ changeText?: string;
45
+ removeText?: string;
46
+ /** Accessible name of the Change action; defaults to `changeText` plus `label`. */
47
+ changeLabel?: string;
48
+ /** Accessible name of the Remove action; defaults to `removeText` plus `label`. */
49
+ removeLabel?: string;
50
+ /** Text alternative for the preview. */
51
+ alt?: string;
52
+ /** Forces the preview kind when the URL has no extension to go by. */
53
+ previewType?: ImageUploadPreviewType;
54
+ /** Replaces the built-in `<img>` / `<video>` preview. */
55
+ preview?: Snippet<[ImageUploadPreviewDetail]>;
56
+ /**
57
+ * Where Change and Remove sit: over the whole preview, or in a strip
58
+ * along its top edge. Defaults to the strip for a video, whose controls
59
+ * an overlay would cover, and to the overlay otherwise.
60
+ */
61
+ actionsPlacement?: ImageUploadActionsPlacement;
62
+ /** Announced to screen readers when a file or URL is set. */
63
+ selectedText?: string;
64
+ /** Announced to screen readers when the value is cleared. */
65
+ removedText?: string;
23
66
  errorMessage?: string;
67
+ errorTitle?: string;
68
+ /** Closing line of the error block; `false` or `""` leaves it out. */
69
+ errorRecovery?: string | false;
24
70
  /** Native `change` event from the hidden file input. */
25
71
  onchange?: (event: Event) => void;
26
- /** Fires when the file chooser is opened. */
72
+ /** Fires before the file chooser opens; `event.preventDefault()` keeps it closed. */
27
73
  onclick?: (event: Event) => void;
74
+ /** Replaces the native file chooser, for a media library or another source. */
75
+ onbrowse?: (event: Event) => void;
28
76
  /** Selected file and preview URL; `{ file: null, url: null }` on remove. */
29
77
  onfileselect?: (detail: ImageUploadFileDetail) => void;
78
+ /** A dropped file that `accept` does not allow. */
79
+ onfilereject?: (file: File) => void;
30
80
  };
31
81
 
32
82
  let {
33
83
  class: className = "",
34
- value = $bindable(null),
84
+ id: idProp,
85
+ label = "",
86
+ value = $bindable<Exclude<Props["value"], undefined>>(),
35
87
  disabled = false,
36
88
  accept = "image/*",
37
- placeholder = "No image selected",
89
+ placeholder: placeholderGiven,
90
+ browseText: browseTextGiven,
91
+ changeText: changeTextGiven,
92
+ removeText: removeTextGiven,
93
+ changeLabel,
94
+ removeLabel,
95
+ alt = "",
96
+ previewType,
97
+ preview,
98
+ actionsPlacement,
99
+ selectedText: selectedTextGiven,
100
+ removedText: removedTextGiven,
38
101
  errorMessage = "",
102
+ errorTitle: errorTitleGiven,
103
+ errorRecovery: errorRecoveryGiven,
39
104
  onchange,
40
105
  onclick,
106
+ onbrowse,
41
107
  onfileselect,
108
+ onfilereject,
42
109
  children: _children,
43
110
  ...restProps
44
111
  }: Props = $props();
45
112
 
113
+ /** Each text: the prop, else the app-wide word from a `ZabiStringsProvider` above this one, else English. */
114
+ const provided = zabiStringsFor("imageUpload");
115
+ const placeholder = $derived(placeholderGiven ?? provided()?.placeholder ?? DEFAULT_IMAGE_UPLOAD_TEXTS.placeholder);
116
+ const browseText = $derived(browseTextGiven ?? provided()?.browseText ?? DEFAULT_IMAGE_UPLOAD_TEXTS.browseText);
117
+ const changeText = $derived(changeTextGiven ?? provided()?.changeText ?? DEFAULT_IMAGE_UPLOAD_TEXTS.changeText);
118
+ const removeText = $derived(removeTextGiven ?? provided()?.removeText ?? DEFAULT_IMAGE_UPLOAD_TEXTS.removeText);
119
+ const selectedText = $derived(selectedTextGiven ?? provided()?.selectedText ?? DEFAULT_IMAGE_UPLOAD_TEXTS.selectedText);
120
+ const removedText = $derived(removedTextGiven ?? provided()?.removedText ?? DEFAULT_IMAGE_UPLOAD_TEXTS.removedText);
121
+ const errorTitle = $derived(errorTitleGiven ?? provided()?.errorTitle ?? DEFAULT_IMAGE_UPLOAD_TEXTS.errorTitle);
122
+ const errorRecovery = $derived(errorRecoveryGiven ?? provided()?.errorRecovery ?? DEFAULT_IMAGE_UPLOAD_TEXTS.errorRecovery);
123
+
124
+ // No fallback on a bindable prop: Svelte refuses `bind:…={undefined}` on
125
+ // one that has a fallback (`props_invalid_value`), and a page that throws
126
+ // while it hydrates never becomes interactive. The default is applied
127
+ // here instead: at once, for the server and the first render, and again
128
+ // whenever a parent hands back `undefined`.
129
+ const applyDefaults = () => {
130
+ if (value === undefined) value = null;
131
+ };
132
+ applyDefaults();
133
+ $effect.pre(applyDefaults);
134
+
135
+ const fallbackId = generateId("image-upload");
136
+ const controlId = $derived(idProp ?? fallbackId);
137
+ const labelId = $derived(`${controlId}-label`);
138
+ const hintId = $derived(`${controlId}-hint`);
139
+ const errorId = $derived(`${controlId}-error`);
140
+
141
+ let host = $state<HTMLDivElement>();
46
142
  let fileInput = $state<HTMLInputElement>();
47
143
  let currentObjectUrl = $state<string | null>(null);
144
+ // An object URL has no extension, so the kind is kept from the file itself.
145
+ let currentObjectType = $state<ImageUploadPreviewType | null>(null);
146
+ // dragenter/dragleave also fire for children; count them instead of toggling.
147
+ let dragDepth = $state(0);
148
+
149
+ const isDragOver = $derived(dragDepth > 0);
150
+
151
+ let statusMessage = $state("");
152
+ let previousValue: string | null | undefined;
153
+ // Set by Change, Remove and the dropzone: the next swap is this control's doing.
154
+ let focusReturnArmed = false;
155
+
156
+ const resolvedPreviewType = $derived.by<ImageUploadPreviewType>(() => {
157
+ if (previewType) return previewType;
158
+ if (!value) return "image";
159
+ if (value === currentObjectUrl && currentObjectType) return currentObjectType;
160
+ return isVideoUrl(value) ? "video" : "image";
161
+ });
162
+
163
+ const actionsInStrip = $derived(
164
+ (actionsPlacement ?? (resolvedPreviewType === "video" ? "strip" : "overlay")) ===
165
+ "strip",
166
+ );
167
+
168
+ function announce(message: string) {
169
+ // An unchanged live region is not read again; a trailing no-break
170
+ // space makes a repeat a change without altering what is heard.
171
+ statusMessage = message === statusMessage ? `${message}\u00a0` : message;
172
+ }
173
+
174
+ /**
175
+ * The dropzone and the actions are different nodes, so filling or clearing
176
+ * the value unmounts whichever had focus and the browser drops focus on
177
+ * `<body>`. Hand it to the control that now carries the id, but only when
178
+ * focus was in here or the swap came from one of this component's own
179
+ * controls, and never when it has since settled somewhere else.
180
+ */
181
+ $effect.pre(() => {
182
+ const current = value;
183
+ untrack(() => {
184
+ if (previousValue === undefined) {
185
+ previousValue = current ?? null;
186
+ return;
187
+ }
188
+ if ((current ?? null) === previousValue) return;
189
+
190
+ const swapped = Boolean(current) !== Boolean(previousValue);
191
+ previousValue = current ?? null;
192
+ announce(current ? selectedText : removedText);
193
+ if (!swapped) return;
194
+
195
+ const hadFocus = Boolean(host?.contains(document.activeElement));
196
+ const armed = focusReturnArmed;
197
+ focusReturnArmed = false;
198
+ if (!hadFocus && !armed) return;
199
+
200
+ void tick().then(() => {
201
+ const active = document.activeElement;
202
+ if (active && active !== document.body) return;
203
+ document.getElementById(controlId)?.focus();
204
+ });
205
+ });
206
+ });
207
+
208
+ const changeName = $derived(
209
+ changeLabel ?? (label ? `${changeText} ${label}` : undefined),
210
+ );
211
+ const removeName = $derived(
212
+ removeLabel ?? (label ? `${removeText} ${label}` : undefined),
213
+ );
48
214
 
49
215
  function revokeCurrentObjectUrl() {
50
216
  if (currentObjectUrl && typeof URL !== "undefined" && URL.revokeObjectURL) {
51
217
  URL.revokeObjectURL(currentObjectUrl);
52
218
  currentObjectUrl = null;
219
+ currentObjectType = null;
53
220
  }
54
221
  }
55
222
 
56
- function handleFileSelect(event: Event) {
57
- if (disabled) return;
58
-
59
- const input = event.target as HTMLInputElement;
60
- if (!input.files || input.files.length === 0) return;
61
-
62
- const file = input.files[0];
223
+ /** Shared by the file chooser and drag and drop. */
224
+ function selectFile(file: File): string | null {
63
225
  let url: string | null = null;
64
226
 
65
227
  if (typeof URL !== "undefined" && URL.createObjectURL) {
@@ -67,8 +229,24 @@
67
229
  url = URL.createObjectURL(file);
68
230
  value = url;
69
231
  currentObjectUrl = url;
232
+ currentObjectType =
233
+ file.type.startsWith("video/") || (!file.type && isVideoPath(file.name))
234
+ ? "video"
235
+ : "image";
70
236
  }
71
237
 
238
+ return url;
239
+ }
240
+
241
+ function handleFileSelect(event: Event) {
242
+ if (disabled) return;
243
+
244
+ const input = event.target as HTMLInputElement;
245
+ if (!input.files || input.files.length === 0) return;
246
+
247
+ const file = input.files[0];
248
+ const url = selectFile(file);
249
+
72
250
  onchange?.(event);
73
251
  onfileselect?.({ file, url });
74
252
  }
@@ -76,6 +254,7 @@
76
254
  function removeImage() {
77
255
  if (disabled) return;
78
256
 
257
+ focusReturnArmed = true;
79
258
  revokeCurrentObjectUrl();
80
259
  value = null;
81
260
  if (fileInput) fileInput.value = "";
@@ -85,14 +264,72 @@
85
264
  function triggerFileSelect(event: Event) {
86
265
  if (disabled) return;
87
266
  onclick?.(event);
267
+ if (event.defaultPrevented) return;
268
+ focusReturnArmed = true;
269
+ if (onbrowse) {
270
+ onbrowse(event);
271
+ return;
272
+ }
88
273
  fileInput?.click();
89
274
  }
90
275
 
91
- function handleDropZoneKeydown(event: KeyboardEvent) {
92
- if (event.key === "Enter" || event.key === " ") {
93
- event.preventDefault();
94
- triggerFileSelect(event);
276
+ /** Same rules the browser applies to the `accept` attribute. */
277
+ function matchesAccept(file: File) {
278
+ const tokens = accept
279
+ .split(",")
280
+ .map((token) => token.trim().toLowerCase())
281
+ .filter(Boolean);
282
+ if (tokens.length === 0) return true;
283
+
284
+ const type = file.type.toLowerCase();
285
+ const name = file.name.toLowerCase();
286
+ return tokens.some((token) => {
287
+ if (token.startsWith(".")) return name.endsWith(token);
288
+ if (token.endsWith("/*")) return type.startsWith(token.slice(0, -1));
289
+ return type === token;
290
+ });
291
+ }
292
+
293
+ function carriesFiles(event: DragEvent) {
294
+ return Array.from(event.dataTransfer?.types ?? []).includes("Files");
295
+ }
296
+
297
+ // A disabled upload still cancels a file drag: left alone, the browser
298
+ // opens the dropped file in the tab and the surrounding form is lost.
299
+ function handleDragEnter(event: DragEvent) {
300
+ if (!carriesFiles(event)) return;
301
+ event.preventDefault();
302
+ if (!disabled) dragDepth += 1;
303
+ }
304
+
305
+ function handleDragOver(event: DragEvent) {
306
+ if (!carriesFiles(event)) return;
307
+ // Without this the browser refuses the drop and opens the file instead.
308
+ event.preventDefault();
309
+ if (event.dataTransfer) event.dataTransfer.dropEffect = disabled ? "none" : "copy";
310
+ }
311
+
312
+ function handleDragLeave() {
313
+ if (dragDepth > 0) dragDepth -= 1;
314
+ }
315
+
316
+ function handleDrop(event: DragEvent) {
317
+ dragDepth = 0;
318
+ if (!carriesFiles(event)) return;
319
+ event.preventDefault();
320
+ if (disabled) return;
321
+
322
+ const file = event.dataTransfer?.files?.[0];
323
+ if (!file) return;
324
+ if (!matchesAccept(file)) {
325
+ onfilereject?.(file);
326
+ return;
95
327
  }
328
+
329
+ const url = selectFile(file);
330
+ // The input did not take part; clear it so the same file can be picked again.
331
+ if (fileInput) fileInput.value = "";
332
+ onfileselect?.({ file, url });
96
333
  }
97
334
 
98
335
  onDestroy(() => {
@@ -100,63 +337,132 @@
100
337
  });
101
338
  </script>
102
339
 
103
- <div class={cn("space-y-3", className)} {...restProps}>
104
- {#if value}
105
- <div class="relative group">
106
- <img
107
- src={value}
108
- alt=""
109
- class="w-full h-32 min-w-64 object-cover rounded-container border-0"
110
- />
340
+ <div bind:this={host} class={cn("space-y-3", className)} {...restProps}>
341
+ {#if label}
342
+ <label
343
+ id={labelId}
344
+ for={controlId}
345
+ class={cn("block text-sm font-medium text-label", disabled && "opacity-60")}
346
+ >
347
+ {label}
348
+ </label>
349
+ {/if}
350
+
351
+ <!-- Wraps both states so a drop is handled the same over either. -->
352
+ <div
353
+ role="presentation"
354
+ class={disabled ? "cursor-not-allowed" : undefined}
355
+ ondragenter={handleDragEnter}
356
+ ondragover={handleDragOver}
357
+ ondragleave={handleDragLeave}
358
+ ondrop={handleDrop}
359
+ data-drag-over={isDragOver ? "" : undefined}
360
+ >
361
+ {#if value}
111
362
  <div
112
- class="absolute inset-0 border border-border-overlay bg-surface-overlay/60 backdrop-blur-md min-w-64 opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity rounded-container flex items-center justify-center"
363
+ class="relative group"
364
+ role={label ? "group" : undefined}
365
+ aria-labelledby={label ? labelId : undefined}
113
366
  >
114
- <div class="flex gap-2">
115
- <Button
116
- variant="secondary"
117
- size="sm"
118
- onclick={triggerFileSelect}
119
- {disabled}
120
- >
121
- Change
122
- </Button>
123
- <Button
124
- variant="danger"
125
- size="sm"
126
- onclick={removeImage}
127
- {disabled}
128
- >
129
- Remove
130
- </Button>
131
- </div>
132
- </div>
133
- </div>
134
- {:else}
135
- <div
136
- class="border-2 border-dashed border-input-border rounded-container min-w-64 p-6 text-center hover:border-action-primary transition-colors {disabled
137
- ? 'cursor-not-allowed opacity-50'
138
- : 'cursor-pointer'}"
139
- onclick={triggerFileSelect}
140
- role="button"
141
- tabindex={disabled ? -1 : 0}
142
- onkeydown={handleDropZoneKeydown}
143
- aria-disabled={disabled}
144
- >
145
- <div class="space-y-3">
367
+ {#if preview}
368
+ {@render preview({ url: value, type: resolvedPreviewType, alt })}
369
+ {:else if resolvedPreviewType === "video"}
370
+ <!-- Never autoplays, so there is no motion to reduce; muted
371
+ until the viewer unmutes it from the native controls. -->
372
+ <video
373
+ src={value}
374
+ controls
375
+ muted
376
+ playsinline
377
+ preload="metadata"
378
+ aria-label={alt || undefined}
379
+ class="w-full h-32 object-cover rounded-container border-0"
380
+ ></video>
381
+ {:else}
382
+ <img
383
+ src={value}
384
+ {alt}
385
+ class="w-full h-32 object-cover rounded-container border-0"
386
+ />
387
+ {/if}
388
+ <!-- Hover cannot reveal the actions on a touch screen, so there
389
+ they stay visible as a strip that leaves the preview in
390
+ view. The layer itself never takes the pointer: only the
391
+ plate under the buttons does, so a video beneath stays
392
+ operable. The plate is opaque so the labels keep their
393
+ contrast whatever the image behind them. -->
146
394
  <div
147
- class="w-12 h-12 mx-auto bg-action-secondary rounded-control flex items-center justify-center"
395
+ class={cn(
396
+ "absolute inset-0 p-1 border border-border-overlay bg-surface-overlay/60 backdrop-blur-md opacity-0 group-hover:opacity-100 group-has-[:focus-visible]:opacity-100 transition-opacity motion-reduce:transition-none rounded-container flex items-center justify-center pointer-events-none",
397
+ "pointer-coarse:opacity-100 [@media(hover:none)]:opacity-100",
398
+ actionsInStrip
399
+ ? "bottom-auto"
400
+ : "pointer-coarse:bottom-auto [@media(hover:none)]:bottom-auto",
401
+ isDragOver && "opacity-100 border-action-primary",
402
+ )}
403
+ data-testid="image-upload-actions"
148
404
  >
149
- <Image size={24} class="text-description" />
150
- </div>
151
- <div>
152
- <p class="font-medium text-headline">{placeholder}</p>
153
- <p class="text-sm text-description">
154
- Click to choose a file
155
- </p>
405
+ <div
406
+ class="pointer-events-auto flex min-w-0 flex-wrap justify-center gap-2 rounded-control bg-surface-overlay p-1"
407
+ >
408
+ <Button
409
+ id={controlId}
410
+ variant="secondary"
411
+ size="sm"
412
+ class="min-w-0"
413
+ onclick={triggerFileSelect}
414
+ aria-label={changeName}
415
+ aria-describedby={errorMessage ? errorId : undefined}
416
+ {disabled}
417
+ >
418
+ <span class="truncate">{changeText}</span>
419
+ </Button>
420
+ <Button
421
+ variant="danger"
422
+ size="sm"
423
+ class="min-w-0"
424
+ onclick={removeImage}
425
+ aria-label={removeName}
426
+ {disabled}
427
+ >
428
+ <span class="truncate">{removeText}</span>
429
+ </Button>
430
+ </div>
156
431
  </div>
157
432
  </div>
158
- </div>
159
- {/if}
433
+ {:else}
434
+ <button
435
+ type="button"
436
+ id={controlId}
437
+ class={cn(
438
+ "focus-ring block w-full border-2 border-dashed border-input-border rounded-container p-6 text-center enabled:hover:border-action-primary enabled:active:bg-surface-active transition-colors",
439
+ // Disabled form controls can swallow pointer events; let a
440
+ // drag land on the wrapper, which cancels it.
441
+ disabled ? "pointer-events-none opacity-50" : "cursor-pointer",
442
+ isDragOver && "border-action-primary bg-action-primary-subtle",
443
+ )}
444
+ onclick={triggerFileSelect}
445
+ {disabled}
446
+ aria-describedby={[label ? hintId : "", errorMessage ? errorId : ""]
447
+ .filter(Boolean)
448
+ .join(" ") || undefined}
449
+ >
450
+ <span class="block space-y-3">
451
+ <span
452
+ class="w-12 h-12 mx-auto bg-action-secondary rounded-control flex items-center justify-center"
453
+ >
454
+ <Image size={24} class="text-description" aria-hidden="true" />
455
+ </span>
456
+ <span id={hintId} class="block">
457
+ <span class="block font-medium text-headline">{placeholder}</span>
458
+ <span class="block text-sm text-description">
459
+ {browseText}
460
+ </span>
461
+ </span>
462
+ </span>
463
+ </button>
464
+ {/if}
465
+ </div>
160
466
 
161
467
  <input
162
468
  bind:this={fileInput}
@@ -168,16 +474,23 @@
168
474
  data-testid="image-upload-input"
169
475
  />
170
476
 
477
+ <div class="sr-only" role="status" aria-live="polite" aria-atomic="true">
478
+ {statusMessage}
479
+ </div>
480
+
171
481
  {#if errorMessage}
172
482
  <!-- Same tinted fill + tinted border Alert uses, rather than a bare
173
483
  step-600 outline on a transparent ground. -->
174
484
  <div
485
+ id={errorId}
175
486
  class="rounded-container border bg-error-subtle border-error-border text-error-text px-3 py-2 text-sm"
176
487
  role="alert"
177
488
  >
178
- <p class="font-medium">Image upload failed</p>
489
+ <p class="font-medium">{errorTitle}</p>
179
490
  <p>{errorMessage}</p>
180
- <p class="mt-1">Recovery action: try another file or retry upload.</p>
491
+ {#if errorRecovery}
492
+ <p class="mt-1">{errorRecovery}</p>
493
+ {/if}
181
494
  </div>
182
495
  {/if}
183
496
  </div>
@@ -1,3 +1,4 @@
1
+ import { type Snippet } from "svelte";
1
2
  import type { HTMLAttributes } from "svelte/elements";
2
3
  export type ImageUploadFileDetail = {
3
4
  /** Selected file, or `null` when the image is removed. */
@@ -5,21 +6,65 @@ export type ImageUploadFileDetail = {
5
6
  /** Object URL used for the preview, or `null`. */
6
7
  url: string | null;
7
8
  };
8
- type Props = Omit<HTMLAttributes<HTMLDivElement>, "onchange" | "onclick"> & {
9
+ export type ImageUploadPreviewType = "image" | "video";
10
+ export type ImageUploadActionsPlacement = "overlay" | "strip";
11
+ export type ImageUploadPreviewDetail = {
12
+ /** The current `value`. */
13
+ url: string;
14
+ /** What the built-in preview would have rendered. */
15
+ type: ImageUploadPreviewType;
16
+ alt: string;
17
+ };
18
+ type Props = Omit<HTMLAttributes<HTMLDivElement>, "onchange" | "onclick" | "id"> & {
9
19
  /** Extra classes for the host element. */
10
20
  class?: string;
21
+ /** Id of the control the label points at; generated when omitted. */
22
+ id?: string;
23
+ /** Visible label, associated with the dropzone. */
24
+ label?: string;
11
25
  /** Preview URL; `bind:value` to read it. */
12
26
  value?: string | null;
13
27
  disabled?: boolean;
14
28
  accept?: string;
15
29
  placeholder?: string;
30
+ /** Second line of the dropzone. */
31
+ browseText?: string;
32
+ changeText?: string;
33
+ removeText?: string;
34
+ /** Accessible name of the Change action; defaults to `changeText` plus `label`. */
35
+ changeLabel?: string;
36
+ /** Accessible name of the Remove action; defaults to `removeText` plus `label`. */
37
+ removeLabel?: string;
38
+ /** Text alternative for the preview. */
39
+ alt?: string;
40
+ /** Forces the preview kind when the URL has no extension to go by. */
41
+ previewType?: ImageUploadPreviewType;
42
+ /** Replaces the built-in `<img>` / `<video>` preview. */
43
+ preview?: Snippet<[ImageUploadPreviewDetail]>;
44
+ /**
45
+ * Where Change and Remove sit: over the whole preview, or in a strip
46
+ * along its top edge. Defaults to the strip for a video, whose controls
47
+ * an overlay would cover, and to the overlay otherwise.
48
+ */
49
+ actionsPlacement?: ImageUploadActionsPlacement;
50
+ /** Announced to screen readers when a file or URL is set. */
51
+ selectedText?: string;
52
+ /** Announced to screen readers when the value is cleared. */
53
+ removedText?: string;
16
54
  errorMessage?: string;
55
+ errorTitle?: string;
56
+ /** Closing line of the error block; `false` or `""` leaves it out. */
57
+ errorRecovery?: string | false;
17
58
  /** Native `change` event from the hidden file input. */
18
59
  onchange?: (event: Event) => void;
19
- /** Fires when the file chooser is opened. */
60
+ /** Fires before the file chooser opens; `event.preventDefault()` keeps it closed. */
20
61
  onclick?: (event: Event) => void;
62
+ /** Replaces the native file chooser, for a media library or another source. */
63
+ onbrowse?: (event: Event) => void;
21
64
  /** Selected file and preview URL; `{ file: null, url: null }` on remove. */
22
65
  onfileselect?: (detail: ImageUploadFileDetail) => void;
66
+ /** A dropped file that `accept` does not allow. */
67
+ onfilereject?: (file: File) => void;
23
68
  };
24
69
  declare const ImageUpload: import("svelte").Component<Props, {}, "value">;
25
70
  type ImageUpload = ReturnType<typeof ImageUpload>;