@blotch/dolan 0.0.0-stage → 22.2.0
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,18 @@
|
|
|
1
|
+
export {
|
|
2
|
+
UiCartesianChart,
|
|
3
|
+
linearScale,
|
|
4
|
+
invertLinear,
|
|
5
|
+
niceNum,
|
|
6
|
+
niceScale,
|
|
7
|
+
generateTicks,
|
|
8
|
+
generateAlignedTicks,
|
|
9
|
+
isValueOutsideDomain,
|
|
10
|
+
gridlineOffset,
|
|
11
|
+
clampWindow,
|
|
12
|
+
panWindow,
|
|
13
|
+
zoomWindow,
|
|
14
|
+
type ChartPoint,
|
|
15
|
+
type ChartDomain,
|
|
16
|
+
type ChartScale,
|
|
17
|
+
type ChartPadding,
|
|
18
|
+
} from './cartesian-chart';
|
|
@@ -0,0 +1,59 @@
|
|
|
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 { UiCheckbox } from './checkbox';
|
|
5
|
+
|
|
6
|
+
describe('UiCheckbox', () => {
|
|
7
|
+
it('renders unchecked by default', async () => {
|
|
8
|
+
const { container } = await render(UiCheckbox);
|
|
9
|
+
expect(container).toBeTruthy();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('toggle() flips checked and emits the `changed` output', async () => {
|
|
13
|
+
const { componentRef } = await render(UiCheckbox);
|
|
14
|
+
const inst = componentRef.instance as UiCheckbox;
|
|
15
|
+
const changes: boolean[] = [];
|
|
16
|
+
inst.changed.subscribe((v) => changes.push(v));
|
|
17
|
+
|
|
18
|
+
inst.toggle();
|
|
19
|
+
expect(inst.checked()).toBe(true);
|
|
20
|
+
inst.toggle();
|
|
21
|
+
expect(inst.checked()).toBe(false);
|
|
22
|
+
expect(changes).toEqual([true, false]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('toggle() is a no-op when disabled is true', async () => {
|
|
26
|
+
const { componentRef } = await render(UiCheckbox);
|
|
27
|
+
const inst = componentRef.instance as UiCheckbox;
|
|
28
|
+
setInputSignal(inst.disabled, true);
|
|
29
|
+
const changed = vi.fn();
|
|
30
|
+
inst.changed.subscribe(changed);
|
|
31
|
+
|
|
32
|
+
inst.toggle();
|
|
33
|
+
expect(inst.checked()).toBe(false);
|
|
34
|
+
expect(changed).not.toHaveBeenCalled();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('applies the "checked" box class and reveals the check mark opacity', async () => {
|
|
38
|
+
const { componentRef, container } = await render(UiCheckbox);
|
|
39
|
+
const inst = componentRef.instance as UiCheckbox;
|
|
40
|
+
inst.checked.set(true);
|
|
41
|
+
await waitForUpdate();
|
|
42
|
+
|
|
43
|
+
// The inner check-glyph is opacity: 1 when checked (opacity: 0 otherwise).
|
|
44
|
+
const check = container.querySelector('text');
|
|
45
|
+
expect(check?.getAttribute('style')).toContain('opacity: 1');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('applies the disabled dim when disabled input is set', async () => {
|
|
49
|
+
const { componentRef, container } = await render(UiCheckbox);
|
|
50
|
+
setInputSignal((componentRef.instance as UiCheckbox).disabled, true);
|
|
51
|
+
await waitForUpdate();
|
|
52
|
+
// opacity-50 class lands on the box wrapper's class attribute.
|
|
53
|
+
const boxes = container.querySelectorAll('view');
|
|
54
|
+
const hasDim = Array.from(boxes).some((el) =>
|
|
55
|
+
el.getAttribute('class')?.includes('opacity-50'),
|
|
56
|
+
);
|
|
57
|
+
expect(hasDim).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ElementRef,
|
|
3
|
+
Component,
|
|
4
|
+
ViewEncapsulation,
|
|
5
|
+
computed,
|
|
6
|
+
effect,
|
|
7
|
+
input,
|
|
8
|
+
model,
|
|
9
|
+
output,
|
|
10
|
+
viewChild,
|
|
11
|
+
} from '@angular/core';
|
|
12
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
13
|
+
|
|
14
|
+
import { type AnimationHandle, popIn, popOut } from '../../utils/animate';
|
|
15
|
+
import { cn } from '../../utils/cn';
|
|
16
|
+
|
|
17
|
+
@Component({
|
|
18
|
+
selector: 'ui-checkbox',
|
|
19
|
+
standalone: true,
|
|
20
|
+
imports: [LYNX_ELEMENTS],
|
|
21
|
+
encapsulation: ViewEncapsulation.None,
|
|
22
|
+
template: `
|
|
23
|
+
<view
|
|
24
|
+
class="h-11 w-11 items-center flex justify-center"
|
|
25
|
+
(bindtap)="toggle()"
|
|
26
|
+
>
|
|
27
|
+
<view [class]="boxClass()">
|
|
28
|
+
<text
|
|
29
|
+
#checkmark
|
|
30
|
+
[class]="checkClass()"
|
|
31
|
+
[style]="checked() ? 'opacity: 1;' : 'opacity: 0;'"
|
|
32
|
+
>
|
|
33
|
+
✓
|
|
34
|
+
</text>
|
|
35
|
+
</view>
|
|
36
|
+
</view>
|
|
37
|
+
`,
|
|
38
|
+
})
|
|
39
|
+
export class UiCheckbox {
|
|
40
|
+
readonly checked = model(false);
|
|
41
|
+
readonly disabled = input(false);
|
|
42
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
43
|
+
|
|
44
|
+
readonly changed = output<boolean>();
|
|
45
|
+
|
|
46
|
+
readonly checkmarkRef = viewChild<ElementRef>('checkmark');
|
|
47
|
+
#checkAnim?: AnimationHandle;
|
|
48
|
+
#previousChecked?: boolean;
|
|
49
|
+
|
|
50
|
+
constructor() {
|
|
51
|
+
// Animate the checkmark on state transitions (skip initial render).
|
|
52
|
+
// `#previousChecked === undefined` guards the first run — we don't want to
|
|
53
|
+
// pop-in the checkmark when the component mounts already-checked.
|
|
54
|
+
// The `isChecked === this.#previousChecked` guard prevents a second animation
|
|
55
|
+
// from firing if the effect re-runs without an actual state change.
|
|
56
|
+
effect(() => {
|
|
57
|
+
const isChecked = this.checked();
|
|
58
|
+
const el = this.checkmarkRef()?.nativeElement;
|
|
59
|
+
// popIn/popOut call el.animate() which is inert in jsdom — the
|
|
60
|
+
// check-mark animation is exercised on-device.
|
|
61
|
+
/* v8 ignore start */
|
|
62
|
+
if (!el || this.#previousChecked === undefined) {
|
|
63
|
+
this.#previousChecked = isChecked;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (isChecked === this.#previousChecked) return;
|
|
67
|
+
this.#previousChecked = isChecked;
|
|
68
|
+
|
|
69
|
+
this.#checkAnim?.cancel();
|
|
70
|
+
this.#checkAnim = isChecked ? popIn(el) : popOut(el);
|
|
71
|
+
/* v8 ignore stop */
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
protected readonly boxClass = computed(() =>
|
|
76
|
+
cn(
|
|
77
|
+
'h-5 w-5 items-center rounded-sm border flex justify-center',
|
|
78
|
+
this.checked()
|
|
79
|
+
? 'border-primary bg-primary'
|
|
80
|
+
: 'border-primary bg-transparent',
|
|
81
|
+
this.disabled() && 'opacity-50',
|
|
82
|
+
this.userClass(),
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
protected readonly checkClass = computed(() =>
|
|
87
|
+
cn('text-[10px] font-bold text-primary-foreground'),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
toggle(): void {
|
|
91
|
+
if (this.disabled()) return;
|
|
92
|
+
const next = !this.checked();
|
|
93
|
+
this.checked.set(next);
|
|
94
|
+
this.changed.emit(next);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UiCheckbox } from './checkbox';
|
|
@@ -0,0 +1,112 @@
|
|
|
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 {
|
|
7
|
+
UiCollapsible,
|
|
8
|
+
UiCollapsibleContent,
|
|
9
|
+
UiCollapsibleTrigger,
|
|
10
|
+
} from './collapsible';
|
|
11
|
+
|
|
12
|
+
@Component({
|
|
13
|
+
standalone: true,
|
|
14
|
+
imports: [UiCollapsible, LYNX_ELEMENTS],
|
|
15
|
+
template: `<ui-collapsible><text>Content</text></ui-collapsible>`,
|
|
16
|
+
})
|
|
17
|
+
class CollapsibleHost {}
|
|
18
|
+
|
|
19
|
+
describe('UiCollapsible', () => {
|
|
20
|
+
it('renders projected content', async () => {
|
|
21
|
+
const { getByText } = await render(CollapsibleHost);
|
|
22
|
+
expect(getByText('Content')).toBeTruthy();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('toggle() flips the open state', async () => {
|
|
26
|
+
const { componentRef } = await render(UiCollapsible);
|
|
27
|
+
const inst = componentRef.instance as UiCollapsible;
|
|
28
|
+
expect(inst.open()).toBe(false);
|
|
29
|
+
inst.toggle();
|
|
30
|
+
expect(inst.open()).toBe(true);
|
|
31
|
+
inst.toggle();
|
|
32
|
+
expect(inst.open()).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('toggle() is a no-op when disabled', async () => {
|
|
36
|
+
const { componentRef } = await render(UiCollapsible);
|
|
37
|
+
const inst = componentRef.instance as UiCollapsible;
|
|
38
|
+
setInputSignal(inst.disabled, true);
|
|
39
|
+
inst.toggle();
|
|
40
|
+
expect(inst.open()).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('UiCollapsibleTrigger', () => {
|
|
45
|
+
it('onTap delegates to the parent collapsible.toggle()', async () => {
|
|
46
|
+
const parentResult = await render(UiCollapsible);
|
|
47
|
+
const parent = parentResult.componentRef.instance as UiCollapsible;
|
|
48
|
+
|
|
49
|
+
const triggerResult = await render(UiCollapsibleTrigger, {
|
|
50
|
+
providers: [{ provide: UiCollapsible, useValue: parent }],
|
|
51
|
+
});
|
|
52
|
+
const trigger = triggerResult.componentRef.instance as UiCollapsibleTrigger;
|
|
53
|
+
(trigger as unknown as { onTap: () => void }).onTap();
|
|
54
|
+
expect(parent.open()).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('applies disabled dim class when parent is disabled', async () => {
|
|
58
|
+
const parentResult = await render(UiCollapsible);
|
|
59
|
+
const parent = parentResult.componentRef.instance as UiCollapsible;
|
|
60
|
+
setInputSignal(parent.disabled, true);
|
|
61
|
+
|
|
62
|
+
const triggerResult = await render(UiCollapsibleTrigger, {
|
|
63
|
+
providers: [{ provide: UiCollapsible, useValue: parent }],
|
|
64
|
+
});
|
|
65
|
+
await waitForUpdate();
|
|
66
|
+
const view = triggerResult.container.querySelector('view');
|
|
67
|
+
expect(view?.getAttribute('class')).toContain('opacity-50');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('UiCollapsibleContent', () => {
|
|
72
|
+
it('runs the reveal animation on open via setTimeout', async () => {
|
|
73
|
+
const parentResult = await render(UiCollapsible);
|
|
74
|
+
const parent = parentResult.componentRef.instance as UiCollapsible;
|
|
75
|
+
|
|
76
|
+
const contentResult = await render(UiCollapsibleContent, {
|
|
77
|
+
providers: [{ provide: UiCollapsible, useValue: parent }],
|
|
78
|
+
});
|
|
79
|
+
const content = contentResult.componentRef.instance as UiCollapsibleContent;
|
|
80
|
+
// JIT + jsdom doesn't populate the `#content` viewChild — patch it with a
|
|
81
|
+
// signal returning the outer view so the setTimeout body's `el` check
|
|
82
|
+
// passes and revealIn actually runs.
|
|
83
|
+
const view = contentResult.container.querySelector('view') as Element;
|
|
84
|
+
(content as unknown as { contentRef: unknown }).contentRef = signal({
|
|
85
|
+
nativeElement: view,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
parent.open.set(true);
|
|
89
|
+
await waitForUpdate();
|
|
90
|
+
await waitForUpdate(); // wait for the effect's setTimeout(_, 0) to fire
|
|
91
|
+
|
|
92
|
+
// Toggle off then on again to hit the cancel path with `#anim` already set.
|
|
93
|
+
parent.open.set(false);
|
|
94
|
+
await waitForUpdate();
|
|
95
|
+
parent.open.set(true);
|
|
96
|
+
await waitForUpdate();
|
|
97
|
+
await waitForUpdate();
|
|
98
|
+
expect(parent.open()).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('bails out inside setTimeout when contentRef is undefined', async () => {
|
|
102
|
+
const parentResult = await render(UiCollapsible);
|
|
103
|
+
const parent = parentResult.componentRef.instance as UiCollapsible;
|
|
104
|
+
await render(UiCollapsibleContent, {
|
|
105
|
+
providers: [{ provide: UiCollapsible, useValue: parent }],
|
|
106
|
+
});
|
|
107
|
+
parent.open.set(true);
|
|
108
|
+
await waitForUpdate();
|
|
109
|
+
await waitForUpdate();
|
|
110
|
+
expect(parent.open()).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ElementRef,
|
|
3
|
+
Component,
|
|
4
|
+
ViewEncapsulation,
|
|
5
|
+
computed,
|
|
6
|
+
effect,
|
|
7
|
+
inject,
|
|
8
|
+
input,
|
|
9
|
+
model,
|
|
10
|
+
viewChild,
|
|
11
|
+
} from '@angular/core';
|
|
12
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
13
|
+
|
|
14
|
+
import { type AnimationHandle, revealIn } from '../../utils/animate';
|
|
15
|
+
import { cn } from '../../utils/cn';
|
|
16
|
+
|
|
17
|
+
@Component({
|
|
18
|
+
selector: 'ui-collapsible',
|
|
19
|
+
standalone: true,
|
|
20
|
+
imports: [LYNX_ELEMENTS],
|
|
21
|
+
encapsulation: ViewEncapsulation.None,
|
|
22
|
+
template: `
|
|
23
|
+
<view [class]="containerClass()">
|
|
24
|
+
<ng-content />
|
|
25
|
+
</view>
|
|
26
|
+
`,
|
|
27
|
+
})
|
|
28
|
+
export class UiCollapsible {
|
|
29
|
+
readonly open = model(false);
|
|
30
|
+
readonly disabled = input(false);
|
|
31
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
32
|
+
|
|
33
|
+
protected readonly containerClass = computed(() =>
|
|
34
|
+
cn('flex-col flex', this.userClass()),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
toggle(): void {
|
|
38
|
+
if (!this.disabled()) {
|
|
39
|
+
this.open.update((v) => !v);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Component({
|
|
45
|
+
selector: 'ui-collapsible-trigger',
|
|
46
|
+
standalone: true,
|
|
47
|
+
imports: [LYNX_ELEMENTS],
|
|
48
|
+
encapsulation: ViewEncapsulation.None,
|
|
49
|
+
template: `
|
|
50
|
+
<view [class]="triggerClass()" (bindtap)="onTap()">
|
|
51
|
+
<ng-content />
|
|
52
|
+
</view>
|
|
53
|
+
`,
|
|
54
|
+
})
|
|
55
|
+
export class UiCollapsibleTrigger {
|
|
56
|
+
readonly #collapsible = inject(UiCollapsible);
|
|
57
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
58
|
+
|
|
59
|
+
protected readonly triggerClass = computed(() =>
|
|
60
|
+
cn(
|
|
61
|
+
'flex-row items-center flex',
|
|
62
|
+
this.#collapsible.disabled() && 'opacity-50',
|
|
63
|
+
this.userClass(),
|
|
64
|
+
),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
protected onTap(): void {
|
|
68
|
+
this.#collapsible.toggle();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@Component({
|
|
73
|
+
selector: 'ui-collapsible-content',
|
|
74
|
+
standalone: true,
|
|
75
|
+
imports: [LYNX_ELEMENTS],
|
|
76
|
+
encapsulation: ViewEncapsulation.None,
|
|
77
|
+
// Mount the content only while open, via `@if`. On Lynx this works because the
|
|
78
|
+
// renderer recreates a destroyed element's native painting node when it is
|
|
79
|
+
// re-inserted (see LynxElement.#recreateSubtree in @blotch/angular-lynx): when
|
|
80
|
+
// the panel reopens, the projected `<ng-content>` — recycled with the re-created
|
|
81
|
+
// wrapper — is rebuilt from cache and renders again on every open. The inline
|
|
82
|
+
// opacity/transform is the resting state; revealIn animates from opacity:0 /
|
|
83
|
+
// translateY(-8px) with fill:'none' so the inline style takes over once the
|
|
84
|
+
// animation ends.
|
|
85
|
+
template: `
|
|
86
|
+
@if (collapsible.open()) {
|
|
87
|
+
<view
|
|
88
|
+
#content
|
|
89
|
+
[class]="contentClass()"
|
|
90
|
+
style="opacity: 1; transform: translateY(0);"
|
|
91
|
+
>
|
|
92
|
+
<ng-content />
|
|
93
|
+
</view>
|
|
94
|
+
}
|
|
95
|
+
`,
|
|
96
|
+
})
|
|
97
|
+
export class UiCollapsibleContent {
|
|
98
|
+
protected readonly collapsible = inject(UiCollapsible);
|
|
99
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
100
|
+
|
|
101
|
+
readonly contentRef = viewChild<ElementRef>('content');
|
|
102
|
+
#anim?: AnimationHandle;
|
|
103
|
+
|
|
104
|
+
constructor() {
|
|
105
|
+
// Animate the reveal whenever the panel opens. contentRef only resolves while
|
|
106
|
+
// the `@if` content is mounted; defer one macrotask so the freshly (re)created
|
|
107
|
+
// element is committed to the native tree before we animate it.
|
|
108
|
+
effect(() => {
|
|
109
|
+
if (!this.collapsible.open()) return;
|
|
110
|
+
setTimeout(() => {
|
|
111
|
+
const el = this.contentRef()?.nativeElement;
|
|
112
|
+
if (!el) return;
|
|
113
|
+
this.#anim?.cancel();
|
|
114
|
+
this.#anim = revealIn(el, { fromY: -8, fill: 'none' });
|
|
115
|
+
}, 0);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
protected readonly contentClass = computed(() =>
|
|
120
|
+
cn('flex-col flex', this.userClass()),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
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 {
|
|
7
|
+
UiDialog,
|
|
8
|
+
UiDialogDescription,
|
|
9
|
+
UiDialogFooter,
|
|
10
|
+
UiDialogHeader,
|
|
11
|
+
UiDialogTitle,
|
|
12
|
+
} from './dialog';
|
|
13
|
+
|
|
14
|
+
@Component({
|
|
15
|
+
standalone: true,
|
|
16
|
+
imports: [
|
|
17
|
+
UiDialog,
|
|
18
|
+
UiDialogHeader,
|
|
19
|
+
UiDialogTitle,
|
|
20
|
+
UiDialogDescription,
|
|
21
|
+
UiDialogFooter,
|
|
22
|
+
LYNX_ELEMENTS,
|
|
23
|
+
],
|
|
24
|
+
template: `
|
|
25
|
+
<ui-dialog [(open)]="open">
|
|
26
|
+
<ui-dialog-header>
|
|
27
|
+
<ui-dialog-title>Title</ui-dialog-title>
|
|
28
|
+
<ui-dialog-description>Description</ui-dialog-description>
|
|
29
|
+
</ui-dialog-header>
|
|
30
|
+
<ui-dialog-footer><text>Actions</text></ui-dialog-footer>
|
|
31
|
+
</ui-dialog>
|
|
32
|
+
`,
|
|
33
|
+
})
|
|
34
|
+
class DialogHost {
|
|
35
|
+
open = true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('UiDialog', () => {
|
|
39
|
+
it('renders header, title, description, and footer', async () => {
|
|
40
|
+
const { getByText } = await render(DialogHost);
|
|
41
|
+
expect(getByText('Title')).toBeTruthy();
|
|
42
|
+
expect(getByText('Description')).toBeTruthy();
|
|
43
|
+
expect(getByText('Actions')).toBeTruthy();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('backdrop tap closes the dialog', async () => {
|
|
47
|
+
const { componentRef } = await render(UiDialog);
|
|
48
|
+
const inst = componentRef.instance as UiDialog;
|
|
49
|
+
inst.open.set(true);
|
|
50
|
+
await waitForUpdate();
|
|
51
|
+
(inst as unknown as { onBackdropTap: () => void }).onBackdropTap();
|
|
52
|
+
expect(inst.open()).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('panel tap is a no-op (catchtap stops bubbling)', async () => {
|
|
56
|
+
const { componentRef } = await render(UiDialog);
|
|
57
|
+
const inst = componentRef.instance as UiDialog;
|
|
58
|
+
expect(() =>
|
|
59
|
+
(inst as unknown as { onPanelTap: () => void }).onPanelTap(),
|
|
60
|
+
).not.toThrow();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('open effect: showing then hiding runs the animate-in and animate-out lifecycles', async () => {
|
|
64
|
+
// Monkey-patch backdrop/panel refs so #animateIn / #animateOut execute in
|
|
65
|
+
// full instead of bailing on !backdrop || !panel.
|
|
66
|
+
const { componentRef, container } = await render(UiDialog);
|
|
67
|
+
const inst = componentRef.instance as UiDialog;
|
|
68
|
+
const view = container.querySelector('view') as Element;
|
|
69
|
+
(inst as unknown as { backdropRef: unknown }).backdropRef = signal({
|
|
70
|
+
nativeElement: view,
|
|
71
|
+
});
|
|
72
|
+
(inst as unknown as { panelRef: unknown }).panelRef = signal({
|
|
73
|
+
nativeElement: view,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
inst.open.set(true);
|
|
77
|
+
await waitForUpdate();
|
|
78
|
+
// Wait for setTimeout chains to flush (nested setTimeouts inside #doOpen).
|
|
79
|
+
await waitForUpdate();
|
|
80
|
+
await waitForUpdate();
|
|
81
|
+
|
|
82
|
+
// Now close it — exercises #doClose + #animateOut. The nested setTimeout
|
|
83
|
+
// inside #doClose runs DURATION.normal (250ms) + 20ms later; wait real time.
|
|
84
|
+
inst.open.set(false);
|
|
85
|
+
await new Promise((r) => setTimeout(r, 320));
|
|
86
|
+
await waitForUpdate();
|
|
87
|
+
expect(inst.open()).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('open effect bails when backdrop/panel refs are undefined', async () => {
|
|
91
|
+
// Covers the early-return branches in #animateIn / #animateOut.
|
|
92
|
+
const { componentRef } = await render(UiDialog);
|
|
93
|
+
const inst = componentRef.instance as UiDialog;
|
|
94
|
+
inst.open.set(true);
|
|
95
|
+
await waitForUpdate();
|
|
96
|
+
await waitForUpdate();
|
|
97
|
+
inst.open.set(false);
|
|
98
|
+
await waitForUpdate();
|
|
99
|
+
expect(inst.open()).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('overlayStyle switches between visible and hidden variants', async () => {
|
|
103
|
+
const { componentRef, container } = await render(UiDialog);
|
|
104
|
+
const inst = componentRef.instance as UiDialog;
|
|
105
|
+
// Default hidden — overlay carries display:none.
|
|
106
|
+
let overlay = container.querySelector('overlay');
|
|
107
|
+
expect(overlay?.getAttribute('style')).toContain('display: none');
|
|
108
|
+
|
|
109
|
+
// Open + advance so overlayVisible flips true.
|
|
110
|
+
const view = container.querySelector('view') as Element;
|
|
111
|
+
(inst as unknown as { backdropRef: unknown }).backdropRef = signal({
|
|
112
|
+
nativeElement: view,
|
|
113
|
+
});
|
|
114
|
+
(inst as unknown as { panelRef: unknown }).panelRef = signal({
|
|
115
|
+
nativeElement: view,
|
|
116
|
+
});
|
|
117
|
+
inst.open.set(true);
|
|
118
|
+
for (let i = 0; i < 10; i++) await waitForUpdate();
|
|
119
|
+
overlay = container.querySelector('overlay');
|
|
120
|
+
expect(overlay?.getAttribute('style')).not.toContain('display: none');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('accepts a user class on the panel', async () => {
|
|
124
|
+
const { componentRef, container } = await render(UiDialog);
|
|
125
|
+
const inst = componentRef.instance as UiDialog;
|
|
126
|
+
setInputSignal(inst.userClass, 'custom-panel');
|
|
127
|
+
await waitForUpdate();
|
|
128
|
+
const panelCandidates = Array.from(container.querySelectorAll('view'));
|
|
129
|
+
expect(
|
|
130
|
+
panelCandidates.some((el) =>
|
|
131
|
+
el.getAttribute('class')?.includes('custom-panel'),
|
|
132
|
+
),
|
|
133
|
+
).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('Dialog sub-components', () => {
|
|
138
|
+
it('UiDialogHeader renders projected content', async () => {
|
|
139
|
+
const { container } = await render(UiDialogHeader);
|
|
140
|
+
expect(container.querySelector('view')).toBeTruthy();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('UiDialogTitle renders projected content', async () => {
|
|
144
|
+
const { container } = await render(UiDialogTitle);
|
|
145
|
+
expect(container.querySelector('text')).toBeTruthy();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('UiDialogDescription renders projected content', async () => {
|
|
149
|
+
const { container } = await render(UiDialogDescription);
|
|
150
|
+
expect(container.querySelector('text')).toBeTruthy();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('UiDialogFooter renders projected content', async () => {
|
|
154
|
+
const { container } = await render(UiDialogFooter);
|
|
155
|
+
expect(container.querySelector('view')).toBeTruthy();
|
|
156
|
+
});
|
|
157
|
+
});
|