import React from 'react'; import { useEditor } from '@craftjs/core'; import { BG_COLORS, SPACING_PRESETS, RADIUS_PRESETS, SHADOW_PRESETS, } from '../../../constants/presets'; import { StylePanelProps, SectionLabel, ColorSwatchGrid, PresetButtonGrid, CollapsibleSection, ArrayPropEditor, SpacingControl, BorderControl, BorderValue, AnimationControl, VisibilityControl, buildBorderShorthand, labelStyle, inputStyle, smallInputStyle, btnActiveStyle, sectionGap, useNodeProp, } from './shared'; /* The full sanitizeInputType (utils/escape.ts) allowlist, plus the two fake "types" (textarea/select) that take their own ContactForm render branch instead of an . Kept as a local list (rather than importing the runtime array from utils/escape.ts) since this is presentation-only -- the actual security boundary is enforced in ContactForm.toHtml via sanitizeInputType, not here. */ const CONTACT_FIELD_TYPES = [ 'text', 'email', 'tel', 'number', 'password', 'url', 'search', 'date', 'checkbox', 'radio', 'textarea', 'select', ]; const moveBtnStyle: React.CSSProperties = { flex: 1, padding: '3px 6px', fontSize: 10, background: '#27272a', color: '#a1a1aa', border: '1px solid #3f3f46', borderRadius: 4, cursor: 'pointer', }; function parseBorderValue(v: unknown): BorderValue { const s = typeof v === 'string' ? v.trim() : ''; if (!s || s === 'none') return { width: '', style: 'none', color: '' }; const m = s.match(/^(\S+)\s+(\S+)\s+(.+)$/); if (!m) return { width: '', style: 'none', color: '' }; return { width: m[1], style: m[2], color: m[3] }; } const SPACING_SIDE_KEYS: { side: 'top' | 'right' | 'bottom' | 'left'; suffix: 'Top' | 'Right' | 'Bottom' | 'Left' }[] = [ { side: 'top', suffix: 'Top' }, { side: 'right', suffix: 'Right' }, { side: 'bottom', suffix: 'Bottom' }, { side: 'left', suffix: 'Left' }, ]; /* ---------- FORM ---------- */ export const FormStylePanel: React.FC = ({ selectedId, nodeProps }) => { const { actions } = useEditor(); const { setProp, setPropStyle } = useNodeProp(selectedId); const style = nodeProps.style || {}; const updateField = (index: number, patch: Record) => { actions.setProp(selectedId, (props: any) => { const updated = [...(props.fields || [])]; updated[index] = { ...updated[index], ...patch }; props.fields = updated; }); }; const moveField = (index: number, direction: -1 | 1) => { actions.setProp(selectedId, (props: any) => { const updated = [...(props.fields || [])]; const newIndex = index + direction; if (newIndex < 0 || newIndex >= updated.length) return; [updated[index], updated[newIndex]] = [updated[newIndex], updated[index]]; props.fields = updated; }); }; return ( <> {/* ContactForm field editor: add/remove/reorder fields, set each field's label, name, type (full allowlist + textarea/select), options (select only), and required flag. */} {nodeProps.fields !== undefined && Array.isArray(nodeProps.fields) && ( (
updateField(index, { label: e.target.value })} placeholder="Label" style={smallInputStyle} /> updateField(index, { name: e.target.value })} placeholder="Field name (e.g. email)" style={smallInputStyle} /> updateField(index, { placeholder: e.target.value })} placeholder="Placeholder" style={smallInputStyle} /> {item.type === 'select' && ( updateField(index, { options: e.target.value.split(',').map((s: string) => s.trim()).filter(Boolean), })} placeholder="Options (comma-separated)" style={smallInputStyle} /> )}
)} emptyItem={{ type: 'text', label: 'New Field', name: 'field', placeholder: '', required: false }} />
)} {/* Contact-form relay: where submissions are emailed. Present on ContactForm and FormContainer (both have recipientEmail/thankYouUrl props). */} {nodeProps.recipientEmail !== undefined && (
setProp('recipientEmail', e.target.value)} placeholder="you@example.com" style={inputStyle} />

Emailed via the site's contact-form relay (an admin must enable it in Server Settings). Leave blank to use the Form Action URL instead.

)} {nodeProps.thankYouUrl !== undefined && (
setProp('thankYouUrl', e.target.value)} placeholder="/thank-you (blank = hosted page)" style={inputStyle} />
)} {/* Form action/method (FormContainer). SearchBar also has an `action` prop but is distinguished via its unique `showButton` prop -- see the dedicated Search block below -- so it doesn't get this label. */} {nodeProps.action !== undefined && nodeProps.showButton === undefined && (
setProp('action', e.target.value)} placeholder="https://..." style={inputStyle} />
)} {/* SearchBar: where the GET search request is submitted. */} {nodeProps.showButton !== undefined && nodeProps.action !== undefined && (
setProp('action', e.target.value)} placeholder="/ (site root) or /search" style={inputStyle} />

Submits a GET request with the query as ?q=... to this URL.

)} {nodeProps.showButton !== undefined && (
)} {nodeProps.method !== undefined && (
{['GET', 'POST'].map((m) => ( ))}
)} {/* Input field props */} {nodeProps.label !== undefined && (
setProp('label', e.target.value)} style={inputStyle} />
)} {nodeProps.placeholder !== undefined && (
setProp('placeholder', e.target.value)} style={inputStyle} />
)} {nodeProps.name !== undefined && (
setProp('name', e.target.value)} style={inputStyle} />
)} {nodeProps.type !== undefined && typeof nodeProps.type === 'string' && (
)} {nodeProps.required !== undefined && (
)} {/* Button text */} {nodeProps.text !== undefined && nodeProps.label === undefined && (
setProp('text', e.target.value)} style={inputStyle} />
)} {/* Subscribe form props */} {nodeProps.buttonText !== undefined && (
setProp('buttonText', e.target.value)} style={inputStyle} />
)} {nodeProps.placeholderText !== undefined && (
setProp('placeholderText', e.target.value)} style={inputStyle} />
)} {nodeProps.successMessage !== undefined && (
setProp('successMessage', e.target.value)} style={inputStyle} />
)} {/* Style */}
Background setPropStyle('backgroundColor', v)} />
Padding setPropStyle('padding', v)} />
Border Radius setPropStyle('borderRadius', v)} />
{/* Box model: margin/padding (per-side), border, shadow, opacity -- common enh-batch rollout, applies to the whole form family since they all spread `style` onto their root element. */} setPropStyle(`margin${SPACING_SIDE_KEYS.find((s) => s.side === side)!.suffix}`, v)} /> setPropStyle(`padding${SPACING_SIDE_KEYS.find((s) => s.side === side)!.suffix}`, v)} /> setPropStyle('border', buildBorderShorthand(v))} />
Box Shadow setPropStyle('boxShadow', v)} />
Opacity setPropStyle('opacity', String(Number(e.target.value) / 100))} style={{ width: '100%' }} />
{/* Animation & Visibility -- gated on the blank/false defaults added to each owned component's craft.props (ContactForm, FormContainer, InputField, TextareaField, FormButton, SubscribeForm, SearchBar). No toHtml change needed: the export's buildDataAttrs() already emits data-animation/data-hide-* from these exact prop names for every node. */} {nodeProps.animation !== undefined && ( { setProp('animation', v.animation); setProp('animationDelay', v.animationDelay); }} /> { setProp('hideOnDesktop', v.hideOnDesktop); setProp('hideOnTablet', v.hideOnTablet); setProp('hideOnMobile', v.hideOnMobile); }} /> )} ); };