@hyperframes/studio 0.8.192 → 0.8.194

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.
@@ -1,4 +1,4 @@
1
- import{n as Qi}from"./index-CHu83hHM.js";/*!
1
+ import{n as Qi}from"./index-CAQ7OcNJ.js";/*!
2
2
  * Copyright (c) 2026-present, Vanilagy and contributors
3
3
  *
4
4
  * This Source Code Form is subject to the terms of the Mozilla Public
@@ -1028,7 +1028,8 @@ function sceneSwapFor(iframe) {
1028
1028
  () => deadline.abort(new Error("the preview took too long to swap")),
1029
1029
  SCENE_SWAP_MS
1030
1030
  );
1031
- cancel?.addEventListener("abort", () => deadline.abort(cancel.reason), { once: true });
1031
+ const onCancel = () => deadline.abort(cancel?.reason);
1032
+ cancel?.addEventListener("abort", onCancel, { once: true });
1032
1033
  const swapping = (async () => {
1033
1034
  const response = await studioApiFetch(url, { signal: deadline.signal });
1034
1035
  if (!response.ok) throw new Error(`preview request failed with ${response.status}`);
@@ -1043,6 +1044,7 @@ function sceneSwapFor(iframe) {
1043
1044
  await Promise.race([swapping, expired]);
1044
1045
  } finally {
1045
1046
  clearTimeout(timer);
1047
+ cancel?.removeEventListener("abort", onCancel);
1046
1048
  }
1047
1049
  iframe.dispatchEvent(new Event(SCENES_SWAPPED));
1048
1050
  };
package/dist/index.html CHANGED
@@ -14,7 +14,7 @@
14
14
  document.documentElement.dataset.theme = "paper";
15
15
  }
16
16
  </script>
17
- <script type="module" crossorigin src="/assets/index-CHu83hHM.js"></script>
17
+ <script type="module" crossorigin src="/assets/index-CAQ7OcNJ.js"></script>
18
18
  <link rel="stylesheet" crossorigin href="/assets/index-CYbvOVp-.css">
19
19
  </head>
20
20
  <body>
package/dist/index.js CHANGED
@@ -48,7 +48,7 @@ import {
48
48
  usePreviewVariablesStore,
49
49
  whenPreviewBooted,
50
50
  writeStudioUiPreferences
51
- } from "./chunk-6SXUUBPD.js";
51
+ } from "./chunk-ORLUDHTU.js";
52
52
  import {
53
53
  Button,
54
54
  ContextMenu,
@@ -17608,7 +17608,7 @@ async function writeProjectFilesWithHistoryInQueue({
17608
17608
 
17609
17609
  // src/utils/gsapSoftReload.ts
17610
17610
  import { COLOR_GRADING_SOURCE_HIDDEN_ATTR as COLOR_GRADING_SOURCE_HIDDEN_ATTR2 } from "@hyperframes/core/color-grading";
17611
- import { motionPathPluginUrl } from "@hyperframes/core/gsap-cdn";
17611
+ import { GSAP_BASE_URL_META, motionPathPluginUrl } from "@hyperframes/core/gsap-cdn";
17612
17612
 
17613
17613
  // src/utils/authoredSource.ts
17614
17614
  import { ensureHfIds, isCompositionTemplate } from "@hyperframes/parsers/hf-ids";
@@ -17870,6 +17870,12 @@ function fileDocs(ownFile, written, nestedFiles) {
17870
17870
 
17871
17871
  // src/utils/gsapSoftReload.ts
17872
17872
  var pendingReadyRetry = null;
17873
+ function previewMotionPathUrl(doc, version) {
17874
+ const baseUrl = doc.head.querySelector(
17875
+ `meta[name="${GSAP_BASE_URL_META}"]`
17876
+ )?.content;
17877
+ return motionPathPluginUrl(version, baseUrl);
17878
+ }
17873
17879
  function ensureMotionPathPluginLoaded(iframe) {
17874
17880
  if (pendingReadyRetry) window.removeEventListener("message", pendingReadyRetry);
17875
17881
  pendingReadyRetry = null;
@@ -17900,7 +17906,7 @@ function ensureMotionPathPluginLoaded(iframe) {
17900
17906
  try {
17901
17907
  win.__hfMotionPathPluginLoading = true;
17902
17908
  const pluginScript = doc.createElement("script");
17903
- pluginScript.src = motionPathPluginUrl(win.gsap?.version);
17909
+ pluginScript.src = previewMotionPathUrl(doc, win.gsap?.version);
17904
17910
  const finalize = () => {
17905
17911
  win.__hfMotionPathPluginLoading = false;
17906
17912
  try {
@@ -18086,7 +18092,7 @@ function applySoftReload(iframe, scriptText, options = {}) {
18086
18092
  }
18087
18093
  win.__hfMotionPathPluginLoading = true;
18088
18094
  const pluginScript = doc.createElement("script");
18089
- pluginScript.src = motionPathPluginUrl(win.gsap?.version);
18095
+ pluginScript.src = previewMotionPathUrl(doc, win.gsap?.version);
18090
18096
  pluginScript.onload = () => {
18091
18097
  win.__hfMotionPathPluginLoading = false;
18092
18098
  executeScript();
@@ -98684,7 +98690,7 @@ function levelOf(settings) {
98684
98690
  return settings.shutterDeg > STRONG_ABOVE ? "strong" : "subtle";
98685
98691
  }
98686
98692
  function patchForLevel(level) {
98687
- return level === "off" ? { enabled: false } : { enabled: true, ...LEVEL_SETTINGS[level] };
98693
+ return level === "off" ? { enabled: false, events: false } : { enabled: true, events: true, ...LEVEL_SETTINGS[level] };
98688
98694
  }
98689
98695
 
98690
98696
  // src/components/panels/MotionBlurPanel.tsx
package/dist/player.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Player
3
- } from "./chunk-6SXUUBPD.js";
3
+ } from "./chunk-ORLUDHTU.js";
4
4
  import "./chunk-7HZGKFFU.js";
5
5
  import "./chunk-T43DM7Q6.js";
6
6
  import "./chunk-MLKGABMK.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.8.192",
3
+ "version": "0.8.194",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -60,12 +60,12 @@
60
60
  "dockview-react": "8.3.1",
61
61
  "gsap": "^3.13.0",
62
62
  "mediabunny": "^1.45.3",
63
- "@hyperframes/core": "0.8.192",
64
- "@hyperframes/parsers": "0.8.192",
65
- "@hyperframes/player": "0.8.192",
66
- "@hyperframes/sdk": "0.8.192",
67
- "@hyperframes/shader-transitions": "0.8.192",
68
- "@hyperframes/studio-server": "0.8.192"
63
+ "@hyperframes/parsers": "0.8.194",
64
+ "@hyperframes/core": "0.8.194",
65
+ "@hyperframes/sdk": "0.8.194",
66
+ "@hyperframes/shader-transitions": "0.8.194",
67
+ "@hyperframes/player": "0.8.194",
68
+ "@hyperframes/studio-server": "0.8.194"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@tailwindcss/vite": "^4.3.3",
@@ -82,7 +82,7 @@
82
82
  "vite": "^6.4.2",
83
83
  "vitest": "^4.1.11",
84
84
  "zustand": "^5.0.0",
85
- "@hyperframes/producer": "0.8.192"
85
+ "@hyperframes/producer": "0.8.194"
86
86
  },
87
87
  "peerDependencies": {
88
88
  "react": "19",
@@ -130,20 +130,29 @@ describe("MotionBlurPanel", () => {
130
130
  expect(html).toContain('data-motion-blur-samples="64"');
131
131
  });
132
132
 
133
- it("Off removes data-motion-blur and leaves every other setting on the root", async () => {
133
+ it("Off removes data-motion-blur and its events, and leaves the other settings on the root", async () => {
134
134
  const host = await mount(
135
135
  ' data-motion-blur="on" data-motion-blur-shutter="720" data-motion-blur-samples="64" data-motion-blur-tier="filter" data-motion-blur-events="on"',
136
136
  );
137
137
  await click(radio(host, "Off"));
138
138
  const html = files["index.html"] ?? "";
139
139
  expect(html).not.toContain('data-motion-blur="on"');
140
- for (const kept of ['shutter="720"', 'samples="64"', 'tier="filter"', 'events="on"']) {
140
+ expect(html).not.toContain("data-motion-blur-events");
141
+ for (const kept of ['shutter="720"', 'samples="64"', 'tier="filter"']) {
141
142
  expect(html).toContain(`data-motion-blur-${kept}`);
142
143
  }
143
144
  });
144
145
 
146
+ it("turns the events attribute on with Subtle and Strong, so callback-driven motion is found", async () => {
147
+ const host = await mount();
148
+ await click(radio(host, "Subtle"));
149
+ expect(files["index.html"]).toContain('data-motion-blur-events="on"');
150
+ await click(radio(host, "Strong"));
151
+ expect(files["index.html"]).toContain('data-motion-blur-events="on"');
152
+ });
153
+
145
154
  it("writes nothing when the chosen level is already set", async () => {
146
- const host = await mount(' data-motion-blur="on"');
155
+ const host = await mount(' data-motion-blur="on" data-motion-blur-events="on"');
147
156
  await click(radio(host, "Subtle"));
148
157
  expect(writeProjectFile).not.toHaveBeenCalled();
149
158
  });
@@ -130,9 +130,19 @@ describe("levelOf and patchForLevel", () => {
130
130
  });
131
131
 
132
132
  it("sets Subtle to the default blur and Strong to the maximum, and leaves the rest alone for Off", () => {
133
- expect(patchForLevel("subtle")).toEqual({ enabled: true, shutterDeg: 240, samples: null });
134
- expect(patchForLevel("strong")).toEqual({ enabled: true, shutterDeg: 720, samples: 64 });
135
- expect(patchForLevel("off")).toEqual({ enabled: false });
133
+ expect(patchForLevel("subtle")).toEqual({
134
+ enabled: true,
135
+ events: true,
136
+ shutterDeg: 240,
137
+ samples: null,
138
+ });
139
+ expect(patchForLevel("strong")).toEqual({
140
+ enabled: true,
141
+ events: true,
142
+ shutterDeg: 720,
143
+ samples: 64,
144
+ });
145
+ expect(patchForLevel("off")).toEqual({ enabled: false, events: false });
136
146
  });
137
147
 
138
148
  it("reads back as the level that was chosen", () => {
@@ -92,7 +92,9 @@ export function levelOf(settings: MotionBlurSettings): BlurLevel {
92
92
  return settings.shutterDeg > STRONG_ABOVE ? "strong" : "subtle";
93
93
  }
94
94
 
95
- /** What choosing `level` changes; Off leaves the shutter, samples and every other setting where they were. */
95
+ /** What choosing `level` changes; blur on turns events on too, and Off leaves the shutter, samples and the rest alone. */
96
96
  export function patchForLevel(level: BlurLevel): Partial<MotionBlurSettings> {
97
- return level === "off" ? { enabled: false } : { enabled: true, ...LEVEL_SETTINGS[level] };
97
+ return level === "off"
98
+ ? { enabled: false, events: false }
99
+ : { enabled: true, events: true, ...LEVEL_SETTINGS[level] };
98
100
  }
