@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,157 @@
|
|
|
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 { UiCartesianChart } from '../cartesian-chart/cartesian-chart';
|
|
5
|
+
import { UiScatterChart, UiScatterSeries } from './scatter-chart';
|
|
6
|
+
|
|
7
|
+
describe('UiScatterChart', () => {
|
|
8
|
+
it('renders with default configuration', async () => {
|
|
9
|
+
const { container } = await render(UiScatterChart);
|
|
10
|
+
expect(container).toBeTruthy();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('derives axis domains from the data extremes', async () => {
|
|
14
|
+
const { componentRef } = await render(UiScatterChart);
|
|
15
|
+
const inst = componentRef.instance as UiScatterChart;
|
|
16
|
+
setInputSignal(inst.data, [
|
|
17
|
+
{ x: 0, y: 0 },
|
|
18
|
+
{ x: 5, y: 8 },
|
|
19
|
+
{ x: 10, y: 3 },
|
|
20
|
+
]);
|
|
21
|
+
await waitForUpdate();
|
|
22
|
+
const cast = inst as unknown as {
|
|
23
|
+
xAxis: () => { domain: readonly [number, number] };
|
|
24
|
+
yAxis: () => { domain: readonly [number, number] };
|
|
25
|
+
effectivePadding: () => {
|
|
26
|
+
top?: number;
|
|
27
|
+
right?: number;
|
|
28
|
+
bottom?: number;
|
|
29
|
+
left?: number;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
expect(cast.xAxis().domain[0]).toBeLessThanOrEqual(0);
|
|
33
|
+
expect(cast.xAxis().domain[1]).toBeGreaterThanOrEqual(10);
|
|
34
|
+
expect(cast.yAxis().domain[0]).toBeLessThanOrEqual(0);
|
|
35
|
+
expect(cast.yAxis().domain[1]).toBeGreaterThanOrEqual(8);
|
|
36
|
+
// Default SCATTER_INSET on every side.
|
|
37
|
+
expect(cast.effectivePadding().top).toBeCloseTo(0.04);
|
|
38
|
+
expect(cast.effectivePadding().right).toBeCloseTo(0.04);
|
|
39
|
+
expect(cast.effectivePadding().bottom).toBeCloseTo(0.04);
|
|
40
|
+
expect(cast.effectivePadding().left).toBeCloseTo(0.04);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('honors explicit xMin / xMax / yMin / yMax overrides', async () => {
|
|
44
|
+
const { componentRef } = await render(UiScatterChart);
|
|
45
|
+
const inst = componentRef.instance as UiScatterChart;
|
|
46
|
+
setInputSignal(inst.data, [
|
|
47
|
+
{ x: 0, y: 0 },
|
|
48
|
+
{ x: 10, y: 10 },
|
|
49
|
+
]);
|
|
50
|
+
setInputSignal(inst.xMin, -5);
|
|
51
|
+
setInputSignal(inst.xMax, 20);
|
|
52
|
+
setInputSignal(inst.yMin, -1);
|
|
53
|
+
setInputSignal(inst.yMax, 15);
|
|
54
|
+
await waitForUpdate();
|
|
55
|
+
const cast = inst as unknown as {
|
|
56
|
+
xAxis: () => { domain: readonly [number, number] };
|
|
57
|
+
yAxis: () => { domain: readonly [number, number] };
|
|
58
|
+
};
|
|
59
|
+
expect(cast.xAxis().domain[0]).toBeLessThanOrEqual(-5);
|
|
60
|
+
expect(cast.xAxis().domain[1]).toBeGreaterThanOrEqual(20);
|
|
61
|
+
expect(cast.yAxis().domain[0]).toBeLessThanOrEqual(-1);
|
|
62
|
+
expect(cast.yAxis().domain[1]).toBeGreaterThanOrEqual(15);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('user padding overrides individual sides of the default inset', async () => {
|
|
66
|
+
const { componentRef } = await render(UiScatterChart);
|
|
67
|
+
const inst = componentRef.instance as UiScatterChart;
|
|
68
|
+
setInputSignal(inst.padding, { top: 0.2 });
|
|
69
|
+
await waitForUpdate();
|
|
70
|
+
const cast = inst as unknown as {
|
|
71
|
+
effectivePadding: () => {
|
|
72
|
+
top?: number;
|
|
73
|
+
right?: number;
|
|
74
|
+
bottom?: number;
|
|
75
|
+
left?: number;
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
expect(cast.effectivePadding().top).toBeCloseTo(0.2);
|
|
79
|
+
expect(cast.effectivePadding().left).toBeCloseTo(0.04);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('exposes the default tick formatter used by the child cartesian chart', async () => {
|
|
83
|
+
const { componentRef } = await render(UiScatterChart);
|
|
84
|
+
const inst = componentRef.instance as UiScatterChart;
|
|
85
|
+
expect(inst.xTickFormat()(3.14159)).toBe('3.14');
|
|
86
|
+
expect(inst.yTickFormat()(1.005)).toBe('1');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('UiScatterSeries', () => {
|
|
91
|
+
const renderSeries = async () => {
|
|
92
|
+
const parentResult = await render(UiCartesianChart);
|
|
93
|
+
const parent = parentResult.componentRef.instance as UiCartesianChart;
|
|
94
|
+
setInputSignal(parent.xDomain, [0, 10]);
|
|
95
|
+
setInputSignal(parent.yDomain, [0, 10]);
|
|
96
|
+
await waitForUpdate();
|
|
97
|
+
const seriesResult = await render(UiScatterSeries, {
|
|
98
|
+
providers: [{ provide: UiCartesianChart, useValue: parent }],
|
|
99
|
+
});
|
|
100
|
+
const series = seriesResult.componentRef.instance as UiScatterSeries;
|
|
101
|
+
setInputSignal(series.data, [
|
|
102
|
+
{ x: 0, y: 0 },
|
|
103
|
+
{ x: 5, y: 5 },
|
|
104
|
+
{ x: 10, y: 10 },
|
|
105
|
+
]);
|
|
106
|
+
await waitForUpdate();
|
|
107
|
+
return { parent, series };
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
it('renders a circular dot per datum with the fallback color', async () => {
|
|
111
|
+
const { series } = await renderSeries();
|
|
112
|
+
const cast = series as unknown as {
|
|
113
|
+
dots: () => { style: string; point: { x: number; y: number } }[];
|
|
114
|
+
};
|
|
115
|
+
const dots = cast.dots();
|
|
116
|
+
expect(dots.length).toBe(3);
|
|
117
|
+
for (const d of dots) {
|
|
118
|
+
expect(d.style).toContain('background-color: var(--primary)');
|
|
119
|
+
expect(d.style).toContain('border-radius');
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('cycles through the per-dot palette when colors are provided', async () => {
|
|
124
|
+
const { series } = await renderSeries();
|
|
125
|
+
setInputSignal(series.colors, ['#f00', '#0f0']);
|
|
126
|
+
await waitForUpdate();
|
|
127
|
+
const cast = series as unknown as { dots: () => { style: string }[] };
|
|
128
|
+
const dots = cast.dots();
|
|
129
|
+
expect(dots[0].style).toContain('#f00');
|
|
130
|
+
expect(dots[1].style).toContain('#0f0');
|
|
131
|
+
expect(dots[2].style).toContain('#f00');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('per-dot sizes drive per-dot radii for a bubble chart', async () => {
|
|
135
|
+
const { series } = await renderSeries();
|
|
136
|
+
setInputSignal(series.sizes, [10, 6]);
|
|
137
|
+
await waitForUpdate();
|
|
138
|
+
const cast = series as unknown as { dots: () => { style: string }[] };
|
|
139
|
+
const dots = cast.dots();
|
|
140
|
+
// radius 10 → width 20px
|
|
141
|
+
expect(dots[0].style).toContain('width: 20.00px');
|
|
142
|
+
// radius 6 → width 12px
|
|
143
|
+
expect(dots[1].style).toContain('width: 12.00px');
|
|
144
|
+
// No entry → falls back to uniform radius (default 4) → width 8px
|
|
145
|
+
expect(dots[2].style).toContain('width: 8.00px');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('emits pointTap with the source point when a dot is tapped', async () => {
|
|
149
|
+
const { series } = await renderSeries();
|
|
150
|
+
const cb = vi.fn();
|
|
151
|
+
series.pointTap.subscribe(cb);
|
|
152
|
+
(
|
|
153
|
+
series as unknown as { onPointTap: (p: { x: number; y: number }) => void }
|
|
154
|
+
).onPointTap({ x: 3, y: 4 });
|
|
155
|
+
expect(cb).toHaveBeenCalledWith({ x: 3, y: 4 });
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Mock Angular (and the transitively-loaded cartesian-chart Angular usage) so
|
|
4
|
+
// importing the module needs no framework runtime. Only the exported pure
|
|
5
|
+
// `computeDots` is exercised; the components are never built.
|
|
6
|
+
vi.mock('@angular/core', () => ({
|
|
7
|
+
Component: () => () => {},
|
|
8
|
+
ViewEncapsulation: { None: 0 },
|
|
9
|
+
computed: () => () => {},
|
|
10
|
+
inject: () => ({}),
|
|
11
|
+
input: () => () => {},
|
|
12
|
+
output: () => () => {},
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock('@blotch/angular-lynx', () => ({
|
|
16
|
+
LYNX_ELEMENTS: [],
|
|
17
|
+
// cartesian-chart (imported transitively) registers these in its decorator.
|
|
18
|
+
LynxGestureDetector: class {},
|
|
19
|
+
PanGesture: class {},
|
|
20
|
+
PinchGesture: class {},
|
|
21
|
+
Gesture: { Simultaneous: () => ({}) },
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
const { computeDots } = await import('./scatter-chart');
|
|
25
|
+
|
|
26
|
+
describe('computeDots', () => {
|
|
27
|
+
it('centers a dot on its point (top-left offset by the radius)', () => {
|
|
28
|
+
const [dot] = computeDots([{ cx: 50, cy: 30, r: 4 }]);
|
|
29
|
+
expect(dot.left).toBe(46); // cx - r
|
|
30
|
+
expect(dot.top).toBe(26); // cy - r
|
|
31
|
+
expect(dot.size).toBe(8); // r * 2 — the box the circle fills
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('honors a per-point radius (a bubble chart)', () => {
|
|
35
|
+
const [small, big] = computeDots([
|
|
36
|
+
{ cx: 10, cy: 10, r: 2 },
|
|
37
|
+
{ cx: 40, cy: 40, r: 9 },
|
|
38
|
+
]);
|
|
39
|
+
expect(small.size).toBe(4);
|
|
40
|
+
expect(small.left).toBe(8);
|
|
41
|
+
expect(big.size).toBe(18);
|
|
42
|
+
expect(big.left).toBe(31); // 40 - 9
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('keeps each dot centered on its own coordinates', () => {
|
|
46
|
+
const dots = computeDots([
|
|
47
|
+
{ cx: 0, cy: 100, r: 3 },
|
|
48
|
+
{ cx: 100, cy: 0, r: 3 },
|
|
49
|
+
]);
|
|
50
|
+
expect(dots[0]).toEqual({ left: -3, top: 97, size: 6 });
|
|
51
|
+
expect(dots[1]).toEqual({ left: 97, top: -3, size: 6 });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('returns an empty array for no points', () => {
|
|
55
|
+
expect(computeDots([])).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
ViewEncapsulation,
|
|
4
|
+
computed,
|
|
5
|
+
inject,
|
|
6
|
+
input,
|
|
7
|
+
output,
|
|
8
|
+
} from '@angular/core';
|
|
9
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
type ChartPadding,
|
|
13
|
+
type ChartPoint,
|
|
14
|
+
UiCartesianChart,
|
|
15
|
+
niceScale,
|
|
16
|
+
} from '../cartesian-chart/cartesian-chart';
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Types
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
// A scatter mark is just an (x, y) datum, so it reuses the shared `ChartPoint`
|
|
23
|
+
// rather than defining its own. That also lets a scatter series and a line
|
|
24
|
+
// series (e.g. a regression line) share one dataset on the same axes.
|
|
25
|
+
|
|
26
|
+
/** One point after projection into plot-local pixel space, with its dot radius. */
|
|
27
|
+
type ProjectedDot = {
|
|
28
|
+
/** Center x of the dot, in px. */
|
|
29
|
+
cx: number;
|
|
30
|
+
/** Center y of the dot, in px. */
|
|
31
|
+
cy: number;
|
|
32
|
+
/** Dot radius in px (varies per point for a bubble chart). */
|
|
33
|
+
r: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** A square (rendered as a circle via border-radius) in plot-local pixel space. */
|
|
37
|
+
export type DotShape = { left: number; top: number; size: number };
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Pure geometry (exported so it can be unit-tested without Angular)
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Turns projected points — already in plot-local pixel space — into square boxes
|
|
45
|
+
* centered on each point, ready to render as circular `<view>`s (the caller sets
|
|
46
|
+
* `border-radius` to half the size). A scatter mark is the simplest chart mark
|
|
47
|
+
* of all: one box per datum, positioned so the point sits at its center.
|
|
48
|
+
*
|
|
49
|
+
* Each dot's radius comes from the point itself, so a per-point radius (a bubble
|
|
50
|
+
* chart) and a uniform radius (a plain scatter) share this one mapping.
|
|
51
|
+
*/
|
|
52
|
+
export const computeDots = (dots: readonly ProjectedDot[]): DotShape[] =>
|
|
53
|
+
dots.map((d) => ({ left: d.cx - d.r, top: d.cy - d.r, size: d.r * 2 }));
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Rendering helpers (mirror the equivalents in line-chart.ts / bar-chart.ts —
|
|
57
|
+
// kept local so this module stays self-contained when scaffolded by `dolan add`)
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Formats a pixel value for an inline style, avoiding scientific notation.
|
|
62
|
+
*/
|
|
63
|
+
const px = (n: number): string => `${n.toFixed(2)}px`;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Default axis label formatter — plain JS only (no `Intl`, absent in Lynx).
|
|
67
|
+
*/
|
|
68
|
+
const defaultTickFormat = (value: number): string =>
|
|
69
|
+
String(Math.round(value * 100) / 100);
|
|
70
|
+
|
|
71
|
+
// Default inner inset (fraction of the plot) for the wrapper. Unlike the line
|
|
72
|
+
// chart, a scatter's marks are dots at the data extremes, so a small inset on
|
|
73
|
+
// every side keeps the edge dots from being clipped by the frame.
|
|
74
|
+
const SCATTER_INSET = 0.04;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A single scatter series drawn inside a `<ui-cartesian-chart>`. Reads the shared
|
|
78
|
+
* scales from the parent chart via DI and renders one circular `<view>` per datum
|
|
79
|
+
* at its (x, y) position.
|
|
80
|
+
*
|
|
81
|
+
* ## Why the host is `position: absolute; width/height: 100%`
|
|
82
|
+
* Identical to `UiLineSeries`: Lynx resolves an absolute child against its
|
|
83
|
+
* **direct parent** (there is no `position: static`), so the host itself must be
|
|
84
|
+
* the plot-sized containing block. Every dot is a direct child of it.
|
|
85
|
+
*
|
|
86
|
+
* ## Per-point colors and sizes
|
|
87
|
+
* `color` fills every dot by default; pass `colors` to tint each dot (a
|
|
88
|
+
* categorical scatter) — the palette cycles if it is shorter than the data. Pass
|
|
89
|
+
* `sizes` to give each dot its own radius (a bubble chart); a point with no entry
|
|
90
|
+
* falls back to the uniform `radius`.
|
|
91
|
+
*/
|
|
92
|
+
@Component({
|
|
93
|
+
selector: 'ui-scatter-series',
|
|
94
|
+
standalone: true,
|
|
95
|
+
imports: [LYNX_ELEMENTS],
|
|
96
|
+
encapsulation: ViewEncapsulation.None,
|
|
97
|
+
host: {
|
|
98
|
+
style:
|
|
99
|
+
'position: absolute; left: 0px; top: 0px; width: 100%; height: 100%;',
|
|
100
|
+
'[class]': 'userClass()',
|
|
101
|
+
},
|
|
102
|
+
template: `
|
|
103
|
+
@for (dot of dots(); track $index) {
|
|
104
|
+
<view [style]="dot.style" (bindtap)="onPointTap(dot.point)"></view>
|
|
105
|
+
}
|
|
106
|
+
`,
|
|
107
|
+
})
|
|
108
|
+
export class UiScatterSeries {
|
|
109
|
+
readonly #chart = inject(UiCartesianChart);
|
|
110
|
+
|
|
111
|
+
readonly data = input.required<ChartPoint[]>();
|
|
112
|
+
/** Fill for every dot. Overridden per dot by `colors` when that is set. */
|
|
113
|
+
readonly color = input('var(--primary)');
|
|
114
|
+
/** Per-dot fills (a categorical palette). Cycles when shorter than the data;
|
|
115
|
+
* an empty array falls back to `color`. */
|
|
116
|
+
readonly colors = input<string[]>([]);
|
|
117
|
+
/** Uniform dot radius in px. Overridden per dot by `sizes`. */
|
|
118
|
+
readonly radius = input(4);
|
|
119
|
+
/** Per-dot radii in px (a bubble chart). A point with no entry uses `radius`. */
|
|
120
|
+
readonly sizes = input<number[]>([]);
|
|
121
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
122
|
+
|
|
123
|
+
readonly pointTap = output<ChartPoint>();
|
|
124
|
+
|
|
125
|
+
// Project each datum into plot-local pixel space via the shared scales, picking
|
|
126
|
+
// up a per-point radius when `sizes` supplies one. Reading the chart's scale
|
|
127
|
+
// signals here makes this recompute whenever the domain or plot size changes.
|
|
128
|
+
readonly #projected = computed(() => {
|
|
129
|
+
const scaleX = this.#chart.xScale();
|
|
130
|
+
const scaleY = this.#chart.yScale();
|
|
131
|
+
const sizes = this.sizes();
|
|
132
|
+
const r = this.radius();
|
|
133
|
+
return this.data().map((point, i) => ({
|
|
134
|
+
cx: scaleX(point.x),
|
|
135
|
+
cy: scaleY(point.y),
|
|
136
|
+
r: sizes[i] ?? r,
|
|
137
|
+
point,
|
|
138
|
+
}));
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
protected readonly dots = computed(() => {
|
|
142
|
+
const projected = this.#projected();
|
|
143
|
+
const fallback = this.color();
|
|
144
|
+
const palette = this.colors();
|
|
145
|
+
// computeDots works purely from the projected pixel fields; zip its output
|
|
146
|
+
// back with each source point so a dot's tap can emit the original datum.
|
|
147
|
+
// A circle is a square with border-radius of half its size.
|
|
148
|
+
return computeDots(projected).map((shape, i) => {
|
|
149
|
+
const fill = palette.length ? palette[i % palette.length] : fallback;
|
|
150
|
+
return {
|
|
151
|
+
point: projected[i].point,
|
|
152
|
+
style:
|
|
153
|
+
`position: absolute; left: ${px(shape.left)}; top: ${px(shape.top)}; ` +
|
|
154
|
+
`width: ${px(shape.size)}; height: ${px(shape.size)}; ` +
|
|
155
|
+
`background-color: ${fill}; border-radius: ${px(shape.size / 2)};`,
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
protected onPointTap(point: ChartPoint): void {
|
|
161
|
+
this.pointTap.emit(point);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* A ready-to-use scatter chart for the common single-series case. Composes a
|
|
167
|
+
* `<ui-cartesian-chart>` with one `<ui-scatter-series>` and auto-derives rounded
|
|
168
|
+
* axis domains + ticks from the data. For a regression/trend line on top, or
|
|
169
|
+
* full control over the axes, use `<ui-cartesian-chart>` with a
|
|
170
|
+
* `<ui-scatter-series>` (and optionally a `<ui-line-series>`) directly.
|
|
171
|
+
*/
|
|
172
|
+
@Component({
|
|
173
|
+
selector: 'ui-scatter-chart',
|
|
174
|
+
standalone: true,
|
|
175
|
+
imports: [LYNX_ELEMENTS, UiCartesianChart, UiScatterSeries],
|
|
176
|
+
encapsulation: ViewEncapsulation.None,
|
|
177
|
+
template: `
|
|
178
|
+
<ui-cartesian-chart
|
|
179
|
+
[xDomain]="xAxis().domain"
|
|
180
|
+
[yDomain]="yAxis().domain"
|
|
181
|
+
[xTicks]="xAxis().ticks"
|
|
182
|
+
[yTicks]="yAxis().ticks"
|
|
183
|
+
[width]="width()"
|
|
184
|
+
[height]="height()"
|
|
185
|
+
[tickCount]="tickCount()"
|
|
186
|
+
[showXGrid]="showXGrid()"
|
|
187
|
+
[xAxisLabel]="xAxisLabel()"
|
|
188
|
+
[yAxisLabel]="yAxisLabel()"
|
|
189
|
+
[padding]="effectivePadding()"
|
|
190
|
+
[xTickFormat]="xTickFormat()"
|
|
191
|
+
[yTickFormat]="yTickFormat()"
|
|
192
|
+
[zoomable]="zoomable()"
|
|
193
|
+
[zoomAxes]="zoomAxes()"
|
|
194
|
+
[showZoomControls]="showZoomControls()"
|
|
195
|
+
[minZoom]="minZoom()"
|
|
196
|
+
[maxZoom]="maxZoom()"
|
|
197
|
+
[zoomStep]="zoomStep()"
|
|
198
|
+
[class]="userClass()"
|
|
199
|
+
>
|
|
200
|
+
<ui-scatter-series
|
|
201
|
+
[data]="data()"
|
|
202
|
+
[color]="color()"
|
|
203
|
+
[colors]="colors()"
|
|
204
|
+
[radius]="radius()"
|
|
205
|
+
[sizes]="sizes()"
|
|
206
|
+
(pointTap)="pointTap.emit($event)"
|
|
207
|
+
/>
|
|
208
|
+
</ui-cartesian-chart>
|
|
209
|
+
`,
|
|
210
|
+
})
|
|
211
|
+
export class UiScatterChart {
|
|
212
|
+
readonly data = input.required<ChartPoint[]>();
|
|
213
|
+
readonly color = input('var(--primary)');
|
|
214
|
+
readonly colors = input<string[]>([]);
|
|
215
|
+
readonly radius = input(4);
|
|
216
|
+
readonly sizes = input<number[]>([]);
|
|
217
|
+
readonly width = input(320);
|
|
218
|
+
readonly height = input(200);
|
|
219
|
+
readonly tickCount = input(5);
|
|
220
|
+
/** Draw vertical gridlines at each x tick (horizontal ones are on by default). */
|
|
221
|
+
readonly showXGrid = input(false);
|
|
222
|
+
/** Title for the x-axis (centered below the tick labels). */
|
|
223
|
+
readonly xAxisLabel = input<string>('');
|
|
224
|
+
/** Title for the y-axis (rotated in the left gutter). */
|
|
225
|
+
readonly yAxisLabel = input<string>('');
|
|
226
|
+
/** Inner inset of the plot area — merged over the default all-side inset
|
|
227
|
+
* (below), so any side you set wins. See {@link ChartPadding}. */
|
|
228
|
+
readonly padding = input<ChartPadding>({});
|
|
229
|
+
/** Force the x-axis lower/upper bound instead of deriving it from the data.
|
|
230
|
+
* Omitted bounds fall back to the data extremes. */
|
|
231
|
+
readonly xMin = input<number | undefined>(undefined);
|
|
232
|
+
readonly xMax = input<number | undefined>(undefined);
|
|
233
|
+
/** Force the y-axis lower/upper bound (e.g. to compare two charts on one scale). */
|
|
234
|
+
readonly yMin = input<number | undefined>(undefined);
|
|
235
|
+
readonly yMax = input<number | undefined>(undefined);
|
|
236
|
+
readonly xTickFormat = input<(value: number) => string>(defaultTickFormat);
|
|
237
|
+
readonly yTickFormat = input<(value: number) => string>(defaultTickFormat);
|
|
238
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
239
|
+
|
|
240
|
+
// Pan/zoom — forwarded to the underlying <ui-cartesian-chart>. Off by default.
|
|
241
|
+
readonly zoomable = input(false);
|
|
242
|
+
readonly zoomAxes = input<'x' | 'y' | 'xy'>('xy');
|
|
243
|
+
readonly showZoomControls = input(true);
|
|
244
|
+
readonly minZoom = input(1);
|
|
245
|
+
readonly maxZoom = input(8);
|
|
246
|
+
readonly zoomStep = input(1.4);
|
|
247
|
+
|
|
248
|
+
readonly pointTap = output<ChartPoint>();
|
|
249
|
+
|
|
250
|
+
// Auto-compute rounded axes from the data extremes, letting an explicit
|
|
251
|
+
// min/max override either end. Unlike the bar/area charts there is no baseline
|
|
252
|
+
// to fold in — both axes simply fit the data. `Math.min/max(...[])` yield
|
|
253
|
+
// ±Infinity for empty data, which `niceScale` handles by padding.
|
|
254
|
+
protected readonly xAxis = computed(() => {
|
|
255
|
+
const xs = this.data().map((p) => p.x);
|
|
256
|
+
const lo = this.xMin() ?? Math.min(...xs);
|
|
257
|
+
const hi = this.xMax() ?? Math.max(...xs);
|
|
258
|
+
return niceScale(lo, hi, this.tickCount());
|
|
259
|
+
});
|
|
260
|
+
protected readonly yAxis = computed(() => {
|
|
261
|
+
const ys = this.data().map((p) => p.y);
|
|
262
|
+
const lo = this.yMin() ?? Math.min(...ys);
|
|
263
|
+
const hi = this.yMax() ?? Math.max(...ys);
|
|
264
|
+
return niceScale(lo, hi, this.tickCount());
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Dots sit at the data extremes, so without an inset the edge dots would be
|
|
268
|
+
// clipped by the frame. Default a small inset on every side; user `padding` is
|
|
269
|
+
// spread last, so an explicit side wins.
|
|
270
|
+
protected readonly effectivePadding = computed<ChartPadding>(() => ({
|
|
271
|
+
top: SCATTER_INSET,
|
|
272
|
+
right: SCATTER_INSET,
|
|
273
|
+
bottom: SCATTER_INSET,
|
|
274
|
+
left: SCATTER_INSET,
|
|
275
|
+
...this.padding(),
|
|
276
|
+
}));
|
|
277
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UiScrollArea } from './scroll-area';
|
|
@@ -0,0 +1,40 @@
|
|
|
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 { UiScrollArea } from './scroll-area';
|
|
7
|
+
|
|
8
|
+
@Component({
|
|
9
|
+
standalone: true,
|
|
10
|
+
imports: [UiScrollArea, LYNX_ELEMENTS],
|
|
11
|
+
template: `<ui-scroll-area><text>Body</text></ui-scroll-area>`,
|
|
12
|
+
})
|
|
13
|
+
class ScrollAreaHost {}
|
|
14
|
+
|
|
15
|
+
describe('UiScrollArea', () => {
|
|
16
|
+
it('renders projected content in the scroll view', async () => {
|
|
17
|
+
const { getByText } = await render(ScrollAreaHost);
|
|
18
|
+
expect(getByText('Body')).toBeTruthy();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('emits an explicit height when the height input is set', async () => {
|
|
22
|
+
const { componentRef, container } = await render(UiScrollArea);
|
|
23
|
+
setInputSignal((componentRef.instance as UiScrollArea).height, '250px');
|
|
24
|
+
await waitForUpdate();
|
|
25
|
+
const scroll = container.querySelector('scroll-view');
|
|
26
|
+
expect(scroll?.getAttribute('style')).toContain('height: 250px');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('flips the content layout for horizontal orientation', async () => {
|
|
30
|
+
const { componentRef, container } = await render(UiScrollArea);
|
|
31
|
+
setInputSignal(
|
|
32
|
+
(componentRef.instance as UiScrollArea).orientation,
|
|
33
|
+
'horizontal',
|
|
34
|
+
);
|
|
35
|
+
await waitForUpdate();
|
|
36
|
+
// The inner content wrapper picks up flex-row for the horizontal branch.
|
|
37
|
+
const rowEl = container.querySelector('.flex-row');
|
|
38
|
+
expect(rowEl).toBeTruthy();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
ViewEncapsulation,
|
|
4
|
+
computed,
|
|
5
|
+
input,
|
|
6
|
+
output,
|
|
7
|
+
} from '@angular/core';
|
|
8
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
9
|
+
|
|
10
|
+
import { cn } from '../../utils/cn';
|
|
11
|
+
|
|
12
|
+
@Component({
|
|
13
|
+
selector: 'ui-scroll-area',
|
|
14
|
+
standalone: true,
|
|
15
|
+
imports: [LYNX_ELEMENTS],
|
|
16
|
+
encapsulation: ViewEncapsulation.None,
|
|
17
|
+
// `|| undefined` on boolean attributes removes the attribute entirely when
|
|
18
|
+
// false. Passing the string "false" (what Angular produces for a false
|
|
19
|
+
// binding without `|| undefined`) is still treated as truthy by Lynx's
|
|
20
|
+
// native attribute parser, so absence is the only way to truly disable.
|
|
21
|
+
template: `
|
|
22
|
+
<scroll-view
|
|
23
|
+
[attr.scroll-orientation]="orientation()"
|
|
24
|
+
[attr.bounces]="bounces() || undefined"
|
|
25
|
+
[attr.scroll-bar-enable]="scrollBar() || undefined"
|
|
26
|
+
[attr.enable-nested-scroll]="nestedScroll() || undefined"
|
|
27
|
+
[attr.upper-threshold]="upperThreshold()"
|
|
28
|
+
[attr.lower-threshold]="lowerThreshold()"
|
|
29
|
+
[class]="scrollClass()"
|
|
30
|
+
[style]="scrollStyle()"
|
|
31
|
+
(bindscrolltoupper)="scrolledToStart.emit()"
|
|
32
|
+
(bindscrolltolower)="scrolledToEnd.emit()"
|
|
33
|
+
>
|
|
34
|
+
<view [class]="contentClass()">
|
|
35
|
+
<ng-content />
|
|
36
|
+
</view>
|
|
37
|
+
</scroll-view>
|
|
38
|
+
`,
|
|
39
|
+
})
|
|
40
|
+
export class UiScrollArea {
|
|
41
|
+
readonly orientation = input<'vertical' | 'horizontal'>('vertical');
|
|
42
|
+
readonly height = input<string>('');
|
|
43
|
+
readonly bounces = input(true);
|
|
44
|
+
readonly scrollBar = input(true);
|
|
45
|
+
readonly nestedScroll = input(false);
|
|
46
|
+
readonly upperThreshold = input(0);
|
|
47
|
+
readonly lowerThreshold = input(0);
|
|
48
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
49
|
+
// Trailing underscore avoids a naming collision with the protected
|
|
50
|
+
// `contentClass` computed property below; the public alias 'contentClass'
|
|
51
|
+
// keeps the template API clean for consumers.
|
|
52
|
+
readonly contentClass_ = input<string>('', { alias: 'contentClass' });
|
|
53
|
+
|
|
54
|
+
readonly scrolledToStart = output<void>();
|
|
55
|
+
readonly scrolledToEnd = output<void>();
|
|
56
|
+
|
|
57
|
+
protected readonly scrollClass = computed(() =>
|
|
58
|
+
cn('w-full', this.userClass()),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Explicit height is required for scroll-view to scroll in Lynx
|
|
62
|
+
protected readonly scrollStyle = computed(() => {
|
|
63
|
+
const h = this.height();
|
|
64
|
+
return h ? `height: ${h};` : '';
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
protected readonly contentClass = computed(() =>
|
|
68
|
+
cn(
|
|
69
|
+
this.orientation() === 'vertical' ? 'flex-col flex' : 'flex-row flex',
|
|
70
|
+
this.contentClass_(),
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UiSelect, UiSelectItem } from './select';
|