import { useRef, useEffect } from 'react' import { toRomanNumeral } from '../lib/theory' import { findLoopPosition } from '../lib/match' // ─── ProgressionBanner — the SLIM LOOP STRIP (task L-50, one-screen.md §1.2) ── // // One full-width status row: key chip · chord history (last 5, age-faded) · // ♻ loop chips. The old right-30% "Now Playing" text-6xl chord + its divider // are GONE per the user directive — the enlarged current history chip and the // accent-glowing active loop chip ARE the now-playing display. Chrome trimmed // (p-4 → p-2, history pb-1 dropped) so the strip lands at ~76px. const HISTORY_SHOWN = 5 export default function ProgressionBanner({ chordHistory, keyInfo, detectedProgression, seedInfo, onChordClick }) { const { root, mode, confidence } = keyInfo ?? {} // Jam-roulette provenance (task L-60, jam-roulette.md §1.3): while a rolled // loop is unconfirmed the loop row swaps its ♻ chrome for 🎲 + an amber chip // naming the source; it flips back to normal the moment live detection // confirms it (App clears seedInfo). The loop chips themselves are identical. const seedBars = Array.isArray(seedInfo?.bars) ? seedInfo.bars.reduce((a, b) => a + (b ?? 0), 0) : null const visible = chordHistory.slice(-HISTORY_SHOWN) const current = visible[visible.length - 1] const loopPos = findLoopPosition(chordHistory, detectedProgression) const currentRef = useRef(null) const prevChord = useRef(null) useEffect(() => { if (current && current !== prevChord.current && currentRef.current) { currentRef.current.animate( [{ opacity: 0, transform: 'scale(0.85)' }, { opacity: 1, transform: 'scale(1)' }], { duration: 200, easing: 'ease-out', fill: 'forwards' } ) prevChord.current = current } }, [current]) return (
{/* ── Key chip ── */}
{root ? ( <> {root} {mode} {confidence && ( {Math.round(confidence * 100)}% )} ) : ( Detecting key… )}
{/* ── Chord history (the enlarged current chip is the "now") ── */} {!chordHistory.length ? (

Start listening…

) : (
{visible.map((chord, i) => { const isCurrent = i === visible.length - 1 const age = visible.length - 1 - i const opacity = Math.max(0.25, 1 - age * 0.09) const rn = root ? toRomanNumeral(chord, root, mode) : '' return (
onChordClick?.(chord)} className={`flex flex-col items-center shrink-0 px-2 py-1 rounded-xl transition-colors duration-200 cursor-pointer ${ isCurrent ? 'bg-accent/10 border border-accent/40 ring-1 ring-accent/20 hover:bg-accent/20' : 'border border-transparent hover:border-border hover:bg-panel' }`} > {chord} {rn || ' '}
) })}
)} {/* ── Loop — on the same row (divider between); playhead chip = the now ── */} {detectedProgression && ( <>
{seedInfo ? '🎲' : '♻'} {detectedProgression.map((chord, i) => { const isActive = i === loopPos const rn = root ? toRomanNumeral(chord, root, mode) : chord return (
onChordClick?.(chord)} className={`flex flex-col items-center px-2 py-0.5 rounded-lg border transition-all duration-200 cursor-pointer ${ isActive ? 'bg-accent/20 border-accent shadow-[0_0_10px_rgba(168,85,247,0.3)] hover:bg-accent/30' : 'bg-border border-border hover:border-gray-500' }`} > {chord} {rn}
) })} {seedInfo ? ( rolled · {seedInfo.styleLabel} · {seedInfo.name} {seedBars != null ? ` · ${seedBars} bars` : ''} — play it! ) : ( → loop )}
)}
) }