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 { render, screen } from '@testing-library/react'
2
+ import { describe, it, expect } from 'vitest'
3
+ import { EmptyState } from './EmptyState'
4
+ import { Button } from './Button'
5
+
6
+ describe('EmptyState', () => {
7
+ it('renders title', () => {
8
+ render(<EmptyState title="No results" />)
9
+ expect(screen.getByText('No results')).toBeInTheDocument()
10
+ })
11
+
12
+ it('renders description', () => {
13
+ render(
14
+ <EmptyState
15
+ title="No data"
16
+ description="Try adjusting your filters"
17
+ />
18
+ )
19
+ expect(screen.getByText('Try adjusting your filters')).toBeInTheDocument()
20
+ })
21
+
22
+ it('renders action button', () => {
23
+ render(
24
+ <EmptyState
25
+ title="Empty"
26
+ action={<Button>Go Back</Button>}
27
+ />
28
+ )
29
+ expect(screen.getByRole('button', { name: /go back/i })).toBeInTheDocument()
30
+ })
31
+
32
+ it('renders all content together', () => {
33
+ render(
34
+ <EmptyState
35
+ title="No alerts"
36
+ description="Your alert queue is clear"
37
+ action={<Button variant="secondary">Refresh</Button>}
38
+ />
39
+ )
40
+ expect(screen.getByText('No alerts')).toBeInTheDocument()
41
+ expect(screen.getByText('Your alert queue is clear')).toBeInTheDocument()
42
+ expect(screen.getByRole('button')).toBeInTheDocument()
43
+ })
44
+
45
+ it('renders without optional props', () => {
46
+ render(<EmptyState title="Minimal" />)
47
+ expect(screen.getByText('Minimal')).toBeInTheDocument()
48
+ })
49
+ })
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ import { InboxIcon } from 'lucide-react';
3
+
4
+ export interface EmptyStateProps {
5
+ title: string;
6
+ description?: string;
7
+ icon?: React.ReactNode;
8
+ action?: React.ReactNode;
9
+ }
10
+
11
+ export function EmptyState({ title, description, icon, action }: EmptyStateProps) {
12
+ return (
13
+ <div className="flex flex-col items-center justify-center py-xxl px-lg text-center">
14
+ <div className="mb-lg text-apple-label-tertiary">
15
+ {icon || <InboxIcon className="w-12 h-12 mx-auto" />}
16
+ </div>
17
+ <h3 className="sf-headline mb-sm">{title}</h3>
18
+ {description && <p className="sf-caption mb-lg max-w-xs">{description}</p>}
19
+ {action}
20
+ </div>
21
+ );
22
+ }
@@ -0,0 +1,44 @@
1
+ import React, { Component, ErrorInfo, ReactNode } from 'react';
2
+ import { Button } from './Button';
3
+
4
+ interface Props {
5
+ children: ReactNode;
6
+ fallback?: ReactNode;
7
+ }
8
+
9
+ interface State {
10
+ hasError: boolean;
11
+ error: Error | null;
12
+ }
13
+
14
+ export class ErrorBoundary extends Component<Props, State> {
15
+ state: State = { hasError: false, error: null };
16
+
17
+ static getDerivedStateFromError(error: Error): State {
18
+ return { hasError: true, error };
19
+ }
20
+
21
+ componentDidCatch(error: Error, info: ErrorInfo) {
22
+ console.error('ErrorBoundary caught:', error, info);
23
+ }
24
+
25
+ handleReset = () => {
26
+ this.setState({ hasError: false, error: null });
27
+ };
28
+
29
+ render() {
30
+ if (this.state.hasError) {
31
+ if (this.props.fallback) return this.props.fallback;
32
+ return (
33
+ <div className="flex flex-col items-center justify-center h-64 text-center">
34
+ <h2 className="sf-title mb-md">Something went wrong</h2>
35
+ <p className="sf-caption mb-lg">{this.state.error?.message}</p>
36
+ <Button variant="primary" onClick={this.handleReset}>
37
+ Try Again
38
+ </Button>
39
+ </div>
40
+ );
41
+ }
42
+ return this.props.children;
43
+ }
44
+ }
@@ -0,0 +1,71 @@
1
+ import { useState, useRef, useEffect } from 'react'
2
+ import { Download, FileSpreadsheet, FileText, FileJson } from 'lucide-react'
3
+ import clsx from 'clsx'
4
+
5
+ interface ExportOption {
6
+ label: string
7
+ format: string
8
+ icon: typeof FileText
9
+ gradient: string
10
+ }
11
+
12
+ const options: ExportOption[] = [
13
+ { label: 'Export CSV', format: 'csv', icon: FileSpreadsheet, gradient: 'from-green-500 to-emerald-600' },
14
+ { label: 'Export PDF', format: 'pdf', icon: FileText, gradient: 'from-red-500 to-rose-600' },
15
+ { label: 'Export JSON', format: 'json', icon: FileJson, gradient: 'from-blue-500 to-indigo-600' },
16
+ ]
17
+
18
+ interface ExportMenuProps {
19
+ onExport: (format: string) => void
20
+ className?: string
21
+ }
22
+
23
+ export function ExportMenu({ onExport, className }: ExportMenuProps) {
24
+ const [open, setOpen] = useState(false)
25
+ const ref = useRef<HTMLDivElement>(null)
26
+
27
+ useEffect(() => {
28
+ const handler = (e: MouseEvent) => {
29
+ if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
30
+ }
31
+ document.addEventListener('mousedown', handler)
32
+ return () => document.removeEventListener('mousedown', handler)
33
+ }, [])
34
+
35
+ return (
36
+ <div ref={ref} className={clsx('relative', className)}>
37
+ <button type="button" onClick={() => setOpen(!open)}
38
+ className="flex items-center gap-2 px-3 py-2 rounded-xl text-sm font-medium
39
+ text-apple-label-secondary border border-white/[0.08]
40
+ hover:bg-white/[0.06] transition-all cursor-pointer">
41
+ <Download className="w-4 h-4" />
42
+ Export
43
+ </button>
44
+ {open && (
45
+ <div className="absolute right-0 top-full mt-2 w-48 rounded-xl overflow-hidden z-50
46
+ bg-[rgba(18,18,26,0.95)] backdrop-blur-xl border border-white/[0.08]
47
+ shadow-[0_12px_40px_rgba(0,0,0,0.4)]
48
+ animate-in fade-in slide-in-from-top-2 duration-200">
49
+ {options.map(opt => {
50
+ const Icon = opt.icon
51
+ return (
52
+ <button key={opt.format} type="button"
53
+ onClick={() => { onExport(opt.format); setOpen(false) }}
54
+ className="w-full flex items-center gap-3 px-4 py-3
55
+ text-sm text-white/80 hover:bg-white/[0.06] hover:text-white
56
+ transition-all cursor-pointer hover:scale-[1.01]">
57
+ <div className={clsx(
58
+ 'w-7 h-7 rounded-lg flex items-center justify-center bg-gradient-to-br',
59
+ opt.gradient,
60
+ )}>
61
+ <Icon className="w-3.5 h-3.5 text-white" />
62
+ </div>
63
+ {opt.label}
64
+ </button>
65
+ )
66
+ })}
67
+ </div>
68
+ )}
69
+ </div>
70
+ )
71
+ }
@@ -0,0 +1,37 @@
1
+ import React from 'react'
2
+ import { useTranslation } from 'react-i18next'
3
+ import { Globe } from 'lucide-react'
4
+
5
+ const LANGUAGES = [
6
+ { code: 'en', label: 'English', flag: '🇺🇸' },
7
+ { code: 'he', label: 'עברית', flag: '🇮🇱' },
8
+ { code: 'ar', label: 'العربية', flag: '🇸🇦' },
9
+ ] as const
10
+
11
+ export function LanguageSwitcher() {
12
+ const { i18n } = useTranslation()
13
+
14
+ const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
15
+ i18n.changeLanguage(e.target.value)
16
+ }
17
+
18
+ return (
19
+ <div className="flex items-center gap-xs">
20
+ <Globe className="w-4 h-4 text-apple-label-secondary" />
21
+ <select
22
+ value={i18n.language}
23
+ onChange={handleChange}
24
+ className="bg-transparent text-sm text-apple-label-secondary
25
+ border-none cursor-pointer appearance-none pr-4
26
+ focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-apple-blue"
27
+ aria-label="Select language"
28
+ >
29
+ {LANGUAGES.map(({ code, label, flag }) => (
30
+ <option key={code} value={code}>
31
+ {flag} {label}
32
+ </option>
33
+ ))}
34
+ </select>
35
+ </div>
36
+ )
37
+ }
@@ -0,0 +1,41 @@
1
+ import { render } from '@testing-library/react'
2
+ import { describe, it, expect } from 'vitest'
3
+ import { LoadingSpinner } from './LoadingSpinner'
4
+
5
+ describe('LoadingSpinner', () => {
6
+ it('renders spinner element', () => {
7
+ const { container } = render(<LoadingSpinner />)
8
+ const spinner = container.querySelector('div[class*="spinner"]')
9
+ expect(spinner).toBeInTheDocument()
10
+ })
11
+
12
+ it('renders small size', () => {
13
+ const { container } = render(<LoadingSpinner size="sm" />)
14
+ const spinner = container.querySelector('div[class*="spinner"]')
15
+ expect(spinner).toHaveClass('w-4', 'h-4')
16
+ })
17
+
18
+ it('renders medium size by default', () => {
19
+ const { container } = render(<LoadingSpinner />)
20
+ const spinner = container.querySelector('div[class*="spinner"]')
21
+ expect(spinner).toHaveClass('w-8', 'h-8')
22
+ })
23
+
24
+ it('renders large size', () => {
25
+ const { container } = render(<LoadingSpinner size="lg" />)
26
+ const spinner = container.querySelector('div[class*="spinner"]')
27
+ expect(spinner).toHaveClass('w-12', 'h-12')
28
+ })
29
+
30
+ it('has circular styling', () => {
31
+ const { container } = render(<LoadingSpinner />)
32
+ const spinner = container.querySelector('div[class*="spinner"]')
33
+ expect(spinner).toHaveClass('rounded-full', 'border-2')
34
+ })
35
+
36
+ it('uses apple blue color', () => {
37
+ const { container } = render(<LoadingSpinner />)
38
+ const spinner = container.querySelector('div[class*="spinner"]')
39
+ expect(spinner).toHaveClass('border-apple-blue', 'border-t-transparent')
40
+ })
41
+ })
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+
3
+ interface LoadingSpinnerProps {
4
+ size?: 'sm' | 'md' | 'lg';
5
+ }
6
+
7
+ export function LoadingSpinner({ size = 'md' }: LoadingSpinnerProps) {
8
+ const sizes = {
9
+ sm: 'w-4 h-4',
10
+ md: 'w-8 h-8',
11
+ lg: 'w-12 h-12',
12
+ };
13
+
14
+ return (
15
+ <div
16
+ role="status"
17
+ aria-label="Loading"
18
+ className={`${sizes[size]} rounded-full border-2 border-apple-blue border-t-transparent spinner`}
19
+ />
20
+ );
21
+ }
@@ -0,0 +1,63 @@
1
+ import React from 'react'
2
+ import clsx from 'clsx'
3
+ import type { LucideIcon } from 'lucide-react'
4
+
5
+ interface MetricCardProps {
6
+ title: string
7
+ value: string | number
8
+ icon: LucideIcon
9
+ color?: 'blue' | 'green' | 'red' | 'orange' | 'teal'
10
+ subtitle?: string
11
+ className?: string
12
+ }
13
+
14
+ const gradients: Record<string, string> = {
15
+ blue: 'from-[#0A84FF] to-[#6366F1]',
16
+ green: 'from-[#30D158] to-[#00B894]',
17
+ red: 'from-[#FF453A] to-[#EC4899]',
18
+ orange: 'from-[#FF9F0A] to-[#F59E0B]',
19
+ teal: 'from-[#00B894] to-[#059669]',
20
+ }
21
+
22
+ const glows: Record<string, string> = {
23
+ blue: 'group-hover:shadow-[0_0_40px_rgba(10,132,255,0.15)]',
24
+ green: 'group-hover:shadow-[0_0_40px_rgba(48,209,88,0.15)]',
25
+ red: 'group-hover:shadow-[0_0_40px_rgba(255,69,58,0.15)]',
26
+ orange: 'group-hover:shadow-[0_0_40px_rgba(255,159,10,0.15)]',
27
+ teal: 'group-hover:shadow-[0_0_40px_rgba(0,184,148,0.15)]',
28
+ }
29
+
30
+ export function MetricCard({
31
+ title, value, icon: Icon, color = 'blue', subtitle, className,
32
+ }: MetricCardProps) {
33
+ return (
34
+ <div className={clsx(
35
+ 'group card-vibrancy p-xl rounded-2xl transition-all duration-300',
36
+ 'hover:scale-[1.02] hover:border-white/[0.12]',
37
+ 'relative overflow-hidden',
38
+ glows[color], className,
39
+ )}>
40
+ {/* shimmer hover effect */}
41
+ <div className="absolute inset-0 -translate-x-full group-hover:translate-x-full
42
+ transition-transform duration-700 bg-gradient-to-r from-transparent via-white/[0.04] to-transparent" />
43
+
44
+ <div className="flex items-center justify-between mb-md relative">
45
+ <p className="text-[11px] tracking-wider uppercase text-apple-label-secondary font-medium">
46
+ {title}
47
+ </p>
48
+ <div className={clsx(
49
+ 'w-9 h-9 rounded-xl flex items-center justify-center bg-gradient-to-br',
50
+ gradients[color],
51
+ )}>
52
+ <Icon className="w-4 h-4 text-white" />
53
+ </div>
54
+ </div>
55
+ <h3 className="text-[36px] font-extrabold tracking-tight text-white leading-none mb-sm">
56
+ {value}
57
+ </h3>
58
+ {subtitle && (
59
+ <p className="text-[12px] text-apple-label-tertiary">{subtitle}</p>
60
+ )}
61
+ </div>
62
+ )
63
+ }
@@ -0,0 +1,51 @@
1
+ import React from 'react'
2
+
3
+ interface ScoreRingProps {
4
+ score: number
5
+ size?: number
6
+ strokeWidth?: number
7
+ }
8
+
9
+ export function ScoreRing({ score, size = 64, strokeWidth = 4 }: ScoreRingProps) {
10
+ const radius = (size - strokeWidth * 2) / 2
11
+ const circumference = 2 * Math.PI * radius
12
+ const offset = circumference - (score / 100) * circumference
13
+ const center = size / 2
14
+
15
+ const color = score >= 80 ? '#FF453A' : score >= 60 ? '#FF9F0A' : '#30D158'
16
+ const colorEnd = score >= 80 ? '#EC4899' : score >= 60 ? '#F59E0B' : '#00B894'
17
+ const glowId = `glow-${score}`
18
+ const gradId = `grad-${score}`
19
+
20
+ return (
21
+ <div className="relative flex-shrink-0" style={{ width: size, height: size }}>
22
+ <svg width={size} height={size} className="-rotate-90">
23
+ <defs>
24
+ <linearGradient id={gradId} x1="0%" y1="0%" x2="100%" y2="0%">
25
+ <stop offset="0%" stopColor={color} />
26
+ <stop offset="100%" stopColor={colorEnd} />
27
+ </linearGradient>
28
+ <filter id={glowId}>
29
+ <feGaussianBlur stdDeviation="3" result="blur" />
30
+ <feMerge>
31
+ <feMergeNode in="blur" />
32
+ <feMergeNode in="SourceGraphic" />
33
+ </feMerge>
34
+ </filter>
35
+ </defs>
36
+ <circle cx={center} cy={center} r={radius}
37
+ fill="none" stroke="rgba(255,255,255,0.06)" strokeWidth={strokeWidth} />
38
+ <circle cx={center} cy={center} r={radius}
39
+ fill="none" stroke={`url(#${gradId})`} strokeWidth={strokeWidth}
40
+ strokeLinecap="round"
41
+ strokeDasharray={circumference}
42
+ strokeDashoffset={offset}
43
+ filter={`url(#${glowId})`}
44
+ className="transition-all duration-1000 ease-out" />
45
+ </svg>
46
+ <span className="absolute inset-0 flex items-center justify-center text-[13px] font-bold text-white">
47
+ {score}%
48
+ </span>
49
+ </div>
50
+ )
51
+ }
@@ -0,0 +1,55 @@
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 { SearchField } from './SearchField'
5
+
6
+ describe('SearchField', () => {
7
+ it('renders search input with placeholder', () => {
8
+ render(<SearchField value="" onChange={vi.fn()} />)
9
+ const input = screen.getByPlaceholderText('Search...')
10
+ expect(input).toBeInTheDocument()
11
+ expect(input).toHaveAttribute('type', 'search')
12
+ })
13
+
14
+ it('renders custom placeholder', () => {
15
+ render(<SearchField placeholder="Find entities..." value="" onChange={vi.fn()} />)
16
+ expect(screen.getByPlaceholderText('Find entities...')).toBeInTheDocument()
17
+ })
18
+
19
+ it('updates input value when user types', async () => {
20
+ const handler = vi.fn()
21
+ render(<SearchField value="" onChange={handler} />)
22
+ const input = screen.getByPlaceholderText('Search...')
23
+ await userEvent.type(input, 'test')
24
+ expect(handler).toHaveBeenCalled()
25
+ })
26
+
27
+ it('displays provided value', () => {
28
+ render(<SearchField value="test value" onChange={vi.fn()} />)
29
+ const input = screen.getByDisplayValue('test value')
30
+ expect(input).toBeInTheDocument()
31
+ })
32
+
33
+ it('calls onSubmit when Enter is pressed', async () => {
34
+ const handler = vi.fn()
35
+ render(<SearchField value="test" onChange={vi.fn()} onSubmit={handler} />)
36
+ const input = screen.getByPlaceholderText('Search...')
37
+ await userEvent.click(input)
38
+ await userEvent.keyboard('{Enter}')
39
+ expect(handler).toHaveBeenCalledOnce()
40
+ })
41
+
42
+ it('does not call onSubmit for other keys', async () => {
43
+ const handler = vi.fn()
44
+ render(<SearchField value="" onChange={vi.fn()} onSubmit={handler} />)
45
+ const input = screen.getByPlaceholderText('Search...')
46
+ await userEvent.type(input, 'a')
47
+ expect(handler).not.toHaveBeenCalled()
48
+ })
49
+
50
+ it('renders search icon', () => {
51
+ const { container } = render(<SearchField value="" onChange={vi.fn()} />)
52
+ const svg = container.querySelector('svg')
53
+ expect(svg).toBeInTheDocument()
54
+ })
55
+ })
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+ import { Search } from 'lucide-react';
3
+
4
+ interface SearchFieldProps {
5
+ placeholder?: string;
6
+ value: string;
7
+ onChange: (value: string) => void;
8
+ onSubmit?: () => void;
9
+ }
10
+
11
+ export function SearchField({
12
+ placeholder = 'Search...',
13
+ value,
14
+ onChange,
15
+ onSubmit,
16
+ }: SearchFieldProps) {
17
+ return (
18
+ <div className="relative">
19
+ <Search className="absolute left-md top-1/2 transform -translate-y-1/2 w-4 h-4 text-apple-label-tertiary" aria-hidden="true" />
20
+ <input
21
+ type="search"
22
+ aria-label={placeholder}
23
+ placeholder={placeholder}
24
+ value={value}
25
+ onChange={(e) => onChange(e.target.value)}
26
+ onKeyDown={(e) => e.key === 'Enter' && onSubmit?.()}
27
+ className="input-field pl-10 w-full"
28
+ />
29
+ </div>
30
+ );
31
+ }
@@ -0,0 +1,57 @@
1
+ import clsx from 'clsx'
2
+
3
+ type Severity = 'critical' | 'high' | 'medium' | 'low' | 'info'
4
+ type Size = 'sm' | 'md' | 'lg'
5
+
6
+ interface SeverityBadgeProps {
7
+ severity: Severity
8
+ size?: Size
9
+ className?: string
10
+ }
11
+
12
+ const config: Record<Severity, { gradient: string; dot: string; border: string; text: string }> = {
13
+ critical: {
14
+ gradient: 'bg-gradient-to-r from-red-500/20 to-rose-500/10',
15
+ dot: 'bg-red-500 shadow-[0_0_8px_rgba(239,68,68,0.6)]',
16
+ border: 'border-red-500/30', text: 'text-red-400',
17
+ },
18
+ high: {
19
+ gradient: 'bg-gradient-to-r from-orange-500/20 to-amber-500/10',
20
+ dot: 'bg-orange-500 shadow-[0_0_8px_rgba(249,115,22,0.6)]',
21
+ border: 'border-orange-500/30', text: 'text-orange-400',
22
+ },
23
+ medium: {
24
+ gradient: 'bg-gradient-to-r from-yellow-500/20 to-amber-400/10',
25
+ dot: 'bg-yellow-500 shadow-[0_0_8px_rgba(234,179,8,0.6)]',
26
+ border: 'border-yellow-500/30', text: 'text-yellow-400',
27
+ },
28
+ low: {
29
+ gradient: 'bg-gradient-to-r from-green-500/20 to-emerald-400/10',
30
+ dot: 'bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.6)]',
31
+ border: 'border-green-500/30', text: 'text-green-400',
32
+ },
33
+ info: {
34
+ gradient: 'bg-gradient-to-r from-blue-500/20 to-indigo-400/10',
35
+ dot: 'bg-blue-500 shadow-[0_0_8px_rgba(59,130,246,0.6)]',
36
+ border: 'border-blue-500/30', text: 'text-blue-400',
37
+ },
38
+ }
39
+
40
+ const sizeClasses: Record<Size, string> = {
41
+ sm: 'px-2 py-0.5 text-[10px]',
42
+ md: 'px-2.5 py-1 text-[11px]',
43
+ lg: 'px-3 py-1.5 text-xs',
44
+ }
45
+
46
+ export function SeverityBadge({ severity, size = 'md', className }: SeverityBadgeProps) {
47
+ const c = config[severity]
48
+ return (
49
+ <span className={clsx(
50
+ 'inline-flex items-center gap-1.5 rounded-full font-semibold uppercase tracking-wider border',
51
+ c.gradient, c.border, c.text, sizeClasses[size], className,
52
+ )}>
53
+ <span className={clsx('w-1.5 h-1.5 rounded-full animate-pulse', c.dot)} />
54
+ {severity}
55
+ </span>
56
+ )
57
+ }
@@ -0,0 +1,37 @@
1
+ import { Sun, Moon } from 'lucide-react'
2
+ import { useEffect, useState } from 'react'
3
+
4
+ export function ThemeToggle() {
5
+ const [dark, setDark] = useState(() => {
6
+ if (typeof window === 'undefined') return true
7
+ return localStorage.getItem('theme') !== 'light'
8
+ })
9
+
10
+ useEffect(() => {
11
+ const root = document.documentElement
12
+ if (dark) {
13
+ root.classList.add('dark')
14
+ localStorage.setItem('theme', 'dark')
15
+ } else {
16
+ root.classList.remove('dark')
17
+ localStorage.setItem('theme', 'light')
18
+ }
19
+ }, [dark])
20
+
21
+ return (
22
+ <button
23
+ type="button"
24
+ onClick={() => setDark(d => !d)}
25
+ className="w-9 h-9 flex items-center justify-center rounded-xl
26
+ bg-white/[0.06] hover:bg-white/[0.1] border border-white/[0.08]
27
+ transition-all duration-200 cursor-pointer
28
+ active:scale-90"
29
+ aria-label={dark ? 'Switch to light mode' : 'Switch to dark mode'}
30
+ >
31
+ {dark
32
+ ? <Sun className="w-4 h-4 text-amber-400" />
33
+ : <Moon className="w-4 h-4 text-indigo-400" />
34
+ }
35
+ </button>
36
+ )
37
+ }
@@ -0,0 +1,79 @@
1
+ import { render, screen, act } from '@testing-library/react'
2
+ import { describe, it, expect, vi } from 'vitest'
3
+ import { ToastProvider, useToast } from './Toast'
4
+
5
+ function TestConsumer() {
6
+ const { toast } = useToast()
7
+ return (
8
+ <div>
9
+ <button onClick={() => toast('Info message')}>Show Info</button>
10
+ <button onClick={() => toast('Success!', 'success')}>Show Success</button>
11
+ <button onClick={() => toast('Error!', 'error')}>Show Error</button>
12
+ </div>
13
+ )
14
+ }
15
+
16
+ const renderToast = () =>
17
+ render(
18
+ <ToastProvider>
19
+ <TestConsumer />
20
+ </ToastProvider>
21
+ )
22
+
23
+ describe('ToastProvider', () => {
24
+ it('renders children', () => {
25
+ renderToast()
26
+ expect(screen.getByText('Show Info')).toBeInTheDocument()
27
+ })
28
+
29
+ it('shows toast when triggered', async () => {
30
+ renderToast()
31
+ await act(async () => {
32
+ screen.getByText('Show Info').click()
33
+ })
34
+ expect(screen.getByRole('alert')).toHaveTextContent('Info message')
35
+ })
36
+
37
+ it('renders success toast with green background', async () => {
38
+ renderToast()
39
+ await act(async () => {
40
+ screen.getByText('Show Success').click()
41
+ })
42
+ expect(screen.getByRole('alert')).toHaveClass('bg-green-500')
43
+ })
44
+
45
+ it('renders error toast with red background', async () => {
46
+ renderToast()
47
+ await act(async () => {
48
+ screen.getByText('Show Error').click()
49
+ })
50
+ expect(screen.getByRole('alert')).toHaveClass('bg-red-500')
51
+ })
52
+
53
+ it('renders info toast with blue background', async () => {
54
+ renderToast()
55
+ await act(async () => {
56
+ screen.getByText('Show Info').click()
57
+ })
58
+ expect(screen.getByRole('alert')).toHaveClass('bg-blue-500')
59
+ })
60
+
61
+ it('has aria-live polite region', () => {
62
+ const { container } = renderToast()
63
+ expect(container.querySelector('[aria-live="polite"]')).toBeInTheDocument()
64
+ })
65
+
66
+ it('auto-removes toast after timeout', async () => {
67
+ vi.useFakeTimers()
68
+ renderToast()
69
+ await act(async () => {
70
+ screen.getByText('Show Info').click()
71
+ })
72
+ expect(screen.getByRole('alert')).toBeInTheDocument()
73
+ await act(async () => {
74
+ vi.advanceTimersByTime(4100)
75
+ })
76
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument()
77
+ vi.useRealTimers()
78
+ })
79
+ })