@hyperframes/studio 0.7.59 → 0.7.61

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 (78) hide show
  1. package/dist/assets/hyperframes-player-Xvx2hkrc.js +459 -0
  2. package/dist/assets/{index-CrkAdJkb.js → index-2mxh_HSy.js} +201 -201
  3. package/dist/assets/{index-B995FG46.js → index-BCpoiv9S.js} +1 -1
  4. package/dist/assets/index-BhWig0mx.css +1 -0
  5. package/dist/assets/{index-FvzmPhfG.js → index-D-uFclFj.js} +1 -1
  6. package/dist/index.html +2 -2
  7. package/dist/index.js +1441 -770
  8. package/dist/index.js.map +1 -1
  9. package/package.json +7 -7
  10. package/src/App.tsx +4 -9
  11. package/src/components/DesignPanelPromoteProvider.tsx +27 -1
  12. package/src/components/StudioHeader.tsx +2 -3
  13. package/src/components/StudioRightPanel.tsx +47 -28
  14. package/src/components/editor/PromotableControl.tsx +4 -2
  15. package/src/components/editor/PropertyPanel.tsx +151 -149
  16. package/src/components/editor/PropertyPanelFlat.tsx +44 -42
  17. package/src/components/editor/manualEditingAvailability.test.ts +6 -6
  18. package/src/components/editor/manualEditingAvailability.ts +5 -3
  19. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +2 -2
  20. package/src/components/editor/propertyPanelFlatMotionSection.tsx +1 -1
  21. package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +10 -0
  22. package/src/components/editor/propertyPanelFlatPrimitives.tsx +3 -2
  23. package/src/components/editor/propertyPanelFlatSelectRow.tsx +7 -1
  24. package/src/components/editor/propertyPanelFlatStyleHelpers.ts +0 -13
  25. package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +21 -9
  26. package/src/components/editor/propertyPanelFlatStyleSections.tsx +15 -32
  27. package/src/components/editor/propertyPanelFlatTextSection.tsx +3 -3
  28. package/src/components/editor/propertyPanelInputCoverage.test.tsx +14 -0
  29. package/src/components/editor/propertyPanelPrimitives.tsx +9 -1
  30. package/src/components/storyboard/AgentChatMessageButton.test.tsx +47 -0
  31. package/src/components/storyboard/AgentChatMessageButton.tsx +45 -0
  32. package/src/components/storyboard/StoryboardFrameFocus.tsx +189 -66
  33. package/src/components/storyboard/StoryboardLoaded.tsx +121 -23
  34. package/src/components/storyboard/StoryboardReviewGuide.tsx +300 -0
  35. package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +170 -0
  36. package/src/components/storyboard/storyboardReviewStage.test.ts +40 -0
  37. package/src/components/storyboard/storyboardReviewStage.ts +45 -0
  38. package/src/components/storyboard/useFrameComments.ts +22 -6
  39. package/src/contexts/FileManagerContext.tsx +3 -0
  40. package/src/contexts/PanelLayoutContext.tsx +3 -0
  41. package/src/contexts/ViewModeContext.tsx +60 -7
  42. package/src/hooks/timelineEditingHelpers.ts +2 -2
  43. package/src/hooks/useAddAssetAtPlayhead.test.ts +44 -0
  44. package/src/hooks/useAddAssetAtPlayhead.ts +21 -0
  45. package/src/hooks/useDomEditCommits.test.tsx +41 -1
  46. package/src/hooks/useDomEditCommits.ts +2 -2
  47. package/src/hooks/useDomEditSession.ts +1 -1
  48. package/src/hooks/useEditorSave.ts +1 -1
  49. package/src/hooks/useFileManager.projectOwnership.test.tsx +14 -3
  50. package/src/hooks/useFileManager.ts +76 -12
  51. package/src/hooks/usePanelLayout.test.ts +118 -0
  52. package/src/hooks/usePanelLayout.ts +24 -1
  53. package/src/hooks/usePreviewPersistence.ts +4 -1
  54. package/src/hooks/useRazorSplit.history.test.tsx +12 -4
  55. package/src/hooks/useRazorSplit.test.tsx +88 -0
  56. package/src/hooks/useRazorSplit.testHelpers.ts +3 -2
  57. package/src/hooks/useRazorSplit.ts +50 -16
  58. package/src/hooks/useSlideshowTabState.test.ts +96 -0
  59. package/src/hooks/useSlideshowTabState.ts +61 -0
  60. package/src/hooks/useTimelineEditing.ts +2 -0
  61. package/src/hooks/useTimelineEditingTypes.ts +2 -1
  62. package/src/hooks/useTimelineGroupEditing.ts +1 -1
  63. package/src/player/lib/playbackTypes.ts +7 -0
  64. package/src/player/lib/runtimeProtocol.test.ts +6 -1
  65. package/src/player/lib/timelineDOM.ts +8 -11
  66. package/src/player/lib/timelineIframeHelpers.ts +153 -107
  67. package/src/utils/domEditSaveQueue.test.ts +19 -0
  68. package/src/utils/domEditSaveQueue.ts +2 -1
  69. package/src/utils/sdkCutover.test.ts +21 -5
  70. package/src/utils/sdkEditTransaction.ts +5 -4
  71. package/src/utils/studioFileHistory.ts +6 -4
  72. package/src/utils/studioFileVersion.test.ts +36 -0
  73. package/src/utils/studioFileVersion.ts +23 -0
  74. package/src/utils/studioSaveDiagnostics.test.ts +18 -0
  75. package/src/utils/studioSaveDiagnostics.ts +21 -0
  76. package/dist/assets/hyperframes-player-Bm07FLkl.js +0 -459
  77. package/dist/assets/index-Dj5p8U_A.css +0 -1
  78. package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +0 -23
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.7.59",
3
+ "version": "0.7.61",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -46,11 +46,11 @@
46
46
  "gsap": "^3.13.0",
