diff --git a/neode-ui/src/components/CompanionIntroOverlay.vue b/neode-ui/src/components/CompanionIntroOverlay.vue index c7d9ea8a..c0ace1a1 100644 --- a/neode-ui/src/components/CompanionIntroOverlay.vue +++ b/neode-ui/src/components/CompanionIntroOverlay.vue @@ -78,6 +78,7 @@ import { ref, onMounted, watch } from 'vue' import * as QRCode from 'qrcode' import { IS_DEMO } from '@/composables/useDemoIntro' +import { useLoginTransitionStore } from '@/stores/loginTransition' const STORAGE_KEY = 'neode_companion_intro_seen' // Absolute URL so the QR works when scanned by a phone (a relative path has no @@ -93,17 +94,40 @@ const visible = ref(false) const qrDataUrl = ref('') const companionDownloadUrl = import.meta.env.VITE_COMPANION_APK_URL || DEFAULT_DOWNLOAD_URL +const loginTransition = useLoginTransitionStore() + +// Base delay before the popup may appear, and extra breathing room after the +// dashboard entrance cinematic ends so the popup never cuts into the reveal. +const BASE_DELAY_MS = 5000 +const POST_INTRO_GRACE_MS = 2000 + onMounted(() => { try { if (localStorage.getItem(STORAGE_KEY) !== '1') { - // Delay slightly so it doesn't compete with login animation - setTimeout(() => { visible.value = true }, 5000) + setTimeout(maybeShow, BASE_DELAY_MS) } } catch { // localStorage unavailable } }) +function maybeShow() { + if (loginTransition.introCinematicPlaying) { + // First-visit reveal is still running — hold until it finishes. + const stop = watch( + () => loginTransition.introCinematicPlaying, + (playing) => { + if (!playing) { + stop() + setTimeout(() => { visible.value = true }, POST_INTRO_GRACE_MS) + } + }, + ) + return + } + visible.value = true +} + watch(visible, async (isVisible) => { if (!isVisible) return qrDataUrl.value = await QRCode.toDataURL(companionDownloadUrl, { diff --git a/neode-ui/src/stores/loginTransition.ts b/neode-ui/src/stores/loginTransition.ts index c1004000..8931fbdc 100644 --- a/neode-ui/src/stores/loginTransition.ts +++ b/neode-ui/src/stores/loginTransition.ts @@ -15,6 +15,12 @@ export const useLoginTransitionStore = defineStore('loginTransition', () => { const pendingWelcomeTyping = ref(false) /** Trigger welcome typing on Home - set true after dashboard animation finishes */ const startWelcomeTyping = ref(false) + /** + * True while the dashboard entrance cinematic (zoom/glitch reveal) is + * playing. Overlays that would visually compete with it (e.g. the + * companion intro popup) hold off until this flips back to false. + */ + const introCinematicPlaying = ref(false) function setJustLoggedIn(value: boolean) { justLoggedIn.value = value @@ -32,6 +38,10 @@ export const useLoginTransitionStore = defineStore('loginTransition', () => { startWelcomeTyping.value = value } + function setIntroCinematicPlaying(value: boolean) { + introCinematicPlaying.value = value + } + return { justLoggedIn, setJustLoggedIn, @@ -41,5 +51,7 @@ export const useLoginTransitionStore = defineStore('loginTransition', () => { setPendingWelcomeTyping, startWelcomeTyping, setStartWelcomeTyping, + introCinematicPlaying, + setIntroCinematicPlaying, } }) diff --git a/neode-ui/src/views/Dashboard.vue b/neode-ui/src/views/Dashboard.vue index f6d84fe9..77ba6832 100644 --- a/neode-ui/src/views/Dashboard.vue +++ b/neode-ui/src/views/Dashboard.vue @@ -373,6 +373,7 @@ onMounted(() => { // un-forced call would gate itself silent. playDashboardLoadOomph(true) showZoomIn.value = true + loginTransition.setIntroCinematicPlaying(true) loginTransition.setPendingWelcomeTyping(true) loginTransition.setJustCompletedOnboarding(false) loginTransition.setJustLoggedIn(false) @@ -383,7 +384,10 @@ onMounted(() => { scheduledTimeout(triggerRevealGlitch, 500) scheduledTimeout(triggerRevealGlitch, 1200) scheduledTimeout(triggerRevealGlitch, 2000) - scheduledTimeout(() => { showZoomIn.value = false }, 8000) + scheduledTimeout(() => { + showZoomIn.value = false + loginTransition.setIntroCinematicPlaying(false) + }, 8000) scheduledTimeout(() => { loginTransition.setStartWelcomeTyping(true) loginTransition.setPendingWelcomeTyping(false) @@ -405,6 +409,9 @@ onMounted(() => { onBeforeUnmount(() => { document.body.classList.remove('dashboard-active') + // Timers are cleared below; make sure overlays waiting on the cinematic + // aren't left blocked forever if we unmount mid-reveal. + loginTransition.setIntroCinematicPlaying(false) window.removeEventListener('keydown', handleKioskShortcuts) for (const id of pendingTimers) clearTimeout(id) pendingTimers.length = 0