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.
- package/LICENSE +201 -0
- package/README.md +55 -0
- package/docs/DetailView.md +173 -0
- package/docs/ModelTable.md +128 -0
- package/docs/README.md +98 -0
- package/docs/admin-assistant.md +72 -0
- package/docs/admin-bouncer.md +120 -0
- package/docs/admin-cloudwatch.md +38 -0
- package/docs/admin-credentials.md +116 -0
- package/docs/admin-dashboard.md +15 -0
- package/docs/admin-devices-geoip.md +284 -0
- package/docs/admin-dns-certificates.md +122 -0
- package/docs/admin-dns-records.md +65 -0
- package/docs/admin-dns-registrar.md +33 -0
- package/docs/admin-dns.md +80 -0
- package/docs/admin-identity-users.md +103 -0
- package/docs/admin-incidents.md +71 -0
- package/docs/admin-jobs.md +426 -0
- package/docs/admin-members.md +112 -0
- package/docs/admin-messaging.md +56 -0
- package/docs/admin-metrics-explorer.md +63 -0
- package/docs/admin-monitoring.md +99 -0
- package/docs/admin-network-security.md +393 -0
- package/docs/admin-phonehub.md +27 -0
- package/docs/admin-push.md +36 -0
- package/docs/admin-rules.md +87 -0
- package/docs/admin-security-tickets.md +40 -0
- package/docs/admin-settings.md +78 -0
- package/docs/admin-shortlinks.md +32 -0
- package/docs/admin-storage.md +141 -0
- package/docs/auth-pages.md +212 -0
- package/docs/auth.md +58 -0
- package/docs/calendar.md +122 -0
- package/docs/charts.md +303 -0
- package/docs/client.md +206 -0
- package/docs/collection-multiselect.md +117 -0
- package/docs/collection-select.md +132 -0
- package/docs/combobox.md +105 -0
- package/docs/dataview.md +226 -0
- package/docs/datepicker.md +151 -0
- package/docs/daterange.md +170 -0
- package/docs/datetimepicker.md +249 -0
- package/docs/defineModel.md +72 -0
- package/docs/detail-primitives.md +174 -0
- package/docs/feedback.md +85 -0
- package/docs/file-upload-ux.md +146 -0
- package/docs/forms.md +435 -0
- package/docs/grouping-and-fmt.md +98 -0
- package/docs/idioms.md +112 -0
- package/docs/image-editor.md +101 -0
- package/docs/loading.md +242 -0
- package/docs/location-address.md +76 -0
- package/docs/markdown.md +203 -0
- package/docs/menus-and-access.md +89 -0
- package/docs/multiselect-dropdown.md +153 -0
- package/docs/params.md +73 -0
- package/docs/popover.md +130 -0
- package/docs/realtime.md +61 -0
- package/docs/record-feed.md +238 -0
- package/docs/right-panel.md +42 -0
- package/docs/tabs-and-form-wizard.md +60 -0
- package/docs/taginput.md +120 -0
- package/docs/timepicker.md +173 -0
- package/docs/timezone-select.md +147 -0
- package/docs/worldmap.md +302 -0
- package/package.json +55 -0
- package/src/admin/assistant/AssistantFeed.tsx +29 -0
- package/src/admin/assistant/AssistantPanel.tsx +65 -0
- package/src/admin/assistant/api.ts +17 -0
- package/src/admin/assistant/data.ts +63 -0
- package/src/admin/assistant/index.ts +21 -0
- package/src/admin/assistant/launchers.tsx +33 -0
- package/src/admin/assistant/pages.tsx +78 -0
- package/src/admin/assistant/streaming.ts +238 -0
- package/src/admin/assistant/types.ts +30 -0
- package/src/admin/bouncer/devices.tsx +230 -0
- package/src/admin/bouncer/index.ts +34 -0
- package/src/admin/bouncer/models.ts +199 -0
- package/src/admin/bouncer/signals.tsx +182 -0
- package/src/admin/bouncer/signatures.tsx +220 -0
- package/src/admin/cloudwatch/CloudWatchChart.tsx +61 -0
- package/src/admin/cloudwatch/CloudWatchDashboardPage.tsx +52 -0
- package/src/admin/cloudwatch/CloudWatchResourceDetail.tsx +33 -0
- package/src/admin/cloudwatch/data.ts +163 -0
- package/src/admin/cloudwatch/index.ts +16 -0
- package/src/admin/credentials/group-api-keys.tsx +433 -0
- package/src/admin/credentials/index.ts +4 -0
- package/src/admin/credentials/models.ts +244 -0
- package/src/admin/credentials/secret-dialog.tsx +77 -0
- package/src/admin/credentials/webhook-subscriptions.tsx +423 -0
- package/src/admin/dashboard/AdminDashboardPage.tsx +101 -0
- package/src/admin/dashboard/data.ts +60 -0
- package/src/admin/dashboard/index.ts +2 -0
- package/src/admin/dns/CertificateDetail.tsx +115 -0
- package/src/admin/dns/CertificateLifecyclePoller.tsx +141 -0
- package/src/admin/dns/CertificateRequestDialog.tsx +103 -0
- package/src/admin/dns/CertificatesPage.tsx +85 -0
- package/src/admin/dns/DnsRecordEditor.tsx +110 -0
- package/src/admin/dns/DnsRecordsPage.tsx +36 -0
- package/src/admin/dns/DnsRecordsPanel.tsx +71 -0
- package/src/admin/dns/DomainCertificatesSection.tsx +40 -0
- package/src/admin/dns/DomainDetail.tsx +41 -0
- package/src/admin/dns/DomainOnboardingDialog.tsx +27 -0
- package/src/admin/dns/DomainPurchaseDetail.tsx +11 -0
- package/src/admin/dns/DomainPurchaseWizard.tsx +112 -0
- package/src/admin/dns/DomainPurchasesPage.tsx +16 -0
- package/src/admin/dns/DomainsPage.tsx +27 -0
- package/src/admin/dns/ProviderCredentialsPage.tsx +304 -0
- package/src/admin/dns/RegistrantContactPage.tsx +42 -0
- package/src/admin/dns/api.ts +515 -0
- package/src/admin/dns/certificate-data.ts +221 -0
- package/src/admin/dns/data.ts +32 -0
- package/src/admin/dns/dns-data.ts +266 -0
- package/src/admin/dns/dns-integration.ts +24 -0
- package/src/admin/dns/domain-links.ts +37 -0
- package/src/admin/dns/index.ts +86 -0
- package/src/admin/dns/models.ts +418 -0
- package/src/admin/dns/purchase-data.ts +67 -0
- package/src/admin/dns/registrant-data.ts +52 -0
- package/src/admin/identity/index.ts +5 -0
- package/src/admin/identity/members/GroupMembersPanel.tsx +146 -0
- package/src/admin/identity/members/MemberDetail.tsx +226 -0
- package/src/admin/identity/members/MembersPage.tsx +144 -0
- package/src/admin/identity/members/index.ts +24 -0
- package/src/admin/identity/members/member-flows.tsx +132 -0
- package/src/admin/identity/members/models.ts +204 -0
- package/src/admin/identity/users/UserDetail.tsx +257 -0
- package/src/admin/identity/users/UsersPage.tsx +163 -0
- package/src/admin/identity/users/index.ts +27 -0
- package/src/admin/identity/users/models.ts +232 -0
- package/src/admin/identity/users/sections/ApiKeysSection.tsx +119 -0
- package/src/admin/identity/users/sections/AuditSection.tsx +190 -0
- package/src/admin/identity/users/sections/DevicesSection.tsx +140 -0
- package/src/admin/identity/users/sections/GroupsSection.tsx +64 -0
- package/src/admin/identity/users/sections/LoginsSection.tsx +106 -0
- package/src/admin/identity/users/sections/MetadataPanel.tsx +36 -0
- package/src/admin/identity/users/sections/NotificationsSection.tsx +117 -0
- package/src/admin/identity/users/sections/OAuthSection.tsx +72 -0
- package/src/admin/identity/users/sections/OverviewSection.tsx +109 -0
- package/src/admin/identity/users/sections/PermissionsSection.tsx +35 -0
- package/src/admin/identity/users/sections/PersonalSection.tsx +73 -0
- package/src/admin/identity/users/sections/ProfileSection.tsx +175 -0
- package/src/admin/identity/users/sections/SecuritySection.tsx +123 -0
- package/src/admin/identity/users/sections/actions.tsx +752 -0
- package/src/admin/identity/users/sections/permission-catalog.ts +138 -0
- package/src/admin/identity/users/sections/queries.ts +74 -0
- package/src/admin/identity/users/sections/shared.tsx +299 -0
- package/src/admin/incidents/EventDetail.tsx +43 -0
- package/src/admin/incidents/EventsPage.tsx +52 -0
- package/src/admin/incidents/IncidentDetail.tsx +68 -0
- package/src/admin/incidents/IncidentsPage.tsx +102 -0
- package/src/admin/incidents/forensics.tsx +73 -0
- package/src/admin/incidents/index.ts +16 -0
- package/src/admin/incidents/models.ts +113 -0
- package/src/admin/incidents/sanitize.ts +71 -0
- package/src/admin/index.ts +383 -0
- package/src/admin/jobs/JobDashboardPage.tsx +111 -0
- package/src/admin/jobs/JobDetail.tsx +721 -0
- package/src/admin/jobs/JobRunnersPage.tsx +216 -0
- package/src/admin/jobs/JobsTablePage.tsx +122 -0
- package/src/admin/jobs/RunnerDetail.tsx +576 -0
- package/src/admin/jobs/ScheduledTaskDetail.tsx +421 -0
- package/src/admin/jobs/ScheduledTasksPage.tsx +167 -0
- package/src/admin/jobs/columns.tsx +297 -0
- package/src/admin/jobs/control.ts +488 -0
- package/src/admin/jobs/index.ts +85 -0
- package/src/admin/jobs/models.ts +599 -0
- package/src/admin/jobs/queries.ts +272 -0
- package/src/admin/jobs/scheduled-task-form.tsx +493 -0
- package/src/admin/jobs/sections/JobChannelsPanel.tsx +142 -0
- package/src/admin/jobs/sections/JobOperationsSection.tsx +423 -0
- package/src/admin/jobs/sections/JobRunnersStrip.tsx +82 -0
- package/src/admin/jobs/sections/JobThroughputSection.tsx +118 -0
- package/src/admin/messaging/EmailDomainWizard.tsx +24 -0
- package/src/admin/messaging/EmailDomainsPage.tsx +17 -0
- package/src/admin/messaging/EmailTemplatesPage.tsx +13 -0
- package/src/admin/messaging/MailboxesPage.tsx +15 -0
- package/src/admin/messaging/PublicMessagesPage.tsx +13 -0
- package/src/admin/messaging/SandboxedEmailPreview.tsx +23 -0
- package/src/admin/messaging/SentMessagesPage.tsx +11 -0
- package/src/admin/messaging/api.ts +17 -0
- package/src/admin/messaging/data.ts +10 -0
- package/src/admin/messaging/index.ts +23 -0
- package/src/admin/messaging/models.ts +76 -0
- package/src/admin/messaging/push/PushPage.tsx +44 -0
- package/src/admin/messaging/push/api.ts +17 -0
- package/src/admin/messaging/push/index.ts +9 -0
- package/src/admin/messaging/push/models.ts +43 -0
- package/src/admin/messaging/sanitize.ts +44 -0
- package/src/admin/monitoring/LogInspector.tsx +160 -0
- package/src/admin/monitoring/LogsPage.tsx +135 -0
- package/src/admin/monitoring/MetricsExplorerPage.tsx +360 -0
- package/src/admin/monitoring/MetricsPermissionsPage.tsx +166 -0
- package/src/admin/monitoring/MetricsSourcePicker.tsx +136 -0
- package/src/admin/monitoring/index.ts +55 -0
- package/src/admin/monitoring/metrics-explorer-client.ts +229 -0
- package/src/admin/monitoring/metrics-explorer-data.ts +158 -0
- package/src/admin/monitoring/models.ts +155 -0
- package/src/admin/network/BlockedIPsPage.tsx +194 -0
- package/src/admin/network/BlockedIpDetail.tsx +262 -0
- package/src/admin/network/FirewallLogPage.tsx +115 -0
- package/src/admin/network/IPSetDetail.tsx +280 -0
- package/src/admin/network/IPSetEditor.tsx +191 -0
- package/src/admin/network/IPSetsPage.tsx +177 -0
- package/src/admin/network/geofence/BlocksTab.tsx +289 -0
- package/src/admin/network/geofence/ExemptionsTab.tsx +349 -0
- package/src/admin/network/geofence/GeofencingPage.tsx +119 -0
- package/src/admin/network/geofence/PostureHeader.tsx +184 -0
- package/src/admin/network/geofence/RuleEditor.tsx +176 -0
- package/src/admin/network/geofence/RulesTab.tsx +254 -0
- package/src/admin/network/geofence/SimulatorTab.tsx +281 -0
- package/src/admin/network/geofence/geofence-data.ts +674 -0
- package/src/admin/network/index.ts +68 -0
- package/src/admin/network/models.ts +607 -0
- package/src/admin/phonehub/PhoneHubPage.tsx +57 -0
- package/src/admin/phonehub/api.ts +47 -0
- package/src/admin/phonehub/data.ts +6 -0
- package/src/admin/phonehub/index.ts +10 -0
- package/src/admin/phonehub/models.ts +68 -0
- package/src/admin/rules/HandlerChainBuilder.tsx +51 -0
- package/src/admin/rules/RuleSetDetailPage.tsx +33 -0
- package/src/admin/rules/RuleSetsPage.tsx +33 -0
- package/src/admin/rules/editors.tsx +54 -0
- package/src/admin/rules/handler-dsl.ts +213 -0
- package/src/admin/rules/index.ts +13 -0
- package/src/admin/rules/models.ts +112 -0
- package/src/admin/security/devices/LoginEventDetail.tsx +123 -0
- package/src/admin/security/devices/LoginLocationMap.tsx +321 -0
- package/src/admin/security/devices/LoginLocationsPage.tsx +162 -0
- package/src/admin/security/devices/UserDeviceDetail.tsx +546 -0
- package/src/admin/security/devices/UserDeviceLocationDetail.tsx +272 -0
- package/src/admin/security/devices/UserDevicesPage.tsx +84 -0
- package/src/admin/security/devices/index.ts +40 -0
- package/src/admin/security/devices/models.ts +470 -0
- package/src/admin/security/geoip/GeoIpCachePage.tsx +135 -0
- package/src/admin/security/geoip/GeoIpDossier.tsx +804 -0
- package/src/admin/security/geoip/geoip-forms.ts +102 -0
- package/src/admin/security/geoip/index.ts +27 -0
- package/src/admin/security/geoip/models.ts +397 -0
- package/src/admin/security/index.ts +31 -0
- package/src/admin/security/models.ts +137 -0
- package/src/admin/security/tickets.tsx +562 -0
- package/src/admin/security-permissions.ts +5 -0
- package/src/admin/settings/SettingDetail.tsx +57 -0
- package/src/admin/settings/SettingEditor.tsx +157 -0
- package/src/admin/settings/SettingsPage.tsx +72 -0
- package/src/admin/settings/index.ts +19 -0
- package/src/admin/settings/model.ts +143 -0
- package/src/admin/shortlinks/ShortlinkDetail.tsx +55 -0
- package/src/admin/shortlinks/ShortlinkHistoryPage.tsx +18 -0
- package/src/admin/shortlinks/ShortlinksPage.tsx +59 -0
- package/src/admin/shortlinks/index.ts +18 -0
- package/src/admin/shortlinks/models.ts +246 -0
- package/src/admin/storage/BackendsPage.tsx +43 -0
- package/src/admin/storage/BucketDetail.tsx +77 -0
- package/src/admin/storage/BucketsPage.tsx +64 -0
- package/src/admin/storage/FileManagerDetail.tsx +36 -0
- package/src/admin/storage/FilePreview.tsx +54 -0
- package/src/admin/storage/FileUploadSurface.tsx +250 -0
- package/src/admin/storage/FileView.tsx +109 -0
- package/src/admin/storage/FilesPage.tsx +53 -0
- package/src/admin/storage/file-renditions.ts +99 -0
- package/src/admin/storage/index.ts +31 -0
- package/src/admin/storage/models.ts +585 -0
- package/src/admin/storage/storage-dialogs.tsx +80 -0
- package/src/charts/CircularProgress.tsx +281 -0
- package/src/charts/KPIStrip.tsx +162 -0
- package/src/charts/KPITile.tsx +125 -0
- package/src/charts/MetricsChart.tsx +459 -0
- package/src/charts/MetricsMiniWidget.tsx +588 -0
- package/src/charts/MiniChart.tsx +283 -0
- package/src/charts/PieChart.tsx +354 -0
- package/src/charts/SeriesChart.tsx +378 -0
- package/src/charts/chart-dialogs.tsx +149 -0
- package/src/charts/exportChart.ts +144 -0
- package/src/charts/index.ts +28 -0
- package/src/charts/pie-math.ts +127 -0
- package/src/charts/stats.ts +265 -0
- package/src/charts/worldmap/WorldMap.tsx +649 -0
- package/src/charts/worldmap/countryCentroids.ts +304 -0
- package/src/charts/worldmap/geo.ts +293 -0
- package/src/charts/worldmap/use-world-land.ts +47 -0
- package/src/charts/worldmap/world-land.ts +18 -0
- package/src/charts/worldmap/worldmap-data.ts +285 -0
- package/src/client/auth.ts +737 -0
- package/src/client/client.ts +268 -0
- package/src/client/duid.ts +31 -0
- package/src/client/errors.ts +33 -0
- package/src/client/group-context.ts +28 -0
- package/src/client/group.tsx +126 -0
- package/src/client/hooks.ts +50 -0
- package/src/client/index.ts +37 -0
- package/src/client/jwt.ts +75 -0
- package/src/client/location.ts +285 -0
- package/src/client/lookups.ts +41 -0
- package/src/client/markdown-fallback.tsx +115 -0
- package/src/client/markdown-parse.ts +386 -0
- package/src/client/markdown-sanitize.ts +217 -0
- package/src/client/markdown.ts +122 -0
- package/src/client/me.ts +188 -0
- package/src/client/mock.ts +9984 -0
- package/src/client/model.ts +227 -0
- package/src/client/params.ts +292 -0
- package/src/client/realtime-mock.ts +221 -0
- package/src/client/realtime.ts +561 -0
- package/src/client/record-feed.ts +329 -0
- package/src/client/safe-export.ts +71 -0
- package/src/client/types.ts +278 -0
- package/src/client/upload.ts +421 -0
- package/src/ui/AddressField.tsx +273 -0
- package/src/ui/AttachmentQueue.tsx +92 -0
- package/src/ui/CollectionMultiSelect.tsx +622 -0
- package/src/ui/CollectionSelect.tsx +517 -0
- package/src/ui/ComboBox.tsx +336 -0
- package/src/ui/DetailView.tsx +438 -0
- package/src/ui/FileDrop.tsx +194 -0
- package/src/ui/FileField.tsx +369 -0
- package/src/ui/FilterBar.tsx +269 -0
- package/src/ui/FormFields.tsx +77 -0
- package/src/ui/FormView.tsx +360 -0
- package/src/ui/FormWizard.tsx +336 -0
- package/src/ui/GroupSwitcher.tsx +202 -0
- package/src/ui/Guarded.tsx +16 -0
- package/src/ui/MarkdownView.tsx +89 -0
- package/src/ui/ModelTable.tsx +848 -0
- package/src/ui/MultiSelectDropdown.tsx +390 -0
- package/src/ui/Popover.tsx +204 -0
- package/src/ui/RecordFeed.tsx +627 -0
- package/src/ui/RequiresGroup.tsx +32 -0
- package/src/ui/RightPanel.tsx +107 -0
- package/src/ui/SidebarNav.tsx +330 -0
- package/src/ui/Tabs.tsx +161 -0
- package/src/ui/TagInput.tsx +381 -0
- package/src/ui/ThemeProvider.tsx +52 -0
- package/src/ui/UploadQueue.tsx +603 -0
- package/src/ui/armed-button.tsx +91 -0
- package/src/ui/date/Calendar.tsx +498 -0
- package/src/ui/date/DatePicker.tsx +289 -0
- package/src/ui/date/DateRangePicker.tsx +280 -0
- package/src/ui/date/DateTimePicker.tsx +587 -0
- package/src/ui/date/PresetRail.tsx +165 -0
- package/src/ui/date/TimePicker.tsx +624 -0
- package/src/ui/date/TimezoneSelect.tsx +267 -0
- package/src/ui/date/fns.ts +679 -0
- package/src/ui/detail/DataView.tsx +692 -0
- package/src/ui/detail/FlowStrip.tsx +80 -0
- package/src/ui/detail/KnownFieldsCard.tsx +121 -0
- package/src/ui/detail/MetadataSection.tsx +219 -0
- package/src/ui/detail/StackTraceView.tsx +150 -0
- package/src/ui/detail/StatusPanel.tsx +73 -0
- package/src/ui/detail/Timeline.tsx +52 -0
- package/src/ui/detail/index.ts +12 -0
- package/src/ui/field-registry.tsx +428 -0
- package/src/ui/field-wire.ts +322 -0
- package/src/ui/form-autosave.ts +583 -0
- package/src/ui/format.ts +444 -0
- package/src/ui/grouping.ts +150 -0
- package/src/ui/image-editor/ImageEditor.tsx +484 -0
- package/src/ui/image-editor/canvas.ts +254 -0
- package/src/ui/image-editor/index.ts +3 -0
- package/src/ui/image-editor/math.ts +416 -0
- package/src/ui/index.ts +58 -0
- package/src/ui/loading.tsx +604 -0
- package/src/ui/menu-registry.ts +213 -0
- package/src/ui/modal.tsx +297 -0
- package/src/ui/password.tsx +302 -0
- package/src/ui/schema-form-core.tsx +315 -0
- package/src/ui/toast.tsx +219 -0
- 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
|
+
}
|