@hyperframes/studio 0.8.142 → 0.8.144

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 (76) hide show
  1. package/dist/assets/hyperframes-player-Bw_jIaSX.js +473 -0
  2. package/dist/assets/{index-B-wffzRH.js → index-BVM9NuWl.js} +153 -153
  3. package/dist/assets/{index-D3Vij9t1.js → index-Cj7W_c7p.js} +1 -1
  4. package/dist/assets/{index-D5q2_7Gw.js → index-DyH9dT7b.js} +1 -1
  5. package/dist/{chunk-TFLU2MGI.js → chunk-3GHTHPZD.js} +8 -1
  6. package/dist/chunk-3GHTHPZD.js.map +1 -0
  7. package/dist/{chunk-H2DUOXTF.js → chunk-G3EDKRZH.js} +3 -3
  8. package/dist/{chunk-UYWON5AX.js → chunk-PEAKCOUW.js} +7 -3
  9. package/dist/chunk-PEAKCOUW.js.map +1 -0
  10. package/dist/{chunk-LOVBLSMR.js → chunk-Y2OHHNM2.js} +35 -19
  11. package/dist/chunk-Y2OHHNM2.js.map +1 -0
  12. package/dist/{domEditingLayers-KBTPQKGA.js → domEditingLayers-PYST4G45.js} +3 -3
  13. package/dist/index.html +1 -1
  14. package/dist/index.js +261 -230
  15. package/dist/index.js.map +1 -1
  16. package/dist/player.js +3 -3
  17. package/dist/ui.d.ts +4 -5
  18. package/dist/ui.js +1 -1
  19. package/package.json +8 -8
  20. package/src/components/MediaPreview.tsx +2 -1
  21. package/src/components/editor/DomEditSelectionChrome.test.tsx +103 -0
  22. package/src/components/editor/DomEditSelectionChrome.tsx +42 -16
  23. package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +36 -0
  24. package/src/components/editor/domEditOverlayGestures.ts +1 -0
  25. package/src/components/editor/domEditOverlayStartGesture.ts +5 -0
  26. package/src/components/editor/domEditResizeLocal.test.ts +81 -1
  27. package/src/components/editor/domEditResizeLocal.ts +27 -3
  28. package/src/components/editor/domEditingLayers.ts +4 -1
  29. package/src/components/editor/domEditingTextFields.test.ts +18 -0
  30. package/src/components/editor/manualEditsDom.ts +12 -12
  31. package/src/components/editor/mediaAudioEdits.test.ts +12 -0
  32. package/src/components/editor/mediaAudioEdits.ts +5 -5
  33. package/src/components/editor/previewGestureStarts.ts +14 -5
  34. package/src/components/editor/propertyPanelFill.tsx +2 -1
  35. package/src/components/editor/propertyPanelFlatMediaSection.tsx +3 -4
  36. package/src/components/editor/propertyPanelMediaSection.tsx +3 -4
  37. package/src/components/editor/resizeDraft.ts +13 -4
  38. package/src/components/editor/useColorGradingController.ts +2 -1
  39. package/src/components/editor/useDomEditOverlayGestures.ts +5 -3
  40. package/src/components/ui/Menu.test.tsx +67 -5
  41. package/src/components/ui/Menu.tsx +21 -4
  42. package/src/components/ui/Popover.tsx +11 -5
  43. package/src/hooks/gsapResizeIntercept.test.ts +46 -11
  44. package/src/hooks/gsapResizeIntercept.ts +25 -20
  45. package/src/hooks/useDomEditCommits.ts +2 -1
  46. package/src/hooks/useFileManager.ts +2 -1
  47. package/src/hooks/useFileTree.ts +2 -1
  48. package/src/hooks/useRenderClipContent.ts +15 -3
  49. package/src/hooks/useThumbnailStripSize.test.tsx +123 -0
  50. package/src/hooks/useThumbnailStripSize.ts +19 -8
  51. package/src/player/components/CompositionThumbnail.test.ts +3 -0
  52. package/src/player/components/ImageThumbnail.test.tsx +4 -1
  53. package/src/player/components/VideoThumbnail.test.tsx +13 -2
  54. package/src/player/components/clipMenuNormalize.ts +2 -2
  55. package/src/player/components/clipPeakMap.ts +2 -2
  56. package/src/player/components/thumbnailUtils.test.ts +33 -0
  57. package/src/player/components/thumbnailUtils.ts +5 -0
  58. package/src/player/hooks/timelineSyncHydration.ts +2 -1
  59. package/src/player/lib/mediaProbe.test.ts +12 -0
  60. package/src/player/lib/mediaProbe.ts +4 -2
  61. package/src/utils/authoredSource.test.ts +25 -0
  62. package/src/utils/authoredSource.ts +3 -5
  63. package/src/utils/rounding.ts +7 -0
  64. package/src/utils/sourcePatcher.test.ts +135 -0
  65. package/src/utils/sourcePatcher.ts +102 -186
  66. package/src/utils/studioHelpers.test.ts +23 -0
  67. package/src/utils/studioHelpers.ts +3 -2
  68. package/src/utils/studioSelectionSnapshot.test.ts +31 -0
  69. package/src/utils/timelineAssetDrop.test.ts +14 -0
  70. package/src/utils/timelineAssetDrop.ts +2 -1
  71. package/dist/assets/hyperframes-player-sAsRuV73.js +0 -473
  72. package/dist/chunk-LOVBLSMR.js.map +0 -1
  73. package/dist/chunk-TFLU2MGI.js.map +0 -1
  74. package/dist/chunk-UYWON5AX.js.map +0 -1
  75. /package/dist/{chunk-H2DUOXTF.js.map → chunk-G3EDKRZH.js.map} +0 -0
  76. /package/dist/{domEditingLayers-KBTPQKGA.js.map → domEditingLayers-PYST4G45.js.map} +0 -0
