@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,186 @@
|
|
|
1
|
+
import { Component } 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 { UiToggle, UiToggleGroup, UiToggleGroupItem } from './toggle';
|
|
7
|
+
|
|
8
|
+
@Component({
|
|
9
|
+
standalone: true,
|
|
10
|
+
imports: [UiToggle, LYNX_ELEMENTS],
|
|
11
|
+
template: `<ui-toggle><text>Bold</text></ui-toggle>`,
|
|
12
|
+
})
|
|
13
|
+
class ToggleHost {}
|
|
14
|
+
|
|
15
|
+
describe('UiToggle', () => {
|
|
16
|
+
it('renders projected content', async () => {
|
|
17
|
+
const { getByText } = await render(ToggleHost);
|
|
18
|
+
expect(getByText('Bold')).toBeTruthy();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('onTap flips the pressed model', async () => {
|
|
22
|
+
const { componentRef } = await render(UiToggle);
|
|
23
|
+
const inst = componentRef.instance as unknown as UiToggle & {
|
|
24
|
+
onTap: () => void;
|
|
25
|
+
};
|
|
26
|
+
inst.onTap();
|
|
27
|
+
expect(inst.pressed()).toBe(true);
|
|
28
|
+
inst.onTap();
|
|
29
|
+
expect(inst.pressed()).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('onTap is a no-op when disabled', async () => {
|
|
33
|
+
const { componentRef } = await render(UiToggle);
|
|
34
|
+
const inst = componentRef.instance as unknown as UiToggle & {
|
|
35
|
+
onTap: () => void;
|
|
36
|
+
};
|
|
37
|
+
setInputSignal(inst.disabled, true);
|
|
38
|
+
await waitForUpdate();
|
|
39
|
+
inst.onTap();
|
|
40
|
+
expect(inst.pressed()).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('press handlers run cleanly (enabled + disabled)', async () => {
|
|
44
|
+
const { componentRef } = await render(UiToggle);
|
|
45
|
+
const inst = componentRef.instance as unknown as UiToggle & {
|
|
46
|
+
onPressStart: () => void;
|
|
47
|
+
onPressEnd: () => void;
|
|
48
|
+
onPressCancel: () => void;
|
|
49
|
+
};
|
|
50
|
+
expect(() => inst.onPressStart()).not.toThrow();
|
|
51
|
+
expect(() => inst.onPressEnd()).not.toThrow();
|
|
52
|
+
expect(() => inst.onPressCancel()).not.toThrow();
|
|
53
|
+
setInputSignal((inst as unknown as UiToggle).disabled, true);
|
|
54
|
+
await waitForUpdate();
|
|
55
|
+
expect(() => inst.onPressStart()).not.toThrow();
|
|
56
|
+
expect(() => inst.onPressEnd()).not.toThrow();
|
|
57
|
+
expect(() => inst.onPressCancel()).not.toThrow();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('applies the accent background when pressed', async () => {
|
|
61
|
+
const { componentRef, container } = await render(UiToggle);
|
|
62
|
+
const inst = componentRef.instance as UiToggle;
|
|
63
|
+
inst.pressed.set(true);
|
|
64
|
+
await waitForUpdate();
|
|
65
|
+
const view = container.querySelector('view');
|
|
66
|
+
expect(view?.getAttribute('class')).toContain('bg-accent');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('applies outline variant and small size classes', async () => {
|
|
70
|
+
const { componentRef, container } = await render(UiToggle);
|
|
71
|
+
const inst = componentRef.instance as UiToggle;
|
|
72
|
+
setInputSignal(inst.variant, 'outline');
|
|
73
|
+
setInputSignal(inst.size, 'sm');
|
|
74
|
+
await waitForUpdate();
|
|
75
|
+
const view = container.querySelector('view');
|
|
76
|
+
expect(view?.getAttribute('class')).toContain('border-border');
|
|
77
|
+
expect(view?.getAttribute('class')).toContain('h-9');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('applies large size classes', async () => {
|
|
81
|
+
const { componentRef, container } = await render(UiToggle);
|
|
82
|
+
const inst = componentRef.instance as UiToggle;
|
|
83
|
+
setInputSignal(inst.size, 'lg');
|
|
84
|
+
await waitForUpdate();
|
|
85
|
+
expect(container.querySelector('view')?.getAttribute('class')).toContain(
|
|
86
|
+
'h-11',
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('UiToggleGroup', () => {
|
|
92
|
+
it('single mode replaces selection and allows deselection', async () => {
|
|
93
|
+
const { componentRef } = await render(UiToggleGroup);
|
|
94
|
+
const inst = componentRef.instance as UiToggleGroup;
|
|
95
|
+
inst.toggle('a');
|
|
96
|
+
expect(inst.value()).toEqual(['a']);
|
|
97
|
+
inst.toggle('b');
|
|
98
|
+
expect(inst.value()).toEqual(['b']);
|
|
99
|
+
// Tap active item again to deselect (unique to single-mode).
|
|
100
|
+
inst.toggle('b');
|
|
101
|
+
expect(inst.value()).toEqual([]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('multiple mode toggles individual items', async () => {
|
|
105
|
+
const { componentRef } = await render(UiToggleGroup);
|
|
106
|
+
const inst = componentRef.instance as UiToggleGroup;
|
|
107
|
+
setInputSignal(inst.type, 'multiple');
|
|
108
|
+
await waitForUpdate();
|
|
109
|
+
inst.toggle('a');
|
|
110
|
+
inst.toggle('b');
|
|
111
|
+
expect(inst.value()).toEqual(['a', 'b']);
|
|
112
|
+
inst.toggle('a');
|
|
113
|
+
expect(inst.value()).toEqual(['b']);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('toggle() is a no-op when the group is disabled', async () => {
|
|
117
|
+
const { componentRef } = await render(UiToggleGroup);
|
|
118
|
+
const inst = componentRef.instance as UiToggleGroup;
|
|
119
|
+
setInputSignal(inst.disabled, true);
|
|
120
|
+
await waitForUpdate();
|
|
121
|
+
inst.toggle('a');
|
|
122
|
+
expect(inst.value()).toEqual([]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('isSelected reflects the current value list', async () => {
|
|
126
|
+
const { componentRef } = await render(UiToggleGroup);
|
|
127
|
+
const inst = componentRef.instance as UiToggleGroup;
|
|
128
|
+
inst.toggle('a');
|
|
129
|
+
expect(inst.isSelected('a')).toBe(true);
|
|
130
|
+
expect(inst.isSelected('b')).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('UiToggleGroupItem', () => {
|
|
135
|
+
const renderItem = async (
|
|
136
|
+
parentValue: string[] = [],
|
|
137
|
+
parentDisabled = false,
|
|
138
|
+
) => {
|
|
139
|
+
const parentResult = await render(UiToggleGroup);
|
|
140
|
+
const parent = parentResult.componentRef.instance as UiToggleGroup;
|
|
141
|
+
if (parentValue.length) parent.value.set(parentValue);
|
|
142
|
+
if (parentDisabled) setInputSignal(parent.disabled, true);
|
|
143
|
+
await waitForUpdate();
|
|
144
|
+
|
|
145
|
+
const itemResult = await render(UiToggleGroupItem, {
|
|
146
|
+
providers: [{ provide: UiToggleGroup, useValue: parent }],
|
|
147
|
+
});
|
|
148
|
+
const item = itemResult.componentRef.instance as UiToggleGroupItem;
|
|
149
|
+
setInputSignal(item.itemValue, 'a');
|
|
150
|
+
await waitForUpdate();
|
|
151
|
+
return { parent, item, itemResult };
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
it('onTap delegates to the parent group', async () => {
|
|
155
|
+
const { parent, item } = await renderItem();
|
|
156
|
+
(item as unknown as { onTap: () => void }).onTap();
|
|
157
|
+
expect(parent.value()).toEqual(['a']);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('highlights when selected', async () => {
|
|
161
|
+
const { itemResult } = await renderItem(['a']);
|
|
162
|
+
const view = itemResult.container.querySelector('view');
|
|
163
|
+
expect(view?.getAttribute('class')).toContain('bg-accent');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('press handlers run cleanly (enabled + disabled)', async () => {
|
|
167
|
+
const { item } = await renderItem();
|
|
168
|
+
const cast = item as unknown as {
|
|
169
|
+
onPressStart: () => void;
|
|
170
|
+
onPressEnd: () => void;
|
|
171
|
+
onPressCancel: () => void;
|
|
172
|
+
};
|
|
173
|
+
expect(() => cast.onPressStart()).not.toThrow();
|
|
174
|
+
expect(() => cast.onPressEnd()).not.toThrow();
|
|
175
|
+
|
|
176
|
+
const disabled = await renderItem([], true);
|
|
177
|
+
const cast2 = disabled.item as unknown as {
|
|
178
|
+
onPressStart: () => void;
|
|
179
|
+
onPressEnd: () => void;
|
|
180
|
+
onPressCancel: () => void;
|
|
181
|
+
};
|
|
182
|
+
expect(() => cast2.onPressStart()).not.toThrow();
|
|
183
|
+
expect(() => cast2.onPressEnd()).not.toThrow();
|
|
184
|
+
expect(() => cast2.onPressCancel()).not.toThrow();
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ElementRef,
|
|
3
|
+
Component,
|
|
4
|
+
ViewEncapsulation,
|
|
5
|
+
computed,
|
|
6
|
+
inject,
|
|
7
|
+
input,
|
|
8
|
+
model,
|
|
9
|
+
viewChild,
|
|
10
|
+
} from '@angular/core';
|
|
11
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
12
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
type AnimationHandle,
|
|
16
|
+
pressDown,
|
|
17
|
+
pressRelease,
|
|
18
|
+
} from '../../utils/animate';
|
|
19
|
+
import { cn } from '../../utils/cn';
|
|
20
|
+
|
|
21
|
+
const toggleVariants = cva('items-center rounded-md flex justify-center', {
|
|
22
|
+
variants: {
|
|
23
|
+
variant: {
|
|
24
|
+
default: '',
|
|
25
|
+
outline: 'border border-border',
|
|
26
|
+
},
|
|
27
|
+
size: {
|
|
28
|
+
default: 'h-10 px-3',
|
|
29
|
+
sm: 'h-9 px-2.5',
|
|
30
|
+
lg: 'h-11 px-5',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
defaultVariants: {
|
|
34
|
+
variant: 'default',
|
|
35
|
+
size: 'default',
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export type ToggleVariant = NonNullable<
|
|
40
|
+
VariantProps<typeof toggleVariants>['variant']
|
|
41
|
+
>;
|
|
42
|
+
export type ToggleSize = NonNullable<
|
|
43
|
+
VariantProps<typeof toggleVariants>['size']
|
|
44
|
+
>;
|
|
45
|
+
|
|
46
|
+
@Component({
|
|
47
|
+
selector: 'ui-toggle',
|
|
48
|
+
standalone: true,
|
|
49
|
+
imports: [LYNX_ELEMENTS],
|
|
50
|
+
encapsulation: ViewEncapsulation.None,
|
|
51
|
+
template: `
|
|
52
|
+
<view
|
|
53
|
+
#container
|
|
54
|
+
[class]="containerClass()"
|
|
55
|
+
(bindtouchstart)="onPressStart()"
|
|
56
|
+
(bindtouchend)="onPressEnd()"
|
|
57
|
+
(bindtouchcancel)="onPressCancel()"
|
|
58
|
+
(bindmousedown)="onPressStart()"
|
|
59
|
+
(bindmouseup)="onPressEnd()"
|
|
60
|
+
(bindmouseleave)="onPressCancel()"
|
|
61
|
+
(bindtap)="onTap()"
|
|
62
|
+
>
|
|
63
|
+
<ng-content />
|
|
64
|
+
</view>
|
|
65
|
+
`,
|
|
66
|
+
})
|
|
67
|
+
export class UiToggle {
|
|
68
|
+
readonly pressed = model(false);
|
|
69
|
+
readonly variant = input<ToggleVariant>('default');
|
|
70
|
+
readonly size = input<ToggleSize>('default');
|
|
71
|
+
readonly disabled = input(false);
|
|
72
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
73
|
+
|
|
74
|
+
readonly containerRef = viewChild<ElementRef>('container');
|
|
75
|
+
#pressAnim?: AnimationHandle;
|
|
76
|
+
|
|
77
|
+
protected readonly containerClass = computed(() =>
|
|
78
|
+
cn(
|
|
79
|
+
toggleVariants({ variant: this.variant(), size: this.size() }),
|
|
80
|
+
this.pressed() ? 'bg-accent' : 'bg-transparent',
|
|
81
|
+
this.disabled() && 'opacity-50',
|
|
82
|
+
this.userClass(),
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
protected onPressStart(): void {
|
|
87
|
+
if (this.disabled()) return;
|
|
88
|
+
this.#pressAnim?.cancel();
|
|
89
|
+
this.#pressAnim = pressDown(this.containerRef()?.nativeElement);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
protected onPressEnd(): void {
|
|
93
|
+
if (this.disabled()) return;
|
|
94
|
+
this.#pressAnim?.cancel();
|
|
95
|
+
this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
protected onPressCancel(): void {
|
|
99
|
+
this.#pressAnim?.cancel();
|
|
100
|
+
this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
protected onTap(): void {
|
|
104
|
+
if (this.disabled()) return;
|
|
105
|
+
this.pressed.update((v) => !v);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@Component({
|
|
110
|
+
selector: 'ui-toggle-group',
|
|
111
|
+
standalone: true,
|
|
112
|
+
imports: [LYNX_ELEMENTS],
|
|
113
|
+
encapsulation: ViewEncapsulation.None,
|
|
114
|
+
template: `
|
|
115
|
+
<view [class]="containerClass()">
|
|
116
|
+
<ng-content />
|
|
117
|
+
</view>
|
|
118
|
+
`,
|
|
119
|
+
})
|
|
120
|
+
export class UiToggleGroup {
|
|
121
|
+
readonly type = input<'single' | 'multiple'>('single');
|
|
122
|
+
readonly value = model<string[]>([]);
|
|
123
|
+
readonly variant = input<ToggleVariant>('default');
|
|
124
|
+
readonly size = input<ToggleSize>('default');
|
|
125
|
+
readonly disabled = input(false);
|
|
126
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
127
|
+
|
|
128
|
+
protected readonly containerClass = computed(() =>
|
|
129
|
+
cn('flex-row items-center gap-1 flex', this.userClass()),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
toggle(val: string): void {
|
|
133
|
+
if (this.disabled()) return;
|
|
134
|
+
if (this.type() === 'single') {
|
|
135
|
+
const current = this.value();
|
|
136
|
+
// Single mode allows deselection: tapping the active item again clears
|
|
137
|
+
// the selection to []. This differs from a radio group, which requires
|
|
138
|
+
// one item to always be selected.
|
|
139
|
+
this.value.set(current.includes(val) ? [] : [val]);
|
|
140
|
+
} else {
|
|
141
|
+
this.value.update((list) =>
|
|
142
|
+
list.includes(val) ? list.filter((v) => v !== val) : [...list, val],
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
isSelected(val: string): boolean {
|
|
148
|
+
return this.value().includes(val);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@Component({
|
|
153
|
+
selector: 'ui-toggle-group-item',
|
|
154
|
+
standalone: true,
|
|
155
|
+
imports: [LYNX_ELEMENTS],
|
|
156
|
+
encapsulation: ViewEncapsulation.None,
|
|
157
|
+
template: `
|
|
158
|
+
<view
|
|
159
|
+
#container
|
|
160
|
+
[class]="containerClass()"
|
|
161
|
+
(bindtouchstart)="onPressStart()"
|
|
162
|
+
(bindtouchend)="onPressEnd()"
|
|
163
|
+
(bindtouchcancel)="onPressCancel()"
|
|
164
|
+
(bindmousedown)="onPressStart()"
|
|
165
|
+
(bindmouseup)="onPressEnd()"
|
|
166
|
+
(bindmouseleave)="onPressCancel()"
|
|
167
|
+
(bindtap)="onTap()"
|
|
168
|
+
>
|
|
169
|
+
<ng-content />
|
|
170
|
+
</view>
|
|
171
|
+
`,
|
|
172
|
+
})
|
|
173
|
+
export class UiToggleGroupItem {
|
|
174
|
+
readonly #group = inject(UiToggleGroup);
|
|
175
|
+
|
|
176
|
+
readonly itemValue = input.required<string>({ alias: 'value' });
|
|
177
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
178
|
+
|
|
179
|
+
readonly containerRef = viewChild<ElementRef>('container');
|
|
180
|
+
#pressAnim?: AnimationHandle;
|
|
181
|
+
|
|
182
|
+
protected readonly isSelected = computed(() =>
|
|
183
|
+
this.#group.isSelected(this.itemValue()),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
protected readonly containerClass = computed(() =>
|
|
187
|
+
cn(
|
|
188
|
+
toggleVariants({
|
|
189
|
+
variant: this.#group.variant(),
|
|
190
|
+
size: this.#group.size(),
|
|
191
|
+
}),
|
|
192
|
+
this.isSelected() ? 'bg-accent' : 'bg-transparent',
|
|
193
|
+
this.#group.disabled() && 'opacity-50',
|
|
194
|
+
this.userClass(),
|
|
195
|
+
),
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
protected onPressStart(): void {
|
|
199
|
+
if (this.#group.disabled()) return;
|
|
200
|
+
this.#pressAnim?.cancel();
|
|
201
|
+
this.#pressAnim = pressDown(this.containerRef()?.nativeElement);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
protected onPressEnd(): void {
|
|
205
|
+
if (this.#group.disabled()) return;
|
|
206
|
+
this.#pressAnim?.cancel();
|
|
207
|
+
this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
protected onPressCancel(): void {
|
|
211
|
+
this.#pressAnim?.cancel();
|
|
212
|
+
this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
protected onTap(): void {
|
|
216
|
+
this.#group.toggle(this.itemValue());
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
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 { UiText, typographyVariants } from './typography';
|
|
6
|
+
|
|
7
|
+
@Component({
|
|
8
|
+
standalone: true,
|
|
9
|
+
imports: [UiText, LYNX_ELEMENTS],
|
|
10
|
+
template: `<ui-text [variant]="variant">Hello</ui-text>`,
|
|
11
|
+
})
|
|
12
|
+
class TypographyHost {
|
|
13
|
+
variant:
|
|
14
|
+
| 'h1'
|
|
15
|
+
| 'h2'
|
|
16
|
+
| 'h3'
|
|
17
|
+
| 'h4'
|
|
18
|
+
| 'p'
|
|
19
|
+
| 'lead'
|
|
20
|
+
| 'large'
|
|
21
|
+
| 'small'
|
|
22
|
+
| 'muted' = 'p';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('typographyVariants', () => {
|
|
26
|
+
it('returns default variant classes', () => {
|
|
27
|
+
expect(typographyVariants()).toContain('text-base');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it.each([
|
|
31
|
+
'h1',
|
|
32
|
+
'h2',
|
|
33
|
+
'h3',
|
|
34
|
+
'h4',
|
|
35
|
+
'p',
|
|
36
|
+
'lead',
|
|
37
|
+
'large',
|
|
38
|
+
'small',
|
|
39
|
+
'muted',
|
|
40
|
+
] as const)('returns %s variant classes', (variant) => {
|
|
41
|
+
expect(typographyVariants({ variant })).toBeTruthy();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('UiText', () => {
|
|
46
|
+
it('renders projected content', async () => {
|
|
47
|
+
const { getByText } = await render(TypographyHost);
|
|
48
|
+
expect(getByText('Hello')).toBeTruthy();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('renders with no host wrapper (uses default input value)', async () => {
|
|
52
|
+
const { getByText } = await render(UiText);
|
|
53
|
+
expect(getByText).toBeTruthy();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Component, ViewEncapsulation, computed, input } from '@angular/core';
|
|
2
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
3
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
4
|
+
|
|
5
|
+
import { cn } from '../../utils/cn';
|
|
6
|
+
|
|
7
|
+
export const typographyVariants = cva('', {
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
h1: 'text-4xl font-bold text-foreground',
|
|
11
|
+
h2: 'text-3xl font-semibold text-foreground',
|
|
12
|
+
h3: 'text-2xl font-semibold text-foreground',
|
|
13
|
+
h4: 'text-xl font-semibold text-foreground',
|
|
14
|
+
p: 'text-base text-foreground',
|
|
15
|
+
lead: 'text-xl text-muted-foreground',
|
|
16
|
+
large: 'text-lg font-semibold text-foreground',
|
|
17
|
+
small: 'text-sm font-medium text-foreground',
|
|
18
|
+
muted: 'text-sm text-muted-foreground',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
defaultVariants: {
|
|
22
|
+
variant: 'p',
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export type TypographyVariant = NonNullable<
|
|
27
|
+
VariantProps<typeof typographyVariants>['variant']
|
|
28
|
+
>;
|
|
29
|
+
|
|
30
|
+
@Component({
|
|
31
|
+
selector: 'ui-text',
|
|
32
|
+
standalone: true,
|
|
33
|
+
imports: [LYNX_ELEMENTS],
|
|
34
|
+
encapsulation: ViewEncapsulation.None,
|
|
35
|
+
template: `<text [class]="textClass()"><ng-content /></text>`,
|
|
36
|
+
})
|
|
37
|
+
export class UiText {
|
|
38
|
+
readonly variant = input<TypographyVariant>('p');
|
|
39
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
40
|
+
|
|
41
|
+
protected readonly textClass = computed(() =>
|
|
42
|
+
cn(typographyVariants({ variant: this.variant() }), this.userClass()),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @blotch/ui dark theme
|
|
3
|
+
*
|
|
4
|
+
* Complete rgba() color values referenced directly via var(--x) — see
|
|
5
|
+
* default.css for why complete rgba values are used instead of HSL on Lynx.
|
|
6
|
+
*
|
|
7
|
+
* Apply via a `.dark` class on the page element, or import as an
|
|
8
|
+
* alternative to default.css.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
page.dark {
|
|
12
|
+
--background: rgba(9, 9, 11, 1);
|
|
13
|
+
--foreground: rgba(250, 250, 250, 1);
|
|
14
|
+
|
|
15
|
+
--card: rgba(9, 9, 11, 1);
|
|
16
|
+
--card-foreground: rgba(250, 250, 250, 1);
|
|
17
|
+
|
|
18
|
+
--primary: rgba(250, 250, 250, 1);
|
|
19
|
+
--primary-foreground: rgba(24, 24, 27, 1);
|
|
20
|
+
|
|
21
|
+
--secondary: rgba(39, 39, 42, 1);
|
|
22
|
+
--secondary-foreground: rgba(250, 250, 250, 1);
|
|
23
|
+
|
|
24
|
+
--muted: rgba(39, 39, 42, 1);
|
|
25
|
+
--muted-foreground: rgba(161, 161, 170, 1);
|
|
26
|
+
|
|
27
|
+
--accent: rgba(39, 39, 42, 1);
|
|
28
|
+
--accent-foreground: rgba(250, 250, 250, 1);
|
|
29
|
+
|
|
30
|
+
--destructive: rgba(127, 29, 29, 1);
|
|
31
|
+
--destructive-foreground: rgba(250, 250, 250, 1);
|
|
32
|
+
--destructive-subtle: rgba(127, 29, 29, 0.1);
|
|
33
|
+
|
|
34
|
+
--border: rgba(39, 39, 42, 1);
|
|
35
|
+
--input: rgba(39, 39, 42, 1);
|
|
36
|
+
--ring: rgba(212, 212, 216, 1);
|
|
37
|
+
/* Translucent variant of --ring for a softer focus outline (see default.css). */
|
|
38
|
+
--ring-subtle: rgba(212, 212, 216, 0.4);
|
|
39
|
+
|
|
40
|
+
--radius: 8px;
|
|
41
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @blotch/ui default theme — light mode
|
|
3
|
+
*
|
|
4
|
+
* CSS custom properties hold COMPLETE rgba() color values (e.g.
|
|
5
|
+
* `--primary: rgba(24, 24, 27, 1)`), referenced directly via `var(--x)` by the
|
|
6
|
+
* tailwind plugin. This is the pattern used by the official React Lynx Tailwind
|
|
7
|
+
* example.
|
|
8
|
+
*
|
|
9
|
+
* Why not the shadcn/ui approach (raw HSL channels wrapped in
|
|
10
|
+
* `hsl(var(--x) / <alpha-value>)`): Lynx's native CSS engine only parses
|
|
11
|
+
* comma-separated hsl(), but Tailwind emits the modern space-separated form
|
|
12
|
+
* `hsl(var(--x) / 1)`, which Lynx silently drops (transparent). Storing complete
|
|
13
|
+
* rgba() values and referencing them directly sidesteps color-function parsing
|
|
14
|
+
* entirely and renders correctly on Lynx.
|
|
15
|
+
*
|
|
16
|
+
* Tradeoff: Tailwind opacity modifiers on semantic colors (e.g. bg-primary/50)
|
|
17
|
+
* do NOT work, because the color is an opaque value with no separable channels.
|
|
18
|
+
* Where a translucent semantic color is genuinely needed, add a dedicated
|
|
19
|
+
* rgba() variable (see --destructive-subtle) or use a solid token.
|
|
20
|
+
*
|
|
21
|
+
* Apply to `page` (Lynx root element) or import globally.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
page {
|
|
25
|
+
--background: rgba(255, 255, 255, 1);
|
|
26
|
+
--foreground: rgba(9, 9, 11, 1);
|
|
27
|
+
|
|
28
|
+
--card: rgba(255, 255, 255, 1);
|
|
29
|
+
--card-foreground: rgba(9, 9, 11, 1);
|
|
30
|
+
|
|
31
|
+
--primary: rgba(24, 24, 27, 1);
|
|
32
|
+
--primary-foreground: rgba(250, 250, 250, 1);
|
|
33
|
+
|
|
34
|
+
--secondary: rgba(244, 244, 245, 1);
|
|
35
|
+
--secondary-foreground: rgba(24, 24, 27, 1);
|
|
36
|
+
|
|
37
|
+
--muted: rgba(244, 244, 245, 1);
|
|
38
|
+
--muted-foreground: rgba(113, 113, 122, 1);
|
|
39
|
+
|
|
40
|
+
--accent: rgba(244, 244, 245, 1);
|
|
41
|
+
--accent-foreground: rgba(24, 24, 27, 1);
|
|
42
|
+
|
|
43
|
+
--destructive: rgba(239, 68, 68, 1);
|
|
44
|
+
--destructive-foreground: rgba(250, 250, 250, 1);
|
|
45
|
+
--destructive-subtle: rgba(239, 68, 68, 0.1);
|
|
46
|
+
|
|
47
|
+
--border: rgba(228, 228, 231, 1);
|
|
48
|
+
--input: rgba(228, 228, 231, 1);
|
|
49
|
+
--ring: rgba(24, 24, 27, 1);
|
|
50
|
+
/* Translucent variant of --ring for a softer focus outline. A separate
|
|
51
|
+
pre-composed value is needed because Tailwind opacity modifiers can't
|
|
52
|
+
apply to the opaque --ring, and Lynx can't inject alpha into it either. */
|
|
53
|
+
--ring-subtle: rgba(24, 24, 27, 0.4);
|
|
54
|
+
|
|
55
|
+
--radius: 8px;
|
|
56
|
+
}
|