@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.
@@ -1,4 +1,4 @@
1
- import{n as Qi}from"./index-CS_m0TkX.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-CS_m0TkX.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
@@ -5026,17 +5026,7 @@ function matchesSelector(tweenSelector, querySelector) {
5026
5026
  }
5027
5027
 
5028
5028
  // src/hooks/gsapShared.ts
5029
- var PROPERTY_DEFAULTS = {
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] = PROPERTY_DEFAULTS[k] ?? 0;
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] = PROPERTY_DEFAULTS[k] ?? 0;
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 ?? PROPERTY_DEFAULTS[property] ?? 0 }, ...stops];
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
- label: options?.label ?? "Edit layer",
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
- 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
+ }
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] : PROPERTY_DEFAULTS[key2] ?? 0;
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] = PROPERTY_DEFAULTS[key2] ?? 0;
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 = PROPERTY_DEFAULTS[property] ?? 0;
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 = PROPERTY_DEFAULTS[property] ?? 0;
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.150",
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/parsers": "0.8.150",
64
- "@hyperframes/player": "0.8.150",
65
- "@hyperframes/sdk": "0.8.150",
66
- "@hyperframes/studio-server": "0.8.150",
67
- "@hyperframes/core": "0.8.150",
68
- "@hyperframes/shader-transitions": "0.8.150"
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.150"
85
+ "@hyperframes/producer": "0.8.152"
86
86
  },
87
87
  "peerDependencies": {
88
88
  "react": "19",
@@ -25,17 +25,7 @@ export interface IframeGsap {
25
25
 
26
26
  // ── Constants ─────────────────────────────────────────────────────────────────
27
27
 
28
- export const PROPERTY_DEFAULTS: Record<string, number> = {
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
- 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;