@hyperframes/studio 0.6.6 → 0.6.7

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 (55) hide show
  1. package/dist/assets/{hyperframes-player-T-ME1rqL.js → hyperframes-player-D0Yi3xMP.js} +2 -2
  2. package/dist/assets/{index-Bne9FFeo.css → index-Ckqo37Co.css} +1 -1
  3. package/dist/assets/index-Yvtxngdi.js +116 -0
  4. package/dist/index.html +2 -2
  5. package/package.json +4 -4
  6. package/src/App.tsx +54 -31
  7. package/src/components/StudioGlobalDragOverlay.tsx +26 -0
  8. package/src/components/StudioRightPanel.tsx +0 -2
  9. package/src/components/editor/DomEditOverlay.test.ts +1 -0
  10. package/src/components/editor/DomEditOverlay.tsx +2 -1
  11. package/src/components/editor/PropertyPanel.tsx +27 -36
  12. package/src/components/editor/domEditingElement.ts +1 -0
  13. package/src/components/editor/manualEdits.test.ts +39 -466
  14. package/src/components/editor/manualEdits.ts +6 -168
  15. package/src/components/editor/manualEditsDom.ts +361 -1
  16. package/src/components/editor/manualEditsParsing.ts +2 -240
  17. package/src/components/editor/manualEditsTypes.ts +1 -40
  18. package/src/components/editor/useDomEditOverlayGestures.ts +25 -8
  19. package/src/components/nle/NLEPreview.tsx +1 -1
  20. package/src/components/sidebar/CompositionsTab.tsx +9 -3
  21. package/src/contexts/DomEditContext.tsx +3 -0
  22. package/src/contexts/FileManagerContext.tsx +3 -0
  23. package/src/hooks/useAppHotkeys.ts +1 -4
  24. package/src/hooks/useDomEditCommits.ts +82 -77
  25. package/src/hooks/useDomEditSession.ts +4 -16
  26. package/src/hooks/useFileManager.ts +10 -1
  27. package/src/hooks/useManifestPersistence.ts +51 -187
  28. package/src/hooks/usePanelLayout.ts +10 -3
  29. package/src/hooks/usePreviewInteraction.ts +0 -1
  30. package/src/hooks/useStudioUrlState.ts +188 -0
  31. package/src/player/components/Player.tsx +15 -1
  32. package/src/player/components/PlayerControls.test.ts +17 -0
  33. package/src/player/components/PlayerControls.tsx +61 -0
  34. package/src/player/hooks/usePlaybackKeyboard.test.ts +174 -0
  35. package/src/player/hooks/usePlaybackKeyboard.ts +18 -15
  36. package/src/player/hooks/useTimelinePlayer.seek.test.ts +329 -0
  37. package/src/player/hooks/useTimelinePlayer.ts +76 -18
  38. package/src/player/hooks/useTimelineSyncCallbacks.ts +10 -4
  39. package/src/player/lib/playbackAdapter.test.ts +50 -0
  40. package/src/player/lib/playbackAdapter.ts +2 -2
  41. package/src/player/lib/playbackTypes.ts +1 -1
  42. package/src/player/lib/timelineDOM.ts +4 -2
  43. package/src/player/lib/timelineIframeHelpers.ts +63 -7
  44. package/src/player/store/playerStore.test.ts +105 -1
  45. package/src/player/store/playerStore.ts +12 -1
  46. package/src/utils/projectRouting.test.ts +15 -0
  47. package/src/utils/projectRouting.ts +46 -9
  48. package/src/utils/sourcePatcher.ts +50 -14
  49. package/src/utils/studioPreviewHelpers.test.ts +56 -0
  50. package/src/utils/studioPreviewHelpers.ts +51 -13
  51. package/src/utils/studioUiPreferences.test.ts +3 -0
  52. package/src/utils/studioUiPreferences.ts +4 -0
  53. package/src/utils/studioUrlState.test.ts +249 -0
  54. package/src/utils/studioUrlState.ts +135 -0
  55. package/dist/assets/index-DYqqzECY.js +0 -117
@@ -6,12 +6,21 @@ import { saveProjectFilesWithHistory } from "../utils/studioFileHistory";
6
6
  import { primaryFontFamilyValue } from "../utils/studioFontHelpers";
7
7
  import { getDomEditTargetKey, type DomEditSelection } from "../components/editor/domEditing";
