@hyperframes/studio 0.7.46 → 0.7.47

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 (110) hide show
  1. package/dist/assets/index-Dq7FEg0K.css +1 -0
  2. package/dist/assets/{index-BGVoOXdg.js → index-DsckwbdW.js} +1 -1
  3. package/dist/assets/index-VsAbY3rl.js +423 -0
  4. package/dist/assets/{index-DO2MAFSG.js → index-uvB30_yI.js} +1 -1
  5. package/dist/index.d.ts +96 -29
  6. package/dist/index.html +2 -2
  7. package/dist/index.js +8443 -4305
  8. package/dist/index.js.map +1 -1
  9. package/package.json +7 -7
  10. package/src/App.tsx +26 -32
  11. package/src/components/DesignPanelPromoteProvider.tsx +45 -0
  12. package/src/components/PanelTabButton.tsx +31 -0
  13. package/src/components/StudioPreviewArea.tsx +75 -3
  14. package/src/components/StudioRightPanel.tsx +111 -113
  15. package/src/components/editor/LayersPanel.tsx +2 -22
  16. package/src/components/editor/PromotableControl.tsx +105 -0
  17. package/src/components/editor/propertyPanelSections.tsx +42 -21
  18. package/src/components/panels/VariablesBindElement.tsx +266 -0
  19. package/src/components/panels/VariablesDeclarationForm.test.ts +135 -0
  20. package/src/components/panels/VariablesDeclarationForm.tsx +321 -0
  21. package/src/components/panels/VariablesOtherCompositions.tsx +159 -0
  22. package/src/components/panels/VariablesPanel.tsx +554 -0
  23. package/src/components/panels/VariablesRowAction.tsx +25 -0
  24. package/src/components/panels/VariablesValueControls.tsx +215 -0
  25. package/src/components/renders/useRenderQueue.ts +12 -0
  26. package/src/contexts/TimelineEditContext.tsx +7 -2
  27. package/src/contexts/VariablePromoteContext.tsx +138 -0
  28. package/src/contexts/variablePromoteHelpers.test.ts +124 -0
  29. package/src/contexts/variablePromoteHelpers.ts +53 -0
  30. package/src/contexts/variablePromoteIntegration.test.ts +80 -0
  31. package/src/hooks/previewVariablesStore.ts +34 -0
  32. package/src/hooks/timelineEditingHelpers.test.ts +104 -0
  33. package/src/hooks/timelineEditingHelpers.ts +386 -10
  34. package/src/hooks/useDomEditWiring.ts +9 -7
  35. package/src/hooks/useDomSelection.test.ts +36 -2
  36. package/src/hooks/useDomSelection.ts +56 -75
  37. package/src/hooks/useElementLifecycleOps.ts +57 -17
  38. package/src/hooks/usePreviewDocumentVersion.ts +27 -0
  39. package/src/hooks/useProjectCompositionVariables.ts +131 -0
  40. package/src/hooks/useStudioContextValue.ts +6 -1
  41. package/src/hooks/useStudioSdkSessions.ts +37 -0
  42. package/src/hooks/useTimelineEditing.test.tsx +963 -0
  43. package/src/hooks/useTimelineEditing.ts +122 -100
  44. package/src/hooks/useTimelineEditingTypes.ts +15 -0
  45. package/src/hooks/useTimelineGroupEditing.ts +370 -0
  46. package/src/hooks/useTimelineSelectionPreviewSync.test.tsx +129 -0
  47. package/src/hooks/useTimelineSelectionPreviewSync.ts +130 -0
  48. package/src/hooks/useVariablesPersist.ts +61 -0
  49. package/src/player/components/BeatStrip.tsx +19 -3
  50. package/src/player/components/Player.tsx +7 -1
  51. package/src/player/components/Timeline.test.ts +38 -0
  52. package/src/player/components/Timeline.tsx +102 -80
  53. package/src/player/components/TimelineCanvas.tsx +340 -296
  54. package/src/player/components/TimelineClip.test.tsx +11 -0
  55. package/src/player/components/TimelineClipDiamonds.tsx +40 -9
  56. package/src/player/components/TimelineDragGhost.tsx +59 -0
  57. package/src/player/components/TimelineDropInsertionLine.tsx +32 -0
  58. package/src/player/components/TimelineLayerGroupHeader.tsx +97 -0
  59. package/src/player/components/TimelineLayerGutter.tsx +61 -0
  60. package/src/player/components/TimelineRuler.tsx +4 -2
  61. package/src/player/components/TimelineSelectionOverlays.tsx +55 -0
  62. package/src/player/components/timelineCallbacks.ts +20 -2
  63. package/src/player/components/timelineClipDragPreview.ts +52 -0
  64. package/src/player/components/timelineDragDrop.ts +19 -3
  65. package/src/player/components/timelineDropIndicator.test.ts +48 -0
  66. package/src/player/components/timelineDropIndicator.ts +33 -0
  67. package/src/player/components/timelineEditing.test.ts +251 -1
  68. package/src/player/components/timelineEditing.ts +178 -19
  69. package/src/player/components/timelineGroupEditing.ts +159 -0
  70. package/src/player/components/timelineLayerDrag.test.ts +200 -0
  71. package/src/player/components/timelineLayerDrag.ts +383 -0
  72. package/src/player/components/timelineLayout.test.ts +34 -0
  73. package/src/player/components/timelineLayout.ts +47 -0
  74. package/src/player/components/timelineMarqueeSelection.test.ts +87 -0
  75. package/src/player/components/timelineSnapTargets.test.ts +144 -0
  76. package/src/player/components/timelineSnapTargets.ts +164 -0
  77. package/src/player/components/timelineStacking.ts +62 -0
  78. package/src/player/components/timelineTrackOrder.test.ts +144 -0
  79. package/src/player/components/timelineTrackOrder.ts +173 -0
  80. package/src/player/components/useTimelineActiveClips.test.ts +20 -0
  81. package/src/player/components/useTimelineActiveClips.ts +15 -4
  82. package/src/player/components/useTimelineClipDrag.test.tsx +506 -0
  83. package/src/player/components/useTimelineClipDrag.ts +172 -126
  84. package/src/player/components/useTimelineClipGroupDrag.ts +417 -0
  85. package/src/player/components/useTimelineKeyframeHandlers.ts +73 -0
  86. package/src/player/components/useTimelineMarqueeSelection.test.tsx +231 -0
  87. package/src/player/components/useTimelineMarqueeSelection.ts +276 -0
  88. package/src/player/hooks/useExpandedTimelineElements.ts +3 -0
  89. package/src/player/hooks/useTimelinePlayer.test.ts +4 -1
  90. package/src/player/hooks/useTimelinePlayer.ts +7 -0
  91. package/src/player/lib/layerOrdering.test.ts +77 -0
  92. package/src/player/lib/layerOrdering.ts +98 -0
  93. package/src/player/lib/playbackTypes.ts +3 -0
  94. package/src/player/lib/timelineDOM.test.ts +103 -0
  95. package/src/player/lib/timelineDOM.ts +80 -0
  96. package/src/player/lib/timelineElementHelpers.ts +1 -1
  97. package/src/player/store/playerStore.test.ts +92 -0
  98. package/src/player/store/playerStore.ts +69 -9
  99. package/src/utils/blockInstaller.ts +2 -14
  100. package/src/utils/editHistory.test.ts +61 -0
  101. package/src/utils/editHistory.ts +7 -1
  102. package/src/utils/rootDuration.test.ts +34 -0
  103. package/src/utils/rootDuration.ts +17 -0
  104. package/src/utils/sdkCutover.ts +45 -0
  105. package/src/utils/studioHelpers.test.ts +31 -0
  106. package/src/utils/studioHelpers.ts +29 -1
  107. package/src/utils/studioUrlState.test.ts +29 -0
  108. package/src/utils/studioUrlState.ts +16 -1
  109. package/dist/assets/index-BsLQ2qGn.js +0 -420
  110. package/dist/assets/index-DoVLXke0.css +0 -1
