@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
|
@@ -1,13 +1,30 @@
|
|
|
1
|
+
import { Component } from '@angular/core';
|
|
1
2
|
import { render } from '@blotch/angular-lynx-testing-library';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { lynxMainThread } from '../../../test-utils/lynx-main-thread';
|
|
3
5
|
import { UiSkeleton } from './skeleton';
|
|
4
6
|
|
|
7
|
+
@Component({
|
|
8
|
+
imports: [UiSkeleton],
|
|
9
|
+
template: `<ui-skeleton class="h-4 w-32" />`,
|
|
10
|
+
})
|
|
11
|
+
class SkeletonHost {}
|
|
12
|
+
|
|
5
13
|
describe('UiSkeleton', () => {
|
|
6
|
-
it('renders
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
it('renders a muted placeholder with the given class', async () => {
|
|
15
|
+
const { container } = await render(SkeletonHost);
|
|
16
|
+
const skeleton = container.querySelector('.bg-muted');
|
|
17
|
+
expect(skeleton?.classList).toContain('h-4');
|
|
18
|
+
expect(skeleton?.classList).toContain('w-32');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('pulses', async () => {
|
|
22
|
+
const animate = vi.spyOn(lynxMainThread(), '__ElementAnimate');
|
|
23
|
+
const { container } = await render(SkeletonHost);
|
|
24
|
+
expect(animate).toHaveBeenCalledWith(
|
|
25
|
+
container.querySelector('.bg-muted'),
|
|
26
|
+
expect.any(Array),
|
|
27
|
+
);
|
|
28
|
+
animate.mockRestore();
|
|
12
29
|
});
|
|
13
30
|
});
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from '@angular/core';
|
|
10
10
|
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { pulse } from '../../utils/animate';
|
|
13
13
|
import { cn } from '../../utils/cn';
|
|
14
14
|
|
|
15
15
|
@Component({
|
|
@@ -22,22 +22,16 @@ import { cn } from '../../utils/cn';
|
|
|
22
22
|
export class UiSkeleton {
|
|
23
23
|
readonly userClass = input<string>('', { alias: 'class' });
|
|
24
24
|
|
|
25
|
-
readonly skeletonRef = viewChild<ElementRef>('skeleton');
|
|
26
|
-
#pulseAnim?: AnimationHandle;
|
|
25
|
+
readonly skeletonRef = viewChild.required<ElementRef>('skeleton');
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
this.#pulseAnim?.cancel();
|
|
37
|
-
this.#pulseAnim = pulse(el);
|
|
38
|
-
/* v8 ignore stop */
|
|
39
|
-
});
|
|
40
|
-
}
|
|
27
|
+
/**
|
|
28
|
+
* Pulses the skeleton. A field rather than a constructor: Angular's test
|
|
29
|
+
* build keeps a function for a constructor that nothing calls, which an
|
|
30
|
+
* app's coverage report would show as uncovered.
|
|
31
|
+
*/
|
|
32
|
+
protected readonly pulsing = effect(() => {
|
|
33
|
+
pulse(this.skeletonRef().nativeElement);
|
|
34
|
+
});
|
|
41
35
|
|
|
42
36
|
protected readonly skeletonClass = computed(() =>
|
|
43
37
|
cn('rounded-md bg-muted', this.userClass()),
|
|
@@ -1,39 +1,84 @@
|
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
1
2
|
import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import {
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { lynxMainThread } from '../../../test-utils/lynx-main-thread';
|
|
5
|
+
import { ICON_LIGHT_COLORS } from '../icon/icon-theme';
|
|
4
6
|
import { UiSpinner } from './spinner';
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
@Component({
|
|
9
|
+
imports: [UiSpinner],
|
|
10
|
+
template: `
|
|
11
|
+
<ui-spinner [size]="size()" [color]="color()" [class]="className()" />
|
|
12
|
+
`,
|
|
13
|
+
})
|
|
14
|
+
class SpinnerHost {
|
|
15
|
+
readonly size = signal<'xs' | 'sm' | 'md' | 'lg'>('md');
|
|
16
|
+
readonly color = signal<string | undefined>(undefined);
|
|
17
|
+
readonly className = signal('');
|
|
18
|
+
}
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
const renderSpinner = async () => {
|
|
21
|
+
const result = await render(SpinnerHost);
|
|
22
|
+
return {
|
|
23
|
+
...result,
|
|
24
|
+
host: result.componentRef.instance as SpinnerHost,
|
|
25
|
+
// The spinner's own view: the one that holds the svg.
|
|
26
|
+
spinner: () => result.container.querySelector('svg')!.parentElement!,
|
|
27
|
+
svgContent: () =>
|
|
28
|
+
result.container.querySelector('svg')!.getAttribute('content'),
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
describe('UiSpinner', () => {
|
|
33
|
+
it('renders the loader at the default size', async () => {
|
|
34
|
+
const { spinner } = await renderSpinner();
|
|
35
|
+
expect(spinner().getAttribute('style')).toContain('width: 24px');
|
|
36
|
+
expect(spinner().getAttribute('style')).toContain('height: 24px');
|
|
23
37
|
});
|
|
24
38
|
|
|
25
39
|
it('resizes for each supported size', async () => {
|
|
26
|
-
const {
|
|
40
|
+
const { host, spinner } = await renderSpinner();
|
|
27
41
|
for (const [size, px] of [
|
|
28
42
|
['xs', 16],
|
|
29
43
|
['sm', 20],
|
|
30
44
|
['md', 24],
|
|
31
45
|
['lg', 32],
|
|
32
46
|
] as const) {
|
|
33
|
-
|
|
47
|
+
host.size.set(size);
|
|
34
48
|
await waitForUpdate();
|
|
35
|
-
|
|
36
|
-
expect(view?.getAttribute('style')).toContain(`${px}px`);
|
|
49
|
+
expect(spinner().getAttribute('style')).toContain(`width: ${px}px`);
|
|
37
50
|
}
|
|
38
51
|
});
|
|
52
|
+
|
|
53
|
+
it("draws in the theme's foreground when no color is set", async () => {
|
|
54
|
+
// Lynx's SVG parser reads neither `currentColor` nor CSS variables.
|
|
55
|
+
const { svgContent } = await renderSpinner();
|
|
56
|
+
expect(svgContent()).toContain(ICON_LIGHT_COLORS['foreground']);
|
|
57
|
+
expect(svgContent()).not.toContain('currentColor');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('draws in a literal color or a theme token', async () => {
|
|
61
|
+
const { host, svgContent } = await renderSpinner();
|
|
62
|
+
host.color.set('rgb(0, 0, 0)');
|
|
63
|
+
await waitForUpdate();
|
|
64
|
+
expect(svgContent()).toContain('rgb(0, 0, 0)');
|
|
65
|
+
|
|
66
|
+
host.color.set('var(--primary-foreground)');
|
|
67
|
+
await waitForUpdate();
|
|
68
|
+
expect(svgContent()).toContain(ICON_LIGHT_COLORS['primary-foreground']);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('takes the class input', async () => {
|
|
72
|
+
const { host, spinner } = await renderSpinner();
|
|
73
|
+
host.className.set('ml-2');
|
|
74
|
+
await waitForUpdate();
|
|
75
|
+
expect(spinner().getAttribute('class')).toBe('ml-2');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('turns continuously', async () => {
|
|
79
|
+
const animate = vi.spyOn(lynxMainThread(), '__ElementAnimate');
|
|
80
|
+
const { spinner } = await renderSpinner();
|
|
81
|
+
expect(animate).toHaveBeenCalledWith(spinner(), expect.any(Array));
|
|
82
|
+
animate.mockRestore();
|
|
83
|
+
});
|
|
39
84
|
});
|
|
@@ -9,13 +9,15 @@ import {
|
|
|
9
9
|
} from '@angular/core';
|
|
10
10
|
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
11
11
|
|
|
12
|
+
import { cn } from '../../utils/cn';
|
|
13
|
+
import { injectIconColor } from '../icon/icon-theme';
|
|
14
|
+
|
|
12
15
|
const LOADER_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4"/><path d="m16.2 7.8 2.9-2.9"/><path d="M18 12h4"/><path d="m16.2 16.2 2.9 2.9"/><path d="M12 18v4"/><path d="m4.9 19.1 2.9-2.9"/><path d="M2 12h4"/><path d="m4.9 4.9 2.9 2.9"/></svg>`;
|
|
13
16
|
|
|
14
17
|
const SIZE_MAP = { xs: 16, sm: 20, md: 24, lg: 32 } as const;
|
|
15
18
|
|
|
16
19
|
@Component({
|
|
17
20
|
selector: 'ui-spinner',
|
|
18
|
-
standalone: true,
|
|
19
21
|
imports: [LYNX_ELEMENTS],
|
|
20
22
|
encapsulation: ViewEncapsulation.None,
|
|
21
23
|
// SVG markup is passed via [attr.content] rather than as child elements.
|
|
@@ -24,58 +26,63 @@ const SIZE_MAP = { xs: 16, sm: 20, md: 24, lg: 32 } as const;
|
|
|
24
26
|
// SVG markup string as the `content` attribute is the Lynx-native way to
|
|
25
27
|
// embed vector graphics.
|
|
26
28
|
template: `
|
|
27
|
-
<view #spinner [style]="sizeStyle()">
|
|
29
|
+
<view #spinner [class]="containerClass()" [style]="sizeStyle()">
|
|
28
30
|
<svg [attr.content]="svgContent()" style="width: 100%; height: 100%;" />
|
|
29
31
|
</view>
|
|
30
32
|
`,
|
|
31
33
|
})
|
|
32
34
|
export class UiSpinner {
|
|
33
35
|
readonly size = input<'xs' | 'sm' | 'md' | 'lg'>('md');
|
|
36
|
+
/**
|
|
37
|
+
* A literal color or a theme token (`var(--primary)`). Without one, the
|
|
38
|
+
* spinner takes the color its container sets (see `UI_ICON_COLOR`), then the
|
|
39
|
+
* theme's foreground.
|
|
40
|
+
*/
|
|
34
41
|
readonly color = input<string | undefined>(undefined);
|
|
42
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
35
43
|
|
|
36
|
-
readonly
|
|
44
|
+
readonly #resolveColor = injectIconColor();
|
|
37
45
|
|
|
38
|
-
|
|
39
|
-
effect(() => {
|
|
40
|
-
const el = this.spinnerRef()?.nativeElement;
|
|
41
|
-
// el.animate() is inert in jsdom (see animate.ts's transitionTransform
|
|
42
|
-
// rationale). The continuous rotation is exercised on-device.
|
|
43
|
-
/* v8 ignore start */
|
|
44
|
-
if (!el) return;
|
|
45
|
-
el.animate(
|
|
46
|
-
[{ transform: 'rotate(0deg)' }, { transform: 'rotate(360deg)' }],
|
|
47
|
-
{
|
|
48
|
-
duration: 1600,
|
|
49
|
-
iterations: Infinity,
|
|
50
|
-
easing: 'linear',
|
|
51
|
-
},
|
|
52
|
-
);
|
|
53
|
-
/* v8 ignore stop */
|
|
54
|
-
});
|
|
55
|
-
}
|
|
46
|
+
readonly spinnerRef = viewChild.required<ElementRef>('spinner');
|
|
56
47
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// render as each tick re-runs and re-throws. split/join is ES2019-safe and
|
|
72
|
-
// is preferred over `replace(/.../g)` here because the search term is a
|
|
73
|
-
// plain literal and needs no regex escaping.
|
|
74
|
-
return LOADER_SVG.split('currentColor').join(c);
|
|
75
|
-
}
|
|
76
|
-
return LOADER_SVG;
|
|
48
|
+
/**
|
|
49
|
+
* Turns the spinner for as long as it's shown. A field rather than a
|
|
50
|
+
* constructor: Angular's test build keeps a function for a constructor that
|
|
51
|
+
* nothing calls, which an app's coverage report would show as uncovered.
|
|
52
|
+
*/
|
|
53
|
+
protected readonly rotation = effect(() => {
|
|
54
|
+
this.spinnerRef().nativeElement.animate(
|
|
55
|
+
[{ transform: 'rotate(0deg)' }, { transform: 'rotate(360deg)' }],
|
|
56
|
+
{
|
|
57
|
+
duration: 1600,
|
|
58
|
+
iterations: Infinity,
|
|
59
|
+
easing: 'linear',
|
|
60
|
+
},
|
|
61
|
+
);
|
|
77
62
|
});
|
|
78
63
|
|
|
64
|
+
// Lynx SVG elements don't inherit the CSS `color` property into their stroke
|
|
65
|
+
// attributes, so we substitute the literal `currentColor` in the SVG string
|
|
66
|
+
// rather than relying on `currentColor` propagation via CSS. The color is
|
|
67
|
+
// resolved first: the SVG parser can't read `var(--x)` either (see
|
|
68
|
+
// icon-theme.ts).
|
|
69
|
+
//
|
|
70
|
+
// Uses split().join() instead of String.prototype.replaceAll(). replaceAll is
|
|
71
|
+
// ES2021, but Lynx's main thread runs on PrimJS at an ES2019 target; because
|
|
72
|
+
// it's a runtime method (not syntax) the build's down-leveling does not
|
|
73
|
+
// polyfill it, so it simply doesn't exist there. Calling it throws
|
|
74
|
+
// "main-thread.js exception: not a function" from inside Angular change
|
|
75
|
+
// detection, which aborts the whole tick before Lynx flushes the element tree
|
|
76
|
+
// — so the spinner never paints, and the error count climbs on every render
|
|
77
|
+
// as each tick re-runs and re-throws. split/join is ES2019-safe and is
|
|
78
|
+
// preferred over `replace(/.../g)` here because the search term is a plain
|
|
79
|
+
// literal and needs no regex escaping.
|
|
80
|
+
protected readonly svgContent = computed(() =>
|
|
81
|
+
LOADER_SVG.split('currentColor').join(this.#resolveColor(this.color())),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
protected readonly containerClass = computed(() => cn(this.userClass()));
|
|
85
|
+
|
|
79
86
|
protected readonly sizeStyle = computed(() => {
|
|
80
87
|
const px = SIZE_MAP[this.size()];
|
|
81
88
|
return `width: ${px}px; height: ${px}px;`;
|
|
@@ -1,63 +1,133 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
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';
|
|
4
20
|
import { UiSwitch } from './switch';
|
|
5
21
|
|
|
22
|
+
@Component({
|
|
23
|
+
imports: [UiSwitch],
|
|
24
|
+
template: `
|
|
25
|
+
<ui-switch
|
|
26
|
+
[(checked)]="checked"
|
|
27
|
+
[disabled]="disabled()"
|
|
28
|
+
class="ml-2"
|
|
29
|
+
(changed)="changes.push($event)"
|
|
30
|
+
/>
|
|
31
|
+
`,
|
|
32
|
+
})
|
|
33
|
+
class SwitchHost {
|
|
34
|
+
readonly checked = signal(false);
|
|
35
|
+
readonly disabled = signal(false);
|
|
36
|
+
readonly changes: boolean[] = [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const renderSwitch = async () => {
|
|
40
|
+
const result = await render(SwitchHost);
|
|
41
|
+
return {
|
|
42
|
+
...result,
|
|
43
|
+
host: result.componentRef.instance as SwitchHost,
|
|
44
|
+
track: () => result.container.querySelector('.flex-row')!,
|
|
45
|
+
thumb: () => result.container.querySelector('.bg-background')!,
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** `__ElementAnimate`'s first argument for starting an animation. */
|
|
50
|
+
const START = 0;
|
|
51
|
+
/** `__ElementAnimate`'s first argument for canceling one. */
|
|
52
|
+
const CANCEL = 3;
|
|
53
|
+
|
|
6
54
|
describe('UiSwitch', () => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
55
|
+
// Watched per test: the testing library resets the main thread after each.
|
|
56
|
+
let animate: MockInstance<LynxMainThread['__ElementAnimate']>;
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
animate = vi.spyOn(lynxMainThread(), '__ElementAnimate');
|
|
10
59
|
});
|
|
60
|
+
afterEach(() => animate.mockRestore());
|
|
11
61
|
|
|
12
|
-
it('
|
|
13
|
-
const {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
inst.toggle();
|
|
19
|
-
expect(inst.checked()).toBe(true);
|
|
20
|
-
inst.toggle();
|
|
21
|
-
expect(inst.checked()).toBe(false);
|
|
22
|
-
expect(emitted).toEqual([true, false]);
|
|
62
|
+
it('renders off, with the thumb at the left and the given class', async () => {
|
|
63
|
+
const { track, thumb } = await renderSwitch();
|
|
64
|
+
expect(track().classList).toContain('bg-input');
|
|
65
|
+
expect(track().classList).toContain('ml-2');
|
|
66
|
+
expect(thumb().getAttribute('style')).toContain('translateX(2px)');
|
|
23
67
|
});
|
|
24
68
|
|
|
25
|
-
it('
|
|
26
|
-
const {
|
|
27
|
-
const inst = componentRef.instance as UiSwitch;
|
|
28
|
-
setInputSignal(inst.disabled, true);
|
|
29
|
-
const listener = vi.fn();
|
|
30
|
-
inst.changed.subscribe(listener);
|
|
69
|
+
it('turns on and off on tap, emitting `changed`', async () => {
|
|
70
|
+
const { host, track, thumb } = await renderSwitch();
|
|
31
71
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
expect(
|
|
72
|
+
fireEvent.tap(track());
|
|
73
|
+
await waitForUpdate();
|
|
74
|
+
expect(host.checked()).toBe(true);
|
|
75
|
+
expect(track().classList).toContain('bg-primary');
|
|
76
|
+
expect(thumb().getAttribute('style')).toContain('translateX(22px)');
|
|
77
|
+
|
|
78
|
+
fireEvent.tap(track());
|
|
79
|
+
await waitForUpdate();
|
|
80
|
+
expect(host.checked()).toBe(false);
|
|
81
|
+
expect(host.changes).toEqual([true, false]);
|
|
35
82
|
});
|
|
36
83
|
|
|
37
|
-
it('
|
|
38
|
-
const {
|
|
39
|
-
const inst = componentRef.instance as UiSwitch;
|
|
40
|
-
let thumb = container.querySelectorAll('view')[1];
|
|
41
|
-
expect(thumb?.getAttribute('style')).toContain('translateX(2px)');
|
|
84
|
+
it('springs the thumb across the track, squishing it halfway', async () => {
|
|
85
|
+
const { track, thumb } = await renderSwitch();
|
|
42
86
|
|
|
43
|
-
|
|
87
|
+
fireEvent.tap(track());
|
|
88
|
+
expect(animate).toHaveBeenCalledWith(thumb(), [
|
|
89
|
+
START,
|
|
90
|
+
expect.any(String),
|
|
91
|
+
[
|
|
92
|
+
{ transform: 'translateX(2px) scale(1)' },
|
|
93
|
+
{ transform: 'translateX(12px) scale(1.1)' },
|
|
94
|
+
{ transform: 'translateX(22px) scale(1)' },
|
|
95
|
+
],
|
|
96
|
+
expect.objectContaining({ fill: 'forwards' }),
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
// Turning it back off cuts the first spring short and springs it back.
|
|
100
|
+
fireEvent.tap(track());
|
|
101
|
+
expect(animate).toHaveBeenCalledWith(thumb(), [CANCEL, expect.any(String)]);
|
|
102
|
+
expect(animate).toHaveBeenLastCalledWith(thumb(), [
|
|
103
|
+
START,
|
|
104
|
+
expect.any(String),
|
|
105
|
+
[
|
|
106
|
+
{ transform: 'translateX(22px) scale(1)' },
|
|
107
|
+
{ transform: 'translateX(12px) scale(1.1)' },
|
|
108
|
+
{ transform: 'translateX(2px) scale(1)' },
|
|
109
|
+
],
|
|
110
|
+
expect.objectContaining({ fill: 'forwards' }),
|
|
111
|
+
]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('follows its checked binding', async () => {
|
|
115
|
+
const { host, thumb } = await renderSwitch();
|
|
116
|
+
host.checked.set(true);
|
|
44
117
|
await waitForUpdate();
|
|
45
|
-
thumb
|
|
46
|
-
expect(thumb?.getAttribute('style')).toContain('translateX(22px)');
|
|
118
|
+
expect(thumb().getAttribute('style')).toContain('translateX(22px)');
|
|
47
119
|
});
|
|
48
120
|
|
|
49
|
-
it('
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
setInputSignal(inst.userClass, 'custom-cls');
|
|
121
|
+
it('dims and ignores taps while disabled', async () => {
|
|
122
|
+
const { host, track } = await renderSwitch();
|
|
123
|
+
host.disabled.set(true);
|
|
124
|
+
await waitForUpdate();
|
|
125
|
+
expect(track().classList).toContain('opacity-50');
|
|
126
|
+
|
|
127
|
+
fireEvent.tap(track());
|
|
57
128
|
await waitForUpdate();
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
expect(
|
|
61
|
-
expect(cls).toContain('custom-cls');
|
|
129
|
+
expect(host.checked()).toBe(false);
|
|
130
|
+
expect(host.changes).toEqual([]);
|
|
131
|
+
expect(animate).not.toHaveBeenCalled();
|
|
62
132
|
});
|
|
63
133
|
});
|
|
@@ -52,7 +52,8 @@ export class UiSwitch {
|
|
|
52
52
|
|
|
53
53
|
readonly changed = output<boolean>();
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
/** The thumb is always in the template, so toggle() can always slide it. */
|
|
56
|
+
readonly thumbRef = viewChild.required<ElementRef>('thumb');
|
|
56
57
|
#activeAnimation?: AnimationHandle;
|
|
57
58
|
|
|
58
59
|
protected readonly trackClass = computed(() =>
|
|
@@ -76,21 +77,15 @@ export class UiSwitch {
|
|
|
76
77
|
toggle(): void {
|
|
77
78
|
if (this.disabled()) return;
|
|
78
79
|
|
|
79
|
-
const thumb = this.thumbRef()
|
|
80
|
+
const thumb = this.thumbRef().nativeElement;
|
|
80
81
|
const wasChecked = this.checked();
|
|
81
82
|
const next = !wasChecked;
|
|
82
83
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const from = wasChecked ? THUMB_OFFSET_ON : THUMB_OFFSET_OFF;
|
|
89
|
-
const to = next ? THUMB_OFFSET_ON : THUMB_OFFSET_OFF;
|
|
90
|
-
// Spring translate with squish effect at midpoint
|
|
91
|
-
this.#activeAnimation = springTranslateX(thumb, from, to);
|
|
92
|
-
}
|
|
93
|
-
/* v8 ignore stop */
|
|
84
|
+
this.#activeAnimation?.cancel();
|
|
85
|
+
const from = wasChecked ? THUMB_OFFSET_ON : THUMB_OFFSET_OFF;
|
|
86
|
+
const to = next ? THUMB_OFFSET_ON : THUMB_OFFSET_OFF;
|
|
87
|
+
// Spring translate with squish effect at midpoint
|
|
88
|
+
this.#activeAnimation = springTranslateX(thumb, from, to);
|
|
94
89
|
|
|
95
90
|
this.checked.set(next);
|
|
96
91
|
this.changed.emit(next);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UiTabBar, UiTabBarItem } from './tab-bar';
|