portal-mojo 0.1.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 (369) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +55 -0
  3. package/docs/DetailView.md +173 -0
  4. package/docs/ModelTable.md +128 -0
  5. package/docs/README.md +98 -0
  6. package/docs/admin-assistant.md +72 -0
  7. package/docs/admin-bouncer.md +120 -0
  8. package/docs/admin-cloudwatch.md +38 -0
  9. package/docs/admin-credentials.md +116 -0
  10. package/docs/admin-dashboard.md +15 -0
  11. package/docs/admin-devices-geoip.md +284 -0
  12. package/docs/admin-dns-certificates.md +122 -0
  13. package/docs/admin-dns-records.md +65 -0
  14. package/docs/admin-dns-registrar.md +33 -0
  15. package/docs/admin-dns.md +80 -0
  16. package/docs/admin-identity-users.md +103 -0
  17. package/docs/admin-incidents.md +71 -0
  18. package/docs/admin-jobs.md +426 -0
  19. package/docs/admin-members.md +112 -0
  20. package/docs/admin-messaging.md +56 -0
  21. package/docs/admin-metrics-explorer.md +63 -0
  22. package/docs/admin-monitoring.md +99 -0
  23. package/docs/admin-network-security.md +393 -0
  24. package/docs/admin-phonehub.md +27 -0
  25. package/docs/admin-push.md +36 -0
  26. package/docs/admin-rules.md +87 -0
  27. package/docs/admin-security-tickets.md +40 -0
  28. package/docs/admin-settings.md +78 -0
  29. package/docs/admin-shortlinks.md +32 -0
  30. package/docs/admin-storage.md +141 -0
  31. package/docs/auth-pages.md +212 -0
  32. package/docs/auth.md +58 -0
  33. package/docs/calendar.md +122 -0
  34. package/docs/charts.md +303 -0
  35. package/docs/client.md +206 -0
  36. package/docs/collection-multiselect.md +117 -0
  37. package/docs/collection-select.md +132 -0
  38. package/docs/combobox.md +105 -0
  39. package/docs/dataview.md +226 -0
  40. package/docs/datepicker.md +151 -0
  41. package/docs/daterange.md +170 -0
  42. package/docs/datetimepicker.md +249 -0
  43. package/docs/defineModel.md +72 -0
  44. package/docs/detail-primitives.md +174 -0
  45. package/docs/feedback.md +85 -0
  46. package/docs/file-upload-ux.md +146 -0
  47. package/docs/forms.md +435 -0
  48. package/docs/grouping-and-fmt.md +98 -0
  49. package/docs/idioms.md +112 -0
  50. package/docs/image-editor.md +101 -0
  51. package/docs/loading.md +242 -0
  52. package/docs/location-address.md +76 -0
  53. package/docs/markdown.md +203 -0
  54. package/docs/menus-and-access.md +89 -0
  55. package/docs/multiselect-dropdown.md +153 -0
  56. package/docs/params.md +73 -0
  57. package/docs/popover.md +130 -0
  58. package/docs/realtime.md +61 -0
  59. package/docs/record-feed.md +238 -0
  60. package/docs/right-panel.md +42 -0
  61. package/docs/tabs-and-form-wizard.md +60 -0
  62. package/docs/taginput.md +120 -0
  63. package/docs/timepicker.md +173 -0
  64. package/docs/timezone-select.md +147 -0
  65. package/docs/worldmap.md +302 -0
  66. package/package.json +55 -0
  67. package/src/admin/assistant/AssistantFeed.tsx +29 -0
  68. package/src/admin/assistant/AssistantPanel.tsx +65 -0
  69. package/src/admin/assistant/api.ts +17 -0
  70. package/src/admin/assistant/data.ts +63 -0
  71. package/src/admin/assistant/index.ts +21 -0
  72. package/src/admin/assistant/launchers.tsx +33 -0
  73. package/src/admin/assistant/pages.tsx +78 -0
  74. package/src/admin/assistant/streaming.ts +238 -0
  75. package/src/admin/assistant/types.ts +30 -0
  76. package/src/admin/bouncer/devices.tsx +230 -0
  77. package/src/admin/bouncer/index.ts +34 -0
  78. package/src/admin/bouncer/models.ts +199 -0
  79. package/src/admin/bouncer/signals.tsx +182 -0
  80. package/src/admin/bouncer/signatures.tsx +220 -0
  81. package/src/admin/cloudwatch/CloudWatchChart.tsx +61 -0
  82. package/src/admin/cloudwatch/CloudWatchDashboardPage.tsx +52 -0
  83. package/src/admin/cloudwatch/CloudWatchResourceDetail.tsx +33 -0
  84. package/src/admin/cloudwatch/data.ts +163 -0
  85. package/src/admin/cloudwatch/index.ts +16 -0
  86. package/src/admin/credentials/group-api-keys.tsx +433 -0
  87. package/src/admin/credentials/index.ts +4 -0
  88. package/src/admin/credentials/models.ts +244 -0
  89. package/src/admin/credentials/secret-dialog.tsx +77 -0
  90. package/src/admin/credentials/webhook-subscriptions.tsx +423 -0
  91. package/src/admin/dashboard/AdminDashboardPage.tsx +101 -0
  92. package/src/admin/dashboard/data.ts +60 -0
  93. package/src/admin/dashboard/index.ts +2 -0
  94. package/src/admin/dns/CertificateDetail.tsx +115 -0
  95. package/src/admin/dns/CertificateLifecyclePoller.tsx +141 -0
  96. package/src/admin/dns/CertificateRequestDialog.tsx +103 -0
  97. package/src/admin/dns/CertificatesPage.tsx +85 -0
  98. package/src/admin/dns/DnsRecordEditor.tsx +110 -0
  99. package/src/admin/dns/DnsRecordsPage.tsx +36 -0
  100. package/src/admin/dns/DnsRecordsPanel.tsx +71 -0
  101. package/src/admin/dns/DomainCertificatesSection.tsx +40 -0
  102. package/src/admin/dns/DomainDetail.tsx +41 -0
  103. package/src/admin/dns/DomainOnboardingDialog.tsx +27 -0
  104. package/src/admin/dns/DomainPurchaseDetail.tsx +11 -0
  105. package/src/admin/dns/DomainPurchaseWizard.tsx +112 -0
  106. package/src/admin/dns/DomainPurchasesPage.tsx +16 -0
  107. package/src/admin/dns/DomainsPage.tsx +27 -0
  108. package/src/admin/dns/ProviderCredentialsPage.tsx +304 -0
  109. package/src/admin/dns/RegistrantContactPage.tsx +42 -0
  110. package/src/admin/dns/api.ts +515 -0
  111. package/src/admin/dns/certificate-data.ts +221 -0
  112. package/src/admin/dns/data.ts +32 -0
  113. package/src/admin/dns/dns-data.ts +266 -0
  114. package/src/admin/dns/dns-integration.ts +24 -0
  115. package/src/admin/dns/domain-links.ts +37 -0
  116. package/src/admin/dns/index.ts +86 -0
  117. package/src/admin/dns/models.ts +418 -0
  118. package/src/admin/dns/purchase-data.ts +67 -0
  119. package/src/admin/dns/registrant-data.ts +52 -0
  120. package/src/admin/identity/index.ts +5 -0
  121. package/src/admin/identity/members/GroupMembersPanel.tsx +146 -0
  122. package/src/admin/identity/members/MemberDetail.tsx +226 -0
  123. package/src/admin/identity/members/MembersPage.tsx +144 -0
  124. package/src/admin/identity/members/index.ts +24 -0
  125. package/src/admin/identity/members/member-flows.tsx +132 -0
  126. package/src/admin/identity/members/models.ts +204 -0
  127. package/src/admin/identity/users/UserDetail.tsx +257 -0
  128. package/src/admin/identity/users/UsersPage.tsx +163 -0
  129. package/src/admin/identity/users/index.ts +27 -0
  130. package/src/admin/identity/users/models.ts +232 -0
  131. package/src/admin/identity/users/sections/ApiKeysSection.tsx +119 -0
  132. package/src/admin/identity/users/sections/AuditSection.tsx +190 -0
  133. package/src/admin/identity/users/sections/DevicesSection.tsx +140 -0
  134. package/src/admin/identity/users/sections/GroupsSection.tsx +64 -0
  135. package/src/admin/identity/users/sections/LoginsSection.tsx +106 -0
  136. package/src/admin/identity/users/sections/MetadataPanel.tsx +36 -0
  137. package/src/admin/identity/users/sections/NotificationsSection.tsx +117 -0
  138. package/src/admin/identity/users/sections/OAuthSection.tsx +72 -0
  139. package/src/admin/identity/users/sections/OverviewSection.tsx +109 -0
  140. package/src/admin/identity/users/sections/PermissionsSection.tsx +35 -0
  141. package/src/admin/identity/users/sections/PersonalSection.tsx +73 -0
  142. package/src/admin/identity/users/sections/ProfileSection.tsx +175 -0
  143. package/src/admin/identity/users/sections/SecuritySection.tsx +123 -0
  144. package/src/admin/identity/users/sections/actions.tsx +752 -0
  145. package/src/admin/identity/users/sections/permission-catalog.ts +138 -0
  146. package/src/admin/identity/users/sections/queries.ts +74 -0
  147. package/src/admin/identity/users/sections/shared.tsx +299 -0
  148. package/src/admin/incidents/EventDetail.tsx +43 -0
  149. package/src/admin/incidents/EventsPage.tsx +52 -0
  150. package/src/admin/incidents/IncidentDetail.tsx +68 -0
  151. package/src/admin/incidents/IncidentsPage.tsx +102 -0
  152. package/src/admin/incidents/forensics.tsx +73 -0
  153. package/src/admin/incidents/index.ts +16 -0
  154. package/src/admin/incidents/models.ts +113 -0
  155. package/src/admin/incidents/sanitize.ts +71 -0
  156. package/src/admin/index.ts +383 -0
  157. package/src/admin/jobs/JobDashboardPage.tsx +111 -0
  158. package/src/admin/jobs/JobDetail.tsx +721 -0
  159. package/src/admin/jobs/JobRunnersPage.tsx +216 -0
  160. package/src/admin/jobs/JobsTablePage.tsx +122 -0
  161. package/src/admin/jobs/RunnerDetail.tsx +576 -0
  162. package/src/admin/jobs/ScheduledTaskDetail.tsx +421 -0
  163. package/src/admin/jobs/ScheduledTasksPage.tsx +167 -0
  164. package/src/admin/jobs/columns.tsx +297 -0
  165. package/src/admin/jobs/control.ts +488 -0
  166. package/src/admin/jobs/index.ts +85 -0
  167. package/src/admin/jobs/models.ts +599 -0
  168. package/src/admin/jobs/queries.ts +272 -0
  169. package/src/admin/jobs/scheduled-task-form.tsx +493 -0
  170. package/src/admin/jobs/sections/JobChannelsPanel.tsx +142 -0
  171. package/src/admin/jobs/sections/JobOperationsSection.tsx +423 -0
  172. package/src/admin/jobs/sections/JobRunnersStrip.tsx +82 -0
  173. package/src/admin/jobs/sections/JobThroughputSection.tsx +118 -0
  174. package/src/admin/messaging/EmailDomainWizard.tsx +24 -0
  175. package/src/admin/messaging/EmailDomainsPage.tsx +17 -0
  176. package/src/admin/messaging/EmailTemplatesPage.tsx +13 -0
  177. package/src/admin/messaging/MailboxesPage.tsx +15 -0
  178. package/src/admin/messaging/PublicMessagesPage.tsx +13 -0
  179. package/src/admin/messaging/SandboxedEmailPreview.tsx +23 -0
  180. package/src/admin/messaging/SentMessagesPage.tsx +11 -0
  181. package/src/admin/messaging/api.ts +17 -0
  182. package/src/admin/messaging/data.ts +10 -0
  183. package/src/admin/messaging/index.ts +23 -0
  184. package/src/admin/messaging/models.ts +76 -0
  185. package/src/admin/messaging/push/PushPage.tsx +44 -0
  186. package/src/admin/messaging/push/api.ts +17 -0
  187. package/src/admin/messaging/push/index.ts +9 -0
  188. package/src/admin/messaging/push/models.ts +43 -0
  189. package/src/admin/messaging/sanitize.ts +44 -0
  190. package/src/admin/monitoring/LogInspector.tsx +160 -0
  191. package/src/admin/monitoring/LogsPage.tsx +135 -0
  192. package/src/admin/monitoring/MetricsExplorerPage.tsx +360 -0
  193. package/src/admin/monitoring/MetricsPermissionsPage.tsx +166 -0
  194. package/src/admin/monitoring/MetricsSourcePicker.tsx +136 -0
  195. package/src/admin/monitoring/index.ts +55 -0
  196. package/src/admin/monitoring/metrics-explorer-client.ts +229 -0
  197. package/src/admin/monitoring/metrics-explorer-data.ts +158 -0
  198. package/src/admin/monitoring/models.ts +155 -0
  199. package/src/admin/network/BlockedIPsPage.tsx +194 -0
  200. package/src/admin/network/BlockedIpDetail.tsx +262 -0
  201. package/src/admin/network/FirewallLogPage.tsx +115 -0
  202. package/src/admin/network/IPSetDetail.tsx +280 -0
  203. package/src/admin/network/IPSetEditor.tsx +191 -0
  204. package/src/admin/network/IPSetsPage.tsx +177 -0
  205. package/src/admin/network/geofence/BlocksTab.tsx +289 -0
  206. package/src/admin/network/geofence/ExemptionsTab.tsx +349 -0
  207. package/src/admin/network/geofence/GeofencingPage.tsx +119 -0
  208. package/src/admin/network/geofence/PostureHeader.tsx +184 -0
  209. package/src/admin/network/geofence/RuleEditor.tsx +176 -0
  210. package/src/admin/network/geofence/RulesTab.tsx +254 -0
  211. package/src/admin/network/geofence/SimulatorTab.tsx +281 -0
  212. package/src/admin/network/geofence/geofence-data.ts +674 -0
  213. package/src/admin/network/index.ts +68 -0
  214. package/src/admin/network/models.ts +607 -0
  215. package/src/admin/phonehub/PhoneHubPage.tsx +57 -0
  216. package/src/admin/phonehub/api.ts +47 -0
  217. package/src/admin/phonehub/data.ts +6 -0
  218. package/src/admin/phonehub/index.ts +10 -0
  219. package/src/admin/phonehub/models.ts +68 -0
  220. package/src/admin/rules/HandlerChainBuilder.tsx +51 -0
  221. package/src/admin/rules/RuleSetDetailPage.tsx +33 -0
  222. package/src/admin/rules/RuleSetsPage.tsx +33 -0
  223. package/src/admin/rules/editors.tsx +54 -0
  224. package/src/admin/rules/handler-dsl.ts +213 -0
  225. package/src/admin/rules/index.ts +13 -0
  226. package/src/admin/rules/models.ts +112 -0
  227. package/src/admin/security/devices/LoginEventDetail.tsx +123 -0
  228. package/src/admin/security/devices/LoginLocationMap.tsx +321 -0
  229. package/src/admin/security/devices/LoginLocationsPage.tsx +162 -0
  230. package/src/admin/security/devices/UserDeviceDetail.tsx +546 -0
  231. package/src/admin/security/devices/UserDeviceLocationDetail.tsx +272 -0
  232. package/src/admin/security/devices/UserDevicesPage.tsx +84 -0
  233. package/src/admin/security/devices/index.ts +40 -0
  234. package/src/admin/security/devices/models.ts +470 -0
  235. package/src/admin/security/geoip/GeoIpCachePage.tsx +135 -0
  236. package/src/admin/security/geoip/GeoIpDossier.tsx +804 -0
  237. package/src/admin/security/geoip/geoip-forms.ts +102 -0
  238. package/src/admin/security/geoip/index.ts +27 -0
  239. package/src/admin/security/geoip/models.ts +397 -0
  240. package/src/admin/security/index.ts +31 -0
  241. package/src/admin/security/models.ts +137 -0
  242. package/src/admin/security/tickets.tsx +562 -0
  243. package/src/admin/security-permissions.ts +5 -0
  244. package/src/admin/settings/SettingDetail.tsx +57 -0
  245. package/src/admin/settings/SettingEditor.tsx +157 -0
  246. package/src/admin/settings/SettingsPage.tsx +72 -0
  247. package/src/admin/settings/index.ts +19 -0
  248. package/src/admin/settings/model.ts +143 -0
  249. package/src/admin/shortlinks/ShortlinkDetail.tsx +55 -0
  250. package/src/admin/shortlinks/ShortlinkHistoryPage.tsx +18 -0
  251. package/src/admin/shortlinks/ShortlinksPage.tsx +59 -0
  252. package/src/admin/shortlinks/index.ts +18 -0
  253. package/src/admin/shortlinks/models.ts +246 -0
  254. package/src/admin/storage/BackendsPage.tsx +43 -0
  255. package/src/admin/storage/BucketDetail.tsx +77 -0
  256. package/src/admin/storage/BucketsPage.tsx +64 -0
  257. package/src/admin/storage/FileManagerDetail.tsx +36 -0
  258. package/src/admin/storage/FilePreview.tsx +54 -0
  259. package/src/admin/storage/FileUploadSurface.tsx +250 -0
  260. package/src/admin/storage/FileView.tsx +109 -0
  261. package/src/admin/storage/FilesPage.tsx +53 -0
  262. package/src/admin/storage/file-renditions.ts +99 -0
  263. package/src/admin/storage/index.ts +31 -0
  264. package/src/admin/storage/models.ts +585 -0
  265. package/src/admin/storage/storage-dialogs.tsx +80 -0
  266. package/src/charts/CircularProgress.tsx +281 -0
  267. package/src/charts/KPIStrip.tsx +162 -0
  268. package/src/charts/KPITile.tsx +125 -0
  269. package/src/charts/MetricsChart.tsx +459 -0
  270. package/src/charts/MetricsMiniWidget.tsx +588 -0
  271. package/src/charts/MiniChart.tsx +283 -0
  272. package/src/charts/PieChart.tsx +354 -0
  273. package/src/charts/SeriesChart.tsx +378 -0
  274. package/src/charts/chart-dialogs.tsx +149 -0
  275. package/src/charts/exportChart.ts +144 -0
  276. package/src/charts/index.ts +28 -0
  277. package/src/charts/pie-math.ts +127 -0
  278. package/src/charts/stats.ts +265 -0
  279. package/src/charts/worldmap/WorldMap.tsx +649 -0
  280. package/src/charts/worldmap/countryCentroids.ts +304 -0
  281. package/src/charts/worldmap/geo.ts +293 -0
  282. package/src/charts/worldmap/use-world-land.ts +47 -0
  283. package/src/charts/worldmap/world-land.ts +18 -0
  284. package/src/charts/worldmap/worldmap-data.ts +285 -0
  285. package/src/client/auth.ts +737 -0
  286. package/src/client/client.ts +268 -0
  287. package/src/client/duid.ts +31 -0
  288. package/src/client/errors.ts +33 -0
  289. package/src/client/group-context.ts +28 -0
  290. package/src/client/group.tsx +126 -0
  291. package/src/client/hooks.ts +50 -0
  292. package/src/client/index.ts +37 -0
  293. package/src/client/jwt.ts +75 -0
  294. package/src/client/location.ts +285 -0
  295. package/src/client/lookups.ts +41 -0
  296. package/src/client/markdown-fallback.tsx +115 -0
  297. package/src/client/markdown-parse.ts +386 -0
  298. package/src/client/markdown-sanitize.ts +217 -0
  299. package/src/client/markdown.ts +122 -0
  300. package/src/client/me.ts +188 -0
  301. package/src/client/mock.ts +9984 -0
  302. package/src/client/model.ts +227 -0
  303. package/src/client/params.ts +292 -0
  304. package/src/client/realtime-mock.ts +221 -0
  305. package/src/client/realtime.ts +561 -0
  306. package/src/client/record-feed.ts +329 -0
  307. package/src/client/safe-export.ts +71 -0
  308. package/src/client/types.ts +278 -0
  309. package/src/client/upload.ts +421 -0
  310. package/src/ui/AddressField.tsx +273 -0
  311. package/src/ui/AttachmentQueue.tsx +92 -0
  312. package/src/ui/CollectionMultiSelect.tsx +622 -0
  313. package/src/ui/CollectionSelect.tsx +517 -0
  314. package/src/ui/ComboBox.tsx +336 -0
  315. package/src/ui/DetailView.tsx +438 -0
  316. package/src/ui/FileDrop.tsx +194 -0
  317. package/src/ui/FileField.tsx +369 -0
  318. package/src/ui/FilterBar.tsx +269 -0
  319. package/src/ui/FormFields.tsx +77 -0
  320. package/src/ui/FormView.tsx +360 -0
  321. package/src/ui/FormWizard.tsx +336 -0
  322. package/src/ui/GroupSwitcher.tsx +202 -0
  323. package/src/ui/Guarded.tsx +16 -0
  324. package/src/ui/MarkdownView.tsx +89 -0
  325. package/src/ui/ModelTable.tsx +848 -0
  326. package/src/ui/MultiSelectDropdown.tsx +390 -0
  327. package/src/ui/Popover.tsx +204 -0
  328. package/src/ui/RecordFeed.tsx +627 -0
  329. package/src/ui/RequiresGroup.tsx +32 -0
  330. package/src/ui/RightPanel.tsx +107 -0
  331. package/src/ui/SidebarNav.tsx +330 -0
  332. package/src/ui/Tabs.tsx +161 -0
  333. package/src/ui/TagInput.tsx +381 -0
  334. package/src/ui/ThemeProvider.tsx +52 -0
  335. package/src/ui/UploadQueue.tsx +603 -0
  336. package/src/ui/armed-button.tsx +91 -0
  337. package/src/ui/date/Calendar.tsx +498 -0
  338. package/src/ui/date/DatePicker.tsx +289 -0
  339. package/src/ui/date/DateRangePicker.tsx +280 -0
  340. package/src/ui/date/DateTimePicker.tsx +587 -0
  341. package/src/ui/date/PresetRail.tsx +165 -0
  342. package/src/ui/date/TimePicker.tsx +624 -0
  343. package/src/ui/date/TimezoneSelect.tsx +267 -0
  344. package/src/ui/date/fns.ts +679 -0
  345. package/src/ui/detail/DataView.tsx +692 -0
  346. package/src/ui/detail/FlowStrip.tsx +80 -0
  347. package/src/ui/detail/KnownFieldsCard.tsx +121 -0
  348. package/src/ui/detail/MetadataSection.tsx +219 -0
  349. package/src/ui/detail/StackTraceView.tsx +150 -0
  350. package/src/ui/detail/StatusPanel.tsx +73 -0
  351. package/src/ui/detail/Timeline.tsx +52 -0
  352. package/src/ui/detail/index.ts +12 -0
  353. package/src/ui/field-registry.tsx +428 -0
  354. package/src/ui/field-wire.ts +322 -0
  355. package/src/ui/form-autosave.ts +583 -0
  356. package/src/ui/format.ts +444 -0
  357. package/src/ui/grouping.ts +150 -0
  358. package/src/ui/image-editor/ImageEditor.tsx +484 -0
  359. package/src/ui/image-editor/canvas.ts +254 -0
  360. package/src/ui/image-editor/index.ts +3 -0
  361. package/src/ui/image-editor/math.ts +416 -0
  362. package/src/ui/index.ts +58 -0
  363. package/src/ui/loading.tsx +604 -0
  364. package/src/ui/menu-registry.ts +213 -0
  365. package/src/ui/modal.tsx +297 -0
  366. package/src/ui/password.tsx +302 -0
  367. package/src/ui/schema-form-core.tsx +315 -0
  368. package/src/ui/toast.tsx +219 -0
  369. package/src/ui/ui.tsx +45 -0
