39948-vm/frontend/src/components/RuntimeElement.tsx

104 lines
3.3 KiB
TypeScript

/**
* RuntimeElement Component
*
* Renders a single UI element with interactive effects at runtime.
* Handles hover, focus, and active states for element effects.
*/
import React from 'react';
import { useElementEffects } from '../hooks/useElementEffects';
import { buildElementStyle } from '../lib/elementStyles';
import {
buildTransitionStyle,
buildAppearAnimationStyle,
hasAnyEffects,
type ElementEffectProperties,
} from '../lib/elementEffects';
interface RuntimeElementProps {
element: any;
onClick: () => void;
children: React.ReactNode;
}
const RuntimeElement: React.FC<RuntimeElementProps> = ({
element,
onClick,
children,
}) => {
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 element style
const baseStyle: React.CSSProperties = {
left: `${xPercent}%`,
top: `${yPercent}%`,
transform: `translate(-50%, -50%)${rotation ? ` rotate(${rotation}deg)` : ''}`,
...buildElementStyle(element),
};
// Merge transform if effect style has transform
let mergedStyle: React.CSSProperties = { ...baseStyle };
// Handle transform merging - effect transform overrides base (except for position)
if (effectStyle.transform) {
// Preserve the translate and rotation, add effect transform
mergedStyle.transform = `translate(-50%, -50%)${rotation ? ` rotate(${rotation}deg)` : ''} ${effectStyle.transform}`;
// Remove transform from effectStyle to avoid double application
const { transform, ...restEffectStyle } = effectStyle;
mergedStyle = { ...mergedStyle, ...restEffectStyle };
} else {
mergedStyle = { ...mergedStyle, ...effectStyle };
}
// Add transition if element has any effects
if (hasAnyEffects(effectProperties)) {
const transitionStyle = buildTransitionStyle(effectProperties);
mergedStyle = { ...mergedStyle, ...transitionStyle };
}
// Add appear animation if configured
if (effectProperties.appearAnimation) {
const animationStyle = buildAppearAnimationStyle(effectProperties);
mergedStyle = { ...mergedStyle, ...animationStyle };
}
return (
<div
className='absolute cursor-pointer'
style={mergedStyle}
onClick={onClick}
tabIndex={0}
{...eventHandlers}
>
{children}
</div>
);
};
export default RuntimeElement;