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
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { HTMLAttributes } from "svelte/elements";
3
3
  import Badge from "../atoms/Badge.svelte";
4
- import { isDevBuild, isInsideAppShell } from "../util/app-shell.js";
4
+ import { getAppShell, isDevBuild } from "../util/app-shell.js";
5
5
  import {
6
6
  activeTabHref,
7
7
  badgeText,
@@ -9,6 +9,7 @@
9
9
  type BottomTabBarItem,
10
10
  } from "../util/bottom-tab-bar.js";
11
11
  import { cn } from "../util/cn.js";
12
+ import { resolveScrolledUnder, watchScrollEdge, type ScrollEdgeMode } from "../util/scroll-edge.js";
12
13
 
13
14
  /**
14
15
  * The navigation bar at the bottom of a phone screen: three to five
@@ -19,11 +20,11 @@
19
20
  *
20
21
  * On its own the bar is fixed over the page and reserves no room. Give the
21
22
  * page `padding-bottom` and `scroll-padding-bottom` of the bar's height
22
- * (`calc(65px + env(safe-area-inset-bottom))`), or the end of the
23
+ * (`calc(64px + env(safe-area-inset-bottom))`), or the end of the
23
24
  * content, and a focused control there, sits under it. `AppShell` does
24
25
  * this for you.
25
26
  *
26
- * The bar is one row, 65px tall, at every width and text size. It is
27
+ * The bar is one row, 64px tall, at every width and text size. It is
27
28
  * chrome: when the reader enlarges text, the room belongs to the page.
28
29
  * So its sizes are in px; a label is one line, grows with the text only
29
30
  * up to 1.3 times its size and no further than its tab has room for, and
@@ -31,6 +32,14 @@
31
32
  * fit. Where a tab is narrower than 52px (five tabs below 260px) the bar
32
33
  * shows icons only. The link is always named by its full label.
33
34
  *
35
+ * The bar is flush with the page and page-coloured at rest; once content
36
+ * is under it (not at the end of what scrolls) it turns to frosted glass
37
+ * with a hairline above it. Inside an `AppShell` the shell says when; on
38
+ * its own the bar watches the window.
39
+ *
40
+ * With `floating` the bar is a glass capsule inset from the screen edges
41
+ * instead: always glass, since content is always under it.
42
+ *
34
43
  * ```svelte
35
44
  * <BottomTabBar
36
45
  * items={[
@@ -52,6 +61,15 @@
52
61
  * SvelteKit pass `page.url.pathname`, so the server marks it too.
53
62
  */
54
63
  active?: string;
64
+ /**
65
+ * The href or path of the tab a person has just pressed, while its
66
+ * page is still on its way. It takes the mark (the pill, the heavier
67
+ * icon, the semibold label) at once, but `aria-current="page"` stays
68
+ * on the tab for `active` until `active` changes, and the pending link
69
+ * has `aria-busy`. Left out, or when it matches no tab, the mark and
70
+ * `aria-current` are on the same tab, as before.
71
+ */
72
+ pending?: string;
55
73
  /** Accessible name of the navigation landmark. */
56
74
  label?: string;
57
75
  /**
@@ -67,23 +85,54 @@
67
85
  * an `AppShell`, which places the bar itself.
68
86
  */
69
87
  position?: "fixed" | "static";
88
+ /**
89
+ * When the bar shows the glass and the hairline. `auto` follows the
90
+ * scroll position: flush at the end of the content, glass while
91
+ * content is under it. `always` is glass all the time, `never` flush
92
+ * all the time.
93
+ */
94
+ scrollEdge?: ScrollEdgeMode;
95
+ /**
96
+ * Draws the bar as a rounded capsule of glass, inset 12px from the
97
+ * screen edges (and from the home indicator), instead of edge to edge.
98
+ * Always the regular material, because content is always under it, so
99
+ * `scrollEdge` has no effect. The height a shell measures includes
100
+ * the 12px. The landmark, the safe-area insets and the position stay
101
+ * on the `<nav>`.
102
+ */
103
+ floating?: boolean;
70
104
  class?: string;
