Blocks

Integration Marketplace Block

Platform UI

A responsive app-directory pattern for discovering connectors, reviewing permissions, and launching integration installs.

Developer Experience

Integration marketplace

Copy this into a settings area, developer console, onboarding flow, or platform marketplace where customers need to discover integrations before entering an install workflow.

1200px

<script setup>
import { computed, ref, watch } from 'vue';
import {
	DomButton,
	DomDialog,
	DomNativeSelect,
	DomTabs,
	DomTextInput,
	DomToggle,
} from '@getdom/studio/vue';

const categoryOptions = [
	{ label: 'All categories', value: 'All' },
	{ label: 'CRM', value: 'CRM' },
	{ label: 'Support', value: 'Support' },
	{ label: 'Communication', value: 'Communication' },
	{ label: 'Data warehouse', value: 'Data warehouse' },
	{ label: 'Finance', value: 'Finance' },
];

const sortOptions = [
	{ label: 'Recommended', value: 'recommended' },
	{ label: 'Most installed', value: 'installs' },
	{ label: 'Newest', value: 'newest' },
];

const installTabs = [
	{ key: 'overview', label: 'Overview' },
	{ key: 'permissions', label: 'Permissions' },
	{ key: 'activity', label: 'Activity' },
];

const connectors = [
	{
		id: 'salesforce',
		name: 'Salesforce',
		category: 'CRM',
		description: 'Sync accounts, contacts, owners, opportunities, and lifecycle stages.',
		status: 'Installed',
		owner: 'Maya Chen',
		lastSyncAt: '8 minutes ago',
		health: 'Healthy',
		installCount: 18420,
		rating: '4.9',
		recommended: true,
		newest: false,
		accent: 'bg-sky-500',
		capabilities: ['Account enrichment', 'Opportunity signals', 'Two-way contacts'],
		metrics: [
			{ label: 'Success rate', value: '99.2%' },
			{ label: 'Records synced', value: '18.4k' },
			{ label: 'Next sync', value: '7m' },
		],
		scopes: [
			{ key: 'accounts.read', label: 'Read accounts', description: 'Import account names, domains, tiers, and owners.', enabled: true, required: true },
			{ key: 'contacts.read', label: 'Read contacts', description: 'Import contact names, emails, roles, and lifecycle status.', enabled: true, required: true },
			{ key: 'opportunities.read', label: 'Read opportunities', description: 'Use open pipeline for health and expansion signals.', enabled: true, required: false },
			{ key: 'contacts.write', label: 'Write contact updates', description: 'Push lifecycle stage and product activity updates back.', enabled: false, required: false },
		],
		activity: [
			{ label: '18,420 records synced', actor: 'Sync worker', time: '8 minutes ago', tone: 'success' },
			{ label: 'Opportunity scope enabled', actor: 'Maya Chen', time: 'Yesterday', tone: 'neutral' },
			{ label: 'OAuth token refreshed', actor: 'System', time: 'Jun 08', tone: 'neutral' },
		],
	},
	{
		id: 'zendesk',
		name: 'Zendesk',
		category: 'Support',
		description: 'Bring ticket volume, SLA state, CSAT, and customer support history into account views.',
		status: 'Needs review',
		owner: 'Support Ops',
		lastSyncAt: '2 hours ago',
		health: 'Permission issue',
		installCount: 12890,
		rating: '4.7',
		recommended: true,
		newest: false,
		accent: 'bg-emerald-500',
		capabilities: ['Ticket timeline', 'SLA alerts', 'Internal notes'],
		metrics: [
			{ label: 'Success rate', value: '91.7%' },
			{ label: 'Records synced', value: '4.8k' },
			{ label: 'Next sync', value: 'Paused' },
		],
		scopes: [
			{ key: 'tickets.read', label: 'Read tickets', description: 'Import ticket metadata, requester, tags, and SLA state.', enabled: true, required: true },
			{ key: 'users.read', label: 'Read users', description: 'Match requesters to contacts in the workspace.', enabled: true, required: true },
			{ key: 'comments.read', label: 'Read comments', description: 'Show recent conversation context in account timelines.', enabled: false, required: false },
			{ key: 'tickets.write', label: 'Write ticket notes', description: 'Create internal notes from customer success workflows.', enabled: false, required: false },
		],
		activity: [
			{ label: 'Sync paused after permission error', actor: 'Monitoring', time: '2 hours ago', tone: 'warning' },
			{ label: 'Tickets imported', actor: 'Sync worker', time: '4 hours ago', tone: 'success' },
			{ label: 'Admin notified for reconnect', actor: 'System', time: 'Today 09:20', tone: 'neutral' },
		],
	},
	{
		id: 'slack',
		name: 'Slack',
		category: 'Communication',
		description: 'Send alerts, route approvals, and create customer-room notifications.',
		status: 'Available',
		owner: 'Not connected',
		lastSyncAt: 'Never',
		health: 'Ready to install',
		installCount: 22800,
		rating: '4.8',
		recommended: true,
		newest: false,
		accent: 'bg-violet-500',
		capabilities: ['Approval routing', 'Channel alerts', 'Slash commands'],
		metrics: [
			{ label: 'Install type', value: 'OAuth' },
			{ label: 'Setup time', value: '4m' },
			{ label: 'Admin consent', value: 'Optional' },
		],
		scopes: [
			{ key: 'channels.read', label: 'Read channels', description: 'Select destinations for alerts and approvals.', enabled: true, required: true },
			{ key: 'chat.write', label: 'Send messages', description: 'Post workflow updates into selected channels.', enabled: true, required: true },
			{ key: 'users.read', label: 'Read workspace users', description: 'Map app users to Slack members for mentions.', enabled: false, required: false },
			{ key: 'commands.write', label: 'Install shortcuts', description: 'Add command shortcuts for support and approval actions.', enabled: false, required: false },
		],
		activity: [
			{ label: 'Connector reviewed by security', actor: 'Ari Grant', time: 'Jun 06', tone: 'neutral' },
			{ label: 'Install requested by Sales Ops', actor: 'Nina Patel', time: 'Jun 04', tone: 'neutral' },
			{ label: 'Provider catalog updated', actor: 'System', time: 'Jun 01', tone: 'neutral' },
		],
	},
	{
		id: 'bigquery',
		name: 'BigQuery',
		category: 'Data warehouse',
		description: 'Export product events, account snapshots, and billing state into warehouse tables.',
		status: 'Installed',
		owner: 'Data Platform',
		lastSyncAt: '22 minutes ago',
		health: 'Healthy',
		installCount: 9400,
		rating: '4.6',
		recommended: false,
		newest: false,
		accent: 'bg-amber-500',
		capabilities: ['Warehouse export', 'Schema checks', 'Daily compaction'],
		metrics: [
			{ label: 'Success rate', value: '98.6%' },
			{ label: 'Rows exported', value: '2.6m' },
			{ label: 'Next sync', value: '38m' },
		],
		scopes: [
			{ key: 'datasets.write', label: 'Write datasets', description: 'Create and update managed analytics datasets.', enabled: true, required: true },
			{ key: 'tables.write', label: 'Write tables', description: 'Append events and account snapshots to tables.', enabled: true, required: true },
			{ key: 'jobs.read', label: 'Read job status', description: 'Check export job state and surface failed runs.', enabled: true, required: false },
			{ key: 'datasets.read', label: 'Read datasets', description: 'Inspect existing schemas before migration.', enabled: false, required: false },
		],
		activity: [
			{ label: 'Export job completed', actor: 'Sync worker', time: '22 minutes ago', tone: 'success' },
			{ label: 'Schema drift warning resolved', actor: 'Data Platform', time: 'Yesterday', tone: 'success' },
			{ label: 'Hourly cadence enabled', actor: 'Maya Chen', time: 'Jun 07', tone: 'neutral' },
		],
	},
	{
		id: 'stripe',
		name: 'Stripe',
		category: 'Finance',
		description: 'Import subscription state, invoices, payment risk, and customer revenue metrics.',
		status: 'Available',
		owner: 'Not connected',
		lastSyncAt: 'Never',
		health: 'Ready to install',
		installCount: 17200,
		rating: '4.9',
		recommended: false,
		newest: true,
		accent: 'bg-indigo-500',
		capabilities: ['MRR snapshots', 'Invoice timeline', 'Payment status'],
		metrics: [
			{ label: 'Install type', value: 'API key' },
			{ label: 'Setup time', value: '6m' },
			{ label: 'Admin consent', value: 'Required' },
		],
		scopes: [
			{ key: 'customers.read', label: 'Read customers', description: 'Match billing customers to product accounts.', enabled: true, required: true },
			{ key: 'subscriptions.read', label: 'Read subscriptions', description: 'Import plan, renewal, and cancellation state.', enabled: true, required: true },
			{ key: 'invoices.read', label: 'Read invoices', description: 'Show open, paid, and failed invoice history.', enabled: true, required: false },
			{ key: 'webhooks.write', label: 'Create webhooks', description: 'Register billing event callbacks automatically.', enabled: false, required: false },
		],
		activity: [
			{ label: 'Provider added to marketplace', actor: 'Platform team', time: 'Today 11:08', tone: 'success' },
			{ label: 'Security review completed', actor: 'Risk review', time: 'Yesterday', tone: 'neutral' },
			{ label: 'Webhook templates published', actor: 'System', time: 'Jun 08', tone: 'neutral' },
		],
	},
	{
		id: 'hubspot',
		name: 'HubSpot',
		category: 'CRM',
		description: 'Map companies, contacts, deals, and marketing lifecycle stages into your workspace.',
		status: 'Available',
		owner: 'Not connected',
		lastSyncAt: 'Never',
		health: 'Ready to install',
		installCount: 15100,
		rating: '4.7',
		recommended: false,
		newest: true,
		accent: 'bg-orange-500',
		capabilities: ['Deal sync', 'Company fields', 'Marketing lifecycle'],
		metrics: [
			{ label: 'Install type', value: 'OAuth' },
			{ label: 'Setup time', value: '5m' },
			{ label: 'Admin consent', value: 'Required' },
		],
		scopes: [
			{ key: 'crm.objects.contacts.read', label: 'Read contacts', description: 'Import contact identity, lifecycle stage, and owner.', enabled: true, required: true },
			{ key: 'crm.objects.companies.read', label: 'Read companies', description: 'Import company firmographics and account ownership.', enabled: true, required: true },
			{ key: 'crm.objects.deals.read', label: 'Read deals', description: 'Bring pipeline amount and stage into customer health.', enabled: false, required: false },
			{ key: 'crm.objects.contacts.write', label: 'Write contact updates', description: 'Push product-qualified lifecycle changes back.', enabled: false, required: false },
		],
		activity: [
			{ label: 'Connector added to catalog', actor: 'Platform team', time: 'Today 08:30', tone: 'success' },
			{ label: 'Field mapping template drafted', actor: 'Solutions', time: 'Yesterday', tone: 'neutral' },
			{ label: 'OAuth app approved', actor: 'Security', time: 'Jun 09', tone: 'neutral' },
		],
	},
];