@@ -1,5 +1,3 @@
1
- // Pre-existing-complex timeline hook (DOM patch + GSAP position shift/scale +
2
- // playback-start resolution).
3
1
  // fallow-ignore-file complexity
4
2
  import { useCallback, useRef } from "react";
5
3
  import type { TimelineElement } from "../player";
@@ -21,25 +19,32 @@ import {
21
19
  resolveDroppedAssetDuration,
22
20
  } from "../utils/studioHelpers";
23
21
  import {
22
+ applyTimelineStackingReorder,
24
23
  buildPatchTarget,
25
24
  patchIframeDomTiming,
26
- resolveResizePlaybackStart,
27
25
  persistTimelineEdit,
28
26
  readFileContent,
29
- applyPatchByTarget,
27
+ foldedShiftGsapMutation,
28
+ foldedScaleGsapMutation,
30
29
  formatTimelineAttributeNumber,
31
- shiftGsapPositions,
32
- scaleGsapPositions,
30
+ finishTimelineTimingFallback,
31
+ extendRootDurationIfNeeded,
32
+ buildTimelineMoveTimingPatch,
33
+ buildTimelineResizeTimingPatch,
33
34
  } from "./timelineEditingHelpers";
34
35
  import type { PersistTimelineEditInput } from "./timelineEditingHelpers";