47
47
  "marked": "^14.1.4",
48
48
  "mediabunny": "^1.45.3",
49
- "@hyperframes/core": "0.7.59",
50
- "@hyperframes/studio-server": "0.7.59",
51
- "@hyperframes/sdk": "0.7.59",
52
- "@hyperframes/parsers": "0.7.59",
53
- "@hyperframes/player": "0.7.59"
49
+ "@hyperframes/core": "0.7.61",
50
+ "@hyperframes/parsers": "0.7.61",
51
+ "@hyperframes/player": "0.7.61",
52
+ "@hyperframes/sdk": "0.7.61",
53
+ "@hyperframes/studio-server": "0.7.61"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/react": "19",
@@ -65,7 +65,7 @@
65
65
  "vite": "^6.4.2",
66
66
  "vitest": "^3.2.4",
67
67
  "zustand": "^5.0.0",
68
- "@hyperframes/producer": "0.7.59"
68
+ "@hyperframes/producer": "0.7.61"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "react": "19",
package/src/App.tsx CHANGED
@@ -23,6 +23,7 @@ import { useDomEditSession } from "./hooks/useDomEditSession";
23
23
  import { useSdkSelectionSync } from "./hooks/useSdkSelectionSync";
24
24
  import { useStudioSdkSessions } from "./hooks/useStudioSdkSessions";
25
25
  import { useBlockHandlers } from "./hooks/useBlockHandlers";
26
+ import { useAddAssetAtPlayhead } from "./hooks/useAddAssetAtPlayhead";
26
27
  import { useAppHotkeys } from "./hooks/useAppHotkeys";
27
28
  import { useClipboard } from "./hooks/useClipboard";
28
29
  import { deleteSelectedKeyframes } from "./hooks/timelineEditingHelpers";
@@ -64,7 +65,6 @@ import {
64
65
  } from "./utils/studioUrlState";
65
66
  import { trackStudioSessionStart } from "./telemetry/events";
66
67
  import { hasFiredSessionStart, markSessionStartFired } from "./telemetry/config";
67
-
68
68
  type CanvasRect = { left: number; top: number; width: number; height: number };
69
69
  // fallow-ignore-next-line complexity
