@hyperframes/studio 0.8.10 → 0.8.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.8.10",
3
+ "version": "0.8.11",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -47,11 +47,11 @@
47
47
  "gsap": "^3.13.0",
48
48
  "marked": "^14.1.4",
49
49
  "mediabunny": "^1.45.3",
50
- "@hyperframes/core": "0.8.10",
51
- "@hyperframes/parsers": "0.8.10",
52
- "@hyperframes/player": "0.8.10",
53
- "@hyperframes/sdk": "0.8.10",
54
- "@hyperframes/studio-server": "0.8.10"
50
+ "@hyperframes/core": "0.8.11",
51
+ "@hyperframes/parsers": "0.8.11",
52
+ "@hyperframes/player": "0.8.11",
53
+ "@hyperframes/studio-server": "0.8.11",
54
+ "@hyperframes/sdk": "0.8.11"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@types/react": "19",
@@ -68,7 +68,7 @@
68
68
  "vite": "^6.4.2",
69
69
  "vitest": "^3.2.4",
70
70
  "zustand": "^5.0.0",
71
- "@hyperframes/producer": "0.8.10"
71
+ "@hyperframes/producer": "0.8.11"
72
72
  },
73
73
  "peerDependencies": {
74
74
  "react": "19",
@@ -68,6 +68,37 @@ describe("TimelineFxButton", () => {
68
68
  expect(document.querySelector('[role="dialog"]')).toBeTruthy();
69
69
  });
70
70
 
71
+ // The reported symptom was "the grouping button did nothing". The dialog WAS
72
+ // opening — it positioned at `anchorRect.bottom + 4` with no flip and no
73
+ // clamp, and this button lives in a track header at the bottom of the studio
74
+ // window, so it opened past the viewport edge. The test below it passed
75
+ // throughout: happy-dom reports an all-zero rect for an unlaid-out button,
76
+ // which lands the dialog at top:4 — on screen, and nothing like the app.
77
+ it("flips the group dialog above the anchor when there is no room below", () => {
78
+ const host = mount(<TimelineFxButton variant="group-pointer" onGroupClips={vi.fn()} />);
79
+ const fx = byTextButton(host, "FX");
80
+ // A track header near the bottom edge of the (1024x768) window.
81
+ fx!.getBoundingClientRect = () =>
82
+ ({ left: 300, top: 760, right: 320, bottom: 776, width: 20, height: 16 }) as DOMRect;
83
+ act(() => fx?.click());
84
+ const dialog = document.querySelector('[role="dialog"]') as HTMLElement;
85
+ expect(dialog).toBeTruthy();
86
+ // Above the anchor, and fully inside the viewport: 644 + 112 === 756.
87
+ expect(dialog.style.top).toBe("644px");
88
+ });
89
+
90
+ it("keeps the group dialog inside the right edge of the window", () => {
91
+ const host = mount(<TimelineFxButton variant="group-pointer" onGroupClips={vi.fn()} />);
92
+ const fx = byTextButton(host, "FX");
93
+ // Hard against the right edge: 224px wide + a 12px margin has to fit.
94
+ fx!.getBoundingClientRect = () =>
95
+ ({ left: 1010, top: 100, right: 1024, bottom: 116, width: 14, height: 16 }) as DOMRect;
96
+ act(() => fx?.click());
97
+ const dialog = document.querySelector('[role="dialog"]') as HTMLElement;
98
+ expect(dialog.style.left).toBe("788px");
99
+ expect(dialog.style.top).toBe("120px");
100
+ });
101
+
71
102
  it("group-pointer variant offers Group instead of a popover", () => {
72
103
  const onGroupClips = vi.fn();
73
104
  const host = mount(<TimelineFxButton variant="group-pointer" onGroupClips={onGroupClips} />);
@@ -18,6 +18,24 @@ import {
18
18
  } from "@hyperframes/core/audio-fx";
19
19
  import type { HfAudioNameKind } from "@hyperframes/core/audio-carve";
20
20
  import { TimelineFxPopover } from "../../components/editor/TimelineFxPopover.js";
21
+ import { resolveFloatingPanelPosition } from "../../components/editor/floatingPanel.js";
22
+
23
+ // Estimated, like FORMAT_PANEL_SIZE in RenderQueue: `w-56` is exact, and only
24
+ // the flip decision uses the height — the clamp keeps the dialog on screen
25
+ // either way.
26
+ const GROUP_DIALOG_SIZE = { width: 224, height: 112 };
27
+
28
+ /** Where the grouping dialog goes: flipped above the anchor when there is no
29
+ * room below, and clamped so neither edge leaves the viewport. */
30
+ function groupDialogPosition(anchorRect: DOMRect): { left: number; top: number } {
31
+ const { left, top } = resolveFloatingPanelPosition(
32
+ anchorRect,
33
+ { width: window.innerWidth, height: window.innerHeight },
34
+ GROUP_DIALOG_SIZE,
35
+ { offset: 4 },
36
+ );
37
+ return { left, top };
38
+ }
21
39
 
22
40
  function parseFxChainOrEmpty(raw: string | undefined): HfAudioFxChain {
23
41
  if (!raw) return { version: 1, nodes: [] };
@@ -80,7 +98,12 @@ export function TimelineFxButton(props: TimelineFxButtonProps) {
80
98
  role="dialog"
81
99
  aria-label="Group these clips to add effects"
82
100
  className="z-[200] w-56 rounded-md border border-white/10 bg-[#1b1b1f] p-2.5 text-[11px] text-white/75 shadow-xl"
83
- style={{ position: "fixed", left: anchorRect.left, top: anchorRect.bottom + 4 }}
101
+ // Was `top: anchorRect.bottom + 4` with no flip and no clamp. This
102
+ // button lives in a track header at the BOTTOM of the studio
103
+ // window, so the dialog opened past the viewport edge and the
104
+ // click read as doing nothing at all. Same helper the other body
105
+ // portals position with.
106
+ style={{ position: "fixed", ...groupDialogPosition(anchorRect) }}
84
107
  onPointerDown={(event) => event.stopPropagation()}
85
108
  >
86
109
  <p>Group these clips to add effects to all of them.</p>