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,267 @@
1
+ // TimezoneSelect — the searchable IANA zone picker. Ported from web-mojo
2
+ // src/core/forms/inputs/TimezoneSelect.js (196 lines, read in full).
3
+ //
4
+ // A thin layer over the house <ComboBox>: zones from
5
+ // Intl.supportedValuesOf('timeZone') (curated fallback when the engine has
6
+ // no such API), each labelled with its CURRENT UTC offset —
7
+ // "America/New_York (UTC−05:00)", Unicode minus, zero-padded. Embedded by
8
+ // the time pickers (#1269 / DateTimePicker) and usable as its own field.
9
+ //
10
+ // Fixed here BY CONSTRUCTION (web-mojo bugs, do not reintroduce):
11
+ // - Commit-only change. web-mojo's ComboBox emitted `change` per keystroke,
12
+ // so this picker leaked typed filter fragments ("Amer") into its value and
13
+ // FormView's 300ms autosave saved them. Our ComboBox commits on selection
14
+ // or settle only, and `allowCustom` is false here — a value can ONLY ever
15
+ // come out of the list.
16
+ // - Offsets recompute on every open gesture. web-mojo built the labels once
17
+ // in onAfterRender, so a long-lived picker kept showing pre-DST offsets.
18
+ // - The committed zone is always an option. V8's supportedValuesOf returns
19
+ // ICU-canonical ids ('Asia/Calcutta', 'Europe/Kiev', 'America/Godthab'),
20
+ // while Python/django-mojo stores the modern IANA primaries
21
+ // ('Asia/Kolkata', 'Europe/Kyiv', 'America/Nuuk'). Without this, a stored
22
+ // zone was unlabelled, unfindable and un-re-selectable — the user had to
23
+ // change it to save anything. Engines ACCEPT the aliases as input, so the
24
+ // offset label works for them.
25
+ import { useMemo, useRef, useState } from 'react';
26
+ import { ComboBox, type ComboOption } from '../ComboBox';
27
+ import { ianaOffset } from './fns';
28
+
29
+ /**
30
+ * The curated fallback list, verbatim from web-mojo (order included). Only
31
+ * ever used when the engine has no `Intl.supportedValuesOf`.
32
+ *
33
+ * Carried as-is on purpose, warts included: `America/Honolulu` is not a real
34
+ * IANA zone (the real one, `Pacific/Honolulu`, is also in the list). It
35
+ * labels as a bare name — `ianaOffset` returns null for zones the engine
36
+ * rejects — rather than throwing.
37
+ */
38
+ export const FALLBACK_TIMEZONES: readonly string[] = [
39
+ 'UTC', 'GMT',
40
+ 'America/New_York', 'America/Chicago', 'America/Denver', 'America/Los_Angeles',
41
+ 'America/Anchorage', 'America/Honolulu', 'America/Phoenix',
42
+ 'America/Toronto', 'America/Vancouver', 'America/Mexico_City',
43
+ 'America/Sao_Paulo', 'America/Buenos_Aires', 'America/Bogota',
44
+ 'Europe/London', 'Europe/Dublin', 'Europe/Paris', 'Europe/Berlin',
45
+ 'Europe/Madrid', 'Europe/Rome', 'Europe/Amsterdam', 'Europe/Stockholm',
46
+ 'Europe/Athens', 'Europe/Moscow', 'Europe/Istanbul',
47
+ 'Africa/Cairo', 'Africa/Johannesburg', 'Africa/Lagos', 'Africa/Nairobi',
48
+ 'Asia/Dubai', 'Asia/Tehran', 'Asia/Karachi', 'Asia/Kolkata',
49
+ 'Asia/Bangkok', 'Asia/Singapore', 'Asia/Hong_Kong', 'Asia/Shanghai',
50
+ 'Asia/Tokyo', 'Asia/Seoul', 'Asia/Jakarta', 'Asia/Manila',
51
+ 'Australia/Perth', 'Australia/Sydney', 'Australia/Melbourne',
52
+ 'Pacific/Auckland', 'Pacific/Fiji', 'Pacific/Honolulu',
53
+ ];
54
+
55
+ /** U+2212 — the typographic minus the offset labels use for negatives. */
56
+ const MINUS = '−';
57
+
58
+ /** One open is several DOM events (pointerdown → focus, or keydown → focus);
59
+ relabelling once per second of gestures covers them all and is orders of
60
+ magnitude finer than any DST transition. */
61
+ const OPEN_COALESCE_MS = 1000;
62
+
63
+ let warnedNoIntl = false;
64
+ let cachedEngineZones: string[] | null = null;
65
+
66
+ /**
67
+ * The engine's zone list, resolved once per session. Falls back to
68
+ * FALLBACK_TIMEZONES with ONE module-level warn (never per instance, never
69
+ * per render) when `Intl.supportedValuesOf` is missing or refuses 'timeZone'.
70
+ */
71
+ function engineZones(): string[] {
72
+ if (cachedEngineZones) return cachedEngineZones;
73
+ let zones: string[] | null = null;
74
+ try {
75
+ if (typeof Intl.supportedValuesOf === 'function') {
76
+ const supported = Intl.supportedValuesOf('timeZone');
77
+ if (Array.isArray(supported) && supported.length) zones = supported.slice();
78
+ }
79
+ } catch {
80
+ // Engine has the function but not the key — fall through to curated.
81
+ }
82
+ if (!zones) {
83
+ if (!warnedNoIntl) {
84
+ warnedNoIntl = true;
85
+ console.warn('TimezoneSelect: Intl.supportedValuesOf("timeZone") unavailable — falling back to the curated zone list.');
86
+ }
87
+ zones = FALLBACK_TIMEZONES.slice();
88
+ } else if (!zones.includes('UTC')) {
89
+ // Deliberate one-line deviation from source: V8 omits UTC from
90
+ // supportedValuesOf, and a django-mojo portal whose picker cannot
91
+ // choose UTC is broken. The curated list leads with it too.
92
+ zones = ['UTC', ...zones];
93
+ }
94
+ cachedEngineZones = zones;
95
+ return zones;
96
+ }
97
+
98
+ /** The browser's own zone, 'UTC' when the engine can't say. */
99
+ export function localTimezone(): string {
100
+ try {
101
+ return new Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
102
+ } catch {
103
+ return 'UTC';
104
+ }
105
+ }
106
+
107
+ /** Caller's fixed list (order preserved) or the engine list. An EMPTY array
108
+ is treated as "no override", as in source. */
109
+ function zoneList(timezones?: readonly string[] | null): string[] {
110
+ return timezones && timezones.length ? timezones.slice() : engineZones();
111
+ }
112
+
113
+ /**
114
+ * The zone an empty value displays.
115
+ *
116
+ * A caller-supplied list constrains the choice, so a local zone outside it
117
+ * falls back to its first entry. The engine/curated list does not: the
118
+ * user's own zone is the right default even when the list can't browse it
119
+ * (ComboBox then shows it verbatim and warns once — house rule 4).
120
+ */
121
+ function defaultZone(zones: string[], isOverride: boolean): string {
122
+ const local = localTimezone();
123
+ if (isOverride && !zones.includes(local)) return zones.length ? zones[0] : local;
124
+ return local;
125
+ }
126
+
127
+ function resolveWithin(
128
+ value: string | null | undefined,
129
+ zones: string[],
130
+ isOverride: boolean,
131
+ defaultToLocal: boolean,
132
+ ): string | null {
133
+ if (value != null && value !== '') return value;
134
+ return defaultToLocal ? defaultZone(zones, isOverride) : null;
135
+ }
136
+
137
+ export interface ResolveTimezoneOptions {
138
+ timezones?: readonly string[] | null;
139
+ defaultToLocal?: boolean;
140
+ }
141
+
142
+ /**
143
+ * The EFFECTIVE zone a `<TimezoneSelect>` displays for a given value — and
144
+ * therefore the value a form should post for it (plain IANA string, or null
145
+ * when `defaultToLocal` is off and nothing is picked). The field registry's
146
+ * getFormValue is exactly this call.
147
+ */
148
+ export function resolveTimezone(
149
+ value: string | null | undefined,
150
+ options: ResolveTimezoneOptions = {},
151
+ ): string | null {
152
+ // A committed value wins verbatim — no list needs building for it.
153
+ if (value != null && value !== '') return value;
154
+ const isOverride = !!(options.timezones && options.timezones.length);
155
+ return resolveWithin(value, zoneList(options.timezones), isOverride, options.defaultToLocal !== false);
156
+ }
157
+
158
+ /**
159
+ * "America/New_York (UTC−05:00)" — `ianaOffset` does the shortOffset
160
+ * formatToParts work (GMT/UTC → +00:00, zero-padded hours); the sign becomes
161
+ * a Unicode minus here. Zones the engine rejects label as the bare name.
162
+ */
163
+ function zoneLabel(zone: string, at: Date): string {
164
+ const offset = ianaOffset(zone, at);
165
+ return offset ? `${zone} (UTC${offset.replace('-', MINUS)})` : zone;
166
+ }
167
+
168
+ export interface TimezoneChangeEvent {
169
+ value: string;
170
+ /** What the picker displayed before — the EFFECTIVE zone, so a default
171
+ that was never committed still reports honestly. */
172
+ oldValue: string | null;
173
+ }
174
+
175
+ export interface TimezoneSelectProps {
176
+ /** Committed IANA zone. Empty (null / '') displays the default. */
177
+ value?: string | null;
178
+ /** COMMIT only, and only when the zone actually changes. */
179
+ onChange?: (e: TimezoneChangeEvent) => void;
180
+ /** Fixed list overriding the engine's, in the order given. */
181
+ timezones?: readonly string[] | null;
182
+ /** Empty value displays the local zone. Default true. */
183
+ defaultToLocal?: boolean;
184
+ /** Renders a hidden input under this name, for native form posts. */
185
+ name?: string;
186
+ placeholder?: string;
187
+ disabled?: boolean;
188
+ required?: boolean;
189
+ /** Cap on rendered suggestions. Default: the whole list (source parity —
190
+ web-mojo's ComboBox scrolled every zone; it never capped). */
191
+ maxSuggestions?: number;
192
+ id?: string;
193
+ className?: string;
194
+ }
195
+
196
+ export function TimezoneSelect({
197
+ value = null,
198
+ onChange,
199
+ timezones = null,
200
+ defaultToLocal = true,
201
+ name,
202
+ placeholder = 'Search timezone...',
203
+ disabled = false,
204
+ required = false,
205
+ maxSuggestions,
206
+ id,
207
+ className,
208
+ }: TimezoneSelectProps) {
209
+ // Bumped by every gesture that can open the list; the memo key that
210
+ // relabels the zones (see OPEN_COALESCE_MS). Holding the instant in state
211
+ // keeps the label memo pure — no hidden Date.now() inside it.
212
+ const [stamp, setStamp] = useState(() => Date.now());
213
+ const markOpenGesture = () => setStamp((prev) => (Date.now() - prev > OPEN_COALESCE_MS ? Date.now() : prev));
214
+
215
+ const isOverride = !!(timezones && timezones.length);
216
+ const zones = useMemo(() => zoneList(timezones), [timezones]);
217
+ const current = resolveWithin(value, zones, isOverride, defaultToLocal);
218
+
219
+ // The committed zone is ALWAYS an option (see the alias note above): it
220
+ // leads the list so it is visible, labelled and re-selectable.
221
+ const options = useMemo<ComboOption[]>(() => {
222
+ const at = new Date(stamp);
223
+ const list = current !== null && !zones.includes(current) ? [current, ...zones] : zones;
224
+ return list.map((zone) => ({ value: zone, label: zoneLabel(zone, at) }));
225
+ }, [zones, stamp, current]);
226
+
227
+ // A zone the ENGINE cannot resolve (no offset at all) is a data bug, not
228
+ // a render event: warn once per (instance, value) and still display it.
229
+ const probedRef = useRef<Set<string>>(new Set());
230
+ if (current !== null && !probedRef.current.has(current)) {
231
+ probedRef.current.add(current);
232
+ if (ianaOffset(current) === null) {
233
+ console.warn(`TimezoneSelect: ${JSON.stringify(current)} is not a timezone this engine recognizes — displaying it verbatim`);
234
+ }
235
+ }
236
+
237
+ return (
238
+ <div
239
+ className={`tz-select${className ? ` ${className}` : ''}`}
240
+ onFocus={markOpenGesture}
241
+ onPointerDown={markOpenGesture}
242
+ onKeyDown={(e) => { if (e.key === 'ArrowDown') markOpenGesture(); }}
243
+ >
244
+ <ComboBox
245
+ value={current}
246
+ options={options}
247
+ // allowCustom:false is the whole commit-only guarantee: typed
248
+ // text that matches nothing reverts, it never becomes a value.
249
+ allowCustom={false}
250
+ maxSuggestions={maxSuggestions ?? options.length}
251
+ placeholder={placeholder}
252
+ disabled={disabled}
253
+ required={required}
254
+ id={id}
255
+ onChange={(next) => {
256
+ const zone = String(next);
257
+ // ComboBox already skips no-op commits; this keeps the
258
+ // {value, oldValue} contract true independently of it.
259
+ if (zone === current) return;
260
+ onChange?.({ value: zone, oldValue: current });
261
+ }}
262
+ />
263
+ {/* Native form posts get the plain IANA string (source parity). */}
264
+ {name && <input type="hidden" name={name} value={current ?? ''} />}
265
+ </div>
266
+ );
267
+ }