@hyperframes/studio 0.7.60 → 0.7.62
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-CWpmBjzv.js +459 -0
- package/dist/assets/{index-D6etaey-.js → index-BJzSiIBj.js} +1 -1
- package/dist/assets/index-Ceyz8Qt2.css +1 -0
- package/dist/assets/{index-Dh_WhagG.js → index-DJQPB78e.js} +1 -1
- package/dist/assets/index-vDRVQtTK.js +426 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.html +2 -2
- package/dist/index.js +4392 -3314
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +15 -15
- package/src/components/DesignPanelPromoteProvider.tsx +27 -1
- package/src/components/EditorShell.tsx +12 -0
- package/src/components/StudioHeader.tsx +2 -3
- package/src/components/StudioLeftSidebar.tsx +3 -0
- package/src/components/StudioRightPanel.tsx +34 -26
- package/src/components/editor/DomEditSelectionChrome.test.tsx +51 -0
- package/src/components/editor/PromotableControl.tsx +4 -2
- package/src/components/editor/PropertyPanel.tsx +151 -149
- package/src/components/editor/PropertyPanelFlat.tsx +50 -42
- package/src/components/editor/compositionReliabilityFixture.integration.test.ts +105 -0
- package/src/components/editor/domEditing.test.ts +34 -0
- package/src/components/editor/manualEditingAvailability.test.ts +6 -6
- package/src/components/editor/manualEditingAvailability.ts +5 -3
- package/src/components/editor/propertyPanelColor.test.tsx +31 -0
- package/src/components/editor/propertyPanelColor.tsx +150 -28
- package/src/components/editor/propertyPanelCommitField.tsx +178 -0
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +44 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +4 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.ts +0 -13
- package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +26 -10
- package/src/components/editor/propertyPanelFlatStyleSections.tsx +21 -32
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +47 -4
- package/src/components/editor/propertyPanelFlatTextSection.tsx +10 -2
- package/src/components/editor/propertyPanelInputCoverage.test.tsx +18 -1
- package/src/components/editor/propertyPanelPrimitives.tsx +3 -92
- package/src/components/editor/propertyPanelSections.tsx +2 -2
- package/src/components/editor/propertyPanelTypes.ts +2 -0
- package/src/components/editor/useInspectorGestureTransaction.test.tsx +37 -0
- package/src/components/editor/useInspectorGestureTransaction.ts +60 -0
- package/src/components/nle/TimelinePane.tsx +6 -0
- package/src/components/sidebar/CompositionsTab.drag.test.tsx +87 -0
- package/src/components/sidebar/CompositionsTab.tsx +45 -2
- package/src/components/sidebar/LeftSidebar.tsx +3 -0
- package/src/components/storyboard/AgentChatMessageButton.test.tsx +47 -0
- package/src/components/storyboard/AgentChatMessageButton.tsx +45 -0
- package/src/components/storyboard/StoryboardFrameFocus.tsx +189 -66
- package/src/components/storyboard/StoryboardLoaded.tsx +121 -23
- package/src/components/storyboard/StoryboardReviewGuide.tsx +300 -0
- package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +170 -0
- package/src/components/storyboard/storyboardReviewStage.test.ts +40 -0
- package/src/components/storyboard/storyboardReviewStage.ts +45 -0
- package/src/components/storyboard/useFrameComments.ts +22 -6
- package/src/contexts/StudioContext.tsx +4 -0
- package/src/contexts/ViewModeContext.tsx +60 -7
- package/src/hooks/timelineEditingHelpers.ts +10 -4
- package/src/hooks/useAddAssetAtPlayhead.test.ts +44 -0
- package/src/hooks/useAddAssetAtPlayhead.ts +21 -0
- package/src/hooks/usePreviewPersistence.ts +7 -1
- package/src/hooks/useRazorSplit.history.test.tsx +21 -28
- package/src/hooks/useRazorSplit.test.tsx +20 -9
- package/src/hooks/useRazorSplit.testHelpers.ts +34 -22
- package/src/hooks/useRazorSplit.ts +78 -350
- package/src/hooks/useSlideshowTabState.test.ts +96 -0
- package/src/hooks/useSlideshowTabState.ts +61 -0
- package/src/hooks/useTimelineAddAtPlayhead.ts +15 -0
- package/src/hooks/useTimelineAssetDropOps.ts +52 -2
- package/src/hooks/useTimelineEditing.ts +19 -17
- package/src/hooks/useTimelineGroupEditing.ts +6 -7
- package/src/player/components/Timeline.tsx +12 -1
- package/src/player/components/timelineAuthoredMoveTarget.ts +77 -0
- package/src/player/components/timelineAuthoredTrack.ts +29 -0
- package/src/player/components/timelineCallbacks.ts +4 -0
- package/src/player/components/timelineClipDragCommit.test.ts +176 -0
- package/src/player/components/timelineClipDragCommit.ts +77 -105
- package/src/player/components/timelineClipDragPreview.test.ts +33 -1
- package/src/player/components/timelineClipDragPreview.ts +2 -1
- package/src/player/components/timelineCollision.test.ts +18 -0
- package/src/player/components/timelineCollision.ts +12 -9
- package/src/player/components/timelineDragDrop.ts +28 -7
- package/src/player/components/timelineEditCapabilities.ts +4 -1
- package/src/player/components/timelineGroupEditing.test.ts +10 -0
- package/src/player/components/timelineGroupEditing.ts +4 -3
- package/src/player/components/timelineLaneMoveRefresh.ts +14 -0
- package/src/player/components/timelineLayout.ts +3 -1
- package/src/player/components/timelineZMirror.ts +1 -1
- package/src/player/components/useTimelineClipDrag.ts +5 -0
- package/src/player/components/useTimelineEditPinning.ts +12 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +42 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +3 -0
- package/src/player/lib/playbackTypes.ts +9 -0
- package/src/player/lib/runtimeProtocol.test.ts +6 -1
- package/src/player/lib/timelineDOM.test.ts +58 -0
- package/src/player/lib/timelineDOM.ts +26 -12
- package/src/player/lib/timelineElementHelpers.ts +21 -11
- package/src/player/lib/timelineIframeHelpers.ts +153 -107
- package/src/player/store/playerStore.ts +2 -1
- package/src/utils/razorSplitTransaction.test.ts +175 -0
- package/src/utils/razorSplitTransaction.ts +200 -0
- package/src/utils/studioPreviewHelpers.test.ts +27 -0
- package/src/utils/timelineCompositionDrop.test.ts +12 -0
- package/src/utils/timelineCompositionDrop.ts +16 -0
- package/src/utils/timelineCompositionInsert.test.ts +142 -0
- package/src/utils/timelineCompositionInsert.ts +90 -0
- package/src/utils/timelineElementSplit.test.ts +14 -2
- package/src/utils/timelineElementSplit.ts +5 -1
- package/dist/assets/hyperframes-player-3XTTaVNf.js +0 -459
- package/dist/assets/index-DXbu6IPT.css +0 -1
- package/dist/assets/index-cH6NfVV_.js +0 -426
- package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +0 -23
|
@@ -96,6 +96,10 @@ export interface TimelinePaneProps {
|
|
|
96
96
|
blockName: string,
|
|
97
97
|
placement: Pick<TimelineElement, "start" | "track">,
|
|
98
98
|
) => Promise<void> | void;
|
|
99
|
+
onCompositionDrop?: (
|
|
100
|
+
sourcePath: string,
|
|
101
|
+
placement: Pick<TimelineElement, "start" | "track">,
|
|
102
|
+
) => Promise<void> | void;
|
|
99
103
|
onBlockedEditAttempt?: (element: TimelineElement, intent: BlockedTimelineEditIntent) => void;
|
|
100
104
|
onSelectTimelineElement?: (element: TimelineElement | null) => void;
|
|
101
105
|
}
|
|
@@ -109,6 +113,7 @@ export function TimelinePane({
|
|
|
109
113
|
onDeleteElement,
|
|
110
114
|
onAssetDrop,
|
|
111
115
|
onBlockDrop,
|
|
116
|
+
onCompositionDrop,
|
|
112
117
|
onBlockedEditAttempt,
|
|
113
118
|
onSelectTimelineElement,
|
|
114
119
|
}: TimelinePaneProps) {
|
|
@@ -273,6 +278,7 @@ export function TimelinePane({
|
|
|
273
278
|
onDeleteElement={handleDeleteElement}
|
|
274
279
|
onAssetDrop={onAssetDrop}
|
|
275
280
|
onBlockDrop={onBlockDrop}
|
|
281
|
+
onCompositionDrop={onCompositionDrop}
|
|
276
282
|
onMoveElement={handleMoveElement}
|
|
277
283
|
onMoveElements={handleMoveElements}
|
|
278
284
|
onResizeElement={handleResizeElement}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { act } from "react";
|
|
4
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { TIMELINE_COMPOSITION_MIME } from "../../utils/timelineCompositionDrop";
|
|
7
|
+
import { CompositionsTab } from "./CompositionsTab";
|
|
8
|
+
|
|
9
|
+
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
|
|
10
|
+
(
|
|
11
|
+
window as unknown as { happyDOM: { settings: { disableIframePageLoading: boolean } } }
|
|
12
|
+
).happyDOM.settings.disableIframePageLoading = true;
|
|
13
|
+
|
|
14
|
+
let root: Root | null = null;
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
if (root) act(() => root?.unmount());
|
|
18
|
+
root = null;
|
|
19
|
+
document.body.innerHTML = "";
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function mount(onSelect = vi.fn(), onAddToTimeline = vi.fn()) {
|
|
23
|
+
const host = document.createElement("div");
|
|
24
|
+
document.body.append(host);
|
|
25
|
+
root = createRoot(host);
|
|
26
|
+
act(() => {
|
|
27
|
+
root?.render(
|
|
28
|
+
<CompositionsTab
|
|
29
|
+
projectId="demo"
|
|
30
|
+
compositions={["compositions/headline.html"]}
|
|
31
|
+
activeComposition={null}
|
|
32
|
+
onSelect={onSelect}
|
|
33
|
+
onAddToTimeline={onAddToTimeline}
|
|
34
|
+
/>,
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
const card = host.querySelector<HTMLElement>('[draggable="true"]');
|
|
38
|
+
if (!card) throw new Error("composition card did not render");
|
|
39
|
+
return { host, card, onSelect, onAddToTimeline };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("composition card drag", () => {
|
|
43
|
+
it("keeps ordinary click navigation", () => {
|
|
44
|
+
const { card, onSelect } = mount();
|
|
45
|
+
act(() => card.click());
|
|
46
|
+
expect(onSelect).toHaveBeenCalledWith("compositions/headline.html");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("emits only source identity and suppresses the click following a drag", () => {
|
|
50
|
+
const { card, onSelect } = mount();
|
|
51
|
+
const data = new Map<string, string>();
|
|
52
|
+
const event = new Event("dragstart", { bubbles: true });
|
|
53
|
+
Object.defineProperty(event, "dataTransfer", {
|
|
54
|
+
value: {
|
|
55
|
+
effectAllowed: "none",
|
|
56
|
+
setData: (type: string, value: string) => data.set(type, value),
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
act(() => {
|
|
60
|
+
card.dispatchEvent(event);
|
|
61
|
+
card.click();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(JSON.parse(data.get(TIMELINE_COMPOSITION_MIME) ?? "null")).toEqual({
|
|
65
|
+
sourcePath: "compositions/headline.html",
|
|
66
|
+
});
|
|
67
|
+
expect(card.className).toContain("select-none");
|
|
68
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("offers pointer and keyboard add-at-playhead actions without opening the card", () => {
|
|
72
|
+
const { host, onSelect, onAddToTimeline } = mount();
|
|
73
|
+
const add = host.querySelector<HTMLButtonElement>(
|
|
74
|
+
'[aria-label="Add headline to timeline at playhead"]',
|
|
75
|
+
);
|
|
76
|
+
if (!add) throw new Error("add action did not render");
|
|
77
|
+
act(() => {
|
|
78
|
+
add.click();
|
|
79
|
+
add.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
|
80
|
+
add.click();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(onAddToTimeline).toHaveBeenCalledTimes(2);
|
|
84
|
+
expect(onAddToTimeline).toHaveBeenLastCalledWith("compositions/headline.html");
|
|
85
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { memo, useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { setPreviewMediaMuted } from "../../player/lib/timelineIframeHelpers";
|
|
3
|
+
import { TIMELINE_COMPOSITION_MIME } from "../../utils/timelineCompositionDrop";
|
|
3
4
|
|
|
4
5
|
interface CompositionsTabProps {
|
|
5
6
|
projectId: string;
|
|
@@ -7,6 +8,7 @@ interface CompositionsTabProps {
|
|
|
7
8
|
activeComposition: string | null;
|
|
8
9
|
onSelect: (comp: string) => void;
|
|
9
10
|
onRenderComposition?: (comp: string) => void;
|
|
11
|
+
onAddToTimeline?: (comp: string) => void;
|
|
10
12
|
isRendering?: boolean;
|
|
11
13
|
lintFindingsByFile?: Map<string, { count: number; messages: string[] }>;
|
|
12
14
|
}
|
|
@@ -115,6 +117,7 @@ function CompCard({
|
|
|
115
117
|
onRender,
|
|
116
118
|
isRendering,
|
|
117
119
|
lintInfo,
|
|
120
|
+
onAddToTimeline,
|
|
118
121
|
}: {
|
|
119
122
|
projectId: string;
|
|
120
123
|
comp: string;
|
|
@@ -123,12 +126,14 @@ function CompCard({
|
|
|
123
126
|
onRender?: () => void;
|
|
124
127
|
isRendering?: boolean;
|
|
125
128
|
lintInfo?: { count: number; messages: string[] };
|
|
129
|
+
onAddToTimeline?: () => void;
|
|
126
130
|
}) {
|
|
127
131
|
const [hovered, setHovered] = useState(false);
|
|
128
132
|
const [stageSize, setStageSize] = useState(DEFAULT_PREVIEW_STAGE);
|
|
129
133
|
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
|
130
134
|
const hoverTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
131
135
|
const syncTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
136
|
+
const draggedRef = useRef(false);
|
|
132
137
|
|
|
133
138
|
const requestIframePlaybackSync = useCallback((shouldPlay: boolean) => {
|
|
134
139
|
if (syncTimer.current) {
|
|
@@ -179,10 +184,32 @@ function CompCard({
|
|
|
179
184
|
|
|
180
185
|
return (
|
|
181
186
|
<div
|
|
182
|
-
|
|
187
|
+
role="button"
|
|
188
|
+
tabIndex={0}
|
|
189
|
+
draggable
|
|
190
|
+
onDragStart={(event) => {
|
|
191
|
+
draggedRef.current = true;
|
|
192
|
+
event.dataTransfer.effectAllowed = "copy";
|
|
193
|
+
event.dataTransfer.setData(TIMELINE_COMPOSITION_MIME, JSON.stringify({ sourcePath: comp }));
|
|
194
|
+
}}
|
|
195
|
+
onDragEnd={() => {
|
|
196
|
+
window.setTimeout(() => {
|
|
197
|
+
draggedRef.current = false;
|
|
198
|
+
}, 0);
|
|
199
|
+
}}
|
|
200
|
+
onClick={() => {
|
|
201
|
+
if (!draggedRef.current) onSelect();
|
|
202
|
+
}}
|
|
203
|
+
onKeyDown={(event) => {
|
|
204
|
+
if (event.target !== event.currentTarget) return;
|
|
205
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
206
|
+
event.preventDefault();
|
|
207
|
+
onSelect();
|
|
208
|
+
}
|
|
209
|
+
}}
|
|
183
210
|
onPointerEnter={handleEnter}
|
|
184
211
|
onPointerLeave={handleLeave}
|
|
185
|
-
className={`group/card w-full text-left px-2 py-1.5 flex items-center gap-2.5 transition-colors cursor-
|
|
212
|
+
className={`group/card w-full select-none text-left px-2 py-1.5 flex items-center gap-2.5 transition-colors cursor-grab active:cursor-grabbing ${
|
|
186
213
|
isActive
|
|
187
214
|
? "bg-studio-accent/10 border-l-2 border-studio-accent"
|
|
188
215
|
: "border-l-2 border-transparent hover:bg-neutral-800/50"
|
|
@@ -232,6 +259,20 @@ function CompCard({
|
|
|
232
259
|
</div>
|
|
233
260
|
<span className="text-[9px] text-neutral-600 truncate block">{comp}</span>
|
|
234
261
|
</div>
|
|
262
|
+
{onAddToTimeline && (
|
|
263
|
+
<button
|
|
264
|
+
type="button"
|
|
265
|
+
title={`Add ${name} to timeline at playhead`}
|
|
266
|
+
aria-label={`Add ${name} to timeline at playhead`}
|
|
267
|
+
onClick={(event) => {
|
|
268
|
+
event.stopPropagation();
|
|
269
|
+
onAddToTimeline();
|
|
270
|
+
}}
|
|
271
|
+
className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded text-neutral-600 opacity-0 transition-[color,background-color,opacity] hover:bg-neutral-800 hover:text-studio-accent group-hover/card:opacity-100 group-focus-within/card:opacity-100 focus:opacity-100"
|
|
272
|
+
>
|
|
273
|
+
<span aria-hidden="true">+</span>
|
|
274
|
+
</button>
|
|
275
|
+
)}
|
|
235
276
|
{onRender && (
|
|
236
277
|
<button
|
|
237
278
|
type="button"
|
|
@@ -274,6 +315,7 @@ export const CompositionsTab = memo(function CompositionsTab({
|
|
|
274
315
|
activeComposition,
|
|
275
316
|
onSelect,
|
|
276
317
|
onRenderComposition,
|
|
318
|
+
onAddToTimeline,
|
|
277
319
|
isRendering,
|
|
278
320
|
lintFindingsByFile,
|
|
279
321
|
}: CompositionsTabProps) {
|
|
@@ -295,6 +337,7 @@ export const CompositionsTab = memo(function CompositionsTab({
|
|
|
295
337
|
isActive={activeComposition === comp}
|
|
296
338
|
onSelect={() => onSelect(comp)}
|
|
297
339
|
onRender={onRenderComposition ? () => onRenderComposition(comp) : undefined}
|
|
340
|
+
onAddToTimeline={onAddToTimeline ? () => onAddToTimeline(comp) : undefined}
|
|
298
341
|
isRendering={isRendering}
|
|
299
342
|
lintInfo={lintFindingsByFile?.get(comp)}
|
|
300
343
|
/>
|
|
@@ -61,6 +61,7 @@ interface LeftSidebarProps {
|
|
|
61
61
|
onPreviewBlock?: (preview: BlockPreviewInfo | null) => void;
|
|
62
62
|
takeoverContent?: ReactNode;
|
|
63
63
|
onAddAssetToTimeline?: (path: string) => void;
|
|
64
|
+
onAddCompositionToTimeline?: (path: string) => void;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
export const LeftSidebar = memo(
|
|
@@ -94,6 +95,7 @@ export const LeftSidebar = memo(
|
|
|
94
95
|
onPreviewBlock,
|
|
95
96
|
takeoverContent,
|
|
96
97
|
onAddAssetToTimeline,
|
|
98
|
+
onAddCompositionToTimeline,
|
|
97
99
|
},
|
|
98
100
|
ref,
|
|
99
101
|
) {
|
|
@@ -220,6 +222,7 @@ export const LeftSidebar = memo(
|
|
|
220
222
|
compositions={compositions}
|
|
221
223
|
activeComposition={activeComposition}
|
|
222
224
|
onSelect={onSelectComposition}
|
|
225
|
+
onAddToTimeline={onAddCompositionToTimeline}
|
|
223
226
|
onRenderComposition={onRenderComposition}
|
|
224
227
|
isRendering={isRendering}
|
|
225
228
|
lintFindingsByFile={lintFindingsByFile}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { AgentChatMessageButton } from "./AgentChatMessageButton";
|
|
7
|
+
|
|
8
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
document.body.innerHTML = "";
|
|
12
|
+
vi.useRealTimers();
|
|
13
|
+
vi.restoreAllMocks();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("AgentChatMessageButton", () => {
|
|
17
|
+
it("reports a successful copy and resets to an explicit re-copy action", async () => {
|
|
18
|
+
vi.useFakeTimers();
|
|
19
|
+
const writeText = vi.fn().mockResolvedValue(undefined);
|
|
20
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
21
|
+
configurable: true,
|
|
22
|
+
value: { writeText },
|
|
23
|
+
});
|
|
24
|
+
const onCopied = vi.fn();
|
|
25
|
+
const host = document.createElement("div");
|
|
26
|
+
document.body.append(host);
|
|
27
|
+
const root = createRoot(host);
|
|
28
|
+
act(() => {
|
|
29
|
+
root.render(<AgentChatMessageButton message="handoff" onCopied={onCopied} />);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const button = host.querySelector("button");
|
|
33
|
+
if (!button) throw new Error("copy button not rendered");
|
|
34
|
+
await act(async () => {
|
|
35
|
+
button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
36
|
+
await Promise.resolve();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(writeText).toHaveBeenCalledWith("handoff");
|
|
40
|
+
expect(onCopied).toHaveBeenCalledOnce();
|
|
41
|
+
expect(button.textContent).toBe("Copied — paste in your agent chat");
|
|
42
|
+
|
|
43
|
+
act(() => vi.advanceTimersByTime(3000));
|
|
44
|
+
expect(button.textContent).toBe("Copy again");
|
|
45
|
+
act(() => root.unmount());
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Button } from "../ui/Button";
|
|
3
|
+
|
|
4
|
+
export const APPLY_STORYBOARD_FEEDBACK_MESSAGE =
|
|
5
|
+
"Read the storyboard feedback I saved in .hyperframes/frame-comments.json and revise the frames.";
|
|
6
|
+
|
|
7
|
+
export function AgentChatMessageButton({
|
|
8
|
+
message,
|
|
9
|
+
label = "Copy prompt for agent",
|
|
10
|
+
onCopied,
|
|
11
|
+
}: {
|
|
12
|
+
message: string;
|
|
13
|
+
label?: string;
|
|
14
|
+
onCopied?: () => void;
|
|
15
|
+
}) {
|
|
16
|
+
const [copyState, setCopyState] = useState<"idle" | "copied" | "again" | "failed">("idle");
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (copyState !== "copied") return;
|
|
20
|
+
const timeout = window.setTimeout(() => setCopyState("again"), 3000);
|
|
21
|
+
return () => window.clearTimeout(timeout);
|
|
22
|
+
}, [copyState]);
|
|
23
|
+
|
|
24
|
+
const copyMessage = async () => {
|
|
25
|
+
try {
|
|
26
|
+
await navigator.clipboard.writeText(message);
|
|
27
|
+
setCopyState("copied");
|
|
28
|
+
onCopied?.();
|
|
29
|
+
} catch {
|
|
30
|
+
setCopyState("failed");
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Button size="sm" variant="secondary" onClick={() => void copyMessage()}>
|
|
36
|
+
{copyState === "copied"
|
|
37
|
+
? "Copied — paste in your agent chat"
|
|
38
|
+
: copyState === "again"
|
|
39
|
+
? "Copy again"
|
|
40
|
+
: copyState === "failed"
|
|
41
|
+
? "Copy failed"
|
|
42
|
+
: label}
|
|
43
|
+
</Button>
|
|
44
|
+
);
|
|
45
|
+
}
|