Skip to content

Commit 46908b1

Browse files
ersinkocclaude
andcommitted
🎨 feat(ui): redesign remaining 12 workflow node components
Complete visual identity for all 23 node types: - Delay: rose gradient, large duration display, animated clock when running - Switch: fuchsia gradient, case chips, active branch highlight - ErrorHandler: red gradient, warning stripes, dashed border, ON/OFF badge - SubWorkflow: indigo gradient, workflow name, depth badge, input mapping preview - Approval: amber gradient, stamp icon, pulsing "awaiting" state, timeout badge - StickyNote: flat colored bg, tilted 2deg, folded corner, no handles - Parallel: teal gradient, SVG fan-out lines, branch count badge, label chips - Merge: teal gradient, SVG converging arrows, mode badge - DataStore: cyan gradient, SVG cylinder icon, operation badges (GET/SET/DELETE) - SchemaValidator: orange gradient, strict badge, required fields, property preview - Map: sky gradient, array brackets flow visual, dark code block - WebhookResponse: rose gradient, status code badge, no bottom handle (terminal) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 22f99c1 commit 46908b1

12 files changed

Lines changed: 833 additions & 395 deletions

packages/ui/src/components/workflows/ApprovalNode.tsx

Lines changed: 66 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
/**
22
* ApprovalNode — ReactFlow node for human approval gates in workflows.
3-
* Pauses execution and waits for manual approval/rejection.
4-
* Amber/yellow color theme.
3+
* Human-in-the-loop gate with amber-to-yellow gradient header,
4+
* "Requires Approval" badge, timeout countdown, and pulsing amber state.
55
*/
66

77
import { memo } from 'react';
88
import { Handle, Position, type Node, type NodeProps } from '@xyflow/react';
9-
import { ShieldCheck, CheckCircle2, XCircle, Activity, AlertCircle, Clock } from '../icons';
9+
import { ShieldCheck, CheckCircle2, XCircle, AlertCircle, Clock } from '../icons';
1010
import type { NodeExecutionStatus } from '../../api/types';
1111

