@hyperframes/studio 0.8.50 → 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 (215) hide show
  1. package/dist/assets/{hyperframes-player-CxxDxHTW.js → hyperframes-player-Cn_PpHqP.js} +3 -3
  2. package/dist/assets/index-C05SNrvY.js +360 -0
  3. package/dist/assets/{index-DUAIWxkI.js → index-CPS3Cvj8.js} +1 -1
  4. package/dist/assets/{index-Dt3qBI-9.js → index-Clrb11lj.js} +1 -1
  5. package/dist/assets/index-DUR9qzQM.css +1 -0
  6. package/dist/index.d.ts +18 -13
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +1867 -3335
  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 +128 -141
  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.tsx +4 -7
  20. package/src/components/ExternalFileConflictBanner.tsx +16 -16
  21. package/src/components/LintModal.tsx +6 -16
  22. package/src/components/MediaPreview.tsx +2 -2
  23. package/src/components/SaveQueuePausedBanner.tsx +2 -2
  24. package/src/components/StudioHeader.tsx +2 -62
  25. package/src/components/StudioLeftSidebar.tsx +7 -4
  26. package/src/components/StudioOverlays.tsx +1 -1
  27. package/src/components/StudioRightPanel.tsx +4 -4
  28. package/src/components/StudioToast.tsx +2 -2
  29. package/src/components/TimelineToolbar.tsx +7 -7
  30. package/src/components/dock/Dock.test.tsx +192 -0
  31. package/src/components/dock/Dock.tsx +264 -0
  32. package/src/components/dock/dock.css +23 -0
  33. package/src/components/dock/dockLayout.ts +58 -0
  34. package/src/components/dock/dockLayoutSchema.ts +66 -0
  35. package/src/components/dock/dockLayoutStore.ts +91 -0
  36. package/src/components/dock/panelRegistry.ts +67 -0
  37. package/src/components/editor/AnimationCard.tsx +4 -4
  38. package/src/components/editor/AnimationCardParts.tsx +6 -6
  39. package/src/components/editor/BlockParamsPanel.tsx +4 -4
  40. package/src/components/editor/BorderRadiusEditor.tsx +1 -1
  41. package/src/components/editor/CanvasContextMenu.tsx +1 -1
  42. package/src/components/editor/ComputedTweenNotice.tsx +1 -1
  43. package/src/components/editor/DomEditOverlay.tsx +1 -1
  44. package/src/components/editor/EaseCurveSection.tsx +1 -1
  45. package/src/components/editor/EaseParamFields.tsx +5 -5
  46. package/src/components/editor/FileTree.tsx +4 -4
  47. package/src/components/editor/FileTreeIcons.tsx +1 -1
  48. package/src/components/editor/FileTreeNodes.tsx +13 -13
  49. package/src/components/editor/InlineTextToolbar.tsx +1 -1
  50. package/src/components/editor/InspectorHeaderActions.tsx +3 -3
  51. package/src/components/editor/KeyframeDiamond.tsx +1 -1
  52. package/src/components/editor/KeyframeEaseList.tsx +1 -1
  53. package/src/components/editor/LayersPanel.tsx +3 -3
  54. package/src/components/editor/MarqueeOverlay.tsx +1 -1
  55. package/src/components/editor/OffCanvasIndicators.tsx +1 -1
  56. package/src/components/editor/PromotableControl.tsx +2 -2
  57. package/src/components/editor/PropertyPanelEmptyState.tsx +7 -7
  58. package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
  59. package/src/components/editor/SnapToolbar.tsx +2 -2
  60. package/src/components/editor/TimelineFxPopover.tsx +1 -1
  61. package/src/components/editor/TopologyLens.tsx +4 -4
  62. package/src/components/editor/Transform3DCube.tsx +2 -2
  63. package/src/components/editor/audioFxRevealTarget.ts +1 -0
  64. package/src/components/editor/domEditing.test.ts +1 -1
  65. package/src/components/editor/domEditingAgentPrompt.test.ts +8 -6
  66. package/src/components/editor/domEditingAgentPrompt.ts +4 -3
  67. package/src/components/editor/propertyPanelColor.tsx +11 -11
  68. package/src/components/editor/propertyPanelColorCurveGraph.tsx +1 -1
  69. package/src/components/editor/propertyPanelColorGradingControls.tsx +7 -9
  70. package/src/components/editor/propertyPanelColorGradingSection.tsx +6 -6
  71. package/src/components/editor/propertyPanelColorGradingSlider.tsx +5 -5
  72. package/src/components/editor/propertyPanelColorWheels.tsx +1 -1
  73. package/src/components/editor/propertyPanelCommitField.tsx +1 -1
  74. package/src/components/editor/propertyPanelFill.tsx +4 -4
  75. package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
  76. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +7 -7
  77. package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
  78. package/src/components/editor/propertyPanelFlatMediaSection.tsx +3 -3
  79. package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +1 -1
  80. package/src/components/editor/propertyPanelFlatPrimitives.tsx +10 -10
  81. package/src/components/editor/propertyPanelFlatSelectRow.tsx +3 -3
  82. package/src/components/editor/propertyPanelFlatTextSection.test.tsx +8 -9
  83. package/src/components/editor/propertyPanelFlatTextSection.tsx +5 -5
  84. package/src/components/editor/propertyPanelFlatToggle.tsx +1 -1
  85. package/src/components/editor/propertyPanelFont.tsx +5 -5
  86. package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
  87. package/src/components/editor/propertyPanelFxControls.test.tsx +1 -1
  88. package/src/components/editor/propertyPanelFxControls.tsx +6 -6
  89. package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
  90. package/src/components/editor/propertyPanelFxPresetStyle.ts +2 -2
  91. package/src/components/editor/propertyPanelGradingNumberField.tsx +1 -1
  92. package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
  93. package/src/components/editor/propertyPanelPrimitives.tsx +7 -7
  94. package/src/components/editor/propertyPanelSections.tsx +8 -8
  95. package/src/components/feedback/StudioFeedbackCard.tsx +7 -7
  96. package/src/components/feedback/feedbackTrigger.ts +0 -1
  97. package/src/components/nle/AssetPreviewOverlay.tsx +6 -2
  98. package/src/components/nle/CompositionBreadcrumb.tsx +3 -3
  99. package/src/components/nle/NLEPreview.tsx +3 -3
  100. package/src/components/nle/PreviewOverlays.tsx +6 -6
  101. package/src/components/nle/PreviewPane.tsx +2 -2
  102. package/src/components/nle/TimelinePane.tsx +3 -3
  103. package/src/components/nle/TimelineResizeDivider.tsx +2 -2
  104. package/src/components/panels/SlideshowPanel.tsx +2 -2
  105. package/src/components/panels/SlideshowSubPanels.tsx +20 -17
  106. package/src/components/panels/VariablesBindElement.tsx +2 -2
  107. package/src/components/panels/VariablesDeclarationForm.tsx +2 -2
  108. package/src/components/panels/VariablesOtherCompositions.tsx +1 -1
  109. package/src/components/panels/VariablesPanel.tsx +3 -3
  110. package/src/components/panels/VariablesValueControls.tsx +2 -2
  111. package/src/components/renders/FfmpegRequiredNotice.tsx +5 -5
  112. package/src/components/renders/RenderQueue.test.tsx +7 -0
  113. package/src/components/renders/RenderQueue.tsx +6 -6
  114. package/src/components/renders/RenderQueueItem.tsx +6 -6
  115. package/src/components/sidebar/AssetCard.tsx +8 -8
  116. package/src/components/sidebar/AssetContextMenu.tsx +8 -8
  117. package/src/components/sidebar/AssetsTab.tsx +2 -2
  118. package/src/components/sidebar/AudioRow.tsx +6 -6
  119. package/src/components/sidebar/BlocksTab.tsx +6 -6
  120. package/src/components/sidebar/CompositionsTab.tsx +5 -5
  121. package/src/components/sidebar/GlobalAssetsView.tsx +1 -1
  122. package/src/components/sidebar/LeftSidebar.lintBadge.test.tsx +58 -0
  123. package/src/components/sidebar/LeftSidebar.tsx +19 -5
  124. package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
  125. package/src/components/ui/Button.test.tsx +60 -0
  126. package/src/components/ui/Button.tsx +4 -4
  127. package/src/components/ui/SearchInput.tsx +2 -2
  128. package/src/components/ui/Tooltip.tsx +1 -1
  129. package/src/hooks/useAppHotkeys.ts +1 -1
  130. package/src/hooks/useLintModal.test.tsx +65 -0
  131. package/src/hooks/useLintModal.ts +3 -0
  132. package/src/hooks/useSdkSession.lifecycle.test.tsx +49 -4
  133. package/src/hooks/useSdkSession.ts +43 -10
  134. package/src/hooks/useTimelineAssetDropOps.test.tsx +41 -1
  135. package/src/hooks/useTimelineAssetDropOps.ts +52 -11
  136. package/src/main.tsx +9 -3
  137. package/src/player/components/AudioWaveform.tsx +1 -1
  138. package/src/player/components/AutomationSelectionMenu.tsx +2 -2
  139. package/src/player/components/ClipContextMenu.tsx +3 -3
  140. package/src/player/components/CompositionThumbnail.test.ts +0 -3
  141. package/src/player/components/CompositionThumbnail.tsx +3 -4
  142. package/src/player/components/EditModal.tsx +1 -1
  143. package/src/player/components/ImageThumbnail.tsx +1 -1
  144. package/src/player/components/KeyframeDiamondContextMenu.tsx +4 -4
  145. package/src/player/components/LayerDisclosureRow.tsx +2 -2
  146. package/src/player/components/PlayerControls.tsx +1 -1
  147. package/src/player/components/ShortcutsPanel.tsx +9 -9
  148. package/src/player/components/SpeedMenu.tsx +3 -5
  149. package/src/player/components/Timeline.tsx +2 -1
  150. package/src/player/components/TimelineCanvas.tsx +17 -10
  151. package/src/player/components/TimelineDiamondConnectors.tsx +1 -1
  152. package/src/player/components/TimelineEmptyState.tsx +5 -5
  153. package/src/player/components/TimelineFxButton.tsx +6 -6
  154. package/src/player/components/TimelineGroupHeader.tsx +5 -5
  155. package/src/player/components/TimelineGroupLaneLabels.tsx +1 -1
  156. package/src/player/components/TimelineGroupRow.tsx +1 -1
  157. package/src/player/components/TimelineLanes.test.tsx +32 -0
  158. package/src/player/components/TimelineLanes.tsx +2 -5
  159. package/src/player/components/TimelineLayerGroupHeader.tsx +2 -2
  160. package/src/player/components/TimelineRuler.tsx +1 -1
  161. package/src/player/components/TimelineShortcutHint.tsx +1 -1
  162. package/src/player/components/TimelineTrackHeader.tsx +1 -1
  163. package/src/player/components/TimelineTrackPlainHeader.tsx +3 -3
  164. package/src/player/components/TimelineTypes.ts +2 -1
  165. package/src/player/components/TrackGapContextMenu.tsx +1 -1
  166. package/src/player/components/VideoThumbnail.tsx +4 -2
  167. package/src/player/components/VolumeControl.tsx +2 -2
  168. package/src/player/components/timelineCallbacks.ts +8 -8
  169. package/src/player/components/timelineClipDragPreview.ts +51 -27
  170. package/src/player/components/timelineClipDragTypes.ts +3 -0
  171. package/src/player/components/timelineDragDrop.test.tsx +61 -1
  172. package/src/player/components/timelineDragDrop.ts +57 -15
  173. package/src/player/components/timelineLaneProps.ts +3 -0
  174. package/src/player/components/timelineSnapping.test.ts +27 -0
  175. package/src/player/components/timelineSnapping.ts +14 -1
  176. package/src/player/components/trackHeaderLabelRows.tsx +4 -4
  177. package/src/player/components/useTimelineClipDrag.resize.test.tsx +136 -2
  178. package/src/player/components/useTimelineClipDrag.ts +30 -3
  179. package/src/player/lib/describeClips.ts +3 -54
  180. package/src/styles/iconTokens.ts +20 -0
  181. package/src/styles/palette-reset.css +6 -0
  182. package/src/styles/studio.css +24 -21
  183. package/src/styles/tailwind-preset.shared.js +3 -0
  184. package/src/styles/theme.css +213 -0
  185. package/src/styles/theme.test.ts +196 -0
  186. package/src/utils/studioUiPreferences.ts +7 -0
  187. package/src/utils/timelineDropTrackInsert.test.ts +134 -0
  188. package/src/utils/timelineDropTrackInsert.ts +83 -0
  189. package/dist/assets/index-n2ml4tUn.js +0 -442
  190. package/dist/assets/index-pO6p0H27.css +0 -1
  191. package/src/components/storyboard/AgentChatMessageButton.test.tsx +0 -47
  192. package/src/components/storyboard/AgentChatMessageButton.tsx +0 -45
  193. package/src/components/storyboard/FramePoster.test.tsx +0 -61
  194. package/src/components/storyboard/FramePoster.tsx +0 -86
  195. package/src/components/storyboard/StoryboardDirection.tsx +0 -45
  196. package/src/components/storyboard/StoryboardFrameFocus.tsx +0 -412
  197. package/src/components/storyboard/StoryboardFrameTile.tsx +0 -117
  198. package/src/components/storyboard/StoryboardGrid.tsx +0 -55
  199. package/src/components/storyboard/StoryboardLoaded.tsx +0 -307
  200. package/src/components/storyboard/StoryboardReviewGuide.tsx +0 -300
  201. package/src/components/storyboard/StoryboardScriptPanel.tsx +0 -26
  202. package/src/components/storyboard/StoryboardSourceEditor.tsx +0 -231
  203. package/src/components/storyboard/StoryboardStatusLegend.tsx +0 -21
  204. package/src/components/storyboard/StoryboardView.tsx +0 -169
  205. package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +0 -187
  206. package/src/components/storyboard/frameComments.test.ts +0 -95
  207. package/src/components/storyboard/frameComments.ts +0 -115
  208. package/src/components/storyboard/frameStatus.ts +0 -36
  209. package/src/components/storyboard/storyboardReviewStage.test.ts +0 -40
  210. package/src/components/storyboard/storyboardReviewStage.ts +0 -45
  211. package/src/components/storyboard/useFrameComments.ts +0 -98
  212. package/src/contexts/ViewModeContext.tsx +0 -142
  213. package/src/hooks/useProjectSignaturePoll.test.tsx +0 -135
  214. package/src/hooks/useProjectSignaturePoll.ts +0 -68
  215. package/src/hooks/useStoryboard.ts +0 -99
