@handled-ai/design-system 0.8.0 → 0.9.0

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 (336) hide show
  1. package/README.md +14 -4
  2. package/dist/charts/bar-chart-component.d.ts +24 -0
  3. package/dist/charts/bar-chart-component.js +123 -0
  4. package/dist/charts/bar-chart-component.js.map +1 -0
  5. package/dist/charts/chart-tooltip.d.ts +26 -0
  6. package/dist/charts/chart-tooltip.js +69 -0
  7. package/dist/charts/chart-tooltip.js.map +1 -0
  8. package/dist/charts/chart.d.ts +64 -0
  9. package/dist/charts/chart.js +285 -0
  10. package/dist/charts/chart.js.map +1 -0
  11. package/dist/charts/donut-chart.d.ts +21 -0
  12. package/dist/charts/donut-chart.js +96 -0
  13. package/dist/charts/donut-chart.js.map +1 -0
  14. package/dist/charts/index.d.ts +11 -0
  15. package/dist/charts/index.js +10 -0
  16. package/dist/charts/index.js.map +1 -0
  17. package/dist/charts/pipeline-overview.d.ts +76 -0
  18. package/dist/charts/pipeline-overview.js +372 -0
  19. package/dist/charts/pipeline-overview.js.map +1 -0
  20. package/dist/charts/sankey-chart.d.ts +52 -0
  21. package/dist/charts/sankey-chart.js +219 -0
  22. package/dist/charts/sankey-chart.js.map +1 -0
  23. package/dist/charts/top-line-metrics.d.ts +26 -0
  24. package/dist/charts/top-line-metrics.js +224 -0
  25. package/dist/charts/top-line-metrics.js.map +1 -0
  26. package/dist/charts/trend-area-chart.d.ts +21 -0
  27. package/dist/charts/trend-area-chart.js +150 -0
  28. package/dist/charts/trend-area-chart.js.map +1 -0
  29. package/dist/charts/volume-analysis-chart.d.ts +19 -0
  30. package/dist/charts/volume-analysis-chart.js +121 -0
  31. package/dist/charts/volume-analysis-chart.js.map +1 -0
  32. package/dist/components/activity-detail.d.ts +38 -0
  33. package/dist/components/activity-detail.js +163 -0
  34. package/dist/components/activity-detail.js.map +1 -0
  35. package/dist/components/activity-log.d.ts +21 -0
  36. package/dist/components/activity-log.js +61 -0
  37. package/dist/components/activity-log.js.map +1 -0
  38. package/dist/components/agent-popover.d.ts +71 -0
  39. package/dist/components/agent-popover.js +282 -0
  40. package/dist/components/agent-popover.js.map +1 -0
  41. package/dist/components/agent-widget.d.ts +24 -0
  42. package/dist/components/agent-widget.js +117 -0
  43. package/dist/components/agent-widget.js.map +1 -0
  44. package/dist/components/avatar.d.ts +13 -0
  45. package/dist/components/avatar.js +140 -0
  46. package/dist/components/avatar.js.map +1 -0
  47. package/dist/components/badge.d.ts +12 -0
  48. package/dist/components/badge.js +75 -0
  49. package/dist/components/badge.js.map +1 -0
  50. package/dist/components/button.d.ts +13 -0
  51. package/dist/components/button.js +83 -0
  52. package/dist/components/button.js.map +1 -0
  53. package/dist/components/card.d.ts +11 -0
  54. package/dist/components/card.js +119 -0
  55. package/dist/components/card.js.map +1 -0
  56. package/dist/components/contact-list.d.ts +34 -0
  57. package/dist/components/contact-list.js +84 -0
  58. package/dist/components/contact-list.js.map +1 -0
  59. package/dist/components/dashboard-cards.d.ts +10 -0
  60. package/dist/components/dashboard-cards.js +164 -0
  61. package/dist/components/dashboard-cards.js.map +1 -0
  62. package/dist/components/data-table-display.d.ts +19 -0
  63. package/dist/components/data-table-display.js +109 -0
  64. package/dist/components/data-table-display.js.map +1 -0
  65. package/dist/components/data-table-filter.d.ts +18 -0
  66. package/dist/components/data-table-filter.js +107 -0
  67. package/dist/components/data-table-filter.js.map +1 -0
  68. package/dist/components/data-table-quick-views.d.ts +13 -0
  69. package/dist/components/data-table-quick-views.js +90 -0
  70. package/dist/components/data-table-quick-views.js.map +1 -0
  71. package/dist/components/data-table-toolbar.d.ts +18 -0
  72. package/dist/components/data-table-toolbar.js +45 -0
  73. package/dist/components/data-table-toolbar.js.map +1 -0
  74. package/dist/components/data-table.d.ts +39 -0
  75. package/dist/components/data-table.js +821 -0
  76. package/dist/components/data-table.js.map +1 -0
  77. package/dist/components/detail-view.d.ts +44 -0
  78. package/dist/components/detail-view.js +165 -0
  79. package/dist/components/detail-view.js.map +1 -0
  80. package/dist/components/dialog.d.ts +19 -0
  81. package/dist/components/dialog.js +188 -0
  82. package/dist/components/dialog.js.map +1 -0
  83. package/dist/components/dropdown-menu.d.ts +27 -0
  84. package/dist/components/dropdown-menu.js +279 -0
  85. package/dist/components/dropdown-menu.js.map +1 -0
  86. package/dist/components/entity-panel.d.ts +69 -0
  87. package/dist/components/entity-panel.js +584 -0
  88. package/dist/components/entity-panel.js.map +1 -0
  89. package/dist/components/inbox-row.d.ts +27 -0
  90. package/dist/components/inbox-row.js +139 -0
  91. package/dist/components/inbox-row.js.map +1 -0
  92. package/dist/components/inbox-toolbar.d.ts +21 -0
  93. package/dist/components/inbox-toolbar.js +203 -0
  94. package/dist/components/inbox-toolbar.js.map +1 -0
  95. package/dist/components/input.d.ts +5 -0
  96. package/dist/components/input.js +50 -0
  97. package/dist/components/input.js.map +1 -0
  98. package/dist/components/insights-filter-bar.d.ts +21 -0
  99. package/dist/components/insights-filter-bar.js +99 -0
  100. package/dist/components/insights-filter-bar.js.map +1 -0
  101. package/dist/components/item-list-display.d.ts +22 -0
  102. package/dist/components/item-list-display.js +240 -0
  103. package/dist/components/item-list-display.js.map +1 -0
  104. package/dist/components/item-list-filter.d.ts +16 -0
  105. package/dist/components/item-list-filter.js +87 -0
  106. package/dist/components/item-list-filter.js.map +1 -0
  107. package/dist/components/item-list-toolbar.d.ts +25 -0
  108. package/dist/components/item-list-toolbar.js +79 -0
  109. package/dist/components/item-list-toolbar.js.map +1 -0
  110. package/dist/components/item-list.d.ts +20 -0
  111. package/dist/components/item-list.js +702 -0
  112. package/dist/components/item-list.js.map +1 -0
  113. package/dist/components/label.d.ts +6 -0
  114. package/dist/components/label.js +55 -0
  115. package/dist/components/label.js.map +1 -0
  116. package/dist/components/message.d.ts +23 -0
  117. package/dist/components/message.js +117 -0
  118. package/dist/components/message.js.map +1 -0
  119. package/dist/components/metric-card.d.ts +25 -0
  120. package/dist/components/metric-card.js +107 -0
  121. package/dist/components/metric-card.js.map +1 -0
  122. package/dist/components/performance-metrics-table.d.ts +38 -0
  123. package/dist/components/performance-metrics-table.js +342 -0
  124. package/dist/components/performance-metrics-table.js.map +1 -0
  125. package/dist/components/preview-list.d.ts +14 -0
  126. package/dist/components/preview-list.js +83 -0
  127. package/dist/components/preview-list.js.map +1 -0
  128. package/dist/components/progress.d.ts +6 -0
  129. package/dist/components/progress.js +69 -0
  130. package/dist/components/progress.js.map +1 -0
  131. package/dist/components/quick-action-chat-area.d.ts +24 -0
  132. package/dist/components/quick-action-chat-area.js +178 -0
  133. package/dist/components/quick-action-chat-area.js.map +1 -0
  134. package/dist/components/quick-action-modal.d.ts +30 -0
  135. package/dist/components/quick-action-modal.js +288 -0
  136. package/dist/components/quick-action-modal.js.map +1 -0
  137. package/dist/components/quick-action-sidebar-nav.d.ts +51 -0
  138. package/dist/components/quick-action-sidebar-nav.js +528 -0
  139. package/dist/components/quick-action-sidebar-nav.js.map +1 -0
  140. package/dist/components/recommended-actions-section.d.ts +23 -0
  141. package/dist/components/recommended-actions-section.js +215 -0
  142. package/dist/components/recommended-actions-section.js.map +1 -0
  143. package/dist/components/report-card.d.ts +26 -0
  144. package/dist/components/report-card.js +69 -0
  145. package/dist/components/report-card.js.map +1 -0
  146. package/dist/components/score-analysis-modal.d.ts +26 -0
  147. package/dist/components/score-analysis-modal.js +141 -0
  148. package/dist/components/score-analysis-modal.js.map +1 -0
  149. package/dist/components/score-breakdown.d.ts +17 -0
  150. package/dist/components/score-breakdown.js +162 -0
  151. package/dist/components/score-breakdown.js.map +1 -0
  152. package/dist/components/score-feedback.d.ts +40 -0
  153. package/dist/components/score-feedback.js +209 -0
  154. package/dist/components/score-feedback.js.map +1 -0
  155. package/dist/components/score-ring.d.ts +14 -0
  156. package/dist/components/score-ring.js +79 -0
  157. package/dist/components/score-ring.js.map +1 -0
  158. package/dist/components/scroll-area.d.ts +7 -0
  159. package/dist/components/scroll-area.js +101 -0
  160. package/dist/components/scroll-area.js.map +1 -0
  161. package/dist/components/select.d.ts +17 -0
  162. package/dist/components/select.js +228 -0
  163. package/dist/components/select.js.map +1 -0
  164. package/dist/components/separator.d.ts +6 -0
  165. package/dist/components/separator.js +61 -0
  166. package/dist/components/separator.js.map +1 -0
  167. package/dist/components/sheet.d.ts +16 -0
  168. package/dist/components/sheet.js +168 -0
  169. package/dist/components/sheet.js.map +1 -0
  170. package/dist/components/sidebar.d.ts +73 -0
  171. package/dist/components/sidebar.js +723 -0
  172. package/dist/components/sidebar.js.map +1 -0
  173. package/dist/components/signal-feedback-inline.d.ts +51 -0
  174. package/dist/components/signal-feedback-inline.js +548 -0
  175. package/dist/components/signal-feedback-inline.js.map +1 -0
  176. package/dist/components/simple-data-table.d.ts +15 -0
  177. package/dist/components/simple-data-table.js +91 -0
  178. package/dist/components/simple-data-table.js.map +1 -0
  179. package/dist/components/skeleton.d.ts +5 -0
  180. package/dist/components/skeleton.js +44 -0
  181. package/dist/components/skeleton.js.map +1 -0
  182. package/dist/components/status-badge.d.ts +10 -0
  183. package/dist/components/status-badge.js +82 -0
  184. package/dist/components/status-badge.js.map +1 -0
  185. package/dist/components/styled-bar-list.d.ts +20 -0
  186. package/dist/components/styled-bar-list.js +59 -0
  187. package/dist/components/styled-bar-list.js.map +1 -0
  188. package/dist/components/suggested-actions.d.ts +110 -0
  189. package/dist/components/suggested-actions.js +1538 -0
  190. package/dist/components/suggested-actions.js.map +1 -0
  191. package/dist/components/table.d.ts +12 -0
  192. package/dist/components/table.js +147 -0
  193. package/dist/components/table.js.map +1 -0
  194. package/dist/components/tabs.d.ts +14 -0
  195. package/dist/components/tabs.js +129 -0
  196. package/dist/components/tabs.js.map +1 -0
  197. package/dist/components/textarea.d.ts +5 -0
  198. package/dist/components/textarea.js +47 -0
  199. package/dist/components/textarea.js.map +1 -0
  200. package/dist/components/timeline-activity.d.ts +34 -0
  201. package/dist/components/timeline-activity.js +181 -0
  202. package/dist/components/timeline-activity.js.map +1 -0
  203. package/dist/components/tooltip.d.ts +9 -0
  204. package/dist/components/tooltip.js +93 -0
  205. package/dist/components/tooltip.js.map +1 -0
  206. package/dist/components/view-mode-toggle.d.ts +16 -0
  207. package/dist/components/view-mode-toggle.js +24 -0
  208. package/dist/components/view-mode-toggle.js.map +1 -0
  209. package/dist/hooks/use-mobile.d.ts +3 -0
  210. package/dist/hooks/use-mobile.js +21 -0
  211. package/dist/hooks/use-mobile.js.map +1 -0
  212. package/dist/index.d.ts +68 -1878
  213. package/dist/index.js +69 -10918
  214. package/dist/index.js.map +1 -1
  215. package/dist/lib/icons.d.ts +18 -0
  216. package/dist/lib/icons.js +21 -0
  217. package/dist/lib/icons.js.map +1 -0
  218. package/dist/lib/utils.d.ts +5 -0
  219. package/dist/lib/utils.js +9 -0
  220. package/dist/lib/utils.js.map +1 -0
  221. package/dist/prototype/index.d.ts +20 -0
  222. package/dist/prototype/index.js +8 -0
  223. package/dist/prototype/index.js.map +1 -0
  224. package/dist/prototype/prototype-accounts-view.d.ts +22 -0
  225. package/dist/prototype/prototype-accounts-view.js +70 -0
  226. package/dist/prototype/prototype-accounts-view.js.map +1 -0
  227. package/dist/prototype/prototype-admin-view.d.ts +21 -0
  228. package/dist/prototype/prototype-admin-view.js +53 -0
  229. package/dist/prototype/prototype-admin-view.js.map +1 -0
  230. package/dist/prototype/prototype-config.d.ts +226 -0
  231. package/dist/prototype/prototype-config.js +1 -0
  232. package/dist/prototype/prototype-config.js.map +1 -0
  233. package/dist/prototype/prototype-inbox-view.d.ts +48 -0
  234. package/dist/prototype/prototype-inbox-view.js +701 -0
  235. package/dist/prototype/prototype-inbox-view.js.map +1 -0
  236. package/dist/prototype/prototype-insights-view.d.ts +23 -0
  237. package/dist/prototype/prototype-insights-view.js +335 -0
  238. package/dist/prototype/prototype-insights-view.js.map +1 -0
  239. package/dist/prototype/prototype-shell.d.ts +40 -0
  240. package/dist/prototype/prototype-shell.js +190 -0
  241. package/dist/prototype/prototype-shell.js.map +1 -0
  242. package/dist/prototype/prototype-work-queue-view.d.ts +8 -0
  243. package/dist/prototype/prototype-work-queue-view.js +17 -0
  244. package/dist/prototype/prototype-work-queue-view.js.map +1 -0
  245. package/dist/three/agent-orb.d.ts +39 -0
  246. package/dist/three/agent-orb.js +500 -0
  247. package/dist/three/agent-orb.js.map +1 -0
  248. package/dist/three/index.d.ts +2 -0
  249. package/dist/three/index.js +2 -0
  250. package/dist/three/index.js.map +1 -0
  251. package/package.json +98 -17
  252. package/src/charts/bar-chart-component.tsx +150 -0
  253. package/src/charts/chart-tooltip.tsx +86 -0
  254. package/src/charts/chart.tsx +371 -0
  255. package/src/charts/donut-chart.tsx +112 -0
  256. package/src/charts/index.ts +13 -0
  257. package/src/charts/pipeline-overview.tsx +476 -0
  258. package/src/charts/sankey-chart.tsx +290 -0
  259. package/src/charts/top-line-metrics.tsx +261 -0
  260. package/src/charts/trend-area-chart.tsx +150 -0
  261. package/src/charts/volume-analysis-chart.tsx +124 -0
  262. package/src/components/activity-detail.tsx +233 -0
  263. package/src/components/activity-log.tsx +89 -0
  264. package/src/components/agent-popover.tsx +373 -0
  265. package/src/components/agent-widget.tsx +163 -0
  266. package/src/components/avatar.tsx +109 -0
  267. package/src/components/badge.tsx +48 -0
  268. package/src/components/button.tsx +59 -0
  269. package/src/components/card.tsx +92 -0
  270. package/src/components/contact-list.tsx +121 -0
  271. package/src/components/dashboard-cards.tsx +170 -0
  272. package/src/components/data-table-display.tsx +139 -0
  273. package/src/components/data-table-filter.tsx +138 -0
  274. package/src/components/data-table-quick-views.tsx +103 -0
  275. package/src/components/data-table-toolbar.tsx +56 -0
  276. package/src/components/data-table.tsx +915 -0
  277. package/src/components/detail-view.tsx +237 -0
  278. package/src/components/dialog.tsx +158 -0
  279. package/src/components/dropdown-menu.tsx +257 -0
  280. package/src/components/entity-panel.tsx +767 -0
  281. package/src/components/inbox-row.tsx +132 -0
  282. package/src/components/inbox-toolbar.tsx +213 -0
  283. package/src/components/input.tsx +21 -0
  284. package/src/components/insights-filter-bar.tsx +132 -0
  285. package/src/components/item-list-display.tsx +278 -0
  286. package/src/components/item-list-filter.tsx +118 -0
  287. package/src/components/item-list-toolbar.tsx +97 -0
  288. package/src/components/item-list.tsx +843 -0
  289. package/src/components/label.tsx +24 -0
  290. package/src/components/message.tsx +83 -0
  291. package/src/components/metric-card.tsx +178 -0
  292. package/src/components/performance-metrics-table.tsx +442 -0
  293. package/src/components/preview-list.tsx +62 -0
  294. package/src/components/progress.tsx +31 -0
  295. package/src/components/quick-action-chat-area.tsx +156 -0
  296. package/src/components/quick-action-modal.tsx +331 -0
  297. package/src/components/quick-action-sidebar-nav.tsx +592 -0
  298. package/src/components/recommended-actions-section.tsx +258 -0
  299. package/src/components/report-card.tsx +106 -0
  300. package/src/components/score-analysis-modal.tsx +172 -0
  301. package/src/components/score-breakdown.tsx +179 -0
  302. package/src/components/score-feedback.tsx +288 -0
  303. package/src/components/score-ring.tsx +87 -0
  304. package/src/components/scroll-area.tsx +58 -0
  305. package/src/components/select.tsx +190 -0
  306. package/src/components/separator.tsx +28 -0
  307. package/src/components/sheet.tsx +143 -0
  308. package/src/components/sidebar.tsx +726 -0
  309. package/src/components/signal-feedback-inline.tsx +591 -0
  310. package/src/components/simple-data-table.tsx +124 -0
  311. package/src/components/skeleton.tsx +15 -0
  312. package/src/components/status-badge.tsx +63 -0
  313. package/src/components/styled-bar-list.tsx +70 -0
  314. package/src/components/suggested-actions.tsx +1985 -0
  315. package/src/components/table.tsx +116 -0
  316. package/src/components/tabs.tsx +91 -0
  317. package/src/components/textarea.tsx +18 -0
  318. package/src/components/timeline-activity.tsx +234 -0
  319. package/src/components/tooltip.tsx +57 -0
  320. package/src/components/view-mode-toggle.tsx +39 -0
  321. package/src/hooks/use-mobile.ts +21 -0
  322. package/src/index.ts +77 -0
  323. package/src/lib/icons.ts +18 -0
  324. package/src/lib/utils.ts +6 -0
  325. package/src/prototype/index.ts +11 -0
  326. package/src/prototype/prototype-accounts-view.tsx +112 -0
  327. package/src/prototype/prototype-admin-view.tsx +67 -0
  328. package/src/prototype/prototype-config.ts +243 -0
  329. package/src/prototype/prototype-inbox-view.tsx +810 -0
  330. package/src/prototype/prototype-insights-view.tsx +379 -0
  331. package/src/prototype/prototype-shell.tsx +219 -0
  332. package/src/prototype/prototype-work-queue-view.tsx +30 -0
  333. package/src/styles/globals.css +299 -0
  334. package/src/three/agent-orb.tsx +557 -0
  335. package/src/three/index.ts +5 -0
  336. package/src/types/r3f.d.ts +8 -0
