@hyperframes/studio 0.7.57 → 0.7.59

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 (247) hide show
  1. package/dist/assets/{hyperframes-player--Z69cEkE.js → hyperframes-player-Bm07FLkl.js} +1 -1
  2. package/dist/assets/{index-KsfE1bUu.js → index-B995FG46.js} +1 -1
  3. package/dist/assets/index-CrkAdJkb.js +426 -0
  4. package/dist/assets/index-Dj5p8U_A.css +1 -0
  5. package/dist/assets/{index-Bf1x1y8H.js → index-FvzmPhfG.js} +1 -1
  6. package/dist/{chunk-5QSIMBEJ.js → chunk-OBAG3GWK.js} +33 -21
  7. package/dist/chunk-OBAG3GWK.js.map +1 -0
  8. package/dist/{domEditingLayers-6LQGKPOI.js → domEditingLayers-2CKWGEHS.js} +2 -2
  9. package/dist/index.d.ts +27 -7
  10. package/dist/index.html +2 -2
  11. package/dist/index.js +18029 -11563
  12. package/dist/index.js.map +1 -1
  13. package/dist/styles/tailwind-preset.d.ts +5 -0
  14. package/dist/styles/tailwind-preset.js +8 -1
  15. package/dist/styles/tailwind-preset.js.map +1 -1
  16. package/package.json +7 -7
  17. package/src/App.tsx +6 -6
  18. package/src/components/DesignPanelPromoteProvider.tsx +15 -2
  19. package/src/components/StudioFeedbackBar.tsx +2 -2
  20. package/src/components/StudioRightPanel.tsx +29 -51
  21. package/src/components/editor/AnimationCard.test.tsx +134 -0
  22. package/src/components/editor/AnimationCard.tsx +19 -4
  23. package/src/components/editor/ArcPathControls.tsx +1 -0
  24. package/src/components/editor/CanvasContextMenu.test.tsx +27 -14
  25. package/src/components/editor/CanvasContextMenu.tsx +71 -6
  26. package/src/components/editor/DomEditOverlay.tsx +5 -2
  27. package/src/components/editor/GestureRecordControl.tsx +7 -1
  28. package/src/components/editor/GsapAnimationSection.tsx +109 -14
  29. package/src/components/editor/InspectorHeaderActions.tsx +42 -4
  30. package/src/components/editor/LayersPanel.tsx +110 -99
  31. package/src/components/editor/PropertyPanel.test.tsx +948 -0
  32. package/src/components/editor/PropertyPanel.tsx +144 -148
  33. package/src/components/editor/PropertyPanelEmptyState.test.tsx +74 -0
  34. package/src/components/editor/PropertyPanelEmptyState.tsx +179 -1
  35. package/src/components/editor/PropertyPanelFlat.tsx +580 -0
  36. package/src/components/editor/PropertyPanelFlatFooter.test.tsx +83 -0
  37. package/src/components/editor/PropertyPanelFlatFooter.tsx +73 -0
  38. package/src/components/editor/PropertyPanelFlatHeader.test.tsx +79 -0
  39. package/src/components/editor/PropertyPanelFlatHeader.tsx +112 -0
  40. package/src/components/editor/canvasContextMenuZOrder.test.ts +57 -0
  41. package/src/components/editor/canvasContextMenuZOrder.ts +40 -1
  42. package/src/components/editor/domEditingDom.ts +5 -6
  43. package/src/components/editor/domEditingElement.ts +15 -32
  44. package/src/components/editor/gsapAnimationCallbacks.ts +27 -0
  45. package/src/components/editor/gsapLivePreview.ts +34 -0
  46. package/src/components/editor/layersPanelSort.ts +80 -0
  47. package/src/components/editor/manualEditingAvailability.test.ts +10 -0
  48. package/src/components/editor/manualEditingAvailability.ts +9 -0
  49. package/src/components/editor/propertyPanel3dTransform.tsx +5 -0
  50. package/src/components/editor/propertyPanelColor.test.tsx +28 -0
  51. package/src/components/editor/propertyPanelColor.tsx +31 -1
  52. package/src/components/editor/propertyPanelColorGradingControls.tsx +11 -2
  53. package/src/components/editor/propertyPanelColorGradingSection.tsx +45 -365
  54. package/src/components/editor/propertyPanelColorGradingSlider.tsx +19 -5
  55. package/src/components/editor/propertyPanelFill.tsx +22 -5
  56. package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +622 -0
  57. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +570 -0
  58. package/src/components/editor/propertyPanelFlatLayoutSection.test.tsx +293 -0
  59. package/src/components/editor/propertyPanelFlatLayoutSection.tsx +383 -0
  60. package/src/components/editor/propertyPanelFlatMaskInsetRows.tsx +102 -0
  61. package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +436 -0
  62. package/src/components/editor/propertyPanelFlatMediaSection.tsx +286 -0
  63. package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +272 -0
  64. package/src/components/editor/propertyPanelFlatMotionSection.tsx +315 -0
  65. package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +1174 -0
  66. package/src/components/editor/propertyPanelFlatPrimitives.tsx +563 -0
  67. package/src/components/editor/propertyPanelFlatSelectRow.tsx +101 -0
  68. package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +23 -0
  69. package/src/components/editor/propertyPanelFlatStyleHelpers.ts +27 -0
  70. package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +598 -0
  71. package/src/components/editor/propertyPanelFlatStyleSections.tsx +516 -0
  72. package/src/components/editor/propertyPanelFlatTextSection.test.tsx +465 -0
  73. package/src/components/editor/propertyPanelFlatTextSection.tsx +407 -0
  74. package/src/components/editor/propertyPanelFlatTimingDerivation.test.ts +71 -0
  75. package/src/components/editor/propertyPanelFlatTimingDerivation.ts +59 -0
  76. package/src/components/editor/propertyPanelFlatToggle.test.tsx +63 -0
  77. package/src/components/editor/propertyPanelFlatToggle.tsx +53 -0
  78. package/src/components/editor/propertyPanelFont.test.tsx +30 -0
  79. package/src/components/editor/propertyPanelFont.tsx +136 -95
  80. package/src/components/editor/propertyPanelHelpers.ts +73 -0
  81. package/src/components/editor/propertyPanelInputCoverage.test.tsx +587 -0
  82. package/src/components/editor/propertyPanelMediaSection.tsx +10 -0
  83. package/src/components/editor/propertyPanelPrimitives.tsx +60 -29
  84. package/src/components/editor/propertyPanelSections.test.tsx +161 -0
  85. package/src/components/editor/propertyPanelSections.tsx +154 -90
  86. package/src/components/editor/propertyPanelStyleSections.tsx +12 -1
  87. package/src/components/editor/propertyPanelTimingSection.tsx +1 -1
  88. package/src/components/editor/propertyPanelTypes.ts +14 -1
  89. package/src/components/editor/propertyPanelValueTier.test.ts +32 -0
  90. package/src/components/editor/propertyPanelValueTier.ts +28 -0
  91. package/src/components/editor/useColorGradingController.test.ts +418 -0
  92. package/src/components/editor/useColorGradingController.ts +596 -0
  93. package/src/components/editor/useLayerRevealOverride.test.ts +169 -0
  94. package/src/components/editor/useLayerRevealOverride.ts +389 -0
  95. package/src/components/nle/PreviewOverlays.tsx +27 -2
  96. package/src/components/nle/TimelinePane.test.ts +3 -0
  97. package/src/components/nle/TimelinePane.tsx +11 -1
  98. package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +440 -0
  99. package/src/components/nle/useCanvasZOrderTimelineMirror.ts +191 -0
  100. package/src/components/nle/useTimelineEditCallbacks.ts +1 -0
  101. package/src/components/nle/zLaneGesture.test.ts +176 -0
  102. package/src/components/nle/zLaneGesture.ts +77 -0
  103. package/src/components/panels/VariablesPanel.tsx +4 -0
  104. package/src/components/sidebar/CompositionsTab.test.ts +28 -2
  105. package/src/components/sidebar/CompositionsTab.tsx +3 -1
  106. package/src/components/storyboard/FramePoster.tsx +24 -4
  107. package/src/components/storyboard/StoryboardFrameFocus.tsx +4 -0
  108. package/src/components/storyboard/StoryboardFrameTile.tsx +30 -1
  109. package/src/components/storyboard/StoryboardGrid.tsx +23 -1
  110. package/src/components/storyboard/StoryboardLoaded.tsx +58 -2
  111. package/src/components/storyboard/StoryboardView.tsx +9 -2
  112. package/src/components/storyboard/frameComments.test.ts +95 -0
  113. package/src/components/storyboard/frameComments.ts +115 -0
  114. package/src/components/storyboard/useFrameComments.ts +82 -0
  115. package/src/contexts/DesignPanelInputContext.test.tsx +81 -0
  116. package/src/contexts/DesignPanelInputContext.tsx +48 -0
  117. package/src/contexts/DomEditContext.tsx +4 -0
  118. package/src/contexts/VariablePromoteContext.test.tsx +67 -0
  119. package/src/contexts/VariablePromoteContext.tsx +10 -5
  120. package/src/hooks/domEditCommitRunner.ts +11 -0
  121. package/src/hooks/domEditCommitTypes.ts +17 -1
  122. package/src/hooks/elementLifecycleOpsTestUtils.ts +25 -0
  123. package/src/hooks/fetchStubTestUtils.ts +16 -0
  124. package/src/hooks/gsapScriptCommitTypes.ts +6 -4
  125. package/src/hooks/timelineEditingHelpers.test.ts +92 -1
  126. package/src/hooks/timelineEditingHelpers.ts +32 -15
  127. package/src/hooks/timelineMoveAdapter.ts +2 -1
  128. package/src/hooks/timelineTimingSync.test.ts +703 -33
  129. package/src/hooks/timelineTimingSync.ts +282 -97
  130. package/src/hooks/timelineTrackVisibility.test.ts +55 -1
  131. package/src/hooks/timelineTrackVisibility.ts +31 -13
  132. package/src/hooks/useAppHotkeys.ts +9 -0
  133. package/src/hooks/useClipboard.ts +19 -17
  134. package/src/hooks/useDomEditAttributeCommits.ts +123 -1
  135. package/src/hooks/useDomEditCommits.test.tsx +316 -38
  136. package/src/hooks/useDomEditCommits.ts +81 -162
  137. package/src/hooks/useDomEditCommitsHelpers.ts +158 -0
  138. package/src/hooks/useDomEditSession.test.tsx +3 -1
  139. package/src/hooks/useDomEditSession.ts +18 -6
  140. package/src/hooks/useDomEditTextCommits.ts +14 -9
  141. package/src/hooks/useDomEditWiring.ts +10 -9
  142. package/src/hooks/useElementLifecycleOps.test.tsx +426 -60
  143. package/src/hooks/useElementLifecycleOps.ts +182 -84
  144. package/src/hooks/useFileManager.projectOwnership.test.tsx +98 -0
  145. package/src/hooks/useFileManager.ts +82 -60
  146. package/src/hooks/useGsapAnimationOps.ts +7 -6
  147. package/src/hooks/useGsapKeyframeOps.ts +6 -5
  148. package/src/hooks/useGsapPropertyDebounce.ts +43 -28
  149. package/src/hooks/useGsapPropertyDebounceFlush.test.ts +35 -0
  150. package/src/hooks/useGsapScriptCommits.test.tsx +48 -2
  151. package/src/hooks/useGsapScriptCommits.ts +150 -50
  152. package/src/hooks/useGsapSelectionHandlers.test.tsx +116 -0
  153. package/src/hooks/useGsapSelectionHandlers.ts +94 -31
  154. package/src/hooks/useInspectorSplitResize.ts +51 -0
  155. package/src/hooks/usePersistentEditHistory.projectOwnership.test.tsx +60 -0
  156. package/src/hooks/usePersistentEditHistory.test.ts +49 -1
  157. package/src/hooks/usePersistentEditHistory.ts +71 -31
  158. package/src/hooks/usePreviewPersistence.ts +2 -2
  159. package/src/hooks/useProjectCompositionVariables.ts +10 -10
  160. package/src/hooks/useProjectSignaturePoll.test.tsx +135 -0
  161. package/src/hooks/useProjectSignaturePoll.ts +68 -0
  162. package/src/hooks/useSdkSession.lifecycle.test.tsx +137 -0
  163. package/src/hooks/useSdkSession.ts +155 -14
  164. package/src/hooks/useSlideshowPersist.ts +7 -1
  165. package/src/hooks/useStoryboard.ts +23 -4
  166. package/src/hooks/useTimelineEditing.test.tsx +306 -155
  167. package/src/hooks/useTimelineEditing.ts +111 -101
  168. package/src/hooks/useTimelineEditingTypes.ts +4 -1
  169. package/src/hooks/useTimelineGroupEditing.ts +51 -5
  170. package/src/hooks/useVariablesPersist.ts +9 -7
  171. package/src/icons/SystemIcons.tsx +2 -0
  172. package/src/player/components/Timeline.test.ts +31 -21
  173. package/src/player/components/Timeline.tsx +62 -91
  174. package/src/player/components/TimelineCanvas.tsx +36 -5
  175. package/src/player/components/TimelineLanes.tsx +32 -10
  176. package/src/player/components/TimelineOverlays.tsx +36 -0
  177. package/src/player/components/TimelineRuler.tsx +18 -5
  178. package/src/player/components/TrackGapContextMenu.tsx +116 -0
  179. package/src/player/components/timelineCallbacks.ts +4 -1
  180. package/src/player/components/timelineClipDragCommit.test.ts +279 -3
  181. package/src/player/components/timelineClipDragCommit.ts +173 -71
  182. package/src/player/components/timelineGapCommit.test.ts +163 -0
  183. package/src/player/components/timelineGapCommit.ts +103 -0
  184. package/src/player/components/timelineGaps.test.ts +221 -0
  185. package/src/player/components/timelineGaps.ts +182 -0
  186. package/src/player/components/timelineLayout.test.ts +2 -1
  187. package/src/player/components/timelineLayout.ts +22 -6
  188. package/src/player/components/timelineMarquee.test.ts +20 -10
  189. package/src/player/components/timelineMarquee.ts +9 -2
  190. package/src/player/components/timelineStackingSync.test.ts +42 -1
  191. package/src/player/components/timelineStackingSync.ts +31 -16
  192. package/src/player/components/timelineZMirror.test.ts +667 -0
  193. package/src/player/components/timelineZMirror.ts +393 -0
  194. package/src/player/components/timelineZoom.test.ts +2 -2
  195. package/src/player/components/timelineZoom.ts +7 -3
  196. package/src/player/components/useTimelineClipDrag.ts +1 -1
  197. package/src/player/components/useTimelineGapHighlights.test.ts +119 -0
  198. package/src/player/components/useTimelineGapHighlights.ts +110 -0
  199. package/src/player/components/useTimelinePlayhead.ts +4 -3
  200. package/src/player/components/useTimelineRangeSelection.ts +6 -4
  201. package/src/player/components/useTimelineScrollViewport.ts +73 -0
  202. package/src/player/components/useTimelineStackingSync.test.tsx +0 -1
  203. package/src/player/components/useTimelineStackingSync.ts +5 -3
  204. package/src/player/components/useTimelineTrackDerivations.ts +38 -0
  205. package/src/player/components/useTrackGapMenu.ts +155 -0
  206. package/src/player/hooks/useExpandedTimelineElements.test.ts +67 -4
  207. package/src/player/hooks/useExpandedTimelineElements.ts +12 -1
  208. package/src/player/hooks/useTimelineSyncCallbacks.ts +2 -0
  209. package/src/player/lib/layerOrdering.test.ts +1 -15
  210. package/src/player/lib/layerOrdering.ts +5 -28
  211. package/src/player/lib/timelineDOM.test.ts +72 -0
  212. package/src/player/lib/timelineDOM.ts +3 -0
  213. package/src/player/lib/timelineElementHelpers.ts +35 -11
  214. package/src/player/store/playerStore.ts +12 -1
  215. package/src/styles/studio.css +22 -0
  216. package/src/styles/tailwind-preset.shared.js +7 -0
  217. package/src/telemetry/events.test.ts +10 -0
  218. package/src/telemetry/events.ts +1 -0
  219. package/src/utils/designInputTracking.test.ts +97 -0
  220. package/src/utils/designInputTracking.ts +80 -0
  221. package/src/utils/domEditSaveQueue.ts +4 -3
  222. package/src/utils/editHistory.test.ts +82 -0
  223. package/src/utils/editHistory.ts +14 -2
  224. package/src/utils/gsapSoftReload.test.ts +54 -115
  225. package/src/utils/gsapSoftReload.ts +42 -152
  226. package/src/utils/gsapUndoRestore.test.ts +269 -0
  227. package/src/utils/gsapUndoRestore.ts +269 -0
  228. package/src/utils/sdkCutover.gate.test.ts +7 -7
  229. package/src/utils/sdkCutover.test.ts +490 -57
  230. package/src/utils/sdkCutover.ts +181 -219
  231. package/src/utils/sdkEditTransaction.ts +292 -0
  232. package/src/utils/sdkResolverAttempts.ts +94 -0
  233. package/src/utils/sdkResolverShadow.test.ts +148 -0
  234. package/src/utils/sdkResolverShadow.ts +139 -106
  235. package/src/utils/setSlideshowManifest.test.ts +0 -9
  236. package/src/utils/setSlideshowManifest.ts +20 -27
  237. package/src/utils/sourceScopedSelectorIndex.ts +29 -0
  238. package/src/utils/studioFileHistory.test.ts +41 -0
  239. package/src/utils/studioFileHistory.ts +30 -27
  240. package/src/utils/studioFileMutationCoordinator.ts +45 -0
  241. package/src/utils/studioHelpers.test.ts +45 -0
  242. package/src/utils/studioHelpers.ts +45 -14
  243. package/src/utils/studioSaveDiagnostics.ts +17 -0
  244. package/dist/assets/index-DfmYkU44.js +0 -423
  245. package/dist/assets/index-_pqzyxB1.css +0 -1
  246. package/dist/chunk-5QSIMBEJ.js.map +0 -1
  247. /package/dist/{domEditingLayers-6LQGKPOI.js.map → domEditingLayers-2CKWGEHS.js.map} +0 -0
