@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,430 @@
1
+ import {
2
+ booleanAttribute,
3
+ Component,
4
+ computed,
5
+ effect,
6
+ ElementRef,
7
+ inject,
8
+ input,
9
+ numberAttribute,
10
+ signal,
11
+ ViewEncapsulation,
12
+ } from '@angular/core';
13
+ import { LYNX_ELEMENTS, LynxAccessibilitySettings } from '@blotch/angular-lynx';
14
+
15
+ import { cn } from '../../utils/cn';
16
+ import {
17
+ injectColorScheme,
18
+ injectThemeColors,
19
+ resolveIconColor,
20
+ UI_ICON_COLOR,
21
+ } from '../icon/icon-theme';
22
+ import { injectGlassDesign, injectSystemDrawsGlass } from './glass-design';
23
+
24
+ /**
25
+ * `'regular'` glass, the default, frosts what's behind it and suits most
26
+ * controls. `'clear'` glass barely frosts it, for controls over photos and
27
+ * video; put a dimming layer under it when what's behind is bright (see
28
+ * `bg-glass-dim`). Don't mix the two on one screen.
29
+ */
30
+ export type GlassVariant = 'regular' | 'clear';
31
+
32
+ /**
33
+ * A capsule (fully rounded ends, the default for controls), a circle (give
34
+ * it equal width and height) or a rectangle with `radius` corners.
35
+ */
36
+ export type GlassShape = 'capsule' | 'circle' | 'rounded';
37
+
38
+ /**
39
+ * The blur material the frosted design draws on iOS 15 to 25: UIKit's
40
+ * adaptive materials, from the most translucent to the most opaque, and the
41
+ * bars' own. Apple gave each role its own: `'chrome'` for bars (the dock,
42
+ * navigation bar and toolbar), `'regular'` for menus, popovers and alerts,
43
+ * `'thin'` for buttons floating over content.
44
+ */
45
+ export type GlassMaterial =
46
+ | 'ultra-thin'
47
+ | 'thin'
48
+ | 'regular'
49
+ | 'thick'
50
+ | 'chrome';
51
+
52
+ /**
53
+ * What a glass element draws:
54
+ *
55
+ * - `'glass'`: Liquid Glass.
56
+ * - `'frosted'`: a blur material, on iOS 15 to 25.
57
+ * - `'solid'`: an opaque surface. That's Material on Android, unless the app
58
+ * opted into glass there, and glass under Reduce Transparency on Android
59
+ * and the web, which can't adapt the glass they draw. iOS adapts its own.
60
+ */
61
+ export type GlassSurface = 'glass' | 'frosted' | 'solid';
62
+
63
+ /**
64
+ * How much the glass blurs where the blur is drawn by the runtime: Android's
65
+ * approximation, and the web before WebGL takes over. iOS's glass ignores it.
66
+ * Clear glass barely blurs.
67
+ */
68
+ const GLASS_BLUR: Readonly<Record<GlassVariant, string>> = {
69
+ regular: '16px',
70
+ clear: '6px',
71
+ };
72
+
73
+ /**
74
+ * The blur of the frosted fallback, the `blur-effect` styles from before
75
+ * iOS 13, which hosts without the materials draw (see `#attributes`). The
76
+ * materials have Apple's own.
77
+ */
78
+ const FROSTED_BLUR = '20px';
79
+
80
+ /**
81
+ * A solid surface floats over the content as glass does, so it gets
82
+ * Material's elevation shadow (level 2). An inline style, because Tailwind's
83
+ * shadow classes draw nothing on Lynx.
84
+ */
85
+ const SOLID_ELEVATION =
86
+ '0 1px 2px rgba(0, 0, 0, 0.3), 0 2px 6px rgba(0, 0, 0, 0.15)';
87
+
88
+ /** AngularLynx's wrapper of the native `blur-view` the component sits on. */
89
+ type BlurViewElement = { setAttribute(name: string, value: unknown): void };
90
+
91
+ /**
92
+ * The color an icon projected into the glass draws with when it's given
93
+ * none: the glass's foreground (see UI_ICON_COLOR), the way text there reads
94
+ * `text-glass-foreground`. A named function, not an inline factory: the
95
+ * component's metadata keeps its own copy of an inline one, which nothing
96
+ * calls, so an app's coverage report would show it as uncovered.
97
+ */
98
+ const glassIconColor = () => {
99
+ const glass = inject(UiGlass);
100
+ return computed(() => glass.foreground());
101
+ };
102
+
103
+ /**
104
+ * Liquid Glass, for the controls and bars that float above an app's content:
105
+ * a `<blur-view>` that draws the right material on each platform.
106
+ *
107
+ * ```html
108
+ * <blur-view ui-glass class="flex-row items-center gap-2 px-4 py-2">
109
+ * <text class="text-glass-foreground">Now playing</text>
110
+ * </blur-view>
111
+ * ```
112
+ *
113
+ * It sits on the `<blur-view>` itself rather than wrapping one, so the
114
+ * element you lay out and style is the glass, and what you put in it is
115
+ * inside the glass. That matters on iOS: interactive glass reacts only to
116
+ * touches on itself and what's in it, not on views stacked over it.
117
+ *
118
+ * What it draws follows the platform (see GlassDesign): UIKit's Liquid Glass
119
+ * on iOS 26 and later, a frosted blur material on iOS 15 to 25, an opaque
120
+ * Material surface on Android unless the app opts into glass with
121
+ * `provideGlass({ android: 'glass' })`, and the runtime's WebGL glass in the
122
+ * web preview. It also follows the user's accessibility settings where the
123
+ * platform doesn't: see {@link surface}.
124
+ *
125
+ * Text and icons inside read `text-glass-foreground` (and
126
+ * `text-glass-muted-foreground`), which the glass points at the right color
127
+ * for its variant and the user's contrast setting. Glass belongs to the
128
+ * layer above content, and shouldn't be layered on glass.
129
+ */
130
+ @Component({
131
+ selector: 'blur-view[ui-glass]',
132
+ imports: [LYNX_ELEMENTS],
133
+ encapsulation: ViewEncapsulation.None,
134
+ // Icons projected into the glass inject this through its element injector,
135
+ // which content projection keeps them under.
136
+ providers: [{ provide: UI_ICON_COLOR, useFactory: glassIconColor }],
137
+ host: {
138
+ '[class]': 'surfaceClass()',
139
+ '[style.border-radius.px]': 'cornerRadius()',
140
+ '[style.background-color]': 'solidTint()',
141
+ '[style.box-shadow]': 'elevation()',
142
+ },
143
+ // iOS draws no background on a blur view, so a frosted material takes its
144
+ // tint from a fill under the content, the shape of the glass: the filled
145
+ // button of the design before Liquid Glass.
146
+ template: `
147
+ @if (frostedTint(); as color) {
148
+ <view
149
+ [class]="fillClass()"
150
+ [style.background-color]="color"
151
+ [style.border-radius.px]="cornerRadius()"
152
+ ></view>
153
+ }
154
+ <ng-content />
155
+ `,
156
+ })
157
+ export class UiGlass {
158
+ /** `'regular'` or `'clear'`; see GlassVariant. */
159
+ readonly variant = input<GlassVariant>('regular');
160
+ /** `'capsule'`, `'circle'` or `'rounded'`; see GlassShape. */
161
+ readonly shape = input<GlassShape>('capsule');
162
+ /**
163
+ * The corner radius of a `'rounded'` shape, in px. For a shape nested in
164
+ * it, see `concentricRadius()`.
165
+ */
166
+ readonly radius = input(20, { transform: numberAttribute });
167
+ /**
168
+ * A color the glass takes on, such as `var(--glass-tint)` for the theme's
169
+ * tint or `#2563eb`: a theme token or any CSS color. Use it sparingly, for
170
+ * the screen's main action. It fills a solid surface or a frosted
171
+ * material, as Material, and iOS before Liquid Glass, fill a button.
172
+ */
173
+ readonly tint = input<string | undefined>(undefined);
174
+ /**
175
+ * The glass reacts to touch, swelling and brightening under the finger as
176
+ * a Liquid Glass control does: on iOS 26 and later, and in the web preview.
177
+ */
178
+ readonly interactive = input(false, { transform: booleanAttribute });
179
+ /**
180
+ * The blur material of the frosted design, on iOS 15 to 25: the one Apple
181
+ * used for the glass's role (see GlassMaterial). Clear glass draws its
182
+ * dark form, in either theme, as Apple's controls over media did. Liquid
183
+ * Glass has one material, and ignores it.
184
+ */
185
+ readonly material = input<GlassMaterial>('regular');
186
+ /**
187
+ * Pairs this shape with another across a change, as SwiftUI's
188
+ * `glassEffectID` does: when a shape with an id goes (removed, or hidden
189
+ * with `visibility`) and one with the same id comes in the same update,
190
+ * the two are one piece of glass changing shape. The new one grows out of
191
+ * the old one's frame and corners, and the old one's content fades out in
192
+ * it. In the same `ui-glass-group`, the group merges the two as they move.
193
+ *
194
+ * On iOS 26 and later, in an app `ng build --platform ios` makes (its
195
+ * blur-view adds this). Elsewhere one shape replaces the other at once.
196
+ */
197
+ readonly glassId = input<string | undefined>(undefined);
198
+
199
+ readonly #settings = inject(LynxAccessibilitySettings);
200
+ readonly #systemDrawsGlass = injectSystemDrawsGlass();
201
+ readonly #scheme = injectColorScheme();
202
+ readonly #colors = injectThemeColors();
203
+ readonly #host = inject<ElementRef<BlurViewElement>>(ElementRef);
204
+
205
+ /**
206
+ * The design on this platform, which components built on the glass follow
207
+ * for their own form (see GlassDesign).
208
+ */
209
+ readonly design = injectGlassDesign();
210
+
211
+ /**
212
+ * The id a component pairs this shape under, such as a menu that grows out
213
+ * of it (see pair). It takes the place of `glassId` while it's set.
214
+ */
215
+ readonly #pairedId = signal<string | null>(null);
216
+
217
+ /**
218
+ * @internal Pairs the shape under `id` (see glassId), or stops pairing it
219
+ * (null): for a component that grows its own glass out of this shape, such
220
+ * as the menu of a glass button that opens one.
221
+ */
222
+ pair(id: string | null): void {
223
+ this.#pairedId.set(id);
224
+ }
225
+
226
+ /**
227
+ * What the glass draws (see GlassSurface): the design's material, or an
228
+ * opaque surface. Where the runtime draws the glass (Android's opt-in and
229
+ * the web), Reduce Transparency turns it opaque, as UIKit turns its own.
230
+ */
231
+ readonly surface = computed<GlassSurface>(() => {
232
+ const design = this.design();
233
+ if (design === 'material') return 'solid';
234
+ if (!this.#systemDrawsGlass && this.#settings.reduceTransparency()) {
235
+ return 'solid';
236
+ }
237
+ return design;
238
+ });
239
+
240
+ /** Whether what's behind shows through clear glass. */
241
+ readonly #seeThrough = computed(
242
+ () => this.variant() === 'clear' && this.surface() !== 'solid',
243
+ );
244
+
245
+ /**
246
+ * The color of text and icons on this glass, as a `var()`: the clear
247
+ * foreground on see-through clear glass, which sits over photos and video,
248
+ * and the glass foreground otherwise. Projected `<ui-icon>`s get it, and an
249
+ * SVG of your own can resolve it the way icons do (see icon-theme.ts).
250
+ */
251
+ readonly foreground = computed(() =>
252
+ this.#seeThrough()
253
+ ? 'var(--glass-clear-foreground)'
254
+ : 'var(--glass-foreground)',
255
+ );
256
+
257
+ /**
258
+ * The tint as a color the native element can parse: an attribute can't
259
+ * read CSS variables, so a theme token is swapped for its value in the
260
+ * active scheme, as icons do.
261
+ */
262
+ readonly #tintColor = computed(() => {
263
+ const tint = this.tint();
264
+ return tint ? resolveIconColor(tint, this.#colors()) : null;
265
+ });
266
+
267
+ /**
268
+ * A tinted solid surface is filled with the tint, as Material's filled
269
+ * buttons are, and as iOS draws tinted glass under Reduce Transparency.
270
+ */
271
+ protected readonly solidTint = computed(() =>
272
+ this.surface() === 'solid' ? this.#tintColor() : null,
273
+ );
274
+
275
+ protected readonly frostedTint = computed(() =>
276
+ this.surface() === 'frosted' ? this.#tintColor() : null,
277
+ );
278
+
279
+ protected readonly fillClass = computed(() =>
280
+ cn(
281
+ 'h-full w-full absolute left-0 top-0',
282
+ this.shape() !== 'rounded' && 'rounded-full',
283
+ ),
284
+ );
285
+
286
+ protected readonly elevation = computed(() =>
287
+ this.surface() === 'solid' ? SOLID_ELEVATION : null,
288
+ );
289
+
290
+ protected readonly cornerRadius = computed(() =>
291
+ this.shape() === 'rounded' ? this.radius() : null,
292
+ );
293
+
294
+ protected readonly surfaceClass = computed(() => {
295
+ const increaseContrast = this.#settings.increaseContrast();
296
+ return cn(
297
+ this.shape() !== 'rounded' && 'rounded-full',
298
+ // Material's tonal surface, and the opaque glass of Reduce
299
+ // Transparency, unless a tint fills it (see solidTint). iOS ignores a
300
+ // blur-view's background, but never draws a solid surface.
301
+ this.surface() === 'solid' && !this.tint() && 'bg-secondary',
302
+ // Increase Contrast gives the glass a visible edge, as UIKit gives its
303
+ // own glass one.
304
+ increaseContrast &&
305
+ !this.#systemDrawsGlass &&
306
+ 'border border-glass-foreground',
307
+ // The glass points the foreground tokens its content reads at the
308
+ // right color (a variable redefined here reaches the projected text,
309
+ // since variables inherit and `color` doesn't): white on see-through
310
+ // clear glass, and full strength for muted text under Increase
311
+ // Contrast.
312
+ this.#seeThrough()
313
+ ? '[--glass-foreground:var(--glass-clear-foreground)] [--glass-muted-foreground:var(--glass-clear-foreground)]'
314
+ : increaseContrast &&
315
+ '[--glass-muted-foreground:var(--glass-foreground)]',
316
+ );
317
+ });
318
+
319
+ /**
320
+ * The blur-view's attributes for the surface. The glass and the materials
321
+ * are forced to the app's color scheme (`ios-user-interface-style`), which
322
+ * iOS would otherwise take from the device: an app in its light theme on a
323
+ * phone in dark mode would get dark glass under dark text. A solid surface
324
+ * turns the blur off, and on Android stops the blur-view redrawing the page
325
+ * behind it on every frame (`enable-auto-blur`), which it does even with no
326
+ * radius.
327
+ *
328
+ * The frosted design draws `blur-material`, which the iOS shell's
329
+ * blur-view adds (packages/native's BlurView.swift): Lynx's own
330
+ * has only the `blur-effect` styles from before iOS 13, a single weight in
331
+ * a single appearance. A host without it, such as Lynx Explorer, ignores
332
+ * the attribute and draws the `blur-effect` instead, picked for the scheme
333
+ * since those styles don't adapt.
334
+ */
335
+ readonly #attributes = computed(() => {
336
+ const surface = this.surface();
337
+ const variant = this.variant();
338
+ // Apple's controls over media used the dark materials in either theme.
339
+ const dark =
340
+ this.#scheme() === 'dark' ||
341
+ (surface === 'frosted' && variant === 'clear');
342
+ let effect = 'light';
343
+ let radius = '0px';
344
+ let material: GlassMaterial | null = null;
345
+ if (surface === 'glass') {
346
+ effect = 'glass';
347
+ radius = GLASS_BLUR[variant];
348
+ } else if (surface === 'frosted') {
349
+ effect = dark ? 'dark' : 'extra-light';
350
+ radius = FROSTED_BLUR;
351
+ material = this.material();
352
+ }
353
+ return {
354
+ 'blur-effect': effect,
355
+ 'blur-radius': radius,
356
+ 'blur-material': material,
357
+ 'enable-auto-blur': surface !== 'solid',
358
+ 'glass-style': variant,
359
+ 'glass-tint-color': this.#tintColor(),
360
+ 'glass-interactive': this.interactive() || null,
361
+ 'glass-effect-id': this.#pairedId() ?? this.glassId() ?? null,
362
+ 'ios-user-interface-style': dark ? 'dark' : 'light',
363
+ };
364
+ });
365
+
366
+ // Set on the element as they are, rather than through host attribute
367
+ // bindings, which turn every value into a string: Lynx reads
368
+ // `glass-interactive` and `enable-auto-blur` as booleans. `null` removes an
369
+ // attribute, which restores Lynx's default.
370
+ protected readonly setAttributes = effect(() => {
371
+ const element = this.#host.nativeElement;
372
+ for (const [name, value] of Object.entries(this.#attributes())) {
373
+ element.setAttribute(name, value);
374
+ }
375
+ });
376
+ }
377
+
378
+ /**
379
+ * Merges the glass elements inside it, as iOS 26 draws a group of glass
380
+ * controls: shapes closer than `spacing` flow into each other like droplets,
381
+ * and the group draws them in one pass. Put a `<blur-view ui-glass>` directly
382
+ * inside it for each shape: iOS merges the glass in the group's own content,
383
+ * and a wrapper that draws anything of its own comes between them.
384
+ *
385
+ * ```html
386
+ * <blur-view ui-glass-group [spacing]="16" class="flex-row gap-2">
387
+ * <blur-view ui-glass shape="circle" class="h-12 w-12">…</blur-view>
388
+ * <blur-view ui-glass shape="circle" class="h-12 w-12">…</blur-view>
389
+ * </blur-view>
390
+ * ```
391
+ *
392
+ * The group draws nothing itself. Shapes merge on iOS 26 and later and in
393
+ * the web preview. Android's glass doesn't merge, and the frosted and
394
+ * Material designs have no glass to merge.
395
+ *
396
+ * Moving or resizing a shape, with a CSS transition for example, morphs the
397
+ * merged glass with it. On iOS 26, in an app `ng build --platform ios`
398
+ * makes, a shape that's added or removed (or shown and hidden with
399
+ * `visibility`) materializes or dissolves, as UIKit's glass does, and two
400
+ * shapes with the same `glassId` morph into each other (see UiGlass's
401
+ * `glassId`). Elsewhere a shape that's added or removed appears or
402
+ * disappears at once.
403
+ */
404
+ @Component({
405
+ selector: 'blur-view[ui-glass-group]',
406
+ encapsulation: ViewEncapsulation.None,
407
+ template: `<ng-content />`,
408
+ })
409
+ export class UiGlassGroup {
410
+ /** How close two glass shapes come before they merge, in px. */
411
+ readonly spacing = input(0, { transform: numberAttribute });
412
+
413
+ readonly #design = injectGlassDesign();
414
+ readonly #host = inject<ElementRef<BlurViewElement>>(ElementRef);
415
+
416
+ // As UiGlass sets its own (see there). A container that isn't merging
417
+ // glass is a plain transparent view: `light` with no radius draws nothing,
418
+ // where `glass-container` would log an error before iOS 26. It never blurs,
419
+ // so on Android it never redraws the page behind it either.
420
+ protected readonly material = effect(() => {
421
+ const element = this.#host.nativeElement;
422
+ element.setAttribute(
423
+ 'blur-effect',
424
+ this.#design() === 'glass' ? 'glass-container' : 'light',
425
+ );
426
+ element.setAttribute('blur-radius', '0px');
427
+ element.setAttribute('enable-auto-blur', false);
428
+ element.setAttribute('spacing', this.spacing());
429
+ });
430
+ }
@@ -0,0 +1,19 @@
1
+ export {
2
+ UiGlass,
3
+ UiGlassGroup,
4
+ type GlassMaterial,
5
+ type GlassShape,
6
+ type GlassSurface,
7
+ type GlassVariant,
8
+ } from './glass';
9
+ export {
10
+ FIRST_GLASS_IOS,
11
+ UI_GLASS_OPTIONS,
12
+ glassDesignFor,
13
+ injectGlassDesign,
14
+ injectSystemDrawsGlass,
15
+ provideGlass,
16
+ type GlassDesign,
17
+ type GlassOptions,
18
+ } from './glass-design';
19
+ export { concentricRadius } from './concentric-radius';
@@ -11,9 +11,12 @@ import {
11
11
  ICON_LIGHT_COLORS,
12
12
  UI_ICON_COLOR,
13
13
  UI_ICON_THEME,
14
+ UI_THEME_COLORS,
15
+ injectColorScheme,
14
16
  injectIconColor,
15
17
  provideIconTheme,
16
18
  resolveIconColor,
19
+ type ThemeColors,
17
20
  } from './icon-theme';
