@hyperframes/studio 0.8.124 → 0.8.126

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 (69) hide show
  1. package/dist/assets/{hyperframes-player-ColOZ_78.js → hyperframes-player-BbbN-wWN.js} +1 -1
  2. package/dist/assets/{index-BuSTrK-Z.js → index-B8kU2xXl.js} +177 -177
  3. package/dist/assets/{index-CVuUfURE.js → index-Cll7uTNy.js} +1 -1
  4. package/dist/assets/{index-jQEqFKMk.js → index-nvLF29ZH.js} +1 -1
  5. package/dist/index.d.ts +43 -35
  6. package/dist/index.html +1 -1
  7. package/dist/index.js +4458 -4072
  8. package/dist/index.js.map +1 -1
  9. package/package.json +9 -8
  10. package/src/components/editor/DomEditSelectionChrome.test.tsx +5 -0
  11. package/src/components/editor/DomEditSelectionChrome.tsx +1 -0
  12. package/src/components/editor/MotionPathNode.tsx +56 -0
  13. package/src/components/editor/MotionPathOverlay.layerPress.test.tsx +140 -48
  14. package/src/components/editor/MotionPathOverlay.test.tsx +45 -0
  15. package/src/components/editor/MotionPathOverlay.tsx +30 -61
  16. package/src/components/editor/canvasContextMenuZOrder.ts +1 -10
  17. package/src/components/editor/domEditOverlayGestures.ts +2 -0
  18. package/src/components/editor/domEditOverlayStartGesture.ts +6 -0
  19. package/src/components/editor/dragUndoPaint.test.ts +170 -2
  20. package/src/components/editor/gestureUndoRevert.ts +104 -0
  21. package/src/components/editor/manualEditsSeekReapply.ts +3 -6
  22. package/src/components/editor/manualOffsetDrag.ts +3 -15
  23. package/src/components/editor/motionPathGeometry.test.ts +48 -1
  24. package/src/components/editor/motionPathGeometry.ts +46 -8
  25. package/src/components/editor/motionPathHome.ts +75 -0
  26. package/src/components/editor/motionPathLayerNode.test.ts +43 -35
  27. package/src/components/editor/motionPathLayerNode.ts +20 -16
  28. package/src/components/editor/reapplyPositionEditsAfterSeek.test.ts +20 -0
  29. package/src/components/editor/studioMotion.ts +7 -19
  30. package/src/components/editor/useDomEditNudge.ts +1 -1
  31. package/src/components/editor/useDomEditOverlayGestures.ts +9 -5
  32. package/src/components/editor/useMotionPathData.test.tsx +236 -0
  33. package/src/components/editor/useMotionPathData.ts +23 -61
  34. package/src/components/sidebar/CompositionsTab.thumbnails.test.tsx +1 -0
  35. package/src/hooks/gestureTransaction.test.ts +60 -0
  36. package/src/hooks/gestureTransaction.ts +5 -2
  37. package/src/hooks/gsapGestureOutcome.ts +9 -2
  38. package/src/hooks/gsapParsedTween.ts +16 -0
  39. package/src/hooks/gsapResizeDropPoint.test.ts +39 -1
  40. package/src/hooks/gsapResizeElementSize.test.tsx +27 -0
  41. package/src/hooks/gsapResizeIntercept.ts +55 -68
  42. package/src/hooks/gsapRuntimeKeyframes.realGsap.test.ts +48 -0
  43. package/src/hooks/gsapRuntimeKeyframes.ts +10 -3
  44. package/src/hooks/gsapScriptCommitTypes.ts +2 -0
  45. package/src/hooks/gsapShared.ts +2 -1
  46. package/src/hooks/gsapShared.writeTarget.test.ts +10 -0
  47. package/src/hooks/intersectionObserverTestUtils.ts +16 -0
  48. package/src/hooks/resizeObserverTestUtils.ts +9 -2
  49. package/src/hooks/timelineEditingHelpers.ts +2 -9
  50. package/src/hooks/useGsapAwareEditing.groupPlan.test.tsx +29 -1
  51. package/src/hooks/useGsapAwareEditing.test.tsx +1 -0
  52. package/src/hooks/useGsapAwareEditing.ts +21 -25
  53. package/src/hooks/useGsapAwareGroupMove.ts +4 -6
  54. package/src/hooks/useGsapScriptCommits.test.tsx +42 -1
  55. package/src/hooks/useGsapScriptCommits.ts +12 -3
  56. package/src/hooks/useThumbnailStripSize.test.tsx +236 -1
  57. package/src/hooks/useThumbnailStripSize.ts +206 -17
  58. package/src/noWindowBoundDomChecks.test.ts +31 -0
  59. package/src/player/components/CompositionThumbnail.tsx +10 -6
  60. package/src/player/components/ImageThumbnail.tsx +11 -5
  61. package/src/player/components/ThumbnailTiles.test.tsx +30 -0
  62. package/src/player/components/ThumbnailTiles.tsx +41 -0
  63. package/src/player/components/VideoThumbnail.test.tsx +116 -9
  64. package/src/player/components/VideoThumbnail.tsx +11 -5
  65. package/src/player/lib/timelineGroupInfo.realm.test.ts +20 -0
  66. package/src/player/lib/timelineGroupInfo.ts +2 -1
  67. package/src/utils/studioPendingEdits.test.ts +52 -0
  68. package/src/utils/studioPendingEdits.ts +55 -10
  69. package/src/webmcp/webmcpTestUtils.ts +2 -2
