added dimentions settings for gallery header image and cards previews
This commit is contained in:
parent
ca3b3725f9
commit
c235909c54
2
frontend/next-env.d.ts
vendored
2
frontend/next-env.d.ts
vendored
@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <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
|
||||
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -48,7 +48,9 @@ export function useProjectSelector({
|
||||
const response = await axios.get(
|
||||
'/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);
|
||||
} catch (error: unknown) {
|
||||
const errorMessage =
|
||||
|
||||
@ -91,7 +91,10 @@ const CanvasElement: React.FC<CanvasElementProps> = ({
|
||||
|
||||
// Add transition for interactive effects (preview mode only)
|
||||
if (!isEditMode && hasAnyEffects(effectProperties)) {
|
||||
positionStyle = { ...positionStyle, ...buildTransitionStyle(effectProperties) };
|
||||
positionStyle = {
|
||||
...positionStyle,
|
||||
...buildTransitionStyle(effectProperties),
|
||||
};
|
||||
}
|
||||
|
||||
// Add appear animation (ALWAYS - for WYSIWYG)
|
||||
|
||||
@ -367,7 +367,9 @@ export function ElementEditorPanel({
|
||||
}
|
||||
navType={selectedElement.navType}
|
||||
navLabel={selectedElement.navLabel || ''}
|
||||
navLabelFontFamily={selectedElement.navLabelFontFamily || ''}
|
||||
navLabelFontFamily={
|
||||
selectedElement.navLabelFontFamily || ''
|
||||
}
|
||||
navDisabled={selectedElement.navDisabled || false}
|
||||
iconUrl={selectedElement.iconUrl || ''}
|
||||
targetPageSlug={selectedElement.targetPageSlug || ''}
|
||||
@ -604,11 +606,20 @@ export function ElementEditorPanel({
|
||||
selectedElement.galleryHeaderBorderRadius || '',
|
||||
galleryHeaderBorder:
|
||||
selectedElement.galleryHeaderBorder || '',
|
||||
galleryHeaderWidth:
|
||||
selectedElement.galleryHeaderWidth || '',
|
||||
galleryHeaderHeight:
|
||||
selectedElement.galleryHeaderHeight || '',
|
||||
galleryHeaderMinHeight:
|
||||
selectedElement.galleryHeaderMinHeight || '',
|
||||
galleryHeaderMaxHeight:
|
||||
selectedElement.galleryHeaderMaxHeight || '',
|
||||
}}
|
||||
onChange={(prop, value) =>
|
||||
onUpdateElement({ [prop]: value || undefined })
|
||||
}
|
||||
showFont
|
||||
showDimensions
|
||||
/>
|
||||
<GallerySectionStyleInputs
|
||||
sectionLabel='Title'
|
||||
@ -695,6 +706,10 @@ export function ElementEditorPanel({
|
||||
selectedElement.galleryCardTitleFontWeight || '',
|
||||
galleryCardTitleShadow:
|
||||
selectedElement.galleryCardTitleShadow || '',
|
||||
galleryCardAspectRatio:
|
||||
selectedElement.galleryCardAspectRatio || '',
|
||||
galleryCardMinHeight:
|
||||
selectedElement.galleryCardMinHeight || '',
|
||||
}}
|
||||
onChange={(prop, value) =>
|
||||
onUpdateElement({ [prop]: value || undefined })
|
||||
@ -702,6 +717,7 @@ export function ElementEditorPanel({
|
||||
showGap
|
||||
showColumns
|
||||
showTitleStyles
|
||||
showAspectRatio
|
||||
/>
|
||||
<p className='text-[11px] font-semibold text-gray-700 pt-2'>
|
||||
General Element Styles
|
||||
|
||||
@ -144,7 +144,11 @@ const CarouselSettingsSection: React.FC<CarouselSettingsSectionProps> = ({
|
||||
<select
|
||||
value={slide.imageUrl}
|
||||
onChange={(event) =>
|
||||
onUpdateSlide(slide.id, 'imageUrl', event.target.value)
|
||||
onUpdateSlide(
|
||||
slide.id,
|
||||
'imageUrl',
|
||||
event.target.value,
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value=''>Not selected</option>
|
||||
|
||||
@ -89,7 +89,9 @@ const GalleryCarouselSettingsSectionCompact: React.FC<
|
||||
placeholder='W (rem)'
|
||||
value={prevWidth}
|
||||
onChange={(event) =>
|
||||
onUpdateElement({ galleryCarouselPrevWidth: event.target.value })
|
||||
onUpdateElement({
|
||||
galleryCarouselPrevWidth: event.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<input
|
||||
@ -100,7 +102,9 @@ const GalleryCarouselSettingsSectionCompact: React.FC<
|
||||
placeholder='H (rem)'
|
||||
value={prevHeight}
|
||||
onChange={(event) =>
|
||||
onUpdateElement({ galleryCarouselPrevHeight: event.target.value })
|
||||
onUpdateElement({
|
||||
galleryCarouselPrevHeight: event.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@ -136,7 +140,9 @@ const GalleryCarouselSettingsSectionCompact: React.FC<
|
||||
placeholder='W (rem)'
|
||||
value={nextWidth}
|
||||
onChange={(event) =>
|
||||
onUpdateElement({ galleryCarouselNextWidth: event.target.value })
|
||||
onUpdateElement({
|
||||
galleryCarouselNextWidth: event.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<input
|
||||
@ -147,7 +153,9 @@ const GalleryCarouselSettingsSectionCompact: React.FC<
|
||||
placeholder='H (rem)'
|
||||
value={nextHeight}
|
||||
onChange={(event) =>
|
||||
onUpdateElement({ galleryCarouselNextHeight: event.target.value })
|
||||
onUpdateElement({
|
||||
galleryCarouselNextHeight: event.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@ -183,7 +191,9 @@ const GalleryCarouselSettingsSectionCompact: React.FC<
|
||||
placeholder='W (rem)'
|
||||
value={backWidth}
|
||||
onChange={(event) =>
|
||||
onUpdateElement({ galleryCarouselBackWidth: event.target.value })
|
||||
onUpdateElement({
|
||||
galleryCarouselBackWidth: event.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<input
|
||||
@ -194,7 +204,9 @@ const GalleryCarouselSettingsSectionCompact: React.FC<
|
||||
placeholder='H (rem)'
|
||||
value={backHeight}
|
||||
onChange={(event) =>
|
||||
onUpdateElement({ galleryCarouselBackHeight: event.target.value })
|
||||
onUpdateElement({
|
||||
galleryCarouselBackHeight: event.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@ -210,7 +222,8 @@ const GalleryCarouselSettingsSectionCompact: React.FC<
|
||||
)}
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -18,6 +18,8 @@ interface GallerySectionStyleInputsProps {
|
||||
showGap?: boolean;
|
||||
showBlur?: boolean;
|
||||
showTitleStyles?: boolean;
|
||||
showDimensions?: boolean;
|
||||
showAspectRatio?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -33,6 +35,8 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
|
||||
showGap = false,
|
||||
showBlur = false,
|
||||
showTitleStyles = false,
|
||||
showDimensions = false,
|
||||
showAspectRatio = false,
|
||||
}) => {
|
||||
return (
|
||||
<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'>
|
||||
{/* Background Color */}
|
||||
<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
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
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)'
|
||||
/>
|
||||
</div>
|
||||
@ -53,7 +61,9 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
|
||||
{/* Text Color (not for wrapper) */}
|
||||
{prefix !== 'galleryWrapper' && !showTitleStyles && (
|
||||
<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
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
value={values[`${prefix}Color`] || ''}
|
||||
@ -65,7 +75,9 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
|
||||
|
||||
{/* Padding */}
|
||||
<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
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
value={values[`${prefix}Padding`] || ''}
|
||||
@ -116,7 +128,9 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
|
||||
<input
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
value={values[`${prefix}BackdropBlur`] || ''}
|
||||
onChange={(e) => onChange(`${prefix}BackdropBlur`, e.target.value)}
|
||||
onChange={(e) =>
|
||||
onChange(`${prefix}BackdropBlur`, e.target.value)
|
||||
}
|
||||
placeholder='4px'
|
||||
/>
|
||||
</div>
|
||||
@ -125,14 +139,18 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
|
||||
{/* Grid Columns (optional) */}
|
||||
{showColumns && (
|
||||
<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
|
||||
type='number'
|
||||
min='1'
|
||||
max='6'
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
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'
|
||||
/>
|
||||
</div>
|
||||
@ -141,7 +159,9 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
|
||||
{/* Font Size (optional) */}
|
||||
{showFont && (
|
||||
<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
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
value={values[`${prefix}FontSize`] || ''}
|
||||
@ -154,7 +174,9 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
|
||||
{/* Font Weight (optional) */}
|
||||
{showFont && (
|
||||
<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
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
value={values[`${prefix}FontWeight`] || ''}
|
||||
@ -184,54 +206,165 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
|
||||
</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) */}
|
||||
{showTitleStyles && (
|
||||
<>
|
||||
<p className='text-[10px] text-gray-500 pt-1'>Card title overlay:</p>
|
||||
<div className='grid grid-cols-2 gap-2'>
|
||||
<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
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
value={values.galleryCardTitleColor || ''}
|
||||
onChange={(e) => onChange('galleryCardTitleColor', e.target.value)}
|
||||
onChange={(e) =>
|
||||
onChange('galleryCardTitleColor', e.target.value)
|
||||
}
|
||||
placeholder='#ffffff'
|
||||
/>
|
||||
</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
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
value={values.galleryCardTitleBackgroundColor || ''}
|
||||
onChange={(e) => onChange('galleryCardTitleBackgroundColor', e.target.value)}
|
||||
onChange={(e) =>
|
||||
onChange('galleryCardTitleBackgroundColor', e.target.value)
|
||||
}
|
||||
placeholder='transparent'
|
||||
/>
|
||||
</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
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
value={values.galleryCardTitleFontSize || ''}
|
||||
onChange={(e) => onChange('galleryCardTitleFontSize', e.target.value)}
|
||||
onChange={(e) =>
|
||||
onChange('galleryCardTitleFontSize', e.target.value)
|
||||
}
|
||||
placeholder='0.75rem'
|
||||
/>
|
||||
</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
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
value={values.galleryCardTitleFontWeight || ''}
|
||||
onChange={(e) => onChange('galleryCardTitleFontWeight', e.target.value)}
|
||||
onChange={(e) =>
|
||||
onChange('galleryCardTitleFontWeight', e.target.value)
|
||||
}
|
||||
placeholder='700'
|
||||
/>
|
||||
</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
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
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)'
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -61,7 +61,9 @@ const GallerySettingsSectionCompact: React.FC<
|
||||
<div className='space-y-3'>
|
||||
{/* Header Settings */}
|
||||
<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
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
|
||||
@ -133,7 +133,9 @@ const NavigationSettingsSectionCompact: React.FC<
|
||||
<select
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
value={navLabelFontFamily}
|
||||
onChange={(event) => onChange('navLabelFontFamily', event.target.value)}
|
||||
onChange={(event) =>
|
||||
onChange('navLabelFontFamily', event.target.value)
|
||||
}
|
||||
>
|
||||
<option value=''>Not set</option>
|
||||
{FONT_OPTIONS.map((font) => (
|
||||
|
||||
@ -327,7 +327,9 @@ export function useElementSettingsForm(options: UseElementSettingsFormOptions) {
|
||||
mediaMuted: Boolean(settings.mediaMuted),
|
||||
carouselPrevIconUrl: String(settings.carouselPrevIconUrl || ''),
|
||||
carouselNextIconUrl: String(settings.carouselNextIconUrl || ''),
|
||||
carouselCaptionFontFamily: String(settings.carouselCaptionFontFamily || ''),
|
||||
carouselCaptionFontFamily: String(
|
||||
settings.carouselCaptionFontFamily || '',
|
||||
),
|
||||
galleryTitleFontFamily: String(settings.galleryTitleFontFamily || ''),
|
||||
galleryTextFontFamily: String(settings.galleryTextFontFamily || ''),
|
||||
galleryCards: Array.isArray(settings.galleryCards)
|
||||
@ -658,16 +660,19 @@ export function useElementSettingsForm(options: UseElementSettingsFormOptions) {
|
||||
settings.descriptionText = state.descriptionText;
|
||||
// Only include if explicitly set - allows CSS inheritance from wrapper
|
||||
if (state.descriptionTitleFontSize.trim()) {
|
||||
settings.descriptionTitleFontSize = state.descriptionTitleFontSize.trim();
|
||||
settings.descriptionTitleFontSize =
|
||||
state.descriptionTitleFontSize.trim();
|
||||
}
|
||||
if (state.descriptionTextFontSize.trim()) {
|
||||
settings.descriptionTextFontSize = state.descriptionTextFontSize.trim();
|
||||
}
|
||||
if (state.descriptionTitleFontFamily.trim()) {
|
||||
settings.descriptionTitleFontFamily = state.descriptionTitleFontFamily.trim();
|
||||
settings.descriptionTitleFontFamily =
|
||||
state.descriptionTitleFontFamily.trim();
|
||||
}
|
||||
if (state.descriptionTextFontFamily.trim()) {
|
||||
settings.descriptionTextFontFamily = state.descriptionTextFontFamily.trim();
|
||||
settings.descriptionTextFontFamily =
|
||||
state.descriptionTextFontFamily.trim();
|
||||
}
|
||||
if (state.descriptionTitleColor.trim()) {
|
||||
settings.descriptionTitleColor = state.descriptionTitleColor.trim();
|
||||
@ -698,7 +703,8 @@ export function useElementSettingsForm(options: UseElementSettingsFormOptions) {
|
||||
}));
|
||||
settings.carouselPrevIconUrl = state.carouselPrevIconUrl.trim();
|
||||
settings.carouselNextIconUrl = state.carouselNextIconUrl.trim();
|
||||
settings.carouselCaptionFontFamily = state.carouselCaptionFontFamily.trim();
|
||||
settings.carouselCaptionFontFamily =
|
||||
state.carouselCaptionFontFamily.trim();
|
||||
}
|
||||
|
||||
// Media type settings
|
||||
|
||||
@ -392,23 +392,23 @@ export default function RuntimePresentation({
|
||||
<>
|
||||
<Head>
|
||||
<title>{project?.name || 'Presentation'}</title>
|
||||
{faviconUrl && <link key="favicon" rel="icon" href={faviconUrl} />}
|
||||
{faviconUrl && <link key='favicon' rel='icon' href={faviconUrl} />}
|
||||
{ogImageUrl && (
|
||||
<>
|
||||
<meta key="og:image" property="og:image" content={ogImageUrl} />
|
||||
<meta key='og:image' property='og:image' content={ogImageUrl} />
|
||||
<meta
|
||||
key="twitter:image:src"
|
||||
property="twitter:image:src"
|
||||
key='twitter:image:src'
|
||||
property='twitter:image:src'
|
||||
content={ogImageUrl}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{project?.name && (
|
||||
<>
|
||||
<meta key="og:title" property="og:title" content={project.name} />
|
||||
<meta key='og:title' property='og:title' content={project.name} />
|
||||
<meta
|
||||
key="twitter:title"
|
||||
property="twitter:title"
|
||||
key='twitter:title'
|
||||
property='twitter:title'
|
||||
content={project.name}
|
||||
/>
|
||||
</>
|
||||
@ -416,13 +416,13 @@ export default function RuntimePresentation({
|
||||
{project?.description && (
|
||||
<>
|
||||
<meta
|
||||
key="og:description"
|
||||
property="og:description"
|
||||
key='og:description'
|
||||
property='og:description'
|
||||
content={project.description}
|
||||
/>
|
||||
<meta
|
||||
key="twitter:description"
|
||||
property="twitter:description"
|
||||
key='twitter:description'
|
||||
property='twitter:description'
|
||||
content={project.description}
|
||||
/>
|
||||
</>
|
||||
|
||||
@ -205,9 +205,17 @@ const GalleryCarouselOverlay: React.FC<GalleryCarouselOverlayProps> = ({
|
||||
);
|
||||
// For prev/next, also update the other button's Y position
|
||||
if (draggingButton === 'prev') {
|
||||
onButtonPositionChange('next', currentPositions.nextX, currentPositions.prevY);
|
||||
onButtonPositionChange(
|
||||
'next',
|
||||
currentPositions.nextX,
|
||||
currentPositions.prevY,
|
||||
);
|
||||
} else if (draggingButton === 'next') {
|
||||
onButtonPositionChange('prev', currentPositions.prevX, currentPositions.nextY);
|
||||
onButtonPositionChange(
|
||||
'prev',
|
||||
currentPositions.prevX,
|
||||
currentPositions.nextY,
|
||||
);
|
||||
}
|
||||
}
|
||||
setDraggingButton(null);
|
||||
@ -377,7 +385,6 @@ const GalleryCarouselOverlay: React.FC<GalleryCarouselOverlayProps> = ({
|
||||
backWidth,
|
||||
backHeight,
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -95,13 +95,9 @@ const DescriptionElement: React.FC<DescriptionElementProps> = ({
|
||||
return (
|
||||
<div className={className} style={style}>
|
||||
<div className='p-4'>
|
||||
<p style={titleStyle}>
|
||||
{element.descriptionTitle || ''}
|
||||
</p>
|
||||
<p style={titleStyle}>{element.descriptionTitle || ''}</p>
|
||||
{element.descriptionText && (
|
||||
<p style={textStyle}>
|
||||
{element.descriptionText}
|
||||
</p>
|
||||
<p style={textStyle}>{element.descriptionText}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -47,35 +47,63 @@ const GalleryElement: React.FC<GalleryElementProps> = ({
|
||||
const title = element.galleryTitle;
|
||||
|
||||
// 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 spanStyle = useMemo(() => buildGallerySpanStyle(element), [element]);
|
||||
const spanGridStyle = useMemo(() => buildGallerySpanGridStyle(element), [element]);
|
||||
const spanGridStyle = useMemo(
|
||||
() => buildGallerySpanGridStyle(element),
|
||||
[element],
|
||||
);
|
||||
const cardStyle = useMemo(() => buildGalleryCardStyle(element), [element]);
|
||||
const cardTitleStyle = useMemo(() => buildGalleryCardTitleStyle(element), [element]);
|
||||
const cardGridStyle = useMemo(() => buildGalleryCardGridStyle(element), [element]);
|
||||
const cardTitleStyle = useMemo(
|
||||
() => buildGalleryCardTitleStyle(element),
|
||||
[element],
|
||||
);
|
||||
const cardGridStyle = useMemo(
|
||||
() => buildGalleryCardGridStyle(element),
|
||||
[element],
|
||||
);
|
||||
|
||||
// Build wrapper style from general element styles with gallery defaults
|
||||
const wrapperDefaults = GALLERY_SECTION_DEFAULTS.wrapper;
|
||||
const wrapperStyle: CSSProperties = useMemo(() => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: style.backgroundColor || wrapperDefaults.backgroundColor,
|
||||
padding: style.padding || wrapperDefaults.padding,
|
||||
borderRadius: style.borderRadius || wrapperDefaults.borderRadius,
|
||||
border: style.border,
|
||||
gap: style.gap || wrapperDefaults.gap,
|
||||
backdropFilter: wrapperDefaults.backdropFilter,
|
||||
WebkitBackdropFilter: wrapperDefaults.backdropFilter,
|
||||
// Visual properties that should apply to the wrapper, not outer positioning div
|
||||
boxShadow: style.boxShadow,
|
||||
opacity: style.opacity,
|
||||
// Inheritable text styles - cascade to child sections
|
||||
color: style.color,
|
||||
fontSize: style.fontSize,
|
||||
fontWeight: style.fontWeight,
|
||||
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]);
|
||||
const wrapperStyle: CSSProperties = useMemo(
|
||||
() => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: style.backgroundColor || wrapperDefaults.backgroundColor,
|
||||
padding: style.padding || wrapperDefaults.padding,
|
||||
borderRadius: style.borderRadius || wrapperDefaults.borderRadius,
|
||||
border: style.border,
|
||||
gap: style.gap || wrapperDefaults.gap,
|
||||
backdropFilter: wrapperDefaults.backdropFilter,
|
||||
WebkitBackdropFilter: wrapperDefaults.backdropFilter,
|
||||
// Visual properties that should apply to the wrapper, not outer positioning div
|
||||
boxShadow: style.boxShadow,
|
||||
opacity: style.opacity,
|
||||
// Inheritable text styles - cascade to child sections
|
||||
color: style.color,
|
||||
fontSize: style.fontSize,
|
||||
fontWeight: style.fontWeight,
|
||||
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)
|
||||
const {
|
||||
@ -95,19 +123,20 @@ const GalleryElement: React.FC<GalleryElementProps> = ({
|
||||
|
||||
return (
|
||||
<div className={className} style={outerStyle}>
|
||||
<div
|
||||
className='min-w-[200px]'
|
||||
style={wrapperStyle}
|
||||
>
|
||||
<div className='min-w-[200px]' style={wrapperStyle}>
|
||||
{/* 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 ? (
|
||||
<div style={{ ...headerStyle, padding: 0, overflow: 'hidden' }}>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={resolve(headerImageUrl)}
|
||||
alt=''
|
||||
className='w-full h-auto object-cover'
|
||||
className='object-cover'
|
||||
style={{
|
||||
width: '100%',
|
||||
height: headerStyle.height || 'auto',
|
||||
}}
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
@ -157,39 +186,53 @@ const GalleryElement: React.FC<GalleryElementProps> = ({
|
||||
{/* Gallery cards */}
|
||||
{cards.length > 0 && (
|
||||
<div style={cardGridStyle}>
|
||||
{cards.map((card, index) => (
|
||||
<div
|
||||
key={card.id}
|
||||
className={`relative aspect-[4/3] min-w-[50px] min-h-[40px] overflow-hidden ${
|
||||
onCardClick
|
||||
? 'cursor-pointer hover:ring-2 hover:ring-white hover:ring-offset-1 hover:ring-offset-black/50 transition-all'
|
||||
: ''
|
||||
}`}
|
||||
style={cardStyle}
|
||||
onClick={(e) => {
|
||||
if (onCardClick) {
|
||||
e.stopPropagation();
|
||||
onCardClick(index);
|
||||
{cards.map((card, index) => {
|
||||
// Build card container style with aspect ratio and dimensions
|
||||
const cardContainerStyle: CSSProperties = {
|
||||
...cardStyle,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
minWidth: '50px',
|
||||
// Use aspect-ratio from cardStyle, fallback to 4/3
|
||||
aspectRatio: cardStyle.aspectRatio || '4/3',
|
||||
// Use minHeight from cardStyle, fallback to 40px
|
||||
minHeight: cardStyle.minHeight || '40px',
|
||||
};
|
||||
|
||||
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'
|
||||
: ''
|
||||
}
|
||||
}}
|
||||
>
|
||||
{card.imageUrl && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={resolve(card.imageUrl)}
|
||||
alt={card.title || ''}
|
||||
className='absolute inset-0 w-full h-full object-cover'
|
||||
style={{ borderRadius: cardStyle.borderRadius }}
|
||||
draggable={false}
|
||||
/>
|
||||
)}
|
||||
{card.title && (
|
||||
<div className='absolute inset-0 flex items-center justify-center'>
|
||||
<span style={cardTitleStyle}>{card.title}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
style={cardContainerStyle}
|
||||
onClick={(e) => {
|
||||
if (onCardClick) {
|
||||
e.stopPropagation();
|
||||
onCardClick(index);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{card.imageUrl && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={resolve(card.imageUrl)}
|
||||
alt={card.title || ''}
|
||||
className='absolute inset-0 w-full h-full object-cover'
|
||||
style={{ borderRadius: cardContainerStyle.borderRadius }}
|
||||
draggable={false}
|
||||
/>
|
||||
)}
|
||||
{card.title && (
|
||||
<div className='absolute inset-0 flex items-center justify-center'>
|
||||
<span style={cardTitleStyle}>{card.title}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -69,12 +69,8 @@ const TooltipElement: React.FC<TooltipElementProps> = ({
|
||||
return (
|
||||
<div className={className} style={style}>
|
||||
<div className='p-3 max-w-[200px]'>
|
||||
<p style={titleFontStyle}>
|
||||
{element.tooltipTitle}
|
||||
</p>
|
||||
<p style={{ opacity: 0.7, ...textFontStyle }}>
|
||||
{element.tooltipText}
|
||||
</p>
|
||||
<p style={titleFontStyle}>{element.tooltipTitle}</p>
|
||||
<p style={{ opacity: 0.7, ...textFontStyle }}>{element.tooltipText}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -51,7 +51,8 @@ function validateAssetType(file, expectedType) {
|
||||
const hasExtensionMatch = extension && extensions.includes(extension);
|
||||
|
||||
if (!hasMimeMatch && !hasExtensionMatch) {
|
||||
const typeLabel = expectedType.charAt(0).toUpperCase() + expectedType.slice(1);
|
||||
const typeLabel =
|
||||
expectedType.charAt(0).toUpperCase() + expectedType.slice(1);
|
||||
return {
|
||||
valid: false,
|
||||
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) {
|
||||
const maxSizeMB = (schema.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
|
||||
|
||||
@ -363,8 +363,8 @@ export function useConstructorElements({
|
||||
update: (spanId: string, patch: Partial<GalleryInfoSpan>) => {
|
||||
if (!selectedElement || !isGalleryElementType(selectedElement.type))
|
||||
return;
|
||||
const nextSpans = (selectedElement.galleryInfoSpans || []).map((span) =>
|
||||
span.id === spanId ? { ...span, ...patch } : span,
|
||||
const nextSpans = (selectedElement.galleryInfoSpans || []).map(
|
||||
(span) => (span.id === spanId ? { ...span, ...patch } : span),
|
||||
);
|
||||
updateSelectedElement({ galleryInfoSpans: nextSpans });
|
||||
},
|
||||
|
||||
@ -630,7 +630,11 @@ export function usePreloadOrchestrator(
|
||||
const checkObject = (obj: Record<string, unknown>) => {
|
||||
if (!obj || typeof obj !== 'object') return;
|
||||
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);
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
checkObject(value as Record<string, unknown>);
|
||||
|
||||
@ -30,7 +30,9 @@ interface ProjectAssetInput {
|
||||
* Extracts storage paths from URLs (handles both formats) and resolves them
|
||||
* to presigned URLs for access.
|
||||
*/
|
||||
export function useProjectAssets(project: ProjectAssetInput | null): ProjectAssets {
|
||||
export function useProjectAssets(
|
||||
project: ProjectAssetInput | null,
|
||||
): ProjectAssets {
|
||||
const [assets, setAssets] = useState<ProjectAssets>({
|
||||
faviconUrl: null,
|
||||
ogImageUrl: null,
|
||||
@ -89,7 +91,12 @@ export function useProjectAssets(project: ProjectAssetInput | null): ProjectAsse
|
||||
// Reset loading state and resolve
|
||||
setAssets((prev) => ({ ...prev, isLoading: true }));
|
||||
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;
|
||||
}
|
||||
|
||||
@ -101,8 +101,7 @@ export type EffectPropName = (typeof EFFECT_PROPS)[number];
|
||||
export function buildTransitionStyle(
|
||||
effects: Partial<ElementEffectProperties>,
|
||||
): CSSProperties {
|
||||
const duration =
|
||||
normalizeDuration(effects.hoverTransitionDuration) || '0.2s';
|
||||
const duration = normalizeDuration(effects.hoverTransitionDuration) || '0.2s';
|
||||
return {
|
||||
transition: `all ${duration} ease`,
|
||||
};
|
||||
@ -228,8 +227,7 @@ export function buildAppearAnimationStyle(
|
||||
return {};
|
||||
}
|
||||
|
||||
const duration =
|
||||
normalizeDuration(effects.appearAnimationDuration) || '0.3s';
|
||||
const duration = normalizeDuration(effects.appearAnimationDuration) || '0.3s';
|
||||
const easing = effects.appearAnimationEasing || 'ease';
|
||||
|
||||
return {
|
||||
|
||||
@ -12,12 +12,20 @@ import { getFontByKey, getFontStyle } from './fonts';
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export const GALLERY_SECTION_DEFAULTS: Record<GallerySectionName, CSSProperties> = {
|
||||
export const GALLERY_SECTION_DEFAULTS: Record<
|
||||
GallerySectionName,
|
||||
CSSProperties
|
||||
> = {
|
||||
header: {
|
||||
fontSize: '1.5rem', // text-2xl
|
||||
fontWeight: '700', // font-bold
|
||||
@ -104,10 +112,12 @@ const normalizeWithUnit = (value: unknown, unit: string): string => {
|
||||
};
|
||||
|
||||
/** 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) */
|
||||
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
|
||||
@ -155,11 +165,27 @@ export function buildGalleryHeaderStyle(
|
||||
applyIfSet(style, 'backgroundColor', element.galleryHeaderBackgroundColor);
|
||||
// Inheritable properties: only apply if explicitly set (allows CSS inheritance from wrapper)
|
||||
applyIfSet(style, 'color', element.galleryHeaderColor);
|
||||
applyIfSet(style, 'fontSize', element.galleryHeaderFontSize, normalizeRemValue);
|
||||
applyIfSet(
|
||||
style,
|
||||
'fontSize',
|
||||
element.galleryHeaderFontSize,
|
||||
normalizeRemValue,
|
||||
);
|
||||
applyIfSet(style, 'fontWeight', element.galleryHeaderFontWeight);
|
||||
// Non-inheritable properties: use defaults
|
||||
applyWithDefault(style, 'padding', element.galleryHeaderPadding, defaults.padding, normalizeRemValue);
|
||||
applyIfSet(style, 'borderRadius', element.galleryHeaderBorderRadius, normalizeRemValue);
|
||||
applyWithDefault(
|
||||
style,
|
||||
'padding',
|
||||
element.galleryHeaderPadding,
|
||||
defaults.padding,
|
||||
normalizeRemValue,
|
||||
);
|
||||
applyIfSet(
|
||||
style,
|
||||
'borderRadius',
|
||||
element.galleryHeaderBorderRadius,
|
||||
normalizeRemValue,
|
||||
);
|
||||
applyIfSet(style, 'border', element.galleryHeaderBorder); // Complex value, no normalization
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@ -185,14 +227,36 @@ export function buildGalleryTitleStyle(
|
||||
const defaults = GALLERY_SECTION_DEFAULTS.title;
|
||||
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)
|
||||
applyIfSet(style, 'color', element.galleryTitleColor);
|
||||
applyIfSet(style, 'fontSize', element.galleryTitleFontSize, normalizeRemValue);
|
||||
applyIfSet(
|
||||
style,
|
||||
'fontSize',
|
||||
element.galleryTitleFontSize,
|
||||
normalizeRemValue,
|
||||
);
|
||||
applyIfSet(style, 'fontWeight', element.galleryTitleFontWeight);
|
||||
// Non-inheritable properties: use defaults
|
||||
applyWithDefault(style, 'padding', element.galleryTitlePadding, defaults.padding, normalizeRemValue);
|
||||
applyWithDefault(style, 'borderRadius', element.galleryTitleBorderRadius, defaults.borderRadius, normalizeRemValue);
|
||||
applyWithDefault(
|
||||
style,
|
||||
'padding',
|
||||
element.galleryTitlePadding,
|
||||
defaults.padding,
|
||||
normalizeRemValue,
|
||||
);
|
||||
applyWithDefault(
|
||||
style,
|
||||
'borderRadius',
|
||||
element.galleryTitleBorderRadius,
|
||||
defaults.borderRadius,
|
||||
normalizeRemValue,
|
||||
);
|
||||
applyIfSet(style, 'border', element.galleryTitleBorder); // Complex value, no normalization
|
||||
|
||||
// Apply font family with font library resolution
|
||||
@ -218,18 +282,36 @@ export function buildGallerySpanStyle(
|
||||
const defaults = GALLERY_SECTION_DEFAULTS.span;
|
||||
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)
|
||||
applyIfSet(style, 'color', element.gallerySpanColor);
|
||||
applyIfSet(style, 'fontSize', element.gallerySpanFontSize, normalizeRemValue);
|
||||
applyIfSet(style, 'fontWeight', element.gallerySpanFontWeight);
|
||||
// Non-inheritable properties: use defaults
|
||||
applyWithDefault(style, 'padding', element.gallerySpanPadding, defaults.padding, normalizeRemValue);
|
||||
applyWithDefault(style, 'borderRadius', element.gallerySpanBorderRadius, defaults.borderRadius, normalizeRemValue);
|
||||
applyWithDefault(
|
||||
style,
|
||||
'padding',
|
||||
element.gallerySpanPadding,
|
||||
defaults.padding,
|
||||
normalizeRemValue,
|
||||
);
|
||||
applyWithDefault(
|
||||
style,
|
||||
'borderRadius',
|
||||
element.gallerySpanBorderRadius,
|
||||
defaults.borderRadius,
|
||||
normalizeRemValue,
|
||||
);
|
||||
applyIfSet(style, 'border', element.gallerySpanBorder); // Complex value, no normalization
|
||||
|
||||
// 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) {
|
||||
const font = getFontByKey(fontKey);
|
||||
if (font) {
|
||||
@ -267,10 +349,32 @@ export function buildGalleryCardStyle(
|
||||
const defaults = GALLERY_SECTION_DEFAULTS.card;
|
||||
const style: CSSProperties = {};
|
||||
|
||||
applyWithDefault(style, 'backgroundColor', element.galleryCardBackgroundColor, undefined);
|
||||
applyWithDefault(style, 'borderRadius', element.galleryCardBorderRadius, defaults.borderRadius, normalizeRemValue);
|
||||
applyWithDefault(
|
||||
style,
|
||||
'backgroundColor',
|
||||
element.galleryCardBackgroundColor,
|
||||
undefined,
|
||||
);
|
||||
applyWithDefault(
|
||||
style,
|
||||
'borderRadius',
|
||||
element.galleryCardBorderRadius,
|
||||
defaults.borderRadius,
|
||||
normalizeRemValue,
|
||||
);
|
||||
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;
|
||||
}
|
||||
|
||||
@ -285,7 +389,12 @@ export function buildGalleryCardTitleStyle(
|
||||
// Inheritable properties: only apply if explicitly set (allows CSS inheritance from wrapper)
|
||||
// Note: card titles typically need white text for visibility over images
|
||||
applyIfSet(style, 'color', element.galleryCardTitleColor);
|
||||
applyIfSet(style, 'fontSize', element.galleryCardTitleFontSize, normalizeRemValue);
|
||||
applyIfSet(
|
||||
style,
|
||||
'fontSize',
|
||||
element.galleryCardTitleFontSize,
|
||||
normalizeRemValue,
|
||||
);
|
||||
applyIfSet(style, 'fontWeight', element.galleryCardTitleFontWeight);
|
||||
|
||||
if (element.galleryCardTitleBackgroundColor) {
|
||||
@ -298,7 +407,8 @@ export function buildGalleryCardTitleStyle(
|
||||
style.textShadow = shadow;
|
||||
} else {
|
||||
// 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
|
||||
@ -359,6 +469,10 @@ export const GALLERY_SECTION_STYLE_PROPS = [
|
||||
'galleryHeaderPadding',
|
||||
'galleryHeaderBorderRadius',
|
||||
'galleryHeaderBorder',
|
||||
'galleryHeaderWidth',
|
||||
'galleryHeaderHeight',
|
||||
'galleryHeaderMinHeight',
|
||||
'galleryHeaderMaxHeight',
|
||||
// Title
|
||||
'galleryTitleBackgroundColor',
|
||||
'galleryTitleColor',
|
||||
@ -390,6 +504,11 @@ export const GALLERY_SECTION_STYLE_PROPS = [
|
||||
'galleryCardTitleFontSize',
|
||||
'galleryCardTitleFontWeight',
|
||||
'galleryCardTitleShadow',
|
||||
'galleryCardWidth',
|
||||
'galleryCardHeight',
|
||||
'galleryCardMinHeight',
|
||||
'galleryCardAspectRatio',
|
||||
] as const;
|
||||
|
||||
export type GallerySectionStyleProp = (typeof GALLERY_SECTION_STYLE_PROPS)[number];
|
||||
export type GallerySectionStyleProp =
|
||||
(typeof GALLERY_SECTION_STYLE_PROPS)[number];
|
||||
|
||||
@ -283,15 +283,31 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
<meta property='og:url' content={url} />
|
||||
<meta property='og:site_name' content='https://flatlogic.com/' />
|
||||
<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 property='og:image:type' content='image/png' />
|
||||
<meta property='og:image:width' content={imageWidth} />
|
||||
<meta property='og:image:height' content={imageHeight} />
|
||||
<meta property='twitter:card' content='summary_large_image' />
|
||||
<meta key='twitter:title' 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
|
||||
key='twitter:title'
|
||||
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:height' content={imageHeight} />
|
||||
<link key='favicon' rel='icon' href='/favicon.svg' />
|
||||
|
||||
@ -1164,7 +1164,9 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
|
||||
// Update the active carousel element to reflect the new positions
|
||||
setActiveGalleryCarousel((prev) =>
|
||||
prev ? { ...prev, element: { ...prev.element, ...positionPatch } } : null,
|
||||
prev
|
||||
? { ...prev, element: { ...prev.element, ...positionPatch } }
|
||||
: null,
|
||||
);
|
||||
},
|
||||
[activeGalleryCarousel, updateSelectedElement],
|
||||
@ -1459,15 +1461,9 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
initialIndex={activeGalleryCarousel.initialIndex}
|
||||
onClose={() => setActiveGalleryCarousel(null)}
|
||||
resolveUrl={resolveUrlWithBlob}
|
||||
prevIconUrl={
|
||||
activeGalleryCarousel.element.galleryCarouselPrevIconUrl
|
||||
}
|
||||
nextIconUrl={
|
||||
activeGalleryCarousel.element.galleryCarouselNextIconUrl
|
||||
}
|
||||
backIconUrl={
|
||||
activeGalleryCarousel.element.galleryCarouselBackIconUrl
|
||||
}
|
||||
prevIconUrl={activeGalleryCarousel.element.galleryCarouselPrevIconUrl}
|
||||
nextIconUrl={activeGalleryCarousel.element.galleryCarouselNextIconUrl}
|
||||
backIconUrl={activeGalleryCarousel.element.galleryCarouselBackIconUrl}
|
||||
backLabel={
|
||||
activeGalleryCarousel.element.galleryCarouselBackLabel || 'BACK'
|
||||
}
|
||||
|
||||
@ -363,7 +363,9 @@ const ElementTypeDefaultDetailsPage = () => {
|
||||
<CarouselSettingsSection
|
||||
carouselPrevIconUrl={form.state.carouselPrevIconUrl}
|
||||
carouselNextIconUrl={form.state.carouselNextIconUrl}
|
||||
carouselCaptionFontFamily={form.state.carouselCaptionFontFamily}
|
||||
carouselCaptionFontFamily={
|
||||
form.state.carouselCaptionFontFamily
|
||||
}
|
||||
carouselSlides={form.state.carouselSlides}
|
||||
onAddSlide={form.addCarouselSlide}
|
||||
onRemoveSlide={form.removeCarouselSlide}
|
||||
|
||||
@ -548,7 +548,9 @@ const ProjectElementDefaultDetailsPage = () => {
|
||||
<CarouselSettingsSection
|
||||
carouselPrevIconUrl={form.state.carouselPrevIconUrl}
|
||||
carouselNextIconUrl={form.state.carouselNextIconUrl}
|
||||
carouselCaptionFontFamily={form.state.carouselCaptionFontFamily}
|
||||
carouselCaptionFontFamily={
|
||||
form.state.carouselCaptionFontFamily
|
||||
}
|
||||
carouselSlides={form.state.carouselSlides}
|
||||
onAddSlide={form.addCarouselSlide}
|
||||
onRemoveSlide={form.removeCarouselSlide}
|
||||
|
||||
@ -226,13 +226,18 @@ const EditProjectsPage = () => {
|
||||
: 'Select logo from Assets'}
|
||||
</option>
|
||||
{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*/, '')}
|
||||
</option>
|
||||
))}
|
||||
{values.logo_url &&
|
||||
!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}>
|
||||
{values.logo_url}
|
||||
@ -259,13 +264,18 @@ const EditProjectsPage = () => {
|
||||
: 'Select favicon from Assets logos'}
|
||||
</option>
|
||||
{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*/, '')}
|
||||
</option>
|
||||
))}
|
||||
{values.favicon_url &&
|
||||
!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}>
|
||||
{values.favicon_url}
|
||||
@ -292,13 +302,18 @@ const EditProjectsPage = () => {
|
||||
: 'Select OG image from Assets logos'}
|
||||
</option>
|
||||
{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*/, '')}
|
||||
</option>
|
||||
))}
|
||||
{values.og_image_url &&
|
||||
!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}>
|
||||
{values.og_image_url}
|
||||
|
||||
@ -110,6 +110,11 @@ export interface CanvasElement extends BaseCanvasElement {
|
||||
galleryHeaderPadding?: string;
|
||||
galleryHeaderBorderRadius?: string;
|
||||
galleryHeaderBorder?: string;
|
||||
// Gallery Section Styles - Header Dimensions
|
||||
galleryHeaderWidth?: string;
|
||||
galleryHeaderHeight?: string;
|
||||
galleryHeaderMinHeight?: string;
|
||||
galleryHeaderMaxHeight?: string;
|
||||
// Gallery Section Styles - Title
|
||||
galleryTitleBackgroundColor?: string;
|
||||
galleryTitleColor?: string;
|
||||
@ -140,6 +145,11 @@ export interface CanvasElement extends BaseCanvasElement {
|
||||
galleryCardTitleFontSize?: string;
|
||||
galleryCardTitleFontWeight?: string;
|
||||
galleryCardTitleShadow?: string;
|
||||
// Gallery Section Styles - Card Dimensions
|
||||
galleryCardWidth?: string;
|
||||
galleryCardHeight?: string;
|
||||
galleryCardMinHeight?: string;
|
||||
galleryCardAspectRatio?: string;
|
||||
// Gallery Section Styles - Wrapper
|
||||
galleryWrapperBackgroundColor?: string;
|
||||
galleryWrapperPadding?: string;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user