jengacore components · @jengacore/kpi-strip
KpiStrip
One ledger of 2–4 numbers in Bricolage with a comparison line each. Not floating tiles.
Screened today1,284,330+4.1% vs last Monday
Open alerts427 over SLA · 9 unassigned
Blocked value38,920USD12 transfers
Median decision14MINTarget 30 min · met
Install
terminal
npx shadcn@latest add @jengacore/kpi-stripWithout the namespace configured: npx shadcn@latest add https://ui.jengacore.com/r/<new-york-v4|base-nova>/kpi-strip.json
Registry dependencies
Source
components/jc/kpi-strip.tsx
import * as React from "react"
import { cn } from "@/lib/utils"
export type Kpi = {
/** Mono eyebrow label. */
label: string
value: React.ReactNode
/** Unit or currency, set in mono after the value. */
unit?: string
/** One comparison line. Wrap the meaningful part in <b> to colour it. */
note?: React.ReactNode
/** Colour the <b> in the note only when the change is good or bad for the reader. */
trend?: "up" | "down" | "warn"
/** Optional sparkline: recent values, oldest first. Drawn in the chart colour, with the first value as a baseline. */
spark?: number[]
/** Chart series colour for the sparkline (1 = ours/primary … 4 = baseline grey). */
series?: 1 | 2 | 3 | 4
}
/** A 200×30 sparkline: no axes, no fill, no animation of the number itself. */
function Sparkline({ values, series = 1 }: { values: number[]; series?: 1 | 2 | 3 | 4 }) {
if (values.length < 2) return null
const min = Math.min(...values)
const max = Math.max(...values)
const y = (v: number) => (max === min ? 15 : 27 - ((v - min) / (max - min)) * 24)
const step = 200 / (values.length - 1)
const d = values.map((v, i) => `${i ? "L" : "M"}${(i * step).toFixed(1)} ${y(v).toFixed(1)}`).join(" ")
return (
<svg viewBox="0 0 200 30" preserveAspectRatio="none" aria-hidden className="mt-1.5 block h-[30px] w-full">
<path d={`M0 ${y(values[0] ?? min).toFixed(1)} H200`} className="stroke-border" strokeWidth={1} strokeDasharray="2 3" fill="none" vectorEffect="non-scaling-stroke" />
<path d={d} stroke={`var(--chart-${series})`} strokeWidth={1.5} fill="none" strokeLinejoin="round" vectorEffect="non-scaling-stroke" />
</svg>
)
}
/**
* One ledger of 2–4 numbers (not floating tiles). Value in Bricolage with tabular figures.
* Never fake precision: round to what an operator acts on.
*/
export function KpiStrip({ items, className }: { items: Kpi[]; className?: string }) {
return (
<section
data-slot="kpi-strip"
className={cn(
"grid overflow-hidden rounded-lg border border-border bg-card shadow-card max-md:grid-cols-2! contrast:border-input",
className
)}
style={{ gridTemplateColumns: `repeat(${Math.min(items.length, 4)}, minmax(0, 1fr))` }}
>
{items.slice(0, 4).map((k) => (
<div key={k.label} className="flex min-w-0 flex-col gap-1 border-r border-b border-border px-5 pt-4 pb-3 last:border-r-0 md:border-b-0">
<span className="eyebrow">{k.label}</span>
<span className="font-display text-[30px] leading-9 font-semibold tracking-[-0.01em] tabular-nums">
{k.value}
{k.unit ? <small className="ml-1.5 font-mono text-xs font-medium tracking-normal text-muted-foreground">{k.unit}</small> : null}
</span>
{k.note ? (
<span
className={cn(
"text-[12.5px] text-muted-foreground [&_b]:font-medium",
k.trend === "up" && "[&_b]:text-success",
k.trend === "down" && "[&_b]:text-destructive",
k.trend === "warn" && "[&_b]:text-warning"
)}
>
{k.note}
</span>
) : null}
{k.spark ? <Sparkline values={k.spark} series={k.series} /> : null}
</div>
))}
</section>
)
}