@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.
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,249 @@
1
+ /**
2
+ * FunnelPreview Component
3
+ *
4
+ * Real-time preview of funnel execution on sample entities.
5
+ *
6
+ * Design Goal:
7
+ * Users should immediately see how their funnel configuration affects entities.
8
+ * Preview updates automatically as they edit rules, providing instant feedback
9
+ * on match rates and stage-by-stage filtering results.
10
+ *
11
+ * Visual Hierarchy Rationale:
12
+ * 1. Match stats bar (top) - Most important: overall funnel effectiveness
13
+ * 2. Stage breakdown - Shows filtering flow and where entities drop off
14
+ * 3. Sample entities - Concrete examples of what matched
15
+ *
16
+ * Interaction Notes:
17
+ * - Debounced updates (300ms) prevent excessive computation during editing
18
+ * - Loading skeleton maintains layout and reduces perceived lag
19
+ * - Custom entity renderer via render prop for domain-specific display
20
+ *
21
+ * Responsive Behavior:
22
+ * - Single column layout works on all screen sizes
23
+ * - Entity cards stack vertically
24
+ * - Stage breakdown scrolls horizontally on mobile if needed
25
+ *
26
+ * Accessibility Considerations:
27
+ * - ARIA live region announces when preview updates
28
+ * - Loading state communicated to screen readers
29
+ * - All metrics have text labels (color is supplementary)
30
+ * - Keyboard navigation through entity cards
31
+ *
32
+ * Component Specification:
33
+ * - States: loading, empty (no matches), populated
34
+ * - Behaviors: auto-compute on funnel/entity changes
35
+ * - Variations: with/without custom entity renderer
36
+ */
37
+
38
+ import { useEffect, useState, type ReactNode } from 'react';
39
+ import type { Funnel } from '../../types';
40
+ import { FunnelEngine, type ExecutionResult } from '../../core/engine';
41
+ import { useDebouncedValue } from '../../hooks/useDebouncedValue';
42
+ import { PreviewStats } from './PreviewStats';
43
+ import { StageBreakdown, type StagePreviewStats } from './StageBreakdown';
44
+ import { EntityCard } from './EntityCard';
45
+ import { LoadingPreview } from './LoadingPreview';
46
+
47
+ // ============================================================================
48
+ // Types
49
+ // ============================================================================
50
+
51
+ export interface PreviewResult {
52
+ totalMatched: number;
53
+ totalExcluded: number;
54
+ matchPercentage: number;
55
+ previewEntities: any[];
56
+ stageStats: Record<string, StagePreviewStats>;
57
+ }
58
+
59
+ export interface FunnelPreviewProps {
60
+ funnel: Funnel;
61
+ sampleEntities: any[];
62
+ onPreview?: (result: PreviewResult) => void;
63
+ renderEntity?: (entity: any) => ReactNode;
64
+ maxPreviewEntities?: number;
65
+ className?: string;
66
+ }
67
+
68
+ // ============================================================================
69
+ // Helpers
70
+ // ============================================================================
71
+
72
+ /**
73
+ * Convert ExecutionResult to PreviewResult
74
+ */
75
+ function convertToPreviewResult(
76
+ execResult: ExecutionResult,
77
+ maxEntities: number = 10
78
+ ): PreviewResult {
79
+ const { matched, total_matched, total_excluded, stage_stats } = execResult;
80
+
81
+ // Calculate match percentage
82
+ const total = total_matched + total_excluded;
83
+ const matchPercentage =
84
+ total > 0 ? Math.round((total_matched / total) * 100) : 0;
85
+
86
+ // Get preview entities (first N matched)
87
+ const previewEntities = matched.slice(0, maxEntities).map((r) => r.entity);
88
+
89
+ // Convert stage stats to preview format
90
+ const previewStageStats: Record<string, StagePreviewStats> = {};
91
+
92
+ Object.entries(stage_stats).forEach(([stageId, stats]) => {
93
+ previewStageStats[stageId] = {
94
+ stage_id: stats.stage_id,
95
+ stage_name: stats.stage_name,
96
+ input_count: stats.input_count,
97
+ excluded_count: stats.excluded_count,
98
+ remaining_count: stats.input_count - stats.excluded_count,
99
+ };
100
+ });
101
+
102
+ return {
103
+ totalMatched: total_matched,
104
+ totalExcluded: total_excluded,
105
+ matchPercentage,
106
+ previewEntities,
107
+ stageStats: previewStageStats,
108
+ };
109
+ }
110
+
111
+ // ============================================================================
112
+ // Component
113
+ // ============================================================================
114
+
115
+ export function FunnelPreview({
116
+ funnel,
117
+ sampleEntities,
118
+ onPreview,
119
+ renderEntity,
120
+ maxPreviewEntities = 10,
121
+ className = '',
122
+ }: FunnelPreviewProps) {
123
+ const [result, setResult] = useState<PreviewResult | null>(null);
124
+ const [isComputing, setIsComputing] = useState(false);
125
+
126
+ // Debounce funnel to avoid excessive computation
127
+ const debouncedFunnel = useDebouncedValue(funnel, 300);
128
+
129
+ // Compute preview when funnel or entities change
130
+ useEffect(() => {
131
+ async function compute() {
132
+ setIsComputing(true);
133
+
134
+ try {
135
+ const engine = new FunnelEngine();
136
+ const execResult = engine.execute(debouncedFunnel, sampleEntities);
137
+ const previewResult = convertToPreviewResult(
138
+ execResult,
139
+ maxPreviewEntities
140
+ );
141
+
142
+ setResult(previewResult);
143
+
144
+ // Notify parent component
145
+ if (onPreview) {
146
+ onPreview(previewResult);
147
+ }
148
+ } catch (error) {
149
+ console.error('Preview computation failed:', error);
150
+ // Set empty result on error
151
+ setResult({
152
+ totalMatched: 0,
153
+ totalExcluded: sampleEntities.length,
154
+ matchPercentage: 0,
155
+ previewEntities: [],
156
+ stageStats: {},
157
+ });
158
+ } finally {
159
+ setIsComputing(false);
160
+ }
161
+ }
162
+
163
+ compute();
164
+ }, [debouncedFunnel, sampleEntities, maxPreviewEntities, onPreview]);
165
+
166
+ // Loading state
167
+ if (isComputing && !result) {
168
+ return (
169
+ <div className={className}>
170
+ <LoadingPreview />
171
+ </div>
172
+ );
173
+ }
174
+
175
+ // No result yet
176
+ if (!result) {
177
+ return null;
178
+ }
179
+
180
+ const { totalMatched, totalExcluded, matchPercentage, previewEntities, stageStats } =
181
+ result;
182
+
183
+ return (
184
+ <div className={className} role="region" aria-label="Funnel preview">
185
+ <h2 className="text-lg font-semibold text-gray-900 mb-4">
186
+ Preview Results
187
+ </h2>
188
+
189
+ {/* Match stats */}
190
+ <PreviewStats
191
+ totalMatched={totalMatched}
192
+ totalExcluded={totalExcluded}
193
+ matchPercentage={matchPercentage}
194
+ className="mb-6"
195
+ />
196
+
197
+ {/* Stage breakdown */}
198
+ {funnel.stages.length > 0 && (
199
+ <StageBreakdown
200
+ stageStats={stageStats}
201
+ stages={funnel.stages}
202
+ className="mb-6"
203
+ />
204
+ )}
205
+
206
+ {/* Sample matches */}
207
+ <div>
208
+ <h3 className="text-sm font-semibold text-gray-700 mb-3">
209
+ Sample Matches ({Math.min(previewEntities.length, maxPreviewEntities)} of{' '}
210
+ {totalMatched.toLocaleString()})
211
+ </h3>
212
+
213
+ {previewEntities.length === 0 ? (
214
+ <div className="p-8 text-center bg-gray-50 rounded-lg border-2 border-dashed border-gray-300">
215
+ <p className="text-gray-600">No entities matched this funnel</p>
216
+ <p className="text-sm text-gray-500 mt-1">
217
+ Try adjusting your filter rules
218
+ </p>
219
+ </div>
220
+ ) : (
221
+ <div className="space-y-3">
222
+ {previewEntities.map((entity, index) => (
223
+ <EntityCard
224
+ key={index}
225
+ entity={entity}
226
+ renderEntity={renderEntity}
227
+ />
228
+ ))}
229
+ {totalMatched > maxPreviewEntities && (
230
+ <div className="text-center py-2 text-sm text-gray-500">
231
+ + {(totalMatched - maxPreviewEntities).toLocaleString()} more...
232
+ </div>
233
+ )}
234
+ </div>
235
+ )}
236
+ </div>
237
+
238
+ {/* Loading overlay */}
239
+ {isComputing && (
240
+ <div className="absolute inset-0 bg-white bg-opacity-75 flex items-center justify-center rounded-lg">
241
+ <div className="flex items-center gap-2 text-gray-600">
242
+ <div className="w-5 h-5 border-2 border-gray-300 border-t-blue-600 rounded-full animate-spin" />
243
+ <span className="text-sm font-medium">Updating preview...</span>
244
+ </div>
245
+ </div>
246
+ )}
247
+ </div>
248
+ );
249
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * LoadingPreview Component
3
+ *
4
+ * Skeleton loading state for FunnelPreview.
5
+ *
6
+ * Design Rationale:
7
+ * - Skeleton shapes match actual content layout
8
+ * - Pulse animation indicates loading
9
+ * - Gray placeholders maintain visual hierarchy
10
+ * - Reduces perceived loading time
11
+ *
12
+ * Accessibility:
13
+ * - ARIA live region announces loading state
14
+ * - Screen readers informed of status change
15
+ */
16
+
17
+ export function LoadingPreview() {
18
+ return (
19
+ <div className="animate-pulse" role="status" aria-live="polite">
20
+ <span className="sr-only">Loading preview...</span>
21
+
22
+ {/* Stats bar skeleton */}
23
+ <div className="space-y-2 mb-6">
24
+ <div className="h-8 bg-gray-200 rounded-lg" />
25
+ <div className="flex justify-between">
26
+ <div className="h-4 w-32 bg-gray-200 rounded" />
27
+ <div className="h-4 w-32 bg-gray-200 rounded" />
28
+ </div>
29
+ </div>
30
+
31
+ {/* Stage breakdown skeleton */}
32
+ <div className="mb-6">
33
+ <div className="h-5 w-40 bg-gray-200 rounded mb-3" />
34
+ <div className="space-y-2">
35
+ {[1, 2, 3].map((i) => (
36
+ <div
37
+ key={i}
38
+ className="h-12 bg-gray-100 rounded-lg flex items-center px-3 gap-3"
39
+ >
40
+ <div className="w-6 h-6 bg-gray-200 rounded-full" />
41
+ <div className="h-4 bg-gray-200 rounded flex-1" />
42
+ <div className="h-4 w-16 bg-gray-200 rounded" />
43
+ </div>
44
+ ))}
45
+ </div>
46
+ </div>
47
+
48
+ {/* Entity cards skeleton */}
49
+ <div className="space-y-3">
50
+ <div className="h-5 w-48 bg-gray-200 rounded mb-3" />
51
+ {[1, 2, 3].map((i) => (
52
+ <div key={i} className="h-20 bg-gray-100 border border-gray-200 rounded-lg p-3">
53
+ <div className="h-4 bg-gray-200 rounded w-3/4 mb-2" />
54
+ <div className="h-3 bg-gray-200 rounded w-1/2" />
55
+ </div>
56
+ ))}
57
+ </div>
58
+ </div>
59
+ );
60
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * PreviewStats Component
3
+ *
4
+ * Progress bar showing matched vs excluded entities with percentage.
5
+ *
6
+ * Design Rationale:
7
+ * - Horizontal progress bar shows match rate at a glance
8
+ * - Green (matched) and red (excluded) create clear visual distinction
9
+ * - Percentage provides quick understanding of funnel effectiveness
10
+ * - Counts below bar provide exact numbers
11
+ *
12
+ * Accessibility:
13
+ * - ARIA progressbar role for screen readers
14
+ * - Text labels provide non-visual indication
15
+ * - Sufficient color contrast
16
+ */
17
+
18
+ interface PreviewStatsProps {
19
+ totalMatched: number;
20
+ totalExcluded: number;
21
+ matchPercentage: number;
22
+ className?: string;
23
+ }
24
+
25
+ export function PreviewStats({
26
+ totalMatched,
27
+ totalExcluded,
28
+ matchPercentage,
29
+ className = '',
30
+ }: PreviewStatsProps) {
31
+ const total = totalMatched + totalExcluded;
32
+
33
+ return (
34
+ <div className={`space-y-2 ${className}`}>
35
+ {/* Progress bar */}
36
+ <div className="relative h-8 bg-gray-200 rounded-lg overflow-hidden">
37
+ <div
38
+ className="absolute inset-y-0 left-0 bg-gradient-to-r from-green-500 to-green-600 transition-all duration-300 flex items-center justify-center"
39
+ style={{ width: `${matchPercentage}%` }}
40
+ role="progressbar"
41
+ aria-valuenow={matchPercentage}
42
+ aria-valuemin={0}
43
+ aria-valuemax={100}
44
+ aria-label={`${totalMatched} of ${total} matched (${matchPercentage}%)`}
45
+ >
46
+ {matchPercentage > 15 && (
47
+ <span className="text-xs font-semibold text-white">
48
+ {totalMatched.toLocaleString()}/{total.toLocaleString()} ({matchPercentage}%)
49
+ </span>
50
+ )}
51
+ </div>
52
+ {matchPercentage <= 15 && (
53
+ <div className="absolute inset-0 flex items-center justify-center">
54
+ <span className="text-xs font-semibold text-gray-600">
55
+ {totalMatched.toLocaleString()}/{total.toLocaleString()} ({matchPercentage}%)
56
+ </span>
57
+ </div>
58
+ )}
59
+ </div>
60
+
61
+ {/* Match/Exclude counts */}
62
+ <div className="flex items-center justify-between text-sm">
63
+ <div className="flex items-center gap-1.5">
64
+ <div className="w-3 h-3 rounded-sm bg-green-500" />
65
+ <span className="font-medium text-gray-700">
66
+ {totalMatched.toLocaleString()} Matched
67
+ </span>
68
+ </div>
69
+ <div className="flex items-center gap-1.5">
70
+ <div className="w-3 h-3 rounded-sm bg-gray-300" />
71
+ <span className="font-medium text-gray-700">
72
+ {totalExcluded.toLocaleString()} Excluded
73
+ </span>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ );
78
+ }