@startsimpli/funnels 0.1.3 → 0.1.5

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 -3243
  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 -3196
  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 -21
  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 -19
  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 -391
  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 -388
  151. package/dist/store/index.js.map +0 -1
@@ -0,0 +1,321 @@
1
+ /**
2
+ * FunnelStageBuilder - Main component for creating/editing funnel stages
3
+ *
4
+ * Features:
5
+ * - Drag-and-drop reordering
6
+ * - Add/remove stages
7
+ * - Stage configuration (name, description, filters, actions)
8
+ * - Visual flow with arrows
9
+ * - Expandable stage cards
10
+ */
11
+
12
+ import React, { useState, useCallback } from 'react';
13
+ import {
14
+ DndContext,
15
+ closestCenter,
16
+ KeyboardSensor,
17
+ PointerSensor,
18
+ useSensor,
19
+ useSensors,
20
+ DragEndEvent,
21
+ } from '@dnd-kit/core';
22
+ import {
23
+ arrayMove,
24
+ SortableContext,
25
+ sortableKeyboardCoordinates,
26
+ verticalListSortingStrategy,
27
+ } from '@dnd-kit/sortable';
28
+ import { Funnel, FunnelStage, FieldDefinition } from '../../types';
29
+ import { StageCard } from './StageCard';
30
+ import { AddStageButton } from './AddStageButton';
31
+
32
+ export interface FunnelStageBuilderProps {
33
+ /** The funnel being edited */
34
+ funnel: Funnel;
35
+
36
+ /** Callback when funnel is updated */
37
+ onUpdate: (funnel: Funnel) => void;
38
+
39
+ /** Available fields for filter rules */
40
+ fieldRegistry: FieldDefinition[];
41
+
42
+ /** Optional CSS class */
43
+ className?: string;
44
+ }
45
+
46
+ /**
47
+ * Generate unique stage ID
48
+ */
49
+ function generateStageId(): string {
50
+ return `stage-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
51
+ }
52
+
53
+ /**
54
+ * Create a new empty stage
55
+ */
56
+ function createEmptyStage(order: number): FunnelStage {
57
+ return {
58
+ id: generateStageId(),
59
+ order,
60
+ name: `Stage ${order + 1}`,
61
+ description: '',
62
+ filter_logic: 'AND',
63
+ rules: [],
64
+ match_action: 'continue',
65
+ no_match_action: 'continue',
66
+ match_tags: [],
67
+ no_match_tags: [],
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Validate stage name uniqueness
73
+ */
74
+ function validateStageName(name: string, stages: FunnelStage[], currentStageId?: string): string | null {
75
+ const trimmedName = name.trim();
76
+
77
+ if (!trimmedName) {
78
+ return 'Stage name is required';
79
+ }
80
+
81
+ const duplicate = stages.find(
82
+ s => s.id !== currentStageId && s.name.trim().toLowerCase() === trimmedName.toLowerCase()
83
+ );
84
+
85
+ if (duplicate) {
86
+ return 'Stage name must be unique';
87
+ }
88
+
89
+ return null;
90
+ }
91
+
92
+ /**
93
+ * Main FunnelStageBuilder component
94
+ */
95
+ export function FunnelStageBuilder({
96
+ funnel,
97
+ onUpdate,
98
+ fieldRegistry,
99
+ className = '',
100
+ }: FunnelStageBuilderProps) {
101
+ // Track expanded stages
102
+ const [expandedStages, setExpandedStages] = useState<Set<string>>(
103
+ new Set(funnel.stages.map(s => s.id))
104
+ );
105
+
106
+ // Validation errors
107
+ const [errors, setErrors] = useState<Map<string, string>>(new Map());
108
+
109
+ // Configure drag-and-drop sensors
110
+ const sensors = useSensors(
111
+ useSensor(PointerSensor),
112
+ useSensor(KeyboardSensor, {
113
+ coordinateGetter: sortableKeyboardCoordinates,
114
+ })
115
+ );
116
+
117
+ /**
118
+ * Toggle stage expanded/collapsed
119
+ */
120
+ const toggleExpanded = useCallback((stageId: string) => {
121
+ setExpandedStages(prev => {
122
+ const next = new Set(prev);
123
+ if (next.has(stageId)) {
124
+ next.delete(stageId);
125
+ } else {
126
+ next.add(stageId);
127
+ }
128
+ return next;
129
+ });
130
+ }, []);
131
+
132
+ /**
133
+ * Add new stage
134
+ */
135
+ const handleAddStage = useCallback((insertAfterIndex?: number) => {
136
+ const newOrder = insertAfterIndex !== undefined ? insertAfterIndex + 1 : funnel.stages.length;
137
+ const newStage = createEmptyStage(newOrder);
138
+
139
+ // Update orders for stages after insertion point
140
+ const updatedStages = funnel.stages.map(stage => {
141
+ if (stage.order >= newOrder) {
142
+ return { ...stage, order: stage.order + 1 };
143
+ }
144
+ return stage;
145
+ });
146
+
147
+ // Insert new stage
148
+ updatedStages.splice(newOrder, 0, newStage);
149
+
150
+ // Expand new stage
151
+ setExpandedStages(prev => new Set(prev).add(newStage.id));
152
+
153
+ onUpdate({
154
+ ...funnel,
155
+ stages: updatedStages,
156
+ });
157
+ }, [funnel, onUpdate]);
158
+
159
+ /**
160
+ * Remove stage
161
+ */
162
+ const handleRemoveStage = useCallback((stageId: string) => {
163
+ const stageIndex = funnel.stages.findIndex(s => s.id === stageId);
164
+ if (stageIndex === -1) return;
165
+
166
+ // Remove stage
167
+ const updatedStages = funnel.stages.filter(s => s.id !== stageId);
168
+
169
+ // Reorder remaining stages
170
+ updatedStages.forEach((stage, index) => {
171
+ stage.order = index;
172
+ });
173
+
174
+ // Remove from expanded set
175
+ setExpandedStages(prev => {
176
+ const next = new Set(prev);
177
+ next.delete(stageId);
178
+ return next;
179
+ });
180
+
181
+ // Remove validation errors
182
+ setErrors(prev => {
183
+ const next = new Map(prev);
184
+ next.delete(stageId);
185
+ return next;
186
+ });
187
+
188
+ onUpdate({
189
+ ...funnel,
190
+ stages: updatedStages,
191
+ });
192
+ }, [funnel, onUpdate]);
193
+
194
+ /**
195
+ * Update stage
196
+ */
197
+ const handleUpdateStage = useCallback((updatedStage: FunnelStage) => {
198
+ // Validate stage name
199
+ const nameError = validateStageName(updatedStage.name, funnel.stages, updatedStage.id);
200
+
201
+ setErrors(prev => {
202
+ const next = new Map(prev);
203
+ if (nameError) {
204
+ next.set(updatedStage.id, nameError);
205
+ } else {
206
+ next.delete(updatedStage.id);
207
+ }
208
+ return next;
209
+ });
210
+
211
+ const updatedStages = funnel.stages.map(stage =>
212
+ stage.id === updatedStage.id ? updatedStage : stage
213
+ );
214
+
215
+ onUpdate({
216
+ ...funnel,
217
+ stages: updatedStages,
218
+ });
219
+ }, [funnel, onUpdate]);
220
+
221
+ /**
222
+ * Handle drag end
223
+ */
224
+ const handleDragEnd = useCallback((event: DragEndEvent) => {
225
+ const { active, over } = event;
226
+
227
+ if (!over || active.id === over.id) {
228
+ return;
229
+ }
230
+
231
+ const oldIndex = funnel.stages.findIndex(s => s.id === active.id);
232
+ const newIndex = funnel.stages.findIndex(s => s.id === over.id);
233
+
234
+ if (oldIndex === -1 || newIndex === -1) {
235
+ return;
236
+ }
237
+
238
+ // Reorder stages
239
+ const reorderedStages = arrayMove(funnel.stages, oldIndex, newIndex);
240
+
241
+ // Update order values
242
+ reorderedStages.forEach((stage, index) => {
243
+ stage.order = index;
244
+ });
245
+
246
+ onUpdate({
247
+ ...funnel,
248
+ stages: reorderedStages,
249
+ });
250
+ }, [funnel, onUpdate]);
251
+
252
+ return (
253
+ <div className={`funnel-stage-builder ${className}`}>
254
+ {/* Add stage at top */}
255
+ <div className="mb-4">
256
+ <AddStageButton onClick={() => handleAddStage()} position="top" />
257
+ </div>
258
+
259
+ {/* Stage list with drag-and-drop */}
260
+ <DndContext
261
+ sensors={sensors}
262
+ collisionDetection={closestCenter}
263
+ onDragEnd={handleDragEnd}
264
+ >
265
+ <SortableContext
266
+ items={funnel.stages.map(s => s.id)}
267
+ strategy={verticalListSortingStrategy}
268
+ >
269
+ {funnel.stages.map((stage, index) => (
270
+ <div key={stage.id} className="stage-wrapper">
271
+ <StageCard
272
+ stage={stage}
273
+ expanded={expandedStages.has(stage.id)}
274
+ onToggleExpanded={() => toggleExpanded(stage.id)}
275
+ onUpdate={handleUpdateStage}
276
+ onRemove={() => handleRemoveStage(stage.id)}
277
+ fieldRegistry={fieldRegistry}
278
+ error={errors.get(stage.id)}
279
+ showWarnings={stage.rules.length === 0}
280
+ />
281
+
282
+ {/* Flow arrow between stages */}
283
+ {index < funnel.stages.length - 1 && (
284
+ <div className="stage-arrow">
285
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none">
286
+ <path
287
+ d="M12 5v14m0 0l-4-4m4 4l4-4"
288
+ stroke="currentColor"
289
+ strokeWidth="2"
290
+ strokeLinecap="round"
291
+ strokeLinejoin="round"
292
+ />
293
+ </svg>
294
+ </div>
295
+ )}
296
+ </div>
297
+ ))}
298
+ </SortableContext>
299
+ </DndContext>
300
+
301
+ {/* Add stage at bottom */}
302
+ {funnel.stages.length > 0 && (
303
+ <div className="mt-4">
304
+ <AddStageButton
305
+ onClick={() => handleAddStage(funnel.stages.length - 1)}
306
+ position="bottom"
307
+ />
308
+ </div>
309
+ )}
310
+
311
+ {/* Empty state */}
312
+ {funnel.stages.length === 0 && (
313
+ <div className="empty-state">
314
+ <p className="text-gray-500 text-center py-8">
315
+ No stages yet. Add your first stage to get started.
316
+ </p>
317
+ </div>
318
+ )}
319
+ </div>
320
+ );
321
+ }
@@ -0,0 +1,341 @@
1
+ # FunnelStageBuilder
2
+
3
+ Visual builder component for creating and editing funnel stages with drag-and-drop reordering.
4
+
5
+ ## Overview
6
+
7
+ The FunnelStageBuilder provides a complete interface for managing funnel stages:
8
+
9
+ - **Drag-and-Drop Reordering** - Reorder stages visually using @dnd-kit
10
+ - **Add/Remove Stages** - Add stages at top, bottom, or inline
11
+ - **Stage Configuration** - Name, description, filter logic, actions, and tags
12
+ - **Visual Flow** - Arrows showing flow between stages
13
+ - **Expandable Cards** - Collapse stages to see high-level summary
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities react react-dom
19
+ ```
20
+
21
+ ## Basic Usage
22
+
23
+ ```tsx
24
+ import { FunnelStageBuilder } from '@simpli/funnels/components';
25
+ import { Funnel, FieldDefinition } from '@simpli/funnels';
26
+
27
+ const fieldRegistry: FieldDefinition[] = [
28
+ {
29
+ name: 'firm.stage',
30
+ label: 'Investment Stage',
31
+ type: 'string',
32
+ operators: ['eq', 'ne', 'in'],
33
+ category: 'Firm',
34
+ },
35
+ // ... more fields
36
+ ];
37
+
38
+ function MyComponent() {
39
+ const [funnel, setFunnel] = useState<Funnel>({
40
+ id: 'funnel-1',
41
+ name: 'My Funnel',
42
+ status: 'draft',
43
+ input_type: 'contacts',
44
+ stages: [],
45
+ created_at: new Date().toISOString(),
46
+ updated_at: new Date().toISOString(),
47
+ });
48
+
49
+ return (
50
+ <FunnelStageBuilder
51
+ funnel={funnel}
52
+ onUpdate={setFunnel}
53
+ fieldRegistry={fieldRegistry}
54
+ />
55
+ );
56
+ }
57
+ ```
58
+
59
+ ## Props
60
+
61
+ ### FunnelStageBuilderProps
62
+
63
+ | Prop | Type | Required | Description |
64
+ |------|------|----------|-------------|
65
+ | `funnel` | `Funnel` | Yes | The funnel being edited |
66
+ | `onUpdate` | `(funnel: Funnel) => void` | Yes | Callback when funnel is updated |
67
+ | `fieldRegistry` | `FieldDefinition[]` | Yes | Available fields for filter rules |
68
+ | `className` | `string` | No | Optional CSS class |
69
+
70
+ ## Features
71
+
72
+ ### 1. Drag-and-Drop Reordering
73
+
74
+ Uses @dnd-kit for accessible, performant drag-and-drop:
75
+
76
+ ```tsx
77
+ // Automatically handles:
78
+ // - Stage reordering
79
+ // - Order value updates
80
+ // - Visual feedback during drag
81
+ // - Keyboard navigation (Space/Enter to grab, Arrow keys to move)
82
+ ```
83
+
84
+ ### 2. Add/Remove Stages
85
+
86
+ ```tsx
87
+ // Add stage at top
88
+ <AddStageButton onClick={() => handleAddStage()} position="top" />
89
+
90
+ // Add stage at bottom
91
+ <AddStageButton onClick={() => handleAddStage(lastIndex)} position="bottom" />
92
+
93
+ // Remove stage
94
+ <button onClick={() => handleRemoveStage(stageId)}>Delete</button>
95
+ ```
96
+
97
+ ### 3. Stage Configuration
98
+
99
+ Each stage can be configured with:
100
+
101
+ - **Name** - Required, must be unique
102
+ - **Description** - Optional
103
+ - **Filter Logic** - AND (all rules must match) or OR (at least one must match)
104
+ - **Rules** - Array of filter rules (requires FilterRuleEditor component)
105
+ - **Match Action** - What to do when rules match:
106
+ - `continue` - Continue to next stage
107
+ - `tag` - Add tags and stop
108
+ - `tag_continue` - Add tags and continue
109
+ - `output` - Add to output and stop
110
+ - **No Match Action** - What to do when rules don't match:
111
+ - `continue` - Continue to next stage
112
+ - `exclude` - Exclude from output and stop
113
+ - `tag_exclude` - Add tags, exclude, and stop
114
+ - **Tags** - Tags to add on match/no-match
115
+
116
+ ### 4. Validation
117
+
118
+ Built-in validation for:
119
+
120
+ - **Required name** - Stage must have a name
121
+ - **Unique names** - No duplicate stage names
122
+ - **Warning for empty rules** - Shows warning if stage has no filter rules
123
+
124
+ ```tsx
125
+ // Validation errors shown inline
126
+ <div className="error-message">
127
+ Stage name must be unique
128
+ </div>
129
+
130
+ // Warnings shown inline
131
+ <div className="warning-message">
132
+ Stage has no filter rules
133
+ </div>
134
+ ```
135
+
136
+ ### 5. Debounced Updates
137
+
138
+ Text inputs (name, description) use debounced updates to avoid excessive callbacks:
139
+
140
+ ```tsx
141
+ // Name updates after 300ms
142
+ // Description updates after 500ms
143
+ ```
144
+
145
+ ## Components
146
+
147
+ ### FunnelStageBuilder
148
+
149
+ Main container component that orchestrates all functionality.
150
+
151
+ ### StageCard
152
+
153
+ Individual stage card with:
154
+ - Drag handle
155
+ - Expand/collapse toggle
156
+ - Delete button
157
+ - Validation errors/warnings
158
+ - Collapsed summary
159
+ - Expanded form
160
+
161
+ ### StageForm
162
+
163
+ Stage configuration form with:
164
+ - Name input
165
+ - Description textarea
166
+ - Filter logic toggle
167
+ - Match/no-match actions
168
+ - Tags input
169
+ - Rules editor placeholder
170
+
171
+ ### StageActions
172
+
173
+ Action dropdowns for match/no-match actions with descriptions.
174
+
175
+ ### TagInput
176
+
177
+ Multi-tag input component:
178
+ - Add tags with Enter or comma
179
+ - Remove tags with click
180
+ - Visual tag chips
181
+ - Duplicate prevention
182
+
183
+ ### AddStageButton
184
+
185
+ Button to add new stages with context-aware labels.
186
+
187
+ ## Styling
188
+
189
+ The component comes with base styles in `FunnelStageBuilder.css`:
190
+
191
+ ```tsx
192
+ import './FunnelStageBuilder.css';
193
+ ```
194
+
195
+ Key CSS classes:
196
+
197
+ - `.funnel-stage-builder` - Main container
198
+ - `.stage-card` - Stage card
199
+ - `.stage-header` - Card header
200
+ - `.stage-form` - Stage configuration form
201
+ - `.drag-handle` - Drag handle button
202
+ - `.tag-chip` - Tag pill
203
+ - `.error-message` - Validation error
204
+ - `.warning-message` - Warning message
205
+
206
+ Override styles by targeting these classes or passing custom `className`.
207
+
208
+ ## Accessibility
209
+
210
+ The component follows accessibility best practices:
211
+
212
+ - **Keyboard Navigation** - Full keyboard support for drag-and-drop
213
+ - **ARIA Labels** - All interactive elements have appropriate labels
214
+ - **Focus Management** - Logical tab order
215
+ - **Screen Reader Support** - Semantic HTML and ARIA attributes
216
+
217
+ ## Testing
218
+
219
+ The component includes comprehensive tests:
220
+
221
+ ```bash
222
+ npm run test
223
+ ```
224
+
225
+ Test coverage includes:
226
+
227
+ - Rendering with 0, 1, and multiple stages
228
+ - Adding stages at top/bottom
229
+ - Removing stages
230
+ - Reordering stages (drag-and-drop)
231
+ - Expanding/collapsing stages
232
+ - Form input changes
233
+ - Validation (unique names, required fields)
234
+ - Tag input (add/remove tags)
235
+ - Action dropdowns
236
+
237
+ ## Integration Notes
238
+
239
+ ### FilterRuleEditor Integration
240
+
241
+ The component includes a placeholder for the FilterRuleEditor:
242
+
243
+ ```tsx
244
+ {/* TODO: Integrate FilterRuleEditor component once available */}
245
+ <div className="rules-placeholder">
246
+ <p>FilterRuleEditor component will be integrated here</p>
247
+ </div>
248
+ ```
249
+
250
+ When integrating FilterRuleEditor:
251
+
252
+ ```tsx
253
+ import { FilterRuleEditor } from '../FilterRuleEditor';
254
+
255
+ // In StageForm.tsx
256
+ <FilterRuleEditor
257
+ rules={stage.rules}
258
+ onUpdate={(rules) => onUpdate({ ...stage, rules })}
259
+ fieldRegistry={fieldRegistry}
260
+ />
261
+ ```
262
+
263
+ ### State Management
264
+
265
+ The component is stateless - all state is managed by parent:
266
+
267
+ ```tsx
268
+ // Parent manages funnel state
269
+ const [funnel, setFunnel] = useState<Funnel>(...);
270
+
271
+ // Component calls onUpdate when user makes changes
272
+ <FunnelStageBuilder
273
+ funnel={funnel}
274
+ onUpdate={setFunnel}
275
+ fieldRegistry={fieldRegistry}
276
+ />
277
+ ```
278
+
279
+ For global state management, wrap with your state library:
280
+
281
+ ```tsx
282
+ // With Zustand
283
+ import { useFunnelStore } from './store';
284
+
285
+ function MyComponent() {
286
+ const { funnel, updateFunnel } = useFunnelStore();
287
+
288
+ return (
289
+ <FunnelStageBuilder
290
+ funnel={funnel}
291
+ onUpdate={updateFunnel}
292
+ fieldRegistry={fieldRegistry}
293
+ />
294
+ );
295
+ }
296
+ ```
297
+
298
+ ## Examples
299
+
300
+ See `FunnelStageBuilder.stories.tsx` for interactive examples:
301
+
302
+ - Empty state
303
+ - Single stage
304
+ - Multiple stages
305
+ - With validation errors
306
+ - Different action combinations
307
+
308
+ Run Storybook to view:
309
+
310
+ ```bash
311
+ npm run storybook
312
+ ```
313
+
314
+ ## Generic Design
315
+
316
+ This component works with **any** entity type and field registry:
317
+
318
+ ```tsx
319
+ // Works for investors
320
+ const investorFields = [
321
+ { name: 'firm.stage', label: 'Stage', type: 'string', operators: ['eq'] },
322
+ ];
323
+
324
+ // Works for recipes
325
+ const recipeFields = [
326
+ { name: 'recipe.cuisine', label: 'Cuisine', type: 'string', operators: ['eq'] },
327
+ ];
328
+
329
+ // Works for leads
330
+ const leadFields = [
331
+ { name: 'lead.score', label: 'Score', type: 'number', operators: ['gt'] },
332
+ ];
333
+
334
+ // Same component, different field registries!
335
+ ```
336
+
337
+ No domain-specific logic. Pure, generic UI.
338
+
339
+ ## License
340
+
341
+ MIT