@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,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
+ }