@hyperframes/studio 0.8.151 → 0.8.152
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-DJt8E7k4.js → hyperframes-player-0BY7-PK7.js} +1 -1
- package/dist/assets/{index-DWIHrAY2.js → index-COrWdzlj.js} +1 -1
- package/dist/assets/{index-BGks9KLI.js → index-p-VXwuJ2.js} +132 -132
- package/dist/assets/{index-DgbLblGR.js → index-rmD5k_ES.js} +1 -1
- package/dist/index.d.ts +24 -31
- package/dist/index.html +1 -1
- package/dist/index.js +46 -6
- package/package.json +8 -8
- package/src/hooks/useDomEditCommits.test.tsx +5 -0
- package/src/hooks/useDomEditCommits.ts +13 -0
- package/src/hooks/useDomEditPersist.test.tsx +8 -0
- package/src/hooks/useDomEditPersist.ts +13 -6
- package/src/hooks/usePersistentEditHistory.test.ts +20 -0
- package/src/hooks/usePersistentEditHistory.ts +2 -1
- package/src/utils/sdkCutover.test.ts +4 -1
- package/src/utils/sdkCutover.ts +12 -7
- package/src/utils/sdkEditTransaction.ts +13 -3
- package/src/utils/studioFileHistory.ts +4 -0
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export { Player } from './player.js';
|
|
|
12
12
|
import { MusicBeatAnalysis } from '@hyperframes/core/beats';
|
|
13
13
|
import * as _hyperframes_parsers from '@hyperframes/parsers';
|
|
14
14
|
import { CanvasResolution } from '@hyperframes/parsers';
|
|
15
|
+
import { HistoryTarget } from '@hyperframes/studio-server';
|
|
15
16
|
export { SeamTransitionFrameSource, SeamTransitionLoopHandle, SeamTransitionLoopOptions, playSeamTransitionLoop } from '@hyperframes/shader-transitions';
|
|
16
17
|
import '@base-ui/react/tabs';
|
|
17
18
|
import 'cn';
|
|
@@ -784,6 +785,19 @@ declare function buildPickerAgentPrompt({ selection, userInstruction, }: {
|
|
|
784
785
|
}): string;
|
|
785
786
|
declare function buildPickerAgentContextPreview(selection: HyperframePickerElementInfo | null): string;
|
|
786
787
|
|
|
788
|
+
type EditContextTarget = HistoryTarget;
|
|
789
|
+
interface RecordEditInput$4 {
|
|
790
|
+
targets?: EditContextTarget[];
|
|
791
|
+
label: string;
|
|
792
|
+
coalesceKey?: string;
|
|
793
|
+
coalesceMs?: number;
|
|
794
|
+
files: Record<string, {
|
|
795
|
+
before: string;
|
|
796
|
+
after: string;
|
|
797
|
+
}>;
|
|
798
|
+
created?: string[];
|
|
799
|
+
}
|
|
800
|
+
|
|
787
801
|
interface SdkSessionPublication {
|
|
788
802
|
candidate: Composition;
|
|
789
803
|
expectedSession: Composition;
|
|
@@ -828,17 +842,6 @@ interface TimelineRangeSelection {
|
|
|
828
842
|
anchorY: number;
|
|
829
843
|
}
|
|
830
844
|
|
|
831
|
-
interface RecordEditInput$5 {
|
|
832
|
-
label: string;
|
|
833
|
-
coalesceKey?: string;
|
|
834
|
-
coalesceMs?: number;
|
|
835
|
-
files: Record<string, {
|
|
836
|
-
before: string;
|
|
837
|
-
after: string;
|
|
838
|
-
}>;
|
|
839
|
-
created?: string[];
|
|
840
|
-
}
|
|
841
|
-
|
|
842
845
|
interface ImportedFontAsset {
|
|
843
846
|
family: string;
|
|
844
847
|
path: string;
|
|
@@ -872,7 +875,7 @@ type CommitDomAttributeBatch = (selection: DomEditSelection, operations: PatchOp
|
|
|
872
875
|
prepareContent?: (html: string) => string;
|
|
873
876
|
}) => Promise<boolean>;
|
|
874
877
|
|
|
875
|
-
interface RecordEditInput$
|
|
878
|
+
interface RecordEditInput$3 {
|
|
876
879
|
label: string;
|
|
877
880
|
coalesceKey?: string;
|
|
878
881
|
files: Record<string, {
|
|
@@ -896,7 +899,7 @@ interface UseTimelineEditingOptions {
|
|
|
896
899
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
897
900
|
writeProjectFile: (path: string, content: string, expectedContent?: string) => Promise<void>;
|
|
898
901
|
observeProjectFileVersion?: (path: string, version: string | null) => void;
|
|
899
|
-
recordEdit: (input: RecordEditInput$
|
|
902
|
+
recordEdit: (input: RecordEditInput$3) => Promise<void>;
|
|
900
903
|
reloadPreview: () => void;
|
|
901
904
|
previewIframeRef: RefObject<HTMLIFrameElement | null>;
|
|
902
905
|
pendingTimelineEditPathRef: MutableRefObject<Set<string>>;
|
|
@@ -2888,7 +2891,7 @@ declare function usePersistentEditHistory({ projectId }: UsePersistentEditHistor
|
|
|
2888
2891
|
createdAt: number;
|
|
2889
2892
|
}[];
|
|
2890
2893
|
};
|
|
2891
|
-
recordEdit: ({ label, coalesceKey, coalesceMs, files, created }: RecordEditInput$
|
|
2894
|
+
recordEdit: ({ label, coalesceKey, coalesceMs, files, targets, created }: RecordEditInput$4) => Promise<void>;
|
|
2892
2895
|
claimKey: (key: string) => string;
|
|
2893
2896
|
undo: (callbacks: ApplyCallbacks) => Promise<ApplyResult>;
|
|
2894
2897
|
redo: (callbacks: ApplyCallbacks) => Promise<ApplyResult>;
|
|
@@ -3128,7 +3131,7 @@ declare const useAudioMetersVisible: zustand.UseBoundStore<zustand.StoreApi<{
|
|
|
3128
3131
|
type InPlace = {
|
|
3129
3132
|
inPlace?: boolean;
|
|
3130
3133
|
};
|
|
3131
|
-
interface RecordEditInput$
|
|
3134
|
+
interface RecordEditInput$2 {
|
|
3132
3135
|
label: string;
|
|
3133
3136
|
coalesceKey?: string;
|
|
3134
3137
|
files: Record<string, {
|
|
@@ -3142,7 +3145,7 @@ interface UseClipboardOptions {
|
|
|
3142
3145
|
domEditSelectionRef: React.MutableRefObject<DomEditSelection | null>;
|
|
3143
3146
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
3144
3147
|
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
3145
|
-
recordEdit: (input: RecordEditInput$
|
|
3148
|
+
recordEdit: (input: RecordEditInput$2) => Promise<void>;
|
|
3146
3149
|
reloadPreview: () => void;
|
|
3147
3150
|
handleTimelineElementsDelete: (elements: TimelineElement[]) => Promise<void>;
|
|
3148
3151
|
handleDomEditElementDelete: (selection: DomEditSelection) => Promise<void>;
|
|
@@ -3391,7 +3394,7 @@ type DomEditCommitOutcome = {
|
|
|
3391
3394
|
|
|
3392
3395
|
/** One entry in the studio's edit history, as `useDomEditSession`'s caller
|
|
3393
3396
|
* supplies it. */
|
|
3394
|
-
interface RecordEditInput$
|
|
3397
|
+
interface RecordEditInput$1 {
|
|
3395
3398
|
label: string;
|
|
3396
3399
|
coalesceKey?: string;
|
|
3397
3400
|
files: Record<string, {
|
|
@@ -3419,7 +3422,7 @@ interface UseDomEditSessionParams extends DomEditTimelineParams {
|
|
|
3419
3422
|
writeProjectFile: (path: string, content: string, expectedContent?: string) => Promise<void>;
|
|
3420
3423
|
updateEditingFileContent: (path: string, content: string) => void;
|
|
3421
3424
|
editHistory: {
|
|
3422
|
-
recordEdit: (entry: RecordEditInput$
|
|
3425
|
+
recordEdit: (entry: RecordEditInput$1) => Promise<void>;
|
|
3423
3426
|
};
|
|
3424
3427
|
fileTree: string[];
|
|
3425
3428
|
importedFontAssetsRef: React.MutableRefObject<ImportedFontAsset[]>;
|
|
@@ -3613,7 +3616,7 @@ interface HistoryPreviewRestore {
|
|
|
3613
3616
|
paths?: string[];
|
|
3614
3617
|
files?: RestoreFiles;
|
|
3615
3618
|
}
|
|
3616
|
-
interface RecordEditInput
|
|
3619
|
+
interface RecordEditInput {
|
|
3617
3620
|
label: string;
|
|
3618
3621
|
coalesceKey?: string;
|
|
3619
3622
|
files: Record<string, {
|
|
@@ -3625,7 +3628,7 @@ interface UsePreviewPersistenceParams {
|
|
|
3625
3628
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
3626
3629
|
readOptionalProjectFile: (path: string) => Promise<string>;
|
|
3627
3630
|
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
3628
|
-
recordEdit: (entry: RecordEditInput
|
|
3631
|
+
recordEdit: (entry: RecordEditInput) => Promise<void>;
|
|
3629
3632
|
previewIframeRef: React.MutableRefObject<HTMLIFrameElement | null>;
|
|
3630
3633
|
activeCompPathRef: React.MutableRefObject<string | null>;
|
|
3631
3634
|
/** Called to reload the preview after undo/redo or external file changes. */
|
|
@@ -3863,23 +3866,13 @@ interface DomEditZOrder {
|
|
|
3863
3866
|
}
|
|
3864
3867
|
declare function useDomEditZOrder(): DomEditZOrder;
|
|
3865
3868
|
|
|
3866
|
-
interface RecordEditInput {
|
|
3867
|
-
label: string;
|
|
3868
|
-
coalesceKey?: string;
|
|
3869
|
-
coalesceMs?: number;
|
|
3870
|
-
files: Record<string, {
|
|
3871
|
-
before: string;
|
|
3872
|
-
after: string;
|
|
3873
|
-
}>;
|
|
3874
|
-
}
|
|
3875
|
-
|
|
3876
3869
|
interface UseDomStyleCommitOptions {
|
|
3877
3870
|
projectId: string | null;
|
|
3878
3871
|
/** The preview the host already runs: the style applies there live, and it reseeks after a save. */
|
|
3879
3872
|
iframeRef: React.MutableRefObject<HTMLIFrameElement | null>;
|
|
3880
3873
|
/** The same function objects the host gives the timeline, so there is one writer and one undo list. */
|
|
3881
3874
|
writeProjectFile: (path: string, content: string, expectedContent?: string) => Promise<void>;
|
|
3882
|
-
recordEdit: (entry: RecordEditInput) => Promise<void>;
|
|
3875
|
+
recordEdit: (entry: RecordEditInput$4) => Promise<void>;
|
|
3883
3876
|
activeCompPath?: string | null;
|
|
3884
3877
|
showToast?: (message: string, tone?: "error" | "info") => void;
|
|
3885
3878
|
}
|
package/dist/index.html
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
document.documentElement.dataset.theme = "paper";
|
|
15
15
|
}
|
|
16
16
|
</script>
|
|
17
|
-
<script type="module" crossorigin src="/assets/index-
|
|
17
|
+
<script type="module" crossorigin src="/assets/index-p-VXwuJ2.js"></script>
|
|
18
18
|
<link rel="stylesheet" crossorigin href="/assets/index-DkQKsFxx.css">
|
|
19
19
|
</head>
|
|
20
20
|
<body>
|
package/dist/index.js
CHANGED
|
@@ -44187,6 +44187,14 @@ async function rollbackWrite(targetPath, originalContent, expectedCurrentContent
|
|
|
44187
44187
|
);
|
|
44188
44188
|
}
|
|
44189
44189
|
}
|
|
44190
|
+
function historyEntryOptions(options) {
|
|
44191
|
+
return {
|
|
44192
|
+
label: options?.label ?? "Edit layer",
|
|
44193
|
+
...options?.targets && { targets: options.targets },
|
|
44194
|
+
...options?.coalesceKey ? { coalesceKey: options.coalesceKey } : {},
|
|
44195
|
+
...options?.coalesceMs != null ? { coalesceMs: options.coalesceMs } : {}
|
|
44196
|
+
};
|
|
44197
|
+
}
|
|
44190
44198
|
async function writeAndRecord(after, targetPath, originalContent, deps, options) {
|
|
44191
44199
|
markSelfWrite(targetPath, after);
|
|
44192
44200
|
try {
|
|
@@ -44196,9 +44204,7 @@ async function writeAndRecord(after, targetPath, originalContent, deps, options)
|
|
|
44196
44204
|
}
|
|
44197
44205
|
try {
|
|
44198
44206
|
await deps.editHistory.recordEdit({
|
|
44199
|
-
|
|
44200
|
-
...options?.coalesceKey ? { coalesceKey: options.coalesceKey } : {},
|
|
44201
|
-
...options?.coalesceMs != null ? { coalesceMs: options.coalesceMs } : {},
|
|
44207
|
+
...historyEntryOptions(options),
|
|
44202
44208
|
files: { [targetPath]: { before: originalContent, after } }
|
|
44203
44209
|
});
|
|
44204
44210
|
return null;
|
|
@@ -44318,7 +44324,18 @@ async function sdkCutoverPersist(selection, ops, originalContent, targetPath, sd
|
|
|
44318
44324
|
(session) => {
|
|
44319
44325
|
for (const editOp of patchOpsToSdkEditOps(hfId, ops)) session.dispatch(editOp);
|
|
44320
44326
|
},
|
|
44321
|
-
|
|
44327
|
+
{
|
|
44328
|
+
...options,
|
|
44329
|
+
targets: [
|
|
44330
|
+
{
|
|
44331
|
+
file: targetPath,
|
|
44332
|
+
hfId,
|
|
44333
|
+
...selection.id && { id: selection.id },
|
|
44334
|
+
...selection.selector && { selector: selection.selector },
|
|
44335
|
+
...selection.selectorIndex !== void 0 && { selectorIndex: selection.selectorIndex }
|
|
44336
|
+
}
|
|
44337
|
+
]
|
|
44338
|
+
}
|
|
44322
44339
|
);
|
|
44323
44340
|
trackCutoverResult(result, "dom", { hfId, opCount: ops.length });
|
|
44324
44341
|
return result;
|
|
@@ -74892,13 +74909,14 @@ function usePersistentEditHistory({ projectId }) {
|
|
|
74892
74909
|
return () => setStudioPendingEditClaimClock(null);
|
|
74893
74910
|
}, [own]);
|
|
74894
74911
|
const recordEdit = useCallback99(
|
|
74895
|
-
async ({ label, coalesceKey, coalesceMs, files, created = [] }) => {
|
|
74912
|
+
async ({ label, coalesceKey, coalesceMs, files, targets, created = [] }) => {
|
|
74896
74913
|
if (!projectId) return;
|
|
74897
74914
|
const paths = [...Object.keys(files), ...created];
|
|
74898
74915
|
own.overtake();
|
|
74899
74916
|
const reply = await post(historyUrl(projectId, "/claim"), {
|
|
74900
74917
|
label,
|
|
74901
74918
|
paths,
|
|
74919
|
+
...targets?.length && { targets },
|
|
74902
74920
|
overwrote: await overwroteVersions(files),
|
|
74903
74921
|
...coalesceKey && {
|
|
74904
74922
|
coalesceKey: claimKey(coalesceKey),
|
|
@@ -80574,6 +80592,17 @@ function useDomEditPersist({
|
|
|
80574
80592
|
const finalContent2 = prepared.content;
|
|
80575
80593
|
await editHistory.recordEdit({
|
|
80576
80594
|
...history2,
|
|
80595
|
+
targets: [
|
|
80596
|
+
{
|
|
80597
|
+
file: targetPath,
|
|
80598
|
+
...selection.id && { id: selection.id },
|
|
80599
|
+
...selection.hfId && { hfId: selection.hfId },
|
|
80600
|
+
...selection.selector && { selector: selection.selector },
|
|
80601
|
+
...selection.selectorIndex !== void 0 && {
|
|
80602
|
+
selectorIndex: selection.selectorIndex
|
|
80603
|
+
}
|
|
80604
|
+
}
|
|
80605
|
+
],
|
|
80577
80606
|
files: { [targetPath]: { before: originalContent, after: finalContent2 } }
|
|
80578
80607
|
});
|
|
80579
80608
|
return { patchData: patchData2, patchedContent: patchedContent2, finalContent: finalContent2 };
|
|
@@ -81836,7 +81865,18 @@ function useDomEditCommits({
|
|
|
81836
81865
|
label: options.label,
|
|
81837
81866
|
coalesceKey: options.coalesceKey,
|
|
81838
81867
|
coalesceMs: options.coalesceMs,
|
|
81839
|
-
files
|
|
81868
|
+
files,
|
|
81869
|
+
targets: batches.filter((batch) => Object.hasOwn(files, batch.sourceFile)).flatMap(
|
|
81870
|
+
(batch) => batch.patches.map(({ target }) => ({
|
|
81871
|
+
file: batch.sourceFile,
|
|
81872
|
+
...target.id && { id: target.id },
|
|
81873
|
+
...target.hfId && { hfId: target.hfId },
|
|
81874
|
+
...target.selector && { selector: target.selector },
|
|
81875
|
+
...target.selectorIndex !== void 0 && {
|
|
81876
|
+
selectorIndex: target.selectorIndex
|
|
81877
|
+
}
|
|
81878
|
+
}))
|
|
81879
|
+
)
|
|
81840
81880
|
});
|
|
81841
81881
|
}
|
|
81842
81882
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.152",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -60,12 +60,12 @@
|
|
|
60
60
|
"dockview-react": "8.3.1",
|
|
61
61
|
"gsap": "^3.13.0",
|
|
62
62
|
"mediabunny": "^1.45.3",
|
|
63
|
-
"@hyperframes/
|
|
64
|
-
"@hyperframes/
|
|
65
|
-
"@hyperframes/
|
|
66
|
-
"@hyperframes/
|
|
67
|
-
"@hyperframes/
|
|
68
|
-
"@hyperframes/
|
|
63
|
+
"@hyperframes/core": "0.8.152",
|
|
64
|
+
"@hyperframes/parsers": "0.8.152",
|
|
65
|
+
"@hyperframes/player": "0.8.152",
|
|
66
|
+
"@hyperframes/sdk": "0.8.152",
|
|
67
|
+
"@hyperframes/shader-transitions": "0.8.152",
|
|
68
|
+
"@hyperframes/studio-server": "0.8.152"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@tailwindcss/vite": "^4.3.3",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"vite": "^6.4.2",
|
|
83
83
|
"vitest": "^4.1.11",
|
|
84
84
|
"zustand": "^5.0.0",
|
|
85
|
-
"@hyperframes/producer": "0.8.
|
|
85
|
+
"@hyperframes/producer": "0.8.152"
|
|
86
86
|
},
|
|
87
87
|
"peerDependencies": {
|
|
88
88
|
"react": "19",
|
|
@@ -350,6 +350,11 @@ describe("useDomEditCommits z-index reorder persistence", () => {
|
|
|
350
350
|
// round-trip that separates them.
|
|
351
351
|
coalesceMs: Number.POSITIVE_INFINITY,
|
|
352
352
|
files: { "index.html": { before: original, after } },
|
|
353
|
+
targets: [
|
|
354
|
+
{ file: "index.html", id: "a", hfId: "hf-card" },
|
|
355
|
+
{ file: "index.html", id: "b" },
|
|
356
|
+
{ file: "index.html", id: "c" },
|
|
357
|
+
],
|
|
353
358
|
});
|
|
354
359
|
// FIX: a z-only reorder must NOT remount the preview iframe ("the blink").
|
|
355
360
|
// The live DOM + store already hold the final state and the server matched
|
|
@@ -178,6 +178,19 @@ export function useDomEditCommits({
|
|
|
178
178
|
coalesceKey: options.coalesceKey,
|
|
179
179
|
coalesceMs: options.coalesceMs,
|
|
180
180
|
files,
|
|
181
|
+
targets: batches
|
|
182
|
+
.filter((batch) => Object.hasOwn(files, batch.sourceFile))
|
|
183
|
+
.flatMap((batch) =>
|
|
184
|
+
batch.patches.map(({ target }) => ({
|
|
185
|
+
file: batch.sourceFile,
|
|
186
|
+
...(target.id && { id: target.id }),
|
|
187
|
+
...(target.hfId && { hfId: target.hfId }),
|
|
188
|
+
...(target.selector && { selector: target.selector }),
|
|
189
|
+
...(target.selectorIndex !== undefined && {
|
|
190
|
+
selectorIndex: target.selectorIndex,
|
|
191
|
+
}),
|
|
192
|
+
})),
|
|
193
|
+
),
|
|
181
194
|
});
|
|
182
195
|
}
|
|
183
196
|
return {
|
|
@@ -116,6 +116,14 @@ describe("useDomEditPersist", () => {
|
|
|
116
116
|
coalesceKey: undefined,
|
|
117
117
|
coalesceMs: undefined,
|
|
118
118
|
files: { "index.html": { before: SOURCE, after: PATCHED } },
|
|
119
|
+
targets: [
|
|
120
|
+
{
|
|
121
|
+
file: "index.html",
|
|
122
|
+
hfId: "hf-card",
|
|
123
|
+
selector: '[data-hf-id="hf-card"]',
|
|
124
|
+
selectorIndex: 0,
|
|
125
|
+
},
|
|
126
|
+
],
|
|
119
127
|
});
|
|
120
128
|
expect(params.forceReloadSdkSession).toHaveBeenCalledTimes(1);
|
|
121
129
|
expect(params.reloadPreview).toHaveBeenCalledTimes(1);
|
|
@@ -22,12 +22,8 @@ import { reseekPreviewRuntime } from "./timelineTrackVisibility";
|
|
|
22
22
|
import { serializeStudioFileMutations } from "../utils/studioFileMutationCoordinator";
|
|
23
23
|
import { readProjectFileContent } from "../utils/studioFileHistory";
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
coalesceKey?: string;
|
|
28
|
-
coalesceMs?: number;
|
|
29
|
-
files: Record<string, { before: string; after: string }>;
|
|
30
|
-
}
|
|
25
|
+
import type { RecordEditInput } from "../utils/studioFileHistory";
|
|
26
|
+
export type { RecordEditInput } from "../utils/studioFileHistory";
|
|
31
27
|
|
|
32
28
|
export interface UseDomEditPersistParams {
|
|
33
29
|
activeCompPath: string | null;
|
|
@@ -164,6 +160,17 @@ export function useDomEditPersist({
|
|
|
164
160
|
|
|
165
161
|
await editHistory.recordEdit({
|
|
166
162
|
...history,
|
|
163
|
+
targets: [
|
|
164
|
+
{
|
|
165
|
+
file: targetPath,
|
|
166
|
+
...(selection.id && { id: selection.id }),
|
|
167
|
+
...(selection.hfId && { hfId: selection.hfId }),
|
|
168
|
+
...(selection.selector && { selector: selection.selector }),
|
|
169
|
+
...(selection.selectorIndex !== undefined && {
|
|
170
|
+
selectorIndex: selection.selectorIndex,
|
|
171
|
+
}),
|
|
172
|
+
},
|
|
173
|
+
],
|
|
167
174
|
files: { [targetPath]: { before: originalContent, after: finalContent } },
|
|
168
175
|
});
|
|
169
176
|
return { patchData, patchedContent, finalContent };
|
|
@@ -700,3 +700,23 @@ it("an undo taken before the view caught up with the edit still reports the prev
|
|
|
700
700
|
files: { "index.html": { previous: "B", restored: "A" } },
|
|
701
701
|
});
|
|
702
702
|
});
|
|
703
|
+
|
|
704
|
+
it("stores DOM targets in existing history and retains them through undo and redo", async () => {
|
|
705
|
+
const { hook, save, readFile, history } = await studio();
|
|
706
|
+
save("B");
|
|
707
|
+
const targets = [{ file: "index.html", id: "title", selector: ".title", selectorIndex: 1 }];
|
|
708
|
+
await act(() =>
|
|
709
|
+
hook().recordEdit({
|
|
710
|
+
label: "Title",
|
|
711
|
+
targets,
|
|
712
|
+
files: { "index.html": { before: "A", after: "B" } },
|
|
713
|
+
}),
|
|
714
|
+
);
|
|
715
|
+
expect(history.list().at(-1)?.targets).toEqual(targets);
|
|
716
|
+
await vi.waitFor(() => expect(hook().canUndo).toBe(true));
|
|
717
|
+
await act(() => hook().undo({ readFile }));
|
|
718
|
+
expect(history.list().at(-1)?.targets).toEqual(targets);
|
|
719
|
+
await vi.waitFor(() => expect(hook().canRedo).toBe(true));
|
|
720
|
+
await act(() => hook().redo({ readFile }));
|
|
721
|
+
expect(history.list().at(-1)?.targets).toEqual(targets);
|
|
722
|
+
});
|
|
@@ -240,13 +240,14 @@ export function usePersistentEditHistory({ projectId }: UsePersistentEditHistory
|
|
|
240
240
|
}, [own]);
|
|
241
241
|
|
|
242
242
|
const recordEdit = useCallback(
|
|
243
|
-
async ({ label, coalesceKey, coalesceMs, files, created = [] }: RecordEditInput) => {
|
|
243
|
+
async ({ label, coalesceKey, coalesceMs, files, targets, created = [] }: RecordEditInput) => {
|
|
244
244
|
if (!projectId) return;
|
|
245
245
|
const paths = [...Object.keys(files), ...created];
|
|
246
246
|
own.overtake();
|
|
247
247
|
const reply = await post(historyUrl(projectId, "/claim"), {
|
|
248
248
|
label,
|
|
249
249
|
paths,
|
|
250
|
+
...(targets?.length && { targets }),
|
|
250
251
|
overwrote: await overwroteVersions(files),
|
|
251
252
|
...(coalesceKey && {
|
|
252
253
|
coalesceKey: claimKey(coalesceKey),
|
|
@@ -358,7 +358,10 @@ describe("sdkCutoverPersist", () => {
|
|
|
358
358
|
label: "Resize layer box",
|
|
359
359
|
});
|
|
360
360
|
expect(deps.editHistory.recordEdit).toHaveBeenCalledWith(
|
|
361
|
-
expect.objectContaining({
|
|
361
|
+
expect.objectContaining({
|
|
362
|
+
label: "Resize layer box",
|
|
363
|
+
targets: [{ file: "/comp.html", hfId: "hf-abc" }],
|
|
364
|
+
}),
|
|
362
365
|
);
|
|
363
366
|
});
|
|
364
367
|
|
package/src/utils/sdkCutover.ts
CHANGED
|
@@ -32,7 +32,6 @@ export type {
|
|
|
32
32
|
CutoverResult,
|
|
33
33
|
PublishSdkSession,
|
|
34
34
|
} from "./sdkEditTransaction";
|
|
35
|
-
|
|
36
35
|
function sdkFamilyEnabled(family: StudioSdkOperationFamily): boolean {
|
|
37
36
|
const configured = Object.prototype.hasOwnProperty.call(
|
|
38
37
|
studioAvailability,
|
|
@@ -54,7 +53,6 @@ function trackCutoverResult(
|
|
|
54
53
|
failedCutover(result.error, family, context);
|
|
55
54
|
}
|
|
56
55
|
}
|
|
57
|
-
|
|
58
56
|
/** True when targetPath isn't the composition the SDK session models. */
|
|
59
57
|
function wrongCompositionFile(deps: CutoverDeps, targetPath: string): boolean {
|
|
60
58
|
return deps.compositionPath != null && targetPath !== deps.compositionPath;
|
|
@@ -105,7 +103,18 @@ export async function sdkCutoverPersist(
|
|
|
105
103
|
(session) => {
|
|
106
104
|
for (const editOp of patchOpsToSdkEditOps(hfId, ops)) session.dispatch(editOp);
|
|
107
105
|
},
|
|
108
|
-
|
|
106
|
+
{
|
|
107
|
+
...options,
|
|
108
|
+
targets: [
|
|
109
|
+
{
|
|
110
|
+
file: targetPath,
|
|
111
|
+
hfId,
|
|
112
|
+
...(selection.id && { id: selection.id }),
|
|
113
|
+
...(selection.selector && { selector: selection.selector }),
|
|
114
|
+
...(selection.selectorIndex !== undefined && { selectorIndex: selection.selectorIndex }),
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
},
|
|
109
118
|
);
|
|
110
119
|
trackCutoverResult(result, "dom", { hfId, opCount: ops.length });
|
|
111
120
|
return result;
|
|
@@ -155,7 +164,6 @@ export async function sdkTimingPersist(
|
|
|
155
164
|
return failedCutover(error, "timing", { hfId, opCount: 1 });
|
|
156
165
|
}
|
|
157
166
|
}
|
|
158
|
-
|
|
159
167
|
export async function sdkTimingBatchPersist(
|
|
160
168
|
changes: Array<{
|
|
161
169
|
hfId: string;
|
|
@@ -224,7 +232,6 @@ type SdkGsapTweenOp =
|
|
|
224
232
|
| { kind: "add"; target: string; spec: GsapTweenSpec }
|
|
225
233
|
| { kind: "set"; animationId: string; properties: Partial<GsapTweenSpec> }
|
|
226
234
|
| { kind: "remove"; animationId: string };
|
|
227
|
-
|
|
228
235
|
export function sdkGsapTweenPersist(
|
|
229
236
|
targetPath: string,
|
|
230
237
|
op: SdkGsapTweenOp,
|
|
@@ -276,7 +283,6 @@ export function sdkGsapTweenPersist(
|
|
|
276
283
|
});
|
|
277
284
|
});
|
|
278
285
|
}
|
|
279
|
-
|
|
280
286
|
async function dispatchGsapOpAndPersist(
|
|
281
287
|
family: "gsap-animation" | "gsap-keyframe",
|
|
282
288
|
targetPath: string,
|
|
@@ -327,7 +333,6 @@ async function dispatchGsapOpAndPersist(
|
|
|
327
333
|
return failedCutover(error, family, { opCount: 1 });
|
|
328
334
|
}
|
|
329
335
|
}
|
|
330
|
-
|
|
331
336
|
export function sdkGsapKeyframePersist(
|
|
332
337
|
targetPath: string,
|
|
333
338
|
animationId: string,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { EditContextTarget } from "./studioFileHistory";
|
|
1
2
|
import { openComposition, type Composition } from "@hyperframes/sdk";
|
|
2
3
|
import { hashContent, markSelfWrite } from "../hooks/sdkSelfWriteRegistry";
|
|
3
4
|
import { trackStudioEvent } from "./studioTelemetry";
|
|
@@ -31,6 +32,7 @@ export interface CutoverDeps {
|
|
|
31
32
|
editHistory: {
|
|
32
33
|
recordEdit: (entry: {
|
|
33
34
|
label: string;
|
|
35
|
+
targets?: EditContextTarget[];
|
|
34
36
|
coalesceKey?: string;
|
|
35
37
|
coalesceMs?: number;
|
|
36
38
|
files: Record<string, { before: string; after: string }>;
|
|
@@ -56,6 +58,7 @@ export interface CutoverDeps {
|
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
export interface CutoverOptions {
|
|
61
|
+
targets?: EditContextTarget[];
|
|
59
62
|
label?: string;
|
|
60
63
|
coalesceKey?: string;
|
|
61
64
|
/** Coalesce window (ms); Infinity folds across a slow round-trip. */
|
|
@@ -232,6 +235,15 @@ async function rollbackWrite(
|
|
|
232
235
|
}
|
|
233
236
|
}
|
|
234
237
|
|
|
238
|
+
function historyEntryOptions(options?: CutoverOptions) {
|
|
239
|
+
return {
|
|
240
|
+
label: options?.label ?? "Edit layer",
|
|
241
|
+
...(options?.targets && { targets: options.targets }),
|
|
242
|
+
...(options?.coalesceKey ? { coalesceKey: options.coalesceKey } : {}),
|
|
243
|
+
...(options?.coalesceMs != null ? { coalesceMs: options.coalesceMs } : {}),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
235
247
|
async function writeAndRecord(
|
|
236
248
|
after: string,
|
|
237
249
|
targetPath: string,
|
|
@@ -247,9 +259,7 @@ async function writeAndRecord(
|
|
|
247
259
|
}
|
|
248
260
|
try {
|
|
249
261
|
await deps.editHistory.recordEdit({
|
|
250
|
-
|
|
251
|
-
...(options?.coalesceKey ? { coalesceKey: options.coalesceKey } : {}),
|
|
252
|
-
...(options?.coalesceMs != null ? { coalesceMs: options.coalesceMs } : {}),
|
|
262
|
+
...historyEntryOptions(options),
|
|
253
263
|
files: { [targetPath]: { before: originalContent, after } },
|
|
254
264
|
});
|
|
255
265
|
return null;
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import type { HistoryTarget } from "@hyperframes/studio-server";
|
|
1
2
|
import { buildProjectApiPath } from "./projectRouting";
|
|
2
3
|
import type { MutableRefObject } from "react";
|
|
3
4
|
import { serializeStudioFileMutations } from "./studioFileMutationCoordinator";
|
|
4
5
|
import { createStudioSaveHttpError } from "./studioSaveDiagnostics";
|
|
5
6
|
import { studioApiFetch } from "./studioApiFetch";
|
|
6
7
|
|
|
8
|
+
export type EditContextTarget = HistoryTarget;
|
|
9
|
+
|
|
7
10
|
export interface RecordEditInput {
|
|
11
|
+
targets?: EditContextTarget[];
|
|
8
12
|
label: string;
|
|
9
13
|
coalesceKey?: string;
|
|
10
14
|
coalesceMs?: number;
|