39948-vm/frontend/docs/navigation-smooth-transitions.md
2026-07-03 16:11:24 +02:00

855 lines
31 KiB
Markdown

# 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 && (
<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:**
```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:
<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 `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 (
<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:
```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)