@hyperframes/studio 0.8.149 → 0.8.150

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-ByJU5J6n.js";/*!
1
+ import{n as Qi}from"./index-CS_m0TkX.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
package/dist/index.d.ts CHANGED
@@ -354,9 +354,10 @@ interface TimelineElement {
354
354
  /** Clip-edge fades from `data-fade-in` / `data-fade-out`, seconds; absent means none. */
355
355
  fadeIn?: number;
356
356
  fadeOut?: number;
357
- /** Verbatim `data-fx-chain` / `data-automation`; see automationLaneData. */
357
+ /** Verbatim `data-fx-chain` / `data-automation` (`masterChain`: the root's); see automationLaneData. */
358
358
  fxChain?: string;
359
359
  automation?: string;
360
+ masterChain?: string;
360
361
  /** Path from data-composition-src — identifies sub-composition elements */
361
362
  compositionSrc?: string;
362
363
  /** Set by data-timeline-locked on the host element — disables move and trim in Studio. */
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-ByJU5J6n.js"></script>
17
+ <script type="module" crossorigin src="/assets/index-CS_m0TkX.js"></script>
18
18
  <link rel="stylesheet" crossorigin href="/assets/index-DkQKsFxx.css">
19
19
  </head>
20
20
  <body>
package/dist/index.js CHANGED
@@ -29115,6 +29115,7 @@ import { useCallback as useCallback38, useRef as useRef46 } from "react";
29115
29115
  // src/player/lib/timelineDOM.ts
29116
29116
  import { resolveCssStackingContextId } from "@hyperframes/core/runtime/stacking-context";
29117
29117
  import { readClipTiming as readClipTiming2 } from "@hyperframes/core/composition-contract";
29118
+ import { readMasterFxChain } from "@hyperframes/core/audio-groups";
29118
29119
  import { linkScopeOf } from "@hyperframes/core/media-link";
29119
29120
  import { createRuntimeStartTimeResolver as createRuntimeStartTimeResolver2 } from "@hyperframes/core/runtime/start-resolver";
29120
29121
 
@@ -29693,6 +29694,8 @@ function createTimelineElementFromManifestClip(params) {
29693
29694
  if (info.fxChain) entry2.audioGroupFxChain = info.fxChain;
29694
29695
  if (info.automation) entry2.audioGroupAutomation = info.automation;
29695
29696
  }
29697
+ const masterChain = readMasterFxChain(doc ?? hostEl.ownerDocument);
29698
+ if (masterChain) entry2.masterChain = masterChain;
29696
29699
  const fxChain = hostEl.getAttribute("data-fx-chain");
29697
29700
  if (fxChain) entry2.fxChain = fxChain;
29698
29701
  const automation = hostEl.getAttribute("data-automation");
@@ -29815,6 +29818,8 @@ function parseTimelineFromDOM(doc, rootDuration, timelines) {
29815
29818
  const resolvedSrc = mediaEl.src || void 0;
29816
29819
  if (resolvedSrc) entry2.src = resolvedSrc;
29817
29820
  }
29821
+ const domMasterChain = readMasterFxChain(doc);
29822
+ if (domMasterChain) entry2.masterChain = domMasterChain;
29818
29823
  const domFxChain = el.getAttribute("data-fx-chain");
29819
29824
  if (domFxChain) entry2.fxChain = domFxChain;
29820
29825
  const domAutomation = el.getAttribute("data-automation");
@@ -74831,7 +74836,9 @@ function claimHeld(reply, label) {
74831
74836
  if (reply.ok)
74832
74837
  return reply.body?.claimed?.id ?? null;
74833
74838
  if (reply.status !== 404)
74834
- console.error(`"${label}" was not recorded as your edit: ${reply.error}`);
74839
+ console.error(
74840
+ `[history] ${reply.status}: "${label}" was not recorded as your edit: ${reply.error}`
74841
+ );
74835
74842
  return null;
74836
74843
  }
74837
74844
  async function overwroteVersions(files) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.8.149",
3
+ "version": "0.8.150",
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.149",
64
- "@hyperframes/player": "0.8.149",
65
- "@hyperframes/parsers": "0.8.149",
66
- "@hyperframes/shader-transitions": "0.8.149",
67
- "@hyperframes/sdk": "0.8.149",
68
- "@hyperframes/studio-server": "0.8.149"
63
+ "@hyperframes/parsers": "0.8.150",
64
+ "@hyperframes/player": "0.8.150",
65
+ "@hyperframes/sdk": "0.8.150",
66
+ "@hyperframes/studio-server": "0.8.150",
67
+ "@hyperframes/core": "0.8.150",
68
+ "@hyperframes/shader-transitions": "0.8.150"
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.149"
85
+ "@hyperframes/producer": "0.8.150"
86
86
  },
