groove-dev 0.16.4 → 0.17.1

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 (34) hide show
  1. package/README.md +18 -16
  2. package/node_modules/@groove-dev/daemon/integrations-registry.json +417 -0
  3. package/node_modules/@groove-dev/daemon/src/api.js +204 -0
  4. package/node_modules/@groove-dev/daemon/src/index.js +9 -0
  5. package/node_modules/@groove-dev/daemon/src/integrations.js +475 -0
  6. package/node_modules/@groove-dev/daemon/src/introducer.js +23 -0
  7. package/node_modules/@groove-dev/daemon/src/process.js +59 -0
  8. package/node_modules/@groove-dev/daemon/src/registry.js +2 -1
  9. package/node_modules/@groove-dev/daemon/src/scheduler.js +336 -0
  10. package/node_modules/@groove-dev/daemon/src/validate.js +10 -0
  11. package/node_modules/@groove-dev/gui/dist/assets/index-CEf7nLM2.js +156 -0
  12. package/node_modules/@groove-dev/gui/dist/index.html +1 -1
  13. package/node_modules/@groove-dev/gui/src/App.jsx +6 -0
  14. package/node_modules/@groove-dev/gui/src/components/SpawnPanel.jsx +98 -7
  15. package/node_modules/@groove-dev/gui/src/views/IntegrationsStore.jsx +1171 -0
  16. package/node_modules/@groove-dev/gui/src/views/ScheduleManager.jsx +614 -0
  17. package/package.json +2 -2
  18. package/packages/daemon/integrations-registry.json +417 -0
  19. package/packages/daemon/src/api.js +204 -0
  20. package/packages/daemon/src/index.js +9 -0
  21. package/packages/daemon/src/integrations.js +475 -0
  22. package/packages/daemon/src/introducer.js +23 -0
  23. package/packages/daemon/src/process.js +59 -0
  24. package/packages/daemon/src/registry.js +2 -1
  25. package/packages/daemon/src/scheduler.js +336 -0
  26. package/packages/daemon/src/validate.js +10 -0
  27. package/packages/gui/dist/assets/index-CEf7nLM2.js +156 -0
  28. package/packages/gui/dist/index.html +1 -1
  29. package/packages/gui/src/App.jsx +6 -0
  30. package/packages/gui/src/components/SpawnPanel.jsx +98 -7
  31. package/packages/gui/src/views/IntegrationsStore.jsx +1171 -0
  32. package/packages/gui/src/views/ScheduleManager.jsx +614 -0
  33. package/node_modules/@groove-dev/gui/dist/assets/index-B_VHpncx.js +0 -153
  34. package/packages/gui/dist/assets/index-B_VHpncx.js +0 -153
