@hyperframes/studio 0.6.73 → 0.6.75

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 (63) hide show
  1. package/dist/assets/index-DcyZuBcU.css +1 -0
  2. package/dist/assets/index-uB_W2GDl.js +140 -0
  3. package/dist/index.html +2 -2
  4. package/package.json +4 -4
  5. package/src/App.tsx +30 -24
  6. package/src/components/StudioPreviewArea.tsx +101 -26
  7. package/src/components/StudioRightPanel.tsx +3 -0
  8. package/src/components/StudioToast.tsx +18 -0
  9. package/src/components/TimelineToolbar.tsx +230 -4
  10. package/src/components/editor/AnimationCard.tsx +68 -4
  11. package/src/components/editor/DomEditOverlay.tsx +70 -1
  12. package/src/components/editor/GridOverlay.tsx +50 -0
  13. package/src/components/editor/KeyframeDiamond.tsx +49 -0
  14. package/src/components/editor/KeyframeNavigation.tsx +139 -0
  15. package/src/components/editor/LayersPanel.test.ts +135 -0
  16. package/src/components/editor/LayersPanel.tsx +151 -15
  17. package/src/components/editor/PropertyPanel.tsx +293 -140
  18. package/src/components/editor/SnapGuideOverlay.tsx +166 -0
  19. package/src/components/editor/SnapToolbar.tsx +163 -0
  20. package/src/components/editor/SpringEaseEditor.tsx +256 -0
  21. package/src/components/editor/domEditOverlayGestures.ts +7 -0
  22. package/src/components/editor/domEditOverlayStartGesture.ts +28 -0
  23. package/src/components/editor/gsapAnimationConstants.ts +42 -0
  24. package/src/components/editor/gsapAnimationHelpers.ts +2 -1
  25. package/src/components/editor/manualEditingAvailability.ts +6 -0
  26. package/src/components/editor/manualEditsDom.ts +56 -2
  27. package/src/components/editor/manualOffsetDrag.ts +19 -3
  28. package/src/components/editor/propertyPanelHelpers.ts +90 -0
  29. package/src/components/editor/propertyPanelTimingSection.tsx +64 -0
  30. package/src/components/editor/snapEngine.test.ts +657 -0
  31. package/src/components/editor/snapEngine.ts +575 -0
  32. package/src/components/editor/snapTargetCollection.ts +147 -0
  33. package/src/components/editor/useDomEditOverlayGestures.ts +137 -10
  34. package/src/components/editor/useLayerDrag.ts +213 -0
  35. package/src/components/nle/NLELayout.tsx +18 -0
  36. package/src/contexts/DomEditContext.tsx +27 -0
  37. package/src/hooks/gsapRuntimeBridge.ts +585 -0
  38. package/src/hooks/gsapRuntimeKeyframes.ts +170 -0
  39. package/src/hooks/useAnimatedPropertyCommit.ts +131 -0
  40. package/src/hooks/useAppHotkeys.ts +63 -1
  41. package/src/hooks/useDomEditCommits.ts +88 -4
  42. package/src/hooks/useDomEditSession.ts +179 -65
  43. package/src/hooks/useGsapScriptCommits.ts +144 -7
  44. package/src/hooks/useGsapSelectionHandlers.ts +202 -0
  45. package/src/hooks/useGsapTweenCache.ts +174 -3
  46. package/src/hooks/useTimelineEditing.ts +93 -0
  47. package/src/icons/SystemIcons.tsx +2 -0
  48. package/src/player/components/ClipContextMenu.tsx +99 -0
  49. package/src/player/components/KeyframeDiamondContextMenu.tsx +164 -0
  50. package/src/player/components/Timeline.test.ts +2 -1
  51. package/src/player/components/Timeline.tsx +108 -68
  52. package/src/player/components/TimelineCanvas.tsx +47 -1
  53. package/src/player/components/TimelineClip.tsx +8 -3
  54. package/src/player/components/TimelineClipDiamonds.tsx +174 -0
  55. package/src/player/components/timelineDragDrop.ts +103 -0
  56. package/src/player/components/timelineLayout.ts +1 -1
  57. package/src/player/store/playerStore.ts +42 -0
  58. package/src/utils/editHistory.ts +1 -1
  59. package/src/utils/optimisticUpdate.test.ts +53 -0
  60. package/src/utils/optimisticUpdate.ts +18 -0
  61. package/src/utils/studioUiPreferences.ts +17 -0
  62. package/dist/assets/index-CrxThtSJ.css +0 -1
  63. package/dist/assets/index-Dc2HfqON.js +0 -140
