@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,354 @@
|
|
|
1
|
+
# FilterRuleEditor Architecture
|
|
2
|
+
|
|
3
|
+
## Component Hierarchy
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
FilterRuleEditor
|
|
7
|
+
├─ LogicToggle (AND/OR switch)
|
|
8
|
+
├─ Empty State (when no rules)
|
|
9
|
+
│ └─ Add First Rule button
|
|
10
|
+
└─ Rule List (when rules exist)
|
|
11
|
+
├─ RuleRow (for each rule)
|
|
12
|
+
│ ├─ FieldSelector (grouped dropdown)
|
|
13
|
+
│ ├─ OperatorSelector (filtered by field type)
|
|
14
|
+
│ └─ ValueInput (dynamic based on field + operator)
|
|
15
|
+
│ ├─ TextValueInput
|
|
16
|
+
│ ├─ NumberValueInput
|
|
17
|
+
│ ├─ DateValueInput
|
|
18
|
+
│ ├─ BooleanValueInput
|
|
19
|
+
│ ├─ ChoiceValueInput
|
|
20
|
+
│ └─ MultiChoiceValueInput
|
|
21
|
+
│ └─ Delete button
|
|
22
|
+
├─ Logic Connector (AND/OR pill between rules)
|
|
23
|
+
└─ Validation Errors (per rule)
|
|
24
|
+
└─ Add Rule button
|
|
25
|
+
└─ Rule Summary (count + logic description)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Data Flow
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
32
|
+
│ FilterRuleEditor │
|
|
33
|
+
│ │
|
|
34
|
+
│ State: │
|
|
35
|
+
│ - rules: FilterRule[] │
|
|
36
|
+
│ - logic: 'AND' | 'OR' │
|
|
37
|
+
│ │
|
|
38
|
+
│ Props: │
|
|
39
|
+
│ - fieldRegistry: FieldDefinition[] ← Defines available fields │
|
|
40
|
+
│ - onChange: (rules) => void ← Parent updates state │
|
|
41
|
+
│ - onLogicChange: (logic) => void ← Parent updates logic │
|
|
42
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
43
|
+
│
|
|
44
|
+
│ passes fieldRegistry + rule
|
|
45
|
+
▼
|
|
46
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
47
|
+
│ RuleRow │
|
|
48
|
+
│ │
|
|
49
|
+
│ Responsibilities: │
|
|
50
|
+
│ - Render field selector │
|
|
51
|
+
│ - Filter operators by field type │
|
|
52
|
+
│ - Select appropriate value input │
|
|
53
|
+
│ - Handle rule updates │
|
|
54
|
+
│ - Handle rule deletion │
|
|
55
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
56
|
+
│ │ │
|
|
57
|
+
│ │ │
|
|
58
|
+
▼ ▼ ▼
|
|
59
|
+
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
60
|
+
│ FieldSelector │ │OperatorSelector │ │ ValueInput │
|
|
61
|
+
│ │ │ │ │ (Factory) │
|
|
62
|
+
│ Groups fields │ │ Filters ops by │ │ │
|
|
63
|
+
│ by category │ │ field type │ │ Selects input │
|
|
64
|
+
│ │ │ │ │ based on: │
|
|
65
|
+
│ │ │ │ │ - Field type │
|
|
66
|
+
│ │ │ │ │ - Operator │
|
|
67
|
+
│ │ │ │ │ - Constraints │
|
|
68
|
+
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Value Input Selection Logic
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
┌─────────────────────────────────────────────────────────┐
|
|
75
|
+
│ RuleRow.renderValueInput() │
|
|
76
|
+
│ │
|
|
77
|
+
│ 1. Check if operator needs value │
|
|
78
|
+
│ ├─ isnull, isnotnull, is_true, is_false → null │
|
|
79
|
+
│ └─ All others → continue │
|
|
80
|
+
│ │
|
|
81
|
+
│ 2. Check if multi-value operator │
|
|
82
|
+
│ ├─ in, not_in, has_any, has_all → MultiChoiceValue │
|
|
83
|
+
│ └─ Single value → continue │
|
|
84
|
+
│ │
|
|
85
|
+
│ 3. Switch on field type │
|
|
86
|
+
│ ├─ string │
|
|
87
|
+
│ │ ├─ Has choices + operator=eq → ChoiceValue │
|
|
88
|
+
│ │ └─ No choices → TextValue │
|
|
89
|
+
│ ├─ number → NumberValue │
|
|
90
|
+
│ ├─ date → DateValue │
|
|
91
|
+
│ ├─ boolean → BooleanValue │
|
|
92
|
+
│ ├─ tag → TextValue (for tag name) │
|
|
93
|
+
│ └─ default → TextValue │
|
|
94
|
+
└─────────────────────────────────────────────────────────┘
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## State Management
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
Parent Component (e.g., FunnelStageBuilder)
|
|
101
|
+
│
|
|
102
|
+
├─ useState: rules = []
|
|
103
|
+
├─ useState: logic = 'AND'
|
|
104
|
+
│
|
|
105
|
+
└─ <FilterRuleEditor
|
|
106
|
+
rules={rules}
|
|
107
|
+
onChange={setRules}
|
|
108
|
+
logic={logic}
|
|
109
|
+
onLogicChange={setLogic}
|
|
110
|
+
fieldRegistry={investorFields}
|
|
111
|
+
/>
|
|
112
|
+
|
|
113
|
+
User Actions:
|
|
114
|
+
├─ Add Rule → onChange([...rules, newRule])
|
|
115
|
+
├─ Update Rule → onChange(rules.map((r, i) => i === index ? updated : r))
|
|
116
|
+
├─ Remove Rule → onChange(rules.filter((r, i) => i !== index))
|
|
117
|
+
└─ Change Logic → onLogicChange('AND' | 'OR')
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Field Registry Structure
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
FieldRegistry = FieldDefinition[]
|
|
124
|
+
|
|
125
|
+
FieldDefinition {
|
|
126
|
+
name: 'firm.stage' ← Dot-notation path
|
|
127
|
+
label: 'Firm Stage' ← Display label
|
|
128
|
+
type: 'string' ← Data type
|
|
129
|
+
category: 'Investment' ← Grouping
|
|
130
|
+
operators: ['eq', 'in'] ← Valid operators
|
|
131
|
+
constraints: { ← Optional constraints
|
|
132
|
+
choices: ['Seed', 'A', 'B'] ← For dropdowns
|
|
133
|
+
min_value: 0 ← For number inputs
|
|
134
|
+
max_value: 100 ← For number inputs
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
┌─────────────────────────────────────────────────┐
|
|
139
|
+
│ Field Registry → FieldSelector │
|
|
140
|
+
│ │
|
|
141
|
+
│ Groups by category: │
|
|
142
|
+
│ ┌─────────────────────────┐ │
|
|
143
|
+
│ │ Profile ▶ │ │
|
|
144
|
+
│ │ - Name │ │
|
|
145
|
+
│ │ - Email │ │
|
|
146
|
+
│ │ Investment ▶ │ │
|
|
147
|
+
│ │ - Firm Stage │ ← field.category │
|
|
148
|
+
│ │ - Check Size │ │
|
|
149
|
+
│ │ Metrics ▶ │ │
|
|
150
|
+
│ │ - ICP Score │ │
|
|
151
|
+
│ └─────────────────────────┘ │
|
|
152
|
+
└─────────────────────────────────────────────────┘
|
|
153
|
+
|
|
154
|
+
┌─────────────────────────────────────────────────┐
|
|
155
|
+
│ Field Type → OperatorSelector │
|
|
156
|
+
│ │
|
|
157
|
+
│ Filters operators: │
|
|
158
|
+
│ string → [eq, ne, contains, in] │
|
|
159
|
+
│ number → [eq, gt, lt, gte, lte] │
|
|
160
|
+
│ boolean → [eq, is_true, is_false] │
|
|
161
|
+
│ date → [eq, gt, lt, gte, lte] │
|
|
162
|
+
│ tag → [has_tag, not_has_tag] │
|
|
163
|
+
└─────────────────────────────────────────────────┘
|
|
164
|
+
|
|
165
|
+
┌─────────────────────────────────────────────────┐
|
|
166
|
+
│ Constraints → ValueInput │
|
|
167
|
+
│ │
|
|
168
|
+
│ constraints.choices → ChoiceValue dropdown │
|
|
169
|
+
│ constraints.min_value → NumberValue min prop │
|
|
170
|
+
│ constraints.max_value → NumberValue max prop │
|
|
171
|
+
└─────────────────────────────────────────────────┘
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Validation Flow
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
FilterRuleEditor
|
|
178
|
+
│
|
|
179
|
+
├─ For each rule:
|
|
180
|
+
│ │
|
|
181
|
+
│ ├─ validateFilterRule(rule)
|
|
182
|
+
│ │ ├─ Check field_path exists
|
|
183
|
+
│ │ ├─ Check operator exists
|
|
184
|
+
│ │ └─ Check value (if required for operator)
|
|
185
|
+
│ │
|
|
186
|
+
│ └─ Collect errors[]
|
|
187
|
+
│
|
|
188
|
+
└─ Display errors below each rule
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Example: User Creates Investor Filter
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
1. User clicks "Add Rule"
|
|
195
|
+
→ onChange([...rules, { field_path: '', operator: 'eq', value: null }])
|
|
196
|
+
|
|
197
|
+
2. User selects field: "Firm Stage"
|
|
198
|
+
→ rule.field_path = 'firm.stage'
|
|
199
|
+
→ FieldSelector finds field definition
|
|
200
|
+
→ OperatorSelector shows ['eq', 'ne', 'in']
|
|
201
|
+
→ Auto-select first operator: 'eq'
|
|
202
|
+
→ ValueInput sees: type='string', constraints.choices=['Seed','A','B']
|
|
203
|
+
→ Renders: ChoiceValueInput
|
|
204
|
+
|
|
205
|
+
3. User selects operator: "is one of"
|
|
206
|
+
→ rule.operator = 'in'
|
|
207
|
+
→ ValueInput detects multi-value operator
|
|
208
|
+
→ Switches to: MultiChoiceValueInput
|
|
209
|
+
|
|
210
|
+
4. User selects values: ['Series A', 'Series B']
|
|
211
|
+
→ rule.value = ['Series A', 'Series B']
|
|
212
|
+
→ MultiChoiceValueInput shows tags
|
|
213
|
+
→ onChange(updatedRules)
|
|
214
|
+
|
|
215
|
+
5. Parent receives updated rules:
|
|
216
|
+
[{
|
|
217
|
+
field_path: 'firm.stage',
|
|
218
|
+
operator: 'in',
|
|
219
|
+
value: ['Series A', 'Series B']
|
|
220
|
+
}]
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Performance Considerations
|
|
224
|
+
|
|
225
|
+
### Memoization
|
|
226
|
+
- Field grouping is computed once per field registry change
|
|
227
|
+
- Operator filtering happens per field selection (not every render)
|
|
228
|
+
- Value input selection is pure function (no side effects)
|
|
229
|
+
|
|
230
|
+
### Re-render Optimization
|
|
231
|
+
- Each RuleRow is independent (updating one doesn't re-render others)
|
|
232
|
+
- LogicToggle is separate component (doesn't re-render with rule changes)
|
|
233
|
+
- Value inputs are lazy-loaded (only rendered when needed)
|
|
234
|
+
|
|
235
|
+
### Bundle Size
|
|
236
|
+
- Zero external dependencies (beyond React)
|
|
237
|
+
- Lightweight Tailwind classes (purged in production)
|
|
238
|
+
- Tree-shakeable exports
|
|
239
|
+
|
|
240
|
+
## Testing Strategy
|
|
241
|
+
|
|
242
|
+
### Unit Tests (FilterRuleEditor.test.tsx)
|
|
243
|
+
- Rule validation
|
|
244
|
+
- Field registry grouping
|
|
245
|
+
- Operator filtering
|
|
246
|
+
- Value constraints
|
|
247
|
+
- Multi-domain testing (investors, recipes)
|
|
248
|
+
|
|
249
|
+
### Integration Tests
|
|
250
|
+
- Rule CRUD operations
|
|
251
|
+
- Logic toggle behavior
|
|
252
|
+
- Field → Operator → Value cascade
|
|
253
|
+
- Error display
|
|
254
|
+
|
|
255
|
+
### Visual Tests (Storybook)
|
|
256
|
+
- Empty state
|
|
257
|
+
- Single rule
|
|
258
|
+
- Multiple rules
|
|
259
|
+
- Complex filters
|
|
260
|
+
- Different domains
|
|
261
|
+
|
|
262
|
+
## Accessibility
|
|
263
|
+
|
|
264
|
+
### Keyboard Navigation
|
|
265
|
+
```
|
|
266
|
+
Tab Order:
|
|
267
|
+
1. Logic Toggle (radio buttons)
|
|
268
|
+
2. Rule 1 Field Selector (select)
|
|
269
|
+
3. Rule 1 Operator Selector (select)
|
|
270
|
+
4. Rule 1 Value Input (varies)
|
|
271
|
+
5. Rule 1 Delete Button
|
|
272
|
+
6. [Logic Connector - not focusable]
|
|
273
|
+
7. Rule 2 Field Selector
|
|
274
|
+
8. ...
|
|
275
|
+
9. Add Rule Button
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Screen Reader
|
|
279
|
+
- Field labels announced
|
|
280
|
+
- Selected values announced
|
|
281
|
+
- Error messages announced
|
|
282
|
+
- Rule count announced
|
|
283
|
+
- Logic mode announced
|
|
284
|
+
|
|
285
|
+
### Focus Management
|
|
286
|
+
- Adding rule → focus new field selector
|
|
287
|
+
- Removing rule → focus previous rule or add button
|
|
288
|
+
- Submitting → focus first error
|
|
289
|
+
|
|
290
|
+
## Extension Points
|
|
291
|
+
|
|
292
|
+
### Custom Value Inputs
|
|
293
|
+
```tsx
|
|
294
|
+
// Add new value input type
|
|
295
|
+
const CustomValueInput = ({ value, onChange }) => {
|
|
296
|
+
// Your custom input logic
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// Use in RuleRow
|
|
300
|
+
if (fieldType === 'custom') {
|
|
301
|
+
return <CustomValueInput value={rule.value} onChange={handleValueChange} />;
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Custom Operators
|
|
306
|
+
```typescript
|
|
307
|
+
// Add to types/index.ts
|
|
308
|
+
export type Operator =
|
|
309
|
+
| 'eq'
|
|
310
|
+
| 'custom_op'; // Your custom operator
|
|
311
|
+
|
|
312
|
+
// Add to constants.ts
|
|
313
|
+
export const OPERATOR_LABELS: Record<Operator, string> = {
|
|
314
|
+
eq: 'equals',
|
|
315
|
+
custom_op: 'my custom operator',
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// Implement in core/operators.ts
|
|
319
|
+
export function applyOperator(operator: Operator, actual: any, expected: any): boolean {
|
|
320
|
+
if (operator === 'custom_op') {
|
|
321
|
+
return customLogic(actual, expected);
|
|
322
|
+
}
|
|
323
|
+
// ...
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Custom Field Types
|
|
328
|
+
```typescript
|
|
329
|
+
// Add to types/index.ts
|
|
330
|
+
export type FieldType =
|
|
331
|
+
| 'string'
|
|
332
|
+
| 'custom_type'; // Your custom type
|
|
333
|
+
|
|
334
|
+
// Add to getValidOperators()
|
|
335
|
+
export function getValidOperators(fieldType: FieldType): ValidOperators {
|
|
336
|
+
if (fieldType === 'custom_type') {
|
|
337
|
+
return ['custom_op'];
|
|
338
|
+
}
|
|
339
|
+
// ...
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Handle in RuleRow.renderValueInput()
|
|
343
|
+
if (type === 'custom_type') {
|
|
344
|
+
return <CustomValueInput />;
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## Related Documentation
|
|
349
|
+
|
|
350
|
+
- [Main Package README](../../README.md)
|
|
351
|
+
- [Type System](../../types/index.ts)
|
|
352
|
+
- [Funnel Engine](../../core/engine.ts)
|
|
353
|
+
- [Field Resolution](../../core/field-resolver.ts)
|
|
354
|
+
- [Rule Evaluation](../../core/evaluator.ts)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FieldSelector Component
|
|
3
|
+
*
|
|
4
|
+
* Dropdown for selecting a field from the field registry, grouped by category.
|
|
5
|
+
*
|
|
6
|
+
* Design Goal:
|
|
7
|
+
* Users need to select from potentially dozens of fields. Grouping by category
|
|
8
|
+
* (Profile, Investment, Metrics, etc.) makes fields easier to find.
|
|
9
|
+
*
|
|
10
|
+
* Visual Hierarchy Rationale:
|
|
11
|
+
* - Categories shown with indentation for visual hierarchy
|
|
12
|
+
* - Field names indented under categories
|
|
13
|
+
* - Selected field shown in dropdown button
|
|
14
|
+
* - Full-width for consistency with other inputs
|
|
15
|
+
*
|
|
16
|
+
* Interaction Notes:
|
|
17
|
+
* - Click to open dropdown
|
|
18
|
+
* - Category labels are not selectable (disabled options)
|
|
19
|
+
* - Field selection updates the rule
|
|
20
|
+
* - Keyboard navigation supported
|
|
21
|
+
*
|
|
22
|
+
* Responsive Behavior:
|
|
23
|
+
* - Full width on all screen sizes
|
|
24
|
+
* - Dropdown scrolls if too many options
|
|
25
|
+
*
|
|
26
|
+
* Accessibility Considerations:
|
|
27
|
+
* - Proper label association
|
|
28
|
+
* - Category grouping preserved in select
|
|
29
|
+
* - Focus visible on keyboard navigation
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { FieldDefinition } from '../../types';
|
|
33
|
+
|
|
34
|
+
interface FieldSelectorProps {
|
|
35
|
+
fields: FieldDefinition[];
|
|
36
|
+
value: string;
|
|
37
|
+
onChange: (fieldName: string) => void;
|
|
38
|
+
error?: string;
|
|
39
|
+
className?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function FieldSelector({
|
|
43
|
+
fields,
|
|
44
|
+
value,
|
|
45
|
+
onChange,
|
|
46
|
+
error,
|
|
47
|
+
className = '',
|
|
48
|
+
}: FieldSelectorProps) {
|
|
49
|
+
// Group fields by category
|
|
50
|
+
const grouped = fields.reduce((acc, field) => {
|
|
51
|
+
const category = field.category || 'Other';
|
|
52
|
+
if (!acc[category]) {
|
|
53
|
+
acc[category] = [];
|
|
54
|
+
}
|
|
55
|
+
acc[category].push(field);
|
|
56
|
+
return acc;
|
|
57
|
+
}, {} as Record<string, FieldDefinition[]>);
|
|
58
|
+
|
|
59
|
+
const categories = Object.keys(grouped).sort();
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className={`flex flex-col gap-1 ${className}`}>
|
|
63
|
+
<label htmlFor="field-selector" className="text-xs font-medium text-gray-700">
|
|
64
|
+
Field
|
|
65
|
+
</label>
|
|
66
|
+
<select
|
|
67
|
+
id="field-selector"
|
|
68
|
+
value={value}
|
|
69
|
+
onChange={(e) => onChange(e.target.value)}
|
|
70
|
+
className={`
|
|
71
|
+
w-full px-3 py-2 text-sm border rounded-md
|
|
72
|
+
bg-white
|
|
73
|
+
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
|
|
74
|
+
${error ? 'border-red-500' : 'border-gray-300'}
|
|
75
|
+
`}
|
|
76
|
+
>
|
|
77
|
+
<option value="">Select a field...</option>
|
|
78
|
+
{categories.map((category) => (
|
|
79
|
+
<optgroup key={category} label={category}>
|
|
80
|
+
{grouped[category].map((field) => (
|
|
81
|
+
<option key={field.name} value={field.name}>
|
|
82
|
+
{field.label}
|
|
83
|
+
</option>
|
|
84
|
+
))}
|
|
85
|
+
</optgroup>
|
|
86
|
+
))}
|
|
87
|
+
</select>
|
|
88
|
+
{error && <span className="text-xs text-red-600">{error}</span>}
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|