chord-component
Version:
Lit-based web components for displaying musical chord diagrams and chord lists
255 lines (219 loc) • 6.11 kB
text/typescript
import { LitElement, css, html } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import { SVGuitarChord } from 'svguitar';
import { instruments, chordOnInstrument, chordToNotes } from './music-utils.js';
import { chordDataService } from './chord-data-service.js';
import type { InstrumentDefault } from './default-chords.js';
/**
* A web component that displays a chord diagram for various instruments.
*
* @element chord-diagram
*
* @attr {string} instrument - The instrument to display the chord for (default: 'Standard Ukulele')
* @attr {string} chord - The chord name to display (e.g., 'C', 'Am7', 'F#dim')
*
* @example
* ```html
* <chord-diagram chord="C" instrument="Standard Ukulele"></chord-diagram>
* <chord-diagram chord="Am7" instrument="Standard Guitar"></chord-diagram>
* ```
*/
('chord-diagram')
export class ChordDiagram extends LitElement {
static styles = css`
:host {
display: block;
width: 100%;
min-width: 100px;
max-width: 150px;
border: 1px solid #4a5568;
border-radius: 4px;
background: #2d3748;
padding: 0.5rem;
box-sizing: border-box;
}
.chord {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.chord span {
color: #f8f8f8;
font-size: 0.9rem;
font-weight: 500;
margin-bottom: 0.25rem;
text-align: center;
}
.diagram {
width: 100%;
display: flex;
justify-content: center;
}
.diagram :global(svg) {
max-width: 100%;
height: auto;
}
.error {
color: #fc8181;
font-size: 0.8rem;
text-align: center;
padding: 0.5rem;
}
`
/**
* The instrument to display the chord for
*/
({
type: String
})
instrument = 'Standard Ukulele';
/**
* The chord name to display
*/
({
type: String
})
chord = '';
('.diagram')
container?: HTMLElement;
()
private chordData: Record<string, InstrumentDefault> = {};
()
private isLoading = false;
()
private loadError: string | null = null;
async connectedCallback() {
super.connectedCallback();
await this.loadChordData();
}
async updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
// Reload chord data if instrument changes
if (changedProperties.has('instrument')) {
await this.loadChordData();
}
}
private async loadChordData() {
this.isLoading = true;
this.loadError = null;
try {
const result = await chordDataService.getChordData(this.instrument);
this.chordData = result.data;
} catch (error) {
console.error('Failed to load chord data:', error);
this.loadError = 'Failed to load chord data';
this.chordData = {};
} finally {
this.isLoading = false;
}
}
render() {
if (this.isLoading) {
return html`
<div class='chord'>
<div style="color: #90cdf4; font-size: 0.8rem; text-align: center; padding: 0.5rem;">
Loading...
</div>
</div>
`;
}
if (this.loadError) {
return html`
<div class='chord'>
<div class='error'>${this.loadError}</div>
</div>
`;
}
if (!this.chord) {
return html`
<div class='chord'>
<div class='error'>No chord specified</div>
</div>
`;
}
const instrumentObject = instruments.find(({name}) => name === this.instrument);
if (!instrumentObject) {
return html`
<div class='chord'>
<span>${this.chord.replace(/(maj)$/, '')}</span>
<div class='error'>Unknown instrument: ${this.instrument}</div>
</div>
`;
}
const chordFinder = chordOnInstrument(instrumentObject);
// Given the chord name (G7, Bbmin), we need the notes in the chord
const chordObject = chordToNotes(this.chord);
if (!chordObject || !chordObject.notes || chordObject.notes.length === 0) {
return html`
<div class='chord'>
<span>${this.chord.replace(/(maj)$/, '')}</span>
<div class='error'>Unknown chord: ${this.chord}</div>
</div>
`;
}
// Check if we have a default for this chord/instrument combination
const chartSettings = this.chordData[this.chord] ?
this.chordData[this.chord] :
{
barres: [],
fingers: chordFinder(chordObject) || []
};
// Auto-calculate position based on chord data (not stored with chord)
const arrayOfFrets: number[] = chartSettings.fingers.map(([, fret]): number =>
typeof fret === 'number' ? fret : Infinity
);
const barreFrets = chartSettings.barres.map((b: any) => typeof b.fret === 'number' ? b.fret : 0);
const allChordFrets = [...arrayOfFrets, ...barreFrets];
const minChordFret = allChordFrets.length > 0 ? Math.min(...allChordFrets.filter(f => f > 0)) : 1;
const maxChordFret = allChordFrets.length > 0 ? Math.max(...allChordFrets, 0) : 4;
let position = 1;
if (maxChordFret > 4) {
// For high chords, start from the lowest fret
position = Math.max(1, minChordFret);
}
// Determine fret range to display
let fretCount: number;
let displayPosition: number;
if (position > 1 || maxChordFret > 4) {
// High position chord - show from position
fretCount = Math.max(maxChordFret - position + 1, 4);
displayPosition = position;
} else {
// Low position chord - show from fret 1
fretCount = Math.max(maxChordFret, 4);
displayPosition = 1;
}
// Create a container div for SVGuitar
const divEl = document.createElement("div");
try {
const chart = new SVGuitarChord(divEl);
chart
.configure({
strings: instrumentObject.strings.length,
frets: fretCount,
position: displayPosition,
tuning: [...instrumentObject.strings]
})
.chord({
fingers: chartSettings.fingers,
barres: chartSettings.barres
})
.draw();
return html`
<div class='chord'>
<span>${this.chord.replace(/(maj)$/, '')}</span>
<div class='diagram'>${divEl.firstChild}</div>
</div>
`;
} catch (error) {
console.error('Error generating chord diagram:', error);
return html`
<div class='chord'>
<span>${this.chord.replace(/(maj)$/, '')}</span>
<div class='error'>Error generating diagram</div>
</div>
`;
}
}
}