@@ -13,6 +13,7 @@ import {
13
13
  resolveTimelineSelectionSeekTime,
14
14
  } from "../../utils/studioHelpers";
15
15
  import { Layers } from "../../icons/SystemIcons";
16
+ import { useLayerDrag, isLayerDraggable, type LayerReorderEvent } from "./useLayerDrag";
16
17
 
17
18
  const TAG_ICONS: Record<string, string> = {
18
19
  video: "Vi",
@@ -51,6 +52,7 @@ interface CollapsedState {
51
52
  [key: string]: boolean;
52
53
  }
53
54
 
55
+ // fallow-ignore-next-line complexity
54
56
  export const LayersPanel = memo(function LayersPanel() {
55
57
  const {
56
58
  previewIframeRef,
@@ -59,12 +61,19 @@ export const LayersPanel = memo(function LayersPanel() {
59
61
  compositionLoading,
60
62
  timelineElements,
61
63
  currentTime,
64
+ showToast,
62
65
  } = useStudioContext();
63
- const { domEditSelection, applyDomSelection, updateDomEditHoverSelection } = useDomEditContext();
66
+ const {
67
+ domEditSelection,
68
+ applyDomSelection,
69
+ updateDomEditHoverSelection,
70
+ handleDomZIndexReorderCommit,
71
+ } = useDomEditContext();
64
72
 
65
73
  const [layers, setLayers] = useState<DomEditLayerItem[]>([]);
66
74
  const [collapsed, setCollapsed] = useState<CollapsedState>({});
67
75
  const prevDocVersionRef = useRef(0);
76
+ const scrollContainerRef = useRef<HTMLDivElement>(null);
68
77
 
69
78
  const isMasterView = !activeCompPath || activeCompPath === "index.html";
70
79
 
@@ -87,7 +96,7 @@ export const LayersPanel = memo(function LayersPanel() {
87
96
  activeCompositionPath: activeCompPath,
88
97
  isMasterView,
89
98
  });
90
- setLayers(items);
99
+ setLayers(sortLayersByZIndex(items));
91
100
  }, [previewIframeRef, activeCompPath, isMasterView]);
92
101
 
93
102
  useEffect(() => {
@@ -119,7 +128,6 @@ export const LayersPanel = memo(function LayersPanel() {
119
128
  isMasterView,
120
129
  preferClipAncestor: false,
121
130
  }),
122
- // LayersPanel has no projectId; probe is skipped when projectId is absent
123
131
  [activeCompPath, isMasterView],
124
132
  );
125
133
 
@@ -130,8 +138,6 @@ export const LayersPanel = memo(function LayersPanel() {
130
138
 
131
139
  let matchedId = findMatchingTimelineElementId(selection, timelineElements);
132
140
 
133
- // No direct match — walk up DOM ancestors to find the nearest element
134
- // that has a timeline entry (e.g. a child of scene1 seeks to scene1.start)
135
141
  if (!matchedId) {
136
142
  const sourceFile = selection.sourceFile ?? "index.html";
137
143
  let ancestor = layer.element.parentElement;
@@ -185,10 +191,52 @@ export const LayersPanel = memo(function LayersPanel() {
185
191
  setCollapsed((prev) => ({ ...prev, [key]: !prev[key] }));
186
192
  }, []);
187
193
 
188
- const selectedKey = domEditSelection ? getDomEditLayerKey(domEditSelection) : null;
194
+ const handleReorder = useCallback(
195
+ (event: LayerReorderEvent) => {
196
+ const { siblingLayers, fromIndex, toIndex } = event;
197
+ const reordered = [...siblingLayers];
198
+ const [moved] = reordered.splice(fromIndex, 1);
199
+ reordered.splice(toIndex, 0, moved);
200
+
201
+ const existingValues = siblingLayers.map((l) => getElementZIndex(l.element));
202
+ const sorted = [...existingValues].sort((a, b) => b - a);
203
+ const hasDupes = sorted.some((v, i) => i > 0 && v === sorted[i - 1]);
204
+ const zValues = hasDupes ? reordered.map((_, i) => reordered.length - i) : sorted;
205
+
206
+ const entries = reordered.map((layer, i) => ({
207
+ element: layer.element,
208
+ zIndex: zValues[i],
209
+ id: layer.id,
210
+ selector: layer.selector,
211
+ selectorIndex: layer.selectorIndex,
212
+ sourceFile: layer.sourceFile,
213
+ }));
214
+
215
+ handleDomZIndexReorderCommit(entries);
216
+ },
217
+ [handleDomZIndexReorderCommit],
218
+ );
189
219
 
220
+ const selectedKey = domEditSelection ? getDomEditLayerKey(domEditSelection) : null;
190
221
  const visibleLayers = getVisibleLayers(layers, collapsed);
191
222
 
223
+ const handleSingleSibling = useCallback(() => {
224
+ showToast("Only one layer at this level", "info");
225
+ }, [showToast]);
226
+
227
+ const {
228
+ dragKey,
229
+ insertionLineY,
230
+ handleRowPointerDown,
231
+ handleContainerPointerMove,
232
+ handleContainerPointerUp,
233
+ } = useLayerDrag({
234
+ visibleLayers,
235
+ scrollContainerRef,
236
+ onReorder: handleReorder,
237
+ onSingleSibling: handleSingleSibling,
238
+ });
239
+
192
240
  if (layers.length === 0) {
193
241
  return (
194
242
  <div className="flex h-full flex-col items-center justify-center bg-neutral-900 px-6 text-center">
@@ -207,9 +255,17 @@ export const LayersPanel = memo(function LayersPanel() {
207
255
  <div className="border-b border-white/10 px-3 py-2 text-[11px] text-neutral-500">
208
256
  {layers.length} layer{layers.length === 1 ? "" : "s"}
209
257
  </div>
210
- <div className="min-h-0 flex-1 overflow-y-auto py-1">
211
- {visibleLayers.map((layer) => {
258
+ <div
259
+ ref={scrollContainerRef}
260
+ className="relative min-h-0 flex-1 overflow-y-auto py-1"
261
+ onPointerMove={handleContainerPointerMove}
262
+ onPointerUp={handleContainerPointerUp}
263
+ onPointerCancel={handleContainerPointerUp}
264
+ >
265
+ {visibleLayers.map((layer, index) => {
212
266
  const selected = layer.key === selectedKey;
267
+ const isDragged = layer.key === dragKey;
268
+ const draggable = isLayerDraggable(layer);
213
269
  const isCollapsed = collapsed[layer.key] ?? false;
214
270
  const hasChildren = layer.childCount > 0;
215
271
  const isCompHost = isCompositionHost(layer.element);
@@ -217,21 +273,25 @@ export const LayersPanel = memo(function LayersPanel() {
217
273
  return (
218
274
  <div
219
275
  key={layer.key}
276
+ data-layer-index={index}
220
277
  role="button"
221
278
  tabIndex={0}
222
- onClick={() => handleSelectLayer(layer)}
223
- onPointerEnter={() => handleLayerHover(layer)}
279
+ onClick={() => !dragKey && handleSelectLayer(layer)}
280
+ onPointerDown={(e) => handleRowPointerDown(index, e)}
281
+ onPointerEnter={() => !dragKey && handleLayerHover(layer)}
224
282
  onKeyDown={(e) => {
225
283
  if (e.key === "Enter" || e.key === " ") {
226
284
  e.preventDefault();
227
285
  handleSelectLayer(layer);
228
286
  }
229
287
  }}
230
- className={`group flex w-full cursor-pointer items-center gap-1.5 px-2 py-1 text-left transition-colors ${
231
- selected
232
- ? "bg-studio-accent/14 text-studio-accent"
233
- : "text-neutral-300 hover:bg-white/[0.04] hover:text-neutral-100"
234
- }`}
288
+ className={`group flex w-full items-center gap-1.5 px-2 py-1 text-left transition-colors ${
289
+ isDragged
290
+ ? "opacity-40"
291
+ : selected
292
+ ? "bg-studio-accent/14 text-studio-accent"
293
+ : "text-neutral-300 hover:bg-white/[0.04] hover:text-neutral-100"
294
+ } ${dragKey ? "cursor-grabbing" : draggable ? "cursor-pointer" : "cursor-not-allowed opacity-50"}`}
235
295
  style={{ paddingLeft: 8 + layer.depth * 16 }}
236
296
  >
237
297
  {hasChildren ? (
@@ -271,11 +331,87 @@ export const LayersPanel = memo(function LayersPanel() {
271
331
  </div>
272
332
  );
273
333
  })}
334
+ {insertionLineY != null && (
335
+ <div
336
+ className="pointer-events-none absolute left-2 right-2 h-0.5 bg-studio-accent"
337
+ style={{ top: insertionLineY }}
338
+ />
339
+ )}
274
340
  </div>
275
341
  </div>
276
342
  );
277
343
  });
278
344
 
345
+ // ── Pure helpers ──────────────────────────────────────────────────────
346
+
347
+ // fallow-ignore-next-line complexity
348
+ function getElementZIndex(element: HTMLElement): number {
349
+ try {
350
+ const inline = element.style?.zIndex;
351
+ if (inline && inline !== "auto") {
352
+ const parsed = parseInt(inline, 10);
353
+ if (Number.isFinite(parsed)) return parsed;
354
+ }
355
+ const win = element.ownerDocument?.defaultView;
356
+ if (!win) return 0;
357
+ const value = win.getComputedStyle(element).zIndex;
358
+ if (value === "auto" || value === "") return 0;
359
+ const parsed = parseInt(value, 10);
360
+ return Number.isFinite(parsed) ? parsed : 0;
361
+ } catch {
362
+ return 0;
363
+ }
364
+ }
365
+
366
+ // fallow-ignore-next-line complexity
367
+ export function sortLayersByZIndex(layers: DomEditLayerItem[]): DomEditLayerItem[] {
368
+ if (layers.length <= 1) return layers;
369
+
370
+ const minDepth = layers[0].depth;
371
+ for (let i = 1; i < layers.length; i++) {
372
+ if (layers[i].depth < minDepth) return layers;
373
+ }
374
+
375
+ const chunks: Array<{ root: DomEditLayerItem; children: DomEditLayerItem[]; domIndex: number }> =
376
+ [];
377
+
378
+ for (let i = 0; i < layers.length; i++) {
379
+ if (layers[i].depth === minDepth) {
380
+ const children: DomEditLayerItem[] = [];
381
+ let j = i + 1;
382
+ while (j < layers.length && layers[j].depth > minDepth) {
383
+ children.push(layers[j]);
384
+ j++;
385
+ }
386
+ chunks.push({ root: layers[i], children, domIndex: chunks.length });
387
+ }
388
+ }
389
+
390
+ if (chunks.length <= 1) {
391
+ if (chunks.length === 1 && chunks[0].children.length > 0) {
392
+ const sorted = sortLayersByZIndex(chunks[0].children);
393
+ return [chunks[0].root, ...sorted];
394
+ }
395
+ return layers;
396
+ }
397
+
398
+ chunks.sort((a, b) => {
399
+ const zA = getElementZIndex(a.root.element);
400
+ const zB = getElementZIndex(b.root.element);
401
+ if (zA !== zB) return zB - zA;
402
+ return b.domIndex - a.domIndex;
403
+ });
404
+
405
+ const result: DomEditLayerItem[] = [];
406
+ for (const chunk of chunks) {
407
+ result.push(chunk.root);
408
+ if (chunk.children.length > 0) {
409
+ result.push(...sortLayersByZIndex(chunk.children));
410
+ }
411
+ }
412
+ return result;
413
+ }
414
+
279
415
  function getVisibleLayers(
280
416
  layers: DomEditLayerItem[],
281
417
  collapsed: CollapsedState,