/** * LayoutEngine * Grid-based layout management system for CMS content assembly */ import React, { useState, useCallback, useMemo } from "react"; import { LayoutConfig, ContentZone, CMSComponent, RenderContext, GridConfig, ResponsiveConfig, } from "../types/components.js"; // ======================================== // Types & Interfaces // ======================================== export interface LayoutEngineProps { layout: LayoutConfig; zones: ContentZone[]; context: RenderContext; editable?: boolean; onLayoutChange?: (layout: LayoutConfig) => void; onZoneUpdate?: (zoneId: string, updates: Partial) => void; onComponentAdd?: ( zoneId: string, component: CMSComponent, index?: number ) => void; onComponentRemove?: (zoneId: string, componentId: string) => void; _onComponentMove?: ( fromZone: string, toZone: string, componentId: string, index?: number ) => void; className?: string; style?: React.CSSProperties; } export interface GridLayoutProps { config: GridConfig; zones: ContentZone[]; _context: RenderContext; editable: boolean; _onZoneUpdate: (zoneId: string, updates: Partial) => void; _onComponentAdd: ( zoneId: string, component: CMSComponent, index?: number ) => void; _onComponentRemove: (zoneId: string, componentId: string) => void; } export interface ResponsiveLayoutProps { config: ResponsiveConfig; zones: ContentZone[]; _context: RenderContext; editable: boolean; _onZoneUpdate: (zoneId: string, updates: Partial) => void; _onComponentAdd: ( zoneId: string, component: CMSComponent, index?: number ) => void; _onComponentRemove: (zoneId: string, componentId: string) => void; } export interface LayoutControlsProps { layout: LayoutConfig; onLayoutChange: (layout: LayoutConfig) => void; editable: boolean; } // ======================================== // Utility Functions // ======================================== const generateLayoutStyles = (layout: LayoutConfig): React.CSSProperties => { const styles: React.CSSProperties = { display: layout.display || "block", position: layout.position || "static", }; // Grid layout if (layout.display === "grid") { if (layout.gridTemplateColumns) { styles.gridTemplateColumns = layout.gridTemplateColumns; } if (layout.gridTemplateRows) { styles.gridTemplateRows = layout.gridTemplateRows; } if (layout.gridGap) { styles.gap = layout.gridGap; } if (layout.gridTemplate) { styles.gridTemplate = layout.gridTemplate; } } // Flexbox layout if (layout.display === "flex") { if (layout.flexDirection) { styles.flexDirection = layout.flexDirection; } if (layout.justifyContent) { styles.justifyContent = layout.justifyContent; } if (layout.alignItems) { styles.alignItems = layout.alignItems; } if (layout.flexWrap) { styles.flexWrap = layout.flexWrap; } if (layout.gap) { styles.gap = layout.gap; } } // Dimensions if (layout.width) styles.width = layout.width; if (layout.height) styles.height = layout.height; if (layout.maxWidth) styles.maxWidth = layout.maxWidth; if (layout.maxHeight) styles.maxHeight = layout.maxHeight; if (layout.minWidth) styles.minWidth = layout.minWidth; if (layout.minHeight) styles.minHeight = layout.minHeight; // Spacing if (layout.margin) { if (typeof layout.margin === "object") { if (layout.margin.all) styles.margin = layout.margin.all; if (layout.margin.x) { styles.marginLeft = layout.margin.x; styles.marginRight = layout.margin.x; } if (layout.margin.y) { styles.marginTop = layout.margin.y; styles.marginBottom = layout.margin.y; } if (layout.margin.top) styles.marginTop = layout.margin.top; if (layout.margin.right) styles.marginRight = layout.margin.right; if (layout.margin.bottom) styles.marginBottom = layout.margin.bottom; if (layout.margin.left) styles.marginLeft = layout.margin.left; } else { styles.margin = layout.margin; } } if (layout.padding) { if (typeof layout.padding === "object") { if (layout.padding.all) styles.padding = layout.padding.all; if (layout.padding.x) { styles.paddingLeft = layout.padding.x; styles.paddingRight = layout.padding.x; } if (layout.padding.y) { styles.paddingTop = layout.padding.y; styles.paddingBottom = layout.padding.y; } if (layout.padding.top) styles.paddingTop = layout.padding.top; if (layout.padding.right) styles.paddingRight = layout.padding.right; if (layout.padding.bottom) styles.paddingBottom = layout.padding.bottom; if (layout.padding.left) styles.paddingLeft = layout.padding.left; } else { styles.padding = layout.padding; } } return styles; }; const getResponsiveStyles = ( config: ResponsiveConfig, currentBreakpoint: string ): React.CSSProperties => { const breakpointConfig = config.breakpoints[currentBreakpoint]; if (!breakpointConfig) return {}; return generateLayoutStyles(breakpointConfig); }; // ======================================== // Layout Controls Component // ======================================== const LayoutControls: React.FC = ({ layout, onLayoutChange, editable, }) => { const [showControls, setShowControls] = useState(false); const handleDisplayChange = useCallback( (display: string) => { onLayoutChange({ ...layout, display: display as LayoutConfig["display"], }); }, [layout, onLayoutChange] ); const handleGridColumnsChange = useCallback( (columns: string) => { onLayoutChange({ ...layout, gridTemplateColumns: columns, }); }, [layout, onLayoutChange] ); const handleGridRowsChange = useCallback( (rows: string) => { onLayoutChange({ ...layout, gridTemplateRows: rows, }); }, [layout, onLayoutChange] ); const handleGapChange = useCallback( (gap: string) => { onLayoutChange({ ...layout, gap, gridGap: gap, }); }, [layout, onLayoutChange] ); if (!editable) return null; return (
{showControls && (

Layout Settings

{layout.display === "grid" && ( <>
handleGridColumnsChange(e.target.value)} placeholder="repeat(3, 1fr)" style={{ width: "100%", padding: "4px 8px", border: "1px solid #d1d5db", borderRadius: "4px", fontSize: "12px", }} />
handleGridRowsChange(e.target.value)} placeholder="auto" style={{ width: "100%", padding: "4px 8px", border: "1px solid #d1d5db", borderRadius: "4px", fontSize: "12px", }} />
)}
handleGapChange(e.target.value)} placeholder="1rem" style={{ width: "100%", padding: "4px 8px", border: "1px solid #d1d5db", borderRadius: "4px", fontSize: "12px", }} />
)}
); }; // ======================================== // Grid Layout Component // ======================================== const GridLayout: React.FC = ({ config, zones, _context, editable, _onZoneUpdate, _onComponentAdd, _onComponentRemove, }) => { const gridStyles = useMemo( () => ({ display: "grid", gridTemplateColumns: config.columns || "repeat(auto-fit, minmax(250px, 1fr))", gridTemplateRows: config.rows || "auto", gap: config.gap || "1rem", gridTemplateAreas: config.areas ? `"${config.areas.join('" "')}"` : undefined, }), [config] ); return (
{zones.map((zone) => (
{/* ContentZone would be rendered here */}
Zone: {zone.name}
{zone.components.length} components
))}
); }; // ======================================== // Responsive Layout Component // ======================================== const ResponsiveLayout: React.FC = ({ config, zones, _context, editable, _onZoneUpdate, _onComponentAdd, _onComponentRemove, }) => { const [currentBreakpoint, setCurrentBreakpoint] = useState("desktop"); const responsiveStyles = useMemo(() => { return getResponsiveStyles(config, currentBreakpoint); }, [config, currentBreakpoint]); const breakpointControls = useMemo(() => { if (!editable) return null; return (
{Object.keys(config.breakpoints).map((breakpoint) => ( ))}
); }, [config.breakpoints, currentBreakpoint, editable]); return (
{breakpointControls}
{zones.map((zone) => (
{/* ContentZone would be rendered here */}
Zone: {zone.name}
{zone.components.length} components
Breakpoint: {currentBreakpoint}
))}
); }; // ======================================== // Main LayoutEngine Component // ======================================== export const LayoutEngine: React.FC = ({ layout, zones, context, editable = false, onLayoutChange, onZoneUpdate, onComponentAdd, onComponentRemove, _onComponentMove, className = "", style = {}, }) => { const handleZoneUpdate = useCallback( (zoneId: string, updates: Partial) => { if (onZoneUpdate) { onZoneUpdate(zoneId, updates); } }, [onZoneUpdate] ); const handleComponentAdd = useCallback( (zoneId: string, component: CMSComponent, index?: number) => { if (onComponentAdd) { onComponentAdd(zoneId, component, index); } }, [onComponentAdd] ); const handleComponentRemove = useCallback( (zoneId: string, componentId: string) => { if (onComponentRemove) { onComponentRemove(zoneId, componentId); } }, [onComponentRemove] ); const handleLayoutChange = useCallback( (newLayout: LayoutConfig) => { if (onLayoutChange) { onLayoutChange(newLayout); } }, [onLayoutChange] ); const layoutStyles = useMemo(() => { return { ...generateLayoutStyles(layout), ...style, }; }, [layout, style]); const renderLayout = useMemo(() => { // Grid-based layout if (layout.display === "grid" && layout.gridConfig) { return ( ); } // Responsive layout if (layout.responsive) { return ( ); } // Default layout return (
{zones.map((zone) => (
{/* ContentZone would be rendered here */}
Zone: {zone.name}
{zone.components.length} components
))}
); }, [ layout, zones, context, editable, layoutStyles, handleZoneUpdate, handleComponentAdd, handleComponentRemove, ]); return (
{editable && onLayoutChange && ( )} {renderLayout}
); }; // ======================================== // Export Default // ======================================== export default LayoutEngine;