edith003/src/common/utils/ComponentRenderer.tsx

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>
);
};