// game-parts.jsx — shared UI bits for the prediction game. // ── PM2.5 colour scale (Colombia / OMS-ish ranges, μg/m³) ───────── function pmColor(v){ if(v <= 12) return '#00E676'; // Buena if(v <= 25) return '#FFEB3B'; // Aceptable if(v <= 37) return '#FFB300'; // Dañina grupos sensibles if(v <= 55) return '#FF8A3D'; // Dañina if(v <= 150) return '#E8735A'; // Muy dañina return '#B0306E'; // Peligrosa } function pmLabel(v){ if(v <= 12) return 'Buena'; if(v <= 25) return 'Aceptable'; if(v <= 37) return 'Dañina para sensibles'; if(v <= 55) return 'Dañina'; if(v <= 150) return 'Muy dañina'; return 'Peligrosa'; } function pmEmoji(v){ if(v <= 12) return '😊'; if(v <= 25) return '🙂'; if(v <= 37) return '😐'; if(v <= 55) return '😷'; if(v <= 150) return '🤢'; return '☠️'; } // ── Top chrome ──────────────────────────────────────────────────── function TopBar({step, total, onRestart, onRetryLevel, label}){ return (
SIATA
{label || 'Juego · ¿Puedes predecir el aire?'}
{Array.from({length:total}).map((_,i)=>(
))}
{String(step+1).padStart(2,'0')} / {String(total).padStart(2,'0')}
{onRetryLevel && ( )} {onRestart && ( )}
); } function BottomBar(){ return (
siata.gov.co · Valle de Aburrá · 2025 Pronóstico operacional · 1 corrida/día
); } // ── Big primary button ──────────────────────────────────────────── function PrimaryBtn({children, onClick, disabled, variant}){ const v = variant || 'cyan'; const styles = { cyan: {bg:'linear-gradient(135deg, #0DCCC0 0%, #00897B 100%)', sh:'rgba(13,204,192,0.4)'}, green: {bg:'linear-gradient(135deg, #84BD45 0%, #5e8a2e 100%)', sh:'rgba(132,189,69,0.4)'}, ghost: {bg:'transparent', sh:'transparent'}, }[v]; return ( ); } // ── PM2.5 colour scale legend ──────────────────────────────────── function PMScale({value, showValue=true}){ const stops = [ {v:0, c:'#00E676', l:'Buena'}, {v:12, c:'#FFEB3B', l:'Aceptable'}, {v:25, c:'#FFB300', l:'Sensibles'}, {v:37, c:'#FF8A3D', l:'Dañina'}, {v:55, c:'#E8735A', l:'Muy dañina'}, ]; const max = 80; const pct = Math.min(100, (value/max)*100); return (
{showValue && (
)}
{stops.map((s,i)=>(
{s.l}
{s.v}{i===stops.length-1?'+':''} μg
))}
); } // ── Card shell ─────────────────────────────────────────────────── function Card({children, style, accent}){ return (
{accent && (
)} {children}
); } // ── Kicker + Title ─────────────────────────────────────────────── function ScreenHead({kicker, title, sub}){ return (
{kicker}

{title}

{sub && (

{sub}

)}
); } // ── Animated number ────────────────────────────────────────────── function AnimNum({value, decimals=0, duration=600}){ const [v, setV] = React.useState(value); const prevRef = React.useRef(value); React.useEffect(()=>{ const from = prevRef.current; const to = value; const t0 = performance.now(); let raf; const tick = (t)=>{ const k = Math.min(1, (t-t0)/duration); const eased = 1 - Math.pow(1-k, 3); setV(from + (to-from)*eased); if(k<1) raf = requestAnimationFrame(tick); else prevRef.current = to; }; raf = requestAnimationFrame(tick); return ()=> cancelAnimationFrame(raf); },[value]); return {v.toFixed(decimals)}; } Object.assign(window, { pmColor, pmLabel, pmEmoji, TopBar, BottomBar, PrimaryBtn, PMScale, Card, ScreenHead, AnimNum, });