zabi-components 7.0.2 → 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 (433) hide show
  1. package/CHANGELOG.md +1924 -0
  2. package/README.md +287 -11
  3. package/THEME.md +149 -332
  4. package/THEMING.md +1329 -163
  5. package/dist/atoms/ActionPanel.svelte +24 -9
  6. package/dist/atoms/ActionPanel.svelte.d.ts +2 -0
  7. package/dist/atoms/Avatar.svelte +104 -0
  8. package/dist/atoms/Avatar.svelte.d.ts +34 -0
  9. package/dist/atoms/Badge.svelte +124 -58
  10. package/dist/atoms/Badge.svelte.d.ts +17 -5
  11. package/dist/atoms/Button.svelte +212 -62
  12. package/dist/atoms/Button.svelte.d.ts +33 -2
  13. package/dist/atoms/Card.svelte +31 -28
  14. package/dist/atoms/Card.svelte.d.ts +5 -2
  15. package/dist/atoms/CardContent.svelte +10 -2
  16. package/dist/atoms/CardContent.svelte.d.ts +2 -0
  17. package/dist/atoms/CardFooter.svelte +10 -2
  18. package/dist/atoms/CardFooter.svelte.d.ts +2 -0
  19. package/dist/atoms/CardHeader.svelte +26 -6
  20. package/dist/atoms/CardHeader.svelte.d.ts +2 -0
  21. package/dist/atoms/Checkbox.svelte +52 -21
  22. package/dist/atoms/Checkbox.svelte.d.ts +6 -2
  23. package/dist/atoms/CodeBlock.svelte +35 -9
  24. package/dist/atoms/CodeBlock.svelte.d.ts +13 -3
  25. package/dist/atoms/ColorPicker.svelte +272 -52
  26. package/dist/atoms/ColorPicker.svelte.d.ts +6 -1
  27. package/dist/atoms/Container.svelte +17 -10
  28. package/dist/atoms/DateField.svelte +107 -0
  29. package/dist/atoms/DateField.svelte.d.ts +53 -0
  30. package/dist/atoms/Divider.svelte +4 -4
  31. package/dist/atoms/FeatureCard.svelte +4 -3
  32. package/dist/atoms/FieldMessages.svelte +69 -0
  33. package/dist/atoms/FieldMessages.svelte.d.ts +23 -0
  34. package/dist/atoms/FloatingActionButton.svelte +174 -0
  35. package/dist/atoms/FloatingActionButton.svelte.d.ts +51 -0
  36. package/dist/atoms/Heading.svelte +93 -32
  37. package/dist/atoms/Heading.svelte.d.ts +24 -4
  38. package/dist/atoms/IconButton.svelte +242 -53
  39. package/dist/atoms/IconButton.svelte.d.ts +49 -4
  40. package/dist/atoms/Input.svelte +267 -74
  41. package/dist/atoms/Input.svelte.d.ts +32 -2
  42. package/dist/atoms/List.svelte +13 -2
  43. package/dist/atoms/List.svelte.d.ts +2 -0
  44. package/dist/atoms/ListItem.svelte +44 -11
  45. package/dist/atoms/ListItem.svelte.d.ts +5 -0
  46. package/dist/atoms/ListItemLeading.svelte +4 -2
  47. package/dist/atoms/ListItemLeading.svelte.d.ts +1 -1
  48. package/dist/atoms/OptimizedImage.svelte +11 -2
  49. package/dist/atoms/OptimizedImage.svelte.d.ts +2 -0
  50. package/dist/atoms/Progress.svelte +31 -9
  51. package/dist/atoms/Progress.svelte.d.ts +6 -0
  52. package/dist/atoms/Radio.svelte +23 -3
  53. package/dist/atoms/Radio.svelte.d.ts +6 -2
  54. package/dist/atoms/Rating.svelte +494 -0
  55. package/dist/atoms/Rating.svelte.d.ts +56 -0
  56. package/dist/atoms/Select.svelte +475 -135
  57. package/dist/atoms/Select.svelte.d.ts +52 -2
  58. package/dist/atoms/SelectionControl.svelte +99 -18
  59. package/dist/atoms/SelectionControl.svelte.d.ts +6 -3
  60. package/dist/atoms/Skeleton.svelte +5 -3
  61. package/dist/atoms/Slider.svelte +309 -0
  62. package/dist/atoms/Slider.svelte.d.ts +40 -0
  63. package/dist/atoms/Spinner.svelte +57 -0
  64. package/dist/atoms/Spinner.svelte.d.ts +19 -0
  65. package/dist/atoms/Table.svelte +152 -6
  66. package/dist/atoms/Table.svelte.d.ts +15 -3
  67. package/dist/atoms/TemporalField.svelte +140 -0
  68. package/dist/atoms/TemporalField.svelte.d.ts +32 -0
  69. package/dist/atoms/Text.svelte +56 -15
  70. package/dist/atoms/Text.svelte.d.ts +13 -4
  71. package/dist/atoms/Textarea.svelte +57 -45
  72. package/dist/atoms/Textarea.svelte.d.ts +14 -2
  73. package/dist/atoms/ThemeToggle.svelte +238 -56
  74. package/dist/atoms/ThemeToggle.svelte.d.ts +28 -3
  75. package/dist/atoms/TimeField.svelte +108 -0
  76. package/dist/atoms/TimeField.svelte.d.ts +54 -0
  77. package/dist/atoms/Toast.svelte +25 -10
  78. package/dist/atoms/Toast.svelte.d.ts +2 -0
  79. package/dist/atoms/Toggle.svelte +60 -19
  80. package/dist/atoms/Toggle.svelte.d.ts +18 -2
  81. package/dist/atoms/Tooltip.svelte +583 -40
  82. package/dist/atoms/Tooltip.svelte.d.ts +26 -0
  83. package/dist/atoms/ZabiStringsProvider.svelte +45 -0
  84. package/dist/atoms/ZabiStringsProvider.svelte.d.ts +27 -0
  85. package/dist/atoms/index.d.ts +17 -0
  86. package/dist/atoms/index.js +12 -0
  87. package/dist/atoms/selection-control.styles.d.ts +36 -5
  88. package/dist/atoms/selection-control.styles.js +40 -9
  89. package/dist/components/atoms/index.d.ts +17 -0
  90. package/dist/components/atoms/index.d.ts.map +1 -1
  91. package/dist/components/atoms/selection-control.styles.d.ts +36 -5
  92. package/dist/components/atoms/selection-control.styles.d.ts.map +1 -1
  93. package/dist/components/index.d.ts +83 -3
  94. package/dist/components/index.d.ts.map +1 -1
  95. package/dist/components/molecules/index.d.ts +46 -0
  96. package/dist/components/molecules/index.d.ts.map +1 -1
  97. package/dist/components/molecules/toast-store.d.ts +53 -7
  98. package/dist/components/molecules/toast-store.d.ts.map +1 -1
  99. package/dist/components/organisms/index.d.ts +6 -0
  100. package/dist/components/organisms/index.d.ts.map +1 -1
  101. package/dist/components/types/variants.d.ts +15 -1
  102. package/dist/components/types/variants.d.ts.map +1 -1
  103. package/dist/components/util/app-shell.d.ts +28 -0
  104. package/dist/components/util/app-shell.d.ts.map +1 -0
  105. package/dist/components/util/avatar.d.ts +36 -0
  106. package/dist/components/util/avatar.d.ts.map +1 -0
  107. package/dist/components/util/bottom-sheet.d.ts +30 -0
  108. package/dist/components/util/bottom-sheet.d.ts.map +1 -0
  109. package/dist/components/util/bottom-tab-bar.d.ts +35 -0
  110. package/dist/components/util/bottom-tab-bar.d.ts.map +1 -0
  111. package/dist/components/util/calendar.d.ts +56 -0
  112. package/dist/components/util/calendar.d.ts.map +1 -0
  113. package/dist/components/util/cn.d.ts +8 -0
  114. package/dist/components/util/cn.d.ts.map +1 -0
  115. package/dist/components/util/collapsible.d.ts +58 -0
  116. package/dist/components/util/collapsible.d.ts.map +1 -0
  117. package/dist/components/util/confirm-dialog.d.ts +12 -0
  118. package/dist/components/util/confirm-dialog.d.ts.map +1 -0
  119. package/dist/components/util/date.d.ts +96 -0
  120. package/dist/components/util/date.d.ts.map +1 -0
  121. package/dist/components/util/drawer.d.ts +13 -0
  122. package/dist/components/util/drawer.d.ts.map +1 -0
  123. package/dist/components/util/dropdown.d.ts +47 -0
  124. package/dist/components/util/dropdown.d.ts.map +1 -0
  125. package/dist/components/util/field.d.ts +39 -0
  126. package/dist/components/util/field.d.ts.map +1 -0
  127. package/dist/components/util/fit-in-viewport.d.ts +201 -0
  128. package/dist/components/util/fit-in-viewport.d.ts.map +1 -0
  129. package/dist/components/util/focus-utils.d.ts +75 -2
  130. package/dist/components/util/focus-utils.d.ts.map +1 -1
  131. package/dist/components/util/hydration.d.ts +45 -0
  132. package/dist/components/util/hydration.d.ts.map +1 -0
  133. package/dist/components/util/keyboard-inset.d.ts +18 -0
  134. package/dist/components/util/keyboard-inset.d.ts.map +1 -0
  135. package/dist/components/util/media-grid.d.ts +51 -0
  136. package/dist/components/util/media-grid.d.ts.map +1 -0
  137. package/dist/components/util/media.d.ts +6 -0
  138. package/dist/components/util/media.d.ts.map +1 -0
  139. package/dist/components/util/overlay-chrome.d.ts +53 -0
  140. package/dist/components/util/overlay-chrome.d.ts.map +1 -0
  141. package/dist/components/util/overlay.d.ts +72 -0
  142. package/dist/components/util/overlay.d.ts.map +1 -0
  143. package/dist/components/util/photo.d.ts +215 -0
  144. package/dist/components/util/photo.d.ts.map +1 -0
  145. package/dist/components/util/portal.d.ts +23 -0
  146. package/dist/components/util/portal.d.ts.map +1 -0
  147. package/dist/components/util/pull-to-refresh.d.ts +27 -0
  148. package/dist/components/util/pull-to-refresh.d.ts.map +1 -0
  149. package/dist/components/util/radio-keys.d.ts +25 -0
  150. package/dist/components/util/radio-keys.d.ts.map +1 -0
  151. package/dist/components/util/rating.d.ts +23 -0
  152. package/dist/components/util/rating.d.ts.map +1 -0
  153. package/dist/components/util/ready-made-strings.d.ts +77 -0
  154. package/dist/components/util/ready-made-strings.d.ts.map +1 -0
  155. package/dist/components/util/scroll-padding.d.ts +20 -0
  156. package/dist/components/util/scroll-padding.d.ts.map +1 -0
  157. package/dist/components/util/segmented-control.d.ts +16 -0
  158. package/dist/components/util/segmented-control.d.ts.map +1 -0
  159. package/dist/components/util/select.d.ts +41 -0
  160. package/dist/components/util/select.d.ts.map +1 -0
  161. package/dist/components/util/sheet-drag.d.ts +60 -0
  162. package/dist/components/util/sheet-drag.d.ts.map +1 -0
  163. package/dist/components/util/sidebar.d.ts +63 -0
  164. package/dist/components/util/sidebar.d.ts.map +1 -0
  165. package/dist/components/util/sortable-list.d.ts +65 -0
  166. package/dist/components/util/sortable-list.d.ts.map +1 -0
  167. package/dist/components/util/ssr-safe.d.ts +4 -4
  168. package/dist/components/util/ssr-safe.d.ts.map +1 -1
  169. package/dist/components/util/stepper.d.ts +40 -0
  170. package/dist/components/util/stepper.d.ts.map +1 -0
  171. package/dist/components/util/swipeable-list-item.d.ts +36 -0
  172. package/dist/components/util/swipeable-list-item.d.ts.map +1 -0
  173. package/dist/components/util/theme-mode.d.ts +80 -0
  174. package/dist/components/util/theme-mode.d.ts.map +1 -0
  175. package/dist/components/util/toaster.d.ts +93 -0
  176. package/dist/components/util/toaster.d.ts.map +1 -0
  177. package/dist/components/util/tooltip.d.ts +10 -0
  178. package/dist/components/util/tooltip.d.ts.map +1 -0
  179. package/dist/components/util/top-navbar.d.ts +11 -0
  180. package/dist/components/util/top-navbar.d.ts.map +1 -0
  181. package/dist/components/util/touch-target.d.ts +31 -0
  182. package/dist/components/util/touch-target.d.ts.map +1 -0
  183. package/dist/components/util/zabi-strings.d.ts +133 -0
  184. package/dist/components/util/zabi-strings.d.ts.map +1 -0
  185. package/dist/create-theme/cli.js +169 -0
  186. package/dist/create-theme/index.d.ts +176 -0
  187. package/dist/create-theme/index.js +817 -0
  188. package/dist/create-theme/lib/ramp-math.js +128 -0
  189. package/dist/create-theme/lib/resolve.js +141 -0
  190. package/dist/create-theme/theme-data.js +1116 -0
  191. package/dist/index.d.ts +83 -3
  192. package/dist/index.js +60 -8
  193. package/dist/molecules/Alert.svelte +73 -66
  194. package/dist/molecules/Alert.svelte.d.ts +9 -1
  195. package/dist/molecules/AppBar.svelte +415 -0
  196. package/dist/molecules/AppBar.svelte.d.ts +69 -0
  197. package/dist/molecules/AvatarGroup.svelte +116 -0
  198. package/dist/molecules/AvatarGroup.svelte.d.ts +40 -0
  199. package/dist/molecules/BottomSheet.svelte +540 -0
  200. package/dist/molecules/BottomSheet.svelte.d.ts +93 -0
  201. package/dist/molecules/BottomTabBar.svelte +297 -0
  202. package/dist/molecules/BottomTabBar.svelte.d.ts +64 -0
  203. package/dist/molecules/Calendar.svelte +597 -0
  204. package/dist/molecules/Calendar.svelte.d.ts +72 -0
  205. package/dist/molecules/Collapsible.svelte +243 -0
  206. package/dist/molecules/Collapsible.svelte.d.ts +44 -0
  207. package/dist/molecules/CollapsibleGroup.svelte +126 -0
  208. package/dist/molecules/CollapsibleGroup.svelte.d.ts +14 -0
  209. package/dist/molecules/ComponentDemo.svelte +25 -5
  210. package/dist/molecules/ComponentDemo.svelte.d.ts +5 -0
  211. package/dist/molecules/ConfirmDialog.svelte +253 -0
  212. package/dist/molecules/ConfirmDialog.svelte.d.ts +55 -0
  213. package/dist/molecules/ContactForm.svelte +46 -19
  214. package/dist/molecules/ContactForm.svelte.d.ts +10 -0
  215. package/dist/molecules/Drawer.svelte +394 -0
  216. package/dist/molecules/Drawer.svelte.d.ts +55 -0
  217. package/dist/molecules/Dropdown.svelte +540 -50
  218. package/dist/molecules/Dropdown.svelte.d.ts +49 -6
  219. package/dist/molecules/DropdownItem.svelte +122 -0
  220. package/dist/molecules/DropdownItem.svelte.d.ts +28 -0
  221. package/dist/molecules/EmptyState.svelte +56 -10
  222. package/dist/molecules/EmptyState.svelte.d.ts +8 -0
  223. package/dist/molecules/Form.svelte +25 -14
  224. package/dist/molecules/Form.svelte.d.ts +13 -5
  225. package/dist/molecules/FormField.svelte +11 -2
  226. package/dist/molecules/FormField.svelte.d.ts +2 -0
  227. package/dist/molecules/Header.svelte +13 -3
  228. package/dist/molecules/Header.svelte.d.ts +2 -0
  229. package/dist/molecules/ImageUpload.svelte +422 -70
  230. package/dist/molecules/ImageUpload.svelte.d.ts +61 -5
  231. package/dist/molecules/MediaGrid.svelte +527 -0
  232. package/dist/molecules/MediaGrid.svelte.d.ts +77 -0
  233. package/dist/molecules/Modal.svelte +340 -45
  234. package/dist/molecules/Modal.svelte.d.ts +59 -7
  235. package/dist/molecules/NavigationMenu.svelte +15 -3
  236. package/dist/molecules/NavigationMenu.svelte.d.ts +4 -0
  237. package/dist/molecules/NavigationMenuContent.svelte +91 -36
  238. package/dist/molecules/NavigationMenuContent.svelte.d.ts +2 -0
  239. package/dist/molecules/NavigationMenuItem.svelte +10 -3
  240. package/dist/molecules/NavigationMenuItem.svelte.d.ts +2 -0
  241. package/dist/molecules/NavigationMenuLink.svelte +10 -2
  242. package/dist/molecules/NavigationMenuLink.svelte.d.ts +2 -0
  243. package/dist/molecules/NavigationMenuList.svelte +19 -4
  244. package/dist/molecules/NavigationMenuList.svelte.d.ts +2 -0
  245. package/dist/molecules/NavigationMenuTrigger.svelte +16 -3
  246. package/dist/molecules/NavigationMenuTrigger.svelte.d.ts +2 -0
  247. package/dist/molecules/Page.svelte +38 -2
  248. package/dist/molecules/Page.svelte.d.ts +13 -0
  249. package/dist/molecules/PhotoGrid.svelte +433 -0
  250. package/dist/molecules/PhotoGrid.svelte.d.ts +62 -0
  251. package/dist/molecules/PhotoViewer.svelte +1116 -0
  252. package/dist/molecules/PhotoViewer.svelte.d.ts +56 -0
  253. package/dist/molecules/PropsTable.svelte +26 -11
  254. package/dist/molecules/PropsTable.svelte.d.ts +5 -0
  255. package/dist/molecules/PullToRefresh.svelte +263 -0
  256. package/dist/molecules/PullToRefresh.svelte.d.ts +52 -0
  257. package/dist/molecules/RadioGroup.svelte +57 -15
  258. package/dist/molecules/RadioGroup.svelte.d.ts +16 -3
  259. package/dist/molecules/Section.svelte +13 -5
  260. package/dist/molecules/Section.svelte.d.ts +3 -1
  261. package/dist/molecules/SegmentedControl.svelte +323 -0
  262. package/dist/molecules/SegmentedControl.svelte.d.ts +34 -0
  263. package/dist/molecules/SidebarBrandHeader.svelte +24 -5
  264. package/dist/molecules/SidebarBrandHeader.svelte.d.ts +5 -0
  265. package/dist/molecules/SidebarFooter.svelte +39 -8
  266. package/dist/molecules/SidebarFooter.svelte.d.ts +5 -0
  267. package/dist/molecules/SidebarNavSection.svelte +12 -4
  268. package/dist/molecules/SidebarNavSection.svelte.d.ts +2 -0
  269. package/dist/molecules/SlideUp.svelte +253 -30
  270. package/dist/molecules/SlideUp.svelte.d.ts +31 -4
  271. package/dist/molecules/SortableList.svelte +632 -0
  272. package/dist/molecules/SortableList.svelte.d.ts +69 -0
  273. package/dist/molecules/Stepper.svelte +548 -0
  274. package/dist/molecules/Stepper.svelte.d.ts +39 -0
  275. package/dist/molecules/StickyActionBar.svelte +191 -0
  276. package/dist/molecules/StickyActionBar.svelte.d.ts +65 -0
  277. package/dist/molecules/SwipeableListItem.svelte +334 -0
  278. package/dist/molecules/SwipeableListItem.svelte.d.ts +54 -0
  279. package/dist/molecules/Tabs.svelte +254 -24
  280. package/dist/molecules/Tabs.svelte.d.ts +15 -4
  281. package/dist/molecules/Toaster.svelte +352 -6
  282. package/dist/molecules/Toaster.svelte.d.ts +95 -2
  283. package/dist/molecules/ToasterToast.svelte +279 -62
  284. package/dist/molecules/ToasterToast.svelte.d.ts +11 -0
  285. package/dist/molecules/UnsavedChangesBar.svelte +212 -0
  286. package/dist/molecules/UnsavedChangesBar.svelte.d.ts +43 -0
  287. package/dist/molecules/index.d.ts +46 -0
  288. package/dist/molecules/index.js +27 -0
  289. package/dist/molecules/toast-store.d.ts +53 -7
  290. package/dist/molecules/toast-store.js +21 -0
  291. package/dist/organisms/AppShell.svelte +196 -0
  292. package/dist/organisms/AppShell.svelte.d.ts +61 -0
  293. package/dist/organisms/SidebarAccountPanel.svelte +111 -12
  294. package/dist/organisms/SidebarAccountPanel.svelte.d.ts +21 -0
  295. package/dist/organisms/SidebarNavigation.svelte +193 -69
  296. package/dist/organisms/SidebarNavigation.svelte.d.ts +31 -1
  297. package/dist/organisms/SidebarPanel.svelte +45 -16
  298. package/dist/organisms/SidebarPanel.svelte.d.ts +2 -0
  299. package/dist/organisms/SidebarShell.svelte +263 -0
  300. package/dist/organisms/SidebarShell.svelte.d.ts +106 -0
  301. package/dist/organisms/TopNavbar.svelte +329 -33
  302. package/dist/organisms/TopNavbar.svelte.d.ts +28 -0
  303. package/dist/organisms/index.d.ts +6 -0
  304. package/dist/organisms/index.js +4 -0
  305. package/dist/routes/lib/variant-utils.d.ts +26 -0
  306. package/dist/routes/lib/{variant-utils.ts → variant-utils.js} +15 -46
  307. package/dist/types/components.d.ts +4 -0
  308. package/dist/types/components.d.ts.map +1 -1
  309. package/dist/types/events.js +1 -0
  310. package/dist/types/index.d.ts +605 -25
  311. package/dist/types/index.d.ts.map +1 -1
  312. package/dist/types/index.js +3 -0
  313. package/dist/types/variants.ts +17 -1
  314. package/dist/util/app-shell.d.ts +27 -0
  315. package/dist/util/app-shell.js +72 -0
  316. package/dist/util/avatar.d.ts +35 -0
  317. package/dist/util/avatar.js +52 -0
  318. package/dist/util/bottom-sheet.d.ts +29 -0
  319. package/dist/util/bottom-sheet.js +43 -0
  320. package/dist/util/bottom-tab-bar.d.ts +34 -0
  321. package/dist/util/bottom-tab-bar.js +54 -0
  322. package/dist/util/calendar.d.ts +55 -0
  323. package/dist/util/calendar.js +74 -0
  324. package/dist/util/cn.d.ts +7 -0
  325. package/dist/util/cn.js +48 -0
  326. package/dist/util/collapsible.d.ts +57 -0
  327. package/dist/util/collapsible.js +23 -0
  328. package/dist/util/confirm-dialog.d.ts +11 -0
  329. package/dist/util/confirm-dialog.js +2 -0
  330. package/dist/util/date.d.ts +95 -0
  331. package/dist/util/date.js +237 -0
  332. package/dist/util/drawer.d.ts +12 -0
  333. package/dist/util/drawer.js +7 -0
  334. package/dist/util/dropdown.d.ts +46 -0
  335. package/dist/util/dropdown.js +21 -0
  336. package/dist/util/field.d.ts +38 -0
  337. package/dist/util/field.js +38 -0
  338. package/dist/util/fit-in-viewport.d.ts +200 -0
  339. package/dist/util/fit-in-viewport.js +399 -0
  340. package/dist/util/focus-utils.d.ts +75 -2
  341. package/dist/util/focus-utils.js +182 -9
  342. package/dist/util/hydration.d.ts +44 -0
  343. package/dist/util/hydration.js +58 -0
  344. package/dist/util/keyboard-inset.d.ts +17 -0
  345. package/dist/util/keyboard-inset.js +53 -0
  346. package/dist/util/media-grid.d.ts +50 -0
  347. package/dist/util/media-grid.js +81 -0
  348. package/dist/util/media.d.ts +5 -0
  349. package/dist/util/media.js +10 -0
  350. package/dist/util/overlay-chrome.d.ts +52 -0
  351. package/dist/util/overlay-chrome.js +56 -0
  352. package/dist/util/overlay.d.ts +71 -0
  353. package/dist/util/overlay.js +240 -0
  354. package/dist/util/photo.d.ts +214 -0
  355. package/dist/util/photo.js +206 -0
  356. package/dist/util/portal.d.ts +22 -0
  357. package/dist/util/portal.js +50 -0
  358. package/dist/util/pull-to-refresh.d.ts +26 -0
  359. package/dist/util/pull-to-refresh.js +30 -0
  360. package/dist/util/radio-keys.d.ts +24 -0
  361. package/dist/util/radio-keys.js +49 -0
  362. package/dist/util/rating.d.ts +22 -0
  363. package/dist/util/rating.js +26 -0
  364. package/dist/util/ready-made-strings.d.ts +76 -0
  365. package/dist/util/ready-made-strings.js +67 -0
  366. package/dist/util/scroll-padding.d.ts +19 -0
  367. package/dist/util/scroll-padding.js +44 -0
  368. package/dist/util/segmented-control.d.ts +15 -0
  369. package/dist/util/segmented-control.js +1 -0
  370. package/dist/util/select.d.ts +40 -0
  371. package/dist/util/select.js +63 -0
  372. package/dist/util/sheet-drag.d.ts +59 -0
  373. package/dist/util/sheet-drag.js +241 -0
  374. package/dist/util/sidebar.d.ts +62 -0
  375. package/dist/util/sidebar.js +38 -0
  376. package/dist/util/sortable-list.d.ts +64 -0
  377. package/dist/util/sortable-list.js +53 -0
  378. package/dist/util/ssr-safe.d.ts +4 -4
  379. package/dist/util/stepper.d.ts +39 -0
  380. package/dist/util/stepper.js +38 -0
  381. package/dist/util/swipeable-list-item.d.ts +35 -0
  382. package/dist/util/swipeable-list-item.js +32 -0
  383. package/dist/util/theme-mode.d.ts +79 -0
  384. package/dist/util/theme-mode.js +121 -0
  385. package/dist/util/toaster.d.ts +92 -0
  386. package/dist/util/toaster.js +75 -0
  387. package/dist/util/tooltip.d.ts +9 -0
  388. package/dist/util/tooltip.js +18 -0
  389. package/dist/util/top-navbar.d.ts +10 -0
  390. package/dist/util/top-navbar.js +5 -0
  391. package/dist/util/touch-target.d.ts +30 -0
  392. package/dist/util/touch-target.js +30 -0
  393. package/dist/util/zabi-strings.d.ts +132 -0
  394. package/dist/util/zabi-strings.js +105 -0
  395. package/dist/zabi-components-colors.css +5 -2
  396. package/dist/zabi-components-theme-dark-only.css +574 -225
  397. package/dist/zabi-components-theme-dark.css +574 -225
  398. package/dist/zabi-components-theme-only.css +1680 -317
  399. package/dist/zabi-components-theme.css +1680 -317
  400. package/dist/zabi-components.css +4473 -1158
  401. package/docs/lucide-icons.md +41 -16
  402. package/docs/theme-imports.md +81 -11
  403. package/package.json +51 -29
  404. package/dist/lib/layout-width-tokens.d.ts +0 -17
  405. package/dist/lib/layout-width-tokens.d.ts.map +0 -1
  406. package/dist/lib/showcase/component-docs/Button.d.ts +0 -3
  407. package/dist/lib/showcase/component-docs/Button.d.ts.map +0 -1
  408. package/dist/lib/showcase/component-docs/Input.d.ts +0 -3
  409. package/dist/lib/showcase/component-docs/Input.d.ts.map +0 -1
  410. package/dist/lib/showcase/component-docs/List.d.ts +0 -3
  411. package/dist/lib/showcase/component-docs/List.d.ts.map +0 -1
  412. package/dist/lib/showcase/component-docs/Modal.d.ts +0 -3
  413. package/dist/lib/showcase/component-docs/Modal.d.ts.map +0 -1
  414. package/dist/lib/showcase/component-docs/Radio.d.ts +0 -3
  415. package/dist/lib/showcase/component-docs/Radio.d.ts.map +0 -1
  416. package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts +0 -3
  417. package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts.map +0 -1
  418. package/dist/lib/showcase/component-docs/Skeleton.d.ts +0 -3
  419. package/dist/lib/showcase/component-docs/Skeleton.d.ts.map +0 -1
  420. package/dist/lib/showcase/component-docs/_shared.d.ts +0 -12
  421. package/dist/lib/showcase/component-docs/_shared.d.ts.map +0 -1
  422. package/dist/lib/showcase/component-docs/index.d.ts +0 -4
  423. package/dist/lib/showcase/component-docs/index.d.ts.map +0 -1
  424. package/dist/lib/showcase/components-catalog.d.ts +0 -3
  425. package/dist/lib/showcase/components-catalog.d.ts.map +0 -1
  426. package/dist/lib/showcase/components-showcase-constants.d.ts +0 -89
  427. package/dist/lib/showcase/components-showcase-constants.d.ts.map +0 -1
  428. package/dist/lib/showcase/docs-sidebar-helpers.d.ts +0 -15
  429. package/dist/lib/showcase/docs-sidebar-helpers.d.ts.map +0 -1
  430. package/dist/routes/lib/focus-utils.d.ts +0 -12
  431. package/dist/routes/lib/focus-utils.ts +0 -110
  432. package/dist/routes/lib/ssr-safe.ts +0 -58
  433. package/scripts/fix-lucide-svelte-icon-dts.js +0 -35
