@hyperframes/studio 0.8.24 → 0.8.26

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 (93) hide show
  1. package/dist/assets/{hyperframes-player-CTQCs_KG.js → hyperframes-player-C-CArn13.js} +1 -1
  2. package/dist/assets/{index-okdE01ky.js → index-BIXyJgK5.js} +1 -1
  3. package/dist/assets/{index-Bq3M0sjr.js → index-D5yni1t6.js} +218 -218
  4. package/dist/assets/index-DnRfAiK2.css +1 -0
  5. package/dist/assets/{index-9WJFcXTP.js → index-pfeP2Y63.js} +1 -1
  6. package/dist/{chunk-6BT6DTB4.js → chunk-A7S5SGNA.js} +2 -2
  7. package/dist/chunk-A7S5SGNA.js.map +1 -0
  8. package/dist/{domEditingLayers-URA7BLWE.js → domEditingLayers-JMMVOGTO.js} +2 -2
  9. package/dist/index.d.ts +4 -0
  10. package/dist/index.html +2 -2
  11. package/dist/index.js +6230 -5019
  12. package/dist/index.js.map +1 -1
  13. package/package.json +8 -7
  14. package/src/components/editor/TopologyLens.test.tsx +294 -0
  15. package/src/components/editor/TopologyLens.tsx +206 -0
  16. package/src/components/editor/domEditOverlayGeometry.ts +14 -0
  17. package/src/components/editor/domEditOverlayGeometryBatch.test.ts +18 -0
  18. package/src/components/editor/domEditingLayers.test.ts +9 -0
  19. package/src/components/editor/domEditingLayers.ts +8 -8
  20. package/src/components/editor/topologyLensGeometry.test.ts +44 -0
  21. package/src/components/editor/topologyLensGeometry.ts +119 -0
  22. package/src/components/editor/topologyLensState.test.ts +116 -0
  23. package/src/components/editor/topologyLensState.ts +72 -0
  24. package/src/components/nle/PreviewOverlays.topologyLens.test.tsx +126 -0
  25. package/src/components/nle/PreviewOverlays.tsx +24 -18
  26. package/src/components/sidebar/CompositionsTab.drag.test.tsx +16 -0
  27. package/src/components/sidebar/CompositionsTab.tsx +10 -3
  28. package/src/components/ui/HyperframesLoader.tsx +3 -48
  29. package/src/components/ui/HyperframesMark.tsx +53 -0
  30. package/src/contexts/DomEditContext.tsx +12 -0
  31. package/src/hooks/domEditCommitRunner.test.ts +59 -0
  32. package/src/hooks/domEditCommitRunner.ts +55 -20
  33. package/src/hooks/domEditCommitTypes.ts +8 -1
  34. package/src/hooks/domEditTextCommitPlan.ts +47 -0
  35. package/src/hooks/timelineTrackVisibility.test.ts +7 -1
  36. package/src/hooks/timelineTrackVisibility.ts +5 -5
  37. package/src/hooks/useDomEditAttributeCommits.ts +4 -1
  38. package/src/hooks/useDomEditCommits.test.tsx +243 -8
  39. package/src/hooks/useDomEditCommits.ts +69 -14
  40. package/src/hooks/useDomEditPositionPatchCommit.test.tsx +3 -4
  41. package/src/hooks/useDomEditPositionPatchCommit.ts +27 -29
  42. package/src/hooks/useDomEditSession.ts +6 -2
  43. package/src/hooks/useDomEditTextCommits.test.tsx +86 -2
  44. package/src/hooks/useDomEditTextCommits.ts +64 -86
  45. package/src/hooks/useDomEditWiring.ts +1 -1
  46. package/src/hooks/useDomSelection.ts +2 -0
  47. package/src/hooks/useDomSelectionTypes.ts +2 -0
  48. package/src/hooks/useExternalFileChangeCoordinator.test.tsx +71 -4
  49. package/src/hooks/useExternalFileChangeCoordinator.ts +18 -9
  50. package/src/hooks/useGsapAnimationFetchFallback.test.ts +13 -1
  51. package/src/hooks/useGsapAnimationFetchFallback.ts +7 -3
  52. package/src/hooks/useGsapAnimationOps.test.tsx +101 -0
  53. package/src/hooks/useGsapAnimationOps.ts +4 -4
  54. package/src/hooks/useGsapAwareEditing.test.tsx +30 -2
  55. package/src/hooks/useGsapAwareEditing.ts +31 -10
  56. package/src/hooks/useGsapSelectionHandlers.test.tsx +3 -1
  57. package/src/hooks/useGsapSelectionHandlers.ts +21 -11
  58. package/src/hooks/useRenderClipContent.test.ts +27 -0
  59. package/src/hooks/useRenderClipContent.ts +5 -0
  60. package/src/hooks/useStudioExternalFileChanges.ts +5 -0
  61. package/src/player/components/CompositionThumbnail.test.ts +66 -1
  62. package/src/player/components/CompositionThumbnail.tsx +6 -0
  63. package/src/player/hooks/timelineSyncHydration.ts +10 -2
  64. package/src/player/store/playerStore.test.ts +24 -0
  65. package/src/player/store/thumbnailSlice.ts +6 -0
  66. package/src/styles/studio.css +179 -0
  67. package/src/utils/domEditSaveQueue.test.ts +5 -3
  68. package/src/utils/domEditSaveQueue.ts +6 -1
  69. package/src/webmcp/StudioAgentTools.test.ts +70 -0
  70. package/src/webmcp/StudioAgentTools.tsx +89 -21
  71. package/src/webmcp/handles.test.ts +172 -15
  72. package/src/webmcp/handles.ts +152 -35
  73. package/src/webmcp/tools/animationTools.test.ts +241 -60
  74. package/src/webmcp/tools/animationTools.ts +191 -104
  75. package/src/webmcp/tools/contentTools.test.ts +103 -41
  76. package/src/webmcp/tools/contentTools.ts +149 -105
  77. package/src/webmcp/tools/inspectTools.test.ts +47 -5
  78. package/src/webmcp/tools/inspectTools.ts +59 -19
  79. package/src/webmcp/tools/lookTools.test.ts +185 -38
  80. package/src/webmcp/tools/lookTools.ts +134 -21
  81. package/src/webmcp/tools/selectionTools.test.ts +105 -9
  82. package/src/webmcp/tools/selectionTools.ts +34 -19
  83. package/src/webmcp/tools/targetedWriteTools.test.ts +125 -0
  84. package/src/webmcp/tools/transformTools.test.ts +140 -54
  85. package/src/webmcp/tools/transformTools.ts +215 -56
  86. package/src/webmcp/useStudioAgentTools.test.tsx +310 -10
  87. package/src/webmcp/useStudioAgentTools.ts +27 -15
  88. package/src/webmcp/webmcpTestUtils.ts +37 -0
  89. package/src/webmcp/writeCoordinator.test.ts +375 -0
  90. package/src/webmcp/writeCoordinator.ts +473 -0
  91. package/dist/assets/index-yGhfxxoL.css +0 -1
  92. package/dist/chunk-6BT6DTB4.js.map +0 -1
  93. /package/dist/{domEditingLayers-URA7BLWE.js.map → domEditingLayers-JMMVOGTO.js.map} +0 -0
