Add Craft.js site builder (v2) - complete rebuild from GrapesJS
Rebuilt the visual site builder from scratch using Craft.js, React 18, and TypeScript. The new editor renders directly in the DOM (no iframe), supports 40+ components, multi-page with shared header/footer, 16 templates, full-spectrum color/gradient controls, custom head code injection, save/publish workflow, and auto-save. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useEditor } from '@craftjs/core';
|
||||
import {
|
||||
BG_COLORS,
|
||||
SPACING_PRESETS,
|
||||
RADIUS_PRESETS,
|
||||
} from '../../../constants/presets';
|
||||
import {
|
||||
StylePanelProps,
|
||||
SectionLabel,
|
||||
ColorSwatchGrid,
|
||||
PresetButtonGrid,
|
||||
CollapsibleSection,
|
||||
labelStyle,
|
||||
inputStyle,
|
||||
btnActiveStyle,
|
||||
sectionGap,
|
||||
} from './shared';
|
||||
|
||||
/* ---------- FORM ---------- */
|
||||
export const FormStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps }) => {
|
||||
const { actions } = useEditor();
|
||||
|
||||
const setProp = useCallback((key: string, value: any) => {
|
||||
actions.setProp(selectedId, (props: any) => { props[key] = value; });
|
||||
}, [actions, selectedId]);
|
||||
|
||||
const setPropStyle = useCallback((key: string, value: string) => {
|
||||
actions.setProp(selectedId, (props: any) => {
|
||||
props.style = { ...props.style, [key]: value };
|
||||
});
|
||||
}, [actions, selectedId]);
|
||||
|
||||
const style = nodeProps.style || {};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Form action/method */}
|
||||
{nodeProps.action !== undefined && (
|
||||
<div style={sectionGap}>
|
||||
<label style={labelStyle}>Form Action URL</label>
|
||||
<input type="text" value={nodeProps.action || ''} onChange={(e) => setProp('action', e.target.value)} placeholder="https://..." style={inputStyle} />
|
||||
</div>
|
||||
)}
|
||||
{nodeProps.method !== undefined && (
|
||||
<div style={sectionGap}>
|
||||
<label style={labelStyle}>Method</label>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
{['GET', 'POST'].map((m) => (
|
||||
<button key={m} onClick={() => setProp('method', m)} style={btnActiveStyle(nodeProps.method === m)}>{m}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Input field props */}
|
||||
{nodeProps.label !== undefined && (
|
||||
<div style={sectionGap}>
|
||||
<label style={labelStyle}>Label</label>
|
||||
<input type="text" value={nodeProps.label || ''} onChange={(e) => setProp('label', e.target.value)} style={inputStyle} />
|
||||
</div>
|
||||
)}
|
||||
{nodeProps.placeholder !== undefined && (
|
||||
<div style={sectionGap}>
|
||||
<label style={labelStyle}>Placeholder</label>
|
||||
<input type="text" value={nodeProps.placeholder || ''} onChange={(e) => setProp('placeholder', e.target.value)} style={inputStyle} />
|
||||
</div>
|
||||
)}
|
||||
{nodeProps.name !== undefined && (
|
||||
<div style={sectionGap}>
|
||||
<label style={labelStyle}>Field Name</label>
|
||||
<input type="text" value={nodeProps.name || ''} onChange={(e) => setProp('name', e.target.value)} style={inputStyle} />
|
||||
</div>
|
||||
)}
|
||||
{nodeProps.type !== undefined && typeof nodeProps.type === 'string' && (
|
||||
<div style={sectionGap}>
|
||||
<label style={labelStyle}>Input Type</label>
|
||||
<select value={nodeProps.type} onChange={(e) => setProp('type', e.target.value)} style={{ ...inputStyle, cursor: 'pointer' }}>
|
||||
{['text', 'email', 'password', 'number', 'tel', 'url'].map((t) => (
|
||||
<option key={t} value={t}>{t}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
{nodeProps.required !== undefined && (
|
||||
<div style={sectionGap}>
|
||||
<label style={{ ...labelStyle, display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={nodeProps.required || false} onChange={(e) => setProp('required', e.target.checked)} />
|
||||
Required
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Button text */}
|
||||
{nodeProps.text !== undefined && nodeProps.label === undefined && (
|
||||
<div style={sectionGap}>
|
||||
<label style={labelStyle}>Button Text</label>
|
||||
<input type="text" value={nodeProps.text || ''} onChange={(e) => setProp('text', e.target.value)} style={inputStyle} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Subscribe form props */}
|
||||
{nodeProps.buttonText !== undefined && (
|
||||
<div style={sectionGap}>
|
||||
<label style={labelStyle}>Button Text</label>
|
||||
<input type="text" value={nodeProps.buttonText || ''} onChange={(e) => setProp('buttonText', e.target.value)} style={inputStyle} />
|
||||
</div>
|
||||
)}
|
||||
{nodeProps.placeholderText !== undefined && (
|
||||
<div style={sectionGap}>
|
||||
<label style={labelStyle}>Placeholder Text</label>
|
||||
<input type="text" value={nodeProps.placeholderText || ''} onChange={(e) => setProp('placeholderText', e.target.value)} style={inputStyle} />
|
||||
</div>
|
||||
)}
|
||||
{nodeProps.successMessage !== undefined && (
|
||||
<div style={sectionGap}>
|
||||
<label style={labelStyle}>Success Message</label>
|
||||
<input type="text" value={nodeProps.successMessage || ''} onChange={(e) => setProp('successMessage', e.target.value)} style={inputStyle} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Style */}
|
||||
<CollapsibleSection title="Style" defaultOpen={false}>
|
||||
<div className="guided-section">
|
||||
<SectionLabel>Background</SectionLabel>
|
||||
<ColorSwatchGrid colors={BG_COLORS} activeValue={style.backgroundColor} onSelect={(v: string) => setPropStyle('backgroundColor', v)} />
|
||||
</div>
|
||||
<div className="guided-section">
|
||||
<SectionLabel>Padding</SectionLabel>
|
||||
<PresetButtonGrid presets={SPACING_PRESETS} activeValue={style.padding as string} onSelect={(v) => setPropStyle('padding', v)} />
|
||||
</div>
|
||||
<div className="guided-section">
|
||||
<SectionLabel>Border Radius</SectionLabel>
|
||||
<PresetButtonGrid presets={RADIUS_PRESETS} activeValue={style.borderRadius as string} onSelect={(v) => setPropStyle('borderRadius', v)} />
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user