@hyperframes/studio 0.7.55 → 0.7.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{index-BXaqaVKt.js → index-BWnOxAiH.js} +1 -1
- package/dist/assets/{index-CPetwHFV.js → index-C4csZims.js} +1 -1
- package/dist/assets/index-CmVCjZjp.js +423 -0
- package/dist/assets/index-D-GyYi2d.css +1 -0
- package/dist/{chunk-SOTCF4DF.js → chunk-5QSIMBEJ.js} +2 -1
- package/dist/chunk-5QSIMBEJ.js.map +1 -0
- package/dist/{domEditingLayers-2ECJK24D.js → domEditingLayers-6LQGKPOI.js} +2 -2
- package/dist/index.d.ts +146 -144
- package/dist/index.html +2 -2
- package/dist/index.js +40414 -37680
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +106 -96
- package/src/components/EditorShell.tsx +253 -0
- package/src/components/StudioGlobalDragOverlay.tsx +1 -3
- package/src/components/StudioLeftSidebar.tsx +13 -3
- package/src/components/StudioRightPanel.tsx +8 -3
- package/src/components/TimelineToolbar.test.tsx +2 -2
- package/src/components/TimelineToolbar.tsx +179 -121
- package/src/components/editor/DomEditCropHandles.test.tsx +74 -3
- package/src/components/editor/DomEditCropHandles.tsx +64 -29
- package/src/components/editor/DomEditOverlay.test.ts +245 -279
- package/src/components/editor/DomEditOverlay.tsx +158 -126
- package/src/components/editor/DomEditOverlayHover.test.tsx +90 -0
- package/src/components/editor/DomEditRotateHandle.tsx +38 -18
- package/src/components/editor/DomEditSelectionChrome.test.tsx +80 -0
- package/src/components/editor/DomEditSelectionChrome.tsx +257 -0
- package/src/components/editor/LayersPanel.test.ts +66 -2
- package/src/components/editor/LayersPanel.tsx +43 -1
- package/src/components/editor/OffCanvasIndicators.test.tsx +34 -0
- package/src/components/editor/OffCanvasIndicators.tsx +49 -10
- package/src/components/editor/SnapGuideOverlay.tsx +28 -23
- package/src/components/editor/SnapToolbar.test.tsx +0 -1
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +214 -0
- package/src/components/editor/anchoredResizeReleaseShift.test.ts +106 -10
- package/src/components/editor/domEditNudge.test.ts +80 -0
- package/src/components/editor/domEditNudge.ts +44 -0
- package/src/components/editor/domEditOverlayGeometry.test.ts +167 -1
- package/src/components/editor/domEditOverlayGeometry.ts +280 -20
- package/src/components/editor/domEditOverlayGestures.ts +44 -41
- package/src/components/editor/domEditOverlayStartGesture.ts +62 -2
- package/src/components/editor/domEditResizeLocal.test.ts +131 -0
- package/src/components/editor/domEditResizeLocal.ts +125 -0
- package/src/components/editor/domEditingDom.ts +1 -1
- package/src/components/editor/manualEditsDomPatches.test.ts +47 -0
- package/src/components/editor/manualOffsetDrag.ts +55 -12
- package/src/components/editor/offCanvasIndicatorGeometry.ts +31 -10
- package/src/components/editor/offCanvasIndicatorRefresh.test.tsx +25 -1
- package/src/components/editor/resizeDraft.ts +108 -0
- package/src/components/editor/snapEngine.test.ts +12 -78
- package/src/components/editor/snapEngine.ts +13 -53
- package/src/components/editor/useDomEditNudge.test.tsx +228 -0
- package/src/components/editor/useDomEditNudge.ts +254 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +115 -116
- package/src/components/editor/useDomEditOverlayRects.ts +9 -2
- package/src/components/nle/AssetPreviewOverlay.tsx +147 -0
- package/src/components/nle/NLEContext.test.ts +144 -0
- package/src/components/nle/NLEContext.tsx +328 -0
- package/src/components/nle/NLEPreview.tsx +1 -1
- package/src/components/nle/PreviewOverlays.tsx +240 -0
- package/src/components/nle/PreviewPane.tsx +163 -0
- package/src/components/nle/TimelinePane.test.ts +60 -0
- package/src/components/nle/TimelinePane.tsx +293 -0
- package/src/components/nle/TimelineResizeDivider.tsx +8 -2
- package/src/components/nle/useCompositionStack.test.tsx +44 -0
- package/src/components/nle/useCompositionStack.ts +17 -9
- package/src/components/nle/useTimelineEditCallbacks.ts +217 -0
- package/src/components/sidebar/AssetCard.tsx +326 -0
- package/src/components/sidebar/AssetContextMenu.tsx +15 -30
- package/src/components/sidebar/AssetsTab.test.ts +96 -0
- package/src/components/sidebar/AssetsTab.tsx +71 -206
- package/src/components/sidebar/AudioRow.tsx +39 -2
- package/src/components/sidebar/BlocksTab.tsx +8 -1
- package/src/components/sidebar/LeftSidebar.tsx +4 -1
- package/src/components/sidebar/assetHelpers.ts +29 -0
- package/src/contexts/DomEditContext.tsx +9 -0
- package/src/contexts/StudioContext.tsx +9 -8
- package/src/contexts/TimelineEditContext.tsx +3 -7
- package/src/hooks/deleteSelectedKeyframes.ts +35 -0
- package/src/hooks/domEditCommitTypes.ts +12 -1
- package/src/hooks/domSelectionTestHarness.ts +14 -0
- package/src/hooks/gestureTransaction.test.ts +311 -0
- package/src/hooks/gestureTransaction.ts +199 -0
- package/src/hooks/gsapDragCommit.test.ts +141 -41
- package/src/hooks/gsapDragCommit.ts +64 -74
- package/src/hooks/gsapDragStaticSetHelpers.ts +9 -21
- package/src/hooks/gsapResizeIntercept.test.ts +69 -0
- package/src/hooks/gsapResizeIntercept.ts +38 -10
- package/src/hooks/gsapRuntimeBridge.test.ts +49 -13
- package/src/hooks/gsapRuntimeBridge.ts +4 -4
- package/src/hooks/gsapScriptCommitTypes.ts +16 -5
- package/src/hooks/gsapShared.test.ts +19 -1
- package/src/hooks/gsapShared.ts +14 -0
- package/src/hooks/patchDocumentRootDuration.test.ts +71 -0
- package/src/hooks/timelineEditingGsap.ts +14 -0
- package/src/hooks/timelineEditingHelpers.test.ts +34 -1
- package/src/hooks/timelineEditingHelpers.ts +22 -48
- package/src/hooks/timelineMoveAdapter.test.ts +77 -0
- package/src/hooks/timelineMoveAdapter.ts +37 -0
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +36 -8
- package/src/hooks/useAnimatedPropertyCommit.ts +31 -26
- package/src/hooks/useAppHotkeys.ts +16 -25
- package/src/hooks/useBlockHandlers.ts +55 -27
- package/src/hooks/useContextMenuDismiss.ts +31 -7
- package/src/hooks/useDomEditCommits.test.tsx +154 -1
- package/src/hooks/useDomEditCommits.ts +100 -3
- package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
- package/src/hooks/useDomEditSession.ts +2 -2
- package/src/hooks/useDomEditTextCommits.test.tsx +135 -0
- package/src/hooks/useDomEditTextCommits.ts +8 -3
- package/src/hooks/useDomEditWiring.ts +7 -9
- package/src/hooks/useDomGeometryCommits.ts +17 -2
- package/src/hooks/useDomSelection.test.ts +2 -36
- package/src/hooks/useDomSelection.ts +81 -51
- package/src/hooks/useDomSelectionSelectionGuards.test.ts +276 -0
- package/src/hooks/useElementLifecycleOps.test.tsx +258 -0
- package/src/hooks/useElementLifecycleOps.ts +40 -71
- package/src/hooks/useEnableKeyframes.test.ts +142 -1
- package/src/hooks/useEnableKeyframes.ts +73 -29
- package/src/hooks/useGestureCommit.test.tsx +130 -0
- package/src/hooks/useGestureCommit.ts +32 -8
- package/src/hooks/useGroupCommits.ts +7 -28
- package/src/hooks/useGsapAwareEditing.test.tsx +239 -0
- package/src/hooks/useGsapAwareEditing.ts +115 -20
- package/src/hooks/useGsapKeyframeOps.test.tsx +59 -0
- package/src/hooks/useGsapKeyframeOps.ts +38 -10
- package/src/hooks/useGsapScriptCommits.test.tsx +40 -1
- package/src/hooks/useGsapScriptCommits.ts +134 -50
- package/src/hooks/useGsapSelectionHandlers.ts +49 -16
- package/src/hooks/useMusicBeatAnalysis.ts +72 -36
- package/src/hooks/usePersistentEditHistory.test.ts +33 -0
- package/src/hooks/usePersistentEditHistory.ts +92 -58
- package/src/hooks/usePreviewPersistence.ts +25 -7
- package/src/hooks/useRazorSplit.history.test.tsx +148 -18
- package/src/hooks/useRazorSplit.test.ts +269 -0
- package/src/hooks/useRazorSplit.testHelpers.ts +64 -0
- package/src/hooks/useRazorSplit.ts +107 -36
- package/src/hooks/useRenderClipContent.ts +24 -6
- package/src/hooks/useStudioContextValue.ts +15 -5
- package/src/hooks/useStudioUrlState.ts +2 -4
- package/src/hooks/useTimelineEditing.test.tsx +20 -29
- package/src/hooks/useTimelineEditingTypes.ts +6 -0
- package/src/hooks/useTimelineGroupEditing.ts +18 -4
- package/src/index.ts +1 -1
- package/src/player/components/ImageThumbnail.test.tsx +173 -0
- package/src/player/components/ImageThumbnail.tsx +160 -0
- package/src/player/components/PlayheadIndicator.tsx +50 -5
- package/src/player/components/Timeline.test.ts +102 -19
- package/src/player/components/Timeline.tsx +194 -194
- package/src/player/components/TimelineCanvas.tsx +207 -521
- package/src/player/components/TimelineClip.tsx +4 -1
- package/src/player/components/TimelineClipDiamonds.tsx +9 -40
- package/src/player/components/TimelineEmptyState.tsx +3 -1
- package/src/player/components/TimelineLanes.tsx +487 -0
- package/src/player/components/TimelineOverlays.tsx +122 -0
- package/src/player/components/VideoThumbnail.test.tsx +152 -0
- package/src/player/components/VideoThumbnail.tsx +28 -7
- package/src/player/components/thumbnailUtils.test.ts +127 -0
- package/src/player/components/thumbnailUtils.ts +54 -0
- package/src/player/components/timelineCallbacks.ts +18 -18
- package/src/player/components/timelineClipChildren.tsx +39 -0
- package/src/player/components/timelineClipDragCommit.test.ts +943 -0
- package/src/player/components/timelineClipDragCommit.ts +401 -0
- package/src/player/components/timelineClipDragPreview.test.ts +144 -0
- package/src/player/components/timelineClipDragPreview.ts +323 -38
- package/src/player/components/timelineClipDragTypes.ts +65 -0
- package/src/player/components/timelineCollision.test.ts +42 -2
- package/src/player/components/timelineDragDrop.ts +69 -60
- package/src/player/components/timelineEditCapabilities.ts +52 -0
- package/src/player/components/timelineEditing.test.ts +1 -1
- package/src/player/components/timelineEditing.ts +76 -59
- package/src/player/components/timelineGroupEditing.test.ts +126 -0
- package/src/player/components/timelineGroupEditing.ts +133 -0
- package/src/player/components/timelineGroupResizeCommit.ts +54 -0
- package/src/player/components/timelineLayout.test.ts +95 -23
- package/src/player/components/timelineLayout.ts +120 -61
- package/src/player/components/timelineMarquee.test.ts +197 -0
- package/src/player/components/timelineMarquee.ts +112 -0
- package/src/player/components/timelineOptimisticRevision.ts +42 -0
- package/src/player/components/timelineTheme.ts +6 -1
- package/src/player/components/useResolvedTimelineEditCallbacks.ts +25 -3
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +241 -0
- package/src/player/components/useTimelineClipDrag.ts +380 -386
- package/src/player/components/useTimelineEditPinning.ts +121 -0
- package/src/player/components/useTimelineGeometry.ts +129 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -16
- package/src/player/components/useTimelineRangeSelection.ts +341 -50
- package/src/player/components/useTimelineStackingSync.test.tsx +67 -0
- package/src/player/components/useTimelineStackingSync.ts +83 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +0 -3
- package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
- package/src/player/hooks/useTimelinePlayer.ts +48 -53
- package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
- package/src/player/index.ts +3 -2
- package/src/player/lib/time.test.ts +19 -19
- package/src/player/lib/time.ts +6 -2
- package/src/player/lib/timelineDOM.test.ts +25 -98
- package/src/player/lib/timelineDOM.ts +12 -81
- package/src/player/lib/timelineElementHelpers.ts +54 -5
- package/src/player/lib/timelineIframeHelpers.ts +2 -0
- package/src/player/store/playerStore.ts +62 -3
- package/src/styles/studio.css +27 -0
- package/src/utils/assetPreviewStore.ts +33 -0
- package/src/utils/blockInstaller.ts +10 -2
- package/src/utils/editHistory.test.ts +35 -0
- package/src/utils/gsapSoftReload.test.ts +119 -1
- package/src/utils/gsapSoftReload.ts +153 -0
- package/src/utils/mediaTypes.ts +3 -2
- package/src/utils/resizeDebug.ts +55 -0
- package/src/utils/rootDuration.test.ts +90 -1
- package/src/utils/rootDuration.ts +76 -13
- package/src/utils/sdkCutover.ts +4 -0
- package/src/utils/studioFileHistory.ts +37 -7
- package/src/utils/studioHelpers.ts +62 -0
- package/src/utils/studioPreviewHelpers.test.ts +65 -8
- package/src/utils/studioPreviewHelpers.ts +10 -0
- package/src/utils/studioTelemetry.ts +4 -1
- package/src/utils/studioUrlState.test.ts +0 -1
- package/src/utils/timelineAssetDrop.ts +62 -0
- package/src/utils/timelineDiscovery.ts +0 -17
- package/dist/assets/index-BRwkMj0w.js +0 -423
- package/dist/assets/index-Dq7FEg0K.css +0 -1
- package/dist/chunk-SOTCF4DF.js.map +0 -1
- package/src/components/StudioPreviewArea.tsx +0 -500
- package/src/components/nle/NLELayout.test.ts +0 -12
- package/src/components/nle/NLELayout.tsx +0 -591
- package/src/player/components/TimelineLayerGutter.tsx +0 -61
- package/src/player/components/TimelineSelectionOverlays.tsx +0 -55
- package/src/player/components/timelineMarqueeSelection.test.ts +0 -87
- package/src/player/components/timelineSnapTargets.test.ts +0 -144
- package/src/player/components/timelineSnapTargets.ts +0 -164
- package/src/player/components/useTimelineClipDrag.test.tsx +0 -506
- package/src/player/components/useTimelineClipGroupDrag.ts +0 -417
- package/src/player/components/useTimelineMarqueeSelection.test.tsx +0 -231
- package/src/player/components/useTimelineMarqueeSelection.ts +0 -276
- package/src/utils/timelineDiscovery.test.ts +0 -90
- /package/dist/{domEditingLayers-2ECJK24D.js.map → domEditingLayers-6LQGKPOI.js.map} +0 -0
|
@@ -308,3 +308,65 @@ export async function resolveDroppedAssetDuration(
|
|
|
308
308
|
media.load();
|
|
309
309
|
return duration;
|
|
310
310
|
}
|
|
311
|
+
|
|
312
|
+
export async function resolveDroppedAssetDimensions(
|
|
313
|
+
projectId: string,
|
|
314
|
+
assetPath: string,
|
|
315
|
+
kind: TimelineAssetKind,
|
|
316
|
+
): Promise<{ width: number; height: number } | null> {
|
|
317
|
+
if (kind === "audio") return null;
|
|
318
|
+
const src = `/api/projects/${projectId}/preview/${assetPath}`;
|
|
319
|
+
|
|
320
|
+
if (kind === "image") {
|
|
321
|
+
return new Promise((resolve) => {
|
|
322
|
+
const img = new Image();
|
|
323
|
+
const timeout = window.setTimeout(() => resolve(null), 3000);
|
|
324
|
+
img.addEventListener(
|
|
325
|
+
"load",
|
|
326
|
+
() => {
|
|
327
|
+
window.clearTimeout(timeout);
|
|
328
|
+
resolve(
|
|
329
|
+
img.naturalWidth > 0 && img.naturalHeight > 0
|
|
330
|
+
? { width: img.naturalWidth, height: img.naturalHeight }
|
|
331
|
+
: null,
|
|
332
|
+
);
|
|
333
|
+
},
|
|
334
|
+
{ once: true },
|
|
335
|
+
);
|
|
336
|
+
img.addEventListener(
|
|
337
|
+
"error",
|
|
338
|
+
() => {
|
|
339
|
+
window.clearTimeout(timeout);
|
|
340
|
+
resolve(null);
|
|
341
|
+
},
|
|
342
|
+
{ once: true },
|
|
343
|
+
);
|
|
344
|
+
img.src = src;
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return new Promise((resolve) => {
|
|
349
|
+
const video = document.createElement("video");
|
|
350
|
+
video.preload = "metadata";
|
|
351
|
+
const timeout = window.setTimeout(() => resolve(null), 3000);
|
|
352
|
+
const finalize = (value: { width: number; height: number } | null) => {
|
|
353
|
+
window.clearTimeout(timeout);
|
|
354
|
+
video.src = "";
|
|
355
|
+
video.load();
|
|
356
|
+
resolve(value);
|
|
357
|
+
};
|
|
358
|
+
video.addEventListener(
|
|
359
|
+
"loadedmetadata",
|
|
360
|
+
() => {
|
|
361
|
+
finalize(
|
|
362
|
+
video.videoWidth > 0 && video.videoHeight > 0
|
|
363
|
+
? { width: video.videoWidth, height: video.videoHeight }
|
|
364
|
+
: null,
|
|
365
|
+
);
|
|
366
|
+
},
|
|
367
|
+
{ once: true },
|
|
368
|
+
);
|
|
369
|
+
video.addEventListener("error", () => finalize(null), { once: true });
|
|
370
|
+
video.src = src;
|
|
371
|
+
});
|
|
372
|
+
}
|
|
@@ -25,6 +25,15 @@ function stubRect(el: Element, rect: DOMRect): void {
|
|
|
25
25
|
el.getBoundingClientRect = () => rect;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** Create and attach a preview iframe, returning it with its (asserted) document. */
|
|
29
|
+
function createPreviewIframe(): { iframe: HTMLIFrameElement; doc: Document } {
|
|
30
|
+
const iframe = document.createElement("iframe");
|
|
31
|
+
document.body.append(iframe);
|
|
32
|
+
const doc = iframe.contentDocument;
|
|
33
|
+
if (!doc) throw new Error("Expected iframe document");
|
|
34
|
+
return { iframe, doc };
|
|
35
|
+
}
|
|
36
|
+
|
|
28
37
|
describe("coversComposition (full-bleed canvas-pick exclusion)", () => {
|
|
29
38
|
const viewport = { width: 1920, height: 1080 };
|
|
30
39
|
|
|
@@ -106,10 +115,7 @@ describe("pauseStudioPreviewPlayback", () => {
|
|
|
106
115
|
|
|
107
116
|
describe("getPreviewTargetFromPointer", () => {
|
|
108
117
|
it("skips candidates hidden from author hit-testing by inherited pointer-events:none", () => {
|
|
109
|
-
const iframe =
|
|
110
|
-
document.body.append(iframe);
|
|
111
|
-
const doc = iframe.contentDocument;
|
|
112
|
-
if (!doc) throw new Error("Expected iframe document");
|
|
118
|
+
const { iframe, doc } = createPreviewIframe();
|
|
113
119
|
|
|
114
120
|
doc.body.innerHTML = `
|
|
115
121
|
<main id="scene" data-composition-id="scene">
|
|
@@ -141,10 +147,7 @@ describe("getPreviewTargetFromPointer", () => {
|
|
|
141
147
|
});
|
|
142
148
|
|
|
143
149
|
it("honors a CSS-class pointer-events:auto opt-in under a pointer-events:none ancestor", () => {
|
|
144
|
-
const iframe =
|
|
145
|
-
document.body.append(iframe);
|
|
146
|
-
const doc = iframe.contentDocument;
|
|
147
|
-
if (!doc) throw new Error("Expected iframe document");
|
|
150
|
+
const { iframe, doc } = createPreviewIframe();
|
|
148
151
|
|
|
149
152
|
doc.head.innerHTML = `<style>.clickable { pointer-events: auto; }</style>`;
|
|
150
153
|
doc.body.innerHTML = `
|
|
@@ -172,4 +175,58 @@ describe("getPreviewTargetFromPointer", () => {
|
|
|
172
175
|
|
|
173
176
|
iframe.remove();
|
|
174
177
|
});
|
|
178
|
+
|
|
179
|
+
it("selects a full-bleed <video> instead of skipping to the element behind it", () => {
|
|
180
|
+
const { iframe, doc } = createPreviewIframe();
|
|
181
|
+
|
|
182
|
+
doc.body.innerHTML = `
|
|
183
|
+
<main id="scene" data-composition-id="scene">
|
|
184
|
+
<div id="backdrop"></div>
|
|
185
|
+
<video id="hero"></video>
|
|
186
|
+
</main>
|
|
187
|
+
`;
|
|
188
|
+
|
|
189
|
+
const scene = doc.getElementById("scene");
|
|
190
|
+
const backdrop = doc.getElementById("backdrop");
|
|
191
|
+
const hero = doc.getElementById("hero");
|
|
192
|
+
if (!scene || !backdrop || !hero) throw new Error("Expected preview fixture elements");
|
|
193
|
+
|
|
194
|
+
stubRect(iframe, domRect(0, 0, 400, 300));
|
|
195
|
+
stubRect(scene, domRect(0, 0, 400, 300));
|
|
196
|
+
stubRect(backdrop, domRect(0, 0, 400, 300));
|
|
197
|
+
// Full-bleed hero video painted on top of a full-bleed backdrop.
|
|
198
|
+
stubRect(hero, domRect(0, 0, 400, 300));
|
|
199
|
+
doc.elementsFromPoint = () => [hero, backdrop, scene];
|
|
200
|
+
|
|
201
|
+
// Before the fix the video was full-bleed-excluded and the picker fell through
|
|
202
|
+
// to the backdrop (or null). It must now return the video itself.
|
|
203
|
+
expect(getPreviewTargetFromPointer(iframe, 200, 150, "index.html")).toBe(hero);
|
|
204
|
+
|
|
205
|
+
iframe.remove();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("still excludes a full-bleed non-media container so clicks reach inner content", () => {
|
|
209
|
+
const { iframe, doc } = createPreviewIframe();
|
|
210
|
+
|
|
211
|
+
doc.body.innerHTML = `
|
|
212
|
+
<main id="scene" data-composition-id="scene">
|
|
213
|
+
<div id="wrapper"><h1 id="headline">Title</h1></div>
|
|
214
|
+
</main>
|
|
215
|
+
`;
|
|
216
|
+
|
|
217
|
+
const scene = doc.getElementById("scene");
|
|
218
|
+
const wrapper = doc.getElementById("wrapper");
|
|
219
|
+
const headline = doc.getElementById("headline");
|
|
220
|
+
if (!scene || !wrapper || !headline) throw new Error("Expected preview fixture elements");
|
|
221
|
+
|
|
222
|
+
stubRect(iframe, domRect(0, 0, 400, 300));
|
|
223
|
+
stubRect(scene, domRect(0, 0, 400, 300));
|
|
224
|
+
stubRect(wrapper, domRect(0, 0, 400, 300));
|
|
225
|
+
stubRect(headline, domRect(40, 40, 160, 48));
|
|
226
|
+
doc.elementsFromPoint = () => [headline, wrapper, scene];
|
|
227
|
+
|
|
228
|
+
expect(getPreviewTargetFromPointer(iframe, 80, 64, "index.html")).toBe(headline);
|
|
229
|
+
|
|
230
|
+
iframe.remove();
|
|
231
|
+
});
|
|
175
232
|
});
|
|
@@ -21,6 +21,15 @@ interface PreviewLocalPointer {
|
|
|
21
21
|
// should remain canvas-selectable.
|
|
22
22
|
const FULL_BLEED_RATIO = 0.95;
|
|
23
23
|
|
|
24
|
+
// Media leaves (a hero/background video, a full-bleed image, an <svg>/<canvas>
|
|
25
|
+
// backdrop) ARE the content a user clicks — they must stay canvas-selectable even
|
|
26
|
+
// at full-bleed. Only empty containers (scene wrappers, layout backdrops) get
|
|
27
|
+
// excluded. Without this, a full-bleed <video> is skipped and the click lands on
|
|
28
|
+
// whatever sits behind it — the reported "can't select videos / selects the layer
|
|
29
|
+
// behind" bug (and the "needs a second click" symptom, where the first click
|
|
30
|
+
// resolves through the video to nothing and only the hover fallback recovers).
|
|
31
|
+
const FULL_BLEED_SELECTABLE_MEDIA_TAGS = new Set(["video", "img", "canvas", "svg"]);
|
|
32
|
+
|
|
24
33
|
export function coversComposition(
|
|
25
34
|
elRect: { width: number; height: number },
|
|
26
35
|
viewport: DomEditViewport,
|
|
@@ -33,6 +42,7 @@ export function coversComposition(
|
|
|
33
42
|
}
|
|
34
43
|
|
|
35
44
|
function isFullBleedTarget(el: HTMLElement, viewport: DomEditViewport): boolean {
|
|
45
|
+
if (FULL_BLEED_SELECTABLE_MEDIA_TAGS.has(el.tagName.toLowerCase())) return false;
|
|
36
46
|
return coversComposition(el.getBoundingClientRect(), viewport);
|
|
37
47
|
}
|
|
38
48
|
|
|
@@ -42,7 +42,10 @@ function getSessionProperties(): EventProperties {
|
|
|
42
42
|
viewport_width: window.innerWidth,
|
|
43
43
|
viewport_height: window.innerHeight,
|
|
44
44
|
user_agent: navigator.userAgent,
|
|
45
|
-
|
|
45
|
+
// Route slug only — drop the query string, which carries the current
|
|
46
|
+
// selection (selId / selSelector are the user's own element ids/CSS
|
|
47
|
+
// selectors) and other view state we must not send to analytics.
|
|
48
|
+
url_hash: location.hash.replace(/#project\//, "").split("?")[0],
|
|
46
49
|
};
|
|
47
50
|
}
|
|
48
51
|
|
|
@@ -79,7 +79,6 @@ function renderStudioUrlStateHarness(
|
|
|
79
79
|
previewIframeRef: { current: null },
|
|
80
80
|
rightPanelTab: "renders",
|
|
81
81
|
rightCollapsed: true,
|
|
82
|
-
timelineVisible: true,
|
|
83
82
|
activeCompPathHydrated: true,
|
|
84
83
|
domEditSelection: null,
|
|
85
84
|
buildDomSelectionFromTarget: () => Promise.resolve(null),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AUDIO_EXT, IMAGE_EXT, VIDEO_EXT } from "./mediaTypes";
|
|
2
|
+
import { patchRootCompositionDuration, readRootCompositionDuration } from "./rootDuration";
|
|
2
3
|
import { roundToCenti } from "./rounding";
|
|
3
4
|
import { COMPOSITION_ROOT_OPEN_TAG_RE } from "./compositionPatterns";
|
|
4
5
|
|
|
@@ -98,6 +99,8 @@ export function resolveTimelineAssetInitialGeometry(source: string): {
|
|
|
98
99
|
|
|
99
100
|
export function buildTimelineAssetInsertHtml(input: {
|
|
100
101
|
id: string;
|
|
102
|
+
/** Stable hf-id stamped as data-hf-id by the NLE drop path (optional in the legacy path). */
|
|
103
|
+
hfId?: string;
|
|
101
104
|
assetPath: string;
|
|
102
105
|
kind: TimelineAssetKind;
|
|
103
106
|
start: number;
|
|
@@ -136,3 +139,62 @@ export function insertTimelineAssetIntoSource(source: string, assetHtml: string)
|
|
|
136
139
|
.join("\n");
|
|
137
140
|
return `${source.slice(0, insertAt)}\n${childIndent}${indented}${source.slice(insertAt)}`;
|
|
138
141
|
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Set the composition root's `data-duration` to `contentEnd` (grow OR shrink) so the
|
|
145
|
+
* timeline length tracks content — the content-driven counterpart to
|
|
146
|
+
* extendCompositionDurationIfNeeded's grow-only ratchet. Used after edits that can
|
|
147
|
+
* reduce the furthest clip end (delete/trim). No-op when `contentEnd` is not > 0, so
|
|
148
|
+
* an empty timeline keeps its declared duration instead of collapsing to 0.
|
|
149
|
+
*/
|
|
150
|
+
export function setCompositionDurationToContent(source: string, contentEnd: number): string {
|
|
151
|
+
if (!Number.isFinite(contentEnd) || contentEnd <= 0) return source;
|
|
152
|
+
const rootDur = readRootCompositionDuration(source);
|
|
153
|
+
if (rootDur == null) return source;
|
|
154
|
+
const next = roundToCenti(contentEnd);
|
|
155
|
+
if (rootDur === next) return source;
|
|
156
|
+
return patchRootCompositionDuration(source, String(next));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function extendCompositionDurationIfNeeded(source: string, requiredEnd: number): string {
|
|
160
|
+
const rootDur = readRootCompositionDuration(source);
|
|
161
|
+
if (rootDur == null || !Number.isFinite(rootDur) || requiredEnd <= rootDur) return source;
|
|
162
|
+
return patchRootCompositionDuration(source, String(roundToCenti(requiredEnd)));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Set the composition root's `data-duration` to `contentEnd` (grow OR shrink) so the
|
|
167
|
+
* timeline length tracks content — the content-driven counterpart to
|
|
168
|
+
* extendCompositionDurationIfNeeded's grow-only ratchet. Used after edits that can
|
|
169
|
+
* reduce the furthest clip end (delete/trim). No-op when `contentEnd` is not > 0, so
|
|
170
|
+
* an empty timeline keeps its declared duration instead of collapsing to 0.
|
|
171
|
+
*/
|
|
172
|
+
export function fitTimelineAssetGeometry(
|
|
173
|
+
natural: { width: number; height: number } | null,
|
|
174
|
+
comp: { width: number; height: number },
|
|
175
|
+
): { left: number; top: number; width: number; height: number } {
|
|
176
|
+
if (!natural || natural.width <= 0 || natural.height <= 0) {
|
|
177
|
+
return { left: 0, top: 0, width: comp.width, height: comp.height };
|
|
178
|
+
}
|
|
179
|
+
const scale = Math.min(1, comp.width / natural.width, comp.height / natural.height);
|
|
180
|
+
const width = Math.round(natural.width * scale);
|
|
181
|
+
const height = Math.round(natural.height * scale);
|
|
182
|
+
return {
|
|
183
|
+
left: Math.round((comp.width - width) / 2),
|
|
184
|
+
top: Math.round((comp.height - height) / 2),
|
|
185
|
+
width,
|
|
186
|
+
height,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function resolveTimelineAssetCompositionSize(source: string): {
|
|
191
|
+
width: number;
|
|
192
|
+
height: number;
|
|
193
|
+
} {
|
|
194
|
+
const width = Number.parseFloat(source.match(/\bdata-width=(["'])([^"']+)\1/i)?.[2] ?? "");
|
|
195
|
+
const height = Number.parseFloat(source.match(/\bdata-height=(["'])([^"']+)\1/i)?.[2] ?? "");
|
|
196
|
+
return {
|
|
197
|
+
width: Number.isFinite(width) && width > 0 ? Math.round(width) : 640,
|
|
198
|
+
height: Number.isFinite(height) && height > 0 ? Math.round(height) : 360,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
export const TIMELINE_TOGGLE_SHORTCUT_LABEL = "Shift+T";
|
|
2
|
-
type TimelineToggleHotkeyEvent = Pick<
|
|
3
|
-
KeyboardEvent,
|
|
4
|
-
"key" | "shiftKey" | "metaKey" | "ctrlKey" | "altKey" | "target"
|
|
5
|
-
>;
|
|
6
|
-
|
|
7
1
|
interface EditableTargetLike {
|
|
8
2
|
tagName?: string;
|
|
9
3
|
isContentEditable?: boolean;
|
|
@@ -28,14 +22,3 @@ export function isEditableTarget(target: EventTarget | null): boolean {
|
|
|
28
22
|
),
|
|
29
23
|
);
|
|
30
24
|
}
|
|
31
|
-
|
|
32
|
-
export function shouldHandleTimelineToggleHotkey(event: TimelineToggleHotkeyEvent): boolean {
|
|
33
|
-
if (event.metaKey || event.ctrlKey || event.altKey) return false;
|
|
34
|
-
if (!event.shiftKey) return false;
|
|
35
|
-
if (event.key.toLowerCase() !== "t") return false;
|
|
36
|
-
return !isEditableTarget(event.target);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function getTimelineToggleTitle(timelineVisible: boolean): string {
|
|
40
|
-
return `${timelineVisible ? "Hide" : "Show"} timeline editor (${TIMELINE_TOGGLE_SHORTCUT_LABEL})`;
|
|
41
|
-
}
|