@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
@@ -1,7 +1,9 @@
1
+ import { NgTemplateOutlet } from '@angular/common';
1
2
  import {
2
- type ElementRef,
3
3
  type OnDestroy,
4
4
  Component,
5
+ Directive,
6
+ ElementRef,
5
7
  ViewEncapsulation,
6
8
  computed,
7
9
  effect,
@@ -27,19 +29,62 @@ import {
27
29
  slideOut,
28
30
  } from '../../utils/animate';
29
31
  import { cn } from '../../utils/cn';
30
-
32
+ import { UiGlass } from '../glass/glass';
33
+ import { injectGlassDesign } from '../glass/glass-design';
34
+ import { type PopoverAnchor, UiPopoverPanel } from '../popover/popover-panel';
35
+
36
+ /** How many action sheets there have been, for their glass ids. */
37
+ let sheetCount = 0;
38
+
39
+ /**
40
+ * The width of an action sheet opened from a control: iOS's, and Material's
41
+ * widest menu.
42
+ */
43
+ const ANCHORED_WIDTH = { glass: 270, material: 280 } as const;
44
+
45
+ /**
46
+ * An action sheet: a short list of choices about an action the user took,
47
+ * such as saving or deleting a draft.
48
+ *
49
+ * ```html
50
+ * <ui-button (pressed)="open.set(true)">Share</ui-button>
51
+ * <ui-action-sheet [(open)]="open">
52
+ * <ui-action-sheet-title>Share this document</ui-action-sheet-title>
53
+ * <ui-action-sheet-item (pressed)="copyLink()">Copy link</ui-action-sheet-item>
54
+ * <ui-action-sheet-item variant="destructive" (pressed)="remove()">Delete</ui-action-sheet-item>
55
+ * <ui-action-sheet-cancel />
56
+ * </ui-action-sheet>
57
+ * ```
58
+ *
59
+ * By default it slides up from the bottom of the screen over a dimmed
60
+ * backdrop, with Cancel in a tile of its own below the actions, and takes
61
+ * the screen until it closes.
62
+ *
63
+ * Mark the control that presents it with `[uiActionSheetAnchor]="sheet"`
64
+ * to open it from that control instead, as iOS 26 does: there it's Liquid
65
+ * Glass that grows out of the control, and on Android a Material 3 menu
66
+ * that unfolds beside it. Nothing dims, Cancel gives way to a touch outside,
67
+ * and that touch still reaches what it lands on. Before iOS 26, iPhone
68
+ * action sheets always rose from the bottom, so it does there too.
69
+ */
31
70
  @Component({
32
71
  selector: 'ui-action-sheet',
33
- imports: [LYNX_ELEMENTS],
72
+ imports: [LYNX_ELEMENTS, NgTemplateOutlet, UiPopoverPanel],
34
73
  encapsulation: ViewEncapsulation.None,
35
74
  template: `
36
- <overlay [attr.visible]="overlayVisible()" [style]="overlayStyle()">
75
+ <!-- Android's back button asks the overlay to close (requestclose),
76
+ which closes the sheet as Cancel does. -->
77
+ <overlay
78
+ [attr.visible]="overlayVisible()"
79
+ [style]="overlayStyle()"
80
+ (bindrequestclose)="close()"
81
+ >
37
82
  <!-- bg-black/50 dims the content behind the sheet. The fadeIn/fadeOut in
38
83
  #animateIn/#animateOut already tween this element's opacity over
39
84
  DURATION.normal, so the dim animates in lockstep with the panel slide —
40
- matching the dialog/bottom-sheet backdrop. A named color with an opacity
41
- modifier is required here: bg-black/50 compiles to rgb(0 0 0 / 0.5),
42
- which Lynx accepts, whereas opacity modifiers on semantic tokens don't. -->
85
+ matching the dialog/bottom-sheet backdrop. It's a named color, not a
86
+ theme one, because the dim is black in light and dark mode alike;
87
+ bg-black/50 compiles to rgb(0 0 0 / 0.5), which Lynx accepts. -->
43
88
  <view
44
89
  #backdrop
45
90
  class="h-full w-full bg-black/50"
@@ -62,7 +107,9 @@ import { cn } from '../../utils/cn';
62
107
  class="flex-col rounded-lg border border-border bg-card flex overflow-hidden"
63
108
  style="box-shadow: 0 2px 16px rgba(0, 0, 0, 0.15);"
64
109
  >
65
- <ng-content />
110
+ @if (!anchored()) {
111
+ <ng-container [ngTemplateOutlet]="actions" />
112
+ }
66
113
  </view>
67
114
  <!-- Cancel button is slotted separately so it renders outside the
68
115
  rounded card — matching the iOS action sheet visual convention
@@ -71,6 +118,28 @@ import { cn } from '../../utils/cn';
71
118
  </view>
72
119
  </view>
73
120
  </overlay>
121
+ @if (anchorElement(); as anchor) {
122
+ <!-- Opened from its control: a popover's panel, which leaves the rest
123
+ of the screen as it was. A touch outside it is the cancel, as
124
+ UIKit drops an action sheet's Cancel when it shows it as a
125
+ popover, so the Cancel tile above stays hidden. -->
126
+ <ui-popover-panel
127
+ #anchoredPanel
128
+ [open]="open()"
129
+ [anchor]="anchor"
130
+ [width]="anchoredWidth()"
131
+ [glassId]="glassId"
132
+ (dismissed)="close()"
133
+ (closed)="closed.emit()"
134
+ >
135
+ <view [class]="anchoredClass()">
136
+ <ng-container [ngTemplateOutlet]="actions" />
137
+ </view>
138
+ </ui-popover-panel>
139
+ }
140
+ <!-- One copy of the title and items, for whichever form the sheet takes:
141
+ Angular projects content once, so it can't be in both. -->
142
+ <ng-template #actions><ng-content /></ng-template>
74
143
  `,
75
144
  })
76
145
  export class UiActionSheet implements OnDestroy {
@@ -79,6 +148,57 @@ export class UiActionSheet implements OnDestroy {
79
148
 
80
149
  readonly closed = output<void>();
81
150
 
151
+ readonly design = injectGlassDesign();
152
+ readonly #anchor = signal<PopoverAnchor | null>(null);
153
+
154
+ /**
155
+ * The id the anchored sheet's glass and a glass control share (see
156
+ * UiGlass's `glassId`), unique to the sheet, so it takes over the
157
+ * control's glass as it grows out of it.
158
+ */
159
+ readonly glassId = `ui-action-sheet-${++sheetCount}`;
160
+
161
+ /**
162
+ * The control the sheet opens from, on a design that opens it there: iOS
163
+ * 26's glass and Android's Material. iPhone action sheets rose from the
164
+ * bottom edge before iOS 26 whatever presented them, so `frosted` keeps
165
+ * the bottom sheet.
166
+ */
167
+ protected readonly anchorElement = computed(() =>
168
+ this.design() === 'frosted' ? null : this.#anchor(),
169
+ );
170
+
171
+ /** Whether the sheet opens from its control rather than from the bottom. */
172
+ readonly anchored = computed(() => this.anchorElement() !== null);
173
+
174
+ // Optional: only there while the sheet is anchored.
175
+ readonly anchoredPanel = viewChild<UiPopoverPanel>('anchoredPanel');
176
+
177
+ /**
178
+ * Whether the sheet covers its control now: on iOS 26 it grows out of the
179
+ * control and shrinks back into it, which hides meanwhile.
180
+ */
181
+ readonly coversAnchor = computed(
182
+ () => this.anchoredPanel()?.coversAnchor() ?? false,
183
+ );
184
+
185
+ /**
186
+ * How the sheet's title and items draw: as today's bottom sheet, as
187
+ * iOS 26's glass popover, or as a Material 3 menu.
188
+ */
189
+ readonly look = computed<'sheet' | 'glass' | 'material'>(() => {
190
+ if (!this.anchored()) return 'sheet';
191
+ return this.design() === 'material' ? 'material' : 'glass';
192
+ });
193
+
194
+ protected readonly anchoredWidth = computed(() =>
195
+ this.look() === 'material' ? ANCHORED_WIDTH.material : ANCHORED_WIDTH.glass,
196
+ );
197
+
198
+ protected readonly anchoredClass = computed(() =>
199
+ cn('flex-col flex', this.look() === 'material' ? 'py-2' : 'py-1.5'),
200
+ );
201
+
82
202
  protected readonly overlayVisible = signal(false);
83
203
  /**
84
204
  * `width`/`height`/`top`/`left` are web-only necessities: on web
@@ -92,30 +212,39 @@ export class UiActionSheet implements OnDestroy {
92
212
  : 'position: fixed; overflow: visible; width: 100vw; height: 100vh; top: 0; left: 0; z-index: 999; display: none;',
93
213
  );
94
214
 
95
- readonly backdropRef = viewChild<ElementRef>('backdrop');
96
- readonly panelRef = viewChild<ElementRef>('panel');
215
+ // Required: both are always in the template (the overlay is hidden, never
216
+ // removed), and nothing reads them before the first render.
217
+ readonly backdropRef = viewChild.required<ElementRef>('backdrop');
218
+ readonly panelRef = viewChild.required<ElementRef>('panel');
97
219
  #backdropAnim?: AnimationHandle;
98
220
  #panelAnim?: AnimationHandle;
99
- #hasBeenOpen = false;
221
+ #sheetOpen = false;
100
222
  // The open and close sequences' pending timers, so ngOnDestroy can stop
101
223
  // them.
102
224
  readonly #timers = new Set<ReturnType<typeof setTimeout>>();
103
225
 
104
- constructor() {
105
- // `#hasBeenOpen` prevents the close animation from running on the initial
106
- // effect evaluation when `open` starts as false. Without this guard, the
107
- // first run would call #doClose() and emit `closed` before the action
108
- // sheet has ever been opened.
109
- effect(() => {
110
- const isOpen = this.open();
111
- if (isOpen) {
112
- this.#hasBeenOpen = true;
113
- this.#doOpen();
114
- } else if (this.#hasBeenOpen) {
115
- this.#doClose();
116
- }
117
- });
118
- }
226
+ /**
227
+ * Opens or closes the action sheet as `open` changes. A field rather than a
228
+ * constructor: Angular's test build keeps a function for a constructor that
229
+ * nothing calls, which an app's coverage report would show as uncovered.
230
+ *
231
+ * `#sheetOpen` says the bottom sheet is open or opening, so only a sheet
232
+ * that opened closes. Without it, the first run, with `open` false, would
233
+ * call #doClose() and emit `closed` before the action sheet ever opened,
234
+ * and so would an anchor arriving or leaving while it's closed.
235
+ */
236
+ protected readonly openOrClose = effect(() => {
237
+ // An anchored sheet opens in its popover panel instead, which follows
238
+ // `open` itself.
239
+ const isOpen = this.open() && !this.anchored();
240
+ if (isOpen) {
241
+ this.#sheetOpen = true;
242
+ this.#doOpen();
243
+ } else if (this.#sheetOpen) {
244
+ this.#sheetOpen = false;
245
+ this.#doClose();
246
+ }
247
+ });
119
248
 
120
249
  protected readonly panelClass = computed(() =>
121
250
  cn('w-full flex-col gap-2 px-4 pb-6 flex', this.userClass()),
@@ -133,6 +262,21 @@ export class UiActionSheet implements OnDestroy {
133
262
  this.open.set(false);
134
263
  }
135
264
 
265
+ /**
266
+ * @internal Called by the `uiActionSheetAnchor` directive.
267
+ */
268
+ setAnchor(anchor: PopoverAnchor | null): void {
269
+ this.#anchor.set(anchor);
270
+ }
271
+
272
+ /**
273
+ * @internal A touch began on the anchor, which opens the sheet rather than
274
+ * landing outside it (see UiPopoverPanel.touchedAnchor).
275
+ */
276
+ touchedAnchor(): void {
277
+ this.anchoredPanel()?.touchedAnchor();
278
+ }
279
+
136
280
  /**
137
281
  * No-op tap handler for the panel. `catchtap` (vs `bindtap`) already stops
138
282
  * the tap from bubbling to the backdrop — Lynx controls propagation via the
@@ -190,9 +334,8 @@ export class UiActionSheet implements OnDestroy {
190
334
  }
191
335
 
192
336
  #animateIn(): void {
193
- const backdrop = this.backdropRef()?.nativeElement;
194
- const panel = this.panelRef()?.nativeElement;
195
- if (!backdrop || !panel) return;
337
+ const backdrop = this.backdropRef().nativeElement;
338
+ const panel = this.panelRef().nativeElement;
196
339
 
197
340
  this.#backdropAnim?.cancel();
198
341
  this.#panelAnim?.cancel();
@@ -210,9 +353,8 @@ export class UiActionSheet implements OnDestroy {
210
353
  }
211
354
 
212
355
  #animateOut(): void {
213
- const backdrop = this.backdropRef()?.nativeElement;
214
- const panel = this.panelRef()?.nativeElement;
215
- if (!backdrop || !panel) return;
356
+ const backdrop = this.backdropRef().nativeElement;
357
+ const panel = this.panelRef().nativeElement;
216
358
 
217
359
  this.#backdropAnim?.cancel();
218
360
  this.#panelAnim?.cancel();
@@ -225,28 +367,73 @@ export class UiActionSheet implements OnDestroy {
225
367
  }
226
368
  }
227
369
 
370
+ /** How an action sheet's title and items draw; see UiActionSheet.look. */
371
+ type ActionSheetLook = 'sheet' | 'glass' | 'material';
372
+
373
+ /**
374
+ * The look of the sheet a title or item is in. Today's bottom sheet when
375
+ * it's in none.
376
+ */
377
+ const lookOf = (sheet: UiActionSheet | null) =>
378
+ computed<ActionSheetLook>(() => sheet?.look() ?? 'sheet');
379
+
380
+ // The bottom sheet's tiles; iOS 26's popover, whose rows are 44pt tall with
381
+ // the text in the middle, as UIKit's action sheets keep it in every form;
382
+ // and Material 3's menu, with 48dp rows and the text at the start.
383
+ const TITLE_CONTAINER: Readonly<Record<ActionSheetLook, string>> = {
384
+ sheet: 'items-center px-4 py-3 flex justify-center',
385
+ glass: 'items-center px-4 pb-1 pt-2 flex justify-center',
386
+ material: 'items-center px-3 pb-1 pt-2 flex',
387
+ };
388
+ const TITLE_TEXT: Readonly<Record<ActionSheetLook, string>> = {
389
+ sheet: 'text-sm text-muted-foreground',
390
+ glass: 'text-center text-[13px] text-glass-muted-foreground',
391
+ material: 'text-[13px] text-glass-muted-foreground',
392
+ };
393
+ const ITEM_CONTAINER: Readonly<Record<ActionSheetLook, string>> = {
394
+ sheet: 'items-center border-t border-border px-4 py-3 flex justify-center',
395
+ glass:
396
+ 'min-h-[44px] items-center px-4 py-[11px] flex justify-center relative',
397
+ material: 'min-h-[48px] items-center px-3 py-3 flex relative',
398
+ };
399
+ const ITEM_TEXT: Readonly<Record<ActionSheetLook, string>> = {
400
+ sheet: 'text-base',
401
+ glass: 'text-center text-[17px]',
402
+ material: 'text-[16px]',
403
+ };
404
+
405
+ /**
406
+ * The action sheet's title, muted, above its items. A screen reader reads
407
+ * it as a heading.
408
+ */
228
409
  @Component({
229
410
  selector: 'ui-action-sheet-title',
230
411
  imports: [LYNX_ELEMENTS],
231
412
  encapsulation: ViewEncapsulation.None,
232
413
  template: `
233
414
  <view [class]="containerClass()">
234
- <text [class]="textClass()"><ng-content /></text>
415
+ <text [class]="textClass()" accessibility-traits="header"
416
+ ><ng-content
417
+ /></text>
235
418
  </view>
236
419
  `,
237
420
  })
238
421
  export class UiActionSheetTitle {
422
+ readonly #look = lookOf(inject(UiActionSheet, { optional: true }));
423
+
239
424
  readonly userClass = input<string>('', { alias: 'class' });
240
425
 
241
426
  protected readonly containerClass = computed(() =>
242
- cn('items-center px-4 py-3 flex justify-center', this.userClass()),
427
+ cn(TITLE_CONTAINER[this.#look()], this.userClass()),
243
428
  );
244
429
 
245
- protected readonly textClass = computed(() =>
246
- cn('text-sm text-muted-foreground'),
247
- );
430
+ protected readonly textClass = computed(() => TITLE_TEXT[this.#look()]);
248
431
  }
249
432
 
433
+ /**
434
+ * One of the action sheet's choices: a tap emits `pressed` and closes the
435
+ * sheet. A screen reader reads it as a button.
436
+ */
250
437
  @Component({
251
438
  selector: 'ui-action-sheet-item',
252
439
  imports: [LYNX_ELEMENTS],
@@ -263,51 +450,86 @@ export class UiActionSheetTitle {
263
450
  (bindmouseleave)="onPressCancel()"
264
451
  (bindtap)="onTap()"
265
452
  >
266
- <text [class]="textClass()"><ng-content /></text>
453
+ @if (look() !== 'sheet') {
454
+ <!-- A menu row's pressed highlight (see UiMenuItem's highlightClass):
455
+ rows in a popover light up rather than shrink. -->
456
+ <view
457
+ [class]="highlightClass()"
458
+ [style.opacity]="held() ? highlightOpacity() : 0"
459
+ ></view>
460
+ }
461
+ <text [class]="textClass()" accessibility-traits="button"
462
+ ><ng-content
463
+ /></text>
267
464
  </view>
268
465
  `,
269
466
  })
270
467
  export class UiActionSheetItem {
271
468
  readonly #sheet = inject(UiActionSheet);
469
+ protected readonly look = lookOf(this.#sheet);
272
470
 
273
471
  readonly variant = input<'default' | 'destructive'>('default');
274
472
  readonly userClass = input<string>('', { alias: 'class' });
275
473
 
276
474
  readonly pressed = output<void>();
277
475
 
278
- readonly containerRef = viewChild<ElementRef>('container');
476
+ // Required: the container is always in the template.
477
+ readonly containerRef = viewChild.required<ElementRef>('container');
279
478
  #pressAnim?: AnimationHandle;
280
479
 
281
- protected readonly containerClass = computed(() =>
480
+ /** Whether a finger (or the mouse) is down on a row in a popover. */
481
+ protected readonly held = signal(false);
482
+
483
+ // As UiMenuItem's: iOS 26's inset capsule, and Material's full-width
484
+ // state layer.
485
+ protected readonly highlightClass = computed(() =>
282
486
  cn(
283
- 'items-center border-t border-border px-4 py-3 flex justify-center',
284
- this.userClass(),
487
+ 'bg-glass-foreground absolute bottom-0 top-0',
488
+ this.look() === 'glass'
489
+ ? 'rounded-full left-[10px] right-[10px]'
490
+ : 'left-0 right-0',
285
491
  ),
286
492
  );
493
+ protected readonly highlightOpacity = computed(() =>
494
+ this.look() === 'glass' ? 0.08 : 0.1,
495
+ );
287
496
 
288
- protected readonly textClass = computed(() =>
289
- cn(
290
- 'text-base',
291
- this.variant() === 'destructive' ? 'text-destructive' : 'text-primary',
292
- ),
497
+ protected readonly containerClass = computed(() =>
498
+ cn(ITEM_CONTAINER[this.look()], this.userClass()),
293
499
  );
294
500
 
501
+ protected readonly textClass = computed(() => {
502
+ const destructive = this.variant() === 'destructive';
503
+ return cn(
504
+ ITEM_TEXT[this.look()],
505
+ destructive && 'text-destructive',
506
+ !destructive &&
507
+ (this.look() === 'sheet' ? 'text-primary' : 'text-glass-foreground'),
508
+ );
509
+ });
510
+
295
511
  protected onPressStart(): void {
512
+ if (this.look() !== 'sheet') {
513
+ this.held.set(true);
514
+ return;
515
+ }
296
516
  this.#pressAnim?.cancel();
297
517
  this.#pressAnim = pressDown(
298
- this.containerRef()?.nativeElement,
518
+ this.containerRef().nativeElement,
299
519
  SCALE.pressDownLight,
300
520
  );
301
521
  }
302
522
 
303
523
  protected onPressEnd(): void {
524
+ this.held.set(false);
304
525
  this.#pressAnim?.cancel();
305
- this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
526
+ this.#pressAnim = pressRelease(this.containerRef().nativeElement);
306
527
  }
307
528
 
308
529
  protected onPressCancel(): void {
530
+ this.held.set(false);
309
531
  this.#pressAnim?.cancel();
310
- this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
532
+ this.#pressAnim = pressRelease(this.containerRef().nativeElement);
311
533
  }
312
534
 
313
535
  protected onTap(): void {
@@ -316,6 +538,11 @@ export class UiActionSheetItem {
316
538
  }
317
539
  }
318
540
 
541
+ /**
542
+ * The action sheet's Cancel, in a tile of its own below the actions: a tap
543
+ * closes the sheet. An action sheet opened from its control has none: a
544
+ * touch outside it cancels.
545
+ */
319
546
  @Component({
320
547
  selector: 'ui-action-sheet-cancel',
321
548
  imports: [LYNX_ELEMENTS],
@@ -333,7 +560,9 @@ export class UiActionSheetItem {
333
560
  (bindmouseleave)="onPressCancel()"
334
561
  (bindtap)="onTap()"
335
562
  >
336
- <text [class]="textClass()">{{ label() }}</text>
563
+ <text [class]="textClass()" accessibility-traits="button">{{
564
+ label()
565
+ }}</text>
337
566
  </view>
338
567
  `,
339
568
  })
@@ -343,7 +572,8 @@ export class UiActionSheetCancel {
343
572
  readonly label = input('Cancel');
344
573
  readonly userClass = input<string>('', { alias: 'class' });
345
574
 
346
- readonly containerRef = viewChild<ElementRef>('container');
575
+ // Required: the container is always in the template.
576
+ readonly containerRef = viewChild.required<ElementRef>('container');
347
577
  #pressAnim?: AnimationHandle;
348
578
 
349
579
  protected readonly containerClass = computed(() =>
@@ -360,22 +590,76 @@ export class UiActionSheetCancel {
360
590
  protected onPressStart(): void {
361
591
  this.#pressAnim?.cancel();
362
592
  this.#pressAnim = pressDown(
363
- this.containerRef()?.nativeElement,
593
+ this.containerRef().nativeElement,
364
594
  SCALE.pressDownLight,
365
595
  );
366
596
  }
367
597
 
368
598
  protected onPressEnd(): void {
369
599
  this.#pressAnim?.cancel();
370
- this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
600
+ this.#pressAnim = pressRelease(this.containerRef().nativeElement);
371
601
  }
372
602
 
373
603
  protected onPressCancel(): void {
374
604
  this.#pressAnim?.cancel();
375
- this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
605
+ this.#pressAnim = pressRelease(this.containerRef().nativeElement);
376
606
  }
377
607
 
378
608
  protected onTap(): void {
379
609
  this.#sheet.close();
380
610
  }
381
611
  }
612
+
613
+ /**
614
+ * Marks the control that presents an action sheet, which then opens from it
615
+ * instead of from the bottom of the screen: Liquid Glass that grows out of
616
+ * it on iOS 26, and a Material 3 menu beside it on Android.
617
+ *
618
+ * ```html
619
+ * <ui-button [uiActionSheetAnchor]="sheet" (pressed)="open.set(true)">
620
+ * Share
621
+ * </ui-button>
622
+ * <ui-action-sheet #sheet [(open)]="open">…</ui-action-sheet>
623
+ * ```
624
+ *
625
+ * It only says where the sheet opens from: open it as before, from the
626
+ * control's own tap handler. A touch on the control doesn't count as one
627
+ * outside the sheet, so it doesn't close it.
628
+ */
629
+ @Directive({
630
+ selector: '[uiActionSheetAnchor]',
631
+ host: {
632
+ // Bubbling, from what the touch landed on in the control: Lynx 4.1 ran
633
+ // no capture listener on iOS (see UiPopoverPanel).
634
+ '(bindtouchstart)': 'sheet().touchedAnchor()',
635
+ '(bindmousedown)': 'sheet().touchedAnchor()',
636
+ '[style.visibility]': 'sheet().coversAnchor() ? "hidden" : null',
637
+ },
638
+ })
639
+ export class UiActionSheetAnchor {
640
+ /** The action sheet this control presents. */
641
+ readonly sheet = input.required<UiActionSheet>({
642
+ alias: 'uiActionSheetAnchor',
643
+ });
644
+
645
+ readonly #element =
646
+ inject<ElementRef<PopoverAnchor>>(ElementRef).nativeElement;
647
+ readonly #glass = inject(UiGlass, { self: true, optional: true });
648
+
649
+ /**
650
+ * Points the sheet at this control, and away from it when the directive
651
+ * is destroyed or points at another sheet. A control that's glass itself
652
+ * (`<blur-view ui-glass [uiActionSheetAnchor]>`) shares the sheet's glass
653
+ * id meanwhile, so the sheet takes its glass over (see UiPopoverPanel's
654
+ * coversAnchor).
655
+ */
656
+ protected readonly register = effect((onCleanup) => {
657
+ const sheet = this.sheet();
658
+ sheet.setAnchor(this.#element);
659
+ this.#glass?.pair(sheet.glassId);
660
+ onCleanup(() => {
661
+ sheet.setAnchor(null);
662
+ this.#glass?.pair(null);
663
+ });
664
+ });
665
+ }
@@ -1,5 +1,6 @@
1
1
  export {
2
2
  UiActionSheet,
3
+ UiActionSheetAnchor,
3
4
  UiActionSheetTitle,
4
5
  UiActionSheetItem,
5
6
  UiActionSheetCancel,