feat(banner): slim loop strip — big Now Playing chord removed (task L-50, commit 1/2)

Key chip + last-5 history + loop chips on one wrapping row; the
playhead loop chip and the enlarged current history chip are the
"now"; p-2 chrome trim. The text-6xl Now Playing column, its divider,
and the dead currentRN computation are gone — the loop shows once.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
vadimwit
2026-07-11 16:28:25 +01:00
parent 8e7a648660
commit 800fd43778
+67 -84
View File
@@ -2,15 +2,21 @@ import { useRef, useEffect } from 'react'
import { toRomanNumeral } from '../lib/theory'
import { findLoopPosition } from '../lib/match'
const HISTORY_SHOWN = 8
// ─── 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, currentChord, onChordClick }) {
export default function ProgressionBanner({ chordHistory, keyInfo, detectedProgression, onChordClick }) {
const { root, mode, confidence } = keyInfo ?? {}
const visible = chordHistory.slice(-HISTORY_SHOWN)
const current = visible[visible.length - 1]
const loopPos = findLoopPosition(chordHistory, detectedProgression)
const currentRN = root && current ? toRomanNumeral(current, root, mode) : ''
const currentRef = useRef(null)
const prevChord = useRef(null)
@@ -25,70 +31,68 @@ export default function ProgressionBanner({ chordHistory, keyInfo, detectedProgr
}, [current])
return (
<div className="bg-panel border border-border rounded-2xl p-4 mb-3 flex gap-4">
<div className="bg-panel border border-border rounded-2xl p-2 mb-3 flex flex-wrap items-end gap-x-3 gap-y-1">
{/* ── Left: key + chord history + loop ── */}
<div className="w-full lg:w-[70%] min-w-0 flex flex-col gap-2">
{/* Key + history on one row */}
<div className="flex items-end gap-3">
<div className="shrink-0 flex items-baseline gap-1.5">
{root ? (
<>
<span className="text-2xl font-bold text-accent">{root}</span>
<span className="text-gray-400 text-sm">{mode}</span>
{confidence && (
<span className="text-xs text-gray-600">{Math.round(confidence * 100)}%</span>
)}
</>
) : (
<span className="text-gray-600 text-sm">Detecting key</span>
{/* ── Key chip ── */}
<div className="shrink-0 flex items-baseline gap-1.5 self-center">
{root ? (
<>
<span className="text-2xl font-bold text-accent">{root}</span>
<span className="text-gray-400 text-sm">{mode}</span>
{confidence && (
<span className="text-xs text-gray-600">{Math.round(confidence * 100)}%</span>
)}
</div>
</>
) : (
<span className="text-gray-600 text-sm">Detecting key</span>
)}
</div>
<div className="w-px h-6 bg-border shrink-0" />
<div className="w-px h-6 bg-border shrink-0 self-center" />
{!chordHistory.length ? (
<p className="text-gray-600 text-sm">Start listening</p>
) : (
<div className="flex items-end gap-1 overflow-x-auto pb-1">
{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 (
<div
key={i}
ref={isCurrent ? currentRef : null}
style={{ opacity }}
onClick={() => 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'
}`}
>
<span className={`font-black leading-none tracking-tight ${
isCurrent ? 'text-3xl text-accent' : 'text-xl text-gray-200'
}`}>
{chord}
</span>
<span className={`text-xs font-semibold mt-0.5 ${
isCurrent ? 'text-amber-400' : 'text-gray-500'
}`}>
{rn || '\u00A0'}
</span>
</div>
)
})}
</div>
)}
{/* ── Chord history (the enlarged current chip is the "now") ── */}
{!chordHistory.length ? (
<p className="text-gray-600 text-sm self-center">Start listening</p>
) : (
<div className="flex items-end gap-1 overflow-x-auto">
{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 (
<div
key={i}
ref={isCurrent ? currentRef : null}
style={{ opacity }}
onClick={() => 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'
}`}
>
<span className={`font-black leading-none tracking-tight ${
isCurrent ? 'text-3xl text-accent' : 'text-xl text-gray-200'
}`}>
{chord}
</span>
<span className={`text-xs font-semibold mt-0.5 ${
isCurrent ? 'text-amber-400' : 'text-gray-500'
}`}>
{rn || ' '}
</span>
</div>
)
})}
</div>
)}
{/* Loop */}
{detectedProgression && (
<div className="flex items-center gap-1.5 flex-wrap">
{/* ── Loop — on the same row (divider between); playhead chip = the now ── */}
{detectedProgression && (
<>
<div className="w-px h-6 bg-border shrink-0 self-center" />
<div className="flex items-center gap-1.5 flex-wrap self-center">
<span className="text-xs text-gray-500"></span>
{detectedProgression.map((chord, i) => {
const isActive = i === loopPos
@@ -112,29 +116,8 @@ export default function ProgressionBanner({ chordHistory, keyInfo, detectedProgr
})}
<span className="text-gray-600 text-xs"> loop</span>
</div>
)}
</div>
{/* ── Divider ── */}
<div className="hidden lg:block w-px bg-border shrink-0" />
{/* ── Right: big chord ── */}
<div className="hidden lg:flex w-[30%] flex-col items-center justify-center gap-1">
{current ? (
<button
onClick={() => onChordClick?.(current)}
className="flex flex-col items-center gap-1 px-4 py-2 rounded-xl hover:bg-accent/10 transition-colors group"
title="Click to see voicings"
>
<p className="text-xs text-gray-600 uppercase tracking-widest">Now Playing</p>
<div className="text-6xl font-black text-amber-400 leading-none group-hover:text-accent transition-colors">{current}</div>
<div className="text-sm text-gray-500">{currentRN}</div>
<p className="text-[10px] text-gray-700 group-hover:text-gray-500 transition-colors">tap for voicings</p>
</button>
) : (
<p className="text-gray-600 text-xs text-center">Play a chord</p>
)}
</div>
</>
)}
</div>
)