Files
pocket-chords/docs/superpowers/specs/2026-04-09-chord-diagram-design.md

135 lines
5.7 KiB
Markdown

# Chord Diagram Feature Design
**Date:** 2026-04-09
## Overview
A chord diagram feature for the song detail page that shows users how to play each chord on piano or guitar. The core component is dumb — it receives only a chord name string and renders the diagram. All music theory and voicing logic lives in a separate library layer.
## Architecture
Three cleanly separated layers:
```
chord name string ("Cmaj7")
[theory layer] — tonal parses name → root + note set {C, E, G, B}
[voicing layer] — maps note set → renderable positions
├── Piano: note set → highlight keys on 1-octave keyboard
└── Guitar: chord quality + root → transpose moveable shape template
[render layer] — dumb components, no music theory knowledge
├── <PianoKeys notes={["C","E","G","B"]} />
└── <GuitarFretboard frets={[x,3,2,0,1,0]} baseFret={1} />
```
## Files
```
app/app/
lib/
chord-voicing.ts # theory layer: tonal → notes + guitar voicing
guitar-voicings.ts # data: ~25 quality templates
components/
chord-diagram/
piano-keys.tsx # dumb renderer: string[] of note names → keyboard
guitar-fretboard.tsx # dumb renderer: frets[] + baseFret → fretboard grid
chord-diagram.tsx # entry point: chord+instrument → voicing → renderer
chord-grid.tsx # wrapped grid of ChordDiagram cards for all song chords
```
## Component API
### `<ChordDiagram>`
```tsx
<ChordDiagram chord="Cmaj7" instrument="piano" />
<ChordDiagram chord="Am" instrument="guitar" />
```
Renders nothing (graceful empty) if the chord cannot be parsed or has no voicing.
### `<ChordGrid>`
```tsx
<ChordGrid chords={["Em7", "G", "Dsus4", "Am7"]} />
```
Owns the `instrument` state (`"piano" | "guitar"`), persisted to `localStorage` as `chordDiagramInstrument`. Renders a global piano/guitar toggle and a `flex-wrap` grid of `<ChordDiagram>` cards.
## Diagram Styles
- **Piano:** dot notation — white keys with filled circles on pressed keys, black keys overlaid. 1 fixed octave shown (C to B); notes are matched by name regardless of octave.
- **Guitar:** standard vertical fretboard — nut at top, 4 frets shown, dots on finger positions, O/X above strings for open/muted. Barre indicator where applicable.
## Theory Layer (`chord-voicing.ts`)
Uses `@tonaljs/tonal` (already in npm, tree-shakeable):
```ts
export function getPianoNotes(chord: string): string[]
// "Cmaj7" → ["C", "E", "G", "B"]
// Returns [] if unparseable
export function getGuitarVoicing(chord: string): GuitarVoicing | null
// "Am" → { frets: [0,0,2,2,1,0], baseFret: 1, barre: null }
// Returns null if quality not in voicing map
```
## Guitar Voicing Data (`guitar-voicings.ts`)
~25 moveable barre-chord templates keyed by `tonal` chord type name. Each template is a barre shape (no open strings) so it can be transposed by shifting `baseFret`. Two shape families are used: E-shapes (root on 6th string) and A-shapes (root on 5th string). `baseFret` is computed as the semitone distance from the template shape's root string pitch (E or A) to the target chord root.
```ts
interface GuitarVoicingTemplate {
frets: (number | null)[] // 6 strings; null = muted; fret numbers relative to baseFret
baseFret: number // 1 in template; shifted when transposing to target root
barre: number | null // fret (relative to baseFret) to draw barre, or null
rootString: 'E' | 'A' // which string carries the root (determines transposition offset)
}
```
Quality names match `tonal`'s `Chord.get(name).type` output (e.g. `"major"`, `"minor"`, `"major seventh"`, `"dominant seventh"`, `"minor seventh"`, `"diminished"`, `"augmented"`, `"suspended fourth"`, `"suspended second"`, `"half-diminished"`, `"dominant seventh flat five"`, etc.). ~25 entries total.
If `tonal` returns a quality name not in the map, `getGuitarVoicing` returns `null` and the diagram renders a "no guitar voicing" placeholder.
## Layout & Integration
### Breakpoint
`lg` (Tailwind) divides mobile from desktop layout.
### Mobile
- Below `lg`: lyrics and diagrams in a single column.
- **Inline popup:** tapping a chord name in `chord-chart.tsx` sets `activeChord` state in `songs.$id.tsx`. An inline `<ChordDiagram>` panel appears immediately below the tapped line. It closes when the scroll container fires a `scroll` event.
- **Bottom grid:** `<ChordGrid>` rendered after `<ChordChart>` in the scroll column. Not sticky — scrolls with content.
### Desktop
- At `lg` and above: `songs.$id.tsx` switches to a two-column layout.
- Left column: `<ChordChart>` (existing).
- Right column: `<ChordGrid>` showing all unique chords in the song, wrapped. No inline popup on desktop (side column is always visible).
### Chord list source
`songs.$id.tsx` derives `uniqueChords: string[]` from `displayed.sections` — all unique chord names in order of first appearance, deduplicated. This list is passed to `<ChordGrid>` and also used to determine which chord names in `<ChordChart>` are tappable.
### Instrument toggle
Global piano/guitar toggle lives in `<ChordGrid>`. State persisted to `localStorage` as `chordDiagramInstrument`. Switching updates all visible diagrams at once.
## Error / Unknown Chord Handling
- `getPianoNotes` returns `[]``<PianoKeys>` renders with no dots highlighted and a subtle "?" label.
- `getGuitarVoicing` returns `null``<GuitarFretboard>` renders an empty fretboard with a "no voicing" label.
- Unparseable chord name (garbage string) → same fallback as above.
## Dependency
Add `tonal` to `app/package.json`. It is tree-shakeable; only chord parsing and note utilities will be bundled.