71
105
  };
72
106
 
73
107
  let {
74
108
  items,
75
109
  active,
110
+ pending,
76
111
  label = "Main",
77
112
  badgeLabel = defaultBadgeLabel,
78
113
  badgeMax = 99,
79
114
  position,
115
+ scrollEdge = "auto",
116
+ floating = false,
80
117
  class: className = "",
81
118
  ...restProps
82
119
  }: Props = $props();
83
120
 
84
- const inShell = isInsideAppShell();
121
+ const shell = getAppShell();
122
+ const inShell = shell !== undefined;
85
123
  const resolvedPosition = $derived(position ?? (inShell ? "static" : "fixed"));
86
124
 
125
+ let host: HTMLElement | undefined = $state();
126
+ /** Content is under the bar: from the shell if there is one, else from the window. */
127
+ let ownScrolled = $state(false);
128
+ $effect(() => {
129
+ if (shell || scrollEdge !== "auto" || !host) return;
130
+ return watchScrollEdge(null, (edge) => (ownScrolled = edge.bottom));
131
+ });
132
+ const scrolledUnder = $derived(
133
+ resolveScrolledUnder(scrollEdge, shell ? shell.scrolledBottom : ownScrolled),
134
+ );
135
+
87
136
  /**
88
137
  * The page's own path, used while `active` is not given. Empty on the
89
138
  * server and until the bar has mounted: nothing here may read `location`
@@ -110,6 +159,11 @@
110
159
  });
111
160
 
112
161
  const activeHref = $derived(activeTabHref(items, active ?? locationPath));
162
+ const pendingHref = $derived(
163
+ pending !== undefined && pending !== "" ? activeTabHref(items, pending) : undefined,
164
+ );
165
+ /** The tab that wears the mark: the pending one, else the current one. */
166
+ const markHref = $derived(pendingHref ?? activeHref);
113
167
 
