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,30 @@
1
+ import { Menu } from 'lucide-react'
2
+ import { NotificationBell } from './NotificationBell'
3
+
4
+ interface MobileHeaderProps {
5
+ onMenuToggle: () => void
6
+ }
7
+
8
+ export function MobileHeader({ onMenuToggle }: MobileHeaderProps) {
9
+ return (
10
+ <header className="md:hidden sticky top-0 z-40 h-14 flex items-center justify-between px-4"
11
+ style={{
12
+ background: 'rgba(10,10,15,0.85)',
13
+ backdropFilter: 'blur(20px) saturate(180%)',
14
+ borderBottom: '1px solid rgba(255,255,255,0.06)',
15
+ }}>
16
+ <button type="button" onClick={onMenuToggle}
17
+ className="w-9 h-9 flex items-center justify-center rounded-xl
18
+ hover:bg-white/[0.08] transition-all cursor-pointer active:scale-90">
19
+ <Menu className="w-5 h-5 text-apple-label-secondary" />
20
+ </button>
21
+
22
+ <div className="flex items-center gap-1">
23
+ <span className="text-base font-extrabold text-white tracking-tight">AQ</span>
24
+ <span className="w-1.5 h-1.5 rounded-full bg-gradient-to-br from-[#0A84FF] to-[#6366F1] -mt-2" />
25
+ </div>
26
+
27
+ <NotificationBell />
28
+ </header>
29
+ )
30
+ }
@@ -0,0 +1,63 @@
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 { MemoryRouter } from 'react-router-dom'
5
+ import { NavGroup } from './NavGroup'
6
+ import { Home, Bell } from 'lucide-react'
7
+
8
+ const mockSection = {
9
+ title: 'Main',
10
+ items: [
11
+ { icon: Home, label: 'Dashboard', path: '/dashboard' },
12
+ { icon: Bell, label: 'Alerts', path: '/alerts' },
13
+ ],
14
+ }
15
+
16
+ const renderNavGroup = (path = '/') =>
17
+ render(
18
+ <MemoryRouter initialEntries={[path]}>
19
+ <NavGroup section={mockSection} userRole="admin" onNavigate={vi.fn()} />
20
+ </MemoryRouter>
21
+ )
22
+
23
+ describe('NavGroup', () => {
24
+ it('renders section title', () => {
25
+ renderNavGroup()
26
+ expect(screen.getByText(/main/i)).toBeInTheDocument()
27
+ })
28
+
29
+ it('renders all nav items as links', () => {
30
+ renderNavGroup()
31
+ const links = screen.getAllByRole('link')
32
+ expect(links).toHaveLength(2)
33
+ })
34
+
35
+ it('renders correct hrefs', () => {
36
+ renderNavGroup()
37
+ expect(screen.getAllByRole('link')[0]).toHaveAttribute('href', '/dashboard')
38
+ expect(screen.getAllByRole('link')[1]).toHaveAttribute('href', '/alerts')
39
+ })
40
+
41
+ it('highlights active link', () => {
42
+ renderNavGroup('/dashboard')
43
+ const link = screen.getAllByRole('link')[0]
44
+ expect(link).toHaveClass('bg-white/5', 'text-white')
45
+ })
46
+
47
+ it('does not highlight inactive links', () => {
48
+ renderNavGroup('/dashboard')
49
+ const link = screen.getAllByRole('link')[1]
50
+ expect(link).toHaveClass('text-apple-label-secondary')
51
+ })
52
+
53
+ it('calls onNavigate when link clicked', async () => {
54
+ const handler = vi.fn()
55
+ render(
56
+ <MemoryRouter>
57
+ <NavGroup section={mockSection} userRole="admin" onNavigate={handler} />
58
+ </MemoryRouter>
59
+ )
60
+ await userEvent.click(screen.getAllByRole('link')[0])
61
+ expect(handler).toHaveBeenCalledOnce()
62
+ })
63
+ })
@@ -0,0 +1,64 @@
1
+ import React from 'react';
2
+ import { Link, useLocation } from 'react-router-dom';
3
+ import { useTranslation } from 'react-i18next';
4
+ import clsx from 'clsx';
5
+ import type { NavSection } from './navItems';
6
+ import { canAccess } from './navItems';
7
+
8
+ interface NavGroupProps {
9
+ section: NavSection;
10
+ userRole: string;
11
+ collapsed?: boolean;
12
+ onNavigate: () => void;
13
+ }
14
+
15
+ export function NavGroup({ section, userRole, collapsed, onNavigate }: NavGroupProps) {
16
+ const { t } = useTranslation('nav');
17
+ const location = useLocation();
18
+
19
+ // Hide entire section if user lacks access
20
+ if (!canAccess(userRole, section.minRole)) return null;
21
+
22
+ const visibleItems = section.items.filter(
23
+ (item) => canAccess(userRole, item.minRole),
24
+ );
25
+ if (visibleItems.length === 0) return null;
26
+
27
+ const isActive = (path: string) =>
28
+ location.pathname === path || (path !== '/dashboard' && location.pathname.startsWith(path));
29
+
30
+ return (
31
+ <div className="mb-lg">
32
+ {!collapsed && (
33
+ <p className="text-[10px] uppercase tracking-[0.12em] text-apple-label-tertiary px-lg mb-sm font-medium">
34
+ {t(section.title.toLowerCase())}
35
+ </p>
36
+ )}
37
+ <div className="space-y-px">
38
+ {visibleItems.map(({ icon: Icon, label, path }) => (
39
+ <Link
40
+ key={path}
41
+ to={path}
42
+ onClick={onNavigate}
43
+ title={collapsed ? label : undefined}
44
+ className={clsx(
45
+ 'flex items-center gap-md rounded-apple-md transition-all cursor-pointer relative',
46
+ collapsed ? 'justify-center px-sm py-md' : 'px-lg py-sm',
47
+ isActive(path)
48
+ ? 'bg-white/5 text-white'
49
+ : 'text-apple-label-secondary hover:bg-white/5 hover:text-white'
50
+ )}
51
+ >
52
+ {isActive(path) && (
53
+ <span className="absolute left-0 top-1/2 -translate-y-1/2 w-[3px] h-4 bg-apple-blue rounded-r-full" />
54
+ )}
55
+ <Icon className="w-4 h-4 flex-shrink-0" />
56
+ {!collapsed && (
57
+ <span className="text-[14px]">{t(label.toLowerCase().replace(/ /g, '_'))}</span>
58
+ )}
59
+ </Link>
60
+ ))}
61
+ </div>
62
+ </div>
63
+ );
64
+ }
@@ -0,0 +1,89 @@
1
+ import React, { useState, useEffect, useRef } from 'react'
2
+ import { Bell } from 'lucide-react'
3
+ import { api } from '../../api/client'
4
+
5
+ interface Notification {
6
+ id: string
7
+ action: string
8
+ details: string
9
+ created_at: string
10
+ }
11
+
12
+ export function NotificationBell() {
13
+ const [open, setOpen] = useState(false)
14
+ const [items, setItems] = useState<Notification[]>([])
15
+ const [unread, setUnread] = useState(0)
16
+ const ref = useRef<HTMLDivElement>(null!)
17
+
18
+ useEffect(() => {
19
+ fetchNotifications()
20
+ const interval = setInterval(fetchNotifications, 30000)
21
+ return () => clearInterval(interval)
22
+ }, [])
23
+
24
+ useEffect(() => {
25
+ const handler = (e: MouseEvent) => {
26
+ if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
27
+ }
28
+ document.addEventListener('mousedown', handler)
29
+ return () => document.removeEventListener('mousedown', handler)
30
+ }, [])
31
+
32
+ async function fetchNotifications() {
33
+ try {
34
+ const data = await api.get<{ entries: Notification[]; total: number }>('/audit?limit=10')
35
+ setItems(data.entries ?? [])
36
+ setUnread(data.total > 0 ? Math.min(data.total, 9) : 0)
37
+ } catch { /* silent */ }
38
+ }
39
+
40
+ return (
41
+ <div ref={ref} className="relative">
42
+ <button onClick={() => { setOpen(!open); setUnread(0) }}
43
+ className="relative p-sm hover:bg-white/[0.06] rounded-apple-md transition-colors cursor-pointer">
44
+ <Bell className="w-5 h-5 text-apple-label-secondary" />
45
+ {unread > 0 && (
46
+ <span className="absolute -top-0.5 -right-0.5 w-4 h-4 bg-apple-red rounded-full text-[10px] text-white flex items-center justify-center font-bold">
47
+ {unread}
48
+ </span>
49
+ )}
50
+ </button>
51
+
52
+ {open && (
53
+ <div className="absolute right-0 top-10 w-80 bg-[#1a1a24] border border-white/10 rounded-apple-lg shadow-2xl overflow-hidden z-50">
54
+ <div className="flex border-b border-white/[0.06]">
55
+ <Tab label="Notifications" active />
56
+ </div>
57
+ <div className="max-h-72 overflow-y-auto">
58
+ {items.length === 0 && (
59
+ <p className="p-lg text-sm text-apple-label-tertiary text-center">No notifications</p>
60
+ )}
61
+ {items.map((n) => (
62
+ <NotificationRow key={n.id} item={n} />
63
+ ))}
64
+ </div>
65
+ </div>
66
+ )}
67
+ </div>
68
+ )
69
+ }
70
+
71
+ function Tab({ label, active }: { label: string; active: boolean }) {
72
+ return (
73
+ <button className={`flex-1 py-md text-sm font-medium ${active ? 'text-white border-b-2 border-apple-blue' : 'text-apple-label-tertiary'}`}>
74
+ {label}
75
+ </button>
76
+ )
77
+ }
78
+
79
+ function NotificationRow({ item }: { item: Notification }) {
80
+ const time = new Date(item.created_at).toLocaleString(undefined, {
81
+ month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit',
82
+ })
83
+ return (
84
+ <div className="px-lg py-md border-b border-white/[0.04] hover:bg-white/[0.03]">
85
+ <p className="text-sm text-white">{item.action}</p>
86
+ <p className="text-[11px] text-apple-label-tertiary mt-xs">{time}</p>
87
+ </div>
88
+ )
89
+ }
@@ -0,0 +1,61 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import { describe, it, expect } from 'vitest'
3
+ import { PageHeader } from './PageHeader'
4
+ import { Button } from '../ui/Button'
5
+
6
+ describe('PageHeader', () => {
7
+ it('renders page title', () => {
8
+ render(<PageHeader title="Dashboard" />)
9
+ expect(screen.getByRole('heading', { name: /dashboard/i })).toBeInTheDocument()
10
+ })
11
+
12
+ it('renders description when provided', () => {
13
+ render(
14
+ <PageHeader
15
+ title="Alerts"
16
+ description="Review pending alerts"
17
+ />
18
+ )
19
+ expect(screen.getByText('Review pending alerts')).toBeInTheDocument()
20
+ })
21
+
22
+ it('does not render description when not provided', () => {
23
+ const { container } = render(<PageHeader title="Test" />)
24
+ const captions = container.querySelectorAll('.sf-caption')
25
+ expect(captions.length).toBe(0)
26
+ })
27
+
28
+ it('renders action element when provided', () => {
29
+ render(
30
+ <PageHeader
31
+ title="Test"
32
+ action={<Button>Export</Button>}
33
+ />
34
+ )
35
+ expect(screen.getByRole('button', { name: /export/i })).toBeInTheDocument()
36
+ })
37
+
38
+ it('renders title and description together', () => {
39
+ render(
40
+ <PageHeader
41
+ title="Configuration"
42
+ description="Manage system settings"
43
+ />
44
+ )
45
+ expect(screen.getByRole('heading', { name: /configuration/i })).toBeInTheDocument()
46
+ expect(screen.getByText('Manage system settings')).toBeInTheDocument()
47
+ })
48
+
49
+ it('renders title, description, and action together', () => {
50
+ render(
51
+ <PageHeader
52
+ title="Screening"
53
+ description="Screen new entities"
54
+ action={<Button variant="primary">New</Button>}
55
+ />
56
+ )
57
+ expect(screen.getByRole('heading', { name: /screening/i })).toBeInTheDocument()
58
+ expect(screen.getByText('Screen new entities')).toBeInTheDocument()
59
+ expect(screen.getByRole('button', { name: /new/i })).toBeInTheDocument()
60
+ })
61
+ })
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+
3
+ interface PageHeaderProps {
4
+ title: string;
5
+ description?: string;
6
+ action?: React.ReactNode;
7
+ }
8
+
9
+ export function PageHeader({ title, description, action }: PageHeaderProps) {
10
+ return (
11
+ <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-lg mb-xxl">
12
+ <div>
13
+ <h1 className="sf-title mb-sm">{title}</h1>
14
+ {description && <p className="sf-caption">{description}</p>}
15
+ </div>
16
+ {action && <div className="flex-shrink-0">{action}</div>}
17
+ </div>
18
+ );
19
+ }
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+ import { Navigate } from 'react-router-dom';
3
+ import { useAuth } from '../../context/AuthContext';
4
+ import { LoadingSpinner } from '../ui/LoadingSpinner';
5
+
6
+ interface Props {
7
+ children: React.ReactNode;
8
+ requiredRole?: string;
9
+ }
10
+
11
+ export function ProtectedRoute({ children, requiredRole }: Props) {
12
+ const { isAuthenticated, loading, user } = useAuth();
13
+
14
+ if (loading) {
15
+ return (
16
+ <div className="flex items-center justify-center h-screen">
17
+ <LoadingSpinner />
18
+ </div>
19
+ );
20
+ }
21
+
22
+ if (!isAuthenticated) {
23
+ return <Navigate to="/login" replace />;
24
+ }
25
+
26
+ if (requiredRole && user?.role !== requiredRole && user?.role !== 'admin') {
27
+ return <Navigate to="/dashboard" replace />;
28
+ }
29
+
30
+ return <>{children}</>;
31
+ }
@@ -0,0 +1,17 @@
1
+ import React from 'react'
2
+ import MarketingNav from '../../pages/marketing/MarketingNav'
3
+ import FooterSection from '../../pages/marketing/FooterSection'
4
+
5
+ interface PublicLayoutProps {
6
+ children: React.ReactNode
7
+ }
8
+
9
+ export default function PublicLayout({ children }: PublicLayoutProps) {
10
+ return (
11
+ <div className="min-h-screen bg-black flex flex-col">
12
+ <MarketingNav />
13
+ <main className="flex-1">{children}</main>
14
+ <FooterSection />
15
+ </div>
16
+ )
17
+ }
@@ -0,0 +1,67 @@
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 { BrowserRouter } from 'react-router-dom'
5
+ import { Sidebar } from './Sidebar'
6
+
7
+ vi.mock('../../context/AuthContext', () => ({
8
+ useAuth: () => ({
9
+ user: { id: '1', email: 'john@example.com', role: 'admin', tenant_id: 't1' },
10
+ loading: false,
11
+ isAuthenticated: true,
12
+ }),
13
+ }))
14
+
15
+ const renderWithRouter = (component: React.ReactElement) => {
16
+ return render(<BrowserRouter>{component}</BrowserRouter>)
17
+ }
18
+
19
+ describe('Sidebar', () => {
20
+ it('renders sidebar logo', () => {
21
+ renderWithRouter(<Sidebar isOpen={true} onClose={vi.fn()} />)
22
+ expect(screen.getByAltText('AMLIQ')).toBeInTheDocument()
23
+ })
24
+
25
+ it('renders main and compliance nav sections', () => {
26
+ renderWithRouter(<Sidebar isOpen={true} onClose={vi.fn()} />)
27
+ expect(screen.getByText('Dashboard')).toBeInTheDocument()
28
+ expect(screen.getByText('Cases')).toBeInTheDocument()
29
+ expect(screen.getByText('PEP Screening')).toBeInTheDocument()
30
+ expect(screen.getByText('Adverse Media')).toBeInTheDocument()
31
+ })
32
+
33
+ it('renders section titles', () => {
34
+ renderWithRouter(<Sidebar isOpen={true} onClose={vi.fn()} />)
35
+ expect(screen.getByText('Main')).toBeInTheDocument()
36
+ expect(screen.getByText('Compliance')).toBeInTheDocument()
37
+ expect(screen.getByText('System')).toBeInTheDocument()
38
+ })
39
+
40
+ it('renders user display name from email', () => {
41
+ renderWithRouter(<Sidebar isOpen={true} onClose={vi.fn()} />)
42
+ expect(screen.getByText('john')).toBeInTheDocument()
43
+ })
44
+
45
+ it('calls onClose on link click', async () => {
46
+ const handler = vi.fn()
47
+ renderWithRouter(<Sidebar isOpen={true} onClose={handler} />)
48
+ await userEvent.click(screen.getByText('Dashboard').closest('a')!)
49
+ expect(handler).toHaveBeenCalledOnce()
50
+ })
51
+
52
+ it('renders correct compliance hrefs', () => {
53
+ renderWithRouter(<Sidebar isOpen={true} onClose={vi.fn()} />)
54
+ expect(screen.getByText('Cases').closest('a'))
55
+ .toHaveAttribute('href', '/compliance/cases')
56
+ expect(screen.getByText('Risk Assessment').closest('a'))
57
+ .toHaveAttribute('href', '/compliance/risk')
58
+ })
59
+
60
+ it('hides sidebar when isOpen is false', () => {
61
+ const { container } = renderWithRouter(
62
+ <Sidebar isOpen={false} onClose={vi.fn()} />
63
+ )
64
+ const sidebar = container.querySelector('div[class*="-translate-x-full"]')
65
+ expect(sidebar).toBeInTheDocument()
66
+ })
67
+ })
@@ -0,0 +1,92 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { X, ChevronsLeft, ChevronsRight } from 'lucide-react';
3
+ import { Link } from 'react-router-dom';
4
+ import clsx from 'clsx';
5
+ import { Avatar } from '../ui/Avatar';
6
+ import { NavGroup } from './NavGroup';
7
+ import { navSections } from './navItems';
8
+ import { useAuth } from '../../context/AuthContext';
9
+
10
+ interface SidebarProps {
11
+ isOpen: boolean;
12
+ onClose: () => void;
13
+ }
14
+
15
+ export function Sidebar({ isOpen, onClose }: SidebarProps) {
16
+ const { user } = useAuth();
17
+ const [collapsed, setCollapsed] = useState(false);
18
+ const displayName = user?.email?.split('@')[0] ?? 'User';
19
+ const displayEmail = user?.email ?? 'Compliance Officer';
20
+ const userRole = user?.role ?? 'viewer';
21
+
22
+ useEffect(() => {
23
+ const handleEscape = (e: KeyboardEvent) => {
24
+ if (e.key === 'Escape' && isOpen) onClose();
25
+ };
26
+ document.addEventListener('keydown', handleEscape);
27
+ return () => document.removeEventListener('keydown', handleEscape);
28
+ }, [isOpen, onClose]);
29
+
30
+ const width = collapsed ? 'w-16' : 'w-64';
31
+
32
+ return (
33
+ <>
34
+ {isOpen && (
35
+ <div className="fixed inset-0 bg-black/60 md:hidden z-40" onClick={onClose} aria-hidden="true" />
36
+ )}
37
+ <div
38
+ className={clsx(
39
+ 'fixed md:relative h-screen flex flex-col transition-all duration-200 z-50',
40
+ width,
41
+ 'bg-gradient-to-b from-[#0E0E16] to-[#08080C]',
42
+ 'backdrop-blur-[24px] border-r border-white/[0.06]',
43
+ 'md:translate-x-0',
44
+ isOpen ? 'translate-x-0' : '-translate-x-full'
45
+ )}
46
+ >
47
+ <SidebarHeader collapsed={collapsed} onClose={onClose} onToggle={() => setCollapsed(!collapsed)} />
48
+ <div className="mx-3 mb-sm h-px bg-gradient-to-r from-transparent via-white/[0.06] to-transparent" />
49
+
50
+ <nav className="flex-1 px-sm py-sm overflow-y-auto" aria-label="Main navigation">
51
+ {navSections.map((section) => (
52
+ <NavGroup key={section.title} section={section} userRole={userRole}
53
+ collapsed={collapsed} onNavigate={onClose} />
54
+ ))}
55
+ </nav>
56
+
57
+ {!collapsed && (
58
+ <div className="p-lg border-t border-white/[0.04]">
59
+ <Link to="/config" className="flex items-center gap-md hover:bg-white/[0.04] rounded-apple-md p-sm transition-colors">
60
+ <Avatar name={displayName} size="md" />
61
+ <div className="flex-1 min-w-0">
62
+ <p className="text-[14px] font-medium text-white truncate">{displayName}</p>
63
+ <p className="text-[11px] text-apple-label-tertiary truncate">{displayEmail}</p>
64
+ </div>
65
+ </Link>
66
+ </div>
67
+ )}
68
+ </div>
69
+ </>
70
+ );
71
+ }
72
+
73
+ function SidebarHeader({ collapsed, onClose, onToggle }: {
74
+ collapsed: boolean; onClose: () => void; onToggle: () => void;
75
+ }) {
76
+ return (
77
+ <div className="relative p-md flex items-center justify-between">
78
+ <div className="flex items-center gap-2 relative">
79
+ <img src="/logo.svg" alt="AMLIQ" className="h-7 w-7" />
80
+ {!collapsed && <span className="text-white font-bold text-sm">AMLIQ</span>}
81
+ </div>
82
+ <button onClick={onClose} aria-label="Close menu"
83
+ className="md:hidden p-sm hover:bg-white/[0.08] rounded-apple-md cursor-pointer">
84
+ <X className="w-5 h-5" />
85
+ </button>
86
+ <button onClick={onToggle} aria-label={collapsed ? 'Expand' : 'Collapse'}
87
+ className="hidden md:block p-sm hover:bg-white/[0.08] rounded-apple-md cursor-pointer">
88
+ {collapsed ? <ChevronsRight className="w-4 h-4" /> : <ChevronsLeft className="w-4 h-4" />}
89
+ </button>
90
+ </div>
91
+ );
92
+ }
@@ -0,0 +1,47 @@
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 { Toolbar } from './Toolbar'
5
+
6
+ vi.mock('../ui/LanguageSwitcher', () => ({
7
+ LanguageSwitcher: () => <div data-testid="lang-switcher" />,
8
+ }))
9
+
10
+ describe('Toolbar', () => {
11
+ it('renders toggle menu button with aria-label', () => {
12
+ render(<Toolbar onMenuClick={vi.fn()} />)
13
+ expect(screen.getByLabelText('Toggle menu')).toBeInTheDocument()
14
+ })
15
+
16
+ it('calls onMenuClick when menu button clicked', async () => {
17
+ const handler = vi.fn()
18
+ render(<Toolbar onMenuClick={handler} />)
19
+ await userEvent.click(screen.getByLabelText('Toggle menu'))
20
+ expect(handler).toHaveBeenCalledOnce()
21
+ })
22
+
23
+ it('renders notifications button', () => {
24
+ render(<Toolbar onMenuClick={vi.fn()} />)
25
+ expect(screen.getByLabelText('Notifications, unread')).toBeInTheDocument()
26
+ })
27
+
28
+ it('renders settings button', () => {
29
+ render(<Toolbar onMenuClick={vi.fn()} />)
30
+ expect(screen.getByLabelText('Settings')).toBeInTheDocument()
31
+ })
32
+
33
+ it('has screen reader text for unread notifications', () => {
34
+ render(<Toolbar onMenuClick={vi.fn()} />)
35
+ expect(screen.getByText('You have unread notifications')).toBeInTheDocument()
36
+ })
37
+
38
+ it('renders language switcher', () => {
39
+ render(<Toolbar onMenuClick={vi.fn()} />)
40
+ expect(screen.getByTestId('lang-switcher')).toBeInTheDocument()
41
+ })
42
+
43
+ it('renders as header element', () => {
44
+ render(<Toolbar onMenuClick={vi.fn()} />)
45
+ expect(screen.getByRole('banner')).toBeInTheDocument()
46
+ })
47
+ })
@@ -0,0 +1,68 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Menu, Search, LogOut } from 'lucide-react';
3
+ import { useNavigate } from 'react-router-dom';
4
+ import { LanguageSwitcher } from '../ui/LanguageSwitcher';
5
+ import { NotificationBell } from './NotificationBell';
6
+ import { CommandPalette } from './CommandPalette';
7
+ import { Avatar } from '../ui/Avatar';
8
+ import { useAuth } from '../../context/AuthContext';
9
+
10
+ interface ToolbarProps {
11
+ onMenuClick: () => void;
12
+ }
13
+
14
+ export function Toolbar({ onMenuClick }: ToolbarProps) {
15
+ const [cmdOpen, setCmdOpen] = useState(false);
16
+ const { user, logout } = useAuth();
17
+ const navigate = useNavigate();
18
+ const displayName = user?.email?.split('@')[0] ?? 'User';
19
+
20
+ useEffect(() => {
21
+ const handler = (e: KeyboardEvent) => {
22
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
23
+ e.preventDefault();
24
+ setCmdOpen((v) => !v);
25
+ }
26
+ };
27
+ document.addEventListener('keydown', handler);
28
+ return () => document.removeEventListener('keydown', handler);
29
+ }, []);
30
+
31
+ return (
32
+ <>
33
+ <header className={
34
+ 'sticky top-0 z-40 px-lg py-md ' +
35
+ 'bg-[rgba(10,10,15,0.8)] backdrop-blur-[24px] backdrop-saturate-[180%] ' +
36
+ 'border-b border-white/[0.06]'
37
+ }>
38
+ <div className="flex items-center gap-lg">
39
+ <button onClick={onMenuClick} aria-label="Toggle menu"
40
+ className="md:hidden p-sm hover:bg-white/[0.08] rounded-apple-md cursor-pointer">
41
+ <Menu className="w-5 h-5" />
42
+ </button>
43
+
44
+ <button onClick={() => setCmdOpen(true)}
45
+ className="hidden md:flex items-center gap-md px-lg py-sm bg-white/[0.04] hover:bg-white/[0.08] rounded-apple-md transition-colors cursor-pointer max-w-xs flex-1">
46
+ <Search className="w-4 h-4 text-apple-label-tertiary" />
47
+ <span className="text-sm text-apple-label-tertiary">Search...</span>
48
+ <kbd className="ml-auto text-[10px] text-apple-label-tertiary bg-white/5 px-1.5 py-0.5 rounded">⌘K</kbd>
49
+ </button>
50
+
51
+ <div className="flex items-center gap-sm ml-auto">
52
+ <LanguageSwitcher />
53
+ <NotificationBell />
54
+ <div className="flex items-center gap-md pl-md border-l border-white/[0.06]">
55
+ <Avatar name={displayName} size="sm" />
56
+ <button onClick={() => { logout(); navigate('/login'); }}
57
+ aria-label="Logout"
58
+ className="p-sm hover:bg-white/[0.08] rounded-apple-md cursor-pointer">
59
+ <LogOut className="w-4 h-4 text-apple-label-tertiary" />
60
+ </button>
61
+ </div>
62
+ </div>
63
+ </div>
64
+ </header>
65
+ <CommandPalette open={cmdOpen} onClose={() => setCmdOpen(false)} />
66
+ </>
67
+ );
68
+ }