39948-vm/frontend/src/components/Constructor/ConstructorToolbar.tsx
2026-05-05 17:25:53 +02:00

353 lines
11 KiB
TypeScript

/**
* 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,
mdiImageMultiple,
mdiViewCarousel,
mdiTooltipText,
mdiSwapHorizontal,
mdiText,
mdiPlus,
mdiExitToApp,
mdiChevronLeft,
mdiChevronRight,
mdiMusicNote,
mdiVideo,
} 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<HTMLDivElement, ConstructorToolbarProps>(
(
{
position,
onDragStart,
pages,
activePageId,
onPageChange,
interactionMode,
onModeChange,
onSelectMenuItem,
allowedNavigationTypes,
onAddElement,
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<HTMLButtonElement>(null);
const elementsTriggerRef = useRef<HTMLButtonElement>(null);
// 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 items-center gap-1.5 px-3 py-2 rounded text-sm font-medium text-white/90 hover:bg-white/20 transition-colors';
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';
// Collapsed state
if (isCollapsed) {
return (
<div
ref={ref}
className='fixed z-[1000] flex items-center gap-1 px-2 py-1.5 rounded-lg bg-white/10 backdrop-blur-xl border border-white/30 shadow-xl'
style={{ left: position.x, top: position.y }}
>
<div
className='cursor-move flex items-center justify-center w-10 h-10 text-white/60'
onMouseDown={onDragStart}
>
<BaseIcon path={mdiDotsVertical} size={24} />
</div>
<button
type='button'
onClick={() => setIsCollapsed(false)}
className='flex items-center justify-center w-10 h-10 rounded text-white/60 hover:text-white/90 hover:bg-white/20 transition-colors'
title='Expand toolbar'
>
<BaseIcon path={mdiChevronRight} size={26} />
</button>
<span className='text-base font-medium text-white/80 truncate max-w-[140px] pr-2'>
{pages.find((p) => p.id === activePageId)?.name || 'Page'}
</span>
</div>
);
}
return (
<div
ref={ref}
className='fixed z-[1000] flex items-center gap-2 px-2 py-1.5 rounded-lg bg-white/10 backdrop-blur-xl border border-white/30 shadow-xl max-w-[95vw]'
style={{ left: position.x, top: position.y }}
>
{/* Drag Handle */}
<div
className='cursor-move flex items-center justify-center w-10 h-10 text-white/60 hover:text-white/90'
onMouseDown={onDragStart}
>
<BaseIcon path={mdiDotsVertical} size={24} />
</div>
{/* Page Selector - reuse existing component */}
<PageSelector
pages={pages}
activePageId={activePageId}
onPageChange={onPageChange}
/>
{/* Mode Toggle - reuse with compact=true */}
<InteractionModeToggle
mode={interactionMode}
onModeChange={onModeChange}
compact
/>
{/* Divider */}
<div className='w-px h-8 bg-white/30' />
{/* Backgrounds Dropdown */}
<div className='relative'>
<button
ref={bgTriggerRef}
type='button'
onClick={() => toggleDropdown('bg')}
className={triggerBtnClass}
>
<BaseIcon path={mdiImageMultiple} size={18} />
<span>BG</span>
<BaseIcon path={mdiChevronDown} size={16} />
</button>
{activeDropdown === 'bg' && (
<ClickOutside
onClickOutside={closeDropdown}
excludedElements={[bgTriggerRef]}
>
<div className={dropdownPanelClass}>
<MenuActionButton
icon={mdiImageMultiple}
label='Background Image'
onClick={() =>
handleMenuAction(() => onSelectMenuItem('background_image'))
}
/>
<MenuActionButton
icon={mdiVideo}
label='Background Video'
onClick={() =>
handleMenuAction(() => onSelectMenuItem('background_video'))
}
/>
<MenuActionButton
icon={mdiMusicNote}
label='Background Audio'
onClick={() =>
handleMenuAction(() => onSelectMenuItem('background_audio'))
}
/>
</div>
</ClickOutside>
)}
</div>
{/* Elements Dropdown */}
<div className='relative'>
<button
ref={elementsTriggerRef}
type='button'
onClick={() => toggleDropdown('elements')}
className={triggerBtnClass}
>
<BaseIcon path={mdiPlus} size={18} />
<span>Elements</span>
<BaseIcon path={mdiChevronDown} size={16} />
</button>
{activeDropdown === 'elements' && (
<ClickOutside
onClickOutside={closeDropdown}
excludedElements={[elementsTriggerRef]}
>
<div className={dropdownPanelClass}>
<MenuActionButton
icon={mdiSwapHorizontal}
label='Navigation Button'
onClick={() =>
handleMenuAction(() =>
onAddElement(allowedNavigationTypes[0]),
)
}
/>
<MenuActionButton
icon={mdiSwapHorizontal}
label='Transition'
onClick={() =>
handleMenuAction(() =>
onSelectMenuItem('create_transition'),
)
}
/>
<MenuActionButton
icon={mdiImageMultiple}
label='Gallery'
onClick={() =>
handleMenuAction(() => onAddElement('gallery'))
}
/>
<MenuActionButton
icon={mdiViewCarousel}
label='Carousel'
onClick={() =>
handleMenuAction(() => onAddElement('carousel'))
}
/>
<MenuActionButton
icon={mdiTooltipText}
label='Tooltip'
onClick={() =>
handleMenuAction(() => onAddElement('tooltip'))
}
/>
<MenuActionButton
icon={mdiText}
label='Description'
onClick={() =>
handleMenuAction(() => onAddElement('description'))
}
/>
<MenuActionButton
icon={mdiVideo}
label='Video Player'
onClick={() =>
handleMenuAction(() => onAddElement('video_player'))
}
/>
<MenuActionButton
icon={mdiMusicNote}
label='Audio Player'
onClick={() =>
handleMenuAction(() => onAddElement('audio_player'))
}
/>
</div>
</ClickOutside>
)}
</div>
{/* Divider */}
<div className='w-px h-8 bg-white/30' />
{/* Create Page Button */}
<button
type='button'
onClick={onCreatePage}
disabled={isCreatingPage}
className={`${triggerBtnClass} ${isCreatingPage ? 'opacity-50 cursor-not-allowed' : ''}`}
>
<BaseIcon path={mdiPlus} size={18} />
<span>{isCreatingPage ? 'Creating...' : 'Page'}</span>
</button>
{/* Save Button - reuse BaseButton with subtitle */}
<BaseButton
small
color='info'
label={isSaving ? 'Saving...' : 'Save'}
subtitle={
lastSavedAt
? dataFormatter.relativeTimestamp(lastSavedAt)
: undefined
}
onClick={onSave}
disabled={isSaving}
/>
{/* Save to Stage Button */}
<BaseButton
small
color='success'
label={isSavingToStage ? 'Saving...' : 'Stage'}
subtitle={
lastSavedToStageAt
? dataFormatter.relativeTimestamp(lastSavedToStageAt)
: undefined
}
onClick={onSaveToStage}
disabled={isSavingToStage}
/>
{/* Exit Button */}
<button
type='button'
onClick={onExit}
className='flex items-center justify-center w-10 h-10 rounded text-red-400 hover:text-red-300 hover:bg-red-500/20 transition-colors'
title='Exit constructor'
>
<BaseIcon path={mdiExitToApp} size={26} />
</button>
{/* Collapse Toggle */}
<button
type='button'
onClick={() => setIsCollapsed(true)}
className='flex items-center justify-center w-10 h-10 rounded text-white/60 hover:text-white/90 hover:bg-white/20 transition-colors'
title='Collapse toolbar'
>
<BaseIcon path={mdiChevronLeft} size={26} />
</button>
</div>
);
},
);
ConstructorToolbar.displayName = 'ConstructorToolbar';
export default ConstructorToolbar;