@@ -2,6 +2,7 @@
2
2
  import { describe, expect, it, vi } from "vitest";
3
3
  import {
4
4
  addStudioPendingEditFlushListener,
5
+ adoptingStudioPendingEdit,
5
6
  beginStudioPendingEdit,
6
7
  flushStudioPendingEdits,
7
8
  hasStudioPendingEdits,
@@ -312,3 +313,54 @@ describe("a drain of only the current edits", () => {
312
313
  landLater();
313
314
  });
314
315
  });
316
+
317
+ describe("an edit undo painted back while it saves", () => {
318
+ function paintedBackEdit() {
319
+ const box = { look: "edited" };
320
+ const edit = beginStudioPendingEdit(() => {
321
+ const shown = box.look;
322
+ box.look = "start";
323
+ return () => (box.look = shown);
324
+ });
325
+ let landSave!: () => void;
326
+ const inFlight = edit.adopt(() => adoptingStudioPendingEdit())!;
327
+ edit.settle(new Promise<void>((resolve) => (landSave = resolve)));
328
+ const shown = paintBackNewestStudioPendingEdit()!;
329
+ return { box, inFlight, shown, landSave };
330
+ }
331
+
332
+ it("draws on the edit as shown, then shows it undone again in the same task", () => {
333
+ const { box, inFlight, shown, landSave } = paintedBackEdit();
334
+ let drawnOver = "";
335
+ inFlight.drawKeepingUndone(() => {
336
+ drawnOver = box.look;
337
+ box.look += "+size";
338
+ });
339
+ expect(drawnOver).toBe("edited");
340
+ expect(box.look).toBe("start");
341
+ shown.showAgain();
342
+ expect(box.look).toBe("edited+size");
343
+ landSave();
344
+ });
345
+
346
+ it("keeps a redraw until the edit is shown again", () => {
347
+ const { inFlight, shown, landSave } = paintedBackEdit();
348
+ const redraw = vi.fn();
349
+ inFlight.drawUnlessUndone(redraw);
350
+ expect(redraw).not.toHaveBeenCalled();
351
+ shown.showAgain();
352
+ expect(redraw).toHaveBeenCalledTimes(1);
353
+ landSave();
354
+ });
355
+ });
356
+
357
+ it("counts an edit whose later write failed as saved once an earlier write landed, so undo still steps it", async () => {
358
+ const edit = beginStudioPendingEdit(() => () => undefined);
359
+ const committed = edit.adopt(async () => {
360
+ adoptingStudioPendingEdit()!.markSaved();
361
+ throw new Error("the crop save failed");
362
+ });
363
+ edit.settle(committed.catch(() => undefined));
364
+ const shown = paintBackNewestStudioPendingEdit()!;
365
+ await expect(shown.landed()).resolves.toBe(true);
366
+ });
@@ -14,11 +14,25 @@ export type StudioEditRevert = () => () => void;
14
14
  interface PendingEdit {
15
15
  revert: StudioEditRevert | null;
16
16
  landed: () => Promise<boolean>;
17
+ redraws: Array<() => void>;
18
+ showAgain: (() => void) | null;
19
+ }
20
+
21
+ export interface StudioEditInFlight {
22
+ reverted: () => boolean;
23
+ within: <T>(run: () => T) => T;
24
+ drawUnlessUndone: (draw: () => void) => void;
25
+ drawKeepingUndone: <T>(draw: () => T) => T;
26
+ markSaved: () => void;
17
27
  }
18
28
 
19
29
  const pendingEdits = new Map<Promise<unknown>, PendingEdit>();
20
30
  const NOT_SAVED = () => Promise.resolve(false);
