@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.7.60",
3
+ "version": "0.7.62",
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.60",
50
- "@hyperframes/player": "0.7.60",
51
- "@hyperframes/sdk": "0.7.60",
52
- "@hyperframes/parsers": "0.7.60",
53
- "@hyperframes/studio-server": "0.7.60"
49
+ "@hyperframes/core": "0.7.62",
50
+ "@hyperframes/player": "0.7.62",
51
+ "@hyperframes/studio-server": "0.7.62",
52
+ "@hyperframes/parsers": "0.7.62",
53
+ "@hyperframes/sdk": "0.7.62"
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.60"
68
+ "@hyperframes/producer": "0.7.62"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "react": "19",
package/src/App.tsx CHANGED
@@ -57,6 +57,7 @@ import { FileManagerProvider } from "./contexts/FileManagerContext";
57
57
  import { DomEditProvider } from "./contexts/DomEditContext";
58
58
  import { StudioSplash } from "./components/StudioSplash";
59
59
  import { useServerConnection } from "./hooks/useServerConnection";
60
+ import { useTimelineAddAtPlayhead } from "./hooks/useTimelineAddAtPlayhead";
60
61
  import {
61
62
  normalizeStudioCompositionPath,
62
63
  readStudioUrlStateFromWindow,
@@ -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
- type CanvasRect = { left: number; top: number; width: number; height: number };
68
68
  // fallow-ignore-next-line complexity
69
69
  export function StudioApp() {
70
70
  const { projectId, resolving, waitingForServer } = useServerConnection();
@@ -99,9 +99,6 @@ export function StudioApp() {
99
99
  const timelineDuration = usePlayerStore((s) => s.duration);
100
100
  const isPlaying = usePlayerStore((s) => s.isPlaying);
101
101
  const isMasterView = !activeCompPath || activeCompPath === "index.html";
102
- const activePreviewUrl = activeCompPath
103
- ? `/api/projects/${projectId}/preview/comp/${activeCompPath}`
104
- : null;
105
102
  const effectiveTimelineDuration = useMemo(() => {
106
103
  const maxEnd =
107
104
  timelineElements.length > 0
@@ -195,13 +192,12 @@ export function StudioApp() {
195
192
  },
196
193
  [timelineEditing.handleTimelineGroupMove],
197
194
  );
198
- const handleAddAssetAtPlayhead = useCallback(
199
- (assetPath: string) =>
200
- timelineEditing.handleTimelineAssetDrop(assetPath, {
201
- start: usePlayerStore.getState().currentTime,
202
- track: 0,
203
- }),
204
- [timelineEditing],
195
+ const {
196
+ addAssetAtPlayhead: handleAddAssetAtPlayhead,
197
+ addCompositionAtPlayhead: handleAddCompositionAtPlayhead,
198
+ } = useTimelineAddAtPlayhead(
199
+ timelineEditing.handleTimelineAssetDrop,
200
+ timelineEditing.handleTimelineCompositionDrop,
205
201
  );
206
202
  const {
207
203
  activeBlockParams,
@@ -344,7 +340,9 @@ export function StudioApp() {
344
340
  const renderClipContent = useRenderClipContent({
345
341
  projectIdRef: fileManager.projectIdRef,
346
342
  compIdToSrc,
347
- activePreviewUrl,
343
+ activePreviewUrl: activeCompPath
344
+ ? `/api/projects/${projectId}/preview/comp/${activeCompPath}`
345
+ : null,
348
346
  effectiveTimelineDuration,
349
347
  });
350
348
  const compositionDimensions = useCompositionDimensions();
@@ -375,14 +373,13 @@ export function StudioApp() {
375
373
  });
376
374
  handleToggleRecordingRef.current = handleToggleRecording;
377
375
  const recordingToggle = STUDIO_KEYFRAMES_ENABLED ? handleToggleRecording : undefined;
378
- const canvasRectRef = useRef<CanvasRect | null>(null);
376
+ const canvasRectRef = useRef<DOMRect | null>(null);
379
377
  useLayoutEffect(() => {
380
378
  if (gestureState !== "recording" || !previewIframe) {
381
379
  canvasRectRef.current = null;
382
380
  return;
383
381
  }
384
- const r = previewIframe.getBoundingClientRect();
385
- canvasRectRef.current = { left: r.left, top: r.top, width: r.width, height: r.height };
382
+ canvasRectRef.current = previewIframe.getBoundingClientRect();
386
383
  }, [gestureState, previewIframe]);
387
384
  const handlePreviewIframeRef = useCallback(
388
385
  (iframe: HTMLIFrameElement | null) => {
@@ -517,6 +514,7 @@ export function StudioApp() {
517
514
  lintFindingCount={lintModal?.length ?? findingsByFile.size}
518
515
  lintFindingsByFile={findingsByFile}
519
516
  onAddAssetToTimeline={handleAddAssetAtPlayhead}
517
+ onAddCompositionToTimeline={handleAddCompositionAtPlayhead}
520
518
  />
521
519
  }
522
520
  right={
@@ -533,6 +531,7 @@ export function StudioApp() {
533
531
  onToggleRecording={recordingToggle}
534
532
  sdkSession={sdkHandle.session}
535
533
  publishSdkSession={sdkHandle.publish}
534
+ forceReloadSdkSession={sdkHandle.forceReload}
536
535
  reloadPreview={reloadPreview}
537
536
  domEditSaveTimestampRef={domEditSaveTimestampRef}
538
537
  recordEdit={editHistory.recordEdit}
@@ -545,6 +544,7 @@ export function StudioApp() {
545
544
  handleTimelineElementDelete={timelineEditing.handleTimelineElementDelete}
546
545
  handleTimelineAssetDrop={timelineEditing.handleTimelineAssetDrop}
547
546
  handleTimelineBlockDrop={handleTimelineBlockDrop}
547
+ handleTimelineCompositionDrop={timelineEditing.handleTimelineCompositionDrop}
548
548
  handlePreviewBlockDrop={handlePreviewBlockDrop}
549
549
  handleTimelineFileDrop={timelineEditing.handleTimelineFileDrop}
550
550
  handleTimelineElementMove={timelineEditing.handleTimelineElementMove}
@@ -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],
@@ -42,6 +42,10 @@ export interface EditorShellProps extends TimelineEditCallbackDeps {
42
42
  blockName: string,
43
43
  placement: TimelineDropPlacement,
44
44
  ) => Promise<void> | void;
45
+ handleTimelineCompositionDrop?: (
46
+ sourcePath: string,
47
+ placement: TimelineDropPlacement,
48
+ ) => Promise<void> | void;
45
49
  handlePreviewBlockDrop?: (
46
50
  blockName: string,
47
51
  position: { left: number; top: number },
@@ -72,6 +76,7 @@ export function EditorShell({
72
76
  handleTimelineElementDelete,
73
77
  handleTimelineAssetDrop,
74
78
  handleTimelineBlockDrop,
79
+ handleTimelineCompositionDrop,
75
80
  handlePreviewBlockDrop,
76
81
  handleTimelineFileDrop,
77
82
  handleTimelineElementMove,
@@ -140,6 +145,7 @@ export function EditorShell({
140
145
  onFileDrop={handleTimelineFileDrop}
141
146
  onAssetDrop={handleTimelineAssetDrop}
142
147
  onBlockDrop={handleTimelineBlockDrop}
148
+ onCompositionDrop={handleTimelineCompositionDrop}
143
149
  onDeleteElement={handleTimelineElementDelete}
144
150
  previewOverlay={
145
151
  <PreviewOverlays
@@ -174,6 +180,10 @@ interface EditorShellBodyProps {
174
180
  onFileDrop: (files: File[], placement?: TimelineDropPlacement) => Promise<void> | void;
175
181
  onAssetDrop: (assetPath: string, placement: TimelineDropPlacement) => Promise<void> | void;
176
182
  onBlockDrop?: (blockName: string, placement: TimelineDropPlacement) => Promise<void> | void;
183
+ onCompositionDrop?: (
184
+ sourcePath: string,
185
+ placement: TimelineDropPlacement,
186
+ ) => Promise<void> | void;
177
187
  onDeleteElement: (element: TimelineElement) => Promise<void> | void;
178
188
  }
179
189
 
@@ -189,6 +199,7 @@ function EditorShellBody({
189
199
  onFileDrop,
190
200
  onAssetDrop,
191
201
  onBlockDrop,
202
+ onCompositionDrop,
192
203
  onDeleteElement,
193
204
  }: EditorShellBodyProps) {
194
205
  const { compositionStack, updateCompositionStack, containerRef } = useNLEContext();
@@ -233,6 +244,7 @@ function EditorShellBody({
233
244
  onFileDrop={onFileDrop}
234
245
  onAssetDrop={onAssetDrop}
235
246
  onBlockDrop={onBlockDrop}
247
+ onCompositionDrop={onCompositionDrop}
236
248
  onDeleteElement={onDeleteElement}
237
249
  onSelectTimelineElement={onSelectTimelineElement}
238
250
  timelineFooter={
@@ -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.
@@ -19,6 +19,7 @@ export interface StudioLeftSidebarProps {
19
19
  lintFindingCount?: number;
20
20
  lintFindingsByFile?: Map<string, { count: number; messages: string[] }>;
21
21
  onAddAssetToTimeline?: (path: string) => void;
22
+ onAddCompositionToTimeline?: (path: string) => void;
22
23
  }
23
24
 
24
25
  // fallow-ignore-next-line complexity
@@ -32,6 +33,7 @@ export function StudioLeftSidebar({
32
33
  lintFindingCount,
33
34
  lintFindingsByFile,
34
35
  onAddAssetToTimeline,
36
+ onAddCompositionToTimeline,
35
37
  }: StudioLeftSidebarProps) {
36
38
  const {
37
39
  leftCollapsed,
@@ -150,6 +152,7 @@ export function StudioLeftSidebar({
150
152
  onAddBlock={onAddBlock}
151
153
  onPreviewBlock={onPreviewBlock}
152
154
  onAddAssetToTimeline={onAddAssetToTimeline}
155
+ onAddCompositionToTimeline={onAddCompositionToTimeline}
153
156
  />
154
157
  {/* Vertical resize divider: 3px visible seam, 8px pointer-capture zone via
155
158
  the absolutely-positioned inner hit area. The outer element is w-[3px] so
@@ -1,17 +1,15 @@
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
13
  import {
16
14
  STUDIO_FLAT_INSPECTOR_ENABLED,
17
15
  STUDIO_INSPECTOR_PANELS_ENABLED,
@@ -19,6 +17,7 @@ import {
19
17
  import type { Composition } from "@hyperframes/sdk";
20
18
  import type { EditHistoryKind } from "../utils/editHistory";
21
19
  import { useSlideshowPersist, type UseSlideshowPersistParams } from "../hooks/useSlideshowPersist";
20
+ import { useSlideshowTabState } from "../hooks/useSlideshowTabState";
22
21
  import { DesignPanelPromoteProvider } from "./DesignPanelPromoteProvider";
23
22
 
24
23
  import { useStudioPlaybackContext, useStudioShellContext } from "../contexts/StudioContext";
@@ -51,6 +50,19 @@ export interface StudioRightPanelProps {
51
50
  /** Dependencies for the Slideshow persist callback, threaded from App.tsx. */
52
51
  sdkSession: Composition | null;
53
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;
54
66
  reloadPreview: () => void;
55
67
  domEditSaveTimestampRef: MutableRefObject<number>;
56
68
  recordEdit: (entry: {
@@ -71,6 +83,7 @@ export function StudioRightPanel({
71
83
  onToggleRecording,
72
84
  sdkSession,
73
85
  publishSdkSession,
86
+ forceReloadSdkSession,
74
87
  reloadPreview,
75
88
  domEditSaveTimestampRef,
76
89
  recordEdit,
@@ -154,6 +167,7 @@ export function StudioRightPanel({
154
167
  readProjectFile,
155
168
  writeProjectFile,
156
169
  fileTree,
170
+ editingFile,
157
171
  } = useFileManagerContext();
158
172
 
159
173
  // Discrete ops (toggle, reorder, add/delete, hotspot): persist immediately,
@@ -202,22 +216,13 @@ export function StudioRightPanel({
202
216
  const renderJobs = renderQueue.jobs as RenderJob[];
203
217
  const inspectorTabActive = rightPanelTab === "design" || rightPanelTab === "layers";
204
218
 
205
- // Derive scene list from the live clip manifest in the preview iframe.
206
- // fallow-ignore-next-line complexity
207
- const slideshowScenes = useMemo<SceneInfo[]>(() => {
208
- try {
209
- const win = previewIframeRef.current?.contentWindow as IframeWindow | null;
210
- return (win?.__clipManifest?.scenes ?? []).map((s) => ({
211
- id: s.id,
212
- label: s.label,
213
- start: s.start,
214
- duration: s.duration,
215
- }));
216
- } catch {
217
- return [];
218
- }
219
- // eslint-disable-next-line react-hooks/exhaustive-deps
220
- }, [previewIframeRef, rightPanelTab, refreshKey]);
219
+ const { isSlideshowComposition, slideshowScenes } = useSlideshowTabState({
220
+ editingFileContent: editingFile?.content,
221
+ previewIframeRef,
222
+ refreshKey,
223
+ rightPanelTab,
224
+ setRightPanelTab,
225
+ });
221
226
  const designPaneOpen = inspectorTabActive && rightInspectorPanes.design && designPanelActive;
222
227
  const layersPaneOpen =
223
228
  inspectorTabActive && rightInspectorPanes.layers && STUDIO_INSPECTOR_PANELS_ENABLED;
@@ -336,6 +341,7 @@ export function StudioRightPanel({
336
341
  recordEdit={recordEdit}
337
342
  reloadPreview={reloadPreview}
338
343
  domEditSaveTimestampRef={domEditSaveTimestampRef}
344
+ forceReloadSharedSdkSession={forceReloadSdkSession}
339
345
  >
340
346
  <PropertyPanel
341
347
  projectId={projectId}
@@ -491,12 +497,14 @@ export function StudioRightPanel({
491
497
  active={rightPanelTab === "renders"}
492
498
  onClick={() => setRightPanelTab("renders")}
493
499
  />
494
- <PanelTabButton
495
- label="Slideshow"
496
- tooltip="Slideshow branching editor"
497
- active={rightPanelTab === "slideshow"}
498
- onClick={() => setRightPanelTab("slideshow")}
499
- />
500
+ {isSlideshowComposition && (
501
+ <PanelTabButton
502
+ label="Slideshow"
503
+ tooltip="Slideshow branching editor"
504
+ active={rightPanelTab === "slideshow"}
505
+ onClick={() => setRightPanelTab("slideshow")}
506
+ />
507
+ )}
500
508
  <PanelTabButton
501
509
  label="Variables"
502
510
  tooltip="Template variables — declare, preview with values"
@@ -513,7 +521,7 @@ export function StudioRightPanel({
513
521
  compositionPath={activeBlockParams.compositionPath}
514
522
  onClose={onCloseBlockParams ?? (() => {})}
515
523
  />
516
- ) : rightPanelTab === "slideshow" ? (
524
+ ) : rightPanelTab === "slideshow" && isSlideshowComposition ? (
517
525
  <SlideshowPanel
518
526
  scenes={slideshowScenes}
519
527
  onPersist={onPersistSlideshow}
@@ -9,6 +9,57 @@ import { DomEditSelectionChrome } from "./DomEditSelectionChrome";
9
9
  (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
10
10
 
11
11
  describe("DomEditSelectionChrome crop composition", () => {
12
+ it("renders overlay-only transparent chrome at headline geometry without changing composition bytes", () => {
13
+ const composition = document.implementation.createHTMLDocument();
14
+ composition.body.innerHTML = `
15
+ <section class="hl-block"><div class="hl-mask" style="overflow:hidden;background:transparent">
16
+ <h1 class="hl-text">Launch title</h1>
17
+ </div></section>
18
+ `;
19
+ const headline = composition.querySelector<HTMLElement>(".hl-text")!;
20
+ const before = composition.documentElement.outerHTML;
21
+ const selection = {
22
+ element: headline,
23
+ selector: ".hl-text",
24
+ capabilities: {
25
+ canCrop: false,
26
+ canApplyManualOffset: false,
27
+ canApplyManualSize: false,
28
+ canApplyManualRotation: false,
29
+ },
30
+ } as unknown as DomEditSelection;
31
+ const host = document.createElement("div");
32
+ document.body.append(host);
33
+ const root = createRoot(host);
34
+ act(() => {
35
+ root.render(
36
+ <DomEditSelectionChrome
37
+ selection={selection}
38
+ overlayRect={{ left: 44, top: 52, width: 220, height: 48, editScaleX: 1, editScaleY: 1 }}
39
+ allowCanvasMovement={false}
40
+ boxRef={createRef()}
41
+ boxChromeClass="border border-studio-accent/80"
42
+ boxClipPath={undefined}
43
+ selectionKey="headline"
44
+ groupSelectionCount={0}
45
+ blockedMoveRef={createRef()}
46
+ gestures={{ startGesture: vi.fn() } as never}
47
+ onStyleCommit={vi.fn()}
48
+ onBoxMouseDown={vi.fn()}
49
+ onBoxClick={vi.fn()}
50
+ />,
51
+ );
52
+ });
53
+ const chrome = host.querySelector<HTMLElement>('[data-dom-edit-selection-box="true"]')!;
54
+ expect(chrome.style.cssText).toContain("left: 44px");
55
+ expect(chrome.style.cssText).toContain("width: 220px");
56
+ expect(chrome.style.background).toBe("");
57
+ expect(chrome.className).not.toMatch(/bg-/);
58
+ expect(composition.documentElement.outerHTML).toBe(before);
59
+ act(() => root.unmount());
60
+ host.remove();
61
+ });
62
+
12
63
  it("places rotated crop UI in exactly one oriented coordinate plane", () => {
13
64
  const element = document.createElement("div");
14
65
  element.id = "clip";
@@ -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>