first commit
This commit is contained in:
112
src/components/shared/MetricCard.tsx
Normal file
112
src/components/shared/MetricCard.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user