@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
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
Component,
|
|
4
4
|
ViewEncapsulation,
|
|
5
5
|
computed,
|
|
6
|
+
contentChildren,
|
|
6
7
|
effect,
|
|
7
8
|
inject,
|
|
8
9
|
input,
|
|
@@ -11,7 +12,7 @@ import {
|
|
|
11
12
|
signal,
|
|
12
13
|
viewChild,
|
|
13
14
|
} from '@angular/core';
|
|
14
|
-
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
15
|
+
import { LYNX_ELEMENTS, LynxAccessibilitySettings } from '@blotch/angular-lynx';
|
|
15
16
|
|
|
16
17
|
import {
|
|
17
18
|
type AnimationHandle,
|
|
@@ -21,10 +22,37 @@ import {
|
|
|
21
22
|
revealIn,
|
|
22
23
|
} from '../../utils/animate';
|
|
23
24
|
import { cn } from '../../utils/cn';
|
|
25
|
+
import {
|
|
26
|
+
type SegmentedHost,
|
|
27
|
+
segmentPlace,
|
|
28
|
+
UI_SEGMENTED_HOST,
|
|
29
|
+
UiSegmentedControl,
|
|
30
|
+
UiSegmentedIndicator,
|
|
31
|
+
} from '../segmented-control/segmented-control';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A tab list's look: shadcn's tabs, or `'segmented'` for the platform's
|
|
35
|
+
* segmented control (see UiTabsList).
|
|
36
|
+
*/
|
|
37
|
+
export type TabsListVariant = 'default' | 'segmented';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* `UiTabsTrigger`, for the list's content query. A call rather than the
|
|
41
|
+
* class itself: for a class, Angular's test build keeps an arrow function
|
|
42
|
+
* that nothing calls, which an app's coverage report would show as
|
|
43
|
+
* uncovered.
|
|
44
|
+
*/
|
|
45
|
+
const triggerType = () => UiTabsTrigger;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The list, as the segmented control it provides reads it. Named, not
|
|
49
|
+
* inline: the component's metadata keeps its own copy of an inline factory,
|
|
50
|
+
* which nothing calls.
|
|
51
|
+
*/
|
|
52
|
+
const tabsListHost = (): SegmentedHost => inject(UiTabsList).segmentedHost;
|
|
24
53
|
|
|
25
54
|
@Component({
|
|
26
55
|
selector: 'ui-tabs',
|
|
27
|
-
standalone: true,
|
|
28
56
|
imports: [LYNX_ELEMENTS],
|
|
29
57
|
encapsulation: ViewEncapsulation.None,
|
|
30
58
|
template: `
|
|
@@ -44,8 +72,10 @@ export class UiTabs {
|
|
|
44
72
|
// When the active tab changes, we compare the old and new index in this
|
|
45
73
|
// list to determine slide direction (1 = right, -1 = left). This creates
|
|
46
74
|
// a spatial relationship: tabs feel like they're arranged left-to-right,
|
|
47
|
-
// with content sliding in from the direction of the selected tab.
|
|
48
|
-
|
|
75
|
+
// with content sliding in from the direction of the selected tab. A Set
|
|
76
|
+
// keeps the order tabs first registered in and ignores a re-registration
|
|
77
|
+
// (a trigger re-created by `@if` or `@for`).
|
|
78
|
+
readonly #tabOrder = new Set<string>();
|
|
49
79
|
#previousValue: string | undefined;
|
|
50
80
|
readonly direction = signal<1 | -1>(1);
|
|
51
81
|
|
|
@@ -53,43 +83,38 @@ export class UiTabs {
|
|
|
53
83
|
cn('flex-col flex', this.userClass()),
|
|
54
84
|
);
|
|
55
85
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// Defensive: signal dedupes same-value writes so the effect never re-runs
|
|
66
|
-
// with an unchanged `currentValue`. Kept for safety on-device.
|
|
67
|
-
/* v8 ignore next 1 */
|
|
68
|
-
if (currentValue === this.#previousValue) return;
|
|
69
|
-
|
|
70
|
-
const prevIndex = this.#tabOrder.indexOf(this.#previousValue);
|
|
71
|
-
const nextIndex = this.#tabOrder.indexOf(currentValue);
|
|
72
|
-
// The `prevIndex !== nextIndex` check is defensive — the earlier
|
|
73
|
-
// `if (currentValue === this.#previousValue) return;` guard makes an
|
|
74
|
-
// equal-index case unreachable in normal flow.
|
|
75
|
-
if (
|
|
76
|
-
prevIndex !== -1 &&
|
|
77
|
-
nextIndex !== -1 &&
|
|
78
|
-
/* v8 ignore next 1 */ prevIndex !== nextIndex
|
|
79
|
-
) {
|
|
80
|
-
this.direction.set(nextIndex > prevIndex ? 1 : -1);
|
|
81
|
-
}
|
|
86
|
+
/**
|
|
87
|
+
* Computes the slide direction when the active value changes, whether from
|
|
88
|
+
* select() or from the model being set. A field rather than a constructor:
|
|
89
|
+
* Angular's test build keeps a function for a constructor that nothing
|
|
90
|
+
* calls, which an app's coverage report would show as uncovered.
|
|
91
|
+
*/
|
|
92
|
+
protected readonly trackingDirection = effect(() => {
|
|
93
|
+
const currentValue = this.value();
|
|
94
|
+
if (this.#previousValue === undefined) {
|
|
82
95
|
this.#previousValue = currentValue;
|
|
83
|
-
|
|
84
|
-
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// The value can change and change back before the effect runs (two taps
|
|
99
|
+
// in one frame): the tab didn't move, so the direction stays.
|
|
100
|
+
if (currentValue === this.#previousValue) return;
|
|
101
|
+
|
|
102
|
+
const order = [...this.#tabOrder];
|
|
103
|
+
const prevIndex = order.indexOf(this.#previousValue);
|
|
104
|
+
const nextIndex = order.indexOf(currentValue);
|
|
105
|
+
// A value with no trigger has no place in the order to slide from or to.
|
|
106
|
+
// Two different registered values always have different indexes.
|
|
107
|
+
if (prevIndex !== -1 && nextIndex !== -1) {
|
|
108
|
+
this.direction.set(nextIndex > prevIndex ? 1 : -1);
|
|
109
|
+
}
|
|
110
|
+
this.#previousValue = currentValue;
|
|
111
|
+
});
|
|
85
112
|
|
|
86
113
|
/**
|
|
87
114
|
* Called by UiTabsTrigger to register ordering for direction detection.
|
|
88
115
|
*/
|
|
89
116
|
registerTab(value: string): void {
|
|
90
|
-
|
|
91
|
-
this.#tabOrder.push(value);
|
|
92
|
-
}
|
|
117
|
+
this.#tabOrder.add(value);
|
|
93
118
|
}
|
|
94
119
|
|
|
95
120
|
select(value: string): void {
|
|
@@ -98,52 +123,138 @@ export class UiTabs {
|
|
|
98
123
|
}
|
|
99
124
|
}
|
|
100
125
|
|
|
126
|
+
/**
|
|
127
|
+
* The row of tab triggers.
|
|
128
|
+
*
|
|
129
|
+
* `variant="segmented"` draws the platform's segmented control instead (see
|
|
130
|
+
* UiSegmentedControl), Apple's control for switching between closely
|
|
131
|
+
* related views: iOS 26's capsule with a thumb that lifts into Liquid Glass
|
|
132
|
+
* as it slides between the tabs (it can be dragged across them too), iOS 13
|
|
133
|
+
* to 18's raised thumb before iOS 26, and Material 3 Expressive's connected
|
|
134
|
+
* button group on Android. It fills the width it's given, and the tabs
|
|
135
|
+
* share it.
|
|
136
|
+
*
|
|
137
|
+
* The row is a tab bar to VoiceOver (`tabbar`), which reads each trigger as
|
|
138
|
+
* a tab, and the selected one as selected.
|
|
139
|
+
*/
|
|
101
140
|
@Component({
|
|
102
141
|
selector: 'ui-tabs-list',
|
|
103
|
-
|
|
104
|
-
imports: [LYNX_ELEMENTS],
|
|
142
|
+
imports: [LYNX_ELEMENTS, UiSegmentedIndicator],
|
|
105
143
|
encapsulation: ViewEncapsulation.None,
|
|
144
|
+
providers: [
|
|
145
|
+
UiSegmentedControl,
|
|
146
|
+
{ provide: UI_SEGMENTED_HOST, useFactory: tabsListHost },
|
|
147
|
+
],
|
|
148
|
+
// The mouse leaving the row lets go of an indicator dragged in the web
|
|
149
|
+
// preview, where a drag gets no touchcancel.
|
|
106
150
|
template: `
|
|
107
|
-
<view
|
|
151
|
+
<view
|
|
152
|
+
accessibility-traits="tabbar"
|
|
153
|
+
[class]="listClass()"
|
|
154
|
+
(bindmouseleave)="segmentedControl.cancel()"
|
|
155
|
+
>
|
|
156
|
+
@if (segmentedControl.underlay()) {
|
|
157
|
+
<ui-segmented-indicator />
|
|
158
|
+
}
|
|
108
159
|
<ng-content />
|
|
109
160
|
</view>
|
|
110
161
|
`,
|
|
111
162
|
})
|
|
112
163
|
export class UiTabsList {
|
|
164
|
+
/**
|
|
165
|
+
* `'default'`, or `'segmented'` for the platform's segmented control.
|
|
166
|
+
*/
|
|
167
|
+
readonly variant = input<TabsListVariant>('default');
|
|
113
168
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
114
169
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
this.
|
|
170
|
+
readonly #tabs = inject(UiTabs);
|
|
171
|
+
protected readonly segmentedControl = inject(UiSegmentedControl);
|
|
172
|
+
/** The triggers in the list, in order. */
|
|
173
|
+
readonly triggers = contentChildren(triggerType(), { descendants: true });
|
|
174
|
+
|
|
175
|
+
/** What the segmented control reads from the list. */
|
|
176
|
+
readonly segmentedHost: SegmentedHost = {
|
|
177
|
+
segmented: computed(() => this.variant() === 'segmented'),
|
|
178
|
+
segmentValues: computed(() =>
|
|
179
|
+
this.triggers().map((trigger) => trigger.triggerValue()),
|
|
125
180
|
),
|
|
181
|
+
selectedValues: computed(() => [this.#tabs.value()]),
|
|
182
|
+
// One tab is active at a time, and tabs are never disabled.
|
|
183
|
+
multiple: signal(false).asReadonly(),
|
|
184
|
+
disabled: signal(false).asReadonly(),
|
|
185
|
+
select: (value) => this.#tabs.select(value),
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
protected readonly listClass = computed(() =>
|
|
189
|
+
this.segmentedControl.enabled()
|
|
190
|
+
? // A segmented control fills the width it's given, as UIKit's does
|
|
191
|
+
// in a form, and its tabs share it.
|
|
192
|
+
cn(this.segmentedControl.trackClass(), this.userClass())
|
|
193
|
+
: cn(
|
|
194
|
+
// `self-start` keeps the tab bar hugging its triggers. The parent <ui-tabs>
|
|
195
|
+
// is a `flex flex-col` column, and in Lynx's linear/flex layout a child with
|
|
196
|
+
// no explicit width stretches to fill the parent's cross axis — so without
|
|
197
|
+
// it the muted pill spans the full width, leaving empty space around the
|
|
198
|
+
// tabs. Same lever as the badge fix: Lynx has no `inline-flex`, so
|
|
199
|
+
// align-self is how you opt a child into shrink-to-fit.
|
|
200
|
+
'flex-row items-center self-start rounded-lg bg-muted p-1 flex',
|
|
201
|
+
this.userClass(),
|
|
202
|
+
),
|
|
126
203
|
);
|
|
127
204
|
}
|
|
128
205
|
|
|
129
206
|
@Component({
|
|
130
207
|
selector: 'ui-tabs-trigger',
|
|
131
|
-
standalone: true,
|
|
132
208
|
imports: [LYNX_ELEMENTS],
|
|
133
209
|
encapsulation: ViewEncapsulation.None,
|
|
210
|
+
host: {
|
|
211
|
+
'[class]': 'hostClass()',
|
|
212
|
+
// The segmented control places its indicator from each tab's width.
|
|
213
|
+
'(bindlayoutchange)': 'measure($event)',
|
|
214
|
+
},
|
|
215
|
+
// The label is the accessibility element, a button (a tab, in the list's
|
|
216
|
+
// tab bar) named by its own text. A view would need a label of its own.
|
|
217
|
+
// The selected tab is `selected` to a screen reader, which iOS reads:
|
|
218
|
+
// lynxAccessibilityTraits adds the trait to `button` on iOS, and leaves
|
|
219
|
+
// Android, which reads one trait, with `button`.
|
|
134
220
|
template: `
|
|
135
|
-
<view
|
|
136
|
-
|
|
221
|
+
<view
|
|
222
|
+
#trigger
|
|
223
|
+
[class]="triggerClass()"
|
|
224
|
+
[style]="triggerStyle()"
|
|
225
|
+
(bindtap)="onTap()"
|
|
226
|
+
(bindtouchstart)="press($event)"
|
|
227
|
+
(bindtouchmove)="move($event)"
|
|
228
|
+
(bindtouchend)="release()"
|
|
229
|
+
(bindtouchcancel)="cancel()"
|
|
230
|
+
(bindmousedown)="press($event)"
|
|
231
|
+
(bindmousemove)="move($event)"
|
|
232
|
+
(bindmouseup)="release()"
|
|
233
|
+
(bindmouseleave)="leave()"
|
|
234
|
+
>
|
|
235
|
+
@if (stateLayerClass(); as stateLayer) {
|
|
236
|
+
<view [class]="stateLayer"></view>
|
|
237
|
+
}
|
|
238
|
+
<text
|
|
239
|
+
accessibility-traits="button"
|
|
240
|
+
[lynxAccessibilityTraits]="{ selected: isActive() }"
|
|
241
|
+
[class]="textClass()"
|
|
242
|
+
><ng-content
|
|
243
|
+
/></text>
|
|
137
244
|
</view>
|
|
138
245
|
`,
|
|
139
246
|
})
|
|
140
247
|
export class UiTabsTrigger {
|
|
141
248
|
readonly #tabs = inject(UiTabs);
|
|
249
|
+
readonly #settings = inject(LynxAccessibilitySettings);
|
|
250
|
+
// Both absent for a trigger outside a <ui-tabs-list>.
|
|
251
|
+
readonly #list = inject(UiTabsList, { optional: true });
|
|
252
|
+
readonly #segmented = inject(UiSegmentedControl, { optional: true });
|
|
142
253
|
|
|
143
254
|
readonly triggerValue = input.required<string>({ alias: 'value' });
|
|
144
255
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
145
256
|
|
|
146
|
-
readonly triggerRef = viewChild<ElementRef>('trigger');
|
|
257
|
+
readonly triggerRef = viewChild.required<ElementRef>('trigger');
|
|
147
258
|
#anim?: AnimationHandle;
|
|
148
259
|
#previousActive?: boolean;
|
|
149
260
|
|
|
@@ -151,60 +262,99 @@ export class UiTabsTrigger {
|
|
|
151
262
|
() => this.#tabs.value() === this.triggerValue(),
|
|
152
263
|
);
|
|
153
264
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
effect(() => {
|
|
162
|
-
const active = this.isActive();
|
|
163
|
-
const el = this.triggerRef()?.nativeElement;
|
|
164
|
-
if (!el || this.#previousActive === undefined) {
|
|
165
|
-
this.#previousActive = active;
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
// Defensive: `isActive` computed dedupes same-value emissions so the
|
|
169
|
-
// effect never re-runs with an unchanged `active`. Kept for safety.
|
|
170
|
-
/* v8 ignore next 1 */
|
|
171
|
-
if (active === this.#previousActive) return;
|
|
172
|
-
this.#previousActive = active;
|
|
265
|
+
/**
|
|
266
|
+
* The segmented control this trigger is a segment of, or null when its
|
|
267
|
+
* list is the default tabs (or it has no list).
|
|
268
|
+
*/
|
|
269
|
+
readonly #segment = computed(() =>
|
|
270
|
+
this.#segmented?.enabled() ? this.#segmented : null,
|
|
271
|
+
);
|
|
173
272
|
|
|
174
|
-
|
|
273
|
+
/**
|
|
274
|
+
* A segment's host is the track's flex item, an equal share of it. Angular
|
|
275
|
+
* puts the app's static `class` on the host as well as on the trigger, so
|
|
276
|
+
* the two are merged here: `class="flex-[2]"` makes a wider tab.
|
|
277
|
+
*/
|
|
278
|
+
protected readonly hostClass = computed(() => {
|
|
279
|
+
const segment = this.#segment();
|
|
280
|
+
return segment ? cn(segment.segmentHostClass, this.userClass()) : '';
|
|
281
|
+
});
|
|
175
282
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
283
|
+
protected readonly triggerStyle = computed(
|
|
284
|
+
() => this.#segment()?.segmentStyle(this.triggerValue()) ?? null,
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
protected readonly stateLayerClass = computed(
|
|
288
|
+
() => this.#segment()?.stateLayerClass(this.triggerValue()) ?? null,
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
/** Registers this trigger with the parent for direction detection. */
|
|
292
|
+
protected readonly registering = effect(() => {
|
|
293
|
+
this.#tabs.registerTab(this.triggerValue());
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
/** Animates the trigger's background when it's selected or deselected. */
|
|
297
|
+
protected readonly animatingSelection = effect(() => {
|
|
298
|
+
const active = this.isActive();
|
|
299
|
+
const el = this.triggerRef().nativeElement;
|
|
300
|
+
const wasActive = this.#previousActive;
|
|
301
|
+
this.#previousActive = active;
|
|
302
|
+
// The first run only records the starting state, and only a change has
|
|
303
|
+
// anything to animate. (`isActive` is a computed, so the effect only
|
|
304
|
+
// re-runs once it changes; the second check is a safeguard.) A segment
|
|
305
|
+
// doesn't animate itself: the segmented control's indicator moves. Under
|
|
306
|
+
// Reduce Motion the trigger changes at once.
|
|
307
|
+
if (
|
|
308
|
+
wasActive === undefined ||
|
|
309
|
+
wasActive === active ||
|
|
310
|
+
this.#segment() ||
|
|
311
|
+
this.#settings.reduceMotion()
|
|
312
|
+
) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
this.#anim?.cancel();
|
|
205
317
|
|
|
206
|
-
|
|
207
|
-
|
|
318
|
+
if (active) {
|
|
319
|
+
// Activation: spring-based scale + opacity for a lively entrance
|
|
320
|
+
this.#anim = el.animate(
|
|
321
|
+
[
|
|
322
|
+
{ transform: 'scaleX(0.95)', opacity: 0.7 },
|
|
323
|
+
{ transform: 'scaleX(1)', opacity: 1 },
|
|
324
|
+
],
|
|
325
|
+
{
|
|
326
|
+
duration: DURATION.normal,
|
|
327
|
+
easing: EASING.springSubtle,
|
|
328
|
+
fill: 'forwards',
|
|
329
|
+
},
|
|
330
|
+
);
|
|
331
|
+
} else {
|
|
332
|
+
// Deactivation: quick shrink + fade for a graceful exit
|
|
333
|
+
this.#anim = el.animate(
|
|
334
|
+
[
|
|
335
|
+
{ transform: 'scaleX(1)', opacity: 1 },
|
|
336
|
+
{ transform: 'scaleX(0.97)', opacity: 0.7 },
|
|
337
|
+
],
|
|
338
|
+
{
|
|
339
|
+
duration: DURATION.fast,
|
|
340
|
+
easing: EASING.accelerate,
|
|
341
|
+
fill: 'forwards',
|
|
342
|
+
},
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
protected readonly triggerClass = computed(() => {
|
|
348
|
+
const segment = this.#segment();
|
|
349
|
+
if (segment) {
|
|
350
|
+
// The list provides the segmented control, so a segment is in one.
|
|
351
|
+
const place = segmentPlace(this.#list!.triggers(), this);
|
|
352
|
+
return cn(
|
|
353
|
+
segment.segmentClass(this.triggerValue(), place),
|
|
354
|
+
this.userClass(),
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
return cn(
|
|
208
358
|
// No `flex-1` here: now that the list shrinks to fit its content (see
|
|
209
359
|
// `self-start` in UiTabsList), stretching triggers to equal width is
|
|
210
360
|
// pointless, and `flex-basis: 0` growth combined with Lynx treating
|
|
@@ -214,24 +364,62 @@ export class UiTabsTrigger {
|
|
|
214
364
|
'items-center rounded-sm px-3 py-1.5 flex justify-center',
|
|
215
365
|
this.isActive() ? 'bg-background' : 'bg-transparent',
|
|
216
366
|
this.userClass(),
|
|
217
|
-
)
|
|
218
|
-
);
|
|
367
|
+
);
|
|
368
|
+
});
|
|
219
369
|
|
|
220
|
-
protected readonly textClass = computed(
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
370
|
+
protected readonly textClass = computed(
|
|
371
|
+
() =>
|
|
372
|
+
this.#segment()?.labelClass(this.triggerValue()) ??
|
|
373
|
+
cn(
|
|
374
|
+
'text-sm font-medium',
|
|
375
|
+
this.isActive() ? 'text-foreground' : 'text-muted-foreground',
|
|
376
|
+
),
|
|
225
377
|
);
|
|
226
378
|
|
|
227
379
|
select(): void {
|
|
228
380
|
this.#tabs.select(this.triggerValue());
|
|
229
381
|
}
|
|
382
|
+
|
|
383
|
+
protected measure(event: unknown): void {
|
|
384
|
+
this.#segmented?.measure(this.triggerValue(), event);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* A press. A segment's touches go to the segmented control, which draws
|
|
389
|
+
* their feedback and moves its indicator.
|
|
390
|
+
*/
|
|
391
|
+
protected press(event: unknown): void {
|
|
392
|
+
this.#segment()?.press(this.triggerValue(), event);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
protected move(event: unknown): void {
|
|
396
|
+
this.#segment()?.move(event);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
protected release(): void {
|
|
400
|
+
this.#segment()?.release();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
protected cancel(): void {
|
|
404
|
+
this.#segment()?.cancel();
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
protected leave(): void {
|
|
408
|
+
this.#segment()?.leave(this.triggerValue());
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* A tap that ends a drag of the indicator doesn't select this tab: the
|
|
413
|
+
* drag chose already.
|
|
414
|
+
*/
|
|
415
|
+
protected onTap(): void {
|
|
416
|
+
if (this.#segment()?.tap(this.triggerValue()) === false) return;
|
|
417
|
+
this.select();
|
|
418
|
+
}
|
|
230
419
|
}
|
|
231
420
|
|
|
232
421
|
@Component({
|
|
233
422
|
selector: 'ui-tabs-content',
|
|
234
|
-
standalone: true,
|
|
235
423
|
imports: [LYNX_ELEMENTS],
|
|
236
424
|
encapsulation: ViewEncapsulation.None,
|
|
237
425
|
host: {
|
|
@@ -250,6 +438,10 @@ export class UiTabsTrigger {
|
|
|
250
438
|
// makes the host lay its content out vertically when shown.
|
|
251
439
|
'[style.display]': "isActive() ? 'flex' : 'none'",
|
|
252
440
|
'[style.flexDirection]': "'column'",
|
|
441
|
+
// A hidden panel's 0×0 doesn't stop iOS drawing its text: each line is
|
|
442
|
+
// laid out at 0×0 too, and its glyphs spill out from the panel's corner,
|
|
443
|
+
// over the tab list. Clipping the hidden host to its 0×0 hides them.
|
|
444
|
+
'[style.overflow]': "isActive() ? null : 'hidden'",
|
|
253
445
|
},
|
|
254
446
|
template: `
|
|
255
447
|
<view #content [class]="contentClass()">
|
|
@@ -259,11 +451,12 @@ export class UiTabsTrigger {
|
|
|
259
451
|
})
|
|
260
452
|
export class UiTabsContent {
|
|
261
453
|
readonly #tabs = inject(UiTabs);
|
|
454
|
+
readonly #settings = inject(LynxAccessibilitySettings);
|
|
262
455
|
|
|
263
456
|
readonly contentValue = input.required<string>({ alias: 'value' });
|
|
264
457
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
265
458
|
|
|
266
|
-
readonly contentRef = viewChild<ElementRef>('content');
|
|
459
|
+
readonly contentRef = viewChild.required<ElementRef>('content');
|
|
267
460
|
#anim?: AnimationHandle;
|
|
268
461
|
#isFirstRender = true;
|
|
269
462
|
|
|
@@ -271,43 +464,45 @@ export class UiTabsContent {
|
|
|
271
464
|
() => this.#tabs.value() === this.contentValue(),
|
|
272
465
|
);
|
|
273
466
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
467
|
+
/**
|
|
468
|
+
* Animates the content's reveal when its tab becomes active.
|
|
469
|
+
*
|
|
470
|
+
* The panel is ALWAYS rendered and shown/hidden via the host's `display`
|
|
471
|
+
* (see the `host` block) rather than wrapped in `@if`. On Lynx, destroying a
|
|
472
|
+
* subtree that holds projected `<ng-content>` sends that content through the
|
|
473
|
+
* renderer's "park on the page root" path (it must keep still-alive
|
|
474
|
+
* projected nodes from dying), which drops inactive panels at the bottom of
|
|
475
|
+
* the page and made re-showing them unreliable — the symptom was tab content
|
|
476
|
+
* that vanished and never came back. Keeping every panel mounted and only
|
|
477
|
+
* toggling `display` never destroys or re-parents anything: inactive panels
|
|
478
|
+
* collapse to 0×0 (Lynx `display: none`) so they take no space, and the
|
|
479
|
+
* active one simply reappears in place. That also means the content view
|
|
480
|
+
* always exists, so `contentRef` is required.
|
|
481
|
+
*/
|
|
482
|
+
protected readonly revealing = effect(() => {
|
|
483
|
+
if (!this.isActive()) return;
|
|
484
|
+
// Defer one tick so the display flip to 'flex' has applied before we animate.
|
|
485
|
+
setTimeout(() => {
|
|
486
|
+
// Under Reduce Motion the panel just appears.
|
|
487
|
+
if (this.#settings.reduceMotion()) return;
|
|
488
|
+
const el = this.contentRef().nativeElement;
|
|
489
|
+
|
|
490
|
+
this.#anim?.cancel();
|
|
491
|
+
|
|
492
|
+
if (this.#isFirstRender) {
|
|
493
|
+
// First render: subtle vertical reveal (no directional context yet)
|
|
494
|
+
this.#isFirstRender = false;
|
|
495
|
+
this.#anim = revealIn(el, { fromY: 6, duration: DURATION.normal });
|
|
496
|
+
} else {
|
|
497
|
+
// Subsequent renders: direction-aware horizontal slide
|
|
498
|
+
const dir = this.#tabs.direction();
|
|
499
|
+
this.#anim = directionalSlideIn(el, dir, {
|
|
500
|
+
distance: 24,
|
|
501
|
+
duration: 300,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}, 0);
|
|
505
|
+
});
|
|
311
506
|
|
|
312
507
|
// Visibility is controlled at the host (see `host` block); the inner view is
|
|
313
508
|
// always a normal flex column that fills the shown host.
|