amliq-dashboard 2.0.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 (492) hide show
  1. package/.env.example +3 -0
  2. package/.env.production +1 -0
  3. package/Dockerfile +26 -0
  4. package/QUICK_START.md +267 -0
  5. package/README.md +161 -0
  6. package/TESTING_GUIDE.md +322 -0
  7. package/dist/.well-known/ai-plugin.json +15 -0
  8. package/dist/_headers +8 -0
  9. package/dist/_redirects +1 -0
  10. package/dist/assets/APIKeys-BvwFauIs.js +6 -0
  11. package/dist/assets/APIKeys-DaMSBZQL.js +1 -0
  12. package/dist/assets/AdverseMedia-DckXfMzS.js +4 -0
  13. package/dist/assets/AlertDetailPage-Bc2_zwu_.js +1 -0
  14. package/dist/assets/AlertQueue-DFtBiRoM.js +8 -0
  15. package/dist/assets/Analytics-DtruB5lQ.js +1 -0
  16. package/dist/assets/AuditTrail-BK68ePu0.js +1 -0
  17. package/dist/assets/AuthDivider-gRHEt7gx.js +1 -0
  18. package/dist/assets/Badge-B9VFkofy.js +1 -0
  19. package/dist/assets/BatchJobs-Bz6KFnXD.js +6 -0
  20. package/dist/assets/BillingPage-R9nQ5nFb.js +11 -0
  21. package/dist/assets/Card-Cw-T_-oU.js +1 -0
  22. package/dist/assets/CaseDetail-DYs7Zg_y.js +7 -0
  23. package/dist/assets/CaseManagement-B1Q4Dp1Y.js +1 -0
  24. package/dist/assets/ComplianceReport-Dhssqc9T.js +1 -0
  25. package/dist/assets/Configuration-B92kl9T0.js +1 -0
  26. package/dist/assets/CryptoScreening-C7yFlskC.js +1 -0
  27. package/dist/assets/Dashboard-w4-Nhv9q.js +17 -0
  28. package/dist/assets/DataSources-u3-Ri_5_.js +3 -0
  29. package/dist/assets/EDDWorkflow-CFzlEO0e.js +1 -0
  30. package/dist/assets/EmptyState-Do0xJAT9.js +6 -0
  31. package/dist/assets/ForgotPassword-BPYyQFQp.js +1 -0
  32. package/dist/assets/LandingPage-CjaP0zw4.js +41 -0
  33. package/dist/assets/ListsMarketplace-CG6KkT9B.js +6 -0
  34. package/dist/assets/Login-677LEGP1.js +1 -0
  35. package/dist/assets/MFASetup-BxqCPvui.js +1 -0
  36. package/dist/assets/Monitoring-9TN9MK4y.js +1 -0
  37. package/dist/assets/Onboarding-Cf0Y83lX.js +1 -0
  38. package/dist/assets/Operations-CcXwc2xw.js +10 -0
  39. package/dist/assets/Overview-B_Ky0VWV.js +1 -0
  40. package/dist/assets/PEPScreening-pUQjTH9c.js +1 -0
  41. package/dist/assets/PageHeader-BObHFn0i.js +1 -0
  42. package/dist/assets/PrivacyPolicy-DZ2xrKir.js +1 -0
  43. package/dist/assets/ResetPassword-AtxtpIG1.js +1 -0
  44. package/dist/assets/RiskAssessment-Dzuh1Usv.js +1 -0
  45. package/dist/assets/SARForm-D8w2ZGe-.js +1 -0
  46. package/dist/assets/SanctionsLists-CPA3UH2v.js +1 -0
  47. package/dist/assets/ScheduledTasks-Dsjk-6UR.js +1 -0
  48. package/dist/assets/ScreenEntity-D1U0YL7v.js +3 -0
  49. package/dist/assets/ScreeningProgress-BW49PzoB.js +66 -0
  50. package/dist/assets/SearchField-CulFKdiI.js +1 -0
  51. package/dist/assets/Signup-C0FmFOo_.js +1 -0
  52. package/dist/assets/SystemHealth-B_8u4eva.js +1 -0
  53. package/dist/assets/TaskHistory-BCmEM89q.js +3 -0
  54. package/dist/assets/Team-CUo_FlU7.js +1 -0
  55. package/dist/assets/TenantDetail-CgZQHUY8.js +1 -0
  56. package/dist/assets/Tenants-RB9E9ick.js +2 -0
  57. package/dist/assets/TermsOfService-BfL-kndX.js +1 -0
  58. package/dist/assets/TransactionMonitoring-DGh_T22s.js +14 -0
  59. package/dist/assets/TxnScreening-B7cm2-eh.js +1 -0
  60. package/dist/assets/UBOChain-falXAsCo.js +1 -0
  61. package/dist/assets/Users-Doakpg9c.js +1 -0
  62. package/dist/assets/Webhooks-BZpAfaxv.js +1 -0
  63. package/dist/assets/alert-triangle-YCFVm7B_.js +6 -0
  64. package/dist/assets/alerts-COUnTwWY.js +1 -0
  65. package/dist/assets/arrow-left-9ApLqP95.js +6 -0
  66. package/dist/assets/check-VTcvVtIU.js +6 -0
  67. package/dist/assets/check-circle-2-Dn-lG5YQ.js +6 -0
  68. package/dist/assets/code-DIJRWFVv.js +6 -0
  69. package/dist/assets/fingerprint-C0MGVtax.js +6 -0
  70. package/dist/assets/flag-B8_Gwxh6.js +6 -0
  71. package/dist/assets/index-CfdYcqRM.js +289 -0
  72. package/dist/assets/index-HPU_Rhdu.css +1 -0
  73. package/dist/assets/layers-zHz2o4vo.js +6 -0
  74. package/dist/assets/loader-B9_dNihg.js +6 -0
  75. package/dist/assets/mail-BijL2qwp.js +6 -0
  76. package/dist/assets/plus-i0oBIIPS.js +6 -0
  77. package/dist/assets/refresh-cw-CSuAwutt.js +6 -0
  78. package/dist/assets/useAnalytics-Bj8IONcw.js +72 -0
  79. package/dist/assets/x-circle-DLGKvijE.js +6 -0
  80. package/dist/favicon.svg +15 -0
  81. package/dist/index.html +51 -0
  82. package/dist/llms.txt +28 -0
  83. package/dist/logo.png +0 -0
  84. package/dist/logo.svg +17 -0
  85. package/dist/manifest.json +44 -0
  86. package/dist/robots.txt +15 -0
  87. package/dist/schema.json +13 -0
  88. package/dist/sitemap.xml +28 -0
  89. package/dist/sw.js +67 -0
  90. package/e2e/auth-setup.ts +20 -0
  91. package/e2e/billing.spec.ts +50 -0
  92. package/e2e/cases.spec.ts +53 -0
  93. package/e2e/compliance.spec.ts +65 -0
  94. package/e2e/config.spec.ts +56 -0
  95. package/e2e/dashboard.spec.ts +47 -0
  96. package/e2e/fixtures.ts +33 -0
  97. package/e2e/lists.spec.ts +43 -0
  98. package/e2e/login.spec.ts +64 -0
  99. package/e2e/media.spec.ts +48 -0
  100. package/e2e/mocks.ts +51 -0
  101. package/e2e/monitoring.spec.ts +44 -0
  102. package/e2e/navigation.spec.ts +56 -0
  103. package/e2e/onboarding.spec.ts +49 -0
  104. package/e2e/responsive.spec.ts +40 -0
  105. package/e2e/risk.spec.ts +61 -0
  106. package/e2e/screening.spec.ts +76 -0
  107. package/e2e/team.spec.ts +53 -0
  108. package/index.html +50 -0
  109. package/package.json +47 -0
  110. package/playwright.config.ts +35 -0
  111. package/postcss.config.js +6 -0
  112. package/public/.well-known/ai-plugin.json +15 -0
  113. package/public/_headers +8 -0
  114. package/public/_redirects +1 -0
  115. package/public/favicon.svg +15 -0
  116. package/public/llms.txt +28 -0
  117. package/public/logo.png +0 -0
  118. package/public/logo.svg +17 -0
  119. package/public/manifest.json +44 -0
  120. package/public/robots.txt +15 -0
  121. package/public/schema.json +13 -0
  122. package/public/sitemap.xml +28 -0
  123. package/public/sw.js +67 -0
  124. package/scripts/test-runner.sh +152 -0
  125. package/src/App.tsx +48 -0
  126. package/src/api/alerts.ts +19 -0
  127. package/src/api/analytics.ts +6 -0
  128. package/src/api/audit.ts +11 -0
  129. package/src/api/auth.ts +26 -0
  130. package/src/api/billing.ts +54 -0
  131. package/src/api/cases.ts +48 -0
  132. package/src/api/client.ts +50 -0
  133. package/src/api/config.ts +30 -0
  134. package/src/api/edd.ts +25 -0
  135. package/src/api/enforcement.ts +33 -0
  136. package/src/api/lists.ts +24 -0
  137. package/src/api/monitoring.ts +82 -0
  138. package/src/api/pep.ts +30 -0
  139. package/src/api/risk.ts +26 -0
  140. package/src/api/screening.ts +37 -0
  141. package/src/api/team.ts +27 -0
  142. package/src/api/transactions.ts +37 -0
  143. package/src/components/admin/TenantCards.tsx +51 -0
  144. package/src/components/alerts/AlertActions.test.tsx +80 -0
  145. package/src/components/alerts/AlertActions.tsx +68 -0
  146. package/src/components/alerts/AlertCard.test.tsx +86 -0
  147. package/src/components/alerts/AlertCard.tsx +60 -0
  148. package/src/components/alerts/AlertDetailSidebar.tsx +63 -0
  149. package/src/components/alerts/AlertFilters.test.tsx +73 -0
  150. package/src/components/alerts/AlertFilters.tsx +88 -0
  151. package/src/components/alerts/EntityDetailsCard.test.tsx +61 -0
  152. package/src/components/alerts/EntityDetailsCard.tsx +37 -0
  153. package/src/components/alerts/NotesCard.test.tsx +39 -0
  154. package/src/components/alerts/NotesCard.tsx +28 -0
  155. package/src/components/auth/AuthDivider.tsx +18 -0
  156. package/src/components/auth/LoginForm.tsx +62 -0
  157. package/src/components/auth/LoginLeftPanel.tsx +43 -0
  158. package/src/components/auth/PasswordStrength.tsx +26 -0
  159. package/src/components/auth/SignInButtons.tsx +46 -0
  160. package/src/components/auth/SignupLeftPanel.tsx +35 -0
  161. package/src/components/auth/countries.ts +17 -0
  162. package/src/components/charts/AreaChart.tsx +45 -0
  163. package/src/components/charts/BarChart.tsx +48 -0
  164. package/src/components/charts/DonutChart.tsx +47 -0
  165. package/src/components/compliance/CaseActions.tsx +74 -0
  166. package/src/components/compliance/CaseTimeline.tsx +68 -0
  167. package/src/components/compliance/MediaResultCard.tsx +87 -0
  168. package/src/components/compliance/PEPResultCard.tsx +78 -0
  169. package/src/components/config/MatchingModesCard.test.tsx +75 -0
  170. package/src/components/config/MatchingModesCard.tsx +40 -0
  171. package/src/components/config/ScreeningLayersCard.test.tsx +57 -0
  172. package/src/components/config/ScreeningLayersCard.tsx +36 -0
  173. package/src/components/config/ThresholdsCard.test.tsx +79 -0
  174. package/src/components/config/ThresholdsCard.tsx +51 -0
  175. package/src/components/dashboard/ActivityFeed.tsx +93 -0
  176. package/src/components/dashboard/DashboardGreeting.tsx +23 -0
  177. package/src/components/dashboard/DashboardSkeleton.tsx +35 -0
  178. package/src/components/dashboard/QuickActions.tsx +52 -0
  179. package/src/components/dashboard/TopEntitiesTable.tsx +63 -0
  180. package/src/components/data/ComplianceMetrics.test.tsx +32 -0
  181. package/src/components/data/ComplianceMetrics.tsx +40 -0
  182. package/src/components/data/ConfidenceScore.test.tsx +62 -0
  183. package/src/components/data/ConfidenceScore.tsx +27 -0
  184. package/src/components/data/StatCard.test.tsx +72 -0
  185. package/src/components/data/StatCard.tsx +63 -0
  186. package/src/components/data/StatusBadge.test.tsx +63 -0
  187. package/src/components/data/StatusBadge.tsx +39 -0
  188. package/src/components/layout/AppShell.tsx +48 -0
  189. package/src/components/layout/Breadcrumbs.tsx +48 -0
  190. package/src/components/layout/CommandPalette.tsx +81 -0
  191. package/src/components/layout/DashboardLayout.tsx +19 -0
  192. package/src/components/layout/MobileHeader.tsx +30 -0
  193. package/src/components/layout/NavGroup.test.tsx +63 -0
  194. package/src/components/layout/NavGroup.tsx +64 -0
  195. package/src/components/layout/NotificationBell.tsx +89 -0
  196. package/src/components/layout/PageHeader.test.tsx +61 -0
  197. package/src/components/layout/PageHeader.tsx +19 -0
  198. package/src/components/layout/ProtectedRoute.tsx +31 -0
  199. package/src/components/layout/PublicLayout.tsx +17 -0
  200. package/src/components/layout/Sidebar.test.tsx +67 -0
  201. package/src/components/layout/Sidebar.tsx +92 -0
  202. package/src/components/layout/Toolbar.test.tsx +47 -0
  203. package/src/components/layout/Toolbar.tsx +68 -0
  204. package/src/components/layout/navItems.ts +94 -0
  205. package/src/components/lists/ListCard.tsx +78 -0
  206. package/src/components/lists/ListMarketplaceCard.tsx +77 -0
  207. package/src/components/screening/CircularConfidence.tsx +38 -0
  208. package/src/components/screening/LimitReachedBanner.tsx +31 -0
  209. package/src/components/screening/ListSelector.tsx +64 -0
  210. package/src/components/screening/MatchDetailHeader.tsx +64 -0
  211. package/src/components/screening/MatchEntityInfo.tsx +56 -0
  212. package/src/components/screening/MatchEvidenceBars.tsx +65 -0
  213. package/src/components/screening/MatchMetadata.tsx +95 -0
  214. package/src/components/screening/ScreenResults.tsx +49 -0
  215. package/src/components/screening/ScreeningForm.test.tsx +83 -0
  216. package/src/components/screening/ScreeningForm.tsx +100 -0
  217. package/src/components/screening/ScreeningLayersList.test.tsx +33 -0
  218. package/src/components/screening/ScreeningLayersList.tsx +46 -0
  219. package/src/components/screening/ScreeningProgress.tsx +91 -0
  220. package/src/components/screening/ScreeningQuotaBanner.tsx +64 -0
  221. package/src/components/screening/ScreeningResultCard.tsx +47 -0
  222. package/src/components/screening/ScreeningResultRow.tsx +45 -0
  223. package/src/components/screening/ScreeningResults.test.tsx +37 -0
  224. package/src/components/screening/ScreeningResults.tsx +33 -0
  225. package/src/components/screening/ShareResults.tsx +103 -0
  226. package/src/components/screening/ThresholdSlider.tsx +23 -0
  227. package/src/components/transactions/WebhookCTA.tsx +63 -0
  228. package/src/components/ui/Avatar.test.tsx +47 -0
  229. package/src/components/ui/Avatar.tsx +35 -0
  230. package/src/components/ui/Badge.test.tsx +49 -0
  231. package/src/components/ui/Badge.tsx +33 -0
  232. package/src/components/ui/Button.test.tsx +56 -0
  233. package/src/components/ui/Button.tsx +46 -0
  234. package/src/components/ui/Card.test.tsx +61 -0
  235. package/src/components/ui/Card.tsx +29 -0
  236. package/src/components/ui/ConfirmModal.tsx +67 -0
  237. package/src/components/ui/Divider.test.tsx +24 -0
  238. package/src/components/ui/Divider.tsx +5 -0
  239. package/src/components/ui/EmptyState.test.tsx +49 -0
  240. package/src/components/ui/EmptyState.tsx +22 -0
  241. package/src/components/ui/ErrorBoundary.tsx +44 -0
  242. package/src/components/ui/ExportMenu.tsx +71 -0
  243. package/src/components/ui/LanguageSwitcher.tsx +37 -0
  244. package/src/components/ui/LoadingSpinner.test.tsx +41 -0
  245. package/src/components/ui/LoadingSpinner.tsx +21 -0
  246. package/src/components/ui/MetricCard.tsx +63 -0
  247. package/src/components/ui/ScoreRing.tsx +51 -0
  248. package/src/components/ui/SearchField.test.tsx +55 -0
  249. package/src/components/ui/SearchField.tsx +31 -0
  250. package/src/components/ui/SeverityBadge.tsx +57 -0
  251. package/src/components/ui/ThemeToggle.tsx +37 -0
  252. package/src/components/ui/Toast.test.tsx +79 -0
  253. package/src/components/ui/Toast.tsx +75 -0
  254. package/src/components/ui/Toggle.test.tsx +85 -0
  255. package/src/components/ui/Toggle.tsx +46 -0
  256. package/src/context/AuthContext.tsx +71 -0
  257. package/src/data/pepProfiles.ts +76 -0
  258. package/src/data/pepProfilesExtra.ts +58 -0
  259. package/src/hooks/useAlerts.ts +36 -0
  260. package/src/hooks/useAnalytics.ts +23 -0
  261. package/src/hooks/useApi.test.ts +79 -0
  262. package/src/hooks/useApi.ts +33 -0
  263. package/src/hooks/useAudit.ts +28 -0
  264. package/src/hooks/useBilling.ts +38 -0
  265. package/src/hooks/useConfig.ts +35 -0
  266. package/src/hooks/useDebounce.test.ts +84 -0
  267. package/src/hooks/useDebounce.ts +15 -0
  268. package/src/hooks/useDirection.ts +15 -0
  269. package/src/hooks/useLists.ts +34 -0
  270. package/src/hooks/useMediaQuery.test.ts +97 -0
  271. package/src/hooks/useMediaQuery.ts +28 -0
  272. package/src/hooks/useScreening.ts +33 -0
  273. package/src/hooks/useSidebar.ts +18 -0
  274. package/src/hooks/useUsage.ts +27 -0
  275. package/src/i18n/config.ts +33 -0
  276. package/src/i18n/locales/ar/admin.json +19 -0
  277. package/src/i18n/locales/ar/alerts.json +52 -0
  278. package/src/i18n/locales/ar/analytics.json +9 -0
  279. package/src/i18n/locales/ar/audit.json +12 -0
  280. package/src/i18n/locales/ar/auth.json +60 -0
  281. package/src/i18n/locales/ar/batch.json +5 -0
  282. package/src/i18n/locales/ar/billing.json +41 -0
  283. package/src/i18n/locales/ar/common.json +65 -0
  284. package/src/i18n/locales/ar/compliance.json +83 -0
  285. package/src/i18n/locales/ar/config.json +13 -0
  286. package/src/i18n/locales/ar/dashboard.json +19 -0
  287. package/src/i18n/locales/ar/errors.json +9 -0
  288. package/src/i18n/locales/ar/index.ts +29 -0
  289. package/src/i18n/locales/ar/legal.json +10 -0
  290. package/src/i18n/locales/ar/lists.json +6 -0
  291. package/src/i18n/locales/ar/marketing.json +110 -0
  292. package/src/i18n/locales/ar/monitoring.json +15 -0
  293. package/src/i18n/locales/ar/nav.json +35 -0
  294. package/src/i18n/locales/ar/onboarding.json +25 -0
  295. package/src/i18n/locales/ar/platform.json +23 -0
  296. package/src/i18n/locales/ar/screening.json +26 -0
  297. package/src/i18n/locales/ar/team.json +11 -0
  298. package/src/i18n/locales/en/admin.json +21 -0
  299. package/src/i18n/locales/en/alerts.json +52 -0
  300. package/src/i18n/locales/en/analytics.json +9 -0
  301. package/src/i18n/locales/en/audit.json +12 -0
  302. package/src/i18n/locales/en/auth.json +60 -0
  303. package/src/i18n/locales/en/batch.json +5 -0
  304. package/src/i18n/locales/en/billing.json +87 -0
  305. package/src/i18n/locales/en/common.json +65 -0
  306. package/src/i18n/locales/en/compliance.json +83 -0
  307. package/src/i18n/locales/en/config.json +29 -0
  308. package/src/i18n/locales/en/dashboard.json +23 -0
  309. package/src/i18n/locales/en/errors.json +9 -0
  310. package/src/i18n/locales/en/index.ts +29 -0
  311. package/src/i18n/locales/en/legal.json +114 -0
  312. package/src/i18n/locales/en/lists.json +6 -0
  313. package/src/i18n/locales/en/marketing.json +174 -0
  314. package/src/i18n/locales/en/monitoring.json +15 -0
  315. package/src/i18n/locales/en/nav.json +35 -0
  316. package/src/i18n/locales/en/onboarding.json +31 -0
  317. package/src/i18n/locales/en/platform.json +25 -0
  318. package/src/i18n/locales/en/screening.json +26 -0
  319. package/src/i18n/locales/en/team.json +12 -0
  320. package/src/i18n/locales/he/admin.json +19 -0
  321. package/src/i18n/locales/he/alerts.json +52 -0
  322. package/src/i18n/locales/he/analytics.json +9 -0
  323. package/src/i18n/locales/he/audit.json +12 -0
  324. package/src/i18n/locales/he/auth.json +60 -0
  325. package/src/i18n/locales/he/batch.json +5 -0
  326. package/src/i18n/locales/he/billing.json +41 -0
  327. package/src/i18n/locales/he/common.json +65 -0
  328. package/src/i18n/locales/he/compliance.json +83 -0
  329. package/src/i18n/locales/he/config.json +13 -0
  330. package/src/i18n/locales/he/dashboard.json +19 -0
  331. package/src/i18n/locales/he/errors.json +9 -0
  332. package/src/i18n/locales/he/index.ts +29 -0
  333. package/src/i18n/locales/he/legal.json +10 -0
  334. package/src/i18n/locales/he/lists.json +6 -0
  335. package/src/i18n/locales/he/marketing.json +110 -0
  336. package/src/i18n/locales/he/monitoring.json +15 -0
  337. package/src/i18n/locales/he/nav.json +35 -0
  338. package/src/i18n/locales/he/onboarding.json +25 -0
  339. package/src/i18n/locales/he/platform.json +23 -0
  340. package/src/i18n/locales/he/screening.json +26 -0
  341. package/src/i18n/locales/he/team.json +11 -0
  342. package/src/index.css +112 -0
  343. package/src/main.tsx +15 -0
  344. package/src/pages/APIKeys.tsx +120 -0
  345. package/src/pages/AddMonitorModal.tsx +30 -0
  346. package/src/pages/AdverseMedia.test.tsx +18 -0
  347. package/src/pages/AdverseMedia.tsx +89 -0
  348. package/src/pages/AlertDetailPage.tsx +64 -0
  349. package/src/pages/AlertQueue.test.tsx +48 -0
  350. package/src/pages/AlertQueue.tsx +63 -0
  351. package/src/pages/Analytics.tsx +50 -0
  352. package/src/pages/AuditTrail.tsx +64 -0
  353. package/src/pages/BatchJobs.tsx +79 -0
  354. package/src/pages/CaseDetail.tsx +72 -0
  355. package/src/pages/CaseManagement.test.tsx +31 -0
  356. package/src/pages/CaseManagement.tsx +92 -0
  357. package/src/pages/Configuration.test.tsx +96 -0
  358. package/src/pages/Configuration.tsx +123 -0
  359. package/src/pages/CryptoScreening.tsx +109 -0
  360. package/src/pages/Dashboard.test.tsx +51 -0
  361. package/src/pages/Dashboard.tsx +66 -0
  362. package/src/pages/EDDWorkflow.test.tsx +29 -0
  363. package/src/pages/EDDWorkflow.tsx +73 -0
  364. package/src/pages/ForgotPassword.test.tsx +49 -0
  365. package/src/pages/ForgotPassword.tsx +67 -0
  366. package/src/pages/ListsMarketplace.tsx +102 -0
  367. package/src/pages/Login.test.tsx +100 -0
  368. package/src/pages/Login.tsx +57 -0
  369. package/src/pages/MFASetup.tsx +114 -0
  370. package/src/pages/MonitorProfileCard.tsx +27 -0
  371. package/src/pages/Monitoring.tsx +68 -0
  372. package/src/pages/Onboarding.test.tsx +36 -0
  373. package/src/pages/Onboarding.tsx +60 -0
  374. package/src/pages/PEPScreening.test.tsx +15 -0
  375. package/src/pages/PEPScreening.tsx +100 -0
  376. package/src/pages/ResetPassword.tsx +81 -0
  377. package/src/pages/RiskAssessment.test.tsx +15 -0
  378. package/src/pages/RiskAssessment.tsx +108 -0
  379. package/src/pages/SanctionsLists.tsx +74 -0
  380. package/src/pages/ScreenEntity.test.tsx +82 -0
  381. package/src/pages/ScreenEntity.tsx +76 -0
  382. package/src/pages/Signup.test.tsx +98 -0
  383. package/src/pages/Signup.tsx +92 -0
  384. package/src/pages/TaskHistory.tsx +183 -0
  385. package/src/pages/Team.test.tsx +15 -0
  386. package/src/pages/Team.tsx +140 -0
  387. package/src/pages/TransactionMonitoring.test.tsx +18 -0
  388. package/src/pages/TransactionMonitoring.tsx +118 -0
  389. package/src/pages/TxnScreening.tsx +125 -0
  390. package/src/pages/UBOChain.test.tsx +35 -0
  391. package/src/pages/UBOChain.tsx +65 -0
  392. package/src/pages/Webhooks.tsx +137 -0
  393. package/src/pages/admin/DataSources.tsx +230 -0
  394. package/src/pages/admin/Operations.tsx +103 -0
  395. package/src/pages/admin/ScheduledTasks.tsx +155 -0
  396. package/src/pages/admin/SystemHealth.tsx +58 -0
  397. package/src/pages/admin/TenantDetail.tsx +62 -0
  398. package/src/pages/admin/Tenants.test.tsx +20 -0
  399. package/src/pages/admin/Tenants.tsx +63 -0
  400. package/src/pages/admin/opsRunners.ts +81 -0
  401. package/src/pages/admin/opsTerminal.tsx +63 -0
  402. package/src/pages/billing/ActiveSubscriptions.tsx +41 -0
  403. package/src/pages/billing/AddProductModal.tsx +99 -0
  404. package/src/pages/billing/BillingPage.test.tsx +30 -0
  405. package/src/pages/billing/BillingPage.tsx +67 -0
  406. package/src/pages/billing/CurrentPlan.tsx +50 -0
  407. package/src/pages/billing/InvoiceList.tsx +104 -0
  408. package/src/pages/billing/InvoiceRow.tsx +37 -0
  409. package/src/pages/billing/LemonSqueezySetup.tsx +51 -0
  410. package/src/pages/billing/PaymentAlert.tsx +33 -0
  411. package/src/pages/billing/PlanComparison.tsx +53 -0
  412. package/src/pages/billing/ProductUsage.tsx +43 -0
  413. package/src/pages/billing/PromoCodeInput.tsx +58 -0
  414. package/src/pages/billing/SeatManager.tsx +80 -0
  415. package/src/pages/billing/SeatRow.tsx +32 -0
  416. package/src/pages/billing/SubscriptionCard.tsx +73 -0
  417. package/src/pages/billing/UpgradeModal.tsx +53 -0
  418. package/src/pages/billing/UsageHistory.tsx +37 -0
  419. package/src/pages/billing/UsageMeter.tsx +26 -0
  420. package/src/pages/billing/UsageOverview.tsx +38 -0
  421. package/src/pages/legal/PrivacyPolicy.tsx +25 -0
  422. package/src/pages/legal/TermsOfService.tsx +25 -0
  423. package/src/pages/marketing/BundleCallout.tsx +24 -0
  424. package/src/pages/marketing/CTASection.tsx +48 -0
  425. package/src/pages/marketing/CaseStudy.tsx +37 -0
  426. package/src/pages/marketing/ComparisonTable.tsx +66 -0
  427. package/src/pages/marketing/CompetitiveEdge.tsx +55 -0
  428. package/src/pages/marketing/DataCoverage.tsx +54 -0
  429. package/src/pages/marketing/DataRain.tsx +30 -0
  430. package/src/pages/marketing/EnterpriseCTA.tsx +26 -0
  431. package/src/pages/marketing/FAQItem.tsx +27 -0
  432. package/src/pages/marketing/FAQSchema.tsx +43 -0
  433. package/src/pages/marketing/FAQSection.tsx +19 -0
  434. package/src/pages/marketing/FeatureDetail.tsx +19 -0
  435. package/src/pages/marketing/FeaturesGrid.tsx +60 -0
  436. package/src/pages/marketing/FeaturesSpotlight.tsx +68 -0
  437. package/src/pages/marketing/FooterSection.tsx +92 -0
  438. package/src/pages/marketing/GradientOrbs.tsx +26 -0
  439. package/src/pages/marketing/HeroSearch.tsx +113 -0
  440. package/src/pages/marketing/HeroSection.tsx +72 -0
  441. package/src/pages/marketing/LandingPage.tsx +45 -0
  442. package/src/pages/marketing/LogoCloud.tsx +31 -0
  443. package/src/pages/marketing/LogoMarquee.tsx +45 -0
  444. package/src/pages/marketing/MarketingNav.tsx +80 -0
  445. package/src/pages/marketing/MatchingLayers.tsx +78 -0
  446. package/src/pages/marketing/MobileMenu.tsx +45 -0
  447. package/src/pages/marketing/PricingCard.tsx +57 -0
  448. package/src/pages/marketing/PricingFeatureRow.tsx +16 -0
  449. package/src/pages/marketing/PricingSection.tsx +103 -0
  450. package/src/pages/marketing/PricingToggle.tsx +40 -0
  451. package/src/pages/marketing/ProductPricingCards.tsx +23 -0
  452. package/src/pages/marketing/ProductShowcase.tsx +81 -0
  453. package/src/pages/marketing/ProductTabs.tsx +45 -0
  454. package/src/pages/marketing/QuoteRotator.tsx +56 -0
  455. package/src/pages/marketing/StatsBar.tsx +81 -0
  456. package/src/pages/marketing/StatsSection.tsx +44 -0
  457. package/src/pages/marketing/TestimonialCard.tsx +38 -0
  458. package/src/pages/marketing/TestimonialsSection.tsx +32 -0
  459. package/src/pages/marketing/animations.tsx +56 -0
  460. package/src/pages/onboarding/CountryStep.tsx +38 -0
  461. package/src/pages/onboarding/ListsStep.tsx +44 -0
  462. package/src/pages/onboarding/StepIndicator.tsx +34 -0
  463. package/src/pages/onboarding/ThresholdStep.tsx +49 -0
  464. package/src/pages/platform/APIKeys.tsx +102 -0
  465. package/src/pages/platform/Overview.tsx +58 -0
  466. package/src/pages/platform/Users.tsx +110 -0
  467. package/src/pages/reporting/ComplianceReport.tsx +99 -0
  468. package/src/pages/reporting/SARForm.tsx +99 -0
  469. package/src/routes/appRoutes.tsx +60 -0
  470. package/src/routes/compliance.tsx +28 -0
  471. package/src/routes/lazyCompliance.ts +34 -0
  472. package/src/routes/lazyPages.ts +35 -0
  473. package/src/routes/lazyPlatform.ts +5 -0
  474. package/src/routes/platform.tsx +15 -0
  475. package/src/styles/effects.css +76 -0
  476. package/src/test/setup.ts +25 -0
  477. package/src/test/utils.tsx +49 -0
  478. package/src/types/alert.ts +31 -0
  479. package/src/types/analytics.ts +16 -0
  480. package/src/types/audit.ts +11 -0
  481. package/src/types/billing.ts +60 -0
  482. package/src/types/common.ts +15 -0
  483. package/src/types/config.ts +19 -0
  484. package/src/types/entity.ts +34 -0
  485. package/src/types/index.ts +8 -0
  486. package/src/types/list.ts +15 -0
  487. package/src/types/screening.ts +32 -0
  488. package/src/vite-env.d.ts +1 -0
  489. package/tailwind.config.js +65 -0
  490. package/tsconfig.json +22 -0
  491. package/vite.config.ts +11 -0
  492. package/vitest.config.ts +19 -0
