@hyperframes/studio 0.6.117 → 0.6.118

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 +1 @@
1
- import{g as P}from"./index-CWpmpjkU.js";function j(c,d){for(var s=0;s<d.length;s++){const a=d[s];if(typeof a!="string"&&!Array.isArray(a)){for(const i in a)if(i!=="default"&&!(i in c)){const l=Object.getOwnPropertyDescriptor(a,i);l&&Object.defineProperty(c,i,l.get?l:{enumerable:!0,get:()=>a[i]})}}}return Object.freeze(Object.defineProperty(c,Symbol.toStringTag,{value:"Module"}))}var v={},w;function k(){if(w)return v;w=1,Object.defineProperty(v,"__esModule",{value:!0}),v.default=d;var c=window.OfflineAudioContext||window.webkitOfflineAudioContext;function d(e){var r=a(e);return r.start(0),[i,y,O(e.sampleRate),s].reduce(function(t,o){return o(t)},r.buffer.getChannelData(0))}function s(e){return e.sort(function(r,t){return t.count-r.count}).splice(0,5)[0].tempo}function a(e){var r=e.length,t=e.numberOfChannels,o=e.sampleRate,n=new c(t,r,o),u=n.createBufferSource();u.buffer=e;var f=n.createBiquadFilter();return f.type="lowpass",u.connect(f),f.connect(n.destination),u}function i(e){for(var r=[],t=.9,o=.3,n=15;r.length<n&&t>=o;)r=l(e,t),t-=.05;if(r.length<n)throw new Error("Could not find enough samples for a reliable detection.");return r}function l(e,r){for(var t=[],o=0,n=e.length;o<n;o+=1)e[o]>r&&(t.push(o),o+=1e4);return t}function y(e){var r=[];return e.forEach(function(t,o){for(var n=function(x){var g=e[o+x]-t,_=r.some(function(h){if(h.interval===g)return h.count+=1});_||r.push({interval:g,count:1})},u=0;u<10;u+=1)n(u)}),r}function O(e){return function(r){var t=[];return r.forEach(function(o){if(o.interval!==0){for(var n=60/(o.interval/e);n<90;)n*=2;for(;n>180;)n/=2;n=Math.round(n);var u=t.some(function(f){if(f.tempo===n)return f.count+=o.count});u||t.push({tempo:n,count:o.count})}}),t}}return v}var p,b;function q(){return b||(b=1,p=k().default),p}var m=q();const A=P(m),D=j({__proto__:null,default:A},[m]);export{D as i};
1
+ import{g as P}from"./index-CUtjhezZ.js";function j(c,d){for(var s=0;s<d.length;s++){const a=d[s];if(typeof a!="string"&&!Array.isArray(a)){for(const i in a)if(i!=="default"&&!(i in c)){const l=Object.getOwnPropertyDescriptor(a,i);l&&Object.defineProperty(c,i,l.get?l:{enumerable:!0,get:()=>a[i]})}}}return Object.freeze(Object.defineProperty(c,Symbol.toStringTag,{value:"Module"}))}var v={},w;function k(){if(w)return v;w=1,Object.defineProperty(v,"__esModule",{value:!0}),v.default=d;var c=window.OfflineAudioContext||window.webkitOfflineAudioContext;function d(e){var r=a(e);return r.start(0),[i,y,O(e.sampleRate),s].reduce(function(t,o){return o(t)},r.buffer.getChannelData(0))}function s(e){return e.sort(function(r,t){return t.count-r.count}).splice(0,5)[0].tempo}function a(e){var r=e.length,t=e.numberOfChannels,o=e.sampleRate,n=new c(t,r,o),u=n.createBufferSource();u.buffer=e;var f=n.createBiquadFilter();return f.type="lowpass",u.connect(f),f.connect(n.destination),u}function i(e){for(var r=[],t=.9,o=.3,n=15;r.length<n&&t>=o;)r=l(e,t),t-=.05;if(r.length<n)throw new Error("Could not find enough samples for a reliable detection.");return r}function l(e,r){for(var t=[],o=0,n=e.length;o<n;o+=1)e[o]>r&&(t.push(o),o+=1e4);return t}function y(e){var r=[];return e.forEach(function(t,o){for(var n=function(x){var g=e[o+x]-t,_=r.some(function(h){if(h.interval===g)return h.count+=1});_||r.push({interval:g,count:1})},u=0;u<10;u+=1)n(u)}),r}function O(e){return function(r){var t=[];return r.forEach(function(o){if(o.interval!==0){for(var n=60/(o.interval/e);n<90;)n*=2;for(;n>180;)n/=2;n=Math.round(n);var u=t.some(function(f){if(f.tempo===n)return f.count+=o.count});u||t.push({tempo:n,count:o.count})}}),t}}return v}var p,b;function q(){return b||(b=1,p=k().default),p}var m=q();const A=P(m),D=j({__proto__:null,default:A},[m]);export{D as i};
package/dist/index.html CHANGED
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
6
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
7
  <title>HyperFrames Studio</title>
