@blotch/dolan 22.2.3 → 22.2.5

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 (227) hide show
  1. package/dist/commands/add.js +9 -1
  2. package/dist/commands/add.js.map +1 -1
  3. package/dist/commands/add.test.js +38 -3
  4. package/dist/commands/add.test.js.map +1 -1
  5. package/dist/commands/doctor.js +258 -21
  6. package/dist/commands/doctor.js.map +1 -1
  7. package/dist/commands/doctor.test.js +425 -11
  8. package/dist/commands/doctor.test.js.map +1 -1
  9. package/dist/commands/init.d.ts +2 -2
  10. package/dist/commands/init.js +38 -18
  11. package/dist/commands/init.js.map +1 -1
  12. package/dist/commands/init.test.js +105 -2
  13. package/dist/commands/init.test.js.map +1 -1
  14. package/dist/commands/theme.js +1 -1
  15. package/dist/commands/theme.js.map +1 -1
  16. package/dist/commands/update.d.ts +7 -0
  17. package/dist/commands/update.js +194 -50
  18. package/dist/commands/update.js.map +1 -1
  19. package/dist/commands/update.test.js +570 -3
  20. package/dist/commands/update.test.js.map +1 -1
  21. package/dist/lockfile.d.ts +17 -0
  22. package/dist/lockfile.js +56 -1
  23. package/dist/lockfile.js.map +1 -1
  24. package/dist/lockfile.test.d.ts +1 -0
  25. package/dist/lockfile.test.js +65 -0
  26. package/dist/lockfile.test.js.map +1 -0
  27. package/dist/registry.d.ts +6 -0
  28. package/dist/registry.js +119 -11
  29. package/dist/registry.js.map +1 -1
  30. package/dist/registry.test.js +61 -6
  31. package/dist/registry.test.js.map +1 -1
  32. package/dist/test-utils.d.ts +27 -0
  33. package/dist/test-utils.js +45 -1
  34. package/dist/test-utils.js.map +1 -1
  35. package/dist/testing/fire-lynx-event.d.ts +14 -0
  36. package/dist/testing/fire-lynx-event.js +19 -0
  37. package/dist/testing/fire-lynx-event.js.map +1 -0
  38. package/dist/testing/fire-lynx-event.test.d.ts +1 -0
  39. package/dist/testing/fire-lynx-event.test.js +44 -0
  40. package/dist/testing/fire-lynx-event.test.js.map +1 -0
  41. package/dist/testing/lynx-main-thread.d.ts +22 -0
  42. package/dist/testing/lynx-main-thread.js +11 -0
  43. package/dist/testing/lynx-main-thread.js.map +1 -0
  44. package/dist/testing/lynx-main-thread.test.d.ts +1 -0
  45. package/dist/testing/lynx-main-thread.test.js +24 -0
  46. package/dist/testing/lynx-main-thread.test.js.map +1 -0
  47. package/dist/testing/set-input-signal.d.ts +5 -4
  48. package/dist/testing/set-input-signal.js +5 -4
  49. package/dist/testing/set-input-signal.js.map +1 -1
  50. package/dist/ui/boolean-inputs.test.ts +76 -0
  51. package/dist/ui/components/accordion/accordion.test.ts +158 -144
  52. package/dist/ui/components/accordion/accordion.ts +19 -14
  53. package/dist/ui/components/action-sheet/action-sheet.test.ts +637 -183
  54. package/dist/ui/components/action-sheet/action-sheet.ts +339 -55
  55. package/dist/ui/components/action-sheet/index.ts +1 -0
  56. package/dist/ui/components/alert/alert.test.ts +67 -19
  57. package/dist/ui/components/alert/alert.ts +14 -16
  58. package/dist/ui/components/alert-dialog/alert-dialog.test.ts +740 -129
  59. package/dist/ui/components/alert-dialog/alert-dialog.ts +672 -58
  60. package/dist/ui/components/alert-dialog/index.ts +5 -0
  61. package/dist/ui/components/area-chart/area-chart.test.ts +81 -1
  62. package/dist/ui/components/area-chart/area-chart.ts +3 -2
  63. package/dist/ui/components/avatar/avatar.test.ts +89 -45
  64. package/dist/ui/components/badge/badge.test.ts +57 -23
  65. package/dist/ui/components/badge/badge.ts +14 -15
  66. package/dist/ui/components/bar-chart/bar-chart.test.ts +82 -1
  67. package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +1314 -273
  68. package/dist/ui/components/bottom-sheet/bottom-sheet.ts +1046 -119
  69. package/dist/ui/components/bottom-sheet/index.ts +7 -1
  70. package/dist/ui/components/button/button.test.ts +646 -2
  71. package/dist/ui/components/button/button.ts +443 -64
  72. package/dist/ui/components/button/index.ts +2 -0
  73. package/dist/ui/components/button-group/button-group.test.ts +528 -2
  74. package/dist/ui/components/button-group/button-group.ts +428 -70
  75. package/dist/ui/components/button-group/index.ts +3 -0
  76. package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +82 -1
  77. package/dist/ui/components/card/card.test.ts +97 -61
  78. package/dist/ui/components/card/card.ts +2 -1
  79. package/dist/ui/components/chart/chart-presets.test.ts +197 -1
  80. package/dist/ui/components/chart/chart-presets.ts +14 -7
  81. package/dist/ui/components/chart/chart-theme.ts +12 -0
  82. package/dist/ui/components/chart/chart.test.ts +1031 -0
  83. package/dist/ui/components/chart/chart.ts +66 -46
  84. package/dist/ui/components/checkbox/checkbox.test.ts +124 -42
  85. package/dist/ui/components/checkbox/checkbox.ts +25 -24
  86. package/dist/ui/components/collapsible/collapsible.test.ts +158 -81
  87. package/dist/ui/components/collapsible/collapsible.ts +21 -15
  88. package/dist/ui/components/context-menu/context-menu.test.ts +224 -0
  89. package/dist/ui/components/context-menu/context-menu.ts +110 -0
  90. package/dist/ui/components/context-menu/index.ts +1 -0
  91. package/dist/ui/components/dialog/dialog.test.ts +210 -152
  92. package/dist/ui/components/dialog/dialog.ts +35 -29
  93. package/dist/ui/components/dock/dock-scroll.test.ts +130 -0
  94. package/dist/ui/components/dock/dock-scroll.ts +142 -0
  95. package/dist/ui/components/dock/dock-search.test.ts +594 -0
  96. package/dist/ui/components/dock/dock.test.ts +752 -0
  97. package/dist/ui/components/dock/dock.ts +1438 -0
  98. package/dist/ui/components/dock/index.ts +2 -0
  99. package/dist/ui/components/dynamic-color/dynamic-color.test.ts +144 -0
  100. package/dist/ui/components/dynamic-color/dynamic-color.ts +146 -0
  101. package/dist/ui/components/dynamic-color/index.ts +5 -0
  102. package/dist/ui/components/empty-state/empty-state.test.ts +28 -39
  103. package/dist/ui/components/glass/concentric-radius.test.ts +13 -0
  104. package/dist/ui/components/glass/concentric-radius.ts +17 -0
  105. package/dist/ui/components/glass/glass-design.test.ts +78 -0
  106. package/dist/ui/components/glass/glass-design.ts +119 -0
  107. package/dist/ui/components/glass/glass.test.ts +376 -0
  108. package/dist/ui/components/glass/glass.ts +430 -0
  109. package/dist/ui/components/glass/index.ts +19 -0
  110. package/dist/ui/components/icon/icon-theme.test.ts +64 -0
  111. package/dist/ui/components/icon/icon-theme.ts +70 -16
  112. package/dist/ui/components/icon/icon.test.ts +52 -41
  113. package/dist/ui/components/input/input.test.ts +178 -101
  114. package/dist/ui/components/input/input.ts +17 -13
  115. package/dist/ui/components/label/label.test.ts +11 -24
  116. package/dist/ui/components/label/label.ts +8 -2
  117. package/dist/ui/components/line-chart/line-chart.test.ts +76 -1
  118. package/dist/ui/components/line-chart/line-chart.ts +3 -2
  119. package/dist/ui/components/list/list.test.ts +80 -49
  120. package/dist/ui/components/list/list.ts +5 -4
  121. package/dist/ui/components/menu/index.ts +25 -0
  122. package/dist/ui/components/menu/menu-placement.test.ts +153 -0
  123. package/dist/ui/components/menu/menu-placement.ts +159 -0
  124. package/dist/ui/components/menu/menu-popup.test.ts +636 -0
  125. package/dist/ui/components/menu/menu-popup.ts +565 -0
  126. package/dist/ui/components/menu/menu.test.ts +523 -0
  127. package/dist/ui/components/menu/menu.ts +455 -0
  128. package/dist/ui/components/nav-drawer/nav-drawer.test.ts +425 -260
  129. package/dist/ui/components/nav-drawer/nav-drawer.ts +75 -58
  130. package/dist/ui/components/navigation-bar/index.ts +1 -0
  131. package/dist/ui/components/navigation-bar/navigation-bar.test.ts +425 -0
  132. package/dist/ui/components/navigation-bar/navigation-bar.ts +411 -0
  133. package/dist/ui/components/pagination/pagination.test.ts +136 -86
  134. package/dist/ui/components/popover/index.ts +9 -0
  135. package/dist/ui/components/popover/popover-panel.test.ts +735 -0
  136. package/dist/ui/components/popover/popover-panel.ts +603 -0
  137. package/dist/ui/components/popover/popover.test.ts +286 -0
  138. package/dist/ui/components/popover/popover.ts +207 -0
  139. package/dist/ui/components/progress/progress.test.ts +102 -46
  140. package/dist/ui/components/progress/progress.ts +54 -55
  141. package/dist/ui/components/radio-group/radio-group.test.ts +128 -83
  142. package/dist/ui/components/radio-group/radio-group.ts +26 -25
  143. package/dist/ui/components/scatter-chart/scatter-chart.test.ts +74 -1
  144. package/dist/ui/components/scroll-area/scroll-area.test.ts +72 -36
  145. package/dist/ui/components/scroll-area/scroll-area.ts +4 -3
  146. package/dist/ui/components/search-field/index.ts +15 -0
  147. package/dist/ui/components/search-field/keyboard.test.ts +147 -0
  148. package/dist/ui/components/search-field/keyboard.ts +123 -0
  149. package/dist/ui/components/search-field/search-field.test.ts +549 -0
  150. package/dist/ui/components/search-field/search-field.ts +628 -0
  151. package/dist/ui/components/segmented-control/index.ts +8 -0
  152. package/dist/ui/components/segmented-control/segmented-control.test.ts +861 -0
  153. package/dist/ui/components/segmented-control/segmented-control.ts +884 -0
  154. package/dist/ui/components/select/select.test.ts +138 -78
  155. package/dist/ui/components/select/select.ts +19 -12
  156. package/dist/ui/components/separator/separator.test.ts +18 -17
  157. package/dist/ui/components/sheet/sheet.test.ts +151 -11
  158. package/dist/ui/components/sheet/sheet.ts +68 -5
  159. package/dist/ui/components/skeleton/skeleton.test.ts +24 -7
  160. package/dist/ui/components/skeleton/skeleton.ts +10 -16
  161. package/dist/ui/components/slider/index.ts +1 -0
  162. package/dist/ui/components/slider/slider.test.ts +695 -0
  163. package/dist/ui/components/slider/slider.ts +901 -0
  164. package/dist/ui/components/spinner/spinner.test.ts +64 -46
  165. package/dist/ui/components/spinner/spinner.ts +16 -19
  166. package/dist/ui/components/switch/index.ts +1 -1
  167. package/dist/ui/components/switch/switch.test.ts +534 -48
  168. package/dist/ui/components/switch/switch.ts +613 -48
  169. package/dist/ui/components/tab-bar/index.ts +1 -0
  170. package/dist/ui/components/tab-bar/tab-bar.test.ts +280 -0
  171. package/dist/ui/components/tab-bar/tab-bar.ts +230 -0
  172. package/dist/ui/components/tabs/index.ts +7 -1
  173. package/dist/ui/components/tabs/tabs.test.ts +484 -150
  174. package/dist/ui/components/tabs/tabs.ts +344 -149
  175. package/dist/ui/components/textarea/textarea.test.ts +206 -100
  176. package/dist/ui/components/textarea/textarea.ts +16 -13
  177. package/dist/ui/components/toast/toast.test.ts +434 -350
  178. package/dist/ui/components/toast/toast.ts +42 -51
  179. package/dist/ui/components/toggle/index.ts +1 -0
  180. package/dist/ui/components/toggle/toggle.test.ts +559 -146
  181. package/dist/ui/components/toggle/toggle.ts +241 -26
  182. package/dist/ui/components/toolbar/bar-scroll.test.ts +310 -0
  183. package/dist/ui/components/toolbar/bar-scroll.ts +372 -0
  184. package/dist/ui/components/toolbar/index.ts +12 -0
  185. package/dist/ui/components/toolbar/toolbar.test.ts +438 -0
  186. package/dist/ui/components/toolbar/toolbar.ts +535 -0
  187. package/dist/ui/theme/chart-colors.test.ts +8 -3
  188. package/dist/ui/theme/dark.css +38 -2
  189. package/dist/ui/theme/default.css +55 -7
  190. package/dist/ui/theme/icon-colors.test.ts +8 -3
  191. package/dist/ui/theme/rgb-channels.test.ts +84 -0
  192. package/dist/ui/theme/tailwind-build.test.ts +114 -0
  193. package/dist/ui/theme/tailwind-css.test.ts +83 -0
  194. package/dist/ui/theme/tailwind-plugin.test.ts +42 -2
  195. package/dist/ui/theme/tailwind-plugin.ts +93 -29
  196. package/dist/ui/theme/tailwind.css +113 -0
  197. package/dist/ui/utils/animate.test.ts +13 -1
  198. package/dist/utils/analyze.d.ts +19 -0
  199. package/dist/utils/analyze.js +16 -0
  200. package/dist/utils/analyze.js.map +1 -1
  201. package/dist/utils/analyze.test.js +14 -1
  202. package/dist/utils/analyze.test.js.map +1 -1
  203. package/dist/utils/installed.d.ts +25 -1
  204. package/dist/utils/installed.js +33 -0
  205. package/dist/utils/installed.js.map +1 -1
  206. package/dist/utils/installed.test.js +55 -1
  207. package/dist/utils/installed.test.js.map +1 -1
  208. package/dist/utils/resolve-paths.d.ts +4 -4
  209. package/dist/utils/resolve-paths.js +5 -7
  210. package/dist/utils/resolve-paths.js.map +1 -1
  211. package/dist/utils/resolve-paths.test.js +9 -5
  212. package/dist/utils/resolve-paths.test.js.map +1 -1
  213. package/dist/utils/tailwind.d.ts +45 -0
  214. package/dist/utils/tailwind.js +90 -0
  215. package/dist/utils/tailwind.js.map +1 -0
  216. package/dist/utils/tailwind.test.d.ts +1 -0
  217. package/dist/utils/tailwind.test.js +134 -0
  218. package/dist/utils/tailwind.test.js.map +1 -0
  219. package/package.json +9 -1
  220. package/dist/ui/components/area-chart/area-chart-render.test.ts +0 -63
  221. package/dist/ui/components/bar-chart/bar-chart-render.test.ts +0 -58
  222. package/dist/ui/components/button/button-render.test.ts +0 -132
  223. package/dist/ui/components/button-group/button-group-render.test.ts +0 -146
  224. package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +0 -59
  225. package/dist/ui/components/chart/chart-render.test.ts +0 -730
  226. package/dist/ui/components/line-chart/line-chart-render.test.ts +0 -83
  227. package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +0 -64
