@blotch/dolan 0.0.0-stage → 22.2.1
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 +22 -0
- package/dist/commands/add.js +132 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/add.test.d.ts +1 -0
- package/dist/commands/add.test.js +209 -0
- package/dist/commands/add.test.js.map +1 -0
- package/dist/commands/diff.d.ts +13 -0
- package/dist/commands/diff.js +155 -0
- package/dist/commands/diff.js.map +1 -0
- package/dist/commands/diff.test.d.ts +1 -0
- package/dist/commands/diff.test.js +327 -0
- package/dist/commands/diff.test.js.map +1 -0
- package/dist/commands/doctor.d.ts +15 -0
- package/dist/commands/doctor.js +248 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/doctor.test.d.ts +1 -0
- package/dist/commands/doctor.test.js +301 -0
- package/dist/commands/doctor.test.js.map +1 -0
- package/dist/commands/eject.d.ts +11 -0
- package/dist/commands/eject.js +55 -0
- package/dist/commands/eject.js.map +1 -0
- package/dist/commands/eject.test.d.ts +1 -0
- package/dist/commands/eject.test.js +151 -0
- package/dist/commands/eject.test.js.map +1 -0
- package/dist/commands/info.d.ts +1 -0
- package/dist/commands/info.js +73 -0
- package/dist/commands/info.js.map +1 -0
- package/dist/commands/info.test.d.ts +1 -0
- package/dist/commands/info.test.js +143 -0
- package/dist/commands/info.test.js.map +1 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +98 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/init.test.d.ts +1 -0
- package/dist/commands/init.test.js +154 -0
- package/dist/commands/init.test.js.map +1 -0
- package/dist/commands/list.d.ts +16 -0
- package/dist/commands/list.js +124 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/list.test.d.ts +1 -0
- package/dist/commands/list.test.js +285 -0
- package/dist/commands/list.test.js.map +1 -0
- package/dist/commands/outdated.d.ts +13 -0
- package/dist/commands/outdated.js +116 -0
- package/dist/commands/outdated.js.map +1 -0
- package/dist/commands/outdated.test.d.ts +1 -0
- package/dist/commands/outdated.test.js +208 -0
- package/dist/commands/outdated.test.js.map +1 -0
- package/dist/commands/remove.d.ts +18 -0
- package/dist/commands/remove.js +161 -0
- package/dist/commands/remove.js.map +1 -0
- package/dist/commands/remove.test.d.ts +1 -0
- package/dist/commands/remove.test.js +361 -0
- package/dist/commands/remove.test.js.map +1 -0
- package/dist/commands/theme.d.ts +1 -0
- package/dist/commands/theme.js +66 -0
- package/dist/commands/theme.js.map +1 -0
- package/dist/commands/theme.test.d.ts +1 -0
- package/dist/commands/theme.test.js +133 -0
- package/dist/commands/theme.test.js.map +1 -0
- package/dist/commands/update.d.ts +31 -0
- package/dist/commands/update.js +601 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/commands/update.test.d.ts +1 -0
- package/dist/commands/update.test.js +1233 -0
- package/dist/commands/update.test.js.map +1 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.js +19 -0
- package/dist/config.js.map +1 -0
- package/dist/config.test.d.ts +1 -0
- package/dist/config.test.js +48 -0
- package/dist/config.test.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +89 -0
- package/dist/index.js.map +1 -0
- package/dist/lockfile.d.ts +21 -0
- package/dist/lockfile.js +39 -0
- package/dist/lockfile.js.map +1 -0
- package/dist/registry.d.ts +14 -0
- package/dist/registry.js +85 -0
- package/dist/registry.js.map +1 -0
- package/dist/registry.test.d.ts +1 -0
- package/dist/registry.test.js +95 -0
- package/dist/registry.test.js.map +1 -0
- package/dist/test-utils.d.ts +16 -0
- package/dist/test-utils.js +65 -0
- package/dist/test-utils.js.map +1 -0
- package/dist/ui/components/accordion/accordion.test.ts +194 -0
- package/dist/ui/components/accordion/accordion.ts +173 -0
- package/dist/ui/components/accordion/index.ts +6 -0
- package/dist/ui/components/action-sheet/action-sheet.test.ts +211 -0
- package/dist/ui/components/action-sheet/action-sheet.ts +353 -0
- package/dist/ui/components/action-sheet/index.ts +6 -0
- package/dist/ui/components/alert/alert.test.ts +48 -0
- package/dist/ui/components/alert/alert.ts +104 -0
- package/dist/ui/components/alert/index.ts +1 -0
- package/dist/ui/components/alert-dialog/alert-dialog.test.ts +141 -0
- package/dist/ui/components/alert-dialog/alert-dialog.ts +243 -0
- package/dist/ui/components/alert-dialog/index.ts +7 -0
- package/dist/ui/components/area-chart/area-chart-render.test.ts +121 -0
- package/dist/ui/components/area-chart/area-chart.test.ts +158 -0
- package/dist/ui/components/area-chart/area-chart.ts +349 -0
- package/dist/ui/components/area-chart/index.ts +6 -0
- package/dist/ui/components/aspect-ratio/aspect-ratio.test.ts +10 -0
- package/dist/ui/components/aspect-ratio/aspect-ratio.ts +28 -0
- package/dist/ui/components/aspect-ratio/index.ts +1 -0
- package/dist/ui/components/avatar/avatar.test.ts +69 -0
- package/dist/ui/components/avatar/avatar.ts +131 -0
- package/dist/ui/components/avatar/index.ts +1 -0
- package/dist/ui/components/badge/badge.test.ts +49 -0
- package/dist/ui/components/badge/badge.ts +97 -0
- package/dist/ui/components/badge/index.ts +1 -0
- package/dist/ui/components/bar-chart/bar-chart-render.test.ts +174 -0
- package/dist/ui/components/bar-chart/bar-chart.test.ts +73 -0
- package/dist/ui/components/bar-chart/bar-chart.ts +327 -0
- package/dist/ui/components/bar-chart/index.ts +6 -0
- package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +163 -0
- package/dist/ui/components/bottom-sheet/bottom-sheet.ts +339 -0
- package/dist/ui/components/bottom-sheet/index.ts +1 -0
- package/dist/ui/components/button/button-render.test.ts +113 -0
- package/dist/ui/components/button/button.test.ts +70 -0
- package/dist/ui/components/button/button.ts +170 -0
- package/dist/ui/components/button/index.ts +6 -0
- package/dist/ui/components/button-group/button-group-render.test.ts +146 -0
- package/dist/ui/components/button-group/button-group.test.ts +54 -0
- package/dist/ui/components/button-group/button-group.ts +209 -0
- package/dist/ui/components/button-group/index.ts +8 -0
- package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +174 -0
- package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +96 -0
- package/dist/ui/components/candlestick-chart/candlestick-chart.ts +355 -0
- package/dist/ui/components/candlestick-chart/index.ts +7 -0
- package/dist/ui/components/card/card.test.ts +110 -0
- package/dist/ui/components/card/card.ts +162 -0
- package/dist/ui/components/card/index.ts +8 -0
- package/dist/ui/components/cartesian-chart/cartesian-chart-render.test.ts +188 -0
- package/dist/ui/components/cartesian-chart/cartesian-chart.test.ts +549 -0
- package/dist/ui/components/cartesian-chart/cartesian-chart.ts +1140 -0
- package/dist/ui/components/cartesian-chart/index.ts +18 -0
- package/dist/ui/components/checkbox/checkbox.test.ts +59 -0
- package/dist/ui/components/checkbox/checkbox.ts +96 -0
- package/dist/ui/components/checkbox/index.ts +1 -0
- package/dist/ui/components/collapsible/collapsible.test.ts +112 -0
- package/dist/ui/components/collapsible/collapsible.ts +122 -0
- package/dist/ui/components/collapsible/index.ts +5 -0
- package/dist/ui/components/dialog/dialog.test.ts +157 -0
- package/dist/ui/components/dialog/dialog.ts +248 -0
- package/dist/ui/components/dialog/index.ts +7 -0
- package/dist/ui/components/empty-state/empty-state.test.ts +60 -0
- package/dist/ui/components/empty-state/empty-state.ts +46 -0
- package/dist/ui/components/empty-state/index.ts +1 -0
- package/dist/ui/components/icon/icon.test.ts +52 -0
- package/dist/ui/components/icon/icon.ts +54 -0
- package/dist/ui/components/icon/icons.ts +569 -0
- package/dist/ui/components/icon/index.ts +2 -0
- package/dist/ui/components/input/index.ts +1 -0
- package/dist/ui/components/input/input.test.ts +134 -0
- package/dist/ui/components/input/input.ts +189 -0
- package/dist/ui/components/label/index.ts +1 -0
- package/dist/ui/components/label/label.test.ts +38 -0
- package/dist/ui/components/label/label.ts +24 -0
- package/dist/ui/components/line-chart/index.ts +6 -0
- package/dist/ui/components/line-chart/line-chart-render.test.ts +121 -0
- package/dist/ui/components/line-chart/line-chart.test.ts +81 -0
- package/dist/ui/components/line-chart/line-chart.ts +273 -0
- package/dist/ui/components/list/index.ts +1 -0
- package/dist/ui/components/list/list.test.ts +35 -0
- package/dist/ui/components/list/list.ts +71 -0
- package/dist/ui/components/nav-drawer/index.ts +8 -0
- package/dist/ui/components/nav-drawer/nav-drawer.test.ts +267 -0
- package/dist/ui/components/nav-drawer/nav-drawer.ts +504 -0
- package/dist/ui/components/pagination/index.ts +1 -0
- package/dist/ui/components/pagination/pagination.test.ts +96 -0
- package/dist/ui/components/pagination/pagination.ts +147 -0
- package/dist/ui/components/progress/index.ts +1 -0
- package/dist/ui/components/progress/progress.test.ts +71 -0
- package/dist/ui/components/progress/progress.ts +118 -0
- package/dist/ui/components/radio-group/index.ts +1 -0
- package/dist/ui/components/radio-group/radio-group.test.ts +115 -0
- package/dist/ui/components/radio-group/radio-group.ts +129 -0
- package/dist/ui/components/scatter-chart/index.ts +6 -0
- package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +157 -0
- package/dist/ui/components/scatter-chart/scatter-chart.test.ts +57 -0
- package/dist/ui/components/scatter-chart/scatter-chart.ts +277 -0
- package/dist/ui/components/scroll-area/index.ts +1 -0
- package/dist/ui/components/scroll-area/scroll-area.test.ts +40 -0
- package/dist/ui/components/scroll-area/scroll-area.ts +73 -0
- package/dist/ui/components/select/index.ts +1 -0
- package/dist/ui/components/select/select.test.ts +107 -0
- package/dist/ui/components/select/select.ts +146 -0
- package/dist/ui/components/separator/index.ts +1 -0
- package/dist/ui/components/separator/separator.test.ts +26 -0
- package/dist/ui/components/separator/separator.ts +45 -0
- package/dist/ui/components/sheet/index.ts +7 -0
- package/dist/ui/components/sheet/sheet.test.ts +44 -0
- package/dist/ui/components/sheet/sheet.ts +114 -0
- package/dist/ui/components/skeleton/index.ts +1 -0
- package/dist/ui/components/skeleton/skeleton.test.ts +13 -0
- package/dist/ui/components/skeleton/skeleton.ts +45 -0
- package/dist/ui/components/spinner/index.ts +1 -0
- package/dist/ui/components/spinner/spinner.test.ts +39 -0
- package/dist/ui/components/spinner/spinner.ts +83 -0
- package/dist/ui/components/switch/index.ts +1 -0
- package/dist/ui/components/switch/switch.test.ts +63 -0
- package/dist/ui/components/switch/switch.ts +98 -0
- package/dist/ui/components/tabs/index.ts +1 -0
- package/dist/ui/components/tabs/tabs.test.ts +199 -0
- package/dist/ui/components/tabs/tabs.ts +317 -0
- package/dist/ui/components/textarea/index.ts +1 -0
- package/dist/ui/components/textarea/textarea.test.ts +138 -0
- package/dist/ui/components/textarea/textarea.ts +221 -0
- package/dist/ui/components/toast/index.ts +9 -0
- package/dist/ui/components/toast/toast-state.test.ts +54 -0
- package/dist/ui/components/toast/toast-state.ts +46 -0
- package/dist/ui/components/toast/toast.test.ts +342 -0
- package/dist/ui/components/toast/toast.ts +527 -0
- package/dist/ui/components/toggle/index.ts +7 -0
- package/dist/ui/components/toggle/toggle.test.ts +186 -0
- package/dist/ui/components/toggle/toggle.ts +218 -0
- package/dist/ui/components/typography/index.ts +5 -0
- package/dist/ui/components/typography/typography.test.ts +55 -0
- package/dist/ui/components/typography/typography.ts +44 -0
- package/dist/ui/theme/dark.css +41 -0
- package/dist/ui/theme/default.css +56 -0
- package/dist/ui/theme/tailwind-plugin.test.ts +124 -0
- package/dist/ui/theme/tailwind-plugin.ts +151 -0
- package/dist/ui/utils/animate.test.ts +324 -0
- package/dist/ui/utils/animate.ts +544 -0
- package/dist/ui/utils/cn.test.ts +27 -0
- package/dist/ui/utils/cn.ts +6 -0
- package/dist/utils/analyze.d.ts +32 -0
- package/dist/utils/analyze.js +109 -0
- package/dist/utils/analyze.js.map +1 -0
- package/dist/utils/analyze.test.d.ts +1 -0
- package/dist/utils/analyze.test.js +146 -0
- package/dist/utils/analyze.test.js.map +1 -0
- package/dist/utils/animate.d.ts +210 -0
- package/dist/utils/animate.js +423 -0
- package/dist/utils/animate.js.map +1 -0
- package/dist/utils/animate.test.d.ts +1 -0
- package/dist/utils/animate.test.js +365 -0
- package/dist/utils/animate.test.js.map +1 -0
- package/dist/utils/banner.d.ts +1 -0
- package/dist/utils/banner.js +15 -0
- package/dist/utils/banner.js.map +1 -0
- package/dist/utils/banner.test.d.ts +1 -0
- package/dist/utils/banner.test.js +29 -0
- package/dist/utils/banner.test.js.map +1 -0
- package/dist/utils/cn.d.ts +2 -0
- package/dist/utils/cn.js +6 -0
- package/dist/utils/cn.js.map +1 -0
- package/dist/utils/cn.test.d.ts +1 -0
- package/dist/utils/cn.test.js +21 -0
- package/dist/utils/cn.test.js.map +1 -0
- package/dist/utils/format.d.ts +31 -0
- package/dist/utils/format.js +95 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/format.test.d.ts +1 -0
- package/dist/utils/format.test.js +71 -0
- package/dist/utils/format.test.js.map +1 -0
- package/dist/utils/logger.d.ts +6 -0
- package/dist/utils/logger.js +16 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/logger.test.d.ts +1 -0
- package/dist/utils/logger.test.js +36 -0
- package/dist/utils/logger.test.js.map +1 -0
- package/dist/utils/resolve-paths.d.ts +12 -0
- package/dist/utils/resolve-paths.js +29 -0
- package/dist/utils/resolve-paths.js.map +1 -0
- package/dist/utils/resolve-paths.test.d.ts +1 -0
- package/dist/utils/resolve-paths.test.js +52 -0
- package/dist/utils/resolve-paths.test.js.map +1 -0
- package/package.json +56 -4
- package/README.md +0 -3
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ElementRef,
|
|
3
|
+
Component,
|
|
4
|
+
ViewEncapsulation,
|
|
5
|
+
computed,
|
|
6
|
+
effect,
|
|
7
|
+
input,
|
|
8
|
+
viewChild,
|
|
9
|
+
} from '@angular/core';
|
|
10
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
11
|
+
|
|
12
|
+
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
|
+
|
|
14
|
+
const SIZE_MAP = { xs: 16, sm: 20, md: 24, lg: 32 } as const;
|
|
15
|
+
|
|
16
|
+
@Component({
|
|
17
|
+
selector: 'ui-spinner',
|
|
18
|
+
standalone: true,
|
|
19
|
+
imports: [LYNX_ELEMENTS],
|
|
20
|
+
encapsulation: ViewEncapsulation.None,
|
|
21
|
+
// SVG markup is passed via [attr.content] rather than as child elements.
|
|
22
|
+
// Lynx's native <svg> element can't contain child nodes in the flat element
|
|
23
|
+
// model — the renderer doesn't support nested SVG elements. Passing the full
|
|
24
|
+
// SVG markup string as the `content` attribute is the Lynx-native way to
|
|
25
|
+
// embed vector graphics.
|
|
26
|
+
template: `
|
|
27
|
+
<view #spinner [style]="sizeStyle()">
|
|
28
|
+
<svg [attr.content]="svgContent()" style="width: 100%; height: 100%;" />
|
|
29
|
+
</view>
|
|
30
|
+
`,
|
|
31
|
+
})
|
|
32
|
+
export class UiSpinner {
|
|
33
|
+
readonly size = input<'xs' | 'sm' | 'md' | 'lg'>('md');
|
|
34
|
+
readonly color = input<string | undefined>(undefined);
|
|
35
|
+
|
|
36
|
+
readonly spinnerRef = viewChild<ElementRef>('spinner');
|
|
37
|
+
|
|
38
|
+
constructor() {
|
|
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
|
+
}
|
|
56
|
+
|
|
57
|
+
protected readonly svgContent = computed(() => {
|
|
58
|
+
const c = this.color();
|
|
59
|
+
if (c) {
|
|
60
|
+
// Lynx SVG elements don't inherit the CSS `color` property into their
|
|
61
|
+
// stroke attributes, so we substitute the literal `currentColor` in the
|
|
62
|
+
// SVG string rather than relying on `currentColor` propagation via CSS.
|
|
63
|
+
//
|
|
64
|
+
// Uses split().join() instead of String.prototype.replaceAll(). replaceAll
|
|
65
|
+
// is ES2021, but Lynx's main thread runs on PrimJS at an ES2019 target;
|
|
66
|
+
// because it's a runtime method (not syntax) the build's down-leveling does
|
|
67
|
+
// not polyfill it, so it simply doesn't exist there. Calling it throws
|
|
68
|
+
// "main-thread.js exception: not a function" from inside Angular change
|
|
69
|
+
// detection, which aborts the whole tick before Lynx flushes the element
|
|
70
|
+
// tree — so the spinner never paints, and the error count climbs on every
|
|
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;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
protected readonly sizeStyle = computed(() => {
|
|
80
|
+
const px = SIZE_MAP[this.size()];
|
|
81
|
+
return `width: ${px}px; height: ${px}px;`;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UiSwitch } from './switch';
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { setInputSignal } from '../../../test-utils/set-input-signal';
|
|
4
|
+
import { UiSwitch } from './switch';
|
|
5
|
+
|
|
6
|
+
describe('UiSwitch', () => {
|
|
7
|
+
it('renders in the off position by default', async () => {
|
|
8
|
+
const { container } = await render(UiSwitch);
|
|
9
|
+
expect(container).toBeTruthy();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('toggle() flips checked and emits `changed`', async () => {
|
|
13
|
+
const { componentRef } = await render(UiSwitch);
|
|
14
|
+
const inst = componentRef.instance as UiSwitch;
|
|
15
|
+
const emitted: boolean[] = [];
|
|
16
|
+
inst.changed.subscribe((v) => emitted.push(v));
|
|
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]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('toggle() is a no-op when disabled', async () => {
|
|
26
|
+
const { componentRef } = await render(UiSwitch);
|
|
27
|
+
const inst = componentRef.instance as UiSwitch;
|
|
28
|
+
setInputSignal(inst.disabled, true);
|
|
29
|
+
const listener = vi.fn();
|
|
30
|
+
inst.changed.subscribe(listener);
|
|
31
|
+
|
|
32
|
+
inst.toggle();
|
|
33
|
+
expect(inst.checked()).toBe(false);
|
|
34
|
+
expect(listener).not.toHaveBeenCalled();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('positions the thumb differently in ON vs OFF state', async () => {
|
|
38
|
+
const { componentRef, container } = await render(UiSwitch);
|
|
39
|
+
const inst = componentRef.instance as UiSwitch;
|
|
40
|
+
let thumb = container.querySelectorAll('view')[1];
|
|
41
|
+
expect(thumb?.getAttribute('style')).toContain('translateX(2px)');
|
|
42
|
+
|
|
43
|
+
inst.checked.set(true);
|
|
44
|
+
await waitForUpdate();
|
|
45
|
+
thumb = container.querySelectorAll('view')[1];
|
|
46
|
+
expect(thumb?.getAttribute('style')).toContain('translateX(22px)');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('applies opacity-50 when disabled and includes a user class', async () => {
|
|
50
|
+
// Covers the disabled=true branch of trackClass() (line 62) and the
|
|
51
|
+
// userClass() concatenation. Without a render + detectChanges cycle the
|
|
52
|
+
// computed never runs with disabled=true and the branch stays uncovered.
|
|
53
|
+
const { componentRef, container } = await render(UiSwitch);
|
|
54
|
+
const inst = componentRef.instance as UiSwitch;
|
|
55
|
+
setInputSignal(inst.disabled, true);
|
|
56
|
+
setInputSignal(inst.userClass, 'custom-cls');
|
|
57
|
+
await waitForUpdate();
|
|
58
|
+
const track = container.querySelectorAll('view')[0];
|
|
59
|
+
const cls = track?.getAttribute('class') ?? '';
|
|
60
|
+
expect(cls).toContain('opacity-50');
|
|
61
|
+
expect(cls).toContain('custom-cls');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ElementRef,
|
|
3
|
+
Component,
|
|
4
|
+
ViewEncapsulation,
|
|
5
|
+
computed,
|
|
6
|
+
input,
|
|
7
|
+
model,
|
|
8
|
+
output,
|
|
9
|
+
viewChild,
|
|
10
|
+
} from '@angular/core';
|
|
11
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
12
|
+
|
|
13
|
+
import { type AnimationHandle, springTranslateX } from '../../utils/animate';
|
|
14
|
+
import { cn } from '../../utils/cn';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Pixel offsets for the thumb within the 44px track.
|
|
18
|
+
*
|
|
19
|
+
* The thumb is positioned with a px-based `translateX`, so the track and thumb
|
|
20
|
+
* MUST be sized in px too (see the `w-[44px]`/`w-[20px]` arbitrary values in the
|
|
21
|
+
* templates below) — NOT with Tailwind's rem-based scale (`w-11`/`w-5`).
|
|
22
|
+
*
|
|
23
|
+
* Why: Tailwind's default width scale compiles to rem (`w-11` → `2.75rem`), and
|
|
24
|
+
* on Lynx `1rem` resolves against the root `<page>` font-size, which defaults to
|
|
25
|
+
* 14px (`DEFAULT_FONT_SIZE_DP`) — not the 16px browsers use. So `w-11` renders as
|
|
26
|
+
* 38.5px on device, not 44px. Mixing that rem-sized track with a px `translateX`
|
|
27
|
+
* made the ON thumb overshoot the right edge (it was offset 22px inside a 38.5px
|
|
28
|
+
* track). Sizing everything in px keeps the geometry self-consistent regardless
|
|
29
|
+
* of the ambient font-size.
|
|
30
|
+
*
|
|
31
|
+
* Track is 44px wide, 24px tall; thumb is 20px square. OFF: 2px from the left
|
|
32
|
+
* edge; ON: 2px from the right edge (44 - 20 - 2 = 22).
|
|
33
|
+
*/
|
|
34
|
+
const THUMB_OFFSET_OFF = 2;
|
|
35
|
+
const THUMB_OFFSET_ON = 22;
|
|
36
|
+
|
|
37
|
+
@Component({
|
|
38
|
+
selector: 'ui-switch',
|
|
39
|
+
standalone: true,
|
|
40
|
+
imports: [LYNX_ELEMENTS],
|
|
41
|
+
encapsulation: ViewEncapsulation.None,
|
|
42
|
+
template: `
|
|
43
|
+
<view [class]="trackClass()" (bindtap)="toggle()">
|
|
44
|
+
<view #thumb [class]="thumbClass()" [style]="thumbPositionStyle()" />
|
|
45
|
+
</view>
|
|
46
|
+
`,
|
|
47
|
+
})
|
|
48
|
+
export class UiSwitch {
|
|
49
|
+
readonly checked = model(false);
|
|
50
|
+
readonly disabled = input(false);
|
|
51
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
52
|
+
|
|
53
|
+
readonly changed = output<boolean>();
|
|
54
|
+
|
|
55
|
+
readonly thumbRef = viewChild<ElementRef>('thumb');
|
|
56
|
+
#activeAnimation?: AnimationHandle;
|
|
57
|
+
|
|
58
|
+
protected readonly trackClass = computed(() =>
|
|
59
|
+
cn(
|
|
60
|
+
'h-[24px] w-[44px] flex-row items-center rounded-full flex',
|
|
61
|
+
this.checked() ? 'bg-primary' : 'bg-input',
|
|
62
|
+
this.disabled() && 'opacity-50',
|
|
63
|
+
this.userClass(),
|
|
64
|
+
),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
protected readonly thumbClass = computed(() =>
|
|
68
|
+
cn('h-[20px] w-[20px] rounded-full bg-background'),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
protected readonly thumbPositionStyle = computed(
|
|
72
|
+
() =>
|
|
73
|
+
`transform: translateX(${this.checked() ? THUMB_OFFSET_ON : THUMB_OFFSET_OFF}px);`,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
toggle(): void {
|
|
77
|
+
if (this.disabled()) return;
|
|
78
|
+
|
|
79
|
+
const thumb = this.thumbRef()?.nativeElement;
|
|
80
|
+
const wasChecked = this.checked();
|
|
81
|
+
const next = !wasChecked;
|
|
82
|
+
|
|
83
|
+
// springTranslateX calls thumb.animate() which is inert in jsdom (see
|
|
84
|
+
// animate.ts). The spring physics of the thumb is exercised on-device.
|
|
85
|
+
/* v8 ignore start */
|
|
86
|
+
if (thumb) {
|
|
87
|
+
this.#activeAnimation?.cancel();
|
|
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 */
|
|
94
|
+
|
|
95
|
+
this.checked.set(next);
|
|
96
|
+
this.changed.emit(next);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UiTabs, UiTabsList, UiTabsTrigger, UiTabsContent } from './tabs';
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
|
|
3
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { setInputSignal } from '../../../test-utils/set-input-signal';
|
|
6
|
+
import { UiTabs, UiTabsContent, UiTabsList, UiTabsTrigger } from './tabs';
|
|
7
|
+
|
|
8
|
+
@Component({
|
|
9
|
+
standalone: true,
|
|
10
|
+
imports: [UiTabs, UiTabsList, UiTabsTrigger, UiTabsContent, LYNX_ELEMENTS],
|
|
11
|
+
template: `
|
|
12
|
+
<ui-tabs [(value)]="value">
|
|
13
|
+
<ui-tabs-list>
|
|
14
|
+
<ui-tabs-trigger value="a">A</ui-tabs-trigger>
|
|
15
|
+
<ui-tabs-trigger value="b">B</ui-tabs-trigger>
|
|
16
|
+
</ui-tabs-list>
|
|
17
|
+
<ui-tabs-content value="a"><text>Panel A</text></ui-tabs-content>
|
|
18
|
+
<ui-tabs-content value="b"><text>Panel B</text></ui-tabs-content>
|
|
19
|
+
</ui-tabs>
|
|
20
|
+
`,
|
|
21
|
+
})
|
|
22
|
+
class TabsHost {
|
|
23
|
+
value = 'a';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('UiTabs', () => {
|
|
27
|
+
it('renders the tab triggers and the active panel', async () => {
|
|
28
|
+
const { getByText } = await render(TabsHost);
|
|
29
|
+
expect(getByText('A')).toBeTruthy();
|
|
30
|
+
expect(getByText('B')).toBeTruthy();
|
|
31
|
+
expect(getByText('Panel A')).toBeTruthy();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('select() sets the model value and emits `changed`', async () => {
|
|
35
|
+
const { componentRef } = await render(UiTabs);
|
|
36
|
+
const inst = componentRef.instance as UiTabs;
|
|
37
|
+
const emitted: string[] = [];
|
|
38
|
+
inst.changed.subscribe((v) => emitted.push(v));
|
|
39
|
+
inst.select('x');
|
|
40
|
+
expect(inst.value()).toBe('x');
|
|
41
|
+
expect(emitted).toEqual(['x']);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('registerTab() records tab order without duplicates', async () => {
|
|
45
|
+
const { componentRef } = await render(UiTabs);
|
|
46
|
+
const inst = componentRef.instance as UiTabs;
|
|
47
|
+
inst.registerTab('a');
|
|
48
|
+
inst.registerTab('b');
|
|
49
|
+
inst.registerTab('a'); // duplicate, should be ignored
|
|
50
|
+
// Seed the effect with a known previousValue so subsequent transitions
|
|
51
|
+
// exercise the direction-set branch (not the initial-run early return).
|
|
52
|
+
inst.value.set('a');
|
|
53
|
+
await waitForUpdate();
|
|
54
|
+
// a=0, b=1 → nextIndex > prevIndex → direction=1 (forward branch)
|
|
55
|
+
inst.value.set('b');
|
|
56
|
+
await waitForUpdate();
|
|
57
|
+
await waitForUpdate();
|
|
58
|
+
expect(inst.direction()).toBe(1);
|
|
59
|
+
// Reverse → direction=-1 (backward branch)
|
|
60
|
+
inst.value.set('a');
|
|
61
|
+
await waitForUpdate();
|
|
62
|
+
await waitForUpdate();
|
|
63
|
+
expect(inst.direction()).toBe(-1);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('direction effect ignores unknown values', async () => {
|
|
67
|
+
// Covers the branch where prevIndex or nextIndex is -1.
|
|
68
|
+
const { componentRef } = await render(UiTabs);
|
|
69
|
+
const inst = componentRef.instance as UiTabs;
|
|
70
|
+
inst.value.set('unknown');
|
|
71
|
+
inst.value.set('also-unknown');
|
|
72
|
+
await waitForUpdate();
|
|
73
|
+
await waitForUpdate();
|
|
74
|
+
expect(inst.direction()).toBe(1); // unchanged from default
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('direction effect leaves direction alone when transitioning from unknown to known', async () => {
|
|
78
|
+
const { componentRef } = await render(UiTabs);
|
|
79
|
+
const inst = componentRef.instance as UiTabs;
|
|
80
|
+
inst.registerTab('a');
|
|
81
|
+
inst.registerTab('b');
|
|
82
|
+
inst.value.set('unknown');
|
|
83
|
+
await waitForUpdate();
|
|
84
|
+
inst.value.set('a');
|
|
85
|
+
await waitForUpdate();
|
|
86
|
+
expect(inst.direction()).toBe(1);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('direction effect leaves direction alone when transitioning from known to unknown', async () => {
|
|
90
|
+
const { componentRef } = await render(UiTabs);
|
|
91
|
+
const inst = componentRef.instance as UiTabs;
|
|
92
|
+
inst.registerTab('a');
|
|
93
|
+
inst.registerTab('b');
|
|
94
|
+
inst.value.set('a');
|
|
95
|
+
await waitForUpdate();
|
|
96
|
+
inst.value.set('unknown');
|
|
97
|
+
await waitForUpdate();
|
|
98
|
+
expect(inst.direction()).toBe(1);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('UiTabsList', () => {
|
|
103
|
+
it('renders projected content with the list container class', async () => {
|
|
104
|
+
const { container } = await render(UiTabsList);
|
|
105
|
+
const view = container.querySelector('view');
|
|
106
|
+
expect(view?.getAttribute('class')).toContain('rounded-lg');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('UiTabsTrigger', () => {
|
|
111
|
+
const renderTrigger = async (activeValue = 'a') => {
|
|
112
|
+
const tabsResult = await render(UiTabs);
|
|
113
|
+
const tabs = tabsResult.componentRef.instance as UiTabs;
|
|
114
|
+
tabs.value.set(activeValue);
|
|
115
|
+
|
|
116
|
+
const triggerResult = await render(UiTabsTrigger, {
|
|
117
|
+
providers: [{ provide: UiTabs, useValue: tabs }],
|
|
118
|
+
});
|
|
119
|
+
const trigger = triggerResult.componentRef.instance as UiTabsTrigger;
|
|
120
|
+
setInputSignal(trigger.triggerValue, 'a');
|
|
121
|
+
const view = triggerResult.container.querySelector('view') as Element;
|
|
122
|
+
(trigger as unknown as { triggerRef: unknown }).triggerRef = signal({
|
|
123
|
+
nativeElement: view,
|
|
124
|
+
});
|
|
125
|
+
await waitForUpdate();
|
|
126
|
+
await waitForUpdate();
|
|
127
|
+
return { tabs, trigger, triggerResult };
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
it('select() delegates to the parent tabs', async () => {
|
|
131
|
+
const { tabs, trigger } = await renderTrigger('a');
|
|
132
|
+
trigger.select();
|
|
133
|
+
expect(tabs.value()).toBe('a');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('effect runs the activation animation when active flips true', async () => {
|
|
137
|
+
// Active path — `#previousActive` starts undefined so the first flip
|
|
138
|
+
// exercises the initialization guard, and toggling activeValue drives the
|
|
139
|
+
// effect through both animate() branches.
|
|
140
|
+
const { tabs } = await renderTrigger('b');
|
|
141
|
+
// Switch tabs so trigger becomes active — activation branch.
|
|
142
|
+
tabs.value.set('a');
|
|
143
|
+
await waitForUpdate();
|
|
144
|
+
await waitForUpdate();
|
|
145
|
+
// Deactivation branch.
|
|
146
|
+
tabs.value.set('b');
|
|
147
|
+
await waitForUpdate();
|
|
148
|
+
await waitForUpdate();
|
|
149
|
+
expect(tabs.value()).toBe('b');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('UiTabsContent', () => {
|
|
154
|
+
it('runs the reveal + directional slide animations when active flips', async () => {
|
|
155
|
+
const tabsResult = await render(UiTabs);
|
|
156
|
+
const tabs = tabsResult.componentRef.instance as UiTabs;
|
|
157
|
+
tabs.registerTab('a');
|
|
158
|
+
tabs.registerTab('b');
|
|
159
|
+
|
|
160
|
+
const contentResult = await render(UiTabsContent, {
|
|
161
|
+
providers: [{ provide: UiTabs, useValue: tabs }],
|
|
162
|
+
});
|
|
163
|
+
const content = contentResult.componentRef.instance as UiTabsContent;
|
|
164
|
+
setInputSignal(content.contentValue, 'a');
|
|
165
|
+
const view = contentResult.container.querySelector('view') as Element;
|
|
166
|
+
(content as unknown as { contentRef: unknown }).contentRef = signal({
|
|
167
|
+
nativeElement: view,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Activate — first render path (revealIn).
|
|
171
|
+
tabs.value.set('a');
|
|
172
|
+
await waitForUpdate();
|
|
173
|
+
// The effect schedules setTimeout(_, 0); wait one more tick for it.
|
|
174
|
+
await waitForUpdate();
|
|
175
|
+
|
|
176
|
+
// Switch off and back on — subsequent path (directionalSlideIn) with cancel.
|
|
177
|
+
tabs.value.set('b');
|
|
178
|
+
await waitForUpdate();
|
|
179
|
+
tabs.value.set('a');
|
|
180
|
+
await waitForUpdate();
|
|
181
|
+
await waitForUpdate();
|
|
182
|
+
|
|
183
|
+
expect(tabs.value()).toBe('a');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('setTimeout body bails out when contentRef is undefined', async () => {
|
|
187
|
+
const tabsResult = await render(UiTabs);
|
|
188
|
+
const tabs = tabsResult.componentRef.instance as UiTabs;
|
|
189
|
+
const contentResult = await render(UiTabsContent, {
|
|
190
|
+
providers: [{ provide: UiTabs, useValue: tabs }],
|
|
191
|
+
});
|
|
192
|
+
const content = contentResult.componentRef.instance as UiTabsContent;
|
|
193
|
+
setInputSignal(content.contentValue, 'a');
|
|
194
|
+
tabs.value.set('a');
|
|
195
|
+
await waitForUpdate();
|
|
196
|
+
await waitForUpdate();
|
|
197
|
+
expect(tabs.value()).toBe('a');
|
|
198
|
+
});
|
|
199
|
+
});
|