@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.
- package/dist/assets/index-DcyZuBcU.css +1 -0
- package/dist/assets/index-uB_W2GDl.js +140 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +30 -24
- package/src/components/StudioPreviewArea.tsx +101 -26
- package/src/components/StudioRightPanel.tsx +3 -0
- package/src/components/StudioToast.tsx +18 -0
- package/src/components/TimelineToolbar.tsx +230 -4
- package/src/components/editor/AnimationCard.tsx +68 -4
- package/src/components/editor/DomEditOverlay.tsx +70 -1
- package/src/components/editor/GridOverlay.tsx +50 -0
- package/src/components/editor/KeyframeDiamond.tsx +49 -0
- package/src/components/editor/KeyframeNavigation.tsx +139 -0
- package/src/components/editor/LayersPanel.test.ts +135 -0
- package/src/components/editor/LayersPanel.tsx +151 -15
- package/src/components/editor/PropertyPanel.tsx +293 -140
- package/src/components/editor/SnapGuideOverlay.tsx +166 -0
- package/src/components/editor/SnapToolbar.tsx +163 -0
- package/src/components/editor/SpringEaseEditor.tsx +256 -0
- package/src/components/editor/domEditOverlayGestures.ts +7 -0
- package/src/components/editor/domEditOverlayStartGesture.ts +28 -0
- package/src/components/editor/gsapAnimationConstants.ts +42 -0
- package/src/components/editor/gsapAnimationHelpers.ts +2 -1
- package/src/components/editor/manualEditingAvailability.ts +6 -0
- package/src/components/editor/manualEditsDom.ts +56 -2
- package/src/components/editor/manualOffsetDrag.ts +19 -3
- package/src/components/editor/propertyPanelHelpers.ts +90 -0
- package/src/components/editor/propertyPanelTimingSection.tsx +64 -0
- package/src/components/editor/snapEngine.test.ts +657 -0
- package/src/components/editor/snapEngine.ts +575 -0
- package/src/components/editor/snapTargetCollection.ts +147 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +137 -10
- package/src/components/editor/useLayerDrag.ts +213 -0
- package/src/components/nle/NLELayout.tsx +18 -0
- package/src/contexts/DomEditContext.tsx +27 -0
- package/src/hooks/gsapRuntimeBridge.ts +585 -0
- package/src/hooks/gsapRuntimeKeyframes.ts +170 -0
- package/src/hooks/useAnimatedPropertyCommit.ts +131 -0
- package/src/hooks/useAppHotkeys.ts +63 -1
- package/src/hooks/useDomEditCommits.ts +88 -4
- package/src/hooks/useDomEditSession.ts +179 -65
- package/src/hooks/useGsapScriptCommits.ts +144 -7
- package/src/hooks/useGsapSelectionHandlers.ts +202 -0
- package/src/hooks/useGsapTweenCache.ts +174 -3
- package/src/hooks/useTimelineEditing.ts +93 -0
- package/src/icons/SystemIcons.tsx +2 -0
- package/src/player/components/ClipContextMenu.tsx +99 -0
- package/src/player/components/KeyframeDiamondContextMenu.tsx +164 -0
- package/src/player/components/Timeline.test.ts +2 -1
- package/src/player/components/Timeline.tsx +108 -68
- package/src/player/components/TimelineCanvas.tsx +47 -1
- package/src/player/components/TimelineClip.tsx +8 -3
- package/src/player/components/TimelineClipDiamonds.tsx +174 -0
- package/src/player/components/timelineDragDrop.ts +103 -0
- package/src/player/components/timelineLayout.ts +1 -1
- package/src/player/store/playerStore.ts +42 -0
- package/src/utils/editHistory.ts +1 -1
- package/src/utils/optimisticUpdate.test.ts +53 -0
- package/src/utils/optimisticUpdate.ts +18 -0
- package/src/utils/studioUiPreferences.ts +17 -0
- package/dist/assets/index-CrxThtSJ.css +0 -1
- 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 {
|
|
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
|
|
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
|
|
211
|
-
{
|
|
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
|
-
|
|
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
|
|
231
|
-
|
|
232
|
-
? "
|
|
233
|
-
:
|
|
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,
|