// ExplorePanel — refactored into Knowledge Center parts (task L-22, per // docs/design/knowledge-center.md §7 step 1). // // This file now exports the named building blocks the Knowledge Center shell // (JamGuide.jsx) composes: // // — the shared foundation/intermediate filter // — controlled root × quality picker row // — KB progression browser + famous progressions // — picker (follows the live chord) → VoicingBrowser // // The default export remains a thin standalone composition of the parts (the // panel is verified-orphaned — no importer — so it exists only so the file // stays a complete, mountable component). GuitarGrid/PianoGrid are kept as // exported no-audio fallbacks per the D-20 IA map (§2). import { useEffect, useMemo, useState } from 'react' import ChordBox from './ChordBox' import CircleOfFifths from './CircleOfFifths' import MiniPiano from './MiniPiano' import VoicingBrowser from './VoicingBrowser' import kb from '../data/kb/index.js' import { getGuitarVoicings, getPianoTechniques, parseChord } from '../lib/voicings' import { CHORD_TYPES, NOTES, getChordsInKey, toRomanNumeral } from '../lib/theory' import { FAMOUS_PROGRESSIONS, progressionInKey } from '../lib/education' const CHORD_TYPE_OPTIONS = [ { key: 'maj', label: 'Major' }, { key: 'min', label: 'Minor' }, { key: 'dom7', label: '7' }, { key: 'maj7', label: 'maj7' }, { key: 'min7', label: 'm7' }, { key: 'dim', label: 'dim' }, { key: 'dim7', label: 'dim7' }, { key: 'half_dim', label: 'm7♭5' }, { key: 'aug', label: 'aug' }, { key: 'sus4', label: 'sus4' }, { key: 'sus2', label: 'sus2' }, { key: 'maj6', label: '6' }, { key: 'min6', label: 'm6' }, { key: 'add9', label: 'add9' }, ] const MAJOR_TYPES = new Set(['maj','maj7','maj6','add9','sus4','sus2','aug','dom7']) // Progressions/licks without a `level` count as foundation (D-20 §4). const levelOf = (item) => (item?.level === 'intermediate' ? 'intermediate' : 'foundation') // ─── Level filter chips (shared by Explore + Licks toolbars) ────────────────── // Two toggle chips, both on by default. The SHELL owns the `levels` state // ({foundation, intermediate}) and enforces "both can't be off"; the chip for // the last active level advertises the no-op via its title. export function LevelChips({ levels = {}, onToggle }) { const defs = [ { key: 'foundation', label: 'Foundation' }, { key: 'intermediate', label: 'Intermediate' }, ] return (
{defs.map(d => { const active = !!levels[d.key] const lastActive = active && !defs.some(o => o.key !== d.key && levels[o.key]) return ( ) })}
) } // Level badge on cards — mirrors LickCard's badge treatment (amber = the // existing secondary-tone token; foundation stays quiet). function LevelBadge({ level }) { if (level === 'intermediate') { return ( intermediate ) } if (level === 'foundation') { return ( foundation ) } return null } // ─── Quick-pick chip row ────────────────────────────────────────────────────── function ChipRow({ label, chords, active, keyInfo, onSelect }) { if (!chords?.length) return null return (
{label}
{chords.map(chord => { const rn = keyInfo?.root ? toRomanNumeral(chord, keyInfo.root, keyInfo.mode) : '' return ( ) })}
) } // ─── Chord picker toolbar (controlled: root × quality) ─────────────────────── export function ChordPickerToolbar({ root, typeKey, onRootChange, onTypeChange }) { const chordName = root + (CHORD_TYPES[typeKey]?.suffix ?? '') return (
{NOTES.map(n => ( ))}
{chordName}
) } // ─── Guitar voicings grid (no-audio fallback; superseded by VoicingBrowser) ── export function GuitarGrid({ chordName }) { const voicings = getGuitarVoicings(chordName) if (!voicings.length) return

No voicings for {chordName}.

return (
{voicings.map((v, i) => (

{v.label}

))}

Purple = chord tone · finger numbers inside dots (1=index 4=pinky) · fret number on left if not starting at fret 1

) } // ─── Piano techniques grid (no-audio fallback; superseded by VoicingBrowser) ─ export function PianoGrid({ chordName }) { const parsed = parseChord(chordName) const techniques = getPianoTechniques(chordName) const rootPc = parsed?.rootPc ?? 0 if (!techniques.length) return

No techniques for {chordName}.

return (
{techniques.map((t, i) => (

{t.name}

{t.desc}

Tip: {t.tip}

))}
) } // ─── Famous progressions using this chord as tonic ─────────────────────────── // NOTE (D-20 §4, recorded Maestro call): FAMOUS_PROGRESSIONS carries no `level` // field — these cards show no badge and are EXEMPT from the level filter. function ProgressionCards({ chordName, onChordClick }) { const parsed = parseChord(chordName) if (!parsed) return null const { rootPc, type } = parsed const root = NOTES[rootPc] const isMajor = MAJOR_TYPES.has(type) const matching = FAMOUS_PROGRESSIONS.filter(p => { const q0 = p.qualities[0] return isMajor ? MAJOR_TYPES.has(q0) : !MAJOR_TYPES.has(q0) }).slice(0, 6) return (

Famous progressions with {chordName} as the tonic. Click any chord to see its voicings.

