123 lines
4.2 KiB
TypeScript
123 lines
4.2 KiB
TypeScript
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<ComponentRendererProps> = ({
|
|
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<HTMLDivElement>(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 (
|
|
<div ref={domRef} style={{ display: 'contents' }}>
|
|
{renderComponent()}
|
|
</div>
|
|
);
|
|
};
|