@hyperframes/studio 0.8.25 → 0.8.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.8.25",
3
+ "version": "0.8.27",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -48,11 +48,11 @@
48
48
  "gsap": "^3.13.0",
49
49
  "marked": "^14.1.4",
50
50
  "mediabunny": "^1.45.3",
51
- "@hyperframes/parsers": "0.8.25",
52
- "@hyperframes/core": "0.8.25",
53
- "@hyperframes/player": "0.8.25",
54
- "@hyperframes/studio-server": "0.8.25",
55
- "@hyperframes/sdk": "0.8.25"
51
+ "@hyperframes/core": "0.8.27",
52
+ "@hyperframes/parsers": "0.8.27",
53
+ "@hyperframes/sdk": "0.8.27",
54
+ "@hyperframes/studio-server": "0.8.27",
55
+ "@hyperframes/player": "0.8.27"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@types/react": "19",
@@ -69,7 +69,7 @@
69
69
  "vite": "^6.4.2",
70
70
  "vitest": "^3.2.4",
71
71
  "zustand": "^5.0.0",
72
- "@hyperframes/producer": "0.8.25"
72
+ "@hyperframes/producer": "0.8.27"
73
73
  },
74
74
  "peerDependencies": {
75
75
  "react": "19",
@@ -316,10 +316,15 @@ describe("toggleTimelineElementHidden", () => {
316
316
  const iframe = document.createElement("iframe");
317
317
  document.body.append(iframe);
318
318
  const seek = vi.fn();
319
+ const forceTimelineRebind = vi.fn();
319
320
  const win = iframe.contentWindow;
320
321
  if (!win) throw new Error("Expected iframe contentWindow");
321
- const playerWindow: Window & { __player?: { seek?: (time: number) => void } } = win;
322
+ const playerWindow: Window & {
323
+ __player?: { seek?: (time: number) => void };
324
+ __hfForceTimelineRebind?: () => void;
325
+ } = win;
322
326
  playerWindow.__player = { seek };
327
+ playerWindow.__hfForceTimelineRebind = forceTimelineRebind;
323
328
 
324
329
  const files = new Map([
325
330
  [
@@ -369,6 +374,7 @@ describe("toggleTimelineElementHidden", () => {
369
374
  expect(recordEdit).toHaveBeenCalledTimes(1);
370
375
  expect(recordEdit.mock.calls[0]?.[0]?.label).toBe("Hide element");
371
376
  expect(seek).toHaveBeenCalledWith(1.25);
377
+ expect(forceTimelineRebind).toHaveBeenCalledTimes(1);
372
378
  expect(
373
379
  usePlayerStore.getState().elements.find((el) => el.key === "index.html:#hero")?.hidden,
374
380
  ).toBe(true);
@@ -1,6 +1,8 @@
1
1
  import { useCallback } from "react";
2
2
  import { usePlayerStore, type TimelineElement } from "../player";
3
+ import { reseekPreviewAtTime } from "../player/hooks/timelineSyncHydration";
3
4
  import { useExpandedTimelineElements } from "../player/hooks/useExpandedTimelineElements";
5
+ import { applySoftReloadFinalization } from "../utils/gsapSoftReload";
4
6
  import {
5
7
  timelineTrackOrder,
6
8
  trackDisplayNumber,
@@ -106,11 +108,9 @@ function patchLiveHiddenState(
106
108
  }
107
109
 
108
110
  export function reseekPreviewRuntime(iframe: HTMLIFrameElement | null): void {
109
- try {
110
- const win: (Window & { __player?: { seek?: (time: number) => void } }) | null =
111
- iframe?.contentWindow ?? null;
112
- win?.__player?.seek?.(usePlayerStore.getState().currentTime);
113
- } catch {}
111
+ const store = usePlayerStore.getState();
112
+ if (applySoftReloadFinalization(iframe, store.currentTime)) return;
113
+ reseekPreviewAtTime({ seek: store.requestSeek }, store.currentTime);
114
114
  }
115
115
 
116
116
  export function groupElementsByTargetPath(
@@ -981,6 +981,42 @@ describe("useDomEditCommits style persist handling", () => {
981
981
  }
982
982
  });
983
983
 
984
+ it("rebinds the paused preview after a saved optimistic style commit", async () => {
985
+ stubPatchFetch({
986
+ ok: true,
987
+ changed: true,
988
+ matched: true,
989
+ path: "index.html",
990
+ version: '"sha256:changed"',
991
+ });
992
+ const { iframe, element } = createPreviewElement();
993
+ const seek = vi.fn();
994
+ const forceTimelineRebind = vi.fn();
995
+ Object.defineProperty(iframe.contentWindow, "__player", {
996
+ configurable: true,
997
+ value: { seek },
998
+ });
999
+ Object.defineProperty(iframe.contentWindow, "__hfForceTimelineRebind", {
1000
+ configurable: true,
1001
+ value: forceTimelineRebind,
1002
+ });
1003
+ usePlayerStore.setState({ currentTime: 2.4 });
1004
+ const rendered = renderDomEditCommits(createSelection(element), iframe);
1005
+
1006
+ try {
1007
+ await act(async () => {
1008
+ await rendered.hook.handleDomStyleCommit("color", "blue");
1009
+ });
1010
+
1011
+ expect(seek).toHaveBeenCalledWith(2.4);
1012
+ expect(forceTimelineRebind).toHaveBeenCalledTimes(1);
1013
+ expect(rendered.reloadPreview).not.toHaveBeenCalled();
1014
+ } finally {
1015
+ rendered.cleanup();
1016
+ usePlayerStore.getState().reset();
1017
+ }
1018
+ });
1019
+
984
1020
  it("toasts and reverts a style commit when the patch request rejects", async () => {
985
1021
  const { element, rendered, cleanup } = await commitStyleAgainst(new Error("network down"));
986
1022
 
@@ -34,6 +34,7 @@ import {
34
34
  } from "./useDomEditCommitsHelpers";
35
35
  import type { CutoverResult } from "../utils/sdkCutover";
36
36
  import { studioWriteHeaders } from "../utils/studioFileVersion";
37
+ import { reseekPreviewRuntime } from "./timelineTrackVisibility";
37
38
  interface RecordEditInput {
38
39
  label: string;
39
40
  kind: EditHistoryKind;
@@ -154,6 +155,10 @@ export function useDomEditCommits({
154
155
  if (options?.shouldSave && !options.shouldSave()) return;
155
156
 
156
157
  const targetPath = selection.sourceFile || activeCompPath || "index.html";
158
+ const completePersistence = <T>(result: T, changed: boolean): T => {
159
+ if (options?.skipRefresh && changed) reseekPreviewRuntime(previewIframeRef.current);
160
+ return result;
161
+ };
157
162
 
158
163
  const readResponse = await fetch(
159
164
  `/api/projects/${pid}/files/${encodeURIComponent(targetPath)}`,
@@ -201,7 +206,10 @@ export function useDomEditCommits({
201
206
  if (cutover.status === "committed") {
202
207
  // SDK handled it — its in-memory doc is already current, so do NOT
203
208
  // forceReload (that would echo-reload the session we just wrote).
204
- return { sourceFile: targetPath, version: cutover.version, changed: true };
209
+ return completePersistence(
210
+ { sourceFile: targetPath, version: cutover.version, changed: true },
211
+ true,
212
+ );
205
213
  }
206
214
  }
207
215
 
@@ -249,9 +257,12 @@ export function useDomEditCommits({
249
257
  throw new DomEditPersistUnresolvableError(targetPath);
250
258
  }
251
259
  warnDomEditPersistNoOp(selection, operations);
252
- return typeof patchData.path === "string" && typeof patchData.version === "string"
253
- ? { sourceFile: patchData.path, version: patchData.version, changed: false }
254
- : undefined;
260
+ return completePersistence(
261
+ typeof patchData.path === "string" && typeof patchData.version === "string"
262
+ ? { sourceFile: patchData.path, version: patchData.version, changed: false }
263
+ : undefined,
264
+ false,
265
+ );
255
266
  }
256
267
 
257
268
  const patchedContent =
@@ -289,11 +300,14 @@ export function useDomEditCommits({
289
300
  if (!options?.skipRefresh) {
290
301
  reloadPreview();
291
302
  }
292
- return finalContent === patchedContent &&
293
- typeof patchData.path === "string" &&
294
- typeof patchData.version === "string"
295
- ? { sourceFile: patchData.path, version: patchData.version, changed: true }
296
- : undefined;
303
+ return completePersistence(
304
+ finalContent === patchedContent &&
305
+ typeof patchData.path === "string" &&
306
+ typeof patchData.version === "string"
307
+ ? { sourceFile: patchData.path, version: patchData.version, changed: true }
308
+ : undefined,
309
+ true,
310
+ );
297
311
  },
298
312
  [
299
313
  activeCompPath,
@@ -305,6 +319,7 @@ export function useDomEditCommits({
305
319
  showToast,
306
320
  forceReloadSdkSession,
307
321
  onTrySdkPersist,
322
+ previewIframeRef,
308
323
  ],
309
324
  );
310
325
 
@@ -290,6 +290,15 @@ export function resolveReloadSeekTime(input: {
290
290
  return Math.min(target, input.duration);
291
291
  }
292
292
 
293
+ type SeekablePreview = Pick<PlaybackAdapter, "seek">;
294
+
295
+ /** Re-run the current frame even when the runtime already reports that time. */
296
+ export function reseekPreviewAtTime(preview: SeekablePreview, time: number): void {
297
+ const target = Number.isFinite(time) && time > 0 ? time : 0;
298
+ preview.seek(target > 0.001 ? Math.max(0, target - 0.001) : 0.001);
299
+ preview.seek(target);
300
+ }
301
+
293
302
  export function seekAdapterToRestorePoint(
294
303
  adapter: PlaybackAdapter,
295
304
  pendingSeekRef: { current: number | null },
@@ -303,8 +312,7 @@ export function seekAdapterToRestorePoint(
303
312
  });
304
313
  pendingSeekRef.current = null;
305
314
  if (storeSeek != null) usePlayerStore.getState().clearSeekRequest();
306
- adapter.seek(startTime > 0.001 ? Math.max(0, startTime - 0.001) : 0.001);
307
- adapter.seek(startTime);
315
+ reseekPreviewAtTime(adapter, startTime);
308
316
  return startTime;
309
317
  }
310
318