zabi-components 7.0.2 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (433) hide show
  1. package/CHANGELOG.md +1924 -0
  2. package/README.md +287 -11
  3. package/THEME.md +149 -332
  4. package/THEMING.md +1329 -163
  5. package/dist/atoms/ActionPanel.svelte +24 -9
  6. package/dist/atoms/ActionPanel.svelte.d.ts +2 -0
  7. package/dist/atoms/Avatar.svelte +104 -0
  8. package/dist/atoms/Avatar.svelte.d.ts +34 -0
  9. package/dist/atoms/Badge.svelte +124 -58
  10. package/dist/atoms/Badge.svelte.d.ts +17 -5
  11. package/dist/atoms/Button.svelte +212 -62
  12. package/dist/atoms/Button.svelte.d.ts +33 -2
  13. package/dist/atoms/Card.svelte +31 -28
  14. package/dist/atoms/Card.svelte.d.ts +5 -2
  15. package/dist/atoms/CardContent.svelte +10 -2
  16. package/dist/atoms/CardContent.svelte.d.ts +2 -0
  17. package/dist/atoms/CardFooter.svelte +10 -2
  18. package/dist/atoms/CardFooter.svelte.d.ts +2 -0
  19. package/dist/atoms/CardHeader.svelte +26 -6
  20. package/dist/atoms/CardHeader.svelte.d.ts +2 -0
  21. package/dist/atoms/Checkbox.svelte +52 -21
  22. package/dist/atoms/Checkbox.svelte.d.ts +6 -2
  23. package/dist/atoms/CodeBlock.svelte +35 -9
  24. package/dist/atoms/CodeBlock.svelte.d.ts +13 -3
  25. package/dist/atoms/ColorPicker.svelte +272 -52
  26. package/dist/atoms/ColorPicker.svelte.d.ts +6 -1
  27. package/dist/atoms/Container.svelte +17 -10
  28. package/dist/atoms/DateField.svelte +107 -0
  29. package/dist/atoms/DateField.svelte.d.ts +53 -0
  30. package/dist/atoms/Divider.svelte +4 -4
  31. package/dist/atoms/FeatureCard.svelte +4 -3
  32. package/dist/atoms/FieldMessages.svelte +69 -0
  33. package/dist/atoms/FieldMessages.svelte.d.ts +23 -0
  34. package/dist/atoms/FloatingActionButton.svelte +174 -0
  35. package/dist/atoms/FloatingActionButton.svelte.d.ts +51 -0
  36. package/dist/atoms/Heading.svelte +93 -32
  37. package/dist/atoms/Heading.svelte.d.ts +24 -4
  38. package/dist/atoms/IconButton.svelte +242 -53
  39. package/dist/atoms/IconButton.svelte.d.ts +49 -4
  40. package/dist/atoms/Input.svelte +267 -74
  41. package/dist/atoms/Input.svelte.d.ts +32 -2
  42. package/dist/atoms/List.svelte +13 -2
  43. package/dist/atoms/List.svelte.d.ts +2 -0
  44. package/dist/atoms/ListItem.svelte +44 -11
  45. package/dist/atoms/ListItem.svelte.d.ts +5 -0
  46. package/dist/atoms/ListItemLeading.svelte +4 -2
  47. package/dist/atoms/ListItemLeading.svelte.d.ts +1 -1
  48. package/dist/atoms/OptimizedImage.svelte +11 -2
  49. package/dist/atoms/OptimizedImage.svelte.d.ts +2 -0
  50. package/dist/atoms/Progress.svelte +31 -9
  51. package/dist/atoms/Progress.svelte.d.ts +6 -0
  52. package/dist/atoms/Radio.svelte +23 -3
  53. package/dist/atoms/Radio.svelte.d.ts +6 -2
  54. package/dist/atoms/Rating.svelte +494 -0
  55. package/dist/atoms/Rating.svelte.d.ts +56 -0
  56. package/dist/atoms/Select.svelte +475 -135
  57. package/dist/atoms/Select.svelte.d.ts +52 -2
  58. package/dist/atoms/SelectionControl.svelte +99 -18
  59. package/dist/atoms/SelectionControl.svelte.d.ts +6 -3
  60. package/dist/atoms/Skeleton.svelte +5 -3
  61. package/dist/atoms/Slider.svelte +309 -0
  62. package/dist/atoms/Slider.svelte.d.ts +40 -0
  63. package/dist/atoms/Spinner.svelte +57 -0
  64. package/dist/atoms/Spinner.svelte.d.ts +19 -0
  65. package/dist/atoms/Table.svelte +152 -6
  66. package/dist/atoms/Table.svelte.d.ts +15 -3
  67. package/dist/atoms/TemporalField.svelte +140 -0
  68. package/dist/atoms/TemporalField.svelte.d.ts +32 -0
  69. package/dist/atoms/Text.svelte +56 -15
  70. package/dist/atoms/Text.svelte.d.ts +13 -4
  71. package/dist/atoms/Textarea.svelte +57 -45
  72. package/dist/atoms/Textarea.svelte.d.ts +14 -2
  73. package/dist/atoms/ThemeToggle.svelte +238 -56
  74. package/dist/atoms/ThemeToggle.svelte.d.ts +28 -3
  75. package/dist/atoms/TimeField.svelte +108 -0
  76. package/dist/atoms/TimeField.svelte.d.ts +54 -0
  77. package/dist/atoms/Toast.svelte +25 -10
  78. package/dist/atoms/Toast.svelte.d.ts +2 -0
  79. package/dist/atoms/Toggle.svelte +60 -19
  80. package/dist/atoms/Toggle.svelte.d.ts +18 -2
  81. package/dist/atoms/Tooltip.svelte +583 -40
  82. package/dist/atoms/Tooltip.svelte.d.ts +26 -0
  83. package/dist/atoms/ZabiStringsProvider.svelte +45 -0
  84. package/dist/atoms/ZabiStringsProvider.svelte.d.ts +27 -0
  85. package/dist/atoms/index.d.ts +17 -0
  86. package/dist/atoms/index.js +12 -0
  87. package/dist/atoms/selection-control.styles.d.ts +36 -5
  88. package/dist/atoms/selection-control.styles.js +40 -9
  89. package/dist/components/atoms/index.d.ts +17 -0
  90. package/dist/components/atoms/index.d.ts.map +1 -1
  91. package/dist/components/atoms/selection-control.styles.d.ts +36 -5
  92. package/dist/components/atoms/selection-control.styles.d.ts.map +1 -1
  93. package/dist/components/index.d.ts +83 -3
  94. package/dist/components/index.d.ts.map +1 -1
  95. package/dist/components/molecules/index.d.ts +46 -0
  96. package/dist/components/molecules/index.d.ts.map +1 -1
  97. package/dist/components/molecules/toast-store.d.ts +53 -7
  98. package/dist/components/molecules/toast-store.d.ts.map +1 -1
  99. package/dist/components/organisms/index.d.ts +6 -0
  100. package/dist/components/organisms/index.d.ts.map +1 -1
  101. package/dist/components/types/variants.d.ts +15 -1
  102. package/dist/components/types/variants.d.ts.map +1 -1
  103. package/dist/components/util/app-shell.d.ts +28 -0
  104. package/dist/components/util/app-shell.d.ts.map +1 -0
  105. package/dist/components/util/avatar.d.ts +36 -0
  106. package/dist/components/util/avatar.d.ts.map +1 -0
  107. package/dist/components/util/bottom-sheet.d.ts +30 -0
  108. package/dist/components/util/bottom-sheet.d.ts.map +1 -0
  109. package/dist/components/util/bottom-tab-bar.d.ts +35 -0
  110. package/dist/components/util/bottom-tab-bar.d.ts.map +1 -0
  111. package/dist/components/util/calendar.d.ts +56 -0
  112. package/dist/components/util/calendar.d.ts.map +1 -0
  113. package/dist/components/util/cn.d.ts +8 -0
  114. package/dist/components/util/cn.d.ts.map +1 -0
  115. package/dist/components/util/collapsible.d.ts +58 -0
  116. package/dist/components/util/collapsible.d.ts.map +1 -0
  117. package/dist/components/util/confirm-dialog.d.ts +12 -0
  118. package/dist/components/util/confirm-dialog.d.ts.map +1 -0
  119. package/dist/components/util/date.d.ts +96 -0
  120. package/dist/components/util/date.d.ts.map +1 -0
  121. package/dist/components/util/drawer.d.ts +13 -0
  122. package/dist/components/util/drawer.d.ts.map +1 -0
  123. package/dist/components/util/dropdown.d.ts +47 -0
  124. package/dist/components/util/dropdown.d.ts.map +1 -0
  125. package/dist/components/util/field.d.ts +39 -0
  126. package/dist/components/util/field.d.ts.map +1 -0
  127. package/dist/components/util/fit-in-viewport.d.ts +201 -0
  128. package/dist/components/util/fit-in-viewport.d.ts.map +1 -0
  129. package/dist/components/util/focus-utils.d.ts +75 -2
  130. package/dist/components/util/focus-utils.d.ts.map +1 -1
  131. package/dist/components/util/hydration.d.ts +45 -0
  132. package/dist/components/util/hydration.d.ts.map +1 -0
  133. package/dist/components/util/keyboard-inset.d.ts +18 -0
  134. package/dist/components/util/keyboard-inset.d.ts.map +1 -0
  135. package/dist/components/util/media-grid.d.ts +51 -0
  136. package/dist/components/util/media-grid.d.ts.map +1 -0
  137. package/dist/components/util/media.d.ts +6 -0
  138. package/dist/components/util/media.d.ts.map +1 -0
  139. package/dist/components/util/overlay-chrome.d.ts +53 -0
  140. package/dist/components/util/overlay-chrome.d.ts.map +1 -0
  141. package/dist/components/util/overlay.d.ts +72 -0
  142. package/dist/components/util/overlay.d.ts.map +1 -0
  143. package/dist/components/util/photo.d.ts +215 -0
  144. package/dist/components/util/photo.d.ts.map +1 -0
  145. package/dist/components/util/portal.d.ts +23 -0
  146. package/dist/components/util/portal.d.ts.map +1 -0
  147. package/dist/components/util/pull-to-refresh.d.ts +27 -0
  148. package/dist/components/util/pull-to-refresh.d.ts.map +1 -0
  149. package/dist/components/util/radio-keys.d.ts +25 -0
  150. package/dist/components/util/radio-keys.d.ts.map +1 -0
  151. package/dist/components/util/rating.d.ts +23 -0
  152. package/dist/components/util/rating.d.ts.map +1 -0
  153. package/dist/components/util/ready-made-strings.d.ts +77 -0
  154. package/dist/components/util/ready-made-strings.d.ts.map +1 -0
  155. package/dist/components/util/scroll-padding.d.ts +20 -0
  156. package/dist/components/util/scroll-padding.d.ts.map +1 -0
  157. package/dist/components/util/segmented-control.d.ts +16 -0
  158. package/dist/components/util/segmented-control.d.ts.map +1 -0
  159. package/dist/components/util/select.d.ts +41 -0
  160. package/dist/components/util/select.d.ts.map +1 -0
  161. package/dist/components/util/sheet-drag.d.ts +60 -0
  162. package/dist/components/util/sheet-drag.d.ts.map +1 -0
  163. package/dist/components/util/sidebar.d.ts +63 -0
  164. package/dist/components/util/sidebar.d.ts.map +1 -0
  165. package/dist/components/util/sortable-list.d.ts +65 -0
  166. package/dist/components/util/sortable-list.d.ts.map +1 -0
  167. package/dist/components/util/ssr-safe.d.ts +4 -4
  168. package/dist/components/util/ssr-safe.d.ts.map +1 -1
  169. package/dist/components/util/stepper.d.ts +40 -0
  170. package/dist/components/util/stepper.d.ts.map +1 -0
  171. package/dist/components/util/swipeable-list-item.d.ts +36 -0
  172. package/dist/components/util/swipeable-list-item.d.ts.map +1 -0
  173. package/dist/components/util/theme-mode.d.ts +80 -0
  174. package/dist/components/util/theme-mode.d.ts.map +1 -0
  175. package/dist/components/util/toaster.d.ts +93 -0
  176. package/dist/components/util/toaster.d.ts.map +1 -0
  177. package/dist/components/util/tooltip.d.ts +10 -0
  178. package/dist/components/util/tooltip.d.ts.map +1 -0
  179. package/dist/components/util/top-navbar.d.ts +11 -0
  180. package/dist/components/util/top-navbar.d.ts.map +1 -0
  181. package/dist/components/util/touch-target.d.ts +31 -0
  182. package/dist/components/util/touch-target.d.ts.map +1 -0
  183. package/dist/components/util/zabi-strings.d.ts +133 -0
  184. package/dist/components/util/zabi-strings.d.ts.map +1 -0
  185. package/dist/create-theme/cli.js +169 -0
  186. package/dist/create-theme/index.d.ts +176 -0
  187. package/dist/create-theme/index.js +817 -0
  188. package/dist/create-theme/lib/ramp-math.js +128 -0
  189. package/dist/create-theme/lib/resolve.js +141 -0
  190. package/dist/create-theme/theme-data.js +1116 -0
  191. package/dist/index.d.ts +83 -3
  192. package/dist/index.js +60 -8
  193. package/dist/molecules/Alert.svelte +73 -66
  194. package/dist/molecules/Alert.svelte.d.ts +9 -1
  195. package/dist/molecules/AppBar.svelte +415 -0
  196. package/dist/molecules/AppBar.svelte.d.ts +69 -0
  197. package/dist/molecules/AvatarGroup.svelte +116 -0
  198. package/dist/molecules/AvatarGroup.svelte.d.ts +40 -0
  199. package/dist/molecules/BottomSheet.svelte +540 -0
  200. package/dist/molecules/BottomSheet.svelte.d.ts +93 -0
  201. package/dist/molecules/BottomTabBar.svelte +297 -0
  202. package/dist/molecules/BottomTabBar.svelte.d.ts +64 -0
  203. package/dist/molecules/Calendar.svelte +597 -0
  204. package/dist/molecules/Calendar.svelte.d.ts +72 -0
  205. package/dist/molecules/Collapsible.svelte +243 -0
  206. package/dist/molecules/Collapsible.svelte.d.ts +44 -0
  207. package/dist/molecules/CollapsibleGroup.svelte +126 -0
  208. package/dist/molecules/CollapsibleGroup.svelte.d.ts +14 -0
  209. package/dist/molecules/ComponentDemo.svelte +25 -5
  210. package/dist/molecules/ComponentDemo.svelte.d.ts +5 -0
  211. package/dist/molecules/ConfirmDialog.svelte +253 -0
  212. package/dist/molecules/ConfirmDialog.svelte.d.ts +55 -0
  213. package/dist/molecules/ContactForm.svelte +46 -19
  214. package/dist/molecules/ContactForm.svelte.d.ts +10 -0
  215. package/dist/molecules/Drawer.svelte +394 -0
  216. package/dist/molecules/Drawer.svelte.d.ts +55 -0
  217. package/dist/molecules/Dropdown.svelte +540 -50
  218. package/dist/molecules/Dropdown.svelte.d.ts +49 -6
  219. package/dist/molecules/DropdownItem.svelte +122 -0
  220. package/dist/molecules/DropdownItem.svelte.d.ts +28 -0
  221. package/dist/molecules/EmptyState.svelte +56 -10
  222. package/dist/molecules/EmptyState.svelte.d.ts +8 -0
  223. package/dist/molecules/Form.svelte +25 -14
  224. package/dist/molecules/Form.svelte.d.ts +13 -5
  225. package/dist/molecules/FormField.svelte +11 -2
  226. package/dist/molecules/FormField.svelte.d.ts +2 -0
  227. package/dist/molecules/Header.svelte +13 -3
  228. package/dist/molecules/Header.svelte.d.ts +2 -0
  229. package/dist/molecules/ImageUpload.svelte +422 -70
  230. package/dist/molecules/ImageUpload.svelte.d.ts +61 -5
  231. package/dist/molecules/MediaGrid.svelte +527 -0
  232. package/dist/molecules/MediaGrid.svelte.d.ts +77 -0
  233. package/dist/molecules/Modal.svelte +340 -45
  234. package/dist/molecules/Modal.svelte.d.ts +59 -7
  235. package/dist/molecules/NavigationMenu.svelte +15 -3
  236. package/dist/molecules/NavigationMenu.svelte.d.ts +4 -0
  237. package/dist/molecules/NavigationMenuContent.svelte +91 -36
  238. package/dist/molecules/NavigationMenuContent.svelte.d.ts +2 -0
  239. package/dist/molecules/NavigationMenuItem.svelte +10 -3
  240. package/dist/molecules/NavigationMenuItem.svelte.d.ts +2 -0
  241. package/dist/molecules/NavigationMenuLink.svelte +10 -2
  242. package/dist/molecules/NavigationMenuLink.svelte.d.ts +2 -0
  243. package/dist/molecules/NavigationMenuList.svelte +19 -4
  244. package/dist/molecules/NavigationMenuList.svelte.d.ts +2 -0
  245. package/dist/molecules/NavigationMenuTrigger.svelte +16 -3
  246. package/dist/molecules/NavigationMenuTrigger.svelte.d.ts +2 -0
  247. package/dist/molecules/Page.svelte +38 -2
  248. package/dist/molecules/Page.svelte.d.ts +13 -0
  249. package/dist/molecules/PhotoGrid.svelte +433 -0
  250. package/dist/molecules/PhotoGrid.svelte.d.ts +62 -0
  251. package/dist/molecules/PhotoViewer.svelte +1116 -0
  252. package/dist/molecules/PhotoViewer.svelte.d.ts +56 -0
  253. package/dist/molecules/PropsTable.svelte +26 -11
  254. package/dist/molecules/PropsTable.svelte.d.ts +5 -0
  255. package/dist/molecules/PullToRefresh.svelte +263 -0
  256. package/dist/molecules/PullToRefresh.svelte.d.ts +52 -0
  257. package/dist/molecules/RadioGroup.svelte +57 -15
  258. package/dist/molecules/RadioGroup.svelte.d.ts +16 -3
  259. package/dist/molecules/Section.svelte +13 -5
  260. package/dist/molecules/Section.svelte.d.ts +3 -1
  261. package/dist/molecules/SegmentedControl.svelte +323 -0
  262. package/dist/molecules/SegmentedControl.svelte.d.ts +34 -0
  263. package/dist/molecules/SidebarBrandHeader.svelte +24 -5
  264. package/dist/molecules/SidebarBrandHeader.svelte.d.ts +5 -0
  265. package/dist/molecules/SidebarFooter.svelte +39 -8
  266. package/dist/molecules/SidebarFooter.svelte.d.ts +5 -0
  267. package/dist/molecules/SidebarNavSection.svelte +12 -4
  268. package/dist/molecules/SidebarNavSection.svelte.d.ts +2 -0
  269. package/dist/molecules/SlideUp.svelte +253 -30
  270. package/dist/molecules/SlideUp.svelte.d.ts +31 -4
  271. package/dist/molecules/SortableList.svelte +632 -0
  272. package/dist/molecules/SortableList.svelte.d.ts +69 -0
  273. package/dist/molecules/Stepper.svelte +548 -0
  274. package/dist/molecules/Stepper.svelte.d.ts +39 -0
  275. package/dist/molecules/StickyActionBar.svelte +191 -0
  276. package/dist/molecules/StickyActionBar.svelte.d.ts +65 -0
  277. package/dist/molecules/SwipeableListItem.svelte +334 -0
  278. package/dist/molecules/SwipeableListItem.svelte.d.ts +54 -0
  279. package/dist/molecules/Tabs.svelte +254 -24
  280. package/dist/molecules/Tabs.svelte.d.ts +15 -4
  281. package/dist/molecules/Toaster.svelte +352 -6
  282. package/dist/molecules/Toaster.svelte.d.ts +95 -2
  283. package/dist/molecules/ToasterToast.svelte +279 -62
  284. package/dist/molecules/ToasterToast.svelte.d.ts +11 -0
  285. package/dist/molecules/UnsavedChangesBar.svelte +212 -0
  286. package/dist/molecules/UnsavedChangesBar.svelte.d.ts +43 -0
  287. package/dist/molecules/index.d.ts +46 -0
  288. package/dist/molecules/index.js +27 -0
  289. package/dist/molecules/toast-store.d.ts +53 -7
  290. package/dist/molecules/toast-store.js +21 -0
  291. package/dist/organisms/AppShell.svelte +196 -0
  292. package/dist/organisms/AppShell.svelte.d.ts +61 -0
  293. package/dist/organisms/SidebarAccountPanel.svelte +111 -12
  294. package/dist/organisms/SidebarAccountPanel.svelte.d.ts +21 -0
  295. package/dist/organisms/SidebarNavigation.svelte +193 -69
  296. package/dist/organisms/SidebarNavigation.svelte.d.ts +31 -1
  297. package/dist/organisms/SidebarPanel.svelte +45 -16
  298. package/dist/organisms/SidebarPanel.svelte.d.ts +2 -0
  299. package/dist/organisms/SidebarShell.svelte +263 -0
  300. package/dist/organisms/SidebarShell.svelte.d.ts +106 -0
  301. package/dist/organisms/TopNavbar.svelte +329 -33
  302. package/dist/organisms/TopNavbar.svelte.d.ts +28 -0
  303. package/dist/organisms/index.d.ts +6 -0
  304. package/dist/organisms/index.js +4 -0
  305. package/dist/routes/lib/variant-utils.d.ts +26 -0
  306. package/dist/routes/lib/{variant-utils.ts → variant-utils.js} +15 -46
  307. package/dist/types/components.d.ts +4 -0
  308. package/dist/types/components.d.ts.map +1 -1
  309. package/dist/types/events.js +1 -0
  310. package/dist/types/index.d.ts +605 -25
  311. package/dist/types/index.d.ts.map +1 -1
  312. package/dist/types/index.js +3 -0
  313. package/dist/types/variants.ts +17 -1
  314. package/dist/util/app-shell.d.ts +27 -0
  315. package/dist/util/app-shell.js +72 -0
  316. package/dist/util/avatar.d.ts +35 -0
  317. package/dist/util/avatar.js +52 -0
  318. package/dist/util/bottom-sheet.d.ts +29 -0
  319. package/dist/util/bottom-sheet.js +43 -0
  320. package/dist/util/bottom-tab-bar.d.ts +34 -0
  321. package/dist/util/bottom-tab-bar.js +54 -0
  322. package/dist/util/calendar.d.ts +55 -0
  323. package/dist/util/calendar.js +74 -0
  324. package/dist/util/cn.d.ts +7 -0
  325. package/dist/util/cn.js +48 -0
  326. package/dist/util/collapsible.d.ts +57 -0
  327. package/dist/util/collapsible.js +23 -0
  328. package/dist/util/confirm-dialog.d.ts +11 -0
  329. package/dist/util/confirm-dialog.js +2 -0
  330. package/dist/util/date.d.ts +95 -0
  331. package/dist/util/date.js +237 -0
  332. package/dist/util/drawer.d.ts +12 -0
  333. package/dist/util/drawer.js +7 -0
  334. package/dist/util/dropdown.d.ts +46 -0
  335. package/dist/util/dropdown.js +21 -0
  336. package/dist/util/field.d.ts +38 -0
  337. package/dist/util/field.js +38 -0
  338. package/dist/util/fit-in-viewport.d.ts +200 -0
  339. package/dist/util/fit-in-viewport.js +399 -0
  340. package/dist/util/focus-utils.d.ts +75 -2
  341. package/dist/util/focus-utils.js +182 -9
  342. package/dist/util/hydration.d.ts +44 -0
  343. package/dist/util/hydration.js +58 -0
  344. package/dist/util/keyboard-inset.d.ts +17 -0
  345. package/dist/util/keyboard-inset.js +53 -0
  346. package/dist/util/media-grid.d.ts +50 -0
  347. package/dist/util/media-grid.js +81 -0
  348. package/dist/util/media.d.ts +5 -0
  349. package/dist/util/media.js +10 -0
  350. package/dist/util/overlay-chrome.d.ts +52 -0
  351. package/dist/util/overlay-chrome.js +56 -0
  352. package/dist/util/overlay.d.ts +71 -0
  353. package/dist/util/overlay.js +240 -0
  354. package/dist/util/photo.d.ts +214 -0
  355. package/dist/util/photo.js +206 -0
  356. package/dist/util/portal.d.ts +22 -0
  357. package/dist/util/portal.js +50 -0
  358. package/dist/util/pull-to-refresh.d.ts +26 -0
  359. package/dist/util/pull-to-refresh.js +30 -0
  360. package/dist/util/radio-keys.d.ts +24 -0
  361. package/dist/util/radio-keys.js +49 -0
  362. package/dist/util/rating.d.ts +22 -0
  363. package/dist/util/rating.js +26 -0
  364. package/dist/util/ready-made-strings.d.ts +76 -0
  365. package/dist/util/ready-made-strings.js +67 -0
  366. package/dist/util/scroll-padding.d.ts +19 -0
  367. package/dist/util/scroll-padding.js +44 -0
  368. package/dist/util/segmented-control.d.ts +15 -0
  369. package/dist/util/segmented-control.js +1 -0
  370. package/dist/util/select.d.ts +40 -0
  371. package/dist/util/select.js +63 -0
  372. package/dist/util/sheet-drag.d.ts +59 -0
  373. package/dist/util/sheet-drag.js +241 -0
  374. package/dist/util/sidebar.d.ts +62 -0
  375. package/dist/util/sidebar.js +38 -0
  376. package/dist/util/sortable-list.d.ts +64 -0
  377. package/dist/util/sortable-list.js +53 -0
  378. package/dist/util/ssr-safe.d.ts +4 -4
  379. package/dist/util/stepper.d.ts +39 -0
  380. package/dist/util/stepper.js +38 -0
  381. package/dist/util/swipeable-list-item.d.ts +35 -0
  382. package/dist/util/swipeable-list-item.js +32 -0
  383. package/dist/util/theme-mode.d.ts +79 -0
  384. package/dist/util/theme-mode.js +121 -0
  385. package/dist/util/toaster.d.ts +92 -0
  386. package/dist/util/toaster.js +75 -0
  387. package/dist/util/tooltip.d.ts +9 -0
  388. package/dist/util/tooltip.js +18 -0
  389. package/dist/util/top-navbar.d.ts +10 -0
  390. package/dist/util/top-navbar.js +5 -0
  391. package/dist/util/touch-target.d.ts +30 -0
  392. package/dist/util/touch-target.js +30 -0
  393. package/dist/util/zabi-strings.d.ts +132 -0
  394. package/dist/util/zabi-strings.js +105 -0
  395. package/dist/zabi-components-colors.css +5 -2
  396. package/dist/zabi-components-theme-dark-only.css +574 -225
  397. package/dist/zabi-components-theme-dark.css +574 -225
  398. package/dist/zabi-components-theme-only.css +1680 -317
  399. package/dist/zabi-components-theme.css +1680 -317
  400. package/dist/zabi-components.css +4473 -1158
  401. package/docs/lucide-icons.md +41 -16
  402. package/docs/theme-imports.md +81 -11
  403. package/package.json +51 -29
  404. package/dist/lib/layout-width-tokens.d.ts +0 -17
  405. package/dist/lib/layout-width-tokens.d.ts.map +0 -1
  406. package/dist/lib/showcase/component-docs/Button.d.ts +0 -3
  407. package/dist/lib/showcase/component-docs/Button.d.ts.map +0 -1
  408. package/dist/lib/showcase/component-docs/Input.d.ts +0 -3
  409. package/dist/lib/showcase/component-docs/Input.d.ts.map +0 -1
  410. package/dist/lib/showcase/component-docs/List.d.ts +0 -3
  411. package/dist/lib/showcase/component-docs/List.d.ts.map +0 -1
  412. package/dist/lib/showcase/component-docs/Modal.d.ts +0 -3
  413. package/dist/lib/showcase/component-docs/Modal.d.ts.map +0 -1
  414. package/dist/lib/showcase/component-docs/Radio.d.ts +0 -3
  415. package/dist/lib/showcase/component-docs/Radio.d.ts.map +0 -1
  416. package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts +0 -3
  417. package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts.map +0 -1
  418. package/dist/lib/showcase/component-docs/Skeleton.d.ts +0 -3
  419. package/dist/lib/showcase/component-docs/Skeleton.d.ts.map +0 -1
  420. package/dist/lib/showcase/component-docs/_shared.d.ts +0 -12
  421. package/dist/lib/showcase/component-docs/_shared.d.ts.map +0 -1
  422. package/dist/lib/showcase/component-docs/index.d.ts +0 -4
  423. package/dist/lib/showcase/component-docs/index.d.ts.map +0 -1
  424. package/dist/lib/showcase/components-catalog.d.ts +0 -3
  425. package/dist/lib/showcase/components-catalog.d.ts.map +0 -1
  426. package/dist/lib/showcase/components-showcase-constants.d.ts +0 -89
  427. package/dist/lib/showcase/components-showcase-constants.d.ts.map +0 -1
  428. package/dist/lib/showcase/docs-sidebar-helpers.d.ts +0 -15
  429. package/dist/lib/showcase/docs-sidebar-helpers.d.ts.map +0 -1
  430. package/dist/routes/lib/focus-utils.d.ts +0 -12
  431. package/dist/routes/lib/focus-utils.ts +0 -110
  432. package/dist/routes/lib/ssr-safe.ts +0 -58
  433. package/scripts/fix-lucide-svelte-icon-dts.js +0 -35
