diff --git a/frontend/src/components/ElementSettings/InfoPanelSettingsSection.tsx b/frontend/src/components/ElementSettings/InfoPanelSettingsSection.tsx index 20baa8f..ed4979b 100644 --- a/frontend/src/components/ElementSettings/InfoPanelSettingsSection.tsx +++ b/frontend/src/components/ElementSettings/InfoPanelSettingsSection.tsx @@ -32,6 +32,7 @@ const InfoPanelSettingsSection: React.FC = ({ infoPanelTriggerLabel, infoPanelTriggerFontFamily, infoPanelDisabled, + infoPanelOpenByDefault, // Header section infoPanelHeaderImageUrl, infoPanelHeaderText, @@ -186,6 +187,19 @@ const InfoPanelSettingsSection: React.FC = ({ Disable this info panel + + + + diff --git a/frontend/src/components/ElementSettings/InfoPanelSettingsSectionCompact.tsx b/frontend/src/components/ElementSettings/InfoPanelSettingsSectionCompact.tsx index e72f801..00c2b2b 100644 --- a/frontend/src/components/ElementSettings/InfoPanelSettingsSectionCompact.tsx +++ b/frontend/src/components/ElementSettings/InfoPanelSettingsSectionCompact.tsx @@ -400,6 +400,22 @@ const InfoPanelSettingsSectionCompact: React.FC< +
+ +

+ Opens the panel when this page renders +

+
+
@@ -1184,82 +1234,154 @@ export default function RuntimePresentation({ /> )} - {activeInfoPanelGallery && ( + {activeInfoPanelGallery && activeInfoPanelGalleryElement && ( setActiveInfoPanelGallery(null)} resolveUrl={resolveUrlWithBlob} - prevIconUrl={activeInfoPanel?.galleryCarouselPrevIconUrl} - nextIconUrl={activeInfoPanel?.galleryCarouselNextIconUrl} - backIconUrl={activeInfoPanel?.galleryCarouselBackIconUrl} - backLabel={activeInfoPanel?.galleryCarouselBackLabel || 'BACK'} - prevX={activeInfoPanel?.galleryCarouselPrevX} - prevY={activeInfoPanel?.galleryCarouselPrevY} - nextX={activeInfoPanel?.galleryCarouselNextX} - nextY={activeInfoPanel?.galleryCarouselNextY} - backX={activeInfoPanel?.galleryCarouselBackX} - backY={activeInfoPanel?.galleryCarouselBackY} - prevWidth={activeInfoPanel?.galleryCarouselPrevWidth} - prevHeight={activeInfoPanel?.galleryCarouselPrevHeight} - nextWidth={activeInfoPanel?.galleryCarouselNextWidth} - nextHeight={activeInfoPanel?.galleryCarouselNextHeight} - backWidth={activeInfoPanel?.galleryCarouselBackWidth} - backHeight={activeInfoPanel?.galleryCarouselBackHeight} + prevIconUrl={ + activeInfoPanelGalleryElement.galleryCarouselPrevIconUrl + } + nextIconUrl={ + activeInfoPanelGalleryElement.galleryCarouselNextIconUrl + } + backIconUrl={ + activeInfoPanelGalleryElement.galleryCarouselBackIconUrl + } + backLabel={ + activeInfoPanelGalleryElement.galleryCarouselBackLabel || + 'BACK' + } + prevX={activeInfoPanelGalleryElement.galleryCarouselPrevX} + prevY={activeInfoPanelGalleryElement.galleryCarouselPrevY} + nextX={activeInfoPanelGalleryElement.galleryCarouselNextX} + nextY={activeInfoPanelGalleryElement.galleryCarouselNextY} + backX={activeInfoPanelGalleryElement.galleryCarouselBackX} + backY={activeInfoPanelGalleryElement.galleryCarouselBackY} + prevWidth={ + activeInfoPanelGalleryElement.galleryCarouselPrevWidth + } + prevHeight={ + activeInfoPanelGalleryElement.galleryCarouselPrevHeight + } + nextWidth={ + activeInfoPanelGalleryElement.galleryCarouselNextWidth + } + nextHeight={ + activeInfoPanelGalleryElement.galleryCarouselNextHeight + } + backWidth={ + activeInfoPanelGalleryElement.galleryCarouselBackWidth + } + backHeight={ + activeInfoPanelGalleryElement.galleryCarouselBackHeight + } letterboxStyles={letterboxStyles} isEditMode={false} pageTransitionSettings={transitionSettings} - galleryElement={activeInfoPanel || undefined} + galleryElement={activeInfoPanelGalleryElement} /> )} {/* Info Panel Overlay */} - {activeInfoPanel && ( - <> - { - setActiveInfoPanel(null); - setActiveDetailImage(null); - setActiveInfoPanelGallery(null); - setRuntimeSelectedImageId(null); - }} - resolveUrl={resolveUrlWithBlob} - letterboxStyles={letterboxStyles} - cssVars={cssVars} - onImageClick={(image) => setActiveDetailImage(image)} - onOpenGallery={handleInfoPanelOpenGallery} - onUseAsBackground={handleInfoPanelUseAsBackground} - onSelectImage={(imageId) => - setRuntimeSelectedImageId(imageId) - } - onNavigateToPage={handleInfoPanelNavigateToPage} - onOpenExternalUrl={handleInfoPanelOpenExternalUrl} - active360ItemId={ - activeDetailImage?.itemType === '360' - ? activeDetailImage.id - : null - } - /> - {activeDetailImage && ( - setActiveDetailImage(null)} + {activeInfoPanelElements.map((activeInfoPanel, panelIndex) => { + const selectedImageId = + runtimeSelectedImageIds[activeInfoPanel.id]; + const panelDetailImage = activeDetailImages[activeInfoPanel.id]; + + return ( + + { + setActiveInfoPanelIds((current) => + current.filter( + (panelId) => panelId !== activeInfoPanel.id, + ), + ); + setActiveDetailImages((current) => { + const next = { ...current }; + delete next[activeInfoPanel.id]; + return next; + }); + setActiveInfoPanelGallery((current) => + current?.panelId === activeInfoPanel.id + ? null + : current, + ); + setRuntimeSelectedImageIds((current) => { + const next = { ...current }; + delete next[activeInfoPanel.id]; + return next; + }); + }} resolveUrl={resolveUrlWithBlob} letterboxStyles={letterboxStyles} cssVars={cssVars} + renderBackdrop={panelIndex === 0} + onBackdropClose={() => { + setActiveInfoPanelIds([]); + setActiveDetailImages({}); + setActiveInfoPanelGallery(null); + setRuntimeSelectedImageIds({}); + }} + onImageClick={(image) => + setActiveDetailImages((current) => ({ + ...current, + [activeInfoPanel.id]: image, + })) + } + onOpenGallery={(items, initialIndex) => + handleInfoPanelOpenGallery( + activeInfoPanel.id, + items, + initialIndex, + ) + } + onUseAsBackground={(item) => + handleInfoPanelUseAsBackground(activeInfoPanel.id, item) + } + onSelectImage={(imageId) => + setRuntimeSelectedImageIds((current) => ({ + ...current, + [activeInfoPanel.id]: imageId, + })) + } + onNavigateToPage={handleInfoPanelNavigateToPage} + onOpenExternalUrl={handleInfoPanelOpenExternalUrl} + active360ItemId={ + panelDetailImage?.itemType === '360' + ? panelDetailImage.id + : null + } /> - )} - - )} + {panelDetailImage && ( + + setActiveDetailImages((current) => { + const next = { ...current }; + delete next[activeInfoPanel.id]; + return next; + }) + } + resolveUrl={resolveUrlWithBlob} + letterboxStyles={letterboxStyles} + cssVars={cssVars} + /> + )} + + ); + })} {/* End inner canvas container */} diff --git a/frontend/src/components/UiElements/InfoPanelOverlay.tsx b/frontend/src/components/UiElements/InfoPanelOverlay.tsx index e19cad7..8117271 100644 --- a/frontend/src/components/UiElements/InfoPanelOverlay.tsx +++ b/frontend/src/components/UiElements/InfoPanelOverlay.tsx @@ -53,6 +53,10 @@ interface InfoPanelOverlayProps { onOpenExternalUrl?: (url: string) => void; /** Callback when a media item should replace the current screen background */ onUseAsBackground?: (image: InfoPanelImage) => void; + /** Whether this overlay instance should render the fullscreen backdrop layer */ + renderBackdrop?: boolean; + /** Optional close handler for backdrop clicks; defaults to onClose */ + onBackdropClose?: () => void; isEditMode?: boolean; /** Callback when panel position changes (edit mode only) */ onPanelPositionChange?: (xPercent: number, yPercent: number) => void; @@ -140,6 +144,8 @@ const InfoPanelOverlay: React.FC = ({ onNavigateToPage, onOpenExternalUrl, onUseAsBackground, + renderBackdrop = true, + onBackdropClose, isEditMode = false, onPanelPositionChange, active360ItemId, @@ -371,20 +377,20 @@ const InfoPanelOverlay: React.FC = ({ const handleBackdropClick = useCallback( (e: React.MouseEvent) => { if (e.target === overlayRef.current && !isEditMode) { - onClose(); + (onBackdropClose || onClose)(); } }, - [onClose, isEditMode], + [onBackdropClose, onClose, isEditMode], ); // Handle touch on backdrop const handleBackdropTouch = useCallback( (e: React.TouchEvent) => { if (e.target === overlayRef.current && !isEditMode) { - onClose(); + (onBackdropClose || onClose)(); } }, - [onClose, isEditMode], + [onBackdropClose, onClose, isEditMode], ); // Extract panel styling from element @@ -462,16 +468,20 @@ const InfoPanelOverlay: React.FC = ({ return ( <> {/* Backdrop overlay - separate from panel for correct z-index stacking */} -
+ {renderBackdrop && ( +
+ )} {/* Inner container constrained to canvas bounds */}
value === true || (typeof value === 'string' && value.trim().toLowerCase() === 'true'); +/** + * Find all enabled Info Panel elements configured to open with the page. + */ +export const findDefaultOpenInfoPanelElements = ( + elements: CanvasElement[], +): CanvasElement[] => + elements.filter( + (element) => + isInfoPanelElementType(element.type) && + isElementFlagEnabled(element.infoPanelOpenByDefault) && + !isElementFlagEnabled(element.infoPanelDisabled), + ); + /** * Normalize appearDelaySec value */ diff --git a/frontend/src/pages/constructor.tsx b/frontend/src/pages/constructor.tsx index 9dd846c..17a6768 100644 --- a/frontend/src/pages/constructor.tsx +++ b/frontend/src/pages/constructor.tsx @@ -64,6 +64,7 @@ import { normalizeAppearDurationSec, ELEMENT_TYPE_LABELS, getNavigationButtonKind, + findDefaultOpenInfoPanelElements, isElementFlagEnabled, isNavigationElementType, isDescriptionElementType, @@ -320,12 +321,14 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => { initialIndex: number; } | null>(null); // Info panel overlay state - const [activeInfoPanel, setActiveInfoPanel] = useState<{ - elementId: string; - } | null>(null); - const [activeDetailImage, setActiveDetailImage] = - useState(null); + const [activeInfoPanelIds, setActiveInfoPanelIds] = useState([]); + const defaultInfoPanelPageIdRef = useRef(null); + const elementsPageIdRef = useRef(null); + const [activeDetailImages, setActiveDetailImages] = useState< + Record + >({}); const [activeInfoPanelGallery, setActiveInfoPanelGallery] = useState<{ + panelId: string; items: GalleryCarouselMediaItem[]; initialIndex: number; } | null>(null); @@ -430,11 +433,14 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => { ); }, [activeGalleryCarousel, elements]); - // Look up current element for info panel (so it receives updates from element editor) - const activeInfoPanelElement = useMemo(() => { - if (!activeInfoPanel) return null; - return elements.find((el) => el.id === activeInfoPanel.elementId) || null; - }, [activeInfoPanel, elements]); + // Look up current elements for info panels (so they receive updates). + const activeInfoPanelElements = useMemo( + () => + activeInfoPanelIds + .map((panelId) => elements.find((element) => element.id === panelId)) + .filter((element): element is CanvasElement => Boolean(element)), + [activeInfoPanelIds, elements], + ); // In edit mode, show overlay when info_panel element is selected (even without click) const editModeInfoPanelElement = useMemo(() => { @@ -445,18 +451,42 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => { // In edit mode the overlay is only a selected-element preview. Do not keep // runtime-open Info Panel state above the canvas after selection is cleared. - const infoPanelElementToRender = isConstructorEditMode + const infoPanelElementsToRender = isConstructorEditMode ? editModeInfoPanelElement - : activeInfoPanelElement; - const shouldShowInfoPanelOverlays = !!infoPanelElementToRender; + ? [editModeInfoPanelElement] + : [] + : activeInfoPanelElements; + const shouldShowInfoPanelOverlays = infoPanelElementsToRender.length > 0; + const activeInfoPanelGalleryElement = activeInfoPanelGallery + ? elements.find( + (element) => element.id === activeInfoPanelGallery.panelId, + ) || null + : null; // Reset info panel state when switching between edit and interact modes useEffect(() => { - setActiveInfoPanel(null); - setActiveDetailImage(null); + setActiveInfoPanelIds([]); + setActiveDetailImages({}); setActiveInfoPanelGallery(null); }, [isConstructorEditMode]); + useEffect(() => { + if (isConstructorEditMode) { + defaultInfoPanelPageIdRef.current = null; + return; + } + if (elementsPageIdRef.current !== activePageId) return; + if (!activePageId || defaultInfoPanelPageIdRef.current === activePageId) + return; + + defaultInfoPanelPageIdRef.current = activePageId; + const defaultOpenInfoPanels = findDefaultOpenInfoPanelElements(elements); + + setActiveInfoPanelIds(defaultOpenInfoPanels.map((element) => element.id)); + setActiveDetailImages({}); + setActiveInfoPanelGallery(null); + }, [activePageId, elements, isConstructorEditMode]); + // Draggable panels using useDraggable hook const { position: toolbarPosition, onDragStart: onToolbarDragStart } = useDraggable({ @@ -668,8 +698,8 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => { pages.find((page) => page.slug === targetPageSlug) || null; if (!targetPage) return; - setActiveInfoPanel(null); - setActiveDetailImage(null); + setActiveInfoPanelIds([]); + setActiveDetailImages({}); setActiveInfoPanelGallery(null); switchToPage(targetPage).then(() => { clearSelection(); @@ -688,7 +718,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => { }, []); const handleInfoPanelUseAsBackground = useCallback( - (item: InfoPanelImage) => { + (panelId: string, item: InfoPanelImage) => { const mediaType = item.itemType === 'video' ? 'video' @@ -715,14 +745,20 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => { : '', navCurrentBgAudioUrl, ); - setActiveDetailImage(null); - setActiveInfoPanelGallery(null); + setActiveDetailImages((current) => { + const next = { ...current }; + delete next[panelId]; + return next; + }); + setActiveInfoPanelGallery((current) => + current?.panelId === panelId ? null : current, + ); }, [navCurrentBgAudioUrl, navSetBackgroundDirectly], ); const handleInfoPanelOpenGallery = useCallback( - (items: InfoPanelImage[], initialIndex: number) => { + (panelId: string, items: InfoPanelImage[], initialIndex: number) => { const activeItemId = items[initialIndex]?.id; const galleryItems = items .map((item) => { @@ -749,8 +785,13 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => { .filter((item): item is GalleryCarouselMediaItem => Boolean(item)); if (galleryItems.length === 0) return; - setActiveDetailImage(null); + setActiveDetailImages((current) => { + const next = { ...current }; + delete next[panelId]; + return next; + }); setActiveInfoPanelGallery({ + panelId, items: galleryItems, initialIndex: Math.max( 0, @@ -1102,13 +1143,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => { if (createdPage?.id) { setSuccessMessage('Page duplicated.'); } - }, [ - activePage, - activePageId, - duplicatePage, - existingSlugs, - saveConstructor, - ]); + }, [activePage, activePageId, duplicatePage, existingSlugs, saveConstructor]); const handleShowDeletePageModal = useCallback(() => { if (!activePageId || !activePage) { @@ -1135,7 +1170,9 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => { const currentIndex = sortedPages.findIndex( (page) => page.id === activePageId, ); - const remainingPages = sortedPages.filter((page) => page.id !== activePageId); + const remainingPages = sortedPages.filter( + (page) => page.id !== activePageId, + ); const fallbackPage = currentIndex >= 0 ? remainingPages[Math.min(currentIndex, remainingPages.length - 1)] @@ -1226,6 +1263,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => { useEffect(() => { if (!activePage) { + elementsPageIdRef.current = null; setElements([]); clearSelection(); updateBackgroundFromPage(null); @@ -1480,6 +1518,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => { }) : []; + elementsPageIdRef.current = activePage.id; setElements(normalizedElements); setSelectedMenuItem('none'); @@ -1831,8 +1870,14 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => { (element: CanvasElement) => { if (isConstructorEditMode) return; if (isInfoPanelElementType(element.type)) { - setActiveInfoPanel({ elementId: element.id }); - setActiveDetailImage(null); + setActiveInfoPanelIds((current) => + current.includes(element.id) ? current : [...current, element.id], + ); + setActiveDetailImages((current) => { + const next = { ...current }; + delete next[element.id]; + return next; + }); } }, [isConstructorEditMode], @@ -2385,9 +2430,9 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => { ) } onInfoPanelClick={() => handleInfoPanelClick(element)} - isInfoPanelOpen={ - activeInfoPanel?.elementId === element.id - } + isInfoPanelOpen={activeInfoPanelIds.includes( + element.id, + )} letterboxStyles={letterboxStyles} pageTransitionSettings={transitionSettings} preloadCache={{ @@ -2493,105 +2538,153 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => { /> )} - {activeInfoPanelGallery && ( + {activeInfoPanelGallery && activeInfoPanelGalleryElement && ( setActiveInfoPanelGallery(null)} resolveUrl={resolveUrlWithBlob} - prevIconUrl={infoPanelElementToRender?.galleryCarouselPrevIconUrl} - nextIconUrl={infoPanelElementToRender?.galleryCarouselNextIconUrl} - backIconUrl={infoPanelElementToRender?.galleryCarouselBackIconUrl} + prevIconUrl={activeInfoPanelGalleryElement.galleryCarouselPrevIconUrl} + nextIconUrl={activeInfoPanelGalleryElement.galleryCarouselNextIconUrl} + backIconUrl={activeInfoPanelGalleryElement.galleryCarouselBackIconUrl} backLabel={ - infoPanelElementToRender?.galleryCarouselBackLabel || 'BACK' + activeInfoPanelGalleryElement.galleryCarouselBackLabel || 'BACK' } - prevX={infoPanelElementToRender?.galleryCarouselPrevX} - prevY={infoPanelElementToRender?.galleryCarouselPrevY} - nextX={infoPanelElementToRender?.galleryCarouselNextX} - nextY={infoPanelElementToRender?.galleryCarouselNextY} - backX={infoPanelElementToRender?.galleryCarouselBackX} - backY={infoPanelElementToRender?.galleryCarouselBackY} - prevWidth={infoPanelElementToRender?.galleryCarouselPrevWidth} - prevHeight={infoPanelElementToRender?.galleryCarouselPrevHeight} - nextWidth={infoPanelElementToRender?.galleryCarouselNextWidth} - nextHeight={infoPanelElementToRender?.galleryCarouselNextHeight} - backWidth={infoPanelElementToRender?.galleryCarouselBackWidth} - backHeight={infoPanelElementToRender?.galleryCarouselBackHeight} + prevX={activeInfoPanelGalleryElement.galleryCarouselPrevX} + prevY={activeInfoPanelGalleryElement.galleryCarouselPrevY} + nextX={activeInfoPanelGalleryElement.galleryCarouselNextX} + nextY={activeInfoPanelGalleryElement.galleryCarouselNextY} + backX={activeInfoPanelGalleryElement.galleryCarouselBackX} + backY={activeInfoPanelGalleryElement.galleryCarouselBackY} + prevWidth={activeInfoPanelGalleryElement.galleryCarouselPrevWidth} + prevHeight={activeInfoPanelGalleryElement.galleryCarouselPrevHeight} + nextWidth={activeInfoPanelGalleryElement.galleryCarouselNextWidth} + nextHeight={activeInfoPanelGalleryElement.galleryCarouselNextHeight} + backWidth={activeInfoPanelGalleryElement.galleryCarouselBackWidth} + backHeight={activeInfoPanelGalleryElement.galleryCarouselBackHeight} letterboxStyles={letterboxStyles} isEditMode={false} pageTransitionSettings={transitionSettings} - galleryElement={infoPanelElementToRender || undefined} + galleryElement={activeInfoPanelGalleryElement} /> )} {/* Info Panel Overlay */} - {shouldShowInfoPanelOverlays && infoPanelElementToRender && ( - <> - { - setActiveInfoPanel(null); - setActiveDetailImage(null); - setActiveInfoPanelGallery(null); - }} - resolveUrl={resolveUrlWithBlob} - letterboxStyles={letterboxStyles} - cssVars={canvasCssVars} - onImageClick={(image) => setActiveDetailImage(image)} - onOpenGallery={handleInfoPanelOpenGallery} - onUseAsBackground={handleInfoPanelUseAsBackground} - onNavigateToPage={handleInfoPanelNavigateToPage} - onOpenExternalUrl={handleInfoPanelOpenExternalUrl} - onSelectImage={ - isConstructorEditMode - ? (imageId) => { - updateSelectedElement({ - infoPanelSelectedImageId: imageId, + {shouldShowInfoPanelOverlays && + infoPanelElementsToRender.map( + (infoPanelElementToRender, panelIndex) => { + const panelDetailImage = + activeDetailImages[infoPanelElementToRender.id]; + + return ( + + { + setActiveInfoPanelIds((current) => + current.filter( + (panelId) => panelId !== infoPanelElementToRender.id, + ), + ); + setActiveDetailImages((current) => { + const next = { ...current }; + delete next[infoPanelElementToRender.id]; + return next; }); + setActiveInfoPanelGallery((current) => + current?.panelId === infoPanelElementToRender.id + ? null + : current, + ); + }} + resolveUrl={resolveUrlWithBlob} + letterboxStyles={letterboxStyles} + cssVars={canvasCssVars} + renderBackdrop={panelIndex === 0} + onBackdropClose={() => { + setActiveInfoPanelIds([]); + setActiveDetailImages({}); + setActiveInfoPanelGallery(null); + }} + onImageClick={(image) => + setActiveDetailImages((current) => ({ + ...current, + [infoPanelElementToRender.id]: image, + })) } - : undefined - } - isEditMode={isConstructorEditMode} - onPanelPositionChange={ - isConstructorEditMode - ? (xPercent, yPercent) => { - updateSelectedElement({ - panelXPercent: xPercent, - panelYPercent: yPercent, - }); + onOpenGallery={(items, initialIndex) => + handleInfoPanelOpenGallery( + infoPanelElementToRender.id, + items, + initialIndex, + ) } - : undefined - } - active360ItemId={ - activeDetailImage?.itemType === '360' - ? activeDetailImage.id - : null - } - /> - {/* In edit mode, always show detail panel (with placeholder if no image selected) */} - {(activeDetailImage || isConstructorEditMode) && ( - setActiveDetailImage(null)} - resolveUrl={resolveUrlWithBlob} - letterboxStyles={letterboxStyles} - cssVars={canvasCssVars} - isEditMode={isConstructorEditMode} - onDetailPositionChange={ - isConstructorEditMode - ? (xPercent, yPercent) => { - updateSelectedElement({ - detailXPercent: xPercent, - detailYPercent: yPercent, - }); + onUseAsBackground={(item) => + handleInfoPanelUseAsBackground( + infoPanelElementToRender.id, + item, + ) + } + onNavigateToPage={handleInfoPanelNavigateToPage} + onOpenExternalUrl={handleInfoPanelOpenExternalUrl} + onSelectImage={ + isConstructorEditMode + ? (imageId) => { + updateSelectedElement({ + infoPanelSelectedImageId: imageId, + }); + } + : undefined + } + isEditMode={isConstructorEditMode} + onPanelPositionChange={ + isConstructorEditMode + ? (xPercent, yPercent) => { + updateSelectedElement({ + panelXPercent: xPercent, + panelYPercent: yPercent, + }); + } + : undefined + } + active360ItemId={ + panelDetailImage?.itemType === '360' + ? panelDetailImage.id + : null + } + /> + {/* In edit mode, always show detail panel (with placeholder if no image selected) */} + {(panelDetailImage || isConstructorEditMode) && ( + + setActiveDetailImages((current) => { + const next = { ...current }; + delete next[infoPanelElementToRender.id]; + return next; + }) } - : undefined - } - /> - )} - - )} + resolveUrl={resolveUrlWithBlob} + letterboxStyles={letterboxStyles} + cssVars={canvasCssVars} + isEditMode={isConstructorEditMode} + onDetailPositionChange={ + isConstructorEditMode + ? (xPercent, yPercent) => { + updateSelectedElement({ + detailXPercent: xPercent, + detailYPercent: yPercent, + }); + } + : undefined + } + /> + )} + + ); + }, + )} {/* Create Page Modal */} { form.state.infoPanelTriggerFontFamily } infoPanelDisabled={form.state.infoPanelDisabled} + infoPanelOpenByDefault={form.state.infoPanelOpenByDefault} // Header section infoPanelHeaderImageUrl={ form.state.infoPanelHeaderImageUrl diff --git a/frontend/src/pages/project-element-defaults/[id].tsx b/frontend/src/pages/project-element-defaults/[id].tsx index 0cb97e5..ecbd9fd 100644 --- a/frontend/src/pages/project-element-defaults/[id].tsx +++ b/frontend/src/pages/project-element-defaults/[id].tsx @@ -593,6 +593,7 @@ const ProjectElementDefaultDetailsPage = () => { form.state.infoPanelTriggerFontFamily } infoPanelDisabled={form.state.infoPanelDisabled} + infoPanelOpenByDefault={form.state.infoPanelOpenByDefault} // Header section infoPanelHeaderImageUrl={form.state.infoPanelHeaderImageUrl} infoPanelHeaderText={form.state.infoPanelHeaderText} diff --git a/frontend/src/types/constructor.ts b/frontend/src/types/constructor.ts index c00d008..ad0aa4c 100644 --- a/frontend/src/types/constructor.ts +++ b/frontend/src/types/constructor.ts @@ -472,6 +472,7 @@ export interface CanvasElement extends BaseCanvasElement { infoPanelTriggerLabel?: string; infoPanelTriggerFontFamily?: string; infoPanelDisabled?: boolean; + infoPanelOpenByDefault?: boolean; // Component 2: Info Panel (section-based like Gallery) // Header section