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,91 @@
|
|
|
1
|
+
// ArmedButton — two-step confirm for destructive, NO-UNDO actions, ported
|
|
2
|
+
// from maestro workspaces/js/dom.js `armedButton()` (lines 238-264).
|
|
3
|
+
//
|
|
4
|
+
// The first click ARMS the button — danger styling plus a label that names
|
|
5
|
+
// the blast radius ("Click again to revoke") — and only the second click
|
|
6
|
+
// fires. It auto-disarms after `disarmMs` (source: 6s), so a stray click can
|
|
7
|
+
// never leave a live trigger sitting on screen; unmount disarms for free
|
|
8
|
+
// (React state dies with the node — the source's "re-render drops the node"
|
|
9
|
+
// fail-safe, same cost: one extra click, never an unintended destroy).
|
|
10
|
+
//
|
|
11
|
+
// This is the house idiom for IRREVERSIBLE actions. Reach for it instead of
|
|
12
|
+
// `modal.confirm` when the action needs no input and the confirm dialog
|
|
13
|
+
// would be pure friction (inline row actions, danger zones). Actions that
|
|
14
|
+
// collect input (a disable reason) keep their form modal; actions that can
|
|
15
|
+
// be UNDONE act immediately and offer `toast.undo` instead.
|
|
16
|
+
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
|
17
|
+
|
|
18
|
+
export interface ArmedButtonProps {
|
|
19
|
+
/** Resting label ("Revoke key"). */
|
|
20
|
+
label: ReactNode;
|
|
21
|
+
/** Armed label — MUST name the blast radius ("Click again to revoke"). */
|
|
22
|
+
armedLabel: ReactNode;
|
|
23
|
+
/** Fires on the second click only. Await-ed; re-clicks ignored while pending. */
|
|
24
|
+
onConfirm: () => void | Promise<void>;
|
|
25
|
+
title?: string;
|
|
26
|
+
/** Extra classes on top of `btn armed-btn` (e.g. 'btn-compact'). */
|
|
27
|
+
className?: string;
|
|
28
|
+
/** Auto-disarm window in ms. Source parity: 6000. */
|
|
29
|
+
disarmMs?: number;
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
/** Optional bootstrap-icon shown before the resting label. */
|
|
32
|
+
icon?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function ArmedButton({
|
|
36
|
+
label, armedLabel, onConfirm, title, className, disarmMs = 6000, disabled = false, icon,
|
|
37
|
+
}: ArmedButtonProps) {
|
|
38
|
+
const [armed, setArmed] = useState(false);
|
|
39
|
+
const [busy, setBusy] = useState(false);
|
|
40
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
41
|
+
|
|
42
|
+
const disarm = () => {
|
|
43
|
+
if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null; }
|
|
44
|
+
setArmed(false);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Unmount clears the timer; the armed state itself dies with the node.
|
|
48
|
+
useEffect(() => () => { if (timerRef.current) clearTimeout(timerRef.current); }, []);
|
|
49
|
+
|
|
50
|
+
// Escape disarms — a keyboard user who armed by accident needs an exit
|
|
51
|
+
// that isn't "wait 6 seconds" or "click the thing you fear".
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (!armed) return;
|
|
54
|
+
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') disarm(); };
|
|
55
|
+
document.addEventListener('keydown', onKey);
|
|
56
|
+
return () => document.removeEventListener('keydown', onKey);
|
|
57
|
+
}, [armed]);
|
|
58
|
+
|
|
59
|
+
const click = async (e: React.MouseEvent) => {
|
|
60
|
+
// Armed buttons live inside clickable rows — the arm/fire click must
|
|
61
|
+
// never double as a row open (source parity: e.stopPropagation()).
|
|
62
|
+
e.stopPropagation();
|
|
63
|
+
if (busy) return;
|
|
64
|
+
if (!armed) {
|
|
65
|
+
setArmed(true);
|
|
66
|
+
timerRef.current = setTimeout(() => { timerRef.current = null; setArmed(false); }, disarmMs);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
disarm();
|
|
70
|
+
try {
|
|
71
|
+
setBusy(true);
|
|
72
|
+
await onConfirm();
|
|
73
|
+
} finally {
|
|
74
|
+
setBusy(false);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<button
|
|
80
|
+
type="button"
|
|
81
|
+
className={`btn armed-btn${armed ? ' is-armed' : ''}${className ? ` ${className}` : ''}`}
|
|
82
|
+
title={title ?? (typeof label === 'string' ? label : undefined)}
|
|
83
|
+
disabled={disabled || busy}
|
|
84
|
+
onClick={(e) => { void click(e); }}
|
|
85
|
+
>
|
|
86
|
+
{armed
|
|
87
|
+
? <><i className="bi bi-exclamation-triangle-fill" /> {armedLabel}</>
|
|
88
|
+
: <>{icon && <i className={`bi ${icon}`} />} {label}</>}
|
|
89
|
+
</button>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
// Calendar — the picker engine. Ported from web-mojo
|
|
2
|
+
// src/core/forms/inputs/calendar/Calendar.js (623 lines, read in full).
|
|
3
|
+
//
|
|
4
|
+
// Renders day-grid / month-grid / year-grid based on `precision` and the
|
|
5
|
+
// current view (zoom level). Supports single-value and range selection at
|
|
6
|
+
// every precision. Same engine, three precisions.
|
|
7
|
+
//
|
|
8
|
+
// Cross-page anchor persistence: once a range start is committed, paging
|
|
9
|
+
// with prev/next or keyboard does not clear it. Hover preview tints visible
|
|
10
|
+
// cells up to the cursor regardless of where the anchor sits.
|
|
11
|
+
//
|
|
12
|
+
// Drill-down zoom: clicking the header label moves up one level
|
|
13
|
+
// (day → month → year). Selecting a tile above the configured `precision`
|
|
14
|
+
// drills back down; a tile at precision commits.
|
|
15
|
+
//
|
|
16
|
+
// React shape: SELECTION IS CONTROLLED — `value` (single) / `startValue` +
|
|
17
|
+
// `endValue` (range) are props; commits arrive via callbacks and paint only
|
|
18
|
+
// once the owner writes them back. The component owns view / page / anchor /
|
|
19
|
+
// hover as internal state. External prop writes keep the source's
|
|
20
|
+
// setValue/setRange semantics: a new `value` re-pages to it; a new range
|
|
21
|
+
// clears any in-progress anchor. While an anchor is active the committed
|
|
22
|
+
// range is suppressed (source nulls it); Escape cancels the anchor and the
|
|
23
|
+
// committed props paint again — the one deviation from source, which left
|
|
24
|
+
// the calendar blank until the next commit (nothing consumed range:cancel).
|
|
25
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
26
|
+
import type { KeyboardEvent as ReactKeyboardEvent, ReactNode } from 'react';
|
|
27
|
+
import {
|
|
28
|
+
addMonths,
|
|
29
|
+
compareByPrecision, compareYm, compareYmd,
|
|
30
|
+
daysInMonth,
|
|
31
|
+
formatByPrecision, formatYm, formatYmd,
|
|
32
|
+
monthNames,
|
|
33
|
+
parseByPrecision, parseYmd,
|
|
34
|
+
today,
|
|
35
|
+
weekdayNames, weekdayWithFirstDay,
|
|
36
|
+
} from './fns';
|
|
37
|
+
import type { ParsedDate, ParsedYm, ParsedYmd, Precision } from './fns';
|
|
38
|
+
|
|
39
|
+
export type { ParsedDate, ParsedYm, ParsedYmd, Precision } from './fns';
|
|
40
|
+
|
|
41
|
+
export const PRECISION_RANK: Record<Precision, number> = { year: 0, month: 1, day: 2 };
|
|
42
|
+
|
|
43
|
+
export type CalendarMode = 'single' | 'range';
|
|
44
|
+
|
|
45
|
+
export interface CalendarSelectEvent { value: string; parsed: ParsedDate }
|
|
46
|
+
export interface CalendarRangeSelectEvent { start: string; end: string }
|
|
47
|
+
export interface CalendarRangeStartEvent { anchor: string }
|
|
48
|
+
export interface CalendarViewChangeEvent { view: Precision }
|
|
49
|
+
export interface CalendarNavigateEvent { delta: number }
|
|
50
|
+
|
|
51
|
+
export interface CalendarProps {
|
|
52
|
+
/** Grid the picker commits at: 'day' | 'month' | 'year'. Default 'day'. */
|
|
53
|
+
precision?: Precision;
|
|
54
|
+
/** 'single' | 'range'. Default 'single'. */
|
|
55
|
+
mode?: CalendarMode;
|
|
56
|
+
/** Day view only: 1 | 2 side-by-side month panes. Default 1. */
|
|
57
|
+
months?: 1 | 2;
|
|
58
|
+
/** Single mode — canonical string at precision (YYYY-MM-DD / YYYY-MM / YYYY). */
|
|
59
|
+
value?: string | null;
|
|
60
|
+
/** Range mode — canonical strings at precision. */
|
|
61
|
+
startValue?: string | null;
|
|
62
|
+
endValue?: string | null;
|
|
63
|
+
/** Bounds, compared at the PANE precision being painted (a year tile is
|
|
64
|
+
* disabled only when the whole year is out of bounds). */
|
|
65
|
+
min?: string | null;
|
|
66
|
+
max?: string | null;
|
|
67
|
+
/** Individually disabled days, "YYYY-MM-DD" — day grids only. */
|
|
68
|
+
disabledDates?: string[];
|
|
69
|
+
/** First weekday: 0=Sun, 1=Mon, … Default 1. */
|
|
70
|
+
firstDay?: number;
|
|
71
|
+
/** BCP-47 locale for month/weekday names. Default 'en-US'. */
|
|
72
|
+
locale?: string;
|
|
73
|
+
/** Initial page (first visible month / decade seed). Defaults to the
|
|
74
|
+
* selection, else today. */
|
|
75
|
+
year?: number | null;
|
|
76
|
+
month?: number | null;
|
|
77
|
+
className?: string;
|
|
78
|
+
/** Single mode: a value was committed at picker precision. */
|
|
79
|
+
onSelect?: (event: CalendarSelectEvent) => void;
|
|
80
|
+
/** Range mode: both ends committed (ordered). */
|
|
81
|
+
onRangeSelect?: (event: CalendarRangeSelectEvent) => void;
|
|
82
|
+
/** Range mode: first anchor placed (no commit yet). */
|
|
83
|
+
onRangeStart?: (event: CalendarRangeStartEvent) => void;
|
|
84
|
+
/** Range mode: in-progress anchor cancelled (Escape). */
|
|
85
|
+
onRangeCancel?: () => void;
|
|
86
|
+
/** Zoom level changed (drill-down or header zoom-out). */
|
|
87
|
+
onViewChange?: (event: CalendarViewChangeEvent) => void;
|
|
88
|
+
/** Page changed (prev/next/PageUp/PageDown). */
|
|
89
|
+
onNavigate?: (event: CalendarNavigateEvent) => void;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const CELL = 'mojo-calendar-cell';
|
|
93
|
+
|
|
94
|
+
function decadeStart(year: number): number { return Math.floor(year / 10) * 10; }
|
|
95
|
+
|
|
96
|
+
/** Unknown enum-ish prop values fall back WITH a warn — never render nothing. */
|
|
97
|
+
function normPrecision(p: Precision | undefined): Precision {
|
|
98
|
+
if (p == null || p === 'day' || p === 'month' || p === 'year') return p ?? 'day';
|
|
99
|
+
console.warn(`Calendar: unknown precision "${String(p)}" — falling back to "day"`);
|
|
100
|
+
return 'day';
|
|
101
|
+
}
|
|
102
|
+
function normMode(m: CalendarMode | undefined): CalendarMode {
|
|
103
|
+
if (m == null || m === 'single' || m === 'range') return m ?? 'single';
|
|
104
|
+
console.warn(`Calendar: unknown mode "${String(m)}" — falling back to "single"`);
|
|
105
|
+
return 'single';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function Calendar(props: CalendarProps) {
|
|
109
|
+
const precision = normPrecision(props.precision);
|
|
110
|
+
const mode = normMode(props.mode);
|
|
111
|
+
const {
|
|
112
|
+
value = null, startValue = null, endValue = null,
|
|
113
|
+
min = null, max = null, disabledDates,
|
|
114
|
+
firstDay = 1, locale = 'en-US',
|
|
115
|
+
year = null, month = null, className = '',
|
|
116
|
+
onSelect, onRangeSelect, onRangeStart, onRangeCancel, onViewChange, onNavigate,
|
|
117
|
+
} = props;
|
|
118
|
+
const panes = Math.max(1, Math.min(2, (props.months ?? 1) | 0));
|
|
119
|
+
|
|
120
|
+
// ── Internal state: view / page / anchor / hover ──────────────
|
|
121
|
+
const [view, setView] = useState<Precision>(precision);
|
|
122
|
+
const [page, setPage] = useState<{ y: number; m: number }>(() => {
|
|
123
|
+
const t = today();
|
|
124
|
+
const seed = (mode === 'single'
|
|
125
|
+
? parseByPrecision(value, precision)
|
|
126
|
+
: (parseByPrecision(startValue, precision) || parseByPrecision(endValue, precision))) || t;
|
|
127
|
+
return {
|
|
128
|
+
y: year != null ? Number(year) : seed.y,
|
|
129
|
+
m: month != null ? Number(month) : (seed.m ?? t.m),
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
const [anchor, setAnchor] = useState<ParsedDate | null>(null);
|
|
133
|
+
const [hover, setHover] = useState<ParsedDate | null>(null);
|
|
134
|
+
|
|
135
|
+
// Precision prop change = a different picker: reset zoom + interaction.
|
|
136
|
+
const prevPrecision = useRef(precision);
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (prevPrecision.current === precision) return;
|
|
139
|
+
prevPrecision.current = precision;
|
|
140
|
+
setView(precision);
|
|
141
|
+
setAnchor(null);
|
|
142
|
+
setHover(null);
|
|
143
|
+
}, [precision]);
|
|
144
|
+
|
|
145
|
+
// ── Controlled-selection sync (setValue / setRange semantics) ─
|
|
146
|
+
// Our own commits echo back as prop changes — those must not re-page or
|
|
147
|
+
// cancel anything, so remember what we emitted and skip the echo.
|
|
148
|
+
const lastEmittedValue = useRef<string | null>(null);
|
|
149
|
+
const lastEmittedRange = useRef<{ start: string; end: string } | null>(null);
|
|
150
|
+
const prevValueProp = useRef<string | null>(value);
|
|
151
|
+
const prevStartProp = useRef<string | null>(startValue);
|
|
152
|
+
const prevEndProp = useRef<string | null>(endValue);
|
|
153
|
+
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
if (value === prevValueProp.current) return;
|
|
156
|
+
prevValueProp.current = value;
|
|
157
|
+
const echo = value !== null && value === lastEmittedValue.current;
|
|
158
|
+
lastEmittedValue.current = null;
|
|
159
|
+
if (echo || mode !== 'single') return;
|
|
160
|
+
// setValue semantics: page over to the new value (when it parses).
|
|
161
|
+
const parsed = parseByPrecision(value, precision);
|
|
162
|
+
if (parsed) setPage((p) => ({ y: parsed.y, m: parsed.m ?? p.m }));
|
|
163
|
+
}, [value, mode, precision]);
|
|
164
|
+
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
if (startValue === prevStartProp.current && endValue === prevEndProp.current) return;
|
|
167
|
+
prevStartProp.current = startValue;
|
|
168
|
+
prevEndProp.current = endValue;
|
|
169
|
+
const emitted = lastEmittedRange.current;
|
|
170
|
+
lastEmittedRange.current = null;
|
|
171
|
+
if (emitted && emitted.start === startValue && emitted.end === endValue) return;
|
|
172
|
+
// setRange semantics: a programmatic range write cancels any
|
|
173
|
+
// in-progress anchor (do NOT write range props from onRangeStart).
|
|
174
|
+
setAnchor(null);
|
|
175
|
+
setHover(null);
|
|
176
|
+
}, [startValue, endValue]);
|
|
177
|
+
|
|
178
|
+
// ── Derived selection / constraints ───────────────────────────
|
|
179
|
+
const selected = useMemo(
|
|
180
|
+
() => (mode === 'single' ? parseByPrecision(value, precision) : null),
|
|
181
|
+
[mode, value, precision],
|
|
182
|
+
);
|
|
183
|
+
const startP = useMemo(
|
|
184
|
+
() => (mode === 'range' ? parseByPrecision(startValue, precision) : null),
|
|
185
|
+
[mode, startValue, precision],
|
|
186
|
+
);
|
|
187
|
+
const endP = useMemo(
|
|
188
|
+
() => (mode === 'range' ? parseByPrecision(endValue, precision) : null),
|
|
189
|
+
[mode, endValue, precision],
|
|
190
|
+
);
|
|
191
|
+
const minP = useMemo(
|
|
192
|
+
() => parseByPrecision(min, precision) || (min ? parseYmd(min) : null),
|
|
193
|
+
[min, precision],
|
|
194
|
+
);
|
|
195
|
+
const maxP = useMemo(
|
|
196
|
+
() => parseByPrecision(max, precision) || (max ? parseYmd(max) : null),
|
|
197
|
+
[max, precision],
|
|
198
|
+
);
|
|
199
|
+
const disabledSet = useMemo(
|
|
200
|
+
() => new Set((disabledDates ?? []).map((s) => formatYmd(parseYmd(s))).filter(Boolean)),
|
|
201
|
+
[disabledDates],
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const isDisabledAt = (cur: ParsedDate, level: Precision): boolean => {
|
|
205
|
+
if (minP && compareByPrecision(cur, minP, level) < 0) return true;
|
|
206
|
+
if (maxP && compareByPrecision(cur, maxP, level) > 0) return true;
|
|
207
|
+
if (level === 'day' && disabledSet.size) {
|
|
208
|
+
if (disabledSet.has(formatYmd(cur as ParsedYmd))) return true;
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// ── Range visuals ─────────────────────────────────────────────
|
|
214
|
+
// While an anchor is active it (plus hover) defines the painted span and
|
|
215
|
+
// the committed range is suppressed — in-PREVIEW classes, distinct from
|
|
216
|
+
// the committed in-RANGE classes.
|
|
217
|
+
const rangeClasses = (cur: ParsedDate, level: Precision): string[] => {
|
|
218
|
+
const out: string[] = [];
|
|
219
|
+
let start = startP;
|
|
220
|
+
let end = endP;
|
|
221
|
+
if (anchor) {
|
|
222
|
+
const other = hover || anchor;
|
|
223
|
+
const cmp = compareByPrecision(anchor, other, level);
|
|
224
|
+
start = cmp <= 0 ? anchor : other;
|
|
225
|
+
end = cmp <= 0 ? other : anchor;
|
|
226
|
+
}
|
|
227
|
+
if (!start || !end) return out;
|
|
228
|
+
|
|
229
|
+
const inClass = anchor ? `${CELL}-in-preview` : `${CELL}-in-range`;
|
|
230
|
+
const cmpS = compareByPrecision(cur, start, level);
|
|
231
|
+
const cmpE = compareByPrecision(cur, end, level);
|
|
232
|
+
if (cmpS > 0 && cmpE < 0) out.push(inClass);
|
|
233
|
+
if (cmpS === 0) {
|
|
234
|
+
out.push(`${CELL}-anchor`, `${CELL}-anchor-start`);
|
|
235
|
+
if (compareByPrecision(start, end, level) === 0) out.push(`${CELL}-anchor-solo`);
|
|
236
|
+
else out.push(inClass);
|
|
237
|
+
}
|
|
238
|
+
if (cmpE === 0 && compareByPrecision(start, end, level) !== 0) {
|
|
239
|
+
out.push(`${CELL}-anchor`, `${CELL}-anchor-end`, inClass);
|
|
240
|
+
}
|
|
241
|
+
return out;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// ── Commit / interaction ──────────────────────────────────────
|
|
245
|
+
const commitValue = (val: ParsedDate) => {
|
|
246
|
+
if (mode === 'single') {
|
|
247
|
+
const str = formatByPrecision(val, precision);
|
|
248
|
+
lastEmittedValue.current = str;
|
|
249
|
+
onSelect?.({ value: str, parsed: val });
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (!anchor) {
|
|
253
|
+
setAnchor(val);
|
|
254
|
+
setHover(null);
|
|
255
|
+
onRangeStart?.({ anchor: formatByPrecision(val, precision) });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
// Second click — commit range, swapping if needed.
|
|
259
|
+
const cmp = compareByPrecision(anchor, val, precision);
|
|
260
|
+
const start = cmp <= 0 ? anchor : val;
|
|
261
|
+
const end = cmp <= 0 ? val : anchor;
|
|
262
|
+
setAnchor(null);
|
|
263
|
+
setHover(null);
|
|
264
|
+
const startStr = formatByPrecision(start, precision);
|
|
265
|
+
const endStr = formatByPrecision(end, precision);
|
|
266
|
+
lastEmittedRange.current = { start: startStr, end: endStr };
|
|
267
|
+
onRangeSelect?.({ start: startStr, end: endStr });
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const clickYear = (yr: number) => {
|
|
271
|
+
if (precision === 'year') { commitValue({ y: yr }); return; }
|
|
272
|
+
// Drill down into that year.
|
|
273
|
+
setPage((p) => ({ ...p, y: yr }));
|
|
274
|
+
setView('month');
|
|
275
|
+
onViewChange?.({ view: 'month' });
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const clickMonth = (ym: ParsedYm) => {
|
|
279
|
+
if (precision === 'month') { commitValue(ym); return; }
|
|
280
|
+
// Drill down into that month.
|
|
281
|
+
setPage({ y: ym.y, m: ym.m });
|
|
282
|
+
setView('day');
|
|
283
|
+
onViewChange?.({ view: 'day' });
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const clickDay = (ymd: ParsedYmd) => { commitValue(ymd); };
|
|
287
|
+
|
|
288
|
+
const navigate = (delta: number) => {
|
|
289
|
+
setPage((p) => {
|
|
290
|
+
if (view === 'day') {
|
|
291
|
+
const next = addMonths({ y: p.y, m: p.m }, delta);
|
|
292
|
+
return { y: next.y, m: next.m };
|
|
293
|
+
}
|
|
294
|
+
if (view === 'month') return { ...p, y: p.y + delta };
|
|
295
|
+
return { ...p, y: decadeStart(p.y) + delta * 10 };
|
|
296
|
+
});
|
|
297
|
+
onNavigate?.({ delta });
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const zoomOut = (level: Precision) => {
|
|
301
|
+
if (level === 'day') { setView('month'); onViewChange?.({ view: 'month' }); return; }
|
|
302
|
+
if (level === 'month') { setView('year'); onViewChange?.({ view: 'year' }); }
|
|
303
|
+
// year — already at top
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const hoverCell = (parsed: ParsedDate, disabled: boolean) => {
|
|
307
|
+
if (mode !== 'range' || !anchor || disabled) return;
|
|
308
|
+
// Same functional-set trick as source's same-hover check: returning
|
|
309
|
+
// the identical reference skips the re-render.
|
|
310
|
+
setHover((h) => (h && compareByPrecision(h, parsed, view) === 0 ? h : parsed));
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const rootMouseLeave = () => {
|
|
314
|
+
if (mode !== 'range' || !anchor) return;
|
|
315
|
+
setHover((h) => (h ? null : h));
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const rootKeyDown = (event: ReactKeyboardEvent<HTMLDivElement>) => {
|
|
319
|
+
const key = event.key;
|
|
320
|
+
if (key === 'Escape') {
|
|
321
|
+
if (anchor) {
|
|
322
|
+
setAnchor(null);
|
|
323
|
+
setHover(null);
|
|
324
|
+
onRangeCancel?.();
|
|
325
|
+
event.preventDefault();
|
|
326
|
+
}
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (key === 'PageUp') { navigate(-1); event.preventDefault(); return; }
|
|
330
|
+
if (key === 'PageDown') { navigate(1); event.preventDefault(); }
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// ── Panes ─────────────────────────────────────────────────────
|
|
334
|
+
const renderHead = (label: ReactNode, level: Precision, showPrev: boolean, showNext: boolean) => (
|
|
335
|
+
<div className="mojo-calendar-head">
|
|
336
|
+
<button
|
|
337
|
+
type="button"
|
|
338
|
+
className="mojo-calendar-head-label"
|
|
339
|
+
data-level={level}
|
|
340
|
+
onClick={() => zoomOut(level)}
|
|
341
|
+
>
|
|
342
|
+
{label}
|
|
343
|
+
</button>
|
|
344
|
+
<div className="mojo-calendar-nav">
|
|
345
|
+
{showPrev && (
|
|
346
|
+
<button type="button" className="mojo-calendar-nav-btn" aria-label="Previous"
|
|
347
|
+
onClick={() => navigate(-1)}>{'‹'}</button>
|
|
348
|
+
)}
|
|
349
|
+
{showNext && (
|
|
350
|
+
<button type="button" className="mojo-calendar-nav-btn" aria-label="Next"
|
|
351
|
+
onClick={() => navigate(1)}>{'›'}</button>
|
|
352
|
+
)}
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
const renderDayPane = (y: number, m: number, showPrev: boolean, showNext: boolean) => {
|
|
358
|
+
const firstWeekday = weekdayWithFirstDay(y, m, 1, firstDay);
|
|
359
|
+
const total = daysInMonth(y, m);
|
|
360
|
+
const todayP = today();
|
|
361
|
+
|
|
362
|
+
const cells: ReactNode[] = [];
|
|
363
|
+
for (let i = 0; i < firstWeekday; i++) {
|
|
364
|
+
cells.push(<div key={`b${i}`} className={`${CELL} ${CELL}-blank`} />);
|
|
365
|
+
}
|
|
366
|
+
for (let d = 1; d <= total; d++) {
|
|
367
|
+
const cur: ParsedYmd = { y, m, d };
|
|
368
|
+
const disabled = isDisabledAt(cur, 'day');
|
|
369
|
+
const classes = [CELL];
|
|
370
|
+
if (cur.y === todayP.y && cur.m === todayP.m && cur.d === todayP.d) classes.push(`${CELL}-today`);
|
|
371
|
+
if (disabled) classes.push(`${CELL}-disabled`);
|
|
372
|
+
if (mode === 'range') classes.push(...rangeClasses(cur, 'day'));
|
|
373
|
+
else if (selected && compareYmd(cur, selected) === 0) classes.push(`${CELL}-selected`);
|
|
374
|
+
cells.push(
|
|
375
|
+
<button
|
|
376
|
+
key={d}
|
|
377
|
+
type="button"
|
|
378
|
+
className={classes.join(' ')}
|
|
379
|
+
disabled={disabled}
|
|
380
|
+
data-ymd={formatYmd(cur)}
|
|
381
|
+
onClick={() => clickDay(cur)}
|
|
382
|
+
onMouseEnter={() => hoverCell(cur, disabled)}
|
|
383
|
+
>
|
|
384
|
+
<span className="mojo-calendar-cell-inner">{d}</span>
|
|
385
|
+
</button>,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const names = monthNames(locale, 'long');
|
|
390
|
+
return (
|
|
391
|
+
<div key={`${y}-${m}`} className="mojo-calendar-pane">
|
|
392
|
+
{renderHead(
|
|
393
|
+
<>{names[m - 1]} <span className="mojo-calendar-year">{y}</span></>,
|
|
394
|
+
'day', showPrev, showNext,
|
|
395
|
+
)}
|
|
396
|
+
<div className="mojo-calendar-grid mojo-calendar-grid-day">
|
|
397
|
+
{weekdayNames(locale, firstDay, 'short').map((w, i) => (
|
|
398
|
+
<div key={i} className="mojo-calendar-weekday">{w}</div>
|
|
399
|
+
))}
|
|
400
|
+
</div>
|
|
401
|
+
<div className="mojo-calendar-grid mojo-calendar-grid-day">{cells}</div>
|
|
402
|
+
</div>
|
|
403
|
+
);
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
const renderMonthPane = (y: number) => {
|
|
407
|
+
const names = monthNames(locale, 'short');
|
|
408
|
+
const cells: ReactNode[] = [];
|
|
409
|
+
for (let m = 1; m <= 12; m++) {
|
|
410
|
+
const cur: ParsedYm = { y, m };
|
|
411
|
+
const disabled = isDisabledAt(cur, 'month');
|
|
412
|
+
const classes = [CELL];
|
|
413
|
+
if (disabled) classes.push(`${CELL}-disabled`);
|
|
414
|
+
if (precision === 'month' && mode === 'range') classes.push(...rangeClasses(cur, 'month'));
|
|
415
|
+
else if (precision === 'month' && mode === 'single' && selected && compareYm(cur, selected) === 0) {
|
|
416
|
+
classes.push(`${CELL}-selected`);
|
|
417
|
+
}
|
|
418
|
+
cells.push(
|
|
419
|
+
<button
|
|
420
|
+
key={m}
|
|
421
|
+
type="button"
|
|
422
|
+
className={classes.join(' ')}
|
|
423
|
+
disabled={disabled}
|
|
424
|
+
data-ym={formatYm(cur)}
|
|
425
|
+
onClick={() => clickMonth(cur)}
|
|
426
|
+
onMouseEnter={() => hoverCell(cur, disabled)}
|
|
427
|
+
>
|
|
428
|
+
<span className="mojo-calendar-cell-inner">{names[m - 1]}</span>
|
|
429
|
+
</button>,
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
return (
|
|
433
|
+
<div className="mojo-calendar-pane">
|
|
434
|
+
{renderHead(String(y), 'month', true, true)}
|
|
435
|
+
<div className="mojo-calendar-grid mojo-calendar-grid-month">{cells}</div>
|
|
436
|
+
</div>
|
|
437
|
+
);
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const renderYearPane = (decade: number) => {
|
|
441
|
+
const cells: ReactNode[] = [];
|
|
442
|
+
for (let i = 0; i < 12; i++) {
|
|
443
|
+
const yr = decade + i;
|
|
444
|
+
const cur: ParsedDate = { y: yr };
|
|
445
|
+
const disabled = isDisabledAt(cur, 'year');
|
|
446
|
+
const classes = [CELL];
|
|
447
|
+
if (disabled) classes.push(`${CELL}-disabled`);
|
|
448
|
+
if (precision === 'year' && mode === 'range') classes.push(...rangeClasses(cur, 'year'));
|
|
449
|
+
else if (precision === 'year' && mode === 'single' && selected && selected.y === yr) {
|
|
450
|
+
classes.push(`${CELL}-selected`);
|
|
451
|
+
}
|
|
452
|
+
cells.push(
|
|
453
|
+
<button
|
|
454
|
+
key={yr}
|
|
455
|
+
type="button"
|
|
456
|
+
className={classes.join(' ')}
|
|
457
|
+
disabled={disabled}
|
|
458
|
+
data-year={String(yr)}
|
|
459
|
+
onClick={() => clickYear(yr)}
|
|
460
|
+
onMouseEnter={() => hoverCell(cur, disabled)}
|
|
461
|
+
>
|
|
462
|
+
<span className="mojo-calendar-cell-inner">{yr}</span>
|
|
463
|
+
</button>,
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
return (
|
|
467
|
+
<div className="mojo-calendar-pane">
|
|
468
|
+
{renderHead(`${decade} – ${decade + 11}`, 'year', true, true)}
|
|
469
|
+
<div className="mojo-calendar-grid mojo-calendar-grid-year">{cells}</div>
|
|
470
|
+
</div>
|
|
471
|
+
);
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
// ── Render ────────────────────────────────────────────────────
|
|
475
|
+
const dayPanes: ReactNode[] = [];
|
|
476
|
+
if (view === 'day') {
|
|
477
|
+
for (let i = 0; i < panes; i++) {
|
|
478
|
+
let y = page.y;
|
|
479
|
+
let m = page.m + i;
|
|
480
|
+
while (m > 12) { m -= 12; y += 1; }
|
|
481
|
+
dayPanes.push(renderDayPane(y, m, i === 0, i === panes - 1));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return (
|
|
486
|
+
<div
|
|
487
|
+
className={`mojo-calendar mojo-calendar-${precision}${className ? ` ${className}` : ''}`}
|
|
488
|
+
onMouseLeave={rootMouseLeave}
|
|
489
|
+
onKeyDown={rootKeyDown}
|
|
490
|
+
>
|
|
491
|
+
<div className={panes > 1 && view === 'day' ? 'mojo-calendar-multi' : 'mojo-calendar-single'}>
|
|
492
|
+
{view === 'day' && dayPanes}
|
|
493
|
+
{view === 'month' && renderMonthPane(page.y)}
|
|
494
|
+
{view === 'year' && renderYearPane(decadeStart(page.y))}
|
|
495
|
+
</div>
|
|
496
|
+
</div>
|
|
497
|
+
);
|
|
498
|
+
}
|