@@ -1,26 +1,64 @@
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";
5
+ import IconButton from "../atoms/IconButton.svelte";
6
+ import ExternalLink from "@lucide/svelte/icons/external-link";
7
+ import Menu from "@lucide/svelte/icons/menu";
8
+ import X from "@lucide/svelte/icons/x";
3
9
  import type { Component, Snippet } from "svelte";
10
+ import { generateId } from "../util/ssr-safe.js";
11
+ import { cn } from "../util/cn.js";
12
+ import type { ThemeToggleLabels } from "../util/theme-mode.js";
13
+ import { DEFAULT_TOP_NAVBAR_STRINGS, type TopNavbarStrings } from "../util/top-navbar.js";
14
+ import { isInsideToastRegion } from "../util/focus-utils.js";
4
15
 
5
16
  export interface TopNavbarNavItem {
6
17
  label: string;
7
18
  href: string;
8
19
  icon?: Component<{ size?: number; class?: string }>;
9
20
  iconFilled?: Component<{ size?: number; class?: string }>;
21
+ /** Marks the link and opens it in a new tab. Defaults to true for absolute URLs. */
22
+ external?: boolean;
10
23
  }
11
24
 
25
+ export type TopNavbarCollapseAt = "sm" | "md" | "lg" | "xl";
26
+
12
27
  interface Props {
13
28
  brand?: string;
14
29
  brandHref?: string;
15
30
  /** `aria-label` on `<nav>` when multiple nav landmarks exist. */
16
31
  ariaLabel?: string;
17
32
  showThemeToggle?: boolean;
33
+ /**
34
+ * How the theme toggle steps: `"two"` flips light and dark, `"three"`
35
+ * goes through system, light and dark. As `modes` of ThemeToggle.
36
+ */
37
+ themeModes?: "two" | "three";
38
+ /**
39
+ * The `localStorage` key the theme choice is kept under; `null` keeps
40
+ * nothing. As `storageKey` of ThemeToggle, whose default applies when
41
+ * this is left out.
42
+ */
43
+ themeStorageKey?: string | null;
44
+ /** The texts of the theme toggle's name, for another language. As `labels` of ThemeToggle. */
45
+ themeLabels?: Partial<ThemeToggleLabels>;
46
+ /** The words the bar says by itself: the menu button's two names and the note on a link that opens a new tab. */
47
+ strings?: Partial<TopNavbarStrings>;
48
+ class?: string;
49
+ /** @deprecated use `class`. */
18
50
  className?: string;
19
51
  /** Slim mode: only the `nav` region (no full chrome bar). */
20
52
  embedded?: boolean;
21
53
  items?: TopNavbarNavItem[];
22
54
  navVariant?: "header" | "sidebar";
23
55
  currentPath?: string;
56
+ /**
57
+ * Where the links move from the phone menu into the bar: at this
58
+ * breakpoint and wider they sit in the row. Raise it when the row has
59
+ * more items than fit at 768px.
60
+ */
61
+ collapseAt?: TopNavbarCollapseAt;
24
62
  preventNavigation?: boolean;
25
63
  onclick?: (event: Event) => void;
26
64
  }