1212
export interface ApprovalNodeData extends Record<string, unknown> {
@@ -44,12 +44,14 @@ function ApprovalNodeComponent({ data, selected }: NodeProps<ApprovalNodeType>)
4444
const status = (data.executionStatus as NodeExecutionStatus | undefined) ?? 'pending';
4545
const style = statusStyles[status];
4646
const StatusIcon = statusIcons[status];
47+
const timeout = data.timeoutMinutes as number | undefined;
48+
const message = (data.approvalMessage as string) ?? '';
4749

4850
return (
4951
<div
5052
className={`
51-
relative min-w-[180px] max-w-[260px] rounded-lg border-2 shadow-sm
52-
bg-amber-50 dark:bg-amber-950/30
53+
relative min-w-[210px] max-w-[300px] rounded-lg border-2 shadow-md overflow-hidden
54+
bg-white dark:bg-gray-900
5355
${style.border} ${style.bg}
5456
${selected ? 'ring-2 ring-amber-500 ring-offset-1' : ''}
5557
${status === 'running' ? 'animate-pulse' : ''}
@@ -63,60 +65,82 @@ function ApprovalNodeComponent({ data, selected }: NodeProps<ApprovalNodeType>)
6365
className="!w-3 !h-3 !bg-amber-500 !border-2 !border-white dark:!border-amber-950"
6466
/>
6567

66-
{/* Content */}
67-
<div className="px-3 py-2.5">
68-
{/* Header */}
69-
<div className="flex items-center gap-2">
70-
<div className="w-6 h-6 rounded-full bg-amber-500/20 flex items-center justify-center shrink-0">
71-
<ShieldCheck className="w-3.5 h-3.5 text-amber-600 dark:text-amber-400" />
72-
</div>
73-
<span className="font-medium text-sm text-amber-900 dark:text-amber-100 truncate flex-1">
74-
{(data.label as string) || 'Approval Gate'}
75-
</span>
76-
{StatusIcon && (
77-
<StatusIcon
78-
className={`w-4 h-4 shrink-0 ${
79-
status === 'success'
80-
? 'text-success'
68+
{/* Gradient Header Bar */}
69+
<div className="bg-gradient-to-r from-amber-500 to-yellow-400 px-3 py-2 flex items-center gap-2">
70+
<div className="w-6 h-6 rounded-full bg-white/20 flex items-center justify-center shrink-0">
71+
<ShieldCheck className="w-3.5 h-3.5 text-white" />
72+
</div>
73+
<span className="font-semibold text-sm text-white truncate flex-1 drop-shadow-sm">
74+
{(data.label as string) || 'Approval Gate'}
75+
</span>
76+
{StatusIcon && (
77+
<StatusIcon
78+
className={`w-4 h-4 shrink-0 ${
79+
status === 'success'
80+
? 'text-emerald-200'
81+
: status === 'error'
82+
? 'text-red-200'
83+
: status === 'running'
84+
? 'text-white'
85+
: 'text-white/60'
86+
}`}
87+
/>
88+
)}
89+
</div>
90+
91+
{/* Body Content */}
92+
<div className="px-3 py-2 space-y-2">
93+
{/* "Requires Approval" large badge */}
94+
<div className="flex items-center justify-center">
95+
<span
96+
className={`inline-flex items-center gap-1.5 px-3 py-1 text-[10px] font-bold rounded-full uppercase tracking-wider ${
97+
status === 'running'
98+
? 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300 animate-pulse ring-2 ring-amber-300 dark:ring-amber-600'
99+
: status === 'success'
100+
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300'
81101
: status === 'error'
82-
? 'text-error'
83-
: status === 'running'
84-
? 'text-warning'
85-
: 'text-text-muted'
86-
}`}
87-
/>
88-
)}
102+
? 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300'
103+
: 'bg-amber-50 text-amber-600 dark:bg-amber-900/30 dark:text-amber-400'
104+
}`}
105+
>
106+
{/* Stamp icon */}
107+
<svg className="w-3.5 h-3.5" viewBox="0 0 16 16" fill="currentColor">
108+
<path d="M6 1a1 1 0 0 1 1 1v3.586l1.707-1.707a1 1 0 0 1 1.414 1.414L8.414 7H10a1 1 0 1 1 0 2H6a1 1 0 1 1 0-2h1.586L5.879 5.293a1 1 0 0 1 1.414-1.414L9 5.586V2a1 1 0 0 1 1-1h0zM2 11a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1v-2z" />
109+
</svg>
110+
{status === 'running' ? 'Awaiting Approval' : 'Requires Approval'}
111+
</span>
89112
</div>
90113

91-
{/* Description badge */}
92-
{data.approvalMessage && (
93-
<div className="flex items-center gap-1.5 mt-1">
94-
<span className="px-1.5 py-0.5 text-[9px] font-bold rounded bg-amber-500/20 text-amber-700 dark:text-amber-300 truncate">
95-
{data.approvalMessage as string}
114+
{/* Timeout countdown badge */}
115+
{timeout != null && timeout > 0 && (
116+
<div className="flex items-center justify-center gap-1">
117+
<Clock className="w-3 h-3 text-amber-500" />
118+
<span className="text-[10px] font-semibold text-amber-600 dark:text-amber-400">
119+
{timeout}m timeout
96120
</span>
97121
</div>
98122
)}
99123

100-
{/* Timeout badge */}
101-
{data.timeoutMinutes && (
102-
<div className="flex items-center gap-1 mt-1">
103-
<Activity className="w-2.5 h-2.5 text-amber-500" />
104-
<span className="text-[10px] text-text-muted dark:text-dark-text-muted">
105-
{data.timeoutMinutes as number}min timeout
106-
</span>
107-
</div>
124+
{/* Message preview */}
125+
{message && (
126+
<p
127+
className="text-[10px] text-gray-500 dark:text-gray-400 italic truncate"
128+
title={message}
129+
>
130+
&ldquo;{message.slice(0, 60)}{message.length > 60 ? '...' : ''}&rdquo;
131+
</p>
108132
)}
109133

110134
{/* Error message */}
111135
{status === 'error' && data.executionError && (
112-
<p className="text-xs text-error mt-1 truncate" title={data.executionError as string}>
136+
<p className="text-xs text-error truncate" title={data.executionError as string}>
113137
{data.executionError as string}
114138
</p>
115139
)}
116140

117141
{/* Duration */}
118142
{data.executionDuration != null && (
119-
<p className="text-[10px] text-text-muted dark:text-dark-text-muted mt-1">
143+
<p className="text-[10px] text-text-muted dark:text-dark-text-muted">
120144
{(data.executionDuration as number) < 1000
121145
? `${data.executionDuration}ms`
122146
: `${((data.executionDuration as number) / 1000).toFixed(1)}s`}

packages/ui/src/components/workflows/DataStoreNode.tsx

Lines changed: 73 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/**
22
* DataStoreNode — Key-value data store operations in workflows.
3-
* Cyan/teal color theme.
3+
* Database/storage look with cyan gradient header, color-coded operation badges,
4+
* monospace key display, and dimmed namespace prefix.
45
*/
56

67
import { memo } from 'react';
@@ -26,6 +27,15 @@ export interface DataStoreNodeData extends Record<string, unknown> {
2627

2728
export type DataStoreNodeType = Node<DataStoreNodeData>;
2829

30+
/** Per-operation badge colors */
31+
const operationStyles: Record<string, { bg: string; text: string }> = {
32+
get: { bg: 'bg-emerald-100 dark:bg-emerald-900/40', text: 'text-emerald-700 dark:text-emerald-300' },
33+
set: { bg: 'bg-blue-100 dark:bg-blue-900/40', text: 'text-blue-700 dark:text-blue-300' },
34+
delete: { bg: 'bg-red-100 dark:bg-red-900/40', text: 'text-red-700 dark:text-red-300' },
35+
list: { bg: 'bg-violet-100 dark:bg-violet-900/40', text: 'text-violet-700 dark:text-violet-300' },
36+
has: { bg: 'bg-amber-100 dark:bg-amber-900/40', text: 'text-amber-700 dark:text-amber-300' },
37+
};
38+
2939
const statusStyles: Record<NodeExecutionStatus, { border: string; bg: string }> = {
3040
pending: { border: 'border-cyan-300 dark:border-cyan-700', bg: '' },
3141
running: { border: 'border-warning', bg: 'bg-warning/5' },
@@ -45,82 +55,104 @@ function DataStoreNodeComponent({ data, selected }: NodeProps<DataStoreNodeType>
4555
const status = (data.executionStatus as NodeExecutionStatus | undefined) ?? 'pending';
4656
const style = statusStyles[status];
4757
const StatusIcon = statusIcons[status];
58+
const operation = (data.operation as string) ?? '';
59+
const opStyle = operationStyles[operation] ?? {
60+
bg: 'bg-cyan-100 dark:bg-cyan-900/40',
61+
text: 'text-cyan-700 dark:text-cyan-300',
62+
};
4863

4964
return (
5065
<div
5166
className={`
52-
relative min-w-[180px] max-w-[260px] rounded-lg border-2 shadow-sm
53-
bg-cyan-50 dark:bg-cyan-950/30
67+
relative min-w-[200px] max-w-[280px] rounded-lg border-2 shadow-md overflow-hidden
68+
bg-white dark:bg-gray-900
5469
${style.border} ${style.bg}
5570
${selected ? 'ring-2 ring-cyan-500 ring-offset-1' : ''}
5671
${status === 'running' ? 'animate-pulse' : ''}
5772
transition-all duration-200
5873
`}
5974
>
75+
{/* Input Handle */}
6076
<Handle
6177
type="target"
6278
position={Position.Top}
6379
className="!w-3 !h-3 !bg-cyan-500 !border-2 !border-white dark:!border-cyan-950"
6480
/>
6581

66-
<div className="px-3 py-2.5">
82+
{/* Gradient Header Bar */}
83+
<div className="bg-gradient-to-r from-cyan-500 to-sky-500 px-3 py-2 flex items-center gap-2">
84+
<div className="w-6 h-6 rounded-full bg-white/20 flex items-center justify-center shrink-0">
85+
<Database className="w-3.5 h-3.5 text-white" />
86+
</div>
87+
<span className="font-semibold text-sm text-white truncate flex-1">
88+
{(data.label as string) || 'Data Store'}
89+
</span>
90+
{StatusIcon && (
91+
<StatusIcon
92+
className={`w-4 h-4 shrink-0 ${
93+
status === 'success'
94+
? 'text-emerald-200'
95+
: status === 'error'
96+
? 'text-red-200'
97+
: status === 'running'
98+
? 'text-amber-200'
99+
: 'text-white/60'
100+
}`}
101+
/>
102+
)}
103+
</div>
104+
105+
{/* Body Content */}
106+
<div className="px-3 py-2 space-y-1.5">
107+
{/* Cylinder visual + operation badge */}
67108
<div className="flex items-center gap-2">
68-
<div className="w-6 h-6 rounded-full bg-cyan-500/20 flex items-center justify-center shrink-0">
69-
<Database className="w-3.5 h-3.5 text-cyan-600 dark:text-cyan-400" />
70-
</div>
71-
<span className="font-medium text-sm text-cyan-900 dark:text-cyan-100 truncate flex-1">
72-
{(data.label as string) || 'Data Store'}
73-
</span>
74-
{StatusIcon && (
75-
<StatusIcon
76-
className={`w-4 h-4 shrink-0 ${
77-
status === 'success'
78-
? 'text-success'
79-
: status === 'error'
80-
? 'text-error'
81-
: status === 'running'
82-
? 'text-warning'
83-
: 'text-text-muted'
84-
}`}
85-
/>
109+
{/* Mini cylinder icon */}
110+
<svg className="w-5 h-6 text-cyan-300 dark:text-cyan-700 shrink-0" viewBox="0 0 20 24" fill="none" stroke="currentColor" strokeWidth="1.5">
111+
<ellipse cx="10" cy="5" rx="8" ry="3" />
112+
<path d="M2 5v14c0 1.66 3.58 3 8 3s8-1.34 8-3V5" />
113+
<ellipse cx="10" cy="12" rx="8" ry="2" strokeDasharray="3 2" opacity="0.4" />
114+
</svg>
115+
{operation && (
116+
<span className={`px-2 py-0.5 text-[10px] font-extrabold rounded uppercase tracking-wider ${opStyle.bg} ${opStyle.text}`}>
117+
{operation}
118+
</span>
86119
)}
87120
</div>
88121

89-
{data.operation && (
90-
<div className="flex items-center gap-1.5 mt-1">
91-
<span className="px-1.5 py-0.5 text-[9px] font-bold rounded bg-cyan-500/20 text-cyan-700 dark:text-cyan-300 uppercase">
92-
{data.operation as string}
93-
</span>
122+
{/* Key in monospace with namespace prefix */}
123+
{(data.key || data.namespace) && (
124+
<div className="bg-gray-50 dark:bg-gray-800/50 rounded px-2 py-1 overflow-hidden">
125+
<p className="text-[10px] font-mono truncate">
126+
{data.namespace && (
127+
<span className="text-gray-400 dark:text-gray-500">
128+
{data.namespace as string}/
129+
</span>
130+
)}
131+
<span className="text-cyan-700 dark:text-cyan-300 font-semibold">
132+
{(data.key as string) ?? ''}
133+
</span>
134+
</p>
94135
</div>
95136
)}
96137

97-
{data.key && (
98-
<p className="text-[10px] text-cyan-600/70 dark:text-cyan-400/50 mt-1 truncate font-mono">
99-
{data.key as string}
100-
</p>
101-
)}
102-
103-
{data.namespace && (
104-
<p className="text-[10px] text-text-muted dark:text-dark-text-muted mt-0.5 truncate">
105-
ns: {data.namespace as string}
106-
</p>
107-
)}
108-
138+
{/* Error message */}
109139
{status === 'error' && data.executionError && (
110-
<p className="text-xs text-error mt-1 truncate" title={data.executionError as string}>
140+
<p className="text-xs text-error truncate" title={data.executionError as string}>
111141
{data.executionError as string}
112142
</p>
113143
)}
114144

145+
{/* Duration */}
115146
{data.executionDuration != null && (
116-
<p className="text-[10px] text-text-muted dark:text-dark-text-muted mt-1">
147+
<p className="text-[10px] text-text-muted dark:text-dark-text-muted">
117148
{(data.executionDuration as number) < 1000
118149
? `${data.executionDuration}ms`
119150
: `${((data.executionDuration as number) / 1000).toFixed(1)}s`}
120151
</p>
121152
)}
122153
</div>
123154

155+
{/* Output Handle */}
124156
<Handle
125157
type="source"
126158
position={Position.Bottom}

0 commit comments

Comments
 (0)