Footer tools stacked in all modes; fix silent phrase-mode metronome

Layout: tuner + metronome now stack vertically everywhere (was phrase-only).

Phrase metronome bug: clicks were gated on rhythm.active, but phrase mode
has no on/off switch (the sync checkbox replaces it) and the default is
active:false — so a synced run stayed silent with no in-mode remedy.
Now: phrase mode is always armed (sync checkbox = free vs synced,
sound checkbox = clicks), and phrase clickSound reads rhythm.sound only.
Single-note modes keep the rhythm.active on/off semantics.
This commit is contained in:
2026-09-20 00:10:37 +02:00
parent c1f8f46cfe
commit e43a17efb3
3 changed files with 17 additions and 11 deletions

View File

@@ -68,8 +68,8 @@ export const Controls: React.FC<ControlsProps> = ({ currentPitch }) => {
)}
{shouldShowFilters && <TargetNoteControls />}
{/* Bottom Section: Tools Grid (2 Columns now) */}
<div className="tools-grid" style={inPhraseMode ? { gridTemplateColumns: '1fr' } : undefined}>
{/* Bottom Section: Tools Grid (stacked in all modes) */}
<div className="tools-grid" style={{ gridTemplateColumns: '1fr' }}>
{/* Tool 1: Tuner */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', border: '1px solid rgba(128,128,128,0.2)', padding: '12px', borderRadius: '8px' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>

View File

@@ -55,10 +55,15 @@ export const MetronomeWidget: React.FC<MetronomeWidgetProps> = ({
const isSync = syncMode === 'option' && !!rhythm.syncToExercise;
const runActive = gate === true;
// Phrase mode (syncMode 'option') has no on/off switch — the sync
// checkbox replaces it (unchecked = free metronome, checked = synced to
// the exercise), so the metronome is always armed there. Gating on
// rhythm.active would leave phrase users with no way to enable clicks.
const armed = syncMode === 'option' ? true : rhythm.active;
// Sync: pendulum/click only while the run is active (the run's scheduler
// is the click source; the widget's own scheduler stays off).
// Free/plain: the widget's own scheduler runs whenever armed.
const running = isSync ? runActive : rhythm.active;
const running = isSync ? runActive : armed;
const { restart } = useMetronome({
bpm: rhythm.bpm,
@@ -78,7 +83,6 @@ export const MetronomeWidget: React.FC<MetronomeWidgetProps> = ({
}, [isSync, runActive]);
const beatMs = 60000 / Math.max(1, rhythm.bpm);
const armed = rhythm.active;
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>

View File

@@ -85,13 +85,15 @@ export const PhraseTrainer = forwardRef<PhraseHandle, PhraseTrainerProps>(
const pace: PhraseSettings['pace'] = settings.rhythm.syncToExercise ? 'tempo' : 'step';
phrase.pace = pace;
// Click decision — ONE source: the metronome widget's sound checkbox.
// - Sync (tempo): run clicks when armed && sound (count-in incl.);
// widget silent while idle.
// - Free metronome: widget clicks freely; the run stays silent to
// avoid doubling (sync off in tempo = free widget during runs).
phrase.clickSound = pace === 'tempo'
? settings.rhythm.active && settings.rhythm.sound
: settings.rhythm.active && settings.rhythm.sound && !settings.rhythm.syncToExercise;
// Phrase mode has no metronome on/off switch (the sync checkbox
// replaces it, see MetronomeWidget), so rhythm.active must NOT gate
// clicks here — with the default active:false a synced run would
// stay silent with no way to fix it from this mode.
// - Sync (tempo): run clicks when sound is on (count-in included);
// the widget stays silent while idle.
// - Free metronome (step): the widget clicks freely; the run stays
// silent to avoid doubling.
phrase.clickSound = settings.rhythm.sound;
const setPhrase = useCallback((updates: Partial<PhraseSettings>) => {
updateSettings(s => ({ ...s, phrase: { ...getPhraseSettings(s), ...updates } }));
}, [updateSettings]);