@hyperframes/studio 0.8.77 → 0.8.79

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.
Files changed (47) hide show
  1. package/dist/assets/hyperframes-player-Bx_Kw4Ey.js +464 -0
  2. package/dist/assets/{index-DcxWQ7Za.js → index-CJP92F4-.js} +1 -1
  3. package/dist/assets/{index-2GgHjQq-.js → index-DBKySIo7.js} +187 -187
  4. package/dist/assets/{index-CsFCRdMo.js → index-ORZXYB0k.js} +1 -1
  5. package/dist/{chunk-5VANA2TG.js → chunk-I7XNAIOF.js} +2 -1
  6. package/dist/{domEditingLayers-IGPLTI5E.js → domEditingLayers-MHFDKAQ7.js} +2 -2
  7. package/dist/index.d.ts +23 -15
  8. package/dist/index.html +1 -1
  9. package/dist/index.js +459 -257
  10. package/dist/index.js.map +1 -1
  11. package/package.json +8 -7
  12. package/src/App.tsx +1 -0
  13. package/src/components/nle/useTimelineEditCallbacks.ts +8 -2
  14. package/src/hooks/timelineAudioGroupVolume.ts +21 -11
  15. package/src/hooks/timelineEditPermission.ts +47 -13
  16. package/src/hooks/timelineElementFxAttribute.ts +19 -13
  17. package/src/hooks/useClipboard.source.test.ts +340 -0
  18. package/src/hooks/useClipboard.ts +141 -62
  19. package/src/hooks/useElementPicker.test.ts +172 -0
  20. package/src/hooks/useElementPicker.ts +141 -93
  21. package/src/hooks/useFrameCapture.test.tsx +67 -0
  22. package/src/hooks/useFrameCapture.ts +18 -9
  23. package/src/hooks/usePersistentEditHistory.test.ts +2 -2
  24. package/src/hooks/usePersistentEditHistory.ts +2 -2
  25. package/src/hooks/useTimelineEditing.test.tsx +59 -0
  26. package/src/hooks/useTimelineEditing.ts +42 -33
  27. package/src/index.ts +8 -0
  28. package/src/player/components/Player.test.ts +5 -0
  29. package/src/player/components/Player.tsx +1 -0
  30. package/src/player/components/VideoThumbnail.test.tsx +13 -0
  31. package/src/player/components/VideoThumbnail.tsx +1 -1
  32. package/src/player/components/timelineCallbacks.ts +3 -2
  33. package/src/player/hooks/useTimelinePlayer.hiddenPause.test.ts +75 -0
  34. package/src/player/hooks/useTimelinePlayer.play.test.ts +36 -0
  35. package/src/player/hooks/useTimelinePlayer.ts +25 -28
  36. package/src/player/hooks/useTimelineSyncCallbacks.ts +12 -4
  37. package/src/player/lib/thumbnailVideoDecoder.test.ts +102 -6
  38. package/src/player/lib/thumbnailVideoDecoder.ts +20 -5
  39. package/src/telemetry/client.test.ts +26 -0
  40. package/src/telemetry/policy.test.ts +10 -0
  41. package/src/telemetry/policy.ts +7 -0
  42. package/src/utils/authoredSource.ts +92 -0
  43. package/src/utils/gsapSoftReload.test.ts +29 -0
  44. package/src/utils/gsapSoftReload.ts +3 -7
  45. package/dist/assets/hyperframes-player-BSDTJ9EF.js +0 -464
  46. /package/dist/{chunk-5VANA2TG.js.map → chunk-I7XNAIOF.js.map} +0 -0
  47. /package/dist/{domEditingLayers-IGPLTI5E.js.map → domEditingLayers-MHFDKAQ7.js.map} +0 -0