@@ -30,11 +68,17 @@
30
68
  brandHref,
31
69
  ariaLabel,
32
70
  showThemeToggle = true,
33
- className = "",
71
+ themeModes = "two",
72
+ themeStorageKey,
73
+ themeLabels,
74
+ strings,
75
+ class: classAttr = "",
76
+ className: legacyClass = "",
34
77
  embedded = false,
35
78
  items = [],
36
79
  navVariant = "header",
37
80
  currentPath = "",
81
+ collapseAt = "md",
38
82
  preventNavigation = false,
39
83
  onclick,
40
84
  nav,
@@ -42,12 +86,141 @@
42
86
  ...restProps
43
87
  }: Props & { nav?: Snippet; actions?: Snippet } = $props();
44
88
 
89
+ /** `class` is the public prop; `className` is a deprecated alias.
90
+ * Both are merged here so existing call sites keep working. */
91
+ const className = $derived(cn(`${classAttr} ${legacyClass}`));
92
+
93
+ const mobileMenuId = generateId("topnavbar-menu");
94
+
95
+ /** The app-wide words for this component, from a `ZabiStringsProvider` above it, if there is one. */
96
+ const provided = zabiStringsFor("topNavbar");
97
+ const text = $derived(mergeStrings(DEFAULT_TOP_NAVBAR_STRINGS, provided(), strings));
98
+
45
99
  let isMenuOpen = $state(false);
