@blotch/dolan 22.2.4 → 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 (124) hide show
  1. package/dist/commands/add.test.js +2 -0
  2. package/dist/commands/add.test.js.map +1 -1
  3. package/dist/commands/doctor.js +78 -21
  4. package/dist/commands/doctor.js.map +1 -1
  5. package/dist/commands/doctor.test.js +167 -26
  6. package/dist/commands/doctor.test.js.map +1 -1
  7. package/dist/commands/init.test.js +4 -7
  8. package/dist/commands/init.test.js.map +1 -1
  9. package/dist/commands/update.d.ts +3 -1
  10. package/dist/commands/update.js +78 -15
  11. package/dist/commands/update.js.map +1 -1
  12. package/dist/commands/update.test.js +257 -2
  13. package/dist/commands/update.test.js.map +1 -1
  14. package/dist/lockfile.d.ts +10 -0
  15. package/dist/lockfile.js +20 -1
  16. package/dist/lockfile.js.map +1 -1
  17. package/dist/lockfile.test.js +43 -3
  18. package/dist/lockfile.test.js.map +1 -1
  19. package/dist/registry.js +73 -11
  20. package/dist/registry.js.map +1 -1
  21. package/dist/registry.test.js +16 -3
  22. package/dist/registry.test.js.map +1 -1
  23. package/dist/test-utils.d.ts +24 -2
  24. package/dist/test-utils.js +41 -2
  25. package/dist/test-utils.js.map +1 -1
  26. package/dist/ui/boolean-inputs.test.ts +76 -0
  27. package/dist/ui/components/action-sheet/action-sheet.test.ts +370 -1
  28. package/dist/ui/components/action-sheet/action-sheet.ts +306 -28
  29. package/dist/ui/components/action-sheet/index.ts +1 -0
  30. package/dist/ui/components/alert-dialog/alert-dialog.test.ts +552 -8
  31. package/dist/ui/components/alert-dialog/alert-dialog.ts +644 -36
  32. package/dist/ui/components/alert-dialog/index.ts +5 -0
  33. package/dist/ui/components/area-chart/area-chart.ts +3 -2
  34. package/dist/ui/components/badge/badge.ts +2 -1
  35. package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +968 -3
  36. package/dist/ui/components/bottom-sheet/bottom-sheet.ts +1021 -95
  37. package/dist/ui/components/bottom-sheet/index.ts +7 -1
  38. package/dist/ui/components/button/button.test.ts +36 -1
  39. package/dist/ui/components/button/button.ts +17 -7
  40. package/dist/ui/components/button-group/button-group.test.ts +53 -9
  41. package/dist/ui/components/button-group/button-group.ts +67 -21
  42. package/dist/ui/components/card/card.test.ts +16 -0
  43. package/dist/ui/components/card/card.ts +2 -1
  44. package/dist/ui/components/chart/chart-presets.ts +6 -6
  45. package/dist/ui/components/chart/chart.test.ts +14 -0
  46. package/dist/ui/components/chart/chart.ts +10 -3
  47. package/dist/ui/components/checkbox/checkbox.ts +2 -1
  48. package/dist/ui/components/collapsible/collapsible.ts +2 -1
  49. package/dist/ui/components/dialog/dialog.ts +6 -6
  50. package/dist/ui/components/dock/dock-scroll.ts +9 -0
  51. package/dist/ui/components/dock/dock-search.test.ts +594 -0
  52. package/dist/ui/components/dock/dock.test.ts +4 -1
  53. package/dist/ui/components/dock/dock.ts +529 -51
  54. package/dist/ui/components/dynamic-color/dynamic-color.test.ts +144 -0
  55. package/dist/ui/components/dynamic-color/dynamic-color.ts +146 -0
  56. package/dist/ui/components/dynamic-color/index.ts +5 -0
  57. package/dist/ui/components/glass/glass.test.ts +39 -1
  58. package/dist/ui/components/glass/glass.ts +87 -17
  59. package/dist/ui/components/glass/index.ts +1 -0
  60. package/dist/ui/components/icon/icon-theme.test.ts +28 -0
  61. package/dist/ui/components/icon/icon-theme.ts +38 -7
  62. package/dist/ui/components/input/input.ts +2 -1
  63. package/dist/ui/components/label/label.ts +8 -2
  64. package/dist/ui/components/line-chart/line-chart.ts +3 -2
  65. package/dist/ui/components/list/list.ts +5 -4
  66. package/dist/ui/components/menu/menu-popup.test.ts +18 -14
  67. package/dist/ui/components/menu/menu-popup.ts +24 -3
  68. package/dist/ui/components/menu/menu.test.ts +99 -5
  69. package/dist/ui/components/menu/menu.ts +74 -34
  70. package/dist/ui/components/nav-drawer/nav-drawer.ts +3 -2
  71. package/dist/ui/components/navigation-bar/index.ts +1 -0
  72. package/dist/ui/components/navigation-bar/navigation-bar.test.ts +425 -0
  73. package/dist/ui/components/navigation-bar/navigation-bar.ts +411 -0
  74. package/dist/ui/components/popover/index.ts +9 -0
  75. package/dist/ui/components/popover/popover-panel.test.ts +735 -0
  76. package/dist/ui/components/popover/popover-panel.ts +603 -0
  77. package/dist/ui/components/popover/popover.test.ts +286 -0
  78. package/dist/ui/components/popover/popover.ts +207 -0
  79. package/dist/ui/components/progress/progress.ts +2 -1
  80. package/dist/ui/components/radio-group/radio-group.ts +2 -1
  81. package/dist/ui/components/scroll-area/scroll-area.ts +4 -3
  82. package/dist/ui/components/search-field/index.ts +15 -0
  83. package/dist/ui/components/search-field/keyboard.test.ts +147 -0
  84. package/dist/ui/components/search-field/keyboard.ts +123 -0
  85. package/dist/ui/components/search-field/search-field.test.ts +549 -0
  86. package/dist/ui/components/search-field/search-field.ts +628 -0
  87. package/dist/ui/components/segmented-control/index.ts +8 -0
  88. package/dist/ui/components/segmented-control/segmented-control.test.ts +861 -0
  89. package/dist/ui/components/segmented-control/segmented-control.ts +884 -0
  90. package/dist/ui/components/select/select.ts +2 -1
  91. package/dist/ui/components/sheet/sheet.test.ts +90 -3
  92. package/dist/ui/components/sheet/sheet.ts +59 -4
  93. package/dist/ui/components/slider/index.ts +1 -0
  94. package/dist/ui/components/slider/slider.test.ts +695 -0
  95. package/dist/ui/components/slider/slider.ts +901 -0
  96. package/dist/ui/components/switch/index.ts +1 -1
  97. package/dist/ui/components/switch/switch.test.ts +464 -48
  98. package/dist/ui/components/switch/switch.ts +613 -43
  99. package/dist/ui/components/tabs/index.ts +7 -1
  100. package/dist/ui/components/tabs/tabs.test.ts +278 -2
  101. package/dist/ui/components/tabs/tabs.ts +233 -30
  102. package/dist/ui/components/textarea/textarea.ts +2 -1
  103. package/dist/ui/components/toggle/index.ts +1 -0
  104. package/dist/ui/components/toggle/toggle.test.ts +310 -4
  105. package/dist/ui/components/toggle/toggle.ts +241 -26
  106. package/dist/ui/components/toolbar/bar-scroll.test.ts +310 -0
  107. package/dist/ui/components/toolbar/bar-scroll.ts +372 -0
  108. package/dist/ui/components/toolbar/index.ts +12 -0
  109. package/dist/ui/components/toolbar/toolbar.test.ts +438 -0
  110. package/dist/ui/components/toolbar/toolbar.ts +535 -0
  111. package/dist/utils/analyze.d.ts +10 -5
  112. package/dist/utils/analyze.js +3 -2
  113. package/dist/utils/analyze.js.map +1 -1
  114. package/dist/utils/installed.d.ts +13 -1
  115. package/dist/utils/installed.js +17 -0
  116. package/dist/utils/installed.js.map +1 -1
  117. package/dist/utils/installed.test.js +39 -1
  118. package/dist/utils/installed.test.js.map +1 -1
  119. package/dist/utils/tailwind.d.ts +25 -5
  120. package/dist/utils/tailwind.js +53 -14
  121. package/dist/utils/tailwind.js.map +1 -1
  122. package/dist/utils/tailwind.test.js +85 -22
  123. package/dist/utils/tailwind.test.js.map +1 -1
  124. package/package.json +1 -1
