/** * 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; /** 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) => 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 (
{!isCollapsed && ( <> {/* Background Image Settings */} {selectedMenuItem === 'background_image' && ( { setBackgroundImageUrl(value); if (value) setBackgroundVideoUrl(''); }} /> )} {/* Background Video Settings */} {selectedMenuItem === 'background_video' && ( { 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' && ( )} {/* Create Transition Form */} {selectedMenuItem === 'create_transition' && ( )} {/* Element Settings */} {selectedElement && ( <> setActiveTab(tab as 'general' | 'css' | 'effects') } tabs={[ { id: 'general', label: 'General' }, { id: 'css', label: 'CSS' }, { id: 'effects', label: 'Effects' }, ]} /> {/* General Tab */} {activeTab === 'general' && ( <> { 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) && ( { 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) && ( updateSelectedElement({ [prop]: value }) } /> )} {/* Description Settings */} {isDescriptionElementType(selectedElement.type) && ( updateSelectedElement({ [prop]: value }) } /> )} {/* Media Settings */} {isMediaElementType(selectedElement.type) && ( updateSelectedElement({ [prop]: value }) } /> )} {/* Gallery Settings */} {isGalleryElementType(selectedElement.type) && ( <> updateSelectedElement(patch)} onAddInfoSpan={galleryInfoSpans.add} onUpdateInfoSpan={galleryInfoSpans.update} onRemoveInfoSpan={galleryInfoSpans.remove} onAddCard={galleryCards.add} onUpdateCard={galleryCards.update} onRemoveCard={galleryCards.remove} /> )} {/* Carousel Settings */} {isCarouselElementType(selectedElement.type) && ( )} )} {/* CSS Styles Tab */} {activeTab === 'css' && ( <> {/* Gallery Section Styles (shown first for gallery elements) */} {isGalleryElementType(selectedElement.type) && (

Gallery Section Styles

updateSelectedElement({ [prop]: value || undefined }) } showFont showDimensions showTextAlign /> updateSelectedElement({ [prop]: value || undefined }) } showFont showTextAlign /> updateSelectedElement({ [prop]: value || undefined }) } showFont showGap showColumns showTextAlign /> updateSelectedElement({ [prop]: value || undefined }) } showGap showColumns showTitleStyles showAspectRatio />

General Element Styles

)} handleCssPropertyChange( prop, value, updateSelectedElement, ) } /> )} {/* Effects Tab */} {activeTab === 'effects' && ( { updateSelectedElement({ [prop]: value || undefined, }); }} /> )} )} )}
); } export default ElementEditorPanel;