@greenblacksky/honmoon 0.0.0-stage → 0.4.1

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.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +85 -2
  3. package/package.json +58 -4
  4. package/src/lib/audio/context.ts +29 -0
  5. package/src/lib/audio/metronome.svelte.ts +146 -0
  6. package/src/lib/audio/mic.ts +254 -0
  7. package/src/lib/audio/pitch.ts +133 -0
  8. package/src/lib/audio/rhythmplay.ts +152 -0
  9. package/src/lib/audio/synth.ts +409 -0
  10. package/src/lib/audio/voiceplay.ts +60 -0
  11. package/src/lib/drills/defs.ts +459 -0
  12. package/src/lib/drills/questions.ts +920 -0
  13. package/src/lib/drills/registry.svelte.ts +167 -0
  14. package/src/lib/drills/runtime.svelte.ts +629 -0
  15. package/src/lib/drills/verdict.ts +241 -0
  16. package/src/lib/index.ts +117 -0
  17. package/src/lib/learn/describe.ts +72 -0
  18. package/src/lib/learn/pools.ts +44 -0
  19. package/src/lib/learn/progress.ts +100 -0
  20. package/src/lib/learn/session.ts +304 -0
  21. package/src/lib/music/bass.ts +139 -0
  22. package/src/lib/music/clefs.ts +218 -0
  23. package/src/lib/music/intervals.ts +94 -0
  24. package/src/lib/music/notes.ts +133 -0
  25. package/src/lib/music/rhythm.ts +624 -0
  26. package/src/lib/music/scales.ts +560 -0
  27. package/src/lib/music/voice.ts +227 -0
  28. package/src/lib/music/warmups.ts +318 -0
  29. package/src/lib/nav.svelte.ts +53 -0
  30. package/src/lib/notation/fretboard.ts +187 -0
  31. package/src/lib/notation/glyphdata.ts +101 -0
  32. package/src/lib/notation/glyphs.ts +71 -0
  33. package/src/lib/notation/icons.ts +12 -0
  34. package/src/lib/notation/keyboard.ts +166 -0
  35. package/src/lib/notation/rhythmsvg.ts +341 -0
  36. package/src/lib/notation/staff.ts +288 -0
  37. package/src/lib/notation/svg.ts +37 -0
  38. package/src/lib/notation/tab.ts +80 -0
  39. package/src/lib/store/honmoon.svelte.ts +329 -0
  40. package/src/lib/store/normalize.ts +23 -0
  41. package/src/lib/store/sync.ts +43 -0
  42. package/src/lib/store/types.ts +101 -0
  43. package/src/lib/styles/honmoon.css +2064 -0
  44. package/src/lib/ui/AccRow.svelte +23 -0
  45. package/src/lib/ui/BackBar.svelte +23 -0
  46. package/src/lib/ui/BassRow.svelte +30 -0
  47. package/src/lib/ui/Chips.svelte +23 -0
  48. package/src/lib/ui/DataCard.svelte +66 -0
  49. package/src/lib/ui/Fretboard.svelte +71 -0
  50. package/src/lib/ui/Keyboard.svelte +54 -0
  51. package/src/lib/ui/MetronomePanel.svelte +101 -0
  52. package/src/lib/ui/RefBar.svelte +28 -0
  53. package/src/lib/ui/RhythmBar.svelte +9 -0
  54. package/src/lib/ui/Shell.svelte +96 -0
  55. package/src/lib/ui/StaffRun.svelte +46 -0
  56. package/src/lib/ui/Stat.svelte +13 -0
  57. package/src/lib/ui/actions.ts +36 -0
  58. package/src/lib/ui/keyfollow.svelte.ts +48 -0
  59. package/src/lib/ui/overlay.ts +15 -0
  60. package/src/lib/ui/refdata.ts +355 -0
  61. package/src/lib/ui/scaleboard.ts +420 -0
  62. package/src/lib/ui/screens/DegreesRef.svelte +405 -0
  63. package/src/lib/ui/screens/DrillScreen.svelte +1155 -0
  64. package/src/lib/ui/screens/Explorer.svelte +245 -0
  65. package/src/lib/ui/screens/FamilyScreen.svelte +280 -0
  66. package/src/lib/ui/screens/Glossary.svelte +65 -0
  67. package/src/lib/ui/screens/IntervalRef.svelte +37 -0
  68. package/src/lib/ui/screens/LearnIndex.svelte +44 -0
  69. package/src/lib/ui/screens/MetronomeScreen.svelte +17 -0
  70. package/src/lib/ui/screens/PracticeHome.svelte +159 -0
  71. package/src/lib/ui/screens/RhythmRef.svelte +239 -0
  72. package/src/lib/ui/screens/ScalesScreen.svelte +210 -0
  73. package/src/lib/ui/screens/SessionDone.svelte +119 -0
  74. package/src/lib/ui/screens/SetupScreen.svelte +548 -0
  75. package/src/lib/ui/screens/StaffMap.svelte +230 -0
  76. package/src/lib/ui/screens/StatsScreen.svelte +478 -0
  77. package/src/lib/ui/screens/TapCalibration.svelte +186 -0
  78. package/src/lib/ui/tapfix.ts +93 -0
  79. package/src/lib/ui/types.ts +40 -0
  80. package/src/lib/util/list.ts +4 -0
  81. package/src/lib/util/random.ts +23 -0
  82. package/src/lib/util/text.ts +50 -0
