@hyperframes/studio 0.6.0-alpha.9 → 0.6.1

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 (111) hide show
  1. package/dist/assets/hyperframes-player-CzwFysqv.js +418 -0
  2. package/dist/assets/index-D1JDq7Gg.css +1 -0
  3. package/dist/assets/index-hYc4aP7M.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 +421 -4303
  8. package/src/captions/components/CaptionOverlay.tsx +13 -246
  9. package/src/captions/components/CaptionOverlayUtils.ts +221 -0
  10. package/src/components/AskAgentModal.tsx +120 -0
  11. package/src/components/StudioHeader.tsx +133 -0
  12. package/src/components/StudioLeftSidebar.tsx +125 -0
  13. package/src/components/StudioPreviewArea.tsx +167 -0
  14. package/src/components/StudioRightPanel.tsx +198 -0
  15. package/src/components/TimelineToolbar.tsx +89 -0
  16. package/src/components/editor/DomEditOverlay.tsx +88 -993
  17. package/src/components/editor/EaseCurveEditor.tsx +221 -0
  18. package/src/components/editor/FileTree.tsx +13 -621
  19. package/src/components/editor/FileTreeIcons.tsx +128 -0
  20. package/src/components/editor/FileTreeNodes.tsx +496 -0
  21. package/src/components/editor/MotionPanel.tsx +16 -390
  22. package/src/components/editor/MotionPanelFields.tsx +185 -0
  23. package/src/components/editor/PropertyPanel.test.ts +0 -49
  24. package/src/components/editor/PropertyPanel.tsx +132 -2763
  25. package/src/components/editor/domEditOverlayGeometry.ts +211 -0
  26. package/src/components/editor/domEditOverlayGestures.ts +138 -0
  27. package/src/components/editor/domEditOverlayStartGesture.ts +155 -0
  28. package/src/components/editor/domEditing.ts +44 -1117
  29. package/src/components/editor/domEditingAgentPrompt.ts +97 -0
  30. package/src/components/editor/domEditingDom.ts +266 -0
  31. package/src/components/editor/domEditingElement.ts +329 -0
  32. package/src/components/editor/domEditingLayers.ts +460 -0
  33. package/src/components/editor/domEditingTypes.ts +125 -0
  34. package/src/components/editor/manualEditingAvailability.test.ts +2 -2
  35. package/src/components/editor/manualEditingAvailability.ts +1 -1
  36. package/src/components/editor/manualEdits.ts +84 -1049
  37. package/src/components/editor/manualEditsDom.ts +436 -0
  38. package/src/components/editor/manualEditsParsing.ts +280 -0
  39. package/src/components/editor/manualEditsSnapshot.ts +333 -0
  40. package/src/components/editor/manualEditsTypes.ts +141 -0
  41. package/src/components/editor/propertyPanelColor.tsx +371 -0
  42. package/src/components/editor/propertyPanelFill.tsx +421 -0
  43. package/src/components/editor/propertyPanelFont.tsx +455 -0
  44. package/src/components/editor/propertyPanelHelpers.ts +401 -0
  45. package/src/components/editor/propertyPanelPrimitives.tsx +357 -0
  46. package/src/components/editor/propertyPanelSections.tsx +453 -0
  47. package/src/components/editor/propertyPanelStyleSections.tsx +411 -0
  48. package/src/components/editor/studioMotion.ts +47 -434
  49. package/src/components/editor/studioMotionOps.ts +299 -0
  50. package/src/components/editor/studioMotionTypes.ts +168 -0
  51. package/src/components/editor/useDomEditOverlayGestures.ts +393 -0
  52. package/src/components/editor/useDomEditOverlayRects.ts +207 -0
  53. package/src/components/nle/NLELayout.tsx +68 -155
  54. package/src/components/nle/NLEPreview.tsx +3 -0
  55. package/src/components/nle/useCompositionStack.ts +126 -0
  56. package/src/components/renders/RenderQueue.tsx +102 -31
  57. package/src/components/renders/useRenderQueue.ts +8 -2
  58. package/src/components/sidebar/LeftSidebar.tsx +186 -186
  59. package/src/contexts/DomEditContext.tsx +137 -0
  60. package/src/contexts/FileManagerContext.tsx +110 -0
  61. package/src/contexts/PanelLayoutContext.tsx +68 -0
  62. package/src/contexts/StudioContext.tsx +135 -0
  63. package/src/hooks/useAppHotkeys.ts +326 -0
  64. package/src/hooks/useAskAgentModal.ts +162 -0
  65. package/src/hooks/useCaptionDetection.ts +132 -0
  66. package/src/hooks/useCompositionDimensions.ts +25 -0
  67. package/src/hooks/useConsoleErrorCapture.ts +60 -0
  68. package/src/hooks/useDomEditCommits.ts +437 -0
  69. package/src/hooks/useDomEditSession.ts +342 -0
  70. package/src/hooks/useDomEditTextCommits.ts +330 -0
  71. package/src/hooks/useDomSelection.ts +398 -0
  72. package/src/hooks/useFileManager.ts +431 -0
  73. package/src/hooks/useFrameCapture.ts +77 -0
  74. package/src/hooks/useLintModal.ts +35 -0
  75. package/src/hooks/useManifestPersistence.ts +492 -0
  76. package/src/hooks/usePanelLayout.ts +68 -0
  77. package/src/hooks/usePreviewInteraction.ts +153 -0
  78. package/src/hooks/useRenderClipContent.ts +124 -0
  79. package/src/hooks/useTimelineEditing.ts +472 -0
  80. package/src/hooks/useToast.ts +20 -0
  81. package/src/player/components/Player.tsx +33 -2
  82. package/src/player/components/Timeline.test.ts +0 -8
  83. package/src/player/components/Timeline.tsx +196 -1518
  84. package/src/player/components/TimelineCanvas.tsx +434 -0
  85. package/src/player/components/TimelineClip.tsx +9 -244
  86. package/src/player/components/TimelineEmptyState.tsx +102 -0
  87. package/src/player/components/TimelineRuler.tsx +90 -0
  88. package/src/player/components/timelineIcons.tsx +49 -0
  89. package/src/player/components/timelineLayout.ts +215 -0
  90. package/src/player/components/timelineUtils.ts +211 -0
  91. package/src/player/components/useTimelineClipDrag.ts +388 -0
  92. package/src/player/components/useTimelinePlayhead.ts +200 -0
  93. package/src/player/components/useTimelineRangeSelection.ts +135 -0
  94. package/src/player/hooks/usePlaybackKeyboard.ts +171 -0
  95. package/src/player/hooks/useTimelinePlayer.ts +105 -1371
  96. package/src/player/hooks/useTimelineSyncCallbacks.ts +288 -0
  97. package/src/player/lib/playbackAdapter.ts +145 -0
  98. package/src/player/lib/playbackShortcuts.ts +68 -0
  99. package/src/player/lib/playbackTypes.ts +60 -0
  100. package/src/player/lib/timelineDOM.ts +373 -0
  101. package/src/player/lib/timelineElementHelpers.ts +303 -0
  102. package/src/player/lib/timelineIframeHelpers.ts +269 -0
  103. package/src/utils/domEditHelpers.ts +50 -0
  104. package/src/utils/studioFontHelpers.ts +83 -0
  105. package/src/utils/studioHelpers.ts +214 -0
  106. package/src/utils/studioPreviewHelpers.ts +185 -0
  107. package/src/utils/timelineDiscovery.ts +1 -1
  108. package/dist/assets/hyperframes-player-DjsVzYFP.js +0 -418
  109. package/dist/assets/index-14zH9lqh.css +0 -1
  110. package/dist/assets/index-DYCiFGWQ.js +0 -108
  111. package/src/player/components/TimelineClip.test.ts +0 -92
