feat: GuitarFretboard component
This commit is contained in:
103
app/app/components/chord-diagram/guitar-fretboard.tsx
Normal file
103
app/app/components/chord-diagram/guitar-fretboard.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="flex items-center justify-center text-muted-foreground text-xs h-20 w-24">
|
||||||
|
no voicing
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { frets, baseFret, barre } = voicing;
|
||||||
|
|
||||||
|
// Show fret number label when not at open position
|
||||||
|
const showFretLabel = baseFret > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center gap-1">
|
||||||
|
{/* Open/muted string indicators above nut */}
|
||||||
|
<div className="flex" style={{ gap: 2 }}>
|
||||||
|
{frets.map((f, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="flex items-center justify-center text-xs font-mono"
|
||||||
|
style={{ width: 14 }}
|
||||||
|
>
|
||||||
|
{f === null ? (
|
||||||
|
<span className="text-destructive">✕</span>
|
||||||
|
) : f === 0 ? (
|
||||||
|
<span className="text-muted-foreground">○</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Fretboard grid */}
|
||||||
|
<div className="relative flex" style={{ gap: 0 }}>
|
||||||
|
{/* Fret number label */}
|
||||||
|
{showFretLabel && (
|
||||||
|
<div
|
||||||
|
className="absolute text-xs text-muted-foreground font-mono"
|
||||||
|
style={{ left: -18, top: 0, lineHeight: '18px' }}
|
||||||
|
>
|
||||||
|
{baseFret}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Strings (columns) */}
|
||||||
|
{frets.map((fret, stringIdx) => (
|
||||||
|
<div key={stringIdx} className="relative" style={{ width: 14 }}>
|
||||||
|
{/* Nut or top border */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: showFretLabel ? 2 : 4,
|
||||||
|
background: showFretLabel ? 'hsl(var(--border))' : 'hsl(var(--foreground))',
|
||||||
|
margin: '0 2px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* 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 (
|
||||||
|
<div
|
||||||
|
key={fretIdx}
|
||||||
|
className="flex items-center justify-center border-b border-border"
|
||||||
|
style={{ height: 18, borderLeft: stringIdx === 0 ? '1px solid hsl(var(--border))' : undefined, borderRight: '1px solid hsl(var(--border))' }}
|
||||||
|
>
|
||||||
|
{hasDot && (
|
||||||
|
<div className="w-3 h-3 rounded-full bg-primary z-10" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Barre indicator — only for actual barre chords (not open position) */}
|
||||||
|
{barre !== null && barre > 0 && (
|
||||||
|
<div
|
||||||
|
className="absolute rounded-full bg-primary"
|
||||||
|
style={{
|
||||||
|
left: 2,
|
||||||
|
right: 2,
|
||||||
|
height: 8,
|
||||||
|
top: (showFretLabel ? 2 : 4) + (barre - baseFret) * 18 + 5,
|
||||||
|
zIndex: 5,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user