39948-vm/frontend/src/components/ElementSettings/useElementSettingsForm.ts
2026-04-03 17:50:02 +04:00

761 lines
26 KiB
TypeScript

/**
* 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<FormState>(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<string, unknown>) => {
const settings = parseJsonObject<Record<string, unknown>>(
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<string, unknown>) => ({
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<string, unknown>) => ({
id: String(slide?.id || createLocalId()),
imageUrl: String(slide?.imageUrl ?? ''),
caption: String(slide?.caption ?? ''),
}),
)
: [],
});
},
[],
);
/**
* Update a single field
*/
const setField = useCallback(
<K extends keyof FormState>(field: K, value: FormState[K]) => {
setState((prev) => ({ ...prev, [field]: value }));
},
[],
);
/**
* Update multiple fields at once
*/
const setFields = useCallback((updates: Partial<FormState>) => {
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<string, unknown> => {
const borderWidthValue = toUnitValue(state.border, 'px');
const borderRadiusValue = toUnitValue(state.borderRadius, 'px');
const settings: Record<string, unknown> = {
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 };