@@ -1,412 +0,0 @@
1
- import { useCallback, useEffect, useState } from "react";
2
- import { setFrameVoiceover } from "@hyperframes/core/storyboard";
3
- import type { StoryboardFrameView } from "../../hooks/useStoryboard";
4
- import { useFileManagerContext } from "../../contexts/FileManagerContext";
5
- import { useViewMode } from "../../contexts/ViewModeContext";
6
- import { Button } from "../ui/Button";
7
- import { FramePoster, posterTime } from "./FramePoster";
8
- import {
9
- AgentChatMessageButton,
10
- APPLY_STORYBOARD_FEEDBACK_MESSAGE,
11
- } from "./AgentChatMessageButton";
12
- import { FRAME_STATUS_META } from "./frameStatus";
13
- import type { CommentsSubmitState } from "./useFrameComments";
14
- import { isTypingTarget } from "../../utils/typingTarget";
15
-
16
- export interface StoryboardFrameFocusProps {
17
- projectId: string;
18
- /** Path to STORYBOARD.md (edits are written here). */
19
- storyboardPath: string;
20
- frame: StoryboardFrameView;
21
- frameCount: number;
22
- onBack: () => void;
23
- onNavigate: (delta: number) => void;
24
- /** Re-parse the manifest after an edit is saved. */
25
- onSaved: () => void;
26
- /** Select a composition in the timeline (sets active comp + editing file + sidebar highlight). */
27
- onSelectComposition: (path: string) => void;
28
- /** Whether SCRIPT.md exists and owns final narration/TTS. */
29
- scriptExists: boolean;
30
- /** Shared board draft for this frame, preserved when entering/leaving focus. */
31
- commentDraft: string;
32
- onCommentDraftChange: (text: string) => void;
33
- pendingComment: string | null;
34
- pendingCommentCount: number;
35
- commentDraftCount: number;
36
- commentsSubmitState: CommentsSubmitState;
37
- commentsSubmitError: string | null;
38
- feedbackMessageCopied: boolean;
39
- onFeedbackMessageCopied: () => void;
40
- onSaveFeedback: () => void;
41
- /** Project signature the board was loaded with (busts the poster cache). */
42
- posterVersion?: string;
43
- }
44
-
45
- /**
46
- * Full-area focus on a single frame: large poster, editable voiceover guide,
47
- * review feedback, full narrative, and a jump into the live preview. Edits
48
- * are written back to STORYBOARD.md in place (markdown stays canonical).
49
- *
50
- * Mounted with a `key` per frame, so `draft` initializes from the frame and a
51
- * save-triggered reload never clobbers in-progress typing.
52
- */
53
- // fallow-ignore-next-line complexity
54
- export function StoryboardFrameFocus({
55
- projectId,
56
- storyboardPath,
57
- frame,
58
- frameCount,
59
- onBack,
60
- onNavigate,
61
- onSaved,
62
- onSelectComposition,
63
- scriptExists,
64
- commentDraft,
65
- onCommentDraftChange,
66
- pendingComment,
67
- pendingCommentCount,
68
- commentDraftCount,
69
- commentsSubmitState,
70
- commentsSubmitError,
71
- feedbackMessageCopied,
72
- onFeedbackMessageCopied,
73
- onSaveFeedback,
74
- posterVersion,
75
- }: StoryboardFrameFocusProps) {
76
- const { readProjectFile, writeProjectFile } = useFileManagerContext();
77
- const { setViewMode, registerViewModeGuard } = useViewMode();
78
- const [draft, setDraft] = useState(frame.voiceover ?? "");
79
- const [savedVoiceover, setSavedVoiceover] = useState(frame.voiceover ?? "");
80
- const [busy, setBusy] = useState(false);
81
- const [error, setError] = useState<string | null>(null);
82
-
83
- const applyEdit = useCallback(
84
- async (edit: (source: string) => string) => {
85
- if (busy) return false; // one read-modify-write at a time; avoids a lost update
86
- setBusy(true);
87
- setError(null);
88
- try {
89
- const source = await readProjectFile(storyboardPath);
90
- await writeProjectFile(storyboardPath, edit(source));
91
- onSaved();
92
- return true;
93
- } catch (err: unknown) {
94
- setError(err instanceof Error ? err.message : "failed to save");
95
- return false;
96
- } finally {
97
- setBusy(false);
98
- }
99
- },
100
- [readProjectFile, writeProjectFile, storyboardPath, onSaved, busy],
101
- );
102
-
103
- const title = frame.title ?? `Frame ${frame.index}`;
104
- const dirty = draft !== savedVoiceover;
105
- const canOpenPreview = frame.status !== "outline" && frame.srcExists && Boolean(frame.src);
106
-
107
- const saveVoiceover = useCallback(async () => {
108
- const saved = await applyEdit((src) => setFrameVoiceover(src, frame.index, draft));
109
- if (saved) setSavedVoiceover(draft);
110
- }, [applyEdit, frame.index, draft]);
111
-
112
- // Closing the tab with a dirty voiceover would lose it silently — same
113
- // guard the sibling markdown editor registers for the same class of loss.
114
- useEffect(() => {
115
- if (!dirty) return;
116
- const onBeforeUnload = (e: BeforeUnloadEvent) => {
117
- e.preventDefault();
118
- };
119
- window.addEventListener("beforeunload", onBeforeUnload);
120
- return () => window.removeEventListener("beforeunload", onBeforeUnload);
121
- }, [dirty]);
122
-
123
- // Leaving the frame drops the in-memory voiceover draft; confirm while it's
124
- // dirty. An in-flight save does NOT count as safe: if it fails after unmount
125
- // the error lands on an unmounted component and the draft is silently lost,
126
- // so keep confirming until the save actually lands (dirty clears on success).
127
- const confirmLeave = useCallback(
128
- () => !dirty || window.confirm("Discard unsaved voiceover changes?"),
129
- [dirty],
130
- );
131
- useEffect(() => registerViewModeGuard(confirmLeave), [confirmLeave, registerViewModeGuard]);
132
-
133
- const handleBack = () => {
134
- if (confirmLeave()) onBack();
135
- };
136
- const handleNavigate = (delta: number) => {
137
- if (confirmLeave()) onNavigate(delta);
138
- };
139
-
140
- // ←/→ navigate frames, Esc returns to the Board — but never while typing in a field.
141
- useEffect(() => {
142
- // fallow-ignore-next-line complexity
143
- const onKey = (e: KeyboardEvent) => {
144
- if (isTypingTarget(document.activeElement)) return;
145
- if (e.key === "Escape") handleBack();
146
- else if (e.key === "ArrowLeft" && frame.index > 1) handleNavigate(-1);
147
- else if (e.key === "ArrowRight" && frame.index < frameCount) handleNavigate(1);
148
- };
149
- window.addEventListener("keydown", onKey);
150
- return () => window.removeEventListener("keydown", onKey);
151
- });
152
-
153
- const openInPreview = () => {
154
- if (!setViewMode("timeline")) return;
155
- if (frame.src) onSelectComposition(frame.src);
156
- };
157
-
158
- return (
159
- <div className="flex w-full max-w-[100vw] flex-1 min-h-0 min-w-0 flex-col overflow-hidden bg-neutral-950 text-neutral-200">
160
- <div className="flex items-center gap-3 border-b border-neutral-800 px-4 py-2">
161
- <button
162
- type="button"
163
- onClick={handleBack}
164
- className="rounded px-2 py-1 text-xs font-medium text-neutral-300 hover:bg-neutral-800"
165
- >
166
- ← Board
167
- </button>
168
- <span className="min-w-0 flex-1 truncate text-sm font-medium text-neutral-200">
169
- Frame {frame.number ?? frame.index} — {title}
170
- </span>
171
- <div className="flex shrink-0 items-center gap-1">
172
- <NavButton
173
- label="‹ Prev"
174
- disabled={frame.index <= 1}
175
- onClick={() => handleNavigate(-1)}
176
- />
177
- <NavButton
178
- label="Next ›"
179
- disabled={frame.index >= frameCount}
180
- onClick={() => handleNavigate(1)}
181
- />
182
- </div>
183
- </div>
184
-
185
- {(commentDraftCount > 0 || pendingCommentCount > 0) && (
186
- <div className="flex flex-wrap items-center justify-between gap-2 border-b border-neutral-800 bg-neutral-900/80 px-4 py-2">
187
- <div>
188
- <div className="text-xs font-medium text-neutral-200">
189
- {commentDraftCount > 0 ? "Feedback ready to save" : "Feedback saved"}
190
- </div>
191
- <div className="text-[11px] text-neutral-500">
192
- {commentDraftCount > 0
193
- ? "Save this batch and copy the message for your agent."
194
- : feedbackMessageCopied
195
- ? "Message copied — paste it in your terminal or IDE agent chat."
196
- : "The agent has not been notified yet."}
197
- </div>
198
- </div>
199
- {commentDraftCount > 0 ? (
200
- <Button
201
- size="sm"
202
- variant="primary"
203
- onClick={onSaveFeedback}
204
- loading={commentsSubmitState === "saving"}
205
- >
206
- Save &amp; copy message ({commentDraftCount})
207
- </Button>
208
- ) : (
209
- <AgentChatMessageButton
210
- message={APPLY_STORYBOARD_FEEDBACK_MESSAGE}
211
- label={feedbackMessageCopied ? "Copy again" : "Copy prompt for agent"}
212
- onCopied={onFeedbackMessageCopied}
213
- />
214
- )}
215
- </div>
216
- )}
217
-
218
- <div className="flex flex-1 min-h-0 flex-col overflow-auto lg:flex-row lg:overflow-hidden">
219
- <div className="flex w-full shrink-0 items-center justify-center bg-neutral-900/40 p-4 sm:p-8 lg:h-full lg:w-3/5">
220
- <div className="aspect-video w-full max-w-[900px] overflow-hidden rounded-lg border border-neutral-800 bg-neutral-900">
221
- {canOpenPreview && frame.src ? (
222
- <FramePoster
223
- projectId={projectId}
224
- src={frame.src}
225
- seconds={posterTime(frame)}
226
- title={title}
227
- surface="hero"
228
- posterVersion={posterVersion}
229
- />
230
- ) : (
231
- <FramePlan frame={frame} />
232
- )}
233
- </div>
234
- </div>
235
-
236
- <div className="w-full shrink-0 space-y-6 border-t border-neutral-800 px-4 py-5 sm:px-6 lg:h-full lg:w-2/5 lg:overflow-auto lg:border-t-0 lg:border-l">
237
- <ReadOnlyStatus status={frame.status} />
238
-
239
- <div className="flex flex-wrap gap-x-6 gap-y-1 text-[11px] text-neutral-500">
240
- {frame.duration && <span>Duration {frame.duration}</span>}
241
- {frame.transitionIn && <span>Transition {frame.transitionIn}</span>}
242
- </div>
243
-
244
- <section>
245
- <div className="mb-1 flex items-center justify-between">
246
- <h3
247
- className="text-xs font-semibold uppercase tracking-wider text-neutral-400"
248
- title="Storyboard voiceover is a guide; SCRIPT.md is the final TTS source."
249
- >
250
- 🎙 Voiceover <span className="font-normal normal-case text-neutral-600">guide</span>
251
- </h3>
252
- <Button
253
- size="sm"
254
- variant="primary"
255
- onClick={saveVoiceover}
256
- disabled={!dirty}
257
- loading={busy}
258
- >
259
- {busy ? "Saving…" : "Save voiceover"}
260
- </Button>
261
- </div>
262
- <textarea
263
- value={draft}
264
- onChange={(e) => setDraft(e.target.value)}
265
- rows={3}
266
- placeholder="What the narrator says over this frame…"
267
- className="w-full resize-y rounded border border-neutral-800 bg-neutral-900 p-2 text-sm text-neutral-200 outline-none focus:border-neutral-600"
268
- />
269
- <p className="mt-1 text-[11px] text-neutral-600">
270
- {dirty
271
- ? "Unsaved changes"
272
- : scriptExists
273
- ? "Saved. SCRIPT.md drives final TTS."
274
- : "Saved to STORYBOARD.md. This voiceover guides narration for the frame."}
275
- </p>
276
- {error && <p className="mt-1 text-[11px] text-red-400">{error}</p>}
277
- </section>
278
-
279
- <section>
280
- <div className="mb-1">
281
- <h3 className="text-xs font-semibold uppercase tracking-wider text-neutral-400">
282
- Frame feedback
283
- </h3>
284
- </div>
285
- <textarea
286
- value={commentDraft}
287
- onChange={(e) => onCommentDraftChange(e.target.value)}
288
- rows={3}
289
- placeholder="Tell your agent what to change in this frame…"
290
- aria-label={`Comment on ${title}`}
291
- className="w-full resize-y rounded border border-neutral-800 bg-neutral-900 p-2 text-sm text-neutral-200 placeholder:text-neutral-600 outline-none focus:border-sky-700"
292
- />
293
- {pendingCommentCount > 0 ? (
294
- <div className="mt-2 flex flex-wrap items-center justify-between gap-2 rounded-md border border-sky-900/70 bg-sky-950/20 px-2.5 py-2">
295
- <p className="text-[11px] text-sky-200">
296
- Paste the agent prompt in your terminal or IDE chat.
297
- </p>
298
- <AgentChatMessageButton
299
- message={APPLY_STORYBOARD_FEEDBACK_MESSAGE}
300
- onCopied={onFeedbackMessageCopied}
301
- />
302
- </div>
303
- ) : (
304
- <p className="mt-1 text-[11px] text-neutral-600">
305
- {commentDraftCount > 0
306
- ? "Your change is ready. Save it using the review bar above."
307
- : "Add a change to prepare feedback for the agent."}
308
- </p>
309
- )}
310
- {pendingComment && (
311
- <p className="mt-1 text-[11px] text-sky-400/90">
312
- <span className="font-medium">Pending:</span> “{pendingComment}”
313
- </p>
314
- )}
315
- {commentsSubmitError && (
316
- <p className="mt-1 text-[11px] text-red-400">
317
- Couldn’t submit: {commentsSubmitError}
318
- </p>
319
- )}
320
- </section>
321
-
322
- {frame.narrative && (
323
- <section>
324
- <h3 className="mb-1 text-xs font-semibold uppercase tracking-wider text-neutral-400">
325
- Narrative
326
- </h3>
327
- <p className="whitespace-pre-wrap text-sm text-neutral-300">{frame.narrative}</p>
328
- </section>
329
- )}
330
-
331
- {canOpenPreview ? (
332
- <Button size="sm" variant="secondary" onClick={openInPreview}>
333
- Open in Preview →
334
- </Button>
335
- ) : (
336
- <div className="rounded-md border border-neutral-800 bg-neutral-900/60 px-3 py-2 text-xs text-neutral-500">
337
- Preview becomes available after your agent builds this frame.
338
- </div>
339
- )}
340
- </div>
341
- </div>
342
- </div>
343
- );
344
- }
345
-
346
- function NavButton({
347
- label,
348
- disabled,
349
- onClick,
350
- }: {
351
- label: string;
352
- disabled: boolean;
353
- onClick: () => void;
354
- }) {
355
- return (
356
- <button
357
- type="button"
358
- onClick={onClick}
359
- disabled={disabled}
360
- className="rounded px-2 py-1 text-xs font-medium text-neutral-300 hover:bg-neutral-800 enabled:active:scale-[0.98] transition-transform disabled:opacity-30"
361
- >
362
- {label}
363
- </button>
364
- );
365
- }
366
-
367
- function ReadOnlyStatus({ status }: { status: StoryboardFrameView["status"] }) {
368
- const meta = FRAME_STATUS_META[status];
369
- return (
370
- <div className="flex items-center gap-2">
371
- <span className="text-xs font-semibold uppercase tracking-wider text-neutral-500">
372
- Status
373
- </span>
374
- <span className={`rounded px-2 py-1 text-xs font-medium ${meta.chipClass}`}>
375
- {meta.label}
376
- </span>
377
- <span className="text-[11px] text-neutral-600">Updated by your agent</span>
378
- </div>
379
- );
380
- }
381
-
382
- // The empty state selects copy from frame status and whichever storyboard fields are available.
383
- // fallow-ignore-next-line complexity
384
- function FramePlan({ frame }: { frame: StoryboardFrameView }) {
385
- const title = frame.title ?? `Frame ${frame.index}`;
386
- const isOutline = frame.status === "outline";
387
- return (
388
- <div className="flex h-full w-full items-center justify-center bg-[radial-gradient(circle_at_center,_rgba(38,38,38,0.8),_rgba(10,10,10,1))] p-10">
389
- <div className="max-w-xl text-center">
390
- <span className="rounded-full border border-neutral-700 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-wider text-neutral-400">
391
- {isOutline ? "Planned frame" : "Preview unavailable"}
392
- </span>
393
- <h2 className="mt-4 text-2xl font-semibold text-neutral-100">{title}</h2>
394
- {frame.scene && (
395
- <p className="mt-3 text-base leading-relaxed text-neutral-300">{frame.scene}</p>
396
- )}
397
- {!frame.scene && frame.narrative && (
398
- <p className="mt-3 line-clamp-4 text-sm leading-relaxed text-neutral-400">
399
- {frame.narrative}
400
- </p>
401
- )}
402
- <p className="mt-5 text-xs text-neutral-600">
403
- {isOutline
404
- ? "A visual preview will appear when your agent builds the sketch."
405
- : frame.src
406
- ? `Frame file not found: ${frame.src}`
407
- : "This frame does not link to a source file."}
408
- </p>
409
- </div>
410
- </div>
411
- );
412
- }
@@ -1,117 +0,0 @@
1
- import type { StoryboardFrameView } from "../../hooks/useStoryboard";
2
- import { FramePoster, posterTime } from "./FramePoster";
3
- import { FRAME_STATUS_META } from "./frameStatus";
4
-
5
- export interface StoryboardFrameTileProps {
6
- projectId: string;
7
- frame: StoryboardFrameView;
8
- /** Open this frame in the full-area focus view. */
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;
17
- }
18
-
19
- function firstLine(text: string): string {
20
- return (
21
- text
22
- .split("\n")
23
- .find((line) => line.trim().length > 0)
24
- ?.trim() ?? ""
25
- );
26
- }
27
-
28
- function placeholderMessage(frame: StoryboardFrameView): string {
29
- if (frame.status === "outline") return "Not built yet";
30
- if (frame.src && !frame.srcExists) return "Frame file not found";
31
- return "No preview";
32
- }
33
-
34
- /** A single contact-sheet tile: poster preview + its metadata. Click to focus. */
35
- // fallow-ignore-next-line complexity
36
- export function StoryboardFrameTile({
37
- projectId,
38
- frame,
39
- onOpen,
40
- commentDraft,
41
- onCommentDraftChange,
42
- pendingComment,
43
- posterVersion,
44
- }: StoryboardFrameTileProps) {
45
- const meta = FRAME_STATUS_META[frame.status];
46
- const renderable = frame.srcExists && frame.status !== "outline";
47
- const title = frame.title ?? `Frame ${frame.index}`;
48
- const sceneLine = frame.scene ?? firstLine(frame.narrative);
49
-
50
- return (
51
- <article className="min-w-0">
52
- <button
53
- type="button"
54
- onClick={() => onOpen(frame.index)}
55
- className="group relative block aspect-video w-full overflow-hidden rounded-lg border border-neutral-800 bg-neutral-900 text-left transition-colors hover:border-neutral-600"
56
- >
57
- <div className="absolute left-2 top-2 z-10 flex h-6 min-w-6 items-center justify-center rounded-full bg-black/70 px-1.5 text-xs font-semibold text-neutral-100">
58
- {frame.number ?? frame.index}
59
- </div>
60
- {renderable && frame.src ? (
61
- <FramePoster
62
- projectId={projectId}
63
- src={frame.src}
64
- seconds={posterTime(frame)}
65
- title={title}
66
- posterVersion={posterVersion}
67
- />
68
- ) : (
69
- <FrameTilePlaceholder frame={frame} />
70
- )}
71
- </button>
72
-
73
- <div className="mt-2 flex items-start justify-between gap-2">
74
- <h3 className="truncate text-sm font-medium text-neutral-200">{title}</h3>
75
- <span
76
- title={meta.tooltip}
77
- aria-label={`Status: ${meta.label} — ${meta.tooltip}`}
78
- className={`shrink-0 cursor-default rounded px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide ${meta.chipClass}`}
79
- >
80
- {meta.label}
81
- </span>
82
- </div>
83
- {sceneLine && <p className="mt-0.5 line-clamp-2 text-xs text-neutral-400">{sceneLine}</p>}
84
- {frame.voiceover && (
85
- <p className="mt-1 line-clamp-2 text-xs italic text-neutral-500">
86
- <span aria-hidden="true">🎙 </span>“{frame.voiceover}”
87
- </p>
88
- )}
89
- <div className="mt-1 flex gap-3 text-[11px] text-neutral-600">
90
- {frame.duration && <span>{frame.duration}</span>}
91
- {frame.transitionIn && <span>↘ {frame.transitionIn}</span>}
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
- )}
106
- </article>
107
- );
108
- }
109
-
110
- function FrameTilePlaceholder({ frame }: { frame: StoryboardFrameView }) {
111
- return (
112
- <div className="flex h-full w-full flex-col items-center justify-center gap-1 border border-dashed border-neutral-700 bg-neutral-950 text-center">
113
- <span className="text-xs font-medium text-neutral-400">{frame.title ?? "Outline"}</span>
114
- <span className="text-[11px] text-neutral-600">{placeholderMessage(frame)}</span>
115
- </div>
116
- );
117
- }
@@ -1,55 +0,0 @@
1
- import type { StoryboardFrameView } from "../../hooks/useStoryboard";
2
- import type { FrameCommentEntry } from "./frameComments";
3
- import { StoryboardFrameTile } from "./StoryboardFrameTile";
4
-
5
- export interface StoryboardGridProps {
6
- projectId: string;
7
- frames: StoryboardFrameView[];
8
- /** Open a frame in the full-area focus view. */
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;
17
- }
18
-
19
- /** The contact sheet: ordered frame tiles in a responsive grid. */
20
- export function StoryboardGrid({
21
- projectId,
22
- frames,
23
- onOpenFrame,
24
- commentDrafts,
25
- onCommentDraftChange,
26
- pendingComments,
27
- posterVersion,
28
- }: StoryboardGridProps) {
29
- if (frames.length === 0) {
30
- return (
31
- <div className="mt-8 rounded-lg border border-dashed border-neutral-800 px-6 py-12 text-center text-sm text-neutral-500">
32
- This storyboard has no frames yet.
33
- </div>
34
- );
35
- }
36
-
37
- return (
38
- <div className="mt-8 grid gap-x-6 gap-y-8 [grid-template-columns:repeat(auto-fill,minmax(300px,1fr))]">
39
- {frames.map((frame) => (
40
- <StoryboardFrameTile
41
- key={frame.index}
42
- projectId={projectId}
43
- frame={frame}
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}
51
- />
52
- ))}
53
- </div>
54
- );
55
- }