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
+32 -12
View File
@@ -33,7 +33,10 @@ function noteColor(isChordTone, isPenta, isScale, mono = false) {
return null
}
export default function BassFretboard({ keyInfo, currentChord, monoColor = false }) {
// `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).
export default function BassFretboard({ keyInfo, currentChord, monoColor = false, compact = false }) {
const { root, mode } = keyInfo ?? {}
if (!root) return null
@@ -44,19 +47,40 @@ export default function BassFretboard({ keyInfo, currentChord, monoColor = false
? new Set(getChordTones(currentChord).map(n => NOTES.indexOf(n)))
: new Set()
const heading = (
<p className={`text-sm text-gray-500 uppercase tracking-widest ${compact ? '' : 'mb-4'}`}>
Bass {root} {mode}
{currentChord && <span className="text-amber-400 ml-2">/ {currentChord}</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 items-center text-xs text-gray-500 flex-wrap gap-3' : 'mt-3 flex 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>
</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">
Bass {root} {mode}
{currentChord && <span className="text-amber-400 ml-2">/ {currentChord}</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={3 * STRING_H + 12}
@@ -139,11 +163,7 @@ export default function BassFretboard({ keyInfo, currentChord, monoColor = false
</svg>
</div>
<div className="mt-3 flex 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>
</div>
{!compact && legend}
</div>
)
}