import { Card, Skeleton, Text } from "@mantine/core"; import type { LucideIcon } from "lucide-react"; import type { ReactNode } from "react"; import { cn } from "@/lib/utils"; export interface KpiItem { label: string; value: ReactNode; /** Optional leading icon rendered in a tinted chip. */ icon?: LucideIcon; /** Secondary line under the label (e.g. a unit or comparison). */ hint?: string; /** * Mantine color name for the icon chip (e.g. "edr-green", "red", "yellow"). * Defaults to the brand green so a strip reads as uniform unless a page opts * into semantic tints. */ color?: string; /** * Optional change vs a prior period, rendered as a ▲/▼ chip next to the value * (green up, red down, muted zero). E.g. today's count minus yesterday's. */ delta?: number; } export interface KpiStripProps { items: KpiItem[]; /** Show skeletons in place of values while data loads. */ loading?: boolean; } /** * A single bordered card divided into up to five KPI cells: * `[ kpi | kpi | kpi ]`. Hairline dividers separate cells (vertical on wide * screens, horizontal when they wrap). Surface, border and shadow all come from * the theme — no per-cell backgrounds, gradients or custom shadows. */ export function KpiStrip({ items, loading = false }: KpiStripProps) { // The spec caps a strip at five cells; extra items are dropped rather than // silently overflowing into an unreadable row. const cells = items.slice(0, 5); return (
{cells.map((item, index) => { const Icon = item.icon; const color = item.color ?? "edr-green"; return (
0 && "border-t border-edr-border sm:border-l sm:border-t-0", )} > {Icon ? (
) : null}
{loading ? ( ) : (
{item.value} {item.delta != null && item.delta !== 0 ? ( 0 ? "edr-green" : "red"} style={{ whiteSpace: "nowrap" }} > {item.delta > 0 ? "▲" : "▼"} {Math.abs(item.delta)} ) : null}
)} {item.label} {item.hint ? ` · ${item.hint}` : ""}
); })}
); } export default KpiStrip;