const selectedCategory = ref('All');
const selectedSort = ref('recommended');
const searchQuery = ref('');
const selectedConnectorId = ref(connectors[0].id);
const dialogOpen = ref(false);
const activeTab = ref('overview');
const selectedScopes = ref([]);
const scheduledSync = ref(true);
const installedIds = ref(connectors.filter((connector) => connector.status === 'Installed').map((connector) => connector.id));

const selectedConnector = computed(() => connectors.find((connector) => connector.id === selectedConnectorId.value) || connectors[0]);
const selectedRequiredCount = computed(() => selectedConnector.value.scopes.filter((scope) => scope.required && selectedScopes.value.includes(scope.key)).length);
const requiredScopeCount = computed(() => selectedConnector.value.scopes.filter((scope) => scope.required).length);
const optionalScopeCount = computed(() => selectedScopes.value.length - selectedRequiredCount.value);
const dialogStatus = computed(() => installedIds.value.includes(selectedConnector.value.id) ? 'Installed' : selectedConnector.value.status);
const primaryActionLabel = computed(() => {
	if (dialogStatus.value === 'Needs review') return 'Reconnect';
	if (dialogStatus.value === 'Installed') return 'Save settings';
	return 'Connect integration';
});
const filteredConnectors = computed(() => {
	const query = searchQuery.value.trim().toLowerCase();
	const filtered = connectors.filter((connector) => {
		const matchesCategory = selectedCategory.value === 'All' || connector.category === selectedCategory.value;
		const matchesQuery = !query || [
			connector.name,
			connector.category,
			connector.description,
			...connector.capabilities,
		].join(' ').toLowerCase().includes(query);

		return matchesCategory && matchesQuery;
	});

	return [...filtered].sort((a, b) => {
		if (selectedSort.value === 'installs') return b.installCount - a.installCount;
		if (selectedSort.value === 'newest') return Number(b.newest) - Number(a.newest);
		return Number(b.recommended) - Number(a.recommended) || b.installCount - a.installCount;
	});
});
const featuredConnectors = computed(() => connectors.filter((connector) => connector.recommended).slice(0, 3));
const installedCount = computed(() => installedIds.value.length);
const reviewCount = computed(() => connectors.filter((connector) => connector.status === 'Needs review').length);

