@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.
- package/dist/assets/{hyperframes-player-DjsVzYFP.js → hyperframes-player-DOFETgjy.js} +1 -1
- package/dist/assets/index-D1JDq7Gg.css +1 -0
- package/dist/assets/index-DUqUmaoH.js +117 -0
- package/dist/favicon.svg +14 -0
- package/dist/index.html +3 -2
- package/package.json +9 -9
- package/src/App.tsx +428 -4299
- package/src/components/AskAgentModal.tsx +120 -0
- package/src/components/StudioHeader.tsx +133 -0
- package/src/components/StudioLeftSidebar.tsx +125 -0
- package/src/components/StudioPreviewArea.tsx +163 -0
- package/src/components/StudioRightPanel.tsx +198 -0
- package/src/components/TimelineToolbar.tsx +89 -0
- package/src/components/editor/DomEditOverlay.tsx +15 -1
- package/src/components/editor/PropertyPanel.test.ts +0 -49
- package/src/components/editor/PropertyPanel.tsx +132 -2763
- package/src/components/editor/domEditing.ts +38 -5
- package/src/components/editor/manualEditingAvailability.test.ts +2 -2
- package/src/components/editor/manualEditingAvailability.ts +1 -1
- package/src/components/editor/manualEdits.ts +32 -0
- package/src/components/editor/propertyPanelColor.tsx +371 -0
- package/src/components/editor/propertyPanelFill.tsx +421 -0
- package/src/components/editor/propertyPanelFont.tsx +455 -0
- package/src/components/editor/propertyPanelHelpers.ts +401 -0
- package/src/components/editor/propertyPanelPrimitives.tsx +357 -0
- package/src/components/editor/propertyPanelSections.tsx +453 -0
- package/src/components/editor/propertyPanelStyleSections.tsx +411 -0
- package/src/components/nle/NLELayout.tsx +8 -11
- package/src/components/nle/NLEPreview.tsx +3 -0
- package/src/components/renders/RenderQueue.tsx +102 -31
- package/src/components/renders/useRenderQueue.ts +8 -2
- package/src/components/sidebar/LeftSidebar.tsx +186 -186
- package/src/contexts/DomEditContext.tsx +137 -0
- package/src/contexts/FileManagerContext.tsx +110 -0
- package/src/contexts/PanelLayoutContext.tsx +68 -0
- package/src/contexts/StudioContext.tsx +135 -0
- package/src/hooks/useAppHotkeys.ts +326 -0
- package/src/hooks/useAskAgentModal.ts +162 -0
- package/src/hooks/useCaptionDetection.ts +132 -0
- package/src/hooks/useCompositionDimensions.ts +25 -0
- package/src/hooks/useConsoleErrorCapture.ts +60 -0
- package/src/hooks/useDomEditCommits.ts +437 -0
- package/src/hooks/useDomEditSession.ts +342 -0
- package/src/hooks/useDomEditTextCommits.ts +330 -0
- package/src/hooks/useDomSelection.ts +398 -0
- package/src/hooks/useFileManager.ts +431 -0
- package/src/hooks/useFrameCapture.ts +77 -0
- package/src/hooks/useLintModal.ts +35 -0
- package/src/hooks/useManifestPersistence.ts +492 -0
- package/src/hooks/usePanelLayout.ts +68 -0
- package/src/hooks/usePreviewInteraction.ts +153 -0
- package/src/hooks/useRenderClipContent.ts +124 -0
- package/src/hooks/useTimelineEditing.ts +472 -0
- package/src/player/components/Player.tsx +33 -2
- package/src/player/components/Timeline.test.ts +0 -8
- package/src/player/components/Timeline.tsx +10 -103
- package/src/player/components/TimelineClip.tsx +9 -244
- package/src/player/hooks/useTimelinePlayer.ts +140 -103
- package/src/utils/domEditHelpers.ts +50 -0
- package/src/utils/studioFontHelpers.ts +83 -0
- package/src/utils/studioHelpers.ts +214 -0
- package/src/utils/studioPreviewHelpers.ts +185 -0
- package/src/utils/timelineDiscovery.ts +1 -1
- package/dist/assets/index-14zH9lqh.css +0 -1
- package/dist/assets/index-DYCiFGWQ.js +0 -108
- 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
|
+
}
|