Revert to version b4a42c7
This commit is contained in:
parent
e0f32356ba
commit
e0a848b50c
@ -20,9 +20,7 @@ passport.use(
|
||||
async (req, token, done) => {
|
||||
try {
|
||||
// Use lightweight auth query - only loads essential fields + permissions
|
||||
const user = await UsersDBApi.findByForAuth({
|
||||
email: token.user.email,
|
||||
});
|
||||
const user = await UsersDBApi.findByForAuth({ email: token.user.email });
|
||||
|
||||
if (user && user.disabled) {
|
||||
return done(new Error(`User '${user.email}' is disabled`));
|
||||
|
||||
@ -455,16 +455,10 @@ const downloadFile = async (req, res) => {
|
||||
const chunkSize = end - start + 1;
|
||||
|
||||
res.status(206);
|
||||
res.setHeader(
|
||||
'Content-Range',
|
||||
`bytes ${start}-${end}/${stats.size}`,
|
||||
);
|
||||
res.setHeader('Content-Range', `bytes ${start}-${end}/${stats.size}`);
|
||||
res.setHeader('Content-Length', chunkSize);
|
||||
res.setHeader('ETag', etag);
|
||||
res.setHeader(
|
||||
'Cache-Control',
|
||||
`public, max-age=${config.s3CacheMaxAge}`,
|
||||
);
|
||||
res.setHeader('Cache-Control', `public, max-age=${config.s3CacheMaxAge}`);
|
||||
|
||||
return fs.createReadStream(cachePath, { start, end }).pipe(res);
|
||||
}
|
||||
@ -490,22 +484,12 @@ const downloadFile = async (req, res) => {
|
||||
const rangeHeader = req.headers.range;
|
||||
if (rangeHeader) {
|
||||
// For Range requests, we need to get file size first via headObject
|
||||
const headResult = await s3.download(privateUrl, {
|
||||
signal,
|
||||
headOnly: true,
|
||||
});
|
||||
const headResult = await s3.download(privateUrl, { signal, headOnly: true });
|
||||
const totalSize = headResult.contentLength;
|
||||
|
||||
if (!totalSize) {
|
||||
log.warn(
|
||||
{ privateUrl },
|
||||
'Cannot determine file size for range request',
|
||||
);
|
||||
return res
|
||||
.status(500)
|
||||
.send(
|
||||
createErrorResponse('Cannot determine file size', 'SIZE_UNKNOWN'),
|
||||
);
|
||||
log.warn({ privateUrl }, 'Cannot determine file size for range request');
|
||||
return res.status(500).send(createErrorResponse('Cannot determine file size', 'SIZE_UNKNOWN'));
|
||||
}
|
||||
|
||||
const range = parseRangeHeader(rangeHeader, totalSize);
|
||||
@ -526,18 +510,12 @@ const downloadFile = async (req, res) => {
|
||||
res.status(206);
|
||||
res.setHeader('Content-Range', `bytes ${start}-${end}/${totalSize}`);
|
||||
res.setHeader('Content-Length', chunkSize);
|
||||
if (rangeResult.contentType)
|
||||
res.setHeader('Content-Type', rangeResult.contentType);
|
||||
res.setHeader(
|
||||
'Cache-Control',
|
||||
`public, max-age=${config.s3CacheMaxAge}`,
|
||||
);
|
||||
if (rangeResult.contentType) res.setHeader('Content-Type', rangeResult.contentType);
|
||||
res.setHeader('Cache-Control', `public, max-age=${config.s3CacheMaxAge}`);
|
||||
|
||||
if (typeof rangeResult.body.pipe === 'function') {
|
||||
return rangeResult.body.pipe(res);
|
||||
} else if (
|
||||
typeof rangeResult.body.transformToByteArray === 'function'
|
||||
) {
|
||||
} else if (typeof rangeResult.body.transformToByteArray === 'function') {
|
||||
const bytes = await rangeResult.body.transformToByteArray();
|
||||
return res.send(Buffer.from(bytes));
|
||||
} else {
|
||||
@ -660,9 +638,7 @@ const downloadFile = async (req, res) => {
|
||||
const localFilePath = path.join(config.uploadDir, privateUrl);
|
||||
|
||||
if (!fs.existsSync(localFilePath)) {
|
||||
return res
|
||||
.status(404)
|
||||
.send(createErrorResponse('File not found', 'NOT_FOUND'));
|
||||
return res.status(404).send(createErrorResponse('File not found', 'NOT_FOUND'));
|
||||
}
|
||||
|
||||
const stats = fs.statSync(localFilePath);
|
||||
|
||||
@ -725,11 +725,7 @@ class TourPagesService extends BaseService {
|
||||
* @returns {Promise<Object>} Status object with ready keys and their reversed URLs
|
||||
*/
|
||||
static async checkReverseVideoStatus(storageKeys) {
|
||||
if (
|
||||
!storageKeys ||
|
||||
!Array.isArray(storageKeys) ||
|
||||
storageKeys.length === 0
|
||||
) {
|
||||
if (!storageKeys || !Array.isArray(storageKeys) || storageKeys.length === 0) {
|
||||
return { ready: {}, pending: [], allReady: true };
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -10,11 +10,11 @@
|
||||
import React from 'react';
|
||||
import UiElementRenderer from '../UiElements/UiElementRenderer';
|
||||
import { useElementEffects } from '../../hooks/useElementEffects';
|
||||
import { useAppearAnimation } from '../../hooks/useAppearAnimation';
|
||||
import {
|
||||
buildTransitionStyle,
|
||||
buildAppearAnimationStyle,
|
||||
hasAnyEffects,
|
||||
extractEffectProperties,
|
||||
type ElementEffectProperties,
|
||||
} from '../../lib/elementEffects';
|
||||
import type { CanvasElement as CanvasElementType } from '../../types/constructor';
|
||||
import type { ResolvedTransitionSettings } from '../../types/transition';
|
||||
@ -57,38 +57,31 @@ const CanvasElement: React.FC<CanvasElementProps> = ({
|
||||
pageTransitionSettings,
|
||||
preloadCache,
|
||||
}) => {
|
||||
// Extract effect properties from element using helper
|
||||
const effectProperties = extractEffectProperties(
|
||||
element as unknown as Record<string, unknown>,
|
||||
);
|
||||
|
||||
// Use appear animation hook (removes animation after completion to unlock properties)
|
||||
const { animationStyle, onAnimationEnd, hasAnimationEnded } =
|
||||
useAppearAnimation(
|
||||
effectProperties,
|
||||
element.id, // Reset animation when element changes
|
||||
);
|
||||
// Extract effect properties from element
|
||||
const effectProperties: Partial<ElementEffectProperties> = {
|
||||
appearAnimation: element.appearAnimation,
|
||||
appearAnimationDuration: element.appearAnimationDuration,
|
||||
appearAnimationEasing: element.appearAnimationEasing,
|
||||
hoverScale: element.hoverScale,
|
||||
hoverOpacity: element.hoverOpacity,
|
||||
hoverBackgroundColor: element.hoverBackgroundColor,
|
||||
hoverColor: element.hoverColor,
|
||||
hoverBoxShadow: element.hoverBoxShadow,
|
||||
hoverTransitionDuration: element.hoverTransitionDuration,
|
||||
focusScale: element.focusScale,
|
||||
focusOpacity: element.focusOpacity,
|
||||
focusOutline: element.focusOutline,
|
||||
focusBoxShadow: element.focusBoxShadow,
|
||||
activeScale: element.activeScale,
|
||||
activeOpacity: element.activeOpacity,
|
||||
activeBackgroundColor: element.activeBackgroundColor,
|
||||
};
|
||||
|
||||
// Use effects hook - disabled in edit mode to avoid interfering with dragging
|
||||
const { effectStyle, eventHandlers, onPersistClick } = useElementEffects(
|
||||
const { effectStyle, eventHandlers } = useElementEffects(
|
||||
isEditMode ? {} : effectProperties,
|
||||
isEditMode
|
||||
? undefined
|
||||
: {
|
||||
resetKey: element.id,
|
||||
appearAnimationCompleted:
|
||||
hasAnimationEnded || !effectProperties.appearAnimation,
|
||||
},
|
||||
);
|
||||
|
||||
// Combined click handler (only persist click in preview mode)
|
||||
const handleClick = () => {
|
||||
if (!isEditMode) {
|
||||
onPersistClick();
|
||||
}
|
||||
onClick();
|
||||
};
|
||||
|
||||
// Clamp position to canvas bounds (0-100%)
|
||||
const clamp = (value: number, min: number, max: number) =>
|
||||
Math.min(Math.max(value, min), max);
|
||||
@ -121,6 +114,14 @@ const CanvasElement: React.FC<CanvasElementProps> = ({
|
||||
};
|
||||
}
|
||||
|
||||
// Add appear animation (ALWAYS - for WYSIWYG)
|
||||
if (effectProperties.appearAnimation) {
|
||||
positionStyle = {
|
||||
...positionStyle,
|
||||
...buildAppearAnimationStyle(effectProperties),
|
||||
};
|
||||
}
|
||||
|
||||
// Handle keyboard interaction for accessibility
|
||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
@ -129,68 +130,29 @@ const CanvasElement: React.FC<CanvasElementProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// Check if appear animation uses transform (slide/scale need separate wrapper)
|
||||
const needsAnimationWrapper =
|
||||
effectProperties.appearAnimation &&
|
||||
effectProperties.appearAnimation !== 'fade';
|
||||
|
||||
// Render content
|
||||
const content = (
|
||||
<UiElementRenderer
|
||||
element={element}
|
||||
resolveUrl={resolveUrl}
|
||||
isSelected={isSelected}
|
||||
isEditMode={isEditMode}
|
||||
onGalleryCardClick={onGalleryCardClick}
|
||||
onCarouselButtonPositionChange={onCarouselButtonPositionChange}
|
||||
letterboxStyles={letterboxStyles}
|
||||
pageTransitionSettings={pageTransitionSettings}
|
||||
preloadCache={preloadCache}
|
||||
/>
|
||||
);
|
||||
|
||||
// For slide/scale (uses transform), use separate wrapper to avoid conflict with positioning
|
||||
if (needsAnimationWrapper) {
|
||||
return (
|
||||
<div
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
data-constructor-element-id={element.id}
|
||||
className='absolute cursor-pointer'
|
||||
style={positionStyle}
|
||||
onMouseDown={isEditMode ? onMouseDown : undefined}
|
||||
onClick={handleClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
{...(!isEditMode ? eventHandlers : {})}
|
||||
>
|
||||
<div style={animationStyle} onAnimationEnd={onAnimationEnd}>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Fade or no animation: apply animation to positioning div
|
||||
const combinedStyle = effectProperties.appearAnimation
|
||||
? { ...positionStyle, ...animationStyle }
|
||||
: positionStyle;
|
||||
|
||||
return (
|
||||
<div
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
data-constructor-element-id={element.id}
|
||||
className='absolute cursor-pointer'
|
||||
style={combinedStyle}
|
||||
style={positionStyle}
|
||||
onMouseDown={isEditMode ? onMouseDown : undefined}
|
||||
onClick={handleClick}
|
||||
onClick={onClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
onAnimationEnd={
|
||||
effectProperties.appearAnimation ? onAnimationEnd : undefined
|
||||
}
|
||||
{...(!isEditMode ? eventHandlers : {})}
|
||||
>
|
||||
{content}
|
||||
<UiElementRenderer
|
||||
element={element}
|
||||
resolveUrl={resolveUrl}
|
||||
isSelected={isSelected}
|
||||
isEditMode={isEditMode}
|
||||
onGalleryCardClick={onGalleryCardClick}
|
||||
onCarouselButtonPositionChange={onCarouselButtonPositionChange}
|
||||
letterboxStyles={letterboxStyles}
|
||||
pageTransitionSettings={pageTransitionSettings}
|
||||
preloadCache={preloadCache}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -85,8 +85,8 @@ const CreatePageModal: React.FC<CreatePageModalProps> = ({
|
||||
|
||||
return (
|
||||
<CardBoxModal
|
||||
title='Create page'
|
||||
buttonColor='info'
|
||||
title="Create page"
|
||||
buttonColor="info"
|
||||
buttonLabel={isCreating ? 'Creating...' : 'Create'}
|
||||
isConfirmDisabled={isConfirmDisabled}
|
||||
isActive={isActive}
|
||||
@ -95,23 +95,23 @@ const CreatePageModal: React.FC<CreatePageModalProps> = ({
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
htmlFor='create-page-name'
|
||||
className='block text-sm font-semibold mb-1'
|
||||
htmlFor="create-page-name"
|
||||
className="block text-sm font-semibold mb-1"
|
||||
>
|
||||
Page name
|
||||
</label>
|
||||
<input
|
||||
id='create-page-name'
|
||||
type='text'
|
||||
id="create-page-name"
|
||||
type="text"
|
||||
value={pageName}
|
||||
onChange={(e) => handleNameChange(e.target.value)}
|
||||
placeholder='Enter page name'
|
||||
className='w-full border border-gray-300 rounded px-3 py-2 bg-white dark:bg-dark-800'
|
||||
placeholder="Enter page name"
|
||||
className="w-full border border-gray-300 rounded px-3 py-2 bg-white dark:bg-dark-800"
|
||||
autoFocus
|
||||
maxLength={255}
|
||||
/>
|
||||
{(nameError || nameValidationError) && (
|
||||
<p className='text-xs text-red-600 mt-1'>
|
||||
<p className="text-xs text-red-600 mt-1">
|
||||
{nameError || nameValidationError}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@ -764,21 +764,6 @@ export function ElementEditorPanel({
|
||||
activeOpacity: selectedElement.activeOpacity || '',
|
||||
activeBackgroundColor:
|
||||
selectedElement.activeBackgroundColor || '',
|
||||
// Hover reveal values
|
||||
hoverReveal: selectedElement.hoverReveal ? 'true' : '',
|
||||
hoverRevealInitialOpacity:
|
||||
selectedElement.hoverRevealInitialOpacity || '',
|
||||
hoverRevealTargetOpacity:
|
||||
selectedElement.hoverRevealTargetOpacity || '',
|
||||
hoverRevealDuration:
|
||||
selectedElement.hoverRevealDuration || '',
|
||||
hoverRevealDelay: selectedElement.hoverRevealDelay || '',
|
||||
hoverRevealPersist: selectedElement.hoverRevealPersist
|
||||
? 'true'
|
||||
: '',
|
||||
hoverPersistOnClick: selectedElement.hoverPersistOnClick
|
||||
? 'true'
|
||||
: '',
|
||||
// Slide transition values (gallery/carousel)
|
||||
slideTransitionType:
|
||||
selectedElement.type === 'gallery'
|
||||
@ -873,15 +858,6 @@ export function ElementEditorPanel({
|
||||
value || undefined,
|
||||
});
|
||||
}
|
||||
} else if (
|
||||
prop === 'hoverReveal' ||
|
||||
prop === 'hoverRevealPersist' ||
|
||||
prop === 'hoverPersistOnClick'
|
||||
) {
|
||||
// Boolean properties - convert 'true' string to boolean
|
||||
updateSelectedElement({
|
||||
[prop]: value === 'true',
|
||||
});
|
||||
} else {
|
||||
// Standard effect properties
|
||||
updateSelectedElement({
|
||||
|
||||
@ -114,98 +114,6 @@ const EffectsSettingsSection: React.FC<EffectsSettingsSectionProps> = ({
|
||||
placeholder='e.g. 0.2'
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label='Persist effects after click'>
|
||||
<label className='inline-flex items-center gap-2'>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={values.hoverPersistOnClick === 'true'}
|
||||
onChange={(e) =>
|
||||
onChange(
|
||||
'hoverPersistOnClick',
|
||||
e.target.checked ? 'true' : '',
|
||||
)
|
||||
}
|
||||
/>
|
||||
Keep hover effects visible after clicking
|
||||
</label>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
{/* Hover Reveal Subsection */}
|
||||
<div className='mt-4 pt-4 border-t border-gray-200'>
|
||||
<p className='text-xs font-medium text-gray-700 mb-3'>Hover Reveal</p>
|
||||
<p className='text-xs text-gray-500 mb-3'>
|
||||
Element starts hidden and reveals on hover. Useful for hints or
|
||||
hidden navigation.
|
||||
</p>
|
||||
<div className='grid gap-3 md:grid-cols-2'>
|
||||
<FormField label='Enable Hover Reveal'>
|
||||
<label className='inline-flex items-center gap-2'>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={values.hoverReveal === 'true'}
|
||||
onChange={(e) =>
|
||||
onChange('hoverReveal', e.target.checked ? 'true' : '')
|
||||
}
|
||||
/>
|
||||
Element starts invisible
|
||||
</label>
|
||||
</FormField>
|
||||
<FormField label='Stay visible after hover'>
|
||||
<label className='inline-flex items-center gap-2'>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={values.hoverRevealPersist === 'true'}
|
||||
onChange={(e) =>
|
||||
onChange(
|
||||
'hoverRevealPersist',
|
||||
e.target.checked ? 'true' : '',
|
||||
)
|
||||
}
|
||||
disabled={values.hoverReveal !== 'true'}
|
||||
/>
|
||||
Persist visibility
|
||||
</label>
|
||||
</FormField>
|
||||
<FormField label='Initial opacity'>
|
||||
<input
|
||||
value={values.hoverRevealInitialOpacity || ''}
|
||||
onChange={(e) =>
|
||||
onChange('hoverRevealInitialOpacity', e.target.value)
|
||||
}
|
||||
placeholder='0'
|
||||
disabled={values.hoverReveal !== 'true'}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label='Target opacity'>
|
||||
<input
|
||||
value={values.hoverRevealTargetOpacity || ''}
|
||||
onChange={(e) =>
|
||||
onChange('hoverRevealTargetOpacity', e.target.value)
|
||||
}
|
||||
placeholder='1'
|
||||
disabled={values.hoverReveal !== 'true'}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label='Reveal duration (sec)'>
|
||||
<input
|
||||
value={values.hoverRevealDuration || ''}
|
||||
onChange={(e) =>
|
||||
onChange('hoverRevealDuration', e.target.value)
|
||||
}
|
||||
placeholder='0.3'
|
||||
disabled={values.hoverReveal !== 'true'}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label='Reveal delay (sec)'>
|
||||
<input
|
||||
value={values.hoverRevealDelay || ''}
|
||||
onChange={(e) => onChange('hoverRevealDelay', e.target.value)}
|
||||
placeholder='0'
|
||||
disabled={values.hoverReveal !== 'true'}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -152,108 +152,6 @@ const EffectsSettingsSectionCompact: React.FC<
|
||||
placeholder='0.2'
|
||||
/>
|
||||
</div>
|
||||
<div className='col-span-2'>
|
||||
<label className='mb-1 flex items-center gap-2 text-[10px] text-white/60'>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={values.hoverPersistOnClick === 'true'}
|
||||
onChange={(e) =>
|
||||
onChange(
|
||||
'hoverPersistOnClick',
|
||||
e.target.checked ? 'true' : '',
|
||||
)
|
||||
}
|
||||
className='rounded border-gray-300'
|
||||
/>
|
||||
Persist effects after click
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Hover Reveal Subsection */}
|
||||
<div className='col-span-2 mt-2 pt-2 border-t border-white/20'>
|
||||
<p className='mb-2 text-[10px] font-medium text-white/80'>
|
||||
Hover Reveal
|
||||
</p>
|
||||
</div>
|
||||
<div className='col-span-2'>
|
||||
<label className='mb-1 flex items-center gap-2 text-[10px] text-white/60'>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={values.hoverReveal === 'true'}
|
||||
onChange={(e) =>
|
||||
onChange('hoverReveal', e.target.checked ? 'true' : '')
|
||||
}
|
||||
className='rounded border-gray-300'
|
||||
/>
|
||||
Enable Hover Reveal
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label className='mb-1 block text-[10px] text-white/60'>
|
||||
Initial Opacity
|
||||
</label>
|
||||
<input
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
value={values.hoverRevealInitialOpacity || ''}
|
||||
onChange={(e) =>
|
||||
onChange('hoverRevealInitialOpacity', e.target.value)
|
||||
}
|
||||
placeholder='0'
|
||||
disabled={values.hoverReveal !== 'true'}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className='mb-1 block text-[10px] text-white/60'>
|
||||
Target Opacity
|
||||
</label>
|
||||
<input
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
value={values.hoverRevealTargetOpacity || ''}
|
||||
onChange={(e) =>
|
||||
onChange('hoverRevealTargetOpacity', e.target.value)
|
||||
}
|
||||
placeholder='1'
|
||||
disabled={values.hoverReveal !== 'true'}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className='mb-1 block text-[10px] text-white/60'>
|
||||
Duration (sec)
|
||||
</label>
|
||||
<input
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
value={values.hoverRevealDuration || ''}
|
||||
onChange={(e) => onChange('hoverRevealDuration', e.target.value)}
|
||||
placeholder='0.3'
|
||||
disabled={values.hoverReveal !== 'true'}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className='mb-1 block text-[10px] text-white/60'>
|
||||
Delay (sec)
|
||||
</label>
|
||||
<input
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
value={values.hoverRevealDelay || ''}
|
||||
onChange={(e) => onChange('hoverRevealDelay', e.target.value)}
|
||||
placeholder='0'
|
||||
disabled={values.hoverReveal !== 'true'}
|
||||
/>
|
||||
</div>
|
||||
<div className='col-span-2'>
|
||||
<label className='mb-1 flex items-center gap-2 text-[10px] text-white/60'>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={values.hoverRevealPersist === 'true'}
|
||||
onChange={(e) =>
|
||||
onChange('hoverRevealPersist', e.target.checked ? 'true' : '')
|
||||
}
|
||||
className='rounded border-gray-300'
|
||||
disabled={values.hoverReveal !== 'true'}
|
||||
/>
|
||||
Stay visible after hover (persist)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -53,15 +53,6 @@ export interface EffectsSettingsFormValues {
|
||||
activeScale?: string;
|
||||
activeOpacity?: string;
|
||||
activeBackgroundColor?: string;
|
||||
// Hover Reveal
|
||||
hoverReveal?: string;
|
||||
hoverRevealInitialOpacity?: string;
|
||||
hoverRevealTargetOpacity?: string;
|
||||
hoverRevealDuration?: string;
|
||||
hoverRevealDelay?: string;
|
||||
hoverRevealPersist?: string;
|
||||
// Persist hover effects after click
|
||||
hoverPersistOnClick?: string;
|
||||
// Slide transition override (Gallery/Carousel only)
|
||||
// These override page transition settings for this element's slides
|
||||
slideTransitionType?: string;
|
||||
|
||||
@ -87,16 +87,6 @@ interface FormState {
|
||||
activeOpacity: string;
|
||||
activeBackgroundColor: string;
|
||||
|
||||
// Hover Reveal settings
|
||||
hoverReveal: boolean;
|
||||
hoverRevealInitialOpacity: string;
|
||||
hoverRevealTargetOpacity: string;
|
||||
hoverRevealDuration: string;
|
||||
hoverRevealDelay: string;
|
||||
hoverRevealPersist: boolean;
|
||||
// Persist hover effects after click
|
||||
hoverPersistOnClick: boolean;
|
||||
|
||||
// Navigation settings
|
||||
iconUrl: string;
|
||||
navLabel: string;
|
||||
@ -192,14 +182,6 @@ const initialState: FormState = {
|
||||
activeScale: '',
|
||||
activeOpacity: '',
|
||||
activeBackgroundColor: '',
|
||||
// Hover Reveal settings
|
||||
hoverReveal: false,
|
||||
hoverRevealInitialOpacity: '',
|
||||
hoverRevealTargetOpacity: '',
|
||||
hoverRevealDuration: '',
|
||||
hoverRevealDelay: '',
|
||||
hoverRevealPersist: false,
|
||||
hoverPersistOnClick: false,
|
||||
iconUrl: '',
|
||||
navLabel: '',
|
||||
navLabelFontFamily: '',
|
||||
@ -302,18 +284,6 @@ export function useElementSettingsForm(options: UseElementSettingsFormOptions) {
|
||||
activeScale: String(settings.activeScale || ''),
|
||||
activeOpacity: String(settings.activeOpacity || ''),
|
||||
activeBackgroundColor: String(settings.activeBackgroundColor || ''),
|
||||
// Hover Reveal settings
|
||||
hoverReveal: Boolean(settings.hoverReveal),
|
||||
hoverRevealInitialOpacity: String(
|
||||
settings.hoverRevealInitialOpacity || '',
|
||||
),
|
||||
hoverRevealTargetOpacity: String(
|
||||
settings.hoverRevealTargetOpacity || '',
|
||||
),
|
||||
hoverRevealDuration: String(settings.hoverRevealDuration || ''),
|
||||
hoverRevealDelay: String(settings.hoverRevealDelay || ''),
|
||||
hoverRevealPersist: Boolean(settings.hoverRevealPersist),
|
||||
hoverPersistOnClick: Boolean(settings.hoverPersistOnClick),
|
||||
appearDelaySec: String(settings.appearDelaySec ?? 0),
|
||||
appearDurationSec:
|
||||
settings.appearDurationSec === null ||
|
||||
@ -452,14 +422,6 @@ export function useElementSettingsForm(options: UseElementSettingsFormOptions) {
|
||||
activeScale: state.activeScale,
|
||||
activeOpacity: state.activeOpacity,
|
||||
activeBackgroundColor: state.activeBackgroundColor,
|
||||
// Convert booleans to strings for form compatibility
|
||||
hoverReveal: state.hoverReveal ? 'true' : '',
|
||||
hoverRevealInitialOpacity: state.hoverRevealInitialOpacity,
|
||||
hoverRevealTargetOpacity: state.hoverRevealTargetOpacity,
|
||||
hoverRevealDuration: state.hoverRevealDuration,
|
||||
hoverRevealDelay: state.hoverRevealDelay,
|
||||
hoverRevealPersist: state.hoverRevealPersist ? 'true' : '',
|
||||
hoverPersistOnClick: state.hoverPersistOnClick ? 'true' : '',
|
||||
};
|
||||
}, [state]);
|
||||
|
||||
@ -662,30 +624,6 @@ export function useElementSettingsForm(options: UseElementSettingsFormOptions) {
|
||||
if (activeBackgroundColorValue)
|
||||
settings.activeBackgroundColor = activeBackgroundColorValue;
|
||||
|
||||
// Hover Reveal properties (booleans saved directly, strings trimmed)
|
||||
if (state.hoverReveal) settings.hoverReveal = true;
|
||||
const hoverRevealInitialOpacityValue = toOptionalTrimmed(
|
||||
state.hoverRevealInitialOpacity,
|
||||
);
|
||||
const hoverRevealTargetOpacityValue = toOptionalTrimmed(
|
||||
state.hoverRevealTargetOpacity,
|
||||
);
|
||||
const hoverRevealDurationValue = toOptionalTrimmed(
|
||||
state.hoverRevealDuration,
|
||||
);
|
||||
const hoverRevealDelayValue = toOptionalTrimmed(state.hoverRevealDelay);
|
||||
|
||||
if (hoverRevealInitialOpacityValue)
|
||||
settings.hoverRevealInitialOpacity = hoverRevealInitialOpacityValue;
|
||||
if (hoverRevealTargetOpacityValue)
|
||||
settings.hoverRevealTargetOpacity = hoverRevealTargetOpacityValue;
|
||||
if (hoverRevealDurationValue)
|
||||
settings.hoverRevealDuration = hoverRevealDurationValue;
|
||||
if (hoverRevealDelayValue)
|
||||
settings.hoverRevealDelay = hoverRevealDelayValue;
|
||||
if (state.hoverRevealPersist) settings.hoverRevealPersist = true;
|
||||
if (state.hoverPersistOnClick) settings.hoverPersistOnClick = true;
|
||||
|
||||
// Navigation type settings
|
||||
if (isNavigationType) {
|
||||
settings.iconUrl = state.iconUrl.trim();
|
||||
|
||||
@ -9,11 +9,11 @@
|
||||
import React from 'react';
|
||||
import UiElementRenderer from './UiElements/UiElementRenderer';
|
||||
import { useElementEffects } from '../hooks/useElementEffects';
|
||||
import { useAppearAnimation } from '../hooks/useAppearAnimation';
|
||||
import {
|
||||
buildTransitionStyle,
|
||||
buildAppearAnimationStyle,
|
||||
hasAnyEffects,
|
||||
extractEffectProperties,
|
||||
type ElementEffectProperties,
|
||||
} from '../lib/elementEffects';
|
||||
import type { CanvasElement } from '../types/constructor';
|
||||
import type { ResolvedTransitionSettings } from '../types/transition';
|
||||
@ -52,34 +52,29 @@ const RuntimeElement: React.FC<RuntimeElementProps> = ({
|
||||
const yPercent = clamp(element.yPercent ?? 50, 0, 100);
|
||||
const rotation = element.rotation ?? 0;
|
||||
|
||||
// Extract effect properties from element using helper
|
||||
const effectProperties = extractEffectProperties(
|
||||
element as unknown as Record<string, unknown>,
|
||||
);
|
||||
|
||||
// Use appear animation hook (removes animation after completion to unlock properties)
|
||||
const { animationStyle, onAnimationEnd, hasAnimationEnded } =
|
||||
useAppearAnimation(
|
||||
effectProperties,
|
||||
element.id, // Reset animation when element changes
|
||||
);
|
||||
|
||||
// Use effects hook for interactive states with animation coordination
|
||||
const { effectStyle, eventHandlers, onPersistClick } = useElementEffects(
|
||||
effectProperties,
|
||||
{
|
||||
resetKey: element.id, // Reset reveal on element change
|
||||
appearAnimationCompleted:
|
||||
hasAnimationEnded || !effectProperties.appearAnimation,
|
||||
},
|
||||
);
|
||||
|
||||
// Combined click handler
|
||||
const handleClick = () => {
|
||||
onPersistClick(); // Toggle persistence state
|
||||
onClick(); // Original navigation action
|
||||
// Extract effect properties from element
|
||||
const effectProperties: Partial<ElementEffectProperties> = {
|
||||
appearAnimation: element.appearAnimation,
|
||||
appearAnimationDuration: element.appearAnimationDuration,
|
||||
appearAnimationEasing: element.appearAnimationEasing,
|
||||
hoverScale: element.hoverScale,
|
||||
hoverOpacity: element.hoverOpacity,
|
||||
hoverBackgroundColor: element.hoverBackgroundColor,
|
||||
hoverColor: element.hoverColor,
|
||||
hoverBoxShadow: element.hoverBoxShadow,
|
||||
hoverTransitionDuration: element.hoverTransitionDuration,
|
||||
focusScale: element.focusScale,
|
||||
focusOpacity: element.focusOpacity,
|
||||
focusOutline: element.focusOutline,
|
||||
focusBoxShadow: element.focusBoxShadow,
|
||||
activeScale: element.activeScale,
|
||||
activeOpacity: element.activeOpacity,
|
||||
activeBackgroundColor: element.activeBackgroundColor,
|
||||
};
|
||||
|
||||
// Use effects hook for interactive states
|
||||
const { effectStyle, eventHandlers } = useElementEffects(effectProperties);
|
||||
|
||||
// Build base position style
|
||||
let positionStyle: React.CSSProperties = {
|
||||
left: `${xPercent}%`,
|
||||
@ -104,58 +99,28 @@ const RuntimeElement: React.FC<RuntimeElementProps> = ({
|
||||
positionStyle = { ...positionStyle, ...transitionStyle };
|
||||
}
|
||||
|
||||
// Check if appear animation uses transform (slide/scale need separate wrapper)
|
||||
const needsAnimationWrapper =
|
||||
effectProperties.appearAnimation &&
|
||||
effectProperties.appearAnimation !== 'fade';
|
||||
|
||||
// Render content (with or without animation wrapper)
|
||||
const content = (
|
||||
<UiElementRenderer
|
||||
element={element}
|
||||
resolveUrl={resolveUrl}
|
||||
onGalleryCardClick={onGalleryCardClick}
|
||||
letterboxStyles={letterboxStyles}
|
||||
pageTransitionSettings={pageTransitionSettings}
|
||||
preloadCache={preloadCache}
|
||||
/>
|
||||
);
|
||||
|
||||
// For fade animation (opacity only), apply to positioning div
|
||||
// For slide/scale (uses transform), use separate wrapper to avoid conflict
|
||||
if (needsAnimationWrapper) {
|
||||
return (
|
||||
<div
|
||||
className='absolute cursor-pointer'
|
||||
style={positionStyle}
|
||||
onClick={handleClick}
|
||||
tabIndex={0}
|
||||
{...eventHandlers}
|
||||
>
|
||||
<div style={animationStyle} onAnimationEnd={onAnimationEnd}>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
// Add appear animation if configured
|
||||
if (effectProperties.appearAnimation) {
|
||||
const animationStyle = buildAppearAnimationStyle(effectProperties);
|
||||
positionStyle = { ...positionStyle, ...animationStyle };
|
||||
}
|
||||
|
||||
// Fade or no animation: apply animation to positioning div
|
||||
const combinedStyle = effectProperties.appearAnimation
|
||||
? { ...positionStyle, ...animationStyle }
|
||||
: positionStyle;
|
||||
|
||||
return (
|
||||
<div
|
||||
className='absolute cursor-pointer'
|
||||
style={combinedStyle}
|
||||
onClick={handleClick}
|
||||
style={positionStyle}
|
||||
onClick={onClick}
|
||||
tabIndex={0}
|
||||
onAnimationEnd={
|
||||
effectProperties.appearAnimation ? onAnimationEnd : undefined
|
||||
}
|
||||
{...eventHandlers}
|
||||
>
|
||||
{content}
|
||||
<UiElementRenderer
|
||||
element={element}
|
||||
resolveUrl={resolveUrl}
|
||||
onGalleryCardClick={onGalleryCardClick}
|
||||
letterboxStyles={letterboxStyles}
|
||||
pageTransitionSettings={pageTransitionSettings}
|
||||
preloadCache={preloadCache}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -286,11 +286,7 @@ export default function RuntimePresentation({
|
||||
} = navState;
|
||||
|
||||
// Integrate useTransitionPlayback hook for smooth transitions (matches Constructor pattern)
|
||||
const {
|
||||
isBuffering,
|
||||
isVideoReady,
|
||||
phase: transitionPhase,
|
||||
} = useTransitionPlayback({
|
||||
const { isBuffering, isVideoReady, phase: transitionPhase } = useTransitionPlayback({
|
||||
videoRef: transitionVideoRef,
|
||||
transition: transitionPreview
|
||||
? {
|
||||
@ -553,6 +549,7 @@ export default function RuntimePresentation({
|
||||
// navShowElements: true when phase is 'idle' or 'fading_in'
|
||||
const areTransitionsReady = preloadOrchestrator?.areTransitionsReady ?? true;
|
||||
|
||||
|
||||
const handleElementClick = useCallback(
|
||||
(element: CanvasElement) => {
|
||||
// Block navigation while transition is actively playing or buffering
|
||||
@ -852,8 +849,7 @@ export default function RuntimePresentation({
|
||||
preloadOrchestrator
|
||||
? {
|
||||
getReadyBlob: preloadOrchestrator.getReadyBlob,
|
||||
getCachedBlobUrl:
|
||||
preloadOrchestrator.getCachedBlobUrl,
|
||||
getCachedBlobUrl: preloadOrchestrator.getCachedBlobUrl,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
|
||||
@ -901,7 +901,9 @@ const TourFlowManager = () => {
|
||||
maxLength={255}
|
||||
/>
|
||||
{nameValidationError && (
|
||||
<p className='text-xs text-red-600 mt-1'>{nameValidationError}</p>
|
||||
<p className='text-xs text-red-600 mt-1'>
|
||||
{nameValidationError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</CardBoxModal>
|
||||
@ -911,9 +913,7 @@ const TourFlowManager = () => {
|
||||
title='Edit page name'
|
||||
buttonColor='info'
|
||||
buttonLabel={isSavingPageName ? 'Saving...' : 'Save'}
|
||||
isConfirmDisabled={
|
||||
Boolean(editNameValidationError) || isSavingPageName
|
||||
}
|
||||
isConfirmDisabled={Boolean(editNameValidationError) || isSavingPageName}
|
||||
isActive={isEditPageModalActive}
|
||||
onConfirm={handleSavePageName}
|
||||
onCancel={isSavingPageName ? undefined : closeEditPageModal}
|
||||
@ -964,10 +964,9 @@ const TourFlowManager = () => {
|
||||
const canDelete =
|
||||
entry.type === 'page' ? canDeletePage : canDeleteTransition;
|
||||
const isDeleting = deletingId === entry.id;
|
||||
const pageData =
|
||||
entry.type === 'page'
|
||||
? pages.find((p) => p.id === entry.id)
|
||||
: null;
|
||||
const pageData = entry.type === 'page'
|
||||
? pages.find((p) => p.id === entry.id)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<li key={`${entry.type}-${entry.id}`}>
|
||||
|
||||
@ -27,7 +27,11 @@ const VideoPlayerElement: React.FC<VideoPlayerElementProps> = ({
|
||||
className,
|
||||
style,
|
||||
}) => {
|
||||
const { videoRef, resolvedUrl, isBuffering } = useVideoPlayer({
|
||||
const {
|
||||
videoRef,
|
||||
resolvedUrl,
|
||||
isBuffering,
|
||||
} = useVideoPlayer({
|
||||
sourceUrl: element.mediaUrl,
|
||||
preloadCache,
|
||||
autoplay: Boolean(element.mediaAutoplay),
|
||||
|
||||
@ -290,9 +290,11 @@
|
||||
@keyframes element-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,108 +0,0 @@
|
||||
/**
|
||||
* useAppearAnimation Hook
|
||||
*
|
||||
* Manages appear animation lifecycle to prevent conflicts with interactive effects.
|
||||
*
|
||||
* Problem: CSS animations with `animation-fill-mode: forwards` lock animated
|
||||
* properties (opacity, transform), preventing CSS transitions from working
|
||||
* on hover/focus/active states.
|
||||
*
|
||||
* Solution: Track animation completion via `animationend` event and remove
|
||||
* the animation style after it completes, unlocking properties for transitions.
|
||||
*
|
||||
* Cross-browser: Uses standard `animationend` event supported by all modern
|
||||
* browsers (Chrome, Firefox, Safari, Edge, iOS Safari, Android Chrome).
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import type { CSSProperties, AnimationEventHandler } from 'react';
|
||||
import {
|
||||
buildAppearAnimationStyle,
|
||||
type ElementEffectProperties,
|
||||
} from '../lib/elementEffects';
|
||||
|
||||
interface UseAppearAnimationResult {
|
||||
/** Animation style to apply (empty after animation completes) */
|
||||
animationStyle: CSSProperties;
|
||||
/** Handler to attach to element's onAnimationEnd */
|
||||
onAnimationEnd: AnimationEventHandler<HTMLElement>;
|
||||
/** Whether the appear animation has completed */
|
||||
hasAnimationEnded: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for managing appear animation with proper cleanup.
|
||||
*
|
||||
* @param effects - Element effect properties containing appear animation config
|
||||
* @param key - Optional key to reset animation (e.g., page slug for re-triggering on navigation)
|
||||
* @returns Object with animationStyle and onAnimationEnd handler
|
||||
*
|
||||
* @example
|
||||
* const { animationStyle, onAnimationEnd } = useAppearAnimation(effectProperties);
|
||||
* return (
|
||||
* <div
|
||||
* style={{ ...baseStyle, ...animationStyle }}
|
||||
* onAnimationEnd={onAnimationEnd}
|
||||
* >
|
||||
* {content}
|
||||
* </div>
|
||||
* );
|
||||
*/
|
||||
export function useAppearAnimation(
|
||||
effects: Partial<ElementEffectProperties>,
|
||||
key?: string | number,
|
||||
): UseAppearAnimationResult {
|
||||
// Track whether animation has completed
|
||||
const [hasAnimationEnded, setHasAnimationEnded] = useState(false);
|
||||
|
||||
// Track the animation name to identify our animation in the event
|
||||
const animationNameRef = useRef<string | null>(null);
|
||||
|
||||
// Reset animation state when key changes (e.g., navigating to new page)
|
||||
useEffect(() => {
|
||||
setHasAnimationEnded(false);
|
||||
}, [key]);
|
||||
|
||||
// Build animation style and capture animation name
|
||||
const animationStyle = (() => {
|
||||
// If no appear animation or animation already ended, return empty
|
||||
if (!effects.appearAnimation || hasAnimationEnded) {
|
||||
animationNameRef.current = null;
|
||||
return {};
|
||||
}
|
||||
|
||||
const style = buildAppearAnimationStyle(effects);
|
||||
|
||||
// Extract animation name from the style for event matching
|
||||
if (style.animation) {
|
||||
const match = String(style.animation).match(/^([\w-]+)/);
|
||||
animationNameRef.current = match ? match[1] : null;
|
||||
}
|
||||
|
||||
return style;
|
||||
})();
|
||||
|
||||
// Handler for animationend event
|
||||
const onAnimationEnd: AnimationEventHandler<HTMLElement> = useCallback(
|
||||
(event) => {
|
||||
// Only handle our animation (ignore child animations)
|
||||
// Check if the animation name matches our appear animation
|
||||
const targetAnimationName = animationNameRef.current;
|
||||
|
||||
if (
|
||||
targetAnimationName &&
|
||||
event.animationName === targetAnimationName &&
|
||||
event.target === event.currentTarget
|
||||
) {
|
||||
setHasAnimationEnded(true);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
animationStyle,
|
||||
onAnimationEnd,
|
||||
hasAnimationEnded,
|
||||
};
|
||||
}
|
||||
@ -76,8 +76,7 @@ export function useCSVHandling(
|
||||
responseType: 'blob',
|
||||
});
|
||||
|
||||
const contentType =
|
||||
(response.headers['content-type'] as string) || 'text/csv';
|
||||
const contentType = (response.headers['content-type'] as string) || 'text/csv';
|
||||
const blob = new Blob([response.data], { type: contentType });
|
||||
const link = document.createElement('a');
|
||||
link.href = window.URL.createObjectURL(blob);
|
||||
|
||||
@ -1,23 +1,21 @@
|
||||
/**
|
||||
* useElementEffects Hook
|
||||
*
|
||||
* Manages element interactive effects (hover, focus, active, reveal) at runtime.
|
||||
* Manages element interactive effects (hover, focus, active) at runtime.
|
||||
* Since CSS pseudo-classes don't work with inline styles, this hook
|
||||
* handles state-based style application via JavaScript events.
|
||||
*/
|
||||
|
||||
import { useCallback, useState, useEffect } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import type { CSSProperties } from 'react';
|
||||
import {
|
||||
buildHoverStyle,
|
||||
buildFocusStyle,
|
||||
buildActiveStyle,
|
||||
buildTransitionStyle,
|
||||
buildHoverRevealStyle,
|
||||
hasHoverEffects,
|
||||
hasFocusEffects,
|
||||
hasActiveEffects,
|
||||
hasHoverReveal,
|
||||
type ElementEffectProperties,
|
||||
} from '../lib/elementEffects';
|
||||
|
||||
@ -25,15 +23,6 @@ interface ElementEffectState {
|
||||
isHovered: boolean;
|
||||
isFocused: boolean;
|
||||
isActive: boolean;
|
||||
isRevealed: boolean;
|
||||
isClickPersisted: boolean;
|
||||
}
|
||||
|
||||
interface UseElementEffectsOptions {
|
||||
/** Key to reset reveal state (e.g., element.id or page slug) */
|
||||
resetKey?: string | number;
|
||||
/** Whether appear animation has completed (to coordinate with reveal) */
|
||||
appearAnimationCompleted?: boolean;
|
||||
}
|
||||
|
||||
interface UseElementEffectsResult {
|
||||
@ -47,22 +36,17 @@ interface UseElementEffectsResult {
|
||||
onBlur: () => void;
|
||||
onMouseDown: () => void;
|
||||
onMouseUp: () => void;
|
||||
onTouchStart: () => void;
|
||||
onTouchEnd: () => void;
|
||||
};
|
||||
/** Call this in onClick to toggle hover persistence (if enabled) */
|
||||
onPersistClick: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for managing element interactive effects.
|
||||
*
|
||||
* @param effects - Element effect properties
|
||||
* @param options - Optional configuration (resetKey, appearAnimationCompleted)
|
||||
* @returns Object with effectStyle and eventHandlers
|
||||
*
|
||||
* @example
|
||||
* const { effectStyle, eventHandlers } = useElementEffects(element, { resetKey: element.id });
|
||||
* const { effectStyle, eventHandlers } = useElementEffects(element);
|
||||
* return (
|
||||
* <div style={{ ...baseStyle, ...effectStyle }} {...eventHandlers}>
|
||||
* {content}
|
||||
@ -71,34 +55,16 @@ interface UseElementEffectsResult {
|
||||
*/
|
||||
export function useElementEffects(
|
||||
effects: Partial<ElementEffectProperties>,
|
||||
options?: UseElementEffectsOptions,
|
||||
): UseElementEffectsResult {
|
||||
const { resetKey, appearAnimationCompleted = true } = options ?? {};
|
||||
|
||||
const [state, setState] = useState<ElementEffectState>({
|
||||
isHovered: false,
|
||||
isFocused: false,
|
||||
isActive: false,
|
||||
isRevealed: false,
|
||||
isClickPersisted: false,
|
||||
});
|
||||
|
||||
// Reset reveal state and click-persisted state when resetKey changes (e.g., page navigation)
|
||||
useEffect(() => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
isRevealed: false,
|
||||
isClickPersisted: false,
|
||||
}));
|
||||
}, [resetKey]);
|
||||
|
||||
const onMouseEnter = useCallback(() => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
isHovered: true,
|
||||
isRevealed: effects.hoverReveal ? true : prev.isRevealed,
|
||||
}));
|
||||
}, [effects.hoverReveal]);
|
||||
setState((prev) => ({ ...prev, isHovered: true }));
|
||||
}, []);
|
||||
|
||||
const onMouseLeave = useCallback(() => {
|
||||
setState((prev) => ({ ...prev, isHovered: false, isActive: false }));
|
||||
@ -120,74 +86,22 @@ export function useElementEffects(
|
||||
setState((prev) => ({ ...prev, isActive: false }));
|
||||
}, []);
|
||||
|
||||
// Touch handlers for mobile devices
|
||||
const onTouchStart = useCallback(() => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
isHovered: true,
|
||||
isRevealed: effects.hoverReveal ? true : prev.isRevealed,
|
||||
}));
|
||||
}, [effects.hoverReveal]);
|
||||
|
||||
const onTouchEnd = useCallback(() => {
|
||||
// For hover reveal, keep visibility on touch (like persist mode)
|
||||
if (!effects.hoverReveal) {
|
||||
setState((prev) => ({ ...prev, isHovered: false, isActive: false }));
|
||||
}
|
||||
}, [effects.hoverReveal]);
|
||||
|
||||
// Click handler for toggling hover persistence
|
||||
const onClick = useCallback(() => {
|
||||
if (effects.hoverPersistOnClick) {
|
||||
// Toggle: first click persists, second click removes, third persists, etc.
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
isClickPersisted: !prev.isClickPersisted,
|
||||
}));
|
||||
}
|
||||
}, [effects.hoverPersistOnClick]);
|
||||
|
||||
// Build effective style based on current state
|
||||
// Priority: active > focus > hover reveal > hover > base
|
||||
// Priority: active > focus > hover > base
|
||||
let effectStyle: CSSProperties = {};
|
||||
|
||||
// Add transition for smooth effect changes
|
||||
if (
|
||||
hasHoverEffects(effects) ||
|
||||
hasFocusEffects(effects) ||
|
||||
hasActiveEffects(effects) ||
|
||||
hasHoverReveal(effects)
|
||||
hasActiveEffects(effects)
|
||||
) {
|
||||
effectStyle = { ...effectStyle, ...buildTransitionStyle(effects) };
|
||||
}
|
||||
|
||||
// Apply hover reveal style (controls opacity for reveal elements)
|
||||
// Only apply initial opacity AFTER appear animation completes to avoid conflict
|
||||
if (hasHoverReveal(effects) && appearAnimationCompleted) {
|
||||
// With persist OR click-persisted: stays visible
|
||||
// Without: only visible while hovering
|
||||
const shouldShow =
|
||||
effects.hoverRevealPersist || state.isClickPersisted
|
||||
? state.isRevealed || state.isClickPersisted
|
||||
: state.isHovered;
|
||||
effectStyle = {
|
||||
...effectStyle,
|
||||
...buildHoverRevealStyle(effects, shouldShow),
|
||||
};
|
||||
}
|
||||
|
||||
// Apply hover effects when hovered OR click-persisted (but not active)
|
||||
// Skip opacity when hoverReveal is active (reveal controls it)
|
||||
const shouldApplyHover = state.isHovered || state.isClickPersisted;
|
||||
if (shouldApplyHover && !state.isActive && hasHoverEffects(effects)) {
|
||||
const hoverStyle = buildHoverStyle(effects);
|
||||
if (hasHoverReveal(effects)) {
|
||||
// Exclude opacity from hover style when reveal is active
|
||||
const { opacity, ...restHoverStyle } = hoverStyle;
|
||||
effectStyle = { ...effectStyle, ...restHoverStyle };
|
||||
} else {
|
||||
effectStyle = { ...effectStyle, ...hoverStyle };
|
||||
}
|
||||
// Apply hover effects when hovered (but not active)
|
||||
if (state.isHovered && !state.isActive && hasHoverEffects(effects)) {
|
||||
effectStyle = { ...effectStyle, ...buildHoverStyle(effects) };
|
||||
}
|
||||
|
||||
// Apply focus effects when focused
|
||||
@ -209,9 +123,6 @@ export function useElementEffects(
|
||||
onBlur,
|
||||
onMouseDown,
|
||||
onMouseUp,
|
||||
onTouchStart,
|
||||
onTouchEnd,
|
||||
},
|
||||
onPersistClick: onClick,
|
||||
};
|
||||
}
|
||||
|
||||
@ -131,8 +131,7 @@ export function useNetworkAware(): UseNetworkAwareResult {
|
||||
if (networkInfo.effectiveType === 'slow-2g') return false;
|
||||
if (networkInfo.effectiveType === '2g') return false;
|
||||
if (networkInfo.effectiveType === '3g') return false;
|
||||
if (networkInfo.downlink !== undefined && networkInfo.downlink < 2)
|
||||
return false;
|
||||
if (networkInfo.downlink !== undefined && networkInfo.downlink < 2) return false;
|
||||
if (networkInfo.rtt !== undefined && networkInfo.rtt > 500) return false;
|
||||
return true;
|
||||
}, [networkInfo]);
|
||||
|
||||
@ -348,10 +348,7 @@ export function usePreloadOrchestrator(
|
||||
const currentPageElements = elements.filter(
|
||||
(el) => el.pageId === currentPageId,
|
||||
);
|
||||
const elementAssets = extractElementAssets(
|
||||
currentPageElements,
|
||||
currentPageId,
|
||||
);
|
||||
const elementAssets = extractElementAssets(currentPageElements, currentPageId);
|
||||
|
||||
// Collect storage paths for presigned URL batch request
|
||||
const storagePaths: string[] = [];
|
||||
@ -516,8 +513,7 @@ export function usePreloadOrchestrator(
|
||||
const job = createDownloadJob(
|
||||
`elem-img-${asset.storageKey}`,
|
||||
asset.storageKey,
|
||||
PRELOAD_CONFIG.priority.currentPage +
|
||||
PRELOAD_CONFIG.priority.assetType.image,
|
||||
PRELOAD_CONFIG.priority.currentPage + PRELOAD_CONFIG.priority.assetType.image,
|
||||
'image',
|
||||
);
|
||||
if (job) {
|
||||
@ -597,8 +593,7 @@ export function usePreloadOrchestrator(
|
||||
`trans-rev-${link.from_pageId}-${link.to_pageId}`,
|
||||
reverseVideoUrl,
|
||||
PRELOAD_CONFIG.priority.currentPage +
|
||||
PRELOAD_CONFIG.priority.assetType.transition -
|
||||
10,
|
||||
PRELOAD_CONFIG.priority.assetType.transition - 10,
|
||||
'transition',
|
||||
);
|
||||
if (job) {
|
||||
@ -633,14 +628,7 @@ export function usePreloadOrchestrator(
|
||||
} else {
|
||||
addAssetsToQueue();
|
||||
}
|
||||
}, [
|
||||
enabled,
|
||||
currentPageId,
|
||||
networkInfo.isOnline,
|
||||
elements,
|
||||
pages,
|
||||
pageLinks,
|
||||
]);
|
||||
}, [enabled, currentPageId, networkInfo.isOnline, elements, pages, pageLinks]);
|
||||
|
||||
const isCurrentPageReady =
|
||||
currentPhase === 'phase2_transitions' || currentPhase === 'complete';
|
||||
|
||||
@ -368,11 +368,7 @@ export function useTransitionPlayback(
|
||||
if (!Number.isFinite(durationSec) || durationSec <= 0) return;
|
||||
const finishBeforeEndMs = getFinishBeforeEndMs();
|
||||
const finishMs = Math.max(100, durationSec * 1000 - finishBeforeEndMs);
|
||||
setTimer(
|
||||
'finishByDuration',
|
||||
() => finishPlayback('duration-timer'),
|
||||
finishMs,
|
||||
);
|
||||
setTimer('finishByDuration', () => finishPlayback('duration-timer'), finishMs);
|
||||
};
|
||||
|
||||
const attemptPlay = () => {
|
||||
@ -529,20 +525,12 @@ export function useTransitionPlayback(
|
||||
const timeSinceProgress = Date.now() - lastProgressTimeRef.current;
|
||||
|
||||
if (!video.paused && !isWaitingForDataRef.current) {
|
||||
setTimer(
|
||||
'progressCheck',
|
||||
checkProgress,
|
||||
progressTimeout.checkIntervalMs,
|
||||
);
|
||||
setTimer('progressCheck', checkProgress, progressTimeout.checkIntervalMs);
|
||||
return;
|
||||
}
|
||||
|
||||
if (timeSinceProgress < actualNoProgressMs) {
|
||||
setTimer(
|
||||
'progressCheck',
|
||||
checkProgress,
|
||||
progressTimeout.checkIntervalMs,
|
||||
);
|
||||
setTimer('progressCheck', checkProgress, progressTimeout.checkIntervalMs);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -155,8 +155,7 @@ export function useVideoBlobUrl({
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!cancelled) {
|
||||
const resolveError =
|
||||
err instanceof Error ? err : new Error(String(err));
|
||||
const resolveError = err instanceof Error ? err : new Error(String(err));
|
||||
setError(resolveError);
|
||||
setIsResolving(false);
|
||||
onErrorRef.current?.(resolveError);
|
||||
|
||||
@ -7,13 +7,7 @@
|
||||
* - Progress-based timeout detection
|
||||
*/
|
||||
|
||||
import {
|
||||
useState,
|
||||
useRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
type RefObject,
|
||||
} from 'react';
|
||||
import { useState, useRef, useCallback, useEffect, type RefObject } from 'react';
|
||||
import { logger } from '../../lib/logger';
|
||||
import { TRANSITION_CONFIG } from '../../config/transition.config';
|
||||
|
||||
@ -94,8 +88,7 @@ export function useVideoBufferingState({
|
||||
(isMobile
|
||||
? progressTimeout.noProgressMs * progressTimeout.mobileMultiplier
|
||||
: progressTimeout.noProgressMs);
|
||||
const actualCheckIntervalMs =
|
||||
checkIntervalMs ?? progressTimeout.checkIntervalMs;
|
||||
const actualCheckIntervalMs = checkIntervalMs ?? progressTimeout.checkIntervalMs;
|
||||
|
||||
const stopProgressMonitor = useCallback(() => {
|
||||
if (progressTimeoutRef.current) {
|
||||
@ -124,19 +117,13 @@ export function useVideoBufferingState({
|
||||
|
||||
// If playing normally (not waiting), continue monitoring
|
||||
if (!video.paused && !isWaitingForDataRef.current) {
|
||||
progressTimeoutRef.current = setTimeout(
|
||||
checkProgress,
|
||||
actualCheckIntervalMs,
|
||||
);
|
||||
progressTimeoutRef.current = setTimeout(checkProgress, actualCheckIntervalMs);
|
||||
return;
|
||||
}
|
||||
|
||||
// If waiting but received data recently, keep waiting
|
||||
if (timeSinceProgress < actualNoProgressMs) {
|
||||
progressTimeoutRef.current = setTimeout(
|
||||
checkProgress,
|
||||
actualCheckIntervalMs,
|
||||
);
|
||||
progressTimeoutRef.current = setTimeout(checkProgress, actualCheckIntervalMs);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -150,10 +137,7 @@ export function useVideoBufferingState({
|
||||
onProgressTimeoutRef.current?.();
|
||||
};
|
||||
|
||||
progressTimeoutRef.current = setTimeout(
|
||||
checkProgress,
|
||||
actualCheckIntervalMs,
|
||||
);
|
||||
progressTimeoutRef.current = setTimeout(checkProgress, actualCheckIntervalMs);
|
||||
}, [videoRef, actualNoProgressMs, actualCheckIntervalMs]);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
|
||||
@ -95,8 +95,7 @@ export function useVideoErrorRecovery({
|
||||
if (!error) return false;
|
||||
|
||||
const errorCode = error.code;
|
||||
const errorMessage =
|
||||
(error as MediaError & { message?: string }).message || '';
|
||||
const errorMessage = (error as MediaError & { message?: string }).message || '';
|
||||
|
||||
logger.error('useVideoErrorRecovery: Video error', {
|
||||
code: errorCode,
|
||||
@ -113,13 +112,10 @@ export function useVideoErrorRecovery({
|
||||
decodeRetryCountRef.current < actualMaxDecodeRetries
|
||||
) {
|
||||
decodeRetryCountRef.current++;
|
||||
logger.info(
|
||||
'useVideoErrorRecovery: Safari decode error, attempting reload',
|
||||
{
|
||||
attempt: decodeRetryCountRef.current,
|
||||
maxAttempts: actualMaxDecodeRetries,
|
||||
},
|
||||
);
|
||||
logger.info('useVideoErrorRecovery: Safari decode error, attempting reload', {
|
||||
attempt: decodeRetryCountRef.current,
|
||||
maxAttempts: actualMaxDecodeRetries,
|
||||
});
|
||||
video.load();
|
||||
video.play().catch(() => {
|
||||
/* ignore play errors during recovery */
|
||||
@ -137,12 +133,9 @@ export function useVideoErrorRecovery({
|
||||
!didTryProxyRef.current
|
||||
) {
|
||||
didTryProxyRef.current = true;
|
||||
logger.info(
|
||||
'useVideoErrorRecovery: Presigned URL failed, retrying with proxy',
|
||||
{
|
||||
storageKey: currentStorageKey.slice(-40),
|
||||
},
|
||||
);
|
||||
logger.info('useVideoErrorRecovery: Presigned URL failed, retrying with proxy', {
|
||||
storageKey: currentStorageKey.slice(-40),
|
||||
});
|
||||
markPresignedUrlFailed(currentStorageKey);
|
||||
const proxyUrl = buildProxyUrl(currentStorageKey);
|
||||
onRetryWithSourceRef.current?.(proxyUrl);
|
||||
|
||||
@ -6,13 +6,7 @@
|
||||
* - requestAnimationFrame fallback (older browsers)
|
||||
*/
|
||||
|
||||
import {
|
||||
useState,
|
||||
useRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
type RefObject,
|
||||
} from 'react';
|
||||
import { useState, useRef, useCallback, useEffect, type RefObject } from 'react';
|
||||
import { logger } from '../../lib/logger';
|
||||
import { scheduleAfterPaint } from '../../lib/browserUtils';
|
||||
|
||||
@ -66,11 +60,8 @@ export function useVideoFirstFrame({
|
||||
if (callbackIdRef.current !== null) {
|
||||
const video = videoRef.current;
|
||||
if (video && 'cancelVideoFrameCallback' in video) {
|
||||
(
|
||||
video as HTMLVideoElement & {
|
||||
cancelVideoFrameCallback: (id: number) => void;
|
||||
}
|
||||
).cancelVideoFrameCallback(callbackIdRef.current);
|
||||
(video as HTMLVideoElement & { cancelVideoFrameCallback: (id: number) => void })
|
||||
.cancelVideoFrameCallback(callbackIdRef.current);
|
||||
}
|
||||
callbackIdRef.current = null;
|
||||
}
|
||||
@ -86,16 +77,11 @@ export function useVideoFirstFrame({
|
||||
|
||||
// Use requestVideoFrameCallback for precise frame-level timing (Safari 15.4+)
|
||||
if ('requestVideoFrameCallback' in video) {
|
||||
const rvfc = (
|
||||
video as HTMLVideoElement & {
|
||||
requestVideoFrameCallback: (
|
||||
callback: (
|
||||
now: number,
|
||||
metadata: VideoFrameCallbackMetadata,
|
||||
) => void,
|
||||
) => number;
|
||||
}
|
||||
).requestVideoFrameCallback.bind(video);
|
||||
const rvfc = (video as HTMLVideoElement & {
|
||||
requestVideoFrameCallback: (
|
||||
callback: (now: number, metadata: VideoFrameCallbackMetadata) => void
|
||||
) => number;
|
||||
}).requestVideoFrameCallback.bind(video);
|
||||
|
||||
// First callback: frame is composited, safe to show overlay
|
||||
callbackIdRef.current = rvfc((_now, _metadata) => {
|
||||
@ -113,9 +99,7 @@ export function useVideoFirstFrame({
|
||||
if (!didFireRef.current) {
|
||||
didFireRef.current = true;
|
||||
setIsFirstFramePainted(true);
|
||||
logger.info(
|
||||
'useVideoFirstFrame: First frame painted (rAF fallback)',
|
||||
);
|
||||
logger.info('useVideoFirstFrame: First frame painted (rAF fallback)');
|
||||
onFirstFrameRef.current?.();
|
||||
}
|
||||
});
|
||||
|
||||
@ -6,13 +6,7 @@
|
||||
* and background video playback.
|
||||
*/
|
||||
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
type RefObject,
|
||||
} from 'react';
|
||||
import { useCallback, useEffect, useRef, useState, type RefObject } from 'react';
|
||||
import { logger } from '../../lib/logger';
|
||||
import { useVideoBlobUrl, type PreloadCacheProvider } from './useVideoBlobUrl';
|
||||
import { useVideoBufferingState } from './useVideoBufferingState';
|
||||
@ -271,9 +265,7 @@ export function useVideoPlaybackCore({
|
||||
'playbackStart',
|
||||
() => {
|
||||
if (!didStartPlaybackRef.current) {
|
||||
logger.warn(
|
||||
'useVideoPlaybackCore: Playback start timeout, retrying',
|
||||
);
|
||||
logger.warn('useVideoPlaybackCore: Playback start timeout, retrying');
|
||||
play();
|
||||
}
|
||||
},
|
||||
|
||||
@ -11,13 +11,7 @@
|
||||
* not for transition or background videos which have their own hooks.
|
||||
*/
|
||||
|
||||
import {
|
||||
useRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
type RefObject,
|
||||
} from 'react';
|
||||
import { useRef, useCallback, useEffect, useState, type RefObject } from 'react';
|
||||
import { logger } from '../../lib/logger';
|
||||
import { useVideoBlobUrl, type PreloadCacheProvider } from './useVideoBlobUrl';
|
||||
import { useVideoErrorRecovery } from './useVideoErrorRecovery';
|
||||
|
||||
@ -39,9 +39,7 @@ export interface UseVideoTimeoutsResult {
|
||||
* clearTimer('watchdog');
|
||||
*/
|
||||
export function useVideoTimeouts(): UseVideoTimeoutsResult {
|
||||
const timersRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(
|
||||
new Map(),
|
||||
);
|
||||
const timersRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());
|
||||
|
||||
const clearTimer = useCallback((name: string) => {
|
||||
const timer = timersRef.current.get(name);
|
||||
|
||||
@ -37,13 +37,6 @@ export const ELEMENT_EFFECT_PROPS = [
|
||||
'activeScale',
|
||||
'activeOpacity',
|
||||
'activeBackgroundColor',
|
||||
'hoverReveal',
|
||||
'hoverRevealInitialOpacity',
|
||||
'hoverRevealTargetOpacity',
|
||||
'hoverRevealDuration',
|
||||
'hoverRevealDelay',
|
||||
'hoverRevealPersist',
|
||||
'hoverPersistOnClick',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
|
||||
@ -69,15 +69,6 @@ export interface ElementEffectProperties {
|
||||
activeScale?: string;
|
||||
activeOpacity?: string;
|
||||
activeBackgroundColor?: string;
|
||||
// Hover Reveal effects
|
||||
hoverReveal?: boolean;
|
||||
hoverRevealInitialOpacity?: string;
|
||||
hoverRevealTargetOpacity?: string;
|
||||
hoverRevealDuration?: string;
|
||||
hoverRevealDelay?: string;
|
||||
hoverRevealPersist?: boolean;
|
||||
// Persist hover effects after click (applies to all hover effects)
|
||||
hoverPersistOnClick?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -100,13 +91,6 @@ export const EFFECT_PROPS = [
|
||||
'activeScale',
|
||||
'activeOpacity',
|
||||
'activeBackgroundColor',
|
||||
'hoverReveal',
|
||||
'hoverRevealInitialOpacity',
|
||||
'hoverRevealTargetOpacity',
|
||||
'hoverRevealDuration',
|
||||
'hoverRevealDelay',
|
||||
'hoverRevealPersist',
|
||||
'hoverPersistOnClick',
|
||||
] as const;
|
||||
|
||||
export type EffectPropName = (typeof EFFECT_PROPS)[number];
|
||||
@ -303,54 +287,6 @@ export function hasAnyEffects(
|
||||
Boolean(effects.appearAnimation) ||
|
||||
hasHoverEffects(effects) ||
|
||||
hasFocusEffects(effects) ||
|
||||
hasActiveEffects(effects) ||
|
||||
hasHoverReveal(effects)
|
||||
hasActiveEffects(effects)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if element has hover reveal enabled.
|
||||
*/
|
||||
export function hasHoverReveal(
|
||||
effects: Partial<ElementEffectProperties>,
|
||||
): boolean {
|
||||
return Boolean(effects.hoverReveal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build hover reveal style based on current reveal state.
|
||||
*/
|
||||
export function buildHoverRevealStyle(
|
||||
effects: Partial<ElementEffectProperties>,
|
||||
isRevealed: boolean,
|
||||
): CSSProperties {
|
||||
if (!effects.hoverReveal) return {};
|
||||
|
||||
const initialOpacity = parseFloat(effects.hoverRevealInitialOpacity || '0');
|
||||
const targetOpacity = parseFloat(effects.hoverRevealTargetOpacity || '1');
|
||||
const duration = normalizeDuration(effects.hoverRevealDuration) || '0.3s';
|
||||
const delay = normalizeDuration(effects.hoverRevealDelay) || '0s';
|
||||
|
||||
return {
|
||||
opacity: isRevealed ? targetOpacity : initialOpacity,
|
||||
transition: `opacity ${duration} ease ${delay}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract effect properties from element to avoid duplication
|
||||
* in RuntimeElement and CanvasElement.
|
||||
* Uses EFFECT_PROPS array as source of truth.
|
||||
*/
|
||||
export function extractEffectProperties(
|
||||
element: Record<string, unknown>,
|
||||
): Partial<ElementEffectProperties> {
|
||||
const props: Partial<ElementEffectProperties> = {};
|
||||
for (const key of EFFECT_PROPS) {
|
||||
const value = element[key];
|
||||
if (value !== undefined) {
|
||||
(props as Record<string, unknown>)[key] = value;
|
||||
}
|
||||
}
|
||||
return props;
|
||||
}
|
||||
|
||||
@ -531,10 +531,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
[navNavigateToPage, applyPageSelection],
|
||||
);
|
||||
|
||||
const {
|
||||
isBuffering: isTransitionBuffering,
|
||||
isVideoReady: isTransitionVideoReady,
|
||||
} = useTransitionPlayback({
|
||||
const { isBuffering: isTransitionBuffering, isVideoReady: isTransitionVideoReady } = useTransitionPlayback({
|
||||
videoRef: transitionVideoRef,
|
||||
transition: transitionPreview
|
||||
? {
|
||||
@ -1301,10 +1298,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
// Note: Background ready state is reset atomically by navigateToPage/switchToPage
|
||||
|
||||
// Check if transition can be played using shared helper
|
||||
const canPlayTransition = hasPlayableTransition(
|
||||
transitionSource,
|
||||
direction,
|
||||
);
|
||||
const canPlayTransition = hasPlayableTransition(transitionSource, direction);
|
||||
|
||||
// Check if video is already cached (use video even on slow network if cached)
|
||||
const transitionUrl = transitionSource.transitionVideoUrl;
|
||||
@ -1313,8 +1307,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
|
||||
// Use video if: has playable transition AND (cached OR good network)
|
||||
const useVideoTransition =
|
||||
canPlayTransition &&
|
||||
(isTransitionCached || shouldUseVideoTransitions);
|
||||
canPlayTransition && (isTransitionCached || shouldUseVideoTransitions);
|
||||
|
||||
if (!useVideoTransition) {
|
||||
closeTransitionPreview();
|
||||
@ -1933,8 +1926,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
pageTransitionSettings={transitionSettings}
|
||||
preloadCache={{
|
||||
getReadyBlob: preloadOrchestrator.getReadyBlob,
|
||||
getCachedBlobUrl:
|
||||
preloadOrchestrator.getCachedBlobUrl,
|
||||
getCachedBlobUrl: preloadOrchestrator.getCachedBlobUrl,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -184,16 +184,7 @@ const ElementTypeDefaultDetailsPage = () => {
|
||||
// Handler for effects section changes
|
||||
const handleEffectChange = useCallback(
|
||||
(prop: string, value: string) => {
|
||||
// Convert string 'true'/'false' to boolean for boolean fields
|
||||
if (
|
||||
prop === 'hoverReveal' ||
|
||||
prop === 'hoverRevealPersist' ||
|
||||
prop === 'hoverPersistOnClick'
|
||||
) {
|
||||
setField(prop as keyof typeof form.state, (value === 'true') as never);
|
||||
} else {
|
||||
setField(prop as keyof typeof form.state, value as never);
|
||||
}
|
||||
setField(prop as keyof typeof form.state, value);
|
||||
},
|
||||
[setField],
|
||||
);
|
||||
|
||||
@ -311,16 +311,7 @@ const ProjectElementDefaultDetailsPage = () => {
|
||||
// Handler for effects section changes
|
||||
const handleEffectChange = useCallback(
|
||||
(prop: string, value: string) => {
|
||||
// Convert string 'true'/'false' to boolean for boolean fields
|
||||
if (
|
||||
prop === 'hoverReveal' ||
|
||||
prop === 'hoverRevealPersist' ||
|
||||
prop === 'hoverPersistOnClick'
|
||||
) {
|
||||
setField(prop as keyof typeof form.state, (value === 'true') as never);
|
||||
} else {
|
||||
setField(prop as keyof typeof form.state, value as never);
|
||||
}
|
||||
setField(prop as keyof typeof form.state, value);
|
||||
},
|
||||
[setField],
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user