/** * useElementSettingsForm * * Shared hook for managing element settings form state. * Used by element-type-defaults, project-element-defaults, and constructor pages. */ import { useCallback, useState } from 'react'; import type { ElementStyleProperties } from '../../lib/elementStyles'; import type { CanvasElement, CanvasElementType, GalleryCard, CarouselSlide, InfoPanelSectionType, InfoPanelSectionInstance, } from '../../types/constructor'; import { DEFAULT_INFO_PANEL_SECTIONS, generateSectionId, } from '../../types/constructor'; import { parseJsonObject } from '../../lib/parseJson'; import { extractNumericValue, clampPercent, parseNullableNumber, toUnitValue, toOptionalTrimmed, } from './types'; import { createLocalId, isNavigationElementType, isDescriptionElementType, isGalleryElementType, isCarouselElementType, isMediaElementType, isInfoPanelElementType, } from '../../lib/elementDefaults'; interface UseElementSettingsFormOptions { elementType: CanvasElementType | string; } interface FormState { // Common settings label: string; xPercent: string; yPercent: string; appearDelaySec: string; appearDurationSec: string; // CSS style settings width: string; height: string; minWidth: string; maxWidth: string; minHeight: string; maxHeight: string; margin: string; padding: string; gap: string; fontSize: string; lineHeight: string; fontWeight: string; border: string; borderRadius: string; opacity: string; boxShadow: string; display: string; position: string; justifyContent: string; alignItems: string; textAlign: string; zIndex: string; backgroundColor: string; color: string; // Effect settings appearAnimation: string; appearAnimationDuration: string; appearAnimationEasing: string; hoverScale: string; hoverOpacity: string; hoverBackgroundColor: string; hoverColor: string; hoverBoxShadow: string; hoverTransitionDuration: string; focusScale: string; focusOpacity: string; focusOutline: string; focusBoxShadow: string; activeScale: string; activeOpacity: string; activeBackgroundColor: string; // Hover Reveal settings hoverReveal: boolean; hoverRevealInitialOpacity: string; hoverRevealTargetOpacity: string; hoverRevealDuration: string; hoverRevealDelay: string; hoverRevealPersist: boolean; // Persist hover effects after click hoverPersistOnClick: boolean; // Navigation settings iconUrl: string; navLabel: string; navLabelFontFamily: string; navType: 'forward' | 'back'; navDisabled: boolean; targetPageId: string; targetPageSlug: string; transitionVideoUrl: string; transitionReverseMode: 'auto_reverse' | 'separate_video'; reverseVideoUrl: string; // Description settings descriptionTitle: string; descriptionText: string; descriptionTitleFontSize: string; descriptionTextFontSize: string; descriptionTitleFontFamily: string; descriptionTextFontFamily: string; descriptionTitleColor: string; descriptionTextColor: string; // Media settings mediaUrl: string; mediaAutoplay: boolean; mediaLoop: boolean; mediaMuted: boolean; // Carousel settings carouselPrevIconUrl: string; carouselNextIconUrl: string; carouselCaptionFontFamily: string; // Gallery settings galleryTitleFontFamily: string; galleryTextFontFamily: string; // Complex arrays galleryCards: GalleryCard[]; carouselSlides: CarouselSlide[]; // Info Panel settings infoPanelTriggerLabel: string; infoPanelTriggerFontFamily: string; infoPanelDisabled: boolean; panelTitle: string; panelText: string; panelXPercent: string; panelYPercent: string; panelWidth: string; panelHeight: string; panelBackgroundColor: string; panelBorderRadius: string; panelPadding: string; panelBackdropBlur: string; panelTitleColor: string; panelTitleFontSize: string; panelTitleFontFamily: string; panelTextColor: string; panelTextFontSize: string; panelTextFontFamily: string; panelOverlayColor: string; detailXPercent: string; detailYPercent: string; detailWidth: string; detailHeight: string; detailBackgroundColor: string; detailBorderRadius: string; detailPadding: string; detailCaptionFontFamily: string; // Note: detailOverlayColor removed - parent InfoPanelOverlay provides backdrop // Info Panel - Header Section (like Gallery) infoPanelHeaderImageUrl: string; infoPanelHeaderText: string; infoPanelHeaderBackgroundColor: string; infoPanelHeaderColor: string; infoPanelHeaderFontFamily: string; infoPanelHeaderFontSize: string; infoPanelHeaderFontWeight: string; infoPanelHeaderPadding: string; infoPanelHeaderBorderRadius: string; infoPanelHeaderTextAlign: string; infoPanelHeaderMinHeight: string; // Info Panel - Title Section enhanced styling infoPanelTitleBackgroundColor: string; infoPanelTitlePadding: string; infoPanelTitleFontWeight: string; infoPanelTitleTextAlign: string; // Info Panel - Span styling infoPanelSpanBackgroundColor: string; infoPanelSpanColor: string; infoPanelSpanFontFamily: string; infoPanelSpanFontSize: string; infoPanelSpanPadding: string; infoPanelSpanBorderRadius: string; // Info Panel - Card styling infoPanelCardBackgroundColor: string; infoPanelCardBorderRadius: string; infoPanelCardAspectRatio: string; infoPanelCardMinHeight: string; infoPanelCardTitleBackgroundColor: string; infoPanelCardTitleColor: string; infoPanelCardTitleFontFamily: string; infoPanelCardTitleFontSize: string; infoPanelCardTitlePadding: string; // Info Panel - Section instances (order + per-section settings) infoPanelSections: InfoPanelSectionInstance[]; } const initialState: FormState = { label: '', xPercent: '0', yPercent: '0', appearDelaySec: '0', appearDurationSec: '', width: '', height: '', minWidth: '', maxWidth: '', minHeight: '', maxHeight: '', margin: '', padding: '', gap: '', fontSize: '', lineHeight: '', fontWeight: '', border: '', borderRadius: '', opacity: '', boxShadow: '', display: '', position: '', justifyContent: '', alignItems: '', textAlign: '', zIndex: '', backgroundColor: '', color: '', // Effect settings appearAnimation: '', appearAnimationDuration: '', appearAnimationEasing: '', hoverScale: '', hoverOpacity: '', hoverBackgroundColor: '', hoverColor: '', hoverBoxShadow: '', hoverTransitionDuration: '', focusScale: '', focusOpacity: '', focusOutline: '', focusBoxShadow: '', activeScale: '', activeOpacity: '', activeBackgroundColor: '', // Hover Reveal settings hoverReveal: false, hoverRevealInitialOpacity: '', hoverRevealTargetOpacity: '', hoverRevealDuration: '', hoverRevealDelay: '', hoverRevealPersist: false, hoverPersistOnClick: false, iconUrl: '', navLabel: '', navLabelFontFamily: '', navType: 'forward', navDisabled: false, targetPageId: '', targetPageSlug: '', transitionVideoUrl: '', transitionReverseMode: 'auto_reverse', reverseVideoUrl: '', descriptionTitle: '', descriptionText: '', descriptionTitleFontSize: '', descriptionTextFontSize: '', descriptionTitleFontFamily: '', descriptionTextFontFamily: '', descriptionTitleColor: '', descriptionTextColor: '', mediaUrl: '', mediaAutoplay: false, mediaLoop: false, mediaMuted: false, carouselPrevIconUrl: '', carouselNextIconUrl: '', carouselCaptionFontFamily: '', galleryTitleFontFamily: '', galleryTextFontFamily: '', galleryCards: [], carouselSlides: [], // Info Panel settings infoPanelTriggerLabel: '', infoPanelTriggerFontFamily: '', infoPanelDisabled: false, panelTitle: '', panelText: '', panelXPercent: '0', panelYPercent: '0', panelWidth: '', panelHeight: '', panelBackgroundColor: '', panelBorderRadius: '', panelPadding: '', panelBackdropBlur: '', panelTitleColor: '', panelTitleFontSize: '', panelTitleFontFamily: '', panelTextColor: '', panelTextFontSize: '', panelTextFontFamily: '', panelOverlayColor: '', detailXPercent: '0', detailYPercent: '0', detailWidth: '', detailHeight: '', detailBackgroundColor: '', detailBorderRadius: '', detailPadding: '', detailCaptionFontFamily: '', // Info Panel - Header Section infoPanelHeaderImageUrl: '', infoPanelHeaderText: '', infoPanelHeaderBackgroundColor: '', infoPanelHeaderColor: '', infoPanelHeaderFontFamily: '', infoPanelHeaderFontSize: '', infoPanelHeaderFontWeight: '', infoPanelHeaderPadding: '', infoPanelHeaderBorderRadius: '', infoPanelHeaderTextAlign: '', infoPanelHeaderMinHeight: '', // Info Panel - Title Section enhanced infoPanelTitleBackgroundColor: '', infoPanelTitlePadding: '', infoPanelTitleFontWeight: '', infoPanelTitleTextAlign: '', // Info Panel - Span styling infoPanelSpanBackgroundColor: '', infoPanelSpanColor: '', infoPanelSpanFontFamily: '', infoPanelSpanFontSize: '', infoPanelSpanPadding: '', infoPanelSpanBorderRadius: '', // Info Panel - Card styling infoPanelCardBackgroundColor: '', infoPanelCardBorderRadius: '', infoPanelCardAspectRatio: '', infoPanelCardMinHeight: '', infoPanelCardTitleBackgroundColor: '', infoPanelCardTitleColor: '', infoPanelCardTitleFontFamily: '', infoPanelCardTitleFontSize: '', infoPanelCardTitlePadding: '', // Info Panel - Section instances infoPanelSections: DEFAULT_INFO_PANEL_SECTIONS.map((s) => ({ ...s })), }; export function useElementSettingsForm(options: UseElementSettingsFormOptions) { const { elementType } = options; const [state, setState] = useState(initialState); // Type detection using shared helpers from elementDefaults const isNavigationType = isNavigationElementType(elementType); const isDescriptionType = isDescriptionElementType(elementType); const isGalleryType = isGalleryElementType(elementType); const isCarouselType = isCarouselElementType(elementType); const isMediaType = isMediaElementType(elementType); const isInfoPanelType = isInfoPanelElementType(elementType); /** * Apply settings from JSON to form state */ const applySettings = useCallback( (settingsValue?: string | Record) => { const settings = parseJsonObject>( settingsValue, {}, ); setState({ label: String(settings.label || ''), xPercent: String(settings.xPercent ?? 0), yPercent: String(settings.yPercent ?? 0), width: extractNumericValue(settings.width), height: extractNumericValue(settings.height), minWidth: extractNumericValue(settings.minWidth), maxWidth: extractNumericValue(settings.maxWidth), minHeight: extractNumericValue(settings.minHeight), maxHeight: extractNumericValue(settings.maxHeight), margin: String(settings.margin || ''), padding: String(settings.padding || ''), gap: String(settings.gap || ''), fontSize: String(settings.fontSize || ''), lineHeight: String(settings.lineHeight || ''), fontWeight: String(settings.fontWeight || ''), border: extractNumericValue(settings.border), borderRadius: extractNumericValue(settings.borderRadius), opacity: settings.opacity === 0 ? '0' : String(settings.opacity || ''), boxShadow: String(settings.boxShadow || ''), display: String(settings.display || ''), position: String(settings.position || ''), justifyContent: String(settings.justifyContent || ''), alignItems: String(settings.alignItems || ''), textAlign: String(settings.textAlign || ''), zIndex: String(settings.zIndex || ''), backgroundColor: String(settings.backgroundColor || ''), color: String(settings.color || ''), // Effect settings appearAnimation: String(settings.appearAnimation || ''), appearAnimationDuration: String(settings.appearAnimationDuration || ''), appearAnimationEasing: String(settings.appearAnimationEasing || ''), hoverScale: String(settings.hoverScale || ''), hoverOpacity: String(settings.hoverOpacity || ''), hoverBackgroundColor: String(settings.hoverBackgroundColor || ''), hoverColor: String(settings.hoverColor || ''), hoverBoxShadow: String(settings.hoverBoxShadow || ''), hoverTransitionDuration: String(settings.hoverTransitionDuration || ''), focusScale: String(settings.focusScale || ''), focusOpacity: String(settings.focusOpacity || ''), focusOutline: String(settings.focusOutline || ''), focusBoxShadow: String(settings.focusBoxShadow || ''), activeScale: String(settings.activeScale || ''), activeOpacity: String(settings.activeOpacity || ''), activeBackgroundColor: String(settings.activeBackgroundColor || ''), // Hover Reveal settings hoverReveal: Boolean(settings.hoverReveal), hoverRevealInitialOpacity: String( settings.hoverRevealInitialOpacity || '', ), hoverRevealTargetOpacity: String( settings.hoverRevealTargetOpacity || '', ), hoverRevealDuration: String(settings.hoverRevealDuration || ''), hoverRevealDelay: String(settings.hoverRevealDelay || ''), hoverRevealPersist: Boolean(settings.hoverRevealPersist), hoverPersistOnClick: Boolean(settings.hoverPersistOnClick), appearDelaySec: String(settings.appearDelaySec ?? 0), appearDurationSec: settings.appearDurationSec === null || settings.appearDurationSec === undefined ? '' : String(settings.appearDurationSec), iconUrl: String(settings.iconUrl || ''), navLabel: String(settings.navLabel || ''), navLabelFontFamily: String(settings.navLabelFontFamily || ''), navType: settings.navType === 'back' ? 'back' : 'forward', navDisabled: Boolean(settings.navDisabled), targetPageId: String(settings.targetPageId || ''), targetPageSlug: String(settings.targetPageSlug || ''), transitionVideoUrl: String(settings.transitionVideoUrl || ''), transitionReverseMode: settings.transitionReverseMode === 'separate_video' ? 'separate_video' : 'auto_reverse', reverseVideoUrl: String(settings.reverseVideoUrl || ''), descriptionTitle: String(settings.descriptionTitle || ''), descriptionText: String(settings.descriptionText || ''), descriptionTitleFontSize: String( settings.descriptionTitleFontSize || '', ), descriptionTextFontSize: String(settings.descriptionTextFontSize || ''), descriptionTitleFontFamily: String( settings.descriptionTitleFontFamily || '', ), descriptionTextFontFamily: String( settings.descriptionTextFontFamily || '', ), descriptionTitleColor: String(settings.descriptionTitleColor || ''), descriptionTextColor: String(settings.descriptionTextColor || ''), mediaUrl: String(settings.mediaUrl || ''), mediaAutoplay: Boolean(settings.mediaAutoplay), mediaLoop: Boolean(settings.mediaLoop), mediaMuted: Boolean(settings.mediaMuted), carouselPrevIconUrl: String(settings.carouselPrevIconUrl || ''), carouselNextIconUrl: String(settings.carouselNextIconUrl || ''), carouselCaptionFontFamily: String( settings.carouselCaptionFontFamily || '', ), galleryTitleFontFamily: String(settings.galleryTitleFontFamily || ''), galleryTextFontFamily: String(settings.galleryTextFontFamily || ''), galleryCards: Array.isArray(settings.galleryCards) ? settings.galleryCards.map((card: Record) => ({ id: String(card?.id || createLocalId()), imageUrl: String(card?.imageUrl ?? ''), title: String(card?.title ?? ''), description: String(card?.description ?? ''), })) : [], carouselSlides: Array.isArray(settings.carouselSlides) ? settings.carouselSlides.map((slide: Record) => ({ id: String(slide?.id || createLocalId()), imageUrl: String(slide?.imageUrl ?? ''), caption: String(slide?.caption ?? ''), })) : [], // Info Panel settings infoPanelTriggerLabel: String(settings.infoPanelTriggerLabel || ''), infoPanelTriggerFontFamily: String( settings.infoPanelTriggerFontFamily || '', ), infoPanelDisabled: Boolean(settings.infoPanelDisabled), panelTitle: String(settings.panelTitle || ''), panelText: String(settings.panelText || ''), panelXPercent: String(settings.panelXPercent ?? 0), panelYPercent: String(settings.panelYPercent ?? 0), panelWidth: String(settings.panelWidth || ''), panelHeight: String(settings.panelHeight || ''), panelBackgroundColor: String(settings.panelBackgroundColor || ''), panelBorderRadius: String(settings.panelBorderRadius || ''), panelPadding: String(settings.panelPadding || ''), panelBackdropBlur: String(settings.panelBackdropBlur || ''), panelTitleColor: String(settings.panelTitleColor || ''), panelTitleFontSize: String(settings.panelTitleFontSize || ''), panelTitleFontFamily: String(settings.panelTitleFontFamily || ''), panelTextColor: String(settings.panelTextColor || ''), panelTextFontSize: String(settings.panelTextFontSize || ''), panelTextFontFamily: String(settings.panelTextFontFamily || ''), panelOverlayColor: String(settings.panelOverlayColor || ''), detailXPercent: String(settings.detailXPercent ?? 0), detailYPercent: String(settings.detailYPercent ?? 0), detailWidth: String(settings.detailWidth || ''), detailHeight: String(settings.detailHeight || ''), detailBackgroundColor: String(settings.detailBackgroundColor || ''), detailBorderRadius: String(settings.detailBorderRadius || ''), detailPadding: String(settings.detailPadding || ''), detailCaptionFontFamily: String(settings.detailCaptionFontFamily || ''), // Info Panel - Header Section infoPanelHeaderImageUrl: String(settings.infoPanelHeaderImageUrl || ''), infoPanelHeaderText: String(settings.infoPanelHeaderText || ''), infoPanelHeaderBackgroundColor: String( settings.infoPanelHeaderBackgroundColor || '', ), infoPanelHeaderColor: String(settings.infoPanelHeaderColor || ''), infoPanelHeaderFontFamily: String( settings.infoPanelHeaderFontFamily || '', ), infoPanelHeaderFontSize: String(settings.infoPanelHeaderFontSize || ''), infoPanelHeaderFontWeight: String( settings.infoPanelHeaderFontWeight || '', ), infoPanelHeaderPadding: String(settings.infoPanelHeaderPadding || ''), infoPanelHeaderBorderRadius: String( settings.infoPanelHeaderBorderRadius || '', ), infoPanelHeaderTextAlign: String( settings.infoPanelHeaderTextAlign || '', ), infoPanelHeaderMinHeight: String( settings.infoPanelHeaderMinHeight || '', ), // Info Panel - Title Section enhanced infoPanelTitleBackgroundColor: String( settings.infoPanelTitleBackgroundColor || '', ), infoPanelTitlePadding: String(settings.infoPanelTitlePadding || ''), infoPanelTitleFontWeight: String( settings.infoPanelTitleFontWeight || '', ), infoPanelTitleTextAlign: String(settings.infoPanelTitleTextAlign || ''), // Info Panel - Span styling infoPanelSpanBackgroundColor: String( settings.infoPanelSpanBackgroundColor || '', ), infoPanelSpanColor: String(settings.infoPanelSpanColor || ''), infoPanelSpanFontFamily: String(settings.infoPanelSpanFontFamily || ''), infoPanelSpanFontSize: String(settings.infoPanelSpanFontSize || ''), infoPanelSpanPadding: String(settings.infoPanelSpanPadding || ''), infoPanelSpanBorderRadius: String( settings.infoPanelSpanBorderRadius || '', ), // Info Panel - Card styling infoPanelCardBackgroundColor: String( settings.infoPanelCardBackgroundColor || '', ), infoPanelCardBorderRadius: String( settings.infoPanelCardBorderRadius || '', ), infoPanelCardAspectRatio: String( settings.infoPanelCardAspectRatio || '', ), infoPanelCardMinHeight: String(settings.infoPanelCardMinHeight || ''), infoPanelCardTitleBackgroundColor: String( settings.infoPanelCardTitleBackgroundColor || '', ), infoPanelCardTitleColor: String(settings.infoPanelCardTitleColor || ''), infoPanelCardTitleFontFamily: String( settings.infoPanelCardTitleFontFamily || '', ), infoPanelCardTitleFontSize: String( settings.infoPanelCardTitleFontSize || '', ), infoPanelCardTitlePadding: String( settings.infoPanelCardTitlePadding || '', ), // Info Panel - Section instances infoPanelSections: Array.isArray(settings.infoPanelSections) ? (settings.infoPanelSections as InfoPanelSectionInstance[]) : DEFAULT_INFO_PANEL_SECTIONS.map((s) => ({ ...s })), }); }, [], ); /** * Update a single field */ const setField = useCallback( (field: K, value: FormState[K]) => { setState((prev) => ({ ...prev, [field]: value })); }, [], ); /** * Update multiple fields at once */ const setFields = useCallback((updates: Partial) => { setState((prev) => ({ ...prev, ...updates })); }, []); /** * Get CSS style values for StyleSettingsSection */ const getStyleValues = useCallback((): ElementStyleProperties => { return { width: state.width, height: state.height, minWidth: state.minWidth, maxWidth: state.maxWidth, minHeight: state.minHeight, maxHeight: state.maxHeight, margin: state.margin, padding: state.padding, gap: state.gap, fontSize: state.fontSize, lineHeight: state.lineHeight, fontWeight: state.fontWeight, border: state.border, borderRadius: state.borderRadius, opacity: state.opacity, boxShadow: state.boxShadow, display: state.display, position: state.position, justifyContent: state.justifyContent, alignItems: state.alignItems, textAlign: state.textAlign, zIndex: state.zIndex, backgroundColor: state.backgroundColor, color: state.color, }; }, [state]); /** * Get effect values for EffectsSettingsSection */ const getEffectValues = useCallback(() => { return { appearAnimation: state.appearAnimation, appearAnimationDuration: state.appearAnimationDuration, appearAnimationEasing: state.appearAnimationEasing, hoverScale: state.hoverScale, hoverOpacity: state.hoverOpacity, hoverBackgroundColor: state.hoverBackgroundColor, hoverColor: state.hoverColor, hoverBoxShadow: state.hoverBoxShadow, hoverTransitionDuration: state.hoverTransitionDuration, focusScale: state.focusScale, focusOpacity: state.focusOpacity, focusOutline: state.focusOutline, focusBoxShadow: state.focusBoxShadow, activeScale: state.activeScale, activeOpacity: state.activeOpacity, activeBackgroundColor: state.activeBackgroundColor, // Convert booleans to strings for form compatibility hoverReveal: state.hoverReveal ? 'true' : '', hoverRevealInitialOpacity: state.hoverRevealInitialOpacity, hoverRevealTargetOpacity: state.hoverRevealTargetOpacity, hoverRevealDuration: state.hoverRevealDuration, hoverRevealDelay: state.hoverRevealDelay, hoverRevealPersist: state.hoverRevealPersist ? 'true' : '', hoverPersistOnClick: state.hoverPersistOnClick ? 'true' : '', }; }, [state]); /** * Gallery card operations */ const addGalleryCard = useCallback(() => { setState((prev) => ({ ...prev, galleryCards: [ ...prev.galleryCards, { id: createLocalId(), imageUrl: '', title: `Card ${prev.galleryCards.length + 1}`, description: '', }, ], })); }, []); const removeGalleryCard = useCallback((cardId: string) => { setState((prev) => ({ ...prev, galleryCards: prev.galleryCards.filter((card) => card.id !== cardId), })); }, []); const updateGalleryCard = useCallback( (cardId: string, field: keyof GalleryCard, value: string) => { setState((prev) => ({ ...prev, galleryCards: prev.galleryCards.map((card) => card.id === cardId ? { ...card, [field]: value } : card, ), })); }, [], ); /** * Carousel slide operations */ const addCarouselSlide = useCallback(() => { setState((prev) => ({ ...prev, carouselSlides: [ ...prev.carouselSlides, { id: createLocalId(), imageUrl: '', caption: `Slide ${prev.carouselSlides.length + 1}`, }, ], })); }, []); const removeCarouselSlide = useCallback((slideId: string) => { setState((prev) => ({ ...prev, carouselSlides: prev.carouselSlides.filter( (slide) => slide.id !== slideId, ), })); }, []); const updateCarouselSlide = useCallback( (slideId: string, field: keyof CarouselSlide, value: string) => { setState((prev) => ({ ...prev, carouselSlides: prev.carouselSlides.map((slide) => slide.id === slideId ? { ...slide, [field]: value } : slide, ), })); }, [], ); /** * Info panel section operations */ const moveInfoPanelSection = useCallback( (sectionId: string, direction: 'up' | 'down') => { setState((prev) => { const sections = [...prev.infoPanelSections]; const index = sections.findIndex((s) => s.id === sectionId); if (index === -1) return prev; const newIndex = direction === 'up' ? index - 1 : index + 1; if (newIndex < 0 || newIndex >= sections.length) return prev; [sections[index], sections[newIndex]] = [ sections[newIndex], sections[index], ]; return { ...prev, infoPanelSections: sections }; }); }, [], ); const removeInfoPanelSection = useCallback((sectionId: string) => { setState((prev) => ({ ...prev, infoPanelSections: prev.infoPanelSections.filter( (s) => s.id !== sectionId, ), })); }, []); const addInfoPanelSection = useCallback( (sectionType: InfoPanelSectionType) => { setState((prev) => { const newSection: InfoPanelSectionInstance = { id: generateSectionId(), type: sectionType, // Initialize with default settings for data sections ...(sectionType === 'spans' && { columns: 3, gap: '8', spans: [] }), ...(sectionType === 'cards' && { columns: 2, gap: '8', images: [] }), ...(sectionType === 'images' && { images: [] }), }; return { ...prev, infoPanelSections: [...prev.infoPanelSections, newSection], }; }); }, [], ); /** * Build settings JSON for saving */ const buildSettingsJson = useCallback((): Record => { const borderWidthValue = toUnitValue(state.border, 'px'); const borderRadiusValue = toUnitValue(state.borderRadius, 'px'); const settings: Record = { label: state.label.trim(), xPercent: clampPercent(state.xPercent), yPercent: clampPercent(state.yPercent), border: borderWidthValue ? `${borderWidthValue} solid currentColor` : 'none', appearDelaySec: Number(state.appearDelaySec) >= 0 ? Number(state.appearDelaySec) : 0, appearDurationSec: parseNullableNumber(state.appearDurationSec), }; // borderRadius: only include if has value (allows cascade from global defaults) if (borderRadiusValue) settings.borderRadius = borderRadiusValue; // Dimensional CSS properties const widthValue = toUnitValue(state.width, 'vw'); const heightValue = toUnitValue(state.height, 'vh'); const minWidthValue = toUnitValue(state.minWidth, 'vw'); const maxWidthValue = toUnitValue(state.maxWidth, 'vw'); const minHeightValue = toUnitValue(state.minHeight, 'vh'); const maxHeightValue = toUnitValue(state.maxHeight, 'vh'); if (widthValue) settings.width = widthValue; if (heightValue) settings.height = heightValue; if (minWidthValue) settings.minWidth = minWidthValue; if (maxWidthValue) settings.maxWidth = maxWidthValue; if (minHeightValue) settings.minHeight = minHeightValue; if (maxHeightValue) settings.maxHeight = maxHeightValue; // Other CSS properties const marginValue = toOptionalTrimmed(state.margin); const paddingValue = toOptionalTrimmed(state.padding); const gapValue = toOptionalTrimmed(state.gap); const fontSizeValue = toOptionalTrimmed(state.fontSize); const lineHeightValue = toOptionalTrimmed(state.lineHeight); const fontWeightValue = toOptionalTrimmed(state.fontWeight); const opacityValue = toOptionalTrimmed(state.opacity); const boxShadowValue = toOptionalTrimmed(state.boxShadow); const displayValue = toOptionalTrimmed(state.display); const positionValue = toOptionalTrimmed(state.position); const justifyContentValue = toOptionalTrimmed(state.justifyContent); const alignItemsValue = toOptionalTrimmed(state.alignItems); const textAlignValue = toOptionalTrimmed(state.textAlign); const zIndexValue = toOptionalTrimmed(state.zIndex); if (marginValue) settings.margin = marginValue; if (paddingValue) settings.padding = paddingValue; if (gapValue) settings.gap = gapValue; if (fontSizeValue) settings.fontSize = fontSizeValue; if (lineHeightValue) settings.lineHeight = lineHeightValue; if (fontWeightValue) settings.fontWeight = fontWeightValue; if (opacityValue) settings.opacity = opacityValue; if (boxShadowValue) settings.boxShadow = boxShadowValue; if (displayValue) settings.display = displayValue; if (positionValue) settings.position = positionValue; if (justifyContentValue) settings.justifyContent = justifyContentValue; if (alignItemsValue) settings.alignItems = alignItemsValue; if (textAlignValue) settings.textAlign = textAlignValue; if (zIndexValue) settings.zIndex = zIndexValue; // Additional CSS properties const backgroundColorValue = toOptionalTrimmed(state.backgroundColor); const colorValue = toOptionalTrimmed(state.color); if (backgroundColorValue) settings.backgroundColor = backgroundColorValue; if (colorValue) settings.color = colorValue; // Effect properties const appearAnimationValue = toOptionalTrimmed(state.appearAnimation); const appearAnimationDurationValue = toOptionalTrimmed( state.appearAnimationDuration, ); const appearAnimationEasingValue = toOptionalTrimmed( state.appearAnimationEasing, ); const hoverScaleValue = toOptionalTrimmed(state.hoverScale); const hoverOpacityValue = toOptionalTrimmed(state.hoverOpacity); const hoverBackgroundColorValue = toOptionalTrimmed( state.hoverBackgroundColor, ); const hoverColorValue = toOptionalTrimmed(state.hoverColor); const hoverBoxShadowValue = toOptionalTrimmed(state.hoverBoxShadow); const hoverTransitionDurationValue = toOptionalTrimmed( state.hoverTransitionDuration, ); const focusScaleValue = toOptionalTrimmed(state.focusScale); const focusOpacityValue = toOptionalTrimmed(state.focusOpacity); const focusOutlineValue = toOptionalTrimmed(state.focusOutline); const focusBoxShadowValue = toOptionalTrimmed(state.focusBoxShadow); const activeScaleValue = toOptionalTrimmed(state.activeScale); const activeOpacityValue = toOptionalTrimmed(state.activeOpacity); const activeBackgroundColorValue = toOptionalTrimmed( state.activeBackgroundColor, ); if (appearAnimationValue) settings.appearAnimation = appearAnimationValue; if (appearAnimationDurationValue) settings.appearAnimationDuration = appearAnimationDurationValue; if (appearAnimationEasingValue) settings.appearAnimationEasing = appearAnimationEasingValue; if (hoverScaleValue) settings.hoverScale = hoverScaleValue; if (hoverOpacityValue) settings.hoverOpacity = hoverOpacityValue; if (hoverBackgroundColorValue) settings.hoverBackgroundColor = hoverBackgroundColorValue; if (hoverColorValue) settings.hoverColor = hoverColorValue; if (hoverBoxShadowValue) settings.hoverBoxShadow = hoverBoxShadowValue; if (hoverTransitionDurationValue) settings.hoverTransitionDuration = hoverTransitionDurationValue; if (focusScaleValue) settings.focusScale = focusScaleValue; if (focusOpacityValue) settings.focusOpacity = focusOpacityValue; if (focusOutlineValue) settings.focusOutline = focusOutlineValue; if (focusBoxShadowValue) settings.focusBoxShadow = focusBoxShadowValue; if (activeScaleValue) settings.activeScale = activeScaleValue; if (activeOpacityValue) settings.activeOpacity = activeOpacityValue; if (activeBackgroundColorValue) settings.activeBackgroundColor = activeBackgroundColorValue; // Hover Reveal properties (booleans saved directly, strings trimmed) if (state.hoverReveal) settings.hoverReveal = true; const hoverRevealInitialOpacityValue = toOptionalTrimmed( state.hoverRevealInitialOpacity, ); const hoverRevealTargetOpacityValue = toOptionalTrimmed( state.hoverRevealTargetOpacity, ); const hoverRevealDurationValue = toOptionalTrimmed( state.hoverRevealDuration, ); const hoverRevealDelayValue = toOptionalTrimmed(state.hoverRevealDelay); if (hoverRevealInitialOpacityValue) settings.hoverRevealInitialOpacity = hoverRevealInitialOpacityValue; if (hoverRevealTargetOpacityValue) settings.hoverRevealTargetOpacity = hoverRevealTargetOpacityValue; if (hoverRevealDurationValue) settings.hoverRevealDuration = hoverRevealDurationValue; if (hoverRevealDelayValue) settings.hoverRevealDelay = hoverRevealDelayValue; if (state.hoverRevealPersist) settings.hoverRevealPersist = true; if (state.hoverPersistOnClick) settings.hoverPersistOnClick = true; // Navigation type settings if (isNavigationType) { settings.iconUrl = state.iconUrl.trim(); settings.navLabel = state.navLabel.trim(); settings.navLabelFontFamily = state.navLabelFontFamily.trim(); settings.navType = state.navType; settings.navDisabled = state.navDisabled; settings.targetPageId = state.targetPageId.trim(); settings.targetPageSlug = state.targetPageSlug.trim(); settings.transitionVideoUrl = state.transitionVideoUrl.trim(); settings.transitionReverseMode = state.transitionReverseMode; settings.reverseVideoUrl = state.reverseVideoUrl.trim(); } // Description type settings // Note: Color/fontSize/fontWeight cascade from General Element Styles via CSS inheritance // Only set section-specific values if explicitly configured (allows inheritance) if (isDescriptionType) { settings.iconUrl = state.iconUrl.trim(); settings.descriptionTitle = state.descriptionTitle.trim(); settings.descriptionText = state.descriptionText; // Only include if explicitly set - allows CSS inheritance from wrapper if (state.descriptionTitleFontSize.trim()) { settings.descriptionTitleFontSize = state.descriptionTitleFontSize.trim(); } if (state.descriptionTextFontSize.trim()) { settings.descriptionTextFontSize = state.descriptionTextFontSize.trim(); } if (state.descriptionTitleFontFamily.trim()) { settings.descriptionTitleFontFamily = state.descriptionTitleFontFamily.trim(); } if (state.descriptionTextFontFamily.trim()) { settings.descriptionTextFontFamily = state.descriptionTextFontFamily.trim(); } if (state.descriptionTitleColor.trim()) { settings.descriptionTitleColor = state.descriptionTitleColor.trim(); } if (state.descriptionTextColor.trim()) { settings.descriptionTextColor = state.descriptionTextColor.trim(); } } // Gallery type settings if (isGalleryType) { settings.galleryCards = state.galleryCards.map((card, index) => ({ id: String(card.id || createLocalId()), imageUrl: card.imageUrl.trim(), title: card.title.trim(), description: card.description, })); settings.galleryTitleFontFamily = state.galleryTitleFontFamily.trim(); settings.galleryTextFontFamily = state.galleryTextFontFamily.trim(); } // Carousel type settings if (isCarouselType) { settings.carouselSlides = state.carouselSlides.map((slide) => ({ id: String(slide.id || createLocalId()), imageUrl: slide.imageUrl.trim(), caption: slide.caption.trim(), })); settings.carouselPrevIconUrl = state.carouselPrevIconUrl.trim(); settings.carouselNextIconUrl = state.carouselNextIconUrl.trim(); settings.carouselCaptionFontFamily = state.carouselCaptionFontFamily.trim(); } // Media type settings if (isMediaType) { settings.mediaUrl = state.mediaUrl.trim(); settings.mediaAutoplay = state.mediaAutoplay; settings.mediaLoop = state.mediaLoop; settings.mediaMuted = state.mediaMuted; } // Info Panel type settings if (isInfoPanelType) { // Trigger button settings settings.iconUrl = state.iconUrl.trim(); settings.infoPanelTriggerLabel = state.infoPanelTriggerLabel.trim(); settings.infoPanelTriggerFontFamily = state.infoPanelTriggerFontFamily.trim(); settings.infoPanelDisabled = state.infoPanelDisabled; // Panel content settings.panelTitle = state.panelTitle.trim(); settings.panelText = state.panelText; // Panel position & styling settings.panelXPercent = clampPercent(state.panelXPercent); settings.panelYPercent = clampPercent(state.panelYPercent); const panelWidthValue = toOptionalTrimmed(state.panelWidth); const panelHeightValue = toOptionalTrimmed(state.panelHeight); if (panelWidthValue) settings.panelWidth = panelWidthValue; if (panelHeightValue) settings.panelHeight = panelHeightValue; const panelBgColorValue = toOptionalTrimmed(state.panelBackgroundColor); const panelBorderRadiusValue = toOptionalTrimmed(state.panelBorderRadius); const panelPaddingValue = toOptionalTrimmed(state.panelPadding); const panelBackdropBlurValue = toOptionalTrimmed(state.panelBackdropBlur); if (panelBgColorValue) settings.panelBackgroundColor = panelBgColorValue; if (panelBorderRadiusValue) settings.panelBorderRadius = panelBorderRadiusValue; if (panelPaddingValue) settings.panelPadding = panelPaddingValue; if (panelBackdropBlurValue) settings.panelBackdropBlur = panelBackdropBlurValue; // Panel text styling const panelTitleColorValue = toOptionalTrimmed(state.panelTitleColor); const panelTitleFontSizeValue = toOptionalTrimmed( state.panelTitleFontSize, ); const panelTitleFontFamilyValue = toOptionalTrimmed( state.panelTitleFontFamily, ); const panelTextColorValue = toOptionalTrimmed(state.panelTextColor); const panelTextFontSizeValue = toOptionalTrimmed(state.panelTextFontSize); const panelTextFontFamilyValue = toOptionalTrimmed( state.panelTextFontFamily, ); const panelOverlayColorValue = toOptionalTrimmed(state.panelOverlayColor); if (panelTitleColorValue) settings.panelTitleColor = panelTitleColorValue; if (panelTitleFontSizeValue) settings.panelTitleFontSize = panelTitleFontSizeValue; if (panelTitleFontFamilyValue) settings.panelTitleFontFamily = panelTitleFontFamilyValue; if (panelTextColorValue) settings.panelTextColor = panelTextColorValue; if (panelTextFontSizeValue) settings.panelTextFontSize = panelTextFontSizeValue; if (panelTextFontFamilyValue) settings.panelTextFontFamily = panelTextFontFamilyValue; if (panelOverlayColorValue) settings.panelOverlayColor = panelOverlayColorValue; // Detail panel position & styling settings.detailXPercent = clampPercent(state.detailXPercent); settings.detailYPercent = clampPercent(state.detailYPercent); const detailWidthValue = toOptionalTrimmed(state.detailWidth); const detailHeightValue = toOptionalTrimmed(state.detailHeight); if (detailWidthValue) settings.detailWidth = detailWidthValue; if (detailHeightValue) settings.detailHeight = detailHeightValue; const detailBgColorValue = toOptionalTrimmed(state.detailBackgroundColor); const detailBorderRadiusValue = toOptionalTrimmed( state.detailBorderRadius, ); const detailPaddingValue = toOptionalTrimmed(state.detailPadding); if (detailBgColorValue) settings.detailBackgroundColor = detailBgColorValue; if (detailBorderRadiusValue) settings.detailBorderRadius = detailBorderRadiusValue; if (detailPaddingValue) settings.detailPadding = detailPaddingValue; const detailCaptionFontFamilyValue = toOptionalTrimmed( state.detailCaptionFontFamily, ); if (detailCaptionFontFamilyValue) settings.detailCaptionFontFamily = detailCaptionFontFamilyValue; // Note: detailOverlayColor removed - parent InfoPanelOverlay provides backdrop // Header Section (like Gallery) const headerImageUrlValue = toOptionalTrimmed( state.infoPanelHeaderImageUrl, ); const headerTextValue = toOptionalTrimmed(state.infoPanelHeaderText); if (headerImageUrlValue) settings.infoPanelHeaderImageUrl = headerImageUrlValue; if (headerTextValue) settings.infoPanelHeaderText = headerTextValue; const headerBgColorValue = toOptionalTrimmed( state.infoPanelHeaderBackgroundColor, ); const headerColorValue = toOptionalTrimmed(state.infoPanelHeaderColor); const headerFontFamilyValue = toOptionalTrimmed( state.infoPanelHeaderFontFamily, ); const headerFontSizeValue = toOptionalTrimmed( state.infoPanelHeaderFontSize, ); const headerFontWeightValue = toOptionalTrimmed( state.infoPanelHeaderFontWeight, ); const headerPaddingValue = toOptionalTrimmed( state.infoPanelHeaderPadding, ); const headerBorderRadiusValue = toOptionalTrimmed( state.infoPanelHeaderBorderRadius, ); const headerTextAlignValue = toOptionalTrimmed( state.infoPanelHeaderTextAlign, ); const headerMinHeightValue = toOptionalTrimmed( state.infoPanelHeaderMinHeight, ); if (headerBgColorValue) settings.infoPanelHeaderBackgroundColor = headerBgColorValue; if (headerColorValue) settings.infoPanelHeaderColor = headerColorValue; if (headerFontFamilyValue) settings.infoPanelHeaderFontFamily = headerFontFamilyValue; if (headerFontSizeValue) settings.infoPanelHeaderFontSize = headerFontSizeValue; if (headerFontWeightValue) settings.infoPanelHeaderFontWeight = headerFontWeightValue; if (headerPaddingValue) settings.infoPanelHeaderPadding = headerPaddingValue; if (headerBorderRadiusValue) settings.infoPanelHeaderBorderRadius = headerBorderRadiusValue; if (headerTextAlignValue) settings.infoPanelHeaderTextAlign = headerTextAlignValue; if (headerMinHeightValue) settings.infoPanelHeaderMinHeight = headerMinHeightValue; // Title Section enhanced styling const titleBgColorValue = toOptionalTrimmed( state.infoPanelTitleBackgroundColor, ); const titlePaddingValue = toOptionalTrimmed(state.infoPanelTitlePadding); const titleFontWeightValue = toOptionalTrimmed( state.infoPanelTitleFontWeight, ); const titleTextAlignValue = toOptionalTrimmed( state.infoPanelTitleTextAlign, ); if (titleBgColorValue) settings.infoPanelTitleBackgroundColor = titleBgColorValue; if (titlePaddingValue) settings.infoPanelTitlePadding = titlePaddingValue; if (titleFontWeightValue) settings.infoPanelTitleFontWeight = titleFontWeightValue; if (titleTextAlignValue) settings.infoPanelTitleTextAlign = titleTextAlignValue; // Span styling const spanBgColorValue = toOptionalTrimmed( state.infoPanelSpanBackgroundColor, ); const spanColorValue = toOptionalTrimmed(state.infoPanelSpanColor); const spanFontFamilyValue = toOptionalTrimmed( state.infoPanelSpanFontFamily, ); const spanFontSizeValue = toOptionalTrimmed(state.infoPanelSpanFontSize); const spanPaddingValue = toOptionalTrimmed(state.infoPanelSpanPadding); const spanBorderRadiusValue = toOptionalTrimmed( state.infoPanelSpanBorderRadius, ); if (spanBgColorValue) settings.infoPanelSpanBackgroundColor = spanBgColorValue; if (spanColorValue) settings.infoPanelSpanColor = spanColorValue; if (spanFontFamilyValue) settings.infoPanelSpanFontFamily = spanFontFamilyValue; if (spanFontSizeValue) settings.infoPanelSpanFontSize = spanFontSizeValue; if (spanPaddingValue) settings.infoPanelSpanPadding = spanPaddingValue; if (spanBorderRadiusValue) settings.infoPanelSpanBorderRadius = spanBorderRadiusValue; // Card styling const cardBgColorValue = toOptionalTrimmed( state.infoPanelCardBackgroundColor, ); const cardBorderRadiusValue = toOptionalTrimmed( state.infoPanelCardBorderRadius, ); const cardAspectRatioValue = toOptionalTrimmed( state.infoPanelCardAspectRatio, ); const cardMinHeightValue = toOptionalTrimmed( state.infoPanelCardMinHeight, ); if (cardBgColorValue) settings.infoPanelCardBackgroundColor = cardBgColorValue; if (cardBorderRadiusValue) settings.infoPanelCardBorderRadius = cardBorderRadiusValue; if (cardAspectRatioValue) settings.infoPanelCardAspectRatio = cardAspectRatioValue; if (cardMinHeightValue) settings.infoPanelCardMinHeight = cardMinHeightValue; // Card title styling const cardTitleBgColorValue = toOptionalTrimmed( state.infoPanelCardTitleBackgroundColor, ); const cardTitleColorValue = toOptionalTrimmed( state.infoPanelCardTitleColor, ); const cardTitleFontFamilyValue = toOptionalTrimmed( state.infoPanelCardTitleFontFamily, ); const cardTitleFontSizeValue = toOptionalTrimmed( state.infoPanelCardTitleFontSize, ); const cardTitlePaddingValue = toOptionalTrimmed( state.infoPanelCardTitlePadding, ); if (cardTitleBgColorValue) settings.infoPanelCardTitleBackgroundColor = cardTitleBgColorValue; if (cardTitleColorValue) settings.infoPanelCardTitleColor = cardTitleColorValue; if (cardTitleFontFamilyValue) settings.infoPanelCardTitleFontFamily = cardTitleFontFamilyValue; if (cardTitleFontSizeValue) settings.infoPanelCardTitleFontSize = cardTitleFontSizeValue; if (cardTitlePaddingValue) settings.infoPanelCardTitlePadding = cardTitlePaddingValue; // Section instances - always save (contains order and per-section settings) settings.infoPanelSections = state.infoPanelSections; } return settings; }, [ state, isNavigationType, isDescriptionType, isGalleryType, isCarouselType, isMediaType, isInfoPanelType, ]); return { state, setField, setFields, applySettings, getStyleValues, getEffectValues, // Type checks isNavigationType, isDescriptionType, isGalleryType, isCarouselType, isMediaType, isInfoPanelType, // Gallery operations addGalleryCard, removeGalleryCard, updateGalleryCard, // Carousel operations addCarouselSlide, removeCarouselSlide, updateCarouselSlide, // Info panel section ordering operations moveInfoPanelSection, removeInfoPanelSection, addInfoPanelSection, // Build JSON buildSettingsJson, }; } export type { FormState };