watch(selectedConnector, syncDialogState, { immediate: true });

function openConnector(connector) {
	selectedConnectorId.value = connector.id;
	activeTab.value = 'overview';
	dialogOpen.value = true;
}

function syncDialogState() {
	selectedScopes.value = selectedConnector.value.scopes.filter((scope) => scope.enabled).map((scope) => scope.key);
	scheduledSync.value = selectedConnector.value.status !== 'Available';
}

function toggleScope(scope) {
	if (scope.required) return;
	selectedScopes.value = selectedScopes.value.includes(scope.key)
		? selectedScopes.value.filter((key) => key !== scope.key)
		: [...selectedScopes.value, scope.key];
}

function saveConnector() {
	if (!installedIds.value.includes(selectedConnector.value.id)) {
		installedIds.value = [...installedIds.value, selectedConnector.value.id];
	}
	dialogOpen.value = false;
}

function displayStatus(connector) {
	return installedIds.value.includes(connector.id) ? 'Installed' : connector.status;
}

function statusClasses(status) {
	return {
		Installed: 'bg-success/15 text-success',
		'Needs review': 'bg-warning/15 text-warning',
		Available: 'bg-secondary text-muted-fg',
	}[status] || 'bg-secondary text-muted-fg';
}

function activityClasses(tone) {
	return {
		success: 'bg-success',
		warning: 'bg-warning',
		neutral: 'bg-muted-fg',
	}[tone] || 'bg-muted-fg';
}
</script>

