@blotch/dolan 22.2.3 → 22.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/add.js +9 -1
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/add.test.js +38 -3
- package/dist/commands/add.test.js.map +1 -1
- package/dist/commands/doctor.js +258 -21
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.js +425 -11
- package/dist/commands/doctor.test.js.map +1 -1
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.js +38 -18
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/init.test.js +105 -2
- package/dist/commands/init.test.js.map +1 -1
- package/dist/commands/theme.js +1 -1
- package/dist/commands/theme.js.map +1 -1
- package/dist/commands/update.d.ts +7 -0
- package/dist/commands/update.js +194 -50
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/update.test.js +570 -3
- package/dist/commands/update.test.js.map +1 -1
- package/dist/lockfile.d.ts +17 -0
- package/dist/lockfile.js +56 -1
- package/dist/lockfile.js.map +1 -1
- package/dist/lockfile.test.d.ts +1 -0
- package/dist/lockfile.test.js +65 -0
- package/dist/lockfile.test.js.map +1 -0
- package/dist/registry.d.ts +6 -0
- package/dist/registry.js +119 -11
- package/dist/registry.js.map +1 -1
- package/dist/registry.test.js +61 -6
- package/dist/registry.test.js.map +1 -1
- package/dist/test-utils.d.ts +27 -0
- package/dist/test-utils.js +45 -1
- package/dist/test-utils.js.map +1 -1
- package/dist/testing/fire-lynx-event.d.ts +14 -0
- package/dist/testing/fire-lynx-event.js +19 -0
- package/dist/testing/fire-lynx-event.js.map +1 -0
- package/dist/testing/fire-lynx-event.test.d.ts +1 -0
- package/dist/testing/fire-lynx-event.test.js +44 -0
- package/dist/testing/fire-lynx-event.test.js.map +1 -0
- package/dist/testing/lynx-main-thread.d.ts +22 -0
- package/dist/testing/lynx-main-thread.js +11 -0
- package/dist/testing/lynx-main-thread.js.map +1 -0
- package/dist/testing/lynx-main-thread.test.d.ts +1 -0
- package/dist/testing/lynx-main-thread.test.js +24 -0
- package/dist/testing/lynx-main-thread.test.js.map +1 -0
- package/dist/testing/set-input-signal.d.ts +5 -4
- package/dist/testing/set-input-signal.js +5 -4
- package/dist/testing/set-input-signal.js.map +1 -1
- package/dist/ui/boolean-inputs.test.ts +76 -0
- package/dist/ui/components/accordion/accordion.test.ts +158 -144
- package/dist/ui/components/accordion/accordion.ts +19 -14
- package/dist/ui/components/action-sheet/action-sheet.test.ts +637 -183
- package/dist/ui/components/action-sheet/action-sheet.ts +339 -55
- package/dist/ui/components/action-sheet/index.ts +1 -0
- package/dist/ui/components/alert/alert.test.ts +67 -19
- package/dist/ui/components/alert/alert.ts +14 -16
- package/dist/ui/components/alert-dialog/alert-dialog.test.ts +740 -129
- package/dist/ui/components/alert-dialog/alert-dialog.ts +672 -58
- package/dist/ui/components/alert-dialog/index.ts +5 -0
- package/dist/ui/components/area-chart/area-chart.test.ts +81 -1
- package/dist/ui/components/area-chart/area-chart.ts +3 -2
- package/dist/ui/components/avatar/avatar.test.ts +89 -45
- package/dist/ui/components/badge/badge.test.ts +57 -23
- package/dist/ui/components/badge/badge.ts +14 -15
- package/dist/ui/components/bar-chart/bar-chart.test.ts +82 -1
- package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +1314 -273
- package/dist/ui/components/bottom-sheet/bottom-sheet.ts +1046 -119
- package/dist/ui/components/bottom-sheet/index.ts +7 -1
- package/dist/ui/components/button/button.test.ts +646 -2
- package/dist/ui/components/button/button.ts +443 -64
- package/dist/ui/components/button/index.ts +2 -0
- package/dist/ui/components/button-group/button-group.test.ts +528 -2
- package/dist/ui/components/button-group/button-group.ts +428 -70
- package/dist/ui/components/button-group/index.ts +3 -0
- package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +82 -1
- package/dist/ui/components/card/card.test.ts +97 -61
- package/dist/ui/components/card/card.ts +2 -1
- package/dist/ui/components/chart/chart-presets.test.ts +197 -1
- package/dist/ui/components/chart/chart-presets.ts +14 -7
- package/dist/ui/components/chart/chart-theme.ts +12 -0
- package/dist/ui/components/chart/chart.test.ts +1031 -0
- package/dist/ui/components/chart/chart.ts +66 -46
- package/dist/ui/components/checkbox/checkbox.test.ts +124 -42
- package/dist/ui/components/checkbox/checkbox.ts +25 -24
- package/dist/ui/components/collapsible/collapsible.test.ts +158 -81
- package/dist/ui/components/collapsible/collapsible.ts +21 -15
- package/dist/ui/components/context-menu/context-menu.test.ts +224 -0
- package/dist/ui/components/context-menu/context-menu.ts +110 -0
- package/dist/ui/components/context-menu/index.ts +1 -0
- package/dist/ui/components/dialog/dialog.test.ts +210 -152
- package/dist/ui/components/dialog/dialog.ts +35 -29
- package/dist/ui/components/dock/dock-scroll.test.ts +130 -0
- package/dist/ui/components/dock/dock-scroll.ts +142 -0
- package/dist/ui/components/dock/dock-search.test.ts +594 -0
- package/dist/ui/components/dock/dock.test.ts +752 -0
- package/dist/ui/components/dock/dock.ts +1438 -0
- package/dist/ui/components/dock/index.ts +2 -0
- 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/empty-state/empty-state.test.ts +28 -39
- package/dist/ui/components/glass/concentric-radius.test.ts +13 -0
- package/dist/ui/components/glass/concentric-radius.ts +17 -0
- package/dist/ui/components/glass/glass-design.test.ts +78 -0
- package/dist/ui/components/glass/glass-design.ts +119 -0
- package/dist/ui/components/glass/glass.test.ts +376 -0
- package/dist/ui/components/glass/glass.ts +430 -0
- package/dist/ui/components/glass/index.ts +19 -0
- package/dist/ui/components/icon/icon-theme.test.ts +64 -0
- package/dist/ui/components/icon/icon-theme.ts +70 -16
- package/dist/ui/components/icon/icon.test.ts +52 -41
- package/dist/ui/components/input/input.test.ts +178 -101
- package/dist/ui/components/input/input.ts +17 -13
- package/dist/ui/components/label/label.test.ts +11 -24
- package/dist/ui/components/label/label.ts +8 -2
- package/dist/ui/components/line-chart/line-chart.test.ts +76 -1
- package/dist/ui/components/line-chart/line-chart.ts +3 -2
- package/dist/ui/components/list/list.test.ts +80 -49
- package/dist/ui/components/list/list.ts +5 -4
- package/dist/ui/components/menu/index.ts +25 -0
- package/dist/ui/components/menu/menu-placement.test.ts +153 -0
- package/dist/ui/components/menu/menu-placement.ts +159 -0
- package/dist/ui/components/menu/menu-popup.test.ts +636 -0
- package/dist/ui/components/menu/menu-popup.ts +565 -0
- package/dist/ui/components/menu/menu.test.ts +523 -0
- package/dist/ui/components/menu/menu.ts +455 -0
- package/dist/ui/components/nav-drawer/nav-drawer.test.ts +425 -260
- package/dist/ui/components/nav-drawer/nav-drawer.ts +75 -58
- 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/pagination/pagination.test.ts +136 -86
- 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.test.ts +102 -46
- package/dist/ui/components/progress/progress.ts +54 -55
- package/dist/ui/components/radio-group/radio-group.test.ts +128 -83
- package/dist/ui/components/radio-group/radio-group.ts +26 -25
- package/dist/ui/components/scatter-chart/scatter-chart.test.ts +74 -1
- package/dist/ui/components/scroll-area/scroll-area.test.ts +72 -36
- 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.test.ts +138 -78
- package/dist/ui/components/select/select.ts +19 -12
- package/dist/ui/components/separator/separator.test.ts +18 -17
- package/dist/ui/components/sheet/sheet.test.ts +151 -11
- package/dist/ui/components/sheet/sheet.ts +68 -5
- package/dist/ui/components/skeleton/skeleton.test.ts +24 -7
- package/dist/ui/components/skeleton/skeleton.ts +10 -16
- 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/spinner/spinner.test.ts +64 -46
- package/dist/ui/components/spinner/spinner.ts +16 -19
- package/dist/ui/components/switch/index.ts +1 -1
- package/dist/ui/components/switch/switch.test.ts +534 -48
- package/dist/ui/components/switch/switch.ts +613 -48
- package/dist/ui/components/tab-bar/index.ts +1 -0
- package/dist/ui/components/tab-bar/tab-bar.test.ts +280 -0
- package/dist/ui/components/tab-bar/tab-bar.ts +230 -0
- package/dist/ui/components/tabs/index.ts +7 -1
- package/dist/ui/components/tabs/tabs.test.ts +484 -150
- package/dist/ui/components/tabs/tabs.ts +344 -149
- package/dist/ui/components/textarea/textarea.test.ts +206 -100
- package/dist/ui/components/textarea/textarea.ts +16 -13
- package/dist/ui/components/toast/toast.test.ts +434 -350
- package/dist/ui/components/toast/toast.ts +42 -51
- package/dist/ui/components/toggle/index.ts +1 -0
- package/dist/ui/components/toggle/toggle.test.ts +559 -146
- 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/ui/theme/chart-colors.test.ts +8 -3
- package/dist/ui/theme/dark.css +38 -2
- package/dist/ui/theme/default.css +55 -7
- package/dist/ui/theme/icon-colors.test.ts +8 -3
- package/dist/ui/theme/rgb-channels.test.ts +84 -0
- package/dist/ui/theme/tailwind-build.test.ts +114 -0
- package/dist/ui/theme/tailwind-css.test.ts +83 -0
- package/dist/ui/theme/tailwind-plugin.test.ts +42 -2
- package/dist/ui/theme/tailwind-plugin.ts +93 -29
- package/dist/ui/theme/tailwind.css +113 -0
- package/dist/ui/utils/animate.test.ts +13 -1
- package/dist/utils/analyze.d.ts +19 -0
- package/dist/utils/analyze.js +16 -0
- package/dist/utils/analyze.js.map +1 -1
- package/dist/utils/analyze.test.js +14 -1
- package/dist/utils/analyze.test.js.map +1 -1
- package/dist/utils/installed.d.ts +25 -1
- package/dist/utils/installed.js +33 -0
- package/dist/utils/installed.js.map +1 -1
- package/dist/utils/installed.test.js +55 -1
- package/dist/utils/installed.test.js.map +1 -1
- package/dist/utils/resolve-paths.d.ts +4 -4
- package/dist/utils/resolve-paths.js +5 -7
- package/dist/utils/resolve-paths.js.map +1 -1
- package/dist/utils/resolve-paths.test.js +9 -5
- package/dist/utils/resolve-paths.test.js.map +1 -1
- package/dist/utils/tailwind.d.ts +45 -0
- package/dist/utils/tailwind.js +90 -0
- package/dist/utils/tailwind.js.map +1 -0
- package/dist/utils/tailwind.test.d.ts +1 -0
- package/dist/utils/tailwind.test.js +134 -0
- package/dist/utils/tailwind.test.js.map +1 -0
- package/package.json +9 -1
- package/dist/ui/components/area-chart/area-chart-render.test.ts +0 -63
- package/dist/ui/components/bar-chart/bar-chart-render.test.ts +0 -58
- package/dist/ui/components/button/button-render.test.ts +0 -132
- package/dist/ui/components/button-group/button-group-render.test.ts +0 -146
- package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +0 -59
- package/dist/ui/components/chart/chart-render.test.ts +0 -730
- package/dist/ui/components/line-chart/line-chart-render.test.ts +0 -83
- package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +0 -64
|
@@ -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,36 +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
|
-
readonly
|
|
56
|
-
readonly
|
|
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
|
|
259
|
+
// display:none rather than leaving the tree), and the sequences only read
|
|
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.
|
|
262
|
+
readonly backdropRef = viewChild.required<ElementRef>('backdrop');
|
|
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');
|
|
57
270
|
#backdropAnim?: AnimationHandle;
|
|
58
271
|
#panelAnim?: AnimationHandle;
|
|
59
272
|
#hasBeenOpen = false;
|
|
@@ -61,21 +274,26 @@ export class UiAlertDialog implements OnDestroy {
|
|
|
61
274
|
// them.
|
|
62
275
|
readonly #timers = new Set<ReturnType<typeof setTimeout>>();
|
|
63
276
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
277
|
+
/**
|
|
278
|
+
* Plays the open or close sequence whenever `open` changes. A field rather
|
|
279
|
+
* than a constructor: Angular's test build keeps a function for a
|
|
280
|
+
* constructor that nothing calls, which an app's coverage report would show
|
|
281
|
+
* as uncovered.
|
|
282
|
+
*
|
|
283
|
+
* `#hasBeenOpen` prevents the close animation from running on the initial
|
|
284
|
+
* effect evaluation when `open` starts as false. Without this guard, the
|
|
285
|
+
* first run would call #doClose() immediately and emit `closed` before the
|
|
286
|
+
* dialog has ever been opened.
|
|
287
|
+
*/
|
|
288
|
+
protected readonly openAndClose = effect(() => {
|
|
289
|
+
const isOpen = this.open();
|
|
290
|
+
if (isOpen) {
|
|
291
|
+
this.#hasBeenOpen = true;
|
|
292
|
+
this.#doOpen();
|
|
293
|
+
} else if (this.#hasBeenOpen) {
|
|
294
|
+
this.#doClose();
|
|
295
|
+
}
|
|
296
|
+
});
|
|
79
297
|
|
|
80
298
|
/**
|
|
81
299
|
* `width`/`height`/`top`/`left` are web-only necessities: on web
|
|
@@ -92,20 +310,43 @@ export class UiAlertDialog implements OnDestroy {
|
|
|
92
310
|
// `bg-black/50` supplies the dim. It lives on the backdrop specifically so
|
|
93
311
|
// the existing fadeIn/fadeOut (opacity 0↔1, staggered 30ms ahead of the
|
|
94
312
|
// panel scale-in) animate the dim in lockstep with the dialog appearing —
|
|
95
|
-
// no separate animation needed.
|
|
96
|
-
//
|
|
97
|
-
//
|
|
313
|
+
// no separate animation needed. It's a named color, not a theme one,
|
|
314
|
+
// because the dim is black in light and dark mode alike; it compiles to
|
|
315
|
+
// `rgb(0 0 0 / 0.5)`, which Lynx accepts.
|
|
98
316
|
protected readonly backdropClass = computed(() =>
|
|
99
317
|
cn('items-center flex justify-center', 'h-full w-full', 'bg-black/50'),
|
|
100
318
|
);
|
|
101
319
|
|
|
102
|
-
|
|
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(() =>
|
|
103
330
|
cn(
|
|
104
|
-
'
|
|
105
|
-
this.
|
|
331
|
+
'h-full w-full absolute left-0 top-0',
|
|
332
|
+
this.look() === 'material' ? 'bg-black/[0.32]' : 'bg-black/20',
|
|
106
333
|
),
|
|
107
334
|
);
|
|
108
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
|
+
|
|
109
350
|
/**
|
|
110
351
|
* No-op tap handler for the panel. `catchtap` (vs `bindtap`) already stops
|
|
111
352
|
* the tap from bubbling to the backdrop — Lynx controls propagation via the
|
|
@@ -163,9 +404,8 @@ export class UiAlertDialog implements OnDestroy {
|
|
|
163
404
|
}
|
|
164
405
|
|
|
165
406
|
#animateIn(): void {
|
|
166
|
-
const backdrop = this.backdropRef()
|
|
167
|
-
const panel = this.panelRef()
|
|
168
|
-
if (!backdrop || !panel) return;
|
|
407
|
+
const backdrop = this.backdropRef().nativeElement;
|
|
408
|
+
const panel = this.panelRef().nativeElement;
|
|
169
409
|
|
|
170
410
|
this.#backdropAnim?.cancel();
|
|
171
411
|
this.#panelAnim?.cancel();
|
|
@@ -181,34 +421,56 @@ export class UiAlertDialog implements OnDestroy {
|
|
|
181
421
|
// feels like it's emerging from behind the dimmed overlay rather than
|
|
182
422
|
// appearing simultaneously with it.
|
|
183
423
|
this.#later(() => {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
+
});
|
|
190
433
|
}, 30);
|
|
191
434
|
}
|
|
192
435
|
|
|
193
436
|
#animateOut(): void {
|
|
194
|
-
const backdrop = this.backdropRef()
|
|
195
|
-
const
|
|
196
|
-
if (!backdrop || !panel) return;
|
|
437
|
+
const backdrop = this.backdropRef().nativeElement;
|
|
438
|
+
const motion = this.#motion();
|
|
197
439
|
|
|
198
440
|
this.#backdropAnim?.cancel();
|
|
199
441
|
this.#panelAnim?.cancel();
|
|
200
442
|
|
|
201
|
-
this.#panelAnim =
|
|
202
|
-
duration: 180
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
+
});
|
|
207
451
|
|
|
208
452
|
this.#backdropAnim = fadeOut(backdrop, { duration: DURATION.normal });
|
|
209
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
|
+
}
|
|
210
464
|
}
|
|
211
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
|
+
|
|
212
474
|
@Component({
|
|
213
475
|
selector: 'ui-alert-dialog-header',
|
|
214
476
|
imports: [LYNX_ELEMENTS],
|
|
@@ -222,8 +484,24 @@ export class UiAlertDialog implements OnDestroy {
|
|
|
222
484
|
export class UiAlertDialogHeader {
|
|
223
485
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
224
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
|
+
*/
|
|
225
494
|
protected readonly containerClass = computed(() =>
|
|
226
|
-
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
|
+
),
|
|
227
505
|
);
|
|
228
506
|
}
|
|
229
507
|
|
|
@@ -236,8 +514,23 @@ export class UiAlertDialogHeader {
|
|
|
236
514
|
export class UiAlertDialogTitle {
|
|
237
515
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
238
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
|
+
*/
|
|
239
523
|
protected readonly textClass = computed(() =>
|
|
240
|
-
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
|
+
),
|
|
241
534
|
);
|
|
242
535
|
}
|
|
243
536
|
|
|
@@ -250,25 +543,346 @@ export class UiAlertDialogTitle {
|
|
|
250
543
|
export class UiAlertDialogDescription {
|
|
251
544
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
252
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
|
+
*/
|
|
253
552
|
protected readonly textClass = computed(() =>
|
|
254
|
-
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
|
+
),
|
|
255
562
|
);
|
|
256
563
|
}
|
|
257
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
|
+
|
|
258
573
|
@Component({
|
|
259
574
|
selector: 'ui-alert-dialog-footer',
|
|
260
575
|
imports: [LYNX_ELEMENTS],
|
|
261
576
|
encapsulation: ViewEncapsulation.None,
|
|
262
577
|
template: `
|
|
263
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
|
+
}
|
|
264
585
|
<ng-content />
|
|
265
586
|
</view>
|
|
266
587
|
`,
|
|
267
588
|
})
|
|
268
589
|
export class UiAlertDialogFooter {
|
|
269
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');
|
|
270
596
|
|
|
271
|
-
|
|
272
|
-
|
|
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()),
|
|
273
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),
|
|
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
|
+
}
|
|
274
888
|
}
|