@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.
- package/dist/assets/{hyperframes-player--Z69cEkE.js → hyperframes-player-Bm07FLkl.js} +1 -1
- package/dist/assets/{index-KsfE1bUu.js → index-B995FG46.js} +1 -1
- package/dist/assets/index-CrkAdJkb.js +426 -0
- package/dist/assets/index-Dj5p8U_A.css +1 -0
- package/dist/assets/{index-Bf1x1y8H.js → index-FvzmPhfG.js} +1 -1
- package/dist/{chunk-5QSIMBEJ.js → chunk-OBAG3GWK.js} +33 -21
- package/dist/chunk-OBAG3GWK.js.map +1 -0
- package/dist/{domEditingLayers-6LQGKPOI.js → domEditingLayers-2CKWGEHS.js} +2 -2
- package/dist/index.d.ts +27 -7
- package/dist/index.html +2 -2
- package/dist/index.js +18029 -11563
- package/dist/index.js.map +1 -1
- package/dist/styles/tailwind-preset.d.ts +5 -0
- package/dist/styles/tailwind-preset.js +8 -1
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +6 -6
- package/src/components/DesignPanelPromoteProvider.tsx +15 -2
- package/src/components/StudioFeedbackBar.tsx +2 -2
- package/src/components/StudioRightPanel.tsx +29 -51
- package/src/components/editor/AnimationCard.test.tsx +134 -0
- package/src/components/editor/AnimationCard.tsx +19 -4
- package/src/components/editor/ArcPathControls.tsx +1 -0
- package/src/components/editor/CanvasContextMenu.test.tsx +27 -14
- package/src/components/editor/CanvasContextMenu.tsx +71 -6
- package/src/components/editor/DomEditOverlay.tsx +5 -2
- package/src/components/editor/GestureRecordControl.tsx +7 -1
- package/src/components/editor/GsapAnimationSection.tsx +109 -14
- package/src/components/editor/InspectorHeaderActions.tsx +42 -4
- package/src/components/editor/LayersPanel.tsx +110 -99
- package/src/components/editor/PropertyPanel.test.tsx +948 -0
- package/src/components/editor/PropertyPanel.tsx +144 -148
- package/src/components/editor/PropertyPanelEmptyState.test.tsx +74 -0
- package/src/components/editor/PropertyPanelEmptyState.tsx +179 -1
- package/src/components/editor/PropertyPanelFlat.tsx +580 -0
- package/src/components/editor/PropertyPanelFlatFooter.test.tsx +83 -0
- package/src/components/editor/PropertyPanelFlatFooter.tsx +73 -0
- package/src/components/editor/PropertyPanelFlatHeader.test.tsx +79 -0
- package/src/components/editor/PropertyPanelFlatHeader.tsx +112 -0
- package/src/components/editor/canvasContextMenuZOrder.test.ts +57 -0
- package/src/components/editor/canvasContextMenuZOrder.ts +40 -1
- package/src/components/editor/domEditingDom.ts +5 -6
- package/src/components/editor/domEditingElement.ts +15 -32
- package/src/components/editor/gsapAnimationCallbacks.ts +27 -0
- package/src/components/editor/gsapLivePreview.ts +34 -0
- package/src/components/editor/layersPanelSort.ts +80 -0
- package/src/components/editor/manualEditingAvailability.test.ts +10 -0
- package/src/components/editor/manualEditingAvailability.ts +9 -0
- package/src/components/editor/propertyPanel3dTransform.tsx +5 -0
- package/src/components/editor/propertyPanelColor.test.tsx +28 -0
- package/src/components/editor/propertyPanelColor.tsx +31 -1
- package/src/components/editor/propertyPanelColorGradingControls.tsx +11 -2
- package/src/components/editor/propertyPanelColorGradingSection.tsx +45 -365
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +19 -5
- package/src/components/editor/propertyPanelFill.tsx +22 -5
- package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +622 -0
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +570 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.test.tsx +293 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.tsx +383 -0
- package/src/components/editor/propertyPanelFlatMaskInsetRows.tsx +102 -0
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +436 -0
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +286 -0
- package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +272 -0
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +315 -0
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +1174 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +563 -0
- package/src/components/editor/propertyPanelFlatSelectRow.tsx +101 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +23 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.ts +27 -0
- package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +598 -0
- package/src/components/editor/propertyPanelFlatStyleSections.tsx +516 -0
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +465 -0
- package/src/components/editor/propertyPanelFlatTextSection.tsx +407 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.test.ts +71 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.ts +59 -0
- package/src/components/editor/propertyPanelFlatToggle.test.tsx +63 -0
- package/src/components/editor/propertyPanelFlatToggle.tsx +53 -0
- package/src/components/editor/propertyPanelFont.test.tsx +30 -0
- package/src/components/editor/propertyPanelFont.tsx +136 -95
- package/src/components/editor/propertyPanelHelpers.ts +73 -0
- package/src/components/editor/propertyPanelInputCoverage.test.tsx +587 -0
- package/src/components/editor/propertyPanelMediaSection.tsx +10 -0
- package/src/components/editor/propertyPanelPrimitives.tsx +60 -29
- package/src/components/editor/propertyPanelSections.test.tsx +161 -0
- package/src/components/editor/propertyPanelSections.tsx +154 -90
- package/src/components/editor/propertyPanelStyleSections.tsx +12 -1
- package/src/components/editor/propertyPanelTimingSection.tsx +1 -1
- package/src/components/editor/propertyPanelTypes.ts +14 -1
- package/src/components/editor/propertyPanelValueTier.test.ts +32 -0
- package/src/components/editor/propertyPanelValueTier.ts +28 -0
- package/src/components/editor/useColorGradingController.test.ts +418 -0
- package/src/components/editor/useColorGradingController.ts +596 -0
- package/src/components/editor/useLayerRevealOverride.test.ts +169 -0
- package/src/components/editor/useLayerRevealOverride.ts +389 -0
- package/src/components/nle/PreviewOverlays.tsx +27 -2
- package/src/components/nle/TimelinePane.test.ts +3 -0
- package/src/components/nle/TimelinePane.tsx +11 -1
- package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +440 -0
- package/src/components/nle/useCanvasZOrderTimelineMirror.ts +191 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +1 -0
- package/src/components/nle/zLaneGesture.test.ts +176 -0
- package/src/components/nle/zLaneGesture.ts +77 -0
- package/src/components/panels/VariablesPanel.tsx +4 -0
- package/src/components/sidebar/CompositionsTab.test.ts +28 -2
- package/src/components/sidebar/CompositionsTab.tsx +3 -1
- package/src/components/storyboard/FramePoster.tsx +24 -4
- package/src/components/storyboard/StoryboardFrameFocus.tsx +4 -0
- package/src/components/storyboard/StoryboardFrameTile.tsx +30 -1
- package/src/components/storyboard/StoryboardGrid.tsx +23 -1
- package/src/components/storyboard/StoryboardLoaded.tsx +58 -2
- package/src/components/storyboard/StoryboardView.tsx +9 -2
- package/src/components/storyboard/frameComments.test.ts +95 -0
- package/src/components/storyboard/frameComments.ts +115 -0
- package/src/components/storyboard/useFrameComments.ts +82 -0
- package/src/contexts/DesignPanelInputContext.test.tsx +81 -0
- package/src/contexts/DesignPanelInputContext.tsx +48 -0
- package/src/contexts/DomEditContext.tsx +4 -0
- package/src/contexts/VariablePromoteContext.test.tsx +67 -0
- package/src/contexts/VariablePromoteContext.tsx +10 -5
- package/src/hooks/domEditCommitRunner.ts +11 -0
- package/src/hooks/domEditCommitTypes.ts +17 -1
- package/src/hooks/elementLifecycleOpsTestUtils.ts +25 -0
- package/src/hooks/fetchStubTestUtils.ts +16 -0
- package/src/hooks/gsapScriptCommitTypes.ts +6 -4
- package/src/hooks/timelineEditingHelpers.test.ts +92 -1
- package/src/hooks/timelineEditingHelpers.ts +32 -15
- package/src/hooks/timelineMoveAdapter.ts +2 -1
- package/src/hooks/timelineTimingSync.test.ts +703 -33
- package/src/hooks/timelineTimingSync.ts +282 -97
- package/src/hooks/timelineTrackVisibility.test.ts +55 -1
- package/src/hooks/timelineTrackVisibility.ts +31 -13
- package/src/hooks/useAppHotkeys.ts +9 -0
- package/src/hooks/useClipboard.ts +19 -17
- package/src/hooks/useDomEditAttributeCommits.ts +123 -1
- package/src/hooks/useDomEditCommits.test.tsx +316 -38
- package/src/hooks/useDomEditCommits.ts +81 -162
- package/src/hooks/useDomEditCommitsHelpers.ts +158 -0
- package/src/hooks/useDomEditSession.test.tsx +3 -1
- package/src/hooks/useDomEditSession.ts +18 -6
- package/src/hooks/useDomEditTextCommits.ts +14 -9
- package/src/hooks/useDomEditWiring.ts +10 -9
- package/src/hooks/useElementLifecycleOps.test.tsx +426 -60
- package/src/hooks/useElementLifecycleOps.ts +182 -84
- package/src/hooks/useFileManager.projectOwnership.test.tsx +98 -0
- package/src/hooks/useFileManager.ts +82 -60
- package/src/hooks/useGsapAnimationOps.ts +7 -6
- package/src/hooks/useGsapKeyframeOps.ts +6 -5
- package/src/hooks/useGsapPropertyDebounce.ts +43 -28
- package/src/hooks/useGsapPropertyDebounceFlush.test.ts +35 -0
- package/src/hooks/useGsapScriptCommits.test.tsx +48 -2
- package/src/hooks/useGsapScriptCommits.ts +150 -50
- package/src/hooks/useGsapSelectionHandlers.test.tsx +116 -0
- package/src/hooks/useGsapSelectionHandlers.ts +94 -31
- package/src/hooks/useInspectorSplitResize.ts +51 -0
- package/src/hooks/usePersistentEditHistory.projectOwnership.test.tsx +60 -0
- package/src/hooks/usePersistentEditHistory.test.ts +49 -1
- package/src/hooks/usePersistentEditHistory.ts +71 -31
- package/src/hooks/usePreviewPersistence.ts +2 -2
- package/src/hooks/useProjectCompositionVariables.ts +10 -10
- package/src/hooks/useProjectSignaturePoll.test.tsx +135 -0
- package/src/hooks/useProjectSignaturePoll.ts +68 -0
- package/src/hooks/useSdkSession.lifecycle.test.tsx +137 -0
- package/src/hooks/useSdkSession.ts +155 -14
- package/src/hooks/useSlideshowPersist.ts +7 -1
- package/src/hooks/useStoryboard.ts +23 -4
- package/src/hooks/useTimelineEditing.test.tsx +306 -155
- package/src/hooks/useTimelineEditing.ts +111 -101
- package/src/hooks/useTimelineEditingTypes.ts +4 -1
- package/src/hooks/useTimelineGroupEditing.ts +51 -5
- package/src/hooks/useVariablesPersist.ts +9 -7
- package/src/icons/SystemIcons.tsx +2 -0
- package/src/player/components/Timeline.test.ts +31 -21
- package/src/player/components/Timeline.tsx +62 -91
- package/src/player/components/TimelineCanvas.tsx +36 -5
- package/src/player/components/TimelineLanes.tsx +32 -10
- package/src/player/components/TimelineOverlays.tsx +36 -0
- package/src/player/components/TimelineRuler.tsx +18 -5
- package/src/player/components/TrackGapContextMenu.tsx +116 -0
- package/src/player/components/timelineCallbacks.ts +4 -1
- package/src/player/components/timelineClipDragCommit.test.ts +279 -3
- package/src/player/components/timelineClipDragCommit.ts +173 -71
- package/src/player/components/timelineGapCommit.test.ts +163 -0
- package/src/player/components/timelineGapCommit.ts +103 -0
- package/src/player/components/timelineGaps.test.ts +221 -0
- package/src/player/components/timelineGaps.ts +182 -0
- package/src/player/components/timelineLayout.test.ts +2 -1
- package/src/player/components/timelineLayout.ts +22 -6
- package/src/player/components/timelineMarquee.test.ts +20 -10
- package/src/player/components/timelineMarquee.ts +9 -2
- package/src/player/components/timelineStackingSync.test.ts +42 -1
- package/src/player/components/timelineStackingSync.ts +31 -16
- package/src/player/components/timelineZMirror.test.ts +667 -0
- package/src/player/components/timelineZMirror.ts +393 -0
- package/src/player/components/timelineZoom.test.ts +2 -2
- package/src/player/components/timelineZoom.ts +7 -3
- package/src/player/components/useTimelineClipDrag.ts +1 -1
- package/src/player/components/useTimelineGapHighlights.test.ts +119 -0
- package/src/player/components/useTimelineGapHighlights.ts +110 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -3
- package/src/player/components/useTimelineRangeSelection.ts +6 -4
- package/src/player/components/useTimelineScrollViewport.ts +73 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +0 -1
- package/src/player/components/useTimelineStackingSync.ts +5 -3
- package/src/player/components/useTimelineTrackDerivations.ts +38 -0
- package/src/player/components/useTrackGapMenu.ts +155 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +67 -4
- package/src/player/hooks/useExpandedTimelineElements.ts +12 -1
- package/src/player/hooks/useTimelineSyncCallbacks.ts +2 -0
- package/src/player/lib/layerOrdering.test.ts +1 -15
- package/src/player/lib/layerOrdering.ts +5 -28
- package/src/player/lib/timelineDOM.test.ts +72 -0
- package/src/player/lib/timelineDOM.ts +3 -0
- package/src/player/lib/timelineElementHelpers.ts +35 -11
- package/src/player/store/playerStore.ts +12 -1
- package/src/styles/studio.css +22 -0
- package/src/styles/tailwind-preset.shared.js +7 -0
- package/src/telemetry/events.test.ts +10 -0
- package/src/telemetry/events.ts +1 -0
- package/src/utils/designInputTracking.test.ts +97 -0
- package/src/utils/designInputTracking.ts +80 -0
- package/src/utils/domEditSaveQueue.ts +4 -3
- package/src/utils/editHistory.test.ts +82 -0
- package/src/utils/editHistory.ts +14 -2
- package/src/utils/gsapSoftReload.test.ts +54 -115
- package/src/utils/gsapSoftReload.ts +42 -152
- package/src/utils/gsapUndoRestore.test.ts +269 -0
- package/src/utils/gsapUndoRestore.ts +269 -0
- package/src/utils/sdkCutover.gate.test.ts +7 -7
- package/src/utils/sdkCutover.test.ts +490 -57
- package/src/utils/sdkCutover.ts +181 -219
- package/src/utils/sdkEditTransaction.ts +292 -0
- package/src/utils/sdkResolverAttempts.ts +94 -0
- package/src/utils/sdkResolverShadow.test.ts +148 -0
- package/src/utils/sdkResolverShadow.ts +139 -106
- package/src/utils/setSlideshowManifest.test.ts +0 -9
- package/src/utils/setSlideshowManifest.ts +20 -27
- package/src/utils/sourceScopedSelectorIndex.ts +29 -0
- package/src/utils/studioFileHistory.test.ts +41 -0
- package/src/utils/studioFileHistory.ts +30 -27
- package/src/utils/studioFileMutationCoordinator.ts +45 -0
- package/src/utils/studioHelpers.test.ts +45 -0
- package/src/utils/studioHelpers.ts +45 -14
- package/src/utils/studioSaveDiagnostics.ts +17 -0
- package/dist/assets/index-DfmYkU44.js +0 -423
- package/dist/assets/index-_pqzyxB1.css +0 -1
- package/dist/chunk-5QSIMBEJ.js.map +0 -1
- /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 {
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
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({
|
|
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:
|
|
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 }) {
|