@@ -0,0 +1,476 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { TrendingUp, Info, ArrowRight } from "lucide-react"
5
+ import { ResponsiveSankey } from "@nivo/sankey"
6
+
7
+ import { cn } from "../lib/utils"
8
+ import {
9
+ Tooltip,
10
+ TooltipContent,
11
+ TooltipProvider,
12
+ TooltipTrigger,
13
+ } from "../components/tooltip"
14
+
15
+ export interface PipelineStage {
16
+ id: string
17
+ label: string
18
+ count: number
19
+ trend: string
20
+ nextConversion: string | null
21
+ }
22
+
23
+ export interface PipelineStageMetrics {
24
+ medianTime: string
25
+ avgTime: string
26
+ dropOffs: { reason: string; count: number; pct: string }[]
27
+ }
28
+
29
+ export interface PipelineStageTiming {
30
+ median: string
31
+ avg: string
32
+ }
33
+
34
+ export interface PipelineFilterBreakdown {
35
+ [stageId: string]: Record<string, number>
36
+ }
37
+
38
+ const SEGMENT_PALETTE = [
39
+ "#0F4C3A",
40
+ "#15803d",
41
+ "#0ea5e9",
42
+ "#8b5cf6",
43
+ "#f59e0b",
44
+ "#ef4444",
45
+ ]
46
+
47
+ const DROP_OFF_NODES = [
48
+ { id: "Lost/Other", nodeColor: "#CBD5E1" },
49
+ { id: "Coverage", nodeColor: "#F59E0B" },
50
+ { id: "Unqualified", nodeColor: "#F59E0B" },
51
+ { id: "No Contact", nodeColor: "#F59E0B" },
52
+ { id: "Intake Drop", nodeColor: "#F59E0B" },
53
+ { id: "No Show/Cancel", nodeColor: "#F59E0B" },
54
+ ]
55
+
56
+ function StageHoverCard({
57
+ title,
58
+ count,
59
+ metrics,
60
+ }: {
61
+ title: string
62
+ count: number | string
63
+ metrics?: PipelineStageMetrics
64
+ }) {
65
+ return (
66
+ <div className="w-[260px] overflow-hidden rounded-lg border border-border bg-card font-sans text-left shadow-xl">
67
+ <div className="border-b border-border p-3">
68
+ <div className="mb-0.5 text-xs font-medium text-muted-foreground">{title}</div>
69
+ <div className="text-2xl font-bold text-foreground">{count}</div>
70
+ </div>
71
+
72
+ {metrics && (
73
+ <div className="grid grid-cols-2 gap-2 border-b border-border bg-muted/30 px-3 py-2">
74
+ <div>
75
+ <div className="text-[9px] font-semibold uppercase tracking-wider text-muted-foreground/70">
76
+ Median
77
+ </div>
78
+ <div className="text-xs font-bold text-foreground">
79
+ {metrics.medianTime}
80
+ </div>
81
+ </div>
82
+ <div className="border-l border-border pl-2">
83
+ <div className="text-[9px] font-semibold uppercase tracking-wider text-muted-foreground/70">
84
+ Average
85
+ </div>
86
+ <div className="text-xs font-bold text-foreground">
87
+ {metrics.avgTime}
88
+ </div>
89
+ </div>
90
+ </div>
91
+ )}
92
+
93
+ {metrics?.dropOffs && metrics.dropOffs.length > 0 && (
94
+ <div className="p-2">
95
+ <div className="mb-1.5 px-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
96
+ Drop-off Reasons
97
+ </div>
98
+ <div className="space-y-0.5">
99
+ {metrics.dropOffs.map((drop, i) => (
100
+ <div
101
+ key={i}
102
+ className="group flex cursor-pointer items-center justify-between rounded p-1.5 text-xs transition-colors hover:bg-muted"
103
+ >
104
+ <span className="truncate pr-2 text-muted-foreground group-hover:text-foreground">
105
+ {drop.reason}
106
+ </span>
107
+ <div className="flex items-center gap-1.5">
108
+ <span className="text-[10px] text-muted-foreground/70">{drop.pct}</span>
109
+ <span className="inline-flex h-4 min-w-[20px] items-center justify-center rounded border border-border bg-muted px-1 text-[9px] font-semibold text-muted-foreground group-hover:bg-muted group-hover:text-foreground">
110
+ {drop.count}
111
+ </span>
112
+ </div>
113
+ </div>
114
+ ))}
115
+ </div>
116
+ </div>
117
+ )}
118
+
119
+ <div className="flex cursor-pointer items-center justify-between border-t border-border bg-muted px-3 py-2 text-xs font-medium text-blue-600 transition-colors hover:bg-muted hover:text-blue-700">
120
+ View in Work Queue <ArrowRight className="h-3 w-3" />
121
+ </div>
122
+ </div>
123
+ )
124
+ }
125
+
126
+ export interface PipelineOverviewProps {
127
+ title?: string
128
+ stages: PipelineStage[]
129
+ stageMetrics: Record<string, PipelineStageMetrics>
130
+ stageTimings: (PipelineStageTiming | null)[]
131
+ filterOptions?: string[]
132
+ onFilterChange?: (filterOption: string) => void
133
+ filterBreakdowns?: Record<string, PipelineFilterBreakdown>
134
+ countingModes?: string[]
135
+ countingModeTooltip?: string
136
+ /** Main pipeline flow nodes (after the first stage) */
137
+ flowNodes?: { id: string; nodeColor: string }[]
138
+ /** Drop-off distribution from initial stage: { "Lost/Other": 56, "Coverage": 40, ... } */
139
+ dropOffDistribution?: Record<string, number>
140
+ /** Flow links after the first stage (middle of pipeline onward) */
141
+ flowLinks?: { source: string; target: string; value: number }[]
142
+ totalReceived?: number
143
+ /** Dollar amounts to display on terminal node labels: { "Retained": "$18.2M" } */
144
+ nodeAmounts?: Record<string, string>
145
+ /** Unit noun for standard node/link tooltips (e.g. "signals", "accounts"). */
146
+ unitLabel?: string
147
+ /** Unit noun for terminal/drop-off node tooltips (e.g. "opportunities"). Defaults to unitLabel. */
148
+ terminalUnitLabel?: string
149
+ /** Node IDs that should use terminalUnitLabel. Defaults to keys of dropOffDistribution. */
150
+ terminalNodeIds?: string[]
151
+ /** Sankey chart margins, merged with defaults { top: 20, right: 120, bottom: 20, left: 140 }. */
152
+ sankeyMargin?: { top?: number; right?: number; bottom?: number; left?: number }
153
+ /** Gap between Sankey node bar and its outside label. */
154
+ sankeyLabelPadding?: number
155
+ /** When true, conversion badges use `flex`; when false, `hidden xl:flex`. */
156
+ alwaysShowConversionBadges?: boolean
157
+ /** Color for dynamically generated drop-off nodes. */
158
+ dropOffNodeColor?: string
159
+ onViewInWorkQueue?: (stageId: string) => void
160
+ className?: string
161
+ }
162
+
163
+ export function PipelineOverview({
164
+ title = "Pipeline Overview",
165
+ stages,
166
+ stageMetrics,
167
+ stageTimings,
168
+ filterOptions = ["Facility", "Source", "Lead Source", "Payer", "Channel"],
169
+ onFilterChange,
170
+ filterBreakdowns,
171
+ countingModes = ["Unique Patients", "All Referrals"],
172
+ countingModeTooltip = "Patients may be referred through multiple channels. 'All Referrals' shows total volume; 'Unique Patients' deduplicates to show distinct patient counts.",
173
+ flowNodes = [
174
+ { id: "Contacted", nodeColor: "#2A8F7A" },
175
+ { id: "Intake Sent", nodeColor: "#3DB4A0" },
176
+ { id: "Intake Done", nodeColor: "#4CC9B0" },
177
+ { id: "Scheduled", nodeColor: "#5FCFBC" },
178
+ { id: "Completed", nodeColor: "#79E2C9" },
179
+ ],
180
+ dropOffDistribution = {
181
+ "Lost/Other": 56,
182
+ Coverage: 40,
183
+ Unqualified: 30,
184
+ },
185
+ flowLinks = [
186
+ { source: "Contacted", target: "Intake Sent", value: 660 },
187
+ { source: "Contacted", target: "No Contact", value: 60 },
188
+ { source: "Intake Sent", target: "Intake Done", value: 612 },
189
+ { source: "Intake Sent", target: "Intake Drop", value: 48 },
190
+ { source: "Intake Done", target: "Scheduled", value: 612 },
191
+ { source: "Scheduled", target: "Completed", value: 520 },
192
+ { source: "Scheduled", target: "No Show/Cancel", value: 92 },
193
+ ],
194
+ totalReceived = 847,
195
+ nodeAmounts,
196
+ unitLabel,
197
+ terminalUnitLabel,
198
+ terminalNodeIds,
199
+ sankeyMargin,
200
+ sankeyLabelPadding,
201
+ alwaysShowConversionBadges = false,
202
+ dropOffNodeColor,
203
+ onViewInWorkQueue: _onViewInWorkQueue,
204
+ className,
205
+ }: PipelineOverviewProps) {
206
+ const [selectedFilter, setSelectedFilter] = React.useState(filterOptions[0])
207
+ const [countingMode, setCountingMode] = React.useState(countingModes[0])
208
+
209
+ const effectiveUnitLabel = unitLabel ?? "items"
210
+ const effectiveTerminalUnitLabel = terminalUnitLabel ?? effectiveUnitLabel
211
+ const effectiveTerminalNodeIds = React.useMemo(
212
+ () => new Set(terminalNodeIds ?? Object.keys(dropOffDistribution)),
213
+ [terminalNodeIds, dropOffDistribution],
214
+ )
215
+ const effectiveMargin = React.useMemo(
216
+ () => ({ top: 20, right: 120, bottom: 20, left: 140, ...sankeyMargin }),
217
+ [sankeyMargin],
218
+ )
219
+ const effectiveLabelPadding = sankeyLabelPadding ?? 16
220
+
221
+ const sankeyData = React.useMemo(() => {
222
+ const breakdown =
223
+ filterBreakdowns?.[selectedFilter]?.received ??
224
+ ({ [stages[0]?.label ?? "Received"]: totalReceived } as Record<
225
+ string,
226
+ number
227
+ >)
228
+
229
+ const segments = Object.entries(breakdown).map(([name, value], index) => ({
230
+ id: name,
231
+ nodeColor: SEGMENT_PALETTE[index % SEGMENT_PALETTE.length],
232
+ value,
233
+ }))
234
+
235
+ const dropOffKeys = Object.keys(dropOffDistribution)
236
+ const dropOffNodes = dropOffKeys.length > 0
237
+ ? dropOffKeys.map((reason) => ({
238
+ id: reason,
239
+ nodeColor: dropOffNodeColor ?? "#F59E0B",
240
+ }))
241
+ : DROP_OFF_NODES
242
+
243
+ const nodes = [
244
+ ...segments,
245
+ ...flowNodes,
246
+ ...dropOffNodes,
247
+ ]
248
+
249
+ const nodeIds = new Set(nodes.map((n) => n.id))
250
+ for (const link of flowLinks) {
251
+ for (const endpoint of [link.source, link.target]) {
252
+ if (!nodeIds.has(endpoint)) {
253
+ nodes.push({ id: endpoint, nodeColor: dropOffNodeColor ?? "#F59E0B" })
254
+ nodeIds.add(endpoint)
255
+ }
256
+ }
257
+ }
258
+
259
+ const links: { source: string; target: string; value: number }[] = []
260
+
261
+ const firstFlowNode = flowNodes[0]?.id ?? "Contacted"
262
+
263
+ segments.forEach((segment) => {
264
+ if (segment.value > 0) {
265
+ links.push({
266
+ source: segment.id,
267
+ target: firstFlowNode,
268
+ value: segment.value,
269
+ })
270
+ }
271
+ })
272
+
273
+ for (const [reason, count] of Object.entries(dropOffDistribution)) {
274
+ if (count > 0) {
275
+ links.push({ source: firstFlowNode, target: reason, value: count })
276
+ }
277
+ }
278
+
279
+ flowLinks.forEach((link) => links.push({ ...link }))
280
+
281
+ return { nodes, links }
282
+ }, [
283
+ selectedFilter,
284
+ filterBreakdowns,
285
+ stages,
286
+ totalReceived,
287
+ flowNodes,
288
+ dropOffDistribution,
289
+ dropOffNodeColor,
290
+ flowLinks,
291
+ ])
292
+
293
+ return (
294
+ <div
295
+ className={cn(
296
+ "rounded-xl border border-border bg-card p-5 shadow-sm",
297
+ className,
298
+ )}
299
+ >
300
+ {/* Header */}
301
+ <div className="mb-6 flex flex-col gap-4">
302
+ <div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
303
+ <div className="flex items-center gap-4">
304
+ <h3 className="text-lg font-semibold text-foreground">{title}</h3>
305
+
306
+ {/* Counting Mode Toggle */}
307
+ {countingModes.length > 1 && (
308
+ <div className="flex items-center gap-2 rounded-lg border border-border bg-muted p-1">
309
+ {countingModes.map((mode) => (
310
+ <button
311
+ key={mode}
312
+ onClick={() => setCountingMode(mode)}
313
+ className={cn(
314
+ "rounded-md px-3 py-1 text-xs font-medium transition-all",
315
+ countingMode === mode
316
+ ? "border border-border bg-background text-foreground shadow-sm"
317
+ : "text-muted-foreground hover:text-foreground",
318
+ )}
319
+ >
320
+ {mode}
321
+ </button>
322
+ ))}
323
+ <TooltipProvider>
324
+ <Tooltip>
325
+ <TooltipTrigger asChild>
326
+ <Info className="ml-1 h-3.5 w-3.5 cursor-help text-muted-foreground/70" />
327
+ </TooltipTrigger>
328
+ <TooltipContent className="max-w-[250px] text-xs">
329
+ {countingModeTooltip}
330
+ </TooltipContent>
331
+ </Tooltip>
332
+ </TooltipProvider>
333
+ </div>
334
+ )}
335
+ </div>
336
+
337
+ {/* Filter Tabs */}
338
+ {filterOptions.length > 1 && (
339
+ <div className="flex items-center gap-1 self-start rounded-lg bg-muted p-1 md:self-auto">
340
+ {filterOptions.map((option) => (
341
+ <button
342
+ key={option}
343
+ onClick={() => { setSelectedFilter(option); onFilterChange?.(option); }}
344
+ className={cn(
345
+ "h-7 rounded-md border-none bg-transparent px-3 text-xs font-medium shadow-none transition-all hover:bg-background",
346
+ selectedFilter === option &&
347
+ "bg-background text-foreground shadow-sm",
348
+ )}
349
+ >
350
+ {option}
351
+ </button>
352
+ ))}
353
+ </div>
354
+ )}
355
+ </div>
356
+ </div>
357
+
358
+ {/* Stage Metrics Row */}
359
+ <div className="mb-2 grid gap-4" style={{ gridTemplateColumns: `repeat(${stages.length}, minmax(0, 1fr))` }}>
360
+ {stages.map((stage, index) => {
361
+ const details = stageMetrics[stage.id]
362
+ const timing = stageTimings[index] ?? null
363
+
364
+ return (
365
+ <TooltipProvider key={stage.id} delayDuration={100}>
366
+ <Tooltip>
367
+ <TooltipTrigger asChild>
368
+ <div className="group relative flex cursor-pointer flex-col items-center rounded-lg p-2 text-center transition-colors hover:bg-muted">
369
+ <div
370
+ className="mb-1 w-full truncate text-xs font-medium text-muted-foreground"
371
+ title={stage.label}
372
+ >
373
+ {stage.label}
374
+ </div>
375
+ <div className="mb-1 text-2xl font-bold text-foreground">
376
+ {stage.count.toLocaleString()}
377
+ </div>
378
+ <div className="mb-1 flex items-center justify-center gap-1 text-xs font-medium text-emerald-600">
379
+ {stage.trend} <TrendingUp className="h-3 w-3" />
380
+ </div>
381
+
382
+ {/* Conversion badge + timing between stages */}
383
+ {index < stages.length - 1 && stage.nextConversion && (
384
+ <div className={cn(
385
+ alwaysShowConversionBadges ? "flex" : "hidden xl:flex",
386
+ "absolute -right-2 top-1/2 z-10 -translate-y-1/2 translate-x-1/2 flex-col items-center",
387
+ )}>
388
+ <span className="z-10 whitespace-nowrap rounded-full border border-border bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground shadow-sm">
389
+ {stage.nextConversion}
390
+ </span>
391
+ {timing && (
392
+ <div className="mt-1 flex flex-col items-center">
393
+ <div className="h-2 w-px bg-border" />
394
+ <div className="whitespace-nowrap rounded bg-background/80 px-1 py-0.5 text-[9px] font-medium leading-3 text-muted-foreground/70 backdrop-blur-[2px]">
395
+ <span className="mr-1 font-semibold text-muted-foreground">
396
+ Med: {timing.median}
397
+ </span>
398
+ <span className="text-muted-foreground/70">
399
+ Avg: {timing.avg}
400
+ </span>
401
+ </div>
402
+ </div>
403
+ )}
404
+ </div>
405
+ )}
406
+
407
+ {/* Connector line down to Sankey */}
408
+ <div className="mx-auto mb-1 mt-2 h-4 w-px bg-border" />
409
+ </div>
410
+ </TooltipTrigger>
411
+ <TooltipContent
412
+ className="border-none bg-transparent p-0 shadow-none"
413
+ sideOffset={8}
414
+ >
415
+ <StageHoverCard
416
+ title={stage.label}
417
+ count={stage.count}
418
+ metrics={details}
419
+ />
420
+ </TooltipContent>
421
+ </Tooltip>
422
+ </TooltipProvider>
423
+ )
424
+ })}
425
+ </div>
426
+
427
+ {/* Sankey Chart */}
428
+ <div
429
+ className="relative mt-4 w-full"
430
+ style={{ height: 400, minWidth: 0 }}
431
+ >
432
+ <ResponsiveSankey
433
+ data={sankeyData}
434
+ margin={effectiveMargin}
435
+ align="justify"
436
+ colors={(node: { nodeColor?: string }) => node.nodeColor || "#94a3b8"}
437
+ nodeOpacity={1}
438
+ nodeHoverOthersOpacity={0.35}
439
+ nodeThickness={18}
440
+ nodeSpacing={16}
441
+ nodeBorderWidth={0}
442
+ nodeBorderRadius={3}
443
+ linkOpacity={0.5}
444
+ linkHoverOthersOpacity={0.1}
445
+ linkContract={3}
446
+ enableLinkGradient
447
+ labelPosition="outside"
448
+ labelOrientation="horizontal"
449
+ labelPadding={effectiveLabelPadding}
450
+ label={(node: { id: string }) => nodeAmounts?.[node.id] ? `${node.id} (${nodeAmounts[node.id]})` : node.id}
451
+ labelTextColor={{ from: "color", modifiers: [["darker", 1]] }}
452
+ nodeTooltip={({ node }: { node: { id: string; value: number } }) => {
453
+ const unit = effectiveTerminalNodeIds.has(node.id) ? effectiveTerminalUnitLabel : effectiveUnitLabel
454
+ return (
455
+ <div className="rounded-md border border-border bg-card p-2 text-xs shadow-lg">
456
+ <span className="mb-1 block font-bold">{node.id}{nodeAmounts?.[node.id] ? ` — ${nodeAmounts[node.id]}` : ""}</span>
457
+ <span>{node.value} {unit}</span>
458
+ </div>
459
+ )
460
+ }}
461
+ linkTooltip={({ link }: { link: { source: { id: string }; target: { id: string }; value: number } }) => {
462
+ const unit = effectiveTerminalNodeIds.has(link.target.id) ? effectiveTerminalUnitLabel : effectiveUnitLabel
463
+ return (
464
+ <div className="rounded-md border border-border bg-card p-2 text-xs shadow-lg">
465
+ <span className="mb-1 block font-bold">
466
+ {link.source.id} → {link.target.id}
467
+ </span>
468
+ <span>{link.value} {unit}</span>
469
+ </div>
470
+ )
471
+ }}
472
+ />
473
+ </div>
474
+ </div>
475
+ )
476
+ }