+
-
{message}
-
+
{message}
+
This presentation is optimized for landscape viewing
diff --git a/frontend/src/components/RuntimePresentation.tsx b/frontend/src/components/RuntimePresentation.tsx
index f942ea4..4bdfe53 100644
--- a/frontend/src/components/RuntimePresentation.tsx
+++ b/frontend/src/components/RuntimePresentation.tsx
@@ -94,7 +94,7 @@ export default function RuntimePresentation({
useCanvasScale({
designWidth: currentPage?.design_width ?? undefined,
designHeight: currentPage?.design_height ?? undefined,
- });
+ });
const [transitionPreview, setTransitionPreview] = useState<{
targetPageId: string;
@@ -542,186 +542,193 @@ export default function RuntimePresentation({
backgroundRepeat: 'no-repeat',
}}
>
-
- {/* Background image element - z-1 keeps it below backdrop blur (z-5).
+
+ {/* Background image element - z-1 keeps it below backdrop blur (z-5).
CSS backgroundImage provides instant display.
Use native img for blob URLs to prevent repeated fetch requests from Next.js Image. */}
- {backgroundImageUrl && !backgroundVideoUrl && (
-
- {backgroundImageUrl.startsWith('blob:') ? (
- // eslint-disable-next-line @next/next/no-img-element
-

{
- setIsBackgroundReady(true);
- pageSwitch.markBackgroundReady();
- }}
- onError={() => {
- setIsBackgroundReady(true);
- pageSwitch.markBackgroundReady();
- }}
- />
- ) : (
-
{
- setIsBackgroundReady(true);
- pageSwitch.markBackgroundReady();
- }}
- onError={() => {
- setIsBackgroundReady(true);
- pageSwitch.markBackgroundReady();
+ {backgroundImageUrl && !backgroundVideoUrl && (
+
+ {backgroundImageUrl.startsWith('blob:') ? (
+ // eslint-disable-next-line @next/next/no-img-element
+

{
+ setIsBackgroundReady(true);
+ pageSwitch.markBackgroundReady();
+ }}
+ onError={() => {
+ setIsBackgroundReady(true);
+ pageSwitch.markBackgroundReady();
+ }}
+ />
+ ) : (
+
{
+ setIsBackgroundReady(true);
+ pageSwitch.markBackgroundReady();
+ }}
+ onError={() => {
+ setIsBackgroundReady(true);
+ pageSwitch.markBackgroundReady();
+ }}
+ />
+ )}
+
+ )}
+
+ {/* Previous background overlay - shows during direct navigation until new bg is ready */}
+ {pageSwitch.previousBgImageUrl &&
+ pageSwitch.isSwitching &&
+ !pageSwitch.isNewBgReady && (
+
)}
-
- )}
- {/* Previous background overlay - shows during direct navigation until new bg is ready */}
- {pageSwitch.previousBgImageUrl &&
- pageSwitch.isSwitching &&
- !pageSwitch.isNewBgReady && (
-
)}
- {/* Background video - z-1 keeps it below backdrop blur (z-5) */}
- {backgroundVideoUrl && (
-
- )}
+ {/* Page elements - z-40 ensures they appear above carousel background (z-10) and carousel controls (z-30) */}
+
+ {pageElements.map((element: CanvasElement) => (
+ handleElementClick(element)}
+ resolveUrl={resolveUrlWithBlob}
+ onGalleryCardClick={(cardIndex) =>
+ handleGalleryCardClick(element, cardIndex)
+ }
+ />
+ ))}
+
- {/* Page elements - z-40 ensures they appear above carousel background (z-10) and carousel controls (z-30) */}
-
- {pageElements.map((element: CanvasElement) => (
-
handleElementClick(element)}
- resolveUrl={resolveUrlWithBlob}
- onGalleryCardClick={(cardIndex) =>
- handleGalleryCardClick(element, cardIndex)
- }
+ {/* Controls: Offline toggle and Fullscreen button */}
+
+
- ))}
-
-
- {/* Controls: Offline toggle and Fullscreen button */}
-
-
-
-
-
- {/* Transition overlay - uses useTransitionPlayback hook for smooth transitions */}
- {/* Opacity is 0 during 'preparing' phase to show old page while video loads */}
- {/* Also fades to 0 when isOverlayFadingOut to reveal the new page underneath */}
- {transitionPreview && (
-
-
- )}
- {/* Gallery Carousel Overlay */}
- {activeGalleryCarousel && (
- setActiveGalleryCarousel(null)}
- resolveUrl={resolveUrlWithBlob}
- prevIconUrl={
- activeGalleryCarousel.element.galleryCarouselPrevIconUrl
- }
- nextIconUrl={
- activeGalleryCarousel.element.galleryCarouselNextIconUrl
- }
- backIconUrl={
- activeGalleryCarousel.element.galleryCarouselBackIconUrl
- }
- backLabel={
- activeGalleryCarousel.element.galleryCarouselBackLabel || 'BACK'
- }
- prevX={activeGalleryCarousel.element.galleryCarouselPrevX}
- prevY={activeGalleryCarousel.element.galleryCarouselPrevY}
- nextX={activeGalleryCarousel.element.galleryCarouselNextX}
- nextY={activeGalleryCarousel.element.galleryCarouselNextY}
- backX={activeGalleryCarousel.element.galleryCarouselBackX}
- backY={activeGalleryCarousel.element.galleryCarouselBackY}
- prevWidth={activeGalleryCarousel.element.galleryCarouselPrevWidth}
- prevHeight={
- activeGalleryCarousel.element.galleryCarouselPrevHeight
- }
- nextWidth={activeGalleryCarousel.element.galleryCarouselNextWidth}
- nextHeight={
- activeGalleryCarousel.element.galleryCarouselNextHeight
- }
- backWidth={activeGalleryCarousel.element.galleryCarouselBackWidth}
- backHeight={
- activeGalleryCarousel.element.galleryCarouselBackHeight
- }
- isEditMode={false}
- />
- )}
-
+ {/* Transition overlay - uses useTransitionPlayback hook for smooth transitions */}
+ {/* Opacity is 0 during 'preparing' phase to show old page while video loads */}
+ {/* Also fades to 0 when isOverlayFadingOut to reveal the new page underneath */}
+ {transitionPreview && (
+
+
+
+ )}
+
+ {/* Gallery Carousel Overlay */}
+ {activeGalleryCarousel && (
+ setActiveGalleryCarousel(null)}
+ resolveUrl={resolveUrlWithBlob}
+ prevIconUrl={
+ activeGalleryCarousel.element.galleryCarouselPrevIconUrl
+ }
+ nextIconUrl={
+ activeGalleryCarousel.element.galleryCarouselNextIconUrl
+ }
+ backIconUrl={
+ activeGalleryCarousel.element.galleryCarouselBackIconUrl
+ }
+ backLabel={
+ activeGalleryCarousel.element.galleryCarouselBackLabel ||
+ 'BACK'
+ }
+ prevX={activeGalleryCarousel.element.galleryCarouselPrevX}
+ prevY={activeGalleryCarousel.element.galleryCarouselPrevY}
+ nextX={activeGalleryCarousel.element.galleryCarouselNextX}
+ nextY={activeGalleryCarousel.element.galleryCarouselNextY}
+ backX={activeGalleryCarousel.element.galleryCarouselBackX}
+ backY={activeGalleryCarousel.element.galleryCarouselBackY}
+ prevWidth={
+ activeGalleryCarousel.element.galleryCarouselPrevWidth
+ }
+ prevHeight={
+ activeGalleryCarousel.element.galleryCarouselPrevHeight
+ }
+ nextWidth={
+ activeGalleryCarousel.element.galleryCarouselNextWidth
+ }
+ nextHeight={
+ activeGalleryCarousel.element.galleryCarouselNextHeight
+ }
+ backWidth={
+ activeGalleryCarousel.element.galleryCarouselBackWidth
+ }
+ backHeight={
+ activeGalleryCarousel.element.galleryCarouselBackHeight
+ }
+ isEditMode={false}
+ />
+ )}
+
{/* End inner canvas container */}
diff --git a/frontend/src/components/UiElements/elements/CarouselElement.tsx b/frontend/src/components/UiElements/elements/CarouselElement.tsx
index 1cec1bf..e9addc0 100644
--- a/frontend/src/components/UiElements/elements/CarouselElement.tsx
+++ b/frontend/src/components/UiElements/elements/CarouselElement.tsx
@@ -248,7 +248,8 @@ const CarouselElement: React.FC = ({
if (!value || value.trim() === '') return undefined;
const trimmed = value.trim();
// If value already uses canvas units or calc, preserve it
- if (trimmed.includes('var(--cu') || trimmed.includes('--cu')) return trimmed;
+ if (trimmed.includes('var(--cu') || trimmed.includes('--cu'))
+ return trimmed;
if (/^calc\(/i.test(trimmed)) return trimmed;
// If value already has other units, convert them
const vwMatch = trimmed.match(/^(-?\d*\.?\d+)vw$/i);
diff --git a/frontend/src/hooks/useBackgroundDimensionSuggestion.ts b/frontend/src/hooks/useBackgroundDimensionSuggestion.ts
deleted file mode 100644
index 9c097a7..0000000
--- a/frontend/src/hooks/useBackgroundDimensionSuggestion.ts
+++ /dev/null
@@ -1,217 +0,0 @@
-/**
- * useBackgroundDimensionSuggestion Hook
- *
- * Detects background media dimensions and suggests canvas size updates.
- * Used in constructor to prompt user when background resolution differs from project settings.
- */
-
-import { useEffect, useCallback, useState, useRef } from 'react';
-import { logger } from '../lib/logger';
-
-interface MediaDimensions {
- width: number;
- height: number;
-}
-
-interface UseBackgroundDimensionSuggestionOptions {
- /** URL of the background media (image or video) */
- mediaUrl: string | undefined;
- /** Current project design width */
- currentDesignWidth: number;
- /** Current project design height */
- currentDesignHeight: number;
- /** Callback when dimensions differ from current settings */
- onSuggest: (width: number, height: number) => void;
- /** Whether suggestion is enabled */
- enabled?: boolean;
-}
-
-/**
- * Detect dimensions of an image URL.
- */
-async function getImageDimensions(
- url: string,
-): Promise {
- return new Promise((resolve) => {
- const img = new Image();
- img.onload = () => {
- resolve({
- width: img.naturalWidth,
- height: img.naturalHeight,
- });
- };
- img.onerror = () => {
- resolve(null);
- };
- img.src = url;
- });
-}
-
-/**
- * Detect dimensions of a video URL.
- */
-async function getVideoDimensions(
- url: string,
-): Promise {
- return new Promise((resolve) => {
- const video = document.createElement('video');
- video.preload = 'metadata';
-
- video.onloadedmetadata = () => {
- resolve({
- width: video.videoWidth,
- height: video.videoHeight,
- });
- // Clean up
- video.src = '';
- video.load();
- };
-
- video.onerror = () => {
- resolve(null);
- };
-
- video.src = url;
- });
-}
-
-/**
- * Determine if URL is likely a video based on extension.
- */
-function isVideoUrl(url: string): boolean {
- const videoExtensions = ['.mp4', '.webm', '.mov', '.avi', '.mkv', '.m4v'];
- const lowercaseUrl = url.toLowerCase();
- return videoExtensions.some((ext) => lowercaseUrl.includes(ext));
-}
-
-/**
- * Check if URL is a resolved/loadable URL (not a raw storage path).
- * Only process: http(s) URLs, blob URLs, data URLs
- */
-function isLoadableUrl(url: string): boolean {
- return (
- url.startsWith('http://') ||
- url.startsWith('https://') ||
- url.startsWith('blob:') ||
- url.startsWith('data:')
- );
-}
-
-/**
- * Get media dimensions from URL (image or video).
- */
-async function getMediaDimensions(
- url: string,
-): Promise {
- if (isVideoUrl(url)) {
- return getVideoDimensions(url);
- }
- return getImageDimensions(url);
-}
-
-/**
- * Hook to detect background media dimensions and suggest canvas updates.
- *
- * @example
- * const { suggestion, dismissSuggestion } = useBackgroundDimensionSuggestion({
- * mediaUrl: backgroundImageUrl,
- * currentDesignWidth: project.design_width ?? 1920,
- * currentDesignHeight: project.design_height ?? 1080,
- * onSuggest: (width, height) => {
- * // Show suggestion UI
- * },
- * });
- */
-export function useBackgroundDimensionSuggestion({
- mediaUrl,
- currentDesignWidth,
- currentDesignHeight,
- onSuggest,
- enabled = true,
-}: UseBackgroundDimensionSuggestionOptions) {
- const [suggestion, setSuggestion] = useState(null);
- const lastCheckedUrlRef = useRef(null);
- const dismissedUrlsRef = useRef>(new Set());
-
- const dismissSuggestion = useCallback(() => {
- if (mediaUrl) {
- dismissedUrlsRef.current.add(mediaUrl);
- }
- setSuggestion(null);
- }, [mediaUrl]);
-
- const acceptSuggestion = useCallback(() => {
- if (suggestion) {
- onSuggest(suggestion.width, suggestion.height);
- setSuggestion(null);
- }
- }, [suggestion, onSuggest]);
-
- useEffect(() => {
- if (!enabled || !mediaUrl) {
- setSuggestion(null);
- return;
- }
-
- // Skip raw storage paths - only process resolved URLs (http, https, blob, data)
- if (!isLoadableUrl(mediaUrl)) {
- return;
- }
-
- // Skip if already checked this URL
- if (lastCheckedUrlRef.current === mediaUrl) {
- return;
- }
-
- // Skip if user dismissed this URL
- if (dismissedUrlsRef.current.has(mediaUrl)) {
- return;
- }
-
- lastCheckedUrlRef.current = mediaUrl;
-
- const detectDimensions = async () => {
- try {
- const dimensions = await getMediaDimensions(mediaUrl);
-
- if (!dimensions) {
- logger.warn('[CanvasSuggestion] Failed to detect media dimensions', {
- url: mediaUrl,
- });
- return;
- }
-
- // Check if dimensions differ significantly (allow for small rounding)
- const widthDiff = Math.abs(dimensions.width - currentDesignWidth);
- const heightDiff = Math.abs(dimensions.height - currentDesignHeight);
- const threshold = 10; // pixels
-
- if (widthDiff > threshold || heightDiff > threshold) {
- logger.info('[CanvasSuggestion] Detected different dimensions', {
- media: dimensions,
- current: { width: currentDesignWidth, height: currentDesignHeight },
- });
- setSuggestion(dimensions);
- }
- } catch (error) {
- logger.error(
- '[CanvasSuggestion] Error detecting dimensions',
- error instanceof Error ? error : { error },
- );
- }
- };
-
- void detectDimensions();
- }, [mediaUrl, currentDesignWidth, currentDesignHeight, enabled]);
-
- return {
- /** Suggested dimensions from detected media */
- suggestion,
- /** Dismiss the current suggestion */
- dismissSuggestion,
- /** Accept the suggestion and call onSuggest callback */
- acceptSuggestion,
- /** Whether a suggestion is currently available */
- hasSuggestion: suggestion !== null,
- };
-}
diff --git a/frontend/src/hooks/useCanvasScale.ts b/frontend/src/hooks/useCanvasScale.ts
index 71f289a..dfbaf17 100644
--- a/frontend/src/hooks/useCanvasScale.ts
+++ b/frontend/src/hooks/useCanvasScale.ts
@@ -53,7 +53,9 @@ interface CanvasScaleResult {
*
* return {content}
;
*/
-export function useCanvasScale(options?: UseCanvasScaleOptions): CanvasScaleResult {
+export function useCanvasScale(
+ options?: UseCanvasScaleOptions,
+): CanvasScaleResult {
const designWidth = options?.designWidth ?? CANVAS_CONFIG.defaults.width;
const designHeight = options?.designHeight ?? CANVAS_CONFIG.defaults.height;
diff --git a/frontend/src/hooks/useConstructorData.ts b/frontend/src/hooks/useConstructorData.ts
index 27eace2..e4237ef 100644
--- a/frontend/src/hooks/useConstructorData.ts
+++ b/frontend/src/hooks/useConstructorData.ts
@@ -29,7 +29,11 @@ const EMPTY_ASSETS: Asset[] = [];
interface UseConstructorDataResult {
// Project
- project: { name: string; design_width?: number; design_height?: number } | null;
+ project: {
+ name: string;
+ design_width?: number;
+ design_height?: number;
+ } | null;
projectName: string;
// Pages
diff --git a/frontend/src/hooks/useOfflineMode.ts b/frontend/src/hooks/useOfflineMode.ts
index ab6074b..65e44e3 100644
--- a/frontend/src/hooks/useOfflineMode.ts
+++ b/frontend/src/hooks/useOfflineMode.ts
@@ -82,7 +82,13 @@ const formatBytes = (bytes: number): string => {
export function useOfflineMode(
options: UseOfflineModeOptions,
): UseOfflineModeResult {
- const { projectId, projectSlug, projectName, pages, enabled = true } = options;
+ const {
+ projectId,
+ projectSlug,
+ projectName,
+ pages,
+ enabled = true,
+ } = options;
const [projectInfo, setProjectInfo] = useState(null);
const [discoveredAssets, setDiscoveredAssets] = useState([]);
@@ -141,7 +147,9 @@ export function useOfflineMode(
// Find the asset to get its size
const asset = assetsRef.current.find(
- (a) => a.storageKey === data.storageKey || `offline-${a.storageKey}` === data.assetId,
+ (a) =>
+ a.storageKey === data.storageKey ||
+ `offline-${a.storageKey}` === data.assetId,
);
const assetSize = asset?.sizeBytes || 0;
@@ -152,7 +160,10 @@ export function useOfflineMode(
const downloaded = downloadedCountRef.current;
const dlBytes = downloadedBytesRef.current;
const total = assetsRef.current.length;
- const totalSize = assetsRef.current.reduce((sum, a) => sum + (a.sizeBytes || 0), 0);
+ const totalSize = assetsRef.current.reduce(
+ (sum, a) => sum + (a.sizeBytes || 0),
+ 0,
+ );
// Update state
setDownloadedAssets(downloaded);
@@ -266,7 +277,9 @@ export function useOfflineMode(
// Discover assets from pages (frontend-only, no backend call)
const assets = discoverAssets();
if (assets.length === 0) {
- throw new Error('No assets discovered. Make sure pages data is provided.');
+ throw new Error(
+ 'No assets discovered. Make sure pages data is provided.',
+ );
}
// Store assets for event-driven progress tracking
@@ -275,7 +288,10 @@ export function useOfflineMode(
setTotalAssets(assets.length);
// Estimate total size (may not have exact sizes for all assets)
- const estimatedTotalSize = assets.reduce((sum, a) => sum + (a.sizeBytes || 0), 0);
+ const estimatedTotalSize = assets.reduce(
+ (sum, a) => sum + (a.sizeBytes || 0),
+ 0,
+ );
setTotalBytes(estimatedTotalSize);
// Create or update project record
@@ -307,7 +323,8 @@ export function useOfflineMode(
// Partial downloads from online preload need to be re-downloaded fully for offline
const assetsToDownload: AssetToCache[] = [];
for (const asset of assets) {
- const assetInfo: CachedAssetInfo | null = await StorageManager.getAssetInfo(asset.storageKey);
+ const assetInfo: CachedAssetInfo | null =
+ await StorageManager.getAssetInfo(asset.storageKey);
if (assetInfo?.exists && !assetInfo.isPartial) {
// Fully cached, skip
@@ -318,10 +335,13 @@ export function useOfflineMode(
});
} else if (assetInfo?.exists && assetInfo.isPartial) {
// Partial download - need full download for offline
- logger.info('[useOfflineMode] Upgrading partial download for offline', {
- storageKey: asset.storageKey.slice(-50),
- partialSize: assetInfo.sizeBytes,
- });
+ logger.info(
+ '[useOfflineMode] Upgrading partial download for offline',
+ {
+ storageKey: asset.storageKey.slice(-50),
+ partialSize: assetInfo.sizeBytes,
+ },
+ );
assetsToDownload.push(asset);
} else {
// Not cached at all
@@ -367,9 +387,12 @@ export function useOfflineMode(
count: Object.keys(presignedUrls).length,
});
} catch (err) {
- logger.warn('[useOfflineMode] Failed to fetch presigned URLs, using proxy', {
- error: err instanceof Error ? err.message : 'unknown',
- });
+ logger.warn(
+ '[useOfflineMode] Failed to fetch presigned URLs, using proxy',
+ {
+ error: err instanceof Error ? err.message : 'unknown',
+ },
+ );
}
}
@@ -378,7 +401,9 @@ export function useOfflineMode(
// Progress is tracked via event subscriptions (see useEffect above)
for (const asset of assetsToDownload) {
// Resolve download URL - prefer presigned, fallback to proxy
- const downloadUrl = presignedUrls[asset.storageKey] || resolveAssetPlaybackUrl(asset.storageKey);
+ const downloadUrl =
+ presignedUrls[asset.storageKey] ||
+ resolveAssetPlaybackUrl(asset.storageKey);
downloadManager
.addJob({
@@ -491,7 +516,9 @@ export function useOfflineMode(
// Computed values
const isDownloaded = status === 'downloaded';
const isDownloading = status === 'downloading' && !isPaused;
- const estimatedSize = discoveredAssets.reduce((sum, a) => sum + (a.sizeBytes || 0), 0) || totalBytes;
+ const estimatedSize =
+ discoveredAssets.reduce((sum, a) => sum + (a.sizeBytes || 0), 0) ||
+ totalBytes;
return {
isOfflineCapable,
diff --git a/frontend/src/lib/assetCache/AssetCacheService.ts b/frontend/src/lib/assetCache/AssetCacheService.ts
index d8052b7..3736251 100644
--- a/frontend/src/lib/assetCache/AssetCacheService.ts
+++ b/frontend/src/lib/assetCache/AssetCacheService.ts
@@ -33,7 +33,11 @@ import type {
PreloadPageLink,
PreloadElement,
} from '../../types/preload';
-import type { AssetType, AssetVariantType, CachedAssetInfo } from '../../types/offline';
+import type {
+ AssetType,
+ AssetVariantType,
+ CachedAssetInfo,
+} from '../../types/offline';
// Re-export for convenience
export type { CachedAssetInfo };
@@ -102,7 +106,9 @@ export class AssetCacheService {
* Get cache status for a single asset
* Returns whether it exists, if it's partial, and size info
*/
- static async getAssetInfo(storageKey: string): Promise {
+ static async getAssetInfo(
+ storageKey: string,
+ ): Promise {
return StorageManager.getAssetInfo(storageKey);
}
@@ -225,7 +231,8 @@ export class AssetCacheService {
// Determine download parameters based on mode
const isOnlineMode = mode === 'online';
- const isCurrentPageAsset = currentPageId && asset.pageId === currentPageId;
+ const isCurrentPageAsset =
+ currentPageId && asset.pageId === currentPageId;
// Online mode: use partial downloads for neighbor page media
// Offline mode: always full downloads
diff --git a/frontend/src/lib/assetCache/assetDiscovery.ts b/frontend/src/lib/assetCache/assetDiscovery.ts
index cd1657a..7c1591e 100644
--- a/frontend/src/lib/assetCache/assetDiscovery.ts
+++ b/frontend/src/lib/assetCache/assetDiscovery.ts
@@ -47,10 +47,7 @@ const DEFAULT_OPTIONS: AssetDiscoveryOptions = {
/**
* Classify asset type based on field name
*/
-export function classifyAssetType(
- fieldName: string,
- url: string,
-): AssetType {
+export function classifyAssetType(fieldName: string, url: string): AssetType {
const lowerField = fieldName.toLowerCase();
const lowerUrl = url.toLowerCase();
@@ -331,7 +328,11 @@ export function getPrioritizedAssets(
assets.push({
...asset,
- priority: calculateAssetPriority(asset.assetType, isCurrentPage, distance),
+ priority: calculateAssetPriority(
+ asset.assetType,
+ isCurrentPage,
+ distance,
+ ),
});
};
@@ -348,8 +349,8 @@ export function getPrioritizedAssets(
const currentPageElements = elements.filter(
(el) => el.pageId === currentPageId,
);
- extractElementAssets(currentPageElements, currentPageId).forEach((asset) =>
- addAssetWithPriority(asset, true, 0),
+ extractElementAssets(currentPageElements, currentPageId).forEach(
+ (asset) => addAssetWithPriority(asset, true, 0),
);
}
@@ -402,7 +403,12 @@ export function toPreloadAssetInfo(asset: AssetToCache): PreloadAssetInfo {
return {
url: asset.originalUrl,
pageId: asset.pageId,
- assetType: assetType as 'image' | 'video' | 'audio' | 'transition' | 'other',
+ assetType: assetType as
+ | 'image'
+ | 'video'
+ | 'audio'
+ | 'transition'
+ | 'other',
priority: asset.priority,
};
}
diff --git a/frontend/src/lib/assetCache/index.ts b/frontend/src/lib/assetCache/index.ts
index 551221f..67c20fc 100644
--- a/frontend/src/lib/assetCache/index.ts
+++ b/frontend/src/lib/assetCache/index.ts
@@ -4,7 +4,11 @@
* Unified asset caching for online preload and offline download.
*/
-export { AssetCacheService, type DownloadProgress, type QueueDownloadOptions } from './AssetCacheService';
+export {
+ AssetCacheService,
+ type DownloadProgress,
+ type QueueDownloadOptions,
+} from './AssetCacheService';
export type { CachedAssetInfo } from '../../types/offline';
export {
diff --git a/frontend/src/lib/canvasScale.ts b/frontend/src/lib/canvasScale.ts
index 1afede8..1c35617 100644
--- a/frontend/src/lib/canvasScale.ts
+++ b/frontend/src/lib/canvasScale.ts
@@ -130,7 +130,13 @@ export function remToDesignPx(value: number): number {
*/
export function normalizeToCanvasUnits(
value: string | number | undefined,
- property: 'width' | 'height' | 'fontSize' | 'padding' | 'borderRadius' | 'gap',
+ property:
+ | 'width'
+ | 'height'
+ | 'fontSize'
+ | 'padding'
+ | 'borderRadius'
+ | 'gap',
designWidth: number = CANVAS_CONFIG.defaults.width,
designHeight: number = CANVAS_CONFIG.defaults.height,
): string {
diff --git a/frontend/src/lib/gallerySectionStyles.ts b/frontend/src/lib/gallerySectionStyles.ts
index afe3a42..f836dd0 100644
--- a/frontend/src/lib/gallerySectionStyles.ts
+++ b/frontend/src/lib/gallerySectionStyles.ts
@@ -417,7 +417,8 @@ export function buildGallerySpanGridStyle(
element: Partial,
): CSSProperties {
const columns = getGalleryGridColumns(element, 'span');
- const gap = normalizeCanvasUnit(element.gallerySpanGap) || 'calc(8 * var(--cu, 1px))';
+ const gap =
+ normalizeCanvasUnit(element.gallerySpanGap) || 'calc(8 * var(--cu, 1px))';
return {
display: 'grid',
@@ -518,7 +519,8 @@ export function buildGalleryCardGridStyle(
element: Partial,
): CSSProperties {
const columns = getGalleryGridColumns(element, 'card');
- const gap = normalizeCanvasUnit(element.galleryCardGap) || 'calc(8 * var(--cu, 1px))';
+ const gap =
+ normalizeCanvasUnit(element.galleryCardGap) || 'calc(8 * var(--cu, 1px))';
return {
display: 'grid',
diff --git a/frontend/src/lib/offline/DownloadManager.ts b/frontend/src/lib/offline/DownloadManager.ts
index 55b68a5..2b5d697 100644
--- a/frontend/src/lib/offline/DownloadManager.ts
+++ b/frontend/src/lib/offline/DownloadManager.ts
@@ -92,9 +92,12 @@ class DownloadManagerClass {
// For partial downloads, check session cache first (fast path)
if (isPartialDownload && this.partialDownloadsReady.has(storageKey)) {
- logger.info('[DownloadManager] Partial download already ready (session)', {
- storageKey: storageKey.slice(-50),
- });
+ logger.info(
+ '[DownloadManager] Partial download already ready (session)',
+ {
+ storageKey: storageKey.slice(-50),
+ },
+ );
return;
}
@@ -416,10 +419,13 @@ class DownloadManagerClass {
markPresignedUrlFailed(job.storageKey);
const proxyUrl = buildProxyUrl(job.storageKey);
- logger.info('[DownloadManager] Presigned URL failed, retrying with proxy', {
- storageKey: job.storageKey.slice(-50),
- error: errorMessage,
- });
+ logger.info(
+ '[DownloadManager] Presigned URL failed, retrying with proxy',
+ {
+ storageKey: job.storageKey.slice(-50),
+ error: errorMessage,
+ },
+ );
// Update job to use proxy URL and retry immediately
job.url = proxyUrl;
@@ -718,7 +724,10 @@ class DownloadManagerClass {
* This enables the SW to cache the full response when the browser fetches the media
* during playback, using the canonical storage key instead of the expiring presigned URL.
*/
- private registerUrlForCaching(presignedUrl: string, storageKey: string): void {
+ private registerUrlForCaching(
+ presignedUrl: string,
+ storageKey: string,
+ ): void {
if (navigator.serviceWorker?.controller) {
navigator.serviceWorker.controller.postMessage({
type: 'REGISTER_CACHE_URL',
diff --git a/frontend/src/pages/constructor.tsx b/frontend/src/pages/constructor.tsx
index 79303df..08a0897 100644
--- a/frontend/src/pages/constructor.tsx
+++ b/frontend/src/pages/constructor.tsx
@@ -86,9 +86,7 @@ import {
type NavigationElementType,
} from '../context/ConstructorContext';
import { useCanvasScale } from '../hooks/useCanvasScale';
-import { useBackgroundDimensionSuggestion } from '../hooks/useBackgroundDimensionSuggestion';
import { CANVAS_CONFIG } from '../config/canvas.config';
-import { CanvasDimensionSuggestion } from '../components/CanvasDimensionSuggestion';
// Constructor helpers (extracted utilities)
import {
@@ -195,35 +193,6 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
backgroundVideoEndTime,
} = usePageBackground();
- // Background dimension auto-detection for canvas size suggestions
- const handleDimensionSuggestionAccept = useCallback(
- async (width: number, height: number) => {
- if (!projectId) return;
- try {
- await axios.put(`/projects/${projectId}`, {
- data: { design_width: width, design_height: height },
- });
- // Refetch to update the project data
- await refetchData();
- } catch (error) {
- logger.error('Failed to update project dimensions', error instanceof Error ? error : { error });
- }
- },
- [projectId, refetchData],
- );
-
- const {
- suggestion: dimensionSuggestion,
- dismissSuggestion: dismissDimensionSuggestion,
- acceptSuggestion: acceptDimensionSuggestion,
- } = useBackgroundDimensionSuggestion({
- mediaUrl: backgroundImageUrl || backgroundVideoUrl,
- currentDesignWidth: project?.design_width ?? 1920,
- currentDesignHeight: project?.design_height ?? 1080,
- onSuggest: handleDimensionSuggestionAccept,
- enabled: !isElementEditMode && !!project,
- });
-
const [selectedMenuItem, setSelectedMenuItem] =
useState('none');
// Transition preview state managed by useTransitionPreview hook (below)
@@ -389,6 +358,16 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
// Destructure stable callback reference to avoid infinite loops in useEffect deps
const pageSwitchToPage = pageSwitch.switchToPage;
+ // Use shared background transition hook for direct navigation clearing and fade-in
+ // (No fade-out needed in constructor - transitions complete immediately)
+ // NOTE: Must be defined before switchToPage callback which uses resetFadeIn
+ const { isFadingIn, elementsOpacity, resetFadeIn } = useBackgroundTransition({
+ pageSwitch,
+ fadeIn: {
+ hasActiveTransition: Boolean(transitionPreview),
+ },
+ });
+
// Helper to switch pages without flash
// Uses usePageSwitch hook to resolve blob URLs from preload cache
// Also updates storage path state for editing/saving purposes
@@ -424,7 +403,12 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
},
);
},
- [pageSwitchToPage, updateBackgroundFromPage, applyPageSelection, resetFadeIn],
+ [
+ pageSwitchToPage,
+ updateBackgroundFromPage,
+ applyPageSelection,
+ resetFadeIn,
+ ],
);
const { isBuffering: isReverseBuffering } = useTransitionPlayback({
@@ -481,15 +465,6 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
},
});
- // Use shared background transition hook for direct navigation clearing and fade-in
- // (No fade-out needed in constructor - transitions complete immediately)
- const { isFadingIn, elementsOpacity, resetFadeIn } = useBackgroundTransition({
- pageSwitch,
- fadeIn: {
- hasActiveTransition: Boolean(transitionPreview),
- },
- });
-
const iconPreloadTargets = useMemo(() => {
const preloadableTypes: CanvasElementType[] = [
'navigation_next',
@@ -1479,7 +1454,11 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
ref={canvasRef}
tabIndex={-1}
className={`z-20 overflow-clip ${hasFullWidthCarousel ? 'bg-transparent' : 'bg-black'}`}
- style={{ ...canvasCssVars, ...letterboxStyles, ...canvasBackgroundStyle }}
+ style={{
+ ...canvasCssVars,
+ ...letterboxStyles,
+ ...canvasBackgroundStyle,
+ }}
>
{
/>
)}
- {/* Canvas Dimension Suggestion */}
- {dimensionSuggestion && (
-
- )}
-