70
70
  export function StudioApp() {
@@ -166,6 +166,7 @@ export function StudioApp() {
166
166
  timelineElements,
167
167
  showToast,
168
168
  writeProjectFile: fileManager.writeProjectFile,
169
+ observeProjectFileVersion: fileManager.observeProjectFileVersion,
169
170
  recordEdit: editHistory.recordEdit,
170
171
  domEditSaveTimestampRef,
171
172
  reloadPreview,
@@ -195,14 +196,7 @@ export function StudioApp() {
195
196
  },
196
197
  [timelineEditing.handleTimelineGroupMove],
197
198
  );
198
- const handleAddAssetAtPlayhead = useCallback(
199
- (assetPath: string) =>
200
- timelineEditing.handleTimelineAssetDrop(assetPath, {
201
- start: usePlayerStore.getState().currentTime,
202
- track: 0,
203
- }),
204
- [timelineEditing],
205
- );
199
+ const handleAddAssetAtPlayhead = useAddAssetAtPlayhead(timelineEditing.handleTimelineAssetDrop);
206
200
  const {
207
201
  activeBlockParams,
208
202
  setActiveBlockParams,
@@ -533,6 +527,7 @@ export function StudioApp() {
533
527
  onToggleRecording={recordingToggle}
534
528
  sdkSession={sdkHandle.session}
535
529
  publishSdkSession={sdkHandle.publish}
530
+ forceReloadSdkSession={sdkHandle.forceReload}
536
531
  reloadPreview={reloadPreview}
537
532
  domEditSaveTimestampRef={domEditSaveTimestampRef}
538
533
  recordEdit={editHistory.recordEdit}
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import { useCallback, type ReactNode } from "react";
13
+ import type { Composition } from "@hyperframes/sdk";
13
14
  import type { DomEditSelection } from "./editor/domEditingTypes";
14
15
  import { useSdkSession } from "../hooks/useSdkSession";
15
16
  import { useVariablesPersist, type UseVariablesPersistParams } from "../hooks/useVariablesPersist";
@@ -24,6 +25,7 @@ export function DesignPanelPromoteProvider({
24
25
  projectId,
25
26
  activeCompPath,
26
27
  showToast,
28
+ forceReloadSharedSdkSession,
27
29
  children,
28
30
  ...persistDeps
29
31
  }: PersistDeps & {
@@ -31,16 +33,40 @@ export function DesignPanelPromoteProvider({
31
33
  projectId: string | null;
32
34
  activeCompPath: string | null;
33
35
  showToast: (message: string, tone?: "error" | "info") => void;
36
+ /**
37
+ * Forces the app's SHARED SDK session (Variables tab, Slideshow, etc.) to
38
+ * re-open from disk. This provider opens its OWN session, separate from
39
+ * that shared one, so a persist through it never fires the shared session's
40
+ * own "change" event. When the target happens to be the same file the
41
+ * shared session already has open, that session is left holding stale
42
+ * in-memory content after a successful persist — worse, the self-write-echo
43
+ * registry that would normally reload it on the next file-change
44
+ * notification is keyed by file path only (not by session instance), so it
45
+ * mistakes this provider's write for its own echo and stays stale
46
+ * indefinitely. Called unconditionally (not gated on targetPath matching
47
+ * activeCompPath): re-opening a file that didn't change is a harmless
48
+ * no-op re-parse, cheaper than the bug class a subtly-wrong path guard
49
+ * could reintroduce.
50
+ */
51
+ forceReloadSharedSdkSession?: () => void;
34
52
  children: ReactNode;
35
53
  }) {
36
54
  const targetPath = selection?.sourceFile || activeCompPath || "index.html";
37
55
  const handle = useSdkSession(projectId, targetPath, persistDeps.domEditSaveTimestampRef);
38
- const persist = useVariablesPersist({
56
+ const rawPersist = useVariablesPersist({
39
57
  ...persistDeps,
40
58
  sdkSession: handle.session,
41
59
  publishSdkSession: handle.publish,
42
60
  activeCompPath: targetPath,
43
61
  });
62
+ const persist = useCallback(
63
+ async (label: string, mutate: (session: Composition) => void) => {
64
+ const committed = await rawPersist(label, mutate);
65
+ if (committed) forceReloadSharedSdkSession?.();
66
+ return committed;
67
+ },
68
+ [rawPersist, forceReloadSharedSdkSession],
69
+ );
44
70
  const handlePersistError = useCallback(
45
71
  (error: unknown) => showToast(getStudioSaveErrorMessage(error), "error"),
46
72
  [showToast],
@@ -149,14 +149,13 @@ const VIEW_MODE_OPTIONS: Array<{ mode: StudioViewMode; label: string }> = [
149
149
  ];
150
150
 
151
151
  /** Segmented control switching the main stage between storyboard and preview. */
152
- function ViewModeToggle() {
152
+ export function ViewModeToggle() {
153
153
  const { viewMode, setViewMode } = useViewMode();
154
154
  const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);
155
155
 
156
156
  const selectMode = (mode: StudioViewMode) => {
157
157
  if (mode === viewMode) return;
158
- trackStudioEvent("view_mode_toggle", { mode });
159
- setViewMode(mode);
158
+ if (setViewMode(mode)) trackStudioEvent("view_mode_toggle", { mode });
160
159
  };
161
160
 
162
161
  // Complete APG tabs pattern: roving tabIndex + arrow-key navigation.
@@ -1,21 +1,23 @@
1
- import { useCallback, useEffect, useMemo, useRef, type MutableRefObject } from "react";
1
+ import { useCallback, useEffect, useRef, type MutableRefObject } from "react";
2
2
  import { PropertyPanel } from "./editor/PropertyPanel";
3
3
  import { LayersPanel } from "./editor/LayersPanel";
4
4
  import { CaptionPropertyPanel } from "../captions/components/CaptionPropertyPanel";
5
5
  import { BlockParamsPanel } from "./editor/BlockParamsPanel";
6
6
  import { RenderQueue } from "./renders/RenderQueue";
7
7
  import { SlideshowPanel } from "./panels/SlideshowPanel";
8
- import type { SceneInfo } from "./panels/SlideshowPanel";
9
8
  import { VariablesPanel } from "./panels/VariablesPanel";
10
9
  import { PanelTabButton } from "./PanelTabButton";
11
10
  import { usePreviewVariablesStore } from "../hooks/previewVariablesStore";
12
11
  import type { RenderJob } from "./renders/useRenderQueue";
13
12
  import type { BlockParam } from "@hyperframes/core/registry";
14
- import type { IframeWindow } from "../player/lib/playbackTypes";
15
- import { STUDIO_INSPECTOR_PANELS_ENABLED } from "./editor/manualEditingAvailability";
13
+ import {
14
+ STUDIO_FLAT_INSPECTOR_ENABLED,
15
+ STUDIO_INSPECTOR_PANELS_ENABLED,
16
+ } from "./editor/manualEditingAvailability";
16
17
  import type { Composition } from "@hyperframes/sdk";
17
18
  import type { EditHistoryKind } from "../utils/editHistory";
18
19
  import { useSlideshowPersist, type UseSlideshowPersistParams } from "../hooks/useSlideshowPersist";
20
+ import { useSlideshowTabState } from "../hooks/useSlideshowTabState";
19
21
  import { DesignPanelPromoteProvider } from "./DesignPanelPromoteProvider";
20
22
 
21
23
  import { useStudioPlaybackContext, useStudioShellContext } from "../contexts/StudioContext";
@@ -48,6 +50,19 @@ export interface StudioRightPanelProps {
48
50
  /** Dependencies for the Slideshow persist callback, threaded from App.tsx. */
49
51
  sdkSession: Composition | null;
50
52
  publishSdkSession: NonNullable<UseSlideshowPersistParams["publishSdkSession"]>;
53
+ /**
54
+ * Forces THIS `sdkSession` to re-open from disk. DesignPanelPromoteProvider
55
+ * opens its own separate SDK session scoped to the selected element's own
56
+ * file (needed so promoting inside a sub-composition binds a variable there,
57
+ * not on the host) — for a top-level selection that's the SAME file this
58
+ * session already has open, so a write through that other session leaves
59
+ * this one holding stale in-memory content. The self-write-echo registry
60
+ * that normally suppresses redundant reloads is keyed by file path only, not
61
+ * by session instance, so it wrongly treats the sibling session's write as
62
+ * "our own echo" and never reloads on its own — this must be called
63
+ * explicitly after such a write.
64
+ */
65
+ forceReloadSdkSession?: () => void;
51
66
  reloadPreview: () => void;
52
67
  domEditSaveTimestampRef: MutableRefObject<number>;
53
68
  recordEdit: (entry: {
@@ -68,6 +83,7 @@ export function StudioRightPanel({
68
83
  onToggleRecording,
69
84
  sdkSession,
70
85
  publishSdkSession,
86
+ forceReloadSdkSession,
71
87
  reloadPreview,
72
88
  domEditSaveTimestampRef,
73
89
  recordEdit,
@@ -80,6 +96,7 @@ export function StudioRightPanel({
80
96
  setRightPanelTab,
81
97
  rightInspectorPanes,
82
98
  toggleRightInspectorPane,
99
+ setExclusiveRightInspectorPane,
83
100
  handlePanelResizeStart,
84
101
  handlePanelResizeMove,
85
102
  handlePanelResizeEnd,
@@ -150,6 +167,7 @@ export function StudioRightPanel({
150
167
  readProjectFile,
151
168
  writeProjectFile,
152
169
  fileTree,
170
+ editingFile,
153
171
  } = useFileManagerContext();
154
172
 
155
173
  // Discrete ops (toggle, reorder, add/delete, hotspot): persist immediately,
@@ -198,22 +216,13 @@ export function StudioRightPanel({
198
216
  const renderJobs = renderQueue.jobs as RenderJob[];
199
217
  const inspectorTabActive = rightPanelTab === "design" || rightPanelTab === "layers";
200
218
 
201
- // Derive scene list from the live clip manifest in the preview iframe.
202
- // fallow-ignore-next-line complexity
203
- const slideshowScenes = useMemo<SceneInfo[]>(() => {
204
- try {
205
- const win = previewIframeRef.current?.contentWindow as IframeWindow | null;
206
- return (win?.__clipManifest?.scenes ?? []).map((s) => ({
207
- id: s.id,
208
- label: s.label,
209
- start: s.start,
210
- duration: s.duration,
211
- }));
212
- } catch {
213
- return [];
214
- }
215
- // eslint-disable-next-line react-hooks/exhaustive-deps
216
- }, [previewIframeRef, rightPanelTab, refreshKey]);
219
+ const { isSlideshowComposition, slideshowScenes } = useSlideshowTabState({
220
+ editingFileContent: editingFile?.content,
221
+ previewIframeRef,
222
+ refreshKey,
223
+ rightPanelTab,
224
+ setRightPanelTab,
225
+ });
217
226
  const designPaneOpen = inspectorTabActive && rightInspectorPanes.design && designPanelActive;
218
227
  const layersPaneOpen =
219
228
  inspectorTabActive && rightInspectorPanes.layers && STUDIO_INSPECTOR_PANELS_ENABLED;
@@ -223,6 +232,13 @@ export function StudioRightPanel({
223
232
  setRightPanelTab(pane);
224
233
  return;
225
234
  }
235
+ // Flat inspector: Layers always renders full-height by itself (see the
236
+ // render branch below), so the two panes are mutually exclusive here —
237
+ // otherwise both tabs could show "active" while only one actually shows.
238
+ if (STUDIO_FLAT_INSPECTOR_ENABLED) {
239
+ setExclusiveRightInspectorPane(pane);
240
+ return;
241
+ }
226
242
  toggleRightInspectorPane(pane);
227
243
  };
228
244
 
@@ -325,6 +341,7 @@ export function StudioRightPanel({
325
341
  recordEdit={recordEdit}
326
342
  reloadPreview={reloadPreview}
327
343
  domEditSaveTimestampRef={domEditSaveTimestampRef}
344
+ forceReloadSharedSdkSession={forceReloadSdkSession}
328
345
  >
329
346
  <PropertyPanel
330
347
  projectId={projectId}
@@ -480,12 +497,14 @@ export function StudioRightPanel({
480
497
  active={rightPanelTab === "renders"}
481
498
  onClick={() => setRightPanelTab("renders")}
482
499
  />
483
- <PanelTabButton
484
- label="Slideshow"
485
- tooltip="Slideshow branching editor"
486
- active={rightPanelTab === "slideshow"}
487
- onClick={() => setRightPanelTab("slideshow")}
488
- />
500
+ {isSlideshowComposition && (
501
+ <PanelTabButton
502
+ label="Slideshow"
503
+ tooltip="Slideshow branching editor"
504
+ active={rightPanelTab === "slideshow"}
505
+ onClick={() => setRightPanelTab("slideshow")}
506
+ />
507
+ )}
489
508
  <PanelTabButton
490
509
  label="Variables"
491
510
  tooltip="Template variables — declare, preview with values"
@@ -502,7 +521,7 @@ export function StudioRightPanel({
502
521
  compositionPath={activeBlockParams.compositionPath}
503
522
  onClose={onCloseBlockParams ?? (() => {})}
504
523
  />
505
- ) : rightPanelTab === "slideshow" ? (
524
+ ) : rightPanelTab === "slideshow" && isSlideshowComposition ? (
506
525
  <SlideshowPanel
507
526
  scenes={slideshowScenes}
508
527
  onPersist={onPersistSlideshow}
@@ -516,7 +535,7 @@ export function StudioRightPanel({
516
535
  domEditSaveTimestampRef={domEditSaveTimestampRef}
517
536
  recordEdit={recordEdit}
518
537
  />
519
- ) : layersPaneOpen && designPaneOpen ? (
538
+ ) : layersPaneOpen && designPaneOpen && !STUDIO_FLAT_INSPECTOR_ENABLED ? (
520
539
  <div ref={splitContainerRef} className="flex h-full min-h-0 min-w-0 flex-col">
521
540
  <div
522
541
  className="min-h-[120px] overflow-hidden"
@@ -81,7 +81,9 @@ export function PromotableControl({
81
81
  {rendered}
82
82
  {bound && (
83
83
  <span
84
- className="pointer-events-none absolute right-1.5 top-0 z-10 inline-flex max-w-[60%] items-center gap-1 truncate rounded bg-studio-accent/20 px-1 py-px font-mono text-[8px] font-medium text-studio-accent"
84
+ // Sits above the row (not on top of top-0) so it clears a value that
85
+ // renders flush to the row's right edge, e.g. flat Font/Color rows.
86
+ className="pointer-events-none absolute -top-2 right-1.5 z-10 inline-flex max-w-[60%] items-center gap-1 truncate rounded bg-studio-accent/20 px-1 py-px font-mono text-[8px] font-medium text-studio-accent"
85
87
  title={`Bound to variable "${promote.boundId}"`}
86
88
  >
87
89
  ◆ {promote.boundId}
@@ -95,7 +97,7 @@ export function PromotableControl({
95
97
  e.stopPropagation();
96
98
  promote.promote();
97
99
  }}
98
- className="absolute right-1.5 top-0 z-10 inline-flex items-center gap-1 rounded bg-neutral-800/80 px-1 py-px font-mono text-[8px] font-medium text-neutral-400 opacity-70 transition-colors hover:bg-studio-accent/20 hover:text-studio-accent hover:opacity-100"
100
+ className="absolute -top-2 right-1.5 z-10 inline-flex items-center gap-1 rounded bg-neutral-800/80 px-1 py-px font-mono text-[8px] font-medium text-neutral-400 opacity-70 transition-colors hover:bg-studio-accent/20 hover:text-studio-accent hover:opacity-100"
99
101
  >
100
102
  ◇ var
101
103
  </button>