@gblp/chord-finder 0.1.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.
package/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # @gblp/chord-finder
2
+
3
+ Standalone Angular 22 component for finding guitar chords and rendering SVG chord diagrams.
4
+
5
+ ```ts
6
+ import { Component } from '@angular/core';
7
+ import { ChordFinderComponent } from '@gblp/chord-finder';
8
+
9
+ @Component({
10
+ imports: [ChordFinderComponent],
11
+ template: '<the-chords-chord-finder />'
12
+ })
13
+ export class App {}
14
+ ```
15
+
16
+ Build the package with `npm run build:lib`. The publishable Angular package is written to `dist/chord-finder`.
@@ -0,0 +1,369 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, computed, ChangeDetectionStrategy, Component, Injectable, signal, viewChild } from '@angular/core';
3
+ import * as i2 from '@angular/forms';
4
+ import { FormsModule } from '@angular/forms';
5
+ import guitarDbJson from '@tombatossals/chords-db/lib/guitar.json';
6
+
7
+ class ChordDiagram {
8
+ title = input.required(/* @ts-ignore */
9
+ ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
10
+ position = input.required(/* @ts-ignore */
11
+ ...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
12
+ positionIndex = input(0, /* @ts-ignore */
13
+ ...(ngDevMode ? [{ debugName: "positionIndex" }] : /* istanbul ignore next */ []));
14
+ width = 240;
15
+ height = 330;
16
+ left = 54;
17
+ top = 112;
18
+ stringGap = 28;
19
+ fretGap = 34;
20
+ dotRadius = 13;
21
+ stringLabels = ['E', 'A', 'D', 'G', 'B', 'E'];
22
+ fretCount = computed(() => Math.max(5, ...this.position().frets.filter((fret) => fret > 0)), /* @ts-ignore */
23
+ ...(ngDevMode ? [{ debugName: "fretCount" }] : /* istanbul ignore next */ []));
24
+ fretLines = computed(() => Array.from({ length: this.fretCount() + 1 }, (_, index) => index), /* @ts-ignore */
25
+ ...(ngDevMode ? [{ debugName: "fretLines" }] : /* istanbul ignore next */ []));
26
+ stringLines = computed(() => Array.from({ length: 6 }, (_, index) => index), /* @ts-ignore */
27
+ ...(ngDevMode ? [{ debugName: "stringLines" }] : /* istanbul ignore next */ []));
28
+ visibleDots = computed(() => {
29
+ const barres = new Set(this.position().barres ?? []);
30
+ const fingers = this.position().fingers ?? [];
31
+ return this.position().frets
32
+ .map((fret, stringIndex) => ({
33
+ fret,
34
+ stringIndex,
35
+ finger: fingers[stringIndex] > 0 ? fingers[stringIndex] : undefined
36
+ }))
37
+ .filter(({ fret }) => fret > 0)
38
+ .filter(({ fret }) => !barres.has(fret));
39
+ }, /* @ts-ignore */
40
+ ...(ngDevMode ? [{ debugName: "visibleDots" }] : /* istanbul ignore next */ []));
41
+ barreSpans = computed(() => {
42
+ const barres = this.position().barres ?? [];
43
+ const fingers = this.position().fingers ?? [];
44
+ return barres
45
+ .map((barreFret) => {
46
+ const playableStrings = this.position().frets
47
+ .map((fret, index) => ({ fret, index }))
48
+ .filter(({ fret }) => fret >= barreFret);
49
+ if (playableStrings.length < 2)
50
+ return null;
51
+ const firstString = playableStrings[0].index;
52
+ const lastString = playableStrings[playableStrings.length - 1].index;
53
+ const x = this.stringX(firstString) - this.dotRadius;
54
+ const y = this.dotY(barreFret) - this.dotRadius;
55
+ const width = this.stringX(lastString) - this.stringX(firstString) + this.dotRadius * 2;
56
+ const labelX = x + width / 2;
57
+ const labelY = y + this.dotRadius;
58
+ const finger = fingers[firstString] > 0 ? fingers[firstString] : undefined;
59
+ return {
60
+ fret: barreFret,
61
+ x,
62
+ y,
63
+ width,
64
+ labelX,
65
+ labelY,
66
+ finger
67
+ };
68
+ })
69
+ .filter((span) => span !== null);
70
+ }, /* @ts-ignore */
71
+ ...(ngDevMode ? [{ debugName: "barreSpans" }] : /* istanbul ignore next */ []));
72
+ get viewBox() {
73
+ return `0 0 ${this.width} ${this.height}`;
74
+ }
75
+ baseFret() {
76
+ return this.position().baseFret ?? 1;
77
+ }
78
+ stringX(index) {
79
+ return this.left + index * this.stringGap;
80
+ }
81
+ fretY(index) {
82
+ return this.top + index * this.fretGap;
83
+ }
84
+ dotY(fret) {
85
+ return this.top + (fret - 0.5) * this.fretGap;
86
+ }
87
+ markerFor(fret) {
88
+ if (fret === -1)
89
+ return '×';
90
+ if (fret === 0)
91
+ return '○';
92
+ return '';
93
+ }
94
+ fretLabel() {
95
+ return String(this.baseFret());
96
+ }
97
+ positionCode() {
98
+ return this.position().frets.map((fret) => (fret === -1 ? 'x' : String(fret))).join('');
99
+ }
100
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: ChordDiagram, deps: [], target: i0.ɵɵFactoryTarget.Component });
101
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.4", type: ChordDiagram, isStandalone: true, selector: "app-chord-diagram", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: true, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: true, transformFunction: null }, positionIndex: { classPropertyName: "positionIndex", publicName: "positionIndex", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<svg class=\"chord-svg\" [attr.viewBox]=\"viewBox\" role=\"img\" [attr.aria-label]=\"title() + ' guitar chord diagram'\">\n <title>{{ title() }} - posici\u00F3n {{ positionIndex() + 1 }} - {{ positionCode() }}</title>\n\n <rect class=\"card-bg\" x=\"0\" y=\"0\" [attr.width]=\"width\" [attr.height]=\"height\" rx=\"24\" fill=\"#ffffff\" />\n\n <text x=\"120\" y=\"50\" text-anchor=\"middle\" class=\"title\">{{ title() }}</text>\n\n <g class=\"markers\">\n @for (fret of position().frets; track $index) {\n <text [attr.x]=\"stringX($index)\" y=\"92\" text-anchor=\"middle\">{{ markerFor(fret) }}</text>\n }\n </g>\n\n <text x=\"26\" [attr.y]=\"fretY(0) + 28\" text-anchor=\"middle\" class=\"fret-number\">{{ fretLabel() }}</text>\n\n <g class=\"grid\">\n @for (line of fretLines(); track line) {\n <line\n [attr.x1]=\"stringX(0)\"\n [attr.x2]=\"stringX(5)\"\n [attr.y1]=\"fretY(line)\"\n [attr.y2]=\"fretY(line)\"\n [class.nut]=\"line === 0 && baseFret() === 1\"\n />\n }\n\n @for (line of stringLines(); track line) {\n <line\n [attr.x1]=\"stringX(line)\"\n [attr.x2]=\"stringX(line)\"\n [attr.y1]=\"fretY(0)\"\n [attr.y2]=\"fretY(fretCount())\"\n />\n }\n </g>\n\n <g class=\"barres\">\n @for (barre of barreSpans(); track barre.fret) {\n <rect\n [attr.x]=\"barre.x\"\n [attr.y]=\"barre.y\"\n [attr.width]=\"barre.width\"\n [attr.height]=\"dotRadius * 2\"\n rx=\"13\"\n />\n @if (barre.finger) {\n <text [attr.x]=\"barre.labelX\" [attr.y]=\"barre.labelY\" text-anchor=\"middle\">{{ barre.finger }}</text>\n }\n }\n </g>\n\n <g class=\"dots\">\n @for (dot of visibleDots(); track dot.stringIndex) {\n <circle [attr.cx]=\"stringX(dot.stringIndex)\" [attr.cy]=\"dotY(dot.fret)\" [attr.r]=\"dotRadius\" />\n @if (dot.finger) {\n <text [attr.x]=\"stringX(dot.stringIndex)\" [attr.y]=\"dotY(dot.fret)\" text-anchor=\"middle\">{{ dot.finger }}</text>\n }\n }\n </g>\n\n <g class=\"string-labels\">\n @for (label of stringLabels; track label + $index) {\n <text [attr.x]=\"stringX($index)\" [attr.y]=\"fretY(fretCount()) + 32\" text-anchor=\"middle\">{{ label }}</text>\n }\n </g>\n</svg>\n", styles: [":host{display:block}.chord-svg{width:240px;height:330px;display:block;font-family:Roboto,Arial,sans-serif;font-weight:400;border-radius:24px;background:#fff;box-shadow:0 18px 36px #00000059}.card-bg{fill:#fff}.title{font-size:42px;font-weight:400;letter-spacing:-.03em;fill:#000}.grid line{stroke:#000;stroke-width:2.6;stroke-linecap:square;vector-effect:non-scaling-stroke}.grid line.nut{stroke-width:7}.barres rect,.dots circle{fill:#000}.barres text,.dots text{fill:#fff;font-size:16px;font-weight:400;dominant-baseline:central;alignment-baseline:middle;pointer-events:none}.markers text{fill:#000;font-size:30px;font-weight:400}.fret-number{fill:#000;font-size:42px;font-weight:400}.string-labels text{fill:#000;font-size:18px;font-weight:400}\n"], changeDetection: i0.ChangeDetectionStrategy.Eager });
102
+ }
103
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: ChordDiagram, decorators: [{
104
+ type: Component,
105
+ args: [{ selector: 'app-chord-diagram', imports: [], changeDetection: ChangeDetectionStrategy.Eager, template: "<svg class=\"chord-svg\" [attr.viewBox]=\"viewBox\" role=\"img\" [attr.aria-label]=\"title() + ' guitar chord diagram'\">\n <title>{{ title() }} - posici\u00F3n {{ positionIndex() + 1 }} - {{ positionCode() }}</title>\n\n <rect class=\"card-bg\" x=\"0\" y=\"0\" [attr.width]=\"width\" [attr.height]=\"height\" rx=\"24\" fill=\"#ffffff\" />\n\n <text x=\"120\" y=\"50\" text-anchor=\"middle\" class=\"title\">{{ title() }}</text>\n\n <g class=\"markers\">\n @for (fret of position().frets; track $index) {\n <text [attr.x]=\"stringX($index)\" y=\"92\" text-anchor=\"middle\">{{ markerFor(fret) }}</text>\n }\n </g>\n\n <text x=\"26\" [attr.y]=\"fretY(0) + 28\" text-anchor=\"middle\" class=\"fret-number\">{{ fretLabel() }}</text>\n\n <g class=\"grid\">\n @for (line of fretLines(); track line) {\n <line\n [attr.x1]=\"stringX(0)\"\n [attr.x2]=\"stringX(5)\"\n [attr.y1]=\"fretY(line)\"\n [attr.y2]=\"fretY(line)\"\n [class.nut]=\"line === 0 && baseFret() === 1\"\n />\n }\n\n @for (line of stringLines(); track line) {\n <line\n [attr.x1]=\"stringX(line)\"\n [attr.x2]=\"stringX(line)\"\n [attr.y1]=\"fretY(0)\"\n [attr.y2]=\"fretY(fretCount())\"\n />\n }\n </g>\n\n <g class=\"barres\">\n @for (barre of barreSpans(); track barre.fret) {\n <rect\n [attr.x]=\"barre.x\"\n [attr.y]=\"barre.y\"\n [attr.width]=\"barre.width\"\n [attr.height]=\"dotRadius * 2\"\n rx=\"13\"\n />\n @if (barre.finger) {\n <text [attr.x]=\"barre.labelX\" [attr.y]=\"barre.labelY\" text-anchor=\"middle\">{{ barre.finger }}</text>\n }\n }\n </g>\n\n <g class=\"dots\">\n @for (dot of visibleDots(); track dot.stringIndex) {\n <circle [attr.cx]=\"stringX(dot.stringIndex)\" [attr.cy]=\"dotY(dot.fret)\" [attr.r]=\"dotRadius\" />\n @if (dot.finger) {\n <text [attr.x]=\"stringX(dot.stringIndex)\" [attr.y]=\"dotY(dot.fret)\" text-anchor=\"middle\">{{ dot.finger }}</text>\n }\n }\n </g>\n\n <g class=\"string-labels\">\n @for (label of stringLabels; track label + $index) {\n <text [attr.x]=\"stringX($index)\" [attr.y]=\"fretY(fretCount()) + 32\" text-anchor=\"middle\">{{ label }}</text>\n }\n </g>\n</svg>\n", styles: [":host{display:block}.chord-svg{width:240px;height:330px;display:block;font-family:Roboto,Arial,sans-serif;font-weight:400;border-radius:24px;background:#fff;box-shadow:0 18px 36px #00000059}.card-bg{fill:#fff}.title{font-size:42px;font-weight:400;letter-spacing:-.03em;fill:#000}.grid line{stroke:#000;stroke-width:2.6;stroke-linecap:square;vector-effect:non-scaling-stroke}.grid line.nut{stroke-width:7}.barres rect,.dots circle{fill:#000}.barres text,.dots text{fill:#fff;font-size:16px;font-weight:400;dominant-baseline:central;alignment-baseline:middle;pointer-events:none}.markers text{fill:#000;font-size:30px;font-weight:400}.fret-number{fill:#000;font-size:42px;font-weight:400}.string-labels text{fill:#000;font-size:18px;font-weight:400}\n"] }]
106
+ }], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: true }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: true }] }], positionIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "positionIndex", required: false }] }] } });
107
+
108
+ class ChordService {
109
+ maxBatchSize = 5;
110
+ guitarDb = guitarDbJson;
111
+ dbRootMap = {
112
+ C: 'C',
113
+ 'C#': 'Csharp',
114
+ Db: 'Csharp',
115
+ D: 'D',
116
+ 'D#': 'Eb',
117
+ Eb: 'Eb',
118
+ E: 'E',
119
+ Fb: 'E',
120
+ 'E#': 'F',
121
+ F: 'F',
122
+ 'F#': 'Fsharp',
123
+ Gb: 'Fsharp',
124
+ G: 'G',
125
+ 'G#': 'Ab',
126
+ Ab: 'Ab',
127
+ A: 'A',
128
+ 'A#': 'Bb',
129
+ Bb: 'Bb',
130
+ B: 'B',
131
+ Cb: 'B',
132
+ 'B#': 'C'
133
+ };
134
+ suffixAliases = {
135
+ '': 'major',
136
+ M: 'major',
137
+ maj: 'major',
138
+ major: 'major',
139
+ m: 'minor',
140
+ min: 'minor',
141
+ '-': 'minor',
142
+ minor: 'minor',
143
+ Δ: 'maj7',
144
+ maj7: 'maj7',
145
+ M7: 'maj7',
146
+ m7: 'm7',
147
+ min7: 'm7',
148
+ dim: 'dim',
149
+ diminished: 'dim',
150
+ aug: 'aug',
151
+ augmented: 'aug',
152
+ sus: 'sus4',
153
+ sus2: 'sus2',
154
+ sus4: 'sus4',
155
+ add9: 'add9',
156
+ 4: 'sus4'
157
+ };
158
+ search(input) {
159
+ const tokens = input
160
+ .split(',')
161
+ .map((token) => token.trim())
162
+ .filter(Boolean);
163
+ const limitedTokens = tokens.slice(0, this.maxBatchSize);
164
+ return {
165
+ results: limitedTokens.map((token, index) => this.searchSingle(token, index)),
166
+ wasLimited: tokens.length > this.maxBatchSize
167
+ };
168
+ }
169
+ suffixes() {
170
+ return this.guitarDb.suffixes;
171
+ }
172
+ searchSingle(token, index) {
173
+ const parsed = this.parseChordName(token);
174
+ if (!parsed) {
175
+ return {
176
+ id: `${index}-${token}`,
177
+ raw: token,
178
+ displayName: token,
179
+ positions: [],
180
+ error: 'Nombre inválido. Prueba C, F#, C#m, Bb, Am7 o Dsus4.'
181
+ };
182
+ }
183
+ const chordFamily = this.guitarDb.chords[parsed.dbRoot];
184
+ if (!chordFamily) {
185
+ return {
186
+ id: `${index}-${parsed.displayName}`,
187
+ raw: token,
188
+ displayName: parsed.displayName,
189
+ positions: [],
190
+ error: 'Raíz no encontrada en chords-db.'
191
+ };
192
+ }
193
+ const chord = chordFamily.find((item) => item.suffix === parsed.suffix);
194
+ if (!chord) {
195
+ return {
196
+ id: `${index}-${parsed.displayName}`,
197
+ raw: token,
198
+ displayName: parsed.displayName,
199
+ dbName: `${parsed.dbRoot} ${parsed.suffix}`,
200
+ positions: [],
201
+ error: `El tipo "${parsed.suffix}" no existe para este acorde en la base actual.`
202
+ };
203
+ }
204
+ return {
205
+ id: `${index}-${parsed.dbRoot}-${parsed.suffix}`,
206
+ raw: token,
207
+ displayName: parsed.displayName,
208
+ dbName: `${chord.key} ${chord.suffix}`,
209
+ chord,
210
+ positions: chord.positions,
211
+ };
212
+ }
213
+ parseChordName(raw) {
214
+ const cleaned = raw
215
+ .replaceAll('♯', '#')
216
+ .replaceAll('♭', 'b')
217
+ .replace(/\s+/g, '');
218
+ const match = cleaned.match(/^([A-Ga-g])([#b]?)(.*)$/);
219
+ if (!match)
220
+ return null;
221
+ const letter = match[1].toUpperCase();
222
+ const accidental = match[2] ?? '';
223
+ const suffixRaw = match[3] ?? '';
224
+ const root = `${letter}${accidental}`;
225
+ const dbRoot = this.dbRootMap[root];
226
+ if (!dbRoot)
227
+ return null;
228
+ const suffix = this.normalizeSuffix(suffixRaw);
229
+ if (!suffix)
230
+ return null;
231
+ return {
232
+ raw,
233
+ root,
234
+ dbRoot,
235
+ displayName: `${root}${this.displaySuffix(suffix)}`,
236
+ suffix
237
+ };
238
+ }
239
+ normalizeSuffix(rawSuffix) {
240
+ const trimmed = rawSuffix.trim();
241
+ if (this.suffixAliases[trimmed] !== undefined) {
242
+ return this.suffixAliases[trimmed];
243
+ }
244
+ const lower = trimmed.toLowerCase();
245
+ if (this.suffixAliases[lower] !== undefined) {
246
+ return this.suffixAliases[lower];
247
+ }
248
+ if (this.guitarDb.suffixes.includes(trimmed)) {
249
+ return trimmed;
250
+ }
251
+ if (this.guitarDb.suffixes.includes(lower)) {
252
+ return lower;
253
+ }
254
+ return null;
255
+ }
256
+ displaySuffix(suffix) {
257
+ if (suffix === 'major')
258
+ return '';
259
+ if (suffix === 'minor')
260
+ return 'm';
261
+ return suffix;
262
+ }
263
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: ChordService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
264
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: ChordService, providedIn: 'root' });
265
+ }
266
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: ChordService, decorators: [{
267
+ type: Injectable,
268
+ args: [{ providedIn: 'root' }]
269
+ }] });
270
+
271
+ class ChordFinderComponent {
272
+ chordService;
273
+ query = 'C';
274
+ results = signal([], /* @ts-ignore */
275
+ ...(ngDevMode ? [{ debugName: "results" }] : /* istanbul ignore next */ []));
276
+ wasLimited = signal(false, /* @ts-ignore */
277
+ ...(ngDevMode ? [{ debugName: "wasLimited" }] : /* istanbul ignore next */ []));
278
+ selectedPositionIndex = {};
279
+ supportedSuffixes = computed(() => this.chordService.suffixes().slice(0, 18).join(', '), /* @ts-ignore */
280
+ ...(ngDevMode ? [{ debugName: "supportedSuffixes" }] : /* istanbul ignore next */ []));
281
+ resultsRow = viewChild.required('resultsRow');
282
+ constructor(chordService) {
283
+ this.chordService = chordService;
284
+ this.runSearch();
285
+ }
286
+ async exportPng() {
287
+ const svgs = Array.from(this.resultsRow().nativeElement.querySelectorAll('svg.chord-svg'));
288
+ if (!svgs.length)
289
+ return;
290
+ const padding = 32;
291
+ const gap = 24;
292
+ const width = 240;
293
+ const height = 330;
294
+ const scale = 2;
295
+ const canvas = document.createElement('canvas');
296
+ canvas.width = (padding * 2 + svgs.length * width + (svgs.length - 1) * gap) * scale;
297
+ canvas.height = (padding * 2 + height) * scale;
298
+ const ctx = canvas.getContext('2d');
299
+ if (!ctx)
300
+ return;
301
+ ctx.scale(scale, scale);
302
+ ctx.fillStyle = '#ffffff';
303
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
304
+ for (const [index, svg] of svgs.entries()) {
305
+ const clone = svg.cloneNode(true);
306
+ clone.insertAdjacentHTML('afterbegin', `<style>
307
+ .chord-svg{font-family:Roboto,Arial,sans-serif;font-weight:400}
308
+ .card-bg{fill:#fff}.title{font-size:42px;font-weight:400;fill:#000}
309
+ .grid line{stroke:#000;stroke-width:2.6;stroke-linecap:square}
310
+ .grid line.nut{stroke-width:7}.barres rect,.dots circle{fill:#000}
311
+ .barres text,.dots text{fill:#fff;font-size:16px;font-weight:400;dominant-baseline:central;alignment-baseline:middle}
312
+ .markers text{fill:#000;font-size:30px;font-weight:400}
313
+ .fret-number{fill:#000;font-size:42px;font-weight:400}
314
+ .string-labels text{fill:#000;font-size:18px;font-weight:400}
315
+ </style>`);
316
+ const url = URL.createObjectURL(new Blob([new XMLSerializer().serializeToString(clone)], { type: 'image/svg+xml' }));
317
+ const image = new Image();
318
+ image.src = url;
319
+ await image.decode();
320
+ ctx.drawImage(image, padding + index * (width + gap), padding, width, height);
321
+ URL.revokeObjectURL(url);
322
+ }
323
+ const link = document.createElement('a');
324
+ link.download = 'chords.png';
325
+ link.href = canvas.toDataURL('image/png');
326
+ link.click();
327
+ }
328
+ runSearch() {
329
+ const { results, wasLimited } = this.chordService.search(this.query);
330
+ this.results.set(results);
331
+ this.wasLimited.set(wasLimited);
332
+ for (const result of results) {
333
+ if (this.selectedPositionIndex[result.id] === undefined) {
334
+ this.selectedPositionIndex[result.id] = 0;
335
+ }
336
+ }
337
+ }
338
+ selectPosition(resultId, value) {
339
+ this.selectedPositionIndex[resultId] = Number(value);
340
+ }
341
+ selectedIndex(resultId) {
342
+ return this.selectedPositionIndex[resultId] ?? 0;
343
+ }
344
+ selectedPosition(result) {
345
+ if (!result.positions.length)
346
+ return null;
347
+ return result.positions[this.selectedPositionIndex[result.id] ?? 0] ?? result.positions[0];
348
+ }
349
+ trackByResultId(_, result) {
350
+ return result.id;
351
+ }
352
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: ChordFinderComponent, deps: [{ token: ChordService }], target: i0.ɵɵFactoryTarget.Component });
353
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.4", type: ChordFinderComponent, isStandalone: true, selector: "the-chords-chord-finder", viewQueries: [{ propertyName: "resultsRow", first: true, predicate: ["resultsRow"], descendants: true, isSignal: true }], ngImport: i0, template: "<main class=\"page-shell\">\n <section class=\"hero-card\">\n <p class=\"eyebrow\">Angular \u00B7 SVG \u00B7 chords-db</p>\n <h1>Chord Finder</h1>\n\n <p class=\"description\">\n Escribe hasta <strong>5 acordes</strong> separados por coma. Soporta sostenidos y bemoles:\n <strong>C, F#, C#m, Bb</strong>.\n </p>\n\n <div class=\"search-row\">\n <label for=\"chordInput\">Input de acordes</label>\n <input id=\"chordInput\" type=\"text\" [(ngModel)]=\"query\" (input)=\"runSearch()\" placeholder=\"C, F#, C#m, Bb\"\n autocomplete=\"off\" />\n <button type=\"button\" (click)=\"exportPng()\" [disabled]=\"!results().length\">\n Exportar PNG\n </button>\n </div>\n\n @if (wasLimited()) {\n <p class=\"warning\">Solo se renderizan los primeros 5 acordes por ahora.</p>\n }\n\n <p class=\"hint\">\n Tipos disponibles desde la base: {{ supportedSuffixes() }}...\n </p>\n </section>\n\n <section #resultsRow class=\"results-row\" aria-label=\"Chord results\">\n @for (result of results(); track trackByResultId($index, result)) {\n <article class=\"result-card\" [class.has-error]=\"result.error\">\n <header class=\"result-header\">\n <div>\n <p class=\"result-label\">Acorde</p>\n <h2>{{ result.displayName }}</h2>\n @if (result.dbName && result.raw !== result.displayName) {\n <p class=\"db-name\">Normalizado como {{ result.dbName }}</p>\n }\n </div>\n\n @if (result.positions.length > 1) {\n <label class=\"select-label\">\n Posici\u00F3n\n <select [ngModel]=\"selectedPositionIndex[result.id]\" (ngModelChange)=\"selectPosition(result.id, $event)\">\n @for (position of result.positions; track $index) {\n <option [value]=\"$index\">{{ $index + 1 }} de {{ result.positions.length }}</option>\n }\n </select>\n </label>\n }\n </header>\n\n @if (result.error) {\n <p class=\"error-text\">{{ result.error }}</p>\n } @else if (selectedPosition(result); as position) {\n <div class=\"svg-wrap\">\n <app-chord-diagram [title]=\"result.displayName\" [position]=\"position\"\n [positionIndex]=\"selectedIndex(result.id)\" />\n </div>\n }\n </article>\n }\n </section>\n\n <footer class=\"app-footer\">\n <p>\n Open source project:\n <a href=\"https://github.com/elparaquecosadeque/chord-generator\" target=\"_blank\" rel=\"noreferrer\">\n elparaquecosadeque/chord-generator\n </a>.\n Contributions are welcome.\n </p>\n <p>\n Built with AI assistance under supervision from the repository owner.\n </p>\n </footer>\n</main>\n", styles: [":host{box-sizing:border-box;display:block;min-height:100%;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}:host *,:host *:before,:host *:after{box-sizing:inherit}.page-shell{min-height:100%;padding:clamp(24px,5vw,56px);color:#f8f8ff;background:radial-gradient(circle at 10% 12%,rgba(0,255,255,.25),transparent 28%),radial-gradient(circle at 92% 4%,rgba(255,0,230,.25),transparent 24%),radial-gradient(circle at 50% 110%,rgba(255,242,0,.16),transparent 30%),linear-gradient(135deg,#020004,#050012 48%,#000)}.hero-card{max-width:1020px;padding:clamp(24px,4vw,38px);border:1px solid rgba(0,255,255,.8);border-radius:30px;background:#000000c7;box-shadow:0 0 30px #00ffff52,inset 0 0 26px #ff00e621}.eyebrow{margin:0 0 14px;color:#00fff0;font-size:13px;font-weight:900;letter-spacing:.22em;text-transform:uppercase}h1{margin:0;max-width:820px;font-size:clamp(44px,8vw,92px);line-height:.88;letter-spacing:-.06em;text-shadow:0 0 18px rgba(0,255,240,.8),0 0 32px rgba(255,0,230,.5)}.description,.hint{max-width:760px;margin:22px 0 0;color:#dedbff;font-size:18px;line-height:1.55}.description strong{color:#fe0}.search-row{display:grid;gap:10px;margin-top:28px}.search-row label,.select-label,.result-label{color:#ff2bd6;font-size:12px;font-weight:900;letter-spacing:.14em;text-transform:uppercase}.search-row input{width:min(760px,100%);padding:18px 20px;color:#fff;background:#000;border:2px solid #ff2bd6;border-radius:18px;font-size:clamp(22px,4vw,32px);font-weight:900;outline:none;box-shadow:0 0 18px #ff2bd694,inset 0 0 18px #00ffff1f}.search-row input:focus{border-color:#00fff0;box-shadow:0 0 24px #00fff0c7,inset 0 0 18px #ff2bd633}.search-row button{width:fit-content;padding:12px 18px;color:#000;background:#00fff0;border:0;border-radius:12px;font-weight:900;cursor:pointer}.search-row button:disabled{opacity:.45;cursor:not-allowed}.warning{margin:16px 0 0;color:#fe0;font-weight:900}.results-row{display:flex;flex-wrap:wrap;gap:32px;margin-top:34px;align-items:flex-start}.result-card{width:292px;padding:18px;border:1px solid rgba(0,255,240,.76);border-radius:30px;background:#000c;box-shadow:0 0 22px #00fff040,inset 0 0 18px #ff2bd61f}.result-card.has-error{border-color:#ff315a;box-shadow:0 0 22px #ff315a59}.result-header{display:grid;gap:12px;margin-bottom:16px}.result-label,.db-name{margin:0}.result-header h2{margin:4px 0 0;color:#fff;font-size:30px}.db-name{margin-top:6px;color:#00fff0;font-size:12px;font-weight:800}.select-label{display:grid;gap:8px}select{width:100%;padding:11px 12px;color:#fff;background:#050008;border:1px solid #ff2bd6;border-radius:12px;font-weight:800;outline:none}select:focus{border-color:#00fff0}.svg-wrap{display:grid;place-items:center;padding:0;background:transparent;border-radius:28px}.error-text{color:#ff7b95;font-weight:800;line-height:1.45}.app-footer{max-width:1020px;margin-top:40px;padding-top:20px;border-top:1px solid rgba(0,255,240,.42);color:#dedbff;font-size:14px;line-height:1.6}.app-footer p{margin:0}.app-footer p+p{margin-top:6px}.app-footer a{color:#00fff0;font-weight:900}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.SelectControlValueAccessor, selector: "select:not([multiple]):not([ngNoCva])[formControlName],select:not([multiple]):not([ngNoCva])[formControl],select:not([multiple]):not([ngNoCva])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: ChordDiagram, selector: "app-chord-diagram", inputs: ["title", "position", "positionIndex"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
354
+ }
355
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: ChordFinderComponent, decorators: [{
356
+ type: Component,
357
+ args: [{ selector: 'the-chords-chord-finder', standalone: true, imports: [FormsModule, ChordDiagram], changeDetection: ChangeDetectionStrategy.Eager, template: "<main class=\"page-shell\">\n <section class=\"hero-card\">\n <p class=\"eyebrow\">Angular \u00B7 SVG \u00B7 chords-db</p>\n <h1>Chord Finder</h1>\n\n <p class=\"description\">\n Escribe hasta <strong>5 acordes</strong> separados por coma. Soporta sostenidos y bemoles:\n <strong>C, F#, C#m, Bb</strong>.\n </p>\n\n <div class=\"search-row\">\n <label for=\"chordInput\">Input de acordes</label>\n <input id=\"chordInput\" type=\"text\" [(ngModel)]=\"query\" (input)=\"runSearch()\" placeholder=\"C, F#, C#m, Bb\"\n autocomplete=\"off\" />\n <button type=\"button\" (click)=\"exportPng()\" [disabled]=\"!results().length\">\n Exportar PNG\n </button>\n </div>\n\n @if (wasLimited()) {\n <p class=\"warning\">Solo se renderizan los primeros 5 acordes por ahora.</p>\n }\n\n <p class=\"hint\">\n Tipos disponibles desde la base: {{ supportedSuffixes() }}...\n </p>\n </section>\n\n <section #resultsRow class=\"results-row\" aria-label=\"Chord results\">\n @for (result of results(); track trackByResultId($index, result)) {\n <article class=\"result-card\" [class.has-error]=\"result.error\">\n <header class=\"result-header\">\n <div>\n <p class=\"result-label\">Acorde</p>\n <h2>{{ result.displayName }}</h2>\n @if (result.dbName && result.raw !== result.displayName) {\n <p class=\"db-name\">Normalizado como {{ result.dbName }}</p>\n }\n </div>\n\n @if (result.positions.length > 1) {\n <label class=\"select-label\">\n Posici\u00F3n\n <select [ngModel]=\"selectedPositionIndex[result.id]\" (ngModelChange)=\"selectPosition(result.id, $event)\">\n @for (position of result.positions; track $index) {\n <option [value]=\"$index\">{{ $index + 1 }} de {{ result.positions.length }}</option>\n }\n </select>\n </label>\n }\n </header>\n\n @if (result.error) {\n <p class=\"error-text\">{{ result.error }}</p>\n } @else if (selectedPosition(result); as position) {\n <div class=\"svg-wrap\">\n <app-chord-diagram [title]=\"result.displayName\" [position]=\"position\"\n [positionIndex]=\"selectedIndex(result.id)\" />\n </div>\n }\n </article>\n }\n </section>\n\n <footer class=\"app-footer\">\n <p>\n Open source project:\n <a href=\"https://github.com/elparaquecosadeque/chord-generator\" target=\"_blank\" rel=\"noreferrer\">\n elparaquecosadeque/chord-generator\n </a>.\n Contributions are welcome.\n </p>\n <p>\n Built with AI assistance under supervision from the repository owner.\n </p>\n </footer>\n</main>\n", styles: [":host{box-sizing:border-box;display:block;min-height:100%;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}:host *,:host *:before,:host *:after{box-sizing:inherit}.page-shell{min-height:100%;padding:clamp(24px,5vw,56px);color:#f8f8ff;background:radial-gradient(circle at 10% 12%,rgba(0,255,255,.25),transparent 28%),radial-gradient(circle at 92% 4%,rgba(255,0,230,.25),transparent 24%),radial-gradient(circle at 50% 110%,rgba(255,242,0,.16),transparent 30%),linear-gradient(135deg,#020004,#050012 48%,#000)}.hero-card{max-width:1020px;padding:clamp(24px,4vw,38px);border:1px solid rgba(0,255,255,.8);border-radius:30px;background:#000000c7;box-shadow:0 0 30px #00ffff52,inset 0 0 26px #ff00e621}.eyebrow{margin:0 0 14px;color:#00fff0;font-size:13px;font-weight:900;letter-spacing:.22em;text-transform:uppercase}h1{margin:0;max-width:820px;font-size:clamp(44px,8vw,92px);line-height:.88;letter-spacing:-.06em;text-shadow:0 0 18px rgba(0,255,240,.8),0 0 32px rgba(255,0,230,.5)}.description,.hint{max-width:760px;margin:22px 0 0;color:#dedbff;font-size:18px;line-height:1.55}.description strong{color:#fe0}.search-row{display:grid;gap:10px;margin-top:28px}.search-row label,.select-label,.result-label{color:#ff2bd6;font-size:12px;font-weight:900;letter-spacing:.14em;text-transform:uppercase}.search-row input{width:min(760px,100%);padding:18px 20px;color:#fff;background:#000;border:2px solid #ff2bd6;border-radius:18px;font-size:clamp(22px,4vw,32px);font-weight:900;outline:none;box-shadow:0 0 18px #ff2bd694,inset 0 0 18px #00ffff1f}.search-row input:focus{border-color:#00fff0;box-shadow:0 0 24px #00fff0c7,inset 0 0 18px #ff2bd633}.search-row button{width:fit-content;padding:12px 18px;color:#000;background:#00fff0;border:0;border-radius:12px;font-weight:900;cursor:pointer}.search-row button:disabled{opacity:.45;cursor:not-allowed}.warning{margin:16px 0 0;color:#fe0;font-weight:900}.results-row{display:flex;flex-wrap:wrap;gap:32px;margin-top:34px;align-items:flex-start}.result-card{width:292px;padding:18px;border:1px solid rgba(0,255,240,.76);border-radius:30px;background:#000c;box-shadow:0 0 22px #00fff040,inset 0 0 18px #ff2bd61f}.result-card.has-error{border-color:#ff315a;box-shadow:0 0 22px #ff315a59}.result-header{display:grid;gap:12px;margin-bottom:16px}.result-label,.db-name{margin:0}.result-header h2{margin:4px 0 0;color:#fff;font-size:30px}.db-name{margin-top:6px;color:#00fff0;font-size:12px;font-weight:800}.select-label{display:grid;gap:8px}select{width:100%;padding:11px 12px;color:#fff;background:#050008;border:1px solid #ff2bd6;border-radius:12px;font-weight:800;outline:none}select:focus{border-color:#00fff0}.svg-wrap{display:grid;place-items:center;padding:0;background:transparent;border-radius:28px}.error-text{color:#ff7b95;font-weight:800;line-height:1.45}.app-footer{max-width:1020px;margin-top:40px;padding-top:20px;border-top:1px solid rgba(0,255,240,.42);color:#dedbff;font-size:14px;line-height:1.6}.app-footer p{margin:0}.app-footer p+p{margin-top:6px}.app-footer a{color:#00fff0;font-weight:900}\n"] }]
358
+ }], ctorParameters: () => [{ type: ChordService }], propDecorators: { resultsRow: [{ type: i0.ViewChild, args: ['resultsRow', { isSignal: true }] }] } });
359
+
360
+ /*
361
+ * Public API Surface of chord-finder
362
+ */
363
+
364
+ /**
365
+ * Generated bundle index. Do not edit.
366
+ */
367
+
368
+ export { ChordFinderComponent };
369
+ //# sourceMappingURL=gblp-chord-finder.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gblp-chord-finder.mjs","sources":["../../../projects/chord-finder/src/lib/components/chord-diagram/chord-diagram.ts","../../../projects/chord-finder/src/lib/components/chord-diagram/chord-diagram.html","../../../projects/chord-finder/src/lib/services/chord.service.ts","../../../projects/chord-finder/src/lib/chord-finder.ts","../../../projects/chord-finder/src/lib/chord-finder.html","../../../projects/chord-finder/src/public-api.ts","../../../projects/chord-finder/src/gblp-chord-finder.ts"],"sourcesContent":["import { Component, computed, input, ChangeDetectionStrategy } from '@angular/core';\nimport { ChordsDbPosition } from '../../models/chord.model';\n\ninterface BarreSpan {\n fret: number;\n x: number;\n y: number;\n width: number;\n labelX: number;\n labelY: number;\n finger: number | undefined;\n}\n\ninterface DotMarker {\n fret: number;\n stringIndex: number;\n finger?: number;\n}\n\n@Component({\n selector: 'app-chord-diagram',\n imports: [],\n templateUrl: './chord-diagram.html',\n changeDetection: ChangeDetectionStrategy.Eager,\n styleUrl: './chord-diagram.scss'\n})\nexport class ChordDiagram {\n title = input.required<string>();\n position = input.required<ChordsDbPosition>();\n positionIndex = input<number>(0);\n\n readonly width = 240;\n readonly height = 330;\n readonly left = 54;\n readonly top = 112;\n readonly stringGap = 28;\n readonly fretGap = 34;\n readonly dotRadius = 13;\n readonly stringLabels = ['E', 'A', 'D', 'G', 'B', 'E'];\n\n fretCount = computed(() => Math.max(5, ...this.position().frets.filter((fret) => fret > 0)));\n\n fretLines = computed(() => Array.from({ length: this.fretCount() + 1 }, (_, index) => index));\n\n stringLines = computed(() => Array.from({ length: 6 }, (_, index) => index));\n\n visibleDots = computed<DotMarker[]>(() => {\n const barres = new Set(this.position().barres ?? []);\n const fingers = this.position().fingers ?? [];\n\n return this.position().frets\n .map((fret, stringIndex) => ({\n fret,\n stringIndex,\n finger: fingers[stringIndex] > 0 ? fingers[stringIndex] : undefined\n }))\n .filter(({ fret }) => fret > 0)\n .filter(({ fret }) => !barres.has(fret));\n });\n\n barreSpans = computed<BarreSpan[]>(() => {\n const barres = this.position().barres ?? [];\n const fingers = this.position().fingers ?? [];\n\n return barres\n .map((barreFret) => {\n const playableStrings = this.position().frets\n .map((fret, index) => ({ fret, index }))\n .filter(({ fret }) => fret >= barreFret);\n\n if (playableStrings.length < 2) return null;\n\n const firstString = playableStrings[0].index;\n const lastString = playableStrings[playableStrings.length - 1].index;\n const x = this.stringX(firstString) - this.dotRadius;\n const y = this.dotY(barreFret) - this.dotRadius;\n const width = this.stringX(lastString) - this.stringX(firstString) + this.dotRadius * 2;\n const labelX = x + width / 2;\n const labelY = y + this.dotRadius;\n const finger = fingers[firstString] > 0 ? fingers[firstString] : undefined;\n\n return {\n fret: barreFret,\n x,\n y,\n width,\n labelX,\n labelY,\n finger\n } satisfies BarreSpan;\n })\n .filter((span): span is BarreSpan => span !== null);\n });\n\n get viewBox(): string {\n return `0 0 ${this.width} ${this.height}`;\n }\n\n baseFret(): number {\n return this.position().baseFret ?? 1;\n }\n\n stringX(index: number): number {\n return this.left + index * this.stringGap;\n }\n\n fretY(index: number): number {\n return this.top + index * this.fretGap;\n }\n\n dotY(fret: number): number {\n return this.top + (fret - 0.5) * this.fretGap;\n }\n\n markerFor(fret: number): string {\n if (fret === -1) return '×';\n if (fret === 0) return '○';\n return '';\n }\n\n fretLabel(): string {\n return String(this.baseFret());\n }\n\n positionCode(): string {\n return this.position().frets.map((fret) => (fret === -1 ? 'x' : String(fret))).join('');\n }\n}\n","<svg class=\"chord-svg\" [attr.viewBox]=\"viewBox\" role=\"img\" [attr.aria-label]=\"title() + ' guitar chord diagram'\">\n <title>{{ title() }} - posición {{ positionIndex() + 1 }} - {{ positionCode() }}</title>\n\n <rect class=\"card-bg\" x=\"0\" y=\"0\" [attr.width]=\"width\" [attr.height]=\"height\" rx=\"24\" fill=\"#ffffff\" />\n\n <text x=\"120\" y=\"50\" text-anchor=\"middle\" class=\"title\">{{ title() }}</text>\n\n <g class=\"markers\">\n @for (fret of position().frets; track $index) {\n <text [attr.x]=\"stringX($index)\" y=\"92\" text-anchor=\"middle\">{{ markerFor(fret) }}</text>\n }\n </g>\n\n <text x=\"26\" [attr.y]=\"fretY(0) + 28\" text-anchor=\"middle\" class=\"fret-number\">{{ fretLabel() }}</text>\n\n <g class=\"grid\">\n @for (line of fretLines(); track line) {\n <line\n [attr.x1]=\"stringX(0)\"\n [attr.x2]=\"stringX(5)\"\n [attr.y1]=\"fretY(line)\"\n [attr.y2]=\"fretY(line)\"\n [class.nut]=\"line === 0 && baseFret() === 1\"\n />\n }\n\n @for (line of stringLines(); track line) {\n <line\n [attr.x1]=\"stringX(line)\"\n [attr.x2]=\"stringX(line)\"\n [attr.y1]=\"fretY(0)\"\n [attr.y2]=\"fretY(fretCount())\"\n />\n }\n </g>\n\n <g class=\"barres\">\n @for (barre of barreSpans(); track barre.fret) {\n <rect\n [attr.x]=\"barre.x\"\n [attr.y]=\"barre.y\"\n [attr.width]=\"barre.width\"\n [attr.height]=\"dotRadius * 2\"\n rx=\"13\"\n />\n @if (barre.finger) {\n <text [attr.x]=\"barre.labelX\" [attr.y]=\"barre.labelY\" text-anchor=\"middle\">{{ barre.finger }}</text>\n }\n }\n </g>\n\n <g class=\"dots\">\n @for (dot of visibleDots(); track dot.stringIndex) {\n <circle [attr.cx]=\"stringX(dot.stringIndex)\" [attr.cy]=\"dotY(dot.fret)\" [attr.r]=\"dotRadius\" />\n @if (dot.finger) {\n <text [attr.x]=\"stringX(dot.stringIndex)\" [attr.y]=\"dotY(dot.fret)\" text-anchor=\"middle\">{{ dot.finger }}</text>\n }\n }\n </g>\n\n <g class=\"string-labels\">\n @for (label of stringLabels; track label + $index) {\n <text [attr.x]=\"stringX($index)\" [attr.y]=\"fretY(fretCount()) + 32\" text-anchor=\"middle\">{{ label }}</text>\n }\n </g>\n</svg>\n","import { Injectable } from '@angular/core';\nimport guitarDbJson from '@tombatossals/chords-db/lib/guitar.json';\nimport { ChordSearchResult, ChordsDbInstrument, ParsedChord } from '../models/chord.model';\n\n@Injectable({ providedIn: 'root' })\nexport class ChordService {\n readonly maxBatchSize = 5;\n\n private readonly guitarDb = guitarDbJson as ChordsDbInstrument;\n\n private readonly dbRootMap: Record<string, string> = {\n C: 'C',\n 'C#': 'Csharp',\n Db: 'Csharp',\n D: 'D',\n 'D#': 'Eb',\n Eb: 'Eb',\n E: 'E',\n Fb: 'E',\n 'E#': 'F',\n F: 'F',\n 'F#': 'Fsharp',\n Gb: 'Fsharp',\n G: 'G',\n 'G#': 'Ab',\n Ab: 'Ab',\n A: 'A',\n 'A#': 'Bb',\n Bb: 'Bb',\n B: 'B',\n Cb: 'B',\n 'B#': 'C'\n };\n\n private readonly suffixAliases: Record<string, string> = {\n '': 'major',\n M: 'major',\n maj: 'major',\n major: 'major',\n m: 'minor',\n min: 'minor',\n '-': 'minor',\n minor: 'minor',\n Δ: 'maj7',\n maj7: 'maj7',\n M7: 'maj7',\n m7: 'm7',\n min7: 'm7',\n dim: 'dim',\n diminished: 'dim',\n aug: 'aug',\n augmented: 'aug',\n sus: 'sus4',\n sus2: 'sus2',\n sus4: 'sus4',\n add9: 'add9',\n 4: 'sus4'\n };\n\n search(input: string): { results: ChordSearchResult[]; wasLimited: boolean } {\n const tokens = input\n .split(',')\n .map((token) => token.trim())\n .filter(Boolean);\n\n const limitedTokens = tokens.slice(0, this.maxBatchSize);\n\n return {\n results: limitedTokens.map((token, index) => this.searchSingle(token, index)),\n wasLimited: tokens.length > this.maxBatchSize\n };\n }\n\n suffixes(): string[] {\n return this.guitarDb.suffixes;\n }\n\n private searchSingle(token: string, index: number): ChordSearchResult {\n const parsed = this.parseChordName(token);\n\n if (!parsed) {\n return {\n id: `${index}-${token}`,\n raw: token,\n displayName: token,\n positions: [],\n error: 'Nombre inválido. Prueba C, F#, C#m, Bb, Am7 o Dsus4.'\n };\n }\n const chordFamily = this.guitarDb.chords[parsed.dbRoot];\n\n if (!chordFamily) {\n return {\n id: `${index}-${parsed.displayName}`,\n raw: token,\n displayName: parsed.displayName,\n positions: [],\n error: 'Raíz no encontrada en chords-db.'\n };\n }\n\n const chord = chordFamily.find((item) => item.suffix === parsed.suffix);\n\n if (!chord) {\n return {\n id: `${index}-${parsed.displayName}`,\n raw: token,\n displayName: parsed.displayName,\n dbName: `${parsed.dbRoot} ${parsed.suffix}`,\n positions: [],\n error: `El tipo \"${parsed.suffix}\" no existe para este acorde en la base actual.`\n };\n }\n\n return {\n id: `${index}-${parsed.dbRoot}-${parsed.suffix}`,\n raw: token,\n displayName: parsed.displayName,\n dbName: `${chord.key} ${chord.suffix}`,\n chord,\n positions: chord.positions,\n };\n }\n\n private parseChordName(raw: string): ParsedChord | null {\n const cleaned = raw\n .replaceAll('♯', '#')\n .replaceAll('♭', 'b')\n .replace(/\\s+/g, '');\n\n const match = cleaned.match(/^([A-Ga-g])([#b]?)(.*)$/);\n if (!match) return null;\n\n const letter = match[1].toUpperCase();\n const accidental = match[2] ?? '';\n const suffixRaw = match[3] ?? '';\n const root = `${letter}${accidental}`;\n const dbRoot = this.dbRootMap[root];\n\n if (!dbRoot) return null;\n\n const suffix = this.normalizeSuffix(suffixRaw);\n if (!suffix) return null;\n\n return {\n raw,\n root,\n dbRoot,\n displayName: `${root}${this.displaySuffix(suffix)}`,\n suffix\n };\n }\n\n private normalizeSuffix(rawSuffix: string): string | null {\n const trimmed = rawSuffix.trim();\n\n if (this.suffixAliases[trimmed] !== undefined) {\n return this.suffixAliases[trimmed];\n }\n\n const lower = trimmed.toLowerCase();\n if (this.suffixAliases[lower] !== undefined) {\n return this.suffixAliases[lower];\n }\n\n if (this.guitarDb.suffixes.includes(trimmed)) {\n return trimmed;\n }\n\n if (this.guitarDb.suffixes.includes(lower)) {\n return lower;\n }\n\n return null;\n }\n\n private displaySuffix(suffix: string): string {\n if (suffix === 'major') return '';\n if (suffix === 'minor') return 'm';\n return suffix;\n }\n}\n","import { Component, ElementRef, computed, signal, viewChild, ChangeDetectionStrategy } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { ChordDiagram } from './components/chord-diagram/chord-diagram';\nimport { ChordSearchResult, ChordsDbPosition } from './models/chord.model';\nimport { ChordService } from './services/chord.service';\n\n@Component({\n selector: 'the-chords-chord-finder',\n standalone: true,\n imports: [FormsModule, ChordDiagram],\n templateUrl: './chord-finder.html',\n changeDetection: ChangeDetectionStrategy.Eager,\n styleUrl: './chord-finder.scss'\n})\nexport class ChordFinderComponent {\n query = 'C';\n results = signal<ChordSearchResult[]>([]);\n wasLimited = signal(false);\n selectedPositionIndex: Record<string, number> = {};\n supportedSuffixes = computed(() => this.chordService.suffixes().slice(0, 18).join(', '));\n\n resultsRow = viewChild.required<ElementRef<HTMLElement>>('resultsRow');\n\n constructor(private readonly chordService: ChordService) {\n this.runSearch();\n }\n\n async exportPng(): Promise<void> {\n const svgs = Array.from(this.resultsRow().nativeElement.querySelectorAll<SVGSVGElement>('svg.chord-svg'));\n if (!svgs.length) return;\n\n const padding = 32;\n const gap = 24;\n const width = 240;\n const height = 330;\n const scale = 2;\n\n const canvas = document.createElement('canvas');\n canvas.width = (padding * 2 + svgs.length * width + (svgs.length - 1) * gap) * scale;\n canvas.height = (padding * 2 + height) * scale;\n\n const ctx = canvas.getContext('2d');\n if (!ctx) return;\n\n ctx.scale(scale, scale);\n ctx.fillStyle = '#ffffff';\n ctx.fillRect(0, 0, canvas.width, canvas.height);\n\n for (const [index, svg] of svgs.entries()) {\n const clone = svg.cloneNode(true) as SVGSVGElement;\n\n clone.insertAdjacentHTML('afterbegin', `<style>\n .chord-svg{font-family:Roboto,Arial,sans-serif;font-weight:400}\n .card-bg{fill:#fff}.title{font-size:42px;font-weight:400;fill:#000}\n .grid line{stroke:#000;stroke-width:2.6;stroke-linecap:square}\n .grid line.nut{stroke-width:7}.barres rect,.dots circle{fill:#000}\n .barres text,.dots text{fill:#fff;font-size:16px;font-weight:400;dominant-baseline:central;alignment-baseline:middle}\n .markers text{fill:#000;font-size:30px;font-weight:400}\n .fret-number{fill:#000;font-size:42px;font-weight:400}\n .string-labels text{fill:#000;font-size:18px;font-weight:400}\n </style>`);\n\n const url = URL.createObjectURL(new Blob([new XMLSerializer().serializeToString(clone)], { type: 'image/svg+xml' }));\n const image = new Image();\n image.src = url;\n await image.decode();\n\n ctx.drawImage(image, padding + index * (width + gap), padding, width, height);\n URL.revokeObjectURL(url);\n }\n\n const link = document.createElement('a');\n link.download = 'chords.png';\n link.href = canvas.toDataURL('image/png');\n link.click();\n }\n\n\n runSearch(): void {\n const { results, wasLimited } = this.chordService.search(this.query);\n this.results.set(results);\n this.wasLimited.set(wasLimited);\n\n for (const result of results) {\n if (this.selectedPositionIndex[result.id] === undefined) {\n this.selectedPositionIndex[result.id] = 0;\n }\n }\n }\n\n selectPosition(resultId: string, value: string | number): void {\n this.selectedPositionIndex[resultId] = Number(value);\n }\n\n selectedIndex(resultId: string): number {\n return this.selectedPositionIndex[resultId] ?? 0;\n }\n\n selectedPosition(result: ChordSearchResult): ChordsDbPosition | null {\n if (!result.positions.length) return null;\n return result.positions[this.selectedPositionIndex[result.id] ?? 0] ?? result.positions[0];\n }\n\n trackByResultId(_: number, result: ChordSearchResult): string {\n return result.id;\n }\n}\n","<main class=\"page-shell\">\n <section class=\"hero-card\">\n <p class=\"eyebrow\">Angular · SVG · chords-db</p>\n <h1>Chord Finder</h1>\n\n <p class=\"description\">\n Escribe hasta <strong>5 acordes</strong> separados por coma. Soporta sostenidos y bemoles:\n <strong>C, F#, C#m, Bb</strong>.\n </p>\n\n <div class=\"search-row\">\n <label for=\"chordInput\">Input de acordes</label>\n <input id=\"chordInput\" type=\"text\" [(ngModel)]=\"query\" (input)=\"runSearch()\" placeholder=\"C, F#, C#m, Bb\"\n autocomplete=\"off\" />\n <button type=\"button\" (click)=\"exportPng()\" [disabled]=\"!results().length\">\n Exportar PNG\n </button>\n </div>\n\n @if (wasLimited()) {\n <p class=\"warning\">Solo se renderizan los primeros 5 acordes por ahora.</p>\n }\n\n <p class=\"hint\">\n Tipos disponibles desde la base: {{ supportedSuffixes() }}...\n </p>\n </section>\n\n <section #resultsRow class=\"results-row\" aria-label=\"Chord results\">\n @for (result of results(); track trackByResultId($index, result)) {\n <article class=\"result-card\" [class.has-error]=\"result.error\">\n <header class=\"result-header\">\n <div>\n <p class=\"result-label\">Acorde</p>\n <h2>{{ result.displayName }}</h2>\n @if (result.dbName && result.raw !== result.displayName) {\n <p class=\"db-name\">Normalizado como {{ result.dbName }}</p>\n }\n </div>\n\n @if (result.positions.length > 1) {\n <label class=\"select-label\">\n Posición\n <select [ngModel]=\"selectedPositionIndex[result.id]\" (ngModelChange)=\"selectPosition(result.id, $event)\">\n @for (position of result.positions; track $index) {\n <option [value]=\"$index\">{{ $index + 1 }} de {{ result.positions.length }}</option>\n }\n </select>\n </label>\n }\n </header>\n\n @if (result.error) {\n <p class=\"error-text\">{{ result.error }}</p>\n } @else if (selectedPosition(result); as position) {\n <div class=\"svg-wrap\">\n <app-chord-diagram [title]=\"result.displayName\" [position]=\"position\"\n [positionIndex]=\"selectedIndex(result.id)\" />\n </div>\n }\n </article>\n }\n </section>\n\n <footer class=\"app-footer\">\n <p>\n Open source project:\n <a href=\"https://github.com/elparaquecosadeque/chord-generator\" target=\"_blank\" rel=\"noreferrer\">\n elparaquecosadeque/chord-generator\n </a>.\n Contributions are welcome.\n </p>\n <p>\n Built with AI assistance under supervision from the repository owner.\n </p>\n </footer>\n</main>\n","/*\r\n * Public API Surface of chord-finder\r\n */\r\n\r\nexport * from './lib/chord-finder';\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":["i1.ChordService"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@gblp/chord-finder",
3
+ "version": "0.1.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "peerDependencies": {
8
+ "@angular/common": "^22.0.0",
9
+ "@angular/core": "^22.0.0",
10
+ "@angular/forms": "^22.0.0"
11
+ },
12
+ "dependencies": {
13
+ "@tombatossals/chords-db": "0.5.1",
14
+ "tslib": "^2.8.1"
15
+ },
16
+ "sideEffects": false,
17
+ "module": "fesm2022/gblp-chord-finder.mjs",
18
+ "typings": "types/gblp-chord-finder.d.ts",
19
+ "exports": {
20
+ "./package.json": {
21
+ "default": "./package.json"
22
+ },
23
+ ".": {
24
+ "types": "./types/gblp-chord-finder.d.ts",
25
+ "default": "./fesm2022/gblp-chord-finder.mjs"
26
+ }
27
+ },
28
+ "type": "module"
29
+ }
@@ -0,0 +1,64 @@
1
+ import * as _angular_core from '@angular/core';
2
+ import { ElementRef } from '@angular/core';
3
+
4
+ interface ChordsDbPosition {
5
+ frets: number[];
6
+ fingers?: number[];
7
+ barres?: number[];
8
+ capo?: boolean;
9
+ baseFret?: number;
10
+ midi?: number[];
11
+ }
12
+ interface ChordsDbChord {
13
+ key: string;
14
+ suffix: string;
15
+ positions: ChordsDbPosition[];
16
+ }
17
+ interface ChordSearchResult {
18
+ id: string;
19
+ raw: string;
20
+ displayName: string;
21
+ dbName?: string;
22
+ chord?: ChordsDbChord;
23
+ positions: ChordsDbPosition[];
24
+ error?: string;
25
+ }
26
+
27
+ declare class ChordService {
28
+ readonly maxBatchSize = 5;
29
+ private readonly guitarDb;
30
+ private readonly dbRootMap;
31
+ private readonly suffixAliases;
32
+ search(input: string): {
33
+ results: ChordSearchResult[];
34
+ wasLimited: boolean;
35
+ };
36
+ suffixes(): string[];
37
+ private searchSingle;
38
+ private parseChordName;
39
+ private normalizeSuffix;
40
+ private displaySuffix;
41
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<ChordService, never>;
42
+ static ɵprov: _angular_core.ɵɵInjectableDeclaration<ChordService>;
43
+ }
44
+
45
+ declare class ChordFinderComponent {
46
+ private readonly chordService;
47
+ query: string;
48
+ results: _angular_core.WritableSignal<ChordSearchResult[]>;
49
+ wasLimited: _angular_core.WritableSignal<boolean>;
50
+ selectedPositionIndex: Record<string, number>;
51
+ supportedSuffixes: _angular_core.Signal<string>;
52
+ resultsRow: _angular_core.Signal<ElementRef<HTMLElement>>;
53
+ constructor(chordService: ChordService);
54
+ exportPng(): Promise<void>;
55
+ runSearch(): void;
56
+ selectPosition(resultId: string, value: string | number): void;
57
+ selectedIndex(resultId: string): number;
58
+ selectedPosition(result: ChordSearchResult): ChordsDbPosition | null;
59
+ trackByResultId(_: number, result: ChordSearchResult): string;
60
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<ChordFinderComponent, never>;
61
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<ChordFinderComponent, "the-chords-chord-finder", never, {}, {}, never, never, true, never>;
62
+ }
63
+
64
+ export { ChordFinderComponent };