@hyperframes/studio 0.5.0-alpha.11 → 0.5.0-alpha.12

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/dist/index.html CHANGED
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
6
  <title>HyperFrames Studio</title>
7
- <script type="module" crossorigin src="/assets/index-Bl4Deziq.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-JhhmFie-.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-KioPDrX6.css">
9
9
  </head>
10
10
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.5.0-alpha.11",
3
+ "version": "0.5.0-alpha.12",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,8 +32,8 @@
32
32
  "@phosphor-icons/react": "^2.1.10",
33
33
  "codemirror": "^6.0.1",
34
34
  "motion": "^12.38.0",
35
- "@hyperframes/core": "0.5.0-alpha.11",
36
- "@hyperframes/player": "0.5.0-alpha.11"
35
+ "@hyperframes/core": "0.5.0-alpha.12",
36
+ "@hyperframes/player": "0.5.0-alpha.12"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/react": "^19.0.0",
@@ -47,7 +47,7 @@
47
47
  "vite": "^6.4.2",
48
48
  "vitest": "^3.2.4",
49
49
  "zustand": "^5.0.0",
50
- "@hyperframes/producer": "0.5.0-alpha.11"
50
+ "@hyperframes/producer": "0.5.0-alpha.12"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "react": "^18.0.0 || ^19.0.0",
package/src/App.tsx CHANGED
@@ -98,6 +98,10 @@ interface AppToast {
98
98
  tone: "error" | "info";
99
99
  }
100
100
 
101
+ function getTimelineElementLabel(element: TimelineElement): string {
102
+ return element.label || element.id || element.tag;
103
+ }
104
+
101
105
  type RightPanelTab = "design" | "renders";
102
106
 
