Skip to content

Commit dbe2118

Browse files
committed
feat: add stat component
- Add Stat component with grid-based layout - Add StatLabel, StatValue, StatDescription, and StatChange subcomponents - Add StatIcon with box variant and color prop (primary, blue, green, orange, red, purple, yellow) - Auto-size icons in StatChange component - Include demo with revenue, users, orders, and conversion rate stats
1 parent 87523de commit dbe2118

File tree

2 files changed

+189
-0
lines changed

2 files changed

+189
-0
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import {
2+
ArrowDown,
3+
ArrowUp,
4+
DollarSign,
5+
ShoppingCart,
6+
Users,
7+
} from "lucide-react";
8+
import {
9+
Stat,
10+
StatChange,
11+
StatIcon,
12+
StatLabel,
13+
StatValue,
14+
} from "@/registry/default/ui/stat";
15+
16+
export default function StatDemo() {
17+
return (
18+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
19+
<Stat>
20+
<StatLabel>Total Revenue</StatLabel>
21+
<StatValue>$45,231</StatValue>
22+
<StatIcon variant="box" color="primary">
23+
<DollarSign />
24+
</StatIcon>
25+
<StatChange trend="up">
26+
<ArrowUp />
27+
+20.1% from last month
28+
</StatChange>
29+
</Stat>
30+
31+
<Stat>
32+
<StatLabel>Active Users</StatLabel>
33+
<StatValue>2,350</StatValue>
34+
<StatIcon variant="box" color="blue">
35+
<Users />
36+
</StatIcon>
37+
<StatChange trend="up">
38+
<ArrowUp />
39+
+180 from last week
40+
</StatChange>
41+
</Stat>
42+
43+
<Stat>
44+
<StatLabel>Total Orders</StatLabel>
45+
<StatValue>1,234</StatValue>
46+
<StatIcon variant="box" color="orange">
47+
<ShoppingCart />
48+
</StatIcon>
49+
<StatChange trend="down">
50+
<ArrowDown />
51+
-4.3% from last month
52+
</StatChange>
53+
</Stat>
54+
55+
<Stat>
56+
<StatLabel>Conversion Rate</StatLabel>
57+
<StatValue>3.2%</StatValue>
58+
<StatIcon variant="box" color="green">
59+
<ArrowUp />
60+
</StatIcon>
61+
<StatChange trend="neutral">No change from last week</StatChange>
62+
</Stat>
63+
</div>
64+
);
65+
}

docs/registry/default/ui/stat.tsx

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { cva, type VariantProps } from "class-variance-authority";
2+
import type * as React from "react";
3+
4+
import { cn } from "@/lib/utils";
5+
6+
function Stat({ className, ...props }: React.ComponentProps<"div">) {
7+
return (
8+
<div
9+
data-slot="stat"
10+
className={cn(
11+
"grid grid-cols-[1fr_auto] gap-x-4 gap-y-1 rounded-lg border bg-card p-4 text-card-foreground shadow-sm",
12+
"**:data-[slot=stat-label]:col-span-1",
13+
"**:data-[slot=stat-value]:col-span-1",
14+
"**:data-[slot=stat-icon]:col-start-2 **:data-[slot=stat-icon]:row-span-2 **:data-[slot=stat-icon]:row-start-1 **:data-[slot=stat-icon]:self-start",
15+
"**:data-[slot=stat-change]:col-span-2",
16+
"**:data-[slot=stat-description]:col-span-2",
17+
className,
18+
)}
19+
{...props}
20+
/>
21+
);
22+
}
23+
24+
function StatLabel({ className, ...props }: React.ComponentProps<"div">) {
25+
return (
26+
<div
27+
data-slot="stat-label"
28+
className={cn("font-medium text-muted-foreground text-sm", className)}
29+
{...props}
30+
/>
31+
);
32+
}
33+
34+
function StatValue({ className, ...props }: React.ComponentProps<"div">) {
35+
return (
36+
<div
37+
data-slot="stat-value"
38+
className={cn("font-semibold text-2xl tracking-tight", className)}
39+
{...props}
40+
/>
41+
);
42+
}
43+
44+
function StatDescription({ className, ...props }: React.ComponentProps<"div">) {
45+
return (
46+
<div
47+
data-slot="stat-description"
48+
className={cn("text-muted-foreground text-xs", className)}
49+
{...props}
50+
/>
51+
);
52+
}
53+
54+
function StatChange({
55+
className,
56+
trend,
57+
...props
58+
}: React.ComponentProps<"div"> & { trend?: "up" | "down" | "neutral" }) {
59+
return (
60+
<div
61+
data-slot="stat-change"
62+
data-trend={trend}
63+
className={cn(
64+
"inline-flex items-center gap-1 font-medium text-xs [&_svg:not([class*='size-'])]:size-3 [&_svg]:pointer-events-none [&_svg]:shrink-0",
65+
{
66+
"text-green-600 dark:text-green-400": trend === "up",
67+
"text-red-600 dark:text-red-400": trend === "down",
68+
"text-muted-foreground": trend === "neutral" || !trend,
69+
},
70+
className,
71+
)}
72+
{...props}
73+
/>
74+
);
75+
}
76+
77+
const statIconVariants = cva(
78+
"flex shrink-0 items-center justify-center [&_svg]:pointer-events-none",
79+
{
80+
variants: {
81+
variant: {
82+
default: "text-muted-foreground",
83+
box: "size-9 rounded-md border [&_svg:not([class*='size-'])]:size-4",
84+
},
85+
color: {
86+
default: "bg-muted text-muted-foreground",
87+
primary: "border-primary/20 bg-primary/10 text-primary",
88+
blue: "border-blue-500/20 bg-blue-500/10 text-blue-600 dark:text-blue-400",
89+
green:
90+
"border-green-500/20 bg-green-500/10 text-green-600 dark:text-green-400",
91+
orange:
92+
"border-orange-500/20 bg-orange-500/10 text-orange-600 dark:text-orange-400",
93+
red: "border-red-500/20 bg-red-500/10 text-red-600 dark:text-red-400",
94+
purple:
95+
"border-purple-500/20 bg-purple-500/10 text-purple-600 dark:text-purple-400",
96+
yellow:
97+
"border-yellow-500/20 bg-yellow-500/10 text-yellow-600 dark:text-yellow-400",
98+
},
99+
},
100+
defaultVariants: {
101+
variant: "default",
102+
color: "default",
103+
},
104+
},
105+
);
106+
107+
function StatIcon({
108+
className,
109+
variant = "default",
110+
color = "default",
111+
...props
112+
}: React.ComponentProps<"div"> & VariantProps<typeof statIconVariants>) {
113+
return (
114+
<div
115+
data-slot="stat-icon"
116+
data-variant={variant}
117+
data-color={color}
118+
className={cn(statIconVariants({ variant, color, className }))}
119+
{...props}
120+
/>
121+
);
122+
}
123+
124+
export { Stat, StatLabel, StatValue, StatDescription, StatChange, StatIcon };

0 commit comments

Comments
 (0)