@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,462 @@
1
+ /**
2
+ * FilterRuleEditor Storybook Stories
3
+ *
4
+ * Demonstrates the BRUTALLY GENERIC filter rule builder across different domains.
5
+ */
6
+
7
+ import { useState } from 'react';
8
+ import { FilterRuleEditor } from './FilterRuleEditor';
9
+ import { FilterRule, FilterLogic, FieldDefinition } from '../../types';
10
+
11
+ export default {
12
+ title: 'Components/FilterRuleEditor',
13
+ component: FilterRuleEditor,
14
+ };
15
+
16
+ // ============================================================================
17
+ // Investor Field Registry
18
+ // ============================================================================
19
+
20
+ const investorFields: FieldDefinition[] = [
21
+ {
22
+ name: 'name',
23
+ label: 'Name',
24
+ type: 'string',
25
+ category: 'Profile',
26
+ operators: ['eq', 'ne', 'contains', 'startswith', 'isnotnull'],
27
+ },
28
+ {
29
+ name: 'email',
30
+ label: 'Email',
31
+ type: 'string',
32
+ category: 'Profile',
33
+ operators: ['eq', 'contains', 'isnotnull'],
34
+ },
35
+ {
36
+ name: 'firm.name',
37
+ label: 'Firm Name',
38
+ type: 'string',
39
+ category: 'Investment',
40
+ operators: ['eq', 'contains', 'startswith'],
41
+ },
42
+ {
43
+ name: 'firm.stage',
44
+ label: 'Firm Stage',
45
+ type: 'string',
46
+ category: 'Investment',
47
+ operators: ['eq', 'ne', 'in'],
48
+ constraints: {
49
+ choices: ['Pre-Seed', 'Seed', 'Series A', 'Series B', 'Series C', 'Growth', 'Late Stage'],
50
+ },
51
+ },
52
+ {
53
+ name: 'firm.check_size_min',
54
+ label: 'Check Size Min ($)',
55
+ type: 'number',
56
+ category: 'Investment',
57
+ operators: ['eq', 'ne', 'gt', 'lt', 'gte', 'lte'],
58
+ constraints: { min_value: 0 },
59
+ },
60
+ {
61
+ name: 'firm.check_size_max',
62
+ label: 'Check Size Max ($)',
63
+ type: 'number',
64
+ category: 'Investment',
65
+ operators: ['eq', 'ne', 'gt', 'lt', 'gte', 'lte'],
66
+ constraints: { min_value: 0 },
67
+ },
68
+ {
69
+ name: 'metrics.icp_score',
70
+ label: 'ICP Score',
71
+ type: 'number',
72
+ category: 'Metrics',
73
+ operators: ['eq', 'ne', 'gt', 'lt', 'gte', 'lte'],
74
+ constraints: { min_value: 0, max_value: 100 },
75
+ },
76
+ {
77
+ name: 'metrics.aum_usd',
78
+ label: 'AUM (USD)',
79
+ type: 'number',
80
+ category: 'Metrics',
81
+ operators: ['eq', 'ne', 'gt', 'lt', 'gte', 'lte'],
82
+ constraints: { min_value: 0 },
83
+ },
84
+ {
85
+ name: 'tags',
86
+ label: 'Tags',
87
+ type: 'tag',
88
+ category: 'Metadata',
89
+ operators: ['has_tag', 'not_has_tag'],
90
+ },
91
+ {
92
+ name: 'last_contact_date',
93
+ label: 'Last Contact Date',
94
+ type: 'date',
95
+ category: 'Engagement',
96
+ operators: ['eq', 'ne', 'gt', 'lt', 'gte', 'lte'],
97
+ },
98
+ ];
99
+
100
+ // ============================================================================
101
+ // Recipe Field Registry
102
+ // ============================================================================
103
+
104
+ const recipeFields: FieldDefinition[] = [
105
+ {
106
+ name: 'title',
107
+ label: 'Recipe Title',
108
+ type: 'string',
109
+ category: 'Basic Info',
110
+ operators: ['eq', 'contains', 'startswith'],
111
+ },
112
+ {
113
+ name: 'description',
114
+ label: 'Description',
115
+ type: 'string',
116
+ category: 'Basic Info',
117
+ operators: ['contains'],
118
+ },
119
+ {
120
+ name: 'cuisine',
121
+ label: 'Cuisine',
122
+ type: 'string',
123
+ category: 'Classification',
124
+ operators: ['eq', 'in'],
125
+ constraints: {
126
+ choices: ['Italian', 'French', 'Chinese', 'Japanese', 'Mexican', 'Indian', 'Thai', 'Greek'],
127
+ },
128
+ },
129
+ {
130
+ name: 'difficulty',
131
+ label: 'Difficulty Level',
132
+ type: 'string',
133
+ category: 'Classification',
134
+ operators: ['eq', 'in'],
135
+ constraints: {
136
+ choices: ['Easy', 'Medium', 'Hard', 'Expert'],
137
+ },
138
+ },
139
+ {
140
+ name: 'prep_time_minutes',
141
+ label: 'Prep Time (min)',
142
+ type: 'number',
143
+ category: 'Time',
144
+ operators: ['eq', 'ne', 'gt', 'lt', 'gte', 'lte'],
145
+ constraints: { min_value: 0 },
146
+ },
147
+ {
148
+ name: 'cook_time_minutes',
149
+ label: 'Cook Time (min)',
150
+ type: 'number',
151
+ category: 'Time',
152
+ operators: ['eq', 'ne', 'gt', 'lt', 'gte', 'lte'],
153
+ constraints: { min_value: 0 },
154
+ },
155
+ {
156
+ name: 'servings',
157
+ label: 'Servings',
158
+ type: 'number',
159
+ category: 'Yield',
160
+ operators: ['eq', 'ne', 'gt', 'lt', 'gte', 'lte'],
161
+ constraints: { min_value: 1 },
162
+ },
163
+ {
164
+ name: 'is_vegetarian',
165
+ label: 'Vegetarian',
166
+ type: 'boolean',
167
+ category: 'Dietary',
168
+ operators: ['eq', 'is_true', 'is_false'],
169
+ },
170
+ {
171
+ name: 'is_vegan',
172
+ label: 'Vegan',
173
+ type: 'boolean',
174
+ category: 'Dietary',
175
+ operators: ['eq', 'is_true', 'is_false'],
176
+ },
177
+ {
178
+ name: 'is_gluten_free',
179
+ label: 'Gluten Free',
180
+ type: 'boolean',
181
+ category: 'Dietary',
182
+ operators: ['eq', 'is_true', 'is_false'],
183
+ },
184
+ {
185
+ name: 'created_at',
186
+ label: 'Created Date',
187
+ type: 'date',
188
+ category: 'Metadata',
189
+ operators: ['eq', 'ne', 'gt', 'lt', 'gte', 'lte'],
190
+ },
191
+ ];
192
+
193
+ // ============================================================================
194
+ // Stories
195
+ // ============================================================================
196
+
197
+ /**
198
+ * Empty state - no rules yet
199
+ */
200
+ export const Empty = () => {
201
+ const [rules, setRules] = useState<FilterRule[]>([]);
202
+ const [logic, setLogic] = useState<FilterLogic>('AND');
203
+
204
+ return (
205
+ <div className="max-w-4xl mx-auto p-8">
206
+ <h2 className="text-2xl font-bold mb-4">Empty Filter Editor</h2>
207
+ <FilterRuleEditor
208
+ rules={rules}
209
+ onChange={setRules}
210
+ fieldRegistry={investorFields}
211
+ logic={logic}
212
+ onLogicChange={setLogic}
213
+ />
214
+ <pre className="mt-4 p-4 bg-gray-100 rounded text-xs">
215
+ {JSON.stringify({ logic, rules }, null, 2)}
216
+ </pre>
217
+ </div>
218
+ );
219
+ };
220
+
221
+ /**
222
+ * Investor filters - find high-fit Series A investors
223
+ */
224
+ export const InvestorFilters = () => {
225
+ const [rules, setRules] = useState<FilterRule[]>([
226
+ {
227
+ field_path: 'firm.stage',
228
+ operator: 'in',
229
+ value: ['Series A', 'Series B'],
230
+ },
231
+ {
232
+ field_path: 'firm.check_size_min',
233
+ operator: 'gte',
234
+ value: 500000,
235
+ },
236
+ {
237
+ field_path: 'metrics.icp_score',
238
+ operator: 'gte',
239
+ value: 70,
240
+ },
241
+ ]);
242
+ const [logic, setLogic] = useState<FilterLogic>('AND');
243
+
244
+ return (
245
+ <div className="max-w-4xl mx-auto p-8">
246
+ <h2 className="text-2xl font-bold mb-4">Investor Filters</h2>
247
+ <p className="text-gray-600 mb-6">
248
+ Find Series A/B investors with min $500K check size and ICP score 70+
249
+ </p>
250
+ <FilterRuleEditor
251
+ rules={rules}
252
+ onChange={setRules}
253
+ fieldRegistry={investorFields}
254
+ logic={logic}
255
+ onLogicChange={setLogic}
256
+ />
257
+ <pre className="mt-4 p-4 bg-gray-100 rounded text-xs overflow-auto">
258
+ {JSON.stringify({ logic, rules }, null, 2)}
259
+ </pre>
260
+ </div>
261
+ );
262
+ };
263
+
264
+ /**
265
+ * Recipe filters - find quick vegetarian recipes
266
+ */
267
+ export const RecipeFilters = () => {
268
+ const [rules, setRules] = useState<FilterRule[]>([
269
+ {
270
+ field_path: 'cuisine',
271
+ operator: 'in',
272
+ value: ['Italian', 'French', 'Japanese'],
273
+ },
274
+ {
275
+ field_path: 'difficulty',
276
+ operator: 'eq',
277
+ value: 'Easy',
278
+ },
279
+ {
280
+ field_path: 'prep_time_minutes',
281
+ operator: 'lte',
282
+ value: 30,
283
+ },
284
+ {
285
+ field_path: 'is_vegetarian',
286
+ operator: 'is_true',
287
+ value: null,
288
+ },
289
+ ]);
290
+ const [logic, setLogic] = useState<FilterLogic>('AND');
291
+
292
+ return (
293
+ <div className="max-w-4xl mx-auto p-8">
294
+ <h2 className="text-2xl font-bold mb-4">Recipe Filters</h2>
295
+ <p className="text-gray-600 mb-6">
296
+ Find easy vegetarian recipes from Italian/French/Japanese cuisine, under 30 min prep
297
+ </p>
298
+ <FilterRuleEditor
299
+ rules={rules}
300
+ onChange={setRules}
301
+ fieldRegistry={recipeFields}
302
+ logic={logic}
303
+ onLogicChange={setLogic}
304
+ />
305
+ <pre className="mt-4 p-4 bg-gray-100 rounded text-xs overflow-auto">
306
+ {JSON.stringify({ logic, rules }, null, 2)}
307
+ </pre>
308
+ </div>
309
+ );
310
+ };
311
+
312
+ /**
313
+ * OR logic - flexible matching
314
+ */
315
+ export const OrLogic = () => {
316
+ const [rules, setRules] = useState<FilterRule[]>([
317
+ {
318
+ field_path: 'name',
319
+ operator: 'contains',
320
+ value: 'Ventures',
321
+ },
322
+ {
323
+ field_path: 'tags',
324
+ operator: 'has_tag',
325
+ value: 'hot-lead',
326
+ },
327
+ {
328
+ field_path: 'metrics.icp_score',
329
+ operator: 'gte',
330
+ value: 90,
331
+ },
332
+ ]);
333
+ const [logic, setLogic] = useState<FilterLogic>('OR');
334
+
335
+ return (
336
+ <div className="max-w-4xl mx-auto p-8">
337
+ <h2 className="text-2xl font-bold mb-4">OR Logic Example</h2>
338
+ <p className="text-gray-600 mb-6">
339
+ Match if name contains "Ventures" OR has "hot-lead" tag OR ICP score is 90+
340
+ </p>
341
+ <FilterRuleEditor
342
+ rules={rules}
343
+ onChange={setRules}
344
+ fieldRegistry={investorFields}
345
+ logic={logic}
346
+ onLogicChange={setLogic}
347
+ />
348
+ <pre className="mt-4 p-4 bg-gray-100 rounded text-xs overflow-auto">
349
+ {JSON.stringify({ logic, rules }, null, 2)}
350
+ </pre>
351
+ </div>
352
+ );
353
+ };
354
+
355
+ /**
356
+ * Complex filters - many rules
357
+ */
358
+ export const ComplexFilters = () => {
359
+ const [rules, setRules] = useState<FilterRule[]>([
360
+ {
361
+ field_path: 'firm.stage',
362
+ operator: 'in',
363
+ value: ['Series A', 'Series B', 'Series C'],
364
+ },
365
+ {
366
+ field_path: 'firm.check_size_min',
367
+ operator: 'gte',
368
+ value: 1000000,
369
+ },
370
+ {
371
+ field_path: 'firm.check_size_max',
372
+ operator: 'lte',
373
+ value: 10000000,
374
+ },
375
+ {
376
+ field_path: 'metrics.icp_score',
377
+ operator: 'gte',
378
+ value: 80,
379
+ },
380
+ {
381
+ field_path: 'metrics.aum_usd',
382
+ operator: 'gte',
383
+ value: 100000000,
384
+ },
385
+ {
386
+ field_path: 'email',
387
+ operator: 'isnotnull',
388
+ value: null,
389
+ },
390
+ ]);
391
+ const [logic, setLogic] = useState<FilterLogic>('AND');
392
+
393
+ return (
394
+ <div className="max-w-4xl mx-auto p-8">
395
+ <h2 className="text-2xl font-bold mb-4">Complex Filters</h2>
396
+ <p className="text-gray-600 mb-6">
397
+ Multiple conditions: stage, check size range, ICP score, AUM, and email presence
398
+ </p>
399
+ <FilterRuleEditor
400
+ rules={rules}
401
+ onChange={setRules}
402
+ fieldRegistry={investorFields}
403
+ logic={logic}
404
+ onLogicChange={setLogic}
405
+ />
406
+ <pre className="mt-4 p-4 bg-gray-100 rounded text-xs overflow-auto max-h-96">
407
+ {JSON.stringify({ logic, rules }, null, 2)}
408
+ </pre>
409
+ </div>
410
+ );
411
+ };
412
+
413
+ /**
414
+ * Interactive playground
415
+ */
416
+ export const Playground = () => {
417
+ const [rules, setRules] = useState<FilterRule[]>([]);
418
+ const [logic, setLogic] = useState<FilterLogic>('AND');
419
+ const [domain, setDomain] = useState<'investor' | 'recipe'>('investor');
420
+
421
+ const fields = domain === 'investor' ? investorFields : recipeFields;
422
+
423
+ return (
424
+ <div className="max-w-4xl mx-auto p-8">
425
+ <div className="flex items-center justify-between mb-6">
426
+ <h2 className="text-2xl font-bold">Filter Builder Playground</h2>
427
+ <div className="flex gap-2">
428
+ <button
429
+ onClick={() => setDomain('investor')}
430
+ className={`px-4 py-2 rounded ${
431
+ domain === 'investor'
432
+ ? 'bg-blue-600 text-white'
433
+ : 'bg-gray-200 text-gray-700'
434
+ }`}
435
+ >
436
+ Investor Domain
437
+ </button>
438
+ <button
439
+ onClick={() => setDomain('recipe')}
440
+ className={`px-4 py-2 rounded ${
441
+ domain === 'recipe'
442
+ ? 'bg-blue-600 text-white'
443
+ : 'bg-gray-200 text-gray-700'
444
+ }`}
445
+ >
446
+ Recipe Domain
447
+ </button>
448
+ </div>
449
+ </div>
450
+ <FilterRuleEditor
451
+ rules={rules}
452
+ onChange={setRules}
453
+ fieldRegistry={fields}
454
+ logic={logic}
455
+ onLogicChange={setLogic}
456
+ />
457
+ <pre className="mt-4 p-4 bg-gray-100 rounded text-xs overflow-auto max-h-96">
458
+ {JSON.stringify({ domain, logic, rules }, null, 2)}
459
+ </pre>
460
+ </div>
461
+ );
462
+ };