@@ -1,36 +1,241 @@
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";
4
+ import type { HTMLAttributes } from "svelte/elements";
3
5
  import Button from "../atoms/Button.svelte";
4
- import { Image } from "@lucide/svelte";
6
+ import Image from "@lucide/svelte/icons/image";
7
+ import { cn } from "../util/cn.js";
8
+ import { isVideoPath, isVideoUrl } from "../util/media.js";
9
+ import { generateId } from "../util/ssr-safe.js";
5
10
 
6
- interface Props {
11
+ export type ImageUploadFileDetail = {
12
+ /** Selected file, or `null` when the image is removed. */
13
+ file: File | null;
14
+ /** Object URL used for the preview, or `null`. */
15
+ url: string | null;
16
+ };
17
+
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"> & {
31
+ /** Extra classes for the host element. */
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;
37
+ /** Preview URL; `bind:value` to read it. */
7
38
  value?: string | null;
8
39
  disabled?: boolean;
9
40
  accept?: string;
10
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;
11
66
  errorMessage?: string;
67
+ errorTitle?: string;
68
+ /** Closing line of the error block; `false` or `""` leaves it out. */
69
+ errorRecovery?: string | false;
70
+ /** Native `change` event from the hidden file input. */
12
71
  onchange?: (event: Event) => void;
72
+ /** Fires before the file chooser opens; `event.preventDefault()` keeps it closed. */
13
73
  onclick?: (event: Event) => void;
14
- }
74
+ /** Replaces the native file chooser, for a media library or another source. */
75
+ onbrowse?: (event: Event) => void;
76
+ /** Selected file and preview URL; `{ file: null, url: null }` on remove. */
77
+ onfileselect?: (detail: ImageUploadFileDetail) => void;
78
+ /** A dropped file that `accept` does not allow. */
79
+ onfilereject?: (file: File) => void;
80
+ };
15
81
 
