import type { ReactNode } from "react"; import { cn } from "./cn"; export interface MetricProps { value: ReactNode; unit?: ReactNode; label: ReactNode; delta?: number | null; deltaLabel?: string; note?: ReactNode; size?: "sm" | "md" | "lg"; className?: string; /** render as skeleton */ loading?: boolean; href?: string; } /** Big figure in mono/tabular, tight tracking; unit in small caps beside; label under. */ export function Metric({ value, unit, label, delta, deltaLabel, note, size = "md", className, loading }: MetricProps) { const vs = size === "lg" ? "text-[30px] sm:text-[36px]" : size === "sm" ? "text-[18px]" : "text-[24px] sm:text-[28px]"; return (
{loading ? : {value}} {unit && !loading && {unit}} {delta !== undefined && delta !== null && !loading && ( 0 ? "text-ok" : delta < 0 ? "text-danger" : "text-ink-3")} title={deltaLabel}> {delta > 0 ? "+" : ""} {delta} )}
{label}
{note &&
{note}
}
); } /** * Metric strip. Under 640 px: a 2-column grid so every figure is visible without scrolling (an odd * last cell spans both columns); ≥ 640 px: one row of `cols` columns. Hairline separators, no cards. */ export function MetricRow({ children, className, cols }: { children: ReactNode; className?: string; cols?: number }) { return (
*]:min-w-0", // phones: vertical hairline between the two columns, horizontal hairline between rows, odd last cell full width "max-sm:[&>*:nth-child(even)]:border-l max-sm:[&>*:nth-child(n+3)]:border-t max-sm:[&>*:nth-child(odd)]:pl-0 max-sm:[&>*:last-child:nth-child(odd)]:col-span-2 max-sm:[&>*]:border-hair", // ≥ sm: one row, separators between cells "sm:[grid-template-columns:var(--metric-cols)] sm:[&>*+*]:border-l sm:[&>*]:border-hair", className, )} style={{ ["--metric-cols" as string]: `repeat(${cols ?? 4}, minmax(0, 1fr))` }} > {children}
); } /** Wrapper for one metric cell inside MetricRow. */ export function MetricCell({ children, className }: { children: ReactNode; className?: string }) { return
{children}
; }