@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
@@ -0,0 +1,286 @@
1
+ import { Component, signal } from '@angular/core';
2
+ import {
3
+ fireEvent,
4
+ render,
5
+ waitForUpdate,
6
+ } from '@blotch/angular-lynx-testing-library';
7
+ import { LYNX_ELEMENTS, LynxAccessibilitySettings } from '@blotch/angular-lynx';
8
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
9
+ import { fireLynxEvent } from '../../../test-utils/fire-lynx-event';
10
+ import { UiGlass } from '../glass/glass';
11
+ import { UiPopover, UiPopoverContent, UiPopoverTrigger } from './popover';
12
+
13
+ @Component({
14
+ imports: [
15
+ LYNX_ELEMENTS,
16
+ UiGlass,
17
+ UiPopover,
18
+ UiPopoverContent,
19
+ UiPopoverTrigger,
20
+ ],
21
+ template: `
22
+ <ui-popover
23
+ [open]="open()"
24
+ (openChange)="open.set($event)"
25
+ (closed)="closed()"
26
+ >
27
+ @if (glassTrigger()) {
28
+ <blur-view
29
+ ui-glass
30
+ uiPopoverTrigger
31
+ class="trigger"
32
+ [accessibility-element]="true"
33
+ accessibility-traits="button"
34
+ accessibility-label="Tip"
35
+ >
36
+ <text>Tip</text>
37
+ </blur-view>
38
+ } @else if (withTrigger()) {
39
+ <view
40
+ uiPopoverTrigger
41
+ class="trigger"
42
+ [accessibility-element]="true"
43
+ accessibility-traits="button"
44
+ accessibility-label="Tip"
45
+ >
46
+ <text>Tip</text>
47
+ </view>
48
+ }
49
+ <ui-popover-content class="w-60 p-3">
50
+ <text>Liked trips show up first.</text>
51
+ </ui-popover-content>
52
+ </ui-popover>
53
+ <view class="like" (bindtap)="liked()"><text>Like</text></view>
54
+ `,
55
+ })
56
+ class PopoverHost {
57
+ readonly open = signal(false);
58
+ readonly withTrigger = signal(true);
59
+ readonly glassTrigger = signal(false);
60
+ readonly closed = vi.fn();
61
+ readonly liked = vi.fn();
62
+ }
63
+
64
+ /**
65
+ * The Lynx function that runs an element's UI methods, such as measuring it
66
+ * (`boundingClientRect`). The testing library defines it on the global
67
+ * object.
68
+ */
69
+ type InvokeUIMethod = {
70
+ __InvokeUIMethod: (
71
+ element: Element,
72
+ method: string,
73
+ params: Record<string, unknown>,
74
+ callback: (result: { code: number; data?: unknown }) => void,
75
+ ) => void;
76
+ };
77
+
78
+ /** Accessibility settings a test can change, as the host app would. */
79
+ const settings = {
80
+ reduceMotion: signal(false),
81
+ reduceTransparency: signal(false),
82
+ increaseContrast: signal(false),
83
+ };
84
+
85
+ /**
86
+ * Renders the popover on a platform (none for Material), measuring elements
87
+ * as `boundingClientRect` would on a phone: the safe area's markers a
88
+ * phone's screen, the content 240 by 80, and anything else, the trigger, a
89
+ * small box near the top.
90
+ */
91
+ const renderPopover = async (
92
+ platform: { platform: string; osVersion: string } | null = {
93
+ platform: 'iOS',
94
+ osVersion: '26.0',
95
+ },
96
+ ) => {
97
+ vi.stubGlobal('SystemInfo', platform ?? undefined);
98
+ const measured: Element[] = [];
99
+ vi.spyOn(
100
+ globalThis as unknown as InvokeUIMethod,
101
+ '__InvokeUIMethod',
102
+ ).mockImplementation((element, method, _params, callback) => {
103
+ if (method !== 'boundingClientRect') {
104
+ callback({ code: 0 });
105
+ return;
106
+ }
107
+ measured.push(element);
108
+ const style = element.getAttribute('style') ?? '';
109
+ const data = style.includes('safe-area-inset')
110
+ ? { left: 0, top: 0, width: 402, height: 840 }
111
+ : element.classList.contains('shrink-0')
112
+ ? { left: 0, top: 0, width: 240, height: 80 }
113
+ : { left: 16, top: 80, width: 80, height: 44 };
114
+ callback({ code: 0, data });
115
+ });
116
+ const result = await render(PopoverHost, {
117
+ providers: [{ provide: LynxAccessibilitySettings, useValue: settings }],
118
+ });
119
+ const host = result.componentRef.instance as PopoverHost;
120
+ return {
121
+ ...result,
122
+ host,
123
+ measured,
124
+ trigger: () => result.container.querySelector('.trigger') as HTMLElement,
125
+ panel: () =>
126
+ result.container.querySelector(
127
+ 'blur-view[ui-glass]:not(.trigger)',
128
+ ) as HTMLElement | null,
129
+ };
130
+ };
131
+
132
+ type Rendered = Awaited<ReturnType<typeof renderPopover>>;
133
+
134
+ /**
135
+ * Waits until the popover has grown to its frame, its content shown.
136
+ */
137
+ const waitForOpen = async ({ container }: Rendered) => {
138
+ await vi.waitFor(() =>
139
+ expect(
140
+ (container.querySelector('.shrink-0') as HTMLElement | null)?.style
141
+ .opacity,
142
+ ).toBe('1'),
143
+ );
144
+ };
145
+
146
+ /**
147
+ * Lets Lynx's timers run: the touch check waits for the next one.
148
+ */
149
+ const nextTimer = () => new Promise((resolve) => setTimeout(resolve, 5));
150
+
151
+ // The renderer warns that <ui-popover> and its parts aren't Lynx elements.
152
+ beforeEach(() => {
153
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
154
+ });
155
+
156
+ afterEach(() => {
157
+ vi.restoreAllMocks();
158
+ vi.unstubAllGlobals();
159
+ });
160
+
161
+ describe('UiPopover', () => {
162
+ it('opens and closes when its trigger is tapped', async () => {
163
+ const rendered = await renderPopover();
164
+ const { host, trigger, getByText } = rendered;
165
+
166
+ fireEvent.tap(trigger());
167
+ expect(host.open()).toBe(true);
168
+ await waitForOpen(rendered);
169
+ expect(getByText('Liked trips show up first.')).toBeTruthy();
170
+ // It opened from the trigger.
171
+ expect(rendered.measured[0]).toBe(trigger());
172
+
173
+ fireEvent.tap(trigger());
174
+ expect(host.open()).toBe(false);
175
+ await vi.waitFor(() => expect(host.closed).toHaveBeenCalledTimes(1));
176
+ await waitForUpdate();
177
+ expect(rendered.panel()).toBeNull();
178
+ });
179
+
180
+ it('closes on a touch outside it, which still reaches what it touched', async () => {
181
+ const rendered = await renderPopover();
182
+ rendered.host.open.set(true);
183
+ await waitForOpen(rendered);
184
+
185
+ // A tap on the like button: Lynx tells the popover through its global
186
+ // listener, and the button gets its tap as ever.
187
+ fireLynxEvent(rendered.panel()!, 'touchstart', {}, 'global-bindEvent');
188
+ fireEvent.tap(rendered.container.querySelector('.like')!);
189
+ await nextTimer();
190
+ expect(rendered.host.open()).toBe(false);
191
+ expect(rendered.host.liked).toHaveBeenCalledTimes(1);
192
+ });
193
+
194
+ it("doesn't take a touch on its trigger for one outside it", async () => {
195
+ const rendered = await renderPopover();
196
+ rendered.host.open.set(true);
197
+ await waitForOpen(rendered);
198
+
199
+ fireLynxEvent(rendered.trigger(), 'touchstart', {}, 'bindEvent');
200
+ fireLynxEvent(rendered.panel()!, 'touchstart', {}, 'global-bindEvent');
201
+ await nextTimer();
202
+ fireLynxEvent(rendered.trigger(), 'mousedown', {}, 'bindEvent');
203
+ fireLynxEvent(rendered.panel()!, 'mousedown', {}, 'global-bindEvent');
204
+ await nextTimer();
205
+ expect(rendered.host.open()).toBe(true);
206
+ });
207
+
208
+ it('hides its trigger while Liquid Glass grows out of it', async () => {
209
+ const rendered = await renderPopover();
210
+ expect(rendered.trigger().style.visibility).toBe('');
211
+
212
+ rendered.host.open.set(true);
213
+ await waitForOpen(rendered);
214
+ expect(rendered.trigger().style.visibility).toBe('hidden');
215
+
216
+ rendered.host.open.set(false);
217
+ await vi.waitFor(() => expect(rendered.host.closed).toHaveBeenCalled());
218
+ await waitForUpdate();
219
+ expect(rendered.trigger().style.visibility).toBe('');
220
+ // A trigger that isn't glass has no glass to hand over.
221
+ expect(rendered.trigger().hasAttribute('glass-effect-id')).toBe(false);
222
+ });
223
+
224
+ it("takes a glass trigger's glass over, shown as the trigger hides", async () => {
225
+ const rendered = await renderPopover();
226
+ rendered.host.glassTrigger.set(true);
227
+ await waitForUpdate();
228
+ const id = rendered.trigger().getAttribute('glass-effect-id');
229
+ expect(id).toMatch(/^ui-popover-\d+$/);
230
+
231
+ rendered.host.open.set(true);
232
+ // Shown at the trigger's place in the same update that hides the
233
+ // trigger, under the same id.
234
+ await waitForOpen(rendered);
235
+ expect(rendered.panel()!.style.visibility).toBe('');
236
+ expect(rendered.panel()!.getAttribute('glass-effect-id')).toBe(id);
237
+ expect(rendered.trigger().style.visibility).toBe('hidden');
238
+
239
+ rendered.host.open.set(false);
240
+ await vi.waitFor(() => expect(rendered.host.closed).toHaveBeenCalled());
241
+ await waitForUpdate();
242
+ expect(rendered.panel()).toBeNull();
243
+ expect(rendered.trigger().style.visibility).toBe('');
244
+
245
+ // A trigger that goes takes its glass with it.
246
+ rendered.host.glassTrigger.set(false);
247
+ await waitForUpdate();
248
+ expect(rendered.trigger().tagName.toLowerCase()).toBe('view');
249
+ });
250
+
251
+ it('leaves its trigger showing beside a Material surface', async () => {
252
+ const rendered = await renderPopover(null);
253
+ rendered.host.open.set(true);
254
+ await waitForOpen(rendered);
255
+
256
+ expect(rendered.trigger().style.visibility).toBe('');
257
+ });
258
+
259
+ it('opens from itself when nothing is its trigger', async () => {
260
+ const rendered = await renderPopover();
261
+ rendered.host.withTrigger.set(false);
262
+ await waitForUpdate();
263
+
264
+ rendered.host.open.set(true);
265
+ await waitForOpen(rendered);
266
+ // The popover's own element, which holds the content's slot.
267
+ expect(rendered.measured[0]).not.toBe(rendered.trigger());
268
+ expect(rendered.measured[0]?.classList.contains('like')).toBe(false);
269
+ });
270
+ });
271
+
272
+ describe('UiPopoverContent', () => {
273
+ it('is a padded column, with its class on its own element', async () => {
274
+ const rendered = await renderPopover();
275
+ rendered.host.open.set(true);
276
+ await waitForOpen(rendered);
277
+
278
+ const content = rendered.getByText(
279
+ 'Liked trips show up first.',
280
+ ).parentElement!;
281
+ expect(content.classList).toContain('flex-col');
282
+ expect(content.classList).toContain('w-60');
283
+ expect(content.classList).toContain('p-3');
284
+ expect(content.classList).not.toContain('p-4');
285
+ });
286
+ });
@@ -0,0 +1,207 @@
1
+ import {
2
+ Component,
3
+ DestroyRef,
4
+ Directive,
5
+ ElementRef,
6
+ ViewEncapsulation,
7
+ computed,
8
+ inject,
9
+ input,
10
+ model,
11
+ output,
12
+ signal,
13
+ viewChild,
14
+ } from '@angular/core';
15
+ import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
16
+
17
+ import { cn } from '../../utils/cn';
18
+ import { UiGlass } from '../glass/glass';
19
+ import { type PopoverAnchor, UiPopoverPanel } from './popover-panel';
20
+
21
+ /** How many popovers there have been, for their glass ids. */
22
+ let popoverCount = 0;
23
+
24
+ /**
25
+ * A popover's content: a column, padded, that a `ui-popover` shows when it
26
+ * opens.
27
+ *
28
+ * ```html
29
+ * <ui-popover-content class="w-64 gap-1">
30
+ * <text class="text-base font-semibold text-glass-foreground">Kyoto</text>
31
+ * <text class="text-sm text-glass-muted-foreground">Day 3 of 5</text>
32
+ * </ui-popover-content>
33
+ * ```
34
+ *
35
+ * Its classes go on its own element, so `class` sets its width, padding and
36
+ * spacing. It's as wide as its content by default, and no wider than the
37
+ * screen. Color text in it with `text-glass-foreground` and
38
+ * `text-glass-muted-foreground`, which suit the glass and Material's solid
39
+ * surface alike.
40
+ */
41
+ @Component({
42
+ selector: 'ui-popover-content',
43
+ encapsulation: ViewEncapsulation.None,
44
+ // The classes on the host itself: it's the column, with no inner view in
45
+ // a host of no size of its own between it and the panel.
46
+ host: { '[class]': 'contentClass()' },
47
+ template: `<ng-content />`,
48
+ })
49
+ export class UiPopoverContent {
50
+ readonly userClass = input<string>('', { alias: 'class' });
51
+
52
+ protected readonly contentClass = computed(() =>
53
+ cn('flex-col gap-1 p-4 flex', this.userClass()),
54
+ );
55
+ }
56
+
57
+ /**
58
+ * A popover: content that opens from the control that presents it, and
59
+ * leaves the rest of the screen as it was.
60
+ *
61
+ * ```html
62
+ * <ui-popover>
63
+ * <ui-button uiPopoverTrigger variant="outline">Details</ui-button>
64
+ * <ui-popover-content class="w-64">
65
+ * <text class="text-sm text-glass-foreground">Check-in from 3pm.</text>
66
+ * </ui-popover-content>
67
+ * </ui-popover>
68
+ * ```
69
+ *
70
+ * Mark the element that opens it with `uiPopoverTrigger`; what's in the
71
+ * `<ui-popover-content>` is the popover. A tap on the trigger opens and
72
+ * closes it, and a touch anywhere else closes it without being swallowed:
73
+ * nothing dims, and the button under the finger still gets the tap.
74
+ * `[(open)]` opens and closes it from code.
75
+ *
76
+ * On iOS 26 and later it's Liquid Glass that grows out of the trigger, over
77
+ * it, as iOS 26 grows a popover out of its source item. Before iOS 26 it's
78
+ * the frosted material beside the trigger, with an arrow pointing at it, as
79
+ * UIKit drew a popover on iPhone. On Android it's a Material 3 menu surface
80
+ * that unfolds below the trigger, or Liquid Glass with
81
+ * `provideGlass({ android: 'glass' })`. It stays inside the safe area, and
82
+ * opens upward near the bottom of the screen.
83
+ */
84
+ @Component({
85
+ selector: 'ui-popover',
86
+ imports: [LYNX_ELEMENTS, UiPopoverPanel],
87
+ encapsulation: ViewEncapsulation.None,
88
+ template: `
89
+ <ng-content />
90
+ <ui-popover-panel
91
+ #panel
92
+ [open]="open()"
93
+ [anchor]="anchor()"
94
+ [glassId]="glassId"
95
+ (dismissed)="close()"
96
+ (closed)="closed.emit()"
97
+ >
98
+ <ng-content select="ui-popover-content" />
99
+ </ui-popover-panel>
100
+ `,
101
+ })
102
+ export class UiPopover {
103
+ /** Whether the popover is open. */
104
+ readonly open = model(false);
105
+ /** The popover has closed and its close animation has finished. */
106
+ readonly closed = output<void>();
107
+
108
+ readonly #host = inject<ElementRef<PopoverAnchor>>(ElementRef);
109
+ readonly #trigger = signal<PopoverAnchor | null>(null);
110
+
111
+ /**
112
+ * The id the popover's glass and a glass trigger share (see UiGlass's
113
+ * `glassId`), unique to the popover, so it takes over the trigger's glass
114
+ * as it grows out of it.
115
+ */
116
+ readonly glassId = `ui-popover-${++popoverCount}`;
117
+
118
+ // Required: always in the template.
119
+ readonly panel = viewChild.required<UiPopoverPanel>('panel');
120
+
121
+ /**
122
+ * What the popover opens from: its trigger, or the popover's own element
123
+ * when nothing is marked `uiPopoverTrigger`.
124
+ */
125
+ protected readonly anchor = computed(
126
+ () => this.#trigger() ?? this.#host.nativeElement,
127
+ );
128
+
129
+ /** Whether the popover covers its trigger now; see coversAnchor. */
130
+ readonly coversTrigger = computed(() => this.panel().coversAnchor());
131
+
132
+ toggle(): void {
133
+ this.open.update((open) => !open);
134
+ }
135
+
136
+ close(): void {
137
+ this.open.set(false);
138
+ }
139
+
140
+ /**
141
+ * @internal Called by the `uiPopoverTrigger` directive.
142
+ */
143
+ setTrigger(trigger: PopoverAnchor | null): void {
144
+ this.#trigger.set(trigger);
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Marks the element that opens a `ui-popover`: a tap on it opens or closes
150
+ * the popover, which opens from it.
151
+ *
152
+ * ```html
153
+ * <ui-popover>
154
+ * <ui-button uiPopoverTrigger>Details</ui-button>
155
+ * …
156
+ * </ui-popover>
157
+ * ```
158
+ *
159
+ * A touch on the trigger doesn't count as one outside the popover, so a tap
160
+ * on it closes an open popover rather than closing and reopening it.
161
+ */
162
+ @Directive({
163
+ selector: '[uiPopoverTrigger]',
164
+ host: {
165
+ '(click)': 'popover.toggle()',
166
+ // Bubbling, from what the touch landed on in the trigger: Lynx 4.1 ran
167
+ // no capture listener on iOS (see UiPopoverPanel).
168
+ '(bindtouchstart)': 'popover.panel().touchedAnchor()',
169
+ '(bindmousedown)': 'popover.panel().touchedAnchor()',
170
+ '[style.visibility]': 'popover.coversTrigger() ? "hidden" : null',
171
+ },
172
+ })
173
+ export class UiPopoverTrigger {
174
+ protected readonly popover = inject(UiPopover);
175
+
176
+ /**
177
+ * Points the popover at this element until the directive is destroyed. A
178
+ * trigger that's glass itself (`<blur-view ui-glass uiPopoverTrigger>`)
179
+ * shares the popover's glass id meanwhile, so the popover takes its glass
180
+ * over (see UiPopoverPanel's coversAnchor).
181
+ */
182
+ protected readonly unregister = registerTrigger(
183
+ this.popover,
184
+ inject<ElementRef<PopoverAnchor>>(ElementRef).nativeElement,
185
+ inject(UiGlass, { self: true, optional: true }),
186
+ inject(DestroyRef),
187
+ );
188
+ }
189
+
190
+ /**
191
+ * Makes `trigger` the element `popover` opens from, and pairs its `glass`,
192
+ * if it's glass, with the popover's, until `destroyRef` is destroyed.
193
+ * Returns what takes the registration back early.
194
+ */
195
+ const registerTrigger = (
196
+ popover: UiPopover,
197
+ trigger: PopoverAnchor,
198
+ glass: UiGlass | null,
199
+ destroyRef: DestroyRef,
200
+ ): (() => void) => {
201
+ popover.setTrigger(trigger);
202
+ glass?.pair(popover.glassId);
203
+ return destroyRef.onDestroy(() => {
204
+ popover.setTrigger(null);
205
+ glass?.pair(null);
206
+ });
207
+ };
@@ -2,6 +2,7 @@ import {
2
2
  type ElementRef,
3
3
  Component,
4
4
  ViewEncapsulation,
5
+ booleanAttribute,
5
6
  computed,
6
7
  effect,
7
8
  input,
@@ -29,7 +30,7 @@ export class UiProgress {
29
30
  /**
30
31
  * When true, shows an indeterminate (looping) animation
31
32
  */
32
- readonly indeterminate = input(false);
33
+ readonly indeterminate = input(false, { transform: booleanAttribute });
33
34
  readonly userClass = input<string>('', { alias: 'class' });
34
35
 
35
36
  readonly fillRef = viewChild.required<ElementRef>('fill');
@@ -2,6 +2,7 @@ import {
2
2
  type ElementRef,
3
3
  Component,
4
4
  ViewEncapsulation,
5
+ booleanAttribute,
5
6
  computed,
6
7
  effect,
7
8
  inject,
@@ -28,7 +29,7 @@ import { cn } from '../../utils/cn';
28
29
  })
29
30
  export class UiRadioGroup {
30
31
  readonly value = model<string>('');
31
- readonly disabled = input(false);
32
+ readonly disabled = input(false, { transform: booleanAttribute });
32
33
  readonly userClass = input<string>('', { alias: 'class' });
33
34
 
34
35
  readonly changed = output<string>();
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  Component,
3
3
  ViewEncapsulation,
4
+ booleanAttribute,
4
5
  computed,
5
6
  input,
6
7
  output,
@@ -40,9 +41,9 @@ import { cn } from '../../utils/cn';
40
41
  export class UiScrollArea {
41
42
  readonly orientation = input<'vertical' | 'horizontal'>('vertical');
42
43
  readonly height = input<string>('');
43
- readonly bounces = input(true);
44
- readonly scrollBar = input(true);
45
- readonly nestedScroll = input(false);
44
+ readonly bounces = input(true, { transform: booleanAttribute });
45
+ readonly scrollBar = input(true, { transform: booleanAttribute });
46
+ readonly nestedScroll = input(false, { transform: booleanAttribute });
46
47
  readonly upperThreshold = input(0);
47
48
  readonly lowerThreshold = input(0);
48
49
  readonly userClass = input<string>('', { alias: 'class' });
@@ -0,0 +1,15 @@
1
+ export {
2
+ UI_SEARCH_FIELD_CONTAINER,
3
+ UiSearchField,
4
+ type SearchFieldContainer,
5
+ } from './search-field';
6
+ export {
7
+ KEYBOARD_GAP,
8
+ keyboardHeightOf,
9
+ keyboardLift,
10
+ keyboardScreen,
11
+ measureForKeyboard,
12
+ type KeyboardHeightEvent,
13
+ type KeyboardRect,
14
+ type KeyboardScreen,
15
+ } from './keyboard';