@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,14 +1,19 @@
|
|
|
1
|
+
import { NgTemplateOutlet } from '@angular/common';
|
|
1
2
|
import {
|
|
2
3
|
type ElementRef,
|
|
3
4
|
Component,
|
|
4
5
|
ViewEncapsulation,
|
|
6
|
+
booleanAttribute,
|
|
5
7
|
computed,
|
|
8
|
+
contentChild,
|
|
9
|
+
contentChildren,
|
|
6
10
|
inject,
|
|
7
11
|
input,
|
|
8
12
|
output,
|
|
13
|
+
signal,
|
|
9
14
|
viewChild,
|
|
10
15
|
} from '@angular/core';
|
|
11
|
-
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
16
|
+
import { LYNX_ELEMENTS, LynxAccessibilitySettings } from '@blotch/angular-lynx';
|
|
12
17
|
import { cva, type VariantProps } from 'class-variance-authority';
|
|
13
18
|
|
|
14
19
|
import {
|
|
@@ -17,14 +22,22 @@ import {
|
|
|
17
22
|
pressRelease,
|
|
18
23
|
} from '../../utils/animate';
|
|
19
24
|
import { cn } from '../../utils/cn';
|
|
25
|
+
import { concentricRadius } from '../glass/concentric-radius';
|
|
26
|
+
import { UiGlass } from '../glass/glass';
|
|
27
|
+
import {
|
|
28
|
+
injectGlassDesign,
|
|
29
|
+
injectSystemDrawsGlass,
|
|
30
|
+
} from '../glass/glass-design';
|
|
31
|
+
import { UiIcon } from '../icon/icon';
|
|
32
|
+
import { UI_ICON_COLOR } from '../icon/icon-theme';
|
|
20
33
|
|
|
21
34
|
/**
|
|
22
|
-
* Per-item styling. Corners are intentionally left
|
|
23
|
-
* container clips the whole bar with `overflow-hidden`
|
|
24
|
-
* the outer edges round off while every internal seam
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
35
|
+
* Per-item styling of the solid variants. Corners are intentionally left
|
|
36
|
+
* square here: the group container clips the whole bar with `overflow-hidden`
|
|
37
|
+
* + `rounded-md`, so only the outer edges round off while every internal seam
|
|
38
|
+
* stays crisp, and the items need no positional styling (Lynx's CSS engine
|
|
39
|
+
* has no `:first-child`/`:last-child`). Their outline is the container's
|
|
40
|
+
* border, which the clip doesn't cut.
|
|
28
41
|
*/
|
|
29
42
|
export const buttonGroupItemVariants = cva('items-center flex justify-center', {
|
|
30
43
|
variants: {
|
|
@@ -38,6 +51,7 @@ export const buttonGroupItemVariants = cva('items-center flex justify-center', {
|
|
|
38
51
|
default: 'h-10 px-4',
|
|
39
52
|
sm: 'h-9 px-3',
|
|
40
53
|
lg: 'h-11 px-6',
|
|
54
|
+
icon: 'h-10 w-10',
|
|
41
55
|
},
|
|
42
56
|
},
|
|
43
57
|
defaultVariants: {
|
|
@@ -46,32 +60,96 @@ export const buttonGroupItemVariants = cva('items-center flex justify-center', {
|
|
|
46
60
|
},
|
|
47
61
|
});
|
|
48
62
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
variant: {
|
|
52
|
-
default: 'text-secondary-foreground',
|
|
53
|
-
outline: 'text-foreground',
|
|
54
|
-
},
|
|
55
|
-
size: {
|
|
56
|
-
default: 'text-sm',
|
|
57
|
-
sm: 'text-xs',
|
|
58
|
-
lg: 'text-base',
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
defaultVariants: {
|
|
62
|
-
variant: 'default',
|
|
63
|
-
size: 'default',
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
export type ButtonGroupVariant = NonNullable<
|
|
63
|
+
/** A variant drawn with solid colors, on every platform. */
|
|
64
|
+
export type SolidButtonGroupVariant = NonNullable<
|
|
68
65
|
VariantProps<typeof buttonGroupItemVariants>['variant']
|
|
69
66
|
>;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The glass variants: the group is one glass shape with plain items in it,
|
|
70
|
+
* the way iOS groups toolbar items on one glass background. `glass-clear` is
|
|
71
|
+
* for a group over photos and video.
|
|
72
|
+
*/
|
|
73
|
+
export type GlassButtonGroupVariant = 'glass' | 'glass-clear';
|
|
74
|
+
|
|
75
|
+
export type ButtonGroupVariant =
|
|
76
|
+
| SolidButtonGroupVariant
|
|
77
|
+
| GlassButtonGroupVariant;
|
|
70
78
|
export type ButtonGroupSize = NonNullable<
|
|
71
79
|
VariantProps<typeof buttonGroupItemVariants>['size']
|
|
72
80
|
>;
|
|
73
81
|
export type ButtonGroupOrientation = 'horizontal' | 'vertical';
|
|
74
82
|
|
|
83
|
+
/**
|
|
84
|
+
* How a group looks on this platform: the solid variants everywhere, and the
|
|
85
|
+
* glass ones as one glass shape (`'glass'`) or, on Android by default, as
|
|
86
|
+
* Material 3 Expressive's connected button group (`'material'`).
|
|
87
|
+
*/
|
|
88
|
+
export type ButtonGroupLook = 'solid' | 'glass' | 'material';
|
|
89
|
+
|
|
90
|
+
/** Each size's item height, in px, for the corner radii. */
|
|
91
|
+
const ITEM_HEIGHTS: Readonly<Record<ButtonGroupSize, number>> = {
|
|
92
|
+
default: 40,
|
|
93
|
+
sm: 36,
|
|
94
|
+
lg: 44,
|
|
95
|
+
icon: 40,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** How far the items sit inside a glass group's edge, in px (`p-1`). */
|
|
99
|
+
const GLASS_INSET = 4;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The inner corners of Material's connected button group, in px: the
|
|
103
|
+
* group's ends are fully round, the corners between items small.
|
|
104
|
+
*/
|
|
105
|
+
const MATERIAL_INNER_RADIUS = 8;
|
|
106
|
+
|
|
107
|
+
const LABEL_SIZES: Readonly<Record<ButtonGroupSize, string>> = {
|
|
108
|
+
default: 'text-sm',
|
|
109
|
+
sm: 'text-xs',
|
|
110
|
+
lg: 'text-base',
|
|
111
|
+
icon: 'text-sm',
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const SOLID_LABEL_COLORS: Readonly<Record<SolidButtonGroupVariant, string>> = {
|
|
115
|
+
default: 'text-secondary-foreground',
|
|
116
|
+
outline: 'text-foreground',
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/** The token of each solid variant's label color, for projected icons. */
|
|
120
|
+
const SOLID_FOREGROUNDS: Readonly<Record<SolidButtonGroupVariant, string>> = {
|
|
121
|
+
default: 'var(--secondary-foreground)',
|
|
122
|
+
outline: 'var(--foreground)',
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/** The size classes of an item in a glass or Material group. */
|
|
126
|
+
const ITEM_SIZES: Readonly<Record<ButtonGroupSize, string>> = {
|
|
127
|
+
default: 'h-10 px-4',
|
|
128
|
+
sm: 'h-9 px-3',
|
|
129
|
+
lg: 'h-11 px-6',
|
|
130
|
+
icon: 'h-10 w-10',
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* `UiIcon`, for `contentChild()`. A call rather than the class itself: for a
|
|
135
|
+
* class, Angular's test build keeps an arrow function that nothing calls,
|
|
136
|
+
* which an app's coverage report would show as uncovered.
|
|
137
|
+
*/
|
|
138
|
+
const iconType = () => UiIcon;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* `UiButtonGroupItem`, for the group's `contentChildren()` (see iconType).
|
|
142
|
+
*/
|
|
143
|
+
const itemType = () => UiButtonGroupItem;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The color an icon projected into an item draws with when it's given none:
|
|
147
|
+
* the items' label color (see UI_ICON_COLOR). Named, not inline: the
|
|
148
|
+
* component's metadata keeps its own copy of an inline factory, which
|
|
149
|
+
* nothing calls.
|
|
150
|
+
*/
|
|
151
|
+
const groupIconColor = () => inject(UiButtonGroup).foreground;
|
|
152
|
+
|
|
75
153
|
/**
|
|
76
154
|
* A row (or column) of connected action buttons — a toolbar of related
|
|
77
155
|
* commands rendered as a single, seamless control.
|
|
@@ -81,36 +159,190 @@ export type ButtonGroupOrientation = 'horizontal' | 'vertical';
|
|
|
81
159
|
* the `UiToggleGroup`/`UiToggleGroupItem` pattern. Unlike a toggle group this
|
|
82
160
|
* carries no selection state: every item is an independent action that emits
|
|
83
161
|
* `pressed`.
|
|
162
|
+
*
|
|
163
|
+
* With a glass variant the group is one glass shape with plain items in it,
|
|
164
|
+
* as iOS 26 groups toolbar items on one background, rather than a glass
|
|
165
|
+
* shape per item merged in a `ui-glass-group`: merged circles join with
|
|
166
|
+
* pinched waists, not the straight edges of a toolbar's capsule, and glass
|
|
167
|
+
* isn't layered on glass. On Android it's Material's connected button group
|
|
168
|
+
* by default.
|
|
84
169
|
*/
|
|
85
170
|
@Component({
|
|
86
171
|
selector: 'ui-button-group',
|
|
87
|
-
|
|
88
|
-
imports: [LYNX_ELEMENTS],
|
|
172
|
+
imports: [LYNX_ELEMENTS, NgTemplateOutlet, UiGlass],
|
|
89
173
|
encapsulation: ViewEncapsulation.None,
|
|
174
|
+
// Icons projected into the items inject this: they're declared inside the
|
|
175
|
+
// group, so the group's element injector is one of theirs.
|
|
176
|
+
providers: [{ provide: UI_ICON_COLOR, useFactory: groupIconColor }],
|
|
90
177
|
template: `
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
178
|
+
@if (look() === 'glass') {
|
|
179
|
+
<!-- The frame is the group's box and, under clear glass, its dimming
|
|
180
|
+
layer, in the glass's shape. Before iOS 26 the glass is the thin
|
|
181
|
+
material, as the buttons' is. -->
|
|
182
|
+
<view [class]="frameClass()" [style.border-radius.px]="cornerRadius()">
|
|
183
|
+
<blur-view
|
|
184
|
+
ui-glass
|
|
185
|
+
[shape]="capsule() ? 'capsule' : 'rounded'"
|
|
186
|
+
[radius]="radius()"
|
|
187
|
+
[variant]="variant() === 'glass-clear' ? 'clear' : 'regular'"
|
|
188
|
+
material="thin"
|
|
189
|
+
[interactive]="interactive()"
|
|
190
|
+
[class]="glassClass()"
|
|
191
|
+
>
|
|
192
|
+
<ng-container [ngTemplateOutlet]="items" />
|
|
193
|
+
</blur-view>
|
|
194
|
+
</view>
|
|
195
|
+
} @else {
|
|
196
|
+
<view [class]="containerClass()">
|
|
197
|
+
<ng-container [ngTemplateOutlet]="items" />
|
|
198
|
+
</view>
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
<!-- One copy of the items, for whichever form the group takes: Angular
|
|
202
|
+
projects content once. -->
|
|
203
|
+
<ng-template #items><ng-content /></ng-template>
|
|
94
204
|
`,
|
|
95
205
|
})
|
|
96
206
|
export class UiButtonGroup {
|
|
97
207
|
readonly orientation = input<ButtonGroupOrientation>('horizontal');
|
|
208
|
+
/**
|
|
209
|
+
* `'default'` or `'outline'`, or `'glass'` / `'glass-clear'` for one glass
|
|
210
|
+
* background (see GlassButtonGroupVariant).
|
|
211
|
+
*/
|
|
98
212
|
readonly variant = input<ButtonGroupVariant>('default');
|
|
213
|
+
/** The items' height and padding; `'icon'` makes square icon-only items. */
|
|
99
214
|
readonly size = input<ButtonGroupSize>('default');
|
|
100
|
-
readonly disabled = input(false);
|
|
215
|
+
readonly disabled = input(false, { transform: booleanAttribute });
|
|
101
216
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
102
217
|
|
|
218
|
+
readonly #settings = inject(LynxAccessibilitySettings);
|
|
219
|
+
readonly #design = injectGlassDesign();
|
|
220
|
+
readonly #systemDrawsGlass = injectSystemDrawsGlass();
|
|
221
|
+
|
|
222
|
+
/** The items in the group, in order. */
|
|
223
|
+
readonly items = contentChildren(itemType(), { descendants: true });
|
|
224
|
+
|
|
225
|
+
/** The form the group takes on this platform, for its items. */
|
|
226
|
+
readonly look = computed<ButtonGroupLook>(() => {
|
|
227
|
+
const variant = this.variant();
|
|
228
|
+
if (variant !== 'glass' && variant !== 'glass-clear') return 'solid';
|
|
229
|
+
return this.#design() === 'material' ? 'material' : 'glass';
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The glass's corner radius, in px: half an item's height plus the inset,
|
|
234
|
+
* the ends a capsule of the items would have. A column of wider items
|
|
235
|
+
* takes it for its corners.
|
|
236
|
+
*/
|
|
237
|
+
protected readonly radius = computed(
|
|
238
|
+
() => ITEM_HEIGHTS[this.size()] / 2 + GLASS_INSET,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The corner radius of an item's pressed highlight, in px. In glass it's
|
|
243
|
+
* concentric with the group's corners, so the gap between them stays even
|
|
244
|
+
* all the way round; in Material's connected group it's the small inner
|
|
245
|
+
* corner, and an item at one of the group's ends clips it to its round
|
|
246
|
+
* end (see materialEndRadius).
|
|
247
|
+
*/
|
|
248
|
+
readonly itemRadius = computed(() =>
|
|
249
|
+
this.look() === 'material'
|
|
250
|
+
? MATERIAL_INNER_RADIUS
|
|
251
|
+
: concentricRadius(this.radius(), GLASS_INSET),
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* A row, or a column of square icon items, is a capsule. A column of
|
|
256
|
+
* wider items has round corners instead. A capsule takes `rounded-full`
|
|
257
|
+
* rather than a radius of half its height, which iOS draws with
|
|
258
|
+
* squared-off ends.
|
|
259
|
+
*/
|
|
260
|
+
protected readonly capsule = computed(
|
|
261
|
+
() => this.orientation() === 'horizontal' || this.size() === 'icon',
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
/** The frame's corners, under glass that isn't a capsule. */
|
|
265
|
+
protected readonly cornerRadius = computed(() =>
|
|
266
|
+
this.capsule() ? null : this.radius(),
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* How round Material's group is at its ends, in px: half an item's
|
|
271
|
+
* height, which makes a capsule of a row or of a column of square icons,
|
|
272
|
+
* and round corners on a column of wider items. The items at the ends
|
|
273
|
+
* draw them (see UiButtonGroupItem's cornerStyle).
|
|
274
|
+
*/
|
|
275
|
+
readonly materialEndRadius = computed(() => ITEM_HEIGHTS[this.size()] / 2);
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* As UiButton's: the glass reacts to touch, unless Reduce Motion is on
|
|
279
|
+
* where the runtime draws it.
|
|
280
|
+
*/
|
|
281
|
+
protected readonly interactive = computed(
|
|
282
|
+
() =>
|
|
283
|
+
!this.disabled() &&
|
|
284
|
+
(this.#systemDrawsGlass || !this.#settings.reduceMotion()),
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Clear glass that's see-through (not the opaque surface Reduce
|
|
289
|
+
* Transparency makes of the runtime's glass) sits on the dimming layer,
|
|
290
|
+
* and its items turn white.
|
|
291
|
+
*/
|
|
292
|
+
readonly #dimmed = computed(
|
|
293
|
+
() =>
|
|
294
|
+
this.variant() === 'glass-clear' &&
|
|
295
|
+
this.look() === 'glass' &&
|
|
296
|
+
(this.#systemDrawsGlass || !this.#settings.reduceTransparency()),
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
/** The items' label color, as a `var()`, for projected icons. */
|
|
300
|
+
readonly foreground = computed(() => {
|
|
301
|
+
const variant = this.variant();
|
|
302
|
+
if (variant !== 'glass' && variant !== 'glass-clear') {
|
|
303
|
+
return SOLID_FOREGROUNDS[variant];
|
|
304
|
+
}
|
|
305
|
+
if (this.look() === 'material') return 'var(--secondary-foreground)';
|
|
306
|
+
return this.#dimmed()
|
|
307
|
+
? 'var(--glass-clear-foreground)'
|
|
308
|
+
: 'var(--glass-foreground)';
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
readonly #direction = computed(() =>
|
|
312
|
+
this.orientation() === 'vertical' ? 'flex-col' : 'flex-row',
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
protected readonly frameClass = computed(() =>
|
|
316
|
+
cn(
|
|
317
|
+
// `self-start` stops the group stretching to fill a column parent so
|
|
318
|
+
// it hugs its buttons like a real toolbar. Pass `class="w-full"` to
|
|
319
|
+
// override.
|
|
320
|
+
'flex-col self-start flex',
|
|
321
|
+
this.capsule() && 'rounded-full',
|
|
322
|
+
this.#dimmed() && 'bg-glass-dim',
|
|
323
|
+
this.userClass(),
|
|
324
|
+
),
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
protected readonly glassClass = computed(() =>
|
|
328
|
+
cn('p-1 flex', this.#direction()),
|
|
329
|
+
);
|
|
330
|
+
|
|
103
331
|
protected readonly containerClass = computed(() =>
|
|
104
332
|
cn(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
333
|
+
this.look() === 'material'
|
|
334
|
+
? // Material's connected group: the items, 2px apart, show the page
|
|
335
|
+
// between them. No clip: the end items round the group's ends
|
|
336
|
+
// themselves, and a clip would cut their outlines.
|
|
337
|
+
'gap-0.5 flex'
|
|
338
|
+
: // The dividers between items are the group's own `bg-border`
|
|
339
|
+
// showing through a 1px `gap-px` seam — no per-item borders needed,
|
|
340
|
+
// which keeps items position-agnostic. `overflow-hidden` +
|
|
341
|
+
// `rounded-md` then clip the square item corners so the bar reads
|
|
342
|
+
// as one rounded control.
|
|
343
|
+
'gap-px rounded-md border border-border bg-border flex overflow-hidden',
|
|
112
344
|
'self-start',
|
|
113
|
-
this
|
|
345
|
+
this.#direction(),
|
|
114
346
|
this.userClass(),
|
|
115
347
|
),
|
|
116
348
|
);
|
|
@@ -122,13 +354,16 @@ export class UiButtonGroup {
|
|
|
122
354
|
*/
|
|
123
355
|
@Component({
|
|
124
356
|
selector: 'ui-button-group-item',
|
|
125
|
-
standalone: true,
|
|
126
357
|
imports: [LYNX_ELEMENTS],
|
|
127
358
|
encapsulation: ViewEncapsulation.None,
|
|
128
359
|
template: `
|
|
129
360
|
<view
|
|
130
361
|
#container
|
|
131
362
|
[class]="containerClass()"
|
|
363
|
+
[style]="cornerStyle()"
|
|
364
|
+
[attr.accessibility-element]="accessibilityLabel() ? 'true' : null"
|
|
365
|
+
[attr.accessibility-traits]="accessibilityLabel() ? 'button' : null"
|
|
366
|
+
[attr.accessibility-label]="accessibilityLabel() || null"
|
|
132
367
|
(bindtouchstart)="onPressStart()"
|
|
133
368
|
(bindtouchend)="onPressEnd()"
|
|
134
369
|
(bindtouchcancel)="onPressCancel()"
|
|
@@ -137,68 +372,177 @@ export class UiButtonGroup {
|
|
|
137
372
|
(bindmouseleave)="onPressCancel()"
|
|
138
373
|
(bindtap)="onTap()"
|
|
139
374
|
>
|
|
140
|
-
|
|
375
|
+
@if (highlightClass(); as highlight) {
|
|
376
|
+
<view [class]="highlight" [style.border-radius.px]="radius()"></view>
|
|
377
|
+
}
|
|
378
|
+
<ng-content select="ui-icon" />
|
|
379
|
+
@if (showLabel()) {
|
|
380
|
+
<text
|
|
381
|
+
[attr.accessibility-traits]="accessibilityLabel() ? null : 'button'"
|
|
382
|
+
[class]="labelClass()"
|
|
383
|
+
><ng-content
|
|
384
|
+
/></text>
|
|
385
|
+
}
|
|
141
386
|
</view>
|
|
142
387
|
`,
|
|
143
388
|
})
|
|
144
389
|
export class UiButtonGroupItem {
|
|
145
390
|
readonly #group = inject(UiButtonGroup);
|
|
391
|
+
readonly #settings = inject(LynxAccessibilitySettings);
|
|
146
392
|
|
|
147
|
-
readonly disabled = input(false);
|
|
393
|
+
readonly disabled = input(false, { transform: booleanAttribute });
|
|
394
|
+
/**
|
|
395
|
+
* What a screen reader says for the item. An icon-only item needs one; an
|
|
396
|
+
* item with a label reads its label without it.
|
|
397
|
+
*/
|
|
398
|
+
readonly accessibilityLabel = input<string | undefined>(undefined, {
|
|
399
|
+
alias: 'accessibility-label',
|
|
400
|
+
});
|
|
148
401
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
149
402
|
|
|
150
403
|
readonly pressed = output<void>();
|
|
151
404
|
|
|
152
405
|
readonly containerRef = viewChild<ElementRef>('container');
|
|
406
|
+
/** A `<ui-icon>` projected into the item, which sits before the label. */
|
|
407
|
+
readonly projectedIcon = contentChild(iconType());
|
|
153
408
|
#pressAnim?: AnimationHandle;
|
|
154
409
|
|
|
410
|
+
/** A finger (or the mouse) is down on the item. */
|
|
411
|
+
protected readonly pressing = signal(false);
|
|
412
|
+
|
|
155
413
|
// Disabled when either the whole group or this specific item is disabled.
|
|
156
414
|
protected readonly isDisabled = computed(
|
|
157
415
|
() => this.#group.disabled() || this.disabled(),
|
|
158
416
|
);
|
|
159
417
|
|
|
160
|
-
protected readonly
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
418
|
+
protected readonly radius = computed(() => this.#group.itemRadius());
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Whether the item is the group's first and its last, which round the
|
|
422
|
+
* group's ends in Material. Found from the group's items, as Lynx's CSS
|
|
423
|
+
* has no `:first-child`/`:last-child`.
|
|
424
|
+
*/
|
|
425
|
+
readonly #first = computed(() => this.#group.items()[0] === this);
|
|
426
|
+
readonly #last = computed(() => {
|
|
427
|
+
const items = this.#group.items();
|
|
428
|
+
return items[items.length - 1] === this;
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* A Material item's corners: small between items, and round at the
|
|
433
|
+
* group's ends, as Material's connected buttons are. The end items draw
|
|
434
|
+
* the ends themselves, rather than the group clipping them, so that the
|
|
435
|
+
* outline Increase Contrast gives them follows the curve. Inline, as how
|
|
436
|
+
* round the ends are depends on the size.
|
|
437
|
+
*/
|
|
438
|
+
protected readonly cornerStyle = computed(() => {
|
|
439
|
+
if (this.#group.look() !== 'material') return null;
|
|
440
|
+
const inner = `${MATERIAL_INNER_RADIUS}px`;
|
|
441
|
+
const end = `${this.#group.materialEndRadius()}px`;
|
|
442
|
+
// The left or top corners, and the right or bottom ones.
|
|
443
|
+
const start = this.#first() ? end : inner;
|
|
444
|
+
const finish = this.#last() ? end : inner;
|
|
445
|
+
const column = this.#group.orientation() === 'vertical';
|
|
446
|
+
return {
|
|
447
|
+
'border-top-left-radius': start,
|
|
448
|
+
'border-top-right-radius': column ? start : finish,
|
|
449
|
+
'border-bottom-right-radius': finish,
|
|
450
|
+
'border-bottom-left-radius': column ? finish : start,
|
|
451
|
+
};
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Glass and Material items show a highlight while pressed, in place: the
|
|
456
|
+
* glass around them is what reacts on iOS, and Material marks a press with
|
|
457
|
+
* a state layer. Solid items shrink, or dim under Reduce Motion.
|
|
458
|
+
*/
|
|
459
|
+
readonly #highlights = computed(() => this.#group.look() !== 'solid');
|
|
460
|
+
|
|
461
|
+
protected readonly showLabel = computed(
|
|
462
|
+
() => !(this.projectedIcon() && this.#group.size() === 'icon'),
|
|
169
463
|
);
|
|
170
464
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
)
|
|
465
|
+
readonly #gap = computed(
|
|
466
|
+
() => !!this.projectedIcon() && this.showLabel() && 'gap-2',
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
protected readonly containerClass = computed(() => {
|
|
470
|
+
const variant = this.#group.variant();
|
|
471
|
+
const size = this.#group.size();
|
|
472
|
+
const look = this.#group.look();
|
|
473
|
+
const disabled = this.isDisabled() && 'opacity-50';
|
|
474
|
+
if (variant !== 'glass' && variant !== 'glass-clear') {
|
|
475
|
+
return cn(
|
|
476
|
+
buttonGroupItemVariants({ variant, size }),
|
|
477
|
+
this.#gap(),
|
|
478
|
+
this.pressing() && this.#settings.reduceMotion() && 'opacity-70',
|
|
479
|
+
disabled,
|
|
480
|
+
this.userClass(),
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
return cn(
|
|
484
|
+
'flex-row items-center flex relative justify-center',
|
|
485
|
+
ITEM_SIZES[size],
|
|
486
|
+
look === 'material' &&
|
|
487
|
+
cn(
|
|
488
|
+
'bg-secondary overflow-hidden',
|
|
489
|
+
this.#settings.increaseContrast() && 'border border-foreground',
|
|
490
|
+
),
|
|
491
|
+
this.#gap(),
|
|
492
|
+
disabled,
|
|
493
|
+
this.userClass(),
|
|
494
|
+
);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* The pressed highlight: the label's color at a low opacity, under the
|
|
499
|
+
* content, in the item's concentric corners. It comes and goes with the
|
|
500
|
+
* finger, with no motion.
|
|
501
|
+
*/
|
|
502
|
+
protected readonly highlightClass = computed(
|
|
503
|
+
() =>
|
|
504
|
+
this.pressing() &&
|
|
505
|
+
this.#highlights() &&
|
|
506
|
+
cn(
|
|
507
|
+
'h-full w-full absolute left-0 top-0',
|
|
508
|
+
this.#group.look() === 'material'
|
|
509
|
+
? 'bg-secondary-foreground opacity-10'
|
|
510
|
+
: 'bg-glass-foreground opacity-15',
|
|
511
|
+
),
|
|
178
512
|
);
|
|
179
513
|
|
|
514
|
+
protected readonly labelClass = computed(() => {
|
|
515
|
+
const variant = this.#group.variant();
|
|
516
|
+
let color: string;
|
|
517
|
+
if (variant !== 'glass' && variant !== 'glass-clear') {
|
|
518
|
+
color = SOLID_LABEL_COLORS[variant];
|
|
519
|
+
} else if (this.#group.look() === 'material') {
|
|
520
|
+
color = 'text-secondary-foreground';
|
|
521
|
+
} else {
|
|
522
|
+
// The glass points this at the clear foreground on clear glass.
|
|
523
|
+
color = 'text-glass-foreground';
|
|
524
|
+
}
|
|
525
|
+
return cn('font-medium', LABEL_SIZES[this.#group.size()], color);
|
|
526
|
+
});
|
|
527
|
+
|
|
180
528
|
protected onPressStart(): void {
|
|
181
529
|
if (this.isDisabled()) return;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
530
|
+
this.pressing.set(true);
|
|
531
|
+
if (!this.#scales()) return;
|
|
532
|
+
// See UiButton: press feedback is an el.setStyle()-driven CSS transition.
|
|
185
533
|
this.#pressAnim?.cancel();
|
|
186
534
|
this.#pressAnim = pressDown(this.containerRef()?.nativeElement);
|
|
187
535
|
}
|
|
188
536
|
|
|
189
537
|
protected onPressEnd(): void {
|
|
190
538
|
if (this.isDisabled()) return;
|
|
191
|
-
|
|
192
|
-
this.#pressAnim?.cancel();
|
|
193
|
-
this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
|
|
539
|
+
this.#release();
|
|
194
540
|
}
|
|
195
541
|
|
|
196
542
|
protected onPressCancel(): void {
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
this.#pressAnim?.cancel();
|
|
201
|
-
this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
|
|
543
|
+
// Release on cancel too — otherwise a touch stolen by a scroll gesture
|
|
544
|
+
// leaves the item stuck in its pressed-down state.
|
|
545
|
+
this.#release();
|
|
202
546
|
}
|
|
203
547
|
|
|
204
548
|
protected onTap(): void {
|
|
@@ -206,4 +550,18 @@ export class UiButtonGroupItem {
|
|
|
206
550
|
this.pressed.emit();
|
|
207
551
|
}
|
|
208
552
|
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Solid items shrink under the finger, unless Reduce Motion is on.
|
|
556
|
+
*/
|
|
557
|
+
#scales(): boolean {
|
|
558
|
+
return !this.#highlights() && !this.#settings.reduceMotion();
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
#release(): void {
|
|
562
|
+
this.pressing.set(false);
|
|
563
|
+
if (!this.#scales()) return;
|
|
564
|
+
this.#pressAnim?.cancel();
|
|
565
|
+
this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
|
|
566
|
+
}
|
|
209
567
|
}
|
|
@@ -2,7 +2,10 @@ export {
|
|
|
2
2
|
UiButtonGroup,
|
|
3
3
|
UiButtonGroupItem,
|
|
4
4
|
buttonGroupItemVariants,
|
|
5
|
+
type ButtonGroupLook,
|
|
5
6
|
type ButtonGroupVariant,
|
|
6
7
|
type ButtonGroupSize,
|
|
7
8
|
type ButtonGroupOrientation,
|
|
9
|
+
type GlassButtonGroupVariant,
|
|
10
|
+
type SolidButtonGroupVariant,
|
|
8
11
|
} from './button-group';
|