Skip to content

Commit 5dcd647

Browse files
committed
fix(icons): render brand icons legibly when bare and on light tiles
Monochrome brand icons hardcoded a single white or black fill matched to their colored tile, so they vanished when rendered bare on the home Suggested actions list (white-on-white in light mode, black-on-black in dark mode). Convert those marks to currentColor so they adapt to context, and make tile foregrounds contrast-aware via getTileIconColorClass instead of a hardcoded text-white. Also centralize all color math in apps/sim/lib/colors (perceived brightness, hex/rgb/hsl conversion, contrast-text) and route every consumer through it: the bare-icon audit, block tiles, logs trace view, whitelabeling theming, workspace presence, and the PPTX renderer no longer carry duplicate copies. Adds a bare-icon CI audit (scripts/check-bare-icons.ts) and authoring guidance.
1 parent 4298e57 commit 5dcd647

37 files changed

Lines changed: 642 additions & 260 deletions

File tree

.claude/commands/add-block.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,13 @@ Please provide the SVG and I'll convert it to a React component.
732732
You can usually find this in the service's brand/press kit page, or copy it from their website.
733733
```
734734

735+
When converting the SVG: a **monochrome** logo (single white or black mark) must
736+
use `fill='currentColor'`, never a hardcoded `#fff`/`#000000`. Block icons render
737+
both inside their `bgColor` tile and "bare" on a neutral page (the home Suggested
738+
actions list) in light and dark mode; a hardcoded white/black mark goes invisible
739+
bare on the matching background. Multi-color brand logos keep their own fills.
740+
Verify with `bun run check:bare-icons`.
741+
735742
## Advanced Mode for Optional Fields
736743

737744
Optional fields that are rarely used should be set to `mode: 'advanced'` so they don't clutter the basic UI. This includes:

.claude/commands/add-integration.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,31 @@ Once the user provides the SVG:
279279
2. Create a React component that spreads props
280280
3. Ensure viewBox is preserved from the original SVG
281281

282+
### Theme-safety (bare rendering) — REQUIRED
283+
284+
The icon renders both inside its colored `bgColor` tile AND "bare" (no tile) on a
285+
neutral page — e.g. the home **Suggested actions** list — in both light and dark
286+
mode. A monochrome logo whose paths hardcode a single near-white or near-black
287+
fill is invisible bare on the matching background (white-on-white in light mode,
288+
black-on-black in dark mode).
289+
290+
Rules when adding the SVG:
291+
292+
- **Monochrome logos** (a single white or black mark): draw the shape with
293+
`fill='currentColor'`, not `fill='#fff'` / `fill='#000000'`. It then inherits
294+
white inside dark tiles, near-black inside light tiles (via
295+
`getTileIconColorClass`), and the theme-aware `var(--text-icon)` bare — legible
296+
everywhere. Do NOT set `iconColor` for these.
297+
- **Multi-color brand logos** (their own vivid fills): keep the hardcoded fills.
298+
They read on any background. Only set `iconColor` (a vivid brand hex, never a
299+
near-black/near-white tile color) if the bare icon should adopt a brand tint.
300+
- A large white shape with a tiny vivid accent (e.g. a logo where the body is the
301+
white negative space) still vanishes bare — convert the body to `currentColor`.
302+
303+
Verify with `bun run check:bare-icons` (also runs in CI). It flags purely
304+
monochrome hazards; for partial-accent logos, eyeball the suggested-actions list
305+
in both light and dark mode.
306+
282307
## Step 5: Create Triggers (Optional)
283308

284309
If the service supports webhooks, create triggers using the generic `buildTriggerSubBlocks` helper.
@@ -466,6 +491,7 @@ If creating V2 versions (API-aligned outputs):
466491
- [ ] Asked user to provide SVG
467492
- [ ] Added icon to `components/icons.tsx`
468493
- [ ] Icon spreads props correctly
494+
- [ ] Monochrome marks use `fill='currentColor'` (not hardcoded white/black) so the icon renders bare in light AND dark mode — verified with `bun run check:bare-icons`
469495

470496
### Triggers (if service supports webhooks)
471497
- [ ] Created `triggers/{service}/` directory

.github/workflows/test-build.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ jobs:
125125
- name: Client boundary import audit
126126
run: bun run check:client-boundary
127127

128+
- name: Bare-icon theme-safety audit
129+
run: bun run check:bare-icons
130+
128131
- name: Verify realtime prune graph
129132
run: bun run check:realtime-prune
130133

