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,63 +1,153 @@
1
1
  <script lang="ts">
2
- import { onMount } from 'svelte';
2
+ import { onMount, untrack } 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';
15
13
  import { cn } from "../util/cn.js";
14
+ import { createVelocityTracker, DRAG_SLOP, FLICK_VELOCITY, prefersReducedMotion, timeOf } from "../util/sheet-drag.js";
15
+ import {
16
+ DEFAULT_TOASTER_STRINGS,
17
+ easeStandard,
18
+ resolveToastDuration,
19
+ toastsReplaced,
20
+ toastTexts,
21
+ TOAST_SECONDS_AFTER_HOLD,
22
+ TOAST_SWIPE_DISMISS_FRACTION,
23
+ type ToastDuration,
24
+ type ToasterStrings,
25
+ type ToastPauseChange,
26
+ } from "../util/toaster.js";
16
27
 
17
28
  interface Props {
18
29
  /** Extra classes for the host element. */
19
30
  class?: string;
20
31
  toast: ToastItem;
32
+ /** Every word the toast says by itself; the Toaster passes its own. */
33
+ strings?: ToasterStrings;
34
+ /** Shows the time left as a sentence, with a button that stops the timer. */
35
+ showCountdown?: boolean;
36
+ /** What a toast pushed without a duration gets, in place of medium; the Toaster passes its own. */
37
+ defaultDuration?: ToastDuration;
38
+ /** Called when a pointer, a finger or focus starts or stops holding the timer. */
39
+ onpausechange?: (detail: ToastPauseChange) => void;
40
+ /** The compact form for a phone: one row, docked, sliding out from the edge it sits on. */
41
+ snackbar?: boolean;
42
+ /** A touch or a pen drags the toast away, sideways (and down, in the snackbar). */
43
+ swipeToDismiss?: boolean;
44
+ /** Draws the time bar (`true`/`false`); unset, it is left out of a snackbar toast with nothing to press. */
45
+ timeLine?: boolean;
21
46
  }
22
47
 
23
- let { class: className = "", toast }: Props = $props();
24
-
25
- /** Seconds until auto-dismiss; `0` means no countdown (manual dismiss only). */
26
- function autoDismissSeconds(duration: number | undefined): number {
27
- if (duration === undefined) return 14;
28
- if (duration <= 0) return 0;
29
- return Math.max(1, Math.ceil(duration / 1000));
30
- }
31
-
32
- const maxSeconds = $derived(autoDismissSeconds(toast.duration));
48
+ let {
49
+ class: className = "",
50
+ toast,
51
+ strings = DEFAULT_TOASTER_STRINGS,
52
+ showCountdown = false,
53
+ defaultDuration,
54
+ onpausechange,
55
+ snackbar = false,
56
+ swipeToDismiss = true,
57
+ timeLine,
58
+ }: Props = $props();
33
59
 
34
60
  let count = $state(0);
35
61
  let timerActive = $state(false);
36
62
  let isExpanded = $state(false);
37
- /** Hover or keyboard focus inside the toast pauses auto-dismiss (WCAG 2.2.1). */
63
+ /**
64
+ * What holds the timer (WCAG 2.2.1): a mouse or a stylus over the toast,
65
+ * a finger on it, keyboard focus inside it.
66
+ */
38
67
  let hovered = $state(false);
68
+ let held = $state(false);
39
69
  let focusWithin = $state(false);
40
- const paused = $derived(hovered || focusWithin);
70
+ const paused = $derived(hovered || held || focusWithin);
41
71
 
42
72
  let intervalRef: ReturnType<typeof setInterval> | undefined;
43
73
 
44
- const expandableText = $derived((toast.detail ?? toast.message).trim());
74
+ const texts = $derived(toastTexts(toast, strings));
75
+ const title = $derived(texts.title);
76
+ const message = $derived(texts.message);
77
+ const headerTitle = $derived(texts.headerTitle);
78
+ const secondLine = $derived(texts.secondLine);
45
79
 