87
87
  "peerDependencies": {
88
88
  "react": "19",
@@ -561,7 +561,9 @@ it("a claim the server refused is logged, and the write stays undoable as a chan
561
561
  files: { "index.html": { before: "A", after: "B" } },
562
562
  }),
563
563
  );
564
- expect(logged).toHaveBeenCalledWith('"Moved Title" was not recorded as your edit: disk full');
564
+ expect(logged).toHaveBeenCalledWith(
565
+ '[history] 500: "Moved Title" was not recorded as your edit: disk full',
566
+ );
565
567
  restoreFetch();
566
568
  expect(await act(() => hook().undo({ readFile }))).toMatchObject({
567
569
  ok: true,
@@ -570,6 +572,46 @@ it("a claim the server refused is logged, and the write stays undoable as a chan
570
572
  expect(file()).toBe("A");
571
573
  });
572
574
 
575
+ it.each([
576
+ [
577
+ "the server refused",
578
+ 409,
579
+ () => answer("/history/claim", 409, { error: "newer edit" }),
580
+ "newer edit",
581
+ ],
582
+ [
583
+ "could not reach the server",
584
+ 0,
585
+ () => {
586
+ const real = globalThis.fetch;
587
+ vi.stubGlobal("fetch", (url: string, init?: RequestInit) =>
588
+ url.endsWith("/history/claim")
589
+ ? Promise.reject(new TypeError("fetch failed"))
590
+ : real(url, init),
591
+ );
592
+ },
593
+ "Studio could not reach its server.",
594
+ ],
595
+ ] as const)(
596
+ "a claim that %s logs under [history] with its status",
597
+ async (_, status, stub, reason) => {
598
+ const { hook, save } = await studio();
599
+ const logged = vi.spyOn(console, "error").mockImplementation(() => {});
600
+ cleanup.push(() => logged.mockRestore());
601
+ stub();
602
+ save("B");
603
+ await act(() =>
604
+ hook().recordEdit({
605
+ label: "Moved Title",
606
+ files: { "index.html": { before: "A", after: "B" } },
607
+ }),
608
+ );
609
+ expect(logged).toHaveBeenCalledWith(
610
+ `[history] ${status}: "Moved Title" was not recorded as your edit: ${reason}`,
611
+ );
612
+ },
613
+ );
614
+
573
615
  it("a step whose reply cannot be read says so, instead of throwing", async () => {
574
616
  const { hook, readFile } = await studio();
575
617
  answer("/history/step", 200, "<html>");
@@ -154,7 +154,9 @@ function claimHeld(reply: Awaited<ReturnType<typeof post>>, label: string): stri
154
154
  if (reply.ok)
155
155
  return (reply.body as { claimed: { id: string } | null } | null)?.claimed?.id ?? null;
156
156
  if (reply.status !== 404)
157
- console.error(`"${label}" was not recorded as your edit: ${reply.error}`);
157
+ console.error(
158
+ `[history] ${reply.status}: "${label}" was not recorded as your edit: ${reply.error}`,
159
+ );
158
160
  return null;
159
161
  }
160
162
 
@@ -632,6 +632,61 @@ describe("audio FX attributes on parsed elements", () => {
632
632
  });
633
633
  });
634
634
 
635
+ describe("the master chain on parsed elements", () => {
636
+ const CHAIN = '{"version":1,"nodes":[{"type":"truepeak","id":"m1","params":{}}]}';
637
+ const parseVideo = (rootAttrs: string, inner = ""): TimelineElement | undefined => {
638
+ const doc = new DOMParser().parseFromString(
639
+ `<div data-composition-id="main" data-start="0" data-duration="10" ${rootAttrs}>
640
+ <video id="talk" data-start="0" data-duration="10" data-has-audio="true"></video>${inner}
641
+ </div>`,
642
+ "text/html",
643
+ );
644
+ return parseTimelineFromDOM(doc, 10).find((e) => e.domId === "talk");
645
+ };
646
+
647
+ it("copies the root's data-fx-chain onto an element that has none of its own", () => {
648
+ const talk = parseVideo(`data-fx-chain='${CHAIN}'`);
649
+ expect(talk?.masterChain).toBe(CHAIN);
650
+ expect(talk?.fxChain).toBeUndefined();
651
+ });
652
+
653
+ it("leaves it unset when the root carries none", () => {
654
+ expect(parseVideo("")?.masterChain).toBeUndefined();
655
+ });
656
+
657
+ it("ignores a sub-composition root's chain", () => {
658
+ const inner = `<div data-composition-id="inner" data-fx-chain='${CHAIN}'></div>`;
659
+ expect(parseVideo("", inner)?.masterChain).toBeUndefined();
660
+ });
661
+
662
+ it("carries it on the manifest path too", () => {
663
+ const doc = makeDoc(`
664
+ <div data-composition-id="main" data-fx-chain='${CHAIN}'>
665
+ <video id="talk" data-has-audio="true"></video>
666
+ </div>
667
+ `);
668
+ const element = createTimelineElementFromManifestClip({
669
+ clip: {
670
+ id: "talk",
671
+ label: "Talk",
672
+ kind: "video",
673
+ tagName: "video",
674
+ start: 0,
675
+ duration: 10,
676
+ track: 0,
677
+ compositionId: null,
678
+ parentCompositionId: "main",
679
+ compositionSrc: null,
680
+ assetUrl: null,
681
+ },
682
+ fallbackIndex: 0,
683
+ doc,
684
+ hostEl: doc.getElementById("talk"),
685
+ });
686
+ expect(element.masterChain).toBe(CHAIN);
687
+ });
688
+ });
689
+
635
690
  // A composition clip trimmed by 30 px collapsed to its first nested video's 1.77 s: that length capped the trim.
