@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,225 @@
1
+ /**
2
+ * FilterRuleEditor Component
3
+ *
4
+ * Main component for creating and editing filter rules.
5
+ *
6
+ * Design Goal:
7
+ * Users need to build complex filters by combining multiple rules with AND/OR logic.
8
+ * This component provides a visual rule builder that dynamically adapts to any
9
+ * entity type (investors, recipes, contacts, etc.) based on the field registry.
10
+ *
11
+ * Visual Hierarchy Rationale:
12
+ * - Logic toggle at top sets the mode (AND/OR)
13
+ * - Rules stacked vertically for clarity
14
+ * - AND/OR connectors between rules reinforce the logic visually
15
+ * - Add Rule button at bottom for progressive disclosure
16
+ * - Empty state shown when no rules exist
17
+ *
18
+ * Component Specification:
19
+ * - Logic toggle: Switch between AND/OR
20
+ * - Rule rows: Each rule is a self-contained row
21
+ * - Add button: Adds a new empty rule
22
+ * - Remove button: Each rule has a delete button
23
+ * - Validation: Real-time validation with error messages
24
+ *
25
+ * Interaction Notes:
26
+ * - Users can add unlimited rules (within reason)
27
+ * - Removing a rule updates immediately
28
+ * - Logic toggle applies to all rules
29
+ * - Each rule validates independently
30
+ * - Focus management when adding/removing rules
31
+ *
32
+ * Responsive Behavior:
33
+ * - Full width container
34
+ * - Rule rows responsive (see RuleRow component)
35
+ * - Add button full-width on mobile
36
+ *
37
+ * Accessibility Considerations:
38
+ * - ARIA labels for controls
39
+ * - Keyboard navigation throughout
40
+ * - Focus management for add/remove actions
41
+ * - Screen reader announces rule count
42
+ * - Error messages associated with fields
43
+ *
44
+ * Design Handoff Notes:
45
+ * - Uses Tailwind utility classes
46
+ * - Border radius: rounded-lg (8px)
47
+ * - Spacing: 4px units (gap-4, p-4)
48
+ * - Colors: gray-{100-800}, blue-{500-600}, red-{500-600}
49
+ * - Shadows: shadow-sm on hover
50
+ * - Transitions: transition-all, duration-200
51
+ */
52
+
53
+ import { FilterRule, FilterLogic, FieldDefinition } from '../../types';
54
+ import { LogicToggle } from './LogicToggle';
55
+ import { RuleRow } from './RuleRow';
56
+ import { validateFilterRule } from '../../types';
57
+
58
+ export interface FilterRuleEditorProps {
59
+ /** Current filter rules */
60
+ rules: FilterRule[];
61
+
62
+ /** Callback when rules change */
63
+ onChange: (rules: FilterRule[]) => void;
64
+
65
+ /** Field registry for the entity type */
66
+ fieldRegistry: FieldDefinition[];
67
+
68
+ /** Filter logic (AND/OR) */
69
+ logic?: FilterLogic;
70
+
71
+ /** Callback when logic changes */
72
+ onLogicChange?: (logic: FilterLogic) => void;
73
+
74
+ /** Additional CSS classes */
75
+ className?: string;
76
+ }
77
+
78
+ export function FilterRuleEditor({
79
+ rules,
80
+ onChange,
81
+ fieldRegistry,
82
+ logic = 'AND',
83
+ onLogicChange,
84
+ className = '',
85
+ }: FilterRuleEditorProps) {
86
+ // Add a new empty rule
87
+ const handleAddRule = () => {
88
+ const newRule: FilterRule = {
89
+ field_path: '',
90
+ operator: 'eq' as any,
91
+ value: null,
92
+ };
93
+ onChange([...rules, newRule]);
94
+ };
95
+
96
+ // Update a specific rule
97
+ const handleUpdateRule = (index: number, updatedRule: FilterRule) => {
98
+ const newRules = [...rules];
99
+ newRules[index] = updatedRule;
100
+ onChange(newRules);
101
+ };
102
+
103
+ // Remove a specific rule
104
+ const handleRemoveRule = (index: number) => {
105
+ const newRules = rules.filter((_, i) => i !== index);
106
+ onChange(newRules);
107
+ };
108
+
109
+ // Validate rules
110
+ const ruleErrors = rules.map((rule) => validateFilterRule(rule));
111
+
112
+ return (
113
+ <div className={`flex flex-col gap-4 ${className}`}>
114
+ {/* Logic toggle */}
115
+ {onLogicChange && (
116
+ <LogicToggle logic={logic} onChange={onLogicChange} />
117
+ )}
118
+
119
+ {/* Empty state */}
120
+ {rules.length === 0 && (
121
+ <div className="flex flex-col items-center justify-center py-12 border-2 border-dashed border-gray-300 rounded-lg bg-gray-50">
122
+ <svg
123
+ className="w-12 h-12 text-gray-400 mb-3"
124
+ fill="none"
125
+ viewBox="0 0 24 24"
126
+ stroke="currentColor"
127
+ >
128
+ <path
129
+ strokeLinecap="round"
130
+ strokeLinejoin="round"
131
+ strokeWidth={2}
132
+ d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
133
+ />
134
+ </svg>
135
+ <p className="text-sm text-gray-600 mb-4">No filter rules yet</p>
136
+ <button
137
+ type="button"
138
+ onClick={handleAddRule}
139
+ className="
140
+ inline-flex items-center gap-2
141
+ px-4 py-2
142
+ text-sm font-medium text-white
143
+ bg-blue-600 hover:bg-blue-700
144
+ rounded-md
145
+ focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
146
+ transition-colors
147
+ "
148
+ >
149
+ <span className="text-lg">+</span>
150
+ Add First Rule
151
+ </button>
152
+ </div>
153
+ )}
154
+
155
+ {/* Rule list */}
156
+ {rules.length > 0 && (
157
+ <div className="flex flex-col gap-3">
158
+ {rules.map((rule, index) => (
159
+ <div key={index}>
160
+ <RuleRow
161
+ rule={rule}
162
+ onChange={(updatedRule) => handleUpdateRule(index, updatedRule)}
163
+ onRemove={() => handleRemoveRule(index)}
164
+ fieldRegistry={fieldRegistry}
165
+ />
166
+
167
+ {/* Logic connector between rules */}
168
+ {index < rules.length - 1 && (
169
+ <div className="flex items-center justify-center py-2">
170
+ <span className="px-3 py-1 text-xs font-semibold text-gray-700 bg-gray-100 border border-gray-300 rounded-full">
171
+ {logic}
172
+ </span>
173
+ </div>
174
+ )}
175
+
176
+ {/* Validation errors */}
177
+ {ruleErrors[index].length > 0 && (
178
+ <div className="mt-2 px-4 py-2 bg-red-50 border border-red-200 rounded text-sm text-red-700">
179
+ {ruleErrors[index].map((error, i) => (
180
+ <div key={i}>{error}</div>
181
+ ))}
182
+ </div>
183
+ )}
184
+ </div>
185
+ ))}
186
+ </div>
187
+ )}
188
+
189
+ {/* Add rule button (when rules exist) */}
190
+ {rules.length > 0 && (
191
+ <button
192
+ type="button"
193
+ onClick={handleAddRule}
194
+ className="
195
+ w-full
196
+ flex items-center justify-center gap-2
197
+ px-4 py-3
198
+ text-sm font-medium text-blue-600
199
+ bg-white hover:bg-blue-50
200
+ border-2 border-dashed border-blue-300 hover:border-blue-400
201
+ rounded-lg
202
+ focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
203
+ transition-colors
204
+ "
205
+ aria-label={`Add rule ${rules.length + 1}`}
206
+ >
207
+ <span className="text-xl">+</span>
208
+ Add Rule
209
+ </button>
210
+ )}
211
+
212
+ {/* Rule count and summary */}
213
+ {rules.length > 0 && (
214
+ <div className="flex items-center justify-between text-xs text-gray-500">
215
+ <span>
216
+ {rules.length} {rules.length === 1 ? 'rule' : 'rules'}
217
+ </span>
218
+ <span>
219
+ {logic === 'AND' ? 'All rules must match' : 'Any rule can match'}
220
+ </span>
221
+ </div>
222
+ )}
223
+ </div>
224
+ );
225
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * LogicToggle Component
3
+ *
4
+ * Toggle between AND/OR filter logic.
5
+ *
6
+ * Design Goal:
7
+ * Users need to switch between requiring all rules to match (AND)
8
+ * or any rule to match (OR).
9
+ *
10
+ * Visual Hierarchy Rationale:
11
+ * - Radio button pattern is familiar and clearly shows mutually exclusive options
12
+ * - Horizontal layout keeps it compact
13
+ * - Blue accent color indicates selected state
14
+ * - Label explains the logic clearly
15
+ *
16
+ * Interaction Notes:
17
+ * - Click either option to change logic
18
+ * - Visual feedback on hover and focus
19
+ * - Keyboard accessible (Tab to focus, Arrow keys to change)
20
+ */
21
+
22
+ import { FilterLogic } from '../../types';
23
+
24
+ interface LogicToggleProps {
25
+ logic: FilterLogic;
26
+ onChange: (logic: FilterLogic) => void;
27
+ className?: string;
28
+ }
29
+
30
+ export function LogicToggle({ logic, onChange, className = '' }: LogicToggleProps) {
31
+ return (
32
+ <div className={`flex items-center gap-4 ${className}`}>
33
+ <span className="text-sm font-medium text-gray-700">Logic:</span>
34
+
35
+ <label className="flex items-center gap-2 cursor-pointer">
36
+ <input
37
+ type="radio"
38
+ name="filter-logic"
39
+ value="AND"
40
+ checked={logic === 'AND'}
41
+ onChange={(e) => onChange(e.target.value as FilterLogic)}
42
+ className="w-4 h-4 text-blue-600 border-gray-300 focus:ring-blue-500"
43
+ />
44
+ <span className="text-sm text-gray-700">
45
+ AND <span className="text-xs text-gray-500">(all must match)</span>
46
+ </span>
47
+ </label>
48
+
49
+ <label className="flex items-center gap-2 cursor-pointer">
50
+ <input
51
+ type="radio"
52
+ name="filter-logic"
53
+ value="OR"
54
+ checked={logic === 'OR'}
55
+ onChange={(e) => onChange(e.target.value as FilterLogic)}
56
+ className="w-4 h-4 text-blue-600 border-gray-300 focus:ring-blue-500"
57
+ />
58
+ <span className="text-sm text-gray-700">
59
+ OR <span className="text-xs text-gray-500">(any can match)</span>
60
+ </span>
61
+ </label>
62
+ </div>
63
+ );
64
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * OperatorSelector Component
3
+ *
4
+ * Dropdown for selecting an operator, filtered by field type.
5
+ *
6
+ * Design Goal:
7
+ * Show only valid operators for the selected field type. For example,
8
+ * numeric fields show comparison operators (>, <, >=, <=) while string
9
+ * fields show text operators (contains, starts with, etc.).
10
+ *
11
+ * Visual Hierarchy Rationale:
12
+ * - Labeled clearly as "Operator"
13
+ * - Human-readable labels (not technical symbols)
14
+ * - Full-width for alignment with other inputs
15
+ *
16
+ * Interaction Notes:
17
+ * - Only enabled when field is selected
18
+ * - Shows operators valid for current field type
19
+ * - Resets to first valid operator when field changes
20
+ *
21
+ * Accessibility Considerations:
22
+ * - Disabled state clearly indicated
23
+ * - Proper label association
24
+ * - Focus visible on keyboard navigation
25
+ */
26
+
27
+ import { Operator, FieldDefinition } from '../../types';
28
+ import { OPERATOR_LABELS } from './constants';
29
+
30
+ interface OperatorSelectorProps {
31
+ operators: Operator[];
32
+ value: Operator | '';
33
+ onChange: (operator: Operator) => void;
34
+ disabled?: boolean;
35
+ error?: string;
36
+ className?: string;
37
+ }
38
+
39
+ export function OperatorSelector({
40
+ operators,
41
+ value,
42
+ onChange,
43
+ disabled = false,
44
+ error,
45
+ className = '',
46
+ }: OperatorSelectorProps) {
47
+ return (
48
+ <div className={`flex flex-col gap-1 ${className}`}>
49
+ <label htmlFor="operator-selector" className="text-xs font-medium text-gray-700">
50
+ Operator
51
+ </label>
52
+ <select
53
+ id="operator-selector"
54
+ value={value}
55
+ onChange={(e) => onChange(e.target.value as Operator)}
56
+ disabled={disabled}
57
+ className={`
58
+ w-full px-3 py-2 text-sm border rounded-md
59
+ bg-white
60
+ focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
61
+ disabled:bg-gray-100 disabled:cursor-not-allowed disabled:text-gray-500
62
+ ${error ? 'border-red-500' : 'border-gray-300'}
63
+ `}
64
+ >
65
+ <option value="">Select operator...</option>
66
+ {operators.map((op) => (
67
+ <option key={op} value={op}>
68
+ {OPERATOR_LABELS[op]}
69
+ </option>
70
+ ))}
71
+ </select>
72
+ {error && <span className="text-xs text-red-600">{error}</span>}
73
+ </div>
74
+ );
75
+ }
@@ -0,0 +1,291 @@
1
+ # FilterRuleEditor Component
2
+
3
+ **BRUTALLY GENERIC** filter rule builder that works with any entity type.
4
+
5
+ ## Overview
6
+
7
+ The FilterRuleEditor is a visual rule builder for creating complex filter conditions. It dynamically adapts to any entity type (investors, recipes, contacts, etc.) based on the field registry provided.
8
+
9
+ ## Features
10
+
11
+ - **Dynamic Field Selection** - Fields grouped by category from field registry
12
+ - **Type-Aware Operators** - Operators filtered by field type
13
+ - **Adaptive Value Input** - Input component changes based on field type + operator:
14
+ - String fields → Text input or dropdown (if choices)
15
+ - Number fields → Number input with min/max
16
+ - Date fields → Date picker
17
+ - Boolean fields → Checkbox
18
+ - Choice fields → Single or multi-select
19
+ - **AND/OR Logic** - Toggle between requiring all rules or any rule to match
20
+ - **Real-time Validation** - Validates rules as they're built
21
+ - **Add/Remove Rules** - Unlimited rules with individual delete buttons
22
+
23
+ ## Usage
24
+
25
+ ```tsx
26
+ import { FilterRuleEditor } from '@simpli/funnels';
27
+ import { useState } from 'react';
28
+
29
+ function MyComponent() {
30
+ const [rules, setRules] = useState<FilterRule[]>([]);
31
+ const [logic, setLogic] = useState<FilterLogic>('AND');
32
+
33
+ return (
34
+ <FilterRuleEditor
35
+ rules={rules}
36
+ onChange={setRules}
37
+ fieldRegistry={myFieldRegistry}
38
+ logic={logic}
39
+ onLogicChange={setLogic}
40
+ />
41
+ );
42
+ }
43
+ ```
44
+
45
+ ## Example: Investor Filters
46
+
47
+ ```tsx
48
+ const investorFields: FieldDefinition[] = [
49
+ {
50
+ name: 'firm.stage',
51
+ label: 'Firm Stage',
52
+ type: 'string',
53
+ category: 'Investment',
54
+ operators: ['eq', 'in'],
55
+ constraints: {
56
+ choices: ['Seed', 'Series A', 'Series B'],
57
+ },
58
+ },
59
+ {
60
+ name: 'firm.check_size_min',
61
+ label: 'Check Size Min',
62
+ type: 'number',
63
+ category: 'Investment',
64
+ operators: ['gte', 'lte'],
65
+ },
66
+ {
67
+ name: 'metrics.icp_score',
68
+ label: 'ICP Score',
69
+ type: 'number',
70
+ category: 'Metrics',
71
+ operators: ['gte', 'lte'],
72
+ constraints: { min_value: 0, max_value: 100 },
73
+ },
74
+ ];
75
+
76
+ // Creates rules like:
77
+ // - Firm stage is one of [Series A, Series B]
78
+ // - Check size min >= $500,000
79
+ // - ICP score >= 70
80
+ ```
81
+
82
+ ## Example: Recipe Filters
83
+
84
+ ```tsx
85
+ const recipeFields: FieldDefinition[] = [
86
+ {
87
+ name: 'cuisine',
88
+ label: 'Cuisine',
89
+ type: 'string',
90
+ category: 'Classification',
91
+ operators: ['eq', 'in'],
92
+ constraints: {
93
+ choices: ['Italian', 'French', 'Japanese'],
94
+ },
95
+ },
96
+ {
97
+ name: 'difficulty',
98
+ label: 'Difficulty',
99
+ type: 'string',
100
+ category: 'Classification',
101
+ operators: ['eq'],
102
+ constraints: {
103
+ choices: ['Easy', 'Medium', 'Hard'],
104
+ },
105
+ },
106
+ {
107
+ name: 'prep_time_minutes',
108
+ label: 'Prep Time',
109
+ type: 'number',
110
+ category: 'Time',
111
+ operators: ['lte'],
112
+ },
113
+ {
114
+ name: 'is_vegetarian',
115
+ label: 'Vegetarian',
116
+ type: 'boolean',
117
+ category: 'Dietary',
118
+ operators: ['is_true', 'is_false'],
119
+ },
120
+ ];
121
+
122
+ // Creates rules like:
123
+ // - Cuisine is one of [Italian, French]
124
+ // - Difficulty equals Easy
125
+ // - Prep time <= 30 minutes
126
+ // - Vegetarian is true
127
+ ```
128
+
129
+ ## Field Registry
130
+
131
+ The component is completely driven by the field registry you provide. Each field definition specifies:
132
+
133
+ ```typescript
134
+ interface FieldDefinition {
135
+ name: string; // Field path (e.g., 'firm.stage')
136
+ label: string; // Human-readable label
137
+ type: FieldType; // 'string' | 'number' | 'boolean' | 'date' | 'tag'
138
+ category?: string; // Grouping category
139
+ operators: Operator[]; // Valid operators for this field
140
+ constraints?: {
141
+ choices?: any[]; // For dropdowns
142
+ min_value?: number; // For number/date inputs
143
+ max_value?: number; // For number/date inputs
144
+ };
145
+ }
146
+ ```
147
+
148
+ ## Component Architecture
149
+
150
+ ```
151
+ FilterRuleEditor/
152
+ ├── FilterRuleEditor.tsx # Main container
153
+ ├── LogicToggle.tsx # AND/OR switch
154
+ ├── RuleRow.tsx # Single rule row
155
+ ├── FieldSelector.tsx # Field dropdown (grouped)
156
+ ├── OperatorSelector.tsx # Operator dropdown (filtered)
157
+ ├── ValueInputs/
158
+ │ ├── TextValueInput.tsx
159
+ │ ├── NumberValueInput.tsx
160
+ │ ├── DateValueInput.tsx
161
+ │ ├── BooleanValueInput.tsx
162
+ │ ├── ChoiceValueInput.tsx
163
+ │ └── MultiChoiceValueInput.tsx
164
+ └── constants.ts # Operator labels
165
+ ```
166
+
167
+ ## Value Input Logic
168
+
169
+ The component automatically selects the appropriate input based on field type and operator:
170
+
171
+ | Field Type | Operator | Input Type |
172
+ |------------|----------|------------|
173
+ | `string` (with choices) | `eq` | Single-select dropdown |
174
+ | `string` (with choices) | `in` | Multi-select with tags |
175
+ | `string` | `contains` | Text input |
176
+ | `number` | `gte`, `lte`, etc. | Number input |
177
+ | `date` | `gte`, `lte`, etc. | Date picker |
178
+ | `boolean` | `eq` | Checkbox |
179
+ | `tag` | `has_tag` | Text input |
180
+
181
+ Operators like `isnull`, `isnotnull`, `is_true`, `is_false` don't show a value input (they don't need one).
182
+
183
+ ## Validation
184
+
185
+ Rules are validated in real-time using `validateFilterRule()`:
186
+
187
+ - Field path is required
188
+ - Operator is required
189
+ - Value is required (except for null-check operators)
190
+
191
+ Validation errors appear below each rule.
192
+
193
+ ## Styling
194
+
195
+ Uses Tailwind CSS utility classes:
196
+ - Border radius: `rounded-lg` (8px)
197
+ - Spacing: 4px units (`gap-4`, `p-4`)
198
+ - Colors: `gray-{100-800}`, `blue-{500-600}`, `red-{500-600}`
199
+ - Shadows: `shadow-sm` on hover
200
+ - Transitions: `transition-colors`
201
+
202
+ Fully responsive:
203
+ - Desktop (lg+): 3-column grid
204
+ - Tablet (md): 2-column grid
205
+ - Mobile: Stacked vertically
206
+
207
+ ## Tests
208
+
209
+ See `FilterRuleEditor.test.tsx` for comprehensive tests covering:
210
+ - Rule validation
211
+ - Investor domain filters
212
+ - Recipe domain filters
213
+ - Complex multi-rule scenarios
214
+ - Field registry grouping
215
+ - Operator filtering
216
+ - Value constraints
217
+
218
+ All tests pass. Run with:
219
+
220
+ ```bash
221
+ npm run test
222
+ ```
223
+
224
+ ## Storybook
225
+
226
+ See `FilterRuleEditor.stories.tsx` for interactive examples:
227
+ - Empty state
228
+ - Investor filters (Series A/B, check size, ICP score)
229
+ - Recipe filters (cuisine, difficulty, prep time, dietary)
230
+ - OR logic example
231
+ - Complex filters with many rules
232
+ - Interactive playground
233
+
234
+ ## Accessibility
235
+
236
+ - Proper ARIA labels for all inputs
237
+ - Keyboard navigation throughout
238
+ - Focus management for add/remove
239
+ - Screen reader announces rule count
240
+ - Error messages associated with fields
241
+ - Native form controls (select, input, checkbox)
242
+
243
+ ## Performance
244
+
245
+ - Efficient re-renders (only affected components update)
246
+ - Debounced validation
247
+ - Memoized field grouping
248
+ - Lightweight dependencies
249
+
250
+ ## Integration
251
+
252
+ Works seamlessly with the Funnel Engine:
253
+
254
+ ```tsx
255
+ const funnel: Funnel = {
256
+ id: 'my-funnel',
257
+ name: 'Qualified Investors',
258
+ stages: [
259
+ {
260
+ id: 'stage-1',
261
+ name: 'Filter Stage',
262
+ filter_logic: logic, // From FilterRuleEditor
263
+ rules: rules, // From FilterRuleEditor
264
+ match_action: 'output',
265
+ no_match_action: 'exclude',
266
+ },
267
+ ],
268
+ };
269
+
270
+ const engine = new FunnelEngine();
271
+ const results = await engine.run(funnel, entities);
272
+ ```
273
+
274
+ ## Future Enhancements
275
+
276
+ Potential additions:
277
+ - Drag-and-drop rule reordering
278
+ - Rule grouping (nested AND/OR)
279
+ - Rule templates/presets
280
+ - Import/export rules as JSON
281
+ - Visual query builder mode
282
+ - Advanced operators (regex, geo-distance)
283
+ - Custom value input components
284
+ - Rule duplication
285
+
286
+ ## Related Components
287
+
288
+ - `FunnelStageBuilder` - Build complete funnel stages
289
+ - `FunnelPreview` - Preview funnel results
290
+ - `FunnelVisualFlow` - Visualize funnel flow
291
+ - `FunnelCard` - Display funnel summary