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
@@ -2,55 +2,121 @@
2
2
  import { onMount } from 'svelte';
3
3
  import { cubicOut } from 'svelte/easing';
4
4
  import { slide, fade, fly } from 'svelte/transition';
5
- import {
6
- AlertCircle,
7
- AlertTriangle,
8
- CheckCircle,
9
- ChevronDown,
10
- Info,
11
- X,
12
- } from '@lucide/svelte';
5
+ import AlertCircle from '@lucide/svelte/icons/circle-alert';
6
+ import AlertTriangle from '@lucide/svelte/icons/triangle-alert';
7
+ import CheckCircle from '@lucide/svelte/icons/circle-check-big';
8
+ import ChevronDown from '@lucide/svelte/icons/chevron-down';
9
+ import Info from '@lucide/svelte/icons/info';
10
+ import X from '@lucide/svelte/icons/x';
13
11
  import Button from '../atoms/Button.svelte';
14
12
  import { toastStore, type ToastItem } from './toast-store.js';
13
+ import { cn } from "../util/cn.js";
14
+ import {
15
+ DEFAULT_TOASTER_STRINGS,
16
+ resolveToastDuration,
17
+ TOAST_SECONDS_AFTER_HOLD,
18
+ type ToastDuration,
19
+ type ToasterStrings,
20
+ type ToastPauseChange,
21
+ } from "../util/toaster.js";
15
22
 
16
23
  interface Props {
24
+ /** Extra classes for the host element. */
25
+ class?: string;
17
26
  toast: ToastItem;
27
+ /** Every word the toast says by itself; the Toaster passes its own. */
28
+ strings?: ToasterStrings;
29
+ /** Shows the time left as a sentence, with a button that stops the timer. */
30
+ showCountdown?: boolean;
31
+ /** What a toast pushed without a duration gets, in place of medium; the Toaster passes its own. */
32
+ defaultDuration?: ToastDuration;
33
+ /** Called when a pointer, a finger or focus starts or stops holding the timer. */
34
+ onpausechange?: (detail: ToastPauseChange) => void;
18
35
  }
19
36
 
20
- let { toast }: Props = $props();
21
-
22
- /** Seconds until auto-dismiss; `0` means no countdown (manual dismiss only). */
23
- function autoDismissSeconds(duration: number | undefined): number {
24
- if (duration === undefined) return 14;
25
- if (duration <= 0) return 0;
26
- return Math.max(1, Math.ceil(duration / 1000));
27
- }
28
-
29
- const maxSeconds = $derived(autoDismissSeconds(toast.duration));
37
+ let {
38
+ class: className = "",
39
+ toast,
40
+ strings = DEFAULT_TOASTER_STRINGS,
41
+ showCountdown = false,
42
+ defaultDuration,
43
+ onpausechange,
44
+ }: Props = $props();
30
45
 
31
46
  let count = $state(0);
32
47
  let timerActive = $state(false);
33
48
  let isExpanded = $state(false);
49
+ /**
50
+ * What holds the timer (WCAG 2.2.1): a mouse or a stylus over the toast,
51
+ * a finger on it, keyboard focus inside it.
52
+ */
53
+ let hovered = $state(false);
54
+ let held = $state(false);
55
+ let focusWithin = $state(false);
56
+ const paused = $derived(hovered || held || focusWithin);
34
57
 
35
58
  let intervalRef: ReturnType<typeof setInterval> | undefined;
36
59
 
37
- const expandableText = $derived((toast.detail ?? toast.message).trim());
60
+ const title = $derived(toast.title?.trim() ?? "");
61
+ const message = $derived(toast.message.trim());
62
+
63
+ /**
64
+ * What a toast pushed with neither a title nor a message says: a word
65
+ * for its type. A toast with a message says the message.
66
+ */
67
+ const fallbackTitle = $derived(
68
+ toast.type === 'success'
69
+ ? strings.successTitle
70
+ : toast.type === 'error'
71
+ ? strings.errorTitle
72
+ : toast.type === 'warning'
73
+ ? strings.warningTitle
74
+ : strings.infoTitle,
75
+ );
76
+
77
+ /** The first line: the title, or the message when there is no title. */
78
+ const headerTitle = $derived(title || message || fallbackTitle);
79
+ /** The second line: the message, under a title that says something else. */
80
+ const secondLine = $derived(title && message && message !== title ? message : "");
38
81
 
