Skip to content

Commit 64fbf0d

Browse files
icecrasher321claude
andcommitted
refactor(canvas): drop dead markers and share the tile-brightness maths
Review pass over the branch against staging. Dead code removed: - `tileIconColorClass` in the renderer package — never called; only its `isLightTileColor` sibling is. - `data-connection-selector-search-frost`, `data-workflow-cursor-edge` and `data-workflow-cursor-source-side` — written on three elements, read by no stylesheet, selector or test. - `CHIP_TARGET_SELECTOR_TYPES`, `MAX_CHIPS` and `chipPriority` were exported from `canvas-rows.ts` but only used inside it. Consolidated the one real divergence: the renderer package carried a hand-copied mirror of the app's perceived-brightness maths, because it may not import app code. The copy had already drifted — it dropped the `white`/`black` keyword handling, so a block shipping `bgColor: 'white'` would render a white `currentColor` icon on a white tile on the canvas while every other surface drew it black. No block ships one today, which is exactly why nothing caught it. The function now lives in `@sim/utils/color` and both sides import it; only the 0.75 threshold stays local to each. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 8927266 commit 64fbf0d

8 files changed

Lines changed: 57 additions & 79 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/connection-block-selector/connection-block-selector.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -483,10 +483,7 @@ export function ConnectionBlockSelector({ id, data }: NodeProps<ConnectionBlockS
483483
</>
484484
)}
485485
</Command.List>
486-
<div
487-
data-connection-selector-search-frost=''
488-
className='nodrag nopan absolute inset-x-[3px] top-[3px] z-20 flex h-12 cursor-text items-center gap-2 rounded-t-[13px] bg-[linear-gradient(to_bottom,var(--surface-2)_0%,color-mix(in_srgb,var(--surface-2)_88%,transparent)_68%,transparent_100%)] px-2.5 pb-2'
489-
>
486+
<div className='nodrag nopan absolute inset-x-[3px] top-[3px] z-20 flex h-12 cursor-text items-center gap-2 rounded-t-[13px] bg-[linear-gradient(to_bottom,var(--surface-2)_0%,color-mix(in_srgb,var(--surface-2)_88%,transparent)_68%,transparent_100%)] px-2.5 pb-2'>
490487
<Search className='size-[14px] flex-shrink-0 text-[var(--text-muted)]' />
491488
<Command.Input
492489
ref={inputRef}

apps/sim/lib/colors/brightness.ts

