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

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.
@@ -64,9 +64,12 @@ function wrapTimeline(tl: TimelineLike): PlaybackAdapter {
64
64
  }
65
65
 
66
66
  function resolveMediaElement(el: Element): HTMLMediaElement | HTMLImageElement | null {
67
- if (el instanceof HTMLMediaElement || el instanceof HTMLImageElement) return el;
67
+ const win = el.ownerDocument.defaultView ?? window;
68
+ const MediaElementCtor = win.HTMLMediaElement ?? globalThis.HTMLMediaElement;
69
+ const ImageElementCtor = win.HTMLImageElement ?? globalThis.HTMLImageElement;
70
+ if (el instanceof MediaElementCtor || el instanceof ImageElementCtor) return el;
68
71
  const candidate = el.querySelector("video, audio, img");
69
- return candidate instanceof HTMLMediaElement || candidate instanceof HTMLImageElement
72
+ return candidate instanceof MediaElementCtor || candidate instanceof ImageElementCtor
70
73
  ? candidate
71
74
  : null;
72
75
  }
@@ -92,7 +95,9 @@ function applyMediaMetadataFromElement(entry: TimelineElement, el: Element): voi
92
95
  const src = mediaEl.getAttribute("src");
93
96
  if (src) entry.src = src;
94
97
 
95
- if (!(mediaEl instanceof HTMLMediaElement)) return;
98
+ const win = mediaEl.ownerDocument.defaultView ?? window;
99
+ const MediaElementCtor = win.HTMLMediaElement ?? globalThis.HTMLMediaElement;
100
+ if (typeof MediaElementCtor === "undefined" || !(mediaEl instanceof MediaElementCtor)) return;
96
101
 
97
102
  const sourceDurationAttr =
98
103
  el.getAttribute("data-source-duration") ?? mediaEl.getAttribute("data-source-duration");
@@ -165,11 +170,24 @@ export function shouldIgnorePlaybackShortcutEvent(
165
170
  );
166
171
  }
167
172
 
173
+ function getTimelineElementDisplayLabel(input: {
174
+ id?: string | null;
175
+ label?: string | null;
176
+ tag?: string | null;
177
+ }): string {
178
+ const label = input.label?.trim();
179
+ if (label) return label;
180
+ const id = input.id?.trim();
181
+ if (id) return id;
182
+ const tag = input.tag?.trim().toLowerCase();
183
+ return tag ? `${tag} clip` : "Timeline clip";
184
+ }
185
+
168
186
  /**
169
187
  * Parse [data-start] elements from a Document into TimelineElement[].
170
188
  * Shared helper — used by onIframeLoad fallback, handleMessage, and enrichMissingCompositions.
171
189
  */
