added dimentions settings for gallery header image and cards previews

This commit is contained in:
Dmitri 2026-04-03 10:37:13 +04:00
parent ca3b3725f9
commit c235909c54
28 changed files with 574 additions and 179 deletions

View File

@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" /> /// <reference path="./build/types/routes.d.ts" />
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.

File diff suppressed because one or more lines are too long

View File

@ -48,7 +48,9 @@ export function useProjectSelector({
const response = await axios.get( const response = await axios.get(
'/projects?limit=100&page=0&sort=desc&field=updatedAt', '/projects?limit=100&page=0&sort=desc&field=updatedAt',
); );
const rows = Array.isArray(response?.data?.rows) ? response.data.rows : []; const rows = Array.isArray(response?.data?.rows)
? response.data.rows
: [];
setProjects(rows); setProjects(rows);
} catch (error: unknown) { } catch (error: unknown) {
const errorMessage = const errorMessage =

View File

@ -91,7 +91,10 @@ const CanvasElement: React.FC<CanvasElementProps> = ({
// Add transition for interactive effects (preview mode only) // Add transition for interactive effects (preview mode only)
if (!isEditMode && hasAnyEffects(effectProperties)) { if (!isEditMode && hasAnyEffects(effectProperties)) {
positionStyle = { ...positionStyle, ...buildTransitionStyle(effectProperties) }; positionStyle = {
...positionStyle,
...buildTransitionStyle(effectProperties),
};
} }
// Add appear animation (ALWAYS - for WYSIWYG) // Add appear animation (ALWAYS - for WYSIWYG)

View File

@ -367,7 +367,9 @@ export function ElementEditorPanel({
} }
navType={selectedElement.navType} navType={selectedElement.navType}
navLabel={selectedElement.navLabel || ''} navLabel={selectedElement.navLabel || ''}
navLabelFontFamily={selectedElement.navLabelFontFamily || ''} navLabelFontFamily={
selectedElement.navLabelFontFamily || ''
}
navDisabled={selectedElement.navDisabled || false} navDisabled={selectedElement.navDisabled || false}
iconUrl={selectedElement.iconUrl || ''} iconUrl={selectedElement.iconUrl || ''}
targetPageSlug={selectedElement.targetPageSlug || ''} targetPageSlug={selectedElement.targetPageSlug || ''}
@ -604,11 +606,20 @@ export function ElementEditorPanel({
selectedElement.galleryHeaderBorderRadius || '', selectedElement.galleryHeaderBorderRadius || '',
galleryHeaderBorder: galleryHeaderBorder:
selectedElement.galleryHeaderBorder || '', selectedElement.galleryHeaderBorder || '',
galleryHeaderWidth:
selectedElement.galleryHeaderWidth || '',
galleryHeaderHeight:
selectedElement.galleryHeaderHeight || '',
galleryHeaderMinHeight:
selectedElement.galleryHeaderMinHeight || '',
galleryHeaderMaxHeight:
selectedElement.galleryHeaderMaxHeight || '',
}} }}
onChange={(prop, value) => onChange={(prop, value) =>
onUpdateElement({ [prop]: value || undefined }) onUpdateElement({ [prop]: value || undefined })
} }
showFont showFont
showDimensions
/> />
<GallerySectionStyleInputs <GallerySectionStyleInputs
sectionLabel='Title' sectionLabel='Title'
@ -695,6 +706,10 @@ export function ElementEditorPanel({
selectedElement.galleryCardTitleFontWeight || '', selectedElement.galleryCardTitleFontWeight || '',
galleryCardTitleShadow: galleryCardTitleShadow:
selectedElement.galleryCardTitleShadow || '', selectedElement.galleryCardTitleShadow || '',
galleryCardAspectRatio:
selectedElement.galleryCardAspectRatio || '',
galleryCardMinHeight:
selectedElement.galleryCardMinHeight || '',
}} }}
onChange={(prop, value) => onChange={(prop, value) =>
onUpdateElement({ [prop]: value || undefined }) onUpdateElement({ [prop]: value || undefined })
@ -702,6 +717,7 @@ export function ElementEditorPanel({
showGap showGap
showColumns showColumns
showTitleStyles showTitleStyles
showAspectRatio
/> />
<p className='text-[11px] font-semibold text-gray-700 pt-2'> <p className='text-[11px] font-semibold text-gray-700 pt-2'>
General Element Styles General Element Styles

View File

@ -144,7 +144,11 @@ const CarouselSettingsSection: React.FC<CarouselSettingsSectionProps> = ({
<select <select
value={slide.imageUrl} value={slide.imageUrl}
onChange={(event) => onChange={(event) =>
onUpdateSlide(slide.id, 'imageUrl', event.target.value) onUpdateSlide(
slide.id,
'imageUrl',
event.target.value,
)
} }
> >
<option value=''>Not selected</option> <option value=''>Not selected</option>

View File

@ -89,7 +89,9 @@ const GalleryCarouselSettingsSectionCompact: React.FC<
placeholder='W (rem)' placeholder='W (rem)'
value={prevWidth} value={prevWidth}
onChange={(event) => onChange={(event) =>
onUpdateElement({ galleryCarouselPrevWidth: event.target.value }) onUpdateElement({
galleryCarouselPrevWidth: event.target.value,
})
} }
/> />
<input <input
@ -100,7 +102,9 @@ const GalleryCarouselSettingsSectionCompact: React.FC<
placeholder='H (rem)' placeholder='H (rem)'
value={prevHeight} value={prevHeight}
onChange={(event) => onChange={(event) =>
onUpdateElement({ galleryCarouselPrevHeight: event.target.value }) onUpdateElement({
galleryCarouselPrevHeight: event.target.value,
})
} }
/> />
</div> </div>
@ -136,7 +140,9 @@ const GalleryCarouselSettingsSectionCompact: React.FC<
placeholder='W (rem)' placeholder='W (rem)'
value={nextWidth} value={nextWidth}
onChange={(event) => onChange={(event) =>
onUpdateElement({ galleryCarouselNextWidth: event.target.value }) onUpdateElement({
galleryCarouselNextWidth: event.target.value,
})
} }
/> />
<input <input
@ -147,7 +153,9 @@ const GalleryCarouselSettingsSectionCompact: React.FC<
placeholder='H (rem)' placeholder='H (rem)'
value={nextHeight} value={nextHeight}
onChange={(event) => onChange={(event) =>
onUpdateElement({ galleryCarouselNextHeight: event.target.value }) onUpdateElement({
galleryCarouselNextHeight: event.target.value,
})
} }
/> />
</div> </div>
@ -183,7 +191,9 @@ const GalleryCarouselSettingsSectionCompact: React.FC<
placeholder='W (rem)' placeholder='W (rem)'
value={backWidth} value={backWidth}
onChange={(event) => onChange={(event) =>
onUpdateElement({ galleryCarouselBackWidth: event.target.value }) onUpdateElement({
galleryCarouselBackWidth: event.target.value,
})
} }
/> />
<input <input
@ -194,7 +204,9 @@ const GalleryCarouselSettingsSectionCompact: React.FC<
placeholder='H (rem)' placeholder='H (rem)'
value={backHeight} value={backHeight}
onChange={(event) => onChange={(event) =>
onUpdateElement({ galleryCarouselBackHeight: event.target.value }) onUpdateElement({
galleryCarouselBackHeight: event.target.value,
})
} }
/> />
</div> </div>
@ -210,7 +222,8 @@ const GalleryCarouselSettingsSectionCompact: React.FC<
)} )}
<p className='text-[10px] text-gray-500 mt-1'> <p className='text-[10px] text-gray-500 mt-1'>
Set icon + dimensions for navigation-style buttons. Drag to reposition. Set icon + dimensions for navigation-style buttons. Drag to
reposition.
</p> </p>
</div> </div>
</div> </div>

View File

@ -18,6 +18,8 @@ interface GallerySectionStyleInputsProps {
showGap?: boolean; showGap?: boolean;
showBlur?: boolean; showBlur?: boolean;
showTitleStyles?: boolean; showTitleStyles?: boolean;
showDimensions?: boolean;
showAspectRatio?: boolean;
} }
/** /**
@ -33,6 +35,8 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
showGap = false, showGap = false,
showBlur = false, showBlur = false,
showTitleStyles = false, showTitleStyles = false,
showDimensions = false,
showAspectRatio = false,
}) => { }) => {
return ( return (
<div className='rounded border border-gray-200 p-2 space-y-2'> <div className='rounded border border-gray-200 p-2 space-y-2'>
@ -41,11 +45,15 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
<div className='grid grid-cols-2 gap-2'> <div className='grid grid-cols-2 gap-2'>
{/* Background Color */} {/* Background Color */}
<div> <div>
<label className='mb-1 block text-[10px] text-gray-600'>BG color</label> <label className='mb-1 block text-[10px] text-gray-600'>
BG color
</label>
<input <input
className='w-full rounded border border-gray-300 px-2 py-1 text-xs' className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={values[`${prefix}BackgroundColor`] || ''} value={values[`${prefix}BackgroundColor`] || ''}
onChange={(e) => onChange(`${prefix}BackgroundColor`, e.target.value)} onChange={(e) =>
onChange(`${prefix}BackgroundColor`, e.target.value)
}
placeholder='rgba(0,0,0,0.6)' placeholder='rgba(0,0,0,0.6)'
/> />
</div> </div>
@ -53,7 +61,9 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
{/* Text Color (not for wrapper) */} {/* Text Color (not for wrapper) */}
{prefix !== 'galleryWrapper' && !showTitleStyles && ( {prefix !== 'galleryWrapper' && !showTitleStyles && (
<div> <div>
<label className='mb-1 block text-[10px] text-gray-600'>Text color</label> <label className='mb-1 block text-[10px] text-gray-600'>
Text color
</label>
<input <input
className='w-full rounded border border-gray-300 px-2 py-1 text-xs' className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={values[`${prefix}Color`] || ''} value={values[`${prefix}Color`] || ''}
@ -65,7 +75,9 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
{/* Padding */} {/* Padding */}
<div> <div>
<label className='mb-1 block text-[10px] text-gray-600'>Padding</label> <label className='mb-1 block text-[10px] text-gray-600'>
Padding
</label>
<input <input
className='w-full rounded border border-gray-300 px-2 py-1 text-xs' className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={values[`${prefix}Padding`] || ''} value={values[`${prefix}Padding`] || ''}
@ -116,7 +128,9 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
<input <input
className='w-full rounded border border-gray-300 px-2 py-1 text-xs' className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={values[`${prefix}BackdropBlur`] || ''} value={values[`${prefix}BackdropBlur`] || ''}
onChange={(e) => onChange(`${prefix}BackdropBlur`, e.target.value)} onChange={(e) =>
onChange(`${prefix}BackdropBlur`, e.target.value)
}
placeholder='4px' placeholder='4px'
/> />
</div> </div>
@ -125,14 +139,18 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
{/* Grid Columns (optional) */} {/* Grid Columns (optional) */}
{showColumns && ( {showColumns && (
<div> <div>
<label className='mb-1 block text-[10px] text-gray-600'>Columns</label> <label className='mb-1 block text-[10px] text-gray-600'>
Columns
</label>
<input <input
type='number' type='number'
min='1' min='1'
max='6' max='6'
className='w-full rounded border border-gray-300 px-2 py-1 text-xs' className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={values[`${prefix}Columns`] || ''} value={values[`${prefix}Columns`] || ''}
onChange={(e) => onChange(`${prefix}Columns`, parseInt(e.target.value) || 3)} onChange={(e) =>
onChange(`${prefix}Columns`, parseInt(e.target.value) || 3)
}
placeholder='3' placeholder='3'
/> />
</div> </div>
@ -141,7 +159,9 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
{/* Font Size (optional) */} {/* Font Size (optional) */}
{showFont && ( {showFont && (
<div> <div>
<label className='mb-1 block text-[10px] text-gray-600'>Font size</label> <label className='mb-1 block text-[10px] text-gray-600'>
Font size
</label>
<input <input
className='w-full rounded border border-gray-300 px-2 py-1 text-xs' className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={values[`${prefix}FontSize`] || ''} value={values[`${prefix}FontSize`] || ''}
@ -154,7 +174,9 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
{/* Font Weight (optional) */} {/* Font Weight (optional) */}
{showFont && ( {showFont && (
<div> <div>
<label className='mb-1 block text-[10px] text-gray-600'>Font weight</label> <label className='mb-1 block text-[10px] text-gray-600'>
Font weight
</label>
<input <input
className='w-full rounded border border-gray-300 px-2 py-1 text-xs' className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={values[`${prefix}FontWeight`] || ''} value={values[`${prefix}FontWeight`] || ''}
@ -184,54 +206,165 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
</div> </div>
)} )}
{/* Header Dimensions (header section only) */}
{showDimensions && (
<>
<p className='text-[10px] text-gray-500 pt-1'>Dimensions:</p>
<div className='grid grid-cols-2 gap-2'>
<div>
<label className='mb-1 block text-[10px] text-gray-600'>
Width
</label>
<input
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={values[`${prefix}Width`] || ''}
onChange={(e) => onChange(`${prefix}Width`, e.target.value)}
placeholder='100%, 300px'
/>
</div>
<div>
<label className='mb-1 block text-[10px] text-gray-600'>
Height
</label>
<input
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={values[`${prefix}Height`] || ''}
onChange={(e) => onChange(`${prefix}Height`, e.target.value)}
placeholder='200px, auto'
/>
</div>
<div>
<label className='mb-1 block text-[10px] text-gray-600'>
Min Height
</label>
<input
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={values[`${prefix}MinHeight`] || ''}
onChange={(e) => onChange(`${prefix}MinHeight`, e.target.value)}
placeholder='150px'
/>
</div>
<div>
<label className='mb-1 block text-[10px] text-gray-600'>
Max Height
</label>
<input
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={values[`${prefix}MaxHeight`] || ''}
onChange={(e) => onChange(`${prefix}MaxHeight`, e.target.value)}
placeholder='400px, 50vh'
/>
</div>
</div>
</>
)}
{/* Card Aspect Ratio and Min Height (cards only) */}
{showAspectRatio && (
<>
<p className='text-[10px] text-gray-500 pt-1'>Card dimensions:</p>
<div className='grid grid-cols-2 gap-2'>
<div>
<label className='mb-1 block text-[10px] text-gray-600'>
Aspect Ratio
</label>
<select
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={values[`${prefix}AspectRatio`] || ''}
onChange={(e) =>
onChange(`${prefix}AspectRatio`, e.target.value)
}
>
<option value=''>4:3 (Default)</option>
<option value='16/9'>16:9 (Widescreen)</option>
<option value='1/1'>1:1 (Square)</option>
<option value='3/2'>3:2</option>
<option value='auto'>Auto</option>
</select>
</div>
<div>
<label className='mb-1 block text-[10px] text-gray-600'>
Min Height
</label>
<input
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={values[`${prefix}MinHeight`] || ''}
onChange={(e) => onChange(`${prefix}MinHeight`, e.target.value)}
placeholder='150px'
/>
</div>
</div>
</>
)}
{/* Card Title Styles (cards only) */} {/* Card Title Styles (cards only) */}
{showTitleStyles && ( {showTitleStyles && (
<> <>
<p className='text-[10px] text-gray-500 pt-1'>Card title overlay:</p> <p className='text-[10px] text-gray-500 pt-1'>Card title overlay:</p>
<div className='grid grid-cols-2 gap-2'> <div className='grid grid-cols-2 gap-2'>
<div> <div>
<label className='mb-1 block text-[10px] text-gray-600'>Title color</label> <label className='mb-1 block text-[10px] text-gray-600'>
Title color
</label>
<input <input
className='w-full rounded border border-gray-300 px-2 py-1 text-xs' className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={values.galleryCardTitleColor || ''} value={values.galleryCardTitleColor || ''}
onChange={(e) => onChange('galleryCardTitleColor', e.target.value)} onChange={(e) =>
onChange('galleryCardTitleColor', e.target.value)
}
placeholder='#ffffff' placeholder='#ffffff'
/> />
</div> </div>
<div> <div>
<label className='mb-1 block text-[10px] text-gray-600'>Title BG</label> <label className='mb-1 block text-[10px] text-gray-600'>
Title BG
</label>
<input <input
className='w-full rounded border border-gray-300 px-2 py-1 text-xs' className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={values.galleryCardTitleBackgroundColor || ''} value={values.galleryCardTitleBackgroundColor || ''}
onChange={(e) => onChange('galleryCardTitleBackgroundColor', e.target.value)} onChange={(e) =>
onChange('galleryCardTitleBackgroundColor', e.target.value)
}
placeholder='transparent' placeholder='transparent'
/> />
</div> </div>
<div> <div>
<label className='mb-1 block text-[10px] text-gray-600'>Title size</label> <label className='mb-1 block text-[10px] text-gray-600'>
Title size
</label>
<input <input
className='w-full rounded border border-gray-300 px-2 py-1 text-xs' className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={values.galleryCardTitleFontSize || ''} value={values.galleryCardTitleFontSize || ''}
onChange={(e) => onChange('galleryCardTitleFontSize', e.target.value)} onChange={(e) =>
onChange('galleryCardTitleFontSize', e.target.value)
}
placeholder='0.75rem' placeholder='0.75rem'
/> />
</div> </div>
<div> <div>
<label className='mb-1 block text-[10px] text-gray-600'>Title weight</label> <label className='mb-1 block text-[10px] text-gray-600'>
Title weight
</label>
<input <input
className='w-full rounded border border-gray-300 px-2 py-1 text-xs' className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={values.galleryCardTitleFontWeight || ''} value={values.galleryCardTitleFontWeight || ''}
onChange={(e) => onChange('galleryCardTitleFontWeight', e.target.value)} onChange={(e) =>
onChange('galleryCardTitleFontWeight', e.target.value)
}
placeholder='700' placeholder='700'
/> />
</div> </div>
</div> </div>
<div> <div>
<label className='mb-1 block text-[10px] text-gray-600'>Title shadow</label> <label className='mb-1 block text-[10px] text-gray-600'>
Title shadow
</label>
<input <input
className='w-full rounded border border-gray-300 px-2 py-1 text-xs' className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={values.galleryCardTitleShadow || ''} value={values.galleryCardTitleShadow || ''}
onChange={(e) => onChange('galleryCardTitleShadow', e.target.value)} onChange={(e) =>
onChange('galleryCardTitleShadow', e.target.value)
}
placeholder='0 1px 3px rgba(0,0,0,0.5)' placeholder='0 1px 3px rgba(0,0,0,0.5)'
/> />
</div> </div>

View File

@ -61,7 +61,9 @@ const GallerySettingsSectionCompact: React.FC<
<div className='space-y-3'> <div className='space-y-3'>
{/* Header Settings */} {/* Header Settings */}
<div className='rounded border border-gray-200 p-2 space-y-2'> <div className='rounded border border-gray-200 p-2 space-y-2'>
<p className='text-[11px] font-semibold text-gray-700'>Gallery header</p> <p className='text-[11px] font-semibold text-gray-700'>
Gallery header
</p>
<select <select
className='w-full rounded border border-gray-300 px-2 py-1 text-xs' className='w-full rounded border border-gray-300 px-2 py-1 text-xs'

View File

@ -133,7 +133,9 @@ const NavigationSettingsSectionCompact: React.FC<
<select <select
className='w-full rounded border border-gray-300 px-2 py-1 text-xs' className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={navLabelFontFamily} value={navLabelFontFamily}
onChange={(event) => onChange('navLabelFontFamily', event.target.value)} onChange={(event) =>
onChange('navLabelFontFamily', event.target.value)
}
> >
<option value=''>Not set</option> <option value=''>Not set</option>
{FONT_OPTIONS.map((font) => ( {FONT_OPTIONS.map((font) => (

View File

@ -327,7 +327,9 @@ export function useElementSettingsForm(options: UseElementSettingsFormOptions) {
mediaMuted: Boolean(settings.mediaMuted), mediaMuted: Boolean(settings.mediaMuted),
carouselPrevIconUrl: String(settings.carouselPrevIconUrl || ''), carouselPrevIconUrl: String(settings.carouselPrevIconUrl || ''),
carouselNextIconUrl: String(settings.carouselNextIconUrl || ''), carouselNextIconUrl: String(settings.carouselNextIconUrl || ''),
carouselCaptionFontFamily: String(settings.carouselCaptionFontFamily || ''), carouselCaptionFontFamily: String(
settings.carouselCaptionFontFamily || '',
),
galleryTitleFontFamily: String(settings.galleryTitleFontFamily || ''), galleryTitleFontFamily: String(settings.galleryTitleFontFamily || ''),
galleryTextFontFamily: String(settings.galleryTextFontFamily || ''), galleryTextFontFamily: String(settings.galleryTextFontFamily || ''),
galleryCards: Array.isArray(settings.galleryCards) galleryCards: Array.isArray(settings.galleryCards)
@ -658,16 +660,19 @@ export function useElementSettingsForm(options: UseElementSettingsFormOptions) {
settings.descriptionText = state.descriptionText; settings.descriptionText = state.descriptionText;
// Only include if explicitly set - allows CSS inheritance from wrapper // Only include if explicitly set - allows CSS inheritance from wrapper
if (state.descriptionTitleFontSize.trim()) { if (state.descriptionTitleFontSize.trim()) {
settings.descriptionTitleFontSize = state.descriptionTitleFontSize.trim(); settings.descriptionTitleFontSize =
state.descriptionTitleFontSize.trim();
} }
if (state.descriptionTextFontSize.trim()) { if (state.descriptionTextFontSize.trim()) {
settings.descriptionTextFontSize = state.descriptionTextFontSize.trim(); settings.descriptionTextFontSize = state.descriptionTextFontSize.trim();
} }
if (state.descriptionTitleFontFamily.trim()) { if (state.descriptionTitleFontFamily.trim()) {
settings.descriptionTitleFontFamily = state.descriptionTitleFontFamily.trim(); settings.descriptionTitleFontFamily =
state.descriptionTitleFontFamily.trim();
} }
if (state.descriptionTextFontFamily.trim()) { if (state.descriptionTextFontFamily.trim()) {
settings.descriptionTextFontFamily = state.descriptionTextFontFamily.trim(); settings.descriptionTextFontFamily =
state.descriptionTextFontFamily.trim();
} }
if (state.descriptionTitleColor.trim()) { if (state.descriptionTitleColor.trim()) {
settings.descriptionTitleColor = state.descriptionTitleColor.trim(); settings.descriptionTitleColor = state.descriptionTitleColor.trim();
@ -698,7 +703,8 @@ export function useElementSettingsForm(options: UseElementSettingsFormOptions) {
})); }));
settings.carouselPrevIconUrl = state.carouselPrevIconUrl.trim(); settings.carouselPrevIconUrl = state.carouselPrevIconUrl.trim();
settings.carouselNextIconUrl = state.carouselNextIconUrl.trim(); settings.carouselNextIconUrl = state.carouselNextIconUrl.trim();
settings.carouselCaptionFontFamily = state.carouselCaptionFontFamily.trim(); settings.carouselCaptionFontFamily =
state.carouselCaptionFontFamily.trim();
} }
// Media type settings // Media type settings

View File

@ -392,23 +392,23 @@ export default function RuntimePresentation({
<> <>
<Head> <Head>
<title>{project?.name || 'Presentation'}</title> <title>{project?.name || 'Presentation'}</title>
{faviconUrl && <link key="favicon" rel="icon" href={faviconUrl} />} {faviconUrl && <link key='favicon' rel='icon' href={faviconUrl} />}
{ogImageUrl && ( {ogImageUrl && (
<> <>
<meta key="og:image" property="og:image" content={ogImageUrl} /> <meta key='og:image' property='og:image' content={ogImageUrl} />
<meta <meta
key="twitter:image:src" key='twitter:image:src'
property="twitter:image:src" property='twitter:image:src'
content={ogImageUrl} content={ogImageUrl}
/> />
</> </>
)} )}
{project?.name && ( {project?.name && (
<> <>
<meta key="og:title" property="og:title" content={project.name} /> <meta key='og:title' property='og:title' content={project.name} />
<meta <meta
key="twitter:title" key='twitter:title'
property="twitter:title" property='twitter:title'
content={project.name} content={project.name}
/> />
</> </>
@ -416,13 +416,13 @@ export default function RuntimePresentation({
{project?.description && ( {project?.description && (
<> <>
<meta <meta
key="og:description" key='og:description'
property="og:description" property='og:description'
content={project.description} content={project.description}
/> />
<meta <meta
key="twitter:description" key='twitter:description'
property="twitter:description" property='twitter:description'
content={project.description} content={project.description}
/> />
</> </>

View File

@ -205,9 +205,17 @@ const GalleryCarouselOverlay: React.FC<GalleryCarouselOverlayProps> = ({
); );
// For prev/next, also update the other button's Y position // For prev/next, also update the other button's Y position
if (draggingButton === 'prev') { if (draggingButton === 'prev') {
onButtonPositionChange('next', currentPositions.nextX, currentPositions.prevY); onButtonPositionChange(
'next',
currentPositions.nextX,
currentPositions.prevY,
);
} else if (draggingButton === 'next') { } else if (draggingButton === 'next') {
onButtonPositionChange('prev', currentPositions.prevX, currentPositions.nextY); onButtonPositionChange(
'prev',
currentPositions.prevX,
currentPositions.nextY,
);
} }
} }
setDraggingButton(null); setDraggingButton(null);
@ -377,7 +385,6 @@ const GalleryCarouselOverlay: React.FC<GalleryCarouselOverlayProps> = ({
backWidth, backWidth,
backHeight, backHeight,
)} )}
</div> </div>
); );
}; };

View File

@ -95,13 +95,9 @@ const DescriptionElement: React.FC<DescriptionElementProps> = ({
return ( return (
<div className={className} style={style}> <div className={className} style={style}>
<div className='p-4'> <div className='p-4'>
<p style={titleStyle}> <p style={titleStyle}>{element.descriptionTitle || ''}</p>
{element.descriptionTitle || ''}
</p>
{element.descriptionText && ( {element.descriptionText && (
<p style={textStyle}> <p style={textStyle}>{element.descriptionText}</p>
{element.descriptionText}
</p>
)} )}
</div> </div>
</div> </div>

View File

@ -47,35 +47,63 @@ const GalleryElement: React.FC<GalleryElementProps> = ({
const title = element.galleryTitle; const title = element.galleryTitle;
// Build section styles from element properties // Build section styles from element properties
const headerStyle = useMemo(() => buildGalleryHeaderStyle(element), [element]); const headerStyle = useMemo(
() => buildGalleryHeaderStyle(element),
[element],
);
const titleStyle = useMemo(() => buildGalleryTitleStyle(element), [element]); const titleStyle = useMemo(() => buildGalleryTitleStyle(element), [element]);
const spanStyle = useMemo(() => buildGallerySpanStyle(element), [element]); const spanStyle = useMemo(() => buildGallerySpanStyle(element), [element]);
const spanGridStyle = useMemo(() => buildGallerySpanGridStyle(element), [element]); const spanGridStyle = useMemo(
() => buildGallerySpanGridStyle(element),
[element],
);
const cardStyle = useMemo(() => buildGalleryCardStyle(element), [element]); const cardStyle = useMemo(() => buildGalleryCardStyle(element), [element]);
const cardTitleStyle = useMemo(() => buildGalleryCardTitleStyle(element), [element]); const cardTitleStyle = useMemo(
const cardGridStyle = useMemo(() => buildGalleryCardGridStyle(element), [element]); () => buildGalleryCardTitleStyle(element),
[element],
);
const cardGridStyle = useMemo(
() => buildGalleryCardGridStyle(element),
[element],
);
// Build wrapper style from general element styles with gallery defaults // Build wrapper style from general element styles with gallery defaults
const wrapperDefaults = GALLERY_SECTION_DEFAULTS.wrapper; const wrapperDefaults = GALLERY_SECTION_DEFAULTS.wrapper;
const wrapperStyle: CSSProperties = useMemo(() => ({ const wrapperStyle: CSSProperties = useMemo(
display: 'flex', () => ({
flexDirection: 'column', display: 'flex',
backgroundColor: style.backgroundColor || wrapperDefaults.backgroundColor, flexDirection: 'column',
padding: style.padding || wrapperDefaults.padding, backgroundColor: style.backgroundColor || wrapperDefaults.backgroundColor,
borderRadius: style.borderRadius || wrapperDefaults.borderRadius, padding: style.padding || wrapperDefaults.padding,
border: style.border, borderRadius: style.borderRadius || wrapperDefaults.borderRadius,
gap: style.gap || wrapperDefaults.gap, border: style.border,
backdropFilter: wrapperDefaults.backdropFilter, gap: style.gap || wrapperDefaults.gap,
WebkitBackdropFilter: wrapperDefaults.backdropFilter, backdropFilter: wrapperDefaults.backdropFilter,
// Visual properties that should apply to the wrapper, not outer positioning div WebkitBackdropFilter: wrapperDefaults.backdropFilter,
boxShadow: style.boxShadow, // Visual properties that should apply to the wrapper, not outer positioning div
opacity: style.opacity, boxShadow: style.boxShadow,
// Inheritable text styles - cascade to child sections opacity: style.opacity,
color: style.color, // Inheritable text styles - cascade to child sections
fontSize: style.fontSize, color: style.color,
fontWeight: style.fontWeight, fontSize: style.fontSize,
lineHeight: style.lineHeight, fontWeight: style.fontWeight,
}), [style.backgroundColor, style.padding, style.borderRadius, style.border, style.gap, style.boxShadow, style.opacity, style.color, style.fontSize, style.fontWeight, style.lineHeight, wrapperDefaults]); lineHeight: style.lineHeight,
}),
[
style.backgroundColor,
style.padding,
style.borderRadius,
style.border,
style.gap,
style.boxShadow,
style.opacity,
style.color,
style.fontSize,
style.fontWeight,
style.lineHeight,
wrapperDefaults,
],
);
// Extract wrapper-related styles from outer style (they go to wrapper, not outer div) // Extract wrapper-related styles from outer style (they go to wrapper, not outer div)
const { const {
@ -95,19 +123,20 @@ const GalleryElement: React.FC<GalleryElementProps> = ({
return ( return (
<div className={className} style={outerStyle}> <div className={className} style={outerStyle}>
<div <div className='min-w-[200px]' style={wrapperStyle}>
className='min-w-[200px]'
style={wrapperStyle}
>
{/* Header: image takes priority, otherwise render text */} {/* Header: image takes priority, otherwise render text */}
{/* Header styles (border, borderRadius, etc.) apply to both image and text modes */} {/* Header styles (border, borderRadius, dimensions) apply to both image and text modes */}
{headerImageUrl ? ( {headerImageUrl ? (
<div style={{ ...headerStyle, padding: 0, overflow: 'hidden' }}> <div style={{ ...headerStyle, padding: 0, overflow: 'hidden' }}>
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
<img <img
src={resolve(headerImageUrl)} src={resolve(headerImageUrl)}
alt='' alt=''
className='w-full h-auto object-cover' className='object-cover'
style={{
width: '100%',
height: headerStyle.height || 'auto',
}}
draggable={false} draggable={false}
/> />
</div> </div>
@ -157,39 +186,53 @@ const GalleryElement: React.FC<GalleryElementProps> = ({
{/* Gallery cards */} {/* Gallery cards */}
{cards.length > 0 && ( {cards.length > 0 && (
<div style={cardGridStyle}> <div style={cardGridStyle}>
{cards.map((card, index) => ( {cards.map((card, index) => {
<div // Build card container style with aspect ratio and dimensions
key={card.id} const cardContainerStyle: CSSProperties = {
className={`relative aspect-[4/3] min-w-[50px] min-h-[40px] overflow-hidden ${ ...cardStyle,
onCardClick position: 'relative',
? 'cursor-pointer hover:ring-2 hover:ring-white hover:ring-offset-1 hover:ring-offset-black/50 transition-all' overflow: 'hidden',
: '' minWidth: '50px',
}`} // Use aspect-ratio from cardStyle, fallback to 4/3
style={cardStyle} aspectRatio: cardStyle.aspectRatio || '4/3',
onClick={(e) => { // Use minHeight from cardStyle, fallback to 40px
if (onCardClick) { minHeight: cardStyle.minHeight || '40px',
e.stopPropagation(); };
onCardClick(index);
return (
<div
key={card.id}
className={
onCardClick
? 'cursor-pointer hover:ring-2 hover:ring-white hover:ring-offset-1 hover:ring-offset-black/50 transition-all'
: ''
} }
}} style={cardContainerStyle}
> onClick={(e) => {
{card.imageUrl && ( if (onCardClick) {
// eslint-disable-next-line @next/next/no-img-element e.stopPropagation();
<img onCardClick(index);
src={resolve(card.imageUrl)} }
alt={card.title || ''} }}
className='absolute inset-0 w-full h-full object-cover' >
style={{ borderRadius: cardStyle.borderRadius }} {card.imageUrl && (
draggable={false} // eslint-disable-next-line @next/next/no-img-element
/> <img
)} src={resolve(card.imageUrl)}
{card.title && ( alt={card.title || ''}
<div className='absolute inset-0 flex items-center justify-center'> className='absolute inset-0 w-full h-full object-cover'
<span style={cardTitleStyle}>{card.title}</span> style={{ borderRadius: cardContainerStyle.borderRadius }}
</div> draggable={false}
)} />
</div> )}
))} {card.title && (
<div className='absolute inset-0 flex items-center justify-center'>
<span style={cardTitleStyle}>{card.title}</span>
</div>
)}
</div>
);
})}
</div> </div>
)} )}
</div> </div>

View File

@ -69,12 +69,8 @@ const TooltipElement: React.FC<TooltipElementProps> = ({
return ( return (
<div className={className} style={style}> <div className={className} style={style}>
<div className='p-3 max-w-[200px]'> <div className='p-3 max-w-[200px]'>
<p style={titleFontStyle}> <p style={titleFontStyle}>{element.tooltipTitle}</p>
{element.tooltipTitle} <p style={{ opacity: 0.7, ...textFontStyle }}>{element.tooltipText}</p>
</p>
<p style={{ opacity: 0.7, ...textFontStyle }}>
{element.tooltipText}
</p>
</div> </div>
</div> </div>
); );

View File

@ -51,7 +51,8 @@ function validateAssetType(file, expectedType) {
const hasExtensionMatch = extension && extensions.includes(extension); const hasExtensionMatch = extension && extensions.includes(extension);
if (!hasMimeMatch && !hasExtensionMatch) { if (!hasMimeMatch && !hasExtensionMatch) {
const typeLabel = expectedType.charAt(0).toUpperCase() + expectedType.slice(1); const typeLabel =
expectedType.charAt(0).toUpperCase() + expectedType.slice(1);
return { return {
valid: false, valid: false,
error: `Invalid file type. Expected ${typeLabel} file but got "${mimeType || 'unknown'}" (${file.name})`, error: `Invalid file type. Expected ${typeLabel} file but got "${mimeType || 'unknown'}" (${file.name})`,
@ -114,7 +115,9 @@ export default class FileUploader {
if (schema.size && file.size > schema.size) { if (schema.size && file.size > schema.size) {
const maxSizeMB = (schema.size / (1024 * 1024)).toFixed(1); const maxSizeMB = (schema.size / (1024 * 1024)).toFixed(1);
const fileSizeMB = (file.size / (1024 * 1024)).toFixed(1); const fileSizeMB = (file.size / (1024 * 1024)).toFixed(1);
throw new Error(`File is too big. Maximum ${maxSizeMB}MB, got ${fileSizeMB}MB`); throw new Error(
`File is too big. Maximum ${maxSizeMB}MB, got ${fileSizeMB}MB`,
);
} }
// Extension validation // Extension validation

View File

@ -363,8 +363,8 @@ export function useConstructorElements({
update: (spanId: string, patch: Partial<GalleryInfoSpan>) => { update: (spanId: string, patch: Partial<GalleryInfoSpan>) => {
if (!selectedElement || !isGalleryElementType(selectedElement.type)) if (!selectedElement || !isGalleryElementType(selectedElement.type))
return; return;
const nextSpans = (selectedElement.galleryInfoSpans || []).map((span) => const nextSpans = (selectedElement.galleryInfoSpans || []).map(
span.id === spanId ? { ...span, ...patch } : span, (span) => (span.id === spanId ? { ...span, ...patch } : span),
); );
updateSelectedElement({ galleryInfoSpans: nextSpans }); updateSelectedElement({ galleryInfoSpans: nextSpans });
}, },

View File

@ -630,7 +630,11 @@ export function usePreloadOrchestrator(
const checkObject = (obj: Record<string, unknown>) => { const checkObject = (obj: Record<string, unknown>) => {
if (!obj || typeof obj !== 'object') return; if (!obj || typeof obj !== 'object') return;
for (const [key, value] of Object.entries(obj)) { for (const [key, value] of Object.entries(obj)) {
if (typeof value === 'string' && value && urlFields.includes(key)) { if (
typeof value === 'string' &&
value &&
urlFields.includes(key)
) {
elementAssetUrls.push(value); elementAssetUrls.push(value);
} else if (typeof value === 'object' && value !== null) { } else if (typeof value === 'object' && value !== null) {
checkObject(value as Record<string, unknown>); checkObject(value as Record<string, unknown>);

View File

@ -30,7 +30,9 @@ interface ProjectAssetInput {
* Extracts storage paths from URLs (handles both formats) and resolves them * Extracts storage paths from URLs (handles both formats) and resolves them
* to presigned URLs for access. * to presigned URLs for access.
*/ */
export function useProjectAssets(project: ProjectAssetInput | null): ProjectAssets { export function useProjectAssets(
project: ProjectAssetInput | null,
): ProjectAssets {
const [assets, setAssets] = useState<ProjectAssets>({ const [assets, setAssets] = useState<ProjectAssets>({
faviconUrl: null, faviconUrl: null,
ogImageUrl: null, ogImageUrl: null,
@ -89,7 +91,12 @@ export function useProjectAssets(project: ProjectAssetInput | null): ProjectAsse
// Reset loading state and resolve // Reset loading state and resolve
setAssets((prev) => ({ ...prev, isLoading: true })); setAssets((prev) => ({ ...prev, isLoading: true }));
resolveAssets(); resolveAssets();
}, [project?.favicon_url, project?.og_image_url, project?.logo_url, resolveAssets]); }, [
project?.favicon_url,
project?.og_image_url,
project?.logo_url,
resolveAssets,
]);
return assets; return assets;
} }

View File

@ -101,8 +101,7 @@ export type EffectPropName = (typeof EFFECT_PROPS)[number];
export function buildTransitionStyle( export function buildTransitionStyle(
effects: Partial<ElementEffectProperties>, effects: Partial<ElementEffectProperties>,
): CSSProperties { ): CSSProperties {
const duration = const duration = normalizeDuration(effects.hoverTransitionDuration) || '0.2s';
normalizeDuration(effects.hoverTransitionDuration) || '0.2s';
return { return {
transition: `all ${duration} ease`, transition: `all ${duration} ease`,
}; };
@ -228,8 +227,7 @@ export function buildAppearAnimationStyle(
return {}; return {};
} }
const duration = const duration = normalizeDuration(effects.appearAnimationDuration) || '0.3s';
normalizeDuration(effects.appearAnimationDuration) || '0.3s';
const easing = effects.appearAnimationEasing || 'ease'; const easing = effects.appearAnimationEasing || 'ease';
return { return {

View File

@ -12,12 +12,20 @@ import { getFontByKey, getFontStyle } from './fonts';
/** /**
* Gallery section names for styling * Gallery section names for styling
*/ */
export type GallerySectionName = 'header' | 'title' | 'span' | 'card' | 'wrapper'; export type GallerySectionName =
| 'header'
| 'title'
| 'span'
| 'card'
| 'wrapper';
/** /**
* Default values for gallery sections to preserve current Tailwind appearance * Default values for gallery sections to preserve current Tailwind appearance
*/ */
export const GALLERY_SECTION_DEFAULTS: Record<GallerySectionName, CSSProperties> = { export const GALLERY_SECTION_DEFAULTS: Record<
GallerySectionName,
CSSProperties
> = {
header: { header: {
fontSize: '1.5rem', // text-2xl fontSize: '1.5rem', // text-2xl
fontWeight: '700', // font-bold fontWeight: '700', // font-bold
@ -104,10 +112,12 @@ const normalizeWithUnit = (value: unknown, unit: string): string => {
}; };
/** Normalize rem values (fontSize, padding, borderRadius, gap) */ /** Normalize rem values (fontSize, padding, borderRadius, gap) */
const normalizeRemValue = (value: unknown): string => normalizeWithUnit(value, 'rem'); const normalizeRemValue = (value: unknown): string =>
normalizeWithUnit(value, 'rem');
/** Normalize pixel values (for properties that use px) */ /** Normalize pixel values (for properties that use px) */
const normalizePxValue = (value: unknown): string => normalizeWithUnit(value, 'px'); const normalizePxValue = (value: unknown): string =>
normalizeWithUnit(value, 'px');
/** /**
* Apply value with default fallback and optional unit normalization * Apply value with default fallback and optional unit normalization
@ -155,11 +165,27 @@ export function buildGalleryHeaderStyle(
applyIfSet(style, 'backgroundColor', element.galleryHeaderBackgroundColor); applyIfSet(style, 'backgroundColor', element.galleryHeaderBackgroundColor);
// Inheritable properties: only apply if explicitly set (allows CSS inheritance from wrapper) // Inheritable properties: only apply if explicitly set (allows CSS inheritance from wrapper)
applyIfSet(style, 'color', element.galleryHeaderColor); applyIfSet(style, 'color', element.galleryHeaderColor);
applyIfSet(style, 'fontSize', element.galleryHeaderFontSize, normalizeRemValue); applyIfSet(
style,
'fontSize',
element.galleryHeaderFontSize,
normalizeRemValue,
);
applyIfSet(style, 'fontWeight', element.galleryHeaderFontWeight); applyIfSet(style, 'fontWeight', element.galleryHeaderFontWeight);
// Non-inheritable properties: use defaults // Non-inheritable properties: use defaults
applyWithDefault(style, 'padding', element.galleryHeaderPadding, defaults.padding, normalizeRemValue); applyWithDefault(
applyIfSet(style, 'borderRadius', element.galleryHeaderBorderRadius, normalizeRemValue); style,
'padding',
element.galleryHeaderPadding,
defaults.padding,
normalizeRemValue,
);
applyIfSet(
style,
'borderRadius',
element.galleryHeaderBorderRadius,
normalizeRemValue,
);
applyIfSet(style, 'border', element.galleryHeaderBorder); // Complex value, no normalization applyIfSet(style, 'border', element.galleryHeaderBorder); // Complex value, no normalization
// Apply font family with font library resolution // Apply font family with font library resolution
@ -173,6 +199,22 @@ export function buildGalleryHeaderStyle(
} }
} }
// Dimension properties - only apply if explicitly set (allows CSS defaults)
applyIfSet(style, 'width', element.galleryHeaderWidth);
applyIfSet(style, 'height', element.galleryHeaderHeight, normalizePxValue);
applyIfSet(
style,
'minHeight',
element.galleryHeaderMinHeight,
normalizePxValue,
);
applyIfSet(
style,
'maxHeight',
element.galleryHeaderMaxHeight,
normalizePxValue,
);
return style; return style;
} }
@ -185,14 +227,36 @@ export function buildGalleryTitleStyle(
const defaults = GALLERY_SECTION_DEFAULTS.title; const defaults = GALLERY_SECTION_DEFAULTS.title;
const style: CSSProperties = {}; const style: CSSProperties = {};
applyWithDefault(style, 'backgroundColor', element.galleryTitleBackgroundColor, defaults.backgroundColor); applyWithDefault(
style,
'backgroundColor',
element.galleryTitleBackgroundColor,
defaults.backgroundColor,
);
// Inheritable properties: only apply if explicitly set (allows CSS inheritance from wrapper) // Inheritable properties: only apply if explicitly set (allows CSS inheritance from wrapper)
applyIfSet(style, 'color', element.galleryTitleColor); applyIfSet(style, 'color', element.galleryTitleColor);
applyIfSet(style, 'fontSize', element.galleryTitleFontSize, normalizeRemValue); applyIfSet(
style,
'fontSize',
element.galleryTitleFontSize,
normalizeRemValue,
);
applyIfSet(style, 'fontWeight', element.galleryTitleFontWeight); applyIfSet(style, 'fontWeight', element.galleryTitleFontWeight);
// Non-inheritable properties: use defaults // Non-inheritable properties: use defaults
applyWithDefault(style, 'padding', element.galleryTitlePadding, defaults.padding, normalizeRemValue); applyWithDefault(
applyWithDefault(style, 'borderRadius', element.galleryTitleBorderRadius, defaults.borderRadius, normalizeRemValue); style,
'padding',
element.galleryTitlePadding,
defaults.padding,
normalizeRemValue,
);
applyWithDefault(
style,
'borderRadius',
element.galleryTitleBorderRadius,
defaults.borderRadius,
normalizeRemValue,
);
applyIfSet(style, 'border', element.galleryTitleBorder); // Complex value, no normalization applyIfSet(style, 'border', element.galleryTitleBorder); // Complex value, no normalization
// Apply font family with font library resolution // Apply font family with font library resolution
@ -218,18 +282,36 @@ export function buildGallerySpanStyle(
const defaults = GALLERY_SECTION_DEFAULTS.span; const defaults = GALLERY_SECTION_DEFAULTS.span;
const style: CSSProperties = {}; const style: CSSProperties = {};
applyWithDefault(style, 'backgroundColor', element.gallerySpanBackgroundColor, defaults.backgroundColor); applyWithDefault(
style,
'backgroundColor',
element.gallerySpanBackgroundColor,
defaults.backgroundColor,
);
// Inheritable properties: only apply if explicitly set (allows CSS inheritance from wrapper) // Inheritable properties: only apply if explicitly set (allows CSS inheritance from wrapper)
applyIfSet(style, 'color', element.gallerySpanColor); applyIfSet(style, 'color', element.gallerySpanColor);
applyIfSet(style, 'fontSize', element.gallerySpanFontSize, normalizeRemValue); applyIfSet(style, 'fontSize', element.gallerySpanFontSize, normalizeRemValue);
applyIfSet(style, 'fontWeight', element.gallerySpanFontWeight); applyIfSet(style, 'fontWeight', element.gallerySpanFontWeight);
// Non-inheritable properties: use defaults // Non-inheritable properties: use defaults
applyWithDefault(style, 'padding', element.gallerySpanPadding, defaults.padding, normalizeRemValue); applyWithDefault(
applyWithDefault(style, 'borderRadius', element.gallerySpanBorderRadius, defaults.borderRadius, normalizeRemValue); style,
'padding',
element.gallerySpanPadding,
defaults.padding,
normalizeRemValue,
);
applyWithDefault(
style,
'borderRadius',
element.gallerySpanBorderRadius,
defaults.borderRadius,
normalizeRemValue,
);
applyIfSet(style, 'border', element.gallerySpanBorder); // Complex value, no normalization applyIfSet(style, 'border', element.gallerySpanBorder); // Complex value, no normalization
// Apply font family with font library resolution (fallback to galleryTextFontFamily for legacy support) // Apply font family with font library resolution (fallback to galleryTextFontFamily for legacy support)
const fontKey = element.gallerySpanFontFamily || element.galleryTextFontFamily; const fontKey =
element.gallerySpanFontFamily || element.galleryTextFontFamily;
if (fontKey) { if (fontKey) {
const font = getFontByKey(fontKey); const font = getFontByKey(fontKey);
if (font) { if (font) {
@ -267,10 +349,32 @@ export function buildGalleryCardStyle(
const defaults = GALLERY_SECTION_DEFAULTS.card; const defaults = GALLERY_SECTION_DEFAULTS.card;
const style: CSSProperties = {}; const style: CSSProperties = {};
applyWithDefault(style, 'backgroundColor', element.galleryCardBackgroundColor, undefined); applyWithDefault(
applyWithDefault(style, 'borderRadius', element.galleryCardBorderRadius, defaults.borderRadius, normalizeRemValue); style,
'backgroundColor',
element.galleryCardBackgroundColor,
undefined,
);
applyWithDefault(
style,
'borderRadius',
element.galleryCardBorderRadius,
defaults.borderRadius,
normalizeRemValue,
);
applyIfSet(style, 'border', element.galleryCardBorder); // Complex value, no normalization applyIfSet(style, 'border', element.galleryCardBorder); // Complex value, no normalization
// Dimension properties - only apply if explicitly set
applyIfSet(style, 'width', element.galleryCardWidth);
applyIfSet(style, 'height', element.galleryCardHeight, normalizePxValue);
applyIfSet(
style,
'minHeight',
element.galleryCardMinHeight,
normalizePxValue,
);
applyIfSet(style, 'aspectRatio', element.galleryCardAspectRatio);
return style; return style;
} }
@ -285,7 +389,12 @@ export function buildGalleryCardTitleStyle(
// Inheritable properties: only apply if explicitly set (allows CSS inheritance from wrapper) // Inheritable properties: only apply if explicitly set (allows CSS inheritance from wrapper)
// Note: card titles typically need white text for visibility over images // Note: card titles typically need white text for visibility over images
applyIfSet(style, 'color', element.galleryCardTitleColor); applyIfSet(style, 'color', element.galleryCardTitleColor);
applyIfSet(style, 'fontSize', element.galleryCardTitleFontSize, normalizeRemValue); applyIfSet(
style,
'fontSize',
element.galleryCardTitleFontSize,
normalizeRemValue,
);
applyIfSet(style, 'fontWeight', element.galleryCardTitleFontWeight); applyIfSet(style, 'fontWeight', element.galleryCardTitleFontWeight);
if (element.galleryCardTitleBackgroundColor) { if (element.galleryCardTitleBackgroundColor) {
@ -298,7 +407,8 @@ export function buildGalleryCardTitleStyle(
style.textShadow = shadow; style.textShadow = shadow;
} else { } else {
// Default drop-shadow-lg equivalent for visibility over images // Default drop-shadow-lg equivalent for visibility over images
style.filter = 'drop-shadow(0 10px 8px rgb(0 0 0 / 0.04)) drop-shadow(0 4px 3px rgb(0 0 0 / 0.1))'; style.filter =
'drop-shadow(0 10px 8px rgb(0 0 0 / 0.04)) drop-shadow(0 4px 3px rgb(0 0 0 / 0.1))';
} }
// Apply font family from galleryTextFontFamily for legacy support // Apply font family from galleryTextFontFamily for legacy support
@ -359,6 +469,10 @@ export const GALLERY_SECTION_STYLE_PROPS = [
'galleryHeaderPadding', 'galleryHeaderPadding',
'galleryHeaderBorderRadius', 'galleryHeaderBorderRadius',
'galleryHeaderBorder', 'galleryHeaderBorder',
'galleryHeaderWidth',
'galleryHeaderHeight',
'galleryHeaderMinHeight',
'galleryHeaderMaxHeight',
// Title // Title
'galleryTitleBackgroundColor', 'galleryTitleBackgroundColor',
'galleryTitleColor', 'galleryTitleColor',
@ -390,6 +504,11 @@ export const GALLERY_SECTION_STYLE_PROPS = [
'galleryCardTitleFontSize', 'galleryCardTitleFontSize',
'galleryCardTitleFontWeight', 'galleryCardTitleFontWeight',
'galleryCardTitleShadow', 'galleryCardTitleShadow',
'galleryCardWidth',
'galleryCardHeight',
'galleryCardMinHeight',
'galleryCardAspectRatio',
] as const; ] as const;
export type GallerySectionStyleProp = (typeof GALLERY_SECTION_STYLE_PROPS)[number]; export type GallerySectionStyleProp =
(typeof GALLERY_SECTION_STYLE_PROPS)[number];

View File

@ -283,15 +283,31 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
<meta property='og:url' content={url} /> <meta property='og:url' content={url} />
<meta property='og:site_name' content='https://flatlogic.com/' /> <meta property='og:site_name' content='https://flatlogic.com/' />
<meta key='og:title' property='og:title' content={title} /> <meta key='og:title' property='og:title' content={title} />
<meta key='og:description' property='og:description' content={description} /> <meta
key='og:description'
property='og:description'
content={description}
/>
<meta key='og:image' property='og:image' content={image} /> <meta key='og:image' property='og:image' content={image} />
<meta property='og:image:type' content='image/png' /> <meta property='og:image:type' content='image/png' />
<meta property='og:image:width' content={imageWidth} /> <meta property='og:image:width' content={imageWidth} />
<meta property='og:image:height' content={imageHeight} /> <meta property='og:image:height' content={imageHeight} />
<meta property='twitter:card' content='summary_large_image' /> <meta property='twitter:card' content='summary_large_image' />
<meta key='twitter:title' property='twitter:title' content={title} /> <meta
<meta key='twitter:description' property='twitter:description' content={description} /> key='twitter:title'
<meta key='twitter:image:src' property='twitter:image:src' content={image} /> property='twitter:title'
content={title}
/>
<meta
key='twitter:description'
property='twitter:description'
content={description}
/>
<meta
key='twitter:image:src'
property='twitter:image:src'
content={image}
/>
<meta property='twitter:image:width' content={imageWidth} /> <meta property='twitter:image:width' content={imageWidth} />
<meta property='twitter:image:height' content={imageHeight} /> <meta property='twitter:image:height' content={imageHeight} />
<link key='favicon' rel='icon' href='/favicon.svg' /> <link key='favicon' rel='icon' href='/favicon.svg' />

View File

@ -1164,7 +1164,9 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
// Update the active carousel element to reflect the new positions // Update the active carousel element to reflect the new positions
setActiveGalleryCarousel((prev) => setActiveGalleryCarousel((prev) =>
prev ? { ...prev, element: { ...prev.element, ...positionPatch } } : null, prev
? { ...prev, element: { ...prev.element, ...positionPatch } }
: null,
); );
}, },
[activeGalleryCarousel, updateSelectedElement], [activeGalleryCarousel, updateSelectedElement],
@ -1459,15 +1461,9 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
initialIndex={activeGalleryCarousel.initialIndex} initialIndex={activeGalleryCarousel.initialIndex}
onClose={() => setActiveGalleryCarousel(null)} onClose={() => setActiveGalleryCarousel(null)}
resolveUrl={resolveUrlWithBlob} resolveUrl={resolveUrlWithBlob}
prevIconUrl={ prevIconUrl={activeGalleryCarousel.element.galleryCarouselPrevIconUrl}
activeGalleryCarousel.element.galleryCarouselPrevIconUrl nextIconUrl={activeGalleryCarousel.element.galleryCarouselNextIconUrl}
} backIconUrl={activeGalleryCarousel.element.galleryCarouselBackIconUrl}
nextIconUrl={
activeGalleryCarousel.element.galleryCarouselNextIconUrl
}
backIconUrl={
activeGalleryCarousel.element.galleryCarouselBackIconUrl
}
backLabel={ backLabel={
activeGalleryCarousel.element.galleryCarouselBackLabel || 'BACK' activeGalleryCarousel.element.galleryCarouselBackLabel || 'BACK'
} }

View File

@ -363,7 +363,9 @@ const ElementTypeDefaultDetailsPage = () => {
<CarouselSettingsSection <CarouselSettingsSection
carouselPrevIconUrl={form.state.carouselPrevIconUrl} carouselPrevIconUrl={form.state.carouselPrevIconUrl}
carouselNextIconUrl={form.state.carouselNextIconUrl} carouselNextIconUrl={form.state.carouselNextIconUrl}
carouselCaptionFontFamily={form.state.carouselCaptionFontFamily} carouselCaptionFontFamily={
form.state.carouselCaptionFontFamily
}
carouselSlides={form.state.carouselSlides} carouselSlides={form.state.carouselSlides}
onAddSlide={form.addCarouselSlide} onAddSlide={form.addCarouselSlide}
onRemoveSlide={form.removeCarouselSlide} onRemoveSlide={form.removeCarouselSlide}

View File

@ -548,7 +548,9 @@ const ProjectElementDefaultDetailsPage = () => {
<CarouselSettingsSection <CarouselSettingsSection
carouselPrevIconUrl={form.state.carouselPrevIconUrl} carouselPrevIconUrl={form.state.carouselPrevIconUrl}
carouselNextIconUrl={form.state.carouselNextIconUrl} carouselNextIconUrl={form.state.carouselNextIconUrl}
carouselCaptionFontFamily={form.state.carouselCaptionFontFamily} carouselCaptionFontFamily={
form.state.carouselCaptionFontFamily
}
carouselSlides={form.state.carouselSlides} carouselSlides={form.state.carouselSlides}
onAddSlide={form.addCarouselSlide} onAddSlide={form.addCarouselSlide}
onRemoveSlide={form.removeCarouselSlide} onRemoveSlide={form.removeCarouselSlide}

View File

@ -226,13 +226,18 @@ const EditProjectsPage = () => {
: 'Select logo from Assets'} : 'Select logo from Assets'}
</option> </option>
{logoAssets.map((asset) => ( {logoAssets.map((asset) => (
<option key={asset.id} value={asset.storage_key || asset.cdn_url}> <option
key={asset.id}
value={asset.storage_key || asset.cdn_url}
>
{(asset.name || '').replace(/^\[[^\]]+\]\s*/, '')} {(asset.name || '').replace(/^\[[^\]]+\]\s*/, '')}
</option> </option>
))} ))}
{values.logo_url && {values.logo_url &&
!logoAssets.some( !logoAssets.some(
(asset) => (asset.storage_key || asset.cdn_url) === values.logo_url, (asset) =>
(asset.storage_key || asset.cdn_url) ===
values.logo_url,
) && ( ) && (
<option value={values.logo_url}> <option value={values.logo_url}>
{values.logo_url} {values.logo_url}
@ -259,13 +264,18 @@ const EditProjectsPage = () => {
: 'Select favicon from Assets logos'} : 'Select favicon from Assets logos'}
</option> </option>
{logoAssets.map((asset) => ( {logoAssets.map((asset) => (
<option key={asset.id} value={asset.storage_key || asset.cdn_url}> <option
key={asset.id}
value={asset.storage_key || asset.cdn_url}
>
{(asset.name || '').replace(/^\[[^\]]+\]\s*/, '')} {(asset.name || '').replace(/^\[[^\]]+\]\s*/, '')}
</option> </option>
))} ))}
{values.favicon_url && {values.favicon_url &&
!logoAssets.some( !logoAssets.some(
(asset) => (asset.storage_key || asset.cdn_url) === values.favicon_url, (asset) =>
(asset.storage_key || asset.cdn_url) ===
values.favicon_url,
) && ( ) && (
<option value={values.favicon_url}> <option value={values.favicon_url}>
{values.favicon_url} {values.favicon_url}
@ -292,13 +302,18 @@ const EditProjectsPage = () => {
: 'Select OG image from Assets logos'} : 'Select OG image from Assets logos'}
</option> </option>
{logoAssets.map((asset) => ( {logoAssets.map((asset) => (
<option key={asset.id} value={asset.storage_key || asset.cdn_url}> <option
key={asset.id}
value={asset.storage_key || asset.cdn_url}
>
{(asset.name || '').replace(/^\[[^\]]+\]\s*/, '')} {(asset.name || '').replace(/^\[[^\]]+\]\s*/, '')}
</option> </option>
))} ))}
{values.og_image_url && {values.og_image_url &&
!logoAssets.some( !logoAssets.some(
(asset) => (asset.storage_key || asset.cdn_url) === values.og_image_url, (asset) =>
(asset.storage_key || asset.cdn_url) ===
values.og_image_url,
) && ( ) && (
<option value={values.og_image_url}> <option value={values.og_image_url}>
{values.og_image_url} {values.og_image_url}

View File

@ -110,6 +110,11 @@ export interface CanvasElement extends BaseCanvasElement {
galleryHeaderPadding?: string; galleryHeaderPadding?: string;
galleryHeaderBorderRadius?: string; galleryHeaderBorderRadius?: string;
galleryHeaderBorder?: string; galleryHeaderBorder?: string;
// Gallery Section Styles - Header Dimensions
galleryHeaderWidth?: string;
galleryHeaderHeight?: string;
galleryHeaderMinHeight?: string;
galleryHeaderMaxHeight?: string;
// Gallery Section Styles - Title // Gallery Section Styles - Title
galleryTitleBackgroundColor?: string; galleryTitleBackgroundColor?: string;
galleryTitleColor?: string; galleryTitleColor?: string;
@ -140,6 +145,11 @@ export interface CanvasElement extends BaseCanvasElement {
galleryCardTitleFontSize?: string; galleryCardTitleFontSize?: string;
galleryCardTitleFontWeight?: string; galleryCardTitleFontWeight?: string;
galleryCardTitleShadow?: string; galleryCardTitleShadow?: string;
// Gallery Section Styles - Card Dimensions
galleryCardWidth?: string;
galleryCardHeight?: string;
galleryCardMinHeight?: string;
galleryCardAspectRatio?: string;
// Gallery Section Styles - Wrapper // Gallery Section Styles - Wrapper
galleryWrapperBackgroundColor?: string; galleryWrapperBackgroundColor?: string;
galleryWrapperPadding?: string; galleryWrapperPadding?: string;