@parent-tobias/chord-component 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Chord Data Service
3
+ *
4
+ * This service provides a unified interface for accessing chord data.
5
+ * It implements a caching strategy using IndexedDB:
6
+ * 1. First checks IndexedDB for cached data
7
+ * 2. Falls back to local default chord data if not found
8
+ * 3. Caches the data in IndexedDB for future use
9
+ * 4. Can be easily extended to fetch from a remote API
10
+ */
11
+
12
+ import { indexedDBService } from './indexed-db-service.js';
13
+ import { systemDefaultChords, type InstrumentDefault } from './default-chords.js';
14
+
15
+ export interface ChordDataSource {
16
+ type: 'indexeddb' | 'local' | 'api';
17
+ timestamp?: number;
18
+ }
19
+
20
+ export interface ChordDataResult {
21
+ data: Record<string, InstrumentDefault>;
22
+ source: ChordDataSource;
23
+ }
24
+
25
+ class ChordDataService {
26
+ private useRemoteAPI: boolean = false;
27
+ private apiEndpoint: string = '';
28
+
29
+ /**
30
+ * Configure the service to use a remote API endpoint
31
+ */
32
+ configureAPI(endpoint: string) {
33
+ this.apiEndpoint = endpoint;
34
+ this.useRemoteAPI = true;
35
+ }
36
+
37
+ /**
38
+ * Disable remote API and use local data
39
+ */
40
+ disableAPI() {
41
+ this.useRemoteAPI = false;
42
+ }
43
+
44
+ /**
45
+ * Get chord data for a specific instrument
46
+ * This method implements the fallback chain: IndexedDB -> API -> Local
47
+ */
48
+ async getChordData(instrument: string): Promise<ChordDataResult> {
49
+ // Step 1: Try to get from IndexedDB cache
50
+ try {
51
+ const cachedData = await indexedDBService.getChordData(instrument);
52
+
53
+ if (cachedData && cachedData.chords) {
54
+ console.log(`[ChordDataService] Loaded ${instrument} from IndexedDB cache`);
55
+ return {
56
+ data: cachedData.chords,
57
+ source: { type: 'indexeddb', timestamp: cachedData.timestamp }
58
+ };
59
+ }
60
+ } catch (error) {
61
+ console.warn('[ChordDataService] IndexedDB error, falling back:', error);
62
+ }
63
+
64
+ // Step 2: Try to fetch from remote API if configured
65
+ if (this.useRemoteAPI && this.apiEndpoint) {
66
+ try {
67
+ const apiData = await this.fetchFromAPI(instrument);
68
+
69
+ if (apiData) {
70
+ console.log(`[ChordDataService] Loaded ${instrument} from remote API`);
71
+
72
+ // Cache the API data in IndexedDB for future use
73
+ await this.cacheChordData(instrument, apiData);
74
+
75
+ return {
76
+ data: apiData,
77
+ source: { type: 'api', timestamp: Date.now() }
78
+ };
79
+ }
80
+ } catch (error) {
81
+ console.warn('[ChordDataService] API fetch error, falling back to local:', error);
82
+ }
83
+ }
84
+
85
+ // Step 3: Fall back to local default data
86
+ console.log(`[ChordDataService] Loaded ${instrument} from local defaults`);
87
+ const localData = systemDefaultChords[instrument] || {};
88
+
89
+ // Cache the local data in IndexedDB for future use
90
+ await this.cacheChordData(instrument, localData);
91
+
92
+ return {
93
+ data: localData,
94
+ source: { type: 'local', timestamp: Date.now() }
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Fetch chord data from remote API
100
+ * This is a placeholder that can be implemented when API is ready
101
+ */
102
+ private async fetchFromAPI(instrument: string): Promise<Record<string, InstrumentDefault> | null> {
103
+ if (!this.apiEndpoint) {
104
+ return null;
105
+ }
106
+
107
+ try {
108
+ const url = `${this.apiEndpoint}?instrument=${encodeURIComponent(instrument)}`;
109
+ const response = await fetch(url);
110
+
111
+ if (!response.ok) {
112
+ throw new Error(`API request failed: ${response.status} ${response.statusText}`);
113
+ }
114
+
115
+ const data = await response.json();
116
+ return data.chords || data; // Flexible response format
117
+ } catch (error) {
118
+ console.error('[ChordDataService] API fetch error:', error);
119
+ return null;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Cache chord data in IndexedDB
125
+ */
126
+ private async cacheChordData(instrument: string, chords: Record<string, InstrumentDefault>): Promise<void> {
127
+ try {
128
+ await indexedDBService.setChordData(instrument, chords);
129
+ console.log(`[ChordDataService] Cached ${instrument} in IndexedDB`);
130
+ } catch (error) {
131
+ console.warn('[ChordDataService] Failed to cache in IndexedDB:', error);
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Get all available instruments from local defaults
137
+ * In the future, this could also query the API
138
+ */
139
+ getAvailableInstruments(): string[] {
140
+ return Object.keys(systemDefaultChords);
141
+ }
142
+
143
+ /**
144
+ * Clear all cached data from IndexedDB
145
+ */
146
+ async clearCache(): Promise<void> {
147
+ await indexedDBService.clearAll();
148
+ console.log('[ChordDataService] Cache cleared');
149
+ }
150
+
151
+ /**
152
+ * Force refresh data from source (API or local) and update cache
153
+ */
154
+ async refreshData(instrument: string): Promise<ChordDataResult> {
155
+ // If using API, fetch fresh data
156
+ if (this.useRemoteAPI && this.apiEndpoint) {
157
+ try {
158
+ const apiData = await this.fetchFromAPI(instrument);
159
+
160
+ if (apiData) {
161
+ await this.cacheChordData(instrument, apiData);
162
+ return {
163
+ data: apiData,
164
+ source: { type: 'api', timestamp: Date.now() }
165
+ };
166
+ }
167
+ } catch (error) {
168
+ console.warn('[ChordDataService] Refresh from API failed:', error);
169
+ }
170
+ }
171
+
172
+ // Fall back to local data
173
+ const localData = systemDefaultChords[instrument] || {};
174
+ await this.cacheChordData(instrument, localData);
175
+
176
+ return {
177
+ data: localData,
178
+ source: { type: 'local', timestamp: Date.now() }
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Check if a specific chord exists for an instrument
184
+ */
185
+ async hasChord(instrument: string, chordName: string): Promise<boolean> {
186
+ const result = await this.getChordData(instrument);
187
+ return chordName in result.data;
188
+ }
189
+
190
+ /**
191
+ * Get data for a specific chord
192
+ * @param instrument - The instrument name
193
+ * @param chordName - The chord name
194
+ * @param preferUser - If true, returns user override if it exists; if false, returns system default only
195
+ */
196
+ async getChord(instrument: string, chordName: string, preferUser: boolean = true): Promise<InstrumentDefault | null> {
197
+ // Check for user override first if preferUser is true
198
+ if (preferUser) {
199
+ try {
200
+ const userChord = await indexedDBService.getUserChord(instrument, chordName);
201
+ if (userChord) {
202
+ return {
203
+ fingers: userChord.fingers,
204
+ barres: userChord.barres,
205
+ position: userChord.position
206
+ };
207
+ }
208
+ } catch (error) {
209
+ console.warn('[ChordDataService] Failed to get user chord:', error);
210
+ }
211
+ }
212
+
213
+ // Fall back to system default
214
+ const result = await this.getChordData(instrument);
215
+ return result.data[chordName] || null;
216
+ }
217
+
218
+ /**
219
+ * Save a user-defined chord override
220
+ */
221
+ async saveUserChord(instrument: string, chordName: string, chordData: InstrumentDefault): Promise<void> {
222
+ await indexedDBService.saveUserChord(instrument, chordName, chordData);
223
+ console.log(`[ChordDataService] Saved user chord: ${instrument} - ${chordName}`);
224
+ }
225
+
226
+ /**
227
+ * Delete a user-defined chord override (revert to system default)
228
+ */
229
+ async deleteUserChord(instrument: string, chordName: string): Promise<void> {
230
+ await indexedDBService.deleteUserChord(instrument, chordName);
231
+ console.log(`[ChordDataService] Deleted user chord: ${instrument} - ${chordName}`);
232
+ }
233
+
234
+ /**
235
+ * Get all user-defined chords for an instrument
236
+ */
237
+ async getUserChordsByInstrument(instrument: string): Promise<Array<{ chordName: string, data: InstrumentDefault }>> {
238
+ const userChords = await indexedDBService.getUserChordsByInstrument(instrument);
239
+ return userChords.map(chord => ({
240
+ chordName: chord.chordName,
241
+ data: {
242
+ fingers: chord.fingers,
243
+ barres: chord.barres,
244
+ position: chord.position
245
+ }
246
+ }));
247
+ }
248
+
249
+ /**
250
+ * Get all user-defined chords across all instruments
251
+ */
252
+ async getAllUserChords(): Promise<Array<{ instrument: string, chordName: string, data: InstrumentDefault }>> {
253
+ const userChords = await indexedDBService.getAllUserChords();
254
+ return userChords.map(chord => ({
255
+ instrument: chord.instrument,
256
+ chordName: chord.chordName,
257
+ data: {
258
+ fingers: chord.fingers,
259
+ barres: chord.barres,
260
+ position: chord.position
261
+ }
262
+ }));
263
+ }
264
+
265
+ /**
266
+ * Clear all user-defined chord overrides
267
+ */
268
+ async clearUserChords(): Promise<void> {
269
+ await indexedDBService.clearUserChords();
270
+ console.log('[ChordDataService] Cleared all user chords');
271
+ }
272
+ }
273
+
274
+ // Export a singleton instance
275
+ export const chordDataService = new ChordDataService();
@@ -0,0 +1,255 @@
1
+ import { LitElement, css, html } from 'lit';
2
+ import { customElement, property, query, state } from 'lit/decorators.js';
3
+ import { SVGuitarChord } from 'svguitar';
4
+
5
+ import { instruments, chordOnInstrument, chordToNotes } from './music-utils.js';
6
+ import { chordDataService } from './chord-data-service.js';
7
+ import type { InstrumentDefault } from './default-chords.js';
8
+
9
+ /**
10
+ * A web component that displays a chord diagram for various instruments.
11
+ *
12
+ * @element chord-diagram
13
+ *
14
+ * @attr {string} instrument - The instrument to display the chord for (default: 'Standard Ukulele')
15
+ * @attr {string} chord - The chord name to display (e.g., 'C', 'Am7', 'F#dim')
16
+ *
17
+ * @example
18
+ * ```html
19
+ * <chord-diagram chord="C" instrument="Standard Ukulele"></chord-diagram>
20
+ * <chord-diagram chord="Am7" instrument="Standard Guitar"></chord-diagram>
21
+ * ```
22
+ */
23
+ @customElement('chord-diagram')
24
+ export class ChordDiagram extends LitElement {
25
+
26
+ static styles = css`
27
+ :host {
28
+ display: block;
29
+ width: 100%;
30
+ min-width: 100px;
31
+ max-width: 150px;
32
+ border: 1px solid #4a5568;
33
+ border-radius: 4px;
34
+ background: #2d3748;
35
+ padding: 0.5rem;
36
+ box-sizing: border-box;
37
+ }
38
+
39
+ .chord {
40
+ display: flex;
41
+ flex-direction: column;
42
+ align-items: center;
43
+ width: 100%;
44
+ }
45
+
46
+ .chord span {
47
+ color: #f8f8f8;
48
+ font-size: 0.9rem;
49
+ font-weight: 500;
50
+ margin-bottom: 0.25rem;
51
+ text-align: center;
52
+ }
53
+
54
+ .diagram {
55
+ width: 100%;
56
+ display: flex;
57
+ justify-content: center;
58
+ }
59
+
60
+ .diagram :global(svg) {
61
+ max-width: 100%;
62
+ height: auto;
63
+ }
64
+
65
+ .error {
66
+ color: #fc8181;
67
+ font-size: 0.8rem;
68
+ text-align: center;
69
+ padding: 0.5rem;
70
+ }
71
+ `
72
+
73
+ /**
74
+ * The instrument to display the chord for
75
+ */
76
+ @property({
77
+ type: String
78
+ })
79
+ instrument = 'Standard Ukulele';
80
+
81
+ /**
82
+ * The chord name to display
83
+ */
84
+ @property({
85
+ type: String
86
+ })
87
+ chord = '';
88
+
89
+ @query('.diagram')
90
+ container?: HTMLElement;
91
+
92
+ @state()
93
+ private chordData: Record<string, InstrumentDefault> = {};
94
+
95
+ @state()
96
+ private isLoading = false;
97
+
98
+ @state()
99
+ private loadError: string | null = null;
100
+
101
+ async connectedCallback() {
102
+ super.connectedCallback();
103
+ await this.loadChordData();
104
+ }
105
+
106
+ async updated(changedProperties: Map<string, any>) {
107
+ super.updated(changedProperties);
108
+
109
+ // Reload chord data if instrument changes
110
+ if (changedProperties.has('instrument')) {
111
+ await this.loadChordData();
112
+ }
113
+ }
114
+
115
+ private async loadChordData() {
116
+ this.isLoading = true;
117
+ this.loadError = null;
118
+
119
+ try {
120
+ const result = await chordDataService.getChordData(this.instrument);
121
+ this.chordData = result.data;
122
+ } catch (error) {
123
+ console.error('Failed to load chord data:', error);
124
+ this.loadError = 'Failed to load chord data';
125
+ this.chordData = {};
126
+ } finally {
127
+ this.isLoading = false;
128
+ }
129
+ }
130
+
131
+ render() {
132
+ if (this.isLoading) {
133
+ return html`
134
+ <div class='chord'>
135
+ <div style="color: #90cdf4; font-size: 0.8rem; text-align: center; padding: 0.5rem;">
136
+ Loading...
137
+ </div>
138
+ </div>
139
+ `;
140
+ }
141
+
142
+ if (this.loadError) {
143
+ return html`
144
+ <div class='chord'>
145
+ <div class='error'>${this.loadError}</div>
146
+ </div>
147
+ `;
148
+ }
149
+
150
+ if (!this.chord) {
151
+ return html`
152
+ <div class='chord'>
153
+ <div class='error'>No chord specified</div>
154
+ </div>
155
+ `;
156
+ }
157
+
158
+ const instrumentObject = instruments.find(({name}) => name === this.instrument);
159
+
160
+ if (!instrumentObject) {
161
+ return html`
162
+ <div class='chord'>
163
+ <span>${this.chord.replace(/(maj)$/, '')}</span>
164
+ <div class='error'>Unknown instrument: ${this.instrument}</div>
165
+ </div>
166
+ `;
167
+ }
168
+
169
+ const chordFinder = chordOnInstrument(instrumentObject);
170
+
171
+ // Given the chord name (G7, Bbmin), we need the notes in the chord
172
+ const chordObject = chordToNotes(this.chord);
173
+
174
+ if (!chordObject || !chordObject.notes || chordObject.notes.length === 0) {
175
+ return html`
176
+ <div class='chord'>
177
+ <span>${this.chord.replace(/(maj)$/, '')}</span>
178
+ <div class='error'>Unknown chord: ${this.chord}</div>
179
+ </div>
180
+ `;
181
+ }
182
+
183
+ // Check if we have a default for this chord/instrument combination
184
+ const chartSettings = this.chordData[this.chord] ?
185
+ this.chordData[this.chord] :
186
+ {
187
+ barres: [],
188
+ fingers: chordFinder(chordObject) || []
189
+ };
190
+
191
+ // Auto-calculate position based on chord data (not stored with chord)
192
+ const arrayOfFrets: number[] = chartSettings.fingers.map(([, fret]): number =>
193
+ typeof fret === 'number' ? fret : Infinity
194
+ );
195
+ const barreFrets = chartSettings.barres.map((b: any) => typeof b.fret === 'number' ? b.fret : 0);
196
+ const allChordFrets = [...arrayOfFrets, ...barreFrets];
197
+
198
+ const minChordFret = allChordFrets.length > 0 ? Math.min(...allChordFrets.filter(f => f > 0)) : 1;
199
+ const maxChordFret = allChordFrets.length > 0 ? Math.max(...allChordFrets, 0) : 4;
200
+
201
+ let position = 1;
202
+ if (maxChordFret > 4) {
203
+ // For high chords, start from the lowest fret
204
+ position = Math.max(1, minChordFret);
205
+ }
206
+
207
+ // Determine fret range to display
208
+ let fretCount: number;
209
+ let displayPosition: number;
210
+
211
+ if (position > 1 || maxChordFret > 4) {
212
+ // High position chord - show from position
213
+ fretCount = Math.max(maxChordFret - position + 1, 4);
214
+ displayPosition = position;
215
+ } else {
216
+ // Low position chord - show from fret 1
217
+ fretCount = Math.max(maxChordFret, 4);
218
+ displayPosition = 1;
219
+ }
220
+
221
+ // Create a container div for SVGuitar
222
+ const divEl = document.createElement("div");
223
+
224
+ try {
225
+ const chart = new SVGuitarChord(divEl);
226
+ chart
227
+ .configure({
228
+ strings: instrumentObject.strings.length,
229
+ frets: fretCount,
230
+ position: displayPosition,
231
+ tuning: [...instrumentObject.strings]
232
+ })
233
+ .chord({
234
+ fingers: chartSettings.fingers,
235
+ barres: chartSettings.barres
236
+ })
237
+ .draw();
238
+
239
+ return html`
240
+ <div class='chord'>
241
+ <span>${this.chord.replace(/(maj)$/, '')}</span>
242
+ <div class='diagram'>${divEl.firstChild}</div>
243
+ </div>
244
+ `;
245
+ } catch (error) {
246
+ console.error('Error generating chord diagram:', error);
247
+ return html`
248
+ <div class='chord'>
249
+ <span>${this.chord.replace(/(maj)$/, '')}</span>
250
+ <div class='error'>Error generating diagram</div>
251
+ </div>
252
+ `;
253
+ }
254
+ }
255
+ }