@gblp/circle-of-fifths 0.3.2 → 0.4.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.
@@ -1,6 +1,274 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, computed, Component, signal, effect } from '@angular/core';
2
+ import { input, output, computed, Component, inject, signal, effect } from '@angular/core';
3
3
  import { NgClass } from '@angular/common';
4
+ import { ChordService, ChordDiagram } from '@gblp/chord-finder';
5
+
6
+ const KEYS = [
7
+ { index: 0, major: 'C', minor: 'Am', sharps: 0, flats: 0 },
8
+ { index: 1, major: 'G', minor: 'Em', sharps: 1, flats: 0 },
9
+ { index: 2, major: 'D', minor: 'Bm', sharps: 2, flats: 0 },
10
+ { index: 3, major: 'A', minor: 'F♯m', sharps: 3, flats: 0 },
11
+ { index: 4, major: 'E', minor: 'C♯m', sharps: 4, flats: 0 },
12
+ { index: 5, major: 'B', minor: 'G♯m', sharps: 5, flats: 0 },
13
+ { index: 6, major: 'F♯', minor: 'D♯m', sharps: 6, flats: 0 },
14
+ { index: 7, major: 'D♭', minor: 'B♭m', sharps: 0, flats: 5 },
15
+ { index: 8, major: 'A♭', minor: 'Fm', sharps: 0, flats: 4 },
16
+ { index: 9, major: 'E♭', minor: 'Cm', sharps: 0, flats: 3 },
17
+ { index: 10, major: 'B♭', minor: 'Gm', sharps: 0, flats: 2 },
18
+ { index: 11, major: 'F', minor: 'Dm', sharps: 0, flats: 1 },
19
+ ];
20
+ const COPY = {
21
+ en: {
22
+ title: 'Circle of Fifths',
23
+ subtitle: 'Click any major or minor key to highlight its diatonic chords',
24
+ ariaLabel: 'Circle of Fifths',
25
+ clickKey: 'Click a key',
26
+ seeChords: 'to see its chords',
27
+ deselect: '↩ click to deselect',
28
+ tonicLegend: 'Tonic (I / i)',
29
+ majorLegend: 'Major chords (IV, V)',
30
+ minorLegend: 'Minor chords (ii, iii, vi)',
31
+ diminishedLegend: 'Diminished (vii° / ii°)',
32
+ diatonicChords: 'Diatonic Chords',
33
+ commonProgressions: 'Common Progressions in',
34
+ generateProgressions: 'Shuffle',
35
+ chorus: 'Chorus',
36
+ bridge: 'Bridge',
37
+ outro: 'Outro',
38
+ verse: 'Verse',
39
+ copy: 'Copy',
40
+ expand: 'Expand',
41
+ collapse: 'Collapse',
42
+ transposeLabel: 'Transpose to:',
43
+ spiceLabels: ['🌶 Spice', '🌶🌶 Spicier', '🔥 Hot!', '↩ Plain'],
44
+ major: 'Major',
45
+ minor: 'Minor',
46
+ diminished: 'diminished',
47
+ relativeMinor: 'Relative minor',
48
+ relativeMajor: 'Relative major',
49
+ showChordDiagrams: 'Show chord diagrams',
50
+ hideChordDiagrams: 'Hide chord diagrams',
51
+ previousPosition: 'Previous position',
52
+ nextPosition: 'Next position',
53
+ previousPositionAll: 'Previous position (all diagrams)',
54
+ nextPositionAll: 'Next position (all diagrams)',
55
+ roles: {
56
+ tonic: 'Tonic',
57
+ supertonic: 'Supertonic',
58
+ mediant: 'Mediant',
59
+ subdominant: 'Subdominant',
60
+ dominant: 'Dominant',
61
+ submediant: 'Submediant',
62
+ leadingTone: 'Leading Tone',
63
+ subtonic: 'Subtonic',
64
+ },
65
+ },
66
+ es: {
67
+ title: 'Círculo de quintas',
68
+ subtitle: 'Haz clic en una tonalidad mayor o menor para resaltar sus acordes diatónicos',
69
+ ariaLabel: 'Círculo de quintas',
70
+ clickKey: 'Haz clic en una tonalidad',
71
+ seeChords: 'para ver sus acordes',
72
+ deselect: '↩ haz clic para deseleccionar',
73
+ tonicLegend: 'Tónica (I / i)',
74
+ majorLegend: 'Acordes mayores (IV, V)',
75
+ minorLegend: 'Acordes menores (ii, iii, vi)',
76
+ diminishedLegend: 'Disminuido (vii° / ii°)',
77
+ diatonicChords: 'Acordes diatónicos',
78
+ commonProgressions: 'Progresiones comunes en',
79
+ generateProgressions: 'Generar nuevos',
80
+ chorus: 'Coro',
81
+ bridge: 'Puente',
82
+ outro: 'Final',
83
+ verse: 'Estrofa',
84
+ copy: 'Copiar',
85
+ expand: 'Expandir',
86
+ collapse: 'Colapsar',
87
+ transposeLabel: 'Transponer a:',
88
+ spiceLabels: ['🌶 Condimentar', '🌶🌶 Más sabor', '🔥 ¡Picante!', '↩ Simple'],
89
+ major: 'Mayor',
90
+ minor: 'Menor',
91
+ diminished: 'disminuido',
92
+ relativeMinor: 'Relativa menor',
93
+ relativeMajor: 'Relativa mayor',
94
+ showChordDiagrams: 'Mostrar acordes de diagramas',
95
+ hideChordDiagrams: 'Ocultar acordes de diagramas',
96
+ previousPosition: 'Posición anterior',
97
+ nextPosition: 'Siguiente posición',
98
+ previousPositionAll: 'Posición anterior (todos)',
99
+ nextPositionAll: 'Siguiente posición (todos)',
100
+ roles: {
101
+ tonic: 'Tónica',
102
+ supertonic: 'Supertónica',
103
+ mediant: 'Mediante',
104
+ subdominant: 'Subdominante',
105
+ dominant: 'Dominante',
106
+ submediant: 'Submediante',
107
+ leadingTone: 'Sensible',
108
+ subtonic: 'Subtónica',
109
+ },
110
+ },
111
+ };
112
+
113
+ class CircleOfFifthsWheel {
114
+ language = input('en', /* @ts-ignore */
115
+ ...(ngDevMode ? [{ debugName: "language" }] : /* istanbul ignore next */ []));
116
+ selectedIndex = input(null, /* @ts-ignore */
117
+ ...(ngDevMode ? [{ debugName: "selectedIndex" }] : /* istanbul ignore next */ []));
118
+ selectedType = input(null, /* @ts-ignore */
119
+ ...(ngDevMode ? [{ debugName: "selectedType" }] : /* istanbul ignore next */ []));
120
+ interactive = input(true, /* @ts-ignore */
121
+ ...(ngDevMode ? [{ debugName: "interactive" }] : /* istanbul ignore next */ []));
122
+ keySelected = output();
123
+ text = computed(() => COPY[this.language()], /* @ts-ignore */
124
+ ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
125
+ keys = KEYS;
126
+ CX = 300;
127
+ CY = 300;
128
+ KEY_SIG = { inner: 250, outer: 288 };
129
+ MAJOR = { inner: 172, outer: 250 };
130
+ MINOR = { inner: 102, outer: 172 };
131
+ get slice() {
132
+ const idx = this.selectedIndex();
133
+ if (idx === null)
134
+ return null;
135
+ return {
136
+ prev: (idx - 1 + 12) % 12,
137
+ curr: idx,
138
+ next: (idx + 1) % 12,
139
+ };
140
+ }
141
+ onKeyClick(index, type) {
142
+ if (!this.interactive())
143
+ return;
144
+ this.keySelected.emit({ index, type });
145
+ }
146
+ getMajorState(index) {
147
+ const s = this.slice;
148
+ if (!s)
149
+ return '';
150
+ const type = this.selectedType();
151
+ if (index === s.curr)
152
+ return type === 'major' ? 'tonic' : 'scale-major';
153
+ if (index === s.prev || index === s.next)
154
+ return 'scale-major';
155
+ return '';
156
+ }
157
+ getMinorState(index) {
158
+ const s = this.slice;
159
+ if (!s)
160
+ return '';
161
+ const type = this.selectedType();
162
+ const dimIdx = (s.curr + 2) % 12;
163
+ if (index === s.curr)
164
+ return type === 'minor' ? 'tonic' : 'scale-minor';
165
+ if (index === s.prev || index === s.next)
166
+ return 'scale-minor';
167
+ if (index === dimIdx)
168
+ return 'scale-diminished';
169
+ return '';
170
+ }
171
+ getMajorRole(index) {
172
+ const s = this.slice;
173
+ if (!s)
174
+ return '';
175
+ const type = this.selectedType();
176
+ if (type === 'major') {
177
+ if (index === s.curr)
178
+ return 'I';
179
+ if (index === s.prev)
180
+ return 'IV';
181
+ if (index === s.next)
182
+ return 'V';
183
+ }
184
+ else {
185
+ if (index === s.curr)
186
+ return 'III';
187
+ if (index === s.prev)
188
+ return 'VI';
189
+ if (index === s.next)
190
+ return 'VII';
191
+ }
192
+ return '';
193
+ }
194
+ getMinorRole(index) {
195
+ const s = this.slice;
196
+ if (!s)
197
+ return '';
198
+ const type = this.selectedType();
199
+ const dimIdx = (s.curr + 2) % 12;
200
+ if (type === 'major') {
201
+ if (index === s.curr)
202
+ return 'vi';
203
+ if (index === s.prev)
204
+ return 'ii';
205
+ if (index === s.next)
206
+ return 'iii';
207
+ if (index === dimIdx)
208
+ return 'vii°';
209
+ }
210
+ else {
211
+ if (index === s.curr)
212
+ return 'i';
213
+ if (index === s.prev)
214
+ return 'iv';
215
+ if (index === s.next)
216
+ return 'v';
217
+ if (index === dimIdx)
218
+ return 'ii°';
219
+ }
220
+ return '';
221
+ }
222
+ arcPath(innerR, outerR, index, gap = 1.5) {
223
+ const mid = index * 30 - 90;
224
+ const s = (mid - 15 + gap) * (Math.PI / 180);
225
+ const e = (mid + 15 - gap) * (Math.PI / 180);
226
+ const cx = this.CX, cy = this.CY;
227
+ const x1 = cx + outerR * Math.cos(s), y1 = cy + outerR * Math.sin(s);
228
+ const x2 = cx + outerR * Math.cos(e), y2 = cy + outerR * Math.sin(e);
229
+ const x3 = cx + innerR * Math.cos(e), y3 = cy + innerR * Math.sin(e);
230
+ const x4 = cx + innerR * Math.cos(s), y4 = cy + innerR * Math.sin(s);
231
+ const f = (n) => n.toFixed(2);
232
+ return `M${f(x1)} ${f(y1)} A${outerR} ${outerR} 0 0 1 ${f(x2)} ${f(y2)} L${f(x3)} ${f(y3)} A${innerR} ${innerR} 0 0 0 ${f(x4)} ${f(y4)}Z`;
233
+ }
234
+ textPos(midR, index) {
235
+ const a = (index * 30 - 90) * (Math.PI / 180);
236
+ return {
237
+ x: this.CX + midR * Math.cos(a),
238
+ y: this.CY + midR * Math.sin(a),
239
+ };
240
+ }
241
+ getAccidentalText(key) {
242
+ if (key.sharps > 0)
243
+ return `${key.sharps}♯`;
244
+ if (key.flats > 0)
245
+ return `${key.flats}♭`;
246
+ return '—';
247
+ }
248
+ get selectedInfo() {
249
+ const idx = this.selectedIndex();
250
+ const type = this.selectedType();
251
+ if (idx === null || type === null)
252
+ return null;
253
+ const key = KEYS[idx];
254
+ const copy = this.text();
255
+ const scale = type === 'major' ? copy.major : copy.minor;
256
+ return {
257
+ name: type === 'major' ? key.major : key.minor,
258
+ scale,
259
+ fullName: `${type === 'major' ? key.major : key.minor} ${scale}`,
260
+ relativeKey: type === 'major'
261
+ ? `${copy.relativeMinor}: ${key.minor}`
262
+ : `${copy.relativeMajor}: ${key.major}`,
263
+ };
264
+ }
265
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: CircleOfFifthsWheel, deps: [], target: i0.ɵɵFactoryTarget.Component });
266
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: CircleOfFifthsWheel, isStandalone: true, selector: "circle-of-fifths-wheel", inputs: { language: { classPropertyName: "language", publicName: "language", isSignal: true, isRequired: false, transformFunction: null }, selectedIndex: { classPropertyName: "selectedIndex", publicName: "selectedIndex", isSignal: true, isRequired: false, transformFunction: null }, selectedType: { classPropertyName: "selectedType", publicName: "selectedType", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { keySelected: "keySelected" }, host: { properties: { "class.readonly": "!interactive()" } }, ngImport: i0, template: "<div class=\"circle-wrapper\">\n <svg\n viewBox=\"0 0 600 600\"\n xmlns=\"http://www.w3.org/2000/svg\"\n [attr.aria-label]=\"text().ariaLabel\"\n >\n <!-- \u2500\u2500 Key Signature Ring \u2500\u2500 -->\n @for (key of keys; track key.index) {\n <path [attr.d]=\"arcPath(KEY_SIG.inner, KEY_SIG.outer, key.index)\" class=\"keysig-segment\" />\n <text\n [attr.x]=\"textPos((KEY_SIG.inner + KEY_SIG.outer) / 2, key.index).x\"\n [attr.y]=\"textPos((KEY_SIG.inner + KEY_SIG.outer) / 2, key.index).y\"\n class=\"keysig-label\"\n >\n {{ getAccidentalText(key) }}\n </text>\n }\n\n <!-- \u2500\u2500 Major Keys Ring \u2500\u2500 -->\n @for (key of keys; track key.index) {\n <g\n [ngClass]=\"['major-group', getMajorState(key.index)]\"\n (click)=\"onKeyClick(key.index, 'major')\"\n >\n <path [attr.d]=\"arcPath(MAJOR.inner, MAJOR.outer, key.index)\" />\n\n <text\n [attr.x]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).x\"\n [attr.y]=\"\n textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).y +\n (getMajorRole(key.index) ? -9 : 0)\n \"\n class=\"major-label\"\n >\n {{ key.major }}\n </text>\n\n @if (getMajorRole(key.index)) {\n <text\n [attr.x]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).x\"\n [attr.y]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).y + 11\"\n class=\"role-label\"\n >\n {{ getMajorRole(key.index) }}\n </text>\n }\n </g>\n }\n\n <!-- \u2500\u2500 Minor Keys Ring \u2500\u2500 -->\n @for (key of keys; track key.index) {\n <g\n [ngClass]=\"['minor-group', getMinorState(key.index)]\"\n (click)=\"onKeyClick(key.index, 'minor')\"\n >\n <path [attr.d]=\"arcPath(MINOR.inner, MINOR.outer, key.index)\" />\n\n <text\n [attr.x]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).x\"\n [attr.y]=\"\n textPos((MINOR.inner + MINOR.outer) / 2, key.index).y +\n (getMinorRole(key.index) ? -8 : 0)\n \"\n class=\"minor-label\"\n >\n {{ key.minor }}\n </text>\n\n @if (getMinorRole(key.index)) {\n <text\n [attr.x]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).x\"\n [attr.y]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).y + 9\"\n class=\"role-label\"\n >\n {{ getMinorRole(key.index) }}\n </text>\n }\n </g>\n }\n\n <!-- \u2500\u2500 Outer border \u2500\u2500 -->\n <circle cx=\"300\" cy=\"300\" r=\"289\" class=\"outer-border\" />\n\n <!-- \u2500\u2500 Center Circle \u2500\u2500 -->\n <circle cx=\"300\" cy=\"300\" r=\"99\" class=\"center-circle\" />\n\n @if (selectedInfo) {\n <text x=\"300\" y=\"283\" class=\"center-name\">{{ selectedInfo.name }}</text>\n <text x=\"300\" y=\"304\" class=\"center-scale\">{{ selectedInfo.scale }}</text>\n <text x=\"300\" y=\"322\" class=\"center-relative\">{{ selectedInfo.relativeKey }}</text>\n @if (interactive()) {\n <text x=\"300\" y=\"338\" class=\"center-deselect\">{{ text().deselect }}</text>\n }\n } @else if (interactive()) {\n <text x=\"300\" y=\"293\" class=\"center-hint\">{{ text().clickKey }}</text>\n <text x=\"300\" y=\"312\" class=\"center-hint-sm\">{{ text().seeChords }}</text>\n }\n </svg>\n</div>\n\n<!-- \u2500\u2500 Legend \u2500\u2500 -->\n<div class=\"legend\">\n <span class=\"legend-item tonic\">{{ text().tonicLegend }}</span>\n <span class=\"legend-item scale-major\">{{ text().majorLegend }}</span>\n <span class=\"legend-item scale-minor\">{{ text().minorLegend }}</span>\n <span class=\"legend-item scale-diminished\">{{ text().diminishedLegend }}</span>\n</div>\n", styles: [":host{--_chords-background: var(--chords-background, #f0f4f8);--_chords-background-alt: var(--chords-background-alt, #f8fafc);--_chords-surface: var(--chords-surface, #ffffff);--_chords-surface-elevated: var(--chords-surface-elevated, #f8fafc);--_chords-text: var(--chords-text, #1e293b);--_chords-muted: var(--chords-muted, #64748b);--_chords-primary: var(--chords-primary, #3b82f6);--_chords-secondary: var(--chords-secondary, #06b6d4);--_chords-highlight: var(--chords-highlight, #f59e0b);--_chords-danger: var(--chords-danger, #a855f7);--_chords-border: var(--chords-border, #e2e8f0);--_chords-on-primary: var(--chords-on-primary, #ffffff);display:block;font-family:Segoe UI,system-ui,sans-serif}:host(.readonly) .major-group,:host(.readonly) .minor-group{cursor:default;pointer-events:none}.circle-wrapper{max-width:580px;margin:0 auto}.circle-wrapper svg{display:block;width:100%;height:auto;overflow:visible}.keysig-segment{fill:var(--_chords-text)}.keysig-label{fill:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background));font-size:12px;font-weight:500;text-anchor:middle;dominant-baseline:central;pointer-events:none}.major-group{cursor:pointer}.major-group path{fill:color-mix(in srgb,var(--_chords-primary) 14%,var(--_chords-surface));stroke:var(--_chords-surface);stroke-width:2;transition:fill .18s ease}.major-group:hover path{fill:color-mix(in srgb,var(--_chords-primary) 24%,var(--_chords-surface))}.major-group .major-label{fill:var(--_chords-text);font-size:15px;font-weight:700;text-anchor:middle;dominant-baseline:central;pointer-events:none;transition:fill .18s ease}.major-group.tonic path{fill:var(--_chords-highlight)}.major-group.tonic:hover path{fill:color-mix(in srgb,var(--_chords-highlight) 84%,var(--_chords-text))}.major-group.tonic .major-label{fill:var(--_chords-on-primary)}.major-group.scale-major path{fill:var(--_chords-primary)}.major-group.scale-major:hover path{fill:color-mix(in srgb,var(--_chords-primary) 82%,var(--_chords-text))}.major-group.scale-major .major-label{fill:var(--_chords-on-primary)}.minor-group{cursor:pointer}.minor-group path{fill:color-mix(in srgb,var(--_chords-secondary) 10%,var(--_chords-surface));stroke:var(--_chords-surface);stroke-width:2;transition:fill .18s ease}.minor-group:hover path{fill:color-mix(in srgb,var(--_chords-secondary) 20%,var(--_chords-surface))}.minor-group .minor-label{fill:color-mix(in srgb,var(--_chords-text) 78%,var(--_chords-muted));font-size:12px;font-weight:600;text-anchor:middle;dominant-baseline:central;pointer-events:none;transition:fill .18s ease}.minor-group.tonic path{fill:var(--_chords-highlight)}.minor-group.tonic:hover path{fill:color-mix(in srgb,var(--_chords-highlight) 84%,var(--_chords-text))}.minor-group.tonic .minor-label{fill:var(--_chords-on-primary)}.minor-group.scale-minor path{fill:var(--_chords-secondary)}.minor-group.scale-minor:hover path{fill:color-mix(in srgb,var(--_chords-secondary) 82%,var(--_chords-text))}.minor-group.scale-minor .minor-label{fill:var(--_chords-on-primary)}.minor-group.scale-diminished path{fill:var(--_chords-danger)}.minor-group.scale-diminished:hover path{fill:color-mix(in srgb,var(--_chords-danger) 82%,var(--_chords-text))}.minor-group.scale-diminished .minor-label{fill:var(--_chords-on-primary)}.role-label{font-size:10px;font-style:italic;font-weight:600;text-anchor:middle;dominant-baseline:central;pointer-events:none;fill:color-mix(in srgb,var(--_chords-on-primary) 85%,transparent)}.outer-border{fill:none;stroke:var(--_chords-text);stroke-width:2.5}.center-circle{fill:var(--_chords-surface);stroke:var(--_chords-border);stroke-width:2;filter:drop-shadow(0 2px 10px color-mix(in srgb,var(--_chords-text) 8%,transparent))}.center-name{font-size:30px;font-weight:800;text-anchor:middle;dominant-baseline:auto;fill:var(--_chords-text);letter-spacing:-.02em}.center-scale{font-size:13px;font-weight:600;text-anchor:middle;dominant-baseline:auto;fill:var(--_chords-muted);text-transform:uppercase;letter-spacing:.08em}.center-relative{font-size:9.5px;font-weight:500;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background))}.center-deselect{font-size:8px;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 42%,var(--_chords-background))}.center-hint{font-size:16px;font-weight:600;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background))}.center-hint-sm{font-size:11px;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 42%,var(--_chords-background))}.legend{display:flex;justify-content:center;gap:16px;flex-wrap:wrap;margin:16px 0 0}.legend .legend-item{display:flex;align-items:center;gap:6px;font-size:.8rem;color:var(--_chords-muted)}.legend .legend-item:before{content:\"\";display:inline-block;width:14px;height:14px;border-radius:3px}.legend .legend-item.tonic:before{background:var(--_chords-highlight)}.legend .legend-item.scale-major:before{background:var(--_chords-primary)}.legend .legend-item.scale-minor:before{background:var(--_chords-secondary)}.legend .legend-item.scale-diminished:before{background:var(--_chords-danger)}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] });
267
+ }
268
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: CircleOfFifthsWheel, decorators: [{
269
+ type: Component,
270
+ args: [{ selector: 'circle-of-fifths-wheel', standalone: true, imports: [NgClass], host: { '[class.readonly]': '!interactive()' }, template: "<div class=\"circle-wrapper\">\n <svg\n viewBox=\"0 0 600 600\"\n xmlns=\"http://www.w3.org/2000/svg\"\n [attr.aria-label]=\"text().ariaLabel\"\n >\n <!-- \u2500\u2500 Key Signature Ring \u2500\u2500 -->\n @for (key of keys; track key.index) {\n <path [attr.d]=\"arcPath(KEY_SIG.inner, KEY_SIG.outer, key.index)\" class=\"keysig-segment\" />\n <text\n [attr.x]=\"textPos((KEY_SIG.inner + KEY_SIG.outer) / 2, key.index).x\"\n [attr.y]=\"textPos((KEY_SIG.inner + KEY_SIG.outer) / 2, key.index).y\"\n class=\"keysig-label\"\n >\n {{ getAccidentalText(key) }}\n </text>\n }\n\n <!-- \u2500\u2500 Major Keys Ring \u2500\u2500 -->\n @for (key of keys; track key.index) {\n <g\n [ngClass]=\"['major-group', getMajorState(key.index)]\"\n (click)=\"onKeyClick(key.index, 'major')\"\n >\n <path [attr.d]=\"arcPath(MAJOR.inner, MAJOR.outer, key.index)\" />\n\n <text\n [attr.x]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).x\"\n [attr.y]=\"\n textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).y +\n (getMajorRole(key.index) ? -9 : 0)\n \"\n class=\"major-label\"\n >\n {{ key.major }}\n </text>\n\n @if (getMajorRole(key.index)) {\n <text\n [attr.x]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).x\"\n [attr.y]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).y + 11\"\n class=\"role-label\"\n >\n {{ getMajorRole(key.index) }}\n </text>\n }\n </g>\n }\n\n <!-- \u2500\u2500 Minor Keys Ring \u2500\u2500 -->\n @for (key of keys; track key.index) {\n <g\n [ngClass]=\"['minor-group', getMinorState(key.index)]\"\n (click)=\"onKeyClick(key.index, 'minor')\"\n >\n <path [attr.d]=\"arcPath(MINOR.inner, MINOR.outer, key.index)\" />\n\n <text\n [attr.x]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).x\"\n [attr.y]=\"\n textPos((MINOR.inner + MINOR.outer) / 2, key.index).y +\n (getMinorRole(key.index) ? -8 : 0)\n \"\n class=\"minor-label\"\n >\n {{ key.minor }}\n </text>\n\n @if (getMinorRole(key.index)) {\n <text\n [attr.x]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).x\"\n [attr.y]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).y + 9\"\n class=\"role-label\"\n >\n {{ getMinorRole(key.index) }}\n </text>\n }\n </g>\n }\n\n <!-- \u2500\u2500 Outer border \u2500\u2500 -->\n <circle cx=\"300\" cy=\"300\" r=\"289\" class=\"outer-border\" />\n\n <!-- \u2500\u2500 Center Circle \u2500\u2500 -->\n <circle cx=\"300\" cy=\"300\" r=\"99\" class=\"center-circle\" />\n\n @if (selectedInfo) {\n <text x=\"300\" y=\"283\" class=\"center-name\">{{ selectedInfo.name }}</text>\n <text x=\"300\" y=\"304\" class=\"center-scale\">{{ selectedInfo.scale }}</text>\n <text x=\"300\" y=\"322\" class=\"center-relative\">{{ selectedInfo.relativeKey }}</text>\n @if (interactive()) {\n <text x=\"300\" y=\"338\" class=\"center-deselect\">{{ text().deselect }}</text>\n }\n } @else if (interactive()) {\n <text x=\"300\" y=\"293\" class=\"center-hint\">{{ text().clickKey }}</text>\n <text x=\"300\" y=\"312\" class=\"center-hint-sm\">{{ text().seeChords }}</text>\n }\n </svg>\n</div>\n\n<!-- \u2500\u2500 Legend \u2500\u2500 -->\n<div class=\"legend\">\n <span class=\"legend-item tonic\">{{ text().tonicLegend }}</span>\n <span class=\"legend-item scale-major\">{{ text().majorLegend }}</span>\n <span class=\"legend-item scale-minor\">{{ text().minorLegend }}</span>\n <span class=\"legend-item scale-diminished\">{{ text().diminishedLegend }}</span>\n</div>\n", styles: [":host{--_chords-background: var(--chords-background, #f0f4f8);--_chords-background-alt: var(--chords-background-alt, #f8fafc);--_chords-surface: var(--chords-surface, #ffffff);--_chords-surface-elevated: var(--chords-surface-elevated, #f8fafc);--_chords-text: var(--chords-text, #1e293b);--_chords-muted: var(--chords-muted, #64748b);--_chords-primary: var(--chords-primary, #3b82f6);--_chords-secondary: var(--chords-secondary, #06b6d4);--_chords-highlight: var(--chords-highlight, #f59e0b);--_chords-danger: var(--chords-danger, #a855f7);--_chords-border: var(--chords-border, #e2e8f0);--_chords-on-primary: var(--chords-on-primary, #ffffff);display:block;font-family:Segoe UI,system-ui,sans-serif}:host(.readonly) .major-group,:host(.readonly) .minor-group{cursor:default;pointer-events:none}.circle-wrapper{max-width:580px;margin:0 auto}.circle-wrapper svg{display:block;width:100%;height:auto;overflow:visible}.keysig-segment{fill:var(--_chords-text)}.keysig-label{fill:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background));font-size:12px;font-weight:500;text-anchor:middle;dominant-baseline:central;pointer-events:none}.major-group{cursor:pointer}.major-group path{fill:color-mix(in srgb,var(--_chords-primary) 14%,var(--_chords-surface));stroke:var(--_chords-surface);stroke-width:2;transition:fill .18s ease}.major-group:hover path{fill:color-mix(in srgb,var(--_chords-primary) 24%,var(--_chords-surface))}.major-group .major-label{fill:var(--_chords-text);font-size:15px;font-weight:700;text-anchor:middle;dominant-baseline:central;pointer-events:none;transition:fill .18s ease}.major-group.tonic path{fill:var(--_chords-highlight)}.major-group.tonic:hover path{fill:color-mix(in srgb,var(--_chords-highlight) 84%,var(--_chords-text))}.major-group.tonic .major-label{fill:var(--_chords-on-primary)}.major-group.scale-major path{fill:var(--_chords-primary)}.major-group.scale-major:hover path{fill:color-mix(in srgb,var(--_chords-primary) 82%,var(--_chords-text))}.major-group.scale-major .major-label{fill:var(--_chords-on-primary)}.minor-group{cursor:pointer}.minor-group path{fill:color-mix(in srgb,var(--_chords-secondary) 10%,var(--_chords-surface));stroke:var(--_chords-surface);stroke-width:2;transition:fill .18s ease}.minor-group:hover path{fill:color-mix(in srgb,var(--_chords-secondary) 20%,var(--_chords-surface))}.minor-group .minor-label{fill:color-mix(in srgb,var(--_chords-text) 78%,var(--_chords-muted));font-size:12px;font-weight:600;text-anchor:middle;dominant-baseline:central;pointer-events:none;transition:fill .18s ease}.minor-group.tonic path{fill:var(--_chords-highlight)}.minor-group.tonic:hover path{fill:color-mix(in srgb,var(--_chords-highlight) 84%,var(--_chords-text))}.minor-group.tonic .minor-label{fill:var(--_chords-on-primary)}.minor-group.scale-minor path{fill:var(--_chords-secondary)}.minor-group.scale-minor:hover path{fill:color-mix(in srgb,var(--_chords-secondary) 82%,var(--_chords-text))}.minor-group.scale-minor .minor-label{fill:var(--_chords-on-primary)}.minor-group.scale-diminished path{fill:var(--_chords-danger)}.minor-group.scale-diminished:hover path{fill:color-mix(in srgb,var(--_chords-danger) 82%,var(--_chords-text))}.minor-group.scale-diminished .minor-label{fill:var(--_chords-on-primary)}.role-label{font-size:10px;font-style:italic;font-weight:600;text-anchor:middle;dominant-baseline:central;pointer-events:none;fill:color-mix(in srgb,var(--_chords-on-primary) 85%,transparent)}.outer-border{fill:none;stroke:var(--_chords-text);stroke-width:2.5}.center-circle{fill:var(--_chords-surface);stroke:var(--_chords-border);stroke-width:2;filter:drop-shadow(0 2px 10px color-mix(in srgb,var(--_chords-text) 8%,transparent))}.center-name{font-size:30px;font-weight:800;text-anchor:middle;dominant-baseline:auto;fill:var(--_chords-text);letter-spacing:-.02em}.center-scale{font-size:13px;font-weight:600;text-anchor:middle;dominant-baseline:auto;fill:var(--_chords-muted);text-transform:uppercase;letter-spacing:.08em}.center-relative{font-size:9.5px;font-weight:500;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background))}.center-deselect{font-size:8px;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 42%,var(--_chords-background))}.center-hint{font-size:16px;font-weight:600;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background))}.center-hint-sm{font-size:11px;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 42%,var(--_chords-background))}.legend{display:flex;justify-content:center;gap:16px;flex-wrap:wrap;margin:16px 0 0}.legend .legend-item{display:flex;align-items:center;gap:6px;font-size:.8rem;color:var(--_chords-muted)}.legend .legend-item:before{content:\"\";display:inline-block;width:14px;height:14px;border-radius:3px}.legend .legend-item.tonic:before{background:var(--_chords-highlight)}.legend .legend-item.scale-major:before{background:var(--_chords-primary)}.legend .legend-item.scale-minor:before{background:var(--_chords-secondary)}.legend .legend-item.scale-diminished:before{background:var(--_chords-danger)}\n"] }]
271
+ }], propDecorators: { language: [{ type: i0.Input, args: [{ isSignal: true, alias: "language", required: false }] }], selectedIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedIndex", required: false }] }], selectedType: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedType", required: false }] }], interactive: [{ type: i0.Input, args: [{ isSignal: true, alias: "interactive", required: false }] }], keySelected: [{ type: i0.Output, args: ["keySelected"] }] } });
4
272
 
