@hyperframes/studio 0.6.58 → 0.6.60
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-B5EnhVCT.css +1 -0
- package/dist/assets/index-DG5-N9Mj.js +139 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +63 -127
- package/src/components/AskAgentModal.tsx +14 -2
- package/src/components/StudioRightPanel.tsx +1 -0
- package/src/components/editor/AnimationCard.tsx +60 -53
- package/src/components/editor/EaseCurveSection.tsx +5 -2
- package/src/components/editor/PropertyPanel.tsx +3 -1
- package/src/components/editor/domEditingAgentPrompt.ts +14 -0
- package/src/components/editor/domEditingElement.ts +34 -15
- package/src/components/editor/gsapAnimationConstants.ts +3 -1
- package/src/components/editor/gsapAnimationHelpers.test.ts +67 -0
- package/src/components/editor/gsapAnimationHelpers.ts +36 -0
- package/src/components/editor/propertyPanelPrimitives.tsx +1 -1
- package/src/components/nle/NLELayout.tsx +2 -1
- package/src/hooks/useDomEditSession.ts +18 -10
- package/src/hooks/useDomSelection.ts +35 -1
- package/src/hooks/useFileManager.ts +4 -5
- package/src/hooks/useGsapScriptCommits.ts +3 -0
- package/src/hooks/usePreviewInteraction.ts +67 -3
- package/src/hooks/useStudioContextValue.ts +138 -0
- package/src/utils/studioPreviewHelpers.ts +38 -0
- package/dist/assets/index-B1XH-ptc.js +0 -138
- package/dist/assets/index-DH9QNjuX.css +0 -1
package/dist/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
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-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-DG5-N9Mj.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-B5EnhVCT.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.60",
|
|
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.60",
|
|
35
|
+
"@hyperframes/player": "0.6.60"
|
|
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.60"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"react": "19",
|
package/src/App.tsx
CHANGED
|
@@ -26,10 +26,11 @@ import { useCompositionDimensions } from "./hooks/useCompositionDimensions";
|
|
|
26
26
|
import { useToast } from "./hooks/useToast";
|
|
27
27
|
import { useStudioUrlState } from "./hooks/useStudioUrlState";
|
|
28
28
|
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
buildStudioContextValue,
|
|
30
|
+
useDragOverlay,
|
|
31
|
+
useInspectorState,
|
|
32
|
+
} from "./hooks/useStudioContextValue";
|
|
33
|
+
import { buildAgentContextPreview } from "./components/editor/domEditingAgentPrompt";
|
|
33
34
|
import type { DomEditSelection } from "./components/editor/domEditing";
|
|
34
35
|
import { AskAgentModal } from "./components/AskAgentModal";
|
|
35
36
|
import { StudioGlobalDragOverlay } from "./components/StudioGlobalDragOverlay";
|
|
@@ -38,7 +39,7 @@ import { StudioLeftSidebar } from "./components/StudioLeftSidebar";
|
|
|
38
39
|
import { StudioPreviewArea } from "./components/StudioPreviewArea";
|
|
39
40
|
import { StudioRightPanel } from "./components/StudioRightPanel";
|
|
40
41
|
import { TimelineToolbar } from "./components/TimelineToolbar";
|
|
41
|
-
import { StudioProvider
|
|
42
|
+
import { StudioProvider } from "./contexts/StudioContext";
|
|
42
43
|
import { PanelLayoutProvider } from "./contexts/PanelLayoutContext";
|
|
43
44
|
import { FileManagerProvider } from "./contexts/FileManagerContext";
|
|
44
45
|
import { DomEditProvider } from "./contexts/DomEditContext";
|
|
@@ -51,6 +52,7 @@ import {
|
|
|
51
52
|
import { trackStudioSessionStart } from "./telemetry/events";
|
|
52
53
|
import { hasFiredSessionStart, markSessionStartFired } from "./telemetry/config";
|
|
53
54
|
|
|
55
|
+
// fallow-ignore-next-line complexity
|
|
54
56
|
export function StudioApp() {
|
|
55
57
|
const { projectId, resolving, waitingForServer } = useServerConnection();
|
|
56
58
|
const initialUrlStateRef = useRef(readStudioUrlStateFromWindow());
|
|
@@ -184,6 +186,26 @@ export function StudioApp() {
|
|
|
184
186
|
uploadProjectFiles: fileManager.uploadProjectFiles,
|
|
185
187
|
});
|
|
186
188
|
|
|
189
|
+
const blockCtx = useMemo(
|
|
190
|
+
() => ({
|
|
191
|
+
activeCompPath,
|
|
192
|
+
timelineElements,
|
|
193
|
+
readProjectFile: fileManager.readProjectFile,
|
|
194
|
+
writeProjectFile: fileManager.writeProjectFile,
|
|
195
|
+
recordEdit: editHistory.recordEdit,
|
|
196
|
+
refreshFileTree: fileManager.refreshFileTree,
|
|
197
|
+
reloadPreview,
|
|
198
|
+
showToast,
|
|
199
|
+
}),
|
|
200
|
+
[
|
|
201
|
+
activeCompPath,
|
|
202
|
+
timelineElements,
|
|
203
|
+
fileManager,
|
|
204
|
+
editHistory.recordEdit,
|
|
205
|
+
reloadPreview,
|
|
206
|
+
showToast,
|
|
207
|
+
],
|
|
208
|
+
);
|
|
187
209
|
const handleAddBlock = useCallback(
|
|
188
210
|
(blockName: string) => {
|
|
189
211
|
if (!projectId) return;
|
|
@@ -191,16 +213,9 @@ export function StudioApp() {
|
|
|
191
213
|
const result = await addBlockToProject({
|
|
192
214
|
projectId,
|
|
193
215
|
blockName,
|
|
194
|
-
|
|
216
|
+
...blockCtx,
|
|
195
217
|
previewIframe: previewIframeRef.current,
|
|
196
218
|
currentTime: usePlayerStore.getState().currentTime,
|
|
197
|
-
timelineElements,
|
|
198
|
-
readProjectFile: fileManager.readProjectFile,
|
|
199
|
-
writeProjectFile: fileManager.writeProjectFile,
|
|
200
|
-
recordEdit: editHistory.recordEdit,
|
|
201
|
-
refreshFileTree: fileManager.refreshFileTree,
|
|
202
|
-
reloadPreview,
|
|
203
|
-
showToast,
|
|
204
219
|
});
|
|
205
220
|
const params = result?.block.type === "hyperframes:block" ? result.block.params : undefined;
|
|
206
221
|
if (params?.length) {
|
|
@@ -215,82 +230,35 @@ export function StudioApp() {
|
|
|
215
230
|
}
|
|
216
231
|
})();
|
|
217
232
|
},
|
|
218
|
-
[
|
|
219
|
-
projectId,
|
|
220
|
-
activeCompPath,
|
|
221
|
-
timelineElements,
|
|
222
|
-
fileManager.readProjectFile,
|
|
223
|
-
fileManager.writeProjectFile,
|
|
224
|
-
fileManager.refreshFileTree,
|
|
225
|
-
editHistory.recordEdit,
|
|
226
|
-
reloadPreview,
|
|
227
|
-
showToast,
|
|
228
|
-
panelLayout,
|
|
229
|
-
],
|
|
233
|
+
[projectId, blockCtx, panelLayout],
|
|
230
234
|
);
|
|
231
|
-
|
|
232
235
|
const handleTimelineBlockDrop = useCallback(
|
|
233
236
|
(blockName: string, placement: { start: number; track: number }) => {
|
|
234
237
|
if (!projectId) return;
|
|
235
238
|
void addBlockToProject({
|
|
236
239
|
projectId,
|
|
237
240
|
blockName,
|
|
238
|
-
activeCompPath,
|
|
239
241
|
placement,
|
|
242
|
+
...blockCtx,
|
|
240
243
|
previewIframe: previewIframeRef.current,
|
|
241
244
|
currentTime: usePlayerStore.getState().currentTime,
|
|
242
|
-
timelineElements,
|
|
243
|
-
readProjectFile: fileManager.readProjectFile,
|
|
244
|
-
writeProjectFile: fileManager.writeProjectFile,
|
|
245
|
-
recordEdit: editHistory.recordEdit,
|
|
246
|
-
refreshFileTree: fileManager.refreshFileTree,
|
|
247
|
-
reloadPreview,
|
|
248
|
-
showToast,
|
|
249
245
|
});
|
|
250
246
|
},
|
|
251
|
-
[
|
|
252
|
-
projectId,
|
|
253
|
-
activeCompPath,
|
|
254
|
-
timelineElements,
|
|
255
|
-
fileManager.readProjectFile,
|
|
256
|
-
fileManager.writeProjectFile,
|
|
257
|
-
fileManager.refreshFileTree,
|
|
258
|
-
editHistory.recordEdit,
|
|
259
|
-
reloadPreview,
|
|
260
|
-
showToast,
|
|
261
|
-
],
|
|
247
|
+
[projectId, blockCtx],
|
|
262
248
|
);
|
|
263
|
-
|
|
264
249
|
const handlePreviewBlockDrop = useCallback(
|
|
265
250
|
(blockName: string, position: { left: number; top: number }) => {
|
|
266
251
|
if (!projectId) return;
|
|
267
252
|
void addBlockToProject({
|
|
268
253
|
projectId,
|
|
269
254
|
blockName,
|
|
270
|
-
activeCompPath,
|
|
271
255
|
visualPosition: position,
|
|
256
|
+
...blockCtx,
|
|
272
257
|
previewIframe: previewIframeRef.current,
|
|
273
258
|
currentTime: usePlayerStore.getState().currentTime,
|
|
274
|
-
timelineElements,
|
|
275
|
-
readProjectFile: fileManager.readProjectFile,
|
|
276
|
-
writeProjectFile: fileManager.writeProjectFile,
|
|
277
|
-
recordEdit: editHistory.recordEdit,
|
|
278
|
-
refreshFileTree: fileManager.refreshFileTree,
|
|
279
|
-
reloadPreview,
|
|
280
|
-
showToast,
|
|
281
259
|
});
|
|
282
260
|
},
|
|
283
|
-
[
|
|
284
|
-
projectId,
|
|
285
|
-
activeCompPath,
|
|
286
|
-
timelineElements,
|
|
287
|
-
fileManager.readProjectFile,
|
|
288
|
-
fileManager.writeProjectFile,
|
|
289
|
-
fileManager.refreshFileTree,
|
|
290
|
-
editHistory.recordEdit,
|
|
291
|
-
reloadPreview,
|
|
292
|
-
showToast,
|
|
293
|
-
],
|
|
261
|
+
[projectId, blockCtx],
|
|
294
262
|
);
|
|
295
263
|
|
|
296
264
|
const clearDomSelectionRef = useRef<() => void>(() => {});
|
|
@@ -414,30 +382,22 @@ export function StudioApp() {
|
|
|
414
382
|
resetErrors: resetConsoleErrors,
|
|
415
383
|
} = useConsoleErrorCapture(previewIframe);
|
|
416
384
|
|
|
417
|
-
const
|
|
418
|
-
const dragCounterRef = useRef(0);
|
|
385
|
+
const dragOverlay = useDragOverlay(fileManager.handleImportFiles);
|
|
419
386
|
|
|
420
|
-
const { syncPreviewTimelineHotkey, syncPreviewHistoryHotkey } = appHotkeys;
|
|
421
387
|
const handlePreviewIframeRef = useCallback(
|
|
422
388
|
(iframe: HTMLIFrameElement | null) => {
|
|
423
389
|
previewIframeRef.current = iframe;
|
|
424
390
|
setPreviewIframe(iframe);
|
|
425
|
-
syncPreviewTimelineHotkey(iframe);
|
|
426
|
-
syncPreviewHistoryHotkey(iframe);
|
|
391
|
+
appHotkeys.syncPreviewTimelineHotkey(iframe);
|
|
392
|
+
appHotkeys.syncPreviewHistoryHotkey(iframe);
|
|
427
393
|
resetConsoleErrors();
|
|
428
394
|
refreshPreviewDocumentVersion();
|
|
429
395
|
},
|
|
430
|
-
[
|
|
431
|
-
refreshPreviewDocumentVersion,
|
|
432
|
-
resetConsoleErrors,
|
|
433
|
-
syncPreviewHistoryHotkey,
|
|
434
|
-
syncPreviewTimelineHotkey,
|
|
435
|
-
],
|
|
396
|
+
[appHotkeys, resetConsoleErrors, refreshPreviewDocumentVersion],
|
|
436
397
|
);
|
|
437
|
-
|
|
438
398
|
const handleSelectComposition = useCallback(
|
|
439
399
|
(comp: string) => {
|
|
440
|
-
setActiveCompPath(comp
|
|
400
|
+
setActiveCompPath(comp.endsWith(".html") ? comp : null);
|
|
441
401
|
fileManager.setEditingFile({ path: comp, content: null });
|
|
442
402
|
fetch(`/api/projects/${projectId}/files/${comp}`)
|
|
443
403
|
.then((r) => r.json())
|
|
@@ -447,23 +407,19 @@ export function StudioApp() {
|
|
|
447
407
|
[projectId, fileManager],
|
|
448
408
|
);
|
|
449
409
|
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
const shouldShowSelectedDomBounds =
|
|
464
|
-
inspectorPanelActive && !panelLayout.rightCollapsed && !isPlaying;
|
|
465
|
-
const inspectorButtonActive =
|
|
466
|
-
STUDIO_INSPECTOR_PANELS_ENABLED && !panelLayout.rightCollapsed && inspectorPanelActive;
|
|
410
|
+
const {
|
|
411
|
+
selectedStudioMotion,
|
|
412
|
+
designPanelActive,
|
|
413
|
+
motionPanelActive,
|
|
414
|
+
inspectorPanelActive,
|
|
415
|
+
inspectorButtonActive,
|
|
416
|
+
shouldShowSelectedDomBounds,
|
|
417
|
+
} = useInspectorState(
|
|
418
|
+
panelLayout.rightPanelTab,
|
|
419
|
+
panelLayout.rightCollapsed,
|
|
420
|
+
isPlaying,
|
|
421
|
+
domEditSession.domEditSelection,
|
|
422
|
+
);
|
|
467
423
|
|
|
468
424
|
useStudioUrlState({
|
|
469
425
|
projectId,
|
|
@@ -484,8 +440,7 @@ export function StudioApp() {
|
|
|
484
440
|
initialState: initialUrlStateRef.current,
|
|
485
441
|
});
|
|
486
442
|
|
|
487
|
-
|
|
488
|
-
const studioCtxValue: StudioContextValue = {
|
|
443
|
+
const studioCtxValue = buildStudioContextValue({
|
|
489
444
|
projectId: projectId!,
|
|
490
445
|
activeCompPath,
|
|
491
446
|
setActiveCompPath,
|
|
@@ -498,12 +453,7 @@ export function StudioApp() {
|
|
|
498
453
|
currentTime,
|
|
499
454
|
timelineElements,
|
|
500
455
|
isPlaying,
|
|
501
|
-
editHistory
|
|
502
|
-
canUndo: editHistory.canUndo,
|
|
503
|
-
canRedo: editHistory.canRedo,
|
|
504
|
-
undoLabel: editHistory.undoLabel,
|
|
505
|
-
redoLabel: editHistory.redoLabel,
|
|
506
|
-
},
|
|
456
|
+
editHistory,
|
|
507
457
|
handleUndo: appHotkeys.handleUndo,
|
|
508
458
|
handleRedo: appHotkeys.handleRedo,
|
|
509
459
|
renderQueue: {
|
|
@@ -519,7 +469,7 @@ export function StudioApp() {
|
|
|
519
469
|
refreshPreviewDocumentVersion,
|
|
520
470
|
timelineVisible,
|
|
521
471
|
toggleTimelineVisibility,
|
|
522
|
-
};
|
|
472
|
+
});
|
|
523
473
|
|
|
524
474
|
if (resolving || waitingForServer || !projectId) {
|
|
525
475
|
return <StudioSplash waiting={waitingForServer} />;
|
|
@@ -533,28 +483,10 @@ export function StudioApp() {
|
|
|
533
483
|
<DomEditProvider value={domEditSession}>
|
|
534
484
|
<div
|
|
535
485
|
className="flex flex-col h-full w-full bg-neutral-950 relative"
|
|
536
|
-
onDragOver={
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
}
|
|
540
|
-
onDragEnter={(e) => {
|
|
541
|
-
if (!e.dataTransfer.types.includes("Files")) return;
|
|
542
|
-
e.preventDefault();
|
|
543
|
-
dragCounterRef.current++;
|
|
544
|
-
setGlobalDragOver(true);
|
|
545
|
-
}}
|
|
546
|
-
onDragLeave={() => {
|
|
547
|
-
dragCounterRef.current--;
|
|
548
|
-
if (dragCounterRef.current === 0) setGlobalDragOver(false);
|
|
549
|
-
}}
|
|
550
|
-
onDrop={(e) => {
|
|
551
|
-
dragCounterRef.current = 0;
|
|
552
|
-
setGlobalDragOver(false);
|
|
553
|
-
if (e.defaultPrevented) return;
|
|
554
|
-
e.preventDefault();
|
|
555
|
-
if (e.dataTransfer.files.length)
|
|
556
|
-
fileManager.handleImportFiles(e.dataTransfer.files);
|
|
557
|
-
}}
|
|
486
|
+
onDragOver={dragOverlay.onDragOver}
|
|
487
|
+
onDragEnter={dragOverlay.onDragEnter}
|
|
488
|
+
onDragLeave={dragOverlay.onDragLeave}
|
|
489
|
+
onDrop={dragOverlay.onDrop}
|
|
558
490
|
>
|
|
559
491
|
<StudioHeader
|
|
560
492
|
captureFrameHref={frameCapture.captureFrameHref}
|
|
@@ -620,6 +552,10 @@ export function StudioApp() {
|
|
|
620
552
|
{domEditSession.agentModalOpen && domEditSession.domEditSelection && (
|
|
621
553
|
<AskAgentModal
|
|
622
554
|
selectionLabel={domEditSession.domEditSelection.label}
|
|
555
|
+
contextPreview={buildAgentContextPreview(
|
|
556
|
+
domEditSession.domEditSelection,
|
|
557
|
+
activeCompPath,
|
|
558
|
+
)}
|
|
623
559
|
anchorPoint={domEditSession.agentModalAnchorPoint}
|
|
624
560
|
onSubmit={domEditSession.handleAgentModalSubmit}
|
|
625
561
|
onClose={() => {
|
|
@@ -630,7 +566,7 @@ export function StudioApp() {
|
|
|
630
566
|
/>
|
|
631
567
|
)}
|
|
632
568
|
|
|
633
|
-
{
|
|
569
|
+
{dragOverlay.active && <StudioGlobalDragOverlay />}
|
|
634
570
|
|
|
635
571
|
{appToast && (
|
|
636
572
|
<div
|
|
@@ -26,11 +26,13 @@ function getAgentModalPositionStyle(
|
|
|
26
26
|
|
|
27
27
|
export function AskAgentModal({
|
|
28
28
|
selectionLabel,
|
|
29
|
+
contextPreview,
|
|
29
30
|
anchorPoint = null,
|
|
30
31
|
onSubmit,
|
|
31
32
|
onClose,
|
|
32
33
|
}: {
|
|
33
34
|
selectionLabel: string;
|
|
35
|
+
contextPreview?: string;
|
|
34
36
|
anchorPoint?: AgentModalAnchorPoint | null;
|
|
35
37
|
onSubmit: (instruction: string) => void;
|
|
36
38
|
onClose: () => void;
|
|
@@ -66,7 +68,7 @@ export function AskAgentModal({
|
|
|
66
68
|
>
|
|
67
69
|
<div className="flex items-center justify-between px-5 py-4 border-b border-neutral-800/60">
|
|
68
70
|
<div>
|
|
69
|
-
<h3 className="text-sm font-medium text-neutral-200">
|
|
71
|
+
<h3 className="text-sm font-medium text-neutral-200">Copy prompt to AI agent</h3>
|
|
70
72
|
<p className="text-xs text-neutral-500 mt-0.5">
|
|
71
73
|
{selectionLabel.length > 50 ? `${selectionLabel.slice(0, 49)}…` : selectionLabel}
|
|
72
74
|
</p>
|
|
@@ -89,7 +91,7 @@ export function AskAgentModal({
|
|
|
89
91
|
</svg>
|
|
90
92
|
</button>
|
|
91
93
|
</div>
|
|
92
|
-
<div className="px-5 py-4">
|
|
94
|
+
<div className="px-5 py-4 space-y-3">
|
|
93
95
|
<textarea
|
|
94
96
|
ref={inputRef}
|
|
95
97
|
className="w-full h-24 px-3 py-2 rounded-lg border border-neutral-800 bg-neutral-900/60 text-sm text-neutral-200 placeholder-neutral-600 resize-none focus:outline-none focus:border-studio-accent/60 focus:ring-1 focus:ring-studio-accent/30"
|
|
@@ -101,6 +103,16 @@ export function AskAgentModal({
|
|
|
101
103
|
if (e.key === "Escape") onClose();
|
|
102
104
|
}}
|
|
103
105
|
/>
|
|
106
|
+
{contextPreview && (
|
|
107
|
+
<details className="group">
|
|
108
|
+
<summary className="text-[11px] text-neutral-500 cursor-pointer select-none hover:text-neutral-400">
|
|
109
|
+
Context included in prompt
|
|
110
|
+
</summary>
|
|
111
|
+
<pre className="mt-2 max-h-40 overflow-auto rounded-lg bg-neutral-900/80 px-3 py-2 text-[11px] leading-relaxed text-neutral-500 whitespace-pre-wrap break-words border border-neutral-800/50">
|
|
112
|
+
{contextPreview}
|
|
113
|
+
</pre>
|
|
114
|
+
</details>
|
|
115
|
+
)}
|
|
104
116
|
</div>
|
|
105
117
|
<div className="flex items-center justify-between px-5 py-3 border-t border-neutral-800/60">
|
|
106
118
|
<span className="text-[11px] text-neutral-600">
|
|
@@ -8,23 +8,49 @@ import {
|
|
|
8
8
|
EASE_LABELS,
|
|
9
9
|
METHOD_LABELS,
|
|
10
10
|
METHOD_TOOLTIPS,
|
|
11
|
+
PERCENT_PROPS,
|
|
11
12
|
PROP_LABELS,
|
|
12
13
|
PROP_TOOLTIPS,
|
|
13
14
|
PROP_UNITS,
|
|
14
15
|
} from "./gsapAnimationConstants";
|
|
16
|
+
import { buildTweenSummary } from "./gsapAnimationHelpers";
|
|
15
17
|
import { EaseCurveSection } from "./EaseCurveSection";
|
|
18
|
+
const BOOLEAN_PROPS = new Set(["visibility"]);
|
|
16
19
|
|
|
17
|
-
const PERCENT_PROPS = new Set(["opacity", "autoAlpha"]);
|
|
18
20
|
function isPercentProp(prop: string): boolean {
|
|
19
21
|
return PERCENT_PROPS.has(prop);
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
function displayValue(prop: string, val: number | string): string {
|
|
23
|
-
|
|
25
|
+
if (isPercentProp(prop)) return String(Math.round(Math.max(0, Math.min(1, Number(val))) * 100));
|
|
26
|
+
return String(val);
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
function adjustedValue(prop: string, raw: string): string {
|
|
27
|
-
|
|
30
|
+
if (isPercentProp(prop)) return String(Math.max(0, Math.min(1, Number(raw) / 100)));
|
|
31
|
+
return raw;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function RemoveButton({ onClick, title }: { onClick: () => void; title: string }) {
|
|
35
|
+
return (
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
onClick={onClick}
|
|
39
|
+
className="flex-shrink-0 rounded p-0.5 text-neutral-600 transition-colors hover:bg-neutral-800 hover:text-red-400"
|
|
40
|
+
title={title}
|
|
41
|
+
>
|
|
42
|
+
<svg
|
|
43
|
+
width="12"
|
|
44
|
+
height="12"
|
|
45
|
+
viewBox="0 0 12 12"
|
|
46
|
+
fill="none"
|
|
47
|
+
stroke="currentColor"
|
|
48
|
+
strokeWidth="1.5"
|
|
49
|
+
>
|
|
50
|
+
<path d="M3 3l6 6M9 3l-6 6" />
|
|
51
|
+
</svg>
|
|
52
|
+
</button>
|
|
53
|
+
);
|
|
28
54
|
}
|
|
29
55
|
|
|
30
56
|
function PropertyRow({
|
|
@@ -40,6 +66,30 @@ function PropertyRow({
|
|
|
40
66
|
onRemove: () => void;
|
|
41
67
|
removeTitle: string;
|
|
42
68
|
}) {
|
|
69
|
+
if (BOOLEAN_PROPS.has(prop)) {
|
|
70
|
+
const isVisible = val === "visible" || val === 1;
|
|
71
|
+
return (
|
|
72
|
+
<div className="flex items-center gap-1">
|
|
73
|
+
<div className="min-w-0 flex-1 flex items-center gap-2 px-2 py-1 rounded-lg bg-neutral-900 border border-neutral-800">
|
|
74
|
+
<span className="flex-1 text-[11px] font-medium text-neutral-500">
|
|
75
|
+
{PROP_LABELS[prop] ?? prop}
|
|
76
|
+
</span>
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
onClick={() => onCommit(isVisible ? "hidden" : "visible")}
|
|
80
|
+
className={`flex-shrink-0 w-7 h-4 rounded-full transition-colors relative ${isVisible ? "bg-emerald-500/30" : "bg-neutral-700"}`}
|
|
81
|
+
title={isVisible ? "Visible — click to hide" : "Hidden — click to show"}
|
|
82
|
+
>
|
|
83
|
+
<span
|
|
84
|
+
className={`absolute top-0.5 h-3 w-3 rounded-full transition-transform ${isVisible ? "bg-emerald-400 translate-x-3.5" : "bg-neutral-500 translate-x-0.5"}`}
|
|
85
|
+
/>
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
<RemoveButton onClick={onRemove} title={removeTitle} />
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
43
93
|
return (
|
|
44
94
|
<div className="flex items-center gap-1">
|
|
45
95
|
<div className="min-w-0 flex-1">
|
|
@@ -53,23 +103,7 @@ function PropertyRow({
|
|
|
53
103
|
onCommit={(raw) => onCommit(adjustedValue(prop, raw))}
|
|
54
104
|
/>
|
|
55
105
|
</div>
|
|
56
|
-
<
|
|
57
|
-
type="button"
|
|
58
|
-
onClick={onRemove}
|
|
59
|
-
className="flex-shrink-0 rounded p-0.5 text-neutral-600 transition-colors hover:bg-neutral-800 hover:text-red-400"
|
|
60
|
-
title={removeTitle}
|
|
61
|
-
>
|
|
62
|
-
<svg
|
|
63
|
-
width="12"
|
|
64
|
-
height="12"
|
|
65
|
-
viewBox="0 0 12 12"
|
|
66
|
-
fill="none"
|
|
67
|
-
stroke="currentColor"
|
|
68
|
-
strokeWidth="1.5"
|
|
69
|
-
>
|
|
70
|
-
<path d="M3 3l6 6M9 3l-6 6" />
|
|
71
|
-
</svg>
|
|
72
|
-
</button>
|
|
106
|
+
<RemoveButton onClick={onRemove} title={removeTitle} />
|
|
73
107
|
</div>
|
|
74
108
|
);
|
|
75
109
|
}
|
|
@@ -124,36 +158,6 @@ function AddPropertyTrigger({
|
|
|
124
158
|
);
|
|
125
159
|
}
|
|
126
160
|
|
|
127
|
-
// fallow-ignore-next-line complexity
|
|
128
|
-
function buildTweenSummary(animation: GsapAnimation): string {
|
|
129
|
-
const easeName = animation.ease ?? "none";
|
|
130
|
-
const ease = EASE_LABELS[easeName] ?? easeName;
|
|
131
|
-
const props = Object.entries(animation.properties);
|
|
132
|
-
const target = animation.targetSelector;
|
|
133
|
-
const dur = animation.duration ?? 0;
|
|
134
|
-
const pos = animation.position;
|
|
135
|
-
const propDescs = props.map(([p, v]) => {
|
|
136
|
-
const label = (PROP_LABELS[p] ?? p).toLowerCase();
|
|
137
|
-
const unit = PROP_UNITS[p] ?? "";
|
|
138
|
-
return `${label} to ${v}${unit}`;
|
|
139
|
-
});
|
|
140
|
-
const propText = propDescs.length > 0 ? propDescs.join(", ") : "no properties yet";
|
|
141
|
-
if (animation.method === "set") return `At ${pos}s, instantly set ${target}'s ${propText}.`;
|
|
142
|
-
if (animation.method === "from")
|
|
143
|
-
return `Starting at ${pos}s, over ${dur}s, ${target} enters from ${propText} using a ${ease.toLowerCase()} curve.`;
|
|
144
|
-
if (animation.method === "fromTo") {
|
|
145
|
-
const fromProps = Object.entries(animation.fromProperties ?? {});
|
|
146
|
-
const fromDescs = fromProps.map(([p, v]) => {
|
|
147
|
-
const label = (PROP_LABELS[p] ?? p).toLowerCase();
|
|
148
|
-
const unit = PROP_UNITS[p] ?? "";
|
|
149
|
-
return `${label} ${v}${unit}`;
|
|
150
|
-
});
|
|
151
|
-
const fromText = fromDescs.length > 0 ? fromDescs.join(", ") : "—";
|
|
152
|
-
return `Starting at ${pos}s, over ${dur}s, ${target} animates from [${fromText}] to [${propText}] using a ${ease.toLowerCase()} curve.`;
|
|
153
|
-
}
|
|
154
|
-
return `Starting at ${pos}s, over ${dur}s, animate ${target}'s ${propText} using a ${ease.toLowerCase()} curve.`;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
161
|
function parseNumericOrString(raw: string): number | string {
|
|
158
162
|
const num = Number(raw);
|
|
159
163
|
return Number.isFinite(num) ? num : raw;
|
|
@@ -201,8 +205,11 @@ export const AnimationCard = memo(function AnimationCard({
|
|
|
201
205
|
[animation.properties],
|
|
202
206
|
);
|
|
203
207
|
const availableProps = useMemo(
|
|
204
|
-
() =>
|
|
205
|
-
|
|
208
|
+
() =>
|
|
209
|
+
SUPPORTED_PROPS.filter(
|
|
210
|
+
(p) => !usedProps.has(p) && (animation.method === "set" || !BOOLEAN_PROPS.has(p)),
|
|
211
|
+
),
|
|
212
|
+
[usedProps, animation.method],
|
|
206
213
|
);
|
|
207
214
|
|
|
208
215
|
const usedFromProps = useMemo(
|
|
@@ -210,7 +217,7 @@ export const AnimationCard = memo(function AnimationCard({
|
|
|
210
217
|
[animation.fromProperties],
|
|
211
218
|
);
|
|
212
219
|
const availableFromProps = useMemo(
|
|
213
|
-
() => SUPPORTED_PROPS.filter((p) => !usedFromProps.has(p)),
|
|
220
|
+
() => SUPPORTED_PROPS.filter((p) => !usedFromProps.has(p) && !BOOLEAN_PROPS.has(p)),
|
|
214
221
|
[usedFromProps],
|
|
215
222
|
);
|
|
216
223
|
|
|
@@ -118,11 +118,14 @@ export function EaseCurveSection({
|
|
|
118
118
|
{progress !== null ? "Playing…" : "Preview"}
|
|
119
119
|
</button>
|
|
120
120
|
</div>
|
|
121
|
-
<div
|
|
121
|
+
<div
|
|
122
|
+
className="overflow-hidden rounded pt-[72px] -mt-[72px]"
|
|
123
|
+
style={{ aspectRatio: `${w}/${h}` }}
|
|
124
|
+
>
|
|
122
125
|
<svg
|
|
123
126
|
ref={svgRef}
|
|
124
127
|
width="100%"
|
|
125
|
-
height=
|
|
128
|
+
height="100%"
|
|
126
129
|
viewBox={`0 0 ${w} ${h}`}
|
|
127
130
|
preserveAspectRatio="none"
|
|
128
131
|
style={{ overflow: "visible" }}
|
|
@@ -135,6 +135,7 @@ function TimingSection({
|
|
|
135
135
|
/* PropertyPanel */
|
|
136
136
|
/* ------------------------------------------------------------------ */
|
|
137
137
|
|
|
138
|
+
// fallow-ignore-next-line complexity
|
|
138
139
|
export const PropertyPanel = memo(function PropertyPanel({
|
|
139
140
|
projectId,
|
|
140
141
|
projectDir,
|
|
@@ -229,6 +230,7 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
229
230
|
});
|
|
230
231
|
};
|
|
231
232
|
|
|
233
|
+
// fallow-ignore-next-line complexity
|
|
232
234
|
const commitManualSize = (axis: "width" | "height", nextValue: string) => {
|
|
233
235
|
const parsed = parsePxMetricValue(nextValue);
|
|
234
236
|
if (parsed == null || parsed <= 0) return;
|
|
@@ -281,7 +283,7 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
281
283
|
className="inline-flex h-8 items-center justify-center gap-2 rounded-xl border border-neutral-700 bg-neutral-950 px-3.5 text-[11px] font-medium text-neutral-100 transition-colors hover:border-studio-accent/40 hover:text-studio-accent"
|
|
282
284
|
>
|
|
283
285
|
<MessageSquare size={15} />
|
|
284
|
-
<span>{copiedAgentPrompt ? "Prompt copied" : "
|
|
286
|
+
<span>{copiedAgentPrompt ? "Prompt copied" : "Copy prompt to AI agent"}</span>
|
|
285
287
|
</button>
|
|
286
288
|
</div>
|
|
287
289
|
</div>
|
|
@@ -95,3 +95,17 @@ export function buildElementAgentPrompt({
|
|
|
95
95
|
|
|
96
96
|
return lines.join("\n");
|
|
97
97
|
}
|
|
98
|
+
|
|
99
|
+
export function buildAgentContextPreview(
|
|
100
|
+
selection: DomEditSelection,
|
|
101
|
+
activeCompPath: string | null,
|
|
102
|
+
): string {
|
|
103
|
+
return [
|
|
104
|
+
`Composition: ${selection.compositionPath}`,
|
|
105
|
+
`Source: ${selection.sourceFile || activeCompPath || "index.html"}`,
|
|
106
|
+
`Selector: ${selection.selector ?? "(none)"} Tag: <${selection.tagName}>`,
|
|
107
|
+
selection.textContent ? `Text: ${selection.textContent}` : "",
|
|
108
|
+
]
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
.join("\n");
|
|
111
|
+
}
|