@startsimpli/funnels 0.1.4 → 0.1.6

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.
Files changed (151) hide show
  1. package/package.json +9 -31
  2. package/src/api/README.md +507 -0
  3. package/src/api/adapter.ts +106 -0
  4. package/src/api/client.test.ts +640 -0
  5. package/src/api/client.ts +385 -0
  6. package/src/api/default-adapter.ts +243 -0
  7. package/src/api/index.ts +24 -0
  8. package/src/components/FilterRuleEditor/ARCHITECTURE.md +354 -0
  9. package/src/components/FilterRuleEditor/FieldSelector.tsx +91 -0
  10. package/src/components/FilterRuleEditor/FilterRuleEditor.stories.tsx +462 -0
  11. package/src/components/FilterRuleEditor/FilterRuleEditor.test.tsx +520 -0
  12. package/src/components/FilterRuleEditor/FilterRuleEditor.tsx +225 -0
  13. package/src/components/FilterRuleEditor/LogicToggle.tsx +64 -0
  14. package/src/components/FilterRuleEditor/OperatorSelector.tsx +75 -0
  15. package/src/components/FilterRuleEditor/README.md +291 -0
  16. package/src/components/FilterRuleEditor/RuleRow.tsx +246 -0
  17. package/src/components/FilterRuleEditor/ValueInputs/BooleanValueInput.tsx +54 -0
  18. package/src/components/FilterRuleEditor/ValueInputs/ChoiceValueInput.tsx +83 -0
  19. package/src/components/FilterRuleEditor/ValueInputs/DateValueInput.tsx +70 -0
  20. package/src/components/FilterRuleEditor/ValueInputs/MultiChoiceValueInput.tsx +132 -0
  21. package/src/components/FilterRuleEditor/ValueInputs/NumberValueInput.tsx +73 -0
  22. package/src/components/FilterRuleEditor/ValueInputs/TextValueInput.tsx +50 -0
  23. package/src/components/FilterRuleEditor/ValueInputs/index.ts +12 -0
  24. package/src/components/FilterRuleEditor/constants.ts +64 -0
  25. package/src/components/FilterRuleEditor/index.ts +14 -0
  26. package/src/components/FunnelCard/DESIGN.md +447 -0
  27. package/src/components/FunnelCard/FunnelCard.stories.tsx +484 -0
  28. package/src/components/FunnelCard/FunnelCard.test.ts +257 -0
  29. package/src/components/FunnelCard/FunnelCard.test.tsx +336 -0
  30. package/src/components/FunnelCard/FunnelCard.tsx +204 -0
  31. package/src/components/FunnelCard/FunnelStats.tsx +68 -0
  32. package/src/components/FunnelCard/IMPLEMENTATION_SUMMARY.md +505 -0
  33. package/src/components/FunnelCard/INSTALLATION.md +304 -0
  34. package/src/components/FunnelCard/MatchBar.tsx +49 -0
  35. package/src/components/FunnelCard/README.md +294 -0
  36. package/src/components/FunnelCard/StageIndicator.tsx +62 -0
  37. package/src/components/FunnelCard/StatusBadge.tsx +52 -0
  38. package/src/components/FunnelCard/index.ts +14 -0
  39. package/src/components/FunnelPreview/EntityCard.tsx +72 -0
  40. package/src/components/FunnelPreview/FunnelPreview.stories.tsx +227 -0
  41. package/src/components/FunnelPreview/FunnelPreview.test.tsx +316 -0
  42. package/src/components/FunnelPreview/FunnelPreview.tsx +249 -0
  43. package/src/components/FunnelPreview/LoadingPreview.tsx +60 -0
  44. package/src/components/FunnelPreview/PreviewStats.tsx +78 -0
  45. package/src/components/FunnelPreview/README.md +337 -0
  46. package/src/components/FunnelPreview/StageBreakdown.tsx +94 -0
  47. package/src/components/FunnelPreview/example.tsx +286 -0
  48. package/src/components/FunnelPreview/index.ts +14 -0
  49. package/src/components/FunnelRunHistory/COMPONENT_SUMMARY.md +246 -0
  50. package/src/components/FunnelRunHistory/FunnelRunHistory.stories.tsx +272 -0
  51. package/src/components/FunnelRunHistory/FunnelRunHistory.test.tsx +323 -0
  52. package/src/components/FunnelRunHistory/FunnelRunHistory.tsx +329 -0
  53. package/src/components/FunnelRunHistory/README.md +325 -0
  54. package/src/components/FunnelRunHistory/RunActions.tsx +168 -0
  55. package/src/components/FunnelRunHistory/RunDetailsModal.tsx +221 -0
  56. package/src/components/FunnelRunHistory/RunFilters.tsx +128 -0
  57. package/src/components/FunnelRunHistory/RunRow.tsx +122 -0
  58. package/src/components/FunnelRunHistory/RunStatusBadge.tsx +75 -0
  59. package/src/components/FunnelRunHistory/StageBreakdownList.tsx +110 -0
  60. package/src/components/FunnelRunHistory/index.ts +51 -0
  61. package/src/components/FunnelRunHistory/types.ts +40 -0
  62. package/src/components/FunnelRunHistory/utils.test.ts +126 -0
  63. package/src/components/FunnelRunHistory/utils.ts +100 -0
  64. package/src/components/FunnelStageBuilder/AddStageButton.tsx +52 -0
  65. package/src/components/FunnelStageBuilder/FunnelStageBuilder.css +413 -0
  66. package/src/components/FunnelStageBuilder/FunnelStageBuilder.stories.tsx +312 -0
  67. package/src/components/FunnelStageBuilder/FunnelStageBuilder.test.tsx +304 -0
  68. package/src/components/FunnelStageBuilder/FunnelStageBuilder.tsx +321 -0
  69. package/src/components/FunnelStageBuilder/README.md +341 -0
  70. package/src/components/FunnelStageBuilder/StageActions.test.tsx +205 -0
  71. package/src/components/FunnelStageBuilder/StageActions.tsx +126 -0
  72. package/src/components/FunnelStageBuilder/StageCard.tsx +202 -0
  73. package/src/components/FunnelStageBuilder/StageForm.tsx +262 -0
  74. package/src/components/FunnelStageBuilder/TagInput.test.tsx +178 -0
  75. package/src/components/FunnelStageBuilder/TagInput.tsx +129 -0
  76. package/src/components/FunnelStageBuilder/index.ts +21 -0
  77. package/src/components/FunnelVisualFlow/FlowLegend.tsx +77 -0
  78. package/{dist/components/index.css → src/components/FunnelVisualFlow/FunnelVisualFlow.css} +89 -13
  79. package/src/components/FunnelVisualFlow/FunnelVisualFlow.stories.tsx +254 -0
  80. package/src/components/FunnelVisualFlow/FunnelVisualFlow.test.tsx +208 -0
  81. package/src/components/FunnelVisualFlow/FunnelVisualFlow.tsx +229 -0
  82. package/src/components/FunnelVisualFlow/README.md +323 -0
  83. package/src/components/FunnelVisualFlow/StageNode.tsx +188 -0
  84. package/src/components/FunnelVisualFlow/example.tsx +227 -0
  85. package/src/components/FunnelVisualFlow/index.ts +10 -0
  86. package/src/components/index.ts +102 -0
  87. package/src/core/README.md +307 -0
  88. package/src/core/engine.test.ts +1087 -0
  89. package/src/core/engine.ts +329 -0
  90. package/src/core/evaluator.example.ts +353 -0
  91. package/src/core/evaluator.test.ts +639 -0
  92. package/src/core/evaluator.ts +261 -0
  93. package/src/core/field-resolver.example.ts +175 -0
  94. package/src/core/field-resolver.test.ts +541 -0
  95. package/src/core/field-resolver.ts +247 -0
  96. package/src/core/index.ts +34 -0
  97. package/src/core/operators.test.ts +539 -0
  98. package/src/core/operators.ts +241 -0
  99. package/src/hooks/index.ts +5 -0
  100. package/src/hooks/useDebouncedValue.ts +28 -0
  101. package/src/index.ts +155 -0
  102. package/src/store/README.md +342 -0
  103. package/src/store/create-funnel-store.test.ts +686 -0
  104. package/src/store/create-funnel-store.ts +538 -0
  105. package/src/store/index.ts +9 -0
  106. package/src/store/types.ts +294 -0
  107. package/src/stories/CrossDomain.stories.tsx +149 -0
  108. package/src/stories/Welcome.stories.tsx +81 -0
  109. package/src/stories/demo-data/index.ts +3 -0
  110. package/src/stories/demo-data/investors.ts +216 -0
  111. package/src/stories/demo-data/leads.ts +223 -0
  112. package/src/stories/demo-data/recipes.ts +217 -0
  113. package/src/test/setup.ts +5 -0
  114. package/src/types/index.ts +843 -0
  115. package/dist/client-3ESO2NHy.d.ts +0 -310
  116. package/dist/client-CZu03ACp.d.cts +0 -310
  117. package/dist/components/index.cjs +0 -3241
  118. package/dist/components/index.cjs.map +0 -1
  119. package/dist/components/index.css.map +0 -1
  120. package/dist/components/index.d.cts +0 -726
  121. package/dist/components/index.d.ts +0 -726
  122. package/dist/components/index.js +0 -3194
  123. package/dist/components/index.js.map +0 -1
  124. package/dist/core/index.cjs +0 -500
  125. package/dist/core/index.cjs.map +0 -1
  126. package/dist/core/index.d.cts +0 -359
  127. package/dist/core/index.d.ts +0 -359
  128. package/dist/core/index.js +0 -486
  129. package/dist/core/index.js.map +0 -1
  130. package/dist/hooks/index.cjs +0 -20
  131. package/dist/hooks/index.cjs.map +0 -1
  132. package/dist/hooks/index.d.cts +0 -11
  133. package/dist/hooks/index.d.ts +0 -11
  134. package/dist/hooks/index.js +0 -18
  135. package/dist/hooks/index.js.map +0 -1
  136. package/dist/index-BGDEXbuz.d.cts +0 -434
  137. package/dist/index-BGDEXbuz.d.ts +0 -434
  138. package/dist/index.cjs +0 -4499
  139. package/dist/index.cjs.map +0 -1
  140. package/dist/index.css +0 -198
  141. package/dist/index.css.map +0 -1
  142. package/dist/index.d.cts +0 -99
  143. package/dist/index.d.ts +0 -99
  144. package/dist/index.js +0 -4421
  145. package/dist/index.js.map +0 -1
  146. package/dist/store/index.cjs +0 -389
  147. package/dist/store/index.cjs.map +0 -1
  148. package/dist/store/index.d.cts +0 -225
  149. package/dist/store/index.d.ts +0 -225
  150. package/dist/store/index.js +0 -386
  151. package/dist/store/index.js.map +0 -1
