groove-dev 0.27.74 → 0.27.77
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/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +256 -4
- package/node_modules/@groove-dev/daemon/src/conversations.js +16 -0
- package/node_modules/@groove-dev/daemon/src/index.js +41 -1
- package/node_modules/@groove-dev/daemon/src/preview.js +32 -2
- package/node_modules/@groove-dev/daemon/src/process.js +9 -1
- package/node_modules/@groove-dev/daemon/src/providers/base.js +4 -0
- package/node_modules/@groove-dev/daemon/src/providers/codex.js +38 -0
- package/node_modules/@groove-dev/daemon/src/providers/grok.js +156 -0
- package/node_modules/@groove-dev/daemon/src/providers/index.js +5 -1
- package/node_modules/@groove-dev/daemon/src/providers/nano-banana.js +103 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BbmPDhuW.js +8616 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-kbR5tOHu.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/app.css +41 -0
- package/node_modules/@groove-dev/gui/src/app.jsx +2 -0
- package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +16 -5
- package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +49 -11
- package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +144 -24
- package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +26 -2
- package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +105 -52
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +5 -2
- package/node_modules/@groove-dev/gui/src/components/layout/welcome-splash.jsx +215 -88
- package/node_modules/@groove-dev/gui/src/components/preview/preview-toolbar.jsx +109 -0
- package/node_modules/@groove-dev/gui/src/components/preview/preview-workspace.jsx +278 -0
- package/node_modules/@groove-dev/gui/src/components/preview/screenshot-overlay.jsx +237 -0
- package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +6 -2
- package/node_modules/@groove-dev/gui/src/stores/groove.js +149 -9
- package/node_modules/@groove-dev/gui/src/views/preview.jsx +6 -0
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +199 -114
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +256 -4
- package/packages/daemon/src/conversations.js +16 -0
- package/packages/daemon/src/index.js +41 -1
- package/packages/daemon/src/preview.js +32 -2
- package/packages/daemon/src/process.js +9 -1
- package/packages/daemon/src/providers/base.js +4 -0
- package/packages/daemon/src/providers/codex.js +38 -0
- package/packages/daemon/src/providers/grok.js +156 -0
- package/packages/daemon/src/providers/index.js +5 -1
- package/packages/daemon/src/providers/nano-banana.js +103 -0
- package/packages/gui/dist/assets/index-BbmPDhuW.js +8616 -0
- package/packages/gui/dist/assets/index-kbR5tOHu.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/app.css +41 -0
- package/packages/gui/src/app.jsx +2 -0
- package/packages/gui/src/components/chat/chat-header.jsx +16 -5
- package/packages/gui/src/components/chat/chat-input.jsx +49 -11
- package/packages/gui/src/components/chat/chat-messages.jsx +144 -24
- package/packages/gui/src/components/chat/chat-view.jsx +26 -2
- package/packages/gui/src/components/chat/model-picker.jsx +105 -52
- package/packages/gui/src/components/layout/activity-bar.jsx +5 -2
- package/packages/gui/src/components/layout/welcome-splash.jsx +215 -88
- package/packages/gui/src/components/preview/preview-toolbar.jsx +109 -0
- package/packages/gui/src/components/preview/preview-workspace.jsx +278 -0
- package/packages/gui/src/components/preview/screenshot-overlay.jsx +237 -0
- package/packages/gui/src/components/ui/toast.jsx +6 -2
- package/packages/gui/src/stores/groove.js +149 -9
- package/packages/gui/src/views/preview.jsx +6 -0
- package/packages/gui/src/views/settings.jsx +199 -114
- package/welcome.png +0 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-DFP3r2yE.js +0 -8615
- package/node_modules/@groove-dev/gui/dist/assets/index-QR7lyguO.css +0 -1
- package/packages/gui/dist/assets/index-DFP3r2yE.js +0 -8615
- package/packages/gui/dist/assets/index-QR7lyguO.css +0 -1
|
@@ -1,121 +1,248 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
-
import { useState } from 'react';
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
3
|
import { useGrooveStore } from '../../stores/groove';
|
|
4
4
|
import { HEX, hexAlpha } from '../../lib/theme-hex';
|
|
5
5
|
import { cn } from '../../lib/cn';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
FolderOpen, Radio, X, Plus, ExternalLink, Loader2, Unplug,
|
|
8
|
+
} from 'lucide-react';
|
|
7
9
|
import { FolderBrowser } from '../agents/folder-browser';
|
|
8
10
|
import { QuickConnect } from '../settings/quick-connect';
|
|
11
|
+
import { StatusDot } from '../ui/status-dot';
|
|
9
12
|
import { ToastContainer } from '../ui/toast';
|
|
10
13
|
|
|
11
14
|
export function WelcomeSplash() {
|
|
12
15
|
const recentProjects = useGrooveStore((s) => s.recentProjects);
|
|
13
16
|
const setProjectDir = useGrooveStore((s) => s.setProjectDir);
|
|
17
|
+
const removeRecentProject = useGrooveStore((s) => s.removeRecentProject);
|
|
14
18
|
const remoteHomedir = useGrooveStore((s) => s.remoteHomedir);
|
|
19
|
+
const savedTunnels = useGrooveStore((s) => s.savedTunnels);
|
|
20
|
+
const fetchTunnels = useGrooveStore((s) => s.fetchTunnels);
|
|
21
|
+
const deleteTunnel = useGrooveStore((s) => s.deleteTunnel);
|
|
22
|
+
const connectTunnel = useGrooveStore((s) => s.connectTunnel);
|
|
23
|
+
const disconnectTunnel = useGrooveStore((s) => s.disconnectTunnel);
|
|
24
|
+
const toggleQuickConnect = useGrooveStore((s) => s.toggleQuickConnect);
|
|
25
|
+
const addToast = useGrooveStore((s) => s.addToast);
|
|
26
|
+
|
|
15
27
|
const [browsing, setBrowsing] = useState(false);
|
|
28
|
+
const [connectingId, setConnectingId] = useState(null);
|
|
29
|
+
|
|
30
|
+
useEffect(() => { fetchTunnels(); }, [fetchTunnels]);
|
|
31
|
+
|
|
32
|
+
const visibleProjects = (recentProjects || []).slice(0, 8);
|
|
33
|
+
const hasRecent = visibleProjects.length > 0;
|
|
34
|
+
const hasTunnels = savedTunnels.length > 0;
|
|
35
|
+
const hasRightContent = hasRecent || hasTunnels;
|
|
16
36
|
|
|
17
|
-
|
|
37
|
+
async function handleTunnelClick(server) {
|
|
38
|
+
if (server.active) {
|
|
39
|
+
if (window.groove?.remote?.openWindow) {
|
|
40
|
+
window.groove.remote.openWindow(server.localPort, server.name);
|
|
41
|
+
} else {
|
|
42
|
+
window.open(`http://localhost:${server.localPort}?instance=${encodeURIComponent(server.name)}`, '_blank');
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
setConnectingId(server.id);
|
|
47
|
+
try {
|
|
48
|
+
await connectTunnel(server.id);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
addToast('error', 'Connection failed', err?.message || 'Unknown error');
|
|
51
|
+
}
|
|
52
|
+
setConnectingId(null);
|
|
53
|
+
}
|
|
18
54
|
|
|
19
55
|
return (
|
|
20
56
|
<div
|
|
21
|
-
className="fixed inset-0 z-50 flex items-center justify-center
|
|
57
|
+
className="fixed inset-0 z-50 flex items-center justify-center"
|
|
22
58
|
style={{ background: `radial-gradient(ellipse at 50% 40%, ${hexAlpha(HEX.accent, 0.06)} 0%, transparent 70%) ${HEX.surface0}` }}
|
|
23
59
|
>
|
|
24
|
-
<div className=
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
>
|
|
47
|
-
<div className="w-12 h-12 rounded-lg bg-accent/20 flex items-center justify-center group-hover:scale-110 transition-transform flex-shrink-0">
|
|
48
|
-
<Zap size={24} className="text-accent" />
|
|
49
|
-
</div>
|
|
50
|
-
<div className="flex-1 min-w-0">
|
|
51
|
-
<div className="text-base font-semibold text-text-0 font-sans">Start with a Planner</div>
|
|
52
|
-
<div className="text-sm text-text-2 font-sans mt-0.5">Describe what you want to build and let AI plan the perfect team</div>
|
|
60
|
+
<div className={cn(
|
|
61
|
+
'w-full max-w-4xl px-8 flex gap-12',
|
|
62
|
+
hasRightContent ? 'items-start' : 'items-center justify-center',
|
|
63
|
+
'max-md:flex-col max-md:items-center max-md:gap-8 max-md:overflow-y-auto max-md:max-h-[100vh] max-md:py-12',
|
|
64
|
+
)}>
|
|
65
|
+
{/* ── Left Panel ─────────────────────────────────────── */}
|
|
66
|
+
<div className={cn(
|
|
67
|
+
'flex flex-col',
|
|
68
|
+
hasRightContent ? 'w-[55%] max-md:w-full' : 'w-full max-w-lg',
|
|
69
|
+
hasRightContent ? 'pt-[10vh]' : 'items-center text-center',
|
|
70
|
+
)}>
|
|
71
|
+
{/* Hero */}
|
|
72
|
+
<div className={cn('flex items-center gap-4 mb-6', !hasRightContent && 'flex-col')}>
|
|
73
|
+
<div
|
|
74
|
+
className="w-16 h-16 rounded-full flex items-center justify-center flex-shrink-0"
|
|
75
|
+
style={{
|
|
76
|
+
background: hexAlpha(HEX.accent, 0.08),
|
|
77
|
+
border: `1px solid ${hexAlpha(HEX.accent, 0.15)}`,
|
|
78
|
+
boxShadow: `0 0 40px ${hexAlpha(HEX.accent, 0.1)}`,
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
<img src="/favicon.png" className="w-9 h-9 rounded-full" alt="Groove" />
|
|
53
82
|
</div>
|
|
54
|
-
<div
|
|
55
|
-
|
|
83
|
+
<div>
|
|
84
|
+
<h1 className="text-2xl font-bold text-text-0 font-sans tracking-tight">Welcome to Groove</h1>
|
|
85
|
+
<p className="text-sm text-text-2 font-sans mt-0.5">Your AI coding team, ready in minutes</p>
|
|
56
86
|
</div>
|
|
57
|
-
</
|
|
58
|
-
</div>
|
|
87
|
+
</div>
|
|
59
88
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
89
|
+
{/* Action cards */}
|
|
90
|
+
<div className="flex flex-col gap-3 mt-4 w-full">
|
|
91
|
+
<button
|
|
92
|
+
onClick={() => setBrowsing(true)}
|
|
93
|
+
className="w-full flex items-center gap-4 p-5 rounded-lg border border-accent/25 bg-gradient-to-r from-accent/8 to-accent/3 hover:from-accent/14 hover:to-accent/6 hover:border-accent/40 transition-all cursor-pointer group text-left"
|
|
94
|
+
>
|
|
95
|
+
<div className="w-11 h-11 rounded-lg bg-accent/20 flex items-center justify-center group-hover:scale-110 transition-transform flex-shrink-0">
|
|
96
|
+
<FolderOpen size={22} className="text-accent" />
|
|
97
|
+
</div>
|
|
98
|
+
<div className="flex-1 min-w-0">
|
|
99
|
+
<div className="text-base font-semibold text-text-0 font-sans">Open Project</div>
|
|
100
|
+
<div className="text-sm text-text-2 font-sans mt-0.5">Browse the filesystem to pick a project</div>
|
|
101
|
+
</div>
|
|
102
|
+
</button>
|
|
74
103
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
104
|
+
<button
|
|
105
|
+
onClick={toggleQuickConnect}
|
|
106
|
+
className="w-full flex items-center gap-4 p-5 rounded-lg border border-border bg-surface-1 hover:bg-surface-2 hover:border-border transition-all cursor-pointer group text-left"
|
|
107
|
+
>
|
|
108
|
+
<div className="w-11 h-11 rounded-lg bg-surface-4 flex items-center justify-center group-hover:scale-110 transition-transform flex-shrink-0">
|
|
109
|
+
<Radio size={22} className="text-text-1" />
|
|
110
|
+
</div>
|
|
111
|
+
<div className="flex-1 min-w-0">
|
|
112
|
+
<div className="text-base font-semibold text-text-0 font-sans">Connect to Remote</div>
|
|
113
|
+
<div className="text-sm text-text-2 font-sans mt-0.5">SSH tunnel to a server</div>
|
|
114
|
+
</div>
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
{/* Keyboard shortcuts */}
|
|
119
|
+
<p className="text-xs text-text-4 font-sans mt-8">
|
|
120
|
+
<kbd className="font-mono bg-surface-4 px-1.5 py-0.5 rounded text-text-3">Cmd+K</kbd>
|
|
121
|
+
<span className="mx-1.5">command palette</span>
|
|
122
|
+
<span className="text-text-4 mx-1">·</span>
|
|
123
|
+
<kbd className="font-mono bg-surface-4 px-1.5 py-0.5 rounded text-text-3">Cmd+N</kbd>
|
|
124
|
+
<span className="mx-1.5">spawn</span>
|
|
125
|
+
<span className="text-text-4 mx-1">·</span>
|
|
126
|
+
<kbd className="font-mono bg-surface-4 px-1.5 py-0.5 rounded text-text-3">Cmd+J</kbd>
|
|
127
|
+
<span className="mx-1.5">terminal</span>
|
|
128
|
+
</p>
|
|
87
129
|
</div>
|
|
88
130
|
|
|
89
|
-
{/*
|
|
90
|
-
{
|
|
91
|
-
<div className="w-full
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
131
|
+
{/* ── Right Panel ────────────────────────────────────── */}
|
|
132
|
+
{hasRightContent && (
|
|
133
|
+
<div className="w-[45%] max-md:w-full pt-[10vh] max-md:pt-0 min-w-0 max-h-[80vh] overflow-y-auto">
|
|
134
|
+
{/* Recent Projects */}
|
|
135
|
+
{hasRecent && (
|
|
136
|
+
<div>
|
|
137
|
+
<div className="text-2xs font-mono text-text-3 uppercase tracking-widest mb-2">Recent</div>
|
|
138
|
+
<div className="flex flex-col">
|
|
139
|
+
{visibleProjects.map((project) => (
|
|
140
|
+
<div
|
|
141
|
+
key={project.path}
|
|
142
|
+
className="group flex items-center gap-2 px-2 py-1.5 rounded-sm hover:bg-surface-1 transition-colors"
|
|
143
|
+
>
|
|
144
|
+
<button
|
|
145
|
+
onClick={() => setProjectDir(project.path)}
|
|
146
|
+
className="flex-1 min-w-0 text-left cursor-pointer"
|
|
147
|
+
>
|
|
148
|
+
<div className="text-sm font-medium text-text-1 hover:text-accent truncate transition-colors">
|
|
149
|
+
{project.name}
|
|
150
|
+
</div>
|
|
151
|
+
<div className="text-2xs font-mono text-text-4 truncate">{project.path}</div>
|
|
152
|
+
</button>
|
|
153
|
+
<button
|
|
154
|
+
onClick={(e) => { e.stopPropagation(); removeRecentProject(project.path); }}
|
|
155
|
+
className="opacity-0 group-hover:opacity-100 p-0.5 text-text-4 hover:text-danger cursor-pointer transition-all flex-shrink-0"
|
|
156
|
+
title="Remove from recent"
|
|
157
|
+
>
|
|
158
|
+
<X size={14} />
|
|
159
|
+
</button>
|
|
160
|
+
</div>
|
|
161
|
+
))}
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
|
|
166
|
+
{/* Divider */}
|
|
167
|
+
{hasRecent && hasTunnels && (
|
|
168
|
+
<div className="border-t border-border-subtle my-4" />
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
{/* SSH Connections */}
|
|
172
|
+
{hasTunnels && (
|
|
173
|
+
<div>
|
|
174
|
+
<div className="text-2xs font-mono text-text-3 uppercase tracking-widest mb-2">SSH Connections</div>
|
|
175
|
+
<div className="flex flex-col">
|
|
176
|
+
{savedTunnels.map((server) => (
|
|
177
|
+
<div
|
|
178
|
+
key={server.id}
|
|
179
|
+
className={cn(
|
|
180
|
+
'group flex items-center gap-2 px-2 py-1.5 rounded-sm hover:bg-surface-1 transition-colors',
|
|
181
|
+
connectingId === server.id && 'opacity-60 pointer-events-none',
|
|
182
|
+
)}
|
|
183
|
+
>
|
|
184
|
+
<button
|
|
185
|
+
onClick={() => handleTunnelClick(server)}
|
|
186
|
+
disabled={connectingId === server.id}
|
|
187
|
+
className="flex-1 min-w-0 text-left cursor-pointer"
|
|
188
|
+
>
|
|
189
|
+
<div className="flex items-center gap-1.5">
|
|
190
|
+
<span className="text-sm font-medium text-text-1 hover:text-accent truncate transition-colors">
|
|
191
|
+
{server.name}
|
|
192
|
+
</span>
|
|
193
|
+
{server.active && <StatusDot status="running" size="sm" />}
|
|
194
|
+
</div>
|
|
195
|
+
<div className="text-2xs font-mono text-text-4 truncate">{server.user}@{server.host}</div>
|
|
196
|
+
</button>
|
|
197
|
+
<div className="flex items-center gap-1 flex-shrink-0">
|
|
198
|
+
{connectingId === server.id ? (
|
|
199
|
+
<Loader2 size={14} className="text-text-3 animate-spin" />
|
|
200
|
+
) : server.active ? (
|
|
201
|
+
<>
|
|
202
|
+
<button
|
|
203
|
+
onClick={() => handleTunnelClick(server)}
|
|
204
|
+
className="opacity-0 group-hover:opacity-100 flex items-center gap-0.5 text-2xs text-success hover:text-success/80 cursor-pointer transition-all"
|
|
205
|
+
>
|
|
206
|
+
<ExternalLink size={11} /> Open
|
|
207
|
+
</button>
|
|
208
|
+
<button
|
|
209
|
+
onClick={async () => {
|
|
210
|
+
await disconnectTunnel(server.id);
|
|
211
|
+
addToast('info', 'Disconnected', server.name);
|
|
212
|
+
}}
|
|
213
|
+
className="opacity-0 group-hover:opacity-100 p-0.5 text-text-4 hover:text-danger cursor-pointer transition-all"
|
|
214
|
+
title="Disconnect"
|
|
215
|
+
>
|
|
216
|
+
<Unplug size={12} />
|
|
217
|
+
</button>
|
|
218
|
+
</>
|
|
219
|
+
) : null}
|
|
220
|
+
<button
|
|
221
|
+
onClick={(e) => { e.stopPropagation(); deleteTunnel(server.id); }}
|
|
222
|
+
className="opacity-0 group-hover:opacity-100 p-0.5 text-text-4 hover:text-danger cursor-pointer transition-all flex-shrink-0"
|
|
223
|
+
title="Remove connection"
|
|
224
|
+
>
|
|
225
|
+
<X size={14} />
|
|
226
|
+
</button>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
))}
|
|
230
|
+
</div>
|
|
95
231
|
<button
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
className="bg-surface-1 rounded-sm border border-border-subtle px-4 py-3 cursor-pointer hover:bg-surface-2 transition-colors text-left"
|
|
232
|
+
onClick={toggleQuickConnect}
|
|
233
|
+
className="flex items-center gap-1 text-xs text-accent hover:underline font-sans cursor-pointer mt-2 px-2"
|
|
99
234
|
>
|
|
100
|
-
<
|
|
101
|
-
<div className="text-2xs font-mono text-text-4 truncate mt-1">{project.path}</div>
|
|
235
|
+
<Plus size={12} /> Add Connection
|
|
102
236
|
</button>
|
|
103
|
-
|
|
104
|
-
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
|
|
240
|
+
{/* Empty state */}
|
|
241
|
+
{!hasRecent && !hasTunnels && (
|
|
242
|
+
<p className="text-sm text-text-4 italic px-2">No recent activity</p>
|
|
243
|
+
)}
|
|
105
244
|
</div>
|
|
106
245
|
)}
|
|
107
|
-
|
|
108
|
-
{/* Keyboard shortcuts */}
|
|
109
|
-
<p className="text-xs text-text-4 font-sans mt-8">
|
|
110
|
-
<kbd className="font-mono bg-surface-4 px-1.5 py-0.5 rounded text-text-3">Cmd+K</kbd>
|
|
111
|
-
<span className="mx-1.5">command palette</span>
|
|
112
|
-
<span className="text-text-4 mx-1">·</span>
|
|
113
|
-
<kbd className="font-mono bg-surface-4 px-1.5 py-0.5 rounded text-text-3">Cmd+N</kbd>
|
|
114
|
-
<span className="mx-1.5">spawn</span>
|
|
115
|
-
<span className="text-text-4 mx-1">·</span>
|
|
116
|
-
<kbd className="font-mono bg-surface-4 px-1.5 py-0.5 rounded text-text-3">Cmd+J</kbd>
|
|
117
|
-
<span className="mx-1.5">terminal</span>
|
|
118
|
-
</p>
|
|
119
246
|
</div>
|
|
120
247
|
|
|
121
248
|
<FolderBrowser
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useState, useRef, useEffect } from 'react';
|
|
3
|
+
import { RefreshCw, Monitor, Tablet, Smartphone, Camera, X } from 'lucide-react';
|
|
4
|
+
import { cn } from '../../lib/cn';
|
|
5
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
6
|
+
|
|
7
|
+
const DEVICE_SIZES = [
|
|
8
|
+
{ id: 'desktop', icon: Monitor, label: 'Desktop', width: '100%' },
|
|
9
|
+
{ id: 'tablet', icon: Tablet, label: 'Tablet (768px)', width: '768px' },
|
|
10
|
+
{ id: 'mobile', icon: Smartphone, label: 'Mobile (375px)', width: '375px' },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export function PreviewToolbar({ onRefresh }) {
|
|
14
|
+
const previewState = useGrooveStore((s) => s.previewState);
|
|
15
|
+
const setPreviewDevice = useGrooveStore((s) => s.setPreviewDevice);
|
|
16
|
+
const toggleScreenshotMode = useGrooveStore((s) => s.toggleScreenshotMode);
|
|
17
|
+
const closePreview = useGrooveStore((s) => s.closePreview);
|
|
18
|
+
|
|
19
|
+
const [confirming, setConfirming] = useState(false);
|
|
20
|
+
const timerRef = useRef(null);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
return () => { if (timerRef.current) clearTimeout(timerRef.current); };
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
function handleClose() {
|
|
27
|
+
if (confirming) {
|
|
28
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
29
|
+
setConfirming(false);
|
|
30
|
+
closePreview();
|
|
31
|
+
} else {
|
|
32
|
+
setConfirming(true);
|
|
33
|
+
timerRef.current = setTimeout(() => setConfirming(false), 2000);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const proxyUrl = previewState.teamId
|
|
38
|
+
? `${window.location.origin}/api/preview/${previewState.teamId}/proxy/`
|
|
39
|
+
: previewState.url;
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="h-10 flex items-center gap-2 px-3 bg-surface-3 border-b border-border flex-shrink-0">
|
|
43
|
+
{/* URL display */}
|
|
44
|
+
<div className="flex-1 min-w-0 h-7 flex items-center px-3 rounded-md bg-surface-1 border border-border-subtle">
|
|
45
|
+
<span className="text-2xs font-mono text-text-3 truncate">{proxyUrl || 'No URL'}</span>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
{/* Refresh */}
|
|
49
|
+
<button
|
|
50
|
+
onClick={onRefresh}
|
|
51
|
+
className="w-7 h-7 flex items-center justify-center rounded-md text-text-3 hover:text-accent hover:bg-accent/10 transition-colors cursor-pointer"
|
|
52
|
+
title="Refresh"
|
|
53
|
+
>
|
|
54
|
+
<RefreshCw size={14} />
|
|
55
|
+
</button>
|
|
56
|
+
|
|
57
|
+
{/* Device size toggles */}
|
|
58
|
+
<div className="flex items-center gap-0.5 px-1 py-0.5 rounded-md bg-surface-1 border border-border-subtle">
|
|
59
|
+
{DEVICE_SIZES.map((device) => (
|
|
60
|
+
<button
|
|
61
|
+
key={device.id}
|
|
62
|
+
onClick={() => setPreviewDevice(device.id)}
|
|
63
|
+
className={cn(
|
|
64
|
+
'w-7 h-6 flex items-center justify-center rounded transition-colors cursor-pointer',
|
|
65
|
+
previewState.deviceSize === device.id
|
|
66
|
+
? 'text-accent bg-accent/10'
|
|
67
|
+
: 'text-text-3 hover:text-text-1',
|
|
68
|
+
)}
|
|
69
|
+
title={device.label}
|
|
70
|
+
>
|
|
71
|
+
<device.icon size={13} />
|
|
72
|
+
</button>
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{/* Screenshot */}
|
|
77
|
+
<button
|
|
78
|
+
onClick={toggleScreenshotMode}
|
|
79
|
+
className={cn(
|
|
80
|
+
'w-7 h-7 flex items-center justify-center rounded-md transition-colors cursor-pointer',
|
|
81
|
+
previewState.screenshotMode
|
|
82
|
+
? 'text-accent bg-accent/10'
|
|
83
|
+
: 'text-text-3 hover:text-accent hover:bg-accent/10',
|
|
84
|
+
)}
|
|
85
|
+
title="Screenshot"
|
|
86
|
+
>
|
|
87
|
+
<Camera size={14} />
|
|
88
|
+
</button>
|
|
89
|
+
|
|
90
|
+
{/* Close preview — two-click confirmation */}
|
|
91
|
+
<button
|
|
92
|
+
onClick={handleClose}
|
|
93
|
+
className={cn(
|
|
94
|
+
'h-7 flex items-center justify-center rounded-md transition-all cursor-pointer',
|
|
95
|
+
confirming
|
|
96
|
+
? 'px-2 gap-1.5 bg-danger/15 text-danger border border-danger/25'
|
|
97
|
+
: 'w-7 text-text-3 hover:text-danger hover:bg-danger/10',
|
|
98
|
+
)}
|
|
99
|
+
title="Close Preview"
|
|
100
|
+
>
|
|
101
|
+
{confirming ? (
|
|
102
|
+
<span className="text-2xs font-semibold font-sans whitespace-nowrap">Close?</span>
|
|
103
|
+
) : (
|
|
104
|
+
<X size={14} />
|
|
105
|
+
)}
|
|
106
|
+
</button>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|