From 5103f40196d336652b2ec738fd5f4e2c75857fe4 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sun, 29 Mar 2026 11:56:19 +0200 Subject: [PATCH] feat: replace Unicode diagram icons with lucide SVG icons Each of the ~40 node types now has a distinct, semantically meaningful lucide icon rendered as crisp SVG paths. Compound node headers also show their icon left-aligned in the header bar. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ProcessDiagram/CompoundNode.tsx | 12 +- .../components/ProcessDiagram/DiagramNode.tsx | 16 +- .../components/ProcessDiagram/node-colors.ts | 250 +++++++++++++++++- 3 files changed, 257 insertions(+), 21 deletions(-) diff --git a/ui/src/components/ProcessDiagram/CompoundNode.tsx b/ui/src/components/ProcessDiagram/CompoundNode.tsx index 7c065bac..aa7e0a30 100644 --- a/ui/src/components/ProcessDiagram/CompoundNode.tsx +++ b/ui/src/components/ProcessDiagram/CompoundNode.tsx @@ -1,7 +1,7 @@ import type { DiagramNode as DiagramNodeType, DiagramEdge as DiagramEdgeType } from '../../api/queries/diagrams'; import type { NodeConfig } from './types'; import type { NodeExecutionState, IterationInfo } from '../ExecutionDiagram/types'; -import { colorForType, isCompoundType } from './node-colors'; +import { colorForType, isCompoundType, iconForType, type IconElement } from './node-colors'; import { DiagramNode } from './DiagramNode'; import { DiagramEdge } from './DiagramEdge'; import styles from './ProcessDiagram.module.css'; @@ -118,7 +118,15 @@ export function CompoundNode({ - {/* Header label */} + {/* Header icon (left-aligned) */} + + {iconForType(node.type).map((el: IconElement, i: number) => + 'd' in el + ? + : + )} + + {/* Header label (centered) */} - {/* Icon */} - - {icon} - + {/* Icon (lucide 24×24 scaled to 14px) */} + + {iconElements.map((el: IconElement, i: number) => + 'd' in el + ? + : + )} + {/* Type name + detail (clipped to available width) */} diff --git a/ui/src/components/ProcessDiagram/node-colors.ts b/ui/src/components/ProcessDiagram/node-colors.ts index 614a6b67..f14c1db6 100644 --- a/ui/src/components/ProcessDiagram/node-colors.ts +++ b/ui/src/components/ProcessDiagram/node-colors.ts @@ -91,17 +91,241 @@ export function isCompletionCompoundType(type: string | undefined): boolean { return !!type && COMPLETION_COMPOUND_TYPES.has(type); } -/** Icon character for a node type */ -export function iconForType(type: string | undefined): string { - if (!type) return '\u2699'; // gear - const t = type.toUpperCase(); - if (t === 'ENDPOINT') return '\u25B6'; // play - if (t === 'TO' || t === 'TO_DYNAMIC' || t === 'DIRECT' || t === 'SEDA') return '\u25A0'; // square - if (t.startsWith('EIP_CHOICE') || t === 'EIP_WHEN' || t === 'EIP_OTHERWISE') return '\u25C6'; // diamond - if (t === 'ON_COMPLETION') return '\u2714'; // checkmark - if (t === 'ON_EXCEPTION' || t === 'ERROR_HANDLER' || t.startsWith('TRY') || t.startsWith('DO_')) return '\u26A0'; // warning - if (t === 'EIP_SPLIT' || t === 'EIP_MULTICAST') return '\u2442'; // fork - if (t === 'EIP_LOOP') return '\u21BA'; // loop arrow - if (t === 'EIP_WIRE_TAP' || t === 'EIP_ENRICH' || t === 'EIP_POLL_ENRICH') return '\u2197'; // arrow - return '\u2699'; // gear +/** + * Lucide-based icon definitions for node types. + * Each icon is an array of SVG element descriptors (path d-strings or circles). + */ +export type IconElement = { d: string } | { cx: number; cy: number; r: number }; + +// Lucide icon path data (24×24 viewBox, stroke-based) +const ICONS: Record = { + // Play — ENDPOINT + play: [{ d: 'M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z' }], + // Send — TO + send: [ + { d: 'M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z' }, + { d: 'm21.854 2.147-10.94 10.939' }, + ], + // Shuffle — TO_DYNAMIC + shuffle: [ + { d: 'm18 14 4 4-4 4' }, + { d: 'm18 2 4 4-4 4' }, + { d: 'M2 18h1.973a4 4 0 0 0 3.3-1.7l5.454-8.6a4 4 0 0 1 3.3-1.7H22' }, + { d: 'M2 6h1.972a4 4 0 0 1 3.6 2.2' }, + { d: 'M22 18h-6.041a4 4 0 0 1-3.3-1.8l-.359-.45' }, + ], + // Zap — DIRECT + zap: [{ d: 'M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z' }], + // Layers — SEDA + layers: [ + { d: 'M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z' }, + { d: 'M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12' }, + { d: 'M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17' }, + ], + // Cog — PROCESSOR (default) + cog: [ + { d: 'M11 10.27 7 3.34' }, { d: 'm11 13.73-4 6.93' }, + { d: 'M12 22v-2' }, { d: 'M12 2v2' }, { d: 'M14 12h8' }, + { d: 'm17 20.66-1-1.73' }, { d: 'm17 3.34-1 1.73' }, + { d: 'M2 12h2' }, { d: 'm20.66 17-1.73-1' }, { d: 'm20.66 7-1.73 1' }, + { d: 'm3.34 17 1.73-1' }, { d: 'm3.34 7 1.73 1' }, + { cx: 12, cy: 12, r: 3 }, + ], + // Coffee — BEAN + coffee: [ + { d: 'M10 2v2' }, { d: 'M14 2v2' }, { d: 'M6 2v2' }, + { d: 'M16 8a1 1 0 0 1 1 1v8a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1h14a4 4 0 1 1 0 8h-1' }, + ], + // FileText — LOG + fileText: [ + { d: 'M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z' }, + { d: 'M14 2v5a1 1 0 0 0 1 1h5' }, + { d: 'M10 9H8' }, { d: 'M16 13H8' }, { d: 'M16 17H8' }, + ], + // Tag — SET_HEADER + tag: [{ d: 'M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z' }, { cx: 7.5, cy: 7.5, r: 1 }], + // PenLine — SET_BODY + penLine: [ + { d: 'M13 21h8' }, + { d: 'M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z' }, + ], + // RefreshCw — TRANSFORM + refreshCw: [ + { d: 'M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8' }, + { d: 'M21 3v5h-5' }, + { d: 'M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16' }, + { d: 'M8 16H3v5' }, + ], + // Package — MARSHAL + package: [ + { d: 'M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z' }, + { d: 'M12 22V12' }, { d: 'm7.5 4.27 9 5.15' }, + ], + // PackageOpen — UNMARSHAL + packageOpen: [ + { d: 'M12 22v-9' }, + { d: 'M15.17 2.21a1.67 1.67 0 0 1 1.63 0L21 4.57a1.93 1.93 0 0 1 0 3.36L8.82 14.79a1.655 1.655 0 0 1-1.64 0L3 12.43a1.93 1.93 0 0 1 0-3.36z' }, + { d: 'M20 13v3.87a2.06 2.06 0 0 1-1.11 1.83l-6 3.08a1.93 1.93 0 0 1-1.78 0l-6-3.08A2.06 2.06 0 0 1 4 16.87V13' }, + { d: 'M21 12.43a1.93 1.93 0 0 0 0-3.36L8.83 2.2a1.64 1.64 0 0 0-1.63 0L3 4.57a1.93 1.93 0 0 0 0 3.36l12.18 6.86a1.636 1.636 0 0 0 1.63 0z' }, + ], + // GitBranch — EIP_CHOICE + gitBranch: [{ d: 'M15 6a9 9 0 0 0-9 9V3' }, { cx: 18, cy: 6, r: 3 }, { cx: 6, cy: 18, r: 3 }], + // CircleAlert — EIP_WHEN (condition) + circleAlert: [{ cx: 12, cy: 12, r: 10 }, { d: 'M12 8v4' }, { d: 'M12 16h.01' }], + // CircleDot — EIP_OTHERWISE (default) + circleDot: [{ cx: 12, cy: 12, r: 10 }, { cx: 12, cy: 12, r: 1 }], + // Split — EIP_SPLIT + split: [ + { d: 'M16 3h5v5' }, { d: 'M8 3H3v5' }, + { d: 'M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3' }, + { d: 'm15 9 6-6' }, + ], + // Share2 — EIP_MULTICAST + share2: [ + { cx: 18, cy: 5, r: 3 }, { cx: 6, cy: 12, r: 3 }, { cx: 18, cy: 19, r: 3 }, + { d: 'M8.59 13.51 15.42 17.49' }, { d: 'M15.41 6.51 8.59 10.49' }, + ], + // Repeat — EIP_LOOP + repeat: [ + { d: 'm17 2 4 4-4 4' }, + { d: 'M3 11v-1a4 4 0 0 1 4-4h14' }, + { d: 'm7 22-4-4 4-4' }, + { d: 'M21 13v1a4 4 0 0 1-4 4H3' }, + ], + // Merge — EIP_AGGREGATE + merge: [{ d: 'm8 6 4-4 4 4' }, { d: 'M12 2v10.3a4 4 0 0 1-1.172 2.872L4 22' }, { d: 'm20 22-5-5' }], + // Funnel — EIP_FILTER + funnel: [{ d: 'M10 20a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341L21.74 4.67A1 1 0 0 0 21 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14z' }], + // Users — EIP_RECIPIENT_LIST + users: [ + { d: 'M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2' }, + { cx: 9, cy: 7, r: 4 }, + { d: 'M16 3.128a4 4 0 0 1 0 7.744' }, + { d: 'M22 21v-2a4 4 0 0 0-3-3.87' }, + ], + // Navigation — EIP_ROUTING_SLIP + navigation: [{ d: 'M3,11 L22,2 L13,21 L11,13Z' }], + // Waypoints — EIP_DYNAMIC_ROUTER + waypoints: [ + { d: 'm10.586 5.414-5.172 5.172' }, { d: 'm18.586 13.414-5.172 5.172' }, + { d: 'M6 12h12' }, + { cx: 12, cy: 20, r: 2 }, { cx: 12, cy: 4, r: 2 }, + { cx: 20, cy: 12, r: 2 }, { cx: 4, cy: 12, r: 2 }, + ], + // Scale — EIP_LOAD_BALANCE + scale: [ + { d: 'M12 3v18' }, + { d: 'm19 8 3 8a5 5 0 0 1-6 0zV7' }, + { d: 'M3 7h1a17 17 0 0 0 8-2 17 17 0 0 0 8 2h1' }, + { d: 'm5 8 3 8a5 5 0 0 1-6 0zV7' }, + { d: 'M7 21h10' }, + ], + // Gauge — EIP_THROTTLE + gauge: [{ d: 'm12 14 4-4' }, { d: 'M3.34 19a10 10 0 1 1 17.32 0' }], + // Clock — EIP_DELAY + clock: [{ cx: 12, cy: 12, r: 10 }, { d: 'M12 6v6l4 2' }], + // Hash — EIP_IDEMPOTENT_CONSUMER + hash: [{ d: 'M4 9L20 9' }, { d: 'M4 15L20 15' }, { d: 'M10 3L8 21' }, { d: 'M16 3L14 21' }], + // ZapOff — EIP_CIRCUIT_BREAKER + zapOff: [ + { d: 'M10.513 4.856 13.12 2.17a.5.5 0 0 1 .86.46l-1.377 4.317' }, + { d: 'M15.656 10H20a1 1 0 0 1 .78 1.63l-1.72 1.773' }, + { d: 'M16.273 16.273 10.88 21.83a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14H4a1 1 0 0 1-.78-1.63l4.507-4.643' }, + { d: 'm2 2 20 20' }, + ], + // Workflow — EIP_PIPELINE + workflow: [ + { d: 'M5,3h4a2,2 0 0 1 2,2v4a2,2 0 0 1-2,2h-4a2,2 0 0 1-2,-2v-4a2,2 0 0 1 2,-2Z' }, + { d: 'M7 11v4a2 2 0 0 0 2 2h4' }, + { d: 'M15,13h4a2,2 0 0 1 2,2v4a2,2 0 0 1-2,2h-4a2,2 0 0 1-2,-2v-4a2,2 0 0 1 2,-2Z' }, + ], + // ShieldAlert — ERROR_HANDLER + shieldAlert: [ + { d: 'M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z' }, + { d: 'M12 8v4' }, { d: 'M12 16h.01' }, + ], + // OctagonAlert — ON_EXCEPTION + octagonAlert: [ + { d: 'M15.312 2a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586l-4.688-4.688A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2z' }, + { d: 'M12 8v4' }, { d: 'M12 16h.01' }, + ], + // Shield — DO_TRY + shield: [{ d: 'M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z' }], + // Hand — DO_CATCH + hand: [ + { d: 'M18 11V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2' }, + { d: 'M14 10V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2v2' }, + { d: 'M10 10.5V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2v8' }, + { d: 'M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15' }, + ], + // ListChecks — DO_FINALLY + listChecks: [ + { d: 'M13 5h8' }, { d: 'M13 12h8' }, { d: 'M13 19h8' }, + { d: 'm3 17 2 2 4-4' }, { d: 'm3 7 2 2 4-4' }, + ], + // Eye — EIP_WIRE_TAP + eye: [ + { d: 'M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0' }, + { cx: 12, cy: 12, r: 3 }, + ], + // CirclePlus — EIP_ENRICH + circlePlus: [{ cx: 12, cy: 12, r: 10 }, { d: 'M8 12h8' }, { d: 'M12 8v8' }], + // CircleArrowDown — EIP_POLL_ENRICH + circleArrowDown: [{ cx: 12, cy: 12, r: 10 }, { d: 'M12 8v8' }, { d: 'm8 12 4 4 4-4' }], + // CircleCheck — ON_COMPLETION + circleCheck: [{ cx: 12, cy: 12, r: 10 }, { d: 'm9 12 2 2 4-4' }], +}; + +const ICON_MAP: Record = { + ENDPOINT: ICONS.play, + TO: ICONS.send, + TO_DYNAMIC: ICONS.shuffle, + DIRECT: ICONS.zap, + SEDA: ICONS.layers, + + PROCESSOR: ICONS.cog, + BEAN: ICONS.coffee, + LOG: ICONS.fileText, + SET_HEADER: ICONS.tag, + SET_BODY: ICONS.penLine, + TRANSFORM: ICONS.refreshCw, + MARSHAL: ICONS.package, + UNMARSHAL: ICONS.packageOpen, + + EIP_CHOICE: ICONS.gitBranch, + EIP_WHEN: ICONS.circleAlert, + EIP_OTHERWISE: ICONS.circleDot, + EIP_SPLIT: ICONS.split, + EIP_MULTICAST: ICONS.share2, + EIP_LOOP: ICONS.repeat, + EIP_AGGREGATE: ICONS.merge, + EIP_FILTER: ICONS.funnel, + EIP_RECIPIENT_LIST: ICONS.users, + EIP_ROUTING_SLIP: ICONS.navigation, + EIP_DYNAMIC_ROUTER: ICONS.waypoints, + EIP_LOAD_BALANCE: ICONS.scale, + EIP_THROTTLE: ICONS.gauge, + EIP_DELAY: ICONS.clock, + EIP_IDEMPOTENT_CONSUMER: ICONS.hash, + EIP_CIRCUIT_BREAKER: ICONS.zapOff, + EIP_PIPELINE: ICONS.workflow, + + ERROR_HANDLER: ICONS.shieldAlert, + ON_EXCEPTION: ICONS.octagonAlert, + TRY_CATCH: ICONS.shield, + DO_TRY: ICONS.shield, + DO_CATCH: ICONS.hand, + DO_FINALLY: ICONS.listChecks, + + EIP_WIRE_TAP: ICONS.eye, + EIP_ENRICH: ICONS.circlePlus, + EIP_POLL_ENRICH: ICONS.circleArrowDown, + + ON_COMPLETION: ICONS.circleCheck, +}; + +/** Lucide SVG icon elements for a node type (24×24 viewBox, stroke-based) */ +export function iconForType(type: string | undefined): IconElement[] { + if (!type) return ICONS.cog; + return ICON_MAP[type.toUpperCase()] ?? ICONS.cog; }