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.
Files changed (70) hide show
  1. package/node_modules/@groove-dev/cli/package.json +1 -1
  2. package/node_modules/@groove-dev/daemon/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/src/api.js +256 -4
  4. package/node_modules/@groove-dev/daemon/src/conversations.js +16 -0
  5. package/node_modules/@groove-dev/daemon/src/index.js +41 -1
  6. package/node_modules/@groove-dev/daemon/src/preview.js +32 -2
  7. package/node_modules/@groove-dev/daemon/src/process.js +9 -1
  8. package/node_modules/@groove-dev/daemon/src/providers/base.js +4 -0
  9. package/node_modules/@groove-dev/daemon/src/providers/codex.js +38 -0
  10. package/node_modules/@groove-dev/daemon/src/providers/grok.js +156 -0
  11. package/node_modules/@groove-dev/daemon/src/providers/index.js +5 -1
  12. package/node_modules/@groove-dev/daemon/src/providers/nano-banana.js +103 -0
  13. package/node_modules/@groove-dev/gui/dist/assets/index-BbmPDhuW.js +8616 -0
  14. package/node_modules/@groove-dev/gui/dist/assets/index-kbR5tOHu.css +1 -0
  15. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  16. package/node_modules/@groove-dev/gui/package.json +1 -1
  17. package/node_modules/@groove-dev/gui/src/app.css +41 -0
  18. package/node_modules/@groove-dev/gui/src/app.jsx +2 -0
  19. package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +16 -5
  20. package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +49 -11
  21. package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +144 -24
  22. package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +26 -2
  23. package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +105 -52
  24. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +5 -2
  25. package/node_modules/@groove-dev/gui/src/components/layout/welcome-splash.jsx +215 -88
  26. package/node_modules/@groove-dev/gui/src/components/preview/preview-toolbar.jsx +109 -0
  27. package/node_modules/@groove-dev/gui/src/components/preview/preview-workspace.jsx +278 -0
  28. package/node_modules/@groove-dev/gui/src/components/preview/screenshot-overlay.jsx +237 -0
  29. package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +6 -2
  30. package/node_modules/@groove-dev/gui/src/stores/groove.js +149 -9
  31. package/node_modules/@groove-dev/gui/src/views/preview.jsx +6 -0
  32. package/node_modules/@groove-dev/gui/src/views/settings.jsx +199 -114
  33. package/package.json +1 -1
  34. package/packages/cli/package.json +1 -1
  35. package/packages/daemon/package.json +1 -1
  36. package/packages/daemon/src/api.js +256 -4
  37. package/packages/daemon/src/conversations.js +16 -0
  38. package/packages/daemon/src/index.js +41 -1
  39. package/packages/daemon/src/preview.js +32 -2
  40. package/packages/daemon/src/process.js +9 -1
  41. package/packages/daemon/src/providers/base.js +4 -0
  42. package/packages/daemon/src/providers/codex.js +38 -0
  43. package/packages/daemon/src/providers/grok.js +156 -0
  44. package/packages/daemon/src/providers/index.js +5 -1
  45. package/packages/daemon/src/providers/nano-banana.js +103 -0
  46. package/packages/gui/dist/assets/index-BbmPDhuW.js +8616 -0
  47. package/packages/gui/dist/assets/index-kbR5tOHu.css +1 -0
  48. package/packages/gui/dist/index.html +2 -2
  49. package/packages/gui/package.json +1 -1
  50. package/packages/gui/src/app.css +41 -0
  51. package/packages/gui/src/app.jsx +2 -0
  52. package/packages/gui/src/components/chat/chat-header.jsx +16 -5
  53. package/packages/gui/src/components/chat/chat-input.jsx +49 -11
  54. package/packages/gui/src/components/chat/chat-messages.jsx +144 -24
  55. package/packages/gui/src/components/chat/chat-view.jsx +26 -2
  56. package/packages/gui/src/components/chat/model-picker.jsx +105 -52
  57. package/packages/gui/src/components/layout/activity-bar.jsx +5 -2
  58. package/packages/gui/src/components/layout/welcome-splash.jsx +215 -88
  59. package/packages/gui/src/components/preview/preview-toolbar.jsx +109 -0
  60. package/packages/gui/src/components/preview/preview-workspace.jsx +278 -0
  61. package/packages/gui/src/components/preview/screenshot-overlay.jsx +237 -0
  62. package/packages/gui/src/components/ui/toast.jsx +6 -2
  63. package/packages/gui/src/stores/groove.js +149 -9
  64. package/packages/gui/src/views/preview.jsx +6 -0
  65. package/packages/gui/src/views/settings.jsx +199 -114
  66. package/welcome.png +0 -0
  67. package/node_modules/@groove-dev/gui/dist/assets/index-DFP3r2yE.js +0 -8615
  68. package/node_modules/@groove-dev/gui/dist/assets/index-QR7lyguO.css +0 -1
  69. package/packages/gui/dist/assets/index-DFP3r2yE.js +0 -8615
  70. 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 { Zap, FolderOpen, Radio } from 'lucide-react';
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
- const visibleProjects = (recentProjects || []).slice(0, 6);
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 overflow-y-auto"
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="max-w-2xl w-full px-8 py-16 flex flex-col items-center text-center">
25
- {/* Hero icon */}
26
- <div
27
- className="w-24 h-24 rounded-full flex items-center justify-center mb-6"
28
- style={{
29
- background: hexAlpha(HEX.accent, 0.08),
30
- border: `1px solid ${hexAlpha(HEX.accent, 0.15)}`,
31
- boxShadow: `0 0 40px ${hexAlpha(HEX.accent, 0.1)}`,
32
- }}
33
- >
34
- <img src="/favicon.png" className="w-12 h-12 rounded-full" alt="Groove" />
35
- </div>
36
-
37
- {/* Headline */}
38
- <h1 className="text-3xl font-bold text-text-0 font-sans tracking-tight">Welcome to Groove</h1>
39
- <p className="text-sm text-text-2 font-sans mt-2">Your AI coding team, ready in minutes</p>
40
-
41
- {/* Primary action */}
42
- <div className="w-full mt-10">
43
- <button
44
- onClick={() => useGrooveStore.setState({ showProjectPicker: false })}
45
- 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"
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 className="text-accent text-xs font-semibold font-sans flex-shrink-0 opacity-60 group-hover:opacity-100 transition-opacity">
55
- Recommended
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
- </button>
58
- </div>
87
+ </div>
59
88
 