36
+ import type { TimelineStackingReorderIntent } from "../player/components/timelineEditing";
35
37
  import {
36
38
  useTimelineElementVisibilityEditing,
37
39
  useTimelineTrackVisibilityEditing,
38
40
  } from "./timelineTrackVisibility";
41
+ import { useTimelineGroupEditing } from "./useTimelineGroupEditing";
39
42
  import { sdkTimingPersist } from "../utils/sdkCutover";
40
43
  import type { UseTimelineEditingOptions } from "./useTimelineEditingTypes";
41
44
 
42
- // ── Hook ──
45
+ type TimelineMoveUpdates = Pick<TimelineElement, "start" | "track"> & {
46
+ stackingReorder?: TimelineStackingReorderIntent | null;
47
+ };
43
48
 
44
49
  export function useTimelineEditing({
45
50
  projectId,
@@ -56,6 +61,7 @@ export function useTimelineEditing({
56
61
  isRecordingRef,
57
62
  sdkSession,
58
63
  forceReloadSdkSession,
64
+ handleDomZIndexReorderCommitRef,
59
65
  }: UseTimelineEditingOptions) {
60
66
  const projectIdRef = useRef(projectId);
61
67
  projectIdRef.current = projectId;
@@ -92,7 +98,6 @@ export function useTimelineEditing({
92
98
  }),
93
99
  )
94
100
  .then(() => {
95
- // Server wrote the file; resync the stale in-memory SDK doc.
96
101
  forceReloadSdkSession?.();
97
102
  });
98
103
  editQueueRef.current = queued.catch((error) => {
@@ -111,68 +116,99 @@ export function useTimelineEditing({
111
116
  forceReloadSdkSession,
112
117
  ],
113
118
  );
119
+ const groupEditing = useTimelineGroupEditing({
120
+ activeCompPath,
121
+ domEditSaveTimestampRef,
122
+ editQueueRef,
123
+ forceReloadSdkSession,
124
+ isRecordingRef,
125
+ pendingTimelineEditPathRef,
126
+ previewIframeRef,
127
+ projectIdRef,
128
+ recordEdit,
129
+ reloadPreview,
130
+ sdkSession,
131
+ showToast,
132
+ writeProjectFile,
133
+ });
114
134
 
115
- // fallow-ignore-next-line complexity
116
135
  const handleTimelineElementMove = useCallback(
117
136
  // fallow-ignore-next-line complexity
118
- (element: TimelineElement, updates: Pick<TimelineElement, "start" | "track">) => {
119
- patchIframeDomTiming(previewIframeRef.current, element, [
120
- ["data-start", formatTimelineAttributeNumber(updates.start)],
121
- ["data-track-index", String(updates.track)],
122
- ]);
137
+ (element: TimelineElement, updates: TimelineMoveUpdates) => {
123
138
  const targetPath = element.sourceFile || activeCompPath || "index.html";
139
+ const startChanged = updates.start !== element.start;
140
+
141
+ if (startChanged) {
142
+ patchIframeDomTiming(previewIframeRef.current, element, [
143
+ ["data-start", formatTimelineAttributeNumber(updates.start)],
144
+ ]);
145
+ }
146
+
147
+ const reorderDone = applyTimelineStackingReorder({
148
+ element,
149
+ stackingReorder: updates.stackingReorder,
150
+ timelineElements,
151
+ iframe: previewIframeRef.current,
152
+ activeCompPath,
153
+ commit: handleDomZIndexReorderCommitRef?.current,
154
+ });
155
+
156
+ if (!startChanged) return reorderDone;
157
+
124
158
  const buildMovePatches: PersistTimelineEditInput["buildPatches"] = (original, target) => {
125
- let patched = applyPatchByTarget(original, target, {
126
- type: "attribute",
127
- property: "start",
128
- value: formatTimelineAttributeNumber(updates.start),
129
- });
130
- return applyPatchByTarget(patched, target, {
131
- type: "attribute",
132
- property: "track-index",
133
- value: String(updates.track),
134
- });
159
+ return buildTimelineMoveTimingPatch(original, target, updates.start, element.duration);
135
160
  };
136
- // Server-path fallback (no SDK session): persist the attr patch, then
137
- // shift GSAP tween positions on the server and reload the preview — the
138
- // SDK path folds both into setTiming, but the fallback must do them
139
- // explicitly or the clip moves while its GSAP tweens stay put + the
140
- // preview never refreshes. coalesceKey mirrors the SDK branch so undo
141
- // granularity is identical on either path.
142
161
  const coalesceKey = `timeline-move:${element.hfId ?? element.id}`;
143
162
  const moveFallback = () =>
144
163
  enqueueEdit(element, "Move timeline clip", buildMovePatches, coalesceKey).then(() => {
145
164
  const pid = projectIdRef.current;
146
165
  const delta = updates.start - element.start;
147
- if (delta !== 0 && element.domId && pid) {
148
- return shiftGsapPositions(pid, targetPath, element.domId, delta)
149
- .then(() => reloadPreview())
150
- .catch((err) => console.error("[Timeline] Failed to shift GSAP positions", err));
151
- }
152
- return reloadPreview();
153
- });
154
- if (sdkSession && element.hfId) {
155
- return sdkTimingPersist(
156
- element.hfId,
157
- targetPath,
158
- { start: updates.start, trackIndex: updates.track },
159
- sdkSession,
160
- {
161
- editHistory: { recordEdit },
162
- writeProjectFile,
166
+ const domId = element.domId;
167
+ return finishTimelineTimingFallback({
168
+ iframe: previewIframeRef.current,
169
+ needsExtension,
170
+ rootDurationSeconds: updates.start + element.duration,
163
171
  reloadPreview,
164
- domEditSaveTimestampRef,
165
- compositionPath: activeCompPath,
166
- // Capture on-disk bytes as the undo `before` so undoing a timing move
167
- // restores the file verbatim, not a normalized full-DOM re-emit.
168
- readProjectFile: (path) => readFileContent(projectIdRef.current ?? "", path),
169
- },
170
- { label: "Move timeline clip", coalesceKey },
171
- ).then((handled) => {
172
- if (!handled) return moveFallback();
172
+ gsapMutation:
173
+ delta !== 0 && domId && pid
174
+ ? foldedShiftGsapMutation({
175
+ projectId: pid,
176
+ targetPath,
177
+ domId,
178
+ delta,
179
+ label: "Move timeline clip",
180
+ coalesceKey,
181
+ recordEdit,
182
+ })
183
+ : undefined,
184
+ onGsapError: (err) => console.error("[Timeline] Failed to shift GSAP positions", err),
185
+ });
173
186
  });
174
- }
175
- return moveFallback();
187
+ const needsExtension = extendRootDurationIfNeeded(updates.start + element.duration);
188
+ return reorderDone.then(() => {
189
+ if (sdkSession && element.hfId && !needsExtension) {
190
+ return sdkTimingPersist(
191
+ element.hfId,
192
+ targetPath,
193
+ { start: updates.start },
194
+ sdkSession,
195
+ {
196
+ editHistory: { recordEdit },
197
+ writeProjectFile,
198
+ reloadPreview,
199
+ domEditSaveTimestampRef,
200
+ compositionPath: activeCompPath,
201
+ // Capture on-disk bytes as the undo `before` so undoing a timing move
202
+ // restores the file verbatim, not a normalized full-DOM re-emit.
203
+ readProjectFile: (path) => readFileContent(projectIdRef.current ?? "", path),
204
+ },
205
+ { label: "Move timeline clip", coalesceKey },
206
+ ).then((handled) => {
207
+ if (!handled) return moveFallback();
208
+ });
209
+ }
210
+ return moveFallback();
211
+ });
176
212
  },
177
213
  [
178
214
  previewIframeRef,
@@ -183,10 +219,11 @@ export function useTimelineEditing({
183
219
  writeProjectFile,
184
220
  reloadPreview,
185
221
  domEditSaveTimestampRef,
222
+ timelineElements,
223
+ handleDomZIndexReorderCommitRef,
186
224
  ],
187
225
  );
188
226
 
189
- // fallow-ignore-next-line complexity
190
227
  const handleTimelineElementResize = useCallback(
191
228
  // fallow-ignore-next-line complexity
192
229
  (
@@ -210,60 +247,44 @@ export function useTimelineEditing({
210
247
  patchIframeDomTiming(previewIframeRef.current, element, liveAttrs);
211
248
  const targetPath = element.sourceFile || activeCompPath || "index.html";
212
249
  const buildResizePatches: PersistTimelineEditInput["buildPatches"] = (original, target) => {
213
- const pbs = resolveResizePlaybackStart(original, target, element, updates);
214
- let patched = applyPatchByTarget(original, target, {
215
- type: "attribute",
216
- property: "start",
217
- value: formatTimelineAttributeNumber(updates.start),
218
- });
219
- patched = applyPatchByTarget(patched, target, {
220
- type: "attribute",
221
- property: "duration",
222
- value: formatTimelineAttributeNumber(updates.duration),
223
- });
224
- if (pbs) {
225
- patched = applyPatchByTarget(patched, target, {
226
- type: "attribute",
227
- property: pbs.attrName,
228
- value: formatTimelineAttributeNumber(pbs.value),
229
- });
230
- }
231
- return patched;
250
+ return buildTimelineResizeTimingPatch(original, target, element, updates);
232
251
  };
233
- // SDK path: skip when a playback-start adjustment is needed (setTiming has no pbs field).
234
- // The second clause fires because trimming the start of a clip that has a
235
- // playback-start attribute implicitly shifts that in-point — which the SDK
236
- // setTiming op can't express — so those resizes must take the server path.
237
252
  const hasPbsAdjustment =
238
253
  updates.playbackStart != null ||
239
254
  (updates.start !== element.start && element.playbackStart != null);
240
255
  // Server-path fallback: after persisting the attr patch, scale GSAP tween
241
- // positions/durations on the server and reload the preview. The SDK path
242
- // folds both into setTiming; the fallback must do them explicitly or the
243
- // clip resizes while its GSAP tweens keep their old timing + the preview
244
- // never refreshes. coalesceKey mirrors the SDK branch for undo parity.
256
+ // positions/durations on the server. Extending edits can keep the iframe
257
+ // live unless a GSAP source rewrite needs a fresh run.
245
258
  const coalesceKey = `timeline-resize:${element.hfId ?? element.id}`;
246
259
  const timingChanged =
247
260
  updates.start !== element.start || updates.duration !== element.duration;
261
+ const needsExtension = extendRootDurationIfNeeded(updates.start + updates.duration);
248
262
  const resizeFallback = () =>
249
263
  enqueueEdit(element, "Resize timeline clip", buildResizePatches, coalesceKey).then(() => {
250
264
  const pid = projectIdRef.current;
251
- if (timingChanged && element.domId && pid) {
252
- return scaleGsapPositions(
253
- pid,
254
- targetPath,
255
- element.domId,
256
- element.start,
257
- element.duration,
258
- updates.start,
259
- updates.duration,
260
- )
261
- .then(() => reloadPreview())
262
- .catch((err) => console.error("[Timeline] Failed to scale GSAP positions", err));
263
- }
264
- return reloadPreview();
265
+ const domId = element.domId;
266
+ return finishTimelineTimingFallback({
267
+ iframe: previewIframeRef.current,
268
+ needsExtension,
269
+ rootDurationSeconds: updates.start + updates.duration,
270
+ reloadPreview,
271
+ gsapMutation:
272
+ timingChanged && domId && pid
273
+ ? foldedScaleGsapMutation({
274
+ projectId: pid,
275
+ targetPath,
276
+ domId,
277
+ from: { start: element.start, duration: element.duration },
278
+ to: { start: updates.start, duration: updates.duration },
279
+ label: "Resize timeline clip",
280
+ coalesceKey,
281
+ recordEdit,
282
+ })
283
+ : undefined,
284
+ onGsapError: (err) => console.error("[Timeline] Failed to scale GSAP positions", err),
285
+ });
265
286
  });
266
- if (sdkSession && element.hfId && !hasPbsAdjustment) {
287
+ if (sdkSession && element.hfId && !hasPbsAdjustment && !needsExtension) {
267
288
  return sdkTimingPersist(
268
289
  element.hfId,
269
290
  targetPath,
@@ -574,5 +595,6 @@ export function useTimelineEditing({
574
595
  handleTimelineAssetDrop,
575
596
  handleTimelineFileDrop,
576
597
  handleBlockedTimelineEdit,
598
+ ...groupEditing,
577
599
  };
578
600
  }
@@ -10,6 +10,20 @@ interface RecordEditInput {
10
10
  files: Record<string, { before: string; after: string }>;
11
11
  }
12
12
 
13
+ // Resolves once the z-index patches are persisted, so a caller that also writes
14
+ // the same file (e.g. a timing move) can order its write after this one.
15
+ export type TimelineZIndexReorderCommit = (
16
+ entries: Array<{
17
+ element: HTMLElement;
18
+ zIndex: number;
19
+ id?: string;
20
+ selector?: string;
21
+ selectorIndex?: number;
22
+ sourceFile: string;
23
+ key?: string;
24
+ }>,
25
+ ) => Promise<void>;
26
+
13
27
  export interface UseTimelineEditingOptions {
14
28
  projectId: string | null;
15
29
  activeCompPath: string | null;
@@ -27,4 +41,5 @@ export interface UseTimelineEditingOptions {
27
41
  sdkSession?: Composition | null;
28
42
  /** Resync the SDK session after a server-authoritative timeline write. */
29
43
  forceReloadSdkSession?: () => void;
44
+ handleDomZIndexReorderCommitRef?: MutableRefObject<TimelineZIndexReorderCommit | null>;
30
45
  }