@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.
Files changed (237) hide show
  1. package/dist/assets/{index-BXaqaVKt.js → index-BWnOxAiH.js} +1 -1
  2. package/dist/assets/{index-CPetwHFV.js → index-C4csZims.js} +1 -1
  3. package/dist/assets/index-CmVCjZjp.js +423 -0
  4. package/dist/assets/index-D-GyYi2d.css +1 -0
  5. package/dist/{chunk-SOTCF4DF.js → chunk-5QSIMBEJ.js} +2 -1
  6. package/dist/chunk-5QSIMBEJ.js.map +1 -0
  7. package/dist/{domEditingLayers-2ECJK24D.js → domEditingLayers-6LQGKPOI.js} +2 -2
  8. package/dist/index.d.ts +146 -144
  9. package/dist/index.html +2 -2
  10. package/dist/index.js +40414 -37680
  11. package/dist/index.js.map +1 -1
  12. package/package.json +7 -7
  13. package/src/App.tsx +106 -96
  14. package/src/components/EditorShell.tsx +253 -0
  15. package/src/components/StudioGlobalDragOverlay.tsx +1 -3
  16. package/src/components/StudioLeftSidebar.tsx +13 -3
  17. package/src/components/StudioRightPanel.tsx +8 -3
  18. package/src/components/TimelineToolbar.test.tsx +2 -2
  19. package/src/components/TimelineToolbar.tsx +179 -121
  20. package/src/components/editor/DomEditCropHandles.test.tsx +74 -3
  21. package/src/components/editor/DomEditCropHandles.tsx +64 -29
  22. package/src/components/editor/DomEditOverlay.test.ts +245 -279
  23. package/src/components/editor/DomEditOverlay.tsx +158 -126
  24. package/src/components/editor/DomEditOverlayHover.test.tsx +90 -0
  25. package/src/components/editor/DomEditRotateHandle.tsx +38 -18
  26. package/src/components/editor/DomEditSelectionChrome.test.tsx +80 -0
  27. package/src/components/editor/DomEditSelectionChrome.tsx +257 -0
  28. package/src/components/editor/LayersPanel.test.ts +66 -2
  29. package/src/components/editor/LayersPanel.tsx +43 -1
  30. package/src/components/editor/OffCanvasIndicators.test.tsx +34 -0
  31. package/src/components/editor/OffCanvasIndicators.tsx +49 -10
  32. package/src/components/editor/SnapGuideOverlay.tsx +28 -23
  33. package/src/components/editor/SnapToolbar.test.tsx +0 -1
  34. package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +214 -0
  35. package/src/components/editor/anchoredResizeReleaseShift.test.ts +106 -10
  36. package/src/components/editor/domEditNudge.test.ts +80 -0
  37. package/src/components/editor/domEditNudge.ts +44 -0
  38. package/src/components/editor/domEditOverlayGeometry.test.ts +167 -1
  39. package/src/components/editor/domEditOverlayGeometry.ts +280 -20
  40. package/src/components/editor/domEditOverlayGestures.ts +44 -41
  41. package/src/components/editor/domEditOverlayStartGesture.ts +62 -2
  42. package/src/components/editor/domEditResizeLocal.test.ts +131 -0
  43. package/src/components/editor/domEditResizeLocal.ts +125 -0
  44. package/src/components/editor/domEditingDom.ts +1 -1
  45. package/src/components/editor/manualEditsDomPatches.test.ts +47 -0
  46. package/src/components/editor/manualOffsetDrag.ts +55 -12
  47. package/src/components/editor/offCanvasIndicatorGeometry.ts +31 -10
  48. package/src/components/editor/offCanvasIndicatorRefresh.test.tsx +25 -1
  49. package/src/components/editor/resizeDraft.ts +108 -0
  50. package/src/components/editor/snapEngine.test.ts +12 -78
  51. package/src/components/editor/snapEngine.ts +13 -53
  52. package/src/components/editor/useDomEditNudge.test.tsx +228 -0
  53. package/src/components/editor/useDomEditNudge.ts +254 -0
  54. package/src/components/editor/useDomEditOverlayGestures.ts +115 -116
  55. package/src/components/editor/useDomEditOverlayRects.ts +9 -2
  56. package/src/components/nle/AssetPreviewOverlay.tsx +147 -0
  57. package/src/components/nle/NLEContext.test.ts +144 -0
  58. package/src/components/nle/NLEContext.tsx +328 -0
  59. package/src/components/nle/NLEPreview.tsx +1 -1
  60. package/src/components/nle/PreviewOverlays.tsx +240 -0
  61. package/src/components/nle/PreviewPane.tsx +163 -0
  62. package/src/components/nle/TimelinePane.test.ts +60 -0
  63. package/src/components/nle/TimelinePane.tsx +293 -0
  64. package/src/components/nle/TimelineResizeDivider.tsx +8 -2
  65. package/src/components/nle/useCompositionStack.test.tsx +44 -0
  66. package/src/components/nle/useCompositionStack.ts +17 -9
  67. package/src/components/nle/useTimelineEditCallbacks.ts +217 -0
  68. package/src/components/sidebar/AssetCard.tsx +326 -0
  69. package/src/components/sidebar/AssetContextMenu.tsx +15 -30
  70. package/src/components/sidebar/AssetsTab.test.ts +96 -0
  71. package/src/components/sidebar/AssetsTab.tsx +71 -206
  72. package/src/components/sidebar/AudioRow.tsx +39 -2
  73. package/src/components/sidebar/BlocksTab.tsx +8 -1
  74. package/src/components/sidebar/LeftSidebar.tsx +4 -1
  75. package/src/components/sidebar/assetHelpers.ts +29 -0
  76. package/src/contexts/DomEditContext.tsx +9 -0
  77. package/src/contexts/StudioContext.tsx +9 -8
  78. package/src/contexts/TimelineEditContext.tsx +3 -7
  79. package/src/hooks/deleteSelectedKeyframes.ts +35 -0
  80. package/src/hooks/domEditCommitTypes.ts +12 -1
  81. package/src/hooks/domSelectionTestHarness.ts +14 -0
  82. package/src/hooks/gestureTransaction.test.ts +311 -0
  83. package/src/hooks/gestureTransaction.ts +199 -0
  84. package/src/hooks/gsapDragCommit.test.ts +141 -41
  85. package/src/hooks/gsapDragCommit.ts +64 -74
  86. package/src/hooks/gsapDragStaticSetHelpers.ts +9 -21
  87. package/src/hooks/gsapResizeIntercept.test.ts +69 -0
  88. package/src/hooks/gsapResizeIntercept.ts +38 -10
  89. package/src/hooks/gsapRuntimeBridge.test.ts +49 -13
  90. package/src/hooks/gsapRuntimeBridge.ts +4 -4
  91. package/src/hooks/gsapScriptCommitTypes.ts +16 -5
  92. package/src/hooks/gsapShared.test.ts +19 -1
  93. package/src/hooks/gsapShared.ts +14 -0
  94. package/src/hooks/patchDocumentRootDuration.test.ts +71 -0
  95. package/src/hooks/timelineEditingGsap.ts +14 -0
  96. package/src/hooks/timelineEditingHelpers.test.ts +34 -1
  97. package/src/hooks/timelineEditingHelpers.ts +22 -48
  98. package/src/hooks/timelineMoveAdapter.test.ts +77 -0
  99. package/src/hooks/timelineMoveAdapter.ts +37 -0
  100. package/src/hooks/useAnimatedPropertyCommit.test.tsx +36 -8
  101. package/src/hooks/useAnimatedPropertyCommit.ts +31 -26
  102. package/src/hooks/useAppHotkeys.ts +16 -25
  103. package/src/hooks/useBlockHandlers.ts +55 -27
  104. package/src/hooks/useContextMenuDismiss.ts +31 -7
  105. package/src/hooks/useDomEditCommits.test.tsx +154 -1
  106. package/src/hooks/useDomEditCommits.ts +100 -3
  107. package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
  108. package/src/hooks/useDomEditSession.ts +2 -2
  109. package/src/hooks/useDomEditTextCommits.test.tsx +135 -0
  110. package/src/hooks/useDomEditTextCommits.ts +8 -3
  111. package/src/hooks/useDomEditWiring.ts +7 -9
  112. package/src/hooks/useDomGeometryCommits.ts +17 -2
  113. package/src/hooks/useDomSelection.test.ts +2 -36
  114. package/src/hooks/useDomSelection.ts +81 -51
  115. package/src/hooks/useDomSelectionSelectionGuards.test.ts +276 -0
  116. package/src/hooks/useElementLifecycleOps.test.tsx +258 -0
  117. package/src/hooks/useElementLifecycleOps.ts +40 -71
  118. package/src/hooks/useEnableKeyframes.test.ts +142 -1
  119. package/src/hooks/useEnableKeyframes.ts +73 -29
  120. package/src/hooks/useGestureCommit.test.tsx +130 -0
  121. package/src/hooks/useGestureCommit.ts +32 -8
  122. package/src/hooks/useGroupCommits.ts +7 -28
  123. package/src/hooks/useGsapAwareEditing.test.tsx +239 -0
  124. package/src/hooks/useGsapAwareEditing.ts +115 -20
  125. package/src/hooks/useGsapKeyframeOps.test.tsx +59 -0
  126. package/src/hooks/useGsapKeyframeOps.ts +38 -10
  127. package/src/hooks/useGsapScriptCommits.test.tsx +40 -1
  128. package/src/hooks/useGsapScriptCommits.ts +134 -50
  129. package/src/hooks/useGsapSelectionHandlers.ts +49 -16
  130. package/src/hooks/useMusicBeatAnalysis.ts +72 -36
  131. package/src/hooks/usePersistentEditHistory.test.ts +33 -0
  132. package/src/hooks/usePersistentEditHistory.ts +92 -58
  133. package/src/hooks/usePreviewPersistence.ts +25 -7
  134. package/src/hooks/useRazorSplit.history.test.tsx +148 -18
  135. package/src/hooks/useRazorSplit.test.ts +269 -0
  136. package/src/hooks/useRazorSplit.testHelpers.ts +64 -0
  137. package/src/hooks/useRazorSplit.ts +107 -36
  138. package/src/hooks/useRenderClipContent.ts +24 -6
  139. package/src/hooks/useStudioContextValue.ts +15 -5
  140. package/src/hooks/useStudioUrlState.ts +2 -4
  141. package/src/hooks/useTimelineEditing.test.tsx +20 -29
  142. package/src/hooks/useTimelineEditingTypes.ts +6 -0
  143. package/src/hooks/useTimelineGroupEditing.ts +18 -4
  144. package/src/index.ts +1 -1
  145. package/src/player/components/ImageThumbnail.test.tsx +173 -0
  146. package/src/player/components/ImageThumbnail.tsx +160 -0
  147. package/src/player/components/PlayheadIndicator.tsx +50 -5
  148. package/src/player/components/Timeline.test.ts +102 -19
  149. package/src/player/components/Timeline.tsx +194 -194
  150. package/src/player/components/TimelineCanvas.tsx +207 -521
  151. package/src/player/components/TimelineClip.tsx +4 -1
  152. package/src/player/components/TimelineClipDiamonds.tsx +9 -40
  153. package/src/player/components/TimelineEmptyState.tsx +3 -1
  154. package/src/player/components/TimelineLanes.tsx +487 -0
  155. package/src/player/components/TimelineOverlays.tsx +122 -0
  156. package/src/player/components/VideoThumbnail.test.tsx +152 -0
  157. package/src/player/components/VideoThumbnail.tsx +28 -7
  158. package/src/player/components/thumbnailUtils.test.ts +127 -0
  159. package/src/player/components/thumbnailUtils.ts +54 -0
  160. package/src/player/components/timelineCallbacks.ts +18 -18
  161. package/src/player/components/timelineClipChildren.tsx +39 -0
  162. package/src/player/components/timelineClipDragCommit.test.ts +943 -0
  163. package/src/player/components/timelineClipDragCommit.ts +401 -0
  164. package/src/player/components/timelineClipDragPreview.test.ts +144 -0
  165. package/src/player/components/timelineClipDragPreview.ts +323 -38
  166. package/src/player/components/timelineClipDragTypes.ts +65 -0
  167. package/src/player/components/timelineCollision.test.ts +42 -2
  168. package/src/player/components/timelineDragDrop.ts +69 -60
  169. package/src/player/components/timelineEditCapabilities.ts +52 -0
  170. package/src/player/components/timelineEditing.test.ts +1 -1
  171. package/src/player/components/timelineEditing.ts +76 -59
  172. package/src/player/components/timelineGroupEditing.test.ts +126 -0
  173. package/src/player/components/timelineGroupEditing.ts +133 -0
  174. package/src/player/components/timelineGroupResizeCommit.ts +54 -0
  175. package/src/player/components/timelineLayout.test.ts +95 -23
  176. package/src/player/components/timelineLayout.ts +120 -61
  177. package/src/player/components/timelineMarquee.test.ts +197 -0
  178. package/src/player/components/timelineMarquee.ts +112 -0
  179. package/src/player/components/timelineOptimisticRevision.ts +42 -0
  180. package/src/player/components/timelineTheme.ts +6 -1
  181. package/src/player/components/useResolvedTimelineEditCallbacks.ts +25 -3
  182. package/src/player/components/useTimelineClipDrag.resize.test.tsx +241 -0
  183. package/src/player/components/useTimelineClipDrag.ts +380 -386
  184. package/src/player/components/useTimelineEditPinning.ts +121 -0
  185. package/src/player/components/useTimelineGeometry.ts +129 -0
  186. package/src/player/components/useTimelinePlayhead.ts +4 -16
  187. package/src/player/components/useTimelineRangeSelection.ts +341 -50
  188. package/src/player/components/useTimelineStackingSync.test.tsx +67 -0
  189. package/src/player/components/useTimelineStackingSync.ts +83 -0
  190. package/src/player/hooks/useExpandedTimelineElements.ts +0 -3
  191. package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
  192. package/src/player/hooks/useTimelinePlayer.ts +48 -53
  193. package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
  194. package/src/player/index.ts +3 -2
  195. package/src/player/lib/time.test.ts +19 -19
  196. package/src/player/lib/time.ts +6 -2
  197. package/src/player/lib/timelineDOM.test.ts +25 -98
  198. package/src/player/lib/timelineDOM.ts +12 -81
  199. package/src/player/lib/timelineElementHelpers.ts +54 -5
  200. package/src/player/lib/timelineIframeHelpers.ts +2 -0
  201. package/src/player/store/playerStore.ts +62 -3
  202. package/src/styles/studio.css +27 -0
  203. package/src/utils/assetPreviewStore.ts +33 -0
  204. package/src/utils/blockInstaller.ts +10 -2
  205. package/src/utils/editHistory.test.ts +35 -0
  206. package/src/utils/gsapSoftReload.test.ts +119 -1
  207. package/src/utils/gsapSoftReload.ts +153 -0
  208. package/src/utils/mediaTypes.ts +3 -2
  209. package/src/utils/resizeDebug.ts +55 -0
  210. package/src/utils/rootDuration.test.ts +90 -1
  211. package/src/utils/rootDuration.ts +76 -13
  212. package/src/utils/sdkCutover.ts +4 -0
  213. package/src/utils/studioFileHistory.ts +37 -7
  214. package/src/utils/studioHelpers.ts +62 -0
  215. package/src/utils/studioPreviewHelpers.test.ts +65 -8
  216. package/src/utils/studioPreviewHelpers.ts +10 -0
  217. package/src/utils/studioTelemetry.ts +4 -1
  218. package/src/utils/studioUrlState.test.ts +0 -1
  219. package/src/utils/timelineAssetDrop.ts +62 -0
  220. package/src/utils/timelineDiscovery.ts +0 -17
  221. package/dist/assets/index-BRwkMj0w.js +0 -423
  222. package/dist/assets/index-Dq7FEg0K.css +0 -1
  223. package/dist/chunk-SOTCF4DF.js.map +0 -1
  224. package/src/components/StudioPreviewArea.tsx +0 -500
  225. package/src/components/nle/NLELayout.test.ts +0 -12
  226. package/src/components/nle/NLELayout.tsx +0 -591
  227. package/src/player/components/TimelineLayerGutter.tsx +0 -61
  228. package/src/player/components/TimelineSelectionOverlays.tsx +0 -55
  229. package/src/player/components/timelineMarqueeSelection.test.ts +0 -87
  230. package/src/player/components/timelineSnapTargets.test.ts +0 -144
  231. package/src/player/components/timelineSnapTargets.ts +0 -164
  232. package/src/player/components/useTimelineClipDrag.test.tsx +0 -506
  233. package/src/player/components/useTimelineClipGroupDrag.ts +0 -417
  234. package/src/player/components/useTimelineMarqueeSelection.test.tsx +0 -231
  235. package/src/player/components/useTimelineMarqueeSelection.ts +0 -276
  236. package/src/utils/timelineDiscovery.test.ts +0 -90
  237. /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 = document.createElement("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 = document.createElement("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
- url_hash: location.hash.replace(/#project\//, ""),
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
- }