60
- {/* Secondary actions */}
61
- <div className="w-full grid grid-cols-2 gap-3 mt-3">
62
- <button
63
- onClick={() => setBrowsing(true)}
64
- className="w-full flex items-center gap-3 p-4 rounded-lg border border-border bg-surface-1 hover:bg-surface-2 hover:border-border transition-all cursor-pointer group text-left"
65
- >
66
- <div className="w-10 h-10 rounded-lg bg-surface-4 flex items-center justify-center group-hover:scale-110 transition-transform flex-shrink-0">
67
- <FolderOpen size={20} className="text-text-1" />
68
- </div>
69
- <div className="min-w-0">
70
- <div className="text-sm font-semibold text-text-0 font-sans">Open Project</div>
71
- <div className="text-xs text-text-3 font-sans mt-0.5">Browse the filesystem</div>
72
- </div>
73
- </button>
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
- <button
76
- onClick={() => useGrooveStore.getState().toggleQuickConnect()}
77
- className="w-full flex items-center gap-3 p-4 rounded-lg border border-border bg-surface-1 hover:bg-surface-2 hover:border-border transition-all cursor-pointer group text-left"
78
- >
79
- <div className="w-10 h-10 rounded-lg bg-surface-4 flex items-center justify-center group-hover:scale-110 transition-transform flex-shrink-0">
80
- <Radio size={20} className="text-text-1" />
81
- </div>
82
- <div className="min-w-0">
83
- <div className="text-sm font-semibold text-text-0 font-sans">Connect to Remote</div>
84
- <div className="text-xs text-text-3 font-sans mt-0.5">SSH tunnel to a server</div>
85
- </div>
86
- </button>
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">&middot;</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">&middot;</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
- {/* Recent projects */}
90
- {visibleProjects.length > 0 && (
91
- <div className="w-full mt-8">
92
- <div className="text-2xs font-mono text-text-3 uppercase tracking-widest mb-2 text-left">Recent</div>
93
- <div className={cn('grid gap-2', visibleProjects.length === 1 ? 'grid-cols-1' : visibleProjects.length === 2 ? 'grid-cols-2' : 'grid-cols-3')}>
94
- {visibleProjects.map((project) => (
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
- key={project.path}
97
- onClick={() => setProjectDir(project.path)}
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
- <div className="text-sm font-medium text-text-0 truncate">{project.name}</div>
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
- </div>
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">&middot;</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">&middot;</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
+ }