@@ -0,0 +1,411 @@
1
+ import { Location, LocationStrategy } from '@angular/common';
2
+ import {
3
+ Component,
4
+ DestroyRef,
5
+ Injector,
6
+ ViewEncapsulation,
7
+ booleanAttribute,
8
+ computed,
9
+ inject,
10
+ input,
11
+ signal,
12
+ } from '@angular/core';
13
+ import { ROUTES, Router } from '@angular/router';
14
+ import {
15
+ LYNX_ELEMENTS,
16
+ LynxAccessibilitySettings,
17
+ LynxLocationStrategy,
18
+ } from '@blotch/angular-lynx';
19
+
20
+ import { DURATION, EASING } from '../../utils/animate';
21
+ import { cn } from '../../utils/cn';
22
+ import { UiGlass } from '../glass/glass';
23
+ import { type GlassDesign, injectGlassDesign } from '../glass/glass-design';
24
+ import { edgeFadeOverlay, platformOf, UiBarState } from '../toolbar/bar-scroll';
25
+ import { UiToolbarItem } from '../toolbar/toolbar';
26
+
27
+ /**
28
+ * The bar's height below the status bar, in px: 44pt on iOS, with and without
29
+ * Liquid Glass, and Material 3's 64dp small and center-aligned top app bars.
30
+ */
31
+ const BAR_HEIGHT: Readonly<Record<GlassDesign, number>> = {
32
+ glass: 44,
33
+ frosted: 44,
34
+ material: 64,
35
+ };
36
+
37
+ /**
38
+ * The row's padding at each end, in px: iOS 26's bars float their controls
39
+ * 16pt in, the bars before it 8pt, and Material's top app bar has its icon
40
+ * buttons' 4dp in each end's own padding.
41
+ */
42
+ const ROW_PADDING: Readonly<Record<GlassDesign, number>> = {
43
+ glass: 16,
44
+ frosted: 8,
45
+ material: 0,
46
+ };
47
+
48
+ /** The space between a centered title and the controls beside it, in px. */
49
+ const TITLE_GAP = 8;
50
+
51
+ /** What Lynx's `layoutchange` event carries, as far as the bar reads it. */
52
+ type LayoutChangeEvent = Event & { detail: { width: number } };
53
+
54
+ /**
55
+ * How the inline title comes in as the large title scrolls under the bar,
56
+ * and how Material's bar changes color as content scrolls under it.
57
+ */
58
+ const TITLE_TRANSITION = `opacity ${DURATION.fast}ms ${EASING.standard}`;
59
+ const CONTAINER_TRANSITION = `background-color ${DURATION.normal}ms ${EASING.standard}`;
60
+
61
+ /**
62
+ * The router's history, when the app set the router up with `provideRouter()`
63
+ * from `@blotch/angular-lynx`, whose in-memory history says whether there's
64
+ * a screen to go back to. Without the router there's nothing to go back
65
+ * through, and its LocationStrategy is never created: on Lynx the browser's
66
+ * one it would fall back to can't be.
67
+ */
68
+ const historyOf = (injector: Injector): LynxLocationStrategy | null => {
69
+ if (!injector.get(ROUTES, null)) return null;
70
+ const strategy = injector.get(LocationStrategy);
71
+ return strategy instanceof LynxLocationStrategy ? strategy : null;
72
+ };
73
+
74
+ /**
75
+ * The bar at the top of a screen: its title, a Back button while there's a
76
+ * screen to go back to, and the screen's actions, floating over the content,
77
+ * which scrolls beneath it.
78
+ *
79
+ * ```html
80
+ * <ui-navigation-bar title="Notes" largeTitle>
81
+ * <ui-toolbar-item leading label="Edit" (pressed)="edit()" />
82
+ * <ui-toolbar-group>
83
+ * <ui-toolbar-item icon="search" label="Search" (pressed)="search()" />
84
+ * <ui-toolbar-item icon="plus" label="New note" (pressed)="add()" />
85
+ * </ui-toolbar-group>
86
+ * </ui-navigation-bar>
87
+ * <scroll-view ui-bar-scroll scroll-orientation="vertical" class="h-full">
88
+ * …
89
+ * </scroll-view>
90
+ * ```
91
+ *
92
+ * Items and groups go at the trailing end, or after the Back button with the
93
+ * `leading` attribute. Put `ui-bar-scroll` on the screen's scroll view: its
94
+ * content then starts below the bar and the status bar, the large title
95
+ * scrolls with it, and the bar follows it.
96
+ *
97
+ * Its form follows the platform (see GlassDesign):
98
+ *
99
+ * - `'glass'` (iOS 26 and later, the web, Android with the glass opt-in): no
100
+ * bar background. The items float in glass, grouped items share one
101
+ * capsule, the Back button is a glass circle with a chevron, and the title
102
+ * is plain text. Content fades out as it scrolls beneath them, iOS's
103
+ * scroll edge effect.
104
+ * - `'frosted'` (iOS 15 to 25): the navigation bar of that time, clear while
105
+ * the content is at its top and on the bars' chrome material with a
106
+ * hairline once content scrolls under it. Items and the Back button, a chevron with the
107
+ * previous screen's title, are in the tint color.
108
+ * - `'material'` (Android): Material 3's small top app bar, or center-aligned
109
+ * with `titleCentered`, whose container turns from the surface color to the
110
+ * surface container's as content scrolls under it. The Back button is its
111
+ * navigation icon, an arrow.
112
+ *
113
+ * With `largeTitle`, the title starts large at the top of the content, and
114
+ * the bar shows it once it has scrolled under: iOS's large title, and
115
+ * Material 3 Expressive's medium flexible app bar.
116
+ */
117
+ @Component({
118
+ selector: 'ui-navigation-bar',
119
+ imports: [LYNX_ELEMENTS, UiGlass, UiToolbarItem],
120
+ encapsulation: ViewEncapsulation.None,
121
+ host: {
122
+ '[class]': 'hostClass()',
123
+ '[style.transition]': 'containerTransition()',
124
+ '[style.background-image]': 'edgeFade()',
125
+ },
126
+ // The frosted material, the bars' chrome, is added and removed rather
127
+ // than faded: a blur view under a fading opacity draws wrongly on iOS.
128
+ template: `
129
+ @if (frostedBackground()) {
130
+ <blur-view
131
+ ui-glass
132
+ shape="rounded"
133
+ [radius]="0"
134
+ material="chrome"
135
+ class="absolute bottom-0 left-0 right-0 top-0"
136
+ ></blur-view>
137
+ <view class="h-[0.5px] bg-border absolute bottom-0 left-0 right-0"></view>
138
+ }
139
+ <view
140
+ [class]="rowClass()"
141
+ [style]="rowStyle()"
142
+ (bindlayoutchange)="measureRow($event)"
143
+ >
144
+ @if (!titleAtStart()) {
145
+ <!-- An unconstrained, invisible copy of the title, which measures
146
+ how wide it would be on one line. -->
147
+ <view
148
+ class="flex-row opacity-0 flex absolute left-0 top-0"
149
+ accessibility-elements-hidden="true"
150
+ >
151
+ <text
152
+ [class]="titleClass()"
153
+ (bindlayoutchange)="measureTitle($event)"
154
+ >{{ title() }}</text
155
+ >
156
+ </view>
157
+ }
158
+ <view
159
+ [class]="leadingClass()"
160
+ (bindlayoutchange)="measureLeading($event)"
161
+ >
162
+ @if (backShown()) {
163
+ <ui-toolbar-item back [label]="backLabel()" (pressed)="goBack()" />
164
+ }
165
+ <ng-content select="[leading]" />
166
+ </view>
167
+ <view
168
+ [class]="titleBoxClass()"
169
+ [style]="titleBoxStyle()"
170
+ [attr.accessibility-elements-hidden]="titleShown() ? null : true"
171
+ >
172
+ <text
173
+ [text-maxline]="1"
174
+ accessibility-traits="header"
175
+ [class]="titleClass()"
176
+ >{{ title() }}</text
177
+ >
178
+ </view>
179
+ <view
180
+ [class]="trailingClass()"
181
+ (bindlayoutchange)="measureTrailing($event)"
182
+ >
183
+ <ng-content />
184
+ </view>
185
+ </view>
186
+ `,
187
+ })
188
+ export class UiNavigationBar {
189
+ /** The screen's title: a word or a short phrase. */
190
+ readonly title = input('');
191
+ /**
192
+ * The title starts large at the top of the content, and moves into the bar
193
+ * as it scrolls under it. Needs `ui-bar-scroll` on the screen's scroll
194
+ * view, which draws it.
195
+ */
196
+ readonly largeTitle = input(false, { transform: booleanAttribute });
197
+ /**
198
+ * Material's center-aligned top app bar, rather than the small one with
199
+ * the title at the start. iOS always centers the title in the bar.
200
+ */
201
+ readonly titleCentered = input(false, { transform: booleanAttribute });
202
+ /**
203
+ * The previous screen's title, which the Back button shows beside its
204
+ * chevron before iOS 26, and which screen readers say for it everywhere.
205
+ */
206
+ readonly backLabel = input('Back');
207
+ /**
208
+ * Where Back goes when there's no screen to go back to, such as a detail
209
+ * screen the app opened on: a URL for `Router.navigateByUrl()`, usually
210
+ * the screen above it. It replaces the current screen in the history.
211
+ * Without it, the Back button shows only while there's history.
212
+ */
213
+ readonly backRoute = input<string | undefined>(undefined);
214
+ /**
215
+ * Hides the Back button, for a screen that's a starting point though
216
+ * there's history behind it, such as a tab's first screen.
217
+ */
218
+ readonly backButtonHidden = input(false, { transform: booleanAttribute });
219
+ readonly userClass = input<string>('', { alias: 'class' });
220
+
221
+ readonly #state = inject(UiBarState);
222
+ readonly #settings = inject(LynxAccessibilitySettings);
223
+ readonly #injector = inject(Injector);
224
+ readonly #history = historyOf(this.#injector);
225
+ readonly #platform = platformOf(this.#injector);
226
+
227
+ /** The design on this platform (see GlassDesign), which the items follow. */
228
+ readonly design = injectGlassDesign();
229
+
230
+ readonly #material = computed(() => this.design() === 'material');
231
+ /** No fading or color changing under Reduce Motion. */
232
+ readonly #animate = computed(() => !this.#settings.reduceMotion());
233
+
234
+ /** The widths of the bar, its two ends and its title, from Lynx's layout. */
235
+ protected readonly rowWidth = signal(0);
236
+ protected readonly leadingWidth = signal(0);
237
+ protected readonly trailingWidth = signal(0);
238
+ protected readonly titleWidth = signal(0);
239
+
240
+ /**
241
+ * Whether there's a screen to go back to in the router's history: there
242
+ * is after a navigation, and isn't on the first screen.
243
+ */
244
+ readonly canGoBack = computed(() => this.#history?.canGoBack() ?? false);
245
+
246
+ protected readonly backShown = computed(
247
+ () => !this.backButtonHidden() && (this.canGoBack() || !!this.backRoute()),
248
+ );
249
+
250
+ // What the scroll view under the bar makes room for: the bar below the
251
+ // status bar, and the large title at the top of its content.
252
+ protected readonly attachment = this.#state.attach(
253
+ this.#state.topBar,
254
+ {
255
+ height: computed(() => BAR_HEIGHT[this.design()]),
256
+ largeTitle: computed(() => (this.largeTitle() ? this.title() : null)),
257
+ },
258
+ inject(DestroyRef),
259
+ );
260
+
261
+ /**
262
+ * The title shows in the bar unless the large title is showing at the top
263
+ * of the content: before it has scrolled under the bar. Without a scroll
264
+ * view there's no large title, so the bar shows the title.
265
+ */
266
+ protected readonly titleShown = computed(
267
+ () =>
268
+ !this.largeTitle() ||
269
+ !this.#state.content() ||
270
+ this.#state.largeTitleUnder(),
271
+ );
272
+
273
+ /**
274
+ * On Android, glass bars fade the content beneath them themselves, since
275
+ * its scroll view takes no mask (see edgeFadeOverlay).
276
+ */
277
+ protected readonly edgeFade = computed(() =>
278
+ this.design() === 'glass' && this.#platform === 'Android'
279
+ ? edgeFadeOverlay('bottom')
280
+ : null,
281
+ );
282
+
283
+ protected readonly frostedBackground = computed(
284
+ () => this.design() === 'frosted' && this.#state.scrolledUnderTop(),
285
+ );
286
+
287
+ /** Material's small top app bar puts the title at the start. */
288
+ protected readonly titleAtStart = computed(
289
+ () => this.#material() && !this.titleCentered(),
290
+ );
291
+
292
+ protected readonly hostClass = computed(() =>
293
+ cn(
294
+ 'flex-col flex absolute left-0 right-0 top-0 pt-safe',
295
+ this.#material() &&
296
+ (this.#state.scrolledUnderTop() ? 'bg-secondary' : 'bg-background'),
297
+ this.userClass(),
298
+ ),
299
+ );
300
+
301
+ protected readonly containerTransition = computed(() =>
302
+ this.#material() && this.#animate() ? CONTAINER_TRANSITION : null,
303
+ );
304
+
305
+ protected readonly rowClass = computed(() =>
306
+ cn(
307
+ 'flex-row items-center flex relative justify-between',
308
+ this.#material() ? 'h-[64px]' : 'h-[44px]',
309
+ ),
310
+ );
311
+
312
+ protected readonly rowStyle = computed(() => {
313
+ const padding = `${ROW_PADDING[this.design()]}px`;
314
+ return { 'padding-left': padding, 'padding-right': padding };
315
+ });
316
+
317
+ /**
318
+ * The ends keep their controls' width. Material's small bar puts its
319
+ * navigation icon 4dp in, and the title 16dp in without one (`min-w`), or
320
+ * right after it: Compose's TopAppBar layout.
321
+ */
322
+ protected readonly leadingClass = computed(() =>
323
+ cn(
324
+ 'shrink-0 flex-row items-center flex',
325
+ this.#material() ? 'pl-[4px]' : 'gap-[8px]',
326
+ this.titleAtStart() && 'min-w-[12px]',
327
+ ),
328
+ );
329
+
330
+ protected readonly trailingClass = computed(() =>
331
+ cn(
332
+ 'shrink-0 flex-row items-center flex',
333
+ this.#material() ? 'pr-[4px]' : 'gap-[8px]',
334
+ ),
335
+ );
336
+
337
+ /**
338
+ * A centered title is centered on the screen, clear of the wider end on
339
+ * both sides, when it fits there; otherwise it's centered in the room
340
+ * between the ends, and shortens with an ellipsis when even that's too
341
+ * little: UIKit's inline title. At the start, it takes the room between
342
+ * the ends.
343
+ */
344
+ protected readonly titleBoxClass = computed(() =>
345
+ this.titleAtStart()
346
+ ? 'flex-1 flex-row items-center flex px-[4px]'
347
+ : 'flex-row items-center flex absolute bottom-0 top-0 justify-center',
348
+ );
349
+
350
+ protected readonly titleBoxStyle = computed(() => {
351
+ const style: Record<string, string> = {
352
+ opacity: this.titleShown() ? '1' : '0',
353
+ transition: this.#animate() ? TITLE_TRANSITION : 'none',
354
+ };
355
+ if (!this.titleAtStart()) {
356
+ const padding = ROW_PADDING[this.design()];
357
+ const leading = padding + this.leadingWidth() + TITLE_GAP;
358
+ const trailing = padding + this.trailingWidth() + TITLE_GAP;
359
+ const centered = Math.max(leading, trailing);
360
+ const fits = this.titleWidth() <= this.rowWidth() - 2 * centered;
361
+ style['left'] = `${fits ? centered : leading}px`;
362
+ style['right'] = `${fits ? centered : trailing}px`;
363
+ }
364
+ return style;
365
+ });
366
+
367
+ protected readonly titleClass = computed(() =>
368
+ cn(
369
+ this.#material()
370
+ ? 'text-[22px] leading-7'
371
+ : 'text-[17px] font-semibold leading-[22px]',
372
+ !this.titleAtStart() && 'text-center',
373
+ 'text-foreground',
374
+ ),
375
+ );
376
+
377
+ protected measureRow(event: Event): void {
378
+ this.rowWidth.set((event as LayoutChangeEvent).detail.width);
379
+ }
380
+
381
+ protected measureTitle(event: Event): void {
382
+ this.titleWidth.set((event as LayoutChangeEvent).detail.width);
383
+ }
384
+
385
+ protected measureLeading(event: Event): void {
386
+ this.leadingWidth.set((event as LayoutChangeEvent).detail.width);
387
+ }
388
+
389
+ protected measureTrailing(event: Event): void {
390
+ this.trailingWidth.set((event as LayoutChangeEvent).detail.width);
391
+ }
392
+
393
+ /**
394
+ * Goes back, as the Back button does: through the router's history when
395
+ * there's a screen to go back to, which runs the screen's guards as the
396
+ * browser's back button does, or else to `backRoute`, in place of this
397
+ * screen. With neither, it does nothing.
398
+ */
399
+ goBack(): void {
400
+ if (this.canGoBack()) {
401
+ this.#injector.get(Location).back();
402
+ return;
403
+ }
404
+ const route = this.backRoute();
405
+ if (route) {
406
+ void this.#injector
407
+ .get(Router)
408
+ .navigateByUrl(route, { replaceUrl: true });
409
+ }
410
+ }
411
+ }