diff --git a/src/components/FreePlayPicker.vue b/src/components/FreePlayPicker.vue
index 33fbd70..1296b2b 100644
--- a/src/components/FreePlayPicker.vue
+++ b/src/components/FreePlayPicker.vue
@@ -1,7 +1,7 @@
@@ -77,7 +82,7 @@ function selectSubStage(quality: ChordQuality | ScaleMode | IntervalGroup, acces
:disabled="!subStage.accessible"
@click="selectSubStage(subStage.quality, subStage.accessible)"
>
- {{ formatQualityLabel(subStage.quality) }}
+ {{ subStageLabel(subStage.quality) }}
@@ -146,7 +151,6 @@ function selectSubStage(quality: ChordQuality | ScaleMode | IntervalGroup, acces
text-align: left;
cursor: pointer;
font-size: 0.9rem;
- text-transform: capitalize;
}
.sub-stage-btn:last-child {
diff --git a/src/components/TheoryModal.vue b/src/components/TheoryModal.vue
index 88748d5..059472a 100644
--- a/src/components/TheoryModal.vue
+++ b/src/components/TheoryModal.vue
@@ -8,7 +8,7 @@ import type { ScaleMode } from '@/music/data/scales'
import { INTERVAL_CATALOG } from '@/music/data/intervals'
import type { IntervalGroup } from '@/music/data/intervals'
import { SCALE_DESCRIPTIONS, MODE_PARENT } from '@/music/data/theory'
-import { formatQualityLabel } from '@/music/display'
+import { formatQualityLabel, isChordSymbolQuality } from '@/music/display'
import { intervalToDegreeLabel } from '@/music/matrix'
import PianoKeyboard from '@/components/PianoKeyboard.vue'
@@ -58,7 +58,10 @@ const catalogEntry = computed(() => {
const intervals = computed(() => catalogEntry.value?.intervals ?? [])
-const title = computed(() => capitalize(formatQualityLabel(props.quality)))
+const title = computed(() => {
+ const label = formatQualityLabel(props.quality)
+ return isChordSymbolQuality(props.quality) ? label : capitalize(label)
+})
const subtitle = computed(() => {
const meta = MODE_PARENT[props.quality as ScaleMode]
diff --git a/src/components/__tests__/FreePlayPicker.spec.ts b/src/components/__tests__/FreePlayPicker.spec.ts
index f9ace01..d23d7aa 100644
--- a/src/components/__tests__/FreePlayPicker.spec.ts
+++ b/src/components/__tests__/FreePlayPicker.spec.ts
@@ -127,6 +127,35 @@ describe('FreePlayPicker', () => {
})
})
+ describe('sub-stage label casing', () => {
+ it('keeps minor tetrad symbols lowercase (m7, mΔ7)', async () => {
+ const access = makeAccess([
+ {
+ accessible: true,
+ subStages: [
+ { accessible: true, quality: 'm7' },
+ { accessible: true, quality: 'mMaj7' },
+ ],
+ },
+ ])
+ const wrapper = mount(FreePlayPicker, {
+ props: { freePlayAccess: access, initialStage: 0 },
+ })
+ await nextTick()
+ const labels = wrapper.findAll('.sub-stage-btn').map((b) => b.text())
+ expect(labels).toEqual(['m7', 'mΔ7'])
+ })
+
+ it('title-cases word-phrase labels (triads)', async () => {
+ const wrapper = mount(FreePlayPicker, {
+ props: { freePlayAccess: ACCESS_STAGE0_PARTIAL, initialStage: 0 },
+ })
+ await nextTick()
+ const labels = wrapper.findAll('.sub-stage-btn').map((b) => b.text())
+ expect(labels).toEqual(['Major Triad', 'Minor Triad', 'Augmented Triad', 'Diminished Triad'])
+ })
+ })
+
describe('play emission', () => {
it('emits play with the quality when an accessible sub-stage is clicked', async () => {
const wrapper = mount(FreePlayPicker, {
diff --git a/src/components/__tests__/TheoryModal.spec.ts b/src/components/__tests__/TheoryModal.spec.ts
index 278e2c8..8eaa77b 100644
--- a/src/components/__tests__/TheoryModal.spec.ts
+++ b/src/components/__tests__/TheoryModal.spec.ts
@@ -23,6 +23,16 @@ describe('TheoryModal', () => {
expect(wrapper.find('.modal-subtitle').exists()).toBe(false)
})
+ it('keeps minor tetrad symbol lowercase in title (m7)', () => {
+ const wrapper = mount(TheoryModal, { props: { quality: 'm7' } })
+ expect(wrapper.find('.modal-title').text()).toBe('m7')
+ })
+
+ it('keeps minor-major tetrad symbol lowercase in title (mMaj7)', () => {
+ const wrapper = mount(TheoryModal, { props: { quality: 'mMaj7' } })
+ expect(wrapper.find('.modal-title').text()).toBe('mΔ7')
+ })
+
it('shows interval group label as title', () => {
const wrapper = mount(TheoryModal, { props: { quality: 'thirds' } })
expect(wrapper.find('.modal-title').text()).toBe('Thirds')
diff --git a/src/music/display.ts b/src/music/display.ts
index da064ce..db92796 100644
--- a/src/music/display.ts
+++ b/src/music/display.ts
@@ -51,6 +51,12 @@ export function formatQualityLabel(quality: ChordQuality | ScaleMode | IntervalG
return SCALE_LABEL[quality as ScaleMode] ?? quality
}
+// Tetrad/extended chord labels (e.g. 'm7', 'mΔ7') are notation symbols, not words —
+// they must never be capitalized, unlike the word-phrase labels used for triads/scales/intervals.
+export function isChordSymbolQuality(quality: ChordQuality | ScaleMode | IntervalGroup): boolean {
+ return quality in CHORD_CATALOG && !(quality in TRIAD_LABEL)
+}
+
export function formatPuzzleTitle(note: string, quality: ChordQuality | ScaleMode | IntervalGroup): string {
if (quality in CHORD_CATALOG) {
const def = CHORD_CATALOG[quality as ChordQuality]