@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,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
+ }