fix(studio): lift block drop handling above DomEditOverlay

The DomEditOverlay sits at z-10 with pointer-events:auto over the
preview, intercepting all drag events before they reach NLEPreview's
viewport. Move block drop handling from NLEPreview up to the wrapper
div in NLELayout that contains both the preview and the overlay, so
drag-and-drop from the Catalog panel onto the preview area works
regardless of inspector state.
This commit is contained in:
Miguel Ángel
2026-05-21 18:28:17 -04:00
parent 289aa03499
commit 0999ed56e6
2 changed files with 33 additions and 19 deletions
@@ -13,6 +13,7 @@ import type { TimelineElement } from "../../player";
import type { BlockedTimelineEditIntent } from "../../player/components/timelineEditing";
import { NLEPreview } from "./NLEPreview";
import { CompositionBreadcrumb } from "./CompositionBreadcrumb";
import { usePreviewBlockDrop } from "./usePreviewBlockDrop";
import { useCompositionStack } from "./useCompositionStack";
import {
TIMELINE_TOGGLE_SHORTCUT_LABEL,
@@ -136,6 +137,22 @@ export const NLELayout = memo(function NLELayout({
usePlayerStore.getState().reset();
}
const stageRefForDrop = useRef<HTMLDivElement | null>(null);
const handleStageRef = useCallback((ref: React.RefObject<HTMLDivElement | null>) => {
stageRefForDrop.current = ref.current;
}, []);
const {
isDragOver: previewDragOver,
handleDragOver: handlePreviewDragOver,
handleDragLeave: handlePreviewDragLeave,
handleDrop: handlePreviewDrop,
} = usePreviewBlockDrop({
portrait,
stageRef: stageRefForDrop as React.RefObject<HTMLDivElement | null>,
onBlockDrop: onPreviewBlockDrop,
});
// Lightweight reload: change iframe src instead of destroying the Player.
// refreshPlayer() saves the seek position and appends a cache-busting _t
// param — the Player instance stays alive so the adapter is available for
@@ -349,7 +366,13 @@ export const NLELayout = memo(function NLELayout({
>
{/* Preview + player controls */}
<div className="flex-1 min-h-0 flex flex-col">
<div className="flex-1 min-h-0 relative" data-preview-pan-surface="true">
<div
className="flex-1 min-h-0 relative"
data-preview-pan-surface="true"
onDragOver={handlePreviewDragOver}
onDragLeave={handlePreviewDragLeave}
onDrop={handlePreviewDrop}
>
<NLEPreview
projectId={projectId}
iframeRef={iframeRef}
@@ -358,8 +381,11 @@ export const NLELayout = memo(function NLELayout({
portrait={portrait}
directUrl={directUrl}
suppressLoadingOverlay={hasLoadedOnceRef.current}
onBlockDrop={onPreviewBlockDrop}
onStageRef={handleStageRef}
/>
{previewDragOver && (
<div className="absolute inset-2 z-40 rounded-lg border-2 border-dashed border-studio-accent/50 bg-studio-accent/[0.04] pointer-events-none" />
)}
{!isFullscreen && previewOverlay}
</div>
<div className="bg-neutral-950 border-t border-neutral-800/50 flex-shrink-0">
@@ -12,8 +12,6 @@ import {
type PreviewZoomState,
} from "./previewZoom";
import { readStudioUiPreferences, writeStudioUiPreferences } from "../../utils/studioUiPreferences";
import { usePreviewBlockDrop } from "./usePreviewBlockDrop";
interface NLEPreviewProps {
projectId: string;
iframeRef: Ref<HTMLIFrameElement>;
@@ -22,7 +20,7 @@ interface NLEPreviewProps {
portrait?: boolean;
directUrl?: string;
suppressLoadingOverlay?: boolean;
onBlockDrop?: (blockName: string, position: { left: number; top: number }) => void;
onStageRef?: (ref: React.RefObject<HTMLDivElement | null>) => void;
}
export function getPreviewPlayerKey({
@@ -92,11 +90,14 @@ export const NLEPreview = memo(function NLEPreview({
portrait,
directUrl,
suppressLoadingOverlay,
onBlockDrop,
onStageRef,
}: NLEPreviewProps) {
const activeKey = getPreviewPlayerKey({ projectId, directUrl });
const viewportRef = useRef<HTMLDivElement>(null);
const stageRef = useRef<HTMLDivElement>(null);
useEffect(() => {
onStageRef?.(stageRef);
}, [onStageRef]);
const [stageSize, setStageSize] = useState(() => resolvePreviewStageSize(0, 0, portrait));
const zoomRef = useRef<PreviewZoomState>(loadInitialZoom());
@@ -367,13 +368,6 @@ export const NLEPreview = memo(function NLEPreview({
};
}, [applyPan]);
const {
isDragOver: previewDragOver,
handleDragOver: handlePreviewDragOver,
handleDragLeave: handlePreviewDragLeave,
handleDrop: handlePreviewDrop,
} = usePreviewBlockDrop({ portrait, stageRef, onBlockDrop });
const initial = zoomRef.current;
return (
@@ -383,9 +377,6 @@ export const NLEPreview = memo(function NLEPreview({
className="relative flex-1 flex items-center justify-center p-2 overflow-hidden min-h-0 outline-none focus:ring-1 focus:ring-studio-accent/40 bg-neutral-700"
tabIndex={0}
aria-label="Composition preview"
onDragOver={handlePreviewDragOver}
onDragLeave={handlePreviewDragLeave}
onDrop={handlePreviewDrop}
>
<div className="absolute inset-2 flex items-center justify-center pointer-events-none">
<div
@@ -415,9 +406,6 @@ export const NLEPreview = memo(function NLEPreview({
/>
</div>
</div>
{previewDragOver && (
<div className="absolute inset-2 z-40 rounded-lg border-2 border-dashed border-studio-accent/50 bg-studio-accent/[0.04] pointer-events-none" />
)}
<div
ref={hudRef}
className="pointer-events-none absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-50 rounded-lg px-4 py-2 text-sm font-mono tabular-nums text-white/90 bg-black/60 backdrop-blur-sm shadow-lg"