@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
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { TimelineElement } from "../store/playerStore";
|
|
12
12
|
import type { ClipManifestClip } from "./playbackTypes";
|
|
13
|
+
import { resolveCssStackingContextId } from "@hyperframes/core/runtime/stacking-context";
|
|
13
14
|
import {
|
|
14
15
|
resolveMediaElement,
|
|
15
16
|
applyMediaMetadataFromElement,
|
|
@@ -228,6 +229,7 @@ export function createImplicitTimelineLayersFromDOM(
|
|
|
228
229
|
selector,
|
|
229
230
|
selectorIndex,
|
|
230
231
|
sourceFile,
|
|
232
|
+
stackingContextId: resolveCssStackingContextId(child),
|
|
231
233
|
start: 0,
|
|
232
234
|
tag: child.tagName.toLowerCase(),
|
|
233
235
|
timingSource: "implicit",
|
|
@@ -303,6 +305,7 @@ export function parseTimelineFromDOM(doc: Document, rootDuration: number): Timel
|
|
|
303
305
|
selector,
|
|
304
306
|
selectorIndex,
|
|
305
307
|
sourceFile,
|
|
308
|
+
stackingContextId: resolveCssStackingContextId(el),
|
|
306
309
|
timingSource: "authored",
|
|
307
310
|
zIndex: readTimelineElementZIndex(el),
|
|
308
311
|
};
|
|
@@ -10,6 +10,31 @@
|
|
|
10
10
|
import type { TimelineElement } from "../store/playerStore";
|
|
11
11
|
import type { ClipManifestClip } from "./playbackTypes";
|
|
12
12
|
import { isFinitePositive } from "./playbackAdapter";
|
|
13
|
+
import { getSourceScopedSelectorIndex } from "../../utils/sourceScopedSelectorIndex";
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Layer-reveal lift transparency
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Attributes carrying the pre-lift state of a Layers-panel selection reveal
|
|
21
|
+
* (useLayerRevealOverride): while a layer is selected it PAINTS on top via a
|
|
22
|
+
* temporary inline z-index, but the lift is a purely visual, ephemeral studio
|
|
23
|
+
* affordance — every z reader must keep reporting the element's TRUE z
|
|
24
|
+
* (stored here) so menus, badges, the lane mirror, and the panel sort never
|
|
25
|
+
* reason on the lifted value. A z-reorder commit removes the attributes (the
|
|
26
|
+
* commit is the new truth).
|
|
27
|
+
*/
|
|
28
|
+
export const LAYER_REVEAL_PRIOR_Z_ATTR = "data-hf-reveal-prior-z";
|
|
29
|
+
export const LAYER_REVEAL_PRIOR_POSITION_ATTR = "data-hf-reveal-prior-pos";
|
|
30
|
+
|
|
31
|
+
/** The lifted element's true (pre-lift) z, or null when no lift is active. */
|
|
32
|
+
export function readLayerRevealPriorZ(el: Element): number | null {
|
|
33
|
+
const raw = el.getAttribute(LAYER_REVEAL_PRIOR_Z_ATTR);
|
|
34
|
+
if (raw == null) return null;
|
|
35
|
+
const n = Number.parseInt(raw, 10);
|
|
36
|
+
return Number.isFinite(n) ? n : null;
|
|
37
|
+
}
|
|
13
38
|
|
|
14
39
|
// ---------------------------------------------------------------------------
|
|
15
40
|
// Duration attribute helpers
|
|
@@ -22,8 +47,11 @@ import { isFinitePositive } from "./playbackAdapter";
|
|
|
22
47
|
* back to computed style; "auto" / empty / unparseable ⇒ 0. Works with a
|
|
23
48
|
* detached parse Document (no defaultView) as well as a live iframe. Mirrors
|
|
24
49
|
* canvasContextMenuZOrder.parseZIndex semantics so the two directions agree.
|
|
50
|
+
* Reveal-lift transparent: an active lift reports the stored TRUE z.
|
|
25
51
|
*/
|
|
26
52
|
export function readTimelineElementZIndex(el: Element): number {
|
|
53
|
+
const prior = readLayerRevealPriorZ(el);
|
|
54
|
+
if (prior != null) return prior;
|
|
27
55
|
const html = el as HTMLElement;
|
|
28
56
|
const parseZ = (value: string | null | undefined): number | null => {
|
|
29
57
|
if (value == null || value === "" || value === "auto") return null;
|
|
@@ -237,17 +265,13 @@ export function getTimelineElementSelectorIndex(
|
|
|
237
265
|
el: Element,
|
|
238
266
|
selector: string | undefined,
|
|
239
267
|
): number | undefined {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
return matchIndex >= 0 ? matchIndex : undefined;
|
|
248
|
-
} catch {
|
|
249
|
-
return undefined;
|
|
250
|
-
}
|
|
268
|
+
return getSourceScopedSelectorIndex(
|
|
269
|
+
doc,
|
|
270
|
+
el,
|
|
271
|
+
selector,
|
|
272
|
+
getTimelineElementSourceFile(el),
|
|
273
|
+
getTimelineElementSourceFile,
|
|
274
|
+
);
|
|
251
275
|
}
|
|
252
276
|
|
|
253
277
|
export function buildTimelineElementKey(params: {
|
|
@@ -44,7 +44,7 @@ export interface TimelineElement {
|
|
|
44
44
|
zIndex?: number;
|
|
45
45
|
/** True when the effective z-index was authored inline or through CSS, not auto. */
|
|
46
46
|
hasExplicitZIndex?: boolean;
|
|
47
|
-
/**
|
|
47
|
+
/** Canonical CSS stacking context this element's z-index participates in. */
|
|
48
48
|
stackingContextId?: string | null;
|
|
49
49
|
/** Nearest parent composition context, matching RuntimeTimelineClip. */
|
|
50
50
|
parentCompositionId?: string | null;
|
|
@@ -124,6 +124,13 @@ interface PlayerState {
|
|
|
124
124
|
zoomMode: ZoomMode;
|
|
125
125
|
/** Timeline zoom percent relative to the fit width when in manual mode */
|
|
126
126
|
manualZoomPercent: number;
|
|
127
|
+
/**
|
|
128
|
+
* Bumped on every live z-index edit (handleDomZIndexReorderCommit apply AND
|
|
129
|
+
* rollback). Flashless z commits (skipReload) never reload the iframe or
|
|
130
|
+
* bump refreshKey, so DOM-derived views (the Layers panel's z-sorted tree)
|
|
131
|
+
* subscribe to this to re-read the live DOM while playback is paused.
|
|
132
|
+
*/
|
|
133
|
+
zEditVersion: number;
|
|
127
134
|
/** Work-area in-point (seconds). When set, loop starts here and A jumps here. */
|
|
128
135
|
inPoint: number | null;
|
|
129
136
|
/** Work-area out-point (seconds). When set, loop ends here and E jumps here. */
|
|
@@ -212,6 +219,7 @@ interface PlayerState {
|
|
|
212
219
|
) => void;
|
|
213
220
|
setZoomMode: (mode: ZoomMode) => void;
|
|
214
221
|
setManualZoomPercent: (percent: number) => void;
|
|
222
|
+
bumpZEditVersion: () => void;
|
|
215
223
|
setInPoint: (time: number | null) => void;
|
|
216
224
|
setOutPoint: (time: number | null) => void;
|
|
217
225
|
reset: () => void;
|
|
@@ -274,6 +282,7 @@ export interface DomClipChild {
|
|
|
274
282
|
/** The manifest sub-comp host clip id this descendant ultimately lives under. */
|
|
275
283
|
hostId: string;
|
|
276
284
|
label: string;
|
|
285
|
+
stackingContextId: string;
|
|
277
286
|
}
|
|
278
287
|
|
|
279
288
|
interface BeatHistoryEntry {
|
|
@@ -308,6 +317,7 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
|
|
|
308
317
|
loopEnabled: false,
|
|
309
318
|
zoomMode: "fit",
|
|
310
319
|
manualZoomPercent: 100,
|
|
320
|
+
zEditVersion: 0,
|
|
311
321
|
timelinePps: 100,
|
|
312
322
|
timelineFitPps: 100,
|
|
313
323
|
inPoint: null,
|
|
@@ -494,6 +504,7 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
|
|
|
494
504
|
}),
|
|
495
505
|
setManualZoomPercent: (percent) =>
|
|
496
506
|
set({ manualZoomPercent: Math.max(10, Math.min(2000, Math.round(percent))) }),
|
|
507
|
+
bumpZEditVersion: () => set((state) => ({ zEditVersion: state.zEditVersion + 1 })),
|
|
497
508
|
setCurrentTime: (time) => set({ currentTime: Number.isFinite(time) ? time : 0 }),
|
|
498
509
|
setDuration: (duration) => set({ duration: Number.isFinite(duration) ? duration : 0 }),
|
|
499
510
|
setTimelineReady: (ready) => set({ timelineReady: ready }),
|
package/src/styles/studio.css
CHANGED
|
@@ -447,3 +447,25 @@ body {
|
|
|
447
447
|
animation: none;
|
|
448
448
|
}
|
|
449
449
|
}
|
|
450
|
+
|
|
451
|
+
/* Flat inspector group header/body entrance (FlatGroupHeader) */
|
|
452
|
+
@keyframes hf-flat-group-in {
|
|
453
|
+
from {
|
|
454
|
+
opacity: 0;
|
|
455
|
+
transform: translateY(-4px);
|
|
456
|
+
}
|
|
457
|
+
to {
|
|
458
|
+
opacity: 1;
|
|
459
|
+
transform: translateY(0);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.hf-flat-group-enter {
|
|
464
|
+
animation: hf-flat-group-in 120ms ease-out;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
@media (prefers-reduced-motion: reduce) {
|
|
468
|
+
.hf-flat-group-enter {
|
|
469
|
+
animation: none;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
@@ -12,11 +12,16 @@ const studioPreset = {
|
|
|
12
12
|
},
|
|
13
13
|
panel: {
|
|
14
14
|
bg: "#0C0C0E",
|
|
15
|
+
// Open inspector-section body — slightly lighter than headers (bg)
|
|
16
|
+
// so the recessed scrollable region reads distinct.
|
|
17
|
+
"bg-inset": "#121214",
|
|
15
18
|
input: "#161618",
|
|
16
19
|
surface: "#18181B",
|
|
17
20
|
hover: "#27272A",
|
|
18
21
|
border: "#1E1E1E",
|
|
19
22
|
"border-input": "#27272A",
|
|
23
|
+
hairline: "#1A1A1C",
|
|
24
|
+
"text-0": "#FAFAFA",
|
|
20
25
|
"text-1": "#E4E4E7",
|
|
21
26
|
"text-2": "#A1A1AA",
|
|
22
27
|
"text-3": "#71717A",
|
|
@@ -24,6 +29,8 @@ const studioPreset = {
|
|
|
24
29
|
"text-5": "#3F3F46",
|
|
25
30
|
accent: "#3CE6AC",
|
|
26
31
|
danger: "#EF4444",
|
|
32
|
+
media: "#00E3FF",
|
|
33
|
+
container: "#F5A623",
|
|
27
34
|
},
|
|
28
35
|
},
|
|
29
36
|
},
|
|
@@ -12,6 +12,7 @@ const {
|
|
|
12
12
|
trackStudioRenderStart,
|
|
13
13
|
trackStudioRazorSplit,
|
|
14
14
|
trackStudioExpandedClipEdit,
|
|
15
|
+
trackStudioFeedback,
|
|
15
16
|
} = await import("./events");
|
|
16
17
|
|
|
17
18
|
describe("studio telemetry events", () => {
|
|
@@ -69,4 +70,13 @@ describe("studio telemetry events", () => {
|
|
|
69
70
|
trackStudioExpandedClipEdit({ action: "resize" });
|
|
70
71
|
expect(trackEvent).toHaveBeenCalledWith("studio_expanded_clip_edit", { action: "resize" });
|
|
71
72
|
});
|
|
73
|
+
|
|
74
|
+
it.each([0, 10])("trackStudioFeedback preserves NPS boundary %i and its scale", (rating) => {
|
|
75
|
+
trackStudioFeedback({ rating });
|
|
76
|
+
|
|
77
|
+
expect(trackEvent).toHaveBeenCalledWith(
|
|
78
|
+
"survey sent",
|
|
79
|
+
expect.objectContaining({ $survey_response: rating, rating_scale: 10 }),
|
|
80
|
+
);
|
|
81
|
+
});
|
|
72
82
|
});
|
package/src/telemetry/events.ts
CHANGED
|
@@ -67,6 +67,7 @@ export function trackStudioFeedback(props: { rating: number; comment?: string })
|
|
|
67
67
|
trackEvent("survey sent", {
|
|
68
68
|
$survey_id: "studio_experience",
|
|
69
69
|
$survey_response: props.rating,
|
|
70
|
+
rating_scale: 10,
|
|
70
71
|
...(props.comment ? { $survey_response_2: props.comment } : {}),
|
|
71
72
|
doctor_summary: getBrowserDoctorSummary(),
|
|
72
73
|
source: "studio",
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const trackStudioEvent = vi.fn();
|
|
4
|
+
vi.mock("./studioTelemetry", () => ({
|
|
5
|
+
trackStudioEvent: (...args: unknown[]) => trackStudioEvent(...args),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
__resetDesignInputThrottle,
|
|
10
|
+
slugifyDesignInput,
|
|
11
|
+
trackDesignInput,
|
|
12
|
+
} from "./designInputTracking";
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
trackStudioEvent.mockReset();
|
|
16
|
+
__resetDesignInputThrottle();
|
|
17
|
+
vi.restoreAllMocks();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
vi.restoreAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("trackDesignInput", () => {
|
|
25
|
+
it("emits one design_input event with ui/section/control/name", () => {
|
|
26
|
+
trackDesignInput({ ui: "flat", section: "Style", control: "metric", name: "Opacity" });
|
|
27
|
+
expect(trackStudioEvent).toHaveBeenCalledTimes(1);
|
|
28
|
+
expect(trackStudioEvent).toHaveBeenCalledWith("design_input", {
|
|
29
|
+
ui: "flat",
|
|
30
|
+
section: "style",
|
|
31
|
+
control: "metric",
|
|
32
|
+
name: "opacity",
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("slugifies compound names and sections", () => {
|
|
37
|
+
trackDesignInput({
|
|
38
|
+
ui: "classic",
|
|
39
|
+
section: "Color Grading",
|
|
40
|
+
control: "slider",
|
|
41
|
+
name: "Font Size",
|
|
42
|
+
});
|
|
43
|
+
expect(trackStudioEvent).toHaveBeenCalledWith("design_input", {
|
|
44
|
+
ui: "classic",
|
|
45
|
+
section: "color-grading",
|
|
46
|
+
control: "slider",
|
|
47
|
+
name: "font-size",
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("marks an unresolved name as 'unnamed' (R3 coverage signal)", () => {
|
|
52
|
+
trackDesignInput({ ui: "classic", section: "style", control: "button", name: "" });
|
|
53
|
+
expect(trackStudioEvent).toHaveBeenCalledWith(
|
|
54
|
+
"design_input",
|
|
55
|
+
expect.objectContaining({ name: "unnamed" }),
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("coalesces repeated fires of the same input within the window (R4)", () => {
|
|
60
|
+
const nowSpy = vi.spyOn(performance, "now");
|
|
61
|
+
// Same key, three quick fires -> 1 event.
|
|
62
|
+
nowSpy.mockReturnValue(1000);
|
|
63
|
+
trackDesignInput({ ui: "flat", section: "style", control: "slider", name: "opacity" });
|
|
64
|
+
nowSpy.mockReturnValue(1100);
|
|
65
|
+
trackDesignInput({ ui: "flat", section: "style", control: "slider", name: "opacity" });
|
|
66
|
+
nowSpy.mockReturnValue(1500);
|
|
67
|
+
trackDesignInput({ ui: "flat", section: "style", control: "slider", name: "opacity" });
|
|
68
|
+
expect(trackStudioEvent).toHaveBeenCalledTimes(1);
|
|
69
|
+
|
|
70
|
+
// A different input within the window is NOT collapsed.
|
|
71
|
+
nowSpy.mockReturnValue(1550);
|
|
72
|
+
trackDesignInput({ ui: "flat", section: "style", control: "slider", name: "scale" });
|
|
73
|
+
expect(trackStudioEvent).toHaveBeenCalledTimes(2);
|
|
74
|
+
|
|
75
|
+
// Same input after the window fires again.
|
|
76
|
+
nowSpy.mockReturnValue(2200);
|
|
77
|
+
trackDesignInput({ ui: "flat", section: "style", control: "slider", name: "opacity" });
|
|
78
|
+
expect(trackStudioEvent).toHaveBeenCalledTimes(3);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("never throws even if the underlying tracker throws", () => {
|
|
82
|
+
trackStudioEvent.mockImplementation(() => {
|
|
83
|
+
throw new Error("ingest down");
|
|
84
|
+
});
|
|
85
|
+
expect(() =>
|
|
86
|
+
trackDesignInput({ ui: "flat", section: "style", control: "metric", name: "opacity" }),
|
|
87
|
+
).not.toThrow();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("slugifyDesignInput", () => {
|
|
92
|
+
it("lowercases, collapses non-alphanumerics, and trims dashes", () => {
|
|
93
|
+
expect(slugifyDesignInput(" Border Radius (px) ")).toBe("border-radius-px");
|
|
94
|
+
expect(slugifyDesignInput("X")).toBe("x");
|
|
95
|
+
expect(slugifyDesignInput("---")).toBe("");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { trackStudioEvent } from "./studioTelemetry";
|
|
2
|
+
|
|
3
|
+
// Per-input usage telemetry for the design (inspector) panel. Both inspector UIs
|
|
4
|
+
// (classic PropertyPanel, flat PropertyPanelFlat) funnel their inputs through this
|
|
5
|
+
// one helper so usage can be ranked by input to find removal candidates. Emits the
|
|
6
|
+
// batched `studio:design_input` event via trackStudioEvent (opt-out-aware, never-throw).
|
|
7
|
+
//
|
|
8
|
+
// Fire convention (kept consistent across both UIs):
|
|
9
|
+
// - Discrete controls (metric/text/select/segmented/toggle/color/button) fire only
|
|
10
|
+
// when a real committed value change happens (the commit site guards on
|
|
11
|
+
// next !== current).
|
|
12
|
+
// - Continuous controls (sliders, scrub) fire once per user interaction that
|
|
13
|
+
// produced commits, even if the net value ends unchanged — net-change-at-settle
|
|
14
|
+
// is unreliable for them because mid-drag commits have already advanced the value,
|
|
15
|
+
// so "did the user work this control" is the honest signal. The coalescing window
|
|
16
|
+
// below collapses the many mid-drag commits into that single event.
|
|
17
|
+
|
|
18
|
+
export type DesignInputUi = "flat" | "classic";
|
|
19
|
+
|
|
20
|
+
export interface DesignInputDescriptor {
|
|
21
|
+
ui: DesignInputUi;
|
|
22
|
+
/** Section slug the input lives under (e.g. "style", "color-grading"). */
|
|
23
|
+
section: string;
|
|
24
|
+
/** Control kind: "metric" | "slider" | "select" | "segmented" | "toggle" | "color" | "text" | "button" | … */
|
|
25
|
+
control: string;
|
|
26
|
+
/** Input identity — the field label or CSS/GSAP property. Slugified for stable ranking. */
|
|
27
|
+
name: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Continuous controls (sliders, scrub, wheel-nudge, live-commit text) fire many
|
|
31
|
+
// commits per interaction. Collapse repeated fires of the same input within this
|
|
32
|
+
// window into one event so a single drag counts once (R4).
|
|
33
|
+
const COALESCE_WINDOW_MS = 600;
|
|
34
|
+
|
|
35
|
+
const lastFiredByKey = new Map<string, number>();
|
|
36
|
+
|
|
37
|
+
function now(): number {
|
|
38
|
+
// performance.now() is monotonic and available in the studio runtime and jsdom.
|
|
39
|
+
return typeof performance !== "undefined" && typeof performance.now === "function"
|
|
40
|
+
? performance.now()
|
|
41
|
+
: 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function slugifyDesignInput(value: string): string {
|
|
45
|
+
return value
|
|
46
|
+
.trim()
|
|
47
|
+
.toLowerCase()
|
|
48
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
49
|
+
.replace(/(^-|-$)/g, "");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Test seam: clear the coalescing state between cases. */
|
|
53
|
+
export function __resetDesignInputThrottle(): void {
|
|
54
|
+
lastFiredByKey.clear();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function trackDesignInput(descriptor: DesignInputDescriptor): void {
|
|
58
|
+
try {
|
|
59
|
+
const section = slugifyDesignInput(descriptor.section) || "unknown";
|
|
60
|
+
const name = slugifyDesignInput(descriptor.name);
|
|
61
|
+
// An input with no resolvable name is useless for the removal analysis (R3).
|
|
62
|
+
// Emit it anyway (so a coverage test can catch it) but under an explicit marker.
|
|
63
|
+
const control = descriptor.control || "unknown";
|
|
64
|
+
const key = `${descriptor.ui}:${section}:${control}:${name || "unnamed"}`;
|
|
65
|
+
|
|
66
|
+
const t = now();
|
|
67
|
+
const last = lastFiredByKey.get(key);
|
|
68
|
+
if (last !== undefined && t - last < COALESCE_WINDOW_MS) return;
|
|
69
|
+
lastFiredByKey.set(key, t);
|
|
70
|
+
|
|
71
|
+
trackStudioEvent("design_input", {
|
|
72
|
+
ui: descriptor.ui,
|
|
73
|
+
section,
|
|
74
|
+
control,
|
|
75
|
+
name: name || "unnamed",
|
|
76
|
+
});
|
|
77
|
+
} catch {
|
|
78
|
+
// Telemetry must never break the edit path.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -13,7 +13,7 @@ interface DomEditSaveQueueOptions {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export interface DomEditSaveQueue {
|
|
16
|
-
enqueue: (save: () => Promise<
|
|
16
|
+
enqueue: <T>(save: () => Promise<T>) => Promise<T>;
|
|
17
17
|
waitForIdle: () => Promise<void>;
|
|
18
18
|
reset: () => void;
|
|
19
19
|
destroy: () => void;
|
|
@@ -52,10 +52,11 @@ export function createDomEditSaveQueue(options: DomEditSaveQueueOptions = {}): D
|
|
|
52
52
|
});
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
-
const run = async (save: () => Promise<
|
|
55
|
+
const run = async <T>(save: () => Promise<T>): Promise<T> => {
|
|
56
56
|
try {
|
|
57
|
-
await save();
|
|
57
|
+
const result = await save();
|
|
58
58
|
if (!breakerOpen) consecutiveFailures = 0;
|
|
59
|
+
return result;
|
|
59
60
|
} catch (error) {
|
|
60
61
|
consecutiveFailures += 1;
|
|
61
62
|
if (consecutiveFailures >= failureThreshold) open(error);
|
|
@@ -277,6 +277,88 @@ describe("edit history", () => {
|
|
|
277
277
|
expect(state.undo[0].files["index.html"].after).toBe("timing+gsap");
|
|
278
278
|
});
|
|
279
279
|
|
|
280
|
+
it("keeps timing-only files when a multi-file GSAP follow-up touches one path", () => {
|
|
281
|
+
const timing = buildEditHistoryEntry({
|
|
282
|
+
projectId: "project-1",
|
|
283
|
+
label: "Move timeline clips",
|
|
284
|
+
kind: "timeline",
|
|
285
|
+
coalesceKey: "timeline-group-move:a,b",
|
|
286
|
+
files: {
|
|
287
|
+
"index.html": { before: "index-before", after: "index-timing" },
|
|
288
|
+
"scene.html": { before: "scene-before", after: "scene-timing" },
|
|
289
|
+
},
|
|
290
|
+
now: 0,
|
|
291
|
+
id: "timing",
|
|
292
|
+
});
|
|
293
|
+
const gsap = buildEditHistoryEntry({
|
|
294
|
+
projectId: "project-1",
|
|
295
|
+
label: "Move timeline clips",
|
|
296
|
+
kind: "timeline",
|
|
297
|
+
coalesceKey: "timeline-group-move:a,b",
|
|
298
|
+
coalesceMs: 10_000,
|
|
299
|
+
files: {
|
|
300
|
+
"index.html": { before: "index-timing", after: "index-timing+gsap" },
|
|
301
|
+
},
|
|
302
|
+
now: 2000,
|
|
303
|
+
id: "gsap",
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const state = pushEditHistoryEntry(
|
|
307
|
+
pushEditHistoryEntry(createEmptyEditHistory(), timing),
|
|
308
|
+
gsap,
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
expect(state.undo).toHaveLength(1);
|
|
312
|
+
expect(Object.keys(state.undo[0].files)).toEqual(["index.html", "scene.html"]);
|
|
313
|
+
expect(state.undo[0].files).toMatchObject({
|
|
314
|
+
"index.html": { before: "index-before", after: "index-timing+gsap" },
|
|
315
|
+
"scene.html": { before: "scene-before", after: "scene-timing" },
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("keeps a discontinuous GSAP follow-up separate and preserves foreign bytes on undo", () => {
|
|
320
|
+
const timing = buildEditHistoryEntry({
|
|
321
|
+
projectId: "project-1",
|
|
322
|
+
label: "Move timeline clip",
|
|
323
|
+
kind: "timeline",
|
|
324
|
+
coalesceKey: "timeline-move:clip",
|
|
325
|
+
files: { "index.html": { before: "A", after: "B" } },
|
|
326
|
+
now: 0,
|
|
327
|
+
id: "timing",
|
|
328
|
+
});
|
|
329
|
+
const gsapAfterForeignWrite = buildEditHistoryEntry({
|
|
330
|
+
projectId: "project-1",
|
|
331
|
+
label: "Move timeline clip",
|
|
332
|
+
kind: "timeline",
|
|
333
|
+
coalesceKey: "timeline-move:clip",
|
|
334
|
+
coalesceMs: 10_000,
|
|
335
|
+
files: { "index.html": { before: "F", after: "G" } },
|
|
336
|
+
now: 2000,
|
|
337
|
+
id: "gsap",
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const state = pushEditHistoryEntry(
|
|
341
|
+
pushEditHistoryEntry(createEmptyEditHistory(), timing),
|
|
342
|
+
gsapAfterForeignWrite,
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
expect(state.undo.map((entry) => entry.id)).toEqual(["timing", "gsap"]);
|
|
346
|
+
const undoGsap = undoEditHistory(state, { "index.html": hashEditHistoryContent("G") }, 3000);
|
|
347
|
+
expect(undoGsap.ok).toBe(true);
|
|
348
|
+
expect(undoGsap.filesToWrite).toEqual({ "index.html": "F" });
|
|
349
|
+
|
|
350
|
+
const undoTiming = undoEditHistory(
|
|
351
|
+
undoGsap.state,
|
|
352
|
+
{ "index.html": hashEditHistoryContent("F") },
|
|
353
|
+
4000,
|
|
354
|
+
);
|
|
355
|
+
expect(undoTiming).toMatchObject({
|
|
356
|
+
ok: false,
|
|
357
|
+
reason: "content-mismatch",
|
|
358
|
+
filesToWrite: {},
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
280
362
|
it("does not merge a slow follow-up without the coalesceMs override", () => {
|
|
281
363
|
const timing = buildEditHistoryEntry({
|
|
282
364
|
projectId: "project-1",
|
package/src/utils/editHistory.ts
CHANGED
|
@@ -126,9 +126,21 @@ export function pushEditHistoryEntry(
|
|
|
126
126
|
previous &&
|
|
127
127
|
previous.coalesceKey &&
|
|
128
128
|
previous.coalesceKey === entry.coalesceKey &&
|
|
129
|
-
entry.createdAt - previous.createdAt <= coalesceMs
|
|
129
|
+
entry.createdAt - previous.createdAt <= coalesceMs &&
|
|
130
|
+
// A shared key/window is not enough: bytes from an external writer may
|
|
131
|
+
// have landed between the two Studio-owned edits. Only fold a path when
|
|
132
|
+
// the follow-up starts from the exact output the previous entry recorded.
|
|
133
|
+
// Otherwise keep both entries so undo restores the foreign bytes first and
|
|
134
|
+
// the older entry's content check safely blocks at that ownership boundary.
|
|
135
|
+
Object.entries(entry.files).every(([path, snapshot]) => {
|
|
136
|
+
const previousSnapshot = previous.files[path];
|
|
137
|
+
return !previousSnapshot || previousSnapshot.after === snapshot.before;
|
|
138
|
+
})
|
|
130
139
|
) {
|
|
131
|
-
|
|
140
|
+
// A follow-up may touch only a subset of the files in the original edit
|
|
141
|
+
// (for example, only one file in a multi-file timing move has GSAP). Keep
|
|
142
|
+
// previous-only paths in the single coalesced undo entry.
|
|
143
|
+
const files: Record<string, EditHistoryFileSnapshot> = { ...previous.files };
|
|
132
144
|
for (const [path, snapshot] of Object.entries(entry.files)) {
|
|
133
145
|
const previousSnapshot = previous.files[path];
|
|
134
146
|
files[path] = previousSnapshot
|