@hyperframes/studio 0.6.0-alpha.8 → 0.6.0
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-DjsVzYFP.js → hyperframes-player-DOFETgjy.js} +1 -1
- package/dist/assets/index-D1JDq7Gg.css +1 -0
- package/dist/assets/index-DUqUmaoH.js +117 -0
- package/dist/favicon.svg +14 -0
- package/dist/index.html +3 -2
- package/package.json +9 -9
- package/src/App.tsx +428 -4299
- package/src/components/AskAgentModal.tsx +120 -0
- package/src/components/StudioHeader.tsx +133 -0
- package/src/components/StudioLeftSidebar.tsx +125 -0
- package/src/components/StudioPreviewArea.tsx +163 -0
- package/src/components/StudioRightPanel.tsx +198 -0
- package/src/components/TimelineToolbar.tsx +89 -0
- package/src/components/editor/DomEditOverlay.tsx +15 -1
- package/src/components/editor/PropertyPanel.test.ts +0 -49
- package/src/components/editor/PropertyPanel.tsx +132 -2763
- package/src/components/editor/domEditing.ts +38 -5
- package/src/components/editor/manualEditingAvailability.test.ts +2 -2
- package/src/components/editor/manualEditingAvailability.ts +1 -1
- package/src/components/editor/manualEdits.ts +32 -0
- package/src/components/editor/propertyPanelColor.tsx +371 -0
- package/src/components/editor/propertyPanelFill.tsx +421 -0
- package/src/components/editor/propertyPanelFont.tsx +455 -0
- package/src/components/editor/propertyPanelHelpers.ts +401 -0
- package/src/components/editor/propertyPanelPrimitives.tsx +357 -0
- package/src/components/editor/propertyPanelSections.tsx +453 -0
- package/src/components/editor/propertyPanelStyleSections.tsx +411 -0
- package/src/components/nle/NLELayout.tsx +8 -11
- package/src/components/nle/NLEPreview.tsx +3 -0
- package/src/components/renders/RenderQueue.tsx +102 -31
- package/src/components/renders/useRenderQueue.ts +8 -2
- package/src/components/sidebar/LeftSidebar.tsx +186 -186
- package/src/contexts/DomEditContext.tsx +137 -0
- package/src/contexts/FileManagerContext.tsx +110 -0
- package/src/contexts/PanelLayoutContext.tsx +68 -0
- package/src/contexts/StudioContext.tsx +135 -0
- package/src/hooks/useAppHotkeys.ts +326 -0
- package/src/hooks/useAskAgentModal.ts +162 -0
- package/src/hooks/useCaptionDetection.ts +132 -0
- package/src/hooks/useCompositionDimensions.ts +25 -0
- package/src/hooks/useConsoleErrorCapture.ts +60 -0
- package/src/hooks/useDomEditCommits.ts +437 -0
- package/src/hooks/useDomEditSession.ts +342 -0
- package/src/hooks/useDomEditTextCommits.ts +330 -0
- package/src/hooks/useDomSelection.ts +398 -0
- package/src/hooks/useFileManager.ts +431 -0
- package/src/hooks/useFrameCapture.ts +77 -0
- package/src/hooks/useLintModal.ts +35 -0
- package/src/hooks/useManifestPersistence.ts +492 -0
- package/src/hooks/usePanelLayout.ts +68 -0
- package/src/hooks/usePreviewInteraction.ts +153 -0
- package/src/hooks/useRenderClipContent.ts +124 -0
- package/src/hooks/useTimelineEditing.ts +472 -0
- package/src/player/components/Player.tsx +35 -4
- package/src/player/components/Timeline.test.ts +0 -8
- package/src/player/components/Timeline.tsx +10 -103
- package/src/player/components/TimelineClip.tsx +9 -244
- package/src/player/hooks/useTimelinePlayer.ts +140 -103
- package/src/utils/domEditHelpers.ts +50 -0
- package/src/utils/studioFontHelpers.ts +83 -0
- package/src/utils/studioHelpers.ts +214 -0
- package/src/utils/studioPreviewHelpers.ts +185 -0
- package/src/utils/timelineDiscovery.ts +1 -1
- package/dist/assets/index-14zH9lqh.css +0 -1
- package/dist/assets/index-ClYcrksa.js +0 -108
- package/src/player/components/TimelineClip.test.ts +0 -92
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { liveTime, usePlayerStore } from "../player";
|
|
3
|
+
import {
|
|
4
|
+
getPreviewLocalPointer,
|
|
5
|
+
buildRasterClickSelectionContext,
|
|
6
|
+
pauseStudioPreviewPlayback,
|
|
7
|
+
} from "../utils/studioPreviewHelpers";
|
|
8
|
+
import { STUDIO_PREVIEW_SELECTION_ENABLED } from "../components/editor/manualEditingAvailability";
|
|
9
|
+
import {
|
|
10
|
+
isLargeRasterDomEditSelection,
|
|
11
|
+
type DomEditSelection,
|
|
12
|
+
} from "../components/editor/domEditing";
|
|
13
|
+
import type { AgentModalAnchorPoint } from "../utils/studioHelpers";
|
|
14
|
+
|
|
15
|
+
// ── Types ──
|
|
16
|
+
|
|
17
|
+
export interface UsePreviewInteractionParams {
|
|
18
|
+
captionEditMode: boolean;
|
|
19
|
+
compositionLoading: boolean;
|
|
20
|
+
previewIframeRef: React.MutableRefObject<HTMLIFrameElement | null>;
|
|
21
|
+
activeCompPath: string | null;
|
|
22
|
+
showToast: (message: string, tone?: "error" | "info") => void;
|
|
23
|
+
|
|
24
|
+
// From useDomSelection
|
|
25
|
+
applyDomSelection: (
|
|
26
|
+
selection: DomEditSelection | null,
|
|
27
|
+
options?: { revealPanel?: boolean; additive?: boolean; preserveGroup?: boolean },
|
|
28
|
+
) => void;
|
|
29
|
+
resolveDomSelectionFromPreviewPoint: (
|
|
30
|
+
clientX: number,
|
|
31
|
+
clientY: number,
|
|
32
|
+
options?: { preferClipAncestor?: boolean },
|
|
33
|
+
) => DomEditSelection | null;
|
|
34
|
+
updateDomEditHoverSelection: (selection: DomEditSelection | null) => void;
|
|
35
|
+
|
|
36
|
+
// From useAskAgentModal
|
|
37
|
+
preloadAgentPromptSnippet: (selection: DomEditSelection) => Promise<void>;
|
|
38
|
+
setAgentPromptSelectionContext: (context: string | undefined) => void;
|
|
39
|
+
setAgentModalAnchorPoint: (point: AgentModalAnchorPoint | null) => void;
|
|
40
|
+
setAgentModalOpen: (open: boolean) => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Hook ──
|
|
44
|
+
|
|
45
|
+
export function usePreviewInteraction({
|
|
46
|
+
captionEditMode,
|
|
47
|
+
compositionLoading,
|
|
48
|
+
previewIframeRef,
|
|
49
|
+
showToast,
|
|
50
|
+
applyDomSelection,
|
|
51
|
+
resolveDomSelectionFromPreviewPoint,
|
|
52
|
+
updateDomEditHoverSelection,
|
|
53
|
+
preloadAgentPromptSnippet,
|
|
54
|
+
setAgentPromptSelectionContext,
|
|
55
|
+
setAgentModalAnchorPoint,
|
|
56
|
+
setAgentModalOpen,
|
|
57
|
+
}: UsePreviewInteractionParams) {
|
|
58
|
+
const handlePreviewCanvasMouseDown = useCallback(
|
|
59
|
+
(e: React.MouseEvent<HTMLDivElement>, options?: { preferClipAncestor?: boolean }) => {
|
|
60
|
+
if (!STUDIO_PREVIEW_SELECTION_ENABLED || captionEditMode || compositionLoading) return;
|
|
61
|
+
const nextSelection = resolveDomSelectionFromPreviewPoint(e.clientX, e.clientY, {
|
|
62
|
+
preferClipAncestor: options?.preferClipAncestor ?? false,
|
|
63
|
+
});
|
|
64
|
+
if (!nextSelection) {
|
|
65
|
+
if (!e.shiftKey) applyDomSelection(null, { revealPanel: false });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
e.stopPropagation();
|
|
70
|
+
const localPointer = previewIframeRef.current
|
|
71
|
+
? getPreviewLocalPointer(previewIframeRef.current, e.clientX, e.clientY)
|
|
72
|
+
: null;
|
|
73
|
+
applyDomSelection(nextSelection, { additive: e.shiftKey });
|
|
74
|
+
if (
|
|
75
|
+
!e.shiftKey &&
|
|
76
|
+
localPointer &&
|
|
77
|
+
isLargeRasterDomEditSelection(nextSelection, localPointer.viewport)
|
|
78
|
+
) {
|
|
79
|
+
setAgentPromptSelectionContext(
|
|
80
|
+
buildRasterClickSelectionContext(nextSelection, localPointer),
|
|
81
|
+
);
|
|
82
|
+
setAgentModalAnchorPoint({ x: e.clientX, y: e.clientY });
|
|
83
|
+
void preloadAgentPromptSnippet(nextSelection);
|
|
84
|
+
setAgentModalOpen(true);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
[
|
|
88
|
+
applyDomSelection,
|
|
89
|
+
captionEditMode,
|
|
90
|
+
compositionLoading,
|
|
91
|
+
preloadAgentPromptSnippet,
|
|
92
|
+
resolveDomSelectionFromPreviewPoint,
|
|
93
|
+
previewIframeRef,
|
|
94
|
+
setAgentModalAnchorPoint,
|
|
95
|
+
setAgentModalOpen,
|
|
96
|
+
setAgentPromptSelectionContext,
|
|
97
|
+
],
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const handlePreviewCanvasPointerMove = useCallback(
|
|
101
|
+
(e: React.PointerEvent<HTMLDivElement>, options?: { preferClipAncestor?: boolean }) => {
|
|
102
|
+
if (!STUDIO_PREVIEW_SELECTION_ENABLED || captionEditMode || compositionLoading) {
|
|
103
|
+
updateDomEditHoverSelection(null);
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const nextSelection = resolveDomSelectionFromPreviewPoint(e.clientX, e.clientY, {
|
|
108
|
+
preferClipAncestor: options?.preferClipAncestor ?? false,
|
|
109
|
+
});
|
|
110
|
+
updateDomEditHoverSelection(nextSelection);
|
|
111
|
+
return nextSelection;
|
|
112
|
+
},
|
|
113
|
+
[
|
|
114
|
+
captionEditMode,
|
|
115
|
+
compositionLoading,
|
|
116
|
+
resolveDomSelectionFromPreviewPoint,
|
|
117
|
+
updateDomEditHoverSelection,
|
|
118
|
+
],
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const handlePreviewCanvasPointerLeave = useCallback(() => {
|
|
122
|
+
updateDomEditHoverSelection(null);
|
|
123
|
+
}, [updateDomEditHoverSelection]);
|
|
124
|
+
|
|
125
|
+
const handleBlockedDomMove = useCallback(
|
|
126
|
+
(selection: DomEditSelection) => {
|
|
127
|
+
showToast(
|
|
128
|
+
selection.capabilities.reasonIfDisabled ??
|
|
129
|
+
"This element can't be adjusted directly from the preview.",
|
|
130
|
+
"info",
|
|
131
|
+
);
|
|
132
|
+
},
|
|
133
|
+
[showToast],
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const handleDomManualDragStart = useCallback(() => {
|
|
137
|
+
const pausedTime = pauseStudioPreviewPlayback(previewIframeRef.current);
|
|
138
|
+
const playerStore = usePlayerStore.getState();
|
|
139
|
+
playerStore.setIsPlaying(false);
|
|
140
|
+
if (pausedTime != null) {
|
|
141
|
+
playerStore.setCurrentTime(pausedTime);
|
|
142
|
+
liveTime.notify(pausedTime);
|
|
143
|
+
}
|
|
144
|
+
}, [previewIframeRef]);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
handlePreviewCanvasMouseDown,
|
|
148
|
+
handlePreviewCanvasPointerMove,
|
|
149
|
+
handlePreviewCanvasPointerLeave,
|
|
150
|
+
handleBlockedDomMove,
|
|
151
|
+
handleDomManualDragStart,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { useCallback, type ReactNode } from "react";
|
|
2
|
+
import { createElement } from "react";
|
|
3
|
+
import { CompositionThumbnail, VideoThumbnail } from "../player";
|
|
4
|
+
import type { TimelineElement } from "../player";
|
|
5
|
+
import { AudioWaveform } from "../player/components/AudioWaveform";
|
|
6
|
+
import { getTimelineElementLabel } from "../utils/studioHelpers";
|
|
7
|
+
|
|
8
|
+
interface UseRenderClipContentOptions {
|
|
9
|
+
projectIdRef: { current: string | null };
|
|
10
|
+
compIdToSrc: Map<string, string>;
|
|
11
|
+
activePreviewUrl: string | null;
|
|
12
|
+
effectiveTimelineDuration: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useRenderClipContent({
|
|
16
|
+
projectIdRef,
|
|
17
|
+
compIdToSrc,
|
|
18
|
+
activePreviewUrl,
|
|
19
|
+
effectiveTimelineDuration,
|
|
20
|
+
}: UseRenderClipContentOptions) {
|
|
21
|
+
return useCallback(
|
|
22
|
+
(el: TimelineElement, style: { clip: string; label: string }): ReactNode => {
|
|
23
|
+
const pid = projectIdRef.current;
|
|
24
|
+
if (!pid) return null;
|
|
25
|
+
|
|
26
|
+
// Resolve composition source path using the compIdToSrc map
|
|
27
|
+
let compSrc = el.compositionSrc;
|
|
28
|
+
if (compSrc && compIdToSrc.size > 0) {
|
|
29
|
+
const resolved =
|
|
30
|
+
compIdToSrc.get(el.id) ||
|
|
31
|
+
compIdToSrc.get(compSrc.replace(/^compositions\//, "").replace(/\.html$/, ""));
|
|
32
|
+
if (resolved) compSrc = resolved;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Composition clips — always use the comp's own preview URL for thumbnails.
|
|
36
|
+
// This renders the composition in isolation so we get clean frames
|
|
37
|
+
// instead of capturing the master at a time when the comp is fading in.
|
|
38
|
+
if (compSrc) {
|
|
39
|
+
return createElement(CompositionThumbnail, {
|
|
40
|
+
previewUrl: `/api/projects/${pid}/preview/comp/${compSrc}`,
|
|
41
|
+
label: getTimelineElementLabel(el),
|
|
42
|
+
labelColor: style.label,
|
|
43
|
+
accentColor: style.clip,
|
|
44
|
+
seekTime: 0,
|
|
45
|
+
duration: el.duration,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// When drilled into a composition, render all inner elements via
|
|
50
|
+
// CompositionThumbnail at their start time — most accurate visual.
|
|
51
|
+
if (activePreviewUrl && el.duration > 0) {
|
|
52
|
+
return createElement(CompositionThumbnail, {
|
|
53
|
+
previewUrl: activePreviewUrl,
|
|
54
|
+
label: getTimelineElementLabel(el),
|
|
55
|
+
labelColor: style.label,
|
|
56
|
+
accentColor: style.clip,
|
|
57
|
+
selector: el.selector,
|
|
58
|
+
selectorIndex: el.selectorIndex,
|
|
59
|
+
seekTime: el.start,
|
|
60
|
+
duration: el.duration,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const htmlPreviewEligible =
|
|
65
|
+
el.duration > 0 &&
|
|
66
|
+
effectiveTimelineDuration > 0 &&
|
|
67
|
+
el.duration < effectiveTimelineDuration * 0.92 &&
|
|
68
|
+
!/(backdrop|background|overlay|scrim|mask)/i.test(el.id);
|
|
69
|
+
|
|
70
|
+
// Audio clips — waveform visualization
|
|
71
|
+
if (el.tag === "audio") {
|
|
72
|
+
const previewBase = `/api/projects/${pid}/preview/`;
|
|
73
|
+
const previewIdx = el.src?.startsWith("http") ? el.src.indexOf(previewBase) : -1;
|
|
74
|
+
const srcRelative = el.src
|
|
75
|
+
? previewIdx !== -1
|
|
76
|
+
? decodeURIComponent(el.src.slice(previewIdx + previewBase.length))
|
|
77
|
+
: el.src.startsWith("http")
|
|
78
|
+
? null
|
|
79
|
+
: el.src
|
|
80
|
+
: null;
|
|
81
|
+
const audioUrl = srcRelative
|
|
82
|
+
? `/api/projects/${pid}/preview/${srcRelative}`
|
|
83
|
+
: (el.src ?? "");
|
|
84
|
+
const waveformUrl = srcRelative
|
|
85
|
+
? `/api/projects/${pid}/waveform/${srcRelative}`
|
|
86
|
+
: undefined;
|
|
87
|
+
return createElement(AudioWaveform, {
|
|
88
|
+
audioUrl,
|
|
89
|
+
waveformUrl,
|
|
90
|
+
label: getTimelineElementLabel(el),
|
|
91
|
+
labelColor: style.label,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if ((el.tag === "video" || el.tag === "img") && el.src) {
|
|
96
|
+
const mediaSrc = el.src.startsWith("http")
|
|
97
|
+
? el.src
|
|
98
|
+
: `/api/projects/${pid}/preview/${el.src}`;
|
|
99
|
+
return createElement(VideoThumbnail, {
|
|
100
|
+
videoSrc: mediaSrc,
|
|
101
|
+
label: getTimelineElementLabel(el),
|
|
102
|
+
labelColor: style.label,
|
|
103
|
+
duration: el.duration,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (htmlPreviewEligible) {
|
|
108
|
+
return createElement(CompositionThumbnail, {
|
|
109
|
+
previewUrl: `/api/projects/${pid}/preview`,
|
|
110
|
+
label: getTimelineElementLabel(el),
|
|
111
|
+
labelColor: style.label,
|
|
112
|
+
accentColor: style.clip,
|
|
113
|
+
selector: el.selector,
|
|
114
|
+
selectorIndex: el.selectorIndex,
|
|
115
|
+
seekTime: el.start,
|
|
116
|
+
duration: el.duration,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return null;
|
|
121
|
+
},
|
|
122
|
+
[projectIdRef, compIdToSrc, activePreviewUrl, effectiveTimelineDuration],
|
|
123
|
+
);
|
|
124
|
+
}
|