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,5 +1,6 @@
1
1
  import type { Snippet } from "svelte";
2
2
  import type { HTMLAttributes } from "svelte/elements";
3
+ import { type AppShellNavigationContext, type AppShellNavigationMode } from "../util/app-shell.js";
3
4
  /**
4
5
  * The layout of a phone app: a bar at the top, content that scrolls, a
5
6
  * tab bar at the bottom, and none of it under the notch or the home
@@ -7,7 +8,9 @@ import type { HTMLAttributes } from "svelte/elements";
7
8
  *
8
9
  * The shell is exactly as tall as the visible screen (`100dvh`, so the
9
10
  * browser's own bars are accounted for) and only the middle scrolls. The
10
- * header stays at the top of that scrolling area; the footer sits below it.
11
+ * header stays at the top of that scrolling area. The footer lies over
12
+ * the bottom of it, so content passes beneath both bars: they are the
13
+ * page colour at rest and turn to glass once content is under them.
11
14
  *
12
15
  * ```svelte
13
16
  * <AppShell>
@@ -25,11 +28,40 @@ import type { HTMLAttributes } from "svelte/elements";
25
28
  * take, safe areas included, so anything inside the shell can stay clear
26
29
  * of them:
27
30
  *
28
- * - `--app-shell-top-inset`: the height of the header;
31
+ * - `--app-shell-top-inset`: the height of the header (with an `AppBar`
32
+ * `largeTitle`, the height it condenses to);
29
33
  * - `--app-shell-bottom-inset`: the height of the footer.
30
34
  *
31
- * Without a bar the value is the safe-area inset alone. The host is the
32
- * containing block for anything positioned inside it, and such an element
35
+ * Without a bar the value is the safe-area inset alone. With a footer the
36
+ * content takes the bottom inset as padding and the scroller as
37
+ * `scroll-padding-bottom`, so the end of the content and a focused field
38
+ * clear the footer.
39
+ *
40
+ * A third, on the scroller, is for what sits in the content and has to
41
+ * stay above the footer (`StickyActionBar` and `UnsavedChangesBar` read it):
42
+ *
43
+ * - `--app-shell-footer-overlay`: the bottom inset when a footer lies over
44
+ * the scroller, `0px` when there is no footer.
45
+ *
46
+ * The host also says where content is: `data-scrolled-top="true"` once it
47
+ * has scrolled under the header, `data-scrolled-bottom="true"` while it
48
+ * passes under the footer (both `"false"` on the server and before the
49
+ * first measurement). The bars read the same state.
50
+ *
51
+ * To let the consumer choose what stays, compose the header from
52
+ * `AppShellHeaderPart`s: each says `sticky` (the default) or `sticky={false}`
53
+ * and scrolls away with the content, so a bar can leave while the filter
54
+ * row stays, or a large title can go while the bar and the tabs stay. The
55
+ * shell measures the parts to stack the ones that stick, reports only
56
+ * what is stuck as `--app-shell-top-inset` and `scroll-padding-top`, and
57
+ * puts the glass and the hairline under the lowest one. Without parts the
58
+ * header is one block that sticks, as before.
59
+ *
60
+ * With `canvas` the shell is painted with the page colour and the brand
61
+ * wash (`bg-canvas`), and the header bar rests transparent so the wash
62
+ * shows through it.
63
+ *
64
+ * The host is the containing block for anything positioned inside it, and such an element
33
65
  * does not scroll with the content, so a floating button above the tab bar
34
66
  * is `absolute` with `bottom: calc(var(--app-shell-bottom-inset) + 1rem)`.
35
67
  *
@@ -37,9 +69,41 @@ import type { HTMLAttributes } from "svelte/elements";
37
69
  * `<html>`, so an overlay that is moved to `document.body` (a sheet, a
38
70
  * toast) can read them too. With more than one shell on the page, the one
39
71
  * mounted last is the one mirrored there.
72
+ *
73
+ * With `navigation` the shell holds one navigation that is drawn as tabs
74
+ * over the bottom of the screen on a phone, a rail on a tablet and a
75
+ * sidebar on a desktop, and the app writes one layout, not three
76
+ * (`AppNavigation` is the ready-made content). The snippet is rendered
77
+ * once and only CSS moves it, so the server's markup is right at every
78
+ * width and nothing jumps when scripts load. As tabs it lies over the
79
+ * bottom like `footer` (the footer then sits directly above it); as a
80
+ * rail or sidebar it is a full-height column at the inline start, and the
81
+ * header, content and footer take the rest. The host says which with
82
+ * `data-navigation-placement`, and the bottom inset counts the tabs, so a
83
+ * sticky action bar or a floating button clears them too.
84
+ *
85
+ * ```svelte
86
+ * <AppShell>
87
+ * {#snippet header()}<AppBar title="Quizrundan" largeTitle />{/snippet}
88
+ * {#snippet navigation()}<AppNavigation {items} active={page.url.pathname} />{/snippet}
89
+ * <div class="p-4">…</div>
90
+ * </AppShell>
91
+ * ```
92
+ *
93
+ * One more custom property: `--app-shell-start-inset`, the width of the
94
+ * rail or sidebar (`0px` as tabs), also mirrored onto `<html>`.
95
+ *
96
+ * With `flushTop` and no `header` the content starts at the top edge of
97
+ * the screen, under the status bar, so a block of colour at the top of it
98
+ * can run there. The content then handles the safe area itself: pad by
99
+ * `--app-shell-top-inset`, or let an `AppBar position="static"` in the
100
+ * block do it.
40
101
  */
