From 4b7fed5914e35aa06b5d0a470a1f4934d283639e Mon Sep 17 00:00:00 2001 From: Dmitri Date: Tue, 14 Apr 2026 17:45:31 +0400 Subject: [PATCH] fixed carusel adaptivity issue --- .../components/Constructor/CanvasElement.tsx | 14 +- frontend/src/components/RuntimeElement.tsx | 13 +- .../src/components/RuntimePresentation.tsx | 2 + .../UiElements/GalleryCarouselOverlay.tsx | 135 ++++++++------ .../UiElements/UiElementRenderer.tsx | 4 + .../UiElements/elements/CarouselElement.tsx | 164 ++++++++++-------- frontend/src/pages/constructor.tsx | 2 + 7 files changed, 203 insertions(+), 131 deletions(-) diff --git a/frontend/src/components/Constructor/CanvasElement.tsx b/frontend/src/components/Constructor/CanvasElement.tsx index 1442291..113e3bb 100644 --- a/frontend/src/components/Constructor/CanvasElement.tsx +++ b/frontend/src/components/Constructor/CanvasElement.tsx @@ -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 = ({ @@ -47,6 +49,7 @@ const CanvasElement: React.FC = ({ resolveUrl, onGalleryCardClick, onCarouselButtonPositionChange, + letterboxStyles, }) => { // Extract effect properties from element const effectProperties: Partial = { @@ -73,10 +76,16 @@ const CanvasElement: React.FC = ({ 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 = ({ isDisabled={isDisabled} onGalleryCardClick={onGalleryCardClick} onCarouselButtonPositionChange={onCarouselButtonPositionChange} + letterboxStyles={letterboxStyles} /> ); diff --git a/frontend/src/components/RuntimeElement.tsx b/frontend/src/components/RuntimeElement.tsx index 783e063..0689028 100644 --- a/frontend/src/components/RuntimeElement.tsx +++ b/frontend/src/components/RuntimeElement.tsx @@ -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 = ({ 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 = ({ element={element} resolveUrl={resolveUrl} onGalleryCardClick={onGalleryCardClick} + letterboxStyles={letterboxStyles} /> ); diff --git a/frontend/src/components/RuntimePresentation.tsx b/frontend/src/components/RuntimePresentation.tsx index bc2f8f0..f0a09e0 100644 --- a/frontend/src/components/RuntimePresentation.tsx +++ b/frontend/src/components/RuntimePresentation.tsx @@ -713,6 +713,7 @@ export default function RuntimePresentation({ onGalleryCardClick={(cardIndex) => handleGalleryCardClick(element, cardIndex) } + letterboxStyles={letterboxStyles} /> ))} @@ -794,6 +795,7 @@ export default function RuntimePresentation({ backHeight={ activeGalleryCarousel.element.galleryCarouselBackHeight } + letterboxStyles={letterboxStyles} isEditMode={false} /> )} diff --git a/frontend/src/components/UiElements/GalleryCarouselOverlay.tsx b/frontend/src/components/UiElements/GalleryCarouselOverlay.tsx index 85a9056..abb8f09 100644 --- a/frontend/src/components/UiElements/GalleryCarouselOverlay.tsx +++ b/frontend/src/components/UiElements/GalleryCarouselOverlay.tsx @@ -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 = ({ 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(null); + const canvasContainerRef = useRef(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 = ({ 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 = ({ onTouchStart={handleTouchStart} onTouchEnd={handleTouchEnd} > - {/* Fullscreen image */} - {imageUrl && ( - // eslint-disable-next-line @next/next/no-img-element - {currentCard?.title - )} + {/* Inner container constrained to canvas bounds - no conflicting CSS classes */} +
+ {/* Fullscreen image */} + {imageUrl && ( + // eslint-disable-next-line @next/next/no-img-element + {currentCard?.title + )} - {/* 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, + )} +
); }; diff --git a/frontend/src/components/UiElements/UiElementRenderer.tsx b/frontend/src/components/UiElements/UiElementRenderer.tsx index 9db0fdc..928fe34 100644 --- a/frontend/src/components/UiElements/UiElementRenderer.tsx +++ b/frontend/src/components/UiElements/UiElementRenderer.tsx @@ -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 = ({ isDisabled = false, onGalleryCardClick, onCarouselButtonPositionChange, + letterboxStyles, }) => { const { className, style } = useElementWrapperStyle({ element, @@ -97,6 +100,7 @@ export const UiElementRenderer: React.FC = ({ {...commonProps} isEditMode={isEditMode} onButtonPositionChange={onCarouselButtonPositionChange} + letterboxStyles={letterboxStyles} /> ); } diff --git a/frontend/src/components/UiElements/elements/CarouselElement.tsx b/frontend/src/components/UiElements/elements/CarouselElement.tsx index c99059d..ccd8735 100644 --- a/frontend/src/components/UiElements/elements/CarouselElement.tsx +++ b/frontend/src/components/UiElements/elements/CarouselElement.tsx @@ -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 = ({ style, isEditMode = false, onButtonPositionChange, + letterboxStyles, }) => { const resolve = resolveUrl ?? resolveAssetPlaybackUrl; const slides: CarouselSlide[] = element.carouselSlides || []; @@ -65,11 +68,12 @@ const CarouselElement: React.FC = ({ 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 = ({ 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 = ({ 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 = ({ }, [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 = ({ 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 = ({ ) => { 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 = ({ : '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 = ({ // 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 = (
- {currentSlide?.imageUrl && ( - // eslint-disable-next-line @next/next/no-img-element - {currentSlide.caption - )} + {/* Inner container respects letterbox dimensions - overflow:hidden clips content to canvas */} +
+ {currentSlide?.imageUrl && ( + // eslint-disable-next-line @next/next/no-img-element + {currentSlide.caption + )} +
); const fullWidthControls = ( -
- {/* 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, - )} - - )} +
+ {/* Inner container respects letterbox dimensions - overflow:hidden clips content to canvas */} +
+ {/* 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 && ( -
- {currentSlide.caption} -
- )} + {/* Caption */} + {currentSlide?.caption && ( +
+ {currentSlide.caption} +
+ )} +
); diff --git a/frontend/src/pages/constructor.tsx b/frontend/src/pages/constructor.tsx index b5d537e..3267a28 100644 --- a/frontend/src/pages/constructor.tsx +++ b/frontend/src/pages/constructor.tsx @@ -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} />