docs: chord diagram feature spec
This commit is contained in:
134
docs/superpowers/specs/2026-04-09-chord-diagram-design.md
Normal file
134
docs/superpowers/specs/2026-04-09-chord-diagram-design.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user