@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
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { memo } from "react";
|
|
2
|
-
import {
|
|
3
|
-
import { type DomEditSelection } from "./domEditing";
|
|
2
|
+
import { Eye, Layers, MessageSquare, Move, X } from "../../icons/SystemIcons";
|
|
4
3
|
import { readStudioBoxSize, readStudioPathOffset, readStudioRotation } from "./manualEdits";
|
|
5
|
-
import type { ImportedFontAsset } from "./fontAssets";
|
|
6
4
|
import {
|
|
7
5
|
EMPTY_STYLES,
|
|
8
6
|
formatPxMetricValue,
|
|
@@ -14,7 +12,11 @@ import { MetricField, Section } from "./propertyPanelPrimitives";
|
|
|
14
12
|
import { isMediaElement, MediaSection } from "./propertyPanelMediaSection";
|
|
15
13
|
import { TextSection, StyleSections } from "./propertyPanelSections";
|
|
16
14
|
import { GsapAnimationSection } from "./GsapAnimationSection";
|
|
17
|
-
import {
|
|
15
|
+
import { KeyframeNavigation } from "./KeyframeNavigation";
|
|
16
|
+
import { STUDIO_GSAP_PANEL_ENABLED, STUDIO_KEYFRAMES_ENABLED } from "./manualEditingAvailability";
|
|
17
|
+
import { usePlayerStore } from "../../player";
|
|
18
|
+
import { TimingSection } from "./propertyPanelTimingSection";
|
|
19
|
+
import { computeFitToChildrenSize, type PropertyPanelProps } from "./propertyPanelHelpers";
|
|
18
20
|
|
|
19
21
|
// Re-export helpers that external consumers import from this module
|
|
20
22
|
export {
|
|
@@ -28,109 +30,6 @@ export {
|
|
|
28
30
|
setCssFilterFunctionPx,
|
|
29
31
|
} from "./propertyPanelHelpers";
|
|
30
32
|
|
|
31
|
-
interface PropertyPanelProps {
|
|
32
|
-
projectId: string;
|
|
33
|
-
projectDir: string | null;
|
|
34
|
-
assets: string[];
|
|
35
|
-
element: DomEditSelection | null;
|
|
36
|
-
multiSelectCount?: number;
|
|
37
|
-
copiedAgentPrompt: boolean;
|
|
38
|
-
onClearSelection: () => void;
|
|
39
|
-
onSetStyle: (prop: string, value: string) => void | Promise<void>;
|
|
40
|
-
onSetAttribute: (attr: string, value: string) => void | Promise<void>;
|
|
41
|
-
onSetHtmlAttribute: (attr: string, value: string | null) => void | Promise<void>;
|
|
42
|
-
onSetManualOffset: (element: DomEditSelection, next: { x: number; y: number }) => void;
|
|
43
|
-
onSetManualSize: (element: DomEditSelection, next: { width: number; height: number }) => void;
|
|
44
|
-
onSetManualRotation: (element: DomEditSelection, next: { angle: number }) => void;
|
|
45
|
-
onSetText: (value: string, fieldKey?: string) => void;
|
|
46
|
-
onSetTextFieldStyle: (fieldKey: string, property: string, value: string) => void;
|
|
47
|
-
onAddTextField: (afterFieldKey?: string) => string | Promise<string | null> | null;
|
|
48
|
-
onRemoveTextField: (fieldKey: string) => void;
|
|
49
|
-
onAskAgent: () => void;
|
|
50
|
-
onImportAssets?: (files: FileList) => Promise<string[]>;
|
|
51
|
-
fontAssets?: ImportedFontAsset[];
|
|
52
|
-
onImportFonts?: (files: FileList | File[]) => Promise<ImportedFontAsset[]>;
|
|
53
|
-
gsapAnimations?: import("@hyperframes/core/gsap-parser").GsapAnimation[];
|
|
54
|
-
gsapMultipleTimelines?: boolean;
|
|
55
|
-
gsapUnsupportedTimelinePattern?: boolean;
|
|
56
|
-
onUpdateGsapProperty?: (animId: string, prop: string, value: number | string) => void;
|
|
57
|
-
onUpdateGsapMeta?: (
|
|
58
|
-
animId: string,
|
|
59
|
-
updates: { duration?: number; ease?: string; position?: number },
|
|
60
|
-
) => void;
|
|
61
|
-
onDeleteGsapAnimation?: (animId: string) => void;
|
|
62
|
-
onAddGsapProperty?: (animId: string, prop: string) => void;
|
|
63
|
-
onRemoveGsapProperty?: (animId: string, prop: string) => void;
|
|
64
|
-
onUpdateGsapFromProperty?: (animId: string, prop: string, value: number | string) => void;
|
|
65
|
-
onAddGsapFromProperty?: (animId: string, prop: string) => void;
|
|
66
|
-
onRemoveGsapFromProperty?: (animId: string, prop: string) => void;
|
|
67
|
-
onAddGsapAnimation?: (method: "to" | "from" | "set" | "fromTo") => void;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/* ------------------------------------------------------------------ */
|
|
71
|
-
/* TimingSection */
|
|
72
|
-
/* ------------------------------------------------------------------ */
|
|
73
|
-
|
|
74
|
-
function formatTimingValue(seconds: number): string {
|
|
75
|
-
if (!Number.isFinite(seconds) || seconds < 0) return "0.00s";
|
|
76
|
-
return `${seconds.toFixed(2)}s`;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function parseTimingValue(input: string): number | null {
|
|
80
|
-
const cleaned = input.replace(/s$/i, "").trim();
|
|
81
|
-
const parsed = Number.parseFloat(cleaned);
|
|
82
|
-
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function TimingSection({
|
|
86
|
-
element,
|
|
87
|
-
onSetAttribute,
|
|
88
|
-
}: {
|
|
89
|
-
element: DomEditSelection;
|
|
90
|
-
onSetAttribute: (attr: string, value: string) => void | Promise<void>;
|
|
91
|
-
}) {
|
|
92
|
-
const start = Number.parseFloat(element.dataAttributes.start ?? "0") || 0;
|
|
93
|
-
const duration =
|
|
94
|
-
Number.parseFloat(
|
|
95
|
-
element.dataAttributes.duration ?? element.dataAttributes["hf-authored-duration"] ?? "0",
|
|
96
|
-
) || 0;
|
|
97
|
-
const end = start + duration;
|
|
98
|
-
|
|
99
|
-
const commitStart = (nextValue: string) => {
|
|
100
|
-
const parsed = parseTimingValue(nextValue);
|
|
101
|
-
if (parsed == null) return;
|
|
102
|
-
void onSetAttribute("start", parsed.toFixed(2));
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const commitDuration = (nextValue: string) => {
|
|
106
|
-
const parsed = parseTimingValue(nextValue);
|
|
107
|
-
if (parsed == null || parsed <= 0) return;
|
|
108
|
-
void onSetAttribute("duration", parsed.toFixed(2));
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
const commitEnd = (nextValue: string) => {
|
|
112
|
-
const parsed = parseTimingValue(nextValue);
|
|
113
|
-
if (parsed == null || parsed <= start) return;
|
|
114
|
-
void onSetAttribute("duration", (parsed - start).toFixed(2));
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
return (
|
|
118
|
-
<Section title="Timing" icon={<Clock size={15} />}>
|
|
119
|
-
<div className={RESPONSIVE_GRID}>
|
|
120
|
-
<MetricField label="Start" value={formatTimingValue(start)} onCommit={commitStart} />
|
|
121
|
-
<MetricField label="End" value={formatTimingValue(end)} onCommit={commitEnd} />
|
|
122
|
-
</div>
|
|
123
|
-
<div className="mt-3">
|
|
124
|
-
<MetricField
|
|
125
|
-
label="Duration"
|
|
126
|
-
value={formatTimingValue(duration)}
|
|
127
|
-
onCommit={commitDuration}
|
|
128
|
-
/>
|
|
129
|
-
</div>
|
|
130
|
-
</Section>
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
33
|
/* ------------------------------------------------------------------ */
|
|
135
34
|
/* PropertyPanel */
|
|
136
35
|
/* ------------------------------------------------------------------ */
|
|
@@ -158,6 +57,7 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
158
57
|
onImportAssets,
|
|
159
58
|
fontAssets = [],
|
|
160
59
|
onImportFonts,
|
|
60
|
+
previewIframeRef,
|
|
161
61
|
gsapAnimations = [],
|
|
162
62
|
gsapMultipleTimelines,
|
|
163
63
|
gsapUnsupportedTimelinePattern,
|
|
@@ -170,6 +70,11 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
170
70
|
onAddGsapFromProperty,
|
|
171
71
|
onRemoveGsapFromProperty,
|
|
172
72
|
onAddGsapAnimation,
|
|
73
|
+
onAddKeyframe,
|
|
74
|
+
onRemoveKeyframe,
|
|
75
|
+
onConvertToKeyframes,
|
|
76
|
+
onCommitAnimatedProperty,
|
|
77
|
+
onSeekToTime,
|
|
173
78
|
}: PropertyPanelProps) {
|
|
174
79
|
const styles = element?.computedStyles ?? EMPTY_STYLES;
|
|
175
80
|
|
|
@@ -223,6 +128,15 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
223
128
|
const commitManualOffset = (axis: "x" | "y", nextValue: string) => {
|
|
224
129
|
const parsed = parsePxMetricValue(nextValue);
|
|
225
130
|
if (parsed == null) return;
|
|
131
|
+
if (onCommitAnimatedProperty && (gsapAnimId || gsapAnimations.length > 0)) {
|
|
132
|
+
void onCommitAnimatedProperty(element, axis, parsed);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (gsapKeyframes && gsapAnimId && onAddKeyframe) {
|
|
136
|
+
const pct = Math.max(0, Math.min(100, Math.round(currentPct * 10) / 10));
|
|
137
|
+
onAddKeyframe(gsapAnimId, pct, axis, parsed);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
226
140
|
const current = readStudioPathOffset(element.element);
|
|
227
141
|
onSetManualOffset(element, {
|
|
228
142
|
x: axis === "x" ? parsed : current.x,
|
|
@@ -256,6 +170,59 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
256
170
|
onSetManualRotation(element, { angle: parsed });
|
|
257
171
|
};
|
|
258
172
|
|
|
173
|
+
// Keyframe navigation state
|
|
174
|
+
const elStart = Number.parseFloat(element?.dataAttributes?.start ?? "0") || 0;
|
|
175
|
+
const elDuration = Number.parseFloat(element?.dataAttributes?.duration ?? "1") || 0;
|
|
176
|
+
const currentTime = usePlayerStore((s) => s.currentTime);
|
|
177
|
+
const currentPct = elDuration > 0 ? ((currentTime - elStart) / elDuration) * 100 : 0;
|
|
178
|
+
|
|
179
|
+
const gsapKeyframes = gsapAnimations?.find((a) => a.keyframes)?.keyframes?.keyframes ?? null;
|
|
180
|
+
const gsapAnimId =
|
|
181
|
+
gsapAnimations?.find((a) => a.keyframes)?.id ?? gsapAnimations?.[0]?.id ?? null;
|
|
182
|
+
|
|
183
|
+
// Read ALL GSAP-interpolated values at the current seek time.
|
|
184
|
+
// Discovers animated properties from the animation's keyframes/tween vars.
|
|
185
|
+
const gsapRuntimeValues: Record<string, number> | null = (() => {
|
|
186
|
+
if (!gsapAnimId || gsapAnimations.length === 0) return null;
|
|
187
|
+
const iframe = previewIframeRef?.current;
|
|
188
|
+
if (!iframe?.contentWindow) return null;
|
|
189
|
+
const selector = element.id ? `#${element.id}` : element.selector;
|
|
190
|
+
if (!selector) return null;
|
|
191
|
+
try {
|
|
192
|
+
const gsap = (
|
|
193
|
+
iframe.contentWindow as unknown as {
|
|
194
|
+
gsap?: { getProperty: (el: Element, prop: string) => number | string };
|
|
195
|
+
}
|
|
196
|
+
).gsap;
|
|
197
|
+
if (!gsap?.getProperty) return null;
|
|
198
|
+
const el = iframe.contentDocument?.querySelector(selector);
|
|
199
|
+
if (!el) return null;
|
|
200
|
+
const propKeys = new Set<string>();
|
|
201
|
+
for (const anim of gsapAnimations) {
|
|
202
|
+
if (anim.keyframes) {
|
|
203
|
+
for (const kf of anim.keyframes.keyframes) {
|
|
204
|
+
for (const p of Object.keys(kf.properties)) propKeys.add(p);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
for (const p of Object.keys(anim.properties)) propKeys.add(p);
|
|
208
|
+
}
|
|
209
|
+
const result: Record<string, number> = {};
|
|
210
|
+
for (const prop of propKeys) {
|
|
211
|
+
const v = Number(gsap.getProperty(el, prop));
|
|
212
|
+
if (Number.isFinite(v)) result[prop] = Math.round(v * 100) / 100;
|
|
213
|
+
}
|
|
214
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
215
|
+
} catch {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
})();
|
|
219
|
+
|
|
220
|
+
const displayX = gsapRuntimeValues?.x ?? manualOffset.x;
|
|
221
|
+
const displayY = gsapRuntimeValues?.y ?? manualOffset.y;
|
|
222
|
+
const displayW = gsapRuntimeValues?.width ?? resolvedWidth;
|
|
223
|
+
const displayH = gsapRuntimeValues?.height ?? resolvedHeight;
|
|
224
|
+
const displayR = gsapRuntimeValues?.rotation ?? manualRotation.angle;
|
|
225
|
+
|
|
259
226
|
return (
|
|
260
227
|
<div className="flex h-full min-h-0 flex-col overflow-hidden bg-neutral-900 text-neutral-100">
|
|
261
228
|
<div className="border-b border-neutral-800 px-4 py-5">
|
|
@@ -317,41 +284,227 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
317
284
|
|
|
318
285
|
<Section title="Layout" icon={<Move size={15} />}>
|
|
319
286
|
<div className={RESPONSIVE_GRID}>
|
|
320
|
-
<
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
287
|
+
<div className="flex items-center gap-1">
|
|
288
|
+
<div className="flex-1">
|
|
289
|
+
<MetricField
|
|
290
|
+
label="X"
|
|
291
|
+
value={formatPxMetricValue(displayX)}
|
|
292
|
+
disabled={manualOffsetEditingDisabled}
|
|
293
|
+
scrub
|
|
294
|
+
onCommit={(next) => commitManualOffset("x", next)}
|
|
295
|
+
/>
|
|
296
|
+
</div>
|
|
297
|
+
{STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
|
|
298
|
+
<KeyframeNavigation
|
|
299
|
+
property="x"
|
|
300
|
+
keyframes={gsapKeyframes}
|
|
301
|
+
currentPercentage={currentPct}
|
|
302
|
+
onSeek={(pct) => onSeekToTime?.(elStart + (pct / 100) * elDuration)}
|
|
303
|
+
onAddKeyframe={() =>
|
|
304
|
+
onCommitAnimatedProperty &&
|
|
305
|
+
void onCommitAnimatedProperty(element, "x", displayX)
|
|
306
|
+
}
|
|
307
|
+
onRemoveKeyframe={(pct) => onRemoveKeyframe?.(gsapAnimId, pct)}
|
|
308
|
+
onConvertToKeyframes={() => onConvertToKeyframes?.(gsapAnimId)}
|
|
309
|
+
/>
|
|
310
|
+
)}
|
|
311
|
+
</div>
|
|
312
|
+
<div className="flex items-center gap-1">
|
|
313
|
+
<div className="flex-1">
|
|
314
|
+
<MetricField
|
|
315
|
+
label="Y"
|
|
316
|
+
value={formatPxMetricValue(displayY)}
|
|
317
|
+
disabled={manualOffsetEditingDisabled}
|
|
318
|
+
scrub
|
|
319
|
+
onCommit={(next) => commitManualOffset("y", next)}
|
|
320
|
+
/>
|
|
321
|
+
</div>
|
|
322
|
+
{STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
|
|
323
|
+
<KeyframeNavigation
|
|
324
|
+
property="y"
|
|
325
|
+
keyframes={gsapKeyframes}
|
|
326
|
+
currentPercentage={currentPct}
|
|
327
|
+
onSeek={(pct) => onSeekToTime?.(elStart + (pct / 100) * elDuration)}
|
|
328
|
+
onAddKeyframe={() =>
|
|
329
|
+
onCommitAnimatedProperty &&
|
|
330
|
+
void onCommitAnimatedProperty(element, "y", displayY)
|
|
331
|
+
}
|
|
332
|
+
onRemoveKeyframe={(pct) => onRemoveKeyframe?.(gsapAnimId, pct)}
|
|
333
|
+
onConvertToKeyframes={() => onConvertToKeyframes?.(gsapAnimId)}
|
|
334
|
+
/>
|
|
335
|
+
)}
|
|
336
|
+
</div>
|
|
337
|
+
<div className="flex items-center gap-1">
|
|
338
|
+
<div className="flex-1">
|
|
339
|
+
<MetricField
|
|
340
|
+
label="W"
|
|
341
|
+
value={formatPxMetricValue(displayW)}
|
|
342
|
+
disabled={manualSizeEditingDisabled}
|
|
343
|
+
scrub
|
|
344
|
+
onCommit={(next) => commitManualSize("width", next)}
|
|
345
|
+
/>
|
|
346
|
+
</div>
|
|
347
|
+
{STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
|
|
348
|
+
<KeyframeNavigation
|
|
349
|
+
property="width"
|
|
350
|
+
keyframes={gsapKeyframes}
|
|
351
|
+
currentPercentage={currentPct}
|
|
352
|
+
onSeek={(pct) => onSeekToTime?.(elStart + (pct / 100) * elDuration)}
|
|
353
|
+
onAddKeyframe={() =>
|
|
354
|
+
onCommitAnimatedProperty &&
|
|
355
|
+
void onCommitAnimatedProperty(element, "width", displayW)
|
|
356
|
+
}
|
|
357
|
+
onRemoveKeyframe={(pct) => onRemoveKeyframe?.(gsapAnimId, pct)}
|
|
358
|
+
onConvertToKeyframes={() => onConvertToKeyframes?.(gsapAnimId)}
|
|
359
|
+
/>
|
|
360
|
+
)}
|
|
361
|
+
</div>
|
|
362
|
+
<div className="flex items-center gap-1">
|
|
363
|
+
<div className="flex-1">
|
|
364
|
+
<MetricField
|
|
365
|
+
label="H"
|
|
366
|
+
value={formatPxMetricValue(displayH)}
|
|
367
|
+
disabled={manualSizeEditingDisabled}
|
|
368
|
+
scrub
|
|
369
|
+
onCommit={(next) => commitManualSize("height", next)}
|
|
370
|
+
/>
|
|
371
|
+
</div>
|
|
372
|
+
{STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
|
|
373
|
+
<KeyframeNavigation
|
|
374
|
+
property="height"
|
|
375
|
+
keyframes={gsapKeyframes}
|
|
376
|
+
currentPercentage={currentPct}
|
|
377
|
+
onSeek={(pct) => onSeekToTime?.(elStart + (pct / 100) * elDuration)}
|
|
378
|
+
onAddKeyframe={() =>
|
|
379
|
+
onCommitAnimatedProperty &&
|
|
380
|
+
void onCommitAnimatedProperty(element, "height", displayH)
|
|
381
|
+
}
|
|
382
|
+
onRemoveKeyframe={(pct) => onRemoveKeyframe?.(gsapAnimId, pct)}
|
|
383
|
+
onConvertToKeyframes={() => onConvertToKeyframes?.(gsapAnimId)}
|
|
384
|
+
/>
|
|
385
|
+
)}
|
|
386
|
+
</div>
|
|
387
|
+
{element.capabilities.canApplyManualSize && (
|
|
388
|
+
<button
|
|
389
|
+
type="button"
|
|
390
|
+
className="flex-shrink-0 rounded p-1 text-neutral-500 transition-colors hover:bg-neutral-800 hover:text-neutral-300"
|
|
391
|
+
title="Fit to children"
|
|
392
|
+
onClick={() => {
|
|
393
|
+
const size = computeFitToChildrenSize(element);
|
|
394
|
+
if (size) onSetManualSize(element, size);
|
|
395
|
+
}}
|
|
396
|
+
>
|
|
397
|
+
<svg
|
|
398
|
+
width="14"
|
|
399
|
+
height="14"
|
|
400
|
+
viewBox="0 0 14 14"
|
|
401
|
+
fill="none"
|
|
402
|
+
stroke="currentColor"
|
|
403
|
+
strokeWidth="1.2"
|
|
404
|
+
>
|
|
405
|
+
<rect x="2" y="2" width="10" height="10" strokeDasharray="2 1.5" rx="1" />
|
|
406
|
+
<path d="M2 4.5h1m-1 5h1m8-5h1m-1 5h1M4.5 2v1m5-1v1M4.5 11v1m5-1v1" />
|
|
407
|
+
</svg>
|
|
408
|
+
</button>
|
|
409
|
+
)}
|
|
410
|
+
<div className="flex items-center gap-1">
|
|
411
|
+
<div className="flex-1">
|
|
412
|
+
<MetricField
|
|
413
|
+
label="R"
|
|
414
|
+
value={`${displayR}°`}
|
|
415
|
+
onCommit={(next) => commitManualRotation(next.replace("°", ""))}
|
|
416
|
+
/>
|
|
417
|
+
</div>
|
|
418
|
+
{STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
|
|
419
|
+
<KeyframeNavigation
|
|
420
|
+
property="rotation"
|
|
421
|
+
keyframes={gsapKeyframes}
|
|
422
|
+
currentPercentage={currentPct}
|
|
423
|
+
onSeek={(pct) => onSeekToTime?.(elStart + (pct / 100) * elDuration)}
|
|
424
|
+
onAddKeyframe={() =>
|
|
425
|
+
onCommitAnimatedProperty &&
|
|
426
|
+
void onCommitAnimatedProperty(element, "rotation", displayR)
|
|
427
|
+
}
|
|
428
|
+
onRemoveKeyframe={(pct) => onRemoveKeyframe?.(gsapAnimId, pct)}
|
|
429
|
+
onConvertToKeyframes={() => onConvertToKeyframes?.(gsapAnimId)}
|
|
430
|
+
/>
|
|
431
|
+
)}
|
|
432
|
+
</div>
|
|
353
433
|
</div>
|
|
434
|
+
{gsapRuntimeValues && (
|
|
435
|
+
<div className="mt-3 border-t border-neutral-800/40 pt-3">
|
|
436
|
+
<div className="mb-2 text-[10px] font-medium uppercase tracking-wider text-neutral-600">
|
|
437
|
+
3D Transform
|
|
438
|
+
</div>
|
|
439
|
+
<div className={RESPONSIVE_GRID}>
|
|
440
|
+
<div className="flex items-center gap-1">
|
|
441
|
+
<div className="flex-1">
|
|
442
|
+
<MetricField
|
|
443
|
+
label="Z"
|
|
444
|
+
value={formatPxMetricValue(gsapRuntimeValues.z ?? 0)}
|
|
445
|
+
scrub
|
|
446
|
+
onCommit={(next) => {
|
|
447
|
+
const v = parsePxMetricValue(next);
|
|
448
|
+
if (v != null && onCommitAnimatedProperty) {
|
|
449
|
+
void onCommitAnimatedProperty(element, "z", v);
|
|
450
|
+
}
|
|
451
|
+
}}
|
|
452
|
+
/>
|
|
453
|
+
</div>
|
|
454
|
+
{STUDIO_KEYFRAMES_ENABLED && (gsapAnimId || onCommitAnimatedProperty) && (
|
|
455
|
+
<KeyframeNavigation
|
|
456
|
+
property="z"
|
|
457
|
+
keyframes={gsapKeyframes}
|
|
458
|
+
currentPercentage={currentPct}
|
|
459
|
+
onSeek={(pct) => onSeekToTime?.(elStart + (pct / 100) * elDuration)}
|
|
460
|
+
onAddKeyframe={() => {
|
|
461
|
+
if (onCommitAnimatedProperty) {
|
|
462
|
+
void onCommitAnimatedProperty(element, "z", gsapRuntimeValues?.z ?? 0);
|
|
463
|
+
}
|
|
464
|
+
}}
|
|
465
|
+
onRemoveKeyframe={(pct) => gsapAnimId && onRemoveKeyframe?.(gsapAnimId, pct)}
|
|
466
|
+
onConvertToKeyframes={() => gsapAnimId && onConvertToKeyframes?.(gsapAnimId)}
|
|
467
|
+
/>
|
|
468
|
+
)}
|
|
469
|
+
</div>
|
|
470
|
+
<MetricField
|
|
471
|
+
label="Scale"
|
|
472
|
+
value={String(gsapRuntimeValues.scale ?? 1)}
|
|
473
|
+
scrub
|
|
474
|
+
onCommit={(next) => {
|
|
475
|
+
const v = Number.parseFloat(next);
|
|
476
|
+
if (Number.isFinite(v) && onCommitAnimatedProperty) {
|
|
477
|
+
void onCommitAnimatedProperty(element, "scale", v);
|
|
478
|
+
}
|
|
479
|
+
}}
|
|
480
|
+
/>
|
|
481
|
+
<MetricField
|
|
482
|
+
label="RotX"
|
|
483
|
+
value={`${gsapRuntimeValues.rotationX ?? 0}°`}
|
|
484
|
+
onCommit={(next) => {
|
|
485
|
+
const v = Number.parseFloat(next.replace("°", ""));
|
|
486
|
+
if (Number.isFinite(v) && onCommitAnimatedProperty) {
|
|
487
|
+
void onCommitAnimatedProperty(element, "rotationX", v);
|
|
488
|
+
}
|
|
489
|
+
}}
|
|
490
|
+
/>
|
|
491
|
+
<MetricField
|
|
492
|
+
label="RotY"
|
|
493
|
+
value={`${gsapRuntimeValues.rotationY ?? 0}°`}
|
|
494
|
+
onCommit={(next) => {
|
|
495
|
+
const v = Number.parseFloat(next.replace("°", ""));
|
|
496
|
+
if (Number.isFinite(v) && onCommitAnimatedProperty) {
|
|
497
|
+
void onCommitAnimatedProperty(element, "rotationY", v);
|
|
498
|
+
}
|
|
499
|
+
}}
|
|
500
|
+
/>
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
503
|
+
)}
|
|
354
504
|
<div className="mt-3">
|
|
505
|
+
<div className="mb-2 text-[10px] font-medium uppercase tracking-wider text-neutral-600">
|
|
506
|
+
Stacking
|
|
507
|
+
</div>
|
|
355
508
|
<MetricField
|
|
356
509
|
label="Z-index"
|
|
357
510
|
value={String(parseInt(styles["z-index"] || "auto", 10) || 0)}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// fallow-ignore-file unused-file
|
|
2
|
+
import { memo, useRef, type RefObject } from "react";
|
|
3
|
+
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
4
|
+
import type { SnapGuide, SpacingGuide } from "./snapEngine";
|
|
5
|
+
|
|
6
|
+
export interface SnapGuidesState {
|
|
7
|
+
guides: SnapGuide[];
|
|
8
|
+
spacingGuides: SpacingGuide[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const MAX_GUIDES = 6;
|
|
12
|
+
const MAX_SPACING_GUIDES = 4;
|
|
13
|
+
|
|
14
|
+
const GUIDE_COLOR = "rgba(255, 68, 204, 0.85)";
|
|
15
|
+
const SPACING_COLOR = "rgba(255, 68, 204, 0.6)";
|
|
16
|
+
const SPACING_BG = "rgba(255, 68, 204, 0.15)";
|
|
17
|
+
|
|
18
|
+
interface SnapGuideOverlayProps {
|
|
19
|
+
snapGuidesRef: RefObject<SnapGuidesState | null>;
|
|
20
|
+
overlayWidth: number;
|
|
21
|
+
overlayHeight: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const SnapGuideOverlay = memo(function SnapGuideOverlay({
|
|
25
|
+
snapGuidesRef,
|
|
26
|
+
overlayWidth,
|
|
27
|
+
overlayHeight,
|
|
28
|
+
}: SnapGuideOverlayProps) {
|
|
29
|
+
const guideElsRef = useRef<(HTMLDivElement | null)[]>([]);
|
|
30
|
+
const spacingElsRef = useRef<(HTMLDivElement | null)[]>([]);
|
|
31
|
+
const spacingLabelElsRef = useRef<(HTMLSpanElement | null)[]>([]);
|
|
32
|
+
const overlayWidthRef = useRef(overlayWidth);
|
|
33
|
+
overlayWidthRef.current = overlayWidth;
|
|
34
|
+
const overlayHeightRef = useRef(overlayHeight);
|
|
35
|
+
overlayHeightRef.current = overlayHeight;
|
|
36
|
+
|
|
37
|
+
useMountEffect(() => {
|
|
38
|
+
let frame = 0;
|
|
39
|
+
|
|
40
|
+
// fallow-ignore-next-line complexity
|
|
41
|
+
const update = () => {
|
|
42
|
+
frame = requestAnimationFrame(update);
|
|
43
|
+
|
|
44
|
+
const state = snapGuidesRef.current;
|
|
45
|
+
const guides = state?.guides ?? [];
|
|
46
|
+
const spacingGuides = state?.spacingGuides ?? [];
|
|
47
|
+
const w = overlayWidthRef.current;
|
|
48
|
+
const h = overlayHeightRef.current;
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < MAX_GUIDES; i++) {
|
|
51
|
+
const el = guideElsRef.current[i];
|
|
52
|
+
if (!el) continue;
|
|
53
|
+
|
|
54
|
+
const guide = guides[i];
|
|
55
|
+
if (!guide) {
|
|
56
|
+
el.style.display = "none";
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
el.style.display = "";
|
|
61
|
+
if (guide.axis === "x") {
|
|
62
|
+
el.style.left = `${guide.position}px`;
|
|
63
|
+
el.style.top = "0";
|
|
64
|
+
el.style.width = "1px";
|
|
65
|
+
el.style.height = `${h}px`;
|
|
66
|
+
} else {
|
|
67
|
+
el.style.left = "0";
|
|
68
|
+
el.style.top = `${guide.position}px`;
|
|
69
|
+
el.style.width = `${w}px`;
|
|
70
|
+
el.style.height = "1px";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (let i = 0; i < MAX_SPACING_GUIDES; i++) {
|
|
75
|
+
const el = spacingElsRef.current[i];
|
|
76
|
+
const label = spacingLabelElsRef.current[i];
|
|
77
|
+
if (!el) continue;
|
|
78
|
+
|
|
79
|
+
const sg = spacingGuides[i];
|
|
80
|
+
if (!sg) {
|
|
81
|
+
el.style.display = "none";
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
el.style.display = "flex";
|
|
86
|
+
el.style.alignItems = "center";
|
|
87
|
+
el.style.justifyContent = "center";
|
|
88
|
+
if (sg.axis === "x") {
|
|
89
|
+
el.style.left = `${sg.position}px`;
|
|
90
|
+
el.style.top = `${sg.from}px`;
|
|
91
|
+
el.style.width = `${sg.size}px`;
|
|
92
|
+
el.style.height = `${sg.to - sg.from}px`;
|
|
93
|
+
el.style.borderLeft = `1px dashed ${SPACING_COLOR}`;
|
|
94
|
+
el.style.borderRight = `1px dashed ${SPACING_COLOR}`;
|
|
95
|
+
el.style.borderTop = "none";
|
|
96
|
+
el.style.borderBottom = "none";
|
|
97
|
+
} else {
|
|
98
|
+
el.style.left = `${sg.from}px`;
|
|
99
|
+
el.style.top = `${sg.position}px`;
|
|
100
|
+
el.style.width = `${sg.to - sg.from}px`;
|
|
101
|
+
el.style.height = `${sg.size}px`;
|
|
102
|
+
el.style.borderTop = `1px dashed ${SPACING_COLOR}`;
|
|
103
|
+
el.style.borderBottom = `1px dashed ${SPACING_COLOR}`;
|
|
104
|
+
el.style.borderLeft = "none";
|
|
105
|
+
el.style.borderRight = "none";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (label) {
|
|
109
|
+
label.textContent = `${Math.round(sg.size)}`;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
frame = requestAnimationFrame(update);
|
|
115
|
+
return () => cancelAnimationFrame(frame);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div aria-hidden="true" className="pointer-events-none absolute inset-0">
|
|
120
|
+
{Array.from({ length: MAX_GUIDES }, (_, i) => (
|
|
121
|
+
<div
|
|
122
|
+
key={`guide-${i}`}
|
|
123
|
+
ref={(el) => {
|
|
124
|
+
guideElsRef.current[i] = el;
|
|
125
|
+
}}
|
|
126
|
+
style={{
|
|
127
|
+
display: "none",
|
|
128
|
+
position: "absolute",
|
|
129
|
+
backgroundColor: GUIDE_COLOR,
|
|
130
|
+
zIndex: 50,
|
|
131
|
+
}}
|
|
132
|
+
/>
|
|
133
|
+
))}
|
|
134
|
+
|
|
135
|
+
{Array.from({ length: MAX_SPACING_GUIDES }, (_, i) => (
|
|
136
|
+
<div
|
|
137
|
+
key={`spacing-${i}`}
|
|
138
|
+
ref={(el) => {
|
|
139
|
+
spacingElsRef.current[i] = el;
|
|
140
|
+
}}
|
|
141
|
+
style={{
|
|
142
|
+
display: "none",
|
|
143
|
+
position: "absolute",
|
|
144
|
+
zIndex: 50,
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
<span
|
|
148
|
+
ref={(el) => {
|
|
149
|
+
spacingLabelElsRef.current[i] = el;
|
|
150
|
+
}}
|
|
151
|
+
style={{
|
|
152
|
+
fontSize: "10px",
|
|
153
|
+
fontFamily: "monospace",
|
|
154
|
+
color: GUIDE_COLOR,
|
|
155
|
+
backgroundColor: SPACING_BG,
|
|
156
|
+
padding: "0 3px",
|
|
157
|
+
borderRadius: "2px",
|
|
158
|
+
lineHeight: "14px",
|
|
159
|
+
whiteSpace: "nowrap",
|
|
160
|
+
}}
|
|
161
|
+
/>
|
|
162
|
+
</div>
|
|
163
|
+
))}
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
});
|