@@ -0,0 +1,323 @@
1
+ # FunnelVisualFlow Component
2
+
3
+ Interactive funnel visualization using React Flow. Shows funnel stages as nodes in a vertical flow with edges connecting them, displaying entity counts and exclusions between stages.
4
+
5
+ ## Features
6
+
7
+ - **Visual Stage Flow**: Stages displayed as nodes with vertical layout
8
+ - **Run Statistics**: Shows input/matched/excluded counts when run data provided
9
+ - **Color-Coded Actions**: Nodes colored by action type (continue=blue, exclude=red, tag=yellow, output=green)
10
+ - **Interactive**: Click stages for details, click edges to see excluded entities
11
+ - **Auto-Layout**: Vertical flow with automatic positioning
12
+ - **Zoom & Pan**: React Flow controls for navigation
13
+ - **Responsive**: Works on mobile, tablet, and desktop
14
+
15
+ ## Installation
16
+
17
+ The component is part of `@simpli/funnels`. React Flow is included as a dependency.
18
+
19
+ ```bash
20
+ npm install @xyflow/react
21
+ ```
22
+
23
+ ## Basic Usage
24
+
25
+ ```tsx
26
+ import { FunnelVisualFlow } from '@simpli/funnels';
27
+
28
+ function MyFunnelView() {
29
+ const funnel = {
30
+ id: 'funnel-1',
31
+ name: 'Frontend Developer Funnel',
32
+ stages: [
33
+ {
34
+ id: 'stage-1',
35
+ order: 0,
36
+ name: 'High ICP Score',
37
+ filter_logic: 'AND',
38
+ rules: [{ field_path: 'score', operator: 'gte', value: 50 }],
39
+ match_action: 'continue',
40
+ no_match_action: 'exclude',
41
+ },
42
+ // ... more stages
43
+ ],
44
+ // ... other funnel properties
45
+ };
46
+
47
+ return (
48
+ <FunnelVisualFlow
49
+ funnel={funnel}
50
+ height={600}
51
+ />
52
+ );
53
+ }
54
+ ```
55
+
56
+ ## With Run Data
57
+
58
+ Show statistics from a funnel run:
59
+
60
+ ```tsx
61
+ import { FunnelVisualFlow } from '@simpli/funnels';
62
+
63
+ function MyFunnelView() {
64
+ const runData = {
65
+ id: 'run-1',
66
+ funnel_id: 'funnel-1',
67
+ status: 'completed',
68
+ trigger_type: 'manual',
69
+ started_at: new Date().toISOString(),
70
+ total_input: 1000,
71
+ total_matched: 235,
72
+ total_excluded: 765,
73
+ total_tagged: 0,
74
+ stage_stats: {
75
+ 'stage-1': {
76
+ stage_id: 'stage-1',
77
+ stage_name: 'High ICP Score',
78
+ input_count: 1000,
79
+ matched_count: 500,
80
+ not_matched_count: 500,
81
+ excluded_count: 500,
82
+ tagged_count: 0,
83
+ continued_count: 500,
84
+ },
85
+ // ... more stage stats
86
+ },
87
+ };
88
+
89
+ return (
90
+ <FunnelVisualFlow
91
+ funnel={funnel}
92
+ runData={runData}
93
+ height={600}
94
+ />
95
+ );
96
+ }
97
+ ```
98
+
99
+ ## Interactive Mode
100
+
101
+ Handle clicks on stages and edges:
102
+
103
+ ```tsx
104
+ function MyFunnelView() {
105
+ const handleStageClick = (stage: FunnelStage) => {
106
+ console.log('Clicked stage:', stage.name);
107
+ // Show stage details modal, etc.
108
+ };
109
+
110
+ const handleEdgeClick = (fromStageId: string, toStageId: string) => {
111
+ console.log('Clicked edge:', fromStageId, '->', toStageId);
112
+ // Show excluded entities, etc.
113
+ };
114
+
115
+ return (
116
+ <FunnelVisualFlow
117
+ funnel={funnel}
118
+ runData={runData}
119
+ onStageClick={handleStageClick}
120
+ onEdgeClick={handleEdgeClick}
121
+ height={600}
122
+ />
123
+ );
124
+ }
125
+ ```
126
+
127
+ ## Props
128
+
129
+ ### FunnelVisualFlowProps
130
+
131
+ | Prop | Type | Required | Description |
132
+ |------|------|----------|-------------|
133
+ | `funnel` | `Funnel` | Yes | Funnel definition to visualize |
134
+ | `runData` | `FunnelRun` | No | Optional run data for showing counts |
135
+ | `onStageClick` | `(stage: FunnelStage) => void` | No | Callback when stage node is clicked |
136
+ | `onEdgeClick` | `(fromStage: string, toStage: string) => void` | No | Callback when edge is clicked |
137
+ | `className` | `string` | No | Additional CSS classes |
138
+ | `height` | `number \| string` | No | Height of flow container (default: 600px) |
139
+
140
+ ## Visual Design
141
+
142
+ ### Node Colors (by Action Type)
143
+
144
+ - **Blue** (#3b82f6): Continue to next stage
145
+ - **Red** (#ef4444): Exclude entities
146
+ - **Yellow** (#eab308): Tag entities
147
+ - **Green** (#22c55e): Final output
148
+
149
+ ### Node Layout
150
+
151
+ ```
152
+ ┌──────────────────────┐
153
+ │ ① High ICP Score │ ← Circled number (①②③)
154
+ │ 3 rules │ ← Rule count
155
+ │ Continue │ ← Action label
156
+ ├──────────────────────┤
157
+ │ Input: 1000 │ ← Statistics (when run data provided)
158
+ │ Matched: 500 │
159
+ │ Excluded: 500 │
160
+ └──────────────────────┘
161
+ ```
162
+
163
+ ### Edge Labels
164
+
165
+ Edges show the number of entities excluded between stages:
166
+ ```
167
+ Stage 1 (500 continued)
168
+ ↓ -500 ← 500 entities excluded
169
+ Stage 2 (350 continued)
170
+ ```
171
+
172
+ ## Keyboard Navigation
173
+
174
+ - **Tab**: Navigate between nodes
175
+ - **Enter/Space**: Activate focused node
176
+ - **+/-**: Zoom in/out
177
+ - **Arrow keys**: Pan the view
178
+
179
+ ## Customization
180
+
181
+ ### Custom Height
182
+
183
+ ```tsx
184
+ <FunnelVisualFlow
185
+ funnel={funnel}
186
+ height="100vh" // Full viewport height
187
+ />
188
+ ```
189
+
190
+ ### Custom Styling
191
+
192
+ ```tsx
193
+ <FunnelVisualFlow
194
+ funnel={funnel}
195
+ className="my-custom-flow"
196
+ />
197
+ ```
198
+
199
+ ```css
200
+ .my-custom-flow {
201
+ border: 2px solid #e5e7eb;
202
+ border-radius: 12px;
203
+ }
204
+
205
+ /* Customize node appearance */
206
+ .my-custom-flow .stage-node {
207
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
208
+ }
209
+
210
+ /* Customize edge appearance */
211
+ .my-custom-flow .react-flow__edge-path {
212
+ stroke-width: 3;
213
+ }
214
+ ```
215
+
216
+ ## Empty State
217
+
218
+ When a funnel has no stages, an empty state is shown:
219
+
220
+ ```tsx
221
+ <FunnelVisualFlow funnel={emptyFunnel} />
222
+ // Renders: "No stages to visualize"
223
+ ```
224
+
225
+ ## Components
226
+
227
+ ### StageNode
228
+
229
+ Custom React Flow node component for stages. Automatically used by FunnelVisualFlow.
230
+
231
+ ```tsx
232
+ import { StageNode } from '@simpli/funnels';
233
+ ```
234
+
235
+ ### FlowLegend
236
+
237
+ Legend panel explaining color coding. Automatically included in FunnelVisualFlow.
238
+
239
+ ```tsx
240
+ import { FlowLegend } from '@simpli/funnels';
241
+ ```
242
+
243
+ ## Utilities
244
+
245
+ ### getCircledNumber
246
+
247
+ Converts numbers to circled number characters (①②③):
248
+
249
+ ```tsx
250
+ import { getCircledNumber } from '@simpli/funnels';
251
+
252
+ getCircledNumber(1); // '①'
253
+ getCircledNumber(2); // '②'
254
+ getCircledNumber(10); // '⑩'
255
+ getCircledNumber(11); // '11' (plain number for > 10)
256
+ ```
257
+
258
+ ## Accessibility
259
+
260
+ - **Keyboard Navigation**: Full keyboard support via React Flow
261
+ - **ARIA Labels**: All interactive elements have proper labels
262
+ - **Focus Indicators**: Visible focus rings for keyboard navigation
263
+ - **Color + Text**: Not relying on color alone (action labels provided)
264
+ - **Screen Reader**: Semantic HTML with proper roles
265
+
266
+ ## Performance
267
+
268
+ - **Virtualization**: React Flow handles large graphs efficiently
269
+ - **Memoization**: Node and edge calculations are memoized
270
+ - **CSS-only**: No images, pure CSS styling
271
+ - **Lazy Rendering**: Only visible nodes are rendered
272
+
273
+ ## Browser Support
274
+
275
+ - Chrome/Edge: Full support
276
+ - Firefox: Full support
277
+ - Safari: Full support
278
+ - Mobile browsers: Full support with touch gestures
279
+
280
+ ## Examples
281
+
282
+ See `FunnelVisualFlow.stories.tsx` for comprehensive examples:
283
+
284
+ - Empty state
285
+ - Single stage
286
+ - Simple funnel (3 stages)
287
+ - Complex funnel (5+ stages)
288
+ - With run data
289
+ - Different action types
290
+ - Interactive mode
291
+ - Large funnels (10+ stages)
292
+
293
+ ## Generic Design
294
+
295
+ This component is **BRUTALLY GENERIC** and works with any funnel structure:
296
+
297
+ - Investor qualification funnels
298
+ - Recipe recommendation funnels
299
+ - Lead scoring funnels
300
+ - Task prioritization funnels
301
+ - Any sequential filtering workflow
302
+
303
+ No domain-specific logic—purely visualizes the funnel structure.
304
+
305
+ ## Testing
306
+
307
+ Tests cover:
308
+
309
+ - Rendering with 0, 1, and multiple stages
310
+ - Rendering with/without run data
311
+ - Click callbacks
312
+ - Custom props
313
+ - Utility functions
314
+
315
+ Run tests:
316
+
317
+ ```bash
318
+ npm run test -- FunnelVisualFlow.test
319
+ ```
320
+
321
+ ## License
322
+
323
+ Part of the @simpli/funnels package.
@@ -0,0 +1,188 @@
1
+ /**
2
+ * StageNode Component
3
+ *
4
+ * Custom React Flow node representing a funnel stage.
5
+ *
6
+ * Visual Design:
7
+ * - Circled number indicator (①②③)
8
+ * - Stage name (truncated if too long)
9
+ * - Rule count
10
+ * - Match/exclude counts (if run data available)
11
+ * - Color-coded border based on action type
12
+ *
13
+ * Color Coding:
14
+ * - Continue: Blue (#3b82f6)
15
+ * - Exclude/Tag+Exclude: Red (#ef4444)
16
+ * - Tag/Tag+Continue: Yellow (#eab308)
17
+ * - Output: Green (#22c55e)
18
+ *
19
+ * Interaction:
20
+ * - Hover: Border darkens, shadow increases
21
+ * - Click: Triggers onStageClick callback
22
+ * - Focus: Keyboard accessible with ring outline
23
+ *
24
+ * Accessibility:
25
+ * - Semantic HTML with proper roles
26
+ * - ARIA labels for screen readers
27
+ * - Keyboard navigation support
28
+ */
29
+
30
+ import React from 'react';
31
+ import { Handle, Position, NodeProps } from '@xyflow/react';
32
+ import { FunnelStage, StageStats, MatchAction, NoMatchAction } from '../../types';
33
+ import { getCircledNumber } from './FunnelVisualFlow';
34
+
35
+ export interface StageNodeData {
36
+ stage: FunnelStage;
37
+ stats?: StageStats;
38
+ onStageClick?: (stage: FunnelStage) => void;
39
+ }
40
+
41
+ /**
42
+ * Get color based on stage actions
43
+ */
44
+ function getStageColor(stage: FunnelStage): string {
45
+ // Primary action determines color
46
+ const matchAction = stage.match_action;
47
+ const noMatchAction = stage.no_match_action;
48
+
49
+ // Output stages are green
50
+ if (matchAction === 'output') {
51
+ return '#22c55e'; // green-500
52
+ }
53
+
54
+ // Exclude stages are red
55
+ if (noMatchAction === 'exclude' || noMatchAction === 'tag_exclude') {
56
+ return '#ef4444'; // red-500
57
+ }
58
+
59
+ // Tag stages are yellow
60
+ if (matchAction === 'tag' || matchAction === 'tag_continue') {
61
+ return '#eab308'; // yellow-500
62
+ }
63
+
64
+ // Default: continue is blue
65
+ return '#3b82f6'; // blue-500
66
+ }
67
+
68
+ /**
69
+ * Get action label for display
70
+ */
71
+ function getActionLabel(stage: FunnelStage): string {
72
+ const matchAction = stage.match_action;
73
+ const noMatchAction = stage.no_match_action;
74
+
75
+ if (matchAction === 'output') return 'Output';
76
+ if (noMatchAction === 'exclude') return 'Exclude Non-Matches';
77
+ if (noMatchAction === 'tag_exclude') return 'Tag & Exclude';
78
+ if (matchAction === 'tag') return 'Tag Matches';
79
+ if (matchAction === 'tag_continue') return 'Tag & Continue';
80
+
81
+ return 'Continue';
82
+ }
83
+
84
+ export function StageNode({ data }: NodeProps) {
85
+ const { stage, stats, onStageClick } = data as unknown as StageNodeData;
86
+ const color = getStageColor(stage);
87
+ const actionLabel = getActionLabel(stage);
88
+
89
+ const handleClick = () => {
90
+ if (onStageClick) {
91
+ onStageClick(stage);
92
+ }
93
+ };
94
+
95
+ const handleKeyDown = (event: React.KeyboardEvent) => {
96
+ if (event.key === 'Enter' || event.key === ' ') {
97
+ event.preventDefault();
98
+ handleClick();
99
+ }
100
+ };
101
+
102
+ return (
103
+ <>
104
+ {/* Input handle (top) - hidden for first stage */}
105
+ <Handle
106
+ type="target"
107
+ position={Position.Top}
108
+ style={{ background: color, opacity: 0 }}
109
+ isConnectable={false}
110
+ />
111
+
112
+ <div
113
+ className="stage-node"
114
+ onClick={handleClick}
115
+ onKeyDown={handleKeyDown}
116
+ role="button"
117
+ tabIndex={0}
118
+ aria-label={`Stage ${stage.order + 1}: ${stage.name}`}
119
+ style={{
120
+ borderColor: color,
121
+ borderWidth: '2px',
122
+ borderStyle: 'solid',
123
+ }}
124
+ >
125
+ {/* Stage number */}
126
+ <div className="stage-number" style={{ color }}>
127
+ {getCircledNumber(stage.order + 1)}
128
+ </div>
129
+
130
+ {/* Stage name */}
131
+ <div className="stage-name" title={stage.name}>
132
+ {stage.name}
133
+ </div>
134
+
135
+ {/* Rule count */}
136
+ <div className="stage-rules">
137
+ {stage.rules.length} {stage.rules.length === 1 ? 'rule' : 'rules'}
138
+ </div>
139
+
140
+ {/* Action label */}
141
+ <div className="stage-action" style={{ color }}>
142
+ {actionLabel}
143
+ </div>
144
+
145
+ {/* Stats (if available) */}
146
+ {stats && (
147
+ <div className="stage-stats">
148
+ <div className="stat-row">
149
+ <span className="stat-label">Input:</span>
150
+ <span className="stat-value">{stats.input_count}</span>
151
+ </div>
152
+ <div className="stat-row">
153
+ <span className="stat-label">Matched:</span>
154
+ <span className="stat-value text-green-600">
155
+ {stats.matched_count}
156
+ </span>
157
+ </div>
158
+ {stats.excluded_count > 0 && (
159
+ <div className="stat-row">
160
+ <span className="stat-label">Excluded:</span>
161
+ <span className="stat-value text-red-600">
162
+ {stats.excluded_count}
163
+ </span>
164
+ </div>
165
+ )}
166
+ </div>
167
+ )}
168
+
169
+ {/* Description tooltip */}
170
+ {stage.description && (
171
+ <div className="stage-description" title={stage.description}>
172
+ {stage.description.length > 50
173
+ ? `${stage.description.substring(0, 50)}...`
174
+ : stage.description}
175
+ </div>
176
+ )}
177
+ </div>
178
+
179
+ {/* Output handle (bottom) - hidden for last stage */}
180
+ <Handle
181
+ type="source"
182
+ position={Position.Bottom}
183
+ style={{ background: color, opacity: 0 }}
184
+ isConnectable={false}
185
+ />
186
+ </>
187
+ );
188
+ }