@startsimpli/funnels 0.1.4 → 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.
- 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,337 @@
|
|
|
1
|
+
# FunnelPreview Component
|
|
2
|
+
|
|
3
|
+
Real-time preview of funnel execution on sample entities.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
Shows users how their funnel configuration affects entities BEFORE running the full funnel. Updates automatically as they edit filter rules, providing instant feedback on match rates and stage-by-stage results.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
### Basic Example
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { FunnelPreview } from '@simpli/funnels';
|
|
15
|
+
|
|
16
|
+
function MyComponent() {
|
|
17
|
+
const funnel = {
|
|
18
|
+
id: 'my-funnel',
|
|
19
|
+
name: 'Investor Qualification',
|
|
20
|
+
status: 'active',
|
|
21
|
+
input_type: 'contacts',
|
|
22
|
+
stages: [
|
|
23
|
+
{
|
|
24
|
+
id: 'stage-1',
|
|
25
|
+
order: 0,
|
|
26
|
+
name: 'Series A or Later',
|
|
27
|
+
filter_logic: 'AND',
|
|
28
|
+
rules: [
|
|
29
|
+
{
|
|
30
|
+
field_path: 'firm.stage',
|
|
31
|
+
operator: 'in',
|
|
32
|
+
value: ['Series A', 'Series B', 'Series C'],
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
match_action: 'continue',
|
|
36
|
+
no_match_action: 'exclude',
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
created_at: new Date().toISOString(),
|
|
40
|
+
updated_at: new Date().toISOString(),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const sampleInvestors = [
|
|
44
|
+
{ name: 'John Doe', firm: { name: 'Acme VC', stage: 'Series A' }, score: 85 },
|
|
45
|
+
{ name: 'Jane Smith', firm: { name: 'TechCo', stage: 'Seed' }, score: 92 },
|
|
46
|
+
// ... more investors
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<FunnelPreview
|
|
51
|
+
funnel={funnel}
|
|
52
|
+
sampleEntities={sampleInvestors}
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### With Custom Entity Renderer
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
<FunnelPreview
|
|
62
|
+
funnel={funnel}
|
|
63
|
+
sampleEntities={investors}
|
|
64
|
+
renderEntity={(investor) => (
|
|
65
|
+
<div>
|
|
66
|
+
<div className="font-medium">{investor.name}</div>
|
|
67
|
+
<div className="text-sm text-gray-600">
|
|
68
|
+
{investor.firm.name} · {investor.firm.stage} · Score: {investor.score}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
/>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### With Preview Callback
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
<FunnelPreview
|
|
79
|
+
funnel={funnel}
|
|
80
|
+
sampleEntities={entities}
|
|
81
|
+
onPreview={(result) => {
|
|
82
|
+
console.log('Matched:', result.totalMatched);
|
|
83
|
+
console.log('Excluded:', result.totalExcluded);
|
|
84
|
+
console.log('Match %:', result.matchPercentage);
|
|
85
|
+
}}
|
|
86
|
+
/>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Customize Preview Limit
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
<FunnelPreview
|
|
93
|
+
funnel={funnel}
|
|
94
|
+
sampleEntities={entities}
|
|
95
|
+
maxPreviewEntities={20} // Show up to 20 entities (default: 10)
|
|
96
|
+
/>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Props
|
|
100
|
+
|
|
101
|
+
| Prop | Type | Required | Default | Description |
|
|
102
|
+
|------|------|----------|---------|-------------|
|
|
103
|
+
| `funnel` | `Funnel` | Yes | - | The funnel to preview |
|
|
104
|
+
| `sampleEntities` | `any[]` | Yes | - | Sample entities to process |
|
|
105
|
+
| `onPreview` | `(result: PreviewResult) => void` | No | - | Callback when preview updates |
|
|
106
|
+
| `renderEntity` | `(entity: any) => ReactNode` | No | Default renderer | Custom entity display |
|
|
107
|
+
| `maxPreviewEntities` | `number` | No | `10` | Max entities to show |
|
|
108
|
+
| `className` | `string` | No | `''` | Additional CSS classes |
|
|
109
|
+
|
|
110
|
+
## Preview Result
|
|
111
|
+
|
|
112
|
+
The `onPreview` callback receives a `PreviewResult`:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
interface PreviewResult {
|
|
116
|
+
totalMatched: number;
|
|
117
|
+
totalExcluded: number;
|
|
118
|
+
matchPercentage: number;
|
|
119
|
+
previewEntities: any[];
|
|
120
|
+
stageStats: Record<string, StagePreviewStats>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
interface StagePreviewStats {
|
|
124
|
+
stage_id: string;
|
|
125
|
+
stage_name: string;
|
|
126
|
+
input_count: number;
|
|
127
|
+
excluded_count: number;
|
|
128
|
+
remaining_count: number;
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Features
|
|
133
|
+
|
|
134
|
+
### Debounced Updates
|
|
135
|
+
|
|
136
|
+
Preview automatically re-computes when funnel or entities change, with a 300ms debounce to prevent excessive computation during editing.
|
|
137
|
+
|
|
138
|
+
### Loading States
|
|
139
|
+
|
|
140
|
+
Shows skeleton UI on initial load and a subtle overlay during updates.
|
|
141
|
+
|
|
142
|
+
### Empty States
|
|
143
|
+
|
|
144
|
+
Displays helpful message when no entities match the funnel.
|
|
145
|
+
|
|
146
|
+
### Stage Breakdown
|
|
147
|
+
|
|
148
|
+
Visual flow showing how many entities are filtered at each stage.
|
|
149
|
+
|
|
150
|
+
### Match Statistics
|
|
151
|
+
|
|
152
|
+
Progress bar and counts showing overall funnel effectiveness.
|
|
153
|
+
|
|
154
|
+
## Subcomponents
|
|
155
|
+
|
|
156
|
+
All subcomponents are exported and can be used independently:
|
|
157
|
+
|
|
158
|
+
### PreviewStats
|
|
159
|
+
|
|
160
|
+
Shows matched vs excluded counts with progress bar.
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
import { PreviewStats } from '@simpli/funnels';
|
|
164
|
+
|
|
165
|
+
<PreviewStats
|
|
166
|
+
totalMatched={235}
|
|
167
|
+
totalExcluded={765}
|
|
168
|
+
matchPercentage={23.5}
|
|
169
|
+
/>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### StageBreakdown
|
|
173
|
+
|
|
174
|
+
Shows per-stage statistics in funnel flow.
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
import { StageBreakdown } from '@simpli/funnels';
|
|
178
|
+
|
|
179
|
+
<StageBreakdown
|
|
180
|
+
stageStats={stageStats}
|
|
181
|
+
stages={funnel.stages}
|
|
182
|
+
/>
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### EntityCard
|
|
186
|
+
|
|
187
|
+
Displays a single entity with custom renderer.
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
import { EntityCard } from '@simpli/funnels';
|
|
191
|
+
|
|
192
|
+
<EntityCard
|
|
193
|
+
entity={investor}
|
|
194
|
+
renderEntity={(inv) => (
|
|
195
|
+
<div>{inv.name} - {inv.firm.name}</div>
|
|
196
|
+
)}
|
|
197
|
+
/>
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### LoadingPreview
|
|
201
|
+
|
|
202
|
+
Skeleton loading state.
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
import { LoadingPreview } from '@simpli/funnels';
|
|
206
|
+
|
|
207
|
+
{isLoading && <LoadingPreview />}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Examples
|
|
211
|
+
|
|
212
|
+
### Recipe Funnel
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
const recipeFunnel = {
|
|
216
|
+
id: 'recipe-filter',
|
|
217
|
+
name: 'Recipe Recommendations',
|
|
218
|
+
status: 'active',
|
|
219
|
+
input_type: 'any',
|
|
220
|
+
stages: [
|
|
221
|
+
{
|
|
222
|
+
id: 'dietary',
|
|
223
|
+
order: 0,
|
|
224
|
+
name: 'Dietary Restrictions',
|
|
225
|
+
filter_logic: 'AND',
|
|
226
|
+
rules: [
|
|
227
|
+
{ field_path: 'tags', operator: 'has_tag', value: 'vegetarian' },
|
|
228
|
+
],
|
|
229
|
+
match_action: 'continue',
|
|
230
|
+
no_match_action: 'exclude',
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
created_at: new Date().toISOString(),
|
|
234
|
+
updated_at: new Date().toISOString(),
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
<FunnelPreview
|
|
238
|
+
funnel={recipeFunnel}
|
|
239
|
+
sampleEntities={recipes}
|
|
240
|
+
renderEntity={(recipe) => (
|
|
241
|
+
<div>
|
|
242
|
+
<div className="font-medium">{recipe.name}</div>
|
|
243
|
+
<div className="text-sm text-gray-600">
|
|
244
|
+
{recipe.cuisine} · {recipe.difficulty} · {recipe.cookTime}
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
)}
|
|
248
|
+
/>
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Lead Scoring Funnel
|
|
252
|
+
|
|
253
|
+
```tsx
|
|
254
|
+
const leadFunnel = {
|
|
255
|
+
id: 'lead-scoring',
|
|
256
|
+
name: 'Qualified Leads',
|
|
257
|
+
status: 'active',
|
|
258
|
+
input_type: 'any',
|
|
259
|
+
stages: [
|
|
260
|
+
{
|
|
261
|
+
id: 'company-size',
|
|
262
|
+
order: 0,
|
|
263
|
+
name: 'Company Size',
|
|
264
|
+
filter_logic: 'AND',
|
|
265
|
+
rules: [
|
|
266
|
+
{ field_path: 'company.employees', operator: 'gte', value: 50 },
|
|
267
|
+
],
|
|
268
|
+
match_action: 'continue',
|
|
269
|
+
no_match_action: 'exclude',
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
id: 'engagement',
|
|
273
|
+
order: 1,
|
|
274
|
+
name: 'Engagement Score',
|
|
275
|
+
filter_logic: 'AND',
|
|
276
|
+
rules: [
|
|
277
|
+
{ field_path: 'engagement_score', operator: 'gte', value: 7 },
|
|
278
|
+
],
|
|
279
|
+
match_action: 'output',
|
|
280
|
+
no_match_action: 'exclude',
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
created_at: new Date().toISOString(),
|
|
284
|
+
updated_at: new Date().toISOString(),
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
<FunnelPreview
|
|
288
|
+
funnel={leadFunnel}
|
|
289
|
+
sampleEntities={leads}
|
|
290
|
+
renderEntity={(lead) => (
|
|
291
|
+
<div>
|
|
292
|
+
<div className="font-medium">{lead.name}</div>
|
|
293
|
+
<div className="text-sm text-gray-600">
|
|
294
|
+
{lead.company.name} · {lead.company.employees} employees · Score: {lead.engagement_score}
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
)}
|
|
298
|
+
/>
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## Accessibility
|
|
302
|
+
|
|
303
|
+
- ARIA progressbar role on stats bar
|
|
304
|
+
- ARIA live region announces updates
|
|
305
|
+
- Loading states communicated to screen readers
|
|
306
|
+
- Keyboard navigation through entity cards
|
|
307
|
+
- Color is supplementary (text labels provide meaning)
|
|
308
|
+
- Sufficient color contrast (WCAG AA)
|
|
309
|
+
|
|
310
|
+
## Performance
|
|
311
|
+
|
|
312
|
+
- Debounced updates (300ms) prevent excessive re-computation
|
|
313
|
+
- Efficient execution via FunnelEngine
|
|
314
|
+
- Limits preview entities (default 10) for fast rendering
|
|
315
|
+
- Skeleton UI reduces perceived loading time
|
|
316
|
+
|
|
317
|
+
## Design System
|
|
318
|
+
|
|
319
|
+
Uses Tailwind CSS classes matching the Simpli design system:
|
|
320
|
+
|
|
321
|
+
- Colors: `blue-*` (input), `green-*` (matched), `red-*` (excluded), `gray-*` (neutral)
|
|
322
|
+
- Spacing: `gap-2`, `gap-3`, `mb-3`, `mb-6`
|
|
323
|
+
- Typography: `text-sm`, `text-lg`, `font-medium`, `font-semibold`
|
|
324
|
+
- Borders: `rounded-lg`, `border-gray-200`
|
|
325
|
+
- Shadows: `shadow-sm`
|
|
326
|
+
|
|
327
|
+
## BRUTALLY GENERIC
|
|
328
|
+
|
|
329
|
+
Works with ANY entity type:
|
|
330
|
+
|
|
331
|
+
- Investors, firms, contacts, organizations
|
|
332
|
+
- Recipes, ingredients, cooking instructions
|
|
333
|
+
- Leads, opportunities, tasks, projects
|
|
334
|
+
- GitHub repos, PRs, issues, users
|
|
335
|
+
- Products, orders, customers, shipments
|
|
336
|
+
|
|
337
|
+
No domain-specific logic. All filtering based on generic field paths and operators.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StageBreakdown Component
|
|
3
|
+
*
|
|
4
|
+
* Shows per-stage statistics in a funnel flow visualization.
|
|
5
|
+
*
|
|
6
|
+
* Design Rationale:
|
|
7
|
+
* - Sequential list shows funnel flow from top to bottom
|
|
8
|
+
* - Stage numbers (①②③) provide clear ordering
|
|
9
|
+
* - Exclusion count (-N) shows entities removed at each stage
|
|
10
|
+
* - Remaining count shows entities continuing to next stage
|
|
11
|
+
* - Color coding: gray for excluded, green for final matched
|
|
12
|
+
*
|
|
13
|
+
* Accessibility:
|
|
14
|
+
* - Semantic list structure
|
|
15
|
+
* - Clear text labels for all metrics
|
|
16
|
+
* - Color is supplementary to text
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { StageStats } from '../../types';
|
|
20
|
+
|
|
21
|
+
export interface StagePreviewStats {
|
|
22
|
+
stage_id: string;
|
|
23
|
+
stage_name: string;
|
|
24
|
+
input_count: number;
|
|
25
|
+
excluded_count: number;
|
|
26
|
+
remaining_count: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface StageBreakdownProps {
|
|
30
|
+
stageStats: Record<string, StagePreviewStats>;
|
|
31
|
+
stages: Array<{ id: string; name: string; order: number }>;
|
|
32
|
+
className?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function StageBreakdown({
|
|
36
|
+
stageStats,
|
|
37
|
+
stages,
|
|
38
|
+
className = '',
|
|
39
|
+
}: StageBreakdownProps) {
|
|
40
|
+
// Sort stages by order
|
|
41
|
+
const sortedStages = [...stages].sort((a, b) => a.order - b.order);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className={className}>
|
|
45
|
+
<h3 className="text-sm font-semibold text-gray-700 mb-3">
|
|
46
|
+
Stage Breakdown
|
|
47
|
+
</h3>
|
|
48
|
+
|
|
49
|
+
<ol className="space-y-2">
|
|
50
|
+
{sortedStages.map((stage, index) => {
|
|
51
|
+
const stats = stageStats[stage.id];
|
|
52
|
+
if (!stats) return null;
|
|
53
|
+
|
|
54
|
+
const isLast = index === sortedStages.length - 1;
|
|
55
|
+
const excludedCount = stats.excluded_count;
|
|
56
|
+
const remainingCount = stats.remaining_count;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<li
|
|
60
|
+
key={stage.id}
|
|
61
|
+
className="flex items-center justify-between py-2 px-3 bg-gray-50 rounded-lg"
|
|
62
|
+
>
|
|
63
|
+
{/* Stage info */}
|
|
64
|
+
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
65
|
+
<span className="flex-shrink-0 w-6 h-6 flex items-center justify-center bg-blue-100 text-blue-700 rounded-full text-xs font-bold">
|
|
66
|
+
{index + 1}
|
|
67
|
+
</span>
|
|
68
|
+
<span className="text-sm font-medium text-gray-900 truncate">
|
|
69
|
+
{stage.name}
|
|
70
|
+
</span>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{/* Stats */}
|
|
74
|
+
<div className="flex items-center gap-3 text-sm">
|
|
75
|
+
{excludedCount > 0 && (
|
|
76
|
+
<span className="text-red-600 font-medium">
|
|
77
|
+
-{excludedCount.toLocaleString()}
|
|
78
|
+
</span>
|
|
79
|
+
)}
|
|
80
|
+
<span
|
|
81
|
+
className={`font-semibold ${
|
|
82
|
+
isLast ? 'text-green-600' : 'text-gray-700'
|
|
83
|
+
}`}
|
|
84
|
+
>
|
|
85
|
+
{remainingCount.toLocaleString()} {isLast ? 'final' : 'left'}
|
|
86
|
+
</span>
|
|
87
|
+
</div>
|
|
88
|
+
</li>
|
|
89
|
+
);
|
|
90
|
+
})}
|
|
91
|
+
</ol>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FunnelPreview Example Usage
|
|
3
|
+
*
|
|
4
|
+
* This file shows how to use the FunnelPreview component
|
|
5
|
+
* in different scenarios.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { FunnelPreview } from './FunnelPreview';
|
|
9
|
+
import type { Funnel } from '../../types';
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Example 1: Investor Qualification Funnel
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
const investorFunnel: Funnel = {
|
|
16
|
+
id: 'investor-qualification',
|
|
17
|
+
name: 'Investor Qualification',
|
|
18
|
+
description: 'Filter investors by firm stage and check size',
|
|
19
|
+
status: 'active',
|
|
20
|
+
input_type: 'contacts',
|
|
21
|
+
stages: [
|
|
22
|
+
{
|
|
23
|
+
id: 'stage-1',
|
|
24
|
+
order: 0,
|
|
25
|
+
name: 'Series A or Later',
|
|
26
|
+
filter_logic: 'OR',
|
|
27
|
+
rules: [
|
|
28
|
+
{ field_path: 'firm.stage', operator: 'eq', value: 'Series A' },
|
|
29
|
+
{ field_path: 'firm.stage', operator: 'eq', value: 'Series B' },
|
|
30
|
+
{ field_path: 'firm.stage', operator: 'eq', value: 'Series C' },
|
|
31
|
+
],
|
|
32
|
+
match_action: 'continue',
|
|
33
|
+
no_match_action: 'exclude',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'stage-2',
|
|
37
|
+
order: 1,
|
|
38
|
+
name: 'High ICP Score',
|
|
39
|
+
filter_logic: 'AND',
|
|
40
|
+
rules: [{ field_path: 'icp_score', operator: 'gte', value: 80 }],
|
|
41
|
+
match_action: 'output',
|
|
42
|
+
no_match_action: 'exclude',
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
created_at: new Date().toISOString(),
|
|
46
|
+
updated_at: new Date().toISOString(),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const sampleInvestors = [
|
|
50
|
+
{
|
|
51
|
+
name: 'John Doe',
|
|
52
|
+
firm: { name: 'Acme VC', stage: 'Series A' },
|
|
53
|
+
icp_score: 85,
|
|
54
|
+
email: 'john@acme.vc',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'Jane Smith',
|
|
58
|
+
firm: { name: 'TechCo Ventures', stage: 'Seed' },
|
|
59
|
+
icp_score: 92,
|
|
60
|
+
email: 'jane@techco.com',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'Bob Wilson',
|
|
64
|
+
firm: { name: 'Growth Capital', stage: 'Series B' },
|
|
65
|
+
icp_score: 78,
|
|
66
|
+
email: 'bob@growth.capital',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'Alice Johnson',
|
|
70
|
+
firm: { name: 'Early Stage Fund', stage: 'Series A' },
|
|
71
|
+
icp_score: 88,
|
|
72
|
+
email: 'alice@earlystage.fund',
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
export function InvestorQualificationExample() {
|
|
77
|
+
return (
|
|
78
|
+
<div className="max-w-2xl mx-auto p-6">
|
|
79
|
+
<h1 className="text-2xl font-bold mb-6">Investor Qualification Preview</h1>
|
|
80
|
+
|
|
81
|
+
<FunnelPreview
|
|
82
|
+
funnel={investorFunnel}
|
|
83
|
+
sampleEntities={sampleInvestors}
|
|
84
|
+
renderEntity={(investor) => (
|
|
85
|
+
<div>
|
|
86
|
+
<div className="font-medium text-gray-900">{investor.name}</div>
|
|
87
|
+
<div className="text-sm text-gray-600 mt-1">
|
|
88
|
+
{investor.firm.name} · {investor.firm.stage} · ICP Score:{' '}
|
|
89
|
+
{investor.icp_score}
|
|
90
|
+
</div>
|
|
91
|
+
<div className="text-xs text-gray-500 mt-0.5">{investor.email}</div>
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
onPreview={(result) => {
|
|
95
|
+
console.log('Preview updated:', {
|
|
96
|
+
matched: result.totalMatched,
|
|
97
|
+
excluded: result.totalExcluded,
|
|
98
|
+
percentage: result.matchPercentage,
|
|
99
|
+
});
|
|
100
|
+
}}
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// Example 2: Recipe Filtering
|
|
108
|
+
// ============================================================================
|
|
109
|
+
|
|
110
|
+
const recipeFunnel: Funnel = {
|
|
111
|
+
id: 'recipe-filter',
|
|
112
|
+
name: 'Recipe Recommendations',
|
|
113
|
+
description: 'Filter recipes by dietary restrictions and difficulty',
|
|
114
|
+
status: 'active',
|
|
115
|
+
input_type: 'any',
|
|
116
|
+
stages: [
|
|
117
|
+
{
|
|
118
|
+
id: 'dietary',
|
|
119
|
+
order: 0,
|
|
120
|
+
name: 'Vegetarian',
|
|
121
|
+
filter_logic: 'AND',
|
|
122
|
+
rules: [{ field_path: 'tags', operator: 'has_tag', value: 'vegetarian' }],
|
|
123
|
+
match_action: 'continue',
|
|
124
|
+
no_match_action: 'exclude',
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: 'difficulty',
|
|
128
|
+
order: 1,
|
|
129
|
+
name: 'Easy to Moderate',
|
|
130
|
+
filter_logic: 'OR',
|
|
131
|
+
rules: [
|
|
132
|
+
{ field_path: 'difficulty', operator: 'eq', value: 'easy' },
|
|
133
|
+
{ field_path: 'difficulty', operator: 'eq', value: 'moderate' },
|
|
134
|
+
],
|
|
135
|
+
match_action: 'output',
|
|
136
|
+
no_match_action: 'exclude',
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
created_at: new Date().toISOString(),
|
|
140
|
+
updated_at: new Date().toISOString(),
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const sampleRecipes = [
|
|
144
|
+
{
|
|
145
|
+
name: 'Vegetable Stir Fry',
|
|
146
|
+
cuisine: 'Asian',
|
|
147
|
+
difficulty: 'easy',
|
|
148
|
+
cookTime: '20 min',
|
|
149
|
+
tags: ['vegetarian', 'quick', 'healthy'],
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: 'Beef Tacos',
|
|
153
|
+
cuisine: 'Mexican',
|
|
154
|
+
difficulty: 'easy',
|
|
155
|
+
cookTime: '30 min',
|
|
156
|
+
tags: ['quick', 'family-friendly'],
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'Veggie Curry',
|
|
160
|
+
cuisine: 'Indian',
|
|
161
|
+
difficulty: 'moderate',
|
|
162
|
+
cookTime: '45 min',
|
|
163
|
+
tags: ['vegetarian', 'spicy'],
|
|
164
|
+
},
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
export function RecipeFilterExample() {
|
|
168
|
+
return (
|
|
169
|
+
<div className="max-w-2xl mx-auto p-6">
|
|
170
|
+
<h1 className="text-2xl font-bold mb-6">Recipe Filter Preview</h1>
|
|
171
|
+
|
|
172
|
+
<FunnelPreview
|
|
173
|
+
funnel={recipeFunnel}
|
|
174
|
+
sampleEntities={sampleRecipes}
|
|
175
|
+
renderEntity={(recipe) => (
|
|
176
|
+
<div>
|
|
177
|
+
<div className="font-medium text-gray-900">{recipe.name}</div>
|
|
178
|
+
<div className="text-sm text-gray-600 mt-1">
|
|
179
|
+
{recipe.cuisine} · {recipe.difficulty} · {recipe.cookTime}
|
|
180
|
+
</div>
|
|
181
|
+
<div className="text-xs text-gray-500 mt-1">
|
|
182
|
+
{recipe.tags.join(', ')}
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
)}
|
|
186
|
+
maxPreviewEntities={5}
|
|
187
|
+
/>
|
|
188
|
+
</div>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ============================================================================
|
|
193
|
+
// Example 3: Lead Scoring
|
|
194
|
+
// ============================================================================
|
|
195
|
+
|
|
196
|
+
const leadFunnel: Funnel = {
|
|
197
|
+
id: 'lead-scoring',
|
|
198
|
+
name: 'Qualified Leads',
|
|
199
|
+
description: 'Score and filter leads based on company size and engagement',
|
|
200
|
+
status: 'active',
|
|
201
|
+
input_type: 'any',
|
|
202
|
+
stages: [
|
|
203
|
+
{
|
|
204
|
+
id: 'company-size',
|
|
205
|
+
order: 0,
|
|
206
|
+
name: 'Mid-Market and Above',
|
|
207
|
+
filter_logic: 'AND',
|
|
208
|
+
rules: [{ field_path: 'company.employees', operator: 'gte', value: 50 }],
|
|
209
|
+
match_action: 'continue',
|
|
210
|
+
no_match_action: 'exclude',
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
id: 'engagement',
|
|
214
|
+
order: 1,
|
|
215
|
+
name: 'High Engagement',
|
|
216
|
+
filter_logic: 'AND',
|
|
217
|
+
rules: [{ field_path: 'engagement_score', operator: 'gte', value: 7 }],
|
|
218
|
+
match_action: 'tag_continue',
|
|
219
|
+
no_match_action: 'continue',
|
|
220
|
+
match_tags: ['high-engagement'],
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: 'industry',
|
|
224
|
+
order: 2,
|
|
225
|
+
name: 'Target Industries',
|
|
226
|
+
filter_logic: 'OR',
|
|
227
|
+
rules: [
|
|
228
|
+
{ field_path: 'company.industry', operator: 'eq', value: 'Technology' },
|
|
229
|
+
{ field_path: 'company.industry', operator: 'eq', value: 'SaaS' },
|
|
230
|
+
{ field_path: 'company.industry', operator: 'eq', value: 'FinTech' },
|
|
231
|
+
],
|
|
232
|
+
match_action: 'output',
|
|
233
|
+
no_match_action: 'exclude',
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
created_at: new Date().toISOString(),
|
|
237
|
+
updated_at: new Date().toISOString(),
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const sampleLeads = [
|
|
241
|
+
{
|
|
242
|
+
name: 'Sarah Chen',
|
|
243
|
+
title: 'VP of Engineering',
|
|
244
|
+
company: { name: 'TechCorp', industry: 'Technology', employees: 150 },
|
|
245
|
+
engagement_score: 8,
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: 'Mike Brown',
|
|
249
|
+
title: 'CTO',
|
|
250
|
+
company: { name: 'SmallCo', industry: 'Retail', employees: 20 },
|
|
251
|
+
engagement_score: 9,
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: 'Lisa Wang',
|
|
255
|
+
title: 'Head of Product',
|
|
256
|
+
company: { name: 'FinanceApp', industry: 'FinTech', employees: 200 },
|
|
257
|
+
engagement_score: 6,
|
|
258
|
+
},
|
|
259
|
+
];
|
|
260
|
+
|
|
261
|
+
export function LeadScoringExample() {
|
|
262
|
+
return (
|
|
263
|
+
<div className="max-w-2xl mx-auto p-6">
|
|
264
|
+
<h1 className="text-2xl font-bold mb-6">Lead Scoring Preview</h1>
|
|
265
|
+
|
|
266
|
+
<FunnelPreview
|
|
267
|
+
funnel={leadFunnel}
|
|
268
|
+
sampleEntities={sampleLeads}
|
|
269
|
+
renderEntity={(lead) => (
|
|
270
|
+
<div>
|
|
271
|
+
<div className="font-medium text-gray-900">
|
|
272
|
+
{lead.name} - {lead.title}
|
|
273
|
+
</div>
|
|
274
|
+
<div className="text-sm text-gray-600 mt-1">
|
|
275
|
+
{lead.company.name} · {lead.company.industry} ·{' '}
|
|
276
|
+
{lead.company.employees} employees
|
|
277
|
+
</div>
|
|
278
|
+
<div className="text-xs text-gray-500 mt-0.5">
|
|
279
|
+
Engagement Score: {lead.engagement_score}/10
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
)}
|
|
283
|
+
/>
|
|
284
|
+
</div>
|
|
285
|
+
);
|
|
286
|
+
}
|