fix: use var(--x) not hsl(var(--x)) for oklch theme vars, redesign fretboard with explicit string lines
This commit is contained in:
@@ -1,7 +1,13 @@
|
|||||||
import type { GuitarVoicing } from '~/lib/chord-voicing';
|
import type { GuitarVoicing } from '~/lib/chord-voicing';
|
||||||
|
|
||||||
const FRETS_SHOWN = 4;
|
|
||||||
const STRING_COUNT = 6;
|
const STRING_COUNT = 6;
|
||||||
|
const FRET_COUNT = 4;
|
||||||
|
const STRING_SPACING = 13; // px between adjacent string lines
|
||||||
|
const FRET_HEIGHT = 18; // px per fret
|
||||||
|
const DOT_R = 5; // finger dot radius
|
||||||
|
|
||||||
|
const GRID_W = STRING_SPACING * (STRING_COUNT - 1); // 65px
|
||||||
|
const GRID_H = FRET_HEIGHT * FRET_COUNT; // 72px
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
voicing: GuitarVoicing | null;
|
voicing: GuitarVoicing | null;
|
||||||
@@ -10,93 +16,139 @@ interface Props {
|
|||||||
export function GuitarFretboard({ voicing }: Props) {
|
export function GuitarFretboard({ voicing }: Props) {
|
||||||
if (!voicing) {
|
if (!voicing) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center text-muted-foreground text-xs h-20 w-24">
|
<div
|
||||||
|
className="flex items-center justify-center text-muted-foreground text-xs"
|
||||||
|
style={{ height: GRID_H + 20, width: GRID_W + 20 }}
|
||||||
|
>
|
||||||
no voicing
|
no voicing
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { frets, baseFret, barre } = voicing;
|
const { frets, baseFret, barre } = voicing;
|
||||||
|
|
||||||
// Show fret number label when not at open position
|
|
||||||
const showFretLabel = baseFret > 0;
|
const showFretLabel = baseFret > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center gap-1">
|
<div className="flex flex-col items-center">
|
||||||
{/* Open/muted string indicators above nut */}
|
{/* O/X string indicators above nut */}
|
||||||
<div className="flex" style={{ gap: 2 }}>
|
<div className="relative" style={{ width: GRID_W, height: 16 }}>
|
||||||
{frets.map((f, i) => (
|
{frets.map((f, i) =>
|
||||||
<div
|
f === null ? (
|
||||||
key={i}
|
<span
|
||||||
className="flex items-center justify-center text-xs font-mono"
|
key={i}
|
||||||
style={{ width: 14 }}
|
className="absolute font-mono text-destructive"
|
||||||
>
|
style={{ fontSize: 10, top: 2, left: i * STRING_SPACING - 4 }}
|
||||||
{f === null ? (
|
>
|
||||||
<span className="text-destructive">✕</span>
|
✕
|
||||||
) : f === 0 ? (
|
</span>
|
||||||
<span className="text-muted-foreground">○</span>
|
) : f === 0 ? (
|
||||||
) : null}
|
<span
|
||||||
</div>
|
key={i}
|
||||||
))}
|
className="absolute font-mono text-muted-foreground"
|
||||||
|
style={{ fontSize: 10, top: 2, left: i * STRING_SPACING - 5 }}
|
||||||
|
>
|
||||||
|
○
|
||||||
|
</span>
|
||||||
|
) : null
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Fretboard grid */}
|
{/* Fret number + grid */}
|
||||||
<div className="relative flex" style={{ gap: 0 }}>
|
<div className="flex items-start" style={{ gap: 4 }}>
|
||||||
{/* Fret number label */}
|
{/* Fret number label (barre position) */}
|
||||||
{showFretLabel && (
|
<div style={{ width: 12, textAlign: 'right' }}>
|
||||||
<div
|
{showFretLabel && (
|
||||||
className="absolute text-xs text-muted-foreground font-mono"
|
<span
|
||||||
style={{ left: -18, top: 0, lineHeight: '18px' }}
|
className="font-mono text-muted-foreground"
|
||||||
>
|
style={{ fontSize: 9, lineHeight: `${FRET_HEIGHT}px` }}
|
||||||
{baseFret}
|
>
|
||||||
</div>
|
{baseFret}
|
||||||
)}
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Strings (columns) */}
|
{/* Fretboard grid */}
|
||||||
{frets.map((fret, stringIdx) => (
|
<div
|
||||||
<div key={stringIdx} className="relative" style={{ width: 14 }}>
|
className="relative"
|
||||||
{/* Nut or top border */}
|
style={{
|
||||||
|
width: GRID_W,
|
||||||
|
height: GRID_H,
|
||||||
|
// Nut: thick when open position, thin when barre
|
||||||
|
borderTop: showFretLabel
|
||||||
|
? `1px solid var(--border)`
|
||||||
|
: `4px solid var(--foreground)`,
|
||||||
|
borderBottom: `1px solid var(--border)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* String lines (6 vertical lines) */}
|
||||||
|
{Array.from({ length: STRING_COUNT }, (_, i) => (
|
||||||
<div
|
<div
|
||||||
|
key={i}
|
||||||
style={{
|
style={{
|
||||||
height: showFretLabel ? 2 : 4,
|
position: 'absolute',
|
||||||
background: showFretLabel ? 'hsl(var(--border))' : 'hsl(var(--foreground))',
|
top: 0,
|
||||||
margin: '0 2px',
|
bottom: 0,
|
||||||
|
left: i * STRING_SPACING,
|
||||||
|
width: 1,
|
||||||
|
background: 'var(--foreground)',
|
||||||
|
opacity: 0.35,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* 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) */}
|
{/* Fret lines (horizontal, between frets) */}
|
||||||
{barre !== null && barre > 0 && (
|
{Array.from({ length: FRET_COUNT - 1 }, (_, i) => (
|
||||||
<div
|
<div
|
||||||
className="absolute rounded-full bg-primary"
|
key={i}
|
||||||
style={{
|
style={{
|
||||||
left: 2,
|
position: 'absolute',
|
||||||
right: 2,
|
left: 0,
|
||||||
height: 8,
|
right: 0,
|
||||||
top: (showFretLabel ? 2 : 4) + (barre - baseFret) * 18 + 5,
|
top: (i + 1) * FRET_HEIGHT,
|
||||||
zIndex: 5,
|
height: 1,
|
||||||
}}
|
background: 'var(--border)',
|
||||||
/>
|
}}
|
||||||
)}
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Barre bar — only for transposed barre chords */}
|
||||||
|
{barre !== null && barre > 0 && (
|
||||||
|
<div
|
||||||
|
className="absolute rounded-full"
|
||||||
|
style={{
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: DOT_R * 2,
|
||||||
|
top: (barre - baseFret) * FRET_HEIGHT + FRET_HEIGHT / 2 - DOT_R,
|
||||||
|
background: 'var(--primary)',
|
||||||
|
zIndex: 5,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Finger dots */}
|
||||||
|
{frets.map((fret, stringIdx) => {
|
||||||
|
if (fret === null || fret === 0) return null;
|
||||||
|
// Skip strings covered by the barre bar
|
||||||
|
if (barre !== null && fret === barre) return null;
|
||||||
|
const fretIdx = baseFret === 0 ? fret - 1 : fret - baseFret;
|
||||||
|
if (fretIdx < 0 || fretIdx >= FRET_COUNT) return null;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={stringIdx}
|
||||||
|
className="absolute rounded-full"
|
||||||
|
style={{
|
||||||
|
width: DOT_R * 2,
|
||||||
|
height: DOT_R * 2,
|
||||||
|
left: stringIdx * STRING_SPACING - DOT_R,
|
||||||
|
top: fretIdx * FRET_HEIGHT + FRET_HEIGHT / 2 - DOT_R,
|
||||||
|
background: 'var(--primary)',
|
||||||
|
zIndex: 10,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function PianoKeys({ notes }: Props) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex items-center justify-center text-muted-foreground text-xs"
|
className="flex items-center justify-center text-muted-foreground text-xs"
|
||||||
style={{ width: totalWidth, height: WHITE_KEY_H + 14 }}
|
style={{ width: totalWidth, height: WHITE_KEY_H + 26 }}
|
||||||
>
|
>
|
||||||
?
|
?
|
||||||
</div>
|
</div>
|
||||||
@@ -52,7 +52,7 @@ export function PianoKeys({ notes }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: totalWidth }}>
|
<div style={{ width: totalWidth }}>
|
||||||
{/* Keyboard — light container so keys are visible in any theme */}
|
{/* Keyboard — gray container makes keys visible in any theme */}
|
||||||
<div
|
<div
|
||||||
className="relative rounded-sm"
|
className="relative rounded-sm"
|
||||||
style={{ width: totalWidth, height: WHITE_KEY_H, background: '#d0d0d0' }}
|
style={{ width: totalWidth, height: WHITE_KEY_H, background: '#d0d0d0' }}
|
||||||
@@ -69,21 +69,20 @@ export function PianoKeys({ notes }: Props) {
|
|||||||
top: 0,
|
top: 0,
|
||||||
width: WHITE_KEY_W - 2,
|
width: WHITE_KEY_W - 2,
|
||||||
height: WHITE_KEY_H - 1,
|
height: WHITE_KEY_H - 1,
|
||||||
background: active ? 'hsl(var(--primary) / 0.2)' : 'white',
|
background: 'white',
|
||||||
borderBottom: active
|
borderBottom: active ? '3px solid var(--primary)' : '1px solid #bbb',
|
||||||
? '3px solid hsl(var(--primary))'
|
|
||||||
: '1px solid #bbb',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{active && (
|
{active && (
|
||||||
<div
|
<div
|
||||||
className="absolute rounded-full bg-primary"
|
className="absolute rounded-full"
|
||||||
style={{
|
style={{
|
||||||
width: 10,
|
width: 10,
|
||||||
height: 10,
|
height: 10,
|
||||||
bottom: 10,
|
bottom: 10,
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translateX(-50%)',
|
transform: 'translateX(-50%)',
|
||||||
|
background: 'var(--primary)',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -103,19 +102,18 @@ export function PianoKeys({ notes }: Props) {
|
|||||||
top: 0,
|
top: 0,
|
||||||
width: BLACK_KEY_W,
|
width: BLACK_KEY_W,
|
||||||
height: BLACK_KEY_H,
|
height: BLACK_KEY_H,
|
||||||
background: active ? 'hsl(var(--primary))' : '#1c1c1c',
|
background: active ? 'var(--primary)' : '#1c1c1c',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{active && (
|
{active && (
|
||||||
<div
|
<div
|
||||||
className="absolute rounded-full bg-white"
|
className="absolute rounded-full bg-white"
|
||||||
style={{
|
style={{
|
||||||
width: 10,
|
width: 7,
|
||||||
height: 10,
|
height: 7,
|
||||||
bottom: 4,
|
bottom: 5,
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translateX(-50%)',
|
transform: 'translateX(-50%)',
|
||||||
border: '2px solid hsl(var(--primary))',
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -124,15 +122,21 @@ export function PianoKeys({ notes }: Props) {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Note labels below keyboard */}
|
{/* White key note labels */}
|
||||||
<div className="flex" style={{ width: totalWidth, marginTop: 2 }}>
|
<div className="flex" style={{ width: totalWidth, marginTop: 2 }}>
|
||||||
{WHITE_KEYS.map((note) => {
|
{WHITE_KEYS.map((note) => {
|
||||||
const active = activeChroma.has(NOTE_CHROMA[note]);
|
const active = activeChroma.has(NOTE_CHROMA[note]);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={note}
|
key={note}
|
||||||
className={`text-center font-mono select-none ${active ? 'text-primary font-bold' : 'text-muted-foreground'}`}
|
className="text-center font-mono select-none"
|
||||||
style={{ width: WHITE_KEY_W, fontSize: 8, lineHeight: '12px' }}
|
style={{
|
||||||
|
width: WHITE_KEY_W,
|
||||||
|
fontSize: 8,
|
||||||
|
lineHeight: '12px',
|
||||||
|
color: active ? 'var(--primary)' : undefined,
|
||||||
|
fontWeight: active ? 700 : undefined,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{note}
|
{note}
|
||||||
</div>
|
</div>
|
||||||
@@ -140,13 +144,13 @@ export function PianoKeys({ notes }: Props) {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Active note names — unambiguous list including black keys */}
|
{/* Active note names — clearly lists all notes including black keys */}
|
||||||
<div className="flex flex-wrap justify-center gap-1 mt-1">
|
<div className="flex flex-wrap justify-center gap-1" style={{ marginTop: 2 }}>
|
||||||
{notes.map((note) => (
|
{notes.map((note) => (
|
||||||
<span
|
<span
|
||||||
key={note}
|
key={note}
|
||||||
className="font-mono font-semibold text-primary"
|
className="font-mono font-semibold"
|
||||||
style={{ fontSize: 9 }}
|
style={{ fontSize: 9, color: 'var(--primary)' }}
|
||||||
>
|
>
|
||||||
{note}
|
{note}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user