109 lines
3.5 KiB
TypeScript
109 lines
3.5 KiB
TypeScript
/**
|
|
* RuntimeElement Component
|
|
*
|
|
* Renders a single UI element with interactive effects at runtime.
|
|
* Handles hover, focus, active states and positioning.
|
|
* Delegates element styling and content to UiElementRenderer.
|
|
*/
|
|
|
|
import React from 'react';
|
|
import UiElementRenderer from './UiElements/UiElementRenderer';
|
|
import { useElementEffects } from '../hooks/useElementEffects';
|
|
import {
|
|
buildTransitionStyle,
|
|
buildAppearAnimationStyle,
|
|
hasAnyEffects,
|
|
type ElementEffectProperties,
|
|
} from '../lib/elementEffects';
|
|
|
|
interface RuntimeElementProps {
|
|
element: any;
|
|
onClick: () => void;
|
|
/** Optional URL resolver for preloaded blob URLs */
|
|
resolveUrl?: (url: string | undefined) => string;
|
|
/** Gallery card click handler */
|
|
onGalleryCardClick?: (cardIndex: number) => void;
|
|
}
|
|
|
|
const RuntimeElement: React.FC<RuntimeElementProps> = ({
|
|
element,
|
|
onClick,
|
|
resolveUrl,
|
|
onGalleryCardClick,
|
|
}) => {
|
|
const xPercent = element.xPercent ?? 0;
|
|
const yPercent = element.yPercent ?? 0;
|
|
const rotation = element.rotation ?? 0;
|
|
|
|
// 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}%`,
|
|
top: `${yPercent}%`,
|
|
transform: `translate(-50%, -50%)${rotation ? ` rotate(${rotation}deg)` : ''}`,
|
|
};
|
|
|
|
// Merge transform if effect style has transform
|
|
if (effectStyle.transform) {
|
|
// Preserve the translate and rotation, add effect transform
|
|
positionStyle.transform = `translate(-50%, -50%)${rotation ? ` rotate(${rotation}deg)` : ''} ${effectStyle.transform}`;
|
|
// Remove transform from effectStyle to avoid double application
|
|
const { transform, ...restEffectStyle } = effectStyle;
|
|
positionStyle = { ...positionStyle, ...restEffectStyle };
|
|
} else {
|
|
positionStyle = { ...positionStyle, ...effectStyle };
|
|
}
|
|
|
|
// Add transition if element has any effects
|
|
if (hasAnyEffects(effectProperties)) {
|
|
const transitionStyle = buildTransitionStyle(effectProperties);
|
|
positionStyle = { ...positionStyle, ...transitionStyle };
|
|
}
|
|
|
|
// Add appear animation if configured
|
|
if (effectProperties.appearAnimation) {
|
|
const animationStyle = buildAppearAnimationStyle(effectProperties);
|
|
positionStyle = { ...positionStyle, ...animationStyle };
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className='absolute cursor-pointer'
|
|
style={positionStyle}
|
|
onClick={onClick}
|
|
tabIndex={0}
|
|
{...eventHandlers}
|
|
>
|
|
<UiElementRenderer
|
|
element={element}
|
|
resolveUrl={resolveUrl}
|
|
onGalleryCardClick={onGalleryCardClick}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default RuntimeElement;
|