@hyperframes/studio 0.7.57 → 0.7.58

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 (112) hide show
  1. package/dist/assets/{hyperframes-player--Z69cEkE.js → hyperframes-player-CtTDO63S.js} +1 -1
  2. package/dist/assets/{index-DfmYkU44.js → index-B_UvTX3E.js} +204 -204
  3. package/dist/assets/{index-KsfE1bUu.js → index-C47jAC3Q.js} +1 -1
  4. package/dist/assets/{index-Bf1x1y8H.js → index-DeQPzqwH.js} +1 -1
  5. package/dist/assets/index-uahwWkgw.css +1 -0
  6. package/dist/{chunk-5QSIMBEJ.js → chunk-OBAG3GWK.js} +33 -21
  7. package/dist/chunk-OBAG3GWK.js.map +1 -0
  8. package/dist/{domEditingLayers-6LQGKPOI.js → domEditingLayers-2CKWGEHS.js} +2 -2
  9. package/dist/index.d.ts +15 -4
  10. package/dist/index.html +2 -2
  11. package/dist/index.js +5954 -4474
  12. package/dist/index.js.map +1 -1
  13. package/package.json +7 -7
  14. package/src/App.tsx +3 -3
  15. package/src/components/StudioFeedbackBar.tsx +2 -2
  16. package/src/components/editor/CanvasContextMenu.test.tsx +27 -14
  17. package/src/components/editor/CanvasContextMenu.tsx +71 -6
  18. package/src/components/editor/DomEditOverlay.tsx +5 -2
  19. package/src/components/editor/LayersPanel.tsx +110 -99
  20. package/src/components/editor/canvasContextMenuZOrder.test.ts +57 -0
  21. package/src/components/editor/canvasContextMenuZOrder.ts +40 -1
  22. package/src/components/editor/domEditingDom.ts +5 -6
  23. package/src/components/editor/domEditingElement.ts +15 -32
  24. package/src/components/editor/layersPanelSort.ts +80 -0
  25. package/src/components/editor/useLayerRevealOverride.test.ts +169 -0
  26. package/src/components/editor/useLayerRevealOverride.ts +389 -0
  27. package/src/components/nle/PreviewOverlays.tsx +27 -2
  28. package/src/components/nle/TimelinePane.test.ts +3 -0
  29. package/src/components/nle/TimelinePane.tsx +11 -1
  30. package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +440 -0
  31. package/src/components/nle/useCanvasZOrderTimelineMirror.ts +191 -0
  32. package/src/components/nle/useTimelineEditCallbacks.ts +1 -0
  33. package/src/components/nle/zLaneGesture.test.ts +176 -0
  34. package/src/components/nle/zLaneGesture.ts +77 -0
  35. package/src/hooks/domEditCommitTypes.ts +17 -1
  36. package/src/hooks/elementLifecycleOpsTestUtils.ts +25 -0
  37. package/src/hooks/fetchStubTestUtils.ts +16 -0
  38. package/src/hooks/timelineEditingHelpers.test.ts +92 -1
  39. package/src/hooks/timelineEditingHelpers.ts +32 -15
  40. package/src/hooks/timelineMoveAdapter.ts +2 -1
  41. package/src/hooks/timelineTimingSync.test.ts +703 -33
  42. package/src/hooks/timelineTimingSync.ts +282 -97
  43. package/src/hooks/timelineTrackVisibility.test.ts +3 -1
  44. package/src/hooks/timelineTrackVisibility.ts +4 -3
  45. package/src/hooks/useClipboard.ts +19 -17
  46. package/src/hooks/useDomEditCommits.test.tsx +316 -38
  47. package/src/hooks/useDomEditCommits.ts +143 -78
  48. package/src/hooks/useDomEditSession.test.tsx +3 -1
  49. package/src/hooks/useDomEditSession.ts +1 -1
  50. package/src/hooks/useElementLifecycleOps.test.tsx +426 -60
  51. package/src/hooks/useElementLifecycleOps.ts +174 -81
  52. package/src/hooks/usePreviewPersistence.ts +2 -2
  53. package/src/hooks/useTimelineEditing.test.tsx +302 -154
  54. package/src/hooks/useTimelineEditing.ts +97 -93
  55. package/src/hooks/useTimelineEditingTypes.ts +1 -1
  56. package/src/hooks/useTimelineGroupEditing.ts +35 -3
  57. package/src/player/components/Timeline.test.ts +31 -21
  58. package/src/player/components/Timeline.tsx +62 -91
  59. package/src/player/components/TimelineCanvas.tsx +36 -5
  60. package/src/player/components/TimelineLanes.tsx +32 -10
  61. package/src/player/components/TimelineOverlays.tsx +36 -0
  62. package/src/player/components/TimelineRuler.tsx +18 -5
  63. package/src/player/components/TrackGapContextMenu.tsx +116 -0
  64. package/src/player/components/timelineCallbacks.ts +4 -1
  65. package/src/player/components/timelineClipDragCommit.test.ts +279 -3
  66. package/src/player/components/timelineClipDragCommit.ts +173 -71
  67. package/src/player/components/timelineGapCommit.test.ts +163 -0
  68. package/src/player/components/timelineGapCommit.ts +103 -0
  69. package/src/player/components/timelineGaps.test.ts +221 -0
  70. package/src/player/components/timelineGaps.ts +182 -0
  71. package/src/player/components/timelineLayout.test.ts +2 -1
  72. package/src/player/components/timelineLayout.ts +22 -6
  73. package/src/player/components/timelineMarquee.test.ts +20 -10
  74. package/src/player/components/timelineMarquee.ts +9 -2
  75. package/src/player/components/timelineStackingSync.test.ts +42 -1
  76. package/src/player/components/timelineStackingSync.ts +31 -16
  77. package/src/player/components/timelineZMirror.test.ts +667 -0
  78. package/src/player/components/timelineZMirror.ts +393 -0
  79. package/src/player/components/timelineZoom.test.ts +2 -2
  80. package/src/player/components/timelineZoom.ts +7 -3
  81. package/src/player/components/useTimelineClipDrag.ts +1 -1
  82. package/src/player/components/useTimelineGapHighlights.test.ts +119 -0
  83. package/src/player/components/useTimelineGapHighlights.ts +110 -0
  84. package/src/player/components/useTimelinePlayhead.ts +4 -3
  85. package/src/player/components/useTimelineRangeSelection.ts +6 -4
  86. package/src/player/components/useTimelineScrollViewport.ts +73 -0
  87. package/src/player/components/useTimelineStackingSync.test.tsx +0 -1
  88. package/src/player/components/useTimelineStackingSync.ts +5 -3
  89. package/src/player/components/useTimelineTrackDerivations.ts +38 -0
  90. package/src/player/components/useTrackGapMenu.ts +155 -0
  91. package/src/player/hooks/useExpandedTimelineElements.test.ts +67 -4
  92. package/src/player/hooks/useExpandedTimelineElements.ts +12 -1
  93. package/src/player/hooks/useTimelineSyncCallbacks.ts +2 -0
  94. package/src/player/lib/layerOrdering.test.ts +1 -15
  95. package/src/player/lib/layerOrdering.ts +5 -28
  96. package/src/player/lib/timelineDOM.test.ts +72 -0
  97. package/src/player/lib/timelineDOM.ts +3 -0
  98. package/src/player/lib/timelineElementHelpers.ts +35 -11
  99. package/src/player/store/playerStore.ts +12 -1
  100. package/src/telemetry/events.test.ts +10 -0
  101. package/src/telemetry/events.ts +1 -0
  102. package/src/utils/domEditSaveQueue.ts +4 -3
  103. package/src/utils/editHistory.test.ts +82 -0
  104. package/src/utils/editHistory.ts +14 -2
  105. package/src/utils/gsapSoftReload.test.ts +54 -115
  106. package/src/utils/gsapSoftReload.ts +42 -152
  107. package/src/utils/gsapUndoRestore.test.ts +269 -0
  108. package/src/utils/gsapUndoRestore.ts +269 -0
  109. package/src/utils/sourceScopedSelectorIndex.ts +29 -0
  110. package/dist/assets/index-_pqzyxB1.css +0 -1
  111. package/dist/chunk-5QSIMBEJ.js.map +0 -1
  112. /package/dist/{domEditingLayers-6LQGKPOI.js.map → domEditingLayers-2CKWGEHS.js.map} +0 -0
