@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,349 @@
|
|
|
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
|
+
sampleSmoothLine,
|
|
17
|
+
} from '../cartesian-chart/cartesian-chart';
|
|
18
|
+
import { UiLineSeries } from '../line-chart/line-chart';
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Pure geometry (exported so it can be unit-tested without Angular)
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
/** One vertical fill column: a rectangle `height` px tall starting at (`x`, `top`). */
|
|
25
|
+
export type AreaColumn = {
|
|
26
|
+
x: number;
|
|
27
|
+
width: number;
|
|
28
|
+
top: number;
|
|
29
|
+
height: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Linearly interpolates the polyline's y at pixel `x`. `points` must be sorted
|
|
34
|
+
* ascending by x (as cartesian-chart data always is). Returns `null` when `x`
|
|
35
|
+
* falls outside the data's x-range, so the fill is only drawn *under the actual
|
|
36
|
+
* series* — never extrapolated flat past the first or last point.
|
|
37
|
+
*/
|
|
38
|
+
const interpolateY = (
|
|
39
|
+
points: readonly ChartPoint[],
|
|
40
|
+
x: number,
|
|
41
|
+
): number | null => {
|
|
42
|
+
const first = points[0];
|
|
43
|
+
const last = points[points.length - 1];
|
|
44
|
+
if (x < first.x || x > last.x) return null;
|
|
45
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
46
|
+
const p1 = points[i];
|
|
47
|
+
const p2 = points[i + 1];
|
|
48
|
+
if (x >= p1.x && x <= p2.x) {
|
|
49
|
+
const span = p2.x - p1.x;
|
|
50
|
+
if (span === 0) return p1.y; // duplicate x — avoid divide-by-zero
|
|
51
|
+
return p1.y + ((x - p1.x) / span) * (p2.y - p1.y);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/* v8 ignore next 1 */
|
|
55
|
+
return last.y; // defensive: x === last.x but the loop missed it
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Rasterizes the region between a polyline and a horizontal baseline into thin
|
|
60
|
+
* vertical `<view>` columns — the Lynx substitute for a filled area.
|
|
61
|
+
*
|
|
62
|
+
* Lynx has no filled-polygon primitive: `clip-path` supports no `polygon()` and
|
|
63
|
+
* its inline `<svg>` is unreliable, so an area is approximated as a run of
|
|
64
|
+
* adjacent rectangles, one per `stripWidth` band, each reaching from the
|
|
65
|
+
* interpolated line height down to `baseY`. The crisp line is drawn on top (see
|
|
66
|
+
* `UiAreaSeries`), which hides the columns' stepped upper edge. Sampling at each
|
|
67
|
+
* strip's center keeps that step within about `stripWidth / 2 × slope` px.
|
|
68
|
+
*
|
|
69
|
+
* Guards: fewer than two points, or a non-positive `plotWidth`/`stripWidth`,
|
|
70
|
+
* yield no columns; near-zero-height columns (line meeting the baseline) are
|
|
71
|
+
* skipped so we never emit a degenerate 0-height view.
|
|
72
|
+
*/
|
|
73
|
+
export const computeAreaColumns = (
|
|
74
|
+
points: readonly ChartPoint[],
|
|
75
|
+
baseY: number,
|
|
76
|
+
plotWidth: number,
|
|
77
|
+
stripWidth: number,
|
|
78
|
+
): AreaColumn[] => {
|
|
79
|
+
const columns: AreaColumn[] = [];
|
|
80
|
+
if (points.length < 2 || plotWidth <= 0 || stripWidth <= 0) return columns;
|
|
81
|
+
|
|
82
|
+
for (let x = 0; x < plotWidth; x += stripWidth) {
|
|
83
|
+
// Sample the line at the strip's center (clamped to the plot's right edge).
|
|
84
|
+
const center = Math.min(x + stripWidth / 2, plotWidth);
|
|
85
|
+
const lineY = interpolateY(points, center);
|
|
86
|
+
if (lineY === null) continue;
|
|
87
|
+
|
|
88
|
+
const top = Math.min(lineY, baseY);
|
|
89
|
+
const height = Math.abs(lineY - baseY);
|
|
90
|
+
if (height < 0.01) continue; // line rests on the baseline here — nothing to fill
|
|
91
|
+
|
|
92
|
+
// A hair of overlap (stripWidth + 1) closes sub-pixel seams between adjacent
|
|
93
|
+
// columns. Harmless: the shared `opacity` layer composites them once, so the
|
|
94
|
+
// overlaps never double-darken.
|
|
95
|
+
columns.push({ x, width: stripWidth + 1, top, height });
|
|
96
|
+
}
|
|
97
|
+
return columns;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Rendering helpers (mirror the equivalents in line-chart.ts — kept local so
|
|
102
|
+
// this module stays self-contained when scaffolded by `dolan add`)
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Formats a pixel value for an inline style, avoiding scientific notation.
|
|
107
|
+
*/
|
|
108
|
+
const px = (n: number): string => `${n.toFixed(2)}px`;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Default axis label formatter — plain JS only (no `Intl`, absent in Lynx).
|
|
112
|
+
*/
|
|
113
|
+
const defaultTickFormat = (value: number): string =>
|
|
114
|
+
String(Math.round(value * 100) / 100);
|
|
115
|
+
|
|
116
|
+
const clamp = (value: number, lo: number, hi: number): number =>
|
|
117
|
+
Math.max(lo, Math.min(hi, value));
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Width of each fill strip in px. Smaller means a smoother top edge but more
|
|
121
|
+
* native views (roughly `plotWidth / STRIP_WIDTH` per series); 3px keeps a
|
|
122
|
+
* ~250px plot near ~83 views, and the crisp line drawn on top hides the residual
|
|
123
|
+
* stepping. Lynx offers no polygon `clip-path` or reliable `<svg>` fill, so the
|
|
124
|
+
* area is rasterized this way rather than drawn as a single shape.
|
|
125
|
+
*/
|
|
126
|
+
const STRIP_WIDTH = 3;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A single area series drawn inside a `<ui-cartesian-chart>`: a translucent fill
|
|
130
|
+
* beneath a line. It reuses `<ui-line-series>` for the crisp line + dots and adds
|
|
131
|
+
* only the fill — an area chart is, structurally, a line chart with the region
|
|
132
|
+
* down to a baseline filled in.
|
|
133
|
+
*
|
|
134
|
+
* ## Why the host is `position: absolute; width/height: 100%`
|
|
135
|
+
* Identical to `UiLineSeries`: Lynx resolves an absolute child against its
|
|
136
|
+
* **direct parent** (there is no `position: static`), so the host itself must be
|
|
137
|
+
* the plot-sized containing block. Both the fill layer and the nested
|
|
138
|
+
* `<ui-line-series>` are direct children of this host and fill it exactly.
|
|
139
|
+
*
|
|
140
|
+
* ## Why the nested `<ui-line-series>` still finds the chart
|
|
141
|
+
* Element injectors are parented by *template nesting*, not physical projection.
|
|
142
|
+
* `<ui-line-series>` sits in this component's template, so its injector chains up
|
|
143
|
+
* through this host to the `UiCartesianChart` that `<ui-area-series>` is projected
|
|
144
|
+
* into — the same `inject(UiCartesianChart)` that `UiLineSeries` already relies
|
|
145
|
+
* on, one level deeper.
|
|
146
|
+
*/
|
|
147
|
+
@Component({
|
|
148
|
+
selector: 'ui-area-series',
|
|
149
|
+
standalone: true,
|
|
150
|
+
imports: [LYNX_ELEMENTS, UiLineSeries],
|
|
151
|
+
encapsulation: ViewEncapsulation.None,
|
|
152
|
+
host: {
|
|
153
|
+
style:
|
|
154
|
+
'position: absolute; left: 0px; top: 0px; width: 100%; height: 100%;',
|
|
155
|
+
'[class]': 'userClass()',
|
|
156
|
+
},
|
|
157
|
+
template: `
|
|
158
|
+
<!-- Fill layer first so the line + dots (below) paint on top of it. Its own
|
|
159
|
+
opacity makes the whole fill translucent in a single composite —
|
|
160
|
+
independent of the color's format (rgba/var()/named), so there's no need
|
|
161
|
+
to inject an alpha channel into an arbitrary CSS color string. -->
|
|
162
|
+
<view [style]="fillLayerStyle()">
|
|
163
|
+
@for (column of columns(); track $index) {
|
|
164
|
+
<view [style]="column.style"></view>
|
|
165
|
+
}
|
|
166
|
+
</view>
|
|
167
|
+
<ui-line-series
|
|
168
|
+
[data]="data()"
|
|
169
|
+
[color]="color()"
|
|
170
|
+
[strokeWidth]="strokeWidth()"
|
|
171
|
+
[dotRadius]="dotRadius()"
|
|
172
|
+
[showDots]="showDots()"
|
|
173
|
+
[smooth]="smooth()"
|
|
174
|
+
(pointTap)="pointTap.emit($event)"
|
|
175
|
+
/>
|
|
176
|
+
`,
|
|
177
|
+
})
|
|
178
|
+
export class UiAreaSeries {
|
|
179
|
+
readonly #chart = inject(UiCartesianChart);
|
|
180
|
+
|
|
181
|
+
readonly data = input.required<ChartPoint[]>();
|
|
182
|
+
readonly color = input('var(--primary)');
|
|
183
|
+
readonly strokeWidth = input(2);
|
|
184
|
+
readonly dotRadius = input(3);
|
|
185
|
+
readonly showDots = input(true);
|
|
186
|
+
readonly fillOpacity = input(0.2);
|
|
187
|
+
/** Data-space y the fill descends to (the flat edge of the area). */
|
|
188
|
+
readonly baseline = input(0);
|
|
189
|
+
/** Fill under a smooth (monotone-cubic) curve instead of straight segments.
|
|
190
|
+
* Forwarded to the nested line so the crisp edge matches the fill's top. */
|
|
191
|
+
readonly smooth = input(false);
|
|
192
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
193
|
+
|
|
194
|
+
readonly pointTap = output<ChartPoint>();
|
|
195
|
+
|
|
196
|
+
// Project each data point into plot-local pixel space via the shared scales.
|
|
197
|
+
readonly #pixelPoints = computed(() => {
|
|
198
|
+
const scaleX = this.#chart.xScale();
|
|
199
|
+
const scaleY = this.#chart.yScale();
|
|
200
|
+
return this.data().map((point) => ({
|
|
201
|
+
x: scaleX(point.x),
|
|
202
|
+
y: scaleY(point.y),
|
|
203
|
+
}));
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
protected readonly fillLayerStyle = computed(
|
|
207
|
+
() =>
|
|
208
|
+
`position: absolute; left: 0px; top: 0px; width: 100%; height: 100%; opacity: ${this.fillOpacity()};`,
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
protected readonly columns = computed(() => {
|
|
212
|
+
const fill = this.color();
|
|
213
|
+
// Clamp the baseline into the plot so a baseline outside the domain anchors
|
|
214
|
+
// the fill to the nearest edge (top/bottom) instead of drawing off-plot.
|
|
215
|
+
const baseY = clamp(
|
|
216
|
+
this.#chart.yScale()(this.baseline()),
|
|
217
|
+
0,
|
|
218
|
+
this.#chart.plotHeight(),
|
|
219
|
+
);
|
|
220
|
+
// In smooth mode the fill traces the same monotone-cubic curve as the line:
|
|
221
|
+
// sampling the polyline densely means `computeAreaColumns` (which linearly
|
|
222
|
+
// interpolates the height at each strip) closely follows the curve's top edge.
|
|
223
|
+
const pixels = this.#pixelPoints();
|
|
224
|
+
const line = this.smooth() ? sampleSmoothLine(pixels) : pixels;
|
|
225
|
+
return computeAreaColumns(
|
|
226
|
+
line,
|
|
227
|
+
baseY,
|
|
228
|
+
this.#chart.plotWidth(),
|
|
229
|
+
STRIP_WIDTH,
|
|
230
|
+
).map((c) => ({
|
|
231
|
+
style:
|
|
232
|
+
`position: absolute; left: ${px(c.x)}; top: ${px(c.top)}; ` +
|
|
233
|
+
`width: ${px(c.width)}; height: ${px(c.height)}; background-color: ${fill};`,
|
|
234
|
+
}));
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* A ready-to-use area chart for the common single-series case. Composes a
|
|
240
|
+
* `<ui-cartesian-chart>` with one `<ui-area-series>` and auto-derives rounded
|
|
241
|
+
* axis domains + ticks from the data. For multiple series or full control over
|
|
242
|
+
* the axes, use `<ui-cartesian-chart>` with `<ui-area-series>` children directly.
|
|
243
|
+
*/
|
|
244
|
+
@Component({
|
|
245
|
+
selector: 'ui-area-chart',
|
|
246
|
+
standalone: true,
|
|
247
|
+
imports: [LYNX_ELEMENTS, UiCartesianChart, UiAreaSeries],
|
|
248
|
+
encapsulation: ViewEncapsulation.None,
|
|
249
|
+
template: `
|
|
250
|
+
<ui-cartesian-chart
|
|
251
|
+
[xDomain]="xAxis().domain"
|
|
252
|
+
[yDomain]="yAxis().domain"
|
|
253
|
+
[xTicks]="xAxis().ticks"
|
|
254
|
+
[yTicks]="yAxis().ticks"
|
|
255
|
+
[width]="width()"
|
|
256
|
+
[height]="height()"
|
|
257
|
+
[tickCount]="tickCount()"
|
|
258
|
+
[showXGrid]="showXGrid()"
|
|
259
|
+
[xAxisLabel]="xAxisLabel()"
|
|
260
|
+
[yAxisLabel]="yAxisLabel()"
|
|
261
|
+
[padding]="padding()"
|
|
262
|
+
[xTickFormat]="xTickFormat()"
|
|
263
|
+
[yTickFormat]="yTickFormat()"
|
|
264
|
+
[zoomable]="zoomable()"
|
|
265
|
+
[zoomAxes]="zoomAxes()"
|
|
266
|
+
[showZoomControls]="showZoomControls()"
|
|
267
|
+
[minZoom]="minZoom()"
|
|
268
|
+
[maxZoom]="maxZoom()"
|
|
269
|
+
[zoomStep]="zoomStep()"
|
|
270
|
+
[class]="userClass()"
|
|
271
|
+
>
|
|
272
|
+
<ui-area-series
|
|
273
|
+
[data]="data()"
|
|
274
|
+
[color]="color()"
|
|
275
|
+
[strokeWidth]="strokeWidth()"
|
|
276
|
+
[dotRadius]="dotRadius()"
|
|
277
|
+
[showDots]="showDots()"
|
|
278
|
+
[fillOpacity]="fillOpacity()"
|
|
279
|
+
[baseline]="baseline()"
|
|
280
|
+
[smooth]="smooth()"
|
|
281
|
+
(pointTap)="pointTap.emit($event)"
|
|
282
|
+
/>
|
|
283
|
+
</ui-cartesian-chart>
|
|
284
|
+
`,
|
|
285
|
+
})
|
|
286
|
+
export class UiAreaChart {
|
|
287
|
+
readonly data = input.required<ChartPoint[]>();
|
|
288
|
+
readonly color = input('var(--primary)');
|
|
289
|
+
readonly width = input(320);
|
|
290
|
+
readonly height = input(200);
|
|
291
|
+
readonly tickCount = input(5);
|
|
292
|
+
readonly strokeWidth = input(2);
|
|
293
|
+
readonly dotRadius = input(3);
|
|
294
|
+
readonly showDots = input(true);
|
|
295
|
+
readonly fillOpacity = input(0.2);
|
|
296
|
+
readonly baseline = input(0);
|
|
297
|
+
/** Fill under a smooth (monotone-cubic) curve instead of straight segments. */
|
|
298
|
+
readonly smooth = input(false);
|
|
299
|
+
/** Draw vertical gridlines at each x tick. */
|
|
300
|
+
readonly showXGrid = input(false);
|
|
301
|
+
/** Title for the x-axis (centered below the tick labels). */
|
|
302
|
+
readonly xAxisLabel = input<string>('');
|
|
303
|
+
/** Title for the y-axis (rotated in the left gutter). */
|
|
304
|
+
readonly yAxisLabel = input<string>('');
|
|
305
|
+
/** Inner inset of the plot area, each side a fraction (0–1) — see {@link ChartPadding}. */
|
|
306
|
+
readonly padding = input<ChartPadding>({});
|
|
307
|
+
/** Force the x-axis lower/upper bound instead of deriving it from the data.
|
|
308
|
+
* Omitted bounds fall back to the data extremes. */
|
|
309
|
+
readonly xMin = input<number | undefined>(undefined);
|
|
310
|
+
readonly xMax = input<number | undefined>(undefined);
|
|
311
|
+
/** Force the y-axis lower/upper bound. The `baseline` is still folded in, so
|
|
312
|
+
* `yMin` only lowers the floor further — it can't clip the fill's base off. */
|
|
313
|
+
readonly yMin = input<number | undefined>(undefined);
|
|
314
|
+
readonly yMax = input<number | undefined>(undefined);
|
|
315
|
+
readonly xTickFormat = input<(value: number) => string>(defaultTickFormat);
|
|
316
|
+
readonly yTickFormat = input<(value: number) => string>(defaultTickFormat);
|
|
317
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
318
|
+
|
|
319
|
+
// Pan/zoom — forwarded to the underlying <ui-cartesian-chart>. Off by default.
|
|
320
|
+
readonly zoomable = input(false);
|
|
321
|
+
readonly zoomAxes = input<'x' | 'y' | 'xy'>('xy');
|
|
322
|
+
readonly showZoomControls = input(true);
|
|
323
|
+
readonly minZoom = input(1);
|
|
324
|
+
readonly maxZoom = input(8);
|
|
325
|
+
readonly zoomStep = input(1.4);
|
|
326
|
+
|
|
327
|
+
readonly pointTap = output<ChartPoint>();
|
|
328
|
+
|
|
329
|
+
// Auto-compute rounded axes from the data extremes, letting an explicit
|
|
330
|
+
// min/max override either end. `Math.min/max(...[])` yield ±Infinity for empty
|
|
331
|
+
// data, which `niceScale` handles by padding.
|
|
332
|
+
protected readonly xAxis = computed(() => {
|
|
333
|
+
const xs = this.data().map((p) => p.x);
|
|
334
|
+
const lo = this.xMin() ?? Math.min(...xs);
|
|
335
|
+
const hi = this.xMax() ?? Math.max(...xs);
|
|
336
|
+
return niceScale(lo, hi, this.tickCount());
|
|
337
|
+
});
|
|
338
|
+
// Fold the baseline into the y-extent so the axis always spans the whole filled
|
|
339
|
+
// region: a positive-only series still shows the baseline at the bottom, and
|
|
340
|
+
// negative values pull the domain below it. An explicit yMin/yMax overrides the
|
|
341
|
+
// corresponding data-derived end (the baseline still participates in the other).
|
|
342
|
+
protected readonly yAxis = computed(() => {
|
|
343
|
+
const ys = this.data().map((p) => p.y);
|
|
344
|
+
const base = this.baseline();
|
|
345
|
+
const lo = this.yMin() ?? Math.min(base, ...ys);
|
|
346
|
+
const hi = this.yMax() ?? Math.max(base, ...ys);
|
|
347
|
+
return niceScale(lo, hi, this.tickCount());
|
|
348
|
+
});
|
|
349
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { render } from '@blotch/angular-lynx-testing-library';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { UiAspectRatio } from './aspect-ratio';
|
|
4
|
+
|
|
5
|
+
describe('UiAspectRatio', () => {
|
|
6
|
+
it('renders with the default 1:1 ratio', async () => {
|
|
7
|
+
const { container } = await render(UiAspectRatio);
|
|
8
|
+
expect(container).toBeTruthy();
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Component, ViewEncapsulation, computed, input } from '@angular/core';
|
|
2
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
3
|
+
|
|
4
|
+
import { cn } from '../../utils/cn';
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
selector: 'ui-aspect-ratio',
|
|
8
|
+
standalone: true,
|
|
9
|
+
imports: [LYNX_ELEMENTS],
|
|
10
|
+
encapsulation: ViewEncapsulation.None,
|
|
11
|
+
template: `
|
|
12
|
+
<view [class]="containerClass()" [style]="containerStyle()">
|
|
13
|
+
<ng-content />
|
|
14
|
+
</view>
|
|
15
|
+
`,
|
|
16
|
+
})
|
|
17
|
+
export class UiAspectRatio {
|
|
18
|
+
readonly ratio = input(1);
|
|
19
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
20
|
+
|
|
21
|
+
protected readonly containerClass = computed(() =>
|
|
22
|
+
cn('w-full overflow-hidden', this.userClass()),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
protected readonly containerStyle = computed(
|
|
26
|
+
() => `aspect-ratio: ${this.ratio()};`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UiAspectRatio } from './aspect-ratio';
|
|
@@ -0,0 +1,69 @@
|
|
|
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 { UiAvatar } from './avatar';
|
|
7
|
+
|
|
8
|
+
@Component({
|
|
9
|
+
standalone: true,
|
|
10
|
+
imports: [UiAvatar, LYNX_ELEMENTS],
|
|
11
|
+
template: `<ui-avatar [src]="src" [fallback]="fallback" />`,
|
|
12
|
+
})
|
|
13
|
+
class AvatarHost {
|
|
14
|
+
src = '';
|
|
15
|
+
fallback = 'AB';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('UiAvatar', () => {
|
|
19
|
+
it('renders the fallback branch when src is empty', async () => {
|
|
20
|
+
const { container } = await render(AvatarHost);
|
|
21
|
+
expect(container).toBeTruthy();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('renders without a src (default fallback path)', async () => {
|
|
25
|
+
const { container } = await render(UiAvatar);
|
|
26
|
+
expect(container).toBeTruthy();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('renders the image branch when src is programmatically set', async () => {
|
|
30
|
+
// JIT template bindings are inert against input(); drive src directly so
|
|
31
|
+
// the @if(src()) branch fires and the image + skeleton render.
|
|
32
|
+
const { componentRef, container } = await render(UiAvatar);
|
|
33
|
+
setInputSignal(
|
|
34
|
+
(componentRef.instance as UiAvatar).src,
|
|
35
|
+
'https://example.com/avatar.png',
|
|
36
|
+
);
|
|
37
|
+
await waitForUpdate();
|
|
38
|
+
expect(container.querySelector('image')).toBeTruthy();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('keeps the image load / error handlers idempotent', async () => {
|
|
42
|
+
// Directly invoke the protected handlers via the instance to cover the
|
|
43
|
+
// re-entrancy latches — Lynx's on-device behavior re-dispatches the load
|
|
44
|
+
// event when the skeleton animation tears down.
|
|
45
|
+
const { componentRef } = await render(UiAvatar);
|
|
46
|
+
const inst = componentRef.instance as unknown as {
|
|
47
|
+
onImageLoad: () => void;
|
|
48
|
+
onImageError: () => void;
|
|
49
|
+
loaded: () => boolean;
|
|
50
|
+
errored: () => boolean;
|
|
51
|
+
};
|
|
52
|
+
inst.onImageLoad();
|
|
53
|
+
inst.onImageLoad(); // second call is a no-op (loaded latch)
|
|
54
|
+
expect(inst.loaded()).toBe(true);
|
|
55
|
+
|
|
56
|
+
inst.onImageError();
|
|
57
|
+
inst.onImageError(); // second call is a no-op (errored latch)
|
|
58
|
+
expect(inst.errored()).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('applies size-specific classes for each supported size', async () => {
|
|
62
|
+
const { componentRef, container } = await render(UiAvatar);
|
|
63
|
+
for (const size of ['sm', 'default', 'lg', 'xl', '2xl'] as const) {
|
|
64
|
+
setInputSignal((componentRef.instance as UiAvatar).size, size);
|
|
65
|
+
await waitForUpdate();
|
|
66
|
+
expect(container).toBeTruthy();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
ViewEncapsulation,
|
|
4
|
+
computed,
|
|
5
|
+
input,
|
|
6
|
+
signal,
|
|
7
|
+
} from '@angular/core';
|
|
8
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
9
|
+
|
|
10
|
+
import { UiSkeleton } from '../skeleton/skeleton';
|
|
11
|
+
import { cn } from '../../utils/cn';
|
|
12
|
+
|
|
13
|
+
@Component({
|
|
14
|
+
selector: 'ui-avatar',
|
|
15
|
+
standalone: true,
|
|
16
|
+
imports: [LYNX_ELEMENTS, UiSkeleton],
|
|
17
|
+
encapsulation: ViewEncapsulation.None,
|
|
18
|
+
template: `
|
|
19
|
+
<view [class]="containerClass()">
|
|
20
|
+
@if (src() && !errored()) {
|
|
21
|
+
<!-- The <image> stays mounted whenever src is set so its bindload /
|
|
22
|
+
binderror events can fire — an @if-swapped-out image would never
|
|
23
|
+
start loading, so the load event that reveals it would never come.
|
|
24
|
+
Native Lynx <image> renders nothing until loaded (there is no
|
|
25
|
+
broken-image glyph like the web), so while loading the box is
|
|
26
|
+
simply empty; the skeleton below sits on top of it as a placeholder.
|
|
27
|
+
onImageLoad() flips the loaded state, removing the skeleton to
|
|
28
|
+
reveal the image underneath. -->
|
|
29
|
+
<image
|
|
30
|
+
[attr.src]="src()"
|
|
31
|
+
[class]="imageClass()"
|
|
32
|
+
(bindload)="onImageLoad()"
|
|
33
|
+
(binderror)="onImageError()"
|
|
34
|
+
/>
|
|
35
|
+
<!-- Overlay placeholder shown only while the image is loading. It paints
|
|
36
|
+
above the <image> because it comes later in source order (Lynx paint
|
|
37
|
+
order follows the DOM, so no z-index is needed). -->
|
|
38
|
+
@if (!loaded()) {
|
|
39
|
+
<ui-skeleton [class]="skeletonClass()" />
|
|
40
|
+
}
|
|
41
|
+
} @else {
|
|
42
|
+
<!-- Shared by the no-src case and the load-error case: initials if a
|
|
43
|
+
fallback was provided, otherwise a plain muted circle. -->
|
|
44
|
+
<view [class]="fallbackClass()">
|
|
45
|
+
<text [class]="initialsClass()">{{ fallback() }}</text>
|
|
46
|
+
</view>
|
|
47
|
+
}
|
|
48
|
+
</view>
|
|
49
|
+
`,
|
|
50
|
+
})
|
|
51
|
+
export class UiAvatar {
|
|
52
|
+
readonly src = input<string>('');
|
|
53
|
+
readonly fallback = input<string>('');
|
|
54
|
+
readonly size = input<'default' | 'sm' | 'lg' | 'xl' | '2xl'>('default');
|
|
55
|
+
readonly userClass = input<string>('', { alias: 'class' });
|
|
56
|
+
|
|
57
|
+
/** True once the remote image has successfully loaded. */
|
|
58
|
+
protected readonly loaded = signal(false);
|
|
59
|
+
/** True if the remote image failed to load (offline / firewall / 404). */
|
|
60
|
+
protected readonly errored = signal(false);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Reveal the image by removing the skeleton overlay once it loads.
|
|
64
|
+
*
|
|
65
|
+
* Guarded against re-entrancy: on Lynx, starting or canceling an element
|
|
66
|
+
* animation can synchronously re-dispatch the image's `load` event — and
|
|
67
|
+
* removing the skeleton here tears down its pulse animation, which does
|
|
68
|
+
* exactly that. Without the one-way `loaded` latch short-circuiting the
|
|
69
|
+
* re-entry, this handler recurses and overflows the main-thread stack.
|
|
70
|
+
*/
|
|
71
|
+
protected onImageLoad(): void {
|
|
72
|
+
if (this.loaded()) return;
|
|
73
|
+
this.loaded.set(true);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* On load failure, drop the skeleton and fall through to the fallback so a
|
|
78
|
+
* broken image does not leave the placeholder pulsing forever. Guarded for
|
|
79
|
+
* the same re-entrancy reason as onImageLoad().
|
|
80
|
+
*/
|
|
81
|
+
protected onImageError(): void {
|
|
82
|
+
if (this.errored()) return;
|
|
83
|
+
this.errored.set(true);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
protected readonly containerClass = computed(() =>
|
|
87
|
+
cn(
|
|
88
|
+
// `relative` makes the container the positioning context for the absolutely
|
|
89
|
+
// positioned skeleton overlay below.
|
|
90
|
+
'items-center rounded-full flex relative justify-center overflow-hidden',
|
|
91
|
+
{
|
|
92
|
+
'h-10 w-10': this.size() === 'default',
|
|
93
|
+
'h-8 w-8': this.size() === 'sm',
|
|
94
|
+
'h-14 w-14': this.size() === 'lg',
|
|
95
|
+
'h-20 w-20': this.size() === 'xl',
|
|
96
|
+
'h-28 w-28': this.size() === '2xl',
|
|
97
|
+
},
|
|
98
|
+
this.userClass(),
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
protected readonly imageClass = computed(() =>
|
|
103
|
+
cn('h-full w-full', {
|
|
104
|
+
'h-10 w-10': this.size() === 'default',
|
|
105
|
+
'h-8 w-8': this.size() === 'sm',
|
|
106
|
+
'h-14 w-14': this.size() === 'lg',
|
|
107
|
+
'h-20 w-20': this.size() === 'xl',
|
|
108
|
+
'h-28 w-28': this.size() === '2xl',
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Skeleton overlay filling the circular container. `inset-0` and `h-full
|
|
113
|
+
// w-full` together guard against flex-collapse; `rounded-full` matches the
|
|
114
|
+
// container (which also `overflow-hidden`-clips it to a circle).
|
|
115
|
+
protected readonly skeletonClass = computed(() =>
|
|
116
|
+
cn('h-full w-full rounded-full absolute inset-0'),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
protected readonly fallbackClass = computed(() =>
|
|
120
|
+
cn('h-full w-full items-center rounded-full bg-muted flex justify-center'),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
protected readonly initialsClass = computed(() =>
|
|
124
|
+
cn('font-medium text-muted-foreground', {
|
|
125
|
+
'text-sm': this.size() === 'default' || this.size() === 'sm',
|
|
126
|
+
'text-lg': this.size() === 'lg',
|
|
127
|
+
'text-2xl': this.size() === 'xl',
|
|
128
|
+
'text-3xl': this.size() === '2xl',
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UiAvatar } from './avatar';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { render } from '@blotch/angular-lynx-testing-library';
|
|
2
|
+
import { Component } from '@angular/core';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
5
|
+
import { UiBadge, badgeVariants } from './badge';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Host wrappers project content and bind inputs the way real app templates do.
|
|
9
|
+
* Required-input components would need this; UiBadge has none but the pattern
|
|
10
|
+
* keeps content projection (<ng-content>) exercised.
|
|
11
|
+
*/
|
|
12
|
+
@Component({
|
|
13
|
+
standalone: true,
|
|
14
|
+
imports: [UiBadge, LYNX_ELEMENTS],
|
|
15
|
+
template: `<ui-badge [variant]="variant" [animated]="false"
|
|
16
|
+
>Status</ui-badge
|
|
17
|
+
>`,
|
|
18
|
+
})
|
|
19
|
+
class BadgeHost {
|
|
20
|
+
variant: 'default' | 'secondary' | 'destructive' | 'outline' = 'default';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('badgeVariants', () => {
|
|
24
|
+
it('returns default variant classes', () => {
|
|
25
|
+
expect(badgeVariants()).toContain('bg-primary');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('returns each explicit variant', () => {
|
|
29
|
+
expect(badgeVariants({ variant: 'secondary' })).toContain('bg-secondary');
|
|
30
|
+
expect(badgeVariants({ variant: 'destructive' })).toContain(
|
|
31
|
+
'bg-destructive',
|
|
32
|
+
);
|
|
33
|
+
expect(badgeVariants({ variant: 'outline' })).toContain('border');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('UiBadge', () => {
|
|
38
|
+
it('renders projected content', async () => {
|
|
39
|
+
const { getByText } = await render(BadgeHost);
|
|
40
|
+
expect(getByText('Status')).toBeTruthy();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('runs the pop-in animation when animated is true', async () => {
|
|
44
|
+
const { getByText } = await render(UiBadge);
|
|
45
|
+
// With no host binding, animated defaults to true → effect runs popIn on the
|
|
46
|
+
// container. Just assert it renders without throwing.
|
|
47
|
+
expect(getByText).toBeTruthy();
|
|
48
|
+
});
|
|
49
|
+
});
|