@hyperframes/studio 0.8.150 → 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-kup62bDR.js → hyperframes-player-0BY7-PK7.js} +1 -1
- package/dist/assets/{index-B8BBYY8L.js → index-COrWdzlj.js} +1 -1
- package/dist/assets/{index-CS_m0TkX.js → index-p-VXwuJ2.js} +132 -132
- package/dist/assets/{index-DQ5jGodF.js → index-rmD5k_ES.js} +1 -1
- package/dist/index.d.ts +24 -31
- package/dist/index.html +1 -1
- package/dist/index.js +54 -24
- package/package.json +8 -8
- package/src/hooks/gsapShared.ts +1 -11
- 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
|
@@ -5026,17 +5026,7 @@ function matchesSelector(tweenSelector, querySelector) {
|
|
|
5026
5026
|
}
|
|
5027
5027
|
|
|
5028
5028
|
// src/hooks/gsapShared.ts
|
|
5029
|
-
|
|
5030
|
-
opacity: 1,
|
|
5031
|
-
x: 0,
|
|
5032
|
-
y: 0,
|
|
5033
|
-
scale: 1,
|
|
5034
|
-
scaleX: 1,
|
|
5035
|
-
scaleY: 1,
|
|
5036
|
-
rotation: 0,
|
|
5037
|
-
width: 100,
|
|
5038
|
-
height: 100
|
|
5039
|
-
};
|
|
5029
|
+
import { KEYFRAME_PROPERTY_DEFAULTS } from "@hyperframes/core/keyframe-defaults";
|
|
5040
5030
|
function keyframeEases(anim) {
|
|
5041
5031
|
if (!anim.keyframes) return { easeEach: anim.ease };
|
|
5042
5032
|
return { ease: runEaseOf(anim), easeEach: anim.keyframes.easeEach };
|
|
@@ -5327,14 +5317,14 @@ function synthesizeFlatTweenKeyframes(anim) {
|
|
|
5327
5317
|
if (anim.method === "from") {
|
|
5328
5318
|
for (const [k, v] of Object.entries(toProps)) {
|
|
5329
5319
|
rawStart[k] = v;
|
|
5330
|
-
rawEnd[k] =
|
|
5320
|
+
rawEnd[k] = KEYFRAME_PROPERTY_DEFAULTS[k] ?? 0;
|
|
5331
5321
|
}
|
|
5332
5322
|
} else if (anim.method === "fromTo" && fromProps) {
|
|
5333
5323
|
Object.assign(rawStart, fromProps);
|
|
5334
5324
|
Object.assign(rawEnd, toProps);
|
|
5335
5325
|
} else {
|
|
5336
5326
|
for (const [k, v] of Object.entries(toProps)) {
|
|
5337
|
-
rawStart[k] =
|
|
5327
|
+
rawStart[k] = KEYFRAME_PROPERTY_DEFAULTS[k] ?? 0;
|
|
5338
5328
|
rawEnd[k] = v;
|
|
5339
5329
|
}
|
|
5340
5330
|
}
|
|
@@ -23030,7 +23020,7 @@ function propertyStops(animation2, property, start) {
|
|
|
23030
23020
|
}));
|
|
23031
23021
|
const first = stops[0];
|
|
23032
23022
|
if (!first || first.percentage <= 0 || typeof first.value !== "number") return stops;
|
|
23033
|
-
return [{ percentage: 0, value: start ??
|
|
23023
|
+
return [{ percentage: 0, value: start ?? KEYFRAME_PROPERTY_DEFAULTS[property] ?? 0 }, ...stops];
|
|
23034
23024
|
}
|
|
23035
23025
|
function interpolableEnds(before, after) {
|
|
23036
23026
|
if (typeof before.value !== "number" || typeof after.value !== "number") return null;
|
|
@@ -44197,6 +44187,14 @@ async function rollbackWrite(targetPath, originalContent, expectedCurrentContent
|
|
|
44197
44187
|
);
|
|
44198
44188
|
}
|
|
44199
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
|
+
}
|
|
44200
44198
|
async function writeAndRecord(after, targetPath, originalContent, deps, options) {
|
|
44201
44199
|
markSelfWrite(targetPath, after);
|
|
44202
44200
|
try {
|
|
@@ -44206,9 +44204,7 @@ async function writeAndRecord(after, targetPath, originalContent, deps, options)
|
|
|
44206
44204
|
}
|
|
44207
44205
|
try {
|
|
44208
44206
|
await deps.editHistory.recordEdit({
|
|
44209
|
-
|
|
44210
|
-
...options?.coalesceKey ? { coalesceKey: options.coalesceKey } : {},
|
|
44211
|
-
...options?.coalesceMs != null ? { coalesceMs: options.coalesceMs } : {},
|
|
44207
|
+
...historyEntryOptions(options),
|
|
44212
44208
|
files: { [targetPath]: { before: originalContent, after } }
|
|
44213
44209
|
});
|
|
44214
44210
|
return null;
|
|
@@ -44328,7 +44324,18 @@ async function sdkCutoverPersist(selection, ops, originalContent, targetPath, sd
|
|
|
44328
44324
|
(session) => {
|
|
44329
44325
|
for (const editOp of patchOpsToSdkEditOps(hfId, ops)) session.dispatch(editOp);
|
|
44330
44326
|
},
|
|
44331
|
-
|
|
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
|
+
}
|
|
44332
44339
|
);
|
|
44333
44340
|
trackCutoverResult(result, "dom", { hfId, opCount: ops.length });
|
|
44334
44341
|
return result;
|
|
@@ -47644,7 +47651,7 @@ async function commitWholePropertyOffset(selection, anim, newValues, currentPct,
|
|
|
47644
47651
|
const ts = resolveTweenStart(effectiveAnim);
|
|
47645
47652
|
const td = resolveTweenDuration(effectiveAnim);
|
|
47646
47653
|
const keys = Object.keys(newValues);
|
|
47647
|
-
const at = (props, key2) => typeof props[key2] === "number" ? props[key2] :
|
|
47654
|
+
const at = (props, key2) => typeof props[key2] === "number" ? props[key2] : KEYFRAME_PROPERTY_DEFAULTS[key2] ?? 0;
|
|
47648
47655
|
const kfs = effectiveAnim.keyframes?.keyframes ?? synthesizeFlatTweenKeyframes(effectiveAnim)?.keyframes;
|
|
47649
47656
|
if (!kfs || kfs.length === 0) {
|
|
47650
47657
|
await callbacks.commitMutation(
|
|
@@ -74902,13 +74909,14 @@ function usePersistentEditHistory({ projectId }) {
|
|
|
74902
74909
|
return () => setStudioPendingEditClaimClock(null);
|
|
74903
74910
|
}, [own]);
|
|
74904
74911
|
const recordEdit = useCallback99(
|
|
74905
|
-
async ({ label, coalesceKey, coalesceMs, files, created = [] }) => {
|
|
74912
|
+
async ({ label, coalesceKey, coalesceMs, files, targets, created = [] }) => {
|
|
74906
74913
|
if (!projectId) return;
|
|
74907
74914
|
const paths = [...Object.keys(files), ...created];
|
|
74908
74915
|
own.overtake();
|
|
74909
74916
|
const reply = await post(historyUrl(projectId, "/claim"), {
|
|
74910
74917
|
label,
|
|
74911
74918
|
paths,
|
|
74919
|
+
...targets?.length && { targets },
|
|
74912
74920
|
overwrote: await overwroteVersions(files),
|
|
74913
74921
|
...coalesceKey && {
|
|
74914
74922
|
coalesceKey: claimKey(coalesceKey),
|
|
@@ -80584,6 +80592,17 @@ function useDomEditPersist({
|
|
|
80584
80592
|
const finalContent2 = prepared.content;
|
|
80585
80593
|
await editHistory.recordEdit({
|
|
80586
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
|
+
],
|
|
80587
80606
|
files: { [targetPath]: { before: originalContent, after: finalContent2 } }
|
|
80588
80607
|
});
|
|
80589
80608
|
return { patchData: patchData2, patchedContent: patchedContent2, finalContent: finalContent2 };
|
|
@@ -81846,7 +81865,18 @@ function useDomEditCommits({
|
|
|
81846
81865
|
label: options.label,
|
|
81847
81866
|
coalesceKey: options.coalesceKey,
|
|
81848
81867
|
coalesceMs: options.coalesceMs,
|
|
81849
|
-
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
|
+
)
|
|
81850
81880
|
});
|
|
81851
81881
|
}
|
|
81852
81882
|
return {
|
|
@@ -82597,7 +82627,7 @@ async function commitKeyframeAtTimeImpl(selection, absoluteTime, animations, pro
|
|
|
82597
82627
|
}
|
|
82598
82628
|
const backfillDefaults = {};
|
|
82599
82629
|
for (const key2 of Object.keys(properties)) {
|
|
82600
|
-
backfillDefaults[key2] =
|
|
82630
|
+
backfillDefaults[key2] = KEYFRAME_PROPERTY_DEFAULTS[key2] ?? 0;
|
|
82601
82631
|
}
|
|
82602
82632
|
await commitMutation(
|
|
82603
82633
|
selection,
|
|
@@ -83018,7 +83048,7 @@ function useGsapPropertyDebounce(commitMutationSafely, sdk) {
|
|
|
83018
83048
|
const addGsapProperty = useCallback137(
|
|
83019
83049
|
// fallow-ignore-next-line complexity
|
|
83020
83050
|
async (selection, animationId, property) => {
|
|
83021
|
-
let defaultValue =
|
|
83051
|
+
let defaultValue = KEYFRAME_PROPERTY_DEFAULTS[property] ?? 0;
|
|
83022
83052
|
const el = selection.element;
|
|
83023
83053
|
if (property === "width" || property === "height") {
|
|
83024
83054
|
const rect = el.getBoundingClientRect();
|
|
@@ -83140,7 +83170,7 @@ function useGsapPropertyDebounce(commitMutationSafely, sdk) {
|
|
|
83140
83170
|
);
|
|
83141
83171
|
const addGsapFromProperty = useCallback137(
|
|
83142
83172
|
async (selection, animationId, property) => {
|
|
83143
|
-
const defaultValue =
|
|
83173
|
+
const defaultValue = KEYFRAME_PROPERTY_DEFAULTS[property] ?? 0;
|
|
83144
83174
|
const { sdkSession, sdkDeps, activeCompPath } = sdkRef.current ?? {};
|
|
83145
83175
|
if (sdkSession && sdkDeps) {
|
|
83146
83176
|
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
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",
|
package/src/hooks/gsapShared.ts
CHANGED
|
@@ -25,17 +25,7 @@ export interface IframeGsap {
|
|
|
25
25
|
|
|
26
26
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
27
27
|
|
|
28
|
-
export
|
|
29
|
-
opacity: 1,
|
|
30
|
-
x: 0,
|
|
31
|
-
y: 0,
|
|
32
|
-
scale: 1,
|
|
33
|
-
scaleX: 1,
|
|
34
|
-
scaleY: 1,
|
|
35
|
-
rotation: 0,
|
|
36
|
-
width: 100,
|
|
37
|
-
height: 100,
|
|
38
|
-
};
|
|
28
|
+
export { KEYFRAME_PROPERTY_DEFAULTS as PROPERTY_DEFAULTS } from "@hyperframes/core/keyframe-defaults";
|
|
39
29
|
|
|
40
30
|
/** A tween's eases for re-writing it as keyframes: a flat tween's ease must become easeEach,
|
|
41
31
|
* since GSAP eases each percentage segment power1.inOut whatever the tween-level ease says. */
|
|
@@ -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;
|