16
82
  let {
17
- value = null,
83
+ class: className = "",
84
+ id: idProp,
85
+ label = "",
86
+ value = $bindable<Exclude<Props["value"], undefined>>(),
18
87
  disabled = false,
19
88
  accept = "image/*",
20
- 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,
21
101
  errorMessage = "",
22
- children,
102
+ errorTitle: errorTitleGiven,
103
+ errorRecovery: errorRecoveryGiven,
104
+ onchange,
105
+ onclick,
106
+ onbrowse,
107
+ onfileselect,
108
+ onfilereject,
109
+ children: _children,
23
110
  ...restProps
24
- } = $props<Props & { children?: any }>();
111
+ }: Props = $props();
25
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>();
26
142
  let fileInput = $state<HTMLInputElement>();
27
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
+ );
28
214
 
29
215
  function revokeCurrentObjectUrl() {
30
216
  if (currentObjectUrl && typeof URL !== "undefined" && URL.revokeObjectURL) {
31
217
  URL.revokeObjectURL(currentObjectUrl);
32
218
  currentObjectUrl = null;
219
+ currentObjectType = null;
220
+ }
221
+ }
222
+
223
+ /** Shared by the file chooser and drag and drop. */
224
+ function selectFile(file: File): string | null {
225
+ let url: string | null = null;
226
+
227
+ if (typeof URL !== "undefined" && URL.createObjectURL) {
228
+ revokeCurrentObjectUrl();
229
+ url = URL.createObjectURL(file);
230
+ value = url;
231
+ currentObjectUrl = url;
232
+ currentObjectType =
233
+ file.type.startsWith("video/") || (!file.type && isVideoPath(file.name))
234
+ ? "video"
235
+ : "image";
33
236
  }
237
+
238
+ return url;
34
239
  }
