@blotch/dolan 22.2.4 → 22.2.6
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.
- package/dist/commands/add.test.js +2 -0
- package/dist/commands/add.test.js.map +1 -1
- package/dist/commands/doctor.js +78 -21
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.js +167 -26
- package/dist/commands/doctor.test.js.map +1 -1
- package/dist/commands/init.test.js +4 -7
- package/dist/commands/init.test.js.map +1 -1
- package/dist/commands/update.d.ts +3 -1
- package/dist/commands/update.js +78 -15
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/update.test.js +257 -2
- package/dist/commands/update.test.js.map +1 -1
- package/dist/lockfile.d.ts +10 -0
- package/dist/lockfile.js +20 -1
- package/dist/lockfile.js.map +1 -1
- package/dist/lockfile.test.js +43 -3
- package/dist/lockfile.test.js.map +1 -1
- package/dist/registry.js +73 -11
- package/dist/registry.js.map +1 -1
- package/dist/registry.test.js +16 -3
- package/dist/registry.test.js.map +1 -1
- package/dist/test-utils.d.ts +24 -2
- package/dist/test-utils.js +41 -2
- package/dist/test-utils.js.map +1 -1
- package/dist/ui/boolean-inputs.test.ts +76 -0
- package/dist/ui/components/action-sheet/action-sheet.test.ts +370 -1
- package/dist/ui/components/action-sheet/action-sheet.ts +306 -28
- package/dist/ui/components/action-sheet/index.ts +1 -0
- package/dist/ui/components/alert-dialog/alert-dialog.test.ts +552 -8
- package/dist/ui/components/alert-dialog/alert-dialog.ts +644 -36
- package/dist/ui/components/alert-dialog/index.ts +5 -0
- package/dist/ui/components/area-chart/area-chart.ts +3 -2
- package/dist/ui/components/badge/badge.ts +2 -1
- package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +968 -3
- package/dist/ui/components/bottom-sheet/bottom-sheet.ts +1021 -95
- package/dist/ui/components/bottom-sheet/index.ts +7 -1
- package/dist/ui/components/button/button.test.ts +36 -1
- package/dist/ui/components/button/button.ts +17 -7
- package/dist/ui/components/button-group/button-group.test.ts +53 -9
- package/dist/ui/components/button-group/button-group.ts +67 -21
- package/dist/ui/components/card/card.test.ts +16 -0
- package/dist/ui/components/card/card.ts +2 -1
- package/dist/ui/components/chart/chart-presets.ts +6 -6
- package/dist/ui/components/chart/chart.test.ts +14 -0
- package/dist/ui/components/chart/chart.ts +10 -3
- package/dist/ui/components/checkbox/checkbox.ts +2 -1
- package/dist/ui/components/collapsible/collapsible.ts +2 -1
- package/dist/ui/components/dialog/dialog.ts +6 -6
- package/dist/ui/components/dock/dock-scroll.ts +9 -0
- package/dist/ui/components/dock/dock-search.test.ts +594 -0
- package/dist/ui/components/dock/dock.test.ts +4 -1
- package/dist/ui/components/dock/dock.ts +529 -51
- package/dist/ui/components/dynamic-color/dynamic-color.test.ts +144 -0
- package/dist/ui/components/dynamic-color/dynamic-color.ts +146 -0
- package/dist/ui/components/dynamic-color/index.ts +5 -0
- package/dist/ui/components/glass/glass.test.ts +39 -1
- package/dist/ui/components/glass/glass.ts +87 -17
- package/dist/ui/components/glass/index.ts +1 -0
- package/dist/ui/components/icon/icon-theme.test.ts +28 -0
- package/dist/ui/components/icon/icon-theme.ts +38 -7
- package/dist/ui/components/input/input.ts +2 -1
- package/dist/ui/components/label/label.ts +8 -2
- package/dist/ui/components/line-chart/line-chart.ts +3 -2
- package/dist/ui/components/list/list.ts +5 -4
- package/dist/ui/components/menu/menu-popup.test.ts +18 -14
- package/dist/ui/components/menu/menu-popup.ts +24 -3
- package/dist/ui/components/menu/menu.test.ts +99 -5
- package/dist/ui/components/menu/menu.ts +74 -34
- package/dist/ui/components/nav-drawer/nav-drawer.ts +3 -2
- package/dist/ui/components/navigation-bar/index.ts +1 -0
- package/dist/ui/components/navigation-bar/navigation-bar.test.ts +425 -0
- package/dist/ui/components/navigation-bar/navigation-bar.ts +411 -0
- package/dist/ui/components/popover/index.ts +9 -0
- package/dist/ui/components/popover/popover-panel.test.ts +735 -0
- package/dist/ui/components/popover/popover-panel.ts +603 -0
- package/dist/ui/components/popover/popover.test.ts +286 -0
- package/dist/ui/components/popover/popover.ts +207 -0
- package/dist/ui/components/progress/progress.ts +2 -1
- package/dist/ui/components/radio-group/radio-group.ts +2 -1
- package/dist/ui/components/scroll-area/scroll-area.ts +4 -3
- package/dist/ui/components/search-field/index.ts +15 -0
- package/dist/ui/components/search-field/keyboard.test.ts +147 -0
- package/dist/ui/components/search-field/keyboard.ts +123 -0
- package/dist/ui/components/search-field/search-field.test.ts +549 -0
- package/dist/ui/components/search-field/search-field.ts +628 -0
- package/dist/ui/components/segmented-control/index.ts +8 -0
- package/dist/ui/components/segmented-control/segmented-control.test.ts +861 -0
- package/dist/ui/components/segmented-control/segmented-control.ts +884 -0
- package/dist/ui/components/select/select.ts +2 -1
- package/dist/ui/components/sheet/sheet.test.ts +90 -3
- package/dist/ui/components/sheet/sheet.ts +59 -4
- package/dist/ui/components/slider/index.ts +1 -0
- package/dist/ui/components/slider/slider.test.ts +695 -0
- package/dist/ui/components/slider/slider.ts +901 -0
- package/dist/ui/components/switch/index.ts +1 -1
- package/dist/ui/components/switch/switch.test.ts +464 -48
- package/dist/ui/components/switch/switch.ts +613 -43
- package/dist/ui/components/tabs/index.ts +7 -1
- package/dist/ui/components/tabs/tabs.test.ts +278 -2
- package/dist/ui/components/tabs/tabs.ts +233 -30
- package/dist/ui/components/textarea/textarea.ts +2 -1
- package/dist/ui/components/toggle/index.ts +1 -0
- package/dist/ui/components/toggle/toggle.test.ts +310 -4
- package/dist/ui/components/toggle/toggle.ts +241 -26
- package/dist/ui/components/toolbar/bar-scroll.test.ts +310 -0
- package/dist/ui/components/toolbar/bar-scroll.ts +372 -0
- package/dist/ui/components/toolbar/index.ts +12 -0
- package/dist/ui/components/toolbar/toolbar.test.ts +438 -0
- package/dist/ui/components/toolbar/toolbar.ts +535 -0
- package/dist/utils/analyze.d.ts +10 -5
- package/dist/utils/analyze.js +3 -2
- package/dist/utils/analyze.js.map +1 -1
- package/dist/utils/installed.d.ts +13 -1
- package/dist/utils/installed.js +17 -0
- package/dist/utils/installed.js.map +1 -1
- package/dist/utils/installed.test.js +39 -1
- package/dist/utils/installed.test.js.map +1 -1
- package/dist/utils/tailwind.d.ts +25 -5
- package/dist/utils/tailwind.js +53 -14
- package/dist/utils/tailwind.js.map +1 -1
- package/dist/utils/tailwind.test.js +85 -22
- package/dist/utils/tailwind.test.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,17 +1,21 @@
|
|
|
1
|
+
import { NgTemplateOutlet } from '@angular/common';
|
|
1
2
|
import {
|
|
2
|
-
type ElementRef,
|
|
3
3
|
type OnDestroy,
|
|
4
4
|
Component,
|
|
5
|
+
ElementRef,
|
|
5
6
|
ViewEncapsulation,
|
|
7
|
+
booleanAttribute,
|
|
6
8
|
computed,
|
|
9
|
+
contentChildren,
|
|
7
10
|
effect,
|
|
11
|
+
inject,
|
|
8
12
|
input,
|
|
9
13
|
model,
|
|
10
14
|
output,
|
|
11
15
|
signal,
|
|
12
16
|
viewChild,
|
|
13
17
|
} from '@angular/core';
|
|
14
|
-
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
18
|
+
import { LYNX_ELEMENTS, LynxAccessibilitySettings } from '@blotch/angular-lynx';
|
|
15
19
|
|
|
16
20
|
import {
|
|
17
21
|
type AnimationHandle,
|
|
@@ -24,39 +28,245 @@ import {
|
|
|
24
28
|
scaleOut,
|
|
25
29
|
} from '../../utils/animate';
|
|
26
30
|
import { cn } from '../../utils/cn';
|
|
31
|
+
import { UiGlass } from '../glass/glass';
|
|
32
|
+
import { type GlassDesign, injectGlassDesign } from '../glass/glass-design';
|
|
27
33
|
|
|
34
|
+
/**
|
|
35
|
+
* `'default'` is the alert dialog's own panel on every platform. `'glass'`
|
|
36
|
+
* draws the platform's alert: Liquid Glass on iOS 26 and later, the frosted
|
|
37
|
+
* alert of iOS 15 to 25, and Material 3's dialog on Android (see
|
|
38
|
+
* GlassDesign).
|
|
39
|
+
*/
|
|
40
|
+
export type AlertDialogVariant = 'default' | 'glass';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* What the alert dialog draws: its own panel, or the glass variant's form on
|
|
44
|
+
* this platform.
|
|
45
|
+
*/
|
|
46
|
+
export type AlertDialogLook = 'default' | GlassDesign;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* `'cancel'` backs out of the alert, `'destructive'` deletes or can't be
|
|
50
|
+
* undone, and `'default'` is any other action.
|
|
51
|
+
*/
|
|
52
|
+
export type AlertDialogActionVariant = 'default' | 'cancel' | 'destructive';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* How a glass alert's footer lays out its actions: `'auto'` side by side for
|
|
56
|
+
* up to two and stacked for more, as iOS does, or always `'horizontal'` or
|
|
57
|
+
* `'vertical'`, for labels too long to sit side by side.
|
|
58
|
+
*/
|
|
59
|
+
export type AlertDialogFooterOrientation = 'auto' | 'horizontal' | 'vertical';
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* How a panel moves: the scale and offset it opens from and the curve it
|
|
63
|
+
* settles by, and the scale and offset it fades out to.
|
|
64
|
+
*/
|
|
65
|
+
type AlertMotion = {
|
|
66
|
+
readonly fromScale: number;
|
|
67
|
+
readonly fromY: number;
|
|
68
|
+
readonly easing: string;
|
|
69
|
+
readonly toScale: number;
|
|
70
|
+
readonly toY: number;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** The default panel rises and grows in with a slight overshoot. */
|
|
74
|
+
const DEFAULT_MOTION: AlertMotion = {
|
|
75
|
+
fromScale: SCALE.dialogFrom,
|
|
76
|
+
fromY: 10,
|
|
77
|
+
easing: EASING.springSubtle,
|
|
78
|
+
toScale: SCALE.dialogTo,
|
|
79
|
+
toY: 10,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Apple's alerts shrink into place from a little larger as they fade in, and
|
|
84
|
+
* only fade as they go (frames of UIKit's own, slowed down).
|
|
85
|
+
*/
|
|
86
|
+
const APPLE_MOTION: AlertMotion = {
|
|
87
|
+
fromScale: 1.15,
|
|
88
|
+
fromY: 0,
|
|
89
|
+
easing: EASING.sheet,
|
|
90
|
+
toScale: 1,
|
|
91
|
+
toY: 0,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** How each design draws the glass variant's panel. */
|
|
95
|
+
type AlertMetrics = {
|
|
96
|
+
/** The panel's corner radius, in px. */
|
|
97
|
+
readonly radius: number;
|
|
98
|
+
/** The panel's width. */
|
|
99
|
+
readonly width: string;
|
|
100
|
+
/** The space around its content. */
|
|
101
|
+
readonly padding: string;
|
|
102
|
+
readonly motion: AlertMotion;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Measured from UIKit's alerts on the iOS 27 Simulator, Apple's earlier
|
|
107
|
+
* alerts, and Material 3's dialog spec.
|
|
108
|
+
*/
|
|
109
|
+
const METRICS: Readonly<Record<GlassDesign, AlertMetrics>> = {
|
|
110
|
+
// iOS 26's alert: 320pt wide, with its 48pt capsule buttons inset 16pt
|
|
111
|
+
// from the edge, so the corners are concentric with theirs (24 + 16).
|
|
112
|
+
glass: {
|
|
113
|
+
radius: 40,
|
|
114
|
+
width: 'w-full max-w-[320px]',
|
|
115
|
+
padding: 'px-[16px] pb-[16px] pt-[22px]',
|
|
116
|
+
motion: APPLE_MOTION,
|
|
117
|
+
},
|
|
118
|
+
// iOS 15 to 25's alert: 270pt wide, 14pt corners, its buttons edge to
|
|
119
|
+
// edge.
|
|
120
|
+
frosted: {
|
|
121
|
+
radius: 14,
|
|
122
|
+
width: 'w-[270px]',
|
|
123
|
+
padding: '',
|
|
124
|
+
motion: APPLE_MOTION,
|
|
125
|
+
},
|
|
126
|
+
// Material 3's basic dialog: 280 to 560dp wide, 28dp corners, 24dp
|
|
127
|
+
// padding. It grows in from a little smaller, and shrinks back as it fades.
|
|
128
|
+
material: {
|
|
129
|
+
radius: 28,
|
|
130
|
+
width: 'w-full min-w-[280px] max-w-[560px]',
|
|
131
|
+
padding: 'p-[24px]',
|
|
132
|
+
motion: {
|
|
133
|
+
fromScale: SCALE.dialogFrom,
|
|
134
|
+
fromY: 0,
|
|
135
|
+
easing: EASING.decelerate,
|
|
136
|
+
toScale: SCALE.dialogTo,
|
|
137
|
+
toY: 0,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* An alert dialog: a panel that asks the user to confirm or cancel an
|
|
144
|
+
* important action, and closes only when they choose one.
|
|
145
|
+
*
|
|
146
|
+
* ```html
|
|
147
|
+
* <ui-alert-dialog variant="glass" [(open)]="open">
|
|
148
|
+
* <ui-alert-dialog-header>
|
|
149
|
+
* <ui-alert-dialog-title>Delete Account?</ui-alert-dialog-title>
|
|
150
|
+
* <ui-alert-dialog-description>This can't be undone.</ui-alert-dialog-description>
|
|
151
|
+
* </ui-alert-dialog-header>
|
|
152
|
+
* <ui-alert-dialog-footer>
|
|
153
|
+
* <ui-alert-dialog-action variant="cancel">Cancel</ui-alert-dialog-action>
|
|
154
|
+
* <ui-alert-dialog-action variant="destructive" (pressed)="delete()">
|
|
155
|
+
* Delete
|
|
156
|
+
* </ui-alert-dialog-action>
|
|
157
|
+
* </ui-alert-dialog-footer>
|
|
158
|
+
* </ui-alert-dialog>
|
|
159
|
+
* ```
|
|
160
|
+
*
|
|
161
|
+
* With `variant="glass"` it draws the platform's own alert (see
|
|
162
|
+
* AlertDialogVariant), on a `<blur-view ui-glass>`. A blur view can't fade
|
|
163
|
+
* itself: on iOS its material vanishes at less than full opacity. So the
|
|
164
|
+
* glass sits in a plain view that scales and fades, under which iOS draws
|
|
165
|
+
* it as a translucent sheet until it's opaque again, as UIKit draws its own
|
|
166
|
+
* alerts coming and going. The screen's dimming is a layer beside it rather
|
|
167
|
+
* than around it, so it fades on its own.
|
|
168
|
+
*
|
|
169
|
+
* Under Reduce Motion nothing scales, in either variant: the dimming and the
|
|
170
|
+
* panel only fade.
|
|
171
|
+
*/
|
|
28
172
|
@Component({
|
|
29
173
|
selector: 'ui-alert-dialog',
|
|
30
|
-
imports: [LYNX_ELEMENTS],
|
|
174
|
+
imports: [LYNX_ELEMENTS, NgTemplateOutlet, UiGlass],
|
|
31
175
|
encapsulation: ViewEncapsulation.None,
|
|
32
176
|
template: `
|
|
33
177
|
<overlay [attr.visible]="overlayVisible()" [style]="overlayStyle()">
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
178
|
+
<!-- The overlay sizes only its first child, to the screen. With the
|
|
179
|
+
@if straight inside it, the glass panel drew in the top-left
|
|
180
|
+
corner on iOS (see investigations/lynx-vs-web-differences.md). -->
|
|
181
|
+
<view class="h-full w-full">
|
|
182
|
+
@if (look() === 'default') {
|
|
183
|
+
<view #backdrop [class]="backdropClass()">
|
|
184
|
+
<view
|
|
185
|
+
#panel
|
|
186
|
+
[class]="panelClass()"
|
|
187
|
+
[accessibility-exclusive-focus]="overlayVisible()"
|
|
188
|
+
(catchtap)="onPanelTap()"
|
|
189
|
+
>
|
|
190
|
+
<ng-container [ngTemplateOutlet]="content" />
|
|
191
|
+
</view>
|
|
192
|
+
</view>
|
|
193
|
+
} @else {
|
|
194
|
+
<view
|
|
195
|
+
class="h-full w-full items-center px-[40px] flex justify-center"
|
|
196
|
+
>
|
|
197
|
+
<view #backdrop [class]="dimClass()"></view>
|
|
198
|
+
<!-- What scales and fades: see the class's comment. A view of its
|
|
199
|
+
own (not flattened into its parent): on Android, the shadow
|
|
200
|
+
of the Material panel inside an animated flattened view drew
|
|
201
|
+
at twice the view's offset, off at the screen's corner. -->
|
|
202
|
+
<view
|
|
203
|
+
#shell
|
|
204
|
+
class="w-full flex-col items-center flex"
|
|
205
|
+
[flatten]="false"
|
|
206
|
+
>
|
|
207
|
+
<blur-view
|
|
208
|
+
#panel
|
|
209
|
+
ui-glass
|
|
210
|
+
shape="rounded"
|
|
211
|
+
[radius]="metrics().radius"
|
|
212
|
+
[class]="panelClass()"
|
|
213
|
+
[accessibility-exclusive-focus]="overlayVisible()"
|
|
214
|
+
(catchtap)="onPanelTap()"
|
|
215
|
+
>
|
|
216
|
+
<ng-container [ngTemplateOutlet]="content" />
|
|
217
|
+
</blur-view>
|
|
218
|
+
</view>
|
|
219
|
+
</view>
|
|
220
|
+
}
|
|
43
221
|
</view>
|
|
44
222
|
</overlay>
|
|
223
|
+
|
|
224
|
+
<!-- One copy of the content, for whichever panel the dialog draws:
|
|
225
|
+
Angular projects content once, so it can't be in both branches. -->
|
|
226
|
+
<ng-template #content><ng-content /></ng-template>
|
|
45
227
|
`,
|
|
46
228
|
})
|
|
47
229
|
export class UiAlertDialog implements OnDestroy {
|
|
48
230
|
readonly open = model(false);
|
|
231
|
+
/** `'default'` or `'glass'`; see AlertDialogVariant. */
|
|
232
|
+
readonly variant = input<AlertDialogVariant>('default');
|
|
49
233
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
50
234
|
|
|
51
235
|
readonly closed = output<void>();
|
|
52
236
|
|
|
53
237
|
protected readonly overlayVisible = signal(false);
|
|
54
238
|
|
|
55
|
-
|
|
239
|
+
readonly #design = injectGlassDesign();
|
|
240
|
+
readonly #settings = inject(LynxAccessibilitySettings);
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* What the dialog draws (see AlertDialogLook), which its header, title,
|
|
244
|
+
* description, footer and actions follow.
|
|
245
|
+
*/
|
|
246
|
+
readonly look = computed<AlertDialogLook>(() =>
|
|
247
|
+
this.variant() === 'glass' ? this.#design() : 'default',
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
/** How the glass variant draws its panel on this platform. */
|
|
251
|
+
readonly metrics = computed(() => METRICS[this.#design()]);
|
|
252
|
+
|
|
253
|
+
/** How the panel moves as it opens and closes. */
|
|
254
|
+
readonly #motion = computed(() =>
|
|
255
|
+
this.look() === 'default' ? DEFAULT_MOTION : this.metrics().motion,
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
// Required: they're always in the template (the overlay hides with
|
|
56
259
|
// display:none rather than leaving the tree), and the sequences only read
|
|
57
|
-
// them from timers that start after the first render.
|
|
260
|
+
// them from timers that start after the first render. The panel is read as
|
|
261
|
+
// an element: in the glass variant its tag holds the glass component.
|
|
58
262
|
readonly backdropRef = viewChild.required<ElementRef>('backdrop');
|
|
59
|
-
readonly panelRef = viewChild.required
|
|
263
|
+
readonly panelRef = viewChild.required('panel', { read: ElementRef });
|
|
264
|
+
/**
|
|
265
|
+
* What scales and fades in the glass variant: the view the glass sits in
|
|
266
|
+
* (see the class's comment). Only read in that variant, where it's in the
|
|
267
|
+
* template.
|
|
268
|
+
*/
|
|
269
|
+
readonly shellRef = viewChild.required<ElementRef>('shell');
|
|
60
270
|
#backdropAnim?: AnimationHandle;
|
|
61
271
|
#panelAnim?: AnimationHandle;
|
|
62
272
|
#hasBeenOpen = false;
|
|
@@ -107,13 +317,36 @@ export class UiAlertDialog implements OnDestroy {
|
|
|
107
317
|
cn('items-center flex justify-center', 'h-full w-full', 'bg-black/50'),
|
|
108
318
|
);
|
|
109
319
|
|
|
110
|
-
|
|
320
|
+
/**
|
|
321
|
+
* The glass variant dims the screen as its platform does: black at 20%
|
|
322
|
+
* behind Apple's alerts (measured on the iOS 27 Simulator), and Material
|
|
323
|
+
* 3's scrim at 32%. Only a dimming layer, with no blur: neither platform
|
|
324
|
+
* blurs the screen behind an alert, and a plain color draws the same on
|
|
325
|
+
* iOS, Android and the web. (Tailwind's `backdrop-blur-*` would draw
|
|
326
|
+
* nothing there: Lynx has no `backdrop-filter`, and the Lynx preset doesn't
|
|
327
|
+
* generate the classes.)
|
|
328
|
+
*/
|
|
329
|
+
protected readonly dimClass = computed(() =>
|
|
111
330
|
cn(
|
|
112
|
-
'
|
|
113
|
-
this.
|
|
331
|
+
'h-full w-full absolute left-0 top-0',
|
|
332
|
+
this.look() === 'material' ? 'bg-black/[0.32]' : 'bg-black/20',
|
|
114
333
|
),
|
|
115
334
|
);
|
|
116
335
|
|
|
336
|
+
protected readonly panelClass = computed(() =>
|
|
337
|
+
this.look() === 'default'
|
|
338
|
+
? cn(
|
|
339
|
+
'w-4/5 flex-col rounded-lg border border-border bg-background p-6 flex',
|
|
340
|
+
this.userClass(),
|
|
341
|
+
)
|
|
342
|
+
: cn(
|
|
343
|
+
'flex-col flex overflow-hidden',
|
|
344
|
+
this.metrics().width,
|
|
345
|
+
this.metrics().padding,
|
|
346
|
+
this.userClass(),
|
|
347
|
+
),
|
|
348
|
+
);
|
|
349
|
+
|
|
117
350
|
/**
|
|
118
351
|
* No-op tap handler for the panel. `catchtap` (vs `bindtap`) already stops
|
|
119
352
|
* the tap from bubbling to the backdrop — Lynx controls propagation via the
|
|
@@ -188,33 +421,56 @@ export class UiAlertDialog implements OnDestroy {
|
|
|
188
421
|
// feels like it's emerging from behind the dimmed overlay rather than
|
|
189
422
|
// appearing simultaneously with it.
|
|
190
423
|
this.#later(() => {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
424
|
+
const motion = this.#motion();
|
|
425
|
+
this.#panelAnim = this.#settings.reduceMotion()
|
|
426
|
+
? fadeIn(this.#animated(), { duration: DURATION.normal })
|
|
427
|
+
: scaleIn(this.#animated(), {
|
|
428
|
+
duration: DURATION.slow,
|
|
429
|
+
easing: motion.easing,
|
|
430
|
+
fromScale: motion.fromScale,
|
|
431
|
+
fromY: motion.fromY,
|
|
432
|
+
});
|
|
197
433
|
}, 30);
|
|
198
434
|
}
|
|
199
435
|
|
|
200
436
|
#animateOut(): void {
|
|
201
437
|
const backdrop = this.backdropRef().nativeElement;
|
|
202
|
-
const
|
|
438
|
+
const motion = this.#motion();
|
|
203
439
|
|
|
204
440
|
this.#backdropAnim?.cancel();
|
|
205
441
|
this.#panelAnim?.cancel();
|
|
206
442
|
|
|
207
|
-
this.#panelAnim =
|
|
208
|
-
duration: 180
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
443
|
+
this.#panelAnim = this.#settings.reduceMotion()
|
|
444
|
+
? fadeOut(this.#animated(), { duration: 180 })
|
|
445
|
+
: scaleOut(this.#animated(), {
|
|
446
|
+
duration: 180,
|
|
447
|
+
easing: EASING.accelerate,
|
|
448
|
+
toScale: motion.toScale,
|
|
449
|
+
toY: motion.toY,
|
|
450
|
+
});
|
|
213
451
|
|
|
214
452
|
this.#backdropAnim = fadeOut(backdrop, { duration: DURATION.normal });
|
|
215
453
|
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* What the open and close animations scale and fade: the panel, or in the
|
|
457
|
+
* glass variant the view the glass sits in (see the class's comment).
|
|
458
|
+
*/
|
|
459
|
+
#animated(): unknown {
|
|
460
|
+
return this.look() === 'default'
|
|
461
|
+
? this.panelRef().nativeElement
|
|
462
|
+
: this.shellRef().nativeElement;
|
|
463
|
+
}
|
|
216
464
|
}
|
|
217
465
|
|
|
466
|
+
/**
|
|
467
|
+
* The look of the alert dialog a part is in; `'default'` outside one.
|
|
468
|
+
*/
|
|
469
|
+
const injectAlertLook = () => {
|
|
470
|
+
const dialog = inject(UiAlertDialog, { optional: true });
|
|
471
|
+
return computed<AlertDialogLook>(() => dialog?.look() ?? 'default');
|
|
472
|
+
};
|
|
473
|
+
|
|
218
474
|
@Component({
|
|
219
475
|
selector: 'ui-alert-dialog-header',
|
|
220
476
|
imports: [LYNX_ELEMENTS],
|
|
@@ -228,8 +484,24 @@ export class UiAlertDialog implements OnDestroy {
|
|
|
228
484
|
export class UiAlertDialogHeader {
|
|
229
485
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
230
486
|
|
|
487
|
+
readonly #look = injectAlertLook();
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* iOS 26 sets its text 30pt from the panel's edge, further in than its
|
|
491
|
+
* buttons; before iOS 26 Apple centered it, with room above and below
|
|
492
|
+
* before the separator over the buttons.
|
|
493
|
+
*/
|
|
231
494
|
protected readonly containerClass = computed(() =>
|
|
232
|
-
cn(
|
|
495
|
+
cn(
|
|
496
|
+
'flex-col flex',
|
|
497
|
+
{
|
|
498
|
+
default: 'gap-1.5',
|
|
499
|
+
glass: 'gap-[5px] px-[14px]',
|
|
500
|
+
frosted: 'items-center gap-[2px] px-[16px] pb-[20px] pt-[20px]',
|
|
501
|
+
material: 'gap-[16px]',
|
|
502
|
+
}[this.#look()],
|
|
503
|
+
this.userClass(),
|
|
504
|
+
),
|
|
233
505
|
);
|
|
234
506
|
}
|
|
235
507
|
|
|
@@ -242,8 +514,23 @@ export class UiAlertDialogHeader {
|
|
|
242
514
|
export class UiAlertDialogTitle {
|
|
243
515
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
244
516
|
|
|
517
|
+
readonly #look = injectAlertLook();
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Apple's alert titles are 17pt semibold, leading from iOS 26 and centered
|
|
521
|
+
* before it. Material's headline is 24sp regular.
|
|
522
|
+
*/
|
|
245
523
|
protected readonly textClass = computed(() =>
|
|
246
|
-
cn(
|
|
524
|
+
cn(
|
|
525
|
+
{
|
|
526
|
+
default: 'text-lg font-semibold text-foreground',
|
|
527
|
+
glass: 'text-[17px] font-semibold leading-[22px] text-glass-foreground',
|
|
528
|
+
frosted:
|
|
529
|
+
'text-[17px] font-semibold leading-[22px] text-glass-foreground text-center',
|
|
530
|
+
material: 'text-[24px] leading-[32px] text-glass-foreground',
|
|
531
|
+
}[this.#look()],
|
|
532
|
+
this.userClass(),
|
|
533
|
+
),
|
|
247
534
|
);
|
|
248
535
|
}
|
|
249
536
|
|
|
@@ -256,25 +543,346 @@ export class UiAlertDialogTitle {
|
|
|
256
543
|
export class UiAlertDialogDescription {
|
|
257
544
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
258
545
|
|
|
546
|
+
readonly #look = injectAlertLook();
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* iOS 26's message is 15pt in the secondary color; before it, 13pt in the
|
|
550
|
+
* label color. Material's supporting text is 14sp in on-surface-variant.
|
|
551
|
+
*/
|
|
259
552
|
protected readonly textClass = computed(() =>
|
|
260
|
-
cn(
|
|
553
|
+
cn(
|
|
554
|
+
{
|
|
555
|
+
default: 'text-sm text-muted-foreground',
|
|
556
|
+
glass: 'text-[15px] leading-[20px] text-glass-muted-foreground',
|
|
557
|
+
frosted: 'text-[13px] leading-[16px] text-glass-foreground text-center',
|
|
558
|
+
material: 'text-[14px] leading-[20px] text-glass-muted-foreground',
|
|
559
|
+
}[this.#look()],
|
|
560
|
+
this.userClass(),
|
|
561
|
+
),
|
|
261
562
|
);
|
|
262
563
|
}
|
|
263
564
|
|
|
565
|
+
/**
|
|
566
|
+
* `UiAlertDialogAction`, for `contentChildren()`. A call rather than the
|
|
567
|
+
* class itself: the query's metadata keeps an arrow function of its own for
|
|
568
|
+
* a class, which nothing calls, so an app's coverage report would show it as
|
|
569
|
+
* uncovered.
|
|
570
|
+
*/
|
|
571
|
+
const actionType = () => UiAlertDialogAction;
|
|
572
|
+
|
|
264
573
|
@Component({
|
|
265
574
|
selector: 'ui-alert-dialog-footer',
|
|
266
575
|
imports: [LYNX_ELEMENTS],
|
|
267
576
|
encapsulation: ViewEncapsulation.None,
|
|
268
577
|
template: `
|
|
269
578
|
<view [class]="containerClass()">
|
|
579
|
+
@if (separated()) {
|
|
580
|
+
<!-- The hairline over the buttons of Apple's alerts before iOS 26. -->
|
|
581
|
+
<view
|
|
582
|
+
class="h-[0.5px] w-full bg-glass-foreground opacity-20 absolute left-0 top-0"
|
|
583
|
+
></view>
|
|
584
|
+
}
|
|
270
585
|
<ng-content />
|
|
271
586
|
</view>
|
|
272
587
|
`,
|
|
273
588
|
})
|
|
274
589
|
export class UiAlertDialogFooter {
|
|
275
590
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
591
|
+
/**
|
|
592
|
+
* How a glass alert lays out its actions; see AlertDialogFooterOrientation.
|
|
593
|
+
* The default variant always puts them in a row.
|
|
594
|
+
*/
|
|
595
|
+
readonly orientation = input<AlertDialogFooterOrientation>('auto');
|
|
276
596
|
|
|
277
|
-
|
|
278
|
-
|
|
597
|
+
readonly #look = injectAlertLook();
|
|
598
|
+
|
|
599
|
+
/** The actions in the footer, in the order they're written. */
|
|
600
|
+
readonly actions = contentChildren(actionType(), { descendants: true });
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Whether the glass variant stacks the actions: Apple's alerts put two side
|
|
604
|
+
* by side and stack three. Material's dialog keeps its text buttons in a
|
|
605
|
+
* row.
|
|
606
|
+
*/
|
|
607
|
+
readonly stacked = computed(() => {
|
|
608
|
+
const orientation = this.orientation();
|
|
609
|
+
if (orientation !== 'auto') return orientation === 'vertical';
|
|
610
|
+
return this.#look() !== 'material' && this.actions().length > 2;
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* The actions in the order the glass variant shows them. Apple and Material
|
|
615
|
+
* both put the cancel action on the leading side of a row, and Apple puts
|
|
616
|
+
* it at the bottom of a stack, wherever it's written, as UIKit does.
|
|
617
|
+
*/
|
|
618
|
+
readonly order = computed(() => {
|
|
619
|
+
const actions = this.actions();
|
|
620
|
+
const cancels = actions.filter((action) => action.variant() === 'cancel');
|
|
621
|
+
const others = actions.filter((action) => action.variant() !== 'cancel');
|
|
622
|
+
return this.stacked() ? [...others, ...cancels] : [...cancels, ...others];
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
/** Whether an action is preferred, which takes the cancel's emphasis. */
|
|
626
|
+
readonly hasPreferred = computed(() =>
|
|
627
|
+
this.actions().some((action) => action.preferred()),
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
protected readonly separated = computed(() => this.#look() === 'frosted');
|
|
631
|
+
|
|
632
|
+
protected readonly containerClass = computed(() => {
|
|
633
|
+
const look = this.#look();
|
|
634
|
+
if (look === 'default') {
|
|
635
|
+
return cn('flex-row gap-2 pt-4 flex justify-end', this.userClass());
|
|
636
|
+
}
|
|
637
|
+
return cn(
|
|
638
|
+
this.stacked() ? 'flex-col' : 'flex-row',
|
|
639
|
+
'flex',
|
|
640
|
+
{
|
|
641
|
+
glass: 'mt-[20px] gap-[8px]',
|
|
642
|
+
frosted: 'relative',
|
|
643
|
+
material: 'mt-[24px] gap-[8px] justify-end',
|
|
644
|
+
}[look],
|
|
645
|
+
this.userClass(),
|
|
646
|
+
);
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* One of an alert dialog's buttons. Pressing it emits `pressed` and closes
|
|
652
|
+
* the dialog, as every alert button does.
|
|
653
|
+
*
|
|
654
|
+
* ```html
|
|
655
|
+
* <ui-alert-dialog-action variant="cancel">Cancel</ui-alert-dialog-action>
|
|
656
|
+
* <ui-alert-dialog-action variant="destructive" (pressed)="delete()">
|
|
657
|
+
* Delete
|
|
658
|
+
* </ui-alert-dialog-action>
|
|
659
|
+
* ```
|
|
660
|
+
*
|
|
661
|
+
* Put it in a `<ui-alert-dialog-footer>`. Each look draws it as its
|
|
662
|
+
* platform does:
|
|
663
|
+
*
|
|
664
|
+
* - **Default:** an outline button for cancel, a filled primary or
|
|
665
|
+
* destructive one otherwise.
|
|
666
|
+
* - **iOS 26 and later:** a 48pt capsule on a light fill, with the preferred
|
|
667
|
+
* action filled with the tint.
|
|
668
|
+
* - **iOS 15 to 25:** a 44pt text button in the tint between hairlines, the
|
|
669
|
+
* preferred action, or else the cancel, in bold.
|
|
670
|
+
* - **Material:** a text button in the primary color.
|
|
671
|
+
*
|
|
672
|
+
* Destructive actions read in the destructive color everywhere.
|
|
673
|
+
*/
|
|
674
|
+
@Component({
|
|
675
|
+
selector: 'ui-alert-dialog-action',
|
|
676
|
+
imports: [LYNX_ELEMENTS],
|
|
677
|
+
encapsulation: ViewEncapsulation.None,
|
|
678
|
+
host: {
|
|
679
|
+
'[class]': 'hostClass()',
|
|
680
|
+
'[style.order]': 'position()',
|
|
681
|
+
'[style.opacity]': 'hostOpacity()',
|
|
682
|
+
// An action with an accessibility label is one element that says it;
|
|
683
|
+
// without one, its label's own text is read, as a button (see the
|
|
684
|
+
// template), as `ui-button` does.
|
|
685
|
+
'[attr.accessibility-element]': "accessibilityLabel() ? 'true' : null",
|
|
686
|
+
'[attr.accessibility-traits]': "accessibilityLabel() ? 'button' : null",
|
|
687
|
+
'[attr.accessibility-label]': 'accessibilityLabel() || null',
|
|
688
|
+
// Touch for native, mouse for Lynx for Web, which never sends touches
|
|
689
|
+
// from a mouse (see `ui-button`).
|
|
690
|
+
'(bindtouchstart)': 'hold(true)',
|
|
691
|
+
'(bindtouchend)': 'hold(false)',
|
|
692
|
+
'(bindtouchcancel)': 'hold(false)',
|
|
693
|
+
'(bindmousedown)': 'hold(true)',
|
|
694
|
+
'(bindmouseup)': 'hold(false)',
|
|
695
|
+
'(bindmouseleave)': 'hold(false)',
|
|
696
|
+
'(bindtap)': 'choose()',
|
|
697
|
+
},
|
|
698
|
+
template: `
|
|
699
|
+
@if (layerClass(); as layer) {
|
|
700
|
+
<!-- The capsule's fill and pressed highlight, or the pressed
|
|
701
|
+
highlight alone: a view of its own, so it can be translucent
|
|
702
|
+
under an opaque label. -->
|
|
703
|
+
<view [class]="layer" [style.opacity]="layerOpacity()"></view>
|
|
704
|
+
}
|
|
705
|
+
@if (separatorClass(); as line) {
|
|
706
|
+
<view [class]="line"></view>
|
|
707
|
+
}
|
|
708
|
+
<text
|
|
709
|
+
[attr.accessibility-traits]="accessibilityLabel() ? null : 'button'"
|
|
710
|
+
[class]="labelClass()"
|
|
711
|
+
><ng-content
|
|
712
|
+
/></text>
|
|
713
|
+
`,
|
|
714
|
+
})
|
|
715
|
+
export class UiAlertDialogAction {
|
|
716
|
+
/** `'default'`, `'cancel'` or `'destructive'`; see AlertDialogActionVariant. */
|
|
717
|
+
readonly variant = input<AlertDialogActionVariant>('default');
|
|
718
|
+
/**
|
|
719
|
+
* The action to take when the user reads the alert and agrees, such as
|
|
720
|
+
* Save. iOS 26 fills it with the tint and earlier iOS sets it in bold, the
|
|
721
|
+
* way `UIAlertController.preferredAction` draws. Never the cancel action.
|
|
722
|
+
*/
|
|
723
|
+
readonly preferred = input(false, { transform: booleanAttribute });
|
|
724
|
+
/** What a screen reader reads, when the label isn't enough. */
|
|
725
|
+
readonly accessibilityLabel = input<string | undefined>(undefined);
|
|
726
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
727
|
+
|
|
728
|
+
/** The action was chosen. The dialog closes. */
|
|
729
|
+
readonly pressed = output<void>();
|
|
730
|
+
|
|
731
|
+
readonly #dialog = inject(UiAlertDialog);
|
|
732
|
+
readonly #footer = inject(UiAlertDialogFooter);
|
|
733
|
+
|
|
734
|
+
/** Whether a finger (or the mouse) is down on the action. */
|
|
735
|
+
protected readonly held = signal(false);
|
|
736
|
+
|
|
737
|
+
readonly #look = computed(() => this.#dialog.look());
|
|
738
|
+
readonly #row = computed(() => !this.#footer.stacked());
|
|
739
|
+
readonly #destructive = computed(() => this.variant() === 'destructive');
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Where the action is in its footer as the glass variant shows it (see
|
|
743
|
+
* UiAlertDialogFooter.order); the default variant keeps the written order.
|
|
744
|
+
*/
|
|
745
|
+
protected readonly position = computed(() =>
|
|
746
|
+
this.#look() === 'default' ? null : this.#footer.order().indexOf(this),
|
|
279
747
|
);
|
|
748
|
+
|
|
749
|
+
protected readonly hostClass = computed(() => {
|
|
750
|
+
const destructive = this.#destructive();
|
|
751
|
+
const row = this.#row();
|
|
752
|
+
switch (this.#look()) {
|
|
753
|
+
case 'default':
|
|
754
|
+
return cn(
|
|
755
|
+
'h-10 items-center rounded-md px-4 py-2 flex justify-center',
|
|
756
|
+
this.variant() === 'cancel'
|
|
757
|
+
? 'border border-border bg-background'
|
|
758
|
+
: destructive
|
|
759
|
+
? 'bg-destructive'
|
|
760
|
+
: 'bg-primary',
|
|
761
|
+
this.userClass(),
|
|
762
|
+
);
|
|
763
|
+
case 'glass':
|
|
764
|
+
return cn(
|
|
765
|
+
'h-[48px] items-center rounded-full px-[16px] flex relative justify-center overflow-hidden',
|
|
766
|
+
row && 'flex-1',
|
|
767
|
+
this.preferred() &&
|
|
768
|
+
(destructive ? 'bg-destructive' : 'bg-glass-tint'),
|
|
769
|
+
this.userClass(),
|
|
770
|
+
);
|
|
771
|
+
case 'frosted':
|
|
772
|
+
return cn(
|
|
773
|
+
'h-[44px] items-center px-[8px] flex relative justify-center',
|
|
774
|
+
row && 'flex-1',
|
|
775
|
+
this.userClass(),
|
|
776
|
+
);
|
|
777
|
+
default:
|
|
778
|
+
return cn(
|
|
779
|
+
'h-[40px] items-center rounded-full px-[12px] flex relative justify-center overflow-hidden',
|
|
780
|
+
this.userClass(),
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
/** The default variant's solid buttons dim while pressed. */
|
|
786
|
+
protected readonly hostOpacity = computed(() =>
|
|
787
|
+
this.#look() === 'default' && this.held() ? 0.8 : null,
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* iOS 26's capsules sit on a light fill of the glass's foreground, which
|
|
792
|
+
* deepens under the finger (a preferred capsule lightens instead); the
|
|
793
|
+
* other looks only light up while pressed: Material's state layer, in the
|
|
794
|
+
* label's color.
|
|
795
|
+
*/
|
|
796
|
+
protected readonly layerClass = computed(() => {
|
|
797
|
+
const look = this.#look();
|
|
798
|
+
if (look === 'default') return null;
|
|
799
|
+
const fill =
|
|
800
|
+
look === 'glass' && this.preferred()
|
|
801
|
+
? 'bg-glass-tint-foreground'
|
|
802
|
+
: look === 'material'
|
|
803
|
+
? this.#destructive()
|
|
804
|
+
? 'bg-destructive'
|
|
805
|
+
: 'bg-primary'
|
|
806
|
+
: 'bg-glass-foreground';
|
|
807
|
+
return cn('h-full w-full absolute left-0 top-0', fill);
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
protected readonly layerOpacity = computed(() => {
|
|
811
|
+
const held = this.held();
|
|
812
|
+
if (this.#look() === 'glass' && !this.preferred()) {
|
|
813
|
+
return held ? 0.2 : 0.12;
|
|
814
|
+
}
|
|
815
|
+
return held ? 0.1 : 0;
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* The hairline between the buttons of Apple's alerts before iOS 26: beside
|
|
820
|
+
* every button but the first in a row, and above every button but the
|
|
821
|
+
* first in a stack. The footer draws the one above them all.
|
|
822
|
+
*/
|
|
823
|
+
protected readonly separatorClass = computed(() => {
|
|
824
|
+
if (this.#look() !== 'frosted' || this.position() === 0) return null;
|
|
825
|
+
return cn(
|
|
826
|
+
'bg-glass-foreground opacity-20 absolute left-0 top-0',
|
|
827
|
+
this.#row() ? 'h-full w-[0.5px]' : 'h-[0.5px] w-full',
|
|
828
|
+
);
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
protected readonly labelClass = computed(() => {
|
|
832
|
+
const destructive = this.#destructive();
|
|
833
|
+
switch (this.#look()) {
|
|
834
|
+
case 'default':
|
|
835
|
+
return cn(
|
|
836
|
+
'text-sm font-medium',
|
|
837
|
+
this.variant() === 'cancel'
|
|
838
|
+
? 'text-foreground'
|
|
839
|
+
: destructive
|
|
840
|
+
? 'text-destructive-foreground'
|
|
841
|
+
: 'text-primary-foreground',
|
|
842
|
+
);
|
|
843
|
+
case 'glass':
|
|
844
|
+
return cn(
|
|
845
|
+
'text-[17px] text-center',
|
|
846
|
+
this.preferred()
|
|
847
|
+
? cn(
|
|
848
|
+
'font-semibold',
|
|
849
|
+
destructive
|
|
850
|
+
? 'text-destructive-foreground'
|
|
851
|
+
: 'text-glass-tint-foreground',
|
|
852
|
+
)
|
|
853
|
+
: cn(
|
|
854
|
+
'font-medium',
|
|
855
|
+
destructive ? 'text-destructive' : 'text-glass-foreground',
|
|
856
|
+
),
|
|
857
|
+
);
|
|
858
|
+
case 'frosted':
|
|
859
|
+
// UIKit sets the preferred action in bold, or else the cancel.
|
|
860
|
+
return cn(
|
|
861
|
+
'text-[17px] text-center',
|
|
862
|
+
destructive ? 'text-destructive' : 'text-glass-tint',
|
|
863
|
+
(this.preferred() ||
|
|
864
|
+
(this.variant() === 'cancel' && !this.#footer.hasPreferred())) &&
|
|
865
|
+
'font-semibold',
|
|
866
|
+
);
|
|
867
|
+
default:
|
|
868
|
+
return cn(
|
|
869
|
+
'text-[14px] font-medium text-center',
|
|
870
|
+
destructive ? 'text-destructive' : 'text-primary',
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
protected hold(down: boolean): void {
|
|
876
|
+
this.held.set(down);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Emits `pressed` and closes the dialog, once: a second tap while the
|
|
881
|
+
* dialog closes does nothing, as on iOS.
|
|
882
|
+
*/
|
|
883
|
+
protected choose(): void {
|
|
884
|
+
if (!this.#dialog.open()) return;
|
|
885
|
+
this.pressed.emit();
|
|
886
|
+
this.#dialog.open.set(false);
|
|
887
|
+
}
|
|
280
888
|
}
|