This commit is contained in:
valenti
2026-09-08 07:21:34 -04:00
commit a4b224da8e
169 changed files with 27483 additions and 0 deletions
@@ -0,0 +1,111 @@
"use client";
import React from "react";
import { TrendingUp, TrendingDown } from "lucide-react";
import { cn } from "@/lib/utils";
import { Card, CardBody } from "./Card";
import { Badge } from "./Badge";
export function MetricCard({
label,
value,
delta,
tone = "neutral",
icon,
subtitle,
isPercentage,
className,
}: {
label: string;
value: number | string;
delta?: number; // percentage change, e.g. +5.2
tone?: "neutral" | "brand" | "green" | "red" | "amber" | "violet" | "blue";
icon?: React.ReactNode;
subtitle?: string;
isPercentage?: boolean;
className?: string;
}) {
const showValue = isPercentage
? `${Number(value).toFixed(1)}%`
: typeof value === "number"
? value.toLocaleString()
: value;
return (
<Card className={cn("relative overflow-hidden", className)}>
<CardBody className="flex items-start justify-between gap-4">
<div className="min-w-0">
<div className="text-xs font-medium text-slate-500 uppercase tracking-wide">{label}</div>
<div className="mt-1.5 text-2xl font-semibold text-slate-900 tabular-nums">
{showValue}
</div>
{subtitle !== undefined && (
<div className="mt-1 text-xs text-slate-500">{subtitle}</div>
)}
{delta !== undefined && (
<div className="mt-2 inline-flex items-center gap-1 text-xs">
{delta >= 0 ? (
<>
<TrendingUp className="w-3.5 h-3.5 text-emerald-600" />
<span className="text-emerald-700 font-medium">+{delta.toFixed(1)}%</span>
</>
) : (
<>
<TrendingDown className="w-3.5 h-3.5 text-red-600" />
<span className="text-red-700 font-medium">{delta.toFixed(1)}%</span>
</>
)}
<span className="text-slate-400 ml-1">vs last period</span>
</div>
)}
</div>
{icon && (
<div
className={cn(
"w-10 h-10 rounded-lg grid place-items-center shrink-0 border",
tone === "brand" && "bg-brand-50 border-brand-100 text-brand-600",
tone === "green" && "bg-emerald-50 border-emerald-100 text-emerald-600",
tone === "red" && "bg-red-50 border-red-100 text-red-600",
tone === "amber" && "bg-amber-50 border-amber-100 text-amber-600",
tone === "violet" && "bg-violet-50 border-violet-100 text-violet-600",
tone === "blue" && "bg-sky-50 border-sky-100 text-sky-600",
tone === "neutral" && "bg-slate-50 border-slate-200 text-slate-500",
)}
>
{icon}
</div>
)}
</CardBody>
</Card>
);
}
export function MetricPill({
label,
value,
tone = "neutral",
isPercentage,
}: {
label: string;
value: number | string;
tone?: "neutral" | "brand" | "green" | "red" | "amber" | "blue";
isPercentage?: boolean;
}) {
const shown = isPercentage ? `${Number(value).toFixed(1)}%` : value;
const toneMap: Record<typeof tone, string> = {
neutral: "bg-slate-100 text-slate-700",
brand: "bg-brand-50 text-brand-700",
blue: "bg-sky-50 text-sky-700",
green: "bg-emerald-50 text-emerald-700",
red: "bg-red-50 text-red-700",
amber: "bg-amber-50 text-amber-700",
};
return (
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500">{label}</span>
<Badge tone={tone === "neutral" ? "neutral" : tone === "brand" ? "brand" : tone === "green" ? "green" : tone === "red" ? "red" : "amber"}>
<span className="tabular-nums font-semibold">{shown}</span>
</Badge>
</div>
);
}