@hyperframes/studio 0.6.0-alpha.9 → 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.
Files changed (66) hide show
  1. package/dist/assets/{hyperframes-player-DjsVzYFP.js → hyperframes-player-DOFETgjy.js} +1 -1
  2. package/dist/assets/index-D1JDq7Gg.css +1 -0
  3. package/dist/assets/index-DUqUmaoH.js +117 -0
  4. package/dist/favicon.svg +14 -0
  5. package/dist/index.html +3 -2
  6. package/package.json +9 -9
  7. package/src/App.tsx +428 -4299
  8. package/src/components/AskAgentModal.tsx +120 -0
  9. package/src/components/StudioHeader.tsx +133 -0
  10. package/src/components/StudioLeftSidebar.tsx +125 -0
  11. package/src/components/StudioPreviewArea.tsx +163 -0
  12. package/src/components/StudioRightPanel.tsx +198 -0
  13. package/src/components/TimelineToolbar.tsx +89 -0
  14. package/src/components/editor/DomEditOverlay.tsx +15 -1
  15. package/src/components/editor/PropertyPanel.test.ts +0 -49
  16. package/src/components/editor/PropertyPanel.tsx +132 -2763
  17. package/src/components/editor/domEditing.ts +38 -5
  18. package/src/components/editor/manualEditingAvailability.test.ts +2 -2
  19. package/src/components/editor/manualEditingAvailability.ts +1 -1
  20. package/src/components/editor/manualEdits.ts +32 -0
  21. package/src/components/editor/propertyPanelColor.tsx +371 -0
  22. package/src/components/editor/propertyPanelFill.tsx +421 -0
  23. package/src/components/editor/propertyPanelFont.tsx +455 -0
  24. package/src/components/editor/propertyPanelHelpers.ts +401 -0
  25. package/src/components/editor/propertyPanelPrimitives.tsx +357 -0
  26. package/src/components/editor/propertyPanelSections.tsx +453 -0
  27. package/src/components/editor/propertyPanelStyleSections.tsx +411 -0
  28. package/src/components/nle/NLELayout.tsx +8 -11
  29. package/src/components/nle/NLEPreview.tsx +3 -0
  30. package/src/components/renders/RenderQueue.tsx +102 -31
  31. package/src/components/renders/useRenderQueue.ts +8 -2
  32. package/src/components/sidebar/LeftSidebar.tsx +186 -186
  33. package/src/contexts/DomEditContext.tsx +137 -0
  34. package/src/contexts/FileManagerContext.tsx +110 -0
  35. package/src/contexts/PanelLayoutContext.tsx +68 -0
  36. package/src/contexts/StudioContext.tsx +135 -0
  37. package/src/hooks/useAppHotkeys.ts +326 -0
  38. package/src/hooks/useAskAgentModal.ts +162 -0
  39. package/src/hooks/useCaptionDetection.ts +132 -0
  40. package/src/hooks/useCompositionDimensions.ts +25 -0
  41. package/src/hooks/useConsoleErrorCapture.ts +60 -0
  42. package/src/hooks/useDomEditCommits.ts +437 -0
  43. package/src/hooks/useDomEditSession.ts +342 -0
  44. package/src/hooks/useDomEditTextCommits.ts +330 -0
  45. package/src/hooks/useDomSelection.ts +398 -0
  46. package/src/hooks/useFileManager.ts +431 -0
  47. package/src/hooks/useFrameCapture.ts +77 -0
  48. package/src/hooks/useLintModal.ts +35 -0
  49. package/src/hooks/useManifestPersistence.ts +492 -0
  50. package/src/hooks/usePanelLayout.ts +68 -0
  51. package/src/hooks/usePreviewInteraction.ts +153 -0
  52. package/src/hooks/useRenderClipContent.ts +124 -0
  53. package/src/hooks/useTimelineEditing.ts +472 -0
  54. package/src/player/components/Player.tsx +33 -2
  55. package/src/player/components/Timeline.test.ts +0 -8
  56. package/src/player/components/Timeline.tsx +10 -103
  57. package/src/player/components/TimelineClip.tsx +9 -244
  58. package/src/player/hooks/useTimelinePlayer.ts +140 -103
  59. package/src/utils/domEditHelpers.ts +50 -0
  60. package/src/utils/studioFontHelpers.ts +83 -0
  61. package/src/utils/studioHelpers.ts +214 -0
  62. package/src/utils/studioPreviewHelpers.ts +185 -0
  63. package/src/utils/timelineDiscovery.ts +1 -1
  64. package/dist/assets/index-14zH9lqh.css +0 -1
  65. package/dist/assets/index-DYCiFGWQ.js +0 -108
  66. 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
+ }