@hyperframes/studio 0.6.0-alpha.8 → 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 +35 -4
  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-ClYcrksa.js +0 -108
  66. package/src/player/components/TimelineClip.test.ts +0 -92
@@ -0,0 +1,198 @@
1
+ import { PropertyPanel } from "./editor/PropertyPanel";
2
+ import { MotionPanel } from "./editor/MotionPanel";
3
+ import { CaptionPropertyPanel } from "../captions/components/CaptionPropertyPanel";
4
+ import { RenderQueue } from "./renders/RenderQueue";
5
+ import type { RenderJob } from "./renders/useRenderQueue";
6
+ import type { StudioGsapMotion } from "./editor/studioMotion";
7
+ import {
8
+ STUDIO_INSPECTOR_PANELS_ENABLED,
9
+ STUDIO_MOTION_PANEL_ENABLED,
10
+ } from "./editor/manualEditingAvailability";
11
+ import { useCallback } from "react";
12
+ import { resolveDomEditSelection, type DomEditLayerItem } from "./editor/domEditing";
13
+ import { useStudioContext } from "../contexts/StudioContext";
14
+ import { usePanelLayoutContext } from "../contexts/PanelLayoutContext";
15
+ import { useFileManagerContext } from "../contexts/FileManagerContext";
16
+ import { useDomEditContext } from "../contexts/DomEditContext";
17
+
18
+ export interface StudioRightPanelProps {
19
+ selectedStudioMotion: StudioGsapMotion | null;
20
+ designPanelActive: boolean;
21
+ motionPanelActive: boolean;
22
+ }
23
+
24
+ export function StudioRightPanel({
25
+ selectedStudioMotion,
26
+ designPanelActive,
27
+ motionPanelActive,
28
+ }: StudioRightPanelProps) {
29
+ const {
30
+ rightWidth,
31
+ rightPanelTab,
32
+ setRightPanelTab,
33
+ handlePanelResizeStart,
34
+ handlePanelResizeMove,
35
+ handlePanelResizeEnd,
36
+ } = usePanelLayoutContext();
37
+
38
+ const {
39
+ captionEditMode,
40
+ previewIframeRef,
41
+ projectId,
42
+ activeCompPath,
43
+ compositionDimensions,
44
+ waitForPendingDomEditSaves,
45
+ renderQueue,
46
+ } = useStudioContext();
47
+
48
+ const {
49
+ domEditSelection,
50
+ domEditGroupSelections,
51
+ copiedAgentPrompt,
52
+ clearDomSelection,
53
+ handleDomStyleCommit,
54
+ handleDomPathOffsetCommit,
55
+ handleDomBoxSizeCommit,
56
+ handleDomRotationCommit,
57
+ handleDomTextCommit,
58
+ handleDomTextFieldStyleCommit,
59
+ handleDomAddTextField,
60
+ handleDomRemoveTextField,
61
+ handleDomManualEditsReset,
62
+ handleAskAgent,
63
+ handleDomMotionCommit,
64
+ handleDomMotionClear,
65
+ applyDomSelection,
66
+ } = useDomEditContext();
67
+
68
+ const { assets, fontAssets, handleImportFiles, handleImportFonts } = useFileManagerContext();
69
+
70
+ const isMasterView = !activeCompPath || activeCompPath === "index.html";
71
+ const handleSelectLayer = useCallback(
72
+ (layer: DomEditLayerItem) => {
73
+ const selection = resolveDomEditSelection(layer.element, {
74
+ activeCompositionPath: activeCompPath,
75
+ isMasterView,
76
+ preferClipAncestor: false,
77
+ });
78
+ if (selection) applyDomSelection(selection);
79
+ },
80
+ [activeCompPath, isMasterView, applyDomSelection],
81
+ );
82
+
83
+ const renderJobs = renderQueue.jobs as RenderJob[];
84
+
85
+ return (
86
+ <>
87
+ <div
88
+ className="group w-2 flex-shrink-0 cursor-col-resize flex items-center justify-center"
89
+ style={{ touchAction: "none" }}
90
+ onPointerDown={(e) => handlePanelResizeStart("right", e)}
91
+ onPointerMove={handlePanelResizeMove}
92
+ onPointerUp={handlePanelResizeEnd}
93
+ >
94
+ <div className="h-[52px] w-px bg-white/12 transition-colors group-hover:bg-white/18 group-active:bg-white/24" />
95
+ </div>
96
+ <div
97
+ className="flex flex-col border-l border-neutral-800 bg-neutral-900 flex-shrink-0"
98
+ style={{ width: rightWidth }}
99
+ >
100
+ {captionEditMode ? (
101
+ <CaptionPropertyPanel iframeRef={previewIframeRef} />
102
+ ) : (
103
+ <>
104
+ <div className="flex items-center gap-1 border-b border-neutral-800 px-3 py-2">
105
+ {STUDIO_INSPECTOR_PANELS_ENABLED && (
106
+ <>
107
+ <button
108
+ type="button"
109
+ onClick={() => setRightPanelTab("design")}
110
+ className={`h-8 rounded-xl px-3 text-[11px] font-medium transition-colors ${
111
+ rightPanelTab === "design"
112
+ ? "bg-neutral-800 text-white"
113
+ : "text-neutral-500 hover:bg-neutral-800/70 hover:text-neutral-200"
114
+ }`}
115
+ >
116
+ Design
117
+ </button>
118
+ {STUDIO_MOTION_PANEL_ENABLED && (
119
+ <button
120
+ type="button"
121
+ onClick={() => setRightPanelTab("motion")}
122
+ className={`h-8 rounded-xl px-3 text-[11px] font-medium transition-colors ${
123
+ rightPanelTab === "motion"
124
+ ? "bg-neutral-800 text-white"
125
+ : "text-neutral-500 hover:bg-neutral-800/70 hover:text-neutral-200"
126
+ }`}
127
+ >
128
+ Motion
129
+ </button>
130
+ )}
131
+ </>
132
+ )}
133
+ <button
134
+ type="button"
135
+ onClick={() => setRightPanelTab("renders")}
136
+ className={`h-8 rounded-xl px-3 text-[11px] font-medium transition-colors ${
137
+ rightPanelTab === "renders"
138
+ ? "bg-neutral-800 text-white"
139
+ : "text-neutral-500 hover:bg-neutral-800/70 hover:text-neutral-200"
140
+ }`}
141
+ >
142
+ {renderJobs.length > 0 ? `Renders (${renderJobs.length})` : "Renders"}
143
+ </button>
144
+ </div>
145
+ <div className="min-h-0 flex-1">
146
+ {designPanelActive ? (
147
+ <PropertyPanel
148
+ projectId={projectId}
149
+ assets={assets}
150
+ element={domEditGroupSelections.length > 1 ? null : domEditSelection}
151
+ multiSelectCount={domEditGroupSelections.length}
152
+ copiedAgentPrompt={copiedAgentPrompt}
153
+ onClearSelection={clearDomSelection}
154
+ onSetStyle={handleDomStyleCommit}
155
+ onSetManualOffset={handleDomPathOffsetCommit}
156
+ onSetManualSize={handleDomBoxSizeCommit}
157
+ onSetManualRotation={handleDomRotationCommit}
158
+ onSetText={handleDomTextCommit}
159
+ onSetTextFieldStyle={handleDomTextFieldStyleCommit}
160
+ onAddTextField={handleDomAddTextField}
161
+ onRemoveTextField={handleDomRemoveTextField}
162
+ onResetManualEdits={handleDomManualEditsReset}
163
+ onAskAgent={handleAskAgent}
164
+ onImportAssets={handleImportFiles}
165
+ fontAssets={fontAssets}
166
+ onImportFonts={handleImportFonts}
167
+ activeCompositionPath={activeCompPath}
168
+ onSelectLayer={handleSelectLayer}
169
+ />
170
+ ) : motionPanelActive ? (
171
+ <MotionPanel
172
+ element={domEditGroupSelections.length > 1 ? null : domEditSelection}
173
+ motion={selectedStudioMotion}
174
+ onClearSelection={clearDomSelection}
175
+ onSetMotion={handleDomMotionCommit}
176
+ onClearMotion={handleDomMotionClear}
177
+ />
178
+ ) : (
179
+ <RenderQueue
180
+ jobs={renderJobs}
181
+ projectId={projectId}
182
+ onDelete={renderQueue.deleteRender}
183
+ onClearCompleted={renderQueue.clearCompleted}
184
+ onStartRender={async (format, quality, resolution, fps) => {
185
+ await waitForPendingDomEditSaves();
186
+ await renderQueue.startRender({ fps, quality, format, resolution });
187
+ }}
188
+ compositionDimensions={compositionDimensions}
189
+ isRendering={renderQueue.isRendering}
190
+ />
191
+ )}
192
+ </div>
193
+ </>
194
+ )}
195
+ </div>
196
+ </>
197
+ );
198
+ }
@@ -0,0 +1,89 @@
1
+ import {
2
+ getNextTimelineZoomPercent,
3
+ getTimelineZoomPercent,
4
+ } from "../player/components/timelineZoom";
5
+ import { getTimelineToggleTitle } from "../utils/timelineDiscovery";
6
+ import { usePlayerStore } from "../player";
7
+
8
+ interface TimelineToolbarProps {
9
+ toggleTimelineVisibility: () => void;
10
+ }
11
+
12
+ export function TimelineToolbar({ toggleTimelineVisibility }: TimelineToolbarProps) {
13
+ const zoomMode = usePlayerStore((s) => s.zoomMode);
14
+ const manualZoomPercent = usePlayerStore((s) => s.manualZoomPercent);
15
+ const setZoomMode = usePlayerStore((s) => s.setZoomMode);
16
+ const setManualZoomPercent = usePlayerStore((s) => s.setManualZoomPercent);
17
+ const displayedTimelineZoomPercent = getTimelineZoomPercent(zoomMode, manualZoomPercent);
18
+
19
+ return (
20
+ <div className="border-b border-neutral-800/40 bg-neutral-950/96">
21
+ <div className="flex items-center justify-between px-3 py-2">
22
+ <div className="text-[10px] font-medium uppercase tracking-[0.16em] text-neutral-500">
23
+ Timeline
24
+ </div>
25
+ <div className="flex items-center gap-1">
26
+ <button
27
+ type="button"
28
+ onClick={() => setZoomMode("fit")}
29
+ className={`h-7 px-2.5 rounded-md border text-[11px] font-medium transition-colors ${
30
+ zoomMode === "fit"
31
+ ? "border-studio-accent/30 bg-studio-accent/10 text-studio-accent"
32
+ : "border-neutral-800 text-neutral-400 hover:border-neutral-700 hover:text-neutral-200"
33
+ }`}
34
+ title="Fit timeline to width"
35
+ >
36
+ Fit
37
+ </button>
38
+ <button
39
+ type="button"
40
+ onClick={() => {
41
+ setZoomMode("manual");
42
+ setManualZoomPercent(getNextTimelineZoomPercent("out", zoomMode, manualZoomPercent));
43
+ }}
44
+ className="h-7 w-7 rounded-md border border-neutral-800 text-neutral-400 transition-colors hover:border-neutral-700 hover:text-neutral-200"
45
+ title="Zoom out"
46
+ >
47
+ -
48
+ </button>
49
+ <div className="min-w-[58px] text-center text-[10px] font-medium tabular-nums text-neutral-500">
50
+ {`${displayedTimelineZoomPercent}%`}
51
+ </div>
52
+ <button
53
+ type="button"
54
+ onClick={() => {
55
+ setZoomMode("manual");
56
+ setManualZoomPercent(getNextTimelineZoomPercent("in", zoomMode, manualZoomPercent));
57
+ }}
58
+ className="h-7 w-7 rounded-md border border-neutral-800 text-neutral-400 transition-colors hover:border-neutral-700 hover:text-neutral-200"
59
+ title="Zoom in"
60
+ >
61
+ +
62
+ </button>
63
+ <button
64
+ type="button"
65
+ onClick={toggleTimelineVisibility}
66
+ className="ml-1 flex h-7 w-7 items-center justify-center rounded-md text-neutral-500 transition-colors hover:bg-neutral-900 hover:text-neutral-200"
67
+ title={getTimelineToggleTitle(true)}
68
+ aria-label="Hide timeline editor"
69
+ >
70
+ <svg
71
+ width="14"
72
+ height="14"
73
+ viewBox="0 0 24 24"
74
+ fill="none"
75
+ stroke="currentColor"
76
+ strokeWidth="1.8"
77
+ strokeLinecap="round"
78
+ strokeLinejoin="round"
79
+ aria-hidden="true"
80
+ >
81
+ <path d="M5 7h14" />
82
+ <path d="m8 11 4 4 4-4" />
83
+ </svg>
84
+ </button>
85
+ </div>
86
+ </div>
87
+ </div>
88
+ );
89
+ }
@@ -85,6 +85,20 @@ interface DomEditOverlayProps {
85
85
  onRotationCommit: (selection: DomEditSelection, next: { angle: number }) => Promise<void> | void;
86
86
  }
