fixed carusel adaptivity issue

This commit is contained in:
Dmitri 2026-04-14 17:45:31 +04:00
parent 4a61fd1a69
commit 4b7fed5914
7 changed files with 203 additions and 131 deletions

View File

@ -35,6 +35,8 @@ interface CanvasElementProps {
x: number,
y: number,
) => void;
/** Letterbox styles for constraining fullscreen elements to canvas bounds */
letterboxStyles?: React.CSSProperties;
}
const CanvasElement: React.FC<CanvasElementProps> = ({
@ -47,6 +49,7 @@ const CanvasElement: React.FC<CanvasElementProps> = ({
resolveUrl,
onGalleryCardClick,
onCarouselButtonPositionChange,
letterboxStyles,
}) => {
// Extract effect properties from element
const effectProperties: Partial<ElementEffectProperties> = {
@ -73,10 +76,16 @@ const CanvasElement: React.FC<CanvasElementProps> = ({
isEditMode ? {} : effectProperties,
);
// Clamp position to canvas bounds (0-100%)
const clamp = (value: number, min: number, max: number) =>
Math.min(Math.max(value, min), max);
const xClamped = clamp(element.xPercent ?? 50, 0, 100);
const yClamped = clamp(element.yPercent ?? 50, 0, 100);
// Build base position style
let positionStyle: React.CSSProperties = {
left: `${element.xPercent}%`,
top: `${element.yPercent}%`,
left: `${xClamped}%`,
top: `${yClamped}%`,
transform: 'translate(-50%, -50%)',
// Reset button defaults to let UiElementRenderer control styling
background: 'transparent',
@ -130,6 +139,7 @@ const CanvasElement: React.FC<CanvasElementProps> = ({
isDisabled={isDisabled}
onGalleryCardClick={onGalleryCardClick}
onCarouselButtonPositionChange={onCarouselButtonPositionChange}
letterboxStyles={letterboxStyles}
/>
</button>
);

View File

@ -24,16 +24,24 @@ interface RuntimeElementProps {
resolveUrl?: (url: string | undefined) => string;
/** Gallery card click handler */
onGalleryCardClick?: (cardIndex: number) => void;
/** Letterbox styles for constraining fullscreen elements to canvas bounds */
letterboxStyles?: React.CSSProperties;
}
// Clamp position to canvas bounds (0-100%)
const clamp = (value: number, min: number, max: number) =>
Math.min(Math.max(value, min), max);
const RuntimeElement: React.FC<RuntimeElementProps> = ({
element,
onClick,
resolveUrl,
onGalleryCardClick,
letterboxStyles,
}) => {
const xPercent = element.xPercent ?? 0;
const yPercent = element.yPercent ?? 0;
// Clamp coordinates to canvas bounds
const xPercent = clamp(element.xPercent ?? 50, 0, 100);
const yPercent = clamp(element.yPercent ?? 50, 0, 100);
const rotation = element.rotation ?? 0;
// Extract effect properties from element
@ -101,6 +109,7 @@ const RuntimeElement: React.FC<RuntimeElementProps> = ({
element={element}
resolveUrl={resolveUrl}
onGalleryCardClick={onGalleryCardClick}
letterboxStyles={letterboxStyles}
/>
</div>
);

View File

@ -713,6 +713,7 @@ export default function RuntimePresentation({
onGalleryCardClick={(cardIndex) =>
handleGalleryCardClick(element, cardIndex)
}
letterboxStyles={letterboxStyles}
/>
))}
</div>
@ -794,6 +795,7 @@ export default function RuntimePresentation({
backHeight={
activeGalleryCarousel.element.galleryCarouselBackHeight
}
letterboxStyles={letterboxStyles}
isEditMode={false}
/>
)}

View File

@ -44,6 +44,8 @@ interface GalleryCarouselOverlayProps {
x: number,
y: number,
) => void;
// Letterbox styles for constraining overlay to canvas bounds
letterboxStyles?: React.CSSProperties;
}
const clamp = (value: number, min: number, max: number) =>
@ -72,28 +74,38 @@ const GalleryCarouselOverlay: React.FC<GalleryCarouselOverlayProps> = ({
backHeight,
isEditMode = false,
onButtonPositionChange,
letterboxStyles,
}) => {
const resolve = resolveUrl ?? resolveAssetPlaybackUrl;
const [currentIndex, setCurrentIndex] = useState(initialIndex);
const [draggingButton, setDraggingButton] = useState<
'prev' | 'next' | 'back' | null
>(null);
// Clamp positions to canvas bounds (0-100%)
const [positions, setPositions] = useState({
prevX,
prevY,
nextX,
nextY,
backX,
backY,
prevX: clamp(prevX, 0, 100),
prevY: clamp(prevY, 0, 100),
nextX: clamp(nextX, 0, 100),
nextY: clamp(nextY, 0, 100),
backX: clamp(backX, 0, 100),
backY: clamp(backY, 0, 100),
});
const overlayRef = useRef<HTMLDivElement>(null);
const canvasContainerRef = useRef<HTMLDivElement>(null);
const touchStartRef = useRef<{ x: number; y: number } | null>(null);
const positionsRef = useRef(positions);
positionsRef.current = positions;
// Update positions when props change (e.g., when element is re-selected)
// Update positions when props change (clamped to canvas bounds)
useEffect(() => {
setPositions({ prevX, prevY, nextX, nextY, backX, backY });
setPositions({
prevX: clamp(prevX, 0, 100),
prevY: clamp(prevY, 0, 100),
nextX: clamp(nextX, 0, 100),
nextY: clamp(nextY, 0, 100),
backX: clamp(backX, 0, 100),
backY: clamp(backY, 0, 100),
});
}, [prevX, prevY, nextX, nextY, backX, backY]);
// Navigation handlers
@ -169,10 +181,16 @@ const GalleryCarouselOverlay: React.FC<GalleryCarouselOverlayProps> = ({
if (!isEditMode || !draggingButton) return;
const handleMove = (e: MouseEvent) => {
const x = (e.clientX / window.innerWidth) * 100;
const y = (e.clientY / window.innerHeight) * 100;
const clampedX = clamp(x, 2, 98);
const clampedY = clamp(y, 2, 98);
// Calculate position relative to canvas container (strict - no viewport fallback)
const container = canvasContainerRef.current;
if (!container) return;
const rect = container.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100;
const y = ((e.clientY - rect.top) / rect.height) * 100;
// Clamp to canvas bounds (0-100%)
const clampedX = clamp(x, 0, 100);
const clampedY = clamp(y, 0, 100);
setPositions((prev) => {
// Prev and next buttons share the same Y coordinate
@ -346,52 +364,59 @@ const GalleryCarouselOverlay: React.FC<GalleryCarouselOverlayProps> = ({
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
>
{/* Fullscreen image */}
{imageUrl && (
// eslint-disable-next-line @next/next/no-img-element
<img
src={imageUrl}
alt={currentCard?.title || ''}
className='absolute inset-0 h-full w-full object-cover'
draggable={false}
/>
)}
{/* Inner container constrained to canvas bounds - no conflicting CSS classes */}
<div
ref={canvasContainerRef}
className='overflow-hidden'
style={letterboxStyles || { position: 'absolute', inset: 0 }}
>
{/* Fullscreen image */}
{imageUrl && (
// eslint-disable-next-line @next/next/no-img-element
<img
src={imageUrl}
alt={currentCard?.title || ''}
className='absolute inset-0 h-full w-full object-contain'
draggable={false}
/>
)}
{/* Prev button */}
{renderNavButton(
'prev',
positions.prevX,
positions.prevY,
prevIconUrl,
mdiChevronLeft,
undefined,
prevWidth,
prevHeight,
)}
{/* Prev button */}
{renderNavButton(
'prev',
positions.prevX,
positions.prevY,
prevIconUrl,
mdiChevronLeft,
undefined,
prevWidth,
prevHeight,
)}
{/* Next button */}
{renderNavButton(
'next',
positions.nextX,
positions.nextY,
nextIconUrl,
mdiChevronRight,
undefined,
nextWidth,
nextHeight,
)}
{/* Next button */}
{renderNavButton(
'next',
positions.nextX,
positions.nextY,
nextIconUrl,
mdiChevronRight,
undefined,
nextWidth,
nextHeight,
)}
{/* Back button */}
{renderNavButton(
'back',
positions.backX,
positions.backY,
backIconUrl,
mdiArrowLeft,
backLabel,
backWidth,
backHeight,
)}
{/* Back button */}
{renderNavButton(
'back',
positions.backX,
positions.backY,
backIconUrl,
mdiArrowLeft,
backLabel,
backWidth,
backHeight,
)}
</div>
</div>
);
};

View File

@ -51,6 +51,8 @@ export interface UiElementRendererProps {
x: number,
y: number,
) => void;
// Letterbox styles for constraining fullscreen elements to canvas bounds
letterboxStyles?: React.CSSProperties;
}
/**
@ -67,6 +69,7 @@ export const UiElementRenderer: React.FC<UiElementRendererProps> = ({
isDisabled = false,
onGalleryCardClick,
onCarouselButtonPositionChange,
letterboxStyles,
}) => {
const { className, style } = useElementWrapperStyle({
element,
@ -97,6 +100,7 @@ export const UiElementRenderer: React.FC<UiElementRendererProps> = ({
{...commonProps}
isEditMode={isEditMode}
onButtonPositionChange={onCarouselButtonPositionChange}
letterboxStyles={letterboxStyles}
/>
);
}

View File

@ -42,6 +42,8 @@ interface CarouselElementProps {
x: number,
y: number,
) => void;
// Letterbox styles for constraining full-width carousel to canvas bounds
letterboxStyles?: CSSProperties;
}
const clamp = (value: number, min: number, max: number) =>
@ -54,6 +56,7 @@ const CarouselElement: React.FC<CarouselElementProps> = ({
style,
isEditMode = false,
onButtonPositionChange,
letterboxStyles,
}) => {
const resolve = resolveUrl ?? resolveAssetPlaybackUrl;
const slides: CarouselSlide[] = element.carouselSlides || [];
@ -65,11 +68,12 @@ const CarouselElement: React.FC<CarouselElementProps> = ({
const [draggingButton, setDraggingButton] = useState<'prev' | 'next' | null>(
null,
);
// Clamp positions to canvas bounds (0-100%)
const [positions, setPositions] = useState({
prevX: element.carouselPrevX ?? 5,
prevY: element.carouselPrevY ?? 50,
nextX: element.carouselNextX ?? 95,
nextY: element.carouselNextY ?? 50,
prevX: clamp(element.carouselPrevX ?? 5, 0, 100),
prevY: clamp(element.carouselPrevY ?? 50, 0, 100),
nextX: clamp(element.carouselNextX ?? 95, 0, 100),
nextY: clamp(element.carouselNextY ?? 50, 0, 100),
});
// Touch swipe ref
@ -78,13 +82,13 @@ const CarouselElement: React.FC<CarouselElementProps> = ({
const positionsRef = useRef(positions);
positionsRef.current = positions;
// Update positions when props change
// Update positions when props change (clamped to canvas bounds)
useEffect(() => {
setPositions({
prevX: element.carouselPrevX ?? 5,
prevY: element.carouselPrevY ?? 50,
nextX: element.carouselNextX ?? 95,
nextY: element.carouselNextY ?? 50,
prevX: clamp(element.carouselPrevX ?? 5, 0, 100),
prevY: clamp(element.carouselPrevY ?? 50, 0, 100),
nextX: clamp(element.carouselNextX ?? 95, 0, 100),
nextY: clamp(element.carouselNextY ?? 50, 0, 100),
});
}, [
element.carouselPrevX,
@ -199,10 +203,16 @@ const CarouselElement: React.FC<CarouselElementProps> = ({
if (!isEditMode || !draggingButton) return;
const handleMove = (e: MouseEvent) => {
const x = (e.clientX / window.innerWidth) * 100;
const y = (e.clientY / window.innerHeight) * 100;
const clampedX = clamp(x, 2, 98);
const clampedY = clamp(y, 2, 98);
// Calculate position relative to canvas container (strict - no viewport fallback)
const container = overlayRef.current;
if (!container) return;
const rect = container.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100;
const y = ((e.clientY - rect.top) / rect.height) * 100;
// Clamp to canvas bounds (0-100%)
const clampedX = clamp(x, 0, 100);
const clampedY = clamp(y, 0, 100);
setPositions((prev) => ({
...prev,
@ -240,11 +250,8 @@ const CarouselElement: React.FC<CarouselElementProps> = ({
}, [isEditMode, draggingButton, onButtonPositionChange]);
// Convert numeric value to canvas units for responsive scaling
// Previously used vw/vh but now uses canvas units (--cu) for uniform scaling
const toCanvasUnit = (
value?: string,
dimension: 'width' | 'height' = 'width',
): string | undefined => {
// All dimensions use canvas units (--cu) for uniform scaling within project bounds
const toCanvasUnit = (value?: string): string | undefined => {
if (!value || value.trim() === '') return undefined;
const trimmed = value.trim();
// If value already uses canvas units or calc, preserve it
@ -272,10 +279,8 @@ const CarouselElement: React.FC<CarouselElementProps> = ({
return toCU(num);
};
// Alias for backward compatibility
const toViewportUnit = toCanvasUnit;
// Render navigation button for full-width mode
// Coordinates are clamped to 0-100% to stay within canvas bounds
const renderNavButton = (
type: 'prev' | 'next',
x: number,
@ -287,8 +292,12 @@ const CarouselElement: React.FC<CarouselElementProps> = ({
) => {
const isDragging = draggingButton === type;
const hasCustomIcon = iconUrl && iconUrl.trim() !== '';
const widthValue = toCanvasUnit(buttonWidth, 'width');
const heightValue = toCanvasUnit(buttonHeight, 'height');
const widthValue = toCanvasUnit(buttonWidth);
const heightValue = toCanvasUnit(buttonHeight);
// Clamp coordinates to canvas bounds (0-100%)
const clampedX = clamp(x, 0, 100);
const clampedY = clamp(y, 0, 100);
// Navigation-style: custom icon fills button (no backdrop)
const useNavigationStyle = hasCustomIcon && (widthValue || heightValue);
@ -302,8 +311,8 @@ const CarouselElement: React.FC<CarouselElementProps> = ({
: 'cursor-pointer hover:scale-105'
} ${isDragging ? 'scale-110 z-[60]' : ''}`}
style={{
left: `${x}%`,
top: `${y}%`,
left: `${clampedX}%`,
top: `${clampedY}%`,
transform: 'translate(-50%, -50%)',
...(widthValue && { width: widthValue }),
...(heightValue && { height: heightValue }),
@ -342,60 +351,71 @@ const CarouselElement: React.FC<CarouselElementProps> = ({
// Full-width carousel - two layers:
// 1. Background image layer at z-10 (behind canvas z-[46])
// 2. Navigation/caption layer at z-[47] (above canvas z-[46], below UI controls z-50)
// Both layers use letterboxStyles to constrain content to canvas bounds
const fullWidthBackground = (
<div className='fixed inset-0 z-10 overflow-hidden bg-black pointer-events-none'>
{currentSlide?.imageUrl && (
// eslint-disable-next-line @next/next/no-img-element
<img
src={resolve(currentSlide.imageUrl)}
alt={currentSlide.caption || 'Carousel slide'}
className='absolute inset-0 w-full h-full object-cover'
draggable={false}
/>
)}
{/* Inner container respects letterbox dimensions - overflow:hidden clips content to canvas */}
<div
className='overflow-hidden'
style={letterboxStyles || { position: 'absolute', inset: 0 }}
>
{currentSlide?.imageUrl && (
// eslint-disable-next-line @next/next/no-img-element
<img
src={resolve(currentSlide.imageUrl)}
alt={currentSlide.caption || 'Carousel slide'}
className='absolute inset-0 w-full h-full object-contain'
draggable={false}
/>
)}
</div>
</div>
);
const fullWidthControls = (
<div
ref={overlayRef}
className='fixed inset-0 z-[47] pointer-events-none'
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
>
{/* Navigation buttons */}
{showNavigation && (
<>
{renderNavButton(
'prev',
positions.prevX,
positions.prevY,
element.carouselPrevIconUrl,
mdiChevronLeft,
element.carouselPrevWidth,
element.carouselPrevHeight,
)}
{renderNavButton(
'next',
positions.nextX,
positions.nextY,
element.carouselNextIconUrl,
mdiChevronRight,
element.carouselNextWidth,
element.carouselNextHeight,
)}
</>
)}
<div className='fixed inset-0 z-[47] pointer-events-none'>
{/* Inner container respects letterbox dimensions - overflow:hidden clips content to canvas */}
<div
ref={overlayRef}
className='overflow-hidden'
style={letterboxStyles || { position: 'absolute', inset: 0 }}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
>
{/* Navigation buttons */}
{showNavigation && (
<>
{renderNavButton(
'prev',
positions.prevX,
positions.prevY,
element.carouselPrevIconUrl,
mdiChevronLeft,
element.carouselPrevWidth,
element.carouselPrevHeight,
)}
{renderNavButton(
'next',
positions.nextX,
positions.nextY,
element.carouselNextIconUrl,
mdiChevronRight,
element.carouselNextWidth,
element.carouselNextHeight,
)}
</>
)}
{/* Caption */}
{currentSlide?.caption && (
<div
className='absolute bottom-8 left-0 right-0 text-center text-white text-lg px-4'
style={captionFontStyle}
>
{currentSlide.caption}
</div>
)}
{/* Caption */}
{currentSlide?.caption && (
<div
className='absolute bottom-8 left-0 right-0 text-center text-white text-lg px-4'
style={captionFontStyle}
>
{currentSlide.caption}
</div>
)}
</div>
</div>
);

View File

@ -1569,6 +1569,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
y,
)
}
letterboxStyles={letterboxStyles}
/>
);
})
@ -1648,6 +1649,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
nextHeight={activeGalleryCarousel.element.galleryCarouselNextHeight}
backWidth={activeGalleryCarousel.element.galleryCarouselBackWidth}
backHeight={activeGalleryCarousel.element.galleryCarouselBackHeight}
letterboxStyles={letterboxStyles}
isEditMode={isConstructorEditMode}
onButtonPositionChange={handleGalleryCarouselButtonPositionChange}
/>