@blotch/dolan 22.2.1 → 22.2.3
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 +6 -2
- package/dist/commands/add.js +32 -10
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/add.test.js +129 -9
- package/dist/commands/add.test.js.map +1 -1
- package/dist/commands/diff.d.ts +2 -1
- package/dist/commands/diff.js +38 -25
- package/dist/commands/diff.js.map +1 -1
- package/dist/commands/diff.test.js +72 -11
- package/dist/commands/diff.test.js.map +1 -1
- package/dist/commands/doctor.js +41 -27
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.js +93 -9
- package/dist/commands/doctor.test.js.map +1 -1
- package/dist/commands/eject.js +7 -0
- package/dist/commands/eject.js.map +1 -1
- package/dist/commands/eject.test.js +39 -0
- package/dist/commands/eject.test.js.map +1 -1
- package/dist/commands/info.js +3 -2
- package/dist/commands/info.js.map +1 -1
- package/dist/commands/init.d.ts +37 -1
- package/dist/commands/init.js +149 -40
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/init.test.js +285 -26
- package/dist/commands/init.test.js.map +1 -1
- package/dist/commands/install-packages.d.ts +6 -0
- package/dist/commands/install-packages.js +20 -0
- package/dist/commands/install-packages.js.map +1 -0
- package/dist/commands/install-packages.test.d.ts +1 -0
- package/dist/commands/install-packages.test.js +55 -0
- package/dist/commands/install-packages.test.js.map +1 -0
- package/dist/commands/list.js +21 -14
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/list.test.js +62 -5
- package/dist/commands/list.test.js.map +1 -1
- package/dist/commands/outdated.js +11 -10
- package/dist/commands/outdated.js.map +1 -1
- package/dist/commands/outdated.test.js +20 -5
- package/dist/commands/outdated.test.js.map +1 -1
- package/dist/commands/remove.d.ts +4 -0
- package/dist/commands/remove.js +17 -4
- package/dist/commands/remove.js.map +1 -1
- package/dist/commands/remove.test.js +53 -2
- package/dist/commands/remove.test.js.map +1 -1
- package/dist/commands/update.d.ts +6 -0
- package/dist/commands/update.js +59 -24
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/update.test.js +273 -31
- package/dist/commands/update.test.js.map +1 -1
- package/dist/index.js +10 -3
- package/dist/index.js.map +1 -1
- package/dist/registry.d.ts +8 -0
- package/dist/registry.js +32 -24
- package/dist/registry.js.map +1 -1
- package/dist/registry.test.js +76 -37
- package/dist/registry.test.js.map +1 -1
- package/dist/testing/set-input-signal.d.ts +13 -0
- package/dist/testing/set-input-signal.js +20 -0
- package/dist/testing/set-input-signal.js.map +1 -0
- package/dist/testing/set-input-signal.test.d.ts +1 -0
- package/dist/testing/set-input-signal.test.js +20 -0
- package/dist/testing/set-input-signal.test.js.map +1 -0
- package/dist/ui/components/action-sheet/action-sheet.test.ts +48 -1
- package/dist/ui/components/action-sheet/action-sheet.ts +37 -9
- package/dist/ui/components/alert-dialog/alert-dialog.test.ts +49 -2
- package/dist/ui/components/alert-dialog/alert-dialog.ts +43 -12
- package/dist/ui/components/area-chart/area-chart-render.test.ts +52 -110
- package/dist/ui/components/area-chart/area-chart.test.ts +59 -146
- package/dist/ui/components/area-chart/area-chart.ts +100 -315
- package/dist/ui/components/area-chart/index.ts +2 -3
- package/dist/ui/components/bar-chart/bar-chart-render.test.ts +47 -163
- package/dist/ui/components/bar-chart/bar-chart.test.ts +82 -58
- package/dist/ui/components/bar-chart/bar-chart.ts +140 -294
- package/dist/ui/components/bar-chart/index.ts +3 -3
- package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +191 -9
- package/dist/ui/components/bottom-sheet/bottom-sheet.ts +136 -20
- package/dist/ui/components/button/button-render.test.ts +19 -0
- package/dist/ui/components/button/button.test.ts +2 -15
- package/dist/ui/components/button/button.ts +18 -2
- package/dist/ui/components/button-group/button-group.test.ts +2 -21
- package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +48 -163
- package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +107 -85
- package/dist/ui/components/candlestick-chart/candlestick-chart.ts +150 -323
- package/dist/ui/components/candlestick-chart/index.ts +3 -4
- package/dist/ui/components/chart/chart-focus.test.ts +132 -0
- package/dist/ui/components/chart/chart-focus.ts +115 -0
- package/dist/ui/components/chart/chart-presets.test.ts +139 -0
- package/dist/ui/components/chart/chart-presets.ts +271 -0
- package/dist/ui/components/chart/chart-render.test.ts +730 -0
- package/dist/ui/components/chart/chart-scene.test.ts +589 -0
- package/dist/ui/components/chart/chart-scene.ts +483 -0
- package/dist/ui/components/chart/chart-theme.test.ts +85 -0
- package/dist/ui/components/chart/chart-theme.ts +198 -0
- package/dist/ui/components/chart/chart-zoom.test.ts +159 -0
- package/dist/ui/components/chart/chart-zoom.ts +162 -0
- package/dist/ui/components/chart/chart.ts +921 -0
- package/dist/ui/components/chart/index.ts +24 -0
- package/dist/ui/components/dialog/dialog.test.ts +49 -2
- package/dist/ui/components/dialog/dialog.ts +43 -12
- package/dist/ui/components/icon/icon-theme.test.ts +133 -0
- package/dist/ui/components/icon/icon-theme.ts +204 -0
- package/dist/ui/components/icon/icon.test.ts +51 -2
- package/dist/ui/components/icon/icon.ts +38 -21
- package/dist/ui/components/line-chart/index.ts +2 -3
- package/dist/ui/components/line-chart/line-chart-render.test.ts +69 -107
- package/dist/ui/components/line-chart/line-chart.test.ts +63 -67
- package/dist/ui/components/line-chart/line-chart.ts +83 -241
- package/dist/ui/components/list/list.test.ts +69 -2
- package/dist/ui/components/list/list.ts +21 -15
- package/dist/ui/components/nav-drawer/nav-drawer.test.ts +80 -1
- package/dist/ui/components/nav-drawer/nav-drawer.ts +66 -11
- package/dist/ui/components/pagination/pagination.test.ts +62 -0
- package/dist/ui/components/pagination/pagination.ts +55 -8
- package/dist/ui/components/scatter-chart/index.ts +2 -3
- package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +51 -144
- package/dist/ui/components/scatter-chart/scatter-chart.test.ts +57 -47
- package/dist/ui/components/scatter-chart/scatter-chart.ts +104 -250
- package/dist/ui/components/scroll-area/scroll-area.test.ts +37 -2
- package/dist/ui/components/scroll-area/scroll-area.ts +8 -8
- package/dist/ui/components/spinner/spinner.test.ts +27 -0
- package/dist/ui/components/spinner/spinner.ts +33 -23
- package/dist/ui/components/toast/toast.test.ts +37 -0
- package/dist/ui/components/toast/toast.ts +20 -4
- package/dist/ui/theme/chart-colors.test.ts +30 -0
- package/dist/ui/theme/dark.css +8 -0
- package/dist/ui/theme/default.css +8 -0
- package/dist/ui/theme/icon-colors.test.ts +30 -0
- package/dist/ui/theme/tailwind-plugin.ts +10 -1
- package/dist/utils/installed.d.ts +17 -0
- package/dist/utils/installed.js +27 -0
- package/dist/utils/installed.js.map +1 -0
- package/dist/utils/installed.test.d.ts +1 -0
- package/dist/utils/installed.test.js +41 -0
- package/dist/utils/installed.test.js.map +1 -0
- package/dist/utils/interactive.d.ts +9 -0
- package/dist/utils/interactive.js +10 -0
- package/dist/utils/interactive.js.map +1 -0
- package/dist/utils/interactive.test.d.ts +1 -0
- package/dist/utils/interactive.test.js +24 -0
- package/dist/utils/interactive.test.js.map +1 -0
- package/dist/utils/packages.d.ts +36 -0
- package/dist/utils/packages.js +70 -0
- package/dist/utils/packages.js.map +1 -0
- package/dist/utils/packages.test.d.ts +1 -0
- package/dist/utils/packages.test.js +89 -0
- package/dist/utils/packages.test.js.map +1 -0
- package/dist/utils/resolve-paths.d.ts +17 -1
- package/dist/utils/resolve-paths.js +23 -3
- package/dist/utils/resolve-paths.js.map +1 -1
- package/dist/utils/resolve-paths.test.js +34 -2
- package/dist/utils/resolve-paths.test.js.map +1 -1
- package/package.json +5 -1
- package/dist/ui/components/cartesian-chart/cartesian-chart-render.test.ts +0 -188
- package/dist/ui/components/cartesian-chart/cartesian-chart.test.ts +0 -549
- package/dist/ui/components/cartesian-chart/cartesian-chart.ts +0 -1140
- package/dist/ui/components/cartesian-chart/index.ts +0 -18
|
@@ -2,272 +2,114 @@ import {
|
|
|
2
2
|
Component,
|
|
3
3
|
ViewEncapsulation,
|
|
4
4
|
computed,
|
|
5
|
-
inject,
|
|
6
5
|
input,
|
|
7
6
|
output,
|
|
8
7
|
} from '@angular/core';
|
|
9
|
-
import {
|
|
8
|
+
import { d3Curve, defineChart, dot, lineY } from '@tanstack/charts/universal';
|
|
9
|
+
import type { ChartMark, ChartPoint } from '@tanstack/charts/types';
|
|
10
|
+
import { curveMonotoneX } from 'd3-shape';
|
|
10
11
|
|
|
12
|
+
import { UiChart, tooltip } from '../chart/chart';
|
|
11
13
|
import {
|
|
12
|
-
type
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} from '../
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
angle: number;
|
|
30
|
-
};
|
|
14
|
+
type CartesianPresetOptions,
|
|
15
|
+
PRESET_CHART_BINDINGS,
|
|
16
|
+
UiCartesianPreset,
|
|
17
|
+
type XYDatum,
|
|
18
|
+
presetAxes,
|
|
19
|
+
} from '../chart/chart-presets';
|
|
20
|
+
|
|
21
|
+
export type LineChartOptions = {
|
|
22
|
+
readonly data: readonly XYDatum[];
|
|
23
|
+
/** Any CSS color; theme tokens like `var(--primary)` follow dark mode. */
|
|
24
|
+
readonly color?: string;
|
|
25
|
+
readonly strokeWidth?: number;
|
|
26
|
+
readonly dotRadius?: number;
|
|
27
|
+
readonly showDots?: boolean;
|
|
28
|
+
/** Draws a monotone curve through the points instead of straight segments. */
|
|
29
|
+
readonly smooth?: boolean;
|
|
30
|
+
} & CartesianPresetOptions;
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* as a thin `<view>` rotated about its start point (see `UiLineSeries`).
|
|
33
|
+
* The definition `<ui-line-chart>` renders, for when you want to start from
|
|
34
|
+
* the preset and add marks of your own:
|
|
36
35
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* never emit a degenerate 0-width rotated view.
|
|
36
|
+
* ```ts
|
|
37
|
+
* const base = lineChartDefinition({ data });
|
|
38
|
+
* const withTarget = defineChart({ ...base, marks: [...base.marks, ruleY([80])] });
|
|
39
|
+
* ```
|
|
42
40
|
*/
|
|
43
|
-
export const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
41
|
+
export const lineChartDefinition = (options: LineChartOptions) => {
|
|
42
|
+
const {
|
|
43
|
+
data,
|
|
44
|
+
color = 'var(--primary)',
|
|
45
|
+
strokeWidth = 2,
|
|
46
|
+
dotRadius = 3,
|
|
47
|
+
showDots = true,
|
|
48
|
+
smooth = false,
|
|
49
|
+
} = options;
|
|
50
|
+
const curve = smooth ? d3Curve(curveMonotoneX) : undefined;
|
|
51
|
+
const marks: ChartMark<XYDatum, any, any, any, any, any, any>[] = [
|
|
52
|
+
lineY(data, { x: 'x', y: 'y', stroke: color, strokeWidth, curve }),
|
|
53
|
+
];
|
|
54
|
+
if (showDots && dotRadius > 0) {
|
|
55
|
+
marks.push(dot(data, { x: 'x', y: 'y', r: dotRadius, fill: color }));
|
|
56
56
|
}
|
|
57
|
-
return
|
|
57
|
+
return defineChart({
|
|
58
|
+
marks,
|
|
59
|
+
scales: presetAxes(options, {
|
|
60
|
+
x: data.map((d) => d.x),
|
|
61
|
+
y: data.map((d) => d.y),
|
|
62
|
+
}),
|
|
63
|
+
tooltip: options.tooltip === false ? false : tooltip,
|
|
64
|
+
});
|
|
58
65
|
};
|
|
59
66
|
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
// Rendering helpers (mirror the equivalents in cartesian-chart.ts — kept local
|
|
62
|
-
// so this module stays self-contained when scaffolded by `dolan add`)
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Formats a pixel value for an inline style, avoiding scientific notation.
|
|
67
|
-
*/
|
|
68
|
-
const px = (n: number): string => `${n.toFixed(2)}px`;
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Default axis label formatter — plain JS only (no `Intl`, absent in Lynx).
|
|
72
|
-
*/
|
|
73
|
-
const defaultTickFormat = (value: number): string =>
|
|
74
|
-
String(Math.round(value * 100) / 100);
|
|
75
|
-
|
|
76
67
|
/**
|
|
77
|
-
* A single line
|
|
78
|
-
*
|
|
79
|
-
*
|
|
68
|
+
* A single-series line chart. For several series, a reference line or any
|
|
69
|
+
* other mark, build a definition and render it with `<ui-chart>`, starting
|
|
70
|
+
* from {@link lineChartDefinition} if you like.
|
|
80
71
|
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* itself must be the plot-sized containing block — it fills the plot area (its
|
|
85
|
-
* direct parent, a definite-size box). Sizing an *inner* view instead would fail
|
|
86
|
-
* because a content-sized host of absolute-only children collapses to 0×0.
|
|
87
|
-
*/
|
|
88
|
-
@Component({
|
|
89
|
-
selector: 'ui-line-series',
|
|
90
|
-
standalone: true,
|
|
91
|
-
imports: [LYNX_ELEMENTS],
|
|
92
|
-
encapsulation: ViewEncapsulation.None,
|
|
93
|
-
host: {
|
|
94
|
-
style:
|
|
95
|
-
'position: absolute; left: 0px; top: 0px; width: 100%; height: 100%;',
|
|
96
|
-
'[class]': 'userClass()',
|
|
97
|
-
},
|
|
98
|
-
template: `
|
|
99
|
-
<!-- Segments render before dots so each dot paints on top of the line
|
|
100
|
-
(Lynx paint order follows source order; z-index is unnecessary). -->
|
|
101
|
-
@for (segment of segments(); track $index) {
|
|
102
|
-
<view [style]="segment.style"></view>
|
|
103
|
-
}
|
|
104
|
-
@for (dot of dots(); track $index) {
|
|
105
|
-
<view [style]="dot.style" (bindtap)="onPointTap(dot.point)"></view>
|
|
106
|
-
}
|
|
107
|
-
`,
|
|
108
|
-
})
|
|
109
|
-
export class UiLineSeries {
|
|
110
|
-
readonly #chart = inject(UiCartesianChart);
|
|
111
|
-
|
|
112
|
-
readonly data = input.required<ChartPoint[]>();
|
|
113
|
-
readonly color = input('var(--primary)');
|
|
114
|
-
readonly strokeWidth = input(2);
|
|
115
|
-
readonly dotRadius = input(3);
|
|
116
|
-
readonly showDots = input(true);
|
|
117
|
-
/** Draw a smooth (monotone-cubic) curve through the points instead of straight
|
|
118
|
-
* segments. Dots still sit on the raw data points. */
|
|
119
|
-
readonly smooth = input(false);
|
|
120
|
-
readonly userClass = input<string>('', { alias: 'class' });
|
|
121
|
-
|
|
122
|
-
readonly pointTap = output<ChartPoint>();
|
|
123
|
-
|
|
124
|
-
// Project each data point into plot-local pixel space via the shared scales.
|
|
125
|
-
// Reading the chart's scale signals here makes this recompute whenever the
|
|
126
|
-
// domain or plot size changes.
|
|
127
|
-
readonly #pixelPoints = computed(() => {
|
|
128
|
-
const scaleX = this.#chart.xScale();
|
|
129
|
-
const scaleY = this.#chart.yScale();
|
|
130
|
-
return this.data().map((point) => ({
|
|
131
|
-
x: scaleX(point.x),
|
|
132
|
-
y: scaleY(point.y),
|
|
133
|
-
point,
|
|
134
|
-
}));
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
protected readonly segments = computed(() => {
|
|
138
|
-
const stroke = this.strokeWidth();
|
|
139
|
-
const fill = this.color();
|
|
140
|
-
// Smooth mode replaces the raw polyline with a dense monotone-cubic
|
|
141
|
-
// resampling; `computeLineSegments` then rasterizes it identically — the
|
|
142
|
-
// curve is just made of many more, shorter rotated views.
|
|
143
|
-
const pixels = this.#pixelPoints();
|
|
144
|
-
const line = this.smooth() ? sampleSmoothLine(pixels) : pixels;
|
|
145
|
-
return computeLineSegments(line).map((s) => ({
|
|
146
|
-
style:
|
|
147
|
-
`position: absolute; left: ${px(s.x)}; top: ${px(s.y - stroke / 2)}; ` +
|
|
148
|
-
`width: ${px(s.length)}; height: ${px(stroke)}; ` +
|
|
149
|
-
`background-color: ${fill}; border-radius: ${px(stroke / 2)}; ` +
|
|
150
|
-
`transform-origin: 0 50%; transform: rotate(${s.angle.toFixed(2)}deg);`,
|
|
151
|
-
}));
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
protected readonly dots = computed(() => {
|
|
155
|
-
if (!this.showDots()) return [];
|
|
156
|
-
const radius = this.dotRadius();
|
|
157
|
-
const fill = this.color();
|
|
158
|
-
return this.#pixelPoints().map((p) => ({
|
|
159
|
-
point: p.point,
|
|
160
|
-
style:
|
|
161
|
-
`position: absolute; left: ${px(p.x - radius)}; top: ${px(p.y - radius)}; ` +
|
|
162
|
-
`width: ${px(radius * 2)}; height: ${px(radius * 2)}; ` +
|
|
163
|
-
`background-color: ${fill}; border-radius: ${px(radius)};`,
|
|
164
|
-
}));
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
protected onPointTap(point: ChartPoint): void {
|
|
168
|
-
this.pointTap.emit(point);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* A ready-to-use line chart for the common single-series case. Composes a
|
|
174
|
-
* `<ui-cartesian-chart>` with one `<ui-line-series>` and auto-derives rounded
|
|
175
|
-
* axis domains + ticks from the data. For multiple series or full control over
|
|
176
|
-
* the axes, use `<ui-cartesian-chart>` with `<ui-line-series>` children directly.
|
|
72
|
+
* ```html
|
|
73
|
+
* <ui-line-chart [data]="revenue" [smooth]="true" yAxisLabel="Revenue" />
|
|
74
|
+
* ```
|
|
177
75
|
*/
|
|
178
76
|
@Component({
|
|
179
77
|
selector: 'ui-line-chart',
|
|
180
|
-
|
|
181
|
-
imports: [LYNX_ELEMENTS, UiCartesianChart, UiLineSeries],
|
|
78
|
+
imports: [UiChart],
|
|
182
79
|
encapsulation: ViewEncapsulation.None,
|
|
183
80
|
template: `
|
|
184
|
-
<ui-
|
|
185
|
-
[
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
[width]="width()"
|
|
190
|
-
[height]="height()"
|
|
191
|
-
[tickCount]="tickCount()"
|
|
192
|
-
[showXGrid]="showXGrid()"
|
|
193
|
-
[xAxisLabel]="xAxisLabel()"
|
|
194
|
-
[yAxisLabel]="yAxisLabel()"
|
|
195
|
-
[padding]="padding()"
|
|
196
|
-
[xTickFormat]="xTickFormat()"
|
|
197
|
-
[yTickFormat]="yTickFormat()"
|
|
198
|
-
[zoomable]="zoomable()"
|
|
199
|
-
[zoomAxes]="zoomAxes()"
|
|
200
|
-
[showZoomControls]="showZoomControls()"
|
|
201
|
-
[minZoom]="minZoom()"
|
|
202
|
-
[maxZoom]="maxZoom()"
|
|
203
|
-
[zoomStep]="zoomStep()"
|
|
204
|
-
[class]="userClass()"
|
|
205
|
-
>
|
|
206
|
-
<ui-line-series
|
|
207
|
-
[data]="data()"
|
|
208
|
-
[color]="color()"
|
|
209
|
-
[strokeWidth]="strokeWidth()"
|
|
210
|
-
[dotRadius]="dotRadius()"
|
|
211
|
-
[showDots]="showDots()"
|
|
212
|
-
[smooth]="smooth()"
|
|
213
|
-
(pointTap)="pointTap.emit($event)"
|
|
214
|
-
/>
|
|
215
|
-
</ui-cartesian-chart>
|
|
81
|
+
<ui-chart
|
|
82
|
+
[definition]="definition()"
|
|
83
|
+
${PRESET_CHART_BINDINGS}
|
|
84
|
+
(pointSelect)="onSelect($event)"
|
|
85
|
+
/>
|
|
216
86
|
`,
|
|
217
87
|
})
|
|
218
|
-
export class UiLineChart {
|
|
219
|
-
readonly data = input.required<
|
|
88
|
+
export class UiLineChart extends UiCartesianPreset<XYDatum> {
|
|
89
|
+
readonly data = input.required<readonly XYDatum[]>();
|
|
220
90
|
readonly color = input('var(--primary)');
|
|
221
|
-
readonly width = input(320);
|
|
222
|
-
readonly height = input(200);
|
|
223
|
-
readonly tickCount = input(5);
|
|
224
91
|
readonly strokeWidth = input(2);
|
|
225
92
|
readonly dotRadius = input(3);
|
|
226
93
|
readonly showDots = input(true);
|
|
227
|
-
/** Draw a smooth (monotone-cubic) curve through the points. */
|
|
228
94
|
readonly smooth = input(false);
|
|
229
|
-
/** Draw vertical gridlines at each x tick. */
|
|
230
|
-
readonly showXGrid = input(false);
|
|
231
|
-
/** Title for the x-axis (centered below the tick labels). */
|
|
232
|
-
readonly xAxisLabel = input<string>('');
|
|
233
|
-
/** Title for the y-axis (rotated in the left gutter). */
|
|
234
|
-
readonly yAxisLabel = input<string>('');
|
|
235
|
-
/** Inner inset of the plot area, each side a fraction (0–1) — see {@link ChartPadding}. */
|
|
236
|
-
readonly padding = input<ChartPadding>({});
|
|
237
|
-
/** Force the x-axis lower/upper bound instead of deriving it from the data —
|
|
238
|
-
* e.g. `xMin={0}` to anchor the origin. Omitted bounds fall back to the data. */
|
|
239
|
-
readonly xMin = input<number | undefined>(undefined);
|
|
240
|
-
readonly xMax = input<number | undefined>(undefined);
|
|
241
|
-
/** Force the y-axis lower/upper bound (e.g. `yMin={0}` to start at zero). */
|
|
242
|
-
readonly yMin = input<number | undefined>(undefined);
|
|
243
|
-
readonly yMax = input<number | undefined>(undefined);
|
|
244
|
-
readonly xTickFormat = input<(value: number) => string>(defaultTickFormat);
|
|
245
|
-
readonly yTickFormat = input<(value: number) => string>(defaultTickFormat);
|
|
246
|
-
readonly userClass = input<string>('', { alias: 'class' });
|
|
247
95
|
|
|
248
|
-
|
|
249
|
-
readonly
|
|
250
|
-
|
|
251
|
-
readonly
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
protected readonly yAxis = computed(() => {
|
|
268
|
-
const ys = this.data().map((p) => p.y);
|
|
269
|
-
const lo = this.yMin() ?? Math.min(...ys);
|
|
270
|
-
const hi = this.yMax() ?? Math.max(...ys);
|
|
271
|
-
return niceScale(lo, hi, this.tickCount());
|
|
272
|
-
});
|
|
96
|
+
/** The tapped point's datum. */
|
|
97
|
+
readonly pointTap = output<XYDatum>();
|
|
98
|
+
|
|
99
|
+
protected readonly definition = computed(() =>
|
|
100
|
+
lineChartDefinition({
|
|
101
|
+
...this.commonOptions(),
|
|
102
|
+
data: this.data(),
|
|
103
|
+
color: this.color(),
|
|
104
|
+
strokeWidth: this.strokeWidth(),
|
|
105
|
+
dotRadius: this.dotRadius(),
|
|
106
|
+
showDots: this.showDots(),
|
|
107
|
+
smooth: this.smooth(),
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
protected onSelect(point: ChartPoint | null): void {
|
|
112
|
+
const datum = this.datumOf(point);
|
|
113
|
+
if (datum) this.pointTap.emit(datum);
|
|
114
|
+
}
|
|
273
115
|
}
|
|
@@ -1,17 +1,35 @@
|
|
|
1
1
|
import { Component } from '@angular/core';
|
|
2
2
|
import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
|
|
3
3
|
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
4
|
-
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
5
|
import { setInputSignal } from '../../../test-utils/set-input-signal';
|
|
6
6
|
import { UiList } from './list';
|
|
7
7
|
|
|
8
8
|
@Component({
|
|
9
|
-
standalone: true,
|
|
10
9
|
imports: [UiList, LYNX_ELEMENTS],
|
|
11
10
|
template: `<ui-list><text>Item</text></ui-list>`,
|
|
12
11
|
})
|
|
13
12
|
class ListHost {}
|
|
14
13
|
|
|
14
|
+
/**
|
|
15
|
+
* The values each boolean reached Lynx with, through `__SetAttribute`.
|
|
16
|
+
*/
|
|
17
|
+
const watchAttributes = () => {
|
|
18
|
+
const mainThread = (
|
|
19
|
+
globalThis as unknown as {
|
|
20
|
+
lynxTestingEnv: { mainThread: { globalThis: Record<string, unknown> } };
|
|
21
|
+
}
|
|
22
|
+
).lynxTestingEnv.mainThread.globalThis;
|
|
23
|
+
const setAttribute = vi.spyOn(mainThread, '__SetAttribute' as never);
|
|
24
|
+
return {
|
|
25
|
+
last: (name: string): unknown =>
|
|
26
|
+
(setAttribute.mock.calls as unknown[][])
|
|
27
|
+
.filter((call) => call[1] === name)
|
|
28
|
+
.at(-1)?.[2],
|
|
29
|
+
restore: () => setAttribute.mockRestore(),
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
15
33
|
describe('UiList', () => {
|
|
16
34
|
it('renders projected content', async () => {
|
|
17
35
|
// Lynx `<list>` is virtualized — items are only added to the native tree by
|
|
@@ -32,4 +50,53 @@ describe('UiList', () => {
|
|
|
32
50
|
const list = container.querySelector('list');
|
|
33
51
|
expect(list?.getAttribute('style')).toContain('height: 400px');
|
|
34
52
|
});
|
|
53
|
+
|
|
54
|
+
it('sets the gaps as list styles, which Lynx reads, not attributes', async () => {
|
|
55
|
+
// jsdom drops style properties it doesn't know, so the gaps never show in
|
|
56
|
+
// the style attribute: watch what reaches Lynx's inline style API instead.
|
|
57
|
+
const mainThread = (
|
|
58
|
+
globalThis as unknown as {
|
|
59
|
+
lynxTestingEnv: { mainThread: { globalThis: Record<string, unknown> } };
|
|
60
|
+
}
|
|
61
|
+
).lynxTestingEnv.mainThread.globalThis;
|
|
62
|
+
const addInlineStyle = vi.spyOn(mainThread, '__AddInlineStyle' as never);
|
|
63
|
+
const { container, componentRef } = await render(UiList);
|
|
64
|
+
const list = componentRef.instance as UiList;
|
|
65
|
+
setInputSignal(list.mainAxisGap, '8px');
|
|
66
|
+
setInputSignal(list.crossAxisGap, '12px');
|
|
67
|
+
await waitForUpdate();
|
|
68
|
+
|
|
69
|
+
const element = container.querySelector('list');
|
|
70
|
+
expect(addInlineStyle).toHaveBeenCalledWith(
|
|
71
|
+
element,
|
|
72
|
+
'list-main-axis-gap',
|
|
73
|
+
'8px',
|
|
74
|
+
);
|
|
75
|
+
expect(addInlineStyle).toHaveBeenCalledWith(
|
|
76
|
+
element,
|
|
77
|
+
'list-cross-axis-gap',
|
|
78
|
+
'12px',
|
|
79
|
+
);
|
|
80
|
+
expect(element?.getAttribute('list-main-axis-gap')).toBeNull();
|
|
81
|
+
addInlineStyle.mockRestore();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('hands Lynx real booleans, so the defaults that are on can be turned off', async () => {
|
|
85
|
+
// A false used to remove the attribute, which leaves Lynx's default: on.
|
|
86
|
+
const attributes = watchAttributes();
|
|
87
|
+
const { componentRef } = await render(UiList);
|
|
88
|
+
const inst = componentRef.instance as UiList;
|
|
89
|
+
expect(attributes.last('bounces')).toBe(true);
|
|
90
|
+
expect(attributes.last('scroll-bar-enable')).toBe(true);
|
|
91
|
+
setInputSignal(inst.bounces, false);
|
|
92
|
+
setInputSignal(inst.scrollBar, false);
|
|
93
|
+
setInputSignal(inst.nestedScroll, true);
|
|
94
|
+
setInputSignal(inst.sticky, true);
|
|
95
|
+
await waitForUpdate();
|
|
96
|
+
expect(attributes.last('bounces')).toBe(false);
|
|
97
|
+
expect(attributes.last('scroll-bar-enable')).toBe(false);
|
|
98
|
+
expect(attributes.last('enable-nested-scroll')).toBe(true);
|
|
99
|
+
expect(attributes.last('sticky')).toBe(true);
|
|
100
|
+
attributes.restore();
|
|
101
|
+
});
|
|
35
102
|
});
|
|
@@ -11,25 +11,24 @@ import { cn } from '../../utils/cn';
|
|
|
11
11
|
|
|
12
12
|
@Component({
|
|
13
13
|
selector: 'ui-list',
|
|
14
|
-
standalone: true,
|
|
15
14
|
imports: [LYNX_ELEMENTS],
|
|
16
15
|
encapsulation: ViewEncapsulation.None,
|
|
17
|
-
//
|
|
18
|
-
// false
|
|
19
|
-
// attribute
|
|
16
|
+
// The booleans are property bindings to <list>'s inputs, which hand Lynx a
|
|
17
|
+
// real `false`. They used to be `[attr.x]="x() || undefined"`, which removed
|
|
18
|
+
// the attribute when false, and a missing attribute means Lynx's default: on,
|
|
19
|
+
// for `bounces` and `scroll-bar-enable`. So neither could be turned off.
|
|
20
|
+
// (`[attr.x]` would send the string "false" instead.)
|
|
20
21
|
template: `
|
|
21
22
|
<list
|
|
22
23
|
[attr.list-type]="listType()"
|
|
23
24
|
[attr.span-count]="spanCount()"
|
|
24
25
|
[attr.scroll-orientation]="orientation()"
|
|
25
|
-
[
|
|
26
|
-
[
|
|
27
|
-
[
|
|
28
|
-
[
|
|
26
|
+
[bounces]="bounces()"
|
|
27
|
+
[scroll-bar-enable]="scrollBar()"
|
|
28
|
+
[enable-nested-scroll]="nestedScroll()"
|
|
29
|
+
[sticky]="sticky()"
|
|
29
30
|
[attr.sticky-offset]="stickyOffset()"
|
|
30
31
|
[attr.preload-buffer-count]="preloadBufferCount()"
|
|
31
|
-
[attr.list-main-axis-gap]="mainAxisGap()"
|
|
32
|
-
[attr.list-cross-axis-gap]="crossAxisGap()"
|
|
33
32
|
[attr.lower-threshold-item-count]="lowerThresholdItemCount()"
|
|
34
33
|
[attr.upper-threshold-item-count]="upperThresholdItemCount()"
|
|
35
34
|
[class]="listClass()"
|
|
@@ -63,9 +62,16 @@ export class UiList {
|
|
|
63
62
|
|
|
64
63
|
protected readonly listClass = computed(() => cn('w-full', this.userClass()));
|
|
65
64
|
|
|
66
|
-
// Explicit height is required for list to scroll in Lynx
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
// Explicit height is required for list to scroll in Lynx. The gaps go here
|
|
66
|
+
// too: they're CSS properties that Lynx's list reads from its style, and as
|
|
67
|
+
// attributes they did nothing.
|
|
68
|
+
protected readonly listStyle = computed(() =>
|
|
69
|
+
[
|
|
70
|
+
this.height() && `height: ${this.height()};`,
|
|
71
|
+
this.mainAxisGap() && `list-main-axis-gap: ${this.mainAxisGap()};`,
|
|
72
|
+
this.crossAxisGap() && `list-cross-axis-gap: ${this.crossAxisGap()};`,
|
|
73
|
+
]
|
|
74
|
+
.filter(Boolean)
|
|
75
|
+
.join(' '),
|
|
76
|
+
);
|
|
71
77
|
}
|
|
@@ -2,7 +2,9 @@ import { Component, signal } from '@angular/core';
|
|
|
2
2
|
import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
|
|
3
3
|
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
4
4
|
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { DURATION } from '../../utils/animate';
|
|
5
6
|
import { setInputSignal } from '../../../test-utils/set-input-signal';
|
|
7
|
+
import { UI_ICON_COLOR } from '../icon/icon-theme';
|
|
6
8
|
import {
|
|
7
9
|
UiNavDrawer,
|
|
8
10
|
UiNavDrawerContent,
|
|
@@ -13,7 +15,6 @@ import {
|
|
|
13
15
|
} from './nav-drawer';
|
|
14
16
|
|
|
15
17
|
@Component({
|
|
16
|
-
standalone: true,
|
|
17
18
|
imports: [
|
|
18
19
|
UiNavDrawer,
|
|
19
20
|
UiNavDrawerHeader,
|
|
@@ -39,6 +40,47 @@ class NavDrawerHost {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
describe('UiNavDrawer', () => {
|
|
43
|
+
it('stops its close timers when it is destroyed while closing', async () => {
|
|
44
|
+
const warn = vi.spyOn(console, 'warn');
|
|
45
|
+
const { componentRef, unmount } = await render(UiNavDrawer);
|
|
46
|
+
const inst = componentRef.instance as UiNavDrawer;
|
|
47
|
+
inst.open.set(true);
|
|
48
|
+
for (let i = 0; i < 5; i++) await waitForUpdate();
|
|
49
|
+
inst.open.set(false);
|
|
50
|
+
await waitForUpdate();
|
|
51
|
+
// Destroyed during the close animation, as an `@if` around the drawer
|
|
52
|
+
// would do. Its close timer (DURATION.normal + 20) used to fire anyway
|
|
53
|
+
// and emit `closed` from the destroyed output.
|
|
54
|
+
unmount();
|
|
55
|
+
await new Promise((r) => setTimeout(r, DURATION.normal + 50));
|
|
56
|
+
const warnings = warn.mock.calls.map(([message]) => String(message));
|
|
57
|
+
warn.mockRestore();
|
|
58
|
+
expect(warnings.filter((message) => message.includes('NG0953'))).toEqual(
|
|
59
|
+
[],
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('moves a screen reader into the panel and keeps it there while open', async () => {
|
|
64
|
+
const { componentRef, container } = await render(UiNavDrawer);
|
|
65
|
+
const inst = componentRef.instance as UiNavDrawer;
|
|
66
|
+
const panel = container.querySelector(
|
|
67
|
+
'[accessibility-exclusive-focus]',
|
|
68
|
+
) as Element & { invoke?: unknown };
|
|
69
|
+
expect(panel.getAttribute('accessibility-exclusive-focus')).toBe('false');
|
|
70
|
+
const invoke = vi.fn();
|
|
71
|
+
panel.invoke = invoke;
|
|
72
|
+
(inst as unknown as { backdropRef: unknown }).backdropRef = signal({
|
|
73
|
+
nativeElement: panel,
|
|
74
|
+
});
|
|
75
|
+
(inst as unknown as { panelRef: unknown }).panelRef = signal({
|
|
76
|
+
nativeElement: panel,
|
|
77
|
+
});
|
|
78
|
+
inst.open.set(true);
|
|
79
|
+
for (let i = 0; i < 5; i++) await waitForUpdate();
|
|
80
|
+
expect(invoke).toHaveBeenCalledWith('requestAccessibilityFocus');
|
|
81
|
+
expect(panel.getAttribute('accessibility-exclusive-focus')).toBe('true');
|
|
82
|
+
});
|
|
83
|
+
|
|
42
84
|
const patchRefs = (inst: UiNavDrawer, view: Element) => {
|
|
43
85
|
(inst as unknown as { backdropRef: unknown }).backdropRef = signal({
|
|
44
86
|
nativeElement: view,
|
|
@@ -118,12 +160,18 @@ describe('UiNavDrawer', () => {
|
|
|
118
160
|
it('open effect bails when backdrop/panel refs are undefined', async () => {
|
|
119
161
|
const { componentRef } = await render(UiNavDrawer);
|
|
120
162
|
const inst = componentRef.instance as UiNavDrawer;
|
|
163
|
+
const closed = vi.fn();
|
|
164
|
+
inst.closed.subscribe(closed);
|
|
121
165
|
inst.open.set(true);
|
|
122
166
|
await waitForUpdate();
|
|
123
167
|
await waitForUpdate();
|
|
124
168
|
inst.open.set(false);
|
|
125
169
|
await waitForUpdate();
|
|
170
|
+
// Wait out the close sequence, so #animateOut runs (and bails) while the
|
|
171
|
+
// component is still alive. Destroying it clears the sequence's timers.
|
|
172
|
+
await new Promise((r) => setTimeout(r, DURATION.normal + 50));
|
|
126
173
|
expect(inst.open()).toBe(false);
|
|
174
|
+
expect(closed).toHaveBeenCalledOnce();
|
|
127
175
|
});
|
|
128
176
|
});
|
|
129
177
|
|
|
@@ -216,6 +264,25 @@ describe('UiNavDrawerItem', () => {
|
|
|
216
264
|
expect(() => inst.onPressCancel()).not.toThrow();
|
|
217
265
|
});
|
|
218
266
|
|
|
267
|
+
it("destructive points the content's foreground at the destructive color", async () => {
|
|
268
|
+
const { item, itemResult } = await renderItem();
|
|
269
|
+
const view = () => itemResult.container.querySelector('view');
|
|
270
|
+
expect(view()?.getAttribute('class')).not.toContain('--foreground');
|
|
271
|
+
setInputSignal(item.variant, 'destructive');
|
|
272
|
+
await waitForUpdate();
|
|
273
|
+
expect(view()?.getAttribute('class')).toContain(
|
|
274
|
+
'[--foreground:var(--destructive)]',
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('gives projected icons the destructive color', async () => {
|
|
279
|
+
const { item, itemResult } = await renderItem();
|
|
280
|
+
const iconColor = itemResult.componentRef.injector.get(UI_ICON_COLOR);
|
|
281
|
+
expect(iconColor()).toBeUndefined();
|
|
282
|
+
setInputSignal(item.variant, 'destructive');
|
|
283
|
+
expect(iconColor()).toBe('var(--destructive)');
|
|
284
|
+
});
|
|
285
|
+
|
|
219
286
|
it('active item carries the accent background', async () => {
|
|
220
287
|
const { item, itemResult } = await renderItem();
|
|
221
288
|
setInputSignal(item.active, true);
|
|
@@ -242,6 +309,18 @@ describe('UiNavDrawerItem', () => {
|
|
|
242
309
|
});
|
|
243
310
|
|
|
244
311
|
describe('UiNavDrawerTrigger', () => {
|
|
312
|
+
it('is a labeled button for screen readers', async () => {
|
|
313
|
+
// Only the first render is checked: under JIT, the nested <ui-icon>'s
|
|
314
|
+
// static name="menu" never reaches its signal input, so its change
|
|
315
|
+
// detection throws NG0950 and later updates don't land.
|
|
316
|
+
const { container } = await render(UiNavDrawerTrigger);
|
|
317
|
+
const trigger = container.querySelector('[accessibility-element]');
|
|
318
|
+
expect(trigger?.getAttribute('accessibility-traits')).toBe('button');
|
|
319
|
+
expect(trigger?.getAttribute('accessibility-label')).toBe(
|
|
320
|
+
'Open navigation menu',
|
|
321
|
+
);
|
|
322
|
+
});
|
|
323
|
+
|
|
245
324
|
it('onTap emits pressed', async () => {
|
|
246
325
|
const { componentRef } = await render(UiNavDrawerTrigger);
|
|
247
326
|
const inst = componentRef.instance as unknown as UiNavDrawerTrigger & {
|