# 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.