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,233 @@
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { SpacingInput, SpacingValue, parseSpacingShorthand, spacingToShorthand } from './SpacingInput';
|
||||
|
||||
interface AdvancedTabProps {
|
||||
style: CSSProperties;
|
||||
onStyleChange: (updates: CSSProperties) => void;
|
||||
/** Optional: current CSS ID value */
|
||||
cssId?: string;
|
||||
onCssIdChange?: (id: string) => void;
|
||||
/** Optional: current CSS class value */
|
||||
cssClass?: string;
|
||||
onCssClassChange?: (cls: string) => void;
|
||||
/** Optional: show HTML tag selector (for containers) */
|
||||
showTagSelector?: boolean;
|
||||
tag?: string;
|
||||
onTagChange?: (tag: string) => void;
|
||||
/** Responsive visibility */
|
||||
hideOnDesktop?: boolean;
|
||||
onHideOnDesktopChange?: (hide: boolean) => void;
|
||||
hideOnTablet?: boolean;
|
||||
onHideOnTabletChange?: (hide: boolean) => void;
|
||||
hideOnMobile?: boolean;
|
||||
onHideOnMobileChange?: (hide: boolean) => void;
|
||||
/** Entrance animation */
|
||||
animation?: string;
|
||||
onAnimationChange?: (anim: string) => void;
|
||||
animationDelay?: string;
|
||||
onAnimationDelayChange?: (delay: string) => void;
|
||||
}
|
||||
|
||||
const labelStyle: React.CSSProperties = {
|
||||
fontSize: 11, fontWeight: 600, color: '#a1a1aa', display: 'block', marginBottom: 6,
|
||||
textTransform: 'uppercase', letterSpacing: '0.3px',
|
||||
};
|
||||
|
||||
const inputStyle: React.CSSProperties = {
|
||||
width: '100%', padding: '5px 8px', background: '#27272a', color: '#e4e4e7',
|
||||
border: '1px solid #3f3f46', borderRadius: 4, fontSize: 11,
|
||||
};
|
||||
|
||||
const selectStyle: React.CSSProperties = {
|
||||
width: '100%', padding: '5px 8px', background: '#27272a', color: '#e4e4e7',
|
||||
border: '1px solid #3f3f46', borderRadius: 4, fontSize: 11,
|
||||
};
|
||||
|
||||
const TAG_OPTIONS = ['div', 'section', 'article', 'header', 'footer', 'main', 'aside', 'nav'];
|
||||
|
||||
export const AdvancedTab: React.FC<AdvancedTabProps> = ({
|
||||
style,
|
||||
onStyleChange,
|
||||
cssId = '',
|
||||
onCssIdChange,
|
||||
cssClass = '',
|
||||
onCssClassChange,
|
||||
showTagSelector = false,
|
||||
tag,
|
||||
onTagChange,
|
||||
hideOnDesktop = false,
|
||||
onHideOnDesktopChange,
|
||||
hideOnTablet = false,
|
||||
onHideOnTabletChange,
|
||||
hideOnMobile = false,
|
||||
onHideOnMobileChange,
|
||||
animation = 'none',
|
||||
onAnimationChange,
|
||||
animationDelay = '0',
|
||||
onAnimationDelayChange,
|
||||
}) => {
|
||||
// Parse margin and padding from style
|
||||
const margin: SpacingValue = {
|
||||
top: (style.marginTop as string) || '0',
|
||||
right: (style.marginRight as string) || '0',
|
||||
bottom: (style.marginBottom as string) || '0',
|
||||
left: (style.marginLeft as string) || '0',
|
||||
};
|
||||
|
||||
// If there's a shorthand margin, parse it
|
||||
const marginShorthand = style.margin as string | undefined;
|
||||
const resolvedMargin = marginShorthand ? parseSpacingShorthand(marginShorthand) : margin;
|
||||
|
||||
const padding: SpacingValue = {
|
||||
top: (style.paddingTop as string) || '0',
|
||||
right: (style.paddingRight as string) || '0',
|
||||
bottom: (style.paddingBottom as string) || '0',
|
||||
left: (style.paddingLeft as string) || '0',
|
||||
};
|
||||
|
||||
const paddingShorthand = style.padding as string | undefined;
|
||||
const resolvedPadding = paddingShorthand ? parseSpacingShorthand(paddingShorthand) : padding;
|
||||
|
||||
const handleMarginChange = (val: SpacingValue) => {
|
||||
onStyleChange({
|
||||
margin: spacingToShorthand(val),
|
||||
marginTop: undefined,
|
||||
marginRight: undefined,
|
||||
marginBottom: undefined,
|
||||
marginLeft: undefined,
|
||||
} as CSSProperties);
|
||||
};
|
||||
|
||||
const handlePaddingChange = (val: SpacingValue) => {
|
||||
onStyleChange({
|
||||
padding: spacingToShorthand(val),
|
||||
paddingTop: undefined,
|
||||
paddingRight: undefined,
|
||||
paddingBottom: undefined,
|
||||
paddingLeft: undefined,
|
||||
} as CSSProperties);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
|
||||
{/* Margin */}
|
||||
<SpacingInput
|
||||
label="Margin"
|
||||
value={resolvedMargin}
|
||||
onChange={handleMarginChange}
|
||||
/>
|
||||
|
||||
{/* Padding */}
|
||||
<SpacingInput
|
||||
label="Padding"
|
||||
value={resolvedPadding}
|
||||
onChange={handlePaddingChange}
|
||||
/>
|
||||
|
||||
{/* HTML Tag (for containers) */}
|
||||
{showTagSelector && onTagChange && (
|
||||
<div>
|
||||
<label style={labelStyle}>HTML Tag</label>
|
||||
<select
|
||||
value={tag || 'div'}
|
||||
onChange={(e) => onTagChange(e.target.value)}
|
||||
style={selectStyle}
|
||||
>
|
||||
{TAG_OPTIONS.map((t) => (
|
||||
<option key={t} value={t}><{t}></option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CSS ID */}
|
||||
{onCssIdChange && (
|
||||
<div>
|
||||
<label style={labelStyle}>CSS ID</label>
|
||||
<input
|
||||
type="text"
|
||||
value={cssId}
|
||||
onChange={(e) => onCssIdChange(e.target.value.replace(/\s/g, '-'))}
|
||||
placeholder="my-element"
|
||||
style={inputStyle}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CSS Class */}
|
||||
{onCssClassChange && (
|
||||
<div>
|
||||
<label style={labelStyle}>CSS Class</label>
|
||||
<input
|
||||
type="text"
|
||||
value={cssClass}
|
||||
onChange={(e) => onCssClassChange(e.target.value)}
|
||||
placeholder="class-one class-two"
|
||||
style={inputStyle}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Responsive Visibility */}
|
||||
{(onHideOnDesktopChange || onHideOnTabletChange || onHideOnMobileChange) && (
|
||||
<div>
|
||||
<label style={labelStyle}>Visibility</label>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
{([
|
||||
{ key: 'hideOnDesktop' as const, label: 'Desktop', icon: 'fa-desktop', value: hideOnDesktop, onChange: onHideOnDesktopChange },
|
||||
{ key: 'hideOnTablet' as const, label: 'Tablet', icon: 'fa-tablet', value: hideOnTablet, onChange: onHideOnTabletChange },
|
||||
{ key: 'hideOnMobile' as const, label: 'Mobile', icon: 'fa-mobile', value: hideOnMobile, onChange: onHideOnMobileChange },
|
||||
] as const).map(({ key, label, icon, value, onChange }) => (
|
||||
<label key={key} style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 11, color: '#a1a1aa', cursor: 'pointer' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!value}
|
||||
onChange={(e) => onChange && onChange(!e.target.checked)}
|
||||
/>
|
||||
<i className={`fa ${icon}`} />
|
||||
<span>{label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Entrance Animation */}
|
||||
{onAnimationChange && (
|
||||
<div>
|
||||
<label style={labelStyle}>Entrance Animation</label>
|
||||
<select
|
||||
value={animation || 'none'}
|
||||
onChange={(e) => onAnimationChange(e.target.value)}
|
||||
style={selectStyle}
|
||||
>
|
||||
<option value="none">None</option>
|
||||
<option value="fade-in">Fade In</option>
|
||||
<option value="slide-up">Slide Up</option>
|
||||
<option value="slide-left">Slide from Left</option>
|
||||
<option value="slide-right">Slide from Right</option>
|
||||
<option value="zoom-in">Zoom In</option>
|
||||
<option value="bounce">Bounce</option>
|
||||
</select>
|
||||
{animation && animation !== 'none' && onAnimationDelayChange && (
|
||||
<div style={{ marginTop: 6 }}>
|
||||
<label style={labelStyle}>Delay</label>
|
||||
<select
|
||||
value={animationDelay || '0'}
|
||||
onChange={(e) => onAnimationDelayChange(e.target.value)}
|
||||
style={selectStyle}
|
||||
>
|
||||
<option value="0">None</option>
|
||||
<option value="0.2s">0.2s</option>
|
||||
<option value="0.4s">0.4s</option>
|
||||
<option value="0.6s">0.6s</option>
|
||||
<option value="0.8s">0.8s</option>
|
||||
<option value="1s">1s</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user