/** * ConstructorToolbar Component * * Unified toolbar combining page controls and element actions. * Glassmorphism styling with draggable positioning. */ import React, { useState, useRef, useEffect, forwardRef } from 'react'; import { mdiDotsVertical, mdiChevronDown, mdiDelete, mdiImageMultiple, mdiViewCarousel, mdiSwapHorizontal, mdiText, mdiPlus, mdiExitToApp, mdiChevronLeft, mdiChevronRight, mdiChevronUp, mdiContentDuplicate, mdiContentPaste, mdiMusicNote, mdiVideo, mdiInformationOutline, mdiPanoramaHorizontal, } from '@mdi/js'; import BaseIcon from '../BaseIcon'; import BaseButton from '../BaseButton'; import ClickOutside from '../ClickOutside'; import PageSelector from './PageSelector'; import InteractionModeToggle from './InteractionModeToggle'; import MenuActionButton from './MenuActionButton'; import dataFormatter from '../../helpers/dataFormatter'; import type { ConstructorToolbarProps } from './types'; const ConstructorToolbar = forwardRef( ( { position, onDragStart, pages, activePageId, onPageChange, onMovePage, isReorderingPages = false, onDuplicatePage, isDuplicatingPage = false, onDeletePage, canDeletePage = true, isDeletingPage = false, interactionMode, onModeChange, onSelectMenuItem, allowedNavigationTypes, onAddElement, onCopyElement, onPasteElement, canCopyElement = false, canPasteElement = false, onCreatePage, isCreatingPage, onSave, onSaveToStage, isSaving, isSavingToStage, lastSavedAt, lastSavedToStageAt, onExit, }, ref, ) => { // Local UI state const [isCollapsed, setIsCollapsed] = useState(false); const [activeDropdown, setActiveDropdown] = useState< 'bg' | 'elements' | null >(null); // Refs for ClickOutside exclusion (following NavBarItem pattern) const bgTriggerRef = useRef(null); const elementsTriggerRef = useRef(null); const sortedPages = [...pages].sort((a, b) => { const orderA = typeof a.sort_order === 'number' ? a.sort_order : Number.MAX_SAFE_INTEGER; const orderB = typeof b.sort_order === 'number' ? b.sort_order : Number.MAX_SAFE_INTEGER; if (orderA !== orderB) return orderA - orderB; return (a.name || '').localeCompare(b.name || ''); }); const activePageIndex = sortedPages.findIndex( (page) => page.id === activePageId, ); const canMovePageUp = Boolean(onMovePage) && !isReorderingPages && activePageIndex > 0 && sortedPages.length > 1; const canMovePageDown = Boolean(onMovePage) && !isReorderingPages && activePageIndex >= 0 && activePageIndex < sortedPages.length - 1; const canDuplicatePage = Boolean(onDuplicatePage) && !isDuplicatingPage && !isReorderingPages && activePageIndex >= 0; const canDeleteCurrentPage = Boolean(onDeletePage) && canDeletePage && !isDeletingPage && !isReorderingPages && activePageIndex >= 0; const canCopyCurrentElement = Boolean(onCopyElement) && canCopyElement; const canPasteCurrentElement = Boolean(onPasteElement) && canPasteElement; // Keyboard handling (Escape closes dropdown) useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape' && activeDropdown) { setActiveDropdown(null); } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [activeDropdown]); // Dropdown handlers const closeDropdown = () => setActiveDropdown(null); const toggleDropdown = (dropdown: 'bg' | 'elements') => { setActiveDropdown((prev) => (prev === dropdown ? null : dropdown)); }; // Close dropdown after menu action const handleMenuAction = (action: () => void) => { action(); closeDropdown(); }; // Shared button styles const triggerBtnClass = 'flex h-10 items-center gap-1.5 rounded px-3 text-sm font-medium text-white/90 transition-colors hover:bg-white/20'; const iconBtnClass = 'flex h-10 w-10 items-center justify-center rounded border border-white/20 bg-white/10 text-white/70 transition-colors hover:bg-white/20 hover:text-white disabled:cursor-not-allowed disabled:opacity-35'; const sectionClass = 'flex h-[58px] flex-col justify-center gap-1 border-x border-white/15 px-3'; const sectionLabelClass = 'text-center text-[9px] font-semibold uppercase leading-none tracking-wide text-white/50'; const dropdownPanelClass = 'absolute top-full left-0 mt-1 min-w-[180px] py-1 rounded-lg bg-white/50 backdrop-blur-xl border border-white/30 shadow-lg z-10 flex flex-col items-start'; // Collapsed state if (isCollapsed) { return (
{pages.find((p) => p.id === activePageId)?.name || 'Page'}
); } return (
{/* Drag Handle */}
Page actions
{activeDropdown === 'bg' && (
handleMenuAction(() => onSelectMenuItem('background_image'), ) } /> handleMenuAction(() => onSelectMenuItem('background_video'), ) } /> handleMenuAction(() => onSelectMenuItem('background_embed'), ) } /> handleMenuAction(() => onSelectMenuItem('background_audio'), ) } />
)}
Elements actions
{activeDropdown === 'elements' && (
handleMenuAction(() => onAddElement(allowedNavigationTypes[0]), ) } /> handleMenuAction(() => onAddElement('gallery')) } /> handleMenuAction(() => onAddElement('carousel')) } /> handleMenuAction(() => onAddElement('description')) } /> handleMenuAction(() => onAddElement('video_player')) } /> handleMenuAction(() => onAddElement('audio_player')) } /> handleMenuAction(() => onAddElement('info_panel')) } />
)}
{/* Save Button - reuse BaseButton with subtitle */} {/* Save to Stage Button */} {/* Exit Button */} {/* Collapse Toggle */}
); }, ); ConstructorToolbar.displayName = 'ConstructorToolbar'; export default ConstructorToolbar;