@styloviz/stat-card 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +65 -0
- package/fesm2022/styloviz-stat-card.mjs +185 -0
- package/fesm2022/styloviz-stat-card.mjs.map +1 -0
- package/package.json +42 -0
- package/types/styloviz-stat-card.d.ts +136 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 StyloViz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# @styloviz/stat-card
|
|
2
|
+
|
|
3
|
+
> KPI metric cards with sparklines, trend chips and four layout variants.
|
|
4
|
+
|
|
5
|
+
Part of the **StyloViz UI Kit** — a premium Angular 21 + Tailwind CSS 4 dashboard component library. Standalone, `OnPush`, strongly typed, dark-mode ready.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @styloviz/core @styloviz/stat-card
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires `@angular/core` and `@angular/common` >= 21. `@styloviz/core` is a peer dependency shared by every component. Prefer everything at once? `@styloviz/all` installs the whole free tier in one command.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { SvStatCardComponent } from '@styloviz/stat-card';
|
|
19
|
+
|
|
20
|
+
@Component({
|
|
21
|
+
standalone: true,
|
|
22
|
+
imports: [SvStatCardComponent],
|
|
23
|
+
template: `
|
|
24
|
+
<sv-stat-card title="Revenue" [value]="48200" prefix="$" [trendPercent]="12.5" />
|
|
25
|
+
`,
|
|
26
|
+
})
|
|
27
|
+
export class DemoComponent {}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Inputs
|
|
31
|
+
|
|
32
|
+
| Input | Type | Default | Description |
|
|
33
|
+
| --- | --- | --- | --- |
|
|
34
|
+
| `title` | `string` | `''` | Card title / metric label. |
|
|
35
|
+
| `value` | `string \| number` | `''` | Primary metric value — string or number, displayed as-is or formatted. |
|
|
36
|
+
| `prefix` | `string` | `''` | Optional prefix rendered before the value (e.g. "$", "€"). |
|
|
37
|
+
| `suffix` | `string` | `''` | Optional suffix rendered after the value (e.g. "K", "%", "ms"). |
|
|
38
|
+
| `description` | `string` | `''` | Short helper text beneath the title (e.g. "Last 30 days"). |
|
|
39
|
+
| `trend` | `StatCardTrend` | `'neutral'` | Direction of change. |
|
|
40
|
+
| `trendValue` | `number \| null` | `null` | Absolute delta value shown alongside the trend icon. |
|
|
41
|
+
| `trendPercent` | `number \| null` | `null` | Percentage change shown in the trend chip. |
|
|
42
|
+
| `trendLabel` | `string` | `''` | Comparison context label (e.g. "vs last month"). |
|
|
43
|
+
| `variant` | `StatCardVariant` | `'default'` | Color variant — drives icon background, accent line and trend colors. |
|
|
44
|
+
| `layout` | `StatCardLayout` | `'default'` | Card layout mode. |
|
|
45
|
+
| `iconPath` | `string` | `''` | SVG path `d` attribute for the icon. Leave empty to render no icon. |
|
|
46
|
+
| `iconViewBox` | `string` | `'0 0 24 24'` | SVG viewBox for the icon. |
|
|
47
|
+
| `sparkline` | `SparkPoint[]` | `[]` | Array of data points for the inline sparkline. Values should be on a consistent numeric scale; the component normalises them internally. Empty array hides the sparkline. |
|
|
48
|
+
| `loading` | `boolean` | `false` | Show skeleton loading shimmer instead of content. |
|
|
49
|
+
| `linkLabel` | `string` | `''` | Label for the bottom "view more" link. Empty string hides the link. |
|
|
50
|
+
| `customClass` | `string` | `''` | Additional CSS classes merged on the root card element. |
|
|
51
|
+
|
|
52
|
+
## Outputs
|
|
53
|
+
|
|
54
|
+
| Output | Type | Description |
|
|
55
|
+
| --- | --- | --- |
|
|
56
|
+
| `cardClick` | `void` | Emitted when the card body is clicked (the full card is interactive). |
|
|
57
|
+
| `linkClick` | `void` | Emitted when the "view more" link is clicked. |
|
|
58
|
+
|
|
59
|
+
## Documentation
|
|
60
|
+
|
|
61
|
+
Full API reference and live demos: https://styloviz.dev/docs/stat-card
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { input, output, computed, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
+
import { NgClass, DecimalPipe } from '@angular/common';
|
|
4
|
+
|
|
5
|
+
// ─── Component ───────────────────────────────────────────────────────────────
|
|
6
|
+
/**
|
|
7
|
+
* StatCard — A metric summary card for dashboards.
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - 4 layout modes: default · compact · horizontal · minimal
|
|
11
|
+
* - 6 semantic color variants with icon tinting
|
|
12
|
+
* - Trend indicator (up / down / neutral) with % and absolute delta
|
|
13
|
+
* - Inline SVG sparkline chart (no external lib)
|
|
14
|
+
* - Skeleton loading state
|
|
15
|
+
* - Accessible markup (aria-label, role="status")
|
|
16
|
+
* - Click-through CTA link slot
|
|
17
|
+
*/
|
|
18
|
+
class SvStatCardComponent {
|
|
19
|
+
// ── Core content ─────────────────────────────────────────────────────────
|
|
20
|
+
/** Card title / metric label. */
|
|
21
|
+
title = input('', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
|
|
22
|
+
/** Primary metric value — string or number, displayed as-is or formatted. */
|
|
23
|
+
value = input('', ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
24
|
+
/** Optional prefix rendered before the value (e.g. "$", "€"). */
|
|
25
|
+
prefix = input('', ...(ngDevMode ? [{ debugName: "prefix" }] : /* istanbul ignore next */ []));
|
|
26
|
+
/** Optional suffix rendered after the value (e.g. "K", "%", "ms"). */
|
|
27
|
+
suffix = input('', ...(ngDevMode ? [{ debugName: "suffix" }] : /* istanbul ignore next */ []));
|
|
28
|
+
/** Short helper text beneath the title (e.g. "Last 30 days"). */
|
|
29
|
+
description = input('', ...(ngDevMode ? [{ debugName: "description" }] : /* istanbul ignore next */ []));
|
|
30
|
+
// ── Trend ─────────────────────────────────────────────────────────────────
|
|
31
|
+
/** Direction of change. @default 'neutral' */
|
|
32
|
+
trend = input('neutral', ...(ngDevMode ? [{ debugName: "trend" }] : /* istanbul ignore next */ []));
|
|
33
|
+
/** Absolute delta value shown alongside the trend icon. */
|
|
34
|
+
trendValue = input(null, ...(ngDevMode ? [{ debugName: "trendValue" }] : /* istanbul ignore next */ []));
|
|
35
|
+
/** Percentage change shown in the trend chip. */
|
|
36
|
+
trendPercent = input(null, ...(ngDevMode ? [{ debugName: "trendPercent" }] : /* istanbul ignore next */ []));
|
|
37
|
+
/** Comparison context label (e.g. "vs last month"). @default '' */
|
|
38
|
+
trendLabel = input('', ...(ngDevMode ? [{ debugName: "trendLabel" }] : /* istanbul ignore next */ []));
|
|
39
|
+
// ── Appearance ────────────────────────────────────────────────────────────
|
|
40
|
+
/** Color variant — drives icon background, accent line and trend colors. @default 'default' */
|
|
41
|
+
variant = input('default', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
42
|
+
/** Card layout mode. @default 'default' */
|
|
43
|
+
layout = input('default', ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
|
|
44
|
+
/**
|
|
45
|
+
* SVG path `d` attribute for the icon.
|
|
46
|
+
* Leave empty to render no icon.
|
|
47
|
+
*/
|
|
48
|
+
iconPath = input('', ...(ngDevMode ? [{ debugName: "iconPath" }] : /* istanbul ignore next */ []));
|
|
49
|
+
/** SVG viewBox for the icon. @default '0 0 24 24' */
|
|
50
|
+
iconViewBox = input('0 0 24 24', ...(ngDevMode ? [{ debugName: "iconViewBox" }] : /* istanbul ignore next */ []));
|
|
51
|
+
// ── Sparkline ─────────────────────────────────────────────────────────────
|
|
52
|
+
/**
|
|
53
|
+
* Array of data points for the inline sparkline.
|
|
54
|
+
* Values should be on a consistent numeric scale; the component
|
|
55
|
+
* normalises them internally. Empty array hides the sparkline.
|
|
56
|
+
*/
|
|
57
|
+
sparkline = input([], ...(ngDevMode ? [{ debugName: "sparkline" }] : /* istanbul ignore next */ []));
|
|
58
|
+
// ── State ─────────────────────────────────────────────────────────────────
|
|
59
|
+
/** Show skeleton loading shimmer instead of content. @default false */
|
|
60
|
+
loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
61
|
+
// ── Interaction ───────────────────────────────────────────────────────────
|
|
62
|
+
/** Label for the bottom "view more" link. Empty string hides the link. */
|
|
63
|
+
linkLabel = input('', ...(ngDevMode ? [{ debugName: "linkLabel" }] : /* istanbul ignore next */ []));
|
|
64
|
+
/** Additional CSS classes merged on the root card element. */
|
|
65
|
+
customClass = input('', ...(ngDevMode ? [{ debugName: "customClass" }] : /* istanbul ignore next */ []));
|
|
66
|
+
// ── Outputs ───────────────────────────────────────────────────────────────
|
|
67
|
+
/** Emitted when the card body is clicked (the full card is interactive). */
|
|
68
|
+
cardClick = output();
|
|
69
|
+
/** Emitted when the "view more" link is clicked. */
|
|
70
|
+
linkClick = output();
|
|
71
|
+
// ── Computed ──────────────────────────────────────────────────────────────
|
|
72
|
+
hasIcon = computed(() => !!this.iconPath(), ...(ngDevMode ? [{ debugName: "hasIcon" }] : /* istanbul ignore next */ []));
|
|
73
|
+
hasSparkline = computed(() => this.sparkline().length >= 2, ...(ngDevMode ? [{ debugName: "hasSparkline" }] : /* istanbul ignore next */ []));
|
|
74
|
+
hasLink = computed(() => !!this.linkLabel(), ...(ngDevMode ? [{ debugName: "hasLink" }] : /* istanbul ignore next */ []));
|
|
75
|
+
hasTrend = computed(() => this.trend() !== 'neutral' || this.trendPercent() !== null || this.trendValue() !== null, ...(ngDevMode ? [{ debugName: "hasTrend" }] : /* istanbul ignore next */ []));
|
|
76
|
+
// ── Sparkline coordinate helpers ──────────────────────────────────────────
|
|
77
|
+
/** SVG canvas constants for the inline sparkline. */
|
|
78
|
+
static SPARK_W = 120;
|
|
79
|
+
static SPARK_H = 36;
|
|
80
|
+
static SPARK_PAD = 2;
|
|
81
|
+
/**
|
|
82
|
+
* Normalised `"x,y"` coordinate strings for each sparkline point.
|
|
83
|
+
* Computed once and shared by both `sparklinePath` and `sparklineFill`
|
|
84
|
+
* to avoid duplicating the min/max normalisation logic.
|
|
85
|
+
*/
|
|
86
|
+
sparklineCoords = computed(() => {
|
|
87
|
+
const pts = this.sparkline();
|
|
88
|
+
if (pts.length < 2)
|
|
89
|
+
return [];
|
|
90
|
+
const { SPARK_W: W, SPARK_H: H, SPARK_PAD: PAD } = SvStatCardComponent;
|
|
91
|
+
const vals = pts.map(p => p.value);
|
|
92
|
+
const min = Math.min(...vals);
|
|
93
|
+
const range = (Math.max(...vals) - min) || 1;
|
|
94
|
+
return pts.map((p, i) => {
|
|
95
|
+
const x = PAD + (i / (pts.length - 1)) * (W - PAD * 2);
|
|
96
|
+
const y = H - PAD - ((p.value - min) / range) * (H - PAD * 2);
|
|
97
|
+
return `${x.toFixed(1)},${y.toFixed(1)}`;
|
|
98
|
+
});
|
|
99
|
+
}, ...(ngDevMode ? [{ debugName: "sparklineCoords" }] : /* istanbul ignore next */ []));
|
|
100
|
+
/** Normalised sparkline points mapped to an SVG polyline string. */
|
|
101
|
+
sparklinePath = computed(() => {
|
|
102
|
+
const coords = this.sparklineCoords();
|
|
103
|
+
return coords.length ? coords.join(' ') : '';
|
|
104
|
+
}, ...(ngDevMode ? [{ debugName: "sparklinePath" }] : /* istanbul ignore next */ []));
|
|
105
|
+
/** Fill path for the area under the sparkline. */
|
|
106
|
+
sparklineFill = computed(() => {
|
|
107
|
+
const coords = this.sparklineCoords();
|
|
108
|
+
if (!coords.length)
|
|
109
|
+
return '';
|
|
110
|
+
const { SPARK_H: H, SPARK_PAD: PAD } = SvStatCardComponent;
|
|
111
|
+
const first = coords[0].split(',')[0];
|
|
112
|
+
const last = coords[coords.length - 1].split(',')[0];
|
|
113
|
+
return [
|
|
114
|
+
`M ${first},${H - PAD}`,
|
|
115
|
+
`L ${coords.join(' L ')}`,
|
|
116
|
+
`L ${last},${H - PAD}`,
|
|
117
|
+
'Z',
|
|
118
|
+
].join(' ');
|
|
119
|
+
}, ...(ngDevMode ? [{ debugName: "sparklineFill" }] : /* istanbul ignore next */ []));
|
|
120
|
+
/** Formatted value — passes numbers through DecimalPipe logic, strings untouched. */
|
|
121
|
+
displayValue = computed(() => {
|
|
122
|
+
const v = this.value();
|
|
123
|
+
if (typeof v === 'number') {
|
|
124
|
+
if (v >= 1_000_000)
|
|
125
|
+
return (v / 1_000_000).toFixed(1) + 'M';
|
|
126
|
+
if (v >= 1_000)
|
|
127
|
+
return (v / 1_000).toFixed(1) + 'K';
|
|
128
|
+
return v.toLocaleString();
|
|
129
|
+
}
|
|
130
|
+
return v;
|
|
131
|
+
}, ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
|
|
132
|
+
/** Formatted trend percent string, e.g. "12.4%". */
|
|
133
|
+
displayTrendPercent = computed(() => {
|
|
134
|
+
const p = this.trendPercent();
|
|
135
|
+
if (p === null)
|
|
136
|
+
return '';
|
|
137
|
+
return `${Math.abs(p).toFixed(1)}%`;
|
|
138
|
+
}, ...(ngDevMode ? [{ debugName: "displayTrendPercent" }] : /* istanbul ignore next */ []));
|
|
139
|
+
// ── Color maps ────────────────────────────────────────────────────────────
|
|
140
|
+
/** Icon wrapper background + text. */
|
|
141
|
+
iconClasses = computed(() => {
|
|
142
|
+
return 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400';
|
|
143
|
+
}, ...(ngDevMode ? [{ debugName: "iconClasses" }] : /* istanbul ignore next */ []));
|
|
144
|
+
/** Border color for the card — subtle gray, no colored accent. */
|
|
145
|
+
accentClass = computed(() => {
|
|
146
|
+
return 'border-gray-200/80 dark:border-gray-800';
|
|
147
|
+
}, ...(ngDevMode ? [{ debugName: "accentClass" }] : /* istanbul ignore next */ []));
|
|
148
|
+
/** Sparkline stroke + fill color. */
|
|
149
|
+
sparklineColorClass = computed(() => {
|
|
150
|
+
return { stroke: '#6b7280', fill: 'rgba(107,114,128,0.12)' };
|
|
151
|
+
}, ...(ngDevMode ? [{ debugName: "sparklineColorClass" }] : /* istanbul ignore next */ []));
|
|
152
|
+
/** Trend chip color classes. */
|
|
153
|
+
trendClasses = computed(() => {
|
|
154
|
+
switch (this.trend()) {
|
|
155
|
+
case 'up': return 'bg-emerald-50 text-emerald-700 ring-1 ring-emerald-100 dark:bg-emerald-950/50 dark:text-emerald-400 dark:ring-emerald-900/50';
|
|
156
|
+
case 'down': return 'bg-red-50 text-red-700 ring-1 ring-red-100 dark:bg-red-950/50 dark:text-red-400 dark:ring-red-900/50';
|
|
157
|
+
default: return 'bg-gray-100 text-gray-600 ring-1 ring-gray-200 dark:bg-gray-800 dark:text-gray-400 dark:ring-gray-700';
|
|
158
|
+
}
|
|
159
|
+
}, ...(ngDevMode ? [{ debugName: "trendClasses" }] : /* istanbul ignore next */ []));
|
|
160
|
+
/** Left accent bar + background tint for the minimal layout. */
|
|
161
|
+
minimalAccentClass = computed(() => {
|
|
162
|
+
return { bar: 'bg-gray-400 dark:bg-gray-500', bg: '' };
|
|
163
|
+
}, ...(ngDevMode ? [{ debugName: "minimalAccentClass" }] : /* istanbul ignore next */ []));
|
|
164
|
+
/** Plain trend text color used by the minimal layout (no pill). */
|
|
165
|
+
minimalTrendColor = computed(() => {
|
|
166
|
+
switch (this.trend()) {
|
|
167
|
+
case 'up': return 'text-emerald-600 dark:text-emerald-400';
|
|
168
|
+
case 'down': return 'text-red-600 dark:text-red-400';
|
|
169
|
+
default: return 'text-gray-500 dark:text-gray-400';
|
|
170
|
+
}
|
|
171
|
+
}, ...(ngDevMode ? [{ debugName: "minimalTrendColor" }] : /* istanbul ignore next */ []));
|
|
172
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SvStatCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
173
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: SvStatCardComponent, isStandalone: true, selector: "sv-stat-card", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, prefix: { classPropertyName: "prefix", publicName: "prefix", isSignal: true, isRequired: false, transformFunction: null }, suffix: { classPropertyName: "suffix", publicName: "suffix", isSignal: true, isRequired: false, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, trend: { classPropertyName: "trend", publicName: "trend", isSignal: true, isRequired: false, transformFunction: null }, trendValue: { classPropertyName: "trendValue", publicName: "trendValue", isSignal: true, isRequired: false, transformFunction: null }, trendPercent: { classPropertyName: "trendPercent", publicName: "trendPercent", isSignal: true, isRequired: false, transformFunction: null }, trendLabel: { classPropertyName: "trendLabel", publicName: "trendLabel", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, layout: { classPropertyName: "layout", publicName: "layout", isSignal: true, isRequired: false, transformFunction: null }, iconPath: { classPropertyName: "iconPath", publicName: "iconPath", isSignal: true, isRequired: false, transformFunction: null }, iconViewBox: { classPropertyName: "iconViewBox", publicName: "iconViewBox", isSignal: true, isRequired: false, transformFunction: null }, sparkline: { classPropertyName: "sparkline", publicName: "sparkline", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, linkLabel: { classPropertyName: "linkLabel", publicName: "linkLabel", isSignal: true, isRequired: false, transformFunction: null }, customClass: { classPropertyName: "customClass", publicName: "customClass", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { cardClick: "cardClick", linkClick: "linkClick" }, ngImport: i0, template: "<!-- \u2500\u2500 Root card \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n<article\n class=\"group relative flex flex-col overflow-hidden rounded-xl border bg-white\n shadow-sm transition-all duration-200 hover:shadow-md\n dark:bg-gray-900 dark:shadow-gray-950/60\"\n [ngClass]=\"[accentClass(), customClass()]\"\n [attr.aria-label]=\"title()\"\n role=\"region\"\n>\n\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 LOADING SKELETON \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n @if (loading()) {\n <div class=\"flex flex-col gap-4 p-5 animate-pulse\">\n <div class=\"flex items-center justify-between\">\n <div class=\"h-3 w-20 rounded-full bg-gray-200 dark:bg-gray-700/70\"></div>\n <div class=\"h-8 w-8 rounded-xl bg-gray-200 dark:bg-gray-700/70\"></div>\n </div>\n <div class=\"h-7 w-28 rounded-md bg-gray-200 dark:bg-gray-700/70\"></div>\n <div class=\"flex items-center gap-2\">\n <div class=\"h-5 w-12 rounded-full bg-gray-200 dark:bg-gray-700/70\"></div>\n <div class=\"h-3 w-16 rounded-full bg-gray-100 dark:bg-gray-800\"></div>\n </div>\n </div>\n }\n\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 DEFAULT LAYOUT \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n @if (!loading() && layout() === 'default') {\n <div class=\"flex flex-1 flex-col px-5 pt-5 pb-4 gap-3.5\">\n\n <!-- Row 1 \u2014 label + icon -->\n <div class=\"flex items-start justify-between gap-3\">\n <div class=\"flex-1 min-w-0\">\n <p class=\"text-[13px] font-medium tracking-tight text-gray-500 dark:text-gray-400 truncate\">\n {{ title() }}\n </p>\n @if (description()) {\n <p class=\"mt-0.5 text-xs text-gray-500 dark:text-gray-400 truncate\">\n {{ description() }}\n </p>\n }\n </div>\n @if (hasIcon()) {\n <span\n class=\"inline-flex shrink-0 items-center justify-center h-9 w-9 rounded-xl\n ring-1 ring-inset ring-black/[0.06] dark:ring-white/[0.06]\"\n [ngClass]=\"iconClasses()\"\n aria-hidden=\"true\"\n >\n <svg class=\"h-4 w-4\" [attr.viewBox]=\"iconViewBox()\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"1.75\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path [attr.d]=\"iconPath()\" />\n </svg>\n </span>\n }\n </div>\n\n <!-- Row 2 \u2014 primary value -->\n <div class=\"flex items-end justify-between gap-2\">\n <p\n class=\"text-[28px] font-bold tracking-tight text-gray-900 dark:text-white tabular-nums leading-none\"\n aria-live=\"polite\"\n >\n @if (prefix()) {\n <span class=\"text-lg font-semibold text-gray-500 dark:text-gray-400 mr-0.5\">{{ prefix() }}</span>\n }\n {{ displayValue() }}\n @if (suffix()) {\n <span class=\"text-lg font-semibold text-gray-500 dark:text-gray-400 ml-0.5\">{{ suffix() }}</span>\n }\n </p>\n\n <!-- Sparkline (right-aligned) -->\n @if (hasSparkline()) {\n <div class=\"shrink-0 w-[5.5rem] h-8 mb-0.5\" aria-hidden=\"true\">\n <svg viewBox=\"0 0 120 36\" preserveAspectRatio=\"none\" class=\"h-full w-full overflow-visible\">\n <!-- Fill area -->\n <path\n [attr.d]=\"sparklineFill()\"\n [attr.fill]=\"sparklineColorClass().fill\"\n stroke=\"none\"\n />\n <!-- Line -->\n <polyline\n [attr.points]=\"sparklinePath()\"\n fill=\"none\"\n [attr.stroke]=\"sparklineColorClass().stroke\"\n stroke-width=\"1.75\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </div>\n }\n </div>\n\n <!-- Row 3 \u2014 trend + label -->\n @if (hasTrend()) {\n <div class=\"flex items-center gap-2\">\n <span\n class=\"inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px] font-semibold\"\n [ngClass]=\"trendClasses()\"\n >\n <!-- Arrow icon -->\n @if (trend() === 'up') {\n <svg class=\"h-3 w-3\" viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M8 3.293l4.354 4.353-1.414 1.415L9 7.12V13H7V7.12L5.06 9.06 3.646 7.647 8 3.293z\" clip-rule=\"evenodd\"/>\n </svg>\n }\n @if (trend() === 'down') {\n <svg class=\"h-3 w-3\" viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M8 12.707l-4.354-4.353 1.414-1.415L7 8.88V3h2v5.88l1.94-1.94 1.414 1.414L8 12.707z\" clip-rule=\"evenodd\"/>\n </svg>\n }\n @if (displayTrendPercent()) {\n {{ displayTrendPercent() }}\n }\n @if (trendValue() !== null && displayTrendPercent() === '') {\n {{ trendValue()! | number }}\n }\n </span>\n @if (trendLabel()) {\n <span class=\"text-xs text-gray-500 dark:text-gray-400\">{{ trendLabel() }}</span>\n }\n </div>\n }\n </div>\n\n <!-- View more link -->\n @if (hasLink()) {\n <button\n type=\"button\"\n class=\"flex items-center justify-between border-t border-gray-100 px-5 py-2.5\n text-xs font-medium text-gray-500 transition-colors duration-150\n hover:bg-gray-50/80 hover:text-primary-600\n dark:border-gray-800 dark:text-gray-400 dark:hover:bg-gray-800/60\n dark:hover:text-primary-400\"\n (click)=\"linkClick.emit()\"\n >\n <span>{{ linkLabel() }}</span>\n <svg class=\"h-3.5 w-3.5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M5 12h14M12 5l7 7-7 7\" />\n </svg>\n </button>\n }\n }\n\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 COMPACT LAYOUT \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n @if (!loading() && layout() === 'compact') {\n <div class=\"flex items-center gap-3.5 px-4 py-3.5\">\n @if (hasIcon()) {\n <span\n class=\"inline-flex shrink-0 items-center justify-center h-9 w-9 rounded-xl\n ring-1 ring-inset ring-black/[0.06] dark:ring-white/[0.06]\"\n [ngClass]=\"iconClasses()\"\n aria-hidden=\"true\"\n >\n <svg class=\"h-4 w-4\" [attr.viewBox]=\"iconViewBox()\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"1.75\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path [attr.d]=\"iconPath()\" />\n </svg>\n </span>\n }\n <div class=\"flex-1 min-w-0\">\n <p class=\"text-[12px] font-medium text-gray-500 dark:text-gray-400 truncate leading-tight\">{{ title() }}</p>\n <p class=\"text-xl font-bold text-gray-900 dark:text-white tabular-nums leading-snug\">\n @if (prefix()) { <span class=\"text-sm text-gray-500 dark:text-gray-400\">{{ prefix() }}</span> }\n {{ displayValue() }}\n @if (suffix()) { <span class=\"text-sm text-gray-500 dark:text-gray-400\">{{ suffix() }}</span> }\n </p>\n </div>\n @if (hasTrend()) {\n <span\n class=\"inline-flex shrink-0 items-center gap-0.5 rounded-full px-2 py-0.5 text-[11px] font-semibold\"\n [ngClass]=\"trendClasses()\"\n >\n @if (trend() === 'up') {\n <svg class=\"h-3 w-3\" viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M8 3.293l4.354 4.353-1.414 1.415L9 7.12V13H7V7.12L5.06 9.06 3.646 7.647 8 3.293z\" clip-rule=\"evenodd\"/>\n </svg>\n }\n @if (trend() === 'down') {\n <svg class=\"h-3 w-3\" viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M8 12.707l-4.354-4.353 1.414-1.415L7 8.88V3h2v5.88l1.94-1.94 1.414 1.414L8 12.707z\" clip-rule=\"evenodd\"/>\n </svg>\n }\n @if (displayTrendPercent()) { {{ displayTrendPercent() }} }\n </span>\n }\n </div>\n }\n\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 HORIZONTAL LAYOUT \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <!-- Two-column split: left = icon + label + context | right = value + trend -->\n @if (!loading() && layout() === 'horizontal') {\n <div class=\"flex items-stretch min-h-[88px]\">\n\n <!-- Left column \u2014 identity / context -->\n <div class=\"flex flex-1 flex-col justify-center gap-1.5 px-5 py-4\n border-r border-gray-100 dark:border-gray-800\">\n @if (hasIcon()) {\n <span\n class=\"inline-flex items-center justify-center h-9 w-9 rounded-xl mb-1\n ring-1 ring-inset ring-black/[0.06] dark:ring-white/[0.06]\"\n [ngClass]=\"iconClasses()\"\n aria-hidden=\"true\"\n >\n <svg class=\"h-4 w-4\" [attr.viewBox]=\"iconViewBox()\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"1.75\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path [attr.d]=\"iconPath()\" />\n </svg>\n </span>\n }\n <p class=\"text-[13px] font-semibold text-gray-700 dark:text-gray-200 tracking-tight leading-tight truncate\">\n {{ title() }}\n </p>\n @if (description()) {\n <p class=\"text-[11px] text-gray-500 dark:text-gray-400 truncate\">{{ description() }}</p>\n }\n @if (trendLabel()) {\n <p class=\"text-[11px] text-gray-500 dark:text-gray-400\">{{ trendLabel() }}</p>\n }\n </div>\n\n <!-- Right column \u2014 metric + trend -->\n <div class=\"flex flex-col items-end justify-center gap-2 px-5 py-4 shrink-0\">\n <p class=\"text-[26px] font-bold tracking-tight text-gray-900 dark:text-white tabular-nums leading-none\">\n @if (prefix()) {\n <span class=\"text-base font-semibold text-gray-500 dark:text-gray-400 mr-0.5\">{{ prefix() }}</span>\n }\n {{ displayValue() }}\n @if (suffix()) {\n <span class=\"text-base font-semibold text-gray-500 dark:text-gray-400 ml-0.5\">{{ suffix() }}</span>\n }\n </p>\n\n @if (hasTrend()) {\n <span\n class=\"inline-flex items-center gap-0.5 rounded-full px-2 py-0.5 text-[11px] font-semibold\"\n [ngClass]=\"trendClasses()\"\n >\n @if (trend() === 'up') {\n <svg class=\"h-3 w-3\" viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M8 3.293l4.354 4.353-1.414 1.415L9 7.12V13H7V7.12L5.06 9.06 3.646 7.647 8 3.293z\" clip-rule=\"evenodd\"/>\n </svg>\n }\n @if (trend() === 'down') {\n <svg class=\"h-3 w-3\" viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M8 12.707l-4.354-4.353 1.414-1.415L7 8.88V3h2v5.88l1.94-1.94 1.414 1.414L8 12.707z\" clip-rule=\"evenodd\"/>\n </svg>\n }\n {{ displayTrendPercent() }}\n @if (trendValue() !== null && displayTrendPercent() === '') {\n {{ trendValue()! | number }}\n }\n </span>\n }\n\n @if (hasSparkline()) {\n <div class=\"w-20 h-7 mt-1\" aria-hidden=\"true\">\n <svg viewBox=\"0 0 120 36\" preserveAspectRatio=\"none\" class=\"h-full w-full overflow-visible\">\n <path [attr.d]=\"sparklineFill()\" [attr.fill]=\"sparklineColorClass().fill\" stroke=\"none\"/>\n <polyline\n [attr.points]=\"sparklinePath()\"\n fill=\"none\"\n [attr.stroke]=\"sparklineColorClass().stroke\"\n stroke-width=\"1.75\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </div>\n }\n </div>\n </div>\n }\n\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 MINIMAL LAYOUT \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <!-- Accent bar + tinted background + large value + plain trend text (no pill) -->\n @if (!loading() && layout() === 'minimal') {\n <div class=\"flex items-stretch\" [ngClass]=\"minimalAccentClass().bg\">\n\n <!-- Colored left accent bar \u2014\u2014 the defining visual signature of this layout -->\n <div class=\"w-1 shrink-0\" [ngClass]=\"minimalAccentClass().bar\" aria-hidden=\"true\"></div>\n\n <!-- Content -->\n <div class=\"flex flex-1 items-center justify-between gap-4 px-4 py-4\">\n\n <!-- Left: optional icon + label + value -->\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (hasIcon()) {\n <span\n class=\"inline-flex shrink-0 items-center justify-center h-10 w-10 rounded-xl\n ring-1 ring-inset ring-black/[0.06] dark:ring-white/[0.06]\"\n [ngClass]=\"iconClasses()\"\n aria-hidden=\"true\"\n >\n <svg class=\"h-5 w-5\" [attr.viewBox]=\"iconViewBox()\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"1.75\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path [attr.d]=\"iconPath()\" />\n </svg>\n </span>\n }\n <div class=\"min-w-0\">\n <p class=\"text-[10px] font-semibold tracking-widest uppercase text-gray-500 dark:text-gray-400 truncate mb-1\">\n {{ title() }}\n </p>\n <p class=\"text-[30px] font-extrabold text-gray-900 dark:text-white tabular-nums leading-none\">\n @if (prefix()) {\n <span class=\"text-base font-semibold text-gray-500 dark:text-gray-400 mr-0.5\">{{ prefix() }}</span>\n }\n {{ displayValue() }}\n @if (suffix()) {\n <span class=\"text-base font-semibold text-gray-500 dark:text-gray-400 ml-0.5\">{{ suffix() }}</span>\n }\n </p>\n @if (description()) {\n <p class=\"mt-1 text-[11px] text-gray-500 dark:text-gray-400 truncate\">{{ description() }}</p>\n }\n </div>\n </div>\n\n <!-- Right: plain colored trend (no pill/chip) -->\n @if (hasTrend()) {\n <div class=\"flex flex-col items-end shrink-0 gap-0.5\">\n <span\n class=\"inline-flex items-center gap-1 text-sm font-bold\"\n [ngClass]=\"minimalTrendColor()\"\n >\n @if (trend() === 'up') {\n <svg class=\"h-4 w-4\" viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M8 3.293l4.354 4.353-1.414 1.415L9 7.12V13H7V7.12L5.06 9.06 3.646 7.647 8 3.293z\" clip-rule=\"evenodd\"/>\n </svg>\n }\n @if (trend() === 'down') {\n <svg class=\"h-4 w-4\" viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M8 12.707l-4.354-4.353 1.414-1.415L7 8.88V3h2v5.88l1.94-1.94 1.414 1.414L8 12.707z\" clip-rule=\"evenodd\"/>\n </svg>\n }\n {{ displayTrendPercent() }}\n @if (trendValue() !== null && displayTrendPercent() === '') {\n {{ trendValue()! | number }}\n }\n </span>\n @if (trendLabel()) {\n <span class=\"text-[10px] text-gray-500 dark:text-gray-400 text-right leading-tight\">\n {{ trendLabel() }}\n </span>\n }\n </div>\n }\n </div>\n </div>\n }\n\n</article>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "pipe", type: DecimalPipe, name: "number" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
174
|
+
}
|
|
175
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SvStatCardComponent, decorators: [{
|
|
176
|
+
type: Component,
|
|
177
|
+
args: [{ selector: 'sv-stat-card', standalone: true, imports: [NgClass, DecimalPipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- \u2500\u2500 Root card \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n<article\n class=\"group relative flex flex-col overflow-hidden rounded-xl border bg-white\n shadow-sm transition-all duration-200 hover:shadow-md\n dark:bg-gray-900 dark:shadow-gray-950/60\"\n [ngClass]=\"[accentClass(), customClass()]\"\n [attr.aria-label]=\"title()\"\n role=\"region\"\n>\n\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 LOADING SKELETON \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n @if (loading()) {\n <div class=\"flex flex-col gap-4 p-5 animate-pulse\">\n <div class=\"flex items-center justify-between\">\n <div class=\"h-3 w-20 rounded-full bg-gray-200 dark:bg-gray-700/70\"></div>\n <div class=\"h-8 w-8 rounded-xl bg-gray-200 dark:bg-gray-700/70\"></div>\n </div>\n <div class=\"h-7 w-28 rounded-md bg-gray-200 dark:bg-gray-700/70\"></div>\n <div class=\"flex items-center gap-2\">\n <div class=\"h-5 w-12 rounded-full bg-gray-200 dark:bg-gray-700/70\"></div>\n <div class=\"h-3 w-16 rounded-full bg-gray-100 dark:bg-gray-800\"></div>\n </div>\n </div>\n }\n\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 DEFAULT LAYOUT \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n @if (!loading() && layout() === 'default') {\n <div class=\"flex flex-1 flex-col px-5 pt-5 pb-4 gap-3.5\">\n\n <!-- Row 1 \u2014 label + icon -->\n <div class=\"flex items-start justify-between gap-3\">\n <div class=\"flex-1 min-w-0\">\n <p class=\"text-[13px] font-medium tracking-tight text-gray-500 dark:text-gray-400 truncate\">\n {{ title() }}\n </p>\n @if (description()) {\n <p class=\"mt-0.5 text-xs text-gray-500 dark:text-gray-400 truncate\">\n {{ description() }}\n </p>\n }\n </div>\n @if (hasIcon()) {\n <span\n class=\"inline-flex shrink-0 items-center justify-center h-9 w-9 rounded-xl\n ring-1 ring-inset ring-black/[0.06] dark:ring-white/[0.06]\"\n [ngClass]=\"iconClasses()\"\n aria-hidden=\"true\"\n >\n <svg class=\"h-4 w-4\" [attr.viewBox]=\"iconViewBox()\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"1.75\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path [attr.d]=\"iconPath()\" />\n </svg>\n </span>\n }\n </div>\n\n <!-- Row 2 \u2014 primary value -->\n <div class=\"flex items-end justify-between gap-2\">\n <p\n class=\"text-[28px] font-bold tracking-tight text-gray-900 dark:text-white tabular-nums leading-none\"\n aria-live=\"polite\"\n >\n @if (prefix()) {\n <span class=\"text-lg font-semibold text-gray-500 dark:text-gray-400 mr-0.5\">{{ prefix() }}</span>\n }\n {{ displayValue() }}\n @if (suffix()) {\n <span class=\"text-lg font-semibold text-gray-500 dark:text-gray-400 ml-0.5\">{{ suffix() }}</span>\n }\n </p>\n\n <!-- Sparkline (right-aligned) -->\n @if (hasSparkline()) {\n <div class=\"shrink-0 w-[5.5rem] h-8 mb-0.5\" aria-hidden=\"true\">\n <svg viewBox=\"0 0 120 36\" preserveAspectRatio=\"none\" class=\"h-full w-full overflow-visible\">\n <!-- Fill area -->\n <path\n [attr.d]=\"sparklineFill()\"\n [attr.fill]=\"sparklineColorClass().fill\"\n stroke=\"none\"\n />\n <!-- Line -->\n <polyline\n [attr.points]=\"sparklinePath()\"\n fill=\"none\"\n [attr.stroke]=\"sparklineColorClass().stroke\"\n stroke-width=\"1.75\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </div>\n }\n </div>\n\n <!-- Row 3 \u2014 trend + label -->\n @if (hasTrend()) {\n <div class=\"flex items-center gap-2\">\n <span\n class=\"inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px] font-semibold\"\n [ngClass]=\"trendClasses()\"\n >\n <!-- Arrow icon -->\n @if (trend() === 'up') {\n <svg class=\"h-3 w-3\" viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M8 3.293l4.354 4.353-1.414 1.415L9 7.12V13H7V7.12L5.06 9.06 3.646 7.647 8 3.293z\" clip-rule=\"evenodd\"/>\n </svg>\n }\n @if (trend() === 'down') {\n <svg class=\"h-3 w-3\" viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M8 12.707l-4.354-4.353 1.414-1.415L7 8.88V3h2v5.88l1.94-1.94 1.414 1.414L8 12.707z\" clip-rule=\"evenodd\"/>\n </svg>\n }\n @if (displayTrendPercent()) {\n {{ displayTrendPercent() }}\n }\n @if (trendValue() !== null && displayTrendPercent() === '') {\n {{ trendValue()! | number }}\n }\n </span>\n @if (trendLabel()) {\n <span class=\"text-xs text-gray-500 dark:text-gray-400\">{{ trendLabel() }}</span>\n }\n </div>\n }\n </div>\n\n <!-- View more link -->\n @if (hasLink()) {\n <button\n type=\"button\"\n class=\"flex items-center justify-between border-t border-gray-100 px-5 py-2.5\n text-xs font-medium text-gray-500 transition-colors duration-150\n hover:bg-gray-50/80 hover:text-primary-600\n dark:border-gray-800 dark:text-gray-400 dark:hover:bg-gray-800/60\n dark:hover:text-primary-400\"\n (click)=\"linkClick.emit()\"\n >\n <span>{{ linkLabel() }}</span>\n <svg class=\"h-3.5 w-3.5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M5 12h14M12 5l7 7-7 7\" />\n </svg>\n </button>\n }\n }\n\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 COMPACT LAYOUT \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n @if (!loading() && layout() === 'compact') {\n <div class=\"flex items-center gap-3.5 px-4 py-3.5\">\n @if (hasIcon()) {\n <span\n class=\"inline-flex shrink-0 items-center justify-center h-9 w-9 rounded-xl\n ring-1 ring-inset ring-black/[0.06] dark:ring-white/[0.06]\"\n [ngClass]=\"iconClasses()\"\n aria-hidden=\"true\"\n >\n <svg class=\"h-4 w-4\" [attr.viewBox]=\"iconViewBox()\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"1.75\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path [attr.d]=\"iconPath()\" />\n </svg>\n </span>\n }\n <div class=\"flex-1 min-w-0\">\n <p class=\"text-[12px] font-medium text-gray-500 dark:text-gray-400 truncate leading-tight\">{{ title() }}</p>\n <p class=\"text-xl font-bold text-gray-900 dark:text-white tabular-nums leading-snug\">\n @if (prefix()) { <span class=\"text-sm text-gray-500 dark:text-gray-400\">{{ prefix() }}</span> }\n {{ displayValue() }}\n @if (suffix()) { <span class=\"text-sm text-gray-500 dark:text-gray-400\">{{ suffix() }}</span> }\n </p>\n </div>\n @if (hasTrend()) {\n <span\n class=\"inline-flex shrink-0 items-center gap-0.5 rounded-full px-2 py-0.5 text-[11px] font-semibold\"\n [ngClass]=\"trendClasses()\"\n >\n @if (trend() === 'up') {\n <svg class=\"h-3 w-3\" viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M8 3.293l4.354 4.353-1.414 1.415L9 7.12V13H7V7.12L5.06 9.06 3.646 7.647 8 3.293z\" clip-rule=\"evenodd\"/>\n </svg>\n }\n @if (trend() === 'down') {\n <svg class=\"h-3 w-3\" viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M8 12.707l-4.354-4.353 1.414-1.415L7 8.88V3h2v5.88l1.94-1.94 1.414 1.414L8 12.707z\" clip-rule=\"evenodd\"/>\n </svg>\n }\n @if (displayTrendPercent()) { {{ displayTrendPercent() }} }\n </span>\n }\n </div>\n }\n\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 HORIZONTAL LAYOUT \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <!-- Two-column split: left = icon + label + context | right = value + trend -->\n @if (!loading() && layout() === 'horizontal') {\n <div class=\"flex items-stretch min-h-[88px]\">\n\n <!-- Left column \u2014 identity / context -->\n <div class=\"flex flex-1 flex-col justify-center gap-1.5 px-5 py-4\n border-r border-gray-100 dark:border-gray-800\">\n @if (hasIcon()) {\n <span\n class=\"inline-flex items-center justify-center h-9 w-9 rounded-xl mb-1\n ring-1 ring-inset ring-black/[0.06] dark:ring-white/[0.06]\"\n [ngClass]=\"iconClasses()\"\n aria-hidden=\"true\"\n >\n <svg class=\"h-4 w-4\" [attr.viewBox]=\"iconViewBox()\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"1.75\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path [attr.d]=\"iconPath()\" />\n </svg>\n </span>\n }\n <p class=\"text-[13px] font-semibold text-gray-700 dark:text-gray-200 tracking-tight leading-tight truncate\">\n {{ title() }}\n </p>\n @if (description()) {\n <p class=\"text-[11px] text-gray-500 dark:text-gray-400 truncate\">{{ description() }}</p>\n }\n @if (trendLabel()) {\n <p class=\"text-[11px] text-gray-500 dark:text-gray-400\">{{ trendLabel() }}</p>\n }\n </div>\n\n <!-- Right column \u2014 metric + trend -->\n <div class=\"flex flex-col items-end justify-center gap-2 px-5 py-4 shrink-0\">\n <p class=\"text-[26px] font-bold tracking-tight text-gray-900 dark:text-white tabular-nums leading-none\">\n @if (prefix()) {\n <span class=\"text-base font-semibold text-gray-500 dark:text-gray-400 mr-0.5\">{{ prefix() }}</span>\n }\n {{ displayValue() }}\n @if (suffix()) {\n <span class=\"text-base font-semibold text-gray-500 dark:text-gray-400 ml-0.5\">{{ suffix() }}</span>\n }\n </p>\n\n @if (hasTrend()) {\n <span\n class=\"inline-flex items-center gap-0.5 rounded-full px-2 py-0.5 text-[11px] font-semibold\"\n [ngClass]=\"trendClasses()\"\n >\n @if (trend() === 'up') {\n <svg class=\"h-3 w-3\" viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M8 3.293l4.354 4.353-1.414 1.415L9 7.12V13H7V7.12L5.06 9.06 3.646 7.647 8 3.293z\" clip-rule=\"evenodd\"/>\n </svg>\n }\n @if (trend() === 'down') {\n <svg class=\"h-3 w-3\" viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M8 12.707l-4.354-4.353 1.414-1.415L7 8.88V3h2v5.88l1.94-1.94 1.414 1.414L8 12.707z\" clip-rule=\"evenodd\"/>\n </svg>\n }\n {{ displayTrendPercent() }}\n @if (trendValue() !== null && displayTrendPercent() === '') {\n {{ trendValue()! | number }}\n }\n </span>\n }\n\n @if (hasSparkline()) {\n <div class=\"w-20 h-7 mt-1\" aria-hidden=\"true\">\n <svg viewBox=\"0 0 120 36\" preserveAspectRatio=\"none\" class=\"h-full w-full overflow-visible\">\n <path [attr.d]=\"sparklineFill()\" [attr.fill]=\"sparklineColorClass().fill\" stroke=\"none\"/>\n <polyline\n [attr.points]=\"sparklinePath()\"\n fill=\"none\"\n [attr.stroke]=\"sparklineColorClass().stroke\"\n stroke-width=\"1.75\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </div>\n }\n </div>\n </div>\n }\n\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 MINIMAL LAYOUT \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n <!-- Accent bar + tinted background + large value + plain trend text (no pill) -->\n @if (!loading() && layout() === 'minimal') {\n <div class=\"flex items-stretch\" [ngClass]=\"minimalAccentClass().bg\">\n\n <!-- Colored left accent bar \u2014\u2014 the defining visual signature of this layout -->\n <div class=\"w-1 shrink-0\" [ngClass]=\"minimalAccentClass().bar\" aria-hidden=\"true\"></div>\n\n <!-- Content -->\n <div class=\"flex flex-1 items-center justify-between gap-4 px-4 py-4\">\n\n <!-- Left: optional icon + label + value -->\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (hasIcon()) {\n <span\n class=\"inline-flex shrink-0 items-center justify-center h-10 w-10 rounded-xl\n ring-1 ring-inset ring-black/[0.06] dark:ring-white/[0.06]\"\n [ngClass]=\"iconClasses()\"\n aria-hidden=\"true\"\n >\n <svg class=\"h-5 w-5\" [attr.viewBox]=\"iconViewBox()\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"1.75\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path [attr.d]=\"iconPath()\" />\n </svg>\n </span>\n }\n <div class=\"min-w-0\">\n <p class=\"text-[10px] font-semibold tracking-widest uppercase text-gray-500 dark:text-gray-400 truncate mb-1\">\n {{ title() }}\n </p>\n <p class=\"text-[30px] font-extrabold text-gray-900 dark:text-white tabular-nums leading-none\">\n @if (prefix()) {\n <span class=\"text-base font-semibold text-gray-500 dark:text-gray-400 mr-0.5\">{{ prefix() }}</span>\n }\n {{ displayValue() }}\n @if (suffix()) {\n <span class=\"text-base font-semibold text-gray-500 dark:text-gray-400 ml-0.5\">{{ suffix() }}</span>\n }\n </p>\n @if (description()) {\n <p class=\"mt-1 text-[11px] text-gray-500 dark:text-gray-400 truncate\">{{ description() }}</p>\n }\n </div>\n </div>\n\n <!-- Right: plain colored trend (no pill/chip) -->\n @if (hasTrend()) {\n <div class=\"flex flex-col items-end shrink-0 gap-0.5\">\n <span\n class=\"inline-flex items-center gap-1 text-sm font-bold\"\n [ngClass]=\"minimalTrendColor()\"\n >\n @if (trend() === 'up') {\n <svg class=\"h-4 w-4\" viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M8 3.293l4.354 4.353-1.414 1.415L9 7.12V13H7V7.12L5.06 9.06 3.646 7.647 8 3.293z\" clip-rule=\"evenodd\"/>\n </svg>\n }\n @if (trend() === 'down') {\n <svg class=\"h-4 w-4\" viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M8 12.707l-4.354-4.353 1.414-1.415L7 8.88V3h2v5.88l1.94-1.94 1.414 1.414L8 12.707z\" clip-rule=\"evenodd\"/>\n </svg>\n }\n {{ displayTrendPercent() }}\n @if (trendValue() !== null && displayTrendPercent() === '') {\n {{ trendValue()! | number }}\n }\n </span>\n @if (trendLabel()) {\n <span class=\"text-[10px] text-gray-500 dark:text-gray-400 text-right leading-tight\">\n {{ trendLabel() }}\n </span>\n }\n </div>\n }\n </div>\n </div>\n }\n\n</article>\n" }]
|
|
178
|
+
}], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], prefix: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefix", required: false }] }], suffix: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffix", required: false }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], trend: [{ type: i0.Input, args: [{ isSignal: true, alias: "trend", required: false }] }], trendValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "trendValue", required: false }] }], trendPercent: [{ type: i0.Input, args: [{ isSignal: true, alias: "trendPercent", required: false }] }], trendLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "trendLabel", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], layout: [{ type: i0.Input, args: [{ isSignal: true, alias: "layout", required: false }] }], iconPath: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconPath", required: false }] }], iconViewBox: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconViewBox", required: false }] }], sparkline: [{ type: i0.Input, args: [{ isSignal: true, alias: "sparkline", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], linkLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "linkLabel", required: false }] }], customClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "customClass", required: false }] }], cardClick: [{ type: i0.Output, args: ["cardClick"] }], linkClick: [{ type: i0.Output, args: ["linkClick"] }] } });
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Generated bundle index. Do not edit.
|
|
182
|
+
*/
|
|
183
|
+
|
|
184
|
+
export { SvStatCardComponent };
|
|
185
|
+
//# sourceMappingURL=styloviz-stat-card.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"styloviz-stat-card.mjs","sources":["../../../../projects/stat-card-free/src/lib/stat-card.component.ts","../../../../projects/stat-card-free/src/lib/stat-card.component.html","../../../../projects/stat-card-free/src/styloviz-stat-card.ts"],"sourcesContent":["import {\n Component,\n input,\n output,\n computed,\n ChangeDetectionStrategy,\n} from '@angular/core';\nimport { NgClass, DecimalPipe } from '@angular/common';\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type StatCardVariant = 'default';\nexport type StatCardTrend = 'up' | 'down' | 'neutral';\nexport type StatCardLayout = 'default' | 'compact' | 'horizontal' | 'minimal';\n\n/** A single point in the inline sparkline (value 0–100 scale, or raw). */\nexport interface SparkPoint {\n value: number;\n label?: string;\n}\n\n/**\n * All data needed to render a StatCard.\n * Pass these individually via @Input() or compose them into a stats object.\n */\nexport interface StatCardData {\n title: string;\n value: string | number;\n previousValue?: string | number;\n prefix?: string;\n suffix?: string;\n trend?: StatCardTrend;\n trendValue?: number; // absolute delta (e.g. 1234)\n trendPercent?: number; // % change (e.g. 12.4)\n trendLabel?: string; // e.g. \"vs last month\"\n iconPath?: string; // SVG <path d=\"...\">\n iconViewBox?: string; // defaults to \"0 0 24 24\"\n variant?: StatCardVariant;\n sparkline?: SparkPoint[];\n loading?: boolean;\n description?: string;\n linkLabel?: string;\n}\n\n// ─── Component ───────────────────────────────────────────────────────────────\n\n/**\n * StatCard — A metric summary card for dashboards.\n *\n * Features:\n * - 4 layout modes: default · compact · horizontal · minimal\n * - 6 semantic color variants with icon tinting\n * - Trend indicator (up / down / neutral) with % and absolute delta\n * - Inline SVG sparkline chart (no external lib)\n * - Skeleton loading state\n * - Accessible markup (aria-label, role=\"status\")\n * - Click-through CTA link slot\n */\n@Component({\n selector: 'sv-stat-card',\n standalone: true,\n imports: [NgClass, DecimalPipe],\n templateUrl: './stat-card.component.html',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class SvStatCardComponent {\n // ── Core content ─────────────────────────────────────────────────────────\n\n /** Card title / metric label. */\n title = input<string>('');\n\n /** Primary metric value — string or number, displayed as-is or formatted. */\n value = input<string | number>('');\n\n /** Optional prefix rendered before the value (e.g. \"$\", \"€\"). */\n prefix = input<string>('');\n\n /** Optional suffix rendered after the value (e.g. \"K\", \"%\", \"ms\"). */\n suffix = input<string>('');\n\n /** Short helper text beneath the title (e.g. \"Last 30 days\"). */\n description = input<string>('');\n\n // ── Trend ─────────────────────────────────────────────────────────────────\n\n /** Direction of change. @default 'neutral' */\n trend = input<StatCardTrend>('neutral');\n\n /** Absolute delta value shown alongside the trend icon. */\n trendValue = input<number | null>(null);\n\n /** Percentage change shown in the trend chip. */\n trendPercent = input<number | null>(null);\n\n /** Comparison context label (e.g. \"vs last month\"). @default '' */\n trendLabel = input<string>('');\n\n // ── Appearance ────────────────────────────────────────────────────────────\n\n /** Color variant — drives icon background, accent line and trend colors. @default 'default' */\n variant = input<StatCardVariant>('default');\n\n /** Card layout mode. @default 'default' */\n layout = input<StatCardLayout>('default');\n\n /**\n * SVG path `d` attribute for the icon.\n * Leave empty to render no icon.\n */\n iconPath = input<string>('');\n\n /** SVG viewBox for the icon. @default '0 0 24 24' */\n iconViewBox = input<string>('0 0 24 24');\n\n // ── Sparkline ─────────────────────────────────────────────────────────────\n\n /**\n * Array of data points for the inline sparkline.\n * Values should be on a consistent numeric scale; the component\n * normalises them internally. Empty array hides the sparkline.\n */\n sparkline = input<SparkPoint[]>([]);\n\n // ── State ─────────────────────────────────────────────────────────────────\n\n /** Show skeleton loading shimmer instead of content. @default false */\n loading = input<boolean>(false);\n\n // ── Interaction ───────────────────────────────────────────────────────────\n\n /** Label for the bottom \"view more\" link. Empty string hides the link. */\n linkLabel = input<string>('');\n\n /** Additional CSS classes merged on the root card element. */\n customClass = input<string>('');\n\n // ── Outputs ───────────────────────────────────────────────────────────────\n\n /** Emitted when the card body is clicked (the full card is interactive). */\n cardClick = output<void>();\n\n /** Emitted when the \"view more\" link is clicked. */\n linkClick = output<void>();\n\n // ── Computed ──────────────────────────────────────────────────────────────\n\n hasIcon = computed(() => !!this.iconPath());\n hasSparkline = computed(() => this.sparkline().length >= 2);\n hasLink = computed(() => !!this.linkLabel());\n hasTrend = computed(() =>\n this.trend() !== 'neutral' || this.trendPercent() !== null || this.trendValue() !== null\n );\n\n // ── Sparkline coordinate helpers ──────────────────────────────────────────\n\n /** SVG canvas constants for the inline sparkline. */\n private static readonly SPARK_W = 120;\n private static readonly SPARK_H = 36;\n private static readonly SPARK_PAD = 2;\n\n /**\n * Normalised `\"x,y\"` coordinate strings for each sparkline point.\n * Computed once and shared by both `sparklinePath` and `sparklineFill`\n * to avoid duplicating the min/max normalisation logic.\n */\n private sparklineCoords = computed<string[]>(() => {\n const pts = this.sparkline();\n if (pts.length < 2) return [];\n\n const { SPARK_W: W, SPARK_H: H, SPARK_PAD: PAD } = SvStatCardComponent;\n const vals = pts.map(p => p.value);\n const min = Math.min(...vals);\n const range = (Math.max(...vals) - min) || 1;\n\n return pts.map((p, i) => {\n const x = PAD + (i / (pts.length - 1)) * (W - PAD * 2);\n const y = H - PAD - ((p.value - min) / range) * (H - PAD * 2);\n return `${x.toFixed(1)},${y.toFixed(1)}`;\n });\n });\n\n /** Normalised sparkline points mapped to an SVG polyline string. */\n sparklinePath = computed<string>(() => {\n const coords = this.sparklineCoords();\n return coords.length ? coords.join(' ') : '';\n });\n\n /** Fill path for the area under the sparkline. */\n sparklineFill = computed<string>(() => {\n const coords = this.sparklineCoords();\n if (!coords.length) return '';\n\n const { SPARK_H: H, SPARK_PAD: PAD } = SvStatCardComponent;\n const first = coords[0].split(',')[0];\n const last = coords[coords.length - 1].split(',')[0];\n\n return [\n `M ${first},${H - PAD}`,\n `L ${coords.join(' L ')}`,\n `L ${last},${H - PAD}`,\n 'Z',\n ].join(' ');\n });\n\n /** Formatted value — passes numbers through DecimalPipe logic, strings untouched. */\n displayValue = computed<string>(() => {\n const v = this.value();\n if (typeof v === 'number') {\n if (v >= 1_000_000) return (v / 1_000_000).toFixed(1) + 'M';\n if (v >= 1_000) return (v / 1_000).toFixed(1) + 'K';\n return v.toLocaleString();\n }\n return v;\n });\n\n /** Formatted trend percent string, e.g. \"12.4%\". */\n displayTrendPercent = computed<string>(() => {\n const p = this.trendPercent();\n if (p === null) return '';\n return `${Math.abs(p).toFixed(1)}%`;\n });\n\n // ── Color maps ────────────────────────────────────────────────────────────\n\n /** Icon wrapper background + text. */\n iconClasses = computed<string>(() => {\n return 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400';\n });\n\n /** Border color for the card — subtle gray, no colored accent. */\n accentClass = computed<string>(() => {\n return 'border-gray-200/80 dark:border-gray-800';\n });\n\n /** Sparkline stroke + fill color. */\n sparklineColorClass = computed<{ stroke: string; fill: string }>(() => {\n return { stroke: '#6b7280', fill: 'rgba(107,114,128,0.12)' };\n });\n\n /** Trend chip color classes. */\n trendClasses = computed<string>(() => {\n switch (this.trend()) {\n case 'up': return 'bg-emerald-50 text-emerald-700 ring-1 ring-emerald-100 dark:bg-emerald-950/50 dark:text-emerald-400 dark:ring-emerald-900/50';\n case 'down': return 'bg-red-50 text-red-700 ring-1 ring-red-100 dark:bg-red-950/50 dark:text-red-400 dark:ring-red-900/50';\n default: return 'bg-gray-100 text-gray-600 ring-1 ring-gray-200 dark:bg-gray-800 dark:text-gray-400 dark:ring-gray-700';\n }\n });\n\n /** Left accent bar + background tint for the minimal layout. */\n minimalAccentClass = computed<{ bar: string; bg: string }>(() => {\n return { bar: 'bg-gray-400 dark:bg-gray-500', bg: '' };\n });\n\n /** Plain trend text color used by the minimal layout (no pill). */\n minimalTrendColor = computed<string>(() => {\n switch (this.trend()) {\n case 'up': return 'text-emerald-600 dark:text-emerald-400';\n case 'down': return 'text-red-600 dark:text-red-400';\n default: return 'text-gray-500 dark:text-gray-400';\n }\n });\n}\n","<!-- ── Root card ─────────────────────────────────────────────────────────── -->\n<article\n class=\"group relative flex flex-col overflow-hidden rounded-xl border bg-white\n shadow-sm transition-all duration-200 hover:shadow-md\n dark:bg-gray-900 dark:shadow-gray-950/60\"\n [ngClass]=\"[accentClass(), customClass()]\"\n [attr.aria-label]=\"title()\"\n role=\"region\"\n>\n\n <!-- ════════════════════════ LOADING SKELETON ════════════════════════════ -->\n @if (loading()) {\n <div class=\"flex flex-col gap-4 p-5 animate-pulse\">\n <div class=\"flex items-center justify-between\">\n <div class=\"h-3 w-20 rounded-full bg-gray-200 dark:bg-gray-700/70\"></div>\n <div class=\"h-8 w-8 rounded-xl bg-gray-200 dark:bg-gray-700/70\"></div>\n </div>\n <div class=\"h-7 w-28 rounded-md bg-gray-200 dark:bg-gray-700/70\"></div>\n <div class=\"flex items-center gap-2\">\n <div class=\"h-5 w-12 rounded-full bg-gray-200 dark:bg-gray-700/70\"></div>\n <div class=\"h-3 w-16 rounded-full bg-gray-100 dark:bg-gray-800\"></div>\n </div>\n </div>\n }\n\n <!-- ════════════════════════ DEFAULT LAYOUT ══════════════════════════════ -->\n @if (!loading() && layout() === 'default') {\n <div class=\"flex flex-1 flex-col px-5 pt-5 pb-4 gap-3.5\">\n\n <!-- Row 1 — label + icon -->\n <div class=\"flex items-start justify-between gap-3\">\n <div class=\"flex-1 min-w-0\">\n <p class=\"text-[13px] font-medium tracking-tight text-gray-500 dark:text-gray-400 truncate\">\n {{ title() }}\n </p>\n @if (description()) {\n <p class=\"mt-0.5 text-xs text-gray-500 dark:text-gray-400 truncate\">\n {{ description() }}\n </p>\n }\n </div>\n @if (hasIcon()) {\n <span\n class=\"inline-flex shrink-0 items-center justify-center h-9 w-9 rounded-xl\n ring-1 ring-inset ring-black/[0.06] dark:ring-white/[0.06]\"\n [ngClass]=\"iconClasses()\"\n aria-hidden=\"true\"\n >\n <svg class=\"h-4 w-4\" [attr.viewBox]=\"iconViewBox()\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"1.75\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path [attr.d]=\"iconPath()\" />\n </svg>\n </span>\n }\n </div>\n\n <!-- Row 2 — primary value -->\n <div class=\"flex items-end justify-between gap-2\">\n <p\n class=\"text-[28px] font-bold tracking-tight text-gray-900 dark:text-white tabular-nums leading-none\"\n aria-live=\"polite\"\n >\n @if (prefix()) {\n <span class=\"text-lg font-semibold text-gray-500 dark:text-gray-400 mr-0.5\">{{ prefix() }}</span>\n }\n {{ displayValue() }}\n @if (suffix()) {\n <span class=\"text-lg font-semibold text-gray-500 dark:text-gray-400 ml-0.5\">{{ suffix() }}</span>\n }\n </p>\n\n <!-- Sparkline (right-aligned) -->\n @if (hasSparkline()) {\n <div class=\"shrink-0 w-[5.5rem] h-8 mb-0.5\" aria-hidden=\"true\">\n <svg viewBox=\"0 0 120 36\" preserveAspectRatio=\"none\" class=\"h-full w-full overflow-visible\">\n <!-- Fill area -->\n <path\n [attr.d]=\"sparklineFill()\"\n [attr.fill]=\"sparklineColorClass().fill\"\n stroke=\"none\"\n />\n <!-- Line -->\n <polyline\n [attr.points]=\"sparklinePath()\"\n fill=\"none\"\n [attr.stroke]=\"sparklineColorClass().stroke\"\n stroke-width=\"1.75\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </div>\n }\n </div>\n\n <!-- Row 3 — trend + label -->\n @if (hasTrend()) {\n <div class=\"flex items-center gap-2\">\n <span\n class=\"inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px] font-semibold\"\n [ngClass]=\"trendClasses()\"\n >\n <!-- Arrow icon -->\n @if (trend() === 'up') {\n <svg class=\"h-3 w-3\" viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M8 3.293l4.354 4.353-1.414 1.415L9 7.12V13H7V7.12L5.06 9.06 3.646 7.647 8 3.293z\" clip-rule=\"evenodd\"/>\n </svg>\n }\n @if (trend() === 'down') {\n <svg class=\"h-3 w-3\" viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M8 12.707l-4.354-4.353 1.414-1.415L7 8.88V3h2v5.88l1.94-1.94 1.414 1.414L8 12.707z\" clip-rule=\"evenodd\"/>\n </svg>\n }\n @if (displayTrendPercent()) {\n {{ displayTrendPercent() }}\n }\n @if (trendValue() !== null && displayTrendPercent() === '') {\n {{ trendValue()! | number }}\n }\n </span>\n @if (trendLabel()) {\n <span class=\"text-xs text-gray-500 dark:text-gray-400\">{{ trendLabel() }}</span>\n }\n </div>\n }\n </div>\n\n <!-- View more link -->\n @if (hasLink()) {\n <button\n type=\"button\"\n class=\"flex items-center justify-between border-t border-gray-100 px-5 py-2.5\n text-xs font-medium text-gray-500 transition-colors duration-150\n hover:bg-gray-50/80 hover:text-primary-600\n dark:border-gray-800 dark:text-gray-400 dark:hover:bg-gray-800/60\n dark:hover:text-primary-400\"\n (click)=\"linkClick.emit()\"\n >\n <span>{{ linkLabel() }}</span>\n <svg class=\"h-3.5 w-3.5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M5 12h14M12 5l7 7-7 7\" />\n </svg>\n </button>\n }\n }\n\n <!-- ════════════════════════ COMPACT LAYOUT ══════════════════════════════ -->\n @if (!loading() && layout() === 'compact') {\n <div class=\"flex items-center gap-3.5 px-4 py-3.5\">\n @if (hasIcon()) {\n <span\n class=\"inline-flex shrink-0 items-center justify-center h-9 w-9 rounded-xl\n ring-1 ring-inset ring-black/[0.06] dark:ring-white/[0.06]\"\n [ngClass]=\"iconClasses()\"\n aria-hidden=\"true\"\n >\n <svg class=\"h-4 w-4\" [attr.viewBox]=\"iconViewBox()\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"1.75\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path [attr.d]=\"iconPath()\" />\n </svg>\n </span>\n }\n <div class=\"flex-1 min-w-0\">\n <p class=\"text-[12px] font-medium text-gray-500 dark:text-gray-400 truncate leading-tight\">{{ title() }}</p>\n <p class=\"text-xl font-bold text-gray-900 dark:text-white tabular-nums leading-snug\">\n @if (prefix()) { <span class=\"text-sm text-gray-500 dark:text-gray-400\">{{ prefix() }}</span> }\n {{ displayValue() }}\n @if (suffix()) { <span class=\"text-sm text-gray-500 dark:text-gray-400\">{{ suffix() }}</span> }\n </p>\n </div>\n @if (hasTrend()) {\n <span\n class=\"inline-flex shrink-0 items-center gap-0.5 rounded-full px-2 py-0.5 text-[11px] font-semibold\"\n [ngClass]=\"trendClasses()\"\n >\n @if (trend() === 'up') {\n <svg class=\"h-3 w-3\" viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M8 3.293l4.354 4.353-1.414 1.415L9 7.12V13H7V7.12L5.06 9.06 3.646 7.647 8 3.293z\" clip-rule=\"evenodd\"/>\n </svg>\n }\n @if (trend() === 'down') {\n <svg class=\"h-3 w-3\" viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M8 12.707l-4.354-4.353 1.414-1.415L7 8.88V3h2v5.88l1.94-1.94 1.414 1.414L8 12.707z\" clip-rule=\"evenodd\"/>\n </svg>\n }\n @if (displayTrendPercent()) { {{ displayTrendPercent() }} }\n </span>\n }\n </div>\n }\n\n <!-- ════════════════════════ HORIZONTAL LAYOUT ═══════════════════════════ -->\n <!-- Two-column split: left = icon + label + context | right = value + trend -->\n @if (!loading() && layout() === 'horizontal') {\n <div class=\"flex items-stretch min-h-[88px]\">\n\n <!-- Left column — identity / context -->\n <div class=\"flex flex-1 flex-col justify-center gap-1.5 px-5 py-4\n border-r border-gray-100 dark:border-gray-800\">\n @if (hasIcon()) {\n <span\n class=\"inline-flex items-center justify-center h-9 w-9 rounded-xl mb-1\n ring-1 ring-inset ring-black/[0.06] dark:ring-white/[0.06]\"\n [ngClass]=\"iconClasses()\"\n aria-hidden=\"true\"\n >\n <svg class=\"h-4 w-4\" [attr.viewBox]=\"iconViewBox()\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"1.75\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path [attr.d]=\"iconPath()\" />\n </svg>\n </span>\n }\n <p class=\"text-[13px] font-semibold text-gray-700 dark:text-gray-200 tracking-tight leading-tight truncate\">\n {{ title() }}\n </p>\n @if (description()) {\n <p class=\"text-[11px] text-gray-500 dark:text-gray-400 truncate\">{{ description() }}</p>\n }\n @if (trendLabel()) {\n <p class=\"text-[11px] text-gray-500 dark:text-gray-400\">{{ trendLabel() }}</p>\n }\n </div>\n\n <!-- Right column — metric + trend -->\n <div class=\"flex flex-col items-end justify-center gap-2 px-5 py-4 shrink-0\">\n <p class=\"text-[26px] font-bold tracking-tight text-gray-900 dark:text-white tabular-nums leading-none\">\n @if (prefix()) {\n <span class=\"text-base font-semibold text-gray-500 dark:text-gray-400 mr-0.5\">{{ prefix() }}</span>\n }\n {{ displayValue() }}\n @if (suffix()) {\n <span class=\"text-base font-semibold text-gray-500 dark:text-gray-400 ml-0.5\">{{ suffix() }}</span>\n }\n </p>\n\n @if (hasTrend()) {\n <span\n class=\"inline-flex items-center gap-0.5 rounded-full px-2 py-0.5 text-[11px] font-semibold\"\n [ngClass]=\"trendClasses()\"\n >\n @if (trend() === 'up') {\n <svg class=\"h-3 w-3\" viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M8 3.293l4.354 4.353-1.414 1.415L9 7.12V13H7V7.12L5.06 9.06 3.646 7.647 8 3.293z\" clip-rule=\"evenodd\"/>\n </svg>\n }\n @if (trend() === 'down') {\n <svg class=\"h-3 w-3\" viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M8 12.707l-4.354-4.353 1.414-1.415L7 8.88V3h2v5.88l1.94-1.94 1.414 1.414L8 12.707z\" clip-rule=\"evenodd\"/>\n </svg>\n }\n {{ displayTrendPercent() }}\n @if (trendValue() !== null && displayTrendPercent() === '') {\n {{ trendValue()! | number }}\n }\n </span>\n }\n\n @if (hasSparkline()) {\n <div class=\"w-20 h-7 mt-1\" aria-hidden=\"true\">\n <svg viewBox=\"0 0 120 36\" preserveAspectRatio=\"none\" class=\"h-full w-full overflow-visible\">\n <path [attr.d]=\"sparklineFill()\" [attr.fill]=\"sparklineColorClass().fill\" stroke=\"none\"/>\n <polyline\n [attr.points]=\"sparklinePath()\"\n fill=\"none\"\n [attr.stroke]=\"sparklineColorClass().stroke\"\n stroke-width=\"1.75\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </div>\n }\n </div>\n </div>\n }\n\n <!-- ════════════════════════ MINIMAL LAYOUT ══════════════════════════════ -->\n <!-- Accent bar + tinted background + large value + plain trend text (no pill) -->\n @if (!loading() && layout() === 'minimal') {\n <div class=\"flex items-stretch\" [ngClass]=\"minimalAccentClass().bg\">\n\n <!-- Colored left accent bar —— the defining visual signature of this layout -->\n <div class=\"w-1 shrink-0\" [ngClass]=\"minimalAccentClass().bar\" aria-hidden=\"true\"></div>\n\n <!-- Content -->\n <div class=\"flex flex-1 items-center justify-between gap-4 px-4 py-4\">\n\n <!-- Left: optional icon + label + value -->\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (hasIcon()) {\n <span\n class=\"inline-flex shrink-0 items-center justify-center h-10 w-10 rounded-xl\n ring-1 ring-inset ring-black/[0.06] dark:ring-white/[0.06]\"\n [ngClass]=\"iconClasses()\"\n aria-hidden=\"true\"\n >\n <svg class=\"h-5 w-5\" [attr.viewBox]=\"iconViewBox()\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"1.75\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path [attr.d]=\"iconPath()\" />\n </svg>\n </span>\n }\n <div class=\"min-w-0\">\n <p class=\"text-[10px] font-semibold tracking-widest uppercase text-gray-500 dark:text-gray-400 truncate mb-1\">\n {{ title() }}\n </p>\n <p class=\"text-[30px] font-extrabold text-gray-900 dark:text-white tabular-nums leading-none\">\n @if (prefix()) {\n <span class=\"text-base font-semibold text-gray-500 dark:text-gray-400 mr-0.5\">{{ prefix() }}</span>\n }\n {{ displayValue() }}\n @if (suffix()) {\n <span class=\"text-base font-semibold text-gray-500 dark:text-gray-400 ml-0.5\">{{ suffix() }}</span>\n }\n </p>\n @if (description()) {\n <p class=\"mt-1 text-[11px] text-gray-500 dark:text-gray-400 truncate\">{{ description() }}</p>\n }\n </div>\n </div>\n\n <!-- Right: plain colored trend (no pill/chip) -->\n @if (hasTrend()) {\n <div class=\"flex flex-col items-end shrink-0 gap-0.5\">\n <span\n class=\"inline-flex items-center gap-1 text-sm font-bold\"\n [ngClass]=\"minimalTrendColor()\"\n >\n @if (trend() === 'up') {\n <svg class=\"h-4 w-4\" viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M8 3.293l4.354 4.353-1.414 1.415L9 7.12V13H7V7.12L5.06 9.06 3.646 7.647 8 3.293z\" clip-rule=\"evenodd\"/>\n </svg>\n }\n @if (trend() === 'down') {\n <svg class=\"h-4 w-4\" viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M8 12.707l-4.354-4.353 1.414-1.415L7 8.88V3h2v5.88l1.94-1.94 1.414 1.414L8 12.707z\" clip-rule=\"evenodd\"/>\n </svg>\n }\n {{ displayTrendPercent() }}\n @if (trendValue() !== null && displayTrendPercent() === '') {\n {{ trendValue()! | number }}\n }\n </span>\n @if (trendLabel()) {\n <span class=\"text-[10px] text-gray-500 dark:text-gray-400 text-right leading-tight\">\n {{ trendLabel() }}\n </span>\n }\n </div>\n }\n </div>\n </div>\n }\n\n</article>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;;;AA4CA;AAEA;;;;;;;;;;;AAWG;MAQU,mBAAmB,CAAA;;;AAI9B,IAAA,KAAK,GAAS,KAAK,CAAS,EAAE,4EAAC;;AAG/B,IAAA,KAAK,GAAS,KAAK,CAAkB,EAAE,4EAAC;;AAGxC,IAAA,MAAM,GAAQ,KAAK,CAAS,EAAE,6EAAC;;AAG/B,IAAA,MAAM,GAAQ,KAAK,CAAS,EAAE,6EAAC;;AAG/B,IAAA,WAAW,GAAG,KAAK,CAAS,EAAE,kFAAC;;;AAK/B,IAAA,KAAK,GAAU,KAAK,CAAgB,SAAS,4EAAC;;AAG9C,IAAA,UAAU,GAAK,KAAK,CAAgB,IAAI,iFAAC;;AAGzC,IAAA,YAAY,GAAG,KAAK,CAAgB,IAAI,mFAAC;;AAGzC,IAAA,UAAU,GAAK,KAAK,CAAS,EAAE,iFAAC;;;AAKhC,IAAA,OAAO,GAAG,KAAK,CAAkB,SAAS,8EAAC;;AAG3C,IAAA,MAAM,GAAI,KAAK,CAAiB,SAAS,6EAAC;AAE1C;;;AAGG;AACH,IAAA,QAAQ,GAAM,KAAK,CAAS,EAAE,+EAAC;;AAG/B,IAAA,WAAW,GAAG,KAAK,CAAS,WAAW,kFAAC;;AAIxC;;;;AAIG;AACH,IAAA,SAAS,GAAG,KAAK,CAAe,EAAE,gFAAC;;;AAKnC,IAAA,OAAO,GAAG,KAAK,CAAU,KAAK,8EAAC;;;AAK/B,IAAA,SAAS,GAAG,KAAK,CAAS,EAAE,gFAAC;;AAG7B,IAAA,WAAW,GAAG,KAAK,CAAS,EAAE,kFAAC;;;IAK/B,SAAS,GAAI,MAAM,EAAQ;;IAG3B,SAAS,GAAI,MAAM,EAAQ;;AAI3B,IAAA,OAAO,GAAQ,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,8EAAC;AAChD,IAAA,YAAY,GAAG,QAAQ,CAAC,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC,MAAM,IAAI,CAAC,mFAAC;AAC3D,IAAA,OAAO,GAAQ,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,SAAS,EAAE,8EAAC;IACjD,QAAQ,GAAO,QAAQ,CAAC,MACtB,IAAI,CAAC,KAAK,EAAE,KAAK,SAAS,IAAI,IAAI,CAAC,YAAY,EAAE,KAAK,IAAI,IAAI,IAAI,CAAC,UAAU,EAAE,KAAK,IAAI,EAAA,IAAA,SAAA,GAAA,CAAA,EAAA,SAAA,EAAA,UAAA,EAAA,CAAA,8BAAA,EAAA,CAAA,CACzF;;;AAKO,IAAA,OAAgB,OAAO,GAAK,GAAG;AAC/B,IAAA,OAAgB,OAAO,GAAK,EAAE;AAC9B,IAAA,OAAgB,SAAS,GAAG,CAAC;AAErC;;;;AAIG;AACK,IAAA,eAAe,GAAG,QAAQ,CAAW,MAAK;AAChD,QAAA,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,EAAE;AAC5B,QAAA,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC;AAAE,YAAA,OAAO,EAAE;AAE7B,QAAA,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,GAAG,mBAAmB;AACtE,QAAA,MAAM,IAAI,GAAI,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC;QACnC,MAAM,GAAG,GAAK,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;AAC/B,QAAA,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,GAAG,KAAK,CAAC;QAE5C,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,KAAI;YACtB,MAAM,CAAC,GAAG,GAAG,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,GAAG,GAAG,CAAC,CAAC;YACtD,MAAM,CAAC,GAAG,CAAC,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,GAAG,IAAI,KAAK,KAAK,CAAC,GAAG,GAAG,GAAG,CAAC,CAAC;AAC7D,YAAA,OAAO,CAAA,EAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA,CAAA,EAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE;AAC1C,QAAA,CAAC,CAAC;AACJ,IAAA,CAAC,sFAAC;;AAGF,IAAA,aAAa,GAAG,QAAQ,CAAS,MAAK;AACpC,QAAA,MAAM,MAAM,GAAG,IAAI,CAAC,eAAe,EAAE;AACrC,QAAA,OAAO,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE;AAC9C,IAAA,CAAC,oFAAC;;AAGF,IAAA,aAAa,GAAG,QAAQ,CAAS,MAAK;AACpC,QAAA,MAAM,MAAM,GAAG,IAAI,CAAC,eAAe,EAAE;QACrC,IAAI,CAAC,MAAM,CAAC,MAAM;AAAE,YAAA,OAAO,EAAE;QAE7B,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,GAAG,mBAAmB;AAC1D,QAAA,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AACrC,QAAA,MAAM,IAAI,GAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAErD,OAAO;AACL,YAAA,CAAA,EAAA,EAAK,KAAK,CAAA,CAAA,EAAI,CAAC,GAAG,GAAG,CAAA,CAAE;AACvB,YAAA,CAAA,EAAA,EAAK,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA,CAAE;AACzB,YAAA,CAAA,EAAA,EAAK,IAAI,CAAA,CAAA,EAAI,CAAC,GAAG,GAAG,CAAA,CAAE;YACtB,GAAG;AACJ,SAAA,CAAC,IAAI,CAAC,GAAG,CAAC;AACb,IAAA,CAAC,oFAAC;;AAGF,IAAA,YAAY,GAAG,QAAQ,CAAS,MAAK;AACnC,QAAA,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE;AACtB,QAAA,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE;YACzB,IAAI,CAAC,IAAI,SAAS;AAAE,gBAAA,OAAO,CAAC,CAAC,GAAG,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,GAAG;YAC3D,IAAI,CAAC,IAAI,KAAK;AAAM,gBAAA,OAAO,CAAC,CAAC,GAAG,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,GAAO,GAAG;AAC3D,YAAA,OAAO,CAAC,CAAC,cAAc,EAAE;QAC3B;AACA,QAAA,OAAO,CAAC;AACV,IAAA,CAAC,mFAAC;;AAGF,IAAA,mBAAmB,GAAG,QAAQ,CAAS,MAAK;AAC1C,QAAA,MAAM,CAAC,GAAG,IAAI,CAAC,YAAY,EAAE;QAC7B,IAAI,CAAC,KAAK,IAAI;AAAE,YAAA,OAAO,EAAE;AACzB,QAAA,OAAO,CAAA,EAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG;AACrC,IAAA,CAAC,0FAAC;;;AAKF,IAAA,WAAW,GAAG,QAAQ,CAAS,MAAK;AAClC,QAAA,OAAO,+DAA+D;AACxE,IAAA,CAAC,kFAAC;;AAGF,IAAA,WAAW,GAAG,QAAQ,CAAS,MAAK;AAClC,QAAA,OAAO,yCAAyC;AAClD,IAAA,CAAC,kFAAC;;AAGF,IAAA,mBAAmB,GAAG,QAAQ,CAAmC,MAAK;QACpE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,wBAAwB,EAAE;AAC9D,IAAA,CAAC,0FAAC;;AAGF,IAAA,YAAY,GAAG,QAAQ,CAAS,MAAK;AACnC,QAAA,QAAQ,IAAI,CAAC,KAAK,EAAE;AAClB,YAAA,KAAK,IAAI,EAAI,OAAO,8HAA8H;AAClJ,YAAA,KAAK,MAAM,EAAG,OAAO,sGAAsG;AAC3H,YAAA,SAAc,OAAO,uGAAuG;;AAEhI,IAAA,CAAC,mFAAC;;AAGF,IAAA,kBAAkB,GAAG,QAAQ,CAA8B,MAAK;QAC9D,OAAO,EAAE,GAAG,EAAE,8BAA8B,EAAE,EAAE,EAAE,EAAE,EAAE;AACxD,IAAA,CAAC,yFAAC;;AAGF,IAAA,iBAAiB,GAAG,QAAQ,CAAS,MAAK;AACxC,QAAA,QAAQ,IAAI,CAAC,KAAK,EAAE;AAClB,YAAA,KAAK,IAAI,EAAI,OAAO,wCAAwC;AAC5D,YAAA,KAAK,MAAM,EAAE,OAAO,gCAAgC;AACpD,YAAA,SAAa,OAAO,kCAAkC;;AAE1D,IAAA,CAAC,wFAAC;wGAnMS,mBAAmB,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;AAAnB,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,SAAA,EAAA,IAAA,EAAA,mBAAmB,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,cAAA,EAAA,MAAA,EAAA,EAAA,KAAA,EAAA,EAAA,iBAAA,EAAA,OAAA,EAAA,UAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,KAAA,EAAA,EAAA,iBAAA,EAAA,OAAA,EAAA,UAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,MAAA,EAAA,EAAA,iBAAA,EAAA,QAAA,EAAA,UAAA,EAAA,QAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,MAAA,EAAA,EAAA,iBAAA,EAAA,QAAA,EAAA,UAAA,EAAA,QAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,WAAA,EAAA,EAAA,iBAAA,EAAA,aAAA,EAAA,UAAA,EAAA,aAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,KAAA,EAAA,EAAA,iBAAA,EAAA,OAAA,EAAA,UAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,UAAA,EAAA,EAAA,iBAAA,EAAA,YAAA,EAAA,UAAA,EAAA,YAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,YAAA,EAAA,EAAA,iBAAA,EAAA,cAAA,EAAA,UAAA,EAAA,cAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,UAAA,EAAA,EAAA,iBAAA,EAAA,YAAA,EAAA,UAAA,EAAA,YAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,OAAA,EAAA,EAAA,iBAAA,EAAA,SAAA,EAAA,UAAA,EAAA,SAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,MAAA,EAAA,EAAA,iBAAA,EAAA,QAAA,EAAA,UAAA,EAAA,QAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,QAAA,EAAA,EAAA,iBAAA,EAAA,UAAA,EAAA,UAAA,EAAA,UAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,WAAA,EAAA,EAAA,iBAAA,EAAA,aAAA,EAAA,UAAA,EAAA,aAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,SAAA,EAAA,EAAA,iBAAA,EAAA,WAAA,EAAA,UAAA,EAAA,WAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,OAAA,EAAA,EAAA,iBAAA,EAAA,SAAA,EAAA,UAAA,EAAA,SAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,SAAA,EAAA,EAAA,iBAAA,EAAA,WAAA,EAAA,UAAA,EAAA,WAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,WAAA,EAAA,EAAA,iBAAA,EAAA,aAAA,EAAA,UAAA,EAAA,aAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,EAAA,OAAA,EAAA,EAAA,SAAA,EAAA,WAAA,EAAA,SAAA,EAAA,WAAA,EAAA,EAAA,QAAA,EAAA,EAAA,EAAA,QAAA,ECjEhC,u8iBAwWA,EAAA,YAAA,EAAA,CAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,ED3SY,OAAO,+EAAE,WAAW,EAAA,IAAA,EAAA,QAAA,EAAA,CAAA,EAAA,eAAA,EAAA,EAAA,CAAA,uBAAA,CAAA,MAAA,EAAA,CAAA;;4FAInB,mBAAmB,EAAA,UAAA,EAAA,CAAA;kBAP/B,SAAS;+BACE,cAAc,EAAA,UAAA,EACZ,IAAI,EAAA,OAAA,EACP,CAAC,OAAO,EAAE,WAAW,CAAC,EAAA,eAAA,EAEd,uBAAuB,CAAC,MAAM,EAAA,QAAA,EAAA,u8iBAAA,EAAA;;;AE/DjD;;AAEG;;;;"}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@styloviz/stat-card",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "KPI metric cards with sparklines, trend chips and four layout variants.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "sazzad-bs23",
|
|
7
|
+
"homepage": "https://styloviz.dev",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/rhossain/angular-tailwind-dashboard-components.git",
|
|
11
|
+
"directory": "projects/stat-card-free"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/rhossain/angular-tailwind-dashboard-components/issues"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"@angular/common": ">=21.0.0",
|
|
18
|
+
"@angular/core": ">=21.0.0"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
|
25
|
+
},
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"module": "fesm2022/styloviz-stat-card.mjs",
|
|
28
|
+
"typings": "types/styloviz-stat-card.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
"./package.json": {
|
|
31
|
+
"default": "./package.json"
|
|
32
|
+
},
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./types/styloviz-stat-card.d.ts",
|
|
35
|
+
"default": "./fesm2022/styloviz-stat-card.mjs"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"type": "module",
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"tslib": "^2.3.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import * as _angular_core from '@angular/core';
|
|
2
|
+
|
|
3
|
+
type StatCardVariant = 'default';
|
|
4
|
+
type StatCardTrend = 'up' | 'down' | 'neutral';
|
|
5
|
+
type StatCardLayout = 'default' | 'compact' | 'horizontal' | 'minimal';
|
|
6
|
+
/** A single point in the inline sparkline (value 0–100 scale, or raw). */
|
|
7
|
+
interface SparkPoint {
|
|
8
|
+
value: number;
|
|
9
|
+
label?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* All data needed to render a StatCard.
|
|
13
|
+
* Pass these individually via @Input() or compose them into a stats object.
|
|
14
|
+
*/
|
|
15
|
+
interface StatCardData {
|
|
16
|
+
title: string;
|
|
17
|
+
value: string | number;
|
|
18
|
+
previousValue?: string | number;
|
|
19
|
+
prefix?: string;
|
|
20
|
+
suffix?: string;
|
|
21
|
+
trend?: StatCardTrend;
|
|
22
|
+
trendValue?: number;
|
|
23
|
+
trendPercent?: number;
|
|
24
|
+
trendLabel?: string;
|
|
25
|
+
iconPath?: string;
|
|
26
|
+
iconViewBox?: string;
|
|
27
|
+
variant?: StatCardVariant;
|
|
28
|
+
sparkline?: SparkPoint[];
|
|
29
|
+
loading?: boolean;
|
|
30
|
+
description?: string;
|
|
31
|
+
linkLabel?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* StatCard — A metric summary card for dashboards.
|
|
35
|
+
*
|
|
36
|
+
* Features:
|
|
37
|
+
* - 4 layout modes: default · compact · horizontal · minimal
|
|
38
|
+
* - 6 semantic color variants with icon tinting
|
|
39
|
+
* - Trend indicator (up / down / neutral) with % and absolute delta
|
|
40
|
+
* - Inline SVG sparkline chart (no external lib)
|
|
41
|
+
* - Skeleton loading state
|
|
42
|
+
* - Accessible markup (aria-label, role="status")
|
|
43
|
+
* - Click-through CTA link slot
|
|
44
|
+
*/
|
|
45
|
+
declare class SvStatCardComponent {
|
|
46
|
+
/** Card title / metric label. */
|
|
47
|
+
title: _angular_core.InputSignal<string>;
|
|
48
|
+
/** Primary metric value — string or number, displayed as-is or formatted. */
|
|
49
|
+
value: _angular_core.InputSignal<string | number>;
|
|
50
|
+
/** Optional prefix rendered before the value (e.g. "$", "€"). */
|
|
51
|
+
prefix: _angular_core.InputSignal<string>;
|
|
52
|
+
/** Optional suffix rendered after the value (e.g. "K", "%", "ms"). */
|
|
53
|
+
suffix: _angular_core.InputSignal<string>;
|
|
54
|
+
/** Short helper text beneath the title (e.g. "Last 30 days"). */
|
|
55
|
+
description: _angular_core.InputSignal<string>;
|
|
56
|
+
/** Direction of change. @default 'neutral' */
|
|
57
|
+
trend: _angular_core.InputSignal<StatCardTrend>;
|
|
58
|
+
/** Absolute delta value shown alongside the trend icon. */
|
|
59
|
+
trendValue: _angular_core.InputSignal<number | null>;
|
|
60
|
+
/** Percentage change shown in the trend chip. */
|
|
61
|
+
trendPercent: _angular_core.InputSignal<number | null>;
|
|
62
|
+
/** Comparison context label (e.g. "vs last month"). @default '' */
|
|
63
|
+
trendLabel: _angular_core.InputSignal<string>;
|
|
64
|
+
/** Color variant — drives icon background, accent line and trend colors. @default 'default' */
|
|
65
|
+
variant: _angular_core.InputSignal<"default">;
|
|
66
|
+
/** Card layout mode. @default 'default' */
|
|
67
|
+
layout: _angular_core.InputSignal<StatCardLayout>;
|
|
68
|
+
/**
|
|
69
|
+
* SVG path `d` attribute for the icon.
|
|
70
|
+
* Leave empty to render no icon.
|
|
71
|
+
*/
|
|
72
|
+
iconPath: _angular_core.InputSignal<string>;
|
|
73
|
+
/** SVG viewBox for the icon. @default '0 0 24 24' */
|
|
74
|
+
iconViewBox: _angular_core.InputSignal<string>;
|
|
75
|
+
/**
|
|
76
|
+
* Array of data points for the inline sparkline.
|
|
77
|
+
* Values should be on a consistent numeric scale; the component
|
|
78
|
+
* normalises them internally. Empty array hides the sparkline.
|
|
79
|
+
*/
|
|
80
|
+
sparkline: _angular_core.InputSignal<SparkPoint[]>;
|
|
81
|
+
/** Show skeleton loading shimmer instead of content. @default false */
|
|
82
|
+
loading: _angular_core.InputSignal<boolean>;
|
|
83
|
+
/** Label for the bottom "view more" link. Empty string hides the link. */
|
|
84
|
+
linkLabel: _angular_core.InputSignal<string>;
|
|
85
|
+
/** Additional CSS classes merged on the root card element. */
|
|
86
|
+
customClass: _angular_core.InputSignal<string>;
|
|
87
|
+
/** Emitted when the card body is clicked (the full card is interactive). */
|
|
88
|
+
cardClick: _angular_core.OutputEmitterRef<void>;
|
|
89
|
+
/** Emitted when the "view more" link is clicked. */
|
|
90
|
+
linkClick: _angular_core.OutputEmitterRef<void>;
|
|
91
|
+
hasIcon: _angular_core.Signal<boolean>;
|
|
92
|
+
hasSparkline: _angular_core.Signal<boolean>;
|
|
93
|
+
hasLink: _angular_core.Signal<boolean>;
|
|
94
|
+
hasTrend: _angular_core.Signal<boolean>;
|
|
95
|
+
/** SVG canvas constants for the inline sparkline. */
|
|
96
|
+
private static readonly SPARK_W;
|
|
97
|
+
private static readonly SPARK_H;
|
|
98
|
+
private static readonly SPARK_PAD;
|
|
99
|
+
/**
|
|
100
|
+
* Normalised `"x,y"` coordinate strings for each sparkline point.
|
|
101
|
+
* Computed once and shared by both `sparklinePath` and `sparklineFill`
|
|
102
|
+
* to avoid duplicating the min/max normalisation logic.
|
|
103
|
+
*/
|
|
104
|
+
private sparklineCoords;
|
|
105
|
+
/** Normalised sparkline points mapped to an SVG polyline string. */
|
|
106
|
+
sparklinePath: _angular_core.Signal<string>;
|
|
107
|
+
/** Fill path for the area under the sparkline. */
|
|
108
|
+
sparklineFill: _angular_core.Signal<string>;
|
|
109
|
+
/** Formatted value — passes numbers through DecimalPipe logic, strings untouched. */
|
|
110
|
+
displayValue: _angular_core.Signal<string>;
|
|
111
|
+
/** Formatted trend percent string, e.g. "12.4%". */
|
|
112
|
+
displayTrendPercent: _angular_core.Signal<string>;
|
|
113
|
+
/** Icon wrapper background + text. */
|
|
114
|
+
iconClasses: _angular_core.Signal<string>;
|
|
115
|
+
/** Border color for the card — subtle gray, no colored accent. */
|
|
116
|
+
accentClass: _angular_core.Signal<string>;
|
|
117
|
+
/** Sparkline stroke + fill color. */
|
|
118
|
+
sparklineColorClass: _angular_core.Signal<{
|
|
119
|
+
stroke: string;
|
|
120
|
+
fill: string;
|
|
121
|
+
}>;
|
|
122
|
+
/** Trend chip color classes. */
|
|
123
|
+
trendClasses: _angular_core.Signal<string>;
|
|
124
|
+
/** Left accent bar + background tint for the minimal layout. */
|
|
125
|
+
minimalAccentClass: _angular_core.Signal<{
|
|
126
|
+
bar: string;
|
|
127
|
+
bg: string;
|
|
128
|
+
}>;
|
|
129
|
+
/** Plain trend text color used by the minimal layout (no pill). */
|
|
130
|
+
minimalTrendColor: _angular_core.Signal<string>;
|
|
131
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<SvStatCardComponent, never>;
|
|
132
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<SvStatCardComponent, "sv-stat-card", never, { "title": { "alias": "title"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; "prefix": { "alias": "prefix"; "required": false; "isSignal": true; }; "suffix": { "alias": "suffix"; "required": false; "isSignal": true; }; "description": { "alias": "description"; "required": false; "isSignal": true; }; "trend": { "alias": "trend"; "required": false; "isSignal": true; }; "trendValue": { "alias": "trendValue"; "required": false; "isSignal": true; }; "trendPercent": { "alias": "trendPercent"; "required": false; "isSignal": true; }; "trendLabel": { "alias": "trendLabel"; "required": false; "isSignal": true; }; "variant": { "alias": "variant"; "required": false; "isSignal": true; }; "layout": { "alias": "layout"; "required": false; "isSignal": true; }; "iconPath": { "alias": "iconPath"; "required": false; "isSignal": true; }; "iconViewBox": { "alias": "iconViewBox"; "required": false; "isSignal": true; }; "sparkline": { "alias": "sparkline"; "required": false; "isSignal": true; }; "loading": { "alias": "loading"; "required": false; "isSignal": true; }; "linkLabel": { "alias": "linkLabel"; "required": false; "isSignal": true; }; "customClass": { "alias": "customClass"; "required": false; "isSignal": true; }; }, { "cardClick": "cardClick"; "linkClick": "linkClick"; }, never, never, true, never>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export { SvStatCardComponent };
|
|
136
|
+
export type { SparkPoint, StatCardData, StatCardLayout, StatCardTrend, StatCardVariant };
|