Files
2026-09-08 07:21:34 -04:00

83 lines
2.2 KiB
TypeScript

"use client";
import React from "react";
import { cn } from "@/lib/utils";
/**
* Donut-style progress ring.
*
* Used in the Overview dashboard for things like implementation coverage,
* test coverage, project health score, traceability score.
*/
export function ProgressRing({
value,
size = 80,
stroke = 8,
tone = "brand",
label,
sublabel,
className,
}: {
value: number; // 0..100
size?: number;
stroke?: number;
tone?: "brand" | "green" | "amber" | "red" | "violet";
label?: string;
sublabel?: string;
className?: string;
}) {
const r = (size - stroke) / 2;
const c = 2 * Math.PI * r;
const clamped = Math.max(0, Math.min(100, Number(value) || 0));
const offset = c - (clamped / 100) * c;
const toneMap: Record<string, string> = {
brand: "text-brand-600",
green: "text-emerald-600",
amber: "text-amber-600",
red: "text-red-600",
violet: "text-violet-600",
};
return (
<div className={cn("flex items-center gap-4", className)}>
<div className="relative shrink-0" style={{ width: size, height: size }}>
<svg width={size} height={size} className={cn("-rotate-90", toneMap[tone])}>
<circle
cx={size / 2}
cy={size / 2}
r={r}
stroke="currentColor"
strokeOpacity={0.12}
fill="transparent"
strokeWidth={stroke}
/>
<circle
cx={size / 2}
cy={size / 2}
r={r}
stroke="currentColor"
fill="transparent"
strokeWidth={stroke}
strokeLinecap="round"
strokeDasharray={c}
strokeDashoffset={offset}
className="transition-all duration-500 ease-out"
/>
</svg>
<div className="absolute inset-0 grid place-items-center">
<div className="text-sm font-semibold tabular-nums text-slate-900">
{clamped.toFixed(0)}%
</div>
</div>
</div>
{(label || sublabel) && (
<div className="min-w-0">
{label && <div className="text-sm font-medium text-slate-800">{label}</div>}
{sublabel && <div className="text-xs text-slate-500 mt-0.5">{sublabel}</div>}
</div>
)}
</div>
);
}