Skip to content

Commit d4de73b

Browse files
committed
feat(workflow): unify core block colors
1 parent 4c6bfb9 commit d4de73b

7 files changed

Lines changed: 213 additions & 77 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/components/drag-preview.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
export interface DragItemInfo {
55
name: string
66
bgColor: string
7-
iconElement?: HTMLElement | null
7+
iconContainer?: HTMLElement | null
88
}
99

1010
/**
@@ -32,25 +32,23 @@ export function createDragPreview(info: DragItemInfo): HTMLElement {
3232
z-index: 9999;
3333
`
3434

35-
const iconContainer = document.createElement('div')
36-
iconContainer.style.cssText = `
37-
width: 24px;
38-
height: 24px;
39-
border-radius: 6px;
40-
background: ${info.bgColor};
41-
display: flex;
42-
align-items: center;
43-
justify-content: center;
44-
flex-shrink: 0;
45-
`
35+
const iconContainer = info.iconContainer
36+
? (info.iconContainer.cloneNode(true) as HTMLElement)
37+
: document.createElement('div')
38+
iconContainer.style.width = '24px'
39+
iconContainer.style.height = '24px'
40+
iconContainer.style.borderRadius = '6px'
41+
iconContainer.style.display = 'flex'
42+
iconContainer.style.alignItems = 'center'
43+
iconContainer.style.justifyContent = 'center'
44+
iconContainer.style.flexShrink = '0'
45+
if (!info.iconContainer) iconContainer.style.background = info.bgColor
4646

47-
if (info.iconElement) {
48-
const clonedIcon = info.iconElement.cloneNode(true) as HTMLElement
47+
const clonedIcon = iconContainer.querySelector<HTMLElement>('svg, img')
48+
if (clonedIcon) {
4949
clonedIcon.style.width = '16px'
5050
clonedIcon.style.height = '16px'
51-
clonedIcon.style.color = 'white'
5251
clonedIcon.style.flexShrink = '0'
53-
iconContainer.appendChild(clonedIcon)
5452
}
5553

5654
const text = document.createElement('span')

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,16 @@ import {
2121
Info,
2222
} from '@sim/emcn'
2323
import { ChevronDown, Search } from '@sim/emcn/icons'
24+
import { WorkflowTypeIcon } from '@sim/workflow-renderer'
2425
import clsx from 'clsx'
2526
import { useParams } from 'next/navigation'
2627
import { usePostHog } from 'posthog-js/react'
2728
import { captureEvent } from '@/lib/posthog/client'
2829
import { getTriggersForSidebar, hasTriggerCapability } from '@/lib/workflows/triggers/trigger-utils'
29-
import { ToolbarItemContextMenu } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/components'
30+
import {
31+
type DragItemInfo,
32+
ToolbarItemContextMenu,
33+
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/components'
3034
import { useToolbarItemInteractions } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/hooks'
3135
import { LoopTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/loop/loop-config'
3236
import { ParallelTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/parallel/parallel-config'
@@ -53,6 +57,7 @@ interface BlockItem {
5357
config?: BlockConfig
5458
icon?: ComponentType<{ className?: string }>
5559
bgColor?: string
60+
workflowType?: string
5661
docsLink?: string
5762
}
5863

@@ -63,7 +68,7 @@ interface ToolbarItemProps {
6368
e: React.DragEvent<HTMLElement>,
6469
type: string,
6570
enableTriggerMode: boolean,
66-
dragItemInfo?: { name: string; bgColor: string; iconElement: HTMLElement | null }
71+
dragItemInfo?: DragItemInfo
6772
) => void
6873
onClick: (type: string, enableTriggerMode: boolean) => void
6974
onContextMenu: (e: React.MouseEvent, type: string, isTrigger: boolean, docsLink?: string) => void
@@ -83,11 +88,11 @@ const ToolbarItem = memo(function ToolbarItem({
8388

8489
const handleDragStart = useCallback(
8590
(e: React.DragEvent<HTMLElement>) => {
86-
const iconElement = e.currentTarget.querySelector('.toolbar-item-icon')
91+
const iconContainer = e.currentTarget.querySelector<HTMLElement>('[data-toolbar-item-icon]')
8792
onDragStart(e, item.type, isTriggerCapable, {
8893
name: item.name,
8994
bgColor: item.bgColor ?? '#666666',
90-
iconElement: iconElement as HTMLElement | null,
95+
iconContainer,
9196
})
9297
},
9398
[item.type, item.name, item.bgColor, isTriggerCapable, onDragStart]
@@ -129,21 +134,26 @@ const ToolbarItem = memo(function ToolbarItem({
129134
)}
130135
onKeyDown={handleKeyDown}
131136
>
132-
<div
133-
className='relative flex size-[16px] flex-shrink-0 items-center justify-center overflow-hidden rounded-sm [&_img]:size-full'
134-
style={{ background: item.bgColor }}
135-
>
136-
{Icon && (
137-
<Icon
138-
className={clsx(
139-
'toolbar-item-icon transition-transform duration-200',
140-
getTileIconColorClass(item.bgColor),
141-
'group-hover:scale-110',
142-
'size-[10px]'
143-
)}
144-
/>
145-
)}
146-
</div>
137+
{item.workflowType && Icon ? (
138+
<WorkflowTypeIcon type={item.workflowType} Icon={Icon} data-toolbar-item-icon='' />
139+
) : (
140+
<div
141+
data-toolbar-item-icon=''
142+
className='relative flex size-[16px] flex-shrink-0 items-center justify-center overflow-hidden rounded-sm [&_img]:size-full'
143+
style={{ background: item.bgColor }}
144+
>
145+
{Icon && (
146+
<Icon
147+
className={clsx(
148+
'transition-transform duration-200',
149+
getTileIconColorClass(item.bgColor),
150+
'group-hover:scale-110',
151+
'size-[10px]'
152+
)}
153+
/>
154+
)}
155+
</div>
156+
)}
147157
<span className='min-w-0 flex-1 truncate text-[var(--text-body)]'>{item.name}</span>
148158
</div>
149159
)
@@ -236,13 +246,15 @@ function ensureBlockCaches() {
236246
config: block,
237247
icon: block.icon,
238248
bgColor: block.bgColor,
249+
workflowType: block.type,
239250
}))
240251

241252
regularBlockItems.push({
242253
name: LoopTool.name,
243254
type: LoopTool.type,
244255
icon: LoopTool.icon,
245256
bgColor: LoopTool.bgColor,
257+
workflowType: LoopTool.type,
246258
docsLink: LoopTool.docsLink,
247259
})
248260

@@ -251,6 +263,7 @@ function ensureBlockCaches() {
251263
type: ParallelTool.type,
252264
icon: ParallelTool.icon,
253265
bgColor: ParallelTool.bgColor,
266+
workflowType: ParallelTool.type,
254267
docsLink: ParallelTool.docsLink,
255268
})
256269

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/command-items.tsx

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
import type { ComponentType } from 'react'
44
import { memo } from 'react'
5-
import { ChipTag, cn } from '@sim/emcn'
5+
import { cn } from '@sim/emcn'
66
import { File, Workflow } from '@sim/emcn/icons'
7-
import { getWorkflowTypeAccent } from '@sim/workflow-renderer'
7+
import { WorkflowTypeIcon } from '@sim/workflow-renderer'
88
import { Command } from 'cmdk'
99
import type { CommandItemProps } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils'
1010
import { COMMAND_ITEM_CLASSNAME } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils'
@@ -20,18 +20,10 @@ export const MemoizedCommandItem = memo(
2020
workflowType,
2121
label,
2222
}: CommandItemProps) {
23-
const workflowAccent = workflowType ? getWorkflowTypeAccent(workflowType) : null
24-
2523
return (
2624
<Command.Item value={value} onSelect={onSelect} className={COMMAND_ITEM_CLASSNAME}>
27-
{workflowAccent ? (
28-
<ChipTag
29-
variant={workflowAccent.variant}
30-
tone={workflowAccent.tone}
31-
className='size-[16px] flex-shrink-0 justify-center p-0'
32-
>
33-
<Icon className='size-[10px] transition-transform duration-100 group-hover:scale-110' />
34-
</ChipTag>
25+
{workflowType ? (
26+
<WorkflowTypeIcon type={workflowType} Icon={Icon} />
3527
) : (
3628
<div
3729
className='relative flex size-[16px] flex-shrink-0 items-center justify-center overflow-hidden rounded-sm [&_img]:size-full'

apps/sim/lib/workflows/blocks/workflow-block-view-interaction.test.tsx

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22
* @vitest-environment jsdom
33
*/
44
import { act } from 'react'
5-
import { getWorkflowTypeAccent, WorkflowBlockView, WorkflowTypeTag } from '@sim/workflow-renderer'
5+
import {
6+
getWorkflowTypeAccent,
7+
getWorkflowTypeRole,
8+
WorkflowBlockView,
9+
WorkflowTypeIcon,
10+
WorkflowTypeTag,
11+
} from '@sim/workflow-renderer'
612
import { createRoot, type Root } from 'react-dom/client'
713
import { ReactFlowProvider } from 'reactflow'
814
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
@@ -172,14 +178,55 @@ describe('WorkflowBlockView action menu', () => {
172178
})
173179
})
174180

