Initial commit: Velocity-OS migration
This commit is contained in:
92
webos/src/pillars/pipeline/client360/QDRing.tsx
Normal file
92
webos/src/pillars/pipeline/client360/QDRing.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
/**
|
||||
* QDRing
|
||||
* Animated SVG arc representing the Qualification Desire score (0–100).
|
||||
* Color: green (≥70), amber (40–69), 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user