Autosave: 20260319-081925
This commit is contained in:
parent
afabb0cce1
commit
ce847e87d6
@ -84,6 +84,28 @@ type CanvasElement = {
|
|||||||
label: string;
|
label: string;
|
||||||
xPercent: number;
|
xPercent: number;
|
||||||
yPercent: number;
|
yPercent: number;
|
||||||
|
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;
|
||||||
appearDelaySec?: number;
|
appearDelaySec?: number;
|
||||||
appearDurationSec?: number | null;
|
appearDurationSec?: number | null;
|
||||||
iconUrl?: string;
|
iconUrl?: string;
|
||||||
@ -193,6 +215,11 @@ const normalizeAppearDurationSec = (value: unknown) => {
|
|||||||
return Number(parsed);
|
return Number(parsed);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getTrimmedCssValue = (value: unknown) => {
|
||||||
|
if (value === null || value === undefined) return '';
|
||||||
|
return String(value).trim();
|
||||||
|
};
|
||||||
|
|
||||||
const createLocalId = () => {
|
const createLocalId = () => {
|
||||||
if (typeof window !== 'undefined' && window.crypto?.randomUUID) {
|
if (typeof window !== 'undefined' && window.crypto?.randomUUID) {
|
||||||
return window.crypto.randomUUID();
|
return window.crypto.randomUUID();
|
||||||
@ -529,12 +556,14 @@ const createDefaultElement = (
|
|||||||
const mergeElementWithDefaults = (
|
const mergeElementWithDefaults = (
|
||||||
element: CanvasElement,
|
element: CanvasElement,
|
||||||
defaults?: Partial<CanvasElement>,
|
defaults?: Partial<CanvasElement>,
|
||||||
|
options?: { preferElementValues?: boolean },
|
||||||
): CanvasElement => {
|
): CanvasElement => {
|
||||||
if (!defaults) return element;
|
if (!defaults) return element;
|
||||||
|
|
||||||
|
const preferElementValues = Boolean(options?.preferElementValues);
|
||||||
const merged: CanvasElement = {
|
const merged: CanvasElement = {
|
||||||
...element,
|
...(preferElementValues ? defaults : element),
|
||||||
...defaults,
|
...(preferElementValues ? element : defaults),
|
||||||
id: element.id,
|
id: element.id,
|
||||||
type: element.type,
|
type: element.type,
|
||||||
};
|
};
|
||||||
@ -545,9 +574,13 @@ const mergeElementWithDefaults = (
|
|||||||
merged.appearDurationSec = normalizeAppearDurationSec(merged.appearDurationSec);
|
merged.appearDurationSec = normalizeAppearDurationSec(merged.appearDurationSec);
|
||||||
|
|
||||||
if (merged.type === 'gallery') {
|
if (merged.type === 'gallery') {
|
||||||
const cards = Array.isArray(defaults.galleryCards)
|
const cards = preferElementValues
|
||||||
? defaults.galleryCards
|
? Array.isArray(element.galleryCards)
|
||||||
: element.galleryCards || [];
|
? element.galleryCards
|
||||||
|
: defaults.galleryCards || []
|
||||||
|
: Array.isArray(defaults.galleryCards)
|
||||||
|
? defaults.galleryCards
|
||||||
|
: element.galleryCards || [];
|
||||||
merged.galleryCards = cards.map((card, cardIndex) => ({
|
merged.galleryCards = cards.map((card, cardIndex) => ({
|
||||||
id: String(card?.id || createLocalId()),
|
id: String(card?.id || createLocalId()),
|
||||||
imageUrl: String(card?.imageUrl || ''),
|
imageUrl: String(card?.imageUrl || ''),
|
||||||
@ -557,9 +590,13 @@ const mergeElementWithDefaults = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (merged.type === 'carousel') {
|
if (merged.type === 'carousel') {
|
||||||
const slides = Array.isArray(defaults.carouselSlides)
|
const slides = preferElementValues
|
||||||
? defaults.carouselSlides
|
? Array.isArray(element.carouselSlides)
|
||||||
: element.carouselSlides || [];
|
? element.carouselSlides
|
||||||
|
: defaults.carouselSlides || []
|
||||||
|
: Array.isArray(defaults.carouselSlides)
|
||||||
|
? defaults.carouselSlides
|
||||||
|
: element.carouselSlides || [];
|
||||||
merged.carouselSlides = slides.map((slide, slideIndex) => ({
|
merged.carouselSlides = slides.map((slide, slideIndex) => ({
|
||||||
id: String(slide?.id || createLocalId()),
|
id: String(slide?.id || createLocalId()),
|
||||||
imageUrl: String(slide?.imageUrl || ''),
|
imageUrl: String(slide?.imageUrl || ''),
|
||||||
@ -582,7 +619,10 @@ const getElementButtonTitle = (element: CanvasElement) => {
|
|||||||
if (element.type === 'tooltip') return element.tooltipTitle ?? '';
|
if (element.type === 'tooltip') return element.tooltipTitle ?? '';
|
||||||
if (element.type === 'description') return element.descriptionTitle ?? '';
|
if (element.type === 'description') return element.descriptionTitle ?? '';
|
||||||
if (element.type === 'navigation_next' || element.type === 'navigation_prev')
|
if (element.type === 'navigation_next' || element.type === 'navigation_prev')
|
||||||
return element.navLabel ?? '';
|
return (
|
||||||
|
element.navLabel?.trim() ||
|
||||||
|
getNavigationButtonLabel(element.type as NavigationElementType)
|
||||||
|
);
|
||||||
if (
|
if (
|
||||||
(element.type === 'video_player' || element.type === 'audio_player') &&
|
(element.type === 'video_player' || element.type === 'audio_player') &&
|
||||||
element.mediaUrl
|
element.mediaUrl
|
||||||
@ -593,6 +633,65 @@ const getElementButtonTitle = (element: CanvasElement) => {
|
|||||||
return element.label;
|
return element.label;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const buildCanvasElementStyle = (
|
||||||
|
element: CanvasElement,
|
||||||
|
): React.CSSProperties => {
|
||||||
|
const style: React.CSSProperties = {};
|
||||||
|
const width = getTrimmedCssValue(element.width);
|
||||||
|
if (width) style.width = width;
|
||||||
|
const height = getTrimmedCssValue(element.height);
|
||||||
|
if (height) style.height = height;
|
||||||
|
const minWidth = getTrimmedCssValue(element.minWidth);
|
||||||
|
if (minWidth) style.minWidth = minWidth;
|
||||||
|
const maxWidth = getTrimmedCssValue(element.maxWidth);
|
||||||
|
if (maxWidth) style.maxWidth = maxWidth;
|
||||||
|
const minHeight = getTrimmedCssValue(element.minHeight);
|
||||||
|
if (minHeight) style.minHeight = minHeight;
|
||||||
|
const maxHeight = getTrimmedCssValue(element.maxHeight);
|
||||||
|
if (maxHeight) style.maxHeight = maxHeight;
|
||||||
|
const margin = getTrimmedCssValue(element.margin);
|
||||||
|
if (margin) style.margin = margin;
|
||||||
|
const padding = getTrimmedCssValue(element.padding);
|
||||||
|
if (padding) style.padding = padding;
|
||||||
|
const gap = getTrimmedCssValue(element.gap);
|
||||||
|
if (gap) style.gap = gap;
|
||||||
|
const fontSize = getTrimmedCssValue(element.fontSize);
|
||||||
|
if (fontSize) style.fontSize = fontSize;
|
||||||
|
const lineHeight = getTrimmedCssValue(element.lineHeight);
|
||||||
|
if (lineHeight) style.lineHeight = lineHeight;
|
||||||
|
const fontWeight = getTrimmedCssValue(element.fontWeight);
|
||||||
|
if (fontWeight) style.fontWeight = fontWeight as React.CSSProperties['fontWeight'];
|
||||||
|
const border = getTrimmedCssValue(element.border);
|
||||||
|
if (border) style.border = border;
|
||||||
|
const borderRadius = getTrimmedCssValue(element.borderRadius);
|
||||||
|
if (borderRadius) style.borderRadius = borderRadius;
|
||||||
|
const opacity = getTrimmedCssValue(element.opacity);
|
||||||
|
if (opacity) {
|
||||||
|
const parsed = Number(opacity);
|
||||||
|
if (Number.isFinite(parsed)) style.opacity = parsed;
|
||||||
|
}
|
||||||
|
const boxShadow = getTrimmedCssValue(element.boxShadow);
|
||||||
|
if (boxShadow) style.boxShadow = boxShadow;
|
||||||
|
const display = getTrimmedCssValue(element.display);
|
||||||
|
if (display) style.display = display;
|
||||||
|
const position = getTrimmedCssValue(element.position);
|
||||||
|
if (position) style.position = position as React.CSSProperties['position'];
|
||||||
|
const justifyContent = getTrimmedCssValue(element.justifyContent);
|
||||||
|
if (justifyContent)
|
||||||
|
style.justifyContent = justifyContent as React.CSSProperties['justifyContent'];
|
||||||
|
const alignItems = getTrimmedCssValue(element.alignItems);
|
||||||
|
if (alignItems)
|
||||||
|
style.alignItems = alignItems as React.CSSProperties['alignItems'];
|
||||||
|
const textAlign = getTrimmedCssValue(element.textAlign);
|
||||||
|
if (textAlign) style.textAlign = textAlign as React.CSSProperties['textAlign'];
|
||||||
|
const zIndex = getTrimmedCssValue(element.zIndex);
|
||||||
|
if (zIndex) {
|
||||||
|
const parsed = Number(zIndex);
|
||||||
|
if (Number.isFinite(parsed)) style.zIndex = parsed;
|
||||||
|
}
|
||||||
|
return style;
|
||||||
|
};
|
||||||
|
|
||||||
const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const canvasRef = useRef<HTMLDivElement>(null);
|
const canvasRef = useRef<HTMLDivElement>(null);
|
||||||
@ -1104,86 +1203,98 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
(item) =>
|
(item) =>
|
||||||
item && item.type && labelByType[item.type as CanvasElementType],
|
item && item.type && labelByType[item.type as CanvasElementType],
|
||||||
)
|
)
|
||||||
.map((item) => ({
|
.map((item) => {
|
||||||
...item,
|
const elementType = item.type as CanvasElementType;
|
||||||
id: String(item.id || createLocalId()),
|
const normalizedElement: CanvasElement = {
|
||||||
label: labelByType[item.type as CanvasElementType],
|
...item,
|
||||||
xPercent: clamp(Number(item.xPercent || 0), 0, 100),
|
id: String(item.id || createLocalId()),
|
||||||
yPercent: clamp(Number(item.yPercent || 0), 0, 100),
|
label:
|
||||||
appearDelaySec: normalizeAppearDelaySec(item.appearDelaySec),
|
typeof item.label === 'string' && item.label.trim()
|
||||||
appearDurationSec: normalizeAppearDurationSec(item.appearDurationSec),
|
? item.label
|
||||||
galleryCards: Array.isArray(item.galleryCards)
|
: labelByType[elementType],
|
||||||
? item.galleryCards.map((card: any, index: number) => ({
|
xPercent: clamp(Number(item.xPercent || 0), 0, 100),
|
||||||
id: String(card?.id || createLocalId()),
|
yPercent: clamp(Number(item.yPercent || 0), 0, 100),
|
||||||
imageUrl: String(card?.imageUrl || ''),
|
appearDelaySec: normalizeAppearDelaySec(item.appearDelaySec),
|
||||||
title: String(card?.title || `Card ${index + 1}`),
|
appearDurationSec: normalizeAppearDurationSec(item.appearDurationSec),
|
||||||
description: String(card?.description || ''),
|
galleryCards: Array.isArray(item.galleryCards)
|
||||||
}))
|
? item.galleryCards.map((card: any, index: number) => ({
|
||||||
: undefined,
|
id: String(card?.id || createLocalId()),
|
||||||
carouselSlides: Array.isArray(item.carouselSlides)
|
imageUrl: String(card?.imageUrl || ''),
|
||||||
? item.carouselSlides.map((slide: any, index: number) => ({
|
title: String(card?.title || `Card ${index + 1}`),
|
||||||
id: String(slide?.id || createLocalId()),
|
description: String(card?.description || ''),
|
||||||
imageUrl: String(slide?.imageUrl || ''),
|
}))
|
||||||
caption: String(slide?.caption || `Slide ${index + 1}`),
|
: undefined,
|
||||||
}))
|
carouselSlides: Array.isArray(item.carouselSlides)
|
||||||
: undefined,
|
? item.carouselSlides.map((slide: any, index: number) => ({
|
||||||
iconUrl: typeof item.iconUrl === 'string' ? item.iconUrl : '',
|
id: String(slide?.id || createLocalId()),
|
||||||
carouselPrevIconUrl:
|
imageUrl: String(slide?.imageUrl || ''),
|
||||||
typeof item.carouselPrevIconUrl === 'string'
|
caption: String(slide?.caption || `Slide ${index + 1}`),
|
||||||
? item.carouselPrevIconUrl
|
}))
|
||||||
: '',
|
: undefined,
|
||||||
carouselNextIconUrl:
|
iconUrl: typeof item.iconUrl === 'string' ? item.iconUrl : '',
|
||||||
typeof item.carouselNextIconUrl === 'string'
|
carouselPrevIconUrl:
|
||||||
? item.carouselNextIconUrl
|
typeof item.carouselPrevIconUrl === 'string'
|
||||||
: '',
|
? item.carouselPrevIconUrl
|
||||||
tooltipTitle:
|
: '',
|
||||||
typeof item.tooltipTitle === 'string' ? item.tooltipTitle : '',
|
carouselNextIconUrl:
|
||||||
tooltipText:
|
typeof item.carouselNextIconUrl === 'string'
|
||||||
typeof item.tooltipText === 'string' ? item.tooltipText : '',
|
? item.carouselNextIconUrl
|
||||||
descriptionTitle:
|
: '',
|
||||||
typeof item.descriptionTitle === 'string'
|
tooltipTitle:
|
||||||
? item.descriptionTitle
|
typeof item.tooltipTitle === 'string' ? item.tooltipTitle : '',
|
||||||
: '',
|
tooltipText:
|
||||||
descriptionText:
|
typeof item.tooltipText === 'string' ? item.tooltipText : '',
|
||||||
typeof item.descriptionText === 'string'
|
descriptionTitle:
|
||||||
? item.descriptionText
|
typeof item.descriptionTitle === 'string'
|
||||||
: '',
|
? item.descriptionTitle
|
||||||
navLabel: typeof item.navLabel === 'string' ? item.navLabel : '',
|
: '',
|
||||||
navType:
|
descriptionText:
|
||||||
item.navType === 'back' || item.navType === 'forward'
|
typeof item.descriptionText === 'string'
|
||||||
? item.navType
|
? item.descriptionText
|
||||||
: isNavigationElementType(item.type as CanvasElementType)
|
: '',
|
||||||
? getNavigationButtonKind(item.type as NavigationElementType)
|
navLabel: typeof item.navLabel === 'string' ? item.navLabel : '',
|
||||||
: undefined,
|
navType:
|
||||||
targetPageId:
|
item.navType === 'back' || item.navType === 'forward'
|
||||||
typeof item.targetPageId === 'string' ? item.targetPageId : '',
|
? item.navType
|
||||||
transitionVideoUrl:
|
: isNavigationElementType(elementType)
|
||||||
typeof item.transitionVideoUrl === 'string'
|
? getNavigationButtonKind(elementType as NavigationElementType)
|
||||||
? item.transitionVideoUrl
|
: undefined,
|
||||||
: '',
|
targetPageId:
|
||||||
transitionReverseMode:
|
typeof item.targetPageId === 'string' ? item.targetPageId : '',
|
||||||
item.transitionReverseMode === 'separate_video'
|
transitionVideoUrl:
|
||||||
? 'separate_video'
|
typeof item.transitionVideoUrl === 'string'
|
||||||
: ('auto_reverse' as const),
|
? item.transitionVideoUrl
|
||||||
reverseVideoUrl:
|
: '',
|
||||||
typeof item.reverseVideoUrl === 'string'
|
transitionReverseMode:
|
||||||
? item.reverseVideoUrl
|
item.transitionReverseMode === 'separate_video'
|
||||||
: '',
|
? 'separate_video'
|
||||||
transitionDurationSec: item.transitionDurationSec
|
: ('auto_reverse' as const),
|
||||||
? Number(item.transitionDurationSec)
|
reverseVideoUrl:
|
||||||
: undefined,
|
typeof item.reverseVideoUrl === 'string'
|
||||||
mediaUrl: typeof item.mediaUrl === 'string' ? item.mediaUrl : '',
|
? item.reverseVideoUrl
|
||||||
mediaAutoplay:
|
: '',
|
||||||
typeof item.mediaAutoplay === 'boolean'
|
transitionDurationSec: item.transitionDurationSec
|
||||||
? item.mediaAutoplay
|
? Number(item.transitionDurationSec)
|
||||||
: true,
|
: undefined,
|
||||||
mediaLoop:
|
mediaUrl: typeof item.mediaUrl === 'string' ? item.mediaUrl : '',
|
||||||
typeof item.mediaLoop === 'boolean' ? item.mediaLoop : true,
|
mediaAutoplay:
|
||||||
mediaMuted:
|
typeof item.mediaAutoplay === 'boolean'
|
||||||
typeof item.mediaMuted === 'boolean'
|
? item.mediaAutoplay
|
||||||
? item.mediaMuted
|
: true,
|
||||||
: item.type === 'video_player',
|
mediaLoop:
|
||||||
}))
|
typeof item.mediaLoop === 'boolean' ? item.mediaLoop : true,
|
||||||
|
mediaMuted:
|
||||||
|
typeof item.mediaMuted === 'boolean'
|
||||||
|
? item.mediaMuted
|
||||||
|
: item.type === 'video_player',
|
||||||
|
};
|
||||||
|
|
||||||
|
return mergeElementWithDefaults(
|
||||||
|
normalizedElement,
|
||||||
|
uiElementDefaultsByType[elementType],
|
||||||
|
{ preferElementValues: true },
|
||||||
|
);
|
||||||
|
})
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
setElements(normalizedElements);
|
setElements(normalizedElements);
|
||||||
@ -1203,7 +1314,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
setBackgroundImageUrl(activePage.background_image_url || '');
|
setBackgroundImageUrl(activePage.background_image_url || '');
|
||||||
setBackgroundVideoUrl(activePage.background_video_url || '');
|
setBackgroundVideoUrl(activePage.background_video_url || '');
|
||||||
setBackgroundAudioUrl(activePage.background_audio_url || '');
|
setBackgroundAudioUrl(activePage.background_audio_url || '');
|
||||||
}, [activePage, elementIdFromRoute]);
|
}, [activePage, elementIdFromRoute, uiElementDefaultsByType]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (allowedNavigationTypes.length !== 1) return;
|
if (allowedNavigationTypes.length !== 1) return;
|
||||||
@ -1697,6 +1808,8 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
element.type === 'navigation_next' ||
|
element.type === 'navigation_next' ||
|
||||||
element.type === 'navigation_prev'
|
element.type === 'navigation_prev'
|
||||||
) {
|
) {
|
||||||
|
const fallbackNavLabel = getNavigationButtonLabel(element.type);
|
||||||
|
const navigationLabel = element.navLabel?.trim() || fallbackNavLabel;
|
||||||
if (element.iconUrl) {
|
if (element.iconUrl) {
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
@ -1714,7 +1827,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
return (
|
return (
|
||||||
<div className='flex flex-col items-start gap-1'>
|
<div className='flex flex-col items-start gap-1'>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<span>{element.navLabel}</span>
|
<span>{navigationLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
{targetPageName ? (
|
{targetPageName ? (
|
||||||
<span className='text-[10px] text-gray-500'>
|
<span className='text-[10px] text-gray-500'>
|
||||||
@ -2189,6 +2302,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
: 'border-blue-200 bg-white/95'
|
: 'border-blue-200 bg-white/95'
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
|
...buildCanvasElementStyle(element),
|
||||||
left: `${element.xPercent}%`,
|
left: `${element.xPercent}%`,
|
||||||
top: `${element.yPercent}%`,
|
top: `${element.yPercent}%`,
|
||||||
transform: 'translate(-50%, -50%)',
|
transform: 'translate(-50%, -50%)',
|
||||||
|
|||||||
@ -47,6 +47,28 @@ type ConstructorElement = {
|
|||||||
label?: string;
|
label?: string;
|
||||||
xPercent?: number;
|
xPercent?: number;
|
||||||
yPercent?: number;
|
yPercent?: number;
|
||||||
|
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;
|
||||||
appearDelaySec?: number;
|
appearDelaySec?: number;
|
||||||
appearDurationSec?: number | null;
|
appearDurationSec?: number | null;
|
||||||
iconUrl?: string;
|
iconUrl?: string;
|
||||||
@ -113,6 +135,31 @@ const parseNullableNumber = (value: string) => {
|
|||||||
return parsed;
|
return parsed;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const extractNumericValue = (value: unknown) => {
|
||||||
|
const normalized = String(value ?? '').trim();
|
||||||
|
if (!normalized) return '';
|
||||||
|
const matched = normalized.match(/-?\d*\.?\d+/);
|
||||||
|
return matched ? matched[0] : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeNumberString = (value: string) => {
|
||||||
|
const normalized = String(value || '').trim();
|
||||||
|
if (!normalized) return '';
|
||||||
|
const parsed = Number(normalized);
|
||||||
|
if (!Number.isFinite(parsed)) return '';
|
||||||
|
return String(parsed);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toOptionalTrimmed = (value: string) => {
|
||||||
|
const normalized = String(value || '').trim();
|
||||||
|
return normalized ? normalized : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toUnitValue = (value: string, unit: 'vw' | 'vh' | 'px') => {
|
||||||
|
const normalized = normalizeNumberString(value);
|
||||||
|
return normalized ? `${normalized}${unit}` : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
const createLocalId = () => {
|
const createLocalId = () => {
|
||||||
if (typeof window !== 'undefined' && window.crypto?.randomUUID) {
|
if (typeof window !== 'undefined' && window.crypto?.randomUUID) {
|
||||||
return window.crypto.randomUUID();
|
return window.crypto.randomUUID();
|
||||||
@ -141,6 +188,28 @@ const PageElementsProjectEditPage = () => {
|
|||||||
const [label, setLabel] = useState('');
|
const [label, setLabel] = useState('');
|
||||||
const [xPercent, setXPercent] = useState('0');
|
const [xPercent, setXPercent] = useState('0');
|
||||||
const [yPercent, setYPercent] = useState('0');
|
const [yPercent, setYPercent] = useState('0');
|
||||||
|
const [width, setWidth] = useState('');
|
||||||
|
const [height, setHeight] = useState('');
|
||||||
|
const [minWidth, setMinWidth] = useState('');
|
||||||
|
const [maxWidth, setMaxWidth] = useState('');
|
||||||
|
const [minHeight, setMinHeight] = useState('');
|
||||||
|
const [maxHeight, setMaxHeight] = useState('');
|
||||||
|
const [margin, setMargin] = useState('');
|
||||||
|
const [padding, setPadding] = useState('');
|
||||||
|
const [gap, setGap] = useState('');
|
||||||
|
const [fontSize, setFontSize] = useState('');
|
||||||
|
const [lineHeight, setLineHeight] = useState('');
|
||||||
|
const [fontWeight, setFontWeight] = useState('');
|
||||||
|
const [border, setBorder] = useState('');
|
||||||
|
const [borderRadius, setBorderRadius] = useState('');
|
||||||
|
const [opacity, setOpacity] = useState('');
|
||||||
|
const [boxShadow, setBoxShadow] = useState('');
|
||||||
|
const [display, setDisplay] = useState('');
|
||||||
|
const [position, setPosition] = useState('');
|
||||||
|
const [justifyContent, setJustifyContent] = useState('');
|
||||||
|
const [alignItems, setAlignItems] = useState('');
|
||||||
|
const [textAlign, setTextAlign] = useState('');
|
||||||
|
const [zIndex, setZIndex] = useState('');
|
||||||
const [appearDelaySec, setAppearDelaySec] = useState('0');
|
const [appearDelaySec, setAppearDelaySec] = useState('0');
|
||||||
const [appearDurationSec, setAppearDurationSec] = useState('');
|
const [appearDurationSec, setAppearDurationSec] = useState('');
|
||||||
const [iconUrl, setIconUrl] = useState('');
|
const [iconUrl, setIconUrl] = useState('');
|
||||||
@ -168,6 +237,28 @@ const PageElementsProjectEditPage = () => {
|
|||||||
setLabel(String(item.label || ''));
|
setLabel(String(item.label || ''));
|
||||||
setXPercent(String(item.xPercent ?? 0));
|
setXPercent(String(item.xPercent ?? 0));
|
||||||
setYPercent(String(item.yPercent ?? 0));
|
setYPercent(String(item.yPercent ?? 0));
|
||||||
|
setWidth(extractNumericValue(item.width));
|
||||||
|
setHeight(extractNumericValue(item.height));
|
||||||
|
setMinWidth(extractNumericValue(item.minWidth));
|
||||||
|
setMaxWidth(extractNumericValue(item.maxWidth));
|
||||||
|
setMinHeight(extractNumericValue(item.minHeight));
|
||||||
|
setMaxHeight(extractNumericValue(item.maxHeight));
|
||||||
|
setMargin(String(item.margin || ''));
|
||||||
|
setPadding(String(item.padding || ''));
|
||||||
|
setGap(String(item.gap || ''));
|
||||||
|
setFontSize(String(item.fontSize || ''));
|
||||||
|
setLineHeight(String(item.lineHeight || ''));
|
||||||
|
setFontWeight(String(item.fontWeight || ''));
|
||||||
|
setBorder(extractNumericValue(item.border));
|
||||||
|
setBorderRadius(extractNumericValue(item.borderRadius));
|
||||||
|
setOpacity(item.opacity === '0' || item.opacity === 0 ? '0' : String(item.opacity || ''));
|
||||||
|
setBoxShadow(String(item.boxShadow || ''));
|
||||||
|
setDisplay(String(item.display || ''));
|
||||||
|
setPosition(String(item.position || ''));
|
||||||
|
setJustifyContent(String(item.justifyContent || ''));
|
||||||
|
setAlignItems(String(item.alignItems || ''));
|
||||||
|
setTextAlign(String(item.textAlign || ''));
|
||||||
|
setZIndex(String(item.zIndex || ''));
|
||||||
setAppearDelaySec(String(item.appearDelaySec ?? 0));
|
setAppearDelaySec(String(item.appearDelaySec ?? 0));
|
||||||
setAppearDurationSec(item.appearDurationSec === null || item.appearDurationSec === undefined ? '' : String(item.appearDurationSec));
|
setAppearDurationSec(item.appearDurationSec === null || item.appearDurationSec === undefined ? '' : String(item.appearDurationSec));
|
||||||
setIconUrl(String(item.iconUrl || ''));
|
setIconUrl(String(item.iconUrl || ''));
|
||||||
@ -316,15 +407,62 @@ const PageElementsProjectEditPage = () => {
|
|||||||
setSuccessMessage('');
|
setSuccessMessage('');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const borderWidthValue = toUnitValue(border, 'px');
|
||||||
const nextElement: ConstructorElement = {
|
const nextElement: ConstructorElement = {
|
||||||
...element,
|
...element,
|
||||||
label: label.trim(),
|
label: label.trim(),
|
||||||
xPercent: clampPercent(xPercent),
|
xPercent: clampPercent(xPercent),
|
||||||
yPercent: clampPercent(yPercent),
|
yPercent: clampPercent(yPercent),
|
||||||
|
border: borderWidthValue ? `${borderWidthValue} solid currentColor` : 'none',
|
||||||
|
borderRadius: toUnitValue(borderRadius, 'px') || '0px',
|
||||||
appearDelaySec: Number(appearDelaySec) >= 0 ? Number(appearDelaySec) : 0,
|
appearDelaySec: Number(appearDelaySec) >= 0 ? Number(appearDelaySec) : 0,
|
||||||
appearDurationSec: parseNullableNumber(appearDurationSec),
|
appearDurationSec: parseNullableNumber(appearDurationSec),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const widthValue = toUnitValue(width, 'vw');
|
||||||
|
const heightValue = toUnitValue(height, 'vh');
|
||||||
|
const minWidthValue = toUnitValue(minWidth, 'vw');
|
||||||
|
const maxWidthValue = toUnitValue(maxWidth, 'vw');
|
||||||
|
const minHeightValue = toUnitValue(minHeight, 'vh');
|
||||||
|
const maxHeightValue = toUnitValue(maxHeight, 'vh');
|
||||||
|
|
||||||
|
if (widthValue) nextElement.width = widthValue; else delete nextElement.width;
|
||||||
|
if (heightValue) nextElement.height = heightValue; else delete nextElement.height;
|
||||||
|
if (minWidthValue) nextElement.minWidth = minWidthValue; else delete nextElement.minWidth;
|
||||||
|
if (maxWidthValue) nextElement.maxWidth = maxWidthValue; else delete nextElement.maxWidth;
|
||||||
|
if (minHeightValue) nextElement.minHeight = minHeightValue; else delete nextElement.minHeight;
|
||||||
|
if (maxHeightValue) nextElement.maxHeight = maxHeightValue; else delete nextElement.maxHeight;
|
||||||
|
|
||||||
|
const marginValue = toOptionalTrimmed(margin);
|
||||||
|
const paddingValue = toOptionalTrimmed(padding);
|
||||||
|
const gapValue = toOptionalTrimmed(gap);
|
||||||
|
const fontSizeValue = toOptionalTrimmed(fontSize);
|
||||||
|
const lineHeightValue = toOptionalTrimmed(lineHeight);
|
||||||
|
const fontWeightValue = toOptionalTrimmed(fontWeight);
|
||||||
|
const opacityValue = toOptionalTrimmed(opacity);
|
||||||
|
const boxShadowValue = toOptionalTrimmed(boxShadow);
|
||||||
|
const displayValue = toOptionalTrimmed(display);
|
||||||
|
const positionValue = toOptionalTrimmed(position);
|
||||||
|
const justifyContentValue = toOptionalTrimmed(justifyContent);
|
||||||
|
const alignItemsValue = toOptionalTrimmed(alignItems);
|
||||||
|
const textAlignValue = toOptionalTrimmed(textAlign);
|
||||||
|
const zIndexValue = toOptionalTrimmed(zIndex);
|
||||||
|
|
||||||
|
if (marginValue) nextElement.margin = marginValue; else delete nextElement.margin;
|
||||||
|
if (paddingValue) nextElement.padding = paddingValue; else delete nextElement.padding;
|
||||||
|
if (gapValue) nextElement.gap = gapValue; else delete nextElement.gap;
|
||||||
|
if (fontSizeValue) nextElement.fontSize = fontSizeValue; else delete nextElement.fontSize;
|
||||||
|
if (lineHeightValue) nextElement.lineHeight = lineHeightValue; else delete nextElement.lineHeight;
|
||||||
|
if (fontWeightValue) nextElement.fontWeight = fontWeightValue; else delete nextElement.fontWeight;
|
||||||
|
if (opacityValue) nextElement.opacity = opacityValue; else delete nextElement.opacity;
|
||||||
|
if (boxShadowValue) nextElement.boxShadow = boxShadowValue; else delete nextElement.boxShadow;
|
||||||
|
if (displayValue) nextElement.display = displayValue; else delete nextElement.display;
|
||||||
|
if (positionValue) nextElement.position = positionValue; else delete nextElement.position;
|
||||||
|
if (justifyContentValue) nextElement.justifyContent = justifyContentValue; else delete nextElement.justifyContent;
|
||||||
|
if (alignItemsValue) nextElement.alignItems = alignItemsValue; else delete nextElement.alignItems;
|
||||||
|
if (textAlignValue) nextElement.textAlign = textAlignValue; else delete nextElement.textAlign;
|
||||||
|
if (zIndexValue) nextElement.zIndex = zIndexValue; else delete nextElement.zIndex;
|
||||||
|
|
||||||
if (isNavigationType) {
|
if (isNavigationType) {
|
||||||
nextElement.iconUrl = iconUrl.trim();
|
nextElement.iconUrl = iconUrl.trim();
|
||||||
nextElement.navLabel = navLabel.trim();
|
nextElement.navLabel = navLabel.trim();
|
||||||
@ -515,6 +653,89 @@ const PageElementsProjectEditPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
|
|
||||||
|
<CardBox className='mb-4'>
|
||||||
|
<h3 className='mb-3 text-sm font-semibold'>View & stylization</h3>
|
||||||
|
<p className='mb-3 text-xs text-gray-500'>
|
||||||
|
Fill numbers only: width is saved as vw, height as vh, border and radius as px.
|
||||||
|
</p>
|
||||||
|
<div className='grid gap-4 md:grid-cols-2'>
|
||||||
|
<FormField label='Width (vw)'>
|
||||||
|
<input type='number' step='0.1' min='0' value={width} onChange={(event) => setWidth(event.target.value)} disabled={!hasUpdatePermission} placeholder='e.g. 24' />
|
||||||
|
</FormField>
|
||||||
|
<FormField label='Height (vh)'>
|
||||||
|
<input type='number' step='0.1' min='0' value={height} onChange={(event) => setHeight(event.target.value)} disabled={!hasUpdatePermission} placeholder='e.g. 8' />
|
||||||
|
</FormField>
|
||||||
|
<FormField label='Min width (vw)'><input type='number' step='0.1' min='0' value={minWidth} onChange={(event) => setMinWidth(event.target.value)} disabled={!hasUpdatePermission} /></FormField>
|
||||||
|
<FormField label='Max width (vw)'><input type='number' step='0.1' min='0' value={maxWidth} onChange={(event) => setMaxWidth(event.target.value)} disabled={!hasUpdatePermission} /></FormField>
|
||||||
|
<FormField label='Min height (vh)'><input type='number' step='0.1' min='0' value={minHeight} onChange={(event) => setMinHeight(event.target.value)} disabled={!hasUpdatePermission} /></FormField>
|
||||||
|
<FormField label='Max height (vh)'><input type='number' step='0.1' min='0' value={maxHeight} onChange={(event) => setMaxHeight(event.target.value)} disabled={!hasUpdatePermission} /></FormField>
|
||||||
|
<FormField label='Margin'><input value={margin} onChange={(event) => setMargin(event.target.value)} disabled={!hasUpdatePermission} placeholder='e.g. 0 auto / 0.5rem' /></FormField>
|
||||||
|
<FormField label='Padding'><input value={padding} onChange={(event) => setPadding(event.target.value)} disabled={!hasUpdatePermission} placeholder='e.g. 0.5rem 0.75rem' /></FormField>
|
||||||
|
<FormField label='Gap'><input value={gap} onChange={(event) => setGap(event.target.value)} disabled={!hasUpdatePermission} /></FormField>
|
||||||
|
<FormField label='Font size'><input value={fontSize} onChange={(event) => setFontSize(event.target.value)} disabled={!hasUpdatePermission} placeholder='e.g. 0.875rem / clamp(...)' /></FormField>
|
||||||
|
<FormField label='Line height'><input value={lineHeight} onChange={(event) => setLineHeight(event.target.value)} disabled={!hasUpdatePermission} /></FormField>
|
||||||
|
<FormField label='Font weight'><input value={fontWeight} onChange={(event) => setFontWeight(event.target.value)} disabled={!hasUpdatePermission} placeholder='e.g. 500 / bold' /></FormField>
|
||||||
|
<FormField label='Border width (px)'><input type='number' step='1' min='0' value={border} onChange={(event) => setBorder(event.target.value)} disabled={!hasUpdatePermission} placeholder='empty = none' /></FormField>
|
||||||
|
<FormField label='Border radius (px)'><input type='number' step='1' min='0' value={borderRadius} onChange={(event) => setBorderRadius(event.target.value)} disabled={!hasUpdatePermission} placeholder='empty = 0' /></FormField>
|
||||||
|
<FormField label='Opacity'><input value={opacity} onChange={(event) => setOpacity(event.target.value)} disabled={!hasUpdatePermission} placeholder='0..1' /></FormField>
|
||||||
|
<FormField label='Shadow'><input value={boxShadow} onChange={(event) => setBoxShadow(event.target.value)} disabled={!hasUpdatePermission} placeholder='e.g. 0 4px 12px rgba(...)' /></FormField>
|
||||||
|
<FormField label='Display'>
|
||||||
|
<select value={display} onChange={(event) => setDisplay(event.target.value)} disabled={!hasUpdatePermission}>
|
||||||
|
<option value=''>Not set</option>
|
||||||
|
<option value='block'>block</option>
|
||||||
|
<option value='inline-block'>inline-block</option>
|
||||||
|
<option value='flex'>flex</option>
|
||||||
|
<option value='inline-flex'>inline-flex</option>
|
||||||
|
<option value='grid'>grid</option>
|
||||||
|
<option value='none'>none</option>
|
||||||
|
</select>
|
||||||
|
</FormField>
|
||||||
|
<FormField label='Position'>
|
||||||
|
<select value={position} onChange={(event) => setPosition(event.target.value)} disabled={!hasUpdatePermission}>
|
||||||
|
<option value=''>Not set</option>
|
||||||
|
<option value='static'>static</option>
|
||||||
|
<option value='relative'>relative</option>
|
||||||
|
<option value='absolute'>absolute</option>
|
||||||
|
<option value='fixed'>fixed</option>
|
||||||
|
<option value='sticky'>sticky</option>
|
||||||
|
</select>
|
||||||
|
</FormField>
|
||||||
|
<FormField label='Justify content'>
|
||||||
|
<select value={justifyContent} onChange={(event) => setJustifyContent(event.target.value)} disabled={!hasUpdatePermission}>
|
||||||
|
<option value=''>Not set</option>
|
||||||
|
<option value='flex-start'>flex-start</option>
|
||||||
|
<option value='center'>center</option>
|
||||||
|
<option value='flex-end'>flex-end</option>
|
||||||
|
<option value='space-between'>space-between</option>
|
||||||
|
<option value='space-around'>space-around</option>
|
||||||
|
<option value='space-evenly'>space-evenly</option>
|
||||||
|
</select>
|
||||||
|
</FormField>
|
||||||
|
<FormField label='Align items'>
|
||||||
|
<select value={alignItems} onChange={(event) => setAlignItems(event.target.value)} disabled={!hasUpdatePermission}>
|
||||||
|
<option value=''>Not set</option>
|
||||||
|
<option value='stretch'>stretch</option>
|
||||||
|
<option value='flex-start'>flex-start</option>
|
||||||
|
<option value='center'>center</option>
|
||||||
|
<option value='flex-end'>flex-end</option>
|
||||||
|
<option value='baseline'>baseline</option>
|
||||||
|
</select>
|
||||||
|
</FormField>
|
||||||
|
<FormField label='Text align'>
|
||||||
|
<select value={textAlign} onChange={(event) => setTextAlign(event.target.value)} disabled={!hasUpdatePermission}>
|
||||||
|
<option value=''>Not set</option>
|
||||||
|
<option value='left'>left</option>
|
||||||
|
<option value='center'>center</option>
|
||||||
|
<option value='right'>right</option>
|
||||||
|
<option value='justify'>justify</option>
|
||||||
|
</select>
|
||||||
|
</FormField>
|
||||||
|
<FormField label='z-index'>
|
||||||
|
<input value={zIndex} onChange={(event) => setZIndex(event.target.value)} disabled={!hasUpdatePermission} placeholder='e.g. 1 / 10' />
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
{isNavigationType ? (
|
{isNavigationType ? (
|
||||||
<CardBox className='mb-4'>
|
<CardBox className='mb-4'>
|
||||||
<h3 className='mb-3 text-sm font-semibold'>Navigation</h3>
|
<h3 className='mb-3 text-sm font-semibold'>Navigation</h3>
|
||||||
|
|||||||
@ -77,6 +77,31 @@ const parseNullableNumber = (value: string) => {
|
|||||||
return parsed;
|
return parsed;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const extractNumericValue = (value: unknown) => {
|
||||||
|
const normalized = String(value ?? '').trim();
|
||||||
|
if (!normalized) return '';
|
||||||
|
const matched = normalized.match(/-?\d*\.?\d+/);
|
||||||
|
return matched ? matched[0] : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeNumberString = (value: string) => {
|
||||||
|
const normalized = String(value || '').trim();
|
||||||
|
if (!normalized) return '';
|
||||||
|
const parsed = Number(normalized);
|
||||||
|
if (!Number.isFinite(parsed)) return '';
|
||||||
|
return String(parsed);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toOptionalTrimmed = (value: string) => {
|
||||||
|
const normalized = String(value || '').trim();
|
||||||
|
return normalized ? normalized : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toUnitValue = (value: string, unit: 'vw' | 'vh' | 'px') => {
|
||||||
|
const normalized = normalizeNumberString(value);
|
||||||
|
return normalized ? `${normalized}${unit}` : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
const createLocalId = () => {
|
const createLocalId = () => {
|
||||||
if (typeof window !== 'undefined' && window.crypto?.randomUUID) {
|
if (typeof window !== 'undefined' && window.crypto?.randomUUID) {
|
||||||
return window.crypto.randomUUID();
|
return window.crypto.randomUUID();
|
||||||
@ -101,6 +126,28 @@ const UiElementDetailsPage = () => {
|
|||||||
const [label, setLabel] = useState('');
|
const [label, setLabel] = useState('');
|
||||||
const [xPercent, setXPercent] = useState('0');
|
const [xPercent, setXPercent] = useState('0');
|
||||||
const [yPercent, setYPercent] = useState('0');
|
const [yPercent, setYPercent] = useState('0');
|
||||||
|
const [width, setWidth] = useState('');
|
||||||
|
const [height, setHeight] = useState('');
|
||||||
|
const [minWidth, setMinWidth] = useState('');
|
||||||
|
const [maxWidth, setMaxWidth] = useState('');
|
||||||
|
const [minHeight, setMinHeight] = useState('');
|
||||||
|
const [maxHeight, setMaxHeight] = useState('');
|
||||||
|
const [margin, setMargin] = useState('');
|
||||||
|
const [padding, setPadding] = useState('');
|
||||||
|
const [gap, setGap] = useState('');
|
||||||
|
const [fontSize, setFontSize] = useState('');
|
||||||
|
const [lineHeight, setLineHeight] = useState('');
|
||||||
|
const [fontWeight, setFontWeight] = useState('');
|
||||||
|
const [border, setBorder] = useState('');
|
||||||
|
const [borderRadius, setBorderRadius] = useState('');
|
||||||
|
const [opacity, setOpacity] = useState('');
|
||||||
|
const [boxShadow, setBoxShadow] = useState('');
|
||||||
|
const [display, setDisplay] = useState('');
|
||||||
|
const [position, setPosition] = useState('');
|
||||||
|
const [justifyContent, setJustifyContent] = useState('');
|
||||||
|
const [alignItems, setAlignItems] = useState('');
|
||||||
|
const [textAlign, setTextAlign] = useState('');
|
||||||
|
const [zIndex, setZIndex] = useState('');
|
||||||
const [appearDelaySec, setAppearDelaySec] = useState('0');
|
const [appearDelaySec, setAppearDelaySec] = useState('0');
|
||||||
const [appearDurationSec, setAppearDurationSec] = useState('');
|
const [appearDurationSec, setAppearDurationSec] = useState('');
|
||||||
const [iconUrl, setIconUrl] = useState('');
|
const [iconUrl, setIconUrl] = useState('');
|
||||||
@ -135,6 +182,28 @@ const UiElementDetailsPage = () => {
|
|||||||
setLabel(String(settings.label || ''));
|
setLabel(String(settings.label || ''));
|
||||||
setXPercent(String(settings.xPercent ?? 0));
|
setXPercent(String(settings.xPercent ?? 0));
|
||||||
setYPercent(String(settings.yPercent ?? 0));
|
setYPercent(String(settings.yPercent ?? 0));
|
||||||
|
setWidth(extractNumericValue(settings.width));
|
||||||
|
setHeight(extractNumericValue(settings.height));
|
||||||
|
setMinWidth(extractNumericValue(settings.minWidth));
|
||||||
|
setMaxWidth(extractNumericValue(settings.maxWidth));
|
||||||
|
setMinHeight(extractNumericValue(settings.minHeight));
|
||||||
|
setMaxHeight(extractNumericValue(settings.maxHeight));
|
||||||
|
setMargin(String(settings.margin || ''));
|
||||||
|
setPadding(String(settings.padding || ''));
|
||||||
|
setGap(String(settings.gap || ''));
|
||||||
|
setFontSize(String(settings.fontSize || ''));
|
||||||
|
setLineHeight(String(settings.lineHeight || ''));
|
||||||
|
setFontWeight(String(settings.fontWeight || ''));
|
||||||
|
setBorder(extractNumericValue(settings.border));
|
||||||
|
setBorderRadius(extractNumericValue(settings.borderRadius));
|
||||||
|
setOpacity(settings.opacity === 0 ? '0' : String(settings.opacity || ''));
|
||||||
|
setBoxShadow(String(settings.boxShadow || ''));
|
||||||
|
setDisplay(String(settings.display || ''));
|
||||||
|
setPosition(String(settings.position || ''));
|
||||||
|
setJustifyContent(String(settings.justifyContent || ''));
|
||||||
|
setAlignItems(String(settings.alignItems || ''));
|
||||||
|
setTextAlign(String(settings.textAlign || ''));
|
||||||
|
setZIndex(String(settings.zIndex || ''));
|
||||||
setAppearDelaySec(String(settings.appearDelaySec ?? 0));
|
setAppearDelaySec(String(settings.appearDelaySec ?? 0));
|
||||||
setAppearDurationSec(settings.appearDurationSec === null || settings.appearDurationSec === undefined ? '' : String(settings.appearDurationSec));
|
setAppearDurationSec(settings.appearDurationSec === null || settings.appearDurationSec === undefined ? '' : String(settings.appearDurationSec));
|
||||||
setIconUrl(String(settings.iconUrl || ''));
|
setIconUrl(String(settings.iconUrl || ''));
|
||||||
@ -271,14 +340,61 @@ const UiElementDetailsPage = () => {
|
|||||||
const handleSave = useCallback(async () => {
|
const handleSave = useCallback(async () => {
|
||||||
if (!id || !item) return;
|
if (!id || !item) return;
|
||||||
|
|
||||||
|
const borderWidthValue = toUnitValue(border, 'px');
|
||||||
const defaultSettings: Record<string, any> = {
|
const defaultSettings: Record<string, any> = {
|
||||||
label: label.trim(),
|
label: label.trim(),
|
||||||
xPercent: clampPercent(xPercent),
|
xPercent: clampPercent(xPercent),
|
||||||
yPercent: clampPercent(yPercent),
|
yPercent: clampPercent(yPercent),
|
||||||
|
border: borderWidthValue ? `${borderWidthValue} solid currentColor` : 'none',
|
||||||
|
borderRadius: toUnitValue(borderRadius, 'px') || '0px',
|
||||||
appearDelaySec: Number(appearDelaySec) >= 0 ? Number(appearDelaySec) : 0,
|
appearDelaySec: Number(appearDelaySec) >= 0 ? Number(appearDelaySec) : 0,
|
||||||
appearDurationSec: parseNullableNumber(appearDurationSec),
|
appearDurationSec: parseNullableNumber(appearDurationSec),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const widthValue = toUnitValue(width, 'vw');
|
||||||
|
const heightValue = toUnitValue(height, 'vh');
|
||||||
|
const minWidthValue = toUnitValue(minWidth, 'vw');
|
||||||
|
const maxWidthValue = toUnitValue(maxWidth, 'vw');
|
||||||
|
const minHeightValue = toUnitValue(minHeight, 'vh');
|
||||||
|
const maxHeightValue = toUnitValue(maxHeight, 'vh');
|
||||||
|
|
||||||
|
if (widthValue) defaultSettings.width = widthValue;
|
||||||
|
if (heightValue) defaultSettings.height = heightValue;
|
||||||
|
if (minWidthValue) defaultSettings.minWidth = minWidthValue;
|
||||||
|
if (maxWidthValue) defaultSettings.maxWidth = maxWidthValue;
|
||||||
|
if (minHeightValue) defaultSettings.minHeight = minHeightValue;
|
||||||
|
if (maxHeightValue) defaultSettings.maxHeight = maxHeightValue;
|
||||||
|
|
||||||
|
const marginValue = toOptionalTrimmed(margin);
|
||||||
|
const paddingValue = toOptionalTrimmed(padding);
|
||||||
|
const gapValue = toOptionalTrimmed(gap);
|
||||||
|
const fontSizeValue = toOptionalTrimmed(fontSize);
|
||||||
|
const lineHeightValue = toOptionalTrimmed(lineHeight);
|
||||||
|
const fontWeightValue = toOptionalTrimmed(fontWeight);
|
||||||
|
const opacityValue = toOptionalTrimmed(opacity);
|
||||||
|
const boxShadowValue = toOptionalTrimmed(boxShadow);
|
||||||
|
const displayValue = toOptionalTrimmed(display);
|
||||||
|
const positionValue = toOptionalTrimmed(position);
|
||||||
|
const justifyContentValue = toOptionalTrimmed(justifyContent);
|
||||||
|
const alignItemsValue = toOptionalTrimmed(alignItems);
|
||||||
|
const textAlignValue = toOptionalTrimmed(textAlign);
|
||||||
|
const zIndexValue = toOptionalTrimmed(zIndex);
|
||||||
|
|
||||||
|
if (marginValue) defaultSettings.margin = marginValue;
|
||||||
|
if (paddingValue) defaultSettings.padding = paddingValue;
|
||||||
|
if (gapValue) defaultSettings.gap = gapValue;
|
||||||
|
if (fontSizeValue) defaultSettings.fontSize = fontSizeValue;
|
||||||
|
if (lineHeightValue) defaultSettings.lineHeight = lineHeightValue;
|
||||||
|
if (fontWeightValue) defaultSettings.fontWeight = fontWeightValue;
|
||||||
|
if (opacityValue) defaultSettings.opacity = opacityValue;
|
||||||
|
if (boxShadowValue) defaultSettings.boxShadow = boxShadowValue;
|
||||||
|
if (displayValue) defaultSettings.display = displayValue;
|
||||||
|
if (positionValue) defaultSettings.position = positionValue;
|
||||||
|
if (justifyContentValue) defaultSettings.justifyContent = justifyContentValue;
|
||||||
|
if (alignItemsValue) defaultSettings.alignItems = alignItemsValue;
|
||||||
|
if (textAlignValue) defaultSettings.textAlign = textAlignValue;
|
||||||
|
if (zIndexValue) defaultSettings.zIndex = zIndexValue;
|
||||||
|
|
||||||
if (isNavigationType) {
|
if (isNavigationType) {
|
||||||
defaultSettings.iconUrl = iconUrl.trim();
|
defaultSettings.iconUrl = iconUrl.trim();
|
||||||
defaultSettings.navLabel = navLabel.trim();
|
defaultSettings.navLabel = navLabel.trim();
|
||||||
@ -365,8 +481,13 @@ const UiElementDetailsPage = () => {
|
|||||||
carouselSlides,
|
carouselSlides,
|
||||||
currentElementType,
|
currentElementType,
|
||||||
descriptionText,
|
descriptionText,
|
||||||
|
display,
|
||||||
descriptionTitle,
|
descriptionTitle,
|
||||||
|
fontSize,
|
||||||
|
fontWeight,
|
||||||
|
gap,
|
||||||
galleryCards,
|
galleryCards,
|
||||||
|
height,
|
||||||
iconUrl,
|
iconUrl,
|
||||||
id,
|
id,
|
||||||
isActive,
|
isActive,
|
||||||
@ -377,25 +498,42 @@ const UiElementDetailsPage = () => {
|
|||||||
isNavigationType,
|
isNavigationType,
|
||||||
isTooltipType,
|
isTooltipType,
|
||||||
item,
|
item,
|
||||||
|
justifyContent,
|
||||||
label,
|
label,
|
||||||
|
lineHeight,
|
||||||
loadItem,
|
loadItem,
|
||||||
|
margin,
|
||||||
|
maxHeight,
|
||||||
|
maxWidth,
|
||||||
mediaAutoplay,
|
mediaAutoplay,
|
||||||
mediaLoop,
|
mediaLoop,
|
||||||
mediaMuted,
|
mediaMuted,
|
||||||
mediaUrl,
|
mediaUrl,
|
||||||
|
minHeight,
|
||||||
|
minWidth,
|
||||||
name,
|
name,
|
||||||
navLabel,
|
navLabel,
|
||||||
navType,
|
navType,
|
||||||
|
opacity,
|
||||||
|
padding,
|
||||||
|
position,
|
||||||
reverseVideoUrl,
|
reverseVideoUrl,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
|
textAlign,
|
||||||
targetPageId,
|
targetPageId,
|
||||||
tooltipText,
|
tooltipText,
|
||||||
tooltipTitle,
|
tooltipTitle,
|
||||||
transitionDurationSec,
|
transitionDurationSec,
|
||||||
transitionReverseMode,
|
transitionReverseMode,
|
||||||
transitionVideoUrl,
|
transitionVideoUrl,
|
||||||
|
width,
|
||||||
xPercent,
|
xPercent,
|
||||||
yPercent,
|
yPercent,
|
||||||
|
zIndex,
|
||||||
|
alignItems,
|
||||||
|
border,
|
||||||
|
borderRadius,
|
||||||
|
boxShadow,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -490,6 +628,83 @@ const UiElementDetailsPage = () => {
|
|||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<CardBox className='border border-gray-200 dark:border-dark-700'>
|
||||||
|
<h3 className='mb-3 text-sm font-semibold'>View & stylization</h3>
|
||||||
|
<p className='mb-3 text-xs text-gray-500'>
|
||||||
|
Fill numbers only: width is saved as vw, height as vh, border and radius as px.
|
||||||
|
</p>
|
||||||
|
<div className='grid gap-3 md:grid-cols-2'>
|
||||||
|
<FormField label='Width (vw)'><input type='number' step='0.1' min='0' value={width} onChange={(event) => setWidth(event.target.value)} placeholder='e.g. 24' /></FormField>
|
||||||
|
<FormField label='Height (vh)'><input type='number' step='0.1' min='0' value={height} onChange={(event) => setHeight(event.target.value)} placeholder='e.g. 8' /></FormField>
|
||||||
|
<FormField label='Min width (vw)'><input type='number' step='0.1' min='0' value={minWidth} onChange={(event) => setMinWidth(event.target.value)} /></FormField>
|
||||||
|
<FormField label='Max width (vw)'><input type='number' step='0.1' min='0' value={maxWidth} onChange={(event) => setMaxWidth(event.target.value)} /></FormField>
|
||||||
|
<FormField label='Min height (vh)'><input type='number' step='0.1' min='0' value={minHeight} onChange={(event) => setMinHeight(event.target.value)} /></FormField>
|
||||||
|
<FormField label='Max height (vh)'><input type='number' step='0.1' min='0' value={maxHeight} onChange={(event) => setMaxHeight(event.target.value)} /></FormField>
|
||||||
|
<FormField label='Margin'><input value={margin} onChange={(event) => setMargin(event.target.value)} placeholder='e.g. 0 auto / 0.5rem' /></FormField>
|
||||||
|
<FormField label='Padding'><input value={padding} onChange={(event) => setPadding(event.target.value)} placeholder='e.g. 0.5rem 0.75rem' /></FormField>
|
||||||
|
<FormField label='Gap'><input value={gap} onChange={(event) => setGap(event.target.value)} /></FormField>
|
||||||
|
<FormField label='Font size'><input value={fontSize} onChange={(event) => setFontSize(event.target.value)} placeholder='e.g. 0.875rem / clamp(...)' /></FormField>
|
||||||
|
<FormField label='Line height'><input value={lineHeight} onChange={(event) => setLineHeight(event.target.value)} /></FormField>
|
||||||
|
<FormField label='Font weight'><input value={fontWeight} onChange={(event) => setFontWeight(event.target.value)} placeholder='e.g. 500 / bold' /></FormField>
|
||||||
|
<FormField label='Border width (px)'><input type='number' step='1' min='0' value={border} onChange={(event) => setBorder(event.target.value)} placeholder='empty = none' /></FormField>
|
||||||
|
<FormField label='Border radius (px)'><input type='number' step='1' min='0' value={borderRadius} onChange={(event) => setBorderRadius(event.target.value)} placeholder='empty = 0' /></FormField>
|
||||||
|
<FormField label='Opacity'><input value={opacity} onChange={(event) => setOpacity(event.target.value)} placeholder='0..1' /></FormField>
|
||||||
|
<FormField label='Shadow'><input value={boxShadow} onChange={(event) => setBoxShadow(event.target.value)} placeholder='e.g. 0 4px 12px rgba(...)' /></FormField>
|
||||||
|
<FormField label='Display'>
|
||||||
|
<select value={display} onChange={(event) => setDisplay(event.target.value)}>
|
||||||
|
<option value=''>Not set</option>
|
||||||
|
<option value='block'>block</option>
|
||||||
|
<option value='inline-block'>inline-block</option>
|
||||||
|
<option value='flex'>flex</option>
|
||||||
|
<option value='inline-flex'>inline-flex</option>
|
||||||
|
<option value='grid'>grid</option>
|
||||||
|
<option value='none'>none</option>
|
||||||
|
</select>
|
||||||
|
</FormField>
|
||||||
|
<FormField label='Position'>
|
||||||
|
<select value={position} onChange={(event) => setPosition(event.target.value)}>
|
||||||
|
<option value=''>Not set</option>
|
||||||
|
<option value='static'>static</option>
|
||||||
|
<option value='relative'>relative</option>
|
||||||
|
<option value='absolute'>absolute</option>
|
||||||
|
<option value='fixed'>fixed</option>
|
||||||
|
<option value='sticky'>sticky</option>
|
||||||
|
</select>
|
||||||
|
</FormField>
|
||||||
|
<FormField label='Justify content'>
|
||||||
|
<select value={justifyContent} onChange={(event) => setJustifyContent(event.target.value)}>
|
||||||
|
<option value=''>Not set</option>
|
||||||
|
<option value='flex-start'>flex-start</option>
|
||||||
|
<option value='center'>center</option>
|
||||||
|
<option value='flex-end'>flex-end</option>
|
||||||
|
<option value='space-between'>space-between</option>
|
||||||
|
<option value='space-around'>space-around</option>
|
||||||
|
<option value='space-evenly'>space-evenly</option>
|
||||||
|
</select>
|
||||||
|
</FormField>
|
||||||
|
<FormField label='Align items'>
|
||||||
|
<select value={alignItems} onChange={(event) => setAlignItems(event.target.value)}>
|
||||||
|
<option value=''>Not set</option>
|
||||||
|
<option value='stretch'>stretch</option>
|
||||||
|
<option value='flex-start'>flex-start</option>
|
||||||
|
<option value='center'>center</option>
|
||||||
|
<option value='flex-end'>flex-end</option>
|
||||||
|
<option value='baseline'>baseline</option>
|
||||||
|
</select>
|
||||||
|
</FormField>
|
||||||
|
<FormField label='Text align'>
|
||||||
|
<select value={textAlign} onChange={(event) => setTextAlign(event.target.value)}>
|
||||||
|
<option value=''>Not set</option>
|
||||||
|
<option value='left'>left</option>
|
||||||
|
<option value='center'>center</option>
|
||||||
|
<option value='right'>right</option>
|
||||||
|
<option value='justify'>justify</option>
|
||||||
|
</select>
|
||||||
|
</FormField>
|
||||||
|
<FormField label='z-index'><input value={zIndex} onChange={(event) => setZIndex(event.target.value)} placeholder='e.g. 1 / 10' /></FormField>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
{isNavigationType ? (
|
{isNavigationType ? (
|
||||||
<>
|
<>
|
||||||
<FormField label='Icon URL'>
|
<FormField label='Icon URL'>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user