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,304 @@
1
+ // Country centroid table — ported from web-mojo
2
+ // src/extensions/map/countryCentroids.js (itself auto-generated from that
3
+ // repo's countries.geojson, a FeatureCollection of Points). Coordinate
4
+ // values are VERBATIM; only the key set changed, and only because it had
5
+ // to.
6
+ //
7
+ // The source has 249 rows but 244 distinct ISO2 keys: three codes carry
8
+ // several territories, and in a JS object literal the LAST one wins. That
9
+ // silently made `ES` resolve to Canarias rather than Spain — web-mojo's
10
+ // geofence country picker listed "Canarias" and Spanish metrics plotted in
11
+ // the Canary Islands. Duplicate keys are also a hard TS error (TS1117), so
12
+ // the table could not be transcribed as-is regardless.
13
+ //
14
+ // Resolution: the PRIMARY territory keeps the key; the 5 secondary rows are
15
+ // dropped, recorded here in full so nothing is lost and a future
16
+ // subdivision-aware table can restore them:
17
+ //
18
+ // BQ Saba lat 17.632512616389718 lng -63.23739481909494
19
+ // BQ Saint Eustatius lat 17.4919042294197 lng -62.978230589445026
20
+ // ES Canarias lat 28.297665106525546 lng -16.53799441021647
21
+ // TF Juan De Nova Island lat -17.06449193630804 lng 42.74374761089645
22
+ // TF Glorioso Islands lat -11.566224871643417 lng 47.290948081543384
23
+ //
24
+ // Kept: BQ → Bonaire, ES → Spain, TF → French Southern Territories.
25
+
26
+ /** One row of the centroid table: display name + the point to plot. */
27
+ export interface CountryCentroid {
28
+ name: string;
29
+ lat: number;
30
+ lng: number;
31
+ }
32
+
33
+ /** ISO2 → centroid. 244 entries; the framework's only country-name source. */
34
+ export const COUNTRY_CENTROIDS: Record<string, CountryCentroid> = {
35
+ AD: { name: 'Andorra', lat: 42.54859834854764, lng: 1.5802243611232873 },
36
+ AE: { name: 'United Arab Emirates', lat: 24.18250292309135, lng: 54.27920525789581 },
37
+ AF: { name: 'Afghanistan', lat: 34.13402601376932, lng: 66.59216131095278 },
38
+ AG: { name: 'Antigua and Barbuda', lat: 17.07146759372967, lng: -61.78530823226373 },
39
+ AI: { name: 'Anguilla', lat: 18.222874004219086, lng: -63.06008343771806 },
40
+ AL: { name: 'Albania', lat: 41.14165894891656, lng: 20.061082767269493 },
41
+ AM: { name: 'Armenia', lat: 40.17841274230679, lng: 45.05490831965259 },
42
+ AO: { name: 'Angola', lat: -12.167424062667942, lng: 17.651768783079 },
43
+ AQ: { name: 'Antarctica', lat: -77.16987521415838, lng: -177.56451613408842 },
44
+ AR: { name: 'Argentina', lat: -35.697270518120085, lng: -64.53238503843076 },
45
+ AS: { name: 'American Samoa', lat: -14.305711987770538, lng: -170.7007316174498 },
46
+ AT: { name: 'Austria', lat: 47.631858269895794, lng: 13.797778364631036 },
47
+ AU: { name: 'Australia', lat: -25.697337673983082, lng: 134.02277170916162 },
48
+ AW: { name: 'Aruba', lat: 12.515625722992898, lng: -69.97564014284046 },
49
+ AZ: { name: 'Azerbaijan', lat: 40.3920509942049, lng: 48.634592670644324 },
50
+ BA: { name: 'Bosnia and Herzegovina', lat: 44.14415356126429, lng: 17.83467240787538 },
51
+ BB: { name: 'Barbados', lat: 13.183219369337529, lng: -59.557383949150285 },
52
+ BD: { name: 'Bangladesh', lat: 23.673728665121, lng: 90.43212562608613 },
53
+ BE: { name: 'Belgium', lat: 50.6182138854095, lng: 4.675010154696485 },
54
+ BF: { name: 'Burkina Faso', lat: 12.108709036312737, lng: -1.6932816211842325 },
55
+ BG: { name: 'Bulgaria', lat: 42.82043677302438, lng: 25.251739122561908 },
56
+ BH: { name: 'Bahrain', lat: 26.04798501537066, lng: 50.540695402276775 },
57
+ BI: { name: 'Burundi', lat: -3.261251993278643, lng: 29.88518227845293 },
58
+ BJ: { name: 'Benin', lat: 9.503013199615893, lng: 2.305714528830206 },
59
+ BL: { name: 'Saint Barthelemy', lat: 17.90561691241738, lng: -62.83051610005156 },
60
+ BM: { name: 'Bermuda', lat: 32.315067430740726, lng: -64.7458500599169 },
61
+ BN: { name: 'Brunei Darussalam', lat: 4.543205889917609, lng: 114.6430958360464 },
62
+ BO: { name: 'Bolivia', lat: -16.7312488393574, lng: -64.45209597511206 },
63
+ BQ: { name: 'Bonaire', lat: 12.180844982440338, lng: -68.29350445958761 },
64
+ BR: { name: 'Brazil', lat: -11.524630416426652, lng: -54.355206608256424 },
65
+ BS: { name: 'Bahamas', lat: 24.72162633646784, lng: -78.07275370060313 },
66
+ BT: { name: 'Bhutan', lat: 27.42163933959824, lng: 90.46716647173861 },
67
+ BV: { name: 'Bouvet Island', lat: -54.42316679395248, lng: 3.411969465057627 },
68
+ BW: { name: 'Botswana', lat: -22.236609002062902, lng: 23.85779956995608 },
69
+ BY: { name: 'Belarus', lat: 53.46791374543163, lng: 27.964252054715104 },
70
+ BZ: { name: 'Belize', lat: 17.24252476647155, lng: -88.68273510023441 },
71
+ CA: { name: 'Canada', lat: 57.550480044655636, lng: -98.41680517868062 },
72
+ CC: { name: 'Cocos Islands', lat: -12.171249450199545, lng: 96.83688767323002 },
73
+ CD: { name: 'Congo DRC', lat: -3.338629596207896, lng: 23.419827574282188 },
74
+ CF: { name: 'Central African Republic', lat: 6.331390033944319, lng: 20.520743419397256 },
75
+ CG: { name: 'Congo', lat: -0.7294391595233845, lng: 14.879732849491393 },
76
+ CH: { name: 'Switzerland', lat: 46.73678128684938, lng: 8.286928794895285 },
77
+ CI: { name: "Côte d'Ivoire", lat: 7.536779279421307, lng: -5.571710194917734 },
78
+ CK: { name: 'Cook Islands', lat: -21.222613253399842, lng: -159.78768870952257 },
79
+ CL: { name: 'Chile', lat: -37.82938283049967, lng: -70.76863431739216 },
80
+ CM: { name: 'Cameroon', lat: 6.294168487480992, lng: 12.948474142398263 },
81
+ CN: { name: 'China', lat: 38.07325481105728, lng: 104.69113855849604 },
82
+ CO: { name: 'Colombia', lat: 4.187753877352739, lng: -72.6445066243485 },
83
+ CR: { name: 'Costa Rica', lat: 9.863467407406214, lng: -84.14673625701816 },
84
+ CU: { name: 'Cuba', lat: 21.476176522869448, lng: -79.69817857618705 },
85
+ CV: { name: 'Cabo Verde', lat: 15.076411518651643, lng: -23.63401005900474 },
86
+ CW: { name: 'Curacao', lat: 12.199996647939832, lng: -68.96939768599042 },
87
+ CX: { name: 'Christmas Island', lat: -10.446440802183416, lng: 105.70209512200549 },
88
+ CY: { name: 'Cyprus', lat: 35.11700416345239, lng: 33.375346009199205 },
89
+ CZ: { name: 'Czech Republic', lat: 49.74917370930982, lng: 15.383273292023533 },
90
+ DE: { name: 'Germany', lat: 51.08304539800482, lng: 10.426171427430804 },
91
+ DJ: { name: 'Djibouti', lat: 11.750235727618804, lng: 42.613496898789506 },
92
+ DK: { name: 'Denmark', lat: 56.00118817971057, lng: 9.378670542409406 },
93
+ DM: { name: 'Dominica', lat: 15.429269860940513, lng: -61.360471946942994 },
94
+ DO: { name: 'Dominican Republic', lat: 18.77954818522993, lng: -70.43495198520012 },
95
+ DZ: { name: 'Algeria', lat: 28.350969744889056, lng: 2.6558464719769135 },
96
+ EC: { name: 'Ecuador', lat: -1.5642721388853116, lng: -78.4630326109714 },
97
+ EE: { name: 'Estonia', lat: 58.648108311231034, lng: 25.916870250633806 },
98
+ EG: { name: 'Egypt', lat: 26.60517034450628, lng: 30.240135435012338 },
99
+ ER: { name: 'Eritrea', lat: 15.005533147667684, lng: 39.2672401449901 },
100
+ ES: { name: 'Spain', lat: 40.365008336683836, lng: -3.6516251409956983 },
101
+ ET: { name: 'Ethiopia', lat: 8.729389557048396, lng: 39.914902886544276 },
102
+ FI: { name: 'Finland', lat: 65.01578959749911, lng: 25.65738433454702 },
103
+ FJ: { name: 'Fiji', lat: -17.822470952336204, lng: 177.98144613732626 },
104
+ FK: { name: 'Falkland Islands', lat: -51.75901312766726, lng: -58.746646363799854 },
105
+ FM: { name: 'Micronesia', lat: 6.8789448129255435, lng: 158.2291899444527 },
106
+ FO: { name: 'Faroe Islands', lat: 62.130896281495346, lng: -6.9811060913122835 },
107
+ FR: { name: 'France', lat: 46.6423682169416, lng: 2.1940236627886227 },
108
+ GA: { name: 'Gabon', lat: -0.628448459921234, lng: 11.839410898545754 },
109
+ GB: { name: 'United Kingdom', lat: 53.97844735080214, lng: -2.852943909329258 },
110
+ GD: { name: 'Grenada', lat: 12.112926656338907, lng: -61.67937937204098 },
111
+ GE: { name: 'Georgia', lat: 42.17986277737226, lng: 43.378866534112234 },
112
+ GF: { name: 'French Guiana', lat: 3.857429742497078, lng: -53.32232307156624 },
113
+ GG: { name: 'Guernsey', lat: 49.45870771378872, lng: -2.576392582891568 },
114
+ GH: { name: 'Ghana', lat: 7.94530467243628, lng: -1.219233362526581 },
115
+ GI: { name: 'Gibraltar', lat: 36.14022671336082, lng: -5.345549484594358 },
116
+ GL: { name: 'Greenland', lat: 74.16847218965994, lng: -42.07567788066985 },
117
+ GM: { name: 'Gambia', lat: 13.428617959189328, lng: -15.383380385869662 },
118
+ GN: { name: 'Guinea', lat: 10.255986541378112, lng: -10.986948848040218 },
119
+ GP: { name: 'Guadeloupe', lat: 16.24420002705553, lng: -61.54382262282755 },
120
+ GQ: { name: 'Equatorial Guinea', lat: 1.5954643936590733, lng: 10.425456672353823 },
121
+ GR: { name: 'Greece', lat: 39.42012261727978, lng: 23.110368936161876 },
122
+ GS: { name: 'South Georgia and South Sandwich Islands', lat: -54.37666443862139, lng: -36.77509575898928 },
123
+ GT: { name: 'Guatemala', lat: 15.820878515352684, lng: -90.31219349119617 },
124
+ GU: { name: 'Guam', lat: 13.445430479945276, lng: 144.78024458298802 },
125
+ GW: { name: 'Guinea-Bissau', lat: 11.980075324820504, lng: -14.980186756910847 },
126
+ GY: { name: 'Guyana', lat: 4.68211981385056, lng: -58.91352612754667 },
127
+ HM: { name: 'Heard Island and McDonald Islands', lat: -53.084170035513736, lng: 73.49298560844045 },
128
+ HN: { name: 'Honduras', lat: 14.740370695750006, lng: -86.49251679006962 },
129
+ HR: { name: 'Croatia', lat: 44.91192100856702, lng: 16.625761129583374 },
130
+ HT: { name: 'Haiti', lat: 18.883520486983567, lng: -72.89291379842 },
131
+ HU: { name: 'Hungary', lat: 47.22527332486294, lng: 19.39620048366142 },
132
+ ID: { name: 'Indonesia', lat: 0.15591979959037652, lng: 113.96538246847302 },
133
+ IE: { name: 'Ireland', lat: 53.30489539816495, lng: -8.241128545554096 },
134
+ IL: { name: 'Israel', lat: 31.513542220043195, lng: 35.027923472437024 },
135
+ IM: { name: 'Isle of Man', lat: 54.22855301008011, lng: -4.532995055468449 },
136
+ IN: { name: 'India', lat: 23.586300567746722, lng: 81.17300408530181 },
137
+ IO: { name: 'British Indian Ocean Territory', lat: -7.323548444385743, lng: 72.43501618476016 },
138
+ IQ: { name: 'Iraq', lat: 33.105075667527906, lng: 43.832529181056884 },
139
+ IR: { name: 'Iran', lat: 32.906023742890056, lng: 54.237077001065444 },
140
+ IS: { name: 'Iceland', lat: 65.12360920205514, lng: -19.05682967106099 },
141
+ IT: { name: 'Italy', lat: 42.98201127614267, lng: 12.763657166123137 },
142
+ JE: { name: 'Jersey', lat: 49.215396925724306, lng: -2.1291601162653575 },
143
+ JM: { name: 'Jamaica', lat: 18.12207889341651, lng: -77.30358894542778 },
144
+ JO: { name: 'Jordan', lat: 31.387064884449156, lng: 36.95728884547246 },
145
+ JP: { name: 'Japan', lat: 36.76738832597829, lng: 137.46934234351835 },
146
+ KE: { name: 'Kenya', lat: 0.6899182318376179, lng: 37.95309411262371 },
147
+ KG: { name: 'Kyrgyzstan', lat: 41.35698905438358, lng: 74.17532903468319 },
148
+ KH: { name: 'Cambodia', lat: 12.699186865507775, lng: 105.03973078680701 },
149
+ KI: { name: 'Kiribati', lat: 1.8676673749241066, lng: -157.39024189323504 },
150
+ KM: { name: 'Comoros', lat: -11.658861474508491, lng: 43.34826587429403 },
151
+ KN: { name: 'Saint Kitts and Nevis', lat: 17.314736299587768, lng: -62.74560385895787 },
152
+ KP: { name: 'North Korea', lat: 40.19198091470839, lng: 127.3379805653744 },
153
+ KR: { name: 'South Korea', lat: 36.402386712544114, lng: 127.76224551357649 },
154
+ KW: { name: 'Kuwait', lat: 29.281360965443092, lng: 47.56311109320184 },
155
+ KY: { name: 'Cayman Islands', lat: 19.311231805620288, lng: -81.25203208977878 },
156
+ KZ: { name: 'Kazakhstan', lat: 47.641465195176835, lng: 66.3759193479301 },
157
+ LA: { name: 'Laos', lat: 18.117282736873282, lng: 103.76375899026448 },
158
+ LB: { name: 'Lebanon', lat: 33.91160170722086, lng: 35.89651946324749 },
159
+ LC: { name: 'Saint Lucia', lat: 13.895749185129906, lng: -60.9689510935251 },
160
+ LI: { name: 'Liechtenstein', lat: 47.14627562133036, lng: 9.547674672376376 },
161
+ LK: { name: 'Sri Lanka', lat: 7.696630939329944, lng: 80.66931169770622 },
162
+ LR: { name: 'Liberia', lat: 6.52012979398834, lng: -9.258988935497618 },
163
+ LS: { name: 'Lesotho', lat: -29.60168116924817, lng: 28.24475317864227 },
164
+ LT: { name: 'Lithuania', lat: 55.29437393417175, lng: 23.946021605013534 },
165
+ LU: { name: 'Luxembourg', lat: 49.77523454542369, lng: 6.103230338458876 },
166
+ LV: { name: 'Latvia', lat: 56.813853047554154, lng: 24.693671325654403 },
167
+ LY: { name: 'Libya', lat: 27.202915771690794, lng: 17.91133392454237 },
168
+ MA: { name: 'Morocco', lat: 28.687598134979325, lng: -8.817212587250811 },
169
+ MC: { name: 'Monaco', lat: 43.74798224995656, lng: 7.412820873271196 },
170
+ MD: { name: 'Moldova', lat: 47.0725674580696, lng: 28.391111865941348 },
171
+ ME: { name: 'Montenegro', lat: 42.73694835210454, lng: 19.29505087156758 },
172
+ MF: { name: 'Saint Martin', lat: 18.078012113224464, lng: -63.06678525361946 },
173
+ MG: { name: 'Madagascar', lat: -19.04163612493041, lng: 46.68493466832544 },
174
+ MH: { name: 'Marshall Islands', lat: 7.307929900994344, lng: 168.72016025351076 },
175
+ MK: { name: 'North Macedonia', lat: 41.59402890143112, lng: 21.70998923872772 },
176
+ ML: { name: 'Mali', lat: 17.168146208584837, lng: -4.346399841781153 },
177
+ MM: { name: 'Myanmar', lat: 19.901227931399873, lng: 97.08892285397344 },
178
+ MN: { name: 'Mongolia', lat: 47.08644454604851, lng: 103.3987360327455 },
179
+ MP: { name: 'Northern Mariana Islands', lat: 15.178063516432115, lng: 145.74119737203134 },
180
+ MQ: { name: 'Martinique', lat: 14.642697353597645, lng: -61.01432380875083 },
181
+ MR: { name: 'Mauritania', lat: 20.466731212820022, lng: -10.495079045035716 },
182
+ MS: { name: 'Montserrat', lat: 16.735363391460357, lng: -62.18693281256255 },
183
+ MT: { name: 'Malta', lat: 35.890522650899314, lng: 14.441922442508494 },
184
+ MU: { name: 'Mauritius', lat: -20.28142317475198, lng: 57.56415671066546 },
185
+ MV: { name: 'Maldives', lat: -0.6065577168009924, lng: 73.10076245140479 },
186
+ MW: { name: 'Malawi', lat: -13.128986464184024, lng: 34.23441182298881 },
187
+ MX: { name: 'Mexico', lat: 23.87436068093592, lng: -101.55399731630118 },
188
+ MY: { name: 'Malaysia', lat: 3.6716608556387857, lng: 114.63330303992869 },
189
+ MZ: { name: 'Mozambique', lat: -17.525230309488748, lng: 35.208577031290176 },
190
+ NA: { name: 'Namibia', lat: -21.90858163281473, lng: 18.16451345845268 },
191
+ NC: { name: 'New Caledonia', lat: -21.33003372660827, lng: 165.50767040438612 },
192
+ NE: { name: 'Niger', lat: 17.08105392407292, lng: 8.86863247002646 },
193
+ NF: { name: 'Norfolk Island', lat: -29.037654445046282, lng: 167.95259597483337 },
194
+ NG: { name: 'Nigeria', lat: 9.61029352034213, lng: 8.147714845256194 },
195
+ NI: { name: 'Nicaragua', lat: 12.893566631930554, lng: -85.016088327669 },
196
+ NL: { name: 'Netherlands', lat: 52.134054128923886, lng: 5.554136426128487 },
197
+ NO: { name: 'Norway', lat: 64.97775882947745, lng: 16.670259272390894 },
198
+ NP: { name: 'Nepal', lat: 28.300920699755657, lng: 84.1338898313567 },
199
+ NR: { name: 'Nauru', lat: -0.5221021440668993, lng: 166.92937633139178 },
200
+ NU: { name: 'Niue', lat: -19.05230921680393, lng: -169.86878106699083 },
201
+ NZ: { name: 'New Zealand', lat: -43.82765432544426, lng: 170.69035541428696 },
202
+ OM: { name: 'Oman', lat: 20.7242833183209, lng: 55.841088119829 },
203
+ PA: { name: 'Panama', lat: 8.439536749576892, lng: -80.14428761482796 },
204
+ PE: { name: 'Peru', lat: -8.522717984240291, lng: -74.11416196781884 },
205
+ PF: { name: 'French Polynesia', lat: -17.674684080120677, lng: -149.40041671099763 },
206
+ PG: { name: 'Papua New Guinea', lat: -7.156912819152135, lng: 144.8348942994695 },
207
+ PH: { name: 'Philippines', lat: 15.586542251094242, lng: 121.82208941416745 },
208
+ PK: { name: 'Pakistan', lat: 30.116188371410882, lng: 69.08835090769651 },
209
+ PL: { name: 'Poland', lat: 52.06848055692473, lng: 19.43573279234468 },
210
+ PM: { name: 'Saint Pierre and Miquelon', lat: 46.95153913615198, lng: -56.32465353437558 },
211
+ PN: { name: 'Pitcairn', lat: -24.366121747565458, lng: -128.3149848627581 },
212
+ PR: { name: 'Puerto Rico', lat: 18.216224086610914, lng: -66.49425339593509 },
213
+ PS: { name: 'Palestinian Territory', lat: 31.930818736453883, lng: 35.24251184154588 },
214
+ PT: { name: 'Portugal', lat: 39.67529214702138, lng: -7.933662183874006 },
215
+ PW: { name: 'Palau', lat: 7.534775852547396, lng: 134.57965086721052 },
216
+ PY: { name: 'Paraguay', lat: -23.42190559259428, lng: -58.38906357428651 },
217
+ QA: { name: 'Qatar', lat: 25.318528486425386, lng: 51.19794918743203 },
218
+ RE: { name: 'Réunion', lat: -21.119825460665105, lng: 55.54393506194689 },
219
+ RO: { name: 'Romania', lat: 45.82454894397586, lng: 25.094158201563292 },
220
+ RS: { name: 'Serbia', lat: 44.02679870131969, lng: 20.85677444395745 },
221
+ RU: { name: 'Russian Federation', lat: 59.039434214106194, lng: 98.6704990698032 },
222
+ RW: { name: 'Rwanda', lat: -2.014687460047154, lng: 29.919439681764082 },
223
+ SA: { name: 'Saudi Arabia', lat: 24.136038144757897, lng: 44.600958178225596 },
224
+ SB: { name: 'Solomon Islands', lat: -9.613104734596515, lng: 160.16475795033884 },
225
+ SC: { name: 'Seychelles', lat: -4.660002318822744, lng: 55.47250789595527 },
226
+ SD: { name: 'Sudan', lat: 15.67060230984256, lng: 29.951458283594064 },
227
+ SE: { name: 'Sweden', lat: 62.73420986108448, lng: 17.062431988004956 },
228
+ SG: { name: 'Singapore', lat: 1.3528251890006349, lng: 103.81025757634053 },
229
+ SH: { name: 'Saint Helena', lat: -15.962963318340398, lng: -5.717391620813109 },
230
+ SI: { name: 'Slovenia', lat: 46.13759229564504, lng: 14.890636899973781 },
231
+ SJ: { name: 'Svalbard', lat: 78.57318936469626, lng: 16.036378851505283 },
232
+ SK: { name: 'Slovakia', lat: 48.69808390520484, lng: 19.581015362490966 },
233
+ SL: { name: 'Sierra Leone', lat: 8.561330384750587, lng: -11.78656695731115 },
234
+ SM: { name: 'San Marino', lat: 43.942820729097896, lng: 12.461278349581722 },
235
+ SN: { name: 'Senegal', lat: 14.228861491763402, lng: -14.610875368352305 },
236
+ SO: { name: 'Somalia', lat: 6.524534573103924, lng: 45.40037867243972 },
237
+ SR: { name: 'Suriname', lat: 4.098723595920171, lng: -55.855514311561286 },
238
+ SS: { name: 'South Sudan', lat: 7.657782041763295, lng: 30.3851856901788 },
239
+ ST: { name: 'Sao Tome and Principe', lat: 0.22744704294793774, lng: 6.606158796857607 },
240
+ SV: { name: 'El Salvador', lat: 13.758041517055418, lng: -88.85911489238985 },
241
+ SX: { name: 'Sint Maarten', lat: 18.03942608463078, lng: -63.06883135915303 },
242
+ SY: { name: 'Syria', lat: 35.09751106058316, lng: 38.5117323139514 },
243
+ SZ: { name: 'Eswatini', lat: -26.562540935608702, lng: 31.510685746082007 },
244
+ TC: { name: 'Turks and Caicos Islands', lat: 21.799865427483745, lng: -71.74058946811508 },
245
+ TD: { name: 'Chad', lat: 15.283493546654503, lng: 18.427113900363025 },
246
+ TF: { name: 'French Southern Territories', lat: -49.26329687105643, lng: 69.54686984724839 },
247
+ TG: { name: 'Togo', lat: 8.660743037717811, lng: 0.8990857571109684 },
248
+ TH: { name: 'Thailand', lat: 13.66222784745538, lng: 101.08675116214552 },
249
+ TJ: { name: 'Tajikistan', lat: 38.56933138382972, lng: 70.94215281065698 },
250
+ TK: { name: 'Tokelau', lat: -9.195174767256544, lng: -171.85265950722743 },
251
+ TL: { name: 'Timor-Leste', lat: -8.809894630601962, lng: 125.95024049562659 },
252
+ TM: { name: 'Turkmenistan', lat: 39.06069118001429, lng: 58.4577357627054 },
253
+ TN: { name: 'Tunisia', lat: 34.08636179565723, lng: 9.65587551697984 },
254
+ TO: { name: 'Tonga', lat: -21.15927212823853, lng: -175.20415878511247 },
255
+ TR: { name: 'Turkey', lat: 38.93207363123396, lng: 35.56886764076691 },
256
+ TT: { name: 'Trinidad and Tobago', lat: 10.415515638298093, lng: -61.37236579976247 },
257
+ TV: { name: 'Tuvalu', lat: -8.514701506447222, lng: 179.217833977593 },
258
+ TZ: { name: 'Tanzania', lat: -6.355794440041147, lng: 34.81832206060381 },
259
+ UA: { name: 'Ukraine', lat: 48.657532515563794, lng: 31.27377208442636 },
260
+ UG: { name: 'Uganda', lat: 1.2821729218416205, lng: 32.34371768463123 },
261
+ UM: { name: 'United States Minor Outlying Islands', lat: 19.302045812215958, lng: 166.63800339749642 },
262
+ US: { name: 'United States', lat: 38.8208089190304, lng: -96.33161660829639 },
263
+ UY: { name: 'Uruguay', lat: -32.78195043831093, lng: -56.01919523458085 },
264
+ UZ: { name: 'Uzbekistan', lat: 41.4879065244633, lng: 63.8548297593985 },
265
+ VA: { name: 'Vatican City', lat: 41.90402351316735, lng: 12.451312917026133 },
266
+ VC: { name: 'Saint Vincent and the Grenadines', lat: 13.254808122970651, lng: -61.193766354393034 },
267
+ VE: { name: 'Venezuela', lat: 7.148324760507107, lng: -66.36492135985132 },
268
+ VG: { name: 'British Virgin Islands', lat: 18.42195819619707, lng: -64.62406519257699 },
269
+ VI: { name: 'US Virgin Islands', lat: 17.738009708772523, lng: -64.76155341409797 },
270
+ VN: { name: 'Vietnam', lat: 16.517347170839393, lng: 105.91338832758704 },
271
+ VU: { name: 'Vanuatu', lat: -15.189132121699332, lng: 166.84912735669738 },
272
+ WF: { name: 'Wallis and Futuna', lat: -14.283442307826677, lng: -178.12735555777184 },
273
+ WS: { name: 'Samoa', lat: -13.634252953274622, lng: -172.44107655740137 },
274
+ YE: { name: 'Yemen', lat: 16.001392616307445, lng: 47.46815793206386 },
275
+ YT: { name: 'Mayotte', lat: -12.824468416301052, lng: 45.128142327031064 },
276
+ ZA: { name: 'South Africa', lat: -28.55361930679731, lng: 24.75252746489084 },
277
+ ZM: { name: 'Zambia', lat: -13.162832953186246, lng: 27.75521363430896 },
278
+ ZW: { name: 'Zimbabwe', lat: -18.92700121981475, lng: 29.717829640720844 },
279
+ };
280
+
281
+ /**
282
+ * ISO2 (any case) → centroid, or `null` when the code is unknown. Unlike the
283
+ * source this never throws on a non-string; callers feed it wire data.
284
+ */
285
+ export function getCountryCentroid(code: string | null | undefined): CountryCentroid | null {
286
+ if (!code) return null;
287
+ return COUNTRY_CENTROIDS[String(code).toUpperCase()] ?? null;
288
+ }
289
+
290
+ /** ISO2 → display name. Unknown codes pass through UPPERCASED, never blank. */
291
+ export function countryName(code: string | null | undefined): string {
292
+ if (!code) return '';
293
+ const upper = String(code).toUpperCase();
294
+ return COUNTRY_CENTROIDS[upper]?.name ?? upper;
295
+ }
296
+
297
+ /**
298
+ * `{value, label}` options for country pickers, sorted by `localeCompare` on
299
+ * the label — the exact derivation web-mojo's GeofenceRuleForm.js:13-17 did
300
+ * inline, promoted to a toolkit export so the geofence UI has one source.
301
+ */
302
+ export const COUNTRY_OPTIONS: readonly { value: string; label: string }[] = Object.entries(COUNTRY_CENTROIDS)
303
+ .map(([value, info]) => ({ value, label: info.name }))
304
+ .sort((a, b) => a.label.localeCompare(b.label));
@@ -0,0 +1,293 @@
1
+ // geo.ts — the WorldMap's projection math. Pure functions, no React, no DOM:
2
+ // everything here is headless-testable (scripts/verify-worldmap.mjs) and is
3
+ // the piece a future tile-backed map would reuse unchanged.
4
+ //
5
+ // Equirectangular (plate carrée) is the projection web-mojo effectively
6
+ // showed at zoom 1.3 over a demo tile server, and it is the only sane choice
7
+ // for a graticule fallback: lat/lng map to y/x linearly, so the inverse is
8
+ // exact and a "no geometry shipped" map still tells the truth about where a
9
+ // point is.
10
+ //
11
+ // Coordinate validity is `Number.isFinite`, never truthiness — web-mojo's
12
+ // `if (!lng || !lat) return` (MapLibreView.js:223) dropped legitimate points
13
+ // on the equator and the prime meridian.
14
+
15
+ /** A geographic point. `lat` is degrees north, `lng` degrees east. */
16
+ export interface LatLng {
17
+ lat: number;
18
+ lng: number;
19
+ }
20
+
21
+ /** A geographic crop. All four values are degrees. */
22
+ export interface GeoBounds {
23
+ west: number;
24
+ east: number;
25
+ south: number;
26
+ north: number;
27
+ }
28
+
29
+ /**
30
+ * The viewport: which point sits at the center of the frame, and how far in.
31
+ * `zoom` 1 fits the whole `bounds` box; `MAX_ZOOM` is the hard ceiling.
32
+ */
33
+ export interface WorldMapView {
34
+ lat: number;
35
+ lng: number;
36
+ zoom: number;
37
+ }
38
+
39
+ /** Pixel point in the SVG's own coordinate space (origin top-left). */
40
+ export interface PixelPoint {
41
+ x: number;
42
+ y: number;
43
+ }
44
+
45
+ export interface PixelSize {
46
+ width: number;
47
+ height: number;
48
+ }
49
+
50
+ /**
51
+ * The default crop: the whole world minus the polar caps nothing plots in.
52
+ * web-mojo approximated the same framing with `center [10, 20], zoom 1.3` on
53
+ * a Mercator basemap. Antarctica (lat −77) sits OUTSIDE this box on purpose —
54
+ * it is counted as off-bounds, not clamped to the edge; widen `bounds` if a
55
+ * consumer really needs it.
56
+ */
57
+ export const DEFAULT_BOUNDS: GeoBounds = { west: -180, east: 180, south: -58, north: 84 };
58
+
59
+ export const MIN_ZOOM = 1;
60
+ export const MAX_ZOOM = 12;
61
+
62
+ /** The whole-world view: the center of DEFAULT_BOUNDS at zoom 1. */
63
+ export const DEFAULT_VIEW: WorldMapView = { lat: 13, lng: 0, zoom: 1 };
64
+
65
+ /** True when both coordinates are real numbers. `{lat: 0, lng: 0}` passes. */
66
+ export function isFinitePoint(p: LatLng | null | undefined): p is LatLng {
67
+ return !!p && Number.isFinite(p.lat) && Number.isFinite(p.lng);
68
+ }
69
+
70
+ const clamp = (v: number, lo: number, hi: number): number => Math.min(hi, Math.max(lo, v));
71
+
72
+ /** A projection bound to one container size, crop and viewport. */
73
+ export interface Projection {
74
+ size: PixelSize;
75
+ bounds: GeoBounds;
76
+ view: WorldMapView;
77
+ /** Pixels per degree. Uniform in x and y — circles stay circles. */
78
+ scale: number;
79
+ project(p: LatLng): PixelPoint;
80
+ invert(pt: PixelPoint): LatLng;
81
+ /** True when the projected point falls inside the drawn frame. */
82
+ contains(p: LatLng): boolean;
83
+ }
84
+
85
+ export interface ProjectionOptions {
86
+ width: number;
87
+ height: number;
88
+ bounds?: GeoBounds;
89
+ view?: WorldMapView;
90
+ }
91
+
92
+ /**
93
+ * Build an equirectangular projection.
94
+ *
95
+ * At `zoom: 1` the whole `bounds` box fits the frame ("contain"): the scale
96
+ * is `min(width/spanLng, height/spanLat)` in BOTH axes, so a container whose
97
+ * aspect does not match the crop letterboxes rather than stretching. A
98
+ * non-uniform fit would turn every marker into an ellipse and every distance
99
+ * into a lie, which is the opposite of what a fallback map is for.
100
+ */
101
+ export function equirectangular({
102
+ width,
103
+ height,
104
+ bounds = DEFAULT_BOUNDS,
105
+ view = DEFAULT_VIEW,
106
+ }: ProjectionOptions): Projection {
107
+ const w = Math.max(1, width);
108
+ const h = Math.max(1, height);
109
+ const spanLng = Math.max(1e-9, bounds.east - bounds.west);
110
+ const spanLat = Math.max(1e-9, bounds.north - bounds.south);
111
+ const base = Math.min(w / spanLng, h / spanLat);
112
+ const zoom = clamp(Number.isFinite(view.zoom) ? view.zoom : 1, MIN_ZOOM, MAX_ZOOM);
113
+ const scale = base * zoom;
114
+ const cx = w / 2;
115
+ const cy = h / 2;
116
+ const centerLat = Number.isFinite(view.lat) ? view.lat : DEFAULT_VIEW.lat;
117
+ const centerLng = Number.isFinite(view.lng) ? view.lng : DEFAULT_VIEW.lng;
118
+
119
+ const project = (p: LatLng): PixelPoint => ({
120
+ x: cx + (p.lng - centerLng) * scale,
121
+ y: cy - (p.lat - centerLat) * scale,
122
+ });
123
+
124
+ return {
125
+ size: { width: w, height: h },
126
+ bounds,
127
+ view: { lat: centerLat, lng: centerLng, zoom },
128
+ scale,
129
+ project,
130
+ invert: (pt: PixelPoint): LatLng => ({
131
+ lat: centerLat - (pt.y - cy) / scale,
132
+ lng: centerLng + (pt.x - cx) / scale,
133
+ }),
134
+ contains: (p: LatLng): boolean => {
135
+ if (!isFinitePoint(p)) return false;
136
+ const { x, y } = project(p);
137
+ return x >= 0 && x <= w && y >= 0 && y <= h;
138
+ },
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Clamp a viewport so the zoom stays in range and the map can never be
144
+ * dragged out of the frame: on each axis, either the whole crop is visible
145
+ * (then center on it) or the visible window is pinned inside the crop.
146
+ */
147
+ export function clampView(view: WorldMapView, bounds: GeoBounds, size: PixelSize): WorldMapView {
148
+ const spanLng = Math.max(1e-9, bounds.east - bounds.west);
149
+ const spanLat = Math.max(1e-9, bounds.north - bounds.south);
150
+ const w = Math.max(1, size.width);
151
+ const h = Math.max(1, size.height);
152
+ const base = Math.min(w / spanLng, h / spanLat);
153
+ const zoom = clamp(Number.isFinite(view.zoom) ? view.zoom : 1, MIN_ZOOM, MAX_ZOOM);
154
+ const scale = base * zoom;
155
+
156
+ const halfLng = w / 2 / scale;
157
+ const halfLat = h / 2 / scale;
158
+ const midLng = (bounds.west + bounds.east) / 2;
159
+ const midLat = (bounds.south + bounds.north) / 2;
160
+
161
+ const lng = halfLng * 2 >= spanLng
162
+ ? midLng
163
+ : clamp(Number.isFinite(view.lng) ? view.lng : midLng, bounds.west + halfLng, bounds.east - halfLng);
164
+ const lat = halfLat * 2 >= spanLat
165
+ ? midLat
166
+ : clamp(Number.isFinite(view.lat) ? view.lat : midLat, bounds.south + halfLat, bounds.north - halfLat);
167
+
168
+ return { lat, lng, zoom };
169
+ }
170
+
171
+ export interface FitOptions {
172
+ size: PixelSize;
173
+ bounds?: GeoBounds;
174
+ /** Breathing room around the fitted box, in px. Source parity: 50. */
175
+ padding?: number;
176
+ /** Source parity: MapLibre's `fitBounds({maxZoom: 15})`, clamped here. */
177
+ maxZoom?: number;
178
+ /** Returned when there is nothing to fit; its zoom is kept for one point. */
179
+ fallback?: WorldMapView;
180
+ }
181
+
182
+ /**
183
+ * The `fitBounds()` replacement (MapLibreView.js:261-274) — used by the
184
+ * drill-down flow. Zero points returns `fallback` untouched; ONE point
185
+ * re-centers but KEEPS the current zoom, because a single-point bounding box
186
+ * has zero span and "fit" would divide by zero (MapLibre answered that case
187
+ * with a `flyTo`, same idea).
188
+ */
189
+ export function fitViewToPoints(points: readonly LatLng[], opts: FitOptions): WorldMapView {
190
+ const bounds = opts.bounds ?? DEFAULT_BOUNDS;
191
+ const fallback = opts.fallback ?? DEFAULT_VIEW;
192
+ const padding = opts.padding ?? 50;
193
+ const maxZoom = clamp(opts.maxZoom ?? MAX_ZOOM, MIN_ZOOM, MAX_ZOOM);
194
+ const usable = (points ?? []).filter(isFinitePoint);
195
+ if (!usable.length) return fallback;
196
+
197
+ let west = Infinity;
198
+ let east = -Infinity;
199
+ let south = Infinity;
200
+ let north = -Infinity;
201
+ for (const p of usable) {
202
+ west = Math.min(west, p.lng);
203
+ east = Math.max(east, p.lng);
204
+ south = Math.min(south, p.lat);
205
+ north = Math.max(north, p.lat);
206
+ }
207
+ const center = { lat: (south + north) / 2, lng: (west + east) / 2 };
208
+
209
+ const spanLng = east - west;
210
+ const spanLat = north - south;
211
+ if (spanLng <= 0 && spanLat <= 0) {
212
+ return clampView({ ...center, zoom: fallback.zoom }, bounds, opts.size);
213
+ }
214
+
215
+ const w = Math.max(1, opts.size.width);
216
+ const h = Math.max(1, opts.size.height);
217
+ const base = Math.min(w / Math.max(1e-9, bounds.east - bounds.west), h / Math.max(1e-9, bounds.north - bounds.south));
218
+ const usableW = Math.max(1, w - padding * 2);
219
+ const usableH = Math.max(1, h - padding * 2);
220
+ const needed = Math.min(
221
+ spanLng > 0 ? usableW / spanLng : Infinity,
222
+ spanLat > 0 ? usableH / spanLat : Infinity,
223
+ );
224
+ const zoom = clamp(needed / base, MIN_ZOOM, maxZoom);
225
+ return clampView({ ...center, zoom }, bounds, opts.size);
226
+ }
227
+
228
+ /**
229
+ * Split a great-circle-ish segment at the antimeridian.
230
+ *
231
+ * On an equirectangular map a Tokyo→Los Angeles line drawn naively runs
232
+ * BACKWARDS across the entire world. When the shorter path wraps past ±180°
233
+ * this returns two segments — one to the edge, one from the other edge —
234
+ * with the latitude linearly interpolated at the crossing. Otherwise it
235
+ * returns the single original segment.
236
+ */
237
+ export function splitAntimeridian(from: LatLng, to: LatLng): [LatLng, LatLng][] {
238
+ if (!isFinitePoint(from) || !isFinitePoint(to)) return [];
239
+ const dLng = to.lng - from.lng;
240
+ if (Math.abs(dLng) <= 180) return [[from, to]];
241
+
242
+ // The short way wraps: restate `to` on the continuous side of `from`.
243
+ const wrappedLng = dLng > 0 ? to.lng - 360 : to.lng + 360;
244
+ const edge = dLng > 0 ? -180 : 180;
245
+ const t = (edge - from.lng) / (wrappedLng - from.lng);
246
+ const lat = from.lat + (to.lat - from.lat) * t;
247
+ return [
248
+ [from, { lat, lng: edge }],
249
+ [{ lat, lng: -edge }, to],
250
+ ];
251
+ }
252
+
253
+ /**
254
+ * A quadratic-bezier arc between two projected points. The control point sits
255
+ * perpendicular to the chord, offset proportionally to the chord's length, so
256
+ * short hops stay nearly straight and long ones bow. `curvature: 0` degrades
257
+ * to a straight line (the source's flat LineStrings).
258
+ *
259
+ * Returns '' for a zero-length or non-finite pair — the caller skips it
260
+ * rather than emitting `M NaN NaN`.
261
+ *
262
+ * Named `geoArcPath`, not `arcPath`: `charts/pie-math` already exports an
263
+ * `arcPath` (pie/doughnut segments) and both live under `portal-mojo/charts`.
264
+ */
265
+ export function geoArcPath(projection: Projection, from: LatLng, to: LatLng, curvature = 0.22): string {
266
+ if (!isFinitePoint(from) || !isFinitePoint(to)) return '';
267
+ const a = projection.project(from);
268
+ const b = projection.project(to);
269
+ const dx = b.x - a.x;
270
+ const dy = b.y - a.y;
271
+ const len = Math.hypot(dx, dy);
272
+ if (!(len > 0.5)) return '';
273
+ const off = len * curvature;
274
+ // Unit normal, always to the LEFT of a→b, so parallel routes fan the
275
+ // same way instead of crossing each other.
276
+ const cxp = (a.x + b.x) / 2 + (-dy / len) * off;
277
+ const cyp = (a.y + b.y) / 2 + (dx / len) * off;
278
+ return `M${a.x.toFixed(2)},${a.y.toFixed(2)} Q${cxp.toFixed(2)},${cyp.toFixed(2)} ${b.x.toFixed(2)},${b.y.toFixed(2)}`;
279
+ }
280
+
281
+ /**
282
+ * Graticule line positions inside `bounds`. Returns the meridians and
283
+ * parallels the fallback basemap draws; `0` (equator / prime meridian) is
284
+ * flagged so the component can stroke it with the stronger token.
285
+ */
286
+ export function graticule(bounds: GeoBounds, lngStep = 30, latStep = 15) {
287
+ const meridians: number[] = [];
288
+ const parallels: number[] = [];
289
+ const first = (from: number, step: number) => Math.ceil(from / step) * step;
290
+ for (let lng = first(bounds.west, lngStep); lng <= bounds.east; lng += lngStep) meridians.push(lng);
291
+ for (let lat = first(bounds.south, latStep); lat <= bounds.north; lat += latStep) parallels.push(lat);
292
+ return { meridians, parallels };
293
+ }
@@ -0,0 +1,47 @@
1
+ import { useEffect, useState } from 'react';
2
+ import type { GeoLandCollection } from './worldmap-data';
3
+
4
+ /**
5
+ * Lazily load the shipped basemap geometry.
6
+ *
7
+ * `world-land.ts` is ~143KB of coordinates (~40KB gzipped). Importing it
8
+ * statically puts it in whatever chunk touches `portal-mojo/charts`, which in
9
+ * this app is the main bundle — so every page would pay for it, including the
10
+ * many with no map on them. The dynamic `import()` here is what actually makes
11
+ * Rollup emit it as its own chunk, fetched only when a map first renders.
12
+ *
13
+ * The module-level cache means the second and later maps in a session get the
14
+ * geometry synchronously on first render, and concurrent mounts share one
15
+ * request rather than racing.
16
+ *
17
+ * Returns `null` until it resolves, which is not a special case: `WorldMap`
18
+ * treats a null `land` as "draw the ocean + graticule fallback", so a map is
19
+ * fully readable — markers, legend, tooltips, status — before the coastlines
20
+ * arrive, and simply gains them a moment later.
21
+ */
22
+ let cached: GeoLandCollection | null = null;
23
+ let inflight: Promise<GeoLandCollection> | null = null;
24
+
25
+ export function useWorldLand(enabled = true): GeoLandCollection | null {
26
+ const [land, setLand] = useState<GeoLandCollection | null>(cached);
27
+
28
+ useEffect(() => {
29
+ if (!enabled || cached) return;
30
+ let alive = true;
31
+ inflight ??= import('./world-land').then((mod) => {
32
+ cached = mod.WORLD_LAND;
33
+ return cached;
34
+ });
35
+ inflight
36
+ .then((loaded) => { if (alive) setLand(loaded); })
37
+ .catch((err) => {
38
+ // A failed chunk fetch must not blank the map: the fallback is a
39
+ // legitimate rendering, so warn and carry on rather than throw.
40
+ inflight = null;
41
+ console.warn('WorldMap: basemap geometry failed to load; drawing the graticule fallback', err);
42
+ });
43
+ return () => { alive = false; };
44
+ }, [enabled]);
45
+
46
+ return enabled ? land : null;
47
+ }