import { Card, Skeleton, Text } from "@mantine/core"; import type { LucideIcon } from "lucide-react"; import type { ElementType, ReactNode } from "react"; import { Link } from "react-router-dom"; 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; /** * Optional route the cell links to — its detail view. When set the cell * becomes clickable (pointer, hover tint); when absent it stays static. */ href?: string; } 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"; // A cell with an href becomes a link to its detail; without one it // stays a plain div. Same layout classes either way. const Cell: ElementType = item.href ? Link : "div"; const linkProps = item.href ? { to: item.href, "aria-label": `${item.label} — view detail` } : {}; return ( )} className={cn( "flex flex-1 items-center gap-3 px-5 py-4", index > 0 && "border-t border-edr-border sm:border-l sm:border-t-0", item.href && "cursor-pointer no-underline transition-colors hover:bg-gray-50 focus-visible:bg-gray-50", )} > {Icon ? (
) : null}
{loading ? ( ) : (
{item.value} {item.delta != null && item.delta !== 0 ? ( 0 ? "edr-green.7" : "red.7"} style={{ whiteSpace: "nowrap", background: item.delta > 0 ? "var(--mantine-color-edr-green-0)" : "var(--mantine-color-red-0)", borderRadius: 999, padding: "1px 7px", }} > {item.delta > 0 ? "▲" : "▼"} {Math.abs(item.delta)} ) : null}
)} {item.label} {item.hint ? ` · ${item.hint}` : ""}
); })}
); } export default KpiStrip;