8
- <script type="module" crossorigin src="/assets/index-CWpmpjkU.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-CUtjhezZ.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-BvEhf7SS.css">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.6.117",
3
+ "version": "0.6.118",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -36,9 +36,9 @@
36
36
  "dompurify": "^3.2.4",
37
37
  "marked": "^14.1.4",
38
38
  "mediabunny": "^1.45.3",
39
- "@hyperframes/core": "0.6.117",
40
- "@hyperframes/sdk": "0.6.117",
41
- "@hyperframes/player": "0.6.117"
39
+ "@hyperframes/core": "0.6.118",
40
+ "@hyperframes/player": "0.6.118",
41
+ "@hyperframes/sdk": "0.6.118"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/react": "19",
@@ -52,7 +52,7 @@
52
52
  "vite": "^6.4.2",
53
53
  "vitest": "^3.2.4",
54
54
  "zustand": "^5.0.0",
55
- "@hyperframes/producer": "0.6.117"
55
+ "@hyperframes/producer": "0.6.118"
56
56
  },
57
57
  "peerDependencies": {
58
58
  "react": "19",
@@ -11,6 +11,10 @@ export function useFileManagerContext(): FileManagerValue {
11
11
  return ctx;
12
12
  }
13
13
 
14
+ export function useFileManagerContextOptional(): FileManagerValue | null {
15
+ return useContext(FileManagerContext);
16
+ }
17
+
14
18
  export function FileManagerProvider({
15
19
  value: {
16
20
  editingFile,
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef } from "react";
2
2
  import { usePlayerStore } from "../player/store/playerStore";
3
3
  import { isMusicTrack } from "../utils/timelineInspector";
4
4
  import { analyzeMusicFromUrl } from "@hyperframes/core/beats";
5
- import { useFileManagerContext } from "../contexts/FileManagerContext";
5
+ import { useFileManagerContextOptional } from "../contexts/FileManagerContext";
6
6
  import { mergeUserBeats } from "../utils/beatEditing";
7
7
  import {
8
8
  audioRelPathForSrc,
@@ -58,11 +58,18 @@ export function useMusicBeatAnalysis(): void {
58
58
  const setBeatEdits = usePlayerStore((s) => s.setBeatEdits);
59
59
  const setBeatPersist = usePlayerStore((s) => s.setBeatPersist);
60
60
  const resetBeatHistory = usePlayerStore((s) => s.resetBeatHistory);
61
- const { readOptionalProjectFile, writeProjectFile } = useFileManagerContext();
61
+ const fileManager = useFileManagerContextOptional();
62
+ const readOptionalProjectFile = fileManager?.readOptionalProjectFile;
63
+ const writeProjectFile = fileManager?.writeProjectFile;
62
64
 
63
65
  // File IO via ref so the effects only re-run when the track changes.
64
- const ioRef = useRef({ readOptionalProjectFile, writeProjectFile });
65
- ioRef.current = { readOptionalProjectFile, writeProjectFile };
66
+ const ioRef = useRef<
67
+ (ProjectIo & { writeProjectFile: (p: string, c: string) => Promise<void> }) | null
68
+ >(null);
69
+ ioRef.current =
70
+ readOptionalProjectFile && writeProjectFile
71
+ ? { readOptionalProjectFile, writeProjectFile }
72
+ : null;
66
73
 
67
74
  const musicSrc = useMemo(() => {
68
75
  const el = elements.find((e) => isMusicTrack(e));
@@ -78,6 +85,12 @@ export function useMusicBeatAnalysis(): void {
78
85
  resetBeatHistory();
79
86
  return;
80
87
  }
88
+ if (!ioRef.current) {
89
+ setBeatAnalysis(null);
90
+ setBeatEdits(null);
91
+ resetBeatHistory();
92
+ return;
93
+ }
81
94
  let cancelled = false;
82
95
 
83
96
  let promise = analysisCache.get(musicSrc);
@@ -90,7 +103,9 @@ export function useMusicBeatAnalysis(): void {
90
103
  promise
91
104
  .then(async (analysis) => {
92
105
  const detected = { times: analysis.beatTimes, strengths: analysis.beatStrengths };
93
- const { times, strengths, hasFile } = await resolveBeats(beatPath, detected, ioRef.current);
106
+ const io = ioRef.current;
107
+ if (!io) return;
108
+ const { times, strengths, hasFile } = await resolveBeats(beatPath, detected, io);
94
109
  if (cancelled) return;
95
110
  setBeatEdits(null);
96
111
  resetBeatHistory();
@@ -114,10 +129,11 @@ export function useMusicBeatAnalysis(): void {
114
129
  // Flushes any pending write on cleanup so the last edit is never lost. ──
115
130
  useEffect(() => {
116
131
  const beatPath = beatFilePathForSrc(musicSrc);
117
- if (!musicSrc || !beatPath) {
132
+ if (!musicSrc || !beatPath || !ioRef.current) {
118
133
  setBeatPersist(null);
119
134
  return;
120
135
  }
136
+ const io = ioRef.current;
121
137
  const audio = audioRelPathForSrc(musicSrc) ?? "audio";
122
138
  let timer: ReturnType<typeof setTimeout> | null = null;
123
139
  let pending: string | null = null;
@@ -126,7 +142,7 @@ export function useMusicBeatAnalysis(): void {
126
142
  if (pending === null) return;
127
143
  const content = pending;
128
144
  pending = null;
129
- void ioRef.current.writeProjectFile(beatPath, content).catch(() => {});
145
+ void io.writeProjectFile(beatPath, content).catch(() => {});
130
146
  };
131
147
 
132
148
  const persist = () => {
@@ -1,5 +1,11 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import React, { act } from "react";
4
+ import { createRoot } from "react-dom/client";
5
+ import { afterEach } from "vitest";
1
6
  import { describe, it, expect } from "vitest";
2
7
  import {
8
+ Timeline,
3
9
  formatTimelineTickLabel,
4
10
  generateTicks,
5
11
  getDefaultDroppedTrack,
@@ -14,6 +20,41 @@ import {
14
20
  } from "./Timeline";
15
21
  import { RULER_H, TRACK_H } from "./timelineLayout";
16
22
  import { formatTime } from "../lib/time";
23
+ import { usePlayerStore } from "../store/playerStore";
24
+
25
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
26
+
27
+ afterEach(() => {
28
+ document.body.innerHTML = "";
29
+ usePlayerStore.getState().reset();
30
+ });
31
+
32
+ describe("Timeline provider boundary", () => {
33
+ it("renders the public Timeline export without TimelineEditProvider", () => {
34
+ const host = document.createElement("div");
35
+ document.body.append(host);
36
+ Object.defineProperty(host, "clientWidth", {
37
+ configurable: true,
38
+ value: 640,
39
+ });
40
+
41
+ usePlayerStore.setState({
42
+ duration: 4,
43
+ timelineReady: true,
44
+ elements: [{ id: "clip-1", tag: "div", start: 0, duration: 2, track: 0 }],
45
+ });
46
+
47
+ const root = createRoot(host);
48
+
49
+ expect(() => {
50
+ act(() => {
51
+ root.render(React.createElement(Timeline));
52
+ });
53
+ }).not.toThrow();
54
+
55
+ act(() => root.unmount());
56
+ });
57
+ });
17
58
 
18
59
  describe("generateTicks", () => {
19
60
  it("returns empty arrays for duration <= 0", () => {
@@ -21,7 +21,7 @@ import type { DraggedClipState, ResizingClipState, BlockedClipState } from "./us
21
21
  import type { TrackVisualStyle } from "./timelineIcons";
22
22
  import { STUDIO_KEYFRAMES_ENABLED } from "../../components/editor/manualEditingAvailability";
23
23
  import { SPLIT_BOUNDARY_EPSILON_S } from "../../utils/timelineElementSplit";
24
- import { useTimelineEditContext } from "../../contexts/TimelineEditContext";
24
+ import { useTimelineEditContextOptional } from "../../contexts/TimelineEditContext";
25
25
  import { isMusicTrack } from "../../utils/timelineInspector";
26
26
 
27
27
  function ClipLabel({ element, color }: { element: TimelineElement; color: string }) {
@@ -151,7 +151,7 @@ export const TimelineCanvas = memo(function TimelineCanvas({
151
151
  beatAnalysis,
152
152
  }: TimelineCanvasProps) {
153
153
  const { onResizeElement, onMoveElement, onRazorSplit, onRazorSplitAll } =
154
- useTimelineEditContext();
154
+ useTimelineEditContextOptional();
155
155
  const beatDragging = usePlayerStore((s) => s.beatDragging);
156
156
  const draggedElement = draggedClip?.element ?? null;
157
157
  const activeDraggedElement =
@@ -1,5 +1,5 @@
1
1
  import { useMemo } from "react";
2
- import { useTimelineEditContext } from "../../contexts/TimelineEditContext";
2
+ import { useTimelineEditContextOptional } from "../../contexts/TimelineEditContext";
3
3
  import type { TimelineEditCallbacks } from "./timelineCallbacks";
4
4
 
5
5
  // Props a parent (e.g. NLELayout) may pass to <Timeline> to intercept edits —
@@ -15,7 +15,7 @@ export type TimelineEditOverrides = Pick<
15
15
  export function useResolvedTimelineEditCallbacks(
16
16
  overrides: TimelineEditOverrides,
17
17
  ): TimelineEditCallbacks {
18
- const ctx = useTimelineEditContext();
18
+ const ctx = useTimelineEditContextOptional();
19
19
  const { onMoveElement, onResizeElement, onBlockedEditAttempt, onSplitElement } = overrides;
20
20
  return useMemo(
21
21
  () => ({