@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,398 @@
1
+ import { useState, useCallback, useRef, useEffect } from "react";
2
+ import type { TimelineElement } from "../player";
3
+ import { getPreviewTargetFromPointer } from "../utils/studioPreviewHelpers";
4
+ import { findMatchingTimelineElementId, type RightPanelTab } from "../utils/studioHelpers";
5
+ import {
6
+ domEditSelectionsTargetSame,
7
+ domEditSelectionInGroup,
8
+ toggleDomEditGroupSelection,
9
+ replaceDomEditGroupSelection,
10
+ seedDomEditGroupWithSelection,
11
+ } from "../utils/domEditHelpers";
12
+ import { STUDIO_INSPECTOR_PANELS_ENABLED } from "../components/editor/manualEditingAvailability";
13
+ import {
14
+ findElementForSelection,
15
+ findElementForTimelineElement,
16
+ resolveDomEditSelection,
17
+ type DomEditSelection,
18
+ } from "../components/editor/domEditing";
19
+
20
+ // ── Types ──
21
+
22
+ export interface UseDomSelectionParams {
23
+ projectId: string | null;
24
+ activeCompPath: string | null;
25
+ isMasterView: boolean;
26
+ compIdToSrc: Map<string, string>;
27
+ captionEditMode: boolean;
28
+ previewIframeRef: React.MutableRefObject<HTMLIFrameElement | null>;
29
+ timelineElements: TimelineElement[];
30
+ setSelectedTimelineElementId: (id: string | null) => void;
31
+ setRightCollapsed: (collapsed: boolean) => void;
32
+ setRightPanelTab: (tab: RightPanelTab) => void;
33
+ previewIframe: HTMLIFrameElement | null;
34
+ refreshKey: number;
35
+ rightPanelTab: RightPanelTab;
36
+ }
37
+
38
+ export interface UseDomSelectionReturn {
39
+ // State
40
+ domEditSelection: DomEditSelection | null;
41
+ domEditGroupSelections: DomEditSelection[];
42
+ domEditHoverSelection: DomEditSelection | null;
43
+ // Refs
44
+ domEditSelectionRef: React.MutableRefObject<DomEditSelection | null>;
45
+ domEditGroupSelectionsRef: React.MutableRefObject<DomEditSelection[]>;
46
+ domEditHoverSelectionRef: React.MutableRefObject<DomEditSelection | null>;
47
+ // State setters (needed by useDomEditSession for agent-prompt reset flows)
48
+ setDomEditSelection: React.Dispatch<React.SetStateAction<DomEditSelection | null>>;
49
+ setDomEditGroupSelections: React.Dispatch<React.SetStateAction<DomEditSelection[]>>;
50
+ // Callbacks
51
+ applyDomSelection: (
52
+ selection: DomEditSelection | null,
53
+ options?: { revealPanel?: boolean; additive?: boolean; preserveGroup?: boolean },
54
+ ) => void;
55
+ clearDomSelection: () => void;
56
+ buildDomSelectionFromTarget: (
57
+ target: HTMLElement,
58
+ options?: { preferClipAncestor?: boolean },
59
+ ) => DomEditSelection | null;
60
+ resolveDomSelectionFromPreviewPoint: (
61
+ clientX: number,
62
+ clientY: number,
63
+ options?: { preferClipAncestor?: boolean },
64
+ ) => DomEditSelection | null;
65
+ updateDomEditHoverSelection: (selection: DomEditSelection | null) => void;
66
+ buildDomSelectionForTimelineElement: (element: TimelineElement) => DomEditSelection | null;
67
+ handleTimelineElementSelect: (element: TimelineElement | null) => void;
68
+ refreshDomEditSelectionFromPreview: (selection: DomEditSelection) => void;
69
+ refreshDomEditGroupSelectionsFromPreview: (selections: DomEditSelection[]) => void;
70
+ }
71
+
72
+ // ── Hook ──
73
+
74
+ export function useDomSelection({
75
+ projectId,
76
+ activeCompPath,
77
+ isMasterView,
78
+ compIdToSrc,
79
+ captionEditMode,
80
+ previewIframeRef,
81
+ timelineElements,
82
+ setSelectedTimelineElementId,
83
+ setRightCollapsed,
84
+ setRightPanelTab,
85
+ previewIframe,
86
+ refreshKey,
87
+ rightPanelTab,
88
+ }: UseDomSelectionParams): UseDomSelectionReturn {
89
+ // ── State ──
90
+
91
+ const [domEditSelection, setDomEditSelection] = useState<DomEditSelection | null>(null);
92
+ const [domEditGroupSelections, setDomEditGroupSelections] = useState<DomEditSelection[]>([]);
93
+ const [domEditHoverSelection, setDomEditHoverSelection] = useState<DomEditSelection | null>(null);
94
+
95
+ // ── Refs ──
96
+
97
+ const domEditSelectionRef = useRef<DomEditSelection | null>(domEditSelection);
98
+ const domEditGroupSelectionsRef = useRef<DomEditSelection[]>(domEditGroupSelections);
99
+ const domEditHoverSelectionRef = useRef<DomEditSelection | null>(domEditHoverSelection);
100
+
101
+ // Keep refs in sync with state
102
+ domEditSelectionRef.current = domEditSelection;
103
+ domEditGroupSelectionsRef.current = domEditGroupSelections;
104
+ domEditHoverSelectionRef.current = domEditHoverSelection;
105
+
106
+ // ── Callbacks ──
107
+
108
+ const applyDomSelection = useCallback(
109
+ (
110
+ selection: DomEditSelection | null,
111
+ options?: { revealPanel?: boolean; additive?: boolean; preserveGroup?: boolean },
112
+ ) => {
113
+ if (!selection) {
114
+ domEditSelectionRef.current = null;
115
+ domEditGroupSelectionsRef.current = [];
116
+ setDomEditSelection(null);
117
+ setDomEditGroupSelections([]);
118
+ setSelectedTimelineElementId(null);
119
+ return;
120
+ }
121
+ if (!STUDIO_INSPECTOR_PANELS_ENABLED) {
122
+ domEditSelectionRef.current = null;
123
+ domEditGroupSelectionsRef.current = [];
124
+ setDomEditSelection(null);
125
+ setDomEditGroupSelections([]);
126
+ setSelectedTimelineElementId(null);
127
+ return;
128
+ }
129
+
130
+ const isAdditiveSelection = Boolean(options?.additive);
131
+ const currentSelection = domEditSelectionRef.current;
132
+ const previousGroup = domEditGroupSelectionsRef.current;
133
+ const currentGroup = isAdditiveSelection
134
+ ? seedDomEditGroupWithSelection(previousGroup, currentSelection)
135
+ : previousGroup;
136
+ const wasInGroup = domEditSelectionInGroup(currentGroup, selection);
137
+ const nextGroup = options?.preserveGroup
138
+ ? replaceDomEditGroupSelection(currentGroup, selection)
139
+ : isAdditiveSelection
140
+ ? toggleDomEditGroupSelection(currentGroup, selection)
141
+ : [selection];
142
+ const nextSelection = options?.preserveGroup
143
+ ? selection
144
+ : isAdditiveSelection && wasInGroup
145
+ ? domEditSelectionsTargetSame(currentSelection, selection)
146
+ ? (nextGroup[0] ?? null)
147
+ : domEditSelectionInGroup(nextGroup, currentSelection)
148
+ ? currentSelection
149
+ : (nextGroup[0] ?? null)
150
+ : selection;
151
+
152
+ domEditSelectionRef.current = nextSelection;
153
+ domEditGroupSelectionsRef.current = nextGroup;
154
+ setDomEditSelection(nextSelection);
155
+ setDomEditGroupSelections(nextGroup);
156
+
157
+ if (nextSelection) {
158
+ if (options?.revealPanel !== false) {
159
+ setRightCollapsed(false);
160
+ setRightPanelTab("design");
161
+ }
162
+ const nextSelectedTimelineId = findMatchingTimelineElementId(
163
+ nextSelection,
164
+ timelineElements,
165
+ );
166
+ setSelectedTimelineElementId(nextSelectedTimelineId);
167
+ return;
168
+ }
169
+
170
+ setSelectedTimelineElementId(null);
171
+ },
172
+ [setSelectedTimelineElementId, timelineElements, setRightCollapsed, setRightPanelTab],
173
+ );
174
+
175
+ const clearDomSelection = useCallback(() => {
176
+ applyDomSelection(null, { revealPanel: false });
177
+ }, [applyDomSelection]);
178
+
179
+ const buildDomSelectionFromTarget = useCallback(
180
+ (target: HTMLElement, options?: { preferClipAncestor?: boolean }) => {
181
+ return resolveDomEditSelection(target, {
182
+ activeCompositionPath: activeCompPath,
183
+ isMasterView,
184
+ preferClipAncestor: options?.preferClipAncestor,
185
+ });
186
+ },
187
+ [activeCompPath, isMasterView],
188
+ );
189
+
190
+ const resolveDomSelectionFromPreviewPoint = useCallback(
191
+ (clientX: number, clientY: number, options?: { preferClipAncestor?: boolean }) => {
192
+ const iframe = previewIframeRef.current;
193
+ if (!iframe || captionEditMode) return null;
194
+ const target = getPreviewTargetFromPointer(iframe, clientX, clientY, activeCompPath);
195
+ if (!target) return null;
196
+ return buildDomSelectionFromTarget(target, {
197
+ preferClipAncestor: options?.preferClipAncestor,
198
+ });
199
+ },
200
+ [activeCompPath, buildDomSelectionFromTarget, captionEditMode, previewIframeRef],
201
+ );
202
+
203
+ const updateDomEditHoverSelection = useCallback((selection: DomEditSelection | null) => {
204
+ if (domEditSelectionsTargetSame(domEditHoverSelectionRef.current, selection)) return;
205
+ domEditHoverSelectionRef.current = selection;
206
+ setDomEditHoverSelection(selection);
207
+ }, []);
208
+
209
+ const buildDomSelectionForTimelineElement = useCallback(
210
+ (element: TimelineElement): DomEditSelection | null => {
211
+ const iframe = previewIframeRef.current;
212
+ let doc: Document | null = null;
213
+ try {
214
+ doc = iframe?.contentDocument ?? null;
215
+ } catch {
216
+ return null;
217
+ }
218
+ if (!doc) return null;
219
+
220
+ const targetElement = findElementForTimelineElement(doc, element, {
221
+ activeCompositionPath: activeCompPath,
222
+ compIdToSrc,
223
+ isMasterView,
224
+ });
225
+ return targetElement
226
+ ? buildDomSelectionFromTarget(targetElement, { preferClipAncestor: false })
227
+ : null;
228
+ },
229
+ [activeCompPath, buildDomSelectionFromTarget, compIdToSrc, isMasterView, previewIframeRef],
230
+ );
231
+
232
+ const handleTimelineElementSelect = useCallback(
233
+ (element: TimelineElement | null) => {
234
+ if (!STUDIO_INSPECTOR_PANELS_ENABLED) return;
235
+ if (!element) {
236
+ applyDomSelection(null, { revealPanel: false });
237
+ return;
238
+ }
239
+
240
+ const selection = buildDomSelectionForTimelineElement(element);
241
+ if (selection) applyDomSelection(selection);
242
+ },
243
+ [applyDomSelection, buildDomSelectionForTimelineElement],
244
+ );
245
+
246
+ const refreshDomEditSelectionFromPreview = useCallback(
247
+ (selection: DomEditSelection) => {
248
+ const iframe = previewIframeRef.current;
249
+ let doc: Document | null = null;
250
+ try {
251
+ doc = iframe?.contentDocument ?? null;
252
+ } catch {
253
+ return;
254
+ }
255
+ if (!doc) return;
256
+
257
+ const element = findElementForSelection(doc, selection, activeCompPath);
258
+ if (!element) return;
259
+
260
+ const nextSelection = buildDomSelectionFromTarget(element);
261
+ if (nextSelection) {
262
+ applyDomSelection(nextSelection, { revealPanel: false, preserveGroup: true });
263
+ }
264
+ },
265
+ [activeCompPath, applyDomSelection, buildDomSelectionFromTarget, previewIframeRef],
266
+ );
267
+
268
+ const refreshDomEditGroupSelectionsFromPreview = useCallback(
269
+ (selections: DomEditSelection[]) => {
270
+ const iframe = previewIframeRef.current;
271
+ let doc: Document | null = null;
272
+ try {
273
+ doc = iframe?.contentDocument ?? null;
274
+ } catch {
275
+ return;
276
+ }
277
+ if (!doc) return;
278
+
279
+ const nextGroup: DomEditSelection[] = [];
280
+ for (const selection of selections) {
281
+ const element = findElementForSelection(doc, selection, activeCompPath);
282
+ if (!element) continue;
283
+ const nextSelection = buildDomSelectionFromTarget(element);
284
+ if (nextSelection) nextGroup.push(nextSelection);
285
+ }
286
+ if (nextGroup.length === 0) return;
287
+
288
+ const currentSelection = domEditSelectionRef.current;
289
+ const nextSelection =
290
+ nextGroup.find((selection) => domEditSelectionsTargetSame(selection, currentSelection)) ??
291
+ nextGroup[0] ??
292
+ null;
293
+
294
+ domEditSelectionRef.current = nextSelection;
295
+ domEditGroupSelectionsRef.current = nextGroup;
296
+ setDomEditSelection(nextSelection);
297
+ setDomEditGroupSelections(nextGroup);
298
+
299
+ if (nextSelection) {
300
+ setSelectedTimelineElementId(
301
+ findMatchingTimelineElementId(nextSelection, timelineElements),
302
+ );
303
+ } else {
304
+ setSelectedTimelineElementId(null);
305
+ }
306
+ },
307
+ [
308
+ activeCompPath,
309
+ buildDomSelectionFromTarget,
310
+ setSelectedTimelineElementId,
311
+ timelineElements,
312
+ previewIframeRef,
313
+ ],
314
+ );
315
+
316
+ // ── Effects ──
317
+
318
+ // Clear hover on caption mode change
319
+ // eslint-disable-next-line no-restricted-syntax
320
+ useEffect(() => {
321
+ if (captionEditMode) updateDomEditHoverSelection(null);
322
+ }, [captionEditMode, updateDomEditHoverSelection]);
323
+
324
+ // Clear hover on composition/project/preview change
325
+ // eslint-disable-next-line no-restricted-syntax
326
+ useEffect(() => {
327
+ updateDomEditHoverSelection(null);
328
+ }, [activeCompPath, projectId, previewIframe, refreshKey, updateDomEditHoverSelection]);
329
+
330
+ // Clear hover when matching selection
331
+ // eslint-disable-next-line no-restricted-syntax
332
+ useEffect(() => {
333
+ if (!domEditHoverSelection) return;
334
+ const hoverMatchesSelection = domEditSelectionsTargetSame(
335
+ domEditHoverSelection,
336
+ domEditSelection,
337
+ );
338
+ const hoverMatchesGroup = domEditSelectionInGroup(
339
+ domEditGroupSelections,
340
+ domEditHoverSelection,
341
+ );
342
+ if (!hoverMatchesSelection && !hoverMatchesGroup) return;
343
+ updateDomEditHoverSelection(null);
344
+ }, [
345
+ domEditGroupSelections,
346
+ domEditHoverSelection,
347
+ domEditSelection,
348
+ updateDomEditHoverSelection,
349
+ ]);
350
+
351
+ // Clear hover when element disconnected
352
+ // eslint-disable-next-line no-restricted-syntax
353
+ useEffect(() => {
354
+ if (!domEditHoverSelection) return;
355
+ if (domEditHoverSelection.element.isConnected) return;
356
+ updateDomEditHoverSelection(null);
357
+ }, [domEditHoverSelection, updateDomEditHoverSelection]);
358
+
359
+ // Clear selection on caption mode change
360
+ // eslint-disable-next-line no-restricted-syntax
361
+ useEffect(() => {
362
+ if (!captionEditMode) return;
363
+ applyDomSelection(null, { revealPanel: false });
364
+ }, [applyDomSelection, captionEditMode]);
365
+
366
+ // Disabled inspector effect
367
+ // eslint-disable-next-line no-restricted-syntax
368
+ useEffect(() => {
369
+ if (STUDIO_INSPECTOR_PANELS_ENABLED) return;
370
+ updateDomEditHoverSelection(null);
371
+ applyDomSelection(null, { revealPanel: false });
372
+ if (rightPanelTab !== "renders") setRightPanelTab("renders");
373
+ }, [applyDomSelection, rightPanelTab, updateDomEditHoverSelection, setRightPanelTab]);
374
+
375
+ return {
376
+ // State
377
+ domEditSelection,
378
+ domEditGroupSelections,
379
+ domEditHoverSelection,
380
+ // Refs
381
+ domEditSelectionRef,
382
+ domEditGroupSelectionsRef,
383
+ domEditHoverSelectionRef,
384
+ // State setters
385
+ setDomEditSelection,
386
+ setDomEditGroupSelections,
387
+ // Callbacks
388
+ applyDomSelection,
389
+ clearDomSelection,
390
+ buildDomSelectionFromTarget,
391
+ resolveDomSelectionFromPreviewPoint,
392
+ updateDomEditHoverSelection,
393
+ buildDomSelectionForTimelineElement,
394
+ handleTimelineElementSelect,
395
+ refreshDomEditSelectionFromPreview,
396
+ refreshDomEditGroupSelectionsFromPreview,
397
+ };
398
+ }