@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.7.
|
|
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.
|
|
50
|
-
"@hyperframes/player": "0.7.
|
|
51
|
-
"@hyperframes/
|
|
52
|
-
"@hyperframes/parsers": "0.7.
|
|
53
|
-
"@hyperframes/
|
|
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.
|
|
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
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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<
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
|
|
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
|
|
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>
|