<template>
	<div class="w-full overflow-hidden rounded-lg border border-border bg-background text-fg shadow-2xl shadow-black/10">
		<section class="border-b border-border bg-secondary/35 px-4 py-5 sm:px-6">
			<div class="flex flex-col gap-5 xl:flex-row xl:items-end xl:justify-between">
				<div class="max-w-3xl">
					<p class="text-xs font-semibold uppercase tracking-[0.16em] text-muted-fg">Developer platform</p>
					<h3 class="mt-2 text-2xl font-semibold tracking-tight sm:text-3xl">Integration marketplace</h3>
					<p class="mt-2 text-sm leading-6 text-muted-fg">
						Help teams discover approved connectors, understand data access, and launch healthy syncs without sending them through a dense settings console.
					</p>
				</div>
				<div class="grid gap-2 text-sm sm:grid-cols-3 xl:min-w-[28rem]">
					<div class="rounded-lg border border-border bg-background/80 px-3 py-2">
						<p class="text-xs text-muted-fg">Catalog</p>
						<p class="mt-1 font-semibold">{{ connectors.length }} connectors</p>
					</div>
					<div class="rounded-lg border border-border bg-background/80 px-3 py-2">
						<p class="text-xs text-muted-fg">Installed</p>
						<p class="mt-1 font-semibold">{{ installedCount }} live</p>
					</div>
					<div class="rounded-lg border border-border bg-background/80 px-3 py-2">
						<p class="text-xs text-muted-fg">Attention</p>
						<p class="mt-1 font-semibold">{{ reviewCount }} review</p>
					</div>
				</div>
			</div>
		</section>

		<section class="border-b border-border px-4 py-4 sm:px-6">
			<div class="grid gap-3 lg:grid-cols-[minmax(0,1fr)_12rem_12rem]">
				<DomTextInput v-model="searchQuery" label="Search connectors" placeholder="Search by provider, category, or capability" />
				<DomNativeSelect v-model="selectedCategory" :options="categoryOptions" label="Category" />
				<DomNativeSelect v-model="selectedSort" :options="sortOptions" label="Sort" />
			</div>
		</section>

		<section class="grid gap-6 p-4 sm:p-6 xl:grid-cols-[18rem_minmax(0,1fr)]">
			<aside class="space-y-5">
				<div>
					<div class="flex items-center justify-between gap-3">
						<h4 class="font-semibold">Recommended</h4>
						<span class="text-xs font-medium text-muted-fg">{{ featuredConnectors.length }} picks</span>
					</div>
					<div class="mt-3 space-y-2">
						<button
							v-for="connector in featuredConnectors"
							:key="`featured-${connector.id}`"
							type="button"
							class="flex w-full items-center gap-3 rounded-lg border border-border bg-secondary/35 p-3 text-left transition hover:border-primary/50 hover:bg-primary/5"
							@click="openConnector(connector)"
						>
							<span class="grid size-9 shrink-0 place-items-center rounded-lg text-sm font-semibold text-white" :class="connector.accent">
								{{ connector.name.charAt(0) }}
							</span>
							<span class="min-w-0 flex-1">
								<span class="block truncate text-sm font-semibold">{{ connector.name }}</span>
								<span class="mt-1 block truncate text-xs text-muted-fg">{{ connector.capabilities[0] }}</span>
							</span>
						</button>
					</div>
				</div>

				<div class="rounded-lg border border-border skin-raised p-4">
					<h4 class="font-semibold">Install readiness</h4>
					<div class="mt-4 space-y-3 text-sm">
						<div class="flex items-start gap-2">
							<span class="mt-1.5 size-2 rounded-full bg-success"></span>
							<p>Show exactly which data scopes each provider requests.</p>
						</div>
						<div class="flex items-start gap-2">
							<span class="mt-1.5 size-2 rounded-full bg-primary"></span>
							<p>Route available connectors into OAuth or API key setup.</p>
						</div>
						<div class="flex items-start gap-2">
							<span class="mt-1.5 size-2 rounded-full bg-warning"></span>
							<p>Surface reconnect work before background syncs silently fail.</p>
						</div>
					</div>
				</div>
			</aside>

			<main class="min-w-0">
				<div class="mb-4 flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
					<div>
						<h4 class="text-lg font-semibold">Browse connectors</h4>
						<p class="mt-1 text-sm text-muted-fg">{{ filteredConnectors.length }} matching providers</p>
					</div>
					<DomButton size="sm" variant="secondary">
						<svg class="size-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
							<path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
						</svg>
						Request connector
					</DomButton>
				</div>

				<div class="grid gap-4 md:grid-cols-2 2xl:grid-cols-3">
					<button
						v-for="connector in filteredConnectors"
						:key="connector.id"
						type="button"
						class="group flex min-h-[17rem] flex-col rounded-lg border border-border bg-background p-4 text-left transition hover:-translate-y-0.5 hover:border-primary/50 hover:shadow-lg hover:shadow-black/10"
						@click="openConnector(connector)"
					>
						<span class="flex items-start justify-between gap-3">
							<span class="flex min-w-0 items-start gap-3">
								<span class="grid size-11 shrink-0 place-items-center rounded-lg text-base font-semibold text-white" :class="connector.accent">
									{{ connector.name.charAt(0) }}
								</span>
								<span class="min-w-0">
									<span class="block truncate font-semibold">{{ connector.name }}</span>
									<span class="mt-1 block text-xs font-medium text-muted-fg">{{ connector.category }}</span>
								</span>
							</span>
							<span class="shrink-0 rounded-full px-2.5 py-1 text-xs font-semibold" :class="statusClasses(displayStatus(connector))">
								{{ displayStatus(connector) }}
							</span>
						</span>

						<span class="mt-4 block text-sm leading-6 text-muted-fg">{{ connector.description }}</span>

						<span class="mt-4 flex flex-wrap gap-2">
							<span
								v-for="capability in connector.capabilities"
								:key="capability"
								class="rounded-full bg-secondary px-2.5 py-1 text-xs font-medium text-muted-fg"
							>
								{{ capability }}
							</span>
						</span>

						<span class="mt-auto grid gap-2 pt-5 text-xs text-muted-fg sm:grid-cols-3">
							<span class="rounded-lg bg-secondary/50 px-2.5 py-2">
								<span class="block font-medium text-fg">{{ connector.rating }}</span>
								<span>Rating</span>
							</span>
							<span class="rounded-lg bg-secondary/50 px-2.5 py-2">
								<span class="block font-medium text-fg">{{ connector.installCount.toLocaleString() }}</span>
								<span>Installs</span>
							</span>
							<span class="rounded-lg bg-secondary/50 px-2.5 py-2">
								<span class="block font-medium text-fg">{{ connector.health }}</span>
								<span>Health</span>
							</span>
						</span>
					</button>
				</div>

				<div v-if="!filteredConnectors.length" class="rounded-lg border border-dashed border-border p-8 text-center">
					<p class="font-semibold">No connectors match this search.</p>
					<p class="mt-2 text-sm text-muted-fg">Try another category or request a provider from your platform team.</p>
				</div>
			</main>
		</section>

		<DomDialog
			v-model="dialogOpen"
			:title="`${selectedConnector.name} integration`"
			:description="selectedConnector.description"
		>
			<div class="space-y-5">
				<div class="flex items-start justify-between gap-3 rounded-lg border border-border bg-secondary/40 p-3">
					<div class="flex min-w-0 gap-3">
						<span class="grid size-10 shrink-0 place-items-center rounded-lg font-semibold text-white" :class="selectedConnector.accent">
							{{ selectedConnector.name.charAt(0) }}
						</span>
						<div class="min-w-0">
							<p class="font-semibold">{{ selectedConnector.category }}</p>
							<p class="mt-1 text-xs text-muted-fg">{{ selectedConnector.owner }} / {{ selectedConnector.lastSyncAt }}</p>
						</div>
					</div>
					<span class="shrink-0 rounded-full px-2.5 py-1 text-xs font-semibold" :class="statusClasses(dialogStatus)">
						{{ dialogStatus }}
					</span>
				</div>

				<DomTabs v-model="activeTab" :tabs="installTabs">
					<template #overview>
						<div class="space-y-4">
							<div class="grid gap-2 sm:grid-cols-3">
								<div
									v-for="metric in selectedConnector.metrics"
									:key="metric.label"
									class="rounded-lg border border-border p-3"
								>
									<p class="text-xs text-muted-fg">{{ metric.label }}</p>
									<p class="mt-1 font-semibold">{{ metric.value }}</p>
								</div>
							</div>
							<div>
								<h5 class="text-sm font-semibold">Best for</h5>
								<div class="mt-2 flex flex-wrap gap-2">
									<span
										v-for="capability in selectedConnector.capabilities"
										:key="`dialog-${capability}`"
										class="rounded-full bg-secondary px-2.5 py-1 text-xs font-medium text-muted-fg"
									>
										{{ capability }}
									</span>
								</div>
							</div>
						</div>
					</template>

					<template #permissions>
						<div class="space-y-4">
							<div class="rounded-lg border border-border bg-secondary/40 p-3 text-sm">
								<p class="font-semibold">{{ selectedRequiredCount }} of {{ requiredScopeCount }} required scopes selected</p>
								<p class="mt-1 text-muted-fg">{{ optionalScopeCount }} optional scopes enabled for richer workflows.</p>
							</div>
							<div class="divide-y divide-border rounded-lg border border-border">
								<label
									v-for="scope in selectedConnector.scopes"
									:key="scope.key"
									class="flex gap-3 p-3"
									:class="scope.required ? 'bg-secondary/30' : 'bg-background'"
								>
									<input
										type="checkbox"
										class="mt-1 size-4 rounded border-border text-primary"
										:checked="selectedScopes.includes(scope.key)"
										:disabled="scope.required"
										@change="toggleScope(scope)"
									>
									<span class="min-w-0 flex-1">
										<span class="flex flex-wrap items-center gap-2">
											<span class="text-sm font-medium">{{ scope.label }}</span>
											<span v-if="scope.required" class="rounded-full bg-secondary px-2 py-0.5 text-[11px] font-semibold text-muted-fg">Required</span>
										</span>
										<span class="mt-1 block text-xs leading-5 text-muted-fg">{{ scope.description }}</span>
									</span>
								</label>
							</div>
							<DomToggle
								v-model="scheduledSync"
								label="Run scheduled syncs after install"
								description="Turn this off when an admin must review mapping before the first job."
							/>
						</div>
					</template>

					<template #activity>
						<div class="divide-y divide-border rounded-lg border border-border">
							<div
								v-for="event in selectedConnector.activity"
								:key="`${event.label}-${event.time}`"
								class="flex items-start justify-between gap-3 p-3"
							>
								<div class="flex min-w-0 gap-3">
									<span class="mt-1.5 size-2.5 shrink-0 rounded-full" :class="activityClasses(event.tone)"></span>
									<div class="min-w-0">
										<p class="text-sm font-medium">{{ event.label }}</p>
										<p class="mt-1 text-xs text-muted-fg">{{ event.actor }}</p>
									</div>
								</div>
								<p class="shrink-0 text-xs text-muted-fg">{{ event.time }}</p>
							</div>
						</div>
					</template>
				</DomTabs>
			</div>

			<template #footer>
				<DomButton variant="secondary" data-close>Cancel</DomButton>
				<DomButton @click="saveConnector">{{ primaryActionLabel }}</DomButton>
			</template>
		</DomDialog>
	</div>
