@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
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
DestroyRef,
|
|
4
|
+
Directive,
|
|
5
|
+
ElementRef,
|
|
6
|
+
InjectionToken,
|
|
7
|
+
ViewEncapsulation,
|
|
8
|
+
booleanAttribute,
|
|
9
|
+
computed,
|
|
10
|
+
effect,
|
|
11
|
+
inject,
|
|
12
|
+
input,
|
|
13
|
+
model,
|
|
14
|
+
output,
|
|
15
|
+
signal,
|
|
16
|
+
viewChild,
|
|
17
|
+
} from '@angular/core';
|
|
18
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
19
|
+
|
|
20
|
+
import { cn } from '../../utils/cn';
|
|
21
|
+
import { injectGlassDesign } from '../glass/glass-design';
|
|
22
|
+
import { UiIcon } from '../icon/icon';
|
|
23
|
+
import { type IconName } from '../icon/icons';
|
|
24
|
+
import { type MenuElement, UiMenuPopup } from './menu-popup';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* AngularLynx's element, which can add an accessibility trait to the app's
|
|
28
|
+
* own (see LynxElement.setAccessibilityTrait).
|
|
29
|
+
*/
|
|
30
|
+
type AccessibleElement = {
|
|
31
|
+
setAccessibilityTrait(trait: string, on: boolean): void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* What a menu's items need from the menu they're in: `ui-menu` or
|
|
36
|
+
* `ui-context-menu`.
|
|
37
|
+
*/
|
|
38
|
+
export type UiMenuHost = {
|
|
39
|
+
/** Closes the menu, as choosing an item does. */
|
|
40
|
+
close(): void;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The menu an item is in. `ui-menu` and `ui-context-menu` provide it, so the
|
|
45
|
+
* same items, sections and separators work in both.
|
|
46
|
+
*/
|
|
47
|
+
export const UI_MENU = new InjectionToken<UiMenuHost>('UI_MENU');
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A menu item: a label, with an optional icon, that does something and
|
|
51
|
+
* closes the menu.
|
|
52
|
+
*
|
|
53
|
+
* ```html
|
|
54
|
+
* <ui-menu-item label="Copy" icon="copy" (pressed)="copy()" />
|
|
55
|
+
* <ui-menu-item label="Delete" icon="trash" variant="destructive" (pressed)="remove()" />
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* Each design lays it out as its platform does: Apple's 44pt rows, with the
|
|
59
|
+
* icon before the label from iOS 26 and after it before, and Material 3's
|
|
60
|
+
* 48dp rows with a leading icon. A screen reader reads it as a button with
|
|
61
|
+
* its label, and a destructive item as "Delete, destructive".
|
|
62
|
+
*/
|
|
63
|
+
@Component({
|
|
64
|
+
selector: 'ui-menu-item',
|
|
65
|
+
imports: [LYNX_ELEMENTS, UiIcon],
|
|
66
|
+
encapsulation: ViewEncapsulation.None,
|
|
67
|
+
template: `
|
|
68
|
+
<view
|
|
69
|
+
#row
|
|
70
|
+
[class]="rowClass()"
|
|
71
|
+
[accessibility-element]="true"
|
|
72
|
+
accessibility-traits="button"
|
|
73
|
+
[accessibility-label]="spokenLabel()"
|
|
74
|
+
(bindtouchstart)="hold(true)"
|
|
75
|
+
(bindtouchend)="hold(false)"
|
|
76
|
+
(bindtouchcancel)="hold(false)"
|
|
77
|
+
(bindmousedown)="hold(true)"
|
|
78
|
+
(bindmouseup)="hold(false)"
|
|
79
|
+
(bindmouseleave)="hold(false)"
|
|
80
|
+
(bindtap)="choose()"
|
|
81
|
+
>
|
|
82
|
+
<!-- The pressed highlight: the glass's own foreground at a tenth, so it
|
|
83
|
+
suits light and dark glass, and is Material's state layer. -->
|
|
84
|
+
<view
|
|
85
|
+
class="h-full w-full bg-glass-foreground absolute left-0 top-0"
|
|
86
|
+
[style.opacity]="held() ? 0.1 : 0"
|
|
87
|
+
></view>
|
|
88
|
+
@if (checked() !== undefined) {
|
|
89
|
+
<!-- A checkable item keeps the column for the checkmark when it's
|
|
90
|
+
unchecked, so the labels of a group line up. -->
|
|
91
|
+
<view class="h-5 w-5 items-center flex justify-center">
|
|
92
|
+
@if (checked()) {
|
|
93
|
+
<ui-icon name="check" size="sm" [color]="iconColor()" />
|
|
94
|
+
}
|
|
95
|
+
</view>
|
|
96
|
+
}
|
|
97
|
+
@if (leadingIcon(); as name) {
|
|
98
|
+
<ui-icon [name]="name" [size]="iconSize()" [color]="iconColor()" />
|
|
99
|
+
}
|
|
100
|
+
<text [class]="labelClass()">{{ label() }}</text>
|
|
101
|
+
@if (trailingIcon(); as name) {
|
|
102
|
+
<ui-icon [name]="name" [size]="iconSize()" [color]="iconColor()" />
|
|
103
|
+
}
|
|
104
|
+
</view>
|
|
105
|
+
`,
|
|
106
|
+
})
|
|
107
|
+
export class UiMenuItem {
|
|
108
|
+
readonly #menu = inject(UI_MENU);
|
|
109
|
+
readonly #design = injectGlassDesign();
|
|
110
|
+
|
|
111
|
+
/** What the item does, which is also what a screen reader reads. */
|
|
112
|
+
readonly label = input.required<string>();
|
|
113
|
+
/** An icon for a common action, from the icon set. */
|
|
114
|
+
readonly icon = input<IconName | undefined>(undefined);
|
|
115
|
+
/** `'destructive'` for an action that deletes or can't be undone. */
|
|
116
|
+
readonly variant = input<'default' | 'destructive'>('default');
|
|
117
|
+
/**
|
|
118
|
+
* Whether the item shows a checkmark, for an option that's on, such as the
|
|
119
|
+
* current sort order. Leave it unset for an item that's never checked;
|
|
120
|
+
* set it, true or false, on each item of a group of options, so their
|
|
121
|
+
* labels line up.
|
|
122
|
+
*/
|
|
123
|
+
readonly checked = input<boolean | undefined>(undefined);
|
|
124
|
+
/** Shows the item without letting it be chosen. */
|
|
125
|
+
readonly disabled = input(false, { transform: booleanAttribute });
|
|
126
|
+
/**
|
|
127
|
+
* What a screen reader reads, when the label isn't enough. By default the
|
|
128
|
+
* label, and ", destructive" after it for a destructive item.
|
|
129
|
+
*/
|
|
130
|
+
readonly accessibilityLabel = input<string | undefined>(undefined);
|
|
131
|
+
|
|
132
|
+
/** The item was chosen. The menu closes. */
|
|
133
|
+
readonly pressed = output<void>();
|
|
134
|
+
|
|
135
|
+
/** Whether a finger (or the mouse) is down on the item. */
|
|
136
|
+
protected readonly held = signal(false);
|
|
137
|
+
|
|
138
|
+
// Required: always in the template.
|
|
139
|
+
readonly row = viewChild.required<ElementRef<AccessibleElement>>('row');
|
|
140
|
+
|
|
141
|
+
readonly #material = computed(() => this.#design() === 'material');
|
|
142
|
+
readonly #destructive = computed(() => this.variant() === 'destructive');
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The icon goes before the label, except before iOS 26, whose menus put it
|
|
146
|
+
* at the end of the row.
|
|
147
|
+
*/
|
|
148
|
+
readonly #iconLeads = computed(() => this.#design() !== 'frosted');
|
|
149
|
+
protected readonly leadingIcon = computed(() =>
|
|
150
|
+
this.#iconLeads() ? this.icon() : undefined,
|
|
151
|
+
);
|
|
152
|
+
protected readonly trailingIcon = computed(() =>
|
|
153
|
+
this.#iconLeads() ? undefined : this.icon(),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
protected readonly rowClass = computed(() =>
|
|
157
|
+
cn(
|
|
158
|
+
'flex-row items-center gap-3 flex relative',
|
|
159
|
+
// A row grows when its label wraps.
|
|
160
|
+
this.#material()
|
|
161
|
+
? 'min-h-[48px] px-3 py-3'
|
|
162
|
+
: 'min-h-[44px] px-4 py-[11px]',
|
|
163
|
+
this.disabled() && 'opacity-40',
|
|
164
|
+
),
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* A checked item is `selected` to a screen reader, which iOS reads.
|
|
169
|
+
* Through the runtime, which adds the trait to `button` on iOS and leaves
|
|
170
|
+
* Android, which reads one trait, with `button`.
|
|
171
|
+
*/
|
|
172
|
+
protected readonly selectedTrait = effect(() => {
|
|
173
|
+
this.row().nativeElement.setAccessibilityTrait(
|
|
174
|
+
'selected',
|
|
175
|
+
this.checked() === true,
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
protected readonly labelClass = computed(() =>
|
|
179
|
+
cn(
|
|
180
|
+
'flex-1',
|
|
181
|
+
this.#material() ? 'text-[16px]' : 'text-[17px]',
|
|
182
|
+
this.#destructive() ? 'text-destructive' : 'text-glass-foreground',
|
|
183
|
+
),
|
|
184
|
+
);
|
|
185
|
+
protected readonly iconSize = computed(() =>
|
|
186
|
+
this.#material() ? 'md' : 'sm',
|
|
187
|
+
);
|
|
188
|
+
protected readonly iconColor = computed(() =>
|
|
189
|
+
this.#destructive() ? 'var(--destructive)' : 'var(--glass-foreground)',
|
|
190
|
+
);
|
|
191
|
+
protected readonly spokenLabel = computed(
|
|
192
|
+
() =>
|
|
193
|
+
this.accessibilityLabel() ??
|
|
194
|
+
(this.#destructive() ? `${this.label()}, destructive` : this.label()),
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
protected hold(down: boolean): void {
|
|
198
|
+
this.held.set(down && !this.disabled());
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
protected choose(): void {
|
|
202
|
+
if (this.disabled()) return;
|
|
203
|
+
this.pressed.emit();
|
|
204
|
+
this.#menu.close();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* A group of menu items, with an optional title above them.
|
|
210
|
+
*
|
|
211
|
+
* ```html
|
|
212
|
+
* <ui-menu-section title="Sort by">
|
|
213
|
+
* <ui-menu-item label="Name" />
|
|
214
|
+
* <ui-menu-item label="Date" />
|
|
215
|
+
* </ui-menu-section>
|
|
216
|
+
* ```
|
|
217
|
+
*
|
|
218
|
+
* Put a `<ui-menu-separator />` between sections.
|
|
219
|
+
*/
|
|
220
|
+
@Component({
|
|
221
|
+
selector: 'ui-menu-section',
|
|
222
|
+
imports: [LYNX_ELEMENTS],
|
|
223
|
+
encapsulation: ViewEncapsulation.None,
|
|
224
|
+
template: `
|
|
225
|
+
<view class="flex-col flex">
|
|
226
|
+
@if (title()) {
|
|
227
|
+
<text [class]="titleClass()" accessibility-traits="header">
|
|
228
|
+
{{ title() }}
|
|
229
|
+
</text>
|
|
230
|
+
}
|
|
231
|
+
<ng-content />
|
|
232
|
+
</view>
|
|
233
|
+
`,
|
|
234
|
+
})
|
|
235
|
+
export class UiMenuSection {
|
|
236
|
+
readonly #design = injectGlassDesign();
|
|
237
|
+
|
|
238
|
+
/** A title for the section, such as "Sort by". */
|
|
239
|
+
readonly title = input<string | undefined>(undefined);
|
|
240
|
+
|
|
241
|
+
protected readonly titleClass = computed(() =>
|
|
242
|
+
cn(
|
|
243
|
+
'pb-1 pt-2 text-[13px] text-glass-muted-foreground',
|
|
244
|
+
this.#design() === 'material' ? 'px-3' : 'px-4',
|
|
245
|
+
),
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* A line between a menu's sections. Before iOS 26, Apple's menus parted
|
|
251
|
+
* sections with a thick band rather than a line.
|
|
252
|
+
*/
|
|
253
|
+
@Component({
|
|
254
|
+
selector: 'ui-menu-separator',
|
|
255
|
+
imports: [LYNX_ELEMENTS],
|
|
256
|
+
encapsulation: ViewEncapsulation.None,
|
|
257
|
+
template: `<view [class]="lineClass()"></view>`,
|
|
258
|
+
})
|
|
259
|
+
export class UiMenuSeparator {
|
|
260
|
+
readonly #design = injectGlassDesign();
|
|
261
|
+
|
|
262
|
+
protected readonly lineClass = computed(() => {
|
|
263
|
+
switch (this.#design()) {
|
|
264
|
+
case 'glass':
|
|
265
|
+
return 'mx-4 my-1.5 h-px bg-glass-foreground opacity-20';
|
|
266
|
+
case 'frosted':
|
|
267
|
+
return 'h-2 bg-glass-foreground opacity-10';
|
|
268
|
+
default:
|
|
269
|
+
return 'my-2 h-px bg-border';
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* The `ui-menu` a trigger is in, for `UI_MENU`. A named function rather than
|
|
276
|
+
* an inline factory: the component's metadata keeps its own copy of an
|
|
277
|
+
* inline one, which nothing calls, so an app's coverage report would show it
|
|
278
|
+
* as uncovered.
|
|
279
|
+
*/
|
|
280
|
+
const pullDownMenu = (): UiMenuHost => inject(UiMenu);
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* A pull-down menu: a glass menu that grows out of the button that opens it.
|
|
284
|
+
*
|
|
285
|
+
* ```html
|
|
286
|
+
* <ui-menu>
|
|
287
|
+
* <ui-button uiMenuTrigger variant="outline">Sort</ui-button>
|
|
288
|
+
* <ui-menu-item label="Name" icon="type" (pressed)="sortBy('name')" />
|
|
289
|
+
* <ui-menu-item label="Date" icon="calendar" (pressed)="sortBy('date')" />
|
|
290
|
+
* </ui-menu>
|
|
291
|
+
* ```
|
|
292
|
+
*
|
|
293
|
+
* Mark the element that opens it with `uiMenuTrigger`; everything else
|
|
294
|
+
* inside it is the menu. A tap on the trigger opens it; a tap outside it,
|
|
295
|
+
* choosing an item, or Android's back button closes it. `[(open)]` opens and
|
|
296
|
+
* closes it from code.
|
|
297
|
+
*
|
|
298
|
+
* On iOS 26 and later it's Liquid Glass, and grows out of its trigger: one
|
|
299
|
+
* glass shape whose frame grows from the trigger's to the menu's, which the
|
|
300
|
+
* glass follows as it goes, with the trigger hidden under it. Before iOS 26
|
|
301
|
+
* it's the frosted material. On Android it's a Material 3 menu that unfolds
|
|
302
|
+
* below its trigger, or Liquid Glass with `provideGlass({ android: 'glass' })`.
|
|
303
|
+
* It stays on screen and inside the safe area, and opens upward near the
|
|
304
|
+
* bottom of the screen.
|
|
305
|
+
*/
|
|
306
|
+
@Component({
|
|
307
|
+
selector: 'ui-menu',
|
|
308
|
+
imports: [LYNX_ELEMENTS, UiMenuPopup],
|
|
309
|
+
encapsulation: ViewEncapsulation.None,
|
|
310
|
+
providers: [{ provide: UI_MENU, useFactory: pullDownMenu }],
|
|
311
|
+
template: `
|
|
312
|
+
<ng-content />
|
|
313
|
+
<ui-menu-popup
|
|
314
|
+
#popup
|
|
315
|
+
[open]="open()"
|
|
316
|
+
[anchor]="anchor()"
|
|
317
|
+
(dismissed)="close()"
|
|
318
|
+
(closed)="closed.emit()"
|
|
319
|
+
>
|
|
320
|
+
<ng-content select="ui-menu-item, ui-menu-section, ui-menu-separator" />
|
|
321
|
+
</ui-menu-popup>
|
|
322
|
+
`,
|
|
323
|
+
})
|
|
324
|
+
export class UiMenu implements UiMenuHost {
|
|
325
|
+
/** Whether the menu is open. */
|
|
326
|
+
readonly open = model(false);
|
|
327
|
+
/** The menu has closed and its close animation has finished. */
|
|
328
|
+
readonly closed = output<void>();
|
|
329
|
+
|
|
330
|
+
readonly #host = inject<ElementRef<MenuElement>>(ElementRef);
|
|
331
|
+
readonly #trigger = signal<MenuElement | null>(null);
|
|
332
|
+
|
|
333
|
+
// Required: always in the template.
|
|
334
|
+
readonly popup = viewChild.required<UiMenuPopup>('popup');
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* What the menu grows out of: its trigger, or the menu's own element when
|
|
338
|
+
* nothing is marked `uiMenuTrigger`.
|
|
339
|
+
*/
|
|
340
|
+
protected readonly anchor = computed(
|
|
341
|
+
() => this.#trigger() ?? this.#host.nativeElement,
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Whether the menu covers its trigger now: a glass or frosted menu grows
|
|
346
|
+
* out of it and shrinks back into it, so the trigger hides meanwhile, as
|
|
347
|
+
* on iOS. A Material menu opens beside it.
|
|
348
|
+
*/
|
|
349
|
+
readonly coversTrigger = computed(() => {
|
|
350
|
+
const popup = this.popup();
|
|
351
|
+
const phase = popup.phase();
|
|
352
|
+
return (
|
|
353
|
+
popup.design() !== 'material' &&
|
|
354
|
+
phase !== 'closed' &&
|
|
355
|
+
phase !== 'measuring'
|
|
356
|
+
);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
toggle(): void {
|
|
360
|
+
this.open.update((open) => !open);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
close(): void {
|
|
364
|
+
this.open.set(false);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* @internal Called by the `uiMenuTrigger` directive.
|
|
369
|
+
*/
|
|
370
|
+
setTrigger(trigger: MenuElement | null): void {
|
|
371
|
+
this.#trigger.set(trigger);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Marks the element that opens a `ui-menu`: a tap on it opens or closes the
|
|
377
|
+
* menu, and the menu grows out of it.
|
|
378
|
+
*
|
|
379
|
+
* ```html
|
|
380
|
+
* <ui-menu>
|
|
381
|
+
* <ui-button uiMenuTrigger>Options</ui-button>
|
|
382
|
+
* …
|
|
383
|
+
* </ui-menu>
|
|
384
|
+
* ```
|
|
385
|
+
*/
|
|
386
|
+
@Directive({
|
|
387
|
+
selector: '[uiMenuTrigger]',
|
|
388
|
+
host: {
|
|
389
|
+
'(click)': 'menu.toggle()',
|
|
390
|
+
'[style.opacity]': 'menu.coversTrigger() ? 0 : null',
|
|
391
|
+
},
|
|
392
|
+
})
|
|
393
|
+
export class UiMenuTrigger {
|
|
394
|
+
protected readonly menu = inject(UiMenu);
|
|
395
|
+
|
|
396
|
+
/** Points the menu at this element until the directive is destroyed. */
|
|
397
|
+
protected readonly unregister = registerTrigger(
|
|
398
|
+
this.menu,
|
|
399
|
+
inject<ElementRef<MenuElement>>(ElementRef).nativeElement,
|
|
400
|
+
inject(DestroyRef),
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Makes `trigger` the element `menu` grows out of, until `destroyRef` is
|
|
406
|
+
* destroyed. Returns what takes the registration back early.
|
|
407
|
+
*/
|
|
408
|
+
const registerTrigger = (
|
|
409
|
+
menu: UiMenu,
|
|
410
|
+
trigger: MenuElement,
|
|
411
|
+
destroyRef: DestroyRef,
|
|
412
|
+
): (() => void) => {
|
|
413
|
+
menu.setTrigger(trigger);
|
|
414
|
+
return destroyRef.onDestroy(() => menu.setTrigger(null));
|
|
415
|
+
};
|