@@ -0,0 +1,285 @@
1
+ // worldmap-data.ts — the WorldMap's data vocabulary and the pure binding
2
+ // helpers that turn django-mojo payloads into markers. No React, no DOM, so
3
+ // every rule here is headless-testable and the three admin consumers
4
+ // (#1291 logins, #1287 geofence, #1292 dashboard geography) share ONE
5
+ // slug-parsing and sizing implementation instead of three.
6
+ import type { ReactNode } from 'react';
7
+ import { COUNTRY_CENTROIDS, type CountryCentroid } from './countryCentroids';
8
+ import type { LatLng } from './geo';
9
+
10
+ /**
11
+ * Marker/route colors are TOKENS, resolved by the browser at paint time.
12
+ * A theme flip therefore needs no re-render and no recomputed hexes.
13
+ *
14
+ * `scale` is the value ramp: it replaces web-mojo's hardcoded teal→amber
15
+ * rgba mix (MetricsCountryMapView.js:171-178, duplicated verbatim in
16
+ * LoginLocationMapView.js:283-288) with the same visual gradient expressed
17
+ * in tokens, so it reads correctly in both themes.
18
+ */
19
+ export const MAP_TONES = {
20
+ ok: 'var(--ok)',
21
+ bad: 'var(--bad)',
22
+ warn: 'var(--warn)',
23
+ info: 'var(--info)',
24
+ accent: 'var(--accent)',
25
+ mute: 'var(--mute)',
26
+ scale: 'var(--ok)',
27
+ } as const;
28
+
29
+ export type WorldMapTone = keyof typeof MAP_TONES;
30
+
31
+ const clamp01 = (v: number): number => (Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : 0);
32
+
33
+ /**
34
+ * Tone → a CSS color string. `scale` interpolates `--ok` → `--warn` on
35
+ * `intensity` via `color-mix`; every other tone ignores intensity.
36
+ *
37
+ * An unknown tone (wire data can carry anything) falls back to `accent`
38
+ * WITH a console.warn — never to rendering nothing.
39
+ */
40
+ export function toneColor(tone: WorldMapTone = 'accent', intensity = 0): string {
41
+ let key = tone;
42
+ if (!(key in MAP_TONES)) {
43
+ console.warn(`WorldMap: unknown tone "${String(tone)}" — falling back to "accent"`);
44
+ key = 'accent';
45
+ }
46
+ if (key !== 'scale') return MAP_TONES[key];
47
+ const okPct = Math.round((1 - clamp01(intensity)) * 100);
48
+ return `color-mix(in srgb, var(--ok) ${okPct}%, var(--warn))`;
49
+ }
50
+
51
+ // REMOVED 2026-08-06: `LOGIN_EVENT_TONES` / `loginEventTone(eventType)`.
52
+ //
53
+ // Ported from web-mojo's rgba palette at LoginLocationMapView.js:31-41, which
54
+ // keyed off a login "event type" (success_login / failed_login / suspicious).
55
+ // Board #1291 then established from django-mojo source that `UserLoginEvent`
56
+ // has NO `event_type` field and never has — so every real row fell through to
57
+ // the grey `mute` default. That is precisely why web-mojo's login map rendered
58
+ // entirely grey, a bug that went unnoticed for years.
59
+ //
60
+ // The working replacement is `loginRiskTone(row)` in
61
+ // `admin/security/devices/models.ts`: danger on `is_new_country`, warning on
62
+ // `is_new_region`, otherwise success — fields the wire actually sends. Do not
63
+ // reintroduce a tone helper here that keys off an event-type vocabulary.
64
+
65
+ /** One plotted point. `T` is the consumer's own row, handed back to handlers. */
66
+ export interface WorldMapMarker<T = unknown> {
67
+ /** Stable React key and the identity used for hover/click bookkeeping. */
68
+ id?: string;
69
+ lat: number;
70
+ lng: number;
71
+ /** Diameter in px. Default 14. */
72
+ size?: number;
73
+ tone?: WorldMapTone;
74
+ /** 0..1 — drives the `scale` tone's ramp. */
75
+ intensity?: number;
76
+ /** Bootstrap icon class drawn inside the dot, e.g. 'bi bi-broadcast-pin'. */
77
+ icon?: string;
78
+ /** Tooltip heading. */
79
+ label?: string;
80
+ /** Numeric value; the default tooltip renders it under the label. */
81
+ value?: number;
82
+ /** Extra tooltip body — a ReactNode SLOT, never an HTML string. */
83
+ detail?: ReactNode;
84
+ /** Legend bucket. Toggling that key in the legend hides this marker. */
85
+ legendKey?: string;
86
+ /** Typed payload for onMarkerClick/onMarkerDoubleClick/onMarkerHover. */
87
+ data?: T;
88
+ }
89
+
90
+ /** An origin→destination line. Endpoint order decides which way the arc bows. */
91
+ export interface WorldMapRoute {
92
+ id?: string;
93
+ from: LatLng;
94
+ to: LatLng;
95
+ /** 0..1 — width, opacity and (for tone 'scale') color all ride this. */
96
+ intensity?: number;
97
+ tone?: WorldMapTone;
98
+ label?: string;
99
+ legendKey?: string;
100
+ }
101
+
102
+ export interface WorldMapLegendItem {
103
+ key: string;
104
+ label: string;
105
+ tone?: WorldMapTone;
106
+ /** Explicit swatch color; overrides `tone`. Use a token, not a hex. */
107
+ color?: string;
108
+ }
109
+
110
+ // ── Injectable basemap geometry ───────────────────────────────────────
111
+ // The component ships NO coastlines (see docs/worldmap.md — that is an open
112
+ // decision). These are the minimal GeoJSON shapes the `land` prop accepts so
113
+ // geometry can be dropped in later without an API change.
114
+
115
+ /** GeoJSON position: [lng, lat] — note the order, opposite of LatLng. */
116
+ export type GeoPosition = [number, number];
117
+
118
+ export interface GeoPolygon {
119
+ type: 'Polygon';
120
+ coordinates: GeoPosition[][];
121
+ }
122
+
123
+ export interface GeoMultiPolygon {
124
+ type: 'MultiPolygon';
125
+ coordinates: GeoPosition[][][];
126
+ }
127
+
128
+ export interface GeoLandFeature {
129
+ type: 'Feature';
130
+ geometry: GeoPolygon | GeoMultiPolygon | null;
131
+ properties?: Record<string, unknown> | null;
132
+ }
133
+
134
+ export interface GeoLandCollection {
135
+ type: 'FeatureCollection';
136
+ features: GeoLandFeature[];
137
+ }
138
+
139
+ /** Land geometry: a FeatureCollection, a bare feature array, or a geometry. */
140
+ export type WorldMapLand = GeoLandCollection | readonly GeoLandFeature[] | GeoPolygon | GeoMultiPolygon;
141
+
142
+ /** Normalize any accepted `land` shape into rings of [lng, lat] positions. */
143
+ export function landRings(land: WorldMapLand | null | undefined): GeoPosition[][] {
144
+ if (!land) return [];
145
+ // `'type' in land` rather than Array.isArray: TS does not narrow a
146
+ // ReadonlyArray out of a union via Array.isArray.
147
+ const features: readonly GeoLandFeature[] = 'type' in land
148
+ ? (land.type === 'FeatureCollection'
149
+ ? (Array.isArray(land.features) ? land.features : [])
150
+ : [{ type: 'Feature', geometry: land }])
151
+ : land;
152
+
153
+ const rings: GeoPosition[][] = [];
154
+ let skipped = 0;
155
+ for (const feature of features) {
156
+ const geometry = feature?.geometry;
157
+ if (!geometry) { skipped += 1; continue; }
158
+ if (geometry.type === 'Polygon') {
159
+ for (const ring of geometry.coordinates) rings.push(ring);
160
+ } else if (geometry.type === 'MultiPolygon') {
161
+ for (const polygon of geometry.coordinates) for (const ring of polygon) rings.push(ring);
162
+ } else {
163
+ skipped += 1;
164
+ }
165
+ }
166
+ if (skipped > 0) {
167
+ console.warn(`WorldMap: skipped ${skipped} land feature(s) — only Polygon and MultiPolygon geometries are drawn`);
168
+ }
169
+ return rings;
170
+ }
171
+
172
+ // ── Sizing + slug binding ─────────────────────────────────────────────
173
+
174
+ /**
175
+ * Value → marker diameter. Default range [18, 42] reproduces web-mojo's
176
+ * `Math.round(18 + intensity * 24)` exactly (LoginLocationMapView used
177
+ * `* 26`; pass `[18, 44]` for that). A non-positive or non-finite max means
178
+ * "no scale yet" and returns the minimum rather than NaN.
179
+ */
180
+ export function scaleMarkerSize(
181
+ value: number,
182
+ max: number,
183
+ range: readonly [number, number] = [18, 42],
184
+ scale: 'linear' | 'sqrt' = 'linear',
185
+ ): number {
186
+ const [min, top] = range;
187
+ if (!Number.isFinite(max) || max <= 0 || !Number.isFinite(value)) return Math.round(min);
188
+ const i = clamp01(value / max);
189
+ const eased = scale === 'sqrt' ? Math.sqrt(i) : i;
190
+ return Math.round(min + eased * (top - min));
191
+ }
192
+
193
+ /**
194
+ * Metrics key → ISO2 country code, or null.
195
+ *
196
+ * ONE rule covers both shapes found in the wild: bare codes
197
+ * (MetricsCountryMapView.js:107-118 treats each key of `data.data` as an ISO
198
+ * code) and namespaced slugs (`incident_events:country:US`,
199
+ * `firewall:blocks:country:US`, `bouncer:blocks:country:US` —
200
+ * GeographyPanel.js:232-243 takes the trailing segment). Uppercase the last
201
+ * `:`-separated segment; accept it only if it is two characters AND a known
202
+ * country. `api_calls` and `ZZ` therefore both return null.
203
+ */
204
+ export function countryCodeFromSlug(slug: string | null | undefined): string | null {
205
+ if (!slug) return null;
206
+ const last = String(slug).split(':').pop()?.trim().toUpperCase();
207
+ if (!last || last.length !== 2) return null;
208
+ return last in COUNTRY_CENTROIDS ? last : null;
209
+ }
210
+
211
+ /** One country's rolled-up series — the payload markers carry as `data`. */
212
+ export interface CountryTotal {
213
+ code: string;
214
+ name: string;
215
+ centroid: CountryCentroid;
216
+ total: number;
217
+ values: number[];
218
+ /** total / the top country's total, 0..1. */
219
+ intensity: number;
220
+ }
221
+
222
+ export interface CountrySeriesOptions {
223
+ /** Top-N cut. Source default: MetricsCountryMapView's `maxCountries: 12`. */
224
+ maxCountries?: number;
225
+ tone?: WorldMapTone;
226
+ sizeRange?: readonly [number, number];
227
+ sizeScale?: 'linear' | 'sqrt';
228
+ /** Noun in the default tooltip line, e.g. "12,043 Events". */
229
+ metricLabel?: string;
230
+ /** Legend bucket for every produced marker. */
231
+ legendKey?: string;
232
+ }
233
+
234
+ /**
235
+ * The shared binding: a metrics `data.data` map → sorted, top-N,
236
+ * centroid-joined markers. Keys may be bare ISO2 or namespaced slugs; values
237
+ * may be a series array or a single number. Rows that sum to zero, or whose
238
+ * code is not a known country, are dropped — exactly what the source did,
239
+ * only once instead of in each consumer.
240
+ */
241
+ export function countrySeriesToMarkers(
242
+ series: Readonly<Record<string, readonly number[] | number | null | undefined>>,
243
+ opts: CountrySeriesOptions = {},
244
+ ): WorldMapMarker<CountryTotal>[] {
245
+ const {
246
+ maxCountries = 12,
247
+ tone = 'scale',
248
+ sizeRange = [18, 42],
249
+ sizeScale = 'linear',
250
+ metricLabel = 'Events',
251
+ legendKey,
252
+ } = opts;
253
+
254
+ const rows: Omit<CountryTotal, 'intensity'>[] = [];
255
+ for (const [key, raw] of Object.entries(series ?? {})) {
256
+ const code = countryCodeFromSlug(key);
257
+ if (!code) continue;
258
+ const values = Array.isArray(raw) ? raw.map((v) => Number(v) || 0) : [Number(raw) || 0];
259
+ const total = values.reduce((sum, v) => sum + v, 0);
260
+ if (!(total > 0)) continue;
261
+ const centroid = COUNTRY_CENTROIDS[code]!;
262
+ rows.push({ code, name: centroid.name, centroid, total, values });
263
+ }
264
+
265
+ rows.sort((a, b) => b.total - a.total);
266
+ const top = rows.slice(0, Math.max(0, maxCountries));
267
+ const max = top[0]?.total ?? 0;
268
+
269
+ return top.map((row) => {
270
+ const data: CountryTotal = { ...row, intensity: max > 0 ? row.total / max : 0 };
271
+ return {
272
+ id: row.code,
273
+ lat: row.centroid.lat,
274
+ lng: row.centroid.lng,
275
+ size: scaleMarkerSize(row.total, max, sizeRange, sizeScale),
276
+ tone,
277
+ intensity: data.intensity,
278
+ label: row.name,
279
+ value: row.total,
280
+ detail: `${row.total.toLocaleString()} ${metricLabel}`,
281
+ ...(legendKey ? { legendKey } : {}),
282
+ data,
283
+ } satisfies WorldMapMarker<CountryTotal>;
284
+ });
285
+ }