100
+ let navElement = $state<HTMLElement | null>(null);
101
+ let panelElement = $state<HTMLElement | null>(null);
102
+ let menuButtonHolder = $state<HTMLElement | null>(null);
103
+
104
+ /**
105
+ * The classes that move the links between the row and the menu, written
106
+ * out for each breakpoint: Tailwind only generates a class it can read in
107
+ * the source, so `${collapseAt}:hidden` would produce nothing. `query` is
108
+ * the same breakpoint for the script (Tailwind's defaults).
109
+ */
110
+ const COLLAPSE: Record<
111
+ TopNavbarCollapseAt,
112
+ { row: string; menu: string; brand: string; query: string }
113
+ > = {
114
+ sm: { row: "hidden sm:block", menu: "sm:hidden", brand: "sm:shrink-0", query: "(min-width: 40rem)" },
115
+ md: { row: "hidden md:block", menu: "md:hidden", brand: "md:shrink-0", query: "(min-width: 48rem)" },
116
+ lg: { row: "hidden lg:block", menu: "lg:hidden", brand: "lg:shrink-0", query: "(min-width: 64rem)" },
117
+ xl: { row: "hidden xl:block", menu: "xl:hidden", brand: "xl:shrink-0", query: "(min-width: 80rem)" },
118
+ };
119
+ const collapse = $derived(COLLAPSE[collapseAt] ?? COLLAPSE.md);
46
120
 
