@hyperframes/studio 0.4.15 → 0.4.17
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/index-JZr8f8y8.js +115 -0
- package/dist/assets/index-kT65pCwW.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +47 -38
- package/src/components/nle/NLELayout.tsx +4 -0
- package/src/components/nle/TimelineEditorNotice.tsx +156 -0
- package/src/components/sidebar/CompositionsTab.test.ts +37 -0
- package/src/components/sidebar/CompositionsTab.tsx +45 -2
- package/src/player/components/Timeline.test.ts +11 -0
- package/src/player/components/Timeline.tsx +105 -17
- package/src/player/components/TimelineClip.tsx +1 -1
- package/src/player/components/timelineEditing.test.ts +149 -0
- package/src/player/components/timelineEditing.ts +45 -6
- package/src/player/hooks/useTimelinePlayer.ts +5 -1
- package/dist/assets/index-CVm-zeM9.css +0 -1
- package/dist/assets/index-RzXlAX2g.js +0 -93
|
@@ -10,10 +10,14 @@ import { formatTime } from "../lib/time";
|
|
|
10
10
|
import { TimelineClip } from "./TimelineClip";
|
|
11
11
|
import { EditPopover } from "./EditModal";
|
|
12
12
|
import {
|
|
13
|
+
buildClipRangeSelection,
|
|
13
14
|
getTimelineEditCapabilities,
|
|
15
|
+
resolveBlockedTimelineEditIntent,
|
|
14
16
|
resolveTimelineAutoScroll,
|
|
15
17
|
resolveTimelineMove,
|
|
16
18
|
resolveTimelineResize,
|
|
19
|
+
type BlockedTimelineEditIntent,
|
|
20
|
+
type TimelineRangeSelection,
|
|
17
21
|
} from "./timelineEditing";
|
|
18
22
|
import {
|
|
19
23
|
defaultTimelineTheme,
|
|
@@ -29,6 +33,8 @@ const GUTTER = 32;
|
|
|
29
33
|
const TRACK_H = 72;
|
|
30
34
|
const RULER_H = 24;
|
|
31
35
|
const CLIP_Y = 3; // vertical inset inside track
|
|
36
|
+
const CLIP_HANDLE_W = 18;
|
|
37
|
+
const TIMELINE_SCROLL_BUFFER = 24;
|
|
32
38
|
|
|
33
39
|
interface TrackVisualStyle extends TimelineTrackStyle {
|
|
34
40
|
icon: ReactNode;
|
|
@@ -130,6 +136,10 @@ export function getTimelinePlayheadLeft(time: number, pixelsPerSecond: number):
|
|
|
130
136
|
return GUTTER + Math.max(0, time) * Math.max(0, pixelsPerSecond);
|
|
131
137
|
}
|
|
132
138
|
|
|
139
|
+
export function getTimelineCanvasHeight(trackCount: number): number {
|
|
140
|
+
return RULER_H + Math.max(0, trackCount) * TRACK_H + TIMELINE_SCROLL_BUFFER;
|
|
141
|
+
}
|
|
142
|
+
|
|
133
143
|
/* ── Component ──────────────────────────────────────────────────── */
|
|
134
144
|
interface TimelineProps {
|
|
135
145
|
/** Called when user seeks via ruler/track click or playhead drag */
|
|
@@ -157,6 +167,10 @@ interface TimelineProps {
|
|
|
157
167
|
"start" | "duration" | "playbackStart"
|
|
158
168
|
>,
|
|
159
169
|
) => Promise<void> | void;
|
|
170
|
+
onBlockedEditAttempt?: (
|
|
171
|
+
element: import("../store/playerStore").TimelineElement,
|
|
172
|
+
intent: BlockedTimelineEditIntent,
|
|
173
|
+
) => void;
|
|
160
174
|
theme?: Partial<TimelineTheme>;
|
|
161
175
|
}
|
|
162
176
|
|
|
@@ -185,6 +199,14 @@ interface ResizingClipState {
|
|
|
185
199
|
started: boolean;
|
|
186
200
|
}
|
|
187
201
|
|
|
202
|
+
interface BlockedClipState {
|
|
203
|
+
element: TimelineElement;
|
|
204
|
+
intent: BlockedTimelineEditIntent;
|
|
205
|
+
originClientX: number;
|
|
206
|
+
originClientY: number;
|
|
207
|
+
started: boolean;
|
|
208
|
+
}
|
|
209
|
+
|
|
188
210
|
export const Timeline = memo(function Timeline({
|
|
189
211
|
onSeek,
|
|
190
212
|
onDrillDown,
|
|
@@ -193,6 +215,7 @@ export const Timeline = memo(function Timeline({
|
|
|
193
215
|
onFileDrop,
|
|
194
216
|
onMoveElement,
|
|
195
217
|
onResizeElement,
|
|
218
|
+
onBlockedEditAttempt,
|
|
196
219
|
theme: themeOverrides,
|
|
197
220
|
}: TimelineProps = {}) {
|
|
198
221
|
const theme = useMemo(() => ({ ...defaultTimelineTheme, ...themeOverrides }), [themeOverrides]);
|
|
@@ -210,6 +233,11 @@ export const Timeline = memo(function Timeline({
|
|
|
210
233
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
211
234
|
const [hoveredClip, setHoveredClip] = useState<string | null>(null);
|
|
212
235
|
const isDragging = useRef(false);
|
|
236
|
+
const shiftClickClipRef = useRef<{
|
|
237
|
+
element: TimelineElement;
|
|
238
|
+
anchorX: number;
|
|
239
|
+
anchorY: number;
|
|
240
|
+
} | null>(null);
|
|
213
241
|
// Range selection (Shift+drag)
|
|
214
242
|
const [shiftHeld, setShiftHeld] = useState(false);
|
|
215
243
|
useMountEffect(() => {
|
|
@@ -227,18 +255,14 @@ export const Timeline = memo(function Timeline({
|
|
|
227
255
|
});
|
|
228
256
|
const isRangeSelecting = useRef(false);
|
|
229
257
|
const rangeAnchorTime = useRef(0);
|
|
230
|
-
const [rangeSelection, setRangeSelection] = useState<
|
|
231
|
-
start: number;
|
|
232
|
-
end: number;
|
|
233
|
-
anchorX: number;
|
|
234
|
-
anchorY: number;
|
|
235
|
-
} | null>(null);
|
|
258
|
+
const [rangeSelection, setRangeSelection] = useState<TimelineRangeSelection | null>(null);
|
|
236
259
|
const [draggedClip, setDraggedClip] = useState<DraggedClipState | null>(null);
|
|
237
260
|
const draggedClipRef = useRef<DraggedClipState | null>(null);
|
|
238
261
|
draggedClipRef.current = draggedClip;
|
|
239
262
|
const [resizingClip, setResizingClip] = useState<ResizingClipState | null>(null);
|
|
240
263
|
const resizingClipRef = useRef<ResizingClipState | null>(null);
|
|
241
264
|
resizingClipRef.current = resizingClip;
|
|
265
|
+
const blockedClipRef = useRef<BlockedClipState | null>(null);
|
|
242
266
|
const onMoveElementRef = useRef(onMoveElement);
|
|
243
267
|
onMoveElementRef.current = onMoveElement;
|
|
244
268
|
const onResizeElementRef = useRef(onResizeElement);
|
|
@@ -546,6 +570,7 @@ export const Timeline = memo(function Timeline({
|
|
|
546
570
|
const handleWindowPointerMove = (e: PointerEvent) => {
|
|
547
571
|
const drag = draggedClipRef.current;
|
|
548
572
|
const resize = resizingClipRef.current;
|
|
573
|
+
const blocked = blockedClipRef.current;
|
|
549
574
|
if (resize) {
|
|
550
575
|
const distance = Math.abs(e.clientX - resize.originClientX);
|
|
551
576
|
if (!resize.started && distance < 2) return;
|
|
@@ -561,6 +586,8 @@ export const Timeline = memo(function Timeline({
|
|
|
561
586
|
Math.max(resize.element.playbackRate ?? 1, 0.1),
|
|
562
587
|
)
|
|
563
588
|
: Number.POSITIVE_INFINITY;
|
|
589
|
+
const normalizedTag = resize.element.tag.toLowerCase();
|
|
590
|
+
const canSeedPlaybackStart = normalizedTag === "audio" || normalizedTag === "video";
|
|
564
591
|
const nextResize = resolveTimelineResize(
|
|
565
592
|
{
|
|
566
593
|
start: resize.element.start,
|
|
@@ -569,7 +596,10 @@ export const Timeline = memo(function Timeline({
|
|
|
569
596
|
pixelsPerSecond: ppsRef.current,
|
|
570
597
|
minStart: 0,
|
|
571
598
|
maxEnd: Math.min(durationRef.current, resize.element.start + sourceRemaining),
|
|
572
|
-
playbackStart:
|
|
599
|
+
playbackStart:
|
|
600
|
+
resize.edge === "start" && canSeedPlaybackStart
|
|
601
|
+
? (resize.element.playbackStart ?? 0)
|
|
602
|
+
: resize.element.playbackStart,
|
|
573
603
|
playbackRate: resize.element.playbackRate,
|
|
574
604
|
},
|
|
575
605
|
resize.edge,
|
|
@@ -589,6 +619,23 @@ export const Timeline = memo(function Timeline({
|
|
|
589
619
|
);
|
|
590
620
|
return;
|
|
591
621
|
}
|
|
622
|
+
if (blocked) {
|
|
623
|
+
const distance = Math.hypot(
|
|
624
|
+
e.clientX - blocked.originClientX,
|
|
625
|
+
e.clientY - blocked.originClientY,
|
|
626
|
+
);
|
|
627
|
+
const threshold = blocked.intent === "move" ? 4 : 2;
|
|
628
|
+
if (!blocked.started && distance < threshold) return;
|
|
629
|
+
if (!blocked.started) {
|
|
630
|
+
blocked.started = true;
|
|
631
|
+
blockedClipRef.current = blocked;
|
|
632
|
+
suppressClickRef.current = true;
|
|
633
|
+
setShowPopover(false);
|
|
634
|
+
setRangeSelection(null);
|
|
635
|
+
onBlockedEditAttempt?.(blocked.element, blocked.intent);
|
|
636
|
+
}
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
592
639
|
if (!drag) return;
|
|
593
640
|
|
|
594
641
|
const distance = Math.hypot(e.clientX - drag.originClientX, e.clientY - drag.originClientY);
|
|
@@ -644,6 +691,14 @@ export const Timeline = memo(function Timeline({
|
|
|
644
691
|
return;
|
|
645
692
|
}
|
|
646
693
|
|
|
694
|
+
const blocked = blockedClipRef.current;
|
|
695
|
+
if (blocked) {
|
|
696
|
+
blockedClipRef.current = null;
|
|
697
|
+
if (!blocked.started) return;
|
|
698
|
+
clearSuppressedClick();
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
|
|
647
702
|
const drag = draggedClipRef.current;
|
|
648
703
|
if (!drag) return;
|
|
649
704
|
draggedClipRef.current = null;
|
|
@@ -707,6 +762,7 @@ export const Timeline = memo(function Timeline({
|
|
|
707
762
|
return;
|
|
708
763
|
}
|
|
709
764
|
|
|
765
|
+
shiftClickClipRef.current = null;
|
|
710
766
|
// Normal click on a clip — let the clip handle it
|
|
711
767
|
if ((e.target as HTMLElement).closest("[data-clip]")) return;
|
|
712
768
|
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
|
@@ -740,8 +796,14 @@ export const Timeline = memo(function Timeline({
|
|
|
740
796
|
const handlePointerUp = useCallback(() => {
|
|
741
797
|
if (isRangeSelecting.current) {
|
|
742
798
|
isRangeSelecting.current = false;
|
|
743
|
-
|
|
799
|
+
const pendingShiftClick = shiftClickClipRef.current;
|
|
800
|
+
shiftClickClipRef.current = null;
|
|
744
801
|
setRangeSelection((prev) => {
|
|
802
|
+
if (prev && pendingShiftClick && Math.abs(prev.end - prev.start) <= 0.2) {
|
|
803
|
+
setShowPopover(true);
|
|
804
|
+
return buildClipRangeSelection(pendingShiftClick.element, pendingShiftClick);
|
|
805
|
+
}
|
|
806
|
+
// Show popover if range is meaningful (> 0.2s)
|
|
745
807
|
if (prev && Math.abs(prev.end - prev.start) > 0.2) {
|
|
746
808
|
setShowPopover(true);
|
|
747
809
|
return prev;
|
|
@@ -869,7 +931,7 @@ export const Timeline = memo(function Timeline({
|
|
|
869
931
|
);
|
|
870
932
|
}
|
|
871
933
|
|
|
872
|
-
const totalH =
|
|
934
|
+
const totalH = getTimelineCanvasHeight(displayTrackOrder.length);
|
|
873
935
|
const draggedElement = draggedClip?.element ?? null;
|
|
874
936
|
const activeDraggedElement =
|
|
875
937
|
draggedClip?.started === true && draggedElement
|
|
@@ -990,7 +1052,7 @@ export const Timeline = memo(function Timeline({
|
|
|
990
1052
|
{shiftHeld && !rangeSelection && (
|
|
991
1053
|
<div className="absolute inset-0 flex items-center justify-center pointer-events-none z-10">
|
|
992
1054
|
<span className="text-[9px] font-medium" style={{ color: theme.textSecondary }}>
|
|
993
|
-
Drag to
|
|
1055
|
+
Drag or click a clip to edit range
|
|
994
1056
|
</span>
|
|
995
1057
|
</div>
|
|
996
1058
|
)}
|
|
@@ -1108,6 +1170,7 @@ export const Timeline = memo(function Timeline({
|
|
|
1108
1170
|
if (edge === "start" && !capabilities.canTrimStart) return;
|
|
1109
1171
|
if (edge === "end" && !capabilities.canTrimEnd) return;
|
|
1110
1172
|
e.stopPropagation();
|
|
1173
|
+
blockedClipRef.current = null;
|
|
1111
1174
|
setShowPopover(false);
|
|
1112
1175
|
setRangeSelection(null);
|
|
1113
1176
|
setResizingClip({
|
|
@@ -1121,16 +1184,41 @@ export const Timeline = memo(function Timeline({
|
|
|
1121
1184
|
});
|
|
1122
1185
|
}}
|
|
1123
1186
|
onPointerDown={(e) => {
|
|
1187
|
+
if (e.button !== 0) return;
|
|
1188
|
+
if (e.shiftKey) {
|
|
1189
|
+
shiftClickClipRef.current = {
|
|
1190
|
+
element: el,
|
|
1191
|
+
anchorX: e.clientX,
|
|
1192
|
+
anchorY: e.clientY,
|
|
1193
|
+
};
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
const target = e.currentTarget as HTMLElement;
|
|
1197
|
+
const rect = target.getBoundingClientRect();
|
|
1198
|
+
const blockedIntent = resolveBlockedTimelineEditIntent({
|
|
1199
|
+
width: rect.width,
|
|
1200
|
+
offsetX: e.clientX - rect.left,
|
|
1201
|
+
handleWidth: CLIP_HANDLE_W,
|
|
1202
|
+
capabilities,
|
|
1203
|
+
});
|
|
1124
1204
|
if (
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1205
|
+
blockedIntent &&
|
|
1206
|
+
((blockedIntent === "move" && onMoveElement) ||
|
|
1207
|
+
(blockedIntent !== "move" && onResizeElement))
|
|
1208
|
+
) {
|
|
1209
|
+
blockedClipRef.current = {
|
|
1210
|
+
element: el,
|
|
1211
|
+
intent: blockedIntent,
|
|
1212
|
+
originClientX: e.clientX,
|
|
1213
|
+
originClientY: e.clientY,
|
|
1214
|
+
started: false,
|
|
1215
|
+
};
|
|
1130
1216
|
return;
|
|
1217
|
+
}
|
|
1218
|
+
if (!onMoveElement || !capabilities.canMove) return;
|
|
1219
|
+
blockedClipRef.current = null;
|
|
1131
1220
|
setShowPopover(false);
|
|
1132
1221
|
setRangeSelection(null);
|
|
1133
|
-
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
|
1134
1222
|
setDraggedClip({
|
|
1135
1223
|
element: el,
|
|
1136
1224
|
originClientX: e.clientX,
|
|
@@ -1270,7 +1358,7 @@ export const Timeline = memo(function Timeline({
|
|
|
1270
1358
|
Shift
|
|
1271
1359
|
</kbd>
|
|
1272
1360
|
<span className="text-[9px]" style={{ color: theme.textSecondary }}>
|
|
1273
|
-
+ drag to edit range
|
|
1361
|
+
+ drag/click to edit range
|
|
1274
1362
|
</span>
|
|
1275
1363
|
</div>
|
|
1276
1364
|
</div>
|
|
@@ -147,7 +147,7 @@ export const TimelineClip = memo(function TimelineClip({
|
|
|
147
147
|
top: 0,
|
|
148
148
|
bottom: 0,
|
|
149
149
|
width: 18,
|
|
150
|
-
opacity: showHandles ? 1 : 0,
|
|
150
|
+
opacity: showHandles && capabilities.canTrimEnd ? 1 : 0,
|
|
151
151
|
pointerEvents: onResizeStart && capabilities.canTrimEnd ? "auto" : "none",
|
|
152
152
|
zIndex: 4,
|
|
153
153
|
transition: "opacity 120ms ease-out",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import {
|
|
3
|
+
buildClipRangeSelection,
|
|
3
4
|
buildPromptCopyText,
|
|
4
5
|
buildTimelineElementAgentPrompt,
|
|
5
6
|
buildTimelineAgentPrompt,
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
canOffsetTrimClipStart,
|
|
8
9
|
getTimelineEditCapabilities,
|
|
9
10
|
hasPatchableTimelineTarget,
|
|
11
|
+
resolveBlockedTimelineEditIntent,
|
|
10
12
|
resolveTimelineAutoScroll,
|
|
11
13
|
resolveTimelineMove,
|
|
12
14
|
resolveTimelineResize,
|
|
@@ -199,6 +201,14 @@ describe("canOffsetTrimClipStart", () => {
|
|
|
199
201
|
).toBe(true);
|
|
200
202
|
});
|
|
201
203
|
|
|
204
|
+
it("allows front trim for plain audio clips even before media-start exists", () => {
|
|
205
|
+
expect(
|
|
206
|
+
canOffsetTrimClipStart({
|
|
207
|
+
tag: "audio",
|
|
208
|
+
}),
|
|
209
|
+
).toBe(true);
|
|
210
|
+
});
|
|
211
|
+
|
|
202
212
|
it("blocks front trim for generic motion clips", () => {
|
|
203
213
|
expect(
|
|
204
214
|
canOffsetTrimClipStart({
|
|
@@ -223,6 +233,21 @@ describe("hasPatchableTimelineTarget", () => {
|
|
|
223
233
|
});
|
|
224
234
|
|
|
225
235
|
describe("getTimelineEditCapabilities", () => {
|
|
236
|
+
it("does not disable editable audio just because it spans multiple scenes", () => {
|
|
237
|
+
expect(
|
|
238
|
+
getTimelineEditCapabilities({
|
|
239
|
+
tag: "audio",
|
|
240
|
+
duration: 8,
|
|
241
|
+
selector: "#voiceover",
|
|
242
|
+
sourceDuration: 8,
|
|
243
|
+
}),
|
|
244
|
+
).toEqual({
|
|
245
|
+
canMove: true,
|
|
246
|
+
canTrimStart: true,
|
|
247
|
+
canTrimEnd: true,
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
226
251
|
it("disables move and trims for generic motion clips even when patchable", () => {
|
|
227
252
|
expect(
|
|
228
253
|
getTimelineEditCapabilities({
|
|
@@ -299,6 +324,111 @@ describe("getTimelineEditCapabilities", () => {
|
|
|
299
324
|
});
|
|
300
325
|
});
|
|
301
326
|
|
|
327
|
+
describe("resolveBlockedTimelineEditIntent", () => {
|
|
328
|
+
it("returns move when the clip body is blocked", () => {
|
|
329
|
+
expect(
|
|
330
|
+
resolveBlockedTimelineEditIntent({
|
|
331
|
+
width: 160,
|
|
332
|
+
offsetX: 80,
|
|
333
|
+
handleWidth: 18,
|
|
334
|
+
capabilities: {
|
|
335
|
+
canMove: false,
|
|
336
|
+
canTrimStart: false,
|
|
337
|
+
canTrimEnd: false,
|
|
338
|
+
},
|
|
339
|
+
}),
|
|
340
|
+
).toBe("move");
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("returns resize-start when the left edge is blocked", () => {
|
|
344
|
+
expect(
|
|
345
|
+
resolveBlockedTimelineEditIntent({
|
|
346
|
+
width: 160,
|
|
347
|
+
offsetX: 8,
|
|
348
|
+
handleWidth: 18,
|
|
349
|
+
capabilities: {
|
|
350
|
+
canMove: false,
|
|
351
|
+
canTrimStart: false,
|
|
352
|
+
canTrimEnd: true,
|
|
353
|
+
},
|
|
354
|
+
}),
|
|
355
|
+
).toBe("resize-start");
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("returns resize-end when the right edge is blocked", () => {
|
|
359
|
+
expect(
|
|
360
|
+
resolveBlockedTimelineEditIntent({
|
|
361
|
+
width: 160,
|
|
362
|
+
offsetX: 154,
|
|
363
|
+
handleWidth: 18,
|
|
364
|
+
capabilities: {
|
|
365
|
+
canMove: false,
|
|
366
|
+
canTrimStart: true,
|
|
367
|
+
canTrimEnd: false,
|
|
368
|
+
},
|
|
369
|
+
}),
|
|
370
|
+
).toBe("resize-end");
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("does not block the left edge when the clip can still be moved", () => {
|
|
374
|
+
expect(
|
|
375
|
+
resolveBlockedTimelineEditIntent({
|
|
376
|
+
width: 160,
|
|
377
|
+
offsetX: 8,
|
|
378
|
+
handleWidth: 18,
|
|
379
|
+
capabilities: {
|
|
380
|
+
canMove: true,
|
|
381
|
+
canTrimStart: false,
|
|
382
|
+
canTrimEnd: true,
|
|
383
|
+
},
|
|
384
|
+
}),
|
|
385
|
+
).toBe(null);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("does not swallow the full surface of a narrow movable clip", () => {
|
|
389
|
+
expect(
|
|
390
|
+
resolveBlockedTimelineEditIntent({
|
|
391
|
+
width: 12,
|
|
392
|
+
offsetX: 6,
|
|
393
|
+
handleWidth: 18,
|
|
394
|
+
capabilities: {
|
|
395
|
+
canMove: true,
|
|
396
|
+
canTrimStart: false,
|
|
397
|
+
canTrimEnd: false,
|
|
398
|
+
},
|
|
399
|
+
}),
|
|
400
|
+
).toBe(null);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("returns null when the relevant edit is supported", () => {
|
|
404
|
+
expect(
|
|
405
|
+
resolveBlockedTimelineEditIntent({
|
|
406
|
+
width: 160,
|
|
407
|
+
offsetX: 8,
|
|
408
|
+
handleWidth: 18,
|
|
409
|
+
capabilities: {
|
|
410
|
+
canMove: true,
|
|
411
|
+
canTrimStart: true,
|
|
412
|
+
canTrimEnd: true,
|
|
413
|
+
},
|
|
414
|
+
}),
|
|
415
|
+
).toBe(null);
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
describe("buildClipRangeSelection", () => {
|
|
420
|
+
it("anchors the full clip range at the click position", () => {
|
|
421
|
+
expect(
|
|
422
|
+
buildClipRangeSelection({ start: 1.25, duration: 3.5 }, { anchorX: 320, anchorY: 180 }),
|
|
423
|
+
).toEqual({
|
|
424
|
+
start: 1.25,
|
|
425
|
+
end: 4.75,
|
|
426
|
+
anchorX: 320,
|
|
427
|
+
anchorY: 180,
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
302
432
|
describe("resolveTimelineAutoScroll", () => {
|
|
303
433
|
it("does not scroll when the pointer stays away from the edges", () => {
|
|
304
434
|
expect(
|
|
@@ -420,6 +550,25 @@ describe("resolveTimelineResize", () => {
|
|
|
420
550
|
).toEqual({ start: 1.5, duration: 2.5, playbackStart: 1 });
|
|
421
551
|
});
|
|
422
552
|
|
|
553
|
+
it("can seed front trim from an implicit zero playback start", () => {
|
|
554
|
+
expect(
|
|
555
|
+
resolveTimelineResize(
|
|
556
|
+
{
|
|
557
|
+
start: 0,
|
|
558
|
+
duration: 8,
|
|
559
|
+
originClientX: 100,
|
|
560
|
+
pixelsPerSecond: 100,
|
|
561
|
+
minStart: 0,
|
|
562
|
+
maxEnd: 8,
|
|
563
|
+
playbackStart: 0,
|
|
564
|
+
playbackRate: 1,
|
|
565
|
+
},
|
|
566
|
+
"start",
|
|
567
|
+
200,
|
|
568
|
+
),
|
|
569
|
+
).toEqual({ start: 1, duration: 7, playbackStart: 1 });
|
|
570
|
+
});
|
|
571
|
+
|
|
423
572
|
it("prevents extending media left past available source before media-start", () => {
|
|
424
573
|
expect(
|
|
425
574
|
resolveTimelineResize(
|
|
@@ -175,6 +175,15 @@ export interface TimelineEditCapabilities {
|
|
|
175
175
|
canTrimEnd: boolean;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
export type BlockedTimelineEditIntent = "move" | "resize-start" | "resize-end";
|
|
179
|
+
|
|
180
|
+
export interface TimelineRangeSelection {
|
|
181
|
+
start: number;
|
|
182
|
+
end: number;
|
|
183
|
+
anchorX: number;
|
|
184
|
+
anchorY: number;
|
|
185
|
+
}
|
|
186
|
+
|
|
178
187
|
function isDeterministicTimelineWindow(input: {
|
|
179
188
|
tag: string;
|
|
180
189
|
compositionSrc?: string;
|
|
@@ -207,12 +216,7 @@ export function canOffsetTrimClipStart(input: {
|
|
|
207
216
|
if (input.playbackStartAttr != null) return true;
|
|
208
217
|
if (input.playbackStart != null) return true;
|
|
209
218
|
const normalizedTag = input.tag.toLowerCase();
|
|
210
|
-
|
|
211
|
-
return (
|
|
212
|
-
input.sourceDuration != null &&
|
|
213
|
-
Number.isFinite(input.sourceDuration) &&
|
|
214
|
-
input.sourceDuration > 0
|
|
215
|
-
);
|
|
219
|
+
return ["video", "audio"].includes(normalizedTag);
|
|
216
220
|
}
|
|
217
221
|
|
|
218
222
|
export function getTimelineEditCapabilities(input: {
|
|
@@ -235,6 +239,41 @@ export function getTimelineEditCapabilities(input: {
|
|
|
235
239
|
};
|
|
236
240
|
}
|
|
237
241
|
|
|
242
|
+
export function resolveBlockedTimelineEditIntent(input: {
|
|
243
|
+
width: number;
|
|
244
|
+
offsetX: number;
|
|
245
|
+
handleWidth: number;
|
|
246
|
+
capabilities: TimelineEditCapabilities;
|
|
247
|
+
}): BlockedTimelineEditIntent | null {
|
|
248
|
+
if (input.capabilities.canMove) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const safeWidth = Math.max(0, input.width);
|
|
253
|
+
const safeOffsetX = clamp(input.offsetX, 0, safeWidth);
|
|
254
|
+
const safeHandleWidth = Math.max(0, input.handleWidth);
|
|
255
|
+
|
|
256
|
+
if (safeOffsetX <= safeHandleWidth && !input.capabilities.canTrimStart) {
|
|
257
|
+
return "resize-start";
|
|
258
|
+
}
|
|
259
|
+
if (safeOffsetX >= Math.max(0, safeWidth - safeHandleWidth) && !input.capabilities.canTrimEnd) {
|
|
260
|
+
return "resize-end";
|
|
261
|
+
}
|
|
262
|
+
return "move";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function buildClipRangeSelection(
|
|
266
|
+
clip: { start: number; duration: number },
|
|
267
|
+
anchor: { anchorX: number; anchorY: number },
|
|
268
|
+
): TimelineRangeSelection {
|
|
269
|
+
return {
|
|
270
|
+
start: clip.start,
|
|
271
|
+
end: clip.start + clip.duration,
|
|
272
|
+
anchorX: anchor.anchorX,
|
|
273
|
+
anchorY: anchor.anchorY,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
238
277
|
export function buildTimelineAgentPrompt({
|
|
239
278
|
rangeStart,
|
|
240
279
|
rangeEnd,
|
|
@@ -559,7 +559,11 @@ export function useTimelinePlayer() {
|
|
|
559
559
|
|
|
560
560
|
// Convert a runtime timeline message (from iframe postMessage) into TimelineElements
|
|
561
561
|
const processTimelineMessage = useCallback(
|
|
562
|
-
(data: {
|
|
562
|
+
(data: {
|
|
563
|
+
clips: ClipManifestClip[];
|
|
564
|
+
durationInFrames: number;
|
|
565
|
+
scenes?: Array<{ id: string; label: string; start: number; duration: number }>;
|
|
566
|
+
}) => {
|
|
563
567
|
if (!data.clips || data.clips.length === 0) {
|
|
564
568
|
return;
|
|
565
569
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.\!container{width:100%!important}.container{width:100%}@media(min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media(min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media(min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media(min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media(min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.\!visible{visibility:visible!important}.visible{visibility:visible}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.bottom-0{bottom:0}.bottom-1{bottom:.25rem}.bottom-2{bottom:.5rem}.bottom-6{bottom:1.5rem}.bottom-full{bottom:100%}.left-0{left:0}.left-1\/2{left:50%}.left-2{left:.5rem}.right-0{right:0}.right-3{right:.75rem}.top-0{top:0}.top-1{top:.25rem}.top-1\/2{top:50%}.top-2{top:.5rem}.top-full{top:100%}.z-10{z-index:10}.z-20{z-index:20}.z-50{z-index:50}.z-\[100\]{z-index:100}.z-\[1\]{z-index:1}.z-\[200\]{z-index:200}.z-\[2\]{z-index:2}.z-\[90\]{z-index:90}.z-\[91\]{z-index:91}.mx-1{margin-left:.25rem;margin-right:.25rem}.my-0\.5{margin-top:.125rem;margin-bottom:.125rem}.my-1{margin-top:.25rem;margin-bottom:.25rem}.mb-0\.5{margin-bottom:.125rem}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.ml-1\.5{margin-left:.375rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-1{height:.25rem}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-2{height:.5rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-\[1080px\]{height:1080px}.h-\[3px\]{height:3px}.h-\[45px\]{height:45px}.h-\[52px\]{height:52px}.h-\[5px\]{height:5px}.h-full{height:100%}.h-px{height:1px}.max-h-24{max-height:6rem}.max-h-\[70\%\]{max-height:70%}.max-h-\[80vh\]{max-height:80vh}.max-h-full{max-height:100%}.min-h-0{min-height:0px}.min-h-7{min-height:1.75rem}.min-h-8{min-height:2rem}.min-h-9{min-height:2.25rem}.w-1\.5{width:.375rem}.w-14{width:3.5rem}.w-16{width:4rem}.w-2{width:.5rem}.w-20{width:5rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-52{width:13rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-80{width:20rem}.w-\[160px\]{width:160px}.w-\[1920px\]{width:1920px}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:0px}.min-w-7{min-width:1.75rem}.min-w-8{min-width:2rem}.min-w-9{min-width:2.25rem}.min-w-\[140px\]{min-width:140px}.min-w-\[160px\]{min-width:160px}.min-w-\[56px\]{min-width:56px}.min-w-\[58px\]{min-width:58px}.min-w-\[72px\]{min-width:72px}.max-w-\[280px\]{max-width:280px}.max-w-full{max-width:100%}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.grow{flex-grow:1}.-translate-x-1\/2{--tw-translate-x: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-col-resize{cursor:col-resize}.cursor-crosshair{cursor:crosshair}.cursor-default{cursor:default}.cursor-help{cursor:help}.cursor-pointer{cursor:pointer}.cursor-row-resize{cursor:row-resize}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-none{resize:none}.resize-y{resize:vertical}.\!resize{resize:both!important}.resize{resize:both}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-3{gap:.75rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-l{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-l{border-left-width:1px}.border-l-2{border-left-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-none{border-style:none}.border-green-500\/30{border-color:#22c55e4d}.border-neutral-600{--tw-border-opacity: 1;border-color:rgb(82 82 82 / var(--tw-border-opacity, 1))}.border-neutral-700{--tw-border-opacity: 1;border-color:rgb(64 64 64 / var(--tw-border-opacity, 1))}.border-neutral-700\/40{border-color:#40404066}.border-neutral-700\/50{border-color:#40404080}.border-neutral-700\/60{border-color:#40404099}.border-neutral-800{--tw-border-opacity: 1;border-color:rgb(38 38 38 / var(--tw-border-opacity, 1))}.border-neutral-800\/30{border-color:#2626264d}.border-neutral-800\/40{border-color:#26262666}.border-neutral-800\/50{border-color:#26262680}.border-neutral-800\/60{border-color:#26262699}.border-red-500{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity, 1))}.border-red-700\/50{border-color:#b91c1c80}.border-studio-accent{--tw-border-opacity: 1;border-color:rgb(60 230 172 / var(--tw-border-opacity, 1))}.border-studio-accent\/20{border-color:#3ce6ac33}.border-studio-accent\/25{border-color:#3ce6ac40}.border-studio-accent\/30{border-color:#3ce6ac4d}.border-studio-accent\/50{border-color:#3ce6ac80}.border-studio-accent\/60{border-color:#3ce6ac99}.border-transparent{border-color:transparent}.border-white\/20{border-color:#fff3}.border-t-white{--tw-border-opacity: 1;border-top-color:rgb(255 255 255 / var(--tw-border-opacity, 1))}.bg-\[\#0a0a0b\]{--tw-bg-opacity: 1;background-color:rgb(10 10 11 / var(--tw-bg-opacity, 1))}.bg-\[\#3CE6AC\]\/10{background-color:#3ce6ac1a}.bg-\[\#3CE6AC\]\/5{background-color:#3ce6ac0d}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-black\/20{background-color:#0003}.bg-black\/50{background-color:#00000080}.bg-black\/60{background-color:#0009}.bg-black\/80{background-color:#000c}.bg-green-500\/20{background-color:#22c55e33}.bg-green-600{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1))}.bg-neutral-600{--tw-bg-opacity: 1;background-color:rgb(82 82 82 / var(--tw-bg-opacity, 1))}.bg-neutral-700{--tw-bg-opacity: 1;background-color:rgb(64 64 64 / var(--tw-bg-opacity, 1))}.bg-neutral-700\/40{background-color:#40404066}.bg-neutral-800{--tw-bg-opacity: 1;background-color:rgb(38 38 38 / var(--tw-bg-opacity, 1))}.bg-neutral-800\/60{background-color:#26262699}.bg-neutral-800\/70{background-color:#262626b3}.bg-neutral-900{--tw-bg-opacity: 1;background-color:rgb(23 23 23 / var(--tw-bg-opacity, 1))}.bg-neutral-900\/50{background-color:#17171780}.bg-neutral-950{--tw-bg-opacity: 1;background-color:rgb(10 10 10 / var(--tw-bg-opacity, 1))}.bg-red-400{--tw-bg-opacity: 1;background-color:rgb(248 113 113 / var(--tw-bg-opacity, 1))}.bg-red-500\/10{background-color:#ef44441a}.bg-red-600{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.bg-red-900\/60{background-color:#7f1d1d99}.bg-red-900\/90{background-color:#7f1d1de6}.bg-red-950\/30{background-color:#450a0a4d}.bg-studio-accent{--tw-bg-opacity: 1;background-color:rgb(60 230 172 / var(--tw-bg-opacity, 1))}.bg-studio-accent\/10{background-color:#3ce6ac1a}.bg-studio-accent\/15{background-color:#3ce6ac26}.bg-studio-accent\/20{background-color:#3ce6ac33}.bg-studio-accent\/\[0\.03\]{background-color:#3ce6ac08}.bg-studio-accent\/\[0\.05\]{background-color:#3ce6ac0d}.bg-studio-accent\/\[0\.06\]{background-color:#3ce6ac0f}.bg-studio-accent\/\[0\.07\]{background-color:#3ce6ac12}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-white\/10{background-color:#ffffff1a}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0\.5{padding:.125rem}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-0\.5{padding-bottom:.125rem}.pb-3{padding-bottom:.75rem}.pt-1\.5{padding-top:.375rem}.pt-3{padding-top:.75rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[13px\]{font-size:13px}.text-\[9px\]{font-size:9px}.text-base{font-size:1rem;line-height:1.5rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.capitalize{text-transform:capitalize}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing: tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-5{line-height:1.25rem}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.leading-tight{line-height:1.25}.tracking-\[0\.08em\]{letter-spacing:.08em}.tracking-\[0\.16em\]{letter-spacing:.16em}.tracking-wider{letter-spacing:.05em}.text-\[\#09090B\]{--tw-text-opacity: 1;color:rgb(9 9 11 / var(--tw-text-opacity, 1))}.text-amber-400{--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-neutral-100{--tw-text-opacity: 1;color:rgb(245 245 245 / var(--tw-text-opacity, 1))}.text-neutral-200{--tw-text-opacity: 1;color:rgb(229 229 229 / var(--tw-text-opacity, 1))}.text-neutral-300{--tw-text-opacity: 1;color:rgb(212 212 212 / var(--tw-text-opacity, 1))}.text-neutral-400{--tw-text-opacity: 1;color:rgb(163 163 163 / var(--tw-text-opacity, 1))}.text-neutral-500{--tw-text-opacity: 1;color:rgb(115 115 115 / var(--tw-text-opacity, 1))}.text-neutral-600{--tw-text-opacity: 1;color:rgb(82 82 82 / var(--tw-text-opacity, 1))}.text-neutral-700{--tw-text-opacity: 1;color:rgb(64 64 64 / var(--tw-text-opacity, 1))}.text-neutral-950{--tw-text-opacity: 1;color:rgb(10 10 10 / var(--tw-text-opacity, 1))}.text-purple-400{--tw-text-opacity: 1;color:rgb(192 132 252 / var(--tw-text-opacity, 1))}.text-red-200{--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity, 1))}.text-red-300{--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-studio-accent{--tw-text-opacity: 1;color:rgb(60 230 172 / var(--tw-text-opacity, 1))}.text-studio-accent\/50{color:#3ce6ac80}.text-studio-accent\/80{color:#3ce6accc}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-white\/60{color:#fff9}.underline{text-decoration-line:underline}.line-through{text-decoration-line:line-through}.accent-studio-accent{accent-color:#3CE6AC}.opacity-25{opacity:.25}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-black\/40{--tw-shadow-color: rgb(0 0 0 / .4);--tw-shadow: var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.outline-1{outline-width:1px}.-outline-offset-1{outline-offset:-1px}.outline-\[\#3CE6AC\]\/30{outline-color:#3ce6ac4d}.outline-\[\#3CE6AC\]\/40{outline-color:#3ce6ac66}.ring{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-2{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-studio-accent{--tw-ring-opacity: 1;--tw-ring-color: rgb(60 230 172 / var(--tw-ring-opacity, 1))}.ring-white\/50{--tw-ring-color: rgb(255 255 255 / .5)}.blur{--tw-blur: blur(8px);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow{--tw-drop-shadow: drop-shadow(0 1px 2px rgb(0 0 0 / .1)) drop-shadow(0 1px 1px rgb(0 0 0 / .06));filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.invert{--tw-invert: invert(100%);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-150{transition-duration:.15s}.duration-300{transition-duration:.3s}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}body{margin:0;padding:0;background:#0a0a0a;color:#e5e5e5;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;overflow:hidden}#root{width:100vw;height:100vh;height:100dvh}.cm-editor{height:100%;font-size:13px}.cm-editor .cm-scroller{font-family:JetBrains Mono,Fira Code,SF Mono,monospace}.cm-editor.cm-focused{outline:none}.placeholder\:text-neutral-600::-moz-placeholder{--tw-text-opacity: 1;color:rgb(82 82 82 / var(--tw-text-opacity, 1))}.placeholder\:text-neutral-600::placeholder{--tw-text-opacity: 1;color:rgb(82 82 82 / var(--tw-text-opacity, 1))}.last\:border-0:last-child{border-width:0px}.hover\:border-neutral-500:hover{--tw-border-opacity: 1;border-color:rgb(115 115 115 / var(--tw-border-opacity, 1))}.hover\:border-neutral-600:hover{--tw-border-opacity: 1;border-color:rgb(82 82 82 / var(--tw-border-opacity, 1))}.hover\:border-neutral-700:hover{--tw-border-opacity: 1;border-color:rgb(64 64 64 / var(--tw-border-opacity, 1))}.hover\:border-studio-accent\/50:hover{border-color:#3ce6ac80}.hover\:bg-neutral-200:hover{--tw-bg-opacity: 1;background-color:rgb(229 229 229 / var(--tw-bg-opacity, 1))}.hover\:bg-neutral-600:hover{--tw-bg-opacity: 1;background-color:rgb(82 82 82 / var(--tw-bg-opacity, 1))}.hover\:bg-neutral-800:hover{--tw-bg-opacity: 1;background-color:rgb(38 38 38 / var(--tw-bg-opacity, 1))}.hover\:bg-neutral-800\/30:hover{background-color:#2626264d}.hover\:bg-neutral-800\/50:hover{background-color:#26262680}.hover\:bg-red-500:hover{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.hover\:bg-red-600:hover{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.hover\:bg-red-800\/60:hover{background-color:#991b1b99}.hover\:bg-red-900\/30:hover{background-color:#7f1d1d4d}.hover\:bg-studio-accent\/25:hover{background-color:#3ce6ac40}.hover\:bg-studio-accent\/80:hover{background-color:#3ce6accc}.hover\:text-amber-300:hover{--tw-text-opacity: 1;color:rgb(252 211 77 / var(--tw-text-opacity, 1))}.hover\:text-green-400:hover{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.hover\:text-neutral-100:hover{--tw-text-opacity: 1;color:rgb(245 245 245 / var(--tw-text-opacity, 1))}.hover\:text-neutral-200:hover{--tw-text-opacity: 1;color:rgb(229 229 229 / var(--tw-text-opacity, 1))}.hover\:text-neutral-300:hover{--tw-text-opacity: 1;color:rgb(212 212 212 / var(--tw-text-opacity, 1))}.hover\:text-neutral-400:hover{--tw-text-opacity: 1;color:rgb(163 163 163 / var(--tw-text-opacity, 1))}.hover\:text-red-400:hover{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.hover\:text-studio-accent:hover{--tw-text-opacity: 1;color:rgb(60 230 172 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.hover\:ring-1:hover{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.hover\:ring-white\/30:hover{--tw-ring-color: rgb(255 255 255 / .3)}.hover\:brightness-110:hover{--tw-brightness: brightness(1.1);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.focus\:border-\[\#3CE6AC\]:focus{--tw-border-opacity: 1;border-color:rgb(60 230 172 / var(--tw-border-opacity, 1))}.focus\:border-neutral-600:focus{--tw-border-opacity: 1;border-color:rgb(82 82 82 / var(--tw-border-opacity, 1))}.focus\:border-studio-accent:focus{--tw-border-opacity: 1;border-color:rgb(60 230 172 / var(--tw-border-opacity, 1))}.focus\:border-studio-accent\/40:focus{border-color:#3ce6ac66}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.active\:scale-\[0\.97\]:active{--tw-scale-x: .97;--tw-scale-y: .97;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.active\:scale-\[0\.98\]:active{--tw-scale-x: .98;--tw-scale-y: .98;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:scale-125{--tw-scale-x: 1.25;--tw-scale-y: 1.25;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@media(min-width:768px){.md\:inline{display:inline}}
|