@@ -0,0 +1,176 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { runLaneZGesture, runZLaneGesture } from "./zLaneGesture";
3
+
4
+ const durable = { durable: true, allMatched: true, changed: true };
5
+
6
+ function deferred<T>() {
7
+ let resolve!: (v: T) => void;
8
+ let reject!: (e: unknown) => void;
9
+ const promise = new Promise<T>((res, rej) => {
10
+ resolve = res;
11
+ reject = rej;
12
+ });
13
+ return { promise, resolve, reject };
14
+ }
15
+
16
+ describe("runZLaneGesture", () => {
17
+ it("runs the mirror after a durable z commit and resolves its result", async () => {
18
+ const order: string[] = [];
19
+ const result = await runZLaneGesture({
20
+ commitZ: async () => {
21
+ order.push("z");
22
+ return durable;
23
+ },
24
+ mirror: async () => {
25
+ order.push("mirror");
26
+ return true;
27
+ },
28
+ });
29
+ expect(result).toBe(true);
30
+ expect(order).toEqual(["z", "mirror"]);
31
+ });
32
+
33
+ it("skips the mirror when the z commit reports unmatched targets", async () => {
34
+ const mirror = vi.fn(async () => true);
35
+ const result = await runZLaneGesture({
36
+ commitZ: async () => ({ durable: false, allMatched: false, changed: true }),
37
+ mirror,
38
+ });
39
+ expect(result).toBe(false);
40
+ expect(mirror).not.toHaveBeenCalled();
41
+ });
42
+
43
+ it("still mirrors on a void resolution (empty-entries commit path)", async () => {
44
+ const mirror = vi.fn(async () => true);
45
+ await runZLaneGesture({ commitZ: async () => undefined, mirror });
46
+ expect(mirror).toHaveBeenCalledTimes(1);
47
+ });
48
+
49
+ it("serializes gestures: B's z phase waits for A's mirror phase", async () => {
50
+ const order: string[] = [];
51
+ const aMirrorGate = deferred<void>();
52
+
53
+ const a = runZLaneGesture({
54
+ commitZ: async () => {
55
+ order.push("A:z");
56
+ return durable;
57
+ },
58
+ mirror: async () => {
59
+ await aMirrorGate.promise;
60
+ order.push("A:mirror");
61
+ return true;
62
+ },
63
+ });
64
+ const b = runZLaneGesture({
65
+ commitZ: async () => {
66
+ order.push("B:z");
67
+ return durable;
68
+ },
69
+ mirror: async () => {
70
+ order.push("B:mirror");
71
+ return true;
72
+ },
73
+ });
74
+
75
+ // Give B every chance to start early — it must not.
76
+ await new Promise((r) => setTimeout(r, 10));
77
+ expect(order).toEqual(["A:z"]);
78
+
79
+ aMirrorGate.resolve();
80
+ await Promise.all([a, b]);
81
+ expect(order).toEqual(["A:z", "A:mirror", "B:z", "B:mirror"]);
82
+ });
83
+
84
+ it("holds a lane→z gesture until an overlapping z→lane gesture fully settles", async () => {
85
+ const order: string[] = [];
86
+ const mirrorGate = deferred<void>();
87
+ const zFirst = runZLaneGesture({
88
+ commitZ: async () => {
89
+ order.push("z-first:z");
90
+ return durable;
91
+ },
92
+ mirror: async () => {
93
+ order.push("z-first:lane-start");
94
+ await mirrorGate.promise;
95
+ order.push("z-first:lane-end");
96
+ return true;
97
+ },
98
+ });
99
+ const laneFirst = runLaneZGesture({
100
+ commitLane: async () => {
101
+ order.push("lane-first:lane");
102
+ return true;
103
+ },
104
+ commitZ: async () => {
105
+ order.push("lane-first:z");
106
+ },
107
+ });
108
+
109
+ await Promise.resolve();
110
+ expect(order).toEqual(["z-first:z", "z-first:lane-start"]);
111
+ mirrorGate.resolve();
112
+ await Promise.all([zFirst, laneFirst]);
113
+ expect(order).toEqual([
114
+ "z-first:z",
115
+ "z-first:lane-start",
116
+ "z-first:lane-end",
117
+ "lane-first:lane",
118
+ "lane-first:z",
119
+ ]);
120
+ });
121
+
122
+ it("holds a z→lane gesture until an overlapping lane→z gesture fully settles", async () => {
123
+ const order: string[] = [];
124
+ const zGate = deferred<void>();
125
+ const laneFirst = runLaneZGesture({
126
+ commitLane: async () => {
127
+ order.push("lane-first:lane");
128
+ return true;
129
+ },
130
+ commitZ: async () => {
131
+ order.push("lane-first:z-start");
132
+ await zGate.promise;
133
+ order.push("lane-first:z-end");
134
+ },
135
+ });
136
+ const zFirst = runZLaneGesture({
137
+ commitZ: async () => {
138
+ order.push("z-first:z");
139
+ return durable;
140
+ },
141
+ mirror: async () => {
142
+ order.push("z-first:lane");
143
+ return true;
144
+ },
145
+ });
146
+
147
+ await Promise.resolve();
148
+ expect(order).toEqual(["lane-first:lane", "lane-first:z-start"]);
149
+ zGate.resolve();
150
+ await Promise.all([laneFirst, zFirst]);
151
+ expect(order).toEqual([
152
+ "lane-first:lane",
153
+ "lane-first:z-start",
154
+ "lane-first:z-end",
155
+ "z-first:z",
156
+ "z-first:lane",
157
+ ]);
158
+ });
159
+
160
+ it("a failed gesture rejects its caller but never wedges the queue", async () => {
161
+ const boom = new Error("z failed");
162
+ const failed = runZLaneGesture({
163
+ commitZ: async () => {
164
+ throw boom;
165
+ },
166
+ mirror: async () => true,
167
+ });
168
+ await expect(failed).rejects.toBe(boom);
169
+
170
+ const next = await runZLaneGesture({
171
+ commitZ: async () => durable,
172
+ mirror: async () => true,
173
+ });
174
+ expect(next).toBe(true);
175
+ });
176
+ });
@@ -0,0 +1,77 @@
1
+ import type { DomEditPatchBatchesResult } from "../../hooks/domEditCommitTypes";
2
+
3
+ /**
4
+ * Run one COMPLETE z→lane gesture — the z-index persist followed by its
5
+ * timeline lane mirror — as a single serialized transaction.
6
+ *
7
+ * Why a queue: the two phases persist through DIFFERENT pipelines (the z patch
8
+ * rides the DOM-edit save queue, the lane move rides the timeline/SDK move
9
+ * path). Each gesture orders its own phases by awaiting the z persist, but
10
+ * without cross-gesture serialization a second rapid gesture can land between
11
+ * the first gesture's phases and the interleaved file writes can clobber each
12
+ * other. Canvas z→lane and timeline lane→z gestures both chain through this
13
+ * single module-level tail, so gesture B cannot start until gesture A settled.
14
+ *
15
+ * Why the durability gate: a resolved z commit is not necessarily durable —
16
+ * when the server cannot match a patch target, commitDomEditPatchBatches
17
+ * resolves with `durable: false` (after scheduling a reload to
18
+ * reconverge). Mirroring a lane move onto a z state that disk never held
19
+ * would desync track order from what actually paints, so the mirror phase is
20
+ * skipped and the gesture resolves `false`.
21
+ *
22
+ * Failures never wedge the queue: a rejected gesture propagates to ITS caller
23
+ * while the tail continues for the next gesture.
24
+ */
25
+ let gestureTail: Promise<void> | null = null;
26
+
27
+ /** The single ordering owner for every gesture that crosses z/lane persistence. */
28
+ export function serializeZLaneGesture<Result>(run: () => Promise<Result>): Promise<Result> {
29
+ const previous = gestureTail;
30
+ // Preserve synchronous optimistic updates for the first gesture. A queued
31
+ // gesture starts only after the preceding gesture fully settled.
32
+ let gesture: Promise<Result>;
33
+ if (previous) {
34
+ gesture = previous.then(run, run);
35
+ } else {
36
+ try {
37
+ gesture = Promise.resolve(run());
38
+ } catch (error) {
39
+ gesture = Promise.reject(error);
40
+ }
41
+ }
42
+ const tail = gesture.then(
43
+ () => undefined,
44
+ () => undefined,
45
+ );
46
+ gestureTail = tail;
47
+ void tail.then(() => {
48
+ if (gestureTail === tail) gestureTail = null;
49
+ });
50
+ return gesture;
51
+ }
52
+
53
+ export function runZLaneGesture(input: {
54
+ /** Phase 1: persist the z patch (handleDomZIndexReorderCommit). */
55
+ commitZ: () => Promise<DomEditPatchBatchesResult | undefined | void>;
56
+ /** Phase 2: mirror into a timeline lane move; only runs on a durable phase 1. */
57
+ mirror: () => Promise<boolean>;
58
+ }): Promise<boolean> {
59
+ const run = async (): Promise<boolean> => {
60
+ const result = await input.commitZ();
61
+ if (result && !result.durable) return false;
62
+ return input.mirror();
63
+ };
64
+ return serializeZLaneGesture(run);
65
+ }
66
+
67
+ /** Run one lane→z gesture under the same owner as canvas z→lane gestures. */
68
+ export function runLaneZGesture(input: {
69
+ commitLane: () => Promise<boolean>;
70
+ commitZ: () => Promise<void>;
71
+ }): Promise<boolean> {
72
+ return serializeZLaneGesture(async () => {
73
+ if (!(await input.commitLane())) return false;
74
+ await input.commitZ();
75
+ return true;
76
+ });
77
+ }
@@ -6,6 +6,7 @@ import type {
6
6
  VariableValidationIssue,
7
7
  } from "@hyperframes/sdk";
