@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.
Files changed (110) hide show
  1. package/dist/assets/hyperframes-player-CWpmBjzv.js +459 -0
  2. package/dist/assets/{index-D6etaey-.js → index-BJzSiIBj.js} +1 -1
  3. package/dist/assets/index-Ceyz8Qt2.css +1 -0
  4. package/dist/assets/{index-Dh_WhagG.js → index-DJQPB78e.js} +1 -1
  5. package/dist/assets/index-vDRVQtTK.js +426 -0
  6. package/dist/index.d.ts +12 -1
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +4392 -3314
  9. package/dist/index.js.map +1 -1
  10. package/package.json +7 -7
  11. package/src/App.tsx +15 -15
  12. package/src/components/DesignPanelPromoteProvider.tsx +27 -1
  13. package/src/components/EditorShell.tsx +12 -0
  14. package/src/components/StudioHeader.tsx +2 -3
  15. package/src/components/StudioLeftSidebar.tsx +3 -0
  16. package/src/components/StudioRightPanel.tsx +34 -26
  17. package/src/components/editor/DomEditSelectionChrome.test.tsx +51 -0
  18. package/src/components/editor/PromotableControl.tsx +4 -2
  19. package/src/components/editor/PropertyPanel.tsx +151 -149
  20. package/src/components/editor/PropertyPanelFlat.tsx +50 -42
  21. package/src/components/editor/compositionReliabilityFixture.integration.test.ts +105 -0
  22. package/src/components/editor/domEditing.test.ts +34 -0
  23. package/src/components/editor/manualEditingAvailability.test.ts +6 -6
  24. package/src/components/editor/manualEditingAvailability.ts +5 -3
  25. package/src/components/editor/propertyPanelColor.test.tsx +31 -0
  26. package/src/components/editor/propertyPanelColor.tsx +150 -28
  27. package/src/components/editor/propertyPanelCommitField.tsx +178 -0
  28. package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +44 -0
  29. package/src/components/editor/propertyPanelFlatPrimitives.tsx +4 -0
  30. package/src/components/editor/propertyPanelFlatStyleHelpers.ts +0 -13
  31. package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +26 -10
  32. package/src/components/editor/propertyPanelFlatStyleSections.tsx +21 -32
  33. package/src/components/editor/propertyPanelFlatTextSection.test.tsx +47 -4
  34. package/src/components/editor/propertyPanelFlatTextSection.tsx +10 -2
  35. package/src/components/editor/propertyPanelInputCoverage.test.tsx +18 -1
  36. package/src/components/editor/propertyPanelPrimitives.tsx +3 -92
  37. package/src/components/editor/propertyPanelSections.tsx +2 -2
  38. package/src/components/editor/propertyPanelTypes.ts +2 -0
  39. package/src/components/editor/useInspectorGestureTransaction.test.tsx +37 -0
  40. package/src/components/editor/useInspectorGestureTransaction.ts +60 -0
  41. package/src/components/nle/TimelinePane.tsx +6 -0
  42. package/src/components/sidebar/CompositionsTab.drag.test.tsx +87 -0
  43. package/src/components/sidebar/CompositionsTab.tsx +45 -2
  44. package/src/components/sidebar/LeftSidebar.tsx +3 -0
  45. package/src/components/storyboard/AgentChatMessageButton.test.tsx +47 -0
  46. package/src/components/storyboard/AgentChatMessageButton.tsx +45 -0
  47. package/src/components/storyboard/StoryboardFrameFocus.tsx +189 -66
  48. package/src/components/storyboard/StoryboardLoaded.tsx +121 -23
  49. package/src/components/storyboard/StoryboardReviewGuide.tsx +300 -0
  50. package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +170 -0
  51. package/src/components/storyboard/storyboardReviewStage.test.ts +40 -0
  52. package/src/components/storyboard/storyboardReviewStage.ts +45 -0
  53. package/src/components/storyboard/useFrameComments.ts +22 -6
  54. package/src/contexts/StudioContext.tsx +4 -0
  55. package/src/contexts/ViewModeContext.tsx +60 -7
  56. package/src/hooks/timelineEditingHelpers.ts +10 -4
  57. package/src/hooks/useAddAssetAtPlayhead.test.ts +44 -0
  58. package/src/hooks/useAddAssetAtPlayhead.ts +21 -0
  59. package/src/hooks/usePreviewPersistence.ts +7 -1
  60. package/src/hooks/useRazorSplit.history.test.tsx +21 -28
  61. package/src/hooks/useRazorSplit.test.tsx +20 -9
  62. package/src/hooks/useRazorSplit.testHelpers.ts +34 -22
  63. package/src/hooks/useRazorSplit.ts +78 -350
  64. package/src/hooks/useSlideshowTabState.test.ts +96 -0
  65. package/src/hooks/useSlideshowTabState.ts +61 -0
  66. package/src/hooks/useTimelineAddAtPlayhead.ts +15 -0
  67. package/src/hooks/useTimelineAssetDropOps.ts +52 -2
  68. package/src/hooks/useTimelineEditing.ts +19 -17
  69. package/src/hooks/useTimelineGroupEditing.ts +6 -7
  70. package/src/player/components/Timeline.tsx +12 -1
  71. package/src/player/components/timelineAuthoredMoveTarget.ts +77 -0
  72. package/src/player/components/timelineAuthoredTrack.ts +29 -0
  73. package/src/player/components/timelineCallbacks.ts +4 -0
  74. package/src/player/components/timelineClipDragCommit.test.ts +176 -0
  75. package/src/player/components/timelineClipDragCommit.ts +77 -105
  76. package/src/player/components/timelineClipDragPreview.test.ts +33 -1
  77. package/src/player/components/timelineClipDragPreview.ts +2 -1
  78. package/src/player/components/timelineCollision.test.ts +18 -0
  79. package/src/player/components/timelineCollision.ts +12 -9
  80. package/src/player/components/timelineDragDrop.ts +28 -7
  81. package/src/player/components/timelineEditCapabilities.ts +4 -1
  82. package/src/player/components/timelineGroupEditing.test.ts +10 -0
  83. package/src/player/components/timelineGroupEditing.ts +4 -3
  84. package/src/player/components/timelineLaneMoveRefresh.ts +14 -0
  85. package/src/player/components/timelineLayout.ts +3 -1
  86. package/src/player/components/timelineZMirror.ts +1 -1
  87. package/src/player/components/useTimelineClipDrag.ts +5 -0
  88. package/src/player/components/useTimelineEditPinning.ts +12 -0
  89. package/src/player/hooks/useExpandedTimelineElements.test.ts +42 -0
  90. package/src/player/hooks/useExpandedTimelineElements.ts +3 -0
  91. package/src/player/lib/playbackTypes.ts +9 -0
  92. package/src/player/lib/runtimeProtocol.test.ts +6 -1
  93. package/src/player/lib/timelineDOM.test.ts +58 -0
  94. package/src/player/lib/timelineDOM.ts +26 -12
  95. package/src/player/lib/timelineElementHelpers.ts +21 -11
  96. package/src/player/lib/timelineIframeHelpers.ts +153 -107
  97. package/src/player/store/playerStore.ts +2 -1
  98. package/src/utils/razorSplitTransaction.test.ts +175 -0
  99. package/src/utils/razorSplitTransaction.ts +200 -0
  100. package/src/utils/studioPreviewHelpers.test.ts +27 -0
  101. package/src/utils/timelineCompositionDrop.test.ts +12 -0
  102. package/src/utils/timelineCompositionDrop.ts +16 -0
  103. package/src/utils/timelineCompositionInsert.test.ts +142 -0
  104. package/src/utils/timelineCompositionInsert.ts +90 -0
  105. package/src/utils/timelineElementSplit.test.ts +14 -2
  106. package/src/utils/timelineElementSplit.ts +5 -1
  107. package/dist/assets/hyperframes-player-3XTTaVNf.js +0 -459
  108. package/dist/assets/index-DXbu6IPT.css +0 -1
  109. package/dist/assets/index-cH6NfVV_.js +0 -426
  110. 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
- onClick={onSelect}
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-pointer ${
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
+ }