apps/sim/app/(landing)/components/landing-preview/components/landing-preview-panel/landing-preview-panel.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
TYPE_START_BUFFER_MS,
2121
} from '@/app/(landing)/components/landing-preview/components/landing-preview-workflow/workflow-data'
2222
import { trackLandingCta } from '@/app/(landing)/landing-analytics'
23+
import { getTileIconColorClass } from '@/blocks/icon-color'
2324

2425
const AuthModal = dynamic(
2526
() => import('@/app/(landing)/components/auth-modal/auth-modal').then((m) => m.AuthModal),
@@ -389,7 +390,7 @@ function EditorTabContent({ editorPrompt, typedLength }: EditorTabContentProps)
389390
className='flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center rounded-sm'
390391
style={{ background: bgColor }}
391392
>
392-
<BlockIcon className='h-[12px] w-[12px] text-white' />
393+
<BlockIcon className={`h-[12px] w-[12px] ${getTileIconColorClass(bgColor)}`} />
393394
</div>
394395
)}
395396
<span className='min-w-0 flex-1 truncate font-medium text-[#e6e6e6] text-sm'>
@@ -456,7 +457,9 @@ function EditorTabContent({ editorPrompt, typedLength }: EditorTabContentProps)
456457
className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center rounded-[4px]'
457458
style={{ background: tool.bgColor }}
458459
>
459-
<ToolIcon className='h-[10px] w-[10px] text-white' />
460+
<ToolIcon
461+
className={`h-[10px] w-[10px] ${getTileIconColorClass(tool.bgColor)}`}
462+
/>
460463
</div>
461464
)}
462465
<span className='font-normal text-[#e6e6e6] text-[12px]'>{tool.name}</span>

apps/sim/app/(landing)/components/landing-preview/components/landing-preview-workflow/preview-block-node.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
EASE_OUT,
4141
type PreviewTool,
4242
} from '@/app/(landing)/components/landing-preview/components/landing-preview-workflow/workflow-data'
43+
import { getTileIconColorClass } from '@/blocks/icon-color'
4344

