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>
234 lines
7.6 KiB
TypeScript
234 lines
7.6 KiB
TypeScript
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>
|
|
);
|
|
};
|