@hyperframes/studio 0.6.92 → 0.6.94
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-CmRIkCwI.js → index-DvlSlmGV.js} +111 -111
- package/dist/assets/{index-CDy8BuGq.js → index-qQxjvtjI.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +4 -4
- package/src/components/StudioPreviewArea.tsx +0 -9
- package/src/components/editor/DomEditOverlay.tsx +0 -63
- package/src/components/editor/PropertyPanel.tsx +0 -3
- package/src/components/editor/manualEditingAvailability.test.ts +5 -5
- package/src/components/editor/manualEditingAvailability.ts +1 -1
- package/src/components/editor/useOffScreenIndicators.ts +0 -197
package/dist/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
7
7
|
<title>HyperFrames Studio</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-DvlSlmGV.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-rm9tn9nH.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.94",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"@codemirror/view": "6.40.0",
|
|
32
32
|
"@phosphor-icons/react": "^2.1.10",
|
|
33
33
|
"mediabunny": "^1.45.3",
|
|
34
|
-
"@hyperframes/core": "0.6.
|
|
35
|
-
"@hyperframes/player": "0.6.
|
|
34
|
+
"@hyperframes/core": "0.6.94",
|
|
35
|
+
"@hyperframes/player": "0.6.94"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/react": "19",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"vite": "^6.4.2",
|
|
47
47
|
"vitest": "^3.2.4",
|
|
48
48
|
"zustand": "^5.0.0",
|
|
49
|
-
"@hyperframes/producer": "0.6.
|
|
49
|
+
"@hyperframes/producer": "0.6.94"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"react": "19",
|
|
@@ -108,7 +108,6 @@ export function StudioPreviewArea({
|
|
|
108
108
|
handlePreviewCanvasPointerMove,
|
|
109
109
|
handlePreviewCanvasPointerLeave,
|
|
110
110
|
applyDomSelection,
|
|
111
|
-
buildDomSelectionFromTarget,
|
|
112
111
|
handleBlockedDomMove,
|
|
113
112
|
handleDomManualDragStart,
|
|
114
113
|
handleDomPathOffsetCommit,
|
|
@@ -291,14 +290,6 @@ export function StudioPreviewArea({
|
|
|
291
290
|
onRotationCommit={handleDomRotationCommit}
|
|
292
291
|
gridVisible={snapPrefs.gridVisible}
|
|
293
292
|
gridSpacing={snapPrefs.gridSpacing}
|
|
294
|
-
onSelectElementById={async (id) => {
|
|
295
|
-
const iframe = previewIframeRef.current;
|
|
296
|
-
const el = iframe?.contentDocument?.getElementById(id);
|
|
297
|
-
if (!el) return null;
|
|
298
|
-
const sel = await buildDomSelectionFromTarget(el);
|
|
299
|
-
if (sel) applyDomSelection(sel, { revealPanel: true });
|
|
300
|
-
return sel;
|
|
301
|
-
}}
|
|
302
293
|
/>
|
|
303
294
|
<SnapToolbar onSnapChange={setSnapPrefs} />
|
|
304
295
|
{gestureOverlay}
|
|
@@ -13,7 +13,6 @@ import { useDomEditOverlayRects } from "./useDomEditOverlayRects";
|
|
|
13
13
|
import { createDomEditOverlayGestureHandlers } from "./useDomEditOverlayGestures";
|
|
14
14
|
import { SnapGuideOverlay, type SnapGuidesState } from "./SnapGuideOverlay";
|
|
15
15
|
import { GridOverlay } from "./GridOverlay";
|
|
16
|
-
import { useOffScreenIndicators } from "./useOffScreenIndicators";
|
|
17
16
|
|
|
18
17
|
// Re-exports for external consumers — preserving existing import paths.
|
|
19
18
|
export {
|
|
@@ -55,7 +54,6 @@ interface DomEditOverlayProps {
|
|
|
55
54
|
) => void;
|
|
56
55
|
onBlockedMove: (selection: DomEditSelection) => void;
|
|
57
56
|
onManualDragStart?: () => void;
|
|
58
|
-
onSelectElementById?: (id: string) => Promise<DomEditSelection | null>;
|
|
59
57
|
onPathOffsetCommit: (
|
|
60
58
|
selection: DomEditSelection,
|
|
61
59
|
next: { x: number; y: number },
|
|
@@ -85,7 +83,6 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
85
83
|
gridVisible = false,
|
|
86
84
|
gridSpacing = 50,
|
|
87
85
|
onManualDragStart,
|
|
88
|
-
onSelectElementById,
|
|
89
86
|
onPathOffsetCommit,
|
|
90
87
|
onGroupPathOffsetCommit,
|
|
91
88
|
onBoxSizeCommit,
|
|
@@ -215,8 +212,6 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
215
212
|
return () => cancelAnimationFrame(frame);
|
|
216
213
|
});
|
|
217
214
|
|
|
218
|
-
const offScreenIndicators = useOffScreenIndicators({ iframeRef, overlayRef, compRect });
|
|
219
|
-
|
|
220
215
|
const gestures = createDomEditOverlayGestureHandlers({
|
|
221
216
|
overlayRef,
|
|
222
217
|
iframeRef,
|
|
@@ -521,64 +516,6 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
521
516
|
}}
|
|
522
517
|
/>
|
|
523
518
|
))}
|
|
524
|
-
{offScreenIndicators.length > 0 &&
|
|
525
|
-
compRect.width > 0 &&
|
|
526
|
-
offScreenIndicators.map((ind) => {
|
|
527
|
-
const isSelected = selection?.id === ind.elementId;
|
|
528
|
-
return (
|
|
529
|
-
<div
|
|
530
|
-
key={`offscreen-${ind.key}`}
|
|
531
|
-
className={`absolute rounded-sm ${isSelected ? "pointer-events-none" : "cursor-grab"}`}
|
|
532
|
-
style={{
|
|
533
|
-
left: ind.left,
|
|
534
|
-
top: ind.top,
|
|
535
|
-
width: ind.width,
|
|
536
|
-
height: ind.height,
|
|
537
|
-
border: `1.5px dashed var(--panel-accent, #34d399)`,
|
|
538
|
-
opacity: isSelected ? 0.3 : 0.5,
|
|
539
|
-
zIndex: isSelected ? 1 : 5,
|
|
540
|
-
}}
|
|
541
|
-
onPointerDown={
|
|
542
|
-
isSelected
|
|
543
|
-
? undefined
|
|
544
|
-
: (e) => {
|
|
545
|
-
if (e.button !== 0) return;
|
|
546
|
-
e.stopPropagation();
|
|
547
|
-
e.preventDefault();
|
|
548
|
-
const startX = e.clientX;
|
|
549
|
-
const startY = e.clientY;
|
|
550
|
-
const el = e.currentTarget;
|
|
551
|
-
el.setPointerCapture(e.pointerId);
|
|
552
|
-
let deltaX = 0;
|
|
553
|
-
let deltaY = 0;
|
|
554
|
-
let moved = false;
|
|
555
|
-
const onMove = (me: PointerEvent) => {
|
|
556
|
-
deltaX = me.clientX - startX;
|
|
557
|
-
deltaY = me.clientY - startY;
|
|
558
|
-
if (Math.abs(deltaX) > 3 || Math.abs(deltaY) > 3) moved = true;
|
|
559
|
-
if (moved) {
|
|
560
|
-
el.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
|
|
561
|
-
}
|
|
562
|
-
};
|
|
563
|
-
const onUp = async (ue: PointerEvent) => {
|
|
564
|
-
el.releasePointerCapture(ue.pointerId);
|
|
565
|
-
el.removeEventListener("pointermove", onMove);
|
|
566
|
-
el.removeEventListener("pointerup", onUp);
|
|
567
|
-
el.style.transform = "";
|
|
568
|
-
const sel = await onSelectElementById?.(ind.elementId);
|
|
569
|
-
if (moved && sel && onPathOffsetCommit) {
|
|
570
|
-
const scale = compRect.scaleX || 1;
|
|
571
|
-
onPathOffsetCommit(sel, { x: deltaX / scale, y: deltaY / scale });
|
|
572
|
-
}
|
|
573
|
-
};
|
|
574
|
-
el.addEventListener("pointermove", onMove);
|
|
575
|
-
el.addEventListener("pointerup", onUp);
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
title={isSelected ? undefined : `Drag #${ind.elementId}`}
|
|
579
|
-
/>
|
|
580
|
-
);
|
|
581
|
-
})}
|
|
582
519
|
<GridOverlay
|
|
583
520
|
visible={gridVisible}
|
|
584
521
|
spacing={gridSpacing}
|
|
@@ -353,7 +353,6 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
353
353
|
</div>
|
|
354
354
|
</div>
|
|
355
355
|
</div>
|
|
356
|
-
|
|
357
356
|
<div className="flex-1 overflow-y-auto">
|
|
358
357
|
<TextSection
|
|
359
358
|
element={element}
|
|
@@ -369,7 +368,6 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
369
368
|
{element.dataAttributes.start != null && (
|
|
370
369
|
<TimingSection element={element} onSetAttribute={onSetAttribute} />
|
|
371
370
|
)}
|
|
372
|
-
|
|
373
371
|
{isMediaElement(element) && (
|
|
374
372
|
<MediaSection
|
|
375
373
|
projectDir={projectDir}
|
|
@@ -585,7 +583,6 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
585
583
|
</button>
|
|
586
584
|
</div>
|
|
587
585
|
)}
|
|
588
|
-
|
|
589
586
|
{showEditableSections && (
|
|
590
587
|
<StyleSections
|
|
591
588
|
projectId={projectId}
|
|
@@ -25,16 +25,16 @@ describe("manual editing availability", () => {
|
|
|
25
25
|
expect(availability.STUDIO_MOTION_PANEL_ENABLED).toBe(false);
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
it("
|
|
28
|
+
it("enables GSAP drag intercept by default", async () => {
|
|
29
29
|
const availability = await loadAvailabilityWithEnv({});
|
|
30
|
-
expect(availability.STUDIO_GSAP_DRAG_INTERCEPT_ENABLED).toBe(
|
|
30
|
+
expect(availability.STUDIO_GSAP_DRAG_INTERCEPT_ENABLED).toBe(true);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
it("
|
|
33
|
+
it("disables GSAP drag intercept when env var is false", async () => {
|
|
34
34
|
const availability = await loadAvailabilityWithEnv({
|
|
35
|
-
VITE_STUDIO_ENABLE_GSAP_DRAG_INTERCEPT: "
|
|
35
|
+
VITE_STUDIO_ENABLE_GSAP_DRAG_INTERCEPT: "false",
|
|
36
36
|
});
|
|
37
|
-
expect(availability.STUDIO_GSAP_DRAG_INTERCEPT_ENABLED).toBe(
|
|
37
|
+
expect(availability.STUDIO_GSAP_DRAG_INTERCEPT_ENABLED).toBe(false);
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
it("disables preview selection when the inspector panel flag is explicitly off", async () => {
|
|
@@ -74,7 +74,7 @@ export const STUDIO_GSAP_PANEL_ENABLED = resolveStudioBooleanEnvFlag(
|
|
|
74
74
|
export const STUDIO_KEYFRAMES_ENABLED = resolveStudioBooleanEnvFlag(
|
|
75
75
|
env,
|
|
76
76
|
["VITE_STUDIO_ENABLE_KEYFRAMES", "VITE_STUDIO_KEYFRAMES_ENABLED"],
|
|
77
|
-
|
|
77
|
+
false,
|
|
78
78
|
);
|
|
79
79
|
|
|
80
80
|
export const STUDIO_RAZOR_TOOL_ENABLED = resolveStudioBooleanEnvFlag(
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Detects GSAP-animated elements whose center is outside the visible composition
|
|
3
|
-
* area and returns edge-clamped indicator positions for each.
|
|
4
|
-
*/
|
|
5
|
-
import { useRef, useState, type RefObject } from "react";
|
|
6
|
-
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
7
|
-
|
|
8
|
-
export interface OffScreenIndicator {
|
|
9
|
-
key: string;
|
|
10
|
-
elementId: string;
|
|
11
|
-
left: number;
|
|
12
|
-
top: number;
|
|
13
|
-
width: number;
|
|
14
|
-
height: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface CompRect {
|
|
18
|
-
left: number;
|
|
19
|
-
top: number;
|
|
20
|
-
width: number;
|
|
21
|
-
height: number;
|
|
22
|
-
scaleX: number;
|
|
23
|
-
scaleY: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
type TimelineLike = { getChildren?: (nested: boolean) => Array<{ targets?: () => Element[] }> };
|
|
27
|
-
|
|
28
|
-
function isHtmlElement(node: unknown): node is HTMLElement {
|
|
29
|
-
return (
|
|
30
|
-
typeof node === "object" &&
|
|
31
|
-
node !== null &&
|
|
32
|
-
typeof (node as HTMLElement).getBoundingClientRect === "function" &&
|
|
33
|
-
typeof (node as HTMLElement).tagName === "string"
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function collectGsapTargetElements(iframe: HTMLIFrameElement): HTMLElement[] {
|
|
38
|
-
const win = iframe.contentWindow as
|
|
39
|
-
| (Window & { __timelines?: Record<string, TimelineLike> })
|
|
40
|
-
| null;
|
|
41
|
-
if (!win) return [];
|
|
42
|
-
|
|
43
|
-
let timelines: Record<string, TimelineLike> | undefined;
|
|
44
|
-
try {
|
|
45
|
-
timelines = win.__timelines;
|
|
46
|
-
} catch {
|
|
47
|
-
return [];
|
|
48
|
-
}
|
|
49
|
-
if (!timelines) return [];
|
|
50
|
-
|
|
51
|
-
const seen = new Set<HTMLElement>();
|
|
52
|
-
for (const tl of Object.values(timelines)) {
|
|
53
|
-
if (!tl?.getChildren) continue;
|
|
54
|
-
try {
|
|
55
|
-
for (const child of tl.getChildren(true)) {
|
|
56
|
-
if (!child.targets) continue;
|
|
57
|
-
for (const t of child.targets()) {
|
|
58
|
-
if (isHtmlElement(t)) seen.add(t);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
} catch {
|
|
62
|
-
// cross-origin or detached timeline — skip
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return Array.from(seen);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function indicatorsEqual(a: OffScreenIndicator[], b: OffScreenIndicator[]): boolean {
|
|
69
|
-
if (a.length !== b.length) return false;
|
|
70
|
-
for (let i = 0; i < a.length; i++) {
|
|
71
|
-
const ai = a[i]!;
|
|
72
|
-
const bi = b[i]!;
|
|
73
|
-
if (
|
|
74
|
-
ai.key !== bi.key ||
|
|
75
|
-
Math.abs(ai.left - bi.left) > 0.5 ||
|
|
76
|
-
Math.abs(ai.top - bi.top) > 0.5 ||
|
|
77
|
-
Math.abs(ai.width - bi.width) > 0.5 ||
|
|
78
|
-
Math.abs(ai.height - bi.height) > 0.5
|
|
79
|
-
)
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
return true;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function useOffScreenIndicators({
|
|
86
|
-
iframeRef,
|
|
87
|
-
overlayRef,
|
|
88
|
-
compRect,
|
|
89
|
-
}: {
|
|
90
|
-
iframeRef: RefObject<HTMLIFrameElement | null>;
|
|
91
|
-
overlayRef: RefObject<HTMLDivElement | null>;
|
|
92
|
-
compRect: CompRect;
|
|
93
|
-
}): OffScreenIndicator[] {
|
|
94
|
-
const [indicators, setIndicators] = useState<OffScreenIndicator[]>([]);
|
|
95
|
-
const prevRef = useRef<OffScreenIndicator[]>([]);
|
|
96
|
-
const compRectRef = useRef(compRect);
|
|
97
|
-
compRectRef.current = compRect;
|
|
98
|
-
|
|
99
|
-
useMountEffect(() => {
|
|
100
|
-
let frame = 0;
|
|
101
|
-
|
|
102
|
-
const update = () => {
|
|
103
|
-
frame = requestAnimationFrame(update);
|
|
104
|
-
|
|
105
|
-
const iframe = iframeRef.current;
|
|
106
|
-
const overlayEl = overlayRef.current;
|
|
107
|
-
const cr = compRectRef.current;
|
|
108
|
-
if (!iframe || !overlayEl || cr.width <= 0 || cr.height <= 0) {
|
|
109
|
-
if (prevRef.current.length > 0) {
|
|
110
|
-
prevRef.current = [];
|
|
111
|
-
setIndicators([]);
|
|
112
|
-
}
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const iframeRect = iframe.getBoundingClientRect();
|
|
117
|
-
const overlayRect = overlayEl.getBoundingClientRect();
|
|
118
|
-
|
|
119
|
-
const doc = iframe.contentDocument;
|
|
120
|
-
const root =
|
|
121
|
-
doc?.querySelector<HTMLElement>("[data-composition-id]") ?? doc?.documentElement ?? null;
|
|
122
|
-
if (!root) return;
|
|
123
|
-
|
|
124
|
-
const declaredWidth =
|
|
125
|
-
Number.parseFloat(root.getAttribute("data-width") ?? "") || iframeRect.width;
|
|
126
|
-
const declaredHeight =
|
|
127
|
-
Number.parseFloat(root.getAttribute("data-height") ?? "") || iframeRect.height;
|
|
128
|
-
const rootScaleX = iframeRect.width / declaredWidth;
|
|
129
|
-
const rootScaleY = iframeRect.height / declaredHeight;
|
|
130
|
-
|
|
131
|
-
const targets = collectGsapTargetElements(iframe);
|
|
132
|
-
if (targets.length === 0) {
|
|
133
|
-
if (prevRef.current.length > 0) {
|
|
134
|
-
prevRef.current = [];
|
|
135
|
-
setIndicators([]);
|
|
136
|
-
}
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Composition bounds in overlay coordinates
|
|
141
|
-
const compLeft = cr.left;
|
|
142
|
-
const compTop = cr.top;
|
|
143
|
-
const compRight = compLeft + cr.width;
|
|
144
|
-
const compBottom = compTop + cr.height;
|
|
145
|
-
|
|
146
|
-
const next: OffScreenIndicator[] = [];
|
|
147
|
-
const keyCounts = new Map<string, number>();
|
|
148
|
-
|
|
149
|
-
for (const el of targets) {
|
|
150
|
-
if (!el.isConnected) continue;
|
|
151
|
-
|
|
152
|
-
const elRect = el.getBoundingClientRect();
|
|
153
|
-
if (elRect.width <= 0 && elRect.height <= 0) continue;
|
|
154
|
-
|
|
155
|
-
// Element rect in overlay coordinates
|
|
156
|
-
const elLeft = iframeRect.left - overlayRect.left + elRect.left * rootScaleX;
|
|
157
|
-
const elTop = iframeRect.top - overlayRect.top + elRect.top * rootScaleY;
|
|
158
|
-
const elW = elRect.width * rootScaleX;
|
|
159
|
-
const elH = elRect.height * rootScaleY;
|
|
160
|
-
|
|
161
|
-
// Check if the element is fully inside the composition
|
|
162
|
-
if (
|
|
163
|
-
elLeft >= compLeft &&
|
|
164
|
-
elTop >= compTop &&
|
|
165
|
-
elLeft + elW <= compRight &&
|
|
166
|
-
elTop + elH <= compBottom
|
|
167
|
-
) {
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Only elements with a real id attribute can be selected via getElementById
|
|
172
|
-
if (!el.id) continue;
|
|
173
|
-
const count = keyCounts.get(el.id) ?? 0;
|
|
174
|
-
keyCounts.set(el.id, count + 1);
|
|
175
|
-
const key = count > 0 ? `${el.id}:${count}` : el.id;
|
|
176
|
-
next.push({
|
|
177
|
-
key,
|
|
178
|
-
elementId: el.id,
|
|
179
|
-
left: elLeft,
|
|
180
|
-
top: elTop,
|
|
181
|
-
width: elW,
|
|
182
|
-
height: elH,
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (!indicatorsEqual(prevRef.current, next)) {
|
|
187
|
-
prevRef.current = next;
|
|
188
|
-
setIndicators(next);
|
|
189
|
-
}
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
frame = requestAnimationFrame(update);
|
|
193
|
-
return () => cancelAnimationFrame(frame);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
return indicators;
|
|
197
|
-
}
|