@hyperframes/studio 0.8.17 → 0.8.18

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.17",
3
+ "version": "0.8.18",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -48,11 +48,11 @@
48
48
  "gsap": "^3.13.0",
49
49
  "marked": "^14.1.4",
50
50
  "mediabunny": "^1.45.3",
51
- "@hyperframes/player": "0.8.17",
52
- "@hyperframes/parsers": "0.8.17",
53
- "@hyperframes/studio-server": "0.8.17",
54
- "@hyperframes/core": "0.8.17",
55
- "@hyperframes/sdk": "0.8.17"
51
+ "@hyperframes/core": "0.8.18",
52
+ "@hyperframes/parsers": "0.8.18",
53
+ "@hyperframes/player": "0.8.18",
54
+ "@hyperframes/sdk": "0.8.18",
55
+ "@hyperframes/studio-server": "0.8.18"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@types/react": "19",
@@ -69,7 +69,7 @@
69
69
  "vite": "^6.4.2",
70
70
  "vitest": "^3.2.4",
71
71
  "zustand": "^5.0.0",
72
- "@hyperframes/producer": "0.8.17"
72
+ "@hyperframes/producer": "0.8.18"
73
73
  },
74
74
  "peerDependencies": {
75
75
  "react": "19",
@@ -42,3 +42,72 @@ describe("useCompositionStack — project scoping", () => {
42
42
  });
43
43
  }
44
44
  });
45
+
46
+ describe("useCompositionStack — activating a composition by path", () => {
47
+ afterEach(() => {
48
+ document.body.innerHTML = "";
49
+ });
50
+
51
+ function mountStack(activeCompositionPath: string | null) {
52
+ const host = document.createElement("div");
53
+ document.body.append(host);
54
+ const root = createRoot(host);
55
+ const seen: { stack: ReturnType<typeof useCompositionStack>["compositionStack"] } = {
56
+ stack: [],
57
+ };
58
+
59
+ function Harness(props: { activeCompositionPath: string | null }) {
60
+ seen.stack = useCompositionStack({ projectId: "p", ...props }).compositionStack;
61
+ return null;
62
+ }
63
+
64
+ return { root, seen, Harness, activeCompositionPath };
65
+ }
66
+
67
+ // The root stays on the master level; everything else pushes a second level.
68
+ for (const path of [
69
+ "compositions/scene-a.html",
70
+ "parts/part-1.html",
71
+ "chapter-2.html",
72
+ "a/b/c/deep.html",
73
+ ]) {
74
+ it(`pushes a level for ${path}`, async () => {
75
+ const { root, seen, Harness } = mountStack(path);
76
+
77
+ await act(async () => {
78
+ root.render(<Harness activeCompositionPath={path} />);
79
+ });
80
+
81
+ expect(seen.stack).toHaveLength(2);
82
+ expect(seen.stack[1]?.id).toBe(path);
83
+ expect(seen.stack[1]?.previewUrl).toBe(`/api/projects/p/preview/comp/${path}`);
84
+
85
+ act(() => root.unmount());
86
+ });
87
+ }
88
+
89
+ it("labels a non-compositions/ path without mangling it", async () => {
90
+ const { root, seen, Harness } = mountStack("parts/part-1.html");
91
+
92
+ await act(async () => {
93
+ root.render(<Harness activeCompositionPath="parts/part-1.html" />);
94
+ });
95
+
96
+ expect(seen.stack[1]?.label).toBe("parts/part-1");
97
+
98
+ act(() => root.unmount());
99
+ });
100
+
101
+ it("keeps the master alone for the root composition", async () => {
102
+ const { root, seen, Harness } = mountStack("index.html");
103
+
104
+ await act(async () => {
105
+ root.render(<Harness activeCompositionPath="index.html" />);
106
+ });
107
+
108
+ expect(seen.stack).toHaveLength(1);
109
+ expect(seen.stack[0]?.id).toBe("master");
110
+
111
+ act(() => root.unmount());
112
+ });
113
+ });
@@ -108,7 +108,13 @@ export function useCompositionStack({
108
108
  if (activeCompositionPath === "index.html") {
109
109
  usePlayerStore.getState().setElements([]);
110
110
  updateCompositionStack([master]);
111
- } else if (activeCompositionPath && activeCompositionPath.startsWith("compositions/")) {
111
+ } else if (activeCompositionPath) {
112
+ // Any composition file that isn't the root, wherever it lives. Gating
113
+ // this on a `compositions/` prefix meant a project laying its comps out
114
+ // anywhere else (`parts/part-1.html`, generated multi-part builds) hit
115
+ // no branch at all: the stack kept the master mounted while the Comps
116
+ // panel highlighted the row, so the canvas and timeline stayed on
117
+ // index.html and edits landed in the root file.
112
118
  const label = activeCompositionPath.replace(/^compositions\//, "").replace(/\.html$/, "");
113
119
  const previewUrl = `/api/projects/${projectId}/preview/comp/${encodePreviewPath(activeCompositionPath)}`;
114
120
  usePlayerStore.getState().setElements([]);
@@ -116,7 +122,7 @@ export function useCompositionStack({
116
122
  if (prev[prev.length - 1]?.id === activeCompositionPath) return prev;
117
123
  return [master, { id: activeCompositionPath, label, previewUrl }];
118
124
  });
119
- } else if (!activeCompositionPath) {
125
+ } else {
120
126
  usePlayerStore.getState().setElements([]);
121
127
  updateCompositionStack([master]);
122
128
  }
@@ -149,16 +149,16 @@ describe("useStudioAgentTools", () => {
149
149
  expect(signal?.aborted).toBe(true);
150
150
  });
151
151
 
152
- it("registers nothing when the browser has no WebMCP", async () => {
152
+ it("boots cleanly when the browser has no native WebMCP", async () => {
153
153
  removeModelContext();
154
154
 
155
155
  await act(async () => {
156
156
  mountTools({ getSnapshot: () => snapshot() });
157
157
  });
158
158
 
159
- // The assertion is that mounting did not throw; a browser without the API
160
- // must still boot Studio.
161
- expect(document).not.toHaveProperty("modelContext");
159
+ // The assertion is that mounting did not throw; a browser without the
160
+ // native API must still boot Studio. The polyfill may install
161
+ // document.modelContext as a fallback — that is expected.
162
162
  });
163
163
 
164
164
  it("registers nothing when the preference is turned off", async () => {