Skip to content

Commit c6f9fc5

Browse files
creilly11235claude
andcommitted
feat(apollo-vertex): add metric-card registry component
Adds MetricCard and MetricCardSkeleton as a Vertex Component for displaying labeled metrics with optional trend badges. Includes registry entry, tsconfig path alias, documentation page, and navigation entry. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c749406 commit c6f9fc5

6 files changed

Lines changed: 272 additions & 0 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default {
2+
"ai-chat": "AI Chat",
3+
"data-table": "Data Table",
4+
"metric-card": "Metric Card",
5+
shell: "Shell",
6+
};
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { MetricCardTemplate, MetricCardSkeletonTemplate } from '@/templates/MetricCardTemplate';
2+
3+
# Metric Card
4+
5+
Displays a labeled metric with a value, optional trend direction, and percentage change badge. Includes a skeleton loading state for consistent UX during data fetches.
6+
7+
<div className="not-prose my-8 rounded-lg border overflow-hidden bg-gradient-to-br from-muted/40 via-background to-muted/40 dark:from-muted/20 dark:via-background dark:to-muted/20">
8+
<MetricCardTemplate />
9+
</div>
10+
11+
## Features
12+
13+
- **Trend indicator**: Shows an up/down badge colored by positive (green) or negative (red) trend
14+
- **Context-aware direction**: `isLowerBetter` flips what counts as a positive trend (e.g., response time going down is good)
15+
- **Loading skeleton**: `MetricCardSkeleton` matches the card dimensions for seamless loading states
16+
- **Card variants**: Supports the same `variant` prop as the base Card (`default`, `solid`)
17+
18+
## Installation
19+
20+
```bash
21+
npx shadcn@latest add @uipath/metric-card
22+
```
23+
24+
### Dependencies
25+
26+
This component depends on the following registry components:
27+
28+
- [Card](/shadcn-components/card)
29+
- [Badge](/shadcn-components/badge)
30+
- [Skeleton](/shadcn-components/skeleton)
31+
32+
## Usage
33+
34+
```tsx
35+
import { MetricCard, MetricCardSkeleton } from '@/components/ui/metric-card';
36+
```
37+
38+
### Basic
39+
40+
```tsx
41+
<MetricCard label="Total Revenue" value="$12,450" />
42+
```
43+
44+
### With Trend
45+
46+
```tsx
47+
<MetricCard
48+
label="Total Revenue"
49+
value="$12,450"
50+
trend="up"
51+
percentage={8.2}
52+
/>
53+
```
54+
55+
### Lower is Better
56+
57+
Use `isLowerBetter` for metrics where a downward trend is positive, such as response time or error rate.
58+
59+
```tsx
60+
<MetricCard
61+
label="Avg. Response Time"
62+
value="1.2s"
63+
trend="down"
64+
percentage={15}
65+
isLowerBetter
66+
/>
67+
```
68+
69+
### Dashboard Grid
70+
71+
```tsx
72+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
73+
<MetricCard label="Summaries" value={142} trend="up" percentage={12} />
74+
<MetricCard label="Review Time" value="4.2m" trend="down" percentage={8} isLowerBetter />
75+
<MetricCard label="Approved" value={98} trend="up" percentage={5} />
76+
<MetricCard label="Rejected" value={3} trend="down" percentage={20} isLowerBetter />
77+
</div>
78+
```
79+
80+
### Loading State
81+
82+
<div className="not-prose my-8 rounded-lg border overflow-hidden bg-gradient-to-br from-muted/40 via-background to-muted/40 dark:from-muted/20 dark:via-background dark:to-muted/20">
83+
<MetricCardSkeletonTemplate />
84+
</div>
85+
86+
```tsx
87+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
88+
<MetricCardSkeleton />
89+
<MetricCardSkeleton />
90+
<MetricCardSkeleton />
91+
<MetricCardSkeleton />
92+
</div>
93+
```
94+
95+
## API Reference
96+
97+
### MetricCard
98+
99+
| Prop | Type | Default | Description |
100+
|------|------|---------|-------------|
101+
| `label` | `string` || The metric label |
102+
| `value` | `string \| number` || The metric value to display |
103+
| `trend` | `'up' \| 'down' \| 'neutral'` || Direction of the trend |
104+
| `percentage` | `number` || Percentage change amount |
105+
| `isLowerBetter` | `boolean` | `false` | If true, a downward trend is positive (green) |
106+
| `variant` | `'default' \| 'solid'` | `'default'` | Card styling variant |
107+
108+
### MetricCardSkeleton
109+
110+
| Prop | Type | Default | Description |
111+
|------|------|---------|-------------|
112+
| `variant` | `'default' \| 'solid'` | `'default'` | Card styling variant |

