import React, { useState, useRef, useEffect } from 'react'; import ReactDOM from 'react-dom'; import { Palette, X, RefreshCw } from 'lucide-react'; import BackgroundImageControls from './interaction/BackgroundImageControls'; import { HexInput } from './InlineLabelEditor'; const SWATCHES = [ '#ffffff', '#f8fafc', '#f1f5f9', '#e2e8f0', '#cbd5e1', '#fef2f2', '#fff7ed', '#fefce8', '#f0fdf4', '#eff6ff', '#faf5ff', '#fdf4ff', '#000000', 'transparent' ]; // Column background (the reference builder keys under columnBackgrounds.columnN, read by the storefront grid renderer): // backgroundColor, backgroundImageUrl, backgroundImageSize (cover/contain/auto or a custom "N%"), // backgroundImagePosition (9 positions), backgroundImageRepeat, backgroundOverlayColor + backgroundOverlayOpacity (0–100). export default function InlineColumnBackgroundEditor({ isOpen, onClose, columnData = {}, onUpdate, anchorRef }) { // columnData uses the the reference builder keys (columnBackgrounds.columnN); older columnSettings keys are read as a fallback. const [bgColor, setBgColor] = useState(columnData.backgroundColor || columnData.bgColor || 'transparent'); const [bgImage, setBgImage] = useState(columnData.backgroundImageUrl || columnData.bgImage || ''); const [bgSize, setBgSize] = useState(columnData.backgroundImageSize || columnData.bgSize || 'cover'); const [bgPosition, setBgPosition] = useState(columnData.backgroundImagePosition || columnData.bgPosition || 'center'); const [bgRepeat, setBgRepeat] = useState(columnData.backgroundImageRepeat || 'no-repeat'); const [overlayColor, setOverlayColor] = useState(columnData.backgroundOverlayColor || '#000000'); const [overlayOpacity, setOverlayOpacity] = useState(Number(columnData.backgroundOverlayOpacity) || 0); const panelRef = useRef(null); const [position, setPosition] = useState({ top: 0, left: 0 }); useEffect(() => { if (isOpen && anchorRef?.current) { const rect = anchorRef.current.getBoundingClientRect(); const width = 288; let left = rect.left; if (left + width > window.innerWidth - 10) left = window.innerWidth - width - 10; setPosition({ top: Math.max(10, Math.min(rect.top + 30, window.innerHeight - 420)), left: Math.max(10, left) }); } }, [isOpen, anchorRef]); useEffect(() => { if (!isOpen) return; const handler = (e) => { // The WordPress media modal lives outside the popover; picking an image there must not close it. if (e.target.closest && e.target.closest('.media-modal, .media-modal-backdrop')) return; if (panelRef.current && !panelRef.current.contains(e.target) && !anchorRef?.current?.contains(e.target)) { onClose(); } }; const onKey = (e) => { if (e.key !== 'Escape') return; // Escape in the WordPress media modal closes that modal only. const mediaOpen = Array.from(document.querySelectorAll('.media-modal')).some((el) => el.getClientRects().length > 0); if (mediaOpen) return; e.stopPropagation(); onClose(); }; document.addEventListener('mousedown', handler); document.addEventListener('keydown', onKey, true); return () => { document.removeEventListener('mousedown', handler); document.removeEventListener('keydown', onKey, true); }; }, [isOpen, onClose]); const STORED_KEYS = { bgColor: 'backgroundColor', bgImage: 'backgroundImageUrl', bgSize: 'backgroundImageSize', bgPosition: 'backgroundImagePosition' }; const applyChanges = (updates) => { const mapped = {}; Object.entries(updates).forEach(([k, v]) => { mapped[STORED_KEYS[k] || k] = v; }); onUpdate(mapped); }; const setColor = (color) => { setBgColor(color); applyChanges({ bgColor: color }); }; if (!isOpen) return null; return ReactDOM.createPortal(
e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()} className="fixed z-[9999] bg-white rounded-xl shadow-2xl border border-gray-200 p-4 w-72 max-h-[80vh] overflow-y-auto" style={{ top: `${position.top}px`, left: `${position.left}px` }} >

Column Background

{/* Background Color */}
{SWATCHES.map(color => (
setColor(e.target.value)} className="w-8 h-8 flex-shrink-0 rounded border border-gray-200 cursor-pointer" />
{/* Background Image */}
{ const out = {}; if ('url' in patch) { setBgImage(patch.url); out.bgImage = patch.url; } if ('size' in patch) { setBgSize(patch.size); out.bgSize = patch.size; // the reference builder keeps the custom width in its own key as well. if (!['cover', 'contain', 'auto'].includes(patch.size)) out.backgroundImageCustomWidth = patch.size; } if ('position' in patch) { setBgPosition(patch.position); out.bgPosition = patch.position; } if ('repeat' in patch) { setBgRepeat(patch.repeat); out.backgroundImageRepeat = patch.repeat; } applyChanges(out); }} />
{/* Overlay over the image (drawn only when there is an image, like the published form) */} {bgImage && (
{ setOverlayColor(e.target.value); applyChanges({ backgroundOverlayColor: e.target.value, backgroundOverlayOpacity: overlayOpacity }); }} className="w-8 h-8 rounded border border-gray-200 cursor-pointer flex-shrink-0" /> { const v = parseInt(e.target.value, 10) || 0; setOverlayOpacity(v); applyChanges({ backgroundOverlayColor: overlayColor, backgroundOverlayOpacity: v }); }} className="flex-1 accent-gray-900" /> {overlayOpacity}%
)}
, document.body ); }