@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,535 @@
1
+ import { NgTemplateOutlet } from '@angular/common';
2
+ import {
3
+ Component,
4
+ DestroyRef,
5
+ ElementRef,
6
+ Injector,
7
+ ViewEncapsulation,
8
+ booleanAttribute,
9
+ computed,
10
+ effect,
11
+ inject,
12
+ input,
13
+ output,
14
+ signal,
15
+ } from '@angular/core';
16
+ import {
17
+ type BaseLynxElement,
18
+ LYNX_ELEMENTS,
19
+ LynxAccessibilitySettings,
20
+ } from '@blotch/angular-lynx';
21
+
22
+ import { cn } from '../../utils/cn';
23
+ import { UiGlass } from '../glass/glass';
24
+ import {
25
+ type GlassDesign,
26
+ injectGlassDesign,
27
+ injectSystemDrawsGlass,
28
+ } from '../glass/glass-design';
29
+ import { injectIconColor } from '../icon/icon-theme';
30
+ import { type IconName, ICONS } from '../icon/icons';
31
+ import { edgeFadeOverlay, platformOf, UiBarState } from './bar-scroll';
32
+
33
+ /**
34
+ * The toolbar's height above the home indicator, in px, in each design:
35
+ * iOS 26's floating 44pt glass with 8pt under it, the 44pt toolbar of iOS 15
36
+ * to 25, and Material 3 Expressive's 64dp docked toolbar.
37
+ */
38
+ const TOOLBAR_HEIGHT: Readonly<Record<GlassDesign, number>> = {
39
+ glass: 52,
40
+ frosted: 44,
41
+ material: 64,
42
+ };
43
+
44
+ /**
45
+ * An icon's markup, drawn in `color`.
46
+ */
47
+ const iconSvg = (icon: IconName, color: string): string =>
48
+ // split/join, not replaceAll(), which PrimJS's ES2019 runtime lacks.
49
+ ICONS[icon].split('currentColor').join(color);
50
+
51
+ /**
52
+ * Items that belong together, sharing one background: on iOS 26 and later,
53
+ * one Liquid Glass capsule with the items in it, as iOS groups a toolbar's
54
+ * related items. Put it in a `<ui-navigation-bar>` or a `<ui-toolbar>`:
55
+ *
56
+ * ```html
57
+ * <ui-toolbar-group>
58
+ * <ui-toolbar-item icon="share-2" label="Share" (pressed)="share()" />
59
+ * <ui-toolbar-item icon="heart" label="Favorite" (pressed)="favorite()" />
60
+ * </ui-toolbar-group>
61
+ * ```
62
+ *
63
+ * The group is one glass shape with plain items in it, rather than a shape
64
+ * per item merged in a glass group: merged circles join with pinched waists,
65
+ * where iOS's grouped items sit on a straight-edged capsule, and glass isn't
66
+ * layered on glass. Before iOS 26 the items are plain bar buttons in the
67
+ * tint color, and on Android Material's icon buttons, so the group only
68
+ * keeps them together.
69
+ *
70
+ * Keep items with a label apart from items with an icon, in groups of their
71
+ * own: side by side, they read as one button.
72
+ */
73
+ @Component({
74
+ selector: 'ui-toolbar-group',
75
+ imports: [LYNX_ELEMENTS, NgTemplateOutlet, UiGlass],
76
+ encapsulation: ViewEncapsulation.None,
77
+ host: { '[class]': 'hostClass()' },
78
+ template: `
79
+ @if (design() === 'glass') {
80
+ <blur-view
81
+ ui-glass
82
+ class="h-[44px] flex-row items-center flex"
83
+ [interactive]="interactive()"
84
+ >
85
+ <ng-container [ngTemplateOutlet]="items" />
86
+ </blur-view>
87
+ } @else {
88
+ <ng-container [ngTemplateOutlet]="items" />
89
+ }
90
+
91
+ <!-- One copy of the items, for whichever form the group takes: Angular
92
+ projects content once. -->
93
+ <ng-template #items><ng-content /></ng-template>
94
+ `,
95
+ })
96
+ export class UiToolbarGroup {
97
+ readonly userClass = input<string>('', { alias: 'class' });
98
+
99
+ readonly #settings = inject(LynxAccessibilitySettings);
100
+ readonly #systemDrawsGlass = injectSystemDrawsGlass();
101
+
102
+ /** The design on this platform (see GlassDesign), which the items follow. */
103
+ readonly design = injectGlassDesign();
104
+
105
+ /**
106
+ * The glass swells under a finger, as a group of bar items does on iOS 26.
107
+ * Where the runtime draws it (the web preview), that's motion Reduce Motion
108
+ * asks to leave out; iOS tones its own glass down under the setting.
109
+ */
110
+ protected readonly interactive = computed(
111
+ () => this.#systemDrawsGlass || !this.#settings.reduceMotion(),
112
+ );
113
+
114
+ protected readonly hostClass = computed(() =>
115
+ // It keeps its items' width: a bar shortens its title, not its controls.
116
+ cn('shrink-0 flex-row items-center flex', this.userClass()),
117
+ );
118
+ }
119
+
120
+ /**
121
+ * A button in a navigation bar or a toolbar: an icon, or a short label for an
122
+ * action no symbol shows well, such as Edit.
123
+ *
124
+ * ```html
125
+ * <ui-toolbar-item icon="plus" label="New note" (pressed)="add()" />
126
+ * <ui-toolbar-item label="Done" prominent (pressed)="save()" />
127
+ * ```
128
+ *
129
+ * In a `<ui-toolbar-group>` it's a plain item on the group's glass. On its
130
+ * own it draws its own: a glass circle for an icon and a capsule for a label.
131
+ *
132
+ * - `prominent` is the screen's key action, such as Done: tinted glass on iOS
133
+ * 26 and later, a bold label before, and a filled button on Android. Give
134
+ * it its own place, at the trailing end, rather than a group.
135
+ * - `back` draws the platform's Back button: a chevron, and before iOS 26
136
+ * the `label` beside it, the previous screen's title. The navigation bar
137
+ * shows one by itself (see UiNavigationBar); use this for your own.
138
+ *
139
+ * The `label` is also what a screen reader says, so an icon-only item needs
140
+ * one too.
141
+ */
142
+ @Component({
143
+ selector: 'ui-toolbar-item',
144
+ imports: [LYNX_ELEMENTS, NgTemplateOutlet, UiGlass],
145
+ encapsulation: ViewEncapsulation.None,
146
+ host: {
147
+ class: 'flex-row items-center flex shrink-0',
148
+ // The host is the button: one accessibility element named by its label,
149
+ // and the tap target, whose touches start on the glass inside it. The
150
+ // label is an attribute binding: a property binding on a component's
151
+ // host is checked against the DOM schema, which has no
152
+ // `accessibility-label`.
153
+ 'accessibility-element': 'true',
154
+ 'accessibility-traits': 'button',
155
+ '[attr.accessibility-label]': 'label()',
156
+ // Press feedback binds touch and mouse events: native fires only touch,
157
+ // and the web preview, in a browser, only mouse. A canceled touch (a
158
+ // scroll taking over) and a cursor leaving release too.
159
+ '(bindtouchstart)': 'press(true)',
160
+ '(bindmousedown)': 'press(true)',
161
+ '(bindtouchend)': 'press(false)',
162
+ '(bindtouchcancel)': 'press(false)',
163
+ '(bindmouseup)': 'press(false)',
164
+ '(bindmouseleave)': 'press(false)',
165
+ '(bindtap)': 'tap()',
166
+ },
167
+ // On its own on glass, the item is a glass shape, with its content inside
168
+ // the glass, where iOS's interactive glass reacts to touches on it. The
169
+ // content is what dims when disabled: glass under a faded parent turns
170
+ // into a washed-out patch on iOS.
171
+ template: `
172
+ @if (ownGlass()) {
173
+ <blur-view
174
+ ui-glass
175
+ [shape]="iconOnly() ? 'circle' : 'capsule'"
176
+ [tint]="prominent() ? 'var(--glass-tint)' : undefined"
177
+ [interactive]="interactive()"
178
+ class="h-[44px] flex-row items-center flex"
179
+ >
180
+ <view [class]="buttonClass()">
181
+ <ng-container [ngTemplateOutlet]="content" />
182
+ </view>
183
+ </blur-view>
184
+ } @else {
185
+ <view [class]="buttonClass()">
186
+ <ng-container [ngTemplateOutlet]="content" />
187
+ </view>
188
+ }
189
+
190
+ <!-- One copy of the content, for whichever form the item takes. -->
191
+ <ng-template #content>
192
+ @if (highlightClass(); as highlight) {
193
+ <view [class]="highlight"></view>
194
+ }
195
+ @if (iconMarkup(); as markup) {
196
+ <svg [attr.content]="markup" [style]="iconStyle()" />
197
+ }
198
+ @if (labelShown()) {
199
+ <text [class]="labelClass()">{{ label() }}</text>
200
+ }
201
+ </ng-template>
202
+ `,
203
+ })
204
+ export class UiToolbarItem {
205
+ /** What the item does, shown without an icon and read by screen readers. */
206
+ readonly label = input.required<string>();
207
+ /** The [Icon](/dolan/icon) the item shows instead of its label. */
208
+ readonly icon = input<IconName | undefined>(undefined);
209
+ /** The screen's key action, such as Done: tinted, on its own. */
210
+ readonly prominent = input(false, { transform: booleanAttribute });
211
+ /** The platform's Back button, with `label` as the previous screen's title. */
212
+ readonly back = input(false, { transform: booleanAttribute });
213
+ readonly disabled = input(false, { transform: booleanAttribute });
214
+
215
+ /** Emits on a tap, unless the item is disabled. */
216
+ readonly pressed = output<void>();
217
+
218
+ readonly #group = inject(UiToolbarGroup, { optional: true });
219
+ readonly #settings = inject(LynxAccessibilitySettings);
220
+ readonly #systemDrawsGlass = injectSystemDrawsGlass();
221
+ readonly #platform = platformOf(inject(Injector));
222
+ readonly #resolveColor = injectIconColor();
223
+
224
+ readonly #host = inject<ElementRef<BaseLynxElement>>(ElementRef);
225
+
226
+ /** The design on this platform (see GlassDesign). */
227
+ readonly design = injectGlassDesign();
228
+
229
+ /** A finger (or the mouse) is down on the item. */
230
+ protected readonly pressing = signal(false);
231
+
232
+ readonly #glass = computed(() => this.design() === 'glass');
233
+ readonly #material = computed(() => this.design() === 'material');
234
+
235
+ /** On glass, an item on its own is a glass shape of its own. */
236
+ protected readonly ownGlass = computed(() => this.#glass() && !this.#group);
237
+
238
+ /**
239
+ * The icon the item shows: a Back button's chevron (Material's arrow), or
240
+ * the item's own.
241
+ */
242
+ readonly #icon = computed<IconName | undefined>(() => {
243
+ if (!this.back()) return this.icon();
244
+ return this.#material() ? 'arrow-left' : 'chevron-left';
245
+ });
246
+
247
+ /**
248
+ * An item shows its icon alone, except a Back button before iOS 26, which
249
+ * shows the previous screen's title beside its chevron.
250
+ */
251
+ protected readonly iconOnly = computed(
252
+ () => !!this.#icon() && !(this.back() && this.design() === 'frosted'),
253
+ );
254
+
255
+ protected readonly labelShown = computed(
256
+ () => !this.#icon() || !this.iconOnly(),
257
+ );
258
+
259
+ /** A prominent item stands on its own: in a group, it's a plain one. */
260
+ readonly #prominent = computed(() => this.prominent() && !this.#group);
261
+
262
+ /**
263
+ * As UiToolbarGroup's: the glass reacts to touch, unless Reduce Motion is
264
+ * on where the runtime draws it.
265
+ */
266
+ protected readonly interactive = computed(
267
+ () =>
268
+ !this.disabled() &&
269
+ (this.#systemDrawsGlass || !this.#settings.reduceMotion()),
270
+ );
271
+
272
+ /**
273
+ * The color of the item's icon, as a theme token (labelClass colors the
274
+ * label). Glass items take the glass's foreground, and a prominent one its
275
+ * tint's. Before iOS 26 bar buttons were the app's tint color. Material's
276
+ * top app bar draws its navigation icon in the surface's foreground, and
277
+ * its actions in the quieter variant; a prominent action is filled.
278
+ */
279
+ readonly #iconColor = computed(() => {
280
+ const prominent = this.#prominent();
281
+ switch (this.design()) {
282
+ case 'glass':
283
+ return prominent
284
+ ? 'var(--glass-tint-foreground)'
285
+ : 'var(--glass-foreground)';
286
+ case 'material':
287
+ if (prominent) return 'var(--primary-foreground)';
288
+ return this.back() ? 'var(--foreground)' : 'var(--muted-foreground)';
289
+ default:
290
+ return 'var(--primary)';
291
+ }
292
+ });
293
+
294
+ // The icon is drawn here, as the dock draws its own, rather than by a
295
+ // nested <ui-icon>: an SVG can't read a text class, so the token is
296
+ // resolved to its value and written into the markup (see icon-theme.ts).
297
+ protected readonly iconMarkup = computed(() => {
298
+ const icon = this.#icon();
299
+ return icon ? iconSvg(icon, this.#resolveColor(this.#iconColor())) : null;
300
+ });
301
+
302
+ protected readonly iconStyle = computed(() => {
303
+ const size = this.design() === 'glass' ? 22 : 24;
304
+ return { width: `${size}px`, height: `${size}px` };
305
+ });
306
+
307
+ protected readonly buttonClass = computed(() => {
308
+ const iconOnly = this.iconOnly();
309
+ const prominent = this.#prominent();
310
+ let size: string;
311
+ switch (this.design()) {
312
+ case 'glass':
313
+ size = cn('h-[44px]', iconOnly ? 'w-[44px]' : 'px-[16px]');
314
+ break;
315
+ case 'material':
316
+ // A 48dp target for an icon, filled in a 40dp circle when it's
317
+ // prominent; a labeled action is a 40dp text or filled button.
318
+ if (iconOnly) {
319
+ size = prominent
320
+ ? 'h-[40px] w-[40px] m-[4px] rounded-full bg-primary overflow-hidden'
321
+ : 'h-[48px] w-[48px]';
322
+ } else {
323
+ // 4dp in from its 48dp target, as Material insets its buttons.
324
+ size = cn(
325
+ 'mx-[4px] h-[40px] rounded-full px-[12px] overflow-hidden',
326
+ prominent && 'bg-primary px-[16px]',
327
+ );
328
+ }
329
+ break;
330
+ default:
331
+ // A 44pt target. The Back button's chevron sits close to its title.
332
+ size = cn(
333
+ 'h-[44px]',
334
+ iconOnly ? 'min-w-[44px] px-[8px]' : 'gap-[4px] pl-[4px] pr-[8px]',
335
+ );
336
+ }
337
+ return cn(
338
+ 'flex-row items-center flex relative justify-center',
339
+ size,
340
+ // Before iOS 26 a pressed bar button fades, rather than moving.
341
+ this.pressing() && this.design() === 'frosted' && 'opacity-30',
342
+ this.disabled() && 'opacity-50',
343
+ );
344
+ });
345
+
346
+ /**
347
+ * The press's highlight: Material's state layer, and on glass a shade
348
+ * under the finger where the glass doesn't react by itself (Android's
349
+ * glass, or the web's under Reduce Motion). It comes and goes with the
350
+ * finger, with no motion.
351
+ */
352
+ protected readonly highlightClass = computed(() => {
353
+ if (!this.pressing() || this.design() === 'frosted') return null;
354
+ if (this.#material()) {
355
+ // An icon button's layer is a 40dp circle in its 48dp target; a
356
+ // filled or text button's fills it.
357
+ const prominent = this.#prominent();
358
+ return cn(
359
+ 'opacity-10 absolute',
360
+ this.iconOnly() && !prominent
361
+ ? 'h-[40px] w-[40px] rounded-full bg-foreground left-[4px] top-[4px]'
362
+ : 'h-full w-full left-0 top-0',
363
+ prominent ? 'bg-primary-foreground' : !this.iconOnly() && 'bg-primary',
364
+ );
365
+ }
366
+ // The glass swells under the finger by itself: an item's own, or the
367
+ // group's capsule around a grouped item, as iOS 26's does. Except on
368
+ // Android, whose glass doesn't react, and where Reduce Motion stills it.
369
+ const glassReacts = this.interactive() && this.#platform !== 'Android';
370
+ return glassReacts
371
+ ? null
372
+ : 'h-full w-full rounded-full bg-glass-foreground opacity-15 absolute left-0 top-0';
373
+ });
374
+
375
+ protected readonly labelClass = computed(() => {
376
+ const prominent = this.#prominent();
377
+ switch (this.design()) {
378
+ case 'glass':
379
+ return cn(
380
+ 'text-[17px] font-medium leading-[22px]',
381
+ prominent ? 'text-glass-tint-foreground' : 'text-glass-foreground',
382
+ );
383
+ case 'material':
384
+ return cn(
385
+ 'text-[14px] font-medium leading-[20px]',
386
+ prominent ? 'text-primary-foreground' : 'text-primary',
387
+ );
388
+ default:
389
+ return cn(
390
+ 'text-[17px] leading-[22px] text-primary',
391
+ prominent && 'font-semibold',
392
+ );
393
+ }
394
+ });
395
+
396
+ // Screen readers say a disabled item is dimmed. A trait of its own, merged
397
+ // into `button` by the element (see LynxElement.setAccessibilityTrait). Set
398
+ // here rather than with lynxAccessibilityTraits, which the template can't
399
+ // put on the item's own host.
400
+ protected readonly disabledTrait = effect(() => {
401
+ this.#host.nativeElement.setAccessibilityTrait('disabled', this.disabled());
402
+ });
403
+
404
+ protected press(down: boolean): void {
405
+ this.pressing.set(down && !this.disabled());
406
+ }
407
+
408
+ protected tap(): void {
409
+ if (!this.disabled()) this.pressed.emit();
410
+ }
411
+ }
412
+
413
+ /**
414
+ * Flexible space between a toolbar's items, which pushes what follows it to
415
+ * the end, as SwiftUI's `ToolbarSpacer(.flexible)` does:
416
+ *
417
+ * ```html
418
+ * <ui-toolbar>
419
+ * <ui-toolbar-item icon="filter" label="Filter" />
420
+ * <ui-toolbar-spacer />
421
+ * <ui-toolbar-item icon="pencil" label="Compose" />
422
+ * </ui-toolbar>
423
+ * ```
424
+ */
425
+ @Component({
426
+ selector: 'ui-toolbar-spacer',
427
+ encapsulation: ViewEncapsulation.None,
428
+ // The host is the space: a component's host is a plain view on Lynx.
429
+ host: { class: 'flex-1' },
430
+ template: '',
431
+ })
432
+ export class UiToolbarSpacer {}
433
+
434
+ /**
435
+ * A bar of actions along the bottom of the screen, over the content, which
436
+ * scrolls beneath it: iOS's toolbar, and Material 3 Expressive's docked
437
+ * toolbar. Put items, groups and spacers in it:
438
+ *
439
+ * ```html
440
+ * <ui-toolbar>
441
+ * <ui-toolbar-item icon="list-checks" label="Checklist" />
442
+ * <ui-toolbar-item icon="camera" label="Camera" />
443
+ * <ui-toolbar-item icon="pencil" label="New note" />
444
+ * </ui-toolbar>
445
+ * ```
446
+ *
447
+ * Items spread across it, or sit at either end around a
448
+ * `<ui-toolbar-spacer>`. Put `ui-bar-scroll` on the screen's scroll view and
449
+ * its content ends above the toolbar.
450
+ *
451
+ * Its form follows the platform (see GlassDesign):
452
+ *
453
+ * - `'glass'`: no bar at all. The items float in glass above the home
454
+ * indicator, and content fades out as it passes beneath them.
455
+ * - `'frosted'`: the toolbar of iOS 15 to 25, edge to edge, on the bars'
456
+ * chrome material with a hairline on top while content continues beneath
457
+ * it, and clear once it's scrolled to the end.
458
+ * - `'material'`: a docked toolbar on the surface container color.
459
+ */
460
+ @Component({
461
+ selector: 'ui-toolbar',
462
+ imports: [LYNX_ELEMENTS, UiGlass],
463
+ encapsulation: ViewEncapsulation.None,
464
+ host: {
465
+ '[class]': 'hostClass()',
466
+ '[style.background-image]': 'edgeFade()',
467
+ },
468
+ template: `
469
+ @if (frostedBackground()) {
470
+ <blur-view
471
+ ui-glass
472
+ shape="rounded"
473
+ [radius]="0"
474
+ material="chrome"
475
+ class="absolute bottom-0 left-0 right-0 top-0"
476
+ ></blur-view>
477
+ <view class="h-[0.5px] bg-border absolute left-0 right-0 top-0"></view>
478
+ }
479
+ <view [class]="rowClass()"><ng-content /></view>
480
+ `,
481
+ })
482
+ export class UiToolbar {
483
+ readonly userClass = input<string>('', { alias: 'class' });
484
+
485
+ readonly #state = inject(UiBarState);
486
+ readonly #platform = platformOf(inject(Injector));
487
+
488
+ /** The design on this platform (see GlassDesign), which the items follow. */
489
+ readonly design = injectGlassDesign();
490
+
491
+ /** The toolbar's height above the home indicator, for the scroll view. */
492
+ readonly height = computed(() => TOOLBAR_HEIGHT[this.design()]);
493
+
494
+ protected readonly attachment = this.#state.attach(
495
+ this.#state.bottomBar,
496
+ this,
497
+ inject(DestroyRef),
498
+ );
499
+
500
+ /**
501
+ * The frosted toolbar's material shows while content continues beneath
502
+ * it, as iOS 15 to 25 draw it, and goes once it's scrolled to the end.
503
+ */
504
+ protected readonly frostedBackground = computed(
505
+ () => this.design() === 'frosted' && this.#state.scrolledUnderBottom(),
506
+ );
507
+
508
+ /**
509
+ * On Android, glass bars fade the content beneath them themselves, since
510
+ * its scroll view takes no mask (see edgeFadeOverlay).
511
+ */
512
+ protected readonly edgeFade = computed(() =>
513
+ this.design() === 'glass' && this.#platform === 'Android'
514
+ ? edgeFadeOverlay('top')
515
+ : null,
516
+ );
517
+
518
+ protected readonly hostClass = computed(() => {
519
+ const design = this.design();
520
+ return cn(
521
+ 'flex-col flex absolute bottom-0 left-0 right-0',
522
+ design === 'glass' ? 'px-[16px] pb-safe-[8px]' : 'pb-safe',
523
+ design === 'material' && 'bg-secondary',
524
+ this.userClass(),
525
+ );
526
+ });
527
+
528
+ protected readonly rowClass = computed(() =>
529
+ cn(
530
+ 'flex-row items-center flex relative justify-between',
531
+ this.design() === 'material' ? 'h-[64px] px-[16px]' : 'h-[44px]',
532
+ this.design() === 'frosted' ? 'px-[8px]' : 'gap-[8px]',
533
+ ),
534
+ );
535
+ }
@@ -1,3 +1,6 @@
1
+ /// <reference types="node" />
2
+ // Not copied into apps (it checks the theme against a component), so it can
3
+ // read the theme files with Node.
1
4
  import { readFileSync } from 'node:fs';