5
273
  const PROGRESSIONS = {
6
274
  major: [
@@ -104,100 +372,6 @@ const PROGRESSIONS = {
104
372
  },
105
373
  ],
106
374
  };
107
- const COPY = {
108
- en: {
109
- title: 'Circle of Fifths',
110
- subtitle: 'Click any major or minor key to highlight its diatonic chords',
111
- ariaLabel: 'Circle of Fifths',
112
- clickKey: 'Click a key',
113
- seeChords: 'to see its chords',
114
- deselect: '↩ click to deselect',
115
- tonicLegend: 'Tonic (I / i)',
116
- majorLegend: 'Major chords (IV, V)',
117
- minorLegend: 'Minor chords (ii, iii, vi)',
118
- diminishedLegend: 'Diminished (vii° / ii°)',
119
- diatonicChords: 'Diatonic Chords',
120
- commonProgressions: 'Common Progressions in',
121
- generateProgressions: 'Shuffle',
122
- chorus: 'Chorus',
123
- bridge: 'Bridge',
124
- outro: 'Outro',
125
- verse: 'Verse',
126
- copy: 'Copy',
127
- expand: 'Expand',
128
- collapse: 'Collapse',
129
- transposeLabel: 'Transpose to:',
130
- spiceLabels: ['🌶 Spice', '🌶🌶 Spicier', '🔥 Hot!', '↩ Plain'],
131
- major: 'Major',
132
- minor: 'Minor',
133
- diminished: 'diminished',
134
- relativeMinor: 'Relative minor',
135
- relativeMajor: 'Relative major',
136
- roles: {
137
- tonic: 'Tonic',
138
- supertonic: 'Supertonic',
139
- mediant: 'Mediant',
140
- subdominant: 'Subdominant',
141
- dominant: 'Dominant',
142
- submediant: 'Submediant',
143
- leadingTone: 'Leading Tone',
144
- subtonic: 'Subtonic',
145
- },
146
- },
147
- es: {
148
- title: 'Círculo de quintas',
149
- subtitle: 'Haz clic en una tonalidad mayor o menor para resaltar sus acordes diatónicos',
150
- ariaLabel: 'Círculo de quintas',
151
- clickKey: 'Haz clic en una tonalidad',
152
- seeChords: 'para ver sus acordes',
153
- deselect: '↩ haz clic para deseleccionar',
154
- tonicLegend: 'Tónica (I / i)',
155
- majorLegend: 'Acordes mayores (IV, V)',
156
- minorLegend: 'Acordes menores (ii, iii, vi)',
157
- diminishedLegend: 'Disminuido (vii° / ii°)',
158
- diatonicChords: 'Acordes diatónicos',
159
- commonProgressions: 'Progresiones comunes en',
160
- generateProgressions: 'Generar nuevos',
161
- chorus: 'Coro',
162
- bridge: 'Puente',
163
- outro: 'Final',
164
- verse: 'Estrofa',
165
- copy: 'Copiar',
166
- expand: 'Expandir',
167
- collapse: 'Colapsar',
168
- transposeLabel: 'Transponer a:',
169
- spiceLabels: ['🌶 Condimentar', '🌶🌶 Más sabor', '🔥 ¡Picante!', '↩ Simple'],
170
- major: 'Mayor',
171
- minor: 'Menor',
172
- diminished: 'disminuido',
173
- relativeMinor: 'Relativa menor',
174
- relativeMajor: 'Relativa mayor',
175
- roles: {
176
- tonic: 'Tónica',
177
- supertonic: 'Supertónica',
178
- mediant: 'Mediante',
179
- subdominant: 'Subdominante',
180
- dominant: 'Dominante',
181
- submediant: 'Submediante',
182
- leadingTone: 'Sensible',
183
- subtonic: 'Subtónica',
184
- },
185
- },
186
- };
187
- const KEYS = [
188
- { index: 0, major: 'C', minor: 'Am', sharps: 0, flats: 0 },
189
- { index: 1, major: 'G', minor: 'Em', sharps: 1, flats: 0 },
190
- { index: 2, major: 'D', minor: 'Bm', sharps: 2, flats: 0 },
191
- { index: 3, major: 'A', minor: 'F♯m', sharps: 3, flats: 0 },
192
- { index: 4, major: 'E', minor: 'C♯m', sharps: 4, flats: 0 },
193
- { index: 5, major: 'B', minor: 'G♯m', sharps: 5, flats: 0 },
194
- { index: 6, major: 'F♯', minor: 'D♯m', sharps: 6, flats: 0 },
195
- { index: 7, major: 'D♭', minor: 'B♭m', sharps: 0, flats: 5 },
196
- { index: 8, major: 'A♭', minor: 'Fm', sharps: 0, flats: 4 },
197
- { index: 9, major: 'E♭', minor: 'Cm', sharps: 0, flats: 3 },
198
- { index: 10, major: 'B♭', minor: 'Gm', sharps: 0, flats: 2 },
199
- { index: 11, major: 'F', minor: 'Dm', sharps: 0, flats: 1 },
200
- ];
201
375
  function dimChord(idx) {
202
376
  // The diminished chord root is the minor key two steps clockwise (idx+2)
203
377
  return KEYS[(idx + 2) % 12].minor.replace(/m$/, '°');
@@ -258,68 +432,99 @@ function spiceChord(chord, numeral, level) {
258
432
  return chord.endsWith('°') ? chord.slice(0, -1) + suffix : chord + suffix;
259
433
  }
260
434
  class ChordSectionComponent {
261
- chords = input([], /* @ts-ignore */
262
- ...(ngDevMode ? [{ debugName: "chords" }] : /* istanbul ignore next */ []));
435
+ chordService = inject(ChordService);
436
+ selectedIndex = input(null, /* @ts-ignore */
437
+ ...(ngDevMode ? [{ debugName: "selectedIndex" }] : /* istanbul ignore next */ []));
438
+ selectedType = input(null, /* @ts-ignore */
439
+ ...(ngDevMode ? [{ debugName: "selectedType" }] : /* istanbul ignore next */ []));
263
440
  language = input('en', /* @ts-ignore */
264
441
  ...(ngDevMode ? [{ debugName: "language" }] : /* istanbul ignore next */ []));
265
442
  text = computed(() => COPY[this.language()], /* @ts-ignore */
266
443
  ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
267
- match = computed(() => {
268
- const input = this.chords();
269
- if (!input.length)
270
- return null;
271
- const inputSet = new Set(input);
272
- let best = null;
273
- for (let idx = 0; idx < 12; idx++) {
274
- for (const type of ['major', 'minor']) {
275
- // chord names are language-independent; COPY.en is a dummy for label fields
276
- const score = buildChordRows(idx, type, COPY.en).filter((r) => inputSet.has(r.chord)).length;
277
- if (!best || score > best.score)
278
- best = { idx, type, score };
444
+ diagramsExpanded = signal(false, /* @ts-ignore */
445
+ ...(ngDevMode ? [{ debugName: "diagramsExpanded" }] : /* istanbul ignore next */ []));
446
+ // Per-diagram fret position, keyed by ChordSearchResult.id.
447
+ positionIndices = signal({}, /* @ts-ignore */
448
+ ...(ngDevMode ? [{ debugName: "positionIndices" }] : /* istanbul ignore next */ []));
449
+ constructor() {
450
+ // A new key means a new set of diagrams — don't carry stale position
451
+ // choices over from the previous key's chords.
452
+ effect(() => {
453
+ this.selectedIndex();
454
+ this.selectedType();
455
+ this.positionIndices.set({});
456
+ });
457
+ }
458
+ toggleDiagrams() {
459
+ this.diagramsExpanded.update((v) => !v);
460
+ }
461
+ positionIndexFor(id) {
462
+ return this.positionIndices()[id] ?? 0;
463
+ }
464
+ stepPosition(id, total, delta) {
465
+ if (total <= 1)
466
+ return;
467
+ this.positionIndices.update((m) => ({
468
+ ...m,
469
+ [id]: ((m[id] ?? 0) + delta + total) % total,
470
+ }));
471
+ }
472
+ stepAllPositions(delta) {
473
+ this.positionIndices.update((m) => {
474
+ const next = { ...m };
475
+ for (const result of this.chordDiagrams()) {
476
+ const total = result.positions.length;
477
+ if (total <= 1)
478
+ continue;
479
+ next[result.id] = ((next[result.id] ?? 0) + delta + total) % total;
279
480
  }
280
- }
281
- return best && best.score > 0 ? best : null;
282
- }, /* @ts-ignore */
283
- ...(ngDevMode ? [{ debugName: "match" }] : /* istanbul ignore next */ []));
481
+ return next;
482
+ });
483
+ }
284
484
  rows = computed(() => {
285
- const m = this.match();
286
- if (!m)
485
+ const idx = this.selectedIndex();
486
+ const type = this.selectedType();
487
+ if (idx === null || type === null)
287
488
  return [];
288
- return buildChordRows(m.idx, m.type, this.text());
489
+ return buildChordRows(idx, type, this.text());
289
490
  }, /* @ts-ignore */
290
491
  ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
291
492
  info = computed(() => {
292
- const m = this.match();
293
- if (!m)
493
+ const idx = this.selectedIndex();
494
+ const type = this.selectedType();
495
+ if (idx === null || type === null)
294
496
  return null;
295
497
  const copy = this.text();
296
- const key = KEYS[m.idx];
498
+ const key = KEYS[idx];
297
499
  return {
298
- fullName: `${m.type === 'major' ? key.major : key.minor} ${m.type === 'major' ? copy.major : copy.minor}`,
299
- relativeKey: m.type === 'major'
500
+ fullName: `${type === 'major' ? key.major : key.minor} ${type === 'major' ? copy.major : copy.minor}`,
501
+ relativeKey: type === 'major'
300
502
  ? `${copy.relativeMinor}: ${key.minor}`
301
503
  : `${copy.relativeMajor}: ${key.major}`,
302
504
  };
303
505
  }, /* @ts-ignore */
304
506
  ...(ngDevMode ? [{ debugName: "info" }] : /* istanbul ignore next */ []));
507
+ chordDiagrams = computed(() => {
508
+ const rows = this.rows();
509
+ if (!rows.length)
510
+ return [];
511
+ return this.chordService
512
+ .search(rows.map((r) => r.chord).join(','), this.language())
513
+ .filter((r) => !r.error && r.positions.length > 0);
514
+ }, /* @ts-ignore */
515
+ ...(ngDevMode ? [{ debugName: "chordDiagrams" }] : /* istanbul ignore next */ []));
305
516
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ChordSectionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
306
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: ChordSectionComponent, isStandalone: true, selector: "chord-section", inputs: { chords: { classPropertyName: "chords", publicName: "chords", isSignal: true, isRequired: false, transformFunction: null }, language: { classPropertyName: "language", publicName: "language", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (rows().length) {\n <section class=\"chord-section\">\n @if (info(); as info) {\n <h2>{{ info.fullName }} \u2014 {{ text().diatonicChords }}</h2>\n <p class=\"relative-label\">{{ info.relativeKey }}</p>\n }\n <div class=\"chord-grid\">\n @for (row of rows(); track row.numeral) {\n <div class=\"chord-card\" [ngClass]=\"row.state\">\n <div class=\"chord-numeral\">{{ row.numeral }}</div>\n <div class=\"chord-name\">{{ row.chord }}</div>\n <div class=\"chord-type\">{{ row.chordType }}</div>\n <div class=\"chord-role\">{{ row.role }}</div>\n </div>\n }\n </div>\n </section>\n}\n", styles: [":host{--_chords-background: var(--chords-background, #f0f4f8);--_chords-surface: var(--chords-surface, #ffffff);--_chords-surface-elevated: var(--chords-surface-elevated, #f8fafc);--_chords-text: var(--chords-text, #1e293b);--_chords-muted: var(--chords-muted, #64748b);--_chords-primary: var(--chords-primary, #3b82f6);--_chords-secondary: var(--chords-secondary, #06b6d4);--_chords-highlight: var(--chords-highlight, #f59e0b);--_chords-danger: var(--chords-danger, #a855f7);--_chords-border: var(--chords-border, #e2e8f0);--_chords-on-primary: var(--chords-on-primary, #ffffff);display:block;font-family:Segoe UI,system-ui,sans-serif}.chord-section{margin-top:36px;text-align:center}.chord-section h2{margin:0 0 4px;font-size:1.25rem;font-weight:700;color:var(--_chords-text)}.chord-section .relative-label{margin:0 0 20px;font-size:.88rem;color:var(--_chords-muted)}.chord-grid{display:flex;flex-wrap:nowrap;gap:8px;justify-content:center;overflow-x:auto;padding-bottom:4px}.chord-card{border-radius:12px;padding:12px 14px;min-width:82px;flex-shrink:0;text-align:center;background:color-mix(in srgb,var(--_chords-text) 8%,var(--_chords-surface));border:2px solid transparent;transition:transform .15s ease,box-shadow .15s ease}.chord-card:hover{transform:translateY(-2px);box-shadow:0 4px 16px color-mix(in srgb,var(--_chords-text) 10%,transparent)}.chord-card .chord-numeral{font-size:.85rem;font-weight:700;font-style:italic;color:var(--_chords-muted)}.chord-card .chord-name{font-size:1.4rem;font-weight:800;color:var(--_chords-text);margin:2px 0;letter-spacing:-.02em}.chord-card .chord-type{font-size:.65rem;text-transform:uppercase;letter-spacing:.06em;color:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background))}.chord-card .chord-role{font-size:.7rem;color:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background));margin-top:2px}.chord-card.tonic{background:color-mix(in srgb,var(--_chords-highlight) 10%,var(--_chords-surface));border-color:var(--_chords-highlight)}.chord-card.tonic .chord-numeral,.chord-card.tonic .chord-name{color:color-mix(in srgb,var(--_chords-highlight) 58%,var(--_chords-text))}.chord-card.scale-major{background:color-mix(in srgb,var(--_chords-primary) 9%,var(--_chords-surface));border-color:var(--_chords-primary)}.chord-card.scale-major .chord-numeral,.chord-card.scale-major .chord-name{color:color-mix(in srgb,var(--_chords-primary) 65%,var(--_chords-text))}.chord-card.scale-diminished{background:color-mix(in srgb,var(--_chords-danger) 9%,var(--_chords-surface));border-color:var(--_chords-danger)}.chord-card.scale-diminished .chord-numeral,.chord-card.scale-diminished .chord-name{color:color-mix(in srgb,var(--_chords-danger) 65%,var(--_chords-text))}@media(max-width:480px){.chord-card{min-width:75px;padding:10px 12px}}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] });
517
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: ChordSectionComponent, isStandalone: true, selector: "chord-section", inputs: { selectedIndex: { classPropertyName: "selectedIndex", publicName: "selectedIndex", isSignal: true, isRequired: false, transformFunction: null }, selectedType: { classPropertyName: "selectedType", publicName: "selectedType", isSignal: true, isRequired: false, transformFunction: null }, language: { classPropertyName: "language", publicName: "language", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (rows().length) {\n <section class=\"chord-section\">\n @if (info(); as info) {\n <h2>{{ info.fullName }} \u2014 {{ text().diatonicChords }}</h2>\n <p class=\"relative-label\">{{ info.relativeKey }}</p>\n }\n <div class=\"chord-grid\">\n @for (row of rows(); track row.numeral) {\n <div class=\"chord-card\" [ngClass]=\"row.state\">\n <div class=\"chord-numeral\">{{ row.numeral }}</div>\n <div class=\"chord-name\">{{ row.chord }}</div>\n <div class=\"chord-type\">{{ row.chordType }}</div>\n <div class=\"chord-role\">{{ row.role }}</div>\n </div>\n }\n </div>\n\n <button type=\"button\" class=\"btn-diagrams-toggle\" (click)=\"toggleDiagrams()\">\n {{ diagramsExpanded() ? text().hideChordDiagrams : text().showChordDiagrams }}\n </button>\n\n @if (diagramsExpanded()) {\n <div class=\"diagram-grid-row\">\n <div class=\"diagram-grid\">\n @for (result of chordDiagrams(); track result.id) {\n @if (result.positions[positionIndexFor(result.id)]; as position) {\n <div class=\"diagram-wrap\">\n <button\n type=\"button\"\n class=\"diagram-nav-btn\"\n [disabled]=\"result.positions.length <= 1\"\n [attr.aria-label]=\"text().previousPosition\"\n (click)=\"stepPosition(result.id, result.positions.length, -1)\"\n >\n \u25B2\n </button>\n <app-chord-diagram\n [title]=\"result.displayName\"\n [position]=\"position\"\n [positionIndex]=\"positionIndexFor(result.id)\"\n />\n <button\n type=\"button\"\n class=\"diagram-nav-btn\"\n [disabled]=\"result.positions.length <= 1\"\n [attr.aria-label]=\"text().nextPosition\"\n (click)=\"stepPosition(result.id, result.positions.length, 1)\"\n >\n \u25BC\n </button>\n </div>\n }\n }\n </div>\n\n <div class=\"diagram-group-nav\">\n <button type=\"button\" [attr.aria-label]=\"text().previousPositionAll\" (click)=\"stepAllPositions(-1)\">\u25B2</button>\n <button type=\"button\" [attr.aria-label]=\"text().nextPositionAll\" (click)=\"stepAllPositions(1)\">\u25BC</button>\n </div>\n </div>\n }\n </section>\n}\n", styles: [":host{--_chords-background: var(--chords-background, #f0f4f8);--_chords-surface: var(--chords-surface, #ffffff);--_chords-surface-elevated: var(--chords-surface-elevated, #f8fafc);--_chords-text: var(--chords-text, #1e293b);--_chords-muted: var(--chords-muted, #64748b);--_chords-primary: var(--chords-primary, #3b82f6);--_chords-secondary: var(--chords-secondary, #06b6d4);--_chords-highlight: var(--chords-highlight, #f59e0b);--_chords-danger: var(--chords-danger, #a855f7);--_chords-border: var(--chords-border, #e2e8f0);--_chords-on-primary: var(--chords-on-primary, #ffffff);display:block;font-family:Segoe UI,system-ui,sans-serif}.chord-section{margin-top:36px;text-align:center}.chord-section h2{margin:0 0 4px;font-size:1.25rem;font-weight:700;color:var(--_chords-text)}.chord-section .relative-label{margin:0 0 20px;font-size:.88rem;color:var(--_chords-muted)}.chord-grid{display:flex;flex-wrap:wrap;gap:8px;justify-content:center}.chord-card{border-radius:12px;padding:12px 14px;min-width:82px;flex-shrink:0;text-align:center;background:color-mix(in srgb,var(--_chords-text) 8%,var(--_chords-surface));border:2px solid transparent;transition:transform .15s ease,box-shadow .15s ease}.chord-card:hover{transform:translateY(-2px);box-shadow:0 4px 16px color-mix(in srgb,var(--_chords-text) 10%,transparent)}.chord-card .chord-numeral{font-size:.85rem;font-weight:700;font-style:italic;color:var(--_chords-muted)}.chord-card .chord-name{font-size:1.4rem;font-weight:800;color:var(--_chords-text);margin:2px 0;letter-spacing:-.02em}.chord-card .chord-type{font-size:.65rem;text-transform:uppercase;letter-spacing:.06em;color:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background))}.chord-card .chord-role{font-size:.7rem;color:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background));margin-top:2px}.chord-card.tonic{background:color-mix(in srgb,var(--_chords-highlight) 10%,var(--_chords-surface));border-color:var(--_chords-highlight)}.chord-card.tonic .chord-numeral,.chord-card.tonic .chord-name{color:color-mix(in srgb,var(--_chords-highlight) 58%,var(--_chords-text))}.chord-card.scale-major{background:color-mix(in srgb,var(--_chords-primary) 9%,var(--_chords-surface));border-color:var(--_chords-primary)}.chord-card.scale-major .chord-numeral,.chord-card.scale-major .chord-name{color:color-mix(in srgb,var(--_chords-primary) 65%,var(--_chords-text))}.chord-card.scale-diminished{background:color-mix(in srgb,var(--_chords-danger) 9%,var(--_chords-surface));border-color:var(--_chords-danger)}.chord-card.scale-diminished .chord-numeral,.chord-card.scale-diminished .chord-name{color:color-mix(in srgb,var(--_chords-danger) 65%,var(--_chords-text))}@media(max-width:480px){.chord-card{min-width:75px;padding:10px 12px}}.btn-diagrams-toggle{display:block;width:100%;margin-top:14px;padding:10px 14px;font-size:.82rem;font-weight:600;border:1px solid var(--_chords-border);border-radius:10px;background:transparent;color:var(--_chords-text);cursor:pointer;transition:border-color .15s,color .15s}.btn-diagrams-toggle:hover{border-color:var(--_chords-primary);color:var(--_chords-primary)}.diagram-grid-row{display:flex;align-items:center;gap:10px;margin-top:16px}.diagram-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(90px,1fr));justify-items:center;gap:12px;flex:1 1 auto;min-width:0}.diagram-wrap{display:flex;flex-direction:column;align-items:center;gap:4px;width:100%;max-width:130px}.diagram-nav-btn,.diagram-group-nav button{padding:2px 10px;font-size:.7rem;line-height:1.4;border:1px solid var(--_chords-border);border-radius:8px;background:transparent;color:var(--_chords-muted);cursor:pointer;transition:border-color .15s,color .15s}.diagram-nav-btn:hover:not(:disabled),.diagram-group-nav button:hover:not(:disabled){border-color:var(--_chords-primary);color:var(--_chords-primary)}.diagram-nav-btn:disabled,.diagram-group-nav button:disabled{opacity:.35;cursor:not-allowed}.diagram-nav-btn{width:100%}.diagram-group-nav{display:flex;flex-direction:column;justify-content:center;gap:8px;flex:0 0 auto}.diagram-group-nav button{padding:10px 8px}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: ChordDiagram, selector: "app-chord-diagram", inputs: ["title", "position", "positionIndex"] }] });
307
518
  }
308
519
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ChordSectionComponent, decorators: [{
309
520
  type: Component,
310
- args: [{ selector: 'chord-section', standalone: true, imports: [NgClass], template: "@if (rows().length) {\n <section class=\"chord-section\">\n @if (info(); as info) {\n <h2>{{ info.fullName }} \u2014 {{ text().diatonicChords }}</h2>\n <p class=\"relative-label\">{{ info.relativeKey }}</p>\n }\n <div class=\"chord-grid\">\n @for (row of rows(); track row.numeral) {\n <div class=\"chord-card\" [ngClass]=\"row.state\">\n <div class=\"chord-numeral\">{{ row.numeral }}</div>\n <div class=\"chord-name\">{{ row.chord }}</div>\n <div class=\"chord-type\">{{ row.chordType }}</div>\n <div class=\"chord-role\">{{ row.role }}</div>\n </div>\n }\n </div>\n </section>\n}\n", styles: [":host{--_chords-background: var(--chords-background, #f0f4f8);--_chords-surface: var(--chords-surface, #ffffff);--_chords-surface-elevated: var(--chords-surface-elevated, #f8fafc);--_chords-text: var(--chords-text, #1e293b);--_chords-muted: var(--chords-muted, #64748b);--_chords-primary: var(--chords-primary, #3b82f6);--_chords-secondary: var(--chords-secondary, #06b6d4);--_chords-highlight: var(--chords-highlight, #f59e0b);--_chords-danger: var(--chords-danger, #a855f7);--_chords-border: var(--chords-border, #e2e8f0);--_chords-on-primary: var(--chords-on-primary, #ffffff);display:block;font-family:Segoe UI,system-ui,sans-serif}.chord-section{margin-top:36px;text-align:center}.chord-section h2{margin:0 0 4px;font-size:1.25rem;font-weight:700;color:var(--_chords-text)}.chord-section .relative-label{margin:0 0 20px;font-size:.88rem;color:var(--_chords-muted)}.chord-grid{display:flex;flex-wrap:nowrap;gap:8px;justify-content:center;overflow-x:auto;padding-bottom:4px}.chord-card{border-radius:12px;padding:12px 14px;min-width:82px;flex-shrink:0;text-align:center;background:color-mix(in srgb,var(--_chords-text) 8%,var(--_chords-surface));border:2px solid transparent;transition:transform .15s ease,box-shadow .15s ease}.chord-card:hover{transform:translateY(-2px);box-shadow:0 4px 16px color-mix(in srgb,var(--_chords-text) 10%,transparent)}.chord-card .chord-numeral{font-size:.85rem;font-weight:700;font-style:italic;color:var(--_chords-muted)}.chord-card .chord-name{font-size:1.4rem;font-weight:800;color:var(--_chords-text);margin:2px 0;letter-spacing:-.02em}.chord-card .chord-type{font-size:.65rem;text-transform:uppercase;letter-spacing:.06em;color:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background))}.chord-card .chord-role{font-size:.7rem;color:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background));margin-top:2px}.chord-card.tonic{background:color-mix(in srgb,var(--_chords-highlight) 10%,var(--_chords-surface));border-color:var(--_chords-highlight)}.chord-card.tonic .chord-numeral,.chord-card.tonic .chord-name{color:color-mix(in srgb,var(--_chords-highlight) 58%,var(--_chords-text))}.chord-card.scale-major{background:color-mix(in srgb,var(--_chords-primary) 9%,var(--_chords-surface));border-color:var(--_chords-primary)}.chord-card.scale-major .chord-numeral,.chord-card.scale-major .chord-name{color:color-mix(in srgb,var(--_chords-primary) 65%,var(--_chords-text))}.chord-card.scale-diminished{background:color-mix(in srgb,var(--_chords-danger) 9%,var(--_chords-surface));border-color:var(--_chords-danger)}.chord-card.scale-diminished .chord-numeral,.chord-card.scale-diminished .chord-name{color:color-mix(in srgb,var(--_chords-danger) 65%,var(--_chords-text))}@media(max-width:480px){.chord-card{min-width:75px;padding:10px 12px}}\n"] }]
311
- }], propDecorators: { chords: [{ type: i0.Input, args: [{ isSignal: true, alias: "chords", required: false }] }], language: [{ type: i0.Input, args: [{ isSignal: true, alias: "language", required: false }] }] } });
521
+ args: [{ selector: 'chord-section', standalone: true, imports: [NgClass, ChordDiagram], template: "@if (rows().length) {\n <section class=\"chord-section\">\n @if (info(); as info) {\n <h2>{{ info.fullName }} \u2014 {{ text().diatonicChords }}</h2>\n <p class=\"relative-label\">{{ info.relativeKey }}</p>\n }\n <div class=\"chord-grid\">\n @for (row of rows(); track row.numeral) {\n <div class=\"chord-card\" [ngClass]=\"row.state\">\n <div class=\"chord-numeral\">{{ row.numeral }}</div>\n <div class=\"chord-name\">{{ row.chord }}</div>\n <div class=\"chord-type\">{{ row.chordType }}</div>\n <div class=\"chord-role\">{{ row.role }}</div>\n </div>\n }\n </div>\n\n <button type=\"button\" class=\"btn-diagrams-toggle\" (click)=\"toggleDiagrams()\">\n {{ diagramsExpanded() ? text().hideChordDiagrams : text().showChordDiagrams }}\n </button>\n\n @if (diagramsExpanded()) {\n <div class=\"diagram-grid-row\">\n <div class=\"diagram-grid\">\n @for (result of chordDiagrams(); track result.id) {\n @if (result.positions[positionIndexFor(result.id)]; as position) {\n <div class=\"diagram-wrap\">\n <button\n type=\"button\"\n class=\"diagram-nav-btn\"\n [disabled]=\"result.positions.length <= 1\"\n [attr.aria-label]=\"text().previousPosition\"\n (click)=\"stepPosition(result.id, result.positions.length, -1)\"\n >\n \u25B2\n </button>\n <app-chord-diagram\n [title]=\"result.displayName\"\n [position]=\"position\"\n [positionIndex]=\"positionIndexFor(result.id)\"\n />\n <button\n type=\"button\"\n class=\"diagram-nav-btn\"\n [disabled]=\"result.positions.length <= 1\"\n [attr.aria-label]=\"text().nextPosition\"\n (click)=\"stepPosition(result.id, result.positions.length, 1)\"\n >\n \u25BC\n </button>\n </div>\n }\n }\n </div>\n\n <div class=\"diagram-group-nav\">\n <button type=\"button\" [attr.aria-label]=\"text().previousPositionAll\" (click)=\"stepAllPositions(-1)\">\u25B2</button>\n <button type=\"button\" [attr.aria-label]=\"text().nextPositionAll\" (click)=\"stepAllPositions(1)\">\u25BC</button>\n </div>\n </div>\n }\n </section>\n}\n", styles: [":host{--_chords-background: var(--chords-background, #f0f4f8);--_chords-surface: var(--chords-surface, #ffffff);--_chords-surface-elevated: var(--chords-surface-elevated, #f8fafc);--_chords-text: var(--chords-text, #1e293b);--_chords-muted: var(--chords-muted, #64748b);--_chords-primary: var(--chords-primary, #3b82f6);--_chords-secondary: var(--chords-secondary, #06b6d4);--_chords-highlight: var(--chords-highlight, #f59e0b);--_chords-danger: var(--chords-danger, #a855f7);--_chords-border: var(--chords-border, #e2e8f0);--_chords-on-primary: var(--chords-on-primary, #ffffff);display:block;font-family:Segoe UI,system-ui,sans-serif}.chord-section{margin-top:36px;text-align:center}.chord-section h2{margin:0 0 4px;font-size:1.25rem;font-weight:700;color:var(--_chords-text)}.chord-section .relative-label{margin:0 0 20px;font-size:.88rem;color:var(--_chords-muted)}.chord-grid{display:flex;flex-wrap:wrap;gap:8px;justify-content:center}.chord-card{border-radius:12px;padding:12px 14px;min-width:82px;flex-shrink:0;text-align:center;background:color-mix(in srgb,var(--_chords-text) 8%,var(--_chords-surface));border:2px solid transparent;transition:transform .15s ease,box-shadow .15s ease}.chord-card:hover{transform:translateY(-2px);box-shadow:0 4px 16px color-mix(in srgb,var(--_chords-text) 10%,transparent)}.chord-card .chord-numeral{font-size:.85rem;font-weight:700;font-style:italic;color:var(--_chords-muted)}.chord-card .chord-name{font-size:1.4rem;font-weight:800;color:var(--_chords-text);margin:2px 0;letter-spacing:-.02em}.chord-card .chord-type{font-size:.65rem;text-transform:uppercase;letter-spacing:.06em;color:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background))}.chord-card .chord-role{font-size:.7rem;color:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background));margin-top:2px}.chord-card.tonic{background:color-mix(in srgb,var(--_chords-highlight) 10%,var(--_chords-surface));border-color:var(--_chords-highlight)}.chord-card.tonic .chord-numeral,.chord-card.tonic .chord-name{color:color-mix(in srgb,var(--_chords-highlight) 58%,var(--_chords-text))}.chord-card.scale-major{background:color-mix(in srgb,var(--_chords-primary) 9%,var(--_chords-surface));border-color:var(--_chords-primary)}.chord-card.scale-major .chord-numeral,.chord-card.scale-major .chord-name{color:color-mix(in srgb,var(--_chords-primary) 65%,var(--_chords-text))}.chord-card.scale-diminished{background:color-mix(in srgb,var(--_chords-danger) 9%,var(--_chords-surface));border-color:var(--_chords-danger)}.chord-card.scale-diminished .chord-numeral,.chord-card.scale-diminished .chord-name{color:color-mix(in srgb,var(--_chords-danger) 65%,var(--_chords-text))}@media(max-width:480px){.chord-card{min-width:75px;padding:10px 12px}}.btn-diagrams-toggle{display:block;width:100%;margin-top:14px;padding:10px 14px;font-size:.82rem;font-weight:600;border:1px solid var(--_chords-border);border-radius:10px;background:transparent;color:var(--_chords-text);cursor:pointer;transition:border-color .15s,color .15s}.btn-diagrams-toggle:hover{border-color:var(--_chords-primary);color:var(--_chords-primary)}.diagram-grid-row{display:flex;align-items:center;gap:10px;margin-top:16px}.diagram-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(90px,1fr));justify-items:center;gap:12px;flex:1 1 auto;min-width:0}.diagram-wrap{display:flex;flex-direction:column;align-items:center;gap:4px;width:100%;max-width:130px}.diagram-nav-btn,.diagram-group-nav button{padding:2px 10px;font-size:.7rem;line-height:1.4;border:1px solid var(--_chords-border);border-radius:8px;background:transparent;color:var(--_chords-muted);cursor:pointer;transition:border-color .15s,color .15s}.diagram-nav-btn:hover:not(:disabled),.diagram-group-nav button:hover:not(:disabled){border-color:var(--_chords-primary);color:var(--_chords-primary)}.diagram-nav-btn:disabled,.diagram-group-nav button:disabled{opacity:.35;cursor:not-allowed}.diagram-nav-btn{width:100%}.diagram-group-nav{display:flex;flex-direction:column;justify-content:center;gap:8px;flex:0 0 auto}.diagram-group-nav button{padding:10px 8px}\n"] }]
522
+ }], ctorParameters: () => [], propDecorators: { selectedIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedIndex", required: false }] }], selectedType: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedType", required: false }] }], language: [{ type: i0.Input, args: [{ isSignal: true, alias: "language", required: false }] }] } });
312
523
  class CircleOfFifthsComponent {
313
524
  language = input('en', /* @ts-ignore */
314
525
  ...(ngDevMode ? [{ debugName: "language" }] : /* istanbul ignore next */ []));
315
526
  text = computed(() => COPY[this.language()], /* @ts-ignore */
316
527
  ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
317
- CX = 300;
318
- CY = 300;
319
- KEY_SIG = { inner: 250, outer: 288 };
320
- MAJOR = { inner: 172, outer: 250 };
321
- MINOR = { inner: 102, outer: 172 };
322
- keys = KEYS;
323
528
  selectedIndex = signal(null, /* @ts-ignore */
324
529
  ...(ngDevMode ? [{ debugName: "selectedIndex" }] : /* istanbul ignore next */ []));
325
530
  selectedType = signal(null, /* @ts-ignore */
@@ -348,16 +553,6 @@ class CircleOfFifthsComponent {
348
553
  history.replaceState(null, '', location.pathname + location.search + hash);
349
554
  });
350
555
  }
351
- get slice() {
352
- const idx = this.selectedIndex();
353
- if (idx === null)
354
- return null;
355
- return {
356
- prev: (idx - 1 + 12) % 12,
357
- curr: idx,
358
- next: (idx + 1) % 12,
359
- };
360
- }
361
556
  selectKey(index, type) {
362
557
  if (this.selectedIndex() === index && this.selectedType() === type) {
363
558
  this.selectedIndex.set(null);
@@ -416,101 +611,6 @@ class CircleOfFifthsComponent {
416
611
  return [];
417
612
  return KEYS.map((k) => ({ idx: k.index, name: type === 'major' ? k.major : k.minor }));
418
613
  }
419
- getMajorState(index) {
420
- const s = this.slice;
421
- if (!s)
422
- return '';
423
- const type = this.selectedType();
424
- if (index === s.curr)
425
- return type === 'major' ? 'tonic' : 'scale-major';
426
- if (index === s.prev || index === s.next)
427
- return 'scale-major';
428
- return '';
429
- }
430
- getMinorState(index) {
431
- const s = this.slice;
432
- if (!s)
433
- return '';
434
- const type = this.selectedType();
435
- const dimIdx = (s.curr + 2) % 12;
436
- if (index === s.curr)
437
- return type === 'minor' ? 'tonic' : 'scale-minor';
438
- if (index === s.prev || index === s.next)
439
- return 'scale-minor';
440
- if (index === dimIdx)
441
- return 'scale-diminished';
442
- return '';
443
- }
444
- getMajorRole(index) {
445
- const s = this.slice;
446
- if (!s)
447
- return '';
448
- const type = this.selectedType();
449
- if (type === 'major') {
450
- if (index === s.curr)
451
- return 'I';
452
- if (index === s.prev)
453
- return 'IV';
454
- if (index === s.next)
455
- return 'V';
456
- }
457
- else {
458
- if (index === s.curr)
459
- return 'III';
460
- if (index === s.prev)
461
- return 'VI';
462
- if (index === s.next)
463
- return 'VII';
464
- }
465
- return '';
466
- }
467
- getMinorRole(index) {
468
- const s = this.slice;
469
- if (!s)
470
- return '';
471
- const type = this.selectedType();
472
- const dimIdx = (s.curr + 2) % 12;
473
- if (type === 'major') {
474
- if (index === s.curr)
475
- return 'vi';
476
- if (index === s.prev)
477
- return 'ii';
478
- if (index === s.next)
479
- return 'iii';
480
- if (index === dimIdx)
481
- return 'vii°';
482
- }
483
- else {
484
- if (index === s.curr)
485
- return 'i';
486
- if (index === s.prev)
487
- return 'iv';
488
- if (index === s.next)
489
- return 'v';
490
- if (index === dimIdx)
491
- return 'ii°';
492
- }
493
- return '';
494
- }
495
- arcPath(innerR, outerR, index, gap = 1.5) {
496
- const mid = index * 30 - 90;
497
- const s = (mid - 15 + gap) * (Math.PI / 180);
498
- const e = (mid + 15 - gap) * (Math.PI / 180);
499
- const cx = this.CX, cy = this.CY;
500
- const x1 = cx + outerR * Math.cos(s), y1 = cy + outerR * Math.sin(s);
501
- const x2 = cx + outerR * Math.cos(e), y2 = cy + outerR * Math.sin(e);
502
- const x3 = cx + innerR * Math.cos(e), y3 = cy + innerR * Math.sin(e);
503
- const x4 = cx + innerR * Math.cos(s), y4 = cy + innerR * Math.sin(s);
504
- const f = (n) => n.toFixed(2);
505
- return `M${f(x1)} ${f(y1)} A${outerR} ${outerR} 0 0 1 ${f(x2)} ${f(y2)} L${f(x3)} ${f(y3)} A${innerR} ${innerR} 0 0 0 ${f(x4)} ${f(y4)}Z`;
506
- }
507
- textPos(midR, index) {
508
- const a = (index * 30 - 90) * (Math.PI / 180);
509
- return {
510
- x: this.CX + midR * Math.cos(a),
511
- y: this.CY + midR * Math.sin(a),
512
- };
513
- }
514
614
  get chordTable() {
515
615
  const idx = this.selectedIndex();
516
616
  const type = this.selectedType();
@@ -518,16 +618,6 @@ class CircleOfFifthsComponent {
518
618
  return [];
519
619
  return buildChordRows(idx, type, this.text());
520
620
  }
521
- get chordNames() {
522
- return this.chordTable.map((r) => r.chord);
523
- }
524
- getAccidentalText(key) {
525
- if (key.sharps > 0)
526
- return `${key.sharps}♯`;
527
- if (key.flats > 0)
528
- return `${key.flats}♭`;
529
- return '—';
530
- }
531
621
  get selectedInfo() {
532
622
  const idx = this.selectedIndex();
533
623
  const type = this.selectedType();
@@ -535,15 +625,7 @@ class CircleOfFifthsComponent {
535
625
  return null;
536
626
  const key = KEYS[idx];
537
627
  const copy = this.text();
538
- const scale = type === 'major' ? copy.major : copy.minor;
539
- return {
540
- name: type === 'major' ? key.major : key.minor,
541
- scale,
542
- fullName: `${type === 'major' ? key.major : key.minor} ${scale}`,
543
- relativeKey: type === 'major'
544
- ? `${copy.relativeMinor}: ${key.minor}`
545
- : `${copy.relativeMajor}: ${key.major}`,
546
- };
628
+ return { fullName: `${type === 'major' ? key.major : key.minor} ${type === 'major' ? copy.major : copy.minor}` };
547
629
  }
548
630
  deriveSections(numerals, lookup) {
549
631
  const type = this.selectedType();
@@ -598,11 +680,11 @@ class CircleOfFifthsComponent {
598
680
  });
599
681
  }
600
682
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: CircleOfFifthsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
601
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: CircleOfFifthsComponent, isStandalone: true, selector: "the-chords-circle-of-fifths", inputs: { language: { classPropertyName: "language", publicName: "language", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.lang": "language()" } }, ngImport: i0, template: "<div class=\"page\">\n <header class=\"page-header\">\n <h1>{{ text().title }}</h1>\n <p>{{ text().subtitle }}</p>\n </header>\n\n <div class=\"circle-wrapper\">\n <svg\n viewBox=\"0 0 600 600\"\n xmlns=\"http://www.w3.org/2000/svg\"\n [attr.aria-label]=\"text().ariaLabel\"\n >\n <!-- \u2500\u2500 Key Signature Ring \u2500\u2500 -->\n @for (key of keys; track key.index) {\n <path [attr.d]=\"arcPath(KEY_SIG.inner, KEY_SIG.outer, key.index)\" class=\"keysig-segment\" />\n <text\n [attr.x]=\"textPos((KEY_SIG.inner + KEY_SIG.outer) / 2, key.index).x\"\n [attr.y]=\"textPos((KEY_SIG.inner + KEY_SIG.outer) / 2, key.index).y\"\n class=\"keysig-label\"\n >\n {{ getAccidentalText(key) }}\n </text>\n }\n\n <!-- \u2500\u2500 Major Keys Ring \u2500\u2500 -->\n @for (key of keys; track key.index) {\n <g\n [ngClass]=\"['major-group', getMajorState(key.index)]\"\n (click)=\"selectKey(key.index, 'major')\"\n >\n <path [attr.d]=\"arcPath(MAJOR.inner, MAJOR.outer, key.index)\" />\n\n <text\n [attr.x]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).x\"\n [attr.y]=\"\n textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).y +\n (getMajorRole(key.index) ? -9 : 0)\n \"\n class=\"major-label\"\n >\n {{ key.major }}\n </text>\n\n @if (getMajorRole(key.index)) {\n <text\n [attr.x]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).x\"\n [attr.y]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).y + 11\"\n class=\"role-label\"\n >\n {{ getMajorRole(key.index) }}\n </text>\n }\n </g>\n }\n\n <!-- \u2500\u2500 Minor Keys Ring \u2500\u2500 -->\n @for (key of keys; track key.index) {\n <g\n [ngClass]=\"['minor-group', getMinorState(key.index)]\"\n (click)=\"selectKey(key.index, 'minor')\"\n >\n <path [attr.d]=\"arcPath(MINOR.inner, MINOR.outer, key.index)\" />\n\n <text\n [attr.x]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).x\"\n [attr.y]=\"\n textPos((MINOR.inner + MINOR.outer) / 2, key.index).y +\n (getMinorRole(key.index) ? -8 : 0)\n \"\n class=\"minor-label\"\n >\n {{ key.minor }}\n </text>\n\n @if (getMinorRole(key.index)) {\n <text\n [attr.x]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).x\"\n [attr.y]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).y + 9\"\n class=\"role-label\"\n >\n {{ getMinorRole(key.index) }}\n </text>\n }\n </g>\n }\n\n <!-- \u2500\u2500 Outer border \u2500\u2500 -->\n <circle cx=\"300\" cy=\"300\" r=\"289\" class=\"outer-border\" />\n\n <!-- \u2500\u2500 Center Circle \u2500\u2500 -->\n <circle cx=\"300\" cy=\"300\" r=\"99\" class=\"center-circle\" />\n\n @if (selectedInfo) {\n <text x=\"300\" y=\"283\" class=\"center-name\">{{ selectedInfo.name }}</text>\n <text x=\"300\" y=\"304\" class=\"center-scale\">{{ selectedInfo.scale }}</text>\n <text x=\"300\" y=\"322\" class=\"center-relative\">{{ selectedInfo.relativeKey }}</text>\n <text x=\"300\" y=\"338\" class=\"center-deselect\">{{ text().deselect }}</text>\n } @else {\n <text x=\"300\" y=\"293\" class=\"center-hint\">{{ text().clickKey }}</text>\n <text x=\"300\" y=\"312\" class=\"center-hint-sm\">{{ text().seeChords }}</text>\n }\n </svg>\n </div>\n\n <!-- \u2500\u2500 Legend \u2500\u2500 -->\n <div class=\"legend\">\n <span class=\"legend-item tonic\">{{ text().tonicLegend }}</span>\n <span class=\"legend-item scale-major\">{{ text().majorLegend }}</span>\n <span class=\"legend-item scale-minor\">{{ text().minorLegend }}</span>\n <span class=\"legend-item scale-diminished\">{{ text().diminishedLegend }}</span>\n </div>\n\n <chord-section [chords]=\"chordNames\" [language]=\"language()\"></chord-section>\n\n <!-- \u2500\u2500 Chord Progressions \u2500\u2500 -->\n @if (progressions.length) {\n <section class=\"progressions-section\">\n <div class=\"progressions-header\">\n <h2>{{ text().commonProgressions }} {{ selectedInfo?.fullName }}</h2>\n <button class=\"btn-randomize\" (click)=\"randomizeProgressions()\">{{ text().generateProgressions }}</button>\n </div>\n <div class=\"progression-list\">\n @for (prog of progressions; track prog.name) {\n <div class=\"progression-card\">\n <div class=\"prog-header\">\n <span class=\"prog-mood\">{{ prog.mood }}</span>\n <span class=\"prog-name\">{{ prog.name }}</span>\n <span class=\"prog-genre\">{{ prog.genre }}</span>\n </div>\n <div class=\"prog-chords\">\n @for (chord of prog.chords; track $index; let last = $last) {\n <span class=\"prog-chord\">{{ chord }}</span>\n @if (!last) {\n <span class=\"prog-arrow\">\u2192</span>\n }\n }\n </div>\n <div class=\"prog-numerals\">\n @for (num of prog.numerals; track $index; let last = $last) {\n <span>{{ num }}</span>\n @if (!last) {\n <span class=\"prog-arrow\">\u2192</span>\n }\n }\n </div>\n <div class=\"prog-transpose-row\">\n <label class=\"prog-transpose-label\">{{ text().transposeLabel }}</label>\n <select class=\"prog-transpose-select\" (change)=\"setTranspose(prog.nameEn, +$any($event.target).value)\">\n @for (key of transposeKeys; track key.idx) {\n <option [value]=\"key.idx\" [selected]=\"key.idx === prog.transposeIdx\">{{ key.name }}</option>\n }\n </select>\n </div>\n <div class=\"prog-expand-row\">\n <button class=\"btn-expand\" (click)=\"toggleCard(prog.name)\">\n {{ expandedCards().has(prog.name) ? text().collapse : text().expand }}\n </button>\n <button class=\"btn-expand\" (click)=\"copyChords(prog)\">\n {{ text().copy }}\n </button>\n <button class=\"btn-spice\" [class.btn-spice--active]=\"prog.spiceLevel > 0\" (click)=\"cycleSpice(prog.nameEn)\">\n {{ text().spiceLabels[prog.spiceLevel] }}\n </button>\n </div>\n @if (expandedCards().has(prog.name)) {\n <div class=\"prog-sections\">\n @for (section of prog.sections; track section.label) {\n <div class=\"prog-section\">\n <div class=\"prog-section-label\">{{ section.label }}</div>\n <div class=\"prog-chords\">\n @for (chord of section.chords; track $index; let last = $last) {\n <span class=\"prog-chord\">{{ chord }}</span>\n @if (!last) { <span class=\"prog-arrow\">\u2192</span> }\n }\n </div>\n <div class=\"prog-numerals\">\n @for (num of section.numerals; track $index; let last = $last) {\n <span>{{ num }}</span>\n @if (!last) { <span class=\"prog-arrow\">\u2192</span> }\n }\n </div>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n </section>\n }\n</div>\n", styles: [":host{--_chords-background: var(--chords-background, #f0f4f8);--_chords-background-alt: var(--chords-background-alt, #f8fafc);--_chords-surface: var(--chords-surface, #ffffff);--_chords-surface-elevated: var(--chords-surface-elevated, #f8fafc);--_chords-text: var(--chords-text, #1e293b);--_chords-muted: var(--chords-muted, #64748b);--_chords-primary: var(--chords-primary, #3b82f6);--_chords-secondary: var(--chords-secondary, #06b6d4);--_chords-highlight: var(--chords-highlight, #f59e0b);--_chords-danger: var(--chords-danger, #a855f7);--_chords-border: var(--chords-border, #e2e8f0);--_chords-on-primary: var(--chords-on-primary, #ffffff);box-sizing:border-box;display:block;min-height:100%;background:var(--_chords-background);color:var(--_chords-text)}:host *,:host *:before,:host *:after{box-sizing:inherit}.page{max-width:720px;margin:0 auto;padding:24px 16px 48px;font-family:Segoe UI,system-ui,sans-serif}.page-header{text-align:center;margin-bottom:28px}.page-header h1{margin:0 0 6px;font-size:2rem;font-weight:800;color:var(--_chords-text);letter-spacing:-.02em}.page-header p{margin:0;color:var(--_chords-muted);font-size:.95rem}.circle-wrapper{max-width:580px;margin:0 auto}.circle-wrapper svg{display:block;width:100%;height:auto;overflow:visible}.keysig-segment{fill:var(--_chords-text)}.keysig-label{fill:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background));font-size:12px;font-weight:500;text-anchor:middle;dominant-baseline:central;pointer-events:none}.major-group{cursor:pointer}.major-group path{fill:color-mix(in srgb,var(--_chords-primary) 14%,var(--_chords-surface));stroke:var(--_chords-surface);stroke-width:2;transition:fill .18s ease}.major-group:hover path{fill:color-mix(in srgb,var(--_chords-primary) 24%,var(--_chords-surface))}.major-group .major-label{fill:var(--_chords-text);font-size:15px;font-weight:700;text-anchor:middle;dominant-baseline:central;pointer-events:none;transition:fill .18s ease}.major-group.tonic path{fill:var(--_chords-highlight)}.major-group.tonic:hover path{fill:color-mix(in srgb,var(--_chords-highlight) 84%,var(--_chords-text))}.major-group.tonic .major-label{fill:var(--_chords-on-primary)}.major-group.scale-major path{fill:var(--_chords-primary)}.major-group.scale-major:hover path{fill:color-mix(in srgb,var(--_chords-primary) 82%,var(--_chords-text))}.major-group.scale-major .major-label{fill:var(--_chords-on-primary)}.minor-group{cursor:pointer}.minor-group path{fill:color-mix(in srgb,var(--_chords-secondary) 10%,var(--_chords-surface));stroke:var(--_chords-surface);stroke-width:2;transition:fill .18s ease}.minor-group:hover path{fill:color-mix(in srgb,var(--_chords-secondary) 20%,var(--_chords-surface))}.minor-group .minor-label{fill:color-mix(in srgb,var(--_chords-text) 78%,var(--_chords-muted));font-size:12px;font-weight:600;text-anchor:middle;dominant-baseline:central;pointer-events:none;transition:fill .18s ease}.minor-group.tonic path{fill:var(--_chords-highlight)}.minor-group.tonic:hover path{fill:color-mix(in srgb,var(--_chords-highlight) 84%,var(--_chords-text))}.minor-group.tonic .minor-label{fill:var(--_chords-on-primary)}.minor-group.scale-minor path{fill:var(--_chords-secondary)}.minor-group.scale-minor:hover path{fill:color-mix(in srgb,var(--_chords-secondary) 82%,var(--_chords-text))}.minor-group.scale-minor .minor-label{fill:var(--_chords-on-primary)}.minor-group.scale-diminished path{fill:var(--_chords-danger)}.minor-group.scale-diminished:hover path{fill:color-mix(in srgb,var(--_chords-danger) 82%,var(--_chords-text))}.minor-group.scale-diminished .minor-label{fill:var(--_chords-on-primary)}.role-label{font-size:10px;font-style:italic;font-weight:600;text-anchor:middle;dominant-baseline:central;pointer-events:none;fill:color-mix(in srgb,var(--_chords-on-primary) 85%,transparent)}.outer-border{fill:none;stroke:var(--_chords-text);stroke-width:2.5}.center-circle{fill:var(--_chords-surface);stroke:var(--_chords-border);stroke-width:2;filter:drop-shadow(0 2px 10px color-mix(in srgb,var(--_chords-text) 8%,transparent))}.center-name{font-size:30px;font-weight:800;text-anchor:middle;dominant-baseline:auto;fill:var(--_chords-text);letter-spacing:-.02em}.center-scale{font-size:13px;font-weight:600;text-anchor:middle;dominant-baseline:auto;fill:var(--_chords-muted);text-transform:uppercase;letter-spacing:.08em}.center-relative{font-size:9.5px;font-weight:500;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background))}.center-deselect{font-size:8px;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 42%,var(--_chords-background))}.center-hint{font-size:16px;font-weight:600;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background))}.center-hint-sm{font-size:11px;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 42%,var(--_chords-background))}.legend{display:flex;justify-content:center;gap:16px;flex-wrap:wrap;margin:16px 0 0}.legend .legend-item{display:flex;align-items:center;gap:6px;font-size:.8rem;color:var(--_chords-muted)}.legend .legend-item:before{content:\"\";display:inline-block;width:14px;height:14px;border-radius:3px}.legend .legend-item.tonic:before{background:var(--_chords-highlight)}.legend .legend-item.scale-major:before{background:var(--_chords-primary)}.legend .legend-item.scale-minor:before{background:var(--_chords-secondary)}.legend .legend-item.scale-diminished:before{background:var(--_chords-danger)}.progressions-section{margin-top:36px;text-align:center}.progressions-header{display:flex;align-items:center;justify-content:center;gap:12px;margin-bottom:20px;flex-wrap:wrap}.progressions-header h2{margin:0;font-size:1.25rem;font-weight:700;color:var(--_chords-text)}.btn-randomize{padding:6px 14px;font-size:.8rem;font-weight:600;border:1.5px solid var(--_chords-primary);border-radius:20px;background:transparent;color:var(--_chords-primary);cursor:pointer;transition:background .15s,color .15s;white-space:nowrap}.btn-randomize:hover{background:var(--_chords-primary);color:var(--_chords-on-primary)}.progression-list{display:grid;grid-template-columns:repeat(2,1fr);gap:12px}@media(max-width:480px){.progression-list{grid-template-columns:1fr}}.progression-card{background:var(--_chords-surface);border:1.5px solid var(--_chords-border);border-radius:14px;padding:16px 18px;text-align:left;transition:box-shadow .15s ease,transform .15s ease}.progression-card:hover{box-shadow:0 4px 16px color-mix(in srgb,var(--_chords-text) 8%,transparent);transform:translateY(-2px)}.prog-header{display:flex;align-items:baseline;gap:8px;margin-bottom:12px;flex-wrap:wrap}.prog-mood{font-size:.95rem;font-weight:700;color:var(--_chords-text)}.prog-name{font-size:.75rem;font-weight:600;color:var(--_chords-muted);text-transform:uppercase;letter-spacing:.06em}.prog-genre{margin-left:auto;font-size:.7rem;color:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background));font-style:italic}.prog-chords{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-bottom:6px}.prog-chord{font-size:1.15rem;font-weight:800;color:var(--_chords-text);letter-spacing:-.02em}.prog-numerals{display:flex;align-items:center;gap:6px;font-size:.75rem;font-style:italic;color:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background))}.prog-transpose-row{display:flex;align-items:center;gap:6px;margin-top:8px}.prog-transpose-label{font-size:.68rem;font-weight:600;color:var(--_chords-muted);white-space:nowrap}.prog-transpose-select{font-size:.72rem;font-weight:600;color:var(--_chords-muted);background:transparent;border:1px solid var(--_chords-border);border-radius:8px;padding:2px 6px;cursor:pointer;transition:border-color .15s,color .15s;appearance:auto}.prog-transpose-select:hover,.prog-transpose-select:focus{border-color:var(--_chords-primary);color:var(--_chords-primary);outline:none}.prog-expand-row{display:flex;justify-content:flex-end;margin-top:10px}.btn-spice{background:transparent;border:1px solid var(--cof-accent, #e09030);border-radius:6px;color:var(--cof-accent, #e09030);cursor:pointer;font-size:.75rem;padding:4px 10px;transition:background .2s,color .2s}.btn-spice:hover{background:var(--cof-accent, #e09030);color:#fff}.btn-spice.btn-spice--active{background:var(--cof-accent, #e09030);color:#fff;font-weight:600}.btn-expand{padding:3px 10px;font-size:.72rem;font-weight:600;border:1px solid var(--_chords-border);border-radius:12px;background:transparent;color:var(--_chords-muted);cursor:pointer;transition:border-color .15s,color .15s}.btn-expand:hover{border-color:var(--_chords-primary);color:var(--_chords-primary)}.prog-sections{margin-top:12px;border-top:1px solid var(--_chords-border);padding-top:10px;display:flex;flex-direction:column;gap:10px}.prog-section{padding:8px 10px;border-radius:8px;background:color-mix(in srgb,var(--_chords-text) 4%,var(--_chords-surface))}.prog-section-label{font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--_chords-muted);margin-bottom:5px}.prog-arrow{color:color-mix(in srgb,var(--_chords-muted) 42%,var(--_chords-background));font-size:.8rem}@media(max-width:480px){.page-header h1{font-size:1.5rem}}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: ChordSectionComponent, selector: "chord-section", inputs: ["chords", "language"] }] });
683
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: CircleOfFifthsComponent, isStandalone: true, selector: "the-chords-circle-of-fifths", inputs: { language: { classPropertyName: "language", publicName: "language", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.lang": "language()" } }, ngImport: i0, template: "<div class=\"page\">\n <header class=\"page-header\">\n <h1>{{ text().title }}</h1>\n <p>{{ text().subtitle }}</p>\n </header>\n\n <div class=\"circle-and-chords\">\n <circle-of-fifths-wheel\n class=\"circle-and-chords-wheel\"\n [language]=\"language()\"\n [selectedIndex]=\"selectedIndex()\"\n [selectedType]=\"selectedType()\"\n (keySelected)=\"selectKey($event.index, $event.type)\"\n />\n\n <chord-section\n class=\"circle-and-chords-section\"\n [selectedIndex]=\"selectedIndex()\"\n [selectedType]=\"selectedType()\"\n [language]=\"language()\"\n ></chord-section>\n </div>\n\n <!-- \u2500\u2500 Chord Progressions \u2500\u2500 -->\n @if (progressions.length) {\n <section class=\"progressions-section\">\n <div class=\"progressions-header\">\n <h2>{{ text().commonProgressions }} {{ selectedInfo?.fullName }}</h2>\n <button class=\"btn-randomize\" (click)=\"randomizeProgressions()\">{{ text().generateProgressions }}</button>\n </div>\n <div class=\"progression-list\">\n @for (prog of progressions; track prog.name) {\n <div class=\"progression-card\">\n <div class=\"prog-header\">\n <span class=\"prog-mood\">{{ prog.mood }}</span>\n <span class=\"prog-name\">{{ prog.name }}</span>\n <span class=\"prog-genre\">{{ prog.genre }}</span>\n </div>\n <div class=\"prog-chords\">\n @for (chord of prog.chords; track $index; let last = $last) {\n <span class=\"prog-chord\">{{ chord }}</span>\n @if (!last) {\n <span class=\"prog-arrow\">\u2192</span>\n }\n }\n </div>\n <div class=\"prog-numerals\">\n @for (num of prog.numerals; track $index; let last = $last) {\n <span>{{ num }}</span>\n @if (!last) {\n <span class=\"prog-arrow\">\u2192</span>\n }\n }\n </div>\n <div class=\"prog-transpose-row\">\n <label class=\"prog-transpose-label\">{{ text().transposeLabel }}</label>\n <select class=\"prog-transpose-select\" (change)=\"setTranspose(prog.nameEn, +$any($event.target).value)\">\n @for (key of transposeKeys; track key.idx) {\n <option [value]=\"key.idx\" [selected]=\"key.idx === prog.transposeIdx\">{{ key.name }}</option>\n }\n </select>\n </div>\n <div class=\"prog-expand-row\">\n <button class=\"btn-expand\" (click)=\"toggleCard(prog.name)\">\n {{ expandedCards().has(prog.name) ? text().collapse : text().expand }}\n </button>\n <button class=\"btn-expand\" (click)=\"copyChords(prog)\">\n {{ text().copy }}\n </button>\n <button class=\"btn-spice\" [class.btn-spice--active]=\"prog.spiceLevel > 0\" (click)=\"cycleSpice(prog.nameEn)\">\n {{ text().spiceLabels[prog.spiceLevel] }}\n </button>\n </div>\n @if (expandedCards().has(prog.name)) {\n <div class=\"prog-sections\">\n @for (section of prog.sections; track section.label) {\n <div class=\"prog-section\">\n <div class=\"prog-section-label\">{{ section.label }}</div>\n <div class=\"prog-chords\">\n @for (chord of section.chords; track $index; let last = $last) {\n <span class=\"prog-chord\">{{ chord }}</span>\n @if (!last) { <span class=\"prog-arrow\">\u2192</span> }\n }\n </div>\n <div class=\"prog-numerals\">\n @for (num of section.numerals; track $index; let last = $last) {\n <span>{{ num }}</span>\n @if (!last) { <span class=\"prog-arrow\">\u2192</span> }\n }\n </div>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n </section>\n }\n</div>\n", styles: [":host{--_chords-background: var(--chords-background, #f0f4f8);--_chords-background-alt: var(--chords-background-alt, #f8fafc);--_chords-surface: var(--chords-surface, #ffffff);--_chords-surface-elevated: var(--chords-surface-elevated, #f8fafc);--_chords-text: var(--chords-text, #1e293b);--_chords-muted: var(--chords-muted, #64748b);--_chords-primary: var(--chords-primary, #3b82f6);--_chords-secondary: var(--chords-secondary, #06b6d4);--_chords-highlight: var(--chords-highlight, #f59e0b);--_chords-danger: var(--chords-danger, #a855f7);--_chords-border: var(--chords-border, #e2e8f0);--_chords-on-primary: var(--chords-on-primary, #ffffff);box-sizing:border-box;display:block;min-height:100%;background:var(--_chords-background);color:var(--_chords-text)}:host *,:host *:before,:host *:after{box-sizing:inherit}.page{max-width:720px;margin:0 auto;padding:24px 16px 48px;font-family:Segoe UI,system-ui,sans-serif}@media(min-width:900px){.page{max-width:1320px}}.circle-and-chords{display:flex;flex-direction:column;gap:24px}@media(min-width:900px){.circle-and-chords{flex-direction:row;align-items:flex-start}}@media(min-width:900px){.circle-and-chords-wheel{flex:0 1 460px}}.circle-and-chords-section{min-width:0}@media(min-width:900px){.circle-and-chords-section{flex:1 1 auto}}.page-header{text-align:center;margin-bottom:28px}.page-header h1{margin:0 0 6px;font-size:2rem;font-weight:800;color:var(--_chords-text);letter-spacing:-.02em}.page-header p{margin:0;color:var(--_chords-muted);font-size:.95rem}.progressions-section{margin-top:36px;text-align:center}.progressions-header{display:flex;align-items:center;justify-content:center;gap:12px;margin-bottom:20px;flex-wrap:wrap}.progressions-header h2{margin:0;font-size:1.25rem;font-weight:700;color:var(--_chords-text)}.btn-randomize{padding:6px 14px;font-size:.8rem;font-weight:600;border:1.5px solid var(--_chords-primary);border-radius:20px;background:transparent;color:var(--_chords-primary);cursor:pointer;transition:background .15s,color .15s;white-space:nowrap}.btn-randomize:hover{background:var(--_chords-primary);color:var(--_chords-on-primary)}.progression-list{display:grid;grid-template-columns:repeat(2,1fr);gap:12px}@media(max-width:480px){.progression-list{grid-template-columns:1fr}}.progression-card{background:var(--_chords-surface);border:1.5px solid var(--_chords-border);border-radius:14px;padding:16px 18px;text-align:left;transition:box-shadow .15s ease,transform .15s ease}.progression-card:hover{box-shadow:0 4px 16px color-mix(in srgb,var(--_chords-text) 8%,transparent);transform:translateY(-2px)}.prog-header{display:flex;align-items:baseline;gap:8px;margin-bottom:12px;flex-wrap:wrap}.prog-mood{font-size:.95rem;font-weight:700;color:var(--_chords-text)}.prog-name{font-size:.75rem;font-weight:600;color:var(--_chords-muted);text-transform:uppercase;letter-spacing:.06em}.prog-genre{margin-left:auto;font-size:.7rem;color:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background));font-style:italic}.prog-chords{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-bottom:6px}.prog-chord{font-size:1.15rem;font-weight:800;color:var(--_chords-text);letter-spacing:-.02em}.prog-numerals{display:flex;align-items:center;gap:6px;font-size:.75rem;font-style:italic;color:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background))}.prog-transpose-row{display:flex;align-items:center;gap:6px;margin-top:8px}.prog-transpose-label{font-size:.68rem;font-weight:600;color:var(--_chords-muted);white-space:nowrap}.prog-transpose-select{font-size:.72rem;font-weight:600;color:var(--_chords-muted);background:transparent;border:1px solid var(--_chords-border);border-radius:8px;padding:2px 6px;cursor:pointer;transition:border-color .15s,color .15s;appearance:auto}.prog-transpose-select:hover,.prog-transpose-select:focus{border-color:var(--_chords-primary);color:var(--_chords-primary);outline:none}.prog-expand-row{display:flex;justify-content:flex-end;margin-top:10px}.btn-spice{background:transparent;border:1px solid var(--cof-accent, #e09030);border-radius:6px;color:var(--cof-accent, #e09030);cursor:pointer;font-size:.75rem;padding:4px 10px;transition:background .2s,color .2s}.btn-spice:hover{background:var(--cof-accent, #e09030);color:#fff}.btn-spice.btn-spice--active{background:var(--cof-accent, #e09030);color:#fff;font-weight:600}.btn-expand{padding:3px 10px;font-size:.72rem;font-weight:600;border:1px solid var(--_chords-border);border-radius:12px;background:transparent;color:var(--_chords-muted);cursor:pointer;transition:border-color .15s,color .15s}.btn-expand:hover{border-color:var(--_chords-primary);color:var(--_chords-primary)}.prog-sections{margin-top:12px;border-top:1px solid var(--_chords-border);padding-top:10px;display:flex;flex-direction:column;gap:10px}.prog-section{padding:8px 10px;border-radius:8px;background:color-mix(in srgb,var(--_chords-text) 4%,var(--_chords-surface))}.prog-section-label{font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--_chords-muted);margin-bottom:5px}.prog-arrow{color:color-mix(in srgb,var(--_chords-muted) 42%,var(--_chords-background));font-size:.8rem}@media(max-width:480px){.page-header h1{font-size:1.5rem}}\n"], dependencies: [{ kind: "component", type: ChordSectionComponent, selector: "chord-section", inputs: ["selectedIndex", "selectedType", "language"] }, { kind: "component", type: CircleOfFifthsWheel, selector: "circle-of-fifths-wheel", inputs: ["language", "selectedIndex", "selectedType", "interactive"], outputs: ["keySelected"] }] });
602
684
  }
603
685
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: CircleOfFifthsComponent, decorators: [{
604
686
  type: Component,
605
- args: [{ selector: 'the-chords-circle-of-fifths', standalone: true, imports: [NgClass, ChordSectionComponent], host: { '[attr.lang]': 'language()' }, template: "<div class=\"page\">\n <header class=\"page-header\">\n <h1>{{ text().title }}</h1>\n <p>{{ text().subtitle }}</p>\n </header>\n\n <div class=\"circle-wrapper\">\n <svg\n viewBox=\"0 0 600 600\"\n xmlns=\"http://www.w3.org/2000/svg\"\n [attr.aria-label]=\"text().ariaLabel\"\n >\n <!-- \u2500\u2500 Key Signature Ring \u2500\u2500 -->\n @for (key of keys; track key.index) {\n <path [attr.d]=\"arcPath(KEY_SIG.inner, KEY_SIG.outer, key.index)\" class=\"keysig-segment\" />\n <text\n [attr.x]=\"textPos((KEY_SIG.inner + KEY_SIG.outer) / 2, key.index).x\"\n [attr.y]=\"textPos((KEY_SIG.inner + KEY_SIG.outer) / 2, key.index).y\"\n class=\"keysig-label\"\n >\n {{ getAccidentalText(key) }}\n </text>\n }\n\n <!-- \u2500\u2500 Major Keys Ring \u2500\u2500 -->\n @for (key of keys; track key.index) {\n <g\n [ngClass]=\"['major-group', getMajorState(key.index)]\"\n (click)=\"selectKey(key.index, 'major')\"\n >\n <path [attr.d]=\"arcPath(MAJOR.inner, MAJOR.outer, key.index)\" />\n\n <text\n [attr.x]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).x\"\n [attr.y]=\"\n textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).y +\n (getMajorRole(key.index) ? -9 : 0)\n \"\n class=\"major-label\"\n >\n {{ key.major }}\n </text>\n\n @if (getMajorRole(key.index)) {\n <text\n [attr.x]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).x\"\n [attr.y]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).y + 11\"\n class=\"role-label\"\n >\n {{ getMajorRole(key.index) }}\n </text>\n }\n </g>\n }\n\n <!-- \u2500\u2500 Minor Keys Ring \u2500\u2500 -->\n @for (key of keys; track key.index) {\n <g\n [ngClass]=\"['minor-group', getMinorState(key.index)]\"\n (click)=\"selectKey(key.index, 'minor')\"\n >\n <path [attr.d]=\"arcPath(MINOR.inner, MINOR.outer, key.index)\" />\n\n <text\n [attr.x]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).x\"\n [attr.y]=\"\n textPos((MINOR.inner + MINOR.outer) / 2, key.index).y +\n (getMinorRole(key.index) ? -8 : 0)\n \"\n class=\"minor-label\"\n >\n {{ key.minor }}\n </text>\n\n @if (getMinorRole(key.index)) {\n <text\n [attr.x]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).x\"\n [attr.y]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).y + 9\"\n class=\"role-label\"\n >\n {{ getMinorRole(key.index) }}\n </text>\n }\n </g>\n }\n\n <!-- \u2500\u2500 Outer border \u2500\u2500 -->\n <circle cx=\"300\" cy=\"300\" r=\"289\" class=\"outer-border\" />\n\n <!-- \u2500\u2500 Center Circle \u2500\u2500 -->\n <circle cx=\"300\" cy=\"300\" r=\"99\" class=\"center-circle\" />\n\n @if (selectedInfo) {\n <text x=\"300\" y=\"283\" class=\"center-name\">{{ selectedInfo.name }}</text>\n <text x=\"300\" y=\"304\" class=\"center-scale\">{{ selectedInfo.scale }}</text>\n <text x=\"300\" y=\"322\" class=\"center-relative\">{{ selectedInfo.relativeKey }}</text>\n <text x=\"300\" y=\"338\" class=\"center-deselect\">{{ text().deselect }}</text>\n } @else {\n <text x=\"300\" y=\"293\" class=\"center-hint\">{{ text().clickKey }}</text>\n <text x=\"300\" y=\"312\" class=\"center-hint-sm\">{{ text().seeChords }}</text>\n }\n </svg>\n </div>\n\n <!-- \u2500\u2500 Legend \u2500\u2500 -->\n <div class=\"legend\">\n <span class=\"legend-item tonic\">{{ text().tonicLegend }}</span>\n <span class=\"legend-item scale-major\">{{ text().majorLegend }}</span>\n <span class=\"legend-item scale-minor\">{{ text().minorLegend }}</span>\n <span class=\"legend-item scale-diminished\">{{ text().diminishedLegend }}</span>\n </div>\n\n <chord-section [chords]=\"chordNames\" [language]=\"language()\"></chord-section>\n\n <!-- \u2500\u2500 Chord Progressions \u2500\u2500 -->\n @if (progressions.length) {\n <section class=\"progressions-section\">\n <div class=\"progressions-header\">\n <h2>{{ text().commonProgressions }} {{ selectedInfo?.fullName }}</h2>\n <button class=\"btn-randomize\" (click)=\"randomizeProgressions()\">{{ text().generateProgressions }}</button>\n </div>\n <div class=\"progression-list\">\n @for (prog of progressions; track prog.name) {\n <div class=\"progression-card\">\n <div class=\"prog-header\">\n <span class=\"prog-mood\">{{ prog.mood }}</span>\n <span class=\"prog-name\">{{ prog.name }}</span>\n <span class=\"prog-genre\">{{ prog.genre }}</span>\n </div>\n <div class=\"prog-chords\">\n @for (chord of prog.chords; track $index; let last = $last) {\n <span class=\"prog-chord\">{{ chord }}</span>\n @if (!last) {\n <span class=\"prog-arrow\">\u2192</span>\n }\n }\n </div>\n <div class=\"prog-numerals\">\n @for (num of prog.numerals; track $index; let last = $last) {\n <span>{{ num }}</span>\n @if (!last) {\n <span class=\"prog-arrow\">\u2192</span>\n }\n }\n </div>\n <div class=\"prog-transpose-row\">\n <label class=\"prog-transpose-label\">{{ text().transposeLabel }}</label>\n <select class=\"prog-transpose-select\" (change)=\"setTranspose(prog.nameEn, +$any($event.target).value)\">\n @for (key of transposeKeys; track key.idx) {\n <option [value]=\"key.idx\" [selected]=\"key.idx === prog.transposeIdx\">{{ key.name }}</option>\n }\n </select>\n </div>\n <div class=\"prog-expand-row\">\n <button class=\"btn-expand\" (click)=\"toggleCard(prog.name)\">\n {{ expandedCards().has(prog.name) ? text().collapse : text().expand }}\n </button>\n <button class=\"btn-expand\" (click)=\"copyChords(prog)\">\n {{ text().copy }}\n </button>\n <button class=\"btn-spice\" [class.btn-spice--active]=\"prog.spiceLevel > 0\" (click)=\"cycleSpice(prog.nameEn)\">\n {{ text().spiceLabels[prog.spiceLevel] }}\n </button>\n </div>\n @if (expandedCards().has(prog.name)) {\n <div class=\"prog-sections\">\n @for (section of prog.sections; track section.label) {\n <div class=\"prog-section\">\n <div class=\"prog-section-label\">{{ section.label }}</div>\n <div class=\"prog-chords\">\n @for (chord of section.chords; track $index; let last = $last) {\n <span class=\"prog-chord\">{{ chord }}</span>\n @if (!last) { <span class=\"prog-arrow\">\u2192</span> }\n }\n </div>\n <div class=\"prog-numerals\">\n @for (num of section.numerals; track $index; let last = $last) {\n <span>{{ num }}</span>\n @if (!last) { <span class=\"prog-arrow\">\u2192</span> }\n }\n </div>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n </section>\n }\n</div>\n", styles: [":host{--_chords-background: var(--chords-background, #f0f4f8);--_chords-background-alt: var(--chords-background-alt, #f8fafc);--_chords-surface: var(--chords-surface, #ffffff);--_chords-surface-elevated: var(--chords-surface-elevated, #f8fafc);--_chords-text: var(--chords-text, #1e293b);--_chords-muted: var(--chords-muted, #64748b);--_chords-primary: var(--chords-primary, #3b82f6);--_chords-secondary: var(--chords-secondary, #06b6d4);--_chords-highlight: var(--chords-highlight, #f59e0b);--_chords-danger: var(--chords-danger, #a855f7);--_chords-border: var(--chords-border, #e2e8f0);--_chords-on-primary: var(--chords-on-primary, #ffffff);box-sizing:border-box;display:block;min-height:100%;background:var(--_chords-background);color:var(--_chords-text)}:host *,:host *:before,:host *:after{box-sizing:inherit}.page{max-width:720px;margin:0 auto;padding:24px 16px 48px;font-family:Segoe UI,system-ui,sans-serif}.page-header{text-align:center;margin-bottom:28px}.page-header h1{margin:0 0 6px;font-size:2rem;font-weight:800;color:var(--_chords-text);letter-spacing:-.02em}.page-header p{margin:0;color:var(--_chords-muted);font-size:.95rem}.circle-wrapper{max-width:580px;margin:0 auto}.circle-wrapper svg{display:block;width:100%;height:auto;overflow:visible}.keysig-segment{fill:var(--_chords-text)}.keysig-label{fill:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background));font-size:12px;font-weight:500;text-anchor:middle;dominant-baseline:central;pointer-events:none}.major-group{cursor:pointer}.major-group path{fill:color-mix(in srgb,var(--_chords-primary) 14%,var(--_chords-surface));stroke:var(--_chords-surface);stroke-width:2;transition:fill .18s ease}.major-group:hover path{fill:color-mix(in srgb,var(--_chords-primary) 24%,var(--_chords-surface))}.major-group .major-label{fill:var(--_chords-text);font-size:15px;font-weight:700;text-anchor:middle;dominant-baseline:central;pointer-events:none;transition:fill .18s ease}.major-group.tonic path{fill:var(--_chords-highlight)}.major-group.tonic:hover path{fill:color-mix(in srgb,var(--_chords-highlight) 84%,var(--_chords-text))}.major-group.tonic .major-label{fill:var(--_chords-on-primary)}.major-group.scale-major path{fill:var(--_chords-primary)}.major-group.scale-major:hover path{fill:color-mix(in srgb,var(--_chords-primary) 82%,var(--_chords-text))}.major-group.scale-major .major-label{fill:var(--_chords-on-primary)}.minor-group{cursor:pointer}.minor-group path{fill:color-mix(in srgb,var(--_chords-secondary) 10%,var(--_chords-surface));stroke:var(--_chords-surface);stroke-width:2;transition:fill .18s ease}.minor-group:hover path{fill:color-mix(in srgb,var(--_chords-secondary) 20%,var(--_chords-surface))}.minor-group .minor-label{fill:color-mix(in srgb,var(--_chords-text) 78%,var(--_chords-muted));font-size:12px;font-weight:600;text-anchor:middle;dominant-baseline:central;pointer-events:none;transition:fill .18s ease}.minor-group.tonic path{fill:var(--_chords-highlight)}.minor-group.tonic:hover path{fill:color-mix(in srgb,var(--_chords-highlight) 84%,var(--_chords-text))}.minor-group.tonic .minor-label{fill:var(--_chords-on-primary)}.minor-group.scale-minor path{fill:var(--_chords-secondary)}.minor-group.scale-minor:hover path{fill:color-mix(in srgb,var(--_chords-secondary) 82%,var(--_chords-text))}.minor-group.scale-minor .minor-label{fill:var(--_chords-on-primary)}.minor-group.scale-diminished path{fill:var(--_chords-danger)}.minor-group.scale-diminished:hover path{fill:color-mix(in srgb,var(--_chords-danger) 82%,var(--_chords-text))}.minor-group.scale-diminished .minor-label{fill:var(--_chords-on-primary)}.role-label{font-size:10px;font-style:italic;font-weight:600;text-anchor:middle;dominant-baseline:central;pointer-events:none;fill:color-mix(in srgb,var(--_chords-on-primary) 85%,transparent)}.outer-border{fill:none;stroke:var(--_chords-text);stroke-width:2.5}.center-circle{fill:var(--_chords-surface);stroke:var(--_chords-border);stroke-width:2;filter:drop-shadow(0 2px 10px color-mix(in srgb,var(--_chords-text) 8%,transparent))}.center-name{font-size:30px;font-weight:800;text-anchor:middle;dominant-baseline:auto;fill:var(--_chords-text);letter-spacing:-.02em}.center-scale{font-size:13px;font-weight:600;text-anchor:middle;dominant-baseline:auto;fill:var(--_chords-muted);text-transform:uppercase;letter-spacing:.08em}.center-relative{font-size:9.5px;font-weight:500;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background))}.center-deselect{font-size:8px;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 42%,var(--_chords-background))}.center-hint{font-size:16px;font-weight:600;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background))}.center-hint-sm{font-size:11px;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 42%,var(--_chords-background))}.legend{display:flex;justify-content:center;gap:16px;flex-wrap:wrap;margin:16px 0 0}.legend .legend-item{display:flex;align-items:center;gap:6px;font-size:.8rem;color:var(--_chords-muted)}.legend .legend-item:before{content:\"\";display:inline-block;width:14px;height:14px;border-radius:3px}.legend .legend-item.tonic:before{background:var(--_chords-highlight)}.legend .legend-item.scale-major:before{background:var(--_chords-primary)}.legend .legend-item.scale-minor:before{background:var(--_chords-secondary)}.legend .legend-item.scale-diminished:before{background:var(--_chords-danger)}.progressions-section{margin-top:36px;text-align:center}.progressions-header{display:flex;align-items:center;justify-content:center;gap:12px;margin-bottom:20px;flex-wrap:wrap}.progressions-header h2{margin:0;font-size:1.25rem;font-weight:700;color:var(--_chords-text)}.btn-randomize{padding:6px 14px;font-size:.8rem;font-weight:600;border:1.5px solid var(--_chords-primary);border-radius:20px;background:transparent;color:var(--_chords-primary);cursor:pointer;transition:background .15s,color .15s;white-space:nowrap}.btn-randomize:hover{background:var(--_chords-primary);color:var(--_chords-on-primary)}.progression-list{display:grid;grid-template-columns:repeat(2,1fr);gap:12px}@media(max-width:480px){.progression-list{grid-template-columns:1fr}}.progression-card{background:var(--_chords-surface);border:1.5px solid var(--_chords-border);border-radius:14px;padding:16px 18px;text-align:left;transition:box-shadow .15s ease,transform .15s ease}.progression-card:hover{box-shadow:0 4px 16px color-mix(in srgb,var(--_chords-text) 8%,transparent);transform:translateY(-2px)}.prog-header{display:flex;align-items:baseline;gap:8px;margin-bottom:12px;flex-wrap:wrap}.prog-mood{font-size:.95rem;font-weight:700;color:var(--_chords-text)}.prog-name{font-size:.75rem;font-weight:600;color:var(--_chords-muted);text-transform:uppercase;letter-spacing:.06em}.prog-genre{margin-left:auto;font-size:.7rem;color:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background));font-style:italic}.prog-chords{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-bottom:6px}.prog-chord{font-size:1.15rem;font-weight:800;color:var(--_chords-text);letter-spacing:-.02em}.prog-numerals{display:flex;align-items:center;gap:6px;font-size:.75rem;font-style:italic;color:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background))}.prog-transpose-row{display:flex;align-items:center;gap:6px;margin-top:8px}.prog-transpose-label{font-size:.68rem;font-weight:600;color:var(--_chords-muted);white-space:nowrap}.prog-transpose-select{font-size:.72rem;font-weight:600;color:var(--_chords-muted);background:transparent;border:1px solid var(--_chords-border);border-radius:8px;padding:2px 6px;cursor:pointer;transition:border-color .15s,color .15s;appearance:auto}.prog-transpose-select:hover,.prog-transpose-select:focus{border-color:var(--_chords-primary);color:var(--_chords-primary);outline:none}.prog-expand-row{display:flex;justify-content:flex-end;margin-top:10px}.btn-spice{background:transparent;border:1px solid var(--cof-accent, #e09030);border-radius:6px;color:var(--cof-accent, #e09030);cursor:pointer;font-size:.75rem;padding:4px 10px;transition:background .2s,color .2s}.btn-spice:hover{background:var(--cof-accent, #e09030);color:#fff}.btn-spice.btn-spice--active{background:var(--cof-accent, #e09030);color:#fff;font-weight:600}.btn-expand{padding:3px 10px;font-size:.72rem;font-weight:600;border:1px solid var(--_chords-border);border-radius:12px;background:transparent;color:var(--_chords-muted);cursor:pointer;transition:border-color .15s,color .15s}.btn-expand:hover{border-color:var(--_chords-primary);color:var(--_chords-primary)}.prog-sections{margin-top:12px;border-top:1px solid var(--_chords-border);padding-top:10px;display:flex;flex-direction:column;gap:10px}.prog-section{padding:8px 10px;border-radius:8px;background:color-mix(in srgb,var(--_chords-text) 4%,var(--_chords-surface))}.prog-section-label{font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--_chords-muted);margin-bottom:5px}.prog-arrow{color:color-mix(in srgb,var(--_chords-muted) 42%,var(--_chords-background));font-size:.8rem}@media(max-width:480px){.page-header h1{font-size:1.5rem}}\n"] }]
687
+ args: [{ selector: 'the-chords-circle-of-fifths', standalone: true, imports: [ChordSectionComponent, CircleOfFifthsWheel], host: { '[attr.lang]': 'language()' }, template: "<div class=\"page\">\n <header class=\"page-header\">\n <h1>{{ text().title }}</h1>\n <p>{{ text().subtitle }}</p>\n </header>\n\n <div class=\"circle-and-chords\">\n <circle-of-fifths-wheel\n class=\"circle-and-chords-wheel\"\n [language]=\"language()\"\n [selectedIndex]=\"selectedIndex()\"\n [selectedType]=\"selectedType()\"\n (keySelected)=\"selectKey($event.index, $event.type)\"\n />\n\n <chord-section\n class=\"circle-and-chords-section\"\n [selectedIndex]=\"selectedIndex()\"\n [selectedType]=\"selectedType()\"\n [language]=\"language()\"\n ></chord-section>\n </div>\n\n <!-- \u2500\u2500 Chord Progressions \u2500\u2500 -->\n @if (progressions.length) {\n <section class=\"progressions-section\">\n <div class=\"progressions-header\">\n <h2>{{ text().commonProgressions }} {{ selectedInfo?.fullName }}</h2>\n <button class=\"btn-randomize\" (click)=\"randomizeProgressions()\">{{ text().generateProgressions }}</button>\n </div>\n <div class=\"progression-list\">\n @for (prog of progressions; track prog.name) {\n <div class=\"progression-card\">\n <div class=\"prog-header\">\n <span class=\"prog-mood\">{{ prog.mood }}</span>\n <span class=\"prog-name\">{{ prog.name }}</span>\n <span class=\"prog-genre\">{{ prog.genre }}</span>\n </div>\n <div class=\"prog-chords\">\n @for (chord of prog.chords; track $index; let last = $last) {\n <span class=\"prog-chord\">{{ chord }}</span>\n @if (!last) {\n <span class=\"prog-arrow\">\u2192</span>\n }\n }\n </div>\n <div class=\"prog-numerals\">\n @for (num of prog.numerals; track $index; let last = $last) {\n <span>{{ num }}</span>\n @if (!last) {\n <span class=\"prog-arrow\">\u2192</span>\n }\n }\n </div>\n <div class=\"prog-transpose-row\">\n <label class=\"prog-transpose-label\">{{ text().transposeLabel }}</label>\n <select class=\"prog-transpose-select\" (change)=\"setTranspose(prog.nameEn, +$any($event.target).value)\">\n @for (key of transposeKeys; track key.idx) {\n <option [value]=\"key.idx\" [selected]=\"key.idx === prog.transposeIdx\">{{ key.name }}</option>\n }\n </select>\n </div>\n <div class=\"prog-expand-row\">\n <button class=\"btn-expand\" (click)=\"toggleCard(prog.name)\">\n {{ expandedCards().has(prog.name) ? text().collapse : text().expand }}\n </button>\n <button class=\"btn-expand\" (click)=\"copyChords(prog)\">\n {{ text().copy }}\n </button>\n <button class=\"btn-spice\" [class.btn-spice--active]=\"prog.spiceLevel > 0\" (click)=\"cycleSpice(prog.nameEn)\">\n {{ text().spiceLabels[prog.spiceLevel] }}\n </button>\n </div>\n @if (expandedCards().has(prog.name)) {\n <div class=\"prog-sections\">\n @for (section of prog.sections; track section.label) {\n <div class=\"prog-section\">\n <div class=\"prog-section-label\">{{ section.label }}</div>\n <div class=\"prog-chords\">\n @for (chord of section.chords; track $index; let last = $last) {\n <span class=\"prog-chord\">{{ chord }}</span>\n @if (!last) { <span class=\"prog-arrow\">\u2192</span> }\n }\n </div>\n <div class=\"prog-numerals\">\n @for (num of section.numerals; track $index; let last = $last) {\n <span>{{ num }}</span>\n @if (!last) { <span class=\"prog-arrow\">\u2192</span> }\n }\n </div>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n </section>\n }\n</div>\n", styles: [":host{--_chords-background: var(--chords-background, #f0f4f8);--_chords-background-alt: var(--chords-background-alt, #f8fafc);--_chords-surface: var(--chords-surface, #ffffff);--_chords-surface-elevated: var(--chords-surface-elevated, #f8fafc);--_chords-text: var(--chords-text, #1e293b);--_chords-muted: var(--chords-muted, #64748b);--_chords-primary: var(--chords-primary, #3b82f6);--_chords-secondary: var(--chords-secondary, #06b6d4);--_chords-highlight: var(--chords-highlight, #f59e0b);--_chords-danger: var(--chords-danger, #a855f7);--_chords-border: var(--chords-border, #e2e8f0);--_chords-on-primary: var(--chords-on-primary, #ffffff);box-sizing:border-box;display:block;min-height:100%;background:var(--_chords-background);color:var(--_chords-text)}:host *,:host *:before,:host *:after{box-sizing:inherit}.page{max-width:720px;margin:0 auto;padding:24px 16px 48px;font-family:Segoe UI,system-ui,sans-serif}@media(min-width:900px){.page{max-width:1320px}}.circle-and-chords{display:flex;flex-direction:column;gap:24px}@media(min-width:900px){.circle-and-chords{flex-direction:row;align-items:flex-start}}@media(min-width:900px){.circle-and-chords-wheel{flex:0 1 460px}}.circle-and-chords-section{min-width:0}@media(min-width:900px){.circle-and-chords-section{flex:1 1 auto}}.page-header{text-align:center;margin-bottom:28px}.page-header h1{margin:0 0 6px;font-size:2rem;font-weight:800;color:var(--_chords-text);letter-spacing:-.02em}.page-header p{margin:0;color:var(--_chords-muted);font-size:.95rem}.progressions-section{margin-top:36px;text-align:center}.progressions-header{display:flex;align-items:center;justify-content:center;gap:12px;margin-bottom:20px;flex-wrap:wrap}.progressions-header h2{margin:0;font-size:1.25rem;font-weight:700;color:var(--_chords-text)}.btn-randomize{padding:6px 14px;font-size:.8rem;font-weight:600;border:1.5px solid var(--_chords-primary);border-radius:20px;background:transparent;color:var(--_chords-primary);cursor:pointer;transition:background .15s,color .15s;white-space:nowrap}.btn-randomize:hover{background:var(--_chords-primary);color:var(--_chords-on-primary)}.progression-list{display:grid;grid-template-columns:repeat(2,1fr);gap:12px}@media(max-width:480px){.progression-list{grid-template-columns:1fr}}.progression-card{background:var(--_chords-surface);border:1.5px solid var(--_chords-border);border-radius:14px;padding:16px 18px;text-align:left;transition:box-shadow .15s ease,transform .15s ease}.progression-card:hover{box-shadow:0 4px 16px color-mix(in srgb,var(--_chords-text) 8%,transparent);transform:translateY(-2px)}.prog-header{display:flex;align-items:baseline;gap:8px;margin-bottom:12px;flex-wrap:wrap}.prog-mood{font-size:.95rem;font-weight:700;color:var(--_chords-text)}.prog-name{font-size:.75rem;font-weight:600;color:var(--_chords-muted);text-transform:uppercase;letter-spacing:.06em}.prog-genre{margin-left:auto;font-size:.7rem;color:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background));font-style:italic}.prog-chords{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-bottom:6px}.prog-chord{font-size:1.15rem;font-weight:800;color:var(--_chords-text);letter-spacing:-.02em}.prog-numerals{display:flex;align-items:center;gap:6px;font-size:.75rem;font-style:italic;color:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background))}.prog-transpose-row{display:flex;align-items:center;gap:6px;margin-top:8px}.prog-transpose-label{font-size:.68rem;font-weight:600;color:var(--_chords-muted);white-space:nowrap}.prog-transpose-select{font-size:.72rem;font-weight:600;color:var(--_chords-muted);background:transparent;border:1px solid var(--_chords-border);border-radius:8px;padding:2px 6px;cursor:pointer;transition:border-color .15s,color .15s;appearance:auto}.prog-transpose-select:hover,.prog-transpose-select:focus{border-color:var(--_chords-primary);color:var(--_chords-primary);outline:none}.prog-expand-row{display:flex;justify-content:flex-end;margin-top:10px}.btn-spice{background:transparent;border:1px solid var(--cof-accent, #e09030);border-radius:6px;color:var(--cof-accent, #e09030);cursor:pointer;font-size:.75rem;padding:4px 10px;transition:background .2s,color .2s}.btn-spice:hover{background:var(--cof-accent, #e09030);color:#fff}.btn-spice.btn-spice--active{background:var(--cof-accent, #e09030);color:#fff;font-weight:600}.btn-expand{padding:3px 10px;font-size:.72rem;font-weight:600;border:1px solid var(--_chords-border);border-radius:12px;background:transparent;color:var(--_chords-muted);cursor:pointer;transition:border-color .15s,color .15s}.btn-expand:hover{border-color:var(--_chords-primary);color:var(--_chords-primary)}.prog-sections{margin-top:12px;border-top:1px solid var(--_chords-border);padding-top:10px;display:flex;flex-direction:column;gap:10px}.prog-section{padding:8px 10px;border-radius:8px;background:color-mix(in srgb,var(--_chords-text) 4%,var(--_chords-surface))}.prog-section-label{font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--_chords-muted);margin-bottom:5px}.prog-arrow{color:color-mix(in srgb,var(--_chords-muted) 42%,var(--_chords-background));font-size:.8rem}@media(max-width:480px){.page-header h1{font-size:1.5rem}}\n"] }]
606
688
  }], ctorParameters: () => [], propDecorators: { language: [{ type: i0.Input, args: [{ isSignal: true, alias: "language", required: false }] }] } });
607
689
 
608
690
  /*
@@ -613,5 +695,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
613
695
  * Generated bundle index. Do not edit.
614
696
  */
615
697
 
616
- export { ChordSectionComponent, CircleOfFifthsComponent };
698
+ export { ChordSectionComponent, CircleOfFifthsComponent, CircleOfFifthsWheel };
617
699
  //# sourceMappingURL=gblp-circle-of-fifths.mjs.map