39948-vm/frontend/src/components/RuntimeElement.tsx
2026-03-30 21:28:00 +04:00

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;