@nuxtagentic/integrations 0.0.0-stage → 0.1.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 (51) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +175 -2
  3. package/client.ts +124 -0
  4. package/module.ts +44 -0
  5. package/package.json +52 -3
  6. package/runtime/assets/logos/discord.svg +1 -0
  7. package/runtime/assets/logos/github.svg +1 -0
  8. package/runtime/assets/logos/gmail.svg +1 -0
  9. package/runtime/assets/logos/googleads.svg +1 -0
  10. package/runtime/assets/logos/googleanalytics.svg +1 -0
  11. package/runtime/assets/logos/googlecalendar.svg +1 -0
  12. package/runtime/assets/logos/googledrive.svg +1 -0
  13. package/runtime/assets/logos/googlesearchconsole.svg +1 -0
  14. package/runtime/assets/logos/hubspot.svg +1 -0
  15. package/runtime/assets/logos/intercom.svg +1 -0
  16. package/runtime/assets/logos/linear.svg +1 -0
  17. package/runtime/assets/logos/mcp.svg +1 -0
  18. package/runtime/assets/logos/meta.svg +1 -0
  19. package/runtime/assets/logos/notion.svg +1 -0
  20. package/runtime/assets/logos/reddit.svg +1 -0
  21. package/runtime/assets/logos/sentry.svg +1 -0
  22. package/runtime/assets/logos/shopify.svg +1 -0
  23. package/runtime/assets/logos/slack.svg +1 -0
  24. package/runtime/assets/logos/stripe.svg +1 -0
  25. package/runtime/assets/logos/tiktok-dark.svg +1 -0
  26. package/runtime/assets/logos/tiktok.svg +1 -0
  27. package/runtime/assets/logos/vercel.svg +1 -0
  28. package/runtime/assets/logos/x.svg +1 -0
  29. package/runtime/components/IntegrationCard.vue +82 -0
  30. package/runtime/components/IntegrationCatalog.vue +130 -0
  31. package/runtime/components/IntegrationConnectionPanel.vue +589 -0
  32. package/runtime/components/IntegrationLogo.vue +41 -0
  33. package/runtime/pages/integrations.vue +481 -0
  34. package/runtime/server/api/integrations/approvals/[id].post.ts +69 -0
  35. package/runtime/server/api/integrations/catalog/[provider]/action.post.ts +35 -0
  36. package/runtime/server/api/integrations/catalog/[provider]/connection.post.ts +242 -0
  37. package/runtime/server/api/integrations/catalog/[provider]/tools.get.ts +40 -0
  38. package/runtime/server/api/integrations/catalog/index.get.ts +48 -0
  39. package/runtime/server/api/integrations/catalog/tools.get.ts +27 -0
  40. package/runtime/server/plugins/connect-maintenance.ts +30 -0
  41. package/runtime/server/utils/approval-continuation.ts +37 -0
  42. package/runtime/server/utils/google-mcp.ts +296 -0
  43. package/runtime/server/utils/integration-adapters.ts +836 -0
  44. package/runtime/server/utils/integration-context.ts +510 -0
  45. package/runtime/server/utils/integration-crypto.ts +13 -0
  46. package/runtime/server/utils/request-body.ts +38 -0
  47. package/runtime/shared/agent.ts +14 -0
  48. package/runtime/shared/catalog.ts +566 -0
  49. package/runtime/shared/google-mcp.ts +8 -0
  50. package/runtime/workspace.ts +15 -0
  51. package/schema.ts +179 -0