8
8
  import {
9
- removeStudioManualEditsForSelection,
10
- type StudioManualEditManifest,
11
- upsertStudioBoxSizeEdit,
12
- upsertStudioPathOffsetEdit,
13
- upsertStudioRotationEdit,
9
+ applyStudioPathOffset,
10
+ applyStudioBoxSize,
11
+ applyStudioRotation,
12
+ clearStudioPathOffset,
13
+ clearStudioBoxSize,
14
+ clearStudioRotation,
14
15
  } from "../components/editor/manualEdits";
16
+ import {
17
+ buildPathOffsetPatches,
18
+ buildBoxSizePatches,
19
+ buildRotationPatches,
20
+ buildClearPathOffsetPatches,
21
+ buildClearBoxSizePatches,
22
+ buildClearRotationPatches,
23
+ } from "../components/editor/manualEditsDom";
15
24
  import {
16
25
  removeStudioMotionForSelection,
17
26
  type StudioGsapMotion,
@@ -48,15 +57,11 @@ export interface UseDomEditCommitsParams {
48
57
  activeCompPath: string | null;
49
58
  previewIframeRef: React.MutableRefObject<HTMLIFrameElement | null>;
50
59
  showToast: (message: string, tone?: "error" | "info") => void;
51
- commitStudioManualEditManifestOptimistically: (
52
- updateManifest: (manifest: StudioManualEditManifest) => StudioManualEditManifest,
53
- options: { label: string; coalesceKey: string },
54
- ) => void;
60
+ queueDomEditSave: (save: () => Promise<void>) => Promise<void>;
55
61
  commitStudioMotionManifestOptimistically: (
56
62
  updateManifest: (manifest: StudioMotionManifest) => StudioMotionManifest,
57
63
  options: { label: string; coalesceKey: string },
58
64
  ) => void;
59
- applyCurrentStudioManualEditsToPreview: (iframe: HTMLIFrameElement | null) => void;
60
65
  applyCurrentStudioMotionToPreview: (iframe: HTMLIFrameElement | null) => void;
61
66
  writeProjectFile: (path: string, content: string) => Promise<void>;
62
67
  domEditSaveTimestampRef: React.MutableRefObject<number>;
@@ -69,15 +74,12 @@ export interface UseDomEditCommitsParams {
69
74
 
70
75
  // From useDomSelection
71
76
  domEditSelection: DomEditSelection | null;
72
- domEditSelectionRef: React.MutableRefObject<DomEditSelection | null>;
73
- domEditGroupSelectionsRef: React.MutableRefObject<DomEditSelection[]>;
74
77
  applyDomSelection: (
75
78
  selection: DomEditSelection | null,
76
79
  options?: { revealPanel?: boolean; additive?: boolean; preserveGroup?: boolean },
77
80
  ) => void;
78
81
  clearDomSelection: () => void;
79
82
  refreshDomEditSelectionFromPreview: (selection: DomEditSelection) => void;
80
- refreshDomEditGroupSelectionsFromPreview: (selections: DomEditSelection[]) => void;
81
83
  buildDomSelectionFromTarget: (
82
84
  target: HTMLElement,
83
85
  options?: { preferClipAncestor?: boolean },
@@ -90,9 +92,8 @@ export function useDomEditCommits({
90
92
  activeCompPath,
91
93
  previewIframeRef,
92
94
  showToast,
93
- commitStudioManualEditManifestOptimistically,
95
+ queueDomEditSave,
94
96
  commitStudioMotionManifestOptimistically,
95
- applyCurrentStudioManualEditsToPreview,
96
97
  applyCurrentStudioMotionToPreview,
97
98
  writeProjectFile,
98
99
  domEditSaveTimestampRef,
@@ -103,11 +104,9 @@ export function useDomEditCommits({
103
104
  projectIdRef,
104
105
  reloadPreview,
105
106
  domEditSelection,
106
- domEditGroupSelectionsRef,
107
107
  applyDomSelection,
108
108
  clearDomSelection,
109
109
  refreshDomEditSelectionFromPreview,
110
- refreshDomEditGroupSelectionsFromPreview,
111
110
  buildDomSelectionFromTarget,
112
111
  }: UseDomEditCommitsParams) {
113
112
  const resolveImportedFontAsset = useCallback(
@@ -211,98 +210,104 @@ export function useDomEditCommits({
211
210
  resolveImportedFontAsset,
212
211
  });
213
212
 
214
- // ── Manifest commits ──
213
+ // ── Position patch helper ──
214
+
215
+ const commitPositionPatchToHtml = useCallback(
216
+ (
217
+ selection: DomEditSelection,
218
+ patches: Parameters<typeof applyPatchByTarget>[2][],
219
+ options: { label: string; coalesceKey: string; skipRefresh?: boolean },
220
+ ) => {
221
+ void queueDomEditSave(async () => {
222
+ await persistDomEditOperations(selection, patches, {
223
+ label: options.label,
224
+ coalesceKey: options.coalesceKey,
225
+ skipRefresh: options.skipRefresh ?? true,
226
+ });
227
+ }).catch((error) => {
228
+ const message = error instanceof Error ? error.message : "Failed to save position";
229
+ showToast(message);
230
+ });
231
+ },
232
+ [persistDomEditOperations, queueDomEditSave, showToast],
233
+ );
234
+
235
+ // ── Position commits ──
215
236
 
216
237
  const handleDomPathOffsetCommit = useCallback(
217
238
  (selection: DomEditSelection, next: { x: number; y: number }) => {
218
- commitStudioManualEditManifestOptimistically(
219
- (manifest) => upsertStudioPathOffsetEdit(manifest, selection, next),
220
- {
221
- label: "Move layer",
222
- coalesceKey: `path-offset:${getDomEditTargetKey(selection)}`,
223
- },
224
- );
225
- refreshDomEditSelectionFromPreview(selection);
239
+ applyStudioPathOffset(selection.element, next);
240
+ commitPositionPatchToHtml(selection, buildPathOffsetPatches(selection.element), {
241
+ label: "Move layer",
242
+ coalesceKey: `path-offset:${getDomEditTargetKey(selection)}`,
243
+ });
226
244
  },
227
- [commitStudioManualEditManifestOptimistically, refreshDomEditSelectionFromPreview],
245
+ [commitPositionPatchToHtml],
228
246
  );
229
247
 
230
248
  const handleDomGroupPathOffsetCommit = useCallback(
231
249
  (updates: DomEditGroupPathOffsetCommit[]) => {
232
250
  if (updates.length === 0) return;
233
251
  const coalesceKey = updates
234
- .map((update) => getDomEditTargetKey(update.selection))
252
+ .map((u) => getDomEditTargetKey(u.selection))
235
253
  .sort()
236
254
  .join(":");
237
- commitStudioManualEditManifestOptimistically(
238
- (manifest) =>
239
- updates.reduce(
240
- (nextManifest, update) =>
241
- upsertStudioPathOffsetEdit(nextManifest, update.selection, update.next),
242
- manifest,
243
- ),
244
- {
255
+ for (const { selection, next } of updates) {
256
+ applyStudioPathOffset(selection.element, next);
257
+ commitPositionPatchToHtml(selection, buildPathOffsetPatches(selection.element), {
245
258
  label: `Move ${updates.length} layers`,
246
259
  coalesceKey: `group-path-offset:${coalesceKey}`,
247
- },
248
- );
249
- refreshDomEditGroupSelectionsFromPreview(domEditGroupSelectionsRef.current);
260
+ });
261
+ }
250
262
  },
251
- [
252
- commitStudioManualEditManifestOptimistically,
253
- domEditGroupSelectionsRef,
254
- refreshDomEditGroupSelectionsFromPreview,
255
- ],
263
+ [commitPositionPatchToHtml],
256
264
  );
257
265
 
258
266
  const handleDomBoxSizeCommit = useCallback(
259
267
  (selection: DomEditSelection, next: { width: number; height: number }) => {
260
- commitStudioManualEditManifestOptimistically(
261
- (manifest) => upsertStudioBoxSizeEdit(manifest, selection, next),
262
- {
263
- label: "Resize layer box",
264
- coalesceKey: `box-size:${getDomEditTargetKey(selection)}`,
265
- },
266
- );
267
- refreshDomEditSelectionFromPreview(selection);
268
+ applyStudioBoxSize(selection.element, next);
269
+ commitPositionPatchToHtml(selection, buildBoxSizePatches(selection.element), {
270
+ label: "Resize layer box",
271
+ coalesceKey: `box-size:${getDomEditTargetKey(selection)}`,
272
+ });
268
273
  },
269
- [commitStudioManualEditManifestOptimistically, refreshDomEditSelectionFromPreview],
274
+ [commitPositionPatchToHtml],
270
275
  );
271
276
 
272
277
  const handleDomRotationCommit = useCallback(
273
278
  (selection: DomEditSelection, next: { angle: number }) => {
274
- commitStudioManualEditManifestOptimistically(
275
- (manifest) => upsertStudioRotationEdit(manifest, selection, next),
276
- {
277
- label: "Rotate layer",
278
- coalesceKey: `rotation:${getDomEditTargetKey(selection)}`,
279
- },
280
- );
281
- refreshDomEditSelectionFromPreview(selection);
279
+ applyStudioRotation(selection.element, next);
280
+ commitPositionPatchToHtml(selection, buildRotationPatches(selection.element), {
281
+ label: "Rotate layer",
282
+ coalesceKey: `rotation:${getDomEditTargetKey(selection)}`,
283
+ });
282
284
  },
283
- [commitStudioManualEditManifestOptimistically, refreshDomEditSelectionFromPreview],
285
+ [commitPositionPatchToHtml],
284
286
  );
285
287
 
286
288
  const handleDomManualEditsReset = useCallback(
287
289
  (selection: DomEditSelection) => {
288
- commitStudioManualEditManifestOptimistically(
289
- (manifest) => removeStudioManualEditsForSelection(manifest, selection),
290
- {
291
- label: "Reset layer edits",
292
- coalesceKey: `manual-reset:${getDomEditTargetKey(selection)}`,
293
- },
294
- );
295
- applyCurrentStudioManualEditsToPreview(previewIframeRef.current);
296
- refreshDomEditSelectionFromPreview(selection);
290
+ const element = selection.element;
291
+ const clearPatches = [
292
+ ...buildClearPathOffsetPatches(element),
293
+ ...buildClearBoxSizePatches(element),
294
+ ...buildClearRotationPatches(element),
295
+ ];
296
+ clearStudioPathOffset(element);
297
+ clearStudioBoxSize(element);
298
+ clearStudioRotation(element);
299
+ // skipRefresh:false triggers reloadPreview() which re-syncs selection on load
300
+ commitPositionPatchToHtml(selection, clearPatches, {
301
+ label: "Reset layer edits",
302
+ coalesceKey: `manual-reset:${getDomEditTargetKey(selection)}`,
303
+ skipRefresh: false,
304
+ });
297
305
  },
298
- [
299
- applyCurrentStudioManualEditsToPreview,
300
- commitStudioManualEditManifestOptimistically,
301
- refreshDomEditSelectionFromPreview,
302
- previewIframeRef,
303
- ],
306
+ [commitPositionPatchToHtml],
304
307
  );
305
308
 
309
+ // ── Motion commits ──
310
+
306
311
  const handleDomMotionCommit = useCallback(
307
312
  (
308
313
  selection: DomEditSelection,
@@ -2,7 +2,6 @@ import { useEffect } from "react";
2
2
  import type { TimelineElement } from "../player";
3
3
  import { STUDIO_INSPECTOR_PANELS_ENABLED } from "../components/editor/manualEditingAvailability";
4
4
  import { findElementForSelection } from "../components/editor/domEditing";
5
- import type { StudioManualEditManifest } from "../components/editor/manualEdits";
6
5
  import type { StudioMotionManifest } from "../components/editor/studioMotion";
7
6
  import type { ImportedFontAsset } from "../components/editor/fontAssets";
8
7
  import type { EditHistoryKind } from "../utils/editHistory";
@@ -36,15 +35,11 @@ export interface UseDomEditSessionParams {
36
35
  setRightPanelTab: (tab: RightPanelTab) => void;
37
36
  showToast: (message: string, tone?: "error" | "info") => void;
38
37
  refreshPreviewDocumentVersion: () => void;
39
- commitStudioManualEditManifestOptimistically: (
40
- updateManifest: (manifest: StudioManualEditManifest) => StudioManualEditManifest,
41
- options: { label: string; coalesceKey: string },
42
- ) => void;
38
+ queueDomEditSave: (save: () => Promise<void>) => Promise<void>;
43
39
  commitStudioMotionManifestOptimistically: (
44
40
  updateManifest: (manifest: StudioMotionManifest) => StudioMotionManifest,
45
41
  options: { label: string; coalesceKey: string },
46
42
  ) => void;
47
- applyCurrentStudioManualEditsToPreview: (iframe: HTMLIFrameElement | null) => void;
48
43
  applyCurrentStudioMotionToPreview: (iframe: HTMLIFrameElement | null) => void;
49
44
  readProjectFile: (path: string) => Promise<string>;
50
45
  writeProjectFile: (path: string, content: string) => Promise<void>;
@@ -85,9 +80,8 @@ export function useDomEditSession({
85
80
  setRightPanelTab,
86
81
  showToast,
87
82
  refreshPreviewDocumentVersion,
88
- commitStudioManualEditManifestOptimistically,
83
+ queueDomEditSave,
89
84
  commitStudioMotionManifestOptimistically,
90
- applyCurrentStudioManualEditsToPreview,
91
85
  applyCurrentStudioMotionToPreview,
92
86
  readProjectFile: _readProjectFile,
93
87
  writeProjectFile,
@@ -114,7 +108,6 @@ export function useDomEditSession({
114
108
  domEditGroupSelections,
115
109
  domEditHoverSelection,
116
110
  domEditSelectionRef,
117
- domEditGroupSelectionsRef,
118
111
  applyDomSelection,
119
112
  clearDomSelection,
120
113
  buildDomSelectionFromTarget,
@@ -123,7 +116,6 @@ export function useDomEditSession({
123
116
  buildDomSelectionForTimelineElement,
124
117
  handleTimelineElementSelect,
125
118
  refreshDomEditSelectionFromPreview,
126
- refreshDomEditGroupSelectionsFromPreview,
127
119
  } = useDomSelection({
128
120
  projectId,
129
121
  activeCompPath,
@@ -176,7 +168,6 @@ export function useDomEditSession({
176
168
  captionEditMode,
177
169
  compositionLoading,
178
170
  previewIframeRef,
179
- activeCompPath,
180
171
  showToast,
181
172
  applyDomSelection,
182
173
  resolveDomSelectionFromPreviewPoint,
@@ -208,9 +199,8 @@ export function useDomEditSession({
208
199
  activeCompPath,
209
200
  previewIframeRef,
210
201
  showToast,
211
- commitStudioManualEditManifestOptimistically,
202
+ queueDomEditSave,
212
203
  commitStudioMotionManifestOptimistically,
213
- applyCurrentStudioManualEditsToPreview,
214
204
  applyCurrentStudioMotionToPreview,
215
205
  writeProjectFile,
216
206
  domEditSaveTimestampRef,
@@ -221,12 +211,9 @@ export function useDomEditSession({
221
211
  projectIdRef,
222
212
  reloadPreview,
223
213
  domEditSelection,
224
- domEditSelectionRef,
225
- domEditGroupSelectionsRef,
226
214
  applyDomSelection,
227
215
  clearDomSelection,
228
216
  refreshDomEditSelectionFromPreview,
229
- refreshDomEditGroupSelectionsFromPreview,
230
217
  buildDomSelectionFromTarget,
231
218
  });
232
219
 
@@ -333,6 +320,7 @@ export function useDomEditSession({
333
320
  handleBlockedDomMove,
334
321
  handleDomManualDragStart,
335
322
  handleDomEditElementDelete,
323
+ buildDomSelectionFromTarget,
336
324
  buildDomSelectionForTimelineElement,
337
325
  updateDomEditHoverSelection,
338
326
  resolveImportedFontAsset,
@@ -36,6 +36,7 @@ export function useFileManager({
36
36
  const [editingFile, setEditingFile] = useState<EditingFile | null>(null);
37
37
  const [projectDir, setProjectDir] = useState<string | null>(null);
38
38
  const [fileTree, setFileTree] = useState<string[]>([]);
39
+ const [fileTreeLoaded, setFileTreeLoaded] = useState(false);
39
40
 
40
41
  // ── Refs ──
41
42
 
@@ -53,8 +54,12 @@ export function useFileManager({
53
54
 
54
55
  // eslint-disable-next-line no-restricted-syntax
55
56
  useEffect(() => {
56
- if (!projectId) return;
57
+ if (!projectId) {
58
+ setFileTreeLoaded(false);
59
+ return;
60
+ }
57
61
  let cancelled = false;
62
+ setFileTreeLoaded(false);
58
63
  fetch(`/api/projects/${projectId}`)
59
64
  .then((r) => r.json())
60
65
  .then((data: { files?: string[]; dir?: string }) => {
@@ -63,6 +68,9 @@ export function useFileManager({
63
68
  })
64
69
  .catch(() => {
65
70
  if (!cancelled) setProjectDir(null);
71
+ })
72
+ .finally(() => {
73
+ if (!cancelled) setFileTreeLoaded(true);
66
74
  });
67
75
  return () => {
68
76
  cancelled = true;
@@ -396,6 +404,7 @@ export function useFileManager({
396
404
  setEditingFile,
397
405
  projectDir,
398
406
  fileTree,
407
+ fileTreeLoaded,
399
408
  setFileTree,
400
409
 
401
410
  // Refs