@@ -0,0 +1,375 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+ import { mintElementHandle } from "./handles";
4
+ import { previewDoc, selectionFor } from "./webmcpTestUtils";
5
+ import {
6
+ dispatched,
7
+ runTargetedWrite,
8
+ saved,
9
+ studioEditLifecycle,
10
+ type TargetedWriteDeps,
11
+ } from "./writeCoordinator";
12
+
13
+ function fixture() {
14
+ const doc = previewDoc('<div id="target">Target</div>');
15
+ const element = doc.getElementById("target") as HTMLElement;
16
+ const handle = mintElementHandle({
17
+ projectId: "project-a",
18
+ domId: "target",
19
+ sourceFile: "index.html",
20
+ activeCompositionPath: "index.html",
21
+ });
22
+ if (!handle) throw new Error("expected handle");
23
+ const deps: TargetedWriteDeps = {
24
+ getPreviewDocument: () => doc,
25
+ getProjectId: () => "project-a",
26
+ getWriteBlockedReason: () => null,
27
+ buildSelection: async (target) => selectionFor(target),
28
+ applySelection: () => undefined,
29
+ };
30
+ return { deps, element, handle };
31
+ }
32
+
33
+ afterEach(() => {
34
+ studioEditLifecycle.reset();
35
+ document.body.replaceChildren();
36
+ });
37
+
38
+ describe("studio write coordinator", () => {
39
+ it("refuses legacy and stale-project handles before resolving or calling the actor", async () => {
40
+ const { deps } = fixture();
41
+ const actor = vi.fn();
42
+ const buildSelection = vi.fn(deps.buildSelection);
43
+ const legacy = mintElementHandle({
44
+ domId: "target",
45
+ sourceFile: "index.html",
46
+ activeCompositionPath: "index.html",
47
+ });
48
+ const staleProject = mintElementHandle({
49
+ projectId: "project-b",
50
+ domId: "target",
51
+ sourceFile: "index.html",
52
+ activeCompositionPath: "index.html",
53
+ });
54
+
55
+ const legacyResult = await runTargetedWrite(
56
+ { ...deps, buildSelection },
57
+ {
58
+ handle: legacy,
59
+ operation: "set-text",
60
+ signal: new AbortController().signal,
61
+ write: actor,
62
+ },
63
+ );
64
+ const staleResult = await runTargetedWrite(
65
+ { ...deps, buildSelection },
66
+ {
67
+ handle: staleProject,
68
+ operation: "set-text",
69
+ signal: new AbortController().signal,
70
+ write: actor,
71
+ },
72
+ );
73
+
74
+ expect(legacyResult).toMatchObject({ ok: false, stage: "refused", kind: "invalid" });
75
+ expect(staleResult).toMatchObject({
76
+ ok: false,
77
+ stage: "refused",
78
+ kind: "invalid",
79
+ reason: "the handle belongs to a different project",
80
+ });
81
+ expect(buildSelection).not.toHaveBeenCalled();
82
+ expect(actor).not.toHaveBeenCalled();
83
+ });
84
+
85
+ it("refuses an unavailable preview with its exact reason before calling the actor", async () => {
86
+ const { deps, handle } = fixture();
87
+ const actor = vi.fn();
88
+
89
+ const result = await runTargetedWrite(
90
+ { ...deps, getPreviewDocument: () => null },
91
+ {
92
+ handle,
93
+ operation: "set-text",
94
+ signal: new AbortController().signal,
95
+ write: actor,
96
+ },
97
+ );
98
+
99
+ expect(result).toEqual({
100
+ ok: false,
101
+ kind: "blocked",
102
+ reason: "the composition preview is not ready",
103
+ stage: "refused",
104
+ operation: "set-text",
105
+ });
106
+ expect(actor).not.toHaveBeenCalled();
107
+ });
108
+
109
+ it("refuses a missing handle with its exact reason before calling the actor", async () => {
110
+ const { deps } = fixture();
111
+ const actor = vi.fn();
112
+
113
+ const result = await runTargetedWrite(deps, {
114
+ handle: mintElementHandle({
115
+ projectId: "project-a",
116
+ domId: "missing",
117
+ sourceFile: "index.html",
118
+ activeCompositionPath: "index.html",
119
+ }),
120
+ operation: "set-text",
121
+ signal: new AbortController().signal,
122
+ write: actor,
123
+ });
124
+
125
+ expect(result).toEqual({
126
+ ok: false,
127
+ kind: "invalid",
128
+ reason: "the target handle is stale",
129
+ hint: "Call studio_look and retry.",
130
+ stage: "refused",
131
+ operation: "set-text",
132
+ });
133
+ expect(actor).not.toHaveBeenCalled();
134
+ });
135
+
136
+ it("refuses an unsupported element with its exact reason before calling the actor", async () => {
137
+ const { deps, handle } = fixture();
138
+ const actor = vi.fn();
139
+
140
+ const result = await runTargetedWrite(
141
+ { ...deps, buildSelection: async () => null },
142
+ {
143
+ handle,
144
+ operation: "set-text",
145
+ signal: new AbortController().signal,
146
+ write: actor,
147
+ },
148
+ );
149
+
150
+ expect(result).toEqual({
151
+ ok: false,
152
+ kind: "blocked",
153
+ reason: "the target resolved to an element Studio cannot edit",
154
+ hint: "Try a parent or child element from studio_look.",
155
+ stage: "refused",
156
+ operation: "set-text",
157
+ });
158
+ expect(actor).not.toHaveBeenCalled();
159
+ });
160
+
161
+ it("refuses a twice-replaced preview with its exact reason before calling the actor", async () => {
162
+ const firstDoc = previewDoc('<div id="target">First</div>');
163
+ const secondDoc = previewDoc('<div id="target">Second</div>');
164
+ const thirdDoc = previewDoc('<div id="target">Third</div>');
165
+ const handle = mintElementHandle({
166
+ projectId: "project-a",
167
+ domId: "target",
168
+ sourceFile: "index.html",
169
+ activeCompositionPath: "index.html",
170
+ });
171
+ let currentDoc = firstDoc;
172
+ const actor = vi.fn();
173
+ const result = await runTargetedWrite(
174
+ {
175
+ getPreviewDocument: () => currentDoc,
176
+ getProjectId: () => "project-a",
177
+ getWriteBlockedReason: () => null,
178
+ buildSelection: async (element) => {
179
+ currentDoc = currentDoc === firstDoc ? secondDoc : thirdDoc;
180
+ return selectionFor(element);
181
+ },
182
+ applySelection: () => undefined,
183
+ },
184
+ {
185
+ handle,
186
+ operation: "set-text",
187
+ signal: new AbortController().signal,
188
+ write: actor,
189
+ },
190
+ );
191
+
192
+ expect(result).toEqual({
193
+ ok: false,
194
+ kind: "invalid",
195
+ reason: "the target changed while it was resolving",
196
+ hint: "Call studio_look again.",
197
+ stage: "refused",
198
+ operation: "set-text",
199
+ });
200
+ expect(actor).not.toHaveBeenCalled();
201
+ });
202
+
203
+ it("returns a structured refusal when async preflight fails before dispatch", async () => {
204
+ const { deps, handle } = fixture();
205
+ const actor = vi.fn();
206
+
207
+ const result = await runTargetedWrite(deps, {
208
+ handle,
209
+ operation: "update-animation",
210
+ signal: new AbortController().signal,
211
+ preflight: async () => {
212
+ throw new Error("animation ownership is unavailable");
213
+ },
214
+ write: actor,
215
+ });
216
+
217
+ expect(result).toMatchObject({
218
+ ok: false,
219
+ stage: "refused",
220
+ kind: "failed",
221
+ reason: "animation ownership is unavailable",
222
+ });
223
+ expect(actor).not.toHaveBeenCalled();
224
+ });
225
+
226
+ it("refuses when the active project changes during target resolution", async () => {
227
+ const { deps, handle } = fixture();
228
+ const actor = vi.fn();
229
+ let projectId = "project-a";
230
+
231
+ const result = await runTargetedWrite(
232
+ {
233
+ ...deps,
234
+ getProjectId: () => projectId,
235
+ buildSelection: async (element) => {
236
+ projectId = "project-b";
237
+ return selectionFor(element);
238
+ },
239
+ },
240
+ {
241
+ handle,
242
+ operation: "set-text",
243
+ signal: new AbortController().signal,
244
+ write: actor,
245
+ },
246
+ );
247
+
248
+ expect(result).toMatchObject({
249
+ ok: false,
250
+ stage: "refused",
251
+ kind: "blocked",
252
+ reason: "the active project changed while the target was resolving",
253
+ });
254
+ expect(actor).not.toHaveBeenCalled();
255
+ });
256
+
257
+ it("publishes nothing when cancellation refuses the write before dispatch", async () => {
258
+ const { deps, handle } = fixture();
259
+ const actor = vi.fn();
260
+ const controller = new AbortController();
261
+ controller.abort();
262
+
263
+ const result = await runTargetedWrite(deps, {
264
+ handle,
265
+ operation: "set-text",
266
+ signal: controller.signal,
267
+ write: actor,
268
+ });
269
+
270
+ expect(result).toMatchObject({ ok: false, stage: "refused", cancelRequested: true });
271
+ expect(actor).not.toHaveBeenCalled();
272
+ expect(studioEditLifecycle.getSnapshot()).toEqual({ phase: "idle" });
273
+ });
274
+
275
+ it("marks repeat targets only after a terminal receipt and resets on project switch", async () => {
276
+ const { deps, handle } = fixture();
277
+ await runTargetedWrite(deps, {
278
+ handle,
279
+ operation: "set-text",
280
+ signal: new AbortController().signal,
281
+ write: async () => {
282
+ expect(studioEditLifecycle.getSnapshot()).toMatchObject({ targetChanged: true });
283
+ return saved(
284
+ { text: "first", changed: true },
285
+ { sourceFile: "index.html", version: "v1", changed: true },
286
+ );
287
+ },
288
+ });
289
+
290
+ await runTargetedWrite(deps, {
291
+ handle,
292
+ operation: "set-text",
293
+ signal: new AbortController().signal,
294
+ write: async () => {
295
+ expect(studioEditLifecycle.getSnapshot()).toMatchObject({ targetChanged: false });
296
+ return dispatched({ text: "second", changed: true }, true);
297
+ },
298
+ });
299
+
300
+ studioEditLifecycle.activateProject("project-b");
301
+ expect(studioEditLifecycle.getSnapshot()).toEqual({ phase: "idle" });
302
+ });
303
+
304
+ it("does not let an older completion overwrite the newer invocation", () => {
305
+ const target = { handle: "dom:target", sourceFile: "index.html" };
306
+ const older = studioEditLifecycle.begin("project-a", target, "set-text");
307
+ const newer = studioEditLifecycle.begin("project-a", target, "set-style");
308
+
309
+ studioEditLifecycle.finish(older, {
310
+ ok: true,
311
+ stage: "saved",
312
+ target,
313
+ operation: "set-text",
314
+ changed: true,
315
+ evidence: { kind: "content-version", sourceFile: "index.html", version: "v1" },
316
+ });
317
+
318
+ expect(studioEditLifecycle.getSnapshot()).toMatchObject({
319
+ callId: newer,
320
+ operation: "set-style",
321
+ phase: "dispatching",
322
+ });
323
+ });
324
+
325
+ it("does not let an older dismissal clear the newer invocation", () => {
326
+ const target = { handle: "dom:target", sourceFile: "index.html" };
327
+ const older = studioEditLifecycle.begin("project-a", target, "set-text");
328
+ const newer = studioEditLifecycle.begin("project-a", target, "set-style");
329
+
330
+ studioEditLifecycle.dismiss(older);
331
+
332
+ expect(studioEditLifecycle.getSnapshot()).toMatchObject({
333
+ callId: newer,
334
+ operation: "set-style",
335
+ phase: "dispatching",
336
+ });
337
+ });
338
+
339
+ it("dismisses its terminal call without forgetting repeated-target identity", () => {
340
+ const target = { handle: "dom:target", sourceFile: "index.html" };
341
+ const first = studioEditLifecycle.begin("project-a", target, "set-text");
342
+ studioEditLifecycle.finish(first, {
343
+ ok: true,
344
+ stage: "saved",
345
+ target,
346
+ operation: "set-text",
347
+ changed: true,
348
+ evidence: { kind: "content-version", sourceFile: "index.html", version: "v1" },
349
+ });
350
+
351
+ studioEditLifecycle.dismiss(first);
352
+ const repeated = studioEditLifecycle.begin("project-a", target, "set-style");
353
+
354
+ expect(studioEditLifecycle.getSnapshot()).toMatchObject({
355
+ callId: repeated,
356
+ targetChanged: false,
357
+ });
358
+ });
359
+
360
+ it("finishes the owning lifecycle as failed when an actor throws", async () => {
361
+ const { deps, handle } = fixture();
362
+
363
+ const result = await runTargetedWrite(deps, {
364
+ handle,
365
+ operation: "set-text",
366
+ signal: new AbortController().signal,
367
+ write: async () => {
368
+ throw new Error("network down");
369
+ },
370
+ });
371
+
372
+ expect(result).toMatchObject({ ok: false, stage: "failed", reason: "network down" });
373
+ expect(studioEditLifecycle.getSnapshot()).toMatchObject({ phase: "failed", receipt: result });
374
+ });
375
+ });