diff --git a/frontend/src/components/Constructor/CanvasElement.tsx b/frontend/src/components/Constructor/CanvasElement.tsx index 1b50c94..41a9ffe 100644 --- a/frontend/src/components/Constructor/CanvasElement.tsx +++ b/frontend/src/components/Constructor/CanvasElement.tsx @@ -10,9 +10,9 @@ import React from 'react'; import UiElementRenderer from '../UiElements/UiElementRenderer'; import { useElementEffects } from '../../hooks/useElementEffects'; -import { useAppearAnimation } from '../../hooks/useAppearAnimation'; import { buildTransitionStyle, + buildAppearAnimationStyle, hasAnyEffects, type ElementEffectProperties, } from '../../lib/elementEffects'; @@ -82,12 +82,6 @@ const CanvasElement: React.FC = ({ isEditMode ? {} : effectProperties, ); - // Use appear animation hook (removes animation after completion to unlock properties) - const { animationStyle, onAnimationEnd } = useAppearAnimation( - effectProperties, - element.id, // Reset animation when element changes - ); - // Clamp position to canvas bounds (0-100%) const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max); @@ -120,6 +114,14 @@ const CanvasElement: React.FC = ({ }; } + // 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 === ' ') { @@ -128,66 +130,29 @@ const CanvasElement: React.FC = ({ } }; - // Check if appear animation uses transform (slide/scale need separate wrapper) - const needsAnimationWrapper = - effectProperties.appearAnimation && - effectProperties.appearAnimation !== 'fade'; - - // Render content - const content = ( - - ); - - // For slide/scale (uses transform), use separate wrapper to avoid conflict with positioning - if (needsAnimationWrapper) { - return ( -
-
- {content} -
-
- ); - } - - // Fade or no animation: apply animation to positioning div - const combinedStyle = effectProperties.appearAnimation - ? { ...positionStyle, ...animationStyle } - : positionStyle; - return (
- {content} +
); }; diff --git a/frontend/src/components/RuntimeElement.tsx b/frontend/src/components/RuntimeElement.tsx index ededabe..06aa9ca 100644 --- a/frontend/src/components/RuntimeElement.tsx +++ b/frontend/src/components/RuntimeElement.tsx @@ -9,9 +9,9 @@ import React from 'react'; import UiElementRenderer from './UiElements/UiElementRenderer'; import { useElementEffects } from '../hooks/useElementEffects'; -import { useAppearAnimation } from '../hooks/useAppearAnimation'; import { buildTransitionStyle, + buildAppearAnimationStyle, hasAnyEffects, type ElementEffectProperties, } from '../lib/elementEffects'; @@ -75,12 +75,6 @@ const RuntimeElement: React.FC = ({ // Use effects hook for interactive states const { effectStyle, eventHandlers } = useElementEffects(effectProperties); - // Use appear animation hook (removes animation after completion to unlock properties) - const { animationStyle, onAnimationEnd } = useAppearAnimation( - effectProperties, - element.id, // Reset animation when element changes - ); - // Build base position style let positionStyle: React.CSSProperties = { left: `${xPercent}%`, @@ -105,56 +99,28 @@ const RuntimeElement: React.FC = ({ 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 = ( - - ); - - // For fade animation (opacity only), apply to positioning div - // For slide/scale (uses transform), use separate wrapper to avoid conflict - if (needsAnimationWrapper) { - return ( -
-
- {content} -
-
- ); + // 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 (
- {content} +
); }; diff --git a/frontend/src/css/main.css b/frontend/src/css/main.css index 2f6302a..0df2ecb 100644 --- a/frontend/src/css/main.css +++ b/frontend/src/css/main.css @@ -290,9 +290,11 @@ @keyframes element-fade-in { from { opacity: 0; + transform: translate3d(0, 0, 0); } to { opacity: 1; + transform: translate3d(0, 0, 0); } } diff --git a/frontend/src/hooks/useAppearAnimation.ts b/frontend/src/hooks/useAppearAnimation.ts deleted file mode 100644 index e1ad744..0000000 --- a/frontend/src/hooks/useAppearAnimation.ts +++ /dev/null @@ -1,105 +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; -} - -/** - * 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 ( - *
- * {content} - *
- * ); - */ -export function useAppearAnimation( - effects: Partial, - 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(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 = 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, - }; -}