import { useId, type CSSProperties, type ReactNode } from 'react'; export interface GaugeProps { /** Current value (clamped to `[min, max]`). */ value: number; /** Domain minimum. Default 0. */ min?: number; /** Domain maximum. Default 100. */ max?: number; /** SVG size in px (gauge is a half-circle). Default 200. */ size?: number; /** Ring track thickness in px. Default 12. */ thickness?: number; /** Optional override colour. */ color?: string; /** Slot rendered below the gauge dial. */ caption?: ReactNode; /** Accessible label. */ ariaLabel?: string; className?: string; style?: CSSProperties; } /** * `` — half-circle dial for single-metric "fuel-tank" surfaces * (battery state, capacity used, NPS, etc.). * * Wave 9.A.4. Tinted by `--bl-accent`. Out-of-range / non-finite * values clamp safely. */ export function Gauge({ value, min = 0, max = 100, size = 200, thickness = 12, color = 'var(--bl-accent, #6366f1)', caption, ariaLabel, className, style, }: GaugeProps) { const titleId = useId(); const safeValue = Number.isFinite(value) ? value : min; const clamped = Math.max(min, Math.min(max, safeValue)); const pct = max > min ? (clamped - min) / (max - min) : 0; const cx = size / 2; const cy = size * 0.7; const r = Math.min(size / 2 - thickness, size * 0.4); // Half-circle: angle goes from π (left) to 0 (right), i.e. 180° → 0°. const startAngle = Math.PI; const endAngle = Math.PI - pct * Math.PI; const x0 = cx + r * Math.cos(startAngle); const y0 = cy + r * Math.sin(startAngle); const x1 = cx + r * Math.cos(endAngle); const y1 = cy + r * Math.sin(endAngle); const trackD = [ `M ${cx - r} ${cy}`, `A ${r} ${r} 0 0 1 ${cx + r} ${cy}`, ].join(' '); const fillD = pct === 0 ? '' : [ `M ${x0} ${y0}`, `A ${r} ${r} 0 0 1 ${x1} ${y1}`, ].join(' '); return ( {ariaLabel ?? `Gauge ${clamped} of ${max}`} {fillD && ( )} {Math.round(clamped)} {caption && (
{caption}
)}
); }