@hyperframes/studio 0.8.56 → 0.8.57

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 (82) hide show
  1. package/dist/assets/hyperframes-player-c23wnIfc.js +464 -0
  2. package/dist/assets/{index-ZmBtVrEm.css → index-DKJpHYYd.css} +1 -1
  3. package/dist/assets/{index-DFlILHJe.js → index-DgkAzx3n.js} +1 -1
  4. package/dist/assets/{index-CxR7F2YQ.js → index-IDNr3X1H.js} +1 -1
  5. package/dist/assets/{index-BlkJ_Axe.js → index-gbOCDCuZ.js} +194 -194
  6. package/dist/index.d.ts +95 -9
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +15573 -14835
  9. package/dist/index.js.map +1 -1
  10. package/dist/styles.css +1 -1
  11. package/package.json +7 -7
  12. package/src/App.tsx +4 -0
  13. package/src/components/EditorShell.tsx +3 -1
  14. package/src/components/ProjectUnreachableBanner.test.tsx +113 -0
  15. package/src/components/ProjectUnreachableBanner.tsx +89 -0
  16. package/src/components/editor/DomEditOverlay.tsx +0 -15
  17. package/src/components/editor/GridOverlay.tsx +12 -23
  18. package/src/components/editor/MotionPathOverlay.test.tsx +66 -0
  19. package/src/components/editor/MotionPathOverlay.tsx +3 -1
  20. package/src/components/editor/PreviewGuides.test.tsx +6 -1
  21. package/src/components/editor/PreviewGuides.tsx +8 -10
  22. package/src/components/editor/PreviewOverlayProvider.test.tsx +90 -0
  23. package/src/components/editor/PreviewOverlayProvider.tsx +86 -0
  24. package/src/components/editor/SnapToolbar.test.tsx +25 -12
  25. package/src/components/editor/SnapToolbar.tsx +30 -59
  26. package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +2 -2
  27. package/src/components/editor/propertyPanelFlatMediaSection.tsx +46 -12
  28. package/src/components/editor/usePreviewCompositionRect.test.tsx +19 -13
  29. package/src/components/editor/usePreviewCompositionRect.ts +12 -12
  30. package/src/components/editor/useVolumeAutomation.test.tsx +68 -0
  31. package/src/components/editor/useVolumeAutomation.ts +57 -16
  32. package/src/components/nle/NLEContext.tsx +40 -4
  33. package/src/components/nle/NLEPreview.test.ts +66 -1
  34. package/src/components/nle/NLEPreview.tsx +88 -44
  35. package/src/components/nle/PreviewOverlays.tsx +4 -16
  36. package/src/components/nle/PreviewPane.tsx +17 -2
  37. package/src/components/nle/previewReadOnly.test.tsx +1 -0
  38. package/src/hooks/useCompositionDimensions.test.tsx +38 -0
  39. package/src/hooks/useCompositionDimensions.ts +17 -1
  40. package/src/hooks/useDomEditAttributeCommits.test.ts +23 -0
  41. package/src/hooks/useDomEditAttributeCommits.ts +14 -3
  42. package/src/hooks/useFileTree.test.tsx +56 -0
  43. package/src/hooks/useFileTree.ts +8 -1
  44. package/src/hooks/useSdkSession.lifecycle.test.tsx +80 -0
  45. package/src/hooks/useSdkSession.ts +46 -10
  46. package/src/hooks/useServerConnection.hashProject.test.tsx +303 -0
  47. package/src/hooks/useServerConnection.ts +81 -8
  48. package/src/index.ts +12 -0
  49. package/src/player/components/Player.test.ts +141 -13
  50. package/src/player/components/Player.tsx +58 -2
  51. package/src/player/components/ShortcutsPanel.test.tsx +4 -10
  52. package/src/player/components/Timeline.tsx +23 -572
  53. package/src/player/components/TimelineCanvas.tsx +9 -30
  54. package/src/player/components/TimelineOverlays.test.ts +67 -32
  55. package/src/player/components/TimelineOverlays.tsx +46 -107
  56. package/src/player/components/TimelineProvider.tsx +191 -0
  57. package/src/player/components/testRootHarness.ts +18 -0
  58. package/src/player/components/timelineProviderStateBuilders.test.ts +109 -0
  59. package/src/player/components/timelineProviderStateBuilders.ts +83 -0
  60. package/src/player/components/useTimelineClipContextMenu.ts +1 -1
  61. package/src/player/components/useTimelineOverlaysState.ts +100 -0
  62. package/src/player/components/useTimelineProviderState.tsx +583 -0
  63. package/src/player/hooks/playbackAdapterResolution.ts +98 -0
  64. package/src/player/hooks/timelinePlayerTestHarness.ts +120 -0
  65. package/src/player/hooks/usePreviewFirstFrameTelemetry.ts +51 -0
  66. package/src/player/hooks/useShadowPreviewReload.ts +183 -0
  67. package/src/player/hooks/useTimelinePlayer.seek.test.ts +8 -88
  68. package/src/player/hooks/useTimelinePlayer.shadowReload.test.ts +457 -0
  69. package/src/player/hooks/useTimelinePlayer.ts +65 -99
  70. package/src/player/hooks/useTimelineSyncCallbacks.ts +159 -88
  71. package/src/telemetry/events.test.ts +18 -0
  72. package/src/telemetry/events.ts +10 -0
  73. package/src/telemetry/system.test.ts +49 -0
  74. package/src/telemetry/system.ts +6 -0
  75. package/src/telemetry/tabId.test.ts +43 -0
  76. package/src/telemetry/tabId.ts +44 -0
  77. package/src/utils/previewCompositionSize.ts +26 -0
  78. package/src/utils/reloadDebug.ts +2 -4
  79. package/src/utils/sourcePatcher.ts +1 -1
  80. package/src/utils/studioTelemetry.test.ts +14 -0
  81. package/src/utils/studioTelemetry.ts +4 -0
  82. package/dist/assets/hyperframes-player-k8LzMtXX.js +0 -464