</template>

Integration

How to use this block

Use this block when an app needs a central place for customers to discover integrations, compare provider capabilities, review requested data access, and launch the right install path. The layout keeps discovery tile-first, then moves setup detail into a focused dialog so the catalog does not become another dense admin console.

  • Replace connectors with provider catalog records from your backend, including category, install status, health, scopes, and capabilities.
  • Use the primary dialog action to branch into OAuth, API key entry, admin consent, or reconnect flows based on connector status.
  • Persist optional scope choices and scheduled-sync preference before creating tokens or starting background jobs.
  • Connect the activity tab to audit events such as install, token refresh, failed sync, reconnect, scope change, and provider review.
  • Keep required scopes server-defined so client-side edits cannot weaken the permissions needed for a safe integration.

Data

Recommended connector shape

{
	id: 'salesforce',
	name: 'Salesforce',
	category: 'CRM',
	description: 'Sync accounts, contacts, owners, opportunities, and lifecycle stages.',
	status: 'Installed',
	owner: 'Maya Chen',
	lastSyncAt: '8 minutes ago',
	health: 'Healthy',
	installCount: 18420,
	rating: '4.9',
	recommended: true,
	capabilities: ['Account enrichment', 'Opportunity signals', 'Two-way contacts'],
	metrics: [
		{ label: 'Success rate', value: '99.2%' },
		{ label: 'Records synced', value: '18.4k' },
		{ label: 'Next sync', value: '7m' }
	],
	scopes: [
		{ key: 'contacts.read', label: 'Read contacts', enabled: true, required: true },
		{ key: 'contacts.write', label: 'Write contacts', enabled: false, required: false }
	],
	activity: [
		{ label: 'Contacts synced', actor: 'Sync worker', time: '8 minutes ago' }
	]
}

Customization

Implementation notes

Connection flows

Route each connector to the correct install method from the dialog: OAuth redirect, embedded consent, API key entry, reconnect, or admin-approved installation.

Catalog quality

Store provider capability tags, install counts, setup time, review state, and health in your catalog so search and recommendations stay useful.

Future updates

Useful follow-ups include reusable provider icons, OAuth callback states, field mapping steps, compare mode, install-request approvals, and error remediation checklists.