@@ -0,0 +1,49 @@
1
+ import { Card } from '../ui/Card'
2
+ import { ScreeningResultRow } from './ScreeningResultRow'
3
+ import { ShareResults } from './ShareResults'
4
+ import type { ScreenResponse } from '../../types'
5
+
6
+ export type { ScreenResponse }
7
+
8
+ interface Props {
9
+ data: ScreenResponse
10
+ }
11
+
12
+ export function ScreenResults({ data }: Props) {
13
+ const matches = data.matches ?? []
14
+ const totalMatches = data.total_matches ?? matches.length
15
+
16
+ if (totalMatches === 0) {
17
+ return (
18
+ <Card className="text-center py-xxl">
19
+ <div className="text-apple-green text-3xl mb-md">&#10003;</div>
20
+ <p className="sf-headline text-apple-green">No Matches Found</p>
21
+ <p className="sf-caption mt-sm">
22
+ No sanctions matches for &ldquo;{data.query}&rdquo;
23
+ </p>
24
+ <div className="mt-lg flex justify-center">
25
+ <ShareResults data={data} />
26
+ </div>
27
+ </Card>
28
+ )
29
+ }
30
+
31
+ return (
32
+ <div className="space-y-md">
33
+ <div className="flex items-center justify-between">
34
+ <p className="sf-body">
35
+ <span className="sf-headline">{totalMatches}</span> matches
36
+ </p>
37
+ <div className="flex items-center gap-md">
38
+ <span className="sf-caption text-white/50">
39
+ {data.processing_time_ms ?? 0}ms
40
+ </span>
41
+ <ShareResults data={data} />
42
+ </div>
43
+ </div>
44
+ {matches.map((m, i) => (
45
+ <ScreeningResultRow key={m.entity_id} match={m} index={i} />
46
+ ))}
47
+ </div>
48
+ )
49
+ }
@@ -0,0 +1,83 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import userEvent from '@testing-library/user-event'
3
+ import { describe, it, expect, vi } from 'vitest'
4
+ import { ScreeningForm } from './ScreeningForm'
5
+
6
+ describe('ScreeningForm', () => {
7
+ it('renders entity type selection', () => {
8
+ render(<ScreeningForm onSubmit={vi.fn()} />)
9
+ expect(screen.getByRole('button', { name: 'Individual' })).toBeInTheDocument()
10
+ expect(screen.getByRole('button', { name: 'Company' })).toBeInTheDocument()
11
+ })
12
+
13
+ it('renders individual fields by default', () => {
14
+ render(<ScreeningForm onSubmit={vi.fn()} />)
15
+ expect(screen.getByPlaceholderText('First Name')).toBeInTheDocument()
16
+ expect(screen.getByPlaceholderText('Last Name')).toBeInTheDocument()
17
+ expect(screen.getByPlaceholderText('Nationality')).toBeInTheDocument()
18
+ })
19
+
20
+ it('switches to company fields when company type selected', async () => {
21
+ render(<ScreeningForm onSubmit={vi.fn()} />)
22
+ const companyTab = screen.getByRole('button', { name: 'Company' })
23
+ await userEvent.click(companyTab)
24
+ expect(screen.getByPlaceholderText('Company Name')).toBeInTheDocument()
25
+ expect(screen.queryByPlaceholderText('First Name')).not.toBeInTheDocument()
26
+ })
27
+
28
+ it('submits individual form data', async () => {
29
+ const handler = vi.fn()
30
+ render(<ScreeningForm onSubmit={handler} />)
31
+
32
+ await userEvent.type(screen.getByPlaceholderText('First Name'), 'John')
33
+ await userEvent.type(screen.getByPlaceholderText('Last Name'), 'Doe')
34
+ await userEvent.type(screen.getByPlaceholderText('Nationality'), 'US')
35
+
36
+ await userEvent.click(screen.getByRole('button', { name: /screen entity/i }))
37
+
38
+ expect(handler).toHaveBeenCalledWith(
39
+ expect.objectContaining({
40
+ firstName: 'John',
41
+ lastName: 'Doe',
42
+ nationality: 'US',
43
+ }),
44
+ 'individual'
45
+ )
46
+ })
47
+
48
+ it('submits company form data', async () => {
49
+ const handler = vi.fn()
50
+ render(<ScreeningForm onSubmit={handler} />)
51
+
52
+ const companyTab = screen.getByRole('button', { name: 'Company' })
53
+ await userEvent.click(companyTab)
54
+
55
+ await userEvent.type(screen.getByPlaceholderText('Company Name'), 'Acme Corp')
56
+ await userEvent.click(screen.getByRole('button', { name: /screen entity/i }))
57
+
58
+ expect(handler).toHaveBeenCalledWith(
59
+ expect.objectContaining({
60
+ companyName: 'Acme Corp',
61
+ }),
62
+ 'company'
63
+ )
64
+ })
65
+
66
+ it('disables submit button when loading', () => {
67
+ render(<ScreeningForm onSubmit={vi.fn()} loading={true} />)
68
+ expect(screen.getByRole('button', { name: /screening/i })).toBeDisabled()
69
+ })
70
+
71
+ it('shows loading text when loading', () => {
72
+ render(<ScreeningForm onSubmit={vi.fn()} loading={true} />)
73
+ expect(screen.getByText('Screening...')).toBeInTheDocument()
74
+ })
75
+
76
+ it('prevents form submission with Enter key', async () => {
77
+ const handler = vi.fn()
78
+ render(<ScreeningForm onSubmit={handler} />)
79
+ const form = screen.getByPlaceholderText('First Name').closest('form')
80
+ await userEvent.type(form!, '{Enter}')
81
+ expect(handler).not.toHaveBeenCalled()
82
+ })
83
+ })
@@ -0,0 +1,100 @@
1
+ import React, { useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Search } from 'lucide-react';
4
+ import type { EntityType } from '../../types';
5
+
6
+ interface FormData {
7
+ firstName: string;
8
+ lastName: string;
9
+ companyName: string;
10
+ dob: string;
11
+ nationality: string;
12
+ }
13
+
14
+ interface ScreeningFormProps {
15
+ onSubmit: (data: FormData, type: EntityType) => void;
16
+ loading?: boolean;
17
+ }
18
+
19
+ export function ScreeningForm({ onSubmit, loading }: ScreeningFormProps) {
20
+ const { t } = useTranslation('screening');
21
+ const [entityType, setEntityType] = useState<EntityType>('individual');
22
+ const [errors, setErrors] = useState<Record<string, string>>({});
23
+ const [data, setData] = useState<FormData>({
24
+ firstName: '', lastName: '', companyName: '', dob: '', nationality: '',
25
+ });
26
+
27
+ const tabClass = (active: boolean) =>
28
+ `flex-1 py-md text-center text-[14px] font-semibold rounded-apple-md transition-all cursor-pointer ${active
29
+ ? 'bg-gradient-to-r from-apple-blue to-[#00D4AA] text-white shadow-[0_0_16px_rgba(10,132,255,0.3)]'
30
+ : 'text-apple-label-secondary hover:text-white hover:bg-white/[0.04]'}`;
31
+
32
+ const validate = (): boolean => {
33
+ const errs: Record<string, string> = {};
34
+ if (entityType === 'individual') {
35
+ if (!data.firstName.trim()) errs.firstName = 'First name is required';
36
+ if (!data.lastName.trim()) errs.lastName = 'Last name is required';
37
+ } else {
38
+ if (!data.companyName.trim()) errs.companyName = 'Company name is required';
39
+ }
40
+ setErrors(errs);
41
+ return Object.keys(errs).length === 0;
42
+ };
43
+
44
+ const handleSubmit = (e: React.FormEvent) => {
45
+ e.preventDefault();
46
+ if (validate()) onSubmit(data, entityType);
47
+ };
48
+
49
+ const inputCls = (field: string) =>
50
+ `input-field w-full ${errors[field] ? 'border-apple-red' : ''}`;
51
+
52
+ return (
53
+ <div className="card-vibrancy p-xl">
54
+ <div className="flex gap-sm p-xs bg-white/[0.03] rounded-apple-md mb-xl border border-white/[0.04]">
55
+ <button type="button" className={tabClass(entityType === 'individual')}
56
+ onClick={() => setEntityType('individual')}>{t('form.individual')}</button>
57
+ <button type="button" className={tabClass(entityType === 'company')}
58
+ onClick={() => setEntityType('company')}>{t('form.company')}</button>
59
+ </div>
60
+
61
+ <form onSubmit={handleSubmit} className="space-y-lg">
62
+ {entityType === 'individual' ? (
63
+ <>
64
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-lg">
65
+ <div>
66
+ <input type="text" required aria-label={t('form.first_name')}
67
+ placeholder={t('form.first_name')} className={inputCls('firstName')}
68
+ value={data.firstName} onChange={(e) => setData({ ...data, firstName: e.target.value })} />
69
+ {errors.firstName && <p className="text-apple-red text-xs mt-1">{errors.firstName}</p>}
70
+ </div>
71
+ <div>
72
+ <input type="text" required aria-label={t('form.last_name')}
73
+ placeholder={t('form.last_name')} className={inputCls('lastName')}
74
+ value={data.lastName} onChange={(e) => setData({ ...data, lastName: e.target.value })} />
75
+ {errors.lastName && <p className="text-apple-red text-xs mt-1">{errors.lastName}</p>}
76
+ </div>
77
+ </div>
78
+ <input type="date" aria-label={t('form.dob')} className="input-field w-full"
79
+ value={data.dob} onChange={(e) => setData({ ...data, dob: e.target.value })} />
80
+ <input type="text" aria-label={t('form.nationality')} placeholder={t('form.nationality')}
81
+ className="input-field w-full" value={data.nationality}
82
+ onChange={(e) => setData({ ...data, nationality: e.target.value })} />
83
+ </>
84
+ ) : (
85
+ <div>
86
+ <input type="text" required aria-label={t('form.company_name')}
87
+ placeholder={t('form.company_name')} className={inputCls('companyName')}
88
+ value={data.companyName} onChange={(e) => setData({ ...data, companyName: e.target.value })} />
89
+ {errors.companyName && <p className="text-apple-red text-xs mt-1">{errors.companyName}</p>}
90
+ </div>
91
+ )}
92
+ <button type="submit" disabled={loading}
93
+ className="button-primary w-full flex items-center justify-center gap-sm disabled:opacity-50 text-[15px] font-bold tracking-wide">
94
+ <Search className="w-5 h-5" />
95
+ {loading ? t('form.submitting') : t('form.submit')}
96
+ </button>
97
+ </form>
98
+ </div>
99
+ );
100
+ }
@@ -0,0 +1,33 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import userEvent from '@testing-library/user-event'
3
+ import { describe, it, expect } from 'vitest'
4
+ import { ScreeningLayersList } from './ScreeningLayersList'
5
+
6
+ describe('ScreeningLayersList', () => {
7
+ it('renders title', () => {
8
+ render(<ScreeningLayersList />)
9
+ expect(screen.getByText(/screening layers/i)).toBeInTheDocument()
10
+ })
11
+
12
+ it('renders all four layer toggles', () => {
13
+ render(<ScreeningLayersList />)
14
+ expect(screen.getByText(/ofac sdn/i)).toBeInTheDocument()
15
+ expect(screen.getByText(/eu sanctions/i)).toBeInTheDocument()
16
+ expect(screen.getByText(/un consolidated/i)).toBeInTheDocument()
17
+ expect(screen.getByText(/custom lists/i)).toBeInTheDocument()
18
+ })
19
+
20
+ it('all toggles are checked by default', () => {
21
+ const { container } = render(<ScreeningLayersList />)
22
+ const greenToggles = container.querySelectorAll('div[class*="bg-apple-green"]')
23
+ expect(greenToggles.length).toBe(4)
24
+ })
25
+
26
+ it('toggles a layer off when clicked', async () => {
27
+ const { container } = render(<ScreeningLayersList />)
28
+ const toggles = container.querySelectorAll('div[class*="rounded-full"]')
29
+ await userEvent.click(toggles[0])
30
+ const greenToggles = container.querySelectorAll('div[class*="bg-apple-green"]')
31
+ expect(greenToggles.length).toBe(3)
32
+ })
33
+ })
@@ -0,0 +1,46 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Shield, Globe, Flag, Database } from 'lucide-react';
4
+ import { Card } from '../ui/Card';
5
+ import { Toggle } from '../ui/Toggle';
6
+
7
+ const layerConfig = [
8
+ { key: 'ofac', icon: Shield, color: 'text-apple-red', glow: 'shadow-[0_0_12px_rgba(255,69,58,0.15)]', label: 'layers.ofac_sdn' },
9
+ { key: 'eu', icon: Globe, color: 'text-apple-blue', glow: 'shadow-[0_0_12px_rgba(10,132,255,0.15)]', label: 'layers.eu_sanctions' },
10
+ { key: 'un', icon: Flag, color: 'text-apple-orange', glow: 'shadow-[0_0_12px_rgba(255,159,10,0.15)]', label: 'layers.un_consolidated' },
11
+ { key: 'custom', icon: Database, color: 'text-apple-green', glow: 'shadow-[0_0_12px_rgba(48,209,88,0.15)]', label: 'layers.custom_lists' },
12
+ ] as const;
13
+
14
+ type LayerKey = (typeof layerConfig)[number]['key'];
15
+
16
+ export function ScreeningLayersList() {
17
+ const { t } = useTranslation('screening');
18
+ const [layers, setLayers] = React.useState<Record<LayerKey, boolean>>({
19
+ ofac: true, eu: true, un: true, custom: true,
20
+ });
21
+
22
+ return (
23
+ <Card>
24
+ <h3 className="sf-headline mb-lg">{t('layers.title')}</h3>
25
+ <div className="space-y-sm">
26
+ {layerConfig.map(({ key, icon: Icon, color, glow, label }) => (
27
+ <div key={key}
28
+ className={`flex items-center gap-md p-md rounded-apple-md bg-white/[0.03]
29
+ border border-white/[0.04] transition-all hover:bg-white/[0.06]
30
+ ${layers[key] ? glow : ''}`}>
31
+ <div className={`w-8 h-8 rounded-full bg-white/[0.06] flex items-center justify-center flex-shrink-0`}>
32
+ <Icon className={`w-4 h-4 ${color}`} />
33
+ </div>
34
+ <div className="flex-1">
35
+ <Toggle
36
+ checked={layers[key]}
37
+ onChange={(v) => setLayers({ ...layers, [key]: v })}
38
+ label={t(label)}
39
+ />
40
+ </div>
41
+ </div>
42
+ ))}
43
+ </div>
44
+ </Card>
45
+ );
46
+ }
@@ -0,0 +1,91 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { Shield, Search, Fingerprint, Volume2, Hash, Sparkles, Network, Check } from 'lucide-react'
3
+
4
+ const LAYERS = [
5
+ { icon: Search, label: 'Exact Match', desc: 'Unicode normalized comparison' },
6
+ { icon: Fingerprint, label: 'Fuzzy Match', desc: 'Jaro-Winkler similarity' },
7
+ { icon: Volume2, label: 'Phonetic', desc: 'Soundex + Double Metaphone' },
8
+ { icon: Hash, label: 'Token Match', desc: 'Jaccard coefficient' },
9
+ { icon: Sparkles, label: 'AI Embeddings', desc: 'pgvector semantic search' },
10
+ { icon: Network, label: 'Graph', desc: 'Relationship traversal' },
11
+ ] as const
12
+
13
+ interface Props {
14
+ query: string
15
+ }
16
+
17
+ export function ScreeningProgress({ query }: Props) {
18
+ const [activeLayer, setActiveLayer] = useState(0)
19
+ const [completed, setCompleted] = useState<Set<number>>(new Set())
20
+
21
+ useEffect(() => {
22
+ const interval = setInterval(() => {
23
+ setActiveLayer(prev => {
24
+ const next = prev + 1
25
+ if (next >= LAYERS.length) {
26
+ return 0
27
+ }
28
+ setCompleted(c => new Set([...c, prev]))
29
+ return next
30
+ })
31
+ }, 120)
32
+ return () => clearInterval(interval)
33
+ }, [])
34
+
35
+ return (
36
+ <div className="max-w-2xl mx-auto">
37
+ <div className="text-center mb-xl">
38
+ <div className="relative inline-flex items-center justify-center w-20 h-20 mb-md">
39
+ <div className="absolute inset-0 rounded-full bg-apple-blue/20 blur-2xl animate-pulse" />
40
+ <div className="absolute inset-0 rounded-full border-2 border-apple-blue/30 animate-ping" />
41
+ <Shield className="relative w-10 h-10 text-apple-blue" />
42
+ </div>
43
+ <h3 className="sf-headline text-white mb-xs">Screening in Progress</h3>
44
+ <p className="sf-caption text-apple-label-secondary">
45
+ Analyzing <span className="text-apple-blue font-medium">{query}</span> across 2.17M entities
46
+ </p>
47
+ </div>
48
+
49
+ <div className="space-y-sm">
50
+ {LAYERS.map((layer, i) => {
51
+ const isActive = i === activeLayer
52
+ const isDone = completed.has(i)
53
+ const Icon = layer.icon
54
+ return (
55
+ <div key={i}
56
+ className={`flex items-center gap-md p-md rounded-apple-md transition-all duration-300
57
+ ${isActive ? 'bg-apple-blue/10 border border-apple-blue/30 scale-[1.02]' : ''}
58
+ ${isDone && !isActive ? 'bg-white/[0.02] border border-white/[0.04]' : ''}
59
+ ${!isActive && !isDone ? 'bg-white/[0.01] border border-white/[0.02]' : ''}
60
+ `}>
61
+ <div className={`flex items-center justify-center w-9 h-9 rounded-apple-md transition-all
62
+ ${isActive ? 'bg-apple-blue text-white' : ''}
63
+ ${isDone && !isActive ? 'bg-apple-green/15 text-apple-green' : ''}
64
+ ${!isActive && !isDone ? 'bg-white/5 text-apple-label-tertiary' : ''}
65
+ `}>
66
+ {isDone && !isActive ? <Check className="w-4 h-4" /> : <Icon className={`w-4 h-4 ${isActive ? 'animate-pulse' : ''}`} />}
67
+ </div>
68
+ <div className="flex-1">
69
+ <p className={`text-sm font-medium transition-colors
70
+ ${isActive ? 'text-white' : 'text-apple-label-secondary'}`}>
71
+ {layer.label}
72
+ </p>
73
+ <p className="text-xs text-apple-label-tertiary">{layer.desc}</p>
74
+ </div>
75
+ {isActive && (
76
+ <div className="flex gap-xs">
77
+ <span className="w-1.5 h-1.5 rounded-full bg-apple-blue animate-pulse" />
78
+ <span className="w-1.5 h-1.5 rounded-full bg-apple-blue animate-pulse" style={{ animationDelay: '0.2s' }} />
79
+ <span className="w-1.5 h-1.5 rounded-full bg-apple-blue animate-pulse" style={{ animationDelay: '0.4s' }} />
80
+ </div>
81
+ )}
82
+ {isDone && !isActive && (
83
+ <span className="text-xs text-apple-green font-medium">Done</span>
84
+ )}
85
+ </div>
86
+ )
87
+ })}
88
+ </div>
89
+ </div>
90
+ )
91
+ }
@@ -0,0 +1,64 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { useNavigate } from 'react-router-dom'
3
+ import { ArrowUpCircle } from 'lucide-react'
4
+ import { screeningApi, ScreeningQuota } from '../../api/screening'
5
+
6
+ export function ScreeningQuotaBanner() {
7
+ const navigate = useNavigate()
8
+ const [quota, setQuota] = useState<ScreeningQuota | null>(null)
9
+
10
+ useEffect(() => {
11
+ screeningApi.getQuota().then(setQuota).catch(() => null)
12
+ }, [])
13
+
14
+ if (!quota) return null
15
+
16
+ // Unlimited plan or no enforcer
17
+ if (quota.limit < 0) return null
18
+
19
+ const pct = quota.limit > 0 ? (quota.used / quota.limit) * 100 : 0
20
+ const isLow = pct >= 80
21
+ const isExhausted = quota.remaining <= 0
22
+
23
+ const barColor = isExhausted
24
+ ? 'bg-apple-red'
25
+ : isLow
26
+ ? 'bg-amber-500'
27
+ : 'bg-apple-green'
28
+
29
+ const borderColor = isExhausted
30
+ ? 'border-apple-red/20'
31
+ : isLow
32
+ ? 'border-amber-500/20'
33
+ : 'border-white/[0.06]'
34
+
35
+ return (
36
+ <div className={`rounded-apple-md border ${borderColor} bg-white/[0.02] px-lg py-md`}>
37
+ <div className="flex items-center justify-between mb-xs">
38
+ <span className="text-xs text-apple-label-secondary">
39
+ {quota.used.toLocaleString()} / {quota.limit.toLocaleString()} screenings used
40
+ </span>
41
+ {isExhausted ? (
42
+ <button onClick={() => navigate('/billing')}
43
+ className="text-xs text-apple-blue hover:underline cursor-pointer font-medium">
44
+ Upgrade Plan
45
+ </button>
46
+ ) : isLow ? (
47
+ <span className="text-xs text-amber-400">
48
+ {quota.remaining.toLocaleString()} remaining
49
+ </span>
50
+ ) : (
51
+ <span className="text-xs text-apple-label-tertiary">
52
+ {quota.remaining.toLocaleString()} remaining
53
+ </span>
54
+ )}
55
+ </div>
56
+ <div className="w-full h-1.5 bg-white/10 rounded-full overflow-hidden">
57
+ <div
58
+ className={`h-full ${barColor} rounded-full transition-all duration-500`}
59
+ style={{ width: `${Math.min(pct, 100)}%` }}
60
+ />
61
+ </div>
62
+ </div>
63
+ )
64
+ }
@@ -0,0 +1,47 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { CircularConfidence } from './CircularConfidence';
4
+
5
+ interface Result {
6
+ name: string;
7
+ confidence: number;
8
+ }
9
+
10
+ interface Props {
11
+ result: Result;
12
+ index: number;
13
+ }
14
+
15
+ export function ScreeningResultCard({ result, index }: Props) {
16
+ const { t } = useTranslation('screening');
17
+ const delay = `${index * 80}ms`;
18
+
19
+ return (
20
+ <div
21
+ className="card-vibrancy p-lg flex items-center gap-xl animate-fade-in"
22
+ style={{ animationDelay: delay }}
23
+ >
24
+ <CircularConfidence score={result.confidence} />
25
+ <div className="flex-1 min-w-0">
26
+ <h4 className="sf-headline truncate">{result.name}</h4>
27
+ <p className="sf-caption">{t('results.match_found')}</p>
28
+ <ConfidenceBar score={result.confidence} />
29
+ </div>
30
+ </div>
31
+ );
32
+ }
33
+
34
+ function ConfidenceBar({ score }: { score: number }) {
35
+ const color = score >= 80 ? 'from-apple-red to-apple-orange'
36
+ : score >= 60 ? 'from-apple-orange to-apple-yellow'
37
+ : 'from-apple-green to-apple-blue';
38
+
39
+ return (
40
+ <div className="mt-md h-1.5 w-full bg-white/5 rounded-full overflow-hidden">
41
+ <div
42
+ className={`h-full bg-gradient-to-r ${color} rounded-full transition-all duration-500`}
43
+ style={{ width: `${score}%` }}
44
+ />
45
+ </div>
46
+ );
47
+ }
@@ -0,0 +1,45 @@
1
+ import { Card } from '../ui/Card'
2
+ import { MatchDetailHeader } from './MatchDetailHeader'
3
+ import { MatchEntityInfo } from './MatchEntityInfo'
4
+ import { MatchEvidenceBars } from './MatchEvidenceBars'
5
+ import { MatchMetadata } from './MatchMetadata'
6
+ import type { ScreenMatch } from '../../types'
7
+
8
+ export type { ScreenMatch }
9
+
10
+ interface Props {
11
+ match: ScreenMatch
12
+ index?: number
13
+ }
14
+
15
+ export function ScreeningResultRow({ match, index = 0 }: Props) {
16
+ const delay = `${index * 60}ms`
17
+
18
+ return (
19
+ <div className="animate-fade-in" style={{ animationDelay: delay }}>
20
+ <Card>
21
+ <div className="space-y-lg">
22
+ <MatchDetailHeader match={match} />
23
+
24
+ <div className="border-t border-white/[0.06] pt-md">
25
+ <MatchEntityInfo match={match} />
26
+ </div>
27
+
28
+ <div className="border-t border-white/[0.06] pt-md">
29
+ <MatchEvidenceBars layers={match.layers} />
30
+ </div>
31
+
32
+ {match.explanation && (
33
+ <p className="sf-caption text-white/50 italic">
34
+ {match.explanation}
35
+ </p>
36
+ )}
37
+
38
+ <div className="border-t border-white/[0.06] pt-md">
39
+ <MatchMetadata metadata={match.metadata} />
40
+ </div>
41
+ </div>
42
+ </Card>
43
+ </div>
44
+ )
45
+ }
@@ -0,0 +1,37 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import { describe, it, expect } from 'vitest'
3
+ import { ScreeningResults } from './ScreeningResults'
4
+
5
+ const mockResults = [
6
+ { name: 'John Doe', confidence: 0.95 },
7
+ { name: 'Jane Smith', confidence: 0.72 },
8
+ ]
9
+
10
+ describe('ScreeningResults', () => {
11
+ it('renders results title', () => {
12
+ render(<ScreeningResults results={mockResults} />)
13
+ expect(screen.getByText(/screening results/i)).toBeInTheDocument()
14
+ })
15
+
16
+ it('renders all result names', () => {
17
+ render(<ScreeningResults results={mockResults} />)
18
+ expect(screen.getByText('John Doe')).toBeInTheDocument()
19
+ expect(screen.getByText('Jane Smith')).toBeInTheDocument()
20
+ })
21
+
22
+ it('renders match description for each result', () => {
23
+ render(<ScreeningResults results={mockResults} />)
24
+ const matches = screen.getAllByText(/match found/i)
25
+ expect(matches).toHaveLength(2)
26
+ })
27
+
28
+ it('renders empty list for no results', () => {
29
+ const { container } = render(<ScreeningResults results={[]} />)
30
+ expect(container.querySelectorAll('[class*="space-y-md"] > div')).toHaveLength(0)
31
+ })
32
+
33
+ it('renders one result correctly', () => {
34
+ render(<ScreeningResults results={[{ name: 'Solo', confidence: 0.5 }]} />)
35
+ expect(screen.getByText('Solo')).toBeInTheDocument()
36
+ })
37
+ })
@@ -0,0 +1,33 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { ScreeningResultCard } from './ScreeningResultCard';
4
+
5
+ interface Result {
6
+ name: string;
7
+ confidence: number;
8
+ }
9
+
10
+ interface ResultsProps {
11
+ results: Result[];
12
+ processingTime?: number;
13
+ }
14
+
15
+ export function ScreeningResults({ results, processingTime }: ResultsProps) {
16
+ const { t } = useTranslation('screening');
17
+
18
+ return (
19
+ <div className="mt-xxl">
20
+ <div className="flex items-center justify-between mb-lg">
21
+ <h2 className="sf-headline text-xl">{t('results.title')}</h2>
22
+ {processingTime !== undefined && (
23
+ <span className="badge-blue">{processingTime}ms</span>
24
+ )}
25
+ </div>
26
+ <div className="space-y-md">
27
+ {results.map((result, idx) => (
28
+ <ScreeningResultCard key={idx} result={result} index={idx} />
29
+ ))}
30
+ </div>
31
+ </div>
32
+ );
33
+ }