@@ -0,0 +1,357 @@
1
+ import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
2
+ import { adjustNumericToken, FIELD, LABEL, parseNumericToken } from "./propertyPanelHelpers";
3
+
4
+ export function CommitField({
5
+ value,
6
+ disabled,
7
+ liveCommit,
8
+ onCommit,
9
+ }: {
10
+ value: string;
11
+ disabled?: boolean;
12
+ liveCommit?: boolean;
13
+ onCommit: (nextValue: string) => void;
14
+ }) {
15
+ const [draft, setDraft] = useState(value);
16
+ const commitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
17
+ const valueRef = useRef(value);
18
+ const draftRef = useRef(draft);
19
+ const inputRef = useRef<HTMLInputElement>(null);
20
+
21
+ valueRef.current = value;
22
+ draftRef.current = draft;
23
+
24
+ useEffect(() => {
25
+ setDraft(value);
26
+ }, [value]);
27
+
28
+ useEffect(() => {
29
+ const el = inputRef.current;
30
+ if (!el) return;
31
+ const handler = (e: WheelEvent) => {
32
+ if (disabled) return;
33
+ const delta = e.deltaY === 0 ? e.deltaX : e.deltaY;
34
+ if (delta === 0) return;
35
+ const nextDraft = adjustNumericToken(draftRef.current, delta < 0 ? 1 : -1, e);
36
+ if (!nextDraft) return;
37
+ e.preventDefault();
38
+ e.stopPropagation();
39
+ setDraft(nextDraft);
40
+ scheduleCommitRef.current(nextDraft);
41
+ };
42
+ el.addEventListener("wheel", handler, { passive: false });
43
+ return () => el.removeEventListener("wheel", handler);
44
+ }, [disabled]);
45
+
46
+ useEffect(
47
+ () => () => {
48
+ if (commitTimerRef.current) clearTimeout(commitTimerRef.current);
49
+ },
50
+ [],
51
+ );
52
+
53
+ const commitDraft = (nextDraft: string) => {
54
+ if (commitTimerRef.current) clearTimeout(commitTimerRef.current);
55
+ if (nextDraft !== valueRef.current) onCommit(nextDraft);
56
+ };
57
+
58
+ const scheduleCommit = (nextDraft: string) => {
59
+ if (commitTimerRef.current) clearTimeout(commitTimerRef.current);
60
+ commitTimerRef.current = setTimeout(() => {
61
+ if (nextDraft !== valueRef.current) onCommit(nextDraft);
62
+ }, 120);
63
+ };
64
+ const scheduleCommitRef = useRef(scheduleCommit);
65
+ scheduleCommitRef.current = scheduleCommit;
66
+
67
+ return (
68
+ <input
69
+ ref={inputRef}
70
+ type="text"
71
+ value={draft}
72
+ disabled={disabled}
73
+ onChange={(e) => {
74
+ setDraft(e.target.value);
75
+ if (liveCommit) scheduleCommit(e.target.value);
76
+ }}
77
+ onBlur={() => commitDraft(draft)}
78
+ onKeyDown={(e) => {
79
+ if (e.key === "Enter") {
80
+ (e.target as HTMLInputElement).blur();
81
+ return;
82
+ }
83
+ if (e.key !== "ArrowUp" && e.key !== "ArrowDown") return;
84
+ const nextDraft = adjustNumericToken(draft, e.key === "ArrowUp" ? 1 : -1, e);
85
+ if (!nextDraft) return;
86
+ e.preventDefault();
87
+ setDraft(nextDraft);
88
+ scheduleCommit(nextDraft);
89
+ }}
90
+ title={parseNumericToken(value) ? "Scroll or use Arrow keys to adjust" : undefined}
91
+ className="min-w-0 w-full bg-transparent text-[11px] font-medium text-neutral-100 outline-none disabled:cursor-not-allowed disabled:text-neutral-600"
92
+ />
93
+ );
94
+ }
95
+
96
+ /* ------------------------------------------------------------------ */
97
+ /* MetricField */
98
+ /* ------------------------------------------------------------------ */
99
+
100
+ export function MetricField({
101
+ label,
102
+ value,
103
+ disabled,
104
+ liveCommit,
105
+ scrub,
106
+ onCommit,
107
+ }: {
108
+ label: string;
109
+ value: string;
110
+ disabled?: boolean;
111
+ liveCommit?: boolean;
112
+ scrub?: boolean;
113
+ onCommit: (nextValue: string) => void;
114
+ }) {
115
+ const scrubRef = useRef<{ startX: number; startValue: number; pointerId: number } | null>(null);
116
+
117
+ const handleScrubPointerDown = useCallback(
118
+ (e: React.PointerEvent<HTMLSpanElement>) => {
119
+ if (disabled || !scrub) return;
120
+ const parsed = parseFloat(value);
121
+ if (!Number.isFinite(parsed)) return;
122
+ (e.target as HTMLElement).setPointerCapture(e.pointerId);
123
+ scrubRef.current = { startX: e.clientX, startValue: parsed, pointerId: e.pointerId };
124
+ },
125
+ [disabled, scrub, value],
126
+ );
127
+
128
+ const handleScrubPointerMove = useCallback(
129
+ (e: React.PointerEvent<HTMLSpanElement>) => {
130
+ const state = scrubRef.current;
131
+ if (!state) return;
132
+ const delta = e.clientX - state.startX;
133
+ onCommit(String(Math.round(state.startValue + delta)));
134
+ },
135
+ [onCommit],
136
+ );
137
+
138
+ const handleScrubPointerUp = useCallback(() => {
139
+ scrubRef.current = null;
140
+ }, []);
141
+
142
+ const scrubProps =
143
+ scrub && !disabled
144
+ ? ({
145
+ className:
146
+ "flex-shrink-0 text-[11px] font-medium text-neutral-500 cursor-ew-resize select-none",
147
+ onPointerDown: handleScrubPointerDown,
148
+ onPointerMove: handleScrubPointerMove,
149
+ onPointerUp: handleScrubPointerUp,
150
+ } as const)
151
+ : ({ className: "flex-shrink-0 text-[11px] font-medium text-neutral-500" } as const);
152
+
153
+ return (
154
+ <div className={FIELD}>
155
+ <div className="flex min-w-0 items-center gap-3">
156
+ <span {...scrubProps}>{label}</span>
157
+ <CommitField
158
+ value={value}
159
+ disabled={disabled}
160
+ liveCommit={liveCommit}
161
+ onCommit={onCommit}
162
+ />
163
+ </div>
164
+ </div>
165
+ );
166
+ }
167
+
168
+ /* ------------------------------------------------------------------ */
169
+ /* Simple field components */
170
+ /* ------------------------------------------------------------------ */
171
+
172
+ export function DetailField({
173
+ label,
174
+ value,
175
+ disabled,
176
+ onCommit,
177
+ }: {
178
+ label: string;
179
+ value: string;
180
+ disabled?: boolean;
181
+ onCommit: (nextValue: string) => void;
182
+ }) {
183
+ return (
184
+ <label className="grid min-w-0 gap-1.5">
185
+ <span className={LABEL}>{label}</span>
186
+ <div className={FIELD}>
187
+ <CommitField value={value} disabled={disabled} onCommit={onCommit} />
188
+ </div>
189
+ </label>
190
+ );
191
+ }
192
+
193
+ export function SliderControl({
194
+ value,
195
+ min,
196
+ max,
197
+ step,
198
+ displayValue,
199
+ formatDisplayValue,
200
+ disabled,
201
+ onCommit,
202
+ }: {
203
+ value: number;
204
+ min: number;
205
+ max: number;
206
+ step: number;
207
+ displayValue: string;
208
+ formatDisplayValue?: (nextValue: number) => string;
209
+ disabled?: boolean;
210
+ onCommit: (nextValue: number) => void;
211
+ }) {
212
+ const [draft, setDraft] = useState(value);
213
+ const commitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
214
+ const valueRef = useRef(value);
215
+ valueRef.current = value;
216
+
217
+ useEffect(() => {
218
+ setDraft(value);
219
+ }, [value]);
220
+ useEffect(
221
+ () => () => {
222
+ if (commitTimerRef.current) clearTimeout(commitTimerRef.current);
223
+ },
224
+ [],
225
+ );
226
+
227
+ const commitDraft = (nextDraft: number) => {
228
+ if (commitTimerRef.current) clearTimeout(commitTimerRef.current);
229
+ if (nextDraft !== valueRef.current) onCommit(nextDraft);
230
+ };
231
+ const scheduleCommit = (nextDraft: number) => {
232
+ if (commitTimerRef.current) clearTimeout(commitTimerRef.current);
233
+ commitTimerRef.current = setTimeout(() => {
234
+ if (nextDraft !== valueRef.current) onCommit(nextDraft);
235
+ }, 40);
236
+ };
237
+
238
+ return (
239
+ <div className="grid min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-2">
240
+ <input
241
+ type="range"
242
+ min={min}
243
+ max={max}
244
+ step={step}
245
+ value={draft}
246
+ disabled={disabled}
247
+ onChange={(e) => {
248
+ const n = Number(e.target.value);
249
+ setDraft(n);
250
+ scheduleCommit(n);
251
+ }}
252
+ onMouseUp={() => commitDraft(draft)}
253
+ onTouchEnd={() => commitDraft(draft)}
254
+ onBlur={() => commitDraft(draft)}
255
+ className="h-2 min-w-0 w-full cursor-pointer appearance-none rounded-full bg-neutral-800 accent-[#3ce6ac] disabled:cursor-not-allowed"
256
+ />
257
+ <div className="min-w-[52px] rounded-xl border border-neutral-800 bg-neutral-900 px-2 py-2 text-right text-[11px] font-medium text-neutral-100 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]">
258
+ {formatDisplayValue?.(draft) ?? displayValue}
259
+ </div>
260
+ </div>
261
+ );
262
+ }
263
+
264
+ export function SegmentedControl({
265
+ options,
266
+ value,
267
+ disabled,
268
+ onChange,
269
+ }: {
270
+ options: Array<{ label: string; value: string }>;
271
+ value: string;
272
+ disabled?: boolean;
273
+ onChange: (nextValue: string) => void;
274
+ }) {
275
+ return (
276
+ <div
277
+ className="grid min-w-0 gap-1 rounded-xl bg-neutral-900 p-1 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]"
278
+ style={{ gridTemplateColumns: `repeat(${options.length}, minmax(0, 1fr))` }}
279
+ >
280
+ {options.map((option) => (
281
+ <button
282
+ key={option.value}
283
+ type="button"
284
+ disabled={disabled}
285
+ onClick={() => onChange(option.value)}
286
+ className={`min-w-0 truncate rounded-lg px-2 py-1.5 text-[11px] font-medium transition-colors disabled:cursor-not-allowed ${
287
+ option.value === value
288
+ ? "bg-neutral-800 text-white shadow-[0_1px_3px_rgba(0,0,0,0.28)]"
289
+ : "text-neutral-500 hover:text-neutral-200"
290
+ }`}
291
+ >
292
+ {option.label}
293
+ </button>
294
+ ))}
295
+ </div>
296
+ );
297
+ }
298
+
299
+ export function SelectField({
300
+ label,
301
+ value,
302
+ disabled,
303
+ options,
304
+ onChange,
305
+ }: {
306
+ label: string;
307
+ value: string;
308
+ disabled?: boolean;
309
+ options: string[];
310
+ onChange: (nextValue: string) => void;
311
+ }) {
312
+ const renderedOptions = value && !options.includes(value) ? [value, ...options] : options;
313
+ return (
314
+ <label className={`${FIELD} flex items-center gap-3`}>
315
+ <span className="flex-shrink-0 text-[11px] font-medium text-neutral-500">{label}</span>
316
+ <select
317
+ value={value}
318
+ disabled={disabled}
319
+ onChange={(e) => onChange(e.target.value)}
320
+ className="min-w-0 w-full appearance-none bg-transparent text-[11px] font-medium text-neutral-100 outline-none disabled:cursor-not-allowed disabled:text-neutral-600"
321
+ >
322
+ {renderedOptions.map((option) => (
323
+ <option key={option} value={option}>
324
+ {option}
325
+ </option>
326
+ ))}
327
+ </select>
328
+ </label>
329
+ );
330
+ }
331
+
332
+ export function Section({
333
+ title,
334
+ icon,
335
+ children,
336
+ accessory,
337
+ }: {
338
+ title: string;
339
+ icon: ReactNode;
340
+ children: ReactNode;
341
+ accessory?: ReactNode;
342
+ }) {
343
+ return (
344
+ <section className="min-w-0 border-t border-neutral-800/80 px-4 py-4">
345
+ <div className="mb-3 flex min-w-0 flex-wrap items-center justify-between gap-2">
346
+ <div className="flex min-w-0 items-center gap-2.5">
347
+ <span className="flex-shrink-0 text-neutral-500">{icon}</span>
348
+ <h3 className="text-[11px] font-semibold uppercase tracking-[0.12em] text-neutral-300">
349
+ {title}
350
+ </h3>
351
+ </div>
352
+ {accessory}
353
+ </div>
354
+ {children}
355
+ </section>
356
+ );
357
+ }