Added ability to set external URL for navigation buttons
This commit is contained in:
parent
490dd98e52
commit
e14db16290
@ -483,6 +483,10 @@ class TourPagesService extends BaseService {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
static isForwardElementWithTarget(element) {
|
static isForwardElementWithTarget(element) {
|
||||||
|
if (element.navigationTargetMode === 'external_url') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const isForward =
|
const isForward =
|
||||||
element.type === 'navigation_next' ||
|
element.type === 'navigation_next' ||
|
||||||
(element.type?.startsWith?.('navigation') &&
|
(element.type?.startsWith?.('navigation') &&
|
||||||
|
|||||||
@ -175,7 +175,6 @@ export function ElementEditorPanel({
|
|||||||
activePageId,
|
activePageId,
|
||||||
allowedNavigationTypes,
|
allowedNavigationTypes,
|
||||||
normalizeNavigationType,
|
normalizeNavigationType,
|
||||||
onPreviewTransition,
|
|
||||||
} = useConstructorNavigation();
|
} = useConstructorNavigation();
|
||||||
|
|
||||||
const { activeTab, setActiveTab } = useConstructorEditorTab();
|
const { activeTab, setActiveTab } = useConstructorEditorTab();
|
||||||
@ -336,7 +335,11 @@ export function ElementEditorPanel({
|
|||||||
}
|
}
|
||||||
navDisabled={selectedElement.navDisabled || false}
|
navDisabled={selectedElement.navDisabled || false}
|
||||||
iconUrl={selectedElement.iconUrl || ''}
|
iconUrl={selectedElement.iconUrl || ''}
|
||||||
|
navigationTargetMode={
|
||||||
|
selectedElement.navigationTargetMode || 'target_page'
|
||||||
|
}
|
||||||
targetPageSlug={selectedElement.targetPageSlug || ''}
|
targetPageSlug={selectedElement.targetPageSlug || ''}
|
||||||
|
externalUrl={selectedElement.externalUrl || ''}
|
||||||
transitionVideoUrl={
|
transitionVideoUrl={
|
||||||
selectedElement.transitionVideoUrl || ''
|
selectedElement.transitionVideoUrl || ''
|
||||||
}
|
}
|
||||||
@ -363,10 +366,25 @@ export function ElementEditorPanel({
|
|||||||
}
|
}
|
||||||
onChange={(prop, value) => {
|
onChange={(prop, value) => {
|
||||||
if (prop === 'type') {
|
if (prop === 'type') {
|
||||||
const nextType = value as NavigationElementType;
|
if (typeof value === 'object') {
|
||||||
updateSelectedElement(
|
const nextType = (value.type ||
|
||||||
normalizeNavigationType(selectedElement, nextType),
|
selectedElement.type) as NavigationElementType;
|
||||||
);
|
updateSelectedElement({
|
||||||
|
...normalizeNavigationType(
|
||||||
|
selectedElement,
|
||||||
|
nextType,
|
||||||
|
),
|
||||||
|
...value,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const nextType = value as NavigationElementType;
|
||||||
|
updateSelectedElement(
|
||||||
|
normalizeNavigationType(
|
||||||
|
selectedElement,
|
||||||
|
nextType,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
} else if (prop === 'transitionVideoUrl') {
|
} else if (prop === 'transitionVideoUrl') {
|
||||||
const nextVideoUrl = value as string;
|
const nextVideoUrl = value as string;
|
||||||
const resolvedDuration = getDuration(nextVideoUrl);
|
const resolvedDuration = getDuration(nextVideoUrl);
|
||||||
@ -380,13 +398,16 @@ export function ElementEditorPanel({
|
|||||||
targetPageSlug: value as string,
|
targetPageSlug: value as string,
|
||||||
targetPageId: '',
|
targetPageId: '',
|
||||||
});
|
});
|
||||||
|
} else if (prop === 'navigationTargetMode') {
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
updateSelectedElement(value);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
updateSelectedElement({
|
updateSelectedElement({
|
||||||
[prop]: value,
|
[prop]: value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onPreviewTransition={onPreviewTransition}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -203,7 +203,6 @@ export interface ElementEditorPanelProps {
|
|||||||
onUpdateTransitionVideoUrl: (url: string) => void;
|
onUpdateTransitionVideoUrl: (url: string) => void;
|
||||||
onUpdateTransitionSupportsReverse: (value: boolean) => void;
|
onUpdateTransitionSupportsReverse: (value: boolean) => void;
|
||||||
onCreateTransition: () => void;
|
onCreateTransition: () => void;
|
||||||
onPreviewTransition: (direction: 'forward' | 'back') => void;
|
|
||||||
|
|
||||||
// Gallery operations
|
// Gallery operations
|
||||||
onAddGalleryCard: () => void;
|
onAddGalleryCard: () => void;
|
||||||
|
|||||||
@ -6,10 +6,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import BaseButton from '../BaseButton';
|
|
||||||
import type {
|
import type {
|
||||||
AssetOption,
|
AssetOption,
|
||||||
NavigationButtonKind,
|
NavigationButtonKind,
|
||||||
|
NavigationTargetMode,
|
||||||
CanvasElementType,
|
CanvasElementType,
|
||||||
} from '../../types/constructor';
|
} from '../../types/constructor';
|
||||||
import type { TransitionType, EasingFunction } from '../../types/transition';
|
import type { TransitionType, EasingFunction } from '../../types/transition';
|
||||||
@ -42,7 +42,9 @@ interface NavigationSettingsSectionCompactProps {
|
|||||||
navLabelFontFamily: string;
|
navLabelFontFamily: string;
|
||||||
navDisabled: boolean;
|
navDisabled: boolean;
|
||||||
iconUrl: string;
|
iconUrl: string;
|
||||||
|
navigationTargetMode: NavigationTargetMode;
|
||||||
targetPageSlug: string;
|
targetPageSlug: string;
|
||||||
|
externalUrl: string;
|
||||||
transitionVideoUrl: string;
|
transitionVideoUrl: string;
|
||||||
transitionReverseMode: 'auto_reverse' | 'separate_video';
|
transitionReverseMode: 'auto_reverse' | 'separate_video';
|
||||||
reverseVideoUrl: string;
|
reverseVideoUrl: string;
|
||||||
@ -67,11 +69,14 @@ interface NavigationSettingsSectionCompactProps {
|
|||||||
| Partial<{
|
| Partial<{
|
||||||
type: NavigationElementType;
|
type: NavigationElementType;
|
||||||
navType: NavigationButtonKind;
|
navType: NavigationButtonKind;
|
||||||
|
navigationTargetMode: NavigationTargetMode;
|
||||||
label?: string;
|
label?: string;
|
||||||
navLabel?: string;
|
navLabel?: string;
|
||||||
|
targetPageSlug?: string;
|
||||||
|
targetPageId?: string;
|
||||||
|
externalUrl?: string;
|
||||||
}>,
|
}>,
|
||||||
) => void;
|
) => void;
|
||||||
onPreviewTransition?: (direction: 'forward' | 'back') => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const NavigationSettingsSectionCompact: React.FC<
|
const NavigationSettingsSectionCompact: React.FC<
|
||||||
@ -83,7 +88,9 @@ const NavigationSettingsSectionCompact: React.FC<
|
|||||||
navLabelFontFamily,
|
navLabelFontFamily,
|
||||||
navDisabled,
|
navDisabled,
|
||||||
iconUrl,
|
iconUrl,
|
||||||
|
navigationTargetMode,
|
||||||
targetPageSlug,
|
targetPageSlug,
|
||||||
|
externalUrl,
|
||||||
transitionVideoUrl,
|
transitionVideoUrl,
|
||||||
transitionReverseMode,
|
transitionReverseMode,
|
||||||
reverseVideoUrl,
|
reverseVideoUrl,
|
||||||
@ -99,10 +106,13 @@ const NavigationSettingsSectionCompact: React.FC<
|
|||||||
selectedMediaDurationNote,
|
selectedMediaDurationNote,
|
||||||
selectedTransitionDurationNote,
|
selectedTransitionDurationNote,
|
||||||
onChange,
|
onChange,
|
||||||
onPreviewTransition,
|
|
||||||
}) => {
|
}) => {
|
||||||
|
const currentTargetMode: NavigationTargetMode =
|
||||||
|
navigationTargetMode === 'external_url' ? 'external_url' : 'target_page';
|
||||||
const currentKind: NavigationButtonKind =
|
const currentKind: NavigationButtonKind =
|
||||||
navType || (type === 'navigation_prev' ? 'back' : 'forward');
|
currentTargetMode === 'external_url'
|
||||||
|
? 'forward'
|
||||||
|
: navType || (type === 'navigation_prev' ? 'back' : 'forward');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
@ -121,7 +131,15 @@ const NavigationSettingsSectionCompact: React.FC<
|
|||||||
const nextType = allowedNavigationTypes.includes(requestedType)
|
const nextType = allowedNavigationTypes.includes(requestedType)
|
||||||
? requestedType
|
? requestedType
|
||||||
: allowedNavigationTypes[0];
|
: allowedNavigationTypes[0];
|
||||||
onChange('type', nextType);
|
onChange('type', {
|
||||||
|
type: nextType,
|
||||||
|
navType: requestedKind,
|
||||||
|
navigationTargetMode:
|
||||||
|
requestedKind === 'back' ? 'target_page' : currentTargetMode,
|
||||||
|
targetPageSlug: requestedKind === 'back' ? '' : targetPageSlug,
|
||||||
|
targetPageId: '',
|
||||||
|
externalUrl: requestedKind === 'back' ? '' : externalUrl,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
@ -221,217 +239,252 @@ const NavigationSettingsSectionCompact: React.FC<
|
|||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
|
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
|
||||||
Target page
|
Destination
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||||
value={targetPageSlug}
|
value={currentTargetMode}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
onChange('targetPageSlug', event.target.value);
|
const nextMode: NavigationTargetMode =
|
||||||
onChange('targetPageId', ''); // Clear legacy ID
|
event.target.value === 'external_url'
|
||||||
|
? 'external_url'
|
||||||
|
: 'target_page';
|
||||||
|
onChange('navigationTargetMode', {
|
||||||
|
type: 'navigation_next',
|
||||||
|
navType: 'forward',
|
||||||
|
navigationTargetMode: nextMode,
|
||||||
|
targetPageSlug:
|
||||||
|
nextMode === 'target_page' ? targetPageSlug : '',
|
||||||
|
targetPageId: '',
|
||||||
|
externalUrl: nextMode === 'external_url' ? externalUrl : '',
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value=''>Not selected</option>
|
<option value='target_page'>Target page</option>
|
||||||
{pages
|
<option value='external_url'>External URL</option>
|
||||||
.filter((page) => page.id !== activePageId)
|
|
||||||
.map((page, index) => (
|
|
||||||
<option key={page.id} value={page.slug || ''}>
|
|
||||||
{page.name || `Page ${index + 1}`}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{currentTargetMode === 'target_page' && (
|
||||||
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
|
|
||||||
Transition video asset
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
|
||||||
value={transitionVideoUrl}
|
|
||||||
onChange={(event) => {
|
|
||||||
onChange('transitionVideoUrl', event.target.value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option value=''>Not selected</option>
|
|
||||||
{addFallbackAssetOption(
|
|
||||||
transitionVideoOptions,
|
|
||||||
transitionVideoUrl,
|
|
||||||
`Current video · ${transitionVideoUrl}`,
|
|
||||||
).map((option) => (
|
|
||||||
<option key={option.value} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
{selectedTransitionDurationNote && (
|
|
||||||
<p className='mt-1 text-[11px] text-white/60'>
|
|
||||||
{selectedTransitionDurationNote}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
|
|
||||||
Back transition mode
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
|
||||||
value={transitionReverseMode}
|
|
||||||
onChange={(event) =>
|
|
||||||
onChange(
|
|
||||||
'transitionReverseMode',
|
|
||||||
event.target.value === 'separate_video'
|
|
||||||
? 'separate_video'
|
|
||||||
: 'auto_reverse',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value='auto_reverse'>
|
|
||||||
Auto reverse transition video
|
|
||||||
</option>
|
|
||||||
<option value='separate_video'>
|
|
||||||
Use separate back-transition video
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{transitionReverseMode === 'separate_video' && (
|
|
||||||
<div>
|
<div>
|
||||||
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
|
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
|
||||||
Back transition video asset
|
Target page
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||||
value={reverseVideoUrl}
|
value={targetPageSlug}
|
||||||
onChange={(event) =>
|
onChange={(event) => {
|
||||||
onChange('reverseVideoUrl', event.target.value)
|
onChange('targetPageSlug', event.target.value);
|
||||||
}
|
onChange('targetPageId', ''); // Clear legacy ID
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<option value=''>Not selected</option>
|
<option value=''>Not selected</option>
|
||||||
{addFallbackAssetOption(
|
{pages
|
||||||
transitionVideoOptions,
|
.filter((page) => page.id !== activePageId)
|
||||||
reverseVideoUrl,
|
.map((page, index) => (
|
||||||
`Current back video · ${reverseVideoUrl}`,
|
<option key={page.id} value={page.slug || ''}>
|
||||||
).map((option) => (
|
{page.name || `Page ${index + 1}`}
|
||||||
<option key={option.value} value={option.value}>
|
</option>
|
||||||
{option.label}
|
))}
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* CSS Transition Settings (when no video selected) */}
|
{currentTargetMode === 'external_url' && (
|
||||||
{!transitionVideoUrl && (
|
<div>
|
||||||
<>
|
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
|
||||||
<p className='mt-2 text-[11px] italic text-white/60'>
|
External URL
|
||||||
No transition video selected. Configure CSS transition instead:
|
</label>
|
||||||
</p>
|
<input
|
||||||
<div>
|
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||||
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
|
placeholder='https://example.com'
|
||||||
Transition type
|
value={externalUrl}
|
||||||
</label>
|
onChange={(event) =>
|
||||||
<select
|
onChange('externalUrl', event.target.value)
|
||||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
}
|
||||||
value={transitionType}
|
|
||||||
onChange={(event) =>
|
|
||||||
onChange('transitionType', event.target.value)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{CSS_TRANSITION_TYPES.map((opt) => (
|
|
||||||
<option key={opt.value} value={opt.value}>
|
|
||||||
{opt.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
|
|
||||||
Duration (ms)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type='number'
|
|
||||||
min='0'
|
|
||||||
placeholder='Use project default'
|
|
||||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
|
||||||
value={transitionDurationMs}
|
|
||||||
onChange={(event) => {
|
|
||||||
const val = event.target.value;
|
|
||||||
onChange(
|
|
||||||
'transitionDurationMs',
|
|
||||||
val === '' ? '' : Math.max(0, parseInt(val, 10) || 0),
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
|
|
||||||
Easing
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
|
||||||
value={transitionEasing}
|
|
||||||
onChange={(event) =>
|
|
||||||
onChange('transitionEasing', event.target.value)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{CSS_EASING_OPTIONS.map((opt) => (
|
|
||||||
<option key={opt.value} value={opt.value}>
|
|
||||||
{opt.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
|
|
||||||
Overlay color
|
|
||||||
</label>
|
|
||||||
<div className='flex gap-2'>
|
|
||||||
<input
|
|
||||||
type='color'
|
|
||||||
className='h-7 w-10 cursor-pointer rounded border border-gray-300 p-0.5'
|
|
||||||
value={transitionOverlayColor || '#000000'}
|
|
||||||
onChange={(event) =>
|
|
||||||
onChange('transitionOverlayColor', event.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type='text'
|
|
||||||
placeholder='Use project default'
|
|
||||||
className='flex-1 rounded border border-gray-300 px-2 py-1 text-xs'
|
|
||||||
value={transitionOverlayColor}
|
|
||||||
onChange={(event) =>
|
|
||||||
onChange('transitionOverlayColor', event.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{transitionVideoUrl && (
|
|
||||||
<p className='text-[11px] text-white/60'>
|
|
||||||
Transition duration is set automatically from the selected video.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{onPreviewTransition && (
|
|
||||||
<div className='flex gap-2 pt-1'>
|
|
||||||
<BaseButton
|
|
||||||
small
|
|
||||||
color='lightDark'
|
|
||||||
label='Preview Forward'
|
|
||||||
onClick={() => onPreviewTransition('forward')}
|
|
||||||
/>
|
|
||||||
<BaseButton
|
|
||||||
small
|
|
||||||
color='lightDark'
|
|
||||||
label='Preview Back'
|
|
||||||
onClick={() => onPreviewTransition('back')}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{currentTargetMode === 'target_page' && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
|
||||||
|
Transition video asset
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||||
|
value={transitionVideoUrl}
|
||||||
|
onChange={(event) => {
|
||||||
|
onChange('transitionVideoUrl', event.target.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value=''>Not selected</option>
|
||||||
|
{addFallbackAssetOption(
|
||||||
|
transitionVideoOptions,
|
||||||
|
transitionVideoUrl,
|
||||||
|
`Current video · ${transitionVideoUrl}`,
|
||||||
|
).map((option) => (
|
||||||
|
<option key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{selectedTransitionDurationNote && (
|
||||||
|
<p className='mt-1 text-[11px] text-white/60'>
|
||||||
|
{selectedTransitionDurationNote}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
|
||||||
|
Back transition mode
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||||
|
value={transitionReverseMode}
|
||||||
|
onChange={(event) =>
|
||||||
|
onChange(
|
||||||
|
'transitionReverseMode',
|
||||||
|
event.target.value === 'separate_video'
|
||||||
|
? 'separate_video'
|
||||||
|
: 'auto_reverse',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value='auto_reverse'>
|
||||||
|
Auto reverse transition video
|
||||||
|
</option>
|
||||||
|
<option value='separate_video'>
|
||||||
|
Use separate back-transition video
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{transitionReverseMode === 'separate_video' && (
|
||||||
|
<div>
|
||||||
|
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
|
||||||
|
Back transition video asset
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||||
|
value={reverseVideoUrl}
|
||||||
|
onChange={(event) =>
|
||||||
|
onChange('reverseVideoUrl', event.target.value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value=''>Not selected</option>
|
||||||
|
{addFallbackAssetOption(
|
||||||
|
transitionVideoOptions,
|
||||||
|
reverseVideoUrl,
|
||||||
|
`Current back video · ${reverseVideoUrl}`,
|
||||||
|
).map((option) => (
|
||||||
|
<option key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* CSS Transition Settings (when no video selected) */}
|
||||||
|
{!transitionVideoUrl && (
|
||||||
|
<>
|
||||||
|
<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-white/80'>
|
||||||
|
Transition type
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||||
|
value={transitionType}
|
||||||
|
onChange={(event) =>
|
||||||
|
onChange('transitionType', event.target.value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{CSS_TRANSITION_TYPES.map((opt) => (
|
||||||
|
<option key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
|
||||||
|
Duration (ms)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type='number'
|
||||||
|
min='0'
|
||||||
|
placeholder='Use project default'
|
||||||
|
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||||
|
value={transitionDurationMs}
|
||||||
|
onChange={(event) => {
|
||||||
|
const val = event.target.value;
|
||||||
|
onChange(
|
||||||
|
'transitionDurationMs',
|
||||||
|
val === '' ? '' : Math.max(0, parseInt(val, 10) || 0),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
|
||||||
|
Easing
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||||
|
value={transitionEasing}
|
||||||
|
onChange={(event) =>
|
||||||
|
onChange('transitionEasing', event.target.value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{CSS_EASING_OPTIONS.map((opt) => (
|
||||||
|
<option key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className='mb-1 block text-[11px] font-semibold text-white/80'>
|
||||||
|
Overlay color
|
||||||
|
</label>
|
||||||
|
<div className='flex gap-2'>
|
||||||
|
<input
|
||||||
|
type='color'
|
||||||
|
className='h-7 w-10 cursor-pointer rounded border border-gray-300 p-0.5'
|
||||||
|
value={transitionOverlayColor || '#000000'}
|
||||||
|
onChange={(event) =>
|
||||||
|
onChange('transitionOverlayColor', event.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
placeholder='Use project default'
|
||||||
|
className='flex-1 rounded border border-gray-300 px-2 py-1 text-xs'
|
||||||
|
value={transitionOverlayColor}
|
||||||
|
onChange={(event) =>
|
||||||
|
onChange('transitionOverlayColor', event.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{transitionVideoUrl && (
|
||||||
|
<p className='text-[11px] text-white/60'>
|
||||||
|
Transition duration is set automatically from the selected
|
||||||
|
video.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -107,8 +107,10 @@ export interface NavigationSettingsSectionProps {
|
|||||||
navLabelFontFamily: string;
|
navLabelFontFamily: string;
|
||||||
navType: 'forward' | 'back';
|
navType: 'forward' | 'back';
|
||||||
navDisabled: boolean;
|
navDisabled: boolean;
|
||||||
|
navigationTargetMode?: 'target_page' | 'external_url';
|
||||||
targetPageId: string;
|
targetPageId: string;
|
||||||
targetPageSlug: string;
|
targetPageSlug: string;
|
||||||
|
externalUrl?: string;
|
||||||
transitionVideoUrl: string;
|
transitionVideoUrl: string;
|
||||||
transitionReverseMode: 'auto_reverse' | 'separate_video';
|
transitionReverseMode: 'auto_reverse' | 'separate_video';
|
||||||
reverseVideoUrl: string;
|
reverseVideoUrl: string;
|
||||||
|
|||||||
@ -109,8 +109,10 @@ interface FormState {
|
|||||||
navLabelFontFamily: string;
|
navLabelFontFamily: string;
|
||||||
navType: 'forward' | 'back';
|
navType: 'forward' | 'back';
|
||||||
navDisabled: boolean;
|
navDisabled: boolean;
|
||||||
|
navigationTargetMode: 'target_page' | 'external_url';
|
||||||
targetPageId: string;
|
targetPageId: string;
|
||||||
targetPageSlug: string;
|
targetPageSlug: string;
|
||||||
|
externalUrl: string;
|
||||||
transitionVideoUrl: string;
|
transitionVideoUrl: string;
|
||||||
transitionReverseMode: 'auto_reverse' | 'separate_video';
|
transitionReverseMode: 'auto_reverse' | 'separate_video';
|
||||||
reverseVideoUrl: string;
|
reverseVideoUrl: string;
|
||||||
@ -277,8 +279,10 @@ const initialState: FormState = {
|
|||||||
navLabelFontFamily: '',
|
navLabelFontFamily: '',
|
||||||
navType: 'forward',
|
navType: 'forward',
|
||||||
navDisabled: false,
|
navDisabled: false,
|
||||||
|
navigationTargetMode: 'target_page',
|
||||||
targetPageId: '',
|
targetPageId: '',
|
||||||
targetPageSlug: '',
|
targetPageSlug: '',
|
||||||
|
externalUrl: '',
|
||||||
transitionVideoUrl: '',
|
transitionVideoUrl: '',
|
||||||
transitionReverseMode: 'auto_reverse',
|
transitionReverseMode: 'auto_reverse',
|
||||||
reverseVideoUrl: '',
|
reverseVideoUrl: '',
|
||||||
@ -458,8 +462,13 @@ export function useElementSettingsForm(options: UseElementSettingsFormOptions) {
|
|||||||
navLabelFontFamily: String(settings.navLabelFontFamily || ''),
|
navLabelFontFamily: String(settings.navLabelFontFamily || ''),
|
||||||
navType: settings.navType === 'back' ? 'back' : 'forward',
|
navType: settings.navType === 'back' ? 'back' : 'forward',
|
||||||
navDisabled: Boolean(settings.navDisabled),
|
navDisabled: Boolean(settings.navDisabled),
|
||||||
|
navigationTargetMode:
|
||||||
|
settings.navigationTargetMode === 'external_url'
|
||||||
|
? 'external_url'
|
||||||
|
: 'target_page',
|
||||||
targetPageId: String(settings.targetPageId || ''),
|
targetPageId: String(settings.targetPageId || ''),
|
||||||
targetPageSlug: String(settings.targetPageSlug || ''),
|
targetPageSlug: String(settings.targetPageSlug || ''),
|
||||||
|
externalUrl: String(settings.externalUrl || ''),
|
||||||
transitionVideoUrl: String(settings.transitionVideoUrl || ''),
|
transitionVideoUrl: String(settings.transitionVideoUrl || ''),
|
||||||
transitionReverseMode:
|
transitionReverseMode:
|
||||||
settings.transitionReverseMode === 'separate_video'
|
settings.transitionReverseMode === 'separate_video'
|
||||||
@ -977,8 +986,10 @@ export function useElementSettingsForm(options: UseElementSettingsFormOptions) {
|
|||||||
settings.navLabelFontFamily = state.navLabelFontFamily.trim();
|
settings.navLabelFontFamily = state.navLabelFontFamily.trim();
|
||||||
settings.navType = state.navType;
|
settings.navType = state.navType;
|
||||||
settings.navDisabled = state.navDisabled;
|
settings.navDisabled = state.navDisabled;
|
||||||
|
settings.navigationTargetMode = state.navigationTargetMode;
|
||||||
settings.targetPageId = state.targetPageId.trim();
|
settings.targetPageId = state.targetPageId.trim();
|
||||||
settings.targetPageSlug = state.targetPageSlug.trim();
|
settings.targetPageSlug = state.targetPageSlug.trim();
|
||||||
|
settings.externalUrl = state.externalUrl.trim();
|
||||||
settings.transitionVideoUrl = state.transitionVideoUrl.trim();
|
settings.transitionVideoUrl = state.transitionVideoUrl.trim();
|
||||||
settings.transitionReverseMode = state.transitionReverseMode;
|
settings.transitionReverseMode = state.transitionReverseMode;
|
||||||
settings.reverseVideoUrl = state.reverseVideoUrl.trim();
|
settings.reverseVideoUrl = state.reverseVideoUrl.trim();
|
||||||
|
|||||||
@ -750,6 +750,16 @@ export default function RuntimePresentation({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isNavigationType(element.type) &&
|
||||||
|
element.navigationTargetMode === 'external_url'
|
||||||
|
) {
|
||||||
|
if (element.externalUrl) {
|
||||||
|
handleInfoPanelOpenExternalUrl(element.externalUrl);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Get navigation context from hook for history-based back navigation
|
// Get navigation context from hook for history-based back navigation
|
||||||
const navContext = getNavigationContext();
|
const navContext = getNavigationContext();
|
||||||
|
|
||||||
@ -799,6 +809,7 @@ export default function RuntimePresentation({
|
|||||||
isBuffering,
|
isBuffering,
|
||||||
getNavigationContext,
|
getNavigationContext,
|
||||||
setCurrentElementTransitionSettings,
|
setCurrentElementTransitionSettings,
|
||||||
|
handleInfoPanelOpenExternalUrl,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -188,9 +188,6 @@ export interface ConstructorContextValue {
|
|||||||
selectedTransition: string;
|
selectedTransition: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Transition preview
|
|
||||||
onPreviewTransition: (direction: 'forward' | 'back') => void;
|
|
||||||
|
|
||||||
// Navigation settings
|
// Navigation settings
|
||||||
allowedNavigationTypes: NavigationElementType[];
|
allowedNavigationTypes: NavigationElementType[];
|
||||||
normalizeNavigationType: (
|
normalizeNavigationType: (
|
||||||
@ -412,14 +409,12 @@ export function useConstructorNavigation() {
|
|||||||
activePageId: ctx.activePageId,
|
activePageId: ctx.activePageId,
|
||||||
allowedNavigationTypes: ctx.allowedNavigationTypes,
|
allowedNavigationTypes: ctx.allowedNavigationTypes,
|
||||||
normalizeNavigationType: ctx.normalizeNavigationType,
|
normalizeNavigationType: ctx.normalizeNavigationType,
|
||||||
onPreviewTransition: ctx.onPreviewTransition,
|
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
ctx.pages,
|
ctx.pages,
|
||||||
ctx.activePageId,
|
ctx.activePageId,
|
||||||
ctx.allowedNavigationTypes,
|
ctx.allowedNavigationTypes,
|
||||||
ctx.normalizeNavigationType,
|
ctx.normalizeNavigationType,
|
||||||
ctx.onPreviewTransition,
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -211,6 +211,7 @@ export const TYPE_SPECIFIC_DEFAULTS: Partial<
|
|||||||
navLabel: 'Forward',
|
navLabel: 'Forward',
|
||||||
navType: 'forward',
|
navType: 'forward',
|
||||||
navDisabled: false,
|
navDisabled: false,
|
||||||
|
navigationTargetMode: 'target_page',
|
||||||
iconUrl: '',
|
iconUrl: '',
|
||||||
transitionReverseMode: 'auto_reverse',
|
transitionReverseMode: 'auto_reverse',
|
||||||
},
|
},
|
||||||
@ -803,8 +804,14 @@ export const buildElementSettings = (
|
|||||||
if (element.navDisabled !== undefined) {
|
if (element.navDisabled !== undefined) {
|
||||||
settings.navDisabled = element.navDisabled;
|
settings.navDisabled = element.navDisabled;
|
||||||
}
|
}
|
||||||
|
addIfNotEmpty(
|
||||||
|
settings,
|
||||||
|
'navigationTargetMode',
|
||||||
|
element.navigationTargetMode,
|
||||||
|
);
|
||||||
addIfNotEmpty(settings, 'targetPageId', element.targetPageId);
|
addIfNotEmpty(settings, 'targetPageId', element.targetPageId);
|
||||||
addIfNotEmpty(settings, 'targetPageSlug', element.targetPageSlug);
|
addIfNotEmpty(settings, 'targetPageSlug', element.targetPageSlug);
|
||||||
|
addIfNotEmpty(settings, 'externalUrl', element.externalUrl);
|
||||||
addIfNotEmpty(settings, 'transitionVideoUrl', element.transitionVideoUrl);
|
addIfNotEmpty(settings, 'transitionVideoUrl', element.transitionVideoUrl);
|
||||||
addIfNotEmpty(
|
addIfNotEmpty(
|
||||||
settings,
|
settings,
|
||||||
|
|||||||
@ -147,7 +147,9 @@ function collectTargetPageSlugs(element: Record<string, unknown>): string[] {
|
|||||||
if (slug) slugs.add(slug);
|
if (slug) slugs.add(slug);
|
||||||
};
|
};
|
||||||
|
|
||||||
addSlug(element.targetPageSlug);
|
if (element.navigationTargetMode !== 'external_url') {
|
||||||
|
addSlug(element.targetPageSlug);
|
||||||
|
}
|
||||||
|
|
||||||
const sections = Array.isArray(element.infoPanelSections)
|
const sections = Array.isArray(element.infoPanelSections)
|
||||||
? (element.infoPanelSections as Record<string, unknown>[])
|
? (element.infoPanelSections as Record<string, unknown>[])
|
||||||
|
|||||||
@ -520,7 +520,6 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
const {
|
const {
|
||||||
preview: transitionPreview,
|
preview: transitionPreview,
|
||||||
pendingPageId: pendingNavigationPageId,
|
pendingPageId: pendingNavigationPageId,
|
||||||
openPreview: openTransitionPreviewForElement,
|
|
||||||
openPreviewWithTarget,
|
openPreviewWithTarget,
|
||||||
closePreview: closeTransitionPreview,
|
closePreview: closeTransitionPreview,
|
||||||
} = useTransitionPreview({
|
} = useTransitionPreview({
|
||||||
@ -1244,7 +1243,26 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
item && item.type && labelByType[item.type as CanvasElementType],
|
item && item.type && labelByType[item.type as CanvasElementType],
|
||||||
)
|
)
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
const elementType = item.type as CanvasElementType;
|
const navigationTargetMode =
|
||||||
|
item.navigationTargetMode === 'external_url'
|
||||||
|
? 'external_url'
|
||||||
|
: ('target_page' as const);
|
||||||
|
const rawElementType = item.type as CanvasElementType;
|
||||||
|
const elementType =
|
||||||
|
navigationTargetMode === 'external_url' &&
|
||||||
|
isNavigationElementType(rawElementType)
|
||||||
|
? 'navigation_next'
|
||||||
|
: rawElementType;
|
||||||
|
const navType =
|
||||||
|
navigationTargetMode === 'external_url'
|
||||||
|
? 'forward'
|
||||||
|
: item.navType === 'back' || item.navType === 'forward'
|
||||||
|
? item.navType
|
||||||
|
: isNavigationElementType(elementType)
|
||||||
|
? getNavigationButtonKind(
|
||||||
|
elementType as NavigationElementType,
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
const normalizedElement: CanvasElement = {
|
const normalizedElement: CanvasElement = {
|
||||||
...item,
|
...item,
|
||||||
id: String(item.id || createLocalId()),
|
id: String(item.id || createLocalId()),
|
||||||
@ -1411,21 +1429,21 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
? item.descriptionText
|
? item.descriptionText
|
||||||
: '',
|
: '',
|
||||||
navLabel: typeof item.navLabel === 'string' ? item.navLabel : '',
|
navLabel: typeof item.navLabel === 'string' ? item.navLabel : '',
|
||||||
navType:
|
navType,
|
||||||
item.navType === 'back' || item.navType === 'forward'
|
navigationTargetMode,
|
||||||
? item.navType
|
|
||||||
: isNavigationElementType(elementType)
|
|
||||||
? getNavigationButtonKind(
|
|
||||||
elementType as NavigationElementType,
|
|
||||||
)
|
|
||||||
: undefined,
|
|
||||||
// Support both targetPageSlug (new) and targetPageId (legacy)
|
// Support both targetPageSlug (new) and targetPageId (legacy)
|
||||||
targetPageSlug:
|
targetPageSlug:
|
||||||
|
navigationTargetMode !== 'external_url' &&
|
||||||
typeof item.targetPageSlug === 'string'
|
typeof item.targetPageSlug === 'string'
|
||||||
? item.targetPageSlug
|
? item.targetPageSlug
|
||||||
: '',
|
: '',
|
||||||
targetPageId:
|
targetPageId:
|
||||||
typeof item.targetPageId === 'string' ? item.targetPageId : '',
|
navigationTargetMode !== 'external_url' &&
|
||||||
|
typeof item.targetPageId === 'string'
|
||||||
|
? item.targetPageId
|
||||||
|
: '',
|
||||||
|
externalUrl:
|
||||||
|
typeof item.externalUrl === 'string' ? item.externalUrl : '',
|
||||||
transitionVideoUrl:
|
transitionVideoUrl:
|
||||||
typeof item.transitionVideoUrl === 'string'
|
typeof item.transitionVideoUrl === 'string'
|
||||||
? item.transitionVideoUrl
|
? item.transitionVideoUrl
|
||||||
@ -1628,20 +1646,6 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// openTransitionPreviewForElement is now provided by useTransitionPreview hook
|
|
||||||
|
|
||||||
const openTransitionPreview = (direction: 'forward' | 'back') => {
|
|
||||||
if (
|
|
||||||
!selectedElement ||
|
|
||||||
(selectedElement.type !== 'navigation_next' &&
|
|
||||||
selectedElement.type !== 'navigation_prev')
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
openTransitionPreviewForElement(selectedElement, direction);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCanvasElementClick = (element: CanvasElement) => {
|
const onCanvasElementClick = (element: CanvasElement) => {
|
||||||
if (!isConstructorEditMode) {
|
if (!isConstructorEditMode) {
|
||||||
if (isNavigationElementType(element.type)) {
|
if (isNavigationElementType(element.type)) {
|
||||||
@ -1656,6 +1660,13 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
// Cancel any pending fade to prevent stale fade state across navigations
|
// Cancel any pending fade to prevent stale fade state across navigations
|
||||||
resetFadeIn();
|
resetFadeIn();
|
||||||
|
|
||||||
|
if (element.navigationTargetMode === 'external_url') {
|
||||||
|
if (element.externalUrl) {
|
||||||
|
handleInfoPanelOpenExternalUrl(element.externalUrl);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Use shared navigation helpers
|
// Use shared navigation helpers
|
||||||
const direction = getNavigationDirection(element);
|
const direction = getNavigationDirection(element);
|
||||||
|
|
||||||
@ -2088,9 +2099,6 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
// Duration notes
|
// Duration notes
|
||||||
durationNotes,
|
durationNotes,
|
||||||
|
|
||||||
// Transition preview
|
|
||||||
onPreviewTransition: openTransitionPreview,
|
|
||||||
|
|
||||||
// Navigation settings
|
// Navigation settings
|
||||||
allowedNavigationTypes,
|
allowedNavigationTypes,
|
||||||
normalizeNavigationType,
|
normalizeNavigationType,
|
||||||
@ -2139,7 +2147,6 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
infoPanelSectionOps,
|
infoPanelSectionOps,
|
||||||
getDuration,
|
getDuration,
|
||||||
durationNotes,
|
durationNotes,
|
||||||
openTransitionPreview,
|
|
||||||
allowedNavigationTypes,
|
allowedNavigationTypes,
|
||||||
normalizeNavigationType,
|
normalizeNavigationType,
|
||||||
saveConstructor,
|
saveConstructor,
|
||||||
|
|||||||
@ -32,6 +32,11 @@ export type CanvasElementType =
|
|||||||
*/
|
*/
|
||||||
export type NavigationButtonKind = 'forward' | 'back';
|
export type NavigationButtonKind = 'forward' | 'back';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigation destination mode for forward buttons.
|
||||||
|
*/
|
||||||
|
export type NavigationTargetMode = 'target_page' | 'external_url';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Editor menu item type for constructor sidebar
|
* Editor menu item type for constructor sidebar
|
||||||
*/
|
*/
|
||||||
@ -400,10 +405,14 @@ export interface CanvasElement extends BaseCanvasElement {
|
|||||||
navLabelFontFamily?: string;
|
navLabelFontFamily?: string;
|
||||||
navType?: NavigationButtonKind;
|
navType?: NavigationButtonKind;
|
||||||
navDisabled?: boolean;
|
navDisabled?: boolean;
|
||||||
|
/** Destination mode for forward navigation buttons. Defaults to target_page. */
|
||||||
|
navigationTargetMode?: NavigationTargetMode;
|
||||||
/** @deprecated Use targetPageSlug instead - IDs change when copied between environments */
|
/** @deprecated Use targetPageSlug instead - IDs change when copied between environments */
|
||||||
targetPageId?: string;
|
targetPageId?: string;
|
||||||
/** Target page slug for navigation - slugs are consistent across environments */
|
/** Target page slug for navigation - slugs are consistent across environments */
|
||||||
targetPageSlug?: string;
|
targetPageSlug?: string;
|
||||||
|
/** External URL for navigation buttons when navigationTargetMode is external_url. */
|
||||||
|
externalUrl?: string;
|
||||||
transitionVideoUrl?: string;
|
transitionVideoUrl?: string;
|
||||||
/** Storage key for the transition video (for cache lookup) */
|
/** Storage key for the transition video (for cache lookup) */
|
||||||
transitionStorageKey?: string;
|
transitionStorageKey?: string;
|
||||||
@ -793,7 +802,6 @@ export interface EditorNavigationProps {
|
|||||||
sort_order?: number;
|
sort_order?: number;
|
||||||
}>;
|
}>;
|
||||||
activePageId: string;
|
activePageId: string;
|
||||||
onPreviewTransition: (direction: 'forward' | 'back') => void;
|
|
||||||
normalizeNavigationType: (
|
normalizeNavigationType: (
|
||||||
element: CanvasElement,
|
element: CanvasElement,
|
||||||
nextType: 'navigation_next' | 'navigation_prev',
|
nextType: 'navigation_next' | 'navigation_prev',
|
||||||
|
|||||||
@ -77,8 +77,10 @@ export interface PageDataLoaderResult {
|
|||||||
export interface NavigableElement {
|
export interface NavigableElement {
|
||||||
id: string;
|
id: string;
|
||||||
type: string;
|
type: string;
|
||||||
|
navigationTargetMode?: 'target_page' | 'external_url';
|
||||||
targetPageSlug?: string;
|
targetPageSlug?: string;
|
||||||
targetPageId?: string;
|
targetPageId?: string;
|
||||||
|
externalUrl?: string;
|
||||||
transitionVideoUrl?: string;
|
transitionVideoUrl?: string;
|
||||||
transitionReverseMode?: 'auto_reverse' | 'separate_video';
|
transitionReverseMode?: 'auto_reverse' | 'separate_video';
|
||||||
reverseVideoUrl?: string;
|
reverseVideoUrl?: string;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user