diff --git a/frontend/src/css/main.css b/frontend/src/css/main.css index 2f6302a..829708e 100644 --- a/frontend/src/css/main.css +++ b/frontend/src/css/main.css @@ -274,16 +274,14 @@ ============================================================= */ /* Element appear animation keyframes - Safari optimized */ +/* Note: element-fade-in must NOT include transform as it conflicts with + element positioning (translate(-50%, -50%)) causing position shift */ @-webkit-keyframes element-fade-in { from { opacity: 0; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); } to { opacity: 1; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); } } diff --git a/frontend/src/hooks/useElementEffects.ts b/frontend/src/hooks/useElementEffects.ts index 7c2b0f1..a3fd828 100644 --- a/frontend/src/hooks/useElementEffects.ts +++ b/frontend/src/hooks/useElementEffects.ts @@ -6,7 +6,7 @@ * handles state-based style application via JavaScript events. */ -import { useCallback, useState, useEffect } from 'react'; +import { useCallback, useState, useEffect, useRef } from 'react'; import type { CSSProperties } from 'react'; import { buildHoverStyle, @@ -83,13 +83,20 @@ export function useElementEffects( isClickPersisted: false, }); + // Track previous resetKey to avoid running on initial mount + const prevResetKeyRef = useRef(resetKey); + // Reset reveal state and click-persisted state when resetKey changes (e.g., page navigation) + // Only run when resetKey actually CHANGES, not on initial mount (to avoid re-render during animation) useEffect(() => { - setState((prev) => ({ - ...prev, - isRevealed: false, - isClickPersisted: false, - })); + if (prevResetKeyRef.current !== resetKey) { + setState((prev) => ({ + ...prev, + isRevealed: false, + isClickPersisted: false, + })); + prevResetKeyRef.current = resetKey; + } }, [resetKey]); const onMouseEnter = useCallback(() => { diff --git a/frontend/src/lib/elementEffects.ts b/frontend/src/lib/elementEffects.ts index a22262d..ea13b77 100644 --- a/frontend/src/lib/elementEffects.ts +++ b/frontend/src/lib/elementEffects.ts @@ -113,13 +113,58 @@ export type EffectPropName = (typeof EFFECT_PROPS)[number]; /** * Build base transition style for smooth state changes. + * Uses specific properties instead of 'all' to avoid affecting positioning transforms. */ export function buildTransitionStyle( effects: Partial, ): CSSProperties { const duration = normalizeDuration(effects.hoverTransitionDuration) || '0.2s'; + + // Build list of properties to transition based on what's configured + const transitionProps: string[] = []; + + // Always include opacity if any effect might change it + if ( + effects.hoverOpacity || + effects.focusOpacity || + effects.activeOpacity || + effects.hoverReveal + ) { + transitionProps.push('opacity'); + } + + // Include transform only for scale effects (not positioning) + if (effects.hoverScale || effects.focusScale || effects.activeScale) { + transitionProps.push('transform'); + } + + // Background color + if (effects.hoverBackgroundColor || effects.activeBackgroundColor) { + transitionProps.push('background-color'); + } + + // Text color + if (effects.hoverColor) { + transitionProps.push('color'); + } + + // Box shadow + if (effects.hoverBoxShadow || effects.focusBoxShadow) { + transitionProps.push('box-shadow'); + } + + // Outline + if (effects.focusOutline) { + transitionProps.push('outline'); + } + + // If no specific properties, return empty (no transition needed) + if (transitionProps.length === 0) { + return {}; + } + return { - transition: `all ${duration} ease`, + transition: transitionProps.map((p) => `${p} ${duration} ease`).join(', '), }; }