39948-vm/frontend/docs/lib-module.md
2026-07-03 16:11:24 +02:00

46 KiB
Raw Blame History

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:

  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:

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:

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
  • 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:

// 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_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:

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