Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/apollo-vertex/app/components/_meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export default {
kbd: "Kbd",
label: "Label",
menubar: "Menubar",
"metric-card": "Metric Card",
"navigation-menu": "Navigation Menu",
pagination: "Pagination",
popover: "Popover",
Expand Down
112 changes: 112 additions & 0 deletions apps/apollo-vertex/app/components/metric-card/page.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { MetricCardTemplate, MetricCardSkeletonTemplate } from '@/templates/MetricCardTemplate';

# Metric Card

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.

<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">
<MetricCardTemplate />
</div>

## Features

- **Trend indicator**: Shows an up/down badge colored by positive (green) or negative (red) trend
- **Context-aware direction**: `isLowerBetter` flips what counts as a positive trend (e.g., response time going down is good)
- **Loading skeleton**: `MetricCardSkeleton` matches the card dimensions for seamless loading states
- **Card variants**: Supports the same `variant` prop as the base Card (`default`, `solid`)

## Installation

```bash
npx shadcn@latest add @uipath/metric-card
```

### Dependencies

This component depends on the following registry components:

- [Card](/shadcn-components/card)
- [Badge](/shadcn-components/badge)
- [Skeleton](/shadcn-components/skeleton)

## Usage

```tsx
import { MetricCard, MetricCardSkeleton } from '@/components/ui/metric-card';
```

### Basic

```tsx
<MetricCard label="Total Revenue" value="$12,450" />
```

### With Trend

```tsx
<MetricCard
label="Total Revenue"
value="$12,450"
trend="up"
percentage={8.2}
/>
```

### Lower is Better

Use `isLowerBetter` for metrics where a downward trend is positive, such as response time or error rate.

```tsx
<MetricCard
label="Avg. Response Time"
value="1.2s"
trend="down"
percentage={15}
isLowerBetter
/>
```

### Dashboard Grid

```tsx
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<MetricCard label="Summaries" value={142} trend="up" percentage={12} />
<MetricCard label="Review Time" value="4.2m" trend="down" percentage={8} isLowerBetter />
<MetricCard label="Approved" value={98} trend="up" percentage={5} />
<MetricCard label="Rejected" value={3} trend="down" percentage={20} isLowerBetter />
</div>
```

### Loading State

<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">
<MetricCardSkeletonTemplate />
</div>

```tsx
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<MetricCardSkeleton />
<MetricCardSkeleton />
<MetricCardSkeleton />
<MetricCardSkeleton />
</div>
```

## API Reference

### MetricCard

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `label` | `string` | — | The metric label |
| `value` | `string \| number` | — | The metric value to display |
| `trend` | `'up' \| 'down' \| 'neutral'` | — | Direction of the trend |
| `percentage` | `number` | — | Percentage change amount |
| `isLowerBetter` | `boolean` | `false` | If true, a downward trend is positive (green) |
| `variant` | `'default' \| 'solid'` | `'default'` | Card styling variant |

### MetricCardSkeleton

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `variant` | `'default' \| 'solid'` | `'default'` | Card styling variant |
13 changes: 13 additions & 0 deletions apps/apollo-vertex/registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,19 @@
}
]
},
{
"name": "metric-card",
"type": "registry:ui",
"title": "Metric Card",
"description": "Displays a labeled metric with value, optional trend indicator, and percentage change badge.",
"registryDependencies": ["@uipath/card", "@uipath/badge", "skeleton"],
"files": [
{
"path": "registry/metric-card/metric-card.tsx",
"type": "registry:ui"
}
]
},
{
"name": "navigation-menu",
"type": "registry:ui",
Expand Down
94 changes: 94 additions & 0 deletions apps/apollo-vertex/registry/metric-card/metric-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import * as React from "react";

import { Badge } from "@/components/ui/badge";
import { Card, type CardProps } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";

interface MetricCardProps extends React.ComponentProps<"div"> {
label: string;
value: string | number;
trend?: "up" | "down" | "neutral";
percentage?: number;
isLowerBetter?: boolean;
variant?: CardProps["variant"];
}

function MetricCard({
label,
value,
trend,
percentage,
isLowerBetter = false,
variant = "default",
className,
...props
}: MetricCardProps) {
const showTrend =
trend != null &&
trend !== "neutral" &&
percentage != null &&
percentage !== 0;
const isPositiveTrend = isLowerBetter ? trend === "down" : trend === "up";
const sign = trend === "up" ? "+" : "-";
const badgeStatus = isPositiveTrend ? "success" : "error";

return (
<Card
data-slot="metric-card"
variant={variant}
className={className}
{...props}
>
<div data-slot="metric-card-content" className="flex flex-col gap-1 p-6">
<p
data-slot="metric-card-label"
className="text-xs font-semibold leading-tight text-muted-foreground line-clamp-2"
>
{label}
</p>
<div
data-slot="metric-card-value"
className="flex items-center gap-x-3 gap-y-0.5 flex-wrap"
>
<p className="text-2xl md:text-3xl font-bold leading-tight tracking-tight text-foreground">
{value}
</p>
{showTrend && (
<Badge variant="secondary" status={badgeStatus}>
{`${sign}${Math.abs(percentage)}%`}
</Badge>
)}
</div>
</div>
</Card>
);
}

interface MetricCardSkeletonProps extends React.ComponentProps<"div"> {
variant?: CardProps["variant"];
}

function MetricCardSkeleton({
variant = "default",
className,
...props
}: MetricCardSkeletonProps) {
return (
<Card
data-slot="metric-card"
variant={variant}
className={className}
{...props}
>
<div data-slot="metric-card-content" className="flex flex-col gap-1 p-6">
<Skeleton className="h-4 w-28" />
<div className="flex items-center gap-4">
<Skeleton className="h-9 w-14" />
</div>
</div>
</Card>
);
}

export { MetricCard, MetricCardSkeleton };
export type { MetricCardProps, MetricCardSkeletonProps };
46 changes: 46 additions & 0 deletions apps/apollo-vertex/templates/MetricCardTemplate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"use client";

import { MetricCard, MetricCardSkeleton } from "@/components/ui/metric-card";

export function MetricCardTemplate() {
return (
<div className="p-6">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<MetricCard
label="Summaries Generated"
value={1_284}
trend="up"
percentage={12}
/>
<MetricCard
label="Review Time"
value="4.2m"
trend="down"
percentage={8}
isLowerBetter
/>
<MetricCard label="Approved" value={942} trend="up" percentage={5} />
<MetricCard
label="Rejected"
value={18}
trend="down"
percentage={20}
isLowerBetter
/>
</div>
</div>
);
}

export function MetricCardSkeletonTemplate() {
return (
<div className="p-6">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<MetricCardSkeleton />
<MetricCardSkeleton />
<MetricCardSkeleton />
<MetricCardSkeleton />
</div>
</div>
);
}
1 change: 1 addition & 0 deletions apps/apollo-vertex/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"@/components/ui/kbd": ["./registry/kbd/kbd"],
"@/components/ui/label": ["./registry/label/label"],
"@/components/ui/menubar": ["./registry/menubar/menubar"],
"@/components/ui/metric-card": ["./registry/metric-card/metric-card"],
"@/components/ui/native-select": [
"./registry/native-select/native-select"
],
Expand Down
Loading