4733fe84b5
Adopted in BackgroundSectionStylePanel, ButtonStylePanel, ContainerStylePanel, FormStylePanel, GenericPropsEditor, HeroStylePanel, ImageStylePanel, MediaStylePanel, NavStylePanel, SectionTypePanel, SocialStylePanel, TextStylePanel — only where the inline setProp/setPropStyle were behaviorally identical to the hook. PricingStylePanel and FeaturesEditor keep their own array-mutation logic since it differs. Behavior-preserving (task E4.1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
154 lines
7.0 KiB
TypeScript
154 lines
7.0 KiB
TypeScript
import React from 'react';
|
|
import {
|
|
StylePanelProps,
|
|
CollapsibleSection,
|
|
ColorPickerField,
|
|
labelStyle,
|
|
inputStyle,
|
|
btnActiveStyle,
|
|
sectionGap,
|
|
useNodeProp,
|
|
} from './shared';
|
|
import { AssetPicker } from '../../../ui/AssetPicker';
|
|
|
|
/* ---------- HERO STYLE PANEL ---------- */
|
|
export const HeroStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps }) => {
|
|
const { setProp } = useNodeProp(selectedId);
|
|
|
|
const bgType = nodeProps.bgType || 'color';
|
|
|
|
return (
|
|
<>
|
|
<CollapsibleSection title="Content">
|
|
<div style={sectionGap}>
|
|
<label style={labelStyle}>Heading</label>
|
|
<input type="text" value={nodeProps.heading || ''} onChange={(e) => setProp('heading', e.target.value)} style={inputStyle} />
|
|
</div>
|
|
<div style={sectionGap}>
|
|
<label style={labelStyle}>Subtitle</label>
|
|
<textarea value={nodeProps.subtitle || ''} onChange={(e) => setProp('subtitle', e.target.value)} rows={3} style={{ ...inputStyle, resize: 'vertical' as const }} />
|
|
</div>
|
|
<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>
|
|
<div style={sectionGap}>
|
|
<label style={labelStyle}>Button URL</label>
|
|
<input type="text" value={nodeProps.buttonHref || ''} onChange={(e) => setProp('buttonHref', e.target.value)} placeholder="#" style={inputStyle} />
|
|
</div>
|
|
<div style={sectionGap}>
|
|
<label style={labelStyle}>Secondary Button</label>
|
|
<input type="text" value={nodeProps.secondaryButtonText || ''} onChange={(e) => setProp('secondaryButtonText', e.target.value)} placeholder="Leave blank to hide" style={inputStyle} />
|
|
</div>
|
|
</CollapsibleSection>
|
|
|
|
<CollapsibleSection title="Background">
|
|
<div style={sectionGap}>
|
|
<label style={labelStyle}>Type</label>
|
|
<div style={{ display: 'flex', gap: 4 }}>
|
|
{(['color', 'gradient', 'image', 'video'] as const).map((t) => (
|
|
<button key={t} onClick={() => setProp('bgType', t)} style={btnActiveStyle(bgType === t)}>
|
|
{t.charAt(0).toUpperCase() + t.slice(1)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{bgType === 'color' && (
|
|
<ColorPickerField label="Color" value={nodeProps.bgColor || '#1e293b'} onChange={(v) => setProp('bgColor', v)} />
|
|
)}
|
|
|
|
{bgType === 'gradient' && (
|
|
<>
|
|
<div style={{ display: 'flex', gap: 8, ...sectionGap }}>
|
|
<div style={{ flex: 1 }}>
|
|
<ColorPickerField label="From" value={nodeProps.bgGradientFrom || '#667eea'} onChange={(v) => setProp('bgGradientFrom', v)} />
|
|
</div>
|
|
<div style={{ flex: 1 }}>
|
|
<ColorPickerField label="To" value={nodeProps.bgGradientTo || '#764ba2'} onChange={(v) => setProp('bgGradientTo', v)} />
|
|
</div>
|
|
</div>
|
|
<div style={sectionGap}>
|
|
<label style={labelStyle}>Angle: {nodeProps.bgGradientAngle || 135}°</label>
|
|
<input type="range" min={0} max={360} value={nodeProps.bgGradientAngle || 135} onChange={(e) => setProp('bgGradientAngle', parseInt(e.target.value))} style={{ width: '100%' }} />
|
|
</div>
|
|
{/* Gradient preview */}
|
|
<div style={{ height: 24, borderRadius: 4, background: `linear-gradient(${nodeProps.bgGradientAngle || 135}deg, ${nodeProps.bgGradientFrom || '#667eea'}, ${nodeProps.bgGradientTo || '#764ba2'})`, border: '1px solid #3f3f46' }} />
|
|
</>
|
|
)}
|
|
|
|
{bgType === 'image' && (
|
|
<div style={sectionGap}>
|
|
<label style={labelStyle}>Background Image</label>
|
|
<AssetPicker value={nodeProps.bgImage || ''} onChange={(url) => setProp('bgImage', url)} variant="full" />
|
|
</div>
|
|
)}
|
|
|
|
{bgType === 'video' && (
|
|
<div style={sectionGap}>
|
|
<label style={labelStyle}>Background Video</label>
|
|
<AssetPicker
|
|
mediaType="video"
|
|
variant="full"
|
|
value={nodeProps.bgVideo || ''}
|
|
onChange={(url) => setProp('bgVideo', url)}
|
|
placeholder="YouTube, Vimeo, or .mp4 URL"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div style={sectionGap}>
|
|
<label style={labelStyle}>Overlay ({nodeProps.overlayOpacity || 0}%)</label>
|
|
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
|
<input type="color" value={nodeProps.overlayColor || '#000000'} onChange={(e) => setProp('overlayColor', e.target.value)} style={{ width: 30, height: 26, border: 'none', cursor: 'pointer', background: 'none' }} />
|
|
<input type="range" min={0} max={100} value={nodeProps.overlayOpacity || 0} onChange={(e) => setProp('overlayOpacity', parseInt(e.target.value))} style={{ flex: 1 }} />
|
|
</div>
|
|
</div>
|
|
</CollapsibleSection>
|
|
|
|
<CollapsibleSection title="Colors">
|
|
<ColorPickerField label="Text Color" value={nodeProps.textColor || '#ffffff'} onChange={(v) => setProp('textColor', v)} />
|
|
<div style={{ display: 'flex', gap: 8, ...sectionGap }}>
|
|
<div style={{ flex: 1 }}>
|
|
<ColorPickerField label="Button BG" value={nodeProps.buttonBgColor || '#3b82f6'} onChange={(v) => setProp('buttonBgColor', v)} />
|
|
</div>
|
|
<div style={{ flex: 1 }}>
|
|
<ColorPickerField label="Button Text" value={nodeProps.buttonTextColor || '#ffffff'} onChange={(v) => setProp('buttonTextColor', v)} />
|
|
</div>
|
|
</div>
|
|
</CollapsibleSection>
|
|
|
|
<CollapsibleSection title="Layout">
|
|
<div style={sectionGap}>
|
|
<label style={labelStyle}>Minimum Height</label>
|
|
<div style={{ display: 'flex', gap: 4 }}>
|
|
{['300px', '400px', '500px', '600px', '100vh'].map((h) => (
|
|
<button key={h} onClick={() => setProp('minHeight', h)} style={btnActiveStyle(nodeProps.minHeight === h)}>
|
|
{h === '100vh' ? 'Full' : h}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div style={sectionGap}>
|
|
<label style={labelStyle}>Vertical</label>
|
|
<div style={{ display: 'flex', gap: 4 }}>
|
|
{(['top', 'center', 'bottom'] as const).map((v) => (
|
|
<button key={v} onClick={() => setProp('verticalAlign', v)} style={btnActiveStyle(nodeProps.verticalAlign === v)}>{v}</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div style={sectionGap}>
|
|
<label style={labelStyle}>Text Align</label>
|
|
<div style={{ display: 'flex', gap: 4 }}>
|
|
{(['left', 'center', 'right'] as const).map((a) => (
|
|
<button key={a} onClick={() => setProp('textAlign', a)} style={btnActiveStyle(nodeProps.textAlign === a)}>
|
|
<i className={`fa fa-align-${a}`} />
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</CollapsibleSection>
|
|
</>
|
|
);
|
|
};
|