@hyperframes/studio 0.8.49 → 0.8.51

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 (281) hide show
  1. package/dist/assets/{hyperframes-player-CqON2Rzo.js → hyperframes-player-Cn_PpHqP.js} +3 -3
  2. package/dist/assets/index-C05SNrvY.js +360 -0
  3. package/dist/assets/{index-mIgSsKqs.js → index-CPS3Cvj8.js} +1 -1
  4. package/dist/assets/{index-BGovDksE.js → index-Clrb11lj.js} +1 -1
  5. package/dist/assets/index-DUR9qzQM.css +1 -0
  6. package/dist/index.d.ts +29 -16
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +7283 -8606
  9. package/dist/index.js.map +1 -1
  10. package/dist/styles/tailwind-preset.js.map +1 -1
  11. package/package.json +11 -12
  12. package/src/App.tsx +132 -143
  13. package/src/captions/components/CaptionAnimationPanel.tsx +4 -4
  14. package/src/captions/components/CaptionOverlayUtils.ts +2 -3
  15. package/src/captions/components/CaptionPropertyPanel.tsx +3 -3
  16. package/src/captions/components/CaptionTimeline.tsx +1 -1
  17. package/src/captions/components/shared.tsx +2 -2
  18. package/src/components/AskAgentModal.tsx +5 -5
  19. package/src/components/EditorShell.selectionSync.test.tsx +4 -0
  20. package/src/components/EditorShell.tsx +28 -7
  21. package/src/components/ExternalFileConflictBanner.tsx +16 -16
  22. package/src/components/LintModal.tsx +6 -16
  23. package/src/components/MediaPreview.tsx +2 -2
  24. package/src/components/SaveQueuePausedBanner.tsx +2 -2
  25. package/src/components/StudioHeader.tsx +2 -62
  26. package/src/components/StudioLeftSidebar.tsx +7 -4
  27. package/src/components/StudioOverlays.tsx +2 -6
  28. package/src/components/StudioRightPanel.tsx +4 -4
  29. package/src/components/StudioToast.tsx +2 -2
  30. package/src/components/TimelineToolbar.tsx +7 -7
  31. package/src/components/dock/Dock.test.tsx +192 -0
  32. package/src/components/dock/Dock.tsx +264 -0
  33. package/src/components/dock/dock.css +23 -0
  34. package/src/components/dock/dockLayout.ts +58 -0
  35. package/src/components/dock/dockLayoutSchema.ts +66 -0
  36. package/src/components/dock/dockLayoutStore.ts +91 -0
  37. package/src/components/dock/panelRegistry.ts +67 -0
  38. package/src/components/editor/AnimationCard.tsx +4 -4
  39. package/src/components/editor/AnimationCardParts.tsx +6 -6
  40. package/src/components/editor/BlockParamsPanel.tsx +4 -4
  41. package/src/components/editor/BorderRadiusEditor.tsx +1 -1
  42. package/src/components/editor/CanvasContextMenu.tsx +1 -1
  43. package/src/components/editor/ComputedTweenNotice.tsx +1 -1
  44. package/src/components/editor/DomEditOverlay.tsx +1 -1
  45. package/src/components/editor/EaseCurveSection.tsx +1 -1
  46. package/src/components/editor/EaseParamFields.tsx +5 -5
  47. package/src/components/editor/FileTree.tsx +4 -4
  48. package/src/components/editor/FileTreeIcons.tsx +1 -1
  49. package/src/components/editor/FileTreeNodes.tsx +13 -13
  50. package/src/components/editor/InlineTextToolbar.tsx +1 -1
  51. package/src/components/editor/InspectorHeaderActions.tsx +3 -3
  52. package/src/components/editor/KeyframeDiamond.tsx +1 -1
  53. package/src/components/editor/KeyframeEaseList.tsx +1 -1
  54. package/src/components/editor/LayersPanel.tsx +3 -3
  55. package/src/components/editor/MarqueeOverlay.tsx +1 -1
  56. package/src/components/editor/OffCanvasIndicators.tsx +1 -1
  57. package/src/components/editor/PreviewGuides.test.tsx +88 -0
  58. package/src/components/editor/PreviewGuides.tsx +125 -0
  59. package/src/components/editor/PromotableControl.tsx +2 -2
  60. package/src/components/editor/PropertyPanelEmptyState.tsx +7 -7
  61. package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
  62. package/src/components/editor/SnapToolbar.test.tsx +21 -0
  63. package/src/components/editor/SnapToolbar.tsx +27 -3
  64. package/src/components/editor/TimelineFxPopover.tsx +1 -1
  65. package/src/components/editor/TopologyLens.tsx +4 -4
  66. package/src/components/editor/Transform3DCube.tsx +2 -2
  67. package/src/components/editor/audioFxRevealTarget.ts +1 -0
  68. package/src/components/editor/domEditing.test.ts +1 -1
  69. package/src/components/editor/domEditingAgentPrompt.test.ts +8 -6
  70. package/src/components/editor/domEditingAgentPrompt.ts +9 -3
  71. package/src/components/editor/previewGuidesStore.ts +26 -0
  72. package/src/components/editor/propertyPanelColor.tsx +11 -11
  73. package/src/components/editor/propertyPanelColorCurveGraph.tsx +1 -1
  74. package/src/components/editor/propertyPanelColorGradingControls.tsx +7 -9
  75. package/src/components/editor/propertyPanelColorGradingSection.tsx +6 -6
  76. package/src/components/editor/propertyPanelColorGradingSlider.tsx +5 -5
  77. package/src/components/editor/propertyPanelColorWheels.tsx +1 -1
  78. package/src/components/editor/propertyPanelCommitField.tsx +1 -1
  79. package/src/components/editor/propertyPanelFill.tsx +4 -4
  80. package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
  81. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +7 -7
  82. package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
  83. package/src/components/editor/propertyPanelFlatMediaSection.tsx +3 -3
  84. package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +1 -1
  85. package/src/components/editor/propertyPanelFlatPrimitives.tsx +10 -10
  86. package/src/components/editor/propertyPanelFlatSelectRow.tsx +3 -3
  87. package/src/components/editor/propertyPanelFlatTextSection.test.tsx +8 -9
  88. package/src/components/editor/propertyPanelFlatTextSection.tsx +5 -5
  89. package/src/components/editor/propertyPanelFlatToggle.tsx +1 -1
  90. package/src/components/editor/propertyPanelFont.tsx +5 -5
  91. package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
  92. package/src/components/editor/propertyPanelFxControls.test.tsx +1 -1
  93. package/src/components/editor/propertyPanelFxControls.tsx +6 -6
  94. package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
  95. package/src/components/editor/propertyPanelFxPresetStyle.ts +2 -2
  96. package/src/components/editor/propertyPanelGradingNumberField.tsx +1 -1
  97. package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
  98. package/src/components/editor/propertyPanelPrimitives.tsx +7 -7
  99. package/src/components/editor/propertyPanelSections.tsx +8 -8
  100. package/src/components/feedback/StudioFeedbackCard.tsx +7 -7
  101. package/src/components/feedback/feedbackTrigger.ts +0 -1
  102. package/src/components/nle/AssetPreviewOverlay.tsx +6 -2
  103. package/src/components/nle/CompositionBreadcrumb.tsx +3 -3
  104. package/src/components/nle/NLEPreview.test.ts +13 -0
  105. package/src/components/nle/NLEPreview.tsx +12 -7
  106. package/src/components/nle/PreviewOverlays.tsx +6 -6
  107. package/src/components/nle/PreviewPane.test.tsx +52 -0
  108. package/src/components/nle/PreviewPane.tsx +4 -2
  109. package/src/components/nle/TimelinePane.tsx +17 -3
  110. package/src/components/nle/TimelineResizeDivider.tsx +2 -2
  111. package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +1 -1
  112. package/src/components/nle/useCanvasZOrderTimelineMirror.ts +3 -3
  113. package/src/components/panels/SlideshowPanel.tsx +2 -2
  114. package/src/components/panels/SlideshowSubPanels.tsx +20 -17
  115. package/src/components/panels/VariablesBindElement.tsx +2 -2
  116. package/src/components/panels/VariablesDeclarationForm.tsx +2 -2
  117. package/src/components/panels/VariablesOtherCompositions.tsx +1 -1
  118. package/src/components/panels/VariablesPanel.tsx +3 -3
  119. package/src/components/panels/VariablesValueControls.tsx +2 -2
  120. package/src/components/renders/FfmpegRequiredNotice.tsx +5 -5
  121. package/src/components/renders/RenderQueue.test.tsx +7 -0
  122. package/src/components/renders/RenderQueue.tsx +6 -6
  123. package/src/components/renders/RenderQueueItem.tsx +6 -6
  124. package/src/components/sidebar/AssetCard.tsx +8 -8
  125. package/src/components/sidebar/AssetContextMenu.tsx +8 -8
  126. package/src/components/sidebar/AssetsTab.tsx +2 -2
  127. package/src/components/sidebar/AudioRow.tsx +6 -6
  128. package/src/components/sidebar/BlocksTab.tsx +6 -6
  129. package/src/components/sidebar/CompositionsTab.tsx +5 -5
  130. package/src/components/sidebar/GlobalAssetsView.tsx +1 -1
  131. package/src/components/sidebar/LeftSidebar.lintBadge.test.tsx +58 -0
  132. package/src/components/sidebar/LeftSidebar.tsx +19 -5
  133. package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
  134. package/src/components/ui/Button.test.tsx +60 -0
  135. package/src/components/ui/Button.tsx +4 -4
  136. package/src/components/ui/SearchInput.tsx +2 -2
  137. package/src/components/ui/Tooltip.tsx +1 -1
  138. package/src/hooks/appHotkeysDispatch.ts +11 -0
  139. package/src/hooks/timelineAudioGroupCreate.ts +2 -2
  140. package/src/hooks/timelineTrackVisibility.ts +8 -11
  141. package/src/hooks/useAppHotkeys.textEditing.test.tsx +1 -0
  142. package/src/hooks/useAppHotkeys.ts +4 -1
  143. package/src/hooks/useAskAgentModal.ts +2 -0
  144. package/src/hooks/useClipboard.test.ts +234 -0
  145. package/src/hooks/useClipboard.ts +264 -70
  146. package/src/hooks/useGroupCommits.test.ts +0 -1
  147. package/src/hooks/useLintModal.test.tsx +65 -0
  148. package/src/hooks/useLintModal.ts +3 -0
  149. package/src/hooks/useRazorSplit.history.test.tsx +0 -1
  150. package/src/hooks/useRazorSplit.test.ts +0 -2
  151. package/src/hooks/useRazorSplit.test.tsx +0 -1
  152. package/src/hooks/useSdkSession.lifecycle.test.tsx +49 -4
  153. package/src/hooks/useSdkSession.ts +43 -10
  154. package/src/hooks/useStudioContextValue.ts +5 -19
  155. package/src/hooks/useTimelineAssetDropOps.test.tsx +41 -1
  156. package/src/hooks/useTimelineAssetDropOps.ts +52 -11
  157. package/src/hooks/useTimelineEditing.test.tsx +0 -1
  158. package/src/main.tsx +9 -3
  159. package/src/player/components/AudioWaveform.tsx +1 -1
  160. package/src/player/components/AutomationSelectionMenu.tsx +2 -2
  161. package/src/player/components/ClipContextMenu.tsx +90 -10
  162. package/src/player/components/CompositionThumbnail.test.ts +0 -3
  163. package/src/player/components/CompositionThumbnail.tsx +3 -4
  164. package/src/player/components/EditModal.tsx +1 -1
  165. package/src/player/components/ImageThumbnail.tsx +1 -1
  166. package/src/player/components/KeyframeDiamondContextMenu.tsx +4 -4
  167. package/src/player/components/LayerDisclosureRow.tsx +2 -2
  168. package/src/player/components/PlayerControls.tsx +1 -1
  169. package/src/player/components/ShortcutsPanel.tsx +9 -9
  170. package/src/player/components/SpeedMenu.tsx +3 -5
  171. package/src/player/components/Timeline.tsx +16 -15
  172. package/src/player/components/TimelineCanvas.tsx +45 -8
  173. package/src/player/components/TimelineDiamondConnectors.tsx +1 -1
  174. package/src/player/components/TimelineEmptyState.tsx +5 -5
  175. package/src/player/components/TimelineFxButton.tsx +6 -6
  176. package/src/player/components/TimelineGestureOverlay.test.tsx +35 -1
  177. package/src/player/components/TimelineGestureOverlay.tsx +1 -1
  178. package/src/player/components/TimelineGroupHeader.tsx +5 -5
  179. package/src/player/components/TimelineGroupLaneLabels.tsx +1 -1
  180. package/src/player/components/TimelineGroupRow.tsx +1 -1
  181. package/src/player/components/TimelineLanes.test.tsx +54 -1
  182. package/src/player/components/TimelineLanes.tsx +2 -5
  183. package/src/player/components/TimelineLayerGroupHeader.tsx +2 -2
  184. package/src/player/components/TimelineOverlays.tsx +12 -0
  185. package/src/player/components/TimelineRuler.tsx +1 -1
  186. package/src/player/components/TimelineShortcutHint.tsx +1 -1
  187. package/src/player/components/TimelineTrackHeader.tsx +1 -1
  188. package/src/player/components/TimelineTrackPlainHeader.tsx +3 -3
  189. package/src/player/components/TimelineTypes.ts +6 -1
  190. package/src/player/components/TrackGapContextMenu.tsx +1 -1
  191. package/src/player/components/VideoThumbnail.tsx +4 -2
  192. package/src/player/components/VolumeControl.tsx +2 -2
  193. package/src/player/components/timelineAuthoredMoveTarget.ts +1 -2
  194. package/src/player/components/timelineCallbacks.ts +8 -8
  195. package/src/player/components/timelineClipChildren.test.ts +2 -2
  196. package/src/player/components/timelineClipChildren.tsx +2 -1
  197. package/src/player/components/timelineClipDragCommit.ts +2 -2
  198. package/src/player/components/timelineClipDragPreview.ts +51 -27
  199. package/src/player/components/timelineClipDragTypes.ts +3 -0
  200. package/src/player/components/timelineDragDrop.test.tsx +92 -1
  201. package/src/player/components/timelineDragDrop.ts +82 -33
  202. package/src/player/components/timelineEditCapabilities.ts +1 -2
  203. package/src/player/components/timelineEditing.test.ts +0 -16
  204. package/src/player/components/timelineGroupEditing.test.ts +1 -3
  205. package/src/player/components/timelineGroupEditing.ts +0 -1
  206. package/src/player/components/timelineLaneProps.ts +3 -0
  207. package/src/player/components/timelineSnapping.test.ts +27 -0
  208. package/src/player/components/timelineSnapping.ts +14 -1
  209. package/src/player/components/timelineTrackDisplay.ts +1 -1
  210. package/src/player/components/timelineTrackPersistPipeline.test.ts +0 -53
  211. package/src/player/components/trackHeaderLabelRows.tsx +4 -4
  212. package/src/player/components/useTimelineClipContextMenu.ts +32 -0
  213. package/src/player/components/useTimelineClipDrag.resize.test.tsx +136 -2
  214. package/src/player/components/useTimelineClipDrag.ts +30 -3
  215. package/src/player/components/useTimelineTrackDerivations.ts +1 -1
  216. package/src/player/hooks/timelineDomBinding.test.ts +0 -3
  217. package/src/player/hooks/timelineSyncHydration.test.ts +18 -0
  218. package/src/player/hooks/timelineSyncHydration.ts +37 -18
  219. package/src/player/hooks/useTimelinePlayer.test.ts +3 -20
  220. package/src/player/hooks/useTimelineRowElements.test.ts +28 -0
  221. package/src/player/hooks/useTimelineRowElements.ts +17 -0
  222. package/src/player/hooks/useTimelineSyncCallbacks.emptyManifest.test.tsx +41 -0
  223. package/src/player/hooks/useTimelineSyncCallbacks.ts +9 -10
  224. package/src/player/lib/describeClips.test.ts +95 -0
  225. package/src/player/lib/describeClips.ts +52 -0
  226. package/src/player/lib/timelineDOM.test.ts +0 -80
  227. package/src/player/lib/timelineDOM.ts +1 -61
  228. package/src/player/lib/timelineElementHelpers.ts +2 -53
  229. package/src/player/store/playerStore.ts +5 -0
  230. package/src/player/store/timelineElement.ts +1 -3
  231. package/src/shims/canvasBrowserStub.js +50 -0
  232. package/src/styles/iconTokens.ts +20 -0
  233. package/src/styles/palette-reset.css +6 -0
  234. package/src/styles/studio.css +24 -21
  235. package/src/styles/tailwind-preset.shared.js +3 -0
  236. package/src/styles/theme.css +213 -0
  237. package/src/styles/theme.test.ts +196 -0
  238. package/src/utils/clipboardPayload.test.ts +42 -2
  239. package/src/utils/clipboardPayload.ts +57 -32
  240. package/src/utils/previewSafeMargins.test.ts +34 -0
  241. package/src/utils/previewSafeMargins.ts +10 -0
  242. package/src/utils/razorSplitTransaction.test.ts +0 -1
  243. package/src/utils/sdkCanvasComposition.test.ts +77 -0
  244. package/src/utils/studioUiPreferences.test.ts +43 -0
  245. package/src/utils/studioUiPreferences.ts +34 -6
  246. package/src/utils/timelineDropTrackInsert.test.ts +134 -0
  247. package/src/utils/timelineDropTrackInsert.ts +83 -0
  248. package/src/utils/timelineElementSplit.test.ts +1 -2
  249. package/src/utils/timelineElementSplit.ts +0 -1
  250. package/src/webmcp/tools/lookTools.test.ts +40 -0
  251. package/src/webmcp/tools/lookTools.ts +32 -1
  252. package/dist/assets/index-B9LfWNyM.css +0 -1
  253. package/dist/assets/index-Bgmx2h7W.js +0 -441
  254. package/src/components/StudioGlobalDragOverlay.tsx +0 -24
  255. package/src/components/storyboard/AgentChatMessageButton.test.tsx +0 -47
  256. package/src/components/storyboard/AgentChatMessageButton.tsx +0 -45
  257. package/src/components/storyboard/FramePoster.test.tsx +0 -61
  258. package/src/components/storyboard/FramePoster.tsx +0 -86
  259. package/src/components/storyboard/StoryboardDirection.tsx +0 -45
  260. package/src/components/storyboard/StoryboardFrameFocus.tsx +0 -412
  261. package/src/components/storyboard/StoryboardFrameTile.tsx +0 -117
  262. package/src/components/storyboard/StoryboardGrid.tsx +0 -55
  263. package/src/components/storyboard/StoryboardLoaded.tsx +0 -307
  264. package/src/components/storyboard/StoryboardReviewGuide.tsx +0 -300
  265. package/src/components/storyboard/StoryboardScriptPanel.tsx +0 -26
  266. package/src/components/storyboard/StoryboardSourceEditor.tsx +0 -231
  267. package/src/components/storyboard/StoryboardStatusLegend.tsx +0 -21
  268. package/src/components/storyboard/StoryboardView.tsx +0 -169
  269. package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +0 -187
  270. package/src/components/storyboard/frameComments.test.ts +0 -95
  271. package/src/components/storyboard/frameComments.ts +0 -115
  272. package/src/components/storyboard/frameStatus.ts +0 -36
  273. package/src/components/storyboard/storyboardReviewStage.test.ts +0 -40
  274. package/src/components/storyboard/storyboardReviewStage.ts +0 -45
  275. package/src/components/storyboard/useFrameComments.ts +0 -98
  276. package/src/contexts/ViewModeContext.tsx +0 -142
  277. package/src/hooks/useProjectSignaturePoll.test.tsx +0 -135
  278. package/src/hooks/useProjectSignaturePoll.ts +0 -68
  279. package/src/hooks/useStoryboard.ts +0 -99
  280. package/src/player/hooks/useExpandedTimelineElements.test.ts +0 -774
  281. package/src/player/hooks/useExpandedTimelineElements.ts +0 -460
