added additional settings for gallery, made structure more flexible
This commit is contained in:
parent
73f524dab3
commit
8ef30576b1
File diff suppressed because one or more lines are too long
@ -17,6 +17,7 @@ import {
|
||||
GallerySettingsSectionCompact,
|
||||
CarouselSettingsSectionCompact,
|
||||
GalleryCarouselSettingsSectionCompact,
|
||||
GallerySectionStyleInputs,
|
||||
extractNumericValue,
|
||||
} from '../ElementSettings';
|
||||
import BackgroundSettingsEditor from './BackgroundSettingsEditor';
|
||||
@ -132,7 +133,7 @@ interface ElementEditorPanelProps {
|
||||
};
|
||||
galleryInfoSpans: {
|
||||
add: () => void;
|
||||
update: (spanId: string, text: string) => void;
|
||||
update: (spanId: string, patch: Partial<GalleryInfoSpan>) => void;
|
||||
remove: (spanId: string) => void;
|
||||
};
|
||||
carouselSlides: {
|
||||
@ -502,15 +503,9 @@ export function ElementEditorPanel({
|
||||
galleryInfoSpans={
|
||||
selectedElement.galleryInfoSpans || []
|
||||
}
|
||||
galleryColumns={selectedElement.galleryColumns || 3}
|
||||
galleryTitleFontFamily={
|
||||
selectedElement.galleryTitleFontFamily || ''
|
||||
}
|
||||
galleryTextFontFamily={
|
||||
selectedElement.galleryTextFontFamily || ''
|
||||
}
|
||||
galleryCards={selectedElement.galleryCards || []}
|
||||
imageAssetOptions={imageAssetOptions}
|
||||
iconAssetOptions={iconAssetOptions}
|
||||
onUpdateHeader={(patch) => onUpdateElement(patch)}
|
||||
onAddInfoSpan={galleryInfoSpans.add}
|
||||
onUpdateInfoSpan={galleryInfoSpans.update}
|
||||
@ -581,6 +576,141 @@ export function ElementEditorPanel({
|
||||
)}
|
||||
|
||||
{/* CSS Styles Tab */}
|
||||
{activeTab === 'css' && (
|
||||
<>
|
||||
{/* Gallery Section Styles (shown first for gallery elements) */}
|
||||
{isGalleryElementType(selectedElement.type) && (
|
||||
<div className='space-y-2 mb-4'>
|
||||
<p className='text-[11px] font-semibold text-gray-700'>
|
||||
Gallery Section Styles
|
||||
</p>
|
||||
<GallerySectionStyleInputs
|
||||
sectionLabel='Header'
|
||||
prefix='galleryHeader'
|
||||
values={{
|
||||
galleryHeaderBackgroundColor:
|
||||
selectedElement.galleryHeaderBackgroundColor || '',
|
||||
galleryHeaderColor:
|
||||
selectedElement.galleryHeaderColor || '',
|
||||
galleryHeaderFontFamily:
|
||||
selectedElement.galleryHeaderFontFamily || '',
|
||||
galleryHeaderFontSize:
|
||||
selectedElement.galleryHeaderFontSize || '',
|
||||
galleryHeaderFontWeight:
|
||||
selectedElement.galleryHeaderFontWeight || '',
|
||||
galleryHeaderPadding:
|
||||
selectedElement.galleryHeaderPadding || '',
|
||||
galleryHeaderBorderRadius:
|
||||
selectedElement.galleryHeaderBorderRadius || '',
|
||||
galleryHeaderBorder:
|
||||
selectedElement.galleryHeaderBorder || '',
|
||||
}}
|
||||
onChange={(prop, value) =>
|
||||
onUpdateElement({ [prop]: value || undefined })
|
||||
}
|
||||
showFont
|
||||
/>
|
||||
<GallerySectionStyleInputs
|
||||
sectionLabel='Title'
|
||||
prefix='galleryTitle'
|
||||
values={{
|
||||
galleryTitleBackgroundColor:
|
||||
selectedElement.galleryTitleBackgroundColor || '',
|
||||
galleryTitleColor:
|
||||
selectedElement.galleryTitleColor || '',
|
||||
galleryTitleFontFamily:
|
||||
selectedElement.galleryTitleFontFamily || '',
|
||||
galleryTitleFontSize:
|
||||
selectedElement.galleryTitleFontSize || '',
|
||||
galleryTitleFontWeight:
|
||||
selectedElement.galleryTitleFontWeight || '',
|
||||
galleryTitlePadding:
|
||||
selectedElement.galleryTitlePadding || '',
|
||||
galleryTitleBorderRadius:
|
||||
selectedElement.galleryTitleBorderRadius || '',
|
||||
galleryTitleBorder:
|
||||
selectedElement.galleryTitleBorder || '',
|
||||
}}
|
||||
onChange={(prop, value) =>
|
||||
onUpdateElement({ [prop]: value || undefined })
|
||||
}
|
||||
showFont
|
||||
/>
|
||||
<GallerySectionStyleInputs
|
||||
sectionLabel='Info Spans'
|
||||
prefix='gallerySpan'
|
||||
values={{
|
||||
gallerySpanBackgroundColor:
|
||||
selectedElement.gallerySpanBackgroundColor || '',
|
||||
gallerySpanColor:
|
||||
selectedElement.gallerySpanColor || '',
|
||||
gallerySpanFontFamily:
|
||||
selectedElement.gallerySpanFontFamily || '',
|
||||
gallerySpanFontSize:
|
||||
selectedElement.gallerySpanFontSize || '',
|
||||
gallerySpanFontWeight:
|
||||
selectedElement.gallerySpanFontWeight || '',
|
||||
gallerySpanPadding:
|
||||
selectedElement.gallerySpanPadding || '',
|
||||
gallerySpanBorderRadius:
|
||||
selectedElement.gallerySpanBorderRadius || '',
|
||||
gallerySpanBorder:
|
||||
selectedElement.gallerySpanBorder || '',
|
||||
gallerySpanGap: selectedElement.gallerySpanGap || '',
|
||||
gallerySpanColumns:
|
||||
selectedElement.gallerySpanColumns ||
|
||||
selectedElement.galleryColumns ||
|
||||
3,
|
||||
}}
|
||||
onChange={(prop, value) =>
|
||||
onUpdateElement({ [prop]: value || undefined })
|
||||
}
|
||||
showFont
|
||||
showGap
|
||||
showColumns
|
||||
/>
|
||||
<GallerySectionStyleInputs
|
||||
sectionLabel='Image Cards'
|
||||
prefix='galleryCard'
|
||||
values={{
|
||||
galleryCardBackgroundColor:
|
||||
selectedElement.galleryCardBackgroundColor || '',
|
||||
galleryCardBorderRadius:
|
||||
selectedElement.galleryCardBorderRadius || '',
|
||||
galleryCardBorder:
|
||||
selectedElement.galleryCardBorder || '',
|
||||
galleryCardGap: selectedElement.galleryCardGap || '',
|
||||
galleryCardColumns:
|
||||
selectedElement.galleryCardColumns ||
|
||||
selectedElement.galleryColumns ||
|
||||
3,
|
||||
galleryCardTitleColor:
|
||||
selectedElement.galleryCardTitleColor || '',
|
||||
galleryCardTitleBackgroundColor:
|
||||
selectedElement.galleryCardTitleBackgroundColor ||
|
||||
'',
|
||||
galleryCardTitleFontSize:
|
||||
selectedElement.galleryCardTitleFontSize || '',
|
||||
galleryCardTitleFontWeight:
|
||||
selectedElement.galleryCardTitleFontWeight || '',
|
||||
galleryCardTitleShadow:
|
||||
selectedElement.galleryCardTitleShadow || '',
|
||||
}}
|
||||
onChange={(prop, value) =>
|
||||
onUpdateElement({ [prop]: value || undefined })
|
||||
}
|
||||
showGap
|
||||
showColumns
|
||||
showTitleStyles
|
||||
/>
|
||||
<p className='text-[11px] font-semibold text-gray-700 pt-2'>
|
||||
General Element Styles
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === 'css' && (
|
||||
<StyleSettingsSectionCompact
|
||||
values={{
|
||||
|
||||
@ -0,0 +1,244 @@
|
||||
/**
|
||||
* GallerySectionStyleInputs
|
||||
*
|
||||
* Reusable component for gallery section style inputs.
|
||||
* Used in the CSS tab to configure wrapper, header, title, span, and card styles.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { FONT_OPTIONS } from '../../lib/fonts';
|
||||
|
||||
interface GallerySectionStyleInputsProps {
|
||||
sectionLabel: string;
|
||||
prefix: string;
|
||||
values: Record<string, string | number>;
|
||||
onChange: (prop: string, value: string | number) => void;
|
||||
showFont?: boolean;
|
||||
showColumns?: boolean;
|
||||
showGap?: boolean;
|
||||
showBlur?: boolean;
|
||||
showTitleStyles?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable inputs for gallery section styling
|
||||
*/
|
||||
const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
|
||||
sectionLabel,
|
||||
prefix,
|
||||
values,
|
||||
onChange,
|
||||
showFont = false,
|
||||
showColumns = false,
|
||||
showGap = false,
|
||||
showBlur = false,
|
||||
showTitleStyles = false,
|
||||
}) => {
|
||||
return (
|
||||
<div className='rounded border border-gray-200 p-2 space-y-2'>
|
||||
<p className='text-[11px] font-semibold text-gray-700'>{sectionLabel}</p>
|
||||
|
||||
<div className='grid grid-cols-2 gap-2'>
|
||||
{/* Background Color */}
|
||||
<div>
|
||||
<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)}
|
||||
placeholder='rgba(0,0,0,0.6)'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Text Color (not for wrapper) */}
|
||||
{prefix !== 'galleryWrapper' && !showTitleStyles && (
|
||||
<div>
|
||||
<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`] || ''}
|
||||
onChange={(e) => onChange(`${prefix}Color`, e.target.value)}
|
||||
placeholder='#ffffff'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Padding */}
|
||||
<div>
|
||||
<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`] || ''}
|
||||
onChange={(e) => onChange(`${prefix}Padding`, e.target.value)}
|
||||
placeholder='0.75rem'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Border Radius */}
|
||||
<div>
|
||||
<label className='mb-1 block text-[10px] text-gray-600'>Radius</label>
|
||||
<input
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
value={values[`${prefix}BorderRadius`] || ''}
|
||||
onChange={(e) => onChange(`${prefix}BorderRadius`, e.target.value)}
|
||||
placeholder='0.75rem'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Border */}
|
||||
<div>
|
||||
<label className='mb-1 block text-[10px] text-gray-600'>Border</label>
|
||||
<input
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
value={values[`${prefix}Border`] || ''}
|
||||
onChange={(e) => onChange(`${prefix}Border`, e.target.value)}
|
||||
placeholder='1px solid #ccc'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Gap (optional) */}
|
||||
{showGap && (
|
||||
<div>
|
||||
<label className='mb-1 block text-[10px] text-gray-600'>Gap</label>
|
||||
<input
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
value={values[`${prefix}Gap`] || ''}
|
||||
onChange={(e) => onChange(`${prefix}Gap`, e.target.value)}
|
||||
placeholder='0.5rem'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Backdrop Blur (wrapper only) */}
|
||||
{showBlur && (
|
||||
<div>
|
||||
<label className='mb-1 block text-[10px] text-gray-600'>Blur</label>
|
||||
<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)}
|
||||
placeholder='4px'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Grid Columns (optional) */}
|
||||
{showColumns && (
|
||||
<div>
|
||||
<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)}
|
||||
placeholder='3'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Font Size (optional) */}
|
||||
{showFont && (
|
||||
<div>
|
||||
<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`] || ''}
|
||||
onChange={(e) => onChange(`${prefix}FontSize`, e.target.value)}
|
||||
placeholder='0.875rem'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Font Weight (optional) */}
|
||||
{showFont && (
|
||||
<div>
|
||||
<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`] || ''}
|
||||
onChange={(e) => onChange(`${prefix}FontWeight`, e.target.value)}
|
||||
placeholder='500'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Font Family (optional - full width) */}
|
||||
{showFont && (
|
||||
<div>
|
||||
<label className='mb-1 block text-[10px] text-gray-600'>Font</label>
|
||||
<select
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
value={values[`${prefix}FontFamily`] || ''}
|
||||
onChange={(e) => onChange(`${prefix}FontFamily`, e.target.value)}
|
||||
>
|
||||
<option value=''>Not set</option>
|
||||
{FONT_OPTIONS.map((font) => (
|
||||
<option key={font.key} value={font.key}>
|
||||
{font.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</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>
|
||||
<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)}
|
||||
placeholder='#ffffff'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<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)}
|
||||
placeholder='transparent'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<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)}
|
||||
placeholder='0.75rem'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<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)}
|
||||
placeholder='700'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<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)}
|
||||
placeholder='0 1px 3px rgba(0,0,0,0.5)'
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GallerySectionStyleInputs;
|
||||
@ -3,6 +3,7 @@
|
||||
*
|
||||
* Compact gallery element settings for constructor sidebar.
|
||||
* Header image, title, info spans, and card management.
|
||||
* Note: Fonts, columns, and styling are now in the CSS tab.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
@ -12,7 +13,6 @@ import type {
|
||||
AssetOption,
|
||||
} from '../../types/constructor';
|
||||
import { addFallbackAssetOption } from '../../lib/constructorHelpers';
|
||||
import { FONT_OPTIONS } from '../../lib/fonts';
|
||||
|
||||
interface GallerySettingsSectionCompactProps {
|
||||
// Header settings
|
||||
@ -20,24 +20,18 @@ interface GallerySettingsSectionCompactProps {
|
||||
galleryHeaderText: string;
|
||||
galleryTitle: string;
|
||||
galleryInfoSpans: GalleryInfoSpan[];
|
||||
galleryColumns: number;
|
||||
// Font settings
|
||||
galleryTitleFontFamily: string;
|
||||
galleryTextFontFamily: string;
|
||||
// Cards
|
||||
galleryCards: GalleryCard[];
|
||||
imageAssetOptions: AssetOption[];
|
||||
iconAssetOptions: AssetOption[];
|
||||
// Header handlers
|
||||
onUpdateHeader: (patch: {
|
||||
galleryHeaderImageUrl?: string;
|
||||
galleryHeaderText?: string;
|
||||
galleryTitle?: string;
|
||||
galleryColumns?: number;
|
||||
galleryTitleFontFamily?: string;
|
||||
galleryTextFontFamily?: string;
|
||||
}) => void;
|
||||
onAddInfoSpan: () => void;
|
||||
onUpdateInfoSpan: (spanId: string, text: string) => void;
|
||||
onUpdateInfoSpan: (spanId: string, patch: Partial<GalleryInfoSpan>) => void;
|
||||
onRemoveInfoSpan: (spanId: string) => void;
|
||||
// Card handlers
|
||||
onAddCard: () => void;
|
||||
@ -52,11 +46,9 @@ const GallerySettingsSectionCompact: React.FC<
|
||||
galleryHeaderText,
|
||||
galleryTitle,
|
||||
galleryInfoSpans,
|
||||
galleryColumns,
|
||||
galleryTitleFontFamily,
|
||||
galleryTextFontFamily,
|
||||
galleryCards,
|
||||
imageAssetOptions,
|
||||
iconAssetOptions,
|
||||
onUpdateHeader,
|
||||
onAddInfoSpan,
|
||||
onUpdateInfoSpan,
|
||||
@ -107,58 +99,6 @@ const GallerySettingsSectionCompact: React.FC<
|
||||
onUpdateHeader({ galleryTitle: event.target.value })
|
||||
}
|
||||
/>
|
||||
|
||||
<div className='flex items-center gap-2'>
|
||||
<label className='text-[10px] text-gray-600'>Grid columns:</label>
|
||||
<input
|
||||
type='number'
|
||||
min='1'
|
||||
max='6'
|
||||
className='w-16 rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
value={galleryColumns}
|
||||
onChange={(event) =>
|
||||
onUpdateHeader({
|
||||
galleryColumns: Math.max(1, Math.min(6, parseInt(event.target.value) || 3)),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className='text-[10px] text-gray-600'>Title font:</label>
|
||||
<select
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
value={galleryTitleFontFamily}
|
||||
onChange={(event) =>
|
||||
onUpdateHeader({ galleryTitleFontFamily: event.target.value })
|
||||
}
|
||||
>
|
||||
<option value=''>Not set</option>
|
||||
{FONT_OPTIONS.map((font) => (
|
||||
<option key={font.key} value={font.key}>
|
||||
{font.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className='text-[10px] text-gray-600'>Text font:</label>
|
||||
<select
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
value={galleryTextFontFamily}
|
||||
onChange={(event) =>
|
||||
onUpdateHeader({ galleryTextFontFamily: event.target.value })
|
||||
}
|
||||
>
|
||||
<option value=''>Not set</option>
|
||||
{FONT_OPTIONS.map((font) => (
|
||||
<option key={font.key} value={font.key}>
|
||||
{font.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Spans */}
|
||||
@ -175,26 +115,48 @@ const GallerySettingsSectionCompact: React.FC<
|
||||
</div>
|
||||
|
||||
{galleryInfoSpans.map((span, index) => (
|
||||
<div key={span.id} className='flex items-center gap-1'>
|
||||
<input
|
||||
className='flex-1 rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
placeholder={`Span ${index + 1}`}
|
||||
value={span.text}
|
||||
onChange={(event) => onUpdateInfoSpan(span.id, event.target.value)}
|
||||
/>
|
||||
<button
|
||||
type='button'
|
||||
className='text-xs text-red-600 hover:underline px-1'
|
||||
onClick={() => onRemoveInfoSpan(span.id)}
|
||||
<div key={span.id} className='space-y-1'>
|
||||
<div className='flex items-center gap-1'>
|
||||
<input
|
||||
className='flex-1 rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
placeholder={`Span ${index + 1} text`}
|
||||
value={span.text}
|
||||
onChange={(event) =>
|
||||
onUpdateInfoSpan(span.id, { text: event.target.value })
|
||||
}
|
||||
/>
|
||||
<button
|
||||
type='button'
|
||||
className='text-xs text-red-600 hover:underline px-1'
|
||||
onClick={() => onRemoveInfoSpan(span.id)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<select
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
value={span.iconUrl || ''}
|
||||
onChange={(event) =>
|
||||
onUpdateInfoSpan(span.id, { iconUrl: event.target.value })
|
||||
}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<option value=''>No icon (use text)</option>
|
||||
{addFallbackAssetOption(
|
||||
iconAssetOptions,
|
||||
span.iconUrl,
|
||||
`Current icon`,
|
||||
).map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{galleryInfoSpans.length === 0 && (
|
||||
<p className='text-[10px] text-gray-500'>
|
||||
Add spans for brief notes (capacity, price, etc.)
|
||||
Add spans for brief notes (capacity, price, icons, etc.)
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -25,6 +25,7 @@ export { default as MediaSettingsSection } from './MediaSettingsSection';
|
||||
export { default as MediaSettingsSectionCompact } from './MediaSettingsSectionCompact';
|
||||
export { default as GallerySettingsSection } from './GallerySettingsSection';
|
||||
export { default as GallerySettingsSectionCompact } from './GallerySettingsSectionCompact';
|
||||
export { default as GallerySectionStyleInputs } from './GallerySectionStyleInputs';
|
||||
export { default as CarouselSettingsSection } from './CarouselSettingsSection';
|
||||
export { default as CarouselSettingsSectionCompact } from './CarouselSettingsSectionCompact';
|
||||
export { default as GalleryCarouselSettingsSectionCompact } from './GalleryCarouselSettingsSectionCompact';
|
||||
|
||||
@ -65,33 +65,41 @@ const DescriptionElement: React.FC<DescriptionElementProps> = ({
|
||||
|
||||
// Without icon: render styled text description
|
||||
// Background color is controlled via CSS Styles tab (backgroundColor property)
|
||||
// fontWeight from CSS Styles tab is applied to both title and text
|
||||
const fontWeight = element.fontWeight || 'bold';
|
||||
// Inheritable styles (color, fontSize, fontWeight) cascade from General Element Styles
|
||||
// when section-specific values are not set
|
||||
|
||||
// Build title style - only set properties if explicitly configured
|
||||
// fontWeight cascades from General Element Styles via CSS inheritance
|
||||
const titleStyle: CSSProperties = {
|
||||
...titleFontStyle,
|
||||
};
|
||||
if (element.descriptionTitleFontSize) {
|
||||
titleStyle.fontSize = normalizePixelValue(element.descriptionTitleFontSize);
|
||||
}
|
||||
if (element.descriptionTitleColor) {
|
||||
titleStyle.color = element.descriptionTitleColor;
|
||||
}
|
||||
|
||||
// Build text style - only set properties if explicitly configured
|
||||
// fontWeight cascades from General Element Styles via CSS inheritance
|
||||
const textStyle: CSSProperties = {
|
||||
...textFontStyle,
|
||||
};
|
||||
if (element.descriptionTextFontSize) {
|
||||
textStyle.fontSize = normalizePixelValue(element.descriptionTextFontSize);
|
||||
}
|
||||
if (element.descriptionTextColor) {
|
||||
textStyle.color = element.descriptionTextColor;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className} style={style}>
|
||||
<div className='p-4'>
|
||||
<p
|
||||
style={{
|
||||
fontSize:
|
||||
normalizePixelValue(element.descriptionTitleFontSize) || '24px',
|
||||
color: element.descriptionTitleColor || '#ffffff',
|
||||
fontWeight,
|
||||
...titleFontStyle,
|
||||
}}
|
||||
>
|
||||
<p style={titleStyle}>
|
||||
{element.descriptionTitle || ''}
|
||||
</p>
|
||||
{element.descriptionText && (
|
||||
<p
|
||||
style={{
|
||||
fontSize:
|
||||
normalizePixelValue(element.descriptionTextFontSize) || '16px',
|
||||
color: element.descriptionTextColor || '#ffffff',
|
||||
fontWeight,
|
||||
...textFontStyle,
|
||||
}}
|
||||
>
|
||||
<p style={textStyle}>
|
||||
{element.descriptionText}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
* GalleryElement Component
|
||||
*
|
||||
* Gallery element with header image, title, info spans, and grid of image cards.
|
||||
* Renders with unified wrapper styling + content.
|
||||
* Renders with unified wrapper styling + content using section-specific styles.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
@ -13,7 +13,16 @@ import type {
|
||||
GalleryInfoSpan,
|
||||
} from '../../../types/constructor';
|
||||
import { resolveAssetPlaybackUrl } from '../../../lib/assetUrl';
|
||||
import { getFontByKey, getFontStyle } from '../../../lib/fonts';
|
||||
import {
|
||||
buildGalleryHeaderStyle,
|
||||
buildGalleryTitleStyle,
|
||||
buildGallerySpanStyle,
|
||||
buildGallerySpanGridStyle,
|
||||
buildGalleryCardStyle,
|
||||
buildGalleryCardTitleStyle,
|
||||
buildGalleryCardGridStyle,
|
||||
GALLERY_SECTION_DEFAULTS,
|
||||
} from '../../../lib/gallerySectionStyles';
|
||||
|
||||
interface GalleryElementProps {
|
||||
element: CanvasElement;
|
||||
@ -36,31 +45,59 @@ const GalleryElement: React.FC<GalleryElementProps> = ({
|
||||
const headerImageUrl = element.galleryHeaderImageUrl;
|
||||
const headerText = element.galleryHeaderText;
|
||||
const title = element.galleryTitle;
|
||||
const columns = element.galleryColumns || 3;
|
||||
|
||||
// Resolve font keys to full CSS styles (including fontStretch for condensed variants)
|
||||
const titleFontStyle = useMemo(() => {
|
||||
const fontKey = element.galleryTitleFontFamily;
|
||||
if (!fontKey) return {};
|
||||
const font = getFontByKey(fontKey);
|
||||
return font ? getFontStyle(font) : { fontFamily: fontKey };
|
||||
}, [element.galleryTitleFontFamily]);
|
||||
// Build section styles from element properties
|
||||
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 cardStyle = useMemo(() => buildGalleryCardStyle(element), [element]);
|
||||
const cardTitleStyle = useMemo(() => buildGalleryCardTitleStyle(element), [element]);
|
||||
const cardGridStyle = useMemo(() => buildGalleryCardGridStyle(element), [element]);
|
||||
|
||||
const textFontStyle = useMemo(() => {
|
||||
const fontKey = element.galleryTextFontFamily;
|
||||
if (!fontKey) return {};
|
||||
const font = getFontByKey(fontKey);
|
||||
return font ? getFontStyle(font) : { fontFamily: fontKey };
|
||||
}, [element.galleryTextFontFamily]);
|
||||
// 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]);
|
||||
|
||||
// Extract backgroundColor from style to apply to inner content wrapper
|
||||
const { backgroundColor, ...outerStyle } = style;
|
||||
// Extract wrapper-related styles from outer style (they go to wrapper, not outer div)
|
||||
const {
|
||||
backgroundColor: _bg,
|
||||
padding: _pad,
|
||||
borderRadius: _radius,
|
||||
border: _border,
|
||||
gap: _gap,
|
||||
boxShadow: _shadow,
|
||||
opacity: _opacity,
|
||||
color: _color,
|
||||
fontSize: _fontSize,
|
||||
fontWeight: _fontWeight,
|
||||
lineHeight: _lineHeight,
|
||||
...outerStyle
|
||||
} = style;
|
||||
|
||||
return (
|
||||
<div className={className} style={outerStyle}>
|
||||
<div
|
||||
className='flex flex-col gap-2 p-3 rounded-xl min-w-[200px] backdrop-blur-sm'
|
||||
style={{ backgroundColor: backgroundColor || 'rgba(0, 0, 0, 0.6)' }}
|
||||
className='min-w-[200px]'
|
||||
style={wrapperStyle}
|
||||
>
|
||||
{/* Header: image takes priority, otherwise render text */}
|
||||
{headerImageUrl ? (
|
||||
@ -72,56 +109,60 @@ const GalleryElement: React.FC<GalleryElementProps> = ({
|
||||
draggable={false}
|
||||
/>
|
||||
) : headerText ? (
|
||||
<div
|
||||
className='text-2xl font-bold px-1 py-2'
|
||||
style={titleFontStyle}
|
||||
>
|
||||
{headerText}
|
||||
</div>
|
||||
<div style={headerStyle}>{headerText}</div>
|
||||
) : null}
|
||||
|
||||
{/* Title */}
|
||||
{title && (
|
||||
<div
|
||||
className='bg-amber-50 text-slate-800 text-center py-2 px-3 rounded-lg font-semibold text-sm'
|
||||
style={titleFontStyle}
|
||||
>
|
||||
<div className='text-center' style={titleStyle}>
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Info spans */}
|
||||
{infoSpans.length > 0 && (
|
||||
<div
|
||||
className='grid gap-2'
|
||||
style={{ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` }}
|
||||
>
|
||||
{infoSpans.map((span) => (
|
||||
<div
|
||||
key={span.id}
|
||||
className='bg-slate-700 text-amber-50 text-center py-2 px-2 rounded-lg text-xs font-medium'
|
||||
style={textFontStyle}
|
||||
>
|
||||
{span.text}
|
||||
</div>
|
||||
))}
|
||||
<div style={spanGridStyle}>
|
||||
{infoSpans.map((span) => {
|
||||
// For icon spans, remove padding so image fills container
|
||||
const spanItemStyle = span.iconUrl
|
||||
? { ...spanStyle, padding: 0, overflow: 'hidden' }
|
||||
: spanStyle;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={span.id}
|
||||
className='text-center flex items-center justify-center'
|
||||
style={spanItemStyle}
|
||||
>
|
||||
{span.iconUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={resolve(span.iconUrl)}
|
||||
alt=''
|
||||
className='w-full h-full object-cover'
|
||||
draggable={false}
|
||||
/>
|
||||
) : (
|
||||
span.text
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Gallery cards */}
|
||||
{cards.length > 0 && (
|
||||
<div
|
||||
className='grid gap-2 w-full'
|
||||
style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}
|
||||
>
|
||||
<div style={cardGridStyle}>
|
||||
{cards.map((card, index) => (
|
||||
<div
|
||||
key={card.id}
|
||||
className={`relative aspect-[4/3] min-w-[50px] min-h-[40px] ${
|
||||
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();
|
||||
@ -134,18 +175,14 @@ const GalleryElement: React.FC<GalleryElementProps> = ({
|
||||
<img
|
||||
src={resolve(card.imageUrl)}
|
||||
alt={card.title || ''}
|
||||
className='absolute inset-0 w-full h-full object-cover rounded-lg'
|
||||
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
|
||||
className='text-white text-xs font-bold drop-shadow-lg'
|
||||
style={textFontStyle}
|
||||
>
|
||||
{card.title}
|
||||
</span>
|
||||
<span style={cardTitleStyle}>{card.title}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -63,13 +63,16 @@ const TooltipElement: React.FC<TooltipElementProps> = ({
|
||||
}
|
||||
|
||||
// Without icon: render text tooltip
|
||||
// Inheritable styles (color, fontSize, fontWeight) cascade from General Element Styles
|
||||
// Font family can be set per-section via tooltipTitleFontFamily/tooltipTextFontFamily
|
||||
|
||||
return (
|
||||
<div className={className} style={style}>
|
||||
<div className='p-3 max-w-[200px]'>
|
||||
<p className='font-bold text-sm' style={titleFontStyle}>
|
||||
<p style={titleFontStyle}>
|
||||
{element.tooltipTitle}
|
||||
</p>
|
||||
<p className='text-xs opacity-70' style={textFontStyle}>
|
||||
<p style={{ opacity: 0.7, ...textFontStyle }}>
|
||||
{element.tooltipText}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -87,7 +87,7 @@ interface UseConstructorElementsResult {
|
||||
/** Gallery info span operations */
|
||||
galleryInfoSpans: {
|
||||
add: () => void;
|
||||
update: (spanId: string, text: string) => void;
|
||||
update: (spanId: string, patch: Partial<GalleryInfoSpan>) => void;
|
||||
remove: (spanId: string) => void;
|
||||
};
|
||||
/** Carousel slide operations */
|
||||
@ -360,11 +360,11 @@ export function useConstructorElements({
|
||||
];
|
||||
updateSelectedElement({ galleryInfoSpans: nextSpans });
|
||||
},
|
||||
update: (spanId: string, text: string) => {
|
||||
update: (spanId: string, patch: Partial<GalleryInfoSpan>) => {
|
||||
if (!selectedElement || !isGalleryElementType(selectedElement.type))
|
||||
return;
|
||||
const nextSpans = (selectedElement.galleryInfoSpans || []).map((span) =>
|
||||
span.id === spanId ? { ...span, text } : span,
|
||||
span.id === spanId ? { ...span, ...patch } : span,
|
||||
);
|
||||
updateSelectedElement({ galleryInfoSpans: nextSpans });
|
||||
},
|
||||
|
||||
@ -9,10 +9,12 @@ import type {
|
||||
CanvasElement,
|
||||
CanvasElementType,
|
||||
GalleryCard,
|
||||
GalleryInfoSpan,
|
||||
CarouselSlide,
|
||||
NavigationButtonKind,
|
||||
} from '../types/constructor';
|
||||
import { ELEMENT_STYLE_PROPS } from './elementStyles';
|
||||
import { GALLERY_SECTION_STYLE_PROPS } from './gallerySectionStyles';
|
||||
|
||||
/**
|
||||
* Generate a local unique ID for elements
|
||||
@ -230,6 +232,17 @@ export const normalizeGalleryCard = (
|
||||
description: String(card?.description || ''),
|
||||
});
|
||||
|
||||
/**
|
||||
* Normalize a gallery info span from unknown input
|
||||
*/
|
||||
export const normalizeGalleryInfoSpan = (
|
||||
span: Record<string, unknown>,
|
||||
): GalleryInfoSpan => ({
|
||||
id: String(span?.id || createLocalId()),
|
||||
text: String(span?.text || ''),
|
||||
iconUrl: span?.iconUrl ? String(span.iconUrl) : undefined,
|
||||
});
|
||||
|
||||
/**
|
||||
* Normalize a carousel slide from unknown input
|
||||
*/
|
||||
@ -313,6 +326,18 @@ export const mergeElementWithDefaults = (
|
||||
merged.galleryCards = cards.map((card, i) =>
|
||||
normalizeGalleryCard(card as unknown as Record<string, unknown>, i),
|
||||
);
|
||||
|
||||
// Handle gallery info spans array
|
||||
const spans = preferElementValues
|
||||
? Array.isArray(element.galleryInfoSpans)
|
||||
? element.galleryInfoSpans
|
||||
: defaults.galleryInfoSpans || []
|
||||
: Array.isArray(defaults.galleryInfoSpans)
|
||||
? defaults.galleryInfoSpans
|
||||
: element.galleryInfoSpans || [];
|
||||
merged.galleryInfoSpans = spans.map((span) =>
|
||||
normalizeGalleryInfoSpan(span as unknown as Record<string, unknown>),
|
||||
);
|
||||
}
|
||||
|
||||
// Handle carousel slides array
|
||||
@ -360,6 +385,13 @@ export const parseElementSettings = (
|
||||
);
|
||||
}
|
||||
|
||||
// Parse gallery info spans if present
|
||||
if (Array.isArray(settings.galleryInfoSpans)) {
|
||||
settings.galleryInfoSpans = settings.galleryInfoSpans.map((span) =>
|
||||
normalizeGalleryInfoSpan(span as Record<string, unknown>),
|
||||
);
|
||||
}
|
||||
|
||||
// Parse carousel slides if present
|
||||
if (Array.isArray(settings.carouselSlides)) {
|
||||
settings.carouselSlides = settings.carouselSlides.map((slide, i) =>
|
||||
@ -525,6 +557,13 @@ export const buildElementSettings = (
|
||||
description: card.description || '',
|
||||
}));
|
||||
}
|
||||
if (Array.isArray(element.galleryInfoSpans)) {
|
||||
settings.galleryInfoSpans = element.galleryInfoSpans.map((span) => ({
|
||||
id: String(span.id || createLocalId()),
|
||||
text: span.text || '',
|
||||
iconUrl: span.iconUrl || undefined,
|
||||
}));
|
||||
}
|
||||
addIfNotEmpty(
|
||||
settings,
|
||||
'galleryTitleFontFamily',
|
||||
@ -535,6 +574,19 @@ export const buildElementSettings = (
|
||||
'galleryTextFontFamily',
|
||||
element.galleryTextFontFamily,
|
||||
);
|
||||
addIfNotEmpty(settings, 'galleryColumns', element.galleryColumns);
|
||||
addIfNotEmpty(
|
||||
settings,
|
||||
'galleryHeaderImageUrl',
|
||||
element.galleryHeaderImageUrl,
|
||||
);
|
||||
addIfNotEmpty(settings, 'galleryHeaderText', element.galleryHeaderText);
|
||||
addIfNotEmpty(settings, 'galleryTitle', element.galleryTitle);
|
||||
// Gallery section style properties
|
||||
GALLERY_SECTION_STYLE_PROPS.forEach((prop) => {
|
||||
const value = (element as Record<string, unknown>)[prop];
|
||||
addIfNotEmpty(settings, prop, value);
|
||||
});
|
||||
}
|
||||
|
||||
// Carousel type settings
|
||||
|
||||
343
frontend/src/lib/gallerySectionStyles.ts
Normal file
343
frontend/src/lib/gallerySectionStyles.ts
Normal file
@ -0,0 +1,343 @@
|
||||
/**
|
||||
* Gallery Section Styles
|
||||
*
|
||||
* Unified types and utilities for gallery element section styling.
|
||||
* Follows the same pattern as elementStyles.ts for consistency.
|
||||
*/
|
||||
|
||||
import type { CSSProperties } from 'react';
|
||||
import type { CanvasElement } from '../types/constructor';
|
||||
import { getFontByKey, getFontStyle } from './fonts';
|
||||
|
||||
/**
|
||||
* Gallery section names for styling
|
||||
*/
|
||||
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> = {
|
||||
header: {
|
||||
fontSize: '1.5rem', // text-2xl
|
||||
fontWeight: '700', // font-bold
|
||||
padding: '0.25rem 0.5rem', // px-1 py-2
|
||||
},
|
||||
title: {
|
||||
backgroundColor: '#fefce8', // bg-amber-50
|
||||
color: '#1e293b', // text-slate-800
|
||||
fontSize: '0.875rem', // text-sm
|
||||
fontWeight: '600', // font-semibold
|
||||
padding: '0.5rem 0.75rem', // py-2 px-3
|
||||
borderRadius: '0.5rem', // rounded-lg
|
||||
},
|
||||
span: {
|
||||
backgroundColor: '#334155', // bg-slate-700
|
||||
color: '#fef3c7', // text-amber-50
|
||||
fontSize: '0.75rem', // text-xs
|
||||
fontWeight: '500', // font-medium
|
||||
padding: '0.5rem', // py-2 px-2
|
||||
borderRadius: '0.5rem', // rounded-lg
|
||||
},
|
||||
card: {
|
||||
borderRadius: '0.5rem', // rounded-lg
|
||||
},
|
||||
wrapper: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.6)', // bg-black/60
|
||||
padding: '0.75rem', // p-3
|
||||
borderRadius: '0.75rem', // rounded-xl
|
||||
gap: '0.5rem', // gap-2
|
||||
backdropFilter: 'blur(4px)', // backdrop-blur-sm
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Get trimmed value from unknown input
|
||||
*/
|
||||
const getTrimmedValue = (value: unknown): string => {
|
||||
if (value === null || value === undefined) return '';
|
||||
if (value === 0) return '0';
|
||||
return String(value).trim();
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply value with default fallback
|
||||
*/
|
||||
const applyWithDefault = (
|
||||
style: CSSProperties,
|
||||
prop: keyof CSSProperties,
|
||||
value: unknown,
|
||||
defaultValue: unknown,
|
||||
): void => {
|
||||
const trimmed = getTrimmedValue(value);
|
||||
if (trimmed) {
|
||||
(style as Record<string, unknown>)[prop] = trimmed;
|
||||
} else if (defaultValue !== undefined) {
|
||||
(style as Record<string, unknown>)[prop] = defaultValue;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply value only if explicitly set (no default - allows CSS inheritance)
|
||||
*/
|
||||
const applyIfSet = (
|
||||
style: CSSProperties,
|
||||
prop: keyof CSSProperties,
|
||||
value: unknown,
|
||||
): void => {
|
||||
const trimmed = getTrimmedValue(value);
|
||||
if (trimmed) {
|
||||
(style as Record<string, unknown>)[prop] = trimmed;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Build CSS style object for gallery header section
|
||||
*/
|
||||
export function buildGalleryHeaderStyle(
|
||||
element: Partial<CanvasElement>,
|
||||
): CSSProperties {
|
||||
const defaults = GALLERY_SECTION_DEFAULTS.header;
|
||||
const style: CSSProperties = {};
|
||||
|
||||
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);
|
||||
applyIfSet(style, 'fontWeight', element.galleryHeaderFontWeight);
|
||||
// Non-inheritable properties: use defaults
|
||||
applyWithDefault(style, 'padding', element.galleryHeaderPadding, defaults.padding);
|
||||
applyIfSet(style, 'borderRadius', element.galleryHeaderBorderRadius);
|
||||
applyIfSet(style, 'border', element.galleryHeaderBorder);
|
||||
|
||||
// Apply font family with font library resolution
|
||||
const fontKey = element.galleryHeaderFontFamily;
|
||||
if (fontKey) {
|
||||
const font = getFontByKey(fontKey);
|
||||
if (font) {
|
||||
Object.assign(style, getFontStyle(font));
|
||||
} else {
|
||||
style.fontFamily = fontKey;
|
||||
}
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build CSS style object for gallery title section
|
||||
*/
|
||||
export function buildGalleryTitleStyle(
|
||||
element: Partial<CanvasElement>,
|
||||
): CSSProperties {
|
||||
const defaults = GALLERY_SECTION_DEFAULTS.title;
|
||||
const style: CSSProperties = {};
|
||||
|
||||
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);
|
||||
applyIfSet(style, 'fontWeight', element.galleryTitleFontWeight);
|
||||
// Non-inheritable properties: use defaults
|
||||
applyWithDefault(style, 'padding', element.galleryTitlePadding, defaults.padding);
|
||||
applyWithDefault(style, 'borderRadius', element.galleryTitleBorderRadius, defaults.borderRadius);
|
||||
applyIfSet(style, 'border', element.galleryTitleBorder);
|
||||
|
||||
// Apply font family with font library resolution
|
||||
const fontKey = element.galleryTitleFontFamily;
|
||||
if (fontKey) {
|
||||
const font = getFontByKey(fontKey);
|
||||
if (font) {
|
||||
Object.assign(style, getFontStyle(font));
|
||||
} else {
|
||||
style.fontFamily = fontKey;
|
||||
}
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build CSS style object for gallery span items
|
||||
*/
|
||||
export function buildGallerySpanStyle(
|
||||
element: Partial<CanvasElement>,
|
||||
): CSSProperties {
|
||||
const defaults = GALLERY_SECTION_DEFAULTS.span;
|
||||
const style: CSSProperties = {};
|
||||
|
||||
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);
|
||||
applyIfSet(style, 'fontWeight', element.gallerySpanFontWeight);
|
||||
// Non-inheritable properties: use defaults
|
||||
applyWithDefault(style, 'padding', element.gallerySpanPadding, defaults.padding);
|
||||
applyWithDefault(style, 'borderRadius', element.gallerySpanBorderRadius, defaults.borderRadius);
|
||||
applyIfSet(style, 'border', element.gallerySpanBorder);
|
||||
|
||||
// Apply font family with font library resolution (fallback to galleryTextFontFamily for legacy support)
|
||||
const fontKey = element.gallerySpanFontFamily || element.galleryTextFontFamily;
|
||||
if (fontKey) {
|
||||
const font = getFontByKey(fontKey);
|
||||
if (font) {
|
||||
Object.assign(style, getFontStyle(font));
|
||||
} else {
|
||||
style.fontFamily = fontKey;
|
||||
}
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build CSS style object for gallery span grid container
|
||||
*/
|
||||
export function buildGallerySpanGridStyle(
|
||||
element: Partial<CanvasElement>,
|
||||
): CSSProperties {
|
||||
const columns = getGalleryGridColumns(element, 'span');
|
||||
const gap = getTrimmedValue(element.gallerySpanGap) || '0.5rem';
|
||||
|
||||
return {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
|
||||
gap,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build CSS style object for gallery card items
|
||||
*/
|
||||
export function buildGalleryCardStyle(
|
||||
element: Partial<CanvasElement>,
|
||||
): CSSProperties {
|
||||
const defaults = GALLERY_SECTION_DEFAULTS.card;
|
||||
const style: CSSProperties = {};
|
||||
|
||||
applyWithDefault(style, 'backgroundColor', element.galleryCardBackgroundColor, undefined);
|
||||
applyWithDefault(style, 'borderRadius', element.galleryCardBorderRadius, defaults.borderRadius);
|
||||
applyWithDefault(style, 'border', element.galleryCardBorder, undefined);
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build CSS style object for gallery card title overlay
|
||||
*/
|
||||
export function buildGalleryCardTitleStyle(
|
||||
element: Partial<CanvasElement>,
|
||||
): CSSProperties {
|
||||
const style: CSSProperties = {};
|
||||
|
||||
// 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);
|
||||
applyIfSet(style, 'fontWeight', element.galleryCardTitleFontWeight);
|
||||
|
||||
if (element.galleryCardTitleBackgroundColor) {
|
||||
style.backgroundColor = element.galleryCardTitleBackgroundColor;
|
||||
}
|
||||
|
||||
// Handle text shadow
|
||||
const shadow = element.galleryCardTitleShadow;
|
||||
if (shadow) {
|
||||
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))';
|
||||
}
|
||||
|
||||
// Apply font family from galleryTextFontFamily for legacy support
|
||||
const fontKey = element.galleryTextFontFamily;
|
||||
if (fontKey) {
|
||||
const font = getFontByKey(fontKey);
|
||||
if (font) {
|
||||
Object.assign(style, getFontStyle(font));
|
||||
} else {
|
||||
style.fontFamily = fontKey;
|
||||
}
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build CSS style object for gallery cards grid container
|
||||
*/
|
||||
export function buildGalleryCardGridStyle(
|
||||
element: Partial<CanvasElement>,
|
||||
): CSSProperties {
|
||||
const columns = getGalleryGridColumns(element, 'card');
|
||||
const gap = getTrimmedValue(element.galleryCardGap) || '0.5rem';
|
||||
|
||||
return {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(${columns}, 1fr)`,
|
||||
gap,
|
||||
width: '100%',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get grid columns with fallback to legacy galleryColumns
|
||||
*/
|
||||
export function getGalleryGridColumns(
|
||||
element: Partial<CanvasElement>,
|
||||
section: 'span' | 'card',
|
||||
): number {
|
||||
if (section === 'span') {
|
||||
return element.gallerySpanColumns ?? element.galleryColumns ?? 3;
|
||||
}
|
||||
// For cards, use galleryCardColumns with fallback to legacy galleryColumns
|
||||
return element.galleryCardColumns ?? element.galleryColumns ?? 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* All gallery section style property names for iteration
|
||||
*/
|
||||
export const GALLERY_SECTION_STYLE_PROPS = [
|
||||
// Header
|
||||
'galleryHeaderBackgroundColor',
|
||||
'galleryHeaderColor',
|
||||
'galleryHeaderFontFamily',
|
||||
'galleryHeaderFontSize',
|
||||
'galleryHeaderFontWeight',
|
||||
'galleryHeaderPadding',
|
||||
'galleryHeaderBorderRadius',
|
||||
'galleryHeaderBorder',
|
||||
// Title
|
||||
'galleryTitleBackgroundColor',
|
||||
'galleryTitleColor',
|
||||
'galleryTitleFontFamily',
|
||||
'galleryTitleFontSize',
|
||||
'galleryTitleFontWeight',
|
||||
'galleryTitlePadding',
|
||||
'galleryTitleBorderRadius',
|
||||
'galleryTitleBorder',
|
||||
// Spans
|
||||
'gallerySpanBackgroundColor',
|
||||
'gallerySpanColor',
|
||||
'gallerySpanFontFamily',
|
||||
'gallerySpanFontSize',
|
||||
'gallerySpanFontWeight',
|
||||
'gallerySpanPadding',
|
||||
'gallerySpanBorderRadius',
|
||||
'gallerySpanBorder',
|
||||
'gallerySpanGap',
|
||||
'gallerySpanColumns',
|
||||
// Cards
|
||||
'galleryCardBackgroundColor',
|
||||
'galleryCardBorderRadius',
|
||||
'galleryCardBorder',
|
||||
'galleryCardGap',
|
||||
'galleryCardColumns',
|
||||
'galleryCardTitleColor',
|
||||
'galleryCardTitleBackgroundColor',
|
||||
'galleryCardTitleFontSize',
|
||||
'galleryCardTitleFontWeight',
|
||||
'galleryCardTitleShadow',
|
||||
] as const;
|
||||
|
||||
export type GallerySectionStyleProp = (typeof GALLERY_SECTION_STYLE_PROPS)[number];
|
||||
@ -47,6 +47,7 @@ export interface GalleryCard {
|
||||
export interface GalleryInfoSpan {
|
||||
id: string;
|
||||
text: string;
|
||||
iconUrl?: string; // Renders icon instead of text when set
|
||||
}
|
||||
|
||||
/**
|
||||
@ -100,6 +101,52 @@ export interface CanvasElement extends BaseCanvasElement {
|
||||
galleryColumns?: number;
|
||||
galleryTitleFontFamily?: string;
|
||||
galleryTextFontFamily?: string;
|
||||
// Gallery Section Styles - Header
|
||||
galleryHeaderBackgroundColor?: string;
|
||||
galleryHeaderColor?: string;
|
||||
galleryHeaderFontFamily?: string;
|
||||
galleryHeaderFontSize?: string;
|
||||
galleryHeaderFontWeight?: string;
|
||||
galleryHeaderPadding?: string;
|
||||
galleryHeaderBorderRadius?: string;
|
||||
galleryHeaderBorder?: string;
|
||||
// Gallery Section Styles - Title
|
||||
galleryTitleBackgroundColor?: string;
|
||||
galleryTitleColor?: string;
|
||||
galleryTitleFontSize?: string;
|
||||
galleryTitleFontWeight?: string;
|
||||
galleryTitlePadding?: string;
|
||||
galleryTitleBorderRadius?: string;
|
||||
galleryTitleBorder?: string;
|
||||
// Gallery Section Styles - Spans
|
||||
gallerySpanBackgroundColor?: string;
|
||||
gallerySpanColor?: string;
|
||||
gallerySpanFontFamily?: string;
|
||||
gallerySpanFontSize?: string;
|
||||
gallerySpanFontWeight?: string;
|
||||
gallerySpanPadding?: string;
|
||||
gallerySpanBorderRadius?: string;
|
||||
gallerySpanBorder?: string;
|
||||
gallerySpanGap?: string;
|
||||
gallerySpanColumns?: number;
|
||||
// Gallery Section Styles - Cards
|
||||
galleryCardBackgroundColor?: string;
|
||||
galleryCardBorderRadius?: string;
|
||||
galleryCardBorder?: string;
|
||||
galleryCardGap?: string;
|
||||
galleryCardColumns?: number;
|
||||
galleryCardTitleColor?: string;
|
||||
galleryCardTitleBackgroundColor?: string;
|
||||
galleryCardTitleFontSize?: string;
|
||||
galleryCardTitleFontWeight?: string;
|
||||
galleryCardTitleShadow?: string;
|
||||
// Gallery Section Styles - Wrapper
|
||||
galleryWrapperBackgroundColor?: string;
|
||||
galleryWrapperPadding?: string;
|
||||
galleryWrapperBorderRadius?: string;
|
||||
galleryWrapperBorder?: string;
|
||||
galleryWrapperGap?: string;
|
||||
galleryWrapperBackdropBlur?: string;
|
||||
carouselSlides?: CarouselSlide[];
|
||||
carouselCaptionFontFamily?: string;
|
||||
carouselPrevIconUrl?: string;
|
||||
@ -401,7 +448,7 @@ export interface EditorCollectionOpsProps {
|
||||
};
|
||||
galleryInfoSpans: {
|
||||
add: () => void;
|
||||
update: (spanId: string, text: string) => void;
|
||||
update: (spanId: string, patch: Partial<GalleryInfoSpan>) => void;
|
||||
remove: (spanId: string) => void;
|
||||
};
|
||||
carouselSlides: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user