175-
describe('WorkflowTypeTag integration colors', () => {
176-
it('keeps the selected Sim-native accents', () => {
181+
describe('WorkflowTypeTag colors', () => {
182+
it('maps core blocks through semantic workflow roles', () => {
183+
expect(getWorkflowTypeRole('agent')).toBe('agentic')
184+
expect(getWorkflowTypeRole('api')).toBe('interface')
185+
expect(getWorkflowTypeRole('condition')).toBe('logic')
186+
expect(getWorkflowTypeRole('credential')).toBe('state')
187+
expect(getWorkflowTypeRole('router_v2')).toBe('flow')
188+
expect(getWorkflowTypeRole('table')).toBe('records')
189+
expect(getWorkflowTypeRole('a2a')).toBe('neutral')
190+
expect(getWorkflowTypeRole('image_generator_v2')).toBe('generative')
191+
expect(getWorkflowTypeRole('knowledge')).toBe('knowledge')
192+
177193
expect(getWorkflowTypeAccent('agent')).toEqual({ variant: 'workflow', tone: 'inverse' })
178194
expect(getWorkflowTypeAccent('api')).toEqual({ variant: 'workflow', tone: 'blue' })
179-
expect(getWorkflowTypeAccent('loop')).toEqual({ variant: 'solid', tone: 'neutral' })
180-
expect(getWorkflowTypeAccent('parallel')).toEqual({ variant: 'workflow', tone: 'yellow' })
181-
expect(getWorkflowTypeAccent('router')).toEqual({ variant: 'workflow', tone: 'orange' })
182-
expect(getWorkflowTypeAccent('router_v2')).toEqual({ variant: 'workflow', tone: 'orange' })
195+
expect(getWorkflowTypeAccent('loop')).toEqual({ variant: 'workflow', tone: 'ash' })
196+
expect(getWorkflowTypeAccent('parallel')).toEqual({ variant: 'workflow', tone: 'ash' })
197+
expect(getWorkflowTypeAccent('router')).toEqual({ variant: 'workflow', tone: 'ash' })
198+
expect(getWorkflowTypeAccent('condition')).toEqual({ variant: 'workflow', tone: 'orange' })
199+
expect(getWorkflowTypeAccent('image_generator_v2')).toEqual({
200+
variant: 'workflow',
201+
tone: 'purple',
202+
})
203+
expect(getWorkflowTypeAccent('knowledge')).toEqual({ variant: 'workflow', tone: 'cyan' })
204+
})
205+
206+
it('renders compact workflow icons with their canonical fill and ink', () => {
207+
const host = document.createElement('div')
208+
document.body.appendChild(host)
209+
const root = createRoot(host)
210+
mountedRoots.add(root)
211+
mountedHosts.add(host)
212+
213+
act(() =>
214+
root.render(
215+
<>
216+
<WorkflowTypeIcon type='knowledge' Icon={TestIcon} />
217+
<WorkflowTypeIcon type='image_generator_v2' Icon={TestIcon} />
218+
</>
219+
)
220+
)
221+
222+
expect(host.querySelector('[data-workflow-type-icon="knowledge"]')).toHaveClass(
223+
'bg-[#00BBD0]',
224+
'text-[#1A1A1A]'
225+
)
226+
expect(host.querySelector('[data-workflow-type-icon="image_generator_v2"]')).toHaveClass(
227+
'bg-[#AA00FF]',
228+
'text-[#F8F8F8]'
229+
)
183230
})
184231

185232
it('uses the provider background with a contrasting shared icon and label color', () => {

packages/emcn/src/components/chip-tag/chip-tag.tsx

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,10 @@ import { cn } from '../../lib/cn'
4646
* redundant cue rather than the sole carrier of the information — but do not
4747
* reuse either pairing anywhere the label stands alone.
4848
*
49-
* `neutral` is the only tone that is not a solid fill — an unmapped block
50-
* type reads as a white, outlined slot rather than as one more colour in the
51-
* set. Every other tone is fill-only, so it is also the only one whose edge
52-
* depends on the ring rather than on the fill itself.
49+
* `neutral` is the only tone that is not a solid fill — neutral/system blocks
50+
* and unmapped block types read as white, outlined slots rather than as one
51+
* more colour in the set. Every other tone is fill-only, so it is also the
52+
* only one whose edge depends on the ring rather than on the fill itself.
5353
* - `brand` — a provider-owned integration colour supplied through
5454
* `brandColor`. Pair with `brandForeground` so both the icon and label use
5555
* the same contrast rule as integration tiles elsewhere in the product.
@@ -82,6 +82,8 @@ const chipTagVariants = cva(
8282
blue: '',
8383
green: '',
8484
yellow: '',
85+
purple: '',
86+
cyan: '',
8587
},
8688
brandForeground: {
8789
light: '',
@@ -97,10 +99,10 @@ const chipTagVariants = cva(
9799
{
98100
variant: 'workflow',
99101
tone: 'neutral',
100-
/* The only outlined tone. An unmapped block type reads as an empty
101-
slot rather than a colour, so the fill is plain white and an inset
102-
ring — not a border — carries the edge, keeping the tag the same
103-
size as every filled sibling. */
102+
/* The only outlined tone. Neutral/system and unmapped block types read
103+
as empty slots rather than a colour, so the fill is plain white and
104+
an inset ring — not a border — carries the edge, keeping the tag the
105+
same size as every filled sibling. */
104106
className: 'bg-[#FFFFFF] text-[#1A1A1A] shadow-[inset_0_0_0_1px_#C3C3C3]',
105107
},
106108
{ variant: 'workflow', tone: 'inverse', className: 'bg-[#3B3B3B] text-[#F8F8F8]' },
@@ -109,6 +111,8 @@ const chipTagVariants = cva(
109111
{ variant: 'workflow', tone: 'blue', className: 'bg-[#0062FF] text-[#F8F8F8]' },
110112
{ variant: 'workflow', tone: 'green', className: 'bg-[#188F00] text-[#F8F8F8]' },
111113
{ variant: 'workflow', tone: 'yellow', className: 'bg-[#FFEF08] text-[#1A1A1A]' },
114+
{ variant: 'workflow', tone: 'purple', className: 'bg-[#AA00FF] text-[#F8F8F8]' },
115+
{ variant: 'workflow', tone: 'cyan', className: 'bg-[#00BBD0] text-[#1A1A1A]' },
112116
{ variant: 'brand', brandForeground: 'light', className: 'text-[#FFFFFF]' },
113117
{ variant: 'brand', brandForeground: 'dark', className: 'text-[#000000]' },
114118
],

packages/workflow-renderer/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,12 @@ export {
4343
getErrorSourceHandleStyle,
4444
getNearestBranchCursorHandleId,
4545
getWorkflowTypeAccent,
46+
getWorkflowTypeRole,
4647
WorkflowBlockView,
4748
type WorkflowBlockViewProps,
49+
WorkflowTypeIcon,
50+
type WorkflowTypeIconProps,
51+
type WorkflowTypeRole,
4852
WorkflowTypeTag,
4953
type WorkflowTypeTagProps,
5054
} from './workflow-block/workflow-block-view'

0 commit comments

Comments
 (0)