@@ -26,7 +26,11 @@ interface PlayerProps {
26
26
  projectId?: string;
27
27
  directUrl?: string;
28
28
  onLoad: () => void;
29
+ /** Fires once the loaded document is painted and every loader (shader, assets) has cleared. */
30
+ onReadyToShowChange?: (ready: boolean) => void;
31
+ onPreviewError?: (message: string) => void;
29
32
  onCompositionLoadingChange?: (loading: boolean) => void;
33
+ onPainted?: (details: { iframe: HTMLIFrameElement; startedAt: number; loadId: number }) => void;
30
34
  portrait?: boolean;
31
35
  style?: React.CSSProperties;
32
36
  suppressLoadingOverlay?: boolean;
@@ -134,7 +138,10 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
134
138
  projectId,
135
139
  directUrl,
136
140
  onLoad,
141
+ onReadyToShowChange,
142
+ onPreviewError,
137
143
  onCompositionLoadingChange,
144
+ onPainted,
138
145
  portrait,
139
146
  style,
140
147
  suppressLoadingOverlay,
@@ -146,13 +153,25 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
146
153
  const assetPollRef = useRef<ReturnType<typeof setInterval> | null>(null);
147
154
  const assetFadeRef = useRef<ReturnType<typeof setTimeout> | null>(null);
148
155
  const retryPreviewRef = useRef<(() => void) | null>(null);
156
+ const loadStartedAtRef = useRef(0);
157
+ const loadIdRef = useRef(0);
149
158
  const retryCountRef = useRef(0);
159
+ // Read at call time: this element outlives its first props (a shadow preview is
160
+ // promoted in place), so mount-time closures would go stale.
161
+ const onLoadRef = useRef(onLoad);
162
+ onLoadRef.current = onLoad;
163
+ const onReadyToShowChangeRef = useRef(onReadyToShowChange);
164
+ onReadyToShowChangeRef.current = onReadyToShowChange;
165
+ const onPreviewErrorRef = useRef(onPreviewError);
166
+ onPreviewErrorRef.current = onPreviewError;
167
+ const [loaded, setLoaded] = useState(false);
150
168
  const [assetsLoading, setAssetsLoading] = useState(false);
151
169
  const [assetOverlayVisible, setAssetOverlayVisible] = useState(false);
152
170
  const [assetOverlayFading, setAssetOverlayFading] = useState(false);
153
171
  const [assetWaitLong, setAssetWaitLong] = useState(false);
154
172
  const [shaderTransitionLoading, setShaderTransitionLoading] = useState(false);
155
173
  const [compositionLoading, setCompositionLoading] = useState(true);
174
+ const [painted, setPainted] = useState(false);
156
175
  const [compositionOverlayDeferred, setCompositionOverlayDeferred] = useState(true);
157
176
  const [previewError, setPreviewError] = useState<string | null>(null);
158
177
 
@@ -194,6 +213,8 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
194
213
  retryUrl.searchParams.set("_hfStudioRetry", String(retryCountRef.current));
195
214
  setPreviewError(null);
196
215
  setCompositionLoading(true);
216
+ loadStartedAtRef.current = performance.now();
217
+ loadIdRef.current += 1;
197
218
  player.setAttribute("src", retryUrl.pathname + retryUrl.search);
198
219
  };
199
220
  retryPreviewRef.current = retryPreview;
@@ -207,15 +228,23 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
207
228
  setPreviewError(null);
208
229
  setCompositionLoading(false);
209
230
  };
