@hyperframes/studio 0.8.33 → 0.8.34

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 (85) hide show
  1. package/dist/assets/{hyperframes-player-CNhkujVy.js → hyperframes-player-D4aSRr_M.js} +1 -1
  2. package/dist/assets/{index-DOoo96Tb.js → index-CkxPQxLJ.js} +182 -172
  3. package/dist/assets/{index-DiStlnx7.js → index-aNyQBoJv.js} +1 -1
  4. package/dist/assets/{index-CslAQ3JV.js → index-hch4ktcC.js} +1 -1
  5. package/dist/{chunk-A7S5SGNA.js → chunk-7BP32G62.js} +294 -53
  6. package/dist/chunk-7BP32G62.js.map +1 -0
  7. package/dist/{domEditingLayers-JMMVOGTO.js → domEditingLayers-UKMVYXT6.js} +2 -2
  8. package/dist/index.html +1 -1
  9. package/dist/index.js +354 -206
  10. package/dist/index.js.map +1 -1
  11. package/package.json +8 -8
  12. package/src/App.tsx +6 -4
  13. package/src/captions/components/CaptionOverlay.sender.test.tsx +47 -0
  14. package/src/captions/components/CaptionOverlay.tsx +1 -0
  15. package/src/captions/generator.test.ts +30 -0
  16. package/src/captions/generator.ts +5 -3
  17. package/src/captions/hooks/useCaptionSync.ts +3 -2
  18. package/src/components/MediaPreview.tsx +2 -1
  19. package/src/components/editor/AnimationCard.test.tsx +3 -3
  20. package/src/components/editor/PropertyPanel.test.tsx +18 -14
  21. package/src/components/editor/SnapGuideOverlay.tsx +2 -6
  22. package/src/components/editor/domEditLayerWalkCache.test.ts +284 -0
  23. package/src/components/editor/domEditLayerWalkCache.ts +327 -0
  24. package/src/components/editor/domEditOverlayBasis.ts +72 -0
  25. package/src/components/editor/domEditOverlayGeometry.ts +78 -83
  26. package/src/components/editor/domEditOverlayMeasurePass.ts +60 -0
  27. package/src/components/editor/domEditOverlayTransform.ts +47 -9
  28. package/src/components/editor/domEditingDom.ts +63 -14
  29. package/src/components/editor/domEditingElement.ts +56 -14
  30. package/src/components/editor/domEditingLayers.test.ts +48 -0
  31. package/src/components/editor/domEditingLayers.ts +22 -35
  32. package/src/components/editor/offCanvasIndicatorGeometry.complexity.test.ts +373 -0
  33. package/src/components/editor/offCanvasIndicatorGeometry.ts +50 -11
  34. package/src/components/editor/offCanvasIndicatorRefresh.ts +43 -16
  35. package/src/components/editor/overlayFrameLoop.test.ts +246 -0
  36. package/src/components/editor/overlayFrameLoop.ts +204 -0
  37. package/src/components/editor/probeSourceElement.ts +29 -0
  38. package/src/components/editor/propertyPanelFill.tsx +2 -1
  39. package/src/components/editor/propertyPanelFlatEffectsSection.test.tsx +2 -2
  40. package/src/components/editor/useDomEditCompositionRect.ts +2 -4
  41. package/src/components/editor/useDomEditOverlayRects.ts +15 -9
  42. package/src/components/editor/useMotionPathData.ts +2 -4
  43. package/src/components/feedback/projectProvenance.ts +4 -1
  44. package/src/components/nle/NLEContext.tsx +2 -1
  45. package/src/components/nle/PreviewOverlays.tsx +1 -1
  46. package/src/components/nle/useCompositionStack.ts +11 -4
  47. package/src/components/renders/RenderQueue.test.tsx +2 -2
  48. package/src/components/renders/RenderQueueItem.tsx +2 -1
  49. package/src/components/renders/useRenderQueue.ts +3 -2
  50. package/src/components/sidebar/AssetsTab.tsx +2 -1
  51. package/src/components/sidebar/CompositionsTab.tsx +2 -1
  52. package/src/components/storyboard/FramePoster.tsx +2 -1
  53. package/src/hooks/useAskAgentModal.ts +2 -1
  54. package/src/hooks/useCaptionDetection.sender.test.tsx +54 -0
  55. package/src/hooks/useCaptionDetection.ts +3 -1
  56. package/src/hooks/useCompositionContentLoader.ts +3 -1
  57. package/src/hooks/useCompositionDimensions.test.tsx +47 -0
  58. package/src/hooks/useCompositionDimensions.ts +3 -2
  59. package/src/hooks/useDomEditCommits.ts +5 -4
  60. package/src/hooks/useElementLifecycleOps.ts +5 -1
  61. package/src/hooks/useFileTree.ts +5 -4
  62. package/src/hooks/useGroupCommits.ts +2 -1
  63. package/src/hooks/useGsapInteractionFailureTelemetry.test.tsx +2 -2
  64. package/src/hooks/useLintModal.ts +2 -1
  65. package/src/hooks/useRenderClipContent.ts +6 -5
  66. package/src/hooks/useTimelineDeleteOps.ts +5 -1
  67. package/src/player/components/Player.tsx +6 -4
  68. package/src/player/components/TimelineAutomationLane.test.tsx +9 -9
  69. package/src/player/components/thumbnailUtils.ts +6 -2
  70. package/src/player/components/timelineDragDrop.test.tsx +3 -3
  71. package/src/player/lib/timelineIframeHelpers.test.ts +36 -0
  72. package/src/player/lib/timelineIframeHelpers.ts +17 -1
  73. package/src/test-setup.ts +8 -0
  74. package/src/utils/blockInstaller.ts +2 -1
  75. package/src/utils/projectRouting.test.ts +9 -0
  76. package/src/utils/projectRouting.ts +18 -1
  77. package/src/utils/razorSplitTransaction.test.ts +3 -3
  78. package/src/utils/sourceScopedSelectorIndex.test.ts +148 -0
  79. package/src/utils/sourceScopedSelectorIndex.ts +64 -6
  80. package/src/utils/studioFileHistory.ts +2 -1
  81. package/src/utils/studioHelpers.test.ts +5 -1
  82. package/src/utils/studioHelpers.ts +3 -2
  83. package/src/utils/timelineCompositionInsert.test.ts +3 -3
  84. package/dist/chunk-A7S5SGNA.js.map +0 -1
  85. /package/dist/{domEditingLayers-JMMVOGTO.js.map → domEditingLayers-UKMVYXT6.js.map} +0 -0
package/dist/index.js CHANGED
@@ -1,13 +1,18 @@
1
1
  import {
2
+ DOM_EDIT_LAYER_OBSERVER_INIT,
2
3
  buildDefaultDomEditTextField,
3
4
  buildDomEditPatchTarget,
4
5
  buildDomEditRichTextPatchOperation,
5
6
  buildDomEditStylePatchOperation,
6
7
  buildDomEditTextPatchOperation,
8
+ buildProjectApiPath,
9
+ buildProjectHash,
7
10
  buildStableSelector,
8
11
  buildTextFieldChildLocator,
9
12
  collectDomEditLayerItems,
13
+ createDomEditLayerWalkCache,
10
14
  domEditSelectionToFacts,
15
+ drainPendingLayerMutations,
11
16
  escapeCssString,
12
17
  findElementForSelection,
13
18
  findElementForTimelineElement,
@@ -20,12 +25,14 @@ import {
20
25
  isElementVisibleThroughAncestors,
21
26
  isHtmlElement,
22
27
  isTextEditableSelection,
28
+ parseProjectHashRoute,
29
+ parseProjectIdFromHash,
23
30
  readHfId,
24
31
  resolveAllVisualDomEditTargets,
25
32
  resolveDomEditSelection,
26
33
  serializeDomEditTextFields,
27
34
  setCompositionSourceMap
28
- } from "./chunk-A7S5SGNA.js";
35
+ } from "./chunk-7BP32G62.js";
29
36
  import {
30
37
  __export
31
38
  } from "./chunk-PZ5AY32C.js";
