@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.
Files changed (31) hide show
  1. package/dist/assets/index-CLmYRLY-.css +1 -0
  2. package/dist/assets/index-CRvFpc0E.js +84 -0
  3. package/dist/index.html +2 -2
  4. package/package.json +2 -2
  5. package/src/App.tsx +139 -657
  6. package/src/components/LintModal.tsx +149 -0
  7. package/src/components/MediaPreview.tsx +79 -0
  8. package/src/components/editor/FileTree.tsx +50 -40
  9. package/src/components/editor/PropertyPanel.tsx +3 -3
  10. package/src/components/nle/NLELayout.tsx +59 -43
  11. package/src/components/renders/RenderQueue.tsx +19 -16
  12. package/src/components/renders/RenderQueueItem.tsx +13 -8
  13. package/src/components/sidebar/AssetsTab.tsx +34 -144
  14. package/src/components/sidebar/CompositionsTab.tsx +47 -161
  15. package/src/components/sidebar/LeftSidebar.tsx +79 -8
  16. package/src/components/ui/VideoFrameThumbnail.tsx +1 -5
  17. package/src/index.ts +0 -3
  18. package/src/player/components/CompositionThumbnail.tsx +20 -94
  19. package/src/player/components/EditModal.tsx +5 -5
  20. package/src/player/components/PlayerControls.tsx +56 -3
  21. package/src/player/components/Timeline.tsx +13 -17
  22. package/src/player/components/TimelineClip.tsx +0 -1
  23. package/src/player/index.ts +0 -1
  24. package/src/player/store/playerStore.ts +3 -28
  25. package/src/utils/mediaTypes.ts +9 -0
  26. package/dist/assets/index-2uBPlHR_.css +0 -1
  27. package/dist/assets/index-uQ8cgxb3.js +0 -92
  28. package/src/components/ui/ExpandOnHover.tsx +0 -194
  29. package/src/components/ui/ExpandedVideoPreview.tsx +0 -37
  30. package/src/hooks/useCodeEditor.ts +0 -88
  31. 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
- }