231
+ const handlePainted = () => {
232
+ setPainted(true);
233
+ onPainted?.({ iframe, startedAt: loadStartedAtRef.current, loadId: loadIdRef.current });
234
+ };
210
235
  const handleError = (event: Event) => {
211
- setPreviewError(readPreviewErrorMessage(event));
236
+ const message = readPreviewErrorMessage(event);
237
+ onPreviewErrorRef.current?.(message);
238
+ setPreviewError(message);
212
239
  setCompositionLoading(false);
213
240
  };
214
241
  const handleLoad = () => {
215
242
  loadCountRef.current++;
243
+ setLoaded(true);
216
244
  setPreviewError(null);
217
245
  setShaderTransitionLoading(false);
218
246
  setCompositionLoading(true);
247
+ setPainted(false);
219
248
  // Reveal animation on reload (hot-reload, composition switch)
220
249
  if (loadCountRef.current > 1) {
221
250
  container.classList.remove("preview-revealing");
@@ -224,7 +253,7 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
224
253
  const onEnd = () => container.classList.remove("preview-revealing");
225
254
  container.addEventListener("animationend", onEnd, { once: true });
226
255
  }
227
- onLoad();
256
+ onLoadRef.current();
228
257
 
229
258
  // Show a loading overlay until every `<video>`/`<audio>` and Lottie
230
259
  // asset is ready. Without this users can click play before audio has
@@ -274,6 +303,7 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
274
303
  player.addEventListener("click", preventToggle, { capture: true });
275
304
  player.addEventListener("shadertransitionstate", handleShaderTransitionState);
276
305
  player.addEventListener("ready", handleReady);
306
+ player.addEventListener("painted", handlePainted);
277
307
  player.addEventListener("error", handleError);
278
308
 
279
309
  // Bridge the inner iframe to the forwarded ref for useTimelinePlayer.
@@ -291,6 +321,8 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
291
321
  player.style.height = "100%";
292
322
  player.style.display = "block";
293
323
  player.style.background = "transparent";
324
+ loadStartedAtRef.current = performance.now();
325
+ loadIdRef.current += 1;
294
326
  player.setAttribute("src", src);
295
327
  container.appendChild(player);
296
328
 
@@ -313,6 +345,7 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
313
345
  player.removeEventListener("click", preventToggle, { capture: true });
314
346
  player.removeEventListener("shadertransitionstate", handleShaderTransitionState);
315
347
  player.removeEventListener("ready", handleReady);
348
+ player.removeEventListener("painted", handlePainted);
316
349
  player.removeEventListener("error", handleError);
317
350
  if (assetPollRef.current) clearInterval(assetPollRef.current);
318
351
  assetPollRef.current = null;
@@ -394,6 +427,29 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
394
427
  setAssetsLoading(false);
395
428
  };
396
429
 
430
+ const readyToShow =
431
+ loaded &&
432
+ painted &&
433
+ !compositionLoading &&
434
+ !shaderTransitionLoading &&
435
+ !assetsLoading &&
436
+ !previewError;
437
+ // `painted` means the player's own loader has finished fading; two frames of grace on top.
438
+ useEffect(() => {
439
+ if (!readyToShow) {
440
+ onReadyToShowChangeRef.current?.(false);
441
+ return;
442
+ }
443
+ let second = 0;
444
+ const first = requestAnimationFrame(() => {
445
+ second = requestAnimationFrame(() => onReadyToShowChangeRef.current?.(true));
446
+ });
447
+ return () => {
448
+ cancelAnimationFrame(first);
449
+ cancelAnimationFrame(second);
450
+ };
451
+ }, [readyToShow]);
452
+
397
453
  const showCompositionOverlay =
398
454
  !suppressLoadingOverlay &&
399
455
  !compositionOverlayDeferred &&
@@ -1,24 +1,18 @@
1
1
  // @vitest-environment happy-dom
2
2
 
3
3
  import React, { act, Profiler } from "react";
4
- import { createRoot, type Root } from "react-dom/client";
5
- import { afterEach, describe, expect, it, vi } from "vitest";
4
+ import { describe, expect, it, vi } from "vitest";
6
5
  import { ShortcutsPanel } from "./ShortcutsPanel";
6
+ import { createHappyDomRootHarness } from "./testRootHarness";
7
7
 
8
8
  (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
9
9
 
10
- const roots: Root[] = [];
11
-
12
- afterEach(() => {
13
- for (const root of roots.splice(0)) act(() => root.unmount());
14
- document.body.innerHTML = "";
15
- });
10
+ const { mount } = createHappyDomRootHarness();
16
11
 
17
12
  function renderPanel(onRender = vi.fn()) {
18
13
  const host = document.createElement("div");
19
14
  document.body.append(host);
20
- const root = createRoot(host);
21
- roots.push(root);
15
+ const root = mount(host);
22
16
  act(() => {
23
17
  root.render(
24
18
  <Profiler id="shortcuts-panel" onRender={onRender}>