@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.
- package/package.json +9 -31
- package/src/api/README.md +507 -0
- package/src/api/adapter.ts +106 -0
- package/src/api/client.test.ts +640 -0
- package/src/api/client.ts +385 -0
- package/src/api/default-adapter.ts +243 -0
- package/src/api/index.ts +24 -0
- package/src/components/FilterRuleEditor/ARCHITECTURE.md +354 -0
- package/src/components/FilterRuleEditor/FieldSelector.tsx +91 -0
- package/src/components/FilterRuleEditor/FilterRuleEditor.stories.tsx +462 -0
- package/src/components/FilterRuleEditor/FilterRuleEditor.test.tsx +520 -0
- package/src/components/FilterRuleEditor/FilterRuleEditor.tsx +225 -0
- package/src/components/FilterRuleEditor/LogicToggle.tsx +64 -0
- package/src/components/FilterRuleEditor/OperatorSelector.tsx +75 -0
- package/src/components/FilterRuleEditor/README.md +291 -0
- package/src/components/FilterRuleEditor/RuleRow.tsx +246 -0
- package/src/components/FilterRuleEditor/ValueInputs/BooleanValueInput.tsx +54 -0
- package/src/components/FilterRuleEditor/ValueInputs/ChoiceValueInput.tsx +83 -0
- package/src/components/FilterRuleEditor/ValueInputs/DateValueInput.tsx +70 -0
- package/src/components/FilterRuleEditor/ValueInputs/MultiChoiceValueInput.tsx +132 -0
- package/src/components/FilterRuleEditor/ValueInputs/NumberValueInput.tsx +73 -0
- package/src/components/FilterRuleEditor/ValueInputs/TextValueInput.tsx +50 -0
- package/src/components/FilterRuleEditor/ValueInputs/index.ts +12 -0
- package/src/components/FilterRuleEditor/constants.ts +64 -0
- package/src/components/FilterRuleEditor/index.ts +14 -0
- package/src/components/FunnelCard/DESIGN.md +447 -0
- package/src/components/FunnelCard/FunnelCard.stories.tsx +484 -0
- package/src/components/FunnelCard/FunnelCard.test.ts +257 -0
- package/src/components/FunnelCard/FunnelCard.test.tsx +336 -0
- package/src/components/FunnelCard/FunnelCard.tsx +204 -0
- package/src/components/FunnelCard/FunnelStats.tsx +68 -0
- package/src/components/FunnelCard/IMPLEMENTATION_SUMMARY.md +505 -0
- package/src/components/FunnelCard/INSTALLATION.md +304 -0
- package/src/components/FunnelCard/MatchBar.tsx +49 -0
- package/src/components/FunnelCard/README.md +294 -0
- package/src/components/FunnelCard/StageIndicator.tsx +62 -0
- package/src/components/FunnelCard/StatusBadge.tsx +52 -0
- package/src/components/FunnelCard/index.ts +14 -0
- package/src/components/FunnelPreview/EntityCard.tsx +72 -0
- package/src/components/FunnelPreview/FunnelPreview.stories.tsx +227 -0
- package/src/components/FunnelPreview/FunnelPreview.test.tsx +316 -0
- package/src/components/FunnelPreview/FunnelPreview.tsx +249 -0
- package/src/components/FunnelPreview/LoadingPreview.tsx +60 -0
- package/src/components/FunnelPreview/PreviewStats.tsx +78 -0
- package/src/components/FunnelPreview/README.md +337 -0
- package/src/components/FunnelPreview/StageBreakdown.tsx +94 -0
- package/src/components/FunnelPreview/example.tsx +286 -0
- package/src/components/FunnelPreview/index.ts +14 -0
- package/src/components/FunnelRunHistory/COMPONENT_SUMMARY.md +246 -0
- package/src/components/FunnelRunHistory/FunnelRunHistory.stories.tsx +272 -0
- package/src/components/FunnelRunHistory/FunnelRunHistory.test.tsx +323 -0
- package/src/components/FunnelRunHistory/FunnelRunHistory.tsx +329 -0
- package/src/components/FunnelRunHistory/README.md +325 -0
- package/src/components/FunnelRunHistory/RunActions.tsx +168 -0
- package/src/components/FunnelRunHistory/RunDetailsModal.tsx +221 -0
- package/src/components/FunnelRunHistory/RunFilters.tsx +128 -0
- package/src/components/FunnelRunHistory/RunRow.tsx +122 -0
- package/src/components/FunnelRunHistory/RunStatusBadge.tsx +75 -0
- package/src/components/FunnelRunHistory/StageBreakdownList.tsx +110 -0
- package/src/components/FunnelRunHistory/index.ts +51 -0
- package/src/components/FunnelRunHistory/types.ts +40 -0
- package/src/components/FunnelRunHistory/utils.test.ts +126 -0
- package/src/components/FunnelRunHistory/utils.ts +100 -0
- package/src/components/FunnelStageBuilder/AddStageButton.tsx +52 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.css +413 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.stories.tsx +312 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.test.tsx +304 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.tsx +321 -0
- package/src/components/FunnelStageBuilder/README.md +341 -0
- package/src/components/FunnelStageBuilder/StageActions.test.tsx +205 -0
- package/src/components/FunnelStageBuilder/StageActions.tsx +126 -0
- package/src/components/FunnelStageBuilder/StageCard.tsx +202 -0
- package/src/components/FunnelStageBuilder/StageForm.tsx +262 -0
- package/src/components/FunnelStageBuilder/TagInput.test.tsx +178 -0
- package/src/components/FunnelStageBuilder/TagInput.tsx +129 -0
- package/src/components/FunnelStageBuilder/index.ts +21 -0
- package/src/components/FunnelVisualFlow/FlowLegend.tsx +77 -0
- package/{dist/components/index.css → src/components/FunnelVisualFlow/FunnelVisualFlow.css} +89 -13
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.stories.tsx +254 -0
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.test.tsx +208 -0
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.tsx +229 -0
- package/src/components/FunnelVisualFlow/README.md +323 -0
- package/src/components/FunnelVisualFlow/StageNode.tsx +188 -0
- package/src/components/FunnelVisualFlow/example.tsx +227 -0
- package/src/components/FunnelVisualFlow/index.ts +10 -0
- package/src/components/index.ts +102 -0
- package/src/core/README.md +307 -0
- package/src/core/engine.test.ts +1087 -0
- package/src/core/engine.ts +329 -0
- package/src/core/evaluator.example.ts +353 -0
- package/src/core/evaluator.test.ts +639 -0
- package/src/core/evaluator.ts +261 -0
- package/src/core/field-resolver.example.ts +175 -0
- package/src/core/field-resolver.test.ts +541 -0
- package/src/core/field-resolver.ts +247 -0
- package/src/core/index.ts +34 -0
- package/src/core/operators.test.ts +539 -0
- package/src/core/operators.ts +241 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/useDebouncedValue.ts +28 -0
- package/src/index.ts +155 -0
- package/src/store/README.md +342 -0
- package/src/store/create-funnel-store.test.ts +686 -0
- package/src/store/create-funnel-store.ts +538 -0
- package/src/store/index.ts +9 -0
- package/src/store/types.ts +294 -0
- package/src/stories/CrossDomain.stories.tsx +149 -0
- package/src/stories/Welcome.stories.tsx +81 -0
- package/src/stories/demo-data/index.ts +3 -0
- package/src/stories/demo-data/investors.ts +216 -0
- package/src/stories/demo-data/leads.ts +223 -0
- package/src/stories/demo-data/recipes.ts +217 -0
- package/src/test/setup.ts +5 -0
- package/src/types/index.ts +843 -0
- package/dist/client-3ESO2NHy.d.ts +0 -310
- package/dist/client-CZu03ACp.d.cts +0 -310
- package/dist/components/index.cjs +0 -3241
- package/dist/components/index.cjs.map +0 -1
- package/dist/components/index.css.map +0 -1
- package/dist/components/index.d.cts +0 -726
- package/dist/components/index.d.ts +0 -726
- package/dist/components/index.js +0 -3194
- package/dist/components/index.js.map +0 -1
- package/dist/core/index.cjs +0 -500
- package/dist/core/index.cjs.map +0 -1
- package/dist/core/index.d.cts +0 -359
- package/dist/core/index.d.ts +0 -359
- package/dist/core/index.js +0 -486
- package/dist/core/index.js.map +0 -1
- package/dist/hooks/index.cjs +0 -20
- package/dist/hooks/index.cjs.map +0 -1
- package/dist/hooks/index.d.cts +0 -11
- package/dist/hooks/index.d.ts +0 -11
- package/dist/hooks/index.js +0 -18
- package/dist/hooks/index.js.map +0 -1
- package/dist/index-BGDEXbuz.d.cts +0 -434
- package/dist/index-BGDEXbuz.d.ts +0 -434
- package/dist/index.cjs +0 -4499
- package/dist/index.cjs.map +0 -1
- package/dist/index.css +0 -198
- package/dist/index.css.map +0 -1
- package/dist/index.d.cts +0 -99
- package/dist/index.d.ts +0 -99
- package/dist/index.js +0 -4421
- package/dist/index.js.map +0 -1
- package/dist/store/index.cjs +0 -389
- package/dist/store/index.cjs.map +0 -1
- package/dist/store/index.d.cts +0 -225
- package/dist/store/index.d.ts +0 -225
- package/dist/store/index.js +0 -386
- 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
|
+
}
|