@hyperframes/studio 0.8.104 → 0.8.106
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/{hyperframes-player-DAoaajuM.js → hyperframes-player-dl6K6ABp.js} +1 -1
- package/dist/assets/index-CMiYVGv5.js +399 -0
- package/dist/assets/{index-WV9LroAD.js → index-DKT5NF1Y.js} +1 -1
- package/dist/assets/index-lrT-gfHa.css +1 -0
- package/dist/assets/{index-BDK4rTMb.js → index-ooaZZhKf.js} +1 -1
- package/dist/{chunk-62DDWOSH.js → chunk-EEXSYY5F.js} +19 -18
- package/dist/chunk-EEXSYY5F.js.map +1 -0
- package/dist/{chunk-HIGHVWYD.js → chunk-ICTZJPQQ.js} +3 -2
- package/dist/chunk-ICTZJPQQ.js.map +1 -0
- package/dist/{chunk-6GAO75IU.js → chunk-NCUBAVGK.js} +10 -8
- package/dist/chunk-NCUBAVGK.js.map +1 -0
- package/dist/index.css +6 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +15 -4
- package/dist/index.html +11 -2
- package/dist/index.js +10391 -9836
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/ui.js +2 -2
- package/package.json +9 -9
- package/src/captions/components/CaptionAnimationPanel.tsx +2 -2
- package/src/captions/components/CaptionPropertyPanel.tsx +2 -2
- package/src/captions/components/CaptionTimeline.tsx +3 -3
- package/src/captions/keyboard.test.ts +6 -0
- package/src/captions/keyboard.ts +2 -1
- package/src/components/AskAgentModal.tsx +1 -1
- package/src/components/LintModal.tsx +35 -35
- package/src/components/StudioErrorBoundary.tsx +1 -1
- package/src/components/StudioHeader.dom.test.tsx +35 -13
- package/src/components/StudioHeader.tsx +6 -2
- package/src/components/StudioToast.tsx +7 -16
- package/src/components/ThemeToggle.tsx +99 -0
- package/src/components/TimelineToolPicker.tsx +1 -1
- package/src/components/TimelineToolbar.tsx +11 -11
- package/src/components/dock/Dock.tsx +1 -1
- package/src/components/dock/dock.css +9 -1
- package/src/components/editor/AnimationCard.tsx +5 -5
- package/src/components/editor/AnimationCardParts.tsx +1 -1
- package/src/components/editor/BlockParamsPanel.tsx +3 -3
- package/src/components/editor/BorderRadiusEditor.tsx +8 -15
- package/src/components/editor/CanvasContextMenu.tsx +1 -1
- package/src/components/editor/ComputedTweenNotice.tsx +1 -1
- package/src/components/editor/DomEditOverlay.tsx +37 -5
- package/src/components/editor/DomEditRotateHandle.tsx +1 -1
- package/src/components/editor/EaseCurveSection.tsx +14 -19
- package/src/components/editor/EaseModeControls.tsx +2 -2
- package/src/components/editor/EaseParamFields.tsx +8 -8
- package/src/components/editor/FileTree.tsx +1 -1
- package/src/components/editor/FileTreeIcons.tsx +47 -26
- package/src/components/editor/FileTreeNodes.tsx +11 -8
- package/src/components/editor/GestureRecordControl.tsx +3 -3
- package/src/components/editor/GsapAddAnimationControl.tsx +1 -1
- package/src/components/editor/GsapAnimationSection.tsx +2 -2
- package/src/components/editor/InlineTextToolbar.tsx +4 -4
- package/src/components/editor/InspectorHeaderActions.tsx +1 -1
- package/src/components/editor/KeyframeDiamond.tsx +6 -4
- package/src/components/editor/KeyframeNavigation.tsx +4 -4
- package/src/components/editor/LayersPanel.tsx +3 -3
- package/src/components/editor/PromotableControl.tsx +2 -2
- package/src/components/editor/PropertyPanelEmptyState.tsx +4 -4
- package/src/components/editor/PropertyPanelFlatFooter.tsx +2 -2
- package/src/components/editor/PropertyPanelFlatHeader.test.tsx +1 -1
- package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
- package/src/components/editor/SnapToolbar.tsx +5 -5
- package/src/components/editor/SourceEditor.tsx +2 -6
- package/src/components/editor/TimelineFxPopover.tsx +4 -4
- package/src/components/editor/Transform3DCube.tsx +1 -1
- package/src/components/editor/anchoredResizeReleaseShift.test.ts +0 -84
- package/src/components/editor/domEditOverlayStartGesture.ts +127 -115
- package/src/components/editor/easeCurveSvg.tsx +1 -1
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +275 -0
- package/src/components/editor/manualEditsDom.ts +36 -3
- package/src/components/editor/manualEditsDomPatches.test.ts +2 -86
- package/src/components/editor/manualEditsDomPatches.ts +0 -30
- package/src/components/editor/manualEditsSnapshot.ts +1 -0
- package/src/components/editor/marqueeCommit.test.tsx +69 -1
- package/src/components/editor/marqueeCommit.ts +6 -1
- package/src/components/editor/overlayFrameLoop.test.ts +27 -0
- package/src/components/editor/overlayFrameLoop.ts +21 -0
- package/src/components/editor/persistSeam.integration.test.ts +0 -18
- package/src/components/editor/plainTranslate.ts +34 -3
- package/src/components/editor/propertyPanelColorCurveGraph.tsx +2 -2
- package/src/components/editor/propertyPanelColorGradingControls.tsx +1 -1
- package/src/components/editor/propertyPanelColorGradingSection.tsx +8 -8
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +1 -1
- package/src/components/editor/propertyPanelColorSecondary.tsx +4 -4
- package/src/components/editor/propertyPanelFill.tsx +6 -6
- package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
- package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +1 -1
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +6 -9
- package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +2 -2
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +2 -2
- package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +2 -2
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +4 -3
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +1 -1
- package/src/components/editor/propertyPanelFlatSliderReadout.tsx +1 -1
- package/src/components/editor/propertyPanelFont.test.tsx +80 -3
- package/src/components/editor/propertyPanelFont.tsx +71 -48
- package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
- package/src/components/editor/propertyPanelFxControls.tsx +2 -2
- package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
- package/src/components/editor/propertyPanelFxNodeRow.tsx +1 -1
- package/src/components/editor/propertyPanelFxPresetRun.tsx +1 -1
- package/src/components/editor/propertyPanelHelpers.ts +1 -2
- package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
- package/src/components/editor/propertyPanelPrimitives.tsx +1 -1
- package/src/components/editor/propertyPanelSections.tsx +2 -2
- package/src/components/editor/propertyPanelValueTier.test.ts +1 -1
- package/src/components/editor/propertyPanelValueTier.ts +1 -1
- package/src/components/editor/studioCodeTheme.test.ts +20 -0
- package/src/components/editor/studioCodeTheme.ts +99 -0
- package/src/components/editor/useDomEditNudge.test.tsx +31 -0
- package/src/components/editor/useDomEditNudge.ts +2 -2
- package/src/components/editor/useInlineTextEditing.tsx +1 -1
- package/src/components/editor/useMotionPathData.test.tsx +64 -0
- package/src/components/editor/useMotionPathData.ts +5 -1
- package/src/components/feedback/StudioFeedbackCard.tsx +11 -23
- package/src/components/nle/AssetPreviewOverlay.tsx +1 -1
- package/src/components/nle/AudioMeterStrip.tsx +2 -2
- package/src/components/nle/NLEPreview.test.ts +8 -4
- package/src/components/nle/PreviewOverlays.tsx +1 -1
- package/src/components/nle/PreviewPane.tsx +3 -3
- package/src/components/nle/PreviewPoster.tsx +29 -1
- package/src/components/nle/PreviewZoomOverlay.tsx +1 -1
- package/src/components/panels/SlideshowPanel.tsx +4 -4
- package/src/components/panels/SlideshowSubPanels.tsx +19 -21
- package/src/components/panels/VariablesBindElement.tsx +1 -1
- package/src/components/panels/VariablesDeclarationForm.tsx +1 -1
- package/src/components/panels/VariablesPanel.tsx +4 -6
- package/src/components/panels/VariablesRowAction.tsx +1 -1
- package/src/components/renders/FfmpegRequiredNotice.tsx +4 -4
- package/src/components/renders/RenderQueue.tsx +2 -2
- package/src/components/renders/RenderQueueItem.tsx +3 -3
- package/src/components/sidebar/AssetCard.tsx +3 -3
- package/src/components/sidebar/AssetContextMenu.tsx +7 -7
- package/src/components/sidebar/AssetsTab.tsx +6 -6
- package/src/components/sidebar/AudioRow.tsx +5 -3
- package/src/components/sidebar/BlocksTab.tsx +6 -6
- package/src/components/sidebar/CompositionsTab.drag.test.tsx +3 -69
- package/src/components/sidebar/CompositionsTab.rootBadge.test.tsx +4 -50
- package/src/components/sidebar/CompositionsTab.thumbnails.test.tsx +256 -0
- package/src/components/sidebar/CompositionsTab.tsx +65 -12
- package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
- package/src/components/sidebar/SidebarLintButton.tsx +3 -3
- package/src/components/sidebar/compositionsTabTestUtils.tsx +39 -0
- package/src/components/timelineToolbarStyles.ts +3 -3
- package/src/components/ui/Button.tsx +4 -4
- package/src/components/ui/HyperframesLogo.tsx +19 -18
- package/src/components/ui/Input.tsx +1 -1
- package/src/components/ui/Menu.tsx +4 -2
- package/src/hooks/appHotkeysDispatch.ts +2 -1
- package/src/hooks/elementOffsetStager.ts +8 -7
- package/src/hooks/previewPromotionSelection.test.tsx +180 -0
- package/src/hooks/useAppHotkeys.test.ts +32 -0
- package/src/hooks/useAppHotkeys.ts +1 -2
- package/src/hooks/useClipboard.source.test.ts +141 -23
- package/src/hooks/useClipboard.ts +154 -123
- package/src/hooks/useDomEditCommits.ts +0 -3
- package/src/hooks/useDomEditSession.test.tsx +76 -72
- package/src/hooks/useDomEditSession.ts +7 -43
- package/src/hooks/useDomGeometryCommit.test.tsx +4 -1
- package/src/hooks/useDomGeometryCommit.ts +0 -1
- package/src/hooks/useDomGeometryCommits.test.tsx +177 -32
- package/src/hooks/useDomGeometryCommits.ts +4 -49
- package/src/hooks/useGsapTweenCache.ts +1 -2
- package/src/hooks/useInlineTextEdit.ts +16 -2
- package/src/hooks/useLivePreviewIframe.ts +18 -0
- package/src/hooks/usePreviewPersistence.history.test.tsx +47 -0
- package/src/hooks/usePreviewPersistence.ts +2 -5
- package/src/hooks/useRenderClipContent.ts +3 -3
- package/src/index.ts +2 -0
- package/src/livePreviewIframeExport.test.tsx +33 -0
- package/src/main.tsx +4 -1
- package/src/player/components/AudioWaveform.test.tsx +36 -15
- package/src/player/components/AudioWaveform.tsx +24 -7
- package/src/player/components/ClipContextMenu.tsx +1 -1
- package/src/player/components/CompositionThumbnail.tsx +23 -11
- package/src/player/components/EditModal.tsx +6 -6
- package/src/player/components/KeyframeDiamondContextMenu.tsx +2 -2
- package/src/player/components/PlayerControls.tsx +3 -3
- package/src/player/components/ShortcutsPanel.tsx +8 -20
- package/src/player/components/TimelineCanvas.tsx +1 -1
- package/src/player/components/TimelineClip.test.tsx +38 -0
- package/src/player/components/TimelineClip.tsx +16 -8
- package/src/player/components/TimelineClipFades.test.tsx +376 -74
- package/src/player/components/TimelineClipFades.tsx +368 -137
- package/src/player/components/TimelineEmptyState.tsx +2 -2
- package/src/player/components/TimelineLanes.tsx +0 -1
- package/src/player/components/TrackClipCount.tsx +1 -1
- package/src/player/components/VolumeControl.tsx +1 -1
- package/src/player/components/timelineClipChildren.tsx +1 -1
- package/src/player/components/timelineMotionStyles.test.ts +6 -22
- package/src/player/components/trackHeaderLabelRows.tsx +16 -16
- package/src/player/components/useTimelineKeyboardActor.ts +1 -1
- package/src/player/hooks/timelinePlayerTestHarness.ts +27 -0
- package/src/player/hooks/useShadowPreviewReload.ts +72 -5
- package/src/player/lib/automationStoreSync.test.ts +18 -1
- package/src/player/lib/automationStoreSync.ts +29 -17
- package/src/player/lib/thumbnailScheduler.ts +4 -5
- package/src/player/lib/timelineViewportBudgets.test.ts +1 -1
- package/src/player/lib/timelineViewportBudgets.ts +1 -1
- package/src/player/sceneSwap.ts +18 -0
- package/src/styles/components.css +26 -0
- package/src/styles/hex-baseline.json +7 -19
- package/src/styles/studio.css +4 -7
- package/src/styles/tailwind-preset.shared.js +2 -1
- package/src/styles/theme.css +214 -147
- package/src/styles/theme.test.ts +13 -9
- package/src/utils/blockCategories.ts +41 -9
- package/src/utils/clipboardPayload.ts +12 -1
- package/src/utils/studioPendingEdits.ts +21 -0
- package/src/utils/studioTheme.test.tsx +141 -0
- package/src/utils/studioTheme.ts +34 -0
- package/src/utils/studioUiPreferences.ts +3 -0
- package/src/utils/typingTarget.test.ts +8 -1
- package/src/utils/typingTarget.ts +5 -0
- package/dist/assets/index-CQN3M56N.css +0 -1
- package/dist/assets/index-De2aO4XM.js +0 -399
- package/dist/chunk-62DDWOSH.js.map +0 -1
- package/dist/chunk-6GAO75IU.js.map +0 -1
- package/dist/chunk-HIGHVWYD.js.map +0 -1
- package/src/hooks/gsapTargetCache.ts +0 -62
|
@@ -30,6 +30,26 @@ import {
|
|
|
30
30
|
} from "../utils/authoredSource";
|
|
31
31
|
import { serializeStudioFileMutations } from "../utils/studioFileMutationCoordinator";
|
|
32
32
|
|
|
33
|
+
type InPlace = { inPlace?: boolean };
|
|
34
|
+
|
|
35
|
+
const pasteStart = (
|
|
36
|
+
payload: Extract<ClipboardPayload, { kind: "timeline-clip" }>,
|
|
37
|
+
targetPath: string,
|
|
38
|
+
playhead: number,
|
|
39
|
+
options?: InPlace,
|
|
40
|
+
) =>
|
|
41
|
+
options?.inPlace && payload.copiedInComposition === targetPath
|
|
42
|
+
? Math.min(...payload.clips.map((clip) => clip.start))
|
|
43
|
+
: playhead;
|
|
44
|
+
|
|
45
|
+
const pasteWords = (payload: ClipboardPayload) =>
|
|
46
|
+
payload.kind === "timeline-clip"
|
|
47
|
+
? {
|
|
48
|
+
label: clipLabel("Paste", payload.clips.length),
|
|
49
|
+
toast: clipToast("Pasted", payload.clips.length),
|
|
50
|
+
}
|
|
51
|
+
: { label: "Paste element", toast: "Pasted element" };
|
|
52
|
+
|
|
33
53
|
interface RecordEditInput {
|
|
34
54
|
label: string;
|
|
35
55
|
coalesceKey?: string;
|
|
@@ -160,10 +180,17 @@ const hfIdsInFile = (content: string) =>
|
|
|
160
180
|
export function pasteElementHtml(
|
|
161
181
|
content: string,
|
|
162
182
|
payload: { html: string; originSelector?: string; originSelectorIndex?: number },
|
|
183
|
+
fromThisFile = false,
|
|
163
184
|
): string {
|
|
164
185
|
const reminted = remintHfIds(payload.html, new DOMParser(), hfIdsInFile(content));
|
|
165
186
|
const deduped = deduplicateIds(reminted, collectHtmlIds(content));
|
|
166
|
-
|
|
187
|
+
const result = insertAsSibling(
|
|
188
|
+
content,
|
|
189
|
+
deduped,
|
|
190
|
+
payload.originSelector,
|
|
191
|
+
payload.originSelectorIndex,
|
|
192
|
+
);
|
|
193
|
+
return fromThisFile ? carryLook(result, renamedIds(reminted, deduped), 0) : result;
|
|
167
194
|
}
|
|
168
195
|
|
|
169
196
|
/** Shared insertion path for paste and duplicate, anchored at the playhead or the selection's end. Returns the
|
|
@@ -306,6 +333,7 @@ export function useClipboard({
|
|
|
306
333
|
clips,
|
|
307
334
|
sourceFile,
|
|
308
335
|
projectId: projectIdRef.current ?? undefined,
|
|
336
|
+
copiedInComposition: activeCompPath || "index.html",
|
|
309
337
|
};
|
|
310
338
|
});
|
|
311
339
|
}, [activeCompPath, findSelectedClips, readClips, showToast]);
|
|
@@ -324,6 +352,7 @@ export function useClipboard({
|
|
|
324
352
|
kind: "dom-element",
|
|
325
353
|
html: savedMarkupElseLive(parseSavedSource(content), live, sourceFile),
|
|
326
354
|
sourceFile,
|
|
355
|
+
projectId: projectIdRef.current ?? undefined,
|
|
327
356
|
originSelector: domSelection.selector,
|
|
328
357
|
originSelectorIndex: domSelection.selectorIndex,
|
|
329
358
|
};
|
|
@@ -361,134 +390,136 @@ export function useClipboard({
|
|
|
361
390
|
|
|
362
391
|
// Two independent paste modes (timeline clip vs DOM element) behind one guarded save.
|
|
363
392
|
// fallow-ignore-next-line complexity
|
|
364
|
-
const handlePaste = useCallback(
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const pid = projectIdRef.current;
|
|
371
|
-
if (!pid) return;
|
|
372
|
-
|
|
373
|
-
const targetPath = activeCompPath || "index.html";
|
|
374
|
-
try {
|
|
375
|
-
let pastedIds: string[] = [];
|
|
376
|
-
const paste = (originalContent: string) => {
|
|
377
|
-
if (payload.kind !== "timeline-clip") return pasteElementHtml(originalContent, payload);
|
|
378
|
-
const { currentTime, elements } = usePlayerStore.getState();
|
|
379
|
-
const pasted = pasteTimelineClips(
|
|
380
|
-
originalContent,
|
|
381
|
-
payload.clips,
|
|
382
|
-
currentTime,
|
|
383
|
-
elements,
|
|
384
|
-
payload.sourceFile === targetPath && payload.projectId === pid,
|
|
385
|
-
);
|
|
386
|
-
pastedIds = pasted.ids;
|
|
387
|
-
// A clip pasted past the current composition end would exist in the
|
|
388
|
-
// file but never appear on the timeline or in playback/export (the
|
|
389
|
-
// root's data-duration is what actually bounds the render).
|
|
390
|
-
return extendRootDurationInSource(pasted.content, pasted.requiredEnd);
|
|
391
|
-
};
|
|
392
|
-
|
|
393
|
-
const label =
|
|
394
|
-
payload.kind === "timeline-clip"
|
|
395
|
-
? clipLabel("Paste", payload.clips.length)
|
|
396
|
-
: "Paste element";
|
|
397
|
-
|
|
398
|
-
await saveProjectFilesWithHistory({
|
|
399
|
-
projectId: pid,
|
|
400
|
-
label,
|
|
401
|
-
files: { [targetPath]: paste },
|
|
402
|
-
readFile: (path) => readFileContent(pid, path),
|
|
403
|
-
writeFile: writeProjectFile,
|
|
404
|
-
recordEdit,
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
// CapCut: the pasted clip(s) become the selection; the playhead does not
|
|
408
|
-
// move. reloadPreview is a bare refresh-key bump with no selection
|
|
409
|
-
// snapshot of its own; calling setSelection before it is what makes the
|
|
410
|
-
// post-reload store state land on the pasted ids instead of stale ones.
|
|
411
|
-
if (pastedIds.length > 0) {
|
|
412
|
-
usePlayerStore.getState().setSelection(pastedIds.map((id) => elementKey(id, targetPath)));
|
|
393
|
+
const handlePaste = useCallback(
|
|
394
|
+
async (options?: InPlace) => {
|
|
395
|
+
const payload = await clipboardRef.current;
|
|
396
|
+
if (!payload) {
|
|
397
|
+
showToast("Nothing to paste.", "info");
|
|
398
|
+
return;
|
|
413
399
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
400
|
+
const pid = projectIdRef.current;
|
|
401
|
+
if (!pid) return;
|
|
402
|
+
|
|
403
|
+
const targetPath = activeCompPath || "index.html";
|
|
404
|
+
try {
|
|
405
|
+
let pastedIds: string[] = [];
|
|
406
|
+
const fromThisFile = payload.sourceFile === targetPath && payload.projectId === pid;
|
|
407
|
+
const paste = (originalContent: string) => {
|
|
408
|
+
if (payload.kind !== "timeline-clip")
|
|
409
|
+
return pasteElementHtml(originalContent, payload, fromThisFile);
|
|
410
|
+
const { currentTime, elements } = usePlayerStore.getState();
|
|
411
|
+
const pasted = pasteTimelineClips(
|
|
412
|
+
originalContent,
|
|
413
|
+
payload.clips,
|
|
414
|
+
pasteStart(payload, targetPath, currentTime, options),
|
|
415
|
+
elements,
|
|
416
|
+
fromThisFile,
|
|
417
|
+
);
|
|
418
|
+
pastedIds = pasted.ids;
|
|
419
|
+
// A clip pasted past the current composition end would exist in the
|
|
420
|
+
// file but never appear on the timeline or in playback/export (the
|
|
421
|
+
// root's data-duration is what actually bounds the render).
|
|
422
|
+
return extendRootDurationInSource(pasted.content, pasted.requiredEnd);
|
|
423
|
+
};
|
|
426
424
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
425
|
+
const { label, toast } = pasteWords(payload);
|
|
426
|
+
|
|
427
|
+
await saveProjectFilesWithHistory({
|
|
428
|
+
projectId: pid,
|
|
429
|
+
label,
|
|
430
|
+
files: { [targetPath]: paste },
|
|
431
|
+
readFile: (path) => readFileContent(pid, path),
|
|
432
|
+
writeFile: writeProjectFile,
|
|
433
|
+
recordEdit,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// CapCut: the pasted clip(s) become the selection; the playhead does not
|
|
437
|
+
// move. reloadPreview is a bare refresh-key bump with no selection
|
|
438
|
+
// snapshot of its own; calling setSelection before it is what makes the
|
|
439
|
+
// post-reload store state land on the pasted ids instead of stale ones.
|
|
440
|
+
if (pastedIds.length > 0) {
|
|
441
|
+
usePlayerStore.getState().setSelection(pastedIds.map((id) => elementKey(id, targetPath)));
|
|
442
|
+
}
|
|
443
|
+
reloadPreview();
|
|
444
|
+
showToast(toast, "info");
|
|
445
|
+
} catch (error) {
|
|
446
|
+
const message = error instanceof Error ? error.message : "Failed to paste";
|
|
447
|
+
showToast(message);
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
[activeCompPath, recordEdit, reloadPreview, showToast, writeProjectFile],
|
|
451
|
+
);
|
|
445
452
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
};
|
|
453
|
+
// Duplicates the selection right after it, or with `inPlace` at its own time on a track of its own,
|
|
454
|
+
// without touching the clipboard — a pending copy must survive a Cmd+D. Shares
|
|
455
|
+
// pasteTimelineClips with handlePaste; only the anchor and clip source differ (the
|
|
456
|
+
// selection's end or start here, the playhead or the copy's own time there).
|
|
457
|
+
const handleDuplicate = useCallback(
|
|
458
|
+
async (options?: InPlace): Promise<boolean> => {
|
|
459
|
+
const targets = findSelectedClips();
|
|
460
|
+
if (!targets) return false;
|
|
461
|
+
const pid = projectIdRef.current;
|
|
462
|
+
if (!pid) return false;
|
|
463
|
+
|
|
464
|
+
const { elements } = targets;
|
|
465
|
+
const pathOf = (el: TimelineElement) => el.sourceFile || activeCompPath || "index.html";
|
|
466
|
+
const targetPath = pathOf(elements[0]!);
|
|
467
|
+
// The copy lands in targetPath's own clock, so anchor and lane check are local to it.
|
|
468
|
+
const anchorTime = toAuthoredStart(
|
|
469
|
+
elements[0]!,
|
|
470
|
+
options?.inPlace
|
|
471
|
+
? Math.min(...elements.map((el) => el.start))
|
|
472
|
+
: Math.max(...elements.map((el) => el.start + el.duration)),
|
|
473
|
+
);
|
|
458
474
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
475
|
+
try {
|
|
476
|
+
const clips = await readClips(targets);
|
|
477
|
+
let ids: string[] = [];
|
|
478
|
+
const duplicate = (originalContent: string) => {
|
|
479
|
+
const liveElements = usePlayerStore
|
|
480
|
+
.getState()
|
|
481
|
+
.elements.filter((el) => pathOf(el) === targetPath)
|
|
482
|
+
.map((el) => ({ ...el, start: toAuthoredStart(el, el.start) }));
|
|
483
|
+
const pasted = pasteTimelineClips(originalContent, clips, anchorTime, liveElements, true);
|
|
484
|
+
ids = pasted.ids;
|
|
485
|
+
return extendRootDurationInSource(pasted.content, pasted.requiredEnd);
|
|
486
|
+
};
|
|
467
487
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
488
|
+
await saveProjectFilesWithHistory({
|
|
489
|
+
projectId: pid,
|
|
490
|
+
label: clipLabel("Duplicate", clips.length),
|
|
491
|
+
files: { [targetPath]: duplicate },
|
|
492
|
+
readFile: (path) => readFileContent(pid, path),
|
|
493
|
+
writeFile: writeProjectFile,
|
|
494
|
+
recordEdit,
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// The duplicate becomes the selection, mirroring CapCut's own paste
|
|
498
|
+
// convention — there is no CapCut Duplicate to match directly (Cmd+D is
|
|
499
|
+
// a no-op there); this is our own choice for consistency with paste.
|
|
500
|
+
// Select before reloading, same reason as handlePaste above.
|
|
501
|
+
if (ids.length > 0) {
|
|
502
|
+
usePlayerStore.getState().setSelection(ids.map((id) => elementKey(id, targetPath)));
|
|
503
|
+
}
|
|
504
|
+
reloadPreview();
|
|
505
|
+
showToast(clipToast("Duplicated", clips.length), "info");
|
|
506
|
+
return true;
|
|
507
|
+
} catch (error) {
|
|
508
|
+
const message = error instanceof Error ? error.message : "Failed to duplicate";
|
|
509
|
+
showToast(message);
|
|
510
|
+
return false;
|
|
474
511
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
readClips,
|
|
487
|
-
recordEdit,
|
|
488
|
-
reloadPreview,
|
|
489
|
-
showToast,
|
|
490
|
-
writeProjectFile,
|
|
491
|
-
]);
|
|
512
|
+
},
|
|
513
|
+
[
|
|
514
|
+
activeCompPath,
|
|
515
|
+
findSelectedClips,
|
|
516
|
+
readClips,
|
|
517
|
+
recordEdit,
|
|
518
|
+
reloadPreview,
|
|
519
|
+
showToast,
|
|
520
|
+
writeProjectFile,
|
|
521
|
+
],
|
|
522
|
+
);
|
|
492
523
|
|
|
493
524
|
const handleCut = useCallback(async (): Promise<boolean> => {
|
|
494
525
|
const selected = getSelectedElements();
|
|
@@ -271,12 +271,10 @@ export function useDomEditCommits({
|
|
|
271
271
|
|
|
272
272
|
const {
|
|
273
273
|
stageElementPositionOffset,
|
|
274
|
-
handleDomPathOffsetCommit,
|
|
275
274
|
handleDomBoxSizeCommit,
|
|
276
275
|
handleDomRotationCommit,
|
|
277
276
|
handleDomManualEditsReset,
|
|
278
277
|
} = useDomGeometryCommits({
|
|
279
|
-
previewIframeRef,
|
|
280
278
|
showToast,
|
|
281
279
|
commitPositionPatchToHtml,
|
|
282
280
|
readOnlyPreview,
|
|
@@ -316,7 +314,6 @@ export function useDomEditCommits({
|
|
|
316
314
|
handleDomRemoveTextField,
|
|
317
315
|
stageElementPositionOffset,
|
|
318
316
|
commitPositionPatchToHtml,
|
|
319
|
-
handleDomPathOffsetCommit,
|
|
320
317
|
handleDomBoxSizeCommit,
|
|
321
318
|
handleDomRotationCommit,
|
|
322
319
|
handleDomManualEditsReset,
|
|
@@ -9,6 +9,7 @@ import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
|
9
9
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
10
10
|
import type { TimelineElement } from "../player";
|
|
11
11
|
import type { UseDomEditSessionParams } from "./useDomEditSession";
|
|
12
|
+
import { makeSelection } from "./domSelectionTestHarness";
|
|
12
13
|
|
|
13
14
|
const styleOp = (property: string, value: string): PatchOperation => ({
|
|
14
15
|
type: "inline-style",
|
|
@@ -132,26 +133,30 @@ vi.mock("./useDomEditCommits", () => ({
|
|
|
132
133
|
};
|
|
133
134
|
},
|
|
134
135
|
}));
|
|
136
|
+
const iframesHandedOn: Array<{ hook: string; iframe: unknown }> = [];
|
|
135
137
|
vi.mock("./useDomSelection", () => ({
|
|
136
|
-
useDomSelection: () =>
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
138
|
+
useDomSelection: ({ previewIframe }: { previewIframe: unknown }) => {
|
|
139
|
+
iframesHandedOn.push({ hook: "selection", iframe: previewIframe });
|
|
140
|
+
return {
|
|
141
|
+
domEditSelection: domEditSelectionRef.current,
|
|
142
|
+
domEditGroupSelections: [],
|
|
143
|
+
domEditHoverSelection: null,
|
|
144
|
+
activeGroupElement: null,
|
|
145
|
+
domEditSelectionRef,
|
|
146
|
+
domEditGroupSelectionsRef,
|
|
147
|
+
setActiveGroupElement: vi.fn(),
|
|
148
|
+
applyDomSelection: vi.fn(),
|
|
149
|
+
clearDomSelection: vi.fn(),
|
|
150
|
+
buildDomSelectionFromTarget: vi.fn(),
|
|
151
|
+
resolveDomSelectionFromPreviewPoint: vi.fn(),
|
|
152
|
+
resolveAllDomSelectionsFromPreviewPoint: vi.fn(),
|
|
153
|
+
updateDomEditHoverSelection: vi.fn(),
|
|
154
|
+
buildDomSelectionForTimelineElement: vi.fn(),
|
|
155
|
+
handleTimelineElementSelect: vi.fn(),
|
|
156
|
+
refreshDomEditSelectionFromPreview: vi.fn(),
|
|
157
|
+
applyMarqueeSelection: vi.fn(),
|
|
158
|
+
};
|
|
159
|
+
},
|
|
155
160
|
}));
|
|
156
161
|
vi.mock("./useAskAgentModal", () => ({
|
|
157
162
|
useAskAgentModal: () => ({
|
|
@@ -208,33 +213,36 @@ vi.mock("./useGroupCommits", () => ({
|
|
|
208
213
|
}),
|
|
209
214
|
}));
|
|
210
215
|
vi.mock("./useDomEditWiring", () => ({
|
|
211
|
-
useDomEditWiring: () =>
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
216
|
+
useDomEditWiring: ({ previewIframe }: { previewIframe: unknown }) => {
|
|
217
|
+
iframesHandedOn.push({ hook: "wiring", iframe: previewIframe });
|
|
218
|
+
return {
|
|
219
|
+
onClickToSource: vi.fn(),
|
|
220
|
+
selectedGsapAnimations: [],
|
|
221
|
+
gsapMultipleTimelines: false,
|
|
222
|
+
gsapUnsupportedTimelinePattern: false,
|
|
223
|
+
trackGsapInteractionFailure: vi.fn(),
|
|
224
|
+
makeFetchFallback: vi.fn(),
|
|
225
|
+
handleGsapUpdateProperty: vi.fn(),
|
|
226
|
+
handleGsapUpdateMeta: vi.fn(),
|
|
227
|
+
handleGsapDeleteAnimation: vi.fn(),
|
|
228
|
+
handleGsapDeleteAllForElement: vi.fn(),
|
|
229
|
+
handleGsapAddAnimation: vi.fn(),
|
|
230
|
+
handleGsapAddProperty: vi.fn(),
|
|
231
|
+
handleGsapRemoveProperty: vi.fn(),
|
|
232
|
+
handleGsapUpdateFromProperty: vi.fn(),
|
|
233
|
+
handleGsapAddFromProperty: vi.fn(),
|
|
234
|
+
handleGsapRemoveFromProperty: vi.fn(),
|
|
235
|
+
handleGsapAddKeyframe: vi.fn(),
|
|
236
|
+
handleGsapAddKeyframeBatch: vi.fn(),
|
|
237
|
+
handleGsapRemoveKeyframe: vi.fn(),
|
|
238
|
+
handleGsapMoveKeyframeToPlayhead: vi.fn(),
|
|
239
|
+
handleGsapMoveKeyframe: vi.fn(),
|
|
240
|
+
handleGsapResizeKeyframedTween: vi.fn(),
|
|
241
|
+
handleGsapConvertToKeyframes: vi.fn(),
|
|
242
|
+
handleGsapRemoveAllKeyframes: vi.fn(),
|
|
243
|
+
handleResetSelectedElementKeyframes: vi.fn(),
|
|
244
|
+
};
|
|
245
|
+
},
|
|
238
246
|
}));
|
|
239
247
|
vi.mock("./usePreviewInteraction", () => ({
|
|
240
248
|
usePreviewInteraction: () => ({
|
|
@@ -310,32 +318,9 @@ describe("onReorderShadow source filter", () => {
|
|
|
310
318
|
describe("bulk segment ease commits", () => {
|
|
311
319
|
it("uses one ordered batch for many ids and sane paths for one or no ids", async () => {
|
|
312
320
|
const { useDomEditSession } = await import("./useDomEditSession");
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
label: "Hero",
|
|
317
|
-
tagName: "DIV",
|
|
318
|
-
sourceFile: "index.html",
|
|
319
|
-
compositionPath: "index.html",
|
|
320
|
-
isCompositionHost: false,
|
|
321
|
-
isInsideLockedComposition: false,
|
|
322
|
-
boundingBox: { x: 0, y: 0, width: 100, height: 100 },
|
|
323
|
-
textContent: null,
|
|
324
|
-
dataAttributes: {},
|
|
325
|
-
inlineStyles: {},
|
|
326
|
-
computedStyles: {},
|
|
327
|
-
textFields: [],
|
|
328
|
-
capabilities: {
|
|
329
|
-
canSelect: true,
|
|
330
|
-
canEditStyles: true,
|
|
331
|
-
canCrop: true,
|
|
332
|
-
canMove: true,
|
|
333
|
-
canResize: true,
|
|
334
|
-
canApplyManualOffset: true,
|
|
335
|
-
canApplyManualSize: true,
|
|
336
|
-
canApplyManualRotation: true,
|
|
337
|
-
},
|
|
338
|
-
};
|
|
321
|
+
const hero = document.createElement("div");
|
|
322
|
+
hero.id = "hero";
|
|
323
|
+
const selection = makeSelection("Hero", hero);
|
|
339
324
|
domEditSelectionRef.current = selection;
|
|
340
325
|
gsapCommitMutation.mockClear();
|
|
341
326
|
gsapCommitMutation.batch.mockClear();
|
|
@@ -601,3 +586,22 @@ describe("handleDomEditElementDelete routing", () => {
|
|
|
601
586
|
expect(handleDomEditElementsDeleteMock).not.toHaveBeenCalled();
|
|
602
587
|
});
|
|
603
588
|
});
|
|
589
|
+
|
|
590
|
+
describe("a shadow reload promoted without a host re-render", () => {
|
|
591
|
+
it("hands the promoted iframe to the session's selection and preview sync", async () => {
|
|
592
|
+
const { useDomEditSession } = await import("./useDomEditSession");
|
|
593
|
+
const { announcePreviewPromoted } = await import("../player/sceneSwap");
|
|
594
|
+
const [host, promoted] = [0, 1].map(() => document.createElement("iframe"));
|
|
595
|
+
function Probe() {
|
|
596
|
+
useDomEditSession(createSessionParams({ previewIframe: host! }));
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
const root = createRoot(document.createElement("div"));
|
|
600
|
+
act(() => root.render(<Probe />));
|
|
601
|
+
act(() => announcePreviewPromoted({ retired: host!, live: promoted! }));
|
|
602
|
+
const last = (hook: string) => iframesHandedOn.findLast((seen) => seen.hook === hook)?.iframe;
|
|
603
|
+
expect(last("selection")).toBe(promoted);
|
|
604
|
+
expect(last("wiring")).toBe(promoted);
|
|
605
|
+
act(() => root.unmount());
|
|
606
|
+
});
|
|
607
|
+
});
|
|
@@ -24,6 +24,7 @@ import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
|
24
24
|
import { membersForDelete, timelineElementsForDelete } from "./domEditDeleteMembers";
|
|
25
25
|
import type { RecordEditInput } from "./domEditDeleteMembers";
|
|
26
26
|
import type { DomEditTimelineParams } from "./useDomSelectionTypes";
|
|
27
|
+
import { useLivePreviewIframe } from "./useLivePreviewIframe";
|
|
27
28
|
// Re-exported: the delete rule lives in its own module; callers and its test import it from here.
|
|
28
29
|
export { membersForDelete };
|
|
29
30
|
|
|
@@ -86,14 +87,13 @@ export function useDomEditSession({
|
|
|
86
87
|
importedFontAssetsRef,
|
|
87
88
|
projectDir,
|
|
88
89
|
projectIdRef,
|
|
89
|
-
previewIframe,
|
|
90
|
+
previewIframe: hostPreviewIframe,
|
|
90
91
|
refreshKey,
|
|
91
92
|
previewDocumentVersion,
|
|
92
93
|
rightPanelTab,
|
|
93
94
|
applyStudioManualEditsToPreviewRef,
|
|
94
95
|
syncPreviewHotkeys,
|
|
95
96
|
reloadPreview,
|
|
96
|
-
setRefreshKey: _setRefreshKey,
|
|
97
97
|
openSourceForSelection,
|
|
98
98
|
sdkSession,
|
|
99
99
|
publishSdkSession,
|
|
@@ -103,7 +103,7 @@ export function useDomEditSession({
|
|
|
103
103
|
}: UseDomEditSessionParams) {
|
|
104
104
|
const isMasterView = !activeCompPath || activeCompPath === "index.html";
|
|
105
105
|
const previewCaptionEditMode = captionEditMode && !readOnlyPreview;
|
|
106
|
-
|
|
106
|
+
const previewIframe = useLivePreviewIframe(hostPreviewIframe);
|
|
107
107
|
const {
|
|
108
108
|
domEditSelection,
|
|
109
109
|
domEditGroupSelections,
|
|
@@ -173,29 +173,11 @@ export function useDomEditSession({
|
|
|
173
173
|
|
|
174
174
|
const { version: gsapCacheVersion, bump: bumpGsapCache } = useGsapCacheVersion();
|
|
175
175
|
|
|
176
|
-
// ── GSAP script commits ──
|
|
177
|
-
|
|
178
176
|
const {
|
|
179
177
|
commitMutation: gsapCommitMutation,
|
|
180
|
-
updateGsapProperty,
|
|
181
|
-
updateGsapMeta,
|
|
182
|
-
deleteGsapAnimation,
|
|
183
|
-
deleteAllForSelector,
|
|
184
|
-
addGsapAnimation,
|
|
185
|
-
addGsapProperty,
|
|
186
|
-
removeGsapProperty,
|
|
187
|
-
updateGsapFromProperty,
|
|
188
|
-
addGsapFromProperty,
|
|
189
|
-
removeGsapFromProperty,
|
|
190
|
-
addKeyframe,
|
|
191
|
-
addKeyframeBatch,
|
|
192
|
-
removeKeyframe,
|
|
193
|
-
moveKeyframe,
|
|
194
|
-
resizeKeyframedTween,
|
|
195
|
-
convertToKeyframes,
|
|
196
|
-
removeAllKeyframes,
|
|
197
178
|
setArcPath,
|
|
198
179
|
updateArcSegment,
|
|
180
|
+
...gsapScriptEdits
|
|
199
181
|
} = useGsapScriptCommits({
|
|
200
182
|
projectIdRef,
|
|
201
183
|
activeCompPath,
|
|
@@ -211,8 +193,6 @@ export function useDomEditSession({
|
|
|
211
193
|
forceReloadSdkSession,
|
|
212
194
|
});
|
|
213
195
|
|
|
214
|
-
// ── DOM commit handlers ──
|
|
215
|
-
|
|
216
196
|
const {
|
|
217
197
|
resolveImportedFontAsset,
|
|
218
198
|
handleDomStyleCommit,
|
|
@@ -430,23 +410,7 @@ export function useDomEditSession({
|
|
|
430
410
|
applyDomSelection,
|
|
431
411
|
buildDomSelectionFromTarget,
|
|
432
412
|
openSourceForSelection,
|
|
433
|
-
|
|
434
|
-
updateGsapMeta,
|
|
435
|
-
deleteGsapAnimation,
|
|
436
|
-
deleteAllForSelector,
|
|
437
|
-
addGsapAnimation,
|
|
438
|
-
addGsapProperty,
|
|
439
|
-
removeGsapProperty,
|
|
440
|
-
updateGsapFromProperty,
|
|
441
|
-
addGsapFromProperty,
|
|
442
|
-
removeGsapFromProperty,
|
|
443
|
-
addKeyframe,
|
|
444
|
-
addKeyframeBatch,
|
|
445
|
-
removeKeyframe,
|
|
446
|
-
moveKeyframe,
|
|
447
|
-
resizeKeyframedTween,
|
|
448
|
-
convertToKeyframes,
|
|
449
|
-
removeAllKeyframes,
|
|
413
|
+
...gsapScriptEdits,
|
|
450
414
|
handleDomManualEditsReset,
|
|
451
415
|
});
|
|
452
416
|
const {
|
|
@@ -492,8 +456,8 @@ export function useDomEditSession({
|
|
|
492
456
|
handleDomBoxSizeCommit,
|
|
493
457
|
handleDomRotationCommit: handleDomCssRotationCommit,
|
|
494
458
|
commitPositionPatchToHtml,
|
|
495
|
-
addGsapAnimation,
|
|
496
|
-
convertToKeyframes,
|
|
459
|
+
addGsapAnimation: gsapScriptEdits.addGsapAnimation,
|
|
460
|
+
convertToKeyframes: gsapScriptEdits.convertToKeyframes,
|
|
497
461
|
setArcPath,
|
|
498
462
|
updateArcSegment,
|
|
499
463
|
});
|
|
@@ -168,7 +168,10 @@ describe("useDomGeometryCommit, from the package entry", () => {
|
|
|
168
168
|
expect(element.style.getPropertyValue("translate")).toBe("130.5px 90px");
|
|
169
169
|
expect(calls.patches).toEqual([
|
|
170
170
|
expect.objectContaining({
|
|
171
|
-
operations: [
|
|
171
|
+
operations: [
|
|
172
|
+
{ type: "inline-style", property: "translate", value: "130.5px 90px" },
|
|
173
|
+
{ type: "attribute", property: "data-hf-studio-original-inline-translate", value: "" },
|
|
174
|
+
],
|
|
172
175
|
}),
|
|
173
176
|
]);
|
|
174
177
|
expect(calls.urls.filter((url) => url.includes("gsap"))).toEqual([]);
|
|
@@ -111,7 +111,6 @@ export function useDomGeometryCommit({
|
|
|
111
111
|
);
|
|
112
112
|
const { stageElementPositionOffset, handleDomBoxSizeCommit, handleDomRotationCommit } =
|
|
113
113
|
useDomGeometryCommits({
|
|
114
|
-
previewIframeRef: iframeRef,
|
|
115
114
|
showToast,
|
|
116
115
|
commitPositionPatchToHtml: commitWithFreshQueue,
|
|
117
116
|
readOnlyPreview: false,
|