@hyperframes/studio 0.1.13 → 0.1.14
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,194 +0,0 @@
|
|
|
1
|
-
import React, { useState, useRef, useCallback, useEffect, type ReactNode } from "react";
|
|
2
|
-
import { motion, AnimatePresence } from "motion/react";
|
|
3
|
-
|
|
4
|
-
interface ExpandOnHoverProps {
|
|
5
|
-
children: ReactNode;
|
|
6
|
-
expandedContent?: ReactNode | ((close: () => void) => ReactNode);
|
|
7
|
-
expandScale?: number;
|
|
8
|
-
delay?: number;
|
|
9
|
-
className?: string;
|
|
10
|
-
onClick?: () => void;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function ExpandOnHover({
|
|
14
|
-
children,
|
|
15
|
-
expandedContent,
|
|
16
|
-
expandScale = 0.75,
|
|
17
|
-
delay = 300,
|
|
18
|
-
className = "",
|
|
19
|
-
onClick,
|
|
20
|
-
}: ExpandOnHoverProps) {
|
|
21
|
-
const [isExpanded, setIsExpanded] = useState(false);
|
|
22
|
-
const [origin, setOrigin] = useState({ x: 0, y: 0, w: 0, h: 0 });
|
|
23
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
24
|
-
const expandedRef = useRef<HTMLDivElement>(null);
|
|
25
|
-
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
26
|
-
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
27
|
-
|
|
28
|
-
const close = useCallback(() => {
|
|
29
|
-
if (timerRef.current) {
|
|
30
|
-
clearTimeout(timerRef.current);
|
|
31
|
-
timerRef.current = null;
|
|
32
|
-
}
|
|
33
|
-
if (closeTimerRef.current) {
|
|
34
|
-
clearTimeout(closeTimerRef.current);
|
|
35
|
-
closeTimerRef.current = null;
|
|
36
|
-
}
|
|
37
|
-
setIsExpanded(false);
|
|
38
|
-
}, []);
|
|
39
|
-
|
|
40
|
-
const open = useCallback(() => {
|
|
41
|
-
if (!containerRef.current) return;
|
|
42
|
-
const rect = containerRef.current.getBoundingClientRect();
|
|
43
|
-
setOrigin({ x: rect.left, y: rect.top, w: rect.width, h: rect.height });
|
|
44
|
-
setIsExpanded(true);
|
|
45
|
-
}, []);
|
|
46
|
-
|
|
47
|
-
const handleCardEnter = useCallback(() => {
|
|
48
|
-
if (isExpanded) return;
|
|
49
|
-
if (timerRef.current) clearTimeout(timerRef.current);
|
|
50
|
-
timerRef.current = setTimeout(open, delay);
|
|
51
|
-
}, [delay, open, isExpanded]);
|
|
52
|
-
|
|
53
|
-
const handleCardLeave = useCallback(() => {
|
|
54
|
-
if (isExpanded) return;
|
|
55
|
-
if (timerRef.current) {
|
|
56
|
-
clearTimeout(timerRef.current);
|
|
57
|
-
timerRef.current = null;
|
|
58
|
-
}
|
|
59
|
-
}, [isExpanded]);
|
|
60
|
-
|
|
61
|
-
// When expanded: track mouse position. If mouse stays outside the expanded
|
|
62
|
-
// card for 600ms continuously, close. Any re-entry resets the timer.
|
|
63
|
-
// Note: useEffect with [isExpanded] is acceptable — subscribes to window mousemove
|
|
64
|
-
// only while expanded, with cleanup on collapse. Can't be a mount effect.
|
|
65
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
66
|
-
useEffect(() => {
|
|
67
|
-
if (!isExpanded) return;
|
|
68
|
-
|
|
69
|
-
const CLOSE_DELAY = 600; // ms mouse must be outside to close
|
|
70
|
-
const START_DELAY = 400; // ms before we start checking (let animation settle)
|
|
71
|
-
let tracking = false;
|
|
72
|
-
|
|
73
|
-
const startTracking = setTimeout(() => {
|
|
74
|
-
tracking = true;
|
|
75
|
-
}, START_DELAY);
|
|
76
|
-
|
|
77
|
-
const handleMouseMove = (e: MouseEvent) => {
|
|
78
|
-
if (!tracking) return;
|
|
79
|
-
const el = expandedRef.current;
|
|
80
|
-
if (!el) return;
|
|
81
|
-
|
|
82
|
-
const rect = el.getBoundingClientRect();
|
|
83
|
-
// Add generous padding so edge movements don't trigger close
|
|
84
|
-
const pad = 20;
|
|
85
|
-
const inside =
|
|
86
|
-
e.clientX >= rect.left - pad &&
|
|
87
|
-
e.clientX <= rect.right + pad &&
|
|
88
|
-
e.clientY >= rect.top - pad &&
|
|
89
|
-
e.clientY <= rect.bottom + pad;
|
|
90
|
-
|
|
91
|
-
if (inside) {
|
|
92
|
-
// Mouse is inside — cancel any pending close
|
|
93
|
-
if (closeTimerRef.current) {
|
|
94
|
-
clearTimeout(closeTimerRef.current);
|
|
95
|
-
closeTimerRef.current = null;
|
|
96
|
-
}
|
|
97
|
-
} else {
|
|
98
|
-
// Mouse is outside — start close countdown if not already started
|
|
99
|
-
if (!closeTimerRef.current) {
|
|
100
|
-
closeTimerRef.current = setTimeout(() => {
|
|
101
|
-
setIsExpanded(false);
|
|
102
|
-
}, CLOSE_DELAY);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
window.addEventListener("mousemove", handleMouseMove);
|
|
108
|
-
|
|
109
|
-
return () => {
|
|
110
|
-
clearTimeout(startTracking);
|
|
111
|
-
if (closeTimerRef.current) {
|
|
112
|
-
clearTimeout(closeTimerRef.current);
|
|
113
|
-
closeTimerRef.current = null;
|
|
114
|
-
}
|
|
115
|
-
window.removeEventListener("mousemove", handleMouseMove);
|
|
116
|
-
};
|
|
117
|
-
}, [isExpanded]);
|
|
118
|
-
|
|
119
|
-
const vw = typeof window !== "undefined" ? window.innerWidth : 1440;
|
|
120
|
-
const vh = typeof window !== "undefined" ? window.innerHeight : 900;
|
|
121
|
-
const targetW = vw * expandScale;
|
|
122
|
-
const targetH = vh * expandScale;
|
|
123
|
-
const targetX = (vw - targetW) / 2;
|
|
124
|
-
const targetY = (vh - targetH) / 2;
|
|
125
|
-
|
|
126
|
-
return (
|
|
127
|
-
<>
|
|
128
|
-
<div
|
|
129
|
-
ref={containerRef}
|
|
130
|
-
className={className}
|
|
131
|
-
onMouseEnter={handleCardEnter}
|
|
132
|
-
onMouseLeave={handleCardLeave}
|
|
133
|
-
onClick={onClick}
|
|
134
|
-
style={{ opacity: isExpanded ? 0 : 1, transition: "opacity 100ms ease-out" }}
|
|
135
|
-
>
|
|
136
|
-
{children}
|
|
137
|
-
</div>
|
|
138
|
-
|
|
139
|
-
<AnimatePresence>
|
|
140
|
-
{isExpanded && (
|
|
141
|
-
<>
|
|
142
|
-
{/* Backdrop */}
|
|
143
|
-
<motion.div
|
|
144
|
-
initial={{ opacity: 0 }}
|
|
145
|
-
animate={{ opacity: 1 }}
|
|
146
|
-
exit={{ opacity: 0 }}
|
|
147
|
-
transition={{ duration: 0.15 }}
|
|
148
|
-
className="fixed inset-0 z-40 bg-black/60 backdrop-blur-sm"
|
|
149
|
-
onClick={close}
|
|
150
|
-
/>
|
|
151
|
-
{/* Expanded card */}
|
|
152
|
-
<motion.div
|
|
153
|
-
ref={expandedRef}
|
|
154
|
-
initial={{
|
|
155
|
-
left: origin.x,
|
|
156
|
-
top: origin.y,
|
|
157
|
-
width: origin.w,
|
|
158
|
-
height: origin.h,
|
|
159
|
-
}}
|
|
160
|
-
animate={{
|
|
161
|
-
left: targetX,
|
|
162
|
-
top: targetY,
|
|
163
|
-
width: targetW,
|
|
164
|
-
height: targetH,
|
|
165
|
-
}}
|
|
166
|
-
exit={{
|
|
167
|
-
left: origin.x,
|
|
168
|
-
top: origin.y,
|
|
169
|
-
width: origin.w,
|
|
170
|
-
height: origin.h,
|
|
171
|
-
}}
|
|
172
|
-
transition={{
|
|
173
|
-
type: "spring",
|
|
174
|
-
stiffness: 280,
|
|
175
|
-
damping: 28,
|
|
176
|
-
mass: 0.8,
|
|
177
|
-
}}
|
|
178
|
-
className="fixed z-50 overflow-hidden rounded-[16px] shadow-dialog"
|
|
179
|
-
onClick={(e: React.MouseEvent) => {
|
|
180
|
-
e.stopPropagation();
|
|
181
|
-
close();
|
|
182
|
-
onClick?.();
|
|
183
|
-
}}
|
|
184
|
-
>
|
|
185
|
-
{typeof expandedContent === "function"
|
|
186
|
-
? expandedContent(close)
|
|
187
|
-
: (expandedContent ?? children)}
|
|
188
|
-
</motion.div>
|
|
189
|
-
</>
|
|
190
|
-
)}
|
|
191
|
-
</AnimatePresence>
|
|
192
|
-
</>
|
|
193
|
-
);
|
|
194
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
|
-
|
|
3
|
-
interface ExpandedVideoPreviewProps {
|
|
4
|
-
src: string;
|
|
5
|
-
name: string;
|
|
6
|
-
subtitle: string;
|
|
7
|
-
action: ReactNode;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Shared expanded video preview used by AssetsTab (video assets) and
|
|
12
|
-
* the Renders panel. Autoplays the video muted+looped inside a full-bleed
|
|
13
|
-
* card. Caller provides the footer action slot (Copy Path, Open, etc.).
|
|
14
|
-
*/
|
|
15
|
-
export function ExpandedVideoPreview({ src, name, subtitle, action }: ExpandedVideoPreviewProps) {
|
|
16
|
-
return (
|
|
17
|
-
<div className="w-full h-full bg-neutral-950 rounded-[16px] overflow-hidden flex flex-col">
|
|
18
|
-
<div className="flex-1 min-h-0 flex items-center justify-center bg-black p-4">
|
|
19
|
-
<video
|
|
20
|
-
src={src}
|
|
21
|
-
autoPlay
|
|
22
|
-
muted
|
|
23
|
-
loop
|
|
24
|
-
playsInline
|
|
25
|
-
className="max-w-full max-h-full object-contain rounded"
|
|
26
|
-
/>
|
|
27
|
-
</div>
|
|
28
|
-
<div className="px-5 py-3 bg-neutral-900 border-t border-neutral-800/50 flex items-center justify-between flex-shrink-0">
|
|
29
|
-
<div className="min-w-0 flex-1 mr-4">
|
|
30
|
-
<div className="text-sm font-medium text-neutral-200 truncate">{name}</div>
|
|
31
|
-
<div className="text-[10px] text-neutral-600 font-mono mt-0.5 truncate">{subtitle}</div>
|
|
32
|
-
</div>
|
|
33
|
-
{action}
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
36
|
-
);
|
|
37
|
-
}
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { useState, useCallback } from "react";
|
|
2
|
-
|
|
3
|
-
export interface OpenFile {
|
|
4
|
-
path: string;
|
|
5
|
-
content: string;
|
|
6
|
-
savedContent: string;
|
|
7
|
-
isDirty: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface UseCodeEditorReturn {
|
|
11
|
-
openFiles: OpenFile[];
|
|
12
|
-
activeFilePath: string | null;
|
|
13
|
-
activeFile: OpenFile | null;
|
|
14
|
-
openFile: (path: string, content: string) => void;
|
|
15
|
-
closeFile: (path: string) => void;
|
|
16
|
-
setActiveFile: (path: string) => void;
|
|
17
|
-
updateContent: (content: string) => void;
|
|
18
|
-
markSaved: (path: string) => void;
|
|
19
|
-
/** External update — updates saved content, shows reload indicator */
|
|
20
|
-
externalUpdate: (path: string, content: string) => void;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function useCodeEditor(): UseCodeEditorReturn {
|
|
24
|
-
const [openFiles, setOpenFiles] = useState<OpenFile[]>([]);
|
|
25
|
-
const [activeFilePath, setActiveFilePath] = useState<string | null>(null);
|
|
26
|
-
|
|
27
|
-
const activeFile = openFiles.find((f) => f.path === activeFilePath) ?? null;
|
|
28
|
-
|
|
29
|
-
const openFile = useCallback((path: string, content: string) => {
|
|
30
|
-
setOpenFiles((prev) => {
|
|
31
|
-
const existing = prev.find((f) => f.path === path);
|
|
32
|
-
if (existing) return prev;
|
|
33
|
-
return [...prev, { path, content, savedContent: content, isDirty: false }];
|
|
34
|
-
});
|
|
35
|
-
setActiveFilePath(path);
|
|
36
|
-
}, []);
|
|
37
|
-
|
|
38
|
-
const closeFile = useCallback(
|
|
39
|
-
(path: string) => {
|
|
40
|
-
setOpenFiles((prev) => prev.filter((f) => f.path !== path));
|
|
41
|
-
setActiveFilePath((prev) => {
|
|
42
|
-
if (prev === path) {
|
|
43
|
-
const remaining = openFiles.filter((f) => f.path !== path);
|
|
44
|
-
return remaining.length > 0 ? remaining[remaining.length - 1].path : null;
|
|
45
|
-
}
|
|
46
|
-
return prev;
|
|
47
|
-
});
|
|
48
|
-
},
|
|
49
|
-
[openFiles],
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
const updateContent = useCallback(
|
|
53
|
-
(content: string) => {
|
|
54
|
-
setOpenFiles((prev) =>
|
|
55
|
-
prev.map((f) =>
|
|
56
|
-
f.path === activeFilePath ? { ...f, content, isDirty: content !== f.savedContent } : f,
|
|
57
|
-
),
|
|
58
|
-
);
|
|
59
|
-
},
|
|
60
|
-
[activeFilePath],
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
const markSaved = useCallback((path: string) => {
|
|
64
|
-
setOpenFiles((prev) =>
|
|
65
|
-
prev.map((f) => (f.path === path ? { ...f, savedContent: f.content, isDirty: false } : f)),
|
|
66
|
-
);
|
|
67
|
-
}, []);
|
|
68
|
-
|
|
69
|
-
const externalUpdate = useCallback((path: string, content: string) => {
|
|
70
|
-
setOpenFiles((prev) =>
|
|
71
|
-
prev.map((f) =>
|
|
72
|
-
f.path === path ? { ...f, savedContent: content, content, isDirty: false } : f,
|
|
73
|
-
),
|
|
74
|
-
);
|
|
75
|
-
}, []);
|
|
76
|
-
|
|
77
|
-
return {
|
|
78
|
-
openFiles,
|
|
79
|
-
activeFilePath,
|
|
80
|
-
activeFile,
|
|
81
|
-
openFile,
|
|
82
|
-
closeFile,
|
|
83
|
-
setActiveFile: setActiveFilePath,
|
|
84
|
-
updateContent,
|
|
85
|
-
markSaved,
|
|
86
|
-
externalUpdate,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import type { ReactNode, Ref } from "react";
|
|
2
|
-
import { Player } from "./Player";
|
|
3
|
-
import { PlayerControls } from "./PlayerControls";
|
|
4
|
-
import { Timeline } from "./Timeline";
|
|
5
|
-
|
|
6
|
-
interface RenderStatus {
|
|
7
|
-
state: "idle" | "rendering" | "complete" | "error";
|
|
8
|
-
stage?: string;
|
|
9
|
-
progress?: number;
|
|
10
|
-
error?: string;
|
|
11
|
-
onRender?: () => void;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface PreviewPanelProps {
|
|
15
|
-
projectId: string | null;
|
|
16
|
-
hasProject: boolean;
|
|
17
|
-
portrait: boolean;
|
|
18
|
-
iframeRef: Ref<HTMLIFrameElement>;
|
|
19
|
-
onIframeLoad: () => void;
|
|
20
|
-
onTogglePlay: () => void;
|
|
21
|
-
onSeek: (t: number) => void;
|
|
22
|
-
/** Optional render status — pass to show rendering progress/state */
|
|
23
|
-
renderStatus?: RenderStatus;
|
|
24
|
-
/** Optional slot for custom content below the timeline */
|
|
25
|
-
children?: ReactNode;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function PreviewPanel({
|
|
29
|
-
projectId,
|
|
30
|
-
hasProject,
|
|
31
|
-
portrait,
|
|
32
|
-
iframeRef,
|
|
33
|
-
onIframeLoad,
|
|
34
|
-
onTogglePlay,
|
|
35
|
-
onSeek,
|
|
36
|
-
renderStatus,
|
|
37
|
-
children,
|
|
38
|
-
}: PreviewPanelProps) {
|
|
39
|
-
const renderState = renderStatus?.state ?? "idle";
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<div
|
|
43
|
-
className="min-w-0 overflow-hidden"
|
|
44
|
-
style={{
|
|
45
|
-
display: "grid",
|
|
46
|
-
gridTemplateRows: hasProject && projectId ? "1fr auto auto auto" : "1fr",
|
|
47
|
-
height: "100%",
|
|
48
|
-
minHeight: 0,
|
|
49
|
-
}}
|
|
50
|
-
>
|
|
51
|
-
{hasProject && projectId ? (
|
|
52
|
-
<>
|
|
53
|
-
{/* Player — takes all remaining space, constrained for portrait */}
|
|
54
|
-
<div
|
|
55
|
-
className="flex items-center justify-center p-2 overflow-hidden"
|
|
56
|
-
style={{ minHeight: 0, minWidth: 0 }}
|
|
57
|
-
>
|
|
58
|
-
<Player
|
|
59
|
-
ref={iframeRef}
|
|
60
|
-
projectId={projectId}
|
|
61
|
-
onLoad={onIframeLoad}
|
|
62
|
-
portrait={portrait}
|
|
63
|
-
/>
|
|
64
|
-
</div>
|
|
65
|
-
|
|
66
|
-
{/* Controls — fixed height */}
|
|
67
|
-
<div className="bg-neutral-950 border-t border-neutral-800 flex-shrink-0">
|
|
68
|
-
<PlayerControls onTogglePlay={onTogglePlay} onSeek={onSeek} />
|
|
69
|
-
</div>
|
|
70
|
-
|
|
71
|
-
{/* Timeline — capped height, internal scroll */}
|
|
72
|
-
<div
|
|
73
|
-
className="bg-neutral-950 flex-shrink-0 overflow-y-auto"
|
|
74
|
-
style={{ maxHeight: "100px" }}
|
|
75
|
-
>
|
|
76
|
-
<Timeline onSeek={onSeek} />
|
|
77
|
-
</div>
|
|
78
|
-
|
|
79
|
-
{/* Render status — only shown when actively rendering, complete, or error */}
|
|
80
|
-
{renderStatus &&
|
|
81
|
-
(renderState === "rendering" ||
|
|
82
|
-
renderState === "complete" ||
|
|
83
|
-
renderState === "error") && (
|
|
84
|
-
<div className="bg-neutral-950 border-t border-neutral-800 px-4 py-2 flex items-center justify-end gap-2 flex-shrink-0">
|
|
85
|
-
{renderState === "rendering" && (
|
|
86
|
-
<div className="flex-1">
|
|
87
|
-
<div className="flex items-center gap-2">
|
|
88
|
-
<div className="flex-1 h-1.5 bg-neutral-800 rounded-full overflow-hidden">
|
|
89
|
-
<div
|
|
90
|
-
className="h-full bg-blue-500 rounded-full transition-[width] duration-200"
|
|
91
|
-
style={{ width: `${renderStatus.progress ?? 0}%` }}
|
|
92
|
-
/>
|
|
93
|
-
</div>
|
|
94
|
-
<span className="text-xs text-neutral-400 flex-shrink-0">
|
|
95
|
-
{renderStatus.stage || "Rendering..."}
|
|
96
|
-
</span>
|
|
97
|
-
</div>
|
|
98
|
-
</div>
|
|
99
|
-
)}
|
|
100
|
-
{renderState === "complete" && (
|
|
101
|
-
<div className="flex items-center gap-1.5 text-xs text-green-400">
|
|
102
|
-
<svg
|
|
103
|
-
width="14"
|
|
104
|
-
height="14"
|
|
105
|
-
viewBox="0 0 24 24"
|
|
106
|
-
fill="none"
|
|
107
|
-
stroke="currentColor"
|
|
108
|
-
strokeWidth="2"
|
|
109
|
-
strokeLinecap="round"
|
|
110
|
-
strokeLinejoin="round"
|
|
111
|
-
aria-hidden="true"
|
|
112
|
-
>
|
|
113
|
-
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
|
114
|
-
<polyline points="22 4 12 14.01 9 11.01" />
|
|
115
|
-
</svg>
|
|
116
|
-
<span>Complete</span>
|
|
117
|
-
</div>
|
|
118
|
-
)}
|
|
119
|
-
{renderState === "error" && (
|
|
120
|
-
<div className="flex items-center gap-2 text-xs text-red-400">
|
|
121
|
-
<svg
|
|
122
|
-
width="12"
|
|
123
|
-
height="12"
|
|
124
|
-
viewBox="0 0 24 24"
|
|
125
|
-
fill="none"
|
|
126
|
-
stroke="currentColor"
|
|
127
|
-
strokeWidth="2"
|
|
128
|
-
strokeLinecap="round"
|
|
129
|
-
strokeLinejoin="round"
|
|
130
|
-
aria-hidden="true"
|
|
131
|
-
>
|
|
132
|
-
<circle cx="12" cy="12" r="10" />
|
|
133
|
-
<line x1="12" y1="8" x2="12" y2="12" />
|
|
134
|
-
<line x1="12" y1="16" x2="12.01" y2="16" />
|
|
135
|
-
</svg>
|
|
136
|
-
<span className="truncate">{renderStatus.error}</span>
|
|
137
|
-
{renderStatus.onRender && (
|
|
138
|
-
<button
|
|
139
|
-
type="button"
|
|
140
|
-
onClick={renderStatus.onRender}
|
|
141
|
-
className="flex-shrink-0 px-2 py-0.5 text-xs text-neutral-300 hover:text-white hover:bg-neutral-800 rounded transition-colors"
|
|
142
|
-
>
|
|
143
|
-
Retry
|
|
144
|
-
</button>
|
|
145
|
-
)}
|
|
146
|
-
</div>
|
|
147
|
-
)}
|
|
148
|
-
</div>
|
|
149
|
-
)}
|
|
150
|
-
|
|
151
|
-
{/* Optional custom slot */}
|
|
152
|
-
{children}
|
|
153
|
-
</>
|
|
154
|
-
) : (
|
|
155
|
-
<div className="flex items-center justify-center w-full min-w-0">
|
|
156
|
-
<div className="text-center w-full">
|
|
157
|
-
<div className="w-16 h-16 mx-auto mb-4 rounded-card bg-neutral-900 flex items-center justify-center">
|
|
158
|
-
<svg
|
|
159
|
-
width="24"
|
|
160
|
-
height="24"
|
|
161
|
-
viewBox="0 0 24 24"
|
|
162
|
-
fill="none"
|
|
163
|
-
stroke="currentColor"
|
|
164
|
-
strokeWidth="1.5"
|
|
165
|
-
strokeLinecap="round"
|
|
166
|
-
strokeLinejoin="round"
|
|
167
|
-
className="text-neutral-600"
|
|
168
|
-
>
|
|
169
|
-
<polygon points="5 3 19 12 5 21 5 3" />
|
|
170
|
-
</svg>
|
|
171
|
-
</div>
|
|
172
|
-
<p className="text-sm text-neutral-600">Preview will appear here</p>
|
|
173
|
-
<p className="text-xs text-neutral-700 mt-1">
|
|
174
|
-
Send a message to generate a video composition
|
|
175
|
-
</p>
|
|
176
|
-
</div>
|
|
177
|
-
</div>
|
|
178
|
-
)}
|
|
179
|
-
</div>
|
|
180
|
-
);
|
|
181
|
-
}
|