41
102
  type Props = Omit<HTMLAttributes<HTMLDivElement>, "class" | "style"> & {
42
- /** The top bar, an `AppBar`. It handles the safe area above it. */
103
+ /**
104
+ * The top bar, an `AppBar`. It handles the safe area above it. Or
105
+ * several `AppShellHeaderPart`s, each of which sticks or scrolls away.
106
+ */
43
107
  header?: Snippet;
44
108
  /** The scrolling content, rendered in the element `contentElement` names. */
45
109
  children?: Snippet;
@@ -50,8 +114,40 @@ type Props = Omit<HTMLAttributes<HTMLDivElement>, "class" | "style"> & {
50
114
  * its own.
51
115
  */
52
116
  contentElement?: "main" | "div";
53
- /** The bottom bar, a `BottomTabBar`. It handles the safe area below it. */
117
+ /**
118
+ * The bottom bar, a `BottomTabBar`. It lies over the bottom of the
119
+ * scrolling area and handles the safe area below it.
120
+ */
54
121
  footer?: Snippet;
122
+ /**
123
+ * Paints the shell with the page colour and the brand wash (`bg-canvas`)
124
+ * instead of the plain page colour, and lets the wash show through the
125
+ * header bar at rest.
126
+ */
127
+ canvas?: boolean;
128
+ /**
129
+ * One navigation, drawn as tabs, a rail or a sidebar by
130
+ * `navigationPlacement`. Rendered once, first in the DOM: the Tab order
131
+ * is navigation, header, content. `placement` is the forced value, or
132
+ * in `auto` what the viewport says once mounted (`tabs` on the server
133
+ * and before mount); use it only to render different markup per
134
+ * placement, since the layout itself is CSS and right without it.
135
+ */
136
+ navigation?: Snippet<[AppShellNavigationContext]>;
137
+ /**
138
+ * Where the `navigation` goes. `auto` follows the viewport in CSS:
139
+ * tabs below 48rem (768px), a rail from 48rem, a sidebar from 64rem
140
+ * (1024px). The others apply at every width.
141
+ */
142
+ navigationPlacement?: AppShellNavigationMode;
143
+ /**
144
+ * Without a `header`, leaves out the padding the content has for the
145
+ * status bar and the notch, so it starts at the top of the screen and
146
+ * handles the safe area itself. `--app-shell-top-inset` keeps its
147
+ * value. With a `header` it does nothing: the header covers the safe
148
+ * area.
149
+ */
150
+ flushTop?: boolean;
55
151
  class?: string;
56
152
  /** Added after the two custom properties the shell sets. */
57
153
  style?: string;
@@ -0,0 +1,163 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import type { HTMLAttributes } from "svelte/elements";
4
+ import { getAppShell, markAppShell } from "../util/app-shell.js";
5
+ import { cn } from "../util/cn.js";
6
+
7
+ /**
8
+ * One part of an `AppShell` header, and the choice of whether it stays at
9
+ * the top of the screen or scrolls away with the content.
10
+ *
11
+ * Put the parts side by side in the shell's `header` snippet, in the order
12
+ * they are drawn: an `AppBar`, a search field, a row of filter chips,
13
+ * tabs, anything. The shell leaves a header without parts as it was (the
14
+ * whole header sticks).
15
+ *
16
+ * ```svelte
17
+ * <AppShell>
18
+ * {#snippet header()}
19
+ * <AppShellHeaderPart sticky={false}><AppBar title="Quizzes" /></AppShellHeaderPart>
20
+ * <AppShellHeaderPart class="px-4 py-2"><FilterChips /></AppShellHeaderPart>
21
+ * {/snippet}
22
+ * …
23
+ * </AppShell>
24
+ * ```
25
+ *
26
+ * A part that sticks is in the flow like any other, so nothing is measured
27
+ * to make a part that does not stick leave: it is content. Parts that
28
+ * stick stack under one another in the order of the document, with no gap
29
+ * and no overlap, however many parts that do not stick are between them.
30
+ * The shell measures the heights to place them, and reports only what
31
+ * covers content at that moment as `--app-shell-top-inset` (so
32
+ * `scroll-padding-top`, a focused field and an anchor clear exactly the
33
+ * stuck parts).
34
+ *
35
+ * The parts look like one surface: the page colour at rest, glass and one
36
+ * hairline under the lowest stuck part once content is under it. The
37
+ * status bar is covered by whatever is topmost: an `AppBar` pads for it
38
+ * itself, any other first part gets the padding, and a part that sticks
39
+ * further down has a strip of the same surface above it.
40
+ *
41
+ * Keep the parts that stick short: with more than 40% of the screen
42
+ * covered (landscape, 200% text), the lower ones stop sticking and scroll
43
+ * with the content (`data-released`).
44
+ *
45
+ * An `AppBar` with `largeTitle` condenses inside its part as it does in a
46
+ * header without parts. `collapseOnScroll` is for a header without parts;
47
+ * in a part that sticks it leaves its space behind.
48
+ */
49
+ type Props = Omit<HTMLAttributes<HTMLDivElement>, "class"> & {
50
+ /** The part's content. */
51
+ children?: Snippet;
52
+ /**
53
+ * Stays at the top of the screen under the parts that stick above it
54
+ * (`true`, the default), or scrolls away with the content (`false`).
55
+ */
56
+ sticky?: boolean;
57
+ class?: string;
58
+ };
59
+
60
+ let { children, sticky = true, class: className = "", ...restProps }: Props = $props();
61
+
62
+ const shell = getAppShell();
63
+
64
+ /** How far this part sticks above the top: a large title's row (px), told by the `AppBar` inside it. */
65
+ let overscroll = $state(0);
66
+
67
+ // The bars inside report to this part, not to the shell: the part is the thing that sticks.
68
+ if (shell) {
69
+ markAppShell({
70
+ get scrolledTop() {
71
+ return shell.scrolledTop;
72
+ },
73
+ get scrolledBottom() {
74
+ return shell.scrolledBottom;
75
+ },
76
+ get footerOverlays() {
77
+ return shell.footerOverlays;
78
+ },
79
+ get footerHeight() {
80
+ return shell.footerHeight;
81
+ },
82
+ get navigationPlacement() {
83
+ return shell.navigationPlacement;
84
+ },
85
+ setHeaderOverscroll(px) {
86
+ overscroll = px > 0 ? px : 0;
87
+ },
88
+ });
89
+ }
90
+ </script>
91
+
92
+ <!-- The glass and the hairline are the bar surface (`material-bar`). It is left
93
+ off a part that holds an `AppBar`, which has its own; the stylesheet below also
94
+ takes the hairline off every stuck part but the lowest. -->
95
+ <div
96
+ data-app-shell-part
97
+ data-sticky={String(sticky)}
98
+ data-overscroll={overscroll > 0 ? overscroll : undefined}
99
+ data-scrolled-under={sticky ? String(shell?.scrolledTop ?? false) : undefined}
100
+ class={cn("shrink-0", sticky && "material-bar", className)}
101
+ {...restProps}
102
+ >
103
+ {@render children?.()}
104
+ </div>
105
+
106
+ <style>
107
+ /*
108
+ * Global on purpose: the rules read the shell's own markup (the header
109
+ * wrapper, the strip) and the part's siblings, and a scoped class would
110
+ * not reach an AppBar inside it.
111
+ *
112
+ * A part that sticks. The offset is written by the shell once it has
113
+ * measured (`--app-shell-part-top`); until then, on the server and without
114
+ * scripts, the first part sticks at 0 (it is the top, or an AppBar that
115
+ * pads the status bar itself) or under the status bar strip, and a later
116
+ * one under an estimate of an AppBar.
117
+ */
118
+ :global([data-app-shell-part][data-sticky="true"]:not([data-released])) {
119
+ position: sticky;
120
+ z-index: var(--z-sticky);
121
+ top: var(--app-shell-part-top, env(safe-area-inset-top, 0px));
122
+ }
123
+ :global([data-app-shell-header] > [data-app-shell-part][data-sticky="true"]:not([data-released]):first-child),
124
+ :global([data-app-shell-part][data-sticky="true"]:not([data-released]):has([data-appbar])) {
125
+ top: var(--app-shell-part-top, 0px);
126
+ }
127
+ :global(
128
+ [data-app-shell-part][data-sticky="true"]:not([data-released])
129
+ ~ [data-app-shell-part][data-sticky="true"]:not([data-released])
130
+ ) {
131
+ top: var(--app-shell-part-top, calc(56px + env(safe-area-inset-top, 0px)));
132
+ }
133
+
134
+ /* Released on a short screen: the surface goes with the stickiness. */
135
+ :global([data-app-shell-part][data-released]::before) {
136
+ display: none;
137
+ }
138
+
139
+ /* An AppBar inside has the surface already. */
140
+ :global([data-app-shell-part]:has([data-appbar])::before) {
141
+ display: none;
142
+ }
143
+
144
+ /* One surface, one hairline: only the lowest stuck part draws it (in forced colours too). */
145
+ :global(
146
+ [data-app-shell-part][data-sticky="true"]:not([data-released]):has(
147
+ ~ [data-app-shell-part][data-sticky="true"]:not([data-released])
148
+ )::before
149
+ ),
150
+ :global(
151
+ [data-app-shell-part][data-sticky="true"]:not([data-released]):has(
152
+ ~ [data-app-shell-part][data-sticky="true"]:not([data-released])
153
+ )
154
+ .material-bar::before
155
+ ) {
156
+ border-color: transparent !important;
157
+ }
158
+
159
+ /* The first part reaches the top of the screen: it clears the status bar, unless an AppBar does. */
160
+ :global([data-app-shell-header] > [data-app-shell-part]:first-child:not(:has([data-appbar]))) {
161
+ padding-top: env(safe-area-inset-top, 0px);
162
+ }
163
+ </style>
@@ -0,0 +1,57 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ /**
4
+ * One part of an `AppShell` header, and the choice of whether it stays at
5
+ * the top of the screen or scrolls away with the content.
6
+ *
7
+ * Put the parts side by side in the shell's `header` snippet, in the order
8
+ * they are drawn: an `AppBar`, a search field, a row of filter chips,
9
+ * tabs, anything. The shell leaves a header without parts as it was (the
10
+ * whole header sticks).
11
+ *
12
+ * ```svelte
13
+ * <AppShell>
14
+ * {#snippet header()}
15
+ * <AppShellHeaderPart sticky={false}><AppBar title="Quizzes" /></AppShellHeaderPart>
16
+ * <AppShellHeaderPart class="px-4 py-2"><FilterChips /></AppShellHeaderPart>
17
+ * {/snippet}
18
+ * …
19
+ * </AppShell>
20
+ * ```
21
+ *
22
+ * A part that sticks is in the flow like any other, so nothing is measured
23
+ * to make a part that does not stick leave: it is content. Parts that
24
+ * stick stack under one another in the order of the document, with no gap
25
+ * and no overlap, however many parts that do not stick are between them.
26
+ * The shell measures the heights to place them, and reports only what
27
+ * covers content at that moment as `--app-shell-top-inset` (so
28
+ * `scroll-padding-top`, a focused field and an anchor clear exactly the
29
+ * stuck parts).
30
+ *
31
+ * The parts look like one surface: the page colour at rest, glass and one
32
+ * hairline under the lowest stuck part once content is under it. The
33
+ * status bar is covered by whatever is topmost: an `AppBar` pads for it
34
+ * itself, any other first part gets the padding, and a part that sticks
35
+ * further down has a strip of the same surface above it.
36
+ *
37
+ * Keep the parts that stick short: with more than 40% of the screen
38
+ * covered (landscape, 200% text), the lower ones stop sticking and scroll
39
+ * with the content (`data-released`).
40
+ *
41
+ * An `AppBar` with `largeTitle` condenses inside its part as it does in a
42
+ * header without parts. `collapseOnScroll` is for a header without parts;
43
+ * in a part that sticks it leaves its space behind.
44
+ */
45
+ type Props = Omit<HTMLAttributes<HTMLDivElement>, "class"> & {
46
+ /** The part's content. */
47
+ children?: Snippet;
48
+ /**
49
+ * Stays at the top of the screen under the parts that stick above it
50
+ * (`true`, the default), or scrolls away with the content (`false`).
51
+ */
52
+ sticky?: boolean;
53
+ class?: string;
54
+ };
55
+ declare const AppShellHeaderPart: import("svelte").Component<Props, {}, "">;
56
+ type AppShellHeaderPart = ReturnType<typeof AppShellHeaderPart>;
57
+ export default AppShellHeaderPart;
@@ -252,7 +252,7 @@
252
252
  );
253
253
 
254
254
  const iconContainerClasses = $derived(
255
- "flex size-6 shrink-0 items-center justify-center leading-none text-current",
255
+ "flex size-5 shrink-0 items-center justify-center leading-none text-current",
256
256
  );
257
257
  /** With `searchMode: "input"`, the field stays visible even if `onSearchClick` is set (e.g. Storybook). */
258
258
  const shouldRenderSearchButton = $derived(
@@ -266,17 +266,10 @@
266
266
  }
267
267
 
268
268
  /**
269
- * The selected row used to be carried by its fill alone, and the fill was
270
- * 1.05:1 against the sidebar in light mode — not a fill, a rumour. Active
271
- * and idle labels were #18181b and #27272a at the same weight, a 15-unit
272
- * difference in one channel that nobody can see. So in light mode there
273
- * was effectively no indication of where you were.
274
- *
275
- * Three cues now, only one of which is colour-discrimination dependent:
276
- * the label takes --color-nav-menu-item-active (defined all along and
277
- * never referenced — the row said `text-inherit`), a 3px bar marks the
278
- * leading edge, and the tint stays as reinforcement rather than as the
279
- * whole message.
269
+ * One indicator marks the current row: the tinted fill, with the active
270
+ * label colour (--color-nav-menu-item-active) and a heavier weight, so the
271
+ * cue does not depend on telling two similar fills apart. (A 3px bar on
272
+ * the leading edge used to mark it a second time; D127.)
280
273
  */
281
274
  function isItemActive(item: SidebarNavigationItem): boolean {
282
275
  return (
@@ -288,26 +281,25 @@
288
281
  function getNavItemClasses(item: SidebarNavigationItem): string {
289
282
  const isActive = isItemActive(item);
290
283
  const layoutClasses = isCollapsed
291
- ? "flex min-h-10 pointer-coarse:min-h-11 items-center justify-center px-0 py-2"
292
- : "flex min-h-10 pointer-coarse:min-h-11 items-center gap-3 px-2 py-2";
284
+ ? "flex min-h-9 pointer-coarse:min-h-11 items-center justify-center px-0 py-2"
285
+ : "flex min-h-9 pointer-coarse:min-h-11 items-center gap-3 px-2 py-2";
293
286
  const structural =
294
- "focus-ring focus-ring--nav relative w-full cursor-pointer rounded-control no-underline transition-colors duration-150 outline-none";
287
+ "focus-ring focus-ring--nav relative w-full cursor-pointer rounded-control no-underline transition-colors duration-(--duration-base) ease-standard outline-none";
295
288
 
296
289
  if (isActive) {
297
- // In forced colours all three cues are gone: fills (the tint and
298
- // the bar) become the canvas and every link takes the system's
299
- // link colour. An outline is a shape and is drawn there, so the
300
- // current row keeps one. Inset, so a focused row's ring (2px
290
+ // In forced colours the fill is gone and every link takes the
291
+ // system's link colour. An outline is a shape and is drawn there,
292
+ // so the current row keeps one. Inset, so a focused row's ring (2px
301
293
  // outside) reads as a different thing.
302
294
  return (
303
295
  `${structural} ${layoutClasses} bg-nav-menu-active ` +
304
- "text-nav-menu-item-active hover:bg-nav-menu-active " +
296
+ "font-semibold text-nav-menu-item-active hover:bg-nav-menu-active " +
305
297
  "active:bg-nav-menu-active-hover " +
306
298
  "forced-colors:outline-solid forced-colors:outline-2 forced-colors:-outline-offset-2"
307
299
  );
308
300
  }
309
301
 
310
- return `${structural} ${layoutClasses} text-nav-menu-item hover:bg-nav-menu-hover hover:text-nav-menu-item-hover active:bg-surface-active`;
302
+ return `${structural} ${layoutClasses} font-medium text-nav-menu-item hover:bg-nav-menu-hover hover:text-nav-menu-item-hover active:bg-surface-active`;
311
303
  }
312
304
 
313
305
  function handleNavigate(item: SidebarNavigationItem, event: MouseEvent) {
@@ -402,6 +394,7 @@
402
394
  <Button
403
395
  variant={searchTriggerVariant}
404
396
  size={searchTriggerSize}
397
+ class="min-h-9 pointer-coarse:min-h-11"
405
398
  fullWidth
406
399
  onclick={handleSearchClick}
407
400
  >
@@ -429,7 +422,7 @@
429
422
  bind:value={searchValue}
430
423
  placeholder={searchPlaceholder}
431
424
  aria-label={searchPlaceholder}
432
- class="focus-ring focus-ring--nav w-full min-w-0 min-h-10 pointer-coarse:min-h-11 rounded-container border-transparent !bg-transparent py-2 pl-10 text-sm ring-1 ring-input-border hover:!bg-input-hover focus:!bg-transparent"
425
+ class="focus-ring focus-ring--nav w-full min-w-0 min-h-9 pointer-coarse:min-h-11 rounded-container border-transparent !bg-transparent py-2 pl-10 text-sm ring-1 ring-input-border hover:!bg-input-hover focus:!bg-transparent"
433
426
  />
434
427
  </div>
435
428
  {/if}
@@ -438,10 +431,10 @@
438
431
 
439
432
  {#if hasFilteredItems}
440
433
  <div
441
- class="flex w-full min-w-0 flex-col divide-y divide-border"
434
+ class="flex w-full min-w-0 flex-col gap-5"
442
435
  >
443
436
  {#each primarySectionGroups as group, gi (`${gi}-${group.sectionLabel ?? "x"}`)}
444
- <div class="py-4 first:pt-1 last:pb-1">
437
+ <div>
445
438
  <SidebarNavSection
446
439
  title={group.sectionLabel ?? ""}
447
440
  sectionKey={`p-${gi}`}
@@ -473,23 +466,17 @@
473
466
  aria-current={getAriaCurrent(item)}
474
467
  aria-label={isCollapsed ? item.label : undefined}
475
468
  >
476
- {#if isItemActive(item)}
477
- <span
478
- class="absolute left-0 top-1/2 h-5 w-[3px] -translate-y-1/2 rounded-pill bg-nav-menu-item-active"
479
- aria-hidden="true"
480
- ></span>
481
- {/if}
482
469
  {#if Icon}
483
470
  <span
484
471
  class={iconContainerClasses}
485
472
  aria-hidden="true"
486
473
  >
487
- <Icon size={18} />
474
+ <Icon size={20} />
488
475
  </span>
489
476
  {/if}
490
477
  {#if !isCollapsed}
491
478
  <span
492
- class="text-sm font-medium leading-snug text-inherit"
479
+ class="text-sm leading-snug text-inherit"
493
480
  >{item.label}</span
494
481
  >
495
482
  {/if}
@@ -527,7 +514,7 @@
527
514
 
528
515
  {#if filteredSecondaryItems.length > 0}
529
516
  <div
530
- class="mt-3 border-t border-border pt-4"
517
+ class="mt-5"
531
518
  role="presentation"
532
519
  >
533
520
  <SidebarNavSection
@@ -553,12 +540,12 @@
553
540
  class={iconContainerClasses}
554
541
  aria-hidden="true"
555
542
  >
556
- <Icon size={18} />
543
+ <Icon size={20} />
557
544
  </span>
558
545
  {/if}
559
546
  {#if !isCollapsed}
560
547
  <span
561
- class="text-sm font-medium leading-snug text-inherit"
548
+ class="text-sm leading-snug text-inherit"
562
549
  >{item.label}</span
563
550
  >
564
551
  {/if}
@@ -585,7 +572,7 @@
585
572
  {#if !(normalizedSearchTerm && searchMode === "input")}
586
573
  <button
587
574
  type="button"
588
- class="focus-ring focus-ring--nav mt-3 inline-flex min-h-10 pointer-coarse:min-h-11 cursor-pointer items-center rounded-control bg-action-primary px-3 py-2 text-sm font-medium text-action-primary outline-none transition-colors hover:bg-action-primary-hover"
575
+ class="focus-ring focus-ring--nav mt-3 inline-flex min-h-9 pointer-coarse:min-h-11 cursor-pointer items-center rounded-control bg-action-primary px-3 py-2 text-sm font-medium text-action-primary outline-none transition-colors hover:bg-action-primary-hover"
589
576
  onclick={handleEmptyStateAction}
590
577
  >
591
578
  {emptyStateActionLabel}
@@ -20,6 +20,7 @@
20
20
  /** @deprecated use `class`. */
21
21
  className?: string;
22
22
  widthClass?: string;
23
+ /** Both values draw the same flat card now (D127); the prop stays so existing usage compiles. */
23
24
  variant?: "plain" | "elevated";
24
25
  ariaLabel?: string;
25
26
  title?: string;
@@ -92,25 +93,20 @@
92
93
  : items,
93
94
  );
94
95
 
95
- const isElevated = $derived(variant === "elevated");
96
-
97
96
  const containerClasses = $derived.by(() => {
98
97
  const resolvedWidthClass = widthClass.trim() || "w-80";
99
- const shell = isElevated
100
- ? "rounded-container border border-border bg-card text-headline shadow-sm ring-1 ring-border"
101
- : "rounded-container border border-border bg-card text-headline shadow-sm";
98
+ const shell = "rounded-container border border-border bg-card text-headline";
102
99
  return cn(`${resolvedWidthClass} shrink-0 p-5 ${shell} ${className}`);
103
100
  });
104
101
 
105
102
  function getItemClasses(itemId: string): string {
106
103
  const isActive = selectedItemId === itemId;
107
104
  const baseClasses =
108
- "focus-ring focus-ring--nav w-full cursor-pointer rounded-control px-3 py-2 text-left transition-colors duration-150";
105
+ "focus-ring focus-ring--nav w-full cursor-pointer min-h-9 pointer-coarse:min-h-11 rounded-control px-3 py-2 text-left transition-colors duration-(--duration-base) ease-standard";
109
106
  if (isActive) {
110
- // The fill, the shadow and the ring are all dropped in forced
111
- // colours; an outline is drawn there, so the selected item keeps
112
- // a shape of its own.
113
- return `${baseClasses} bg-nav-menu-active text-inherit shadow-sm ring-1 ring-border forced-colors:outline-solid forced-colors:outline-2 forced-colors:-outline-offset-2`;
107
+ // The fill is dropped in forced colours; an outline is drawn
108
+ // there, so the selected item keeps a shape of its own.
109
+ return `${baseClasses} bg-nav-menu-active text-nav-menu-item-active forced-colors:outline-solid forced-colors:outline-2 forced-colors:-outline-offset-2`;
114
110
  }
115
111
  return `${baseClasses} text-nav-menu-item hover:bg-nav-menu-hover hover:text-nav-menu-item-hover active:bg-surface-active`;
116
112
  }
@@ -166,7 +162,7 @@
166
162
  bind:value={searchValue}
167
163
  placeholder={searchPlaceholder}
168
164
  aria-label={searchPlaceholder}
169
- class="w-full min-w-0 min-h-10 pointer-coarse:min-h-11 rounded-container border-transparent !bg-transparent py-2 pl-10 text-sm ring-1 ring-input-border hover:!bg-nav-menu-hover focus:!bg-transparent focus-ring focus-ring--nav"
165
+ class="w-full min-w-0 min-h-9 pointer-coarse:min-h-11 rounded-container border-transparent !bg-transparent py-2 pl-10 text-sm ring-1 ring-input-border hover:!bg-nav-menu-hover focus:!bg-transparent focus-ring focus-ring--nav"
170
166
  />
171
167
  </div>
172
168
  {/if}
@@ -190,12 +186,12 @@
190
186
  class="mt-0.5 inline-flex size-5 shrink-0 items-center justify-center text-current"
191
187
  aria-hidden="true"
192
188
  >
193
- <Icon size={18} />
189
+ <Icon size={20} />
194
190
  </span>
195
191
  {/if}
196
192
  <span class="min-w-0">
197
193
  <span
198
- class="block truncate text-sm font-medium text-current"
194
+ class={cn("block truncate text-sm text-current", selectedItemId === item.id ? "font-semibold" : "font-medium")}
199
195
  >
200
196
  {item.label}
201
197
  </span>
@@ -225,7 +221,7 @@
225
221
  {#if emptyStateActionLabel.trim() && onEmptyStateAction}
226
222
  <button
227
223
  type="button"
228
- class="mt-3 inline-flex min-h-10 pointer-coarse:min-h-11 cursor-pointer items-center rounded-control bg-action-primary px-3 py-2 text-sm font-medium text-action-primary transition-colors hover:bg-action-primary-hover focus-ring focus-ring--nav"
224
+ class="mt-3 inline-flex min-h-9 pointer-coarse:min-h-11 cursor-pointer items-center rounded-control bg-action-primary px-3 py-2 text-sm font-medium text-action-primary transition-colors hover:bg-action-primary-hover focus-ring focus-ring--nav"
229
225
  onclick={handleEmptyStateAction}
230
226
  >
231
227
  {emptyStateActionLabel.trim()}
@@ -14,6 +14,7 @@ interface Props {
14
14
  /** @deprecated use `class`. */
15
15
  className?: string;
16
16
  widthClass?: string;
17
+ /** Both values draw the same flat card now (D127); the prop stays so existing usage compiles. */
17
18
  variant?: "plain" | "elevated";
18
19
  ariaLabel?: string;
19
20
  title?: string;
@@ -150,9 +150,8 @@
150
150
 
151
151
  const containerClasses = $derived.by(() => {
152
152
  const widthClass = isCollapsed ? "w-[72px]" : "w-[266px]";
153
- const railSurface = "border-r border-border bg-background text-headline";
154
- const cardSurface =
155
- "border-r border-border bg-background text-headline shadow-sm";
153
+ const railSurface = "border-r border-border-weak bg-surface-chrome text-headline";
154
+ const cardSurface = "border-r border-border-weak bg-surface-chrome text-headline";
156
155
  const surfaceClasses = isCard ? cardSurface : railSurface;
157
156
  const verticalPad = isCard ? "py-4" : "py-5";
158
157
  // `flex` and `max-lg:hidden` must not both apply below `lg`: with