import React, { type CSSProperties } from "react"; import type { ComponentNode, ComponentType } from '../types/component.types'; import { ComponentRegistry } from '../registry'; import type { RuntimeValues } from '../registry/types'; import { normalizeComponentStyles } from './styleNormalization'; import { useRuntimeOptional } from '../runtime'; interface ComponentRendererProps { component: ComponentNode; isSelected: string | null; lastModifiedId?: string | null; onSelect: (id: string) => void; renderChildren?: (children: ComponentNode[], parentType: ComponentType, parentStyles?: CSSProperties) => React.ReactNode; previewMode?: boolean; parentType?: ComponentType | null; parentStyles?: CSSProperties | null; } // Default no-op runtime values for when context is not available const defaultRuntime: RuntimeValues = { state: { page: {}, global: {} }, resolveText: (text: string) => text, executeHandlers: async () => { }, dispatchEvent: async () => { }, pages: [], navigateToPage: () => { }, setStateValue: () => { }, }; export const ComponentRenderer: React.FC = ({ component, isSelected, lastModifiedId, onSelect, renderChildren, previewMode = false, parentType = null, parentStyles = null, }) => { const isSelectedComponent = isSelected === component.id; const isModified = lastModifiedId === component.id; const domRef = React.useRef(null); // Get runtime context (may be null if not wrapped in provider) const runtimeContext = useRuntimeOptional(); // Scroll into view and pulse when modified and selected (e.g. undo/redo) React.useEffect(() => { if (isSelectedComponent && isModified && !previewMode) { // Use requestAnimationFrame to ensure the DOM is ready (especially for restored components) requestAnimationFrame(() => { if (domRef.current) { domRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); domRef.current.classList.remove('pulse-highlight'); // Trigger reflow to restart animation void domRef.current.offsetWidth; domRef.current.classList.add('pulse-highlight'); } }); } }, [isSelectedComponent, isModified, previewMode]); // Normalize styles to prevent flexbox stretching, passing parent styles so the // normalizer can skip injection when the parent has explicit alignment intent. const normalizedStyles = normalizeComponentStyles(component.type, component.styles, parentType, parentStyles); const normalizedComponent = { ...component, styles: normalizedStyles }; // Helper to render children with parent context. // We pass normalizedStyles so children can see this component's effective alignment. const renderChildrenWithContext = (children: ComponentNode[]) => { if (!renderChildren) return null; return renderChildren(children, component.type, normalizedStyles); }; // Build runtime values from context or defaults const runtime: RuntimeValues = runtimeContext ? { state: runtimeContext.state, resolveText: runtimeContext.resolveText, executeHandlers: runtimeContext.executeHandlers, dispatchEvent: runtimeContext.dispatchEvent, pages: runtimeContext.pages, navigateToPage: runtimeContext.navigateToPage, setStateValue: runtimeContext.setStateValue, } : defaultRuntime; const renderComponent = () => { // Get component definition from registry const definition = ComponentRegistry.get(component.type); if (!definition) { console.warn(`Unknown component type: ${component.type}`); return null; } // Prepare children for containers const childrenContent = definition.canHaveChildren && renderChildren && component.children ? renderChildrenWithContext(component.children) : undefined; // Call the render function from the definition with runtime values return definition.render({ component: normalizedComponent, isSelected: isSelectedComponent, onSelect, previewMode, children: childrenContent, runtime, }); }; if (previewMode) { return <>{renderComponent()}; } return (
{renderComponent()}
); };