SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
6 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
2.9 KB · 65 lines tsx
Raw Blame History
1import type { ReactNode } from "react";2import { cn } from "./cn";34export interface MetricProps {5  value: ReactNode;6  unit?: ReactNode;7  label: ReactNode;8  delta?: number | null;9  deltaLabel?: string;10  note?: ReactNode;11  size?: "sm" | "md" | "lg";12  className?: string;13  /** render as skeleton */14  loading?: boolean;15  href?: string;16}1718/** Big figure in mono/tabular, tight tracking; unit in small caps beside; label under. */19export function Metric({ value, unit, label, delta, deltaLabel, note, size = "md", className, loading }: MetricProps) {20  const vs = size === "lg" ? "text-[30px] sm:text-[36px]" : size === "sm" ? "text-[18px]" : "text-[24px] sm:text-[28px]";21  return (22    <div className={cn("flex min-w-0 flex-col gap-0.5", className)}>23      <div className={cn("flex items-baseline gap-1 leading-none", vs)}>24        {loading ? <span className="inline-block h-[0.9em] w-[3.2em] animate-pulse-soft rounded-xs bg-surface-3" /> : <span className="figure font-medium tracking-[-0.03em] text-ink">{value}</span>}25        {unit && !loading && <span className="text-[0.5em] font-medium text-ink-3">{unit}</span>}26        {delta !== undefined && delta !== null && !loading && (27          <span className={cn("figure ml-1 text-[0.45em] font-medium", delta > 0 ? "text-ok" : delta < 0 ? "text-danger" : "text-ink-3")} title={deltaLabel}>28            {delta > 0 ? "+" : ""}29            {delta}30          </span>31        )}32      </div>33      <div className="label-caps truncate">{label}</div>34      {note && <div className="text-[11px] leading-tight text-ink-3">{note}</div>}35    </div>36  );37}3839/**40 * Metric strip. Under 640 px: a 2-column grid so every figure is visible without scrolling (an odd41 * last cell spans both columns); ≥ 640 px: one row of `cols` columns. Hairline separators, no cards.42 */43export function MetricRow({ children, className, cols }: { children: ReactNode; className?: string; cols?: number }) {44  return (45    <div46      className={cn(47        "grid grid-cols-2 border-y border-hair [&>*]:min-w-0",48        // phones: vertical hairline between the two columns, horizontal hairline between rows, odd last cell full width49        "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",50        // ≥ sm: one row, separators between cells51        "sm:[grid-template-columns:var(--metric-cols)] sm:[&>*+*]:border-l sm:[&>*]:border-hair",52        className,53      )}54      style={{ ["--metric-cols" as string]: `repeat(${cols ?? 4}, minmax(0, 1fr))` }}55    >56      {children}57    </div>58  );59}6061/** Wrapper for one metric cell inside MetricRow. */62export function MetricCell({ children, className }: { children: ReactNode; className?: string }) {63  return <div className={cn("min-w-0 px-3 py-3 sm:px-4 sm:first:pl-0", className)}>{children}</div>;64}65