@@ -532,15 +539,14 @@ var Player = forwardRef2(
532
539
  useMountEffect(() => {
533
540
  const container = containerRef.current;
534
541
  if (!container) return;
542
+ const previewSource = directUrl || (projectId ? buildProjectApiPath(projectId, "/preview") : null);
543
+ if (!previewSource) return;
535
544
  let canceled = false;
536
545
  let cleanup;
537
546
  import("@hyperframes/player").then(() => {
538
547
  if (canceled) return;
539
548
  const player = document.createElement("hyperframes-player");
540
- const srcUrl = new URL(
541
- directUrl || `/api/projects/${projectId}/preview`,
542
- window.location.origin
543
- );
549
+ const srcUrl = new URL(previewSource, window.location.origin);
544
550
  applyPreviewVariablesToUrl(srcUrl);
545
551
  const src = srcUrl.pathname + srcUrl.search;
546
552
  const retryPreview = () => {
@@ -3764,10 +3770,13 @@ var scrubAudioEl = null;
3764
3770
  var scrubStopTimer = null;
3765
3771
  var scrubPrevMuted = null;
3766
3772
  var scrubPrevVolume = null;
3773
+ function isAudioNode(node) {
3774
+ return node !== null && node.namespaceURI === "http://www.w3.org/1999/xhtml" && node.localName === "audio";
3775
+ }
3767
3776
  function resolveScrubAudioEl(doc, musicId) {
3768
3777
  if (musicId) {
3769
3778
  const byId = doc.getElementById(musicId);
3770
- if (byId instanceof HTMLAudioElement) return byId;
3779
+ if (isAudioNode(byId)) return byId;
3771
3780
  }
3772
3781
  return doc.querySelector("audio[data-timeline-role='music']") ?? doc.querySelector("audio");
3773
3782
  }
@@ -13093,7 +13102,7 @@ async function resolveDroppedAssetDuration(projectId, assetPath, kind) {
13093
13102
  if (kind === "image") return DEFAULT_TIMELINE_ASSET_DURATION.image;
13094
13103
  const media = document.createElement(kind === "video" ? "video" : "audio");
13095
13104
  media.preload = "metadata";
13096
- media.src = `/api/projects/${projectId}/preview/${assetPath}`;
13105
+ media.src = buildProjectApiPath(projectId, `/preview/${assetPath}`);
13097
13106
  const duration = await new Promise((resolve) => {
13098
13107
  const timeout = window.setTimeout(() => resolve(DEFAULT_TIMELINE_ASSET_DURATION[kind]), 3e3);
13099
13108
  const finalize = (value) => {
@@ -16401,7 +16410,7 @@ async function retryStudioSave(operation, options = {}) {
16401
16410
 
16402
16411
  // src/utils/studioFileHistory.ts
16403
16412
  async function readProjectFileContent(pid, path) {
16404
- const response = await fetch(`/api/projects/${pid}/files/${encodeURIComponent(path)}`);
16413
+ const response = await fetch(buildProjectApiPath(pid, `/files/${encodeURIComponent(path)}`));
16405
16414
  if (!response.ok) {
16406
16415
  throw await createStudioSaveHttpError(response, `Failed to read ${path}`);
16407
16416
  }
@@ -24058,7 +24067,7 @@ function resolveMediaPreviewUrl(src, projectId, studioOrigin) {
24058
24067
  return src;
24059
24068
  }
24060
24069
  if (!studioOrigin || parsed.origin !== studioOrigin) return src;
24061
- const previewPath = new URL(`/api/projects/${projectId}/preview/`, studioOrigin).pathname;
24070
+ const previewPath = new URL(buildProjectApiPath(projectId, `/preview/`), studioOrigin).pathname;
24062
24071
  if (parsed.pathname.startsWith(previewPath)) return src;
24063
24072
  if (parsed.pathname.startsWith("/api/")) return src;
24064
24073
  try {
@@ -24068,7 +24077,10 @@ function resolveMediaPreviewUrl(src, projectId, studioOrigin) {
24068
24077
  }
24069
24078
  suffix = `${parsed.search}${parsed.hash}`;
24070
24079
  }
24071
- return `/api/projects/${projectId}/preview/${encodePreviewPath(relativePath.replace(/^\/+/, ""))}${suffix}`;
24080
+ return buildProjectApiPath(
24081
+ projectId,
24082
+ `/preview/${encodePreviewPath(relativePath.replace(/^\/+/, ""))}${suffix}`
24083
+ );
24072
24084
  }
24073
24085
 
24074
24086
  // src/player/components/VideoThumbnail.tsx
@@ -26679,7 +26691,7 @@ function useCompositionStack({
26679
26691
  {
26680
26692
  id: "master",
26681
26693
  label: "Master",
26682
- previewUrl: `/api/projects/${projectId}/preview`
26694
+ previewUrl: buildProjectApiPath(projectId, `/preview`)
26683
26695
  }
26684
26696
  ]);
26685
26697
  const onCompositionChangeRef = useRef47(onCompositionChange);
@@ -26725,7 +26737,10 @@ function useCompositionStack({
26725
26737
  return prev.slice(0, -1);
26726
26738
  }
26727
26739
  const label = resolvedPath.split("/").pop()?.replace(/\.html$/, "") || resolvedPath;
26728
- const previewUrl = `/api/projects/${projectId}/preview/comp/${encodePreviewPath(resolvedPath)}`;
26740
+ const previewUrl = buildProjectApiPath(
26741
+ projectId,
26742
+ `/preview/comp/${encodePreviewPath(resolvedPath)}`
26743
+ );
26729
26744
  return [...prev, { id: resolvedPath, label, previewUrl }];
26730
26745
  });
26731
26746
  },
@@ -26736,14 +26751,17 @@ function useCompositionStack({
26736
26751
  const master = {
26737
26752
  id: "master",
26738
26753
  label: "Master",
26739
- previewUrl: `/api/projects/${projectId}/preview`
26754
+ previewUrl: buildProjectApiPath(projectId, `/preview`)
26740
26755
  };
26741
26756
  if (activeCompositionPath === "index.html") {
26742
26757
  usePlayerStore.getState().setElements([]);
26743
26758
  updateCompositionStack([master]);
26744
26759
  } else if (activeCompositionPath) {
26745
26760
  const label = activeCompositionPath.replace(/^compositions\//, "").replace(/\.html$/, "");
26746
- const previewUrl = `/api/projects/${projectId}/preview/comp/${encodePreviewPath(activeCompositionPath)}`;
26761
+ const previewUrl = buildProjectApiPath(
26762
+ projectId,
26763
+ `/preview/comp/${encodePreviewPath(activeCompositionPath)}`
26764
+ );
26747
26765
  usePlayerStore.getState().setElements([]);
26748
26766
  updateCompositionStack((prev) => {
26749
26767
  if (prev[prev.length - 1]?.id === activeCompositionPath) return prev;
@@ -26923,7 +26941,7 @@ function NLEProvider({
26923
26941
  setCompIdToSrc(emptyMap);
26924
26942
  setCompositionSourceMap(emptyMap);
26925
26943
  onCompIdToSrcChangeRef.current?.(emptyMap);
26926
- fetch(`/api/projects/${projectId}/files/index.html`, {
26944
+ fetch(buildProjectApiPath(projectId, `/files/index.html`), {
26927
26945
  signal: controller.signal
26928
26946
  }).then((r) => {
26929
26947
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
@@ -27865,6 +27883,7 @@ var CaptionOverlay = memo20(function CaptionOverlay2({ iframeRef }) {
27865
27883
  };
27866
27884
  const unsubPlayer = usePlayerStore.subscribe(scheduleTick);
27867
27885
  const handleMessage = (e) => {
27886
+ if (!e.source || e.source !== iframeRef.current?.contentWindow) return;
27868
27887
  const data = e.data;
27869
27888
  if (data?.source === "hf-preview") scheduleTick();
27870
27889
  };
@@ -28570,6 +28589,36 @@ function buildAgentContextPreview(selection, activeCompPath) {
28570
28589
  ].filter(Boolean).join("\n");
28571
28590
  }
28572
28591
 
28592
+ // src/components/editor/domEditOverlayBasis.ts
28593
+ function readPositiveDimension(value) {
28594
+ if (!value) return null;
28595
+ const parsed = Number.parseFloat(value);
28596
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
28597
+ }
28598
+ function findOverlayRootElement(doc) {
28599
+ return doc?.querySelector("[data-composition-id]") ?? doc?.documentElement ?? null;
28600
+ }
28601
+ function resolveRootDimensions(root) {
28602
+ if (!root) return null;
28603
+ const rootRect = root.getBoundingClientRect();
28604
+ const width = readPositiveDimension(root.getAttribute("data-width")) ?? rootRect.width;
28605
+ const height = readPositiveDimension(root.getAttribute("data-height")) ?? rootRect.height;
28606
+ if (!width || !height) return null;
28607
+ return { width, height };
28608
+ }
28609
+ function computeOverlayRootScale(overlayEl, iframe, doc) {
28610
+ const iframeRect = iframe.getBoundingClientRect();
28611
+ const overlayRect = overlayEl.getBoundingClientRect();
28612
+ const dims = resolveRootDimensions(findOverlayRootElement(doc));
28613
+ if (!dims) return null;
28614
+ return {
28615
+ iframeRect,
28616
+ overlayRect,
28617
+ rootScaleX: iframeRect.width / dims.width,
28618
+ rootScaleY: iframeRect.height / dims.height
28619
+ };
28620
+ }
28621
+
28573
28622
  // src/components/editor/domEditOverlayTransform.ts
28574
28623
  var COMPOSITION_ROOT_ATTR = "data-composition-id";
28575
28624
  function individualRotateDegrees(value) {
@@ -28587,19 +28636,33 @@ function individualRotateDegrees(value) {
28587
28636
  const deg = Number.parseFloat(angle);
28588
28637
  return Number.isFinite(deg) ? deg : 0;
28589
28638
  }
28590
- function composeElementTransform(element, ops, getStyle) {
28591
- let acc = ops.identity();
28639
+ function ownNodeTransform(node, ops, getStyle) {
28640
+ const style = getStyle(node);
28641
+ if (!style) return null;
28642
+ const transform = style.transform;
28643
+ const own = transform && transform !== "none" ? ops.fromTransform(transform) : ops.identity();
28644
+ if (!own) return null;
28645
+ const spin = individualRotateDegrees(style.rotate);
28646
+ return spin === 0 ? own : ops.compose(ops.fromRotate(spin), own);
28647
+ }
28648
+ function composeElementTransform(element, ops, getStyle, memo52) {
28649
+ const pending2 = [];
28650
+ let above;
28592
28651
  for (let node = element; node; node = node.parentElement) {
28593
- const style = getStyle(node);
28594
- if (!style) return null;
28595
- const transform = style.transform;
28596
- let own = transform && transform !== "none" ? ops.fromTransform(transform) : ops.identity();
28597
- if (!own) return null;
28598
- const spin = individualRotateDegrees(style.rotate);
28599
- if (spin !== 0) own = ops.compose(ops.fromRotate(spin), own);
28600
- acc = ops.compose(own, acc);
28652
+ above = memo52?.get(node);
28653
+ if (above !== void 0) break;
28654
+ pending2.push(node);
28601
28655
  if (node.hasAttribute(COMPOSITION_ROOT_ATTR)) break;
28602
28656
  }
28657
+ let acc = above === void 0 ? ops.identity() : above;
28658
+ for (let i = pending2.length - 1; i >= 0; i -= 1) {
28659
+ const node = pending2[i];
28660
+ if (acc !== null) {
28661
+ const own = ownNodeTransform(node, ops, getStyle);
28662
+ acc = own === null ? null : ops.compose(acc, own);
28663
+ }
28664
+ memo52?.set(node, acc);
28665
+ }
28603
28666
  return acc;
28604
28667
  }
28605
28668
 
@@ -28832,24 +28895,44 @@ function rotateDeltaIntoFrame(deltaX, deltaY, angleDeg) {
28832
28895
  return { deltaX: deltaX * cos - deltaY * sin, deltaY: deltaX * sin + deltaY * cos };
28833
28896
  }
28834
28897
 
28898
+ // src/components/editor/domEditOverlayMeasurePass.ts
28899
+ function createOverlayMeasurePass() {
28900
+ return {
28901
+ visible: /* @__PURE__ */ new Map(),
28902
+ transform: /* @__PURE__ */ new Map(),
28903
+ sourceBoundary: /* @__PURE__ */ new Map(),
28904
+ sourceBoundaryRect: /* @__PURE__ */ new Map()
28905
+ };
28906
+ }
28907
+ function readThroughPass(memo52, key2, compute) {
28908
+ const answered = memo52.get(key2);
28909
+ if (answered !== void 0) return answered;
28910
+ const value = compute();
28911
+ memo52.set(key2, value);
28912
+ return value;
28913
+ }
28914
+
28835
28915
  // src/components/editor/domEditOverlayGeometry.ts
28836
28916
  function isElementVisibleForOverlay(el) {
28837
28917
  return isElementVisibleThroughAncestors(el);
28838
28918
  }
28839
- function readPositiveDimension(value) {
28840
- if (!value) return null;
28841
- const parsed = Number.parseFloat(value);
28842
- return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
28843
- }
28844
- function findSourceBoundary(element) {
28845
- let current = element;
28846
- while (current) {
28847
- if (current.hasAttribute("data-composition-file") || current.hasAttribute("data-composition-src")) {
28848
- return current;
28919
+ var isSourceBoundary = (node) => node.hasAttribute("data-composition-file") || node.hasAttribute("data-composition-src");
28920
+ function findSourceBoundary(element, pass) {
28921
+ const pending2 = [];
28922
+ let boundary;
28923
+ for (let node = element; node; node = node.parentElement) {
28924
+ boundary = pass?.sourceBoundary.get(node);
28925
+ if (boundary !== void 0) break;
28926
+ if (isSourceBoundary(node)) {
28927
+ boundary = node;
28928
+ pass?.sourceBoundary.set(node, node);
28929
+ break;
28849
28930
  }
28850
- current = current.parentElement;
28931
+ pending2.push(node);
28851
28932
  }
28852
- return null;
28933
+ const answer = boundary ?? null;
28934
+ if (pass) for (const node of pending2) pass.sourceBoundary.set(node, answer);
28935
+ return answer;
28853
28936
  }
28854
28937
  function resolveDomEditCoordinateScale(input) {
28855
28938
  const rootScaleX = input.rootScaleX > 0 ? input.rootScaleX : 1;
@@ -28861,8 +28944,8 @@ function resolveDomEditCoordinateScale(input) {
28861
28944
  scaleY: sourceScaleY > 0 ? sourceScaleY : rootScaleY
28862
28945
  };
28863
28946
  }
28864
- function toVisibleOverlayRect(overlayEl, iframe, element) {
28865
- const rect = toOverlayRect(overlayEl, iframe, element);
28947
+ function toVisibleOverlayRect(overlayEl, iframe, element, precomputedScale) {
28948
+ const rect = toOverlayRect(overlayEl, iframe, element, precomputedScale);
28866
28949
  return rect ? { ...rect, ...hugRectForElement(rect, element) } : null;
28867
28950
  }
28868
28951
  function toVisibleOverlayRects(overlayEl, iframe, elements) {
@@ -28873,7 +28956,7 @@ function toVisibleOverlayRects(overlayEl, iframe, elements) {
28873
28956
  return rect ? { ...rect, ...hugRectForElement(rect, element) } : null;
28874
28957
  });
28875
28958
  }
28876
- function readElementTransformSnapshot(win, element) {
28959
+ function readElementTransformSnapshot(win, element, pass) {
28877
28960
  const DOMMatrixCtor = win.DOMMatrix;
28878
28961
  if (!DOMMatrixCtor) return null;
28879
28962
  const cs = win.getComputedStyle(element);
@@ -28887,7 +28970,8 @@ function readElementTransformSnapshot(win, element) {
28887
28970
  const matrix = composeElementTransform(
28888
28971
  element,
28889
28972
  ops,
28890
- (node) => node === element ? cs : win.getComputedStyle(node)
28973
+ (node) => node === element ? cs : win.getComputedStyle(node),
28974
+ pass?.transform
28891
28975
  );
28892
28976
  return matrix ? { matrix, cs } : null;
28893
28977
  } catch {
@@ -28906,36 +28990,17 @@ function rotationDegreesFromMatrix(matrix) {
28906
28990
  return Number.isFinite(deg) ? deg : 0;
28907
28991
  }
28908
28992
  var ROTATION_GATE_EPSILON_DEG = 1e-4;
28909
- function findOverlayRootElement(doc) {
28910
- return doc?.querySelector("[data-composition-id]") ?? doc?.documentElement ?? null;
28911
- }
28912
- function resolveRootDimensions(root) {
28913
- if (!root) return null;
28914
- const rootRect = root.getBoundingClientRect();
28915
- const width = readPositiveDimension(root.getAttribute("data-width")) ?? rootRect.width;
28916
- const height = readPositiveDimension(root.getAttribute("data-height")) ?? rootRect.height;
28917
- if (!width || !height) return null;
28918
- return { width, height };
28919
- }
28920
- function computeOverlayRootScale(overlayEl, iframe, doc) {
28921
- const iframeRect = iframe.getBoundingClientRect();
28922
- const overlayRect = overlayEl.getBoundingClientRect();
28923
- const dims = resolveRootDimensions(findOverlayRootElement(doc));
28924
- if (!dims) return null;
28925
- return {
28926
- iframeRect,
28927
- overlayRect,
28928
- rootScaleX: iframeRect.width / dims.width,
28929
- rootScaleY: iframeRect.height / dims.height
28930
- };
28931
- }
28932
- function toOverlayRect(overlayEl, iframe, element, precomputedScale) {
28993
+ function toOverlayRect(overlayEl, iframe, element, precomputedScale, pass) {
28933
28994
  const scale = precomputedScale ?? computeOverlayRootScale(overlayEl, iframe, iframe.contentDocument);
28934
28995
  if (!scale) return null;
28935
28996
  const { iframeRect, overlayRect, rootScaleX, rootScaleY } = scale;
28936
28997
  const elementRect = element.getBoundingClientRect();
28937
- const sourceBoundary = findSourceBoundary(element);
28938
- const sourceBoundaryRect = sourceBoundary?.getBoundingClientRect();
28998
+ const sourceBoundary = findSourceBoundary(element, pass);
28999
+ const sourceBoundaryRect = sourceBoundary && pass ? readThroughPass(
29000
+ pass.sourceBoundaryRect,
29001
+ sourceBoundary,
29002
+ () => sourceBoundary.getBoundingClientRect()
29003
+ ) : sourceBoundary?.getBoundingClientRect();
28939
29004
  const editScale = resolveDomEditCoordinateScale({
28940
29005
  rootScaleX,
28941
29006
  rootScaleY,
@@ -29005,13 +29070,13 @@ function elementCornerOverlayPoints(overlayEl, iframe, element, precomputedScale
29005
29070
  sw: toOverlay(relCorners.sw)
29006
29071
  };
29007
29072
  }
29008
- function orientedOverlayRect(overlayEl, iframe, element) {
29009
- const scale = computeOverlayRootScale(overlayEl, iframe, iframe.contentDocument);
29073
+ function orientedOverlayRect(overlayEl, iframe, element, precomputedScale, pass) {
29074
+ const scale = precomputedScale ?? computeOverlayRootScale(overlayEl, iframe, iframe.contentDocument);
29010
29075
  if (!scale) return null;
29011
- const base2 = toOverlayRect(overlayEl, iframe, element, scale);
29076
+ const base2 = toOverlayRect(overlayEl, iframe, element, scale, pass);
29012
29077
  if (!base2) return null;
29013
29078
  const win = iframe.contentWindow;
29014
- const transform = win ? readElementTransformSnapshot(win, element) : null;
29079
+ const transform = win ? readElementTransformSnapshot(win, element, pass) : null;
29015
29080
  const angle = transform ? rotationDegreesFromMatrix(transform.matrix) : 0;
29016
29081
  if (Math.abs(angle) < ROTATION_GATE_EPSILON_DEG) return base2;
29017
29082
  const corners = elementCornerOverlayPoints(overlayEl, iframe, element, scale, transform);
@@ -29033,8 +29098,8 @@ function orientedOverlayRect(overlayEl, iframe, element) {
29033
29098
  angle
29034
29099
  };
29035
29100
  }
29036
- function orientedVisibleOverlayRect(overlayEl, iframe, element) {
29037
- const rect = orientedOverlayRect(overlayEl, iframe, element);
29101
+ function orientedVisibleOverlayRect(overlayEl, iframe, element, precomputedScale) {
29102
+ const rect = orientedOverlayRect(overlayEl, iframe, element, precomputedScale);
29038
29103
  return rect ? { ...rect, ...hugRectForElement(rect, element) } : null;
29039
29104
  }
29040
29105
  var OVERLAY_RECT_EPSILON_PX = 0.5;
@@ -29076,20 +29141,26 @@ function resolveDomEditGroupOverlayRect(rects) {
29076
29141
  editScaleY: 1
29077
29142
  };
29078
29143
  }
29079
- function groupAwareOverlayRect(overlayEl, iframe, el) {
29080
- const rect = toOverlayRect(overlayEl, iframe, el);
29144
+ function groupAwareOverlayRect(overlayEl, iframe, el, precomputedScale, pass) {
29145
+ const rect = toOverlayRect(overlayEl, iframe, el, precomputedScale, pass);
29081
29146
  if (!rect || !el.hasAttribute("data-hf-group")) return rect;
29082
29147
  const rects = [];
29083
29148
  for (const child of Array.from(el.children)) {
29084
- const childRect = toOverlayRect(overlayEl, iframe, child);
29149
+ const childRect = toOverlayRect(
29150
+ overlayEl,
29151
+ iframe,
29152
+ child,
29153
+ precomputedScale,
29154
+ pass
29155
+ );
29085
29156
  if (childRect) rects.push(childRect);
29086
29157
  }
29087
29158
  const union = rects.length > 0 ? resolveDomEditGroupOverlayRect(rects) : null;
29088
29159
  if (!union) return rect;
29089
29160
  return { ...union, editScaleX: rect.editScaleX, editScaleY: rect.editScaleY };
29090
29161
  }
29091
- function orientedGroupAwareOverlayRect(overlayEl, iframe, el) {
29092
- return el.hasAttribute("data-hf-group") ? groupAwareOverlayRect(overlayEl, iframe, el) : orientedOverlayRect(overlayEl, iframe, el);
29162
+ function orientedGroupAwareOverlayRect(overlayEl, iframe, el, precomputedScale, pass) {
29163
+ return el.hasAttribute("data-hf-group") ? groupAwareOverlayRect(overlayEl, iframe, el, precomputedScale, pass) : orientedOverlayRect(overlayEl, iframe, el, precomputedScale, pass);
29093
29164
  }
29094
29165
  function filterNestedDomEditGroupItems(items) {
29095
29166
  return items.filter(
@@ -29437,6 +29508,125 @@ function hasDomEditRotationChanged(initialAngle, nextAngle) {
29437
29508
 
29438
29509
  // src/components/editor/useDomEditOverlayRects.ts
29439
29510
  import { useRef as useRef54, useState as useState36 } from "react";
29511
+
29512
+ // src/components/editor/overlayFrameLoop.ts
29513
+ var AWAKE_MS = 400;
29514
+ var IDLE_POLL_MS = 250;
29515
+ var subscribers = /* @__PURE__ */ new Set();
29516
+ var frameId = null;
29517
+ var idleTimerId = null;
29518
+ var awakeUntil = 0;
29519
+ var listenersAttached = false;
29520
+ var WINDOW_EVENTS = [
29521
+ "pointerdown",
29522
+ "pointermove",
29523
+ "pointerup",
29524
+ "wheel",
29525
+ "keydown",
29526
+ "keyup",
29527
+ "scroll",
29528
+ "resize",
29529
+ "visibilitychange"
29530
+ ];
29531
+ var lastPreviewFrame = null;
29532
+ var lastPreviewPlaying = null;
29533
+ function isFromPreviewOrigin(event) {
29534
+ if (typeof window === "undefined") return true;
29535
+ return event.origin === window.location.origin;
29536
+ }
29537
+ function isPreviewPayload(data) {
29538
+ if (data == null || typeof data !== "object") return false;
29539
+ return data.source === "hf-preview";
29540
+ }
29541
+ function previewStateOf(data) {
29542
+ if (data.type !== "state") return null;
29543
+ const frame = data.frame;
29544
+ return {
29545
+ frame: typeof frame === "number" ? frame : null,
29546
+ playing: data.isPlaying === true
29547
+ };
29548
+ }
29549
+ function onPreviewMessage(event) {
29550
+ if (!isFromPreviewOrigin(event)) return;
29551
+ const data = event.data;
29552
+ if (!isPreviewPayload(data)) return;
29553
+ const state = previewStateOf(data);
29554
+ if (state) {
29555
+ if (state.frame === lastPreviewFrame && state.playing === lastPreviewPlaying) return;
29556
+ lastPreviewFrame = state.frame;
29557
+ lastPreviewPlaying = state.playing;
29558
+ }
29559
+ requestOverlayFrames();
29560
+ }
29561
+ function runFrame() {
29562
+ frameId = null;
29563
+ schedule();
29564
+ for (const subscriber of subscribers) {
29565
+ try {
29566
+ subscriber();
29567
+ } catch (error) {
29568
+ queueMicrotask(() => {
29569
+ throw error;
29570
+ });
29571
+ }
29572
+ }
29573
+ }
29574
+ function schedule() {
29575
+ if (subscribers.size === 0) return;
29576
+ if (frameId != null || idleTimerId != null) return;
29577
+ if (performance.now() < awakeUntil) {
29578
+ frameId = requestAnimationFrame(runFrame);
29579
+ return;
29580
+ }
29581
+ idleTimerId = setTimeout(() => {
29582
+ idleTimerId = null;
29583
+ frameId = requestAnimationFrame(runFrame);
29584
+ }, IDLE_POLL_MS);
29585
+ }
29586
+ function requestOverlayFrames() {
29587
+ awakeUntil = performance.now() + AWAKE_MS;
29588
+ if (idleTimerId != null) {
29589
+ clearTimeout(idleTimerId);
29590
+ idleTimerId = null;
29591
+ }
29592
+ schedule();
29593
+ }
29594
+ function attachListeners() {
29595
+ if (listenersAttached || typeof window === "undefined") return;
29596
+ listenersAttached = true;
29597
+ for (const type of WINDOW_EVENTS) {
29598
+ window.addEventListener(type, requestOverlayFrames, { capture: true, passive: true });
29599
+ }
29600
+ window.addEventListener("message", onPreviewMessage, { capture: true, passive: true });
29601
+ }
29602
+ function detachListeners() {
29603
+ if (!listenersAttached || typeof window === "undefined") return;
29604
+ listenersAttached = false;
29605
+ for (const type of WINDOW_EVENTS) {
29606
+ window.removeEventListener(type, requestOverlayFrames, { capture: true });
29607
+ }
29608
+ window.removeEventListener("message", onPreviewMessage, { capture: true });
29609
+ }
29610
+ function subscribeOverlayFrame(update) {
29611
+ subscribers.add(update);
29612
+ attachListeners();
29613
+ requestOverlayFrames();
29614
+ return () => {
29615
+ subscribers.delete(update);
29616
+ if (subscribers.size > 0) return;
29617
+ if (frameId != null) {
29618
+ cancelAnimationFrame(frameId);
29619
+ frameId = null;
29620
+ }
29621
+ if (idleTimerId != null) {
29622
+ clearTimeout(idleTimerId);
29623
+ idleTimerId = null;
29624
+ }
29625
+ detachListeners();
29626
+ };
29627
+ }
29628
+
29629
+ // src/components/editor/useDomEditOverlayRects.ts
29440
29630
  function childRectsEqual(a, b) {
29441
29631
  if (a.length !== b.length) return false;
29442
29632
  for (let i = 0; i < a.length; i++) {
@@ -29492,14 +29682,12 @@ function useDomEditOverlayRects({
29492
29682
  return next;
29493
29683
  };
29494
29684
  useMountEffect(() => {
29495
- let frame = 0;
29496
29685
  const clearAll = () => {
29497
29686
  setOverlayRect(null);
29498
29687
  setHoverRect(null);
29499
29688
  setGroupOverlayItems([]);
29500
29689
  };
29501
29690
  const update = () => {
29502
- frame = requestAnimationFrame(update);
29503
29691
  if (rafPausedRef.current) {
29504
29692
  if (childRectsRef.current.length > 0) {
29505
29693
  childRectsRef.current = [];
@@ -29525,6 +29713,7 @@ function useDomEditOverlayRects({
29525
29713
  clearAll();
29526
29714
  return;
29527
29715
  }
29716
+ const scale = computeOverlayRootScale(overlayEl, iframe, doc);
29528
29717
  if (sel) {
29529
29718
  const el = resolveElementForOverlay(
29530
29719
  doc,
@@ -29533,7 +29722,7 @@ function useDomEditOverlayRects({
29533
29722
  resolvedElementRef
29534
29723
  );
29535
29724
  if (el && isElementVisibleForOverlay(el)) {
29536
- const nextRect = orientedGroupAwareOverlayRect(overlayEl, iframe, el);
29725
+ const nextRect = orientedGroupAwareOverlayRect(overlayEl, iframe, el, scale);
29537
29726
  setOverlayRect(nextRect);
29538
29727
  const descendants = el.querySelectorAll("*");
29539
29728
  if (descendants.length > 0 && descendants.length <= 60) {
@@ -29541,7 +29730,7 @@ function useDomEditOverlayRects({
29541
29730
  for (let i = 0; i < descendants.length; i++) {
29542
29731
  const child = descendants[i];
29543
29732
  if (!child.getBoundingClientRect) continue;
29544
- const r = orientedVisibleOverlayRect(overlayEl, iframe, child);
29733
+ const r = orientedVisibleOverlayRect(overlayEl, iframe, child, scale);
29545
29734
  if (r && r.width > 2 && r.height > 2) nextChildRects.push(r);
29546
29735
  }
29547
29736
  if (!childRectsEqual(childRectsRef.current, nextChildRects)) {
@@ -29576,7 +29765,7 @@ function useDomEditOverlayRects({
29576
29765
  if (liveGroupKeys.has(key2)) continue;
29577
29766
  liveGroupKeys.add(key2);
29578
29767
  const el = resolveGroupElement(doc, groupSelection);
29579
- const base2 = el ? groupAwareOverlayRect(overlayEl, iframe, el) : null;
29768
+ const base2 = el ? groupAwareOverlayRect(overlayEl, iframe, el, scale) : null;
29580
29769
  const rect = base2 && el ? { ...base2, ...hugRectForElement(base2, el) } : base2;
29581
29770
  if (el && rect)
29582
29771
  nextGroupItems.push({ key: key2, selection: groupSelection, element: el, rect });
@@ -29611,10 +29800,9 @@ function useDomEditOverlayRects({
29611
29800
  setHoverRect(null);
29612
29801
  return;
29613
29802
  }
29614
- setHoverRect(orientedGroupAwareOverlayRect(overlayEl, iframe, hoverEl));
29803
+ setHoverRect(orientedGroupAwareOverlayRect(overlayEl, iframe, hoverEl, scale));
29615
29804
  };
29616
- frame = requestAnimationFrame(update);
29617
- return () => cancelAnimationFrame(frame);
29805
+ return subscribeOverlayFrame(update);
29618
29806
  });
29619
29807
  return {
29620
29808
  overlayRect,
@@ -29692,7 +29880,7 @@ function OffCanvasIndicators({
29692
29880
  const selectOffCanvas = async () => {
29693
29881
  const el = elements.current.get(r.key);
29694
29882
  if (!el) return;
29695
- const { resolveDomEditSelection: resolveDomEditSelection2 } = await import("./domEditingLayers-JMMVOGTO.js");
29883
+ const { resolveDomEditSelection: resolveDomEditSelection2 } = await import("./domEditingLayers-UKMVYXT6.js");
29696
29884
  const acp = activeCompositionPathRef.current ?? "index.html";
29697
29885
  const sel = await resolveDomEditSelection2(el, {
29698
29886
  activeCompositionPath: acp,
@@ -32553,9 +32741,7 @@ var SnapGuideOverlay = memo21(function SnapGuideOverlay2({
32553
32741
  height: compositionHeight
32554
32742
  };
32555
32743
  useMountEffect(() => {
32556
- let frame = 0;
32557
32744
  const update = () => {
32558
- frame = requestAnimationFrame(update);
32559
32745
  const state = snapGuidesRef.current;
32560
32746
  const guides = state?.guides ?? [];
32561
32747
  const spacingGuides = state?.spacingGuides ?? [];
@@ -32611,8 +32797,7 @@ var SnapGuideOverlay = memo21(function SnapGuideOverlay2({
32611
32797
  }
32612
32798
  }
32613
32799
  };
32614
- frame = requestAnimationFrame(update);
32615
- return () => cancelAnimationFrame(frame);
32800
+ return subscribeOverlayFrame(update);
32616
32801
  });
32617
32802
  return /* @__PURE__ */ jsxs56("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-0", children: [
32618
32803
  Array.from({ length: MAX_GUIDES }, (_, i) => /* @__PURE__ */ jsx73(
@@ -33269,9 +33454,7 @@ function useDomEditCompositionRect({
33269
33454
  scaleY: 1
33270
33455
  });
33271
33456
  useMountEffect(() => {
33272
- let frame = 0;
33273
33457
  const update = () => {
33274
- frame = requestAnimationFrame(update);
33275
33458
  const iframe = iframeRef.current;
33276
33459
  const overlayEl = overlayRef.current;
33277
33460
  if (!iframe || !overlayEl) return;
@@ -33289,8 +33472,7 @@ function useDomEditCompositionRect({
33289
33472
  const next = { left, top, width: iRect.width, height: iRect.height, scaleX, scaleY };
33290
33473
  setCompRect((prev) => sameRect(prev, next) ? prev : next);
33291
33474
  };
33292
- frame = requestAnimationFrame(update);
33293
- return () => cancelAnimationFrame(frame);
33475
+ return subscribeOverlayFrame(update);
33294
33476
  });
33295
33477
  return compRect;
33296
33478
  }
@@ -33312,7 +33494,12 @@ function extendsOutside(rect, comp) {
33312
33494
  const centerY = rect.top + rect.height / 2;
33313
33495
  return centerX - halfWidth < comp.left || centerX + halfWidth > comp.left + comp.width || centerY - halfHeight < comp.top || centerY + halfHeight > comp.top + comp.height;
33314
33496
  }
33315
- function recomputeOffCanvasIndicators(iframe, overlay, doc, comp, activeCompositionPath, sigRef, elementsRef, setRects) {
33497
+ function measureItemRect(overlay, iframe, element, scale, pass) {
33498
+ if (!isElementComputedVisible(element, pass.visible)) return null;
33499
+ const base2 = orientedGroupAwareOverlayRect(overlay, iframe, element, scale, pass);
33500
+ return base2 ? { ...base2, ...hugRectForElement(base2, element) } : null;
33501
+ }
33502
+ function recomputeOffCanvasIndicators(iframe, overlay, doc, comp, activeCompositionPath, sigRef, elementsRef, setRects, walkCache) {
33316
33503
  if (comp.width <= 0 || !doc) {
33317
33504
  sigRef.current = "";
33318
33505
  elementsRef.current = /* @__PURE__ */ new Map();
@@ -33321,16 +33508,18 @@ function recomputeOffCanvasIndicators(iframe, overlay, doc, comp, activeComposit
33321
33508
  }
33322
33509
  const root = doc.querySelector("[data-composition-id]") ?? doc.body;
33323
33510
  const acp = activeCompositionPath ?? "index.html";
33324
- const items = collectDomEditLayerItems(root, {
33325
- activeCompositionPath: acp,
33326
- isMasterView: !acp || acp === "index.html"
33327
- });
33511
+ const items = collectDomEditLayerItems(
33512
+ root,
33513
+ { activeCompositionPath: acp, isMasterView: !acp || acp === "index.html" },
33514
+ void 0,
33515
+ walkCache
33516
+ );
33517
+ const scale = computeOverlayRootScale(overlay, iframe, doc);
33518
+ const pass = createOverlayMeasurePass();
33328
33519
  const rects = [];
33329
33520
  const elMap = /* @__PURE__ */ new Map();
33330
33521
  for (const item of items) {
33331
- if (!isElementComputedVisible(item.element)) continue;
33332
- const base2 = orientedGroupAwareOverlayRect(overlay, iframe, item.element);
33333
- const r = base2 ? { ...base2, ...hugRectForElement(base2, item.element) } : null;
33522
+ const r = measureItemRect(overlay, iframe, item.element, scale, pass);
33334
33523
  if (!r) continue;
33335
33524
  if (extendsOutside(r, comp)) {
33336
33525
  rects.push({
@@ -33361,20 +33550,14 @@ function clearIndicators(options) {
33361
33550
  options.elementsRef.current = /* @__PURE__ */ new Map();
33362
33551
  options.setRects([]);
33363
33552
  }
33364
- function observeDoc(doc, markDirty) {
33553
+ function observeDoc(doc, cache2, markDirty) {
33365
33554
  const Observer = doc.defaultView?.MutationObserver ?? globalThis.MutationObserver;
33366
33555
  if (!Observer) return null;
33367
- const observer = new Observer(markDirty);
33368
- observer.observe(doc.documentElement, {
33369
- attributes: true,
33370
- // data-hidden is included explicitly: hiding an element writes data-hidden, and
33371
- // although the runtime honoring also writes display:"none" (a style mutation we'd
33372
- // catch anyway), keying on the attribute directly makes the coupling robust to any
33373
- // future throttling of that runtime sync.
33374
- attributeFilter: ["style", "class", "transform", "width", "height", "data-hidden"],
33375
- childList: true,
33376
- subtree: true
33556
+ const observer = new Observer((records) => {
33557
+ cache2.ingest(records);
33558
+ markDirty();
33377
33559
  });
33560
+ observer.observe(doc.documentElement, DOM_EDIT_LAYER_OBSERVER_INIT);
33378
33561
  return observer;
33379
33562
  }
33380
33563
  var RECOMPUTE_INTERVAL_MS = 100;
@@ -33382,20 +33565,21 @@ function rebuildDue(dirty, lastAt, now2) {
33382
33565
  return dirty && now2 - lastAt >= RECOMPUTE_INTERVAL_MS;
33383
33566
  }
33384
33567
  function startOffCanvasIndicatorRefresh(options) {
33385
- let frame = 0;
33386
33568
  let lastCompSig = "";
33387
33569
  let lastRecomputeAt = Number.NEGATIVE_INFINITY;
33570
+ const walkCache = createDomEditLayerWalkCache();
33388
33571
  const markDirty = () => {
33389
33572
  options.dirtyRef.current = true;
33573
+ requestOverlayFrames();
33390
33574
  };
33391
33575
  const attachObserver = (doc) => {
33392
33576
  options.observerRef.current?.disconnect();
33393
- options.observerRef.current = doc?.documentElement ? observeDoc(doc, markDirty) : null;
33577
+ walkCache.invalidateAll();
33578
+ options.observerRef.current = doc?.documentElement ? observeDoc(doc, walkCache, markDirty) : null;
33394
33579
  options.observedDocRef.current = doc;
33395
33580
  options.sigRef.current = "";
33396
33581
  };
33397
33582
  const update = () => {
33398
- frame = requestAnimationFrame(update);
33399
33583
  const iframe = options.iframeRef.current;
33400
33584
  const overlayEl = options.overlayRef.current;
33401
33585
  const doc = iframe?.contentDocument ?? null;
@@ -33417,6 +33601,7 @@ function startOffCanvasIndicatorRefresh(options) {
33417
33601
  if (!rebuildDue(options.dirtyRef.current, lastRecomputeAt, now2)) return;
33418
33602
  lastRecomputeAt = now2;
33419
33603
  options.dirtyRef.current = false;
33604
+ drainPendingLayerMutations(options.observerRef.current, walkCache);
33420
33605
  recomputeOffCanvasIndicators(
33421
33606
  iframe,
33422
33607
  overlayEl,
@@ -33425,12 +33610,13 @@ function startOffCanvasIndicatorRefresh(options) {
33425
33610
  options.activeCompositionPathRef.current,
33426
33611
  options.sigRef,
33427
33612
  options.elementsRef,
33428
- options.setRects
33613
+ options.setRects,
33614
+ walkCache
33429
33615
  );
33430
33616
  };
33431
- frame = requestAnimationFrame(update);
33617
+ const unsubscribe = subscribeOverlayFrame(update);
33432
33618
  return () => {
33433
- cancelAnimationFrame(frame);
33619
+ unsubscribe();
33434
33620
  options.observerRef.current?.disconnect();
33435
33621
  options.observerRef.current = null;
33436
33622
  options.observedDocRef.current = null;
@@ -37066,7 +37252,6 @@ function useMotionPathData(iframeRef, selector) {
37066
37252
  return;
37067
37253
  }
37068
37254
  setHome(null);
37069
- let raf = 0;
37070
37255
  const tick = () => {
37071
37256
  const el = iframeRef.current;
37072
37257
  if (el) {
@@ -37097,10 +37282,8 @@ function useMotionPathData(iframeRef, selector) {
37097
37282
  setPScale((p) => Math.abs(p - ps) < 1e-3 ? p : ps);
37098
37283
  }
37099
37284
  }
37100
- raf = requestAnimationFrame(tick);
37101
37285
  };
37102
- raf = requestAnimationFrame(tick);
37103
- return () => cancelAnimationFrame(raf);
37286
+ return subscribeOverlayFrame(tick);
37104
37287
  }, [selector, iframeRef]);
37105
37288
  useEffect44(() => {
37106
37289
  if (!selector) {
@@ -37703,12 +37886,13 @@ function readPositiveDimensions(width, height) {
37703
37886
  if (parsedWidth === null || parsedHeight === null) return null;
37704
37887
  return { width: parsedWidth, height: parsedHeight };
37705
37888
  }
37706
- function useCompositionDimensions() {
37889
+ function useCompositionDimensions(iframeRef) {
37707
37890
  const [compositionDimensions, setCompositionDimensions] = useState46(
37708
37891
  null
37709
37892
  );
37710
37893
  useMountEffect(() => {
37711
37894
  const handleMessage = (e) => {
37895
+ if (!e.source || e.source !== iframeRef.current?.contentWindow) return;
37712
37896
  const dimensions = readCompositionSizeMessage(e.data);
37713
37897
  if (!dimensions) return;
37714
37898
  setCompositionDimensions(
@@ -39169,7 +39353,10 @@ function useElementLifecycleOps({
39169
39353
  }
39170
39354
  domEditSaveTimestampRef.current = Date.now();
39171
39355
  const removeResponse = await fetch(
39172
- `/api/projects/${pid}/file-mutations/remove-elements/${encodeURIComponent(targetPath)}`,
39356
+ buildProjectApiPath(
39357
+ pid,
39358
+ `/file-mutations/remove-elements/${encodeURIComponent(targetPath)}`
39359
+ ),
39173
39360
  {
39174
39361
  method: "POST",
39175
39362
  headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
@@ -39647,7 +39834,7 @@ function PreviewOverlays({
39647
39834
  }) {
39648
39835
  const { activeCompPath, previewIframeRef } = useStudioShellContext();
39649
39836
  const { captionEditMode, compositionLoading, isPlaying } = useStudioPlaybackContext();
39650
- const compositionDimensions = useCompositionDimensions();
39837
+ const compositionDimensions = useCompositionDimensions(previewIframeRef);
39651
39838
  const captionModelPresent = useCaptionStore((state) => state.model !== null);
39652
39839
  const captionDismissed = useCaptionStore((state) => state.dismissed);
39653
39840
  const captionSyncError = useCaptionStore((state) => state.syncError);
@@ -41796,52 +41983,6 @@ var STUDIO_SEEK_DESCRIPTION = [
41796
41983
  "Returns `ok: true`, or `ok: false` with `kind`, `reason` and a `hint`."
41797
41984
  ].join(" ");
41798
41985
 
41799
- // src/utils/projectRouting.ts
41800
- var PROJECT_HASH_PREFIX = "#project/";
41801
- function decodeHashProjectId(value) {
41802
- try {
41803
- return decodeURIComponent(value);
41804
- } catch {
41805
- return value;
41806
- }
41807
- }
41808
- function normalizeHashParams(params) {
41809
- if (!params) return new URLSearchParams();
41810
- if (params instanceof URLSearchParams) return params;
41811
- const next = new URLSearchParams();
41812
- for (const [key2, value] of Object.entries(params)) {
41813
- if (!key2 || value == null || value === "") continue;
41814
- next.set(key2, value);
41815
- }
41816
- return next;
41817
- }
41818
- function encodeProjectId(projectId) {
41819
- return encodeURIComponent(projectId);
41820
- }
41821
- function buildProjectHash(projectId, params) {
41822
- const search = normalizeHashParams(params).toString();
41823
- return `${PROJECT_HASH_PREFIX}${encodeProjectId(projectId)}${search ? `?${search}` : ""}`;
41824
- }
41825
- function parseProjectHashRoute(hash) {
41826
- if (!hash.startsWith(PROJECT_HASH_PREFIX)) return null;
41827
- const route = hash.slice(PROJECT_HASH_PREFIX.length);
41828
- const queryIndex = route.indexOf("?");
41829
- const encodedProjectId = queryIndex >= 0 ? route.slice(0, queryIndex) : route;
41830
- if (!encodedProjectId || encodedProjectId.includes("/")) return null;
41831
- const rawParams = queryIndex >= 0 ? route.slice(queryIndex + 1) : "";
41832
- return {
41833
- projectId: decodeHashProjectId(encodedProjectId),
41834
- params: new URLSearchParams(rawParams)
41835
- };
41836
- }
41837
- function parseProjectIdFromHash(hash) {
41838
- return parseProjectHashRoute(hash)?.projectId ?? null;
41839
- }
41840
- function buildProjectApiPath(projectId, suffix = "") {
41841
- const normalizedSuffix = suffix && !suffix.startsWith("/") ? `/${suffix}` : suffix;
41842
- return `/api/projects/${encodeProjectId(projectId)}${normalizedSuffix}`;
41843
- }
41844
-
41845
41986
  // src/utils/frameCapture.ts
41846
41987
  function normalizeCompositionPath(compositionPath) {
41847
41988
  return compositionPath && compositionPath !== "master" ? compositionPath : "index.html";
@@ -48645,7 +48786,7 @@ function ImageFillField({
48645
48786
  selectedAsset && /* @__PURE__ */ jsx104("div", { className: "overflow-hidden rounded-xl border border-neutral-800 bg-neutral-900/80", children: /* @__PURE__ */ jsx104(
48646
48787
  "img",
48647
48788
  {
48648
- src: `/api/projects/${projectId}/preview/${selectedAsset}`,
48789
+ src: buildProjectApiPath(projectId, `/preview/${selectedAsset}`),
48649
48790
  alt: selectedAsset.split("/").pop() ?? selectedAsset,
48650
48791
  className: "h-28 w-full object-contain bg-neutral-950/80"
48651
48792
  }
@@ -62863,7 +63004,7 @@ function useRenderQueue(projectId, activeCompPathRef) {
62863
63004
  const loadRenders = useCallback80(async () => {
62864
63005
  if (!projectId) return;
62865
63006
  try {
62866
- const res = await fetch(`/api/projects/${projectId}/renders`);
63007
+ const res = await fetch(buildProjectApiPath(projectId, `/renders`));
62867
63008
  if (!res.ok) {
62868
63009
  setLoadError(`Couldn't load render history (server error ${res.status}).`);
62869
63010
  return;
@@ -62950,7 +63091,7 @@ function useRenderQueue(projectId, activeCompPathRef) {
62950
63091
  }
62951
63092
  let res;
62952
63093
  try {
62953
- res = await fetch(`/api/projects/${projectId}/render`, {
63094
+ res = await fetch(buildProjectApiPath(projectId, `/render`), {
62954
63095
  method: "POST",
62955
63096
  headers: { "Content-Type": "application/json" },
62956
63097
  body: JSON.stringify(body)
@@ -63600,7 +63741,9 @@ async function captureProjectProvenance(projectId, files, compositions) {
63600
63741
  };
63601
63742
  if (!scaffolded) return;
63602
63743
  try {
63603
- const res = await fetch(`/api/projects/${projectId}/files/${encodeURIComponent(CONFIG_FILE)}`);
63744
+ const res = await fetch(
63745
+ buildProjectApiPath(projectId, `/files/${encodeURIComponent(CONFIG_FILE)}`)
63746
+ );
63604
63747
  if (!res.ok) return;
63605
63748
  const envelope = await res.json();
63606
63749
  const raw = envelope.content;
@@ -64276,7 +64419,7 @@ function useCaptionSync(projectId) {
64276
64419
  }
64277
64420
  const seqAtSave = editSeqRef.current;
64278
64421
  const overrides = buildOverrides(state.model);
64279
- fetch(`/api/projects/${pid}/files/${encodeURIComponent("caption-overrides.json")}`, {
64422
+ fetch(buildProjectApiPath(pid, `/files/${encodeURIComponent("caption-overrides.json")}`), {
64280
64423
  method: "PUT",
64281
64424
  headers: { "Content-Type": "text/plain", ...studioWriteHeaders() },
64282
64425
  body: JSON.stringify(overrides, null, 2)
@@ -64334,7 +64477,7 @@ function useCaptionSync(projectId) {
64334
64477
  let data;
64335
64478
  try {
64336
64479
  const res = await fetch(
64337
- `/api/projects/${pid}/files/${encodeURIComponent("caption-overrides.json")}`
64480
+ buildProjectApiPath(pid, `/files/${encodeURIComponent("caption-overrides.json")}`)
64338
64481
  );
64339
64482
  if (!res.ok) return;
64340
64483
  data = await res.json();
@@ -65026,7 +65169,7 @@ function useFileTree({ projectId, projectIdRef }) {
65026
65169
  }
65027
65170
  let cancelled = false;
65028
65171
  setFileTreeLoaded(false);
65029
- fetch(`/api/projects/${projectId}`).then((r) => r.json()).then((data) => {
65172
+ fetch(buildProjectApiPath(projectId)).then((r) => r.json()).then((data) => {
65030
65173
  if (cancelled) return;
65031
65174
  if (data.files) setFileTree(data.files);
65032
65175
  if (data.compositions) setCompositionPaths(data.compositions);
@@ -65044,7 +65187,7 @@ function useFileTree({ projectId, projectIdRef }) {
65044
65187
  const refreshFileTree = useCallback86(async () => {
65045
65188
  const pid = projectIdRef.current;
65046
65189
  if (!pid) return;
65047
- const res = await fetch(`/api/projects/${pid}`);
65190
+ const res = await fetch(buildProjectApiPath(pid));
65048
65191
  const data = await res.json();
65049
65192
  if (data.files) setFileTree(data.files);
65050
65193
  }, [projectIdRef]);
@@ -65054,10 +65197,10 @@ function useFileTree({ projectId, projectIdRef }) {
65054
65197
  [fileTree]
65055
65198
  );
65056
65199
  const fontAssets = useMemo58(
65057
- () => assets.filter((asset) => FONT_EXT.test(asset)).map((asset) => ({
65200
+ () => (projectId ? assets : []).filter((asset) => FONT_EXT.test(asset)).map((asset) => ({
65058
65201
  family: fontFamilyFromAssetPath(asset),
65059
65202
  path: asset,
65060
- url: `/api/projects/${projectId}/preview/${asset}`
65203
+ url: projectId ? buildProjectApiPath(projectId, `/preview/${asset}`) : ""
65061
65204
  })),
65062
65205
  [assets, projectId]
65063
65206
  );
@@ -66760,7 +66903,10 @@ function useTimelineDeleteOps({
66760
66903
  throw new Error(`Timeline element ${target.id} is missing a patchable target`);
66761
66904
  }
66762
66905
  const removeResponse = await fetch(
66763
- `/api/projects/${pid}/file-mutations/remove-element/${encodeURIComponent(targetPath)}`,
66906
+ buildProjectApiPath(
66907
+ pid,
66908
+ `/file-mutations/remove-element/${encodeURIComponent(targetPath)}`
66909
+ ),
66764
66910
  {
66765
66911
  method: "POST",
66766
66912
  headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
@@ -68070,7 +68216,7 @@ function useAskAgentModal({
68070
68216
  const targetPath = selection.sourceFile || activeCompPath || "index.html";
68071
68217
  try {
68072
68218
  const response = await fetch(
68073
- `/api/projects/${pid}/files/${encodeURIComponent(targetPath)}`
68219
+ buildProjectApiPath(pid, `/files/${encodeURIComponent(targetPath)}`)
68074
68220
  );
68075
68221
  if (!response.ok) return;
68076
68222
  const data = await response.json();
@@ -70362,11 +70508,11 @@ function useDomEditCommits({
70362
70508
  const asset = fileTree.find(
70363
70509
  (path) => FONT_EXT.test(path) && fontFamilyFromAssetPath(path).toLowerCase() === family.toLowerCase()
70364
70510
  );
70365
- if (!asset) return null;
70511
+ if (!asset || !projectId) return null;
70366
70512
  return {
70367
70513
  family: fontFamilyFromAssetPath(asset),
70368
70514
  path: asset,
70369
- url: `/api/projects/${projectId}/preview/${asset}`
70515
+ url: buildProjectApiPath(projectId, `/preview/${asset}`)
70370
70516
  };
70371
70517
  },
70372
70518
  [fileTree, projectId, importedFontAssetsRef]
@@ -70386,7 +70532,7 @@ function useDomEditCommits({
70386
70532
  return result;
70387
70533
  };
70388
70534
  const readResponse = await fetch(
70389
- `/api/projects/${pid}/files/${encodeURIComponent(targetPath)}`
70535
+ buildProjectApiPath(pid, `/files/${encodeURIComponent(targetPath)}`)
70390
70536
  );
70391
70537
  if (!readResponse.ok) {
70392
70538
  throw await createStudioSaveHttpError(readResponse, `Failed to read ${targetPath}`);
@@ -70426,7 +70572,7 @@ function useDomEditCommits({
70426
70572
  }
70427
70573
  domEditSaveTimestampRef.current = Date.now();
70428
70574
  const patchResponse = await fetch(
70429
- `/api/projects/${pid}/file-mutations/patch-element/${encodeURIComponent(targetPath)}`,
70575
+ buildProjectApiPath(pid, `/file-mutations/patch-element/${encodeURIComponent(targetPath)}`),
70430
70576
  {
70431
70577
  method: "POST",
70432
70578
  headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
@@ -70691,7 +70837,7 @@ async function commitStructuralMutation(pid, targetPath, route, body, label, dep
70691
70837
  const originalContent = await readProjectFileContent(pid, targetPath);
70692
70838
  deps.domEditSaveTimestampRef.current = Date.now();
70693
70839
  const mutateResponse = await fetch(
70694
- `/api/projects/${pid}/file-mutations/${route}/${encodeURIComponent(targetPath)}`,
70840
+ buildProjectApiPath(pid, `/file-mutations/${route}/${encodeURIComponent(targetPath)}`),
70695
70841
  {
70696
70842
  method: "POST",
70697
70843
  headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
@@ -75244,7 +75390,7 @@ async function installRegistryItem({
75244
75390
  blockName,
75245
75391
  showToast
75246
75392
  }) {
75247
- const response = await fetch(`/api/projects/${projectId}/registry/install`, {
75393
+ const response = await fetch(buildProjectApiPath(projectId, `/registry/install`), {
75248
75394
  method: "POST",
75249
75395
  headers: { "Content-Type": "application/json" },
75250
75396
  body: JSON.stringify({ blockName })
@@ -76443,7 +76589,7 @@ function useCaptionDetection({
76443
76589
  if (!captionSrcPath) return;
76444
76590
  activating = true;
76445
76591
  const srcPath = captionSrcPath;
76446
- fetch(`/api/projects/${projectId}/files/${encodeURIComponent(srcPath)}`).then((r) => r.json()).then((data) => {
76592
+ fetch(buildProjectApiPath(projectId, `/files/${encodeURIComponent(srcPath)}`)).then((r) => r.json()).then((data) => {
76447
76593
  if (!data.content || !doc || !win || useCaptionStore.getState().isEditMode) return;
76448
76594
  const root = doc.querySelector("[data-composition-id]");
76449
76595
  const w = parseInt(root?.getAttribute("data-width") ?? "1920", 10);
@@ -76462,6 +76608,7 @@ function useCaptionDetection({
76462
76608
  });
76463
76609
  };
76464
76610
  const handleMessage = (e) => {
76611
+ if (!e.source || e.source !== previewIframeRef.current?.contentWindow) return;
76465
76612
  const data = e.data;
76466
76613
  if (data?.source === "hf-preview" && (data?.type === "state" || data?.type === "timeline")) {
76467
76614
  if (!acceptStudioRuntimeMessage(data)) return;
@@ -76753,7 +76900,7 @@ var ImageThumbnail = memo37(function ImageThumbnail2({
76753
76900
  function normalizeCompositionSrc(compSrc, projectId, origin) {
76754
76901
  try {
76755
76902
  const parsed = new URL(compSrc, origin);
76756
- const previewPrefix = `/api/projects/${projectId}/preview/`;
76903
+ const previewPrefix = buildProjectApiPath(projectId, `/preview/`);
76757
76904
  if (parsed.pathname.startsWith(previewPrefix)) {
76758
76905
  return parsed.pathname.slice(previewPrefix.length);
76759
76906
  }
@@ -76765,7 +76912,7 @@ function resolvePreviewRelative(src, pid, origin) {
76765
76912
  if (!src) return null;
76766
76913
  try {
76767
76914
  const parsed = new URL(src, origin);
76768
- const base2 = new URL(`/api/projects/${pid}/preview/`, origin).pathname;
76915
+ const base2 = new URL(buildProjectApiPath(pid, `/preview/`), origin).pathname;
76769
76916
  return parsed.pathname.startsWith(base2) ? decodeURIComponent(parsed.pathname.slice(base2.length)) : null;
76770
76917
  } catch {
76771
76918
  return null;
@@ -76784,7 +76931,7 @@ function renderAudioClip(el, pid, sessionEpoch, labelColor, context) {
76784
76931
  const audioUrl = resolveMediaPreviewUrl(el.src ?? "", pid, window.location.origin);
76785
76932
  const srcRelative = resolvePreviewRelative(audioUrl, pid, window.location.origin);
76786
76933
  const encodedRelative = srcRelative ? encodePreviewPath(srcRelative) : null;
76787
- const waveformUrl = encodedRelative ? `/api/projects/${pid}/waveform/${encodedRelative}` : void 0;
76934
+ const waveformUrl = encodedRelative ? buildProjectApiPath(pid, `/waveform/${encodedRelative}`) : void 0;
76788
76935
  const { start, end } = trimFractions(el);
76789
76936
  return createElement3(AudioWaveform, {
76790
76937
  audioUrl,
@@ -76827,7 +76974,7 @@ function useRenderClipContent({
76827
76974
  }
76828
76975
  if (compSrc) {
76829
76976
  return createElement3(CompositionThumbnail, {
76830
- previewUrl: `/api/projects/${pid}/preview/comp/${encodePreviewPath(compSrc)}`,
76977
+ previewUrl: buildProjectApiPath(pid, `/preview/comp/${encodePreviewPath(compSrc)}`),
76831
76978
  label: "",
76832
76979
  labelColor: style.label,
76833
76980
  seekTime: 0,
@@ -76887,7 +77034,7 @@ function useRenderClipContent({
76887
77034
  }
76888
77035
  if (htmlPreviewEligible) {
76889
77036
  return createElement3(CompositionThumbnail, {
76890
- previewUrl: `/api/projects/${pid}/preview`,
77037
+ previewUrl: buildProjectApiPath(pid, `/preview`),
76891
77038
  label: "",
76892
77039
  labelColor: style.label,
76893
77040
  selector: el.selector,
@@ -77103,7 +77250,7 @@ function useLintModal(projectId, refreshKey) {
77103
77250
  if (!projectId) return;
77104
77251
  if (!opts?.background) setLinting(true);
77105
77252
  try {
77106
- const res = await fetch(`/api/projects/${projectId}/lint`);
77253
+ const res = await fetch(buildProjectApiPath(projectId, `/lint`));
77107
77254
  const data = await res.json();
77108
77255
  const parsed = (data.findings ?? []).map(parseFinding);
77109
77256
  if (opts?.background) {
@@ -77238,9 +77385,10 @@ function useCompositionContentLoader({
77238
77385
  }) {
77239
77386
  return useCallback137(
77240
77387
  (comp) => {
77388
+ if (!projectId) return;
77241
77389
  setActiveCompPath(comp.endsWith(".html") ? comp : null);
77242
77390
  setEditingFile({ path: comp, content: null });
77243
- fetch(`/api/projects/${projectId}/files/${comp}`).then(async (r) => {
77391
+ fetch(buildProjectApiPath(projectId, `/files/${encodeURIComponent(comp)}`)).then(async (r) => {
77244
77392
  if (!r.ok) throw new Error(`Failed to load ${comp} (${r.status})`);
77245
77393
  return r.json();
77246
77394
  }).then((data) => {
@@ -79314,7 +79462,7 @@ function CompCard({
79314
79462
  setLivePreviewLoaded(false);
79315
79463
  };
79316
79464
  const name = comp.replace(/^compositions\//, "").replace(/\.html$/, "");
79317
- const previewUrl = `/api/projects/${projectId}/preview/comp/${comp}`;
79465
+ const previewUrl = buildProjectApiPath(projectId, `/preview/comp/${comp}`);
79318
79466
  const thumbnailUrl2 = buildCompositionThumbnailUrl({
79319
79467
  previewUrl,
79320
79468
  seekTime: THUMBNAIL_SEEK_TIME_SECONDS,
@@ -80669,7 +80817,7 @@ var AssetsTab = memo40(function AssetsTab2({
80669
80817
  useEffect121(() => {
80670
80818
  if (manifest404Ref.current.has(projectId)) return;
80671
80819
  let cancelled = false;
80672
- fetch(`/api/projects/${projectId}/preview/.media/manifest.jsonl`).then((r) => {
80820
+ fetch(buildProjectApiPath(projectId, `/preview/.media/manifest.jsonl`)).then((r) => {
80673
80821
  if (!r.ok) {
80674
80822
  manifest404Ref.current.add(projectId);
80675
80823
  return "";
@@ -81683,7 +81831,7 @@ function MediaErrorPanel({ name, filePath }) {
81683
81831
  ] });
81684
81832
  }
81685
81833
  function MediaPreview({ projectId, filePath }) {
81686
- const serveUrl = `/api/projects/${projectId}/preview/${filePath}`;
81834
+ const serveUrl = buildProjectApiPath(projectId, `/preview/${filePath}`);
81687
81835
  const name = filePath.split("/").pop() ?? filePath;
81688
81836
  const [failedPath, setFailedPath] = useState131(null);
81689
81837
  const failed2 = failedPath === filePath;
@@ -83360,7 +83508,7 @@ var RenderQueueItem = memo47(function RenderQueueItem2({
83360
83508
  const [hovered, setHovered] = useState137(false);
83361
83509
  const [videoReady, setVideoReady] = useState137(false);
83362
83510
  const [confirmingDelete, setConfirmingDelete] = useState137(false);
83363
- const fileSrc = `/api/projects/${projectId}/renders/file/${job.filename}`;
83511
+ const fileSrc = buildProjectApiPath(projectId, `/renders/file/${job.filename}`);
83364
83512
  const handleOpen = useCallback157(() => {
83365
83513
  window.open(fileSrc, "_blank");
83366
83514
  }, [fileSrc]);
@@ -87894,7 +88042,7 @@ function FramePoster({
87894
88042
  return /* @__PURE__ */ jsx208("div", { className: "flex h-full w-full items-center justify-center text-[11px] text-neutral-600", children: "Preview unavailable" });
87895
88043
  }
87896
88044
  let url = buildCompositionThumbnailUrl({
87897
- previewUrl: `/api/projects/${projectId}/preview/comp/${src}`,
88045
+ previewUrl: buildProjectApiPath(projectId, `/preview/comp/${src}`),
87898
88046
  seekTime: seconds,
87899
88047
  duration: 0,
87900
88048
  origin: window.location.origin,
@@ -89673,10 +89821,10 @@ function StudioApp() {
89673
89821
  const renderClipContent = useRenderClipContent({
89674
89822
  projectIdRef: fileManager.projectIdRef,
89675
89823
  compIdToSrc,
89676
- activePreviewUrl: activeCompPath ? `/api/projects/${projectId}/preview/comp/${activeCompPath}` : null,
89824
+ activePreviewUrl: activeCompPath && projectId ? buildProjectApiPath(projectId, `/preview/comp/${activeCompPath}`) : null,
89677
89825
  effectiveTimelineDuration
89678
89826
  });
89679
- const compositionDimensions = useCompositionDimensions();
89827
+ const compositionDimensions = useCompositionDimensions(previewIframeRef);
89680
89828
  const { lintModal, linting, handleLint, closeLintModal, findingsByFile } = useLintModal(
89681
89829
  projectId,
89682
89830
  refreshKey