# Lib Module ## Overview The lib module provides **utility libraries and helper functions** used throughout the frontend application. These are pure functions and classes that encapsulate domain-specific logic, separated from React components and Redux state management. **Location:** `frontend/src/lib/` **Statistics:** - **23 files** (~5,608 LOC total) - **2 subdirectories** (offline, offlineDb) - **8 main categories**: Asset Management, Element Utilities, Navigation/Preload, Media Utilities, Audio Utilities, Fonts, General Utilities, Offline/PWA --- ## Architecture Diagram ``` frontend/src/lib/ ├── Core Asset Management │ ├── assetUrl.ts # Presigned URL management (~405 LOC) │ └── imagePreDecode.ts # Image pre-decoding (~202 LOC) │ ├── UI Adaptivity & Element Utilities │ ├── canvasScale.ts # Canvas units & responsive scaling (~219 LOC) │ ├── elementDefaults.ts # Element defaults & type guards (~707 LOC) │ ├── elementStyles.ts # CSS style building with canvas units (~345 LOC) │ ├── elementEffects.ts # Animation/interaction/audio effects (~374 LOC) │ ├── gallerySectionStyles.ts # Gallery section styling (~544 LOC) │ └── constructorHelpers.ts # Constructor page utilities (~279 LOC) │ ├── Navigation & Preloading │ ├── navigationHelpers.ts # Page navigation utilities (~237 LOC) │ ├── extractPageLinks.ts # Page link extraction (~182 LOC) │ ├── tourFlowHelpers.ts # Tour routing utilities (~118 LOC) │ └── resolveSlideTransition.ts # Slide transition cascade resolver (~150 LOC) │ ├── Media Utilities │ ├── mediaDuration.ts # Duration probing (~119 LOC) │ └── mediaHelpers.ts # Duration formatting (~158 LOC) │ ├── Audio Utilities │ └── backgroundAudioController.ts # Audio ducking singleton (~81 LOC) │ ├── Font Configuration │ └── fonts.ts # Font options & utilities (~110 LOC) │ ├── General Utilities │ ├── logger.ts # Structured logging (~163 LOC) │ ├── parseJson.ts # Safe JSON parsing (~89 LOC) │ ├── slugHelpers.ts # URL slug utilities (~88 LOC) │ └── queryClient.ts # React Query client (~155 LOC) │ ├── offline/ # PWA download management │ ├── StorageManager.ts # Cache API + IndexedDB (~294 LOC) │ ├── DownloadManager.ts # Download queue (~591 LOC) │ └── DownloadEventBus.ts # Progress events (~188 LOC) │ └── offlineDb/ # IndexedDB persistence ├── schema.ts # Dexie.js schema (~41 LOC) └── OfflineDbManager.ts # CRUD operations (~342 LOC) ``` --- ## Module Categories ### 1. Core Asset Management #### assetUrl.ts **Purpose:** Resolves relative asset paths to playback URLs with presigned S3 support. **Key Exports:** | Export | Type | Description | |--------|------|-------------| | `resolveAssetPlaybackUrl` | Function | Main URL resolver (handles all URL types) | | `queuePresignedUrls` | Function | Batch presigned URL fetching | | `queuePresignedUrl` | Function | Single presigned URL fetching | | `getPresignedUrl` | Function | Synchronous cache lookup | | `markPresignedUrlsVerified` | Function | Enable direct S3 access | | `disablePresignedUrls` | Function | Fall back to proxy mode | | `arePresignedUrlsDisabled` | Function | Check if presigned URLs disabled | | `extractStoragePath` | Function | Extract storage path from URL | | `isRelativeStoragePath` | Function | Check if path is relative | | `flushPresignedUrlQueue` | Function | Flush pending batch | | `clearPresignedUrlCache` | Function | Clear presigned cache | | `setupPresignedUrlInterceptor` | Function | Setup Axios interceptor for CORS detection | **URL Resolution Logic:** ```typescript export const resolveAssetPlaybackUrl = (value?: string): string => { const normalized = String(value || '').trim(); if (!normalized) return ''; // Data and blob URLs pass through if (normalized.startsWith('data:') || normalized.startsWith('blob:')) return normalized; // Already an API file download URL if (normalized.startsWith('/api/file/download')) return normalized; // File download path (prepend API base) if (normalized.startsWith('/file/download')) return `${baseURLApi}${normalized}`; // Full URLs pass through if (normalized.startsWith('http://') || normalized.startsWith('https://')) return normalized; // Relative path - check presigned URL cache first const presigned = getPresignedUrl(normalized); if (presigned) return presigned; // Fallback to backend proxy return `${baseURLApi}/file/download?privateUrl=${encodeURIComponent(normalized)}`; }; ``` **Presigned URL Batching:** ```typescript // Batch queue for efficient presigned URL requests const BATCH_DELAY_MS = 10; // Small delay to batch concurrent requests const PRESIGN_TTL_MS = 60 * 60 * 1000; // 1 hour TTL const CACHE_BUFFER_MS = 5 * 60 * 1000; // Refresh 5 min before expiry // Usage: Queue multiple URLs for batch presigning const presignedUrls = await queuePresignedUrls([ 'assets/video1.mp4', 'assets/image1.jpg', 'assets/audio1.mp3', ]); ``` --- #### imagePreDecode.ts **Purpose:** Pre-decode images before display to prevent white flashes during transitions. **Key Exports:** | Export | Type | Description | |--------|------|-------------| | `decodeImage` | Function | Decode single image | | `decodeImages` | Function | Decode multiple images in parallel | | `extractPageImageUrls` | Function | Extract all image URLs from page | | `waitForPageImages` | Function | Wait for all page images to decode | **Usage Pattern:** ```typescript // Before page transition - ensure images are decoded await waitForPageImages(targetPage, 2000, { getCachedBlobUrl: preloadOrchestrator.getCachedBlobUrl, preloadedUrls: preloadOrchestrator.preloadedUrls, }); // Now safe to switch - no white flash setActivePage(targetPage); ``` --- ### 2. Element Utilities #### elementDefaults.ts **Purpose:** Single source of truth for UI element default values, creation, and merging. **Key Exports:** | Export | Type | Description | |--------|------|-------------| | `createDefaultElement` | Function | Create new element with defaults | | `mergeElementWithDefaults` | Function | Merge element with project/global defaults | | `buildElementSettings` | Function | Build JSON for database storage | | `parseElementSettings` | Function | Parse JSON from database | | `createDefaultGalleryCard` | Function | Create default gallery card | | `createDefaultCarouselSlide` | Function | Create default carousel slide | | `normalizeGalleryCard` | Function | Normalize gallery card properties | | `normalizeCarouselSlide` | Function | Normalize carousel slide properties | | `normalizeAppearDelaySec` | Function | Normalize appear delay value | | `normalizeAppearDurationSec` | Function | Normalize appear duration value | | `isElementFlagEnabled` | Function | Strictly parse boolean feature flags stored in element JSON | | `findDefaultOpenInfoPanelElements` | Function | Return all enabled Info Panels configured with `infoPanelOpenByDefault` | | `createLocalId` | Function | Generate unique local ID | | `ELEMENT_TYPE_LABELS` | Object | Display labels for element types | | `TYPE_SPECIFIC_DEFAULTS` | Object | Type-specific default values | | `getNavigationButtonLabel` | Function | Get navigation button label | | `getNavigationButtonKind` | Function | Get navigation button kind | | `getNavigationTypeFromKind` | Function | Get navigation type from kind | **Element Type Detection (Type Guards):** ```typescript // Single source of truth for element type detection export const isNavigationElementType = (type: string): type is 'navigation_next' | 'navigation_prev' => type === 'navigation_next' || type === 'navigation_prev'; export const isTooltipElementType = (type: string): type is 'tooltip' => type === 'tooltip'; export const isDescriptionElementType = (type: string): type is 'description' => type === 'description'; export const isGalleryElementType = (type: string): type is 'gallery' => type === 'gallery'; export const isCarouselElementType = (type: string): type is 'carousel' => type === 'carousel'; export const isMediaElementType = (type: string): type is 'video_player' | 'audio_player' => type === 'video_player' || type === 'audio_player'; export const isVideoPlayerElementType = (type: string): type is 'video_player' => type === 'video_player'; export const isAudioPlayerElementType = (type: string): type is 'audio_player' => type === 'audio_player'; export const isLogoElementType = (type: string): type is 'logo' => type === 'logo'; export const isSpotElementType = (type: string): type is 'spot' => type === 'spot'; export const isPopupElementType = (type: string): type is 'popup' => type === 'popup'; ``` **Default Values by Type:** ```typescript export const TYPE_SPECIFIC_DEFAULTS: Partial>> = { navigation_next: { navLabel: 'Forward', navType: 'forward', navDisabled: false, iconUrl: '', transitionReverseMode: 'auto_reverse', }, tooltip: { iconUrl: '', tooltipTitle: 'Tooltip title', tooltipText: 'Tooltip text', }, description: { descriptionTitle: 'TITLE', descriptionText: '', descriptionTitleFontSize: '24px', descriptionTextFontSize: '16px', // ... more properties }, video_player: { mediaUrl: '', mediaAutoplay: true, mediaLoop: true, mediaMuted: true, }, // ... more types }; ``` --- #### canvasScale.ts **Purpose:** Canvas scale utilities for responsive UI. Provides the foundation for the UI Adaptivity System. > **See:** [UI Adaptivity System](ui-adaptivity-system.md) for complete documentation. **Key Exports:** | Export | Type | Description | |--------|------|-------------| | `calculateCanvasScale` | Function | Calculate scale factor for viewport | | `toCU` | Function | Convert design pixels to `calc()` expression | | `isLegacyUnit` | Function | Check if value uses legacy units (px/vw/vh/rem) | | `normalizeToCanvasUnits` | Function | Convert legacy units to canvas units | | `getCanvasCssVars` | Function | Generate CSS custom properties object | | `vwToDesignPx` | Function | Convert vw to design pixels | | `vhToDesignPx` | Function | Convert vh to design pixels | | `remToDesignPx` | Function | Convert rem to design pixels | **Canvas Units (--cu):** ```typescript // toCU converts design pixels to CSS calc expression toCU(24) // → "calc(24 * var(--cu, 1px))" toCU(0) // → "0" // At 1920×1080 on 1920×1080 viewport: --cu = 1px // At 3840×2160 viewport: --cu = 2px (elements 2x larger) // At 960×540 viewport: --cu = 0.5px (elements half size) ``` **Scale Calculation:** ```typescript // Fit design canvas within viewport const scale = calculateCanvasScale( window.innerWidth, // viewportWidth window.innerHeight, // viewportHeight 1920, // designWidth 1080, // designHeight ); // Returns: min(viewportW/designW, viewportH/designH) clamped to [0.1, 4.0] ``` **CSS Custom Properties:** ```typescript const cssVars = getCanvasCssVars(scale, 1920, 1080); // Returns: // { // '--design-width': '1920', // '--design-height': '1080', // '--canvas-scale': '0.75', // '--cu': 'calc(1px * 0.75)', // } ``` --- #### elementStyles.ts **Purpose:** Unified types and utilities for UI element CSS styling with automatic canvas unit conversion. > **See:** [UI Adaptivity System](ui-adaptivity-system.md) for canvas unit documentation. **Key Exports:** | Export | Type | Description | |--------|------|-------------| | `buildElementStyle` | Function | Build React CSSProperties from element | | `normalizePixelValue` | Function | Normalize value to canvas units | | `normalizeViewportWidth` | Function | Convert vw to canvas units | | `normalizeViewportHeight` | Function | Convert vh to canvas units | | `ELEMENT_STYLE_PROPS` | Array | List of CSS property names | | `ElementStyleProperties` | Interface | TypeScript interface for styles | **Supported CSS Properties:** ```typescript export const ELEMENT_STYLE_PROPS = [ 'width', 'height', 'minWidth', 'maxWidth', 'minHeight', 'maxHeight', 'margin', 'padding', 'gap', 'fontSize', 'lineHeight', 'fontWeight', 'fontFamily', 'border', 'borderRadius', 'opacity', 'boxShadow', 'display', 'position', 'justifyContent', 'alignItems', 'textAlign', 'zIndex', 'backgroundColor', 'color', ] as const; ``` **Canvas Unit Normalization:** The module automatically normalizes numeric values to **canvas units (--cu)** for responsive scaling: | Property | Conversion | Example | |----------|------------|---------| | width, minWidth, maxWidth | Canvas units | `"200"` → `"calc(200 * var(--cu, 1px))"` | | height, minHeight, maxHeight | Canvas units | `"100"` → `"calc(100 * var(--cu, 1px))"` | | borderRadius, fontSize | Canvas units | `"12"` → `"calc(12 * var(--cu, 1px))"` | **Legacy Unit Conversion:** | Input | Output | |-------|--------| | `"24px"` | `"calc(24 * var(--cu, 1px))"` | | `"50vw"` | `"calc(960 * var(--cu, 1px))"` (50% of 1920) | | `"25vh"` | `"calc(270 * var(--cu, 1px))"` (25% of 1080) | | `"1.5rem"` | `"calc(24 * var(--cu, 1px))"` (1.5 × 16) | **Edge Cases Handled:** | Input | Output | Reason | |-------|--------|--------| | `"0"` | `"0"` | Zero → no unit needed | | `"var(--cu)"` | preserved | Already uses canvas units | | `"calc(...)"` | preserved | CSS function → preserve | | `"10px 20px"` | preserved | Complex → preserve | **Note:** `border` is NOT auto-normalized - it's a complex shorthand value (e.g., `"2px solid #ccc"`). **Usage:** ```typescript import { toCU } from './canvasScale'; // Build React CSSProperties from element data const style = buildElementStyle({ width: '200', // → "calc(200 * var(--cu, 1px))" height: '100', // → "calc(100 * var(--cu, 1px))" borderRadius: '12', // → "calc(12 * var(--cu, 1px))" opacity: '0.5', display: 'flex', zIndex: '10', }); // Or use toCU directly const buttonStyle = { fontSize: toCU(16), padding: `${toCU(8)} ${toCU(16)}`, }; ``` ### Opacity Percent Helpers `frontend/src/lib/opacityPercent.ts` centralizes conversion between author UI percent inputs and stored CSS opacity values: | Helper | Purpose | |--------|---------| | `opacityToPercentInput(value)` | Displays stored `0..1` opacity as clamped `0..100` percent text | | `percentInputToOpacityValue(value)` | Converts author `0..100` percent input back to a stored `0..1` string | | `percentInputToOpacityNumber(value)` | Converts author percent input to a numeric `0..1` value for system UI controls | Element style/effects forms and constructor system-control opacity fields use these helpers so all opacity inputs clamp consistently. Element opacity is part of the shared element style property list, so global and project element defaults cascade into newly created constructor elements the same way as width, padding, border radius, and other style defaults. --- #### gallerySectionStyles.ts **Purpose:** Unified types and utilities for gallery element section styling with CSS inheritance support. **Key Exports:** | Export | Type | Description | |--------|------|-------------| | `buildGalleryHeaderStyle` | Function | Build CSS for gallery header | | `buildGalleryTitleStyle` | Function | Build CSS for gallery title | | `buildGallerySpanStyle` | Function | Build CSS for info spans | | `buildGallerySpanGridStyle` | Function | Build CSS for span grid | | `buildGalleryCardStyle` | Function | Build CSS for gallery cards | | `buildGalleryCardTitleStyle` | Function | Build CSS for card title overlay | | `buildGalleryCardGridStyle` | Function | Build CSS for card grid | | `getGalleryGridColumns` | Function | Get grid columns with fallback | | `GALLERY_SECTION_DEFAULTS` | Object | Default styles per section | | `GALLERY_SECTION_STYLE_PROPS` | Array | All gallery style property names | **CSS Inheritance:** Gallery sections support CSS inheritance from the wrapper element. Inheritable properties (color, fontSize, fontWeight, lineHeight) are only applied to child sections if explicitly set, allowing values to cascade from General Element Styles. ```typescript // applyIfSet - only applies value if set (allows CSS inheritance) applyIfSet(style, 'color', element.galleryHeaderColor); applyIfSet(style, 'fontSize', element.galleryHeaderFontSize, normalizeRemValue); // applyWithDefault - applies value or falls back to default (blocks inheritance) applyWithDefault(style, 'padding', element.galleryHeaderPadding, defaults.padding, normalizeRemValue); ``` **Unit Normalization (rem and px):** Gallery styles use `rem` units for typography/spacing and `px` for dimensions: | Property | Unit | Default | Example | |----------|------|---------|---------| | fontSize | rem | `"1.5rem"` | `"0.875"` → `"0.875rem"` | | padding | rem | `"0.5rem 0.75rem"` | `"0.5"` → `"0.5rem"` | | borderRadius | rem | `"0.5rem"` | `"0.75"` → `"0.75rem"` | | gap | rem | `"0.5rem"` | `"1"` → `"1rem"` | | galleryHeaderHeight | px | (none) | `"200"` → `"200px"` | | galleryHeaderMinHeight | px | (none) | `"150"` → `"150px"` | | galleryHeaderMaxHeight | px | (none) | `"400"` → `"400px"` | | galleryCardHeight | px | (none) | `"200"` → `"200px"` | | galleryCardMinHeight | px | `"40px"` | `"150"` → `"150px"` | | galleryHeaderWidth | (none) | (none) | Preserved (e.g., `"100%"`) | | galleryCardWidth | (none) | (none) | Preserved (e.g., `"auto"`) | | galleryCardAspectRatio | (none) | `"4/3"` | Preserved (e.g., `"16/9"`) | **Section Defaults:** ```typescript export const GALLERY_SECTION_DEFAULTS = { header: { fontSize: '1.5rem', fontWeight: '700', padding: '0.25rem 0.5rem', textAlign: 'center' }, title: { backgroundColor: '#fefce8', color: '#1e293b', fontSize: '0.875rem', textAlign: 'center', ... }, span: { backgroundColor: '#334155', color: '#fef3c7', fontSize: '0.75rem', textAlign: 'center', ... }, card: { borderRadius: '0.5rem' }, wrapper: { backgroundColor: 'rgba(0,0,0,0.6)', padding: '0.75rem', ... }, }; ``` --- #### elementEffects.ts **Purpose:** Animation and interaction effect utilities for UI elements. **Key Exports:** | Export | Type | Description | |--------|------|-------------| | `buildTransitionStyle` | Function | Build base transition CSS | | `buildHoverStyle` | Function | Build hover state CSS | | `buildFocusStyle` | Function | Build focus state CSS | | `buildActiveStyle` | Function | Build active/press state CSS | | `buildAppearAnimationStyle` | Function | Build appear animation CSS | | `hasHoverEffects` | Function | Check if hover effects configured | | `hasAudioEffects` | Function | Check if audio effects configured | | `hasAnyEffects` | Function | Check if any effects configured (includes audio) | | `EFFECT_PROPS` | Array | List of effect property names | **Appear Animation Types:** ```typescript export type AppearAnimationType = | '' | 'fade' | 'slide-up' | 'slide-down' | 'slide-left' | 'slide-right' | 'scale'; ``` **Effect Properties:** ```typescript export interface ElementEffectProperties { // Appear animation appearAnimation?: AppearAnimationType; appearAnimationDuration?: string; appearAnimationEasing?: string; // Hover effects hoverScale?: string; hoverOpacity?: string; hoverBackgroundColor?: string; hoverColor?: string; hoverBoxShadow?: string; hoverTransitionDuration?: string; // Focus effects focusScale?: string; focusOpacity?: string; focusOutline?: string; focusBoxShadow?: string; // Active/press effects activeScale?: string; activeOpacity?: string; activeBackgroundColor?: string; // Hover Reveal effects hoverReveal?: boolean; hoverRevealInitialOpacity?: string; hoverRevealTargetOpacity?: string; hoverRevealDuration?: string; hoverRevealDelay?: string; hoverRevealPersist?: boolean; hoverPersistOnClick?: boolean; // Audio effects hoverAudioUrl?: string; // Audio to play on hover start clickAudioUrl?: string; // Audio to play on click/active audioVolume?: string; // Volume 0-1 as string } ``` **Important: Animation & Effects Wrapper Pattern** CSS animations with `animation-fill-mode: forwards` lock animated properties (opacity, transform), blocking CSS transitions for hover/focus/active states. To solve this, `CanvasElement` and `RuntimeElement` use a **wrapper div pattern**: ``` Outer div: positioning + appear animation └── Inner div: hover/focus/active effects (independent from animation) ``` This allows interactive effects to work without being blocked by the animation's fill mode. The animation stays on the outer div to maintain the positioning CSS hack (existing element positions depend on the animation's transform). --- #### constructorHelpers.ts **Purpose:** Utility functions for the constructor (tour builder) page. **Key Exports:** | Export | Type | Description | |--------|------|-------------| | `getAssetLabel` | Function | Format asset display label | | `getAssetSourceValue` | Function | Get asset storage key | | `buildAssetOptions` | Function | Build dropdown options for asset type | | `buildBackgroundImageOptions` | Function | Filter background images | | `buildTransitionVideoOptions` | Function | Filter transition videos | | `buildIconAssetOptions` | Function | Filter icon assets | | `getElementButtonTitle` | Function | Get element menu title | | `extractNumericValue` | Function | Extract number from CSS value | **Asset Options Building:** ```typescript // Build options for a specific asset type const videoOptions = buildAssetOptions(assets, 'video'); const audioOptions = buildAudioAssetOptions(assets); const backgroundOptions = buildBackgroundImageOptions(assets); // Transition videos prefer assets marked with type='transition' const transitionOptions = buildTransitionVideoOptions(assets); // Falls back to [TRANSITION] tag in name, then all videos ``` --- ### 3. Navigation & Preloading #### navigationHelpers.ts **Purpose:** Shared utilities for page navigation in RuntimePresentation and constructor. **Key Exports:** | Export | Type | Description | |--------|------|-------------| | `resolveNavigationTarget` | Function | Resolve target page from element | | `isBackNavigation` | Function | Check if navigation is backward | | `getNavigationDirection` | Function | Get 'back' or 'forward' | | `isTransitionBlocking` | Function | Check if transition blocks navigation | | `hasPlayableTransition` | Function | Check if transition can play | **Navigation Resolution:** ```typescript // Resolve target page from element with navigation properties const target = resolveNavigationTarget(element, pages); // Returns: { page, pageId, transitionVideoUrl, isBack } // Check if navigation should be blocked during transition const blocked = isTransitionBlocking(transitionPhase, isBuffering); // true during: 'preparing', 'playing', 'reversing', or buffering ``` --- #### extractPageLinks.ts **Purpose:** Extract synthetic page links and preload elements from tour pages' ui_schema_json. **Key Exports:** | Export | Type | Description | |--------|------|-------------| | `extractPageLinksAndElements` | Function | Extract links and elements for preloading | **Usage Pattern:** ```typescript // Extract preload data from pages const { pageLinks, preloadElements } = extractPageLinksAndElements(pages); // Use with preload orchestrator const preloadOrchestrator = usePreloadOrchestrator({ pages, pageLinks, elements: preloadElements, currentPageId, }); ``` **Extracted Data:** 1. **pageLinks** - Synthetic links for neighbor graph (enables preloading connected pages) 2. **preloadElements** - Elements with asset URLs for preload queue --- #### tourFlowHelpers.ts **Purpose:** Utilities for tour page routing and project handling. **Key Exports:** | Export | Type | Description | |--------|------|-------------| | `toRoutePath` | Function | Normalize value to valid route path | | `routeParts` | Function | Split route path into segments | | `compareRoutes` | Function | Compare routes for sorting | | `getProjectId` | Function | Extract project ID from various formats | | `getRows` | Function | Safe row extraction from API response | **Examples:** ```typescript toRoutePath('my-page') // '/my-page' toRoutePath('//double//slash') // '/double/slash' toRoutePath('') // '/' routeParts('/my/page/path') // ['my', 'page', 'path'] getProjectId({ projectId: '123' }) // '123' getProjectId({ project: { id: '456' } }) // '456' ``` --- #### resolveSlideTransition.ts **Purpose:** Cascade resolver for slide transitions in Gallery/Carousel elements. Resolves settings from page transitions with element-level override support. **Key Exports:** | Export | Type | Description | |--------|------|-------------| | `resolveSlideTransition` | Function | Resolve final slide transition settings with cascade | | `extractCarouselSlideOverride` | Function | Extract override from carousel element | | `extractGallerySlideOverride` | Function | Extract override from gallery element | **Cascade Order:** 1. Element override (highest priority) 2. Page transition settings (from `useTransitionSettings`) 3. Hardcoded fallback: `{ type: 'fade', durationMs: 700, easing: 'ease-in-out', overlayColor: '#000000' }` **Types:** ```typescript interface SlideTransitionOverride { type?: 'fade' | 'none' | ''; durationMs?: number | ''; easing?: EasingFunction | ''; overlayColor?: string; } interface ResolvedSlideTransition { type: 'fade' | 'none'; durationMs: number; easing: EasingFunction; overlayColor: string; } ``` **Usage:** ```typescript import { resolveSlideTransition, extractCarouselSlideOverride, } from '../lib/resolveSlideTransition'; // In carousel component const slideSettings = resolveSlideTransition( pageTransitionSettings, // From useTransitionSettings hook extractCarouselSlideOverride(element), ); // slideSettings is now resolved with cascade // { type: 'fade', durationMs: 700, easing: 'ease-in-out', overlayColor: '#000000' } ``` **Key Behaviors:** - `'video'` page transition type maps to `'fade'` for slides (no video between slides) - Empty string (`''`) values cascade to lower priority settings - `durationMs` must be a positive number to override --- ### 4. Media Utilities #### mediaDuration.ts **Purpose:** Probe media duration from URL or File using HTML5 media elements. **Key Exports:** | Export | Type | Description | |--------|------|-------------| | `probeMediaDuration` | Function | Get duration, width, height from media | | `isVideoMimeType` | Function | Check if MIME type is video | | `isAudioMimeType` | Function | Check if MIME type is audio | **Usage:** ```typescript // Probe video duration and dimensions const result = await probeMediaDuration('https://example.com/video.mp4', 'video'); // Returns: { duration: 30.5, width: 1920, height: 1080 } ``` --- #### mediaHelpers.ts **Purpose:** Media duration probing and formatting utilities. **Key Exports:** | Export | Type | Description | |--------|------|-------------| | `formatDurationNote` | Function | Format seconds to readable string | | `readMediaDuration` | Function | Read duration from URL | | `resolveDurationWithFallback` | Function | Duration with blob fallback | **Examples:** ```typescript formatDurationNote(90) // 'Duration: 1:30' formatDurationNote(45) // 'Duration: 45s' formatDurationNote(null) // 'Duration: unknown' // Resolve with CORS fallback const duration = await resolveDurationWithFallback('assets/video.mp4', 'video'); // First tries direct metadata loading // Falls back to blob download if CORS fails ``` --- ### 5. Audio Utilities #### presentationAudio.ts **Purpose:** Shared helper for detecting whether a presentation has any sound source. Used by runtime and constructor so the global mute/unmute button is shown consistently on every page when the presentation contains audio anywhere. **Rules checked:** - Page `background_audio_url` - Page `background_video_url` with `background_video_muted === false` - Element `hoverAudioUrl` / `clickAudioUrl` - Unmuted `audio_player` and `video_player` elements - Gallery video cards - Info Panel media-section video items Runtime calls `presentationHasAudio(pages)` for the currently loaded stage or production environment. Constructor calls `presentationHasAudio(pages, elements)` and also includes current unsaved background audio/video state so the sound button updates immediately while editing. #### backgroundAudioController.ts **Purpose:** Module-level singleton for coordinating background audio pause/resume when foreground audio (element hover/click sounds) plays. Implements audio ducking pattern. **Key Exports:** | Export | Type | Description | |--------|------|-------------| | `backgroundAudioController` | Object | Singleton controller instance | | `BackgroundAudioController` | Class | Controller class (for testing) | **API:** ```typescript interface BackgroundAudioController { register(audio: HTMLAudioElement | null): void; // Register background audio element notifyForegroundStart(): void; // Called when element audio starts notifyForegroundEnd(): void; // Called when element audio ends reset(): void; // Clear all state } ``` **Usage:** ```typescript import { backgroundAudioController } from '../lib/backgroundAudioController'; // In CanvasBackground - register background audio element useEffect(() => { backgroundAudioController.register(audioRef.current); return () => backgroundAudioController.register(null); }, [audioRef.current]); // In useAudioEffects - notify when element audio plays audio.addEventListener('play', () => { backgroundAudioController.notifyForegroundStart(); }); audio.addEventListener('ended', () => { backgroundAudioController.notifyForegroundEnd(); }); ``` **Key Behaviors:** - **Reference Counting:** Tracks active foreground audio count to handle overlapping sounds - **Graceful Resume:** Only resumes background audio when all foreground audio ends - **Playback State Tracking:** Only pauses if audio was playing, only resumes if it was paused by controller - **Singleton Pattern:** Single instance shared across all components --- ### 6. Font Configuration #### fonts.ts **Purpose:** Centralized configuration for supported fonts in the platform. **Key Exports:** | Export | Type | Description | |--------|------|-------------| | `FONT_OPTIONS` | Array | List of available font options | | `getFontByKey` | Function | Get font option by unique key | | `getFontByValues` | Function | Get font by fontFamily + fontStretch | | `getFontKeyFromValues` | Function | Get font key from stored values | | `getFontStyle` | Function | Get CSS style object for font | **FontOption Interface:** ```typescript interface FontOption { key: string; // Unique key (e.g., 'instrument-sans') label: string; // Display label for dropdowns fontFamily: string; // CSS font-family value fontStretch?: string; // Optional font-stretch for condensed variants } ``` **Available Fonts:** | Key | Label | Font Family | |-----|-------|-------------| | `instrument-sans` | Instrument Sans | 'Instrument Sans Variable', sans-serif | | `instrument-sans-condensed` | Instrument Sans Condensed | 'Instrument Sans Variable', sans-serif (75% stretch) | | `system-ui` | System UI | system-ui, sans-serif | | `arial` | Arial | Arial, sans-serif | | `helvetica` | Helvetica | Helvetica, sans-serif | | `georgia` | Georgia | Georgia, serif | | `times-new-roman` | Times New Roman | 'Times New Roman', serif | | `monospace` | Monospace | monospace | **Usage:** ```typescript import { FONT_OPTIONS, getFontByKey, getFontStyle } from '@/lib/fonts'; // Build dropdown options const options = FONT_OPTIONS.map(f => ({ value: f.key, label: f.label })); // Get font by key and apply style const font = getFontByKey('instrument-sans-condensed'); if (font) { const style = getFontStyle(font); // { fontFamily: "'Instrument Sans Variable', sans-serif", fontStretch: '75%' } } ``` --- ### 7. General Utilities #### logger.ts **Purpose:** Lightweight, isomorphic logger for Next.js applications. **Features:** - Works in browser and SSR - Log levels: debug, info, warn, error - Development: colored console output - Production: structured JSON for log aggregation - Child loggers with additional context **Usage:** ```typescript import { logger } from '@/lib/logger'; logger.info('User logged in', { userId: '123' }); logger.error('Failed to fetch data', error); // Child logger with persistent context const userLogger = logger.child({ userId: '123' }); userLogger.info('Action performed'); // Includes userId automatically ``` **Configuration:** ```typescript // Environment variables NEXT_PUBLIC_LOG_LEVEL=info // debug, info, warn, error ``` --- #### parseJson.ts **Purpose:** Safe JSON parsing utilities for values that may be strings or already parsed. **Key Exports:** | Export | Type | Description | |--------|------|-------------| | `parseJsonObject` | Function | Parse with fallback support | | `parseJsonField` | Function | Lenient parse (returns original on error) | | `getElementPreviewText` | Function | Extract preview text from content | **Usage:** ```typescript // Safe parsing with fallback const schema = parseJsonObject(page.ui_schema_json, {}); const settings = parseJsonObject(value, { enabled: false }); // Lenient parsing - returns original on error const content = parseJsonField(element.content_json); // Extract preview text for display const preview = getElementPreviewText(content); // Checks: title, text, subtitle, description, body, label, value ``` --- #### slugHelpers.ts **Purpose:** URL slug generation and validation utilities. **Key Exports:** | Export | Type | Description | |--------|------|-------------| | `sanitizeSlug` | Function | Sanitize string to valid slug | | `buildUniqueSlug` | Function | Generate unique slug avoiding collisions | | `isValidSlug` | Function | Validate slug format | | `slugPattern` | RegExp | Regex for valid slugs | **Examples:** ```typescript sanitizeSlug('My Page Title') // 'my-page-title' sanitizeSlug('Hello World!') // 'hello-world' const used = new Set(['my-page', 'my-page-2']); buildUniqueSlug('my-page', used) // 'my-page-3' isValidSlug('valid-slug') // true isValidSlug('Invalid Slug') // false ``` --- ### 8. Offline/PWA Module The offline module provides comprehensive PWA support with multi-tier storage. #### Storage Architecture ``` ┌─────────────────────────────────────────────────────────────┐ │ Storage Manager │ │ (Abstraction layer - auto-selects storage by size) │ └─────────────────────────────────────────────────────────────┘ │ ┌───────────────┼───────────────┐ ▼ │ ▼ ┌─────────────────────┐ │ ┌─────────────────────┐ │ Cache API │ │ │ IndexedDB │ │ (< 5MB files) │ │ │ (≥ 5MB files) │ │ │ │ │ │ │ • Fast access │ │ │ • Large file support│ │ • Service worker │ │ │ • Video blobs │ │ • HTTP semantics │ │ │ • Resume capability │ └─────────────────────┘ │ └─────────────────────┘ │ ▼ ┌─────────────────────┐ │ Download Manager │ │ │ │ • Priority queue │ │ • Retry logic │ │ • Progress tracking │ │ • Pause/resume │ └─────────────────────┘ │ ▼ ┌─────────────────────┐ │ Download EventBus │ │ │ │ • preloadStart │ │ • preloadProgress │ │ • preloadComplete │ │ • preloadError │ │ • queueUpdate │ └─────────────────────┘ ``` --- #### offline/StorageManager.ts **Purpose:** Abstraction layer for storing assets in Cache API or IndexedDB. **Key Methods:** | Method | Description | |--------|-------------| | `getStorageQuota()` | Get storage quota information | | `requestPersistentStorage()` | Request persistent storage | | `shouldUseIndexedDB(size)` | Determine storage location | | `storeAsset(url, blob, metadata)` | Store asset (auto-selects storage) | | `getAsset(url)` | Retrieve asset from any storage | | `hasAsset(url)` | Check if asset exists | | `deleteAsset(url, assetId)` | Delete from all locations | | `clearAll()` | Clear all offline storage | **Storage Decision:** ```typescript // Automatic storage selection based on file size static shouldUseIndexedDB(sizeBytes: number): boolean { return sizeBytes >= OFFLINE_CONFIG.storage.indexedDbMinSize; // 5MB } ``` --- #### offline/DownloadManager.ts **Purpose:** Manages asset downloads with queue, retry, and progress tracking. **Key Features:** - Priority-based download queue - Concurrent download limit - Automatic retry with exponential backoff - Progress tracking per download - Pause/resume support - Queue persistence for resume after reload **Key Methods:** | Method | Description | |--------|-------------| | `addJob(params)` | Add download to queue | | `pauseAll()` | Pause all downloads | | `resumeAll()` | Resume downloads | | `cancelJob(jobId)` | Cancel specific download | | `cancelProjectDownloads(projectId)` | Cancel project downloads | | `clearQueue()` | Clear entire queue | | `getStatus()` | Get queue status | | `restoreQueue()` | Restore from IndexedDB | **Priority Calculation:** ```typescript // Priority = assetType priority + variant priority // Higher priority downloads first private calculatePriority(assetType: AssetType, variantType: AssetVariantType): number { const typePriority = PRELOAD_CONFIG.priority.assetType[assetType] || 0; const variantPriority = PRELOAD_CONFIG.priority.variant[variantType] || 0; return typePriority + variantPriority; } ``` --- #### offline/DownloadEventBus.ts **Purpose:** Browser-native EventEmitter for asset preload progress tracking. **Event Types:** | Event | Payload | Description | |-------|---------|-------------| | `preloadStart` | `{ jobId, assetId, url }` | Download started | | `preloadProgress` | `{ jobId, progress, bytesLoaded, totalBytes }` | Progress update | | `preloadComplete` | `{ jobId, assetId }` | Download completed | | `preloadError` | `{ jobId, assetId, error }` | Download failed | | `queueUpdate` | void | Queue state changed | | `projectDownloadProgress` | `{ projectId, progress }` | Project progress | | `projectDownloadComplete` | `{ projectId }` | Project complete | **Usage:** ```typescript import { downloadEventBus } from '@/lib/offline/DownloadEventBus'; // Subscribe to progress const unsubscribe = downloadEventBus.on('preloadProgress', (data) => { console.log(`${data.progress}% - ${data.bytesLoaded}/${data.totalBytes}`); }); // Cleanup unsubscribe(); ``` --- #### offlineDb/schema.ts **Purpose:** IndexedDB schema definition using Dexie.js. **Tables:** ```typescript class OfflineDatabase extends Dexie { assets!: EntityTable; projects!: EntityTable; downloadQueue!: EntityTable; constructor() { super(OFFLINE_CONFIG.dbName); this.version(OFFLINE_CONFIG.dbVersion).stores({ // Large files (videos > 5MB) assets: 'id, projectId, url, variantType, assetType, downloadedAt', // Project offline status projects: 'id, slug, status, lastSyncedAt', // Download queue (for resume) downloadQueue: 'id, projectId, status, priority, addedAt', }); } } ``` --- #### offlineDb/OfflineDbManager.ts **Purpose:** CRUD operations for IndexedDB offline storage. **Asset Operations:** | Method | Description | |--------|-------------| | `storeAsset(asset)` | Store asset blob | | `getAsset(id)` | Get by ID | | `getAssetByUrl(url)` | Get by URL | | `getProjectAssets(projectId)` | Get all project assets | | `deleteAsset(id)` | Delete asset | | `deleteProjectAssets(projectId)` | Delete project assets | | `getTotalAssetsSize()` | Get total storage used | **Project Operations:** | Method | Description | |--------|-------------| | `upsertProject(project)` | Store/update project metadata | | `getProject(id)` | Get by ID | | `getProjectBySlug(slug)` | Get by slug | | `getAllProjects()` | Get all offline projects | | `updateProjectStatus(id, status)` | Update status | | `deleteProject(id)` | Delete project and assets | **Queue Operations:** | Method | Description | |--------|-------------| | `addToQueue(item)` | Add download item | | `getPendingQueue()` | Get pending items | | `updateQueueStatus(id, status)` | Update status | | `updateQueueProgress(id, bytes, total)` | Update progress | | `removeFromQueue(id)` | Remove item | | `resetFailedItems(projectId?)` | Reset failed to queued | --- ## Integration Patterns ### 1. Asset URL Resolution Flow ``` User requests asset │ ▼ resolveAssetPlaybackUrl() │ ├── data:/blob: URLs → Pass through │ ├── /api/file/download → Pass through │ ├── http(s):// URLs → Pass through │ └── Relative paths │ ├── Check presigned cache │ │ │ ├── Found → Return presigned URL │ │ │ └── Not found │ │ │ └── Return proxy URL │ /api/file/download?privateUrl=... │ └── Batch presigning available │ └── queuePresignedUrls() │ └── Fetch batch from server POST /api/file/presign ``` ### 2. Element Creation Flow ``` User adds element │ ▼ createDefaultElement(type) │ ├── Generate ID ├── Set position ├── Apply TYPE_SPECIFIC_DEFAULTS │ ▼ mergeElementWithDefaults(element, projectDefaults) │ ├── Apply style properties from defaults ├── Normalize position/timing ├── Handle arrays (galleryCards, carouselSlides) │ ▼ Element ready for use ``` ### 3. Offline Download Flow ``` User initiates download │ ▼ downloadManager.addJob() │ ├── Check if already cached → Skip ├── Check if in queue → Skip │ ▼ Add to priority queue │ ▼ processQueue() │ ├── Start download (fetch with streaming) │ │ │ ├── Emit preloadStart │ │ │ ├── Stream chunks │ │ │ │ │ ├── Update bytesLoaded │ │ ├── Emit preloadProgress │ │ └── Persist progress to IndexedDB │ │ │ └── Complete │ │ │ ▼ │ StorageManager.storeAsset() │ │ │ ├── Size < 5MB → Cache API │ └── Size ≥ 5MB → IndexedDB │ ├── Emit preloadComplete │ └── On error │ ├── Retry (up to maxRetries) │ └── Emit preloadError ``` --- ## Configuration Dependencies The lib module uses configuration from: | Config File | Purpose | |-------------|---------| | `config/preload.config.ts` | Preload priorities, asset fields, storage limits | | `config/offline.config.ts` | Cache names, DB name/version, event names | | `config/index.ts` | Base API URL | --- ## Best Practices ### 1. Always Use Type Guards ```typescript // Prefer type guards over string comparison if (isNavigationElementType(element.type)) { // TypeScript knows type is 'navigation_next' | 'navigation_prev' } ``` ### 2. Use Presigned URLs When Available ```typescript // Check presigned cache first for performance const url = getPresignedUrl(storageKey); if (url) { // Direct S3 access - faster } else { // Fall back to proxy } ``` ### 3. Pre-decode Images Before Transitions ```typescript // Always wait for images before page switch await waitForPageImages(targetPage, 2000, cacheProvider); setActivePage(targetPage); ``` ### 4. Handle Offline Storage Appropriately ```typescript // Let StorageManager decide storage location await StorageManager.storeAsset(url, blob, metadata); // Small files → Cache API // Large files → IndexedDB ``` --- ## Related Documentation - [hooks-module.md](./hooks-module.md) - Custom hooks using lib utilities - [stores-module.md](./stores-module.md) - Redux state management - [components-module.md](./components-module.md) - Components using lib utilities - [offline-pwa-mode.md](../../documentation/offline-pwa-mode.md) - PWA offline architecture - [assets-preloading.md](../../documentation/assets-preloading.md) - Asset preloading strategy