@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,325 @@
1
+ # FunnelRunHistory Component Suite
2
+
3
+ Comprehensive funnel execution history display with filtering, sorting, pagination, and detailed breakdowns.
4
+
5
+ ## Features
6
+
7
+ - **Run Table** - Paginated list of all funnel executions
8
+ - **Filters** - Filter by status, trigger type, date range
9
+ - **Sorting** - Sorted by date (most recent first)
10
+ - **Auto-refresh** - Polls every 5 seconds when active runs exist
11
+ - **Details Modal** - Click row to see stage-by-stage breakdown
12
+ - **Actions** - Re-run, view results, cancel running jobs
13
+ - **Responsive** - Adapts to different screen sizes
14
+ - **Accessible** - Keyboard navigation, ARIA labels, screen reader support
15
+
16
+ ## Usage
17
+
18
+ ### Basic Usage
19
+
20
+ ```tsx
21
+ import { FunnelRunHistory } from '@simpli/funnels';
22
+ import { FunnelApiClient } from '@simpli/funnels';
23
+
24
+ function MyFunnelPage() {
25
+ const apiClient = new FunnelApiClient(adapter, 'https://api.example.com');
26
+
27
+ return (
28
+ <FunnelRunHistory
29
+ funnelId="funnel-123"
30
+ apiClient={apiClient}
31
+ />
32
+ );
33
+ }
34
+ ```
35
+
36
+ ### With Custom Result Handler
37
+
38
+ ```tsx
39
+ <FunnelRunHistory
40
+ funnelId="funnel-123"
41
+ apiClient={apiClient}
42
+ onViewResults={(run) => {
43
+ // Navigate to results page
44
+ router.push(`/funnels/${run.funnel_id}/runs/${run.id}/results`);
45
+ }}
46
+ />
47
+ ```
48
+
49
+ ## Component Architecture
50
+
51
+ ```
52
+ FunnelRunHistory (main)
53
+ ├── RunFilters (filter controls)
54
+ ├── RunRow (table row)
55
+ │ ├── RunStatusBadge (status display)
56
+ │ └── RunActions (action dropdown)
57
+ └── RunDetailsModal (details dialog)
58
+ └── StageBreakdownList (stage stats)
59
+ ```
60
+
61
+ ## Props
62
+
63
+ ### FunnelRunHistory
64
+
65
+ | Prop | Type | Required | Description |
66
+ |------|------|----------|-------------|
67
+ | `funnelId` | `string` | Yes | Funnel ID to load runs for |
68
+ | `apiClient` | `FunnelApiClient` | Yes | API client instance |
69
+ | `onViewResults` | `(run: FunnelRun) => void` | No | Custom handler for viewing results |
70
+ | `className` | `string` | No | Additional CSS classes |
71
+
72
+ ## Table Columns
73
+
74
+ | Column | Description | Format |
75
+ |--------|-------------|--------|
76
+ | **Date** | When run started | Relative (2h ago) with tooltip |
77
+ | **Status** | Run status | Badge with icon |
78
+ | **Trigger** | How run was triggered | Manual, Scheduled, API, Webhook |
79
+ | **Duration** | Execution time | 2.3s, 1m 23s |
80
+ | **Input** | Total entities processed | Formatted number (1,000) |
81
+ | **Matched** | Entities matched | Formatted number |
82
+ | **%** | Match rate | Percentage (24%) |
83
+ | **Actions** | Action dropdown | View, Re-run, Cancel |
84
+
85
+ ## Status Indicators
86
+
87
+ | Status | Icon | Color | Description |
88
+ |--------|------|-------|-------------|
89
+ | Complete | ✓ | Green | Successfully finished |
90
+ | Running | ⏸ | Blue | Currently executing (spinning) |
91
+ | Failed | ✗ | Red | Error occurred |
92
+ | Pending | ○ | Yellow | Queued, not started |
93
+ | Cancelled | × | Gray | Manually stopped |
94
+
95
+ ## Filters
96
+
97
+ ### Status Filter
98
+ - All (default)
99
+ - Complete
100
+ - Running
101
+ - Failed
102
+ - Pending
103
+ - Cancelled
104
+
105
+ ### Trigger Type Filter
106
+ - All (default)
107
+ - Manual
108
+ - Scheduled
109
+ - Webhook
110
+ - API
111
+
112
+ ### Date Range Filter
113
+ - All time
114
+ - Today
115
+ - Last 7 days
116
+ - Last 30 days (default)
117
+
118
+ ## Actions
119
+
120
+ ### View Details
121
+ Opens modal showing:
122
+ - Run summary (status, duration, trigger)
123
+ - Stage-by-stage breakdown
124
+ - Error messages (if failed)
125
+ - Actions (View Results, Re-run)
126
+
127
+ ### View Results
128
+ Calls `onViewResults` prop or opens details modal by default.
129
+
130
+ ### Re-run
131
+ Creates new run with same funnel configuration:
132
+ - Sets trigger_type to "manual"
133
+ - Adds metadata: `{ re_run_of: original_run_id }`
134
+ - Refreshes list to show new run
135
+
136
+ ### Cancel
137
+ Cancels running/pending jobs:
138
+ - Shows confirmation dialog
139
+ - Calls API to cancel
140
+ - Refreshes to show updated status
141
+ - Only available for running/pending runs
142
+
143
+ ## Auto-Refresh
144
+
145
+ The component automatically polls for updates when there are active runs:
146
+ - **Interval**: 5 seconds
147
+ - **Trigger**: Detects `pending` or `running` status
148
+ - **Cleanup**: Stops polling when no active runs
149
+ - **Manual**: Refresh button available
150
+
151
+ ## Stage Breakdown Modal
152
+
153
+ Click any row to open detailed view:
154
+
155
+ ```
156
+ ┌────────────────────────────────────────┐
157
+ │ Run Details - 2 hours ago │
158
+ │ ──────────────────────────────────────│
159
+ │ Status: Complete ✓ │
160
+ │ Duration: 2.3s │
161
+ │ Triggered by: John Doe (Manual) │
162
+ │ │
163
+ │ Stage Breakdown: │
164
+ │ ① High ICP Score │
165
+ │ Input: 1000 Matched: 500 ▼ -500 │
166
+ │ ② Frontend Stack │
167
+ │ Input: 500 Matched: 350 ▼ -150 │
168
+ │ │
169
+ │ [View Results] [Re-run] [Close] │
170
+ └────────────────────────────────────────┘
171
+ ```
172
+
173
+ ## Pagination
174
+
175
+ - **Page Size**: 10 runs per page (configurable)
176
+ - **Navigation**: Previous/Next buttons
177
+ - **Display**: "Showing 1-10 of 47"
178
+ - **State**: Preserved when filtering/refreshing
179
+
180
+ ## Responsive Behavior
181
+
182
+ ### Desktop (1024px+)
183
+ - Full table with all columns
184
+ - Action dropdown on hover
185
+ - Detailed tooltips
186
+
187
+ ### Tablet (768px-1023px)
188
+ - Hide duration and trigger columns
189
+ - Compact action button
190
+ - Simplified tooltips
191
+
192
+ ### Mobile (<768px)
193
+ - Card-based layout
194
+ - Stack information vertically
195
+ - Tap to expand details
196
+
197
+ ## Accessibility
198
+
199
+ ### Keyboard Navigation
200
+ - **Tab**: Navigate through rows and actions
201
+ - **Enter**: Open row details
202
+ - **Escape**: Close modal/dropdown
203
+ - **Arrow keys**: Navigate dropdown menu
204
+
205
+ ### Screen Readers
206
+ - ARIA labels on all interactive elements
207
+ - Status announcements for loading/errors
208
+ - Table headers properly associated
209
+ - Modal dialog attributes
210
+
211
+ ### Focus Management
212
+ - Focus trap in modal
213
+ - Return focus to trigger after close
214
+ - Visible focus indicators
215
+ - Skip to content links
216
+
217
+ ## Utility Functions
218
+
219
+ ### formatDuration
220
+ ```ts
221
+ formatDuration(2300) // "2.3s"
222
+ formatDuration(65000) // "1m 5s"
223
+ formatDuration(3661000) // "1h 1m"
224
+ ```
225
+
226
+ ### formatRelativeTime
227
+ ```ts
228
+ formatRelativeTime(fiveMinAgo) // "5m ago"
229
+ formatRelativeTime(yesterday) // "1d ago"
230
+ ```
231
+
232
+ ### calculateMatchRate
233
+ ```ts
234
+ calculateMatchRate(235, 1000) // 24
235
+ ```
236
+
237
+ ### formatNumber
238
+ ```ts
239
+ formatNumber(1234567) // "1,234,567"
240
+ ```
241
+
242
+ ## Testing
243
+
244
+ Run tests:
245
+ ```bash
246
+ npm run test
247
+ ```
248
+
249
+ Test files:
250
+ - `utils.test.ts` - Utility function tests
251
+ - `FunnelRunHistory.test.tsx` - Component logic tests
252
+
253
+ ## Examples
254
+
255
+ ### All Runs (No Filters)
256
+
257
+ ```tsx
258
+ <FunnelRunHistory
259
+ funnelId="funnel-123"
260
+ apiClient={apiClient}
261
+ />
262
+ ```
263
+
264
+ ### Only Failed Runs
265
+
266
+ Use the filter UI or implement custom filtering:
267
+
268
+ ```tsx
269
+ // User can filter via UI, or you can pre-filter server-side
270
+ // by creating a custom API call before passing to component
271
+ ```
272
+
273
+ ### Custom Results Page
274
+
275
+ ```tsx
276
+ <FunnelRunHistory
277
+ funnelId="funnel-123"
278
+ apiClient={apiClient}
279
+ onViewResults={(run) => {
280
+ // Custom navigation
281
+ navigate(`/runs/${run.id}/results`);
282
+ }}
283
+ />
284
+ ```
285
+
286
+ ## Generic Support
287
+
288
+ This component works with **any** funnel type:
289
+ - Investor funnels
290
+ - Recipe funnels
291
+ - Lead qualification funnels
292
+ - Task prioritization funnels
293
+ - Organization screening funnels
294
+
295
+ No domain-specific logic - purely generic entity processing.
296
+
297
+ ## Error Handling
298
+
299
+ - **Loading State**: Shows spinner while fetching
300
+ - **Error State**: Displays error message with retry button
301
+ - **Empty State**: Shows helpful message when no runs exist
302
+ - **Network Errors**: Caught and displayed to user
303
+ - **Validation Errors**: Shown in action feedback
304
+
305
+ ## Performance
306
+
307
+ - **Pagination**: Limits results to 10 per page
308
+ - **Auto-refresh**: Only polls when necessary
309
+ - **Debouncing**: Prevents rapid re-renders
310
+ - **Lazy Loading**: Modal only renders when open
311
+ - **Memoization**: Uses React best practices
312
+
313
+ ## Browser Support
314
+
315
+ - Chrome/Edge 90+
316
+ - Firefox 88+
317
+ - Safari 14+
318
+ - Mobile browsers (iOS Safari, Chrome Android)
319
+
320
+ ## Dependencies
321
+
322
+ - React 19+
323
+ - @simpli/funnels core package
324
+ - No external UI libraries required
325
+ - Tailwind CSS for styling
@@ -0,0 +1,168 @@
1
+ /**
2
+ * RunActions Component
3
+ *
4
+ * Action dropdown menu for individual run rows.
5
+ *
6
+ * Design Rationale:
7
+ * - Dropdown menu keeps UI clean and compact
8
+ * - Icons provide visual cues for actions
9
+ * - Disabled states for invalid actions
10
+ * - Confirmation for destructive actions (cancel)
11
+ *
12
+ * Accessibility:
13
+ * - Keyboard navigation (arrow keys, Enter, Escape)
14
+ * - Focus management
15
+ * - ARIA attributes for screen readers
16
+ */
17
+
18
+ import { useState, useRef, useEffect } from 'react';
19
+ import { FunnelRun } from '../../types';
20
+
21
+ interface RunActionsProps {
22
+ run: FunnelRun;
23
+ onViewDetails: (run: FunnelRun) => void;
24
+ onViewResults: (run: FunnelRun) => void;
25
+ onReRun: (run: FunnelRun) => void;
26
+ onCancel?: (run: FunnelRun) => void;
27
+ className?: string;
28
+ }
29
+
30
+ export function RunActions({
31
+ run,
32
+ onViewDetails,
33
+ onViewResults,
34
+ onReRun,
35
+ onCancel,
36
+ className = '',
37
+ }: RunActionsProps) {
38
+ const [isOpen, setIsOpen] = useState(false);
39
+ const dropdownRef = useRef<HTMLDivElement>(null);
40
+
41
+ // Close on outside click
42
+ useEffect(() => {
43
+ const handleClickOutside = (event: MouseEvent) => {
44
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
45
+ setIsOpen(false);
46
+ }
47
+ };
48
+
49
+ if (isOpen) {
50
+ document.addEventListener('mousedown', handleClickOutside);
51
+ return () => document.removeEventListener('mousedown', handleClickOutside);
52
+ }
53
+ }, [isOpen]);
54
+
55
+ // Close on Escape key
56
+ useEffect(() => {
57
+ const handleEscape = (event: KeyboardEvent) => {
58
+ if (event.key === 'Escape' && isOpen) {
59
+ setIsOpen(false);
60
+ }
61
+ };
62
+
63
+ if (isOpen) {
64
+ document.addEventListener('keydown', handleEscape);
65
+ return () => document.removeEventListener('keydown', handleEscape);
66
+ }
67
+ }, [isOpen]);
68
+
69
+ const canCancel = run.status === 'pending' || run.status === 'running';
70
+ const canViewResults = run.status === 'completed';
71
+
72
+ return (
73
+ <div className={`relative ${className}`} ref={dropdownRef}>
74
+ {/* Trigger Button */}
75
+ <button
76
+ onClick={() => setIsOpen(!isOpen)}
77
+ className="p-1 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
78
+ aria-label="Run actions"
79
+ aria-haspopup="true"
80
+ aria-expanded={isOpen}
81
+ >
82
+ <svg
83
+ className="w-5 h-5"
84
+ fill="none"
85
+ stroke="currentColor"
86
+ viewBox="0 0 24 24"
87
+ >
88
+ <path
89
+ strokeLinecap="round"
90
+ strokeLinejoin="round"
91
+ strokeWidth={2}
92
+ d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z"
93
+ />
94
+ </svg>
95
+ </button>
96
+
97
+ {/* Dropdown Menu */}
98
+ {isOpen && (
99
+ <div
100
+ className="absolute right-0 mt-1 w-48 bg-white rounded-md shadow-lg border border-gray-200 z-10"
101
+ role="menu"
102
+ >
103
+ <div className="py-1">
104
+ {/* View Details */}
105
+ <button
106
+ onClick={() => {
107
+ onViewDetails(run);
108
+ setIsOpen(false);
109
+ }}
110
+ className="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2"
111
+ role="menuitem"
112
+ >
113
+ <span aria-hidden="true">👁</span>
114
+ View Details
115
+ </button>
116
+
117
+ {/* View Results */}
118
+ <button
119
+ onClick={() => {
120
+ onViewResults(run);
121
+ setIsOpen(false);
122
+ }}
123
+ disabled={!canViewResults}
124
+ className="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-white"
125
+ role="menuitem"
126
+ >
127
+ <span aria-hidden="true">📊</span>
128
+ View Results
129
+ </button>
130
+
131
+ {/* Re-run */}
132
+ <button
133
+ onClick={() => {
134
+ onReRun(run);
135
+ setIsOpen(false);
136
+ }}
137
+ className="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2"
138
+ role="menuitem"
139
+ >
140
+ <span aria-hidden="true">↻</span>
141
+ Re-run
142
+ </button>
143
+
144
+ {/* Cancel (if running/pending) */}
145
+ {canCancel && onCancel && (
146
+ <>
147
+ <div className="border-t border-gray-200 my-1" />
148
+ <button
149
+ onClick={() => {
150
+ if (confirm('Are you sure you want to cancel this run?')) {
151
+ onCancel(run);
152
+ setIsOpen(false);
153
+ }
154
+ }}
155
+ className="w-full px-4 py-2 text-left text-sm text-red-600 hover:bg-red-50 flex items-center gap-2"
156
+ role="menuitem"
157
+ >
158
+ <span aria-hidden="true">×</span>
159
+ Cancel Run
160
+ </button>
161
+ </>
162
+ )}
163
+ </div>
164
+ </div>
165
+ )}
166
+ </div>
167
+ );
168
+ }
@@ -0,0 +1,221 @@
1
+ /**
2
+ * RunDetailsModal Component
3
+ *
4
+ * Modal dialog showing detailed run information and stage breakdown.
5
+ *
6
+ * Design Rationale:
7
+ * - Modal overlay focuses attention on details
8
+ * - Stage breakdown shows funnel flow clearly
9
+ * - Action buttons provide next steps
10
+ * - Close on overlay click or Escape key
11
+ *
12
+ * Accessibility:
13
+ * - Focus trap within modal
14
+ * - Escape key closes modal
15
+ * - ARIA dialog attributes
16
+ * - Backdrop click closes modal
17
+ */
18
+
19
+ import { useEffect, useRef } from 'react';
20
+ import { FunnelRun } from '../../types';
21
+ import { RunStatusBadge } from './RunStatusBadge';
22
+ import { StageBreakdownList } from './StageBreakdownList';
23
+ import {
24
+ formatDuration,
25
+ formatRelativeTime,
26
+ formatFullTimestamp,
27
+ } from './utils';
28
+
29
+ interface RunDetailsModalProps {
30
+ run: FunnelRun | null;
31
+ onClose: () => void;
32
+ onViewResults?: (run: FunnelRun) => void;
33
+ onReRun?: (run: FunnelRun) => void;
34
+ }
35
+
36
+ export function RunDetailsModal({
37
+ run,
38
+ onClose,
39
+ onViewResults,
40
+ onReRun,
41
+ }: RunDetailsModalProps) {
42
+ const modalRef = useRef<HTMLDivElement>(null);
43
+
44
+ // Close on Escape
45
+ useEffect(() => {
46
+ const handleEscape = (e: KeyboardEvent) => {
47
+ if (e.key === 'Escape') {
48
+ onClose();
49
+ }
50
+ };
51
+
52
+ if (run) {
53
+ document.addEventListener('keydown', handleEscape);
54
+ // Prevent body scroll
55
+ document.body.style.overflow = 'hidden';
56
+ return () => {
57
+ document.removeEventListener('keydown', handleEscape);
58
+ document.body.style.overflow = 'unset';
59
+ };
60
+ }
61
+ }, [run, onClose]);
62
+
63
+ // Focus trap
64
+ useEffect(() => {
65
+ if (run && modalRef.current) {
66
+ const focusableElements = modalRef.current.querySelectorAll<HTMLElement>(
67
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
68
+ );
69
+ const firstElement = focusableElements[0];
70
+ const lastElement = focusableElements[focusableElements.length - 1];
71
+
72
+ firstElement?.focus();
73
+
74
+ const handleTab = (e: KeyboardEvent) => {
75
+ if (e.key !== 'Tab') return;
76
+
77
+ if (e.shiftKey && document.activeElement === firstElement) {
78
+ e.preventDefault();
79
+ lastElement?.focus();
80
+ } else if (!e.shiftKey && document.activeElement === lastElement) {
81
+ e.preventDefault();
82
+ firstElement?.focus();
83
+ }
84
+ };
85
+
86
+ document.addEventListener('keydown', handleTab);
87
+ return () => document.removeEventListener('keydown', handleTab);
88
+ }
89
+ }, [run]);
90
+
91
+ if (!run) return null;
92
+
93
+ const stageStatsArray = Object.values(run.stage_stats);
94
+
95
+ return (
96
+ <div
97
+ className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"
98
+ onClick={onClose}
99
+ role="dialog"
100
+ aria-modal="true"
101
+ aria-labelledby="modal-title"
102
+ >
103
+ <div
104
+ ref={modalRef}
105
+ onClick={(e) => e.stopPropagation()}
106
+ className="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-hidden flex flex-col"
107
+ >
108
+ {/* Header */}
109
+ <div className="px-6 py-4 border-b border-gray-200">
110
+ <div className="flex items-center justify-between">
111
+ <h2 id="modal-title" className="text-lg font-semibold text-gray-900">
112
+ Run Details
113
+ </h2>
114
+ <button
115
+ onClick={onClose}
116
+ className="p-1 text-gray-400 hover:text-gray-600 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
117
+ aria-label="Close modal"
118
+ >
119
+ <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
120
+ <path
121
+ fillRule="evenodd"
122
+ d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
123
+ clipRule="evenodd"
124
+ />
125
+ </svg>
126
+ </button>
127
+ </div>
128
+ </div>
129
+
130
+ {/* Content */}
131
+ <div className="px-6 py-4 overflow-y-auto flex-1">
132
+ {/* Run Summary */}
133
+ <div className="mb-6">
134
+ <div className="grid grid-cols-2 gap-4 mb-4">
135
+ <div>
136
+ <div className="text-xs text-gray-600 mb-1">Status</div>
137
+ <RunStatusBadge status={run.status} />
138
+ </div>
139
+ <div>
140
+ <div className="text-xs text-gray-600 mb-1">Duration</div>
141
+ <div className="text-sm font-medium text-gray-900">
142
+ {formatDuration(run.duration_ms)}
143
+ </div>
144
+ </div>
145
+ <div>
146
+ <div className="text-xs text-gray-600 mb-1">Started</div>
147
+ <div className="text-sm font-medium text-gray-900">
148
+ <span title={formatFullTimestamp(run.started_at)}>
149
+ {formatRelativeTime(run.started_at)}
150
+ </span>
151
+ </div>
152
+ </div>
153
+ <div>
154
+ <div className="text-xs text-gray-600 mb-1">Triggered by</div>
155
+ <div className="text-sm font-medium text-gray-900">
156
+ {run.triggered_by || 'System'} ({run.trigger_type})
157
+ </div>
158
+ </div>
159
+ </div>
160
+
161
+ {/* Error Message */}
162
+ {run.status === 'failed' && run.error && (
163
+ <div className="p-3 bg-red-50 border border-red-200 rounded-lg">
164
+ <div className="text-sm font-medium text-red-800 mb-1">
165
+ Error
166
+ </div>
167
+ <div className="text-sm text-red-700">{run.error}</div>
168
+ </div>
169
+ )}
170
+ </div>
171
+
172
+ {/* Stage Breakdown */}
173
+ <div>
174
+ <h3 className="text-sm font-semibold text-gray-900 mb-3">
175
+ Stage Breakdown
176
+ </h3>
177
+ {stageStatsArray.length > 0 ? (
178
+ <StageBreakdownList stages={stageStatsArray} />
179
+ ) : (
180
+ <div className="text-sm text-gray-500 text-center py-4">
181
+ No stage data available
182
+ </div>
183
+ )}
184
+ </div>
185
+ </div>
186
+
187
+ {/* Footer */}
188
+ <div className="px-6 py-4 border-t border-gray-200 flex items-center justify-end gap-3">
189
+ <button
190
+ onClick={onClose}
191
+ className="px-4 py-2 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"
192
+ >
193
+ Close
194
+ </button>
195
+ {onReRun && (
196
+ <button
197
+ onClick={() => {
198
+ onReRun(run);
199
+ onClose();
200
+ }}
201
+ className="px-4 py-2 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"
202
+ >
203
+ ↻ Re-run
204
+ </button>
205
+ )}
206
+ {onViewResults && run.status === 'completed' && (
207
+ <button
208
+ onClick={() => {
209
+ onViewResults(run);
210
+ onClose();
211
+ }}
212
+ className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
213
+ >
214
+ View Results
215
+ </button>
216
+ )}
217
+ </div>
218
+ </div>
219
+ </div>
220
+ );
221
+ }