zabi-components 8.1.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 (273) hide show
  1. package/CHANGELOG.md +1011 -0
  2. package/README.md +1 -1
  3. package/THEME.md +18 -5
  4. package/THEMING.md +308 -74
  5. package/dist/atoms/ActionPanel.svelte +1 -1
  6. package/dist/atoms/Avatar.svelte +19 -3
  7. package/dist/atoms/Avatar.svelte.d.ts +12 -0
  8. package/dist/atoms/Badge.svelte +37 -8
  9. package/dist/atoms/Badge.svelte.d.ts +6 -0
  10. package/dist/atoms/Block.svelte +74 -0
  11. package/dist/atoms/Block.svelte.d.ts +29 -0
  12. package/dist/atoms/Button.svelte +52 -15
  13. package/dist/atoms/Button.svelte.d.ts +3 -2
  14. package/dist/atoms/Card.svelte +181 -25
  15. package/dist/atoms/Card.svelte.d.ts +55 -3
  16. package/dist/atoms/Checkbox.svelte +33 -0
  17. package/dist/atoms/Checkbox.svelte.d.ts +27 -0
  18. package/dist/atoms/Chip.svelte +351 -0
  19. package/dist/atoms/Chip.svelte.d.ts +57 -0
  20. package/dist/atoms/CodeField.svelte +385 -0
  21. package/dist/atoms/CodeField.svelte.d.ts +61 -0
  22. package/dist/atoms/ColorPicker.svelte +1 -0
  23. package/dist/atoms/DateField.svelte +41 -3
  24. package/dist/atoms/DateField.svelte.d.ts +31 -3
  25. package/dist/atoms/FloatingActionButton.svelte +5 -3
  26. package/dist/atoms/FloatingActionButton.svelte.d.ts +1 -1
  27. package/dist/atoms/Heading.svelte +27 -1
  28. package/dist/atoms/Heading.svelte.d.ts +7 -0
  29. package/dist/atoms/IconButton.svelte +81 -9
  30. package/dist/atoms/IconButton.svelte.d.ts +13 -0
  31. package/dist/atoms/Input.svelte +10 -5
  32. package/dist/atoms/Input.svelte.d.ts +3 -2
  33. package/dist/atoms/List.svelte +3 -2
  34. package/dist/atoms/ListItem.svelte +31 -7
  35. package/dist/atoms/ListItem.svelte.d.ts +6 -0
  36. package/dist/atoms/PickerField.svelte +239 -0
  37. package/dist/atoms/PickerField.svelte.d.ts +67 -0
  38. package/dist/atoms/Progress.svelte +78 -17
  39. package/dist/atoms/Progress.svelte.d.ts +11 -1
  40. package/dist/atoms/Radio.svelte +32 -0
  41. package/dist/atoms/Radio.svelte.d.ts +26 -0
  42. package/dist/atoms/Rating.svelte +132 -6
  43. package/dist/atoms/Rating.svelte.d.ts +24 -3
  44. package/dist/atoms/Select.svelte +24 -26
  45. package/dist/atoms/Select.svelte.d.ts +12 -0
  46. package/dist/atoms/SelectionControl.svelte +99 -6
  47. package/dist/atoms/SelectionControl.svelte.d.ts +25 -0
  48. package/dist/atoms/Skeleton.svelte +114 -18
  49. package/dist/atoms/Skeleton.svelte.d.ts +30 -3
  50. package/dist/atoms/SkeletonGroup.svelte +43 -0
  51. package/dist/atoms/SkeletonGroup.svelte.d.ts +20 -0
  52. package/dist/atoms/Slider.svelte +35 -0
  53. package/dist/atoms/Sparkline.svelte +109 -0
  54. package/dist/atoms/Sparkline.svelte.d.ts +27 -0
  55. package/dist/atoms/SpeechBubble.svelte +187 -0
  56. package/dist/atoms/SpeechBubble.svelte.d.ts +33 -0
  57. package/dist/atoms/Stat.svelte +72 -0
  58. package/dist/atoms/Stat.svelte.d.ts +19 -0
  59. package/dist/atoms/TemporalField.svelte +342 -10
  60. package/dist/atoms/TemporalField.svelte.d.ts +20 -4
  61. package/dist/atoms/Textarea.svelte +100 -5
  62. package/dist/atoms/Textarea.svelte.d.ts +21 -0
  63. package/dist/atoms/TimeColumn.svelte +150 -0
  64. package/dist/atoms/TimeColumn.svelte.d.ts +25 -0
  65. package/dist/atoms/TimeField.svelte +46 -3
  66. package/dist/atoms/TimeField.svelte.d.ts +34 -3
  67. package/dist/atoms/Toast.svelte +6 -5
  68. package/dist/atoms/Toggle.svelte +61 -10
  69. package/dist/atoms/Toggle.svelte.d.ts +15 -0
  70. package/dist/atoms/Tooltip.svelte +20 -1
  71. package/dist/atoms/Tooltip.svelte.d.ts +7 -0
  72. package/dist/atoms/index.d.ts +21 -0
  73. package/dist/atoms/index.js +17 -0
  74. package/dist/atoms/selection-control.styles.d.ts +15 -4
  75. package/dist/atoms/selection-control.styles.js +16 -5
  76. package/dist/components/atoms/index.d.ts +21 -0
  77. package/dist/components/atoms/index.d.ts.map +1 -1
  78. package/dist/components/atoms/selection-control.styles.d.ts +15 -4
  79. package/dist/components/atoms/selection-control.styles.d.ts.map +1 -1
  80. package/dist/components/index.d.ts +27 -3
  81. package/dist/components/index.d.ts.map +1 -1
  82. package/dist/components/map.d.ts +11 -0
  83. package/dist/components/map.d.ts.map +1 -0
  84. package/dist/components/molecules/index.d.ts +7 -2
  85. package/dist/components/molecules/index.d.ts.map +1 -1
  86. package/dist/components/molecules/toast-store.d.ts +30 -0
  87. package/dist/components/molecules/toast-store.d.ts.map +1 -1
  88. package/dist/components/organisms/index.d.ts +6 -1
  89. package/dist/components/organisms/index.d.ts.map +1 -1
  90. package/dist/components/types/variants.d.ts +12 -3
  91. package/dist/components/types/variants.d.ts.map +1 -1
  92. package/dist/components/util/app-shell.d.ts +55 -4
  93. package/dist/components/util/app-shell.d.ts.map +1 -1
  94. package/dist/components/util/avatar.d.ts +3 -0
  95. package/dist/components/util/avatar.d.ts.map +1 -1
  96. package/dist/components/util/bottom-tab-bar.d.ts +6 -0
  97. package/dist/components/util/bottom-tab-bar.d.ts.map +1 -1
  98. package/dist/components/util/chart.d.ts +140 -0
  99. package/dist/components/util/chart.d.ts.map +1 -0
  100. package/dist/components/util/chip.d.ts +66 -0
  101. package/dist/components/util/chip.d.ts.map +1 -0
  102. package/dist/components/util/cn.d.ts.map +1 -1
  103. package/dist/components/util/code-field.d.ts +63 -0
  104. package/dist/components/util/code-field.d.ts.map +1 -0
  105. package/dist/components/util/collapsible.d.ts +6 -0
  106. package/dist/components/util/collapsible.d.ts.map +1 -1
  107. package/dist/components/util/display-voice.d.ts +10 -0
  108. package/dist/components/util/display-voice.d.ts.map +1 -0
  109. package/dist/components/util/field-control.d.ts +30 -0
  110. package/dist/components/util/field-control.d.ts.map +1 -0
  111. package/dist/components/util/field.d.ts +6 -0
  112. package/dist/components/util/field.d.ts.map +1 -1
  113. package/dist/components/util/focus-utils.d.ts +2 -0
  114. package/dist/components/util/focus-utils.d.ts.map +1 -1
  115. package/dist/components/util/map-view.d.ts +144 -0
  116. package/dist/components/util/map-view.d.ts.map +1 -0
  117. package/dist/components/util/photo.d.ts +19 -0
  118. package/dist/components/util/photo.d.ts.map +1 -1
  119. package/dist/components/util/progress.d.ts +19 -0
  120. package/dist/components/util/progress.d.ts.map +1 -0
  121. package/dist/components/util/scroll-edge.d.ts +33 -0
  122. package/dist/components/util/scroll-edge.d.ts.map +1 -0
  123. package/dist/components/util/sheet-drag.d.ts +25 -1
  124. package/dist/components/util/sheet-drag.d.ts.map +1 -1
  125. package/dist/components/util/skeleton.d.ts +15 -0
  126. package/dist/components/util/skeleton.d.ts.map +1 -0
  127. package/dist/components/util/sliding-indicator.d.ts +25 -0
  128. package/dist/components/util/sliding-indicator.d.ts.map +1 -0
  129. package/dist/components/util/temporal-field.d.ts +89 -0
  130. package/dist/components/util/temporal-field.d.ts.map +1 -0
  131. package/dist/components/util/toaster.d.ts +40 -0
  132. package/dist/components/util/toaster.d.ts.map +1 -1
  133. package/dist/components/util/tone-surface.d.ts +34 -0
  134. package/dist/components/util/tone-surface.d.ts.map +1 -0
  135. package/dist/components/util/top-navbar.d.ts +26 -2
  136. package/dist/components/util/top-navbar.d.ts.map +1 -1
  137. package/dist/components/util/zabi-strings.d.ts +9 -0
  138. package/dist/components/util/zabi-strings.d.ts.map +1 -1
  139. package/dist/create-theme/cli.js +6 -2
  140. package/dist/create-theme/index.d.ts +17 -3
  141. package/dist/create-theme/index.js +252 -35
  142. package/dist/create-theme/lib/resolve.js +97 -0
  143. package/dist/create-theme/theme-data.js +1618 -90
  144. package/dist/index.d.ts +27 -3
  145. package/dist/index.js +18 -0
  146. package/dist/map.d.ts +10 -0
  147. package/dist/map.js +9 -0
  148. package/dist/molecules/Alert.svelte +31 -12
  149. package/dist/molecules/Alert.svelte.d.ts +12 -1
  150. package/dist/molecules/AppBar.svelte +287 -28
  151. package/dist/molecules/AppBar.svelte.d.ts +62 -0
  152. package/dist/molecules/AvatarGroup.svelte +1 -1
  153. package/dist/molecules/BarChart.svelte +381 -0
  154. package/dist/molecules/BarChart.svelte.d.ts +45 -0
  155. package/dist/molecules/BottomSheet.svelte +88 -20
  156. package/dist/molecules/BottomSheet.svelte.d.ts +17 -1
  157. package/dist/molecules/BottomTabBar.svelte +134 -28
  158. package/dist/molecules/BottomTabBar.svelte.d.ts +36 -2
  159. package/dist/molecules/Calendar.svelte.d.ts +1 -1
  160. package/dist/molecules/ChipGroup.svelte +325 -0
  161. package/dist/molecules/ChipGroup.svelte.d.ts +41 -0
  162. package/dist/molecules/Collapsible.svelte +158 -54
  163. package/dist/molecules/Collapsible.svelte.d.ts +23 -6
  164. package/dist/molecules/CollapsibleGroup.svelte +16 -6
  165. package/dist/molecules/ConfirmDialog.svelte +5 -1
  166. package/dist/molecules/ConfirmDialog.svelte.d.ts +3 -0
  167. package/dist/molecules/Drawer.svelte +33 -12
  168. package/dist/molecules/Dropdown.svelte +9 -1
  169. package/dist/molecules/Dropdown.svelte.d.ts +2 -0
  170. package/dist/molecules/DropdownItem.svelte +1 -1
  171. package/dist/molecules/ImageUpload.svelte +230 -120
  172. package/dist/molecules/ImageUpload.svelte.d.ts +38 -3
  173. package/dist/molecules/LineChart.svelte +459 -0
  174. package/dist/molecules/LineChart.svelte.d.ts +53 -0
  175. package/dist/molecules/MapView.svelte +565 -0
  176. package/dist/molecules/MapView.svelte.d.ts +88 -0
  177. package/dist/molecules/Modal.svelte +11 -6
  178. package/dist/molecules/PhotoGrid.svelte +100 -10
  179. package/dist/molecules/PhotoGrid.svelte.d.ts +11 -0
  180. package/dist/molecules/PhotoViewer.svelte +121 -24
  181. package/dist/molecules/PhotoViewer.svelte.d.ts +18 -0
  182. package/dist/molecules/PieChart.svelte +421 -0
  183. package/dist/molecules/PieChart.svelte.d.ts +51 -0
  184. package/dist/molecules/RadioGroup.svelte +20 -4
  185. package/dist/molecules/RadioGroup.svelte.d.ts +5 -0
  186. package/dist/molecules/SegmentedControl.svelte +136 -3
  187. package/dist/molecules/SegmentedControl.svelte.d.ts +6 -0
  188. package/dist/molecules/SidebarFooter.svelte +1 -1
  189. package/dist/molecules/SidebarNavSection.svelte +1 -1
  190. package/dist/molecules/SortableList.svelte +1 -0
  191. package/dist/molecules/Stepper.svelte +21 -7
  192. package/dist/molecules/StickyActionBar.svelte +67 -8
  193. package/dist/molecules/StickyActionBar.svelte.d.ts +14 -1
  194. package/dist/molecules/Tabs.svelte +79 -2
  195. package/dist/molecules/Toaster.svelte +171 -7
  196. package/dist/molecules/Toaster.svelte.d.ts +60 -2
  197. package/dist/molecules/ToasterToast.svelte +292 -60
  198. package/dist/molecules/ToasterToast.svelte.d.ts +6 -0
  199. package/dist/molecules/UnsavedChangesBar.svelte +13 -2
  200. package/dist/molecules/index.d.ts +7 -2
  201. package/dist/molecules/index.js +4 -0
  202. package/dist/molecules/toast-store.d.ts +30 -0
  203. package/dist/molecules/toast-store.js +4 -1
  204. package/dist/organisms/AppNavigation.svelte +440 -0
  205. package/dist/organisms/AppNavigation.svelte.d.ts +72 -0
  206. package/dist/organisms/AppShell.svelte +539 -39
  207. package/dist/organisms/AppShell.svelte.d.ts +102 -6
  208. package/dist/organisms/AppShellHeaderPart.svelte +163 -0
  209. package/dist/organisms/AppShellHeaderPart.svelte.d.ts +57 -0
  210. package/dist/organisms/SidebarNavigation.svelte +23 -36
  211. package/dist/organisms/SidebarPanel.svelte +10 -14
  212. package/dist/organisms/SidebarPanel.svelte.d.ts +1 -0
  213. package/dist/organisms/SidebarShell.svelte +2 -3
  214. package/dist/organisms/TopNavbar.svelte +117 -28
  215. package/dist/organisms/TopNavbar.svelte.d.ts +22 -1
  216. package/dist/organisms/TopNavbarSheetContent.svelte +145 -0
  217. package/dist/organisms/TopNavbarSheetContent.svelte.d.ts +24 -0
  218. package/dist/organisms/index.d.ts +6 -1
  219. package/dist/organisms/index.js +4 -0
  220. package/dist/types/components.d.ts +17 -1
  221. package/dist/types/components.d.ts.map +1 -1
  222. package/dist/types/index.d.ts +497 -10
  223. package/dist/types/index.d.ts.map +1 -1
  224. package/dist/types/variants.ts +14 -3
  225. package/dist/util/app-shell.d.ts +55 -4
  226. package/dist/util/app-shell.js +32 -7
  227. package/dist/util/avatar.d.ts +3 -0
  228. package/dist/util/bottom-tab-bar.d.ts +6 -0
  229. package/dist/util/chart.d.ts +139 -0
  230. package/dist/util/chart.js +305 -0
  231. package/dist/util/chip.d.ts +65 -0
  232. package/dist/util/chip.js +58 -0
  233. package/dist/util/cn.js +28 -2
  234. package/dist/util/code-field.d.ts +62 -0
  235. package/dist/util/code-field.js +112 -0
  236. package/dist/util/collapsible.d.ts +6 -0
  237. package/dist/util/display-voice.d.ts +9 -0
  238. package/dist/util/display-voice.js +9 -0
  239. package/dist/util/field-control.d.ts +29 -0
  240. package/dist/util/field-control.js +32 -0
  241. package/dist/util/field.d.ts +6 -0
  242. package/dist/util/focus-utils.d.ts +2 -0
  243. package/dist/util/focus-utils.js +4 -0
  244. package/dist/util/map-view.d.ts +143 -0
  245. package/dist/util/map-view.js +121 -0
  246. package/dist/util/photo.d.ts +19 -0
  247. package/dist/util/photo.js +3 -0
  248. package/dist/util/progress.d.ts +18 -0
  249. package/dist/util/progress.js +17 -0
  250. package/dist/util/scroll-edge.d.ts +32 -0
  251. package/dist/util/scroll-edge.js +87 -0
  252. package/dist/util/sheet-drag.d.ts +25 -1
  253. package/dist/util/sheet-drag.js +71 -1
  254. package/dist/util/skeleton.d.ts +14 -0
  255. package/dist/util/skeleton.js +22 -0
  256. package/dist/util/sliding-indicator.d.ts +24 -0
  257. package/dist/util/sliding-indicator.js +44 -0
  258. package/dist/util/temporal-field.d.ts +88 -0
  259. package/dist/util/temporal-field.js +136 -0
  260. package/dist/util/toaster.d.ts +40 -0
  261. package/dist/util/toaster.js +55 -0
  262. package/dist/util/tone-surface.d.ts +33 -0
  263. package/dist/util/tone-surface.js +35 -0
  264. package/dist/util/top-navbar.d.ts +26 -2
  265. package/dist/util/top-navbar.js +23 -0
  266. package/dist/util/zabi-strings.d.ts +9 -0
  267. package/dist/zabi-components-colors.css +9 -3
  268. package/dist/zabi-components-theme-dark-only.css +474 -164
  269. package/dist/zabi-components-theme-dark.css +474 -164
  270. package/dist/zabi-components-theme-only.css +933 -99
  271. package/dist/zabi-components-theme.css +933 -99
  272. package/dist/zabi-components.css +2275 -321
  273. package/package.json +11 -1
