feat(app): one-screen jam dashboard + jam view (task L-50, commit 2/2)

Two-column grid: LEFT compact instrument view (natural-scale caps
674/562/674, non-compact renders byte-identical) + licks strip +
related-progressions slot (L-51 fills); RIGHT the 500px suggested-
voicings rail, height-bounded with internal scroll (not sticky).
ProgressionSuggestions unmounted (file kept). Jam view: one button,
two layers — CSS h-screen lock with everything below the grid
unmounted, plus best-effort Promise-caught requestFullscreen; Escape
and fullscreenchange stay in sync. Audio contract grep: zero hits.
Critic PASS (SSR 73/73; gate applied a one-class legend restoration
for non-compact byte-identity, re-verified green).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
vadimwit
2026-07-11 16:28:25 +01:00
parent 800fd43778
commit e58a7a624c
5 changed files with 330 additions and 165 deletions
+42 -21
View File
@@ -37,7 +37,11 @@ function noteColor(isChordTone, isPenta, isScale, mono = false) {
return null
}
export default function Fretboard({ keyInfo, currentChord, pentatonicOnly = false, monoColor = false, jamFocusChord = null }) {
// `compact` (task L-50, one-screen.md §2): trimmed card chrome (p-3, legend
// merged onto the heading line) + a natural-width cap on the SVG (max-width =
// its viewBox width, so it never renders above scale 1.0). No fret reduction,
// no transform scaling — the notes stay at their designed size.
export default function Fretboard({ keyInfo, currentChord, pentatonicOnly = false, monoColor = false, jamFocusChord = null, compact = false }) {
const { root, mode } = keyInfo ?? {}
if (!root) return null
@@ -78,20 +82,49 @@ export default function Fretboard({ keyInfo, currentChord, pentatonicOnly = fals
const focusLabel = pc =>
pc === focusThird ? '3' : pc === focusSeventh ? focusSeventhLabel : null
const heading = (
<p className={`text-sm text-gray-500 uppercase tracking-widest ${compact ? '' : 'mb-4'}`}>
Fretboard {root} {mode}
{currentChord && <span className="text-amber-400 ml-2">/ {currentChord}</span>}
{hasFocus && <span className="text-accent ml-2"> guide tones</span>}
</p>
)
const legend = (
// Critic mechanical fix (L-50 gate): non-compact keeps HEAD's exact class
// string so the non-compact render stays byte-identical to the committed one.
<div className={compact ? 'flex flex-wrap items-center text-xs text-gray-500 gap-3' : 'mt-3 flex flex-wrap gap-5 text-xs text-gray-500'}>
<span><span className="text-accent"></span> Chord tone</span>
<span style={{ color: monoColor ? '#c084fc' : '#f59e0b' }}></span><span> Pentatonic</span>
<span style={{ color: monoColor ? '#e9d5ff' : '#6b7280' }}></span><span> Scale</span>
{hasFocus && (
<span className="flex items-center gap-1">
<span
className="inline-block w-3 h-3 rounded-full border-2 border-accent"
/>
Guide tones (3 / {focusSeventhLabel})
</span>
)}
</div>
)
return (
<div className="bg-panel border border-border rounded-2xl p-6">
<p className="text-sm text-gray-500 uppercase tracking-widest mb-4">
Fretboard {root} {mode}
{currentChord && <span className="text-amber-400 ml-2">/ {currentChord}</span>}
{hasFocus && <span className="text-accent ml-2"> guide tones</span>}
</p>
<div className={`bg-panel border border-border rounded-2xl ${compact ? 'p-3' : 'p-6'}`}>
{compact ? (
<div className="mb-2 flex flex-wrap items-center justify-between gap-x-4 gap-y-1">
{heading}
{legend}
</div>
) : (
heading
)}
<div>
<svg
viewBox={`0 0 ${BOARD_W} ${BOARD_H}`}
width="100%"
height="auto"
style={{ display: 'block' }}
style={{ display: 'block', ...(compact ? { maxWidth: BOARD_W } : null) }}
>
{/* Fretboard background */}
<rect x={NUT_X} y={PAD_T - 6} width={BOARD_W - NUT_X - 4} height={5 * STRING_H + 12}
@@ -202,19 +235,7 @@ export default function Fretboard({ keyInfo, currentChord, pentatonicOnly = fals
</svg>
</div>
<div className="mt-3 flex flex-wrap gap-5 text-xs text-gray-500">
<span><span className="text-accent"></span> Chord tone</span>
<span style={{ color: monoColor ? '#c084fc' : '#f59e0b' }}></span><span> Pentatonic</span>
<span style={{ color: monoColor ? '#e9d5ff' : '#6b7280' }}></span><span> Scale</span>
{hasFocus && (
<span className="flex items-center gap-1">
<span
className="inline-block w-3 h-3 rounded-full border-2 border-accent"
/>
Guide tones (3 / {focusSeventhLabel})
</span>
)}
</div>
{!compact && legend}
</div>
)
}