/** * 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, } from '../../types/constructor'; import { parseJsonObject } from '../../lib/parseJson'; import { extractNumericValue, clampPercent, parseNullableNumber, toUnitValue, toOptionalTrimmed, } from './types'; import { createLocalId, isNavigationElementType, isTooltipElementType, isDescriptionElementType, isGalleryElementType, isCarouselElementType, isMediaElementType, } 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; // 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; // Tooltip settings tooltipTitle: string; tooltipText: string; tooltipTitleFontFamily: string; tooltipTextFontFamily: 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[]; } 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: '', iconUrl: '', navLabel: '', navLabelFontFamily: '', navType: 'forward', navDisabled: false, targetPageId: '', targetPageSlug: '', transitionVideoUrl: '', transitionReverseMode: 'auto_reverse', reverseVideoUrl: '', tooltipTitle: '', tooltipText: '', tooltipTitleFontFamily: '', tooltipTextFontFamily: '', descriptionTitle: '', descriptionText: '', descriptionTitleFontSize: '', descriptionTextFontSize: '', descriptionTitleFontFamily: '', descriptionTextFontFamily: '', descriptionTitleColor: '', descriptionTextColor: '', mediaUrl: '', mediaAutoplay: false, mediaLoop: false, mediaMuted: false, carouselPrevIconUrl: '', carouselNextIconUrl: '', carouselCaptionFontFamily: '', galleryTitleFontFamily: '', galleryTextFontFamily: '', galleryCards: [], carouselSlides: [], }; 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 isTooltipType = isTooltipElementType(elementType); const isDescriptionType = isDescriptionElementType(elementType); const isGalleryType = isGalleryElementType(elementType); const isCarouselType = isCarouselElementType(elementType); const isMediaType = isMediaElementType(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 || ''), 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 || ''), tooltipTitle: String(settings.tooltipTitle || ''), tooltipText: String(settings.tooltipText || ''), tooltipTitleFontFamily: String(settings.tooltipTitleFontFamily || ''), tooltipTextFontFamily: String(settings.tooltipTextFontFamily || ''), 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 ?? ''), }), ) : [], }); }, [], ); /** * 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, }; }, [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, ), })); }, [], ); /** * 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; // 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(); } // Tooltip type settings if (isTooltipType) { settings.iconUrl = state.iconUrl.trim(); settings.tooltipTitle = state.tooltipTitle.trim(); settings.tooltipText = state.tooltipText; settings.tooltipTitleFontFamily = state.tooltipTitleFontFamily.trim(); settings.tooltipTextFontFamily = state.tooltipTextFontFamily.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; } return settings; }, [ state, isNavigationType, isTooltipType, isDescriptionType, isGalleryType, isCarouselType, isMediaType, ]); return { state, setField, setFields, applySettings, getStyleValues, getEffectValues, // Type checks isNavigationType, isTooltipType, isDescriptionType, isGalleryType, isCarouselType, isMediaType, // Gallery operations addGalleryCard, removeGalleryCard, updateGalleryCard, // Carousel operations addCarouselSlide, removeCarouselSlide, updateCarouselSlide, // Build JSON buildSettingsJson, }; } export type { FormState };