On-time-delivery SLA tiles showing measured rate against target with breach flags and period-over-period deltas.
npx shadcn@latest add @paragon/sla-trackerAlso installs: number-ticker, tooltip
"use client";
import * as React from "react";
import { useInView, useReducedMotion } from "motion/react";
import { TrendingDown, TrendingUp, AlertTriangle } from "lucide-react";
import { NumberTicker } from "@/registry/paragon/ui/number-ticker";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/registry/paragon/ui/tooltip";
import { cn } from "@/lib/utils";
export interface SlaTile {
label: string;
/** Measured on-time rate 0–1. */
value: number;
/** SLA target 0–1. Below target flags a breach. */
target: number;
/** Optional delta vs prior period, e.g. +0.021. */
delta?: number;
}
export interface SlaTrackerProps extends React.ComponentProps<"div"> {
tiles: SlaTile[];
/** Opt out of the arc sweep, ticker and reveal. */
static?: boolean;
}
// A ring drawn on a single shared scale: the arc length is stroke-dashoffset
// off the full circumference, so the fill maps linearly to the rate.
const R = 15.9155; // circumference = 100, so 1% = 1 length unit
const CIRC = 2 * Math.PI * R;
/**
* On-time-delivery SLA tiles. Each tile pairs the measured rate against its
* target on a single scale: a computed progress ring sweeps to the rate (its
* length is stroke-dashoffset off a circumference of exactly 100 units, so
* percent maps linearly), a tick on the ring marks the target, and the rate
* counts up on a number-ticker. A tile below target flags a breach with the
* destructive token; within a warning band it tints warning. Tiles reveal with
* the house rise, stagger in, and expose their target + delta on hover/focus.
* Reduced motion (or `static`) shows final values with no sweep.
*/
export function SlaTracker({
tiles,
static: isStatic = false,
className,
...props
}: SlaTrackerProps) {
const ref = React.useRef<HTMLDivElement>(null);
const reducedMotion = useReducedMotion();
const inView = useInView(ref, { once: true, margin: "0px 0px -24px 0px" });
const animate = !isStatic && !reducedMotion;
const armed = !animate || inView;
if (tiles.length === 0) {
return (
<div
data-slot="sla-tracker"
className={cn(
"flex w-full max-w-lg items-center justify-center rounded-xl border border-dashed border-border px-4 py-10 text-sm text-muted-foreground",
className,
)}
>
No SLA metrics for this period.
</div>
);
}
return (
<TooltipProvider>
<style href="paragon-sla-tracker" precedence="paragon">{`
@keyframes pg-sla-rise {
from { opacity: 0; transform: translateY(12px); filter: blur(4px); }
to { opacity: 1; transform: translateY(0); filter: blur(0); }
}
@media (prefers-reduced-motion: reduce) {
[data-sla-tile] { animation: none !important; }
}
`}</style>
<div
ref={ref}
data-slot="sla-tracker"
className={cn(
"grid w-full max-w-lg grid-cols-2 gap-3 sm:grid-cols-3",
className,
)}
{...props}
>
{tiles.map((tile, i) => {
const breach = tile.value < tile.target;
// Warning band: within 1.5 points above target but not yet breaching.
const warn = !breach && tile.value < tile.target + 0.015;
const tone = breach ? "destructive" : warn ? "warning" : "success";
const toneVar = `var(--color-${tone})`;
// Computed arc length on the shared 0..100 scale.
const clamped = Math.max(0, Math.min(1, tile.value));
const shown = armed ? clamped : 0;
const targetPct = Math.max(0, Math.min(1, tile.target));
return (
<Tooltip key={tile.label}>
<TooltipTrigger asChild>
<button
type="button"
data-sla-tile
aria-label={`${tile.label}: ${(tile.value * 100).toFixed(
1,
)} percent on-time, target ${(tile.target * 100).toFixed(
1,
)} percent${breach ? ", breach" : ""}`}
className={cn(
"pressable group relative flex flex-col items-start overflow-hidden rounded-xl bg-card p-4 text-left outline-none shadow-border transition-[box-shadow] duration-150 ease-out hover:shadow-border-hover focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
breach && "bg-destructive/[0.04]",
warn && "bg-warning/[0.05]",
)}
style={
animate
? {
animation: `pg-sla-rise var(--duration-base) var(--ease-out) both`,
animationDelay: `${i * 60}ms`,
}
: undefined
}
>
<div className="flex w-full items-start justify-between gap-2">
<p className="min-w-0 truncate text-xs font-medium text-muted-foreground">
{tile.label}
</p>
{breach && (
<AlertTriangle
aria-hidden
className="size-3.5 shrink-0 text-destructive"
/>
)}
</div>
<div className="mt-2 flex items-center gap-3">
{/* Computed progress ring */}
<span className="relative shrink-0" aria-hidden>
<svg
viewBox="0 0 40 40"
className="size-10 -rotate-90"
>
<circle
cx="20"
cy="20"
r={R}
fill="none"
className="stroke-border"
strokeWidth={3}
vectorEffect="non-scaling-stroke"
/>
<circle
cx="20"
cy="20"
r={R}
fill="none"
stroke={toneVar}
strokeWidth={3}
strokeLinecap="round"
vectorEffect="non-scaling-stroke"
strokeDasharray={CIRC}
strokeDashoffset={CIRC * (1 - shown)}
className="transition-[stroke-dashoffset] duration-700 ease-out motion-reduce:transition-none"
/>
{/* Target tick on the same scale */}
<line
x1="20"
y1="20"
x2="20"
y2={20 - R - 2.2}
className="stroke-foreground/45"
strokeWidth={1.5}
strokeLinecap="round"
vectorEffect="non-scaling-stroke"
style={{
transform: `rotate(${targetPct * 360}deg)`,
transformOrigin: "20px 20px",
}}
/>
</svg>
</span>
<div className="min-w-0">
<p
className={cn(
"text-2xl font-semibold leading-none tabular-nums",
breach
? "text-destructive"
: warn
? "text-warning"
: "text-foreground",
)}
>
<NumberTicker
value={tile.value * 100}
static={isStatic}
formatOptions={{
minimumFractionDigits: 1,
maximumFractionDigits: 1,
}}
/>
<span className="text-base">%</span>
</p>
<p className="mt-1 truncate text-[11px] tabular-nums text-muted-foreground">
Target {(tile.target * 100).toFixed(1)}%
</p>
</div>
</div>
{tile.delta !== undefined && (
<span
className={cn(
"mt-2 flex items-center gap-0.5 text-[11px] font-medium tabular-nums",
tile.delta >= 0 ? "text-success" : "text-destructive",
)}
>
{tile.delta >= 0 ? (
<TrendingUp aria-hidden className="size-3" />
) : (
<TrendingDown aria-hidden className="size-3" />
)}
{tile.delta >= 0 ? "+" : ""}
{(tile.delta * 100).toFixed(1)} pts vs. prior
</span>
)}
</button>
</TooltipTrigger>
<TooltipContent side="top">
<span className="font-medium">{tile.label}</span>
<span className="ml-1.5 tabular-nums text-primary-foreground/70">
{(tile.value * 100).toFixed(1)}% / target{" "}
{(tile.target * 100).toFixed(1)}%
{breach ? " · breach" : warn ? " · at risk" : " · on track"}
</span>
</TooltipContent>
</Tooltip>
);
})}
</div>
</TooltipProvider>
);
}