@hyperframes/studio 0.7.56 → 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 (146) hide show
  1. package/dist/assets/hyperframes-player-CtTDO63S.js +459 -0
  2. package/dist/assets/{index-CmVCjZjp.js → index-B_UvTX3E.js} +205 -205
  3. package/dist/assets/{index-BWnOxAiH.js → index-C47jAC3Q.js} +1 -1
  4. package/dist/assets/{index-C4csZims.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 +41 -4
  10. package/dist/index.html +2 -2
  11. package/dist/index.js +7436 -5423
  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 +166 -2
  17. package/src/components/editor/CanvasContextMenu.tsx +100 -20
  18. package/src/components/editor/DomEditOverlay.tsx +34 -62
  19. package/src/components/editor/LayersPanel.tsx +111 -91
  20. package/src/components/editor/canvasContextMenuZOrder.test.ts +243 -2
  21. package/src/components/editor/canvasContextMenuZOrder.ts +184 -23
  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/propertyPanelColorGradingSection.tsx +15 -21
  26. package/src/components/editor/useCanvasContextMenuState.ts +92 -0
  27. package/src/components/editor/useLayerRevealOverride.test.ts +169 -0
  28. package/src/components/editor/useLayerRevealOverride.ts +389 -0
  29. package/src/components/editor/useZOrderCrossedFlash.tsx +68 -0
  30. package/src/components/nle/AssetPreviewOverlay.test.tsx +106 -0
  31. package/src/components/nle/AssetPreviewOverlay.tsx +61 -37
  32. package/src/components/nle/PreviewOverlays.tsx +61 -16
  33. package/src/components/nle/TimelinePane.test.ts +3 -0
  34. package/src/components/nle/TimelinePane.tsx +11 -1
  35. package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +440 -0
  36. package/src/components/nle/useCanvasZOrderTimelineMirror.ts +191 -0
  37. package/src/components/nle/useTimelineEditCallbacks.ts +1 -0
  38. package/src/components/nle/zLaneGesture.test.ts +176 -0
  39. package/src/components/nle/zLaneGesture.ts +77 -0
  40. package/src/components/sidebar/AssetCard.test.tsx +106 -0
  41. package/src/components/sidebar/AssetCard.tsx +19 -2
  42. package/src/components/sidebar/AudioRow.tsx +19 -2
  43. package/src/hooks/domEditCommitTypes.ts +29 -2
  44. package/src/hooks/elementLifecycleOpsTestUtils.ts +25 -0
  45. package/src/hooks/fetchStubTestUtils.ts +16 -0
  46. package/src/hooks/timelineEditingHelpers.test.ts +183 -1
  47. package/src/hooks/timelineEditingHelpers.ts +63 -259
  48. package/src/hooks/timelineMoveAdapter.test.ts +44 -40
  49. package/src/hooks/timelineMoveAdapter.ts +7 -4
  50. package/src/hooks/timelineTimingSync.test.ts +820 -0
  51. package/src/hooks/timelineTimingSync.ts +597 -0
  52. package/src/hooks/timelineTrackVisibility.test.ts +3 -1
  53. package/src/hooks/timelineTrackVisibility.ts +4 -3
  54. package/src/hooks/useCaptionDetection.ts +2 -0
  55. package/src/hooks/useClipboard.ts +19 -17
  56. package/src/hooks/useCompositionDimensions.ts +32 -5
  57. package/src/hooks/useDomEditCommits.test.tsx +392 -23
  58. package/src/hooks/useDomEditCommits.ts +180 -53
  59. package/src/hooks/useDomEditSession.test.tsx +3 -1
  60. package/src/hooks/useDomEditSession.ts +1 -1
  61. package/src/hooks/useElementLifecycleOps.test.tsx +559 -28
  62. package/src/hooks/useElementLifecycleOps.ts +175 -69
  63. package/src/hooks/useElementPicker.ts +7 -8
  64. package/src/hooks/usePreviewPersistence.ts +2 -2
  65. package/src/hooks/useTimelineAssetDropOps.ts +175 -0
  66. package/src/hooks/useTimelineEditing.test.tsx +591 -277
  67. package/src/hooks/useTimelineEditing.ts +202 -282
  68. package/src/hooks/useTimelineEditingTypes.ts +1 -1
  69. package/src/hooks/useTimelineGroupEditing.ts +187 -134
  70. package/src/player/components/PlayerControls.tsx +5 -5
  71. package/src/player/components/PlayheadIndicator.tsx +7 -2
  72. package/src/player/components/Timeline.test.ts +105 -18
  73. package/src/player/components/Timeline.tsx +69 -98
  74. package/src/player/components/TimelineCanvas.tsx +47 -7
  75. package/src/player/components/TimelineLanes.tsx +32 -10
  76. package/src/player/components/TimelineOverlays.tsx +36 -0
  77. package/src/player/components/TimelineRuler.tsx +70 -47
  78. package/src/player/components/TrackGapContextMenu.tsx +116 -0
  79. package/src/player/components/timelineCallbacks.ts +4 -1
  80. package/src/player/components/timelineClipDragCommit.test.ts +370 -11
  81. package/src/player/components/timelineClipDragCommit.ts +267 -68
  82. package/src/player/components/timelineGapCommit.test.ts +163 -0
  83. package/src/player/components/timelineGapCommit.ts +103 -0
  84. package/src/player/components/timelineGaps.test.ts +221 -0
  85. package/src/player/components/timelineGaps.ts +182 -0
  86. package/src/player/components/timelineLayout.test.ts +2 -1
  87. package/src/player/components/timelineLayout.ts +119 -30
  88. package/src/player/components/timelineMarquee.test.ts +20 -10
  89. package/src/player/components/timelineMarquee.ts +9 -2
  90. package/src/player/components/timelineRevealScroll.test.ts +133 -0
  91. package/src/player/components/timelineRevealScroll.ts +91 -0
  92. package/src/player/components/timelineStackingSync.test.ts +102 -1
  93. package/src/player/components/timelineStackingSync.ts +45 -5
  94. package/src/player/components/timelineTrackPersistPipeline.test.ts +168 -0
  95. package/src/player/components/timelineZMirror.test.ts +667 -0
  96. package/src/player/components/timelineZMirror.ts +393 -0
  97. package/src/player/components/timelineZones.test.ts +122 -310
  98. package/src/player/components/timelineZones.ts +82 -135
  99. package/src/player/components/timelineZoom.test.ts +2 -2
  100. package/src/player/components/timelineZoom.ts +7 -3
  101. package/src/player/components/useTimelineClipDrag.ts +1 -1
  102. package/src/player/components/useTimelineGapHighlights.test.ts +119 -0
  103. package/src/player/components/useTimelineGapHighlights.ts +110 -0
  104. package/src/player/components/useTimelinePlayhead.ts +4 -3
  105. package/src/player/components/useTimelineRangeSelection.ts +6 -4
  106. package/src/player/components/useTimelineRevealClip.ts +56 -0
  107. package/src/player/components/useTimelineScrollViewport.ts +73 -0
  108. package/src/player/components/useTimelineStackingSync.test.tsx +11 -2
  109. package/src/player/components/useTimelineStackingSync.ts +17 -4
  110. package/src/player/components/useTimelineTrackDerivations.ts +38 -0
  111. package/src/player/components/useTrackGapMenu.ts +155 -0
  112. package/src/player/hooks/useExpandedTimelineElements.test.ts +67 -4
  113. package/src/player/hooks/useExpandedTimelineElements.ts +19 -1
  114. package/src/player/hooks/useTimelinePlayer.seek.test.ts +55 -0
  115. package/src/player/hooks/useTimelinePlayer.test.ts +36 -3
  116. package/src/player/hooks/useTimelinePlayer.ts +4 -0
  117. package/src/player/hooks/useTimelineSyncCallbacks.ts +11 -1
  118. package/src/player/index.ts +3 -1
  119. package/src/player/lib/layerOrdering.test.ts +1 -15
  120. package/src/player/lib/layerOrdering.ts +5 -28
  121. package/src/player/lib/runtimeProtocol.test.ts +48 -0
  122. package/src/player/lib/runtimeProtocol.ts +65 -0
  123. package/src/player/lib/timelineDOM.test.ts +72 -0
  124. package/src/player/lib/timelineDOM.ts +12 -0
  125. package/src/player/lib/timelineElementHelpers.ts +54 -11
  126. package/src/player/lib/timelineIframeHelpers.ts +2 -4
  127. package/src/player/store/playerStore.test.ts +38 -13
  128. package/src/player/store/playerStore.ts +49 -1
  129. package/src/telemetry/events.test.ts +10 -0
  130. package/src/telemetry/events.ts +1 -0
  131. package/src/utils/assetPreviewDismiss.test.ts +27 -0
  132. package/src/utils/assetPreviewDismiss.ts +29 -0
  133. package/src/utils/domEditSaveQueue.ts +4 -3
  134. package/src/utils/editHistory.test.ts +82 -0
  135. package/src/utils/editHistory.ts +14 -2
  136. package/src/utils/gsapSoftReload.test.ts +54 -115
  137. package/src/utils/gsapSoftReload.ts +42 -152
  138. package/src/utils/gsapUndoRestore.test.ts +269 -0
  139. package/src/utils/gsapUndoRestore.ts +269 -0
  140. package/src/utils/sourceScopedSelectorIndex.ts +29 -0
  141. package/src/utils/timelineAssetDrop.test.ts +144 -52
  142. package/src/utils/timelineAssetDrop.ts +62 -86
  143. package/dist/assets/hyperframes-player-BBrKzTGd.js +0 -459
  144. package/dist/assets/index-D-GyYi2d.css +0 -1
  145. package/dist/chunk-5QSIMBEJ.js.map +0 -1
  146. /package/dist/{domEditingLayers-6LQGKPOI.js.map → domEditingLayers-2CKWGEHS.js.map} +0 -0
@@ -43,6 +43,15 @@ export interface StackingElement {
43
43
  zIndex: number;
44
44
  /** Audio clips have no visual stacking and are excluded from the computation. */
45
45
  isAudio: boolean;
46
+ /** Source document. Leaf z-indexes are comparable only inside this file. */
47
+ sourceFile?: string;
48
+ /**
49
+ * CSS stacking context the clip's node lives in (TimelineElement.stackingContextId).
50
+ * Leaf z-indexes are only comparable WITHIN one context — across contexts the
51
+ * ancestors' z decides paint order — so the sync partitions by this key and
52
+ * never patches across contexts. Null/undefined ⇒ the root context.
53
+ */
54
+ stackingContextId?: string | null;
46
55
  /**
47
56
  * Discovery / DOM document position (optional). Two clips with EQUAL z paint by
48
57
  * DOM order — the one LATER in the DOM paints ON TOP. When supplied, "is A above
@@ -62,6 +71,22 @@ export interface StackingPatch {
62
71
 
63
72
  const EPS = 1e-6;
64
73
 
74
+ /**
75
+ * Canonical paint-scope key: leaf z-indexes are comparable only within the same
76
+ * source document and CSS stacking context. The ONLY place this normalization
77
+ * lives — partitioning, membership checks, and pairwise equality all use it.
78
+ */
79
+ const paintScopeKey = (el: { sourceFile?: string; stackingContextId?: string | null }): string =>
80
+ JSON.stringify([el.sourceFile ?? null, el.stackingContextId ?? null]);
81
+
82
+ /** Canonical paint-scope equality for stacking sync and its inverse mirror. */
83
+ export function samePaintScope(
84
+ a: { sourceFile?: string; stackingContextId?: string | null },
85
+ b: { sourceFile?: string; stackingContextId?: string | null },
86
+ ): boolean {
87
+ return paintScopeKey(a) === paintScopeKey(b);
88
+ }
89
+
65
90
  /**
66
91
  * Two clips overlap in time when their half-open [start, end) intervals intersect.
67
92
  *
@@ -71,7 +96,10 @@ const EPS = 1e-6;
71
96
  * epsilon guards against float fuzz (e.g. 5.0000001 vs 5) spuriously overlapping two
72
97
  * abutting clips and shuffling lanes. The two are intended to differ, not align.
73
98
  */
74
- function overlapsInTime(a: StackingElement, b: StackingElement): boolean {
99
+ function overlapsInTime(
100
+ a: Pick<StackingElement, "start" | "duration">,
101
+ b: Pick<StackingElement, "start" | "duration">,
102
+ ): boolean {
75
103
  return a.start < b.start + b.duration - EPS && b.start < a.start + a.duration - EPS;
76
104
  }
77
105
 
@@ -99,9 +127,13 @@ interface MutZ extends StackingElement {
99
127
  * Does `a` currently paint ON TOP of `b`? Higher z wins; equal z breaks by DOM
100
128
  * order (later in DOM paints on top). When either domIndex is absent, equal z is
101
129
  * treated as "not strictly above" (ambiguous) — callers should supply domIndex to
102
- * disambiguate (see StackingElement.domIndex). Operates on resolved (`MutZ`) clips.
130
+ * disambiguate (see StackingElement.domIndex). Exported (like laneIsAbove) as the
131
+ * ONE paint-order predicate so every consumer agrees on what "paints above" means.
103
132
  */
104
- function paintsAbove(a: MutZ, b: MutZ): boolean {
133
+ function paintsAbove(
134
+ a: Pick<StackingElement, "zIndex" | "domIndex">,
135
+ b: Pick<StackingElement, "zIndex" | "domIndex">,
136
+ ): boolean {
105
137
  if (a.zIndex !== b.zIndex) return a.zIndex > b.zIndex;
106
138
  if (a.domIndex != null && b.domIndex != null) return a.domIndex > b.domIndex;
107
139
  return false;
@@ -284,7 +316,13 @@ export function computeStackingPatches(
284
316
  // z=0 would enter the boundary math as a phantom neighbour at the z-floor. An
285
317
  // unresolved clip is neither a neighbour nor resolvable as an edit, so it is
286
318
  // excluded outright (item 13).
287
- const resolved = elements.filter((e) => Number.isFinite(e.zIndex));
319
+ const allResolved = elements.filter((e) => Number.isFinite(e.zIndex));
320
+
321
+ // Leaf z is only meaningful within ONE source document and stacking context:
322
+ // across either boundary the ancestor composition/context decides paint order.
323
+ // Restrict the computation to the edited clips' own paint scope(s).
324
+ const editedScopes = new Set(allResolved.filter((e) => editedSet.has(e.key)).map(paintScopeKey));
325
+ const resolved = allResolved.filter((e) => editedScopes.has(paintScopeKey(e)));
288
326
 
289
327
  // Mutable z snapshot so edits + cascaded bumps see each other's applied z.
290
328
  const byKey = new Map<string, MutZ>(resolved.map((e) => [e.key, { ...e }]));
@@ -305,7 +343,9 @@ export function computeStackingPatches(
305
343
  // LIFTED neighbour without overlapping the edited clip itself (#2198).
306
344
  const all = [...byKey.values()];
307
345
  const overlappersOf = (clip: MutZ): MutZ[] =>
308
- all.filter((o) => o.key !== clip.key && !o.isAudio && overlapsInTime(clip, o));
346
+ all.filter(
347
+ (o) => o.key !== clip.key && !o.isAudio && samePaintScope(clip, o) && overlapsInTime(clip, o),
348
+ );
309
349
 
310
350
  for (const clip of edited) {
311
351
  resolveEditedZ(clip, overlappersOf(clip), overlappersOf, patchZ);
@@ -0,0 +1,168 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type { TimelineElement } from "../store/playerStore";
3
+ import type { ClipManifestClip } from "../lib/playbackTypes";
4
+ import { createTimelineElementFromManifestClip } from "../lib/timelineDOM";
5
+ import { buildExpandedElements } from "../hooks/useExpandedTimelineElements";
6
+ import { normalizeToZones } from "./timelineZones";
7
+ import { commitDraggedClipMove, type TimelineMoveEdit } from "./timelineClipDragCommit";
8
+ import type { DraggedClipState } from "./useTimelineClipDrag";
9
+
10
+ /**
11
+ * Pipeline test across the REAL manifest→element boundary: no hand-injected
12
+ * `authoredTrack` / `stackingContextId`. A runtime-manifest-shaped payload with
13
+ * SPARSE authored tracks flows through createTimelineElementFromManifestClip →
14
+ * (normalizeToZones | buildChildElements) → commitDraggedClipMove, and the
15
+ * persisted data-track-index must be the AUTHORED target, never the display lane.
16
+ */
17
+
18
+ const manifestClip = (over: Partial<ClipManifestClip>): ClipManifestClip => ({
19
+ id: "x",
20
+ label: "x",
21
+ start: 0,
22
+ duration: 2,
23
+ track: 0,
24
+ stackingContextId: "root",
25
+ kind: "element",
26
+ tagName: "div",
27
+ compositionId: null,
28
+ parentCompositionId: null,
29
+ compositionSrc: null,
30
+ assetUrl: null,
31
+ ...over,
32
+ });
33
+
34
+ function fromManifest(clips: ClipManifestClip[]): TimelineElement[] {
35
+ return clips.map((clip, index) =>
36
+ createTimelineElementFromManifestClip({ clip, fallbackIndex: index }),
37
+ );
38
+ }
39
+
40
+ function drag(
41
+ element: TimelineElement,
42
+ opts: { previewStart: number; previewTrack: number },
43
+ ): DraggedClipState {
44
+ return {
45
+ element,
46
+ originClientX: 0,
47
+ originClientY: 0,
48
+ originScrollLeft: 0,
49
+ originScrollTop: 0,
50
+ pointerClientX: 0,
51
+ pointerClientY: 0,
52
+ pointerOffsetX: 0,
53
+ pointerOffsetY: 0,
54
+ previewStart: opts.previewStart,
55
+ previewTrack: opts.previewTrack,
56
+ desiredTrack: opts.previewTrack,
57
+ insertRow: null,
58
+ snapTime: null,
59
+ snapType: null,
60
+ started: true,
61
+ };
62
+ }
63
+
64
+ /** Commit a lane-change drag and return the single persisted edit batch. */
65
+ function commitLaneChange(
66
+ element: TimelineElement,
67
+ previewTrack: number,
68
+ elements: TimelineElement[],
69
+ trackOrder: number[],
70
+ ): TimelineMoveEdit[] {
71
+ const onMoveElements = vi.fn();
72
+ commitDraggedClipMove(drag(element, { previewStart: element.start, previewTrack }), {
73
+ elements,
74
+ trackOrder,
75
+ updateElement: vi.fn(),
76
+ onMoveElements,
77
+ });
78
+ expect(onMoveElements).toHaveBeenCalledTimes(1);
79
+ return onMoveElements.mock.calls[0][0] as TimelineMoveEdit[];
80
+ }
81
+
82
+ describe("track persist pipeline (manifest → factory → lanes → drag commit)", () => {
83
+ // Sparse authored tracks 3 and 7 (mixed kinds), plus audio on 5, exactly as a
84
+ // runtime manifest would ship them (clip.track is the verbatim data-track-index).
85
+ const sparseManifest = [
86
+ manifestClip({ id: "v", kind: "video", tagName: "video", track: 3, start: 0 }),
87
+ manifestClip({ id: "g", kind: "element", tagName: "div", track: 7, start: 10 }),
88
+ manifestClip({ id: "m", kind: "audio", tagName: "audio", track: 5, start: 0 }),
89
+ ];
90
+
91
+ it("factory records the authored track and stacking context from the manifest clip", () => {
92
+ const [v, g, m] = fromManifest(sparseManifest);
93
+ expect([v.authoredTrack, g.authoredTrack, m.authoredTrack]).toEqual([3, 7, 5]);
94
+ expect(v.stackingContextId).toBe("root");
95
+ });
96
+
97
+ it("a lane change on a sparse file persists the AUTHORED target track, not the display lane", () => {
98
+ // normalizeToZones packs visual tracks {3, 7} onto display lanes {0, 1} and
99
+ // the audio track 5 onto lane 2, preserving the factory-set authoredTrack.
100
+ const elements = normalizeToZones(fromManifest(sparseManifest));
101
+ const byId = new Map(elements.map((e) => [e.id, e]));
102
+ expect(byId.get("v")).toMatchObject({ track: 0, authoredTrack: 3 });
103
+ expect(byId.get("g")).toMatchObject({ track: 1, authoredTrack: 7 });
104
+ expect(byId.get("m")).toMatchObject({ track: 2, authoredTrack: 5 });
105
+
106
+ // Drag the video (lane 0) onto the div's lane (display 1, authored 7).
107
+ const down = commitLaneChange(byId.get("v")!, 1, elements, [0, 1, 2]);
108
+ expect(down).toHaveLength(1);
109
+ expect(down[0].updates.track).toBe(7); // authored, NOT display lane 1
110
+ expect(down[0].updates.track).not.toBe(1);
111
+
112
+ // And the reverse: the div (lane 1) onto the video's lane (display 0, authored 3).
113
+ const up = commitLaneChange(byId.get("g")!, 0, elements, [0, 1, 2]);
114
+ expect(up[0].updates.track).toBe(3); // authored, NOT display lane 0
115
+ });
116
+
117
+ it("an expanded sub-comp child's lane change persists the sibling's authored track from ITS file", () => {
118
+ // Host timeline: the sub-comp host plus a root clip, discovered through the
119
+ // factory and lane-normalized like the store does.
120
+ const hostManifest = [
121
+ manifestClip({
122
+ id: "scene",
123
+ kind: "composition",
124
+ tagName: "div",
125
+ track: 0,
126
+ start: 0,
127
+ duration: 10,
128
+ compositionId: "scene",
129
+ compositionSrc: "scene.html",
130
+ }),
131
+ manifestClip({ id: "root-clip", kind: "video", tagName: "video", track: 1, start: 0 }),
132
+ ];
133
+ // scene.html has SPARSE authored tracks 3 and 7.
134
+ const childClips = [
135
+ manifestClip({ id: "c3", track: 3, start: 1, duration: 2, parentCompositionId: "scene" }),
136
+ manifestClip({ id: "c7", track: 7, start: 4, duration: 2, parentCompositionId: "scene" }),
137
+ ];
138
+ const storeElements = normalizeToZones(fromManifest(hostManifest));
139
+ const parentMap = new Map([
140
+ ["c3", "scene"],
141
+ ["c7", "scene"],
142
+ ]);
143
+ const expanded = buildExpandedElements(
144
+ storeElements,
145
+ [...hostManifest, ...childClips],
146
+ parentMap,
147
+ "scene",
148
+ "scene",
149
+ );
150
+
151
+ // The children replaced the host row: synthetic display lanes, but the
152
+ // authored track (in scene.html's coordinate space) survived the expansion.
153
+ const c3 = expanded.find((e) => e.domId === "c3")!;
154
+ const c7 = expanded.find((e) => e.domId === "c7")!;
155
+ expect(c3).toMatchObject({ authoredTrack: 3, sourceFile: "scene.html" });
156
+ expect(c7).toMatchObject({ authoredTrack: 7, sourceFile: "scene.html" });
157
+ expect(c3.stackingContextId).toBe("root");
158
+ expect(c3.track).not.toBe(3); // display row is synthetic
159
+
160
+ // Drag c3 onto c7's display lane: the persist target is c3's OWN file, so
161
+ // the written track must be c7's authored 7 — not the display-lane integer.
162
+ const trackOrder = [...new Set(expanded.map((e) => e.track))].sort((a, b) => a - b);
163
+ const edits = commitLaneChange(c3, c7.track, expanded, trackOrder);
164
+ expect(edits).toHaveLength(1);
165
+ expect(edits[0].updates.track).toBe(7);
166
+ expect(edits[0].updates.track).not.toBe(c7.track);
167
+ });
168
+ });