35
240
 
36
241
  function handleFileSelect(event: Event) {
@@ -40,90 +245,224 @@
40
245
  if (!input.files || input.files.length === 0) return;
41
246
 
42
247
  const file = input.files[0];
248
+ const url = selectFile(file);
43
249
 
44
- if (typeof URL !== "undefined" && URL.createObjectURL) {
45
- revokeCurrentObjectUrl();
46
- const url = URL.createObjectURL(file);
47
- value = url;
48
- currentObjectUrl = url;
49
- }
250
+ onchange?.(event);
251
+ onfileselect?.({ file, url });
50
252
  }
51
253
 
52
254
  function removeImage() {
53
255
  if (disabled) return;
54
256
 
257
+ focusReturnArmed = true;
55
258
  revokeCurrentObjectUrl();
56
259
  value = null;
260
+ if (fileInput) fileInput.value = "";
261
+ onfileselect?.({ file: null, url: null });
57
262
  }
58
263
 
59
- function triggerFileSelect() {
264
+ function triggerFileSelect(event: Event) {
60
265
  if (disabled) return;
266
+ onclick?.(event);
267
+ if (event.defaultPrevented) return;
268
+ focusReturnArmed = true;
269
+ if (onbrowse) {
270
+ onbrowse(event);
271
+ return;
272
+ }
61
273
  fileInput?.click();
62
274
  }
63
275
 
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;
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 });
333
+ }
334
+
64
335
  onDestroy(() => {
65
336
  revokeCurrentObjectUrl();
66
337
  });
