From 5a4d0ef648798cf679c97e1c8a977fe16dc2bcb4 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Thu, 9 Apr 2026 00:39:43 +0200 Subject: [PATCH] feat: GuitarFretboard component --- .../chord-diagram/guitar-fretboard.tsx | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 app/app/components/chord-diagram/guitar-fretboard.tsx diff --git a/app/app/components/chord-diagram/guitar-fretboard.tsx b/app/app/components/chord-diagram/guitar-fretboard.tsx new file mode 100644 index 0000000..bb9b001 --- /dev/null +++ b/app/app/components/chord-diagram/guitar-fretboard.tsx @@ -0,0 +1,103 @@ +import type { GuitarVoicing } from '~/lib/chord-voicing'; + +const FRETS_SHOWN = 4; +const STRING_COUNT = 6; + +interface Props { + voicing: GuitarVoicing | null; +} + +export function GuitarFretboard({ voicing }: Props) { + if (!voicing) { + return ( +
+ no voicing +
+ ); + } + + const { frets, baseFret, barre } = voicing; + + // Show fret number label when not at open position + const showFretLabel = baseFret > 0; + + return ( +
+ {/* Open/muted string indicators above nut */} +
+ {frets.map((f, i) => ( +
+ {f === null ? ( + + ) : f === 0 ? ( + + ) : null} +
+ ))} +
+ + {/* Fretboard grid */} +
+ {/* Fret number label */} + {showFretLabel && ( +
+ {baseFret} +
+ )} + + {/* Strings (columns) */} + {frets.map((fret, stringIdx) => ( +
+ {/* Nut or top border */} +
+ {/* Fret cells */} + {Array.from({ length: FRETS_SHOWN }, (_, fretIdx) => { + // Open position (baseFret=0): rows represent frets 1-4 (nut is shown above) + // Barre position (baseFret>0): rows represent frets baseFret, baseFret+1, … + const absoluteFret = baseFret === 0 ? fretIdx + 1 : baseFret + fretIdx; + const hasDot = fret !== null && fret > 0 && fret === absoluteFret; + return ( +
+ {hasDot && ( +
+ )} +
+ ); + })} +
+ ))} + + {/* Barre indicator — only for actual barre chords (not open position) */} + {barre !== null && barre > 0 && ( +
+ )} +
+
+ ); +}