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