636
691
  describe("a composition clip's source length", () => {
637
692
  const scene = `
@@ -12,6 +12,7 @@ import type { TimelineElement } from "../store/playerStore";
12
12
  import type { ClipManifestClip, IframeWindow, TimelineLike } from "./playbackTypes";
13
13
  import { resolveCssStackingContextId } from "@hyperframes/core/runtime/stacking-context";
14
14
  import { readClipTiming } from "@hyperframes/core/composition-contract";
15
+ import { readMasterFxChain } from "@hyperframes/core/audio-groups";
15
16
  import { linkScopeOf } from "@hyperframes/core/media-link";
16
17
  import { createRuntimeStartTimeResolver } from "@hyperframes/core/runtime/start-resolver";
17
18
  import { groupInfoFor } from "./timelineGroupInfo";
@@ -167,6 +168,8 @@ export function createTimelineElementFromManifestClip(params: {
167
168
  if (info.fxChain) entry.audioGroupFxChain = info.fxChain;
168
169
  if (info.automation) entry.audioGroupAutomation = info.automation;
169
170
  }
171
+ const masterChain = readMasterFxChain(doc ?? hostEl.ownerDocument);
172
+ if (masterChain) entry.masterChain = masterChain;
170
173
  const fxChain = hostEl.getAttribute("data-fx-chain");
171
174
  if (fxChain) entry.fxChain = fxChain;
172
175
  const automation = hostEl.getAttribute("data-automation");
@@ -330,6 +333,8 @@ export function parseTimelineFromDOM(
330
333
  // Read from the element, like the manifest path does: without these an audio
331
334
  // clip parsed straight from the DOM reserved no automation height and drew no
332
335
  // lanes, while the property panel still showed its chain.
336
+ const domMasterChain = readMasterFxChain(doc);
337
+ if (domMasterChain) entry.masterChain = domMasterChain;
333
338
  const domFxChain = el.getAttribute("data-fx-chain");
334
339
  if (domFxChain) entry.fxChain = domFxChain;
335
340
  const domAutomation = el.getAttribute("data-automation");
@@ -62,9 +62,10 @@ export interface TimelineElement {
62
62
  /** Clip-edge fades from `data-fade-in` / `data-fade-out`, seconds; absent means none. */
63
63
  fadeIn?: number;
64
64
  fadeOut?: number;
65
- /** Verbatim `data-fx-chain` / `data-automation`; see automationLaneData. */
65
+ /** Verbatim `data-fx-chain` / `data-automation` (`masterChain`: the root's); see automationLaneData. */
66
66
  fxChain?: string;
67
67
  automation?: string;
68
+ masterChain?: string;
68
69
  /** Path from data-composition-src — identifies sub-composition elements */
69
70
  compositionSrc?: string;
70
71
  /** Set by data-timeline-locked on the host element — disables move and trim in Studio. */
@@ -69,6 +69,15 @@ describe("hasProjectAudio", () => {
69
69
  expect(hasProjectAudio([clip({ tag: "video", hasAudio: true, ...fields })])).toBe(true);
70
70
  });
71
71
 
72
+ it("counts an audible video when the composition root carries a master chain", () => {
73
+ const masterChain = '{"version":1,"nodes":[]}';
74
+ expect(hasProjectAudio([clip({ tag: "video", hasAudio: true, masterChain })])).toBe(true);
75
+ expect(
76
+ hasProjectAudio([clip({ tag: "video", muted: true, hasAudio: true, masterChain })]),
77
+ ).toBe(false);
78
+ expect(hasProjectAudio([clip({ tag: "video", masterChain })])).toBe(false);
79
+ });
80
+
72
81
  it("ignores a muted video even when it carries processing", () => {
73
82
  expect(hasProjectAudio([clip({ tag: "video", hasAudio: true, muted: true, volume: 2 })])).toBe(
74
83
  false,