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