83 lines
2.2 KiB
TypeScript
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>
|
|
);
|
|
}
|