@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.
@@ -1,4 +1,4 @@
1
- import{n as Qi}from"./index-BGks9KLI.js";/*!
1
+ import{n as Qi}from"./index-p-VXwuJ2.js";/*!
2
2
  * Copyright (c) 2026-present, Vanilagy and contributors
3
3
  *
4
4
  * This Source Code Form is subject to the terms of the Mozilla Public
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$4 {
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$4) => Promise<void>;
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$5) => Promise<void>;
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$3 {
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$3) => Promise<void>;
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$2 {
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$2) => Promise<void>;
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$1 {
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$1) => Promise<void>;
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-BGks9KLI.js"></script>
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
- label: options?.label ?? "Edit layer",
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
- options
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.151",
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/player": "0.8.151",
64
- "@hyperframes/sdk": "0.8.151",
65
- "@hyperframes/core": "0.8.151",
66
- "@hyperframes/shader-transitions": "0.8.151",
67
- "@hyperframes/studio-server": "0.8.151",
68
- "@hyperframes/parsers": "0.8.151"
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.151"
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
- export interface RecordEditInput {
26
- label: string;
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({ label: "Resize layer box" }),
361
+ expect.objectContaining({
362
+ label: "Resize layer box",
363
+ targets: [{ file: "/comp.html", hfId: "hf-abc" }],
364
+ }),
362
365
  );
363
366
  });
364
367
 
@@ -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
- options,
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
- label: options?.label ?? "Edit layer",
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;