@hyperframes/studio 0.8.75 → 0.8.77
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-BSDTJ9EF.js +464 -0
- package/dist/assets/{index-BlUYw1MB.js → index-2GgHjQq-.js} +163 -163
- package/dist/assets/{index-0w6iIlwZ.css → index-BWJqxeaH.css} +1 -1
- package/dist/assets/{index-BiFKcNjs.js → index-CsFCRdMo.js} +1 -1
- package/dist/assets/{index-HjexPsny.js → index-DcxWQ7Za.js} +1 -1
- package/dist/{chunk-7BP32G62.js → chunk-5VANA2TG.js} +12 -1
- package/dist/chunk-5VANA2TG.js.map +1 -0
- package/dist/{domEditingLayers-UKMVYXT6.js → domEditingLayers-IGPLTI5E.js} +4 -2
- package/dist/index.d.ts +34 -69
- package/dist/index.html +2 -2
- package/dist/index.js +5287 -5277
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +7 -7
- package/src/components/StudioRightPanels.types.ts +0 -2
- package/src/components/editor/DomEditOverlay.test.ts +5 -0
- package/src/components/editor/LayersPanel.tsx +3 -14
- package/src/components/editor/PreviewOverlayProvider.test.tsx +6 -1
- package/src/components/editor/domEditing.ts +1 -0
- package/src/components/editor/domEditingElement.ts +13 -0
- package/src/components/editor/domEditingLayers.test.ts +27 -0
- package/src/components/editor/domEditingLayers.ts +9 -0
- package/src/components/editor/gsapLivePreview.test.ts +63 -0
- package/src/components/editor/gsapLivePreview.ts +2 -14
- package/src/components/editor/offCanvasIndicatorRefresh.test.tsx +57 -35
- package/src/components/editor/overlayFrameLoop.test.ts +16 -0
- package/src/components/editor/overlayFrameLoop.ts +11 -0
- package/src/components/editor/propertyPanelHelpers.ts +4 -6
- package/src/components/editor/usePreviewCompositionRect.test.tsx +6 -1
- package/src/components/nle/NLEPreview.test.ts +202 -22
- package/src/components/nle/NLEPreview.tsx +65 -46
- package/src/components/nle/PreviewPoster.tsx +99 -0
- package/src/components/nle/PreviewZoomOverlay.tsx +111 -0
- package/src/components/nle/previewZoom.test.ts +42 -0
- package/src/components/nle/previewZoom.ts +33 -0
- package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +30 -49
- package/src/components/panels/VariablesPanel.tsx +0 -2
- package/src/components/sidebar/CompositionsTab.drag.test.tsx +5 -1
- package/src/components/sidebar/CompositionsTab.rootBadge.test.tsx +14 -0
- package/src/components/sidebar/CompositionsTab.test.ts +4 -6
- package/src/components/sidebar/CompositionsTab.tsx +28 -21
- package/src/components/studioColorGradingScope.ts +0 -3
- package/src/hooks/domEditDeleteMembers.ts +0 -2
- package/src/hooks/gsapScriptCommitTypes.ts +0 -2
- package/src/hooks/keyframeCacheAstLoad.test.ts +31 -1
- package/src/hooks/keyframeCacheAstLoad.ts +2 -1
- package/src/hooks/timelineAudioGroupCreate.ts +0 -1
- package/src/hooks/timelineEditingHelpers.ts +0 -3
- package/src/hooks/timelineTimingSync.ts +0 -1
- package/src/hooks/timelineTrackVisibility.ts +0 -1
- package/src/hooks/useBlockHandlers.ts +0 -2
- package/src/hooks/useClipboard.test.ts +69 -10
- package/src/hooks/useClipboard.ts +28 -19
- package/src/hooks/useDomEditCommits.test.tsx +0 -1
- package/src/hooks/useDomEditCommits.ts +0 -4
- package/src/hooks/useDomEditWiring.ts +1 -3
- package/src/hooks/useEditHistoryActions.test.tsx +31 -14
- package/src/hooks/useEditHistoryActions.ts +12 -7
- package/src/hooks/useEditorSave.ts +0 -3
- package/src/hooks/useElementLifecycleOps.ts +0 -1
- package/src/hooks/useEnableKeyframes.test.ts +9 -1
- package/src/hooks/useFileManager.ts +0 -2
- package/src/hooks/useGestureRecording.test.tsx +1 -1
- package/src/hooks/useGestureRecording.ts +6 -10
- package/src/hooks/useGroupCommits.ts +0 -1
- package/src/hooks/useGsapScriptCommits.ts +0 -1
- package/src/hooks/useGsapTweenCache.element.test.tsx +39 -0
- package/src/hooks/useGsapTweenCache.ts +5 -13
- package/src/hooks/useLintModal.test.tsx +34 -1
- package/src/hooks/useLintModal.ts +32 -30
- package/src/hooks/usePersistentEditHistory.test.ts +221 -315
- package/src/hooks/usePersistentEditHistory.ts +155 -342
- package/src/hooks/usePopulateKeyframeCacheForFile.test.tsx +2 -0
- package/src/hooks/usePreviewPersistence.ts +0 -2
- package/src/hooks/useProjectCompositionVariables.ts +1 -3
- package/src/hooks/useRazorSplit.history.test.tsx +46 -67
- package/src/hooks/useRazorSplit.test.ts +17 -47
- package/src/hooks/useRenderClipContent.test.ts +29 -0
- package/src/hooks/useRenderClipContent.ts +6 -3
- package/src/hooks/useSdkSession.lifecycle.test.tsx +34 -4
- package/src/hooks/useSdkSession.ts +8 -2
- package/src/hooks/useSlideshowPersist.ts +0 -2
- package/src/hooks/useStudioContextValue.ts +6 -1
- package/src/hooks/useTimelineAssetDropOps.ts +0 -1
- package/src/hooks/useTimelineDeleteOps.ts +0 -1
- package/src/hooks/useTimelineEditing.test.tsx +0 -1
- package/src/hooks/useTimelineEditingTypes.ts +0 -2
- package/src/hostMount.test.tsx +53 -22
- package/src/main.tsx +3 -0
- package/src/player/components/CompositionThumbnail.tsx +14 -0
- package/src/player/components/Player.test.ts +49 -0
- package/src/player/components/Player.tsx +21 -7
- package/src/player/components/timelineClipDragCommit.test.ts +7 -39
- package/src/player/hooks/timelineDomBinding.test.ts +50 -0
- package/src/player/hooks/timelineSyncHydration.test.ts +25 -1
- package/src/player/hooks/timelineSyncHydration.ts +3 -3
- package/src/player/hooks/useShadowPreviewReload.ts +1 -0
- package/src/player/hooks/useTimelinePlayer.play.test.ts +120 -0
- package/src/player/hooks/useTimelinePlayer.ts +1 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +2 -0
- package/src/player/lib/automationStoreSync.test.ts +24 -0
- package/src/player/lib/automationStoreSync.ts +3 -9
- package/src/player/lib/playbackScrub.ts +1 -1
- package/src/player/lib/timelineDOM.test.ts +28 -0
- package/src/player/lib/timelineDOM.ts +4 -2
- package/src/player/lib/timelineElementHelpers.ts +78 -9
- package/src/player/lib/timelineIframeHelpers.test.ts +49 -5
- package/src/player/lib/timelineIframeHelpers.ts +13 -12
- package/src/player/store/playerStore.test.ts +35 -1
- package/src/player/store/playerStore.ts +29 -0
- package/src/player/store/readinessSlice.ts +34 -6
- package/src/utils/blockInstaller.ts +0 -3
- package/src/utils/previewPrefetch.test.ts +22 -0
- package/src/utils/previewPrefetch.ts +14 -0
- package/src/utils/razorSplitTransaction.test.ts +0 -1
- package/src/utils/razorSplitTransaction.ts +1 -1
- package/src/utils/sdkEditTransaction.ts +0 -3
- package/src/utils/studioFileHistory.test.ts +0 -9
- package/src/utils/studioFileHistory.ts +1 -5
- package/src/utils/studioUiPreferences.test.ts +9 -3
- package/src/utils/studioUiPreferences.ts +0 -20
- package/src/utils/timelineCompositionInsert.ts +0 -1
- package/dist/assets/hyperframes-player-WbBVbWuO.js +0 -464
- package/dist/chunk-7BP32G62.js.map +0 -1
- package/src/hooks/usePersistentEditHistory.projectOwnership.test.tsx +0 -60
- package/src/utils/editHistory.test.ts +0 -534
- package/src/utils/editHistory.ts +0 -236
- package/src/utils/editHistoryStorage.test.ts +0 -37
- package/src/utils/editHistoryStorage.ts +0 -99
- /package/dist/{domEditingLayers-UKMVYXT6.js.map → domEditingLayers-IGPLTI5E.js.map} +0 -0
|
@@ -33,6 +33,30 @@ beforeEach(() => {
|
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
describe("syncStoredAutomationFromPreview", () => {
|
|
36
|
+
it("reads the element the timeline bound, not an earlier same-id copy in a sub-composition", () => {
|
|
37
|
+
usePlayerStore.setState({ elements: [el({ hfId: "hf-root" })] });
|
|
38
|
+
const doc = document.implementation.createHTMLDocument("preview");
|
|
39
|
+
doc.body.innerHTML =
|
|
40
|
+
'<div data-composition-id="strip"><audio id="bgm" data-hf-id="hf-inner"></audio></div>' +
|
|
41
|
+
'<audio id="bgm" data-hf-id="hf-root"></audio>';
|
|
42
|
+
doc.querySelector('[data-hf-id="hf-inner"]')?.setAttribute("data-automation", TWO_POINTS);
|
|
43
|
+
doc.querySelector('[data-hf-id="hf-root"]')?.setAttribute("data-automation", RESTORED);
|
|
44
|
+
syncStoredAutomationFromPreview(doc);
|
|
45
|
+
expect(usePlayerStore.getState().elements[0]?.automation).toBe(RESTORED);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("reads the copy in the element's own file when a sub-composition repeats its hf-id", () => {
|
|
49
|
+
usePlayerStore.setState({ elements: [el({ hfId: "hf-bgm" })] });
|
|
50
|
+
const doc = document.implementation.createHTMLDocument("preview");
|
|
51
|
+
doc.body.innerHTML =
|
|
52
|
+
'<div data-composition-id="strip" data-composition-src="compositions/strip.html">' +
|
|
53
|
+
'<audio id="bgm" data-hf-id="hf-bgm"></audio></div><audio id="bgm" data-hf-id="hf-bgm"></audio>';
|
|
54
|
+
doc.querySelectorAll("audio")[0]?.setAttribute("data-automation", TWO_POINTS);
|
|
55
|
+
doc.querySelectorAll("audio")[1]?.setAttribute("data-automation", RESTORED);
|
|
56
|
+
syncStoredAutomationFromPreview(doc);
|
|
57
|
+
expect(usePlayerStore.getState().elements[0]?.automation).toBe(RESTORED);
|
|
58
|
+
});
|
|
59
|
+
|
|
36
60
|
it("reads back an envelope an undo restored on the preview", () => {
|
|
37
61
|
// The bug: a soft undo patches the preview document and re-runs the timeline, but
|
|
38
62
|
// the store keeps its own copy of the attributes and that copy is what a lane
|
|
@@ -17,14 +17,7 @@ import { HF_AUDIO_AUTOMATION_ATTR } from "@hyperframes/core/audio-automation";
|
|
|
17
17
|
import { HF_AUDIO_FX_ATTR } from "@hyperframes/core/audio-fx";
|
|
18
18
|
import { usePlayerStore, type TimelineElement } from "../store/playerStore";
|
|
19
19
|
import { groupInfoFor } from "./timelineGroupInfo";
|
|
20
|
-
|
|
21
|
-
/** The preview node an element stands for, by dom id and then by `data-hf-id`. */
|
|
22
|
-
function previewNodeFor(doc: Document, element: TimelineElement): Element | null {
|
|
23
|
-
const domId = element.domId ?? element.id;
|
|
24
|
-
const byId = domId ? doc.getElementById(domId) : null;
|
|
25
|
-
if (byId) return byId;
|
|
26
|
-
return element.hfId ? doc.querySelector(`[data-hf-id="${element.hfId}"]`) : null;
|
|
27
|
-
}
|
|
20
|
+
import { previewElementFinder } from "./timelineElementHelpers";
|
|
28
21
|
|
|
29
22
|
/**
|
|
30
23
|
* Re-read every element's automation and FX-chain attributes from the preview
|
|
@@ -62,10 +55,11 @@ function syncedFields(doc: Document, element: TimelineElement, node: Element) {
|
|
|
62
55
|
|
|
63
56
|
export function syncStoredAutomationFromPreview(doc: Document | null | undefined): void {
|
|
64
57
|
if (!doc) return;
|
|
58
|
+
const findNode = previewElementFinder(doc);
|
|
65
59
|
usePlayerStore.setState((state) => {
|
|
66
60
|
let changed = false;
|
|
67
61
|
const elements = state.elements.map((element) => {
|
|
68
|
-
const node =
|
|
62
|
+
const node = findNode(element);
|
|
69
63
|
if (!node) return element;
|
|
70
64
|
const fields = syncedFields(doc, element, node);
|
|
71
65
|
// Same array back when nothing moved: `elements` keys memos all over the
|
|
@@ -12,5 +12,5 @@ export function scrubMusicAtSeek(iframe: HTMLIFrameElement | null, nextTime: num
|
|
|
12
12
|
if (!music || s.audioMuted) return;
|
|
13
13
|
const rel = nextTime - music.start;
|
|
14
14
|
const audioFileTime = rel >= 0 && rel <= music.duration ? (music.playbackStart ?? 0) + rel : null;
|
|
15
|
-
scrubPreviewAudio(iframe, audioFileTime, music
|
|
15
|
+
scrubPreviewAudio(iframe, audioFileTime, music, s.audioVolume);
|
|
16
16
|
}
|
|
@@ -285,6 +285,34 @@ describe("createTimelineElementFromManifestClip — source-scoped selector ident
|
|
|
285
285
|
});
|
|
286
286
|
});
|
|
287
287
|
|
|
288
|
+
it("keeps the host it was given when a sub-composition is mounted twice", () => {
|
|
289
|
+
const doc = makeDoc(`
|
|
290
|
+
<div data-composition-id="root" data-composition-file="index.html">
|
|
291
|
+
<div id="a" data-hf-id="hf-a" data-composition-id="card" data-composition-src="card.html"></div>
|
|
292
|
+
<div id="b" data-hf-id="hf-b" data-composition-id="card" data-composition-src="card.html"></div>
|
|
293
|
+
</div>
|
|
294
|
+
`);
|
|
295
|
+
const element = createTimelineElementFromManifestClip({
|
|
296
|
+
clip: {
|
|
297
|
+
id: "b",
|
|
298
|
+
label: "Card",
|
|
299
|
+
kind: "composition",
|
|
300
|
+
tagName: "div",
|
|
301
|
+
start: 4,
|
|
302
|
+
duration: 4,
|
|
303
|
+
track: 0,
|
|
304
|
+
compositionId: "card",
|
|
305
|
+
parentCompositionId: "root",
|
|
306
|
+
compositionSrc: null,
|
|
307
|
+
assetUrl: null,
|
|
308
|
+
},
|
|
309
|
+
fallbackIndex: 1,
|
|
310
|
+
doc,
|
|
311
|
+
hostEl: doc.getElementById("b"),
|
|
312
|
+
});
|
|
313
|
+
expect(element).toMatchObject({ domId: "b", hfId: "hf-b", compositionSrc: "card.html" });
|
|
314
|
+
});
|
|
315
|
+
|
|
288
316
|
it("ignores an index.html duplicate when indexing a scene.html selector", () => {
|
|
289
317
|
const doc = makeDoc(`
|
|
290
318
|
<div data-composition-id="root" data-composition-file="index.html">
|
|
@@ -171,8 +171,10 @@ export function createTimelineElementFromManifestClip(params: {
|
|
|
171
171
|
entry.playbackRate ??= 1;
|
|
172
172
|
let resolvedSrc = clip.compositionSrc;
|
|
173
173
|
if (!resolvedSrc) {
|
|
174
|
-
hostEl
|
|
175
|
-
|
|
174
|
+
if (hostEl?.getAttribute("data-composition-id") !== clip.compositionId) {
|
|
175
|
+
hostEl =
|
|
176
|
+
doc?.querySelector(`[data-composition-id="${CSS.escape(clip.compositionId)}"]`) ?? hostEl;
|
|
177
|
+
}
|
|
176
178
|
resolvedSrc =
|
|
177
179
|
hostEl?.getAttribute("data-composition-src") ??
|
|
178
180
|
hostEl?.getAttribute("data-composition-file") ??
|
|
@@ -428,14 +428,83 @@ function nodeMatchesManifestClip(node: Element, clip: ClipManifestClip): boolean
|
|
|
428
428
|
});
|
|
429
429
|
}
|
|
430
430
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
431
|
+
/** Whether `node` sits in the composition the clip was read from, the chain the runtime records outermost first
|
|
432
|
+
* (`resolveNearestCompositionContext` in core's runtime/timeline.ts). A clip without that scope accepts any node. */
|
|
433
|
+
function nodeInClipScope(node: Element, clip: ClipManifestClip): boolean {
|
|
434
|
+
const scope = clip.compositionAncestors;
|
|
435
|
+
if (!scope) return true;
|
|
436
|
+
const ids: string[] = [];
|
|
437
|
+
for (let cursor = node.parentElement; cursor; cursor = cursor.parentElement) {
|
|
438
|
+
const id = cursor.getAttribute("data-composition-id");
|
|
439
|
+
if (id) ids.unshift(id);
|
|
440
|
+
}
|
|
441
|
+
return ids.length === scope.length && ids.every((id, index) => id === scope[index]);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** The first match in the clip's composition across `selectors`; a lone match stands only when none is in scope, as a
|
|
445
|
+
* healed host can stale the clip's chain for a pass. An id can repeat in a sub-composition earlier in the document. */
|
|
446
|
+
function findInClipScope(
|
|
447
|
+
doc: Document,
|
|
448
|
+
clip: ClipManifestClip,
|
|
449
|
+
selectors: string[],
|
|
450
|
+
): Element | null {
|
|
451
|
+
let lone: Element | null = null;
|
|
452
|
+
for (const selector of selectors) {
|
|
453
|
+
const nodes = Array.from(doc.querySelectorAll(selector));
|
|
454
|
+
const scoped = nodes.find((node) => nodeInClipScope(node, clip));
|
|
455
|
+
if (scoped) return scoped;
|
|
456
|
+
if (nodes.length === 1) lone ??= nodes[0];
|
|
457
|
+
}
|
|
458
|
+
return lone;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export type PreviewTarget = Pick<TimelineElement, "hfId" | "domId" | "id" | "sourceFile">;
|
|
462
|
+
|
|
463
|
+
/** Finds a row's preview element by `data-hf-id`, then id, preferring one in the row's own file: both repeat across
|
|
464
|
+
* files. Indexes the document once, so a pass over every row costs one scan. */
|
|
465
|
+
export function previewElementFinder(
|
|
466
|
+
doc: Document,
|
|
467
|
+
selector = "[data-hf-id], [id]",
|
|
468
|
+
): (target: PreviewTarget) => Element | null {
|
|
469
|
+
const byKey = new Map<string, Element[]>();
|
|
470
|
+
const add = (key: string, node: Element) => {
|
|
471
|
+
const nodes = byKey.get(key);
|
|
472
|
+
if (nodes) nodes.push(node);
|
|
473
|
+
else byKey.set(key, [node]);
|
|
474
|
+
};
|
|
475
|
+
for (const node of doc.querySelectorAll(selector)) {
|
|
476
|
+
const hfId = node.getAttribute("data-hf-id");
|
|
477
|
+
const id = node.getAttribute("id");
|
|
478
|
+
if (hfId) add(`hf:${hfId}`, node);
|
|
479
|
+
if (id) add(`id:${id}`, node);
|
|
480
|
+
}
|
|
481
|
+
return (target) => {
|
|
482
|
+
const matches = [
|
|
483
|
+
...((target.hfId && byKey.get(`hf:${target.hfId}`)) || []),
|
|
484
|
+
...(byKey.get(`id:${target.domId ?? target.id}`) ?? []),
|
|
485
|
+
];
|
|
486
|
+
return (
|
|
487
|
+
matches.find((node) => getTimelineElementSourceFile(node) === target.sourceFile) ??
|
|
488
|
+
matches[0] ??
|
|
489
|
+
null
|
|
490
|
+
);
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export function findClipElementById(doc: Document, clip: ClipManifestClip): Element | null {
|
|
495
|
+
if (!clip.id) return null;
|
|
496
|
+
const first = doc.getElementById(clip.id);
|
|
497
|
+
if (!first || nodeInClipScope(first, clip)) return first;
|
|
498
|
+
return findInClipScope(doc, clip, [`[id="${CSS.escape(clip.id)}"]`]);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function findTimelineDomNode(doc: Document, clip: ClipManifestClip): Element | null {
|
|
502
|
+
if (!clip.id) return null;
|
|
503
|
+
const first = doc.getElementById(clip.id);
|
|
504
|
+
if (first && nodeInClipScope(first, clip)) return first;
|
|
505
|
+
const id = CSS.escape(clip.id);
|
|
506
|
+
const byOtherKeys = [`[data-hf-id="${id}"]`, `[data-composition-id="${id}"]`, `.${id}`];
|
|
507
|
+
return findInClipScope(doc, clip, first ? [`[id="${id}"]`, ...byOtherKeys] : byOtherKeys);
|
|
439
508
|
}
|
|
440
509
|
|
|
441
510
|
export function findTimelineDomNodeForClip(
|
|
@@ -445,7 +514,7 @@ export function findTimelineDomNodeForClip(
|
|
|
445
514
|
usedNodes = new Set<Element>(),
|
|
446
515
|
getCandidates = () => getTimelineDomNodes(doc),
|
|
447
516
|
): Element | null {
|
|
448
|
-
const byIdentity =
|
|
517
|
+
const byIdentity = findTimelineDomNode(doc, clip);
|
|
449
518
|
if (byIdentity && !usedNodes.has(byIdentity) && nodeMatchesClipTag(byIdentity, clip))
|
|
450
519
|
return byIdentity;
|
|
451
520
|
|
|
@@ -17,6 +17,31 @@ function makeDoc(html: string): Document {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
describe("buildMissingCompositionElements — hfId (R7)", () => {
|
|
20
|
+
it("labels a row from its own host when a sub-composition repeats the host's id", () => {
|
|
21
|
+
const doc = makeDoc(`
|
|
22
|
+
<div data-composition-id="main">
|
|
23
|
+
<div data-composition-id="strip" data-composition-src="compositions/strip.html"><div id="scene"></div></div>
|
|
24
|
+
<div id="scene" data-hf-id="hf-scene" data-composition-id="scene" data-composition-file="compositions/scene.html"></div>
|
|
25
|
+
</div>
|
|
26
|
+
`);
|
|
27
|
+
const row = {
|
|
28
|
+
id: "scene",
|
|
29
|
+
key: "scene",
|
|
30
|
+
tag: "div",
|
|
31
|
+
start: 0,
|
|
32
|
+
duration: 4,
|
|
33
|
+
track: 0,
|
|
34
|
+
hfId: "hf-scene",
|
|
35
|
+
};
|
|
36
|
+
const { updatedEls, patched } = buildMissingCompositionElements(
|
|
37
|
+
doc,
|
|
38
|
+
window as IframeWindow,
|
|
39
|
+
[row],
|
|
40
|
+
10,
|
|
41
|
+
);
|
|
42
|
+
expect([patched, updatedEls[0]?.compositionSrc]).toEqual([true, "compositions/scene.html"]);
|
|
43
|
+
});
|
|
44
|
+
|
|
20
45
|
it("harvests hfId from data-hf-id on composition host elements", () => {
|
|
21
46
|
const doc = makeDoc(`
|
|
22
47
|
<div data-composition-id="root">
|
|
@@ -104,7 +129,7 @@ describe("scrubPreviewAudio", () => {
|
|
|
104
129
|
audio.pause = vi.fn();
|
|
105
130
|
iframe.contentDocument.body.append(audio);
|
|
106
131
|
|
|
107
|
-
scrubPreviewAudio(iframe, 0.5, "music", 0.4);
|
|
132
|
+
scrubPreviewAudio(iframe, 0.5, { id: "music" }, 0.4);
|
|
108
133
|
|
|
109
134
|
expect(audio.volume).toBeCloseTo(0.1);
|
|
110
135
|
stopScrubPreviewAudio();
|
|
@@ -112,13 +137,13 @@ describe("scrubPreviewAudio", () => {
|
|
|
112
137
|
|
|
113
138
|
/**
|
|
114
139
|
* The preview document is a different realm, so `instanceof HTMLAudioElement`
|
|
115
|
-
* is false for every node in it. That threw the `
|
|
140
|
+
* is false for every node in it. That threw the `music` hint away and left
|
|
116
141
|
* the first `<audio>` in the document as the only route — and the first
|
|
117
142
|
* `<audio>` is often the voiceover, so scrubbing previewed the wrong track.
|
|
118
143
|
* Two elements, music second, is what tells the two paths apart: with one
|
|
119
144
|
* element the fallback reaches the right node by accident.
|
|
120
145
|
*/
|
|
121
|
-
it("previews the track named by
|
|
146
|
+
it("previews the track named by the music row, not the first audio in the document", () => {
|
|
122
147
|
const iframe = document.createElement("iframe");
|
|
123
148
|
document.body.append(iframe);
|
|
124
149
|
const previewDoc = iframe.contentDocument;
|
|
@@ -139,13 +164,32 @@ describe("scrubPreviewAudio", () => {
|
|
|
139
164
|
// The node really is cross-realm; this is the condition, not a contrivance.
|
|
140
165
|
expect(music instanceof HTMLAudioElement).toBe(false);
|
|
141
166
|
|
|
142
|
-
scrubPreviewAudio(iframe, 0.5, "music-bed", 1);
|
|
167
|
+
scrubPreviewAudio(iframe, 0.5, { id: "music-bed" }, 1);
|
|
143
168
|
|
|
144
169
|
expect(music.play).toHaveBeenCalled();
|
|
145
170
|
expect(voiceover.play).not.toHaveBeenCalled();
|
|
146
171
|
stopScrubPreviewAudio();
|
|
147
172
|
});
|
|
148
173
|
|
|
174
|
+
it("previews the music row's own track when a sub-composition repeats its id", () => {
|
|
175
|
+
const iframe = document.createElement("iframe");
|
|
176
|
+
document.body.append(iframe);
|
|
177
|
+
const previewDoc = iframe.contentDocument;
|
|
178
|
+
if (!previewDoc?.body) throw new Error("expected an iframe document");
|
|
179
|
+
previewDoc.body.innerHTML =
|
|
180
|
+
'<div data-composition-id="strip" data-composition-src="compositions/strip.html"><audio id="music"></audio></div>' +
|
|
181
|
+
'<audio id="music" class="root"></audio>';
|
|
182
|
+
const [inner, root] = Array.from(previewDoc.querySelectorAll("audio"));
|
|
183
|
+
for (const audio of [inner, root])
|
|
184
|
+
Object.assign(audio!, { play: vi.fn(async () => {}), pause: vi.fn() });
|
|
185
|
+
|
|
186
|
+
scrubPreviewAudio(iframe, 0.5, { id: "music" }, 1);
|
|
187
|
+
|
|
188
|
+
expect(root!.play).toHaveBeenCalled();
|
|
189
|
+
expect(inner!.play).not.toHaveBeenCalled();
|
|
190
|
+
stopScrubPreviewAudio();
|
|
191
|
+
});
|
|
192
|
+
|
|
149
193
|
/** A scrub audition is media running under a paused clock, which the runtime now
|
|
150
194
|
* stops on sight. So it borrows the element. That a leased element survives the
|
|
151
195
|
* tick is asserted runtime-side in core's `transportPark.test.ts`; here the
|
|
@@ -166,7 +210,7 @@ describe("scrubPreviewAudio", () => {
|
|
|
166
210
|
const releasePausedMedia = vi.fn();
|
|
167
211
|
(previewDoc.defaultView as IframeWindow).__hf = { leasePausedMedia, releasePausedMedia };
|
|
168
212
|
|
|
169
|
-
scrubPreviewAudio(iframe, 0.5, "music", 1);
|
|
213
|
+
scrubPreviewAudio(iframe, 0.5, { id: "music" }, 1);
|
|
170
214
|
|
|
171
215
|
expect(leasePausedMedia).toHaveBeenCalledWith(music);
|
|
172
216
|
expect(releasePausedMedia).not.toHaveBeenCalled();
|
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
getTimelineElementDisplayLabel,
|
|
21
21
|
buildTimelineElementIdentity,
|
|
22
22
|
readTimelineElementZIndex,
|
|
23
|
+
previewElementFinder,
|
|
24
|
+
type PreviewTarget,
|
|
23
25
|
} from "./timelineElementHelpers";
|
|
24
26
|
import { postRuntimeControlMessage } from "./runtimeProtocol";
|
|
25
27
|
import { transitionLabelsForDocument } from "./timelineTransitionMetadata";
|
|
@@ -216,14 +218,14 @@ let scrubStopTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
216
218
|
let scrubPrevMuted: boolean | null = null;
|
|
217
219
|
let scrubPrevVolume: number | null = null;
|
|
218
220
|
|
|
219
|
-
// Resolve the SAME element the store identified as music: prefer
|
|
220
|
-
// the role attribute, and only fall back to the first <audio>
|
|
221
|
-
// voiceover, so the
|
|
221
|
+
// Resolve the SAME element the store identified as music: prefer that row's own
|
|
222
|
+
// element, then the role attribute, and only fall back to the first <audio>
|
|
223
|
+
// (which could be a voiceover, so the row hint matters).
|
|
222
224
|
/**
|
|
223
225
|
* `doc` is the preview iframe's document, so its `<audio>` nodes are instances of
|
|
224
226
|
* the IFRAME's `HTMLAudioElement`, never this module's. `instanceof
|
|
225
227
|
* HTMLAudioElement` here is false for every one of them, which silently threw the
|
|
226
|
-
* `
|
|
228
|
+
* `music` hint away and fell through to "first `<audio>` in the document" — the
|
|
227
229
|
* very thing the comment above warns can be a voiceover. Ask what the node IS.
|
|
228
230
|
* Same rule and same reasoning as packages/core/src/runtime/domRealm.ts.
|
|
229
231
|
*/
|
|
@@ -235,9 +237,9 @@ function isAudioNode(node: Element | null): node is HTMLAudioElement {
|
|
|
235
237
|
);
|
|
236
238
|
}
|
|
237
239
|
|
|
238
|
-
function resolveScrubAudioEl(doc: Document,
|
|
239
|
-
if (
|
|
240
|
-
const byId = doc
|
|
240
|
+
function resolveScrubAudioEl(doc: Document, music?: PreviewTarget | null): HTMLAudioElement | null {
|
|
241
|
+
if (music) {
|
|
242
|
+
const byId = previewElementFinder(doc, "audio")(music);
|
|
241
243
|
if (isAudioNode(byId)) return byId;
|
|
242
244
|
}
|
|
243
245
|
return (
|
|
@@ -283,7 +285,7 @@ function applyScrub(el: HTMLAudioElement, audioFileTime: number, previewVolume:
|
|
|
283
285
|
export function scrubPreviewAudio(
|
|
284
286
|
iframe: HTMLIFrameElement | null,
|
|
285
287
|
audioFileTime: number | null,
|
|
286
|
-
|
|
288
|
+
music?: PreviewTarget | null,
|
|
287
289
|
previewVolume = 1,
|
|
288
290
|
): void {
|
|
289
291
|
if (!iframe) return;
|
|
@@ -298,7 +300,7 @@ export function scrubPreviewAudio(
|
|
|
298
300
|
return;
|
|
299
301
|
}
|
|
300
302
|
if (!doc) return;
|
|
301
|
-
const el = resolveScrubAudioEl(doc,
|
|
303
|
+
const el = resolveScrubAudioEl(doc, music);
|
|
302
304
|
if (el) applyScrub(el, audioFileTime, previewVolume);
|
|
303
305
|
}
|
|
304
306
|
|
|
@@ -512,12 +514,11 @@ export function buildMissingCompositionElements(
|
|
|
512
514
|
|
|
513
515
|
// Patch existing elements that are missing compositionSrc
|
|
514
516
|
let patched = false;
|
|
517
|
+
const findHost = previewElementFinder(doc);
|
|
515
518
|
const updatedEls = (currentEls as TimelineElement[]).map((existing) => {
|
|
516
519
|
if (existing.compositionSrc) return existing;
|
|
517
|
-
// Find the matching DOM host by element id or composition id
|
|
518
520
|
const host =
|
|
519
|
-
doc.
|
|
520
|
-
doc.querySelector(`[data-composition-id="${CSS.escape(existing.id)}"]`);
|
|
521
|
+
findHost(existing) ?? doc.querySelector(`[data-composition-id="${CSS.escape(existing.id)}"]`);
|
|
521
522
|
if (!host) return existing;
|
|
522
523
|
const compSrc =
|
|
523
524
|
host.getAttribute("data-composition-src") || host.getAttribute("data-composition-file");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
2
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
3
|
-
import { usePlayerStore, liveTime, type TimelineElement } from "./playerStore";
|
|
3
|
+
import { usePlayerStore, liveTime, whenPreviewBooted, type TimelineElement } from "./playerStore";
|
|
4
4
|
import { thumbnailRevisionOf } from "./thumbnailSlice";
|
|
5
5
|
|
|
6
6
|
/** The playback/selection state `reset()` restores (persistent prefs asserted separately). */
|
|
@@ -343,9 +343,20 @@ describe("usePlayerStore", () => {
|
|
|
343
343
|
const video = doc.createElement("video");
|
|
344
344
|
Object.defineProperty(video, "readyState", { value: 0, configurable: true });
|
|
345
345
|
doc.body.appendChild(video);
|
|
346
|
+
usePlayerStore.getState().markPreviewLoadStep(doc);
|
|
346
347
|
return { doc, video };
|
|
347
348
|
}
|
|
348
349
|
|
|
350
|
+
it("waits for the document's load step before marking it ready", async () => {
|
|
351
|
+
const doc = document.implementation.createHTMLDocument("composition");
|
|
352
|
+
usePlayerStore.getState().requestTimelineReady(doc);
|
|
353
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
354
|
+
expect(usePlayerStore.getState().timelineReady).toBe(false);
|
|
355
|
+
|
|
356
|
+
usePlayerStore.getState().markPreviewLoadStep(doc);
|
|
357
|
+
await vi.waitFor(() => expect(usePlayerStore.getState().timelineReady).toBe(true));
|
|
358
|
+
});
|
|
359
|
+
|
|
349
360
|
it("resolves timelineReady once the doc's media settles", async () => {
|
|
350
361
|
const { doc, video } = stalledVideoDoc();
|
|
351
362
|
usePlayerStore.getState().requestTimelineReady(doc);
|
|
@@ -748,3 +759,26 @@ describe("liveTime", () => {
|
|
|
748
759
|
expect(result).toBe(false);
|
|
749
760
|
});
|
|
750
761
|
});
|
|
762
|
+
|
|
763
|
+
describe("whenPreviewBooted", () => {
|
|
764
|
+
it("resolves false for a project the user left before its preview booted", async () => {
|
|
765
|
+
usePlayerStore.setState({ timelineProjectId: "project-a", previewBooted: false });
|
|
766
|
+
const waitA = whenPreviewBooted("project-a");
|
|
767
|
+
|
|
768
|
+
usePlayerStore.getState().beginTimelineSession("project-b");
|
|
769
|
+
usePlayerStore.getState().markPreviewBooted();
|
|
770
|
+
|
|
771
|
+
await expect(waitA).resolves.toBe(false);
|
|
772
|
+
await expect(whenPreviewBooted("project-b")).resolves.toBe(true);
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
it("waits through the open of its own project, then resolves on that project's boot", async () => {
|
|
776
|
+
usePlayerStore.setState({ timelineProjectId: "project-a", previewBooted: true });
|
|
777
|
+
const waitB = whenPreviewBooted("project-b");
|
|
778
|
+
|
|
779
|
+
usePlayerStore.getState().beginTimelineSession("project-b");
|
|
780
|
+
usePlayerStore.getState().markPreviewBooted();
|
|
781
|
+
|
|
782
|
+
await expect(waitB).resolves.toBe(true);
|
|
783
|
+
});
|
|
784
|
+
});
|
|
@@ -551,3 +551,32 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
|
|
|
551
551
|
}));
|
|
552
552
|
|
|
553
553
|
attachPlayerStoreDevHandle(usePlayerStore);
|
|
554
|
+
|
|
555
|
+
export function isPreviewBooted(projectId: string): boolean {
|
|
556
|
+
const { previewBooted, timelineProjectId } = usePlayerStore.getState();
|
|
557
|
+
return previewBooted && timelineProjectId === projectId;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** True once projectId's live preview has booted, false once another project replaces it.
|
|
561
|
+
* Open-time work the first frame does not need (server parses, lint) waits on it. */
|
|
562
|
+
export function whenPreviewBooted(projectId: string): Promise<boolean> {
|
|
563
|
+
const openedFrom = usePlayerStore.getState().timelineProjectId;
|
|
564
|
+
let seen = false;
|
|
565
|
+
const settle = (state: PlayerState): boolean | null => {
|
|
566
|
+
if (state.timelineProjectId === projectId) {
|
|
567
|
+
seen = true;
|
|
568
|
+
return state.previewBooted ? true : null;
|
|
569
|
+
}
|
|
570
|
+
return seen || state.timelineProjectId !== openedFrom ? false : null;
|
|
571
|
+
};
|
|
572
|
+
return new Promise((resolve) => {
|
|
573
|
+
const now = settle(usePlayerStore.getState());
|
|
574
|
+
if (now !== null) return resolve(now);
|
|
575
|
+
const stop = usePlayerStore.subscribe((state) => {
|
|
576
|
+
const result = settle(state);
|
|
577
|
+
if (result === null) return;
|
|
578
|
+
stop();
|
|
579
|
+
resolve(result);
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
}
|
|
@@ -8,26 +8,45 @@ import { settleCompositionReadiness } from "@hyperframes/core/composition-readin
|
|
|
8
8
|
|
|
9
9
|
export interface PlaybackReadinessSlice {
|
|
10
10
|
timelineReady: boolean;
|
|
11
|
+
/** Latched when the project's first preview shows its first frame (or fails), kept through edit
|
|
12
|
+
* reloads, so work that must not compete with the boot waits for it once. */
|
|
13
|
+
previewBooted: boolean;
|
|
11
14
|
setTimelineReady: (ready: boolean) => void;
|
|
15
|
+
markPreviewBooted: () => void;
|
|
12
16
|
/** Sets timelineReady once doc's readiness inputs settle, or immediately
|
|
13
|
-
* if doc is null. A wait a later call supersedes never wins the race.
|
|
17
|
+
* if doc is null. A wait a later call supersedes never wins the race.
|
|
18
|
+
* Waits for doc's load step first: it pauses and rewinds the preview, so a
|
|
19
|
+
* Play enabled before it would be undone. */
|
|
14
20
|
requestTimelineReady: (doc: Document | null) => void;
|
|
21
|
+
/** Called by the preview's load step for the document it ran on. */
|
|
22
|
+
markPreviewLoadStep: (doc: Document) => void;
|
|
15
23
|
}
|
|
16
24
|
|
|
17
25
|
let timelineReadyGeneration = 0;
|
|
26
|
+
const loadStepDocs = new WeakSet<Document>();
|
|
27
|
+
let awaitingLoadStep: { doc: Document; generation: number } | null = null;
|
|
18
28
|
|
|
19
29
|
/** For a full timeline reset: bumps the generation so any requestTimelineReady
|
|
20
30
|
* wait in flight can never resolve into what replaced it. */
|
|
21
|
-
export function resetPlaybackReadinessState(): Pick<
|
|
31
|
+
export function resetPlaybackReadinessState(): Pick<
|
|
32
|
+
PlaybackReadinessSlice,
|
|
33
|
+
"timelineReady" | "previewBooted"
|
|
34
|
+
> {
|
|
22
35
|
timelineReadyGeneration++;
|
|
23
|
-
return { timelineReady: false };
|
|
36
|
+
return { timelineReady: false, previewBooted: false };
|
|
24
37
|
}
|
|
25
38
|
|
|
26
39
|
export function createPlaybackReadinessSlice(
|
|
27
40
|
set: StoreApi<PlaybackReadinessSlice>["setState"],
|
|
28
41
|
): PlaybackReadinessSlice {
|
|
42
|
+
const settle = (doc: Document, generation: number) =>
|
|
43
|
+
settleCompositionReadiness(doc, () => {
|
|
44
|
+
if (generation === timelineReadyGeneration) set({ timelineReady: true });
|
|
45
|
+
});
|
|
29
46
|
return {
|
|
30
47
|
timelineReady: false,
|
|
48
|
+
previewBooted: false,
|
|
49
|
+
markPreviewBooted: () => set({ previewBooted: true }),
|
|
31
50
|
setTimelineReady: (ready) => {
|
|
32
51
|
timelineReadyGeneration++;
|
|
33
52
|
set({ timelineReady: ready });
|
|
@@ -35,9 +54,18 @@ export function createPlaybackReadinessSlice(
|
|
|
35
54
|
requestTimelineReady: (doc) => {
|
|
36
55
|
const generation = ++timelineReadyGeneration;
|
|
37
56
|
if (!doc) return set({ timelineReady: true });
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
57
|
+
if (!loadStepDocs.has(doc)) {
|
|
58
|
+
awaitingLoadStep = { doc, generation };
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
settle(doc, generation);
|
|
62
|
+
},
|
|
63
|
+
markPreviewLoadStep: (doc) => {
|
|
64
|
+
loadStepDocs.add(doc);
|
|
65
|
+
if (awaitingLoadStep?.doc !== doc) return;
|
|
66
|
+
const { generation } = awaitingLoadStep;
|
|
67
|
+
awaitingLoadStep = null;
|
|
68
|
+
settle(doc, generation);
|
|
41
69
|
},
|
|
42
70
|
};
|
|
43
71
|
}
|
|
@@ -9,7 +9,6 @@ import { collectHtmlIds } from "./studioHelpers";
|
|
|
9
9
|
import { generateId } from "./generateId";
|
|
10
10
|
import { formatTimelineAttributeNumber } from "../player/components/timelineEditing";
|
|
11
11
|
import { saveProjectFilesWithHistory } from "./studioFileHistory";
|
|
12
|
-
import type { EditHistoryKind } from "./editHistory";
|
|
13
12
|
import { extendRootDurationInSource } from "./rootDuration";
|
|
14
13
|
import { deriveTimelineStoreKeyForDomId } from "../player/lib/timelineElementHelpers";
|
|
15
14
|
|
|
@@ -41,7 +40,6 @@ interface AddBlockOptions {
|
|
|
41
40
|
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
42
41
|
recordEdit: (entry: {
|
|
43
42
|
label: string;
|
|
44
|
-
kind: EditHistoryKind;
|
|
45
43
|
coalesceKey?: string;
|
|
46
44
|
files: Record<string, { before: string; after: string }>;
|
|
47
45
|
}) => Promise<void>;
|
|
@@ -234,7 +232,6 @@ export async function addBlockToProject(
|
|
|
234
232
|
await saveProjectFilesWithHistory({
|
|
235
233
|
projectId,
|
|
236
234
|
label: `Add ${isBlock ? "block" : "component"}: ${block.title}`,
|
|
237
|
-
kind: "timeline",
|
|
238
235
|
files: { [targetPath]: patchedContent },
|
|
239
236
|
readFile: async () => originalContent,
|
|
240
237
|
writeFile: writeProjectFile,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { prefetchPreviewForHash } from "./previewPrefetch";
|
|
4
|
+
|
|
5
|
+
describe("prefetchPreviewForHash", () => {
|
|
6
|
+
afterEach(() => vi.unstubAllGlobals());
|
|
7
|
+
|
|
8
|
+
it("requests the opening project's preview once, not on every hash update", () => {
|
|
9
|
+
const fetchStub = vi.fn(() => Promise.resolve(new Response("")));
|
|
10
|
+
vi.stubGlobal("fetch", fetchStub);
|
|
11
|
+
|
|
12
|
+
prefetchPreviewForHash("#project/demo");
|
|
13
|
+
prefetchPreviewForHash("#project/demo?t=2&tab=design");
|
|
14
|
+
prefetchPreviewForHash("#project/other");
|
|
15
|
+
prefetchPreviewForHash("#settings");
|
|
16
|
+
|
|
17
|
+
expect(fetchStub.mock.calls.map(([url]) => new URL(String(url)).pathname)).toEqual([
|
|
18
|
+
"/api/projects/demo/preview",
|
|
19
|
+
"/api/projects/other/preview",
|
|
20
|
+
]);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { applyPreviewVariablesToUrl } from "../hooks/previewVariablesStore";
|
|
2
|
+
import { buildProjectApiPath, parseProjectIdFromHash } from "./projectRouting";
|
|
3
|
+
|
|
4
|
+
let prefetched: string | null = null;
|
|
5
|
+
|
|
6
|
+
/** Requests the URL's project preview while Studio boots, so the server builds it in parallel. */
|
|
7
|
+
export function prefetchPreviewForHash(hash: string): void {
|
|
8
|
+
const projectId = parseProjectIdFromHash(hash);
|
|
9
|
+
if (!projectId || projectId === prefetched) return;
|
|
10
|
+
prefetched = projectId;
|
|
11
|
+
const url = new URL(buildProjectApiPath(projectId, "/preview"), window.location.origin);
|
|
12
|
+
applyPreviewVariablesToUrl(url);
|
|
13
|
+
fetch(url).catch(() => undefined);
|
|
14
|
+
}
|
|
@@ -133,7 +133,6 @@ describe("runAtomicCutTransaction", () => {
|
|
|
133
133
|
expect(writeProjectFile).not.toHaveBeenCalled();
|
|
134
134
|
expect(recordEdit).toHaveBeenCalledWith({
|
|
135
135
|
label: "Split timeline clip",
|
|
136
|
-
kind: "timeline",
|
|
137
136
|
files: { "index.html": { before: "before", after: "after" } },
|
|
138
137
|
});
|
|
139
138
|
expect(observe).toHaveBeenCalledWith("index.html", '"v1"');
|
|
@@ -181,7 +181,7 @@ export function runAtomicCutTransaction(input: RunAtomicCutInput): Promise<Atomi
|
|
|
181
181
|
result.files.map((file) => [file.path, { before: file.before, after: file.after }]),
|
|
182
182
|
);
|
|
183
183
|
try {
|
|
184
|
-
await input.recordEdit({ label: input.label,
|
|
184
|
+
await input.recordEdit({ label: input.label, files: snapshots });
|
|
185
185
|
} catch (error) {
|
|
186
186
|
try {
|
|
187
187
|
await rollbackUnrecordedCut(result.files, input.writeProjectFile);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { openComposition, type Composition } from "@hyperframes/sdk";
|
|
2
|
-
import type { EditHistoryKind } from "./editHistory";
|
|
3
2
|
import { hashContent, markSelfWrite } from "../hooks/sdkSelfWriteRegistry";
|
|
4
3
|
import { trackStudioEvent } from "./studioTelemetry";
|
|
5
4
|
import { serializeStudioFileMutation } from "./studioFileMutationCoordinator";
|
|
@@ -32,7 +31,6 @@ export interface CutoverDeps {
|
|
|
32
31
|
editHistory: {
|
|
33
32
|
recordEdit: (entry: {
|
|
34
33
|
label: string;
|
|
35
|
-
kind: EditHistoryKind;
|
|
36
34
|
coalesceKey?: string;
|
|
37
35
|
coalesceMs?: number;
|
|
38
36
|
files: Record<string, { before: string; after: string }>;
|
|
@@ -248,7 +246,6 @@ async function writeAndRecord(
|
|
|
248
246
|
try {
|
|
249
247
|
await deps.editHistory.recordEdit({
|
|
250
248
|
label: options?.label ?? "Edit layer",
|
|
251
|
-
kind: "manual",
|
|
252
249
|
...(options?.coalesceKey ? { coalesceKey: options.coalesceKey } : {}),
|
|
253
250
|
...(options?.coalesceMs != null ? { coalesceMs: options.coalesceMs } : {}),
|
|
254
251
|
files: { [targetPath]: { before: originalContent, after } },
|