82
+ /**
83
+ * The part that opens and closes is `detail`, when the toast was given
84
+ * one: the long version. The message is never folded away.
85
+ */
86
+ const expandableText = $derived((toast.detail ?? "").trim());
39
87
  const hasExpandable = $derived(expandableText.length > 0);
40
88
 
41
- const headerTitle = $derived.by(() => {
42
- const t = toast.title?.trim();
43
- if (t) return t;
44
- switch (toast.type) {
45
- case 'success':
46
- return 'Changes saved';
47
- case 'error':
48
- return 'Something went wrong';
49
- case 'warning':
50
- return 'Please review';
51
- default:
52
- return 'Notice';
53
- }
89
+ /**
90
+ * Seconds until the toast closes by itself; `0` is never. What the app
91
+ * said for this toast wins. Otherwise it goes by what the toast is and by
92
+ * how much there is to read in it: the two lines that are shown, not the
93
+ * detail behind its button.
94
+ */
95
+ const maxSeconds = $derived.by(() => {
96
+ const ms = resolveToastDuration(
97
+ {
98
+ duration: toast.duration,
99
+ type: toast.type,
100
+ hasAction: !!toast.action,
101
+ textLength: headerTitle.length + secondLine.length,
102
+ },
103
+ defaultDuration,
104
+ );
105
+ return ms <= 0 ? 0 : Math.max(1, Math.ceil(ms / 1000));
106
+ });
107
+
108
+ /** The time left as words: for the sentence, or for a screen reader to find. */
109
+ const countdownText = $derived(
110
+ paused ? strings.pausedClosesIn(count) : strings.closesIn(count),
111
+ );
112
+
113
+ // Said to the app as well: `data-paused` is the same thing in the markup.
114
+ let reportedPause = false;
115
+ $effect(() => {
116
+ const now = paused;
117
+ if (now === reportedPause) return;
118
+ reportedPause = now;
119
+ onpausechange?.({ id: toast.id, paused: now });
54
120
  });
55
121
 
56
122
  const statusIconClass = $derived(
@@ -90,6 +156,10 @@
90
156
  timerActive = true;
91
157
 
92
158
  intervalRef = setInterval(() => {
159
+ // The state, not the `paused` derived: a tick can land while the
160
+ // toast is animating out, and reading a derived then warns
161
+ // (`derived_inert`).
162
+ if (hovered || held || focusWithin) return;
93
163
  count -= 1;
94
164
  if (count <= 0) {
95
165
  stopTimer();
@@ -109,44 +179,171 @@
109
179
  toastStore.dismiss(toast.id);
110
180
  }
111
181
 
182
+ /** The toast closes even if the handler throws, unless the action opted out. */
183
+ function handleAction() {
184
+ const action = toast.action;
185
+ if (!action) return;
186
+ try {
187
+ action.onclick();
188
+ } finally {
189
+ if (action.dismissOnClick !== false) {
190
+ toastStore.dismiss(toast.id);
191
+ }
192
+ }
193
+ }
194
+
195
+ let expandButton = $state<HTMLButtonElement>();
196
+ let dismissButton = $state<HTMLButtonElement>();
197
+
198
+ /**
199
+ * Okay closes the details and goes with them. Focus moves to the button
200
+ * that opens them again, or it would fall to the page and a keyboard
201
+ * user would be at the top of it.
202
+ */
112
203
  function handleOkay() {
113
204
  isExpanded = false;
205
+ expandButton?.focus();
206
+ }
207
+
208
+ /** The stop button goes with the sentence it is in: focus stays in the toast. */
209
+ function handleStop(event: MouseEvent) {
210
+ const held = event.currentTarget === document.activeElement;
211
+ stopTimer();
212
+ if (held) dismissButton?.focus();
114
213
  }
115
214
 
116
- const toastEnter = { y: 18, duration: 260, opacity: 0, easing: cubicOut };
117
- const toastLeave = { y: 14, duration: 300, opacity: 0, easing: cubicOut };
215
+ /**
216
+ * The kind of pointer last seen on the toast. After a tap a browser makes
217
+ * up mouse events for it, `mouseenter` among them and never a
218
+ * `mouseleave`: taken for a mouse, a tap held the timer for good.
219
+ */
220
+ let lastPointer: string | undefined;
221
+
222
+ function handlePointerEnter(event: PointerEvent) {
223
+ lastPointer = event.pointerType;
224
+ // A mouse, or a stylus in range. A finger is over the toast only while it is on it.
225
+ if (event.pointerType !== "touch") hovered = true;
226
+ }
227
+
228
+ function handlePointerLeave(event: PointerEvent) {
229
+ if (event.pointerType !== "touch") hovered = false;
230
+ }
231
+
232
+ function handleMouseEnter() {
233
+ if (lastPointer !== "touch") hovered = true;
234
+ }
235
+
236
+ function handlePointerDown(event: PointerEvent) {
237
+ lastPointer = event.pointerType;
238
+ if (event.pointerType === "touch") held = true;
239
+ }
240
+
241
+ /**
242
+ * The finger lifts, or the browser takes the touch for a scroll. The
243
+ * timer runs again, with time left to do something about the toast: it
244
+ * was held because someone was reading it.
245
+ */
246
+ function handlePointerRelease(event: PointerEvent) {
247
+ if (event.pointerType !== "touch" || !held) return;
248
+ held = false;
249
+ if (timerActive) count = Math.max(count, Math.min(TOAST_SECONDS_AFTER_HOLD, maxSeconds));
250
+ }
251
+
252
+ function handleFocusOut(event: FocusEvent) {
253
+ const next = event.relatedTarget as Node | null;
254
+ if (!next || !(event.currentTarget as HTMLElement).contains(next)) {
255
+ focusWithin = false;
256
+ }
257
+ }
258
+
259
+ /** No movement where motion is unwanted: the toast appears and goes at once. */
260
+ const reducedMotion =
261
+ typeof window !== 'undefined' &&
262
+ typeof window.matchMedia === 'function' &&
263
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches;
264
+ const motion = (duration: number) => (reducedMotion ? 0 : duration);
265
+
266
+ const toastEnter = { y: 18, duration: motion(260), opacity: 0, easing: cubicOut };
267
+ const toastLeave = { y: 14, duration: motion(300), opacity: 0, easing: cubicOut };
118
268
  </script>
119
269
 
120
270
  <div
121
- class="pointer-events-auto relative w-full min-w-[18rem] overflow-hidden rounded-2xl border border-border bg-surface-1 shadow-xl"
271
+ class={cn("pointer-events-auto relative w-full min-w-[min(18rem,100%)] shrink-0 overflow-hidden rounded-overlay border border-border-overlay bg-surface-overlay shadow-lg", className)}
122
272
  in:fly={toastEnter}
123
273
  out:fly={toastLeave}
124
- role={toast.type === 'error' ? 'alert' : 'status'}
274
+ onpointerenter={handlePointerEnter}
275
+ onpointerleave={handlePointerLeave}
276
+ onpointerdown={handlePointerDown}
277
+ onpointerup={handlePointerRelease}
278
+ onpointercancel={handlePointerRelease}
279
+ onmouseenter={handleMouseEnter}
280
+ onmouseleave={() => (hovered = false)}
281
+ onfocusin={() => (focusWithin = true)}
282
+ onfocusout={handleFocusOut}
283
+ role="group"
284
+ aria-label={headerTitle}
285
+ data-toast-id={toast.id}
286
+ data-paused={paused}
125
287
  >
126
- <div class="p-4">
127
- <div class="flex items-start gap-3">
128
- <div class="shrink-0 pt-0.5">
288
+ <!-- Padding and gaps in px: at 200% text on a phone, room that grew with
289
+ the text left the text itself a column too narrow for one long word. -->
290
+ <div class="p-[16px]">
291
+ <!-- The title keeps 8rem at least, icon included. When the toast has
292
+ less beside its buttons (320px with enlarged text), the buttons go to a
293
+ line of their own under it, at the end. -->
294
+ <div class="flex flex-wrap items-start gap-x-[12px] gap-y-[4px]">
295
+ <!-- Live region holds only title + message, so the countdown and buttons are never re-announced. -->
296
+ <div
297
+ class="flex min-w-0 flex-[1_1_8rem] items-start gap-[12px]"
298
+ role={toast.type === 'error' ? 'alert' : 'status'}
299
+ aria-atomic="true"
300
+ >
301
+ <!-- 20px at every text size, as the padding and the gap beside it
302
+ are px: an icon that grew with the text took the room the text
303
+ needed. Lowered so it stays at the middle of the first line. -->
304
+ <div class="shrink-0 mt-[calc((1.5rem_-_20px)_/_2)]">
129
305
  {#if toast.type === 'success'}
130
- <CheckCircle class="size-5 {statusIconClass}" aria-hidden="true" />
306
+ <CheckCircle class="size-[20px] {statusIconClass}" aria-hidden="true" />
131
307
  {:else if toast.type === 'error'}
132
- <AlertCircle class="size-5 {statusIconClass}" aria-hidden="true" />
308
+ <AlertCircle class="size-[20px] {statusIconClass}" aria-hidden="true" />
133
309
  {:else if toast.type === 'warning'}
134
- <AlertTriangle class="size-5 {statusIconClass}" aria-hidden="true" />
310
+ <AlertTriangle class="size-[20px] {statusIconClass}" aria-hidden="true" />
135
311
  {:else}
136
- <Info class="size-5 {statusIconClass}" aria-hidden="true" />
312
+ <Info class="size-[20px] {statusIconClass}" aria-hidden="true" />
137
313
  {/if}
138
314
  </div>
139
- <h4 class="min-w-0 flex-1 text-base font-semibold text-headline">
140
- {headerTitle}
141
- </h4>
142
- <div class="flex shrink-0 items-center gap-0.5">
315
+ <div class="min-w-0 flex-1">
316
+ <!-- A heading when the app gave the toast a title (or nothing
317
+ at all); a message on its own is a sentence, not a heading. -->
318
+ {#if title || !message}
319
+ <h4 class="text-base font-semibold text-headline [overflow-wrap:anywhere]">
320
+ {headerTitle}
321
+ </h4>
322
+ {:else}
323
+ <p class="text-base font-medium text-headline [overflow-wrap:anywhere]" data-toast-message>
324
+ {headerTitle}
325
+ </p>
326
+ {/if}
327
+ {#if secondLine}
328
+ <p class="mt-1 text-sm text-description [overflow-wrap:anywhere]" data-toast-message>
329
+ {secondLine}
330
+ </p>
331
+ {/if}
332
+ {#if toast.action}
333
+ <!-- The button sits outside the live region, so this is how a screen reader user hears that there is one. -->
334
+ <span class="sr-only">{strings.actionAvailable(toast.action.label)}</span>
335
+ {/if}
336
+ </div>
337
+ </div>
338
+ <div class="ms-auto flex shrink-0 items-center gap-1">
143
339
  {#if hasExpandable}
144
340
  <button
145
341
  type="button"
146
- class="focus-ring cursor-pointer rounded-md p-1 text-description transition-colors hover:bg-base-100 hover:text-headline focus:outline-none"
342
+ class="focus-ring cursor-pointer inline-flex items-center justify-center rounded-control p-1 pointer-coarse:min-h-[44px] pointer-coarse:min-w-[44px] text-description transition-colors hover:bg-surface-overlay-hover hover:text-headline active:bg-surface-active focus:outline-none"
147
343
  aria-expanded={isExpanded}
148
- aria-controls="toaster-expand-{toast.id}"
149
- aria-label={isExpanded ? 'Collapse details' : 'Expand details'}
344
+ aria-controls={isExpanded ? `toaster-expand-${toast.id}` : undefined}
345
+ aria-label={isExpanded ? strings.collapse : strings.expand}
346
+ bind:this={expandButton}
150
347
  onclick={() => (isExpanded = !isExpanded)}
151
348
  >
152
349
  <ChevronDown
@@ -159,26 +356,41 @@
159
356
  {/if}
160
357
  <button
161
358
  type="button"
162
- class="focus-ring cursor-pointer rounded-md p-1 text-description transition-colors hover:bg-base-100 hover:text-headline focus:outline-none"
359
+ class="focus-ring cursor-pointer inline-flex items-center justify-center rounded-control p-1 pointer-coarse:min-h-[44px] pointer-coarse:min-w-[44px] text-description transition-colors hover:bg-surface-overlay-hover hover:text-headline active:bg-surface-active focus:outline-none"
163
360
  onclick={handleDismiss}
164
- aria-label="Dismiss notification"
361
+ aria-label={strings.dismiss}
362
+ bind:this={dismissButton}
165
363
  >
166
364
  <X class="size-5" aria-hidden="true" />
167
365
  </button>
168
366
  </div>
169
367
  </div>
170
368
 
369
+ {#if toast.action}
370
+ <!-- Indented to the title: past the 20px status icon and its 12px gap. -->
371
+ <div class="mt-3 pl-[32px]">
372
+ <Button
373
+ variant="secondary"
374
+ size="sm"
375
+ text={toast.action.label}
376
+ type="button"
377
+ data-toast-action
378
+ onclick={handleAction}
379
+ />
380
+ </div>
381
+ {/if}
382
+
171
383
  {#if hasExpandable && isExpanded}
172
384
  <div
173
385
  id="toaster-expand-{toast.id}"
174
386
  class="mt-3"
175
- transition:slide={{ duration: 200 }}
387
+ transition:slide={{ duration: motion(200) }}
176
388
  >
177
389
  <p class="text-sm text-description">{expandableText}</p>
178
390
  <Button
179
391
  variant="primary"
180
392
  size="sm"
181
- text="Okay"
393
+ text={strings.okay}
182
394
  type="button"
183
395
  class="mt-3"
184
396
  onclick={handleOkay}
@@ -187,25 +399,30 @@
187
399
  {/if}
188
400
  </div>
189
401
 
190
- {#if timerActive}
191
- <div class="px-4 pb-3" transition:fade={{ duration: 180 }}>
192
- <p class="text-xs text-description">
193
- This message will close in {count} seconds.
402
+ {#if timerActive && showCountdown}
403
+ <div class="px-[16px] pb-3" transition:fade={{ duration: motion(180) }}>
404
+ <p class="text-xs text-description" data-toast-countdown>
405
+ {countdownText}
194
406
  <button
195
407
  type="button"
196
- class="focus-ring cursor-pointer rounded-sm text-link underline-offset-2 hover:underline focus:outline-none"
197
- onclick={stopTimer}
408
+ class="focus-ring cursor-pointer rounded-control text-link underline-offset-2 hover:underline active:underline focus:outline-none pointer-coarse:inline-flex pointer-coarse:items-center pointer-coarse:min-h-[44px]"
409
+ onclick={handleStop}
198
410
  >
199
- Click to stop
411
+ {strings.stop}
200
412
  </button>
201
413
  </p>
202
414
  </div>
415
+ {:else if timerActive}
416
+ <!-- The bar below shows the time left to the eye. For a screen reader
417
+ it is here as words: outside the live region, so it is found when the
418
+ toast is read and is not spoken again every second. -->
419
+ <p class="sr-only" data-toast-countdown>{countdownText}</p>
203
420
  {/if}
204
421
 
205
422
  {#if timerActive}
206
423
  <div
207
424
  class="pointer-events-none absolute bottom-0 left-0 right-0 h-1 bg-border/70"
208
- transition:fade={{ duration: 180 }}
425
+ transition:fade={{ duration: motion(180) }}
209
426
  aria-hidden="true"
210
427
  >
211
428
  <div
@@ -1,6 +1,17 @@
1
1
  import { type ToastItem } from './toast-store.js';
2
+ import { type ToastDuration, type ToasterStrings, type ToastPauseChange } from "../util/toaster.js";
2
3
  interface Props {
4
+ /** Extra classes for the host element. */
5
+ class?: string;
3
6
  toast: ToastItem;
7
+ /** Every word the toast says by itself; the Toaster passes its own. */
8
+ strings?: ToasterStrings;
9
+ /** Shows the time left as a sentence, with a button that stops the timer. */
10
+ showCountdown?: boolean;
11
+ /** What a toast pushed without a duration gets, in place of medium; the Toaster passes its own. */
12
+ defaultDuration?: ToastDuration;
13
+ /** Called when a pointer, a finger or focus starts or stops holding the timer. */
14
+ onpausechange?: (detail: ToastPauseChange) => void;
4
15
  }
5
16
  declare const ToasterToast: import("svelte").Component<Props, {}, "">;
6
17
  type ToasterToast = ReturnType<typeof ToasterToast>;
@@ -0,0 +1,212 @@
1
+ <script lang="ts">
2
+ import { DEFAULT_UNSAVED_CHANGES_BAR_TEXTS, zabiStringsFor } from "../util/zabi-strings.js";
3
+ import { tick, untrack, type Snippet } from "svelte";
4
+ import type { HTMLAttributes } from "svelte/elements";
5
+ import Button from "../atoms/Button.svelte";
6
+ import { cn } from "../util/cn.js";
7
+
8
+ type Props = Omit<HTMLAttributes<HTMLDivElement>, "class" | "role"> & {
9
+ /** Shows the bar. Set it while the form differs from what is saved. */
10
+ dirty?: boolean;
11
+ /** Shown in the bar, and announced once when it appears. */
12
+ message?: string;
13
+ /** Accessible name of the bar as a landmark. */
14
+ label?: string;
15
+ saveLabel?: string;
16
+ discardLabel?: string;
17
+ /**
18
+ * Save shows its loading state and Discard is disabled. Set by the
19
+ * bar itself while a promise from `onsave` is pending.
20
+ */
21
+ saving?: boolean;
22
+ /**
23
+ * Called when Save is activated. Return a promise to keep the bar in
24
+ * its saving state until it settles. The bar does not hide itself:
25
+ * clear `dirty` once the save has gone through.
26
+ */
27
+ onsave?: () => void | Promise<unknown>;
28
+ /** Called when Discard is activated. */
29
+ ondiscard?: () => void;
30
+ /**
31
+ * Called when the promise from `onsave` rejects; the bar stays.
32
+ * Without it the error goes to the global error handler.
33
+ */
34
+ onerror?: (error: unknown) => void;
35
+ /**
36
+ * Which edge of its scroll container the bar sticks to. It is sticky,
37
+ * not fixed, so it keeps its place in the flow: put it after the
38
+ * form's last field (`bottom`) or before its first (`top`).
39
+ */
40
+ position?: "bottom" | "top";
41
+ /** Extra buttons, placed before Discard. */
42
+ actions?: Snippet;
43
+ /** Extra classes for the bar. */
44
+ class?: string;
45
+ };
46
+
47
+ let {
48
+ dirty = false,
49
+ message: messageGiven,
50
+ label: labelGiven,
51
+ saveLabel: saveLabelGiven,
52
+ discardLabel: discardLabelGiven,
53
+ saving = false,
54
+ onsave,
55
+ ondiscard,
56
+ onerror,
57
+ position = "bottom",
58
+ actions,
59
+ class: className = "",
60
+ ...restProps
61
+ }: Props = $props();
62
+
63
+ /** Each text: the prop, else the app-wide word from a `ZabiStringsProvider` above this one, else English. */
64
+ const provided = zabiStringsFor("unsavedChangesBar");
65
+ const message = $derived(messageGiven ?? provided()?.message ?? DEFAULT_UNSAVED_CHANGES_BAR_TEXTS.message);
66
+ const label = $derived(labelGiven ?? provided()?.label ?? DEFAULT_UNSAVED_CHANGES_BAR_TEXTS.label);
67
+ const saveLabel = $derived(saveLabelGiven ?? provided()?.saveLabel ?? DEFAULT_UNSAVED_CHANGES_BAR_TEXTS.saveLabel);
68
+ const discardLabel = $derived(discardLabelGiven ?? provided()?.discardLabel ?? DEFAULT_UNSAVED_CHANGES_BAR_TEXTS.discardLabel);
69
+
70
+ /** True while a promise returned by `onsave` is pending. */
71
+ let pending = $state(false);
72
+ const busy = $derived(saving || pending);
73
+
74
+ let host: HTMLDivElement | undefined = $state();
75
+ /** Where focus was before it came into the bar; it goes back there when the bar leaves. */
76
+ let cameFrom: HTMLElement | null = null;
77
+
78
+ async function save() {
79
+ if (busy) return;
80
+ // A synchronous throw is the caller's bug: it propagates.
81
+ const result = onsave?.();
82
+ if (!(result instanceof Promise)) return;
83
+ pending = true;
84
+ try {
85
+ await result;
86
+ } catch (error) {
87
+ // The bar stays, so the caller can show what went wrong. Never
88
+ // swallowed: with no `onerror` it is reported like any other
89
+ // uncaught error.
90
+ if (onerror) onerror(error);
91
+ else if (typeof reportError === "function") reportError(error);
92
+ else console.error(error);
93
+ } finally {
94
+ pending = false;
95
+ }
96
+ }
97
+
98
+ function discard() {
99
+ if (busy) return;
100
+ ondiscard?.();
101
+ }
102
+
103
+ function handleFocusIn(event: FocusEvent) {
104
+ const from = event.relatedTarget;
105
+ if (from instanceof HTMLElement && !host?.contains(from)) cameFrom = from;
106
+ }
107
+
108
+ function canTakeFocus(element: HTMLElement | null): element is HTMLElement {
109
+ return (
110
+ !!element &&
111
+ element.isConnected &&
112
+ !(element as HTMLButtonElement).disabled &&
113
+ !host?.contains(element)
114
+ );
115
+ }
116
+
117
+ /**
118
+ * While busy, Save is loading and stays focusable, so focus that is on
119
+ * it is left there. Discard is disabled, and a disabled button drops
120
+ * focus on `<body>`: focus that was on it (or on an action that is gone)
121
+ * is held on the bar instead, and handed to Save if the bar is still
122
+ * there afterwards (a rejected save).
123
+ */
124
+ let wasBusy = false;
125
+ $effect.pre(() => {
126
+ const now = busy;
127
+ untrack(() => {
128
+ const changed = now !== wasBusy;
129
+ wasBusy = now;
130
+ // Read before the buttons change: afterwards focus may be gone.
131
+ if (!changed || !host?.contains(document.activeElement)) return;
132
+ void tick().then(() => {
133
+ if (now) {
134
+ const active = document.activeElement;
135
+ const usable =
136
+ !!active &&
137
+ active !== host &&
138
+ !!host?.contains(active) &&
139
+ !(active as HTMLButtonElement).disabled;
140
+ if (!usable) host?.focus();
141
+ } else if (dirty && document.activeElement === host) {
142
+ host?.querySelector<HTMLElement>("[data-unsaved-changes-save]")?.focus();
143
+ }
144
+ });
145
+ });
146
+ });
147
+
148
+ /**
149
+ * When the bar leaves, its buttons go with it. If focus was on one, send
150
+ * it back to where it came from, usually the field the user was editing;
151
+ * failing that it stays on the (now empty) host, which keeps the user's
152
+ * place in the page instead of dropping them at the top.
153
+ */
154
+ let wasDirty = false;
155
+ $effect.pre(() => {
156
+ const now = dirty;
157
+ untrack(() => {
158
+ const leaving = wasDirty && !now;
159
+ wasDirty = now;
160
+ if (!leaving || !host?.contains(document.activeElement)) return;
161
+ const target = canTakeFocus(cameFrom) ? cameFrom : host;
162
+ cameFrom = null;
163
+ void tick().then(() => {
164
+ const active = document.activeElement;
165
+ // Never from an element that has taken focus meanwhile.
166
+ if (active && active !== document.body && !host?.contains(active)) return;
167
+ target.focus();
168
+ });
169
+ });
170
+ });
171
+ </script>
172
+
173
+ <!--
174
+ The host is always rendered and empty until `dirty`. The message is a live
175
+ region, and a live region is only announced when its text changes inside an
176
+ element that was already there. The status is on the message alone, so the
177
+ buttons are not read out with it, and nothing here takes focus on appearing.
178
+ -->
179
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
180
+ <div
181
+ bind:this={host}
182
+ tabindex="-1"
183
+ role={dirty ? "region" : undefined}
184
+ aria-label={dirty ? label : undefined}
185
+ aria-busy={dirty && busy ? "true" : undefined}
186
+ data-position={position}
187
+ class={cn(
188
+ "focus-ring",
189
+ dirty &&
190
+ "sticky z-sticky flex flex-wrap items-center justify-between gap-x-4 gap-y-2 rounded-container border border-border-overlay bg-surface-overlay px-4 py-3 shadow-lg",
191
+ // Clear of the home indicator on a phone.
192
+ dirty && (position === "top" ? "top-2" : "bottom-[max(0.5rem,env(safe-area-inset-bottom))]"),
193
+ dirty && className,
194
+ )}
195
+ onfocusin={handleFocusIn}
196
+ {...restProps}
197
+ >
198
+ <p role="status" class={dirty ? "min-w-0 text-sm font-medium text-body" : "sr-only"}>
199
+ {dirty ? message : ""}
200
+ </p>
201
+ {#if dirty}
202
+ <div class="flex flex-wrap items-center gap-2">
203
+ {@render actions?.()}
204
+ <Button variant="ghost" onclick={discard} disabled={busy}>
205
+ {discardLabel}
206
+ </Button>
207
+ <Button onclick={save} loading={busy} data-unsaved-changes-save>
208
+ {saveLabel}
209
+ </Button>
210
+ </div>
211
+ {/if}
212
+ </div>