{matching.map(prog => { const chordsHere = progressionInKey(prog, root) return (
{prog.name} {prog.pattern} {prog.genre.slice(0, 2).map(g => ( {g} ))}
{chordsHere.map((c, i) => ( {i < chordsHere.length - 1 && } ))}

{prog.description}

{prog.songs.length > 0 && (

{prog.songs.slice(0, 3).join(' · ')}

)}
) })}
) } // ─── One KB progression card (the Explore browser hero) ────────────────────── function KbProgressionCard({ prog, keyRootPc, onChordClick }) { const degrees = prog?.degrees ?? [] const qualities = prog?.qualities ?? [] const chords = degrees.map((deg, i) => { const pc = (((keyRootPc + deg) % 12) + 12) % 12 return `${NOTES[pc]}${CHORD_TYPES[qualities[i]]?.suffix ?? ''}` }) const songs = Array.isArray(prog?.songs) ? prog.songs : [] return (
{prog?.name ?? prog?.id ?? 'Untitled'} {Array.isArray(prog?.rn) && prog.rn.length > 0 && ( {prog.rn.join(' – ')} )}
{chords.length > 0 && (
{chords.map((c, i) => ( {i < chords.length - 1 && } ))}
)} {prog?.tip &&

{prog.tip}

} {songs.length > 0 && (

{songs.slice(0, 3).join(' · ')}

)}
) } // ─── Explore section — KB progression browser + famous progressions ────────── // Props: keyInfo (chords render in the detected key; C until one is known), // levels + onToggleLevel (shell-owned shared filter), onChordClick (chord name // string → ChordDetailModal). export function ExploreSection({ keyInfo, levels, onToggleLevel, onChordClick }) { const styles = useMemo( () => Object.entries(kb ?? {}).map(([id, s]) => ({ id, label: s?.meta?.label ?? id })), [] ) const [styleOverride, setStyleOverride] = useState(null) const activeStyle = styleOverride ?? styles[0]?.id const keyRootPc = parseChord(keyInfo?.root ?? '')?.rootPc ?? 0 const keyMode = keyInfo?.mode === 'minor' ? 'minor' : 'major' const tonicName = `${NOTES[keyRootPc]}${keyMode === 'minor' ? 'm' : ''}` const progressions = kb?.[activeStyle]?.progressions ?? [] const visible = progressions.filter(p => levels?.[levelOf(p)]) return (
{/* Toolbar: style chips + shared level filter */}
{styles.map(s => { const active = s.id === activeStyle return ( ) })}
{/* Circle of fifths — live key map with inline diatonics (task D-61). keyInfo here IS App's effectiveKey (App → KnowledgeDock → this section); the circle is read-only — tapping wedges never touches key state. */}

Chords shown in {NOTES[keyRootPc]} {keyMode}{keyInfo?.root ? '' : ' (no key detected yet)'} · tap any chord for voicings

{/* KB progression cards */} {visible.length > 0 ? (
{visible.map((p, i) => ( ))}
) : (

{progressions.length === 0 ? 'No progressions authored for this style yet.' : 'Nothing at the selected level for this style — flip the level filter back on.'}

)} {/* Famous progressions (exempt from the level filter — untagged corpus) */}

Famous progressions · not affected by the level filter

) } // ─── Voicings section — picker (follows the live chord) → VoicingBrowser ───── // Props: keyInfo + chordHistory feed the quick-pick chips; currentChord re-aims // the picker whenever a new chord commits (manual picks hold until then). // `instrument` (L-40, D-40 §3) scopes the browser to App's global selector — // omitted (the orphaned standalone panel below) it falls back to 'both' via // VoicingBrowser's own `show` default. export function VoicingsSection({ keyInfo, chordHistory, currentChord, instrument }) { const [root, setRoot] = useState('C') const [typeKey, setTypeKey] = useState('maj') const [active, setActive] = useState('') useEffect(() => { if (!currentChord) return const p = parseChord(currentChord) if (p) { setRoot(NOTES[p.rootPc]); setTypeKey(p.type); setActive(currentChord) } }, [currentChord]) function selectChord(chord) { setActive(chord) const p = parseChord(chord) if (p) { setRoot(NOTES[p.rootPc]); setTypeKey(p.type) } } const recentChords = [...new Set([...(chordHistory ?? [])].reverse())].slice(0, 12) const keyChords = keyInfo?.root ? getChordsInKey(keyInfo.root, keyInfo.mode ?? 'major') : [] const rootPc = parseChord(root)?.rootPc ?? 0 return (
{(recentChords.length > 0 || keyChords.length > 0) && (
{keyChords.length > 0 && recentChords.length > 0 &&
} {keyChords.length > 0 && ( )}
)} { setRoot(n); setActive('') }} onTypeChange={k => { setTypeKey(k); setActive('') }} />
) } // ─── Standalone panel (thin composition; orphaned — kept mountable) ────────── export default function ExplorePanel({ keyInfo, chordHistory, currentChord, onChordClick }) { const [open, setOpen] = useState(false) const [levels, setLevels] = useState({ foundation: true, intermediate: true }) const toggleLevel = (key) => setLevels(prev => { const next = { ...prev, [key]: !prev[key] } return (next.foundation || next.intermediate) ? next : prev // both can't be off }) return (
{open && (
)}
) }