@startsimpli/funnels 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/package.json +9 -31
  2. package/src/api/README.md +507 -0
  3. package/src/api/adapter.ts +106 -0
  4. package/src/api/client.test.ts +640 -0
  5. package/src/api/client.ts +385 -0
  6. package/src/api/default-adapter.ts +243 -0
  7. package/src/api/index.ts +24 -0
  8. package/src/components/FilterRuleEditor/ARCHITECTURE.md +354 -0
  9. package/src/components/FilterRuleEditor/FieldSelector.tsx +91 -0
  10. package/src/components/FilterRuleEditor/FilterRuleEditor.stories.tsx +462 -0
  11. package/src/components/FilterRuleEditor/FilterRuleEditor.test.tsx +520 -0
  12. package/src/components/FilterRuleEditor/FilterRuleEditor.tsx +225 -0
  13. package/src/components/FilterRuleEditor/LogicToggle.tsx +64 -0
  14. package/src/components/FilterRuleEditor/OperatorSelector.tsx +75 -0
  15. package/src/components/FilterRuleEditor/README.md +291 -0
  16. package/src/components/FilterRuleEditor/RuleRow.tsx +246 -0
  17. package/src/components/FilterRuleEditor/ValueInputs/BooleanValueInput.tsx +54 -0
  18. package/src/components/FilterRuleEditor/ValueInputs/ChoiceValueInput.tsx +83 -0
  19. package/src/components/FilterRuleEditor/ValueInputs/DateValueInput.tsx +70 -0
  20. package/src/components/FilterRuleEditor/ValueInputs/MultiChoiceValueInput.tsx +132 -0
  21. package/src/components/FilterRuleEditor/ValueInputs/NumberValueInput.tsx +73 -0
  22. package/src/components/FilterRuleEditor/ValueInputs/TextValueInput.tsx +50 -0
  23. package/src/components/FilterRuleEditor/ValueInputs/index.ts +12 -0
  24. package/src/components/FilterRuleEditor/constants.ts +64 -0
  25. package/src/components/FilterRuleEditor/index.ts +14 -0
  26. package/src/components/FunnelCard/DESIGN.md +447 -0
  27. package/src/components/FunnelCard/FunnelCard.stories.tsx +484 -0
  28. package/src/components/FunnelCard/FunnelCard.test.ts +257 -0
  29. package/src/components/FunnelCard/FunnelCard.test.tsx +336 -0
  30. package/src/components/FunnelCard/FunnelCard.tsx +204 -0
  31. package/src/components/FunnelCard/FunnelStats.tsx +68 -0
  32. package/src/components/FunnelCard/IMPLEMENTATION_SUMMARY.md +505 -0
  33. package/src/components/FunnelCard/INSTALLATION.md +304 -0
  34. package/src/components/FunnelCard/MatchBar.tsx +49 -0
  35. package/src/components/FunnelCard/README.md +294 -0
  36. package/src/components/FunnelCard/StageIndicator.tsx +62 -0
  37. package/src/components/FunnelCard/StatusBadge.tsx +52 -0
  38. package/src/components/FunnelCard/index.ts +14 -0
  39. package/src/components/FunnelPreview/EntityCard.tsx +72 -0
  40. package/src/components/FunnelPreview/FunnelPreview.stories.tsx +227 -0
  41. package/src/components/FunnelPreview/FunnelPreview.test.tsx +316 -0
  42. package/src/components/FunnelPreview/FunnelPreview.tsx +249 -0
  43. package/src/components/FunnelPreview/LoadingPreview.tsx +60 -0
  44. package/src/components/FunnelPreview/PreviewStats.tsx +78 -0
  45. package/src/components/FunnelPreview/README.md +337 -0
  46. package/src/components/FunnelPreview/StageBreakdown.tsx +94 -0
  47. package/src/components/FunnelPreview/example.tsx +286 -0
  48. package/src/components/FunnelPreview/index.ts +14 -0
  49. package/src/components/FunnelRunHistory/COMPONENT_SUMMARY.md +246 -0
  50. package/src/components/FunnelRunHistory/FunnelRunHistory.stories.tsx +272 -0
  51. package/src/components/FunnelRunHistory/FunnelRunHistory.test.tsx +323 -0
  52. package/src/components/FunnelRunHistory/FunnelRunHistory.tsx +329 -0
  53. package/src/components/FunnelRunHistory/README.md +325 -0
  54. package/src/components/FunnelRunHistory/RunActions.tsx +168 -0
  55. package/src/components/FunnelRunHistory/RunDetailsModal.tsx +221 -0
  56. package/src/components/FunnelRunHistory/RunFilters.tsx +128 -0
  57. package/src/components/FunnelRunHistory/RunRow.tsx +122 -0
  58. package/src/components/FunnelRunHistory/RunStatusBadge.tsx +75 -0
  59. package/src/components/FunnelRunHistory/StageBreakdownList.tsx +110 -0
  60. package/src/components/FunnelRunHistory/index.ts +51 -0
  61. package/src/components/FunnelRunHistory/types.ts +40 -0
  62. package/src/components/FunnelRunHistory/utils.test.ts +126 -0
  63. package/src/components/FunnelRunHistory/utils.ts +100 -0
  64. package/src/components/FunnelStageBuilder/AddStageButton.tsx +52 -0
  65. package/src/components/FunnelStageBuilder/FunnelStageBuilder.css +413 -0
  66. package/src/components/FunnelStageBuilder/FunnelStageBuilder.stories.tsx +312 -0
  67. package/src/components/FunnelStageBuilder/FunnelStageBuilder.test.tsx +304 -0
  68. package/src/components/FunnelStageBuilder/FunnelStageBuilder.tsx +321 -0
  69. package/src/components/FunnelStageBuilder/README.md +341 -0
  70. package/src/components/FunnelStageBuilder/StageActions.test.tsx +205 -0
  71. package/src/components/FunnelStageBuilder/StageActions.tsx +126 -0
  72. package/src/components/FunnelStageBuilder/StageCard.tsx +202 -0
  73. package/src/components/FunnelStageBuilder/StageForm.tsx +262 -0
  74. package/src/components/FunnelStageBuilder/TagInput.test.tsx +178 -0
  75. package/src/components/FunnelStageBuilder/TagInput.tsx +129 -0
  76. package/src/components/FunnelStageBuilder/index.ts +21 -0
  77. package/src/components/FunnelVisualFlow/FlowLegend.tsx +77 -0
  78. package/{dist/components/index.css → src/components/FunnelVisualFlow/FunnelVisualFlow.css} +89 -13
  79. package/src/components/FunnelVisualFlow/FunnelVisualFlow.stories.tsx +254 -0
  80. package/src/components/FunnelVisualFlow/FunnelVisualFlow.test.tsx +208 -0
  81. package/src/components/FunnelVisualFlow/FunnelVisualFlow.tsx +229 -0
  82. package/src/components/FunnelVisualFlow/README.md +323 -0
  83. package/src/components/FunnelVisualFlow/StageNode.tsx +188 -0
  84. package/src/components/FunnelVisualFlow/example.tsx +227 -0
  85. package/src/components/FunnelVisualFlow/index.ts +10 -0
  86. package/src/components/index.ts +102 -0
  87. package/src/core/README.md +307 -0
  88. package/src/core/engine.test.ts +1087 -0
  89. package/src/core/engine.ts +329 -0
  90. package/src/core/evaluator.example.ts +353 -0
  91. package/src/core/evaluator.test.ts +639 -0
  92. package/src/core/evaluator.ts +261 -0
  93. package/src/core/field-resolver.example.ts +175 -0
  94. package/src/core/field-resolver.test.ts +541 -0
  95. package/src/core/field-resolver.ts +247 -0
  96. package/src/core/index.ts +34 -0
  97. package/src/core/operators.test.ts +539 -0
  98. package/src/core/operators.ts +241 -0
  99. package/src/hooks/index.ts +5 -0
  100. package/src/hooks/useDebouncedValue.ts +28 -0
  101. package/src/index.ts +155 -0
  102. package/src/store/README.md +342 -0
  103. package/src/store/create-funnel-store.test.ts +686 -0
  104. package/src/store/create-funnel-store.ts +538 -0
  105. package/src/store/index.ts +9 -0
  106. package/src/store/types.ts +294 -0
  107. package/src/stories/CrossDomain.stories.tsx +149 -0
  108. package/src/stories/Welcome.stories.tsx +81 -0
  109. package/src/stories/demo-data/index.ts +3 -0
  110. package/src/stories/demo-data/investors.ts +216 -0
  111. package/src/stories/demo-data/leads.ts +223 -0
  112. package/src/stories/demo-data/recipes.ts +217 -0
  113. package/src/test/setup.ts +5 -0
  114. package/src/types/index.ts +843 -0
  115. package/dist/client-3ESO2NHy.d.ts +0 -310
  116. package/dist/client-CZu03ACp.d.cts +0 -310
  117. package/dist/components/index.cjs +0 -3243
  118. package/dist/components/index.cjs.map +0 -1
  119. package/dist/components/index.css.map +0 -1
  120. package/dist/components/index.d.cts +0 -726
  121. package/dist/components/index.d.ts +0 -726
  122. package/dist/components/index.js +0 -3196
  123. package/dist/components/index.js.map +0 -1
  124. package/dist/core/index.cjs +0 -500
  125. package/dist/core/index.cjs.map +0 -1
  126. package/dist/core/index.d.cts +0 -359
  127. package/dist/core/index.d.ts +0 -359
  128. package/dist/core/index.js +0 -486
  129. package/dist/core/index.js.map +0 -1
  130. package/dist/hooks/index.cjs +0 -21
  131. package/dist/hooks/index.cjs.map +0 -1
  132. package/dist/hooks/index.d.cts +0 -11
  133. package/dist/hooks/index.d.ts +0 -11
  134. package/dist/hooks/index.js +0 -19
  135. package/dist/hooks/index.js.map +0 -1
  136. package/dist/index-BGDEXbuz.d.cts +0 -434
  137. package/dist/index-BGDEXbuz.d.ts +0 -434
  138. package/dist/index.cjs +0 -4499
  139. package/dist/index.cjs.map +0 -1
  140. package/dist/index.css +0 -198
  141. package/dist/index.css.map +0 -1
  142. package/dist/index.d.cts +0 -99
  143. package/dist/index.d.ts +0 -99
  144. package/dist/index.js +0 -4421
  145. package/dist/index.js.map +0 -1
  146. package/dist/store/index.cjs +0 -391
  147. package/dist/store/index.cjs.map +0 -1
  148. package/dist/store/index.d.cts +0 -225
  149. package/dist/store/index.d.ts +0 -225
  150. package/dist/store/index.js +0 -388
  151. package/dist/store/index.js.map +0 -1
