@hyperframes/studio 0.7.57 → 0.7.59
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--Z69cEkE.js → hyperframes-player-Bm07FLkl.js} +1 -1
- package/dist/assets/{index-KsfE1bUu.js → index-B995FG46.js} +1 -1
- package/dist/assets/index-CrkAdJkb.js +426 -0
- package/dist/assets/index-Dj5p8U_A.css +1 -0
- package/dist/assets/{index-Bf1x1y8H.js → index-FvzmPhfG.js} +1 -1
- package/dist/{chunk-5QSIMBEJ.js → chunk-OBAG3GWK.js} +33 -21
- package/dist/chunk-OBAG3GWK.js.map +1 -0
- package/dist/{domEditingLayers-6LQGKPOI.js → domEditingLayers-2CKWGEHS.js} +2 -2
- package/dist/index.d.ts +27 -7
- package/dist/index.html +2 -2
- package/dist/index.js +18029 -11563
- package/dist/index.js.map +1 -1
- package/dist/styles/tailwind-preset.d.ts +5 -0
- package/dist/styles/tailwind-preset.js +8 -1
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +6 -6
- package/src/components/DesignPanelPromoteProvider.tsx +15 -2
- package/src/components/StudioFeedbackBar.tsx +2 -2
- package/src/components/StudioRightPanel.tsx +29 -51
- package/src/components/editor/AnimationCard.test.tsx +134 -0
- package/src/components/editor/AnimationCard.tsx +19 -4
- package/src/components/editor/ArcPathControls.tsx +1 -0
- package/src/components/editor/CanvasContextMenu.test.tsx +27 -14
- package/src/components/editor/CanvasContextMenu.tsx +71 -6
- package/src/components/editor/DomEditOverlay.tsx +5 -2
- package/src/components/editor/GestureRecordControl.tsx +7 -1
- package/src/components/editor/GsapAnimationSection.tsx +109 -14
- package/src/components/editor/InspectorHeaderActions.tsx +42 -4
- package/src/components/editor/LayersPanel.tsx +110 -99
- package/src/components/editor/PropertyPanel.test.tsx +948 -0
- package/src/components/editor/PropertyPanel.tsx +144 -148
- package/src/components/editor/PropertyPanelEmptyState.test.tsx +74 -0
- package/src/components/editor/PropertyPanelEmptyState.tsx +179 -1
- package/src/components/editor/PropertyPanelFlat.tsx +580 -0
- package/src/components/editor/PropertyPanelFlatFooter.test.tsx +83 -0
- package/src/components/editor/PropertyPanelFlatFooter.tsx +73 -0
- package/src/components/editor/PropertyPanelFlatHeader.test.tsx +79 -0
- package/src/components/editor/PropertyPanelFlatHeader.tsx +112 -0
- package/src/components/editor/canvasContextMenuZOrder.test.ts +57 -0
- package/src/components/editor/canvasContextMenuZOrder.ts +40 -1
- package/src/components/editor/domEditingDom.ts +5 -6
- package/src/components/editor/domEditingElement.ts +15 -32
- package/src/components/editor/gsapAnimationCallbacks.ts +27 -0
- package/src/components/editor/gsapLivePreview.ts +34 -0
- package/src/components/editor/layersPanelSort.ts +80 -0
- package/src/components/editor/manualEditingAvailability.test.ts +10 -0
- package/src/components/editor/manualEditingAvailability.ts +9 -0
- package/src/components/editor/propertyPanel3dTransform.tsx +5 -0
- package/src/components/editor/propertyPanelColor.test.tsx +28 -0
- package/src/components/editor/propertyPanelColor.tsx +31 -1
- package/src/components/editor/propertyPanelColorGradingControls.tsx +11 -2
- package/src/components/editor/propertyPanelColorGradingSection.tsx +45 -365
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +19 -5
- package/src/components/editor/propertyPanelFill.tsx +22 -5
- package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +622 -0
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +570 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.test.tsx +293 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.tsx +383 -0
- package/src/components/editor/propertyPanelFlatMaskInsetRows.tsx +102 -0
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +436 -0
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +286 -0
- package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +272 -0
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +315 -0
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +1174 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +563 -0
- package/src/components/editor/propertyPanelFlatSelectRow.tsx +101 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +23 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.ts +27 -0
- package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +598 -0
- package/src/components/editor/propertyPanelFlatStyleSections.tsx +516 -0
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +465 -0
- package/src/components/editor/propertyPanelFlatTextSection.tsx +407 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.test.ts +71 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.ts +59 -0
- package/src/components/editor/propertyPanelFlatToggle.test.tsx +63 -0
- package/src/components/editor/propertyPanelFlatToggle.tsx +53 -0
- package/src/components/editor/propertyPanelFont.test.tsx +30 -0
- package/src/components/editor/propertyPanelFont.tsx +136 -95
- package/src/components/editor/propertyPanelHelpers.ts +73 -0
- package/src/components/editor/propertyPanelInputCoverage.test.tsx +587 -0
- package/src/components/editor/propertyPanelMediaSection.tsx +10 -0
- package/src/components/editor/propertyPanelPrimitives.tsx +60 -29
- package/src/components/editor/propertyPanelSections.test.tsx +161 -0
- package/src/components/editor/propertyPanelSections.tsx +154 -90
- package/src/components/editor/propertyPanelStyleSections.tsx +12 -1
- package/src/components/editor/propertyPanelTimingSection.tsx +1 -1
- package/src/components/editor/propertyPanelTypes.ts +14 -1
- package/src/components/editor/propertyPanelValueTier.test.ts +32 -0
- package/src/components/editor/propertyPanelValueTier.ts +28 -0
- package/src/components/editor/useColorGradingController.test.ts +418 -0
- package/src/components/editor/useColorGradingController.ts +596 -0
- package/src/components/editor/useLayerRevealOverride.test.ts +169 -0
- package/src/components/editor/useLayerRevealOverride.ts +389 -0
- package/src/components/nle/PreviewOverlays.tsx +27 -2
- package/src/components/nle/TimelinePane.test.ts +3 -0
- package/src/components/nle/TimelinePane.tsx +11 -1
- package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +440 -0
- package/src/components/nle/useCanvasZOrderTimelineMirror.ts +191 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +1 -0
- package/src/components/nle/zLaneGesture.test.ts +176 -0
- package/src/components/nle/zLaneGesture.ts +77 -0
- package/src/components/panels/VariablesPanel.tsx +4 -0
- package/src/components/sidebar/CompositionsTab.test.ts +28 -2
- package/src/components/sidebar/CompositionsTab.tsx +3 -1
- package/src/components/storyboard/FramePoster.tsx +24 -4
- package/src/components/storyboard/StoryboardFrameFocus.tsx +4 -0
- package/src/components/storyboard/StoryboardFrameTile.tsx +30 -1
- package/src/components/storyboard/StoryboardGrid.tsx +23 -1
- package/src/components/storyboard/StoryboardLoaded.tsx +58 -2
- package/src/components/storyboard/StoryboardView.tsx +9 -2
- package/src/components/storyboard/frameComments.test.ts +95 -0
- package/src/components/storyboard/frameComments.ts +115 -0
- package/src/components/storyboard/useFrameComments.ts +82 -0
- package/src/contexts/DesignPanelInputContext.test.tsx +81 -0
- package/src/contexts/DesignPanelInputContext.tsx +48 -0
- package/src/contexts/DomEditContext.tsx +4 -0
- package/src/contexts/VariablePromoteContext.test.tsx +67 -0
- package/src/contexts/VariablePromoteContext.tsx +10 -5
- package/src/hooks/domEditCommitRunner.ts +11 -0
- package/src/hooks/domEditCommitTypes.ts +17 -1
- package/src/hooks/elementLifecycleOpsTestUtils.ts +25 -0
- package/src/hooks/fetchStubTestUtils.ts +16 -0
- package/src/hooks/gsapScriptCommitTypes.ts +6 -4
- package/src/hooks/timelineEditingHelpers.test.ts +92 -1
- package/src/hooks/timelineEditingHelpers.ts +32 -15
- package/src/hooks/timelineMoveAdapter.ts +2 -1
- package/src/hooks/timelineTimingSync.test.ts +703 -33
- package/src/hooks/timelineTimingSync.ts +282 -97
- package/src/hooks/timelineTrackVisibility.test.ts +55 -1
- package/src/hooks/timelineTrackVisibility.ts +31 -13
- package/src/hooks/useAppHotkeys.ts +9 -0
- package/src/hooks/useClipboard.ts +19 -17
- package/src/hooks/useDomEditAttributeCommits.ts +123 -1
- package/src/hooks/useDomEditCommits.test.tsx +316 -38
- package/src/hooks/useDomEditCommits.ts +81 -162
- package/src/hooks/useDomEditCommitsHelpers.ts +158 -0
- package/src/hooks/useDomEditSession.test.tsx +3 -1
- package/src/hooks/useDomEditSession.ts +18 -6
- package/src/hooks/useDomEditTextCommits.ts +14 -9
- package/src/hooks/useDomEditWiring.ts +10 -9
- package/src/hooks/useElementLifecycleOps.test.tsx +426 -60
- package/src/hooks/useElementLifecycleOps.ts +182 -84
- package/src/hooks/useFileManager.projectOwnership.test.tsx +98 -0
- package/src/hooks/useFileManager.ts +82 -60
- package/src/hooks/useGsapAnimationOps.ts +7 -6
- package/src/hooks/useGsapKeyframeOps.ts +6 -5
- package/src/hooks/useGsapPropertyDebounce.ts +43 -28
- package/src/hooks/useGsapPropertyDebounceFlush.test.ts +35 -0
- package/src/hooks/useGsapScriptCommits.test.tsx +48 -2
- package/src/hooks/useGsapScriptCommits.ts +150 -50
- package/src/hooks/useGsapSelectionHandlers.test.tsx +116 -0
- package/src/hooks/useGsapSelectionHandlers.ts +94 -31
- package/src/hooks/useInspectorSplitResize.ts +51 -0
- package/src/hooks/usePersistentEditHistory.projectOwnership.test.tsx +60 -0
- package/src/hooks/usePersistentEditHistory.test.ts +49 -1
- package/src/hooks/usePersistentEditHistory.ts +71 -31
- package/src/hooks/usePreviewPersistence.ts +2 -2
- package/src/hooks/useProjectCompositionVariables.ts +10 -10
- package/src/hooks/useProjectSignaturePoll.test.tsx +135 -0
- package/src/hooks/useProjectSignaturePoll.ts +68 -0
- package/src/hooks/useSdkSession.lifecycle.test.tsx +137 -0
- package/src/hooks/useSdkSession.ts +155 -14
- package/src/hooks/useSlideshowPersist.ts +7 -1
- package/src/hooks/useStoryboard.ts +23 -4
- package/src/hooks/useTimelineEditing.test.tsx +306 -155
- package/src/hooks/useTimelineEditing.ts +111 -101
- package/src/hooks/useTimelineEditingTypes.ts +4 -1
- package/src/hooks/useTimelineGroupEditing.ts +51 -5
- package/src/hooks/useVariablesPersist.ts +9 -7
- package/src/icons/SystemIcons.tsx +2 -0
- package/src/player/components/Timeline.test.ts +31 -21
- package/src/player/components/Timeline.tsx +62 -91
- package/src/player/components/TimelineCanvas.tsx +36 -5
- package/src/player/components/TimelineLanes.tsx +32 -10
- package/src/player/components/TimelineOverlays.tsx +36 -0
- package/src/player/components/TimelineRuler.tsx +18 -5
- package/src/player/components/TrackGapContextMenu.tsx +116 -0
- package/src/player/components/timelineCallbacks.ts +4 -1
- package/src/player/components/timelineClipDragCommit.test.ts +279 -3
- package/src/player/components/timelineClipDragCommit.ts +173 -71
- package/src/player/components/timelineGapCommit.test.ts +163 -0
- package/src/player/components/timelineGapCommit.ts +103 -0
- package/src/player/components/timelineGaps.test.ts +221 -0
- package/src/player/components/timelineGaps.ts +182 -0
- package/src/player/components/timelineLayout.test.ts +2 -1
- package/src/player/components/timelineLayout.ts +22 -6
- package/src/player/components/timelineMarquee.test.ts +20 -10
- package/src/player/components/timelineMarquee.ts +9 -2
- package/src/player/components/timelineStackingSync.test.ts +42 -1
- package/src/player/components/timelineStackingSync.ts +31 -16
- package/src/player/components/timelineZMirror.test.ts +667 -0
- package/src/player/components/timelineZMirror.ts +393 -0
- package/src/player/components/timelineZoom.test.ts +2 -2
- package/src/player/components/timelineZoom.ts +7 -3
- package/src/player/components/useTimelineClipDrag.ts +1 -1
- package/src/player/components/useTimelineGapHighlights.test.ts +119 -0
- package/src/player/components/useTimelineGapHighlights.ts +110 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -3
- package/src/player/components/useTimelineRangeSelection.ts +6 -4
- package/src/player/components/useTimelineScrollViewport.ts +73 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +0 -1
- package/src/player/components/useTimelineStackingSync.ts +5 -3
- package/src/player/components/useTimelineTrackDerivations.ts +38 -0
- package/src/player/components/useTrackGapMenu.ts +155 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +67 -4
- package/src/player/hooks/useExpandedTimelineElements.ts +12 -1
- package/src/player/hooks/useTimelineSyncCallbacks.ts +2 -0
- package/src/player/lib/layerOrdering.test.ts +1 -15
- package/src/player/lib/layerOrdering.ts +5 -28
- package/src/player/lib/timelineDOM.test.ts +72 -0
- package/src/player/lib/timelineDOM.ts +3 -0
- package/src/player/lib/timelineElementHelpers.ts +35 -11
- package/src/player/store/playerStore.ts +12 -1
- package/src/styles/studio.css +22 -0
- package/src/styles/tailwind-preset.shared.js +7 -0
- package/src/telemetry/events.test.ts +10 -0
- package/src/telemetry/events.ts +1 -0
- package/src/utils/designInputTracking.test.ts +97 -0
- package/src/utils/designInputTracking.ts +80 -0
- package/src/utils/domEditSaveQueue.ts +4 -3
- package/src/utils/editHistory.test.ts +82 -0
- package/src/utils/editHistory.ts +14 -2
- package/src/utils/gsapSoftReload.test.ts +54 -115
- package/src/utils/gsapSoftReload.ts +42 -152
- package/src/utils/gsapUndoRestore.test.ts +269 -0
- package/src/utils/gsapUndoRestore.ts +269 -0
- package/src/utils/sdkCutover.gate.test.ts +7 -7
- package/src/utils/sdkCutover.test.ts +490 -57
- package/src/utils/sdkCutover.ts +181 -219
- package/src/utils/sdkEditTransaction.ts +292 -0
- package/src/utils/sdkResolverAttempts.ts +94 -0
- package/src/utils/sdkResolverShadow.test.ts +148 -0
- package/src/utils/sdkResolverShadow.ts +139 -106
- package/src/utils/setSlideshowManifest.test.ts +0 -9
- package/src/utils/setSlideshowManifest.ts +20 -27
- package/src/utils/sourceScopedSelectorIndex.ts +29 -0
- package/src/utils/studioFileHistory.test.ts +41 -0
- package/src/utils/studioFileHistory.ts +30 -27
- package/src/utils/studioFileMutationCoordinator.ts +45 -0
- package/src/utils/studioHelpers.test.ts +45 -0
- package/src/utils/studioHelpers.ts +45 -14
- package/src/utils/studioSaveDiagnostics.ts +17 -0
- package/dist/assets/index-DfmYkU44.js +0 -423
- package/dist/assets/index-_pqzyxB1.css +0 -1
- package/dist/chunk-5QSIMBEJ.js.map +0 -1
- /package/dist/{domEditingLayers-6LQGKPOI.js.map → domEditingLayers-2CKWGEHS.js.map} +0 -0
|
@@ -33,7 +33,8 @@ interface ToggleTimelineTrackHiddenInput {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
interface ToggleTimelineElementHiddenInput extends Omit<ToggleTimelineTrackHiddenInput, "track"> {
|
|
36
|
-
|
|
36
|
+
/** One timeline key, or several to hide/show in a single atomic file write. */
|
|
37
|
+
elementKey: string | readonly string[];
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
interface SetElementsHiddenInput {
|
|
@@ -62,7 +63,7 @@ interface UseTimelineTrackVisibilityEditingInput extends Omit<
|
|
|
62
63
|
|
|
63
64
|
interface UseTimelineElementVisibilityEditingInput extends Omit<
|
|
64
65
|
ToggleTimelineElementHiddenInput,
|
|
65
|
-
"projectId" | "elementKey" | "hidden" | "previewIframe"
|
|
66
|
+
"projectId" | "elementKey" | "hidden" | "previewIframe" | "timelineElements"
|
|
66
67
|
> {
|
|
67
68
|
projectIdRef: ReadonlyRef<string | null>;
|
|
68
69
|
previewIframeRef: ReadonlyRef<HTMLIFrameElement | null>;
|
|
@@ -82,9 +83,10 @@ function patchLiveHiddenState(
|
|
|
82
83
|
iframe: HTMLIFrameElement | null,
|
|
83
84
|
elements: readonly TimelineElement[],
|
|
84
85
|
hidden: boolean,
|
|
86
|
+
activeCompPath: string | null,
|
|
85
87
|
): void {
|
|
86
88
|
for (const element of elements) {
|
|
87
|
-
const target = findTimelineElementInIframe(iframe, element);
|
|
89
|
+
const target = findTimelineElementInIframe(iframe, element, activeCompPath);
|
|
88
90
|
if (!target) continue;
|
|
89
91
|
if (hidden) {
|
|
90
92
|
target.setAttribute("data-hidden", "");
|
|
@@ -134,7 +136,7 @@ async function setElementsHidden({
|
|
|
134
136
|
}: SetElementsHiddenInput): Promise<string[]> {
|
|
135
137
|
if (elements.length === 0) return [];
|
|
136
138
|
|
|
137
|
-
patchLiveHiddenState(previewIframe, elements, hidden);
|
|
139
|
+
patchLiveHiddenState(previewIframe, elements, hidden, activeCompPath);
|
|
138
140
|
reseekPreviewRuntime(previewIframe);
|
|
139
141
|
|
|
140
142
|
const hiddenOperation: PatchOperation = {
|
|
@@ -188,7 +190,7 @@ async function setElementsHidden({
|
|
|
188
190
|
// The optimistic live patch already ran; a patch-target/save failure here would
|
|
189
191
|
// otherwise leave the preview showing the wrong visibility until a reload. Revert
|
|
190
192
|
// the live DOM to the prior state so what's on screen matches what persisted.
|
|
191
|
-
patchLiveHiddenState(previewIframe, elements, !hidden);
|
|
193
|
+
patchLiveHiddenState(previewIframe, elements, !hidden, activeCompPath);
|
|
192
194
|
reseekPreviewRuntime(previewIframe);
|
|
193
195
|
throw error;
|
|
194
196
|
}
|
|
@@ -232,13 +234,21 @@ export async function toggleTimelineElementHidden({
|
|
|
232
234
|
domEditSaveTimestampRef,
|
|
233
235
|
pendingTimelineEditPathRef,
|
|
234
236
|
}: ToggleTimelineElementHiddenInput): Promise<string[]> {
|
|
235
|
-
const
|
|
237
|
+
const keys = new Set(typeof elementKey === "string" ? [elementKey] : elementKey);
|
|
238
|
+
const elements = timelineElements.filter((item) => keys.has(item.key ?? item.id));
|
|
236
239
|
return setElementsHidden({
|
|
237
240
|
projectId,
|
|
238
241
|
activeCompPath,
|
|
239
|
-
elements
|
|
242
|
+
elements,
|
|
240
243
|
hidden,
|
|
241
|
-
label:
|
|
244
|
+
label:
|
|
245
|
+
elements.length > 1
|
|
246
|
+
? hidden
|
|
247
|
+
? `Hide ${elements.length} elements`
|
|
248
|
+
: `Show ${elements.length} elements`
|
|
249
|
+
: hidden
|
|
250
|
+
? "Hide element"
|
|
251
|
+
: "Show element",
|
|
242
252
|
previewIframe,
|
|
243
253
|
writeProjectFile,
|
|
244
254
|
recordEdit,
|
|
@@ -312,7 +322,6 @@ export function useTimelineTrackVisibilityEditing({
|
|
|
312
322
|
export function useTimelineElementVisibilityEditing({
|
|
313
323
|
projectIdRef,
|
|
314
324
|
activeCompPath,
|
|
315
|
-
timelineElements,
|
|
316
325
|
showToast,
|
|
317
326
|
writeProjectFile,
|
|
318
327
|
recordEdit,
|
|
@@ -322,11 +331,20 @@ export function useTimelineElementVisibilityEditing({
|
|
|
322
331
|
isRecordingRef,
|
|
323
332
|
forceReloadSdkSession,
|
|
324
333
|
}: UseTimelineElementVisibilityEditingInput): (
|
|
325
|
-
elementKey: string,
|
|
334
|
+
elementKey: string | readonly string[],
|
|
326
335
|
hidden: boolean,
|
|
327
336
|
) => Promise<void> {
|
|
337
|
+
// Resolve against the EXPANDED rows, not the raw store list — a nested
|
|
338
|
+
// sub-composition child has no entry of its own in the raw list (only its
|
|
339
|
+
// host does), so an elementKey for such a child (the
|
|
340
|
+
// `sourceFile#domId`-shaped virtual key `resolveTimelineIdForSelection`
|
|
341
|
+
// falls back to) would never match anything there and Hide All would
|
|
342
|
+
// silently no-op for it. The expanded list synthesizes a real, patchable
|
|
343
|
+
// TimelineElement (with matching key/domId/sourceFile) for each visible
|
|
344
|
+
// child whenever its host is currently expanded.
|
|
345
|
+
const expandedElements = useExpandedTimelineElements();
|
|
328
346
|
return useCallback(
|
|
329
|
-
async (elementKey: string, hidden: boolean) => {
|
|
347
|
+
async (elementKey: string | readonly string[], hidden: boolean) => {
|
|
330
348
|
if (isRecordingRef?.current) {
|
|
331
349
|
showToast("Cannot edit timeline while recording", "error");
|
|
332
350
|
return;
|
|
@@ -337,7 +355,7 @@ export function useTimelineElementVisibilityEditing({
|
|
|
337
355
|
await toggleTimelineElementHidden({
|
|
338
356
|
projectId: pid,
|
|
339
357
|
activeCompPath,
|
|
340
|
-
timelineElements,
|
|
358
|
+
timelineElements: expandedElements,
|
|
341
359
|
elementKey,
|
|
342
360
|
hidden,
|
|
343
361
|
previewIframe: previewIframeRef.current,
|
|
@@ -356,7 +374,7 @@ export function useTimelineElementVisibilityEditing({
|
|
|
356
374
|
},
|
|
357
375
|
[
|
|
358
376
|
activeCompPath,
|
|
359
|
-
|
|
377
|
+
expandedElements,
|
|
360
378
|
previewIframeRef,
|
|
361
379
|
writeProjectFile,
|
|
362
380
|
recordEdit,
|
|
@@ -9,6 +9,7 @@ import { shouldIgnoreHistoryShortcut } from "../utils/studioHelpers";
|
|
|
9
9
|
import { canSplitElement } from "../utils/timelineElementSplit";
|
|
10
10
|
import { STUDIO_RAZOR_TOOL_ENABLED } from "../components/editor/manualEditingAvailability";
|
|
11
11
|
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
12
|
+
import { serializeStudioFileMutations } from "../utils/studioFileMutationCoordinator";
|
|
12
13
|
|
|
13
14
|
function iframeContentWindow(iframe: HTMLIFrameElement | null): Window | null {
|
|
14
15
|
try {
|
|
@@ -87,6 +88,7 @@ interface HistoryResult {
|
|
|
87
88
|
interface HistoryFileCallbacks {
|
|
88
89
|
readFile: (path: string) => Promise<string>;
|
|
89
90
|
writeFile: (path: string, content: string) => Promise<void>;
|
|
91
|
+
serialize?: <T>(paths: readonly string[], task: () => Promise<T>) => Promise<T>;
|
|
90
92
|
}
|
|
91
93
|
interface EditHistoryHandle {
|
|
92
94
|
undo: (cb: HistoryFileCallbacks) => Promise<HistoryResult>;
|
|
@@ -367,6 +369,11 @@ export function useAppHotkeys({
|
|
|
367
369
|
},
|
|
368
370
|
[domEditSaveTimestampRef, writeProjectFile],
|
|
369
371
|
);
|
|
372
|
+
const serializeHistoryFiles = useCallback(
|
|
373
|
+
<T>(paths: readonly string[], task: () => Promise<T>) =>
|
|
374
|
+
serializeStudioFileMutations(writeProjectFile, paths, task),
|
|
375
|
+
[writeProjectFile],
|
|
376
|
+
);
|
|
370
377
|
|
|
371
378
|
const applyHistory = useCallback(
|
|
372
379
|
async (direction: "undo" | "redo") => {
|
|
@@ -377,6 +384,7 @@ export function useAppHotkeys({
|
|
|
377
384
|
const result = await editHistory[direction]({
|
|
378
385
|
readFile: readHistoryFile,
|
|
379
386
|
writeFile: writeHistoryFile,
|
|
387
|
+
serialize: serializeHistoryFiles,
|
|
380
388
|
});
|
|
381
389
|
if (!result.ok && result.reason === "content-mismatch") {
|
|
382
390
|
showToast(
|
|
@@ -406,6 +414,7 @@ export function useAppHotkeys({
|
|
|
406
414
|
syncHistoryPreviewAfterApply,
|
|
407
415
|
waitForPendingDomEditSaves,
|
|
408
416
|
writeHistoryFile,
|
|
417
|
+
serializeHistoryFiles,
|
|
409
418
|
onAfterUndoRedo,
|
|
410
419
|
activeCompPath,
|
|
411
420
|
forceReloadSdkSession,
|
|
@@ -8,6 +8,7 @@ import { insertTimelineAssetIntoSource } from "../utils/timelineAssetDrop";
|
|
|
8
8
|
import { saveProjectFilesWithHistory } from "../utils/studioFileHistory";
|
|
9
9
|
import type { EditHistoryKind } from "../utils/editHistory";
|
|
10
10
|
import { formatTimelineAttributeNumber } from "../player/components/timelineEditing";
|
|
11
|
+
import { findElementForSelection } from "../components/editor/domEditingElement";
|
|
11
12
|
import { readFileContent } from "./timelineEditingHelpers";
|
|
12
13
|
|
|
13
14
|
interface RecordEditInput {
|
|
@@ -34,6 +35,7 @@ interface UseClipboardOptions {
|
|
|
34
35
|
function getElementOuterHtml(
|
|
35
36
|
iframeRef: React.MutableRefObject<HTMLIFrameElement | null>,
|
|
36
37
|
selection: DomEditSelection,
|
|
38
|
+
activeCompositionPath: string | null,
|
|
37
39
|
): string | null {
|
|
38
40
|
let doc: Document | null = null;
|
|
39
41
|
try {
|
|
@@ -43,15 +45,7 @@ function getElementOuterHtml(
|
|
|
43
45
|
}
|
|
44
46
|
if (!doc) return null;
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
if (selection.id) {
|
|
48
|
-
el = doc.getElementById(selection.id);
|
|
49
|
-
}
|
|
50
|
-
if (!el && selection.selector) {
|
|
51
|
-
const matches = doc.querySelectorAll(selection.selector);
|
|
52
|
-
el = matches[selection.selectorIndex ?? 0] ?? null;
|
|
53
|
-
}
|
|
54
|
-
return el && "outerHTML" in el ? (el as Element).outerHTML : null;
|
|
48
|
+
return findElementForSelection(doc, selection, activeCompositionPath)?.outerHTML ?? null;
|
|
55
49
|
}
|
|
56
50
|
|
|
57
51
|
export function useClipboard({
|
|
@@ -71,6 +65,9 @@ export function useClipboard({
|
|
|
71
65
|
const projectIdRef = useRef(projectId);
|
|
72
66
|
projectIdRef.current = projectId;
|
|
73
67
|
|
|
68
|
+
// The copy-mode branches predate this change; this diff only replaces its
|
|
69
|
+
// duplicated DOM lookup with the canonical composition-aware resolver.
|
|
70
|
+
// fallow-ignore-next-line complexity
|
|
74
71
|
const handleCopy = useCallback((): boolean => {
|
|
75
72
|
const { selectedElementId, elements } = usePlayerStore.getState();
|
|
76
73
|
|
|
@@ -84,13 +81,18 @@ export function useClipboard({
|
|
|
84
81
|
try {
|
|
85
82
|
const doc = previewIframeRef.current?.contentDocument;
|
|
86
83
|
if (doc) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
84
|
+
html =
|
|
85
|
+
findElementForSelection(
|
|
86
|
+
doc,
|
|
87
|
+
{
|
|
88
|
+
hfId: element.hfId,
|
|
89
|
+
id: element.domId,
|
|
90
|
+
selector: element.selector,
|
|
91
|
+
selectorIndex: element.selectorIndex,
|
|
92
|
+
sourceFile: targetPath,
|
|
93
|
+
},
|
|
94
|
+
activeCompPath,
|
|
95
|
+
)?.outerHTML ?? null;
|
|
94
96
|
}
|
|
95
97
|
} catch {
|
|
96
98
|
// cross-origin frame
|
|
@@ -110,7 +112,7 @@ export function useClipboard({
|
|
|
110
112
|
// DOM element copy
|
|
111
113
|
const domSelection = domEditSelectionRef.current;
|
|
112
114
|
if (domSelection) {
|
|
113
|
-
const html = getElementOuterHtml(previewIframeRef, domSelection);
|
|
115
|
+
const html = getElementOuterHtml(previewIframeRef, domSelection, activeCompPath);
|
|
114
116
|
if (!html) {
|
|
115
117
|
showToast("Unable to copy this element.", "info");
|
|
116
118
|
return false;
|
|
@@ -25,6 +25,7 @@ interface DataAttributeCommitOptions {
|
|
|
25
25
|
coalescePrefix: string;
|
|
26
26
|
skipRefresh: boolean;
|
|
27
27
|
refreshAfter?: boolean;
|
|
28
|
+
onSettled?: (ok: boolean) => void;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
function resolveFullAttrName(attr: string, prefixData: boolean | undefined): string {
|
|
@@ -57,6 +58,25 @@ interface CapturedAttributeElement {
|
|
|
57
58
|
previousValue: string | null;
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
interface CapturedMultiAttributeElement {
|
|
62
|
+
element: HTMLElement;
|
|
63
|
+
previousValues: Map<string, string | null>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function captureMultiAttributeElement(
|
|
67
|
+
doc: Document | null | undefined,
|
|
68
|
+
selection: DomEditSelection,
|
|
69
|
+
activeCompPath: string | null,
|
|
70
|
+
fullAttrs: string[],
|
|
71
|
+
): CapturedMultiAttributeElement | null {
|
|
72
|
+
const el = findPreviewAttributeElement(doc, selection, activeCompPath);
|
|
73
|
+
if (!el) return null;
|
|
74
|
+
const previousValues = new Map(
|
|
75
|
+
fullAttrs.map((fullAttr) => [fullAttr, el.getAttribute(fullAttr)]),
|
|
76
|
+
);
|
|
77
|
+
return { element: el, previousValues };
|
|
78
|
+
}
|
|
79
|
+
|
|
60
80
|
function captureAttributeElement(
|
|
61
81
|
doc: Document | null | undefined,
|
|
62
82
|
selection: DomEditSelection,
|
|
@@ -128,6 +148,7 @@ export function useDomEditAttributeCommits({
|
|
|
128
148
|
onError: (error) => reportDomEditPersistFailure(domEditSelection, [op], error, showToast),
|
|
129
149
|
shouldResync: () => isLatestCommit() && !!options.refreshAfter,
|
|
130
150
|
resync: () => refreshDomEditSelectionFromPreview(domEditSelection),
|
|
151
|
+
onSettled: options.onSettled,
|
|
131
152
|
});
|
|
132
153
|
},
|
|
133
154
|
[
|
|
@@ -140,6 +161,105 @@ export function useDomEditAttributeCommits({
|
|
|
140
161
|
],
|
|
141
162
|
);
|
|
142
163
|
|
|
164
|
+
// Commits several data-* attributes on the SAME element in ONE persist call
|
|
165
|
+
// — needed when two attributes together describe a single logical value
|
|
166
|
+
// (e.g. a pinned timing range's start+duration): committing them through two
|
|
167
|
+
// separate sequential `commitDataAttribute` calls leaves a window where the
|
|
168
|
+
// second call resolves `domEditSelection` fresh from current hook state, so
|
|
169
|
+
// a selection change between the two awaits would misdirect it at the
|
|
170
|
+
// NEWLY selected element instead of the one being edited, and a failure of
|
|
171
|
+
// just the second call would leave the two attributes in an inconsistent
|
|
172
|
+
// half-applied state. Bundling them into one `PatchOperation[]` against an
|
|
173
|
+
// explicit, caller-supplied `selection` (not the "current" one) closes both
|
|
174
|
+
// gaps — matching `onCommitAnimatedProperties`'s same-shaped fix for GSAP
|
|
175
|
+
// property batches.
|
|
176
|
+
const commitDataAttributes = useCallback(
|
|
177
|
+
async (
|
|
178
|
+
selection: DomEditSelection,
|
|
179
|
+
attrs: Record<string, string | null>,
|
|
180
|
+
options: DataAttributeCommitOptions,
|
|
181
|
+
) => {
|
|
182
|
+
const iframe = previewIframeRef.current;
|
|
183
|
+
const entries = Object.entries(attrs).map(([attr, value]) => ({
|
|
184
|
+
attr,
|
|
185
|
+
fullAttr: resolveFullAttrName(attr, true),
|
|
186
|
+
value,
|
|
187
|
+
}));
|
|
188
|
+
const commitKey = `${options.coalescePrefix}:${entries
|
|
189
|
+
.map((entry) => entry.attr)
|
|
190
|
+
.sort()
|
|
191
|
+
.join(",")}:${getDomEditTargetKey(selection)}`;
|
|
192
|
+
const isLatestCommit = bumpDomEditCommitMapVersion(
|
|
193
|
+
domAttributeCommitVersionRef.current,
|
|
194
|
+
commitKey,
|
|
195
|
+
);
|
|
196
|
+
const ops: PatchOperation[] = entries.map((entry) => ({
|
|
197
|
+
type: "attribute",
|
|
198
|
+
property: entry.attr,
|
|
199
|
+
value: entry.value,
|
|
200
|
+
}));
|
|
201
|
+
let captured: CapturedMultiAttributeElement | null = null;
|
|
202
|
+
|
|
203
|
+
await runDomEditCommit({
|
|
204
|
+
capture: () => {
|
|
205
|
+
captured = captureMultiAttributeElement(
|
|
206
|
+
iframe?.contentDocument,
|
|
207
|
+
selection,
|
|
208
|
+
activeCompPath,
|
|
209
|
+
entries.map((entry) => entry.fullAttr),
|
|
210
|
+
);
|
|
211
|
+
},
|
|
212
|
+
apply: () => {
|
|
213
|
+
if (!captured) return;
|
|
214
|
+
for (const entry of entries) {
|
|
215
|
+
const nextValue = entry.value === null || entry.value === "" ? null : entry.value;
|
|
216
|
+
setOrRemovePreviewAttribute(captured.element, entry.fullAttr, nextValue);
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
persist: () =>
|
|
220
|
+
persistDomEditOperations(selection, ops, {
|
|
221
|
+
label: options.label,
|
|
222
|
+
coalesceKey: commitKey,
|
|
223
|
+
skipRefresh: options.skipRefresh,
|
|
224
|
+
}),
|
|
225
|
+
shouldRevert: () => isLatestCommit(),
|
|
226
|
+
revert: () => {
|
|
227
|
+
if (!captured) return;
|
|
228
|
+
for (const entry of entries) {
|
|
229
|
+
setOrRemovePreviewAttribute(
|
|
230
|
+
captured.element,
|
|
231
|
+
entry.fullAttr,
|
|
232
|
+
captured.previousValues.get(entry.fullAttr) ?? null,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
onError: (error) => reportDomEditPersistFailure(selection, ops, error, showToast),
|
|
237
|
+
shouldResync: () => isLatestCommit() && !!options.refreshAfter,
|
|
238
|
+
resync: () => refreshDomEditSelectionFromPreview(selection),
|
|
239
|
+
onSettled: options.onSettled,
|
|
240
|
+
});
|
|
241
|
+
},
|
|
242
|
+
[
|
|
243
|
+
activeCompPath,
|
|
244
|
+
persistDomEditOperations,
|
|
245
|
+
refreshDomEditSelectionFromPreview,
|
|
246
|
+
showToast,
|
|
247
|
+
previewIframeRef,
|
|
248
|
+
],
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const handleDomAttributesCommit = useCallback(
|
|
252
|
+
async (selection: DomEditSelection, attrs: Record<string, string>) => {
|
|
253
|
+
await commitDataAttributes(selection, attrs, {
|
|
254
|
+
label: "Edit timing",
|
|
255
|
+
coalescePrefix: "attrs",
|
|
256
|
+
skipRefresh: false,
|
|
257
|
+
refreshAfter: true,
|
|
258
|
+
});
|
|
259
|
+
},
|
|
260
|
+
[commitDataAttributes],
|
|
261
|
+
);
|
|
262
|
+
|
|
143
263
|
const handleDomAttributeCommit = useCallback(
|
|
144
264
|
async (attr: string, value: string) => {
|
|
145
265
|
await commitDataAttribute(attr, value, {
|
|
@@ -153,11 +273,12 @@ export function useDomEditAttributeCommits({
|
|
|
153
273
|
);
|
|
154
274
|
|
|
155
275
|
const handleDomAttributeLiveCommit = useCallback(
|
|
156
|
-
async (attr: string, value: string | null) => {
|
|
276
|
+
async (attr: string, value: string | null, onSettled?: (ok: boolean) => void) => {
|
|
157
277
|
await commitDataAttribute(attr, value, {
|
|
158
278
|
label: `Edit ${attr.replace(/^(data-)?/, "").replace(/-/g, " ")}`,
|
|
159
279
|
coalescePrefix: "attr-live",
|
|
160
280
|
skipRefresh: true,
|
|
281
|
+
onSettled,
|
|
161
282
|
});
|
|
162
283
|
},
|
|
163
284
|
[commitDataAttribute],
|
|
@@ -223,5 +344,6 @@ export function useDomEditAttributeCommits({
|
|
|
223
344
|
handleDomAttributeCommit,
|
|
224
345
|
handleDomAttributeLiveCommit,
|
|
225
346
|
handleDomHtmlAttributeCommit,
|
|
347
|
+
handleDomAttributesCommit,
|
|
226
348
|
};
|
|
227
349
|
}
|