@rendiv/studio 0.1.4 → 0.1.5
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/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/scaffold-project.d.ts +10 -0
- package/dist/scaffold-project.d.ts.map +1 -0
- package/dist/scaffold-project.js +181 -0
- package/dist/scaffold-project.js.map +1 -0
- package/dist/start-studio-workspace.d.ts +16 -0
- package/dist/start-studio-workspace.d.ts.map +1 -0
- package/dist/start-studio-workspace.js +110 -0
- package/dist/start-studio-workspace.js.map +1 -0
- package/dist/start-studio.d.ts +5 -0
- package/dist/start-studio.d.ts.map +1 -1
- package/dist/start-studio.js +8 -5
- package/dist/start-studio.js.map +1 -1
- package/dist/studio-entry-code.d.ts +7 -2
- package/dist/studio-entry-code.d.ts.map +1 -1
- package/dist/studio-entry-code.js +19 -4
- package/dist/studio-entry-code.js.map +1 -1
- package/dist/vite-plugin-studio.d.ts +4 -0
- package/dist/vite-plugin-studio.d.ts.map +1 -1
- package/dist/vite-plugin-studio.js +37 -28
- package/dist/vite-plugin-studio.js.map +1 -1
- package/dist/workspace-entry-code.d.ts +12 -0
- package/dist/workspace-entry-code.d.ts.map +1 -0
- package/dist/workspace-entry-code.js +38 -0
- package/dist/workspace-entry-code.js.map +1 -0
- package/dist/workspace-picker-server.d.ts +27 -0
- package/dist/workspace-picker-server.d.ts.map +1 -0
- package/dist/workspace-picker-server.js +199 -0
- package/dist/workspace-picker-server.js.map +1 -0
- package/package.json +5 -3
- package/ui/StudioApp.tsx +10 -0
- package/ui/TopBar.tsx +32 -2
- package/ui/WorkspacePicker.tsx +423 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
|
|
4
|
+
// Inline colors/fonts to match Studio theme (same as styles.ts)
|
|
5
|
+
const colors = {
|
|
6
|
+
bg: '#0d1117',
|
|
7
|
+
surface: '#161b22',
|
|
8
|
+
surfaceHover: '#1c2128',
|
|
9
|
+
border: '#30363d',
|
|
10
|
+
textPrimary: '#e6edf3',
|
|
11
|
+
textSecondary: '#8b949e',
|
|
12
|
+
accent: '#58a6ff',
|
|
13
|
+
accentMuted: '#1f6feb',
|
|
14
|
+
badge: '#238636',
|
|
15
|
+
error: '#f85149',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const fonts = {
|
|
19
|
+
sans: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
|
20
|
+
mono: '"SF Mono", "Fira Code", Consolas, "Liberation Mono", Menlo, monospace',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
interface WorkspaceProject {
|
|
24
|
+
name: string;
|
|
25
|
+
path: string;
|
|
26
|
+
hasNodeModules: boolean;
|
|
27
|
+
entryPoint: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const WorkspacePicker: React.FC = () => {
|
|
31
|
+
const [projects, setProjects] = useState<WorkspaceProject[]>([]);
|
|
32
|
+
const [loading, setLoading] = useState(true);
|
|
33
|
+
const [creating, setCreating] = useState(false);
|
|
34
|
+
const [newProjectName, setNewProjectName] = useState('');
|
|
35
|
+
const [createError, setCreateError] = useState('');
|
|
36
|
+
const [createStatus, setCreateStatus] = useState('');
|
|
37
|
+
const [switching, setSwitching] = useState<string | null>(null);
|
|
38
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
39
|
+
|
|
40
|
+
const fetchProjects = useCallback(async () => {
|
|
41
|
+
try {
|
|
42
|
+
const res = await fetch('/__rendiv_api__/workspace/projects');
|
|
43
|
+
if (res.ok) {
|
|
44
|
+
const data = await res.json();
|
|
45
|
+
setProjects(data.projects);
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// retry silently
|
|
49
|
+
} finally {
|
|
50
|
+
setLoading(false);
|
|
51
|
+
}
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
fetchProjects();
|
|
56
|
+
}, [fetchProjects]);
|
|
57
|
+
|
|
58
|
+
const handleOpenProject = useCallback(async (project: WorkspaceProject) => {
|
|
59
|
+
if (!project.hasNodeModules) return;
|
|
60
|
+
setSwitching(project.name);
|
|
61
|
+
try {
|
|
62
|
+
await fetch('/__rendiv_api__/workspace/open', {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: { 'Content-Type': 'application/json' },
|
|
65
|
+
body: JSON.stringify({ path: project.path }),
|
|
66
|
+
});
|
|
67
|
+
// Server will restart, browser will auto-reconnect via Vite HMR
|
|
68
|
+
} catch {
|
|
69
|
+
setSwitching(null);
|
|
70
|
+
}
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
const handleCreateProject = useCallback(async () => {
|
|
74
|
+
const name = newProjectName.trim();
|
|
75
|
+
if (!name) return;
|
|
76
|
+
|
|
77
|
+
setCreateError('');
|
|
78
|
+
setCreateStatus('Creating project...');
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const res = await fetch('/__rendiv_api__/workspace/create', {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: { 'Content-Type': 'application/json' },
|
|
84
|
+
body: JSON.stringify({ name }),
|
|
85
|
+
});
|
|
86
|
+
const data = await res.json();
|
|
87
|
+
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
setCreateError(data.error || 'Failed to create project');
|
|
90
|
+
setCreateStatus('');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
setCreateStatus('');
|
|
95
|
+
setNewProjectName('');
|
|
96
|
+
setCreating(false);
|
|
97
|
+
await fetchProjects();
|
|
98
|
+
} catch (err) {
|
|
99
|
+
setCreateError(err instanceof Error ? err.message : 'Failed to create project');
|
|
100
|
+
setCreateStatus('');
|
|
101
|
+
}
|
|
102
|
+
}, [newProjectName, fetchProjects]);
|
|
103
|
+
|
|
104
|
+
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
105
|
+
if (e.key === 'Enter') handleCreateProject();
|
|
106
|
+
if (e.key === 'Escape') {
|
|
107
|
+
setCreating(false);
|
|
108
|
+
setNewProjectName('');
|
|
109
|
+
setCreateError('');
|
|
110
|
+
setCreateStatus('');
|
|
111
|
+
}
|
|
112
|
+
}, [handleCreateProject]);
|
|
113
|
+
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (creating && inputRef.current) {
|
|
116
|
+
inputRef.current.focus();
|
|
117
|
+
}
|
|
118
|
+
}, [creating]);
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div style={rootStyle}>
|
|
122
|
+
<style dangerouslySetInnerHTML={{ __html: globalCSS }} />
|
|
123
|
+
|
|
124
|
+
{/* Header */}
|
|
125
|
+
<div style={headerStyle}>
|
|
126
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
127
|
+
<svg width="28" height="28" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
128
|
+
<rect x="6" y="8" width="26" height="20" rx="3" stroke={colors.accent} strokeWidth="2" opacity="0.35"/>
|
|
129
|
+
<rect x="12" y="14" width="26" height="20" rx="3" stroke={colors.accent} strokeWidth="2"/>
|
|
130
|
+
<polygon points="12,34 30,14 38,14 38,34" fill={colors.accent} opacity="0.25"/>
|
|
131
|
+
<path d="M22 20L30 24L22 28Z" fill={colors.accent}/>
|
|
132
|
+
</svg>
|
|
133
|
+
<span style={{ fontSize: 18, fontWeight: 600, color: colors.textPrimary }}>Rendiv Studio</span>
|
|
134
|
+
</div>
|
|
135
|
+
<span style={{ fontSize: 13, color: colors.textSecondary, fontFamily: fonts.mono }}>Workspace</span>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
{/* Content */}
|
|
139
|
+
<div style={contentStyle}>
|
|
140
|
+
{loading ? (
|
|
141
|
+
<div style={emptyStateStyle}>
|
|
142
|
+
<span style={{ color: colors.textSecondary }}>Loading projects...</span>
|
|
143
|
+
</div>
|
|
144
|
+
) : (
|
|
145
|
+
<>
|
|
146
|
+
<div style={titleRowStyle}>
|
|
147
|
+
<h2 style={{ fontSize: 16, fontWeight: 600, color: colors.textPrimary, margin: 0 }}>
|
|
148
|
+
Projects
|
|
149
|
+
</h2>
|
|
150
|
+
{!creating && (
|
|
151
|
+
<button
|
|
152
|
+
style={newButtonStyle}
|
|
153
|
+
onClick={() => setCreating(true)}
|
|
154
|
+
>
|
|
155
|
+
+ New Project
|
|
156
|
+
</button>
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
{/* Create project form */}
|
|
161
|
+
{creating && (
|
|
162
|
+
<div style={createFormStyle}>
|
|
163
|
+
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
|
164
|
+
<input
|
|
165
|
+
ref={inputRef}
|
|
166
|
+
type="text"
|
|
167
|
+
placeholder="project-name"
|
|
168
|
+
value={newProjectName}
|
|
169
|
+
onChange={(e) => {
|
|
170
|
+
setNewProjectName(e.target.value);
|
|
171
|
+
setCreateError('');
|
|
172
|
+
}}
|
|
173
|
+
onKeyDown={handleKeyDown}
|
|
174
|
+
style={inputStyle}
|
|
175
|
+
disabled={!!createStatus}
|
|
176
|
+
/>
|
|
177
|
+
<button
|
|
178
|
+
style={{
|
|
179
|
+
...actionButtonStyle,
|
|
180
|
+
opacity: newProjectName.trim() && !createStatus ? 1 : 0.5,
|
|
181
|
+
}}
|
|
182
|
+
onClick={handleCreateProject}
|
|
183
|
+
disabled={!newProjectName.trim() || !!createStatus}
|
|
184
|
+
>
|
|
185
|
+
{createStatus || 'Create'}
|
|
186
|
+
</button>
|
|
187
|
+
<button
|
|
188
|
+
style={cancelButtonStyle}
|
|
189
|
+
onClick={() => {
|
|
190
|
+
setCreating(false);
|
|
191
|
+
setNewProjectName('');
|
|
192
|
+
setCreateError('');
|
|
193
|
+
setCreateStatus('');
|
|
194
|
+
}}
|
|
195
|
+
disabled={!!createStatus}
|
|
196
|
+
>
|
|
197
|
+
Cancel
|
|
198
|
+
</button>
|
|
199
|
+
</div>
|
|
200
|
+
{createError && (
|
|
201
|
+
<div style={{ color: colors.error, fontSize: 12, marginTop: 6 }}>{createError}</div>
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
|
|
206
|
+
{/* Project grid */}
|
|
207
|
+
{projects.length === 0 && !creating ? (
|
|
208
|
+
<div style={emptyStateStyle}>
|
|
209
|
+
<div style={{ textAlign: 'center' }}>
|
|
210
|
+
<div style={{ fontSize: 40, marginBottom: 16, opacity: 0.3 }}>🎬</div>
|
|
211
|
+
<div style={{ color: colors.textSecondary, fontSize: 14, marginBottom: 16 }}>
|
|
212
|
+
No projects yet
|
|
213
|
+
</div>
|
|
214
|
+
<button
|
|
215
|
+
style={newButtonStyle}
|
|
216
|
+
onClick={() => setCreating(true)}
|
|
217
|
+
>
|
|
218
|
+
Create your first project
|
|
219
|
+
</button>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
) : (
|
|
223
|
+
<div style={gridStyle}>
|
|
224
|
+
{projects.map((project) => (
|
|
225
|
+
<div
|
|
226
|
+
key={project.name}
|
|
227
|
+
style={{
|
|
228
|
+
...cardStyle,
|
|
229
|
+
cursor: project.hasNodeModules ? 'pointer' : 'default',
|
|
230
|
+
opacity: switching && switching !== project.name ? 0.5 : 1,
|
|
231
|
+
}}
|
|
232
|
+
onClick={() => handleOpenProject(project)}
|
|
233
|
+
onMouseEnter={(e) => {
|
|
234
|
+
if (project.hasNodeModules) {
|
|
235
|
+
e.currentTarget.style.borderColor = colors.accent;
|
|
236
|
+
e.currentTarget.style.backgroundColor = colors.surfaceHover;
|
|
237
|
+
}
|
|
238
|
+
}}
|
|
239
|
+
onMouseLeave={(e) => {
|
|
240
|
+
e.currentTarget.style.borderColor = colors.border;
|
|
241
|
+
e.currentTarget.style.backgroundColor = colors.surface;
|
|
242
|
+
}}
|
|
243
|
+
>
|
|
244
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
245
|
+
<svg width="20" height="20" viewBox="0 0 16 16" fill={colors.accent}>
|
|
246
|
+
<path d="M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3H7.5a.25.25 0 0 1-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75Z"/>
|
|
247
|
+
</svg>
|
|
248
|
+
<span style={{ fontSize: 14, fontWeight: 600, color: colors.textPrimary }}>
|
|
249
|
+
{project.name}
|
|
250
|
+
</span>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
<div style={{ fontSize: 12, color: colors.textSecondary, fontFamily: fonts.mono, marginTop: 8 }}>
|
|
254
|
+
{project.entryPoint}
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
<div style={{ marginTop: 12, display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
258
|
+
{project.hasNodeModules ? (
|
|
259
|
+
<>
|
|
260
|
+
<div style={{ width: 6, height: 6, borderRadius: 3, backgroundColor: colors.badge }} />
|
|
261
|
+
<span style={{ fontSize: 11, color: colors.textSecondary }}>Ready</span>
|
|
262
|
+
</>
|
|
263
|
+
) : (
|
|
264
|
+
<>
|
|
265
|
+
<div style={{ width: 6, height: 6, borderRadius: 3, backgroundColor: '#d29922' }} />
|
|
266
|
+
<span style={{ fontSize: 11, color: '#d29922' }}>Missing node_modules</span>
|
|
267
|
+
</>
|
|
268
|
+
)}
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
{switching === project.name && (
|
|
272
|
+
<div style={{ marginTop: 8, fontSize: 11, color: colors.accent }}>
|
|
273
|
+
Opening...
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
276
|
+
</div>
|
|
277
|
+
))}
|
|
278
|
+
</div>
|
|
279
|
+
)}
|
|
280
|
+
</>
|
|
281
|
+
)}
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// --- Styles ---
|
|
288
|
+
|
|
289
|
+
const globalCSS = `
|
|
290
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
291
|
+
* {
|
|
292
|
+
scrollbar-width: thin;
|
|
293
|
+
scrollbar-color: ${colors.border} transparent;
|
|
294
|
+
}
|
|
295
|
+
::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
296
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
297
|
+
::-webkit-scrollbar-thumb { background: ${colors.border}; border-radius: 4px; }
|
|
298
|
+
::-webkit-scrollbar-thumb:hover { background: ${colors.textSecondary}; }
|
|
299
|
+
`;
|
|
300
|
+
|
|
301
|
+
const rootStyle: React.CSSProperties = {
|
|
302
|
+
display: 'flex',
|
|
303
|
+
flexDirection: 'column',
|
|
304
|
+
height: '100vh',
|
|
305
|
+
backgroundColor: colors.bg,
|
|
306
|
+
color: colors.textPrimary,
|
|
307
|
+
fontFamily: fonts.sans,
|
|
308
|
+
fontSize: 13,
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const headerStyle: React.CSSProperties = {
|
|
312
|
+
display: 'flex',
|
|
313
|
+
alignItems: 'center',
|
|
314
|
+
justifyContent: 'space-between',
|
|
315
|
+
height: 48,
|
|
316
|
+
padding: '0 24px',
|
|
317
|
+
backgroundColor: colors.surface,
|
|
318
|
+
borderBottom: `1px solid ${colors.border}`,
|
|
319
|
+
flexShrink: 0,
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const contentStyle: React.CSSProperties = {
|
|
323
|
+
flex: 1,
|
|
324
|
+
overflow: 'auto',
|
|
325
|
+
padding: '32px 48px',
|
|
326
|
+
maxWidth: 960,
|
|
327
|
+
margin: '0 auto',
|
|
328
|
+
width: '100%',
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const titleRowStyle: React.CSSProperties = {
|
|
332
|
+
display: 'flex',
|
|
333
|
+
alignItems: 'center',
|
|
334
|
+
justifyContent: 'space-between',
|
|
335
|
+
marginBottom: 20,
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const emptyStateStyle: React.CSSProperties = {
|
|
339
|
+
display: 'flex',
|
|
340
|
+
alignItems: 'center',
|
|
341
|
+
justifyContent: 'center',
|
|
342
|
+
minHeight: 300,
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const gridStyle: React.CSSProperties = {
|
|
346
|
+
display: 'grid',
|
|
347
|
+
gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))',
|
|
348
|
+
gap: 16,
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const cardStyle: React.CSSProperties = {
|
|
352
|
+
padding: 16,
|
|
353
|
+
backgroundColor: colors.surface,
|
|
354
|
+
border: `1px solid ${colors.border}`,
|
|
355
|
+
borderRadius: 8,
|
|
356
|
+
transition: 'border-color 0.15s, background-color 0.15s',
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const newButtonStyle: React.CSSProperties = {
|
|
360
|
+
padding: '8px 16px',
|
|
361
|
+
fontSize: 13,
|
|
362
|
+
fontWeight: 600,
|
|
363
|
+
color: '#fff',
|
|
364
|
+
backgroundColor: colors.accentMuted,
|
|
365
|
+
border: 'none',
|
|
366
|
+
borderRadius: 6,
|
|
367
|
+
cursor: 'pointer',
|
|
368
|
+
fontFamily: fonts.sans,
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const createFormStyle: React.CSSProperties = {
|
|
372
|
+
padding: 16,
|
|
373
|
+
backgroundColor: colors.surface,
|
|
374
|
+
border: `1px solid ${colors.border}`,
|
|
375
|
+
borderRadius: 8,
|
|
376
|
+
marginBottom: 20,
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const inputStyle: React.CSSProperties = {
|
|
380
|
+
flex: 1,
|
|
381
|
+
padding: '8px 12px',
|
|
382
|
+
fontSize: 13,
|
|
383
|
+
fontFamily: fonts.mono,
|
|
384
|
+
backgroundColor: colors.bg,
|
|
385
|
+
color: colors.textPrimary,
|
|
386
|
+
border: `1px solid ${colors.border}`,
|
|
387
|
+
borderRadius: 6,
|
|
388
|
+
outline: 'none',
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const actionButtonStyle: React.CSSProperties = {
|
|
392
|
+
padding: '8px 16px',
|
|
393
|
+
fontSize: 13,
|
|
394
|
+
fontWeight: 600,
|
|
395
|
+
color: '#fff',
|
|
396
|
+
backgroundColor: colors.accentMuted,
|
|
397
|
+
border: 'none',
|
|
398
|
+
borderRadius: 6,
|
|
399
|
+
cursor: 'pointer',
|
|
400
|
+
fontFamily: fonts.sans,
|
|
401
|
+
whiteSpace: 'nowrap',
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const cancelButtonStyle: React.CSSProperties = {
|
|
405
|
+
padding: '8px 12px',
|
|
406
|
+
fontSize: 13,
|
|
407
|
+
fontWeight: 500,
|
|
408
|
+
color: colors.textSecondary,
|
|
409
|
+
backgroundColor: 'transparent',
|
|
410
|
+
border: `1px solid ${colors.border}`,
|
|
411
|
+
borderRadius: 6,
|
|
412
|
+
cursor: 'pointer',
|
|
413
|
+
fontFamily: fonts.sans,
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
// --- Mount function ---
|
|
417
|
+
|
|
418
|
+
export function createWorkspaceApp(container: HTMLElement | null): void {
|
|
419
|
+
if (!container) {
|
|
420
|
+
throw new Error('Rendiv Studio: Could not find #root element');
|
|
421
|
+
}
|
|
422
|
+
createRoot(container).render(<WorkspacePicker />);
|
|
423
|
+
}
|