@@ -0,0 +1,1171 @@
1
+ // GROOVE GUI — Integrations Store (MCP Marketplace)
2
+ // FSL-1.1-Apache-2.0 — see LICENSE
3
+
4
+ import React, { useState, useEffect, useCallback } from 'react';
5
+
6
+ const CATEGORY_LABELS = {
7
+ all: 'All',
8
+ communication: 'Communication',
9
+ productivity: 'Productivity',
10
+ finance: 'Finance',
11
+ developer: 'Developer',
12
+ database: 'Database',
13
+ 'smart-home': 'Smart Home',
14
+ analytics: 'Analytics',
15
+ };
16
+
17
+ const CATEGORY_ICONS = {
18
+ communication: '\u2709',
19
+ productivity: '\u2699',
20
+ finance: '\u2696',
21
+ developer: '\u2318',
22
+ database: '\u2261',
23
+ 'smart-home': '\u2302',
24
+ analytics: '\u2315',
25
+ };
26
+
27
+ const ICON_MAP = {
28
+ slack: 'S', github: 'G', stripe: '$', calendar: 'C', email: '@',
29
+ gmail: '@', database: 'D', search: '?', drive: 'D', linear: 'L',
30
+ notion: 'N', discord: 'D', home: 'H', folder: 'F', map: 'M',
31
+ };
32
+
33
+ const SORT_OPTIONS = [
34
+ { id: 'popular', label: 'Popular' },
35
+ { id: 'name', label: 'A\u2013Z' },
36
+ { id: 'category', label: 'Category' },
37
+ ];
38
+
39
+ // Verification badges
40
+ function getVerification(item) {
41
+ if (item.verified === 'mcp-official') return { label: 'Official', color: 'var(--accent)', bg: 'rgba(51, 175, 188, 0.12)' };
42
+ if (item.verified === 'verified') return { label: 'Verified', color: 'var(--green)', bg: 'rgba(74, 225, 104, 0.10)' };
43
+ return null;
44
+ }
45
+
46
+ function VerifiedBadge({ item, size = 'small' }) {
47
+ const v = getVerification(item);
48
+ if (!v) return null;
49
+ const isSmall = size === 'small';
50
+ return (
51
+ <span
52
+ title={`${v.label} MCP server`}
53
+ style={{
54
+ display: 'inline-flex', alignItems: 'center', gap: 3,
55
+ fontSize: isSmall ? 9 : 10, fontWeight: 600,
56
+ color: v.color, background: v.bg,
57
+ padding: isSmall ? '1px 6px' : '2px 8px',
58
+ borderRadius: 3, letterSpacing: 0.3,
59
+ flexShrink: 0, cursor: 'default',
60
+ }}
61
+ >
62
+ <span style={{ fontSize: isSmall ? 8 : 10, lineHeight: 1 }}>{'\u2713'}</span>
63
+ {v.label}
64
+ </span>
65
+ );
66
+ }
67
+
68
+ function sortIntegrations(items, sortBy) {
69
+ const sorted = [...items];
70
+ switch (sortBy) {
71
+ case 'popular': return sorted.sort((a, b) => (b.featured ? 1 : 0) - (a.featured ? 1 : 0) || a.name.localeCompare(b.name));
72
+ case 'name': return sorted.sort((a, b) => a.name.localeCompare(b.name));
73
+ case 'category': return sorted.sort((a, b) => (a.category || '').localeCompare(b.category || '') || a.name.localeCompare(b.name));
74
+ default: return sorted;
75
+ }
76
+ }
77
+
78
+ // -- Credential Setup Modal (Guided Wizard) --
79
+ function CredentialModal({ integration, onClose }) {
80
+ const [values, setValues] = useState({});
81
+ const [saving, setSaving] = useState(false);
82
+ const [saved, setSaved] = useState({});
83
+ const [oauthStatus, setOauthStatus] = useState(null); // null, 'checking', 'not-configured', 'ready', 'connecting'
84
+ const [googleClientId, setGoogleClientId] = useState('');
85
+ const [googleClientSecret, setGoogleClientSecret] = useState('');
86
+ const [showGoogleSetup, setShowGoogleSetup] = useState(false);
87
+
88
+ useEffect(() => {
89
+ if (integration?.authType === 'oauth-google') {
90
+ setOauthStatus('checking');
91
+ fetch('/api/integrations/google-oauth/status')
92
+ .then((r) => r.json())
93
+ .then((data) => setOauthStatus(data.configured ? 'ready' : 'not-configured'))
94
+ .catch(() => setOauthStatus('not-configured'));
95
+ }
96
+ }, [integration]);
97
+
98
+ if (!integration) return null;
99
+
100
+ const isOAuth = integration.authType === 'oauth-google';
101
+ const envKeys = (integration.envKeys || []).filter((ek) => !ek.hidden);
102
+ const setupSteps = integration.setupSteps || [];
103
+
104
+ async function handleSave(key) {
105
+ if (!values[key]) return;
106
+ setSaving(true);
107
+ try {
108
+ const res = await fetch(`/api/integrations/${integration.id}/credentials`, {
109
+ method: 'POST',
110
+ headers: { 'Content-Type': 'application/json' },
111
+ body: JSON.stringify({ key, value: values[key] }),
112
+ });
113
+ if (res.ok) {
114
+ setSaved((prev) => ({ ...prev, [key]: true }));
115
+ setValues((prev) => ({ ...prev, [key]: '' }));
116
+ }
117
+ } catch { /* ignore */ }
118
+ setSaving(false);
119
+ }
120
+
121
+ async function handleGoogleSetup() {
122
+ if (!googleClientId || !googleClientSecret) return;
123
+ setSaving(true);
124
+ try {
125
+ await fetch('/api/integrations/google-oauth/setup', {
126
+ method: 'POST',
127
+ headers: { 'Content-Type': 'application/json' },
128
+ body: JSON.stringify({ clientId: googleClientId, clientSecret: googleClientSecret }),
129
+ });
130
+ setOauthStatus('ready');
131
+ setShowGoogleSetup(false);
132
+ } catch { /* ignore */ }
133
+ setSaving(false);
134
+ }
135
+
136
+ async function handleOAuthConnect() {
137
+ setOauthStatus('connecting');
138
+ try {
139
+ const res = await fetch(`/api/integrations/${integration.id}/oauth/start`, { method: 'POST' });
140
+ const data = await res.json();
141
+ if (data.url) {
142
+ window.open(data.url, '_blank', 'width=600,height=700');
143
+ // Poll for completion
144
+ const poll = setInterval(async () => {
145
+ try {
146
+ const statusRes = await fetch(`/api/integrations/${integration.id}/status`);
147
+ const status = await statusRes.json();
148
+ if (status.configured) {
149
+ clearInterval(poll);
150
+ setOauthStatus('ready');
151
+ onClose();
152
+ }
153
+ } catch { /* ignore */ }
154
+ }, 2000);
155
+ // Stop polling after 5 minutes
156
+ setTimeout(() => clearInterval(poll), 300000);
157
+ }
158
+ } catch {
159
+ setOauthStatus('ready');
160
+ }
161
+ }
162
+
163
+ return (
164
+ <div style={modal.overlay} onClick={onClose}>
165
+ <div style={modal.container} onClick={(e) => e.stopPropagation()}>
166
+ <div style={modal.topBar}>
167
+ <span style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-bright)' }}>
168
+ Connect {integration.name}
169
+ </span>
170
+ <button onClick={onClose} style={modal.closeBtn}>&times;</button>
171
+ </div>
172
+
173
+ <div style={{ padding: '16px 0' }}>
174
+ {/* Setup guide steps */}
175
+ {setupSteps.length > 0 && (
176
+ <div style={{ marginBottom: 20 }}>
177
+ <div style={{
178
+ fontSize: 10, fontWeight: 700, color: 'var(--text-muted)',
179
+ textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 10,
180
+ }}>
181
+ Setup Guide
182
+ </div>
183
+ {setupSteps.map((step, i) => (
184
+ <div key={i} style={{
185
+ display: 'flex', gap: 10, marginBottom: 8,
186
+ fontSize: 11, color: 'var(--text-primary)', lineHeight: 1.5,
187
+ }}>
188
+ <span style={{
189
+ width: 20, height: 20, borderRadius: '50%', flexShrink: 0,
190
+ background: 'var(--bg-active)', color: 'var(--accent)',
191
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
192
+ fontSize: 10, fontWeight: 700,
193
+ }}>
194
+ {i + 1}
195
+ </span>
196
+ <span>{step}</span>
197
+ </div>
198
+ ))}
199
+ </div>
200
+ )}
201
+
202
+ {/* Open setup page button for API key integrations */}
203
+ {integration.setupUrl && !isOAuth && (
204
+ <a
205
+ href={integration.setupUrl}
206
+ target="_blank"
207
+ rel="noopener noreferrer"
208
+ style={{
209
+ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
210
+ padding: '10px 16px', marginBottom: 16,
211
+ background: 'var(--bg-active)', color: 'var(--accent)',
212
+ border: '1px solid var(--accent)', borderRadius: 6,
213
+ fontSize: 12, fontWeight: 600, textDecoration: 'none',
214
+ cursor: 'pointer',
215
+ }}
216
+ >
217
+ Open {integration.name} Settings {'\u2197'}
218
+ </a>
219
+ )}
220
+
221
+ {/* OAuth flow for Google integrations */}
222
+ {isOAuth && (
223
+ <div style={{ marginBottom: 16 }}>
224
+ {oauthStatus === 'checking' && (
225
+ <div style={{ fontSize: 11, color: 'var(--text-muted)', textAlign: 'center', padding: 16 }}>
226
+ Checking Google OAuth setup...
227
+ </div>
228
+ )}
229
+
230
+ {oauthStatus === 'not-configured' && !showGoogleSetup && (
231
+ <div style={{
232
+ padding: 16, borderRadius: 8,
233
+ background: 'rgba(229, 192, 123, 0.06)', border: '1px solid var(--amber)',
234
+ }}>
235
+ <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--amber)', marginBottom: 8 }}>
236
+ One-time Google setup needed
237
+ </div>
238
+ <div style={{ fontSize: 11, color: 'var(--text-dim)', lineHeight: 1.5, marginBottom: 12 }}>
239
+ To connect Google services, you need a Google Cloud project with OAuth credentials.
240
+ This is a one-time setup that works for Gmail, Calendar, and Drive.
241
+ </div>
242
+ <a
243
+ href="https://console.cloud.google.com/apis/credentials"
244
+ target="_blank"
245
+ rel="noopener noreferrer"
246
+ style={{
247
+ display: 'inline-flex', alignItems: 'center', gap: 4,
248
+ fontSize: 11, color: 'var(--accent)', textDecoration: 'none',
249
+ marginBottom: 12,
250
+ }}
251
+ >
252
+ Open Google Cloud Console {'\u2197'}
253
+ </a>
254
+ <div style={{ fontSize: 10, color: 'var(--text-muted)', lineHeight: 1.6, marginBottom: 12 }}>
255
+ 1. Create a project (or select existing){'\n'}
256
+ 2. Configure OAuth consent screen (External, add your email as test user){'\n'}
257
+ 3. Create OAuth Client ID (Desktop app){'\n'}
258
+ 4. Copy the Client ID and Client Secret below
259
+ </div>
260
+ <button
261
+ onClick={() => setShowGoogleSetup(true)}
262
+ style={{ ...modal.saveBtn, width: '100%' }}
263
+ >
264
+ I have my Client ID and Secret
265
+ </button>
266
+ </div>
267
+ )}
268
+
269
+ {oauthStatus === 'not-configured' && showGoogleSetup && (
270
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
271
+ <div>
272
+ <label style={modal.label}>Google OAuth Client ID</label>
273
+ <input
274
+ value={googleClientId}
275
+ onChange={(e) => setGoogleClientId(e.target.value)}
276
+ placeholder="123456789.apps.googleusercontent.com"
277
+ style={modal.input}
278
+ />
279
+ </div>
280
+ <div>
281
+ <label style={modal.label}>Google OAuth Client Secret</label>
282
+ <input
283
+ type="password"
284
+ value={googleClientSecret}
285
+ onChange={(e) => setGoogleClientSecret(e.target.value)}
286
+ placeholder="GOCSPX-..."
287
+ style={modal.input}
288
+ />
289
+ </div>
290
+ <button
291
+ onClick={handleGoogleSetup}
292
+ disabled={saving || !googleClientId || !googleClientSecret}
293
+ style={{
294
+ ...modal.saveBtn, width: '100%',
295
+ opacity: saving || !googleClientId || !googleClientSecret ? 0.4 : 1,
296
+ }}
297
+ >
298
+ {saving ? 'Saving...' : 'Save Google OAuth Credentials'}
299
+ </button>
300
+ <div style={{ fontSize: 9, color: 'var(--text-muted)', textAlign: 'center' }}>
301
+ Stored encrypted, only on this machine. One-time setup for all Google integrations.
302
+ </div>
303
+ </div>
304
+ )}
305
+
306
+ {(oauthStatus === 'ready' || oauthStatus === 'connecting') && (
307
+ <button
308
+ onClick={handleOAuthConnect}
309
+ disabled={oauthStatus === 'connecting'}
310
+ style={{
311
+ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
312
+ width: '100%', padding: '12px 16px',
313
+ background: oauthStatus === 'connecting' ? 'var(--bg-active)' : '#4285f4',
314
+ color: '#fff', border: 'none', borderRadius: 6,
315
+ fontSize: 13, fontWeight: 600, cursor: 'pointer',
316
+ fontFamily: 'var(--font)',
317
+ }}
318
+ >
319
+ {oauthStatus === 'connecting'
320
+ ? 'Waiting for authorization...'
321
+ : `Connect with Google`}
322
+ </button>
323
+ )}
324
+ </div>
325
+ )}
326
+
327
+ {/* API key inputs (only show non-hidden keys) */}
328
+ {envKeys.length > 0 && (
329
+ <div>
330
+ <div style={{
331
+ fontSize: 11, color: 'var(--text-dim)', marginBottom: 12, lineHeight: 1.5,
332
+ }}>
333
+ Paste your credentials below. Values are encrypted and stored locally on this machine only.
334
+ </div>
335
+
336
+ {envKeys.map((ek) => (
337
+ <div key={ek.key} style={{ marginBottom: 14 }}>
338
+ <label style={modal.label}>
339
+ {ek.label || ek.key}
340
+ {ek.required && <span style={{ color: 'var(--red)', marginLeft: 4 }}>*</span>}
341
+ {saved[ek.key] && (
342
+ <span style={{ color: 'var(--green)', marginLeft: 8, fontSize: 10, fontWeight: 500 }}>
343
+ {'\u2713'} saved
344
+ </span>
345
+ )}
346
+ </label>
347
+ <div style={{ display: 'flex', gap: 6 }}>
348
+ <input
349
+ type="password"
350
+ value={values[ek.key] || ''}
351
+ placeholder={ek.placeholder || ek.key}
352
+ onChange={(e) => setValues((prev) => ({ ...prev, [ek.key]: e.target.value }))}
353
+ onKeyDown={(e) => e.key === 'Enter' && handleSave(ek.key)}
354
+ style={modal.input}
355
+ />
356
+ <button
357
+ onClick={() => handleSave(ek.key)}
358
+ disabled={saving || !values[ek.key]}
359
+ style={{
360
+ ...modal.saveBtn,
361
+ opacity: saving || !values[ek.key] ? 0.4 : 1,
362
+ }}
363
+ >
364
+ Save
365
+ </button>
366
+ </div>
367
+ </div>
368
+ ))}
369
+ </div>
370
+ )}
371
+ </div>
372
+ </div>
373
+ </div>
374
+ );
375
+ }
376
+
377
+ // -- Integration Detail Modal --
378
+ function IntegrationDetailModal({ integration, installing, onInstall, onUninstall, onConfigure, onClose }) {
379
+ if (!integration) return null;
380
+
381
+ return (
382
+ <div style={modal.overlay} onClick={onClose}>
383
+ <div style={modal.container} onClick={(e) => e.stopPropagation()}>
384
+ <div style={modal.topBar}>
385
+ <VerifiedBadge item={integration} size="large" />
386
+ <button onClick={onClose} style={modal.closeBtn}>&times;</button>
387
+ </div>
388
+
389
+ {/* Header */}
390
+ <div style={modal.header}>
391
+ <div style={{
392
+ ...modal.icon,
393
+ background: integration.installed && integration.configured
394
+ ? 'var(--green)'
395
+ : integration.installed ? 'var(--amber)' : 'var(--accent)',
396
+ }}>
397
+ {ICON_MAP[integration.icon] || integration.name.charAt(0)}
398
+ </div>
399
+ <div style={modal.headerInfo}>
400
+ <div style={modal.name}>{integration.name}</div>
401
+ <div style={modal.meta}>
402
+ <span style={modal.metaItem}>
403
+ {CATEGORY_ICONS[integration.category] || ''} {CATEGORY_LABELS[integration.category] || integration.category}
404
+ </span>
405
+ <span style={modal.metaItem}>MCP Server</span>
406
+ </div>
407
+ </div>
408
+ </div>
409
+
410
+ {/* Action bar */}
411
+ <div style={modal.actionBar}>
412
+ {integration.installed ? (
413
+ <div style={{ display: 'flex', gap: 8, flex: 1 }}>
414
+ <button
415
+ onClick={() => onConfigure(integration)}
416
+ style={modal.configureBtn}
417
+ disabled={(integration.envKeys || []).length === 0}
418
+ >
419
+ {integration.configured ? 'Reconfigure' : 'Configure'}
420
+ </button>
421
+ <button
422
+ onClick={() => onUninstall(integration.id)}
423
+ disabled={installing === integration.id}
424
+ style={modal.uninstallBtn}
425
+ >
426
+ {installing === integration.id ? 'Removing...' : 'Uninstall'}
427
+ </button>
428
+ </div>
429
+ ) : (
430
+ <button
431
+ onClick={() => onInstall(integration.id)}
432
+ disabled={installing === integration.id}
433
+ style={modal.installBtn}
434
+ >
435
+ {installing === integration.id ? 'Installing...' : 'Install'}
436
+ </button>
437
+ )}
438
+ </div>
439
+
440
+ {/* Status */}
441
+ {integration.installed && (
442
+ <div style={modal.section}>
443
+ <div style={modal.sectionTitle}>Status</div>
444
+ <div style={{
445
+ display: 'flex', alignItems: 'center', gap: 8,
446
+ padding: '8px 12px', borderRadius: 6,
447
+ background: integration.configured ? 'rgba(74, 225, 104, 0.06)' : 'rgba(229, 192, 123, 0.06)',
448
+ border: `1px solid ${integration.configured ? 'var(--green)' : 'var(--amber)'}`,
449
+ }}>
450
+ <span style={{
451
+ width: 8, height: 8, borderRadius: '50%',
452
+ background: integration.configured ? 'var(--green)' : 'var(--amber)',
453
+ }} />
454
+ <span style={{ fontSize: 11, color: integration.configured ? 'var(--green)' : 'var(--amber)' }}>
455
+ {integration.configured ? 'Connected and ready' : 'Credentials needed'}
456
+ </span>
457
+ </div>
458
+ </div>
459
+ )}
460
+
461
+ {/* Description */}
462
+ <div style={modal.section}>
463
+ <div style={modal.sectionTitle}>About</div>
464
+ <div style={modal.description}>{integration.description}</div>
465
+ </div>
466
+
467
+ {/* Required Credentials */}
468
+ {(integration.envKeys || []).length > 0 && (
469
+ <div style={modal.section}>
470
+ <div style={modal.sectionTitle}>Required Credentials</div>
471
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
472
+ {integration.envKeys.map((ek) => (
473
+ <div key={ek.key} style={{
474
+ display: 'flex', alignItems: 'center', gap: 8,
475
+ fontSize: 11, color: 'var(--text-dim)',
476
+ }}>
477
+ <code style={{
478
+ fontSize: 10, padding: '1px 6px', borderRadius: 3,
479
+ background: 'var(--bg-active)', color: 'var(--text-primary)',
480
+ fontFamily: 'var(--font)',
481
+ }}>
482
+ {ek.key}
483
+ </code>
484
+ <span>{ek.label}</span>
485
+ {ek.required && <span style={{ color: 'var(--amber)', fontSize: 9 }}>required</span>}
486
+ </div>
487
+ ))}
488
+ </div>
489
+ </div>
490
+ )}
491
+
492
+ {/* Tags */}
493
+ <div style={modal.section}>
494
+ <div style={modal.sectionTitle}>Tags</div>
495
+ <div style={modal.tagRow}>
496
+ {(integration.tags || []).map((t) => (
497
+ <span key={t} style={modal.tag}>{t}</span>
498
+ ))}
499
+ {(integration.roles || []).map((r) => (
500
+ <span key={r} style={modal.roleTag}>{r}</span>
501
+ ))}
502
+ </div>
503
+ </div>
504
+
505
+ {/* Package Info */}
506
+ {integration.npmPackage && (
507
+ <div style={modal.section}>
508
+ <div style={modal.sectionTitle}>Package</div>
509
+ <code style={{
510
+ fontSize: 11, padding: '6px 10px', borderRadius: 4,
511
+ background: 'var(--bg-active)', color: 'var(--text-primary)',
512
+ display: 'block', fontFamily: 'var(--font)',
513
+ }}>
514
+ {integration.npmPackage}
515
+ </code>
516
+ </div>
517
+ )}
518
+ </div>
519
+ </div>
520
+ );
521
+ }
522
+
523
+ // -- Featured Banner --
524
+ function FeaturedBanner({ integrations, onSelect }) {
525
+ const featured = integrations.filter((s) => s.featured).slice(0, 3);
526
+ if (featured.length === 0) return null;
527
+
528
+ return (
529
+ <div style={styles.featuredSection}>
530
+ <div style={styles.featuredLabel}>Featured Integrations</div>
531
+ <div style={styles.featuredRow}>
532
+ {featured.map((item) => (
533
+ <div
534
+ key={item.id}
535
+ onClick={() => onSelect(item)}
536
+ style={styles.featuredCard}
537
+ onMouseEnter={(e) => { e.currentTarget.style.borderColor = 'var(--accent)'; e.currentTarget.style.transform = 'translateY(-1px)'; }}
538
+ onMouseLeave={(e) => { e.currentTarget.style.borderColor = 'var(--border)'; e.currentTarget.style.transform = 'none'; }}
539
+ >
540
+ <div style={styles.featuredGradient}>
541
+ <div style={{
542
+ ...styles.featuredIcon,
543
+ background: item.installed && item.configured ? 'var(--green)'
544
+ : item.installed ? 'var(--amber)' : 'var(--accent)',
545
+ }}>
546
+ {ICON_MAP[item.icon] || item.name.charAt(0)}
547
+ </div>
548
+ </div>
549
+ <div style={styles.featuredInfo}>
550
+ <div style={styles.featuredName}>{item.name}</div>
551
+ <div style={styles.featuredAuthorRow}>
552
+ <VerifiedBadge item={item} />
553
+ </div>
554
+ <div style={styles.featuredDesc}>{item.description}</div>
555
+ </div>
556
+ {item.installed && item.configured && (
557
+ <div style={styles.connectedBadge}>connected</div>
558
+ )}
559
+ {item.installed && !item.configured && (
560
+ <div style={styles.setupBadge}>needs setup</div>
561
+ )}
562
+ </div>
563
+ ))}
564
+ </div>
565
+ </div>
566
+ );
567
+ }
568
+
569
+ // -- Integration Card --
570
+ function IntegrationCard({ item, onSelect, hovered, onHover }) {
571
+ return (
572
+ <div
573
+ onClick={() => onSelect(item)}
574
+ onMouseEnter={() => onHover(item.id)}
575
+ onMouseLeave={() => onHover(null)}
576
+ style={{
577
+ ...styles.card,
578
+ borderColor: item.installed && item.configured
579
+ ? 'var(--green)'
580
+ : item.installed ? 'var(--amber)'
581
+ : hovered ? 'var(--accent)' : 'var(--border)',
582
+ background: hovered ? 'var(--bg-hover)' : 'var(--bg-surface)',
583
+ transform: hovered ? 'translateY(-1px)' : 'none',
584
+ }}
585
+ >
586
+ {/* Top row */}
587
+ <div style={styles.cardTop}>
588
+ <div style={{
589
+ ...styles.cardIcon,
590
+ background: item.installed && item.configured
591
+ ? 'var(--green)'
592
+ : item.installed ? 'var(--amber)' : 'var(--accent)',
593
+ }}>
594
+ {ICON_MAP[item.icon] || item.name.charAt(0)}
595
+ </div>
596
+ <div style={styles.cardInfo}>
597
+ <div style={styles.cardName}>{item.name}</div>
598
+ <div style={styles.cardAuthorRow}>
599
+ <VerifiedBadge item={item} />
600
+ </div>
601
+ </div>
602
+ {item.installed && item.configured && (
603
+ <div style={styles.connectedBadgeSm}>connected</div>
604
+ )}
605
+ {item.installed && !item.configured && (
606
+ <div style={styles.setupBadgeSm}>setup</div>
607
+ )}
608
+ {!item.installed && (
609
+ <div style={styles.freeBadge}>MCP</div>
610
+ )}
611
+ </div>
612
+
613
+ {/* Description */}
614
+ <div style={styles.cardDesc}>{item.description}</div>
615
+
616
+ {/* Bottom */}
617
+ <div style={styles.cardBottom}>
618
+ <div style={styles.cardStats}>
619
+ {(item.envKeys || []).length > 0 && (
620
+ <span style={styles.cardCredCount}>
621
+ {item.envKeys.length} credential{item.envKeys.length > 1 ? 's' : ''}
622
+ </span>
623
+ )}
624
+ </div>
625
+ <span style={styles.catTag}>
626
+ {CATEGORY_ICONS[item.category] || ''} {CATEGORY_LABELS[item.category] || item.category}
627
+ </span>
628
+ </div>
629
+ </div>
630
+ );
631
+ }
632
+
633
+ // -- Main Store --
634
+ export default function IntegrationsStore() {
635
+ const [integrations, setIntegrations] = useState([]);
636
+ const [categories, setCategories] = useState([]);
637
+ const [search, setSearch] = useState('');
638
+ const [category, setCategory] = useState('all');
639
+ const [sortBy, setSortBy] = useState('popular');
640
+ const [tab, setTab] = useState('browse');
641
+ const [loading, setLoading] = useState(true);
642
+ const [installing, setInstalling] = useState(null);
643
+ const [selectedItem, setSelectedItem] = useState(null);
644
+ const [configuring, setConfiguring] = useState(null);
645
+ const [hovered, setHovered] = useState(null);
646
+
647
+ const fetchIntegrations = useCallback(async () => {
648
+ setLoading(true);
649
+ try {
650
+ const params = new URLSearchParams();
651
+ if (search) params.set('search', search);
652
+ if (category !== 'all') params.set('category', category);
653
+ const res = await fetch(`/api/integrations/registry?${params}`);
654
+ const data = await res.json();
655
+ setIntegrations(data.integrations || []);
656
+ setCategories(data.categories || []);
657
+ } catch { /* ignore */ }
658
+ setLoading(false);
659
+ }, [search, category]);
660
+
661
+ useEffect(() => { fetchIntegrations(); }, [fetchIntegrations]);
662
+
663
+ async function handleInstall(id) {
664
+ setInstalling(id);
665
+ try {
666
+ await fetch(`/api/integrations/${id}/install`, { method: 'POST' });
667
+ await fetchIntegrations();
668
+ // After install, refresh selected item
669
+ if (selectedItem?.id === id) {
670
+ const updated = integrations.find((s) => s.id === id);
671
+ if (updated) setSelectedItem({ ...updated, installed: true });
672
+ }
673
+ } catch { /* ignore */ }
674
+ setInstalling(null);
675
+ }
676
+
677
+ async function handleUninstall(id) {
678
+ setInstalling(id);
679
+ try {
680
+ await fetch(`/api/integrations/${id}`, { method: 'DELETE' });
681
+ await fetchIntegrations();
682
+ if (selectedItem?.id === id) {
683
+ setSelectedItem((prev) => prev ? { ...prev, installed: false, configured: false } : null);
684
+ }
685
+ } catch { /* ignore */ }
686
+ setInstalling(null);
687
+ }
688
+
689
+ function handleSelect(item) {
690
+ setSelectedItem(item);
691
+ }
692
+
693
+ function handleConfigure(item) {
694
+ setSelectedItem(null);
695
+ setConfiguring(item);
696
+ }
697
+
698
+ function handleConfigureClose() {
699
+ setConfiguring(null);
700
+ fetchIntegrations();
701
+ }
702
+
703
+ const installedItems = integrations.filter((s) => s.installed);
704
+ const displayItems = tab === 'installed'
705
+ ? sortIntegrations(installedItems, sortBy)
706
+ : sortIntegrations(integrations, sortBy);
707
+
708
+ return (
709
+ <div style={styles.root}>
710
+ {/* Header */}
711
+ <div style={styles.headerBar}>
712
+ <div style={styles.headerLeft}>
713
+ <div style={styles.title}>Integrations</div>
714
+ <div style={styles.headerTabs}>
715
+ <button
716
+ onClick={() => setTab('browse')}
717
+ style={{
718
+ ...styles.headerTab,
719
+ color: tab === 'browse' ? 'var(--text-bright)' : 'var(--text-dim)',
720
+ borderBottom: tab === 'browse' ? '2px solid var(--accent)' : '2px solid transparent',
721
+ }}
722
+ >
723
+ Browse
724
+ </button>
725
+ <button
726
+ onClick={() => setTab('installed')}
727
+ style={{
728
+ ...styles.headerTab,
729
+ color: tab === 'installed' ? 'var(--text-bright)' : 'var(--text-dim)',
730
+ borderBottom: tab === 'installed' ? '2px solid var(--green)' : '2px solid transparent',
731
+ }}
732
+ >
733
+ Installed ({installedItems.length})
734
+ </button>
735
+ </div>
736
+ </div>
737
+ <div style={styles.headerRight}>
738
+ <span style={styles.headerCount}>
739
+ {integrations.length} integrations
740
+ </span>
741
+ </div>
742
+ </div>
743
+
744
+ {/* Toolbar */}
745
+ <div style={styles.toolbar}>
746
+ <div style={styles.searchRow}>
747
+ <div style={styles.searchWrap}>
748
+ <span style={styles.searchIcon}>{'\u2315'}</span>
749
+ <input
750
+ style={styles.search}
751
+ placeholder="Search integrations..."
752
+ value={search}
753
+ onChange={(e) => setSearch(e.target.value)}
754
+ />
755
+ </div>
756
+ <div style={styles.sortWrap}>
757
+ {SORT_OPTIONS.map((opt) => (
758
+ <button
759
+ key={opt.id}
760
+ onClick={() => setSortBy(opt.id)}
761
+ style={{
762
+ ...styles.sortBtn,
763
+ color: sortBy === opt.id ? 'var(--text-bright)' : 'var(--text-muted)',
764
+ background: sortBy === opt.id ? 'var(--bg-active)' : 'transparent',
765
+ }}
766
+ >
767
+ {opt.label}
768
+ </button>
769
+ ))}
770
+ </div>
771
+ </div>
772
+ {tab === 'browse' && (
773
+ <div style={styles.catRow}>
774
+ <button
775
+ onClick={() => setCategory('all')}
776
+ style={{
777
+ ...styles.catBtn,
778
+ ...(category === 'all' ? styles.catBtnActive : {}),
779
+ }}
780
+ >
781
+ All
782
+ </button>
783
+ {categories.map((cat) => (
784
+ <button
785
+ key={cat.id}
786
+ onClick={() => setCategory(cat.id)}
787
+ style={{
788
+ ...styles.catBtn,
789
+ ...(category === cat.id ? styles.catBtnActive : {}),
790
+ }}
791
+ >
792
+ {CATEGORY_ICONS[cat.id] || ''} {CATEGORY_LABELS[cat.id] || cat.id} ({cat.count})
793
+ </button>
794
+ ))}
795
+ </div>
796
+ )}
797
+ </div>
798
+
799
+ {/* Content */}
800
+ <div style={styles.scrollArea}>
801
+ {tab === 'browse' && !search && category === 'all' && (
802
+ <FeaturedBanner integrations={integrations} onSelect={handleSelect} />
803
+ )}
804
+
805
+ {loading && integrations.length === 0 && (
806
+ <div style={styles.empty}>Loading integrations...</div>
807
+ )}
808
+
809
+ {!loading && displayItems.length === 0 && (
810
+ <div style={styles.empty}>
811
+ {tab === 'installed'
812
+ ? 'No integrations installed yet. Browse to connect your tools.'
813
+ : 'No integrations match your search.'}
814
+ </div>
815
+ )}
816
+
817
+ <div style={styles.grid}>
818
+ {displayItems.map((item) => (
819
+ <IntegrationCard
820
+ key={item.id}
821
+ item={item}
822
+ onSelect={handleSelect}
823
+ hovered={hovered === item.id}
824
+ onHover={setHovered}
825
+ />
826
+ ))}
827
+ </div>
828
+ </div>
829
+
830
+ {/* Detail Modal */}
831
+ <IntegrationDetailModal
832
+ integration={selectedItem}
833
+ installing={installing}
834
+ onInstall={handleInstall}
835
+ onUninstall={handleUninstall}
836
+ onConfigure={handleConfigure}
837
+ onClose={() => setSelectedItem(null)}
838
+ />
839
+
840
+ {/* Credential Setup Modal */}
841
+ <CredentialModal
842
+ integration={configuring}
843
+ onClose={handleConfigureClose}
844
+ />
845
+ </div>
846
+ );
847
+ }
848
+
849
+ // -- Styles --
850
+
851
+ const styles = {
852
+ root: {
853
+ height: '100%', display: 'flex', flexDirection: 'column',
854
+ overflow: 'hidden',
855
+ },
856
+ headerBar: {
857
+ padding: '12px 20px 0',
858
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
859
+ flexShrink: 0,
860
+ },
861
+ headerLeft: {
862
+ display: 'flex', alignItems: 'center', gap: 20,
863
+ },
864
+ title: {
865
+ fontSize: 15, fontWeight: 700, color: 'var(--text-bright)',
866
+ letterSpacing: 0.3,
867
+ },
868
+ headerTabs: {
869
+ display: 'flex', gap: 0,
870
+ },
871
+ headerTab: {
872
+ padding: '6px 14px',
873
+ background: 'none', border: 'none',
874
+ fontSize: 12, fontWeight: 500,
875
+ fontFamily: 'var(--font)', cursor: 'pointer',
876
+ transition: 'color 0.1s',
877
+ },
878
+ headerRight: {
879
+ display: 'flex', alignItems: 'center', gap: 10,
880
+ },
881
+ headerCount: {
882
+ fontSize: 11, color: 'var(--text-muted)',
883
+ },
884
+ toolbar: {
885
+ padding: '10px 20px',
886
+ flexShrink: 0,
887
+ },
888
+ searchRow: {
889
+ display: 'flex', gap: 10, alignItems: 'center',
890
+ },
891
+ searchWrap: {
892
+ flex: 1, position: 'relative',
893
+ },
894
+ searchIcon: {
895
+ position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)',
896
+ color: 'var(--text-muted)', fontSize: 13, pointerEvents: 'none',
897
+ },
898
+ search: {
899
+ width: '100%', padding: '7px 12px 7px 30px',
900
+ background: 'var(--bg-surface)', border: '1px solid var(--border)',
901
+ borderRadius: 6, color: 'var(--text-primary)', fontSize: 12,
902
+ fontFamily: 'var(--font)', outline: 'none',
903
+ },
904
+ sortWrap: {
905
+ display: 'flex', gap: 2,
906
+ },
907
+ sortBtn: {
908
+ padding: '5px 10px',
909
+ background: 'transparent', border: '1px solid transparent',
910
+ borderRadius: 4, fontSize: 10, fontWeight: 500,
911
+ fontFamily: 'var(--font)', cursor: 'pointer',
912
+ transition: 'all 0.1s',
913
+ },
914
+ catRow: {
915
+ display: 'flex', gap: 4, marginTop: 8, flexWrap: 'wrap',
916
+ },
917
+ catBtn: {
918
+ padding: '4px 12px',
919
+ background: 'transparent', border: '1px solid var(--border)',
920
+ borderRadius: 14, color: 'var(--text-dim)', fontSize: 10,
921
+ fontFamily: 'var(--font)', cursor: 'pointer',
922
+ transition: 'all 0.1s',
923
+ },
924
+ catBtnActive: {
925
+ borderColor: 'var(--accent)', color: 'var(--accent)',
926
+ background: 'rgba(51, 175, 188, 0.08)',
927
+ },
928
+ scrollArea: {
929
+ flex: 1, overflowY: 'auto', padding: '4px 20px 20px',
930
+ },
931
+
932
+ // Featured
933
+ featuredSection: { marginBottom: 16 },
934
+ featuredLabel: {
935
+ fontSize: 11, fontWeight: 700, color: 'var(--text-dim)',
936
+ textTransform: 'uppercase', letterSpacing: 1, marginBottom: 8,
937
+ },
938
+ featuredRow: {
939
+ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 8,
940
+ },
941
+ featuredCard: {
942
+ padding: '14px',
943
+ background: 'var(--bg-surface)', border: '1px solid var(--border)',
944
+ borderRadius: 8, cursor: 'pointer',
945
+ transition: 'border-color 0.15s, transform 0.15s',
946
+ position: 'relative', overflow: 'hidden',
947
+ },
948
+ featuredGradient: {
949
+ display: 'flex', alignItems: 'center', marginBottom: 10,
950
+ },
951
+ featuredIcon: {
952
+ width: 40, height: 40, borderRadius: 10,
953
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
954
+ fontSize: 16, fontWeight: 700, color: 'var(--bg-base)',
955
+ },
956
+ featuredInfo: { flex: 1 },
957
+ featuredName: {
958
+ fontSize: 13, fontWeight: 700, color: 'var(--text-bright)',
959
+ },
960
+ featuredAuthorRow: {
961
+ display: 'flex', alignItems: 'center', gap: 6, marginTop: 2,
962
+ },
963
+ featuredDesc: {
964
+ fontSize: 10, color: 'var(--text-dim)', marginTop: 6,
965
+ lineHeight: 1.45, display: '-webkit-box',
966
+ WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden',
967
+ },
968
+ connectedBadge: {
969
+ position: 'absolute', top: 8, right: 8,
970
+ fontSize: 8, fontWeight: 600, color: 'var(--green)',
971
+ border: '1px solid var(--green)', borderRadius: 3,
972
+ padding: '1px 5px', textTransform: 'uppercase', letterSpacing: 0.5,
973
+ },
974
+ setupBadge: {
975
+ position: 'absolute', top: 8, right: 8,
976
+ fontSize: 8, fontWeight: 600, color: 'var(--amber)',
977
+ border: '1px solid var(--amber)', borderRadius: 3,
978
+ padding: '1px 5px', textTransform: 'uppercase', letterSpacing: 0.5,
979
+ },
980
+
981
+ // Grid
982
+ grid: {
983
+ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 8,
984
+ },
985
+ empty: {
986
+ padding: '60px 0', textAlign: 'center',
987
+ color: 'var(--text-dim)', fontSize: 12,
988
+ gridColumn: '1 / -1',
989
+ },
990
+
991
+ // Card
992
+ card: {
993
+ padding: '14px',
994
+ background: 'var(--bg-surface)', border: '1px solid var(--border)',
995
+ borderRadius: 8, cursor: 'pointer',
996
+ transition: 'border-color 0.12s, background 0.12s, transform 0.12s',
997
+ display: 'flex', flexDirection: 'column',
998
+ },
999
+ cardTop: {
1000
+ display: 'flex', alignItems: 'center', gap: 10,
1001
+ },
1002
+ cardIcon: {
1003
+ width: 36, height: 36, borderRadius: 8,
1004
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
1005
+ fontSize: 14, fontWeight: 700, color: 'var(--bg-base)',
1006
+ flexShrink: 0,
1007
+ },
1008
+ cardInfo: {
1009
+ flex: 1, minWidth: 0,
1010
+ },
1011
+ cardName: {
1012
+ fontSize: 12, fontWeight: 600, color: 'var(--text-bright)',
1013
+ },
1014
+ cardAuthorRow: {
1015
+ display: 'flex', alignItems: 'center', gap: 6, marginTop: 2,
1016
+ },
1017
+ connectedBadgeSm: {
1018
+ fontSize: 8, fontWeight: 600, color: 'var(--green)',
1019
+ border: '1px solid var(--green)', borderRadius: 3,
1020
+ padding: '1px 6px', textTransform: 'uppercase', letterSpacing: 0.5,
1021
+ flexShrink: 0,
1022
+ },
1023
+ setupBadgeSm: {
1024
+ fontSize: 8, fontWeight: 600, color: 'var(--amber)',
1025
+ border: '1px solid var(--amber)', borderRadius: 3,
1026
+ padding: '1px 6px', textTransform: 'uppercase', letterSpacing: 0.5,
1027
+ flexShrink: 0,
1028
+ },
1029
+ freeBadge: {
1030
+ fontSize: 9, fontWeight: 500, color: 'var(--text-muted)',
1031
+ flexShrink: 0,
1032
+ },
1033
+ cardDesc: {
1034
+ fontSize: 11, color: 'var(--text-dim)', marginTop: 10,
1035
+ lineHeight: 1.5, flex: 1,
1036
+ display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical',
1037
+ overflow: 'hidden',
1038
+ },
1039
+ cardBottom: {
1040
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
1041
+ marginTop: 10, paddingTop: 8,
1042
+ borderTop: '1px solid var(--border)',
1043
+ },
1044
+ cardStats: {
1045
+ display: 'flex', gap: 10,
1046
+ },
1047
+ cardCredCount: {
1048
+ fontSize: 10, color: 'var(--text-muted)',
1049
+ },
1050
+ catTag: {
1051
+ fontSize: 9, padding: '2px 8px', borderRadius: 4,
1052
+ background: 'var(--bg-active)', color: 'var(--text-dim)',
1053
+ fontWeight: 500,
1054
+ },
1055
+ };
1056
+
1057
+ // -- Modal Styles --
1058
+ const modal = {
1059
+ overlay: {
1060
+ position: 'fixed', inset: 0,
1061
+ background: 'rgba(0, 0, 0, 0.6)',
1062
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
1063
+ zIndex: 1000,
1064
+ backdropFilter: 'blur(2px)',
1065
+ },
1066
+ container: {
1067
+ width: '90%', maxWidth: 520, maxHeight: '85vh',
1068
+ background: 'var(--bg-chrome)', border: '1px solid var(--border)',
1069
+ borderRadius: 10, overflowY: 'auto',
1070
+ padding: '0 24px 24px', position: 'relative',
1071
+ },
1072
+ topBar: {
1073
+ position: 'sticky', top: 0,
1074
+ background: 'var(--bg-chrome)',
1075
+ padding: '16px 0 8px',
1076
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
1077
+ zIndex: 1,
1078
+ },
1079
+ closeBtn: {
1080
+ background: 'none', border: 'none',
1081
+ color: 'var(--text-muted)', fontSize: 20,
1082
+ cursor: 'pointer', padding: '2px 6px',
1083
+ fontFamily: 'var(--font)',
1084
+ },
1085
+ header: {
1086
+ display: 'flex', gap: 14, alignItems: 'flex-start',
1087
+ marginBottom: 16,
1088
+ },
1089
+ icon: {
1090
+ width: 52, height: 52, borderRadius: 12,
1091
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
1092
+ fontSize: 20, fontWeight: 700, color: 'var(--bg-base)',
1093
+ flexShrink: 0,
1094
+ },
1095
+ headerInfo: { flex: 1 },
1096
+ name: {
1097
+ fontSize: 17, fontWeight: 700, color: 'var(--text-bright)',
1098
+ },
1099
+ meta: {
1100
+ display: 'flex', gap: 10, marginTop: 6, flexWrap: 'wrap',
1101
+ },
1102
+ metaItem: {
1103
+ fontSize: 11, color: 'var(--text-dim)',
1104
+ },
1105
+ actionBar: {
1106
+ display: 'flex', alignItems: 'center', gap: 8,
1107
+ padding: '12px 0', borderTop: '1px solid var(--border)',
1108
+ borderBottom: '1px solid var(--border)',
1109
+ marginBottom: 16,
1110
+ },
1111
+ installBtn: {
1112
+ flex: 1, padding: '8px 16px',
1113
+ background: 'var(--accent)', color: 'var(--bg-base)',
1114
+ border: 'none', borderRadius: 6,
1115
+ fontSize: 12, fontWeight: 600, cursor: 'pointer',
1116
+ fontFamily: 'var(--font)',
1117
+ },
1118
+ configureBtn: {
1119
+ flex: 1, padding: '8px 16px',
1120
+ background: 'var(--bg-active)', color: 'var(--text-bright)',
1121
+ border: '1px solid var(--border)', borderRadius: 6,
1122
+ fontSize: 12, fontWeight: 600, cursor: 'pointer',
1123
+ fontFamily: 'var(--font)',
1124
+ },
1125
+ uninstallBtn: {
1126
+ padding: '8px 16px',
1127
+ background: 'transparent', color: 'var(--red)',
1128
+ border: '1px solid var(--red)', borderRadius: 6,
1129
+ fontSize: 12, fontWeight: 500, cursor: 'pointer',
1130
+ fontFamily: 'var(--font)',
1131
+ },
1132
+ section: {
1133
+ marginBottom: 16,
1134
+ },
1135
+ sectionTitle: {
1136
+ fontSize: 11, fontWeight: 700, color: 'var(--text-dim)',
1137
+ textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 8,
1138
+ },
1139
+ description: {
1140
+ fontSize: 12, color: 'var(--text-primary)', lineHeight: 1.6,
1141
+ },
1142
+ tagRow: {
1143
+ display: 'flex', gap: 4, flexWrap: 'wrap',
1144
+ },
1145
+ tag: {
1146
+ fontSize: 10, padding: '2px 8px', borderRadius: 4,
1147
+ background: 'var(--bg-active)', color: 'var(--text-dim)',
1148
+ },
1149
+ roleTag: {
1150
+ fontSize: 10, padding: '2px 8px', borderRadius: 4,
1151
+ background: 'rgba(51, 175, 188, 0.08)', color: 'var(--accent)',
1152
+ border: '1px solid rgba(51, 175, 188, 0.2)',
1153
+ },
1154
+ label: {
1155
+ display: 'block', fontSize: 11, fontWeight: 600,
1156
+ color: 'var(--text-primary)', marginBottom: 4,
1157
+ },
1158
+ input: {
1159
+ flex: 1, padding: '7px 10px',
1160
+ background: 'var(--bg-surface)', border: '1px solid var(--border)',
1161
+ borderRadius: 5, color: 'var(--text-primary)', fontSize: 12,
1162
+ fontFamily: 'var(--font)', outline: 'none',
1163
+ },
1164
+ saveBtn: {
1165
+ padding: '7px 14px',
1166
+ background: 'var(--accent)', color: 'var(--bg-base)',
1167
+ border: 'none', borderRadius: 5,
1168
+ fontSize: 11, fontWeight: 600, cursor: 'pointer',
1169
+ fontFamily: 'var(--font)',
1170
+ },
1171
+ };