31 KiB
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
useReducertransitions - Uses explicit phases instead of boolean flag combinations
- Computes derived state (
isLoading,showSpinner, etc.) from a single phase value - Provides a
PageNavigationContextfor 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 resolutionuseBackgroundState- Background ready trackinguseBackgroundTransition- CSS animation-based crossfadeuseTransitionCleanup- Video cleanup coordinationuseBackgroundUrls- URL resolution for displaypageLoadingUtils- 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 && (
<div className="absolute inset-0 z-10"
style={{ backgroundImage: `url("${previousBgImageUrl}")` }}
/>
)}
|
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:
:root {
--crossfade-duration: 700ms;
--crossfade-easing: cubic-bezier(0.4, 0, 0.2, 1);
}
CSS Classes (main.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
onAnimationEndevent provides reliable completion detection- Duration controlled via CSS variable (single source of truth)
- JS can read duration via
getCrossfadeDuration()utility
Hook Usage:
const { isFadingIn, onFadeInAnimationEnd } = useBackgroundTransition({
pageSwitch,
fadeIn: { hasActiveTransition: false },
});
// In JSX:
<div
className={`absolute inset-0 z-1 ${isFadingIn ? 'animate-crossfade-in' : ''}`}
onAnimationEnd={onFadeInAnimationEnd}
>
{/* New page content */}
</div>
// Previous background:
{previousBgImageUrl && isFadingIn && (
<div className={`absolute inset-0 z-0 ${isFadingIn ? 'animate-crossfade-out' : ''}`}>
...
</div>
)}
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]
|
<img onLoad={() => {
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 && (
<TransitionPreviewOverlay
isBuffering={transitionPhase === 'preparing' || isBuffering}
isFadingOut={pendingTransitionComplete && isBackgroundReady}
/>
)}
|
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
requestAnimationFrameafterisFadingOut=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:
// 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 (
<div
className={`pointer-events-none absolute inset-0 z-2 ${className}`}
style={{
backgroundImage: `url("${imageUrl}")`,
backgroundSize: 'contain',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
}}
/>
);
};
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:
/**
* 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:
// 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:
-
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
- Tries
-
Blob URL Priority - Cache is always checked first
Online: blob URL (cache) > presigned URL > proxy URL Offline: blob URL (cache) > FAIL if not cached -
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 effectsuseTransitionPlayback- Video playback, last frame preservationusePreloadOrchestrator- Asset preloadingusePageNavigation- 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
- Unified State Machine -
usePageNavigationStateconsolidates 6 hooks into atomic state transitions - Race Condition Prevention -
useReducerensures state updates are atomic (no batching issues) - Explicit Phase Tracking - Single
phasevalue (idle, preparing, transitioning, etc.) instead of multiple boolean flags - Derived State via useMemo -
isLoading,showSpinner,showElementscomputed from phase - Unified History Management -
usePageNavigationhook handles history in both components - Browser-Like Back Navigation - History pops when navigating back (via
applyPageSelection(id, isBack=true)) - History Limit -
MAX_HISTORY_LENGTH=50prevents unbounded growth in long sessions - Blob URL Priority - Always checks cache before network
- CSS Animation Crossfade - Uses CSS animations (not transitions) for reliable visual effects
- Smooth Easing - Project-configurable transition easing
- Event-Based Completion -
onAnimationEndevent replaces timer-based duration tracking - Previous Background Overlay - Keeps previous page visible during direct navigation
- Last Frame Preservation - Seeks to duration - 0.05 to avoid black frame
- Timer-Based Finish - Finishes 50ms before video ends to catch last frame
- No Video Fade - Video transitions end instantly without fade-out animation
- Container Hidden While Buffering - Prevents black flash before video ready
- Offline Resilience - Same flow works when offline (if assets cached)
- Context Provider -
PageNavigationContextallows child components to access navigation state
7.2 No Gaps Identified
- State consistency: Single
useReducerprevents 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
usePageNavigationStatehook 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)