fix(onboarding): calibration Tuner step no longer exposes the input-select overlay (#577)

Tester: "at the tune step, pressing the Tuner button starts a second wizard at
the input-select step."

Root cause is stacked full-screen overlays. During onboarding the input-setup
flow runs as #input-setup-overlay (z-210) on top of the onboarding modal
#v3-onboarding (z-200), and note_detect's Calibration Wizard (z-300) launches on
top of that. When the player opens the Tuner, that wizard minimizes itself to
transparent + pointer-events:none so the Tuner (z-1000) is usable — but the
input-setup overlay underneath, still showing its "select your input" card, then
shows through behind the floating tuner and reads as a second wizard.

Two targeted hides so only the active surface is visible:
- input_setup: hide #input-setup-overlay while launchCalibration runs; restore on
  its onDone/onCancel (one always fires on close), so the calibration wizard /
  tuner own the screen.
- onboarding runInputSetup: hide #v3-onboarding for the whole input-setup phase
  (its own overlay replaces it visually); restore in finally before advancing to
  the calibration-challenge step.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Byron Gamatos
2026-06-23 12:51:59 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 7399a2ac63
commit a43e7b13be
2 changed files with 49 additions and 18 deletions
+17 -2
View File
@@ -163,10 +163,25 @@
host.querySelector('[data-is-cal]').addEventListener('click', () => { host.querySelector('[data-is-cal]').addEventListener('click', () => {
if (hasDetector) { if (hasDetector) {
// Hide our own full-screen overlay while note_detect's
// Calibration Wizard runs on top. That wizard goes
// transparent (pointer-events:none) when it minimizes to
// expose the Tuner; if our overlay stayed up it would show
// through — covering the tuner with the still-mounted
// "select your input" card and looking like a second
// wizard at the input step. Restore it on done/cancel
// (one of which always fires when that wizard closes).
const ov = document.getElementById('input-setup-overlay');
const prevDisplay = ov ? ov.style.display : '';
if (ov) ov.style.display = 'none';
const restore = () => {
const o = document.getElementById('input-setup-overlay');
if (o) o.style.display = prevDisplay;
};
window.noteDetect.launchCalibration({ window.noteDetect.launchCalibration({
instrument: inst, instrument: inst,
onDone: () => advance(inst, true), onDone: () => { restore(); advance(inst, true); },
onCancel: () => { /* stay on this panel; user can skip or retry */ }, onCancel: () => { restore(); /* stay on this panel; user can skip or retry */ },
}); });
} else { } else {
advance(inst, true); advance(inst, true);
+32 -16
View File
@@ -222,23 +222,39 @@
if (!instruments.length) return; if (!instruments.length) return;
// Don't let a plugin-load race skip the mandatory input-setup step. // Don't let a plugin-load race skip the mandatory input-setup step.
if (!(await waitForInputSetup(8000))) return; if (!(await waitForInputSetup(8000))) return;
const caps = window.feedBack && window.feedBack.capabilities; // Hide this onboarding modal while the input-setup wizard (its own
if (!caps || typeof caps.command !== 'function') { // full-screen overlay) runs on top. Otherwise both stay stacked, and when
try { await window.feedBackInputSetup.launch(instruments); } catch (e) { /* proceed */ } // the note-detect calibration wizard minimizes to expose the Tuner the
return; // onboarding modal shows through behind the tuner. Restored in `finally`
// before we advance to the calibration-challenge step.
const ob = document.getElementById('v3-onboarding');
const obPrevDisplay = ob ? ob.style.display : '';
if (ob) ob.style.display = 'none';
const restoreOnboarding = () => {
const o = document.getElementById('v3-onboarding');
if (o) o.style.display = obPrevDisplay;
};
try {
const caps = window.feedBack && window.feedBack.capabilities;
if (!caps || typeof caps.command !== 'function') {
try { await window.feedBackInputSetup.launch(instruments); } catch (e) { /* proceed */ }
return;
}
await new Promise((resolve) => {
let settled = false;
let unsub = null;
const done = () => { if (settled) return; settled = true; try { unsub && unsub(); } catch (e) { /* noop */ } resolve(); };
try { unsub = typeof caps.subscribe === 'function' ? caps.subscribe('input-calibration:calibration-done', done) : null; } catch (e) { unsub = null; }
// `run` is fire-and-launch; completion arrives via the event above.
// A non-handled outcome (no owner / plugin absent / error) means
// nothing was launched, so proceed immediately.
caps.command('input-calibration', 'run', { requester: 'onboarding', payload: { instruments } })
.then((r) => { if (!r || r.outcome !== 'handled') done(); })
.catch(() => done());
});
} finally {
restoreOnboarding();
} }
await new Promise((resolve) => {
let settled = false;
let unsub = null;
const done = () => { if (settled) return; settled = true; try { unsub && unsub(); } catch (e) { /* noop */ } resolve(); };
try { unsub = typeof caps.subscribe === 'function' ? caps.subscribe('input-calibration:calibration-done', done) : null; } catch (e) { unsub = null; }
// `run` is fire-and-launch; completion arrives via the event above.
// A non-handled outcome (no owner / plugin absent / error) means
// nothing was launched, so proceed immediately.
caps.command('input-calibration', 'run', { requester: 'onboarding', payload: { instruments } })
.then((r) => { if (!r || r.outcome !== 'handled') done(); })
.catch(() => done());
});
} }
function show(profile, opts) { function show(profile, opts) {