mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-12 11:49:28 +00:00
feat(onboarding): first-run home tour (spotlight coach marks) (#528)
* style(tour): align tour engine + Shepherd bubbles to the v3 fb-* palette The tour/help engine shipped its own indigo/blue dark palette (#181830 / #4080e0) that predates the v3 fee[dB]ack tokens, and the spotlight bubbles themselves used the vendored Shepherd LIGHT default (white card, black text) — both clashed with the navy/sky v3 UI behind them. - Recolor the "?" menu button, popover and first-visit toast to the fb-* tokens (card #1e293b, primary #0ea5e9, border #334155, text #f8fafc/#94a3b8, gold #e8c040 unchanged). - Add a dark .shepherd-* override block (loads after the vendored shepherd.css, which is left pristine for upgrades): dark bubble + arrow, fb-primary Next/Done button, slate secondary button, fb text scale, and bump the modal dim to 0.6 to match the onboarding overlay. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(tour-engine): let client/core tours register into the consolidated menu The tour engine only listed server-discovered plugins (those with a tour.json, populated from /api/plugins) in the "?" menu, and always prompted unseen relevant tours via the toast + button pulse. Generalize register() so a core/client-owned tour can participate: - `name` registers the tour into the menu catalog (_tourPlugins) so it shows in the "?" menu even without a server plugin; never clobbers a real plugin entry. - `autoPrompt:false` opts the tour OUT of the unseen toast + pulse (for tours driven programmatically by their owner), while still listing + running on demand. _unseenRelevant honours it. Both options are additive and default to the prior behaviour. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(v3): add stable tour anchors to home cards + instrument badge Give the first-run home tour stable spotlight targets: #v3-hero on the hero panel and data-tour="continue" on the three continue/pick/browse card variants (dashboard.js), and #v3-instrument-wrap on the topbar instrument selector (badges.js, mirroring the existing #v3-tuner-wrap). The other targets (audio routing, tuner, profile, sidebar nav) already had stable ids. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(onboarding): first-run home tour (spotlight coach marks) After a genuine onboarding completion, dim the home page and spotlight one card at a time with an explanatory bubble + Next, reusing the shared tour engine (Shepherd). 7 stops: Hero/Start Playing → Continue/Pick → Instrument selector → Tuner → Audio Routing → Profile → Sidebar nav. Auto-runs once; replayable forever from the "?" tour menu as "Welcome tour". - New static/v3/onboarding-tour.js: registers the spotlight tour (screens: ['v3-home'], name "Welcome tour", autoPrompt:false) and exposes startFirstRun(), gated on the engine's seen/dismissed state so it never repeats; loaded after tour-engine.js + dashboard.js. - profile.js finish(): trigger startFirstRun() only on a real onboarding completion (!editing) — a later profile edit must not relaunch it. Verified headlessly (native core + Playwright): all 7 anchors resolve, the spotlight advances one bubble at a time in the v3 dark theme, completion marks seen, startFirstRun is once-only, and the "?" menu lists "Welcome tour". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(capabilities): clear the handler timeout timer once the race settles Codex round-6: _withTimeout raced the handler promise against a bare setTimeout but never cleared it, so a handler that resolves first leaves the timer alive until it fires. Harmless at 250ms, but the new 15s MIDI permission-command overrides (discover/open-source) kept the event loop alive ~15s after every successful call (and the test process hung that long) and could accumulate delayed callbacks across repeated scans. Capture the timer and clearTimeout it in a .finally on the race. (Domain/capabilities tests now finish in ~0.1s, not 15s.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi-input): give the built-in Web-MIDI provider a distinct participant id Codex round-7: the built-in Web-MIDI provider registered with participantId 'core.midi-input' — the same id as the domain owner. unregisterProvider() unregisters the provider's participant, so a provider swap/hot-reload would tear down the domain OWNER too, leaving midi-input with no owner for later commands. Register the provider as 'core.midi-input.web-midi'. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(onboarding): don't start the home tour when launching the diagnostic Codex round-7: on the final onboarding step, "Play it now" calls finish() (which started the home tour) and THEN playSong(target). startFirstRun() navigated to v3-home and scheduled the tour, then playSong switched to the player — so the tour spotlighted hidden home elements / stole focus from the diagnostic. Gate the tour on a launchingSong flag (passed by the "Play it now" path); the Skip path stays on home, so the tour still runs there. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
fb06e288e1
commit
c7fb074111
+1
-1
@@ -291,7 +291,7 @@
|
||||
const host = document.getElementById('v3-badge-instrument');
|
||||
if (!host) return;
|
||||
host.innerHTML =
|
||||
'<div class="relative">' +
|
||||
'<div id="v3-instrument-wrap" class="relative">' +
|
||||
'<button type="button" data-inst-toggle title="Instrument: ' + esc(settings.string_count + '-str ' + tuningLabel()) + '" ' +
|
||||
'class="bg-fb-card border border-fb-border/50 rounded-2xl h-[92px] w-16 flex flex-col items-center justify-center gap-2 hover:ring-1 hover:ring-fb-primary/40 transition">' +
|
||||
guitarIcon +
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
const bars = Array.from({ length: segs }, (_, i) =>
|
||||
'<span class="flex-1 h-1.5 rounded-full ' + (i < filled ? 'bg-fb-primary' : 'bg-gray-500/40') + '"></span>').join('');
|
||||
continueCard =
|
||||
'<button id="v3-continue" class="group relative text-left rounded-xl overflow-hidden border border-fb-border/50 bg-fb-card aspect-square self-start flex flex-col justify-end">' +
|
||||
'<button id="v3-continue" data-tour="continue" class="group relative text-left rounded-xl overflow-hidden border border-fb-border/50 bg-fb-card aspect-square self-start flex flex-col justify-end">' +
|
||||
songArt(cont.art_url, 'absolute inset-0 w-full h-full object-cover opacity-60 group-hover:opacity-70 transition') +
|
||||
'<div class="absolute inset-0 bg-gradient-to-t from-black/90 via-black/40 to-transparent"></div>' +
|
||||
tuningChip(cont.tuning_name, 'absolute top-3 right-3') +
|
||||
@@ -146,7 +146,7 @@
|
||||
'<span class="absolute top-3 left-3 text-fb-text/80 group-hover:text-fb-text">▶</span></button>';
|
||||
} else if (pick) {
|
||||
continueCard =
|
||||
'<button id="v3-pick" data-fn="' + esc(pick.filename) + '" class="group relative text-left rounded-xl overflow-hidden border border-fb-border/50 bg-fb-card aspect-square self-start flex flex-col justify-end">' +
|
||||
'<button id="v3-pick" data-tour="continue" data-fn="' + esc(pick.filename) + '" class="group relative text-left rounded-xl overflow-hidden border border-fb-border/50 bg-fb-card aspect-square self-start flex flex-col justify-end">' +
|
||||
songArt(libArtUrl(pick), 'absolute inset-0 w-full h-full object-cover opacity-60 group-hover:opacity-70 transition') +
|
||||
'<div class="absolute inset-0 bg-gradient-to-t from-black/90 via-black/40 to-transparent"></div>' +
|
||||
tuningChip(pick.tuning_name, 'absolute top-3 right-3') +
|
||||
@@ -157,7 +157,7 @@
|
||||
'<span class="absolute top-3 left-3 text-fb-text/80 group-hover:text-fb-text">▶</span></button>';
|
||||
} else {
|
||||
continueCard =
|
||||
'<div class="rounded-xl border border-fb-border/50 bg-fb-card/60 aspect-square self-start flex flex-col items-center justify-center text-center p-4">' +
|
||||
'<div data-tour="continue" class="rounded-xl border border-fb-border/50 bg-fb-card/60 aspect-square self-start flex flex-col items-center justify-center text-center p-4">' +
|
||||
'<div class="text-fb-textDim text-sm mb-3">Pick a song to get started</div>' +
|
||||
'<button id="v3-continue-pick" class="bg-fb-card hover:bg-fb-card/70 border border-fb-border/50 text-fb-text text-sm px-4 py-2 rounded-md">Browse library</button></div>';
|
||||
}
|
||||
@@ -188,7 +188,7 @@
|
||||
'<a href="' + esc(changelogUrl) + '" target="_blank" rel="noopener" class="text-fb-primary hover:text-fb-primaryHi">Patch Notes for ' + esc(ver) + '</a>?</p>' : '') +
|
||||
// Featured grid: hero + continue
|
||||
'<div class="grid lg:grid-cols-3 gap-6 mt-6">' +
|
||||
'<div class="lg:col-span-2 relative rounded-xl overflow-hidden min-h-[480px] flex items-center bg-fb-bg">' +
|
||||
'<div id="v3-hero" class="lg:col-span-2 relative rounded-xl overflow-hidden min-h-[480px] flex items-center bg-fb-bg">' +
|
||||
// Hero artwork (neon note-highway), right-anchored. Placeholder
|
||||
// cropped from the design mock — swap static/v3/brand/hero.png for
|
||||
// the designer's high-res original (same path) when available.
|
||||
|
||||
@@ -875,6 +875,10 @@
|
||||
<script src="/static/v3/songs.js"></script>
|
||||
<script src="/static/v3/lessons.js"></script>
|
||||
<script src="/static/v3/dashboard.js"></script>
|
||||
<!-- First-run home tour: spotlights the home cards via the shared tour
|
||||
engine (tour-engine.js, loaded above). Auto-runs once after onboarding
|
||||
(triggered from profile.js finish()); replayable from the "?" menu. -->
|
||||
<script src="/static/v3/onboarding-tour.js"></script>
|
||||
<script src="/static/v3/feedbarcade.js"></script>
|
||||
<script src="/static/v3/player-chrome.js"></script>
|
||||
<script>
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* fee[dB]ack v0.3.0 — first-run home tour.
|
||||
*
|
||||
* Registers a spotlight tour over the home-page cards with the shared tour
|
||||
* engine (window.slopsmithTour / Shepherd) and auto-runs it once, the first
|
||||
* time the user lands on the home page after completing onboarding. It stays
|
||||
* replayable forever from the per-screen "?" tour menu (registered with
|
||||
* screens: ['v3-home']).
|
||||
*
|
||||
* Anchors (all stable, on-screen while #v3-home is active):
|
||||
* #v3-hero hero / Start Playing (dashboard.js)
|
||||
* [data-tour="continue"] continue / pick a song (dashboard.js, 3 variants)
|
||||
* #v3-instrument-wrap instrument selector badge (badges.js, topbar)
|
||||
* #v3-tuner-wrap tuner badge (badges.js, topbar)
|
||||
* #v3-audio-routing audio routing card (dashboard.js)
|
||||
* [data-v3-open-profile] profile badge (profile.js, topbar)
|
||||
* #v3-nav left sidebar navigation (index.html)
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var TOUR_ID = 'home-onboarding';
|
||||
|
||||
// Each step dims the page and spotlights one element (shape: 'spotlight').
|
||||
// waitFor blocks the step until its target exists, so the async dashboard
|
||||
// re-render kicked off by 'v3:profile-updated' can't race the first step.
|
||||
function buildSteps() {
|
||||
return [
|
||||
{
|
||||
id: 'hero', shape: 'spotlight', position: 'bottom',
|
||||
selector: '#v3-hero', waitFor: '#v3-hero',
|
||||
title: 'Welcome to fee[dB]ack',
|
||||
content: 'This is your home base. Hit Start Playing to drop straight into a song from your library.',
|
||||
},
|
||||
{
|
||||
id: 'continue', shape: 'spotlight', position: 'left',
|
||||
selector: '[data-tour="continue"]', waitFor: '[data-tour="continue"]',
|
||||
title: 'Pick up where you left off',
|
||||
content: 'Your last song resumes right here in one click. Before you’ve played anything, it’s a quick random pick to get you going.',
|
||||
},
|
||||
{
|
||||
id: 'instrument', shape: 'spotlight', position: 'bottom',
|
||||
selector: '#v3-instrument-wrap', waitFor: '#v3-instrument-wrap',
|
||||
title: 'Choose your instrument',
|
||||
content: 'Set your instrument, string count and tuning here. The highway, tuner and scoring all adapt to this selection.',
|
||||
},
|
||||
{
|
||||
id: 'tuner', shape: 'spotlight', position: 'bottom',
|
||||
selector: '#v3-tuner-wrap', waitFor: '#v3-tuner-wrap',
|
||||
title: 'Tune up first',
|
||||
content: 'Open the tuner and match each string until the meter centers — accurate tuning means accurate scoring.',
|
||||
},
|
||||
{
|
||||
id: 'audio', shape: 'spotlight', position: 'top',
|
||||
selector: '#v3-audio-routing', waitFor: '#v3-audio-routing',
|
||||
title: 'Your signal path',
|
||||
content: 'Input → amp / NAM / IR → output, at a glance. Set up and monitor your gear from this card.',
|
||||
},
|
||||
{
|
||||
id: 'profile', shape: 'spotlight', position: 'bottom',
|
||||
selector: '[data-v3-open-profile]', waitFor: '[data-v3-open-profile]',
|
||||
title: 'Track your progress',
|
||||
content: 'Your profile, avatar and rank live here. Watch your accuracy climb and level up as you play.',
|
||||
},
|
||||
{
|
||||
id: 'nav', shape: 'spotlight', position: 'right',
|
||||
selector: '#v3-nav', waitFor: '#v3-nav',
|
||||
title: 'Find everything here',
|
||||
content: 'Browse your full library, lessons and plugins anytime. You can replay this tour from the ? button in the corner.',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function register() {
|
||||
var t = window.slopsmithTour;
|
||||
if (!t || typeof t.register !== 'function') return false;
|
||||
t.register(TOUR_ID, {
|
||||
name: 'Welcome tour', // label in the "?" tour menu
|
||||
screens: ['v3-home'], // relevant only on the home screen
|
||||
autoPrompt: false, // we auto-run it from onboarding; no toast nag
|
||||
buildSteps: buildSteps,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// Auto-run once after a genuine onboarding completion. profile.js gates the
|
||||
// call on !editing (a profile edit must not relaunch it); we additionally
|
||||
// honour the engine's own seen/dismissed state so it never repeats and a
|
||||
// dismissal isn't nagged. Stays replayable from the "?" menu either way.
|
||||
function startFirstRun() {
|
||||
var t = window.slopsmithTour;
|
||||
if (!t || typeof t.start !== 'function') return;
|
||||
try {
|
||||
if (t.hasSeen(TOUR_ID) || t.hasDismissed(TOUR_ID)) return;
|
||||
} catch (e) { /* private mode — fall through and attempt once */ }
|
||||
// Make sure the home screen is in view so the spotlight targets exist;
|
||||
// the per-step waitFor handles the async dashboard render.
|
||||
if (typeof window.showScreen === 'function') {
|
||||
try { window.showScreen('v3-home'); } catch (e) { /* best-effort */ }
|
||||
}
|
||||
// Defer a frame so the 'v3:profile-updated' dashboard re-render has a
|
||||
// chance to begin before Shepherd starts polling for the first target.
|
||||
var raf = window.requestAnimationFrame || function (fn) { return setTimeout(fn, 16); };
|
||||
raf(function () { try { t.start(TOUR_ID); } catch (e) { /* degrade */ } });
|
||||
}
|
||||
|
||||
window.v3OnboardingTour = { startFirstRun: startFirstRun };
|
||||
|
||||
// tour-engine.js assigns window.slopsmithTour at script-eval time, so if it
|
||||
// is loaded before us register() succeeds immediately; otherwise retry once
|
||||
// the DOM (and the engine) are ready.
|
||||
if (!register()) {
|
||||
document.addEventListener('DOMContentLoaded', register, { once: true });
|
||||
}
|
||||
})();
|
||||
+13
-2
@@ -462,7 +462,8 @@
|
||||
return body;
|
||||
}
|
||||
|
||||
async function finish() {
|
||||
async function finish(finishOpts) {
|
||||
finishOpts = finishOpts || {};
|
||||
overlay.remove();
|
||||
await fetchProgress();
|
||||
if (window.v3Progression && typeof window.v3Progression.refresh === 'function') {
|
||||
@@ -471,6 +472,16 @@
|
||||
renderBadge();
|
||||
renderProfileScreen();
|
||||
if (window.slopsmith && window.slopsmith.emit) window.slopsmith.emit('v3:profile-updated', _profile);
|
||||
// First-run only: after a genuine onboarding completion (not a
|
||||
// profile edit), kick off the one-time home tour — but NOT when we're
|
||||
// about to launch the diagnostic ("Play it now"), which navigates to
|
||||
// the player; the tour would otherwise spotlight hidden home elements
|
||||
// and steal focus. The Skip path stays on home, so it runs there.
|
||||
// The engine's seen/dismissed state keeps it once; replayable from "?".
|
||||
if (!editing && !finishOpts.launchingSong &&
|
||||
window.v3OnboardingTour && typeof window.v3OnboardingTour.startFirstRun === 'function') {
|
||||
try { window.v3OnboardingTour.startFirstRun(); } catch (e) { /* never block onboarding */ }
|
||||
}
|
||||
}
|
||||
|
||||
submit.addEventListener('click', async () => {
|
||||
@@ -529,7 +540,7 @@
|
||||
// Step 4 — "Play it now": leave calibration pending (it completes
|
||||
// through the normal scored-stats path) and launch the diagnostic.
|
||||
const target = diagnosticFilename;
|
||||
await finish();
|
||||
await finish({ launchingSong: !!target });
|
||||
if (target && typeof window.playSong === 'function') window.playSong(target);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user