47
121
  function toggleMenu() {
48
122
  isMenuOpen = !isMenuOpen;
49
123
  }
50
124
 
125
+ function focusMenuButton() {
126
+ menuButtonHolder?.querySelector("button")?.focus();
127
+ }
128
+
129
+ /**
130
+ * The menu is a disclosure, not a dialog: nothing is trapped or locked, so
131
+ * it has to get out of the way by itself whenever the user has moved on.
132
+ */
133
+ function handleWindowKeydown(event: KeyboardEvent) {
134
+ // A menu inside the panel (a Dropdown, say) closes on Escape first.
135
+ if (event.key !== "Escape" || !isMenuOpen || event.defaultPrevented) return;
136
+ const focusInside = !!panelElement?.contains(document.activeElement);
137
+ isMenuOpen = false;
138
+ // The link that had focus is gone; without this, focus falls to <body>.
139
+ if (focusInside) focusMenuButton();
140
+ }
141
+
142
+ /** A toast lies over the menu; a press on it, or focus in it, is not leaving the bar. */
143
+ function isOutside(event: Event): boolean {
144
+ if (isInsideToastRegion(event.target)) return false;
145
+ return !!navElement && !(event.target instanceof Node && navElement.contains(event.target));
146
+ }
147
+
148
+ /**
149
+ * A press outside the bar closes the menu once it is complete (the click),
150
+ * not when it starts. The open menu pushes the page down, so closing it
151
+ * on the way down moved the page back up under the pointer and the
152
+ * control that was pressed never got its click.
153
+ */
154
+ let pressing = false;
155
+
156
+ function handleOutsideClick(event: MouseEvent) {
157
+ if (isMenuOpen && isOutside(event)) isMenuOpen = false;
158
+ }
159
+
160
+ /**
161
+ * Focus that leaves the bar by the keyboard or from a script. A press
162
+ * moves focus too, between its `mousedown` and its click (after the
163
+ * finger has lifted, on a touch screen); that one is left to the click.
164
+ */
165
+ function handleOutsideFocus(event: FocusEvent) {
166
+ if (isMenuOpen && !pressing && isOutside(event)) isMenuOpen = false;
167
+ }
168
+
169
+ /** Following a link in the menu: the page under it is about to change. */
170
+ function handlePanelClick(event: MouseEvent) {
171
+ if (event.target instanceof Element && event.target.closest("a[href]")) {
172
+ isMenuOpen = false;
173
+ }
174
+ }
175
+
176
+ // The route changed some other way (history, a link elsewhere on the page).
177
+ let lastPath: string | undefined;
178
+ $effect(() => {
179
+ if (lastPath !== undefined && currentPath !== lastPath) isMenuOpen = false;
180
+ lastPath = currentPath;
181
+ });
182
+
183
+ // Past the breakpoint the menu button is gone, so a menu left open could
184
+ // not be closed, and it would still be open on the way back down.
185
+ $effect(() => {
186
+ if (typeof window === "undefined" || !window.matchMedia) return;
187
+ const wide = window.matchMedia(collapse.query);
188
+ const close = () => {
189
+ if (wide.matches) isMenuOpen = false;
190
+ };
191
+ wide.addEventListener("change", close);
192
+ return () => wide.removeEventListener("change", close);
193
+ });
194
+
195
+ /**
196
+ * The menu scrolls on its own only when it is taller than the room under
197
+ * the bar. A scrolling box clips whatever reaches outside it, and a
198
+ * Dropdown among the `actions` opens past the end of the menu: when the
199
+ * menu fits, that has to stay visible, as it was before the menu could
200
+ * scroll.
201
+ */
202
+ let panelScrolls = $state(false);
203
+ $effect(() => {
204
+ const panel = panelElement;
205
+ const content = panel?.firstElementChild;
206
+ if (!panel || !content) {
207
+ panelScrolls = false;
208
+ return;
209
+ }
210
+ const measure = () => {
211
+ const room = parseFloat(getComputedStyle(panel).maxHeight);
212
+ panelScrolls = Number.isFinite(room) && content.getBoundingClientRect().height > room;
213
+ };
214
+ measure();
215
+ window.addEventListener("resize", measure);
216
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(measure);
217
+ observer?.observe(content);
218
+ return () => {
219
+ window.removeEventListener("resize", measure);
220
+ observer?.disconnect();
221
+ };
222
+ });
223
+
51
224
  function handleNavLinkClick(event: MouseEvent) {
52
225
  if (preventNavigation) {
53
226
  event.preventDefault();
@@ -57,6 +230,12 @@
57
230
  }
58
231
  }