@@ -0,0 +1,245 @@
1
+ <!-- Any scale in any key: the notes, the steps, the degrees, the box on the neck, the staff and the keyboard. -->
2
+ <script lang="ts" module>
3
+ type BoxLabels = 'deg' | 'note' | 'fin';
4
+ /** What the explorer shows; kept across visits. Starts on A minor pentatonic from the E string. */
5
+ const XS = $state({ sid: 'minpent', root: 9, start: 3, labels: 'deg' as BoxLabels });
6
+ </script>
7
+
8
+ <script lang="ts">
9
+ import { honmoon } from '../../store/honmoon.svelte';
10
+ import { STRINGS } from '../../music/bass';
11
+ import {
12
+ SCALES,
13
+ SGROUPS,
14
+ degPretty,
15
+ noteText,
16
+ rootName,
17
+ rootPool,
18
+ scaleDef,
19
+ scaleNotes,
20
+ scaleShape,
21
+ scaleSteps,
22
+ } from '../../music/scales';
23
+ import { kbBase } from '../../notation/keyboard';
24
+ import { playScale } from '../../audio/synth';
25
+ import { go, hasRoute } from '../../nav.svelte';
26
+ import { fretFrac, hscroll } from '../actions';
27
+ import { kbFollow } from '../keyfollow.svelte';
28
+ import { kbHalfStepLine, scaleBoard, stepRowHTML } from '../scaleboard';
29
+ import Fretboard from '../Fretboard.svelte';
30
+ import Keyboard from '../Keyboard.svelte';
31
+ import StaffRun from '../StaffRun.svelte';
32
+ import RefBar from '../RefBar.svelte';
33
+
34
+ const { back }: { back?: { label: string; to: string } } = $props();
35
+
36
+ const scale = $derived(scaleDef(XS.sid));
37
+ const notes = $derived(scaleNotes(XS.sid, XS.root));
38
+ const board = $derived(scaleBoard(XS.sid, XS.root, XS.start, XS.labels));
39
+ /** How far apart the keyboard lights the notes as the scale plays, in seconds. */
40
+ const FOLLOW_STEP_S = 0.33;
41
+ /** C3: the keyboard picture starts at the C at or below this plus the root. */
42
+ const KEYBOARD_BASE = 48;
43
+ const LABEL_CHOICES: [BoxLabels, string][] = [
44
+ ['deg', 'Degrees'],
45
+ ['note', 'Notes'],
46
+ ['fin', 'Fingers'],
47
+ ];
48
+ const START_CHOICES: [number, string][] = [
49
+ [3, 'E string'],
50
+ [2, 'A string'],
51
+ ];
52
+ const isRootNote = (semis: number) => semis % 12 === 0;
53
+ const total = (steps: number[]) => steps.reduce((sum, step) => sum + step, 0);
54
+ const shapeMidis = (shape: { pos: { s: number; f: number }[] }) =>
55
+ shape.pos.map((pos) => STRINGS[pos.s].open + pos.f);
56
+ function hear() {
57
+ playScale(XS.sid, XS.root, XS.start);
58
+ /* follow mode: the keys light in time with the notes */
59
+ kbFollow(shapeMidis(scaleShape(XS.sid, XS.root, XS.start, 1)), FOLLOW_STEP_S);
60
+ }
61
+ </script>
62
+
63
+ <RefBar title="Explorer" {back} resume={false} />
64
+
65
+ <div class="sec">
66
+ <div class="card stack">
67
+ <div>
68
+ <div style="font-family:var(--f-disp);font-size:24px;letter-spacing:.02em">
69
+ {rootName(XS.root) + ' ' + scale.n.toLowerCase()}
70
+ </div>
71
+ <p class="note" style="margin:4px 0 0">{scale.d}</p>
72
+ </div>
73
+ {@html stepRowHTML(XS.sid, XS.root)}
74
+ <p class="note" style="margin:0">
75
+ Big numbers are the notes, small ones their degree. The circles between them are the <b
76
+ >steps</b
77
+ >
78
+ — how many frets from each note to the next. They come to {total(scaleSteps(XS.sid))}, because
79
+ that is one octave.
80
+ </p>
81
+ <div
82
+ class="fbwrap"
83
+ use:hscroll={{
84
+ frac: fretFrac(board.cf, board.lo, board.hi),
85
+ key: XS.sid + XS.root + XS.start,
86
+ }}
87
+ >
88
+ <Fretboard lo={board.lo} hi={board.hi} cells={board.cells} />
89
+ </div>
90
+ <div class="bar">
91
+ <span class="hint">Show</span>
92
+ {#each LABEL_CHOICES as [labels, title] (labels)}
93
+ <button
94
+ class="chip sm"
95
+ data-xs="labels"
96
+ data-val={labels}
97
+ aria-pressed={XS.labels === labels}
98
+ onclick={() => {
99
+ XS.labels = labels;
100
+ }}>{title}</button
101
+ >
102
+ {/each}
103
+ </div>
104
+ <div class="bar">
105
+ <span class="hint">From</span>
106
+ {#each START_CHOICES as [string, title] (string)}
107
+ <button
108
+ class="chip sm"
109
+ data-xs="start"
110
+ data-val={string}
111
+ aria-pressed={XS.start === string}
112
+ onclick={() => {
113
+ XS.start = string;
114
+ }}>{title}</button
115
+ >
116
+ {/each}
117
+ <span class="spacer"></span><button class="ghost" id="hearscale" onclick={hear}
118
+ >♪ Hear it</button
119
+ >
120
+ </div>
121
+ <p class="note">
122
+ Hollow rings are every other place that note occurs on the neck. The filled dots are one
123
+ playable box{board.shape.clean
124
+ ? ', all inside a four-fret span.'
125
+ : ' — this one needs a position shift, marked by the finger numbers jumping back.'}
126
+ </p>
127
+ </div>
128
+ </div>
129
+
130
+ <div class="sec">
131
+ <h2>On the staff</h2>
132
+ <div class="card">
133
+ <div class="fbwrap">
134
+ <StaffRun midis={shapeMidis(board.shape)} pref={honmoon.settings.pref} tappable={false} />
135
+ </div>
136
+ </div>
137
+ </div>
138
+
139
+ <!-- The same scale on a keyboard. On a fretboard every step looks alike, so the
140
+ shape of a scale is invisible; here the half steps are drawn for you —
141
+ they are the places where two lit white keys touch. -->
142
+ <div class="sec">
143
+ <h2>On the keyboard</h2>
144
+ <div class="card stack">
145
+ <Keyboard
146
+ lo={kbBase(KEYBOARD_BASE + XS.root)}
147
+ oct={1}
148
+ tap="play"
149
+ cells={notes.map((note) => ({
150
+ pc: note.pc,
151
+ fill: isRootNote(note.semis) ? 'var(--accent)' : 'var(--good)',
152
+ label: XS.labels === 'note' ? noteText(note) : degPretty(note.deg),
153
+ }))}
154
+ />
155
+ <p class="note" style="margin:0">
156
+ Tap a key to hear it. {@html kbHalfStepLine(XS.sid, XS.root)}
157
+ </p>
158
+ </div>
159
+ </div>
160
+
161
+ <div class="sec">
162
+ <h2>Reading the steps</h2>
163
+ <div class="card stack">
164
+ <p class="note">
165
+ A <b>step</b> is the distance from one note of a scale to the next, counted in frets. One fret
166
+ is a <b>half step</b> — a semitone. Two frets is a <b>whole step</b>. That is the whole
167
+ vocabulary.
168
+ </p>
169
+ <p class="note">
170
+ Every scale is a recipe of steps from its root, and the recipe is what makes it sound the way
171
+ it does. Major is <b>whole whole half whole whole whole half</b>; play that from any note and
172
+ you have that key's major scale. On the bass the shape does not change at all when you move
173
+ key — it just slides up the neck. That is the one real advantage a fretted instrument has over
174
+ a piano, and it is why bassists think in shapes.
175
+ </p>
176
+ <p class="note">
177
+ Pentatonics leave notes out, so they have bigger gaps — minor pentatonic goes <b>3 2 2 3 2</b
178
+ >. Those three-fret jumps are exactly why it sounds open and singable rather than stepwise,
179
+ and why it is so hard to play a wrong note with it.
180
+ </p>
181
+ <p class="note">
182
+ Whatever the scale, the steps add up to <b>12</b>, because twelve semitones is an octave and
183
+ the scale has to arrive home.
184
+ </p>
185
+ </div>
186
+ </div>
187
+
188
+ <div class="sec">
189
+ <h2>Reading the degrees</h2>
190
+ <div class="card stack">
191
+ <p class="note">
192
+ The small number under each note is its <b>degree</b> — which note of the scale it is, counting
193
+ the root as 1.
194
+ </p>
195
+ <p class="note">
196
+ A ♭ or ♯ in front means it is a semitone lower or higher <i
197
+ >than it would be in the major scale</i
198
+ >. The major scale is the ruler everything else is measured against. So ♭3 means “a semitone
199
+ below the 3 you would get in major” — and that single semitone is the entire difference
200
+ between a major and a minor chord.
201
+ </p>
202
+ <p class="note">
203
+ This is why degrees are worth more to you than note names. <b>The ♭7</b> means the same thing
204
+ in every key, and a line built on 1 ♭3 4 5 works wherever you put it. <b>B♭</b> only means something
205
+ once somebody tells you what key you are in.
206
+ </p>
207
+ {#if hasRoute('degref')}<div class="bar">
208
+ <button class="ghost" data-go="degref" onclick={() => go('degref')}
209
+ >More on degrees, with pictures →</button
210
+ >
211
+ </div>{/if}
212
+ </div>
213
+ </div>
214
+
215
+ <div class="sec">
216
+ <h2>Root</h2>
217
+ <div class="levels">
218
+ {#each rootPool() as root (root)}<button
219
+ class="chip sm"
220
+ data-xs="root"
221
+ data-val={root}
222
+ aria-pressed={XS.root === root}
223
+ onclick={() => {
224
+ XS.root = root;
225
+ }}>{rootName(root)}</button
226
+ >{/each}
227
+ </div>
228
+ </div>
229
+
230
+ {#each SGROUPS as group (group)}
231
+ <div class="sec">
232
+ <h2>{group}</h2>
233
+ <div class="levels">
234
+ {#each SCALES.filter((def) => def.g === group) as def (def.id)}<button
235
+ class="chip sm"
236
+ data-xs="sid"
237
+ data-val={def.id}
238
+ aria-pressed={XS.sid === def.id}
239
+ onclick={() => {
240
+ XS.sid = def.id;
241
+ }}>{def.n}</button
242
+ >{/each}
243
+ </div>
244
+ </div>
245
+ {/each}
@@ -0,0 +1,280 @@
1
+ <!-- One family: what it is about, what it keeps next to its drills, and the drills. -->
2
+ <script lang="ts" module>
3
+ /* Rolled up by default, and it stays as you left it across re-draws — a
4
+ panel that snapped shut every time you toggled a chip would be unusable. */
5
+ const FOLD: Record<string, boolean> = $state({});
6
+ </script>
7
+
8
+ <script lang="ts">
9
+ import type { Snippet } from 'svelte';
10
+ import { honmoon, setSetting, queueSave, saveLocal } from '../../store/honmoon.svelte';
11
+ import { LEVELS, accOn, lvl, positions } from '../../music/bass';
12
+ import { staffNotes, multiClef } from '../../music/clefs';
13
+ import { midiName } from '../../music/notes';
14
+ import { vRange } from '../../music/voice';
15
+ import { IC } from '../../notation/icons';
16
+ import { drillOn, famDrills, famHidden, famNoBass, groupDef } from '../../drills/registry.svelte';
17
+ import { drillMastery } from '../../learn/progress';
18
+ import { startDrill } from '../../drills/runtime.svelte';
19
+ import { go, hasRoute } from '../../nav.svelte';
20
+ import { toggledIn } from '../../util/list';
21
+ import AccRow from '../AccRow.svelte';
22
+ import ScalesScreen from './ScalesScreen.svelte';
23
+
24
+ interface Props {
25
+ fam: string;
26
+ /** where the voice family's "Vocal warm-up" button goes */
27
+ voiceWarmup?: string;
28
+ /** anything an app wants to add above the drill list */
29
+ extra?: Snippet<[string]>;
30
+ }
31
+ const { fam, voiceWarmup = 'warmhome', extra }: Props = $props();
32
+
33
+ const group = $derived(groupDef(fam));
34
+ const level = $derived(lvl());
35
+ const drills = $derived(famDrills(fam));
36
+ const noBassCount = $derived(famNoBass(fam));
37
+ const hiddenCount = $derived(famHidden(fam));
38
+ const offHere = $derived(drills.filter((def) => !drillOn(def.id)).length);
39
+ const open = $derived(FOLD[fam] || offHere > 0);
40
+ interface ReadingLink {
41
+ route: string;
42
+ title: string;
43
+ blurb: string;
44
+ }
45
+ /** The reference pages worth reading alongside this family. */
46
+ const refs = $derived.by(() => {
47
+ const links: ReadingLink[] = [];
48
+ if (fam === 'read') {
49
+ links.push({
50
+ route: 'ref',
51
+ title: 'Staff map',
52
+ blurb:
53
+ 'Every note on ' +
54
+ (multiClef() ? 'each clef you have on' : 'the bass clef') +
55
+ ', what it is called and where to play it.',
56
+ });
57
+ links.push({
58
+ route: 'rref',
59
+ title: 'How rhythm is written',
60
+ blurb: 'Values, dots, ties, rests, time signatures and how to count a bar out loud.',
61
+ });
62
+ }
63
+ if (fam === 'ear') {
64
+ links.push({
65
+ route: 'ivref',
66
+ title: 'What an interval is',
67
+ blurb: 'All twelve, what they sound like, and why one shape works in every key.',
68
+ });
69
+ }
70
+ return links.filter((link) => hasRoute(link.route));
71
+ });
72
+
73
+ function toggleDoff(id: string) {
74
+ honmoon.settings.doff = toggledIn(honmoon.settings.doff || [], id);
75
+ saveLocal();
76
+ queueSave();
77
+ }
78
+ </script>
79
+
80
+ {#if fam === 'scale'}
81
+ <ScalesScreen />
82
+ {:else if group}
83
+ <div class="sec">
84
+ <div class="bar">
85
+ <button class="ghost" id="back" onclick={() => go('home')}>← Practice</button>
86
+ <span class="spacer"></span><span class="tick">{group.need}</span>
87
+ </div>
88
+ </div>
89
+ <div class="sec">
90
+ <h2>{group.n}</h2>
91
+ <div class="card"><p class="note" style="margin:0">{group.sb}</p></div>
92
+ </div>
93
+
94
+ {#if fam === 'neck'}
95
+ <div class="sec">
96
+ <h2>Range</h2>
97
+ <div class="levels" id="lvls">
98
+ {#each LEVELS as range (range.id)}
99
+ <button
100
+ class="chip"
101
+ data-lvl={range.id}
102
+ aria-pressed={range.id === level.id}
103
+ onclick={() => setSetting('level', range.id)}>{range.name}</button
104
+ >
105
+ {/each}
106
+ </div>
107
+ <div class="card" style="margin-top:10px"><AccRow /></div>
108
+ <p class="note">
109
+ {level.desc}{accOn() ? ', sharps and flats included' : ', natural notes only'} — {positions()
110
+ .length} fretboard positions, {staffNotes().length} distinct notes on the staff. Both of these
111
+ apply everywhere, not only here: the reading drills, the daily session and the maps on Stats all
112
+ follow them.
113
+ </p>
114
+ </div>
115
+ {/if}
116
+ {#if fam === 'ear'}
117
+ <div class="sec">
118
+ <h2>Intervals in rotation</h2>
119
+ <div class="levels">
120
+ <button
121
+ class="chip sm"
122
+ data-set="ivset"
123
+ data-val="basic"
124
+ aria-pressed={honmoon.settings.ivset !== 'all'}
125
+ onclick={() => setSetting('ivset', 'basic')}>The common six</button
126
+ >
127
+ <button
128
+ class="chip sm"
129
+ data-set="ivset"
130
+ data-val="all"
131
+ aria-pressed={honmoon.settings.ivset === 'all'}
132
+ onclick={() => setSetting('ivset', 'all')}>All twelve</button
133
+ >
134
+ </div>
135
+ <p class="note">
136
+ {drills.some((def) => def.id === 'scalehear')
137
+ ? 'Hear the scale draws on whatever is switched on over in Scales, not on this row. '
138
+ : ''}Start with 2nds, 3rds, 4ths, 5ths and the octave — they cover most of what you will
139
+ meet. The full set adds the tritone, 6ths and 7ths{drills.some((def) => def.id === 'chhear')
140
+ ? ', and both chord drills widen from major/minor to seven types'
141
+ : ''}.
142
+ </p>
143
+ </div>
144
+ {/if}
145
+ {#if fam === 'voice'}
146
+ {@const voiceRange = vRange()}
147
+ <div class="sec">
148
+ <h2>Your voice</h2>
149
+ <div class="card stack">
150
+ <div class="setrow" style="border:none;padding:0">
151
+ <span class="lbl">Range</span><span class="tick"
152
+ >{voiceRange.set
153
+ ? midiName(voiceRange.lo) + ' – ' + midiName(voiceRange.hi)
154
+ : 'not measured — using ' +
155
+ midiName(voiceRange.lo) +
156
+ ' – ' +
157
+ midiName(voiceRange.hi)}</span
158
+ >
159
+ </div>
160
+ <p class="note">
161
+ Everything sung here is transposed to sit inside that range, so nothing asks you for a
162
+ note you do not have.
163
+ </p>
164
+ <div class="bar">
165
+ {#if hasRoute('range')}<button
166
+ class="ghost"
167
+ data-go="range"
168
+ onclick={() => go('range', { from: 'fam' })}>Find my range</button
169
+ >{/if}
170
+ <span class="spacer"></span>
171
+ {#if hasRoute(voiceWarmup)}<button
172
+ class="ghost"
173
+ id="tovoicewarm"
174
+ onclick={() => go(voiceWarmup)}>Vocal warm-up</button
175
+ >{/if}
176
+ </div>
177
+ </div>
178
+ </div>
179
+ {/if}
180
+ {#if noBassCount}
181
+ <div class="sec">
182
+ <div class="card stack">
183
+ <p class="note">
184
+ {noBassCount === 1 ? 'One drill here is' : noBassCount + ' drills here are'} put away because
185
+ you said the bass is not with you. Nothing else in this family needs it.
186
+ </p>
187
+ <div class="bar">
188
+ <button
189
+ class="ghost"
190
+ data-set="bass"
191
+ data-val="true"
192
+ onclick={() => setSetting('bass', true)}>I have it with me</button
193
+ >
194
+ </div>
195
+ </div>
196
+ </div>
197
+ {/if}
198
+ {#if hiddenCount}
199
+ <div class="sec">
200
+ <div class="card stack">
201
+ <p class="note">
202
+ {hiddenCount === 1 ? 'One more drill here is' : hiddenCount + ' more drills here are'} switched
203
+ off: movable do and scale degrees — hearing a note as <i>the fifth of this key</i> rather
204
+ than as
205
+ <i>G</i>. It is a second language, and worth starting once what is below feels easy.
206
+ </p>
207
+ {#if hasRoute('setup')}<div class="bar">
208
+ <button class="ghost" data-go="setup" onclick={() => go('setup')}
209
+ >Turn it on in Setup</button
210
+ >
211
+ </div>{/if}
212
+ </div>
213
+ </div>
214
+ {/if}
215
+ {@render extra?.(fam)}
216
+
217
+ {#if refs.length}
218
+ <div class="sec">
219
+ <h2>Read about it</h2>
220
+ {#each refs as link (link.route)}
221
+ <button class="drill ref" data-go={link.route} onclick={() => go(link.route)}
222
+ ><span class="ic">{@html IC.book}</span><span
223
+ ><span class="nm">{link.title}</span><br /><span class="sb">{link.blurb}</span></span
224
+ ><span class="mast">→</span></button
225
+ >
226
+ {/each}
227
+ </div>
228
+ {/if}
229
+
230
+ <div class="sec">
231
+ <h2>Drills</h2>
232
+ <div class="drills">
233
+ {#each drills as def (def.id)}
234
+ <button
235
+ class="drill"
236
+ class:muted={!drillOn(def.id)}
237
+ data-drill={def.id}
238
+ onclick={() => startDrill(def.id, fam)}
239
+ ><span class="ic">{@html IC[def.ic]}</span>
240
+ <span
241
+ ><span class="nm"
242
+ >{def.nm}{#if !drillOn(def.id)}
243
+ <i class="offtag">not in the session</i>{/if}</span
244
+ >
245
+ <br /><span class="sb">{def.sb}</span></span
246
+ ><span class="mast">{drillMastery(def.id).pct}%</span></button
247
+ >
248
+ {/each}
249
+ </div>
250
+ <div class="fold" class:open>
251
+ <button
252
+ class="foldhead"
253
+ data-fold={fam}
254
+ aria-expanded={open}
255
+ onclick={() => {
256
+ FOLD[fam] = !FOLD[fam];
257
+ }}
258
+ >In the daily session{offHere ? ' · ' + offHere + ' off' : ''}<span class="caret"
259
+ >{open ? '▴' : '▾'}</span
260
+ ></button
261
+ >
262
+ {#if open}
263
+ <div class="levels" style="margin-top:10px">
264
+ {#each drills as def (def.id)}
265
+ <button
266
+ class="chip sm tog"
267
+ data-doff={def.id}
268
+ aria-pressed={drillOn(def.id)}
269
+ onclick={() => toggleDoff(def.id)}>{def.nm}</button
270
+ >
271
+ {/each}
272
+ </div>
273
+ <p class="note">
274
+ Switch one off and the daily session stops asking for it. It stays in the list above, so
275
+ you can still open it whenever you want to give it another go.
276
+ </p>
277
+ {/if}
278
+ </div>
279
+ </div>
280
+ {/if}
@@ -0,0 +1,65 @@
1
+ <!--
2
+ Every word the app uses at you, defined in one line each. The point is not
3
+ to teach the subject — the references do that — but to stop a single
4
+ unfamiliar word from making a whole drill unreadable. Written so that each
5
+ entry stands on its own: you arrive here having met one term, not to read
6
+ the list end to end.
7
+ -->
8
+ <script lang="ts">
9
+ import { GLOSS } from '../refdata';
10
+ import RefBar from '../RefBar.svelte';
11
+ const { back }: { back?: { label: string; to: string } } = $props();
12
+
13
+ let search = $state('');
14
+ /** The text of an HTML snippet, tags dropped. */
15
+ const plain = (html: string) => html.replace(/<[^>]+>/g, '').replace(/&amp;/g, '&');
16
+ const shown = $derived.by(() => {
17
+ const wanted = search.trim().toLowerCase();
18
+ return GLOSS.map((section) => ({
19
+ g: section.g,
20
+ items: section.items.filter(
21
+ ([term, meaning]) =>
22
+ !wanted || (plain(term) + plain(meaning)).toLowerCase().includes(wanted),
23
+ ),
24
+ }));
25
+ });
26
+ const none = $derived(shown.every((section) => !section.items.length));
27
+ </script>
28
+
29
+ <RefBar title="Vocabulary" {back} resume={false} />
30
+ <div class="sec">
31
+ <div class="card stack">
32
+ <p class="note" style="margin:0">
33
+ Every term the drills use at you, one line each. Type to narrow it down.
34
+ </p>
35
+ <input
36
+ class="gfind"
37
+ id="glossq"
38
+ type="search"
39
+ placeholder="Search a word…"
40
+ aria-label="search the vocabulary"
41
+ autocomplete="off"
42
+ bind:value={search}
43
+ />
44
+ <p class="note" id="glossnone" hidden={!none} style="margin:0">Nothing matches that.</p>
45
+ </div>
46
+ </div>
47
+ {#each shown as section (section.g)}
48
+ <div class="sec gsec" hidden={!section.items.length}>
49
+ <h2>{section.g}</h2>
50
+ <div class="card stack">
51
+ {#each section.items as [term, meaning] (term)}<div class="gterm">
52
+ <b>{@html term}</b>
53
+ <p>{@html meaning}</p>
54
+ </div>{/each}
55
+ </div>
56
+ </div>
57
+ {/each}
58
+ <div class="sec">
59
+ <div class="card">
60
+ <p class="note" style="margin:0">
61
+ Still stuck on a word? The references go into each of these properly — this page is only here
62
+ so that one unfamiliar term cannot make a whole drill unreadable.
63
+ </p>
64
+ </div>
65
+ </div>
@@ -0,0 +1,37 @@
1
+ <!-- What an interval is: all twelve, what they sound like, and why one shape works in every key. -->
2
+ <script lang="ts">
3
+ import { IVS } from '../../music/intervals';
4
+ import { playInterval } from '../../audio/synth';
5
+ import RefBar from '../RefBar.svelte';
6
+ const { back }: { back?: { label: string; to: string } } = $props();
7
+ /** Every interval is played up from E3. */
8
+ const START_NOTE = 52;
9
+ </script>
10
+
11
+ <RefBar title="Intervals" {back} />
12
+ <div class="sec">
13
+ <div class="card">
14
+ <p class="note">
15
+ An interval is the distance between two notes, counted in semitones — on bass, in frets. It is
16
+ the same distance wherever you play it, which is why one shape works in every key, and it is
17
+ what you are actually doing when you sing: you do not sing frets, you sing distances.
18
+ </p>
19
+ </div>
20
+ </div>
21
+ <div class="sec">
22
+ <h2>The twelve</h2>
23
+ <div class="card stack">
24
+ {#each IVS as interval (interval.s)}
25
+ <div class="scrow on">
26
+ <button
27
+ class="chip sm"
28
+ data-ivplay={interval.s}
29
+ onclick={() => playInterval(START_NOTE, interval.s, false)}>{interval.ab}</button
30
+ >
31
+ <span class="ivs">{interval.n} · {interval.s}{interval.s === 1 ? ' fret' : ' frets'}</span>
32
+ <p class="note">{interval.c}</p>
33
+ </div>
34
+ {/each}
35
+ </div>
36
+ <p class="note">Tap any of them to hear it played from the same starting note.</p>
37
+ </div>
@@ -0,0 +1,44 @@
1
+ <!--
2
+ Every explanation in the app, in one place. They are still linked from the
3
+ family that uses them, but you should not have to remember which drill owns
4
+ an idea in order to go and read about it.
5
+ -->
6
+ <script lang="ts">
7
+ import { IC } from '../../notation/icons';
8
+ import { go, hasRoute } from '../../nav.svelte';
9
+ import { LEARN } from '../refdata';
10
+ const items = LEARN.filter((item) => hasRoute(item.go));
11
+ </script>
12
+
13
+ <div class="sec">
14
+ <div class="card">
15
+ <p class="note" style="margin:0">
16
+ {items.length} references. Nothing here is a test — read them when a drill turns up something you
17
+ do not recognise, or when you want the idea before the practice.
18
+ </p>
19
+ </div>
20
+ </div>
21
+ <div class="sec">
22
+ <h2>References</h2>
23
+ {#each items as item (item.go)}
24
+ <button class="drill ref" data-go={item.go} onclick={() => go(item.go)}
25
+ ><span class="ic">{@html IC.book}</span><span
26
+ ><span class="nm">{item.n}</span><br /><span class="sb">{item.sb}</span></span
27
+ ><span class="mast">→</span></button
28
+ >
29
+ {/each}
30
+ </div>
31
+ {#if hasRoute('setup')}
32
+ <div class="sec">
33
+ <div class="card">
34
+ <p class="note" style="margin:0">
35
+ How the app decides what to ask you, and why bass is written an octave high, are under <b
36
+ >Setup</b
37
+ >.
38
+ </p>
39
+ <div class="bar" style="margin-top:8px">
40
+ <button class="ghost" onclick={() => go('setup')}>How this works</button>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ {/if}