import { getPentatonicScale, getFullScale, getChordTones, guideTones, NOTES } from '../lib/theory' // Standard tuning: pitch classes of open strings, high-E first (top of diagram) const STRINGS = [ { label: 'e', root: 4 }, // high E { label: 'B', root: 11 }, { label: 'G', root: 7 }, { label: 'D', root: 2 }, { label: 'A', root: 9 }, { label: 'E', root: 4 }, // low E ] const NUM_FRETS = 13 // frets 0 (open) through 12 const FRET_MARKERS = [3, 5, 7, 9] const DOUBLE_MARKER = 12 // Layout constants const NUT_X = 40 // x of the nut line const OPEN_X = 18 // x of open-string dot centres const FRET_W = 52 // pixels per fret const STRING_H = 28 // pixels between strings const PAD_T = 28 // top padding (fret numbers) const PAD_B = 18 // bottom padding (fret marker dots) const BOARD_W = NUT_X + (NUM_FRETS - 1) * FRET_W + 10 const BOARD_H = PAD_T + 5 * STRING_H + PAD_B const DOT_R = 10 // x centre of a fretted note (fret >= 1) const fretX = f => NUT_X + (f - 0.5) * FRET_W // y centre of string si (0 = high e, 5 = low E) const stringY = si => PAD_T + si * STRING_H function noteColor(isChordTone, isPenta, isScale, mono = false) { if (isChordTone) return { fill: '#a855f7', text: '#fff' } if (isPenta) return mono ? { fill: '#c084fc', text: '#1e1b4b' } : { fill: '#f59e0b', text: '#000' } if (isScale) return mono ? { fill: '#e9d5ff', text: '#581c87' } : { fill: '#374151', text: '#d1d5db' } return 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 const pentaSet = new Set(getPentatonicScale(root, mode).map(n => NOTES.indexOf(n))) const scaleSet = pentatonicOnly ? pentaSet : new Set(getFullScale(root, mode).map(n => NOTES.indexOf(n))) const chordSet = currentChord ? new Set(getChordTones(currentChord).map(n => NOTES.indexOf(n))) : new Set() // ── Jam Guide focus: guide tones of the tapped Roadmap station ────────────── // `guideTones` returns { third, seventh, hasSeventh }. We emphasise the 3rd // (the quality-defining tone) and the secondary anchor — the 7th when present, // else the 5th for a triad (hasSeventh:false). These pitch classes get a halo // ring + a small tag so they read as a distinct "target" tier on top of the // normal chord/penta/scale colouring. let focusThird = -1, focusSeventh = -1, focusRootPc = 0 if (jamFocusChord && typeof jamFocusChord.rootPc === 'number') { const gt = guideTones(jamFocusChord.rootPc, jamFocusChord.quality) focusThird = gt.third focusSeventh = gt.seventh focusRootPc = gt.root } const hasFocus = focusThird >= 0 // Defense-in-depth: label the secondary anchor from its ACTUAL interval above // the chord root, so a wrong `hasSeventh` boolean could never mislabel a 5th // or 6th as a "7". 10/11 → "7", 9 → "6", 8 → "♭6"(#5), 7 → "5", 6 → "♭5". const focusSeventhLabel = (() => { const iv = ((focusSeventh - focusRootPc) % 12 + 12) % 12 if (iv === 10 || iv === 11) return '7' if (iv === 9) return '6' if (iv === 8) return '♭6' if (iv === 6) return '♭5' return '5' })() const focusLabel = pc => pc === focusThird ? '3' : pc === focusSeventh ? focusSeventhLabel : null const heading = (

Fretboard — {root} {mode} {currentChord && / {currentChord}} {hasFocus && ◎ guide tones}

) 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.
Chord tone Pentatonic Scale {hasFocus && ( Guide tones (3 / {focusSeventhLabel}) )}
) return (
{compact ? (
{heading} {legend}
) : ( heading )}
{/* Fretboard background */} {/* Fret position marker dots (between strings 2–3 and 3–4) */} {FRET_MARKERS.map(f => ( ))} {/* Double dot at 12 */} {/* Fret lines (1–12) */} {Array.from({ length: NUM_FRETS - 1 }, (_, i) => i + 1).map(f => ( ))} {/* Nut */} {/* Strings */} {STRINGS.map((_, si) => ( ))} {/* Fret numbers */} {[3, 5, 7, 9, 12].map(f => ( {f} ))} {/* String labels */} {STRINGS.map((s, si) => ( {s.label} ))} {/* Note dots */} {STRINGS.flatMap((str, si) => 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) 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} )} ) }) )}
{!compact && legend}
) }