21
- let adopting = false;
31
+ let adopting: StudioEditInFlight | null = null;
32
+
33
+ export function adoptingStudioPendingEdit(): StudioEditInFlight | null {
34
+ return adopting;
35
+ }
22
36
 
23
37
  function waitForPostBlurEffects(): Promise<void> {
24
38
  return new Promise((resolve) => setTimeout(resolve, 0));
@@ -73,7 +87,7 @@ export function trackStudioPendingEdit(
73
87
  if (!result) return undefined;
74
88
  const promise = Promise.resolve(result);
75
89
  if (adopting) return promise;
76
- pendingEdits.set(promise, { revert: null, landed: NOT_SAVED });
90
+ pendingEdits.set(promise, { revert: null, landed: NOT_SAVED, redraws: [], showAgain: null });
77
91
  promise.then(
78
92
  () => pendingEdits.delete(promise),
79
93
  () => pendingEdits.delete(promise),
@@ -97,25 +111,49 @@ export function beginStudioPendingEdit(revert: StudioEditRevert | null) {
97
111
  const entry = pendingEdits.get(promise)!;
98
112
  entry.revert = revert;
99
113
  let landed = Promise.resolve(false);
114
+ let saved = false;
100
115
  entry.landed = () => landed;
116
+ const inFlight: StudioEditInFlight = {
117
+ reverted: () => entry.revert === null && revert !== null,
118
+ within(run) {
119
+ const outer = adopting;
120
+ adopting = inFlight;
121
+ try {
122
+ return run();
123
+ } finally {
124
+ adopting = outer;
125
+ }
126
+ },
127
+ drawUnlessUndone(draw) {
128
+ if (inFlight.reverted()) entry.redraws.push(draw);
129
+ else draw();
130
+ },
131
+ drawKeepingUndone(draw) {
132
+ if (!inFlight.reverted()) return draw();
133
+ entry.showAgain?.();
134
+ try {
135
+ return draw();
136
+ } finally {
137
+ entry.showAgain = revert!();
138
+ }
139
+ },
140
+ markSaved: () => void (saved = true),
141
+ };
101
142
  return {
102
143
  settle,
103
- reverted: () => entry.revert === null && revert !== null,
104
- // Only what `start` registers synchronously is adopted; a registration after an await is a newer edit.
144
+ reverted: inFlight.reverted,
145
+ // Only what `start` registers synchronously is adopted; a later write joins only through `within`.
105
146
  adopt<T>(start: () => T): T {
106
- adopting = true;
107
147
  try {
108
- const committed = start();
148
+ const committed = inFlight.within(start);
109
149
  landed = Promise.resolve(committed).then(
110
150
  () => true,
111
- () => false,
151
+ () => saved,
112
152
  );
113
153
  return committed;
114
154
  } catch (error) {
115
155
  settle();
116
156
  throw error;
117
- } finally {
118
- adopting = false;
119
157
  }
120
158
  },
121
159
  };
@@ -129,7 +167,14 @@ export function paintBackNewestStudioPendingEdit(): {
129
167
  const revert = newest?.revert;
130
168
  if (!newest || !revert) return null;
131
169
  newest.revert = null;
132
- return { showAgain: revert(), landed: newest.landed };
170
+ newest.showAgain = revert();
171
+ return {
172
+ showAgain: () => {
173
+ newest.showAgain?.();
174
+ for (const redraw of newest.redraws.splice(0)) redraw();
175
+ },
176
+ landed: newest.landed,
177
+ };
133
178
  }
134
179
 
135
180
  export function revertNewestStudioPendingEdit(): (() => void) | null {
@@ -5,6 +5,7 @@
5
5
  * existing `hooks/domSelectionTestHarness.ts` convention.
6
6
  */
7
7
 
8
+ import { isHtmlElement } from "@hyperframes/core/runtime/dom-realm";
8
9
  import { expect } from "vitest";
9
10
  import type { DomEditSelection } from "../components/editor/domEditingTypes";
10
11
  import type { ToolFailure, ToolResult } from "./toolResult";
@@ -31,8 +32,7 @@ export function previewDoc(html: string): Document {
31
32
  export function previewElement(html: string, id: string): HTMLElement {
32
33
  const doc = previewDoc(html);
33
34
  const element = doc.getElementById(id);
34
- const HTMLElementCtor = doc.defaultView?.HTMLElement;
35
- if (!HTMLElementCtor || !(element instanceof HTMLElementCtor)) {
35
+ if (!isHtmlElement(element)) {
36
36
  throw new Error(`expected preview element #${id}`);
37
37
  }
38
38
  return element;