113 lines
3.0 KiB
TypeScript
113 lines
3.0 KiB
TypeScript
import type { LucideIcon } from "lucide-react"
|
|
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Progress } from "@/components/ui/progress"
|
|
import type { Tone } from "@/data/types"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
/* ===========================================================================
|
|
The KPI card from IMAGE 1: uppercase label, status chip, icon, headline
|
|
figure, supporting line and an optional progress bar.
|
|
=========================================================================== */
|
|
|
|
const SURFACE: Record<Tone, string> = {
|
|
neutral: "border-line bg-surface",
|
|
primary: "border-line bg-surface",
|
|
good: "border-line bg-surface",
|
|
warn: "border-warn-edge bg-warn-soft",
|
|
bad: "border-bad-edge bg-bad-soft",
|
|
}
|
|
|
|
const VALUE: Record<Tone, string> = {
|
|
neutral: "text-ink",
|
|
primary: "text-ink",
|
|
good: "text-ink",
|
|
warn: "text-ink",
|
|
bad: "text-bad",
|
|
}
|
|
|
|
const ICON_WRAP: Record<Tone, string> = {
|
|
neutral: "bg-surface-sunk text-ink-3",
|
|
primary: "bg-primary-soft text-primary",
|
|
good: "bg-good-soft text-good",
|
|
warn: "bg-warn-soft text-warn",
|
|
bad: "bg-bad-soft text-bad",
|
|
}
|
|
|
|
export interface MetricCardProps {
|
|
label: string
|
|
value: string
|
|
sub: string
|
|
icon: LucideIcon
|
|
badge?: string
|
|
badgeTone?: Tone
|
|
tone?: Tone
|
|
progress?: number
|
|
/** Compact form used on Home (section 10): no progress bar, tighter type. */
|
|
compact?: boolean
|
|
}
|
|
|
|
export function MetricCard({
|
|
label,
|
|
value,
|
|
sub,
|
|
icon: Icon,
|
|
badge,
|
|
badgeTone = "neutral",
|
|
tone = "neutral",
|
|
progress,
|
|
compact = false,
|
|
}: MetricCardProps) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"flex min-w-0 flex-col rounded-[12px] border shadow-xs transition-shadow hover:shadow-sm",
|
|
compact ? "p-2 sm:p-2.5" : "p-2.5 sm:p-3",
|
|
SURFACE[tone]
|
|
)}
|
|
>
|
|
<div className="flex items-start justify-between gap-1.5">
|
|
<span className="min-w-0 truncate text-[10px] font-bold uppercase leading-tight tracking-[0.08em] text-ink-3">
|
|
{label}
|
|
</span>
|
|
<div className="flex shrink-0 items-center gap-1">
|
|
{badge && !compact && (
|
|
<Badge tone={badgeTone} size="sm" className="px-1.5 py-0 text-[9px] font-bold h-4">
|
|
{badge}
|
|
</Badge>
|
|
)}
|
|
<span
|
|
className={cn(
|
|
"grid size-5 shrink-0 place-items-center rounded-[6px]",
|
|
ICON_WRAP[tone === "neutral" ? "primary" : tone]
|
|
)}
|
|
>
|
|
<Icon className="size-3" />
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
className={cn(
|
|
"mt-1.5 truncate font-bold leading-none",
|
|
compact ? "text-[18px]" : "text-[21px]",
|
|
VALUE[tone]
|
|
)}
|
|
>
|
|
{value}
|
|
</div>
|
|
|
|
<p
|
|
className={cn(
|
|
"truncate leading-tight text-ink-2",
|
|
compact ? "mt-0.5 text-[11px]" : "mt-1 text-[11.5px]"
|
|
)}
|
|
>
|
|
{sub}
|
|
</p>
|
|
|
|
{progress !== undefined && !compact && <Progress value={progress} className="mt-2 h-1" />}
|
|
</div>
|
|
)
|
|
}
|