@@ -11,6 +11,14 @@ import {
11
11
  readHfId,
12
12
  type DomEditSelection,
13
13
  } from "../components/editor/domEditing";
14
+ import { LAYER_REVEAL_PRIOR_POSITION_ATTR } from "../player/lib/timelineElementHelpers";
15
+ import {
16
+ beginLayerRevealCommit,
17
+ beginLayerZPersist,
18
+ completeLayerRevealCommit,
19
+ rollbackLayerRevealCommit,
20
+ type LayerRevealCommitOwnership,
21
+ } from "../components/editor/useLayerRevealOverride";
14
22
  import type { CommitDomEditPatchBatches, DomEditPatchBatch } from "./domEditCommitTypes";
15
23
 
16
24
  interface UseElementLifecycleOpsParams extends DomEditCommitBaseParams {
@@ -25,6 +33,36 @@ interface UseElementLifecycleOpsParams extends DomEditCommitBaseParams {
25
33
  onElementDeleted?: (selection: DomEditSelection) => void;
26
34
  }
27
35
 
36
+ // One coalesce key per z-reorder gesture. A monotonic counter — NOT Date.now()
37
+ // / Math.random(), which the determinism rules forbid — matches the
38
+ // laneChangeGestureSeq precedent in timelineClipDragCommit.ts: the key only has
39
+ // to be unique per gesture and identical across the gesture's records.
40
+ let zReorderGestureSeq = 0;
41
+
42
+ /**
43
+ * Undo coalesce key for ONE z-reorder gesture — unique per call. The key
44
+ * carries the action kind + element ids for debuggability, plus a gesture
45
+ * sequence so two SEPARATE user actions (even the same action on the same
46
+ * selection) never share a key. That uniqueness is what makes the unbounded
47
+ * per-gesture coalesce window (see handleDomZIndexReorderCommit) safe: the
48
+ * fold can only ever merge records of the SAME gesture.
49
+ *
50
+ * Exported as THE single implementation of the key: the canvas z-order wiring
51
+ * (PreviewOverlays) mints it once per gesture and passes the same instance to
52
+ * both the z persist and the timeline lane mirror (useCanvasZOrderTimelineMirror)
53
+ * so editHistory folds the z write and the track write into one undo entry —
54
+ * recomputing the key per record would silently split the undo.
55
+ */
56
+ export function zReorderCoalesceKey(
57
+ entries: ReadonlyArray<{ element: HTMLElement; id?: string; selector?: string }>,
58
+ actionKind?: string,
59
+ ): string {
60
+ const ids = entries
61
+ .map((e) => e.id ?? e.selector ?? e.element.getAttribute("data-hf-id") ?? "el")
62
+ .join(":");
63
+ return `z-reorder:${actionKind ?? "reorder"}:${ids}:g${zReorderGestureSeq++}`;
64
+ }
65
+
28
66
  export function useElementLifecycleOps({
29
67
  activeCompPath,
30
68
  showToast,
@@ -130,11 +168,11 @@ export function useElementLifecycleOps({
130
168
  ],
131
169
  );
132
170
 
133
- // ponytail: z-index reorder folds every element patch for a source file in memory and sends
134
- // one patch-elements-batch request, so each file is persisted with one atomic disk write.
171
+ // Z-index reorder folds patches by source file, then sends one aggregate cross-file
172
+ // patch-element-batches request. The server refuses the whole gesture on any unmatched
173
+ // target and rolls back earlier file writes if a later write fails.
135
174
  // No SDK reorder/reparent op exists; DOM sibling order stays server-authoritative if ever needed.
136
175
  const handleDomZIndexReorderCommit = useCallback(
137
- // fallow-ignore-next-line complexity
138
176
  (
139
177
  entries: Array<{
140
178
  element: HTMLElement;
@@ -149,88 +187,143 @@ export function useElementLifecycleOps({
149
187
  actionKind?: string,
150
188
  ) => {
151
189
  if (entries.length === 0) return Promise.resolve();
152
- // Resolver shadow (telemetry-only, decoupled from cutover): record whether
153
- // the SDK resolves each reordered element — the reorderElements op's targets.
154
- onReorderShadow?.(
155
- entries.map((e) => readHfId(e.element)).filter((id): id is string => id != null),
156
- );
157
- // The default key carries the action kind so two DIFFERENT actions on the
158
- // same element set (e.g. "bring-forward" then "send-backward" within the
159
- // coalesce window) never merge into one undo step. Callers that share a
160
- // gesture (lane moves) pass an explicit gestureCoalesceKey instead.
161
- const coalesceKey =
162
- gestureCoalesceKey ??
163
- `z-reorder:${actionKind ?? "reorder"}:${entries.map((e) => e.id ?? e.selector ?? e.element.getAttribute("data-hf-id") ?? "el").join(":")}`;
164
- const patchesBySourceFile = new Map<string, DomEditPatchBatch["patches"]>();
165
- const rollbacks: Array<() => void> = [];
166
- for (const entry of entries) {
167
- const priorZIndex = entry.element.style.zIndex;
168
- const priorPosition = entry.element.style.position;
169
- const priorStoreEntry = entry.key
170
- ? usePlayerStore.getState().elements.find((el) => (el.key ?? el.id) === entry.key)
171
- : undefined;
172
- let positionChanged = false;
173
- entry.element.style.zIndex = String(entry.zIndex);
174
- const patches: Array<{ type: "inline-style"; property: string; value: string }> = [
175
- { type: "inline-style", property: "z-index", value: String(entry.zIndex) },
176
- ];
190
+ // One async owner must bracket reveal tokens, optimistic DOM/store state,
191
+ // atomic persistence, rollback, and the final persistence-count release.
192
+ // Splitting those phases would make transaction ownership less explicit.
193
+ // fallow-ignore-next-line complexity
194
+ return (async () => {
195
+ const releaseZPersists = entries.map((entry) => beginLayerZPersist(entry.element));
177
196
  try {
178
- const win = entry.element.ownerDocument?.defaultView;
179
- if (win && win.getComputedStyle(entry.element).position === "static") {
180
- entry.element.style.position = "relative";
181
- positionChanged = true;
182
- patches.push({ type: "inline-style", property: "position", value: "relative" });
197
+ // Resolver shadow (telemetry-only, decoupled from cutover): record whether
198
+ // the SDK resolves each reordered element the reorderElements op's targets.
199
+ onReorderShadow?.(
200
+ entries.map((e) => readHfId(e.element)).filter((id): id is string => id != null),
201
+ );
202
+ // The default key carries the action kind so two DIFFERENT actions on the
203
+ // same element set (e.g. "bring-forward" then "send-backward" within the
204
+ // coalesce window) never merge into one undo step. Callers that share a
205
+ // gesture (lane moves) pass an explicit gestureCoalesceKey instead.
206
+ const coalesceKey = gestureCoalesceKey ?? zReorderCoalesceKey(entries, actionKind);
207
+ const patchesBySourceFile = new Map<string, DomEditPatchBatch["patches"]>();
208
+ const rollbacks: Array<() => void> = [];
209
+ const revealCommits: Array<{
210
+ element: HTMLElement;
211
+ ownership: LayerRevealCommitOwnership;
212
+ }> = [];
213
+ for (const entry of entries) {
214
+ const priorZIndex = entry.element.style.zIndex;
215
+ const priorPosition = entry.element.style.position;
216
+ const priorStoreEntry = entry.key
217
+ ? usePlayerStore.getState().elements.find((el) => (el.key ?? el.id) === entry.key)
218
+ : undefined;
219
+ let positionChanged = false;
220
+ // An active Layers-panel reveal lift on this element is consumed by
221
+ // this commit: the new z is the truth. Read the parked TRUE position
222
+ // for the static check below (the lift set a temporary
223
+ // position:relative that would otherwise mask the need to persist
224
+ // one), then drop the lift attributes so z readers stop reporting the
225
+ // stale prior (see useLayerRevealOverride / readLayerRevealPriorZ).
226
+ const liftPriorPosition = entry.element.getAttribute(LAYER_REVEAL_PRIOR_POSITION_ATTR);
227
+ const revealOwnership = beginLayerRevealCommit(entry.element);
228
+ if (revealOwnership) {
229
+ revealCommits.push({ element: entry.element, ownership: revealOwnership });
230
+ }
231
+ entry.element.style.zIndex = String(entry.zIndex);
232
+ const patches: Array<{ type: "inline-style"; property: string; value: string }> = [
233
+ { type: "inline-style", property: "z-index", value: String(entry.zIndex) },
234
+ ];
235
+ try {
236
+ const win = entry.element.ownerDocument?.defaultView;
237
+ const effectivePosition =
238
+ liftPriorPosition ??
239
+ (win ? win.getComputedStyle(entry.element).position : undefined);
240
+ if (effectivePosition === "static") {
241
+ entry.element.style.position = "relative";
242
+ positionChanged = true;
243
+ patches.push({ type: "inline-style", property: "position", value: "relative" });
244
+ }
245
+ } catch {
246
+ /* cross-origin or detached — skip */
247
+ }
248
+ if (entry.key) {
249
+ usePlayerStore
250
+ .getState()
251
+ .updateElement(entry.key, { zIndex: entry.zIndex, hasExplicitZIndex: true });
252
+ }
253
+ rollbacks.push(() => {
254
+ if (revealOwnership) {
255
+ rollbackLayerRevealCommit(entry.element, revealOwnership);
256
+ } else {
257
+ entry.element.style.zIndex = priorZIndex;
258
+ if (positionChanged) entry.element.style.position = priorPosition;
259
+ }
260
+ if (entry.key && priorStoreEntry) {
261
+ usePlayerStore.getState().updateElement(entry.key, {
262
+ zIndex: priorStoreEntry.zIndex,
263
+ hasExplicitZIndex: priorStoreEntry.hasExplicitZIndex,
264
+ });
265
+ }
266
+ });
267
+ const filePatches = patchesBySourceFile.get(entry.sourceFile) ?? [];
268
+ filePatches.push({
269
+ target: buildDomEditPatchTarget({
270
+ id: entry.id,
271
+ hfId: readHfId(entry.element),
272
+ selector: entry.selector,
273
+ selectorIndex: entry.selectorIndex,
274
+ }),
275
+ operations: patches,
276
+ });
277
+ patchesBySourceFile.set(entry.sourceFile, filePatches);
183
278
  }
184
- } catch {
185
- /* cross-origin or detached — skip */
186
- }
187
- if (entry.key) {
188
- usePlayerStore
189
- .getState()
190
- .updateElement(entry.key, { zIndex: entry.zIndex, hasExplicitZIndex: true });
191
- }
192
- rollbacks.push(() => {
193
- entry.element.style.zIndex = priorZIndex;
194
- if (positionChanged) entry.element.style.position = priorPosition;
195
- if (entry.key && priorStoreEntry) {
196
- usePlayerStore.getState().updateElement(entry.key, {
197
- zIndex: priorStoreEntry.zIndex,
198
- hasExplicitZIndex: priorStoreEntry.hasExplicitZIndex,
279
+ const batches = [...patchesBySourceFile].map(([sourceFile, patches]) => ({
280
+ sourceFile,
281
+ patches,
282
+ }));
283
+ // Live z state changed with NO reload coming (skipReload below) — nudge
284
+ // DOM-derived views (Layers panel z-sort) to re-read the iframe.
285
+ usePlayerStore.getState().bumpZEditVersion();
286
+ const rollbackOptimisticState = () => {
287
+ for (const rollback of rollbacks) rollback();
288
+ usePlayerStore.getState().bumpZEditVersion();
289
+ };
290
+ // Resolves once every source-file batch is persisted so a same-file timing write
291
+ // can be ordered after it (see applyTimelineStackingReorder callers).
292
+ //
293
+ // skipReload: the live iframe DOM and the player store already hold the
294
+ // final z state (applied synchronously above), and the persisted patch is
295
+ // inline-style-only — a full iframe remount would only blink the preview.
296
+ // commitDomEditPatchBatches still falls back to reloading whenever the
297
+ // server reports an unmatched patch target (live DOM ≠ disk).
298
+ try {
299
+ const result = await commitDomEditPatchBatches(batches, {
300
+ label: "Reorder layers",
301
+ coalesceKey,
302
+ // Unbounded window: every key this commit records under is unique per
303
+ // gesture (zReorderCoalesceKey's gesture seq, or the lane drag's
304
+ // clip-lane-move:<seq>), so the fold can only merge records of the SAME
305
+ // gesture — and those records are separated by a server round-trip
306
+ // (move→z on a lane drag, z→lane-mirror on a canvas action), which
307
+ // under real network latency exceeds the 300ms default window.
308
+ coalesceMs: Number.POSITIVE_INFINITY,
309
+ skipReload: true,
199
310
  });
311
+ if (!result.durable) {
312
+ rollbackOptimisticState();
313
+ return result;
314
+ }
315
+ for (const { element, ownership } of revealCommits) {
316
+ completeLayerRevealCommit(element, ownership);
317
+ }
318
+ return result;
319
+ } catch (error) {
320
+ rollbackOptimisticState();
321
+ throw error;
200
322
  }
201
- });
202
- const filePatches = patchesBySourceFile.get(entry.sourceFile) ?? [];
203
- filePatches.push({
204
- target: buildDomEditPatchTarget({
205
- id: entry.id,
206
- hfId: readHfId(entry.element),
207
- selector: entry.selector,
208
- selectorIndex: entry.selectorIndex,
209
- }),
210
- operations: patches,
211
- });
212
- patchesBySourceFile.set(entry.sourceFile, filePatches);
213
- }
214
- const batches = [...patchesBySourceFile].map(([sourceFile, patches]) => ({
215
- sourceFile,
216
- patches,
217
- }));
218
- // Resolves once every source-file batch is persisted so a same-file timing write
219
- // can be ordered after it (see applyTimelineStackingReorder callers).
220
- //
221
- // skipReload: the live iframe DOM and the player store already hold the
222
- // final z state (applied synchronously above), and the persisted patch is
223
- // inline-style-only — a full iframe remount would only blink the preview.
224
- // commitDomEditPatchBatches still falls back to reloading whenever the
225
- // server reports an unmatched patch target (live DOM ≠ disk).
226
- return commitDomEditPatchBatches(batches, {
227
- label: "Reorder layers",
228
- coalesceKey,
229
- skipReload: true,
230
- }).catch((error) => {
231
- for (const rollback of rollbacks) rollback();
232
- throw error;
233
- });
323
+ } finally {
324
+ for (const release of releaseZPersists) release();
325
+ }
326
+ })();
234
327
  },
235
328
  [commitDomEditPatchBatches, onReorderShadow],
236
329
  );
@@ -10,7 +10,7 @@ import type { EditHistoryKind } from "../utils/editHistory";
10
10
  import { createDomEditSaveQueue } from "../utils/domEditSaveQueue";
11
11
  import { flushStudioPendingEdits } from "../utils/studioPendingEdits";
12
12
  import { trackStudioEvent } from "../utils/studioTelemetry";
13
- import { applyUndoRestoreToPreview, type UndoRestoreFile } from "../utils/gsapSoftReload";
13
+ import { applyUndoRestoreToPreview, type UndoRestoreFile } from "../utils/gsapUndoRestore";
14
14
  import { usePlayerStore } from "../player";
15
15
 
16
16
  /** The restore payload the undo/redo preview-sync consumes (from the history store). */
@@ -156,7 +156,7 @@ export function usePreviewPersistence({
156
156
 
157
157
  // ── Queue / drain helpers ──
158
158
 
159
- const queueDomEditSave = useCallback((save: () => Promise<void>) => {
159
+ const queueDomEditSave = useCallback(<T>(save: () => Promise<T>): Promise<T> => {
160
160
  return domEditSaveQueueRef.current?.enqueue(save) ?? save();
161
161
  }, []);
162
162