Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions src/components/FreePlayPicker.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { STAGE_NAMES } from '@/config/game'
import { formatQualityLabel } from '@/music/display'
import { formatQualityLabel, isChordSymbolQuality } from '@/music/display'
import type { FreePlayStageAccess } from '@/stores/progress'
import type { ChordQuality } from '@/music/data/chords'
import type { ScaleMode } from '@/music/data/scales'
Expand Down Expand Up @@ -45,6 +45,11 @@ function selectSubStage(quality: ChordQuality | ScaleMode | IntervalGroup, acces
if (!accessible) return
emit('play', quality)
}

function subStageLabel(quality: ChordQuality | ScaleMode | IntervalGroup): string {
const label = formatQualityLabel(quality)
return isChordSymbolQuality(quality) ? label : label.replace(/\b\w/g, (c) => c.toUpperCase())
}
</script>

<template>
Expand Down Expand Up @@ -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) }}
</button>
</div>
</div>
Expand Down Expand Up @@ -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 {
Expand Down
7 changes: 5 additions & 2 deletions src/components/TheoryModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -58,7 +58,10 @@ const catalogEntry = computed(() => {

const intervals = computed<string[]>(() => 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<string | null>(() => {
const meta = MODE_PARENT[props.quality as ScaleMode]
Expand Down
29 changes: 29 additions & 0 deletions src/components/__tests__/FreePlayPicker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down
10 changes: 10 additions & 0 deletions src/components/__tests__/TheoryModal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
6 changes: 6 additions & 0 deletions src/music/display.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down