import React, { CSSProperties } from 'react'; import { useNode, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; import { SettingsTabs } from '../../ui/SettingsTabs'; import { BorderControl } from '../../ui/BorderControl'; import { AdvancedTab } from '../../ui/AdvancedTab'; import { AnchorIdField } from '../../ui/AnchorIdField'; interface ContainerProps { style?: CSSProperties; tag?: 'div' | 'section' | 'article' | 'header' | 'footer' | 'main'; children?: React.ReactNode; cssId?: string; cssClass?: string; anchorId?: string; hideOnDesktop?: boolean; hideOnTablet?: boolean; hideOnMobile?: boolean; animation?: string; animationDelay?: string; fullWidth?: boolean; contentWidth?: 'boxed' | 'full'; } // Map textAlign to a flex alignItems value so block-level children (images, // columns, sections) align horizontally — textAlign alone only affects inline // content. Returns undefined when no alignment is set so we leave layout as // normal block flow. const flexAlignFromTextAlign = (textAlign: CSSProperties['textAlign']): CSSProperties => { if (textAlign === 'center') return { display: 'flex', flexDirection: 'column', alignItems: 'center' }; if (textAlign === 'right') return { display: 'flex', flexDirection: 'column', alignItems: 'flex-end' }; if (textAlign === 'left') return { display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }; return {}; }; export const Container: UserComponent = ({ style = {}, tag = 'div', children, fullWidth = false, contentWidth = 'full', anchorId, }) => { const { connectors: { connect, drag } } = useNode(); const needsBoxedWrapper = contentWidth === 'boxed'; const flexStyles = flexAlignFromTextAlign(style.textAlign); const outerStyle: CSSProperties = { minHeight: '40px', ...style, ...(fullWidth ? { width: '100vw', marginLeft: 'calc(-50vw + 50%)' } : {}), ...(needsBoxedWrapper ? {} : flexStyles), }; const el = React.createElement( tag, { ref: (ref: HTMLElement | null): void => { if (ref) connect(drag(ref)); }, style: outerStyle, 'data-craft-container': 'true', id: anchorId || undefined, }, needsBoxedWrapper ? React.createElement('div', { style: { maxWidth: '1200px', margin: '0 auto', ...flexStyles } }, children) : children, ); return el; }; /* ---------- Settings panel ---------- */ const cLabelStyle: React.CSSProperties = { fontSize: 11, fontWeight: 600, color: '#a1a1aa', display: 'block', marginBottom: 6, textTransform: 'uppercase', letterSpacing: '0.3px', }; const cInputStyle: React.CSSProperties = { width: '100%', padding: '5px 8px', background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, fontSize: 11, }; const cPresetBtnStyle = (active: boolean): React.CSSProperties => ({ padding: '3px 8px', fontSize: 11, borderRadius: 4, cursor: 'pointer', border: '1px solid #3f3f46', background: active ? '#3b82f6' : '#27272a', color: active ? '#fff' : '#e4e4e7', }); const cSwatchStyle = (color: string, active: boolean): React.CSSProperties => ({ width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46', backgroundColor: color, cursor: 'pointer', outline: active ? '2px solid #3b82f6' : 'none', outlineOffset: 1, }); const cToggleBtnStyle = (active: boolean): React.CSSProperties => ({ flex: 1, padding: '5px 8px', fontSize: 11, borderRadius: 4, cursor: 'pointer', border: '1px solid #3f3f46', background: active ? '#3b82f6' : '#27272a', color: active ? '#fff' : '#e4e4e7', fontWeight: active ? 600 : 400, textAlign: 'center', }); const ContainerSettings: React.FC = () => { const { actions: { setProp }, props } = useNode((node) => ({ props: node.data.props as ContainerProps, })); const bgColors = ['transparent', '#ffffff', '#f9fafb', '#f1f5f9', '#1f2937', '#111827', '#0f172a', '#3b82f6', '#10b981', '#8b5cf6', '#ec4899', '#f59e0b']; const gradients = [ { label: 'None', value: 'none' }, { label: 'Purple', value: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }, { label: 'Blue', value: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' }, { label: 'Sunset', value: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)' }, { label: 'Dark', value: 'linear-gradient(135deg, #0f172a 0%, #1e3a5f 100%)' }, { label: 'Green', value: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)' }, ]; const alignPresets = [ { label: 'Left', value: 'left', icon: 'fa-align-left' }, { label: 'Center', value: 'center', icon: 'fa-align-center' }, { label: 'Right', value: 'right', icon: 'fa-align-right' }, ]; const currentBg = props.style?.backgroundColor || ''; const currentBgImage = props.style?.backgroundImage || ''; return ( {/* Tag */}
{/* Full Width */}
Breaks out of parent constraints to fill the viewport width
{/* Content Width */}
{props.contentWidth === 'boxed' ? 'Content is centered with a max-width of 1200px' : 'Content fills the full container width'}
} style={
{/* Background Color */}
{bgColors.map((c) => (
{/* Background Gradient */}
{gradients.map((g) => (
{/* Background Image */}
{ const val = e.target.value.trim(); setProp((p: ContainerProps) => { p.style = { ...p.style, backgroundImage: val ? `url('${val}')` : 'none', backgroundSize: 'cover', backgroundPosition: 'center' }; }); }} style={cInputStyle} />
{['cover', 'contain', 'auto'].map((s) => ( ))} {['center', 'top', 'bottom'].map((pos) => ( ))}
{/* Overlay */}
setProp((p: ContainerProps) => { p.style = { ...p.style, ['--overlayColor' as keyof CSSProperties]: e.target.value }; })} style={{ width: 32, height: 24, border: 'none', background: 'none', cursor: 'pointer' }} /> Overlay (via CSS custom property)
{/* Parallax */}
{/* Text Alignment */}
{alignPresets.map((a) => ( ))}
{/* Border */} setProp((p: ContainerProps) => { p.style = { ...p.style, ...updates }; })} />
} advanced={ setProp((p: ContainerProps) => { p.style = { ...p.style, ...updates }; })} showTagSelector tag={props.tag || 'div'} onTagChange={(tag) => setProp((p: ContainerProps) => { p.tag = tag as ContainerProps['tag']; })} cssId={props.cssId || ''} onCssIdChange={(id) => setProp((p: ContainerProps) => { p.cssId = id; })} cssClass={props.cssClass || ''} onCssClassChange={(cls) => setProp((p: ContainerProps) => { p.cssClass = cls; })} hideOnDesktop={props.hideOnDesktop} onHideOnDesktopChange={(v) => setProp((p: ContainerProps) => { p.hideOnDesktop = v; })} hideOnTablet={props.hideOnTablet} onHideOnTabletChange={(v) => setProp((p: ContainerProps) => { p.hideOnTablet = v; })} hideOnMobile={props.hideOnMobile} onHideOnMobileChange={(v) => setProp((p: ContainerProps) => { p.hideOnMobile = v; })} animation={props.animation} onAnimationChange={(v) => setProp((p: ContainerProps) => { p.animation = v; })} animationDelay={props.animationDelay} onAnimationDelayChange={(v) => setProp((p: ContainerProps) => { p.animationDelay = v; })} /> } /> ); }; /* ---------- Craft config ---------- */ Container.craft = { displayName: 'Container', props: { style: { padding: '20px', minHeight: '100px' }, tag: 'div', fullWidth: false, contentWidth: 'full', anchorId: '', }, rules: { canDrag: () => true, canMoveIn: () => true, canMoveOut: () => true, }, related: { settings: ContainerSettings, }, }; /* ---------- HTML export ---------- */ (Container as any).toHtml = (props: ContainerProps, childrenHtml: string) => { const esc = (s: any) => String(s ?? "").replace(//g, '>').replace(/"/g, '"'); const tag = props.tag || 'div'; const isBoxed = props.contentWidth === 'boxed'; const flexStyles = flexAlignFromTextAlign(props.style?.textAlign); const outerCss: CSSProperties = { ...props.style, ...(isBoxed ? {} : flexStyles), }; if (props.fullWidth) { outerCss.width = '100vw'; outerCss.marginLeft = 'calc(-50vw + 50%)'; } const styleStr = cssPropsToString(outerCss); const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : ''; if (isBoxed) { const innerStyle = cssPropsToString({ maxWidth: '1200px', margin: '0 auto', ...flexStyles }); return { html: `<${tag}${idAttr}${styleStr ? ` style="${styleStr}"` : ''}>${childrenHtml}` }; } return { html: `<${tag}${idAttr}${styleStr ? ` style="${styleStr}"` : ''}>${childrenHtml}` }; };