MediaStylePanel's Slides array editor guarded on item.image !== undefined and wrote `image` via AssetPicker's onChange, but ContentSlider (render + toHtml) reads slide.imageSrc. Default slides (imageSrc:'', no `image` key) never showed an image picker at all, and any `image` value written was a silent no-op on render/export. Editor now guards/reads/writes `imageSrc` throughout, and the "add slide" emptyItem matches defaultSlides' exact shape (type/imageSrc/heading/text/bgColor). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
199 lines
8.2 KiB
TypeScript
199 lines
8.2 KiB
TypeScript
import React 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,
|
|
useNodeProp,
|
|
} from './shared';
|
|
import { AssetPicker } from '../../../ui/AssetPicker';
|
|
|
|
/* ---------- MEDIA (Video / Gallery / Map / Slider) ---------- */
|
|
export const MediaStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps }) => {
|
|
const { actions } = useEditor();
|
|
const { setProp, setPropStyle } = useNodeProp(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 && (
|
|
<AssetPicker
|
|
variant="compact"
|
|
value={item.src || ''}
|
|
onChange={(url) => {
|
|
actions.setProp(selectedId, (props: any) => {
|
|
const updated = [...(props.images || [])];
|
|
updated[index] = { ...updated[index], src: url };
|
|
props.images = updated;
|
|
});
|
|
}}
|
|
/>
|
|
)}
|
|
{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.imageSrc !== undefined && (
|
|
<AssetPicker
|
|
variant="compact"
|
|
value={item.imageSrc || ''}
|
|
onChange={(url) => {
|
|
actions.setProp(selectedId, (props: any) => {
|
|
const updated = [...(props.slides || [])];
|
|
updated[index] = { ...updated[index], imageSrc: url };
|
|
props.slides = updated;
|
|
});
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
)}
|
|
emptyItem={{ type: 'image', imageSrc: '', heading: 'New Slide', text: '', bgColor: '' }}
|
|
/>
|
|
</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>
|
|
</>
|
|
);
|
|
};
|