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 c3624387ae1..6e67c2da29f 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 @@ -26,6 +26,7 @@ import { parseProvider, } from '@/lib/oauth' import { getScopeDescription, getServiceConfigByProviderId } from '@/lib/oauth/utils' +import { withBrandIcon } from '@/blocks/brand-icon' import { useCreateCredentialDraft, useWorkspaceCredentials } from '@/hooks/queries/credentials' import { useConnectOAuthService } from '@/hooks/queries/oauth/oauth-connections' @@ -49,11 +50,11 @@ function isHiddenScope(scope: string): boolean { function resolveService( provider: OAuthProvider, serviceId: string -): { providerName: string; ProviderIcon: ServiceIcon } { +): { providerName: string; ProviderIcon: ServiceIcon | null } { const { baseProvider } = parseProvider(provider) const baseProviderConfig = OAUTH_PROVIDERS[baseProvider] let providerName = baseProviderConfig?.name || provider - let ProviderIcon: ServiceIcon = baseProviderConfig?.icon || (() => null) + let ProviderIcon: ServiceIcon | null = baseProviderConfig?.icon ?? null if (baseProviderConfig) { for (const [key, service] of Object.entries(baseProviderConfig.services)) { if (key === serviceId || service.providerId === provider) { @@ -355,7 +356,10 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) { return ( - + {title} diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-chip.test.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-chip.test.tsx index f17fa37ebd7..eafe06203bd 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-chip.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-chip.test.tsx @@ -72,7 +72,11 @@ describe('MentionChipView', () => { .filter((cls) => cls.startsWith('text-') || cls.startsWith('[&]:text-')) expect(ownTextUtilities).toEqual([]) - // The icon's own monochrome fallback is unrelated and must be untouched by this fix. - expect(chip.className).toContain('[&>svg]:text-[var(--text-icon)]') + // The icon's monochrome fallback moved into `BrandIcon`, which owns the glyph color for every + // surface. A descendant color rule here would be a second, silently-losing source of truth. + expect(chip.className).not.toContain('[&>svg]:text-') + expect(container?.querySelector('svg')?.getAttribute('class')).toContain( + 'text-[var(--text-icon)]' + ) }) }) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-chip.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-chip.tsx index 30c27c1bed7..5b737844816 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-chip.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-chip.tsx @@ -3,7 +3,7 @@ import { cn } from '@sim/emcn' import type { ReactNodeViewProps } from '@tiptap/react' import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react' import { useParams, useRouter } from 'next/navigation' -import { getBareIconStyle, type StyleableIcon } from '@/blocks/brand-icon-style' +import { BrandIcon, type StyleableIcon } from '@/blocks/brand-icon' import { mentionIcon } from './mention-icon' import { MarkdownMention, type MentionAttrs } from './mention-node' import { simLinkPath } from './sim-link' @@ -13,8 +13,8 @@ import { simLinkPath } from './sim-link' * in `prompt-editor.tsx`): a borderless inline icon + label that flows with the * surrounding prose — no pill background, no padding, normal weight, body text * color, and a 12px icon. Integration icons keep their brand color via - * {@link getBareIconStyle} (see {@link MentionChipView}); other kinds stay - * monochrome through the `--text-icon` fallback below. + * {@link BrandIcon} (see {@link MentionChipView}); other kinds stay + * monochrome through its `--text-icon` fallback. * * No explicit label color — an element's own explicit `color` always wins over an inherited one * regardless of ancestor specificity, so hardcoding `--text-primary` here (redundant with the prose @@ -24,7 +24,7 @@ import { simLinkPath } from './sim-link' * in rich-markdown-editor.css. */ const CHIP_CLASS = - 'mention-chip mx-px inline-flex items-center gap-1 align-middle leading-[1.5] [&>svg]:size-[12px] [&>svg]:shrink-0 [&>svg]:text-[var(--text-icon)]' + 'mention-chip mx-px inline-flex items-center gap-1 align-middle leading-[1.5] [&>svg]:size-[12px] [&>svg]:shrink-0' /** * Live chip: an entity icon + label matching the chat input's mention rendering. Where the host opted @@ -37,7 +37,6 @@ export function MentionChipView({ node, editor }: ReactNodeViewProps) { const params = useParams() const { kind, id, label } = node.attrs as MentionAttrs const Icon = mentionIcon(kind, id, label) as StyleableIcon | undefined - const iconStyle = Icon ? getBareIconStyle(Icon) : undefined const navigable = editor.storage.mentionMenu?.navigable === true const workspaceId = typeof params.workspaceId === 'string' ? params.workspaceId : undefined const path = navigable && workspaceId ? simLinkPath(workspaceId, kind, id) : null @@ -55,7 +54,7 @@ export function MentionChipView({ node, editor }: ReactNodeViewProps) { onClick={path ? handleClick : undefined} title={label} > - {Icon && } + {Icon && } {label} ) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/chat-context-kind-registry/chat-context-kind-registry.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/chat-context-kind-registry/chat-context-kind-registry.tsx index 1a251e0bc4a..122040a0483 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/chat-context-kind-registry/chat-context-kind-registry.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/chat-context-kind-registry/chat-context-kind-registry.tsx @@ -12,7 +12,7 @@ import { import { AgentSkillsIcon, McpIcon } from '@/components/icons' import { getDocumentIcon } from '@/components/icons/document-icons' import type { ChatContextKind, ChatMessageContext } from '@/app/workspace/[workspaceId]/home/types' -import { getBareIconStyle } from '@/blocks/brand-icon-style' +import { BrandIcon } from '@/blocks/brand-icon' import { getBlockRegistry } from '@/blocks/registry' interface RenderIconArgs { @@ -42,8 +42,7 @@ function renderIntegrationTile({ context, className }: RenderIconArgs): ReactNod if (!context.blockType) return null const block = getBlockRegistry()[context.blockType] if (!block) return null - const Icon = block.icon - return + return } /** diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx index cfddbb4815c..d03f52d0ccb 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx @@ -12,7 +12,7 @@ import { import { getReadTargetBlock } from '@/lib/copilot/tools/client/read-block' import { extractStreamingStringArgument } from '@/lib/copilot/tools/streaming-args' import { getToolStatusDisplayTitle, getWaitCountdownTitle } from '@/lib/copilot/tools/tool-display' -import { getBareIconStyle } from '@/blocks/brand-icon-style' +import { BrandIcon } from '@/blocks/brand-icon' import { getBlockByToolName } from '@/blocks/registry' import type { ToolCallData, ToolCallStatus } from '../../../../types' import { resolveToolDisplayState } from '../../utils' @@ -237,12 +237,7 @@ export function ToolCallItem({ return (
- {BlockIcon && ( - - )} + {BlockIcon && } {isExecuting ? ( {title} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx index c79acb6a2e0..ddde93e34cb 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx @@ -1,6 +1,6 @@ 'use client' -import { createElement, lazy, Suspense, useCallback, useEffect, useMemo, useState } from 'react' +import { lazy, Suspense, useCallback, useEffect, useMemo, useState } from 'react' import { ArrowRight, Check, @@ -61,6 +61,7 @@ import type { import { useServiceAccountConnectTarget } from '@/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/use-service-account-connect' import { useWorkspaceHostContext } from '@/app/workspace/[workspaceId]/providers/workspace-host-provider' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' +import { BrandIcon } from '@/blocks/brand-icon' import { useUpdateWorkspaceCredential, useWorkspaceCredential, @@ -2160,7 +2161,7 @@ function ServiceAccountConnectDisplay({ 'hover-hover:bg-[var(--surface-5)]' )} > - {createElement(target.serviceIcon, { className: 'size-[16px] shrink-0' })} + {displayLabel} {connected ? ( @@ -2257,7 +2258,7 @@ function CredentialLinkDisplay({ 'hover-hover:bg-[var(--surface-5)]' )} > - {createElement(Icon, { className: 'size-[16px] shrink-0' })} + {displayLabel} {connected ? ( diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx index 22750f369a5..12af4420995 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx @@ -20,7 +20,7 @@ import type { MothershipResource, MothershipResourceType, } from '@/app/workspace/[workspaceId]/home/types' -import { getBareIconStyle, type StyleableIcon } from '@/blocks/brand-icon-style' +import { BrandIcon, type StyleableIcon } from '@/blocks/brand-icon' import { logKeys } from '@/hooks/queries/logs' import { mothershipChatKeys } from '@/hooks/queries/mothership-chats' import { folderKeys } from '@/hooks/queries/utils/folder-keys' @@ -85,10 +85,7 @@ function IntegrationDropdownItem({ item }: DropdownItemRenderProps) { if (!Icon) return {item.name} return ( <> - + {item.name} ) 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..32f878c1a27 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 @@ -16,7 +16,7 @@ import { } from '@/lib/integrations' import { captureEvent } from '@/lib/posthog/client' import { ConnectOAuthModal } from '@/app/workspace/[workspaceId]/components/connect-oauth-modal' -import { getBareIconStyle } from '@/blocks/brand-icon-style' +import { BrandIcon } from '@/blocks/brand-icon' import { getAllBlockMeta } from '@/blocks/registry' import type { ModuleTag } from '@/blocks/types' import { useWorkspaceCredentials } from '@/hooks/queries/credentials' @@ -389,10 +389,7 @@ export function SuggestedActions({ onSelectPrompt }: SuggestedActionsProps) { i > 0 && 'border-t' )} > - + {action.label} diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/client-credential-account-modal.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/client-credential-account-modal.tsx index 4dbbc05545d..631d99be287 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/client-credential-account-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/client-credential-account-modal.tsx @@ -20,6 +20,7 @@ import { type ClientCredentialAccountFieldId, partitionClientCredentialFields, } from '@/lib/credentials/client-credential-accounts/descriptors' +import { withBrandIcon } from '@/blocks/brand-icon' import { useCreateWorkspaceCredential, useUpdateWorkspaceCredential, @@ -214,7 +215,7 @@ export function ClientCredentialAccountModal({ onOpenChange={onOpenChange} srTitle={`Add ${serviceName} ${descriptor.connectNoun}`} > - onOpenChange(false)}> + onOpenChange(false)}> Add {serviceName} {descriptor.connectNoun} diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/connect-service-account-modal.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/connect-service-account-modal.tsx index d8f21c6adbc..0c87db8c28d 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/connect-service-account-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/connect-service-account-modal.tsx @@ -30,6 +30,7 @@ import { import { ClientCredentialAccountModal } from '@/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/client-credential-account-modal' import { TokenServiceAccountModal } from '@/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/token-service-account-modal' import { ConnectSlackBotModal } from '@/app/workspace/[workspaceId]/integrations/components/connect-slack-bot-modal/connect-slack-bot-modal' +import { withBrandIcon } from '@/blocks/brand-icon' import { useCreateWorkspaceCredential, useUpdateWorkspaceCredential, @@ -360,7 +361,7 @@ function GoogleServiceAccountModal({ onOpenChange={onOpenChange} srTitle={`Add ${serviceName} service account`} > - onOpenChange(false)}> + onOpenChange(false)}> Add {serviceName} service account @@ -512,7 +513,7 @@ function AtlassianServiceAccountModal({ onOpenChange={onOpenChange} srTitle={`Add ${serviceName} service account`} > - onOpenChange(false)}> + onOpenChange(false)}> Add {serviceName} service account diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/token-service-account-modal.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/token-service-account-modal.tsx index 25967bb9917..16daa225452 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/token-service-account-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/token-service-account-modal.tsx @@ -17,6 +17,7 @@ import { type TokenServiceAccountDescriptor, type TokenServiceAccountField, } from '@/lib/credentials/token-service-accounts/descriptors' +import { withBrandIcon } from '@/blocks/brand-icon' import { useCreateWorkspaceCredential, useUpdateWorkspaceCredential, @@ -141,7 +142,7 @@ export function TokenServiceAccountModal({ onOpenChange={onOpenChange} srTitle={`Add ${serviceName} ${descriptor.connectNoun}`} > - onOpenChange(false)}> + onOpenChange(false)}> Add {serviceName} {descriptor.connectNoun} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx index 5164b2dde68..6d61db64eaa 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx @@ -93,6 +93,7 @@ import { getDocumentIcon } from '@/app/workspace/[workspaceId]/knowledge/compone import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks' +import { BrandIcon } from '@/blocks/brand-icon' import { CONNECTOR_META_REGISTRY } from '@/connectors/registry' import { useKnowledgeBase, useKnowledgeBaseDocuments } from '@/hooks/kb/use-knowledge' import { @@ -1093,7 +1094,7 @@ export function KnowledgeBase({ {connector.status === 'syncing' ? ( ) : ( - ConnectorIcon && + ConnectorIcon && )} {connector.status !== 'active' && connector.status !== 'syncing' && ( ({ label: cred.name || cred.provider, value: cred.id, - icon: connectorConfig.icon, + icon: withBrandIcon(connectorConfig.icon), }) ), { diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal.tsx index d23554278a9..c2e13149394 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal.tsx @@ -27,6 +27,7 @@ import type { } from '@/app/workspace/[workspaceId]/knowledge/[id]/hooks/use-connector-config-fields' import { useConnectorConfigFields } from '@/app/workspace/[workspaceId]/knowledge/[id]/hooks/use-connector-config-fields' import { useWorkspaceHostContext } from '@/app/workspace/[workspaceId]/providers/workspace-host-provider' +import { withBrandIcon } from '@/blocks/brand-icon' import { CONNECTOR_META_REGISTRY } from '@/connectors/registry' import type { ConnectorConfigField, ConnectorMeta } from '@/connectors/types' import type { ConnectorData } from '@/hooks/queries/kb/connectors' @@ -274,7 +275,7 @@ export function EditConnectorModal({ size='md' dismissDisabled={isSaving} > - onOpenChange(false)}> + onOpenChange(false)}> Edit {displayName} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx index d6227a72955..6176f1abe9b 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx @@ -80,6 +80,7 @@ import { import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks' +import { BrandIcon } from '@/blocks/brand-icon' import { CONNECTOR_META_REGISTRY } from '@/connectors/registry' import { useKnowledgeBasesList } from '@/hooks/kb/use-knowledge' import { useCreateFolder, useDeleteFolderMutation, useUpdateFolder } from '@/hooks/queries/folders' @@ -167,8 +168,8 @@ function connectorCell(connectorTypes?: string[]): ResourceCell { return ( - - + + {def.name} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx index a4d21d33df0..0ab6e2fab39 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx @@ -24,7 +24,7 @@ import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider' -import { getBareIconStyle, type StyleableIcon } from '@/blocks/brand-icon-style' +import { BrandIcon } from '@/blocks/brand-icon' import type { SubBlockConfig } from '@/blocks/types' import { useWorkspaceCredential, useWorkspaceCredentials } from '@/hooks/queries/credentials' import { useOAuthCredentials } from '@/hooks/queries/oauth/oauth-credentials' @@ -233,8 +233,7 @@ export function CredentialSelector({ if (!baseProviderConfig) { return } - const Icon: StyleableIcon = baseProviderConfig.icon - return + return }, []) const getProviderName = useCallback((providerName: OAuthProvider) => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/credential-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/credential-selector.tsx index 8d6be7b8e21..17be9d48172 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/credential-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/credential-selector.tsx @@ -1,6 +1,6 @@ 'use client' -import { createElement, useCallback, useMemo, useRef, useState } from 'react' +import { useCallback, useMemo, useRef, useState } from 'react' import { Button, Combobox } from '@sim/emcn' import { SquareArrowUpRight } from '@sim/emcn/icons' import { useParams } from 'next/navigation' @@ -19,6 +19,7 @@ import { ConnectOAuthModal } from '@/app/workspace/[workspaceId]/components/conn import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight' import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider' +import { BrandIcon } from '@/blocks/brand-icon' import { useWorkspaceCredential } from '@/hooks/queries/credentials' import { useOAuthCredentials } from '@/hooks/queries/oauth/oauth-credentials' import { useWorkflowMap } from '@/hooks/queries/workflows' @@ -32,7 +33,7 @@ const getProviderIcon = (providerName: OAuthProvider) => { if (!baseProviderConfig) { return } - return createElement(baseProviderConfig.icon, { className: 'size-3' }) + return } const getProviderName = (providerName: OAuthProvider) => { diff --git a/apps/sim/blocks/brand-icon-style.ts b/apps/sim/blocks/brand-icon-style.ts deleted file mode 100644 index 4e29977cb23..00000000000 --- a/apps/sim/blocks/brand-icon-style.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Brand-icon styling that needs the block registry, kept apart from the pure - * contrast helpers in `@/blocks/icon-color`. - * - * The split is by dependency, not by topic: this module reads `getAllBlocks()`, - * so importing it pulls `blocks/registry-maps` → all 282 block configs → the - * tool registry. `icon-color.ts` needs none of that, and it is imported by the - * public landing `/integrations` page — which was inheriting the whole registry - * purely because the two lived in one file. - * - * Every consumer here is under `app/workspace/**`, where the registry is - * already legitimately present. - */ - -import type { ComponentType, CSSProperties } from 'react' -import { getAllBlocks } from '@/blocks/registry' - -/** A brand icon component that accepts standard styling props. */ -export type StyleableIcon = ComponentType<{ className?: string; style?: CSSProperties }> - -/** - * Lazily-built lookup from a block's icon component to its theme-safe brand - * {@link BlockConfig.iconColor}. Keyed by component reference so callers that - * already hold the icon (suggested actions, credential pickers, …) never need - * to thread a block type or hand-pick a color. Built once on first read since - * the block registry is static for the app's lifetime. - */ -let iconColorByComponent: Map | null = null - -function getIconColorMap(): Map { - if (iconColorByComponent) return iconColorByComponent - const map = new Map() - for (const block of getAllBlocks()) { - if (block.iconColor) map.set(block.icon, block.iconColor) - } - iconColorByComponent = map - return map -} - -/** - * Inline `style` for rendering a brand icon bare (without its colored tile - * background): the block's theme-safe {@link BlockConfig.iconColor} as `color`, - * or `undefined` when none is defined so the caller keeps its own default - * icon styling. - * - * Single-fill icons drawn with `fill='currentColor'` (e.g. HubSpot) adopt the - * color; multi-color brand icons that hardcode their own fills (Slack, Gmail, - * Jira, Salesforce, Google Calendar) ignore it and keep their own colors. - */ -export function getBareIconStyle(icon: StyleableIcon): CSSProperties | undefined { - const color = getIconColorMap().get(icon) - return color ? { color } : undefined -} diff --git a/apps/sim/blocks/brand-icon.test.tsx b/apps/sim/blocks/brand-icon.test.tsx new file mode 100644 index 00000000000..75e62f7a95e --- /dev/null +++ b/apps/sim/blocks/brand-icon.test.tsx @@ -0,0 +1,51 @@ +/** + * @vitest-environment node + */ +import { renderToStaticMarkup } from 'react-dom/server' +import { describe, expect, it, vi } from 'vitest' +import { DropboxIcon } from '@/components/icons' +import { OAUTH_PROVIDERS } from '@/lib/oauth' +import { BrandIcon, withBrandIcon } from '@/blocks/brand-icon' +import { getAllBlocks } from '@/blocks/registry' + +vi.mocked(getAllBlocks).mockReturnValue([ + { icon: DropboxIcon, iconColor: '#0061FF' }, +] as unknown as ReturnType) + +interface PlainIconProps { + className?: string +} + +function PlainIcon({ className }: PlainIconProps) { + return +} + +describe('BrandIcon', () => { + it('tints a registered brand glyph with its block color', () => { + const markup = renderToStaticMarkup() + + expect(markup).toContain('color:#0061FF') + expect(markup).toContain('size-[16px]') + }) + + it('falls back to the muted icon token when no brand color is registered', () => { + const markup = renderToStaticMarkup() + + expect(markup).not.toContain('color:') + expect(markup).toContain('text-[var(--text-icon)]') + }) + + it('gives an OAuth connect surface the color the chat surfaces already use', () => { + const serviceIcon = OAUTH_PROVIDERS.dropbox.services.dropbox.icon + const markup = renderToStaticMarkup() + + expect(markup).toContain('color:#0061FF') + }) +}) + +describe('withBrandIcon', () => { + it('returns a stable component per icon so an icon slot never remounts', () => { + expect(withBrandIcon(DropboxIcon)).toBe(withBrandIcon(DropboxIcon)) + expect(withBrandIcon(PlainIcon)).not.toBe(withBrandIcon(DropboxIcon)) + }) +}) diff --git a/apps/sim/blocks/brand-icon.tsx b/apps/sim/blocks/brand-icon.tsx new file mode 100644 index 00000000000..a1137642629 --- /dev/null +++ b/apps/sim/blocks/brand-icon.tsx @@ -0,0 +1,88 @@ +import type { ComponentType, CSSProperties } from 'react' +import { cn } from '@sim/emcn' +import { getAllBlocks } from '@/blocks/registry' + +/** A brand icon component that accepts standard styling props. */ +export type StyleableIcon = ComponentType<{ className?: string; style?: CSSProperties }> + +/** + * Lazily-built lookup from a block's icon component to its theme-safe brand + * `BlockConfig.iconColor`. Keyed by component reference so callers that + * already hold the icon never need to thread a block type or hand-pick a + * color. Built once on first read since the block registry is static for the + * app's lifetime. + */ +let iconColorByComponent: Map | null = null + +function getIconColor(icon: StyleableIcon): string | undefined { + if (!iconColorByComponent) { + const map = new Map() + for (const block of getAllBlocks()) { + if (block.iconColor) map.set(block.icon, block.iconColor) + } + iconColorByComponent = map + } + return iconColorByComponent.get(icon) +} + +export interface BrandIconProps { + /** The service's brand glyph — a block, OAuth provider, or connector icon. */ + icon: StyleableIcon + /** Sizing and layout only; the component owns the icon's color. */ + className?: string +} + +/** + * The one bare brand glyph: a service icon drawn without its + * `BlockConfig.bgColor` tile. Counterpart to `BlockTile`, which + * owns the tiled treatment. + * + * Color is the component's, not the caller's. Single-fill icons drawn with + * `fill='currentColor'` (Dropbox, HubSpot, …) take their registered brand + * color; multi-color icons that hardcode their own fills (Slack, Gmail, Jira) + * ignore it; anything with no registered color falls back to `--text-icon` so + * a glyph never inherits an arbitrary surrounding text color. + * + * Reaches the block registry, so it is for workspace surfaces only — the + * public landing `/integrations` page uses the registry-free helpers in + * `@/blocks/icon-color` instead. + */ +export function BrandIcon({ icon: Icon, className }: BrandIconProps) { + const color = getIconColor(Icon) + return ( + + ) +} + +/** + * Stable {@link BrandIcon} wrappers, keyed by the icon they wrap so repeated + * renders hand the same component reference back and React never remounts the + * slot it fills. + */ +const brandIconComponents = new WeakMap>() + +export interface BrandIconSlotProps { + /** Supplied by the host slot; sizing and layout only. */ + className?: string +} + +/** + * Adapts a brand glyph for the `icon` prop slots that take a component rather + * than an element — `ChipModalHeader`, dropdown options, command rows — so + * those surfaces get the same treatment as a direct {@link BrandIcon}. + */ +export function withBrandIcon(icon: StyleableIcon): ComponentType { + const cached = brandIconComponents.get(icon) + if (cached) return cached + + function BrandIconSlot({ className }: BrandIconSlotProps) { + return + } + BrandIconSlot.displayName = `BrandIcon(${icon.displayName ?? icon.name ?? 'Icon'})` + + brandIconComponents.set(icon, BrandIconSlot) + return BrandIconSlot +} diff --git a/apps/sim/blocks/icon-color.ts b/apps/sim/blocks/icon-color.ts index 5332a473573..083cd0ddffd 100644 --- a/apps/sim/blocks/icon-color.ts +++ b/apps/sim/blocks/icon-color.ts @@ -2,7 +2,7 @@ * Contrast helpers for brand tiles. Pure colour maths — deliberately free of any * `@/blocks/registry` import so the public landing `/integrations` page can use * these without pulling 282 block configs and the tool registry into its bundle. - * Registry-backed icon styling lives in `@/blocks/brand-icon-style`. + * Registry-backed icon styling lives in `@/blocks/brand-icon`. */ import { isLightColor } from '@/lib/colors'