18
21
 
19
22
  const LIGHT_FOREGROUND = ICON_LIGHT_COLORS['foreground'];
@@ -92,6 +95,41 @@ describe('provideIconTheme', () => {
92
95
  });
93
96
  });
94
97
 
98
+ describe('injectColorScheme', () => {
99
+ const scheme = (providers: Provider[], dark = signal(false)) =>
100
+ runInInjectionContext(
101
+ Injector.create({
102
+ providers: [
103
+ { provide: LynxTheme, useValue: { isDarkMode: dark } },
104
+ {
105
+ provide: UI_ICON_THEME,
106
+ useValue: { light: ICON_LIGHT_COLORS, dark: ICON_DARK_COLORS },
107
+ },
108
+ ...providers,
109
+ ],
110
+ }),
111
+ () => injectColorScheme(),
112
+ );
113
+
114
+ it("is light unless the theme says otherwise, whatever the device's appearance", () => {
115
+ expect(scheme([], signal(true))()).toBe('light');
116
+ expect(scheme([provideIconTheme({ colorScheme: 'dark' })])()).toBe('dark');
117
+ });
118
+
119
+ it("follows the device with 'system', or the app's own state", () => {
120
+ const dark = signal(false);
121
+ const system = scheme([provideIconTheme({ colorScheme: 'system' })], dark);
122
+ expect(system()).toBe('light');
123
+ dark.set(true);
124
+ expect(system()).toBe('dark');
125
+
126
+ const own = signal<'light' | 'dark'>('dark');
127
+ expect(scheme([provideIconTheme({ colorScheme: () => own() })])()).toBe(
128
+ 'dark',
129
+ );
130
+ });
131
+ });
132
+
95
133
  describe('injectIconColor', () => {
96
134
  it('draws with the light foreground by default, even on a dark device', () => {
97
135
  const resolve = setup([], signal(true));
@@ -131,3 +169,29 @@ describe('injectIconColor', () => {
131
169
  expect(resolve(undefined)).toBe(LIGHT_FOREGROUND);
132
170
  });
133
171
  });
172
+
173
+ describe('UI_THEME_COLORS', () => {
174
+ it('replaces the table’s colors for the active scheme, and only those it sets', () => {
175
+ const replaced = signal<ThemeColors>({
176
+ light: { primary: 'rgba(103, 80, 164, 1)' },
177
+ dark: { primary: 'rgba(208, 188, 255, 1)' },
178
+ });
179
+ const dark = signal(false);
180
+ const resolve = setup(
181
+ [
182
+ provideIconTheme({ colorScheme: 'system' }),
183
+ { provide: UI_THEME_COLORS, useValue: replaced },
184
+ ],
185
+ dark,
186
+ );
187
+ expect(resolve('var(--primary)')).toBe('rgba(103, 80, 164, 1)');
188
+ expect(resolve(undefined)).toBe(LIGHT_FOREGROUND);
189
+
190
+ dark.set(true);
191
+ expect(resolve('var(--primary)')).toBe('rgba(208, 188, 255, 1)');
192
+
193
+ // An element with no colors to set leaves the theme's.
194
+ replaced.set({});
195
+ expect(resolve('var(--primary)')).toBe(ICON_DARK_COLORS['primary']);
196
+ });
197
+ });
@@ -23,7 +23,9 @@ import { LynxTheme } from '@blotch/angular-lynx';
23
23
  *
24
24
  * The tables mirror `default.css` and `dark.css` (a test in the theme folder
25
25
  * keeps them in step). If you change a color there, change it here too, or pass
26
- * your own with {@link provideIconTheme}.
26
+ * your own with {@link provideIconTheme}. Colors set at runtime, such as
27
+ * Android's wallpaper colors, come from an element above instead (see
28
+ * {@link UI_THEME_COLORS}).
27
29
  */
28
30
 
29
31
  /** Theme token name (without `--`) to a complete CSS color. */
@@ -80,6 +82,12 @@ export const ICON_LIGHT_COLORS: IconColorTokens = {
80
82
  'chart-3': 'rgba(39, 71, 84, 1)',
81
83
  'chart-4': 'rgba(232, 196, 104, 1)',
82
84
  'chart-5': 'rgba(244, 164, 98, 1)',
85
+ 'glass-tint': 'rgba(24, 24, 27, 1)',
86
+ 'glass-tint-foreground': 'rgba(250, 250, 250, 1)',
87
+ 'glass-foreground': 'rgba(9, 9, 11, 1)',
88
+ 'glass-muted-foreground': 'rgba(63, 63, 70, 1)',
89
+ 'glass-clear-foreground': 'rgba(255, 255, 255, 1)',
90
+ 'glass-dim': 'rgba(0, 0, 0, 0.35)',
83
91
  };
84
92
 
85
93
  export const ICON_DARK_COLORS: IconColorTokens = {
@@ -107,6 +115,12 @@ export const ICON_DARK_COLORS: IconColorTokens = {
107
115
  'chart-3': 'rgba(232, 140, 48, 1)',
108
116
  'chart-4': 'rgba(175, 87, 219, 1)',
109
117
  'chart-5': 'rgba(226, 54, 112, 1)',
118
+ 'glass-tint': 'rgba(250, 250, 250, 1)',
119
+ 'glass-tint-foreground': 'rgba(24, 24, 27, 1)',
120
+ 'glass-foreground': 'rgba(250, 250, 250, 1)',
121
+ 'glass-muted-foreground': 'rgba(212, 212, 216, 1)',
122
+ 'glass-clear-foreground': 'rgba(255, 255, 255, 1)',
123
+ 'glass-dim': 'rgba(0, 0, 0, 0.35)',
110
124
  };
111
125
 
112
126
  export const UI_ICON_THEME = new InjectionToken<UiIconTheme>('UI_ICON_THEME', {
@@ -175,6 +189,60 @@ export const resolveIconColor = (
175
189
  : resolveIconColor(inner.slice(comma + 1), tokens);
176
190
  };
177
191
 
192
+ /**
193
+ * The color scheme the app's theme is in, as {@link provideIconTheme} sets it
194
+ * (`'light'` unless it says otherwise). For what has to match the app's CSS
195
+ * theme but can't read it: icons pick their color table with it, and glass
196
+ * picks a light or dark material.
197
+ */
198
+ export const injectColorScheme = (): Signal<IconColorScheme> => {
199
+ const option = inject(UI_ICON_THEME).colorScheme ?? 'light';
200
+ const appearance = inject(LynxTheme);
201
+ return computed(() =>
202
+ option === 'system'
203
+ ? appearance.isDarkMode()
204
+ ? 'dark'
205
+ : 'light'
206
+ : typeof option === 'function'
207
+ ? option()
208
+ : option,
209
+ );
210
+ };
211
+
212
+ /** Theme colors for each scheme, each a complete CSS color, as in the tables. */
213
+ export type ThemeColors = {
214
+ readonly light?: IconColorTokens;
215
+ readonly dark?: IconColorTokens;
216
+ };
217
+
218
+ /**
219
+ * Theme colors that replace the tables' for everything inside an element,
220
+ * as a signal: an element that sets the theme's CSS variables at runtime
221
+ * provides the values it sets, since SVGs and native attributes can't read
222
+ * them. `UiDynamicColor` provides Android's wallpaper colors this way. Icons,
223
+ * glass tints and charts inside it resolve their tokens against them.
224
+ */
225
+ export const UI_THEME_COLORS = new InjectionToken<Signal<ThemeColors>>(
226
+ 'UI_THEME_COLORS',
227
+ );
228
+
229
+ /**
230
+ * The color table for the app's color scheme, for a component's constructor:
231
+ * the theme's copy, with what an element above sets in its place (see
232
+ * {@link UI_THEME_COLORS}). A signal, so it follows scheme and theme changes.
233
+ */
234
+ export const injectThemeColors = (): Signal<IconColorTokens> => {
235
+ const theme = inject(UI_ICON_THEME);
236
+ const scheme = injectColorScheme();
237
+ const replaced = inject(UI_THEME_COLORS, { optional: true });
238
+
239
+ return computed(() => {
240
+ const active = scheme();
241
+ const colors = replaced?.()[active];
242
+ return colors ? { ...theme[active], ...colors } : theme[active];
243
+ });
244
+ };
245
+
178
246
  /**
179
247
  * For an SVG component's constructor: a function that resolves its `color`
180
248
  * input (or, without one, the surrounding {@link UI_ICON_COLOR}, then the
@@ -182,23 +250,9 @@ export const resolveIconColor = (
182
250
  * `computed()` calling it follows scheme and theme changes.
183
251
  */
184
252
  export const injectIconColor = (): ((color: string | undefined) => string) => {
185
- const theme = inject(UI_ICON_THEME);
186
- const appearance = inject(LynxTheme);
253
+ const tokens = injectThemeColors();
187
254
  const inherited = inject(UI_ICON_COLOR, { optional: true });
188
255
 
189
- const tokens = computed(() => {
190
- const option = theme.colorScheme ?? 'light';
191
- const scheme =
192
- option === 'system'
193
- ? appearance.isDarkMode()
194
- ? 'dark'
195
- : 'light'
196
- : typeof option === 'function'
197
- ? option()
198
- : option;
199
- return scheme === 'dark' ? theme.dark : theme.light;
200
- });
201
-
202
256
  return (color) =>
203
257
  resolveIconColor(color ?? inherited?.() ?? DEFAULT_ICON_COLOR, tokens());
204
258
  };