feat(jamguide): playable VoicingBrowser in chord modal + enlarged stations (task L-21)
ChordDetailModal Guitar/Piano tabs mount VoicingBrowser on top with legacy grips/technique reference preserved below; Jam Guide enlarged station view gains the browser on both instrument tabs; parse-null never mounts a wrong-chord fallback. App.jsx untouched. Critic PASS (SSR-verified both entry points; onFocusChord effects byte-untouched). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import ChordBox from './ChordBox'
|
||||
import MiniPiano from './MiniPiano'
|
||||
import VoicingBrowser from './VoicingBrowser'
|
||||
import { getGuitarVoicings, getPianoTechniques, parseChord } from '../lib/voicings'
|
||||
import { CHORD_TYPES, NOTES, getChordsInKey, toRomanNumeral, getSuggestedProgressions } from '../lib/theory'
|
||||
import { FAMOUS_PROGRESSIONS, progressionInKey, getChordSubstitutions, CHORD_PLAYBOOK } from '../lib/education'
|
||||
@@ -29,12 +30,23 @@ function chordDisplayName(root, typeKey) {
|
||||
}
|
||||
|
||||
function GuitarTab({ chordName }) {
|
||||
// parseChord's `type` is a CHORD_TYPES key — exactly VoicingBrowser's
|
||||
// `quality` prop (same mapping PianoTab/ExploreTab already rely on). L-21.
|
||||
const parsed = parseChord(chordName)
|
||||
const voicings = getGuitarVoicings(chordName)
|
||||
if (!voicings.length) {
|
||||
if (!parsed && !voicings.length) {
|
||||
return <p className="text-gray-500 text-sm text-center py-8">No guitar voicings found for {chordName}.</p>
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
{/* Playable voicing browser (D-21) — browse + audition shapes. */}
|
||||
{parsed && <VoicingBrowser rootPc={parsed.rootPc} quality={parsed.type} />}
|
||||
|
||||
{/* Static grid kept as the fingering reference — it carries finger
|
||||
numbers and barre info the browser doesn't show. */}
|
||||
{voicings.length > 0 && (
|
||||
<div>
|
||||
<p className="text-[11px] uppercase tracking-wider text-gray-600 mb-2">More grips — fingering reference</p>
|
||||
<p className="text-xs text-gray-500 mb-4">
|
||||
Click any voicing to learn it. Purple = chord tones. Finger numbers inside dots (1=index, 4=pinky).
|
||||
Barre chords show the fret number on the left.
|
||||
@@ -60,6 +72,8 @@ function GuitarTab({ chordName }) {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -68,17 +82,28 @@ function PianoTab({ chordName }) {
|
||||
const techniques = getPianoTechniques(chordName)
|
||||
const rootPc = parsed?.rootPc ?? 0
|
||||
|
||||
if (!techniques.length) {
|
||||
if (!parsed && !techniques.length) {
|
||||
return <p className="text-gray-500 text-sm text-center py-8">No piano techniques for {chordName}.</p>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Playable voicing browser (D-21) — parseChord's `type` maps 1:1 to the
|
||||
browser's `quality` (CHORD_TYPES key). L-21. */}
|
||||
{parsed && <VoicingBrowser rootPc={parsed.rootPc} quality={parsed.type} />}
|
||||
|
||||
{/* Technique cards kept below — the name/desc/tip text and LH/RH note
|
||||
breakdown are not covered by the browser. */}
|
||||
{techniques.length > 0 && (
|
||||
<p className="text-[11px] uppercase tracking-wider text-gray-600 mt-1">Techniques — how to use it at the keys</p>
|
||||
)}
|
||||
{techniques.length > 0 && (
|
||||
<p className="text-xs text-gray-500">
|
||||
<span className="text-blue-400 font-semibold">Blue = Left hand</span> ·
|
||||
<span className="text-accent font-semibold">Purple = Right hand</span> ·
|
||||
R marks the root.
|
||||
</p>
|
||||
)}
|
||||
{techniques.map((t, i) => (
|
||||
<div key={i} className="p-4 rounded-xl bg-surface border border-border hover:border-accent/30 transition-colors">
|
||||
<div className="flex flex-col lg:flex-row gap-4 items-start">
|
||||
|
||||
@@ -5,6 +5,7 @@ import { NOTES, CHORD_TYPES } from '../lib/theory'
|
||||
import RoadmapTrack from './RoadmapTrack'
|
||||
import ChordDiagram from './ChordDiagram'
|
||||
import MiniPiano from './MiniPiano'
|
||||
import VoicingBrowser from './VoicingBrowser'
|
||||
import { pianoVoicingChain } from '../lib/piano'
|
||||
|
||||
// ─── JamGuide — the Roadmap bottom dock ───────────────────────────────────────
|
||||
@@ -412,6 +413,14 @@ function RoadmapAssembly({
|
||||
label={`${selected.label}${selected.rn ? ` · ${selected.rn}` : ''}`}
|
||||
/>
|
||||
)}
|
||||
{/* Browse + audition every voicing of this station's chord (L-21).
|
||||
Stations already carry {rootPc, quality} (L-11); the browser is
|
||||
fully self-contained (D-21) and wraps dock-friendly. Purely
|
||||
additive — the onFocusChord guide-tone contract above is
|
||||
untouched (it keys off selectedStation, not this render). */}
|
||||
<div className="w-full">
|
||||
<VoicingBrowser rootPc={selected.rootPc} quality={selected.quality} />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSelectStation(null)}
|
||||
|
||||
Reference in New Issue
Block a user