@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.
- 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 -3243
- 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 -3196
- 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 -21
- 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 -19
- 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 -391
- 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 -388
- 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
|
+
}
|