@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
package/dist/index.html CHANGED
@@ -5,8 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
6
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
7
  <title>HyperFrames Studio</title>
8
- <script type="module" crossorigin src="/assets/index-DYqqzECY.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-Bne9FFeo.css">
8
+ <script type="module" crossorigin src="/assets/index-Yvtxngdi.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-Ckqo37Co.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,8 +32,8 @@
32
32
  "@phosphor-icons/react": "^2.1.10",
33
33
  "codemirror": "^6.0.1",
34
34
  "motion": "^12.38.0",
35
- "@hyperframes/core": "0.6.6",
36
- "@hyperframes/player": "0.6.6"
35
+ "@hyperframes/core": "0.6.7",
36
+ "@hyperframes/player": "0.6.7"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/react": "19",
@@ -47,7 +47,7 @@
47
47
  "vite": "^6.4.2",
48
48
  "vitest": "^3.2.4",
49
49
  "zustand": "^5.0.0",
50
- "@hyperframes/producer": "0.6.6"
50
+ "@hyperframes/producer": "0.6.7"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "react": "19",
package/src/App.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useState, useCallback, useRef, useMemo } from "react";
1
+ import { useState, useCallback, useRef, useMemo, useEffect } from "react";
2
2
  import type { LeftSidebarHandle } from "./components/sidebar/LeftSidebar";
3
3
  import { useRenderQueue } from "./components/renders/useRenderQueue";
4
4
  import { usePlayerStore } from "./player";
@@ -20,6 +20,7 @@ import { useFrameCapture } from "./hooks/useFrameCapture";
20
20
  import { useLintModal } from "./hooks/useLintModal";
21
21
  import { useCompositionDimensions } from "./hooks/useCompositionDimensions";
22
22
  import { useToast } from "./hooks/useToast";