@@ -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,13 +29,56 @@ 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 —
@@ -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
@@ -98,7 +218,7 @@ export class UiActionSheet implements OnDestroy {
98
218
  readonly panelRef = viewChild.required<ElementRef>('panel');
99
219
  #backdropAnim?: AnimationHandle;
100
220
  #panelAnim?: AnimationHandle;
101
- #hasBeenOpen = false;
221
+ #sheetOpen = false;
102
222
  // The open and close sequences' pending timers, so ngOnDestroy can stop
103
223
  // them.
104
224
  readonly #timers = new Set<ReturnType<typeof setTimeout>>();
@@ -108,17 +228,20 @@ export class UiActionSheet implements OnDestroy {
108
228
  * constructor: Angular's test build keeps a function for a constructor that
109
229
  * nothing calls, which an app's coverage report would show as uncovered.
110
230
  *
111
- * `#hasBeenOpen` prevents the close animation from running on the initial
112
- * effect evaluation when `open` starts as false. Without this guard, the
113
- * first run would call #doClose() and emit `closed` before the action
114
- * sheet has ever been opened.
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.
115
235
  */
116
236
  protected readonly openOrClose = effect(() => {
117
- const isOpen = this.open();
237
+ // An anchored sheet opens in its popover panel instead, which follows
238
+ // `open` itself.
239
+ const isOpen = this.open() && !this.anchored();
118
240
  if (isOpen) {
119
- this.#hasBeenOpen = true;
241
+ this.#sheetOpen = true;
120
242
  this.#doOpen();
121
- } else if (this.#hasBeenOpen) {
243
+ } else if (this.#sheetOpen) {
244
+ this.#sheetOpen = false;
122
245
  this.#doClose();
123
246
  }
124
247
  });
@@ -139,6 +262,21 @@ export class UiActionSheet implements OnDestroy {
139
262
  this.open.set(false);
140
263
  }
141
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
+
142
280
  /**
143
281
  * No-op tap handler for the panel. `catchtap` (vs `bindtap`) already stops
144
282
  * the tap from bubbling to the backdrop — Lynx controls propagation via the
@@ -229,28 +367,73 @@ export class UiActionSheet implements OnDestroy {
229
367
  }
230
368
  }
231
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
+ */
232
409
  @Component({
233
410
  selector: 'ui-action-sheet-title',
234
411
  imports: [LYNX_ELEMENTS],
235
412
  encapsulation: ViewEncapsulation.None,
236
413
  template: `
237
414
  <view [class]="containerClass()">
238
- <text [class]="textClass()"><ng-content /></text>
415
+ <text [class]="textClass()" accessibility-traits="header"
416
+ ><ng-content
417
+ /></text>
239
418
  </view>
240
419
  `,
241
420
  })
242
421
  export class UiActionSheetTitle {
422
+ readonly #look = lookOf(inject(UiActionSheet, { optional: true }));
423
+
243
424
  readonly userClass = input<string>('', { alias: 'class' });
244
425
 
245
426
  protected readonly containerClass = computed(() =>
246
- cn('items-center px-4 py-3 flex justify-center', this.userClass()),
427
+ cn(TITLE_CONTAINER[this.#look()], this.userClass()),
247
428
  );
248
429
 
249
- protected readonly textClass = computed(() =>
250
- cn('text-sm text-muted-foreground'),
251
- );
430
+ protected readonly textClass = computed(() => TITLE_TEXT[this.#look()]);
252
431
  }
253
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
+ */
254
437
  @Component({
255
438
  selector: 'ui-action-sheet-item',
256
439
  imports: [LYNX_ELEMENTS],
@@ -267,12 +450,23 @@ export class UiActionSheetTitle {
267
450
  (bindmouseleave)="onPressCancel()"
268
451
  (bindtap)="onTap()"
269
452
  >
270
- <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>
271
464
  </view>
272
465
  `,
273
466
  })
274
467
  export class UiActionSheetItem {
275
468
  readonly #sheet = inject(UiActionSheet);
469
+ protected readonly look = lookOf(this.#sheet);
276
470
 
277
471
  readonly variant = input<'default' | 'destructive'>('default');
278
472
  readonly userClass = input<string>('', { alias: 'class' });
@@ -283,21 +477,42 @@ export class UiActionSheetItem {
283
477
  readonly containerRef = viewChild.required<ElementRef>('container');
284
478
  #pressAnim?: AnimationHandle;
285
479
 
286
- 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(() =>
287
486
  cn(
288
- 'items-center border-t border-border px-4 py-3 flex justify-center',
289
- 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',
290
491
  ),
291
492
  );
493
+ protected readonly highlightOpacity = computed(() =>
494
+ this.look() === 'glass' ? 0.08 : 0.1,
495
+ );
292
496
 
293
- protected readonly textClass = computed(() =>
294
- cn(
295
- 'text-base',
296
- this.variant() === 'destructive' ? 'text-destructive' : 'text-primary',
297
- ),
497
+ protected readonly containerClass = computed(() =>
498
+ cn(ITEM_CONTAINER[this.look()], this.userClass()),
298
499
  );
299
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
+
300
511
  protected onPressStart(): void {
512
+ if (this.look() !== 'sheet') {
513
+ this.held.set(true);
514
+ return;
515
+ }
301
516
  this.#pressAnim?.cancel();
302
517
  this.#pressAnim = pressDown(
303
518
  this.containerRef().nativeElement,
@@ -306,11 +521,13 @@ export class UiActionSheetItem {
306
521
  }
307
522
 
308
523
  protected onPressEnd(): void {
524
+ this.held.set(false);
309
525
  this.#pressAnim?.cancel();
310
526
  this.#pressAnim = pressRelease(this.containerRef().nativeElement);
311
527
  }
312
528
 
313
529
  protected onPressCancel(): void {
530
+ this.held.set(false);
314
531
  this.#pressAnim?.cancel();
315
532
  this.#pressAnim = pressRelease(this.containerRef().nativeElement);
316
533
  }
@@ -321,6 +538,11 @@ export class UiActionSheetItem {
321
538
  }
322
539
  }
323
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
+ */
324
546
  @Component({
325
547
  selector: 'ui-action-sheet-cancel',
326
548
  imports: [LYNX_ELEMENTS],
@@ -338,7 +560,9 @@ export class UiActionSheetItem {
338
560
  (bindmouseleave)="onPressCancel()"
339
561
  (bindtap)="onTap()"
340
562
  >
341
- <text [class]="textClass()">{{ label() }}</text>
563
+ <text [class]="textClass()" accessibility-traits="button">{{
564
+ label()
565
+ }}</text>
342
566
  </view>
343
567
  `,
344
568
  })
@@ -385,3 +609,57 @@ export class UiActionSheetCancel {
385
609
  this.#sheet.close();
386
610
  }
387
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,