114
168
  $effect(() => {
115
169
  if (!isDevBuild()) return;
@@ -135,7 +189,7 @@
135
189
  // the 4px. No gap between pill and label: 32px and a 16px line in a
136
190
  // 56px box leave the same 4px above and below as at the sides.
137
191
  const base =
138
- "focus-ring focus-ring--nav flex min-h-14 w-full min-w-0 flex-col items-center justify-center gap-0 rounded-[20px] px-[4px] text-center no-underline transition-colors duration-150 motion-reduce:transition-none";
192
+ "focus-ring focus-ring--nav flex min-h-14 w-full min-w-0 flex-col items-center justify-center gap-0 rounded-[20px] px-[2px] text-center no-underline transition-colors duration-(--duration-base) motion-reduce:transition-none";
139
193
  return isActive
140
194
  ? cn(base, "font-semibold text-nav-menu-item-active")
141
195
  : cn(
@@ -146,13 +200,22 @@
146
200
  </script>
147
201
 
148
202
  <nav
203
+ bind:this={host}
149
204
  aria-label={label}
150
205
  data-position={resolvedPosition}
206
+ data-bar-edge="bottom"
207
+ data-scrolled-under={String(scrolledUnder)}
208
+ data-floating={floating ? "true" : undefined}
151
209
  class={cn(
152
- "tabbar border-t border-border-weak bg-surface-elevated [--spacing:4px]",
153
- // Clear of the home indicator, and of the rounded corners in landscape.
154
- "pb-[env(safe-area-inset-bottom)] pl-[env(safe-area-inset-left)] pr-[env(safe-area-inset-right)]",
155
- resolvedPosition === "fixed" && "fixed inset-x-0 bottom-0 z-sticky",
210
+ "tabbar [--spacing:4px]",
211
+ floating
212
+ ? // Transparent: the capsule inside carries the glass. The gaps around
213
+ // it let clicks through to the page.
214
+ "pointer-events-none pb-[calc(env(safe-area-inset-bottom)+12px)] pl-[calc(env(safe-area-inset-left)+12px)] pr-[calc(env(safe-area-inset-right)+12px)]"
215
+ : // Clear of the home indicator, and of the rounded corners in landscape.
216
+ "material-bar pb-[env(safe-area-inset-bottom)] pl-[env(safe-area-inset-left)] pr-[env(safe-area-inset-right)]",
217
+ // The layer of the material is placed against the bar, so it is always positioned.
218
+ resolvedPosition === "fixed" ? "fixed inset-x-0 bottom-0 z-sticky" : "relative",
156
219
  className,
157
220
  )}
158
221
  style:--tabbar-count={items.length}
@@ -161,10 +224,26 @@
161
224
  <!-- 8px between the tabs and at the sides while every tab can be 52px
162
225
  wide; less as the bar narrows, down to none, so the tabs keep the width a
163
226
  label needs for as long as there is any. See the style block. -->
227
+ {#if floating}
228
+ <!-- Radius: the tab's box is 20px and sits the list's gap (8px, less
229
+ in a narrow bar) inside the capsule on all four sides, so the capsule
230
+ is 20px plus that gap: outer radius = inner radius + padding. See
231
+ `.tabbar-capsule` in the style block. -->
232
+ <div class="tabbar-capsule material-layer-regular pointer-events-auto relative">
233
+ {@render tabs()}
234
+ </div>
235
+ {:else}
236
+ {@render tabs()}
237
+ {/if}
238
+ </nav>
239
+
240
+ {#snippet tabs()}
164
241
  <ul class="tabbar-list m-0 flex list-none items-stretch py-1">
165
242
  {#each items as item (item.href)}
166
- {@const isActive = item.href === activeHref}
167
- {@const Icon = item.icon}
243
+ {@const isActive = item.href === markHref}
244
+ {@const isCurrent = item.href === activeHref}
245
+ {@const isPending = pendingHref !== undefined && item.href === pendingHref && !isCurrent}
246
+ {@const Icon = isActive && item.activeIcon ? item.activeIcon : item.icon}
168
247
  {@const count = item.badge ?? 0}
169
248
  <li class="tabbar-tab flex">
170
249
  <!-- The name is spelled out when there is a count: built from
@@ -174,25 +253,28 @@
174
253
  <a
175
254
  href={item.href}
176
255
  class={linkClasses(isActive)}
177
- aria-current={isActive ? "page" : undefined}
256
+ aria-current={isCurrent ? "page" : undefined}
257
+ aria-busy={isPending ? "true" : undefined}
178
258
  aria-label={count > 0 ? `${item.label}, ${badgeLabel(count, item)}` : undefined}
179
259
  >
180
- <!-- The pill is the active mark. Its fill is within a shade of
181
- the bar (1.24:1 in light, 1.07:1 in dark), so by itself it
182
- marks nothing for an eye that needs contrast: the 2px
183
- outline inside it, in the action colour, is what reaches
184
- 3:1 against the bar in both themes, with the labels and
185
- in the icon-only bar alike. The icon takes the same
186
- colour. In forced-colors mode the fill is dropped and the
187
- outline is drawn in a system colour, so the active tab
188
- keeps a shape there too. Decorative with all it holds:
189
- the label names the tab. -->
260
+ <!-- The pill is the active mark, with no outline: a pill with a
261
+ ring is what keyboard focus looks like, and the bar would
262
+ seem to hold focus. Selection is not by colour alone. The
263
+ set of marks tells the active tab apart: the pill, the
264
+ icon (the filled `activeIcon`, or the same icon drawn
265
+ heavier), the semibold label and the active colour (4.5:1
266
+ for the label on the pill). WCAG 1.4.11 is met by the set,
267
+ not by the fill, which is within a shade of the bar. In
268
+ forced-colors mode the fill is dropped, so the pill gets
269
+ an outline in a system colour there only and the active
270
+ tab keeps a shape. Decorative with all it holds: the label
271
+ names the tab. -->
190
272
  <span
191
273
  aria-hidden="true"
192
274
  class={cn(
193
- "flex h-8 w-[min(56px,100%)] shrink-0 items-center justify-center rounded-pill transition-colors duration-150 motion-reduce:transition-none",
275
+ "flex h-8 w-[min(56px,100%)] shrink-0 items-center justify-center rounded-pill transition-colors duration-(--duration-base) motion-reduce:transition-none",
194
276
  isActive &&
195
- "bg-nav-menu-active text-(color:--color-action-primary) outline-2 -outline-offset-2 outline-(color:--color-action-primary)",
277
+ "bg-tabbar-active text-nav-menu-item-active forced-colors:outline-2 forced-colors:-outline-offset-2 forced-colors:outline-[color:Highlight]",
196
278
  )}
197
279
  >
198
280
  <!-- The badge is placed against the icon's own 24px
@@ -200,12 +282,17 @@
200
282
  covers that corner by 8px and no more, whatever the
201
283
  text size and however narrow the pill is. -->
202
284
  <span class="relative flex size-6 shrink-0 items-center justify-center">
203
- <Icon size={24} class="shrink-0" />
285
+ <Icon
286
+ size={24}
287
+ class="shrink-0"
288
+ strokeWidth={isActive && !item.activeIcon ? 2.5 : undefined}
289
+ />
204
290
  {#if count > 0}
205
291
  <Badge
206
292
  size="sm"
207
293
  emphasis="solid"
208
- class="tabbar-badge absolute start-4 bottom-4 h-[18px] min-h-[18px] min-w-[18px] px-[5px] py-0 text-[11px] leading-[18px] whitespace-nowrap"
294
+ variant="error"
295
+ class="tabbar-badge ring-2 ring-(color:--color-bar) absolute start-4 bottom-4 h-[18px] min-h-[18px] min-w-[18px] px-[5px] py-0 text-[11px] leading-[18px] whitespace-nowrap"
209
296
  >
210
297
  {badgeText(count, badgeMax)}
211
298
  </Badge>
@@ -219,7 +306,7 @@
219
306
  </li>
220
307
  {/each}
221
308
  </ul>
222
- </nav>
309
+ {/snippet}
223
310
 
224
311
  <style>
225
312
  /*
@@ -238,16 +325,35 @@
238
325
  * 8px apart from 308px up (a 320px phone included), closer below that,
239
326
  * and touch at 260px.
240
327
  */
241
- .tabbar-list {
328
+ .tabbar-list,
329
+ .tabbar-capsule {
242
330
  --tabbar-gap: clamp(
243
331
  0px,
244
332
  calc((100cqi - var(--tabbar-count) * 52px) / (var(--tabbar-count) + 1)),
245
333
  8px
246
334
  );
335
+ }
336
+
337
+ .tabbar-list {
247
338
  gap: var(--tabbar-gap);
248
339
  padding-inline: var(--tabbar-gap);
249
340
  }
250
341
 
342
+ /*
343
+ * The floating capsule and the tabs inside it are nested rounded shapes,
344
+ * so they are concentric: the tabs sit the same distance inside the
345
+ * capsule above and below as at its two ends (the gap, in place of the
346
+ * edge-to-edge bar's 4px), and the capsule's radius is the tab's 20px
347
+ * plus that distance. At 8px the capsule is 72px tall with a 28px corner.
348
+ */
349
+ .tabbar-capsule {
350
+ border-radius: calc(20px + var(--tabbar-gap));
351
+ }
352
+
353
+ .tabbar-capsule .tabbar-list {
354
+ padding-block: var(--tabbar-gap);
355
+ }
356
+
251
357
  /*
252
358
  * A tab is at least 44px wide for as long as that many fit the bar. When
253
359
  * they do not (five tabs below 220px), they share the bar edge to edge:
@@ -1,5 +1,6 @@
1
1
  import type { HTMLAttributes } from "svelte/elements";
2
2
  import { type BottomTabBarItem } from "../util/bottom-tab-bar.js";
3
+ import { type ScrollEdgeMode } from "../util/scroll-edge.js";
3
4
  /**
4
5
  * The navigation bar at the bottom of a phone screen: three to five
5
6
  * destinations, each an icon over a short label, within reach of a thumb.
@@ -9,11 +10,11 @@ import { type BottomTabBarItem } from "../util/bottom-tab-bar.js";
9
10
  *
10
11
  * On its own the bar is fixed over the page and reserves no room. Give the
11
12
  * page `padding-bottom` and `scroll-padding-bottom` of the bar's height
12
- * (`calc(65px + env(safe-area-inset-bottom))`), or the end of the
13
+ * (`calc(64px + env(safe-area-inset-bottom))`), or the end of the
13
14
  * content, and a focused control there, sits under it. `AppShell` does
14
15
  * this for you.
15
16
  *
16
- * The bar is one row, 65px tall, at every width and text size. It is
17
+ * The bar is one row, 64px tall, at every width and text size. It is
17
18
  * chrome: when the reader enlarges text, the room belongs to the page.
18
19
  * So its sizes are in px; a label is one line, grows with the text only
19
20
  * up to 1.3 times its size and no further than its tab has room for, and
@@ -21,6 +22,14 @@ import { type BottomTabBarItem } from "../util/bottom-tab-bar.js";
21
22
  * fit. Where a tab is narrower than 52px (five tabs below 260px) the bar
22
23
  * shows icons only. The link is always named by its full label.
23
24
  *
25
+ * The bar is flush with the page and page-coloured at rest; once content
26
+ * is under it (not at the end of what scrolls) it turns to frosted glass
27
+ * with a hairline above it. Inside an `AppShell` the shell says when; on
28
+ * its own the bar watches the window.
29
+ *
30
+ * With `floating` the bar is a glass capsule inset from the screen edges
31
+ * instead: always glass, since content is always under it.
32
+ *
24
33
  * ```svelte
25
34
  * <BottomTabBar
26
35
  * items={[
@@ -42,6 +51,15 @@ type Props = Omit<HTMLAttributes<HTMLElement>, "class"> & {
42
51
  * SvelteKit pass `page.url.pathname`, so the server marks it too.
43
52
  */
44
53
  active?: string;
54
+ /**
55
+ * The href or path of the tab a person has just pressed, while its
56
+ * page is still on its way. It takes the mark (the pill, the heavier
57
+ * icon, the semibold label) at once, but `aria-current="page"` stays
58
+ * on the tab for `active` until `active` changes, and the pending link
59
+ * has `aria-busy`. Left out, or when it matches no tab, the mark and
60
+ * `aria-current` are on the same tab, as before.
61
+ */
62
+ pending?: string;
45
63
  /** Accessible name of the navigation landmark. */
46
64
  label?: string;
47
65
  /**
@@ -57,6 +75,22 @@ type Props = Omit<HTMLAttributes<HTMLElement>, "class"> & {
57
75
  * an `AppShell`, which places the bar itself.
58
76
  */
59
77
  position?: "fixed" | "static";
78
+ /**
79
+ * When the bar shows the glass and the hairline. `auto` follows the
80
+ * scroll position: flush at the end of the content, glass while
81
+ * content is under it. `always` is glass all the time, `never` flush
82
+ * all the time.
83
+ */
84
+ scrollEdge?: ScrollEdgeMode;
85
+ /**
86
+ * Draws the bar as a rounded capsule of glass, inset 12px from the
87
+ * screen edges (and from the home indicator), instead of edge to edge.
88
+ * Always the regular material, because content is always under it, so
89
+ * `scrollEdge` has no effect. The height a shell measures includes
90
+ * the 12px. The landmark, the safe-area insets and the position stay
91
+ * on the `<nav>`.
92
+ */
93
+ floating?: boolean;
60
94
  class?: string;
61
95
  };
62
96
  declare const BottomTabBar: import("svelte").Component<Props, {}, "">;
@@ -67,6 +67,6 @@ type Props = Omit<HTMLAttributes<HTMLDivElement>, "class" | "onselect"> & {
67
67
  /** Extra classes for the host element. */
68
68
  class?: string;
69
69
  };
70
- declare const Calendar: import("svelte").Component<Props, {}, "month" | "selected">;
70
+ declare const Calendar: import("svelte").Component<Props, {}, "selected" | "month">;
71
71
  type Calendar = ReturnType<typeof Calendar>;
72
72
  export default Calendar;