@@ -0,0 +1,589 @@
1
+ <script setup lang="ts">
2
+ import { computed, useTemplateRef } from 'vue'
3
+ import type {
4
+ IntegrationConnection,
5
+ IntegrationProvider,
6
+ McpServerTool,
7
+ PendingAccount,
8
+ } from '../../client'
9
+ import type { IntegrationDefinition, IntegrationScope } from '../shared/catalog'
10
+ import IntegrationLogo from './IntegrationLogo.vue'
11
+
12
+ const permissions = defineModel<string[]>('permissions', { required: true })
13
+ const approvals = defineModel<string[]>('approvals', { default: () => [] })
14
+ const credentials = defineModel<Record<string, string>>('credentials', { required: true })
15
+ const settings = defineModel<Record<string, string>>('settings', { default: () => ({}) })
16
+ const scope = defineModel<IntegrationScope>('scope', { required: true })
17
+ const server = defineModel<string>('server', { default: '' })
18
+ const {
19
+ integration,
20
+ connections,
21
+ active,
22
+ adding,
23
+ provider,
24
+ manager,
25
+ organization,
26
+ pending,
27
+ tools,
28
+ endpoint,
29
+ busy,
30
+ message,
31
+ } = defineProps<{
32
+ integration: IntegrationDefinition
33
+ /** This integration's connections the viewer can see. */
34
+ connections: IntegrationConnection[]
35
+ /** The connection being managed; none means the account list or the add form. */
36
+ active?: IntegrationConnection
37
+ adding?: boolean
38
+ provider?: IntegrationProvider
39
+ manager: boolean
40
+ organization: boolean
41
+ pending?: PendingAccount
42
+ tools?: McpServerTool[]
43
+ endpoint?: { url: string; secret: string }
44
+ busy?: boolean
45
+ message?: string
46
+ }>()
47
+ const emit = defineEmits<{
48
+ close: []
49
+ add: []
50
+ back: []
51
+ manage: [connection: IntegrationConnection]
52
+ connect: []
53
+ apiKey: []
54
+ save: []
55
+ disconnect: [connection: IntegrationConnection]
56
+ reconnect: [connection: IntegrationConnection]
57
+ endpoint: [rotate: boolean]
58
+ copy: [value: string]
59
+ choose: [target: string]
60
+ }>()
61
+ const element = useTemplateRef('element')
62
+ defineExpose({ element })
63
+
64
+ const auth = computed(() => integration.auth)
65
+ const oauthReady = computed(() => !!auth.value.oauth && !!provider?.configured)
66
+ /** Without a registered OAuth app, providers that take a token still connect. */
67
+ const tokenForm = computed(() => !!auth.value.apiKey && !oauthReady.value)
68
+ const mode = computed(() => {
69
+ if (pending?.targets?.length) {
70
+ return 'choose'
71
+ }
72
+ if (active) {
73
+ return 'manage'
74
+ }
75
+ if (adding || !connections.length) {
76
+ return 'add'
77
+ }
78
+ return 'list'
79
+ })
80
+ const canShare = computed(() => organization && integration.ownership.includes('workspace'))
81
+ const canPersonal = computed(() => integration.ownership.includes('user'))
82
+ const settingFields = computed(() =>
83
+ integration.fields.filter((field) => !field.secret && !field.apiKeyOnly),
84
+ )
85
+ const tokenFields = computed(() => integration.fields)
86
+ const actionItems = computed(() =>
87
+ auth.value.mcp
88
+ ? (tools ?? []).map((tool) => ({
89
+ id: tool.name,
90
+ label: tool.title,
91
+ hint: tool.description,
92
+ write: tool.write,
93
+ }))
94
+ : integration.actions.map((action) => ({ ...action, hint: '', write: false })),
95
+ )
96
+ const isReadOnly = computed(() => !!active && !active.canManage)
97
+ const setupCommand = computed(() =>
98
+ auth.value.oauth ? `pnpm exec cloudconfig connect add ${auth.value.oauth.provider}` : '',
99
+ )
100
+ const accountSummary = computed(() => {
101
+ if (active) {
102
+ const { label, account, scope } = active
103
+ return `${label || account} · ${scopeLabel(scope)}`
104
+ }
105
+ if (connections.length) {
106
+ return `${connections.length} connected ${connections.length === 1 ? 'account' : 'accounts'}`
107
+ }
108
+ return integration.category
109
+ })
110
+ const scopeDescription = computed(() => {
111
+ if (scope.value === 'workspace') {
112
+ return 'Shared with every member’s agents. It keeps working when you leave.'
113
+ }
114
+ return manager || !canShare.value
115
+ ? 'Only your own agents and automations can use it.'
116
+ : 'Only your own agents can use it. Admins add workspace connections.'
117
+ })
118
+ const permissionDescription = computed(() => {
119
+ if (isReadOnly.value) {
120
+ return 'Workspace admins manage this connection.'
121
+ }
122
+ if (auth.value.mcp) {
123
+ return 'Tools marked “Ask first” wait for your approval.'
124
+ }
125
+ switch (integration.id) {
126
+ case 'stripe':
127
+ return 'Refunds require your approval.'
128
+ case 'intercom':
129
+ return 'Reply drafts require your approval before sending.'
130
+ default:
131
+ return 'Only the actions you allow can be used by agents.'
132
+ }
133
+ })
134
+
135
+ function getConnectionStatusClass({ status, enabled }: IntegrationConnection) {
136
+ if (status === 'error') {
137
+ return 'bg-error'
138
+ }
139
+ return enabled ? 'bg-success' : 'bg-(--gray-600)'
140
+ }
141
+
142
+ function toggle(list: string[], id: string, value: boolean | 'indeterminate') {
143
+ return value === true ? [...new Set([...list, id])] : list.filter((entry) => entry !== id)
144
+ }
145
+ function scopeLabel(value: IntegrationScope) {
146
+ return value === 'workspace' ? 'Workspace' : 'Just me'
147
+ }
148
+ </script>
149
+
150
+ <template>
151
+ <div
152
+ ref="element"
153
+ class="flex max-h-[min(760px,calc(100dvh-48px))] flex-col"
154
+ >
155
+ <div class="flex shrink-0 items-start gap-3.5 px-5 pt-5">
156
+ <UButton
157
+ v-if="mode === 'manage' || (mode === 'add' && connections.length)"
158
+ icon="i-lucide-arrow-left"
159
+ aria-label="Back to accounts"
160
+ color="neutral"
161
+ variant="ghost"
162
+ class="-ms-1.5 mt-2 size-8 rounded-md"
163
+ @click="emit('back')"
164
+ />
165
+ <span
166
+ class="grid size-12 shrink-0 place-items-center rounded-xl border border-(--gray-400) bg-(--gray-300)"
167
+ >
168
+ <IntegrationLogo
169
+ :name="integration.logo"
170
+ class="size-6"
171
+ />
172
+ </span>
173
+ <div class="min-w-0 flex-1">
174
+ <h2 class="text-lg leading-6 font-medium">{{ integration.name }}</h2>
175
+ <p class="mt-1 truncate text-[13px] text-dimmed">
176
+ {{ accountSummary }}
177
+ </p>
178
+ </div>
179
+ <UButton
180
+ icon="i-lucide-x"
181
+ aria-label="Close connection"
182
+ color="neutral"
183
+ variant="ghost"
184
+ class="size-8 rounded-md"
185
+ @click="emit('close')"
186
+ />
187
+ </div>
188
+
189
+ <div class="flex min-h-0 flex-col gap-5 overflow-y-auto px-5 py-4">
190
+ <p
191
+ v-if="mode !== 'manage'"
192
+ class="text-sm leading-5 text-toned"
193
+ >
194
+ {{ integration.details }}
195
+ </p>
196
+
197
+ <!-- Accounts -->
198
+ <div
199
+ v-if="mode === 'list'"
200
+ class="flex flex-col gap-2"
201
+ >
202
+ <span class="text-xs text-dimmed">Connected accounts</span>
203
+ <div class="divide-y divide-(--gray-400) rounded-[10px] border border-(--gray-400)">
204
+ <button
205
+ v-for="entry in connections"
206
+ :key="entry.id"
207
+ type="button"
208
+ class="flex min-h-12 w-full items-center gap-3 px-3 text-start hover:bg-(--gray-300) focus-visible:outline-2"
209
+ @click="emit('manage', entry)"
210
+ >
211
+ <span
212
+ class="size-1.5 shrink-0 rounded-full"
213
+ :class="getConnectionStatusClass(entry)"
214
+ />
215
+ <span class="min-w-0 flex-1">
216
+ <span class="block truncate text-[13px] text-highlighted">
217
+ {{ entry.label || entry.account }}
218
+ </span>
219
+ <span class="block truncate text-xs text-dimmed">
220
+ {{
221
+ entry.status === 'error'
222
+ ? 'Needs to be reconnected'
223
+ : `${entry.permissions.length} allowed · ${entry.mine ? 'added by you' : 'shared'}`
224
+ }}
225
+ </span>
226
+ </span>
227
+ <UBadge
228
+ :label="scopeLabel(entry.scope)"
229
+ color="neutral"
230
+ variant="subtle"
231
+ size="sm"
232
+ />
233
+ <UIcon
234
+ name="i-lucide-chevron-right"
235
+ class="size-4 text-dimmed"
236
+ />
237
+ </button>
238
+ </div>
239
+ </div>
240
+
241
+ <!-- Choose an installation or site -->
242
+ <div
243
+ v-if="mode === 'choose' && pending"
244
+ class="flex flex-col gap-2"
245
+ >
246
+ <span class="text-xs text-dimmed">Choose what this connection covers</span>
247
+ <div class="divide-y divide-(--gray-400) rounded-[10px] border border-(--gray-400)">
248
+ <button
249
+ v-for="target in pending.targets"
250
+ :key="target.id"
251
+ type="button"
252
+ class="flex min-h-11 w-full items-center gap-3 px-3 text-start hover:bg-(--gray-300) focus-visible:outline-2"
253
+ :disabled="busy"
254
+ @click="emit('choose', target.id)"
255
+ >
256
+ <span class="min-w-0 flex-1 truncate text-[13px] text-highlighted">
257
+ {{ target.name }}
258
+ </span>
259
+ <span class="truncate text-xs text-dimmed">{{ target.hint }}</span>
260
+ </button>
261
+ </div>
262
+ </div>
263
+
264
+ <!-- Add an account -->
265
+ <template v-if="mode === 'add'">
266
+ <div
267
+ v-if="integration.triggers.length"
268
+ class="flex flex-col gap-2"
269
+ >
270
+ <span class="text-xs text-dimmed">Can start an agent when</span>
271
+ <div class="divide-y divide-(--gray-400) rounded-[10px] border border-(--gray-400)">
272
+ <div
273
+ v-for="trigger in integration.triggers"
274
+ :key="trigger.id"
275
+ class="flex min-h-10 items-center gap-2.5 px-3 text-[13px]"
276
+ >
277
+ <UIcon
278
+ name="i-lucide-zap"
279
+ class="size-3.5 text-muted"
280
+ />
281
+ {{ trigger.label }}
282
+ </div>
283
+ </div>
284
+ </div>
285
+ <div
286
+ v-if="canShare && canPersonal"
287
+ class="flex flex-col gap-2"
288
+ >
289
+ <span class="text-xs text-dimmed">Who agents act as</span>
290
+ <div
291
+ class="grid grid-cols-2 gap-1 rounded-lg border border-(--gray-500) bg-(--gray-200) p-0.5"
292
+ role="radiogroup"
293
+ aria-label="Connection owner"
294
+ >
295
+ <UButton
296
+ v-for="value in ['user', 'workspace'] as const"
297
+ :key="value"
298
+ :label="value === 'user' ? 'Just me' : 'Whole workspace'"
299
+ :icon="value === 'user' ? 'i-lucide-user' : 'i-lucide-users'"
300
+ color="neutral"
301
+ :variant="scope === value ? 'soft' : 'ghost'"
302
+ role="radio"
303
+ :aria-checked="scope === value"
304
+ :disabled="value === 'workspace' && !manager"
305
+ class="h-8 justify-center rounded-md text-[13px]"
306
+ :class="scope === value ? 'bg-(--gray-400) text-highlighted' : ''"
307
+ @click="scope = value"
308
+ />
309
+ </div>
310
+ <p class="text-xs leading-4 text-dimmed">
311
+ {{ scopeDescription }}
312
+ </p>
313
+ </div>
314
+
315
+ <UFormField
316
+ v-if="auth.mcp && !auth.mcp.server"
317
+ label="Server URL"
318
+ >
319
+ <UInput
320
+ v-model="server"
321
+ type="url"
322
+ placeholder="https://mcp.example.com/mcp"
323
+ autocomplete="off"
324
+ class="w-full"
325
+ />
326
+ </UFormField>
327
+
328
+ <UAlert
329
+ v-if="auth.oauth && !oauthReady"
330
+ color="neutral"
331
+ variant="subtle"
332
+ icon="i-lucide-key-round"
333
+ :title="`${provider?.label ?? integration.name} sign-in isn’t set up here yet`"
334
+ :description="
335
+ auth.apiKey
336
+ ? `Paste an access token below, or register the OAuth app: ${setupCommand}`
337
+ : `Register the OAuth app: ${setupCommand}`
338
+ "
339
+ />
340
+ <template v-if="tokenForm">
341
+ <UFormField label="Access token">
342
+ <UInput
343
+ v-model="credentials.token"
344
+ type="password"
345
+ autocomplete="off"
346
+ placeholder="Paste your API token"
347
+ class="w-full"
348
+ />
349
+ </UFormField>
350
+ <UFormField
351
+ v-for="field in tokenFields"
352
+ :key="field.key"
353
+ :label="field.label"
354
+ >
355
+ <UInput
356
+ v-model="credentials[field.key]"
357
+ :type="field.secret ? 'password' : 'text'"
358
+ :placeholder="field.placeholder"
359
+ autocomplete="off"
360
+ class="w-full"
361
+ />
362
+ </UFormField>
363
+ </template>
364
+ <UFormField
365
+ v-for="field in oauthReady ? (provider?.startFields ?? []) : []"
366
+ :key="field.key"
367
+ :label="field.label"
368
+ >
369
+ <UInput
370
+ v-model="credentials[field.key]"
371
+ :placeholder="field.placeholder"
372
+ autocomplete="off"
373
+ class="w-full"
374
+ />
375
+ </UFormField>
376
+ </template>
377
+
378
+ <!-- Manage one account -->
379
+ <template v-if="mode === 'manage' && active">
380
+ <UAlert
381
+ v-if="active.status === 'error'"
382
+ color="error"
383
+ variant="subtle"
384
+ title="This account needs to be reconnected"
385
+ :description="active.statusMessage || 'Its access was revoked or expired.'"
386
+ :actions="
387
+ active.canManage && (auth.oauth || auth.mcp)
388
+ ? [
389
+ {
390
+ label: 'Reconnect',
391
+ color: 'neutral',
392
+ onClick: () => emit('reconnect', active!),
393
+ },
394
+ ]
395
+ : []
396
+ "
397
+ />
398
+ <div class="flex flex-col gap-2">
399
+ <span class="text-xs text-dimmed">
400
+ {{ auth.mcp ? 'Tools agents may use' : 'Allow agents to' }}
401
+ </span>
402
+ <div
403
+ v-if="auth.mcp && !tools"
404
+ class="flex items-center gap-2 text-[13px] text-muted"
405
+ >
406
+ <UIcon
407
+ name="i-lucide-loader-circle"
408
+ class="size-4 animate-spin"
409
+ />
410
+ Loading the server’s tools…
411
+ </div>
412
+ <div
413
+ v-else
414
+ class="max-h-72 divide-y divide-(--gray-400) overflow-y-auto rounded-[10px] border border-(--gray-400)"
415
+ >
416
+ <div
417
+ v-for="action in actionItems"
418
+ :key="action.id"
419
+ class="flex min-h-10 items-center gap-3 px-3"
420
+ >
421
+ <UCheckbox
422
+ :model-value="permissions.includes(action.id)"
423
+ :label="action.label"
424
+ :description="action.hint"
425
+ :disabled="isReadOnly"
426
+ color="neutral"
427
+ class="min-w-0 flex-1 py-2"
428
+ :ui="{
429
+ label: 'text-[13px] font-normal',
430
+ description: 'line-clamp-1 text-xs',
431
+ base: 'size-4 rounded',
432
+ }"
433
+ @update:model-value="
434
+ (value) => (permissions = toggle(permissions, action.id, value))
435
+ "
436
+ />
437
+ <USwitch
438
+ v-if="auth.mcp && permissions.includes(action.id)"
439
+ :model-value="approvals.includes(action.id)"
440
+ :disabled="isReadOnly"
441
+ size="sm"
442
+ label="Ask first"
443
+ :ui="{ label: 'text-xs font-normal text-dimmed' }"
444
+ @update:model-value="(value) => (approvals = toggle(approvals, action.id, value))"
445
+ />
446
+ </div>
447
+ <p
448
+ v-if="!actionItems.length"
449
+ class="px-3 py-2.5 text-[13px] text-dimmed"
450
+ >
451
+ This server offers no tools.
452
+ </p>
453
+ </div>
454
+ </div>
455
+ <UFormField
456
+ v-for="field in settingFields"
457
+ :key="field.key"
458
+ :label="field.label"
459
+ >
460
+ <UInput
461
+ v-model="settings[field.key]"
462
+ :placeholder="field.placeholder"
463
+ :disabled="isReadOnly"
464
+ autocomplete="off"
465
+ class="w-full"
466
+ />
467
+ </UFormField>
468
+ <div
469
+ v-if="integration.triggers.length && active.canManage"
470
+ class="flex flex-col gap-2"
471
+ >
472
+ <UButton
473
+ label="Event endpoint"
474
+ icon="i-lucide-webhook"
475
+ color="neutral"
476
+ variant="ghost"
477
+ class="self-start text-xs"
478
+ @click="emit('endpoint', false)"
479
+ />
480
+ <template v-if="endpoint">
481
+ <p class="text-xs text-muted">
482
+ POST a normalized event to this URL, signed with the secret: send it as a bearer
483
+ token, or as the secret of a GitHub repository webhook. Events start active
484
+ automations that can see this connection.
485
+ </p>
486
+ <UInput
487
+ :model-value="endpoint.url"
488
+ readonly
489
+ aria-label="Event endpoint"
490
+ class="w-full"
491
+ />
492
+ <UInput
493
+ :model-value="endpoint.secret"
494
+ readonly
495
+ type="password"
496
+ aria-label="Event secret"
497
+ class="w-full"
498
+ />
499
+ <div class="flex gap-2">
500
+ <UButton
501
+ label="Copy URL"
502
+ color="neutral"
503
+ variant="outline"
504
+ size="xs"
505
+ @click="emit('copy', endpoint.url)"
506
+ />
507
+ <UButton
508
+ label="Copy secret"
509
+ color="neutral"
510
+ variant="outline"
511
+ size="xs"
512
+ @click="emit('copy', endpoint.secret)"
513
+ />
514
+ <UButton
515
+ label="Rotate secret"
516
+ color="neutral"
517
+ variant="ghost"
518
+ size="xs"
519
+ @click="emit('endpoint', true)"
520
+ />
521
+ </div>
522
+ </template>
523
+ </div>
524
+ </template>
525
+
526
+ <UAlert
527
+ v-if="message"
528
+ color="error"
529
+ variant="subtle"
530
+ :title="message"
531
+ />
532
+ </div>
533
+
534
+ <div class="flex shrink-0 items-center gap-2 border-t border-(--gray-400) px-5 py-3.5">
535
+ <p class="min-w-0 flex-1 text-xs leading-4 text-dimmed">
536
+ {{ permissionDescription }}
537
+ </p>
538
+ <template v-if="mode === 'list'">
539
+ <UButton
540
+ label="Add account"
541
+ icon="i-lucide-plus"
542
+ color="neutral"
543
+ class="h-8 rounded-lg px-3.5 text-[13px]"
544
+ @click="emit('add')"
545
+ />
546
+ </template>
547
+ <template v-else-if="mode === 'manage' && active">
548
+ <UButton
549
+ v-if="active.canManage"
550
+ label="Disconnect"
551
+ color="neutral"
552
+ variant="outline"
553
+ class="h-8 rounded-lg px-3 text-[13px]"
554
+ :disabled="busy"
555
+ @click="emit('disconnect', active)"
556
+ />
557
+ <UButton
558
+ v-if="!isReadOnly"
559
+ label="Save"
560
+ color="neutral"
561
+ class="h-8 rounded-lg px-3.5 text-[13px]"
562
+ :loading="busy"
563
+ @click="emit('save')"
564
+ />
565
+ </template>
566
+ <template v-else-if="mode === 'add'">
567
+ <UButton
568
+ v-if="oauthReady || auth.mcp"
569
+ :label="
570
+ auth.mcp ? 'Sign in to server' : `Connect with ${provider?.label ?? integration.name}`
571
+ "
572
+ color="neutral"
573
+ class="h-8 rounded-lg px-3.5 text-[13px]"
574
+ :loading="busy"
575
+ :disabled="!!auth.mcp && !auth.mcp.server && !server"
576
+ @click="emit('connect')"
577
+ />
578
+ <UButton
579
+ v-else-if="tokenForm"
580
+ :label="`Connect ${integration.name}`"
581
+ color="neutral"
582
+ class="h-8 rounded-lg px-3.5 text-[13px]"
583
+ :loading="busy"
584
+ @click="emit('apiKey')"
585
+ />
586
+ </template>
587
+ </div>
588
+ </div>
589
+ </template>
@@ -0,0 +1,41 @@
1
+ <script setup lang="ts">
2
+ const { name } = defineProps<{ name: string }>()
3
+
4
+ const logos = import.meta.glob<string>('../assets/logos/*.svg', {
5
+ eager: true,
6
+ query: '?url',
7
+ import: 'default',
8
+ })
9
+ const monochrome = ['github', 'linear', 'vercel', 'intercom', 'notion', 'x', 'mcp', 'sentry']
10
+ </script>
11
+
12
+ <template>
13
+ <span
14
+ aria-hidden="true"
15
+ class="inline-flex shrink-0"
16
+ >
17
+ <UIcon
18
+ v-if="!logos[`../assets/logos/${name}.svg`]"
19
+ name="i-lucide-plug"
20
+ class="size-full text-muted"
21
+ />
22
+ <img
23
+ v-else
24
+ :src="logos[`../assets/logos/${name}.svg`]"
25
+ alt=""
26
+ :draggable="false"
27
+ class="size-full object-contain"
28
+ :class="[
29
+ monochrome.includes(name) ? 'dark:invert' : '',
30
+ name === 'tiktok' ? 'dark:hidden' : '',
31
+ ]"
32
+ />
33
+ <img
34
+ v-if="name === 'tiktok'"
35
+ :src="logos['../assets/logos/tiktok-dark.svg']"
36
+ alt=""
37
+ :draggable="false"
38
+ class="hidden size-full object-contain dark:block"
39
+ />
40
+ </span>
41
+ </template>