From 068caf488d98eecc76cefcca8ad92e28ca9dfdcf Mon Sep 17 00:00:00 2001 From: byrongamatos Date: Tue, 23 Jun 2026 12:45:20 +0200 Subject: [PATCH] fix(onboarding): calibration Tuner step no longer exposes the input-select overlay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- plugins/input_setup/screen.js | 19 ++++++++++++-- static/v3/profile.js | 48 +++++++++++++++++++++++------------ 2 files changed, 49 insertions(+), 18 deletions(-) diff --git a/plugins/input_setup/screen.js b/plugins/input_setup/screen.js index 8229dc6c..424c65fb 100644 --- a/plugins/input_setup/screen.js +++ b/plugins/input_setup/screen.js @@ -163,10 +163,25 @@ host.querySelector('[data-is-cal]').addEventListener('click', () => { 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({ instrument: inst, - onDone: () => advance(inst, true), - onCancel: () => { /* stay on this panel; user can skip or retry */ }, + onDone: () => { restore(); advance(inst, true); }, + onCancel: () => { restore(); /* stay on this panel; user can skip or retry */ }, }); } else { advance(inst, true); diff --git a/static/v3/profile.js b/static/v3/profile.js index 8f522498..b46440c9 100644 --- a/static/v3/profile.js +++ b/static/v3/profile.js @@ -222,23 +222,39 @@ if (!instruments.length) return; // Don't let a plugin-load race skip the mandatory input-setup step. if (!(await waitForInputSetup(8000))) return; - const caps = window.feedBack && window.feedBack.capabilities; - if (!caps || typeof caps.command !== 'function') { - try { await window.feedBackInputSetup.launch(instruments); } catch (e) { /* proceed */ } - return; + // Hide this onboarding modal while the input-setup wizard (its own + // full-screen overlay) runs on top. Otherwise both stay stacked, and when + // the note-detect calibration wizard minimizes to expose the Tuner the + // 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) {