@hyperframes/studio 0.7.58 → 0.7.60

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.
Files changed (173) hide show
  1. package/dist/assets/{hyperframes-player-CtTDO63S.js → hyperframes-player-3XTTaVNf.js} +1 -1
  2. package/dist/assets/{index-C47jAC3Q.js → index-D6etaey-.js} +1 -1
  3. package/dist/assets/index-DXbu6IPT.css +1 -0
  4. package/dist/assets/{index-DeQPzqwH.js → index-Dh_WhagG.js} +1 -1
  5. package/dist/assets/index-cH6NfVV_.js +426 -0
  6. package/dist/index.d.ts +12 -3
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +13755 -8627
  9. package/dist/index.js.map +1 -1
  10. package/dist/styles/tailwind-preset.d.ts +5 -0
  11. package/dist/styles/tailwind-preset.js +8 -1
  12. package/dist/styles/tailwind-preset.js.map +1 -1
  13. package/package.json +7 -7
  14. package/src/App.tsx +4 -4
  15. package/src/components/DesignPanelPromoteProvider.tsx +15 -2
  16. package/src/components/StudioRightPanel.tsx +42 -53
  17. package/src/components/editor/AnimationCard.test.tsx +134 -0
  18. package/src/components/editor/AnimationCard.tsx +19 -4
  19. package/src/components/editor/ArcPathControls.tsx +1 -0
  20. package/src/components/editor/GestureRecordControl.tsx +7 -1
  21. package/src/components/editor/GsapAnimationSection.tsx +109 -14
  22. package/src/components/editor/InspectorHeaderActions.tsx +42 -4
  23. package/src/components/editor/PropertyPanel.test.tsx +948 -0
  24. package/src/components/editor/PropertyPanel.tsx +144 -148
  25. package/src/components/editor/PropertyPanelEmptyState.test.tsx +74 -0
  26. package/src/components/editor/PropertyPanelEmptyState.tsx +179 -1
  27. package/src/components/editor/PropertyPanelFlat.tsx +580 -0
  28. package/src/components/editor/PropertyPanelFlatFooter.test.tsx +83 -0
  29. package/src/components/editor/PropertyPanelFlatFooter.tsx +73 -0
  30. package/src/components/editor/PropertyPanelFlatHeader.test.tsx +79 -0
  31. package/src/components/editor/PropertyPanelFlatHeader.tsx +112 -0
  32. package/src/components/editor/gsapAnimationCallbacks.ts +27 -0
  33. package/src/components/editor/gsapLivePreview.ts +34 -0
  34. package/src/components/editor/manualEditingAvailability.test.ts +10 -0
  35. package/src/components/editor/manualEditingAvailability.ts +9 -0
  36. package/src/components/editor/propertyPanel3dTransform.tsx +5 -0
  37. package/src/components/editor/propertyPanelColor.test.tsx +28 -0
  38. package/src/components/editor/propertyPanelColor.tsx +31 -1
  39. package/src/components/editor/propertyPanelColorGradingControls.tsx +11 -2
  40. package/src/components/editor/propertyPanelColorGradingSection.tsx +45 -365
  41. package/src/components/editor/propertyPanelColorGradingSlider.tsx +19 -5
  42. package/src/components/editor/propertyPanelFill.tsx +22 -5
  43. package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +622 -0
  44. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +570 -0
  45. package/src/components/editor/propertyPanelFlatLayoutSection.test.tsx +293 -0
  46. package/src/components/editor/propertyPanelFlatLayoutSection.tsx +383 -0
  47. package/src/components/editor/propertyPanelFlatMaskInsetRows.tsx +102 -0
  48. package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +436 -0
  49. package/src/components/editor/propertyPanelFlatMediaSection.tsx +286 -0
  50. package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +272 -0
  51. package/src/components/editor/propertyPanelFlatMotionSection.tsx +315 -0
  52. package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +1174 -0
  53. package/src/components/editor/propertyPanelFlatPrimitives.tsx +563 -0
  54. package/src/components/editor/propertyPanelFlatSelectRow.tsx +107 -0
  55. package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +23 -0
  56. package/src/components/editor/propertyPanelFlatStyleHelpers.ts +27 -0
  57. package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +598 -0
  58. package/src/components/editor/propertyPanelFlatStyleSections.tsx +516 -0
  59. package/src/components/editor/propertyPanelFlatTextSection.test.tsx +465 -0
  60. package/src/components/editor/propertyPanelFlatTextSection.tsx +407 -0
  61. package/src/components/editor/propertyPanelFlatTimingDerivation.test.ts +71 -0
  62. package/src/components/editor/propertyPanelFlatTimingDerivation.ts +59 -0
  63. package/src/components/editor/propertyPanelFlatToggle.test.tsx +63 -0
  64. package/src/components/editor/propertyPanelFlatToggle.tsx +53 -0
  65. package/src/components/editor/propertyPanelFont.test.tsx +30 -0
  66. package/src/components/editor/propertyPanelFont.tsx +136 -95
  67. package/src/components/editor/propertyPanelHelpers.ts +73 -0
  68. package/src/components/editor/propertyPanelInputCoverage.test.tsx +587 -0
  69. package/src/components/editor/propertyPanelMediaSection.tsx +10 -0
  70. package/src/components/editor/propertyPanelPrimitives.tsx +60 -29
  71. package/src/components/editor/propertyPanelSections.test.tsx +161 -0
  72. package/src/components/editor/propertyPanelSections.tsx +154 -90
  73. package/src/components/editor/propertyPanelStyleSections.tsx +12 -1
  74. package/src/components/editor/propertyPanelTimingSection.tsx +1 -1
  75. package/src/components/editor/propertyPanelTypes.ts +14 -1
  76. package/src/components/editor/propertyPanelValueTier.test.ts +32 -0
  77. package/src/components/editor/propertyPanelValueTier.ts +28 -0
  78. package/src/components/editor/useColorGradingController.test.ts +418 -0
  79. package/src/components/editor/useColorGradingController.ts +596 -0
  80. package/src/components/panels/VariablesPanel.tsx +4 -0
  81. package/src/components/sidebar/CompositionsTab.test.ts +28 -2
  82. package/src/components/sidebar/CompositionsTab.tsx +3 -1
  83. package/src/components/storyboard/FramePoster.tsx +24 -4
  84. package/src/components/storyboard/StoryboardFrameFocus.tsx +4 -0
  85. package/src/components/storyboard/StoryboardFrameTile.tsx +30 -1
  86. package/src/components/storyboard/StoryboardGrid.tsx +23 -1
  87. package/src/components/storyboard/StoryboardLoaded.tsx +58 -2
  88. package/src/components/storyboard/StoryboardView.tsx +9 -2
  89. package/src/components/storyboard/frameComments.test.ts +95 -0
  90. package/src/components/storyboard/frameComments.ts +115 -0
  91. package/src/components/storyboard/useFrameComments.ts +82 -0
  92. package/src/contexts/DesignPanelInputContext.test.tsx +81 -0
  93. package/src/contexts/DesignPanelInputContext.tsx +48 -0
  94. package/src/contexts/DomEditContext.tsx +4 -0
  95. package/src/contexts/FileManagerContext.tsx +3 -0
  96. package/src/contexts/PanelLayoutContext.tsx +3 -0
  97. package/src/contexts/VariablePromoteContext.test.tsx +67 -0
  98. package/src/contexts/VariablePromoteContext.tsx +10 -5
  99. package/src/hooks/domEditCommitRunner.ts +11 -0
  100. package/src/hooks/gsapScriptCommitTypes.ts +6 -4
  101. package/src/hooks/timelineEditingHelpers.ts +2 -2
  102. package/src/hooks/timelineTrackVisibility.test.ts +52 -0
  103. package/src/hooks/timelineTrackVisibility.ts +27 -10
  104. package/src/hooks/useAppHotkeys.ts +9 -0
  105. package/src/hooks/useDomEditAttributeCommits.ts +123 -1
  106. package/src/hooks/useDomEditCommits.test.tsx +41 -1
  107. package/src/hooks/useDomEditCommits.ts +31 -177
  108. package/src/hooks/useDomEditCommitsHelpers.ts +158 -0
  109. package/src/hooks/useDomEditSession.ts +18 -6
  110. package/src/hooks/useDomEditTextCommits.ts +14 -9
  111. package/src/hooks/useDomEditWiring.ts +10 -9
  112. package/src/hooks/useEditorSave.ts +1 -1
  113. package/src/hooks/useElementLifecycleOps.ts +8 -3
  114. package/src/hooks/useFileManager.projectOwnership.test.tsx +109 -0
  115. package/src/hooks/useFileManager.ts +151 -65
  116. package/src/hooks/useGsapAnimationOps.ts +7 -6
  117. package/src/hooks/useGsapKeyframeOps.ts +6 -5
  118. package/src/hooks/useGsapPropertyDebounce.ts +43 -28
  119. package/src/hooks/useGsapPropertyDebounceFlush.test.ts +35 -0
  120. package/src/hooks/useGsapScriptCommits.test.tsx +48 -2
  121. package/src/hooks/useGsapScriptCommits.ts +150 -50
  122. package/src/hooks/useGsapSelectionHandlers.test.tsx +116 -0
  123. package/src/hooks/useGsapSelectionHandlers.ts +94 -31
  124. package/src/hooks/useInspectorSplitResize.ts +51 -0
  125. package/src/hooks/usePanelLayout.test.ts +118 -0
  126. package/src/hooks/usePanelLayout.ts +24 -1
  127. package/src/hooks/usePersistentEditHistory.projectOwnership.test.tsx +60 -0
  128. package/src/hooks/usePersistentEditHistory.test.ts +49 -1
  129. package/src/hooks/usePersistentEditHistory.ts +71 -31
  130. package/src/hooks/usePreviewPersistence.ts +4 -1
  131. package/src/hooks/useProjectCompositionVariables.ts +10 -10
  132. package/src/hooks/useProjectSignaturePoll.test.tsx +135 -0
  133. package/src/hooks/useProjectSignaturePoll.ts +68 -0
  134. package/src/hooks/useRazorSplit.history.test.tsx +12 -4
  135. package/src/hooks/useRazorSplit.test.tsx +88 -0
  136. package/src/hooks/useRazorSplit.testHelpers.ts +3 -2
  137. package/src/hooks/useRazorSplit.ts +50 -16
  138. package/src/hooks/useSdkSession.lifecycle.test.tsx +137 -0
  139. package/src/hooks/useSdkSession.ts +155 -14
  140. package/src/hooks/useSlideshowPersist.ts +7 -1
  141. package/src/hooks/useStoryboard.ts +23 -4
  142. package/src/hooks/useTimelineEditing.test.tsx +4 -1
  143. package/src/hooks/useTimelineEditing.ts +18 -10
  144. package/src/hooks/useTimelineEditingTypes.ts +5 -1
  145. package/src/hooks/useTimelineGroupEditing.ts +17 -3
  146. package/src/hooks/useVariablesPersist.ts +9 -7
  147. package/src/icons/SystemIcons.tsx +2 -0
  148. package/src/styles/studio.css +22 -0
  149. package/src/styles/tailwind-preset.shared.js +7 -0
  150. package/src/utils/designInputTracking.test.ts +97 -0
  151. package/src/utils/designInputTracking.ts +80 -0
  152. package/src/utils/domEditSaveQueue.test.ts +19 -0
  153. package/src/utils/domEditSaveQueue.ts +2 -1
  154. package/src/utils/sdkCutover.gate.test.ts +7 -7
  155. package/src/utils/sdkCutover.test.ts +511 -62
  156. package/src/utils/sdkCutover.ts +181 -219
  157. package/src/utils/sdkEditTransaction.ts +293 -0
  158. package/src/utils/sdkResolverAttempts.ts +94 -0
  159. package/src/utils/sdkResolverShadow.test.ts +148 -0
  160. package/src/utils/sdkResolverShadow.ts +139 -106
  161. package/src/utils/setSlideshowManifest.test.ts +0 -9
  162. package/src/utils/setSlideshowManifest.ts +20 -27
  163. package/src/utils/studioFileHistory.test.ts +41 -0
  164. package/src/utils/studioFileHistory.ts +34 -29
  165. package/src/utils/studioFileMutationCoordinator.ts +45 -0
  166. package/src/utils/studioFileVersion.test.ts +36 -0
  167. package/src/utils/studioFileVersion.ts +23 -0
  168. package/src/utils/studioHelpers.test.ts +45 -0
  169. package/src/utils/studioHelpers.ts +45 -14
  170. package/src/utils/studioSaveDiagnostics.test.ts +18 -0
  171. package/src/utils/studioSaveDiagnostics.ts +38 -0
  172. package/dist/assets/index-B_UvTX3E.js +0 -423
  173. package/dist/assets/index-uahwWkgw.css +0 -1