103
107
  const GENERIC_FONT_FAMILIES = new Set([
@@ -900,7 +904,7 @@ export function StudioApp() {
900
904
  return (
901
905
  <CompositionThumbnail
902
906
  previewUrl={`/api/projects/${pid}/preview/comp/${compSrc}`}
903
- label={el.id || el.tag}
907
+ label={getTimelineElementLabel(el)}
904
908
  labelColor={style.label}
905
909
  accentColor={style.clip}
906
910
  seekTime={0}
@@ -915,7 +919,7 @@ export function StudioApp() {
915
919
  return (
916
920
  <CompositionThumbnail
917
921
  previewUrl={activePreviewUrl}
918
- label={el.id || el.tag}
922
+ label={getTimelineElementLabel(el)}
919
923
  labelColor={style.label}
920
924
  accentColor={style.clip}
921
925
  selector={el.selector}
@@ -953,7 +957,7 @@ export function StudioApp() {
953
957
  <AudioWaveform
954
958
  audioUrl={audioUrl}
955
959
  waveformUrl={waveformUrl}
956
- label={el.id || el.tag}
960
+ label={getTimelineElementLabel(el)}
957
961
  labelColor={style.label}
958
962
  />
959
963
  );
@@ -966,7 +970,7 @@ export function StudioApp() {
966
970
  return (
967
971
  <VideoThumbnail
968
972
  videoSrc={mediaSrc}
969
- label={el.id || el.tag}
973
+ label={getTimelineElementLabel(el)}
970
974
  labelColor={style.label}
971
975
  duration={el.duration}
972
976
  />
@@ -977,7 +981,7 @@ export function StudioApp() {
977
981
  return (
978
982
  <CompositionThumbnail
979
983
  previewUrl={`/api/projects/${pid}/preview`}
980
- label={el.id || el.tag}
984
+ label={getTimelineElementLabel(el)}
981
985
  labelColor={style.label}
982
986
  accentColor={style.clip}
983
987
  selector={el.selector}
@@ -1046,7 +1046,10 @@ export const Timeline = memo(function Timeline({
1046
1046
 
1047
1047
  const getPreviewElement = useCallback(
1048
1048
  (element: TimelineElement): TimelineElement => {
1049
- if (resizingClip?.element.id === element.id) {
1049
+ if (
1050
+ resizingClip &&
1051
+ (resizingClip.element.key ?? resizingClip.element.id) === (element.key ?? element.id)
1052
+ ) {
1050
1053
  return {
1051
1054
  ...element,
1052
1055
  start: resizingClip.previewStart,
@@ -1273,7 +1276,7 @@ export const Timeline = memo(function Timeline({
1273
1276
  draggedClip?.started === true && draggedElement
1274
1277
  ? getRenderedTimelineElement({
1275
1278
  element: draggedElement,
1276
- draggedElementId: draggedElement.id,
1279
+ draggedElementId: draggedElement.key ?? draggedElement.id,
1277
1280
  previewStart: draggedClip.previewStart,
1278
1281
  previewTrack: draggedClip.previewTrack,
1279
1282
  })
@@ -61,6 +61,7 @@ export const TimelineClip = memo(function TimelineClip({
61
61
  ? theme.clipShadowHover
62
62
  : theme.clipShadow;
63
63
  const capabilities = getTimelineEditCapabilities(el);
64
+ const displayLabel = el.label || el.id || el.tag;
64
65
  const showHandles = handleOpacity > 0.01;
65
66
  const baseBackgroundImage = isSelected ? theme.clipBackgroundActive : theme.clipBackground;
66
67
  const glossBackgroundImage = isSelected
@@ -106,7 +107,7 @@ export const TimelineClip = memo(function TimelineClip({
106
107
  title={
107
108
  isComposition
108
109
  ? `${el.compositionSrc} \u2022 Double-click to open`
109
- : `${el.id || el.tag} \u2022 ${el.start.toFixed(1)}s \u2013 ${(el.start + el.duration).toFixed(1)}s`
110
+ : `${displayLabel} \u2022 ${el.start.toFixed(1)}s \u2013 ${(el.start + el.duration).toFixed(1)}s`
110
111
  }
111
112
  onPointerEnter={onHoverStart}
112
113
  onPointerLeave={onHoverEnd}
@@ -53,4 +53,23 @@ describe("getRenderedTimelineElement", () => {
53
53
  }),
54
54
  ).toEqual({ ...element, start: 2.4, track: 3 });
55
55
  });
56
+
57
+ it("uses key before id when matching the dragged clip", () => {
58
+ const element = {
59
+ id: "Card",
60
+ key: "index.html:.card:1",
61
+ tag: "div",
62
+ start: 1,
63
+ duration: 2,
64
+ track: 0,
65
+ };
66
+ expect(
67
+ getRenderedTimelineElement({
68
+ element,
69
+ draggedElementId: "index.html:.card:1",
70
+ previewStart: 2.4,
71
+ previewTrack: 3,
72
+ }),
73
+ ).toEqual({ ...element, start: 2.4, track: 3 });
74
+ });
56
75
  });
@@ -130,7 +130,11 @@ export function getRenderedTimelineElement({
130
130
  previewStart: number | null;
131
131
  previewTrack: number | null;
132
132
  }): TimelineElement {
133
- if (element.id !== draggedElementId || previewStart === null || previewTrack === null) {
133
+ if (
134
+ (element.key ?? element.id) !== draggedElementId ||
135
+ previewStart === null ||
136
+ previewTrack === null
137
+ ) {
134
138
  return element;
135
139
  }
136
140
  return {
@@ -2,8 +2,10 @@ import { describe, expect, it } from "vitest";
2
2
  import { Window } from "happy-dom";
3
3
  import {
4
4
  buildStandaloneRootTimelineElement,
5
+ createTimelineElementFromManifestClip,
5
6
  findTimelineDomNodeForClip,
6
7
  getTimelineElementSelector,
8
+ parseTimelineFromDOM,
7
9
  type ClipManifestClip,
8
10
  mergeTimelineElementsPreservingDowngrades,
9
11
  resolveStandaloneRootCompositionSrc,
@@ -66,6 +68,7 @@ describe("buildStandaloneRootTimelineElement", () => {
66
68
  }),
67
69
  ).toEqual({
68
70
  id: "hero",
71
+ label: "hero",
69
72
  key: 'scenes/hero.html:[data-composition-id="hero"]:0',
70
73
  tag: "div",
71
74
  start: 0,
@@ -146,6 +149,83 @@ describe("findTimelineDomNodeForClip", () => {
146
149
  });
147
150
  });
148
151
 
152
+ describe("anonymous timeline identity", () => {
153
+ it("keeps fallback-parsed anonymous clips distinct when labels match", () => {
154
+ const doc = createDocument(`
155
+ <div data-composition-id="main" data-start="0" data-duration="8">
156
+ <div class="clip card" data-label="Card" data-start="0" data-duration="3" data-track-index="0"></div>
157
+ <div class="clip card" data-label="Card" data-start="3" data-duration="3" data-track-index="1"></div>
158
+ </div>
159
+ `);
160
+
161
+ const elements = parseTimelineFromDOM(doc, 8);
162
+
163
+ expect(elements).toHaveLength(2);
164
+ expect(elements.map((element) => element.label)).toEqual(["Card", "Card"]);
165
+ expect(new Set(elements.map((element) => element.id)).size).toBe(2);
166
+ expect(new Set(elements.map((element) => element.key)).size).toBe(2);
167
+ expect(elements.map((element) => element.selectorIndex)).toEqual([0, 1]);
168
+ });
169
+
170
+ it("keeps runtime-manifest anonymous clips distinct when labels match", () => {
171
+ const doc = createDocument(`
172
+ <div data-composition-id="main" data-start="0" data-duration="8">
173
+ <div class="clip card" data-start="0" data-duration="3" data-track-index="0"></div>
174
+ <div class="clip card" data-start="3" data-duration="3" data-track-index="1"></div>
175
+ </div>
176
+ `);
177
+ const clips = [
178
+ createClip({ id: null, label: "Card", start: 0, duration: 3, track: 0 }),
179
+ createClip({ id: null, label: "Card", start: 3, duration: 3, track: 1 }),
180
+ ];
181
+ const used = new Set<Element>();
182
+ const elements = clips.map((clip, index) => {
183
+ const hostEl = findTimelineDomNodeForClip(doc, clip, index, used);
184
+ if (hostEl) used.add(hostEl);
185
+ return createTimelineElementFromManifestClip({
186
+ clip,
187
+ fallbackIndex: index,
188
+ doc,
189
+ hostEl,
190
+ });
191
+ });
192
+
193
+ expect(elements.map((element) => element.label)).toEqual(["Card", "Card"]);
194
+ expect(new Set(elements.map((element) => element.id)).size).toBe(2);
195
+ expect(new Set(elements.map((element) => element.key)).size).toBe(2);
196
+ expect(elements.map((element) => element.selectorIndex)).toEqual([0, 1]);
197
+ });
198
+
199
+ it("reads media metadata from owner-window media elements", () => {
200
+ const doc = createDocument(`
201
+ <div data-composition-id="main" data-start="0" data-duration="8">
202
+ <div class="clip video-card" data-start="0" data-duration="3" data-track-index="0">
203
+ <video src="/clip.mp4" data-source-duration="12"></video>
204
+ </div>
205
+ </div>
206
+ `);
207
+ const hostEl = doc.querySelector(".video-card");
208
+ const video = hostEl?.querySelector("video");
209
+ if (!hostEl || !video) throw new Error("missing video test fixture");
210
+ Object.defineProperty(video, "defaultPlaybackRate", {
211
+ value: 1.5,
212
+ configurable: true,
213
+ });
214
+
215
+ const element = createTimelineElementFromManifestClip({
216
+ clip: createClip({ kind: "video", tagName: "div" }),
217
+ fallbackIndex: 0,
218
+ doc,
219
+ hostEl,
220
+ });
221
+
222
+ expect(element.tag).toBe("video");
223
+ expect(element.src).toBe("/clip.mp4");
224
+ expect(element.sourceDuration).toBe(12);
225
+ expect(element.playbackRate).toBe(1.5);
226
+ });
227
+ });
228
+
149
229
  describe("mergeTimelineElementsPreservingDowngrades", () => {
150
230
  it("preserves missing current elements when a shorter manifest arrives", () => {
151
231
  expect(
@@ -174,6 +254,65 @@ describe("mergeTimelineElementsPreservingDowngrades", () => {
174
254
  ),
175
255
  ).toEqual([{ id: "hero", tag: "div", start: 0, duration: 4, track: 0 }]);
176
256
  });
257
+
258
+ it("preserves distinct anonymous clips that share the same friendly id label", () => {
259
+ expect(
260
+ mergeTimelineElementsPreservingDowngrades(
261
+ [
262
+ {
263
+ id: "Card",
264
+ key: "index.html:.card:0",
265
+ label: "Card",
266
+ tag: "div",
267
+ start: 0,
268
+ duration: 3,
269
+ track: 0,
270
+ },
271
+ {
272
+ id: "Card",
273
+ key: "index.html:.card:1",
274
+ label: "Card",
275
+ tag: "div",
276
+ start: 3,
277
+ duration: 3,
278
+ track: 1,
279
+ },
280
+ ],
281
+ [
282
+ {
283
+ id: "Card",
284
+ key: "index.html:.card:0",
285
+ label: "Card",
286
+ tag: "div",
287
+ start: 0,
288
+ duration: 3,
289
+ track: 0,
290
+ },
291
+ ],
292
+ 8,
293
+ 8,
294
+ ),
295
+ ).toEqual([
296
+ {
297
+ id: "Card",
298
+ key: "index.html:.card:0",
299
+ label: "Card",
300
+ tag: "div",
301
+ start: 0,
302
+ duration: 3,
303
+ track: 0,
304
+ },
305
+ {
306
+ id: "Card",
307
+ key: "index.html:.card:1",
308
+ label: "Card",
309
+ tag: "div",
310
+ start: 3,
311
+ duration: 3,
312
+ track: 1,
313
+ },
314
+ ]);
315
+ });
177
316
  });
178
317
 
179
318
  describe("shouldIgnorePlaybackShortcutTarget", () => {