diff --git a/src/App.jsx b/src/App.jsx
index e9498bb..c8ea22f 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -61,6 +61,13 @@ export default function App() {
const [showDrumView, setShowDrumView] = useState(false)
const [monoColor, setMonoColor] = useState(() => loadStored('wtf_monoColor', false))
+ // ── Jam Guide → Fretboard cross-link (D-03) ──────────────────────────────────
+ // When a Roadmap station is tapped, JamGuide reports its {rootPc, quality}
+ // here and the main Fretboard highlights that chord's guide tones (3rd/7th).
+ // null = no station focused (Fretboard renders normally). Purely UI state —
+ // NOT read by any audio callback, so it stays out of the ref-sync contract.
+ const [jamFocusChord, setJamFocusChord] = useState(null) // { rootPc, quality } | null
+
// ── Mic permission error ──────────────────────────────────────────────────────
const [micError, setMicError] = useState(null)
@@ -569,7 +576,7 @@ export default function App() {
{/* ── Instrument + progressions row ── */}
Fretboard — {root} {mode}
{currentChord && / {currentChord}}
+ {hasFocus && ◎ guide tones}
@@ -121,23 +150,51 @@ export default function Fretboard({ keyInfo, currentChord, pentatonicOnly = fals
Array.from({ length: NUM_FRETS }, (_, fi) => {
const pc = (str.root + fi) % 12
const color = noteColor(chordSet.has(pc), pentaSet.has(pc), scaleSet.has(pc), monoColor)
- if (!color) return null
+ const tag = hasFocus ? focusLabel(pc) : null
+ // A guide tone outside the current scale still gets emphasised:
+ // draw a faint base dot so the halo has something to sit on.
+ if (!color && !tag) return null
const cx = fi === 0 ? OPEN_X : fretX(fi)
const cy = stringY(si)
+ const baseFill = color ? color.fill : '#2a2a2a'
+ const baseText = color ? color.text : '#a855f7'
return (
-
+ {/* Guide-tone halo: a purple ring around the dot, clearly
+ distinct from the solid chord-tone fill (a "target" marker). */}
+ {tag && (
+
+ )}
+
{NOTES[pc]}
+ {/* Degree badge (3 / 7 / 5) on the halo's upper-right. */}
+ {tag && (
+ <>
+
+
+ {tag}
+
+ >
+ )}
)
})
@@ -145,10 +202,18 @@ export default function Fretboard({ keyInfo, currentChord, pentatonicOnly = fals
-
+
● Chord tone
● Pentatonic
● Scale
+ {hasFocus && (
+
+
+ Guide tones (3 / {focusSeventhLabel})
+
+ )}
)
diff --git a/src/components/JamGuide.jsx b/src/components/JamGuide.jsx
index f4252f3..f795864 100644
--- a/src/components/JamGuide.jsx
+++ b/src/components/JamGuide.jsx
@@ -30,7 +30,7 @@ const INSTRUMENTS = [
{ id: 'bass', label: 'Bass', icon: '🎵' },
]
-export default function JamGuide({ detectedProgression, keyInfo, chordHistory = [], bpm, currentChord }) {
+export default function JamGuide({ detectedProgression, keyInfo, chordHistory = [], bpm, currentChord, onFocusChord }) {
const [open, setOpen] = useState(false)
// Which instruments have at least one KB pack across the registry.
@@ -124,6 +124,7 @@ export default function JamGuide({ detectedProgression, keyInfo, chordHistory =
return {
shape: chords[i]?.shape ?? null,
rootPc,
+ quality: qualities[i] ?? 'maj',
label: `${noteName}${suffix}`,
rn: prog?.rn?.[i] ?? '',
}
@@ -135,6 +136,21 @@ export default function JamGuide({ detectedProgression, keyInfo, chordHistory =
// Reset the selection whenever the loop or style changes underneath us.
useEffect(() => { setSelectedStation(null) }, [match.id, match.style, instrument])
+ // ── Cross-link to the main Fretboard (D-03) ─────────────────────────────────
+ // When a station is selected, report its {rootPc, quality} upward so the
+ // Fretboard can light that chord's guide tones; clear (null) on deselect. The
+ // reset effect above sets selectedStation → null on loop/style/instrument
+ // change, which flows through here and clears the highlight too. Guarded so
+ // the component still works standalone (onFocusChord optional).
+ useEffect(() => {
+ if (!onFocusChord) return
+ const st = selectedStation != null ? stationVoicings[selectedStation] : null
+ onFocusChord(st ? { rootPc: st.rootPc, quality: st.quality } : null)
+ }, [selectedStation, stationVoicings, onFocusChord])
+
+ // Clear the Fretboard highlight when JamGuide unmounts.
+ useEffect(() => () => { onFocusChord?.(null) }, [onFocusChord])
+
return (
diff --git a/src/lib/theory.js b/src/lib/theory.js
index 2043246..c6ed177 100644
--- a/src/lib/theory.js
+++ b/src/lib/theory.js
@@ -260,28 +260,39 @@ function chordTonePcs(rootPc, quality) {
/**
* guideTones(rootPc, quality) → { third, seventh, root }
*
- * The guide tones a soloist targets: a chord's 3rd and 7th. By CHORD_TYPES
- * interval ordering, index 1 is always the 3rd and (for a 7th chord) the last
- * interval is the 7th. For triads with no 7th there is no real guide 7th, so we
- * fall back to the 5th (the next most stable anchor) and flag it via
- * `hasSeventh: false` so a caller can label it honestly ("5th", not "7th").
+ * The guide tones a soloist targets: a chord's 3rd and 7th. Index 1 in every
+ * CHORD_TYPES interval set is the 3rd. A chord has a TRUE 7th only if its
+ * interval set contains 10 (m7) or 11 (M7) — NOT merely if it has 4 tones.
+ * When there is no real 7th (triads, and 4-tone non-7th chords like add9
+ * [0,2,4,7] or maj6/min6 [0,4,7,9]) we fall back to the 5th as the secondary
+ * anchor and flag `hasSeventh: false` so a caller labels it honestly ("5th",
+ * not "7th"). dim/dim7/aug have no perfect 5th, so they anchor on their ♭5/#5.
*
* Returns pitch classes (0–11) so the Roadmap TARGET lane can place dots in any
* key. `root` is included as the third anchor the design's badges reference.
*
- * Sanity (C major): guideTones(0,'maj7') → third 4 (E), seventh 11 (B).
- * guideTones(7,'dom7') → third 11 (B), seventh 5 (F).
- * guideTones(2,'min7') → third 5 (F), seventh 0 (C).
+ * Sanity (C): guideTones(0,'maj7') → third 4 (E), seventh 11 (B), hasSeventh:true.
+ * guideTones(7,'dom7') → third 11 (B), seventh 5 (F), hasSeventh:true.
+ * guideTones(2,'min7') → third 5 (F), seventh 0 (C), hasSeventh:true.
+ * guideTones(0,'add9') → third 4 (E), seventh 7 (G=5th), hasSeventh:false.
+ * guideTones(0,'maj6') / (0,'min6') → seventh 7 (G=5th), hasSeventh:false.
*/
export function guideTones(rootPc, quality) {
const type = CHORD_TYPES[quality] ?? CHORD_TYPES.maj
const r = ((rootPc % 12) + 12) % 12
const ints = type.intervals
const third = (r + ints[1]) % 12 // index 1 is always the 3rd
- const hasSeventh = ints.length >= 4 // CHORD_TYPES 7ths add a 4th tone
- // 7th when present, else the 5th as the secondary anchor (index 2 = the 5th
- // for every triad in CHORD_TYPES, which is what a triad soloist leans on).
- const seventh = (r + ints[hasSeventh ? ints.length - 1 : 2]) % 12
+ // A chord has a TRUE 7th only if its interval set contains 10 (m7) or 11 (M7).
+ // `length >= 4` is wrong: add9 [0,2,4,7] and maj6/min6 [0,4,7,9] are 4-tone
+ // chords with NO seventh, so their secondary anchor must fall back to the 5th —
+ // never badge a 5th/6th as a "7". (add9 → hasSeventh:false, anchor=5th.)
+ const seventhInt = ints.find(i => i === 10 || i === 11) // m7 / M7
+ const hasSeventh = seventhInt !== undefined
+ // Secondary anchor: the true 7th when present; otherwise the perfect 5th (7).
+ // When no perfect 5th exists either (dim/dim7 carry a ♭5=6, aug carries a #5=8),
+ // anchor on whichever altered 5th the chord actually contains.
+ const fifthInt = ints.includes(7) ? 7 : ints.includes(6) ? 6 : ints.includes(8) ? 8 : 7
+ const seventh = (r + (hasSeventh ? seventhInt : fifthInt)) % 12
return { third, seventh, root: r, hasSeventh }
}