@@ -89,6 +89,18 @@ describe("media probe registry", () => {
89
89
  expect(getDurationFromMetadata).toHaveBeenCalledTimes(2);
90
90
  });
91
91
 
92
+ it("probes a dropped clip's URL-spelled src at its own file", async () => {
93
+ await probeMissingSourceDurations(
94
+ [{ id: "clip", tag: "video", src: "assets/My%20clip%20%231.mp4" }],
95
+ "project-a",
96
+ vi.fn(),
97
+ );
98
+
99
+ expect(requestedSources).toEqual([
100
+ `${window.location.origin}/api/projects/project-a/preview/assets/My%20clip%20%231.mp4`,
101
+ ]);
102
+ });
103
+
92
104
  it("probes same-origin rooted media through the active project preview", async () => {
93
105
  const apply = vi.fn();
94
106
  await probeMissingSourceDurations(
@@ -1,4 +1,4 @@
1
- import { resolveMediaPreviewUrl } from "../components/thumbnailUtils";
1
+ import { authoredSrcPath, resolveMediaPreviewUrl } from "../components/thumbnailUtils";
2
2
  import { TIMELINE_VIEWPORT_BUDGETS } from "./timelineViewportBudgets";
3
3
 
4
4
  export interface MediaProbeResult {
@@ -87,7 +87,9 @@ function getCachedProbe(url: string): MediaProbeResult | undefined {
87
87
  }
88
88
 
89
89
  function resolveProbeSource(src: string, projectId: string | null): string {
90
- return projectId ? resolveMediaPreviewUrl(src, projectId, window.location.origin) : src;
90
+ return projectId
91
+ ? resolveMediaPreviewUrl(authoredSrcPath(src), projectId, window.location.origin)
92
+ : src;
91
93
  }
92
94
 
93
95
  function evictMetadataOverflow(): void {
@@ -0,0 +1,25 @@
1
+ // @vitest-environment happy-dom
2
+ import { describe, expect, it } from "vitest";
3
+ import {
4
+ PREVIEW_RASTER_ATTR,
5
+ STUDIO_PREVIEW_LAZY_ATTR,
6
+ } from "@hyperframes/core/studio-preview-mark";
7
+ import { liveMarkupWithoutPreviewMarks } from "./authoredSource";
8
+
9
+ describe("liveMarkupWithoutPreviewMarks", () => {
10
+ it("drops the preview's raster mark from an element and its descendants", () => {
11
+ const live = document.createElement("div");
12
+ live.innerHTML = `<div class="line" ${PREVIEW_RASTER_ATTR}><span class="word">LOUD</span></div>`;
13
+ live.setAttribute(PREVIEW_RASTER_ATTR, "");
14
+ expect(liveMarkupWithoutPreviewMarks(live)).toBe(
15
+ '<div><div class="line"><span class="word">LOUD</span></div></div>',
16
+ );
17
+ expect(live.hasAttribute(PREVIEW_RASTER_ATTR)).toBe(true);
18
+ });
19
+
20
+ it("gives a lazily previewed image back its authored loading", () => {
21
+ const live = document.createElement("div");
22
+ live.innerHTML = `<img src="a.png" loading="lazy" ${STUDIO_PREVIEW_LAZY_ATTR}>`;
23
+ expect(liveMarkupWithoutPreviewMarks(live)).toBe('<div><img src="a.png"></div>');
24
+ });
25
+ });
@@ -1,7 +1,7 @@
1
1
  import { ensureHfIds, isCompositionTemplate } from "@hyperframes/parsers/hf-ids";
2
2
  import {
3
+ PREVIEW_ONLY_ATTRS,
3
4
  STUDIO_PREVIEW_LAZY_ATTR,
4
- STUDIO_PREVIEW_UPCOMING_ATTR,
5
5
  } from "@hyperframes/core/studio-preview-mark";
6
6
 
7
7
  // Stamped as the preview stamps the files it serves, so a live element's hf-id finds its source.
@@ -98,10 +98,8 @@ export function authoredMarkup(authored: Element, live: Element, sourceFile: str
98
98
  export function liveMarkupWithoutPreviewMarks(live: Element): string {
99
99
  const copy = live.cloneNode(true) as Element;
100
100
  for (const el of [copy, ...Array.from(copy.querySelectorAll("*"))]) {
101
- el.removeAttribute(STUDIO_PREVIEW_UPCOMING_ATTR);
102
- if (!el.hasAttribute(STUDIO_PREVIEW_LAZY_ATTR)) continue;
103
- el.removeAttribute("loading");
104
- el.removeAttribute(STUDIO_PREVIEW_LAZY_ATTR);
101
+ if (el.hasAttribute(STUDIO_PREVIEW_LAZY_ATTR)) el.removeAttribute("loading");
102
+ for (const attr of PREVIEW_ONLY_ATTRS) el.removeAttribute(attr);
105
103
  }
106
104
  return copy.outerHTML;
107
105
  }
@@ -8,6 +8,13 @@ export function roundToLayoutPx(val: number): number {
8
8
  return Math.round(val);
9
9
  }
10
10
 
11
+ export function roundToLayoutSize(size: { width: number; height: number }) {
12
+ return {
13
+ width: roundToLayoutPx(Math.max(1, size.width)),
14
+ height: roundToLayoutPx(Math.max(1, size.height)),
15
+ };
16
+ }
17
+
11
18
  /** Round to 2 decimal places (centisecond precision for timeline values). */
12
19
  export function roundToCenti(val: number): number {
13
20
  return Math.round(val * 100) / 100;
@@ -3,11 +3,43 @@ import {
3
3
  applyPatch,
4
4
  applyPatchByTarget,
5
5
  readAttributeByTarget,
6
+ readTagAttribute,
6
7
  readTagSnippetByTarget,
7
8
  type PatchOperation,
8
9
  } from "./sourcePatcher";
9
10
 
10
11
  describe("applyPatchByTarget", () => {
12
+ it("removes all duplicate boolean attributes so unmuting cannot reveal another", () => {
13
+ expect(
14
+ applyPatch(`<video id="clip" muted muted></video>`, "clip", {
15
+ type: "html-attribute",
16
+ property: "muted",
17
+ value: null,
18
+ }),
19
+ ).toBe(`<video id="clip"></video>`);
20
+ });
21
+
22
+ it("locates Unicode custom tags and respects the first duplicate attribute", () => {
23
+ const html = `<my-élement id="real" id="other" data-duration="1"></my-élement>`;
24
+ const op: PatchOperation = { type: "attribute", property: "duration", value: "2" };
25
+ expect(applyPatch(html, "real", op)).toContain('data-duration="2"');
26
+ expect(applyPatch(html, "other", op)).toBe(html);
27
+ });
28
+
29
+ it("reads and patches only the real source outside quoted attribute content", () => {
30
+ const html = `<video id="hero" title="example src='other.mp4'" data-src="lazy.mp4" src="assets/it's.mp4"></video>`;
31
+ expect(readTagAttribute(html, "src")).toBe("assets/it's.mp4");
32
+ const op: PatchOperation = { type: "html-attribute", property: "src", value: "new.mp4" };
33
+ const expected = `<video id="hero" title="example src='other.mp4'" data-src="lazy.mp4" src="new.mp4"></video>`;
34
+ expect(applyPatch(html, "hero", op)).toBe(expected);
35
+ expect(applyPatchByTarget(html, { id: "hero" }, op)).toBe(expected);
36
+ });
37
+
38
+ it("does not locate a target through a prefixed or quoted id", () => {
39
+ const html = `<video data-id="hero" title="id='hero'" src="other.mp4"></video><video id="hero" src="real.mp4"></video>`;
40
+ expect(readTagSnippetByTarget(html, { id: "hero" })).toContain('src="real.mp4"');
41
+ });
42
+
11
43
  it.each(['"', "'"])("patches long and multiline styles quoted with %s", (quote) => {
12
44
  const otherQuote = quote === '"' ? "'" : '"';
13
45
  const op: PatchOperation = { type: "inline-style", property: "opacity", value: "0.5" };
@@ -646,3 +678,106 @@ describe("T3 — hfId targeting (spec for R1)", () => {
646
678
  expect(result).toContain('<h2 class="b">B</h2>');
647
679
  });
648
680
  });
681
+
682
+ describe("file names with special characters", () => {
683
+ const src = (value: string): PatchOperation => ({
684
+ type: "html-attribute",
685
+ property: "src",
686
+ value,
687
+ });
688
+
689
+ it("replaces a double-quoted value that holds an apostrophe instead of adding a second one", () => {
690
+ expect(applyPatch(`<img id="a" src="assets/it's.png">`, "a", src("assets/b.png"))).toBe(
691
+ `<img id="a" src="assets/b.png">`,
692
+ );
693
+ expect(applyPatch(`<img id="a" src='assets/say "hi".png'>`, "a", src("b.png"))).toBe(
694
+ `<img id="a" src="b.png">`,
695
+ );
696
+ });
697
+
698
+ it("writes $ sequences in a value literally", () => {
699
+ expect(applyPatch(`<img id="a" src="old.png">`, "a", src("assets/$100 bill.png"))).toBe(
700
+ `<img id="a" src="assets/$100 bill.png">`,
701
+ );
702
+ expect(applyPatch(`<img id="a">`, "a", src("$&$1.png"))).toBe(
703
+ `<img id="a" src="$&amp;$1.png">`,
704
+ );
705
+ expect(
706
+ applyPatch(`<div id="a" data-src="old">`, "a", {
707
+ type: "attribute",
708
+ property: "src",
709
+ value: "$' and $`",
710
+ }),
711
+ ).toBe(`<div id="a" data-src="$' and $\`">`);
712
+ });
713
+
714
+ it("reads a data attribute whose value holds the other quote kind", () => {
715
+ expect(readAttributeByTarget(`<div id="a" data-title="it's here">`, { id: "a" }, "title")).toBe(
716
+ "it's here",
717
+ );
718
+ });
719
+ });
720
+
721
+ describe("readTagAttribute", () => {
722
+ it("reads both quote kinds and decodes entities without losing the other quote", () => {
723
+ expect(readTagAttribute(`<video src="assets/it's&amp;ours.mp4">`, "src")).toBe(
724
+ "assets/it's&ours.mp4",
725
+ );
726
+ expect(readTagAttribute(`<video src='assets/say "hi".mp4'>`, "src")).toBe(
727
+ 'assets/say "hi".mp4',
728
+ );
729
+ });
730
+
731
+ it("distinguishes an empty attribute from a missing attribute", () => {
732
+ expect(readTagAttribute('<video src="">', "src")).toBe("");
733
+ expect(readTagAttribute("<video>", "src")).toBeUndefined();
734
+ });
735
+ });
736
+
737
+ describe("whole attribute names", () => {
738
+ it.each(["data-src", "x-src", "xml:src", "srcset"])(
739
+ "reads and edits src without matching %s",
740
+ (other) => {
741
+ const tag = `<img id="a" ${other}="assets/other.png" src="assets/original.png">`;
742
+ expect(readTagAttribute(tag, "src")).toBe("assets/original.png");
743
+ for (const patch of [
744
+ (op: PatchOperation) => applyPatch(tag, "a", op),
745
+ (op: PatchOperation) => applyPatchByTarget(tag, { id: "a" }, op),
746
+ ]) {
747
+ expect(patch({ type: "html-attribute", property: "src", value: "assets/new.png" })).toBe(
748
+ `<img id="a" ${other}="assets/other.png" src="assets/new.png">`,
749
+ );
750
+ expect(patch({ type: "html-attribute", property: "src", value: null })).toBe(
751
+ `<img id="a" ${other}="assets/other.png">`,
752
+ );
753
+ }
754
+ expect(readTagAttribute(`<img ${other}="assets/other.png">`, "src")).toBeUndefined();
755
+ },
756
+ );
757
+
758
+ it("does not treat a longer data attribute as a boolean attribute to remove", () => {
759
+ const tag = '<div id="a" data-title-long="keep">';
760
+ expect(
761
+ applyPatchByTarget(tag, { id: "a" }, { type: "attribute", property: "title", value: null }),
762
+ ).toBe(tag);
763
+ });
764
+ });
765
+
766
+ it("removes a boolean attribute without truncating a longer attribute name", () => {
767
+ const tag = '<video id="a" muted-long="keep" muted>';
768
+ expect(
769
+ applyPatchByTarget(
770
+ tag,
771
+ { id: "a" },
772
+ { type: "html-attribute", property: "muted", value: null },
773
+ ),
774
+ ).toBe('<video id="a" muted-long="keep">');
775
+ });
776
+
777
+ it("decodes an authored apostrophe entity once", () => {
778
+ expect(readTagAttribute("<video src='assets/it&apos;s.mp4'>", "src")).toBe("assets/it's.mp4");
779
+ expect(readTagAttribute("<video src='assets/it&#39;s.mp4'>", "src")).toBe("assets/it's.mp4");
780
+ expect(readTagAttribute('<video src="assets/it&amp;apos;s.mp4">', "src")).toBe(
781
+ "assets/it&apos;s.mp4",
782
+ );
783
+ });