diff --git a/apps/sim/app/workspace/[workspaceId]/components/connect-oauth-modal/connect-oauth-modal.tsx b/apps/sim/app/workspace/[workspaceId]/components/connect-oauth-modal/connect-oauth-modal.tsx index a5bd8cdf51a..96b2c2f8e3d 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/connect-oauth-modal/connect-oauth-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/connect-oauth-modal/connect-oauth-modal.tsx @@ -19,6 +19,7 @@ import { useSession } from '@/lib/auth/auth-client' import type { OAuthReturnContext } from '@/lib/credentials/client-state' import { ADD_CONNECTOR_SEARCH_PARAM, writeOAuthReturnContext } from '@/lib/credentials/client-state' import { defaultCredentialDisplayName } from '@/lib/credentials/display-name' +import { resolveIntegrationBlockTypeForOAuth } from '@/lib/integrations' import { getProviderIdFromServiceId, OAUTH_PROVIDERS, @@ -26,6 +27,7 @@ import { parseProvider, } from '@/lib/oauth' import { getScopeDescription, getServiceConfigByProviderId } from '@/lib/oauth/utils' +import { BlockTile } from '@/blocks/block-tile' import { useCreateCredentialDraft, useWorkspaceCredentials } from '@/hooks/queries/credentials' import { useConnectOAuthService } from '@/hooks/queries/oauth/oauth-connections' @@ -173,6 +175,20 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) { return resolveService(provider, props.serviceId ?? providerId) }, [props.serviceName, props.serviceIcon, props.provider, props.serviceId, providerId]) + /** + * The block behind this OAuth identity, so the dialog wears the same brand + * tile the canvas and the integrations catalog do. Falls back to the bare + * `OAUTH_PROVIDERS` mark for an id no catalog integration claims. + */ + const headerIcon = useMemo(() => { + const blockType = resolveIntegrationBlockTypeForOAuth( + props.serviceId, + props.provider, + providerId + ) + return blockType ? : ProviderIcon + }, [props.serviceId, props.provider, providerId, ProviderIcon]) + const workspaceId = isConnect ? props.workspaceId : '' const { data: credentials = [], isPending: credentialsLoading } = useWorkspaceCredentials({ workspaceId, @@ -343,7 +359,7 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) { return ( - + {title} diff --git a/apps/sim/app/workspace/[workspaceId]/components/provider-icon/index.ts b/apps/sim/app/workspace/[workspaceId]/components/provider-icon/index.ts new file mode 100644 index 00000000000..f3206a41318 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/provider-icon/index.ts @@ -0,0 +1 @@ +export { ProviderIcon } from './provider-icon' diff --git a/apps/sim/app/workspace/[workspaceId]/components/provider-icon/provider-icon.tsx b/apps/sim/app/workspace/[workspaceId]/components/provider-icon/provider-icon.tsx new file mode 100644 index 00000000000..dd5f3bbdd55 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/provider-icon/provider-icon.tsx @@ -0,0 +1,35 @@ +'use client' + +import { cn } from '@sim/emcn' +import { SquareArrowUpRight } from '@sim/emcn/icons' +import { OAUTH_PROVIDERS, type OAuthProvider, parseProvider } from '@/lib/oauth' +import { getBareIconStyle, type StyleableIcon } from '@/blocks/brand-icon-style' + +interface ProviderIconProps { + provider: OAuthProvider + className?: string +} + +/** + * The mark for an OAuth provider, tinted with the brand colour its block + * config registers. Credential rows show a bare icon rather than the filled + * tile the canvas uses, so the colour has to come through `iconColor` — but it + * still comes from the same registry, which is what keeps a provider looking + * like itself everywhere it is listed. + * + * `OAUTH_PROVIDERS` carries the icon and no colour at all, so rendering + * straight from it is what left credential surfaces grey while the same + * service was branded a panel away. Falls back to a generic mark for a + * provider that map does not know. + */ +export function ProviderIcon({ provider, className }: ProviderIconProps) { + const { baseProvider } = parseProvider(provider) + const config = OAUTH_PROVIDERS[baseProvider] + + if (!config) return + + const Icon = config.icon as StyleableIcon + return ( + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx index 6f8653d1611..8dd8d3aad39 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx @@ -258,7 +258,6 @@ export function useAvailableResources( id: integration.blockType, name: integration.name, iconComponent: integration.icon, - bgColor: integration.bgColor, })), }, { diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/suggested-actions.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/suggested-actions.tsx index 8641b9b5eab..49f0be5e844 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/suggested-actions.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/suggested-actions.tsx @@ -7,7 +7,6 @@ import { randomFloat } from '@sim/utils/random' import { stripVersionSuffix } from '@sim/utils/string' import { useParams } from 'next/navigation' import { usePostHog } from 'posthog-js/react' -import { GmailIcon, SlackIcon } from '@/components/icons' import { INTEGRATIONS, type OAuthServiceMatch, @@ -16,6 +15,7 @@ import { } from '@/lib/integrations' import { captureEvent } from '@/lib/posthog/client' import { ConnectOAuthModal } from '@/app/workspace/[workspaceId]/components/connect-oauth-modal' +import { getBlockTileIcon } from '@/blocks/accent' import { getBareIconStyle } from '@/blocks/brand-icon-style' import { getAllBlockMeta } from '@/blocks/registry' import type { ModuleTag } from '@/blocks/types' @@ -224,6 +224,16 @@ function computeActions(services: readonly ServiceInfo[], signals: Signals): Act return [...integrations, ...prompts] } +/** + * Integrations pinned to the first paint. Named by block type so the mark comes + * from the same registry every other surface reads, rather than a second copy + * imported here that could drift from the block's own icon. + */ +const INITIAL_INTEGRATIONS = [ + { blockType: 'slack', slug: 'slack', name: 'Slack' }, + { blockType: 'gmail', slug: 'gmail', name: 'Gmail' }, +] as const + /** * Initial actions rendered on first paint, before OAuth/credentials queries * resolve. For users with no connections this is also the final result, so the @@ -231,20 +241,20 @@ function computeActions(services: readonly ServiceInfo[], signals: Signals): Act * before the personalized recompute replaces it. */ const INITIAL_ACTIONS: Action[] = [ - { - kind: 'integration', - id: 'integrate-slack', - label: 'Integrate with Slack', - icon: SlackIcon, - slug: 'slack', - }, - { - kind: 'integration', - id: 'integrate-gmail', - label: 'Integrate with Gmail', - icon: GmailIcon, - slug: 'gmail', - }, + ...INITIAL_INTEGRATIONS.flatMap(({ blockType, slug, name }) => { + const icon = getBlockTileIcon(blockType) + return icon + ? [ + { + kind: 'integration', + id: `integrate-${slug}`, + label: `Integrate with ${name}`, + icon, + slug, + }, + ] + : [] + }), toPromptAction(TABLE_STARTERS[0]), ...CANDIDATES.filter((c) => c.blockType === 'github' && c.featured) .slice(0, 1) diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx index f8fbafb5e47..2a2cbf9dd23 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx @@ -8,14 +8,12 @@ import { useQueryState } from 'nuqs' import { HEADER_ACTION_CLUSTER, PAGE_HEADER_BAR } from '@/components/page-header-bar' import { isChatEnabled } from '@/lib/core/config/env-flags' import { - blockTypeToIconMap, type Integration, resolveCredentialDisplay, resolveOAuthServiceForIntegration, } from '@/lib/integrations' import { credentialProviderMatchesService } from '@/lib/oauth' import { ConnectOAuthModal } from '@/app/workspace/[workspaceId]/components/connect-oauth-modal' -import { RESOURCE_TILE_BASE } from '@/app/workspace/[workspaceId]/components/resource-tile' import { IntegrationSkillsSection } from '@/app/workspace/[workspaceId]/integrations/[block]/integration-skills-section' import { connectParam } from '@/app/workspace/[workspaceId]/integrations/[block]/search-params' import { @@ -34,7 +32,7 @@ import { SettingsResourceRow, } from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row' import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' -import { getTileIconColorClass } from '@/blocks/icon-color' +import { getBlockTileIcon } from '@/blocks/accent' import { storeCuratedPrompt } from '@/blocks/integration-matcher' import { getSuggestedSkillsForBlock, @@ -64,7 +62,6 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration useOAuthReturnRouter() const router = useRouter() const [connectMode, setConnectMode] = useQueryState(connectParam.key, connectParam.parser) - const Icon = blockTypeToIconMap[integration.type] const matchingTemplates = getTemplatesForBlock(integration.type) const suggestedSkills = getSuggestedSkillsForBlock(integration.type) const oauthService = resolveOAuthServiceForIntegration(integration) @@ -233,16 +230,10 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration >
- {Icon ? ( - - ) : ( -
- {integration.name.charAt(0)} -
- )} +

{integration.name}

{integration.description}

@@ -255,7 +246,7 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration } + icon={} title={credential.displayName} description={ credential.description || resolveCredentialDisplay(credential).subtitle @@ -374,8 +365,7 @@ function TemplateIcons({ blockTypes }: TemplateIconsProps) { return ( {blockTypes.map((bt, idx) => { - const ToolIcon = blockTypeToIconMap[bt] - if (!ToolIcon) return null + if (!getBlockTileIcon(bt)) return null const z = TEMPLATE_TILE_Z[idx] if (!z) return null const isTrailing = idx > 0 @@ -389,7 +379,7 @@ function TemplateIcons({ blockTypes }: TemplateIconsProps) { 'outline outline-2 outline-[var(--bg)] transition-[outline-color] duration-150 group-hover:outline-[var(--surface-active)]' )} > - + ) })} diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/components/integrations-showcase/integrations-showcase.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/components/integrations-showcase/integrations-showcase.tsx index 43a25c08ba1..e4b29e32451 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/components/integrations-showcase/integrations-showcase.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/components/integrations-showcase/integrations-showcase.tsx @@ -5,6 +5,7 @@ import { RESOURCE_TILE_PLAIN, } from '@/app/workspace/[workspaceId]/components/resource-tile' import { getBlock } from '@/blocks' +import { getBlockTileIcon } from '@/blocks/accent' import { getTileIconColorClass } from '@/blocks/icon-color' /** @@ -59,7 +60,15 @@ function resolveBrandTileBg(blockType: string): string | null { interface IntegrationTileProps { blockType: string - icon: ComponentType<{ className?: string }> + /** + * Overrides the block's registered mark. Only for a tile whose identity is + * not the block itself — a credential issued by a family service account + * wears the family's corporate mark. Everything else takes the registry's, + * so the tile cannot end up with its fill and its icon from two sources. + */ + icon?: ComponentType<{ className?: string }> + /** Drawn when neither the override nor the registry supplies a mark. */ + fallbackLabel?: string framed?: boolean } @@ -68,16 +77,23 @@ interface IntegrationTileProps { * is a 36px tile used in list rows and headers; the framed variant adds an * outer 44px halo used inside the showcase grid. */ -export function IntegrationTile({ blockType, icon: Icon, framed = false }: IntegrationTileProps) { +export function IntegrationTile({ + blockType, + icon, + fallbackLabel, + framed = false, +}: IntegrationTileProps) { const brandBg = resolveBrandTileBg(blockType) + const Icon = icon ?? getBlockTileIcon(blockType) + const contentClass = getTileIconColorClass(brandBg) if (!framed) { return (
- + {Icon ? : fallbackLabel}
) } @@ -85,10 +101,13 @@ export function IntegrationTile({ blockType, icon: Icon, framed = false }: Integ return (
- + {Icon ? : fallbackLabel}
) diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/connected/[credentialId]/connected-credential-detail.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/connected/[credentialId]/connected-credential-detail.tsx index 2b1ae99f392..705f6647dfd 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/connected/[credentialId]/connected-credential-detail.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/connected/[credentialId]/connected-credential-detail.tsx @@ -8,7 +8,6 @@ import { ChipInput, ChipLink, ChipTextarea, - cn, Send, toast, } from '@sim/emcn' @@ -28,10 +27,6 @@ import { UnsavedChangesModal, useCredentialDetailForm, } from '@/app/workspace/[workspaceId]/components/credential-detail' -import { - RESOURCE_TILE_BASE, - RESOURCE_TILE_PLAIN, -} from '@/app/workspace/[workspaceId]/components/resource-tile' import { ConnectServiceAccountModal, type ServiceAccountProviderId, @@ -244,15 +239,11 @@ export function ConnectedCredentialDetail({ - ) : ( -
- - {resolveProviderLabel(credential.providerId).slice(0, 1) || '?'} - -
- ) + } title={headingTitle} subtitle={display?.detailSubtitle ?? 'Connected service'} diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/integrations.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/integrations.tsx index 9f166f59e8b..3f26e43d390 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/integrations.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/integrations.tsx @@ -14,7 +14,6 @@ import { import { useParams } from 'next/navigation' import { useQueryStates } from 'nuqs' import { - blockTypeToIconMap, formatIntegrationType, INTEGRATIONS, type Integration, @@ -34,6 +33,7 @@ import { } from '@/app/workspace/[workspaceId]/integrations/search-params' import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' import { SettingsResourceRow } from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row' +import { getBlockTileIcon } from '@/blocks/accent' import { useWorkspaceCredentials, type WorkspaceCredential } from '@/hooks/queries/credentials' import { useDebouncedSearchSetter } from '@/hooks/use-debounced-search-setter' import { usePermissionConfig } from '@/hooks/use-permission-config' @@ -68,7 +68,6 @@ interface IntegrationItemProps { workspaceId: string name: string description?: string | null - icon: ComponentType<{ className?: string }> unavailable?: boolean } @@ -78,13 +77,12 @@ function IntegrationItem({ workspaceId, name, description, - icon: Icon, unavailable = false, }: IntegrationItemProps) { return ( } + icon={} title={name} description={ unavailable @@ -348,8 +346,7 @@ export function Integrations() { {filteredCategorySections.map((section) => ( {section.integrations.map((integration) => { - const Icon = blockTypeToIconMap[integration.type] - if (!Icon) return null + if (!getBlockTileIcon(integration.type)) return null const availability = integrationAvailability.get(integration.type.toLowerCase()) const deploymentUnavailable = availability?.state === 'unavailable' || availability?.state === 'misconfigured' @@ -361,7 +358,6 @@ export function Integrations() { workspaceId={workspaceId} name={integration.name} description={integration.description} - icon={Icon} unavailable={integration.authType === 'oauth' && deploymentUnavailable} /> ) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx index 705746d348f..56a5cb790c6 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx @@ -16,7 +16,6 @@ import { ChipModalFooter, ChipModalHeader, type ComboboxOption, - cn, handleKeyboardActivation, Search, } from '@sim/emcn' @@ -31,12 +30,11 @@ import { import { ConnectOAuthModal } from '@/app/workspace/[workspaceId]/components/connect-oauth-modal' import { ConnectorConfigFields } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connector-config-fields' import { hasWorkspaceMaxConnectorAccess } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connector-entitlements' +import { ConnectorTile } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connector-tile' import { SYNC_INTERVALS } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/consts' import { MaxBadge } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/max-badge' import { useConnectorConfigFields } from '@/app/workspace/[workspaceId]/knowledge/[id]/hooks/use-connector-config-fields' import { useWorkspaceHostContext } from '@/app/workspace/[workspaceId]/providers/workspace-host-provider' -import { getBlock } from '@/blocks' -import { getTileIconColorClass } from '@/blocks/icon-color' import { CONNECTOR_META_REGISTRY } from '@/connectors/registry' import type { ConnectorMeta } from '@/connectors/types' import { useCreateConnector } from '@/hooks/queries/kb/connectors' @@ -471,9 +469,6 @@ interface ConnectorTypeCardProps { } function ConnectorTypeCard({ type, config, onClick }: ConnectorTypeCardProps) { - const Icon = config.icon - const brandBg = getBlock(type)?.bgColor ?? null - return (