From 0dbc2093b057fb0e577c89f4ee4328e18cbaff8c Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 12 Aug 2026 11:30:41 -0700 Subject: [PATCH] fix(blocks): give a block one tile everywhere it is listed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A block's tile disagreed with the card it named. The canvas brands only third-party integrations and gives everything first-party its role accent, but the command palette, connection lists, tag menus and output pickers all painted straight from the catalog `bgColor` — so Webhook Trigger showed green in the palette and blue on the canvas it was about to be dropped onto, and the five roleless first-party triggers showed catalog blues where the canvas shows neutral. Read the canvas rule from one place (`hasBlockAccent`) and render it through one component (`BlockTile`), then point every surface that lists a block at them: canvas, editor header, preview, toolbar, palette, connection picker, terminal, logs trace rows, connection lists, tag menus, output pickers and the tables workflow sidebar. Folds in the duplication the split had grown: three copies of `TagIcon`, five hand-rolled tile divs, the toolbar's second encoding of the accent rule, a third icon-contrast helper on its own brightness threshold, and the dead `showColoredIcon` prop every caller passed. Tiles now share the chip radius, and the tile forces its own icon colour so popover and command rows painting `[&_svg]:text-*` can no longer wash out a pale brand tile. Large detail headers (preview panel, trace-view detail) keep their own treatment and are left for a follow-up. --- .../components/trace-view/trace-view.tsx | 19 ++-- .../logs/components/log-details/utils.ts | 6 -- .../enrichment-details/enrichment-details.tsx | 6 +- .../workflow-sidebar/workflow-sidebar.tsx | 43 ++-------- .../output-select/output-select.tsx | 61 ++----------- .../connection-block-selector.tsx | 10 +-- .../connection-blocks/connection-blocks.tsx | 35 +------- .../components/tag-dropdown/tag-dropdown.tsx | 58 +++---------- .../panel/components/editor/editor.tsx | 3 +- .../panel/components/toolbar/toolbar.tsx | 35 ++------ .../entry-block-tile/entry-block-tile.tsx | 26 ++---- .../components/terminal/utils.test.ts | 50 +---------- .../[workflowId]/components/terminal/utils.ts | 18 ---- .../workflow-block/workflow-block.tsx | 3 +- .../preview-editor/preview-editor.tsx | 20 +---- .../components/block/block.tsx | 3 +- .../command-items/command-items.tsx | 28 +----- .../search-groups/search-groups.tsx | 16 ++-- .../search-modal/search-modal.test.tsx | 48 +++++++++++ .../sidebar/components/search-modal/utils.ts | 8 +- apps/sim/blocks/accent.test.ts | 71 +++++++++++++++ apps/sim/blocks/accent.ts | 53 ++++++++++++ apps/sim/blocks/block-tile.tsx | 86 +++++++++++++++++++ 23 files changed, 339 insertions(+), 367 deletions(-) create mode 100644 apps/sim/blocks/accent.test.ts create mode 100644 apps/sim/blocks/accent.ts create mode 100644 apps/sim/blocks/block-tile.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx index 6365ec2e8ce..80c4fea424f 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx @@ -42,11 +42,12 @@ import { getDisplayName, hasErrorInTree, hasUnhandledErrorInTree, - iconColorClass, isIterationType, parseTime, } from '@/app/workspace/[workspaceId]/logs/components/log-details/utils' +import { BlockTile } from '@/blocks/block-tile' import { isCustomBlockType } from '@/blocks/custom/build-config' +import { getTileIconColorClass } from '@/blocks/icon-color' import { useCodeViewerFeatures } from '@/hooks/use-code-viewer' const DEFAULT_TREE_PANE_WIDTH = 240 @@ -331,12 +332,12 @@ const TraceTreeRow = memo(function TraceTreeRow({
)} {!isIterationType(span.type) && ( -
- {BlockIcon && } -
+ )} @@ -711,7 +712,9 @@ const TraceDetailPane = memo(function TraceDetailPane({ span }: { span: TraceSpa className='mt-[2px] flex size-[18px] flex-shrink-0 items-center justify-center overflow-hidden rounded-sm [&_img]:size-full' style={{ background: bgColor }} > - {BlockIcon && } + {BlockIcon && ( + + )}
)}
diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts index 4b993050ecd..b3dc95416bb 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts @@ -86,12 +86,6 @@ export function getBlockIconAndColor( */ const MAX_YIQ_SUM = 255_000 -/** Returns 'text-white' for dark backgrounds, dark text for light ones. */ -export function iconColorClass(bgColor: string): string { - const brightness = perceivedBrightness(bgColor) - return brightness !== null && brightness > 160_000 / MAX_YIQ_SUM ? 'text-[#111111]' : 'text-white' -} - /** * Near-black bgColors disappear against the dark-mode surface (--bg: #1b1b1b). * Below the brightness threshold we fall back to the neutral block color used diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichment-details/enrichment-details.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichment-details/enrichment-details.tsx index c8995487beb..677cd5eedf1 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichment-details/enrichment-details.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichment-details/enrichment-details.tsx @@ -7,10 +7,10 @@ import type { EnrichmentProviderOutcome, EnrichmentRunDetail } from '@/lib/table import { adjustBgForContrast, getBlockIconAndColor, - iconColorClass, } from '@/app/workspace/[workspaceId]/logs/components/log-details/utils' import { useLogDetailsResize } from '@/app/workspace/[workspaceId]/logs/hooks' import { formatDate } from '@/app/workspace/[workspaceId]/logs/utils' +import { getTileIconColorClass } from '@/blocks/icon-color' import { useEnrichmentDetail } from '@/hooks/queries/tables' import { formatCost } from '@/providers/utils' import { useLogDetailsUIStore } from '@/stores/logs/store' @@ -255,7 +255,9 @@ function EnrichmentDetailsContent({ style={{ background: bgColor }} > {ProviderIcon && ( - + )}
diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/workflow-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/workflow-sidebar.tsx index 9dbb32fce28..d2d486561c5 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/workflow-sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/workflow-sidebar.tsx @@ -1,6 +1,5 @@ 'use client' -import type React from 'react' import { useMemo, useState } from 'react' import { Button, @@ -18,7 +17,7 @@ import { Tooltip, toast, } from '@sim/emcn' -import { ArrowLeft, ChevronDown, Repeat, Split, SquareArrowUpRight, X } from '@sim/emcn/icons' +import { ArrowLeft, ChevronDown, SquareArrowUpRight, X } from '@sim/emcn/icons' import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import { useMutation, useQueryClient } from '@tanstack/react-query' @@ -57,8 +56,7 @@ import { RequiredLabel, } from '@/app/workspace/[workspaceId]/tables/[tableId]/components/sidebar-fields' import { PreviewWorkflow } from '@/app/workspace/[workspaceId]/w/components/preview' -import { getBlock } from '@/blocks' -import { getTileIconColorClass } from '@/blocks/icon-color' +import { BlockTile } from '@/blocks/block-tile' import { useAddWorkflowGroup, useUpdateColumn, @@ -140,8 +138,6 @@ interface BlockOutputGroup { blockId: string blockName: string blockType: string - blockIcon: string | React.ComponentType<{ className?: string }> - blockColor: string paths: string[] } @@ -164,25 +160,6 @@ function tableColumnTypeToInputType(colType: ColumnDefinition['type'] | undefine return columnTypeById(colType).workflowInputType } -const TagIcon: React.FC<{ - icon: string | React.ComponentType<{ className?: string }> - color: string -}> = ({ icon, color }) => ( -
- {typeof icon === 'string' ? ( - {icon} - ) : ( - (() => { - const IconComponent = icon - return - })() - )} -
-) - /** * Right-edge sidebar for workflow group configuration. Three flows: * - create a new group (workflow + outputs + deps), @@ -468,20 +445,10 @@ export function WorkflowSidebarBody({ for (const f of flat) { let group = groupsByBlockId.get(f.blockId) if (!group) { - const blockConfig = getBlock(f.blockType) - const blockColor = blockConfig?.bgColor || '#2F55FF' - let blockIcon: string | React.ComponentType<{ className?: string }> = f.blockName - .charAt(0) - .toUpperCase() - if (blockConfig?.icon) blockIcon = blockConfig.icon - else if (f.blockType === 'loop') blockIcon = Repeat - else if (f.blockType === 'parallel') blockIcon = Split group = { blockId: f.blockId, blockName: f.blockName, blockType: f.blockType, - blockIcon, - blockColor, paths: [], } groupsByBlockId.set(f.blockId, group) @@ -504,7 +471,11 @@ export function WorkflowSidebarBody({ section: group.blockName, sectionElement: (
- + {group.blockName}
), diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx index 4b254aafc04..f9fb46787dc 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx @@ -1,47 +1,18 @@ 'use client' -import type React from 'react' import { useMemo } from 'react' import { ChipCombobox, Combobox, type ComboboxOptionGroup, cn } from '@sim/emcn' -import { Repeat, Split } from '@sim/emcn/icons' import { useShallow } from 'zustand/react/shallow' import { type FlattenOutputsBlockInput, flattenWorkflowOutputs, } from '@/lib/workflows/blocks/flatten-outputs' -import { getBlock } from '@/blocks' -import { getTileIconColorClass } from '@/blocks/icon-color' +import { BlockTile } from '@/blocks/block-tile' import { normalizeName } from '@/executor/constants' import { useWorkflowDiffStore } from '@/stores/workflow-diff/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' -/** - * Renders a tag icon with background color for block section headers. - * - * @param icon - Either a letter string or a Lucide icon component - * @param color - Background color for the icon container - * @returns A styled icon element - */ -const TagIcon: React.FC<{ - icon: string | React.ComponentType<{ className?: string }> - color: string -}> = ({ icon, color }) => ( -
- {typeof icon === 'string' ? ( - {icon} - ) : ( - (() => { - const IconComponent = icon - return - })() - )} -
-) - const EMPTY_OUTPUTS: string[] = [] /** @@ -200,16 +171,6 @@ export function OutputSelect({ return `${validOutputs.length} outputs` }, [selectedOutputs, workflowOutputs, placeholder]) - /** - * Gets the background color for a block output based on its type - * @param blockType - The type of the block - * @returns The hex color code for the block - */ - const getOutputColor = (blockType: string) => { - const blockConfig = getBlock(blockType) - return blockConfig?.bgColor || '#2F55FF' - } - /** * Groups outputs by block and sorts by distance from starter block. * Returns ComboboxOptionGroup[] for use with Combobox. @@ -261,25 +222,15 @@ export function OutputSelect({ return sortedGroups.map(({ blockName, outputs }) => { const firstOutput = outputs[0] - const blockConfig = getBlock(firstOutput.blockType) - const blockColor = getOutputColor(firstOutput.blockType) - - let blockIcon: string | React.ComponentType<{ className?: string }> = blockName - .charAt(0) - .toUpperCase() - - if (blockConfig?.icon) { - blockIcon = blockConfig.icon - } else if (firstOutput.blockType === 'loop') { - blockIcon = Repeat - } else if (firstOutput.blockType === 'parallel') { - blockIcon = Split - } return { sectionElement: (
- + {blockName}
), diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/connection-block-selector/connection-block-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/connection-block-selector/connection-block-selector.tsx index 110ede75791..2295f32810c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/connection-block-selector/connection-block-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/connection-block-selector/connection-block-selector.tsx @@ -433,8 +433,9 @@ export function ConnectionBlockSelector({ id, data }: NodeProps ))} @@ -453,7 +454,7 @@ export function ConnectionBlockSelector({ id, data }: NodeProps handleToolOperationSelect(result.item)} icon={result.item.icon} bgColor={result.item.bgColor} - showColoredIcon + blockType={result.item.blockType} label={result.item.name} /> ) @@ -470,8 +471,7 @@ export function ConnectionBlockSelector({ id, data }: NodeProps ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/connection-blocks/connection-blocks.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/connection-blocks/connection-blocks.tsx index da9c088a6b2..5d39c1b100e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/connection-blocks/connection-blocks.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/connection-blocks/connection-blocks.tsx @@ -2,7 +2,6 @@ import { useCallback, useRef, useState } from 'react' import { ChevronDown, handleKeyboardActivation } from '@sim/emcn' -import { Repeat, Split } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import clsx from 'clsx' import { useShallow } from 'zustand/react/shallow' @@ -12,8 +11,7 @@ import { } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/connection-blocks/components/field-item/field-item' import type { ConnectedBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-block-connections' import { useBlockOutputFields } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-output-fields' -import { getTileIconColorClass } from '@/blocks/icon-color' -import { getBlock } from '@/blocks/registry' +import { BlockTile } from '@/blocks/block-tile' import { normalizeName } from '@/executor/constants' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { EMPTY_SUBBLOCK_VALUES, useSubBlockStore } from '@/stores/workflows/subblock/store' @@ -103,8 +101,6 @@ function ConnectionItem({ mergedSubBlocks, sourceBlock, }: ConnectionItemProps) { - const blockConfig = getBlock(connection.type) - const fields = useBlockOutputFields({ blockId: connection.id, blockType: connection.type, @@ -113,19 +109,6 @@ function ConnectionItem({ }) const hasFields = fields.length > 0 - let Icon = blockConfig?.icon - let bgColor = blockConfig?.bgColor || '#6B7280' - - if (!blockConfig) { - if (connection.type === 'loop') { - Icon = Repeat - bgColor = '#2FB3FF' - } else if (connection.type === 'parallel') { - Icon = Split - bgColor = '#FEE12B' - } - } - return (
onToggleExpand(connection.id)) }} > -
- {Icon && ( - - )} -
+ { return convertToNestedTags(root, '', blockName) } -const TagIcon: React.FC<{ - icon: string | React.ComponentType<{ className?: string }> - color: string -}> = ({ icon, color }) => ( -
- {typeof icon === 'string' ? ( - {icon} - ) : ( - (() => { - const IconComponent = icon - return - })() - )} -
-) - /** * Props for the recursive NestedTagRenderer component */ @@ -811,8 +791,7 @@ const BlockRootTagItem: React.FC<{ handleTagSelect: (tag: string, group?: BlockTagGroup) => void itemRefs: React.RefObject> group: BlockTagGroup - tagIcon: string | React.ComponentType<{ className?: string }> - blockColor: string + blockType: string blockName: string }> = ({ rootTag, @@ -822,8 +801,7 @@ const BlockRootTagItem: React.FC<{ handleTagSelect, itemRefs, group, - tagIcon, - blockColor, + blockType, blockName, }) => { const handleMouseEnter = useKeyboardAwareMouseEnter(setSelectedIndex) @@ -844,7 +822,11 @@ const BlockRootTagItem: React.FC<{ } }} > - + {blockName} ) @@ -1727,7 +1709,7 @@ export const TagDropdown: React.FC = ({ <>
- + Variables
@@ -1753,25 +1735,6 @@ export const TagDropdown: React.FC = ({ )} {nestedBlockTagGroups.map((group: NestedBlockTagGroup, groupIndex: number) => { - const blockConfig = getBlock(group.blockType) - let blockColor = blockConfig?.bgColor || BLOCK_COLORS.DEFAULT - - if (group.blockType === 'loop') { - blockColor = BLOCK_COLORS.LOOP - } else if (group.blockType === 'parallel') { - blockColor = BLOCK_COLORS.PARALLEL - } - - let tagIcon: string | React.ComponentType<{ className?: string }> = - group.blockName.charAt(0).toUpperCase() - if (blockConfig?.icon) { - tagIcon = blockConfig.icon - } else if (group.blockType === 'loop') { - tagIcon = Repeat - } else if (group.blockType === 'parallel') { - tagIcon = Split - } - const normalizedBlockName = normalizeName(group.blockName) const rootTagFromTags = group.tags.find((tag) => tag === normalizedBlockName) const rootTag = rootTagFromTags || normalizedBlockName @@ -1788,8 +1751,7 @@ export const TagDropdown: React.FC = ({ handleTagSelect={handleTagSelect} itemRefs={itemRefs} group={group} - tagIcon={tagIcon} - blockColor={blockColor} + blockType={group.blockType} blockName={group.blockName} /> {group.nestedTags.map((nestedTag) => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx index 1503710b6d8..d2a7c102be4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx @@ -54,6 +54,7 @@ import { isBlockProtected, } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils/block-protection-utils' import { PreviewWorkflow } from '@/app/workspace/[workspaceId]/w/components/preview' +import { hasBlockAccent } from '@/blocks/accent' import { isLightTileColor } from '@/blocks/icon-color' import { getBlock } from '@/blocks/registry' import { useFolderMap } from '@/hooks/queries/folders' @@ -107,7 +108,7 @@ export function Editor() { const currentBlock = currentBlockId ? currentWorkflow.getBlockById(currentBlockId) : null const blockConfig = currentBlock ? getBlock(currentBlock.type) : null const typeAccent = getWorkflowTypeAccent(currentBlock?.type ?? '') - const isIntegration = blockConfig?.category === 'tools' + const isIntegration = blockConfig != null && !hasBlockAccent(blockConfig.type) const title = currentBlock?.name || 'Editor' const isBlockNameSearchHighlighted = activeSearchTarget?.targetKind === 'block-name' && activeSearchTarget.blockId === currentBlockId diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx index 5cdfdda7f5a..cffd670abbf 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx @@ -21,7 +21,6 @@ import { Info, } from '@sim/emcn' import { ChevronDown, Search } from '@sim/emcn/icons' -import { hasWorkflowTypeRole, WorkflowTypeIcon } from '@sim/workflow-renderer' import clsx from 'clsx' import { useParams } from 'next/navigation' import { usePostHog } from 'posthog-js/react' @@ -34,6 +33,7 @@ import { import { useToolbarItemInteractions } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/hooks' import { LoopTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/loop/loop-config' import { ParallelTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/parallel/parallel-config' +import { BlockTile } from '@/blocks/block-tile' import { buildCustomBlockConfig, CUSTOM_BLOCK_TILE_COLOR, @@ -41,7 +41,6 @@ import { } from '@/blocks/custom/build-config' import { useCustomBlockOverlayVersion } from '@/blocks/custom/client-overlay' import { getCustomBlockIcon } from '@/blocks/custom/custom-block-icon' -import { getTileIconColorClass } from '@/blocks/icon-color' import { getCanonicalBlocksByCategory } from '@/blocks/registry' import type { BlockConfig } from '@/blocks/types' import { useOrgBrandConfig } from '@/ee/whitelabeling/components/branding-provider' @@ -57,7 +56,6 @@ interface BlockItem { config?: BlockConfig icon?: ComponentType<{ className?: string }> bgColor?: string - workflowType?: string docsLink?: string } @@ -134,26 +132,12 @@ const ToolbarItem = memo(function ToolbarItem({ )} onKeyDown={handleKeyDown} > - {item.workflowType && Icon ? ( - - ) : ( -
- {Icon && ( - - )} -
- )} + {item.name}
) @@ -210,7 +194,6 @@ function getTriggers(overlayVersion: number): BlockItem[] { config: trigger, icon: trigger.icon, bgColor: trigger.bgColor, - workflowType: hasWorkflowTypeRole(trigger.type) ? trigger.type : undefined, docsLink: trigger.docsLink, })) } @@ -247,7 +230,6 @@ function ensureBlockCaches() { config: block, icon: block.icon, bgColor: block.bgColor, - workflowType: block.type, })) regularBlockItems.push({ @@ -255,7 +237,6 @@ function ensureBlockCaches() { type: LoopTool.type, icon: LoopTool.icon, bgColor: LoopTool.bgColor, - workflowType: LoopTool.type, docsLink: LoopTool.docsLink, }) @@ -264,7 +245,6 @@ function ensureBlockCaches() { type: ParallelTool.type, icon: ParallelTool.icon, bgColor: ParallelTool.bgColor, - workflowType: ParallelTool.type, docsLink: ParallelTool.docsLink, }) @@ -274,7 +254,6 @@ function ensureBlockCaches() { config: block, icon: block.icon, bgColor: block.bgColor, - workflowType: hasWorkflowTypeRole(block.type) ? block.type : undefined, })) regularBlockItems.sort((a, b) => a.name.localeCompare(b.name)) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/entry-block-tile/entry-block-tile.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/entry-block-tile/entry-block-tile.tsx index b0ffdf95f69..88ada367e9a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/entry-block-tile/entry-block-tile.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/entry-block-tile/entry-block-tile.tsx @@ -1,35 +1,23 @@ 'use client' import { memo } from 'react' -import { chipIconSlotClass, cn } from '@sim/emcn' -import { WorkflowTypeIcon } from '@sim/workflow-renderer' import { getBlockColor, getBlockIcon, - getEntryAccentType, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils' -import { getTileIconColorClass } from '@/blocks/icon-color' +import { BlockTile } from '@/blocks/block-tile' export interface EntryBlockTileProps { blockType: string } -/** A log row's block tile. @see getEntryAccentType */ +/** A log row's block tile. @see BlockTile */ export const EntryBlockTile = memo(function EntryBlockTile({ blockType }: EntryBlockTileProps) { - const BlockIcon = getBlockIcon(blockType) - const bgColor = getBlockColor(blockType) - const accentType = getEntryAccentType(blockType) - - if (BlockIcon && accentType) { - return - } - return ( -
- {BlockIcon && } -
+ ) }) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.test.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.test.ts index e1b8c5f0f72..ee07dba42dd 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.test.ts @@ -1,7 +1,7 @@ /** * @vitest-environment node */ -import { afterEach, describe, expect, it, vi } from 'vitest' +import { describe, expect, it, vi } from 'vitest' vi.mock('@/blocks', () => ({ getBlock: vi.fn().mockReturnValue(null), @@ -17,13 +17,11 @@ vi.mock('@/stores/constants', () => ({ TERMINAL_BLOCK_COLUMN_WIDTH: { MIN: 120, DEFAULT: 200, MAX: 400 }, })) -import { getBlock } from '@/blocks' import type { ConsoleEntry } from '@/stores/terminal' import { buildEntryTree, type EntryNode, flattenVisibleExecutionRows, - getEntryAccentType, groupEntriesByExecution, } from './utils' @@ -858,49 +856,3 @@ describe('flattenVisibleExecutionRows', () => { expect(rowsExpanded[1].depth).toBe(1) }) }) - -describe('getEntryAccentType', () => { - const mockedGetBlock = vi.mocked(getBlock) - - afterEach(() => { - mockedGetBlock.mockReturnValue(null as never) - }) - - function withCategory(category: string) { - mockedGetBlock.mockReturnValue({ category } as never) - } - - it('accents a core block by its type, mapped or not', () => { - withCategory('blocks') - expect(getEntryAccentType('agent')).toBe('agent') - /* - * An unmapped core block still takes the accent path and lands on `neutral`, - * which is exactly what the block toolbar does for a newly added one — the - * two surfaces must not disagree while the role map catches up. - */ - expect(getEntryAccentType('brand_new_core_block')).toBe('brand_new_core_block') - }) - - it('accents a non-core block only when it carries a canvas role', () => { - withCategory('tools') - expect(getEntryAccentType('table')).toBe('table') - // A role-less integration keeps its own provider colour instead. - expect(getEntryAccentType('gmail')).toBeUndefined() - - withCategory('triggers') - expect(getEntryAccentType('schedule')).toBe('schedule') - expect(getEntryAccentType('some_vendor_trigger')).toBeUndefined() - }) - - it('accents subflows, which carry a role but no registry config', () => { - expect(getEntryAccentType('loop')).toBe('loop') - expect(getEntryAccentType('parallel')).toBe('parallel') - expect(getEntryAccentType('workflow')).toBe('workflow') - }) - - it('leaves the terminal-synthesized run rows on their own status fill', () => { - expect(getEntryAccentType('error')).toBeUndefined() - expect(getEntryAccentType('validation')).toBeUndefined() - expect(getEntryAccentType('cancelled')).toBeUndefined() - }) -}) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.ts index d1b5aa1f07d..c8c04655e6d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.ts @@ -1,6 +1,5 @@ import type React from 'react' import { Ban, CircleX, Repeat, Split, TriangleAlert, Workflow } from '@sim/emcn/icons' -import { hasWorkflowTypeRole } from '@sim/workflow-renderer' import { getBlock } from '@/blocks' import { isWorkflowBlockType } from '@/executor/constants' import { TERMINAL_BLOCK_COLUMN_WIDTH } from '@/stores/constants' @@ -94,23 +93,6 @@ export function getBlockColor(blockType: string): string { return '#6b7280' } -/** - * The type a log row's tile takes its accent from, or `undefined` when the row - * must fall back to the block's own provider colour. - * - * Same rule the block toolbar applies, so a block is accented identically - * wherever it is listed: a core block always takes the canvas role accent (an - * unmapped one lands on `neutral`, exactly as it does in the toolbar), and - * anything else — integrations, triggers, subflows — takes one only if it has a - * role. That second clause is what leaves the terminal's synthesized - * `error`/`validation`/`cancelled` rows on their own status fill: they carry no - * config and no role, so they fall through to the provider-colour branch. - */ -export function getEntryAccentType(blockType: string): string | undefined { - const isCoreBlock = getBlock(blockType)?.category === 'blocks' - return isCoreBlock || hasWorkflowTypeRole(blockType) ? blockType : undefined -} - /** * Determines if a keyboard event originated from a text-editable element */ diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index 889eb2aff47..ad3f911e2fd 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -84,6 +84,7 @@ import { useIsBlockInActiveExecutionHandoff, } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks' import { useBlockDimensions } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions' +import { hasBlockAccent } from '@/blocks/accent' import { useCustomBlockOverlayVersion } from '@/blocks/custom/client-overlay' import { getBlock } from '@/blocks/registry' import { @@ -1219,7 +1220,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({ isExecutionHighlighted={isExecutionHighlighted} Icon={config.icon} iconBgColor={config.bgColor} - isIntegration={config.category === 'tools'} + isIntegration={!hasBlockAccent(config.type)} horizontalHandles={horizontalHandles} shouldShowDefaultHandles={shouldShowDefaultHandles} blockHeight={blockHeight} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx index c359228792f..6a852272a8d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx @@ -45,6 +45,7 @@ import { PreviewContextMenu } from '@/app/workspace/[workspaceId]/w/components/p import { PreviewWorkflow } from '@/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow' import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks' import { getBlock } from '@/blocks' +import { BlockTile } from '@/blocks/block-tile' import { getTileIconColorClass } from '@/blocks/icon-color' import type { BlockConfig, BlockIcon, SubBlockConfig, SubBlockType } from '@/blocks/types' import { normalizeName } from '@/executor/constants' @@ -356,9 +357,6 @@ function ConnectionsSection({ {/* Content - styled like ConnectionBlocks */}
{connections.map((connection) => { - const blockConfig = getBlock(connection.blockType) - const Icon = blockConfig?.icon - const bgColor = blockConfig?.bgColor || '#6B7280' const isExpanded = expandedBlocks.has(connection.blockId) const hasFields = connection.fields.length > 0 @@ -379,21 +377,7 @@ function ConnectionsSection({ handleKeyboardActivation(event, () => toggleBlock(connection.blockId)) }} > -
- {Icon && ( - - )} -
+ typeLabel={canvasPresentation.typeLabel} Icon={IconComponent} iconBgColor={blockConfig.bgColor} - isIntegration={blockConfig.category === 'tools'} + isIntegration={!hasBlockAccent(type)} isEnabled={enabled} /> )} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/command-items.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/command-items.tsx index 5e53d16e56d..c67b2089917 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/command-items.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/command-items.tsx @@ -2,14 +2,12 @@ import type { ComponentType } from 'react' import { memo } from 'react' -import { cn } from '@sim/emcn' import { File, Workflow } from '@sim/emcn/icons' -import { WorkflowTypeIcon } from '@sim/workflow-renderer' import { Command } from 'cmdk' import { HEX_COLOR_REGEX } from '@/lib/branding' import type { CommandItemProps } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils' import { COMMAND_ITEM_CLASSNAME } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils' -import { getTileIconColorClass } from '@/blocks/icon-color' +import { BlockTile } from '@/blocks/block-tile' interface ResultMetaProps { meta?: string @@ -85,31 +83,14 @@ export const MemoizedCommandItem = memo( onSelect, icon: Icon, bgColor, - showColoredIcon, - workflowType, + blockType, label, labelPrefix, meta, }: CommandItemProps) { return ( - {workflowType ? ( - - ) : ( -
- -
- )} + {labelPrefix && {labelPrefix} } {label} @@ -122,8 +103,7 @@ export const MemoizedCommandItem = memo( prev.value === next.value && prev.icon === next.icon && prev.bgColor === next.bgColor && - prev.showColoredIcon === next.showColoredIcon && - prev.workflowType === next.workflowType && + prev.blockType === next.blockType && prev.label === next.label && prev.labelPrefix === next.labelPrefix && prev.meta === next.meta diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/search-groups/search-groups.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/search-groups/search-groups.tsx index 2ff8c4cdaeb..5277dd70a84 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/search-groups/search-groups.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/search-groups/search-groups.tsx @@ -42,8 +42,7 @@ export const BlocksGroup = memo(function BlocksGroup({ onSelect={() => onSelect(block)} icon={block.icon} bgColor={block.bgColor} - showColoredIcon - workflowType={block.type} + blockType={block.type} label={block.name} /> ))} @@ -69,7 +68,7 @@ export const ToolsGroup = memo(function ToolsGroup({ onSelect={() => onSelect(tool)} icon={tool.icon} bgColor={tool.bgColor} - showColoredIcon + blockType={tool.type} label={tool.name} /> ))} @@ -108,8 +107,7 @@ function renderSearchEntry( onSelect={() => handlers.onSelectBlock(entry.item)} icon={entry.item.icon} bgColor={entry.item.bgColor} - showColoredIcon - workflowType={entry.item.type} + blockType={entry.item.type} label={entry.item.name} /> ) @@ -121,7 +119,7 @@ function renderSearchEntry( onSelect={() => handlers.onSelectTool(entry.item)} icon={entry.item.icon} bgColor={entry.item.bgColor} - showColoredIcon + blockType={entry.item.type} label={entry.item.name} /> ) @@ -133,7 +131,7 @@ function renderSearchEntry( onSelect={() => handlers.onSelectTrigger(entry.item)} icon={entry.item.icon} bgColor={entry.item.bgColor} - showColoredIcon + blockType={entry.item.type} label={entry.item.name} /> ) @@ -145,7 +143,7 @@ function renderSearchEntry( onSelect={() => handlers.onSelectToolOperation(entry.item)} icon={entry.item.icon} bgColor={entry.item.bgColor} - showColoredIcon + blockType={entry.item.blockType} labelPrefix={entry.item.serviceName} label={entry.item.name} /> @@ -158,7 +156,6 @@ function renderSearchEntry( onSelect={() => handlers.onSelectConnectedAccount(entry.item)} icon={entry.item.icon} bgColor={entry.item.bgColor} - showColoredIcon label={entry.item.name} /> ) @@ -170,7 +167,6 @@ function renderSearchEntry( onSelect={() => handlers.onSelectIntegration(entry.item)} icon={entry.item.icon} bgColor={entry.item.bgColor} - showColoredIcon label={entry.item.name} /> ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.test.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.test.tsx index e2baf2296b0..e757551e688 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.test.tsx @@ -10,6 +10,7 @@ import { type MothershipSendMessageDetail, } from '@/lib/mothership/events' import { SearchModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal' +import { getBlock } from '@/blocks/registry' const { mockPush, mockSearchState } = vi.hoisted(() => ({ mockPush: vi.fn(), @@ -691,6 +692,53 @@ describe('SearchModal', () => { } }) + it('accents a first-party trigger by its canvas role, not its catalog color', async () => { + const Icon = () => null + const original = { ...mockSearchState.data } + /* + * The palette reads the accent from the block's category, so the row's + * appearance is only meaningful against a registry that reports one — the + * shared mock omits it. + */ + const mockedGetBlock = vi.mocked(getBlock) + const originalGetBlock = mockedGetBlock.getMockImplementation() + mockedGetBlock.mockImplementation( + (type: string) => ({ category: type === 'slack' ? 'tools' : 'triggers', icon: Icon }) as never + ) + mockSearchState.data = { + ...mockSearchState.data, + triggers: [ + { + id: 'generic_webhook', + name: 'Webhook Trigger', + icon: Icon, + bgColor: '#10B981', + type: 'generic_webhook', + }, + { + id: 'slack', + name: 'Slack', + icon: Icon, + bgColor: '#611f69', + type: 'slack', + }, + ], + } + + try { + await act(async () => { + root.render() + }) + + expect(document.querySelector('[data-workflow-type-icon="generic_webhook"]')).not.toBeNull() + // A third-party trigger keeps its brand tile, exactly as the canvas paints it. + expect(document.querySelector('[data-workflow-type-icon="slack"]')).toBeNull() + } finally { + mockSearchState.data = original + if (originalGetBlock) mockedGetBlock.mockImplementation(originalGetBlock) + } + }) + it('keeps the palette open when the query handoff cannot be persisted', async () => { const onOpenChange = vi.fn() const storeSpy = vi.spyOn(MothershipHandoffStorage, 'store').mockReturnValue(false) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils.ts index 00f32405d08..7403ae52c85 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils.ts @@ -186,13 +186,11 @@ export interface CommandItemProps { onSelect: () => void icon: ComponentType<{ className?: string }> bgColor: string - showColoredIcon?: boolean /** - * Core workflow block type. Renders as the shared accent chip only when the - * type has a mapped accent; unmapped types — every integration block — fall - * back to their catalog `bgColor` tile. + * Block the row names, when it names one. Decides whether the tile takes the + * canvas role accent or the row's own `bgColor`. @see hasBlockAccent */ - workflowType?: string + blockType?: string /** Primary text of the row. */ label: string /** De-emphasized lead-in before the label (e.g. a tool operation's service). */ diff --git a/apps/sim/blocks/accent.test.ts b/apps/sim/blocks/accent.test.ts new file mode 100644 index 00000000000..4d8f12fb2f1 --- /dev/null +++ b/apps/sim/blocks/accent.test.ts @@ -0,0 +1,71 @@ +/** + * @vitest-environment node + */ +import { afterEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@/blocks/registry', () => ({ + getBlock: vi.fn().mockReturnValue(null), +})) + +import { hasBlockAccent } from '@/blocks/accent' +import { getBlock } from '@/blocks/registry' + +describe('hasBlockAccent', () => { + const mockedGetBlock = vi.mocked(getBlock) + + afterEach(() => { + mockedGetBlock.mockReturnValue(null as never) + }) + + function withCategory(category: string) { + mockedGetBlock.mockReturnValue({ category } as never) + } + + it('accents a core block, mapped or not', () => { + withCategory('blocks') + expect(hasBlockAccent('agent')).toBe(true) + /* + * An unmapped core block still takes the accent and lands on `neutral`, + * which is what the canvas does for a newly added one — the surfaces must + * not disagree while the role map catches up. + */ + expect(hasBlockAccent('brand_new_core_block')).toBe(true) + }) + + it('accents every first-party trigger, role or no role', () => { + withCategory('triggers') + // Catalogued green, but an `interface` block on the canvas. + expect(hasBlockAccent('generic_webhook')).toBe(true) + expect(hasBlockAccent('schedule')).toBe(true) + /* + * Roleless triggers are the same bug one step further out: the canvas + * paints them `neutral`, so a list keyed off the role map alone would have + * put them back on the catalog blues this rule exists to overrule. + */ + expect(hasBlockAccent('manual_trigger')).toBe(true) + expect(hasBlockAccent('circleback')).toBe(true) + }) + + it('leaves a third-party integration on its brand, even if it gains a role', () => { + withCategory('tools') + expect(hasBlockAccent('gmail')).toBe(false) + /* + * Role membership must not override the category. The canvas brands every + * `tools` block, so accenting one here would reintroduce the mismatch in + * the opposite direction. + */ + expect(hasBlockAccent('table')).toBe(false) + }) + + it('accents configless subflows, which carry a role', () => { + expect(hasBlockAccent('loop')).toBe(true) + expect(hasBlockAccent('parallel')).toBe(true) + expect(hasBlockAccent('workflow')).toBe(true) + }) + + it('leaves the terminal-synthesized run rows on their own status fill', () => { + expect(hasBlockAccent('error')).toBe(false) + expect(hasBlockAccent('validation')).toBe(false) + expect(hasBlockAccent('cancelled')).toBe(false) + }) +}) diff --git a/apps/sim/blocks/accent.ts b/apps/sim/blocks/accent.ts new file mode 100644 index 00000000000..f9ad1101a0d --- /dev/null +++ b/apps/sim/blocks/accent.ts @@ -0,0 +1,53 @@ +import type { ComponentType } from 'react' +import { Repeat, Split } from '@sim/emcn/icons' +import { hasWorkflowTypeRole } from '@sim/workflow-renderer' +import { getBlock } from '@/blocks/registry' + +/** Tile fill for a block that has no config of its own to colour it. */ +export const DEFAULT_BLOCK_TILE_COLOR = '#6B7280' + +/** + * Subflow tiles. Loop and Parallel are canvas blocks with no registry config, + * so every surface that lists them had to special-case the pair; they resolve + * here instead. + */ +const SUBFLOW_ICONS: Record> = { + loop: Repeat, + parallel: Split, +} + +/** + * Whether a block's tile takes the canvas role accent rather than the block's + * own provider colour. This is the canvas's own rule, read from one place so + * every surface that lists a block agrees with the card it names: only a + * third-party integration wears its brand, everything first-party wears its + * role accent — landing on `neutral` when the role map has no entry yet, + * exactly as the canvas does. + * + * Keying off the catalog `bgColor` instead is what made a palette row + * contradict the card it was about to place: `generic_webhook` is catalogued + * green but is an `interface` block on the canvas, and the five roleless + * first-party triggers (`api_trigger`, `chat_trigger`, `circleback`, + * `input_trigger`, `manual_trigger`) are neutral there while their configs + * carry blues and a gradient. + * + * A type with no config at all falls back to role membership, which accents the + * subflows (`loop`, `parallel`, `workflow`) and leaves the terminal's + * synthesized `error`/`validation`/`cancelled` rows on their status fill. + */ +export function hasBlockAccent(blockType: string): boolean { + const config = getBlock(blockType) + return config ? config.category !== 'tools' : hasWorkflowTypeRole(blockType) +} + +/** A block's tile icon, including the subflows that carry no registry config. */ +export function getBlockTileIcon( + blockType: string +): ComponentType<{ className?: string }> | undefined { + return getBlock(blockType)?.icon ?? SUBFLOW_ICONS[blockType] +} + +/** A block's provider tile fill. Only reached when it takes no accent. */ +export function getBlockTileColor(blockType: string): string { + return getBlock(blockType)?.bgColor || DEFAULT_BLOCK_TILE_COLOR +} diff --git a/apps/sim/blocks/block-tile.tsx b/apps/sim/blocks/block-tile.tsx new file mode 100644 index 00000000000..c43c77ac94b --- /dev/null +++ b/apps/sim/blocks/block-tile.tsx @@ -0,0 +1,86 @@ +'use client' + +import type { ComponentType, HTMLAttributes } from 'react' +import { chipIconSlotClass, cn } from '@sim/emcn' +import { WorkflowTypeIcon } from '@sim/workflow-renderer' +import { getBlockTileColor, getBlockTileIcon, hasBlockAccent } from '@/blocks/accent' +import { getTileIconColorClass } from '@/blocks/icon-color' + +/** Slot sizes the tile ships in: the canvas 16px chip, or 14px for dense rows. */ +const TILE_SIZE_CLASS = { + md: 'size-[16px]', + sm: 'size-[14px]', +} as const + +export interface BlockTileProps extends Omit, 'children' | 'style'> { + /** + * Block the tile represents; decides whether it takes the canvas role accent. + * Omitted by rows that name no block — a catalog integration, a section + * header — which keep their own fill. @see hasBlockAccent + */ + blockType?: string + /** Defaults to the block's registered icon. */ + icon?: ComponentType<{ className?: string }> + /** Provider fill, used only when the block takes no accent. */ + bgColor?: string + /** Drawn on the provider tile when there is no icon — usually a name initial. */ + fallbackLabel?: string + size?: keyof typeof TILE_SIZE_CLASS +} + +/** + * The one block tile. Renders the shared canvas accent chip for anything that + * carries a role and the block's provider tile for everything else, so a block + * reads the same in every list it appears in. @see hasBlockAccent + * + * The tile owns its icon colour outright, which is why the contrast class is + * always the `!important` variant: these rows live inside popover, combobox and + * command surfaces that paint descendants through `[&_svg]:text-*`, and a plain + * utility on the icon loses to that parent rule — pale brand tiles would render + * their icon white-on-white. + * + * Reaches the block registry, so it is for workspace surfaces only. Public + * marketing pages keep their own tile rather than pull every block config into + * that bundle — see `@/blocks/icon-color`. + */ +export function BlockTile({ + blockType, + icon, + bgColor, + fallbackLabel, + size = 'md', + className, + ...props +}: BlockTileProps) { + const Icon = icon ?? (blockType ? getBlockTileIcon(blockType) : undefined) + const sizeClass = cn(TILE_SIZE_CLASS[size], className) + + if (blockType && Icon && hasBlockAccent(blockType)) { + return + } + + const fill = bgColor ?? (blockType ? getBlockTileColor(blockType) : undefined) + + return ( +
+ {Icon ? ( + + ) : ( + fallbackLabel && ( + + {fallbackLabel} + + ) + )} +
+ ) +}