@@ -1,95 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { buildCommentsFile, draftEntries, parseCommentsFile, passForFrames } from "./frameComments";
3
-
4
- const frames = [
5
- { index: 1, status: "built" as const, src: "compositions/frames/01-hook.html", title: "Hook" },
6
- {
7
- index: 2,
8
- status: "built" as const,
9
- src: "compositions/frames/02-thesis.html",
10
- title: "Thesis",
11
- },
12
- { index: 3, status: "built" as const, src: "compositions/frames/03-proof.html", title: "Proof" },
13
- ];
14
-
15
- describe("passForFrames", () => {
16
- it("maps the furthest status to the review pass", () => {
17
- expect(passForFrames([{ status: "outline" }, { status: "outline" }])).toBe("storyboard");
18
- expect(passForFrames([{ status: "outline" }, { status: "built" }])).toBe("sketch");
19
- expect(passForFrames([{ status: "built" }, { status: "animated" }])).toBe("final");
20
- });
21
- });
22
-
23
- describe("draftEntries", () => {
24
- it("keeps only non-empty drafts, in board order, with src/title echoed", () => {
25
- const entries = draftEntries(frames, { 3: "swap the chart", 1: " ", 2: "tighter kerning" });
26
- expect(entries).toEqual([
27
- { frame: 2, src: frames[1]?.src, title: "Thesis", text: "tighter kerning" },
28
- { frame: 3, src: frames[2]?.src, title: "Proof", text: "swap the chart" },
29
- ]);
30
- });
31
- });
32
-
33
- describe("parseCommentsFile", () => {
34
- it("reads a well-formed file", () => {
35
- const parsed = parseCommentsFile(
36
- JSON.stringify({
37
- version: 1,
38
- pass: "sketch",
39
- submitted_at: "2026-07-10T00:00:00Z",
40
- comments: [{ frame: 2, text: "hi" }],
41
- }),
42
- );
43
- expect(parsed?.pass).toBe("sketch");
44
- expect(parsed?.comments).toEqual([{ frame: 2, src: undefined, title: undefined, text: "hi" }]);
45
- });
46
-
47
- it("treats empty or malformed input as no file", () => {
48
- expect(parseCommentsFile("")).toBeNull();
49
- expect(parseCommentsFile("not json")).toBeNull();
50
- expect(parseCommentsFile(JSON.stringify({ comments: "nope" }))).toBeNull();
51
- });
52
-
53
- it("drops malformed entries but keeps the valid ones", () => {
54
- const parsed = parseCommentsFile(
55
- JSON.stringify({ comments: [{ frame: 1, text: "ok" }, { frame: "x" }, null] }),
56
- );
57
- expect(parsed?.comments).toEqual([{ frame: 1, src: undefined, title: undefined, text: "ok" }]);
58
- });
59
- });
60
-
61
- describe("buildCommentsFile", () => {
62
- it("merges an unconsumed previous batch: new frames win, others stay", () => {
63
- const previous = parseCommentsFile(
64
- JSON.stringify({
65
- version: 1,
66
- pass: "sketch",
67
- submitted_at: "2026-07-10T00:00:00Z",
68
- comments: [
69
- { frame: 1, text: "old note on hook" },
70
- { frame: 3, text: "old note on proof" },
71
- ],
72
- }),
73
- );
74
- const file = buildCommentsFile(
75
- frames,
76
- { 3: "new note on proof" },
77
- previous,
78
- "2026-07-10T01:00:00Z",
79
- );
80
- expect(file.pass).toBe("sketch");
81
- expect(file.submitted_at).toBe("2026-07-10T01:00:00Z");
82
- expect(file.comments.map((c) => [c.frame, c.text])).toEqual([
83
- [1, "old note on hook"],
84
- [3, "new note on proof"],
85
- ]);
86
- });
87
-
88
- it("writes a fresh batch when no previous file exists", () => {
89
- const file = buildCommentsFile(frames, { 2: "tighter" }, null, "2026-07-10T01:00:00Z");
90
- expect(file.version).toBe(1);
91
- expect(file.comments).toEqual([
92
- { frame: 2, src: frames[1]?.src, title: "Thesis", text: "tighter" },
93
- ]);
94
- });
95
- });
@@ -1,115 +0,0 @@
1
- import type { FrameStatus } from "@hyperframes/core/storyboard";
2
-
3
- /**
4
- * Frame comments — the storyboard review's structured feedback channel.
5
- *
6
- * The board's per-frame comment boxes write one batch file on submit; the
7
- * consuming agent revises exactly the frames named, deletes the file, and
8
- * re-presents. The file shape is a cross-repo contract — keep it in sync with
9
- * skills/hyperframes/references/storyboard-format.md § Frame comments.
10
- */
11
- export const FRAME_COMMENTS_PATH = ".hyperframes/frame-comments.json";
12
-
13
- export type FrameCommentsPass = "storyboard" | "sketch" | "final";
14
-
15
- export interface FrameCommentEntry {
16
- /** The frame's 1-based `index` in the manifest — the key. */
17
- frame: number;
18
- /** Copied from the frame at submit time so a post-submit reorder is detectable. */
19
- src?: string;
20
- title?: string;
21
- text: string;
22
- }
23
-
24
- export interface FrameCommentsFile {
25
- version: 1;
26
- pass: FrameCommentsPass;
27
- submitted_at: string;
28
- comments: FrameCommentEntry[];
29
- }
30
-
31
- interface CommentableFrame {
32
- index: number;
33
- status: FrameStatus;
34
- src?: string;
35
- title?: string;
36
- }
37
-
38
- /** Which review the batch belongs to — the furthest status present on the board. */
39
- export function passForFrames(frames: ReadonlyArray<{ status: FrameStatus }>): FrameCommentsPass {
40
- if (frames.some((f) => f.status === "animated")) return "final";
41
- if (frames.some((f) => f.status === "built")) return "sketch";
42
- return "storyboard";
43
- }
44
-
45
- /** The non-empty drafts as file entries, in board order. */
46
- export function draftEntries(
47
- frames: readonly CommentableFrame[],
48
- drafts: Readonly<Record<number, string>>,
49
- ): FrameCommentEntry[] {
50
- const entries: FrameCommentEntry[] = [];
51
- for (const frame of frames) {
52
- const text = drafts[frame.index]?.trim();
53
- if (!text) continue;
54
- entries.push({ frame: frame.index, src: frame.src, title: frame.title, text });
55
- }
56
- return entries;
57
- }
58
-
59
- function isRecord(value: unknown): value is Record<string, unknown> {
60
- return typeof value === "object" && value !== null;
61
- }
62
-
63
- function parseCommentEntry(item: unknown): FrameCommentEntry | null {
64
- if (!isRecord(item)) return null;
65
- if (typeof item.frame !== "number" || typeof item.text !== "string") return null;
66
- return {
67
- frame: item.frame,
68
- src: typeof item.src === "string" ? item.src : undefined,
69
- title: typeof item.title === "string" ? item.title : undefined,
70
- text: item.text,
71
- };
72
- }
73
-
74
- function parsePass(value: unknown): FrameCommentsPass {
75
- return value === "sketch" || value === "final" ? value : "storyboard";
76
- }
77
-
78
- /** Tolerant read of an existing comments file — anything malformed counts as "no file". */
79
- export function parseCommentsFile(raw: string): FrameCommentsFile | null {
80
- if (!raw.trim()) return null;
81
- let data: unknown;
82
- try {
83
- data = JSON.parse(raw);
84
- } catch {
85
- return null;
86
- }
87
- if (!isRecord(data) || !Array.isArray(data.comments)) return null;
88
- const comments = data.comments
89
- .map(parseCommentEntry)
90
- .filter((entry): entry is FrameCommentEntry => entry !== null);
91
- return {
92
- version: 1,
93
- pass: parsePass(data.pass),
94
- submitted_at: typeof data.submitted_at === "string" ? data.submitted_at : "",
95
- comments,
96
- };
97
- }
98
-
99
- /**
100
- * One file = the currently pending feedback. A resubmit before the agent
101
- * consumed the previous batch keeps the pending entries for frames the new
102
- * batch doesn't mention and overwrites the ones it does.
103
- */
104
- export function buildCommentsFile(
105
- frames: readonly CommentableFrame[],
106
- drafts: Readonly<Record<number, string>>,
107
- previous: FrameCommentsFile | null,
108
- submittedAt: string,
109
- ): FrameCommentsFile {
110
- const fresh = draftEntries(frames, drafts);
111
- const freshIndexes = new Set(fresh.map((entry) => entry.frame));
112
- const kept = previous?.comments.filter((entry) => !freshIndexes.has(entry.frame)) ?? [];
113
- const comments = [...kept, ...fresh].sort((a, b) => a.frame - b.frame);
114
- return { version: 1, pass: passForFrames(frames), submitted_at: submittedAt, comments };
115
- }
@@ -1,36 +0,0 @@
1
- import type { FrameStatus } from "@hyperframes/core/storyboard";
2
-
3
- /**
4
- * Single source of truth for how each frame lifecycle status is presented —
5
- * label, tooltip, description, and the chip/dot color classes — so the tile
6
- * chip and the legend dot can't drift apart.
7
- */
8
- export const FRAME_STATUS_META: Record<
9
- FrameStatus,
10
- { label: string; tooltip: string; description: string; chipClass: string; dotClass: string }
11
- > = {
12
- outline: {
13
- label: "Outline",
14
- tooltip: "Planned in text — no HTML frame built yet.",
15
- description: "Planned in text. Story and intent exist; the visual isn’t built.",
16
- chipClass: "bg-neutral-800 text-neutral-300",
17
- dotClass: "bg-neutral-500",
18
- },
19
- built: {
20
- label: "Built",
21
- tooltip: "Static HTML frame built — not animated yet.",
22
- description: "The HTML frame exists as a static key moment. Look is locked; motion isn’t.",
23
- chipClass: "bg-sky-500/20 text-sky-300",
24
- dotClass: "bg-sky-400",
25
- },
26
- animated: {
27
- label: "Animated",
28
- tooltip: "Keyframed and animated — plays in the final cut.",
29
- description: "Keyframed into motion. This is the file stitched into the final video.",
30
- chipClass: "bg-emerald-500/20 text-emerald-300",
31
- dotClass: "bg-emerald-400",
32
- },
33
- };
34
-
35
- /** The lifecycle order an agent advances each frame through. */
36
- export const FRAME_STATUS_ORDER: FrameStatus[] = ["outline", "built", "animated"];
@@ -1,40 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { deriveStoryboardHandoffStep, deriveStoryboardReviewStage } from "./storyboardReviewStage";
3
-
4
- describe("deriveStoryboardReviewStage", () => {
5
- it.each([
6
- [[], "empty"],
7
- [["outline", "outline"], "plan-review"],
8
- [["outline", "built"], "sketch-in-progress"],
9
- [["outline", "animated"], "sketch-in-progress"],
10
- [["built", "built"], "sketch-review"],
11
- [["built", "animated"], "animation-in-progress"],
12
- [["animated", "animated"], "final-review"],
13
- ] as const)("maps %j to %s", (statuses, expected) => {
14
- expect(deriveStoryboardReviewStage(statuses.map((status) => ({ status }))).stage).toBe(
15
- expected,
16
- );
17
- });
18
-
19
- it("returns counts for the visible progress summary", () => {
20
- expect(
21
- deriveStoryboardReviewStage([
22
- { status: "outline" },
23
- { status: "built" },
24
- { status: "animated" },
25
- { status: "animated" },
26
- ]).counts,
27
- ).toEqual({ outline: 1, built: 1, animated: 2 });
28
- });
29
- });
30
-
31
- describe("storyboard review handoff", () => {
32
- it.each([
33
- [0, 0, 1],
34
- [2, 0, 2],
35
- [0, 3, 3],
36
- [1, 3, 2],
37
- ] as const)("maps %i drafts and %i pending comments to step %i", (drafts, pending, step) => {
38
- expect(deriveStoryboardHandoffStep(drafts, pending)).toBe(step);
39
- });
40
- });
@@ -1,45 +0,0 @@
1
- import type { FrameStatus } from "@hyperframes/core/storyboard";
2
-
3
- export type StoryboardReviewStage =
4
- | "empty"
5
- | "plan-review"
6
- | "sketch-in-progress"
7
- | "sketch-review"
8
- | "animation-in-progress"
9
- | "final-review";
10
-
11
- export interface StoryboardReviewSummary {
12
- stage: StoryboardReviewStage;
13
- counts: Record<FrameStatus, number>;
14
- frameCount: number;
15
- }
16
-
17
- export type StoryboardHandoffStep = 1 | 2 | 3;
18
-
19
- /** Current user action in the Studio → agent feedback handoff. */
20
- export function deriveStoryboardHandoffStep(
21
- draftCount: number,
22
- pendingCount: number,
23
- ): StoryboardHandoffStep {
24
- if (draftCount > 0) return 2;
25
- if (pendingCount > 0) return 3;
26
- return 1;
27
- }
28
-
29
- /** Derive the board-level review moment from agent-owned frame statuses. */
30
- export function deriveStoryboardReviewStage(
31
- frames: ReadonlyArray<{ status: FrameStatus }>,
32
- ): StoryboardReviewSummary {
33
- const counts: Record<FrameStatus, number> = { outline: 0, built: 0, animated: 0 };
34
- for (const frame of frames) counts[frame.status] += 1;
35
-
36
- let stage: StoryboardReviewStage;
37
- if (frames.length === 0) stage = "empty";
38
- else if (counts.outline === frames.length) stage = "plan-review";
39
- else if (counts.outline > 0) stage = "sketch-in-progress";
40
- else if (counts.built === frames.length) stage = "sketch-review";
41
- else if (counts.built > 0) stage = "animation-in-progress";
42
- else stage = "final-review";
43
-
44
- return { stage, counts, frameCount: frames.length };
45
- }
@@ -1,98 +0,0 @@
1
- import { useCallback, useEffect, useMemo, useState } from "react";
2
- import { useFileManagerContext } from "../../contexts/FileManagerContext";
3
- import type { StoryboardFrameView } from "../../hooks/useStoryboard";
4
- import {
5
- FRAME_COMMENTS_PATH,
6
- buildCommentsFile,
7
- parseCommentsFile,
8
- type FrameCommentEntry,
9
- } from "./frameComments";
10
-
11
- export type CommentsSubmitState = "idle" | "saving";
12
-
13
- export interface FrameCommentsValue {
14
- /** Draft text per frame index — "" / absent means no comment. */
15
- drafts: Record<number, string>;
16
- setDraft: (index: number, text: string) => void;
17
- /** How many frames currently carry a non-empty draft. */
18
- draftCount: number;
19
- submitState: CommentsSubmitState;
20
- /** Most recent submit failure, shown next to the submit action. */
21
- submitError: string | null;
22
- /** Write the batch to `.hyperframes/frame-comments.json` and clear the drafts. */
23
- submit: () => Promise<boolean>;
24
- /**
25
- * Comments already submitted but not yet consumed by the agent (the file
26
- * still exists on disk). Refreshed on mount, after submit, and on window
27
- * focus — the agent deletes the file once it has applied the feedback.
28
- */
29
- pending: FrameCommentEntry[] | null;
30
- /** Re-read the comments file — callers hook this to board data refreshes. */
31
- refreshPending: () => Promise<void>;
32
- }
33
-
34
- /** Per-frame comment drafts + the batch submit that writes the comments file. */
35
- export function useFrameComments(frames: StoryboardFrameView[]): FrameCommentsValue {
36
- const { writeProjectFile, readOptionalProjectFile } = useFileManagerContext();
37
- const [drafts, setDrafts] = useState<Record<number, string>>({});
38
- const [submitState, setSubmitState] = useState<CommentsSubmitState>("idle");
39
- const [submitError, setSubmitError] = useState<string | null>(null);
40
- const [pending, setPending] = useState<FrameCommentEntry[] | null>(null);
41
-
42
- const refreshPending = useCallback(async () => {
43
- try {
44
- const parsed = parseCommentsFile(await readOptionalProjectFile(FRAME_COMMENTS_PATH));
45
- setPending(parsed && parsed.comments.length > 0 ? parsed.comments : null);
46
- } catch {
47
- // Transient read failure — keep whatever is currently shown.
48
- }
49
- }, [readOptionalProjectFile]);
50
-
51
- useEffect(() => {
52
- void refreshPending();
53
- const onFocus = () => void refreshPending();
54
- window.addEventListener("focus", onFocus);
55
- return () => window.removeEventListener("focus", onFocus);
56
- }, [refreshPending]);
57
-
58
- const setDraft = useCallback((index: number, text: string) => {
59
- setDrafts((prev) => ({ ...prev, [index]: text }));
60
- }, []);
61
-
62
- const draftCount = useMemo(
63
- () => Object.values(drafts).filter((text) => text.trim().length > 0).length,
64
- [drafts],
65
- );
66
-
67
- // One guarded async transaction owns loading, success, failure, and cleanup state.
68
- // fallow-ignore-next-line complexity
69
- const submit = useCallback(async () => {
70
- if (draftCount === 0 || submitState === "saving") return false;
71
- setSubmitState("saving");
72
- setSubmitError(null);
73
- try {
74
- const previous = parseCommentsFile(await readOptionalProjectFile(FRAME_COMMENTS_PATH));
75
- const file = buildCommentsFile(frames, drafts, previous, new Date().toISOString());
76
- await writeProjectFile(FRAME_COMMENTS_PATH, `${JSON.stringify(file, null, 2)}\n`);
77
- setDrafts({});
78
- setPending(file.comments);
79
- return true;
80
- } catch (err: unknown) {
81
- setSubmitError(err instanceof Error ? err.message : "Failed to submit comments");
82
- return false;
83
- } finally {
84
- setSubmitState("idle");
85
- }
86
- }, [draftCount, submitState, frames, drafts, readOptionalProjectFile, writeProjectFile]);
87
-
88
- return {
89
- drafts,
90
- setDraft,
91
- draftCount,
92
- submitState,
93
- submitError,
94
- submit,
95
- pending,
96
- refreshPending,
97
- };
98
- }
@@ -1,142 +0,0 @@
1
- import {
2
- useCallback,
3
- useContext,
4
- useEffect,
5
- useMemo,
6
- useRef,
7
- useState,
8
- type ReactNode,
9
- } from "react";
10
- import { createStableContext } from "../utils/hmrStableContext";
11
-
12
- /**
13
- * Top-level Studio view mode.
14
- *
15
- * `timeline` is the existing NLE/preview stage. `storyboard` replaces that stage
16
- * with the storyboard contact sheet. The mode is mirrored to the `?view=` query
17
- * param so it survives reloads and — importantly — so an agent can deep-link the
18
- * user straight into the storyboard by navigating the tab to `?view=storyboard`.
19
- */
20
- export type StudioViewMode = "timeline" | "storyboard";
21
- export type ViewModeGuard = (nextMode: StudioViewMode) => boolean;
22
-
23
- const VIEW_QUERY_PARAM = "view";
24
-
25
- function readViewModeFromUrl(): StudioViewMode {
26
- if (typeof window === "undefined") return "timeline";
27
- return new URLSearchParams(window.location.search).get(VIEW_QUERY_PARAM) === "storyboard"
28
- ? "storyboard"
29
- : "timeline";
30
- }
31
-
32
- function writeViewModeToUrl(mode: StudioViewMode): void {
33
- if (typeof window === "undefined") return;
34
- const url = new URL(window.location.href);
35
- if (mode === "storyboard") {
36
- url.searchParams.set(VIEW_QUERY_PARAM, "storyboard");
37
- } else {
38
- url.searchParams.delete(VIEW_QUERY_PARAM);
39
- }
40
- window.history.replaceState(window.history.state, "", url);
41
- }
42
-
43
- interface HistoryLocation {
44
- href: string;
45
- state: unknown;
46
- }
47
-
48
- function readHistoryLocation(): HistoryLocation | null {
49
- if (typeof window === "undefined") return null;
50
- return {
51
- href: window.location.href,
52
- state: window.history.state,
53
- };
54
- }
55
-
56
- export interface ViewModeValue {
57
- viewMode: StudioViewMode;
58
- /** Returns false when an active editor vetoes the transition. */
59
- setViewMode: (mode: StudioViewMode) => boolean;
60
- registerViewModeGuard: (guard: ViewModeGuard) => () => void;
61
- }
62
-
63
- /**
64
- * Owns the view-mode state — initial read from `?view=`, toggling, popstate sync.
65
- * Storyboard mode is always available; no flag gating.
66
- */
67
- export function useViewModeState(): ViewModeValue {
68
- const [viewMode, setMode] = useState<StudioViewMode>(() => readViewModeFromUrl());
69
- const guardsRef = useRef(new Set<ViewModeGuard>());
70
- const acceptedLocationRef = useRef<HistoryLocation | null>(readHistoryLocation());
71
-
72
- const canSetViewMode = useCallback((mode: StudioViewMode) => {
73
- for (const guard of guardsRef.current) {
74
- if (!guard(mode)) return false;
75
- }
76
- return true;
77
- }, []);
78
-
79
- // Reflect genuine browser back/forward between history entries with a different
80
- // `?view=`. Note: our own writes use `replaceState` (below), which does NOT fire
81
- // `popstate`, so this listener never sees them — `setViewMode` updates state directly.
82
- // An agent deep-links by doing a full navigation to `?view=storyboard` (picked up by
83
- // the mount-time read); a scripted `pushState`/`replaceState` to `?view=` would not be
84
- // reflected here, by design.
85
- useEffect(() => {
86
- const onPopState = () => {
87
- const mode = readViewModeFromUrl();
88
- if (mode !== viewMode && !canSetViewMode(mode)) {
89
- const acceptedLocation = acceptedLocationRef.current;
90
- if (acceptedLocation) {
91
- window.history.pushState(acceptedLocation.state, "", acceptedLocation.href);
92
- }
93
- return;
94
- }
95
- setMode(mode);
96
- acceptedLocationRef.current = readHistoryLocation();
97
- };
98
- window.addEventListener("popstate", onPopState);
99
- return () => window.removeEventListener("popstate", onPopState);
100
- }, [canSetViewMode, viewMode]);
101
-
102
- const setViewMode = useCallback(
103
- (mode: StudioViewMode) => {
104
- if (!canSetViewMode(mode)) return false;
105
- setMode(mode);
106
- writeViewModeToUrl(mode);
107
- acceptedLocationRef.current = readHistoryLocation();
108
- return true;
109
- },
110
- [canSetViewMode],
111
- );
112
-
113
- const registerViewModeGuard = useCallback((guard: ViewModeGuard) => {
114
- guardsRef.current.add(guard);
115
- return () => {
116
- guardsRef.current.delete(guard);
117
- };
118
- }, []);
119
-
120
- return useMemo(
121
- () => ({ viewMode, setViewMode, registerViewModeGuard }),
122
- [viewMode, setViewMode, registerViewModeGuard],
123
- );
124
- }
125
-
126
- const ViewModeContext = createStableContext<ViewModeValue | null>("ViewModeContext", null);
127
-
128
- export function useViewMode(): ViewModeValue {
129
- const ctx = useContext(ViewModeContext);
130
- if (!ctx) throw new Error("useViewMode must be used within ViewModeProvider");
131
- return ctx;
132
- }
133
-
134
- export function ViewModeProvider({
135
- value,
136
- children,
137
- }: {
138
- value: ViewModeValue;
139
- children: ReactNode;
140
- }) {
141
- return <ViewModeContext value={value}>{children}</ViewModeContext>;
142
- }