2
5
  import { describe, expect, it } from 'vitest';
3
6
  import {
@@ -11,7 +14,9 @@ import {
11
14
  * copy in step with default.css and dark.css.
12
15
  */
13
16
  const tokens = (file: string): Record<string, string> => {
14
- const css = readFileSync(new URL(file, import.meta.url), 'utf8');
17
+ // Relative to the package, where `ng test` runs: the test runs from a build
18
+ // of it elsewhere, so `import.meta.url` doesn't point next to the CSS.
19
+ const css = readFileSync(`src/lib/theme/${file}`, 'utf8');
15
20
  const values: Record<string, string> = {};
16
21
  for (const match of css.matchAll(/--([\w-]+):\s*(rgba\([^)]*\))/g)) {
17
22
  values[match[1]!] = match[2]!;
@@ -21,10 +26,10 @@ const tokens = (file: string): Record<string, string> => {
21
26
 
22
27
  describe('chart color tables', () => {
23
28
  it('match default.css', () => {
24
- expect(CHART_LIGHT_COLORS).toEqual(tokens('./default.css'));
29
+ expect(CHART_LIGHT_COLORS).toEqual(tokens('default.css'));
25
30
  });
26
31
 
27
32
  it('match dark.css', () => {
28
- expect(CHART_DARK_COLORS).toEqual(tokens('./dark.css'));
33
+ expect(CHART_DARK_COLORS).toEqual(tokens('dark.css'));
29
34
  });
30
35
  });
@@ -1,8 +1,9 @@
1
1
  /*
2
2
  * @blotch/ui dark theme
3
3
  *
4
- * Complete rgba() color values referenced directly via var(--x) — see
5
- * default.css for why complete rgba values are used instead of HSL on Lynx.
4
+ * Complete rgba() color values referenced directly via var(--x), each with
5
+ * its `-rgb` channels for opacity modifiers — see default.css for why both,
6
+ * and why not HSL, on Lynx.
6
7
  *
7
8
  * Apply via a `.dark` class on the page element, or import as an
8
9
  * alternative to default.css.
@@ -10,40 +11,75 @@
10
11
 
11
12
  page.dark {
12
13
  --background: rgba(9, 9, 11, 1);
14
+ --background-rgb: 9, 9, 11;
13
15
  --foreground: rgba(250, 250, 250, 1);
16
+ --foreground-rgb: 250, 250, 250;
14
17
 
15
18
  --card: rgba(9, 9, 11, 1);
19
+ --card-rgb: 9, 9, 11;
16
20
  --card-foreground: rgba(250, 250, 250, 1);
21
+ --card-foreground-rgb: 250, 250, 250;
17
22
 
18
23
  --primary: rgba(250, 250, 250, 1);
24
+ --primary-rgb: 250, 250, 250;
19
25
  --primary-foreground: rgba(24, 24, 27, 1);
26
+ --primary-foreground-rgb: 24, 24, 27;
20
27
 
21
28
  --secondary: rgba(39, 39, 42, 1);
29
+ --secondary-rgb: 39, 39, 42;
22
30
  --secondary-foreground: rgba(250, 250, 250, 1);
31
+ --secondary-foreground-rgb: 250, 250, 250;
23
32
 
24
33
  --muted: rgba(39, 39, 42, 1);
34
+ --muted-rgb: 39, 39, 42;
25
35
  --muted-foreground: rgba(161, 161, 170, 1);
36
+ --muted-foreground-rgb: 161, 161, 170;
26
37
 
27
38
  --accent: rgba(39, 39, 42, 1);
39
+ --accent-rgb: 39, 39, 42;
28
40
  --accent-foreground: rgba(250, 250, 250, 1);
41
+ --accent-foreground-rgb: 250, 250, 250;
29
42
 
30
43
  --destructive: rgba(127, 29, 29, 1);
44
+ --destructive-rgb: 127, 29, 29;
31
45
  --destructive-foreground: rgba(250, 250, 250, 1);
46
+ --destructive-foreground-rgb: 250, 250, 250;
32
47
  --destructive-subtle: rgba(127, 29, 29, 0.1);
33
48
 
34
49
  --border: rgba(39, 39, 42, 1);
50
+ --border-rgb: 39, 39, 42;
35
51
  --input: rgba(39, 39, 42, 1);
52
+ --input-rgb: 39, 39, 42;
36
53
  --ring: rgba(212, 212, 216, 1);
54
+ --ring-rgb: 212, 212, 216;
37
55
  /* Translucent variant of --ring for a softer focus outline (see default.css). */
38
56
  --ring-subtle: rgba(212, 212, 216, 0.4);
39
57
 
40
58
  /* Series palette for charts (bg-chart-1, and chart-theme.ts in the chart
41
59
  component, which mirrors these values for SVG). */
42
60
  --chart-1: rgba(38, 98, 217, 1);
61
+ --chart-1-rgb: 38, 98, 217;
43
62
  --chart-2: rgba(46, 184, 138, 1);
63
+ --chart-2-rgb: 46, 184, 138;
44
64
  --chart-3: rgba(232, 140, 48, 1);
65
+ --chart-3-rgb: 232, 140, 48;
45
66
  --chart-4: rgba(175, 87, 219, 1);
67
+ --chart-4-rgb: 175, 87, 219;
46
68
  --chart-5: rgba(226, 54, 112, 1);
69
+ --chart-5-rgb: 226, 54, 112;
70
+
71
+ /* Liquid Glass (the glass component): see default.css. */
72
+ --glass-tint: rgba(250, 250, 250, 1);
73
+ --glass-tint-rgb: 250, 250, 250;
74
+ --glass-tint-foreground: rgba(24, 24, 27, 1);
75
+ --glass-tint-foreground-rgb: 24, 24, 27;
76
+ --glass-foreground: rgba(250, 250, 250, 1);
77
+ --glass-foreground-rgb: 250, 250, 250;
78
+ --glass-muted-foreground: rgba(212, 212, 216, 1);
79
+ --glass-muted-foreground-rgb: 212, 212, 216;
80
+ --glass-clear-foreground: rgba(255, 255, 255, 1);
81
+ --glass-clear-foreground-rgb: 255, 255, 255;
82
+ --glass-dim: rgba(0, 0, 0, 0.35);
47
83
 
48
84
  --radius: 8px;
49
85
  }