@@ -0,0 +1,92 @@
1
+ import { ensureHfIds, isCompositionTemplate } from "@hyperframes/parsers/hf-ids";
2
+
3
+ // Stamped as the preview stamps the files it serves, so a live element's hf-id finds its source.
4
+ export function parseSavedSource(html: string): Document {
5
+ return new DOMParser().parseFromString(ensureHfIds(html), "text/html");
6
+ }
7
+
8
+ const rootsByDoc = new WeakMap<Document, ParentNode[]>();
9
+
10
+ // A plain template is a runtime clone source, so ids inside it repeat across clones.
11
+ function searchRoots(root: ParentNode): ParentNode[] {
12
+ const templates = Array.from(root.querySelectorAll("template")).filter(isCompositionTemplate);
13
+ return [root, ...templates.flatMap((template) => searchRoots(template.content))];
14
+ }
15
+
16
+ function findByAttribute(doc: Document, name: string, value: string): Element | null {
17
+ let roots = rootsByDoc.get(doc);
18
+ if (!roots) rootsByDoc.set(doc, (roots = searchRoots(doc)));
19
+ const selector = `[${name}="${value.replace(/["\\]/g, "\\$&")}"]`;
20
+ for (const root of roots) {
21
+ const match = root.querySelector(selector);
22
+ if (match) return match;
23
+ }
24
+ return null;
25
+ }
26
+
27
+ export function findAuthoredElement(doc: Document, live: Element): Element | null {
28
+ const hfId = live.getAttribute("data-hf-id");
29
+ if (hfId) return findByAttribute(doc, "data-hf-id", hfId);
30
+ return live.id ? findByAttribute(doc, "id", live.id) : null;
31
+ }
32
+
33
+ export function findAuthoredElementById(doc: Document, live: Element): Element | null {
34
+ return live.id ? findByAttribute(doc, "id", live.id) : null;
35
+ }
36
+
37
+ const PATH_ATTRS = ["src", "href"];
38
+ const CSS_URL = /\burl\(\s*(["']?)([^)"'\s](?:[^)"']*[^)"'\s])?)\1\s*\)/g;
39
+
40
+ function isRelative(path: string): boolean {
41
+ return !!path && !/^(?:[a-z][a-z\d+.-]*:|\/|#)/i.test(path);
42
+ }
43
+
44
+ function toProjectPath(sourceFile: string, path: string): string {
45
+ const parts = sourceFile.split("/").slice(0, -1);
46
+ for (const part of path.split("/")) {
47
+ if (part === "..") parts.pop();
48
+ else if (part !== "." && part !== "") parts.push(part);
49
+ }
50
+ return parts.join("/");
51
+ }
52
+
53
+ function livePathsOf(live: Element): Set<string> {
54
+ const paths = new Set<string>();
55
+ for (const el of [live, ...Array.from(live.querySelectorAll("*"))]) {
56
+ for (const attr of PATH_ATTRS) paths.add(el.getAttribute(attr) ?? "");
57
+ for (const match of (el.getAttribute("style") ?? "").matchAll(CSS_URL)) paths.add(match[2]);
58
+ }
59
+ return paths;
60
+ }
61
+
62
+ // The preview inlines a sub-composition with its relative asset paths rebased to the project
63
+ // root (rewriteAssetPath in @hyperframes/parsers, which is not browser-safe); copy does the same.
64
+ export function authoredMarkup(authored: Element, live: Element, sourceFile: string): string {
65
+ if (!sourceFile.includes("/")) return authored.outerHTML;
66
+ const livePaths = livePathsOf(live);
67
+ const rebase = (path: string): string => {
68
+ if (!isRelative(path)) return path;
69
+ const cut = path.search(/[?#]/);
70
+ const suffix = cut < 0 ? "" : path.slice(cut);
71
+ const rebased = toProjectPath(sourceFile, cut < 0 ? path : path.slice(0, cut)) + suffix;
72
+ return path.startsWith("../") || livePaths.has(rebased) ? rebased : path;
73
+ };
74
+ const copy = authored.cloneNode(true) as Element;
75
+ for (const el of [copy, ...Array.from(copy.querySelectorAll("*"))]) {
76
+ for (const attr of PATH_ATTRS) {
77
+ const value = el.getAttribute(attr);
78
+ if (value) el.setAttribute(attr, rebase(value.trim()));
79
+ }
80
+ const style = el.getAttribute("style");
81
+ if (style) {
82
+ el.setAttribute(
83
+ "style",
84
+ style.replace(
85
+ CSS_URL,
86
+ (_, quote: string, url: string) => `url(${quote}${rebase(url)}${quote})`,
87
+ ),
88
+ );
89
+ }
90
+ }
91
+ return copy.outerHTML;
92
+ }
@@ -488,6 +488,35 @@ describe("applySoftReload authored-opacity restore", () => {
488
488
  expect(el.style.getPropertyPriority("opacity")).toBe("");
489
489
  });
490
490
 
491
+ it("keeps the stamp for an element whose hf-id the written file lacks, even if it shares an id", () => {
492
+ const el = document.createElement("img");
493
+ el.id = "title-card";
494
+ el.setAttribute("data-hf-id", "hf-from-another-file");
495
+ el.setAttribute("data-hf-authored-opacity", "0.5");
496
+ el.style.opacity = "0";
497
+
498
+ const opacity = restoreOpacity(
499
+ el,
500
+ '<html><body><img id="title-card" style="opacity: 0.9"></body></html>',
501
+ );
502
+
503
+ expect(opacity).toBe("0.5");
504
+ });
505
+
506
+ it("does not take a runtime clone's opacity from its plain template", () => {
507
+ const el = document.createElement("li");
508
+ el.id = "item";
509
+ el.setAttribute("data-hf-authored-opacity", "0.5");
510
+ el.style.opacity = "0";
511
+
512
+ const opacity = restoreOpacity(
513
+ el,
514
+ '<html><body><template><li id="item" style="opacity: 0.3"></li></template></body></html>',
515
+ );
516
+
517
+ expect(opacity).toBe("0.5");
518
+ });
519
+
491
520
  it("falls back to the parse-time stamp when no after-write HTML is given", () => {
492
521
  const el = document.createElement("img");
493
522
  el.setAttribute("data-hf-authored-opacity", "0.75");
@@ -1,4 +1,5 @@
1
1
  import { COLOR_GRADING_SOURCE_HIDDEN_ATTR } from "@hyperframes/core/color-grading";
2
+ import { findAuthoredElement, parseSavedSource } from "./authoredSource";
2
3
  import { applyAuthoredInlineOpacity, readStampedAuthoredOpacity } from "./authoredOpacity";
3
4
 
4
5
  type IframeWindow = Window & {
@@ -301,17 +302,12 @@ export function applySoftReload(
301
302
  const findAuthoredSource = (el: HTMLElement): Element | null => {
302
303
  if (authoredDoc === undefined) {
303
304
  try {
304
- authoredDoc = authoredHtml
305
- ? new DOMParser().parseFromString(authoredHtml, "text/html")
306
- : null;
305
+ authoredDoc = authoredHtml ? parseSavedSource(authoredHtml) : null;
307
306
  } catch {
308
307
  authoredDoc = null;
309
308
  }
310
309
  }
311
- if (!authoredDoc) return null;
312
- const hfId = el.getAttribute("data-hf-id");
313
- if (hfId) return authoredDoc.querySelector(`[data-hf-id="${hfId}"]`);
314
- return el.id ? authoredDoc.getElementById(el.id) : null;
310
+ return authoredDoc ? findAuthoredElement(authoredDoc, el) : null;
315
311
  };
316
312
  const readAuthoredOpacity = (el: HTMLElement): string | null => {
317
313
  const source = findAuthoredSource(el);