@blotch/dolan 22.2.2 → 22.2.4

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 (259) hide show
  1. package/dist/commands/add.d.ts +6 -2
  2. package/dist/commands/add.js +41 -11
  3. package/dist/commands/add.js.map +1 -1
  4. package/dist/commands/add.test.js +162 -9
  5. package/dist/commands/add.test.js.map +1 -1
  6. package/dist/commands/diff.d.ts +2 -1
  7. package/dist/commands/diff.js +38 -25
  8. package/dist/commands/diff.js.map +1 -1
  9. package/dist/commands/diff.test.js +72 -11
  10. package/dist/commands/diff.test.js.map +1 -1
  11. package/dist/commands/doctor.js +228 -34
  12. package/dist/commands/doctor.js.map +1 -1
  13. package/dist/commands/doctor.test.js +367 -10
  14. package/dist/commands/doctor.test.js.map +1 -1
  15. package/dist/commands/eject.js +7 -0
  16. package/dist/commands/eject.js.map +1 -1
  17. package/dist/commands/eject.test.js +39 -0
  18. package/dist/commands/eject.test.js.map +1 -1
  19. package/dist/commands/info.js +3 -2
  20. package/dist/commands/info.js.map +1 -1
  21. package/dist/commands/init.d.ts +37 -1
  22. package/dist/commands/init.js +169 -40
  23. package/dist/commands/init.js.map +1 -1
  24. package/dist/commands/init.test.js +392 -27
  25. package/dist/commands/init.test.js.map +1 -1
  26. package/dist/commands/install-packages.d.ts +6 -0
  27. package/dist/commands/install-packages.js +20 -0
  28. package/dist/commands/install-packages.js.map +1 -0
  29. package/dist/commands/install-packages.test.d.ts +1 -0
  30. package/dist/commands/install-packages.test.js +55 -0
  31. package/dist/commands/install-packages.test.js.map +1 -0
  32. package/dist/commands/list.js +21 -14
  33. package/dist/commands/list.js.map +1 -1
  34. package/dist/commands/list.test.js +62 -5
  35. package/dist/commands/list.test.js.map +1 -1
  36. package/dist/commands/outdated.js +11 -10
  37. package/dist/commands/outdated.js.map +1 -1
  38. package/dist/commands/outdated.test.js +20 -5
  39. package/dist/commands/outdated.test.js.map +1 -1
  40. package/dist/commands/remove.d.ts +4 -0
  41. package/dist/commands/remove.js +17 -4
  42. package/dist/commands/remove.js.map +1 -1
  43. package/dist/commands/remove.test.js +53 -2
  44. package/dist/commands/remove.test.js.map +1 -1
  45. package/dist/commands/theme.js +1 -1
  46. package/dist/commands/theme.js.map +1 -1
  47. package/dist/commands/update.d.ts +11 -0
  48. package/dist/commands/update.js +174 -58
  49. package/dist/commands/update.js.map +1 -1
  50. package/dist/commands/update.test.js +587 -33
  51. package/dist/commands/update.test.js.map +1 -1
  52. package/dist/index.js +7 -0
  53. package/dist/index.js.map +1 -1
  54. package/dist/lockfile.d.ts +7 -0
  55. package/dist/lockfile.js +36 -0
  56. package/dist/lockfile.js.map +1 -1
  57. package/dist/lockfile.test.d.ts +1 -0
  58. package/dist/lockfile.test.js +25 -0
  59. package/dist/lockfile.test.js.map +1 -0
  60. package/dist/registry.d.ts +14 -0
  61. package/dist/registry.js +78 -24
  62. package/dist/registry.js.map +1 -1
  63. package/dist/registry.test.js +119 -38
  64. package/dist/registry.test.js.map +1 -1
  65. package/dist/test-utils.d.ts +5 -0
  66. package/dist/test-utils.js +6 -1
  67. package/dist/test-utils.js.map +1 -1
  68. package/dist/testing/fire-lynx-event.d.ts +14 -0
  69. package/dist/testing/fire-lynx-event.js +19 -0
  70. package/dist/testing/fire-lynx-event.js.map +1 -0
  71. package/dist/testing/fire-lynx-event.test.d.ts +1 -0
  72. package/dist/testing/fire-lynx-event.test.js +44 -0
  73. package/dist/testing/fire-lynx-event.test.js.map +1 -0
  74. package/dist/testing/lynx-main-thread.d.ts +22 -0
  75. package/dist/testing/lynx-main-thread.js +11 -0
  76. package/dist/testing/lynx-main-thread.js.map +1 -0
  77. package/dist/testing/lynx-main-thread.test.d.ts +1 -0
  78. package/dist/testing/lynx-main-thread.test.js +24 -0
  79. package/dist/testing/lynx-main-thread.test.js.map +1 -0
  80. package/dist/testing/set-input-signal.d.ts +14 -0
  81. package/dist/testing/set-input-signal.js +21 -0
  82. package/dist/testing/set-input-signal.js.map +1 -0
  83. package/dist/testing/set-input-signal.test.d.ts +1 -0
  84. package/dist/testing/set-input-signal.test.js +20 -0
  85. package/dist/testing/set-input-signal.test.js.map +1 -0
  86. package/dist/ui/components/accordion/accordion.test.ts +158 -144
  87. package/dist/ui/components/accordion/accordion.ts +19 -14
  88. package/dist/ui/components/action-sheet/action-sheet.test.ts +283 -151
  89. package/dist/ui/components/action-sheet/action-sheet.ts +77 -43
  90. package/dist/ui/components/alert/alert.test.ts +67 -19
  91. package/dist/ui/components/alert/alert.ts +14 -16
  92. package/dist/ui/components/alert-dialog/alert-dialog.test.ts +208 -94
  93. package/dist/ui/components/alert-dialog/alert-dialog.ts +75 -38
  94. package/dist/ui/components/area-chart/area-chart.test.ts +134 -119
  95. package/dist/ui/components/area-chart/area-chart.ts +100 -315
  96. package/dist/ui/components/area-chart/index.ts +2 -3
  97. package/dist/ui/components/avatar/avatar.test.ts +89 -45
  98. package/dist/ui/components/badge/badge.test.ts +57 -23
  99. package/dist/ui/components/badge/badge.ts +12 -14
  100. package/dist/ui/components/bar-chart/bar-chart.test.ts +172 -45
  101. package/dist/ui/components/bar-chart/bar-chart.ts +140 -294
  102. package/dist/ui/components/bar-chart/index.ts +3 -3
  103. package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +386 -128
  104. package/dist/ui/components/bottom-sheet/bottom-sheet.ts +170 -53
  105. package/dist/ui/components/button/button.test.ts +611 -2
  106. package/dist/ui/components/button/button.ts +434 -49
  107. package/dist/ui/components/button/index.ts +2 -0
  108. package/dist/ui/components/button-group/button-group.test.ts +484 -2
  109. package/dist/ui/components/button-group/button-group.ts +380 -68
  110. package/dist/ui/components/button-group/index.ts +3 -0
  111. package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +183 -58
  112. package/dist/ui/components/candlestick-chart/candlestick-chart.ts +150 -323
  113. package/dist/ui/components/candlestick-chart/index.ts +3 -4
  114. package/dist/ui/components/card/card.test.ts +82 -62
  115. package/dist/ui/components/chart/chart-focus.test.ts +132 -0
  116. package/dist/ui/components/chart/chart-focus.ts +115 -0
  117. package/dist/ui/components/chart/chart-presets.test.ts +335 -0
  118. package/dist/ui/components/chart/chart-presets.ts +278 -0
  119. package/dist/ui/components/chart/chart-scene.test.ts +589 -0
  120. package/dist/ui/components/chart/chart-scene.ts +483 -0
  121. package/dist/ui/components/chart/chart-theme.test.ts +85 -0
  122. package/dist/ui/components/chart/chart-theme.ts +210 -0
  123. package/dist/ui/components/chart/chart-zoom.test.ts +159 -0
  124. package/dist/ui/components/chart/chart-zoom.ts +162 -0
  125. package/dist/ui/components/chart/chart.test.ts +1017 -0
  126. package/dist/ui/components/chart/chart.ts +934 -0
  127. package/dist/ui/components/chart/index.ts +24 -0
  128. package/dist/ui/components/checkbox/checkbox.test.ts +124 -42
  129. package/dist/ui/components/checkbox/checkbox.ts +23 -23
  130. package/dist/ui/components/collapsible/collapsible.test.ts +158 -81
  131. package/dist/ui/components/collapsible/collapsible.ts +19 -14
  132. package/dist/ui/components/context-menu/context-menu.test.ts +224 -0
  133. package/dist/ui/components/context-menu/context-menu.ts +110 -0
  134. package/dist/ui/components/context-menu/index.ts +1 -0
  135. package/dist/ui/components/dialog/dialog.test.ts +213 -108
  136. package/dist/ui/components/dialog/dialog.ts +72 -35
  137. package/dist/ui/components/dock/dock-scroll.test.ts +130 -0
  138. package/dist/ui/components/dock/dock-scroll.ts +133 -0
  139. package/dist/ui/components/dock/dock.test.ts +749 -0
  140. package/dist/ui/components/dock/dock.ts +960 -0
  141. package/dist/ui/components/dock/index.ts +2 -0
  142. package/dist/ui/components/empty-state/empty-state.test.ts +28 -39
  143. package/dist/ui/components/glass/concentric-radius.test.ts +13 -0
  144. package/dist/ui/components/glass/concentric-radius.ts +17 -0
  145. package/dist/ui/components/glass/glass-design.test.ts +78 -0
  146. package/dist/ui/components/glass/glass-design.ts +119 -0
  147. package/dist/ui/components/glass/glass.test.ts +338 -0
  148. package/dist/ui/components/glass/glass.ts +360 -0
  149. package/dist/ui/components/glass/index.ts +18 -0
  150. package/dist/ui/components/icon/icon-theme.test.ts +169 -0
  151. package/dist/ui/components/icon/icon-theme.ts +227 -0
  152. package/dist/ui/components/icon/icon.test.ts +85 -25
  153. package/dist/ui/components/icon/icon.ts +38 -21
  154. package/dist/ui/components/input/input.test.ts +178 -101
  155. package/dist/ui/components/input/input.ts +15 -12
  156. package/dist/ui/components/label/label.test.ts +11 -24
  157. package/dist/ui/components/line-chart/index.ts +2 -3
  158. package/dist/ui/components/line-chart/line-chart.test.ts +149 -56
  159. package/dist/ui/components/line-chart/line-chart.ts +83 -241
  160. package/dist/ui/components/list/list.test.ts +119 -21
  161. package/dist/ui/components/list/list.ts +21 -15
  162. package/dist/ui/components/menu/index.ts +25 -0
  163. package/dist/ui/components/menu/menu-placement.test.ts +153 -0
  164. package/dist/ui/components/menu/menu-placement.ts +159 -0
  165. package/dist/ui/components/menu/menu-popup.test.ts +632 -0
  166. package/dist/ui/components/menu/menu-popup.ts +544 -0
  167. package/dist/ui/components/menu/menu.test.ts +429 -0
  168. package/dist/ui/components/menu/menu.ts +415 -0
  169. package/dist/ui/components/nav-drawer/nav-drawer.test.ts +435 -191
  170. package/dist/ui/components/nav-drawer/nav-drawer.ts +127 -56
  171. package/dist/ui/components/pagination/pagination.test.ts +177 -65
  172. package/dist/ui/components/pagination/pagination.ts +55 -8
  173. package/dist/ui/components/progress/progress.test.ts +102 -46
  174. package/dist/ui/components/progress/progress.ts +52 -54
  175. package/dist/ui/components/radio-group/radio-group.test.ts +128 -83
  176. package/dist/ui/components/radio-group/radio-group.ts +24 -24
  177. package/dist/ui/components/scatter-chart/index.ts +2 -3
  178. package/dist/ui/components/scatter-chart/scatter-chart.test.ts +131 -26
  179. package/dist/ui/components/scatter-chart/scatter-chart.ts +104 -250
  180. package/dist/ui/components/scroll-area/scroll-area.test.ts +95 -24
  181. package/dist/ui/components/scroll-area/scroll-area.ts +8 -8
  182. package/dist/ui/components/select/select.test.ts +138 -78
  183. package/dist/ui/components/select/select.ts +17 -11
  184. package/dist/ui/components/separator/separator.test.ts +18 -17
  185. package/dist/ui/components/sheet/sheet.test.ts +64 -11
  186. package/dist/ui/components/sheet/sheet.ts +9 -1
  187. package/dist/ui/components/skeleton/skeleton.test.ts +24 -7
  188. package/dist/ui/components/skeleton/skeleton.ts +10 -16
  189. package/dist/ui/components/spinner/spinner.test.ts +67 -22
  190. package/dist/ui/components/spinner/spinner.ts +48 -41
  191. package/dist/ui/components/switch/switch.test.ts +116 -46
  192. package/dist/ui/components/switch/switch.ts +8 -13
  193. package/dist/ui/components/tab-bar/index.ts +1 -0
  194. package/dist/ui/components/tab-bar/tab-bar.test.ts +280 -0
  195. package/dist/ui/components/tab-bar/tab-bar.ts +230 -0
  196. package/dist/ui/components/tabs/tabs.test.ts +215 -157
  197. package/dist/ui/components/tabs/tabs.ts +110 -118
  198. package/dist/ui/components/textarea/textarea.test.ts +206 -100
  199. package/dist/ui/components/textarea/textarea.ts +14 -12
  200. package/dist/ui/components/toast/toast.test.ts +436 -315
  201. package/dist/ui/components/toast/toast.ts +62 -55
  202. package/dist/ui/components/toggle/toggle.test.ts +247 -140
  203. package/dist/ui/theme/chart-colors.test.ts +35 -0
  204. package/dist/ui/theme/dark.css +46 -2
  205. package/dist/ui/theme/default.css +63 -7
  206. package/dist/ui/theme/icon-colors.test.ts +35 -0
  207. package/dist/ui/theme/rgb-channels.test.ts +84 -0
  208. package/dist/ui/theme/tailwind-build.test.ts +114 -0
  209. package/dist/ui/theme/tailwind-css.test.ts +83 -0
  210. package/dist/ui/theme/tailwind-plugin.test.ts +42 -2
  211. package/dist/ui/theme/tailwind-plugin.ts +98 -25
  212. package/dist/ui/theme/tailwind.css +113 -0
  213. package/dist/ui/utils/animate.test.ts +13 -1
  214. package/dist/utils/analyze.d.ts +14 -0
  215. package/dist/utils/analyze.js +15 -0
  216. package/dist/utils/analyze.js.map +1 -1
  217. package/dist/utils/analyze.test.js +14 -1
  218. package/dist/utils/analyze.test.js.map +1 -1
  219. package/dist/utils/installed.d.ts +29 -0
  220. package/dist/utils/installed.js +43 -0
  221. package/dist/utils/installed.js.map +1 -0
  222. package/dist/utils/installed.test.d.ts +1 -0
  223. package/dist/utils/installed.test.js +57 -0
  224. package/dist/utils/installed.test.js.map +1 -0
  225. package/dist/utils/interactive.d.ts +9 -0
  226. package/dist/utils/interactive.js +10 -0
  227. package/dist/utils/interactive.js.map +1 -0
  228. package/dist/utils/interactive.test.d.ts +1 -0
  229. package/dist/utils/interactive.test.js +24 -0
  230. package/dist/utils/interactive.test.js.map +1 -0
  231. package/dist/utils/packages.d.ts +36 -0
  232. package/dist/utils/packages.js +70 -0
  233. package/dist/utils/packages.js.map +1 -0
  234. package/dist/utils/packages.test.d.ts +1 -0
  235. package/dist/utils/packages.test.js +89 -0
  236. package/dist/utils/packages.test.js.map +1 -0
  237. package/dist/utils/resolve-paths.d.ts +17 -1
  238. package/dist/utils/resolve-paths.js +21 -3
  239. package/dist/utils/resolve-paths.js.map +1 -1
  240. package/dist/utils/resolve-paths.test.js +38 -2
  241. package/dist/utils/resolve-paths.test.js.map +1 -1
  242. package/dist/utils/tailwind.d.ts +25 -0
  243. package/dist/utils/tailwind.js +51 -0
  244. package/dist/utils/tailwind.js.map +1 -0
  245. package/dist/utils/tailwind.test.d.ts +1 -0
  246. package/dist/utils/tailwind.test.js +71 -0
  247. package/dist/utils/tailwind.test.js.map +1 -0
  248. package/package.json +13 -1
  249. package/dist/ui/components/area-chart/area-chart-render.test.ts +0 -121
  250. package/dist/ui/components/bar-chart/bar-chart-render.test.ts +0 -174
  251. package/dist/ui/components/button/button-render.test.ts +0 -113
  252. package/dist/ui/components/button-group/button-group-render.test.ts +0 -146
  253. package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +0 -174
  254. package/dist/ui/components/cartesian-chart/cartesian-chart-render.test.ts +0 -188
  255. package/dist/ui/components/cartesian-chart/cartesian-chart.test.ts +0 -525
  256. package/dist/ui/components/cartesian-chart/cartesian-chart.ts +0 -1140
  257. package/dist/ui/components/cartesian-chart/index.ts +0 -18
  258. package/dist/ui/components/line-chart/line-chart-render.test.ts +0 -121
  259. package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +0 -157