@@ -7,7 +7,7 @@ import type { RightPanelTab } from "../utils/studioHelpers";
7
7
  import type { PatchTarget } from "../utils/sourcePatcher";
8
8
  import type { SidebarTab } from "../components/sidebar/LeftSidebar";
9
9
  import type { Composition } from "@hyperframes/sdk";
10
- import { sdkCutoverPersist, sdkDeletePersist } from "../utils/sdkCutover";
10
+ import { sdkCutoverPersist, sdkDeletePersist, type PublishSdkSession } from "../utils/sdkCutover";
11
11
  import { runResolverShadow, recordResolverParity } from "../utils/sdkResolverShadow";
12
12
  import { useAskAgentModal } from "./useAskAgentModal";
13
13
  import { useDomSelection } from "./useDomSelection";
@@ -45,7 +45,7 @@ export interface UseDomEditSessionParams {
45
45
  refreshPreviewDocumentVersion: () => void;
46
46
  queueDomEditSave: <T>(save: () => Promise<T>) => Promise<T>;
47
47
  readProjectFile: (path: string) => Promise<string>;
48
- writeProjectFile: (path: string, content: string) => Promise<void>;
48
+ writeProjectFile: (path: string, content: string, expectedContent?: string) => Promise<void>;
49
49
  updateEditingFileContent: (path: string, content: string) => void;
50
50
  domEditSaveTimestampRef: React.MutableRefObject<number>;
51
51
  editHistory: { recordEdit: (entry: RecordEditInput) => Promise<void> };
@@ -67,6 +67,7 @@ export interface UseDomEditSessionParams {
67
67
  selectSidebarTab?: (tab: SidebarTab) => void;
68
68
  getSidebarTab?: () => SidebarTab;
69
69
  sdkSession?: Composition | null;
70
+ publishSdkSession?: PublishSdkSession;
70
71
  forceReloadSdkSession?: () => void;
71
72
  }
72
73
 
@@ -108,10 +109,10 @@ export function useDomEditSession({
108
109
  selectSidebarTab,
109
110
  getSidebarTab,
110
111
  sdkSession,
112
+ publishSdkSession,
111
113
  forceReloadSdkSession,
112
114
  }: UseDomEditSessionParams) {
113
115
  void _setRefreshKey;
114
-
115
116
  // ── Selection ──
116
117
 
117
118
  const {
@@ -178,7 +179,6 @@ export function useDomEditSession({
178
179
  previewDocumentVersion,
179
180
  refreshDomEditSelectionFromPreview,
180
181
  });
181
-
182
182
  // ── GSAP cache (hoisted so both useGsapScriptCommits and useDomEditWiring share the same instance) ──
183
183
 
184
184
  const { version: gsapCacheVersion, bump: bumpGsapCache } = useGsapCacheVersion();
@@ -217,6 +217,7 @@ export function useDomEditSession({
217
217
  onFileContentChanged: updateEditingFileContent,
218
218
  showToast,
219
219
  sdkSession,
220
+ publishSdkSession,
220
221
  writeProjectFile,
221
222
  forceReloadSdkSession,
222
223
  });
@@ -229,6 +230,7 @@ export function useDomEditSession({
229
230
  handleDomAttributeCommit,
230
231
  handleDomAttributeLiveCommit,
231
232
  handleDomHtmlAttributeCommit,
233
+ handleDomAttributesCommit,
232
234
  handleDomTextCommit,
233
235
  handleDomTextFieldStyleCommit,
234
236
  handleDomAddTextField,
@@ -260,8 +262,13 @@ export function useDomEditSession({
260
262
  ? (selection, operations, originalContent, targetPath, options) => {
261
263
  // Resolver shadow runs regardless of the cutover flag — decoupled tripwire.
262
264
  // Pass originalContent so the runtime-node filter can suppress hf-ids
263
- // absent from source (script-created nodes the SDK can't model).
264
- runResolverShadow(sdkSession, selection.hfId, operations, originalContent);
265
+ // absent from source (script-created nodes the SDK can't model), and
266
+ // the paths so cross-file edits (session models only the active comp)
267
+ // skip instead of emitting structural element_not_found noise.
268
+ runResolverShadow(sdkSession, selection.hfId, operations, originalContent, {
269
+ targetPath,
270
+ compositionPath: activeCompPath,
271
+ });
265
272
  return sdkCutoverPersist(
266
273
  selection,
267
274
  operations,
@@ -274,6 +281,8 @@ export function useDomEditSession({
274
281
  reloadPreview,
275
282
  domEditSaveTimestampRef,
276
283
  compositionPath: activeCompPath,
284
+ readProjectFile,
285
+ publishSession: publishSdkSession,
277
286
  },
278
287
  options,
279
288
  );
@@ -287,6 +296,8 @@ export function useDomEditSession({
287
296
  reloadPreview,
288
297
  domEditSaveTimestampRef,
289
298
  compositionPath: activeCompPath,
299
+ readProjectFile,
300
+ publishSession: publishSdkSession,
290
301
  })
291
302
  : undefined,
292
303
  // Resolver shadow for the z-index reorder edit: it takes the server path (no
@@ -524,6 +535,7 @@ export function useDomEditSession({
524
535
  handleDomAttributeCommit,
525
536
  handleDomAttributeLiveCommit,
526
537
  handleDomHtmlAttributeCommit,
538
+ handleDomAttributesCommit,
527
539
  handleDomPathOffsetCommit: handleGsapAwarePathOffsetCommit,
528
540
  handleDomGroupPathOffsetCommit: handleGsapAwareGroupPathOffsetCommit,
529
541
  handleDomZIndexReorderCommit,
@@ -145,15 +145,19 @@ export function useDomEditTextCommits({
145
145
  const domTextCommitVersionRef = useRef(0);
146
146
  const domStyleCommitVersionRef = useRef(new Map<string, number>());
147
147
 
148
- const { handleDomAttributeCommit, handleDomAttributeLiveCommit, handleDomHtmlAttributeCommit } =
149
- useDomEditAttributeCommits({
150
- activeCompPath,
151
- previewIframeRef,
152
- showToast,
153
- domEditSelection,
154
- refreshDomEditSelectionFromPreview,
155
- persistDomEditOperations,
156
- });
148
+ const {
149
+ handleDomAttributeCommit,
150
+ handleDomAttributeLiveCommit,
151
+ handleDomHtmlAttributeCommit,
152
+ handleDomAttributesCommit,
153
+ } = useDomEditAttributeCommits({
154
+ activeCompPath,
155
+ previewIframeRef,
156
+ showToast,
157
+ domEditSelection,
158
+ refreshDomEditSelectionFromPreview,
159
+ persistDomEditOperations,
160
+ });
157
161
 
158
162
  const handleDomStyleCommit = useCallback(
159
163
  async (property: string, value: string) => {
@@ -471,6 +475,7 @@ export function useDomEditTextCommits({
471
475
  handleDomAttributeCommit,
472
476
  handleDomAttributeLiveCommit,
473
477
  handleDomHtmlAttributeCommit,
478
+ handleDomAttributesCommit,
474
479
  handleDomTextCommit,
475
480
  commitDomTextFields,
476
481
  handleDomTextFieldStyleCommit,
@@ -55,24 +55,24 @@ export interface UseDomEditWiringParams {
55
55
  sel: DomEditSelection,
56
56
  animId: string,
57
57
  updates: { duration?: number; ease?: string; position?: number },
58
- ) => void;
59
- deleteGsapAnimation: (sel: DomEditSelection, animId: string) => void;
60
- deleteAllForSelector: (sel: DomEditSelection, targetSelector: string) => void;
58
+ ) => Promise<void>;
59
+ deleteGsapAnimation: (sel: DomEditSelection, animId: string) => Promise<void>;
60
+ deleteAllForSelector: (sel: DomEditSelection, targetSelector: string) => Promise<void>;
61
61
  addGsapAnimation: (
62
62
  sel: DomEditSelection,
63
63
  method: "to" | "from" | "set" | "fromTo",
64
64
  time: number,
65
65
  ) => Promise<void>;
66
- addGsapProperty: (sel: DomEditSelection, animId: string, prop: string) => void;
67
- removeGsapProperty: (sel: DomEditSelection, animId: string, prop: string) => void;
66
+ addGsapProperty: (sel: DomEditSelection, animId: string, prop: string) => Promise<void>;
67
+ removeGsapProperty: (sel: DomEditSelection, animId: string, prop: string) => Promise<void>;
68
68
  updateGsapFromProperty: (
69
69
  sel: DomEditSelection,
70
70
  animId: string,
71
71
  prop: string,
72
72
  value: number | string,
73
- ) => void;
74
- addGsapFromProperty: (sel: DomEditSelection, animId: string, prop: string) => void;
75
- removeGsapFromProperty: (sel: DomEditSelection, animId: string, prop: string) => void;
73
+ ) => Promise<void>;
74
+ addGsapFromProperty: (sel: DomEditSelection, animId: string, prop: string) => Promise<void>;
75
+ removeGsapFromProperty: (sel: DomEditSelection, animId: string, prop: string) => Promise<void>;
76
76
  addKeyframe: (
77
77
  sel: DomEditSelection,
78
78
  animId: string,
@@ -105,7 +105,7 @@ export interface UseDomEditWiringParams {
105
105
  animId: string,
106
106
  resolvedFromValues?: Record<string, number | string>,
107
107
  ) => Promise<void>;
108
- removeAllKeyframes: (sel: DomEditSelection, animId: string) => void;
108
+ removeAllKeyframes: (sel: DomEditSelection, animId: string) => Promise<void>;
109
109
  handleDomManualEditsReset: (sel: DomEditSelection) => void;
110
110
  }
111
111
 
@@ -245,6 +245,7 @@ export function useDomEditWiring({
245
245
  removeAllKeyframes,
246
246
  handleDomManualEditsReset,
247
247
  selectedGsapAnimations,
248
+ showToast,
248
249
  });
249
250
 
250
251
  // ── Preview sync side-effects ──
@@ -14,7 +14,7 @@ interface UseEditorSaveOptions {
14
14
  editingPathRef: React.RefObject<string | undefined>;
15
15
  projectIdRef: React.RefObject<string | null>;
16
16
  readProjectFile: (path: string) => Promise<string>;
17
- writeProjectFile: (path: string, content: string) => Promise<void>;
17
+ writeProjectFile: (path: string, content: string, expectedContent?: string) => Promise<void>;
18
18
  recordEdit: (input: RecordEditInput) => Promise<void>;
19
19
  domEditSaveTimestampRef: React.MutableRefObject<number>;
20
20
  setRefreshKey: React.Dispatch<React.SetStateAction<number>>;
@@ -20,10 +20,15 @@ import {
20
20
  type LayerRevealCommitOwnership,
21
21
  } from "../components/editor/useLayerRevealOverride";
22
22
  import type { CommitDomEditPatchBatches, DomEditPatchBatch } from "./domEditCommitTypes";
23
+ import { cutoverCommittedOrThrow, type CutoverResult } from "../utils/sdkCutover";
23
24
 
24
25
  interface UseElementLifecycleOpsParams extends DomEditCommitBaseParams {
25
- /** Route delete through SDK when session resolves the hf-id; returns true if handled. */
26
- onTrySdkDelete?: (hfId: string, originalContent: string, targetPath: string) => Promise<boolean>;
26
+ /** Route delete through SDK when session resolves the hf-id. */
27
+ onTrySdkDelete?: (
28
+ hfId: string,
29
+ originalContent: string,
30
+ targetPath: string,
31
+ ) => Promise<CutoverResult>;
27
32
  /** Resolver-shadow tripwire for the reordered targets (telemetry-only, decoupled from cutover). */
28
33
  onReorderShadow?: (targets: string[]) => void;
29
34
  /** Resync the SDK session after a server-fallback delete. */
@@ -97,7 +102,7 @@ export function useElementLifecycleOps({
97
102
 
98
103
  if (onTrySdkDelete && selection.hfId) {
99
104
  const handled = await onTrySdkDelete(selection.hfId, originalContent, targetPath);
100
- if (handled) {
105
+ if (cutoverCommittedOrThrow(handled)) {
101
106
  clearDomSelection();
102
107
  usePlayerStore.getState().setSelectedElementId(null);
103
108
  showToast(`Deleted ${label}. Use Undo to restore it.`, "info");
@@ -0,0 +1,109 @@
1
+ // @vitest-environment happy-dom
2
+ import { act } from "react";
3
+ import { createRoot } from "react-dom/client";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+
6
+ vi.mock("./useFileTree", () => ({
7
+ useFileTree: () => ({
8
+ projectDir: "",
9
+ fileTree: [],
10
+ setFileTree: vi.fn(),
11
+ fileTreeLoaded: true,
12
+ refreshFileTree: vi.fn(async () => {}),
13
+ compositions: [],
14
+ assets: [],
15
+ fontAssets: [],
16
+ }),
17
+ }));
18
+
19
+ vi.mock("./useEditorSave", () => ({
20
+ useEditorSave: () => ({
21
+ saveRafRef: { current: null },
22
+ handleContentChange: vi.fn(),
23
+ }),
24
+ }));
25
+
26
+ import { useFileManager } from "./useFileManager";
27
+
28
+ (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
29
+
30
+ describe("useFileManager project ownership", () => {
31
+ afterEach(() => {
32
+ vi.unstubAllGlobals();
33
+ });
34
+
35
+ it("keeps delayed callbacks bound to the project that created them", async () => {
36
+ let resolveProjectARead: ((value: Response) => void) | undefined;
37
+ const projectARead = new Promise<Response>((resolve) => {
38
+ resolveProjectARead = resolve;
39
+ });
40
+ const fetchMock = vi.fn((url: string, init?: RequestInit) => {
41
+ if (url.endsWith("/files/missing.html") && !init?.method) {
42
+ return Promise.resolve({ ok: false, status: 404 } as Response);
43
+ }
44
+ if (url.includes("project-a") && !init?.method) return projectARead;
45
+ if (!init?.method) {
46
+ return Promise.resolve({
47
+ ok: true,
48
+ json: async () => ({ content: "PROJECT_B", version: "b-v1" }),
49
+ } as Response);
50
+ }
51
+ return Promise.resolve({ ok: true, json: async () => ({ version: "a-v2" }) } as Response);
52
+ });
53
+ vi.stubGlobal("fetch", fetchMock);
54
+
55
+ const captured: { manager: ReturnType<typeof useFileManager> | null } = { manager: null };
56
+ function Probe({ projectId }: { projectId: string }) {
57
+ captured.manager = useFileManager({
58
+ projectId,
59
+ showToast: vi.fn(),
60
+ recordEdit: vi.fn(async () => {}),
61
+ domEditSaveTimestampRef: { current: 0 },
62
+ setRefreshKey: vi.fn(),
63
+ });
64
+ return null;
65
+ }
66
+
67
+ const root = createRoot(document.createElement("div"));
68
+ await act(async () => root.render(<Probe projectId="project-a/../other?x=1" />));
69
+ const managerA = captured.manager;
70
+ if (!managerA) throw new Error("project A manager did not render");
71
+ const delayedRead = managerA.readProjectFile("index.html");
72
+
73
+ await act(async () => root.render(<Probe projectId="project-b#fragment" />));
74
+ const managerB = captured.manager;
75
+ if (!managerB) throw new Error("project B manager did not render");
76
+ expect(managerB.writeProjectFile).not.toBe(managerA.writeProjectFile);
77
+
78
+ resolveProjectARead?.({
79
+ ok: true,
80
+ json: async () => ({ content: "PROJECT_A", version: "a-v1" }),
81
+ } as Response);
82
+ await expect(delayedRead).resolves.toBe("PROJECT_A");
83
+ await managerA.writeProjectFile("index.html", "A_AFTER");
84
+ await managerA.writeProjectFile("missing.html", "A_NEW");
85
+ await expect(managerB.readProjectFile("index.html")).resolves.toBe("PROJECT_B");
86
+ await expect(managerB.readOptionalProjectFile("index.html")).resolves.toBe("PROJECT_B");
87
+
88
+ expect(fetchMock).toHaveBeenCalledWith(
89
+ "/api/projects/project-a%2F..%2Fother%3Fx%3D1/files/index.html",
90
+ );
91
+ expect(fetchMock).toHaveBeenCalledWith(
92
+ "/api/projects/project-a%2F..%2Fother%3Fx%3D1/files/index.html",
93
+ expect.objectContaining({ method: "PUT", body: "A_AFTER" }),
94
+ );
95
+ expect(fetchMock).toHaveBeenCalledWith(
96
+ "/api/projects/project-a%2F..%2Fother%3Fx%3D1/files/missing.html",
97
+ );
98
+ expect(fetchMock).toHaveBeenCalledWith(
99
+ "/api/projects/project-a%2F..%2Fother%3Fx%3D1/files/missing.html",
100
+ expect.objectContaining({ method: "PUT", body: "A_NEW" }),
101
+ );
102
+ expect(fetchMock).toHaveBeenCalledWith("/api/projects/project-b%23fragment/files/index.html");
103
+ expect(fetchMock).toHaveBeenCalledWith(
104
+ "/api/projects/project-b%23fragment/files/index.html?optional=1",
105
+ );
106
+
107
+ await act(async () => root.unmount());
108
+ });
109
+ });