@hyperframes/studio 0.6.88 → 0.6.90
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-BKuDHMYl.js +146 -0
- package/dist/assets/index-D2NkPomd.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +33 -193
- package/src/components/StudioLeftSidebar.tsx +6 -0
- package/src/components/StudioRightPanel.tsx +8 -0
- package/src/components/TimelineToolbar.tsx +54 -31
- package/src/components/editor/AnimationCard.tsx +15 -3
- package/src/components/editor/DomEditOverlay.test.ts +34 -1
- package/src/components/editor/FileTree.tsx +5 -1
- package/src/components/editor/FileTreeNodes.tsx +17 -3
- package/src/components/editor/LayersPanel.tsx +19 -4
- package/src/components/editor/PropertyPanel.tsx +82 -170
- package/src/components/editor/domEditOverlayStartGesture.ts +1 -0
- package/src/components/editor/gsapAnimatesProperty.ts +52 -0
- package/src/components/editor/manualEditsDom.ts +11 -57
- package/src/components/editor/manualOffsetDrag.test.ts +18 -1
- package/src/components/editor/manualOffsetDrag.ts +16 -10
- package/src/components/editor/propertyPanel3dTransform.tsx +133 -0
- package/src/components/editor/propertyPanelHelpers.ts +76 -0
- package/src/components/editor/propertyPanelStyleSections.tsx +1 -9
- package/src/components/editor/useDomEditOverlayGestures.ts +3 -0
- package/src/components/editor/useLayerDrag.ts +6 -3
- package/src/components/renders/RenderQueueItem.tsx +47 -46
- package/src/components/sidebar/CompositionsTab.tsx +15 -2
- package/src/components/sidebar/LeftSidebar.tsx +11 -0
- package/src/hooks/gsapDragCommit.ts +294 -0
- package/src/hooks/gsapKeyframeCacheHelpers.ts +88 -0
- package/src/hooks/gsapRuntimeBridge.ts +49 -402
- package/src/hooks/gsapRuntimeReaders.ts +201 -0
- package/src/hooks/timelineEditingHelpers.ts +148 -0
- package/src/hooks/useAnimatedPropertyCommit.ts +54 -12
- package/src/hooks/useBlockHandlers.ts +150 -0
- package/src/hooks/useClipboard.ts +1 -10
- package/src/hooks/useDomEditPreviewSync.ts +126 -0
- package/src/hooks/useDomEditSession.ts +11 -79
- package/src/hooks/useGestureCommit.ts +166 -0
- package/src/hooks/useGestureRecording.ts +271 -169
- package/src/hooks/useGsapScriptCommits.ts +7 -80
- package/src/hooks/useLintModal.ts +97 -25
- package/src/hooks/useTimelineEditing.ts +10 -132
- package/src/player/components/TimelineCanvas.tsx +24 -7
- package/src/player/components/useTimelinePlayhead.ts +2 -1
- package/src/player/store/playerStore.ts +12 -0
- package/src/utils/gsapSoftReload.ts +18 -1
- package/src/utils/studioUrlState.test.ts +9 -0
- package/dist/assets/index-B9_ctmee.js +0 -143
- package/dist/assets/index-CGlIm_-E.css +0 -1
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
} from "./manualEditsTypes";
|
|
33
33
|
import { roundRotationAngle } from "./manualEditsParsing";
|
|
34
34
|
import { applyStudioMotionFromDom } from "./studioMotion";
|
|
35
|
+
import { gsapAnimatesProperty } from "./gsapAnimatesProperty";
|
|
35
36
|
|
|
36
37
|
/* ── Gesture tracking ─────────────────────────────────────────────── */
|
|
37
38
|
let studioManualEditGestureId = 0;
|
|
@@ -519,70 +520,23 @@ function queryStudioElements(doc: Document, attr: string): HTMLElement[] {
|
|
|
519
520
|
|
|
520
521
|
function reapplyPathOffsets(doc: Document): void {
|
|
521
522
|
for (const el of queryStudioElements(doc, STUDIO_PATH_OFFSET_ATTR)) {
|
|
522
|
-
|
|
523
|
-
// CSS translate into its transform and sets translate: none every tick.
|
|
524
|
-
// Stripping/restoring would oscillate against GSAP's rendering.
|
|
525
|
-
if (gsapAnimatesProperty(el, "x", "y")) continue;
|
|
523
|
+
const gsapSkip = gsapAnimatesProperty(el, "x", "y");
|
|
526
524
|
const x = el.style.getPropertyValue(STUDIO_OFFSET_X_PROP);
|
|
527
525
|
const y = el.style.getPropertyValue(STUDIO_OFFSET_Y_PROP);
|
|
526
|
+
if (gsapSkip) continue;
|
|
528
527
|
if (x || y) {
|
|
529
|
-
applyStudioPathOffset(
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
528
|
+
applyStudioPathOffset(
|
|
529
|
+
el,
|
|
530
|
+
{
|
|
531
|
+
x: Number.parseFloat(x) || 0,
|
|
532
|
+
y: Number.parseFloat(y) || 0,
|
|
533
|
+
},
|
|
534
|
+
{ updateBase: false },
|
|
535
|
+
);
|
|
533
536
|
}
|
|
534
537
|
}
|
|
535
538
|
}
|
|
536
539
|
|
|
537
|
-
function gsapAnimatesProperty(el: HTMLElement, ...props: string[]): boolean {
|
|
538
|
-
const win = el.ownerDocument.defaultView as
|
|
539
|
-
| (Window & {
|
|
540
|
-
__timelines?: Record<
|
|
541
|
-
string,
|
|
542
|
-
{
|
|
543
|
-
getChildren?: (
|
|
544
|
-
deep: boolean,
|
|
545
|
-
) => Array<{ targets?: () => Element[]; vars?: Record<string, unknown> }>;
|
|
546
|
-
}
|
|
547
|
-
>;
|
|
548
|
-
})
|
|
549
|
-
| null;
|
|
550
|
-
if (!win?.__timelines) return false;
|
|
551
|
-
const propSet = new Set(props);
|
|
552
|
-
for (const tl of Object.values(win.__timelines)) {
|
|
553
|
-
if (!tl?.getChildren) continue;
|
|
554
|
-
try {
|
|
555
|
-
for (const child of tl.getChildren(true)) {
|
|
556
|
-
if (!child.targets || !child.vars) continue;
|
|
557
|
-
let targetsEl = false;
|
|
558
|
-
for (const t of child.targets()) {
|
|
559
|
-
if (t === el || (el.id && t.id === el.id)) {
|
|
560
|
-
targetsEl = true;
|
|
561
|
-
break;
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
if (!targetsEl) continue;
|
|
565
|
-
const vars = child.vars;
|
|
566
|
-
for (const p of propSet) {
|
|
567
|
-
if (p in vars) return true;
|
|
568
|
-
}
|
|
569
|
-
if (vars.keyframes && typeof vars.keyframes === "object") {
|
|
570
|
-
for (const kfVal of Object.values(vars.keyframes as Record<string, unknown>)) {
|
|
571
|
-
if (kfVal && typeof kfVal === "object") {
|
|
572
|
-
for (const p of propSet) {
|
|
573
|
-
if (p in (kfVal as Record<string, unknown>)) return true;
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
} catch {
|
|
580
|
-
/* */
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
return false;
|
|
584
|
-
}
|
|
585
|
-
|
|
586
540
|
function reapplyBoxSizes(doc: Document): void {
|
|
587
541
|
for (const el of queryStudioElements(doc, STUDIO_BOX_SIZE_ATTR)) {
|
|
588
542
|
if (gsapAnimatesProperty(el, "width", "height")) continue;
|
|
@@ -66,6 +66,7 @@ describe("measureManualOffsetDragScreenToOffsetMatrix", () => {
|
|
|
66
66
|
it("measures the element center response and restores probe styles", () => {
|
|
67
67
|
const window = new Window();
|
|
68
68
|
const element = window.document.createElement("div");
|
|
69
|
+
element.setAttribute("data-hf-studio-path-offset", "true");
|
|
69
70
|
window.document.body.append(element);
|
|
70
71
|
|
|
71
72
|
element.getBoundingClientRect = () => {
|
|
@@ -109,6 +110,7 @@ describe("measureManualOffsetDragScreenToOffsetMatrix", () => {
|
|
|
109
110
|
iframe.getBoundingClientRect = () => new window.DOMRect(50, 40, 100, 50);
|
|
110
111
|
|
|
111
112
|
const element = iframeDocument.createElement("div");
|
|
113
|
+
element.setAttribute("data-hf-studio-path-offset", "true");
|
|
112
114
|
iframeDocument.body.append(element);
|
|
113
115
|
element.getBoundingClientRect = () => {
|
|
114
116
|
const offsetX = Number.parseFloat(element.style.getPropertyValue(STUDIO_OFFSET_X_PROP)) || 0;
|
|
@@ -130,7 +132,7 @@ describe("measureManualOffsetDragScreenToOffsetMatrix", () => {
|
|
|
130
132
|
expect(nextOffset).toEqual({ x: 100, y: 50 });
|
|
131
133
|
});
|
|
132
134
|
|
|
133
|
-
it("
|
|
135
|
+
it("returns identity matrix for non-path-offset elements with zero initial offset", () => {
|
|
134
136
|
const window = new Window();
|
|
135
137
|
const element = window.document.createElement("div");
|
|
136
138
|
window.document.body.append(element);
|
|
@@ -138,6 +140,21 @@ describe("measureManualOffsetDragScreenToOffsetMatrix", () => {
|
|
|
138
140
|
|
|
139
141
|
const measured = measureManualOffsetDragScreenToOffsetMatrix(element, { x: 0, y: 0 });
|
|
140
142
|
|
|
143
|
+
expect(measured.ok).toBe(true);
|
|
144
|
+
if (measured.ok) {
|
|
145
|
+
expectMatrixClose(measured.matrix, { a: 1, b: 0, c: 0, d: 1 });
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("rejects path-offset elements whose movement response cannot be measured", () => {
|
|
150
|
+
const window = new Window();
|
|
151
|
+
const element = window.document.createElement("div");
|
|
152
|
+
element.setAttribute("data-hf-studio-path-offset", "true");
|
|
153
|
+
window.document.body.append(element);
|
|
154
|
+
element.getBoundingClientRect = () => new window.DOMRect(10, 20, 12, 8);
|
|
155
|
+
|
|
156
|
+
const measured = measureManualOffsetDragScreenToOffsetMatrix(element, { x: 0, y: 0 });
|
|
157
|
+
|
|
141
158
|
expect(measured.ok).toBe(false);
|
|
142
159
|
});
|
|
143
160
|
});
|
|
@@ -142,8 +142,18 @@ export function applyManualOffsetDragMatrix(matrix: ManualOffsetDragMatrix, poin
|
|
|
142
142
|
export function measureManualOffsetDragScreenToOffsetMatrix(
|
|
143
143
|
element: HTMLElement,
|
|
144
144
|
initialOffset: { x: number; y: number },
|
|
145
|
-
options: { probeSize?: number } = {},
|
|
145
|
+
options: { probeSize?: number; scaleX?: number; scaleY?: number } = {},
|
|
146
146
|
): { ok: true; matrix: ManualOffsetDragMatrix } | { ok: false; reason: string } {
|
|
147
|
+
if (
|
|
148
|
+
!element.hasAttribute("data-hf-studio-path-offset") &&
|
|
149
|
+
initialOffset.x === 0 &&
|
|
150
|
+
initialOffset.y === 0
|
|
151
|
+
) {
|
|
152
|
+
const sx = options.scaleX || 1;
|
|
153
|
+
const sy = options.scaleY || 1;
|
|
154
|
+
return { ok: true, matrix: { a: 1 / sx, b: 0, c: 0, d: 1 / sy } };
|
|
155
|
+
}
|
|
156
|
+
|
|
147
157
|
const probeSize = options.probeSize ?? DEFAULT_OFFSET_PROBE_PX;
|
|
148
158
|
if (!Number.isFinite(probeSize) || probeSize <= 0) {
|
|
149
159
|
return { ok: false, reason: "Invalid movement probe size." };
|
|
@@ -235,8 +245,6 @@ export function createManualOffsetDragMember(input: {
|
|
|
235
245
|
input.element.setAttribute("data-hf-drag-initial-offset-x", String(initialOffset.x));
|
|
236
246
|
input.element.setAttribute("data-hf-drag-initial-offset-y", String(initialOffset.y));
|
|
237
247
|
|
|
238
|
-
// Capture GSAP's x/y BEFORE any draft applies gsap.set — the commit path
|
|
239
|
-
// needs the original (uncorrupted) GSAP position to compute the new keyframe value.
|
|
240
248
|
const win = input.element.ownerDocument.defaultView as
|
|
241
249
|
| (Window & {
|
|
242
250
|
gsap?: { getProperty?: (el: Element, prop: string) => number };
|
|
@@ -248,8 +256,6 @@ export function createManualOffsetDragMember(input: {
|
|
|
248
256
|
input.element.setAttribute("data-hf-drag-gsap-base-x", String(gsapX));
|
|
249
257
|
input.element.setAttribute("data-hf-drag-gsap-base-y", String(gsapY));
|
|
250
258
|
|
|
251
|
-
// Pause GSAP timelines during drag to prevent the tween from overwriting
|
|
252
|
-
// the draft's gsap.set on every tick. Track which we paused to resume later.
|
|
253
259
|
if (win?.__timelines) {
|
|
254
260
|
const paused: string[] = [];
|
|
255
261
|
for (const [id, tl] of Object.entries(win.__timelines)) {
|
|
@@ -269,7 +275,10 @@ export function createManualOffsetDragMember(input: {
|
|
|
269
275
|
|
|
270
276
|
const initialPathOffset = captureStudioPathOffset(input.element);
|
|
271
277
|
const gestureToken = beginStudioManualEditGesture(input.element);
|
|
272
|
-
const measured = measureManualOffsetDragScreenToOffsetMatrix(input.element, initialOffset
|
|
278
|
+
const measured = measureManualOffsetDragScreenToOffsetMatrix(input.element, initialOffset, {
|
|
279
|
+
scaleX: input.rect.editScaleX,
|
|
280
|
+
scaleY: input.rect.editScaleY,
|
|
281
|
+
});
|
|
273
282
|
if (!measured.ok) {
|
|
274
283
|
// Fallback: when GSAP transforms interfere with probe measurement, use
|
|
275
284
|
// the preview scale as an approximation. The commit path reads the actual
|
|
@@ -363,7 +372,7 @@ export function endManualOffsetDragMembers(members: ManualOffsetDragMember[]): v
|
|
|
363
372
|
}
|
|
364
373
|
}
|
|
365
374
|
|
|
366
|
-
function resumeGsapTimelines(element: HTMLElement): void {
|
|
375
|
+
export function resumeGsapTimelines(element: HTMLElement): void {
|
|
367
376
|
const ids = element.getAttribute("data-hf-drag-paused-timelines");
|
|
368
377
|
element.removeAttribute("data-hf-drag-paused-timelines");
|
|
369
378
|
if (!ids) return;
|
|
@@ -374,9 +383,6 @@ function resumeGsapTimelines(element: HTMLElement): void {
|
|
|
374
383
|
})
|
|
375
384
|
| null;
|
|
376
385
|
if (!win) return;
|
|
377
|
-
// Re-seek to the current time to restore the paused timeline's render state.
|
|
378
|
-
// play() would start playback; pause() already stops. Seek re-renders at the
|
|
379
|
-
// current position without starting playback.
|
|
380
386
|
const t = win.__player?.getTime?.() ?? 0;
|
|
381
387
|
win.__player?.seek?.(t);
|
|
382
388
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { DomEditSelection } from "./domEditingTypes";
|
|
2
|
+
import { STUDIO_KEYFRAMES_ENABLED } from "./manualEditingAvailability";
|
|
3
|
+
import { MetricField } from "./propertyPanelPrimitives";
|
|
4
|
+
import { KeyframeNavigation } from "./KeyframeNavigation";
|
|
5
|
+
import { formatPxMetricValue, parsePxMetricValue, RESPONSIVE_GRID } from "./propertyPanelHelpers";
|
|
6
|
+
|
|
7
|
+
type KeyframeEntry = Array<{
|
|
8
|
+
percentage: number;
|
|
9
|
+
properties: Record<string, number | string>;
|
|
10
|
+
ease?: string;
|
|
11
|
+
}> | null;
|
|
12
|
+
|
|
13
|
+
interface PropertyPanel3dTransformProps {
|
|
14
|
+
gsapRuntimeValues: Record<string, number>;
|
|
15
|
+
gsapAnimId: string | null;
|
|
16
|
+
gsapKeyframes: KeyframeEntry;
|
|
17
|
+
currentPct: number;
|
|
18
|
+
elStart: number;
|
|
19
|
+
elDuration: number;
|
|
20
|
+
element: DomEditSelection;
|
|
21
|
+
onCommitAnimatedProperty?: (
|
|
22
|
+
element: DomEditSelection,
|
|
23
|
+
property: string,
|
|
24
|
+
value: number,
|
|
25
|
+
) => Promise<void>;
|
|
26
|
+
onSeekToTime?: (time: number) => void;
|
|
27
|
+
onRemoveKeyframe?: (animId: string, pct: number) => void;
|
|
28
|
+
onConvertToKeyframes?: (animId: string) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function PropertyPanel3dTransform({
|
|
32
|
+
gsapRuntimeValues,
|
|
33
|
+
gsapAnimId,
|
|
34
|
+
gsapKeyframes,
|
|
35
|
+
currentPct,
|
|
36
|
+
elStart,
|
|
37
|
+
elDuration,
|
|
38
|
+
element,
|
|
39
|
+
onCommitAnimatedProperty,
|
|
40
|
+
onSeekToTime,
|
|
41
|
+
onRemoveKeyframe,
|
|
42
|
+
onConvertToKeyframes,
|
|
43
|
+
}: PropertyPanel3dTransformProps) {
|
|
44
|
+
return (
|
|
45
|
+
<div className="mt-3 border-t border-neutral-800/40 pt-3">
|
|
46
|
+
<div className="mb-2 text-[10px] font-medium uppercase tracking-wider text-neutral-600">
|
|
47
|
+
3D Transform
|
|
48
|
+
</div>
|
|
49
|
+
<div className={RESPONSIVE_GRID}>
|
|
50
|
+
<div className="flex items-center gap-1">
|
|
51
|
+
<div className="flex-1">
|
|
52
|
+
<MetricField
|
|
53
|
+
label="Z"
|
|
54
|
+
value={formatPxMetricValue(gsapRuntimeValues.z ?? 0)}
|
|
55
|
+
scrub
|
|
56
|
+
onCommit={(next) => {
|
|
57
|
+
const v = parsePxMetricValue(next);
|
|
58
|
+
if (v != null && onCommitAnimatedProperty) {
|
|
59
|
+
void onCommitAnimatedProperty(element, "z", v);
|
|
60
|
+
}
|
|
61
|
+
}}
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
{STUDIO_KEYFRAMES_ENABLED && (gsapAnimId || onCommitAnimatedProperty) && (
|
|
65
|
+
<KeyframeNavigation
|
|
66
|
+
property="z"
|
|
67
|
+
keyframes={gsapKeyframes}
|
|
68
|
+
currentPercentage={currentPct}
|
|
69
|
+
onSeek={(pct) => onSeekToTime?.(elStart + (pct / 100) * elDuration)}
|
|
70
|
+
onAddKeyframe={() => {
|
|
71
|
+
if (onCommitAnimatedProperty) {
|
|
72
|
+
void onCommitAnimatedProperty(element, "z", gsapRuntimeValues?.z ?? 0);
|
|
73
|
+
}
|
|
74
|
+
}}
|
|
75
|
+
onRemoveKeyframe={(pct) => gsapAnimId && onRemoveKeyframe?.(gsapAnimId, pct)}
|
|
76
|
+
onConvertToKeyframes={() => gsapAnimId && onConvertToKeyframes?.(gsapAnimId)}
|
|
77
|
+
/>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
<div className="flex items-center gap-1">
|
|
81
|
+
<div className="flex-1">
|
|
82
|
+
<MetricField
|
|
83
|
+
label="Scale"
|
|
84
|
+
value={String(gsapRuntimeValues.scale ?? 1)}
|
|
85
|
+
scrub
|
|
86
|
+
onCommit={(next) => {
|
|
87
|
+
const v = Number.parseFloat(next);
|
|
88
|
+
if (Number.isFinite(v) && onCommitAnimatedProperty) {
|
|
89
|
+
void onCommitAnimatedProperty(element, "scale", v);
|
|
90
|
+
}
|
|
91
|
+
}}
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
{STUDIO_KEYFRAMES_ENABLED && (gsapAnimId || onCommitAnimatedProperty) && (
|
|
95
|
+
<KeyframeNavigation
|
|
96
|
+
property="scale"
|
|
97
|
+
keyframes={gsapKeyframes}
|
|
98
|
+
currentPercentage={currentPct}
|
|
99
|
+
onSeek={(pct) => onSeekToTime?.(elStart + (pct / 100) * elDuration)}
|
|
100
|
+
onAddKeyframe={() => {
|
|
101
|
+
if (onCommitAnimatedProperty) {
|
|
102
|
+
void onCommitAnimatedProperty(element, "scale", gsapRuntimeValues?.scale ?? 1);
|
|
103
|
+
}
|
|
104
|
+
}}
|
|
105
|
+
onRemoveKeyframe={(pct) => gsapAnimId && onRemoveKeyframe?.(gsapAnimId, pct)}
|
|
106
|
+
onConvertToKeyframes={() => gsapAnimId && onConvertToKeyframes?.(gsapAnimId)}
|
|
107
|
+
/>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
<MetricField
|
|
111
|
+
label="RotX"
|
|
112
|
+
value={`${gsapRuntimeValues.rotationX ?? 0}°`}
|
|
113
|
+
onCommit={(next) => {
|
|
114
|
+
const v = Number.parseFloat(next.replace("°", ""));
|
|
115
|
+
if (Number.isFinite(v) && onCommitAnimatedProperty) {
|
|
116
|
+
void onCommitAnimatedProperty(element, "rotationX", v);
|
|
117
|
+
}
|
|
118
|
+
}}
|
|
119
|
+
/>
|
|
120
|
+
<MetricField
|
|
121
|
+
label="RotY"
|
|
122
|
+
value={`${gsapRuntimeValues.rotationY ?? 0}°`}
|
|
123
|
+
onCommit={(next) => {
|
|
124
|
+
const v = Number.parseFloat(next.replace("°", ""));
|
|
125
|
+
if (Number.isFinite(v) && onCommitAnimatedProperty) {
|
|
126
|
+
void onCommitAnimatedProperty(element, "rotationY", v);
|
|
127
|
+
}
|
|
128
|
+
}}
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -2,6 +2,7 @@ import { parseCssColor, type ParsedColor } from "./colorValue";
|
|
|
2
2
|
import { COMMON_LOCAL_FONT_FAMILIES } from "./fontCatalog";
|
|
3
3
|
import type { DomEditSelection } from "./domEditing";
|
|
4
4
|
import type { ImportedFontAsset } from "./fontAssets";
|
|
5
|
+
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
5
6
|
|
|
6
7
|
export interface PropertyPanelProps {
|
|
7
8
|
projectId: string;
|
|
@@ -505,3 +506,78 @@ export function computeFitToChildrenSize(
|
|
|
505
506
|
const height = Math.round((maxY - minY) * scaleY);
|
|
506
507
|
return width > 0 && height > 0 ? { width, height } : null;
|
|
507
508
|
}
|
|
509
|
+
|
|
510
|
+
// ── GSAP runtime value readers (used by PropertyPanel) ────────────────────
|
|
511
|
+
|
|
512
|
+
export function readGsapRuntimeValuesForPanel(
|
|
513
|
+
gsapAnimId: string | null,
|
|
514
|
+
gsapAnimations: GsapAnimation[],
|
|
515
|
+
element: DomEditSelection,
|
|
516
|
+
previewIframeRef: React.RefObject<HTMLIFrameElement | null>,
|
|
517
|
+
): Record<string, number> | null {
|
|
518
|
+
if (!gsapAnimId || gsapAnimations.length === 0) return null;
|
|
519
|
+
const iframe = previewIframeRef?.current;
|
|
520
|
+
if (!iframe?.contentWindow) return null;
|
|
521
|
+
const selector = element.id ? `#${element.id}` : element.selector;
|
|
522
|
+
if (!selector) return null;
|
|
523
|
+
try {
|
|
524
|
+
const gsap = (
|
|
525
|
+
iframe.contentWindow as unknown as {
|
|
526
|
+
gsap?: { getProperty: (el: Element, prop: string) => number | string };
|
|
527
|
+
}
|
|
528
|
+
).gsap;
|
|
529
|
+
if (!gsap?.getProperty) return null;
|
|
530
|
+
const el = iframe.contentDocument?.querySelector(selector);
|
|
531
|
+
if (!el) return null;
|
|
532
|
+
const propKeys = new Set<string>();
|
|
533
|
+
for (const anim of gsapAnimations) {
|
|
534
|
+
if (anim.keyframes) {
|
|
535
|
+
for (const kf of anim.keyframes.keyframes) {
|
|
536
|
+
for (const p of Object.keys(kf.properties)) propKeys.add(p);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
for (const p of Object.keys(anim.properties)) propKeys.add(p);
|
|
540
|
+
}
|
|
541
|
+
const result: Record<string, number> = {};
|
|
542
|
+
for (const prop of propKeys) {
|
|
543
|
+
const v = Number(gsap.getProperty(el, prop));
|
|
544
|
+
if (Number.isFinite(v)) result[prop] = Math.round(v * 100) / 100;
|
|
545
|
+
}
|
|
546
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
547
|
+
} catch {
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export function readGsapBorderRadiusForPanel(
|
|
553
|
+
gsapRuntimeValues: Record<string, number> | null,
|
|
554
|
+
gsapAnimations: GsapAnimation[],
|
|
555
|
+
element: DomEditSelection,
|
|
556
|
+
previewIframeRef: React.RefObject<HTMLIFrameElement | null>,
|
|
557
|
+
): { tl: number; tr: number; br: number; bl: number } | null {
|
|
558
|
+
if (!gsapRuntimeValues || !("borderRadius" in gsapRuntimeValues)) {
|
|
559
|
+
const hasBRProp = gsapAnimations.some(
|
|
560
|
+
(a) =>
|
|
561
|
+
"borderRadius" in a.properties ||
|
|
562
|
+
a.keyframes?.keyframes.some((kf) => "borderRadius" in kf.properties),
|
|
563
|
+
);
|
|
564
|
+
if (!hasBRProp) return null;
|
|
565
|
+
}
|
|
566
|
+
const iframe = previewIframeRef?.current;
|
|
567
|
+
const selector = element.id ? `#${element.id}` : element.selector;
|
|
568
|
+
if (!iframe?.contentDocument || !selector) return null;
|
|
569
|
+
try {
|
|
570
|
+
const el = iframe.contentDocument.querySelector(selector);
|
|
571
|
+
if (!el) return null;
|
|
572
|
+
const cs = iframe.contentWindow!.getComputedStyle(el);
|
|
573
|
+
const parse = (v: string) => Number.parseFloat(v) || 0;
|
|
574
|
+
return {
|
|
575
|
+
tl: parse(cs.borderTopLeftRadius),
|
|
576
|
+
tr: parse(cs.borderTopRightRadius),
|
|
577
|
+
br: parse(cs.borderBottomRightRadius),
|
|
578
|
+
bl: parse(cs.borderBottomLeftRadius),
|
|
579
|
+
};
|
|
580
|
+
} catch {
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
@@ -369,15 +369,7 @@ export function StyleSections({
|
|
|
369
369
|
</div>
|
|
370
370
|
</Section>
|
|
371
371
|
|
|
372
|
-
<Section
|
|
373
|
-
title="Fill"
|
|
374
|
-
icon={<Palette size={15} />}
|
|
375
|
-
accessory={
|
|
376
|
-
<div className="rounded-full border border-neutral-700 bg-neutral-900 px-2.5 py-1 text-[10px] font-medium uppercase tracking-[0.16em] text-neutral-400">
|
|
377
|
-
{preferredFillMode}
|
|
378
|
-
</div>
|
|
379
|
-
}
|
|
380
|
-
>
|
|
372
|
+
<Section title="Fill" icon={<Palette size={15} />}>
|
|
381
373
|
<div className="space-y-4">
|
|
382
374
|
<SegmentedControl
|
|
383
375
|
disabled={styleEditingDisabled}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
applyManualOffsetDragDraft,
|
|
12
12
|
endManualOffsetDragMembers,
|
|
13
13
|
restoreManualOffsetDragMembers,
|
|
14
|
+
resumeGsapTimelines,
|
|
14
15
|
} from "./manualOffsetDrag";
|
|
15
16
|
import {
|
|
16
17
|
applyStudioBoxSize,
|
|
@@ -401,6 +402,7 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
401
402
|
if (g.kind === "drag" && movedDistance < BLOCKED_MOVE_THRESHOLD_PX) {
|
|
402
403
|
restoreStudioPathOffset(sel.element, g.initialPathOffset);
|
|
403
404
|
endStudioManualEditGesture(sel.element, g.manualEditDragToken);
|
|
405
|
+
resumeGsapTimelines(sel.element);
|
|
404
406
|
if (box) {
|
|
405
407
|
box.style.left = `${g.originLeft}px`;
|
|
406
408
|
box.style.top = `${g.originTop}px`;
|
|
@@ -507,6 +509,7 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
507
509
|
if (g?.mode === "path-offset" && sel) {
|
|
508
510
|
restoreStudioPathOffset(sel.element, g.initialPathOffset);
|
|
509
511
|
endStudioManualEditGesture(sel.element, g.manualEditDragToken);
|
|
512
|
+
resumeGsapTimelines(sel.element);
|
|
510
513
|
restoreGestureOverlayRect(g);
|
|
511
514
|
}
|
|
512
515
|
if (g?.mode === "box-size" && sel) {
|
|
@@ -138,9 +138,6 @@ export function useLayerDrag({
|
|
|
138
138
|
const container = scrollContainerRef.current;
|
|
139
139
|
if (!container) return;
|
|
140
140
|
|
|
141
|
-
e.preventDefault();
|
|
142
|
-
container.setPointerCapture(e.pointerId);
|
|
143
|
-
|
|
144
141
|
dragRef.current = {
|
|
145
142
|
pointerId: e.pointerId,
|
|
146
143
|
startY: e.clientY,
|
|
@@ -163,6 +160,12 @@ export function useLayerDrag({
|
|
|
163
160
|
if (!drag.activated) {
|
|
164
161
|
if (Math.abs(e.clientY - drag.startY) < DRAG_THRESHOLD_PX) return;
|
|
165
162
|
drag.activated = true;
|
|
163
|
+
const container = scrollContainerRef.current;
|
|
164
|
+
if (container && drag.pointerId != null) {
|
|
165
|
+
try {
|
|
166
|
+
container.setPointerCapture(drag.pointerId);
|
|
167
|
+
} catch {}
|
|
168
|
+
}
|
|
166
169
|
setDragKey(visibleLayers[drag.dragLayerIndex]?.key ?? null);
|
|
167
170
|
}
|
|
168
171
|
|
|
@@ -142,53 +142,54 @@ export const RenderQueueItem = memo(function RenderQueueItem({
|
|
|
142
142
|
)}
|
|
143
143
|
</div>
|
|
144
144
|
|
|
145
|
-
{/* Actions */}
|
|
146
|
-
|
|
147
|
-
<
|
|
148
|
-
{isComplete
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
<line x1="12" y1="15" x2="12" y2="3" />
|
|
167
|
-
</svg>
|
|
168
|
-
</button>
|
|
169
|
-
)}
|
|
170
|
-
<button
|
|
171
|
-
onClick={(e) => {
|
|
172
|
-
e.stopPropagation();
|
|
173
|
-
onDelete();
|
|
174
|
-
}}
|
|
175
|
-
className="p-1 rounded text-panel-text-4 hover:text-red-400 transition-colors"
|
|
176
|
-
title="Remove"
|
|
145
|
+
{/* Actions — always visible to prevent layout shifts */}
|
|
146
|
+
<div className="flex items-center gap-1 flex-shrink-0">
|
|
147
|
+
<button
|
|
148
|
+
onClick={isComplete ? handleDownload : undefined}
|
|
149
|
+
className={`p-1 rounded transition-colors ${
|
|
150
|
+
isComplete
|
|
151
|
+
? "text-panel-text-5 hover:text-panel-accent"
|
|
152
|
+
: "text-panel-text-5/30 pointer-events-none"
|
|
153
|
+
}`}
|
|
154
|
+
title={isComplete ? "Download" : "Rendering..."}
|
|
155
|
+
disabled={!isComplete}
|
|
156
|
+
>
|
|
157
|
+
<svg
|
|
158
|
+
width="12"
|
|
159
|
+
height="12"
|
|
160
|
+
viewBox="0 0 24 24"
|
|
161
|
+
fill="none"
|
|
162
|
+
stroke="currentColor"
|
|
163
|
+
strokeWidth="2"
|
|
164
|
+
strokeLinecap="round"
|
|
165
|
+
strokeLinejoin="round"
|
|
177
166
|
>
|
|
178
|
-
<
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
167
|
+
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
|
|
168
|
+
<polyline points="7 10 12 15 17 10" />
|
|
169
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
170
|
+
</svg>
|
|
171
|
+
</button>
|
|
172
|
+
<button
|
|
173
|
+
onClick={(e) => {
|
|
174
|
+
e.stopPropagation();
|
|
175
|
+
onDelete();
|
|
176
|
+
}}
|
|
177
|
+
className="p-1 rounded text-panel-text-5 hover:text-red-400 transition-colors"
|
|
178
|
+
title="Remove"
|
|
179
|
+
>
|
|
180
|
+
<svg
|
|
181
|
+
width="12"
|
|
182
|
+
height="12"
|
|
183
|
+
viewBox="0 0 24 24"
|
|
184
|
+
fill="none"
|
|
185
|
+
stroke="currentColor"
|
|
186
|
+
strokeWidth="2"
|
|
187
|
+
strokeLinecap="round"
|
|
188
|
+
>
|
|
189
|
+
<path d="M18 6L6 18M6 6l12 12" />
|
|
190
|
+
</svg>
|
|
191
|
+
</button>
|
|
192
|
+
</div>
|
|
192
193
|
</div>
|
|
193
194
|
</div>
|
|
194
195
|
);
|
|
@@ -7,6 +7,7 @@ interface CompositionsTabProps {
|
|
|
7
7
|
onSelect: (comp: string) => void;
|
|
8
8
|
onRenderComposition?: (comp: string) => void;
|
|
9
9
|
isRendering?: boolean;
|
|
10
|
+
lintFindingsByFile?: Map<string, { count: number; messages: string[] }>;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
const DEFAULT_PREVIEW_STAGE = { width: 1920, height: 1080 };
|
|
@@ -111,6 +112,7 @@ function CompCard({
|
|
|
111
112
|
onSelect,
|
|
112
113
|
onRender,
|
|
113
114
|
isRendering,
|
|
115
|
+
lintInfo,
|
|
114
116
|
}: {
|
|
115
117
|
projectId: string;
|
|
116
118
|
comp: string;
|
|
@@ -118,6 +120,7 @@ function CompCard({
|
|
|
118
120
|
onSelect: () => void;
|
|
119
121
|
onRender?: () => void;
|
|
120
122
|
isRendering?: boolean;
|
|
123
|
+
lintInfo?: { count: number; messages: string[] };
|
|
121
124
|
}) {
|
|
122
125
|
const [hovered, setHovered] = useState(false);
|
|
123
126
|
const [stageSize, setStageSize] = useState(DEFAULT_PREVIEW_STAGE);
|
|
@@ -215,8 +218,16 @@ function CompCard({
|
|
|
215
218
|
tabIndex={-1}
|
|
216
219
|
/>
|
|
217
220
|
</div>
|
|
218
|
-
<div
|
|
219
|
-
|
|
221
|
+
<div
|
|
222
|
+
className="min-w-0 flex-1"
|
|
223
|
+
title={lintInfo && lintInfo.count > 0 ? lintInfo.messages.join("\n") : undefined}
|
|
224
|
+
>
|
|
225
|
+
<div className="flex items-center gap-1">
|
|
226
|
+
<span className="text-[11px] font-medium text-neutral-300 truncate">{name}</span>
|
|
227
|
+
{lintInfo && lintInfo.count > 0 && (
|
|
228
|
+
<span className="flex-shrink-0 w-2 h-2 rounded-full bg-amber-400" />
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
220
231
|
<span className="text-[9px] text-neutral-600 truncate block">{comp}</span>
|
|
221
232
|
</div>
|
|
222
233
|
{onRender && (
|
|
@@ -262,6 +273,7 @@ export const CompositionsTab = memo(function CompositionsTab({
|
|
|
262
273
|
onSelect,
|
|
263
274
|
onRenderComposition,
|
|
264
275
|
isRendering,
|
|
276
|
+
lintFindingsByFile,
|
|
265
277
|
}: CompositionsTabProps) {
|
|
266
278
|
if (compositions.length === 0) {
|
|
267
279
|
return (
|
|
@@ -282,6 +294,7 @@ export const CompositionsTab = memo(function CompositionsTab({
|
|
|
282
294
|
onSelect={() => onSelect(comp)}
|
|
283
295
|
onRender={onRenderComposition ? () => onRenderComposition(comp) : undefined}
|
|
284
296
|
isRendering={isRendering}
|
|
297
|
+
lintInfo={lintFindingsByFile?.get(comp)}
|
|
285
298
|
/>
|
|
286
299
|
))}
|
|
287
300
|
</div>
|