@blotch/dolan 22.2.3 → 22.2.4
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 +36 -3
- package/dist/commands/add.test.js.map +1 -1
- package/dist/commands/doctor.js +187 -7
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.js +280 -7
- 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 +107 -1
- 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 +5 -0
- package/dist/commands/update.js +126 -45
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/update.test.js +314 -2
- package/dist/commands/update.test.js.map +1 -1
- package/dist/lockfile.d.ts +7 -0
- package/dist/lockfile.js +36 -0
- package/dist/lockfile.js.map +1 -1
- package/dist/lockfile.test.d.ts +1 -0
- package/dist/lockfile.test.js +25 -0
- package/dist/lockfile.test.js.map +1 -0
- package/dist/registry.d.ts +6 -0
- package/dist/registry.js +48 -2
- package/dist/registry.js.map +1 -1
- package/dist/registry.test.js +45 -3
- package/dist/registry.test.js.map +1 -1
- package/dist/test-utils.d.ts +5 -0
- package/dist/test-utils.js +6 -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/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 +272 -187
- package/dist/ui/components/action-sheet/action-sheet.ts +40 -34
- 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 +206 -139
- package/dist/ui/components/alert-dialog/alert-dialog.ts +32 -26
- package/dist/ui/components/area-chart/area-chart.test.ts +81 -1
- 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 +12 -14
- package/dist/ui/components/bar-chart/bar-chart.test.ts +82 -1
- package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +373 -297
- package/dist/ui/components/bottom-sheet/bottom-sheet.ts +34 -33
- package/dist/ui/components/button/button.test.ts +611 -2
- package/dist/ui/components/button/button.ts +431 -62
- package/dist/ui/components/button/index.ts +2 -0
- package/dist/ui/components/button-group/button-group.test.ts +484 -2
- package/dist/ui/components/button-group/button-group.ts +380 -68
- 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 +82 -62
- package/dist/ui/components/chart/chart-presets.test.ts +197 -1
- package/dist/ui/components/chart/chart-presets.ts +8 -1
- package/dist/ui/components/chart/chart-theme.ts +12 -0
- package/dist/ui/components/chart/chart.test.ts +1017 -0
- package/dist/ui/components/chart/chart.ts +56 -43
- package/dist/ui/components/checkbox/checkbox.test.ts +124 -42
- package/dist/ui/components/checkbox/checkbox.ts +23 -23
- package/dist/ui/components/collapsible/collapsible.test.ts +158 -81
- package/dist/ui/components/collapsible/collapsible.ts +19 -14
- 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 +29 -23
- package/dist/ui/components/dock/dock-scroll.test.ts +130 -0
- package/dist/ui/components/dock/dock-scroll.ts +133 -0
- package/dist/ui/components/dock/dock.test.ts +749 -0
- package/dist/ui/components/dock/dock.ts +960 -0
- package/dist/ui/components/dock/index.ts +2 -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 +338 -0
- package/dist/ui/components/glass/glass.ts +360 -0
- package/dist/ui/components/glass/index.ts +18 -0
- package/dist/ui/components/icon/icon-theme.test.ts +36 -0
- package/dist/ui/components/icon/icon-theme.ts +36 -13
- 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 +15 -12
- package/dist/ui/components/label/label.test.ts +11 -24
- package/dist/ui/components/line-chart/line-chart.test.ts +76 -1
- package/dist/ui/components/list/list.test.ts +80 -49
- 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 +632 -0
- package/dist/ui/components/menu/menu-popup.ts +544 -0
- package/dist/ui/components/menu/menu.test.ts +429 -0
- package/dist/ui/components/menu/menu.ts +415 -0
- package/dist/ui/components/nav-drawer/nav-drawer.test.ts +425 -260
- package/dist/ui/components/nav-drawer/nav-drawer.ts +72 -56
- package/dist/ui/components/pagination/pagination.test.ts +136 -86
- package/dist/ui/components/progress/progress.test.ts +102 -46
- package/dist/ui/components/progress/progress.ts +52 -54
- package/dist/ui/components/radio-group/radio-group.test.ts +128 -83
- package/dist/ui/components/radio-group/radio-group.ts +24 -24
- 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/select/select.test.ts +138 -78
- package/dist/ui/components/select/select.ts +17 -11
- package/dist/ui/components/separator/separator.test.ts +18 -17
- package/dist/ui/components/sheet/sheet.test.ts +64 -11
- package/dist/ui/components/sheet/sheet.ts +9 -1
- package/dist/ui/components/skeleton/skeleton.test.ts +24 -7
- package/dist/ui/components/skeleton/skeleton.ts +10 -16
- package/dist/ui/components/spinner/spinner.test.ts +64 -46
- package/dist/ui/components/spinner/spinner.ts +16 -19
- package/dist/ui/components/switch/switch.test.ts +116 -46
- package/dist/ui/components/switch/switch.ts +8 -13
- 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/tabs.test.ts +215 -157
- package/dist/ui/components/tabs/tabs.ts +110 -118
- package/dist/ui/components/textarea/textarea.test.ts +206 -100
- package/dist/ui/components/textarea/textarea.ts +14 -12
- package/dist/ui/components/toast/toast.test.ts +434 -350
- package/dist/ui/components/toast/toast.ts +42 -51
- package/dist/ui/components/toggle/toggle.test.ts +247 -140
- 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 +14 -0
- package/dist/utils/analyze.js +15 -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 +12 -0
- package/dist/utils/installed.js +16 -0
- package/dist/utils/installed.js.map +1 -1
- package/dist/utils/installed.test.js +17 -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 +25 -0
- package/dist/utils/tailwind.js +51 -0
- package/dist/utils/tailwind.js.map +1 -0
- package/dist/utils/tailwind.test.d.ts +1 -0
- package/dist/utils/tailwind.test.js +71 -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,13 +1,22 @@
|
|
|
1
|
+
import { NgTemplateOutlet } from '@angular/common';
|
|
1
2
|
import {
|
|
2
3
|
type ElementRef,
|
|
3
4
|
Component,
|
|
5
|
+
Injector,
|
|
4
6
|
ViewEncapsulation,
|
|
5
7
|
computed,
|
|
8
|
+
contentChild,
|
|
9
|
+
inject,
|
|
6
10
|
input,
|
|
7
11
|
output,
|
|
12
|
+
signal,
|
|
8
13
|
viewChild,
|
|
9
14
|
} from '@angular/core';
|
|
10
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
LYNX_ELEMENTS,
|
|
17
|
+
LynxAccessibilitySettings,
|
|
18
|
+
LynxSystemInfo,
|
|
19
|
+
} from '@blotch/angular-lynx';
|
|
11
20
|
import { cva, type VariantProps } from 'class-variance-authority';
|
|
12
21
|
|
|
13
22
|
import {
|
|
@@ -16,8 +25,19 @@ import {
|
|
|
16
25
|
pressRelease,
|
|
17
26
|
} from '../../utils/animate';
|
|
18
27
|
import { cn } from '../../utils/cn';
|
|
28
|
+
import { UiGlass } from '../glass/glass';
|
|
29
|
+
import {
|
|
30
|
+
injectGlassDesign,
|
|
31
|
+
injectSystemDrawsGlass,
|
|
32
|
+
} from '../glass/glass-design';
|
|
33
|
+
import { UiIcon } from '../icon/icon';
|
|
34
|
+
import { UI_ICON_COLOR } from '../icon/icon-theme';
|
|
19
35
|
import { UiSpinner } from '../spinner/spinner';
|
|
20
36
|
|
|
37
|
+
/**
|
|
38
|
+
* The container classes of the solid variants. The glass variants draw a
|
|
39
|
+
* `<blur-view>`, which classes alone can't, so they aren't here.
|
|
40
|
+
*/
|
|
21
41
|
export const buttonVariants = cva(
|
|
22
42
|
'items-center rounded-md flex justify-center',
|
|
23
43
|
{
|
|
@@ -34,6 +54,7 @@ export const buttonVariants = cva(
|
|
|
34
54
|
sm: 'h-9 rounded-md px-3',
|
|
35
55
|
lg: 'h-11 rounded-md px-8',
|
|
36
56
|
icon: 'h-10 w-10',
|
|
57
|
+
'icon-circle': 'h-10 w-10 rounded-full',
|
|
37
58
|
},
|
|
38
59
|
},
|
|
39
60
|
defaultVariants: {
|
|
@@ -43,52 +64,174 @@ export const buttonVariants = cva(
|
|
|
43
64
|
},
|
|
44
65
|
);
|
|
45
66
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
destructive: 'text-destructive-foreground',
|
|
51
|
-
outline: 'text-foreground',
|
|
52
|
-
secondary: 'text-secondary-foreground',
|
|
53
|
-
ghost: 'text-foreground',
|
|
54
|
-
},
|
|
55
|
-
size: {
|
|
56
|
-
default: 'text-sm',
|
|
57
|
-
sm: 'text-xs',
|
|
58
|
-
lg: 'text-base',
|
|
59
|
-
icon: 'text-sm',
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
defaultVariants: {
|
|
63
|
-
variant: 'default',
|
|
64
|
-
size: 'default',
|
|
65
|
-
},
|
|
66
|
-
});
|
|
67
|
+
/** A variant drawn with solid colors, on every platform. */
|
|
68
|
+
export type SolidButtonVariant = NonNullable<
|
|
69
|
+
VariantProps<typeof buttonVariants>['variant']
|
|
70
|
+
>;
|
|
67
71
|
|
|
68
72
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
73
|
+
* Liquid Glass variants, SwiftUI's button styles: `glass` (`.glass`),
|
|
74
|
+
* `glass-prominent` (`.glassProminent`, tinted for the screen's main action)
|
|
75
|
+
* and `glass-clear` (`.glass(.clear)`, for buttons over photos and video).
|
|
72
76
|
*/
|
|
73
|
-
|
|
77
|
+
export type GlassButtonVariant = 'glass' | 'glass-prominent' | 'glass-clear';
|
|
78
|
+
|
|
79
|
+
export type ButtonVariant = SolidButtonVariant | GlassButtonVariant;
|
|
80
|
+
export type ButtonSize = NonNullable<
|
|
81
|
+
VariantProps<typeof buttonVariants>['size']
|
|
82
|
+
>;
|
|
83
|
+
|
|
84
|
+
const GLASS_VARIANTS: ReadonlySet<ButtonVariant> = new Set<ButtonVariant>([
|
|
85
|
+
'glass',
|
|
86
|
+
'glass-prominent',
|
|
87
|
+
'glass-clear',
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Whether a variant is drawn with glass, narrowing it for the maps below.
|
|
92
|
+
*/
|
|
93
|
+
const isGlassVariant = (
|
|
94
|
+
variant: ButtonVariant,
|
|
95
|
+
): variant is GlassButtonVariant => GLASS_VARIANTS.has(variant);
|
|
96
|
+
|
|
97
|
+
/** The sizes for a single icon, which then needs no label. */
|
|
98
|
+
const ICON_SIZES: ReadonlySet<ButtonSize> = new Set<ButtonSize>([
|
|
99
|
+
'icon',
|
|
100
|
+
'icon-circle',
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
const LABEL_SIZES: Readonly<Record<ButtonSize, string>> = {
|
|
104
|
+
default: 'text-sm',
|
|
105
|
+
sm: 'text-xs',
|
|
106
|
+
lg: 'text-base',
|
|
107
|
+
icon: 'text-sm',
|
|
108
|
+
'icon-circle': 'text-sm',
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const SOLID_LABEL_COLORS: Readonly<Record<SolidButtonVariant, string>> = {
|
|
112
|
+
default: 'text-primary-foreground',
|
|
113
|
+
destructive: 'text-destructive-foreground',
|
|
114
|
+
outline: 'text-foreground',
|
|
115
|
+
secondary: 'text-secondary-foreground',
|
|
116
|
+
ghost: 'text-foreground',
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The token of each solid variant's label color above, for what draws with
|
|
121
|
+
* SVG: the spinner and projected icons can't read the text classes, so they
|
|
122
|
+
* get the token and resolve it themselves (see icon-theme.ts).
|
|
123
|
+
*/
|
|
124
|
+
const SOLID_FOREGROUNDS: Readonly<Record<SolidButtonVariant, string>> = {
|
|
74
125
|
default: 'var(--primary-foreground)',
|
|
75
126
|
destructive: 'var(--destructive-foreground)',
|
|
76
127
|
outline: 'var(--foreground)',
|
|
77
128
|
secondary: 'var(--secondary-foreground)',
|
|
78
129
|
ghost: 'var(--foreground)',
|
|
79
|
-
}
|
|
130
|
+
};
|
|
80
131
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
132
|
+
/**
|
|
133
|
+
* A glass button's box, which its glass fills. Glass buttons are capsules,
|
|
134
|
+
* as SwiftUI draws them, so an icon size is a circle.
|
|
135
|
+
*/
|
|
136
|
+
const glassFrameVariants = cva('flex-col rounded-full flex', {
|
|
137
|
+
variants: {
|
|
138
|
+
size: {
|
|
139
|
+
default: 'h-10',
|
|
140
|
+
sm: 'h-9',
|
|
141
|
+
lg: 'h-11',
|
|
142
|
+
icon: 'h-10 w-10',
|
|
143
|
+
'icon-circle': 'h-10 w-10',
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const GLASS_PADDING: Readonly<Record<ButtonSize, string>> = {
|
|
149
|
+
default: 'px-4',
|
|
150
|
+
sm: 'px-3',
|
|
151
|
+
lg: 'px-8',
|
|
152
|
+
icon: '',
|
|
153
|
+
'icon-circle': '',
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The glass variants as Material 3 Expressive buttons, Android's default
|
|
158
|
+
* (see GlassDesign): `glass` and `glass-clear` are filled tonal buttons,
|
|
159
|
+
* `glass-prominent` a filled one in the glass tint, and the icon sizes
|
|
160
|
+
* Material's round icon buttons. Material's buttons sit flat on the page,
|
|
161
|
+
* with no shadow, and are fully rounded.
|
|
162
|
+
*/
|
|
163
|
+
const materialButtonVariants = cva(
|
|
164
|
+
'flex-row items-center rounded-full flex relative justify-center overflow-hidden',
|
|
165
|
+
{
|
|
166
|
+
variants: {
|
|
167
|
+
variant: {
|
|
168
|
+
glass: 'bg-secondary',
|
|
169
|
+
'glass-clear': 'bg-secondary',
|
|
170
|
+
'glass-prominent': 'bg-glass-tint',
|
|
171
|
+
},
|
|
172
|
+
size: {
|
|
173
|
+
default: 'h-10 px-4',
|
|
174
|
+
sm: 'h-9 px-3',
|
|
175
|
+
lg: 'h-11 px-8',
|
|
176
|
+
icon: 'h-10 w-10',
|
|
177
|
+
'icon-circle': 'h-10 w-10',
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* How a glass variant looks on this platform: `'glass'` draws it on a
|
|
185
|
+
* `<blur-view ui-glass>` (Liquid Glass, or the frosted material before iOS
|
|
186
|
+
* 26), `'material'` as a Material button. The solid variants are `'solid'`
|
|
187
|
+
* everywhere.
|
|
188
|
+
*/
|
|
189
|
+
type ButtonLook = 'solid' | 'glass' | 'material';
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* How the button answers a finger: it shrinks (`'scale'`), dims at once
|
|
193
|
+
* (`'dim'`, under Reduce Motion), shows Material's pressed state layer
|
|
194
|
+
* (`'layer'`), or leaves it to interactive glass, which reacts by itself
|
|
195
|
+
* (`'none'`).
|
|
196
|
+
*/
|
|
197
|
+
type PressFeedback = 'scale' | 'dim' | 'layer' | 'none';
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* `UiIcon`, for `contentChild()`. A call rather than the class itself: for a
|
|
201
|
+
* class, Angular's test build keeps an arrow function that nothing calls,
|
|
202
|
+
* which an app's coverage report would show as uncovered.
|
|
203
|
+
*/
|
|
204
|
+
const iconType = () => UiIcon;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The platform, as SystemInfo names it, or undefined where there's no
|
|
208
|
+
* SystemInfo to read, such as in unit tests, where LynxSystemInfo throws.
|
|
209
|
+
*/
|
|
210
|
+
const platformOf = (injector: Injector): string | undefined => {
|
|
211
|
+
try {
|
|
212
|
+
return injector.get(LynxSystemInfo).platform;
|
|
213
|
+
} catch {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The color an icon projected into the button draws with when it's given
|
|
220
|
+
* none: its label's (see UI_ICON_COLOR). A named function, not an inline
|
|
221
|
+
* factory: the component's metadata keeps its own copy of an inline one,
|
|
222
|
+
* which nothing calls, so an app's coverage report would show it uncovered.
|
|
223
|
+
*/
|
|
224
|
+
const buttonIconColor = () => inject(UiButton).foreground;
|
|
87
225
|
|
|
88
226
|
@Component({
|
|
89
227
|
selector: 'ui-button',
|
|
90
|
-
imports: [LYNX_ELEMENTS, UiSpinner],
|
|
228
|
+
imports: [LYNX_ELEMENTS, NgTemplateOutlet, UiGlass, UiSpinner],
|
|
91
229
|
encapsulation: ViewEncapsulation.None,
|
|
230
|
+
// Icons projected into the button inject this through its host's element
|
|
231
|
+
// injector, which is where Angular looks for projected content: the glass
|
|
232
|
+
// inside the template provides one too, but only for the template's own
|
|
233
|
+
// nodes.
|
|
234
|
+
providers: [{ provide: UI_ICON_COLOR, useFactory: buttonIconColor }],
|
|
92
235
|
template: `
|
|
93
236
|
<!-- Press feedback binds BOTH touch and mouse events on purpose. Native
|
|
94
237
|
(iOS/Android) only ever fires touch; Lynx web runs in the browser, where
|
|
@@ -99,10 +242,17 @@ export type ButtonSize = NonNullable<
|
|
|
99
242
|
touch devices, so there's no double-fire. The scale animation itself lives
|
|
100
243
|
in pressDown/pressRelease — see their JSDoc for why it's a CSS transition
|
|
101
244
|
(which serializes to the DOM on Lynx web) and not el.animate() (whose Web
|
|
102
|
-
Animation runs on web-core's offscreen node and never reaches the screen).
|
|
245
|
+
Animation runs on web-core's offscreen node and never reaches the screen).
|
|
246
|
+
|
|
247
|
+
An icon-only button has no text for a screen reader to read, so with
|
|
248
|
+
an accessibility-label the whole button becomes one element that says
|
|
249
|
+
it. Without one, the label's own text is read, as a button. -->
|
|
103
250
|
<view
|
|
104
251
|
#container
|
|
105
252
|
[class]="containerClass()"
|
|
253
|
+
[attr.accessibility-element]="accessibilityLabel() ? 'true' : null"
|
|
254
|
+
[attr.accessibility-traits]="accessibilityLabel() ? 'button' : null"
|
|
255
|
+
[attr.accessibility-label]="accessibilityLabel() || null"
|
|
106
256
|
(bindtouchstart)="onPressStart()"
|
|
107
257
|
(bindtouchend)="onPressEnd()"
|
|
108
258
|
(bindtouchcancel)="onPressCancel()"
|
|
@@ -111,76 +261,295 @@ export type ButtonSize = NonNullable<
|
|
|
111
261
|
(bindmouseleave)="onPressCancel()"
|
|
112
262
|
(bindtap)="onTap()"
|
|
113
263
|
>
|
|
114
|
-
@if (
|
|
115
|
-
|
|
264
|
+
@if (look() === 'glass') {
|
|
265
|
+
<!-- The container is the button's box, and the glass fills it. Under
|
|
266
|
+
clear glass the container is the dimming layer (see dimmed).
|
|
267
|
+
The content sits in the glass, where iOS's interactive glass
|
|
268
|
+
reacts to touches on it, and is what dims when disabled, as a
|
|
269
|
+
disabled SwiftUI glass button dims its label: glass under a
|
|
270
|
+
faded parent turns into a washed-out patch on iOS. -->
|
|
271
|
+
<blur-view
|
|
272
|
+
ui-glass
|
|
273
|
+
[variant]="variant() === 'glass-clear' ? 'clear' : 'regular'"
|
|
274
|
+
[tint]="
|
|
275
|
+
variant() === 'glass-prominent' ? 'var(--glass-tint)' : undefined
|
|
276
|
+
"
|
|
277
|
+
[interactive]="interactive()"
|
|
278
|
+
[class]="glassClass()"
|
|
279
|
+
>
|
|
280
|
+
<view [class]="glassContentClass()">
|
|
281
|
+
<ng-container [ngTemplateOutlet]="content" />
|
|
282
|
+
</view>
|
|
283
|
+
</blur-view>
|
|
116
284
|
} @else {
|
|
117
|
-
|
|
285
|
+
@if (stateLayerClass(); as layer) {
|
|
286
|
+
<view [class]="layer"></view>
|
|
287
|
+
}
|
|
288
|
+
<ng-container [ngTemplateOutlet]="content" />
|
|
118
289
|
}
|
|
119
290
|
</view>
|
|
291
|
+
|
|
292
|
+
<!-- One copy of the content, for whichever form the button takes: Angular
|
|
293
|
+
projects content once, so it can't be in both branches above. -->
|
|
294
|
+
<ng-template #content>
|
|
295
|
+
@if (loading()) {
|
|
296
|
+
<ui-spinner [size]="spinnerSize()" [color]="foreground()" />
|
|
297
|
+
} @else {
|
|
298
|
+
<ng-content select="ui-icon" />
|
|
299
|
+
@if (showLabel()) {
|
|
300
|
+
<text
|
|
301
|
+
[attr.accessibility-traits]="accessibilityLabel() ? null : 'button'"
|
|
302
|
+
[class]="labelClass()"
|
|
303
|
+
><ng-content
|
|
304
|
+
/></text>
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
</ng-template>
|
|
120
308
|
`,
|
|
121
309
|
})
|
|
122
310
|
export class UiButton {
|
|
311
|
+
/**
|
|
312
|
+
* The look: a solid variant, or a glass one (`'glass'`,
|
|
313
|
+
* `'glass-prominent'`, `'glass-clear'`; see GlassButtonVariant). Keep glass
|
|
314
|
+
* for buttons that float over the content.
|
|
315
|
+
*/
|
|
123
316
|
readonly variant = input<ButtonVariant>('default');
|
|
317
|
+
/**
|
|
318
|
+
* The height and padding. `'icon'` and `'icon-circle'` fit one projected
|
|
319
|
+
* `<ui-icon>`: a rounded square and a circle (glass buttons are circles in
|
|
320
|
+
* either).
|
|
321
|
+
*/
|
|
124
322
|
readonly size = input<ButtonSize>('default');
|
|
125
323
|
readonly disabled = input(false);
|
|
126
324
|
readonly loading = input(false);
|
|
325
|
+
/**
|
|
326
|
+
* What a screen reader says for the button. An icon-only button needs one;
|
|
327
|
+
* a button with a label reads its label without it.
|
|
328
|
+
*/
|
|
329
|
+
readonly accessibilityLabel = input<string | undefined>(undefined, {
|
|
330
|
+
alias: 'accessibility-label',
|
|
331
|
+
});
|
|
127
332
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
128
333
|
|
|
129
334
|
readonly pressed = output<void>();
|
|
130
335
|
|
|
131
336
|
readonly containerRef = viewChild<ElementRef>('container');
|
|
337
|
+
/** A `<ui-icon>` projected into the button, which sits before the label. */
|
|
338
|
+
readonly projectedIcon = contentChild(iconType());
|
|
339
|
+
|
|
340
|
+
readonly #settings = inject(LynxAccessibilitySettings);
|
|
341
|
+
readonly #design = injectGlassDesign();
|
|
342
|
+
readonly #systemDrawsGlass = injectSystemDrawsGlass();
|
|
343
|
+
readonly #platform = platformOf(inject(Injector));
|
|
132
344
|
#pressAnim?: AnimationHandle;
|
|
133
345
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
346
|
+
/** A finger (or the mouse) is down on the button. */
|
|
347
|
+
protected readonly pressing = signal(false);
|
|
348
|
+
|
|
349
|
+
readonly #inactive = computed(() => this.disabled() || this.loading());
|
|
350
|
+
|
|
351
|
+
protected readonly look = computed<ButtonLook>(() => {
|
|
352
|
+
if (!isGlassVariant(this.variant())) return 'solid';
|
|
353
|
+
return this.#design() === 'material' ? 'material' : 'glass';
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* The glass reacts to touch while the button can be pressed. Where the
|
|
358
|
+
* runtime draws it (the web preview), that's motion Reduce Motion asks to
|
|
359
|
+
* leave out; iOS tones its own glass down under the setting.
|
|
360
|
+
*/
|
|
361
|
+
protected readonly interactive = computed(
|
|
362
|
+
() =>
|
|
363
|
+
!this.#inactive() &&
|
|
364
|
+
(this.#systemDrawsGlass || !this.#settings.reduceMotion()),
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* For the glass look: what's behind shows through the glass, which is
|
|
369
|
+
* drawn as glass or a frosted material, not the opaque surface it turns
|
|
370
|
+
* into under Reduce Transparency where the runtime draws it (as
|
|
371
|
+
* UiGlass.surface decides).
|
|
372
|
+
*/
|
|
373
|
+
readonly #seeThrough = computed(
|
|
374
|
+
() => this.#systemDrawsGlass || !this.#settings.reduceTransparency(),
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Interactive glass that answers a press itself, swelling under the
|
|
379
|
+
* finger: UIKit's on iOS 26 and later, and the runtime's in the web
|
|
380
|
+
* preview. Android's blur-view has no interactive glass, and the frosted
|
|
381
|
+
* materials before iOS 26 don't react.
|
|
382
|
+
*/
|
|
383
|
+
readonly #glassReacts = computed(
|
|
384
|
+
() =>
|
|
385
|
+
this.#design() === 'glass' &&
|
|
386
|
+
this.#seeThrough() &&
|
|
387
|
+
this.interactive() &&
|
|
388
|
+
this.#platform !== 'Android',
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* See PressFeedback. Shrinking on top of interactive glass would fight its
|
|
393
|
+
* swell, Material's buttons show a state layer, and under Reduce Motion
|
|
394
|
+
* the button dims rather than moving.
|
|
395
|
+
*/
|
|
396
|
+
readonly #feedback = computed<PressFeedback>(() => {
|
|
397
|
+
const look = this.look();
|
|
398
|
+
if (look === 'material') return 'layer';
|
|
399
|
+
if (look === 'glass' && this.#glassReacts()) return 'none';
|
|
400
|
+
return this.#settings.reduceMotion() ? 'dim' : 'scale';
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
readonly #dimmedByPress = computed(
|
|
404
|
+
() => this.pressing() && this.#feedback() === 'dim',
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Clear glass sits on a dark layer at about a third, so its white label
|
|
409
|
+
* stays legible over bright photos and video: Apple's rule for clear
|
|
410
|
+
* glass. The layer is the container, under the glass.
|
|
411
|
+
*/
|
|
412
|
+
readonly #dimmed = computed(
|
|
413
|
+
() => this.variant() === 'glass-clear' && this.#seeThrough(),
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
protected readonly showLabel = computed(
|
|
417
|
+
() => !(this.projectedIcon() && ICON_SIZES.has(this.size())),
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
/** Space between an icon and the label beside it. */
|
|
421
|
+
readonly #gap = computed(
|
|
422
|
+
() => !!this.projectedIcon() && this.showLabel() && 'gap-2',
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* The color of the label, the spinner and projected icons, as a `var()`.
|
|
427
|
+
* On clear glass it's the clear foreground, which stays white over media.
|
|
428
|
+
*/
|
|
429
|
+
readonly foreground = computed(() => {
|
|
430
|
+
const variant = this.variant();
|
|
431
|
+
if (!isGlassVariant(variant)) return SOLID_FOREGROUNDS[variant];
|
|
432
|
+
if (variant === 'glass-prominent') return 'var(--glass-tint-foreground)';
|
|
433
|
+
if (this.look() === 'material') return 'var(--secondary-foreground)';
|
|
434
|
+
return this.#dimmed()
|
|
435
|
+
? 'var(--glass-clear-foreground)'
|
|
436
|
+
: 'var(--glass-foreground)';
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
protected readonly containerClass = computed(() => {
|
|
440
|
+
const variant = this.variant();
|
|
441
|
+
const size = this.size();
|
|
442
|
+
const inactive = this.#inactive();
|
|
443
|
+
if (!isGlassVariant(variant)) {
|
|
444
|
+
return cn(
|
|
445
|
+
buttonVariants({ variant, size }),
|
|
446
|
+
this.#gap(),
|
|
447
|
+
this.#dimmedByPress() && 'opacity-70',
|
|
448
|
+
inactive && 'opacity-50',
|
|
449
|
+
this.userClass(),
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
if (this.look() === 'material') {
|
|
453
|
+
return cn(
|
|
454
|
+
materialButtonVariants({ variant, size }),
|
|
455
|
+
this.#gap(),
|
|
456
|
+
// A tonal button's edge barely shows on the page, so Increase
|
|
457
|
+
// Contrast outlines it, as glass gets an edge there.
|
|
458
|
+
variant !== 'glass-prominent' &&
|
|
459
|
+
this.#settings.increaseContrast() &&
|
|
460
|
+
'border border-foreground',
|
|
461
|
+
inactive && 'opacity-50',
|
|
462
|
+
this.userClass(),
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
return cn(
|
|
466
|
+
glassFrameVariants({ size }),
|
|
467
|
+
this.#dimmed() && 'bg-glass-dim',
|
|
138
468
|
this.userClass(),
|
|
469
|
+
);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
protected readonly glassClass = computed(() =>
|
|
473
|
+
cn(
|
|
474
|
+
'flex-1 flex-row items-center flex justify-center',
|
|
475
|
+
GLASS_PADDING[this.size()],
|
|
476
|
+
),
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
protected readonly glassContentClass = computed(() =>
|
|
480
|
+
cn(
|
|
481
|
+
'flex-row items-center flex justify-center',
|
|
482
|
+
this.#gap(),
|
|
483
|
+
this.#dimmedByPress() && 'opacity-70',
|
|
484
|
+
this.#inactive() && 'opacity-50',
|
|
139
485
|
),
|
|
140
486
|
);
|
|
141
487
|
|
|
142
|
-
|
|
143
|
-
|
|
488
|
+
/**
|
|
489
|
+
* Material's pressed state: the label's color over the container at 10%,
|
|
490
|
+
* under the content. It appears with the press and goes with the release,
|
|
491
|
+
* with no motion.
|
|
492
|
+
*/
|
|
493
|
+
protected readonly stateLayerClass = computed(
|
|
494
|
+
() =>
|
|
495
|
+
this.pressing() &&
|
|
496
|
+
this.#feedback() === 'layer' &&
|
|
497
|
+
cn(
|
|
498
|
+
'h-full w-full opacity-10 absolute left-0 top-0',
|
|
499
|
+
this.variant() === 'glass-prominent'
|
|
500
|
+
? 'bg-glass-tint-foreground'
|
|
501
|
+
: 'bg-secondary-foreground',
|
|
502
|
+
),
|
|
144
503
|
);
|
|
145
504
|
|
|
505
|
+
protected readonly labelClass = computed(() => {
|
|
506
|
+
const variant = this.variant();
|
|
507
|
+
let color: string;
|
|
508
|
+
if (!isGlassVariant(variant)) color = SOLID_LABEL_COLORS[variant];
|
|
509
|
+
else if (variant === 'glass-prominent') {
|
|
510
|
+
color = 'text-glass-tint-foreground';
|
|
511
|
+
} else if (this.look() === 'material') color = 'text-secondary-foreground';
|
|
512
|
+
// The glass points this at the clear foreground on clear glass.
|
|
513
|
+
else color = 'text-glass-foreground';
|
|
514
|
+
return cn('font-medium', LABEL_SIZES[this.size()], color);
|
|
515
|
+
});
|
|
516
|
+
|
|
146
517
|
protected readonly spinnerSize = computed(() => {
|
|
147
518
|
const s = this.size();
|
|
148
519
|
return s === 'sm' ? ('xs' as const) : ('sm' as const);
|
|
149
520
|
});
|
|
150
521
|
|
|
151
|
-
protected readonly spinnerColor = computed(
|
|
152
|
-
() => SPINNER_COLORS[this.variant()],
|
|
153
|
-
);
|
|
154
|
-
|
|
155
522
|
protected onPressStart(): void {
|
|
156
|
-
if (this
|
|
523
|
+
if (this.#inactive()) return;
|
|
524
|
+
this.pressing.set(true);
|
|
525
|
+
if (this.#feedback() !== 'scale') return;
|
|
157
526
|
// pressDown/pressRelease drive el.setStyle() based CSS transitions on the
|
|
158
|
-
// container ref
|
|
159
|
-
// wired up (see the label spec note), and Lynx web renderer's press
|
|
160
|
-
// feedback is verified end-to-end on-device.
|
|
161
|
-
/* v8 ignore next 2 */
|
|
527
|
+
// container ref (see pressDown for why it isn't el.animate()).
|
|
162
528
|
this.#pressAnim?.cancel();
|
|
163
529
|
this.#pressAnim = pressDown(this.containerRef()?.nativeElement);
|
|
164
530
|
}
|
|
165
531
|
|
|
166
532
|
protected onPressEnd(): void {
|
|
167
|
-
if (this
|
|
168
|
-
|
|
169
|
-
this.#pressAnim?.cancel();
|
|
170
|
-
this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
|
|
533
|
+
if (this.#inactive()) return;
|
|
534
|
+
this.#release();
|
|
171
535
|
}
|
|
172
536
|
|
|
173
537
|
protected onPressCancel(): void {
|
|
174
|
-
// Also
|
|
538
|
+
// Also release on cancel — without this, a canceled touch (e.g. scroll
|
|
175
539
|
// gesture taking over) leaves the button stuck in its pressed-down state.
|
|
176
|
-
|
|
177
|
-
this.#pressAnim?.cancel();
|
|
178
|
-
this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
|
|
540
|
+
this.#release();
|
|
179
541
|
}
|
|
180
542
|
|
|
181
543
|
protected onTap(): void {
|
|
182
|
-
if (!this
|
|
544
|
+
if (!this.#inactive()) {
|
|
183
545
|
this.pressed.emit();
|
|
184
546
|
}
|
|
185
547
|
}
|
|
548
|
+
|
|
549
|
+
#release(): void {
|
|
550
|
+
this.pressing.set(false);
|
|
551
|
+
if (this.#feedback() !== 'scale') return;
|
|
552
|
+
this.#pressAnim?.cancel();
|
|
553
|
+
this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
|
|
554
|
+
}
|
|
186
555
|
}
|