4445
/** Map block type strings to their icon components. */
4546
const BLOCK_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
@@ -198,7 +199,7 @@ export const PreviewBlockNode = memo(function PreviewBlockNode({
198199
className='flex size-[24px] flex-shrink-0 items-center justify-center rounded-[6px]'
199200
style={{ background: bgColor }}
200201
>
201-
{Icon && <Icon className='size-[16px] text-white' />}
202+
{Icon && <Icon className={`size-[16px] ${getTileIconColorClass(bgColor)}`} />}
202203
</div>
203204
<span className='truncate font-medium text-[#e6e6e6] text-[16px]'>{name}</span>
204205
</div>
@@ -247,7 +248,11 @@ export const PreviewBlockNode = memo(function PreviewBlockNode({
247248
className='flex size-[16px] flex-shrink-0 items-center justify-center rounded-[4px]'
248249
style={{ background: tool.bgColor }}
249250
>
250-
{ToolIcon && <ToolIcon className='size-[10px] text-white' />}
251+
{ToolIcon && (
252+
<ToolIcon
253+
className={`size-[10px] ${getTileIconColorClass(tool.bgColor)}`}
254+
/>
255+
)}
251256
</div>
252257
<span className='font-normal text-[#e6e6e6] text-[12px]'>
253258
{tool.name}

apps/sim/app/(landing)/integrations/components/integration-icon.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ComponentType, ElementType, HTMLAttributes, SVGProps } from 'react'
22
import { cn } from '@sim/emcn'
3+
import { getTileIconColorClass } from '@/blocks/icon-color'
34

45
interface IntegrationIconProps extends HTMLAttributes<HTMLElement> {
56
bgColor: string
@@ -39,7 +40,7 @@ export function IntegrationIcon({
3940
{...rest}
4041
>
4142
{Icon ? (
42-
<Icon className={cn(iconClassName, 'text-white')} />
43+
<Icon className={cn(iconClassName, getTileIconColorClass(bgColor))} />
4344
) : (
4445
<span className={cn('text-white leading-none', fallbackClassName)}>{name.charAt(0)}</span>
4546
)}

apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { ConnectServiceAccountModal } from '@/app/workspace/[workspaceId]/integr
1919
import { IntegrationSection } from '@/app/workspace/[workspaceId]/integrations/components/integration-section'
2020
import { IntegrationTile } from '@/app/workspace/[workspaceId]/integrations/components/integrations-showcase'
2121
import { CONNECT_MODE } from '@/app/workspace/[workspaceId]/integrations/connect-route'
22+
import { getTileIconColorClass } from '@/blocks/icon-color'
2223
import { storeCuratedPrompt } from '@/blocks/integration-matcher'
2324
import {
2425
getSuggestedSkillsForBlock,
@@ -176,7 +177,10 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration
176177
<IntegrationTile blockType={integration.type} icon={Icon} />
177178
) : (
178179
<div
179-
className='flex size-9 flex-shrink-0 items-center justify-center rounded-xl border border-[var(--border-1)] text-white'
180+
className={cn(
181+
'flex size-9 flex-shrink-0 items-center justify-center rounded-xl border border-[var(--border-1)]',
182+
getTileIconColorClass(integration.bgColor)
183+
)}
180184
style={{ background: integration.bgColor }}
181185
>
182186
{integration.name.charAt(0)}

apps/sim/app/workspace/[workspaceId]/integrations/components/integrations-showcase/integrations-showcase.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { ComponentType } from 'react'
2+
import { cn } from '@sim/emcn'
23
import { getBlock } from '@/blocks'
4+
import { getTileIconColorClass } from '@/blocks/icon-color'
35

46
/**
57
* URL-encoded SVG used as a mask to carve the bottom-right notch out of the
@@ -72,7 +74,7 @@ export function IntegrationTile({ blockType, icon: Icon, framed = false }: Integ
7274
className='flex size-full items-center justify-center rounded-xl border border-[var(--border-1)] bg-[var(--bg)]'
7375
style={brandBg ? { background: brandBg } : undefined}
7476
>
75-
<Icon className='size-5 text-white' />
77+
<Icon className={cn('size-5', getTileIconColorClass(brandBg))} />
7678
</div>
7779
</div>
7880
)
@@ -84,7 +86,7 @@ export function IntegrationTile({ blockType, icon: Icon, framed = false }: Integ
8486
className='flex size-full items-center justify-center rounded-[9px] border border-[var(--border-1)] bg-[var(--bg)]'
8587
style={brandBg ? { background: brandBg } : undefined}
8688
>
87-
<Icon className='size-6 text-white' />
89+
<Icon className={cn('size-6', getTileIconColorClass(brandBg))} />
8890
</div>
8991
</div>
9092
)

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { MaxBadge } from '@/app/workspace/[workspaceId]/knowledge/[id]/component
3636
import { useConnectorConfigFields } from '@/app/workspace/[workspaceId]/knowledge/[id]/hooks/use-connector-config-fields'
3737
import { isBillingEnabled } from '@/app/workspace/[workspaceId]/settings/navigation'
3838
import { getBlock } from '@/blocks'
39+
import { getTileIconColorClass } from '@/blocks/icon-color'
3940
import { CONNECTOR_META_REGISTRY } from '@/connectors/registry'
4041
import type { ConnectorMeta } from '@/connectors/types'
4142
import { useCreateConnector } from '@/hooks/queries/kb/connectors'
@@ -477,7 +478,12 @@ function ConnectorTypeCard({ type, config, onClick }: ConnectorTypeCardProps) {
477478
)}
478479
style={brandBg ? { background: brandBg } : undefined}
479480
>
480-
<Icon className={cn('size-5', brandBg ? 'text-white' : 'text-[var(--text-icon)]')} />
481+
<Icon
482+
className={cn(
483+
'size-5',
484+
brandBg ? getTileIconColorClass(brandBg) : 'text-[var(--text-icon)]'
485+
)}
486+
/>
481487
</div>
482488
</div>
483489
<div className='flex min-w-0 flex-1 flex-col'>

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { getMissingRequiredScopes } from '@/lib/oauth/utils'
2222
import { ConnectOAuthModal } from '@/app/workspace/[workspaceId]/components/connect-oauth-modal'
2323
import { EditConnectorModal } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal'
2424
import { getBlock } from '@/blocks'
25+
import { getTileIconColorClass } from '@/blocks/icon-color'
2526
import { CONNECTOR_META_REGISTRY } from '@/connectors/registry'
2627
import type { ConnectorData, SyncLogData } from '@/hooks/queries/kb/connectors'
2728
import {
@@ -346,7 +347,10 @@ function ConnectorCard({
346347
>
347348
{Icon && (
348349
<Icon
349-
className={cn('size-5', brandBg ? 'text-white' : 'text-[var(--text-icon)]')}
350+
className={cn(
351+
'size-5',
352+
brandBg ? getTileIconColorClass(brandBg) : 'text-[var(--text-icon)]'
353+
)}
350354
/>
351355
)}
352356
</div>

0 commit comments

Comments
 (0)