@hyperframes/studio 0.8.105 → 0.8.107
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-tnfCiVdR.js → hyperframes-player-h4mktSdm.js} +1 -1
- package/dist/assets/index-CM1xA_jD.js +394 -0
- package/dist/assets/{index-Ca6LZlVb.js → index-D8GKc2D9.js} +1 -1
- package/dist/assets/index-lrT-gfHa.css +1 -0
- package/dist/assets/{index-zpe332hb.js → index-zoqPmBF6.js} +1 -1
- package/dist/{chunk-QADHJFDF.js → chunk-4OZC426H.js} +13 -3
- package/dist/chunk-4OZC426H.js.map +1 -0
- package/dist/index.d.ts +381 -14
- package/dist/index.html +11 -2
- package/dist/index.js +9974 -9045
- package/dist/index.js.map +1 -1
- package/dist/player.js +1 -1
- package/dist/styles.css +1 -1
- package/package.json +9 -9
- package/src/App.tsx +5 -3
- package/src/agentToolsExport.test.ts +8 -0
- package/src/captions/keyboard.test.ts +12 -0
- package/src/captions/keyboard.ts +2 -1
- package/src/components/StudioHeader.dom.test.tsx +31 -9
- package/src/components/StudioHeader.tsx +4 -1
- package/src/components/StudioRightPanels.tsx +2 -0
- package/src/components/ThemeToggle.tsx +99 -0
- package/src/components/TimelineToolbar.test.tsx +27 -0
- package/src/components/TimelineToolbar.tsx +2 -5
- package/src/components/editor/DomEditOverlay.tsx +31 -5
- package/src/components/editor/PropertyPanel.tsx +1 -14
- package/src/components/editor/PropertyPanelFlat.tsx +3 -6
- package/src/components/editor/SnapToolbar.test.tsx +32 -1
- package/src/components/editor/SnapToolbar.tsx +3 -4
- package/src/components/editor/SourceEditor.tsx +2 -6
- package/src/components/editor/domEditOverlayStartGesture.ts +132 -117
- package/src/components/editor/fontAssets.test.ts +12 -22
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +338 -0
- package/src/components/editor/gsapAnimatesProperty.ts +3 -0
- package/src/components/editor/manualEdits.test.ts +4 -3
- package/src/components/editor/manualEditsDom.ts +58 -2
- package/src/components/editor/manualEditsDomPatches.ts +5 -1
- package/src/components/editor/manualOffsetDrag.ts +4 -1
- package/src/components/editor/marqueeCommit.test.tsx +69 -1
- package/src/components/editor/marqueeCommit.ts +6 -1
- package/src/components/editor/mediaAudioEdits.test.ts +212 -0
- package/src/components/editor/mediaAudioEdits.ts +253 -0
- package/src/components/editor/overlayFrameLoop.test.ts +27 -0
- package/src/components/editor/overlayFrameLoop.ts +21 -0
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +286 -87
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +17 -20
- package/src/components/editor/propertyPanelFlatProps.ts +1 -0
- package/src/components/editor/propertyPanelFont.test.tsx +80 -3
- package/src/components/editor/propertyPanelFont.tsx +69 -46
- package/src/components/editor/propertyPanelHelpers.ts +1 -2
- package/src/components/editor/propertyPanelMediaSection.tsx +17 -27
- package/src/components/editor/propertyPanelTypes.ts +2 -0
- package/src/components/editor/studioCodeTheme.test.ts +20 -0
- package/src/components/editor/studioCodeTheme.ts +99 -0
- package/src/components/editor/useDomEditNudge.test.tsx +165 -2
- package/src/components/editor/useDomEditNudge.ts +43 -25
- 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/panels/SlideshowPanel.test.ts +27 -0
- package/src/components/panels/SlideshowPanel.tsx +12 -7
- package/src/contexts/DomEditContext.test.tsx +62 -0
- package/src/contexts/DomEditContext.tsx +94 -77
- package/src/hooks/appHotkeysDispatch.ts +2 -1
- package/src/hooks/canvasQuickSaves.test.tsx +72 -7
- package/src/hooks/domEditCommitTypes.ts +10 -0
- package/src/hooks/domEditPersistFailure.ts +12 -1
- package/src/hooks/domStyleCommit.fonts.test.ts +5 -4
- package/src/hooks/domStyleCommit.ts +2 -8
- package/src/hooks/gsapDragCommit.ts +3 -8
- package/src/hooks/gsapDragPositionCommit.ts +6 -1
- package/src/hooks/gsapEditOutcome.ts +2 -1
- package/src/hooks/gsapResizeElementSize.test.tsx +136 -0
- package/src/hooks/gsapResizeGeometrySweep.test.ts +3 -1
- package/src/hooks/gsapResizeIntercept.test.ts +3 -11
- package/src/hooks/gsapResizeIntercept.ts +6 -2
- package/src/hooks/gsapResizeMixedTween.test.ts +1 -2
- package/src/hooks/gsapRuntimeBridge.test.ts +55 -0
- package/src/hooks/gsapRuntimeBridge.ts +5 -3
- package/src/hooks/gsapRuntimeKeyframes.ts +1 -3
- package/src/hooks/inspectorSaveReload.test.tsx +107 -0
- package/src/hooks/previewPromotionSelection.test.tsx +180 -0
- package/src/hooks/useAppHotkeys.previewForwarding.test.tsx +16 -1
- package/src/hooks/useAppHotkeys.test.ts +49 -0
- package/src/hooks/useAppHotkeys.textEditing.test.tsx +1 -1
- package/src/hooks/useAppHotkeys.ts +4 -5
- package/src/hooks/useAutomationSelectionKeyboard.test.tsx +17 -0
- package/src/hooks/useAutomationSelectionKeyboard.ts +3 -9
- package/src/hooks/useClipboard.source.test.ts +99 -19
- package/src/hooks/useClipboard.ts +145 -124
- package/src/hooks/useDomEditAttributeCommits.ts +71 -29
- package/src/hooks/useDomEditCommits.test.tsx +172 -92
- package/src/hooks/useDomEditCommits.ts +2 -0
- package/src/hooks/useDomEditCommitsHelpers.ts +4 -6
- package/src/hooks/useDomEditPersist.ts +28 -13
- package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
- package/src/hooks/useDomEditSession.test.tsx +76 -72
- package/src/hooks/useDomEditSession.ts +9 -46
- package/src/hooks/useDomEditTextCommits.ts +4 -8
- package/src/hooks/useDomGeometryCommits.ts +11 -2
- package/src/hooks/useEditHistoryActions.paint.test.tsx +122 -3
- package/src/hooks/useGsapAwareEditing.ts +6 -6
- package/src/hooks/useGsapScriptCommits.test.tsx +19 -0
- package/src/hooks/useGsapScriptCommits.ts +5 -4
- package/src/hooks/useGsapTweenCache.ts +1 -2
- package/src/hooks/useInlineTextEdit.test.tsx +27 -0
- package/src/hooks/useInlineTextEdit.ts +21 -5
- package/src/hooks/useKeyframeKeyboard.test.tsx +15 -0
- package/src/hooks/useKeyframeKeyboard.ts +2 -8
- package/src/hooks/useLivePreviewIframe.ts +18 -0
- package/src/hooks/usePreviewPersistence.history.test.tsx +47 -0
- package/src/hooks/usePreviewPersistence.ts +8 -5
- package/src/hooks/useRenderClipContent.ts +3 -3
- package/src/hooks/useTrackPendingTimelineEdit.ts +2 -6
- package/src/index.ts +9 -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/Timeline.test.ts +0 -21
- package/src/player/components/Timeline.tsx +0 -1
- package/src/player/components/TimelineClip.test.tsx +38 -0
- package/src/player/components/TimelineClip.tsx +14 -6
- package/src/player/components/TimelineClipFades.test.tsx +376 -74
- package/src/player/components/TimelineClipFades.tsx +368 -137
- package/src/player/components/timelineLayout.ts +0 -28
- package/src/player/components/useTimelineKeyboardActor.ts +1 -1
- package/src/player/hooks/timelinePlayerTestHarness.ts +27 -0
- package/src/player/hooks/usePlaybackKeyboard.test.ts +43 -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/playbackShortcuts.ts +3 -7
- package/src/player/lib/timelineElementHelpers.test.ts +16 -0
- package/src/player/lib/timelineElementHelpers.ts +16 -2
- package/src/player/lib/timelineViewportBudgets.test.ts +2 -0
- package/src/player/lib/timelineViewportBudgets.ts +4 -0
- package/src/player/sceneSwap.ts +18 -0
- package/src/styles/components.css +26 -0
- package/src/styles/studio.css +6 -0
- package/src/styles/theme.css +10 -0
- package/src/utils/clipboardPayload.ts +10 -1
- package/src/utils/gsapSoftReload.ts +3 -2
- package/src/utils/gsapUndoRestore.browser.test.ts +94 -0
- package/src/utils/gsapUndoRestore.test.ts +145 -1
- package/src/utils/gsapUndoRestore.ts +70 -21
- package/src/utils/sourcePatcher.ts +1 -1
- package/src/utils/studioFontHelpers.ts +2 -23
- package/src/utils/studioHelpers.test.ts +30 -0
- package/src/utils/studioHelpers.ts +18 -4
- package/src/utils/studioPendingEdits.test.ts +63 -0
- package/src/utils/studioPendingEdits.ts +37 -7
- 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 +21 -1
- package/src/utils/typingTarget.ts +8 -0
- package/src/webmcp/useStudioAgentTools.ts +3 -1
- package/dist/assets/index-Cre9X4RS.css +0 -1
- package/dist/assets/index-DEDhPmPD.js +0 -399
- package/dist/chunk-QADHJFDF.js.map +0 -1
|
@@ -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;
|
|
@@ -313,6 +333,7 @@ export function useClipboard({
|
|
|
313
333
|
clips,
|
|
314
334
|
sourceFile,
|
|
315
335
|
projectId: projectIdRef.current ?? undefined,
|
|
336
|
+
copiedInComposition: activeCompPath || "index.html",
|
|
316
337
|
};
|
|
317
338
|
});
|
|
318
339
|
}, [activeCompPath, findSelectedClips, readClips, showToast]);
|
|
@@ -369,136 +390,136 @@ export function useClipboard({
|
|
|
369
390
|
|
|
370
391
|
// Two independent paste modes (timeline clip vs DOM element) behind one guarded save.
|
|
371
392
|
// fallow-ignore-next-line complexity
|
|
372
|
-
const handlePaste = useCallback(
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const pid = projectIdRef.current;
|
|
379
|
-
if (!pid) return;
|
|
380
|
-
|
|
381
|
-
const targetPath = activeCompPath || "index.html";
|
|
382
|
-
try {
|
|
383
|
-
let pastedIds: string[] = [];
|
|
384
|
-
const fromThisFile = payload.sourceFile === targetPath && payload.projectId === pid;
|
|
385
|
-
const paste = (originalContent: string) => {
|
|
386
|
-
if (payload.kind !== "timeline-clip")
|
|
387
|
-
return pasteElementHtml(originalContent, payload, fromThisFile);
|
|
388
|
-
const { currentTime, elements } = usePlayerStore.getState();
|
|
389
|
-
const pasted = pasteTimelineClips(
|
|
390
|
-
originalContent,
|
|
391
|
-
payload.clips,
|
|
392
|
-
currentTime,
|
|
393
|
-
elements,
|
|
394
|
-
fromThisFile,
|
|
395
|
-
);
|
|
396
|
-
pastedIds = pasted.ids;
|
|
397
|
-
// A clip pasted past the current composition end would exist in the
|
|
398
|
-
// file but never appear on the timeline or in playback/export (the
|
|
399
|
-
// root's data-duration is what actually bounds the render).
|
|
400
|
-
return extendRootDurationInSource(pasted.content, pasted.requiredEnd);
|
|
401
|
-
};
|
|
402
|
-
|
|
403
|
-
const label =
|
|
404
|
-
payload.kind === "timeline-clip"
|
|
405
|
-
? clipLabel("Paste", payload.clips.length)
|
|
406
|
-
: "Paste element";
|
|
407
|
-
|
|
408
|
-
await saveProjectFilesWithHistory({
|
|
409
|
-
projectId: pid,
|
|
410
|
-
label,
|
|
411
|
-
files: { [targetPath]: paste },
|
|
412
|
-
readFile: (path) => readFileContent(pid, path),
|
|
413
|
-
writeFile: writeProjectFile,
|
|
414
|
-
recordEdit,
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
// CapCut: the pasted clip(s) become the selection; the playhead does not
|
|
418
|
-
// move. reloadPreview is a bare refresh-key bump with no selection
|
|
419
|
-
// snapshot of its own; calling setSelection before it is what makes the
|
|
420
|
-
// post-reload store state land on the pasted ids instead of stale ones.
|
|
421
|
-
if (pastedIds.length > 0) {
|
|
422
|
-
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;
|
|
423
399
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
+
};
|
|
436
424
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
+
);
|
|
455
452
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
};
|
|
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
|
+
);
|
|
468
474
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
+
};
|
|
477
487
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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;
|
|
484
511
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
readClips,
|
|
497
|
-
recordEdit,
|
|
498
|
-
reloadPreview,
|
|
499
|
-
showToast,
|
|
500
|
-
writeProjectFile,
|
|
501
|
-
]);
|
|
512
|
+
},
|
|
513
|
+
[
|
|
514
|
+
activeCompPath,
|
|
515
|
+
findSelectedClips,
|
|
516
|
+
readClips,
|
|
517
|
+
recordEdit,
|
|
518
|
+
reloadPreview,
|
|
519
|
+
showToast,
|
|
520
|
+
writeProjectFile,
|
|
521
|
+
],
|
|
522
|
+
);
|
|
502
523
|
|
|
503
524
|
const handleCut = useCallback(async (): Promise<boolean> => {
|
|
504
525
|
const selected = getSelectedElements();
|
|
@@ -5,8 +5,11 @@ import {
|
|
|
5
5
|
getDomEditTargetKey,
|
|
6
6
|
type DomEditSelection,
|
|
7
7
|
} from "../components/editor/domEditing";
|
|
8
|
-
import type { PersistDomEditOperations } from "./domEditCommitTypes";
|
|
9
|
-
import {
|
|
8
|
+
import type { CommitDomAttributeBatch, PersistDomEditOperations } from "./domEditCommitTypes";
|
|
9
|
+
import {
|
|
10
|
+
DomEditPersistPreparedWriteError,
|
|
11
|
+
reportDomEditPersistFailure,
|
|
12
|
+
} from "./domEditPersistFailure";
|
|
10
13
|
import { bumpDomEditCommitMapVersion, runDomEditCommit } from "./domEditCommitRunner";
|
|
11
14
|
import { syncStoredAutomationFromPreview } from "../player/lib/automationStoreSync";
|
|
12
15
|
import { HF_AUDIO_GROUP_ATTR, HF_AUDIO_GROUP_TAG } from "@hyperframes/core/audio-groups";
|
|
@@ -54,6 +57,15 @@ export function resolveOptimisticAttributeValue(attr: string, value: string | nu
|
|
|
54
57
|
return value === "false" && HTML_BOOLEAN_ATTRIBUTES.has(attr) ? null : value;
|
|
55
58
|
}
|
|
56
59
|
|
|
60
|
+
function previewAttributeName(op: PatchOperation): string {
|
|
61
|
+
return op.type === "attribute" ? resolveFullAttrName(op.property, true) : op.property;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function previewAttributeValue(op: PatchOperation): string | null {
|
|
65
|
+
if (op.type === "html-attribute") return resolveOptimisticAttributeValue(op.property, op.value);
|
|
66
|
+
return op.value === "" ? null : op.value;
|
|
67
|
+
}
|
|
68
|
+
|
|
57
69
|
function setOrRemovePreviewAttribute(
|
|
58
70
|
el: HTMLElement,
|
|
59
71
|
fullAttr: string,
|
|
@@ -209,7 +221,7 @@ export function useDomEditAttributeCommits({
|
|
|
209
221
|
],
|
|
210
222
|
);
|
|
211
223
|
|
|
212
|
-
// Commits several data-* attributes on the SAME element in ONE persist call
|
|
224
|
+
// Commits several data-* and HTML attributes on the SAME element in ONE persist call
|
|
213
225
|
// — needed when two attributes together describe a single logical value
|
|
214
226
|
// (e.g. a pinned timing range's start+duration): committing them through two
|
|
215
227
|
// separate sequential `commitDataAttribute` calls leaves a window where the
|
|
@@ -221,32 +233,30 @@ export function useDomEditAttributeCommits({
|
|
|
221
233
|
// explicit, caller-supplied `selection` (not the "current" one) closes both
|
|
222
234
|
// gaps — matching `onCommitAnimatedProperties`'s same-shaped fix for GSAP
|
|
223
235
|
// property batches.
|
|
224
|
-
const
|
|
236
|
+
const commitAttributeOps = useCallback(
|
|
225
237
|
async (
|
|
226
238
|
selection: DomEditSelection,
|
|
227
|
-
|
|
228
|
-
options: DataAttributeCommitOptions
|
|
229
|
-
|
|
239
|
+
ops: PatchOperation[],
|
|
240
|
+
options: DataAttributeCommitOptions & {
|
|
241
|
+
prepareContent?: (html: string) => string;
|
|
242
|
+
syncAutomation?: boolean;
|
|
243
|
+
},
|
|
244
|
+
): Promise<boolean> => {
|
|
230
245
|
const iframe = previewIframeRef.current;
|
|
231
|
-
const entries =
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
value,
|
|
246
|
+
const entries = ops.map((op) => ({
|
|
247
|
+
fullAttr: previewAttributeName(op),
|
|
248
|
+
value: previewAttributeValue(op),
|
|
235
249
|
}));
|
|
236
|
-
const commitKey = `${options.coalescePrefix}:${
|
|
237
|
-
.map((
|
|
250
|
+
const commitKey = `${options.coalescePrefix}:${ops
|
|
251
|
+
.map((op) => op.property)
|
|
238
252
|
.sort()
|
|
239
253
|
.join(",")}:${getDomEditTargetKey(selection)}`;
|
|
240
254
|
const isLatestCommit = bumpDomEditCommitMapVersion(
|
|
241
255
|
domAttributeCommitVersionRef.current,
|
|
242
256
|
commitKey,
|
|
243
257
|
);
|
|
244
|
-
const ops: PatchOperation[] = entries.map((entry) => ({
|
|
245
|
-
type: "attribute",
|
|
246
|
-
property: entry.attr,
|
|
247
|
-
value: entry.value,
|
|
248
|
-
}));
|
|
249
258
|
let captured: CapturedMultiAttributeElement | null = null;
|
|
259
|
+
let landed = false;
|
|
250
260
|
|
|
251
261
|
await runDomEditCommit({
|
|
252
262
|
capture: () => {
|
|
@@ -260,8 +270,7 @@ export function useDomEditAttributeCommits({
|
|
|
260
270
|
apply: () => {
|
|
261
271
|
if (!captured) return;
|
|
262
272
|
for (const entry of entries) {
|
|
263
|
-
|
|
264
|
-
setOrRemovePreviewAttribute(captured.element, entry.fullAttr, nextValue);
|
|
273
|
+
setOrRemovePreviewAttribute(captured.element, entry.fullAttr, entry.value);
|
|
265
274
|
}
|
|
266
275
|
},
|
|
267
276
|
persist: () =>
|
|
@@ -269,8 +278,10 @@ export function useDomEditAttributeCommits({
|
|
|
269
278
|
label: options.label,
|
|
270
279
|
coalesceKey: commitKey,
|
|
271
280
|
skipRefresh: options.skipRefresh,
|
|
281
|
+
prepareContent: options.prepareContent,
|
|
272
282
|
}),
|
|
273
|
-
shouldRevert: () =>
|
|
283
|
+
shouldRevert: (error) =>
|
|
284
|
+
isLatestCommit() && !(error instanceof DomEditPersistPreparedWriteError),
|
|
274
285
|
revert: () => {
|
|
275
286
|
if (!captured) return;
|
|
276
287
|
for (const entry of entries) {
|
|
@@ -283,10 +294,19 @@ export function useDomEditAttributeCommits({
|
|
|
283
294
|
},
|
|
284
295
|
onError: (error) => reportDomEditPersistFailure(selection, ops, error, showToast),
|
|
285
296
|
shouldResync: () => isLatestCommit() && !!options.refreshAfter,
|
|
286
|
-
resync: () =>
|
|
287
|
-
|
|
297
|
+
resync: () => {
|
|
298
|
+
refreshDomEditSelectionFromPreview(selection);
|
|
299
|
+
if (options.syncAutomation) {
|
|
300
|
+
syncStoredAutomationFromPreview(previewIframeRef.current?.contentDocument ?? null);
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
onSettled: (ok) => {
|
|
304
|
+
landed = ok;
|
|
305
|
+
options.onSettled?.(ok);
|
|
306
|
+
},
|
|
288
307
|
onFinally: isLatestCommit.release,
|
|
289
308
|
});
|
|
309
|
+
return landed;
|
|
290
310
|
},
|
|
291
311
|
[
|
|
292
312
|
activeCompPath,
|
|
@@ -299,14 +319,35 @@ export function useDomEditAttributeCommits({
|
|
|
299
319
|
|
|
300
320
|
const handleDomAttributesCommit = useCallback(
|
|
301
321
|
async (selection: DomEditSelection, attrs: Record<string, string>) => {
|
|
302
|
-
await
|
|
303
|
-
|
|
304
|
-
|
|
322
|
+
await commitAttributeOps(
|
|
323
|
+
selection,
|
|
324
|
+
Object.entries(attrs).map(([property, value]) => ({
|
|
325
|
+
type: "attribute",
|
|
326
|
+
property,
|
|
327
|
+
value,
|
|
328
|
+
})),
|
|
329
|
+
{
|
|
330
|
+
label: "Edit timing",
|
|
331
|
+
coalescePrefix: "attrs",
|
|
332
|
+
skipRefresh: false,
|
|
333
|
+
refreshAfter: true,
|
|
334
|
+
},
|
|
335
|
+
);
|
|
336
|
+
},
|
|
337
|
+
[commitAttributeOps],
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
const handleDomAttributeBatchCommit: CommitDomAttributeBatch = useCallback(
|
|
341
|
+
(selection, ops, options) =>
|
|
342
|
+
commitAttributeOps(selection, ops, {
|
|
343
|
+
label: options.label,
|
|
344
|
+
coalescePrefix: "attr-batch",
|
|
305
345
|
skipRefresh: false,
|
|
306
346
|
refreshAfter: true,
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
347
|
+
syncAutomation: true,
|
|
348
|
+
prepareContent: options.prepareContent,
|
|
349
|
+
}),
|
|
350
|
+
[commitAttributeOps],
|
|
310
351
|
);
|
|
311
352
|
|
|
312
353
|
const handleDomAttributeCommit = useCallback(
|
|
@@ -438,5 +479,6 @@ export function useDomEditAttributeCommits({
|
|
|
438
479
|
handleDomAttributeQuietCommit,
|
|
439
480
|
handleDomHtmlAttributeCommit,
|
|
440
481
|
handleDomAttributesCommit,
|
|
482
|
+
handleDomAttributeBatchCommit,
|
|
441
483
|
};
|
|
442
484
|
}
|