@@ -98,6 +98,28 @@ describe("sceneSwapFor", () => {
98
98
  expect(swap).not.toHaveBeenCalled();
99
99
  });
100
100
 
101
+ it("leaves no listener on the caller's cancel signal once swaps settle, so a swapped film can be freed", async () => {
102
+ vi.spyOn(globalThis, "fetch").mockImplementation(async () => new Response("<html></html>"));
103
+ const cancel = new AbortController();
104
+ const live = new Set<unknown>();
105
+ const { signal } = cancel;
106
+ const add = signal.addEventListener.bind(signal);
107
+ const remove = signal.removeEventListener.bind(signal);
108
+ signal.addEventListener = ((type: string, listener: EventListener, options?: unknown) => {
109
+ live.add(listener);
110
+ add(type, listener, options as AddEventListenerOptions);
111
+ }) as AbortSignal["addEventListener"];
112
+ signal.removeEventListener = ((type: string, listener: EventListener, options?: unknown) => {
113
+ live.delete(listener);
114
+ remove(type, listener, options as EventListenerOptions);
115
+ }) as AbortSignal["removeEventListener"];
116
+ const { iframe } = swappableIframe();
117
+ const refused = swappableIframe(vi.fn(async () => Promise.reject(new Error("refused")))).iframe;
118
+ for (let n = 0; n < 3; n++) await sceneSwapFor(iframe)!("/preview", () => true, signal);
119
+ await expect(sceneSwapFor(refused)!("/preview", () => true, signal)).rejects.toThrow("refused");
120
+ expect(live.size).toBe(0);
121
+ });
122
+
101
123
  it("swaps a document that arrives in time", async () => {
102
124
  vi.spyOn(globalThis, "fetch").mockImplementation(async () => new Response("<html></html>"));
103
125
  const { iframe, swap } = swappableIframe();
@@ -88,7 +88,8 @@ export function sceneSwapFor(
88
88
  () => deadline.abort(new Error("the preview took too long to swap")),
89
89
  SCENE_SWAP_MS,
90
90
  );
91
- cancel?.addEventListener("abort", () => deadline.abort(cancel.reason), { once: true });
91
+ const onCancel = () => deadline.abort(cancel?.reason);
92
+ cancel?.addEventListener("abort", onCancel, { once: true });
92
93
  const swapping = (async () => {
93
94
  const response = await studioApiFetch(url, { signal: deadline.signal });
94
95
  if (!response.ok) throw new Error(`preview request failed with ${response.status}`);
@@ -104,6 +105,7 @@ export function sceneSwapFor(
104
105
  await Promise.race([swapping, expired]);
105
106
  } finally {
106
107
  clearTimeout(timer);
108
+ cancel?.removeEventListener("abort", onCancel);
107
109
  }
108
110
  iframe.dispatchEvent(new Event(SCENES_SWAPPED));
109
111
  };
@@ -318,6 +318,28 @@ window.__timelines["root"] = tl;`;
318
318
  expect(contentWindow.__hfMotionPathPluginLoading).toBe(false);
319
319
  });
320
320
 
321
+ it("loads a missing soft-reload plugin from the iframe mirror and reports its failure", () => {
322
+ const { iframe, contentWindow } = buildMockIframe({
323
+ gsap: { timeline: vi.fn(), registerPlugin: vi.fn(), version: "3.14.2" },
324
+ });
325
+ const head = iframe.contentDocument!.head;
326
+ const meta = document.createElement("meta");
327
+ meta.name = "hyperframes-gsap-base-url";
328
+ meta.content = "http://localhost:8080/vendor/npm";
329
+ head.appendChild(meta);
330
+ const onAsyncFailure = vi.fn();
331
+ expect(applySoftReload(iframe, MOTION_PATH_SCRIPT_TEXT, { onAsyncFailure })).toBe("applied");
332
+ const plugin = head.querySelector("script");
333
+ expect(plugin?.src).toBe(
334
+ "http://localhost:8080/vendor/npm/gsap@3.14.2/dist/MotionPathPlugin.min.js",
335
+ );
336
+ plugin?.onerror?.(new Event("error"));
337
+ expect(onAsyncFailure).toHaveBeenCalledTimes(1);
338
+ expect(contentWindow.__hfForceTimelineRebind).not.toHaveBeenCalled();
339
+ expect(contentWindow.__hfMotionPathPluginLoading).toBe(false);
340
+ expect(head.querySelectorAll("script")).toHaveLength(1);
341
+ });
342
+
321
343
  it('returns "cannot-soft-reload" when multiple GSAP scripts exist (ambiguous)', () => {
322
344
  const script1 = document.createElement("script");
323
345
  script1.textContent = "const tl = gsap.timeline({ paused: true });";
@@ -490,6 +512,26 @@ describe("ensureMotionPathPluginLoaded", () => {
490
512
  );
491
513
  });
492
514
 
515
+ it("preloads from the iframe mirror once, retaining the runtime version", () => {
516
+ const { iframe, appendedScripts, contentWindow, registerPlugin } = buildBootstrapIframe({
517
+ gsap: { version: "3.14.2", registerPlugin: vi.fn() },
518
+ });
519
+ const meta = document.createElement("meta");
520
+ meta.name = "hyperframes-gsap-base-url";
521
+ meta.content = "http://localhost:8080/vendor/npm";
522
+ iframe.contentDocument!.head.appendChild(meta);
523
+ ensureMotionPathPluginLoaded(iframe);
524
+ ensureMotionPathPluginLoaded(iframe);
525
+ expect(appendedScripts).toHaveLength(1);
526
+ expect(appendedScripts[0]!.src).toBe(
527
+ "http://localhost:8080/vendor/npm/gsap@3.14.2/dist/MotionPathPlugin.min.js",
528
+ );
529
+ appendedScripts[0]!.onerror?.(new Event("error"));
530
+ expect(contentWindow.__hfMotionPathPluginLoading).toBe(false);
531
+ expect(registerPlugin).not.toHaveBeenCalled();
532
+ expect(appendedScripts).toHaveLength(1);
533
+ });
534
+
493
535
  it("appends the plugin script once and registers it on load", () => {
494
536
  const { iframe, contentWindow, appendedScripts, registerPlugin } = buildBootstrapIframe();
495
537
  ensureMotionPathPluginLoaded(iframe);
@@ -1,5 +1,5 @@
1
1
  import { COLOR_GRADING_SOURCE_HIDDEN_ATTR } from "@hyperframes/core/color-grading";
2
- import { motionPathPluginUrl } from "@hyperframes/core/gsap-cdn";
2
+ import { GSAP_BASE_URL_META, motionPathPluginUrl } from "@hyperframes/core/gsap-cdn";
3
3
  import { findAuthoredElement } from "./authoredSource";
4
4
  import { applyAuthoredInlineOpacity, readStampedAuthoredOpacity } from "./authoredOpacity";
5
5
  import {
@@ -50,6 +50,13 @@ type IframeWindow = Window & {
50
50
  // At most one preview waits for its runtime; a newer load replaces it, so a frame that never boots is not kept alive.
51
51
  let pendingReadyRetry: ((event: MessageEvent) => void) | null = null;
52
52
 
53
+ function previewMotionPathUrl(doc: Document, version?: string): string {
54
+ const baseUrl = doc.head.querySelector<HTMLMetaElement>(
55
+ `meta[name="${GSAP_BASE_URL_META}"]`,
56
+ )?.content;
57
+ return motionPathPluginUrl(version, baseUrl);
58
+ }
59
+
53
60
  export function ensureMotionPathPluginLoaded(iframe: HTMLIFrameElement | null): void {
54
61
  if (pendingReadyRetry) window.removeEventListener("message", pendingReadyRetry);
55
62
  pendingReadyRetry = null;
@@ -86,7 +93,7 @@ export function ensureMotionPathPluginLoaded(iframe: HTMLIFrameElement | null):
86
93
  try {
87
94
  win.__hfMotionPathPluginLoading = true;
88
95
  const pluginScript = doc.createElement("script");
89
- pluginScript.src = motionPathPluginUrl(win.gsap?.version);
96
+ pluginScript.src = previewMotionPathUrl(doc, win.gsap?.version);
90
97
  const finalize = () => {
91
98
  win.__hfMotionPathPluginLoading = false;
92
99
  try {
@@ -480,7 +487,7 @@ export function applySoftReload(
480
487
  }
481
488
  win.__hfMotionPathPluginLoading = true;
482
489
  const pluginScript = doc.createElement("script");
483
- pluginScript.src = motionPathPluginUrl(win.gsap?.version);
490
+ pluginScript.src = previewMotionPathUrl(doc, win.gsap?.version);
484
491
  pluginScript.onload = () => {
485
492
  win.__hfMotionPathPluginLoading = false;
486
493
  executeScript();