59
232
 
233
+ /** Absolute URLs leave the site, so they are marked and open in a new tab.
234
+ * `external` overrides that for same-origin links that leave the app. */
235
+ function isExternal(item: TopNavbarNavItem): boolean {
236
+ return item.external ?? (item.href.startsWith("http://") || item.href.startsWith("https://"));
237
+ }
238
+
60
239
  /** App routes: prefix match except `/` and absolute URLs (exact match). */
61
240
  function isNavItemActive(href: string): boolean {
62
241
  if (href.startsWith("http://") || href.startsWith("https://")) {
@@ -74,20 +253,65 @@
74
253
  : "flex flex-col gap-1 md:flex-row md:items-center md:justify-between";
75
254
  });
76
255
 
77
- function getNavItemClasses(): string {
78
- 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";
256
+ /** Where a list of links is drawn: in the bar, in the phone menu, or alone (`embedded`). */
257
+ type NavListPlace = "row" | "menu" | "embedded";
258
+
259
+ function getListClasses(place: NavListPlace): string {
260
+ if (place === "embedded" || navVariant === "sidebar") return ulClasses;
261
+ // The row only shows at the breakpoint and wider, and the menu only
262
+ // below it, so neither needs a breakpoint of its own.
263
+ return place === "row"
264
+ ? "flex flex-row items-center justify-between gap-1"
265
+ : "flex flex-col gap-1";
79
266
  }
80
267
 
81
- function getIconContainerClasses(): string {
82
- return "focus-ring focus-ring--nav group/nav-item flex flex-col items-center justify-center overflow-clip relative rounded-[20px] shrink-0 text-nav-menu-item transition-colors duration-200 outline-none hover:bg-nav-menu-hover hover:text-nav-menu-item-hover aria-[current=page]:bg-nav-menu-active aria-[current=page]:text-nav-menu-item-active";
268
+ function getNavItemClasses(place: NavListPlace = "embedded"): string {
269
+ const base =
270
+ "flex flex-col gap-1 grow h-full items-center justify-center min-h-0 min-w-0 relative shrink-0 cursor-pointer";
271
+ if (place === "row") return `${base} w-auto`;
272
+ if (place === "menu") return `${base} w-full`;
273
+ return `${base} w-full md:w-auto`;
274
+ }
275
+
276
+ /**
277
+ * The active state is resolved here rather than with
278
+ * `aria-[current=page]:text-nav-menu-item-active`.
279
+ *
280
+ * That variant was dead: the semantic utilities are hand-written in
281
+ * app.css and sit outside Tailwind's cascade layer, so an unlayered
282
+ * `.text-nav-menu-item` beats anything Tailwind generates for a variant of
283
+ * the same property. The fill happened to survive only because no plain
284
+ * `bg-*` class competed with it — so the selected tab painted its pill but
285
+ * kept the idle label colour (#27272a where #3c52ba was intended), which
286
+ * is exactly the "active state does almost nothing" failure the sidebar
287
+ * had.
288
+ *
289
+ * Plain classes chosen in JS cannot lose that way.
290
+ */
291
+ function getIconContainerClasses(isActive: boolean): string {
292
+ const base =
293
+ "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";
294
+ return isActive
295
+ ? // Forced colours drop the fill and recolour the label: the
296
+ // current item keeps an inset outline there, which is drawn.
297
+ cn(
298
+ base,
299
+ "bg-nav-menu-active text-nav-menu-item-active forced-colors:outline-solid forced-colors:outline-2 forced-colors:-outline-offset-2",
300
+ )
301
+ : cn(
302
+ base,
303
+ "text-nav-menu-item hover:bg-nav-menu-hover hover:text-nav-menu-item-hover active:bg-nav-menu-active",
304
+ );
83
305
  }
84
306
 
85
307
  function getStateLayerClasses(): string {
86
- return "box-border flex gap-1 h-10 items-center px-4 py-2 relative shrink-0";
308
+ return "box-border flex gap-1 h-10 pointer-coarse:h-11 items-center px-4 py-2 relative shrink-0";
87
309
  }
88
310
 
89
311
  function getLabelClasses(): string {
90
- return "font-medium leading-4 relative shrink-0 text-center text-nowrap tracking-[0.5px] whitespace-pre text-xs text-inherit";
312
+ // `whitespace-pre` used to sit beside `text-nowrap` — same property,
313
+ // and it preserved literal whitespace as a side effect.
314
+ return "font-medium leading-4 relative shrink-0 text-center text-nowrap tracking-wide text-xs text-inherit";
91
315
  }
92
316
 
93
317
  function getIconWrapperClasses(): string {
@@ -95,16 +319,20 @@
95
319
  }
96
320
  </script>
97
321
 
98
- {#snippet defaultNavList()}
99
- <ul class="{ulClasses} list-none m-0 p-0">
322
+ {#snippet defaultNavList(place: NavListPlace)}
323
+ <ul class="{getListClasses(place)} list-none m-0 p-0">
100
324
  {#each items as item (item.href)}
101
325
  {@const isActive = isNavItemActive(item.href)}
102
- <li class={getNavItemClasses()}>
326
+ {@const external = isExternal(item)}
327
+ <li class={getNavItemClasses(place)}>
328
+ <!-- In the menu the link is the whole row, not a pill as wide as its label. -->
103
329
  <a
104
330
  href={item.href}
105
- class={getIconContainerClasses()}
331
+ class={cn(getIconContainerClasses(isActive), place === "menu" && "w-full")}
106
332
  onclick={handleNavLinkClick}
107
333
  aria-current={isActive ? "page" : undefined}
334
+ target={external ? "_blank" : undefined}
335
+ rel={external ? "noopener noreferrer" : undefined}
108
336
  >
109
337
  <div class={getStateLayerClasses()}>
110
338
  {#if item.iconFilled && isActive}
@@ -118,9 +346,17 @@
118
346
  <Icon size={16} class="w-4 h-4" />
119
347
  </div>
120
348
  {/if}
121
- <p class={getLabelClasses()}>
349
+ <span class={getLabelClasses()}>
122
350
  {item.label}
123
- </p>
351
+ </span>
352
+ {#if external}
353
+ <ExternalLink
354
+ size={12}
355
+ class="shrink-0 text-current opacity-70"
356
+ aria-hidden="true"
357
+ />
358
+ <span class="sr-only">{text.opensInNewTab}</span>
359
+ {/if}
124
360
  </div>
125
361
  </a>
126
362
  </li>
@@ -128,81 +364,115 @@
128
364
  </ul>
129
365
  {/snippet}
130
366
 
367
+ <svelte:window
368
+ onkeydown={handleWindowKeydown}
369
+ onmousedowncapture={() => (pressing = true)}
370
+ onmouseupcapture={() => (pressing = false)}
371
+ onpointercancelcapture={() => (pressing = false)}
372
+ onclickcapture={handleOutsideClick}
373
+ onfocusin={handleOutsideFocus}
374
+ />
375
+
131
376
  {#if embedded}
132
377
  <nav class={className} aria-label={ariaLabel} {...restProps}>
133
378
  {#if nav}
134
379
  {@render nav()}
135
380
  {:else}
136
- {@render defaultNavList()}
381
+ {@render defaultNavList("embedded")}
137
382
  {/if}
138
383
  </nav>
139
384
  {:else}
140
385
  <nav
141
- class="border-b border-border bg-background sticky top-0 z-50 {className}"
386
+ bind:this={navElement}
387
+ class={cn("border-b border-border bg-background sticky top-0 z-sticky", className)}
142
388
  aria-label={ariaLabel}
143
389
  {...restProps}
144
390
  >
145
391
  <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
146
392
  <div class="flex justify-between items-center h-16">
147
- <div class="shrink-0">
393
+ <!-- Beside the menu button the brand gives way and is cut short,
394
+ so the button stays on screen however long the name or large the
395
+ text. In the row it keeps its full width, as before. -->
396
+ <div class={cn("flex min-w-0", collapse.brand)}>
148
397
  {#if brandHref}
149
398
  <a
150
399
  href={brandHref}
151
- class="focus-ring text-xl font-bold text-headline hover:opacity-90"
400
+ 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]"
152
401
  >
153
402
  {brand}
154
403
  </a>
155
404
  {:else}
156
- <span class="text-xl font-bold text-headline">{brand}</span>
405
+ <span class="block min-w-0 truncate text-xl font-bold text-headline">{brand}</span>
157
406
  {/if}
158
407
  </div>
159
408
 
160
- <div class="hidden md:block">
161
- <div class="ml-10 flex items-baseline space-x-4">
409
+ <div class={collapse.row}>
410
+ <div class="ml-10 flex items-center space-x-4">
162
411
  {#if nav}
163
412
  {@render nav()}
164
413
  {:else if items.length > 0}
165
- {@render defaultNavList()}
414
+ {@render defaultNavList("row")}
166
415
  {/if}
167
416
  </div>
168
417
  </div>
169
418
 
170
- <div class="hidden md:block">
419
+ <div class={collapse.row}>
171
420
  <div class="ml-4 flex items-center space-x-4">
172
421
  {@render actions?.()}
173
422
  {#if showThemeToggle}
174
- <ThemeToggle />
423
+ <ThemeToggle modes={themeModes} storageKey={themeStorageKey} labels={themeLabels} />
175
424
  {/if}
176
425
  </div>
177
426
  </div>
178
427
 
179
- <div class="md:hidden">
180
- <button
181
- type="button"
182
- class="cursor-pointer text-description hover:text-body"
428
+ <div bind:this={menuButtonHolder} class={cn("ms-2 shrink-0", collapse.menu)}>
429
+ <!--
430
+ Was a bare ☰ glyph in a text-2xl span: font-dependent,
431
+ off the icon scale, and with no disclosure semantics, so
432
+ a screen reader could not tell the menu was open.
433
+ -->
434
+ <IconButton
435
+ variant="ghost"
436
+ label={isMenuOpen ? text.closeMenu : text.openMenu}
183
437
  onclick={toggleMenu}
184
- aria-label="Toggle menu"
438
+ aria-expanded={isMenuOpen}
439
+ aria-controls={isMenuOpen ? mobileMenuId : undefined}
185
440
  >
186
- <span class="text-2xl">☰</span>
187
- </button>
441
+ {#if isMenuOpen}
442
+ <X size={20} />
443
+ {:else}
444
+ <Menu size={20} />
445
+ {/if}
446
+ </IconButton>
188
447
  </div>
189
448
  </div>
190
449
 
450
+ <!-- `aria-controls` above names this panel only while it exists.
451
+ Keeping it in the DOM, hidden, would render `nav` and `actions` a
452
+ second time on every page for every desktop visitor: duplicate ids
453
+ from the caller's snippets, and a second ThemeToggle mounted. -->
191
454
  {#if isMenuOpen}
192
- <div class="md:hidden">
455
+ <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
456
+ <div
457
+ bind:this={panelElement}
458
+ class={cn("topnavbar-menu", collapse.menu)}
459
+ id={mobileMenuId}
460
+ data-scrolls={panelScrolls ? "" : undefined}
461
+ onclick={handlePanelClick}
462
+ >
193
463
  <div
194
464
  class="px-2 pt-2 pb-3 space-y-1 sm:px-3 border-t border-border"
195
465
  >
196
466
  {#if nav}
197
467
  {@render nav()}
198
468
  {:else if items.length > 0}
199
- {@render defaultNavList()}
469
+ {@render defaultNavList("menu")}
200
470
  {/if}
201
471
  <div class="pt-4">
202
472
  {@render actions?.()}
203
473
  {#if showThemeToggle}
204
474
  <div class="mt-2">
205
- <ThemeToggle />
475
+ <ThemeToggle modes={themeModes} storageKey={themeStorageKey} labels={themeLabels} />
206
476
  </div>
207
477
  {/if}
208
478
  </div>
@@ -212,3 +482,29 @@
212
482
  </div>
213
483
  </nav>
214
484
  {/if}
485
+
486
+ <style>
487
+ /*
488
+ * The bar is sticky, so an open menu taller than the screen could never be
489
+ * scrolled into view: the page moved under it and its last control stayed
490
+ * below the fold. It takes what is left of the screen under the 4rem bar
491
+ * (and its 1px border) and scrolls on its own, without handing the scroll
492
+ * on to the page when it reaches an end. `dvh` follows a phone's
493
+ * collapsing address bar; `vh` is for browsers without it. In a web app
494
+ * installed to the home screen the bar starts below the status bar, so
495
+ * that inset comes off as well, or the menu's end would be under the
496
+ * bottom of the screen by as much.
497
+ *
498
+ * Only a menu that does not fit scrolls (`data-scrolls`, set by the
499
+ * script): a scrolling box would clip a Dropdown that opens out of it.
500
+ */
501
+ .topnavbar-menu {
502
+ max-height: calc(100vh - 4rem - 1px - env(safe-area-inset-top, 0px));
503
+ max-height: calc(100dvh - 4rem - 1px - env(safe-area-inset-top, 0px));
504
+ }
505
+
506
+ .topnavbar-menu[data-scrolls] {
507
+ overflow-y: auto;
508
+ overscroll-behavior: contain;
509
+ }
510
+ </style>
@@ -1,4 +1,6 @@
1
1
  import type { Component, Snippet } from "svelte";
2
+ import type { ThemeToggleLabels } from "../util/theme-mode.js";
3
+ import { type TopNavbarStrings } from "../util/top-navbar.js";
2
4
  export interface TopNavbarNavItem {
3
5
  label: string;
4
6
  href: string;
@@ -10,19 +12,45 @@ export interface TopNavbarNavItem {
10
12
  size?: number;
11
13
  class?: string;
12
14
  }>;
15
+ /** Marks the link and opens it in a new tab. Defaults to true for absolute URLs. */
16
+ external?: boolean;
13
17
  }
18
+ export type TopNavbarCollapseAt = "sm" | "md" | "lg" | "xl";
14
19
  interface Props {
15
20
  brand?: string;
16
21
  brandHref?: string;
17
22
  /** `aria-label` on `<nav>` when multiple nav landmarks exist. */
18
23
  ariaLabel?: string;
19
24
  showThemeToggle?: boolean;
25
+ /**
26
+ * How the theme toggle steps: `"two"` flips light and dark, `"three"`
27
+ * goes through system, light and dark. As `modes` of ThemeToggle.
28
+ */
29
+ themeModes?: "two" | "three";
30
+ /**
31
+ * The `localStorage` key the theme choice is kept under; `null` keeps
32
+ * nothing. As `storageKey` of ThemeToggle, whose default applies when
33
+ * this is left out.
34
+ */
35
+ themeStorageKey?: string | null;
36
+ /** The texts of the theme toggle's name, for another language. As `labels` of ThemeToggle. */
37
+ themeLabels?: Partial<ThemeToggleLabels>;
38
+ /** The words the bar says by itself: the menu button's two names and the note on a link that opens a new tab. */
39
+ strings?: Partial<TopNavbarStrings>;
40
+ class?: string;
41
+ /** @deprecated use `class`. */
20
42
  className?: string;
21
43
  /** Slim mode: only the `nav` region (no full chrome bar). */
22
44
  embedded?: boolean;
23
45
  items?: TopNavbarNavItem[];
24
46
  navVariant?: "header" | "sidebar";
25
47
  currentPath?: string;
48
+ /**
49
+ * Where the links move from the phone menu into the bar: at this
50
+ * breakpoint and wider they sit in the row. Raise it when the row has
51
+ * more items than fit at 768px.
52
+ */
53
+ collapseAt?: TopNavbarCollapseAt;
26
54
  preventNavigation?: boolean;
27
55
  onclick?: (event: Event) => void;
28
56
  }
@@ -1,4 +1,10 @@
1
1
  export { default as TopNavbar } from './TopNavbar.svelte';
2
2
  export { default as SidebarNavigation } from './SidebarNavigation.svelte';
3
+ export { default as SidebarShell } from './SidebarShell.svelte';
3
4
  export { default as SidebarAccountPanel } from './SidebarAccountPanel.svelte';
4
5
  export { default as SidebarPanel } from './SidebarPanel.svelte';
6
+ export { default as AppShell } from './AppShell.svelte';
7
+ export { DEFAULT_TOP_NAVBAR_STRINGS } from '../util/top-navbar.js';
8
+ export type { TopNavbarStrings } from '../util/top-navbar.js';
9
+ export { DEFAULT_SIDEBAR_ACCOUNT_PANEL_STRINGS, DEFAULT_SIDEBAR_NAVIGATION_STRINGS, } from '../util/sidebar.js';
10
+ export type { SidebarAccountPanelStrings, SidebarNavigationStrings } from '../util/sidebar.js';
@@ -1,4 +1,8 @@
1
1
  export { default as TopNavbar } from './TopNavbar.svelte';
2
2
  export { default as SidebarNavigation } from './SidebarNavigation.svelte';
3
+ export { default as SidebarShell } from './SidebarShell.svelte';
3
4
  export { default as SidebarAccountPanel } from './SidebarAccountPanel.svelte';
4
5
  export { default as SidebarPanel } from './SidebarPanel.svelte';
6
+ export { default as AppShell } from './AppShell.svelte';
7
+ export { DEFAULT_TOP_NAVBAR_STRINGS } from '../util/top-navbar.js';
8
+ export { DEFAULT_SIDEBAR_ACCOUNT_PANEL_STRINGS, DEFAULT_SIDEBAR_NAVIGATION_STRINGS, } from '../util/sidebar.js';
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Variant class helpers — semantic design tokens only (see src/app.css).
3
+ */
4
+ import type { SemanticVariant } from '../../types/variants.js';
5
+ export type Variant = SemanticVariant;
6
+ /**
7
+ * Border + focus ring classes for text inputs (Input, Textarea).
8
+ */
9
+ export declare function getInputVariantClasses(variant: Variant): string;
10
+ /**
11
+ * Card surfaces — border + background using semantic tokens.
12
+ */
13
+ export declare function getCardVariantClasses(variant: Variant): string;
14
+ /**
15
+ * Generic variant classes for borders, text, or backgrounds.
16
+ */
17
+ export declare function getVariantClasses(variant: Variant, type: 'border' | 'text' | 'bg'): string;
18
+ export declare function getAllVariantClasses(variant: Variant): {
19
+ border: string;
20
+ text: string;
21
+ bg: string;
22
+ };
23
+ /**
24
+ * Create a variant class map for use in reactive statements
25
+ */
26
+ export declare function createVariantClassMap<T extends string>(variants: T[], prefix: string): Record<T, string>;