46 KiB
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:
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:
// 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:
// 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):
// 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:
export const TYPE_SPECIFIC_DEFAULTS: Partial<Record<CanvasElementType, Partial<CanvasElement>>> = {
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 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):
// 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:
// 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:
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 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:
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:
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.
// 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:
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:
export type AppearAnimationType =
| ''
| 'fade'
| 'slide-up'
| 'slide-down'
| 'slide-left'
| 'slide-right'
| 'scale';
Effect Properties:
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:
// 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:
// 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:
// Extract preload data from pages
const { pageLinks, preloadElements } = extractPageLinksAndElements(pages);
// Use with preload orchestrator
const preloadOrchestrator = usePreloadOrchestrator({
pages,
pageLinks,
elements: preloadElements,
currentPageId,
});
Extracted Data:
- pageLinks - Synthetic links for neighbor graph (enables preloading connected pages)
- 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:
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:
- Element override (highest priority)
- Page transition settings (from
useTransitionSettings) - Hardcoded fallback:
{ type: 'fade', durationMs: 700, easing: 'ease-in-out', overlayColor: '#000000' }
Types:
interface SlideTransitionOverride {
type?: 'fade' | 'none' | '';
durationMs?: number | '';
easing?: EasingFunction | '';
overlayColor?: string;
}
interface ResolvedSlideTransition {
type: 'fade' | 'none';
durationMs: number;
easing: EasingFunction;
overlayColor: string;
}
Usage:
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 durationMsmust 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:
// 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:
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_urlwithbackground_video_muted === false - Element
hoverAudioUrl/clickAudioUrl - Unmuted
audio_playerandvideo_playerelements - 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:
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:
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:
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:
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:
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:
// 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:
// Safe parsing with fallback
const schema = parseJsonObject<ConstructorSchema>(page.ui_schema_json, {});
const settings = parseJsonObject<Settings>(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:
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:
// 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:
// 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:
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:
class OfflineDatabase extends Dexie {
assets!: EntityTable<OfflineAsset, 'id'>;
projects!: EntityTable<OfflineProject, 'id'>;
downloadQueue!: EntityTable<DownloadQueueItem, 'id'>;
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
// Prefer type guards over string comparison
if (isNavigationElementType(element.type)) {
// TypeScript knows type is 'navigation_next' | 'navigation_prev'
}
2. Use Presigned URLs When Available
// 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
// Always wait for images before page switch
await waitForPageImages(targetPage, 2000, cacheProvider);
setActivePage(targetPage);
4. Handle Offline Storage Appropriately
// Let StorageManager decide storage location
await StorageManager.storeAsset(url, blob, metadata);
// Small files → Cache API
// Large files → IndexedDB
Related Documentation
- hooks-module.md - Custom hooks using lib utilities
- stores-module.md - Redux state management
- components-module.md - Components using lib utilities
- offline-pwa-mode.md - PWA offline architecture
- assets-preloading.md - Asset preloading strategy