zabi-components 8.0.0 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (364) hide show
  1. package/CHANGELOG.md +1924 -0
  2. package/README.md +243 -10
  3. package/THEME.md +149 -332
  4. package/THEMING.md +1192 -266
  5. package/dist/atoms/ActionPanel.svelte +15 -5
  6. package/dist/atoms/Avatar.svelte +104 -0
  7. package/dist/atoms/Avatar.svelte.d.ts +34 -0
  8. package/dist/atoms/Badge.svelte +36 -7
  9. package/dist/atoms/Badge.svelte.d.ts +2 -2
  10. package/dist/atoms/Button.svelte +177 -27
  11. package/dist/atoms/Button.svelte.d.ts +30 -2
  12. package/dist/atoms/Card.svelte +4 -4
  13. package/dist/atoms/CardHeader.svelte +16 -4
  14. package/dist/atoms/Checkbox.svelte +48 -21
  15. package/dist/atoms/Checkbox.svelte.d.ts +4 -2
  16. package/dist/atoms/CodeBlock.svelte +20 -2
  17. package/dist/atoms/CodeBlock.svelte.d.ts +4 -0
  18. package/dist/atoms/ColorPicker.svelte +259 -49
  19. package/dist/atoms/ColorPicker.svelte.d.ts +3 -0
  20. package/dist/atoms/Container.svelte +16 -8
  21. package/dist/atoms/DateField.svelte +107 -0
  22. package/dist/atoms/DateField.svelte.d.ts +53 -0
  23. package/dist/atoms/FieldMessages.svelte +69 -0
  24. package/dist/atoms/FieldMessages.svelte.d.ts +23 -0
  25. package/dist/atoms/FloatingActionButton.svelte +174 -0
  26. package/dist/atoms/FloatingActionButton.svelte.d.ts +51 -0
  27. package/dist/atoms/Heading.svelte +37 -5
  28. package/dist/atoms/Heading.svelte.d.ts +7 -0
  29. package/dist/atoms/IconButton.svelte +220 -24
  30. package/dist/atoms/IconButton.svelte.d.ts +49 -4
  31. package/dist/atoms/Input.svelte +253 -52
  32. package/dist/atoms/Input.svelte.d.ts +32 -2
  33. package/dist/atoms/List.svelte +4 -1
  34. package/dist/atoms/ListItem.svelte +44 -11
  35. package/dist/atoms/ListItem.svelte.d.ts +5 -0
  36. package/dist/atoms/Progress.svelte +27 -8
  37. package/dist/atoms/Progress.svelte.d.ts +4 -0
  38. package/dist/atoms/Radio.svelte +19 -3
  39. package/dist/atoms/Radio.svelte.d.ts +4 -2
  40. package/dist/atoms/Rating.svelte +494 -0
  41. package/dist/atoms/Rating.svelte.d.ts +56 -0
  42. package/dist/atoms/Select.svelte +433 -91
  43. package/dist/atoms/Select.svelte.d.ts +49 -3
  44. package/dist/atoms/SelectionControl.svelte +94 -17
  45. package/dist/atoms/SelectionControl.svelte.d.ts +4 -3
  46. package/dist/atoms/Skeleton.svelte +1 -1
  47. package/dist/atoms/Slider.svelte +309 -0
  48. package/dist/atoms/Slider.svelte.d.ts +40 -0
  49. package/dist/atoms/Spinner.svelte +57 -0
  50. package/dist/atoms/Spinner.svelte.d.ts +19 -0
  51. package/dist/atoms/Table.svelte +151 -6
  52. package/dist/atoms/Table.svelte.d.ts +15 -3
  53. package/dist/atoms/TemporalField.svelte +140 -0
  54. package/dist/atoms/TemporalField.svelte.d.ts +32 -0
  55. package/dist/atoms/Text.svelte +23 -11
  56. package/dist/atoms/Text.svelte.d.ts +6 -3
  57. package/dist/atoms/Textarea.svelte +55 -44
  58. package/dist/atoms/Textarea.svelte.d.ts +14 -2
  59. package/dist/atoms/ThemeToggle.svelte +223 -44
  60. package/dist/atoms/ThemeToggle.svelte.d.ts +26 -3
  61. package/dist/atoms/TimeField.svelte +108 -0
  62. package/dist/atoms/TimeField.svelte.d.ts +54 -0
  63. package/dist/atoms/Toast.svelte +17 -4
  64. package/dist/atoms/Toast.svelte.d.ts +2 -0
  65. package/dist/atoms/Toggle.svelte +41 -8
  66. package/dist/atoms/Toggle.svelte.d.ts +11 -2
  67. package/dist/atoms/Tooltip.svelte +508 -23
  68. package/dist/atoms/Tooltip.svelte.d.ts +11 -0
  69. package/dist/atoms/ZabiStringsProvider.svelte +45 -0
  70. package/dist/atoms/ZabiStringsProvider.svelte.d.ts +27 -0
  71. package/dist/atoms/index.d.ts +17 -0
  72. package/dist/atoms/index.js +12 -0
  73. package/dist/atoms/selection-control.styles.d.ts +36 -5
  74. package/dist/atoms/selection-control.styles.js +40 -9
  75. package/dist/components/atoms/index.d.ts +17 -0
  76. package/dist/components/atoms/index.d.ts.map +1 -1
  77. package/dist/components/atoms/selection-control.styles.d.ts +36 -5
  78. package/dist/components/atoms/selection-control.styles.d.ts.map +1 -1
  79. package/dist/components/index.d.ts +80 -2
  80. package/dist/components/index.d.ts.map +1 -1
  81. package/dist/components/molecules/index.d.ts +46 -0
  82. package/dist/components/molecules/index.d.ts.map +1 -1
  83. package/dist/components/molecules/toast-store.d.ts +53 -7
  84. package/dist/components/molecules/toast-store.d.ts.map +1 -1
  85. package/dist/components/organisms/index.d.ts +5 -0
  86. package/dist/components/organisms/index.d.ts.map +1 -1
  87. package/dist/components/types/variants.d.ts +15 -1
  88. package/dist/components/types/variants.d.ts.map +1 -1
  89. package/dist/components/util/app-shell.d.ts +28 -0
  90. package/dist/components/util/app-shell.d.ts.map +1 -0
  91. package/dist/components/util/avatar.d.ts +36 -0
  92. package/dist/components/util/avatar.d.ts.map +1 -0
  93. package/dist/components/util/bottom-sheet.d.ts +30 -0
  94. package/dist/components/util/bottom-sheet.d.ts.map +1 -0
  95. package/dist/components/util/bottom-tab-bar.d.ts +35 -0
  96. package/dist/components/util/bottom-tab-bar.d.ts.map +1 -0
  97. package/dist/components/util/calendar.d.ts +56 -0
  98. package/dist/components/util/calendar.d.ts.map +1 -0
  99. package/dist/components/util/collapsible.d.ts +58 -0
  100. package/dist/components/util/collapsible.d.ts.map +1 -0
  101. package/dist/components/util/confirm-dialog.d.ts +12 -0
  102. package/dist/components/util/confirm-dialog.d.ts.map +1 -0
  103. package/dist/components/util/date.d.ts +96 -0
  104. package/dist/components/util/date.d.ts.map +1 -0
  105. package/dist/components/util/drawer.d.ts +13 -0
  106. package/dist/components/util/drawer.d.ts.map +1 -0
  107. package/dist/components/util/dropdown.d.ts +47 -0
  108. package/dist/components/util/dropdown.d.ts.map +1 -0
  109. package/dist/components/util/field.d.ts +39 -0
  110. package/dist/components/util/field.d.ts.map +1 -0
  111. package/dist/components/util/fit-in-viewport.d.ts +201 -0
  112. package/dist/components/util/fit-in-viewport.d.ts.map +1 -0
  113. package/dist/components/util/focus-utils.d.ts +75 -2
  114. package/dist/components/util/focus-utils.d.ts.map +1 -1
  115. package/dist/components/util/hydration.d.ts +45 -0
  116. package/dist/components/util/hydration.d.ts.map +1 -0
  117. package/dist/components/util/keyboard-inset.d.ts +18 -0
  118. package/dist/components/util/keyboard-inset.d.ts.map +1 -0
  119. package/dist/components/util/media-grid.d.ts +51 -0
  120. package/dist/components/util/media-grid.d.ts.map +1 -0
  121. package/dist/components/util/media.d.ts +6 -0
  122. package/dist/components/util/media.d.ts.map +1 -0
  123. package/dist/components/util/overlay-chrome.d.ts +53 -0
  124. package/dist/components/util/overlay-chrome.d.ts.map +1 -0
  125. package/dist/components/util/overlay.d.ts +72 -0
  126. package/dist/components/util/overlay.d.ts.map +1 -0
  127. package/dist/components/util/photo.d.ts +215 -0
  128. package/dist/components/util/photo.d.ts.map +1 -0
  129. package/dist/components/util/portal.d.ts +23 -0
  130. package/dist/components/util/portal.d.ts.map +1 -0
  131. package/dist/components/util/pull-to-refresh.d.ts +27 -0
  132. package/dist/components/util/pull-to-refresh.d.ts.map +1 -0
  133. package/dist/components/util/radio-keys.d.ts +25 -0
  134. package/dist/components/util/radio-keys.d.ts.map +1 -0
  135. package/dist/components/util/rating.d.ts +23 -0
  136. package/dist/components/util/rating.d.ts.map +1 -0
  137. package/dist/components/util/ready-made-strings.d.ts +77 -0
  138. package/dist/components/util/ready-made-strings.d.ts.map +1 -0
  139. package/dist/components/util/scroll-padding.d.ts +20 -0
  140. package/dist/components/util/scroll-padding.d.ts.map +1 -0
  141. package/dist/components/util/segmented-control.d.ts +16 -0
  142. package/dist/components/util/segmented-control.d.ts.map +1 -0
  143. package/dist/components/util/select.d.ts +41 -0
  144. package/dist/components/util/select.d.ts.map +1 -0
  145. package/dist/components/util/sheet-drag.d.ts +60 -0
  146. package/dist/components/util/sheet-drag.d.ts.map +1 -0
  147. package/dist/components/util/sidebar.d.ts +63 -0
  148. package/dist/components/util/sidebar.d.ts.map +1 -0
  149. package/dist/components/util/sortable-list.d.ts +65 -0
  150. package/dist/components/util/sortable-list.d.ts.map +1 -0
  151. package/dist/components/util/stepper.d.ts +40 -0
  152. package/dist/components/util/stepper.d.ts.map +1 -0
  153. package/dist/components/util/swipeable-list-item.d.ts +36 -0
  154. package/dist/components/util/swipeable-list-item.d.ts.map +1 -0
  155. package/dist/components/util/theme-mode.d.ts +80 -0
  156. package/dist/components/util/theme-mode.d.ts.map +1 -0
  157. package/dist/components/util/toaster.d.ts +93 -0
  158. package/dist/components/util/toaster.d.ts.map +1 -0
  159. package/dist/components/util/tooltip.d.ts +10 -0
  160. package/dist/components/util/tooltip.d.ts.map +1 -0
  161. package/dist/components/util/top-navbar.d.ts +11 -0
  162. package/dist/components/util/top-navbar.d.ts.map +1 -0
  163. package/dist/components/util/touch-target.d.ts +31 -0
  164. package/dist/components/util/touch-target.d.ts.map +1 -0
  165. package/dist/components/util/zabi-strings.d.ts +133 -0
  166. package/dist/components/util/zabi-strings.d.ts.map +1 -0
  167. package/dist/create-theme/cli.js +169 -0
  168. package/dist/create-theme/index.d.ts +176 -0
  169. package/dist/create-theme/index.js +817 -0
  170. package/dist/create-theme/lib/ramp-math.js +128 -0
  171. package/dist/create-theme/lib/resolve.js +141 -0
  172. package/dist/create-theme/theme-data.js +1116 -0
  173. package/dist/index.d.ts +80 -2
  174. package/dist/index.js +57 -2
  175. package/dist/molecules/Alert.svelte +33 -5
  176. package/dist/molecules/Alert.svelte.d.ts +2 -0
  177. package/dist/molecules/AppBar.svelte +415 -0
  178. package/dist/molecules/AppBar.svelte.d.ts +69 -0
  179. package/dist/molecules/AvatarGroup.svelte +116 -0
  180. package/dist/molecules/AvatarGroup.svelte.d.ts +40 -0
  181. package/dist/molecules/BottomSheet.svelte +540 -0
  182. package/dist/molecules/BottomSheet.svelte.d.ts +93 -0
  183. package/dist/molecules/BottomTabBar.svelte +297 -0
  184. package/dist/molecules/BottomTabBar.svelte.d.ts +64 -0
  185. package/dist/molecules/Calendar.svelte +597 -0
  186. package/dist/molecules/Calendar.svelte.d.ts +72 -0
  187. package/dist/molecules/Collapsible.svelte +243 -0
  188. package/dist/molecules/Collapsible.svelte.d.ts +44 -0
  189. package/dist/molecules/CollapsibleGroup.svelte +126 -0
  190. package/dist/molecules/CollapsibleGroup.svelte.d.ts +14 -0
  191. package/dist/molecules/ComponentDemo.svelte +16 -4
  192. package/dist/molecules/ComponentDemo.svelte.d.ts +3 -0
  193. package/dist/molecules/ConfirmDialog.svelte +253 -0
  194. package/dist/molecules/ConfirmDialog.svelte.d.ts +55 -0
  195. package/dist/molecules/ContactForm.svelte +34 -17
  196. package/dist/molecules/ContactForm.svelte.d.ts +8 -0
  197. package/dist/molecules/Drawer.svelte +394 -0
  198. package/dist/molecules/Drawer.svelte.d.ts +55 -0
  199. package/dist/molecules/Dropdown.svelte +509 -48
  200. package/dist/molecules/Dropdown.svelte.d.ts +45 -6
  201. package/dist/molecules/DropdownItem.svelte +122 -0
  202. package/dist/molecules/DropdownItem.svelte.d.ts +28 -0
  203. package/dist/molecules/EmptyState.svelte +55 -10
  204. package/dist/molecules/EmptyState.svelte.d.ts +8 -0
  205. package/dist/molecules/FormField.svelte +9 -1
  206. package/dist/molecules/FormField.svelte.d.ts +2 -0
  207. package/dist/molecules/Header.svelte +1 -1
  208. package/dist/molecules/ImageUpload.svelte +383 -70
  209. package/dist/molecules/ImageUpload.svelte.d.ts +47 -2
  210. package/dist/molecules/MediaGrid.svelte +527 -0
  211. package/dist/molecules/MediaGrid.svelte.d.ts +77 -0
  212. package/dist/molecules/Modal.svelte +288 -72
  213. package/dist/molecules/Modal.svelte.d.ts +51 -2
  214. package/dist/molecules/NavigationMenuContent.svelte +58 -1
  215. package/dist/molecules/NavigationMenuItem.svelte +0 -1
  216. package/dist/molecules/NavigationMenuLink.svelte +1 -1
  217. package/dist/molecules/NavigationMenuList.svelte +10 -3
  218. package/dist/molecules/NavigationMenuTrigger.svelte +2 -2
  219. package/dist/molecules/Page.svelte +32 -2
  220. package/dist/molecules/Page.svelte.d.ts +11 -0
  221. package/dist/molecules/PhotoGrid.svelte +433 -0
  222. package/dist/molecules/PhotoGrid.svelte.d.ts +62 -0
  223. package/dist/molecules/PhotoViewer.svelte +1116 -0
  224. package/dist/molecules/PhotoViewer.svelte.d.ts +56 -0
  225. package/dist/molecules/PropsTable.svelte +22 -10
  226. package/dist/molecules/PropsTable.svelte.d.ts +3 -0
  227. package/dist/molecules/PullToRefresh.svelte +263 -0
  228. package/dist/molecules/PullToRefresh.svelte.d.ts +52 -0
  229. package/dist/molecules/RadioGroup.svelte +52 -14
  230. package/dist/molecules/RadioGroup.svelte.d.ts +14 -3
  231. package/dist/molecules/Section.svelte +1 -1
  232. package/dist/molecules/SegmentedControl.svelte +323 -0
  233. package/dist/molecules/SegmentedControl.svelte.d.ts +34 -0
  234. package/dist/molecules/SidebarBrandHeader.svelte +11 -1
  235. package/dist/molecules/SidebarBrandHeader.svelte.d.ts +3 -0
  236. package/dist/molecules/SidebarFooter.svelte +29 -6
  237. package/dist/molecules/SidebarFooter.svelte.d.ts +3 -0
  238. package/dist/molecules/SlideUp.svelte +220 -61
  239. package/dist/molecules/SlideUp.svelte.d.ts +26 -0
  240. package/dist/molecules/SortableList.svelte +632 -0
  241. package/dist/molecules/SortableList.svelte.d.ts +69 -0
  242. package/dist/molecules/Stepper.svelte +548 -0
  243. package/dist/molecules/Stepper.svelte.d.ts +39 -0
  244. package/dist/molecules/StickyActionBar.svelte +191 -0
  245. package/dist/molecules/StickyActionBar.svelte.d.ts +65 -0
  246. package/dist/molecules/SwipeableListItem.svelte +334 -0
  247. package/dist/molecules/SwipeableListItem.svelte.d.ts +54 -0
  248. package/dist/molecules/Tabs.svelte +233 -9
  249. package/dist/molecules/Tabs.svelte.d.ts +7 -0
  250. package/dist/molecules/Toaster.svelte +350 -5
  251. package/dist/molecules/Toaster.svelte.d.ts +95 -2
  252. package/dist/molecules/ToasterToast.svelte +253 -67
  253. package/dist/molecules/ToasterToast.svelte.d.ts +9 -0
  254. package/dist/molecules/UnsavedChangesBar.svelte +212 -0
  255. package/dist/molecules/UnsavedChangesBar.svelte.d.ts +43 -0
  256. package/dist/molecules/index.d.ts +46 -0
  257. package/dist/molecules/index.js +27 -0
  258. package/dist/molecules/toast-store.d.ts +53 -7
  259. package/dist/molecules/toast-store.js +21 -0
  260. package/dist/organisms/AppShell.svelte +196 -0
  261. package/dist/organisms/AppShell.svelte.d.ts +61 -0
  262. package/dist/organisms/SidebarAccountPanel.svelte +107 -12
  263. package/dist/organisms/SidebarAccountPanel.svelte.d.ts +19 -0
  264. package/dist/organisms/SidebarNavigation.svelte +96 -15
  265. package/dist/organisms/SidebarNavigation.svelte.d.ts +29 -1
  266. package/dist/organisms/SidebarPanel.svelte +31 -10
  267. package/dist/organisms/SidebarShell.svelte +162 -11
  268. package/dist/organisms/SidebarShell.svelte.d.ts +56 -2
  269. package/dist/organisms/TopNavbar.svelte +256 -31
  270. package/dist/organisms/TopNavbar.svelte.d.ts +26 -0
  271. package/dist/organisms/index.d.ts +5 -0
  272. package/dist/organisms/index.js +3 -0
  273. package/dist/types/components.d.ts +4 -0
  274. package/dist/types/components.d.ts.map +1 -1
  275. package/dist/types/index.d.ts +605 -25
  276. package/dist/types/index.d.ts.map +1 -1
  277. package/dist/types/variants.ts +17 -1
  278. package/dist/util/app-shell.d.ts +27 -0
  279. package/dist/util/app-shell.js +72 -0
  280. package/dist/util/avatar.d.ts +35 -0
  281. package/dist/util/avatar.js +52 -0
  282. package/dist/util/bottom-sheet.d.ts +29 -0
  283. package/dist/util/bottom-sheet.js +43 -0
  284. package/dist/util/bottom-tab-bar.d.ts +34 -0
  285. package/dist/util/bottom-tab-bar.js +54 -0
  286. package/dist/util/calendar.d.ts +55 -0
  287. package/dist/util/calendar.js +74 -0
  288. package/dist/util/collapsible.d.ts +57 -0
  289. package/dist/util/collapsible.js +23 -0
  290. package/dist/util/confirm-dialog.d.ts +11 -0
  291. package/dist/util/confirm-dialog.js +2 -0
  292. package/dist/util/date.d.ts +95 -0
  293. package/dist/util/date.js +237 -0
  294. package/dist/util/drawer.d.ts +12 -0
  295. package/dist/util/drawer.js +7 -0
  296. package/dist/util/dropdown.d.ts +46 -0
  297. package/dist/util/dropdown.js +21 -0
  298. package/dist/util/field.d.ts +38 -0
  299. package/dist/util/field.js +38 -0
  300. package/dist/util/fit-in-viewport.d.ts +200 -0
  301. package/dist/util/fit-in-viewport.js +399 -0
  302. package/dist/util/focus-utils.d.ts +75 -2
  303. package/dist/util/focus-utils.js +182 -9
  304. package/dist/util/hydration.d.ts +44 -0
  305. package/dist/util/hydration.js +58 -0
  306. package/dist/util/keyboard-inset.d.ts +17 -0
  307. package/dist/util/keyboard-inset.js +53 -0
  308. package/dist/util/media-grid.d.ts +50 -0
  309. package/dist/util/media-grid.js +81 -0
  310. package/dist/util/media.d.ts +5 -0
  311. package/dist/util/media.js +10 -0
  312. package/dist/util/overlay-chrome.d.ts +52 -0
  313. package/dist/util/overlay-chrome.js +56 -0
  314. package/dist/util/overlay.d.ts +71 -0
  315. package/dist/util/overlay.js +240 -0
  316. package/dist/util/photo.d.ts +214 -0
  317. package/dist/util/photo.js +206 -0
  318. package/dist/util/portal.d.ts +22 -0
  319. package/dist/util/portal.js +50 -0
  320. package/dist/util/pull-to-refresh.d.ts +26 -0
  321. package/dist/util/pull-to-refresh.js +30 -0
  322. package/dist/util/radio-keys.d.ts +24 -0
  323. package/dist/util/radio-keys.js +49 -0
  324. package/dist/util/rating.d.ts +22 -0
  325. package/dist/util/rating.js +26 -0
  326. package/dist/util/ready-made-strings.d.ts +76 -0
  327. package/dist/util/ready-made-strings.js +67 -0
  328. package/dist/util/scroll-padding.d.ts +19 -0
  329. package/dist/util/scroll-padding.js +44 -0
  330. package/dist/util/segmented-control.d.ts +15 -0
  331. package/dist/util/segmented-control.js +1 -0
  332. package/dist/util/select.d.ts +40 -0
  333. package/dist/util/select.js +63 -0
  334. package/dist/util/sheet-drag.d.ts +59 -0
  335. package/dist/util/sheet-drag.js +241 -0
  336. package/dist/util/sidebar.d.ts +62 -0
  337. package/dist/util/sidebar.js +38 -0
  338. package/dist/util/sortable-list.d.ts +64 -0
  339. package/dist/util/sortable-list.js +53 -0
  340. package/dist/util/stepper.d.ts +39 -0
  341. package/dist/util/stepper.js +38 -0
  342. package/dist/util/swipeable-list-item.d.ts +35 -0
  343. package/dist/util/swipeable-list-item.js +32 -0
  344. package/dist/util/theme-mode.d.ts +79 -0
  345. package/dist/util/theme-mode.js +121 -0
  346. package/dist/util/toaster.d.ts +92 -0
  347. package/dist/util/toaster.js +75 -0
  348. package/dist/util/tooltip.d.ts +9 -0
  349. package/dist/util/tooltip.js +18 -0
  350. package/dist/util/top-navbar.d.ts +10 -0
  351. package/dist/util/top-navbar.js +5 -0
  352. package/dist/util/touch-target.d.ts +30 -0
  353. package/dist/util/touch-target.js +30 -0
  354. package/dist/util/zabi-strings.d.ts +132 -0
  355. package/dist/util/zabi-strings.js +105 -0
  356. package/dist/zabi-components-colors.css +5 -2
  357. package/dist/zabi-components-theme-dark-only.css +543 -241
  358. package/dist/zabi-components-theme-dark.css +543 -241
  359. package/dist/zabi-components-theme-only.css +1584 -367
  360. package/dist/zabi-components-theme.css +1584 -367
  361. package/dist/zabi-components.css +3885 -518
  362. package/docs/lucide-icons.md +40 -17
  363. package/docs/theme-imports.md +81 -11
  364. package/package.json +35 -8