67
338
  </script>
68
339
 
69
- <div class="space-y-3">
70
- {#if value}
71
- <div class="relative group">
72
- <img
73
- src={value}
74
- alt=""
75
- class="w-full h-32 min-w-64 object-cover rounded-2xl border-0"
76
- />
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}
77
362
  <div
78
- class="absolute inset-0 border border-input-border bg-input min-w-64 opacity-0 group-hover:opacity-100 transition-opacity rounded-2xl flex items-center justify-center"
363
+ class="relative group"
364
+ role={label ? "group" : undefined}
365
+ aria-labelledby={label ? labelId : undefined}
79
366
  >
80
- <div class="flex gap-2">
81
- <Button
82
- variant="secondary"
83
- size="sm"
84
- onclick={triggerFileSelect}
85
- {disabled}
86
- >
87
- Change
88
- </Button>
89
- <Button
90
- variant="danger"
91
- size="sm"
92
- onclick={removeImage}
93
- {disabled}
94
- >
95
- Remove
96
- </Button>
97
- </div>
98
- </div>
99
- </div>
100
- {:else}
101
- <div
102
- class="border-2 border-dashed border-input rounded-2xl min-w-64 p-6 text-center hover:border-brand-500 transition-colors {disabled
103
- ? 'cursor-not-allowed opacity-50'
104
- : 'cursor-pointer'}"
105
- onclick={triggerFileSelect}
106
- role="button"
107
- tabindex={disabled ? -1 : 0}
108
- onkeydown={(e) =>
109
- e.key === "Enter" && !disabled && triggerFileSelect()}
110
- aria-disabled={disabled}
111
- >
112
- <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. -->
113
394
  <div
