39948-vm/frontend/src/components/Constructor/ElementEditorPanel.tsx
2026-05-05 17:25:53 +02:00

879 lines
38 KiB
TypeScript

/**
* ElementEditorPanel Component
*
* Renders the element editor sidebar in the constructor.
* Handles element settings, background settings, and transition creation.
*
* Uses ConstructorContext for all state - only receives local UI props.
*/
import React from 'react';
import {
useConstructorContext,
useConstructorElements,
useConstructorBackground,
useConstructorAssets,
useConstructorCollectionOps,
useConstructorDuration,
useConstructorNavigation,
useConstructorTransitionCreation,
useConstructorEditorTab,
useConstructorMenu,
} from '../../context/ConstructorContext';
import {
ElementSettingsTabsCompact,
StyleSettingsSectionCompact,
EffectsSettingsSectionCompact,
CommonSettingsSectionCompact,
TooltipSettingsSectionCompact,
DescriptionSettingsSectionCompact,
MediaSettingsSectionCompact,
GallerySettingsSectionCompact,
CarouselSettingsSectionCompact,
GalleryCarouselSettingsSectionCompact,
GallerySectionStyleInputs,
extractNumericValue,
} from '../ElementSettings';
import BackgroundSettingsEditor from './BackgroundSettingsEditor';
import CreateTransitionForm from './CreateTransitionForm';
import ElementEditorHeader from './ElementEditorHeader';
import NavigationSettingsSectionCompact from '../ElementSettings/NavigationSettingsSectionCompact';
import {
normalizeAppearDelaySec,
normalizeAppearDurationSec,
isNavigationElementType,
isTooltipElementType,
isDescriptionElementType,
isGalleryElementType,
isCarouselElementType,
isMediaElementType,
isVideoPlayerElementType,
} from '../../lib/elementDefaults';
import type { CanvasElement } from '../../types/constructor';
type NavigationElementType = 'navigation_next' | 'navigation_prev';
// ============================================================================
// Props Interface (Local UI props only)
// ============================================================================
interface ElementEditorPanelProps {
/** Ref for outside click detection */
elementEditorRef: React.RefObject<HTMLDivElement | null>;
/** Draggable position */
position: { x: number; y: number };
/** Whether panel is collapsed */
isCollapsed: boolean;
/** Toggle collapse state */
onToggleCollapse: () => void;
/** Start dragging the panel */
onDragStart: (event: React.MouseEvent) => void;
/** Panel title */
title: string;
}
// ============================================================================
// CSS Property Handler
// ============================================================================
/**
* Handle CSS property changes with unit conversion
*/
const handleCssPropertyChange = (
prop: string,
value: string | number | boolean,
onUpdateElement: (patch: Partial<CanvasElement>) => void,
) => {
const numericProps = [
'width',
'height',
'minWidth',
'maxWidth',
'minHeight',
'maxHeight',
'border',
'borderRadius',
'gap',
];
const getUnit = (p: string) => {
if (['width', 'minWidth', 'maxWidth'].includes(p)) return 'vw';
if (['height', 'minHeight', 'maxHeight'].includes(p)) return 'vh';
if (['border', 'borderRadius'].includes(p)) return 'px';
if (p === 'gap') return 'rem';
return '';
};
if (numericProps.includes(prop)) {
const trimmed = String(value || '').trim();
if (prop === 'border') {
onUpdateElement({
[prop]: trimmed ? `${trimmed}px solid currentColor` : 'none',
});
} else if (prop === 'borderRadius') {
onUpdateElement({
[prop]: trimmed ? `${trimmed}px` : undefined,
});
} else {
const unit = getUnit(prop);
onUpdateElement({
[prop]: trimmed ? `${trimmed}${unit}` : undefined,
});
}
} else {
onUpdateElement({
[prop]: value || undefined,
});
}
};
// ============================================================================
// Component
// ============================================================================
export function ElementEditorPanel({
elementEditorRef,
position,
isCollapsed,
onToggleCollapse,
onDragStart,
title,
}: ElementEditorPanelProps) {
// Get state from context
const {
selectedElement,
selectedElementId,
updateSelectedElement,
removeSelectedElement,
} = useConstructorElements();
const { selectedMenuItem } = useConstructorMenu();
const {
pageBackground,
setBackgroundImageUrl,
setBackgroundVideoUrl,
setBackgroundAudioUrl,
setBackgroundVideoSettings,
} = useConstructorBackground();
const { assetOptions } = useConstructorAssets();
const { galleryCards, galleryInfoSpans, carouselSlides } =
useConstructorCollectionOps();
const { getDuration, durationNotes } = useConstructorDuration();
const {
pages,
activePageId,
allowedNavigationTypes,
normalizeNavigationType,
onPreviewTransition,
} = useConstructorNavigation();
const transitionCreation = useConstructorTransitionCreation();
const { activeTab, setActiveTab } = useConstructorEditorTab();
// ============================================================================
// Render
// ============================================================================
return (
<div
ref={elementEditorRef}
className={`fixed z-[1000] ${isCollapsed ? 'w-[220px]' : 'w-[300px]'} max-h-[calc(100vh-1rem)] overflow-auto rounded-lg border border-white/30 bg-white/10 backdrop-blur-xl p-2 shadow-xl text-sm`}
style={{ left: position.x, top: position.y }}
>
<ElementEditorHeader
title={title}
isCollapsed={isCollapsed}
showRemoveButton={Boolean(selectedElement)}
onToggleCollapse={onToggleCollapse}
onRemove={removeSelectedElement}
onDragStart={onDragStart}
/>
{!isCollapsed && (
<>
{/* Background Image Settings */}
{selectedMenuItem === 'background_image' && (
<BackgroundSettingsEditor
type='image'
value={pageBackground.imageUrl}
options={assetOptions.backgroundImage}
onChange={(value) => {
setBackgroundImageUrl(value);
if (value) setBackgroundVideoUrl('');
}}
/>
)}
{/* Background Video Settings */}
{selectedMenuItem === 'background_video' && (
<BackgroundSettingsEditor
type='video'
value={pageBackground.videoUrl}
options={assetOptions.video}
durationNote={durationNotes.backgroundVideo}
onChange={(value) => {
setBackgroundVideoUrl(value);
if (value) setBackgroundImageUrl('');
}}
videoAutoplay={pageBackground.videoSettings.autoplay}
videoLoop={pageBackground.videoSettings.loop}
videoMuted={pageBackground.videoSettings.muted}
videoStartTime={pageBackground.videoSettings.startTime}
videoEndTime={pageBackground.videoSettings.endTime}
onVideoSettingsChange={setBackgroundVideoSettings}
/>
)}
{/* Background Audio Settings */}
{selectedMenuItem === 'background_audio' && (
<BackgroundSettingsEditor
type='audio'
value={pageBackground.audioUrl}
options={assetOptions.audio}
durationNote={durationNotes.backgroundAudio}
onChange={setBackgroundAudioUrl}
/>
)}
{/* Create Transition Form */}
{selectedMenuItem === 'create_transition' && (
<CreateTransitionForm
name={transitionCreation.name}
videoUrl={transitionCreation.videoUrl}
supportsReverse={transitionCreation.supportsReverse}
videoOptions={assetOptions.transitionVideo}
durationNote={durationNotes.newTransition}
isCreating={transitionCreation.isCreating}
onNameChange={transitionCreation.setName}
onVideoUrlChange={transitionCreation.setVideoUrl}
onSupportsReverseChange={transitionCreation.setSupportsReverse}
onSubmit={transitionCreation.create}
/>
)}
{/* Element Settings */}
{selectedElement && (
<>
<ElementSettingsTabsCompact
activeTab={activeTab}
onTabChange={(tab) =>
setActiveTab(tab as 'general' | 'css' | 'effects')
}
tabs={[
{ id: 'general', label: 'General' },
{ id: 'css', label: 'CSS' },
{ id: 'effects', label: 'Effects' },
]}
/>
{/* General Tab */}
{activeTab === 'general' && (
<>
<CommonSettingsSectionCompact
label={selectedElement.label}
xPercent={String(selectedElement.xPercent ?? 50)}
yPercent={String(selectedElement.yPercent ?? 50)}
appearDelaySec={String(selectedElement.appearDelaySec ?? 0)}
appearDurationSec={
selectedElement.appearDurationSec != null
? String(selectedElement.appearDurationSec)
: ''
}
showPosition={false}
onChange={(prop, value) => {
if (prop === 'label') {
updateSelectedElement({ label: value });
} else if (prop === 'appearDelaySec') {
updateSelectedElement({
appearDelaySec: normalizeAppearDelaySec(value),
});
} else if (prop === 'appearDurationSec') {
updateSelectedElement({
appearDurationSec: normalizeAppearDurationSec(value),
});
}
}}
/>
{/* Navigation Settings */}
{isNavigationElementType(selectedElement.type) && (
<NavigationSettingsSectionCompact
type={
selectedElement.type as
| 'navigation_next'
| 'navigation_prev'
}
navType={selectedElement.navType}
navLabel={selectedElement.navLabel || ''}
navLabelFontFamily={
selectedElement.navLabelFontFamily || ''
}
navDisabled={selectedElement.navDisabled || false}
iconUrl={selectedElement.iconUrl || ''}
targetPageSlug={selectedElement.targetPageSlug || ''}
transitionVideoUrl={
selectedElement.transitionVideoUrl || ''
}
transitionReverseMode={
selectedElement.transitionReverseMode || 'auto_reverse'
}
reverseVideoUrl={selectedElement.reverseVideoUrl || ''}
transitionType={selectedElement.transitionType || ''}
transitionDurationMs={
selectedElement.transitionDurationMs ?? ''
}
transitionEasing={selectedElement.transitionEasing || ''}
transitionOverlayColor={
selectedElement.transitionOverlayColor || ''
}
allowedNavigationTypes={allowedNavigationTypes}
iconAssetOptions={assetOptions.icon}
transitionVideoOptions={assetOptions.transitionVideo}
pages={pages}
activePageId={activePageId || ''}
selectedMediaDurationNote={durationNotes.selectedMedia}
selectedTransitionDurationNote={
durationNotes.selectedTransition
}
onChange={(prop, value) => {
if (prop === 'type') {
const nextType = value as NavigationElementType;
updateSelectedElement(
normalizeNavigationType(selectedElement, nextType),
);
} else if (prop === 'transitionVideoUrl') {
const nextVideoUrl = value as string;
const resolvedDuration = getDuration(nextVideoUrl);
updateSelectedElement({
transitionVideoUrl: nextVideoUrl,
transitionDurationSec:
resolvedDuration || undefined,
});
} else if (prop === 'targetPageSlug') {
updateSelectedElement({
targetPageSlug: value as string,
targetPageId: '',
});
} else {
updateSelectedElement({
[prop]: value,
});
}
}}
onPreviewTransition={onPreviewTransition}
/>
)}
{/* Tooltip Settings */}
{isTooltipElementType(selectedElement.type) && (
<TooltipSettingsSectionCompact
iconUrl={selectedElement.iconUrl || ''}
tooltipTitle={selectedElement.tooltipTitle || ''}
tooltipText={selectedElement.tooltipText || ''}
tooltipTitleFontFamily={
selectedElement.tooltipTitleFontFamily || ''
}
tooltipTextFontFamily={
selectedElement.tooltipTextFontFamily || ''
}
iconAssetOptions={assetOptions.icon}
onChange={(prop, value) =>
updateSelectedElement({ [prop]: value })
}
/>
)}
{/* Description Settings */}
{isDescriptionElementType(selectedElement.type) && (
<DescriptionSettingsSectionCompact
iconUrl={selectedElement.iconUrl || ''}
descriptionTitle={selectedElement.descriptionTitle || ''}
descriptionText={selectedElement.descriptionText || ''}
descriptionTitleFontSize={
selectedElement.descriptionTitleFontSize || '48px'
}
descriptionTextFontSize={
selectedElement.descriptionTextFontSize || '36px'
}
descriptionTitleFontFamily={
selectedElement.descriptionTitleFontFamily || 'inherit'
}
descriptionTextFontFamily={
selectedElement.descriptionTextFontFamily || 'inherit'
}
descriptionTitleColor={
selectedElement.descriptionTitleColor || '#000000'
}
descriptionTextColor={
selectedElement.descriptionTextColor || '#4B5563'
}
iconAssetOptions={assetOptions.icon}
onChange={(prop, value) =>
updateSelectedElement({ [prop]: value })
}
/>
)}
{/* Media Settings */}
{isMediaElementType(selectedElement.type) && (
<MediaSettingsSectionCompact
mediaType={
isVideoPlayerElementType(selectedElement.type)
? 'video'
: 'audio'
}
mediaUrl={selectedElement.mediaUrl || ''}
mediaAutoplay={Boolean(selectedElement.mediaAutoplay)}
mediaLoop={Boolean(selectedElement.mediaLoop)}
mediaMuted={Boolean(selectedElement.mediaMuted)}
videoAssetOptions={assetOptions.video}
audioAssetOptions={assetOptions.audio}
onChange={(prop, value) =>
updateSelectedElement({ [prop]: value })
}
/>
)}
{/* Gallery Settings */}
{isGalleryElementType(selectedElement.type) && (
<>
<GallerySettingsSectionCompact
galleryHeaderImageUrl={
selectedElement.galleryHeaderImageUrl || ''
}
galleryHeaderText={
selectedElement.galleryHeaderText || ''
}
galleryTitle={selectedElement.galleryTitle || ''}
galleryInfoSpans={
selectedElement.galleryInfoSpans || []
}
galleryCards={selectedElement.galleryCards || []}
imageAssetOptions={assetOptions.image}
iconAssetOptions={assetOptions.icon}
onUpdateHeader={(patch) => updateSelectedElement(patch)}
onAddInfoSpan={galleryInfoSpans.add}
onUpdateInfoSpan={galleryInfoSpans.update}
onRemoveInfoSpan={galleryInfoSpans.remove}
onAddCard={galleryCards.add}
onUpdateCard={galleryCards.update}
onRemoveCard={galleryCards.remove}
/>
<GalleryCarouselSettingsSectionCompact
prevIconUrl={
selectedElement.galleryCarouselPrevIconUrl || ''
}
nextIconUrl={
selectedElement.galleryCarouselNextIconUrl || ''
}
backIconUrl={
selectedElement.galleryCarouselBackIconUrl || ''
}
backLabel={
selectedElement.galleryCarouselBackLabel || ''
}
prevWidth={
selectedElement.galleryCarouselPrevWidth || ''
}
prevHeight={
selectedElement.galleryCarouselPrevHeight || ''
}
nextWidth={
selectedElement.galleryCarouselNextWidth || ''
}
nextHeight={
selectedElement.galleryCarouselNextHeight || ''
}
backWidth={
selectedElement.galleryCarouselBackWidth || ''
}
backHeight={
selectedElement.galleryCarouselBackHeight || ''
}
iconAssetOptions={assetOptions.icon}
onUpdateElement={updateSelectedElement}
/>
</>
)}
{/* Carousel Settings */}
{isCarouselElementType(selectedElement.type) && (
<CarouselSettingsSectionCompact
carouselSlides={selectedElement.carouselSlides || []}
carouselPrevIconUrl={
selectedElement.carouselPrevIconUrl || ''
}
carouselNextIconUrl={
selectedElement.carouselNextIconUrl || ''
}
carouselCaptionFontFamily={
selectedElement.carouselCaptionFontFamily || ''
}
carouselFullWidth={
selectedElement.carouselFullWidth || false
}
carouselPrevWidth={
selectedElement.carouselPrevWidth || ''
}
carouselPrevHeight={
selectedElement.carouselPrevHeight || ''
}
carouselNextWidth={
selectedElement.carouselNextWidth || ''
}
carouselNextHeight={
selectedElement.carouselNextHeight || ''
}
iconAssetOptions={assetOptions.icon}
imageAssetOptions={assetOptions.image}
onUpdateElement={updateSelectedElement}
onAddSlide={carouselSlides.add}
onUpdateSlide={carouselSlides.update}
onRemoveSlide={carouselSlides.remove}
/>
)}
</>
)}
{/* CSS Styles Tab */}
{activeTab === 'css' && (
<>
{/* Gallery Section Styles (shown first for gallery elements) */}
{isGalleryElementType(selectedElement.type) && (
<div className='space-y-2 mb-4'>
<p className='text-[11px] font-semibold text-white/90'>
Gallery Section Styles
</p>
<GallerySectionStyleInputs
sectionLabel='Header'
prefix='galleryHeader'
values={{
galleryHeaderBackgroundColor:
selectedElement.galleryHeaderBackgroundColor || '',
galleryHeaderColor:
selectedElement.galleryHeaderColor || '',
galleryHeaderFontFamily:
selectedElement.galleryHeaderFontFamily || '',
galleryHeaderFontSize:
selectedElement.galleryHeaderFontSize || '',
galleryHeaderFontWeight:
selectedElement.galleryHeaderFontWeight || '',
galleryHeaderPadding:
selectedElement.galleryHeaderPadding || '',
galleryHeaderBorderRadius:
selectedElement.galleryHeaderBorderRadius || '',
galleryHeaderBorder:
selectedElement.galleryHeaderBorder || '',
galleryHeaderWidth:
selectedElement.galleryHeaderWidth || '',
galleryHeaderHeight:
selectedElement.galleryHeaderHeight || '',
galleryHeaderMinHeight:
selectedElement.galleryHeaderMinHeight || '',
galleryHeaderMaxHeight:
selectedElement.galleryHeaderMaxHeight || '',
galleryHeaderTextAlign:
selectedElement.galleryHeaderTextAlign || 'center',
}}
onChange={(prop, value) =>
updateSelectedElement({ [prop]: value || undefined })
}
showFont
showDimensions
showTextAlign
/>
<GallerySectionStyleInputs
sectionLabel='Title'
prefix='galleryTitle'
values={{
galleryTitleBackgroundColor:
selectedElement.galleryTitleBackgroundColor || '',
galleryTitleColor:
selectedElement.galleryTitleColor || '',
galleryTitleFontFamily:
selectedElement.galleryTitleFontFamily || '',
galleryTitleFontSize:
selectedElement.galleryTitleFontSize || '',
galleryTitleFontWeight:
selectedElement.galleryTitleFontWeight || '',
galleryTitlePadding:
selectedElement.galleryTitlePadding || '',
galleryTitleBorderRadius:
selectedElement.galleryTitleBorderRadius || '',
galleryTitleBorder:
selectedElement.galleryTitleBorder || '',
galleryTitleTextAlign:
selectedElement.galleryTitleTextAlign || 'center',
}}
onChange={(prop, value) =>
updateSelectedElement({ [prop]: value || undefined })
}
showFont
showTextAlign
/>
<GallerySectionStyleInputs
sectionLabel='Info Spans'
prefix='gallerySpan'
values={{
gallerySpanBackgroundColor:
selectedElement.gallerySpanBackgroundColor || '',
gallerySpanColor:
selectedElement.gallerySpanColor || '',
gallerySpanFontFamily:
selectedElement.gallerySpanFontFamily || '',
gallerySpanFontSize:
selectedElement.gallerySpanFontSize || '',
gallerySpanFontWeight:
selectedElement.gallerySpanFontWeight || '',
gallerySpanPadding:
selectedElement.gallerySpanPadding || '',
gallerySpanBorderRadius:
selectedElement.gallerySpanBorderRadius || '',
gallerySpanBorder:
selectedElement.gallerySpanBorder || '',
gallerySpanGap: selectedElement.gallerySpanGap || '',
gallerySpanColumns:
selectedElement.gallerySpanColumns ||
selectedElement.galleryColumns ||
3,
gallerySpanTextAlign:
selectedElement.gallerySpanTextAlign || 'center',
}}
onChange={(prop, value) =>
updateSelectedElement({ [prop]: value || undefined })
}
showFont
showGap
showColumns
showTextAlign
/>
<GallerySectionStyleInputs
sectionLabel='Image Cards'
prefix='galleryCard'
values={{
galleryCardBackgroundColor:
selectedElement.galleryCardBackgroundColor || '',
galleryCardBorderRadius:
selectedElement.galleryCardBorderRadius || '',
galleryCardBorder:
selectedElement.galleryCardBorder || '',
galleryCardGap: selectedElement.galleryCardGap || '',
galleryCardColumns:
selectedElement.galleryCardColumns ||
selectedElement.galleryColumns ||
3,
galleryCardTitleColor:
selectedElement.galleryCardTitleColor || '',
galleryCardTitleBackgroundColor:
selectedElement.galleryCardTitleBackgroundColor ||
'',
galleryCardTitleFontSize:
selectedElement.galleryCardTitleFontSize || '',
galleryCardTitleFontWeight:
selectedElement.galleryCardTitleFontWeight || '',
galleryCardTitleShadow:
selectedElement.galleryCardTitleShadow || '',
galleryCardAspectRatio:
selectedElement.galleryCardAspectRatio || '',
galleryCardMinHeight:
selectedElement.galleryCardMinHeight || '',
}}
onChange={(prop, value) =>
updateSelectedElement({ [prop]: value || undefined })
}
showGap
showColumns
showTitleStyles
showAspectRatio
/>
<p className='text-[11px] font-semibold text-white/90 pt-2'>
General Element Styles
</p>
</div>
)}
<StyleSettingsSectionCompact
values={{
width: extractNumericValue(selectedElement.width),
height: extractNumericValue(selectedElement.height),
minWidth: extractNumericValue(selectedElement.minWidth),
maxWidth: extractNumericValue(selectedElement.maxWidth),
minHeight: extractNumericValue(selectedElement.minHeight),
maxHeight: extractNumericValue(selectedElement.maxHeight),
margin: selectedElement.margin || '',
padding: selectedElement.padding || '',
gap: extractNumericValue(selectedElement.gap),
fontSize: selectedElement.fontSize || '',
lineHeight: selectedElement.lineHeight || '',
fontWeight: selectedElement.fontWeight || '',
border: extractNumericValue(selectedElement.border),
borderRadius: extractNumericValue(
selectedElement.borderRadius,
),
opacity: selectedElement.opacity || '',
boxShadow: selectedElement.boxShadow || '',
display: selectedElement.display || '',
position: selectedElement.position || '',
justifyContent: selectedElement.justifyContent || '',
alignItems: selectedElement.alignItems || '',
textAlign: selectedElement.textAlign || '',
zIndex: selectedElement.zIndex || '',
backgroundColor: selectedElement.backgroundColor || '',
color: selectedElement.color || '',
}}
onChange={(prop, value) =>
handleCssPropertyChange(
prop,
value,
updateSelectedElement,
)
}
/>
</>
)}
{/* Effects Tab */}
{activeTab === 'effects' && (
<EffectsSettingsSectionCompact
elementType={selectedElement.type}
values={{
appearAnimation: selectedElement.appearAnimation || '',
appearAnimationDuration:
selectedElement.appearAnimationDuration || '',
appearAnimationEasing:
selectedElement.appearAnimationEasing || '',
hoverScale: selectedElement.hoverScale || '',
hoverOpacity: selectedElement.hoverOpacity || '',
hoverBackgroundColor:
selectedElement.hoverBackgroundColor || '',
hoverColor: selectedElement.hoverColor || '',
hoverBoxShadow: selectedElement.hoverBoxShadow || '',
hoverTransitionDuration:
selectedElement.hoverTransitionDuration || '',
focusScale: selectedElement.focusScale || '',
focusOpacity: selectedElement.focusOpacity || '',
focusOutline: selectedElement.focusOutline || '',
focusBoxShadow: selectedElement.focusBoxShadow || '',
activeScale: selectedElement.activeScale || '',
activeOpacity: selectedElement.activeOpacity || '',
activeBackgroundColor:
selectedElement.activeBackgroundColor || '',
// Slide transition values (gallery/carousel)
slideTransitionType:
selectedElement.type === 'gallery'
? selectedElement.gallerySlideTransitionType || ''
: selectedElement.carouselSlideTransitionType || '',
slideTransitionDurationMs:
selectedElement.type === 'gallery'
? selectedElement.gallerySlideTransitionDurationMs !==
undefined &&
selectedElement.gallerySlideTransitionDurationMs !==
''
? String(
selectedElement.gallerySlideTransitionDurationMs,
)
: ''
: selectedElement.carouselSlideTransitionDurationMs !==
undefined &&
selectedElement.carouselSlideTransitionDurationMs !==
''
? String(
selectedElement.carouselSlideTransitionDurationMs,
)
: '',
slideTransitionEasing:
selectedElement.type === 'gallery'
? selectedElement.gallerySlideTransitionEasing || ''
: selectedElement.carouselSlideTransitionEasing || '',
slideTransitionOverlayColor:
selectedElement.type === 'gallery'
? selectedElement.gallerySlideTransitionOverlayColor ||
''
: selectedElement.carouselSlideTransitionOverlayColor ||
'',
}}
onChange={(prop, value) => {
// Handle slide transition properties with proper prefixes
if (prop === 'slideTransitionType') {
const typedValue = (value || undefined) as
| 'fade'
| 'none'
| ''
| undefined;
if (selectedElement.type === 'gallery') {
updateSelectedElement({
gallerySlideTransitionType: typedValue,
});
} else if (selectedElement.type === 'carousel') {
updateSelectedElement({
carouselSlideTransitionType: typedValue,
});
}
} else if (prop === 'slideTransitionDurationMs') {
const ms = value ? parseInt(value, 10) : undefined;
const typedMs = ms !== undefined && ms > 0 ? ms : '';
if (selectedElement.type === 'gallery') {
updateSelectedElement({
gallerySlideTransitionDurationMs: typedMs,
});
} else if (selectedElement.type === 'carousel') {
updateSelectedElement({
carouselSlideTransitionDurationMs: typedMs,
});
}
} else if (prop === 'slideTransitionEasing') {
// Cast to proper type - form values are validated by select options
type EasingValue =
| 'ease-in-out'
| 'ease-in'
| 'ease-out'
| 'linear'
| ''
| undefined;
const typedEasing = (value || undefined) as EasingValue;
if (selectedElement.type === 'gallery') {
updateSelectedElement({
gallerySlideTransitionEasing: typedEasing,
});
} else if (selectedElement.type === 'carousel') {
updateSelectedElement({
carouselSlideTransitionEasing: typedEasing,
});
}
} else if (prop === 'slideTransitionOverlayColor') {
if (selectedElement.type === 'gallery') {
updateSelectedElement({
gallerySlideTransitionOverlayColor:
value || undefined,
});
} else if (selectedElement.type === 'carousel') {
updateSelectedElement({
carouselSlideTransitionOverlayColor:
value || undefined,
});
}
} else {
// Standard effect properties
updateSelectedElement({
[prop]: value || undefined,
});
}
}}
/>
)}
</>
)}
</>
)}
</div>
);
}
export default ElementEditorPanel;