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,170 @@
|
|
|
1
|
+
# DateRangePicker + PresetRail
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
import { DateRangePicker, PresetRail } from 'portal-mojo/ui';
|
|
5
|
+
import type { DateRangeChangeEvent, PresetEntry, PresetsOption } from 'portal-mojo/ui';
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
The range picker built on the [Calendar engine](calendar.md) and the shared
|
|
9
|
+
[Popover](popover.md), with the "Quick range" preset rail. Ported from
|
|
10
|
+
web-mojo's `DateRangePicker.js` + `calendar/PresetSidebar.js`. One
|
|
11
|
+
component, three precisions: `'day'` (default, two side-by-side panes),
|
|
12
|
+
`'month'`, `'year'` — each with its own default preset list, display
|
|
13
|
+
format, and placeholder.
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
const [start, setStart] = useState('');
|
|
17
|
+
const [end, setEnd] = useState('');
|
|
18
|
+
|
|
19
|
+
<DateRangePicker
|
|
20
|
+
start={start}
|
|
21
|
+
end={end}
|
|
22
|
+
onChange={(e) => { setStart(e.start); setEnd(e.end); }}
|
|
23
|
+
/>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Value contract
|
|
27
|
+
|
|
28
|
+
**Controlled, canonical strings.** `start`/`end` are the engine's canonical
|
|
29
|
+
strings at precision — `YYYY-MM-DD` / `YYYY-MM` / `YYYY` — with `''`/null
|
|
30
|
+
as empty. Every commit (completed calendar range, preset pick, clear ✕)
|
|
31
|
+
emits `onChange` and paints only once the owner writes the props back:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
interface DateRangeChangeEvent {
|
|
35
|
+
start: string; // canonical at precision ('' when cleared)
|
|
36
|
+
end: string;
|
|
37
|
+
formatted: string; // display text of the NEW range (what the trigger shows)
|
|
38
|
+
oldStart: string; // the props at emit time
|
|
39
|
+
oldEnd: string;
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`onChange` fires only when the pair actually changes (re-picking the current
|
|
44
|
+
range emits nothing). There is no per-keystroke path — commits only.
|
|
45
|
+
|
|
46
|
+
## `<DateRangePicker>` API
|
|
47
|
+
|
|
48
|
+
| Prop | Notes |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `start` / `end` | Controlled canonical strings (or `''`/null). |
|
|
51
|
+
| `onChange` | `(DateRangeChangeEvent) => void` — see above. |
|
|
52
|
+
| `precision` | `'day' \| 'month' \| 'year'` (default `day`). Unknown values warn + fall back to `day`. |
|
|
53
|
+
| `months` | Day-view panes `1 \| 2`. Default **2 for day precision, 1 for month/year** (source rule). Clamped like the engine. |
|
|
54
|
+
| `presets` | `'default' \| true` → the precision's built-in list; `PresetEntry[]` → custom; absent/`false`/`[]` → no rail. |
|
|
55
|
+
| `displayFormat` | Display tokens (`YYYY YY MMMM MMM MM M DD D`). Defaults: day `MMM DD, YYYY`, month `MMM YYYY`, year `YYYY`. Tokens the precision can't honor are stripped (the source's `_stripIncompatibleTokens`, ported verbatim — trailing separator debris swept). |
|
|
56
|
+
| `separator` | Between the formatted ends. Default `' – '` (spaced en dash). |
|
|
57
|
+
| `placeholder` | Default per precision: `Select date range...` / `Select month range...` / `Select year range...`. |
|
|
58
|
+
| `min` / `max` | Canonical strings, passed straight to the Calendar (compared at the pane precision being painted). |
|
|
59
|
+
| `disabled` | Trigger disabled; popover can't open. |
|
|
60
|
+
| `readOnly` | Trigger shows the value but won't open; no clear ✕. |
|
|
61
|
+
| `required` | Hides the clear ✕ (the field can't be emptied from the trigger). |
|
|
62
|
+
| `inline` | Render rail + calendar in place — no trigger, no popover. |
|
|
63
|
+
| `autoApply` | Default `true`. See semantics below. |
|
|
64
|
+
| `className` / `id` | Root class / trigger button id. |
|
|
65
|
+
|
|
66
|
+
**`autoApply` means auto-CLOSE, not staged apply** (source semantics carried
|
|
67
|
+
exactly): values apply on every commit either way; `autoApply` only decides
|
|
68
|
+
whether completing a range or picking a preset also closes the popover.
|
|
69
|
+
For an explicit Apply step, mount `inline` inside a dialog and stage the
|
|
70
|
+
values in the dialog's own state — that is precisely what FilterBar does.
|
|
71
|
+
|
|
72
|
+
## Trigger anatomy
|
|
73
|
+
|
|
74
|
+
`bi-calendar3-range` icon + formatted text (`start – end`, tabular-nums,
|
|
75
|
+
ellipsized) or muted placeholder + clear ✕. The ✕ renders whenever the
|
|
76
|
+
picker is not `required`/`disabled`/`readOnly` and clears both ends in one
|
|
77
|
+
commit. It is an absolutely-positioned **sibling** button of the trigger —
|
|
78
|
+
web-mojo nested `<button>` inside `<button>`, invalid HTML; do not
|
|
79
|
+
reintroduce. A half-set range displays just the one formatted end
|
|
80
|
+
(deep-linked one-sided `dr_*` filters still render).
|
|
81
|
+
|
|
82
|
+
## Presets
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
type PresetsOption = 'default' | true | PresetEntry[];
|
|
86
|
+
type PresetEntry =
|
|
87
|
+
| { label: string; range: () => { start: ParsedDate | string; end: ParsedDate | string } }
|
|
88
|
+
| { divider: true };
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`range()` is resolved at click time (so "Today" is always today) and may
|
|
92
|
+
return parsed shapes (`{y,m,d}` / `{y,m}` / `{y}`) or canonical strings.
|
|
93
|
+
Default lists (verbatim from source; builders exported as
|
|
94
|
+
`dayPresets()` / `monthPresets()` / `yearPresets()` / `defaultPresets(p)`):
|
|
95
|
+
|
|
96
|
+
| Precision | List |
|
|
97
|
+
|---|---|
|
|
98
|
+
| day | Today · Yesterday · Last 7 days · Last 30 days · Last 90 days · ─ · This month · Last month · This year |
|
|
99
|
+
| month | This month · Last month · Last 3 months · Last 6 months · YTD · Last 12 months |
|
|
100
|
+
| year | This year · Last year · Last 3 years · Last 5 years · Last 10 years |
|
|
101
|
+
|
|
102
|
+
Rail behavior: "Quick range" eyebrow (`eyebrow` prop on `PresetRail`; `''`
|
|
103
|
+
hides it); the picked preset highlights and stays highlighted until a
|
|
104
|
+
**manual calendar anchor starts** (`onRangeStart` → highlight cleared —
|
|
105
|
+
source parity). Dividers consume an index, so `activeIndex` lines up with
|
|
106
|
+
the flat entry list. `<PresetRail>` is exported standalone (controlled
|
|
107
|
+
`activeIndex`, `onSelect` emits `{index, label, start, end, range}` with
|
|
108
|
+
canonical strings); the C2 custom-range dialog can reuse it.
|
|
109
|
+
|
|
110
|
+
## FilterBar integration (the dr_* wire triple)
|
|
111
|
+
|
|
112
|
+
FilterBar's daterange dialog mounts `<DateRangePicker inline months={1}
|
|
113
|
+
presets="default">` and stages `e.start`/`e.end` in dialog state; its
|
|
114
|
+
**Apply** button writes `params.setDateRange(field, start, end)` — the
|
|
115
|
+
`dr_field`/`dr_start`/`dr_end` triple — exactly as the old native date
|
|
116
|
+
inputs did. Same canonical `YYYY-MM-DD` values on the wire, same pills,
|
|
117
|
+
same deep links; only the dialog body changed. The params store remains the
|
|
118
|
+
single source of truth; the picker never touches it directly.
|
|
119
|
+
|
|
120
|
+
## Form-registry aliases (for the #1278 field registry)
|
|
121
|
+
|
|
122
|
+
Source `inputs/index.js` registers three aliases over this one component —
|
|
123
|
+
carry them when the registry lands:
|
|
124
|
+
|
|
125
|
+
| Field type | Component | `precision` |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| `daterange` | DateRangePicker | `day` |
|
|
128
|
+
| `monthrange` | DateRangePicker | `month` |
|
|
129
|
+
| `yearrange` | DateRangePicker | `year` |
|
|
130
|
+
|
|
131
|
+
(Explicit `precision` in the field options beats the alias, per source's
|
|
132
|
+
`createInput`.)
|
|
133
|
+
|
|
134
|
+
## Deviations from source (deliberate)
|
|
135
|
+
|
|
136
|
+
- **Controlled props replace the form-DOM bridge**: no hidden `<input>`s,
|
|
137
|
+
no `name`/`startName`/`endName`/`fieldName`, no `outputFormat`
|
|
138
|
+
(`combined` was `start + separator + end` — derive it if needed), no
|
|
139
|
+
`getFormValue`/`setFormValue`. The #1278 registry adapter owns form
|
|
140
|
+
wiring.
|
|
141
|
+
- **A half-picked anchor does not survive popover close/reopen.** The
|
|
142
|
+
anchor is Calendar-internal state and the popover unmounts its content on
|
|
143
|
+
close; web-mojo kept one live calendar element across opens. The
|
|
144
|
+
load-bearing behavior — the anchor surviving paging/zooming
|
|
145
|
+
mid-selection — is engine-owned and fully preserved; reopening shows the
|
|
146
|
+
committed range. Revisit only if the engine grows a controlled `anchor`
|
|
147
|
+
prop.
|
|
148
|
+
- **Escape closes the popover first** (Popover owns Escape in the capture
|
|
149
|
+
phase — one press, popover only; a second reaches an enclosing modal).
|
|
150
|
+
The engine's Escape-cancels-anchor applies in `inline` mode.
|
|
151
|
+
- `is-invalid` styling was not carried — source's `hasError()` was a stub
|
|
152
|
+
that always returned false.
|
|
153
|
+
|
|
154
|
+
## Pitfalls
|
|
155
|
+
|
|
156
|
+
- **Do not write `start`/`end` back from anything but `onChange`.** A range
|
|
157
|
+
prop write is `setRange` semantics to the engine — it cancels an
|
|
158
|
+
in-progress anchor (that is also why preset picks repaint correctly:
|
|
159
|
+
owner writes back, engine syncs).
|
|
160
|
+
- Presets are **not min/max-clamped** (source parity): a custom preset can
|
|
161
|
+
commit out-of-bounds values even though the grid cells paint disabled.
|
|
162
|
+
Clamp inside your `range()` if bounds matter.
|
|
163
|
+
- The popover body sizes itself (`180px` rail + fixed calendar width);
|
|
164
|
+
`inline` is fluid to its host. Two panes + rail ≈ 850px — don't mount
|
|
165
|
+
two-pane popovers inside narrow scroll containers, and keep dialogs on
|
|
166
|
+
`months={1}`.
|
|
167
|
+
- Styling lives in `apps/portal/src/theme/daterange.css` (trigger
|
|
168
|
+
`.mojo-daterange-*`, rail `.mojo-calendar-preset*`), tokens only, both
|
|
169
|
+
themes; the calendar grid ships in `calendar.css`, the popover shell in
|
|
170
|
+
`popover.css`.
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# DateTimePicker
|
|
2
|
+
|
|
3
|
+
The combined date **and** time field, ported from web-mojo
|
|
4
|
+
`forms/inputs/DateTimePicker.js` (523 lines) — the source's locked **variant
|
|
5
|
+
A**: one popover with the day [`Calendar`](calendar.md) on the left, the
|
|
6
|
+
embedded [`TimePicker`](timepicker.md) steppers on the right under a "Time"
|
|
7
|
+
heading, a full-width [`TimezoneSelect`](timezone-select.md) row below the
|
|
8
|
+
pair, and a Now/Done footer. **One field, one value** — the timezone belongs
|
|
9
|
+
to this picker, it is never a second field.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { DateTimePicker } from 'portal-mojo/ui';
|
|
13
|
+
import type {
|
|
14
|
+
DateTimeValue, DateTimeInput, DateTimeChangeEvent,
|
|
15
|
+
DateTimeObjectValue, DateTimeOutputFormat,
|
|
16
|
+
} from 'portal-mojo/ui';
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Value shapes (the contract)
|
|
20
|
+
|
|
21
|
+
Storage is **always canonical**: `YYYY-MM-DD` + 24h `HH:MM` + an optional IANA
|
|
22
|
+
zone. `displayFormat`/`timeFormat` only decide what the trigger renders;
|
|
23
|
+
`outputFormat` decides what leaves:
|
|
24
|
+
|
|
25
|
+
| `outputFormat` | with a zone | without a zone | empty |
|
|
26
|
+
|---|---|---|---|
|
|
27
|
+
| `'iso'` (default) | `'2026-07-04T14:30:00-07:00'` | `'2026-07-04T14:30:00'` | `''` |
|
|
28
|
+
| `'iana'` (legacy) | `'2026-07-04 14:30 America/Los_Angeles'` | `'2026-07-04 14:30'` | `''` |
|
|
29
|
+
| `'object'` | `{date:'2026-07-04', time:'14:30', timezone:'America/Los_Angeles'}` | `{date, time}` (no `timezone` key) | `null` |
|
|
30
|
+
|
|
31
|
+
`'object'` always yields an object when a date is set — unlike TimePicker, it
|
|
32
|
+
never degrades to a string. The `timezone` key exists (possibly `null`) exactly
|
|
33
|
+
when the zone row is enabled.
|
|
34
|
+
|
|
35
|
+
**The ISO offset is DST-correct at the value's own date.** It is computed with
|
|
36
|
+
`dateFns.ianaOffset(zone, <the selected instant>)`, so `America/Los_Angeles`
|
|
37
|
+
serializes `-07:00` on July 4 and `-08:00` on January 4. (`TimePicker` cannot
|
|
38
|
+
do this — a wall-clock time has no date, so it must resolve against
|
|
39
|
+
`new Date()`.)
|
|
40
|
+
|
|
41
|
+
**Everything above parses back in**, plus the looser shapes the source
|
|
42
|
+
accepted:
|
|
43
|
+
|
|
44
|
+
- date only — `'2026-05-04'` (time reads as `00:00`)
|
|
45
|
+
- 12h text — `'2026-05-04 2:30 PM'`, with or without an IANA tail
|
|
46
|
+
- ISO offsets in every spelling — `Z` → `+00:00`, `+0530` → `+05:30`
|
|
47
|
+
- `T` or a space between date and time
|
|
48
|
+
- loose numbers — `'2026-5-4 14:30'`
|
|
49
|
+
|
|
50
|
+
Two coverage **extensions over `dateFns.parseDateTime`**, both implemented in
|
|
51
|
+
`DateTimePicker.tsx` because the shared parser would otherwise drop data the
|
|
52
|
+
caller supplied:
|
|
53
|
+
|
|
54
|
+
1. an object with **no date keeps its `timezone`** (`parseDateTime` returns
|
|
55
|
+
`null` for the whole thing);
|
|
56
|
+
2. an offset separated from the time by a **space** —
|
|
57
|
+
`'2026-05-04 14:30 -07:00'` (`parseDateTime` reads an offset only when it is
|
|
58
|
+
glued to the time, and a zone *name* only after a space).
|
|
59
|
+
|
|
60
|
+
Anything else parses to empty **with one `console.warn` per distinct value**
|
|
61
|
+
(house rule 4) — never a silent blank.
|
|
62
|
+
|
|
63
|
+
## API
|
|
64
|
+
|
|
65
|
+
| Prop | Notes |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `value` | `string \| {date?, time?, timezone?} \| null` — see above. Omit entirely for an uncontrolled picker |
|
|
68
|
+
| `onChange` | `({value, formatted, oldValue})` — fires on **commit only**, and only when the serialized string actually changed |
|
|
69
|
+
| `displayFormat` | date half of the trigger text (`formatForDisplay` tokens). Default `'MMM DD, YYYY'` |
|
|
70
|
+
| `timeFormat` | `'24h'` (default) \| `'12h'`. Unknown values warn + fall back |
|
|
71
|
+
| `timeStep` | minute stepping increment for the time column, ≥1 (default 1) |
|
|
72
|
+
| `min` / `max` | a date (`'2026-05-04'`) **or** a datetime (`'2026-05-04 09:00'`) — see bounds below |
|
|
73
|
+
| `placeholder` | default `'Pick date & time...'` |
|
|
74
|
+
| `disabled` / `readOnly` / `required` / `invalid` | trigger states; `required`, `disabled` and `readOnly` each suppress the clear ✕ |
|
|
75
|
+
| `inline` | render the panel in place — no trigger, no popover, no Done |
|
|
76
|
+
| `disabledDates` | individually blocked days, `"YYYY-MM-DD"` |
|
|
77
|
+
| `firstDay` / `locale` | passed to the Calendar. Default `1` (Mon) / `'en-US'` |
|
|
78
|
+
| `timezone` | `true` enables the zone row; a `string[]` enables it **and is the zone list** |
|
|
79
|
+
| `timezones` | zone list when `timezone` is `true` (default: the engine's own list) |
|
|
80
|
+
| `outputFormat` | `'iso'` \| `'iana'` \| `'object'`. Unknown values warn + fall back |
|
|
81
|
+
| `renderTimezoneSelect` | the zone-picker slot — see below |
|
|
82
|
+
| `name` | renders a hidden input carrying the serialized string (native-form use) |
|
|
83
|
+
| `id` | trigger id, for a label's `htmlFor` |
|
|
84
|
+
| `className` | appended to the root |
|
|
85
|
+
|
|
86
|
+
`DateTimeChangeEvent.oldValue` is always the **previous canonical serialized
|
|
87
|
+
string** (`''` when empty), in every `outputFormat` — it is the diffing key,
|
|
88
|
+
not a mirror of `value`.
|
|
89
|
+
|
|
90
|
+
Types split in/out on purpose: `value` takes `DateTimeInput` (every part
|
|
91
|
+
optional), `onChange` hands back `DateTimeValue` (parts guaranteed). Writing
|
|
92
|
+
`event.value` straight back into `value` type-checks.
|
|
93
|
+
|
|
94
|
+
## Layout (the locked variant)
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
┌──────────────────────────────┬──────────────────┐
|
|
98
|
+
│ Calendar precision="day" │ TIME │
|
|
99
|
+
│ mode="single" months={1} │ ▲ ▲ [AM] │
|
|
100
|
+
│ │ 09 : 30 [PM] │
|
|
101
|
+
│ │ ▼ ▼ │
|
|
102
|
+
├──────────────────────────────┴──────────────────┤
|
|
103
|
+
│ TIMEZONE │
|
|
104
|
+
│ [ America/Los_Angeles (UTC−07:00) ▾ ] │
|
|
105
|
+
├─────────────────────────────────────────────────┤
|
|
106
|
+
│ Now [Done] │
|
|
107
|
+
└─────────────────────────────────────────────────┘
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The time column embeds `TimePicker inline showFooter={false} timezone={false}`
|
|
111
|
+
— the footerless variant that exists for exactly this. The zone row sits below
|
|
112
|
+
the pair, full width, so the IANA combobox has room for its labels. **Done only
|
|
113
|
+
closes the popover**; every edit committed the moment it was made.
|
|
114
|
+
|
|
115
|
+
## Behavior invariants (from source, keep them true)
|
|
116
|
+
|
|
117
|
+
- **Commit-only pipeline.** `onChange` fires on: a calendar pick, any
|
|
118
|
+
TimePicker commit (stepper, typed + Enter/blur, AM/PM), a zone pick, **Now**,
|
|
119
|
+
and the clear ✕. Typing in a time column changes nothing until it commits.
|
|
120
|
+
Re-committing the same value is silent.
|
|
121
|
+
- **Defaults that make one field out of two.** Picking a date with no time yet
|
|
122
|
+
→ time `00:00`. Committing a time with no date → date **today**, and the
|
|
123
|
+
calendar follows (it reads the same state). **Now** sets both.
|
|
124
|
+
- **min/max**: the DATE part bounds the calendar (out-of-range days are
|
|
125
|
+
disabled). Where the bound also spelled a TIME, that time clamps **on the
|
|
126
|
+
boundary day only** — `min="2026-05-04 09:00"` forbids 08:00 on May 4 and
|
|
127
|
+
nothing at all on May 5. `Now` is clamped through the same rule, so it can
|
|
128
|
+
never escape the window.
|
|
129
|
+
- **The clamp lives in two places on purpose.** The picker clamps every commit,
|
|
130
|
+
*and* hands the embedded TimePicker a day-scoped `min`/`max` so it clamps
|
|
131
|
+
itself. Without the second, the steppers would keep showing the pre-clamp
|
|
132
|
+
value the parent just rejected.
|
|
133
|
+
- **A cleared field keeps its zone** — the zone is a picker setting, not data
|
|
134
|
+
(source `clear()`).
|
|
135
|
+
- The trigger is `bi-calendar3` + `MMM DD, YYYY HH:MM [Zone]` + a clear ✕ that
|
|
136
|
+
appears on hover/focus (suppressed by `required`/`disabled`/`readOnly`).
|
|
137
|
+
|
|
138
|
+
## The timezone seam
|
|
139
|
+
|
|
140
|
+
The picker owns every bit of the zone's value and serialization logic but
|
|
141
|
+
renders the picker itself through a slot. Its **default tenant is the real
|
|
142
|
+
`<TimezoneSelect>`**, so `timezone` alone gives you the full searchable,
|
|
143
|
+
offset-labelled zone list:
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
<DateTimePicker timezone value={value} onChange={(e) => setValue(e.value)} />
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Override only to change the rendering (a shorter list, a different control):
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
<DateTimePicker
|
|
153
|
+
timezone
|
|
154
|
+
renderTimezoneSelect={({ value, onChange, timezones, disabled }) => (
|
|
155
|
+
<TimezoneSelect value={value} timezones={timezones ?? null} disabled={disabled}
|
|
156
|
+
onChange={(e) => onChange(e.value)} />
|
|
157
|
+
)}
|
|
158
|
+
/>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The slot contract is `TimezoneSelectSlotProps` — the same one `TimePicker`
|
|
162
|
+
uses, so a slot written for one drops into the other. Enabling `timezone`
|
|
163
|
+
seeds the browser's local zone **without emitting** (source parity); the owner
|
|
164
|
+
learns it on the first real commit.
|
|
165
|
+
|
|
166
|
+
## State ownership (read this before wiring a form)
|
|
167
|
+
|
|
168
|
+
The picker holds its **own committed state**, seeded from `value` and re-seeded
|
|
169
|
+
whenever `value` changes to something that is not an echo of its own last emit
|
|
170
|
+
— the same shape [`Calendar`](calendar.md) and [`TimePicker`](timepicker.md)
|
|
171
|
+
use. Write `event.value` straight back into `value` and the two stay in
|
|
172
|
+
lockstep; omit `value` entirely and it behaves as an uncontrolled picker.
|
|
173
|
+
|
|
174
|
+
Why not derive purely from props: `'iso'` output carries only the **offset**,
|
|
175
|
+
so re-parsing our own echo would silently degrade `'America/Los_Angeles'` →
|
|
176
|
+
`'-07:00'`. The zone therefore lives in component state, and the echo guard is
|
|
177
|
+
what protects it. Corollary: **writing back the same value is a no-op, and
|
|
178
|
+
writing a different one re-seeds the panel** — to refuse a value, write the
|
|
179
|
+
value you *do* want.
|
|
180
|
+
|
|
181
|
+
## The wire reality (epoch seconds)
|
|
182
|
+
|
|
183
|
+
django-mojo model datetimes are **epoch SECONDS** on the wire — this component
|
|
184
|
+
never speaks them. Its contract is the string/object shapes above; the
|
|
185
|
+
**field-type registry (#1278) owns the save-boundary conversion** in both
|
|
186
|
+
directions:
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
wire (int seconds) ──▶ registry: seconds → 'YYYY-MM-DDTHH:MM:00±HH:MM' ──▶ value
|
|
190
|
+
value ──▶ registry: parse + Date.getTime()/1000 ──▶ wire (int seconds)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Passing an epoch number (or its digits as a string) as `value` parses to empty
|
|
194
|
+
with the usual warn — that is the registry's bug to catch, not a shape this
|
|
195
|
+
picker guesses at. When the zone matters to the *server*, prefer `'iana'` or
|
|
196
|
+
`'object'` output: an offset is a fact about one instant, an IANA name is a
|
|
197
|
+
fact about a place. And remember `fmt.*` accepts epoch seconds directly — do
|
|
198
|
+
not `new Date(epochSeconds)` without `× 1000`.
|
|
199
|
+
|
|
200
|
+
## Deviations from source (deliberate)
|
|
201
|
+
|
|
202
|
+
1. **No-op commits are silent, and `oldValue` is real.** web-mojo called
|
|
203
|
+
`_syncOutputs()` with no argument on nearly every path, which emitted
|
|
204
|
+
`change` *every time* with `oldValue: null` — even when nothing changed.
|
|
205
|
+
Here `onChange` fires only on a real change and always carries the previous
|
|
206
|
+
serialized string.
|
|
207
|
+
2. **Time clamping on the boundary day** (the source only date-bounded the
|
|
208
|
+
calendar, so a `09:00`–`17:30` window was unenforceable on the first and
|
|
209
|
+
last day). A date-only bound still clamps nothing but the date.
|
|
210
|
+
3. **`Now` is clamped** like every other commit path.
|
|
211
|
+
4. **An already-offset zone passes through** `'iso'` serialization
|
|
212
|
+
(`'…-07:00'` re-saves as `'…-07:00'`). `ianaOffset('-07:00')` returns
|
|
213
|
+
`null`, so the source dropped it on re-save.
|
|
214
|
+
5. **The clear ✕ is a sibling of the trigger**, not nested inside it (the
|
|
215
|
+
source produced a `<button>` inside a `<button>`).
|
|
216
|
+
6. **`inline` omits Done** — it exists to dismiss the popover and there is
|
|
217
|
+
none. The source rendered a dead button.
|
|
218
|
+
7. `disabled`/`readOnly` are enforced at the commit boundary, so a locked
|
|
219
|
+
`inline` panel is inert (the source only guarded the popover).
|
|
220
|
+
8. The dead `format` and `autoApply` options (stored, never read in source)
|
|
221
|
+
are not carried, and there is no `hasError()` stub — pass `invalid`.
|
|
222
|
+
|
|
223
|
+
## Styling
|
|
224
|
+
|
|
225
|
+
`apps/portal/src/theme/datetimepicker.css` — layout glue only
|
|
226
|
+
(`mojo-datetime-*`: `trigger` / `trigger-text` / `trigger-clear` /
|
|
227
|
+
`popover-inner` / `row` / `cal-col` / `time-col` / `time-head` / `tz-row` /
|
|
228
|
+
`tz-label` / `tz-host` / `foot`), tokens only, both themes. The grid, the
|
|
229
|
+
steppers, the combobox and the floating shell are already styled by
|
|
230
|
+
`calendar.css`, `timepicker.css`, `combobox.css` and `popover.css`; this file
|
|
231
|
+
adds the two-column popover (520px floor, stacked under 620px), strips the
|
|
232
|
+
embedded TimePicker's card chrome, and gives the trigger the house `.input`
|
|
233
|
+
look so it lines up with DatePicker/TimePicker in a form grid.
|
|
234
|
+
|
|
235
|
+
## Pitfalls
|
|
236
|
+
|
|
237
|
+
- Don't read the time columns to get the value — they may hold an uncommitted
|
|
238
|
+
draft. `onChange`/`value` is the only value channel.
|
|
239
|
+
- `'object'` output is `null` when empty, `''` for every other `outputFormat`.
|
|
240
|
+
Check both if you switch formats at runtime.
|
|
241
|
+
- `min`/`max` are **not** a general datetime window: the time half only applies
|
|
242
|
+
on the boundary day (that is what the calendar can express). A window like
|
|
243
|
+
"May 4 09:00 → May 8 17:30" is honored at both ends and unrestricted between
|
|
244
|
+
them.
|
|
245
|
+
- With the zone row open, **Escape closes the whole picker**, not just the
|
|
246
|
+
combobox dropdown — the shared Popover claims Escape in the capture phase.
|
|
247
|
+
- The zone row's dropdown is locally positioned and can extend past the
|
|
248
|
+
popover's edge; that is intentional (`.mojo-popover` is `overflow: visible`)
|
|
249
|
+
and matches web-mojo.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# defineModel — typed model definitions + hooks
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
import { defineModel, type ModelDef, type ActionOutcome } from 'portal-mojo/client';
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
A model is a **stateless definition** — endpoint + permissions + forms +
|
|
8
|
+
actions — plus hooks bound to it. TanStack Query owns all data; there are
|
|
9
|
+
no model instances (web-mojo's `Model`/`Collection` classes end here).
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
export const UserModel = defineModel<User>({
|
|
13
|
+
name: 'user',
|
|
14
|
+
endpoint: '/api/user',
|
|
15
|
+
permissions: { view: ['users', 'view_users'], manage: ['users', 'manage_users'] },
|
|
16
|
+
forms: { create: { title: 'Add user', submitText: 'Create', fields: [/* Field[] */] } },
|
|
17
|
+
actions: {
|
|
18
|
+
disable: { permissions: ['users', 'manage_users'] }, // response: 'row' (default)
|
|
19
|
+
revoke_sessions: { response: 'payload' }, // action-specific body
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`T` must extend `{ id: number | string }`.
|
|
25
|
+
|
|
26
|
+
## Hooks & helpers on the returned def
|
|
27
|
+
|
|
28
|
+
| Member | Behavior |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `useList(params?, {enabled?})` | `useQuery` list under key `[endpoint, params]` with `keepPreviousData`; `enabled:false` suppresses permission-hidden background reads |
|
|
31
|
+
| `useOne(id \| null)` | one record, key `[endpoint, 'one', id]`; disabled while `id` is null |
|
|
32
|
+
| `useSave()` | mutation `{id, changes}`; `id: null` creates. Handles one fresh-auth 440/retry, then writes the returned row into the one-record cache and invalidates `[endpoint]`. REJECTS on failure |
|
|
33
|
+
| `useDelete()` | mutation `{id}`; removes the one-record cache entry + invalidates |
|
|
34
|
+
| `useAction(name)` | mutation for ONE declared POST_SAVE_ACTION (below), with one fresh-auth 440/retry. THROWS at render time for an undeclared name |
|
|
35
|
+
| `fetchOne(queryClient, id)` | imperative fetch through the same cache key — dedupes with mounted `useOne`s (prefetch pattern) |
|
|
36
|
+
| `invalidate(queryClient)` | invalidate everything under `[endpoint]` |
|
|
37
|
+
| `keys.root / keys.list(params) / keys.one(id)` | key builders for targeted cache surgery |
|
|
38
|
+
|
|
39
|
+
Cache keys are shared with the generic hooks (`useModelList`, `useModel`),
|
|
40
|
+
so `ModelTable` and model hooks read/invalidate one cache.
|
|
41
|
+
|
|
42
|
+
Save/action mutation functions use `withFreshAuth` globally. Their original
|
|
43
|
+
variables and declared response mode survive the retry, and cache updates run
|
|
44
|
+
only on final success. Delete remains unwrapped, as do direct `mojoCall`
|
|
45
|
+
mutations; callers opt those in only when the endpoint is both sensitive and
|
|
46
|
+
safe to retry once.
|
|
47
|
+
|
|
48
|
+
## POST_SAVE_ACTIONS
|
|
49
|
+
|
|
50
|
+
Backend contract (`mojo/models/rest.py on_rest_save`): an action is a key
|
|
51
|
+
in the save body — `POST endpoint/<id>` `{disable: {reason, note}}`. Plain
|
|
52
|
+
fields in the same body save first, then `on_action_<key>(value)` runs.
|
|
53
|
+
The response is either the refreshed row (handler returned nothing) or an
|
|
54
|
+
action-specific payload. **Which one is DECLARED, never sniffed:**
|
|
55
|
+
|
|
56
|
+
- `response: 'row'` (default) → `ActionOutcome.row` is the refreshed record
|
|
57
|
+
(written through to the cache).
|
|
58
|
+
- `response: 'payload'` → `row` is null; read `outcome.body` (e.g.
|
|
59
|
+
`body.message` for `revoke_sessions` — toast the server's text).
|
|
60
|
+
|
|
61
|
+
Invoke: `await disableAction.mutateAsync({ id, payload: { reason: 'abuse' } })`.
|
|
62
|
+
`payload` defaults to `{}`; handlers treat the value as a dict. Errors
|
|
63
|
+
(e.g. `"reason must be one of: abuse, admin"`) REJECT with the server's
|
|
64
|
+
message.
|
|
65
|
+
|
|
66
|
+
## Pitfalls
|
|
67
|
+
|
|
68
|
+
- Don't build a second store around these hooks — pages read query state.
|
|
69
|
+
- The action-name union isn't typed; an undeclared name throws in dev the
|
|
70
|
+
first time the hook renders (fail loud, not a 404 later).
|
|
71
|
+
- UI `permissions` on the def are affordance gates for `useCan`; the server
|
|
72
|
+
re-checks every request.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Detail primitives — the record-page building blocks
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
import {
|
|
5
|
+
StatusPanel, FlowStrip, Timeline, KnownFieldsCard, MetadataSection, StackTraceView,
|
|
6
|
+
} from 'portal-mojo/ui';
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Six small pieces admin record pages are composed from, ported from
|
|
10
|
+
web-mojo's `src/core/views/data/*` primitives plus its admin metadata
|
|
11
|
+
section. They are presentational and stateless (except `MetadataSection`,
|
|
12
|
+
which writes) — drop them inside a `DetailView` section, a `panel`, or a
|
|
13
|
+
`modal.detail`. Styles: `apps/portal/src/theme/detail-primitives.css`.
|
|
14
|
+
|
|
15
|
+
**All three trusted-HTML slots are gone.** web-mojo rendered
|
|
16
|
+
`StatusPanel.meta`, `FlowStrip.value/hint` and `Timeline.detail` as
|
|
17
|
+
trusted HTML with a "the caller must escape interpolations" contract.
|
|
18
|
+
Here they are `ReactNode` props — compose `<code>`/`<strong>`/`<a>`
|
|
19
|
+
directly and the escaping question never arises (architecture rule 6).
|
|
20
|
+
|
|
21
|
+
## StatusPanel — "what state is this in, what do I do next"
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
<StatusPanel
|
|
25
|
+
tone="danger" state="Firing" icon="bi-fire" // icon replaces the tone dot
|
|
26
|
+
headline="Auth brute-force from 203.0.113.0/24"
|
|
27
|
+
meta={<>Triggered <code>4m ago</code> · 218 events</>}
|
|
28
|
+
actions={<><button className="btn btn-primary btn-compact">Acknowledge</button></>}
|
|
29
|
+
/>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The hero banner that opens an Overview section. Tone tints the surface and
|
|
33
|
+
colors the state eyebrow + dot. `actions` is a slot — the caller owns the
|
|
34
|
+
handlers (web-mojo dispatched `data-action` through a global pipeline).
|
|
35
|
+
Container queries stack the actions under the headline below 480px of the
|
|
36
|
+
panel's OWN width, so it behaves inside an `lg` modal.
|
|
37
|
+
|
|
38
|
+
## FlowStrip — STEP 1 → N
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
<FlowStrip
|
|
42
|
+
steps={[
|
|
43
|
+
{ title: 'Match', tone: 'primary', value: <code>category=auth</code>, hint: 'Every condition must match.' },
|
|
44
|
+
{ title: 'Re-trigger', empty: true, value: 'Not configured' },
|
|
45
|
+
]}
|
|
46
|
+
onEditStep={(step, i) => openTab(step, i)}
|
|
47
|
+
/>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
| Step field | Effect |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `num` | eyebrow; defaults to `STEP <n>` |
|
|
53
|
+
| `tone` | 2px cap over the column + colored eyebrow (which stage failed) |
|
|
54
|
+
| `empty` | renders `value` muted-italic ("not configured") |
|
|
55
|
+
| `editable: false` | opts one step out of the pencil |
|
|
56
|
+
| `editTitle` | pencil tooltip; defaults to `Edit <title>` |
|
|
57
|
+
|
|
58
|
+
Column count rides the inline `--flow-strip-cols` custom property; the CSS
|
|
59
|
+
container queries reflow 4 → 2 → 1 off the strip's own width. Empty
|
|
60
|
+
`steps` renders nothing.
|
|
61
|
+
|
|
62
|
+
## Timeline — the vertical event feed
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
<Timeline items={[{ tone: 'danger', title: 'Incident opened', meta: fmt.relative(ev.at), body: <>…</> }]} limit={5} />
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`{tone, title, meta, body}` — source names were `headline/when/detail`.
|
|
69
|
+
`meta` is the right-aligned monospace timestamp column: format it yourself
|
|
70
|
+
(`fmt.relative` / `fmt.datetime`, both epoch-SECONDS aware). `limit` slices
|
|
71
|
+
to the first N ("recent activity" mode). The empty state renders inside the
|
|
72
|
+
`<ol>` so the rail stays visually intact.
|
|
73
|
+
|
|
74
|
+
## KnownFieldsCard — promoted keys + raw blob
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
<KnownFieldsCard
|
|
78
|
+
data={record.metadata ?? {}}
|
|
79
|
+
known={[
|
|
80
|
+
{ key: 'created_by', label: 'Created by' },
|
|
81
|
+
{ key: 'last_resolved', label: 'Resolved', format: 'datetime' }, // an fmt.* name
|
|
82
|
+
{ key: 'os.family', label: 'OS', format: (v, key, data) => <code>{String(v)}</code> },
|
|
83
|
+
{ key: 'agent_prompt', hideEmpty: true },
|
|
84
|
+
]}
|
|
85
|
+
rawLabel="Raw metadata"
|
|
86
|
+
/>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- `key` is a **dotted path** (`os.family`); an own property of that literal
|
|
90
|
+
name wins over traversal.
|
|
91
|
+
- `format` is an `fmt.*` name (`date` | `datetime` | `relative`) or a
|
|
92
|
+
function returning any node. An unknown name **warns and renders the raw
|
|
93
|
+
value** — never nothing (rule 4).
|
|
94
|
+
- Missing values render a muted `—`; `hideEmpty` drops the row instead.
|
|
95
|
+
- Objects with no formatter render as compact JSON in a `<code>`.
|
|
96
|
+
- The raw blob goes in a collapsed `<details>` below (`showRaw={false}` to
|
|
97
|
+
drop it, `rawCollapsed={false}` to start open). Skipped for an empty blob.
|
|
98
|
+
|
|
99
|
+
**Not to be confused with `DataView`** ([dataview.md](dataview.md)):
|
|
100
|
+
KnownFieldsCard takes one **blob** column and promotes a **curated** list of
|
|
101
|
+
known keys, leaving the rest in the raw `<details>`; DataView takes a
|
|
102
|
+
**whole record** and **infers** a renderer for every key from its name and
|
|
103
|
+
value. They coexist happily — `<DataView data={record} />` for the record,
|
|
104
|
+
`<KnownFieldsCard data={record.metadata} known={…} />` for its blob.
|
|
105
|
+
|
|
106
|
+
## MetadataSection — editable key/value CRUD (writes)
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
<MetadataSection
|
|
110
|
+
endpoint="/api/user" id={user.id} metadata={user.metadata}
|
|
111
|
+
onSaved={(next) => qc.setQueryData<User>(['/api/user', 'one', user.id], (p) => p && { ...p, metadata: next })}
|
|
112
|
+
/>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Wire contract:** every mutation is one `POST <endpoint>/<id>` with body
|
|
116
|
+
`{metadata: {…the whole blob…}}` through `mojoCall` — the same single
|
|
117
|
+
envelope-unwrap boundary as everything else. Add, edit and delete all take
|
|
118
|
+
this one path; there is no per-key endpoint.
|
|
119
|
+
|
|
120
|
+
Invariants:
|
|
121
|
+
|
|
122
|
+
1. **Controlled on `metadata`; `onSaved` is REQUIRED.** The component never
|
|
123
|
+
mirrors server state (rule 5) — wire `onSaved` into the TanStack cache or
|
|
124
|
+
parent state or the UI will not move after a save.
|
|
125
|
+
2. **A rejected save is unmissable** (rule 3). `mojoCall` REJECTS; the
|
|
126
|
+
server's own message lands in a persistent inline banner AND the editor
|
|
127
|
+
stays open holding what you typed. web-mojo read `resp.status === 200`
|
|
128
|
+
off a never-rejecting `model.save()` and fired a generic toast — that is
|
|
129
|
+
exactly the trap this port retires. Editing is therefore inline, not a
|
|
130
|
+
modal: a closed modal has no editor state to keep.
|
|
131
|
+
3. **Values are JSON-parsed when they can be** (source semantics): `42`,
|
|
132
|
+
`true` and `{"a":1}` land as real types, everything else stays a string.
|
|
133
|
+
Object values then display **read-only** — a string editor round-trips
|
|
134
|
+
them lossily; remove and re-add to change one.
|
|
135
|
+
|
|
136
|
+
Deletion confirms through `modal.confirm`; success toasts.
|
|
137
|
+
|
|
138
|
+
## StackTraceView — JS **and** Python tracebacks
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
<StackTraceView trace={job.stack_trace} collapseAfter={24} />
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
One parser, both dialects, in this match order (ported verbatim):
|
|
145
|
+
`at fn (file:line:col)` → `at file:line:col` → `File "…", line N, in fn` →
|
|
146
|
+
any `at …` line → context. Non-string traces are JSON-pretty-printed first.
|
|
147
|
+
|
|
148
|
+
- **Re-themed.** The source shipped a hardcoded light-only `<style>` block
|
|
149
|
+
(`#f8f9fa` surface, `#dc3545`/`#0d6efd`/`#6610f2` text) that was
|
|
150
|
+
unreadable in dark mode. This renders semantic classes over tokens.
|
|
151
|
+
- **The exception line is found anywhere.** The source only colored line 0,
|
|
152
|
+
so every Python traceback — which opens with `Traceback (most recent call
|
|
153
|
+
last):` and puts the exception LAST — rendered its most important line as
|
|
154
|
+
plain context. An anchored `^[A-Za-z_][\w.]*(Error|Exception|…)` pattern
|
|
155
|
+
now catches it, including dotted ones (`django.db.utils.IntegrityError`).
|
|
156
|
+
- Frame indentation is preserved so Python frames stay aligned with their
|
|
157
|
+
source lines, terminal-style.
|
|
158
|
+
- Traces longer than `collapseAfter` (default 24) collapse behind a "Show
|
|
159
|
+
all N lines" toggle; `0` disables collapsing. The body scroll-caps at
|
|
160
|
+
600px either way.
|
|
161
|
+
|
|
162
|
+
## Pitfalls
|
|
163
|
+
|
|
164
|
+
- `tone` is portal-mojo's 6-value `Tone` (`success | warning | danger |
|
|
165
|
+
info | muted | primary`), not web-mojo's 7 — `default`/`secondary` both
|
|
166
|
+
map to `muted`. Tones arriving as wire data go through a runtime guard:
|
|
167
|
+
unknown values fall back to `muted` **with a `console.warn`**.
|
|
168
|
+
- Timestamps are never formatted for you. django-mojo sends epoch SECONDS;
|
|
169
|
+
pass them through `fmt.*` before handing them to `Timeline.meta`.
|
|
170
|
+
- `KnownFieldsCard` and `MetadataSection` both render a `metadata` blob and
|
|
171
|
+
are easy to confuse: the card is read-only with promoted known keys; the
|
|
172
|
+
section is the editor. A detail page often wants both. `DataView` is the
|
|
173
|
+
third member of that family — whole-record inference rather than a
|
|
174
|
+
curated key list over one blob.
|