@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,107 @@
|
|
|
1
|
+
import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { setInputSignal } from '../../../test-utils/set-input-signal';
|
|
4
|
+
import { UiSelect, UiSelectItem } from './select';
|
|
5
|
+
|
|
6
|
+
describe('UiSelect', () => {
|
|
7
|
+
it('renders the trigger for the current selection', async () => {
|
|
8
|
+
const { container } = await render(UiSelect);
|
|
9
|
+
expect(container).toBeTruthy();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('displays the placeholder when no value is selected', async () => {
|
|
13
|
+
const { componentRef, container } = await render(UiSelect);
|
|
14
|
+
const inst = componentRef.instance as UiSelect;
|
|
15
|
+
setInputSignal(inst.placeholder, 'Choose...');
|
|
16
|
+
await waitForUpdate();
|
|
17
|
+
expect(container.textContent).toContain('Choose...');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('displays the raw value when no matching item is projected', async () => {
|
|
21
|
+
// Covers the `item?.label() ?? val` fallback path.
|
|
22
|
+
const { componentRef, container } = await render(UiSelect);
|
|
23
|
+
const inst = componentRef.instance as UiSelect;
|
|
24
|
+
inst.value.set('unknown');
|
|
25
|
+
await waitForUpdate();
|
|
26
|
+
expect(container.textContent).toContain('unknown');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('select() updates the model, emits `changed`, and closes the sheet', async () => {
|
|
30
|
+
const { componentRef } = await render(UiSelect);
|
|
31
|
+
const inst = componentRef.instance as UiSelect;
|
|
32
|
+
inst.open();
|
|
33
|
+
const emitted: string[] = [];
|
|
34
|
+
inst.changed.subscribe((v) => emitted.push(v));
|
|
35
|
+
inst.select('b');
|
|
36
|
+
expect(inst.value()).toBe('b');
|
|
37
|
+
expect(emitted).toEqual(['b']);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('toggle() flips the sheet open/closed', async () => {
|
|
41
|
+
const { componentRef } = await render(UiSelect);
|
|
42
|
+
const inst = componentRef.instance as UiSelect;
|
|
43
|
+
inst.toggle();
|
|
44
|
+
inst.toggle();
|
|
45
|
+
// No throw; the internal `sheetOpen` signal is protected — verify indirectly
|
|
46
|
+
// by ensuring subsequent open()/close() calls still work.
|
|
47
|
+
inst.open();
|
|
48
|
+
inst.close();
|
|
49
|
+
expect(inst.value()).toBe('');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('toggle() / open() are no-ops when disabled', async () => {
|
|
53
|
+
const { componentRef } = await render(UiSelect);
|
|
54
|
+
const inst = componentRef.instance as UiSelect;
|
|
55
|
+
setInputSignal(inst.disabled, true);
|
|
56
|
+
await waitForUpdate();
|
|
57
|
+
// No throw and no exception paths — early-return branch is exercised.
|
|
58
|
+
inst.toggle();
|
|
59
|
+
inst.open();
|
|
60
|
+
inst.close();
|
|
61
|
+
expect(inst.value()).toBe('');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('trigger dims when the select is disabled', async () => {
|
|
65
|
+
const { componentRef, container } = await render(UiSelect);
|
|
66
|
+
const inst = componentRef.instance as UiSelect;
|
|
67
|
+
setInputSignal(inst.disabled, true);
|
|
68
|
+
await waitForUpdate();
|
|
69
|
+
const trigger = container.querySelector('view');
|
|
70
|
+
expect(trigger?.getAttribute('class')).toContain('opacity-50');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('UiSelectItem', () => {
|
|
75
|
+
it('onTap delegates to the parent select', async () => {
|
|
76
|
+
const parentResult = await render(UiSelect);
|
|
77
|
+
const parent = parentResult.componentRef.instance as UiSelect;
|
|
78
|
+
|
|
79
|
+
const itemResult = await render(UiSelectItem, {
|
|
80
|
+
providers: [{ provide: UiSelect, useValue: parent }],
|
|
81
|
+
});
|
|
82
|
+
const item = itemResult.componentRef.instance as UiSelectItem;
|
|
83
|
+
setInputSignal(item.itemValue, 'x');
|
|
84
|
+
setInputSignal(item.label, 'X');
|
|
85
|
+
await waitForUpdate();
|
|
86
|
+
|
|
87
|
+
(item as unknown as { onTap: () => void }).onTap();
|
|
88
|
+
expect(parent.value()).toBe('x');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('highlights the selected item via bg-accent', async () => {
|
|
92
|
+
const parentResult = await render(UiSelect);
|
|
93
|
+
const parent = parentResult.componentRef.instance as UiSelect;
|
|
94
|
+
parent.value.set('x');
|
|
95
|
+
|
|
96
|
+
const itemResult = await render(UiSelectItem, {
|
|
97
|
+
providers: [{ provide: UiSelect, useValue: parent }],
|
|
98
|
+
});
|
|
99
|
+
const item = itemResult.componentRef.instance as UiSelectItem;
|
|
100
|
+
setInputSignal(item.itemValue, 'x');
|
|
101
|
+
setInputSignal(item.label, 'X');
|
|
102
|
+
await waitForUpdate();
|
|
103
|
+
|
|
104
|
+
const view = itemResult.container.querySelector('view');
|
|
105
|
+
expect(view?.getAttribute('class')).toContain('bg-accent');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
ViewEncapsulation,
|
|
4
|
+
computed,
|
|
5
|
+
contentChildren,
|
|
6
|
+
forwardRef,
|
|
7
|
+
inject,
|
|
8
|
+
input,
|
|
9
|
+
model,
|
|
10
|
+
output,
|
|
11
|
+
signal,
|
|
12
|
+
} from '@angular/core';
|
|
13
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
14
|
+
|
|
15
|
+
import { UiBottomSheet } from '../bottom-sheet/bottom-sheet';
|
|
16
|
+
import { cn } from '../../utils/cn';
|
|
17
|
+
|
|
18
|
+
const CHEVRON_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="m6 9 6 6 6-6"/></svg>`;
|
|
19
|
+
const CHECK_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="M20 6 9 17l-5-5"/></svg>`;
|
|
20
|
+
|
|
21
|
+
@Component({
|
|
22
|
+
selector: 'ui-select',
|
|
23
|
+
standalone: true,
|
|
24
|
+
imports: [LYNX_ELEMENTS, UiBottomSheet],
|
|
25
|
+
encapsulation: ViewEncapsulation.None,
|
|
26
|
+
template: `
|
|
27
|
+
<view [class]="triggerClass()" (bindtap)="toggle()">
|
|
28
|
+
<text [class]="valueTextClass()">{{ displayText() }}</text>
|
|
29
|
+
<svg [attr.content]="chevronSvg" style="width: 16px; height: 16px;" />
|
|
30
|
+
</view>
|
|
31
|
+
<ui-bottom-sheet [(open)]="sheetOpen">
|
|
32
|
+
<scroll-view scroll-orientation="vertical" style="max-height: 300px;">
|
|
33
|
+
<view class="flex-col pb-4 flex">
|
|
34
|
+
<ng-content />
|
|
35
|
+
</view>
|
|
36
|
+
</scroll-view>
|
|
37
|
+
</ui-bottom-sheet>
|
|
38
|
+
`,
|
|
39
|
+
})
|
|
40
|
+
export class UiSelect {
|
|
41
|
+
readonly value = model<string>('');
|
|
42
|
+
readonly placeholder = input('Select...');
|
|
43
|
+
readonly disabled = input(false);
|
|
44
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
45
|
+
|
|
46
|
+
readonly changed = output<string>();
|
|
47
|
+
|
|
48
|
+
protected readonly chevronSvg = CHEVRON_SVG;
|
|
49
|
+
|
|
50
|
+
// Drives the shared bottom-sheet's `open` model via `[(open)]`. All the
|
|
51
|
+
// overlay/animation/drag behavior lives in ui-bottom-sheet; select only
|
|
52
|
+
// decides when the sheet is open.
|
|
53
|
+
protected readonly sheetOpen = signal(false);
|
|
54
|
+
|
|
55
|
+
// `forwardRef` is required because UiSelectItem is defined later in this
|
|
56
|
+
// file — without it, the reference would be `undefined` at class-definition
|
|
57
|
+
// time and contentChildren would silently query nothing.
|
|
58
|
+
/* v8 ignore next 1 -- forwardRef lambda; JIT+jsdom doesn't wire required child inputs */
|
|
59
|
+
readonly itemRefs = contentChildren(forwardRef(() => UiSelectItem));
|
|
60
|
+
|
|
61
|
+
protected readonly displayText = computed(() => {
|
|
62
|
+
const val = this.value();
|
|
63
|
+
if (!val) return this.placeholder();
|
|
64
|
+
/* v8 ignore next 1 -- .find predicate: unreachable when no content-projected items */
|
|
65
|
+
const item = this.itemRefs().find((i) => i.itemValue() === val);
|
|
66
|
+
return item?.label() ?? val;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
protected readonly triggerClass = computed(() =>
|
|
70
|
+
cn(
|
|
71
|
+
'h-10 flex-row items-center rounded-md border border-input bg-background px-3 py-2 flex justify-between',
|
|
72
|
+
this.disabled() && 'opacity-50',
|
|
73
|
+
this.userClass(),
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
protected readonly valueTextClass = computed(() => {
|
|
78
|
+
const val = this.value();
|
|
79
|
+
return cn(
|
|
80
|
+
'flex-1 text-sm',
|
|
81
|
+
val ? 'text-foreground' : 'text-muted-foreground',
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
select(value: string): void {
|
|
86
|
+
this.value.set(value);
|
|
87
|
+
this.changed.emit(value);
|
|
88
|
+
this.close();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
toggle(): void {
|
|
92
|
+
if (this.disabled()) return;
|
|
93
|
+
this.sheetOpen.update((v) => !v);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
open(): void {
|
|
97
|
+
if (this.disabled()) return;
|
|
98
|
+
this.sheetOpen.set(true);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
close(): void {
|
|
102
|
+
this.sheetOpen.set(false);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@Component({
|
|
107
|
+
selector: 'ui-select-item',
|
|
108
|
+
standalone: true,
|
|
109
|
+
imports: [LYNX_ELEMENTS],
|
|
110
|
+
encapsulation: ViewEncapsulation.None,
|
|
111
|
+
template: `
|
|
112
|
+
<view [class]="containerClass()" (bindtap)="onTap()">
|
|
113
|
+
<text [class]="labelClass()">{{ label() }}</text>
|
|
114
|
+
@if (isSelected()) {
|
|
115
|
+
<svg [attr.content]="checkSvg" style="width: 16px; height: 16px;" />
|
|
116
|
+
}
|
|
117
|
+
</view>
|
|
118
|
+
`,
|
|
119
|
+
})
|
|
120
|
+
export class UiSelectItem {
|
|
121
|
+
readonly #select = inject(UiSelect);
|
|
122
|
+
|
|
123
|
+
readonly itemValue = input.required<string>({ alias: 'value' });
|
|
124
|
+
readonly label = input.required<string>();
|
|
125
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
126
|
+
|
|
127
|
+
protected readonly checkSvg = CHECK_SVG;
|
|
128
|
+
|
|
129
|
+
protected readonly isSelected = computed(
|
|
130
|
+
() => this.#select.value() === this.itemValue(),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
protected readonly containerClass = computed(() =>
|
|
134
|
+
cn(
|
|
135
|
+
'flex-row items-center px-4 py-3 flex justify-between',
|
|
136
|
+
this.isSelected() && 'bg-accent',
|
|
137
|
+
this.userClass(),
|
|
138
|
+
),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
protected readonly labelClass = computed(() => cn('text-sm text-foreground'));
|
|
142
|
+
|
|
143
|
+
protected onTap(): void {
|
|
144
|
+
this.#select.select(this.itemValue());
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UiSeparator } from './separator';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { setInputSignal } from '../../../test-utils/set-input-signal';
|
|
4
|
+
import { UiSeparator } from './separator';
|
|
5
|
+
|
|
6
|
+
describe('UiSeparator', () => {
|
|
7
|
+
it('renders horizontally (default)', async () => {
|
|
8
|
+
const { componentRef } = await render(UiSeparator);
|
|
9
|
+
// The class binding lands on the host element, which is the component's
|
|
10
|
+
// location.nativeElement (a real DOM node representing the ui-separator
|
|
11
|
+
// host). Read the class directly from that node.
|
|
12
|
+
const host = componentRef.location.nativeElement as HTMLElement;
|
|
13
|
+
expect(host.getAttribute('class')).toContain('h-px');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('renders vertically when the orientation input is flipped', async () => {
|
|
17
|
+
const { componentRef } = await render(UiSeparator);
|
|
18
|
+
setInputSignal(
|
|
19
|
+
(componentRef.instance as UiSeparator).orientation,
|
|
20
|
+
'vertical',
|
|
21
|
+
);
|
|
22
|
+
await waitForUpdate();
|
|
23
|
+
const host = componentRef.location.nativeElement as HTMLElement;
|
|
24
|
+
expect(host.getAttribute('class')).toContain('w-px');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Component, ViewEncapsulation, computed, input } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../utils/cn';
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
selector: 'ui-separator',
|
|
7
|
+
standalone: true,
|
|
8
|
+
encapsulation: ViewEncapsulation.None,
|
|
9
|
+
// The styling lives on the HOST element, not an inner <view>, so the separator
|
|
10
|
+
// is a single element (like shadcn's separator on the web). This is essential:
|
|
11
|
+
// an Angular component host whose selector is not a native Lynx element falls
|
|
12
|
+
// back to a plain, UNSTYLED `view` (see runtime CLAUDE.md). If we styled an
|
|
13
|
+
// inner <view> instead, that unstyled host would sit between the parent and the
|
|
14
|
+
// line as an extra flex item — and a zero-content separator has no intrinsic
|
|
15
|
+
// size to give the host, so the host collapses and the line disappears.
|
|
16
|
+
//
|
|
17
|
+
// This is exactly why the vertical separator was invisible: in a `flex flex-row
|
|
18
|
+
// items-center` parent, `items-center` overrides Lynx's default `align-items:
|
|
19
|
+
// stretch`, so the unstyled host collapsed to 0 height and the inner view had
|
|
20
|
+
// nothing to fill. (The horizontal one survived only because a `flex-col`
|
|
21
|
+
// parent stretches the host to full width by default.) Styling the host makes
|
|
22
|
+
// the sizing classes apply to the actual flex item of the parent row.
|
|
23
|
+
host: {
|
|
24
|
+
'[class]': 'separatorClass()',
|
|
25
|
+
},
|
|
26
|
+
template: '',
|
|
27
|
+
})
|
|
28
|
+
export class UiSeparator {
|
|
29
|
+
readonly orientation = input<'horizontal' | 'vertical'>('horizontal');
|
|
30
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
31
|
+
|
|
32
|
+
protected readonly separatorClass = computed(() =>
|
|
33
|
+
cn(
|
|
34
|
+
'bg-border',
|
|
35
|
+
// Horizontal: 1px tall, `w-full` to span the parent's width.
|
|
36
|
+
// Vertical: 1px wide, `self-stretch` (align-self: stretch) to fill the
|
|
37
|
+
// parent row's height. We use align-self rather than `h-full` because a
|
|
38
|
+
// percentage height does not reliably resolve on a flex item in Lynx,
|
|
39
|
+
// whereas align-self stretches to the parent's cross axis directly — and
|
|
40
|
+
// it re-enables stretch even when the parent set `align-items: center`.
|
|
41
|
+
this.orientation() === 'horizontal' ? 'h-px w-full' : 'w-px self-stretch',
|
|
42
|
+
this.userClass(),
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Component } from '@angular/core';
|
|
2
|
+
import { render } from '@blotch/angular-lynx-testing-library';
|
|
3
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
UiSheet,
|
|
7
|
+
UiSheetDescription,
|
|
8
|
+
UiSheetFooter,
|
|
9
|
+
UiSheetHeader,
|
|
10
|
+
UiSheetTitle,
|
|
11
|
+
} from './sheet';
|
|
12
|
+
|
|
13
|
+
@Component({
|
|
14
|
+
standalone: true,
|
|
15
|
+
imports: [
|
|
16
|
+
UiSheet,
|
|
17
|
+
UiSheetHeader,
|
|
18
|
+
UiSheetTitle,
|
|
19
|
+
UiSheetDescription,
|
|
20
|
+
UiSheetFooter,
|
|
21
|
+
LYNX_ELEMENTS,
|
|
22
|
+
],
|
|
23
|
+
template: `
|
|
24
|
+
<ui-sheet [(open)]="open">
|
|
25
|
+
<ui-sheet-header>
|
|
26
|
+
<ui-sheet-title>Title</ui-sheet-title>
|
|
27
|
+
<ui-sheet-description>Body</ui-sheet-description>
|
|
28
|
+
</ui-sheet-header>
|
|
29
|
+
<ui-sheet-footer><text>Buttons</text></ui-sheet-footer>
|
|
30
|
+
</ui-sheet>
|
|
31
|
+
`,
|
|
32
|
+
})
|
|
33
|
+
class SheetHost {
|
|
34
|
+
open = true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('UiSheet', () => {
|
|
38
|
+
it('renders header, title, description, and footer', async () => {
|
|
39
|
+
const { getByText } = await render(SheetHost);
|
|
40
|
+
expect(getByText('Title')).toBeTruthy();
|
|
41
|
+
expect(getByText('Body')).toBeTruthy();
|
|
42
|
+
expect(getByText('Buttons')).toBeTruthy();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
ViewEncapsulation,
|
|
4
|
+
computed,
|
|
5
|
+
input,
|
|
6
|
+
model,
|
|
7
|
+
output,
|
|
8
|
+
} from '@angular/core';
|
|
9
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
10
|
+
|
|
11
|
+
import { UiBottomSheet } from '../bottom-sheet/bottom-sheet';
|
|
12
|
+
import { cn } from '../../utils/cn';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Styled bottom sheet: a thin wrapper over the shared `ui-bottom-sheet`
|
|
16
|
+
* primitive that adds content padding and pairs with the header/title/
|
|
17
|
+
* description/footer layout helpers below. All the overlay, animation, and
|
|
18
|
+
* drag-to-dismiss behavior lives in `ui-bottom-sheet`.
|
|
19
|
+
*
|
|
20
|
+
* The consumer's `class` and the built-in padding are applied to an inner
|
|
21
|
+
* content wrapper (not the panel) — the panel chrome (rounded corners, border,
|
|
22
|
+
* background) comes from the primitive.
|
|
23
|
+
*/
|
|
24
|
+
@Component({
|
|
25
|
+
selector: 'ui-sheet',
|
|
26
|
+
standalone: true,
|
|
27
|
+
imports: [LYNX_ELEMENTS, UiBottomSheet],
|
|
28
|
+
encapsulation: ViewEncapsulation.None,
|
|
29
|
+
template: `
|
|
30
|
+
<ui-bottom-sheet [(open)]="open" (closed)="closed.emit()">
|
|
31
|
+
<view [class]="contentClass()">
|
|
32
|
+
<ng-content />
|
|
33
|
+
</view>
|
|
34
|
+
</ui-bottom-sheet>
|
|
35
|
+
`,
|
|
36
|
+
})
|
|
37
|
+
export class UiSheet {
|
|
38
|
+
readonly open = model(false);
|
|
39
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
40
|
+
|
|
41
|
+
readonly closed = output<void>();
|
|
42
|
+
|
|
43
|
+
protected readonly contentClass = computed(() =>
|
|
44
|
+
cn('px-6 pb-6', this.userClass()),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Component({
|
|
49
|
+
selector: 'ui-sheet-header',
|
|
50
|
+
standalone: true,
|
|
51
|
+
imports: [LYNX_ELEMENTS],
|
|
52
|
+
encapsulation: ViewEncapsulation.None,
|
|
53
|
+
template: `
|
|
54
|
+
<view [class]="containerClass()">
|
|
55
|
+
<ng-content />
|
|
56
|
+
</view>
|
|
57
|
+
`,
|
|
58
|
+
})
|
|
59
|
+
export class UiSheetHeader {
|
|
60
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
61
|
+
|
|
62
|
+
protected readonly containerClass = computed(() =>
|
|
63
|
+
cn('mb-4 flex-col gap-1.5 flex', this.userClass()),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@Component({
|
|
68
|
+
selector: 'ui-sheet-title',
|
|
69
|
+
standalone: true,
|
|
70
|
+
imports: [LYNX_ELEMENTS],
|
|
71
|
+
encapsulation: ViewEncapsulation.None,
|
|
72
|
+
template: `<text [class]="textClass()"><ng-content /></text>`,
|
|
73
|
+
})
|
|
74
|
+
export class UiSheetTitle {
|
|
75
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
76
|
+
|
|
77
|
+
protected readonly textClass = computed(() =>
|
|
78
|
+
cn('text-lg font-semibold text-foreground', this.userClass()),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@Component({
|
|
83
|
+
selector: 'ui-sheet-description',
|
|
84
|
+
standalone: true,
|
|
85
|
+
imports: [LYNX_ELEMENTS],
|
|
86
|
+
encapsulation: ViewEncapsulation.None,
|
|
87
|
+
template: `<text [class]="textClass()"><ng-content /></text>`,
|
|
88
|
+
})
|
|
89
|
+
export class UiSheetDescription {
|
|
90
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
91
|
+
|
|
92
|
+
protected readonly textClass = computed(() =>
|
|
93
|
+
cn('text-sm text-muted-foreground', this.userClass()),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@Component({
|
|
98
|
+
selector: 'ui-sheet-footer',
|
|
99
|
+
standalone: true,
|
|
100
|
+
imports: [LYNX_ELEMENTS],
|
|
101
|
+
encapsulation: ViewEncapsulation.None,
|
|
102
|
+
template: `
|
|
103
|
+
<view [class]="containerClass()">
|
|
104
|
+
<ng-content />
|
|
105
|
+
</view>
|
|
106
|
+
`,
|
|
107
|
+
})
|
|
108
|
+
export class UiSheetFooter {
|
|
109
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
110
|
+
|
|
111
|
+
protected readonly containerClass = computed(() =>
|
|
112
|
+
cn('flex-col gap-2 pt-4 flex', this.userClass()),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UiSkeleton } from './skeleton';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { render } from '@blotch/angular-lynx-testing-library';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { UiSkeleton } from './skeleton';
|
|
4
|
+
|
|
5
|
+
describe('UiSkeleton', () => {
|
|
6
|
+
it('renders and starts the pulse animation', async () => {
|
|
7
|
+
// The `effect()` schedules pulse(el) once the view child resolves — just
|
|
8
|
+
// asserting no throw covers the effect body, host template, and computed
|
|
9
|
+
// class binding in a single mount.
|
|
10
|
+
const { container } = await render(UiSkeleton);
|
|
11
|
+
expect(container).toBeTruthy();
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
import { type AnimationHandle, pulse } from '../../utils/animate';
|
|
13
|
+
import { cn } from '../../utils/cn';
|
|
14
|
+
|
|
15
|
+
@Component({
|
|
16
|
+
selector: 'ui-skeleton',
|
|
17
|
+
standalone: true,
|
|
18
|
+
imports: [LYNX_ELEMENTS],
|
|
19
|
+
encapsulation: ViewEncapsulation.None,
|
|
20
|
+
template: `<view #skeleton [class]="skeletonClass()" />`,
|
|
21
|
+
})
|
|
22
|
+
export class UiSkeleton {
|
|
23
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
24
|
+
|
|
25
|
+
readonly skeletonRef = viewChild<ElementRef>('skeleton');
|
|
26
|
+
#pulseAnim?: AnimationHandle;
|
|
27
|
+
|
|
28
|
+
constructor() {
|
|
29
|
+
// Start the pulse animation once the element is available
|
|
30
|
+
effect(() => {
|
|
31
|
+
const el = this.skeletonRef()?.nativeElement;
|
|
32
|
+
// pulse() calls el.animate() which is inert in jsdom — coverage
|
|
33
|
+
// for the pulsing loop is exercised on-device.
|
|
34
|
+
/* v8 ignore start */
|
|
35
|
+
if (!el) return;
|
|
36
|
+
this.#pulseAnim?.cancel();
|
|
37
|
+
this.#pulseAnim = pulse(el);
|
|
38
|
+
/* v8 ignore stop */
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
protected readonly skeletonClass = computed(() =>
|
|
43
|
+
cn('rounded-md bg-muted', this.userClass()),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UiSpinner } from './spinner';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { setInputSignal } from '../../../test-utils/set-input-signal';
|
|
4
|
+
import { UiSpinner } from './spinner';
|
|
5
|
+
|
|
6
|
+
describe('UiSpinner', () => {
|
|
7
|
+
it('renders the SVG at the default size', async () => {
|
|
8
|
+
const { container } = await render(UiSpinner);
|
|
9
|
+
expect(container).toBeTruthy();
|
|
10
|
+
// The default size is 'md' → sizeStyle should render at 24px.
|
|
11
|
+
const view = container.querySelector('view');
|
|
12
|
+
expect(view?.getAttribute('style')).toContain('24px');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('substitutes currentColor when the color input is set', async () => {
|
|
16
|
+
const { container, componentRef } = await render(UiSpinner);
|
|
17
|
+
setInputSignal((componentRef.instance as UiSpinner).color, 'rgb(0, 0, 0)');
|
|
18
|
+
// Flush CD so the computed re-runs and the `content` attribute updates.
|
|
19
|
+
await waitForUpdate();
|
|
20
|
+
const svg = container.querySelector('svg');
|
|
21
|
+
expect(svg?.getAttribute('content')).toContain('rgb(0, 0, 0)');
|
|
22
|
+
expect(svg?.getAttribute('content')).not.toContain('currentColor');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('resizes for each supported size', async () => {
|
|
26
|
+
const { componentRef, container } = await render(UiSpinner);
|
|
27
|
+
for (const [size, px] of [
|
|
28
|
+
['xs', 16],
|
|
29
|
+
['sm', 20],
|
|
30
|
+
['md', 24],
|
|
31
|
+
['lg', 32],
|
|
32
|
+
] as const) {
|
|
33
|
+
setInputSignal((componentRef.instance as UiSpinner).size, size);
|
|
34
|
+
await waitForUpdate();
|
|
35
|
+
const view = container.querySelector('view');
|
|
36
|
+
expect(view?.getAttribute('style')).toContain(`${px}px`);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|