@blotch/dolan 22.2.2 → 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.d.ts +6 -2
- package/dist/commands/add.js +41 -11
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/add.test.js +162 -9
- package/dist/commands/add.test.js.map +1 -1
- package/dist/commands/diff.d.ts +2 -1
- package/dist/commands/diff.js +38 -25
- package/dist/commands/diff.js.map +1 -1
- package/dist/commands/diff.test.js +72 -11
- package/dist/commands/diff.test.js.map +1 -1
- package/dist/commands/doctor.js +228 -34
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.js +367 -10
- package/dist/commands/doctor.test.js.map +1 -1
- package/dist/commands/eject.js +7 -0
- package/dist/commands/eject.js.map +1 -1
- package/dist/commands/eject.test.js +39 -0
- package/dist/commands/eject.test.js.map +1 -1
- package/dist/commands/info.js +3 -2
- package/dist/commands/info.js.map +1 -1
- package/dist/commands/init.d.ts +37 -1
- package/dist/commands/init.js +169 -40
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/init.test.js +392 -27
- package/dist/commands/init.test.js.map +1 -1
- package/dist/commands/install-packages.d.ts +6 -0
- package/dist/commands/install-packages.js +20 -0
- package/dist/commands/install-packages.js.map +1 -0
- package/dist/commands/install-packages.test.d.ts +1 -0
- package/dist/commands/install-packages.test.js +55 -0
- package/dist/commands/install-packages.test.js.map +1 -0
- package/dist/commands/list.js +21 -14
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/list.test.js +62 -5
- package/dist/commands/list.test.js.map +1 -1
- package/dist/commands/outdated.js +11 -10
- package/dist/commands/outdated.js.map +1 -1
- package/dist/commands/outdated.test.js +20 -5
- package/dist/commands/outdated.test.js.map +1 -1
- package/dist/commands/remove.d.ts +4 -0
- package/dist/commands/remove.js +17 -4
- package/dist/commands/remove.js.map +1 -1
- package/dist/commands/remove.test.js +53 -2
- package/dist/commands/remove.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 +11 -0
- package/dist/commands/update.js +174 -58
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/update.test.js +587 -33
- package/dist/commands/update.test.js.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.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 +14 -0
- package/dist/registry.js +78 -24
- package/dist/registry.js.map +1 -1
- package/dist/registry.test.js +119 -38
- 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 +14 -0
- package/dist/testing/set-input-signal.js +21 -0
- package/dist/testing/set-input-signal.js.map +1 -0
- package/dist/testing/set-input-signal.test.d.ts +1 -0
- package/dist/testing/set-input-signal.test.js +20 -0
- package/dist/testing/set-input-signal.test.js.map +1 -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 +283 -151
- package/dist/ui/components/action-sheet/action-sheet.ts +77 -43
- 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 +208 -94
- package/dist/ui/components/alert-dialog/alert-dialog.ts +75 -38
- package/dist/ui/components/area-chart/area-chart.test.ts +134 -119
- package/dist/ui/components/area-chart/area-chart.ts +100 -315
- package/dist/ui/components/area-chart/index.ts +2 -3
- 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 +172 -45
- package/dist/ui/components/bar-chart/bar-chart.ts +140 -294
- package/dist/ui/components/bar-chart/index.ts +3 -3
- package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +386 -128
- package/dist/ui/components/bottom-sheet/bottom-sheet.ts +170 -53
- package/dist/ui/components/button/button.test.ts +611 -2
- package/dist/ui/components/button/button.ts +434 -49
- 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 +183 -58
- package/dist/ui/components/candlestick-chart/candlestick-chart.ts +150 -323
- package/dist/ui/components/candlestick-chart/index.ts +3 -4
- package/dist/ui/components/card/card.test.ts +82 -62
- package/dist/ui/components/chart/chart-focus.test.ts +132 -0
- package/dist/ui/components/chart/chart-focus.ts +115 -0
- package/dist/ui/components/chart/chart-presets.test.ts +335 -0
- package/dist/ui/components/chart/chart-presets.ts +278 -0
- package/dist/ui/components/chart/chart-scene.test.ts +589 -0
- package/dist/ui/components/chart/chart-scene.ts +483 -0
- package/dist/ui/components/chart/chart-theme.test.ts +85 -0
- package/dist/ui/components/chart/chart-theme.ts +210 -0
- package/dist/ui/components/chart/chart-zoom.test.ts +159 -0
- package/dist/ui/components/chart/chart-zoom.ts +162 -0
- package/dist/ui/components/chart/chart.test.ts +1017 -0
- package/dist/ui/components/chart/chart.ts +934 -0
- package/dist/ui/components/chart/index.ts +24 -0
- 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 +213 -108
- package/dist/ui/components/dialog/dialog.ts +72 -35
- 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 +169 -0
- package/dist/ui/components/icon/icon-theme.ts +227 -0
- package/dist/ui/components/icon/icon.test.ts +85 -25
- package/dist/ui/components/icon/icon.ts +38 -21
- 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/index.ts +2 -3
- package/dist/ui/components/line-chart/line-chart.test.ts +149 -56
- package/dist/ui/components/line-chart/line-chart.ts +83 -241
- package/dist/ui/components/list/list.test.ts +119 -21
- package/dist/ui/components/list/list.ts +21 -15
- 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 +435 -191
- package/dist/ui/components/nav-drawer/nav-drawer.ts +127 -56
- package/dist/ui/components/pagination/pagination.test.ts +177 -65
- package/dist/ui/components/pagination/pagination.ts +55 -8
- 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/index.ts +2 -3
- package/dist/ui/components/scatter-chart/scatter-chart.test.ts +131 -26
- package/dist/ui/components/scatter-chart/scatter-chart.ts +104 -250
- package/dist/ui/components/scroll-area/scroll-area.test.ts +95 -24
- package/dist/ui/components/scroll-area/scroll-area.ts +8 -8
- 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 +67 -22
- package/dist/ui/components/spinner/spinner.ts +48 -41
- 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 +436 -315
- package/dist/ui/components/toast/toast.ts +62 -55
- package/dist/ui/components/toggle/toggle.test.ts +247 -140
- package/dist/ui/theme/chart-colors.test.ts +35 -0
- package/dist/ui/theme/dark.css +46 -2
- package/dist/ui/theme/default.css +63 -7
- package/dist/ui/theme/icon-colors.test.ts +35 -0
- 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 +98 -25
- 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 +29 -0
- package/dist/utils/installed.js +43 -0
- package/dist/utils/installed.js.map +1 -0
- package/dist/utils/installed.test.d.ts +1 -0
- package/dist/utils/installed.test.js +57 -0
- package/dist/utils/installed.test.js.map +1 -0
- package/dist/utils/interactive.d.ts +9 -0
- package/dist/utils/interactive.js +10 -0
- package/dist/utils/interactive.js.map +1 -0
- package/dist/utils/interactive.test.d.ts +1 -0
- package/dist/utils/interactive.test.js +24 -0
- package/dist/utils/interactive.test.js.map +1 -0
- package/dist/utils/packages.d.ts +36 -0
- package/dist/utils/packages.js +70 -0
- package/dist/utils/packages.js.map +1 -0
- package/dist/utils/packages.test.d.ts +1 -0
- package/dist/utils/packages.test.js +89 -0
- package/dist/utils/packages.test.js.map +1 -0
- package/dist/utils/resolve-paths.d.ts +17 -1
- package/dist/utils/resolve-paths.js +21 -3
- package/dist/utils/resolve-paths.js.map +1 -1
- package/dist/utils/resolve-paths.test.js +38 -2
- 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 +13 -1
- package/dist/ui/components/area-chart/area-chart-render.test.ts +0 -121
- package/dist/ui/components/bar-chart/bar-chart-render.test.ts +0 -174
- package/dist/ui/components/button/button-render.test.ts +0 -113
- 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 -174
- package/dist/ui/components/cartesian-chart/cartesian-chart-render.test.ts +0 -188
- package/dist/ui/components/cartesian-chart/cartesian-chart.test.ts +0 -525
- package/dist/ui/components/cartesian-chart/cartesian-chart.ts +0 -1140
- package/dist/ui/components/cartesian-chart/index.ts +0 -18
- package/dist/ui/components/line-chart/line-chart-render.test.ts +0 -121
- package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +0 -157
|
@@ -44,8 +44,10 @@ export class UiTabs {
|
|
|
44
44
|
// When the active tab changes, we compare the old and new index in this
|
|
45
45
|
// list to determine slide direction (1 = right, -1 = left). This creates
|
|
46
46
|
// 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
|
-
|
|
47
|
+
// with content sliding in from the direction of the selected tab. A Set
|
|
48
|
+
// keeps the order tabs first registered in and ignores a re-registration
|
|
49
|
+
// (a trigger re-created by `@if` or `@for`).
|
|
50
|
+
readonly #tabOrder = new Set<string>();
|
|
49
51
|
#previousValue: string | undefined;
|
|
50
52
|
readonly direction = signal<1 | -1>(1);
|
|
51
53
|
|
|
@@ -53,43 +55,38 @@ export class UiTabs {
|
|
|
53
55
|
cn('flex-col flex', this.userClass()),
|
|
54
56
|
);
|
|
55
57
|
|
|
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
|
-
}
|
|
58
|
+
/**
|
|
59
|
+
* Computes the slide direction when the active value changes, whether from
|
|
60
|
+
* select() or from the model being set. A field rather than a constructor:
|
|
61
|
+
* Angular's test build keeps a function for a constructor that nothing
|
|
62
|
+
* calls, which an app's coverage report would show as uncovered.
|
|
63
|
+
*/
|
|
64
|
+
protected readonly trackingDirection = effect(() => {
|
|
65
|
+
const currentValue = this.value();
|
|
66
|
+
if (this.#previousValue === undefined) {
|
|
82
67
|
this.#previousValue = currentValue;
|
|
83
|
-
|
|
84
|
-
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// The value can change and change back before the effect runs (two taps
|
|
71
|
+
// in one frame): the tab didn't move, so the direction stays.
|
|
72
|
+
if (currentValue === this.#previousValue) return;
|
|
73
|
+
|
|
74
|
+
const order = [...this.#tabOrder];
|
|
75
|
+
const prevIndex = order.indexOf(this.#previousValue);
|
|
76
|
+
const nextIndex = order.indexOf(currentValue);
|
|
77
|
+
// A value with no trigger has no place in the order to slide from or to.
|
|
78
|
+
// Two different registered values always have different indexes.
|
|
79
|
+
if (prevIndex !== -1 && nextIndex !== -1) {
|
|
80
|
+
this.direction.set(nextIndex > prevIndex ? 1 : -1);
|
|
81
|
+
}
|
|
82
|
+
this.#previousValue = currentValue;
|
|
83
|
+
});
|
|
85
84
|
|
|
86
85
|
/**
|
|
87
86
|
* Called by UiTabsTrigger to register ordering for direction detection.
|
|
88
87
|
*/
|
|
89
88
|
registerTab(value: string): void {
|
|
90
|
-
|
|
91
|
-
this.#tabOrder.push(value);
|
|
92
|
-
}
|
|
89
|
+
this.#tabOrder.add(value);
|
|
93
90
|
}
|
|
94
91
|
|
|
95
92
|
select(value: string): void {
|
|
@@ -143,7 +140,7 @@ export class UiTabsTrigger {
|
|
|
143
140
|
readonly triggerValue = input.required<string>({ alias: 'value' });
|
|
144
141
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
145
142
|
|
|
146
|
-
readonly triggerRef = viewChild<ElementRef>('trigger');
|
|
143
|
+
readonly triggerRef = viewChild.required<ElementRef>('trigger');
|
|
147
144
|
#anim?: AnimationHandle;
|
|
148
145
|
#previousActive?: boolean;
|
|
149
146
|
|
|
@@ -151,57 +148,52 @@ export class UiTabsTrigger {
|
|
|
151
148
|
() => this.#tabs.value() === this.triggerValue(),
|
|
152
149
|
);
|
|
153
150
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
});
|
|
151
|
+
/** Registers this trigger with the parent for direction detection. */
|
|
152
|
+
protected readonly registering = effect(() => {
|
|
153
|
+
this.#tabs.registerTab(this.triggerValue());
|
|
154
|
+
});
|
|
159
155
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
/* v8 ignore next 1 */
|
|
171
|
-
if (active === this.#previousActive) return;
|
|
172
|
-
this.#previousActive = active;
|
|
156
|
+
/** Animates the trigger's background when it's selected or deselected. */
|
|
157
|
+
protected readonly animatingSelection = effect(() => {
|
|
158
|
+
const active = this.isActive();
|
|
159
|
+
const el = this.triggerRef().nativeElement;
|
|
160
|
+
const wasActive = this.#previousActive;
|
|
161
|
+
this.#previousActive = active;
|
|
162
|
+
// The first run only records the starting state, and only a change has
|
|
163
|
+
// anything to animate. (`isActive` is a computed, so the effect only
|
|
164
|
+
// re-runs once it changes; the second check is a safeguard.)
|
|
165
|
+
if (wasActive === undefined || wasActive === active) return;
|
|
173
166
|
|
|
174
|
-
|
|
167
|
+
this.#anim?.cancel();
|
|
175
168
|
|
|
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
|
-
}
|
|
169
|
+
if (active) {
|
|
170
|
+
// Activation: spring-based scale + opacity for a lively entrance
|
|
171
|
+
this.#anim = el.animate(
|
|
172
|
+
[
|
|
173
|
+
{ transform: 'scaleX(0.95)', opacity: 0.7 },
|
|
174
|
+
{ transform: 'scaleX(1)', opacity: 1 },
|
|
175
|
+
],
|
|
176
|
+
{
|
|
177
|
+
duration: DURATION.normal,
|
|
178
|
+
easing: EASING.springSubtle,
|
|
179
|
+
fill: 'forwards',
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
} else {
|
|
183
|
+
// Deactivation: quick shrink + fade for a graceful exit
|
|
184
|
+
this.#anim = el.animate(
|
|
185
|
+
[
|
|
186
|
+
{ transform: 'scaleX(1)', opacity: 1 },
|
|
187
|
+
{ transform: 'scaleX(0.97)', opacity: 0.7 },
|
|
188
|
+
],
|
|
189
|
+
{
|
|
190
|
+
duration: DURATION.fast,
|
|
191
|
+
easing: EASING.accelerate,
|
|
192
|
+
fill: 'forwards',
|
|
193
|
+
},
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
205
197
|
|
|
206
198
|
protected readonly triggerClass = computed(() =>
|
|
207
199
|
cn(
|
|
@@ -263,7 +255,7 @@ export class UiTabsContent {
|
|
|
263
255
|
readonly contentValue = input.required<string>({ alias: 'value' });
|
|
264
256
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
265
257
|
|
|
266
|
-
readonly contentRef = viewChild<ElementRef>('content');
|
|
258
|
+
readonly contentRef = viewChild.required<ElementRef>('content');
|
|
267
259
|
#anim?: AnimationHandle;
|
|
268
260
|
#isFirstRender = true;
|
|
269
261
|
|
|
@@ -271,43 +263,43 @@ export class UiTabsContent {
|
|
|
271
263
|
() => this.#tabs.value() === this.contentValue(),
|
|
272
264
|
);
|
|
273
265
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
266
|
+
/**
|
|
267
|
+
* Animates the content's reveal when its tab becomes active.
|
|
268
|
+
*
|
|
269
|
+
* The panel is ALWAYS rendered and shown/hidden via the host's `display`
|
|
270
|
+
* (see the `host` block) rather than wrapped in `@if`. On Lynx, destroying a
|
|
271
|
+
* subtree that holds projected `<ng-content>` sends that content through the
|
|
272
|
+
* renderer's "park on the page root" path (it must keep still-alive
|
|
273
|
+
* projected nodes from dying), which drops inactive panels at the bottom of
|
|
274
|
+
* the page and made re-showing them unreliable — the symptom was tab content
|
|
275
|
+
* that vanished and never came back. Keeping every panel mounted and only
|
|
276
|
+
* toggling `display` never destroys or re-parents anything: inactive panels
|
|
277
|
+
* collapse to 0×0 (Lynx `display: none`) so they take no space, and the
|
|
278
|
+
* active one simply reappears in place. That also means the content view
|
|
279
|
+
* always exists, so `contentRef` is required.
|
|
280
|
+
*/
|
|
281
|
+
protected readonly revealing = effect(() => {
|
|
282
|
+
if (!this.isActive()) return;
|
|
283
|
+
// Defer one tick so the display flip to 'flex' has applied before we animate.
|
|
284
|
+
setTimeout(() => {
|
|
285
|
+
const el = this.contentRef().nativeElement;
|
|
293
286
|
|
|
294
|
-
|
|
287
|
+
this.#anim?.cancel();
|
|
295
288
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
289
|
+
if (this.#isFirstRender) {
|
|
290
|
+
// First render: subtle vertical reveal (no directional context yet)
|
|
291
|
+
this.#isFirstRender = false;
|
|
292
|
+
this.#anim = revealIn(el, { fromY: 6, duration: DURATION.normal });
|
|
293
|
+
} else {
|
|
294
|
+
// Subsequent renders: direction-aware horizontal slide
|
|
295
|
+
const dir = this.#tabs.direction();
|
|
296
|
+
this.#anim = directionalSlideIn(el, dir, {
|
|
297
|
+
distance: 24,
|
|
298
|
+
duration: 300,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}, 0);
|
|
302
|
+
});
|
|
311
303
|
|
|
312
304
|
// Visibility is controlled at the host (see `host` block); the inner view is
|
|
313
305
|
// always a normal flex column that fills the shown host.
|
|
@@ -1,115 +1,236 @@
|
|
|
1
|
-
import { signal } from '@angular/core';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import {
|
|
3
|
+
fireEvent,
|
|
4
|
+
render,
|
|
5
|
+
waitForUpdate,
|
|
6
|
+
} from '@blotch/angular-lynx-testing-library';
|
|
7
|
+
import {
|
|
8
|
+
type MockInstance,
|
|
9
|
+
afterEach,
|
|
10
|
+
beforeEach,
|
|
11
|
+
describe,
|
|
12
|
+
expect,
|
|
13
|
+
it,
|
|
14
|
+
vi,
|
|
15
|
+
} from 'vitest';
|
|
16
|
+
import {
|
|
17
|
+
type LynxMainThread,
|
|
18
|
+
lynxMainThread,
|
|
19
|
+
} from '../../../test-utils/lynx-main-thread';
|
|
5
20
|
import { UiTextarea } from './textarea';
|
|
6
21
|
|
|
22
|
+
@Component({
|
|
23
|
+
imports: [UiTextarea],
|
|
24
|
+
template: `
|
|
25
|
+
<ui-textarea
|
|
26
|
+
[(value)]="notes"
|
|
27
|
+
[label]="label()"
|
|
28
|
+
placeholder="Add a note"
|
|
29
|
+
[helperText]="helperText()"
|
|
30
|
+
[error]="error()"
|
|
31
|
+
[disabled]="disabled()"
|
|
32
|
+
[minLines]="minLines()"
|
|
33
|
+
[maxLines]="maxLines()"
|
|
34
|
+
class="w-full"
|
|
35
|
+
(changed)="changes.push($event)"
|
|
36
|
+
(focused)="events.push('focused')"
|
|
37
|
+
(blurred)="events.push('blurred')"
|
|
38
|
+
/>
|
|
39
|
+
`,
|
|
40
|
+
})
|
|
41
|
+
class TextareaHost {
|
|
42
|
+
readonly notes = signal('');
|
|
43
|
+
readonly label = signal('');
|
|
44
|
+
readonly helperText = signal('');
|
|
45
|
+
readonly error = signal('');
|
|
46
|
+
readonly disabled = signal(false);
|
|
47
|
+
readonly minLines = signal(3);
|
|
48
|
+
readonly maxLines = signal<number | undefined>(undefined);
|
|
49
|
+
readonly changes: string[] = [];
|
|
50
|
+
readonly events: string[] = [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const renderTextarea = async () => {
|
|
54
|
+
const result = await render(TextareaHost);
|
|
55
|
+
return {
|
|
56
|
+
...result,
|
|
57
|
+
host: result.componentRef.instance as TextareaHost,
|
|
58
|
+
field: () => result.container.querySelector('textarea')!,
|
|
59
|
+
// The field's own column; the host element renders as a view too.
|
|
60
|
+
column: () => result.container.querySelector('.flex-col')!,
|
|
61
|
+
wrapper: () => result.container.querySelector('.bg-muted') as HTMLElement,
|
|
62
|
+
// The layer that draws the focus ring around the field.
|
|
63
|
+
ring: () => result.container.querySelector('.absolute') as HTMLElement,
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** `__ElementAnimate`'s first argument for starting an animation. */
|
|
68
|
+
const START = 0;
|
|
69
|
+
/** `__ElementAnimate`'s first argument for canceling an animation. */
|
|
70
|
+
const CANCEL = 3;
|
|
71
|
+
|
|
7
72
|
describe('UiTextarea', () => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
73
|
+
// Watched per test: the testing library resets the main thread after each.
|
|
74
|
+
let animate: MockInstance<LynxMainThread['__ElementAnimate']>;
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
animate = vi.spyOn(lynxMainThread(), '__ElementAnimate');
|
|
11
77
|
});
|
|
78
|
+
afterEach(() => animate.mockRestore());
|
|
12
79
|
|
|
13
|
-
it('
|
|
14
|
-
const {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const emitted: string[] = [];
|
|
19
|
-
inst.changed.subscribe((v) => emitted.push(v));
|
|
20
|
-
inst.onInput({ detail: { value: 'hello' } });
|
|
21
|
-
expect(inst.value()).toBe('hello');
|
|
22
|
-
expect(emitted).toEqual(['hello']);
|
|
80
|
+
it('renders a native textarea with its placeholder, and no label', async () => {
|
|
81
|
+
const { field, column, queryByText } = await renderTextarea();
|
|
82
|
+
expect(field().getAttribute('placeholder')).toBe('Add a note');
|
|
83
|
+
expect(column().classList).toContain('w-full');
|
|
84
|
+
expect(queryByText('Add a note')).toBeNull();
|
|
23
85
|
});
|
|
24
86
|
|
|
25
|
-
it('
|
|
26
|
-
const {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
setInputSignal(inst.disabled, true);
|
|
31
|
-
const emitted = vi.fn();
|
|
32
|
-
inst.changed.subscribe(emitted);
|
|
33
|
-
inst.onInput({ detail: { value: 'blocked' } });
|
|
34
|
-
expect(inst.value()).toBe('');
|
|
35
|
-
expect(emitted).not.toHaveBeenCalled();
|
|
87
|
+
it('shows its label', async () => {
|
|
88
|
+
const { host, getByText } = await renderTextarea();
|
|
89
|
+
host.label.set('Notes');
|
|
90
|
+
await waitForUpdate();
|
|
91
|
+
expect(getByText('Notes').classList).toContain('font-medium');
|
|
36
92
|
});
|
|
37
93
|
|
|
38
|
-
it('
|
|
39
|
-
const {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const blurred = vi.fn();
|
|
46
|
-
inst.focused.subscribe(focused);
|
|
47
|
-
inst.blurred.subscribe(blurred);
|
|
48
|
-
inst.onFocus();
|
|
49
|
-
inst.onBlur();
|
|
50
|
-
expect(focused).toHaveBeenCalledTimes(1);
|
|
51
|
-
expect(blurred).toHaveBeenCalledTimes(1);
|
|
94
|
+
it('updates `value` and emits `changed` as the user types', async () => {
|
|
95
|
+
const { host, field } = await renderTextarea();
|
|
96
|
+
// Lynx puts the typed text in the event's detail, not on its target.
|
|
97
|
+
fireEvent.input(field(), { detail: { value: 'Call' } });
|
|
98
|
+
fireEvent.input(field(), { detail: { value: 'Call Sam' } });
|
|
99
|
+
expect(host.notes()).toBe('Call Sam');
|
|
100
|
+
expect(host.changes).toEqual(['Call', 'Call Sam']);
|
|
52
101
|
});
|
|
53
102
|
|
|
54
|
-
it('
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const inst = componentRef.instance as UiTextarea;
|
|
60
|
-
const view = container.querySelector('view') as Element;
|
|
61
|
-
(inst as unknown as { containerRef: unknown }).containerRef = signal({
|
|
62
|
-
nativeElement: view,
|
|
63
|
-
});
|
|
64
|
-
setInputSignal(inst.error, 'Too short');
|
|
103
|
+
it('hides its placeholder while it has a value', async () => {
|
|
104
|
+
// Lynx's iOS textarea would otherwise draw the placeholder over a value
|
|
105
|
+
// the app sets.
|
|
106
|
+
const { host, field } = await renderTextarea();
|
|
107
|
+
host.notes.set('Call Sam');
|
|
65
108
|
await waitForUpdate();
|
|
109
|
+
expect(field().hasAttribute('placeholder')).toBe(false);
|
|
110
|
+
|
|
111
|
+
host.notes.set('');
|
|
66
112
|
await waitForUpdate();
|
|
67
|
-
expect(
|
|
68
|
-
|
|
69
|
-
|
|
113
|
+
expect(field().getAttribute('placeholder')).toBe('Add a note');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("dims and ignores typing while it's disabled", async () => {
|
|
117
|
+
const { host, field, wrapper } = await renderTextarea();
|
|
118
|
+
host.disabled.set(true);
|
|
70
119
|
await waitForUpdate();
|
|
120
|
+
expect(wrapper().classList).toContain('opacity-50');
|
|
121
|
+
|
|
122
|
+
fireEvent.input(field(), { detail: { value: 'blocked' } });
|
|
123
|
+
expect(host.notes()).toBe('');
|
|
124
|
+
expect(host.changes).toEqual([]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('disables the native textarea only while disabled', async () => {
|
|
128
|
+
// Lynx treats any `disabled` value, even false, as disabled, so the
|
|
129
|
+
// attribute has to go rather than be set to false.
|
|
130
|
+
const setAttribute = vi.spyOn(lynxMainThread(), '__SetAttribute');
|
|
131
|
+
const { host, field } = await renderTextarea();
|
|
132
|
+
expect(setAttribute).not.toHaveBeenCalledWith(
|
|
133
|
+
field(),
|
|
134
|
+
'disabled',
|
|
135
|
+
expect.anything(),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
host.disabled.set(true);
|
|
139
|
+
await waitForUpdate();
|
|
140
|
+
expect(setAttribute).toHaveBeenLastCalledWith(field(), 'disabled', 'true');
|
|
141
|
+
setAttribute.mockRestore();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('shows a focus ring while focused, and emits `focused` and `blurred`', async () => {
|
|
145
|
+
const { host, field, ring } = await renderTextarea();
|
|
146
|
+
expect(ring().style.getPropertyValue('opacity')).toBe('0');
|
|
147
|
+
|
|
148
|
+
fireEvent.focus(field());
|
|
71
149
|
await waitForUpdate();
|
|
72
|
-
|
|
150
|
+
expect(ring().style.getPropertyValue('opacity')).toBe('1');
|
|
151
|
+
expect(ring().style.getPropertyValue('box-shadow')).toContain(
|
|
152
|
+
'var(--ring)',
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
fireEvent.blur(field());
|
|
73
156
|
await waitForUpdate();
|
|
157
|
+
expect(ring().style.getPropertyValue('opacity')).toBe('0');
|
|
158
|
+
expect(host.events).toEqual(['focused', 'blurred']);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('shows its helper text', async () => {
|
|
162
|
+
const { host, getByText } = await renderTextarea();
|
|
163
|
+
host.helperText.set('Only you can see this.');
|
|
74
164
|
await waitForUpdate();
|
|
75
|
-
expect(
|
|
165
|
+
expect(getByText('Only you can see this.').classList).toContain(
|
|
166
|
+
'text-muted-foreground',
|
|
167
|
+
);
|
|
76
168
|
});
|
|
77
169
|
|
|
78
|
-
it('
|
|
79
|
-
const {
|
|
80
|
-
|
|
81
|
-
|
|
170
|
+
it('shows an error in place of the helper text, with a destructive ring', async () => {
|
|
171
|
+
const { host, ring, getByText, queryByText } = await renderTextarea();
|
|
172
|
+
host.helperText.set('Only you can see this.');
|
|
173
|
+
host.error.set('Too short');
|
|
82
174
|
await waitForUpdate();
|
|
83
|
-
expect(
|
|
175
|
+
expect(getByText('Too short').classList).toContain('text-destructive');
|
|
176
|
+
expect(queryByText('Only you can see this.')).toBeNull();
|
|
177
|
+
// An invalid field keeps its ring even when it isn't focused.
|
|
178
|
+
expect(ring().style.getPropertyValue('opacity')).toBe('1');
|
|
179
|
+
expect(ring().style.getPropertyValue('box-shadow')).toContain(
|
|
180
|
+
'var(--destructive)',
|
|
181
|
+
);
|
|
84
182
|
});
|
|
85
183
|
|
|
86
|
-
it('
|
|
87
|
-
const {
|
|
88
|
-
const
|
|
89
|
-
|
|
184
|
+
it('shakes when an error appears, but not when it only changes', async () => {
|
|
185
|
+
const { host, column } = await renderTextarea();
|
|
186
|
+
const shakes = () =>
|
|
187
|
+
animate.mock.calls.filter(([, [action]]) => action === START);
|
|
188
|
+
expect(shakes()).toEqual([]);
|
|
189
|
+
|
|
190
|
+
host.error.set('Required');
|
|
191
|
+
await waitForUpdate();
|
|
192
|
+
expect(animate).toHaveBeenCalledWith(column(), [
|
|
193
|
+
START,
|
|
194
|
+
expect.any(String),
|
|
195
|
+
expect.arrayContaining([{ transform: 'translateX(-4px)' }]),
|
|
196
|
+
expect.anything(),
|
|
197
|
+
]);
|
|
198
|
+
expect(shakes()).toHaveLength(1);
|
|
199
|
+
|
|
200
|
+
host.error.set('Too short');
|
|
90
201
|
await waitForUpdate();
|
|
91
|
-
expect(
|
|
202
|
+
expect(shakes()).toHaveLength(1);
|
|
203
|
+
|
|
204
|
+
// Once fixed, the next error shakes again, replacing the last shake.
|
|
205
|
+
host.error.set('');
|
|
206
|
+
await waitForUpdate();
|
|
207
|
+
host.error.set('Required');
|
|
208
|
+
await waitForUpdate();
|
|
209
|
+
expect(shakes()).toHaveLength(2);
|
|
210
|
+
expect(animate).toHaveBeenCalledWith(column(), [
|
|
211
|
+
CANCEL,
|
|
212
|
+
expect.any(String),
|
|
213
|
+
]);
|
|
92
214
|
});
|
|
93
215
|
|
|
94
|
-
it('
|
|
95
|
-
//
|
|
96
|
-
const {
|
|
97
|
-
|
|
98
|
-
|
|
216
|
+
it('opens at minLines tall and grows without limit by default', async () => {
|
|
217
|
+
// Each line is 20px, plus 20px of vertical padding.
|
|
218
|
+
const { host, field, wrapper } = await renderTextarea();
|
|
219
|
+
expect(wrapper().style.getPropertyValue('min-height')).toBe('80px');
|
|
220
|
+
expect(wrapper().style.getPropertyValue('max-height')).toBe('');
|
|
221
|
+
expect(field().hasAttribute('enable-scroll-bar')).toBe(false);
|
|
222
|
+
|
|
223
|
+
host.minLines.set(5);
|
|
99
224
|
await waitForUpdate();
|
|
100
|
-
|
|
101
|
-
const wrapper = container.querySelector('[style*="min-height"]');
|
|
102
|
-
expect(wrapper?.getAttribute('style')).toContain('min-height: 120px');
|
|
103
|
-
expect(wrapper?.getAttribute('style')).not.toContain('max-height');
|
|
225
|
+
expect(wrapper().style.getPropertyValue('min-height')).toBe('120px');
|
|
104
226
|
});
|
|
105
227
|
|
|
106
|
-
it('
|
|
107
|
-
const {
|
|
108
|
-
|
|
109
|
-
setInputSignal(inst.maxLines, 4);
|
|
228
|
+
it('stops growing at maxLines and shows a scroll bar', async () => {
|
|
229
|
+
const { host, field, wrapper } = await renderTextarea();
|
|
230
|
+
host.maxLines.set(4);
|
|
110
231
|
await waitForUpdate();
|
|
111
|
-
|
|
112
|
-
expect(
|
|
232
|
+
expect(wrapper().style.getPropertyValue('max-height')).toBe('100px');
|
|
233
|
+
expect(field().getAttribute('enable-scroll-bar')).toBe('true');
|
|
113
234
|
});
|
|
114
235
|
|
|
115
236
|
it('makes the wrapper a stacking context so the field scrolls with it', async () => {
|
|
@@ -117,22 +238,7 @@ describe('UiTextarea', () => {
|
|
|
117
238
|
// when an ancestor inside the scroll content is a stacking context. Without
|
|
118
239
|
// z-0 on the wrapper, the native field (and its placeholder) stays fixed on
|
|
119
240
|
// screen while the page scrolls.
|
|
120
|
-
const {
|
|
121
|
-
|
|
122
|
-
'[class*="rounded-xl"][class*="bg-muted"]',
|
|
123
|
-
);
|
|
124
|
-
expect(wrapper?.getAttribute('class')?.split(/\s+/)).toContain('z-0');
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('applies the disabled dim class when disabled', async () => {
|
|
128
|
-
// Covers the true branch of `disabled() && 'opacity-50'` in textareaWrapperClass.
|
|
129
|
-
const { componentRef, container } = await render(UiTextarea);
|
|
130
|
-
const inst = componentRef.instance as UiTextarea;
|
|
131
|
-
setInputSignal(inst.disabled, true);
|
|
132
|
-
await waitForUpdate();
|
|
133
|
-
const wrapper = container.querySelector(
|
|
134
|
-
'[class*="rounded-xl"][class*="bg-muted"]',
|
|
135
|
-
);
|
|
136
|
-
expect(wrapper?.getAttribute('class')).toContain('opacity-50');
|
|
241
|
+
const { wrapper } = await renderTextarea();
|
|
242
|
+
expect(wrapper().classList).toContain('z-0');
|
|
137
243
|
});
|
|
138
244
|
});
|