diff --git a/.gitignore b/.gitignore index 0b745e2..b014bd0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target -.env \ No newline at end of file +.env +.superpowers/ \ No newline at end of file diff --git a/docs/superpowers/specs/2026-04-09-chord-diagram-design.md b/docs/superpowers/specs/2026-04-09-chord-diagram-design.md new file mode 100644 index 0000000..b2a996c --- /dev/null +++ b/docs/superpowers/specs/2026-04-09-chord-diagram-design.md @@ -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 + ├── + └── +``` + +## 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 + +### `` + +```tsx + + +``` + +Renders nothing (graceful empty) if the chord cannot be parsed or has no voicing. + +### `` + +```tsx + +``` + +Owns the `instrument` state (`"piano" | "guitar"`), persisted to `localStorage` as `chordDiagramInstrument`. Renders a global piano/guitar toggle and a `flex-wrap` grid of `` 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 `` panel appears immediately below the tapped line. It closes when the scroll container fires a `scroll` event. +- **Bottom grid:** `` rendered after `` 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: `` (existing). +- Right column: `` 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 `` and also used to determine which chord names in `` are tappable. + +### Instrument toggle + +Global piano/guitar toggle lives in ``. State persisted to `localStorage` as `chordDiagramInstrument`. Switching updates all visible diagrams at once. + +## Error / Unknown Chord Handling + +- `getPianoNotes` returns `[]` → `` renders with no dots highlighted and a subtle "?" label. +- `getGuitarVoicing` returns `null` → `` 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.