87
87
 
88
+ function isElementVisibleForOverlay(el: HTMLElement): boolean {
89
+ const win = el.ownerDocument.defaultView;
90
+ if (!win) return true;
91
+ let current: HTMLElement | null = el;
92
+ while (current) {
93
+ const computed = win.getComputedStyle(current);
94
+ if (computed.display === "none" || computed.visibility === "hidden") return false;
95
+ const opacity = Number.parseFloat(computed.opacity);
96
+ if (Number.isFinite(opacity) && opacity <= 0.01) return false;
97
+ current = current.parentElement;
98
+ }
99
+ return true;
100
+ }
101
+
88
102
  function toOverlayRect(
89
103
  overlayEl: HTMLDivElement,
90
104
  iframe: HTMLIFrameElement,
@@ -534,7 +548,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
534
548
 
535
549
  if (sel) {
536
550
  const el = resolveElement(doc, sel, resolvedElementRef);
537
- if (el) {
551
+ if (el && isElementVisibleForOverlay(el)) {
538
552
  setNextOverlayRect(toOverlayRect(overlayEl, iframe, el));
539
553
  } else {
540
554
  clearOverlayRect();
@@ -4,10 +4,8 @@ import {
4
4
  buildStrokeWidthStyleUpdates,
5
5
  getClipPathInsetPx,
6
6
  getCssFilterFunctionPx,
7
- getPropertyPanelVisibleSections,
8
7
  inferBoxShadowPreset,
9
8
  inferClipPathPreset,
10
- isPropertyPanelMediaLikeSelection,
11
9
  normalizePanelPxValue,
12
10
  setCssFilterFunctionPx,
13
11
  } from "./PropertyPanel";
@@ -66,51 +64,4 @@ describe("PropertyPanel style helpers", () => {
66
64
  expect(buildStrokeStyleUpdates("none", "4px")).toEqual([["border-style", "none"]]);
67
65
  expect(buildStrokeStyleUpdates("solid", "4px")).toEqual([["border-style", "solid"]]);
68
66
  });
69
-
70
- it("orders the simplified default inspector sections around high-confidence edits", () => {
71
- expect(
72
- getPropertyPanelVisibleSections({
73
- hasSelection: true,
74
- canEditStyles: true,
75
- hasTextControls: true,
76
- hasColorControls: true,
77
- }),
78
- ).toEqual(["Text", "Layout", "Colors", "Radius", "Shadow"]);
79
-
80
- expect(
81
- getPropertyPanelVisibleSections({
82
- hasSelection: true,
83
- canEditStyles: true,
84
- hasTextControls: false,
85
- hasColorControls: false,
86
- }),
87
- ).toEqual(["Layout", "Radius", "Shadow"]);
88
- });
89
-
90
- it("treats media tags and background-image layers as image-like controls", () => {
91
- expect(
92
- isPropertyPanelMediaLikeSelection({
93
- tagName: "img",
94
- styles: {},
95
- }),
96
- ).toBe(true);
97
-
98
- expect(
99
- isPropertyPanelMediaLikeSelection({
100
- tagName: "div",
101
- styles: {
102
- "background-image": "url(/assets/studio.png)",
103
- },
104
- }),
105
- ).toBe(true);
106
-
107
- expect(
108
- isPropertyPanelMediaLikeSelection({
109
- tagName: "div",
110
- styles: {
111
- "background-image": "none",
112
- },
113
- }),
114
- ).toBe(false);
115
- });
116
67
  });