353 lines
11 KiB
TypeScript
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;
|