apps/apollo-vertex/registry.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,19 @@
738738
}
739739
]
740740
},
741+
{
742+
"name": "metric-card",
743+
"type": "registry:ui",
744+
"title": "Metric Card",
745+
"description": "Displays a labeled metric with value, optional trend indicator, and percentage change badge.",
746+
"registryDependencies": ["@uipath/card", "@uipath/badge", "skeleton"],
747+
"files": [
748+
{
749+
"path": "registry/metric-card/metric-card.tsx",
750+
"type": "registry:ui"
751+
}
752+
]
753+
},
741754
{
742755
"name": "navigation-menu",
743756
"type": "registry:ui",
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import * as React from "react";
2+
3+
import { Badge } from "@/components/ui/badge";
4+
import { Card } from "@/components/ui/card";
5+
import { Skeleton } from "@/components/ui/skeleton";
6+
7+
interface MetricCardProps extends React.ComponentProps<"div"> {
8+
label: string;
9+
value: string | number;
10+
trend?: "up" | "down" | "neutral";
11+
percentage?: number;
12+
isLowerBetter?: boolean;
13+
variant?: "default" | "solid" | "glass";
14+
}
15+
16+
function MetricCard({
17+
label,
18+
value,
19+
trend,
20+
percentage,
21+
isLowerBetter = false,
22+
variant = "default",
23+
className,
24+
...props
25+
}: MetricCardProps) {
26+
const showTrend =
27+
trend != null &&
28+
trend !== "neutral" &&
29+
percentage != null &&
30+
percentage !== 0;
31+
const isPositiveTrend = isLowerBetter ? trend === "down" : trend === "up";
32+
const sign = trend === "up" ? "+" : "-";
33+
const badgeStatus = isPositiveTrend ? "success" : "error";
34+
35+
return (
36+
<Card
37+
data-slot="metric-card"
38+
variant={variant}
39+
className={className}
40+
{...props}
41+
>
42+
<div data-slot="metric-card-content" className="flex flex-col gap-1 p-6">
43+
<p
44+
data-slot="metric-card-label"
45+
className="text-xs font-semibold leading-tight text-muted-foreground line-clamp-2"
46+
>
47+
{label}
48+
</p>
49+
<div
50+
data-slot="metric-card-value"
51+
className="flex items-center gap-x-3 gap-y-0.5 flex-wrap"
52+
>
53+
<p className="text-2xl md:text-3xl font-bold leading-tight tracking-tight text-foreground">
54+
{value}
55+
</p>
56+
{showTrend && (
57+
<Badge variant="secondary" status={badgeStatus}>
58+
{`${sign}${Math.abs(percentage)}%`}
59+
</Badge>
60+
)}
61+
</div>
62+
</div>
63+
</Card>
64+
);
65+
}
66+
67+
interface MetricCardSkeletonProps extends React.ComponentProps<"div"> {
68+
variant?: "default" | "solid" | "glass";
69+
}
70+
71+
function MetricCardSkeleton({
72+
variant = "default",
73+
className,
74+
...props
75+
}: MetricCardSkeletonProps) {
76+
return (
77+
<Card
78+
data-slot="metric-card"
79+
variant={variant}
80+
className={className}
81+
{...props}
82+
>
83+
<div data-slot="metric-card-content" className="flex flex-col gap-1 p-6">
84+
<Skeleton className="h-4 w-28" />
85+
<div className="flex items-center gap-4">
86+
<Skeleton className="h-9 w-14" />
87+
</div>
88+
</div>
89+
</Card>
90+
);
91+
}
92+
93+
export { MetricCard, MetricCardSkeleton };
94+
export type { MetricCardProps, MetricCardSkeletonProps };
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"use client";
2+
3+
import { MetricCard, MetricCardSkeleton } from "@/components/ui/metric-card";
4+
5+
export function MetricCardTemplate() {
6+
return (
7+
<div className="p-6">
8+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
9+
<MetricCard
10+
label="Summaries Generated"
11+
value={1_284}
12+
trend="up"
13+
percentage={12}
14+
/>
15+
<MetricCard
16+
label="Review Time"
17+
value="4.2m"
18+
trend="down"
19+
percentage={8}
20+
isLowerBetter
21+
/>
22+
<MetricCard label="Approved" value={942} trend="up" percentage={5} />
23+
<MetricCard
24+
label="Rejected"
25+
value={18}
26+
trend="down"
27+
percentage={20}
28+
isLowerBetter
29+
/>
30+
</div>
31+
</div>
32+
);
33+
}
34+
35+
export function MetricCardSkeletonTemplate() {
36+
return (
37+
<div className="p-6">
38+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
39+
<MetricCardSkeleton />
40+
<MetricCardSkeleton />
41+
<MetricCardSkeleton />
42+
<MetricCardSkeleton />
43+
</div>
44+
</div>
45+
);
46+
}

apps/apollo-vertex/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"@/components/ui/kbd": ["./registry/kbd/kbd"],
5555
"@/components/ui/label": ["./registry/label/label"],
5656
"@/components/ui/menubar": ["./registry/menubar/menubar"],
57+
"@/components/ui/metric-card": ["./registry/metric-card/metric-card"],
5758
"@/components/ui/native-select": [
5859
"./registry/native-select/native-select"
5960
],

0 commit comments

Comments
 (0)