improved menus in constructor

This commit is contained in:
Dmitri 2026-05-05 17:25:53 +02:00
parent 4634ad9207
commit f06a2b2c97
28 changed files with 650 additions and 524 deletions

View File

@ -58,7 +58,7 @@ const BackgroundSettingsEditor: React.FC<BackgroundSettingsEditorProps> = ({
return (
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
{label}
</label>
<select
@ -81,17 +81,17 @@ const BackgroundSettingsEditor: React.FC<BackgroundSettingsEditorProps> = ({
))}
</select>
{durationNote && (
<p className='mt-1 text-[11px] text-gray-500'>{durationNote}</p>
<p className='mt-1 text-[11px] text-white/60'>{durationNote}</p>
)}
{/* Video Playback Settings */}
{showVideoSettings && (
<div className='mt-3 space-y-2 border-t border-gray-200 pt-3'>
<p className='text-[10px] font-semibold uppercase text-gray-500'>
<div className='mt-3 space-y-2 border-t border-white/20 pt-3'>
<p className='text-[10px] font-semibold uppercase text-white/70'>
Playback Settings
</p>
<label className='flex cursor-pointer items-center gap-2 text-[11px] text-gray-700'>
<label className='flex cursor-pointer items-center gap-2 text-[11px] text-white/80'>
<input
type='checkbox'
className='h-3 w-3 rounded border-gray-300'
@ -103,7 +103,7 @@ const BackgroundSettingsEditor: React.FC<BackgroundSettingsEditorProps> = ({
Autoplay
</label>
<label className='flex cursor-pointer items-center gap-2 text-[11px] text-gray-700'>
<label className='flex cursor-pointer items-center gap-2 text-[11px] text-white/80'>
<input
type='checkbox'
className='h-3 w-3 rounded border-gray-300'
@ -115,7 +115,7 @@ const BackgroundSettingsEditor: React.FC<BackgroundSettingsEditorProps> = ({
Loop
</label>
<label className='flex cursor-pointer items-center gap-2 text-[11px] text-gray-700'>
<label className='flex cursor-pointer items-center gap-2 text-[11px] text-white/80'>
<input
type='checkbox'
className='h-3 w-3 rounded border-gray-300'
@ -129,7 +129,7 @@ const BackgroundSettingsEditor: React.FC<BackgroundSettingsEditorProps> = ({
<div className='flex gap-2'>
<div className='flex-1'>
<label className='mb-1 block text-[10px] text-gray-500'>
<label className='mb-1 block text-[10px] text-white/60'>
Start (sec)
</label>
<input
@ -149,7 +149,7 @@ const BackgroundSettingsEditor: React.FC<BackgroundSettingsEditorProps> = ({
/>
</div>
<div className='flex-1'>
<label className='mb-1 block text-[10px] text-gray-500'>
<label className='mb-1 block text-[10px] text-white/60'>
End (sec)
</label>
<input

View File

@ -1,77 +0,0 @@
/**
* ConstructorControlsPanel Component
*
* Draggable panel with page selector, mode toggle, and exit button.
* Used in constructor for top-level navigation controls.
*/
import React from 'react';
import BaseButton from '../BaseButton';
import { mdiExitToApp } from '@mdi/js';
import PageSelector from './PageSelector';
import InteractionModeToggle from './InteractionModeToggle';
import type { Position, TourPage, ConstructorInteractionMode } from './types';
interface ConstructorControlsPanelProps {
projectId: string;
pages: TourPage[];
activePageId: string;
interactionMode: ConstructorInteractionMode;
position: Position;
onPageChange: (pageId: string) => void;
onModeChange: (mode: ConstructorInteractionMode) => void;
onDragStart: (event: React.MouseEvent) => void;
}
const ConstructorControlsPanel: React.FC<ConstructorControlsPanelProps> = ({
projectId,
pages,
activePageId,
interactionMode,
position,
onPageChange,
onModeChange,
onDragStart,
}) => {
return (
<div
className='fixed z-[1000] w-[min(92vw,460px)] rounded-lg border border-gray-200 bg-white shadow-xl'
style={{
left: position.x,
top: position.y,
}}
>
<div
className='flex cursor-move items-center justify-between rounded-t-lg border-b border-gray-200 bg-gray-50 px-3 py-2'
onMouseDown={onDragStart}
>
<span className='text-xs font-bold uppercase'>
Constructor Controls
</span>
</div>
<div className='space-y-2 p-3'>
<div className='flex flex-wrap items-center gap-2'>
<PageSelector
pages={pages}
activePageId={activePageId}
onPageChange={onPageChange}
/>
<BaseButton
color='lightDark'
label='Exit to Assets'
icon={mdiExitToApp}
href={
projectId ? `/projects/${projectId}` : '/projects/projects-list'
}
/>
</div>
<InteractionModeToggle
mode={interactionMode}
onModeChange={onModeChange}
/>
</div>
</div>
);
};
export default ConstructorControlsPanel;

View File

@ -1,195 +0,0 @@
/**
* ConstructorMenu Component
*
* Draggable menu panel with actions for adding elements, backgrounds, etc.
*/
import React, { forwardRef } from 'react';
import BaseIcon from '../BaseIcon';
import BaseButton from '../BaseButton';
import {
mdiMenu,
mdiImageMultiple,
mdiViewCarousel,
mdiTooltipText,
mdiSwapHorizontal,
mdiText,
mdiPlus,
mdiExitToApp,
} from '@mdi/js';
import MenuActionButton from './MenuActionButton';
import dataFormatter from '../../helpers/dataFormatter';
import type {
Position,
CanvasElementType,
NavigationElementType,
} from './types';
import type { EditorMenuItem } from '../../types/constructor';
interface ConstructorMenuProps {
position: Position;
isOpen: boolean;
allowedNavigationTypes: NavigationElementType[];
isCreatingPage: boolean;
isSaving: boolean;
isSavingToStage: boolean;
onDragStart: (event: React.MouseEvent) => void;
onToggleOpen: () => void;
onSelectMenuItem: (item: EditorMenuItem) => void;
onAddElement: (type: CanvasElementType) => void;
onCreatePage: () => void;
onSave: () => void;
onSaveToStage: () => void;
onExit: () => void;
/** Page's last saved timestamp (updatedAt from tour_pages) */
lastSavedAt?: string | null;
/** Last save-to-stage timestamp */
lastSavedToStageAt?: string | null;
}
const ConstructorMenu = forwardRef<HTMLDivElement, ConstructorMenuProps>(
(
{
position,
isOpen,
allowedNavigationTypes,
isCreatingPage,
isSaving,
isSavingToStage,
onDragStart,
onToggleOpen,
onSelectMenuItem,
onAddElement,
onCreatePage,
onSave,
onSaveToStage,
onExit,
lastSavedAt,
lastSavedToStageAt,
},
ref,
) => {
return (
<div
ref={ref}
className='fixed z-[1000] w-60 border border-gray-200 rounded-lg bg-white shadow-xl'
style={{ left: position.x, top: position.y }}
>
<div
className='flex items-center justify-between px-3 py-2 border-b border-gray-200 cursor-move bg-gray-50 rounded-t-lg'
onMouseDown={onDragStart}
>
<span className='text-xs font-bold uppercase'>Constructor Menu</span>
<button type='button' onClick={onToggleOpen}>
<BaseIcon path={mdiMenu} size={18} />
</button>
</div>
{isOpen && (
<div className='p-2 space-y-1 max-h-[calc(100vh-120px)] overflow-y-auto'>
<MenuActionButton
icon={mdiImageMultiple}
label='Background Image'
onClick={() => onSelectMenuItem('background_image')}
/>
<MenuActionButton
icon={mdiViewCarousel}
label='Background Video'
onClick={() => onSelectMenuItem('background_video')}
/>
<MenuActionButton
icon={mdiTooltipText}
label='Background Audio'
onClick={() => onSelectMenuItem('background_audio')}
/>
<MenuActionButton
icon={mdiSwapHorizontal}
label='Add Navigation Button'
onClick={() => onAddElement(allowedNavigationTypes[0])}
/>
<MenuActionButton
icon={mdiSwapHorizontal}
label='Add Transition'
onClick={() => onSelectMenuItem('create_transition')}
/>
<MenuActionButton
icon={mdiImageMultiple}
label='Add Gallery'
onClick={() => onAddElement('gallery')}
/>
<MenuActionButton
icon={mdiViewCarousel}
label='Add Carousel'
onClick={() => onAddElement('carousel')}
/>
<MenuActionButton
icon={mdiTooltipText}
label='Add Tooltip'
onClick={() => onAddElement('tooltip')}
/>
<MenuActionButton
icon={mdiText}
label='Add Description'
onClick={() => onAddElement('description')}
/>
<MenuActionButton
icon={mdiViewCarousel}
label='Add Video Player'
onClick={() => onAddElement('video_player')}
/>
<MenuActionButton
icon={mdiTooltipText}
label='Add Audio Player'
onClick={() => onAddElement('audio_player')}
/>
<MenuActionButton
icon={mdiPlus}
label={isCreatingPage ? 'Creating Page...' : 'Create New Page'}
onClick={onCreatePage}
disabled={isCreatingPage}
/>
<div className='pt-2 border-t border-gray-200 space-y-1'>
<BaseButton
small
color='info'
label={isSaving ? 'Saving...' : 'Save'}
subtitle={
lastSavedAt
? dataFormatter.relativeTimestamp(lastSavedAt)
: undefined
}
onClick={onSave}
disabled={isSaving}
className='w-full'
/>
<BaseButton
small
color='success'
label={isSavingToStage ? 'Saving...' : 'Save to Stage'}
subtitle={
lastSavedToStageAt
? dataFormatter.relativeTimestamp(lastSavedToStageAt)
: undefined
}
onClick={onSaveToStage}
disabled={isSavingToStage}
className='w-full'
/>
<MenuActionButton
icon={mdiExitToApp}
label='Exit'
onClick={onExit}
className='!text-red-700'
/>
</div>
</div>
)}
</div>
);
},
);
ConstructorMenu.displayName = 'ConstructorMenu';
export default ConstructorMenu;

View File

@ -0,0 +1,352 @@
/**
* 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;

View File

@ -36,8 +36,8 @@ const CreateTransitionForm: React.FC<CreateTransitionFormProps> = ({
onSubmit,
}) => {
return (
<div className='rounded border border-gray-200 p-2 space-y-2'>
<p className='text-[11px] font-semibold text-gray-600'>
<div className='rounded border border-white/20 p-2 space-y-2'>
<p className='text-[11px] font-semibold text-white/80'>
Create next page transition
</p>
@ -61,11 +61,11 @@ const CreateTransitionForm: React.FC<CreateTransitionFormProps> = ({
))}
</select>
<p className='text-[11px] text-gray-500'>
<p className='text-[11px] text-white/60'>
Transition duration is automatic from video metadata. {durationNote}
</p>
<label className='flex items-center gap-2 text-[11px] text-gray-700'>
<label className='flex items-center gap-2 text-[11px] text-white/80'>
<input
type='checkbox'
checked={supportsReverse}

View File

@ -26,16 +26,16 @@ const ElementEditorHeader: React.FC<ElementEditorHeaderProps> = ({
}) => {
return (
<div
className='mb-3 flex items-center justify-between gap-2 cursor-move'
className='mb-2 flex items-center justify-between gap-1 cursor-move'
onMouseDown={onDragStart}
>
<p className='text-xs font-bold uppercase tracking-wide text-gray-700'>
<p className='text-[10px] font-bold uppercase tracking-wide text-white/90 truncate'>
{title}
</p>
<div className='flex items-center gap-2'>
<div className='flex items-center gap-1.5 shrink-0'>
<button
type='button'
className='text-xs text-gray-700 hover:underline'
className='text-[10px] text-white/70 hover:text-white hover:underline'
onClick={onToggleCollapse}
>
{isCollapsed ? 'Expand' : 'Collapse'}
@ -43,10 +43,10 @@ const ElementEditorHeader: React.FC<ElementEditorHeaderProps> = ({
{showRemoveButton && (
<button
type='button'
className='text-xs text-red-600 hover:underline'
className='text-[10px] text-red-400 hover:text-red-300 hover:underline'
onClick={onRemove}
>
Remove element
Remove
</button>
)}
</div>

View File

@ -183,7 +183,7 @@ export function ElementEditorPanel({
return (
<div
ref={elementEditorRef}
className={`fixed z-[1000] ${isCollapsed ? 'w-[260px]' : 'w-[380px]'} max-h-[calc(100vh-2rem)] overflow-auto rounded-lg border border-gray-200 bg-white/95 p-3 shadow-xl`}
className={`fixed z-[1000] ${isCollapsed ? 'w-[220px]' : 'w-[300px]'} max-h-[calc(100vh-1rem)] overflow-auto rounded-lg border border-white/30 bg-white/10 backdrop-blur-xl p-2 shadow-xl text-sm`}
style={{ left: position.x, top: position.y }}
>
<ElementEditorHeader
@ -547,7 +547,7 @@ export function ElementEditorPanel({
{/* Gallery Section Styles (shown first for gallery elements) */}
{isGalleryElementType(selectedElement.type) && (
<div className='space-y-2 mb-4'>
<p className='text-[11px] font-semibold text-gray-700'>
<p className='text-[11px] font-semibold text-white/90'>
Gallery Section Styles
</p>
<GallerySectionStyleInputs
@ -692,7 +692,7 @@ export function ElementEditorPanel({
showTitleStyles
showAspectRatio
/>
<p className='text-[11px] font-semibold text-gray-700 pt-2'>
<p className='text-[11px] font-semibold text-white/90 pt-2'>
General Element Styles
</p>
</div>

View File

@ -10,23 +10,25 @@ import type { ConstructorInteractionMode } from './types';
interface InteractionModeToggleProps {
mode: ConstructorInteractionMode;
onModeChange: (mode: ConstructorInteractionMode) => void;
compact?: boolean;
}
const InteractionModeToggle: React.FC<InteractionModeToggleProps> = ({
mode,
onModeChange,
compact = false,
}) => {
const isEditMode = mode === 'edit';
return (
<div className='flex flex-wrap items-center gap-2'>
<div className='inline-flex overflow-hidden rounded border border-gray-300 bg-white text-xs font-semibold'>
<div className='inline-flex overflow-hidden rounded border border-white/30 bg-white/10 text-xs font-semibold'>
<button
type='button'
className={`px-3 py-1.5 ${
className={`px-3 py-1.5 transition-colors ${
isEditMode
? 'bg-blue-600 text-white'
: 'text-gray-700 hover:bg-gray-50'
? 'bg-blue-500/80 text-white'
: 'text-white/70 hover:bg-white/10'
}`}
onClick={() => onModeChange('edit')}
>
@ -34,21 +36,23 @@ const InteractionModeToggle: React.FC<InteractionModeToggleProps> = ({
</button>
<button
type='button'
className={`border-l border-gray-300 px-3 py-1.5 ${
className={`border-l border-white/30 px-3 py-1.5 transition-colors ${
!isEditMode
? 'bg-blue-600 text-white'
: 'text-gray-700 hover:bg-gray-50'
? 'bg-blue-500/80 text-white'
: 'text-white/70 hover:bg-white/10'
}`}
onClick={() => onModeChange('interact')}
>
Interact mode
</button>
</div>
<span className='text-[11px] text-gray-600'>
{isEditMode
? 'Drag & configure elements.'
: 'Click and interact with rendered elements.'}
</span>
{!compact && (
<span className='text-[11px] text-white/60'>
{isEditMode
? 'Drag & configure elements.'
: 'Click and interact with rendered elements.'}
</span>
)}
</div>
);
};

View File

@ -2,7 +2,7 @@
* MenuActionButton Component
*
* Compact button for constructor menu actions.
* Used in ConstructorMenu for adding elements, backgrounds, etc.
* Used in ConstructorToolbar for adding elements, backgrounds, etc.
*/
import React from 'react';

View File

@ -45,7 +45,10 @@ const PageSelector: React.FC<PageSelectorProps> = ({
return (
<select
className='rounded border border-gray-300 bg-white px-3 py-2 text-sm'
className='rounded border border-white/30 bg-white/20 pl-3 pr-8 py-1.5 text-sm text-white/90 backdrop-blur-sm focus:outline-none focus:ring-1 focus:ring-white/40 appearance-none bg-no-repeat bg-[length:16px] bg-[right_8px_center]'
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='rgba(255,255,255,0.7)' d='M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z'/%3E%3C/svg%3E")`,
}}
value={activePageId ?? ''}
onChange={(event) => onPageChange(event.target.value)}
disabled={disabled}

View File

@ -3,7 +3,7 @@
*
* Types only - import components directly from their files:
* import CanvasElement from './Constructor/CanvasElement';
* import ConstructorMenu from './Constructor/ConstructorMenu';
* import ConstructorToolbar from './Constructor/ConstructorToolbar';
*/
export * from './types';

View File

@ -83,6 +83,7 @@ export interface PageSelectorProps {
export interface InteractionModeToggleProps {
mode: ConstructorInteractionMode;
onModeChange: (mode: ConstructorInteractionMode) => void;
compact?: boolean;
}
/**
@ -301,6 +302,48 @@ export interface ConstructorMenuProps {
isSavingToStage: boolean;
}
/**
* Unified toolbar props - combines controls panel and menu functionality
* Replaces ConstructorControlsPanelProps and ConstructorMenuProps
*/
export interface ConstructorToolbarProps {
// Positioning (from useDraggable)
position: Position;
onDragStart: (event: React.MouseEvent) => void;
// Page selector (reuse PageSelector component)
pages: TourPage[];
activePageId: string;
onPageChange: (pageId: string) => void;
// Mode toggle (reuse InteractionModeToggle with compact=true)
interactionMode: ConstructorInteractionMode;
onModeChange: (mode: ConstructorInteractionMode) => void;
// Background actions (opens ElementEditorPanel)
onSelectMenuItem: (item: EditorMenuItem) => void;
// Element actions
allowedNavigationTypes: NavigationElementType[];
onAddElement: (type: CanvasElementType) => void;
// Page actions
onCreatePage: () => void;
isCreatingPage: boolean;
// Save actions (reuse BaseButton with subtitle for timestamps)
onSave: () => void;
onSaveToStage: () => void;
isSaving: boolean;
isSavingToStage: boolean;
lastSavedAt?: string | null;
lastSavedToStageAt?: string | null;
// Exit
projectId: string;
onExit: () => void;
}
/**
* Menu action button props
*/

View File

@ -71,14 +71,14 @@ const CarouselSettingsSectionCompact: React.FC<
/>
<label
htmlFor='carouselFullWidth'
className='text-[11px] text-gray-700'
className='text-[11px] text-white/80'
>
Full-width mode (background layer)
</label>
</div>
<div className='rounded border border-gray-200 p-2 space-y-2'>
<p className='text-[11px] font-semibold text-gray-700'>
<div className='rounded border border-white/20 p-2 space-y-2'>
<p className='text-[11px] font-semibold text-white/90'>
Navigation icons
</p>
@ -177,7 +177,7 @@ const CarouselSettingsSectionCompact: React.FC<
)}
<div>
<label className='text-[10px] text-gray-600'>Caption font:</label>
<label className='text-[10px] text-white/70'>Caption font:</label>
<select
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={carouselCaptionFontFamily}
@ -195,7 +195,7 @@ const CarouselSettingsSectionCompact: React.FC<
</div>
{carouselFullWidth && (
<p className='text-[10px] text-gray-500 mt-1'>
<p className='text-[10px] text-white/60 mt-1'>
In full-width mode: set icon + dimensions for navigation-style
buttons. Drag to reposition in editor.
</p>
@ -203,7 +203,7 @@ const CarouselSettingsSectionCompact: React.FC<
</div>
<div className='flex items-center justify-between'>
<p className='text-[11px] font-semibold text-gray-600'>
<p className='text-[11px] font-semibold text-white/80'>
Carousel slides
</p>
<button
@ -218,10 +218,10 @@ const CarouselSettingsSectionCompact: React.FC<
{carouselSlides.map((slide, index) => (
<div
key={slide.id}
className='rounded border border-gray-200 p-2 space-y-2'
className='rounded border border-white/20 p-2 space-y-2'
>
<div className='flex items-center justify-between'>
<p className='text-[11px] font-semibold text-gray-700'>
<p className='text-[11px] font-semibold text-white/90'>
Slide {index + 1}
</p>
<button
@ -264,7 +264,7 @@ const CarouselSettingsSectionCompact: React.FC<
))}
{carouselSlides.length === 0 && (
<p className='text-[11px] text-gray-500'>
<p className='text-[11px] text-white/60'>
No slides yet. Click &quot;+ Add slide&quot; to create one.
</p>
)}

View File

@ -16,25 +16,25 @@ const CommonSettingsSectionCompact: React.FC<CommonSettingsSectionProps> = ({
showLabel = true,
}) => {
return (
<div className='mb-2 space-y-2'>
<div className='mb-1.5 space-y-1.5'>
{showLabel && (
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-0.5 block text-[10px] font-semibold text-white/80'>
Label
</label>
<input
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
className='w-full rounded border border-gray-300 px-1.5 py-0.5 text-[11px]'
value={label}
onChange={(event) => onChange('label', event.target.value)}
/>
</div>
)}
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-0.5 block text-[10px] font-semibold text-white/80'>
Appear delay (sec)
</label>
<input
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
className='w-full rounded border border-gray-300 px-1.5 py-0.5 text-[11px]'
type='number'
min='0'
step='0.1'
@ -43,11 +43,11 @@ const CommonSettingsSectionCompact: React.FC<CommonSettingsSectionProps> = ({
/>
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-0.5 block text-[10px] font-semibold text-white/80'>
Appear duration (sec)
</label>
<input
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
className='w-full rounded border border-gray-300 px-1.5 py-0.5 text-[11px]'
type='number'
min='0.1'
step='0.1'
@ -57,7 +57,7 @@ const CommonSettingsSectionCompact: React.FC<CommonSettingsSectionProps> = ({
onChange('appearDurationSec', event.target.value)
}
/>
<p className='mt-1 text-[11px] text-gray-500'>
<p className='mt-0.5 text-[10px] text-white/60'>
Leave empty for unlimited.
</p>
</div>

View File

@ -42,7 +42,7 @@ const DescriptionSettingsSectionCompact: React.FC<
return (
<div className='space-y-2'>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Icon
</label>
<select
@ -64,7 +64,7 @@ const DescriptionSettingsSectionCompact: React.FC<
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Description title
</label>
<input
@ -75,7 +75,7 @@ const DescriptionSettingsSectionCompact: React.FC<
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Description text
</label>
<textarea
@ -87,7 +87,7 @@ const DescriptionSettingsSectionCompact: React.FC<
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Title font size (px)
</label>
<input
@ -101,7 +101,7 @@ const DescriptionSettingsSectionCompact: React.FC<
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Text font size (px)
</label>
<input
@ -115,7 +115,7 @@ const DescriptionSettingsSectionCompact: React.FC<
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Title font family
</label>
<select
@ -135,7 +135,7 @@ const DescriptionSettingsSectionCompact: React.FC<
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Text font family
</label>
<select
@ -155,7 +155,7 @@ const DescriptionSettingsSectionCompact: React.FC<
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Title color
</label>
<input
@ -169,7 +169,7 @@ const DescriptionSettingsSectionCompact: React.FC<
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Text color
</label>
<input

View File

@ -24,12 +24,12 @@ const EffectsSettingsSectionCompact: React.FC<
<div className='space-y-3'>
{/* Appear Animation */}
<div>
<p className='mb-2 text-[11px] font-semibold text-gray-700'>
<p className='mb-2 text-[11px] font-semibold text-white/90'>
Appear Animation
</p>
<div className='grid grid-cols-2 gap-2'>
<div className='col-span-2'>
<label className='mb-1 block text-[10px] text-gray-500'>Type</label>
<label className='mb-1 block text-[10px] text-white/60'>Type</label>
<select
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={values.appearAnimation || ''}
@ -45,7 +45,7 @@ const EffectsSettingsSectionCompact: React.FC<
</select>
</div>
<div>
<label className='mb-1 block text-[10px] text-gray-500'>
<label className='mb-1 block text-[10px] text-white/60'>
Duration (sec)
</label>
<input
@ -58,7 +58,7 @@ const EffectsSettingsSectionCompact: React.FC<
/>
</div>
<div>
<label className='mb-1 block text-[10px] text-gray-500'>
<label className='mb-1 block text-[10px] text-white/60'>
Easing
</label>
<select
@ -80,12 +80,12 @@ const EffectsSettingsSectionCompact: React.FC<
{/* Hover Effects */}
<div>
<p className='mb-2 text-[11px] font-semibold text-gray-700'>
<p className='mb-2 text-[11px] font-semibold text-white/90'>
Hover Effects
</p>
<div className='grid grid-cols-2 gap-2'>
<div>
<label className='mb-1 block text-[10px] text-gray-500'>
<label className='mb-1 block text-[10px] text-white/60'>
Scale
</label>
<input
@ -96,7 +96,7 @@ const EffectsSettingsSectionCompact: React.FC<
/>
</div>
<div>
<label className='mb-1 block text-[10px] text-gray-500'>
<label className='mb-1 block text-[10px] text-white/60'>
Opacity
</label>
<input
@ -107,7 +107,7 @@ const EffectsSettingsSectionCompact: React.FC<
/>
</div>
<div>
<label className='mb-1 block text-[10px] text-gray-500'>
<label className='mb-1 block text-[10px] text-white/60'>
BG color
</label>
<input
@ -118,7 +118,7 @@ const EffectsSettingsSectionCompact: React.FC<
/>
</div>
<div>
<label className='mb-1 block text-[10px] text-gray-500'>
<label className='mb-1 block text-[10px] text-white/60'>
Text color
</label>
<input
@ -129,7 +129,7 @@ const EffectsSettingsSectionCompact: React.FC<
/>
</div>
<div className='col-span-2'>
<label className='mb-1 block text-[10px] text-gray-500'>
<label className='mb-1 block text-[10px] text-white/60'>
Box shadow
</label>
<input
@ -140,7 +140,7 @@ const EffectsSettingsSectionCompact: React.FC<
/>
</div>
<div className='col-span-2'>
<label className='mb-1 block text-[10px] text-gray-500'>
<label className='mb-1 block text-[10px] text-white/60'>
Transition (sec)
</label>
<input
@ -157,12 +157,12 @@ const EffectsSettingsSectionCompact: React.FC<
{/* Focus Effects */}
<div>
<p className='mb-2 text-[11px] font-semibold text-gray-700'>
<p className='mb-2 text-[11px] font-semibold text-white/90'>
Focus Effects
</p>
<div className='grid grid-cols-2 gap-2'>
<div>
<label className='mb-1 block text-[10px] text-gray-500'>
<label className='mb-1 block text-[10px] text-white/60'>
Scale
</label>
<input
@ -173,7 +173,7 @@ const EffectsSettingsSectionCompact: React.FC<
/>
</div>
<div>
<label className='mb-1 block text-[10px] text-gray-500'>
<label className='mb-1 block text-[10px] text-white/60'>
Opacity
</label>
<input
@ -184,7 +184,7 @@ const EffectsSettingsSectionCompact: React.FC<
/>
</div>
<div>
<label className='mb-1 block text-[10px] text-gray-500'>
<label className='mb-1 block text-[10px] text-white/60'>
Outline
</label>
<input
@ -195,7 +195,7 @@ const EffectsSettingsSectionCompact: React.FC<
/>
</div>
<div>
<label className='mb-1 block text-[10px] text-gray-500'>
<label className='mb-1 block text-[10px] text-white/60'>
Box shadow
</label>
<input
@ -210,12 +210,12 @@ const EffectsSettingsSectionCompact: React.FC<
{/* Active/Press Effects */}
<div>
<p className='mb-2 text-[11px] font-semibold text-gray-700'>
<p className='mb-2 text-[11px] font-semibold text-white/90'>
Active/Press Effects
</p>
<div className='grid grid-cols-2 gap-2'>
<div>
<label className='mb-1 block text-[10px] text-gray-500'>
<label className='mb-1 block text-[10px] text-white/60'>
Scale
</label>
<input
@ -226,7 +226,7 @@ const EffectsSettingsSectionCompact: React.FC<
/>
</div>
<div>
<label className='mb-1 block text-[10px] text-gray-500'>
<label className='mb-1 block text-[10px] text-white/60'>
Opacity
</label>
<input
@ -237,7 +237,7 @@ const EffectsSettingsSectionCompact: React.FC<
/>
</div>
<div className='col-span-2'>
<label className='mb-1 block text-[10px] text-gray-500'>
<label className='mb-1 block text-[10px] text-white/60'>
BG color
</label>
<input
@ -254,17 +254,17 @@ const EffectsSettingsSectionCompact: React.FC<
{/* Slide Transition Override - Gallery/Carousel only */}
{showSlideTransition && (
<div className='mt-3 border-t border-gray-200 pt-3'>
<p className='mb-1 text-[11px] font-semibold text-gray-700'>
<div className='mt-3 border-t border-white/20 pt-3'>
<p className='mb-1 text-[11px] font-semibold text-white/90'>
Slide Transition
</p>
<p className='mb-2 text-[10px] text-gray-500'>
<p className='mb-2 text-[10px] text-white/60'>
Override page transition for slides. Leave empty for defaults.
</p>
<div className='grid grid-cols-2 gap-2'>
{/* Type */}
<div>
<label className='mb-1 block text-[10px] text-gray-500'>
<label className='mb-1 block text-[10px] text-white/60'>
Type
</label>
<select
@ -282,7 +282,7 @@ const EffectsSettingsSectionCompact: React.FC<
{/* Duration */}
<div>
<label className='mb-1 block text-[10px] text-gray-500'>
<label className='mb-1 block text-[10px] text-white/60'>
Duration (ms)
</label>
<input
@ -300,7 +300,7 @@ const EffectsSettingsSectionCompact: React.FC<
{/* Easing */}
<div>
<label className='mb-1 block text-[10px] text-gray-500'>
<label className='mb-1 block text-[10px] text-white/60'>
Easing
</label>
<select
@ -320,7 +320,7 @@ const EffectsSettingsSectionCompact: React.FC<
{/* Overlay Color */}
<div>
<label className='mb-1 block text-[10px] text-gray-500'>
<label className='mb-1 block text-[10px] text-white/60'>
Overlay Color
</label>
<div className='flex gap-1'>

View File

@ -42,15 +42,15 @@ export const ElementSettingsTabsCompact: React.FC<ElementSettingsTabsProps> = ({
tabs,
}) => {
return (
<div className='mb-3 inline-flex w-full overflow-hidden rounded border border-gray-300'>
<div className='mb-2 inline-flex w-full overflow-hidden rounded border border-white/30'>
{tabs.map((tab) => (
<button
key={tab.id}
type='button'
className={`flex-1 px-2 py-1.5 text-[11px] font-semibold transition-colors ${
className={`flex-1 px-1.5 py-1 text-[10px] font-semibold transition-colors ${
activeTab === tab.id
? 'bg-blue-600 text-white'
: 'bg-white text-gray-700 hover:bg-gray-50'
: 'bg-white/20 text-white/80 hover:bg-white/30'
}`}
onClick={() => onTabChange(tab.id)}
>

View File

@ -54,13 +54,13 @@ const GalleryCarouselSettingsSectionCompact: React.FC<
}) => {
return (
<div className='mt-3 space-y-2'>
<div className='rounded border border-gray-200 p-2 space-y-2'>
<p className='text-[11px] font-semibold text-gray-700'>
<div className='rounded border border-white/20 p-2 space-y-2'>
<p className='text-[11px] font-semibold text-white/90'>
Carousel navigation
</p>
{/* Previous button */}
<p className='text-[10px] font-medium text-gray-600 mt-1'>Previous</p>
<p className='text-[10px] font-medium text-white/70 mt-1'>Previous</p>
<select
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={prevIconUrl}
@ -111,7 +111,7 @@ const GalleryCarouselSettingsSectionCompact: React.FC<
)}
{/* Next button */}
<p className='text-[10px] font-medium text-gray-600 mt-1'>Next</p>
<p className='text-[10px] font-medium text-white/70 mt-1'>Next</p>
<select
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={nextIconUrl}
@ -162,7 +162,7 @@ const GalleryCarouselSettingsSectionCompact: React.FC<
)}
{/* Back button */}
<p className='text-[10px] font-medium text-gray-600 mt-1'>Back</p>
<p className='text-[10px] font-medium text-white/70 mt-1'>Back</p>
<select
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={backIconUrl}
@ -221,7 +221,7 @@ const GalleryCarouselSettingsSectionCompact: React.FC<
/>
)}
<p className='text-[10px] text-gray-500 mt-1'>
<p className='text-[10px] text-white/60 mt-1'>
Set icon + dimensions for navigation-style buttons. Drag to
reposition.
</p>

View File

@ -41,13 +41,13 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
showTextAlign = false,
}) => {
return (
<div className='rounded border border-gray-200 p-2 space-y-2'>
<p className='text-[11px] font-semibold text-gray-700'>{sectionLabel}</p>
<div className='rounded border border-white/20 p-2 space-y-2'>
<p className='text-[11px] font-semibold text-white/90'>{sectionLabel}</p>
<div className='grid grid-cols-2 gap-2'>
{/* Background Color */}
<div>
<label className='mb-1 block text-[10px] text-gray-600'>
<label className='mb-1 block text-[10px] text-white/70'>
BG color
</label>
<input
@ -63,7 +63,7 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
{/* Text Color (not for wrapper) */}
{prefix !== 'galleryWrapper' && !showTitleStyles && (
<div>
<label className='mb-1 block text-[10px] text-gray-600'>
<label className='mb-1 block text-[10px] text-white/70'>
Text color
</label>
<input
@ -77,7 +77,7 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
{/* Padding */}
<div>
<label className='mb-1 block text-[10px] text-gray-600'>
<label className='mb-1 block text-[10px] text-white/70'>
Padding
</label>
<input
@ -90,7 +90,7 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
{/* Border Radius */}
<div>
<label className='mb-1 block text-[10px] text-gray-600'>Radius</label>
<label className='mb-1 block text-[10px] text-white/70'>Radius</label>
<input
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={values[`${prefix}BorderRadius`] || ''}
@ -101,7 +101,7 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
{/* Border */}
<div>
<label className='mb-1 block text-[10px] text-gray-600'>Border</label>
<label className='mb-1 block text-[10px] text-white/70'>Border</label>
<input
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={values[`${prefix}Border`] || ''}
@ -113,7 +113,7 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
{/* Text Alignment (optional) */}
{showTextAlign && (
<div>
<label className='mb-1 block text-[10px] text-gray-600'>
<label className='mb-1 block text-[10px] text-white/70'>
Align
</label>
<select
@ -131,7 +131,7 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
{/* Gap (optional) */}
{showGap && (
<div>
<label className='mb-1 block text-[10px] text-gray-600'>Gap</label>
<label className='mb-1 block text-[10px] text-white/70'>Gap</label>
<input
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={values[`${prefix}Gap`] || ''}
@ -144,7 +144,7 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
{/* Backdrop Blur (wrapper only) */}
{showBlur && (
<div>
<label className='mb-1 block text-[10px] text-gray-600'>Blur</label>
<label className='mb-1 block text-[10px] text-white/70'>Blur</label>
<input
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={values[`${prefix}BackdropBlur`] || ''}
@ -159,7 +159,7 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
{/* Grid Columns (optional) */}
{showColumns && (
<div>
<label className='mb-1 block text-[10px] text-gray-600'>
<label className='mb-1 block text-[10px] text-white/70'>
Columns
</label>
<input
@ -179,7 +179,7 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
{/* Font Size (optional) */}
{showFont && (
<div>
<label className='mb-1 block text-[10px] text-gray-600'>
<label className='mb-1 block text-[10px] text-white/70'>
Font size
</label>
<input
@ -194,7 +194,7 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
{/* Font Weight (optional) */}
{showFont && (
<div>
<label className='mb-1 block text-[10px] text-gray-600'>
<label className='mb-1 block text-[10px] text-white/70'>
Font weight
</label>
<input
@ -210,7 +210,7 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
{/* Font Family (optional - full width) */}
{showFont && (
<div>
<label className='mb-1 block text-[10px] text-gray-600'>Font</label>
<label className='mb-1 block text-[10px] text-white/70'>Font</label>
<select
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={values[`${prefix}FontFamily`] || ''}
@ -229,10 +229,10 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
{/* Header Dimensions (header section only) */}
{showDimensions && (
<>
<p className='text-[10px] text-gray-500 pt-1'>Dimensions:</p>
<p className='text-[10px] text-white/60 pt-1'>Dimensions:</p>
<div className='grid grid-cols-2 gap-2'>
<div>
<label className='mb-1 block text-[10px] text-gray-600'>
<label className='mb-1 block text-[10px] text-white/70'>
Width
</label>
<input
@ -243,7 +243,7 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
/>
</div>
<div>
<label className='mb-1 block text-[10px] text-gray-600'>
<label className='mb-1 block text-[10px] text-white/70'>
Height
</label>
<input
@ -254,7 +254,7 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
/>
</div>
<div>
<label className='mb-1 block text-[10px] text-gray-600'>
<label className='mb-1 block text-[10px] text-white/70'>
Min Height
</label>
<input
@ -265,7 +265,7 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
/>
</div>
<div>
<label className='mb-1 block text-[10px] text-gray-600'>
<label className='mb-1 block text-[10px] text-white/70'>
Max Height
</label>
<input
@ -282,10 +282,10 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
{/* Card Aspect Ratio and Min Height (cards only) */}
{showAspectRatio && (
<>
<p className='text-[10px] text-gray-500 pt-1'>Card dimensions:</p>
<p className='text-[10px] text-white/60 pt-1'>Card dimensions:</p>
<div className='grid grid-cols-2 gap-2'>
<div>
<label className='mb-1 block text-[10px] text-gray-600'>
<label className='mb-1 block text-[10px] text-white/70'>
Aspect Ratio
</label>
<select
@ -303,7 +303,7 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
</select>
</div>
<div>
<label className='mb-1 block text-[10px] text-gray-600'>
<label className='mb-1 block text-[10px] text-white/70'>
Min Height
</label>
<input
@ -320,10 +320,10 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
{/* Card Title Styles (cards only) */}
{showTitleStyles && (
<>
<p className='text-[10px] text-gray-500 pt-1'>Card title overlay:</p>
<p className='text-[10px] text-white/60 pt-1'>Card title overlay:</p>
<div className='grid grid-cols-2 gap-2'>
<div>
<label className='mb-1 block text-[10px] text-gray-600'>
<label className='mb-1 block text-[10px] text-white/70'>
Title color
</label>
<input
@ -336,7 +336,7 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
/>
</div>
<div>
<label className='mb-1 block text-[10px] text-gray-600'>
<label className='mb-1 block text-[10px] text-white/70'>
Title BG
</label>
<input
@ -349,7 +349,7 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
/>
</div>
<div>
<label className='mb-1 block text-[10px] text-gray-600'>
<label className='mb-1 block text-[10px] text-white/70'>
Title size
</label>
<input
@ -362,7 +362,7 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
/>
</div>
<div>
<label className='mb-1 block text-[10px] text-gray-600'>
<label className='mb-1 block text-[10px] text-white/70'>
Title weight
</label>
<input
@ -376,7 +376,7 @@ const GallerySectionStyleInputs: React.FC<GallerySectionStyleInputsProps> = ({
</div>
</div>
<div>
<label className='mb-1 block text-[10px] text-gray-600'>
<label className='mb-1 block text-[10px] text-white/70'>
Title shadow
</label>
<input

View File

@ -60,8 +60,8 @@ const GallerySettingsSectionCompact: React.FC<
return (
<div className='space-y-3'>
{/* Header Settings */}
<div className='rounded border border-gray-200 p-2 space-y-2'>
<p className='text-[11px] font-semibold text-gray-700'>
<div className='rounded border border-white/20 p-2 space-y-2'>
<p className='text-[11px] font-semibold text-white/90'>
Gallery header
</p>
@ -104,9 +104,9 @@ const GallerySettingsSectionCompact: React.FC<
</div>
{/* Info Spans */}
<div className='rounded border border-gray-200 p-2 space-y-2'>
<div className='rounded border border-white/20 p-2 space-y-2'>
<div className='flex items-center justify-between'>
<p className='text-[11px] font-semibold text-gray-700'>Info spans</p>
<p className='text-[11px] font-semibold text-white/90'>Info spans</p>
<button
type='button'
className='text-xs text-blue-700 hover:underline'
@ -157,7 +157,7 @@ const GallerySettingsSectionCompact: React.FC<
))}
{galleryInfoSpans.length === 0 && (
<p className='text-[10px] text-gray-500'>
<p className='text-[10px] text-white/60'>
Add spans for brief notes (capacity, price, icons, etc.)
</p>
)}
@ -165,7 +165,7 @@ const GallerySettingsSectionCompact: React.FC<
{/* Gallery Cards */}
<div className='flex items-center justify-between'>
<p className='text-[11px] font-semibold text-gray-600'>Gallery cards</p>
<p className='text-[11px] font-semibold text-white/80'>Gallery cards</p>
<button
type='button'
className='text-xs text-blue-700 hover:underline'
@ -178,10 +178,10 @@ const GallerySettingsSectionCompact: React.FC<
{galleryCards.map((card, index) => (
<div
key={card.id}
className='rounded border border-gray-200 p-2 space-y-2'
className='rounded border border-white/20 p-2 space-y-2'
>
<div className='flex items-center justify-between'>
<p className='text-[11px] font-semibold text-gray-700'>
<p className='text-[11px] font-semibold text-white/90'>
Card {index + 1}
</p>
<button
@ -234,7 +234,7 @@ const GallerySettingsSectionCompact: React.FC<
))}
{galleryCards.length === 0 && (
<p className='text-[11px] text-gray-500'>
<p className='text-[11px] text-white/60'>
No cards yet. Click &quot;+ Add card&quot; to create one.
</p>
)}

View File

@ -39,7 +39,7 @@ const MediaSettingsSectionCompact: React.FC<
return (
<div className='space-y-2'>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
{assetLabel}
</label>
<select
@ -60,7 +60,7 @@ const MediaSettingsSectionCompact: React.FC<
</select>
</div>
<label className='flex items-center gap-2 text-[11px] text-gray-700'>
<label className='flex items-center gap-2 text-[11px] text-white/80'>
<input
type='checkbox'
checked={mediaAutoplay}
@ -69,7 +69,7 @@ const MediaSettingsSectionCompact: React.FC<
Autoplay
</label>
<label className='flex items-center gap-2 text-[11px] text-gray-700'>
<label className='flex items-center gap-2 text-[11px] text-white/80'>
<input
type='checkbox'
checked={mediaLoop}
@ -79,7 +79,7 @@ const MediaSettingsSectionCompact: React.FC<
</label>
{mediaType === 'video' && (
<label className='flex items-center gap-2 text-[11px] text-gray-700'>
<label className='flex items-center gap-2 text-[11px] text-white/80'>
<input
type='checkbox'
checked={mediaMuted}

View File

@ -107,7 +107,7 @@ const NavigationSettingsSectionCompact: React.FC<
return (
<div className='space-y-2'>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Type
</label>
<select
@ -140,7 +140,7 @@ const NavigationSettingsSectionCompact: React.FC<
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Button text
</label>
<input
@ -151,7 +151,7 @@ const NavigationSettingsSectionCompact: React.FC<
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Label font
</label>
<select
@ -171,7 +171,7 @@ const NavigationSettingsSectionCompact: React.FC<
</div>
<div>
<label className='flex items-center gap-2 text-[11px] font-semibold text-gray-600'>
<label className='flex items-center gap-2 text-[11px] font-semibold text-white/80'>
<input
type='checkbox'
checked={navDisabled}
@ -182,7 +182,7 @@ const NavigationSettingsSectionCompact: React.FC<
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Icon
</label>
<select
@ -202,7 +202,7 @@ const NavigationSettingsSectionCompact: React.FC<
))}
</select>
{selectedMediaDurationNote && (
<p className='mt-1 text-[11px] text-gray-500'>
<p className='mt-1 text-[11px] text-white/60'>
{selectedMediaDurationNote}
</p>
)}
@ -210,7 +210,7 @@ const NavigationSettingsSectionCompact: React.FC<
{/* Back navigation info text */}
{currentKind === 'back' && (
<p className='text-[11px] italic text-gray-500'>
<p className='text-[11px] italic text-white/60'>
Back button returns to the previous page using the original forward
transition in reverse.
</p>
@ -220,7 +220,7 @@ const NavigationSettingsSectionCompact: React.FC<
{currentKind === 'forward' && (
<>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Target page
</label>
<select
@ -243,7 +243,7 @@ const NavigationSettingsSectionCompact: React.FC<
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Transition video asset
</label>
<select
@ -265,14 +265,14 @@ const NavigationSettingsSectionCompact: React.FC<
))}
</select>
{selectedTransitionDurationNote && (
<p className='mt-1 text-[11px] text-gray-500'>
<p className='mt-1 text-[11px] text-white/60'>
{selectedTransitionDurationNote}
</p>
)}
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Back transition mode
</label>
<select
@ -298,7 +298,7 @@ const NavigationSettingsSectionCompact: React.FC<
{transitionReverseMode === 'separate_video' && (
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Back transition video asset
</label>
<select
@ -325,11 +325,11 @@ const NavigationSettingsSectionCompact: React.FC<
{/* CSS Transition Settings (when no video selected) */}
{!transitionVideoUrl && (
<>
<p className='mt-2 text-[11px] italic text-gray-500'>
<p className='mt-2 text-[11px] italic text-white/60'>
No transition video selected. Configure CSS transition instead:
</p>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Transition type
</label>
<select
@ -347,7 +347,7 @@ const NavigationSettingsSectionCompact: React.FC<
</select>
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Duration (ms)
</label>
<input
@ -366,7 +366,7 @@ const NavigationSettingsSectionCompact: React.FC<
/>
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Easing
</label>
<select
@ -384,7 +384,7 @@ const NavigationSettingsSectionCompact: React.FC<
</select>
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Overlay color
</label>
<div className='flex gap-2'>
@ -411,7 +411,7 @@ const NavigationSettingsSectionCompact: React.FC<
)}
{transitionVideoUrl && (
<p className='text-[11px] text-gray-500'>
<p className='text-[11px] text-white/60'>
Transition duration is set automatically from the selected video.
</p>
)}

View File

@ -14,12 +14,12 @@ const StyleSettingsSectionCompact: React.FC<StyleSettingsSectionProps> = ({
}) => {
return (
<div className='space-y-2'>
<p className='text-[10px] text-gray-500'>
<p className='text-[10px] text-white/60'>
Dimensions = % of canvas, border/radius = px
</p>
<div className='grid grid-cols-2 gap-2'>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Width (%)
</label>
<input
@ -33,7 +33,7 @@ const StyleSettingsSectionCompact: React.FC<StyleSettingsSectionProps> = ({
/>
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Height (%)
</label>
<input
@ -47,7 +47,7 @@ const StyleSettingsSectionCompact: React.FC<StyleSettingsSectionProps> = ({
/>
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Min W (%)
</label>
<input
@ -60,7 +60,7 @@ const StyleSettingsSectionCompact: React.FC<StyleSettingsSectionProps> = ({
/>
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Max W (%)
</label>
<input
@ -73,7 +73,7 @@ const StyleSettingsSectionCompact: React.FC<StyleSettingsSectionProps> = ({
/>
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Min H (%)
</label>
<input
@ -86,7 +86,7 @@ const StyleSettingsSectionCompact: React.FC<StyleSettingsSectionProps> = ({
/>
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Max H (%)
</label>
<input
@ -99,7 +99,7 @@ const StyleSettingsSectionCompact: React.FC<StyleSettingsSectionProps> = ({
/>
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Margin
</label>
<input
@ -110,7 +110,7 @@ const StyleSettingsSectionCompact: React.FC<StyleSettingsSectionProps> = ({
/>
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Padding
</label>
<input
@ -121,7 +121,7 @@ const StyleSettingsSectionCompact: React.FC<StyleSettingsSectionProps> = ({
/>
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Gap (rem)
</label>
<input
@ -135,7 +135,7 @@ const StyleSettingsSectionCompact: React.FC<StyleSettingsSectionProps> = ({
/>
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Font size
</label>
<input
@ -146,7 +146,7 @@ const StyleSettingsSectionCompact: React.FC<StyleSettingsSectionProps> = ({
/>
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Line height
</label>
<input
@ -156,7 +156,7 @@ const StyleSettingsSectionCompact: React.FC<StyleSettingsSectionProps> = ({
/>
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Font weight
</label>
<input
@ -167,7 +167,7 @@ const StyleSettingsSectionCompact: React.FC<StyleSettingsSectionProps> = ({
/>
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Border (px)
</label>
<input
@ -181,7 +181,7 @@ const StyleSettingsSectionCompact: React.FC<StyleSettingsSectionProps> = ({
/>
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Radius (px)
</label>
<input
@ -195,7 +195,7 @@ const StyleSettingsSectionCompact: React.FC<StyleSettingsSectionProps> = ({
/>
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Opacity
</label>
<input
@ -206,7 +206,7 @@ const StyleSettingsSectionCompact: React.FC<StyleSettingsSectionProps> = ({
/>
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
z-index
</label>
<input
@ -219,7 +219,7 @@ const StyleSettingsSectionCompact: React.FC<StyleSettingsSectionProps> = ({
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Box shadow
</label>
<input
@ -232,7 +232,7 @@ const StyleSettingsSectionCompact: React.FC<StyleSettingsSectionProps> = ({
<div className='grid grid-cols-2 gap-2'>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Display
</label>
<select
@ -250,7 +250,7 @@ const StyleSettingsSectionCompact: React.FC<StyleSettingsSectionProps> = ({
</select>
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Position
</label>
<select
@ -267,7 +267,7 @@ const StyleSettingsSectionCompact: React.FC<StyleSettingsSectionProps> = ({
</select>
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Justify
</label>
<select
@ -285,7 +285,7 @@ const StyleSettingsSectionCompact: React.FC<StyleSettingsSectionProps> = ({
</select>
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Align
</label>
<select
@ -302,7 +302,7 @@ const StyleSettingsSectionCompact: React.FC<StyleSettingsSectionProps> = ({
</select>
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Text align
</label>
<select
@ -318,7 +318,7 @@ const StyleSettingsSectionCompact: React.FC<StyleSettingsSectionProps> = ({
</select>
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
BG color
</label>
<input
@ -329,7 +329,7 @@ const StyleSettingsSectionCompact: React.FC<StyleSettingsSectionProps> = ({
/>
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Text color
</label>
<input

View File

@ -34,7 +34,7 @@ const TooltipSettingsSectionCompact: React.FC<
return (
<div className='space-y-2'>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Icon
</label>
<select
@ -56,7 +56,7 @@ const TooltipSettingsSectionCompact: React.FC<
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Tooltip title
</label>
<input
@ -67,7 +67,7 @@ const TooltipSettingsSectionCompact: React.FC<
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Tooltip text
</label>
<textarea
@ -79,7 +79,7 @@ const TooltipSettingsSectionCompact: React.FC<
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Title font family
</label>
<select
@ -99,7 +99,7 @@ const TooltipSettingsSectionCompact: React.FC<
</div>
<div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
Text font family
</label>
<select

View File

@ -280,7 +280,7 @@ export function useConstructorPageActions({
background_audio_url: '',
background_loop: false,
requires_auth: false,
ui_schema_json: JSON.stringify({ elements: [] }),
ui_schema_json: { elements: [] },
// Copy project design dimensions to new page
design_width: project?.design_width ?? null,
design_height: project?.design_height ?? null,

View File

@ -210,8 +210,8 @@ export const createDefaultElement = (
id: createLocalId(),
type,
label: ELEMENT_TYPE_LABELS[type] || type,
xPercent: clamp(12 + index * 4, 5, 80),
yPercent: clamp(16 + index * 6, 8, 85),
xPercent: clamp(45 + index * 3, 10, 85), // Center horizontally
yPercent: clamp(45 + index * 4, 15, 80), // Center vertically
appearDelaySec: 0,
appearDurationSec: null,
};

View File

@ -20,15 +20,30 @@ export const parseJsonObject = <T>(value?: unknown, fallback?: T): T => {
if (!value) return (fallback || ({} as T)) as T;
try {
if (typeof value === 'string') {
const parsed = JSON.parse(value);
return (parsed || fallback || {}) as T;
let result: unknown = value;
// Handle string input - parse JSON
if (typeof result === 'string') {
result = JSON.parse(result);
// Handle double-encoded JSON (string that parses to another string)
// This can happen if JSON was stringified twice
while (typeof result === 'string') {
try {
result = JSON.parse(result);
} catch {
// Not valid JSON, use fallback
return (fallback || ({} as T)) as T;
}
}
}
if (typeof value === 'object') {
return value as T;
// Ensure we return an object, not a primitive
if (result && typeof result === 'object' && !Array.isArray(result)) {
return result as T;
}
// If it's an array or primitive, return fallback
return (fallback || ({} as T)) as T;
} catch {
return (fallback || ({} as T)) as T;

View File

@ -14,8 +14,7 @@ import { flushSync } from 'react-dom';
import BaseButton from '../components/BaseButton';
import CanvasBackground from '../components/Constructor/CanvasBackground';
import TransitionBlackOverlay from '../components/TransitionBlackOverlay';
import ConstructorControlsPanel from '../components/Constructor/ConstructorControlsPanel';
import ConstructorMenu from '../components/Constructor/ConstructorMenu';
import ConstructorToolbar from '../components/Constructor/ConstructorToolbar';
import TransitionPreviewOverlay from '../components/Constructor/TransitionPreviewOverlay';
import CanvasElementComponent from '../components/Constructor/CanvasElement';
import GalleryCarouselOverlay from '../components/UiElements/GalleryCarouselOverlay';
@ -133,7 +132,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
);
const canvasRef = useRef<HTMLDivElement>(null);
const elementEditorRef = useRef<HTMLDivElement>(null);
const menuRef = useRef<HTMLDivElement>(null);
const toolbarRef = useRef<HTMLDivElement>(null);
const [isAuthReady, setIsAuthReady] = useState(false);
const isElementEditMode = mode === 'element_edit';
@ -278,7 +277,8 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
const [constructorInteractionMode, setConstructorInteractionMode] =
useState<ConstructorInteractionMode>('edit');
const [isMenuOpen, setIsMenuOpen] = useState(false);
// Note: isMenuOpen kept for context backward compatibility, not used by ConstructorToolbar
const [isMenuOpen, setIsMenuOpen] = useState(true);
const [isEditorCollapsed, setIsEditorCollapsed] = useState(false);
const [elementEditorTab, setElementEditorTab] = useState<
'general' | 'css' | 'effects'
@ -353,27 +353,17 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
}, [activeGalleryCarousel, elements]);
// Draggable panels using useDraggable hook
const {
position: constructorControlsPosition,
onDragStart: onConstructorControlsDragStart,
} = useDraggable({
initialPosition: { x: 20, y: 20 },
elementWidth: 460,
elementHeight: 64,
});
const { position: menuPosition, onDragStart: onMenuDragStart } = useDraggable(
{
initialPosition: { x: 9999, y: 10 }, // Top right corner (x will be clamped)
elementWidth: 240,
elementHeight: 60,
},
);
const { position: toolbarPosition, onDragStart: onToolbarDragStart } =
useDraggable({
initialPosition: { x: 20, y: 20 },
elementWidth: 200, // Use collapsed width for bounds - expanded can go off-screen
elementHeight: 56,
});
const { position: editorPosition, onDragStart: onElementEditorDragStart } =
useDraggable({
initialPosition: { x: 0, y: 72 },
elementWidth: isEditorCollapsed ? 260 : 380,
initialPosition: { x: 9999, y: 0 }, // Top-right corner (x will be clamped)
elementWidth: isEditorCollapsed ? 220 : 300,
elementHeight: 60,
});
@ -1141,7 +1131,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
// Ignore clicks on menu to allow menu item selection
useOutsideClick({
containerRef: elementEditorRef,
ignoreRefs: [menuRef],
ignoreRefs: [toolbarRef],
ignoreDataAttribute: 'data-constructor-element-id',
selectedValue: selectedElementId,
onOutsideClick: useCallback(() => {
@ -1648,18 +1638,37 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
</div>
{pages.length > 0 && !isElementEditMode && (
<ConstructorControlsPanel
projectId={projectId}
<ConstructorToolbar
ref={toolbarRef}
position={toolbarPosition}
onDragStart={onToolbarDragStart}
pages={pages}
activePageId={activePageId}
interactionMode={constructorInteractionMode}
position={constructorControlsPosition}
onPageChange={(pageId) => {
const page = pages.find((p) => p.id === pageId);
if (page) switchToPage(page);
}}
interactionMode={constructorInteractionMode}
onModeChange={setConstructorInteractionMode}
onDragStart={onConstructorControlsDragStart}
onSelectMenuItem={selectMenuItemForEdit}
allowedNavigationTypes={allowedNavigationTypes}
onAddElement={addElement}
onCreatePage={createPage}
isCreatingPage={isCreatingPage}
onSave={saveConstructor}
onSaveToStage={handleSaveToStage}
isSaving={isSaving}
isSavingToStage={isSavingToStage}
lastSavedAt={lastProjectSaveAt}
lastSavedToStageAt={lastSavedToStage}
projectId={projectId}
onExit={() =>
router.push(
projectId
? `/projects/${projectId}`
: '/projects/projects-list',
)
}
/>
)}
@ -1801,34 +1810,6 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
title={editorTitle}
/>
)}
{pages.length > 0 && !isElementEditMode && (
<ConstructorMenu
ref={menuRef}
position={menuPosition}
isOpen={isMenuOpen}
allowedNavigationTypes={allowedNavigationTypes}
isCreatingPage={isCreatingPage}
isSaving={isSaving}
isSavingToStage={isSavingToStage}
onDragStart={onMenuDragStart}
onToggleOpen={() => setIsMenuOpen((prev) => !prev)}
onSelectMenuItem={selectMenuItemForEdit}
onAddElement={addElement}
onCreatePage={createPage}
onSave={saveConstructor}
onSaveToStage={handleSaveToStage}
onExit={() =>
router.push(
projectId
? `/projects/${projectId}`
: '/projects/projects-list',
)
}
lastSavedAt={lastProjectSaveAt}
lastSavedToStageAt={lastSavedToStage}
/>
)}
</div>
<TransitionPreviewOverlay