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,24 +1,61 @@
1
1
  <script lang="ts">
2
+ import { mergeStrings } from "../util/ready-made-strings.js";
3
+ import { zabiStringsFor } from "../util/zabi-strings.js";
2
4
  import ThemeToggle from "../atoms/ThemeToggle.svelte";
3
5
  import IconButton from "../atoms/IconButton.svelte";
4
- import { ExternalLink, Menu, X } from "@lucide/svelte";
6
+ import BottomSheet from "../molecules/BottomSheet.svelte";
7
+ import TopNavbarSheetContent from "./TopNavbarSheetContent.svelte";
8
+ import ExternalLink from "@lucide/svelte/icons/external-link";
9
+ import Menu from "@lucide/svelte/icons/menu";
10
+ import X from "@lucide/svelte/icons/x";
5
11
  import type { Component, Snippet } from "svelte";
6
12
  import { generateId } from "../util/ssr-safe.js";
13
+ import { getAppShell } from "../util/app-shell.js";
7
14
  import { cn } from "../util/cn.js";
15
+ import { resolveScrolledUnder, watchScrollEdge, type ScrollEdgeMode } from "../util/scroll-edge.js";
16
+ import type { ThemeToggleLabels } from "../util/theme-mode.js";
17
+ import { DEFAULT_TOP_NAVBAR_STRINGS, type TopNavbarStrings } from "../util/top-navbar.js";
18
+ import { isInsideToastRegion } from "../util/focus-utils.js";
8
19
 
9
20
  export interface TopNavbarNavItem {
10
21
  label: string;
11
22
  href: string;
12
23
  icon?: Component<{ size?: number; class?: string }>;
13
24
  iconFilled?: Component<{ size?: number; class?: string }>;
25
+ /** Marks the link and opens it in a new tab. Defaults to true for absolute URLs. */
26
+ external?: boolean;
14
27
  }
15
28
 
29
+ export type TopNavbarCollapseAt = "sm" | "md" | "lg" | "xl";
30
+
31
+ /**
32
+ * How the links open below `collapseAt`: `sheet` slides a modal sheet up
33
+ * from the bottom of the screen; `inline` opens a panel under the bar,
34
+ * which pushes the page down (the menu of 8.1 and earlier).
35
+ */
36
+ export type TopNavbarMenu = "sheet" | "inline";
37
+
16
38
  interface Props {
17
39
  brand?: string;
18
40
  brandHref?: string;
19
41
  /** `aria-label` on `<nav>` when multiple nav landmarks exist. */
20
42
  ariaLabel?: string;
21
43
  showThemeToggle?: boolean;
44
+ /**
45
+ * How the theme toggle steps: `"two"` flips light and dark, `"three"`
46
+ * goes through system, light and dark. As `modes` of ThemeToggle.
47
+ */
48
+ themeModes?: "two" | "three";
49
+ /**
50
+ * The `localStorage` key the theme choice is kept under; `null` keeps
51
+ * nothing. As `storageKey` of ThemeToggle, whose default applies when
52
+ * this is left out.
53
+ */
54
+ themeStorageKey?: string | null;
55
+ /** The texts of the theme toggle's name, for another language. As `labels` of ThemeToggle. */
56
+ themeLabels?: Partial<ThemeToggleLabels>;
57
+ /** The words the bar says by itself: the menu button's two names, the name of the menu sheet and the note on a link that opens a new tab. */
58
+ strings?: Partial<TopNavbarStrings>;
22
59
  class?: string;
23
60
  /** @deprecated use `class`. */
24
61
  className?: string;
@@ -27,8 +64,28 @@
27
64
  items?: TopNavbarNavItem[];
28
65
  navVariant?: "header" | "sidebar";
29
66
  currentPath?: string;
67
+ /**
68
+ * Where the links move from the phone menu into the bar: at this
69
+ * breakpoint and wider they sit in the row. Raise it when the row has
70
+ * more items than fit at 768px.
71
+ */
72
+ collapseAt?: TopNavbarCollapseAt;
73
+ /**
74
+ * How the links open below `collapseAt`. `sheet` (the default) is a
75
+ * bottom sheet: a modal dialog that is as tall as its content, up to
76
+ * the full screen below the status bar, and scrolls inside. `inline` is a panel under the
77
+ * bar, a disclosure that pushes the page down and traps nothing.
78
+ */
79
+ menu?: TopNavbarMenu;
30
80
  preventNavigation?: boolean;
31
81
  onclick?: (event: Event) => void;
82
+ /**
83
+ * When the bar shows the glass and the hairline. `auto` follows the
84
+ * scroll position: flush at the top, glass once content is under it
85
+ * (and while the inline phone menu is open). `always` is glass all the time,
86
+ * `never` flush all the time.
87
+ */
88
+ scrollEdge?: ScrollEdgeMode;
32
89
  }