@@ -0,0 +1,415 @@
1
+ <script lang="ts">
2
+ import { zabiCommonStrings } from "../util/zabi-strings.js";
3
+ import type { Snippet } from "svelte";
4
+ import type { HTMLAttributes } from "svelte/elements";
5
+ import ArrowLeft from "@lucide/svelte/icons/arrow-left";
6
+ import { findScrollParent } from "../util/app-shell.js";
7
+ import { cn } from "../util/cn.js";
8
+
9
+ /**
10
+ * The bar at the top of a phone screen: where you are, the way back, and
11
+ * at most two things to do here.
12
+ *
13
+ * It is one row, 56px tall, whenever the title has 72px or more beside
14
+ * the controls, at every text size. The bar is chrome: when the reader
15
+ * enlarges text, the room belongs to the page under it. So the controls
16
+ * are sized in px (48px targets), the title grows with the text only up
17
+ * to 1.3 times its size, and a title too long for the room it has is cut
18
+ * with an ellipsis (after two lines with `titleLines={2}`). The heading
19
+ * keeps its full text for a screen reader; keep the visible start of it
20
+ * enough to name the screen.
21
+ *
22
+ * The title never has less than 72px. Where the back control, `leading`
23
+ * and the actions leave it less (a wide chip in `leading`, a very narrow
24
+ * bar), it takes a second row of its own, as wide as the bar, and they
25
+ * stay together on the first.
26
+ */
27
+ type Props = Omit<HTMLAttributes<HTMLElement>, "class" | "title"> & {
28
+ /** The name of the screen, as a heading. */
29
+ title?: string;
30
+ /** Heading level of the title. 1 when the bar names the page. */
31
+ headingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
32
+ /** Shows the back control as a link to this address. */
33
+ backHref?: string;
34
+ /**
35
+ * Called when the back control is activated. On its own it makes the
36
+ * control a button; with `backHref` it runs before the link is
37
+ * followed, and `event.preventDefault()` stops that.
38
+ */
39
+ onback?: (event: MouseEvent) => void;
40
+ /** Accessible name of the back control. */
41
+ backLabel?: string;
42
+ /**
43
+ * Hides the bar while the page scrolls down and brings it back as
44
+ * soon as it scrolls up. It follows the nearest scrolling ancestor
45
+ * (the one in `AppShell`), or the window. The bar never hides while
46
+ * keyboard focus is inside it, and focus arriving from the keyboard
47
+ * brings it back. Focus left behind by a tap or a click does not hold
48
+ * it: it hides as usual, and the next key press brings it back.
49
+ */
50
+ collapseOnScroll?: boolean;
51
+ /**
52
+ * How many lines the title may take before it is cut with an
53
+ * ellipsis. The bar is taller only while the second line is in use.
54
+ */
55
+ titleLines?: 1 | 2;
56
+ /**
57
+ * Before the title, after the back control: a logo, an avatar, a
58
+ * chip. Rendered as it is, a child of the bar's row. It keeps its
59
+ * width; when that leaves the title under 72px the title moves to a
60
+ * row of its own, and a `leading` wider than the row is held to the
61
+ * row's width.
62
+ */
63
+ leading?: Snippet;
64
+ /**
65
+ * After the title. At most two icon buttons, at `size="lg"` so each is
66
+ * a 48px target; put anything more in a menu. Their size does not
67
+ * grow with the text size here: inside the bar the spacing scale is
68
+ * in px.
69
+ */
70
+ actions?: Snippet;
71
+ class?: string;
72
+ };
73
+
74
+ let {
75
+ title = "",
76
+ headingLevel = 1,
77
+ backHref,
78
+ onback,
79
+ backLabel: backLabelGiven,
80
+ collapseOnScroll = false,
81
+ titleLines = 1,
82
+ leading,
83
+ actions,
84
+ class: className = "",
85
+ ...restProps
86
+ }: Props = $props();
87
+
88
+ /** Words many components share: a `ZabiStringsProvider` above this one may give them; else English. */
89
+ const common = zabiCommonStrings();
90
+ const backLabel = $derived(backLabelGiven ?? common().back);
91
+
92
+ let host: HTMLElement | undefined = $state();
93
+ /** Scrolled out of view. Only ever true with `collapseOnScroll`. */
94
+ let scrolledAway = $state(false);
95
+ const collapsed = $derived(collapseOnScroll && scrolledAway);
96
+
97
+ /** Scrolling less than this in one direction does not move the bar. */
98
+ const SCROLL_TOLERANCE = 8;
99
+
100
+ $effect(() => {
101
+ if (!collapseOnScroll || !host) return;
102
+ const bar = host;
103
+ const parent = findScrollParent(bar);
104
+ const target: HTMLElement | Window = parent ?? window;
105
+ const position = () => (parent ? parent.scrollTop : window.scrollY);
106
+ const end = () =>
107
+ parent
108
+ ? parent.scrollHeight - parent.clientHeight
109
+ : document.documentElement.scrollHeight - window.innerHeight;
110
+ // Rubber-banding past either end reports positions outside the range.
111
+ const clamped = () => Math.min(Math.max(0, position()), Math.max(0, end()));
112
+
113
+ /**
114
+ * Whether the focus inside the bar is there for the keyboard. Only
115
+ * that holds the bar: after a tap on an action the button stays
116
+ * focused for as long as the user reads on, and a bar held by it
117
+ * would never hide again. `:focus-visible` is not asked, because it
118
+ * answers for the browser's own ring, not for how focus arrived.
119
+ */
120
+ let heldByKeyboard = false;
121
+ /** A press began in the bar and has not yet turned into a click. */
122
+ let pressing = false;
123
+
124
+ const onPointerDown = () => {
125
+ pressing = true;
126
+ };
127
+ const onClick = () => {
128
+ pressing = false;
129
+ };
130
+ const onFocusIn = () => {
131
+ // A press cannot reach a bar that is away, so focus that arrives
132
+ // then is never the pointer's. The focus of a press is not held.
133
+ if (pressing && !scrolledAway) {
134
+ heldByKeyboard = false;
135
+ return;
136
+ }
137
+ heldByKeyboard = true;
138
+ scrolledAway = false;
139
+ };
140
+ const onFocusOut = (event: FocusEvent) => {
141
+ const next = event.relatedTarget;
142
+ if (!(next instanceof Node) || !bar.contains(next)) heldByKeyboard = false;
143
+ };
144
+ /** A key pressed in the bar: the keyboard is in use, wherever focus came from. */
145
+ const onKeyDown = () => {
146
+ heldByKeyboard = true;
147
+ scrolledAway = false;
148
+ };
149
+ /** Any key ends a press that never became a click (a drag off the bar). */
150
+ const onAnyKeyDown = () => {
151
+ pressing = false;
152
+ };
153
+
154
+ let last = clamped();
155
+ const onScroll = () => {
156
+ const now = clamped();
157
+ // At the top the bar is where it was laid out: always showing.
158
+ if (now <= bar.offsetHeight) {
159
+ last = now;
160
+ scrolledAway = false;
161
+ return;
162
+ }
163
+ const delta = now - last;
164
+ if (Math.abs(delta) < SCROLL_TOLERANCE) return;
165
+ last = now;
166
+ scrolledAway =
167
+ delta > 0 && !(heldByKeyboard && bar.contains(document.activeElement));
168
+ };
169
+
170
+ target.addEventListener("scroll", onScroll, { passive: true });
171
+ bar.addEventListener("pointerdown", onPointerDown);
172
+ bar.addEventListener("click", onClick);
173
+ bar.addEventListener("focusin", onFocusIn);
174
+ bar.addEventListener("focusout", onFocusOut);
175
+ bar.addEventListener("keydown", onKeyDown);
176
+ window.addEventListener("keydown", onAnyKeyDown, true);
177
+ return () => {
178
+ target.removeEventListener("scroll", onScroll);
179
+ bar.removeEventListener("pointerdown", onPointerDown);
180
+ bar.removeEventListener("click", onClick);
181
+ bar.removeEventListener("focusin", onFocusIn);
182
+ bar.removeEventListener("focusout", onFocusOut);
183
+ bar.removeEventListener("keydown", onKeyDown);
184
+ window.removeEventListener("keydown", onAnyKeyDown, true);
185
+ scrolledAway = false;
186
+ };
187
+ });
188
+
189
+ /** A 48px ghost target, the same as `IconButton` at `variant="ghost" size="lg"`. */
190
+ const backClasses =
191
+ "focus-ring focus-ring--muted inline-flex size-12 shrink-0 cursor-pointer items-center justify-center rounded-control bg-transparent text-headline transition-colors duration-150 hover:bg-surface-hover active:bg-surface-active";
192
+
193
+ const hasBack = $derived(backHref !== undefined || onback !== undefined);
194
+
195
+ /** The least room a title is left with beside the controls, in px. */
196
+ const TITLE_MIN = 72;
197
+
198
+ /**
199
+ * Where the title is: `inline`, between `leading` and the actions, or on
200
+ * a row of its `own` under them. Null until the bar has been measured
201
+ * (the server, a test DOM without layout): the stylesheet's own fallback
202
+ * then keeps the title at 72px by wrapping.
203
+ *
204
+ * It has to be measured. What the title is left with depends on how wide
205
+ * `leading` is, which is the app's markup: no media or container query
206
+ * knows that. And wrapping alone cannot do it: the actions come after
207
+ * the title, so they would follow it down to the second row.
208
+ */
209
+ let titleRow = $state<"inline" | "own" | null>(null);
210
+ /** The widest `leading` can be on the first row, in px. */
211
+ let leadingMax = $state<number | null>(null);
212
+ let row: HTMLElement | undefined = $state();
213
+
214
+ $effect(() => {
215
+ const bar = row;
216
+ if (!bar || typeof ResizeObserver === "undefined") return;
217
+ // Read here so that a new title or line count measures again.
218
+ void title;
219
+ void titleLines;
220
+
221
+ const measure = () => {
222
+ const style = getComputedStyle(bar);
223
+ const inner =
224
+ bar.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight);
225
+ // Nothing is laid out (a hidden bar): leave it as it is.
226
+ if (!(inner > 0)) return;
227
+ const gap = parseFloat(style.columnGap) || 0;
228
+
229
+ // Every part at the width it asks for, on one line, with the
230
+ // title out of the count: what is left is the title's.
231
+ bar.dataset.measuring = "";
232
+ let fixed = 0;
233
+ let beside = 0;
234
+ let parts = 0;
235
+ for (const child of bar.children) {
236
+ if (!(child instanceof HTMLElement)) continue;
237
+ const part = child.dataset.appbarPart;
238
+ if (part === "title" || child.offsetWidth === 0) continue;
239
+ parts += 1;
240
+ beside += child.offsetWidth;
241
+ // The back control and the actions keep their width whatever happens.
242
+ if (part === "back" || part === "actions") fixed += child.offsetWidth;
243
+ }
244
+ delete bar.dataset.measuring;
245
+
246
+ const left = inner - beside - gap * parts;
247
+ const next = left < TITLE_MIN ? "own" : "inline";
248
+ if (next !== titleRow) titleRow = next;
249
+ const fixedParts = bar.querySelectorAll(
250
+ ":scope > [data-appbar-part='back'], :scope > [data-appbar-part='actions']",
251
+ ).length;
252
+ const room = Math.max(0, Math.floor(inner - fixed - gap * fixedParts));
253
+ if (room !== leadingMax) leadingMax = room;
254
+ };
255
+
256
+ measure();
257
+ const sizes = new ResizeObserver(measure);
258
+ const watch = () => {
259
+ sizes.disconnect();
260
+ sizes.observe(bar);
261
+ for (const child of bar.children) sizes.observe(child);
262
+ };
263
+ watch();
264
+ // `leading` and the actions are the app's: they come and go.
265
+ const nodes = new MutationObserver(() => {
266
+ watch();
267
+ measure();
268
+ });
269
+ nodes.observe(bar, { childList: true });
270
+ return () => {
271
+ sizes.disconnect();
272
+ nodes.disconnect();
273
+ };
274
+ });
275
+ </script>
276
+
277
+ <header
278
+ bind:this={host}
279
+ data-collapsed={collapseOnScroll ? String(collapsed) : undefined}
280
+ class={cn(
281
+ "sticky top-0 z-sticky border-b border-border-weak bg-surface-elevated",
282
+ // Clear of the status bar and the notch, and of the corners in landscape.
283
+ "pt-[env(safe-area-inset-top)] pl-[env(safe-area-inset-left)] pr-[env(safe-area-inset-right)]",
284
+ "transition-transform duration-200 ease-out motion-reduce:transition-none",
285
+ // The strip under the status bar stays, so content never shows through it.
286
+ collapsed && "-translate-y-[calc(100%-env(safe-area-inset-top,0px))]",
287
+ className,
288
+ )}
289
+ {...restProps}
290
+ >
291
+ <!-- `--spacing: 4px`: Tailwind sizes everything from `--spacing`, which
292
+ is 0.25rem, so a 48px button was 96px with the text at 200% and the bar
293
+ wrapped to 225px. In px here, the back control, the actions and whatever
294
+ `leading` holds keep their size, and the title has the rest of the row,
295
+ or a row of its own when the rest is under 72px (see the style block).
296
+ The same at 100%: 0.25rem is 4px.
297
+ Fades out while the bar is away: under a status bar the strip that stays
298
+ is the bottom of the bar, and the controls would show in it. They keep
299
+ their place in the tab order; keyboard focus brings the bar back. -->
300
+ <div
301
+ bind:this={row}
302
+ data-title-row={titleRow ?? undefined}
303
+ style:--appbar-leading-max={leadingMax !== null ? `${leadingMax}px` : undefined}
304
+ class={cn(
305
+ "appbar-row flex min-h-14 items-center gap-2 px-2 py-1 [--spacing:4px]",
306
+ "transition-opacity duration-200 ease-out motion-reduce:transition-none",
307
+ collapsed && "pointer-events-none opacity-0",
308
+ )}
309
+ >
310
+ {#if backHref !== undefined}
311
+ <a href={backHref} class={backClasses} aria-label={backLabel} onclick={onback} data-appbar-part="back">
312
+ <ArrowLeft size={24} class="rtl:rotate-180" aria-hidden="true" />
313
+ </a>
314
+ {:else if onback}
315
+ <button type="button" class={backClasses} aria-label={backLabel} onclick={onback} data-appbar-part="back">
316
+ <ArrowLeft size={24} class="rtl:rotate-180" aria-hidden="true" />
317
+ </button>
318
+ {/if}
319
+ {@render leading?.()}
320
+ {#if title}
321
+ <!-- One line, cut with an ellipsis; two before it is cut with
322
+ `titleLines={2}`. 1.125rem, and no more than 1.3 times that
323
+ (23.4px) however large the text is set.
324
+ One branch per level, not a dynamic element: hydration takes a dynamic
325
+ element out and puts it back, which blurs a control inside it that the user
326
+ had already tabbed to. -->
327
+ {@const titleClasses = cn(
328
+ "appbar-title m-0 text-[length:min(1.125rem,23.4px)] leading-[1.34] font-semibold text-headline",
329
+ titleLines === 2 ? "line-clamp-2 [overflow-wrap:anywhere]" : "truncate",
330
+ !hasBack && !leading && "ps-2",
331
+ )}
332
+ {#if headingLevel === 1}
333
+ <h1 class={titleClasses} data-appbar-part="title">{title}</h1>
334
+ {:else if headingLevel === 2}
335
+ <h2 class={titleClasses} data-appbar-part="title">{title}</h2>
336
+ {:else if headingLevel === 3}
337
+ <h3 class={titleClasses} data-appbar-part="title">{title}</h3>
338
+ {:else if headingLevel === 4}
339
+ <h4 class={titleClasses} data-appbar-part="title">{title}</h4>
340
+ {:else if headingLevel === 5}
341
+ <h5 class={titleClasses} data-appbar-part="title">{title}</h5>
342
+ {:else}
343
+ <h6 class={titleClasses} data-appbar-part="title">{title}</h6>
344
+ {/if}
345
+ {:else}
346
+ <div class="min-w-0 flex-1" data-appbar-part="title"></div>
347
+ {/if}
348
+ {#if actions}
349
+ <div class="ms-auto flex shrink-0 items-center gap-2" data-appbar-part="actions">
350
+ {@render actions()}
351
+ </div>
352
+ {/if}
353
+ </div>
354
+ </header>
355
+
356
+ <style>
357
+ /*
358
+ * Until the bar has measured itself (the server's markup, no scripts):
359
+ * the row may wrap and the title asks for 72px, so it is never narrower.
360
+ * The actions follow it to the second row there, which is the one thing
361
+ * wrapping cannot avoid: they come after the title.
362
+ */
363
+ .appbar-row {
364
+ flex-wrap: wrap;
365
+ /* A `leading` that is wider than the row must not widen the page.
366
+ `clip`, not `hidden`: nothing scrolls, and nothing is cut above or
367
+ below, where a focus ring is. */
368
+ overflow-x: clip;
369
+ }
370
+ .appbar-row :global(> [data-appbar-part="title"]) {
371
+ flex: 1 1 72px;
372
+ min-width: 72px;
373
+ }
374
+ /* `leading`: whatever in the row is not the bar's own. */
375
+ .appbar-row :global(> :not([data-appbar-part])) {
376
+ min-width: 0;
377
+ max-width: var(--appbar-leading-max, 100%);
378
+ }
379
+
380
+ /* Measured, and the title has 72px or more: one row, as it was. */
381
+ .appbar-row[data-title-row="inline"] {
382
+ flex-wrap: nowrap;
383
+ }
384
+ .appbar-row[data-title-row="inline"] :global(> [data-appbar-part="title"]) {
385
+ flex: 1 1 0;
386
+ }
387
+
388
+ /*
389
+ * Measured, and it has less: the title is last and takes a whole row;
390
+ * the back control, `leading` and the actions are the first. Only the
391
+ * drawing order changes: in the document, and so for a screen reader and
392
+ * the Tab key, it is still back, leading, title, actions.
393
+ */
394
+ .appbar-row[data-title-row="own"] :global(> [data-appbar-part="actions"]) {
395
+ order: 1;
396
+ }
397
+ .appbar-row[data-title-row="own"] :global(> [data-appbar-part="title"]) {
398
+ order: 2;
399
+ flex: 1 1 100%;
400
+ /* The same inset as on a row with nothing before it, and the row's
401
+ 4px above and below; 8px more at the sides, in line with the
402
+ icons' edges. */
403
+ padding-inline: 8px;
404
+ padding-block-end: 4px;
405
+ }
406
+
407
+ /* While it measures: every part at the width it asks for, on one line. */
408
+ .appbar-row[data-measuring] {
409
+ flex-wrap: nowrap;
410
+ }
411
+ .appbar-row[data-measuring] :global(> *) {
412
+ flex: none;
413
+ max-width: none;
414
+ }
415
+ </style>
@@ -0,0 +1,69 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ /**
4
+ * The bar at the top of a phone screen: where you are, the way back, and
5
+ * at most two things to do here.
6
+ *
7
+ * It is one row, 56px tall, whenever the title has 72px or more beside
8
+ * the controls, at every text size. The bar is chrome: when the reader
9
+ * enlarges text, the room belongs to the page under it. So the controls
10
+ * are sized in px (48px targets), the title grows with the text only up
11
+ * to 1.3 times its size, and a title too long for the room it has is cut
12
+ * with an ellipsis (after two lines with `titleLines={2}`). The heading
13
+ * keeps its full text for a screen reader; keep the visible start of it
14
+ * enough to name the screen.
15
+ *
16
+ * The title never has less than 72px. Where the back control, `leading`
17
+ * and the actions leave it less (a wide chip in `leading`, a very narrow
18
+ * bar), it takes a second row of its own, as wide as the bar, and they
19
+ * stay together on the first.
20
+ */
21
+ type Props = Omit<HTMLAttributes<HTMLElement>, "class" | "title"> & {
22
+ /** The name of the screen, as a heading. */
23
+ title?: string;
24
+ /** Heading level of the title. 1 when the bar names the page. */
25
+ headingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
26
+ /** Shows the back control as a link to this address. */
27
+ backHref?: string;
28
+ /**
29
+ * Called when the back control is activated. On its own it makes the
30
+ * control a button; with `backHref` it runs before the link is
31
+ * followed, and `event.preventDefault()` stops that.
32
+ */
33
+ onback?: (event: MouseEvent) => void;
34
+ /** Accessible name of the back control. */
35
+ backLabel?: string;
36
+ /**
37
+ * Hides the bar while the page scrolls down and brings it back as
38
+ * soon as it scrolls up. It follows the nearest scrolling ancestor
39
+ * (the one in `AppShell`), or the window. The bar never hides while
40
+ * keyboard focus is inside it, and focus arriving from the keyboard
41
+ * brings it back. Focus left behind by a tap or a click does not hold
42
+ * it: it hides as usual, and the next key press brings it back.
43
+ */
44
+ collapseOnScroll?: boolean;
45
+ /**
46
+ * How many lines the title may take before it is cut with an
47
+ * ellipsis. The bar is taller only while the second line is in use.
48
+ */
49
+ titleLines?: 1 | 2;
50
+ /**
51
+ * Before the title, after the back control: a logo, an avatar, a
52
+ * chip. Rendered as it is, a child of the bar's row. It keeps its
53
+ * width; when that leaves the title under 72px the title moves to a
54
+ * row of its own, and a `leading` wider than the row is held to the
55
+ * row's width.
56
+ */
57
+ leading?: Snippet;
58
+ /**
59
+ * After the title. At most two icon buttons, at `size="lg"` so each is
60
+ * a 48px target; put anything more in a menu. Their size does not
61
+ * grow with the text size here: inside the bar the spacing scale is
62
+ * in px.
63
+ */
64
+ actions?: Snippet;
65
+ class?: string;
66
+ };
67
+ declare const AppBar: import("svelte").Component<Props, {}, "">;
68
+ type AppBar = ReturnType<typeof AppBar>;
69
+ export default AppBar;
@@ -0,0 +1,116 @@
1
+ <script lang="ts">
2
+ import { mergeStrings } from "../util/ready-made-strings.js";
3
+ import { zabiStringsFor } from "../util/zabi-strings.js";
4
+ import type { HTMLAttributes } from "svelte/elements";
5
+ import Avatar from "../atoms/Avatar.svelte";
6
+ import {
7
+ DEFAULT_AVATAR_GROUP_STRINGS,
8
+ splitGroup,
9
+ type AvatarGroupStrings,
10
+ type AvatarPerson,
11
+ type AvatarSize,
12
+ } from "../util/avatar.js";
13
+ import { cn } from "../util/cn.js";
14
+
15
+ /**
16
+ * A row of overlapping avatars: who is going, the members, who rated.
17
+ *
18
+ * It is a list, each avatar named by its person. Past `max` the rest
19
+ * stand behind a "+3" of the same size and shape. Not interactive.
20
+ *
21
+ * Between two avatars is a ring in the colour of the surface under the
22
+ * group. It cannot read that colour, so it is the raised surface (a
23
+ * card) unless the group is told otherwise:
24
+ *
25
+ * ```svelte
26
+ * <AvatarGroup {people} style="--zabi-avatar-ring: var(--color-surface-base)" />
27
+ * ```
28
+ *
29
+ * Other attributes (`id`, `data-*`, `style`, ...) land on the list.
30
+ */
31
+ type Props = Omit<HTMLAttributes<HTMLUListElement>, "class" | "aria-label"> & {
32
+ /** The people, in the order they are shown. */
33
+ people: AvatarPerson[];
34
+ /**
35
+ * How many are shown before the rest become "+N". Below 1 is 1. One
36
+ * person too many is shown and not counted: there is no "+1".
37
+ */
38
+ max?: number;
39
+ /** Diameter of each avatar: 24, 32 and 48px. */
40
+ size?: "sm" | "md" | "lg";
41
+ /** Accessible name of the list ("Who's going"). */
42
+ label?: string;
43
+ /** Overrides for the built-in strings. */
44
+ strings?: Partial<AvatarGroupStrings>;
45
+ /** The language the initials are worked out in; passed to each Avatar. Without it, the page's. */
46
+ locale?: string;
47
+ /** Extra classes for the list. */
48
+ class?: string;
49
+ };
50
+
51
+ let {
52
+ people,
53
+ max = 4,
54
+ size = "md",
55
+ label,
56
+ strings,
57
+ locale,
58
+ class: className = "",
59
+ ...restProps
60
+ }: Props = $props();
61
+
62
+ /** The app-wide words for this component, from a `ZabiStringsProvider` above it, if there is one. */
63
+ const provided = zabiStringsFor("avatarGroup");
64
+ const text = $derived(mergeStrings(DEFAULT_AVATAR_GROUP_STRINGS, provided(), strings));
65
+ const split = $derived(splitGroup(people.length, max));
66
+ const shown = $derived(people.slice(0, split.shown));
67
+ const hiddenNames = $derived(people.slice(split.shown).map((person) => person.name));
68
+
69
+ // A quarter of the circle lies under the next one.
70
+ const OVERLAP: Record<AvatarSize, string> = {
71
+ sm: "-ms-[6px]",
72
+ md: "-ms-[8px]",
73
+ lg: "-ms-[12px]",
74
+ };
75
+ const CHIP: Record<AvatarSize, string> = {
76
+ sm: "size-[24px] text-[10px]",
77
+ md: "size-[32px] text-[12px]",
78
+ lg: "size-[48px] text-[16px]",
79
+ };
80
+
81
+ /** The ring that parts one circle from the next. A shadow, so it takes no room; forced colours draw the avatars' own borders instead. */
82
+ const ring = "rounded-pill shadow-[0_0_0_2px_var(--zabi-avatar-ring,var(--color-surface-raised))]";
83
+ </script>
84
+
85
+ <!-- `role`, because a list without bullets loses its role in Safari. -->
86
+ <ul
87
+ class={cn("m-0 flex list-none items-center p-0", className)}
88
+ role="list"
89
+ aria-label={label || undefined}
90
+ data-avatar-group
91
+ data-size={size}
92
+ {...restProps}
93
+ >
94
+ {#each shown as person, index (index)}
95
+ <li class={cn("flex", index > 0 && (OVERLAP[size] ?? OVERLAP.md))}>
96
+ <Avatar name={person.name} src={person.src} {size} {locale} class={ring} />
97
+ </li>
98
+ {/each}
99
+ {#if split.hidden > 0}
100
+ <li class={cn("flex", OVERLAP[size] ?? OVERLAP.md)}>
101
+ <!-- The same circle as an avatar, in the neutral pair: it is a count, not a person. -->
102
+ <span
103
+ class={cn(
104
+ "inline-flex shrink-0 items-center justify-center border border-transparent bg-neutral-subtle leading-none font-semibold text-neutral-text tabular-nums select-none",
105
+ CHIP[size] ?? CHIP.md,
106
+ ring,
107
+ )}
108
+ role="img"
109
+ aria-label={text.more(split.hidden, hiddenNames)}
110
+ data-avatar-more
111
+ >
112
+ <span aria-hidden="true">+{split.hidden}</span>
113
+ </span>
114
+ </li>
115
+ {/if}
116
+ </ul>
@@ -0,0 +1,40 @@
1
+ import type { HTMLAttributes } from "svelte/elements";
2
+ import { type AvatarGroupStrings, type AvatarPerson } from "../util/avatar.js";
3
+ /**
4
+ * A row of overlapping avatars: who is going, the members, who rated.
5
+ *
6
+ * It is a list, each avatar named by its person. Past `max` the rest
7
+ * stand behind a "+3" of the same size and shape. Not interactive.
8
+ *
9
+ * Between two avatars is a ring in the colour of the surface under the
10
+ * group. It cannot read that colour, so it is the raised surface (a
11
+ * card) unless the group is told otherwise:
12
+ *
13
+ * ```svelte
14
+ * <AvatarGroup {people} style="--zabi-avatar-ring: var(--color-surface-base)" />
15
+ * ```
16
+ *
17
+ * Other attributes (`id`, `data-*`, `style`, ...) land on the list.
18
+ */
19
+ type Props = Omit<HTMLAttributes<HTMLUListElement>, "class" | "aria-label"> & {
20
+ /** The people, in the order they are shown. */
21
+ people: AvatarPerson[];
22
+ /**
23
+ * How many are shown before the rest become "+N". Below 1 is 1. One
24
+ * person too many is shown and not counted: there is no "+1".
25
+ */
26
+ max?: number;
27
+ /** Diameter of each avatar: 24, 32 and 48px. */
28
+ size?: "sm" | "md" | "lg";
29
+ /** Accessible name of the list ("Who's going"). */
30
+ label?: string;
31
+ /** Overrides for the built-in strings. */
32
+ strings?: Partial<AvatarGroupStrings>;
33
+ /** The language the initials are worked out in; passed to each Avatar. Without it, the page's. */
34
+ locale?: string;
35
+ /** Extra classes for the list. */
36
+ class?: string;
37
+ };
38
+ declare const AvatarGroup: import("svelte").Component<Props, {}, "">;
39
+ type AvatarGroup = ReturnType<typeof AvatarGroup>;
40
+ export default AvatarGroup;