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,362 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSiteDesign, DEFAULT_SITE_DESIGN } from '../../state/SiteDesignContext';
|
||||
import { FONT_FAMILIES } from '../../constants/presets';
|
||||
|
||||
type DesignTab = 'basic' | 'advanced';
|
||||
|
||||
/* ---------- Color picker with preset swatches ---------- */
|
||||
|
||||
const COLOR_SWATCHES = [
|
||||
'#3b82f6', '#8b5cf6', '#10b981', '#ef4444',
|
||||
'#f59e0b', '#ec4899', '#06b6d4', '#f97316',
|
||||
];
|
||||
|
||||
const NEUTRAL_SWATCHES = [
|
||||
'#ffffff', '#f9fafb', '#e5e7eb', '#9ca3af',
|
||||
'#6b7280', '#374151', '#1f2937', '#111827',
|
||||
];
|
||||
|
||||
interface ColorFieldProps {
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
swatches?: string[];
|
||||
}
|
||||
|
||||
const ColorField: React.FC<ColorFieldProps> = ({ label, value, onChange, swatches = COLOR_SWATCHES }) => (
|
||||
<div className="guided-section">
|
||||
<label className="guided-section-label">{label}</label>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
|
||||
<input
|
||||
type="color"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
padding: 0,
|
||||
border: '2px solid var(--color-border)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
cursor: 'pointer',
|
||||
background: 'none',
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
className="guided-input"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
style={{ flex: 1, fontSize: 11, fontFamily: 'monospace' }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(8, 1fr)', gap: 4 }}>
|
||||
{swatches.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => onChange(c)}
|
||||
style={{
|
||||
width: '100%',
|
||||
aspectRatio: '1',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
border: value === c ? '2px solid var(--color-accent)' : '2px solid var(--color-border)',
|
||||
background: c,
|
||||
cursor: 'pointer',
|
||||
boxShadow: value === c ? '0 0 0 2px var(--color-accent)' : 'none',
|
||||
transition: 'border-color 0.15s, box-shadow 0.15s',
|
||||
}}
|
||||
title={c}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
/* ---------- Font dropdown ---------- */
|
||||
|
||||
interface FontFieldProps {
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
const FontField: React.FC<FontFieldProps> = ({ label, value, onChange }) => (
|
||||
<div className="guided-section">
|
||||
<label className="guided-section-label">{label}</label>
|
||||
<select
|
||||
className="control-select"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
style={{ fontSize: 12 }}
|
||||
>
|
||||
{FONT_FAMILIES.map((f) => (
|
||||
<option key={f.value} value={f.value} style={{ fontFamily: f.value }}>
|
||||
{f.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
|
||||
/* ---------- Border radius presets ---------- */
|
||||
|
||||
const BORDER_RADIUS_PRESETS = [
|
||||
{ label: '0', value: '0' },
|
||||
{ label: '4px', value: '4px' },
|
||||
{ label: '8px', value: '8px' },
|
||||
{ label: '12px', value: '12px' },
|
||||
{ label: '16px', value: '16px' },
|
||||
];
|
||||
|
||||
interface RadiusFieldProps {
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
const RadiusField: React.FC<RadiusFieldProps> = ({ label, value, onChange }) => (
|
||||
<div className="guided-section">
|
||||
<label className="guided-section-label">{label}</label>
|
||||
<div className="preset-grid" style={{ gridTemplateColumns: 'repeat(5, 1fr)' }}>
|
||||
{BORDER_RADIUS_PRESETS.map((p) => (
|
||||
<button
|
||||
key={p.value}
|
||||
className={`preset-btn ${value === p.value ? 'active' : ''}`}
|
||||
onClick={() => onChange(p.value)}
|
||||
>
|
||||
{p.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
/* ---------- Nav style toggle ---------- */
|
||||
|
||||
interface NavStyleFieldProps {
|
||||
value: 'light' | 'dark';
|
||||
onChange: (value: 'light' | 'dark') => void;
|
||||
}
|
||||
|
||||
const NavStyleField: React.FC<NavStyleFieldProps> = ({ value, onChange }) => (
|
||||
<div className="guided-section">
|
||||
<label className="guided-section-label">Nav Style</label>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
{(['light', 'dark'] as const).map((s) => (
|
||||
<button
|
||||
key={s}
|
||||
className={`preset-btn ${value === s ? 'active' : ''}`}
|
||||
style={{ flex: 1, textTransform: 'capitalize' }}
|
||||
onClick={() => onChange(s)}
|
||||
>
|
||||
{s === 'light' ? 'Light' : 'Dark'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
/* ---------- Main SiteDesignPanel ---------- */
|
||||
|
||||
export const SiteDesignPanel: React.FC = () => {
|
||||
const { design, updateDesign, resetToDefaults } = useSiteDesign();
|
||||
const [tab, setTab] = useState<DesignTab>('basic');
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
|
||||
{/* Header */}
|
||||
<div style={{ marginBottom: 16, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<i className="fa fa-paint-brush" style={{ color: 'var(--color-accent)', fontSize: 14 }} />
|
||||
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--color-text)' }}>
|
||||
Site Design Tokens
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Tab switcher */}
|
||||
<div style={{ display: 'flex', gap: 4, marginBottom: 16 }}>
|
||||
{(['basic', 'advanced'] as DesignTab[]).map((t) => (
|
||||
<button
|
||||
key={t}
|
||||
onClick={() => setTab(t)}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '6px 12px',
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.5px',
|
||||
color: tab === t ? '#fff' : 'var(--color-text-muted)',
|
||||
background: tab === t ? 'var(--color-accent)' : 'var(--color-bg-elevated)',
|
||||
border: tab === t ? '1px solid var(--color-accent)' : '1px solid var(--color-border)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s ease',
|
||||
}}
|
||||
>
|
||||
{t}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Basic tab */}
|
||||
{tab === 'basic' && (
|
||||
<>
|
||||
<ColorField
|
||||
label="Primary Color"
|
||||
value={design.primaryColor}
|
||||
onChange={(v) => updateDesign({ primaryColor: v })}
|
||||
/>
|
||||
<ColorField
|
||||
label="Secondary Color"
|
||||
value={design.secondaryColor}
|
||||
onChange={(v) => updateDesign({ secondaryColor: v })}
|
||||
/>
|
||||
<ColorField
|
||||
label="Accent Color"
|
||||
value={design.accentColor}
|
||||
onChange={(v) => updateDesign({ accentColor: v })}
|
||||
/>
|
||||
<FontField
|
||||
label="Heading Font"
|
||||
value={design.headingFont}
|
||||
onChange={(v) => updateDesign({ headingFont: v })}
|
||||
/>
|
||||
<FontField
|
||||
label="Body Font"
|
||||
value={design.bodyFont}
|
||||
onChange={(v) => updateDesign({ bodyFont: v })}
|
||||
/>
|
||||
<ColorField
|
||||
label="Link Color"
|
||||
value={design.linkColor}
|
||||
onChange={(v) => updateDesign({ linkColor: v })}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Advanced tab */}
|
||||
{tab === 'advanced' && (
|
||||
<>
|
||||
<ColorField
|
||||
label="Primary Color"
|
||||
value={design.primaryColor}
|
||||
onChange={(v) => updateDesign({ primaryColor: v })}
|
||||
/>
|
||||
<ColorField
|
||||
label="Secondary Color"
|
||||
value={design.secondaryColor}
|
||||
onChange={(v) => updateDesign({ secondaryColor: v })}
|
||||
/>
|
||||
<ColorField
|
||||
label="Accent Color"
|
||||
value={design.accentColor}
|
||||
onChange={(v) => updateDesign({ accentColor: v })}
|
||||
/>
|
||||
<ColorField
|
||||
label="Success Color"
|
||||
value={design.successColor}
|
||||
onChange={(v) => updateDesign({ successColor: v })}
|
||||
/>
|
||||
<ColorField
|
||||
label="Warning Color"
|
||||
value={design.warningColor}
|
||||
onChange={(v) => updateDesign({ warningColor: v })}
|
||||
/>
|
||||
<ColorField
|
||||
label="Error Color"
|
||||
value={design.errorColor}
|
||||
onChange={(v) => updateDesign({ errorColor: v })}
|
||||
/>
|
||||
<ColorField
|
||||
label="Background Color"
|
||||
value={design.backgroundColor}
|
||||
onChange={(v) => updateDesign({ backgroundColor: v })}
|
||||
swatches={NEUTRAL_SWATCHES}
|
||||
/>
|
||||
<ColorField
|
||||
label="Text Color"
|
||||
value={design.textColor}
|
||||
onChange={(v) => updateDesign({ textColor: v })}
|
||||
swatches={NEUTRAL_SWATCHES}
|
||||
/>
|
||||
<ColorField
|
||||
label="Muted Text Color"
|
||||
value={design.mutedTextColor}
|
||||
onChange={(v) => updateDesign({ mutedTextColor: v })}
|
||||
swatches={NEUTRAL_SWATCHES}
|
||||
/>
|
||||
<ColorField
|
||||
label="Border Color"
|
||||
value={design.borderColor}
|
||||
onChange={(v) => updateDesign({ borderColor: v })}
|
||||
swatches={NEUTRAL_SWATCHES}
|
||||
/>
|
||||
<FontField
|
||||
label="Heading Font"
|
||||
value={design.headingFont}
|
||||
onChange={(v) => updateDesign({ headingFont: v })}
|
||||
/>
|
||||
<FontField
|
||||
label="Body Font"
|
||||
value={design.bodyFont}
|
||||
onChange={(v) => updateDesign({ bodyFont: v })}
|
||||
/>
|
||||
<FontField
|
||||
label="Button Font"
|
||||
value={design.buttonFont}
|
||||
onChange={(v) => updateDesign({ buttonFont: v })}
|
||||
/>
|
||||
<ColorField
|
||||
label="Link Color"
|
||||
value={design.linkColor}
|
||||
onChange={(v) => updateDesign({ linkColor: v })}
|
||||
/>
|
||||
<RadiusField
|
||||
label="Default Border Radius"
|
||||
value={design.borderRadius}
|
||||
onChange={(v) => updateDesign({ borderRadius: v })}
|
||||
/>
|
||||
<RadiusField
|
||||
label="Button Radius"
|
||||
value={design.buttonRadius}
|
||||
onChange={(v) => updateDesign({ buttonRadius: v })}
|
||||
/>
|
||||
<NavStyleField
|
||||
value={design.navStyle}
|
||||
onChange={(v) => updateDesign({ navStyle: v })}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Reset button */}
|
||||
<div style={{ marginTop: 16, paddingTop: 12, borderTop: '1px solid var(--color-border)' }}>
|
||||
<button
|
||||
onClick={resetToDefaults}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
color: 'var(--color-text-muted)',
|
||||
background: 'var(--color-bg-elevated)',
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s ease',
|
||||
}}
|
||||
>
|
||||
Reset to Defaults
|
||||
</button>
|
||||
<p style={{
|
||||
fontSize: 10,
|
||||
color: 'var(--color-text-dim)',
|
||||
textAlign: 'center',
|
||||
margin: '8px 0 0',
|
||||
lineHeight: 1.4,
|
||||
}}>
|
||||
Design tokens are saved with your project and applied to new components.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user