Files
Velocity-OS/webos/src/pillars/pipeline/client360/QDRing.tsx

93 lines
2.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useRef } from 'react';
import { motion } from 'framer-motion';
/**
* QDRing
* Animated SVG arc representing the Qualification Desire score (0100).
* Color: green (≥70), amber (4069), red (<40).
* Animates from 0 to score on mount; smooth spring on value change.
*/
interface QDRingProps {
score: number;
size?: number; // diameter in px (default 64)
strokeWidth?: number;
color?: string; // overrides semantic color if provided
showLabel?: boolean;
}
export function QDRing({
score,
size = 64,
strokeWidth = 4,
color,
showLabel = false,
}: QDRingProps) {
const clampedScore = Math.max(0, Math.min(100, score));
const r = (size - strokeWidth) / 2;
const cx = size / 2;
const cy = size / 2;
const circumference = 2 * Math.PI * r;
// Arc: 0% = full stroke-dashoffset (hidden), 100% = 0 offset (full)
const offset = circumference * (1 - clampedScore / 100);
// Semantic color
const arcColor = color ?? (
clampedScore >= 70 ? 'var(--color-green)' :
clampedScore >= 40 ? 'var(--color-amber)' :
'var(--color-red)'
);
return (
<svg
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
role="img"
aria-label={`QD Score: ${clampedScore}`}
style={{ transform: 'rotate(-90deg)' }} // Start arc at top
>
{/* Track */}
<circle
cx={cx} cy={cy} r={r}
fill="none"
stroke="rgba(255,255,255,0.08)"
strokeWidth={strokeWidth}
/>
{/* Animated arc */}
<motion.circle
cx={cx} cy={cy} r={r}
fill="none"
stroke={arcColor}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeDasharray={circumference}
initial={{ strokeDashoffset: circumference }}
animate={{ strokeDashoffset: offset }}
transition={{ duration: 0.8, ease: [0.4, 0, 0.2, 1] }}
style={{
filter: `drop-shadow(0 0 4px ${arcColor})`,
}}
/>
{/* Optional center label (shown rotated back) */}
{showLabel && (
<text
x={cx} y={cy}
textAnchor="middle"
dominantBaseline="middle"
fill={arcColor}
fontSize={size * 0.22}
fontWeight="700"
fontFamily="var(--font-sans)"
style={{ transform: `rotate(90deg)`, transformOrigin: `${cx}px ${cy}px` }}
>
{clampedScore}
</text>
)}
</svg>
);
}