Lines changed: 3 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,6 @@
1-
/**
2-
* Perceived brightness (0 = black, 1 = white) of a CSS color, using the ITU-R
3-
* BT.601 (YIQ) luma weights `0.299 R + 0.587 G + 0.114 B`.
4-
*
5-
* This is the perceptual "is it light or dark" measure the app uses for
6-
* foreground/background contrast decisions. It tracks human brightness
7-
* perception better than gamma-corrected relative luminance for the saturated
8-
* brand colors used as tile backgrounds (e.g. it correctly reads bright yellows
9-
* as light), which is why every contrast helper in the app builds on it.
10-
*
11-
* Accepts `#rgb`/`#rrggbb` hex (with or without `#`, optionally quoted) and the
12-
* `white`/`black` keywords. Returns `null` for anything else (named colors,
13-
* gradients, `currentColor`, malformed input) so callers can treat unknown
14-
* values explicitly instead of guessing.
15-
*/
16-
export function perceivedBrightness(color: string): number | null {
17-
const value = color.trim().replace(/['"]/g, '').toLowerCase()
18-
if (value === 'white') return 1
19-
if (value === 'black') return 0
20-
const hex = value.replace('#', '')
21-
let r: number
22-
let g: number
23-
let b: number
24-
if (/^[0-9a-f]{3}$/.test(hex)) {
25-
r = Number.parseInt(hex[0] + hex[0], 16)
26-
g = Number.parseInt(hex[1] + hex[1], 16)
27-
b = Number.parseInt(hex[2] + hex[2], 16)
28-
} else if (/^[0-9a-f]{6}$/.test(hex)) {
29-
r = Number.parseInt(hex.slice(0, 2), 16)
30-
g = Number.parseInt(hex.slice(2, 4), 16)
31-
b = Number.parseInt(hex.slice(4, 6), 16)
32-
} else {
33-
return null
34-
}
35-
return (0.299 * r + 0.587 * g + 0.114 * b) / 255
36-
}
1+
import { perceivedBrightness } from '@sim/utils/color'
2+
3+
export { perceivedBrightness }
374

385
/**
396
* True when `color` is light enough that a white foreground would wash out.

apps/sim/lib/workflows/blocks/canvas-rows.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type { BlockConfig, SubBlockConfig } from '@/blocks/types'
1313
* Selector subblock types whose hydrated value names the block's primary
1414
* target (table, channel, knowledge base, …) — promoted to a chip.
1515
*/
16-
export const CHIP_TARGET_SELECTOR_TYPES = new Set<string>([
16+
const CHIP_TARGET_SELECTOR_TYPES = new Set<string>([
1717
'table-selector',
1818
'knowledge-base-selector',
1919
'workflow-selector',
@@ -29,14 +29,14 @@ export const CHIP_TARGET_SELECTOR_TYPES = new Set<string>([
2929
])
3030

3131
/** Maximum fragments in the statement line; remaining candidates fall back to rows. */
32-
export const MAX_CHIPS = 2
32+
const MAX_CHIPS = 2
3333

3434
/**
3535
* Ranks a subblock for promotion into the card's chips row: the operation
3636
* first, then the primary target selector, then the model. Returns null for
3737
* subblocks that stay as label/value rows.
3838
*/
39-
export function chipPriority(subBlock: SubBlockConfig): number | null {
39+
function chipPriority(subBlock: SubBlockConfig): number | null {
4040
if (subBlock.id === 'operation') return 0
4141
if (CHIP_TARGET_SELECTOR_TYPES.has(subBlock.type)) return 1
4242
if (subBlock.id === 'model') return 2

packages/utils/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
"types": "./src/random.ts",
1919
"default": "./src/random.ts"
2020
},
21+
"./color": {
22+
"types": "./src/color.ts",
23+
"default": "./src/color.ts"
24+
},
2125
"./errors": {
2226
"types": "./src/errors.ts",
2327
"default": "./src/errors.ts"

packages/utils/src/color.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* Perceived brightness (0 = black, 1 = white) of a CSS color, using the ITU-R
3+
* BT.601 (YIQ) luma weights `0.299 R + 0.587 G + 0.114 B`.
4+
*
5+
* This is the perceptual "is it light or dark" measure the product uses for
6+
* foreground/background contrast decisions. It tracks human brightness
7+
* perception better than gamma-corrected relative luminance for the saturated
8+
* brand colors used as tile backgrounds (e.g. it correctly reads bright yellows
9+
* as light), which is why every contrast helper builds on it.
10+
*
11+
* Accepts `#rgb`/`#rrggbb` hex (with or without `#`, optionally quoted) and the
12+
* `white`/`black` keywords. Returns `null` for anything else (named colors,
13+
* gradients, `currentColor`, malformed input) so callers can treat unknown
14+
* values explicitly instead of guessing.
15+
*
16+
* Lives here rather than in `apps/sim` because the canvas renderer package needs
17+
* the same answer and may not import app code. A second copy there drifted on
18+
* the `white`/`black` keywords, which is invisible until a block ships one as
19+
* its tile color and its icon renders white-on-white on the canvas only.
20+
*/
21+
export function perceivedBrightness(color: string): number | null {
22+
const value = color.trim().replace(/['"]/g, '').toLowerCase()
23+
if (value === 'white') return 1
24+
if (value === 'black') return 0
25+
const hex = value.replace('#', '')
26+
let r: number
27+
let g: number
28+
let b: number
29+
if (/^[0-9a-f]{3}$/.test(hex)) {
30+
r = Number.parseInt(hex[0] + hex[0], 16)
31+
g = Number.parseInt(hex[1] + hex[1], 16)
32+
b = Number.parseInt(hex[2] + hex[2], 16)
33+
} else if (/^[0-9a-f]{6}$/.test(hex)) {
34+
r = Number.parseInt(hex.slice(0, 2), 16)
35+
g = Number.parseInt(hex.slice(2, 4), 16)
36+
b = Number.parseInt(hex.slice(4, 6), 16)
37+
} else {
38+
return null
39+
}
40+
return (0.299 * r + 0.587 * g + 0.114 * b) / 255
41+
}
Lines changed: 5 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,18 @@
1+
import { perceivedBrightness } from '@sim/utils/color'
2+
13
/**
24
* Foreground class for a brand icon rendered inside its colored block tile.
35
*
4-
* This is a self-contained mirror of `apps/sim/lib/colors` +
5-
* `getTileIconColorClass` (apps/sim/blocks/icon-color.ts). The renderer package
6-
* is intentionally isolated and must not import app code, so the small bit of
7-
* brightness math it needs lives here. Keep the threshold and behavior in sync
8-
* with the canonical helper.
6+
* The brightness maths is `@sim/utils/color`, shared with `@/blocks/icon-color`,
7+
* so the canvas and the rest of the app can never disagree about which tiles are
8+
* light. Only the threshold lives here, and it matches that helper's.
99
*
1010
* Block icons are increasingly drawn with `fill='currentColor'`, so a tile must
1111
* give them a foreground that contrasts the (fixed, non-theme) brand
1212
* background: white on dark tiles, near-black on clearly light tiles. Hardcoded
1313
* multi-color icons ignore the class and keep their own fills.
1414
*/
1515

16-
/** ITU-R BT.601 perceived brightness (0–1) of a `#rgb`/`#rrggbb` color, else null. */
17-
function perceivedBrightness(color: string): number | null {
18-
const hex = color.trim().replace(/['"#]/g, '').toLowerCase()
19-
let r: number
20-
let g: number
21-
let b: number
22-
if (/^[0-9a-f]{3}$/.test(hex)) {
23-
r = Number.parseInt(hex[0] + hex[0], 16)
24-
g = Number.parseInt(hex[1] + hex[1], 16)
25-
b = Number.parseInt(hex[2] + hex[2], 16)
26-
} else if (/^[0-9a-f]{6}$/.test(hex)) {
27-
r = Number.parseInt(hex.slice(0, 2), 16)
28-
g = Number.parseInt(hex.slice(2, 4), 16)
29-
b = Number.parseInt(hex.slice(4, 6), 16)
30-
} else {
31-
return null
32-
}
33-
return (0.299 * r + 0.587 * g + 0.114 * b) / 255
34-
}
35-
3616
/** Tiles brighter than this flip their icon foreground to near-black. */
3717
const LIGHT_TILE_THRESHOLD = 0.75
3818

@@ -41,8 +21,3 @@ export function isLightTileColor(bgColor: string | null | undefined): boolean {
4121
const brightness = bgColor ? perceivedBrightness(bgColor) : null
4222
return brightness !== null && brightness > LIGHT_TILE_THRESHOLD
4323
}
44-
45-
/** `text-white` on dark/unknown tiles, `text-black` on clearly light tiles. */
46-
export function tileIconColorClass(bgColor: string | null | undefined): string {
47-
return isLightTileColor(bgColor) ? 'text-black' : 'text-white'
48-
}

packages/workflow-renderer/src/subflow/subflow-node-view.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -302,8 +302,6 @@ export function SubflowStartView({
302302
}}
303303
data-nodeid={parentId}
304304
data-handleid={cursorHandleId}
305-
data-workflow-cursor-edge={cursorSourceHandle.edgeSide}
306-
data-workflow-cursor-source-side={cursorSourceHandle.side}
307305
isConnectableStart={true}
308306
isConnectableEnd={false}
309307
onPointerDownCapture={syncCursorSourceHandleBounds}
@@ -564,8 +562,6 @@ export function SubflowNodeView({
564562
}}
565563
data-nodeid={id}
566564
data-handleid={cursorSourceHandle.handleId}
567-
data-workflow-cursor-edge={cursorSourceHandle.edgeSide}
568-
data-workflow-cursor-source-side={cursorSourceHandle.side}
569565
isConnectableStart={true}
570566
isConnectableEnd={false}
571567
onPointerDownCapture={syncCursorSourceHandleBounds}

packages/workflow-renderer/src/workflow-block/workflow-block-view.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -859,8 +859,6 @@ export function WorkflowBlockView({
859859
}}
860860
data-nodeid={id}
861861
data-handleid={cursorSourceHandle.handleId}
862-
data-workflow-cursor-edge={cursorSourceHandle.edgeSide}
863-
data-workflow-cursor-source-side={cursorSourceHandle.side}
864862
isConnectableStart={true}
865863
isConnectableEnd={false}
866864
onPointerDownCapture={syncCursorSourceHandleBounds}

0 commit comments

Comments
 (0)