23
+ import { useStudioUrlState } from "./hooks/useStudioUrlState";
23
24
  import {
24
25
  STUDIO_INSPECTOR_PANELS_ENABLED,
25
26
  STUDIO_MOTION_PANEL_ENABLED,
@@ -27,6 +28,7 @@ import {
27
28
  import { getStudioMotionForSelection } from "./components/editor/studioMotion";
28
29
  import type { DomEditSelection } from "./components/editor/domEditing";
29
30
  import { AskAgentModal } from "./components/AskAgentModal";
31
+ import { StudioGlobalDragOverlay } from "./components/StudioGlobalDragOverlay";
30
32
  import { StudioHeader } from "./components/StudioHeader";
31
33
  import { StudioLeftSidebar } from "./components/StudioLeftSidebar";
32
34
  import { StudioPreviewArea } from "./components/StudioPreviewArea";
@@ -38,11 +40,19 @@ import { FileManagerProvider } from "./contexts/FileManagerContext";
38
40
  import { DomEditProvider } from "./contexts/DomEditContext";
39
41
  import { StudioSplash } from "./components/StudioSplash";
40
42
  import { useServerConnection } from "./hooks/useServerConnection";
43
+ import {
44
+ normalizeStudioCompositionPath,
45
+ readStudioUrlStateFromWindow,
46
+ } from "./utils/studioUrlState";
41
47
 
42
48
  export function StudioApp() {
43
49
  const { projectId, resolving, waitingForServer } = useServerConnection();
50
+ const initialUrlStateRef = useRef(readStudioUrlStateFromWindow());
44
51
 
45
52
  const [activeCompPath, setActiveCompPath] = useState<string | null>(null);
53
+ const [activeCompPathHydrated, setActiveCompPathHydrated] = useState(
54
+ () => initialUrlStateRef.current.activeCompPath == null,
55
+ );
46
56
  const [compIdToSrc, setCompIdToSrc] = useState<Map<string, string>>(new Map());
47
57
  const [previewIframe, setPreviewIframe] = useState<HTMLIFrameElement | null>(null);
48
58
  const [compositionLoading, setCompositionLoading] = useState(true);
@@ -80,7 +90,10 @@ export function StudioApp() {
80
90
  }, []);
81
91
 
82
92
  const [timelineVisible, setTimelineVisible] = useState(
83
- () => readStudioUiPreferences().timelineVisible ?? true,
93
+ () =>
94
+ initialUrlStateRef.current.timelineVisible ??
95
+ readStudioUiPreferences().timelineVisible ??
96
+ true,
84
97
  );
85
98
  const toggleTimelineVisibility = useCallback(() => {
86
99
  setTimelineVisible((v) => {
@@ -89,7 +102,10 @@ export function StudioApp() {
89
102
  });
90
103
  }, []);
91
104
  const { appToast, showToast } = useToast();
92
- const panelLayout = usePanelLayout();
105
+ const panelLayout = usePanelLayout({
106
+ rightCollapsed: initialUrlStateRef.current.rightCollapsed,
107
+ rightPanelTab: initialUrlStateRef.current.rightPanelTab,
108
+ });
93
109
  const editHistory = usePersistentEditHistory({ projectId });
94
110
  const domEditSaveTimestampRef = useRef(0);
95
111
  const reloadPreview = useCallback(() => {
@@ -108,6 +124,18 @@ export function StudioApp() {
108
124
  setRefreshKey,
109
125
  });
110
126
 
127
+ useEffect(() => {
128
+ if (activeCompPathHydrated) return;
129
+ if (!fileManager.fileTreeLoaded) return;
130
+
131
+ const nextCompPath = normalizeStudioCompositionPath(
132
+ initialUrlStateRef.current.activeCompPath,
133
+ fileManager.fileTree,
134
+ );
135
+ setActiveCompPath((current) => (current === nextCompPath ? current : nextCompPath));
136
+ setActiveCompPathHydrated(true);
137
+ }, [activeCompPathHydrated, fileManager.fileTree, fileManager.fileTreeLoaded]);
138
+
111
139
  const manifestPersistence = useManifestPersistence({
112
140
  projectId,
113
141
  showToast,
@@ -116,6 +144,8 @@ export function StudioApp() {
116
144
  recordEdit: editHistory.recordEdit,
117
145
  previewIframeRef,
118
146
  activeCompPathRef,
147
+ domEditSaveTimestampRef,
148
+ reloadPreview: () => setRefreshKey((k) => k + 1),
119
149
  });
120
150
 
121
151
  const timelineEditing = useTimelineEditing({
@@ -169,12 +199,9 @@ export function StudioApp() {
169
199
  setRightPanelTab: panelLayout.setRightPanelTab,
170
200
  showToast,
171
201
  refreshPreviewDocumentVersion,
172
- commitStudioManualEditManifestOptimistically:
173
- manifestPersistence.commitStudioManualEditManifestOptimistically,
202
+ queueDomEditSave: manifestPersistence.queueDomEditSave,
174
203
  commitStudioMotionManifestOptimistically:
175
204
  manifestPersistence.commitStudioMotionManifestOptimistically,
176
- applyCurrentStudioManualEditsToPreview:
177
- manifestPersistence.applyCurrentStudioManualEditsToPreview,
178
205
  applyCurrentStudioMotionToPreview: manifestPersistence.applyCurrentStudioMotionToPreview,
179
206
  readProjectFile: fileManager.readProjectFile,
180
207
  writeProjectFile: fileManager.writeProjectFile,
@@ -284,6 +311,25 @@ export function StudioApp() {
284
311
  const inspectorButtonActive =
285
312
  STUDIO_INSPECTOR_PANELS_ENABLED && !panelLayout.rightCollapsed && inspectorPanelActive;
286
313
 
314
+ useStudioUrlState({
315
+ projectId,
316
+ activeCompPath,
317
+ currentTime,
318
+ duration: effectiveTimelineDuration,
319
+ isPlaying,
320
+ compositionLoading,
321
+ refreshKey,
322
+ previewIframeRef,
323
+ rightPanelTab: panelLayout.rightPanelTab,
324
+ rightCollapsed: panelLayout.rightCollapsed,
325
+ timelineVisible,
326
+ activeCompPathHydrated,
327
+ domEditSelection: domEditSession.domEditSelection,
328
+ buildDomSelectionFromTarget: domEditSession.buildDomSelectionFromTarget,
329
+ applyDomSelection: domEditSession.applyDomSelection,
330
+ initialState: initialUrlStateRef.current,
331
+ });
332
+
287
333
  // StudioProvider performs its own useMemo — no need for a second memo here.
288
334
  const studioCtxValue: StudioContextValue = {
289
335
  projectId: projectId!,
@@ -420,30 +466,7 @@ export function StudioApp() {
420
466
  />
421
467
  )}
422
468
 
423
- {globalDragOver && (
424
- <div className="absolute inset-0 z-[90] flex items-center justify-center bg-black/50 backdrop-blur-sm pointer-events-none">
425
- <div className="flex flex-col items-center gap-3 px-8 py-6 rounded-xl border-2 border-dashed border-studio-accent/60 bg-studio-accent/[0.06]">
426
- <svg
427
- width="32"
428
- height="32"
429
- viewBox="0 0 24 24"
430
- fill="none"
431
- stroke="currentColor"
432
- strokeWidth="1.5"
433
- strokeLinecap="round"
434
- strokeLinejoin="round"
435
- className="text-studio-accent"
436
- >
437
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
438
- <polyline points="7 10 12 15 17 10" />
439
- <line x1="12" y1="15" x2="12" y2="3" />
440
- </svg>
441
- <span className="text-sm font-medium text-studio-accent">
442
- Drop files to import into project
443
- </span>
444
- </div>
445
- </div>
446
- )}
469
+ {globalDragOver && <StudioGlobalDragOverlay />}
447
470
 
448
471
  {appToast && (
449
472
  <div
@@ -0,0 +1,26 @@
1
+ export function StudioGlobalDragOverlay() {
2
+ return (
3
+ <div className="absolute inset-0 z-[90] flex items-center justify-center bg-black/50 backdrop-blur-sm pointer-events-none">
4
+ <div className="flex flex-col items-center gap-3 px-8 py-6 rounded-xl border-2 border-dashed border-studio-accent/60 bg-studio-accent/[0.06]">
5
+ <svg
6
+ width="32"
7
+ height="32"
8
+ viewBox="0 0 24 24"
9
+ fill="none"
10
+ stroke="currentColor"
11
+ strokeWidth="1.5"
12
+ strokeLinecap="round"
13
+ strokeLinejoin="round"
14
+ className="text-studio-accent"
15
+ >
16
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
17
+ <polyline points="7 10 12 15 17 10" />
18
+ <line x1="12" y1="15" x2="12" y2="3" />
19
+ </svg>
20
+ <span className="text-sm font-medium text-studio-accent">
21
+ Drop files to import into project
22
+ </span>
23
+ </div>
24
+ </div>
25
+ );
26
+ }
@@ -59,7 +59,6 @@ export function StudioRightPanel({
59
59
  handleDomTextFieldStyleCommit,
60
60
  handleDomAddTextField,
61
61
  handleDomRemoveTextField,
62
- handleDomManualEditsReset,
63
62
  handleAskAgent,
64
63
  handleDomMotionCommit,
65
64
  handleDomMotionClear,
@@ -173,7 +172,6 @@ export function StudioRightPanel({
173
172
  onSetTextFieldStyle={handleDomTextFieldStyleCommit}
174
173
  onAddTextField={handleDomAddTextField}
175
174
  onRemoveTextField={handleDomRemoveTextField}
176
- onResetManualEdits={handleDomManualEditsReset}
177
175
  onAskAgent={handleAskAgent}
178
176
  onImportAssets={handleImportFiles}
179
177
  fontAssets={fontAssets}
@@ -113,6 +113,7 @@ describe("DomEditOverlay", () => {
113
113
  capabilities: {
114
114
  canEditText: true,
115
115
  canEditLayout: true,
116
+ canMove: true,
116
117
  canApplyManualOffset: true,
117
118
  canApplyManualSize: false,
118
119
  canApplyManualRotation: false,
@@ -138,6 +138,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
138
138
 
139
139
  const gestures = createDomEditOverlayGestureHandlers({
140
140
  overlayRef,
141
+ iframeRef,
141
142
  boxRef,
142
143
  selectionRef,
143
144
  overlayRectRef,
@@ -307,7 +308,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
307
308
  cursor: allowCanvasMovement && groupCanMove ? "move" : "default",
308
309
  }}
309
310
  onPointerDown={(e) => {
310
- if (!allowCanvasMovement || e.shiftKey) return;
311
+ if (!allowCanvasMovement || !groupCanMove || e.shiftKey) return;
311
312
  gestures.startGroupDrag(e);
312
313
  }}
313
314
  onMouseDown={suppressBoxMouseDown}
@@ -1,5 +1,5 @@
1
1
  import { memo } from "react";
2
- import { Eye, Layers, MessageSquare, Move, RotateCcw, X } from "../../icons/SystemIcons";
2
+ import { Eye, Layers, MessageSquare, Move, X } from "../../icons/SystemIcons";
3
3
  import {
4
4
  collectDomEditLayerItems,
5
5
  getDomEditLayerKey,
@@ -46,7 +46,6 @@ interface PropertyPanelProps {
46
46
  onSetTextFieldStyle: (fieldKey: string, property: string, value: string) => void;
47
47
  onAddTextField: (afterFieldKey?: string) => string | Promise<string | null> | null;
48
48
  onRemoveTextField: (fieldKey: string) => void;
49
- onResetManualEdits: (element: DomEditSelection) => void;
50
49
  onAskAgent: () => void;
51
50
  onImportAssets?: (files: FileList) => Promise<string[]>;
52
51
  fontAssets?: ImportedFontAsset[];
@@ -134,7 +133,6 @@ export const PropertyPanel = memo(function PropertyPanel({
134
133
  onSetTextFieldStyle,
135
134
  onAddTextField,
136
135
  onRemoveTextField,
137
- onResetManualEdits,
138
136
  onAskAgent,
139
137
  onImportAssets,
140
138
  fontAssets = [],
@@ -146,30 +144,32 @@ export const PropertyPanel = memo(function PropertyPanel({
146
144
 
147
145
  if (!element) {
148
146
  return (
149
- <div className="flex h-full flex-col items-center justify-center bg-neutral-900 px-6 text-center">
150
- {multiSelectCount > 1 ? (
151
- <>
152
- <Layers size={18} className="mb-3 text-neutral-600" />
153
- <p className="text-sm font-medium text-neutral-200">
154
- {multiSelectCount} elements selected
155
- </p>
156
- <p className="mt-2 max-w-[260px] text-xs leading-5 text-neutral-500">
157
- Select a single element to edit its properties. Click an element in the preview or use
158
- the timeline layer panel.
159
- </p>
160
- </>
161
- ) : (
162
- <>
163
- <Eye size={18} className="mb-3 text-neutral-600" />
164
- <p className="text-sm font-medium text-neutral-200">
165
- Select an element in the preview.
166
- </p>
167
- <p className="mt-2 max-w-[260px] text-xs leading-5 text-neutral-500">
168
- The inspector is tuned for element edits with safer geometry controls, color picking,
169
- and cleaner grouped layer controls.
170
- </p>
171
- </>
172
- )}
147
+ <div className="flex h-full flex-col bg-neutral-900">
148
+ <div className="flex flex-1 flex-col items-center justify-center px-6 text-center">
149
+ {multiSelectCount > 1 ? (
150
+ <>
151
+ <Layers size={18} className="mb-3 text-neutral-600" />
152
+ <p className="text-sm font-medium text-neutral-200">
153
+ {multiSelectCount} elements selected
154
+ </p>
155
+ <p className="mt-2 max-w-[260px] text-xs leading-5 text-neutral-500">
156
+ Select a single element to edit its properties. Click an element in the preview or
157
+ use the timeline layer panel.
158
+ </p>
159
+ </>
160
+ ) : (
161
+ <>
162
+ <Eye size={18} className="mb-3 text-neutral-600" />
163
+ <p className="text-sm font-medium text-neutral-200">
164
+ Select an element in the preview.
165
+ </p>
166
+ <p className="mt-2 max-w-[260px] text-xs leading-5 text-neutral-500">
167
+ The inspector is tuned for element edits with safer geometry controls, color
168
+ picking, and cleaner grouped layer controls.
169
+ </p>
170
+ </>
171
+ )}
172
+ </div>
173
173
  </div>
174
174
  );
175
175
  }
@@ -253,15 +253,6 @@ export const PropertyPanel = memo(function PropertyPanel({
253
253
  <MessageSquare size={15} />
254
254
  <span>{copiedAgentPrompt ? "Prompt copied" : "Ask agent"}</span>
255
255
  </button>
256
- <button
257
- type="button"
258
- onClick={() => onResetManualEdits(element)}
259
- title="Reset move, size, and rotation edits"
260
- className="inline-flex h-8 items-center justify-center gap-2 rounded-xl border border-neutral-700 bg-neutral-950 px-3.5 text-[11px] font-medium text-neutral-100 transition-colors hover:border-neutral-500 hover:text-white"
261
- >
262
- <RotateCcw size={14} />
263
- <span>Reset edits</span>
264
- </button>
265
256
  </div>
266
257
  </div>
267
258
 
@@ -118,6 +118,7 @@ export function getDomLayerPatchTarget(
118
118
  activeCompositionPath: string | null,
119
119
  ): Pick<DomEditSelection, "id" | "selector" | "selectorIndex" | "sourceFile"> | null {
120
120
  if (!isInspectableLayerElement(el)) return null;
121
+ if (el.hasAttribute("data-composition-id")) return null;
121
122
 
122
123
  const selector = buildStableSelector(el);
123
124
  if (!selector) return null;