@runtypelabs/react-flow 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,357 @@
1
+ import React, { memo } from 'react'
2
+ import { Handle, Position } from '@xyflow/react'
3
+ import type { RuntypeNodeData, FlowStep } from '../../types'
4
+
5
+ // ============================================================================
6
+ // Types
7
+ // ============================================================================
8
+
9
+ export interface BaseNodeProps {
10
+ /** Node data containing the step */
11
+ data: RuntypeNodeData
12
+ /** Whether the node is selected */
13
+ selected?: boolean
14
+ /** Node ID */
15
+ id: string
16
+ /** Node type label */
17
+ typeLabel: string
18
+ /** Icon component to display */
19
+ icon: React.ReactNode
20
+ /** Header color (Tailwind class or CSS color) */
21
+ headerColor?: string
22
+ /** Whether to show source handle */
23
+ showSourceHandle?: boolean
24
+ /** Whether to show target handle */
25
+ showTargetHandle?: boolean
26
+ /** Additional source handles for conditional branches */
27
+ additionalSourceHandles?: Array<{
28
+ id: string
29
+ position: Position
30
+ label?: string
31
+ color?: string
32
+ style?: React.CSSProperties
33
+ }>
34
+ /** Children to render in the node body */
35
+ children: React.ReactNode
36
+ }
37
+
38
+ // ============================================================================
39
+ // Styles
40
+ // ============================================================================
41
+
42
+ const styles = {
43
+ container: (selected: boolean) => ({
44
+ minWidth: '280px',
45
+ maxWidth: '320px',
46
+ backgroundColor: '#ffffff',
47
+ borderRadius: '12px',
48
+ border: selected ? '2px solid #6366f1' : '1px solid #e5e7eb',
49
+ boxShadow: selected
50
+ ? '0 0 0 2px rgba(99, 102, 241, 0.2), 0 4px 6px -1px rgba(0, 0, 0, 0.1)'
51
+ : '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
52
+ overflow: 'hidden',
53
+ transition: 'border-color 0.15s ease, box-shadow 0.15s ease',
54
+ }),
55
+ header: (color: string) => ({
56
+ display: 'flex',
57
+ alignItems: 'center',
58
+ gap: '8px',
59
+ padding: '10px 12px',
60
+ backgroundColor: color,
61
+ borderBottom: '1px solid rgba(0, 0, 0, 0.05)',
62
+ }),
63
+ iconWrapper: {
64
+ display: 'flex',
65
+ alignItems: 'center',
66
+ justifyContent: 'center',
67
+ width: '28px',
68
+ height: '28px',
69
+ borderRadius: '6px',
70
+ backgroundColor: 'rgba(255, 255, 255, 0.9)',
71
+ color: '#374151',
72
+ },
73
+ headerContent: {
74
+ flex: 1,
75
+ minWidth: 0,
76
+ },
77
+ typeLabel: {
78
+ fontSize: '10px',
79
+ fontWeight: 600,
80
+ textTransform: 'uppercase' as const,
81
+ letterSpacing: '0.05em',
82
+ color: 'rgba(0, 0, 0, 0.5)',
83
+ marginBottom: '2px',
84
+ },
85
+ stepName: {
86
+ fontSize: '13px',
87
+ fontWeight: 600,
88
+ color: '#1f2937',
89
+ overflow: 'hidden',
90
+ textOverflow: 'ellipsis',
91
+ whiteSpace: 'nowrap' as const,
92
+ },
93
+ body: {
94
+ padding: '12px',
95
+ },
96
+ handle: {
97
+ width: '12px',
98
+ height: '12px',
99
+ borderRadius: '50%',
100
+ border: '2px solid #ffffff',
101
+ },
102
+ targetHandle: {
103
+ backgroundColor: '#6366f1',
104
+ },
105
+ sourceHandle: {
106
+ backgroundColor: '#10b981',
107
+ },
108
+ deleteButton: {
109
+ padding: '4px',
110
+ borderRadius: '4px',
111
+ backgroundColor: 'transparent',
112
+ border: 'none',
113
+ cursor: 'pointer',
114
+ color: '#9ca3af',
115
+ display: 'flex',
116
+ alignItems: 'center',
117
+ justifyContent: 'center',
118
+ transition: 'color 0.15s ease, background-color 0.15s ease',
119
+ },
120
+ enabledBadge: (enabled: boolean) => ({
121
+ display: 'inline-flex',
122
+ alignItems: 'center',
123
+ padding: '2px 6px',
124
+ borderRadius: '4px',
125
+ fontSize: '10px',
126
+ fontWeight: 500,
127
+ backgroundColor: enabled ? 'rgba(16, 185, 129, 0.1)' : 'rgba(239, 68, 68, 0.1)',
128
+ color: enabled ? '#059669' : '#dc2626',
129
+ }),
130
+ }
131
+
132
+ // ============================================================================
133
+ // Header Color Presets
134
+ // ============================================================================
135
+
136
+ export const NODE_HEADER_COLORS: Record<string, string> = {
137
+ prompt: '#f3e8ff', // Purple tint
138
+ 'fetch-url': '#dbeafe', // Blue tint
139
+ 'transform-data': '#fef3c7', // Amber tint
140
+ conditional: '#fce7f3', // Pink tint
141
+ 'send-email': '#d1fae5', // Green tint
142
+ default: '#f3f4f6', // Gray tint
143
+ }
144
+
145
+ // ============================================================================
146
+ // Base Node Component
147
+ // ============================================================================
148
+
149
+ export const BaseNode = memo(function BaseNode({
150
+ data,
151
+ selected = false,
152
+ id,
153
+ typeLabel,
154
+ icon,
155
+ headerColor,
156
+ showSourceHandle = true,
157
+ showTargetHandle = true,
158
+ additionalSourceHandles,
159
+ children,
160
+ }: BaseNodeProps) {
161
+ const { step, onChange, onDelete } = data
162
+ const color = headerColor || NODE_HEADER_COLORS[step.type] || NODE_HEADER_COLORS.default
163
+
164
+ const handleDelete = (e: React.MouseEvent) => {
165
+ e.stopPropagation()
166
+ onDelete?.(id)
167
+ }
168
+
169
+ return (
170
+ <div style={styles.container(selected)}>
171
+ {/* Target Handle (input) - on left for horizontal flow */}
172
+ {showTargetHandle && (
173
+ <Handle
174
+ type="target"
175
+ position={Position.Left}
176
+ style={{ ...styles.handle, ...styles.targetHandle }}
177
+ />
178
+ )}
179
+
180
+ {/* Header */}
181
+ <div style={styles.header(color)}>
182
+ <div style={styles.iconWrapper}>{icon}</div>
183
+ <div style={styles.headerContent}>
184
+ <div style={styles.typeLabel}>{typeLabel}</div>
185
+ <div style={styles.stepName} title={step.name}>
186
+ {step.name || 'Untitled Step'}
187
+ </div>
188
+ </div>
189
+ <div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
190
+ <span style={styles.enabledBadge(step.enabled)}>
191
+ {step.enabled ? 'Active' : 'Disabled'}
192
+ </span>
193
+ {onDelete && (
194
+ <button
195
+ style={styles.deleteButton}
196
+ onClick={handleDelete}
197
+ title="Delete step"
198
+ onMouseEnter={(e) => {
199
+ e.currentTarget.style.backgroundColor = 'rgba(239, 68, 68, 0.1)'
200
+ e.currentTarget.style.color = '#dc2626'
201
+ }}
202
+ onMouseLeave={(e) => {
203
+ e.currentTarget.style.backgroundColor = 'transparent'
204
+ e.currentTarget.style.color = '#9ca3af'
205
+ }}
206
+ >
207
+ <DeleteIcon />
208
+ </button>
209
+ )}
210
+ </div>
211
+ </div>
212
+
213
+ {/* Body - nodrag class prevents React Flow from intercepting events */}
214
+ <div style={styles.body} className="nodrag">{children}</div>
215
+
216
+ {/* Source Handle (output) - on right for horizontal flow */}
217
+ {showSourceHandle && (
218
+ <Handle
219
+ type="source"
220
+ position={Position.Right}
221
+ id="output"
222
+ style={{ ...styles.handle, ...styles.sourceHandle }}
223
+ />
224
+ )}
225
+
226
+ {/* Additional source handles for conditional branches */}
227
+ {additionalSourceHandles?.map((handle) => (
228
+ <Handle
229
+ key={handle.id}
230
+ type="source"
231
+ position={handle.position}
232
+ id={handle.id}
233
+ style={{
234
+ ...styles.handle,
235
+ backgroundColor: handle.color || '#10b981',
236
+ ...handle.style,
237
+ }}
238
+ />
239
+ ))}
240
+ </div>
241
+ )
242
+ })
243
+
244
+ // ============================================================================
245
+ // Icon Components
246
+ // ============================================================================
247
+
248
+ const DeleteIcon = () => (
249
+ <svg
250
+ width="14"
251
+ height="14"
252
+ viewBox="0 0 24 24"
253
+ fill="none"
254
+ stroke="currentColor"
255
+ strokeWidth="2"
256
+ strokeLinecap="round"
257
+ strokeLinejoin="round"
258
+ >
259
+ <path d="M3 6h18" />
260
+ <path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
261
+ <path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
262
+ </svg>
263
+ )
264
+
265
+ // Export common icons for use in step nodes
266
+ export const BrainIcon = () => (
267
+ <svg
268
+ width="16"
269
+ height="16"
270
+ viewBox="0 0 24 24"
271
+ fill="none"
272
+ stroke="currentColor"
273
+ strokeWidth="2"
274
+ strokeLinecap="round"
275
+ strokeLinejoin="round"
276
+ >
277
+ <path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z" />
278
+ <path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z" />
279
+ <path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4" />
280
+ <path d="M17.599 6.5a3 3 0 0 0 .399-1.375" />
281
+ <path d="M6.003 5.125A3 3 0 0 0 6.401 6.5" />
282
+ <path d="M3.477 10.896a4 4 0 0 1 .585-.396" />
283
+ <path d="M19.938 10.5a4 4 0 0 1 .585.396" />
284
+ <path d="M6 18a4 4 0 0 1-1.967-.516" />
285
+ <path d="M19.967 17.484A4 4 0 0 1 18 18" />
286
+ </svg>
287
+ )
288
+
289
+ export const GlobeIcon = () => (
290
+ <svg
291
+ width="16"
292
+ height="16"
293
+ viewBox="0 0 24 24"
294
+ fill="none"
295
+ stroke="currentColor"
296
+ strokeWidth="2"
297
+ strokeLinecap="round"
298
+ strokeLinejoin="round"
299
+ >
300
+ <circle cx="12" cy="12" r="10" />
301
+ <path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
302
+ <path d="M2 12h20" />
303
+ </svg>
304
+ )
305
+
306
+ export const CodeIcon = () => (
307
+ <svg
308
+ width="16"
309
+ height="16"
310
+ viewBox="0 0 24 24"
311
+ fill="none"
312
+ stroke="currentColor"
313
+ strokeWidth="2"
314
+ strokeLinecap="round"
315
+ strokeLinejoin="round"
316
+ >
317
+ <polyline points="16 18 22 12 16 6" />
318
+ <polyline points="8 6 2 12 8 18" />
319
+ </svg>
320
+ )
321
+
322
+ export const GitBranchIcon = () => (
323
+ <svg
324
+ width="16"
325
+ height="16"
326
+ viewBox="0 0 24 24"
327
+ fill="none"
328
+ stroke="currentColor"
329
+ strokeWidth="2"
330
+ strokeLinecap="round"
331
+ strokeLinejoin="round"
332
+ >
333
+ <line x1="6" y1="3" x2="6" y2="15" />
334
+ <circle cx="18" cy="6" r="3" />
335
+ <circle cx="6" cy="18" r="3" />
336
+ <path d="M18 9a9 9 0 0 1-9 9" />
337
+ </svg>
338
+ )
339
+
340
+ export const MailIcon = () => (
341
+ <svg
342
+ width="16"
343
+ height="16"
344
+ viewBox="0 0 24 24"
345
+ fill="none"
346
+ stroke="currentColor"
347
+ strokeWidth="2"
348
+ strokeLinecap="round"
349
+ strokeLinejoin="round"
350
+ >
351
+ <rect width="20" height="16" x="2" y="4" rx="2" />
352
+ <path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
353
+ </svg>
354
+ )
355
+
356
+ export default BaseNode
357
+
@@ -0,0 +1,252 @@
1
+ import React, { memo, useCallback, useState } from 'react'
2
+ import type { NodeProps } from '@xyflow/react'
3
+ import { BaseNode, CodeIcon, NODE_HEADER_COLORS } from './BaseNode'
4
+ import type { RuntypeNodeData, TransformDataStepConfig, FlowStep } from '../../types'
5
+
6
+ // ============================================================================
7
+ // Styles
8
+ // ============================================================================
9
+
10
+ const styles = {
11
+ field: {
12
+ marginBottom: '12px',
13
+ },
14
+ label: {
15
+ display: 'block',
16
+ fontSize: '11px',
17
+ fontWeight: 600,
18
+ color: '#6b7280',
19
+ marginBottom: '4px',
20
+ textTransform: 'uppercase' as const,
21
+ letterSpacing: '0.03em',
22
+ },
23
+ input: {
24
+ width: '100%',
25
+ padding: '8px 10px',
26
+ fontSize: '12px',
27
+ border: '1px solid #e5e7eb',
28
+ borderRadius: '6px',
29
+ backgroundColor: '#f9fafb',
30
+ color: '#1f2937',
31
+ outline: 'none',
32
+ transition: 'border-color 0.15s ease, box-shadow 0.15s ease',
33
+ },
34
+ codeArea: {
35
+ width: '100%',
36
+ padding: '10px',
37
+ fontSize: '11px',
38
+ border: '1px solid #e5e7eb',
39
+ borderRadius: '6px',
40
+ backgroundColor: '#1e1e1e',
41
+ color: '#d4d4d4',
42
+ outline: 'none',
43
+ resize: 'vertical' as const,
44
+ minHeight: '100px',
45
+ fontFamily: '"Fira Code", "Monaco", "Consolas", monospace',
46
+ lineHeight: 1.5,
47
+ tabSize: 2,
48
+ },
49
+ select: {
50
+ width: '100%',
51
+ padding: '8px 10px',
52
+ fontSize: '12px',
53
+ border: '1px solid #e5e7eb',
54
+ borderRadius: '6px',
55
+ backgroundColor: '#f9fafb',
56
+ color: '#1f2937',
57
+ outline: 'none',
58
+ cursor: 'pointer',
59
+ },
60
+ row: {
61
+ display: 'flex',
62
+ gap: '8px',
63
+ },
64
+ languageBadge: (language: string) => {
65
+ const colors: Record<string, { bg: string; text: string }> = {
66
+ javascript: { bg: '#fef3c7', text: '#d97706' },
67
+ typescript: { bg: '#dbeafe', text: '#1d4ed8' },
68
+ python: { bg: '#d1fae5', text: '#059669' },
69
+ }
70
+ const color = colors[language] || colors.javascript
71
+ return {
72
+ display: 'inline-flex',
73
+ alignItems: 'center',
74
+ padding: '2px 8px',
75
+ borderRadius: '4px',
76
+ fontSize: '10px',
77
+ fontWeight: 600,
78
+ backgroundColor: color.bg,
79
+ color: color.text,
80
+ textTransform: 'capitalize' as const,
81
+ }
82
+ },
83
+ lineNumbers: {
84
+ display: 'flex',
85
+ flexDirection: 'column' as const,
86
+ alignItems: 'flex-end',
87
+ paddingRight: '8px',
88
+ marginRight: '8px',
89
+ borderRight: '1px solid #3f3f46',
90
+ color: '#6b7280',
91
+ fontSize: '11px',
92
+ fontFamily: '"Fira Code", "Monaco", "Consolas", monospace',
93
+ lineHeight: 1.5,
94
+ userSelect: 'none' as const,
95
+ },
96
+ codePreview: {
97
+ fontSize: '10px',
98
+ color: '#9ca3af',
99
+ marginTop: '4px',
100
+ },
101
+ }
102
+
103
+ // ============================================================================
104
+ // Code Node Component
105
+ // ============================================================================
106
+
107
+ export const CodeNode = memo(function CodeNode(props: NodeProps) {
108
+ const { data, selected, id } = props as NodeProps & { data: RuntypeNodeData }
109
+ const { step, onChange } = data
110
+ const config = step.config as TransformDataStepConfig
111
+
112
+ const [isExpanded, setIsExpanded] = useState(false)
113
+
114
+ const handleChange = useCallback(
115
+ (field: keyof TransformDataStepConfig, value: unknown) => {
116
+ onChange?.(id, {
117
+ config: {
118
+ ...config,
119
+ [field]: value,
120
+ },
121
+ } as Partial<FlowStep>)
122
+ },
123
+ [id, config, onChange]
124
+ )
125
+
126
+ const handleNameChange = useCallback(
127
+ (e: React.ChangeEvent<HTMLInputElement>) => {
128
+ onChange?.(id, { name: e.target.value })
129
+ },
130
+ [id, onChange]
131
+ )
132
+
133
+ const language = config.language || 'javascript'
134
+ const lineCount = (config.script || '').split('\n').length
135
+
136
+ return (
137
+ <BaseNode
138
+ data={data}
139
+ selected={selected}
140
+ id={id}
141
+ typeLabel="Run Code"
142
+ icon={<CodeIcon />}
143
+ headerColor={NODE_HEADER_COLORS['transform-data']}
144
+ >
145
+ <div style={styles.field}>
146
+ <label style={styles.label}>Step Name</label>
147
+ <input
148
+ style={styles.input}
149
+ value={step.name}
150
+ onChange={handleNameChange}
151
+ placeholder="Enter step name"
152
+ onKeyDown={(e) => e.stopPropagation()}
153
+ />
154
+ </div>
155
+
156
+ <div style={styles.field}>
157
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' }}>
158
+ <label style={styles.label}>Code</label>
159
+ <span style={styles.languageBadge(language)}>{language}</span>
160
+ </div>
161
+ <textarea
162
+ style={styles.codeArea}
163
+ value={config.script || ''}
164
+ onChange={(e) => handleChange('script', e.target.value)}
165
+ placeholder={`// Write your ${language} code here\nreturn { result: input }`}
166
+ spellCheck={false}
167
+ onKeyDown={(e) => e.stopPropagation()}
168
+ />
169
+ <div style={styles.codePreview}>
170
+ {lineCount} line{lineCount !== 1 ? 's' : ''}
171
+ </div>
172
+ </div>
173
+
174
+ {isExpanded && (
175
+ <>
176
+ <div style={styles.row}>
177
+ <div style={{ ...styles.field, flex: 1 }}>
178
+ <label style={styles.label}>Language</label>
179
+ <select
180
+ style={styles.select}
181
+ value={language}
182
+ onChange={(e) => handleChange('language', e.target.value)}
183
+ onKeyDown={(e) => e.stopPropagation()}
184
+ >
185
+ <option value="javascript">JavaScript</option>
186
+ <option value="typescript">TypeScript</option>
187
+ <option value="python">Python</option>
188
+ </select>
189
+ </div>
190
+ <div style={{ ...styles.field, flex: 1 }}>
191
+ <label style={styles.label}>Sandbox</label>
192
+ <select
193
+ style={styles.select}
194
+ value={config.sandboxProvider || 'quickjs'}
195
+ onChange={(e) => handleChange('sandboxProvider', e.target.value)}
196
+ onKeyDown={(e) => e.stopPropagation()}
197
+ >
198
+ <option value="quickjs">QuickJS (Fast)</option>
199
+ <option value="daytona">Daytona (Full)</option>
200
+ </select>
201
+ </div>
202
+ </div>
203
+
204
+ <div style={styles.field}>
205
+ <label style={styles.label}>On Error</label>
206
+ <select
207
+ style={styles.select}
208
+ value={config.errorHandling || 'fail'}
209
+ onChange={(e) => handleChange('errorHandling', e.target.value)}
210
+ onKeyDown={(e) => e.stopPropagation()}
211
+ >
212
+ <option value="fail">Fail</option>
213
+ <option value="continue">Continue</option>
214
+ <option value="default">Use Default</option>
215
+ </select>
216
+ </div>
217
+
218
+ <div style={styles.field}>
219
+ <label style={styles.label}>Output Variable</label>
220
+ <input
221
+ style={styles.input}
222
+ value={config.outputVariable || ''}
223
+ onChange={(e) => handleChange('outputVariable', e.target.value)}
224
+ placeholder="transform_result"
225
+ onKeyDown={(e) => e.stopPropagation()}
226
+ />
227
+ </div>
228
+ </>
229
+ )}
230
+
231
+ <button
232
+ onClick={() => setIsExpanded(!isExpanded)}
233
+ style={{
234
+ width: '100%',
235
+ padding: '6px',
236
+ fontSize: '11px',
237
+ color: '#6366f1',
238
+ backgroundColor: 'transparent',
239
+ border: '1px dashed #e5e7eb',
240
+ borderRadius: '6px',
241
+ cursor: 'pointer',
242
+ marginTop: '4px',
243
+ }}
244
+ >
245
+ {isExpanded ? 'Show Less' : 'Show More Options'}
246
+ </button>
247
+ </BaseNode>
248
+ )
249
+ })
250
+
251
+ export default CodeNode
252
+