80
+ /**
81
+ * The part that opens and closes is `detail`, when the toast was given
82
+ * one: the long version. The message is never folded away.
83
+ */
84
+ const expandableText = $derived((toast.detail ?? "").trim());
46
85
  const hasExpandable = $derived(expandableText.length > 0);
47
86
 
48
- const headerTitle = $derived.by(() => {
49
- const t = toast.title?.trim();
50
- if (t) return t;
51
- switch (toast.type) {
52
- case 'success':
53
- return 'Changes saved';
54
- case 'error':
55
- return 'Something went wrong';
56
- case 'warning':
57
- return 'Please review';
58
- default:
59
- return 'Notice';
60
- }
87
+ /**
88
+ * Seconds until the toast closes by itself; `0` is never. What the app
89
+ * said for this toast wins. Otherwise it goes by what the toast is and by
90
+ * how much there is to read in it: the two lines that are shown, not the
91
+ * detail behind its button.
92
+ */
93
+ const maxSeconds = $derived.by(() => {
94
+ const ms = resolveToastDuration(
95
+ {
96
+ duration: toast.duration,
97
+ type: toast.type,
98
+ hasAction: !!toast.action,
99
+ textLength: headerTitle.length + secondLine.length,
100
+ },
101
+ defaultDuration,
102
+ );
103
+ return ms <= 0 ? 0 : Math.max(1, Math.ceil(ms / 1000));
104
+ });
105
+
106
+ /**
107
+ * A short timed toast pushed with `closable: false` has no control at
108
+ * all. A toast that stays, or has a button of any kind to press, keeps
109
+ * its close button: there must always be a way to dismiss that is not a
110
+ * gesture.
111
+ */
112
+ const bare = $derived(
113
+ toast.closable === false &&
114
+ maxSeconds > 0 &&
115
+ !toast.action &&
116
+ !hasExpandable &&
117
+ !showCountdown &&
118
+ toast.type !== 'error',
119
+ );
120
+
121
+ /**
122
+ * The bar says "you have this long to press". A confirmation with nothing
123
+ * to press, in the snackbar, has no use for it (and WCAG 2.2.1 is about
124
+ * time to act, of which there is none), so it is left out there unless
125
+ * asked for. The stack keeps it. The toast's own setting wins over the Toaster's.
126
+ */
127
+ const drawTimeLine = $derived(toast.timeLine ?? timeLine ?? !(snackbar && bare));
128
+
129
+ /**
130
+ * What a screen reader hears is a live region that is in the page
131
+ * empty and gets its words afterwards: one that is inserted already
132
+ * filled is the pattern screen readers are least dependable on. So the
133
+ * words are put in a moment after the toast is mounted (the toast is
134
+ * still coming in then). The server's markup has them at once.
135
+ */
136
+ let spoken = $state(typeof document === 'undefined');
137
+ const SPEAK_AFTER_MS = 100;
138
+
139
+ /** The time left as words: for the sentence, or for a screen reader to find. */
140
+ const countdownText = $derived(
141
+ paused ? strings.pausedClosesIn(count) : strings.closesIn(count),
142
+ );
143
+
144
+ // Said to the app as well: `data-paused` is the same thing in the markup.
145
+ let reportedPause = false;
146
+ $effect(() => {
147
+ const now = paused;
148
+ if (now === reportedPause) return;
149
+ reportedPause = now;
150
+ onpausechange?.({ id: toast.id, paused: now });
61
151
  });
62
152
 
63
153
  const statusIconClass = $derived(
@@ -88,24 +178,34 @@
88
178
  timerActive = false;
89
179
  }
90
180
 
91
- onMount(() => {
181
+ /** (Re)starts the timer from the full time; a toast that stays has none. */
182
+ function startTimer() {
183
+ stopTimer();
92
184
  const max = maxSeconds;
93
- if (max <= 0) {
94
- return () => {};
95
- }
185
+ if (max <= 0) return;
96
186
  count = max;
97
187
  timerActive = true;
98
188
 
99
189
  intervalRef = setInterval(() => {
100
- if (paused) return;
190
+ // The state, not the `paused` derived: a tick can land while the
191
+ // toast is animating out, and reading a derived then warns
192
+ // (`derived_inert`).
193
+ if (hovered || held || focusWithin) return;
101
194
  count -= 1;
102
195
  if (count <= 0) {
103
196
  stopTimer();
104
197
  toastStore.dismiss(toast.id);
105
198
  }
106
199
  }, 1000);
200
+ }
201
+
202
+ onMount(() => {
203
+ startTimer();
204
+ const speak = setTimeout(() => (spoken = true), SPEAK_AFTER_MS);
107
205
 
108
206
  return () => {
207
+ clearTimeout(speak);
208
+ clearTimeout(settleTimer);
109
209
  if (intervalRef !== undefined) {
110
210
  clearInterval(intervalRef);
111
211
  intervalRef = undefined;
@@ -113,12 +213,207 @@
113
213
  };
114
214
  });
115
215
 
216
+ // The same id pushed again replaces the toast in place: new words, the full time again.
217
+ let shownToast: ToastItem | undefined;
218
+ $effect(() => {
219
+ const current = toast;
220
+ if (shownToast === undefined) {
221
+ shownToast = current;
222
+ return;
223
+ }
224
+ if (current === shownToast) return;
225
+ shownToast = current;
226
+ untrack(() => {
227
+ isExpanded = false;
228
+ startTimer();
229
+ });
230
+ });
231
+
116
232
  function handleDismiss() {
117
233
  toastStore.dismiss(toast.id);
118
234
  }
119
235
 
236
+ /** The toast closes even if the handler throws, unless the action opted out. */
237
+ function handleAction() {
238
+ const action = toast.action;
239
+ if (!action) return;
240
+ try {
241
+ action.onclick();
242
+ } finally {
243
+ if (action.dismissOnClick !== false) {
244
+ toastStore.dismiss(toast.id);
245
+ }
246
+ }
247
+ }
248
+
249
+ let expandButton = $state<HTMLButtonElement>();
250
+ let dismissButton = $state<HTMLButtonElement>();
251
+
252
+ /**
253
+ * Okay closes the details and goes with them. Focus moves to the button
254
+ * that opens them again, or it would fall to the page and a keyboard
255
+ * user would be at the top of it.
256
+ */
120
257
  function handleOkay() {
121
258
  isExpanded = false;
259
+ expandButton?.focus();
260
+ }
261
+
262
+ /** The stop button goes with the sentence it is in: focus stays in the toast. */
263
+ function handleStop(event: MouseEvent) {
264
+ const held = event.currentTarget === document.activeElement;
265
+ stopTimer();
266
+ if (held) dismissButton?.focus();
267
+ }
268
+
269
+ /**
270
+ * A mouse or a stylus holds the timer once it has moved over the toast,
271
+ * not when the toast arrives under it: a browser sends `pointerenter` to
272
+ * an element that appears under a pointer at rest, and a toast shown
273
+ * where the last click was would otherwise be held until the mouse is
274
+ * moved away. A finger holds it while it is on the toast.
275
+ */
276
+ function handlePointerLeave(event: PointerEvent) {
277
+ if (event.pointerType !== "touch") hovered = false;
278
+ }
279
+
280
+ function handlePointerMove(event: PointerEvent) {
281
+ if (dragging) {
282
+ drag(event);
283
+ return;
284
+ }
285
+ if (tracked) maybeStartDrag(event);
286
+ if (event.pointerType === "touch" || hovered) return;
287
+ // A move of no distance is not the pointer moving (a browser repeating
288
+ // its position after the layout changed); one with no such field is.
289
+ const moved =
290
+ typeof event.movementX !== "number" ||
291
+ Number.isNaN(event.movementX) ||
292
+ event.movementX !== 0 ||
293
+ event.movementY !== 0;
294
+ if (moved) hovered = true;
295
+ }
296
+
297
+ // Swiping away: a touch or a pen drags the toast sideways, or down in the
298
+ // snackbar (it is docked at the bottom); past 40% of its width or a flick it
299
+ // goes, otherwise it settles back. Reuses the sheets' drag constants.
300
+ let root = $state<HTMLDivElement>();
301
+ let dragX = $state(0);
302
+ let dragY = $state(0);
303
+ let dragging = $state(false);
304
+ let settling = $state(false);
305
+ let tracked = false;
306
+ let pointerId = -1;
307
+ let startX = 0;
308
+ let startY = 0;
309
+ let axis: "x" | "y" | null = null;
310
+ let swallowClick = false;
311
+ let settleTimer: ReturnType<typeof setTimeout> | undefined;
312
+ const trackX = createVelocityTracker();
313
+ const trackY = createVelocityTracker();
314
+
315
+ function handlePointerDown(event: PointerEvent) {
316
+ if (event.pointerType === "touch") held = true;
317
+ if (!swipeToDismiss || bare || event.pointerType === "mouse" || !event.isPrimary) return;
318
+ // A button keeps its own press.
319
+ if ((event.target as Element | null)?.closest?.("button, a[href], input, select, textarea")) return;
320
+ clearTimeout(settleTimer);
321
+ settling = false;
322
+ tracked = true;
323
+ pointerId = event.pointerId;
324
+ startX = event.clientX;
325
+ startY = event.clientY;
326
+ axis = null;
327
+ const time = timeOf(event);
328
+ trackX.reset(event.clientX, time);
329
+ trackY.reset(event.clientY, time);
330
+ }
331
+
332
+ function drag(event: PointerEvent) {
333
+ if (event.pointerId !== pointerId) return;
334
+ const dx = event.clientX - startX;
335
+ const dy = event.clientY - startY;
336
+ const time = timeOf(event);
337
+ trackX.add(event.clientX, time);
338
+ trackY.add(event.clientY, time);
339
+ dragX = axis === "x" ? dx : 0;
340
+ dragY = axis === "y" ? Math.max(0, dy) : 0;
341
+ }
342
+
343
+ /** Called for every move of a pointer that is down on the toast, before it is a drag. */
344
+ function maybeStartDrag(event: PointerEvent) {
345
+ if (!tracked || event.pointerId !== pointerId) return;
346
+ const dx = event.clientX - startX;
347
+ const dy = event.clientY - startY;
348
+ const time = timeOf(event);
349
+ trackX.add(event.clientX, time);
350
+ trackY.add(event.clientY, time);
351
+ if (Math.hypot(dx, dy) < DRAG_SLOP) return;
352
+ const sideways = Math.abs(dx) > Math.abs(dy);
353
+ // Up is the way a page scrolls, and in the stack there is no edge to go down to.
354
+ if (!sideways && (dy < 0 || !snackbar)) {
355
+ tracked = false;
356
+ return;
357
+ }
358
+ axis = sideways ? "x" : "y";
359
+ dragging = true;
360
+ try {
361
+ root?.setPointerCapture(event.pointerId);
362
+ } catch {
363
+ // A pointer that is gone cannot be captured; the drag simply ends.
364
+ }
365
+ drag(event);
366
+ }
367
+
368
+ function finishDrag(event: PointerEvent, cancelled: boolean) {
369
+ if (!tracked || event.pointerId !== pointerId) return;
370
+ tracked = false;
371
+ if (!dragging) return;
372
+ dragging = false;
373
+ swallowClick = true;
374
+ setTimeout(() => (swallowClick = false), 0);
375
+ const width = root?.offsetWidth || 1;
376
+ const height = root?.offsetHeight || 1;
377
+ const sideways = axis === "x";
378
+ const distance = sideways ? dragX : dragY;
379
+ const velocity = sideways ? trackX.velocity() : trackY.velocity();
380
+ const flicked = Math.abs(velocity) > FLICK_VELOCITY && Math.sign(velocity) === Math.sign(distance);
381
+ const far = Math.abs(distance) > (sideways ? width : height) * TOAST_SWIPE_DISMISS_FRACTION;
382
+ if (!cancelled && (far || flicked)) {
383
+ if (prefersReducedMotion()) {
384
+ toastStore.dismiss(toast.id);
385
+ return;
386
+ }
387
+ // Out the way it was going, then gone.
388
+ settling = true;
389
+ if (sideways) dragX = Math.sign(distance || 1) * (width + 32);
390
+ else dragY = height + 32;
391
+ settleTimer = setTimeout(() => toastStore.dismiss(toast.id), 200);
392
+ return;
393
+ }
394
+ // Back where it was.
395
+ settling = !prefersReducedMotion();
396
+ dragX = 0;
397
+ dragY = 0;
398
+ settleTimer = setTimeout(() => (settling = false), 220);
399
+ }
400
+
401
+ function handleClickCapture(event: MouseEvent) {
402
+ if (!swallowClick) return;
403
+ event.preventDefault();
404
+ event.stopPropagation();
405
+ }
406
+
407
+ /**
408
+ * The finger lifts, or the browser takes the touch for a scroll. The
409
+ * timer runs again, with time left to do something about the toast: it
410
+ * was held because someone was reading it.
411
+ */
412
+ function handlePointerRelease(event: PointerEvent) {
413
+ finishDrag(event, event.type === "pointercancel");
414
+ if (event.pointerType !== "touch" || !held) return;
415
+ held = false;
416
+ if (timerActive) count = Math.max(count, Math.min(TOAST_SECONDS_AFTER_HOLD, maxSeconds));
122
417
  }
123
418
 
124
419
  function handleFocusOut(event: FocusEvent) {
@@ -128,56 +423,157 @@
128
423
  }
129
424
  }
130
425
 
131
- const toastEnter = { y: 18, duration: 260, opacity: 0, easing: cubicOut };
132
- const toastLeave = { y: 14, duration: 300, opacity: 0, easing: cubicOut };
426
+ /** No movement where motion is unwanted: the toast appears and goes at once. */
427
+ const reducedMotion = prefersReducedMotion();
428
+ const motion = (duration: number) => (reducedMotion ? 0 : duration);
429
+
430
+ const toastEnter = { y: 18, duration: motion(260), opacity: 0, easing: cubicOut };
431
+ const toastLeave = { y: 14, duration: motion(300), opacity: 0, easing: cubicOut };
432
+
433
+ /**
434
+ * The snackbar comes out from behind the edge it is docked on and goes
435
+ * back into it: it slides down by its own height and the gap, on the
436
+ * standard curve, with no overshoot. The region clips it at that edge.
437
+ */
438
+ function dock(node: HTMLElement, leaving: boolean) {
439
+ const distance = node.offsetHeight + 24;
440
+ return {
441
+ duration: motion(leaving ? 200 : 280),
442
+ easing: easeStandard,
443
+ css: (t: number, u: number) => `transform: translateY(${u * distance}px); opacity: ${Math.min(1, t * 3)}`,
444
+ };
445
+ }
446
+
447
+ function enter(node: HTMLElement) {
448
+ return snackbar ? dock(node, false) : fly(node, toastEnter);
449
+ }
450
+
451
+ function leave(node: HTMLElement) {
452
+ // Dropped for a newer toast that is coming in where it is: gone at once.
453
+ if (toastsReplaced.delete(toast.id)) return { duration: 0 };
454
+ return snackbar ? dock(node, true) : fly(node, toastLeave);
455
+ }
456
+
457
+ /** One line of text is centred on the buttons beside it; text that wraps stays at the top. */
458
+ let textBox = $state<HTMLDivElement>();
459
+ let oneLine = $state(false);
460
+ $effect(() => {
461
+ const box = textBox;
462
+ void headerTitle;
463
+ void secondLine;
464
+ if (!box || typeof getComputedStyle !== 'function') return;
465
+ const measure = () => {
466
+ const line = parseFloat(getComputedStyle(box).lineHeight) || 24;
467
+ oneLine = box.scrollHeight <= line * 1.5;
468
+ };
469
+ measure();
470
+ if (typeof ResizeObserver !== 'function') return;
471
+ const observer = new ResizeObserver(measure);
472
+ observer.observe(box);
473
+ return () => observer.disconnect();
474
+ });
133
475
  </script>
134
476
 
477
+ <!-- Radius: the snackbar's 4px of padding round a control of 8px make 12px (outer = inner + padding). -->
135
478
  <div
136
- class={cn("pointer-events-auto relative w-full min-w-[18rem] overflow-hidden rounded-overlay border border-border-overlay bg-surface-overlay shadow-lg", className)}
137
- in:fly={toastEnter}
138
- out:fly={toastLeave}
139
- onmouseenter={() => (hovered = true)}
479
+ bind:this={root}
480
+ class={cn(
481
+ "relative w-full min-w-[min(18rem,100%)] shrink-0 overflow-hidden material-thick",
482
+ snackbar ? "rounded-container" : "rounded-overlay",
483
+ // No control in it: a tap goes through to the page.
484
+ bare ? "pointer-events-none" : "pointer-events-auto",
485
+ className,
486
+ )}
487
+ in:enter
488
+ out:leave
489
+ style:transform={dragX !== 0 || dragY !== 0 ? `translate3d(${dragX}px, ${dragY}px, 0)` : undefined}
490
+ style:opacity={dragging || settling ? Math.max(0.2, 1 - Math.hypot(dragX, dragY) / (root?.offsetWidth || 300)) : undefined}
491
+ style:transition={settling ? "transform 200ms var(--ease-standard), opacity 200ms var(--ease-standard)" : undefined}
492
+ style:touch-action={swipeToDismiss && !bare ? (snackbar ? "none" : "pan-y") : undefined}
493
+ onpointerleave={handlePointerLeave}
494
+ onpointerdown={handlePointerDown}
495
+ onpointermove={handlePointerMove}
496
+ onpointerup={handlePointerRelease}
497
+ onpointercancel={handlePointerRelease}
498
+ onclickcapture={handleClickCapture}
140
499
  onmouseleave={() => (hovered = false)}
141
500
  onfocusin={() => (focusWithin = true)}
142
501
  onfocusout={handleFocusOut}
143
502
  role="group"
144
503
  aria-label={headerTitle}
504
+ data-toast-id={toast.id}
145
505
  data-paused={paused}
146
506
  >
147
- <div class="p-4">
148
- <div class="flex items-start gap-3">
507
+ <!-- Padding and gaps in px: at 200% text on a phone, room that grew with
508
+ the text left the text itself a column too narrow for one long word. -->
509
+ <div class={snackbar ? "p-[4px] ps-[12px]" : "p-[16px]"}>
510
+ <!-- The title keeps 8rem at least, icon included. When the toast has
511
+ less beside its buttons (320px with enlarged text), the buttons go to a
512
+ line of their own under it, at the end. -->
513
+ <div class="flex flex-wrap {oneLine ? 'items-center' : 'items-start'} gap-x-[12px] gap-y-[4px] {snackbar ? 'min-h-[44px]' : ''}">
149
514
  <!-- Live region holds only title + message, so the countdown and buttons are never re-announced. -->
150
515
  <div
151
- class="flex min-w-0 flex-1 items-start gap-3"
516
+ class="flex min-w-0 flex-[1_1_8rem] items-start gap-[12px]"
152
517
  role={toast.type === 'error' ? 'alert' : 'status'}
153
518
  aria-atomic="true"
154
519
  >
155
- <div class="shrink-0 mt-0.5">
520
+ <!-- 20px at every text size, as the padding and the gap beside it
521
+ are px: an icon that grew with the text took the room the text
522
+ needed. Lowered so it stays at the middle of the first line. -->
523
+ <div class="shrink-0 mt-[calc((1.5rem_-_20px)_/_2)]">
156
524
  {#if toast.type === 'success'}
157
- <CheckCircle class="size-5 {statusIconClass}" aria-hidden="true" />
525
+ <CheckCircle class="size-[20px] {statusIconClass}" aria-hidden="true" />
158
526
  {:else if toast.type === 'error'}
159
- <AlertCircle class="size-5 {statusIconClass}" aria-hidden="true" />
527
+ <AlertCircle class="size-[20px] {statusIconClass}" aria-hidden="true" />
160
528
  {:else if toast.type === 'warning'}
161
- <AlertTriangle class="size-5 {statusIconClass}" aria-hidden="true" />
529
+ <AlertTriangle class="size-[20px] {statusIconClass}" aria-hidden="true" />
162
530
  {:else}
163
- <Info class="size-5 {statusIconClass}" aria-hidden="true" />
531
+ <Info class="size-[20px] {statusIconClass}" aria-hidden="true" />
164
532
  {/if}
165
533
  </div>
166
- <h4 class="min-w-0 flex-1 text-base font-semibold text-headline">
167
- {headerTitle}
168
- {#if toast.message.trim() && toast.message.trim() !== headerTitle}
169
- <span class="sr-only">{toast.message}</span>
534
+ <div class="min-w-0 flex-1" bind:this={textBox}>
535
+ <!-- A heading when the app gave the toast a title (or nothing
536
+ at all); a message on its own is a sentence, not a heading. -->
537
+ {#if title || !message}
538
+ <h4 class="text-base font-semibold text-headline [overflow-wrap:anywhere]">
539
+ {spoken ? headerTitle : "\u00a0"}
540
+ </h4>
541
+ {:else}
542
+ <p class="text-base font-medium text-headline [overflow-wrap:anywhere]" data-toast-message>
543
+ {spoken ? headerTitle : "\u00a0"}
544
+ </p>
545
+ {/if}
546
+ {#if secondLine}
547
+ <p class="mt-1 text-sm text-description [overflow-wrap:anywhere]" data-toast-message>
548
+ {spoken ? secondLine : "\u00a0"}
549
+ </p>
170
550
  {/if}
171
- </h4>
551
+ {#if toast.action && spoken}
552
+ <!-- The button sits outside the live region, so this is how a screen reader user hears that there is one. -->
553
+ <span class="sr-only">{strings.actionAvailable(toast.action.label)}</span>
554
+ {/if}
555
+ </div>
172
556
  </div>
173
- <div class="flex shrink-0 items-center gap-1">
557
+ {#if !bare}
558
+ <div class="ms-auto flex shrink-0 items-center gap-1">
559
+ {#if snackbar && toast.action}
560
+ <Button
561
+ variant="text"
562
+ size="sm"
563
+ text={toast.action.label}
564
+ type="button"
565
+ data-toast-action
566
+ onclick={handleAction}
567
+ />
568
+ {/if}
174
569
  {#if hasExpandable}
175
570
  <button
176
571
  type="button"
177
- class="focus-ring cursor-pointer rounded-control p-1 text-description transition-colors hover:bg-surface-overlay-hover hover:text-headline focus:outline-none"
572
+ 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"
178
573
  aria-expanded={isExpanded}
179
- aria-controls="toaster-expand-{toast.id}"
180
- aria-label={isExpanded ? 'Collapse details' : 'Expand details'}
574
+ aria-controls={isExpanded ? `toaster-expand-${toast.id}` : undefined}
575
+ aria-label={isExpanded ? strings.collapse : strings.expand}
576
+ bind:this={expandButton}
181
577
  onclick={() => (isExpanded = !isExpanded)}
182
578
  >
183
579
  <ChevronDown
@@ -190,26 +586,42 @@
190
586
  {/if}
191
587
  <button
192
588
  type="button"
193
- class="focus-ring cursor-pointer rounded-control p-1 text-description transition-colors hover:bg-surface-overlay-hover hover:text-headline focus:outline-none"
589
+ 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"
194
590
  onclick={handleDismiss}
195
- aria-label="Dismiss notification"
591
+ aria-label={strings.dismiss}
592
+ bind:this={dismissButton}
196
593
  >
197
594
  <X class="size-5" aria-hidden="true" />
198
595
  </button>
199
596
  </div>
597
+ {/if}
200
598
  </div>
201
599
 
600
+ {#if toast.action && !snackbar}
601
+ <!-- Indented to the title: past the 20px status icon and its 12px gap. -->
602
+ <div class="mt-3 pl-[32px]">
603
+ <Button
604
+ variant="secondary"
605
+ size="sm"
606
+ text={toast.action.label}
607
+ type="button"
608
+ data-toast-action
609
+ onclick={handleAction}
610
+ />
611
+ </div>
612
+ {/if}
613
+
202
614
  {#if hasExpandable && isExpanded}
203
615
  <div
204
616
  id="toaster-expand-{toast.id}"
205
617
  class="mt-3"
206
- transition:slide={{ duration: 200 }}
618
+ transition:slide={{ duration: motion(200) }}
207
619
  >
208
620
  <p class="text-sm text-description">{expandableText}</p>
209
621
  <Button
210
622
  variant="primary"
211
623
  size="sm"
212
- text="Okay"
624
+ text={strings.okay}
213
625
  type="button"
214
626
  class="mt-3"
215
627
  onclick={handleOkay}
@@ -218,29 +630,35 @@
218
630
  {/if}
219
631
  </div>
220
632
 
221
- {#if timerActive}
222
- <div class="px-4 pb-3" transition:fade={{ duration: 180 }}>
633
+ {#if timerActive && showCountdown}
634
+ <div class="px-[16px] pb-3" transition:fade={{ duration: motion(180) }}>
223
635
  <p class="text-xs text-description" data-toast-countdown>
224
- {paused ? 'Paused — closes' : 'This message will close'} in {count} seconds.
636
+ {countdownText}
225
637
  <button
226
638
  type="button"
227
- class="focus-ring cursor-pointer rounded-control text-link underline-offset-2 hover:underline focus:outline-none"
228
- onclick={stopTimer}
639
+ 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]"
640
+ onclick={handleStop}
229
641
  >
230
- Click to stop
642
+ {strings.stop}
231
643
  </button>
232
644
  </p>
233
645
  </div>
646
+ {:else if timerActive}
647
+ <!-- The bar below shows the time left to the eye. For a screen reader
648
+ it is here as words: outside the live region, so it is found when the
649
+ toast is read and is not spoken again every second. -->
650
+ <p class="sr-only" data-toast-countdown>{countdownText}</p>
234
651
  {/if}
235
652
 
236
- {#if timerActive}
653
+ {#if timerActive && drawTimeLine}
237
654
  <div
238
- class="pointer-events-none absolute bottom-0 left-0 right-0 h-1 bg-border/70"
239
- transition:fade={{ duration: 180 }}
655
+ class="pointer-events-none absolute bottom-0 left-0 right-0 h-1 bg-border/70 forced-colors:outline-1 forced-colors:-outline-offset-1 forced-colors:outline-[color:CanvasText]"
656
+ data-toast-timeline
657
+ transition:fade={{ duration: motion(180) }}
240
658
  aria-hidden="true"
241
659
  >
242
660
  <div
243
- class="h-full {progressFillClass} transition-all duration-1000 ease-linear"
661
+ class="h-full {progressFillClass} forced-colors:bg-[color:Highlight]! transition-all duration-1000 ease-linear"
244
662
  style="width: {(count / maxSeconds) * 100}%"
245
663
  ></div>
246
664
  </div>