@startsimpli/funnels 0.1.2

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 (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +564 -0
  3. package/dist/client-3ESO2NHy.d.ts +310 -0
  4. package/dist/client-CZu03ACp.d.cts +310 -0
  5. package/dist/components/index.cjs +3243 -0
  6. package/dist/components/index.cjs.map +1 -0
  7. package/dist/components/index.css +198 -0
  8. package/dist/components/index.css.map +1 -0
  9. package/dist/components/index.d.cts +726 -0
  10. package/dist/components/index.d.ts +726 -0
  11. package/dist/components/index.js +3196 -0
  12. package/dist/components/index.js.map +1 -0
  13. package/dist/core/index.cjs +500 -0
  14. package/dist/core/index.cjs.map +1 -0
  15. package/dist/core/index.d.cts +359 -0
  16. package/dist/core/index.d.ts +359 -0
  17. package/dist/core/index.js +486 -0
  18. package/dist/core/index.js.map +1 -0
  19. package/dist/hooks/index.cjs +21 -0
  20. package/dist/hooks/index.cjs.map +1 -0
  21. package/dist/hooks/index.d.cts +11 -0
  22. package/dist/hooks/index.d.ts +11 -0
  23. package/dist/hooks/index.js +19 -0
  24. package/dist/hooks/index.js.map +1 -0
  25. package/dist/index-BGDEXbuz.d.cts +434 -0
  26. package/dist/index-BGDEXbuz.d.ts +434 -0
  27. package/dist/index.cjs +4499 -0
  28. package/dist/index.cjs.map +1 -0
  29. package/dist/index.css +198 -0
  30. package/dist/index.css.map +1 -0
  31. package/dist/index.d.cts +99 -0
  32. package/dist/index.d.ts +99 -0
  33. package/dist/index.js +4421 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/store/index.cjs +391 -0
  36. package/dist/store/index.cjs.map +1 -0
  37. package/dist/store/index.d.cts +225 -0
  38. package/dist/store/index.d.ts +225 -0
  39. package/dist/store/index.js +388 -0
  40. package/dist/store/index.js.map +1 -0
  41. package/package.json +122 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 StartSimpli
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,564 @@
1
+ # @simpli/funnels
2
+
3
+ > Brutally generic filtering pipeline package for any Simpli product
4
+
5
+ [![Tests](https://img.shields.io/badge/tests-487%20passing-green)]()
6
+ [![TypeScript](https://img.shields.io/badge/typescript-5.9%2B-blue)]()
7
+ [![React](https://img.shields.io/badge/react-18%2B%20%7C%2019%2B-blue)]()
8
+ [![Zustand](https://img.shields.io/badge/zustand-4%2B%20%7C%205%2B-blue)]()
9
+
10
+ ## What is this?
11
+
12
+ A reusable package for building multi-stage filtering funnels. Works with **ANY entity type** - investors, recipes, leads, tasks, GitHub issues, or whatever you dream up.
13
+
14
+ ### The Philosophy
15
+
16
+ **BRUTALLY GENERIC** - No domain-specific types. No investor-specific fields. No recipe-specific logic.
17
+
18
+ It's just:
19
+ 1. Start with entities (any type)
20
+ 2. Apply sequential filter stages
21
+ 3. Each stage: keep/exclude/tag based on rules
22
+ 4. End with filtered subset + accumulated tags/context
23
+
24
+ ### Why Use This?
25
+
26
+ - **Zero domain coupling** - Works for investors, recipes, leads, products, tasks, anything
27
+ - **Type-safe** - Full TypeScript support with generics
28
+ - **Modular** - Import only what you need (tree-shakeable)
29
+ - **Server-compatible** - Core engine has NO React dependencies
30
+ - **Battle-tested** - 487 tests passing
31
+ - **Production-ready** - Used across multiple Simpli products
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ npm install @simpli/funnels
37
+ ```
38
+
39
+ ## Quick Start
40
+
41
+ ### Example 1: Investor Funnel
42
+
43
+ Filter investors for a Series A fundraise:
44
+
45
+ ```typescript
46
+ import { Funnel, FunnelEngine } from '@simpli/funnels';
47
+
48
+ interface Investor {
49
+ name: string;
50
+ firm: {
51
+ stage: string;
52
+ check_size_min: number;
53
+ check_size_max: number;
54
+ };
55
+ }
56
+
57
+ const funnel: Funnel<Investor> = {
58
+ id: 'series-a-funnel',
59
+ name: 'Series A Investor Qualification',
60
+ status: 'active',
61
+ input_type: 'contacts',
62
+ stages: [
63
+ {
64
+ id: 'stage-1',
65
+ order: 0,
66
+ name: 'Stage Filter',
67
+ filter_logic: 'OR',
68
+ rules: [
69
+ { field_path: 'firm.stage', operator: 'eq', value: 'Series A' },
70
+ { field_path: 'firm.stage', operator: 'eq', value: 'Multi-Stage' }
71
+ ],
72
+ match_action: 'tag_continue',
73
+ no_match_action: 'exclude',
74
+ match_tags: ['qualified_stage']
75
+ },
76
+ {
77
+ id: 'stage-2',
78
+ order: 1,
79
+ name: 'Check Size',
80
+ filter_logic: 'AND',
81
+ rules: [
82
+ { field_path: 'firm.check_size_min', operator: 'lte', value: 5000000 },
83
+ { field_path: 'firm.check_size_max', operator: 'gte', value: 3000000 }
84
+ ],
85
+ match_action: 'output',
86
+ no_match_action: 'exclude',
87
+ match_tags: ['qualified']
88
+ }
89
+ ],
90
+ created_at: new Date().toISOString(),
91
+ updated_at: new Date().toISOString()
92
+ };
93
+
94
+ // Execute funnel
95
+ const engine = new FunnelEngine();
96
+ const investors = [/* your data */];
97
+ const results = engine.executeSync(funnel, investors);
98
+
99
+ console.log(`Matched: ${results.matched.length}`);
100
+ console.log(`Excluded: ${results.excluded.length}`);
101
+ ```
102
+
103
+ ### Example 2: Recipe Funnel
104
+
105
+ Find quick, easy, vegetarian recipes:
106
+
107
+ ```typescript
108
+ import { Funnel } from '@simpli/funnels';
109
+
110
+ interface Recipe {
111
+ name: string;
112
+ prep_time_minutes: number;
113
+ difficulty: 'easy' | 'medium' | 'hard';
114
+ dietary_restrictions: string[];
115
+ }
116
+
117
+ const funnel: Funnel<Recipe> = {
118
+ id: 'quick-dinner',
119
+ name: 'Quick Weeknight Dinner',
120
+ status: 'active',
121
+ input_type: 'any',
122
+ stages: [
123
+ {
124
+ id: 'dietary',
125
+ order: 0,
126
+ name: 'Dietary Restrictions',
127
+ filter_logic: 'AND',
128
+ rules: [
129
+ { field_path: 'dietary_restrictions', operator: 'has_all', value: ['vegetarian'] }
130
+ ],
131
+ match_action: 'tag_continue',
132
+ no_match_action: 'exclude',
133
+ match_tags: ['vegetarian']
134
+ },
135
+ {
136
+ id: 'time',
137
+ order: 1,
138
+ name: 'Quick Prep',
139
+ filter_logic: 'AND',
140
+ rules: [
141
+ { field_path: 'prep_time_minutes', operator: 'lte', value: 30 }
142
+ ],
143
+ match_action: 'tag_continue',
144
+ no_match_action: 'exclude',
145
+ match_tags: ['quick']
146
+ },
147
+ {
148
+ id: 'difficulty',
149
+ order: 2,
150
+ name: 'Easy to Make',
151
+ filter_logic: 'OR',
152
+ rules: [
153
+ { field_path: 'difficulty', operator: 'eq', value: 'easy' },
154
+ { field_path: 'difficulty', operator: 'eq', value: 'medium' }
155
+ ],
156
+ match_action: 'output',
157
+ no_match_action: 'exclude',
158
+ match_tags: ['beginner_friendly']
159
+ }
160
+ ],
161
+ created_at: new Date().toISOString(),
162
+ updated_at: new Date().toISOString()
163
+ };
164
+ ```
165
+
166
+ ### Example 3: Lead Scoring
167
+
168
+ Score sales leads based on company size and engagement:
169
+
170
+ ```typescript
171
+ import { Funnel } from '@simpli/funnels';
172
+
173
+ interface Lead {
174
+ company: { size: number };
175
+ engagement: {
176
+ email_opens: number;
177
+ demo_requested: boolean;
178
+ };
179
+ tags: string[];
180
+ }
181
+
182
+ const funnel: Funnel<Lead> = {
183
+ id: 'lead-scoring',
184
+ name: 'Enterprise Lead Scoring',
185
+ status: 'active',
186
+ input_type: 'any',
187
+ stages: [
188
+ {
189
+ id: 'company-size',
190
+ order: 0,
191
+ name: 'Enterprise Size',
192
+ filter_logic: 'AND',
193
+ rules: [
194
+ { field_path: 'company.size', operator: 'gte', value: 100 }
195
+ ],
196
+ match_action: 'tag_continue',
197
+ no_match_action: 'tag_continue',
198
+ match_tags: ['enterprise'],
199
+ no_match_tags: ['smb']
200
+ },
201
+ {
202
+ id: 'engagement',
203
+ order: 1,
204
+ name: 'High Engagement',
205
+ filter_logic: 'OR',
206
+ rules: [
207
+ { field_path: 'engagement.email_opens', operator: 'gte', value: 5 },
208
+ { field_path: 'engagement.demo_requested', operator: 'is_true', value: null }
209
+ ],
210
+ match_action: 'output',
211
+ no_match_action: 'output',
212
+ match_tags: ['hot_lead'],
213
+ match_context: { tier: 'A', score: 100 }
214
+ }
215
+ ],
216
+ created_at: new Date().toISOString(),
217
+ updated_at: new Date().toISOString()
218
+ };
219
+ ```
220
+
221
+ ## Core Concepts
222
+
223
+ ### Funnel
224
+
225
+ A sequential pipeline with multiple filtering stages. Each funnel has:
226
+ - **Stages**: Ordered sequence of filter conditions
227
+ - **Status**: draft | active | paused | archived
228
+ - **Metadata**: Tags, context, ownership info
229
+
230
+ ```typescript
231
+ interface Funnel<TEntity = any> {
232
+ id: string;
233
+ name: string;
234
+ status: 'draft' | 'active' | 'paused' | 'archived';
235
+ stages: FunnelStage<TEntity>[];
236
+ // ... more fields
237
+ }
238
+ ```
239
+
240
+ ### Stage
241
+
242
+ A single filtering step with rules, actions, and tags:
243
+
244
+ ```typescript
245
+ interface FunnelStage<TEntity = any> {
246
+ id: string;
247
+ order: number;
248
+ name: string;
249
+ filter_logic: 'AND' | 'OR';
250
+ rules: FilterRule[];
251
+ match_action: 'continue' | 'tag' | 'tag_continue' | 'output';
252
+ no_match_action: 'continue' | 'exclude' | 'tag_exclude';
253
+ match_tags?: string[];
254
+ no_match_tags?: string[];
255
+ custom_evaluator?: (entity: TEntity) => boolean;
256
+ }
257
+ ```
258
+
259
+ ### Filter Rule
260
+
261
+ A single condition with field path, operator, and value:
262
+
263
+ ```typescript
264
+ interface FilterRule {
265
+ field_path: string; // 'firm.stage', 'recipe.cuisine', 'tags'
266
+ operator: Operator; // 'eq', 'gt', 'contains', 'has_any', etc.
267
+ value: any; // Value to compare against
268
+ negate?: boolean; // Optional negation
269
+ }
270
+ ```
271
+
272
+ **Supported operators:**
273
+ - **Equality**: `eq`, `ne`
274
+ - **Comparison**: `gt`, `lt`, `gte`, `lte`
275
+ - **String**: `contains`, `not_contains`, `startswith`, `endswith`, `matches`
276
+ - **Array**: `in`, `not_in`, `has_any`, `has_all`
277
+ - **Null**: `isnull`, `isnotnull`
278
+ - **Tags**: `has_tag`, `not_has_tag`
279
+ - **Boolean**: `is_true`, `is_false`
280
+
281
+ ### Field Registry
282
+
283
+ Defines what fields are available for filtering in your domain:
284
+
285
+ ```typescript
286
+ const investorRegistry: FieldRegistry = {
287
+ entity_type: 'investor',
288
+ fields: [
289
+ {
290
+ name: 'firm.stage',
291
+ label: 'Investment Stage',
292
+ type: 'string',
293
+ operators: ['eq', 'ne', 'in', 'not_in'],
294
+ category: 'Firm Details',
295
+ constraints: {
296
+ choices: ['Seed', 'Series A', 'Series B', 'Growth']
297
+ }
298
+ },
299
+ {
300
+ name: 'firm.check_size_min',
301
+ label: 'Min Check Size',
302
+ type: 'number',
303
+ operators: ['eq', 'ne', 'gt', 'lt', 'gte', 'lte'],
304
+ category: 'Firm Details'
305
+ }
306
+ ]
307
+ };
308
+ ```
309
+
310
+ ## Modular Exports
311
+
312
+ Import only what you need for optimal bundle size:
313
+
314
+ ```typescript
315
+ // Full package (everything)
316
+ import { FunnelEngine, FunnelPreview, createFunnelStore } from '@simpli/funnels';
317
+
318
+ // Core only (NO React dependencies - perfect for workers, CLI, Node.js)
319
+ import { FunnelEngine, evaluateRule, applyOperator } from '@simpli/funnels/core';
320
+
321
+ // Components only (React UI)
322
+ import { FunnelCard, FunnelPreview, FunnelStageBuilder } from '@simpli/funnels/components';
323
+
324
+ // Hooks only (React hooks)
325
+ import { useDebouncedValue } from '@simpli/funnels/hooks';
326
+
327
+ // State only (Zustand store)
328
+ import { createFunnelStore } from '@simpli/funnels/store';
329
+ ```
330
+
331
+ **Use cases:**
332
+ - **Server-side worker**: `@simpli/funnels/core` (no React, no DOM)
333
+ - **Next.js app**: `@simpli/funnels` (full package)
334
+ - **Component library**: `@simpli/funnels/components` (UI only)
335
+ - **State management**: `@simpli/funnels/store` (Zustand store)
336
+
337
+ ## API Reference
338
+
339
+ See [API_REFERENCE.md](./API_REFERENCE.md) for complete API documentation.
340
+
341
+ ### Core Engine
342
+
343
+ ```typescript
344
+ import { FunnelEngine } from '@simpli/funnels/core';
345
+
346
+ const engine = new FunnelEngine();
347
+
348
+ // Synchronous execution
349
+ const results = engine.executeSync(funnel, entities);
350
+
351
+ // Get matched entities
352
+ const matched = results.matched.map(r => r.entity);
353
+
354
+ // Get excluded entities
355
+ const excluded = results.excluded.map(r => r.entity);
356
+ ```
357
+
358
+ ### React Components
359
+
360
+ ```typescript
361
+ import { FunnelCard, FunnelPreview, FunnelStageBuilder } from '@simpli/funnels/components';
362
+
363
+ // Display funnel card
364
+ <FunnelCard funnel={myFunnel} onEdit={handleEdit} onDelete={handleDelete} />
365
+
366
+ // Preview funnel results
367
+ <FunnelPreview funnel={myFunnel} entities={myData} />
368
+
369
+ // Build funnel stages
370
+ <FunnelStageBuilder
371
+ funnel={myFunnel}
372
+ fieldRegistry={registry}
373
+ onChange={handleChange}
374
+ />
375
+ ```
376
+
377
+ ### State Management
378
+
379
+ ```typescript
380
+ import { createFunnelStore } from '@simpli/funnels/store';
381
+
382
+ const useFunnelStore = createFunnelStore();
383
+
384
+ function MyComponent() {
385
+ const { funnel, updateStage, addStage } = useFunnelStore();
386
+
387
+ // Use store methods
388
+ addStage(newStage);
389
+ updateStage(stage.id, { name: 'New Name' });
390
+ }
391
+ ```
392
+
393
+ ## Architecture
394
+
395
+ ### Why Brutally Generic?
396
+
397
+ Traditional filtering systems hardcode domain models:
398
+
399
+ ```typescript
400
+ // ❌ Domain-specific (not reusable)
401
+ interface InvestorFilter {
402
+ firm_stage: string[];
403
+ check_size_min: number;
404
+ geography: string[];
405
+ }
406
+
407
+ interface RecipeFilter {
408
+ cuisine: string[];
409
+ prep_time_max: number;
410
+ dietary: string[];
411
+ }
412
+ ```
413
+
414
+ With @simpli/funnels, one system handles everything:
415
+
416
+ ```typescript
417
+ // ✅ Brutally generic (reusable everywhere)
418
+ interface FilterRule {
419
+ field_path: string; // Works for ANY field
420
+ operator: Operator; // Works for ANY comparison
421
+ value: any; // Works for ANY value
422
+ }
423
+ ```
424
+
425
+ **Benefits:**
426
+ - Write filtering logic once, use everywhere
427
+ - Add new domains without code changes
428
+ - Type-safe with TypeScript generics
429
+ - Test once, trust everywhere
430
+
431
+ ### Sequential Stage Processing
432
+
433
+ Stages execute in order (0, 1, 2, ...). Each stage can:
434
+
435
+ 1. **Continue** - Pass entity to next stage
436
+ 2. **Exclude** - Remove entity from output (stop processing)
437
+ 3. **Tag** - Add tags and stop processing
438
+ 4. **Tag + Continue** - Add tags and pass to next stage
439
+ 5. **Output** - Mark as matched (final stage)
440
+
441
+ ```typescript
442
+ Stage 0: Filter by investment stage
443
+ ├─ Match → tag 'qualified_stage', continue
444
+ └─ No match → exclude
445
+
446
+ Stage 1: Filter by check size
447
+ ├─ Match → tag 'qualified_check_size', continue
448
+ └─ No match → tag 'excluded_check_size', exclude
449
+
450
+ Stage 2: Filter by geography
451
+ ├─ Match → output (final)
452
+ └─ No match → exclude
453
+ ```
454
+
455
+ ### Accumulated State
456
+
457
+ Tags and context accumulate across stages:
458
+
459
+ ```typescript
460
+ {
461
+ entity: { /* investor data */ },
462
+ matched: true,
463
+ accumulated_tags: ['qualified_stage', 'qualified_check_size', 'qualified_geography'],
464
+ context: {
465
+ stage: 'qualified',
466
+ tier: 'A',
467
+ score: 100
468
+ }
469
+ }
470
+ ```
471
+
472
+ ## Examples
473
+
474
+ See [EXAMPLES.md](./EXAMPLES.md) for 6+ real-world examples:
475
+
476
+ - Investor qualification funnel
477
+ - Recipe recommendation funnel
478
+ - Lead scoring funnel
479
+ - GitHub issue triage funnel
480
+ - E-commerce product filtering
481
+ - Task prioritization funnel
482
+
483
+ ## Integration
484
+
485
+ See [INTEGRATION_GUIDE.md](./INTEGRATION_GUIDE.md) for step-by-step integration instructions.
486
+
487
+ Quick summary:
488
+
489
+ 1. Install package: `npm install @simpli/funnels`
490
+ 2. Create field registry for your domain
491
+ 3. Use components in your UI
492
+ 4. Connect to your API/backend
493
+ 5. Configure Tailwind CSS (if using components)
494
+
495
+ ## Storybook
496
+
497
+ Explore 50+ interactive examples:
498
+
499
+ ```bash
500
+ cd packages/funnels
501
+ npm run storybook
502
+ ```
503
+
504
+ Or view the built Storybook in `./storybook-static/index.html`
505
+
506
+ See [STORYBOOK.md](./STORYBOOK.md) for details.
507
+
508
+ ## Testing
509
+
510
+ The package includes comprehensive test coverage:
511
+
512
+ ```bash
513
+ npm run test # Run all tests
514
+ npm run test:watch # Watch mode
515
+ npm run test:coverage # Coverage report
516
+ ```
517
+
518
+ **Test stats:**
519
+ - **487 tests** passing
520
+ - **Core engine**: 262 tests
521
+ - **Components**: 185 tests
522
+ - **Store**: 29 tests
523
+ - **API client**: 24 tests
524
+
525
+ ## Contributing
526
+
527
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for contribution guidelines.
528
+
529
+ Quick summary:
530
+
531
+ 1. Fork the repo
532
+ 2. Create a feature branch
533
+ 3. Make your changes
534
+ 4. Add tests
535
+ 5. Run `npm test` and `npm run type-check`
536
+ 6. Submit a PR
537
+
538
+ ## Changelog
539
+
540
+ See [CHANGELOG.md](./CHANGELOG.md) for version history.
541
+
542
+ ## License
543
+
544
+ MIT - See [LICENSE](./LICENSE) for details.
545
+
546
+ ---
547
+
548
+ ## Why "Brutally Generic"?
549
+
550
+ Because it works for **literally anything**:
551
+
552
+ - ✅ Investors (VC fundraising)
553
+ - ✅ Recipes (cooking app)
554
+ - ✅ Leads (sales CRM)
555
+ - ✅ GitHub issues (project management)
556
+ - ✅ Products (e-commerce)
557
+ - ✅ Tasks (task manager)
558
+ - ✅ Your domain (whatever it is)
559
+
560
+ **Zero domain-specific types. Just generic entity processing with rich filtering capabilities.**
561
+
562
+ ---
563
+
564
+ Built with ❤️ by the Simpli team