8
8
  import type { EditHistoryKind } from "../../utils/editHistory";
9
+ import type { PublishSdkSession } from "../../utils/sdkCutover";
9
10
  import { useStudioPlaybackContext, useStudioShellContext } from "../../contexts/StudioContext";
10
11
  import { useDomEditContext } from "../../contexts/DomEditContext";
11
12
  import { useFileManagerContext } from "../../contexts/FileManagerContext";
@@ -32,6 +33,7 @@ function shellSingleQuote(value: string): string {
32
33
 
33
34
  interface VariablesPanelProps {
34
35
  sdkSession: Composition | null;
36
+ publishSdkSession: PublishSdkSession;
35
37
  reloadPreview: () => void;
36
38
  domEditSaveTimestampRef: MutableRefObject<number>;
37
39
  recordEdit: (entry: {
@@ -247,6 +249,7 @@ const EMPTY_STATE = (
247
249
  // fallow-ignore-next-line complexity
248
250
  export const VariablesPanel = memo(function VariablesPanel({
249
251
  sdkSession,
252
+ publishSdkSession,
250
253
  reloadPreview,
251
254
  domEditSaveTimestampRef,
252
255
  recordEdit,
@@ -285,6 +288,7 @@ export const VariablesPanel = memo(function VariablesPanel({
285
288
  recordEdit,
286
289
  reloadPreview,
287
290
  domEditSaveTimestampRef,
291
+ publishSdkSession,
288
292
  });
289
293
 
290
294
  const declarations = useMemo(
@@ -1,5 +1,9 @@
1
- import { describe, expect, it } from "vitest";
2
- import { resolveCompositionPreviewScale, resolveThumbnailSeekTime } from "./CompositionsTab";
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import {
3
+ resolveCompositionPreviewScale,
4
+ resolveThumbnailSeekTime,
5
+ syncIframePlayback,
6
+ } from "./CompositionsTab";
3
7
 
4
8
  describe("resolveCompositionPreviewScale", () => {
5
9
  it("scales a 16:9 stage to fit the composition card", () => {
@@ -50,3 +54,25 @@ describe("resolveThumbnailSeekTime", () => {
50
54
  expect(resolveThumbnailSeekTime(Number.NaN)).toBe(3);
51
55
  });
52
56
  });
57
+
58
+ describe("syncIframePlayback", () => {
59
+ it("mutes a composition-card preview before playing it", () => {
60
+ const calls: string[] = [];
61
+ const postMessage = vi.fn(() => calls.push("mute"));
62
+ const player = {
63
+ play: vi.fn(() => calls.push("play")),
64
+ };
65
+ const iframe = {
66
+ contentWindow: { __player: player, postMessage },
67
+ getRootNode: () => ({}),
68
+ } as unknown as HTMLIFrameElement;
69
+
70
+ expect(syncIframePlayback(iframe, true)).toBe(true);
71
+ expect(postMessage).toHaveBeenCalledWith(
72
+ expect.objectContaining({ action: "set-muted", muted: true }),
73
+ "*",
74
+ );
75
+ expect(calls).toEqual(["mute", "play"]);
76
+ expect(player.play).toHaveBeenCalledOnce();
77
+ });
78
+ });
@@ -1,4 +1,5 @@
1
1
  import { memo, useCallback, useEffect, useRef, useState } from "react";
2
+ import { setPreviewMediaMuted } from "../../player/lib/timelineIframeHelpers";
2
3
 
3
4
  interface CompositionsTabProps {
4
5
  projectId: string;
@@ -87,12 +88,13 @@ function resolveIframeDuration(iframe: HTMLIFrameElement | null): number | null
87
88
  }
88
89
  }
89
90
 
90
- function syncIframePlayback(iframe: HTMLIFrameElement | null, shouldPlay: boolean): boolean {
91
+ export function syncIframePlayback(iframe: HTMLIFrameElement | null, shouldPlay: boolean): boolean {
91
92
  try {
92
93
  const player = (iframe?.contentWindow as PreviewWindow | null)?.__player;
93
94
  if (!player) return false;
94
95
 
95
96
  if (shouldPlay) {
97
+ setPreviewMediaMuted(iframe, true);
96
98
  player.play?.();
97
99
  return true;
98
100
  }
@@ -10,6 +10,13 @@ export interface FramePosterProps {
10
10
  title: string;
11
11
  /** `cover` fills+crops (contact-sheet tile); `contain` letterboxes (focus hero). */
12
12
  fit?: "cover" | "contain";
13
+ /**
14
+ * Project content signature to key the poster URL on. The thumbnail route
15
+ * regenerates when the frame's source changes, but the browser only refetches
16
+ * when the URL does — so bake the signature in to pick up fresh posters after
17
+ * a background board refresh.
18
+ */
19
+ posterVersion?: string;
13
20
  }
14
21
 
15
22
  /**
@@ -18,11 +25,19 @@ export interface FramePosterProps {
18
25
  * no postMessage seek, and no client-side fps assumption. Shared by the
19
26
  * contact-sheet tile and the frame-focus view.
20
27
  */
21
- export function FramePoster({ projectId, src, seconds, title, fit = "cover" }: FramePosterProps) {
28
+ export function FramePoster({
29
+ projectId,
30
+ src,
31
+ seconds,
32
+ title,
33
+ fit = "cover",
34
+ posterVersion,
35
+ }: FramePosterProps) {
22
36
  const [failed, setFailed] = useState(false);
23
37
  // The <img> is reused (no key) when a tile/hero swaps to a different frame, so a
24
- // prior load error would stick. Reset when the poster target changes.
25
- useEffect(() => setFailed(false), [src, seconds]);
38
+ // prior load error would stick. Reset when the poster target changes — including
39
+ // a new posterVersion, so a frame that failed mid-write retries once it settles.
40
+ useEffect(() => setFailed(false), [src, seconds, posterVersion]);
26
41
  if (failed) {
27
42
  return (
28
43
  <div className="flex h-full w-full items-center justify-center text-[11px] text-neutral-600">
@@ -30,12 +45,17 @@ export function FramePoster({ projectId, src, seconds, title, fit = "cover" }: F
30
45
  </div>
31
46
  );
32
47
  }
33
- const url = buildCompositionThumbnailUrl({
48
+ let url = buildCompositionThumbnailUrl({
34
49
  previewUrl: `/api/projects/${projectId}/preview/comp/${src}`,
35
50
  seekTime: seconds,
36
51
  duration: 0,
37
52
  origin: window.location.origin,
38
53
  });
54
+ if (posterVersion) {
55
+ const withVersion = new URL(url, window.location.origin);
56
+ withVersion.searchParams.set("sig", posterVersion);
57
+ url = withVersion.toString();
58
+ }
39
59
  return (
40
60
  <img
41
61
  src={url}
@@ -19,6 +19,8 @@ export interface StoryboardFrameFocusProps {
19
19
  onSaved: () => void;
20
20
  /** Select a composition in the timeline (sets active comp + editing file + sidebar highlight). */
21
21
  onSelectComposition: (path: string) => void;
22
+ /** Project signature the board was loaded with (busts the poster cache). */
23
+ posterVersion?: string;
22
24
  }
23
25
 
24
26
  /**
@@ -39,6 +41,7 @@ export function StoryboardFrameFocus({
39
41
  onNavigate,
40
42
  onSaved,
41
43
  onSelectComposition,
44
+ posterVersion,
42
45
  }: StoryboardFrameFocusProps) {
43
46
  const { readProjectFile, writeProjectFile } = useFileManagerContext();
44
47
  const { setViewMode } = useViewMode();
@@ -151,6 +154,7 @@ export function StoryboardFrameFocus({
151
154
  seconds={posterTime(frame)}
152
155
  title={title}
153
156
  fit="contain"
157
+ posterVersion={posterVersion}
154
158
  />
155
159
  ) : (
156
160
  <div className="flex h-full w-full items-center justify-center text-sm text-neutral-600">
@@ -7,6 +7,13 @@ export interface StoryboardFrameTileProps {
7
7
  frame: StoryboardFrameView;
8
8
  /** Open this frame in the full-area focus view. */
9
9
  onOpen: (index: number) => void;
10
+ /** This frame's pending comment draft ("" when none). */
11
+ commentDraft: string;
12
+ onCommentDraftChange: (index: number, text: string) => void;
13
+ /** A submitted comment the agent has not consumed yet (null when none). */
14
+ pendingComment: string | null;
15
+ /** Project signature the board was loaded with (busts the poster cache). */
16
+ posterVersion?: string;
10
17
  }
11
18
 
12
19
  function firstLine(text: string): string {
@@ -26,7 +33,15 @@ function placeholderMessage(frame: StoryboardFrameView): string {
26
33
 
27
34
  /** A single contact-sheet tile: poster preview + its metadata. Click to focus. */
28
35
  // fallow-ignore-next-line complexity
29
- export function StoryboardFrameTile({ projectId, frame, onOpen }: StoryboardFrameTileProps) {
36
+ export function StoryboardFrameTile({
37
+ projectId,
38
+ frame,
39
+ onOpen,
40
+ commentDraft,
41
+ onCommentDraftChange,
42
+ pendingComment,
43
+ posterVersion,
44
+ }: StoryboardFrameTileProps) {
30
45
  const meta = FRAME_STATUS_META[frame.status];
31
46
  const renderable = frame.srcExists && frame.status !== "outline";
32
47
  const title = frame.title ?? `Frame ${frame.index}`;
@@ -48,6 +63,7 @@ export function StoryboardFrameTile({ projectId, frame, onOpen }: StoryboardFram
48
63
  src={frame.src}
49
64
  seconds={posterTime(frame)}
50
65
  title={title}
66
+ posterVersion={posterVersion}
51
67
  />
52
68
  ) : (
53
69
  <FrameTilePlaceholder frame={frame} />
@@ -74,6 +90,19 @@ export function StoryboardFrameTile({ projectId, frame, onOpen }: StoryboardFram
74
90
  {frame.duration && <span>{frame.duration}</span>}
75
91
  {frame.transitionIn && <span>↘ {frame.transitionIn}</span>}
76
92
  </div>
93
+ <textarea
94
+ value={commentDraft}
95
+ onChange={(e) => onCommentDraftChange(frame.index, e.target.value)}
96
+ rows={2}
97
+ placeholder="Comment on this frame…"
98
+ aria-label={`Comment on ${title}`}
99
+ className="mt-2 w-full resize-none rounded-md border border-neutral-800 bg-neutral-900/60 px-2 py-1.5 text-xs text-neutral-200 placeholder:text-neutral-600 focus:border-sky-700 focus:outline-none"
100
+ />
101
+ {pendingComment && (
102
+ <p className="mt-1 text-[11px] text-sky-400/90">
103
+ <span className="font-medium">Pending:</span> “{pendingComment}”
104
+ </p>
105
+ )}
77
106
  </article>
78
107
  );
79
108
  }
@@ -1,4 +1,5 @@
1
1
  import type { StoryboardFrameView } from "../../hooks/useStoryboard";
2
+ import type { FrameCommentEntry } from "./frameComments";
2
3
  import { StoryboardFrameTile } from "./StoryboardFrameTile";
3
4
 
4
5
  export interface StoryboardGridProps {
@@ -6,10 +7,25 @@ export interface StoryboardGridProps {
6
7
  frames: StoryboardFrameView[];
7
8
  /** Open a frame in the full-area focus view. */
8
9
  onOpenFrame: (index: number) => void;
10
+ /** Per-frame comment drafts, keyed by frame index. */
11
+ commentDrafts: Record<number, string>;
12
+ onCommentDraftChange: (index: number, text: string) => void;
13
+ /** Submitted comments the agent has not consumed yet. */
14
+ pendingComments: FrameCommentEntry[] | null;
15
+ /** Project signature the board was loaded with (busts poster caches). */
16
+ posterVersion?: string;
9
17
  }
10
18
 
11
19
  /** The contact sheet: ordered frame tiles in a responsive grid. */
12
- export function StoryboardGrid({ projectId, frames, onOpenFrame }: StoryboardGridProps) {
20
+ export function StoryboardGrid({
21
+ projectId,
22
+ frames,
23
+ onOpenFrame,
24
+ commentDrafts,
25
+ onCommentDraftChange,
26
+ pendingComments,
27
+ posterVersion,
28
+ }: StoryboardGridProps) {
13
29
  if (frames.length === 0) {
14
30
  return (
15
31
  <div className="mt-8 rounded-lg border border-dashed border-neutral-800 px-6 py-12 text-center text-sm text-neutral-500">
@@ -26,6 +42,12 @@ export function StoryboardGrid({ projectId, frames, onOpenFrame }: StoryboardGri
26
42
  projectId={projectId}
27
43
  frame={frame}
28
44
  onOpen={onOpenFrame}
45
+ commentDraft={commentDrafts[frame.index] ?? ""}
46
+ onCommentDraftChange={onCommentDraftChange}
47
+ pendingComment={
48
+ pendingComments?.find((entry) => entry.frame === frame.index)?.text ?? null
49
+ }
50
+ posterVersion={posterVersion}
29
51
  />
30
52
  ))}
31
53
  </div>
@@ -1,11 +1,13 @@
1
- import { useMemo, useState } from "react";
1
+ import { useEffect, useMemo, useState } from "react";
2
2
  import type { StoryboardResponse } from "../../hooks/useStoryboard";
3
+ import { Button } from "../ui/Button";
3
4
  import { StoryboardDirection } from "./StoryboardDirection";
4
5
  import { StoryboardGrid } from "./StoryboardGrid";
5
6
  import { StoryboardStatusLegend } from "./StoryboardStatusLegend";
6
7
  import { StoryboardScriptPanel } from "./StoryboardScriptPanel";
7
8
  import { StoryboardSourceEditor, type SourceFile } from "./StoryboardSourceEditor";
8
9
  import { StoryboardFrameFocus } from "./StoryboardFrameFocus";
10
+ import { useFrameComments, type CommentsSubmitState } from "./useFrameComments";
9
11
 
10
12
  type SubView = "board" | "source";
11
13
 
@@ -33,6 +35,14 @@ export function StoryboardLoaded({
33
35
  const [subView, setSubView] = useState<SubView>("board");
34
36
  const [sourceDirty, setSourceDirty] = useState(false);
35
37
  const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
38
+ const comments = useFrameComments(data.frames);
39
+ // When the board refreshes off a project change (agent revised frames), the
40
+ // agent has likely consumed the comments file too — re-check so the pending
41
+ // banner clears the moment revisions land, not on the next window focus.
42
+ const { refreshPending } = comments;
43
+ useEffect(() => {
44
+ void refreshPending();
45
+ }, [data.signature, refreshPending]);
36
46
  const sourceFiles = useMemo<SourceFile[]>(() => {
37
47
  const files: SourceFile[] = [{ path: data.path, label: data.path }];
38
48
  if (data.script?.exists) files.push({ path: data.script.path, label: data.script.path });
@@ -72,14 +82,23 @@ export function StoryboardLoaded({
72
82
  }
73
83
  onSaved={reload}
74
84
  onSelectComposition={onSelectComposition}
85
+ posterVersion={data.signature}
75
86
  />
76
87
  );
77
88
  }
78
89
 
79
90
  return (
80
91
  <div className="flex flex-1 min-h-0 flex-col bg-neutral-950 text-neutral-200">
81
- <div className="flex items-center border-b border-neutral-800 px-4 py-2">
92
+ <div className="flex items-center gap-3 border-b border-neutral-800 px-4 py-2">
82
93
  <SubViewToggle value={subView} onChange={changeSubView} />
94
+ {subView === "board" && (
95
+ <CommentsSubmitBar
96
+ draftCount={comments.draftCount}
97
+ pendingCount={comments.pending?.length ?? 0}
98
+ submitState={comments.submitState}
99
+ onSubmit={() => void comments.submit()}
100
+ />
101
+ )}
83
102
  </div>
84
103
  {subView === "board" ? (
85
104
  <div className="flex-1 min-h-0 overflow-auto">
@@ -92,6 +111,10 @@ export function StoryboardLoaded({
92
111
  projectId={projectId}
93
112
  frames={data.frames}
94
113
  onOpenFrame={setFocusedIndex}
114
+ commentDrafts={comments.drafts}
115
+ onCommentDraftChange={comments.setDraft}
116
+ pendingComments={comments.pending}
117
+ posterVersion={data.signature}
95
118
  />
96
119
  {data.script && <StoryboardScriptPanel script={data.script} />}
97
120
  </div>
@@ -107,6 +130,39 @@ export function StoryboardLoaded({
107
130
  );
108
131
  }
109
132
 
133
+ /** Batch-submit the per-frame comment drafts to `.hyperframes/frame-comments.json`. */
134
+ function CommentsSubmitBar({
135
+ draftCount,
136
+ pendingCount,
137
+ submitState,
138
+ onSubmit,
139
+ }: {
140
+ draftCount: number;
141
+ pendingCount: number;
142
+ submitState: CommentsSubmitState;
143
+ onSubmit: () => void;
144
+ }) {
145
+ return (
146
+ <div className="ml-auto flex items-center gap-3">
147
+ {pendingCount > 0 && (
148
+ <span className="text-xs text-sky-300">
149
+ {pendingCount} comment{pendingCount > 1 ? "s" : ""} pending — reply anything in your agent
150
+ chat and it will apply them.
151
+ </span>
152
+ )}
153
+ <Button
154
+ variant="primary"
155
+ size="sm"
156
+ loading={submitState === "saving"}
157
+ disabled={draftCount === 0 || submitState === "saving"}
158
+ onClick={onSubmit}
159
+ >
160
+ {draftCount > 0 ? `Submit comments (${draftCount})` : "Submit comments"}
161
+ </Button>
162
+ </div>
163
+ );
164
+ }
165
+
110
166
  const SUB_VIEWS: Array<{ value: SubView; label: string }> = [
111
167
  { value: "board", label: "Board" },
112
168
  { value: "source", label: "Source" },
@@ -1,6 +1,7 @@
1
1
  import { useState, type ReactNode } from "react";
2
2
  import { Copy, Check } from "@phosphor-icons/react";
3
3
  import { useStoryboard } from "../../hooks/useStoryboard";
4
+ import { useProjectSignaturePoll } from "../../hooks/useProjectSignaturePoll";
4
5
  import { copyTextToClipboard } from "../../utils/clipboard";
5
6
  import { Button } from "../ui/Button";
6
7
  import { StoryboardLoaded } from "./StoryboardLoaded";
@@ -19,6 +20,10 @@ export interface StoryboardViewProps {
19
20
  // fallow-ignore-next-line complexity
20
21
  export function StoryboardView({ projectId, onSelectComposition }: StoryboardViewProps) {
21
22
  const { data, loading, error, reload } = useStoryboard(projectId);
23
+ // Keep the board current while an agent writes to the project: when the
24
+ // project signature moves past the one `data` was loaded with, refetch. Also
25
+ // upgrades the empty state the moment STORYBOARD.md lands on disk.
26
+ useProjectSignaturePoll(projectId, data?.signature, reload);
22
27
 
23
28
  if (loading) return <StoryboardFrame>{<Message>Loading storyboard…</Message>}</StoryboardFrame>;
24
29
  if (error) {
@@ -87,12 +92,14 @@ audience: <who it's for>
87
92
  ## Frame 1 — <title>
88
93
  - duration: 5s
89
94
  - transition_in: crossfade
90
- - status: planned
95
+ - status: outline
91
96
  - src: compositions/frames/01-<slug>.html
92
97
 
93
98
  <A sentence or two: what's on screen and what the narration says.>
94
99
 
95
- Add one \`## Frame N\` section per beat. Keep the arc tight.`;
100
+ Add one \`## Frame N\` section per beat. Keep the arc tight.
101
+
102
+ Then run the review loop from the hyperframes-core skill (references/review-loop.md): present the plan as a proposal, offer wireframe sketches on this board, and build on the confirmed layouts.`;
96
103
  }
97
104
 
98
105
  function EmptyState({ path }: { path: string }) {