@hyperframes/studio 0.1.13 → 0.1.15
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-CLmYRLY-.css +1 -0
- package/dist/assets/index-CRvFpc0E.js +84 -0
- package/dist/index.html +2 -2
- package/package.json +2 -2
- package/src/App.tsx +139 -657
- package/src/components/LintModal.tsx +149 -0
- package/src/components/MediaPreview.tsx +79 -0
- package/src/components/editor/FileTree.tsx +50 -40
- package/src/components/editor/PropertyPanel.tsx +3 -3
- package/src/components/nle/NLELayout.tsx +59 -43
- package/src/components/renders/RenderQueue.tsx +19 -16
- package/src/components/renders/RenderQueueItem.tsx +13 -8
- package/src/components/sidebar/AssetsTab.tsx +34 -144
- package/src/components/sidebar/CompositionsTab.tsx +47 -161
- package/src/components/sidebar/LeftSidebar.tsx +79 -8
- package/src/components/ui/VideoFrameThumbnail.tsx +1 -5
- package/src/index.ts +0 -3
- package/src/player/components/CompositionThumbnail.tsx +20 -94
- package/src/player/components/EditModal.tsx +5 -5
- package/src/player/components/PlayerControls.tsx +56 -3
- package/src/player/components/Timeline.tsx +13 -17
- package/src/player/components/TimelineClip.tsx +0 -1
- package/src/player/index.ts +0 -1
- package/src/player/store/playerStore.ts +3 -28
- package/src/utils/mediaTypes.ts +9 -0
- package/dist/assets/index-2uBPlHR_.css +0 -1
- package/dist/assets/index-uQ8cgxb3.js +0 -92
- package/src/components/ui/ExpandOnHover.tsx +0 -194
- package/src/components/ui/ExpandedVideoPreview.tsx +0 -37
- package/src/hooks/useCodeEditor.ts +0 -88
- package/src/player/components/PreviewPanel.tsx +0 -181
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CompositionThumbnail —
|
|
2
|
+
* CompositionThumbnail — Single server-rendered JPEG stretched across the clip.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Uses ResizeObserver to adapt frame count when the clip width changes (zoom).
|
|
4
|
+
* Takes one screenshot at the midpoint of the clip and covers the full width —
|
|
5
|
+
* same approach as After Effects for precomps. This avoids the 1-2s per-frame
|
|
6
|
+
* Puppeteer cost of rendering multiple filmstrip frames.
|
|
9
7
|
*/
|
|
10
8
|
|
|
11
|
-
import { memo
|
|
12
|
-
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
13
|
-
|
|
14
|
-
const CLIP_HEIGHT = 66;
|
|
15
|
-
const MAX_UNIQUE_FRAMES = 6;
|
|
9
|
+
import { memo } from "react";
|
|
16
10
|
|
|
17
11
|
interface CompositionThumbnailProps {
|
|
18
12
|
previewUrl: string;
|
|
@@ -30,95 +24,27 @@ export const CompositionThumbnail = memo(function CompositionThumbnail({
|
|
|
30
24
|
labelColor,
|
|
31
25
|
seekTime = 2,
|
|
32
26
|
duration = 5,
|
|
33
|
-
width = 1920,
|
|
34
|
-
height = 1080,
|
|
35
27
|
}: CompositionThumbnailProps) {
|
|
36
|
-
|
|
37
|
-
const roRef = useRef<ResizeObserver | null>(null);
|
|
38
|
-
|
|
39
|
-
const setRef = useCallback((el: HTMLDivElement | null) => {
|
|
40
|
-
roRef.current?.disconnect();
|
|
41
|
-
if (!el) return;
|
|
42
|
-
|
|
43
|
-
// Walk up to data-clip parent for accurate width
|
|
44
|
-
let target: HTMLElement = el;
|
|
45
|
-
let parent = el.parentElement;
|
|
46
|
-
let depth = 0;
|
|
47
|
-
while (parent && !parent.hasAttribute("data-clip") && depth < 5) {
|
|
48
|
-
parent = parent.parentElement;
|
|
49
|
-
depth++;
|
|
50
|
-
}
|
|
51
|
-
if (parent?.hasAttribute("data-clip")) target = parent;
|
|
52
|
-
|
|
53
|
-
requestAnimationFrame(() => {
|
|
54
|
-
const w = target.clientWidth || target.getBoundingClientRect().width;
|
|
55
|
-
if (w > 0) setContainerWidth(w);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
roRef.current = new ResizeObserver(([entry]) => setContainerWidth(entry.contentRect.width));
|
|
59
|
-
roRef.current.observe(target);
|
|
60
|
-
}, []);
|
|
61
|
-
|
|
62
|
-
useMountEffect(() => () => {
|
|
63
|
-
roRef.current?.disconnect();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// Convert preview URL to thumbnail base URL
|
|
28
|
+
// Single screenshot at the midpoint of the clip
|
|
67
29
|
const thumbnailBase = previewUrl
|
|
68
30
|
.replace("/preview/comp/", "/thumbnail/")
|
|
69
31
|
.replace(/\/preview$/, "/thumbnail/index.html");
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const aspect = width / height;
|
|
73
|
-
const frameW = Math.round(CLIP_HEIGHT * aspect);
|
|
74
|
-
const frameCount = containerWidth > 0 ? Math.max(1, Math.ceil(containerWidth / frameW)) : 1;
|
|
75
|
-
const uniqueFrames = Math.min(frameCount, MAX_UNIQUE_FRAMES);
|
|
76
|
-
|
|
77
|
-
// Each frame tile represents a real position in the clip.
|
|
78
|
-
// Offset slightly (0.5s) into each segment to avoid landing on transition
|
|
79
|
-
// points where content is invisible due to fade-in/fade-out animations.
|
|
80
|
-
const timestamps: number[] = [];
|
|
81
|
-
const pad = Math.min(0.5, duration * 0.05);
|
|
82
|
-
for (let i = 0; i < uniqueFrames; i++) {
|
|
83
|
-
const frac = uniqueFrames === 1 ? 0.5 : i / (uniqueFrames - 1);
|
|
84
|
-
const raw = seekTime + frac * duration;
|
|
85
|
-
// Clamp to [pad, duration - pad] to stay inside visible content
|
|
86
|
-
timestamps.push(seekTime + Math.max(pad, Math.min(duration - pad, raw - seekTime)));
|
|
87
|
-
}
|
|
32
|
+
const midTime = seekTime + duration / 2;
|
|
33
|
+
const url = `${thumbnailBase}?t=${midTime.toFixed(2)}`;
|
|
88
34
|
|
|
89
35
|
return (
|
|
90
|
-
<div
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
<div
|
|
103
|
-
key={i}
|
|
104
|
-
className="flex-shrink-0 h-full relative overflow-hidden bg-neutral-900"
|
|
105
|
-
style={{ width: frameW }}
|
|
106
|
-
>
|
|
107
|
-
<img
|
|
108
|
-
src={url}
|
|
109
|
-
alt=""
|
|
110
|
-
draggable={false}
|
|
111
|
-
loading="lazy"
|
|
112
|
-
onLoad={(e) => {
|
|
113
|
-
(e.target as HTMLImageElement).style.opacity = "1";
|
|
114
|
-
}}
|
|
115
|
-
className="absolute inset-0 w-full h-full object-contain"
|
|
116
|
-
style={{ opacity: 0, transition: "opacity 200ms ease-out" }}
|
|
117
|
-
/>
|
|
118
|
-
</div>
|
|
119
|
-
);
|
|
120
|
-
})}
|
|
121
|
-
</div>
|
|
36
|
+
<div className="absolute inset-0 overflow-hidden bg-neutral-950">
|
|
37
|
+
<img
|
|
38
|
+
src={url}
|
|
39
|
+
alt=""
|
|
40
|
+
draggable={false}
|
|
41
|
+
loading="lazy"
|
|
42
|
+
onLoad={(e) => {
|
|
43
|
+
(e.target as HTMLImageElement).style.opacity = "1";
|
|
44
|
+
}}
|
|
45
|
+
className="absolute inset-0 w-full h-full object-cover"
|
|
46
|
+
style={{ opacity: 0, transition: "opacity 200ms ease-out" }}
|
|
47
|
+
/>
|
|
122
48
|
|
|
123
49
|
{/* Label */}
|
|
124
50
|
<div
|
|
@@ -105,7 +105,7 @@ Preserve all other elements and timing outside this range.`;
|
|
|
105
105
|
{/* Header */}
|
|
106
106
|
<div className="flex items-center justify-between px-4 py-2.5 border-b border-neutral-800/60">
|
|
107
107
|
<div className="flex items-center gap-2">
|
|
108
|
-
<div className="w-1.5 h-1.5 rounded-full bg-
|
|
108
|
+
<div className="w-1.5 h-1.5 rounded-full bg-studio-accent" />
|
|
109
109
|
<span className="text-[11px] font-medium text-neutral-300">
|
|
110
110
|
{formatTime(start)} — {formatTime(end)}
|
|
111
111
|
</span>
|
|
@@ -120,7 +120,7 @@ Preserve all other elements and timing outside this range.`;
|
|
|
120
120
|
<div className="px-4 py-2 border-b border-neutral-800/40 max-h-24 overflow-y-auto">
|
|
121
121
|
{elementsInRange.map((el) => (
|
|
122
122
|
<div key={el.id} className="flex items-center justify-between py-0.5">
|
|
123
|
-
<span className="text-[10px] font-mono text-
|
|
123
|
+
<span className="text-[10px] font-mono text-studio-accent/80">#{el.id}</span>
|
|
124
124
|
<span className="text-[10px] text-neutral-600">{el.tag}</span>
|
|
125
125
|
</div>
|
|
126
126
|
))}
|
|
@@ -141,7 +141,7 @@ Preserve all other elements and timing outside this range.`;
|
|
|
141
141
|
}}
|
|
142
142
|
placeholder="What should change?"
|
|
143
143
|
rows={2}
|
|
144
|
-
className="w-full px-3 py-2 text-xs bg-neutral-800/60 border border-neutral-700/40 rounded-lg text-neutral-200 placeholder:text-neutral-600 resize-none focus:outline-none focus:border-
|
|
144
|
+
className="w-full px-3 py-2 text-xs bg-neutral-800/60 border border-neutral-700/40 rounded-lg text-neutral-200 placeholder:text-neutral-600 resize-none focus:outline-none focus:border-studio-accent/40 transition-colors"
|
|
145
145
|
/>
|
|
146
146
|
</div>
|
|
147
147
|
|
|
@@ -152,11 +152,11 @@ Preserve all other elements and timing outside this range.`;
|
|
|
152
152
|
className={`w-full py-1.5 text-[11px] font-medium rounded-lg transition-all ${
|
|
153
153
|
copied
|
|
154
154
|
? "bg-green-500/20 text-green-400 border border-green-500/30"
|
|
155
|
-
: "bg-
|
|
155
|
+
: "bg-studio-accent/15 text-studio-accent border border-studio-accent/25 hover:bg-studio-accent/25"
|
|
156
156
|
}`}
|
|
157
157
|
>
|
|
158
158
|
{copied ? "Copied!" : "Copy to Agent"}
|
|
159
|
-
{!copied && <span className="text-[9px] text-
|
|
159
|
+
{!copied && <span className="text-[9px] text-studio-accent/50 ml-1.5">Cmd+Enter</span>}
|
|
160
160
|
</button>
|
|
161
161
|
</div>
|
|
162
162
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useRef, useState, useCallback, memo } from "react";
|
|
1
|
+
import { useRef, useState, useCallback, useEffect, memo } from "react";
|
|
2
2
|
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
3
3
|
import { formatTime } from "../lib/time";
|
|
4
4
|
import { usePlayerStore, liveTime } from "../store/playerStore";
|
|
@@ -8,11 +8,15 @@ const SPEED_OPTIONS = [0.25, 0.5, 1, 1.5, 2] as const;
|
|
|
8
8
|
interface PlayerControlsProps {
|
|
9
9
|
onTogglePlay: () => void;
|
|
10
10
|
onSeek: (time: number) => void;
|
|
11
|
+
timelineVisible?: boolean;
|
|
12
|
+
onToggleTimeline?: () => void;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export const PlayerControls = memo(function PlayerControls({
|
|
14
16
|
onTogglePlay,
|
|
15
17
|
onSeek,
|
|
18
|
+
timelineVisible,
|
|
19
|
+
onToggleTimeline,
|
|
16
20
|
}: PlayerControlsProps) {
|
|
17
21
|
// Subscribe to only the fields we render — each selector prevents cascading re-renders
|
|
18
22
|
const isPlaying = usePlayerStore((s) => s.isPlaying);
|
|
@@ -26,6 +30,8 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
26
30
|
const progressThumbRef = useRef<HTMLDivElement>(null);
|
|
27
31
|
const timeDisplayRef = useRef<HTMLSpanElement>(null);
|
|
28
32
|
const seekBarRef = useRef<HTMLDivElement>(null);
|
|
33
|
+
const sliderRef = useRef<HTMLDivElement>(null);
|
|
34
|
+
const speedMenuContainerRef = useRef<HTMLDivElement>(null);
|
|
29
35
|
const isDraggingRef = useRef(false);
|
|
30
36
|
const currentTimeRef = useRef(0);
|
|
31
37
|
|
|
@@ -39,6 +45,7 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
39
45
|
if (progressFillRef.current) progressFillRef.current.style.width = `${pct}%`;
|
|
40
46
|
if (progressThumbRef.current) progressThumbRef.current.style.left = `${pct}%`;
|
|
41
47
|
if (timeDisplayRef.current) timeDisplayRef.current.textContent = formatTime(t);
|
|
48
|
+
if (sliderRef.current) sliderRef.current.setAttribute("aria-valuenow", String(Math.round(t)));
|
|
42
49
|
};
|
|
43
50
|
const unsub = liveTime.subscribe(updateProgress);
|
|
44
51
|
updateProgress(usePlayerStore.getState().currentTime);
|
|
@@ -60,6 +67,22 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
60
67
|
};
|
|
61
68
|
});
|
|
62
69
|
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (!showSpeedMenu) return;
|
|
72
|
+
const handleMouseDown = (e: MouseEvent) => {
|
|
73
|
+
if (
|
|
74
|
+
speedMenuContainerRef.current &&
|
|
75
|
+
!speedMenuContainerRef.current.contains(e.target as Node)
|
|
76
|
+
) {
|
|
77
|
+
setShowSpeedMenu(false);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
document.addEventListener("mousedown", handleMouseDown);
|
|
81
|
+
return () => {
|
|
82
|
+
document.removeEventListener("mousedown", handleMouseDown);
|
|
83
|
+
};
|
|
84
|
+
}, [showSpeedMenu]);
|
|
85
|
+
|
|
63
86
|
const seekFromClientX = useCallback(
|
|
64
87
|
(clientX: number) => {
|
|
65
88
|
const bar = seekBarRef.current;
|
|
@@ -149,7 +172,10 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
149
172
|
|
|
150
173
|
{/* Seek bar — teal progress fill */}
|
|
151
174
|
<div
|
|
152
|
-
ref={
|
|
175
|
+
ref={(el) => {
|
|
176
|
+
(seekBarRef as React.MutableRefObject<HTMLDivElement | null>).current = el;
|
|
177
|
+
(sliderRef as React.MutableRefObject<HTMLDivElement | null>).current = el;
|
|
178
|
+
}}
|
|
153
179
|
role="slider"
|
|
154
180
|
tabIndex={0}
|
|
155
181
|
aria-label="Seek"
|
|
@@ -184,7 +210,7 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
184
210
|
</div>
|
|
185
211
|
|
|
186
212
|
{/* Speed control */}
|
|
187
|
-
<div className="relative flex-shrink-0">
|
|
213
|
+
<div ref={speedMenuContainerRef} className="relative flex-shrink-0">
|
|
188
214
|
<button
|
|
189
215
|
type="button"
|
|
190
216
|
onClick={() => setShowSpeedMenu((v) => !v)}
|
|
@@ -224,6 +250,33 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
224
250
|
</div>
|
|
225
251
|
)}
|
|
226
252
|
</div>
|
|
253
|
+
|
|
254
|
+
{/* Timeline toggle */}
|
|
255
|
+
{onToggleTimeline !== undefined && (
|
|
256
|
+
<button
|
|
257
|
+
onClick={onToggleTimeline}
|
|
258
|
+
className={`w-7 h-7 flex items-center justify-center rounded-md border transition-colors ${
|
|
259
|
+
timelineVisible
|
|
260
|
+
? "text-studio-accent bg-studio-accent/10 border-studio-accent/30"
|
|
261
|
+
: "border-neutral-700 text-neutral-500 hover:text-neutral-300 hover:bg-neutral-800"
|
|
262
|
+
}`}
|
|
263
|
+
title={timelineVisible ? "Hide timeline" : "Show timeline"}
|
|
264
|
+
>
|
|
265
|
+
<svg
|
|
266
|
+
width="13"
|
|
267
|
+
height="13"
|
|
268
|
+
viewBox="0 0 24 24"
|
|
269
|
+
fill="none"
|
|
270
|
+
stroke="currentColor"
|
|
271
|
+
strokeWidth="2"
|
|
272
|
+
strokeLinecap="round"
|
|
273
|
+
>
|
|
274
|
+
<rect x="3" y="13" width="18" height="8" rx="1" />
|
|
275
|
+
<line x1="3" y1="9" x2="21" y2="9" />
|
|
276
|
+
<line x1="3" y1="5" x2="21" y2="5" />
|
|
277
|
+
</svg>
|
|
278
|
+
</button>
|
|
279
|
+
)}
|
|
227
280
|
</div>
|
|
228
281
|
);
|
|
229
282
|
});
|
|
@@ -152,9 +152,6 @@ export function generateTicks(duration: number): { major: number[]; minor: numbe
|
|
|
152
152
|
return { major, minor };
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
/** @deprecated Use formatTime from '../lib/time' instead */
|
|
156
|
-
export const formatTick = formatTime;
|
|
157
|
-
|
|
158
155
|
/* ── Component ──────────────────────────────────────────────────── */
|
|
159
156
|
interface TimelineProps {
|
|
160
157
|
/** Called when user seeks via ruler/track click or playhead drag */
|
|
@@ -170,11 +167,6 @@ interface TimelineProps {
|
|
|
170
167
|
renderClipOverlay?: (element: import("../store/playerStore").TimelineElement) => ReactNode;
|
|
171
168
|
/** Called when files are dropped onto the empty timeline */
|
|
172
169
|
onFileDrop?: (files: File[]) => void;
|
|
173
|
-
/** Called when a clip is moved, resized, or changes track via drag */
|
|
174
|
-
onClipChange?: (
|
|
175
|
-
elementId: string,
|
|
176
|
-
updates: { start?: number; duration?: number; track?: number },
|
|
177
|
-
) => void;
|
|
178
170
|
}
|
|
179
171
|
|
|
180
172
|
export const Timeline = memo(function Timeline({
|
|
@@ -346,12 +338,11 @@ export const Timeline = memo(function Timeline({
|
|
|
346
338
|
|
|
347
339
|
const handlePointerDown = useCallback(
|
|
348
340
|
(e: React.PointerEvent) => {
|
|
349
|
-
if ((e.target as HTMLElement).closest("[data-clip]")) return;
|
|
350
341
|
if (e.button !== 0) return;
|
|
351
|
-
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
|
352
342
|
|
|
353
|
-
// Shift+click starts range selection
|
|
343
|
+
// Shift+click starts range selection — even on clips
|
|
354
344
|
if (e.shiftKey) {
|
|
345
|
+
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
|
355
346
|
isRangeSelecting.current = true;
|
|
356
347
|
setShowPopover(false);
|
|
357
348
|
const rect = scrollRef.current?.getBoundingClientRect();
|
|
@@ -364,6 +355,10 @@ export const Timeline = memo(function Timeline({
|
|
|
364
355
|
return;
|
|
365
356
|
}
|
|
366
357
|
|
|
358
|
+
// Normal click on a clip — let the clip handle it
|
|
359
|
+
if ((e.target as HTMLElement).closest("[data-clip]")) return;
|
|
360
|
+
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
|
361
|
+
|
|
367
362
|
isDragging.current = true;
|
|
368
363
|
setRangeSelection(null);
|
|
369
364
|
setShowPopover(false);
|
|
@@ -434,7 +429,7 @@ export const Timeline = memo(function Timeline({
|
|
|
434
429
|
return (
|
|
435
430
|
<div
|
|
436
431
|
className={`h-full border-t bg-[#0a0a0b] flex flex-col select-none transition-colors duration-150 ${
|
|
437
|
-
isDragOver ? "border-
|
|
432
|
+
isDragOver ? "border-studio-accent/50 bg-studio-accent/[0.03]" : "border-neutral-800/50"
|
|
438
433
|
}`}
|
|
439
434
|
onDragOver={(e) => {
|
|
440
435
|
e.preventDefault();
|
|
@@ -471,7 +466,9 @@ export const Timeline = memo(function Timeline({
|
|
|
471
466
|
<div className="flex-1 flex items-center justify-center">
|
|
472
467
|
<div
|
|
473
468
|
className={`flex items-center gap-3 px-6 py-3 border border-dashed rounded-lg transition-colors duration-150 ${
|
|
474
|
-
isDragOver
|
|
469
|
+
isDragOver
|
|
470
|
+
? "border-studio-accent/60 bg-studio-accent/[0.06]"
|
|
471
|
+
: "border-neutral-700/50"
|
|
475
472
|
}`}
|
|
476
473
|
>
|
|
477
474
|
{isDragOver ? (
|
|
@@ -485,13 +482,13 @@ export const Timeline = memo(function Timeline({
|
|
|
485
482
|
strokeWidth="1.5"
|
|
486
483
|
strokeLinecap="round"
|
|
487
484
|
strokeLinejoin="round"
|
|
488
|
-
className="text-
|
|
485
|
+
className="text-studio-accent flex-shrink-0"
|
|
489
486
|
>
|
|
490
487
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
491
488
|
<polyline points="7 10 12 15 17 10" />
|
|
492
489
|
<line x1="12" y1="15" x2="12" y2="3" />
|
|
493
490
|
</svg>
|
|
494
|
-
<span className="text-[13px] text-
|
|
491
|
+
<span className="text-[13px] text-studio-accent">Drop media files to import</span>
|
|
495
492
|
</>
|
|
496
493
|
) : (
|
|
497
494
|
<>
|
|
@@ -573,7 +570,7 @@ export const Timeline = memo(function Timeline({
|
|
|
573
570
|
{/* Shift hint */}
|
|
574
571
|
{shiftHeld && !rangeSelection && (
|
|
575
572
|
<div className="absolute inset-0 flex items-center justify-center pointer-events-none z-10">
|
|
576
|
-
<span className="text-[9px] text-
|
|
573
|
+
<span className="text-[9px] text-studio-accent/60 font-medium">
|
|
577
574
|
Drag to select range
|
|
578
575
|
</span>
|
|
579
576
|
</div>
|
|
@@ -642,7 +639,6 @@ export const Timeline = memo(function Timeline({
|
|
|
642
639
|
key={clipKey}
|
|
643
640
|
el={el}
|
|
644
641
|
pps={pps}
|
|
645
|
-
trackH={TRACK_H}
|
|
646
642
|
clipY={CLIP_Y}
|
|
647
643
|
isSelected={isSelected}
|
|
648
644
|
isHovered={isHovered}
|
package/src/player/index.ts
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
export { Player } from "./components/Player";
|
|
3
3
|
export { PlayerControls } from "./components/PlayerControls";
|
|
4
4
|
export { Timeline } from "./components/Timeline";
|
|
5
|
-
export { PreviewPanel } from "./components/PreviewPanel";
|
|
6
5
|
export { VideoThumbnail } from "./components/VideoThumbnail";
|
|
7
6
|
export { CompositionThumbnail } from "./components/CompositionThumbnail";
|
|
8
7
|
|
|
@@ -27,10 +27,6 @@ interface PlayerState {
|
|
|
27
27
|
zoomMode: ZoomMode;
|
|
28
28
|
/** Pixels per second when in manual zoom mode */
|
|
29
29
|
pixelsPerSecond: number;
|
|
30
|
-
/** Edit range selection */
|
|
31
|
-
editRangeStart: number | null;
|
|
32
|
-
editRangeEnd: number | null;
|
|
33
|
-
editMode: boolean;
|
|
34
30
|
|
|
35
31
|
setIsPlaying: (playing: boolean) => void;
|
|
36
32
|
setCurrentTime: (time: number) => void;
|
|
@@ -39,11 +35,6 @@ interface PlayerState {
|
|
|
39
35
|
setTimelineReady: (ready: boolean) => void;
|
|
40
36
|
setElements: (elements: TimelineElement[]) => void;
|
|
41
37
|
setSelectedElementId: (id: string | null) => void;
|
|
42
|
-
setEditRange: (start: number | null, end: number | null) => void;
|
|
43
|
-
setEditMode: (active: boolean) => void;
|
|
44
|
-
updateElementStart: (elementId: string, newStart: number) => void;
|
|
45
|
-
updateElementDuration: (elementId: string, newDuration: number) => void;
|
|
46
|
-
updateElementTrack: (elementId: string, newTrack: number) => void;
|
|
47
38
|
updateElement: (
|
|
48
39
|
elementId: string,
|
|
49
40
|
updates: Partial<Pick<TimelineElement, "start" | "duration" | "track">>,
|
|
@@ -76,9 +67,6 @@ export const usePlayerStore = create<PlayerState>((set) => ({
|
|
|
76
67
|
playbackRate: 1,
|
|
77
68
|
zoomMode: "fit",
|
|
78
69
|
pixelsPerSecond: 100,
|
|
79
|
-
editRangeStart: null,
|
|
80
|
-
editRangeEnd: null,
|
|
81
|
-
editMode: false,
|
|
82
70
|
|
|
83
71
|
setIsPlaying: (playing) => set({ isPlaying: playing }),
|
|
84
72
|
setPlaybackRate: (rate) => set({ playbackRate: rate }),
|
|
@@ -89,26 +77,13 @@ export const usePlayerStore = create<PlayerState>((set) => ({
|
|
|
89
77
|
setTimelineReady: (ready) => set({ timelineReady: ready }),
|
|
90
78
|
setElements: (elements) => set({ elements }),
|
|
91
79
|
setSelectedElementId: (id) => set({ selectedElementId: id }),
|
|
92
|
-
setEditRange: (start, end) => set({ editRangeStart: start, editRangeEnd: end }),
|
|
93
|
-
setEditMode: (active) => set({ editMode: active, editRangeStart: null, editRangeEnd: null }),
|
|
94
|
-
updateElementStart: (elementId, newStart) =>
|
|
95
|
-
set((state) => ({
|
|
96
|
-
elements: state.elements.map((el) => (el.id === elementId ? { ...el, start: newStart } : el)),
|
|
97
|
-
})),
|
|
98
|
-
updateElementDuration: (elementId, newDuration) =>
|
|
99
|
-
set((state) => ({
|
|
100
|
-
elements: state.elements.map((el) =>
|
|
101
|
-
el.id === elementId ? { ...el, duration: newDuration } : el,
|
|
102
|
-
),
|
|
103
|
-
})),
|
|
104
|
-
updateElementTrack: (elementId, newTrack) =>
|
|
105
|
-
set((state) => ({
|
|
106
|
-
elements: state.elements.map((el) => (el.id === elementId ? { ...el, track: newTrack } : el)),
|
|
107
|
-
})),
|
|
108
80
|
updateElement: (elementId, updates) =>
|
|
109
81
|
set((state) => ({
|
|
110
82
|
elements: state.elements.map((el) => (el.id === elementId ? { ...el, ...updates } : el)),
|
|
111
83
|
})),
|
|
84
|
+
// Resets project-specific state when switching compositions.
|
|
85
|
+
// playbackRate, zoomMode, and pixelsPerSecond are intentionally preserved
|
|
86
|
+
// because they are user preferences that should survive project switches.
|
|
112
87
|
reset: () =>
|
|
113
88
|
set({
|
|
114
89
|
isPlaying: false,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const IMAGE_EXT = /\.(jpg|jpeg|png|gif|webp|svg|ico)$/i;
|
|
2
|
+
export const VIDEO_EXT = /\.(mp4|webm|mov)$/i;
|
|
3
|
+
export const AUDIO_EXT = /\.(mp3|wav|ogg|m4a|aac)$/i;
|
|
4
|
+
export const FONT_EXT = /\.(woff|woff2|ttf|otf|eot)$/i;
|
|
5
|
+
export const MEDIA_EXT = /\.(mp4|webm|mov|mp3|wav|ogg|m4a|aac|jpg|jpeg|png|gif|webp|svg|ico)$/i;
|
|
6
|
+
|
|
7
|
+
export function isMediaFile(path: string): boolean {
|
|
8
|
+
return MEDIA_EXT.test(path);
|
|
9
|
+
}
|
|
@@ -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}.invisible{visibility:hidden}.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-2{bottom:.5rem}.bottom-full{bottom:100%}.left-0{left:0}.right-0{right:0}.right-3{right:.75rem}.top-0{top:0}.top-1\/2{top:50%}.z-10{z-index:10}.z-20{z-index:20}.z-40{z-index:40}.z-50{z-index:50}.z-\[100\]{z-index:100}.z-\[1\]{z-index:1}.z-\[2\]{z-index:2}.mx-auto{margin-left:auto;margin-right:auto}.mb-0\.5{margin-bottom:.125rem}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.ml-1\.5{margin-left:.375rem}.ml-11{margin-left:2.75rem}.ml-auto{margin-left:auto}.mr-4{margin-right:1rem}.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{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.aspect-video{aspect-ratio:16 / 9}.h-1{height:.25rem}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-16{height:4rem}.h-2{height:.5rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.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-\[3px\]{height:3px}.h-\[45px\]{height:45px}.h-\[5px\]{height:5px}.h-full{height:100%}.h-screen{height:100vh}.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{width:.25rem}.w-1\.5{width:.375rem}.w-16{width:4rem}.w-2{width:.5rem}.w-20{width:5rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-5{width:1.25rem}.w-64{width:16rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-80{width:20rem}.w-\[140px\]{width:140px}.w-full{width:100%}.w-px{width:1px}.w-screen{width:100vw}.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-\[56px\]{min-width:56px}.min-w-\[72px\]{min-width:72px}.max-w-4xl{max-width:56rem}.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}.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-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}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.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}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.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-\[16px\]{border-radius:16px}.rounded-\[4px\]{border-radius:4px}.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-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-\[\#3CE6AC\]{--tw-border-opacity: 1;border-color:rgb(60 230 172 / var(--tw-border-opacity, 1))}.border-blue-400\/60{border-color:#60a5fa99}.border-blue-500{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.border-blue-500\/25{border-color:#3b82f640}.border-blue-500\/30{border-color:#3b82f64d}.border-blue-500\/50{border-color:#3b82f680}.border-green-500\/30{border-color:#22c55e4d}.border-neutral-700{--tw-border-opacity: 1;border-color:rgb(64 64 64 / var(--tw-border-opacity, 1))}.border-neutral-700\/20{border-color:#40404033}.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-transparent{border-color:transparent}.bg-\[\#0a0a0b\]{--tw-bg-opacity: 1;background-color:rgb(10 10 11 / var(--tw-bg-opacity, 1))}.bg-\[\#3CE6AC\]{--tw-bg-opacity: 1;background-color:rgb(60 230 172 / var(--tw-bg-opacity, 1))}.bg-\[\#3CE6AC\]\/10{background-color:#3ce6ac1a}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-black\/60{background-color:#0009}.bg-blue-400{--tw-bg-opacity: 1;background-color:rgb(96 165 250 / var(--tw-bg-opacity, 1))}.bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}.bg-blue-500\/10{background-color:#3b82f61a}.bg-blue-500\/15{background-color:#3b82f626}.bg-blue-500\/20{background-color:#3b82f633}.bg-blue-500\/\[0\.03\]{background-color:#3b82f608}.bg-blue-500\/\[0\.06\]{background-color:#3b82f60f}.bg-blue-950\/20{background-color:#17255433}.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-600\/60{background-color:#52525299}.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\/50{background-color:#26262680}.bg-neutral-800\/60{background-color:#26262699}.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-transparent{background-color:transparent}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.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-3\.5{padding-left:.875rem;padding-right:.875rem}.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}.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-24{padding-top:6rem;padding-bottom:6rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-0\.5{padding-bottom:.125rem}.pb-16{padding-bottom:4rem}.pb-3{padding-bottom:.75rem}.pb-8{padding-bottom:2rem}.pr-1\.5{padding-right:.375rem}.pt-16{padding-top:4rem}.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-2xl{font-size:1.5rem;line-height:2rem}.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-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.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-none{line-height:1}.leading-relaxed{line-height:1.625}.leading-tight{line-height:1.25}.tracking-tight{letter-spacing:-.025em}.tracking-wider{letter-spacing:.05em}.text-\[\#09090B\]{--tw-text-opacity: 1;color:rgb(9 9 11 / var(--tw-text-opacity, 1))}.text-\[\#3CE6AC\]{--tw-text-opacity: 1;color:rgb(60 230 172 / var(--tw-text-opacity, 1))}.text-amber-400{--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity, 1))}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-blue-400\/50{color:#60a5fa80}.text-blue-400\/60{color:#60a5fa99}.text-blue-400\/80{color:#60a5facc}.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-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.opacity-20{opacity:.2}.opacity-25{opacity:.25}.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-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}.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)}.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-\[width\]{transition-property:width;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-200{transition-duration:.2s}.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}.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-\[\#3CE6AC\]\/30:hover{border-color:#3ce6ac4d}.hover\:border-neutral-600:hover{--tw-border-opacity: 1;border-color:rgb(82 82 82 / var(--tw-border-opacity, 1))}.hover\:bg-\[\#3CE6AC\]\/80:hover{background-color:#3ce6accc}.hover\:bg-blue-500:hover{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}.hover\:bg-blue-500\/25:hover{background-color:#3b82f640}.hover\:bg-neutral-200:hover{--tw-bg-opacity: 1;background-color:rgb(229 229 229 / 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-600:hover{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.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-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-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.hover\:shadow-lg:hover{--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)}.hover\:shadow-\[\#3CE6AC\]\/5:hover{--tw-shadow-color: rgb(60 230 172 / .05);--tw-shadow: var(--tw-shadow-colored)}.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-blue-500\/40:focus{border-color:#3b82f666}.focus\:border-neutral-600:focus{--tw-border-opacity: 1;border-color:rgb(82 82 82 / var(--tw-border-opacity, 1))}.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))}.active\:bg-blue-400:active{--tw-bg-opacity: 1;background-color:rgb(96 165 250 / var(--tw-bg-opacity, 1))}.disabled\:pointer-events-none:disabled{pointer-events:none}.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: 640px){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width: 1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}
|