@rendiv/studio 0.1.2 → 0.1.4
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/apply-overrides.d.ts +32 -0
- package/dist/apply-overrides.d.ts.map +1 -0
- package/dist/apply-overrides.js +388 -0
- package/dist/apply-overrides.js.map +1 -0
- package/dist/vite-plugin-studio.d.ts.map +1 -1
- package/dist/vite-plugin-studio.js +179 -0
- package/dist/vite-plugin-studio.js.map +1 -1
- package/package.json +6 -3
- package/ui/Preview.tsx +1 -0
- package/ui/RenderQueue.tsx +15 -55
- package/ui/StudioApp.tsx +368 -32
- package/ui/Terminal.tsx +266 -0
- package/ui/TimelineEditor.tsx +573 -0
- package/ui/TopBar.tsx +9 -9
- package/ui/timeline/track-layout.ts +94 -0
- package/ui/timeline/types.ts +39 -0
- package/ui/timeline/use-timeline-drag.ts +98 -0
- package/ui/timeline/use-timeline-zoom.ts +65 -0
package/ui/StudioApp.tsx
CHANGED
|
@@ -5,20 +5,50 @@ import {
|
|
|
5
5
|
CompositionManagerContext,
|
|
6
6
|
type CompositionEntry,
|
|
7
7
|
type TimelineEntry,
|
|
8
|
+
type TimelineOverride,
|
|
8
9
|
} from '@rendiv/core';
|
|
9
10
|
import { Sidebar } from './Sidebar';
|
|
10
11
|
import { Preview } from './Preview';
|
|
11
12
|
import { TopBar } from './TopBar';
|
|
12
13
|
import { Timeline } from './Timeline';
|
|
14
|
+
import { TimelineEditor } from './TimelineEditor';
|
|
13
15
|
import { RenderQueue, type RenderJob } from './RenderQueue';
|
|
14
|
-
import {
|
|
16
|
+
import { Terminal } from './Terminal';
|
|
17
|
+
import { layoutStyles, scrollbarCSS, colors, fonts } from './styles';
|
|
15
18
|
|
|
16
19
|
// Read the entry point from the generated code's data attribute (set by studio-entry-code)
|
|
17
20
|
const ENTRY_POINT = (window as Record<string, unknown>).__RENDIV_STUDIO_ENTRY__ as string ?? 'src/index.tsx';
|
|
18
21
|
|
|
22
|
+
const ViewToggle: React.FC<{ view: 'editor' | 'tree'; onChange: (v: 'editor' | 'tree') => void }> = ({ view, onChange }) => (
|
|
23
|
+
<div style={{ display: 'flex', gap: 2, padding: '2px', backgroundColor: '#0d1117', borderRadius: 6 }}>
|
|
24
|
+
{(['editor', 'tree'] as const).map((v) => (
|
|
25
|
+
<button
|
|
26
|
+
key={v}
|
|
27
|
+
onClick={() => onChange(v)}
|
|
28
|
+
style={{
|
|
29
|
+
padding: '3px 10px',
|
|
30
|
+
fontSize: 11,
|
|
31
|
+
fontWeight: 500,
|
|
32
|
+
border: 'none',
|
|
33
|
+
borderRadius: 4,
|
|
34
|
+
cursor: 'pointer',
|
|
35
|
+
backgroundColor: view === v ? '#30363d' : 'transparent',
|
|
36
|
+
color: view === v ? '#e6edf3' : '#8b949e',
|
|
37
|
+
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
{v === 'editor' ? 'Tracks' : 'Tree'}
|
|
41
|
+
</button>
|
|
42
|
+
))}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
|
|
19
46
|
const StudioApp: React.FC = () => {
|
|
20
47
|
const [compositions, setCompositions] = useState<CompositionEntry[]>([]);
|
|
21
|
-
const [selectedId, setSelectedId] = useState<string | null>(
|
|
48
|
+
const [selectedId, setSelectedId] = useState<string | null>(() => {
|
|
49
|
+
const hash = window.location.hash.slice(1);
|
|
50
|
+
return hash || null;
|
|
51
|
+
});
|
|
22
52
|
const [inputProps, setInputProps] = useState<Record<string, unknown>>({});
|
|
23
53
|
const [playbackRate, setPlaybackRate] = useState(1);
|
|
24
54
|
const [currentFrame, setCurrentFrame] = useState(0);
|
|
@@ -32,11 +62,34 @@ const StudioApp: React.FC = () => {
|
|
|
32
62
|
const dragStartY = useRef(0);
|
|
33
63
|
const dragStartHeight = useRef(0);
|
|
34
64
|
|
|
65
|
+
// Timeline override state
|
|
66
|
+
const [overrides, setOverrides] = useState<Map<string, TimelineOverride>>(new Map());
|
|
67
|
+
const [timelineView, setTimelineView] = useState<'editor' | 'tree'>(() => {
|
|
68
|
+
return (localStorage.getItem('rendiv-studio:timeline-view') as 'editor' | 'tree') || 'editor';
|
|
69
|
+
});
|
|
70
|
+
const saveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
71
|
+
const selectedIdRef = useRef(selectedId);
|
|
72
|
+
selectedIdRef.current = selectedId;
|
|
73
|
+
const pruneTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
74
|
+
|
|
35
75
|
// Render queue state (server-driven)
|
|
36
76
|
const [renderJobs, setRenderJobs] = useState<RenderJob[]>([]);
|
|
37
|
-
const [queueOpen, setQueueOpen] = useState(false);
|
|
38
77
|
const hasActiveRef = useRef(false);
|
|
39
78
|
|
|
79
|
+
// Right panel state — tabbed panel for Queue and Agent
|
|
80
|
+
const [rightPanel, setRightPanel] = useState<'queue' | 'agent' | null>(() => {
|
|
81
|
+
const stored = localStorage.getItem('rendiv-studio:right-panel');
|
|
82
|
+
if (stored === 'queue' || stored === 'agent') return stored;
|
|
83
|
+
return null;
|
|
84
|
+
});
|
|
85
|
+
const [rightPanelWidth, setRightPanelWidth] = useState(() => {
|
|
86
|
+
const stored = localStorage.getItem('rendiv-studio:right-panel-width');
|
|
87
|
+
return stored ? Number(stored) : 360;
|
|
88
|
+
});
|
|
89
|
+
const isDraggingPanel = useRef(false);
|
|
90
|
+
const panelDragStartX = useRef(0);
|
|
91
|
+
const panelDragStartWidth = useRef(0);
|
|
92
|
+
|
|
40
93
|
const handleResizeMouseDown = useCallback((e: React.MouseEvent) => {
|
|
41
94
|
e.preventDefault();
|
|
42
95
|
isDraggingTimeline.current = true;
|
|
@@ -44,11 +97,23 @@ const StudioApp: React.FC = () => {
|
|
|
44
97
|
dragStartHeight.current = timelineHeight;
|
|
45
98
|
}, [timelineHeight]);
|
|
46
99
|
|
|
100
|
+
const handlePanelResizeMouseDown = useCallback((e: React.MouseEvent) => {
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
isDraggingPanel.current = true;
|
|
103
|
+
panelDragStartX.current = e.clientX;
|
|
104
|
+
panelDragStartWidth.current = rightPanelWidth;
|
|
105
|
+
}, [rightPanelWidth]);
|
|
106
|
+
|
|
47
107
|
useEffect(() => {
|
|
48
108
|
const handleMouseMove = (e: MouseEvent) => {
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
109
|
+
if (isDraggingTimeline.current) {
|
|
110
|
+
const delta = dragStartY.current - e.clientY;
|
|
111
|
+
setTimelineHeight(Math.max(120, dragStartHeight.current + delta));
|
|
112
|
+
}
|
|
113
|
+
if (isDraggingPanel.current) {
|
|
114
|
+
const delta = panelDragStartX.current - e.clientX;
|
|
115
|
+
setRightPanelWidth(Math.max(280, Math.min(800, panelDragStartWidth.current + delta)));
|
|
116
|
+
}
|
|
52
117
|
};
|
|
53
118
|
const handleMouseUp = () => {
|
|
54
119
|
if (isDraggingTimeline.current) {
|
|
@@ -58,6 +123,13 @@ const StudioApp: React.FC = () => {
|
|
|
58
123
|
return h;
|
|
59
124
|
});
|
|
60
125
|
}
|
|
126
|
+
if (isDraggingPanel.current) {
|
|
127
|
+
isDraggingPanel.current = false;
|
|
128
|
+
setRightPanelWidth((w) => {
|
|
129
|
+
localStorage.setItem('rendiv-studio:right-panel-width', String(w));
|
|
130
|
+
return w;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
61
133
|
};
|
|
62
134
|
window.addEventListener('mousemove', handleMouseMove);
|
|
63
135
|
window.addEventListener('mouseup', handleMouseUp);
|
|
@@ -67,17 +139,137 @@ const StudioApp: React.FC = () => {
|
|
|
67
139
|
};
|
|
68
140
|
}, []);
|
|
69
141
|
|
|
142
|
+
// --- Timeline overrides ---
|
|
143
|
+
|
|
144
|
+
// Fetch overrides from server on mount, populate global Map
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
fetch('/__rendiv_api__/timeline/overrides')
|
|
147
|
+
.then((res) => res.ok ? res.json() : { overrides: {} })
|
|
148
|
+
.then((data: { overrides: Record<string, TimelineOverride> }) => {
|
|
149
|
+
const map = new Map<string, TimelineOverride>(Object.entries(data.overrides));
|
|
150
|
+
(window as unknown as Record<string, unknown>).__RENDIV_TIMELINE_OVERRIDES__ = map;
|
|
151
|
+
setOverrides(map);
|
|
152
|
+
// Force re-render of composition tree
|
|
153
|
+
seekRef.current?.(currentFrame);
|
|
154
|
+
})
|
|
155
|
+
.catch(() => {});
|
|
156
|
+
}, []);
|
|
157
|
+
|
|
158
|
+
// Listen for external file changes pushed from the server via WebSocket
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
161
|
+
const hot = (import.meta as any).hot as { on: (event: string, cb: (...args: any[]) => void) => void; off?: (event: string, cb: (...args: any[]) => void) => void } | undefined;
|
|
162
|
+
if (!hot) return;
|
|
163
|
+
|
|
164
|
+
const handler = (data: { overrides: Record<string, TimelineOverride> }) => {
|
|
165
|
+
const map = new Map<string, TimelineOverride>(Object.entries(data.overrides));
|
|
166
|
+
(window as unknown as Record<string, unknown>).__RENDIV_TIMELINE_OVERRIDES__ = map;
|
|
167
|
+
setOverrides(map);
|
|
168
|
+
seekRef.current?.(currentFrame);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
hot.on('rendiv:overrides-update', handler);
|
|
172
|
+
return () => {
|
|
173
|
+
hot.off?.('rendiv:overrides-update', handler);
|
|
174
|
+
};
|
|
175
|
+
}, [currentFrame]);
|
|
176
|
+
|
|
177
|
+
// Debounced save to server
|
|
178
|
+
const saveOverridesToServer = useCallback((map: Map<string, TimelineOverride>) => {
|
|
179
|
+
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
|
|
180
|
+
saveTimerRef.current = setTimeout(() => {
|
|
181
|
+
const obj: Record<string, TimelineOverride> = {};
|
|
182
|
+
map.forEach((v, k) => { obj[k] = v; });
|
|
183
|
+
fetch('/__rendiv_api__/timeline/overrides', {
|
|
184
|
+
method: 'PUT',
|
|
185
|
+
headers: { 'Content-Type': 'application/json' },
|
|
186
|
+
body: JSON.stringify({ overrides: obj }),
|
|
187
|
+
}).catch(() => {});
|
|
188
|
+
}, 300);
|
|
189
|
+
}, []);
|
|
190
|
+
const saveOverridesToServerRef = useRef(saveOverridesToServer);
|
|
191
|
+
saveOverridesToServerRef.current = saveOverridesToServer;
|
|
192
|
+
|
|
193
|
+
const handleOverrideChange = useCallback((namePath: string, override: TimelineOverride) => {
|
|
194
|
+
const w = window as unknown as Record<string, unknown>;
|
|
195
|
+
let map = w.__RENDIV_TIMELINE_OVERRIDES__ as Map<string, TimelineOverride> | undefined;
|
|
196
|
+
if (!map) {
|
|
197
|
+
map = new Map();
|
|
198
|
+
w.__RENDIV_TIMELINE_OVERRIDES__ = map;
|
|
199
|
+
}
|
|
200
|
+
map.set(namePath, override);
|
|
201
|
+
setOverrides(new Map(map));
|
|
202
|
+
seekRef.current?.(currentFrame);
|
|
203
|
+
saveOverridesToServer(map);
|
|
204
|
+
}, [currentFrame, saveOverridesToServer]);
|
|
205
|
+
|
|
206
|
+
const handleOverrideRemove = useCallback((namePath: string) => {
|
|
207
|
+
const w = window as unknown as Record<string, unknown>;
|
|
208
|
+
const map = w.__RENDIV_TIMELINE_OVERRIDES__ as Map<string, TimelineOverride> | undefined;
|
|
209
|
+
if (map) {
|
|
210
|
+
map.delete(namePath);
|
|
211
|
+
setOverrides(new Map(map));
|
|
212
|
+
seekRef.current?.(currentFrame);
|
|
213
|
+
saveOverridesToServer(map);
|
|
214
|
+
}
|
|
215
|
+
}, [currentFrame, saveOverridesToServer]);
|
|
216
|
+
|
|
217
|
+
const handleOverridesClear = useCallback(() => {
|
|
218
|
+
const w = window as unknown as Record<string, unknown>;
|
|
219
|
+
const map = w.__RENDIV_TIMELINE_OVERRIDES__ as Map<string, TimelineOverride> | undefined;
|
|
220
|
+
if (map) map.clear();
|
|
221
|
+
setOverrides(new Map());
|
|
222
|
+
seekRef.current?.(currentFrame);
|
|
223
|
+
fetch('/__rendiv_api__/timeline/overrides', { method: 'DELETE' }).catch(() => {});
|
|
224
|
+
}, [currentFrame]);
|
|
225
|
+
|
|
226
|
+
const handleTimelineViewChange = useCallback((view: 'editor' | 'tree') => {
|
|
227
|
+
setTimelineView(view);
|
|
228
|
+
localStorage.setItem('rendiv-studio:timeline-view', view);
|
|
229
|
+
}, []);
|
|
230
|
+
|
|
70
231
|
// Timeline registry: reads from a shared global Map + listens for sync events.
|
|
232
|
+
// Orphan pruning is debounced to avoid reacting to transient states — when a
|
|
233
|
+
// Sequence effect re-runs (e.g. after an override changes absoluteFrom), React
|
|
234
|
+
// runs the cleanup (which deletes the entry and dispatches sync) before the new
|
|
235
|
+
// effect re-registers it. Without debouncing, the pruning would see the entry
|
|
236
|
+
// as missing during this gap and delete the override.
|
|
71
237
|
useEffect(() => {
|
|
72
238
|
const readEntries = () => {
|
|
73
239
|
const w = window as unknown as Record<string, unknown>;
|
|
74
240
|
const entries = w.__RENDIV_TIMELINE_ENTRIES__ as Map<string, TimelineEntry> | undefined;
|
|
75
|
-
|
|
241
|
+
const list = entries ? Array.from(entries.values()) : [];
|
|
242
|
+
setTimelineEntries(list);
|
|
243
|
+
|
|
244
|
+
// Debounced orphan pruning — wait for effects to settle
|
|
245
|
+
if (pruneTimerRef.current) clearTimeout(pruneTimerRef.current);
|
|
246
|
+
pruneTimerRef.current = setTimeout(() => {
|
|
247
|
+
const currentSelectedId = selectedIdRef.current;
|
|
248
|
+
const overrideMap = w.__RENDIV_TIMELINE_OVERRIDES__ as Map<string, TimelineOverride> | undefined;
|
|
249
|
+
const currentEntries = w.__RENDIV_TIMELINE_ENTRIES__ as Map<string, TimelineEntry> | undefined;
|
|
250
|
+
const currentList = currentEntries ? Array.from(currentEntries.values()) : [];
|
|
251
|
+
if (overrideMap && overrideMap.size > 0 && currentSelectedId) {
|
|
252
|
+
const prefix = `${currentSelectedId}/`;
|
|
253
|
+
const activeNamePaths = new Set(currentList.map((e) => e.namePath));
|
|
254
|
+
let pruned = false;
|
|
255
|
+
for (const key of overrideMap.keys()) {
|
|
256
|
+
if (key.startsWith(prefix) && !activeNamePaths.has(key)) {
|
|
257
|
+
overrideMap.delete(key);
|
|
258
|
+
pruned = true;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (pruned) {
|
|
262
|
+
setOverrides(new Map(overrideMap));
|
|
263
|
+
saveOverridesToServerRef.current(overrideMap);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}, 500);
|
|
76
267
|
};
|
|
77
268
|
readEntries();
|
|
78
269
|
document.addEventListener('rendiv:timeline-sync', readEntries);
|
|
79
270
|
return () => {
|
|
80
271
|
document.removeEventListener('rendiv:timeline-sync', readEntries);
|
|
272
|
+
if (pruneTimerRef.current) clearTimeout(pruneTimerRef.current);
|
|
81
273
|
};
|
|
82
274
|
}, []);
|
|
83
275
|
|
|
@@ -113,13 +305,31 @@ const StudioApp: React.FC = () => {
|
|
|
113
305
|
[compositions, registerComposition, unregisterComposition, selectedId, inputProps],
|
|
114
306
|
);
|
|
115
307
|
|
|
116
|
-
// Auto-select
|
|
308
|
+
// Auto-select composition from URL hash, or fall back to first
|
|
117
309
|
useEffect(() => {
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
310
|
+
if (compositions.length === 0) return;
|
|
311
|
+
if (selectedId !== null && compositions.some((c: CompositionEntry) => c.id === selectedId)) return;
|
|
312
|
+
// Hash composition no longer exists or no selection — fall back to first
|
|
313
|
+
setSelectedId(compositions[0].id);
|
|
121
314
|
}, [compositions, selectedId]);
|
|
122
315
|
|
|
316
|
+
// Sync URL hash when selection changes
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
if (selectedId) {
|
|
319
|
+
window.location.hash = selectedId;
|
|
320
|
+
}
|
|
321
|
+
}, [selectedId]);
|
|
322
|
+
|
|
323
|
+
// Listen for browser back/forward navigation
|
|
324
|
+
useEffect(() => {
|
|
325
|
+
const handleHashChange = () => {
|
|
326
|
+
const hash = window.location.hash.slice(1);
|
|
327
|
+
if (hash) setSelectedId(hash);
|
|
328
|
+
};
|
|
329
|
+
window.addEventListener('hashchange', handleHashChange);
|
|
330
|
+
return () => window.removeEventListener('hashchange', handleHashChange);
|
|
331
|
+
}, []);
|
|
332
|
+
|
|
123
333
|
// Reset input props when switching compositions
|
|
124
334
|
useEffect(() => {
|
|
125
335
|
setInputProps({});
|
|
@@ -174,7 +384,8 @@ const StudioApp: React.FC = () => {
|
|
|
174
384
|
totalFrames: selectedComposition.durationInFrames,
|
|
175
385
|
}),
|
|
176
386
|
});
|
|
177
|
-
|
|
387
|
+
setRightPanel('queue');
|
|
388
|
+
localStorage.setItem('rendiv-studio:right-panel-tab', 'queue');
|
|
178
389
|
}, [selectedComposition, inputProps]);
|
|
179
390
|
|
|
180
391
|
const handleCancelJob = useCallback((jobId: string) => {
|
|
@@ -189,8 +400,18 @@ const StudioApp: React.FC = () => {
|
|
|
189
400
|
fetch('/__rendiv_api__/render/queue/clear', { method: 'POST' });
|
|
190
401
|
}, []);
|
|
191
402
|
|
|
192
|
-
const
|
|
193
|
-
|
|
403
|
+
const handleTogglePanel = useCallback(() => {
|
|
404
|
+
setRightPanel((prev) => {
|
|
405
|
+
if (prev !== null) {
|
|
406
|
+
// Close panel
|
|
407
|
+
localStorage.setItem('rendiv-studio:right-panel', '');
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
// Open to last active tab (default 'queue')
|
|
411
|
+
const tab = (localStorage.getItem('rendiv-studio:right-panel-tab') as 'queue' | 'agent') || 'queue';
|
|
412
|
+
localStorage.setItem('rendiv-studio:right-panel', tab);
|
|
413
|
+
return tab;
|
|
414
|
+
});
|
|
194
415
|
}, []);
|
|
195
416
|
|
|
196
417
|
const queueCount = renderJobs.filter((j) =>
|
|
@@ -207,8 +428,8 @@ const StudioApp: React.FC = () => {
|
|
|
207
428
|
entryPoint={ENTRY_POINT}
|
|
208
429
|
onRender={handleAddRender}
|
|
209
430
|
queueCount={queueCount}
|
|
210
|
-
|
|
211
|
-
|
|
431
|
+
panelOpen={rightPanel !== null}
|
|
432
|
+
onTogglePanel={handleTogglePanel}
|
|
212
433
|
/>
|
|
213
434
|
|
|
214
435
|
<div style={layoutStyles.body}>
|
|
@@ -234,14 +455,67 @@ const StudioApp: React.FC = () => {
|
|
|
234
455
|
</div>
|
|
235
456
|
)}
|
|
236
457
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
458
|
+
{/* Right panel — tabbed: Queue / Agent */}
|
|
459
|
+
{rightPanel !== null && (
|
|
460
|
+
<div style={{ ...rightPanelStyle, width: rightPanelWidth, minWidth: 280 }}>
|
|
461
|
+
<div
|
|
462
|
+
style={panelResizeHandleStyle}
|
|
463
|
+
onMouseDown={handlePanelResizeMouseDown}
|
|
464
|
+
/>
|
|
465
|
+
<div style={tabBarStyle}>
|
|
466
|
+
<div style={{ display: 'flex', gap: 2, padding: 2, backgroundColor: colors.bg, borderRadius: 6 }}>
|
|
467
|
+
{(['queue', 'agent'] as const).map((tab) => (
|
|
468
|
+
<button
|
|
469
|
+
key={tab}
|
|
470
|
+
onClick={() => {
|
|
471
|
+
setRightPanel(tab);
|
|
472
|
+
localStorage.setItem('rendiv-studio:right-panel', tab);
|
|
473
|
+
localStorage.setItem('rendiv-studio:right-panel-tab', tab);
|
|
474
|
+
}}
|
|
475
|
+
style={{
|
|
476
|
+
padding: '3px 10px',
|
|
477
|
+
fontSize: 11,
|
|
478
|
+
fontWeight: 500,
|
|
479
|
+
border: 'none',
|
|
480
|
+
borderRadius: 4,
|
|
481
|
+
cursor: 'pointer',
|
|
482
|
+
backgroundColor: rightPanel === tab ? colors.border : 'transparent',
|
|
483
|
+
color: rightPanel === tab ? colors.textPrimary : colors.textSecondary,
|
|
484
|
+
fontFamily: fonts.sans,
|
|
485
|
+
}}
|
|
486
|
+
>
|
|
487
|
+
{tab === 'queue' ? 'Queue' : 'Agent'}
|
|
488
|
+
{tab === 'queue' && queueCount > 0 && (
|
|
489
|
+
<span style={{ marginLeft: 4, color: colors.accent, fontWeight: 600 }}>({queueCount})</span>
|
|
490
|
+
)}
|
|
491
|
+
</button>
|
|
492
|
+
))}
|
|
493
|
+
</div>
|
|
494
|
+
<button
|
|
495
|
+
type="button"
|
|
496
|
+
onClick={() => {
|
|
497
|
+
setRightPanel(null);
|
|
498
|
+
localStorage.setItem('rendiv-studio:right-panel', '');
|
|
499
|
+
}}
|
|
500
|
+
style={tabCloseStyle}
|
|
501
|
+
title="Close panel"
|
|
502
|
+
>
|
|
503
|
+
{'\u2715'}
|
|
504
|
+
</button>
|
|
505
|
+
</div>
|
|
506
|
+
<div style={{ display: rightPanel === 'queue' ? 'flex' : 'none', flex: 1, flexDirection: 'column' as const, overflow: 'hidden' }}>
|
|
507
|
+
<RenderQueue
|
|
508
|
+
jobs={renderJobs}
|
|
509
|
+
onCancel={handleCancelJob}
|
|
510
|
+
onRemove={handleRemoveJob}
|
|
511
|
+
onClear={handleClearFinished}
|
|
512
|
+
/>
|
|
513
|
+
</div>
|
|
514
|
+
<div style={{ display: rightPanel === 'agent' ? 'flex' : 'none', flex: 1, flexDirection: 'column' as const, overflow: 'hidden' }}>
|
|
515
|
+
<Terminal open={rightPanel === 'agent'} />
|
|
516
|
+
</div>
|
|
517
|
+
</div>
|
|
518
|
+
)}
|
|
245
519
|
</div>
|
|
246
520
|
|
|
247
521
|
{/* Timeline — full-width resizable row */}
|
|
@@ -252,14 +526,38 @@ const StudioApp: React.FC = () => {
|
|
|
252
526
|
onMouseDown={handleResizeMouseDown}
|
|
253
527
|
/>
|
|
254
528
|
<div style={{ flex: 1, overflow: 'auto' }}>
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
529
|
+
{timelineView === 'editor' ? (
|
|
530
|
+
<TimelineEditor
|
|
531
|
+
entries={timelineEntries}
|
|
532
|
+
currentFrame={currentFrame}
|
|
533
|
+
totalFrames={selectedComposition.durationInFrames}
|
|
534
|
+
fps={selectedComposition.fps}
|
|
535
|
+
compositionName={selectedComposition.id}
|
|
536
|
+
onSeek={handleTimelineSeek}
|
|
537
|
+
overrides={overrides}
|
|
538
|
+
onOverrideChange={handleOverrideChange}
|
|
539
|
+
onOverrideRemove={handleOverrideRemove}
|
|
540
|
+
onOverridesClear={handleOverridesClear}
|
|
541
|
+
view={timelineView}
|
|
542
|
+
onViewChange={handleTimelineViewChange}
|
|
543
|
+
/>
|
|
544
|
+
) : (
|
|
545
|
+
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
|
546
|
+
<div style={{ display: 'flex', justifyContent: 'flex-end', padding: '4px 8px', backgroundColor: '#161b22', borderBottom: '1px solid #30363d' }}>
|
|
547
|
+
<ViewToggle view={timelineView} onChange={handleTimelineViewChange} />
|
|
548
|
+
</div>
|
|
549
|
+
<div style={{ flex: 1, overflow: 'auto' }}>
|
|
550
|
+
<Timeline
|
|
551
|
+
entries={timelineEntries}
|
|
552
|
+
currentFrame={currentFrame}
|
|
553
|
+
totalFrames={selectedComposition.durationInFrames}
|
|
554
|
+
fps={selectedComposition.fps}
|
|
555
|
+
onSeek={handleTimelineSeek}
|
|
556
|
+
compositionName={selectedComposition.id}
|
|
557
|
+
/>
|
|
558
|
+
</div>
|
|
559
|
+
</div>
|
|
560
|
+
)}
|
|
263
561
|
</div>
|
|
264
562
|
</div>
|
|
265
563
|
)}
|
|
@@ -274,6 +572,44 @@ const StudioApp: React.FC = () => {
|
|
|
274
572
|
);
|
|
275
573
|
};
|
|
276
574
|
|
|
575
|
+
const rightPanelStyle: React.CSSProperties = {
|
|
576
|
+
position: 'relative',
|
|
577
|
+
height: '100%',
|
|
578
|
+
backgroundColor: colors.surface,
|
|
579
|
+
borderLeft: `1px solid ${colors.border}`,
|
|
580
|
+
display: 'flex',
|
|
581
|
+
flexDirection: 'column',
|
|
582
|
+
flexShrink: 0,
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
const panelResizeHandleStyle: React.CSSProperties = {
|
|
586
|
+
position: 'absolute',
|
|
587
|
+
top: 0,
|
|
588
|
+
left: 0,
|
|
589
|
+
width: 4,
|
|
590
|
+
height: '100%',
|
|
591
|
+
cursor: 'col-resize',
|
|
592
|
+
zIndex: 10,
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
const tabBarStyle: React.CSSProperties = {
|
|
596
|
+
display: 'flex',
|
|
597
|
+
justifyContent: 'space-between',
|
|
598
|
+
alignItems: 'center',
|
|
599
|
+
padding: '6px 8px',
|
|
600
|
+
borderBottom: `1px solid ${colors.border}`,
|
|
601
|
+
flexShrink: 0,
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
const tabCloseStyle: React.CSSProperties = {
|
|
605
|
+
background: 'none',
|
|
606
|
+
border: 'none',
|
|
607
|
+
color: colors.textSecondary,
|
|
608
|
+
cursor: 'pointer',
|
|
609
|
+
fontSize: 12,
|
|
610
|
+
padding: '2px 4px',
|
|
611
|
+
};
|
|
612
|
+
|
|
277
613
|
export function createStudioApp(container: HTMLElement | null): void {
|
|
278
614
|
if (!container) {
|
|
279
615
|
throw new Error('Rendiv Studio: Could not find #root element');
|