@@ -1,13 +1,22 @@
1
+ import { NgTemplateOutlet } from '@angular/common';
1
2
  import {
2
3
  type ElementRef,
3
4
  Component,
5
+ Injector,
4
6
  ViewEncapsulation,
5
7
  computed,
8
+ contentChild,
9
+ inject,
6
10
  input,
7
11
  output,
12
+ signal,
8
13
  viewChild,
9
14
  } from '@angular/core';
10
- import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
15
+ import {
16
+ LYNX_ELEMENTS,
17
+ LynxAccessibilitySettings,
18
+ LynxSystemInfo,
19
+ } from '@blotch/angular-lynx';
11
20
  import { cva, type VariantProps } from 'class-variance-authority';
12
21
 
13
22
  import {
@@ -16,8 +25,19 @@ import {
16
25
  pressRelease,
17
26
  } from '../../utils/animate';
18
27
  import { cn } from '../../utils/cn';
28
+ import { UiGlass } from '../glass/glass';
29
+ import {
30
+ injectGlassDesign,
31
+ injectSystemDrawsGlass,
32
+ } from '../glass/glass-design';
33
+ import { UiIcon } from '../icon/icon';
34
+ import { UI_ICON_COLOR } from '../icon/icon-theme';
19
35
  import { UiSpinner } from '../spinner/spinner';
20
36
 
37
+ /**
38
+ * The container classes of the solid variants. The glass variants draw a
39
+ * `<blur-view>`, which classes alone can't, so they aren't here.
40
+ */
21
41
  export const buttonVariants = cva(
22
42
  'items-center rounded-md flex justify-center',
23
43
  {
@@ -34,6 +54,7 @@ export const buttonVariants = cva(
34
54
  sm: 'h-9 rounded-md px-3',
35
55
  lg: 'h-11 rounded-md px-8',
36
56
  icon: 'h-10 w-10',
57
+ 'icon-circle': 'h-10 w-10 rounded-full',
37
58
  },
38
59
  },
39
60
  defaultVariants: {
@@ -43,40 +64,174 @@ export const buttonVariants = cva(
43
64
  },
44
65
  );
45
66
 
46
- const buttonTextVariants = cva('font-medium', {
67
+ /** A variant drawn with solid colors, on every platform. */
68
+ export type SolidButtonVariant = NonNullable<
69
+ VariantProps<typeof buttonVariants>['variant']
70
+ >;
71
+
72
+ /**
73
+ * Liquid Glass variants, SwiftUI's button styles: `glass` (`.glass`),
74
+ * `glass-prominent` (`.glassProminent`, tinted for the screen's main action)
75
+ * and `glass-clear` (`.glass(.clear)`, for buttons over photos and video).
76
+ */
77
+ export type GlassButtonVariant = 'glass' | 'glass-prominent' | 'glass-clear';
78
+
79
+ export type ButtonVariant = SolidButtonVariant | GlassButtonVariant;
80
+ export type ButtonSize = NonNullable<
81
+ VariantProps<typeof buttonVariants>['size']
82
+ >;
83
+
84
+ const GLASS_VARIANTS: ReadonlySet<ButtonVariant> = new Set<ButtonVariant>([
85
+ 'glass',
86
+ 'glass-prominent',
87
+ 'glass-clear',
88
+ ]);
89
+
90
+ /**
91
+ * Whether a variant is drawn with glass, narrowing it for the maps below.
92
+ */
93
+ const isGlassVariant = (
94
+ variant: ButtonVariant,
95
+ ): variant is GlassButtonVariant => GLASS_VARIANTS.has(variant);
96
+
97
+ /** The sizes for a single icon, which then needs no label. */
98
+ const ICON_SIZES: ReadonlySet<ButtonSize> = new Set<ButtonSize>([
99
+ 'icon',
100
+ 'icon-circle',
101
+ ]);
102
+
103
+ const LABEL_SIZES: Readonly<Record<ButtonSize, string>> = {
104
+ default: 'text-sm',
105
+ sm: 'text-xs',
106
+ lg: 'text-base',
107
+ icon: 'text-sm',
108
+ 'icon-circle': 'text-sm',
109
+ };
110
+
111
+ const SOLID_LABEL_COLORS: Readonly<Record<SolidButtonVariant, string>> = {
112
+ default: 'text-primary-foreground',
113
+ destructive: 'text-destructive-foreground',
114
+ outline: 'text-foreground',
115
+ secondary: 'text-secondary-foreground',
116
+ ghost: 'text-foreground',
117
+ };
118
+
119
+ /**
120
+ * The token of each solid variant's label color above, for what draws with
121
+ * SVG: the spinner and projected icons can't read the text classes, so they
122
+ * get the token and resolve it themselves (see icon-theme.ts).
123
+ */
124
+ const SOLID_FOREGROUNDS: Readonly<Record<SolidButtonVariant, string>> = {
125
+ default: 'var(--primary-foreground)',
126
+ destructive: 'var(--destructive-foreground)',
127
+ outline: 'var(--foreground)',
128
+ secondary: 'var(--secondary-foreground)',
129
+ ghost: 'var(--foreground)',
130
+ };
131
+
132
+ /**
133
+ * A glass button's box, which its glass fills. Glass buttons are capsules,
134
+ * as SwiftUI draws them, so an icon size is a circle.
135
+ */
136
+ const glassFrameVariants = cva('flex-col rounded-full flex', {
47
137
  variants: {
48
- variant: {
49
- default: 'text-primary-foreground',
50
- destructive: 'text-destructive-foreground',
51
- outline: 'text-foreground',
52
- secondary: 'text-secondary-foreground',
53
- ghost: 'text-foreground',
54
- },
55
138
  size: {
56
- default: 'text-sm',
57
- sm: 'text-xs',
58
- lg: 'text-base',
59
- icon: 'text-sm',
139
+ default: 'h-10',
140
+ sm: 'h-9',
141
+ lg: 'h-11',
142
+ icon: 'h-10 w-10',
143
+ 'icon-circle': 'h-10 w-10',
60
144
  },
61
145
  },
62
- defaultVariants: {
63
- variant: 'default',
64
- size: 'default',
65
- },
66
146
  });
67
147
 
68
- export type ButtonVariant = NonNullable<
69
- VariantProps<typeof buttonVariants>['variant']
70
- >;
71
- export type ButtonSize = NonNullable<
72
- VariantProps<typeof buttonVariants>['size']
73
- >;
148
+ const GLASS_PADDING: Readonly<Record<ButtonSize, string>> = {
149
+ default: 'px-4',
150
+ sm: 'px-3',
151
+ lg: 'px-8',
152
+ icon: '',
153
+ 'icon-circle': '',
154
+ };
155
+
156
+ /**
157
+ * The glass variants as Material 3 Expressive buttons, Android's default
158
+ * (see GlassDesign): `glass` and `glass-clear` are filled tonal buttons,
159
+ * `glass-prominent` a filled one in the glass tint, and the icon sizes
160
+ * Material's round icon buttons. Material's buttons sit flat on the page,
161
+ * with no shadow, and are fully rounded.
162
+ */
163
+ const materialButtonVariants = cva(
164
+ 'flex-row items-center rounded-full flex relative justify-center overflow-hidden',
165
+ {
166
+ variants: {
167
+ variant: {
168
+ glass: 'bg-secondary',
169
+ 'glass-clear': 'bg-secondary',
170
+ 'glass-prominent': 'bg-glass-tint',
171
+ },
172
+ size: {
173
+ default: 'h-10 px-4',
174
+ sm: 'h-9 px-3',
175
+ lg: 'h-11 px-8',
176
+ icon: 'h-10 w-10',
177
+ 'icon-circle': 'h-10 w-10',
178
+ },
179
+ },
180
+ },
181
+ );
182
+
183
+ /**
184
+ * How a glass variant looks on this platform: `'glass'` draws it on a
185
+ * `<blur-view ui-glass>` (Liquid Glass, or the frosted material before iOS
186
+ * 26), `'material'` as a Material button. The solid variants are `'solid'`
187
+ * everywhere.
188
+ */
189
+ type ButtonLook = 'solid' | 'glass' | 'material';
190
+
191
+ /**
192
+ * How the button answers a finger: it shrinks (`'scale'`), dims at once
193
+ * (`'dim'`, under Reduce Motion), shows Material's pressed state layer
194
+ * (`'layer'`), or leaves it to interactive glass, which reacts by itself
195
+ * (`'none'`).
196
+ */
197
+ type PressFeedback = 'scale' | 'dim' | 'layer' | 'none';
198
+
199
+ /**
200
+ * `UiIcon`, for `contentChild()`. A call rather than the class itself: for a
201
+ * class, Angular's test build keeps an arrow function that nothing calls,
202
+ * which an app's coverage report would show as uncovered.
203
+ */
204
+ const iconType = () => UiIcon;
205
+
206
+ /**
207
+ * The platform, as SystemInfo names it, or undefined where there's no
208
+ * SystemInfo to read, such as in unit tests, where LynxSystemInfo throws.
209
+ */
210
+ const platformOf = (injector: Injector): string | undefined => {
211
+ try {
212
+ return injector.get(LynxSystemInfo).platform;
213
+ } catch {
214
+ return undefined;
215
+ }
216
+ };
217
+
218
+ /**
219
+ * The color an icon projected into the button draws with when it's given
220
+ * none: its label's (see UI_ICON_COLOR). A named function, not an inline
221
+ * factory: the component's metadata keeps its own copy of an inline one,
222
+ * which nothing calls, so an app's coverage report would show it uncovered.
223
+ */
224
+ const buttonIconColor = () => inject(UiButton).foreground;
74
225
 
75
226
  @Component({
76
227
  selector: 'ui-button',
77
- standalone: true,
78
- imports: [LYNX_ELEMENTS, UiSpinner],
228
+ imports: [LYNX_ELEMENTS, NgTemplateOutlet, UiGlass, UiSpinner],
79
229
  encapsulation: ViewEncapsulation.None,
230
+ // Icons projected into the button inject this through its host's element
231
+ // injector, which is where Angular looks for projected content: the glass
232
+ // inside the template provides one too, but only for the template's own
233
+ // nodes.
234
+ providers: [{ provide: UI_ICON_COLOR, useFactory: buttonIconColor }],
80
235
  template: `
81
236
  <!-- Press feedback binds BOTH touch and mouse events on purpose. Native
82
237
  (iOS/Android) only ever fires touch; Lynx web runs in the browser, where
@@ -87,10 +242,17 @@ export type ButtonSize = NonNullable<
87
242
  touch devices, so there's no double-fire. The scale animation itself lives
88
243
  in pressDown/pressRelease — see their JSDoc for why it's a CSS transition
89
244
  (which serializes to the DOM on Lynx web) and not el.animate() (whose Web
90
- Animation runs on web-core's offscreen node and never reaches the screen). -->
245
+ Animation runs on web-core's offscreen node and never reaches the screen).
246
+
247
+ An icon-only button has no text for a screen reader to read, so with
248
+ an accessibility-label the whole button becomes one element that says
249
+ it. Without one, the label's own text is read, as a button. -->
91
250
  <view
92
251
  #container
93
252
  [class]="containerClass()"
253
+ [attr.accessibility-element]="accessibilityLabel() ? 'true' : null"
254
+ [attr.accessibility-traits]="accessibilityLabel() ? 'button' : null"
255
+ [attr.accessibility-label]="accessibilityLabel() || null"
94
256
  (bindtouchstart)="onPressStart()"
95
257
  (bindtouchend)="onPressEnd()"
96
258
  (bindtouchcancel)="onPressCancel()"
@@ -99,72 +261,295 @@ export type ButtonSize = NonNullable<
99
261
  (bindmouseleave)="onPressCancel()"
100
262
  (bindtap)="onTap()"
101
263
  >
102
- @if (loading()) {
103
- <ui-spinner [size]="spinnerSize()" />
264
+ @if (look() === 'glass') {
265
+ <!-- The container is the button's box, and the glass fills it. Under
266
+ clear glass the container is the dimming layer (see dimmed).
267
+ The content sits in the glass, where iOS's interactive glass
268
+ reacts to touches on it, and is what dims when disabled, as a
269
+ disabled SwiftUI glass button dims its label: glass under a
270
+ faded parent turns into a washed-out patch on iOS. -->
271
+ <blur-view
272
+ ui-glass
273
+ [variant]="variant() === 'glass-clear' ? 'clear' : 'regular'"
274
+ [tint]="
275
+ variant() === 'glass-prominent' ? 'var(--glass-tint)' : undefined
276
+ "
277
+ [interactive]="interactive()"
278
+ [class]="glassClass()"
279
+ >
280
+ <view [class]="glassContentClass()">
281
+ <ng-container [ngTemplateOutlet]="content" />
282
+ </view>
283
+ </blur-view>
104
284
  } @else {
105
- <text [class]="labelClass()"><ng-content /></text>
285
+ @if (stateLayerClass(); as layer) {
286
+ <view [class]="layer"></view>
287
+ }
288
+ <ng-container [ngTemplateOutlet]="content" />
106
289
  }
107
290
  </view>
291
+
292
+ <!-- One copy of the content, for whichever form the button takes: Angular
293
+ projects content once, so it can't be in both branches above. -->
294
+ <ng-template #content>
295
+ @if (loading()) {
296
+ <ui-spinner [size]="spinnerSize()" [color]="foreground()" />
297
+ } @else {
298
+ <ng-content select="ui-icon" />
299
+ @if (showLabel()) {
300
+ <text
301
+ [attr.accessibility-traits]="accessibilityLabel() ? null : 'button'"
302
+ [class]="labelClass()"
303
+ ><ng-content
304
+ /></text>
305
+ }
306
+ }
307
+ </ng-template>
108
308
  `,
109
309
  })
110
310
  export class UiButton {
311
+ /**
312
+ * The look: a solid variant, or a glass one (`'glass'`,
313
+ * `'glass-prominent'`, `'glass-clear'`; see GlassButtonVariant). Keep glass
314
+ * for buttons that float over the content.
315
+ */
111
316
  readonly variant = input<ButtonVariant>('default');
317
+ /**
318
+ * The height and padding. `'icon'` and `'icon-circle'` fit one projected
319
+ * `<ui-icon>`: a rounded square and a circle (glass buttons are circles in
320
+ * either).
321
+ */
112
322
  readonly size = input<ButtonSize>('default');
113
323
  readonly disabled = input(false);
114
324
  readonly loading = input(false);
325
+ /**
326
+ * What a screen reader says for the button. An icon-only button needs one;
327
+ * a button with a label reads its label without it.
328
+ */
329
+ readonly accessibilityLabel = input<string | undefined>(undefined, {
330
+ alias: 'accessibility-label',
331
+ });
115
332
  readonly userClass = input<string>('', { alias: 'class' });
116
333
 
117
334
  readonly pressed = output<void>();
118
335
 
119
336
  readonly containerRef = viewChild<ElementRef>('container');
337
+ /** A `<ui-icon>` projected into the button, which sits before the label. */
338
+ readonly projectedIcon = contentChild(iconType());
339
+
340
+ readonly #settings = inject(LynxAccessibilitySettings);
341
+ readonly #design = injectGlassDesign();
342
+ readonly #systemDrawsGlass = injectSystemDrawsGlass();
343
+ readonly #platform = platformOf(inject(Injector));
120
344
  #pressAnim?: AnimationHandle;
121
345
 
122
- protected readonly containerClass = computed(() =>
123
- cn(
124
- buttonVariants({ variant: this.variant(), size: this.size() }),
125
- (this.disabled() || this.loading()) && 'opacity-50',
346
+ /** A finger (or the mouse) is down on the button. */
347
+ protected readonly pressing = signal(false);
348
+
349
+ readonly #inactive = computed(() => this.disabled() || this.loading());
350
+
351
+ protected readonly look = computed<ButtonLook>(() => {
352
+ if (!isGlassVariant(this.variant())) return 'solid';
353
+ return this.#design() === 'material' ? 'material' : 'glass';
354
+ });
355
+
356
+ /**
357
+ * The glass reacts to touch while the button can be pressed. Where the
358
+ * runtime draws it (the web preview), that's motion Reduce Motion asks to
359
+ * leave out; iOS tones its own glass down under the setting.
360
+ */
361
+ protected readonly interactive = computed(
362
+ () =>
363
+ !this.#inactive() &&
364
+ (this.#systemDrawsGlass || !this.#settings.reduceMotion()),
365
+ );
366
+
367
+ /**
368
+ * For the glass look: what's behind shows through the glass, which is
369
+ * drawn as glass or a frosted material, not the opaque surface it turns
370
+ * into under Reduce Transparency where the runtime draws it (as
371
+ * UiGlass.surface decides).
372
+ */
373
+ readonly #seeThrough = computed(
374
+ () => this.#systemDrawsGlass || !this.#settings.reduceTransparency(),
375
+ );
376
+
377
+ /**
378
+ * Interactive glass that answers a press itself, swelling under the
379
+ * finger: UIKit's on iOS 26 and later, and the runtime's in the web
380
+ * preview. Android's blur-view has no interactive glass, and the frosted
381
+ * materials before iOS 26 don't react.
382
+ */
383
+ readonly #glassReacts = computed(
384
+ () =>
385
+ this.#design() === 'glass' &&
386
+ this.#seeThrough() &&
387
+ this.interactive() &&
388
+ this.#platform !== 'Android',
389
+ );
390
+
391
+ /**
392
+ * See PressFeedback. Shrinking on top of interactive glass would fight its
393
+ * swell, Material's buttons show a state layer, and under Reduce Motion
394
+ * the button dims rather than moving.
395
+ */
396
+ readonly #feedback = computed<PressFeedback>(() => {
397
+ const look = this.look();
398
+ if (look === 'material') return 'layer';
399
+ if (look === 'glass' && this.#glassReacts()) return 'none';
400
+ return this.#settings.reduceMotion() ? 'dim' : 'scale';
401
+ });
402
+
403
+ readonly #dimmedByPress = computed(
404
+ () => this.pressing() && this.#feedback() === 'dim',
405
+ );
406
+
407
+ /**
408
+ * Clear glass sits on a dark layer at about a third, so its white label
409
+ * stays legible over bright photos and video: Apple's rule for clear
410
+ * glass. The layer is the container, under the glass.
411
+ */
412
+ readonly #dimmed = computed(
413
+ () => this.variant() === 'glass-clear' && this.#seeThrough(),
414
+ );
415
+
416
+ protected readonly showLabel = computed(
417
+ () => !(this.projectedIcon() && ICON_SIZES.has(this.size())),
418
+ );
419
+
420
+ /** Space between an icon and the label beside it. */
421
+ readonly #gap = computed(
422
+ () => !!this.projectedIcon() && this.showLabel() && 'gap-2',
423
+ );
424
+
425
+ /**
426
+ * The color of the label, the spinner and projected icons, as a `var()`.
427
+ * On clear glass it's the clear foreground, which stays white over media.
428
+ */
429
+ readonly foreground = computed(() => {
430
+ const variant = this.variant();
431
+ if (!isGlassVariant(variant)) return SOLID_FOREGROUNDS[variant];
432
+ if (variant === 'glass-prominent') return 'var(--glass-tint-foreground)';
433
+ if (this.look() === 'material') return 'var(--secondary-foreground)';
434
+ return this.#dimmed()
435
+ ? 'var(--glass-clear-foreground)'
436
+ : 'var(--glass-foreground)';
437
+ });
438
+
439
+ protected readonly containerClass = computed(() => {
440
+ const variant = this.variant();
441
+ const size = this.size();
442
+ const inactive = this.#inactive();
443
+ if (!isGlassVariant(variant)) {
444
+ return cn(
445
+ buttonVariants({ variant, size }),
446
+ this.#gap(),
447
+ this.#dimmedByPress() && 'opacity-70',
448
+ inactive && 'opacity-50',
449
+ this.userClass(),
450
+ );
451
+ }
452
+ if (this.look() === 'material') {
453
+ return cn(
454
+ materialButtonVariants({ variant, size }),
455
+ this.#gap(),
456
+ // A tonal button's edge barely shows on the page, so Increase
457
+ // Contrast outlines it, as glass gets an edge there.
458
+ variant !== 'glass-prominent' &&
459
+ this.#settings.increaseContrast() &&
460
+ 'border border-foreground',
461
+ inactive && 'opacity-50',
462
+ this.userClass(),
463
+ );
464
+ }
465
+ return cn(
466
+ glassFrameVariants({ size }),
467
+ this.#dimmed() && 'bg-glass-dim',
126
468
  this.userClass(),
469
+ );
470
+ });
471
+
472
+ protected readonly glassClass = computed(() =>
473
+ cn(
474
+ 'flex-1 flex-row items-center flex justify-center',
475
+ GLASS_PADDING[this.size()],
476
+ ),
477
+ );
478
+
479
+ protected readonly glassContentClass = computed(() =>
480
+ cn(
481
+ 'flex-row items-center flex justify-center',
482
+ this.#gap(),
483
+ this.#dimmedByPress() && 'opacity-70',
484
+ this.#inactive() && 'opacity-50',
127
485
  ),
128
486
  );
129
487
 
130
- protected readonly labelClass = computed(() =>
131
- cn(buttonTextVariants({ variant: this.variant(), size: this.size() })),
488
+ /**
489
+ * Material's pressed state: the label's color over the container at 10%,
490
+ * under the content. It appears with the press and goes with the release,
491
+ * with no motion.
492
+ */
493
+ protected readonly stateLayerClass = computed(
494
+ () =>
495
+ this.pressing() &&
496
+ this.#feedback() === 'layer' &&
497
+ cn(
498
+ 'h-full w-full opacity-10 absolute left-0 top-0',
499
+ this.variant() === 'glass-prominent'
500
+ ? 'bg-glass-tint-foreground'
501
+ : 'bg-secondary-foreground',
502
+ ),
132
503
  );
133
504
 
505
+ protected readonly labelClass = computed(() => {
506
+ const variant = this.variant();
507
+ let color: string;
508
+ if (!isGlassVariant(variant)) color = SOLID_LABEL_COLORS[variant];
509
+ else if (variant === 'glass-prominent') {
510
+ color = 'text-glass-tint-foreground';
511
+ } else if (this.look() === 'material') color = 'text-secondary-foreground';
512
+ // The glass points this at the clear foreground on clear glass.
513
+ else color = 'text-glass-foreground';
514
+ return cn('font-medium', LABEL_SIZES[this.size()], color);
515
+ });
516
+
134
517
  protected readonly spinnerSize = computed(() => {
135
518
  const s = this.size();
136
519
  return s === 'sm' ? ('xs' as const) : ('sm' as const);
137
520
  });
138
521
 
139
522
  protected onPressStart(): void {
140
- if (this.disabled() || this.loading()) return;
523
+ if (this.#inactive()) return;
524
+ this.pressing.set(true);
525
+ if (this.#feedback() !== 'scale') return;
141
526
  // pressDown/pressRelease drive el.setStyle() based CSS transitions on the
142
- // container ref. In the JIT test harness the view-child isn't reliably
143
- // wired up (see the label spec note), and Lynx web renderer's press
144
- // feedback is verified end-to-end on-device.
145
- /* v8 ignore next 2 */
527
+ // container ref (see pressDown for why it isn't el.animate()).
146
528
  this.#pressAnim?.cancel();
147
529
  this.#pressAnim = pressDown(this.containerRef()?.nativeElement);
148
530
  }
149
531
 
150
532
  protected onPressEnd(): void {
151
- if (this.disabled() || this.loading()) return;
152
- /* v8 ignore next 2 */
153
- this.#pressAnim?.cancel();
154
- this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
533
+ if (this.#inactive()) return;
534
+ this.#release();
155
535
  }
156
536
 
157
537
  protected onPressCancel(): void {
158
- // Also restore scale on cancel — without this, a canceled touch (e.g. scroll
538
+ // Also release on cancel — without this, a canceled touch (e.g. scroll
159
539
  // gesture taking over) leaves the button stuck in its pressed-down state.
160
- /* v8 ignore next 2 */
161
- this.#pressAnim?.cancel();
162
- this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
540
+ this.#release();
163
541
  }
164
542
 
165
543
  protected onTap(): void {
166
- if (!this.disabled() && !this.loading()) {
544
+ if (!this.#inactive()) {
167
545
  this.pressed.emit();
168
546
  }
169
547
  }
548
+
549
+ #release(): void {
550
+ this.pressing.set(false);
551
+ if (this.#feedback() !== 'scale') return;
552
+ this.#pressAnim?.cancel();
553
+ this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
554
+ }
170
555
  }
@@ -3,4 +3,6 @@ export {
3
3
  buttonVariants,
4
4
  type ButtonVariant,
5
5
  type ButtonSize,
6
+ type GlassButtonVariant,
7
+ type SolidButtonVariant,
6
8
  } from './button';