// instruments.jsx — analytical widgets (gauges, dials, sparklines, eval bar)
// extracted from the Codex direction so every surface (home, dashboard, play,
// exercises) can use them. All take their colour from CSS vars; work in dark
// or light context automatically.
const { useState: useStateI, useEffect: useEffectI, useRef: useRefI } = React;
// ──────────────────────────────────────────────────────────────────────
// 1. BRASS GAUGE — labelled horizontal bar with glowing fill
//
// ──────────────────────────────────────────────────────────────────────
function BrassGauge({ label, value, color = 'var(--gold)', delta, dark = false }){
const labelColor = dark ? 'rgba(255,247,230,.55)' : 'var(--ink-muted)';
const trackBg = dark ? 'rgba(255,247,230,.12)' : 'rgba(25,23,20,.08)';
return (
{label}
{value}%
{delta != null && (
= 0 ? 'var(--olive)' : 'var(--rose)',
fontSize: 9, letterSpacing: 0,
}}>{delta >= 0 ? '↑' : '↓'} {Math.abs(delta)}
)}
{/* subtle tick marks */}
{[25, 50, 75].map(p => (
))}
);
}
// ──────────────────────────────────────────────────────────────────────
// 2. RATING DIAL — half-circle needle gauge for Maia rating
//
// ──────────────────────────────────────────────────────────────────────
function RatingDial({ value = 1500, min = 200, max = 2000, dark = false, label = 'Maia · sparring' }){
const pct = (value - min) / (max - min);
const angle = -180 + pct * 180;
const fg = dark ? '#fff7e6' : 'var(--ink)';
return (
{label}
● {value}
{/* tick marks */}
{Array.from({ length: 9 }, (_, i) => {
const a = (-180 + i * 22.5) * Math.PI / 180;
const r1 = 95, r2 = i % 2 === 0 ? 86 : 90;
return (
);
})}
{/* needle */}
{value}
);
}
// ──────────────────────────────────────────────────────────────────────
// 3. SPARKLINE — animated draw of a path
//
// ──────────────────────────────────────────────────────────────────────
function Sparkline({ points = [], color = 'var(--gold)', height = 80, fill = true, label }){
const max = Math.max(...points), min = Math.min(...points);
const range = max - min || 1;
const w = 320;
const xs = points.map((_, i) => (i / (points.length - 1)) * w);
const ys = points.map(p => height - 10 - ((p - min) / range) * (height - 20));
const path = xs.map((x, i) => `${i === 0 ? 'M' : 'L'} ${x.toFixed(1)} ${ys[i].toFixed(1)}`).join(' ');
const area = `${path} L ${w} ${height} L 0 ${height} Z`;
const id = `spark-${color.replace(/[^a-z]/gi,'')}-${Math.random().toString(36).slice(2,7)}`;
return (
{fill && }
{/* endpoint */}
);
}
// ──────────────────────────────────────────────────────────────────────
// 4. EVAL BAR — vertical Stockfish eval indicator
//
// ──────────────────────────────────────────────────────────────────────
function EvalBar({ value = 0, max = 5, height = 300, depth = 22 }){
// value range: -max to +max; positive = white advantage
const pct = Math.max(0, Math.min(1, (value + max) / (2 * max)));
const whiteHeight = pct * 100;
return (
{/* center line */}
{/* value badge */}
= 0 ? '#1a1612' : '#fffaf2',
color: value >= 0 ? 'var(--gold)' : 'var(--ink)',
border: `1px solid ${value >= 0 ? 'rgba(255,247,230,.3)' : 'var(--line-strong)'}`,
whiteSpace:'nowrap',
}}>{value > 0 ? '+' : ''}{value.toFixed(1)}
{/* depth */}
D · {depth}
);
}
// ──────────────────────────────────────────────────────────────────────
// 5. MOVE LIST — algebraic notation column
// ──────────────────────────────────────────────────────────────────────
function MoveList({ moves = [], current = -1, dark = false }){
const bg = dark ? 'rgba(255,247,230,.04)' : 'var(--paper-200)';
const border = dark ? 'rgba(255,247,230,.10)' : 'var(--line)';
const muted = dark ? 'rgba(255,247,230,.55)' : 'var(--ink-muted)';
const text = dark ? '#fff7e6' : 'var(--ink)';
return (
{moves.map((pair, i) => (
{i + 1}.
{pair[0]}
{pair[1] || ''}
))}
);
}
// ──────────────────────────────────────────────────────────────────────
// 6. INSTRUMENT PANEL — composite card wrapper for instruments
// ──────────────────────────────────────────────────────────────────────
function InstrumentCard({ title, hint, children, dark = false, style = {} }){
const bg = dark
? 'linear-gradient(160deg, #181519 0%, #2a1f24 100%)'
: 'var(--paper-card)';
const border = dark ? 'rgba(255,247,230,.16)' : 'var(--line-strong)';
const text = dark ? '#fff7e6' : 'var(--ink)';
const muted = dark ? 'rgba(255,247,230,.55)' : 'var(--ink-muted)';
return (
{title}
{hint && {hint} }
{children}
);
}
// ──────────────────────────────────────────────────────────────────────
// 7. STREAK FLAME — animated flame icon with day count
// ──────────────────────────────────────────────────────────────────────
function StreakFlame({ days = 7, dark = false }){
const fg = dark ? '#fff7e6' : 'var(--ink)';
return (
);
}
// keyframe for flame
if (typeof document !== 'undefined' && !document.getElementById('pw-instruments-kf')){
const s = document.createElement('style');
s.id = 'pw-instruments-kf';
s.textContent = `
@keyframes pwFlicker {
0%,100% { transform: scaleY(1) rotate(-1deg); }
35% { transform: scaleY(1.08) rotate(2deg); }
65% { transform: scaleY(.95) rotate(-2deg); }
}
`;
document.head.appendChild(s);
}
Object.assign(window, {
BrassGauge, RatingDial, Sparkline, EvalBar, MoveList, InstrumentCard, StreakFlame,
});