@hyperframes/studio 0.6.7 → 0.6.8
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/index-BSe0Kibk.js +115 -0
- package/dist/index.html +1 -1
- package/package.json +4 -4
- package/src/App.tsx +5 -10
- package/src/components/StudioLeftSidebar.tsx +16 -2
- package/src/components/StudioRightPanel.tsx +15 -2
- package/src/components/editor/MotionPanel.tsx +8 -8
- package/src/components/editor/SourceEditor.tsx +14 -0
- package/src/components/editor/manualEdits.ts +2 -0
- package/src/components/editor/manualEditsDom.ts +56 -0
- package/src/components/editor/studioMotion.ts +96 -0
- package/src/components/editor/studioMotionOps.test.ts +445 -0
- package/src/components/editor/studioMotionOps.ts +78 -4
- package/src/components/renders/RenderQueue.tsx +20 -6
- package/src/components/renders/renderSettings.ts +38 -0
- package/src/components/renders/useRenderQueue.ts +11 -1
- package/src/components/sidebar/CompositionsTab.tsx +43 -1
- package/src/components/sidebar/LeftSidebar.tsx +6 -0
- package/src/contexts/FileManagerContext.tsx +6 -0
- package/src/hooks/useDomEditCommits.ts +45 -33
- package/src/hooks/useDomEditSession.ts +26 -25
- package/src/hooks/useFileManager.ts +42 -0
- package/src/hooks/useManifestPersistence.ts +40 -218
- package/src/hooks/usePreviewInteraction.ts +7 -0
- package/src/player/components/Player.tsx +12 -3
- package/src/player/components/PlayerControls.tsx +29 -2
- package/src/player/components/useTimelineRangeSelection.ts +30 -3
- package/src/utils/sourcePatcher.test.ts +285 -0
- package/src/utils/sourcePatcher.ts +26 -6
- package/dist/assets/index-Yvtxngdi.js +0 -116
|
@@ -5,6 +5,8 @@ interface CompositionsTabProps {
|
|
|
5
5
|
compositions: string[];
|
|
6
6
|
activeComposition: string | null;
|
|
7
7
|
onSelect: (comp: string) => void;
|
|
8
|
+
onRenderComposition?: (comp: string) => void;
|
|
9
|
+
isRendering?: boolean;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
const DEFAULT_PREVIEW_STAGE = { width: 1920, height: 1080 };
|
|
@@ -94,11 +96,15 @@ function CompCard({
|
|
|
94
96
|
comp,
|
|
95
97
|
isActive,
|
|
96
98
|
onSelect,
|
|
99
|
+
onRender,
|
|
100
|
+
isRendering,
|
|
97
101
|
}: {
|
|
98
102
|
projectId: string;
|
|
99
103
|
comp: string;
|
|
100
104
|
isActive: boolean;
|
|
101
105
|
onSelect: () => void;
|
|
106
|
+
onRender?: () => void;
|
|
107
|
+
isRendering?: boolean;
|
|
102
108
|
}) {
|
|
103
109
|
const [hovered, setHovered] = useState(false);
|
|
104
110
|
const [stageSize, setStageSize] = useState(DEFAULT_PREVIEW_STAGE);
|
|
@@ -158,7 +164,7 @@ function CompCard({
|
|
|
158
164
|
onClick={onSelect}
|
|
159
165
|
onPointerEnter={handleEnter}
|
|
160
166
|
onPointerLeave={handleLeave}
|
|
161
|
-
className={`w-full text-left px-2 py-1.5 flex items-center gap-2.5 transition-colors cursor-pointer ${
|
|
167
|
+
className={`group/card w-full text-left px-2 py-1.5 flex items-center gap-2.5 transition-colors cursor-pointer ${
|
|
162
168
|
isActive
|
|
163
169
|
? "bg-studio-accent/10 border-l-2 border-studio-accent"
|
|
164
170
|
: "border-l-2 border-transparent hover:bg-neutral-800/50"
|
|
@@ -200,6 +206,38 @@ function CompCard({
|
|
|
200
206
|
<span className="text-[11px] font-medium text-neutral-300 truncate block">{name}</span>
|
|
201
207
|
<span className="text-[9px] text-neutral-600 truncate block">{comp}</span>
|
|
202
208
|
</div>
|
|
209
|
+
{onRender && (
|
|
210
|
+
<button
|
|
211
|
+
type="button"
|
|
212
|
+
title={isRendering ? "Rendering..." : `Render ${name}`}
|
|
213
|
+
aria-label={isRendering ? "Rendering..." : `Render ${name}`}
|
|
214
|
+
disabled={isRendering}
|
|
215
|
+
onClick={(e) => {
|
|
216
|
+
e.stopPropagation();
|
|
217
|
+
onRender();
|
|
218
|
+
}}
|
|
219
|
+
className={`flex-shrink-0 p-1 rounded transition-colors ${
|
|
220
|
+
isRendering
|
|
221
|
+
? "text-neutral-600 cursor-not-allowed"
|
|
222
|
+
: "text-neutral-600 hover:text-studio-accent hover:bg-neutral-800"
|
|
223
|
+
}`}
|
|
224
|
+
>
|
|
225
|
+
<svg
|
|
226
|
+
width="14"
|
|
227
|
+
height="14"
|
|
228
|
+
viewBox="0 0 24 24"
|
|
229
|
+
fill="none"
|
|
230
|
+
stroke="currentColor"
|
|
231
|
+
strokeWidth="2"
|
|
232
|
+
strokeLinecap="round"
|
|
233
|
+
strokeLinejoin="round"
|
|
234
|
+
>
|
|
235
|
+
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
|
|
236
|
+
<polyline points="7 10 12 15 17 10" />
|
|
237
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
238
|
+
</svg>
|
|
239
|
+
</button>
|
|
240
|
+
)}
|
|
203
241
|
</div>
|
|
204
242
|
);
|
|
205
243
|
}
|
|
@@ -209,6 +247,8 @@ export const CompositionsTab = memo(function CompositionsTab({
|
|
|
209
247
|
compositions,
|
|
210
248
|
activeComposition,
|
|
211
249
|
onSelect,
|
|
250
|
+
onRenderComposition,
|
|
251
|
+
isRendering,
|
|
212
252
|
}: CompositionsTabProps) {
|
|
213
253
|
if (compositions.length === 0) {
|
|
214
254
|
return (
|
|
@@ -227,6 +267,8 @@ export const CompositionsTab = memo(function CompositionsTab({
|
|
|
227
267
|
comp={comp}
|
|
228
268
|
isActive={activeComposition === comp}
|
|
229
269
|
onSelect={() => onSelect(comp)}
|
|
270
|
+
onRender={onRenderComposition ? () => onRenderComposition(comp) : undefined}
|
|
271
|
+
isRendering={isRendering}
|
|
230
272
|
/>
|
|
231
273
|
))}
|
|
232
274
|
</div>
|
|
@@ -43,6 +43,8 @@ interface LeftSidebarProps {
|
|
|
43
43
|
onDuplicateFile?: (path: string) => void;
|
|
44
44
|
onMoveFile?: (oldPath: string, newPath: string) => void;
|
|
45
45
|
codeChildren?: ReactNode;
|
|
46
|
+
onRenderComposition?: (comp: string) => void;
|
|
47
|
+
isRendering?: boolean;
|
|
46
48
|
onLint?: () => void;
|
|
47
49
|
linting?: boolean;
|
|
48
50
|
onToggleCollapse?: () => void;
|
|
@@ -69,6 +71,8 @@ export const LeftSidebar = memo(
|
|
|
69
71
|
onDuplicateFile,
|
|
70
72
|
onMoveFile,
|
|
71
73
|
codeChildren,
|
|
74
|
+
onRenderComposition,
|
|
75
|
+
isRendering,
|
|
72
76
|
onLint,
|
|
73
77
|
linting,
|
|
74
78
|
onToggleCollapse,
|
|
@@ -169,6 +173,8 @@ export const LeftSidebar = memo(
|
|
|
169
173
|
compositions={compositions}
|
|
170
174
|
activeComposition={activeComposition}
|
|
171
175
|
onSelect={onSelectComposition}
|
|
176
|
+
onRenderComposition={onRenderComposition}
|
|
177
|
+
isRendering={isRendering}
|
|
172
178
|
/>
|
|
173
179
|
)}
|
|
174
180
|
{tab === "assets" && (
|
|
@@ -26,6 +26,8 @@ export function FileManagerProvider({
|
|
|
26
26
|
readProjectFile,
|
|
27
27
|
writeProjectFile,
|
|
28
28
|
readOptionalProjectFile,
|
|
29
|
+
revealSourceOffset,
|
|
30
|
+
openSourceForSelection,
|
|
29
31
|
handleFileSelect,
|
|
30
32
|
handleContentChange,
|
|
31
33
|
refreshFileTree,
|
|
@@ -62,6 +64,8 @@ export function FileManagerProvider({
|
|
|
62
64
|
readProjectFile,
|
|
63
65
|
writeProjectFile,
|
|
64
66
|
readOptionalProjectFile,
|
|
67
|
+
revealSourceOffset,
|
|
68
|
+
openSourceForSelection,
|
|
65
69
|
handleFileSelect,
|
|
66
70
|
handleContentChange,
|
|
67
71
|
refreshFileTree,
|
|
@@ -92,6 +96,8 @@ export function FileManagerProvider({
|
|
|
92
96
|
readProjectFile,
|
|
93
97
|
writeProjectFile,
|
|
94
98
|
readOptionalProjectFile,
|
|
99
|
+
revealSourceOffset,
|
|
100
|
+
openSourceForSelection,
|
|
95
101
|
handleFileSelect,
|
|
96
102
|
handleContentChange,
|
|
97
103
|
refreshFileTree,
|
|
@@ -20,12 +20,14 @@ import {
|
|
|
20
20
|
buildClearPathOffsetPatches,
|
|
21
21
|
buildClearBoxSizePatches,
|
|
22
22
|
buildClearRotationPatches,
|
|
23
|
+
buildMotionPatches,
|
|
24
|
+
buildClearMotionPatches,
|
|
23
25
|
} from "../components/editor/manualEditsDom";
|
|
24
26
|
import {
|
|
25
|
-
|
|
27
|
+
writeStudioMotionToElement,
|
|
28
|
+
clearStudioMotionFromElement,
|
|
29
|
+
applyStudioMotionFromDom,
|
|
26
30
|
type StudioGsapMotion,
|
|
27
|
-
type StudioMotionManifest,
|
|
28
|
-
upsertStudioGsapMotion,
|
|
29
31
|
} from "../components/editor/studioMotion";
|
|
30
32
|
import { fontFamilyFromAssetPath, type ImportedFontAsset } from "../components/editor/fontAssets";
|
|
31
33
|
import type { DomEditGroupPathOffsetCommit } from "../components/editor/DomEditOverlay";
|
|
@@ -58,11 +60,6 @@ export interface UseDomEditCommitsParams {
|
|
|
58
60
|
previewIframeRef: React.MutableRefObject<HTMLIFrameElement | null>;
|
|
59
61
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
60
62
|
queueDomEditSave: (save: () => Promise<void>) => Promise<void>;
|
|
61
|
-
commitStudioMotionManifestOptimistically: (
|
|
62
|
-
updateManifest: (manifest: StudioMotionManifest) => StudioMotionManifest,
|
|
63
|
-
options: { label: string; coalesceKey: string },
|
|
64
|
-
) => void;
|
|
65
|
-
applyCurrentStudioMotionToPreview: (iframe: HTMLIFrameElement | null) => void;
|
|
66
63
|
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
67
64
|
domEditSaveTimestampRef: React.MutableRefObject<number>;
|
|
68
65
|
editHistory: { recordEdit: (entry: RecordEditInput) => Promise<void> };
|
|
@@ -93,8 +90,6 @@ export function useDomEditCommits({
|
|
|
93
90
|
previewIframeRef,
|
|
94
91
|
showToast,
|
|
95
92
|
queueDomEditSave,
|
|
96
|
-
commitStudioMotionManifestOptimistically,
|
|
97
|
-
applyCurrentStudioMotionToPreview,
|
|
98
93
|
writeProjectFile,
|
|
99
94
|
domEditSaveTimestampRef,
|
|
100
95
|
editHistory,
|
|
@@ -306,43 +301,60 @@ export function useDomEditCommits({
|
|
|
306
301
|
[commitPositionPatchToHtml],
|
|
307
302
|
);
|
|
308
303
|
|
|
309
|
-
// ── Motion commits ──
|
|
304
|
+
// ── Motion commits (HTML-attribute–backed) ──
|
|
310
305
|
|
|
311
306
|
const handleDomMotionCommit = useCallback(
|
|
312
307
|
(
|
|
313
308
|
selection: DomEditSelection,
|
|
314
309
|
motion: Omit<StudioGsapMotion, "kind" | "target" | "updatedAt">,
|
|
315
310
|
) => {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
311
|
+
// 1. Write motion data as JSON attribute on the element
|
|
312
|
+
writeStudioMotionToElement(selection.element, motion);
|
|
313
|
+
// 2. Apply the GSAP timeline from DOM attributes
|
|
314
|
+
let doc: Document | null = null;
|
|
315
|
+
try {
|
|
316
|
+
doc = previewIframeRef.current?.contentDocument ?? null;
|
|
317
|
+
} catch {
|
|
318
|
+
// cross-origin guard
|
|
319
|
+
}
|
|
320
|
+
if (doc) applyStudioMotionFromDom(doc);
|
|
321
|
+
// 3. Build patches and persist to HTML
|
|
322
|
+
const patches = buildMotionPatches(selection.element);
|
|
323
|
+
commitPositionPatchToHtml(selection, patches, {
|
|
324
|
+
label: "Set GSAP motion",
|
|
325
|
+
coalesceKey: `motion:${getDomEditTargetKey(selection)}`,
|
|
326
|
+
});
|
|
323
327
|
refreshDomEditSelectionFromPreview(selection);
|
|
324
328
|
},
|
|
325
|
-
[
|
|
329
|
+
[commitPositionPatchToHtml, previewIframeRef, refreshDomEditSelectionFromPreview],
|
|
326
330
|
);
|
|
327
331
|
|
|
328
332
|
const handleDomMotionClear = useCallback(
|
|
329
333
|
(selection: DomEditSelection) => {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
334
|
+
const clearPatches = buildClearMotionPatches(selection.element);
|
|
335
|
+
// Get gsap from the preview window for proper cleanup
|
|
336
|
+
let gsap: { set?: (target: HTMLElement, vars: Record<string, unknown>) => void } | undefined;
|
|
337
|
+
try {
|
|
338
|
+
gsap = (previewIframeRef.current?.contentWindow as { gsap?: typeof gsap })?.gsap;
|
|
339
|
+
} catch {
|
|
340
|
+
// cross-origin guard
|
|
341
|
+
}
|
|
342
|
+
clearStudioMotionFromElement(selection.element, gsap);
|
|
343
|
+
let doc: Document | null = null;
|
|
344
|
+
try {
|
|
345
|
+
doc = previewIframeRef.current?.contentDocument ?? null;
|
|
346
|
+
} catch {
|
|
347
|
+
// cross-origin guard
|
|
348
|
+
}
|
|
349
|
+
if (doc) applyStudioMotionFromDom(doc);
|
|
350
|
+
commitPositionPatchToHtml(selection, clearPatches, {
|
|
351
|
+
label: "Clear GSAP motion",
|
|
352
|
+
coalesceKey: `motion:${getDomEditTargetKey(selection)}`,
|
|
353
|
+
skipRefresh: false,
|
|
354
|
+
});
|
|
338
355
|
refreshDomEditSelectionFromPreview(selection);
|
|
339
356
|
},
|
|
340
|
-
[
|
|
341
|
-
applyCurrentStudioMotionToPreview,
|
|
342
|
-
commitStudioMotionManifestOptimistically,
|
|
343
|
-
refreshDomEditSelectionFromPreview,
|
|
344
|
-
previewIframeRef,
|
|
345
|
-
],
|
|
357
|
+
[commitPositionPatchToHtml, previewIframeRef, refreshDomEditSelectionFromPreview],
|
|
346
358
|
);
|
|
347
359
|
|
|
348
360
|
const handleDomEditElementDelete = useCallback(
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { useEffect } from "react";
|
|
1
|
+
import { useCallback, useEffect } from "react";
|
|
2
2
|
import type { TimelineElement } from "../player";
|
|
3
3
|
import { STUDIO_INSPECTOR_PANELS_ENABLED } from "../components/editor/manualEditingAvailability";
|
|
4
|
-
import { findElementForSelection } from "../components/editor/domEditing";
|
|
5
|
-
import type { StudioMotionManifest } from "../components/editor/studioMotion";
|
|
4
|
+
import { findElementForSelection, type DomEditSelection } from "../components/editor/domEditing";
|
|
6
5
|
import type { ImportedFontAsset } from "../components/editor/fontAssets";
|
|
7
6
|
import type { EditHistoryKind } from "../utils/editHistory";
|
|
8
7
|
import type { RightPanelTab } from "../utils/studioHelpers";
|
|
8
|
+
import type { PatchTarget } from "../utils/sourcePatcher";
|
|
9
|
+
import type { SidebarTab } from "../components/sidebar/LeftSidebar";
|
|
9
10
|
import { useAskAgentModal } from "./useAskAgentModal";
|
|
10
11
|
import { useDomSelection } from "./useDomSelection";
|
|
11
12
|
import { usePreviewInteraction } from "./usePreviewInteraction";
|
|
@@ -36,11 +37,6 @@ export interface UseDomEditSessionParams {
|
|
|
36
37
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
37
38
|
refreshPreviewDocumentVersion: () => void;
|
|
38
39
|
queueDomEditSave: (save: () => Promise<void>) => Promise<void>;
|
|
39
|
-
commitStudioMotionManifestOptimistically: (
|
|
40
|
-
updateManifest: (manifest: StudioMotionManifest) => StudioMotionManifest,
|
|
41
|
-
options: { label: string; coalesceKey: string },
|
|
42
|
-
) => void;
|
|
43
|
-
applyCurrentStudioMotionToPreview: (iframe: HTMLIFrameElement | null) => void;
|
|
44
40
|
readProjectFile: (path: string) => Promise<string>;
|
|
45
41
|
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
46
42
|
domEditSaveTimestampRef: React.MutableRefObject<number>;
|
|
@@ -55,12 +51,11 @@ export interface UseDomEditSessionParams {
|
|
|
55
51
|
applyStudioManualEditsToPreviewRef: React.MutableRefObject<
|
|
56
52
|
(iframe: HTMLIFrameElement) => Promise<void>
|
|
57
53
|
>;
|
|
58
|
-
applyStudioMotionToPreviewRef: React.MutableRefObject<
|
|
59
|
-
(iframe: HTMLIFrameElement) => Promise<void>
|
|
60
|
-
>;
|
|
61
54
|
syncPreviewHistoryHotkey: (iframe: HTMLIFrameElement | null) => void;
|
|
62
55
|
reloadPreview: () => void;
|
|
63
56
|
setRefreshKey: React.Dispatch<React.SetStateAction<number>>;
|
|
57
|
+
openSourceForSelection?: (sourceFile: string, target: PatchTarget) => void;
|
|
58
|
+
selectSidebarTab?: (tab: SidebarTab) => void;
|
|
64
59
|
}
|
|
65
60
|
|
|
66
61
|
// ── Hook ──
|
|
@@ -81,8 +76,6 @@ export function useDomEditSession({
|
|
|
81
76
|
showToast,
|
|
82
77
|
refreshPreviewDocumentVersion,
|
|
83
78
|
queueDomEditSave,
|
|
84
|
-
commitStudioMotionManifestOptimistically,
|
|
85
|
-
applyCurrentStudioMotionToPreview,
|
|
86
79
|
readProjectFile: _readProjectFile,
|
|
87
80
|
writeProjectFile,
|
|
88
81
|
domEditSaveTimestampRef,
|
|
@@ -95,12 +88,28 @@ export function useDomEditSession({
|
|
|
95
88
|
refreshKey,
|
|
96
89
|
rightPanelTab,
|
|
97
90
|
applyStudioManualEditsToPreviewRef,
|
|
98
|
-
applyStudioMotionToPreviewRef,
|
|
99
91
|
syncPreviewHistoryHotkey,
|
|
100
92
|
reloadPreview,
|
|
101
93
|
setRefreshKey: _setRefreshKey,
|
|
94
|
+
openSourceForSelection,
|
|
95
|
+
selectSidebarTab,
|
|
102
96
|
}: UseDomEditSessionParams) {
|
|
103
97
|
void _setRefreshKey;
|
|
98
|
+
|
|
99
|
+
const onClickToSource = useCallback(
|
|
100
|
+
(selection: DomEditSelection) => {
|
|
101
|
+
if (!openSourceForSelection || !selectSidebarTab) return;
|
|
102
|
+
if (!selection.sourceFile) return;
|
|
103
|
+
selectSidebarTab("code");
|
|
104
|
+
openSourceForSelection(selection.sourceFile, {
|
|
105
|
+
id: selection.id,
|
|
106
|
+
selector: selection.selector,
|
|
107
|
+
selectorIndex: selection.selectorIndex,
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
[openSourceForSelection, selectSidebarTab],
|
|
111
|
+
);
|
|
112
|
+
|
|
104
113
|
// ── Selection (delegated to useDomSelection) ──
|
|
105
114
|
|
|
106
115
|
const {
|
|
@@ -176,6 +185,7 @@ export function useDomEditSession({
|
|
|
176
185
|
setAgentPromptSelectionContext,
|
|
177
186
|
setAgentModalAnchorPoint,
|
|
178
187
|
setAgentModalOpen,
|
|
188
|
+
onClickToSource,
|
|
179
189
|
});
|
|
180
190
|
|
|
181
191
|
// ── Commit handlers (delegated to useDomEditCommits) ──
|
|
@@ -200,8 +210,6 @@ export function useDomEditSession({
|
|
|
200
210
|
previewIframeRef,
|
|
201
211
|
showToast,
|
|
202
212
|
queueDomEditSave,
|
|
203
|
-
commitStudioMotionManifestOptimistically,
|
|
204
|
-
applyCurrentStudioMotionToPreview,
|
|
205
213
|
writeProjectFile,
|
|
206
214
|
domEditSaveTimestampRef,
|
|
207
215
|
editHistory,
|
|
@@ -249,19 +257,13 @@ export function useDomEditSession({
|
|
|
249
257
|
};
|
|
250
258
|
|
|
251
259
|
syncPreviewHistoryHotkey(previewIframe);
|
|
252
|
-
void (
|
|
253
|
-
await applyStudioManualEditsToPreviewRef.current(previewIframe);
|
|
254
|
-
await applyStudioMotionToPreviewRef.current(previewIframe);
|
|
255
|
-
})();
|
|
260
|
+
void applyStudioManualEditsToPreviewRef.current(previewIframe);
|
|
256
261
|
syncSelectionFromDocument();
|
|
257
262
|
refreshPreviewDocumentVersion();
|
|
258
263
|
|
|
259
264
|
const handleLoad = () => {
|
|
260
265
|
syncPreviewHistoryHotkey(previewIframe);
|
|
261
|
-
void (
|
|
262
|
-
await applyStudioManualEditsToPreviewRef.current(previewIframe);
|
|
263
|
-
await applyStudioMotionToPreviewRef.current(previewIframe);
|
|
264
|
-
})();
|
|
266
|
+
void applyStudioManualEditsToPreviewRef.current(previewIframe);
|
|
265
267
|
syncSelectionFromDocument();
|
|
266
268
|
refreshPreviewDocumentVersion();
|
|
267
269
|
};
|
|
@@ -280,7 +282,6 @@ export function useDomEditSession({
|
|
|
280
282
|
refreshPreviewDocumentVersion,
|
|
281
283
|
syncPreviewHistoryHotkey,
|
|
282
284
|
applyStudioManualEditsToPreviewRef,
|
|
283
|
-
applyStudioMotionToPreviewRef,
|
|
284
285
|
]);
|
|
285
286
|
|
|
286
287
|
return {
|
|
@@ -4,6 +4,7 @@ import { FONT_EXT, isMediaFile } from "../utils/mediaTypes";
|
|
|
4
4
|
import { fontFamilyFromAssetPath, type ImportedFontAsset } from "../components/editor/fontAssets";
|
|
5
5
|
import { saveProjectFilesWithHistory } from "../utils/studioFileHistory";
|
|
6
6
|
import type { EditHistoryKind } from "../utils/editHistory";
|
|
7
|
+
import { findTagByTarget, type PatchTarget } from "../utils/sourcePatcher";
|
|
7
8
|
|
|
8
9
|
// ── Types ──
|
|
9
10
|
|
|
@@ -37,6 +38,7 @@ export function useFileManager({
|
|
|
37
38
|
const [projectDir, setProjectDir] = useState<string | null>(null);
|
|
38
39
|
const [fileTree, setFileTree] = useState<string[]>([]);
|
|
39
40
|
const [fileTreeLoaded, setFileTreeLoaded] = useState(false);
|
|
41
|
+
const [revealSourceOffset, setRevealSourceOffset] = useState<number | null>(null);
|
|
40
42
|
|
|
41
43
|
// ── Refs ──
|
|
42
44
|
|
|
@@ -169,6 +171,42 @@ export function useFileManager({
|
|
|
169
171
|
[domEditSaveTimestampRef, readProjectFile, recordEdit, setRefreshKey, writeProjectFile],
|
|
170
172
|
);
|
|
171
173
|
|
|
174
|
+
// ── Open source for selection (click-to-source) ──
|
|
175
|
+
|
|
176
|
+
const revealRequestIdRef = useRef(0);
|
|
177
|
+
const revealAbortRef = useRef<AbortController | null>(null);
|
|
178
|
+
|
|
179
|
+
const openSourceForSelection = useCallback(
|
|
180
|
+
(sourceFile: string, target: PatchTarget) => {
|
|
181
|
+
const pid = projectIdRef.current;
|
|
182
|
+
if (!pid || !sourceFile) return;
|
|
183
|
+
revealAbortRef.current?.abort();
|
|
184
|
+
revealAbortRef.current = null;
|
|
185
|
+
if (editingPathRef.current === sourceFile && editingFile?.content != null) {
|
|
186
|
+
const match = findTagByTarget(editingFile.content, target);
|
|
187
|
+
setRevealSourceOffset(match ? match.start : null);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const requestId = ++revealRequestIdRef.current;
|
|
191
|
+
const controller = new AbortController();
|
|
192
|
+
revealAbortRef.current = controller;
|
|
193
|
+
fetch(`/api/projects/${pid}/files/${encodeURIComponent(sourceFile)}`, {
|
|
194
|
+
signal: controller.signal,
|
|
195
|
+
})
|
|
196
|
+
.then((r) => r.json())
|
|
197
|
+
.then((data: { content?: string }) => {
|
|
198
|
+
if (requestId !== revealRequestIdRef.current) return;
|
|
199
|
+
if (data.content != null) {
|
|
200
|
+
setEditingFile({ path: sourceFile, content: data.content });
|
|
201
|
+
const match = findTagByTarget(data.content, target);
|
|
202
|
+
setRevealSourceOffset(match ? match.start : null);
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
.catch(() => {});
|
|
206
|
+
},
|
|
207
|
+
[editingFile?.content],
|
|
208
|
+
);
|
|
209
|
+
|
|
172
210
|
// ── File tree refresh ──
|
|
173
211
|
|
|
174
212
|
const refreshFileTree = useCallback(async () => {
|
|
@@ -418,6 +456,10 @@ export function useFileManager({
|
|
|
418
456
|
writeProjectFile,
|
|
419
457
|
readOptionalProjectFile,
|
|
420
458
|
|
|
459
|
+
// Click-to-source
|
|
460
|
+
revealSourceOffset,
|
|
461
|
+
openSourceForSelection,
|
|
462
|
+
|
|
421
463
|
// Callbacks
|
|
422
464
|
handleFileSelect,
|
|
423
465
|
handleContentChange,
|