# Navigation & Smooth Transitions Analysis ## Executive Summary Deep analysis of how navigation works in the Tour Builder Platform - from Constructor editing to Runtime Presentations, across Online and Offline modes. This document traces each navigation thread step-by-step to verify that smooth transitions are robust. **Architecture Update:** The page navigation system was refactored from 6+ fragmented hooks into a unified state machine (`usePageNavigationState`). This consolidation: - Prevents race conditions via atomic `useReducer` transitions - Uses explicit phases instead of boolean flag combinations - Computes derived state (`isLoading`, `showSpinner`, etc.) from a single phase value - Provides a `PageNavigationContext` for child component access --- ## 1. NAVIGATION SYSTEM ARCHITECTURE ### 1.1 Core Hooks (Shared Between Constructor & RuntimePresentation) | Hook | File | Purpose | |------|------|---------| | `usePageNavigation` | `hooks/usePageNavigation.ts` | Page navigation state with history tracking, browser-like back behavior | | `usePageNavigationState` | `hooks/usePageNavigationState.ts` | **Unified state machine** - URL resolution, switching, fade effects, cleanup (replaces 6 hooks) | | `useTransitionPlayback` | `hooks/useTransitionPlayback.ts` | Transition video playback, last frame preservation, pre-generated reverse support | | `usePreloadOrchestrator` | `hooks/usePreloadOrchestrator.ts` | Asset preloading with blob URL cache | | `useNetworkAware` | `hooks/useNetworkAware.ts` | Network condition monitoring | **Architecture Refactor Note:** The following hooks were consolidated into `usePageNavigationState`: - `usePageSwitch` - Page switching with blob URL resolution - `useBackgroundState` - Background ready tracking - `useBackgroundTransition` - CSS animation-based crossfade - `useTransitionCleanup` - Video cleanup coordination - `useBackgroundUrls` - URL resolution for display - `pageLoadingUtils` - Loading state computation ### 1.2 Component Integration ``` RuntimePresentation.tsx / constructor.tsx | +----------------------------------------------------------------+ | usePageNavigation | | - Unified page history management (MAX_HISTORY_LENGTH = 50) | | - Browser-like back: history pops when isBack=true | | - Provides getNavigationContext() for resolveNavigationTarget()| | - applyPageSelection(targetPageId, isBack) handles history | +----------------------------------------------------------------+ | +----------------------------------------------------------------+ | usePreloadOrchestrator | | - Preloads assets for current + neighbor pages | | - Provides getReadyBlobUrl() for O(1) instant lookup | | - Provides getCachedBlobUrl() for Cache API lookup | +----------------------------------------------------------------+ | +----------------------------------------------------------------+ | usePageNavigationState (UNIFIED STATE MACHINE) | | - navigateToPage() initiates navigation with URL resolution | | - Tracks phase: idle → preparing → transitioning/loading_bg | | → transition_done → fading_in → idle | | - Maintains currentImageUrl, previousImageUrl for overlay | | - Provides derived state: isLoading, showSpinner, showElements | | - onBackgroundReady() callback for CanvasBackground | | - onTransitionEnded() callback for video transition completion | | - Atomic state via useReducer (prevents race conditions) | +----------------------------------------------------------------+ | +----------------------------------------------------------------+ | useTransitionPlayback | | - Plays transition videos with presigned URL fallback | | - Preserves last frame until onComplete callback | | - Uses pre-generated reversed videos for back navigation | | - Passes isBack to onComplete for proper history management | +----------------------------------------------------------------+ ``` **Note:** `usePageNavigationState` consolidated 6 hooks into a single state machine: - URL resolution, switching, overlay management (from `usePageSwitch`) - Background ready tracking (from `useBackgroundState`) - Fade-out coordination (from `useBackgroundTransition`) - Video cleanup (from `useTransitionCleanup`) - URL resolution for display (from `useBackgroundUrls`) - Loading state computation (from `pageLoadingUtils`) --- ## 2. NAVIGATION WITHOUT TRANSITION VIDEO ### 2.1 Flow Overview ``` User clicks navigation element (no transitionVideoUrl) | handleElementClick / navigateToPage | Direct navigation - no transition video ``` ### 2.2 Step-by-Step Thread Analysis **Thread 1: Navigation Trigger** ``` handleElementClick(element) [RuntimePresentation:309-340] | resolveNavigationTarget(element, pages) [lib/navigationHelpers.ts] +-- Extract targetPageSlug from element +-- Find target page in pages array +-- Return { pageId, transitionVideoUrl, isBack } | navigateToPage(targetPageId, undefined, false) [No transition video] ``` **Thread 2: Page Switch Initiation** ``` navigateToPage(targetPageId, undefined, isBack) [RuntimePresentation:285-315] | No transitionVideoUrl -> Direct navigation path | setIsBackgroundReady(false) [Mark: waiting for new bg] lastInitializedPageIdRef.current = targetPageId | await pageSwitch.switchToPage(targetPage, () => { applyPageSelection(targetPageId, isBack); [usePageNavigation hook] // - If isBack=true AND target matches previous: history pops // - Otherwise: history appends (trimmed to MAX_HISTORY_LENGTH=50) }); ``` **Thread 3: URL Resolution & Overlay Setup** ``` switchToPage(targetPage, onSwitched) [usePageSwitch:372-419] | Save current as previous for overlay: setPreviousBgImageUrl(currentBgImageUrlRef.current) | setIsSwitching(true) setIsNewBgReady(false) | Resolve URLs in parallel (prefer preloaded blob URLs): [imageUrl, videoUrl, audioUrl] = await Promise.all([ resolveToDisplayUrl(background_image_url), resolveMediaUrl(background_video_url), resolveMediaUrl(background_audio_url), ]) ``` **Thread 4: Blob URL Resolution Priority** ``` resolveToDisplayUrl(storagePath) [usePageSwitch:237-306] | 1. getReadyBlobUrl(storagePath) [O(1) Map lookup - same session] -> If found: return immediately (instant) | 2. getCachedBlobUrl(storagePath) [Cache API lookup ~5ms] -> If found: create blob URL, return | 3. resolveAssetPlaybackUrl(storagePath) [Resolve to playback URL] | 4. getReadyBlobUrl(resolvedUrl) [Fallback lookup] -> If found: return | 5. getCachedBlobUrl(resolvedUrl) [Fallback Cache API] -> If found: return | 6. loadImageWithFallback(originalUrl, storageKey) [Network fetch] +-- Try presigned URL +-- On CORS failure: markPresignedUrlFailed() +-- Retry with proxy URL: /api/file/download ``` **Thread 5: Background Display & Overlay** ``` After URL resolution: setCurrentBgImageUrl(imageUrl) setCurrentBgVideoUrl(videoUrl) setCurrentBgAudioUrl(audioUrl) onSwitched() [Notify caller] | For blob URLs (local data): requestAnimationFrame(() => { requestAnimationFrame(() => { setIsNewBgReady(true) [Mark ready after 2 frames] }) }) | For remote images: Wait for Image onLoad callback -> markBackgroundReady() ``` **Thread 6: Render - Previous Background Overlay** ``` RuntimePresentation render [lines 517-528] | {pageSwitch.previousBgImageUrl && pageSwitch.isSwitching && !pageSwitch.isNewBgReady && (
)} | Previous page background stays visible until new one is ready! ``` **Thread 7: Overlay Clearing** ``` useBackgroundTransition effect [lines 144-158] | When: isSwitching && isNewBgReady && previousBgImageUrl | pageSwitch.clearPreviousBackground() +-- setPreviousBgImageUrl('') +-- setIsSwitching(false) +-- revokeBlobUrl(prevUrl) [Memory cleanup] ``` ### 2.3 Summary: Navigation WITHOUT Transition Video | Phase | What's Visible | State | |-------|----------------|-------| | 1. Click | Current page | `isSwitching: false` | | 2. Switch starts | Previous bg (z-0) fading out, New content (z-1) fading in | `isSwitching: true, isFadingIn: true` | | 3. Crossfade | Both visible with CSS animation | `animate-crossfade-out` / `animate-crossfade-in` | | 4. Animation ends | New page fully visible | `onAnimationEnd` fires, `isFadingIn: false` | | 5. Cleanup | New page only | `clearPreviousBackground()` called | ### 2.4 CSS Animation-Based Crossfade The crossfade effect uses CSS animations instead of JS-controlled transitions: **CSS Variables (main.css) - Single Source of Truth:** ```css :root { --crossfade-duration: 700ms; --crossfade-easing: cubic-bezier(0.4, 0, 0.2, 1); } ``` **CSS Classes (main.css):** ```css .animate-crossfade-in { animation: page-crossfade-in var(--crossfade-duration, 700ms) var(--crossfade-easing) forwards; } .animate-crossfade-out { animation: page-crossfade-out var(--crossfade-duration, 700ms) var(--crossfade-easing) forwards; } ``` **Easing Characteristics:** - `cubic-bezier(0.4, 0, 0.2, 1)` - Material Design standard - Slow start prevents abrupt appearance - Smooth acceleration and soft landing **Why CSS Animations (not transitions):** - CSS animations always play when the class is added - Immune to React's render batching that can skip transition states - `onAnimationEnd` event provides reliable completion detection - Duration controlled via CSS variable (single source of truth) - JS can read duration via `getCrossfadeDuration()` utility **Hook Usage:** ```typescript const { isFadingIn, onFadeInAnimationEnd } = useBackgroundTransition({ pageSwitch, fadeIn: { hasActiveTransition: false }, }); // In JSX:
{/* New page content */}
// Previous background: {previousBgImageUrl && isFadingIn && (
...
)} ``` **ROBUSTNESS CHECK: Previous page renders while next page background and UI elements not ready** --- ## 3. NAVIGATION WITH TRANSITION VIDEO ### 3.1 Flow Overview ``` User clicks navigation element (has transitionVideoUrl) | handleElementClick / navigateToPage | Start transition video -> Play -> Keep last frame -> Switch page -> Fade out ``` ### 3.2 Step-by-Step Thread Analysis **Thread 1: Transition Initiation** ``` navigateToPage(targetPageId, transitionVideoUrl, isBack) [RuntimePresentation:272-307] | Has transitionVideoUrl -> Transition path | resetFadeOut() [Clear previous fade state] setPendingTransitionComplete(false) | setTransitionPreview({ targetPageId, videoUrl: resolveAssetPlaybackUrl(transitionVideoUrl), storageKey: transitionVideoUrl, [Raw path for cache lookup] isReverse: isBack, }) ``` **Thread 2: Transition Video Source Resolution** ``` useTransitionPlayback effect triggers [lines 355-771] | resolvePlayableSource() [lines 431-540] | 1. getReadyBlobUrl(storageKey) [O(1) lookup by storage path] -> If found: use cached blob URL | 2. getCachedBlobUrl(storageKey) [Cache API by storage path] -> If found: create blob URL, cache it | 3. Check lastLoadedBlobUrlRef (reuse same session) | 4. getReadyBlobUrl(sourceUrl) [Lookup by resolved URL] | 5. getCachedBlobUrl(sourceUrl) [Cache API by resolved URL] | 6. Network fetch as blob [Fallback - full download] -> axios.get(requestUrl, { responseType: 'blob' }) -> URL.createObjectURL(blob) ``` **Thread 3: Video Playback** ``` loadAndPlay() [lines 542-598] | video.src = playableSourceUrl video.currentTime = 0 video.load() attemptPlay() | onPlaying event fires [lines 633-675] | setPhase('playing') | scheduleFinishByDuration(durationSec) [lines 405-421] +-- finishBeforeEndMs = 50 [Finish 50ms BEFORE end] +-- finishMs = durationSec * 1000 - 50 +-- setTimeout(() => finishPlayback('duration-timer'), finishMs) ``` **Thread 4: Last Frame Preservation (CRITICAL)** ``` finishPlayback(reason) [lines 244-293] | didFinishRef.current = true clearTimers() | video.pause() | *** LAST FRAME PRESERVATION *** if (video.duration && Number.isFinite(video.duration) && video.currentTime >= video.duration - 0.1) { video.currentTime = Math.max(0, video.duration - 0.05) } | Explanation: - Some browsers show BLACK after 'ended' event - Seeking to duration - 0.05 keeps last visible frame - This ensures smooth visual continuity | setPhase('finishing') | Optional: waitForImages() for target page pre-decode | setPhase('completed') onCompleteRef.current(targetPageId) ``` **Thread 5: Page Switch After Transition** ``` onComplete callback (targetPageId, isBack) [RuntimePresentation:146-166] | if (targetPageId) { const targetPage = pages.find(p => p.id === targetPageId) lastInitializedPageIdRef.current = targetPageId | await pageSwitch.switchToPage(targetPage, () => { applyPageSelection(targetPageId, isBack ?? false) [usePageNavigation] // Proper history management: pops on back, appends on forward }) | setIsBackgroundReady(false) setPendingTransitionComplete(true) [Signal: waiting for bg ready] } ``` **Thread 6: Background Image Load Detection** ``` Background image element in render [lines 476-514] | { setIsBackgroundReady(true) pageSwitch.markBackgroundReady() }} /> | Or for video backgrounds: useEffect auto-marks ready when no image or has video ``` **Thread 7: Video Transition Overlay Removal (Instant, With rAF Delay)** ``` TransitionPreviewOverlay.tsx | When: isFadingOut=true passed from parent | useEffect triggers: if (isFadingOut) { requestAnimationFrame(() => { setShouldHide(true) [After one paint frame] }) } | Container opacity: - 0 during initial buffering (before first frame) - 0 when shouldHide=true (after rAF delay - bg is painted) - 1 otherwise (video playing) | transition: 'none' when hiding [NO CSS transition - instant hide] ``` **Thread 8: Render - Transition Overlay Visibility** ``` TransitionPreviewOverlay component [TransitionPreviewOverlay.tsx] | Container opacity controlled by: - isBuffering && !isVideoReady → 0 (initial buffering) - shouldHide → 0 (after rAF delay, new bg painted) - otherwise → opacity prop or 1 | transition: useTransition ? '150ms' : 'none' - 150ms ONLY for initial buffering fade-in - 'none' when hiding (instant hide since last video frame = new bg) | {transitionPreview && ( )} | Container hidden while: preparing, buffering (old page visible through) Container visible when: video ready to play (instant appearance) Overlay removed: instantly after rAF (ensures bg is painted first) ``` **Key Design Decision - Instant Hide with rAF Delay:** - Video itself IS the transition effect - First frame = old page background - Last frame = new page background - Wait one `requestAnimationFrame` after `isFadingOut=true` - This ensures new background is painted before hiding overlay - Overlay hides **instantly** (no CSS transition) - seamless since last frame = new bg - No visual discontinuity or flash ### 3.3 Summary: Navigation WITH Transition Video | Phase | What's Visible | State | |-------|----------------|-------| | 1. Click | Current page | `transitionPhase: 'idle'` | | 2. Preparing | Current page (container hidden) | `transitionPhase: 'preparing', isBuffering: true` | | 3. Playing | Transition video | `transitionPhase: 'playing'` | | 4. Finishing | **Last frame of video** | `transitionPhase: 'finishing'` | | 5. Completed | **Last frame of video** | `pendingTransitionComplete: true` | | 6. Bg loading | **Last frame of video** (z-50) | New bg loading underneath | | 7. Bg ready | **Instant switch** | `setTransitionPreview(null)` (no fade!) | | 8. Done | New page only | Overlay removed | **Key Changes from Previous Implementation:** - Container hidden during buffering (no black flash) - No fade-out animation for video transitions - Instant overlay removal when background ready - Video itself is the transition (first frame = old, last frame = new) **ROBUSTNESS CHECK: Last video frame renders while next page background and UI elements not ready** ### 3.4 PreviousBackgroundOverlay (Simplified) The `PreviousBackgroundOverlay` component was simplified to remove all fade logic: ```tsx // PreviousBackgroundOverlay.tsx - Simplified implementation const PreviousBackgroundOverlay = ({ imageUrl, isSwitching = false, isNewBgReady = false, className = '', }) => { // Simple render logic: show while switching AND new bg not ready const shouldRender = isSwitching && !isNewBgReady && !!imageUrl; if (!shouldRender) return null; return (
); }; ``` **Changes from previous implementation:** - Removed all CSS transition/fade logic - Removed timeout fallbacks - Removed transition event handlers - Simplified to pure show/hide based on props - Hides **instantly** when `isNewBgReady=true` ### 3.5 scheduleAfterPaint Helper The `CanvasBackground.tsx` uses a `scheduleAfterPaint` helper for video first-frame detection: ```typescript /** * Schedule a callback to run after the next browser paint. * Uses double rAF pattern: first rAF schedules for next frame, * second rAF ensures the frame has actually been committed. */ const scheduleAfterPaint = (callback: () => void): void => { requestAnimationFrame(() => { requestAnimationFrame(callback); }); }; ``` **Usage in video first-frame detection:** ```typescript // Using requestVideoFrameCallback for accurate first-frame detection if ('requestVideoFrameCallback' in video) { video.requestVideoFrameCallback(() => { clearTimeout(timeout); scheduleAfterPaint(() => { reportVideoReady(); // Called after frame is painted }); }); } ``` --- ## 4. ONLINE VS OFFLINE MODE ### 4.1 Network Awareness ``` useNetworkAware hook [hooks/useNetworkAware.ts] | Monitors: - navigator.onLine - connection.effectiveType (slow-2g, 2g, 3g, 4g) - connection.downlink (Mbps) - connection.rtt (ms) - connection.saveData (boolean) | Returns: - networkInfo.isOnline - recommendedConcurrency (1-3 based on connection) - shouldPreloadAggressively - suggestOfflineMode ``` ### 4.2 Preload Orchestrator - Online Mode ``` processQueue() [usePreloadOrchestrator:355-485] | Guard: if (!networkInfo.isOnline) return [Skip if offline] | While queue has items && activeDownloads < recommendedConcurrency: +-- Check preloadedUrls.has(url) -> skip if already loaded +-- Check StorageManager.hasAsset(url) -> skip if cached | +-- If cached: createReadyBlobUrl() from cache | preloadWithProgress(url, jobId, assetId) +-- fetch(url) with streaming progress +-- Store in Cache API +-- createReadyBlobUrl() ``` ### 4.3 Preload Orchestrator - Offline Mode ``` processQueue() [usePreloadOrchestrator:355-485] | Guard: if (!networkInfo.isOnline) return [Don't process queue when offline] | BUT: isUrlCached check still works! +-- StorageManager.hasAsset(url) | +-- Check IndexedDB: OfflineDbManager.hasAssetByUrl(url) | +-- Check Cache API: caches.open().match(url) | Assets already downloaded are still accessible! ``` ### 4.4 Navigation Flow - Same for Both Modes The navigation flow is **identical** for online and offline modes: 1. **URL Resolution** - `resolveToDisplayUrl()` / `resolvePlayableSource()` - Tries `getReadyBlobUrl()` first (in-memory Map - O(1)) - Tries `getCachedBlobUrl()` second (Cache API + IndexedDB) - Falls back to network only if not cached 2. **Blob URL Priority** - Cache is always checked first ``` Online: blob URL (cache) > presigned URL > proxy URL Offline: blob URL (cache) > FAIL if not cached ``` 3. **Transition Video** - Same playback logic - If cached: plays from blob URL (instant) - If not cached: depends on network availability ### 4.5 Offline Storage Hierarchy ``` Asset lookup order (StorageManager) | 1. IndexedDB (files >= 5MB) +-- OfflineDbManager.getAssetByUrl(url) | 2. Cache API (files < 5MB) +-- caches.open('tour-builder-assets-v1').match(url) | 3. Network (online only) +-- fetch(presignedUrl || proxyUrl) ``` ### 4.6 Summary: Online vs Offline Differences | Aspect | Online Mode | Offline Mode | |--------|-------------|--------------| | Preload queue | Active processing | Paused (no new downloads) | | Asset resolution | Cache -> Network | Cache only | | Transition video | Cache -> Network | Cache only | | Background image | Cache -> Network | Cache only | | Navigation flow | Same | Same | | Overlay behavior | Same | Same | | Last frame handling | Same | Same | **ROBUSTNESS CHECK: Same navigation flow works in both online and offline modes** --- ## 5. CONSTRUCTOR VS RUNTIME PRESENTATION ### 5.1 Shared Components Both use the **same hooks**: - `usePageNavigationState` - Unified state machine for URL resolution, overlay management, fade effects - `useTransitionPlayback` - Video playback, last frame preservation - `usePreloadOrchestrator` - Asset preloading - `usePageNavigation` - History tracking ### 5.2 Key Differences | Aspect | Constructor | RuntimePresentation | |--------|-------------|---------------------| | State management | `usePageNavigationState` (unified) | `usePageNavigationState` (unified) | | Crossfade animation | Yes (project transition settings) | Yes (project transition settings) | | Crossfade easing | From `transitionSettings.fadeEasing` | From `transitionSettings.fadeEasing` | | Transition video fade-out | **No (instant removal)** | **No (instant removal)** | | Video overlay hiding | Container hidden while buffering | Container hidden while buffering | | Background transition config | Via `usePageNavigationState` | Via `usePageNavigationState` | | Overlay component | `TransitionPreviewOverlay` | `TransitionPreviewOverlay` | | Post-transition cleanup | Via `onTransitionEnded` + `onBackgroundReady` | Via `onTransitionEnded` + `onBackgroundReady` | | Animation end detection | CSS `onAnimationEnd` event | CSS `onAnimationEnd` event | | Edit mode support | Direct background updates (`setBackgroundDirectly`) | N/A | ### 5.3 Constructor Transition Flow ``` onComplete callback (targetPageId, isBack) [constructor.tsx] | if (targetPageId) { await navState.navigateToPage(targetPage, { hasTransition: false, isBack: isBack ?? false, onSwitched: () => applyPageSelection(targetPage.id, isBack ?? false) }) // navigateToPage handles all state transitions atomically // usePageNavigation handles history (pops on back, appends on forward) clearSelection() setSelectedMenuItem('none') } | navState.onTransitionEnded() is called when transition video completes | Video cleanup happens via effect watching pendingTransitionComplete + isBackgroundReady ``` Both constructor and runtime presentation use the same `usePageNavigationState` hook for unified state management. The `usePageNavigation` hook handles browser-like history behavior. --- ## 6. ROBUSTNESS VERIFICATION ### 6.1 Scenario: Navigation WITHOUT Transition (Direct) | Step | What Happens | Verified | |------|--------------|----------| | 1 | User clicks navigation element | YES | | 2 | Previous bg saved to `previousBgImageUrl` | YES | | 3 | `isSwitching: true, isNewBgReady: false` | YES | | 4 | URL resolution (blob -> cache -> network) | YES | | 5 | Previous bg overlay renders (z-10) | YES | | 6 | New bg loads underneath (z-1) | YES | | 7 | Image onLoad -> `markBackgroundReady()` | YES | | 8 | `clearPreviousBackground()` removes overlay | YES | ### 6.2 Scenario: Navigation WITH Transition Video | Step | What Happens | Verified | |------|--------------|----------| | 1 | User clicks navigation element | YES | | 2 | `setTransitionPreview()` triggers hook | YES | | 3 | Video source resolved (blob -> cache -> network) | YES | | 4 | Video plays (opacity: 1) | YES | | 5 | Timer fires 50ms before video ends | YES | | 6 | `finishPlayback()` seeks to duration - 0.05 | YES | | 7 | Last frame stays visible | YES | | 8 | `onComplete()` triggers page switch | YES | | 9 | New bg loads underneath overlay | YES | | 10 | Image onLoad -> `isBackgroundReady: true` | YES | | 11 | Overlay fades out (opacity: 0 transition) | YES | | 12 | Cleanup: remove video src, clear state | YES | ### 6.3 Scenario: Reverse Navigation (Back) | Step | What Happens | Verified | |------|--------------|----------| | 1 | User clicks back navigation | YES | | 2 | `isBack: true` set, uses `reverseVideoUrl` | YES | | 3 | Pre-generated reversed video loaded | YES | | 4 | Forward playback of reversed video | YES | | 5 | Video ends normally | YES | | 6 | `onComplete()` -> `finishPlayback()` | YES | | 7 | Same cleanup flow as forward | YES | **Note:** Reversed videos are pre-generated server-side using FFmpeg when pages are saved. This eliminates client-side frame-stepping and ensures professional audio/video synchronization. ### 6.4 Scenario: Offline Mode | Step | What Happens | Verified | |------|--------------|----------| | 1 | `networkInfo.isOnline: false` | YES | | 2 | Preload queue paused | YES | | 3 | Navigation clicked | YES | | 4 | URL resolved from cache (IndexedDB/Cache API) | YES | | 5 | If cached: works identically to online | YES | | 6 | If not cached: fails gracefully | YES | --- ## 7. CONCLUSION The navigation and smooth transitions system is **robust and comprehensive**: ### 7.1 Key Strengths 1. **Unified State Machine** - `usePageNavigationState` consolidates 6 hooks into atomic state transitions 2. **Race Condition Prevention** - `useReducer` ensures state updates are atomic (no batching issues) 3. **Explicit Phase Tracking** - Single `phase` value (idle, preparing, transitioning, etc.) instead of multiple boolean flags 4. **Derived State via useMemo** - `isLoading`, `showSpinner`, `showElements` computed from phase 5. **Unified History Management** - `usePageNavigation` hook handles history in both components 6. **Browser-Like Back Navigation** - History pops when navigating back (via `applyPageSelection(id, isBack=true)`) 7. **History Limit** - `MAX_HISTORY_LENGTH=50` prevents unbounded growth in long sessions 8. **Blob URL Priority** - Always checks cache before network 9. **CSS Animation Crossfade** - Uses CSS animations (not transitions) for reliable visual effects 10. **Smooth Easing** - Project-configurable transition easing 11. **Event-Based Completion** - `onAnimationEnd` event replaces timer-based duration tracking 12. **Previous Background Overlay** - Keeps previous page visible during direct navigation 13. **Last Frame Preservation** - Seeks to duration - 0.05 to avoid black frame 14. **Timer-Based Finish** - Finishes 50ms before video ends to catch last frame 15. **No Video Fade** - Video transitions end instantly without fade-out animation 16. **Container Hidden While Buffering** - Prevents black flash before video ready 17. **Offline Resilience** - Same flow works when offline (if assets cached) 18. **Context Provider** - `PageNavigationContext` allows child components to access navigation state ### 7.2 No Gaps Identified - **State consistency**: Single `useReducer` prevents race conditions from async batching - **Invalid states impossible**: State machine phases prevent flag combinations like `isLoading && showElements` - **Pages without transition**: Previous page renders while next loads (crossfade animation) - **Pages with transition**: Last frame renders while next loads (instant switch when ready) - **Video overlay**: Container hidden while buffering (no black flash) - **Online mode**: Full preloading and network fallback - **Offline mode**: Graceful cache-first behavior - **Constructor**: Same `usePageNavigationState` hook as RuntimePresentation - **Edit mode**: Direct background updates via `setBackgroundDirectly()` bypass navigation flow --- ## 8. CRITICAL CODE LOCATIONS | Feature | File | Lines | |---------|------|-------| | Page navigation with history | `usePageNavigation.ts` | 62-195 | | History limit (MAX=50) | `usePageNavigation.ts` | 8 | | Browser-like history pop | `usePageNavigation.ts` | 120-127 | | Navigation context | `usePageNavigation.ts` | 164-170 | | **Unified state machine** | `usePageNavigationState.ts` | Full file (~520 LOC) | | State machine reducer | `usePageNavigationState.ts` | 80-180 | | Navigation phases | `usePageNavigationState.ts` | 30-45 | | URL resolution | `usePageNavigationState.ts` | 200-280 | | Derived state (isLoading, etc.) | `usePageNavigationState.ts` | 350-400 | | onBackgroundReady callback | `usePageNavigationState.ts` | 420-450 | | Transition video playback | `useTransitionPlayback.ts` | 355-771 | | Last frame preservation | `useTransitionPlayback.ts` | 251-262 | | Timer-based finish | `useTransitionPlayback.ts` | 405-421 | | isBack in onComplete | `useTransitionPlayback.ts` | 290-296 | | CSS variables (duration, easing) | `css/main.css` | 15-20 | | CSS animations | `css/main.css` | 42-120 | | getCrossfadeDuration utility | `lib/browserUtils.ts` | 14-32 | | TransitionPreviewOverlay | `Constructor/TransitionPreviewOverlay.tsx` | 1-75 | | PageNavigationContext provider | `context/PageNavigationContext.tsx` | Full file (~120 LOC) | | Network awareness | `useNetworkAware.ts` | 68-163 | | Server-side reversal | `backend/src/services/videoProcessing.ts` | N/A | | Preload queue guard | `usePreloadOrchestrator.ts` | 355-358 | | Navigation helpers | `lib/navigationHelpers.ts` | 1-120 | **Deleted Files (consolidated into `usePageNavigationState.ts`):** - `usePageSwitch.ts` (~475 LOC) - `useBackgroundState.ts` (~156 LOC) - `useBackgroundTransition.ts` (~164 LOC) - `useTransitionCleanup.ts` (~108 LOC) - `useBackgroundUrls.ts` (~104 LOC) - `lib/pageLoadingUtils.ts` (~76 LOC)