@@ -0,0 +1,128 @@
1
+ /**
2
+ * RunFilters Component
3
+ *
4
+ * Filter controls for funnel run history.
5
+ *
6
+ * Design Rationale:
7
+ * - Inline filter controls for immediate feedback
8
+ * - Dropdown selects for constrained options
9
+ * - Date range picker for flexible filtering
10
+ * - Clear visual separation from table
11
+ *
12
+ * Interaction Notes:
13
+ * - Filters apply immediately on change (no "Apply" button)
14
+ * - Default to "Last 30 days" for performance
15
+ * - Clear button resets all filters
16
+ */
17
+
18
+ import { FunnelRunStatus, TriggerType } from '../../types';
19
+ import { RunFilters as RunFiltersType } from './types';
20
+
21
+ interface RunFiltersProps {
22
+ filters: RunFiltersType;
23
+ onFiltersChange: (filters: RunFiltersType) => void;
24
+ className?: string;
25
+ }
26
+
27
+ export function RunFilters({
28
+ filters,
29
+ onFiltersChange,
30
+ className = '',
31
+ }: RunFiltersProps) {
32
+ const updateFilter = <K extends keyof RunFiltersType>(
33
+ key: K,
34
+ value: RunFiltersType[K]
35
+ ) => {
36
+ onFiltersChange({ ...filters, [key]: value });
37
+ };
38
+
39
+ const clearFilters = () => {
40
+ onFiltersChange({
41
+ status: 'all',
42
+ trigger_type: 'all',
43
+ date_range: 'month',
44
+ });
45
+ };
46
+
47
+ const hasActiveFilters =
48
+ filters.status !== 'all' ||
49
+ filters.trigger_type !== 'all' ||
50
+ filters.date_range !== 'month';
51
+
52
+ return (
53
+ <div
54
+ className={`flex items-center gap-3 p-3 bg-gray-50 border-b border-gray-200 ${className}`}
55
+ >
56
+ {/* Status Filter */}
57
+ <div className="flex items-center gap-2">
58
+ <label htmlFor="status-filter" className="text-sm font-medium text-gray-700">
59
+ Status:
60
+ </label>
61
+ <select
62
+ id="status-filter"
63
+ value={filters.status || 'all'}
64
+ onChange={(e) =>
65
+ updateFilter('status', e.target.value as FunnelRunStatus | 'all')
66
+ }
67
+ className="px-2 py-1 text-sm border border-gray-300 rounded-md bg-white focus:outline-none focus:ring-2 focus:ring-blue-500"
68
+ >
69
+ <option value="all">All</option>
70
+ <option value="completed">Complete</option>
71
+ <option value="running">Running</option>
72
+ <option value="failed">Failed</option>
73
+ <option value="pending">Pending</option>
74
+ <option value="cancelled">Cancelled</option>
75
+ </select>
76
+ </div>
77
+
78
+ {/* Trigger Type Filter */}
79
+ <div className="flex items-center gap-2">
80
+ <label htmlFor="trigger-filter" className="text-sm font-medium text-gray-700">
81
+ Trigger:
82
+ </label>
83
+ <select
84
+ id="trigger-filter"
85
+ value={filters.trigger_type || 'all'}
86
+ onChange={(e) =>
87
+ updateFilter('trigger_type', e.target.value as TriggerType | 'all')
88
+ }
89
+ className="px-2 py-1 text-sm border border-gray-300 rounded-md bg-white focus:outline-none focus:ring-2 focus:ring-blue-500"
90
+ >
91
+ <option value="all">All</option>
92
+ <option value="manual">Manual</option>
93
+ <option value="scheduled">Scheduled</option>
94
+ <option value="webhook">Webhook</option>
95
+ <option value="api">API</option>
96
+ </select>
97
+ </div>
98
+
99
+ {/* Date Range Filter */}
100
+ <div className="flex items-center gap-2">
101
+ <label htmlFor="date-filter" className="text-sm font-medium text-gray-700">
102
+ Date:
103
+ </label>
104
+ <select
105
+ id="date-filter"
106
+ value={filters.date_range || 'month'}
107
+ onChange={(e) => updateFilter('date_range', e.target.value as any)}
108
+ className="px-2 py-1 text-sm border border-gray-300 rounded-md bg-white focus:outline-none focus:ring-2 focus:ring-blue-500"
109
+ >
110
+ <option value="all">All time</option>
111
+ <option value="today">Today</option>
112
+ <option value="week">Last 7 days</option>
113
+ <option value="month">Last 30 days</option>
114
+ </select>
115
+ </div>
116
+
117
+ {/* Clear Button */}
118
+ {hasActiveFilters && (
119
+ <button
120
+ onClick={clearFilters}
121
+ className="ml-auto px-3 py-1 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500"
122
+ >
123
+ Clear filters
124
+ </button>
125
+ )}
126
+ </div>
127
+ );
128
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * RunRow Component
3
+ *
4
+ * Individual table row for a funnel run.
5
+ *
6
+ * Design Rationale:
7
+ * - Clickable row opens details modal
8
+ * - Hover state provides visual feedback
9
+ * - Action button visible on hover for cleaner UI
10
+ * - Tooltip on date shows full timestamp
11
+ *
12
+ * Interaction Notes:
13
+ * - Click anywhere on row to view details
14
+ * - Action dropdown prevents row click propagation
15
+ * - Keyboard accessible (Enter to open details)
16
+ */
17
+
18
+ import { FunnelRun } from '../../types';
19
+ import { RunStatusBadge } from './RunStatusBadge';
20
+ import { RunActions } from './RunActions';
21
+ import {
22
+ formatDuration,
23
+ formatRelativeTime,
24
+ formatFullTimestamp,
25
+ calculateMatchRate,
26
+ formatNumber,
27
+ } from './utils';
28
+
29
+ interface RunRowProps {
30
+ run: FunnelRun;
31
+ onViewDetails: (run: FunnelRun) => void;
32
+ onViewResults: (run: FunnelRun) => void;
33
+ onReRun: (run: FunnelRun) => void;
34
+ onCancel?: (run: FunnelRun) => void;
35
+ }
36
+
37
+ export function RunRow({
38
+ run,
39
+ onViewDetails,
40
+ onViewResults,
41
+ onReRun,
42
+ onCancel,
43
+ }: RunRowProps) {
44
+ const matchRate =
45
+ run.status === 'completed'
46
+ ? calculateMatchRate(run.total_matched, run.total_input)
47
+ : null;
48
+
49
+ return (
50
+ <tr
51
+ onClick={() => onViewDetails(run)}
52
+ onKeyDown={(e) => {
53
+ if (e.key === 'Enter') {
54
+ onViewDetails(run);
55
+ }
56
+ }}
57
+ tabIndex={0}
58
+ className="border-b border-gray-200 hover:bg-gray-50 cursor-pointer transition-colors focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500"
59
+ >
60
+ {/* Date */}
61
+ <td className="px-4 py-3">
62
+ <span
63
+ className="text-sm text-gray-900"
64
+ title={formatFullTimestamp(run.started_at)}
65
+ >
66
+ {formatRelativeTime(run.started_at)}
67
+ </span>
68
+ </td>
69
+
70
+ {/* Status */}
71
+ <td className="px-4 py-3">
72
+ <RunStatusBadge status={run.status} />
73
+ </td>
74
+
75
+ {/* Trigger */}
76
+ <td className="px-4 py-3">
77
+ <span className="text-sm text-gray-700 capitalize">
78
+ {run.trigger_type}
79
+ </span>
80
+ </td>
81
+
82
+ {/* Duration */}
83
+ <td className="px-4 py-3">
84
+ <span className="text-sm text-gray-900">
85
+ {formatDuration(run.duration_ms)}
86
+ </span>
87
+ </td>
88
+
89
+ {/* Input */}
90
+ <td className="px-4 py-3 text-right">
91
+ <span className="text-sm font-medium text-gray-900">
92
+ {formatNumber(run.total_input)}
93
+ </span>
94
+ </td>
95
+
96
+ {/* Matched */}
97
+ <td className="px-4 py-3 text-right">
98
+ <span className="text-sm font-medium text-green-600">
99
+ {run.status === 'completed' ? formatNumber(run.total_matched) : '-'}
100
+ </span>
101
+ </td>
102
+
103
+ {/* Match Rate % */}
104
+ <td className="px-4 py-3 text-right">
105
+ <span className="text-sm font-medium text-gray-900">
106
+ {matchRate !== null ? `${matchRate}%` : '-'}
107
+ </span>
108
+ </td>
109
+
110
+ {/* Actions */}
111
+ <td className="px-4 py-3" onClick={(e) => e.stopPropagation()}>
112
+ <RunActions
113
+ run={run}
114
+ onViewDetails={onViewDetails}
115
+ onViewResults={onViewResults}
116
+ onReRun={onReRun}
117
+ onCancel={onCancel}
118
+ />
119
+ </td>
120
+ </tr>
121
+ );
122
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * RunStatusBadge Component
3
+ *
4
+ * Displays funnel run status with icon and color coding.
5
+ *
6
+ * Design Rationale:
7
+ * - Icons provide instant visual recognition
8
+ * - Color coding reinforces status meaning
9
+ * - Spinning animation for active states
10
+ * - Accessible with both visual and text indicators
11
+ */
12
+
13
+ import { FunnelRunStatus } from '../../types';
14
+
15
+ interface RunStatusBadgeProps {
16
+ status: FunnelRunStatus;
17
+ className?: string;
18
+ }
19
+
20
+ interface StatusConfig {
21
+ icon: string;
22
+ label: string;
23
+ color: string;
24
+ bgColor: string;
25
+ spinning?: boolean;
26
+ }
27
+
28
+ const statusConfig: Record<FunnelRunStatus, StatusConfig> = {
29
+ completed: {
30
+ icon: '✓',
31
+ label: 'Complete',
32
+ color: 'text-green-800',
33
+ bgColor: 'bg-green-100',
34
+ },
35
+ running: {
36
+ icon: '⏸',
37
+ label: 'Running',
38
+ color: 'text-blue-800',
39
+ bgColor: 'bg-blue-100',
40
+ spinning: true,
41
+ },
42
+ failed: {
43
+ icon: '✗',
44
+ label: 'Failed',
45
+ color: 'text-red-800',
46
+ bgColor: 'bg-red-100',
47
+ },
48
+ pending: {
49
+ icon: '○',
50
+ label: 'Pending',
51
+ color: 'text-yellow-800',
52
+ bgColor: 'bg-yellow-100',
53
+ },
54
+ cancelled: {
55
+ icon: '×',
56
+ label: 'Cancelled',
57
+ color: 'text-gray-800',
58
+ bgColor: 'bg-gray-100',
59
+ },
60
+ };
61
+
62
+ export function RunStatusBadge({ status, className = '' }: RunStatusBadgeProps) {
63
+ const config = statusConfig[status];
64
+
65
+ return (
66
+ <span
67
+ className={`inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium ${config.bgColor} ${config.color} ${className}`}
68
+ >
69
+ <span className={config.spinning ? 'animate-spin' : ''} aria-hidden="true">
70
+ {config.icon}
71
+ </span>
72
+ <span>{config.label}</span>
73
+ </span>
74
+ );
75
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * StageBreakdownList Component
3
+ *
4
+ * Displays stage-by-stage execution statistics.
5
+ *
6
+ * Design Rationale:
7
+ * - Sequential numbering shows funnel flow
8
+ * - Input → Matched with delta shows filtering effect
9
+ * - Color coding: green for matched, red for excluded
10
+ * - Arrow symbols reinforce flow direction
11
+ *
12
+ * Visual Hierarchy:
13
+ * - Stage name is prominent
14
+ * - Numbers are large and easy to scan
15
+ * - Delta shows impact at each stage
16
+ */
17
+
18
+ import { StageStats } from '../../types';
19
+ import { formatNumber } from './utils';
20
+
21
+ interface StageBreakdownListProps {
22
+ stages: StageStats[];
23
+ className?: string;
24
+ }
25
+
26
+ export function StageBreakdownList({
27
+ stages,
28
+ className = '',
29
+ }: StageBreakdownListProps) {
30
+ return (
31
+ <div className={`space-y-3 ${className}`}>
32
+ {stages.map((stage, index) => {
33
+ const delta = stage.matched_count - stage.input_count;
34
+ const matchRate =
35
+ stage.input_count > 0
36
+ ? Math.round((stage.matched_count / stage.input_count) * 100)
37
+ : 0;
38
+
39
+ return (
40
+ <div
41
+ key={stage.stage_id}
42
+ className="p-3 bg-gray-50 rounded-lg border border-gray-200"
43
+ >
44
+ {/* Stage Header */}
45
+ <div className="flex items-center gap-2 mb-2">
46
+ <span className="flex items-center justify-center w-6 h-6 text-xs font-bold text-white bg-blue-600 rounded-full">
47
+ {index + 1}
48
+ </span>
49
+ <h4 className="text-sm font-semibold text-gray-900">
50
+ {stage.stage_name}
51
+ </h4>
52
+ </div>
53
+
54
+ {/* Stage Stats */}
55
+ <div className="grid grid-cols-3 gap-2 text-center">
56
+ {/* Input */}
57
+ <div>
58
+ <div className="text-xs text-gray-600 mb-1">Input</div>
59
+ <div className="text-lg font-bold text-blue-600">
60
+ {formatNumber(stage.input_count)}
61
+ </div>
62
+ </div>
63
+
64
+ {/* Matched */}
65
+ <div>
66
+ <div className="text-xs text-gray-600 mb-1">Matched</div>
67
+ <div className="text-lg font-bold text-green-600">
68
+ {formatNumber(stage.matched_count)}
69
+ </div>
70
+ </div>
71
+
72
+ {/* Match Rate */}
73
+ <div>
74
+ <div className="text-xs text-gray-600 mb-1">Rate</div>
75
+ <div className="text-lg font-bold text-gray-700">{matchRate}%</div>
76
+ </div>
77
+ </div>
78
+
79
+ {/* Delta */}
80
+ {delta !== 0 && (
81
+ <div className="mt-2 pt-2 border-t border-gray-200">
82
+ <div className="flex items-center justify-center gap-1 text-sm">
83
+ <span
84
+ className={`font-medium ${
85
+ delta > 0 ? 'text-green-600' : 'text-red-600'
86
+ }`}
87
+ >
88
+ {delta > 0 ? '▲' : '▼'} {formatNumber(Math.abs(delta))}
89
+ </span>
90
+ <span className="text-gray-500">
91
+ {delta > 0 ? 'added' : 'excluded'}
92
+ </span>
93
+ </div>
94
+ </div>
95
+ )}
96
+
97
+ {/* Error Count */}
98
+ {stage.error_count && stage.error_count > 0 && (
99
+ <div className="mt-2 pt-2 border-t border-gray-200">
100
+ <div className="text-sm text-red-600 text-center">
101
+ ⚠ {formatNumber(stage.error_count)} errors
102
+ </div>
103
+ </div>
104
+ )}
105
+ </div>
106
+ );
107
+ })}
108
+ </div>
109
+ );
110
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * FunnelRunHistory - Component Suite
3
+ *
4
+ * Comprehensive run history display with filtering, sorting, and detailed views.
5
+ *
6
+ * Primary Export:
7
+ * - FunnelRunHistory: Main component
8
+ *
9
+ * Sub-Components (for custom layouts):
10
+ * - RunStatusBadge: Status display
11
+ * - RunFilters: Filter controls
12
+ * - RunRow: Table row
13
+ * - RunActions: Action dropdown
14
+ * - RunDetailsModal: Details modal
15
+ * - StageBreakdownList: Stage statistics
16
+ *
17
+ * Utilities:
18
+ * - formatDuration: Format ms to human-readable
19
+ * - formatRelativeTime: Format date to relative time
20
+ * - calculateMatchRate: Calculate percentage
21
+ * - formatNumber: Format with commas
22
+ *
23
+ * Types:
24
+ * - RunFilters: Filter state type
25
+ * - RunSort: Sort configuration
26
+ * - Pagination: Pagination state
27
+ * - RunAction: Action types
28
+ */
29
+
30
+ export { FunnelRunHistory } from './FunnelRunHistory';
31
+ export { RunStatusBadge } from './RunStatusBadge';
32
+ export { RunFilters } from './RunFilters';
33
+ export { RunRow } from './RunRow';
34
+ export { RunActions } from './RunActions';
35
+ export { RunDetailsModal } from './RunDetailsModal';
36
+ export { StageBreakdownList } from './StageBreakdownList';
37
+
38
+ export {
39
+ formatDuration,
40
+ formatRelativeTime,
41
+ calculateMatchRate,
42
+ formatNumber,
43
+ formatFullTimestamp,
44
+ } from './utils';
45
+
46
+ export type {
47
+ RunFilters as RunFiltersType,
48
+ RunSort,
49
+ Pagination,
50
+ RunAction,
51
+ } from './types';
@@ -0,0 +1,40 @@
1
+ /**
2
+ * FunnelRunHistory Types
3
+ *
4
+ * Shared types for the run history component suite
5
+ */
6
+
7
+ import { FunnelRun, FunnelRunStatus, TriggerType } from '../../types';
8
+
9
+ /**
10
+ * Filter state for run history
11
+ */
12
+ export interface RunFilters {
13
+ status?: FunnelRunStatus | 'all';
14
+ trigger_type?: TriggerType | 'all';
15
+ date_range?: 'all' | 'today' | 'week' | 'month' | 'custom';
16
+ start_date?: Date;
17
+ end_date?: Date;
18
+ }
19
+
20
+ /**
21
+ * Sort configuration for run history
22
+ */
23
+ export interface RunSort {
24
+ field: 'started_at' | 'status' | 'match_rate' | 'duration_ms';
25
+ direction: 'asc' | 'desc';
26
+ }
27
+
28
+ /**
29
+ * Pagination state
30
+ */
31
+ export interface Pagination {
32
+ page: number;
33
+ page_size: number;
34
+ total: number;
35
+ }
36
+
37
+ /**
38
+ * Run action types
39
+ */
40
+ export type RunAction = 'view_details' | 'view_results' | 're_run' | 'cancel';
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Utils Tests
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import {
7
+ formatDuration,
8
+ formatRelativeTime,
9
+ calculateMatchRate,
10
+ formatNumber,
11
+ formatFullTimestamp,
12
+ } from './utils';
13
+
14
+ describe('formatDuration', () => {
15
+ it('should format milliseconds', () => {
16
+ expect(formatDuration(500)).toBe('500ms');
17
+ expect(formatDuration(999)).toBe('999ms');
18
+ });
19
+
20
+ it('should format seconds', () => {
21
+ expect(formatDuration(1000)).toBe('1s');
22
+ expect(formatDuration(5000)).toBe('5s');
23
+ expect(formatDuration(59000)).toBe('59s');
24
+ });
25
+
26
+ it('should format minutes and seconds', () => {
27
+ expect(formatDuration(60000)).toBe('1m');
28
+ expect(formatDuration(65000)).toBe('1m 5s');
29
+ expect(formatDuration(125000)).toBe('2m 5s');
30
+ });
31
+
32
+ it('should format hours and minutes', () => {
33
+ expect(formatDuration(3600000)).toBe('1h');
34
+ expect(formatDuration(3660000)).toBe('1h 1m');
35
+ expect(formatDuration(7320000)).toBe('2h 2m');
36
+ });
37
+
38
+ it('should return "-" for undefined', () => {
39
+ expect(formatDuration(undefined)).toBe('-');
40
+ });
41
+
42
+ it('should handle zero duration', () => {
43
+ expect(formatDuration(0)).toBe('0ms');
44
+ });
45
+ });
46
+
47
+ describe('formatRelativeTime', () => {
48
+ it('should format recent times', () => {
49
+ const now = new Date();
50
+
51
+ // Just now
52
+ const justNow = new Date(now.getTime() - 30000); // 30 seconds ago
53
+ expect(formatRelativeTime(justNow)).toBe('Just now');
54
+ });
55
+
56
+ it('should format minutes ago', () => {
57
+ const now = new Date();
58
+ const fiveMinAgo = new Date(now.getTime() - 5 * 60 * 1000);
59
+ expect(formatRelativeTime(fiveMinAgo)).toBe('5m ago');
60
+ });
61
+
62
+ it('should format hours ago', () => {
63
+ const now = new Date();
64
+ const twoHoursAgo = new Date(now.getTime() - 2 * 60 * 60 * 1000);
65
+ expect(formatRelativeTime(twoHoursAgo)).toBe('2h ago');
66
+ });
67
+
68
+ it('should format days ago', () => {
69
+ const now = new Date();
70
+ const threeDaysAgo = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000);
71
+ expect(formatRelativeTime(threeDaysAgo)).toBe('3d ago');
72
+ });
73
+
74
+ it('should handle string dates', () => {
75
+ const now = new Date();
76
+ const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
77
+ expect(formatRelativeTime(yesterday.toISOString())).toBe('1d ago');
78
+ });
79
+ });
80
+
81
+ describe('calculateMatchRate', () => {
82
+ it('should calculate percentage', () => {
83
+ expect(calculateMatchRate(50, 100)).toBe(50);
84
+ expect(calculateMatchRate(1, 3)).toBe(33);
85
+ expect(calculateMatchRate(2, 3)).toBe(67);
86
+ });
87
+
88
+ it('should handle zero total', () => {
89
+ expect(calculateMatchRate(0, 0)).toBe(0);
90
+ expect(calculateMatchRate(5, 0)).toBe(0);
91
+ });
92
+
93
+ it('should round to nearest integer', () => {
94
+ expect(calculateMatchRate(1, 3)).toBe(33); // 33.33... rounds to 33
95
+ expect(calculateMatchRate(2, 3)).toBe(67); // 66.66... rounds to 67
96
+ });
97
+ });
98
+
99
+ describe('formatNumber', () => {
100
+ it('should format with commas', () => {
101
+ expect(formatNumber(1000)).toBe('1,000');
102
+ expect(formatNumber(1234567)).toBe('1,234,567');
103
+ expect(formatNumber(999)).toBe('999');
104
+ });
105
+
106
+ it('should handle zero', () => {
107
+ expect(formatNumber(0)).toBe('0');
108
+ });
109
+ });
110
+
111
+ describe('formatFullTimestamp', () => {
112
+ it('should format date object', () => {
113
+ const date = new Date('2025-01-15T10:30:45Z');
114
+ const formatted = formatFullTimestamp(date);
115
+ // Format will vary by locale, just check it's a string
116
+ expect(typeof formatted).toBe('string');
117
+ expect(formatted.length).toBeGreaterThan(0);
118
+ });
119
+
120
+ it('should format ISO string', () => {
121
+ const isoString = '2025-01-15T10:30:45Z';
122
+ const formatted = formatFullTimestamp(isoString);
123
+ expect(typeof formatted).toBe('string');
124
+ expect(formatted.length).toBeGreaterThan(0);
125
+ });
126
+ });