33
90
 
34
91
  let {
@@ -36,14 +93,21 @@
36
93
  brandHref,
37
94
  ariaLabel,
38
95
  showThemeToggle = true,
96
+ themeModes = "two",
97
+ themeStorageKey,
98
+ themeLabels,
99
+ strings,
39
100
  class: classAttr = "",
40
101
  className: legacyClass = "",
41
102
  embedded = false,
42
103
  items = [],
43
104
  navVariant = "header",
44
105
  currentPath = "",
106
+ collapseAt = "md",
107
+ menu = "sheet",
45
108
  preventNavigation = false,
46
109
  onclick,
110
+ scrollEdge = "auto",
47
111
  nav,
48
112
  actions,
49
113
  ...restProps
@@ -55,18 +119,156 @@
55
119
 
56
120
  const mobileMenuId = generateId("topnavbar-menu");
57
121
 
122
+ /** The app-wide words for this component, from a `ZabiStringsProvider` above it, if there is one. */
123
+ const provided = zabiStringsFor("topNavbar");
124
+ const text = $derived(mergeStrings<Required<TopNavbarStrings>>(DEFAULT_TOP_NAVBAR_STRINGS, provided(), strings));
125
+
58
126
  let isMenuOpen = $state(false);
127
+ const sheet = $derived(menu !== "inline");
128
+ /** The inline panel is open: only then do the window listeners below have anything to close. */
129
+ const inlineOpen = $derived(isMenuOpen && !sheet);
130
+
131
+ /** Content is under the bar: from the shell if there is one, else from the window. */
132
+ const shell = getAppShell();
133
+ let ownScrolled = $state(false);
134
+ $effect(() => {
135
+ if (shell || scrollEdge !== "auto" || embedded) return;
136
+ return watchScrollEdge(null, (edge) => (ownScrolled = edge.top));
137
+ });
138
+ // An open inline menu sits on the material, whatever the scroll position.
139
+ // The sheet covers the page and the bar under its backdrop: the bar stays as it was.
140
+ const scrolledUnder = $derived(
141
+ resolveScrolledUnder(
142
+ scrollEdge,
143
+ inlineOpen || (shell ? shell.scrolledTop : ownScrolled),
144
+ ),
145
+ );
146
+ let navElement = $state<HTMLElement | null>(null);
147
+ let panelElement = $state<HTMLElement | null>(null);
148
+ let menuButtonHolder = $state<HTMLElement | null>(null);
149
+
150
+ /**
151
+ * The classes that move the links between the row and the menu, written
152
+ * out for each breakpoint: Tailwind only generates a class it can read in
153
+ * the source, so `${collapseAt}:hidden` would produce nothing. `query` is
154
+ * the same breakpoint for the script (Tailwind's defaults).
155
+ */
156
+ const COLLAPSE: Record<
157
+ TopNavbarCollapseAt,
158
+ { row: string; menu: string; brand: string; query: string }
159
+ > = {
160
+ sm: { row: "hidden sm:block", menu: "sm:hidden", brand: "sm:shrink-0", query: "(min-width: 40rem)" },
161
+ md: { row: "hidden md:block", menu: "md:hidden", brand: "md:shrink-0", query: "(min-width: 48rem)" },
162
+ lg: { row: "hidden lg:block", menu: "lg:hidden", brand: "lg:shrink-0", query: "(min-width: 64rem)" },
163
+ xl: { row: "hidden xl:block", menu: "xl:hidden", brand: "xl:shrink-0", query: "(min-width: 80rem)" },
164
+ };
165
+ const collapse = $derived(COLLAPSE[collapseAt] ?? COLLAPSE.md);
59
166
 
60
167
  function toggleMenu() {
61
168
  isMenuOpen = !isMenuOpen;
62
169
  }
63
170
 
171
+ function focusMenuButton() {
172
+ menuButtonHolder?.querySelector("button")?.focus();
173
+ }
174
+
175
+ /**
176
+ * The inline menu is a disclosure, not a dialog: nothing is trapped or
177
+ * locked, so it has to get out of the way by itself whenever the user has
178
+ * moved on. The sheet is a dialog and does all of this itself.
179
+ */
64
180
  function handleWindowKeydown(event: KeyboardEvent) {
65
- if (event.key === "Escape" && isMenuOpen) {
181
+ // A menu inside the panel (a Dropdown, say) closes on Escape first.
182
+ if (event.key !== "Escape" || !inlineOpen || event.defaultPrevented) return;
183
+ const focusInside = !!panelElement?.contains(document.activeElement);
184
+ isMenuOpen = false;
185
+ // The link that had focus is gone; without this, focus falls to <body>.
186
+ if (focusInside) focusMenuButton();
187
+ }
188
+
189
+ /** A toast lies over the menu; a press on it, or focus in it, is not leaving the bar. */
190
+ function isOutside(event: Event): boolean {
191
+ if (isInsideToastRegion(event.target)) return false;
192
+ return !!navElement && !(event.target instanceof Node && navElement.contains(event.target));
193
+ }
194
+
195
+ /**
196
+ * A press outside the bar closes the menu once it is complete (the click),
197
+ * not when it starts. The open menu pushes the page down, so closing it
198
+ * on the way down moved the page back up under the pointer and the
199
+ * control that was pressed never got its click.
200
+ */
201
+ let pressing = false;
202
+
203
+ function handleOutsideClick(event: MouseEvent) {
204
+ if (inlineOpen && isOutside(event)) isMenuOpen = false;
205
+ }
206
+
207
+ /**
208
+ * Focus that leaves the bar by the keyboard or from a script. A press
209
+ * moves focus too, between its `mousedown` and its click (after the
210
+ * finger has lifted, on a touch screen); that one is left to the click.
211
+ */
212
+ function handleOutsideFocus(event: FocusEvent) {
213
+ if (inlineOpen && !pressing && isOutside(event)) isMenuOpen = false;
214
+ }
215
+
216
+ /** Following a link in the menu or the sheet: the page under it is about to change. */
217
+ function handlePanelClick(event: MouseEvent) {
218
+ if (event.target instanceof Element && event.target.closest("a[href]")) {
66
219
  isMenuOpen = false;
67
220
  }
68
221
  }
69
222
 
223
+ // The route changed some other way (history, a link elsewhere on the page).
224
+ let lastPath: string | undefined;
225
+ $effect(() => {
226
+ if (lastPath !== undefined && currentPath !== lastPath) isMenuOpen = false;
227
+ lastPath = currentPath;
228
+ });
229
+
230
+ // Past the breakpoint the menu button is gone, so a menu left open could
231
+ // not be closed, and it would still be open on the way back down. For the
232
+ // sheet, closing also lets go of the scroll lock and the inert page.
233
+ $effect(() => {
234
+ if (typeof window === "undefined" || !window.matchMedia) return;
235
+ const wide = window.matchMedia(collapse.query);
236
+ const close = () => {
237
+ if (wide.matches) isMenuOpen = false;
238
+ };
239
+ wide.addEventListener("change", close);
240
+ return () => wide.removeEventListener("change", close);
241
+ });
242
+
243
+ /**
244
+ * The menu scrolls on its own only when it is taller than the room under
245
+ * the bar. A scrolling box clips whatever reaches outside it, and a
246
+ * Dropdown among the `actions` opens past the end of the menu: when the
247
+ * menu fits, that has to stay visible, as it was before the menu could
248
+ * scroll.
249
+ */
250
+ let panelScrolls = $state(false);
251
+ $effect(() => {
252
+ const panel = panelElement;
253
+ const content = panel?.firstElementChild;
254
+ if (!panel || !content) {
255
+ panelScrolls = false;
256
+ return;
257
+ }
258
+ const measure = () => {
259
+ const room = parseFloat(getComputedStyle(panel).maxHeight);
260
+ panelScrolls = Number.isFinite(room) && content.getBoundingClientRect().height > room;
261
+ };
262
+ measure();
263
+ window.addEventListener("resize", measure);
264
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(measure);
265
+ observer?.observe(content);
266
+ return () => {
267
+ window.removeEventListener("resize", measure);
268
+ observer?.disconnect();
269
+ };
270
+ });
271
+
70
272
  function handleNavLinkClick(event: MouseEvent) {
71
273
  if (preventNavigation) {
72
274
  event.preventDefault();
@@ -76,9 +278,10 @@
76
278
  }
77
279
  }
78
280
 
79
- /** Absolute URLs leave the site, so they are marked and open in a new tab. */
80
- function isExternal(href: string): boolean {
81
- return href.startsWith("http://") || href.startsWith("https://");
281
+ /** Absolute URLs leave the site, so they are marked and open in a new tab.
282
+ * `external` overrides that for same-origin links that leave the app. */
283
+ function isExternal(item: TopNavbarNavItem): boolean {
284
+ return item.external ?? (item.href.startsWith("http://") || item.href.startsWith("https://"));
82
285
  }
83
286
 
84
287
  /** App routes: prefix match except `/` and absolute URLs (exact match). */
@@ -98,8 +301,24 @@
98
301
  : "flex flex-col gap-1 md:flex-row md:items-center md:justify-between";
99
302
  });
100
303
 
101
- function getNavItemClasses(): string {
102
- return "flex flex-col gap-1 grow h-full items-center justify-center min-h-0 min-w-0 relative shrink-0 cursor-pointer w-full md:w-auto";
304
+ /** Where a list of links is drawn: in the bar, in the phone menu, or alone (`embedded`). */
305
+ type NavListPlace = "row" | "menu" | "sheet" | "embedded";
306
+
307
+ function getListClasses(place: NavListPlace): string {
308
+ if (place === "embedded" || navVariant === "sidebar") return ulClasses;
309
+ // The row only shows at the breakpoint and wider, and the menu only
310
+ // below it, so neither needs a breakpoint of its own.
311
+ return place === "row"
312
+ ? "flex flex-row items-center justify-between gap-1"
313
+ : "flex flex-col gap-1";
314
+ }
315
+
316
+ function getNavItemClasses(place: NavListPlace = "embedded"): string {
317
+ const base =
318
+ "flex flex-col gap-1 grow h-full items-center justify-center min-h-0 min-w-0 relative shrink-0 cursor-pointer";
319
+ if (place === "row") return `${base} w-auto`;
320
+ if (place === "menu" || place === "sheet") return `${base} w-full`;
321
+ return `${base} w-full md:w-auto`;
103
322
  }
104
323
 
105
324
  /**
@@ -119,23 +338,30 @@
119
338
  */
120
339
  function getIconContainerClasses(isActive: boolean): string {
121
340
  const base =
122
- "focus-ring focus-ring--nav group/nav-item flex flex-col items-center justify-center overflow-clip relative rounded-pill shrink-0 transition-colors duration-200 outline-none";
341
+ "focus-ring focus-ring--nav group/nav-item flex flex-col items-center justify-center overflow-clip relative rounded-pill shrink-0 transition-colors duration-(--duration-moderate) ease-standard outline-none";
123
342
  return isActive
124
- ? cn(base, "bg-nav-menu-active text-nav-menu-item-active")
343
+ ? // Forced colours drop the fill and recolour the label: the
344
+ // current item keeps an inset outline there, which is drawn.
345
+ cn(
346
+ base,
347
+ "bg-nav-menu-active font-semibold text-nav-menu-item-active forced-colors:outline-solid forced-colors:outline-2 forced-colors:-outline-offset-2",
348
+ )
125
349
  : cn(
126
350
  base,
127
- "text-nav-menu-item hover:bg-nav-menu-hover hover:text-nav-menu-item-hover",
351
+ "font-medium text-nav-menu-item hover:bg-nav-menu-hover hover:text-nav-menu-item-hover active:bg-nav-menu-active",
128
352
  );
129
353
  }
130
354
 
131
- function getStateLayerClasses(): string {
132
- return "box-border flex gap-1 h-10 items-center px-4 py-2 relative shrink-0";
355
+ function getStateLayerClasses(place: NavListPlace): string {
356
+ // A row of the sheet is a touch target on every pointer: 44px.
357
+ const height = place === "sheet" ? "h-11" : "h-10 pointer-coarse:h-11";
358
+ return `box-border flex gap-1 ${height} items-center px-4 py-2 relative shrink-0`;
133
359
  }
134
360
 
135
361
  function getLabelClasses(): string {
136
362
  // `whitespace-pre` used to sit beside `text-nowrap` — same property,
137
363
  // and it preserved literal whitespace as a side effect.
138
- return "font-medium leading-4 relative shrink-0 text-center text-nowrap tracking-wide text-xs text-inherit";
364
+ return "leading-5 relative shrink-0 text-center text-nowrap text-sm text-inherit";
139
365
  }
140
366
 
141
367
  function getIconWrapperClasses(): string {
@@ -143,21 +369,22 @@
143
369
  }
144
370
  </script>
145
371
 
146
- {#snippet defaultNavList()}
147
- <ul class="{ulClasses} list-none m-0 p-0">
372
+ {#snippet defaultNavList(place: NavListPlace)}
373
+ <ul class="{getListClasses(place)} list-none m-0 p-0">
148
374
  {#each items as item (item.href)}
149
375
  {@const isActive = isNavItemActive(item.href)}
150
- {@const external = isExternal(item.href)}
151
- <li class={getNavItemClasses()}>
376
+ {@const external = isExternal(item)}
377
+ <li class={getNavItemClasses(place)}>
378
+ <!-- In the menu and the sheet the link is the whole row, not a pill as wide as its label. -->
152
379
  <a
153
380
  href={item.href}
154
- class={getIconContainerClasses(isActive)}
381
+ class={cn(getIconContainerClasses(isActive), (place === "menu" || place === "sheet") && "w-full")}
155
382
  onclick={handleNavLinkClick}
156
383
  aria-current={isActive ? "page" : undefined}
157
384
  target={external ? "_blank" : undefined}
158
385
  rel={external ? "noopener noreferrer" : undefined}
159
386
  >
160
- <div class={getStateLayerClasses()}>
387
+ <div class={getStateLayerClasses(place)}>
161
388
  {#if item.iconFilled && isActive}
162
389
  {@const Icon = item.iconFilled}
163
390
  <div class={getIconWrapperClasses()}>
@@ -178,7 +405,7 @@
178
405
  class="shrink-0 text-current opacity-70"
179
406
  aria-hidden="true"
180
407
  />
181
- <span class="sr-only">(opens in a new tab)</span>
408
+ <span class="sr-only">{text.opensInNewTab}</span>
182
409
  {/if}
183
410
  </div>
184
411
  </a>
@@ -187,70 +414,93 @@
187
414
  </ul>
188
415
  {/snippet}
189
416
 
190
- <svelte:window onkeydown={handleWindowKeydown} />
417
+ {#snippet sheetLinks()}
418
+ {#if nav}
419
+ {@render nav()}
420
+ {:else}
421
+ {@render defaultNavList("sheet")}
422
+ {/if}
423
+ {/snippet}
424
+
425
+ <svelte:window
426
+ onkeydown={handleWindowKeydown}
427
+ onmousedowncapture={() => (pressing = true)}
428
+ onmouseupcapture={() => (pressing = false)}
429
+ onpointercancelcapture={() => (pressing = false)}
430
+ onclickcapture={handleOutsideClick}
431
+ onfocusin={handleOutsideFocus}
432
+ />
191
433
 
192
434
  {#if embedded}
193
435
  <nav class={className} aria-label={ariaLabel} {...restProps}>
194
436
  {#if nav}
195
437
  {@render nav()}
196
438
  {:else}
197
- {@render defaultNavList()}
439
+ {@render defaultNavList("embedded")}
198
440
  {/if}
199
441
  </nav>
200
442
  {:else}
201
443
  <nav
202
- class={cn("border-b border-border bg-background sticky top-0 z-sticky", className)}
444
+ bind:this={navElement}
445
+ data-scrolled-under={String(scrolledUnder)}
446
+ class={cn("material-bar sticky top-0 z-sticky", className)}
203
447
  aria-label={ariaLabel}
204
448
  {...restProps}
205
449
  >
206
450
  <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
207
451
  <div class="flex justify-between items-center h-16">
208
- <div class="shrink-0">
452
+ <!-- Beside the menu button the brand gives way and is cut short,
453
+ so the button stays on screen however long the name or large the
454
+ text. In the row it keeps its full width, as before. -->
455
+ <div class={cn("flex min-w-0", collapse.brand)}>
209
456
  {#if brandHref}
210
457
  <a
211
458
  href={brandHref}
212
- class="focus-ring text-xl font-bold text-headline transition-colors hover:text-link"
459
+ class="focus-ring block min-w-0 truncate text-xl font-bold text-headline transition-colors hover:text-link active:text-link pointer-coarse:min-h-11 pointer-coarse:leading-[2.75rem]"
213
460
  >
214
461
  {brand}
215
462
  </a>
216
463
  {:else}
217
- <span class="text-xl font-bold text-headline">{brand}</span>
464
+ <span class="block min-w-0 truncate text-xl font-bold text-headline">{brand}</span>
218
465
  {/if}
219
466
  </div>
220
467
 
221
- <div class="hidden md:block">
468
+ <div class={collapse.row}>
222
469
  <div class="ml-10 flex items-center space-x-4">
223
470
  {#if nav}
224
471
  {@render nav()}
225
472
  {:else if items.length > 0}
226
- {@render defaultNavList()}
473
+ {@render defaultNavList("row")}
227
474
  {/if}
228
475
  </div>
229
476
  </div>
230
477
 
231
- <div class="hidden md:block">
478
+ <div class={collapse.row}>
232
479
  <div class="ml-4 flex items-center space-x-4">
233
480
  {@render actions?.()}
234
481
  {#if showThemeToggle}
235
- <ThemeToggle />
482
+ <ThemeToggle modes={themeModes} storageKey={themeStorageKey} labels={themeLabels} />
236
483
  {/if}
237
484
  </div>
238
485
  </div>
239
486
 
240
- <div class="md:hidden">
487
+ <div bind:this={menuButtonHolder} class={cn("ms-2 shrink-0", collapse.menu)}>
241
488
  <!--
242
489
  Was a bare ☰ glyph in a text-2xl span: font-dependent,
243
490
  off the icon scale, and with no disclosure semantics, so
244
491
  a screen reader could not tell the menu was open.
245
492
  -->
493
+ <!-- The sheet is a dialog the button opens; the page under its
494
+ backdrop is inert, so this button is never the one that closes it. -->
246
495
  <IconButton
247
496
  variant="ghost"
248
- label={isMenuOpen ? "Close menu" : "Open menu"}
497
+ label={inlineOpen ? text.closeMenu : text.openMenu}
249
498
  onclick={toggleMenu}
499
+ aria-haspopup={sheet ? "dialog" : undefined}
250
500
  aria-expanded={isMenuOpen}
251
- aria-controls={mobileMenuId}
501
+ aria-controls={isMenuOpen ? mobileMenuId : undefined}
252
502
  >
253
- {#if isMenuOpen}
503
+ {#if inlineOpen}
254
504
  <X size={20} />
255
505
  {:else}
256
506
  <Menu size={20} />
@@ -259,21 +509,32 @@
259
509
  </div>
260
510
  </div>
261
511
 
262
- {#if isMenuOpen}
263
- <div class="md:hidden" id={mobileMenuId}>
512
+ <!-- `aria-controls` above names this panel only while it exists.
513
+ Keeping it in the DOM, hidden, would render `nav` and `actions` a
514
+ second time on every page for every desktop visitor: duplicate ids
515
+ from the caller's snippets, and a second ThemeToggle mounted. -->
516
+ {#if inlineOpen}
517
+ <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
518
+ <div
519
+ bind:this={panelElement}
520
+ class={cn("topnavbar-menu", collapse.menu)}
521
+ id={mobileMenuId}
522
+ data-scrolls={panelScrolls ? "" : undefined}
523
+ onclick={handlePanelClick}
524
+ >
264
525
  <div
265
- class="px-2 pt-2 pb-3 space-y-1 sm:px-3 border-t border-border"
526
+ class="px-2 pt-2 pb-3 space-y-1 sm:px-3 border-t border-border-weak"
266
527
  >
267
528
  {#if nav}
268
529
  {@render nav()}
269
530
  {:else if items.length > 0}
270
- {@render defaultNavList()}
531
+ {@render defaultNavList("menu")}
271
532
  {/if}
272
533
  <div class="pt-4">
273
534
  {@render actions?.()}
274
535
  {#if showThemeToggle}
275
536
  <div class="mt-2">
276
- <ThemeToggle />
537
+ <ThemeToggle modes={themeModes} storageKey={themeStorageKey} labels={themeLabels} />
277
538
  </div>
278
539
  {/if}
279
540
  </div>
@@ -282,4 +543,57 @@
282
543
  {/if}
283
544
  </div>
284
545
  </nav>
546
+
547
+ <!-- Below `collapseAt` the links open in a sheet from the bottom. It is
548
+ portalled to the body: the bar is sticky and glass, and a fixed child of a
549
+ filtered box is placed against that box. It renders nothing until
550
+ it is open, so `nav` and `actions` are not drawn a second time. -->
551
+ {#if sheet}
552
+ <BottomSheet
553
+ bind:isOpen={isMenuOpen}
554
+ fit
555
+ fitMax="full"
556
+ title={text.menuTitle}
557
+ id={mobileMenuId}
558
+ data-topnavbar-sheet=""
559
+ >
560
+ <TopNavbarSheetContent
561
+ links={nav || items.length > 0 ? sheetLinks : undefined}
562
+ navLabel={ariaLabel ?? text.menuTitle}
563
+ {actions}
564
+ {showThemeToggle}
565
+ {themeModes}
566
+ {themeStorageKey}
567
+ {themeLabels}
568
+ themeHeading={text.themeHeading}
569
+ onclick={handlePanelClick}
570
+ />
571
+ </BottomSheet>
572
+ {/if}
285
573
  {/if}
574
+
575
+ <style>
576
+ /*
577
+ * The bar is sticky, so an open menu taller than the screen could never be
578
+ * scrolled into view: the page moved under it and its last control stayed
579
+ * below the fold. It takes what is left of the screen under the 4rem bar
580
+ * and scrolls on its own, without handing the scroll
581
+ * on to the page when it reaches an end. `dvh` follows a phone's
582
+ * collapsing address bar; `vh` is for browsers without it. In a web app
583
+ * installed to the home screen the bar starts below the status bar, so
584
+ * that inset comes off as well, or the menu's end would be under the
585
+ * bottom of the screen by as much.
586
+ *
587
+ * Only a menu that does not fit scrolls (`data-scrolls`, set by the
588
+ * script): a scrolling box would clip a Dropdown that opens out of it.
589
+ */
590
+ .topnavbar-menu {
591
+ max-height: calc(100vh - 4rem - env(safe-area-inset-top, 0px));
592
+ max-height: calc(100dvh - 4rem - env(safe-area-inset-top, 0px));
593
+ }
594
+
595
+ .topnavbar-menu[data-scrolls] {
596
+ overflow-y: auto;
597
+ overscroll-behavior: contain;
598
+ }
599
+ </style>
@@ -1,4 +1,7 @@
1
1
  import type { Component, Snippet } from "svelte";
2
+ import { type ScrollEdgeMode } from "../util/scroll-edge.js";
3
+ import type { ThemeToggleLabels } from "../util/theme-mode.js";
4
+ import { type TopNavbarStrings } from "../util/top-navbar.js";
2
5
  export interface TopNavbarNavItem {
3
6
  label: string;
4
7
  href: string;
@@ -10,13 +13,37 @@ export interface TopNavbarNavItem {
10
13
  size?: number;
11
14
  class?: string;
12
15
  }>;
16
+ /** Marks the link and opens it in a new tab. Defaults to true for absolute URLs. */
17
+ external?: boolean;
13
18
  }
19
+ export type TopNavbarCollapseAt = "sm" | "md" | "lg" | "xl";
20
+ /**
21
+ * How the links open below `collapseAt`: `sheet` slides a modal sheet up
22
+ * from the bottom of the screen; `inline` opens a panel under the bar,
23
+ * which pushes the page down (the menu of 8.1 and earlier).
24
+ */
25
+ export type TopNavbarMenu = "sheet" | "inline";
14
26
  interface Props {
15
27
  brand?: string;
16
28
  brandHref?: string;
17
29
  /** `aria-label` on `<nav>` when multiple nav landmarks exist. */
18
30
  ariaLabel?: string;
19
31
  showThemeToggle?: boolean;
32
+ /**
33
+ * How the theme toggle steps: `"two"` flips light and dark, `"three"`
34
+ * goes through system, light and dark. As `modes` of ThemeToggle.
35
+ */
36
+ themeModes?: "two" | "three";
37
+ /**
38
+ * The `localStorage` key the theme choice is kept under; `null` keeps
39
+ * nothing. As `storageKey` of ThemeToggle, whose default applies when
40
+ * this is left out.
41
+ */
42
+ themeStorageKey?: string | null;
43
+ /** The texts of the theme toggle's name, for another language. As `labels` of ThemeToggle. */
44
+ themeLabels?: Partial<ThemeToggleLabels>;
45
+ /** The words the bar says by itself: the menu button's two names, the name of the menu sheet and the note on a link that opens a new tab. */
46
+ strings?: Partial<TopNavbarStrings>;
20
47
  class?: string;
21
48
  /** @deprecated use `class`. */
22
49
  className?: string;
@@ -25,8 +52,28 @@ interface Props {
25
52
  items?: TopNavbarNavItem[];
26
53
  navVariant?: "header" | "sidebar";
27
54
  currentPath?: string;
55
+ /**
56
+ * Where the links move from the phone menu into the bar: at this
57
+ * breakpoint and wider they sit in the row. Raise it when the row has
58
+ * more items than fit at 768px.
59
+ */
60
+ collapseAt?: TopNavbarCollapseAt;
61
+ /**
62
+ * How the links open below `collapseAt`. `sheet` (the default) is a
63
+ * bottom sheet: a modal dialog that is as tall as its content, up to
64
+ * the full screen below the status bar, and scrolls inside. `inline` is a panel under the
65
+ * bar, a disclosure that pushes the page down and traps nothing.
66
+ */
67
+ menu?: TopNavbarMenu;
28
68
  preventNavigation?: boolean;
29
69
  onclick?: (event: Event) => void;
70
+ /**
71
+ * When the bar shows the glass and the hairline. `auto` follows the
72
+ * scroll position: flush at the top, glass once content is under it
73
+ * (and while the inline phone menu is open). `always` is glass all the time,
74
+ * `never` flush all the time.
75
+ */
76
+ scrollEdge?: ScrollEdgeMode;
30
77
  }
31
78
  type $$ComponentProps = Props & {
32
79
  nav?: Snippet;