@@ -16,8 +16,11 @@ function createToastStore() {
16
16
  title: options.title,
17
17
  detail: options.detail,
18
18
  action: options.action,
19
+ closable: options.closable,
20
+ timeLine: options.timeLine,
19
21
  };
20
- update((list) => [...list, item]);
22
+ // A toast pushed with the id of one that is showing replaces it where it stands.
23
+ update((list) => list.some((t) => t.id === id) ? list.map((t) => (t.id === id ? item : t)) : [...list, item]);
21
24
  return id;
22
25
  },
23
26
  dismiss,
@@ -0,0 +1,440 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import type { HTMLAttributes } from "svelte/elements";
4
+ import Badge from "../atoms/Badge.svelte";
5
+ import BottomTabBar from "../molecules/BottomTabBar.svelte";
6
+ import {
7
+ activeTabHref,
8
+ badgeText,
9
+ defaultBadgeLabel,
10
+ type BottomTabBarItem,
11
+ } from "../util/bottom-tab-bar.js";
12
+ import { getAppShell } from "../util/app-shell.js";
13
+ import { cn } from "../util/cn.js";
14
+
15
+ /**
16
+ * The ready-made content for `AppShell`'s `navigation` slot: one list of
17
+ * destinations that is a tab bar on a phone, a rail on a tablet and a
18
+ * sidebar on a desktop.
19
+ *
20
+ * It takes the `BottomTabBar` items and draws them in whichever form the
21
+ * shell is in, with CSS alone: it renders the tab bar and one side
22
+ * `<nav>`, and the stylesheet shows exactly one of them (the other is
23
+ * `display: none`, so only one landmark is exposed). The rail and the
24
+ * sidebar are the same list of links restyled, so nothing is mounted again
25
+ * when the window is resized past a breakpoint. Outside a shell, or in
26
+ * tabs placement, it is the tab bar.
27
+ *
28
+ * - Rail (a tablet, or `navigationPlacement="rail"`): an 80px column, each
29
+ * destination an icon in the tab bar's pill above a one-line label.
30
+ * - Sidebar (a desktop, or `"sidebar"`): a 256px column of 36px rows with
31
+ * the icon before the label and the count at the end of the row. The
32
+ * active row is the tinted fill alone.
33
+ *
34
+ * Both side forms are the page colour with one hairline on the content
35
+ * side, as tall as the shell, and clear of the safe areas. The list
36
+ * scrolls by itself when it is taller than the screen.
37
+ *
38
+ * ```svelte
39
+ * <AppShell>
40
+ * {#snippet navigation()}
41
+ * <AppNavigation {items} active={page.url.pathname} />
42
+ * {/snippet}
43
+ * …
44
+ * </AppShell>
45
+ * ```
46
+ */
47
+ type Props = Omit<HTMLAttributes<HTMLDivElement>, "class"> & {
48
+ /** The destinations, as for `BottomTabBar`: `{ href, label, icon, activeIcon?, badge? }`. */
49
+ items: BottomTabBarItem[];
50
+ /**
51
+ * The href of the active destination, or the path of the current page:
52
+ * the one for that page or the closest one above it is marked. Left
53
+ * out, the navigation reads the address of the page once it runs in
54
+ * the browser. In SvelteKit pass `page.url.pathname`, so the server
55
+ * marks it too.
56
+ */
57
+ active?: string;
58
+ /** Accessible name of the navigation landmark. */
59
+ label?: string;
60
+ /** What a count adds to the link's accessible name, after the label and a comma. */
61
+ badgeLabel?: (count: number, item: BottomTabBarItem) => string;
62
+ /** Counts above this show as `99+`. The accessible name keeps the real count. */
63
+ badgeMax?: number;
64
+ /** Draws the tab bar form as a floating capsule of glass (see `BottomTabBar`). */
65
+ floating?: boolean;
66
+ /**
67
+ * Above the list in the rail and the sidebar: a logo, a workspace
68
+ * switcher. Not shown as tabs. `placement` is the form the shell
69
+ * says it is in; before the shell has measured the viewport (the
70
+ * server, before mount) it is `sidebar`, the one a desktop shows.
71
+ */
72
+ header?: Snippet<[{ placement: "rail" | "sidebar" }]>;
73
+ /** Below the list in the rail and the sidebar: an account row. Not shown as tabs. */
74
+ footer?: Snippet<[{ placement: "rail" | "sidebar" }]>;
75
+ class?: string;
76
+ };
77
+
78
+ let {
79
+ items,
80
+ active,
81
+ label = "Main",
82
+ badgeLabel = defaultBadgeLabel,
83
+ badgeMax = 99,
84
+ floating = false,
85
+ header,
86
+ footer,
87
+ class: className = "",
88
+ ...restProps
89
+ }: Props = $props();
90
+
91
+ const shell = getAppShell();
92
+ const sidePlacement = $derived<"rail" | "sidebar">(
93
+ shell?.navigationPlacement === "rail" ? "rail" : "sidebar",
94
+ );
95
+
96
+ /**
97
+ * The page's own path, used while `active` is not given: empty on the
98
+ * server and until mounted. The same rule as the tab bar's.
99
+ */
100
+ let locationPath = $state("");
101
+ $effect(() => {
102
+ if (active !== undefined) return;
103
+ const read = () => {
104
+ locationPath = window.location.pathname;
105
+ };
106
+ read();
107
+ window.addEventListener("popstate", read);
108
+ const navigation = (window as { navigation?: EventTarget }).navigation;
109
+ navigation?.addEventListener("currententrychange", read);
110
+ return () => {
111
+ window.removeEventListener("popstate", read);
112
+ navigation?.removeEventListener("currententrychange", read);
113
+ };
114
+ });
115
+ const activeHref = $derived(activeTabHref(items, active ?? locationPath));
116
+
117
+ /**
118
+ * Colours are chosen here, not by an `aria-[current=page]:` variant: the
119
+ * hand-written colour classes sit outside Tailwind's cascade layer and
120
+ * would beat it (see TopNavbar). The geometry is in the style block.
121
+ */
122
+ function linkClasses(isActive: boolean): string {
123
+ const base =
124
+ "an-link focus-ring focus-ring--nav no-underline transition-colors duration-(--duration-base) motion-reduce:transition-none";
125
+ return isActive
126
+ ? cn(base, "font-semibold text-nav-menu-item-active")
127
+ : cn(
128
+ base,
129
+ "font-medium text-nav-menu-item hover:bg-nav-menu-hover hover:text-nav-menu-item-hover active:bg-surface-active",
130
+ );
131
+ }
132
+
133
+ /** The active mark in forced colours: the fill is dropped there, so the shape is an outline. */
134
+ const FORCED =
135
+ "forced-colors:outline-2 forced-colors:-outline-offset-2 forced-colors:outline-[color:Highlight]";
136
+ </script>
137
+
138
+ <!-- The wrapper is what the stylesheet shows or hides: one form per placement. -->
139
+ <div class={cn("an", className)} {...restProps}>
140
+ <div class="an-tabs">
141
+ <BottomTabBar {items} {active} {label} {badgeLabel} {badgeMax} {floating} />
142
+ </div>
143
+
144
+ <nav
145
+ aria-label={label}
146
+ data-an-side
147
+ class="an-side bg-surface-chrome border-e border-border-weak"
148
+ >
149
+ {#if header}
150
+ <div class="an-header">{@render header({ placement: sidePlacement })}</div>
151
+ {/if}
152
+ <ul class="an-list">
153
+ {#each items as item (item.href)}
154
+ {@const isActive = item.href === activeHref}
155
+ {@const Icon = isActive && item.activeIcon ? item.activeIcon : item.icon}
156
+ {@const count = item.badge ?? 0}
157
+ <li class="an-item">
158
+ <a
159
+ href={item.href}
160
+ class={linkClasses(isActive)}
161
+ aria-current={isActive ? "page" : undefined}
162
+ aria-label={count > 0 ? `${item.label}, ${badgeLabel(count, item)}` : undefined}
163
+ >
164
+ <!-- One mark, two shapes: the tab bar's pill around the icon in the
165
+ rail, the whole row in the sidebar. Never an outline or a bar. -->
166
+ {#if isActive}
167
+ <span
168
+ aria-hidden="true"
169
+ class={cn("an-fill an-fill-rail bg-tabbar-active", FORCED)}
170
+ ></span>
171
+ <span
172
+ aria-hidden="true"
173
+ class={cn("an-fill an-fill-side bg-nav-menu-active", FORCED)}
174
+ ></span>
175
+ {/if}
176
+ <span aria-hidden="true" class="an-icon">
177
+ <Icon
178
+ size={24}
179
+ class="an-icon-svg shrink-0"
180
+ strokeWidth={isActive && !item.activeIcon ? 2.5 : undefined}
181
+ />
182
+ </span>
183
+ <span class="an-label">{item.label}</span>
184
+ {#if count > 0}
185
+ <span aria-hidden="true" class="an-badge">
186
+ <Badge
187
+ size="sm"
188
+ emphasis="solid"
189
+ variant="error"
190
+ class="h-[18px] min-h-[18px] min-w-[18px] px-[5px] py-0 text-[11px] leading-[18px] whitespace-nowrap ring-2 ring-(color:--an-badge-ring)"
191
+ >
192
+ {badgeText(count, badgeMax)}
193
+ </Badge>
194
+ </span>
195
+ {/if}
196
+ </a>
197
+ </li>
198
+ {/each}
199
+ </ul>
200
+ {#if footer}
201
+ <div class="an-footer">{@render footer({ placement: sidePlacement })}</div>
202
+ {/if}
203
+ </nav>
204
+ </div>
205
+
206
+ <style>
207
+ /*
208
+ * The placement comes from the ancestor `[data-navigation-placement]`
209
+ * that AppShell sets, and the same two breakpoints it uses (48rem rail,
210
+ * 64rem sidebar; util/app-shell.ts). With none (outside a shell) it is the
211
+ * tab bar. Every size is in px or a custom property, so the rail and the
212
+ * sidebar are one set of rules with different values.
213
+ */
214
+ .an {
215
+ height: 100%;
216
+ }
217
+ .an-tabs {
218
+ display: block;
219
+ }
220
+ .an-side {
221
+ display: none;
222
+
223
+ /* The rail: the defaults. The sidebar's values follow below. */
224
+ --an-width: 80px;
225
+ --an-pad-x: 4px;
226
+ --an-list-gap: 8px;
227
+ --an-dir: column;
228
+ --an-justify: center;
229
+ --an-gap: 0px;
230
+ --an-link-pad-x: 4px;
231
+ --an-min-h: 56px;
232
+ --an-min-h-coarse: 56px;
233
+ --an-radius: 20px;
234
+ --an-icon-w: 56px;
235
+ --an-icon-h: 32px;
236
+ --an-icon-size: 24px;
237
+ --an-label-size: clamp(11px, 0.75rem, 15.6px);
238
+ --an-label-lh: 16px;
239
+ --an-label-align: center;
240
+ --an-label-flex: none;
241
+ --an-label-width: 100%;
242
+ --an-fill-rail-display: block;
243
+ --an-fill-side-display: none;
244
+ --an-badge-position: absolute;
245
+ --an-badge-ms: 0px;
246
+ --an-badge-ring: var(--color-surface-chrome);
247
+
248
+ box-sizing: border-box;
249
+ flex-direction: column;
250
+ height: 100%;
251
+ /* The column grows by the safe area it clears, so the width the app sees is the content's. */
252
+ width: calc(var(--an-width) + env(safe-area-inset-left, 0px));
253
+ padding-block: calc(env(safe-area-inset-top, 0px) + 8px) calc(env(safe-area-inset-bottom, 0px) + 8px);
254
+ padding-inline: calc(env(safe-area-inset-left, 0px) + var(--an-pad-x)) var(--an-pad-x);
255
+ overflow: hidden;
256
+ }
257
+ :global([dir="rtl"]) .an-side {
258
+ padding-inline: var(--an-pad-x) calc(env(safe-area-inset-right, 0px) + var(--an-pad-x));
259
+ }
260
+
261
+ /* One form at a time: the other is not rendered at all, so only one landmark is exposed. */
262
+ :global([data-navigation-placement="rail"]) .an-tabs,
263
+ :global([data-navigation-placement="sidebar"]) .an-tabs {
264
+ display: none;
265
+ }
266
+ :global([data-navigation-placement="rail"]) .an-side,
267
+ :global([data-navigation-placement="sidebar"]) .an-side {
268
+ display: flex;
269
+ }
270
+ @media (min-width: 48rem) {
271
+ :global([data-navigation-placement="auto"]) .an-tabs {
272
+ display: none;
273
+ }
274
+ :global([data-navigation-placement="auto"]) .an-side {
275
+ display: flex;
276
+ }
277
+ }
278
+
279
+ /* The sidebar: the same links, a row each. Forced, or from 64rem in auto. */
280
+ :global([data-navigation-placement="sidebar"]) .an-side {
281
+ --an-width: 256px;
282
+ --an-pad-x: 12px;
283
+ --an-list-gap: 2px;
284
+ --an-dir: row;
285
+ --an-justify: flex-start;
286
+ --an-gap: 12px;
287
+ --an-link-pad-x: 8px;
288
+ --an-min-h: 2.25rem;
289
+ --an-min-h-coarse: 2.75rem;
290
+ --an-radius: var(--radius-control);
291
+ --an-icon-w: 20px;
292
+ --an-icon-h: 20px;
293
+ --an-icon-size: 20px;
294
+ --an-label-size: 0.875rem;
295
+ --an-label-lh: 1.25rem;
296
+ --an-label-align: start;
297
+ --an-label-flex: 1 1 0;
298
+ --an-label-width: auto;
299
+ --an-fill-rail-display: none;
300
+ --an-fill-side-display: block;
301
+ --an-badge-position: static;
302
+ --an-badge-ms: auto;
303
+ --an-badge-ring: transparent;
304
+ }
305
+ @media (min-width: 64rem) {
306
+ :global([data-navigation-placement="auto"]) .an-side {
307
+ --an-width: 256px;
308
+ --an-pad-x: 12px;
309
+ --an-list-gap: 2px;
310
+ --an-dir: row;
311
+ --an-justify: flex-start;
312
+ --an-gap: 12px;
313
+ --an-link-pad-x: 8px;
314
+ --an-min-h: 2.25rem;
315
+ --an-min-h-coarse: 2.75rem;
316
+ --an-radius: var(--radius-control);
317
+ --an-icon-w: 20px;
318
+ --an-icon-h: 20px;
319
+ --an-icon-size: 20px;
320
+ --an-label-size: 0.875rem;
321
+ --an-label-lh: 1.25rem;
322
+ --an-label-align: start;
323
+ --an-label-flex: 1 1 0;
324
+ --an-label-width: auto;
325
+ --an-fill-rail-display: none;
326
+ --an-fill-side-display: block;
327
+ --an-badge-position: static;
328
+ --an-badge-ms: auto;
329
+ --an-badge-ring: transparent;
330
+ }
331
+ }
332
+
333
+ .an-header,
334
+ .an-footer {
335
+ flex: none;
336
+ min-width: 0;
337
+ }
338
+ .an-header {
339
+ margin-block-end: 8px;
340
+ }
341
+ .an-footer {
342
+ margin-block-start: 8px;
343
+ }
344
+
345
+ /* The list scrolls by itself when it is taller than the screen. */
346
+ .an-list {
347
+ display: flex;
348
+ flex: 1 1 auto;
349
+ flex-direction: column;
350
+ gap: var(--an-list-gap);
351
+ min-height: 0;
352
+ margin: 0;
353
+ /* Room for the count that sticks out above the first rail icon. */
354
+ padding: 4px 0 0;
355
+ list-style: none;
356
+ overflow-y: auto;
357
+ }
358
+ .an-item {
359
+ display: flex;
360
+ flex: none;
361
+ }
362
+
363
+ .an-link {
364
+ position: relative;
365
+ display: flex;
366
+ flex-direction: var(--an-dir);
367
+ align-items: center;
368
+ justify-content: var(--an-justify);
369
+ gap: var(--an-gap);
370
+ box-sizing: border-box;
371
+ width: 100%;
372
+ min-width: 0;
373
+ min-height: var(--an-min-h);
374
+ padding-inline: var(--an-link-pad-x);
375
+ border-radius: var(--an-radius);
376
+ }
377
+ @media (pointer: coarse) {
378
+ .an-link {
379
+ min-height: var(--an-min-h-coarse);
380
+ }
381
+ }
382
+
383
+ /* Behind the icon and the label: the pill in the rail, the row in the sidebar. */
384
+ .an-fill {
385
+ position: absolute;
386
+ pointer-events: none;
387
+ }
388
+ .an-fill-rail {
389
+ display: var(--an-fill-rail-display);
390
+ top: 4px;
391
+ inset-inline-start: calc(50% - 28px);
392
+ width: 56px;
393
+ height: 32px;
394
+ border-radius: var(--radius-pill, 9999px);
395
+ }
396
+ .an-fill-side {
397
+ display: var(--an-fill-side-display);
398
+ inset: 0;
399
+ border-radius: var(--an-radius);
400
+ }
401
+
402
+ .an-icon {
403
+ position: relative;
404
+ display: flex;
405
+ flex: none;
406
+ align-items: center;
407
+ justify-content: center;
408
+ width: var(--an-icon-w);
409
+ height: var(--an-icon-h);
410
+ }
411
+ .an-icon :global(.an-icon-svg) {
412
+ width: var(--an-icon-size);
413
+ height: var(--an-icon-size);
414
+ }
415
+
416
+ /* One line, cut with an ellipsis, never wrapped. */
417
+ .an-label {
418
+ position: relative;
419
+ display: block;
420
+ flex: var(--an-label-flex);
421
+ width: var(--an-label-width);
422
+ min-width: 0;
423
+ overflow: hidden;
424
+ font-size: var(--an-label-size);
425
+ line-height: var(--an-label-lh);
426
+ text-align: var(--an-label-align);
427
+ text-overflow: ellipsis;
428
+ white-space: nowrap;
429
+ }
430
+
431
+ /* Over the icon's upper end corner in the rail, at the end of the row in the sidebar. */
432
+ .an-badge {
433
+ position: var(--an-badge-position);
434
+ top: -2px;
435
+ inset-inline-start: calc(50% + 4px);
436
+ display: flex;
437
+ flex: none;
438
+ margin-inline-start: var(--an-badge-ms);
439
+ }
440
+ </style>
@@ -0,0 +1,72 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ import { type BottomTabBarItem } from "../util/bottom-tab-bar.js";
4
+ /**
5
+ * The ready-made content for `AppShell`'s `navigation` slot: one list of
6
+ * destinations that is a tab bar on a phone, a rail on a tablet and a
7
+ * sidebar on a desktop.
8
+ *
9
+ * It takes the `BottomTabBar` items and draws them in whichever form the
10
+ * shell is in, with CSS alone: it renders the tab bar and one side
11
+ * `<nav>`, and the stylesheet shows exactly one of them (the other is
12
+ * `display: none`, so only one landmark is exposed). The rail and the
13
+ * sidebar are the same list of links restyled, so nothing is mounted again
14
+ * when the window is resized past a breakpoint. Outside a shell, or in
15
+ * tabs placement, it is the tab bar.
16
+ *
17
+ * - Rail (a tablet, or `navigationPlacement="rail"`): an 80px column, each
18
+ * destination an icon in the tab bar's pill above a one-line label.
19
+ * - Sidebar (a desktop, or `"sidebar"`): a 256px column of 36px rows with
20
+ * the icon before the label and the count at the end of the row. The
21
+ * active row is the tinted fill alone.
22
+ *
23
+ * Both side forms are the page colour with one hairline on the content
24
+ * side, as tall as the shell, and clear of the safe areas. The list
25
+ * scrolls by itself when it is taller than the screen.
26
+ *
27
+ * ```svelte
28
+ * <AppShell>
29
+ * {#snippet navigation()}
30
+ * <AppNavigation {items} active={page.url.pathname} />
31
+ * {/snippet}
32
+ * …
33
+ * </AppShell>
34
+ * ```
35
+ */
36
+ type Props = Omit<HTMLAttributes<HTMLDivElement>, "class"> & {
37
+ /** The destinations, as for `BottomTabBar`: `{ href, label, icon, activeIcon?, badge? }`. */
38
+ items: BottomTabBarItem[];
39
+ /**
40
+ * The href of the active destination, or the path of the current page:
41
+ * the one for that page or the closest one above it is marked. Left
42
+ * out, the navigation reads the address of the page once it runs in
43
+ * the browser. In SvelteKit pass `page.url.pathname`, so the server
44
+ * marks it too.
45
+ */
46
+ active?: string;
47
+ /** Accessible name of the navigation landmark. */
48
+ label?: string;
49
+ /** What a count adds to the link's accessible name, after the label and a comma. */
50
+ badgeLabel?: (count: number, item: BottomTabBarItem) => string;
51
+ /** Counts above this show as `99+`. The accessible name keeps the real count. */
52
+ badgeMax?: number;
53
+ /** Draws the tab bar form as a floating capsule of glass (see `BottomTabBar`). */
54
+ floating?: boolean;
55
+ /**
56
+ * Above the list in the rail and the sidebar: a logo, a workspace
57
+ * switcher. Not shown as tabs. `placement` is the form the shell
58
+ * says it is in; before the shell has measured the viewport (the
59
+ * server, before mount) it is `sidebar`, the one a desktop shows.
60
+ */
61
+ header?: Snippet<[{
62
+ placement: "rail" | "sidebar";
63
+ }]>;
64
+ /** Below the list in the rail and the sidebar: an account row. Not shown as tabs. */
65
+ footer?: Snippet<[{
66
+ placement: "rail" | "sidebar";
67
+ }]>;
68
+ class?: string;
69
+ };
70
+ declare const AppNavigation: import("svelte").Component<Props, {}, "">;
71
+ type AppNavigation = ReturnType<typeof AppNavigation>;
72
+ export default AppNavigation;