Merge pull request 'fix(ui): restore first-login dashboard entrance + working Replay Intro' (#86) from intro-entrance-fixes into main
All checks were successful
Demo images / Build & push demo images (push) Successful in 2m36s

This commit is contained in:
ai 2026-07-15 09:49:14 +00:00
commit 8caa4c7d07
7 changed files with 55 additions and 4 deletions

View File

@ -395,6 +395,11 @@ onMounted(async () => {
let seenIntro = localStorage.getItem('neode_intro_seen') === '1'
const fromBoot = sessionStorage.getItem('archipelago_from_boot') === '1'
if (fromBoot) sessionStorage.removeItem('archipelago_from_boot')
// One-shot "Replay intro" request from the login screen must survive the
// auto-re-mark below (which otherwise instantly suppresses the replay on any
// onboarded node).
const replayRequested = sessionStorage.getItem('archipelago_replay_intro') === '1'
if (replayRequested) sessionStorage.removeItem('archipelago_replay_intro')
let onboardingComplete: boolean | null = localStorage.getItem('neode_onboarding_complete') === '1' ? true : null
const splashCandidate = !seenIntro
&& (fromBoot || (route.path === '/' && import.meta.env.VITE_DEV_MODE !== 'boot'))
@ -408,7 +413,7 @@ onMounted(async () => {
}
}
if (!seenIntro && onboardingComplete === true) {
if (!replayRequested && !seenIntro && onboardingComplete === true) {
try { localStorage.setItem('neode_intro_seen', '1') } catch { /* noop */ }
seenIntro = true
}
@ -421,6 +426,7 @@ onMounted(async () => {
fromBoot,
devMode: import.meta.env.VITE_DEV_MODE,
onboardingComplete,
replayRequested,
})) {
// Coming from boot screen show the full splash intro (Enter to Exit typing logo)
showSplash.value = true

View File

@ -251,8 +251,15 @@ export function playKeyboardTypingSound() {
}
/** Gaming-style boot thud - soft impact when dashboard loads */
export function playDashboardLoadOomph() {
if (!isFirstInstallPhase()) return
/**
* @param force Play regardless of install phase. The first dashboard entry
* right after the onboarding wizard needs this: by then onboarding is already
* flagged complete in localStorage, so the isFirstInstallPhase() gate (which
* exists to keep ordinary re-logins quiet) would silence the one entrance
* that SHOULD be loud.
*/
export function playDashboardLoadOomph(force = false) {
if (!force && !isFirstInstallPhase()) return
const ctx = getContext()
if (!ctx) return

View File

@ -28,4 +28,14 @@ describe('shouldShowIntroSplash', () => {
onboardingComplete: null,
})).toBe(false)
})
it('an explicit replay request overrides every suppression rule', () => {
expect(shouldShowIntroSplash({
seenIntro: true,
routePath: '/',
fromBoot: false,
onboardingComplete: true,
replayRequested: true,
})).toBe(true)
})
})

View File

@ -4,9 +4,12 @@ export interface IntroSplashDecisionInput {
fromBoot: boolean
devMode?: string
onboardingComplete: boolean | null
/** Explicit "Replay intro" click — overrides every suppression rule. */
replayRequested?: boolean
}
export function shouldShowIntroSplash(input: IntroSplashDecisionInput): boolean {
if (input.replayRequested) return true
if (input.seenIntro) return false
if (input.onboardingComplete === true) return false

View File

@ -369,7 +369,9 @@ onMounted(() => {
if (loginTransition.justCompletedOnboarding) {
// Full glitchy reveal only on the very first dashboard entry
// right after onboarding (one-time event, persists in feel).
playDashboardLoadOomph()
// force=true: onboarding is already marked complete by now, so the
// un-forced call would gate itself silent.
playDashboardLoadOomph(true)
showZoomIn.value = true
loginTransition.setPendingWelcomeTyping(true)
loginTransition.setJustCompletedOnboarding(false)

View File

@ -477,6 +477,7 @@ async function handleLogin() {
stopSynthwave()
whooshAway.value = true
playLoginSuccessWhoosh()
consumeOnboardingFinale()
loginTransition.setJustLoggedIn(true)
await new Promise(r => setTimeout(r, 520))
await router.replace(loginRedirectTo.value).catch(() => {
@ -512,6 +513,7 @@ async function handleTotpVerify() {
stopSynthwave()
whooshAway.value = true
playLoginSuccessWhoosh()
consumeOnboardingFinale()
loginTransition.setJustLoggedIn(true)
await new Promise(r => setTimeout(r, 520))
await router.replace(loginRedirectTo.value).catch(() => {
@ -533,11 +535,28 @@ async function handleTotpVerify() {
}
}
/** A login right after the onboarding wizard (flag set by OnboardingDone)
* gets the FULL dashboard entrance zoom + oomph even though it's a
* regular password login (e.g. the demo). Subsequent logins stay
* deliberately low-key (justLoggedIn only). */
function consumeOnboardingFinale() {
try {
if (sessionStorage.getItem('archy_onboarding_finale') === '1') {
sessionStorage.removeItem('archy_onboarding_finale')
loginTransition.setJustCompletedOnboarding(true)
}
} catch { /* ignore */ }
}
function replayIntro() {
// Clear the intro seen flag
localStorage.removeItem('neode_intro_seen')
// Demo: also clear the per-day gate so the intro plays again now.
if (IS_DEMO) clearDemoIntroSeen()
// On an onboarded node App.vue instantly re-marks the intro as seen (that's
// what keeps fresh browsers on already-onboarded nodes from replaying it)
// this explicit one-shot flag tells it the replay is deliberate.
try { sessionStorage.setItem('archipelago_replay_intro', '1') } catch { /* ignore */ }
// Navigate to root to trigger splash screen
window.location.href = '/'
}

View File

@ -120,6 +120,10 @@ onUnmounted(() => {
function goToLogin() {
playNavSound('action')
// The login that follows the wizard gets the full dashboard entrance
// (zoom + oomph) even when it's a regular password login (e.g. the demo)
// Login.vue consumes this flag on success.
try { sessionStorage.setItem('archy_onboarding_finale', '1') } catch { /* ignore */ }
router.push('/login').catch(() => {})
}
</script>