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,197 @@
|
||||
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,
|
||||
ColorPickerField,
|
||||
ArrayPropEditor,
|
||||
labelStyle,
|
||||
inputStyle,
|
||||
smallInputStyle,
|
||||
sectionGap,
|
||||
} from './shared';
|
||||
|
||||
/* ---------- MEDIA (Video / Gallery / Map / Slider) ---------- */
|
||||
export const MediaStylePanel: 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 (
|
||||
<>
|
||||
{/* Video URL */}
|
||||
{nodeProps.videoUrl !== undefined && (
|
||||
<div style={sectionGap}>
|
||||
<label style={labelStyle}>Video URL</label>
|
||||
<input type="text" value={nodeProps.videoUrl || ''} onChange={(e) => setProp('videoUrl', e.target.value)} placeholder="YouTube, Vimeo, or .mp4 URL" style={inputStyle} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Map URL */}
|
||||
{nodeProps.embedUrl !== undefined && nodeProps.videoUrl === undefined && (
|
||||
<div style={sectionGap}>
|
||||
<label style={labelStyle}>Embed URL</label>
|
||||
<input type="text" value={nodeProps.embedUrl || ''} onChange={(e) => setProp('embedUrl', e.target.value)} placeholder="Google Maps embed URL" style={inputStyle} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Map address */}
|
||||
{nodeProps.address !== undefined && (
|
||||
<div style={sectionGap}>
|
||||
<label style={labelStyle}>Address</label>
|
||||
<input type="text" value={nodeProps.address || ''} onChange={(e) => setProp('address', e.target.value)} placeholder="123 Main St..." style={inputStyle} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Video options */}
|
||||
{nodeProps.autoplay !== undefined && (
|
||||
<div style={sectionGap}>
|
||||
<label style={{ ...labelStyle, display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={nodeProps.autoplay || false} onChange={(e) => setProp('autoplay', e.target.checked)} />
|
||||
Autoplay
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
{nodeProps.loop !== undefined && (
|
||||
<div style={sectionGap}>
|
||||
<label style={{ ...labelStyle, display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={nodeProps.loop || false} onChange={(e) => setProp('loop', e.target.checked)} />
|
||||
Loop
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
{nodeProps.controls !== undefined && (
|
||||
<div style={sectionGap}>
|
||||
<label style={{ ...labelStyle, display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={nodeProps.controls !== false} onChange={(e) => setProp('controls', e.target.checked)} />
|
||||
Show Controls
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Gallery items */}
|
||||
{nodeProps.images !== undefined && Array.isArray(nodeProps.images) && (
|
||||
<CollapsibleSection title="Images">
|
||||
<ArrayPropEditor
|
||||
selectedId={selectedId}
|
||||
propKey="images"
|
||||
items={nodeProps.images}
|
||||
renderItem={(item: any, index: number) => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
{item.src !== undefined && (
|
||||
<input type="text" value={item.src || ''} onChange={(e) => {
|
||||
actions.setProp(selectedId, (props: any) => {
|
||||
const updated = [...(props.images || [])];
|
||||
updated[index] = { ...updated[index], src: e.target.value };
|
||||
props.images = updated;
|
||||
});
|
||||
}} placeholder="Image URL" style={smallInputStyle} />
|
||||
)}
|
||||
{item.caption !== undefined && (
|
||||
<input type="text" value={item.caption || ''} onChange={(e) => {
|
||||
actions.setProp(selectedId, (props: any) => {
|
||||
const updated = [...(props.images || [])];
|
||||
updated[index] = { ...updated[index], caption: e.target.value };
|
||||
props.images = updated;
|
||||
});
|
||||
}} placeholder="Caption" style={smallInputStyle} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
emptyItem={{ src: '', caption: '' }}
|
||||
/>
|
||||
</CollapsibleSection>
|
||||
)}
|
||||
|
||||
{/* Slides */}
|
||||
{nodeProps.slides !== undefined && Array.isArray(nodeProps.slides) && (
|
||||
<CollapsibleSection title="Slides">
|
||||
<ArrayPropEditor
|
||||
selectedId={selectedId}
|
||||
propKey="slides"
|
||||
items={nodeProps.slides}
|
||||
renderItem={(item: any, index: number) => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
{item.heading !== undefined && (
|
||||
<input type="text" value={item.heading || ''} onChange={(e) => {
|
||||
actions.setProp(selectedId, (props: any) => {
|
||||
const updated = [...(props.slides || [])];
|
||||
updated[index] = { ...updated[index], heading: e.target.value };
|
||||
props.slides = updated;
|
||||
});
|
||||
}} placeholder="Heading" style={smallInputStyle} />
|
||||
)}
|
||||
{item.text !== undefined && (
|
||||
<textarea value={item.text || ''} onChange={(e) => {
|
||||
actions.setProp(selectedId, (props: any) => {
|
||||
const updated = [...(props.slides || [])];
|
||||
updated[index] = { ...updated[index], text: e.target.value };
|
||||
props.slides = updated;
|
||||
});
|
||||
}} placeholder="Text" rows={2} style={{ ...smallInputStyle, resize: 'vertical' }} />
|
||||
)}
|
||||
{item.image !== undefined && (
|
||||
<input type="text" value={item.image || ''} onChange={(e) => {
|
||||
actions.setProp(selectedId, (props: any) => {
|
||||
const updated = [...(props.slides || [])];
|
||||
updated[index] = { ...updated[index], image: e.target.value };
|
||||
props.slides = updated;
|
||||
});
|
||||
}} placeholder="Image URL" style={smallInputStyle} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
emptyItem={{ heading: 'New Slide', text: '', image: '' }}
|
||||
/>
|
||||
</CollapsibleSection>
|
||||
)}
|
||||
|
||||
{/* Overlay */}
|
||||
{nodeProps.overlayColor !== undefined && (
|
||||
<CollapsibleSection title="Overlay" defaultOpen={false}>
|
||||
<ColorPickerField label="Color" value={nodeProps.overlayColor || '#000000'} onChange={(v) => setProp('overlayColor', v)} />
|
||||
{nodeProps.overlayOpacity !== undefined && (
|
||||
<div style={sectionGap}>
|
||||
<label style={labelStyle}>Opacity: {nodeProps.overlayOpacity ?? 0}%</label>
|
||||
<input type="range" min={0} max={100} value={nodeProps.overlayOpacity ?? 0} onChange={(e) => setProp('overlayOpacity', parseInt(e.target.value))} style={{ width: '100%' }} />
|
||||
</div>
|
||||
)}
|
||||
</CollapsibleSection>
|
||||
)}
|
||||
|
||||
{/* Background & padding */}
|
||||
<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