114
- class="w-12 h-12 mx-auto bg-action-secondary rounded-lg 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"
115
404
  >
116
- <Image size={24} class="text-description" />
117
- </div>
118
- <div>
119
- <p class="font-medium text-headline">{placeholder}</p>
120
- <p class="text-sm text-description">
121
- Click to choose a file
122
- </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>
123
431
  </div>
124
432
  </div>
125
- </div>
126
- {/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>
127
466
 
128
467
  <input
129
468
  bind:this={fileInput}
@@ -132,13 +471,26 @@
132
471
  onchange={handleFileSelect}
133
472
  {disabled}
134
473
  class="hidden"
474
+ data-testid="image-upload-input"
135
475
  />
136
476
 
477
+ <div class="sr-only" role="status" aria-live="polite" aria-atomic="true">
478
+ {statusMessage}
479
+ </div>
480
+
137
481
  {#if errorMessage}
138
- <div class="rounded-lg border border-error px-3 py-2 text-sm text-error" role="alert">
139
- <p class="font-medium">Image upload failed</p>
482
+ <!-- Same tinted fill + tinted border Alert uses, rather than a bare
483
+ step-600 outline on a transparent ground. -->
484
+ <div
485
+ id={errorId}
486
+ class="rounded-container border bg-error-subtle border-error-border text-error-text px-3 py-2 text-sm"
487
+ role="alert"
488
+ >
489
+ <p class="font-medium">{errorTitle}</p>
140
490
  <p>{errorMessage}</p>
141
- <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}
142
494
  </div>
143
495
  {/if}
144
496
  </div>
@@ -1,15 +1,71 @@
1
- interface Props {
1
+ import { type Snippet } from "svelte";
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ export type ImageUploadFileDetail = {
4
+ /** Selected file, or `null` when the image is removed. */
5
+ file: File | null;
6
+ /** Object URL used for the preview, or `null`. */
7
+ url: string | null;
8
+ };
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"> & {
19
+ /** Extra classes for the host element. */
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;
25
+ /** Preview URL; `bind:value` to read it. */
2
26
  value?: string | null;
3
27
  disabled?: boolean;
4
28
  accept?: string;
5
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;
6
54
  errorMessage?: string;
55
+ errorTitle?: string;
56
+ /** Closing line of the error block; `false` or `""` leaves it out. */
57
+ errorRecovery?: string | false;
58
+ /** Native `change` event from the hidden file input. */
7
59
  onchange?: (event: Event) => void;
60
+ /** Fires before the file chooser opens; `event.preventDefault()` keeps it closed. */
8
61
  onclick?: (event: Event) => void;
9
- }
10
- type $$ComponentProps = Props & {
11
- children?: any;
62
+ /** Replaces the native file chooser, for a media library or another source. */
63
+ onbrowse?: (event: Event) => void;
64
+ /** Selected file and preview URL; `{ file: null, url: null }` on remove. */
65
+ onfileselect?: (detail: ImageUploadFileDetail) => void;
66
+ /** A dropped file that `accept` does not allow. */
67
+ onfilereject?: (file: File) => void;
12
68
  };
13
- declare const ImageUpload: import("svelte").Component<$$ComponentProps, {}, "">;
69
+ declare const ImageUpload: import("svelte").Component<Props, {}, "value">;
14
70
  type ImageUpload = ReturnType<typeof ImageUpload>;
15
71
  export default ImageUpload;