172
- function parseTimelineFromDOM(doc: Document, rootDuration: number): TimelineElement[] {
190
+ export function parseTimelineFromDOM(doc: Document, rootDuration: number): TimelineElement[] {
173
191
  const rootComp = doc.querySelector("[data-composition-id]");
174
192
  const nodes = doc.querySelectorAll("[data-start]");
175
193
  const els: TimelineElement[] = [];
@@ -200,17 +218,24 @@ function parseTimelineFromDOM(doc: Document, rootDuration: number): TimelineElem
200
218
  const selector = getTimelineElementSelector(el);
201
219
  const sourceFile = getTimelineElementSourceFile(el);
202
220
  const selectorIndex = getTimelineElementSelectorIndex(doc, el, selector);
203
- const id = el.id || compId || el.className?.split(" ")[0] || tagLower;
221
+ const label = getTimelineElementDisplayLabel({
222
+ id: el.id || compId || null,
223
+ label: el.getAttribute("data-timeline-label") ?? el.getAttribute("data-label"),
224
+ tag: tagLower,
225
+ });
226
+ const identity = buildTimelineElementIdentity({
227
+ preferredId: el.id || compId || null,
228
+ label,
229
+ fallbackIndex: els.length,
230
+ domId: el.id || undefined,
231
+ selector,
232
+ selectorIndex,
233
+ sourceFile,
234
+ });
204
235
  const entry: TimelineElement = {
205
- id,
206
- key: buildTimelineElementKey({
207
- id,
208
- fallbackIndex: els.length,
209
- domId: el.id || undefined,
210
- selector,
211
- selectorIndex,
212
- sourceFile,
213
- }),
236
+ id: identity.id,
237
+ label,
238
+ key: identity.key,
214
239
  tag: tagLower,
215
240
  start,
216
241
  duration: dur,
@@ -310,6 +335,39 @@ function buildTimelineElementKey(params: {
310
335
  if (params.selector) return `${scope}:${params.selector}:${params.selectorIndex ?? 0}`;
311
336
  return `${scope}:${params.id}:${params.fallbackIndex}`;
312
337
  }
338
+ function buildTimelineElementIdentity(params: {
339
+ preferredId?: string | null;
340
+ label: string;
341
+ fallbackIndex: number;
342
+ domId?: string;
343
+ selector?: string;
344
+ selectorIndex?: number;
345
+ sourceFile?: string;
346
+ }): { id: string; key: string } {
347
+ const id =
348
+ params.preferredId?.trim() ||
349
+ buildTimelineElementKey({
350
+ id: params.label,
351
+ fallbackIndex: params.fallbackIndex,
352
+ domId: params.domId,
353
+ selector: params.selector,
354
+ selectorIndex: params.selectorIndex,
355
+ sourceFile: params.sourceFile,
356
+ });
357
+ const key = buildTimelineElementKey({
358
+ id,
359
+ fallbackIndex: params.fallbackIndex,
360
+ domId: params.domId,
361
+ selector: params.selector,
362
+ selectorIndex: params.selectorIndex,
363
+ sourceFile: params.sourceFile,
364
+ });
365
+ return { id, key };
366
+ }
367
+
368
+ function getTimelineElementIdentity(element: TimelineElement): string {
369
+ return element.key ?? element.id;
370
+ }
313
371
 
314
372
  function getTimelineDomNodes(doc: Document): Element[] {
315
373
  const rootComp = doc.querySelector("[data-composition-id]");
@@ -352,6 +410,103 @@ export function findTimelineDomNodeForClip(
352
410
  return candidates[fallbackIndex] ?? null;
353
411
  }
354
412
 
413
+ export function createTimelineElementFromManifestClip(params: {
414
+ clip: ClipManifestClip;
415
+ fallbackIndex: number;
416
+ doc?: Document | null;
417
+ hostEl?: Element | null;
418
+ }): TimelineElement {
419
+ const { clip, fallbackIndex, doc } = params;
420
+ let hostEl = params.hostEl ?? null;
421
+ const label = getTimelineElementDisplayLabel({
422
+ id: clip.id,
423
+ label: clip.label,
424
+ tag: clip.tagName || clip.kind,
425
+ });
426
+
427
+ let domId: string | undefined;
428
+ let selector: string | undefined;
429
+ let selectorIndex: number | undefined;
430
+ let sourceFile: string | undefined;
431
+
432
+ if (hostEl) {
433
+ domId = hostEl.id || undefined;
434
+ selector = getTimelineElementSelector(hostEl);
435
+ selectorIndex =
436
+ doc && selector ? getTimelineElementSelectorIndex(doc, hostEl, selector) : undefined;
437
+ sourceFile = getTimelineElementSourceFile(hostEl);
438
+ }
439
+
440
+ const identity = buildTimelineElementIdentity({
441
+ preferredId: clip.id,
442
+ label,
443
+ fallbackIndex,
444
+ domId,
445
+ selector,
446
+ selectorIndex,
447
+ sourceFile,
448
+ });
449
+ const entry: TimelineElement = {
450
+ id: identity.id,
451
+ label,
452
+ key: identity.key,
453
+ tag: clip.tagName || clip.kind,
454
+ start: clip.start,
455
+ duration: clip.duration,
456
+ track: clip.track,
457
+ domId,
458
+ selector,
459
+ selectorIndex,
460
+ sourceFile,
461
+ };
462
+
463
+ if (hostEl) {
464
+ applyMediaMetadataFromElement(entry, hostEl);
465
+ }
466
+ if (clip.assetUrl) entry.src = clip.assetUrl;
467
+ if (clip.kind === "composition" && clip.compositionId) {
468
+ let resolvedSrc = clip.compositionSrc;
469
+ if (!resolvedSrc) {
470
+ hostEl = doc?.querySelector(`[data-composition-id="${clip.compositionId}"]`) ?? hostEl;
471
+ resolvedSrc =
472
+ hostEl?.getAttribute("data-composition-src") ??
473
+ hostEl?.getAttribute("data-composition-file") ??
474
+ null;
475
+ }
476
+ if (resolvedSrc) {
477
+ entry.compositionSrc = resolvedSrc;
478
+ } else if (hostEl) {
479
+ const innerVideo = hostEl.querySelector("video[src]");
480
+ if (innerVideo) {
481
+ entry.src = innerVideo.getAttribute("src") || undefined;
482
+ entry.tag = "video";
483
+ }
484
+ }
485
+ if (hostEl) {
486
+ entry.domId = hostEl.id || undefined;
487
+ entry.selector = getTimelineElementSelector(hostEl);
488
+ entry.selectorIndex =
489
+ doc && entry.selector
490
+ ? getTimelineElementSelectorIndex(doc, hostEl, entry.selector)
491
+ : undefined;
492
+ entry.sourceFile = getTimelineElementSourceFile(hostEl);
493
+ const nextIdentity = buildTimelineElementIdentity({
494
+ preferredId: clip.id,
495
+ label,
496
+ fallbackIndex,
497
+ domId: entry.domId,
498
+ selector: entry.selector,
499
+ selectorIndex: entry.selectorIndex,
500
+ sourceFile: entry.sourceFile,
501
+ });
502
+ entry.id = nextIdentity.id;
503
+ entry.key = nextIdentity.key;
504
+ }
505
+ }
506
+
507
+ return entry;
508
+ }
509
+
355
510
  function findTimelineDomNode(doc: Document, id: string): Element | null {
356
511
  return (
357
512
  doc.getElementById(id) ??
@@ -380,6 +535,10 @@ export function buildStandaloneRootTimelineElement(params: {
380
535
 
381
536
  return {
382
537
  id: params.compositionId,
538
+ label: getTimelineElementDisplayLabel({
539
+ id: params.compositionId,
540
+ tag: params.tagName,
541
+ }),
383
542
  key: buildTimelineElementKey({
384
543
  id: params.compositionId,
385
544
  fallbackIndex: 0,
@@ -500,8 +659,10 @@ export function mergeTimelineElementsPreservingDowngrades(
500
659
  return nextElements;
501
660
  }
502
661
 
503
- const nextIds = new Set(nextElements.map((element) => element.id));
504
- const preserved = currentElements.filter((element) => !nextIds.has(element.id));
662
+ const nextIdentities = new Set(nextElements.map(getTimelineElementIdentity));
663
+ const preserved = currentElements.filter(
664
+ (element) => !nextIdentities.has(getTimelineElementIdentity(element)),
665
+ );
505
666
  if (preserved.length === 0) return nextElements;
506
667
  return [...nextElements, ...preserved];
507
668
  }
@@ -876,72 +1037,16 @@ export function useTimelinePlayer() {
876
1037
  }
877
1038
  const usedHostEls = new Set<Element>();
878
1039
  const els: TimelineElement[] = filtered.map((clip, index) => {
879
- let hostEl = iframeDoc
1040
+ const hostEl = iframeDoc
880
1041
  ? findTimelineDomNodeForClip(iframeDoc, clip, index, usedHostEls)
881
1042
  : null;
882
1043
  if (hostEl) usedHostEls.add(hostEl);
883
- const id = clip.id || clip.label || clip.tagName || "element";
884
- const entry: TimelineElement = {
885
- id,
886
- tag: clip.tagName || clip.kind,
887
- start: clip.start,
888
- duration: clip.duration,
889
- track: clip.track,
890
- };
891
- if (hostEl) {
892
- entry.domId = hostEl.id || undefined;
893
- entry.selector = getTimelineElementSelector(hostEl);
894
- entry.selectorIndex =
895
- iframeDoc && entry.selector
896
- ? getTimelineElementSelectorIndex(iframeDoc, hostEl, entry.selector)
897
- : undefined;
898
- entry.sourceFile = getTimelineElementSourceFile(hostEl);
899
- applyMediaMetadataFromElement(entry, hostEl);
900
- }
901
- if (clip.assetUrl) entry.src = clip.assetUrl;
902
- if (clip.kind === "composition" && clip.compositionId) {
903
- // The bundler renames data-composition-src to data-composition-file
904
- // after inlining, so the clip manifest may not have compositionSrc.
905
- // Fall back to reading data-composition-file from the DOM.
906
- let resolvedSrc = clip.compositionSrc;
907
- if (!resolvedSrc) {
908
- hostEl =
909
- iframeDoc?.querySelector(`[data-composition-id="${clip.compositionId}"]`) ?? hostEl;
910
- resolvedSrc =
911
- hostEl?.getAttribute("data-composition-src") ??
912
- hostEl?.getAttribute("data-composition-file") ??
913
- null;
914
- }
915
- if (resolvedSrc) {
916
- entry.compositionSrc = resolvedSrc;
917
- } else if (hostEl) {
918
- // Inline composition (no external file) — expose inner video for thumbnails
919
- const innerVideo = hostEl.querySelector("video[src]");
920
- if (innerVideo) {
921
- entry.src = innerVideo.getAttribute("src") || undefined;
922
- entry.tag = "video";
923
- }
924
- }
925
- if (hostEl) {
926
- const iframeDoc = iframeRef.current?.contentDocument;
927
- entry.domId = hostEl.id || undefined;
928
- entry.selector = getTimelineElementSelector(hostEl);
929
- entry.selectorIndex =
930
- iframeDoc && entry.selector
931
- ? getTimelineElementSelectorIndex(iframeDoc, hostEl, entry.selector)
932
- : undefined;
933
- entry.sourceFile = getTimelineElementSourceFile(hostEl);
934
- }
935
- }
936
- entry.key = buildTimelineElementKey({
937
- id,
1044
+ return createTimelineElementFromManifestClip({
1045
+ clip,
938
1046
  fallbackIndex: index,
939
- domId: entry.domId,
940
- selector: entry.selector,
941
- selectorIndex: entry.selectorIndex,
942
- sourceFile: entry.sourceFile,
1047
+ doc: iframeDoc,
1048
+ hostEl,
943
1049
  });
944
- return entry;
945
1050
  });
946
1051
  const rawDuration = data.durationInFrames / 30;
947
1052
  // Clamp non-finite or absurdly large durations — the runtime can emit
@@ -1055,17 +1160,24 @@ export function useTimelinePlayer() {
1055
1160
  const selector = getTimelineElementSelector(el);
1056
1161
  const sourceFile = getTimelineElementSourceFile(el);
1057
1162
  const selectorIndex = getTimelineElementSelectorIndex(doc, el, selector);
1058
- const id = el.id || compId;
1163
+ const label = getTimelineElementDisplayLabel({
1164
+ id: el.id || compId || null,
1165
+ label: el.getAttribute("data-timeline-label") ?? el.getAttribute("data-label"),
1166
+ tag: el.tagName,
1167
+ });
1168
+ const identity = buildTimelineElementIdentity({
1169
+ preferredId: el.id || compId || null,
1170
+ label,
1171
+ fallbackIndex: missing.length,
1172
+ domId: el.id || undefined,
1173
+ selector,
1174
+ selectorIndex,
1175
+ sourceFile,
1176
+ });
1059
1177
  const entry: TimelineElement = {
1060
- id,
1061
- key: buildTimelineElementKey({
1062
- id,
1063
- fallbackIndex: missing.length,
1064
- domId: el.id || undefined,
1065
- selector,
1066
- selectorIndex,
1067
- sourceFile,
1068
- }),
1178
+ id: identity.id,
1179
+ label,
1180
+ key: identity.key,
1069
1181
  tag: el.tagName.toLowerCase(),
1070
1182
  start,
1071
1183
  duration: dur,
@@ -2,6 +2,7 @@ import { create } from "zustand";
2
2
 
3
3
  export interface TimelineElement {
4
4
  id: string;
5
+ label?: string;
5
6
  key?: string;
6
7
  tag: string;
7
8
  start: number;