@ghxstship/xos-ui 0.0.0-stage → 4.16.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 +21 -0
- package/README.md +32 -2
- package/package.json +76 -4
- package/scripts/generate-stories.mjs +85 -0
- package/src/AIDisclosureBadge/README.md +7 -0
- package/src/AccessGridMatrix/README.md +3 -0
- package/src/Accordion/README.md +7 -0
- package/src/ActivityFeedItem/README.md +3 -0
- package/src/AgencySlate/README.md +3 -0
- package/src/AppShell/README.md +5 -0
- package/src/ApplicationForm/README.md +3 -0
- package/src/AssertionRankBadge/README.md +5 -0
- package/src/AvailabilityCalendar/README.md +3 -0
- package/src/Avatar/Avatar.tsx +56 -0
- package/src/Avatar/README.md +3 -0
- package/src/Avatar/avatar-class.ts +19 -0
- package/src/AvatarStack/README.md +3 -0
- package/src/Badge/README.md +3 -0
- package/src/Banner/README.md +3 -0
- package/src/BidSheet/README.md +3 -0
- package/src/Board/README.md +6 -0
- package/src/BrandMark/BrandMark.tsx +95 -0
- package/src/BrandMark/README.md +7 -0
- package/src/BrandSlot/README.md +6 -0
- package/src/Breadcrumbs/README.md +3 -0
- package/src/BudgetGrid/README.md +10 -0
- package/src/BulkActionBar/README.md +3 -0
- package/src/Button/Button.tsx +100 -0
- package/src/Button/README.md +10 -0
- package/src/Button/button-class.ts +54 -0
- package/src/ButtonLink/README.md +7 -0
- package/src/Calendar/README.md +3 -0
- package/src/Card/README.md +3 -0
- package/src/Chart/README.md +9 -0
- package/src/Charts/Calendar.tsx +228 -0
- package/src/Charts/Chart.tsx +547 -0
- package/src/Charts/Charts.tsx +20 -0
- package/src/Charts/Charts.types.ts +40 -0
- package/src/Charts/Gallery.tsx +25 -0
- package/src/Charts/Heatmap.tsx +116 -0
- package/src/Charts/OrgChart.tsx +53 -0
- package/src/Charts/ResourceSchedule.tsx +91 -0
- package/src/Charts/Sparkline.tsx +59 -0
- package/src/Charts/Timeline.tsx +777 -0
- package/src/Charts/categories.ts +21 -0
- package/src/Charts/timeline-plan.ts +191 -0
- package/src/Checkbox/README.md +6 -0
- package/src/ChecklistRunner/README.md +3 -0
- package/src/Chips/Badge.tsx +27 -0
- package/src/Chips/Chips.shared.tsx +34 -0
- package/src/Chips/Chips.tsx +21 -0
- package/src/Chips/Chips.types.ts +35 -0
- package/src/Chips/DepartmentChip.tsx +16 -0
- package/src/Chips/PhaseChip.tsx +16 -0
- package/src/Chips/PropertyChip.tsx +15 -0
- package/src/Chips/RecordKindChip.tsx +14 -0
- package/src/Chips/StateChip.tsx +19 -0
- package/src/Chips/Tag.tsx +47 -0
- package/src/Chips/URIDChip.tsx +14 -0
- package/src/Chips/chipClass.tsx +8 -0
- package/src/Chips/codeClass.tsx +6 -0
- package/src/ClockButton/README.md +6 -0
- package/src/CodeSample/README.md +7 -0
- package/src/ColorPicker/README.md +5 -0
- package/src/Combobox/README.md +7 -0
- package/src/CommandMenu/CommandMenu.tsx +179 -0
- package/src/CommandMenu/README.md +5 -0
- package/src/ComparisonTable/README.md +9 -0
- package/src/Compass/ChecklistRunner.tsx +128 -0
- package/src/Compass/ClockButton.tsx +57 -0
- package/src/Compass/Compass.shared.tsx +58 -0
- package/src/Compass/Compass.tsx +26 -0
- package/src/Compass/Compass.types.ts +79 -0
- package/src/Compass/DaySheetView.tsx +104 -0
- package/src/Compass/ForceUpdateScreen.tsx +29 -0
- package/src/Compass/LiveActivity.tsx +60 -0
- package/src/Compass/OfflineBanner.tsx +31 -0
- package/src/Compass/PhotoCapture.tsx +69 -0
- package/src/Compass/QuickIncidentSheet.tsx +46 -0
- package/src/Compass/ScanSheet.tsx +83 -0
- package/src/Compass/ShiftCard.tsx +55 -0
- package/src/Compass/SignaturePad.tsx +71 -0
- package/src/Compass/SyncConflictSheet.tsx +60 -0
- package/src/ConsentBanner/ConsentBanner.tsx +210 -0
- package/src/ConsentBanner/ConsentBannerStatic.tsx +163 -0
- package/src/ConsentBanner/ConsentSettings.tsx +213 -0
- package/src/ConsentBanner/README.md +101 -0
- package/src/ConsentBanner/consent.ts +238 -0
- package/src/ConsentBanner/index.ts +40 -0
- package/src/ConsentBanner/static.ts +11 -0
- package/src/ConsentBanner/useConsent.ts +160 -0
- package/src/ContextMenu/README.md +3 -0
- package/src/CoordinateMatrix/README.md +6 -0
- package/src/CredentialBadge/README.md +7 -0
- package/src/CurrencyInput/README.md +7 -0
- package/src/DataTable/DataTable.tsx +1034 -0
- package/src/DataTable/DataTableResizer.tsx +72 -0
- package/src/DataTable/DataTableVirtualizer.tsx +58 -0
- package/src/DataTable/README.md +9 -0
- package/src/DatePicker/README.md +5 -0
- package/src/DateRangePicker/README.md +3 -0
- package/src/DaySheetView/README.md +3 -0
- package/src/DepartmentChip/README.md +3 -0
- package/src/DepartmentGlyph/README.md +18 -0
- package/src/Dialog/README.md +3 -0
- package/src/DiffViewer/README.md +3 -0
- package/src/Divider/README.md +3 -0
- package/src/Drawer/README.md +3 -0
- package/src/DropdownMenu/README.md +3 -0
- package/src/EPKViewer/README.md +3 -0
- package/src/Editing/ColorPicker.tsx +125 -0
- package/src/Editing/DiffViewer.tsx +49 -0
- package/src/Editing/Editing.tsx +29 -0
- package/src/Editing/Editing.types.ts +71 -0
- package/src/Editing/FilterBuilder.tsx +72 -0
- package/src/Editing/QuickAdd.tsx +102 -0
- package/src/Editing/QuickAddChips.tsx +180 -0
- package/src/Editing/contrastRatio.tsx +11 -0
- package/src/Editing/parseQuickAdd.tsx +187 -0
- package/src/Editing/quickAddCore.ts +204 -0
- package/src/Editing/timeShorthand.ts +157 -0
- package/src/Editing/useQuickAdd.ts +201 -0
- package/src/EmergencyCodeCard/README.md +10 -0
- package/src/EmptyState/README.md +5 -0
- package/src/EngagementTimeline/README.md +3 -0
- package/src/ErrorState/README.md +3 -0
- package/src/FAQ/README.md +7 -0
- package/src/FeatureGrid/README.md +7 -0
- package/src/Feedback/Banner.tsx +40 -0
- package/src/Feedback/EmptyState.tsx +32 -0
- package/src/Feedback/ErrorState.tsx +32 -0
- package/src/Feedback/Feedback.shared.tsx +38 -0
- package/src/Feedback/Feedback.tsx +18 -0
- package/src/Feedback/Feedback.types.ts +40 -0
- package/src/Feedback/InlineAlert.tsx +19 -0
- package/src/Feedback/RefusalNotice.tsx +46 -0
- package/src/Feedback/RegionBoundary.tsx +102 -0
- package/src/Feedback/Toast.tsx +31 -0
- package/src/Feedback/ToastRegion.tsx +26 -0
- package/src/Feedback/UndoToast.tsx +97 -0
- package/src/FeedbackWidget/README.md +3 -0
- package/src/Field/Field.tsx +81 -0
- package/src/FileDrop/README.md +3 -0
- package/src/FilePreviewer/README.md +3 -0
- package/src/FilterBuilder/README.md +3 -0
- package/src/ForceUpdateScreen/README.md +3 -0
- package/src/Forms/Checkbox.tsx +14 -0
- package/src/Forms/CurrencyInput.tsx +40 -0
- package/src/Forms/Forms.shared.tsx +5 -0
- package/src/Forms/Forms.tsx +20 -0
- package/src/Forms/Forms.types.ts +33 -0
- package/src/Forms/Input.tsx +28 -0
- package/src/Forms/Money.tsx +15 -0
- package/src/Forms/NumberInput.tsx +85 -0
- package/src/Forms/Radio.tsx +31 -0
- package/src/Forms/Select.tsx +54 -0
- package/src/Forms/Switch.tsx +14 -0
- package/src/Forms/Textarea.tsx +30 -0
- package/src/Gallery/README.md +5 -0
- package/src/GateLifecycleRail/README.md +7 -0
- package/src/GateReadinessPanel/README.md +8 -0
- package/src/Gateway/AgencySlate.tsx +76 -0
- package/src/Gateway/ApplicationForm.tsx +48 -0
- package/src/Gateway/AvailabilityCalendar.tsx +82 -0
- package/src/Gateway/EPKViewer.tsx +77 -0
- package/src/Gateway/EngagementTimeline.tsx +43 -0
- package/src/Gateway/Gateway.shared.tsx +5 -0
- package/src/Gateway/Gateway.tsx +30 -0
- package/src/Gateway/Gateway.types.ts +69 -0
- package/src/Gateway/OnboardingPacket.tsx +61 -0
- package/src/Gateway/OpportunityCard.tsx +73 -0
- package/src/Gateway/OpportunityFilters.tsx +64 -0
- package/src/Gateway/PaymentTracker.tsx +71 -0
- package/src/Gateway/PayoutDetailsForm.tsx +62 -0
- package/src/Gateway/ProfileEditor.tsx +45 -0
- package/src/Gateway/ProgressBar.tsx +30 -0
- package/src/Gateway/RatingDialog.tsx +83 -0
- package/src/Gateway/UpNextCard.tsx +76 -0
- package/src/Glyphs/Glyphs.tsx +66 -0
- package/src/Grids/BidSheet.tsx +63 -0
- package/src/Grids/BudgetGrid.tsx +164 -0
- package/src/Grids/Grids.shared.tsx +5 -0
- package/src/Grids/Grids.tsx +11 -0
- package/src/Grids/Grids.types.ts +38 -0
- package/src/Grids/SpreadsheetGrid.tsx +445 -0
- package/src/Grids/columnTotal.tsx +14 -0
- package/src/Grids/glForCode.tsx +11 -0
- package/src/Grids/parseCell.tsx +16 -0
- package/src/Help/FeedbackWidget.tsx +89 -0
- package/src/Help/Help.tsx +14 -0
- package/src/Help/Help.types.ts +51 -0
- package/src/Help/HelpPanel.tsx +116 -0
- package/src/Help/NotificationCenter.tsx +100 -0
- package/src/Help/OnboardingChecklist.tsx +57 -0
- package/src/Help/ShortcutEditor.tsx +81 -0
- package/src/Help/WhatsNew.tsx +33 -0
- package/src/HelpPanel/README.md +5 -0
- package/src/HoverCard/README.md +3 -0
- package/src/Icon/Icon.tsx +47 -0
- package/src/Icon/README.md +9 -0
- package/src/IconButton/README.md +6 -0
- package/src/Identity/BrandSlot.tsx +20 -0
- package/src/Identity/BrandSlotFrame.tsx +38 -0
- package/src/Identity/CredentialBadge.tsx +92 -0
- package/src/Identity/Identity.tsx +14 -0
- package/src/Identity/Identity.types.ts +25 -0
- package/src/Identity/PresenceIndicator.tsx +37 -0
- package/src/Identity/QRCode.tsx +22 -0
- package/src/Identity/WhiteLabelPreview.tsx +33 -0
- package/src/Identity/qrPath.tsx +16 -0
- package/src/Identity/tokenVariable.tsx +9 -0
- package/src/InlineAlert/README.md +3 -0
- package/src/Input/README.md +8 -0
- package/src/Kbd/Kbd.tsx +30 -0
- package/src/Kbd/README.md +6 -0
- package/src/Layout/AppShell.tsx +151 -0
- package/src/Layout/Layout.tsx +14 -0
- package/src/Layout/Layout.types.ts +43 -0
- package/src/Layout/PageHeader.tsx +22 -0
- package/src/Layout/ResizablePanel.tsx +81 -0
- package/src/Layout/ScrollRegion.tsx +107 -0
- package/src/Layout/SplitView.tsx +78 -0
- package/src/Layout/Stepper.tsx +39 -0
- package/src/LegalText/LegalText.tsx +289 -0
- package/src/LegalText/README.md +10 -0
- package/src/LegalText/index.ts +29 -0
- package/src/LegalText/legal-markdown.ts +581 -0
- package/src/Link/Link.tsx +25 -0
- package/src/Link/README.md +5 -0
- package/src/Link/link-class.ts +10 -0
- package/src/LiveActivity/README.md +3 -0
- package/src/LogoSlotWall/README.md +7 -0
- package/src/MapView/README.md +5 -0
- package/src/Marketing/AIDisclosureBadge.tsx +31 -0
- package/src/Marketing/ButtonLink.tsx +32 -0
- package/src/Marketing/CodeSample.tsx +47 -0
- package/src/Marketing/ComparisonScroll.tsx +7 -0
- package/src/Marketing/ComparisonTable.tsx +104 -0
- package/src/Marketing/FAQ.tsx +26 -0
- package/src/Marketing/FeatureGrid.tsx +65 -0
- package/src/Marketing/GateLifecycleRail.tsx +52 -0
- package/src/Marketing/LogoSlotWall.tsx +53 -0
- package/src/Marketing/Marketing.tsx +49 -0
- package/src/Marketing/Marketing.types.ts +64 -0
- package/src/Marketing/MarketingFooter.tsx +88 -0
- package/src/Marketing/MarketingHero.tsx +45 -0
- package/src/Marketing/MarketingNav.tsx +76 -0
- package/src/Marketing/MarketingPoints.tsx +16 -0
- package/src/Marketing/MarketingSection.tsx +52 -0
- package/src/Marketing/PersonaSwitcher.tsx +137 -0
- package/src/Marketing/PriceDisplay.tsx +56 -0
- package/src/Marketing/PricingTable.tsx +122 -0
- package/src/Marketing/ProofStatStrip.tsx +35 -0
- package/src/Marketing/ScreenshotFrame.tsx +56 -0
- package/src/Marketing/SurfaceTriptych.tsx +43 -0
- package/src/Marketing/Testimonial.tsx +45 -0
- package/src/Marketing/marketing-copy.ts +25 -0
- package/src/Marketing/provided.tsx +36 -0
- package/src/MarketingFooter/README.md +7 -0
- package/src/MarketingHero/README.md +7 -0
- package/src/MarketingNav/README.md +7 -0
- package/src/MarketingSection/README.md +7 -0
- package/src/Media/FilePreviewer.tsx +86 -0
- package/src/Media/MapView.tsx +200 -0
- package/src/Media/Media.tsx +11 -0
- package/src/Media/Media.types.ts +47 -0
- package/src/Media/RichTextEditor.tsx +198 -0
- package/src/Media/toLngLat.tsx +13 -0
- package/src/Menus/ContextMenu.tsx +106 -0
- package/src/Menus/DropdownMenu.tsx +51 -0
- package/src/Menus/Menus.shared.tsx +67 -0
- package/src/Menus/Menus.tsx +13 -0
- package/src/Menus/Menus.types.ts +49 -0
- package/src/Menus/OrgSwitcher.tsx +69 -0
- package/src/Menus/compactTriggerClass.tsx +7 -0
- package/src/Menus/menuClass.tsx +4 -0
- package/src/Menus/menuItemClass.tsx +7 -0
- package/src/Menus/orgSwitcherTriggerClass.tsx +7 -0
- package/src/Money/README.md +6 -0
- package/src/MultiSelect/README.md +3 -0
- package/src/Navigation/Accordion.tsx +59 -0
- package/src/Navigation/Breadcrumbs.tsx +59 -0
- package/src/Navigation/MoreFields.tsx +42 -0
- package/src/Navigation/Navigation.shared.tsx +32 -0
- package/src/Navigation/Navigation.tsx +21 -0
- package/src/Navigation/Navigation.types.ts +63 -0
- package/src/Navigation/Pagination.tsx +30 -0
- package/src/Navigation/SegmentedControl.tsx +87 -0
- package/src/Navigation/Sidebar.tsx +106 -0
- package/src/Navigation/TabBar.tsx +61 -0
- package/src/Navigation/Tabs.tsx +79 -0
- package/src/Navigation/TopNav.tsx +86 -0
- package/src/Navigation/sidebar-classes.ts +18 -0
- package/src/NotificationCenter/README.md +3 -0
- package/src/NumberInput/README.md +3 -0
- package/src/OfflineBanner/README.md +3 -0
- package/src/OnboardingChecklist/README.md +3 -0
- package/src/OnboardingPacket/README.md +3 -0
- package/src/OpportunityCard/README.md +5 -0
- package/src/OpportunityFilters/README.md +3 -0
- package/src/OrgChart/README.md +5 -0
- package/src/OrgSwitcher/README.md +3 -0
- package/src/Overlays/Dialog.tsx +54 -0
- package/src/Overlays/Drawer.tsx +62 -0
- package/src/Overlays/HoverCard.tsx +59 -0
- package/src/Overlays/Overlays.shared.tsx +12 -0
- package/src/Overlays/Overlays.tsx +20 -0
- package/src/Overlays/Overlays.types.ts +66 -0
- package/src/Overlays/Popover.tsx +70 -0
- package/src/Overlays/PropertyList.tsx +19 -0
- package/src/Overlays/ShortcutSheet.tsx +62 -0
- package/src/Overlays/SidePeek.tsx +117 -0
- package/src/Overlays/Tooltip.tsx +73 -0
- package/src/Overlays/panelClass.tsx +6 -0
- package/src/Overlays/popoverPanelClass.tsx +6 -0
- package/src/PageHeader/README.md +3 -0
- package/src/Pagination/README.md +3 -0
- package/src/PaymentTracker/README.md +3 -0
- package/src/PayoutDetailsForm/README.md +3 -0
- package/src/PersonaSwitcher/README.md +7 -0
- package/src/PhaseChip/README.md +5 -0
- package/src/PhaseGlyph/README.md +17 -0
- package/src/PhotoCapture/README.md +3 -0
- package/src/Pickers/Combobox.tsx +91 -0
- package/src/Pickers/DatePicker.tsx +60 -0
- package/src/Pickers/DateRangePicker.tsx +78 -0
- package/src/Pickers/FileDrop.tsx +84 -0
- package/src/Pickers/MultiSelect.tsx +118 -0
- package/src/Pickers/Pickers.shared.tsx +337 -0
- package/src/Pickers/Pickers.tsx +17 -0
- package/src/Pickers/Pickers.types.ts +47 -0
- package/src/Pickers/Slider.tsx +59 -0
- package/src/Pickers/TimePicker.tsx +68 -0
- package/src/Pickers/rankOptions.tsx +12 -0
- package/src/Popover/README.md +3 -0
- package/src/PresenceIndicator/README.md +3 -0
- package/src/PriceDisplay/README.md +7 -0
- package/src/PricingTable/README.md +9 -0
- package/src/ProfileEditor/README.md +3 -0
- package/src/ProgressBar/README.md +3 -0
- package/src/ProofStatStrip/README.md +7 -0
- package/src/PropertyChip/README.md +3 -0
- package/src/ProvenanceBadge/README.md +3 -0
- package/src/QRCode/README.md +6 -0
- package/src/QuickAdd/README.md +5 -0
- package/src/QuickIncidentSheet/README.md +3 -0
- package/src/Radio/README.md +3 -0
- package/src/RadioChannelTable/README.md +7 -0
- package/src/RatingDialog/README.md +3 -0
- package/src/ReconciliationTable/README.md +3 -0
- package/src/RecordKindChip/README.md +3 -0
- package/src/RecordKindGlyph/README.md +34 -0
- package/src/RecordRow/README.md +7 -0
- package/src/RefusalNotice/README.md +5 -0
- package/src/ResizablePanel/README.md +3 -0
- package/src/ResourceSchedule/README.md +6 -0
- package/src/RichTextEditor/README.md +3 -0
- package/src/RichTextEditor/extensions.ts +217 -0
- package/src/RunOfShowLive/README.md +5 -0
- package/src/ScanSheet/README.md +3 -0
- package/src/ScreenshotFrame/README.md +7 -0
- package/src/ScrollRegion/README.md +9 -0
- package/src/SegmentedControl/README.md +5 -0
- package/src/Select/README.md +5 -0
- package/src/ShiftCard/README.md +3 -0
- package/src/ShortcutEditor/README.md +3 -0
- package/src/ShortcutSheet/README.md +3 -0
- package/src/SidePeek/README.md +6 -0
- package/src/Sidebar/README.md +5 -0
- package/src/SignaturePad/README.md +3 -0
- package/src/Skeleton/README.md +3 -0
- package/src/Slider/README.md +3 -0
- package/src/SplitView/README.md +3 -0
- package/src/SpreadsheetGrid/README.md +9 -0
- package/src/StalenessIndicator/README.md +3 -0
- package/src/StatTile/README.md +6 -0
- package/src/StateChip/README.md +6 -0
- package/src/StateIcon/README.md +6 -0
- package/src/StateIcon/StateIcon.tsx +127 -0
- package/src/StatusHeatmap/README.md +15 -0
- package/src/Stepper/README.md +3 -0
- package/src/Surface/Skeleton.tsx +21 -0
- package/src/Surface/Surface.tsx +63 -0
- package/src/SurfaceTriptych/README.md +7 -0
- package/src/Switch/README.md +5 -0
- package/src/SyncConflictSheet/README.md +3 -0
- package/src/TabBar/README.md +7 -0
- package/src/Tabs/README.md +6 -0
- package/src/Tag/README.md +3 -0
- package/src/TemplateAuth/README.md +6 -0
- package/src/TemplateCollection/README.md +6 -0
- package/src/TemplateDashboard/README.md +8 -0
- package/src/TemplateDocument/README.md +5 -0
- package/src/TemplateGatewayShell/README.md +5 -0
- package/src/TemplateGridEditor/README.md +3 -0
- package/src/TemplateRecord/README.md +3 -0
- package/src/TemplateSettings/README.md +5 -0
- package/src/TemplateSplitView/README.md +3 -0
- package/src/TemplateSystem/README.md +5 -0
- package/src/TemplateWizard/README.md +3 -0
- package/src/Templates/TemplateAuth.tsx +16 -0
- package/src/Templates/TemplateCollection.tsx +35 -0
- package/src/Templates/TemplateDashboard.tsx +200 -0
- package/src/Templates/TemplateDocument.tsx +24 -0
- package/src/Templates/TemplateGatewayShell.tsx +36 -0
- package/src/Templates/TemplateGridEditor.tsx +17 -0
- package/src/Templates/TemplateRecord.tsx +31 -0
- package/src/Templates/TemplateSettings.tsx +26 -0
- package/src/Templates/TemplateSplitView.tsx +27 -0
- package/src/Templates/TemplateSystem.tsx +19 -0
- package/src/Templates/TemplateSystemLink.tsx +55 -0
- package/src/Templates/TemplateWizard.tsx +43 -0
- package/src/Templates/Templates.tsx +29 -0
- package/src/Templates/Templates.types.ts +153 -0
- package/src/Testimonial/README.md +7 -0
- package/src/Textarea/README.md +3 -0
- package/src/TimePicker/README.md +5 -0
- package/src/Timeline/README.md +6 -0
- package/src/Toast/README.md +3 -0
- package/src/Tooltip/README.md +3 -0
- package/src/TopNav/README.md +5 -0
- package/src/TreeView/README.md +3 -0
- package/src/URIDChip/README.md +5 -0
- package/src/UndoToast/README.md +3 -0
- package/src/UpNextCard/README.md +6 -0
- package/src/Views/ActivityFeedItem.tsx +52 -0
- package/src/Views/Board.tsx +229 -0
- package/src/Views/BulkActionBar.tsx +76 -0
- package/src/Views/RecordList.tsx +146 -0
- package/src/Views/RecordRow.tsx +132 -0
- package/src/Views/StatTile.tsx +88 -0
- package/src/Views/TreeView.tsx +161 -0
- package/src/Views/Views.tsx +18 -0
- package/src/Views/Views.types.ts +99 -0
- package/src/WhatsNew/README.md +3 -0
- package/src/WhiteLabelPreview/README.md +8 -0
- package/src/XOS/AccessGridMatrix.tsx +111 -0
- package/src/XOS/AssertionRankBadge.tsx +27 -0
- package/src/XOS/CoordinateMatrix.tsx +123 -0
- package/src/XOS/EmergencyCodeCard.tsx +109 -0
- package/src/XOS/GateReadinessPanel.tsx +100 -0
- package/src/XOS/ProvenanceBadge.tsx +19 -0
- package/src/XOS/RadioChannelTable.tsx +50 -0
- package/src/XOS/ReconciliationTable.tsx +64 -0
- package/src/XOS/RunOfShowLive.tsx +97 -0
- package/src/XOS/StalenessIndicator.tsx +31 -0
- package/src/XOS/StatusHeatmap.tsx +200 -0
- package/src/XOS/XOS.shared.tsx +90 -0
- package/src/XOS/XOS.tsx +29 -0
- package/src/XOS/XOS.types.ts +99 -0
- package/src/XOS/resolveStep.tsx +16 -0
- package/src/brand/defaults/README.md +13 -0
- package/src/brand/defaults/app-icon.svg +6 -0
- package/src/brand/defaults/favicon.svg +6 -0
- package/src/brand/defaults/logo-dark.svg +6 -0
- package/src/brand/defaults/logo-light.svg +6 -0
- package/src/brand/defaults/splash.svg +6 -0
- package/src/index.ts +606 -0
- package/src/lib/context.tsx +156 -0
- package/src/lib/cx.ts +4 -0
- package/src/lib/format.ts +200 -0
- package/src/lib/hooks.ts +187 -0
- package/src/lib/i18n.ts +97 -0
- package/src/lib/icons.ts +45 -0
- package/src/lib/live.ts +92 -0
- package/src/lib/preferences.ts +242 -0
- package/src/lib/route-transition.tsx +22 -0
- package/src/lib/selection.ts +92 -0
- package/src/lib/shortcut-targets.ts +125 -0
- package/src/lib/theme.ts +50 -0
- package/src/lib/undo-stack.ts +125 -0
- package/src/marketing.ts +33 -0
- package/src/pwa/action-outcome.tsx +81 -0
- package/src/pwa/analytics.ts +236 -0
- package/src/pwa/browser-push.tsx +132 -0
- package/src/pwa/client.ts +126 -0
- package/src/pwa/index.ts +40 -0
- package/src/pwa/offline-key.ts +18 -0
- package/src/pwa/service-worker.ts +229 -0
- package/src/static.ts +10 -0
- package/src/styles/base.css +208 -0
- package/src/styles/components.css +453 -0
- package/src/styles/index.css +16 -0
- package/src/styles/marketing.css +1260 -0
- package/src/styles/reference.css +5792 -0
- package/src/types/reference.d.ts +1608 -0
- package/tailwind.config.cjs +3 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { createContext, useContext, useEffect, useMemo, type ReactNode } from "react";
|
|
2
|
+
import { Direction } from "radix-ui";
|
|
3
|
+
import { currentConfig, directionOf, translate, type MessageVars, type Messages } from "./i18n.ts";
|
|
4
|
+
import {
|
|
5
|
+
displayTimeZone,
|
|
6
|
+
formatCompact,
|
|
7
|
+
formatCurrency,
|
|
8
|
+
formatDate,
|
|
9
|
+
formatList,
|
|
10
|
+
formatNumber,
|
|
11
|
+
formatZonedDateTime,
|
|
12
|
+
} from "./format.ts";
|
|
13
|
+
import { applyTheme, type ThemePreference } from "./theme.ts";
|
|
14
|
+
import { DEFAULT_PREFERENCES, type DisplayPreferences } from "./preferences.ts";
|
|
15
|
+
import { resolveUnitSystem, type UnitSystem } from "@xos/i18n/units";
|
|
16
|
+
|
|
17
|
+
interface LocaleState {
|
|
18
|
+
locale: string;
|
|
19
|
+
messages: Messages;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const LocaleContext = createContext<LocaleState | null>(null);
|
|
23
|
+
const PreferencesContext = createContext<DisplayPreferences>(DEFAULT_PREFERENCES);
|
|
24
|
+
/** The viewer's own IANA time zone (locale.time_zone), for me.time_zone_display. */
|
|
25
|
+
const ViewerZoneContext = createContext<string | null>(null);
|
|
26
|
+
|
|
27
|
+
export interface XOSProviderProps {
|
|
28
|
+
/** BCP 47 locale for copy and Intl formatting; en-US by default. */
|
|
29
|
+
locale?: string;
|
|
30
|
+
/** Flat `ui` messages merged over the catalog, keyed without the namespace. */
|
|
31
|
+
messages?: Messages;
|
|
32
|
+
/** Applies a theme preference to the document root while mounted. */
|
|
33
|
+
theme?: ThemePreference;
|
|
34
|
+
/** The person's display preferences (density, date format, units, week start); defaults when omitted. */
|
|
35
|
+
preferences?: DisplayPreferences;
|
|
36
|
+
children: ReactNode;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Sets the locale, text direction and, optionally, the theme for every component below it. Without a
|
|
41
|
+
* provider, components read the locale set by `configure`.
|
|
42
|
+
*/
|
|
43
|
+
export function XOSProvider({ locale, messages, theme, preferences, children }: XOSProviderProps) {
|
|
44
|
+
const fallback = currentConfig();
|
|
45
|
+
const resolvedLocale = locale ?? fallback.locale;
|
|
46
|
+
const value = useMemo(
|
|
47
|
+
() => ({ locale: resolvedLocale, messages: { ...fallback.messages, ...messages } }),
|
|
48
|
+
[resolvedLocale, fallback.messages, messages],
|
|
49
|
+
);
|
|
50
|
+
useEffect(() => (theme === undefined ? undefined : applyTheme(theme)), [theme]);
|
|
51
|
+
return (
|
|
52
|
+
<LocaleContext.Provider value={value}>
|
|
53
|
+
<PreferencesContext.Provider value={preferences ?? DEFAULT_PREFERENCES}>
|
|
54
|
+
<Direction.Provider dir={directionOf(resolvedLocale)}>{children}</Direction.Provider>
|
|
55
|
+
</PreferencesContext.Provider>
|
|
56
|
+
</LocaleContext.Provider>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Sets the display preferences for the components below it, under an `XOSProvider`. The app shells use it
|
|
62
|
+
* with the person's resolved settings, so a saved setting shows on the page it was saved from.
|
|
63
|
+
*/
|
|
64
|
+
export function PreferencesProvider({
|
|
65
|
+
preferences,
|
|
66
|
+
timeZone,
|
|
67
|
+
children,
|
|
68
|
+
}: {
|
|
69
|
+
preferences: DisplayPreferences;
|
|
70
|
+
/** The viewer's own IANA time zone (locale.time_zone), which "my time zone" shows schedule times in. */
|
|
71
|
+
timeZone?: string | null;
|
|
72
|
+
children: ReactNode;
|
|
73
|
+
}) {
|
|
74
|
+
return (
|
|
75
|
+
<PreferencesContext.Provider value={preferences}>
|
|
76
|
+
<ViewerZoneContext.Provider value={timeZone ?? null}>{children}</ViewerZoneContext.Provider>
|
|
77
|
+
</PreferencesContext.Provider>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The person's display preferences from the nearest provider (Section 4.5.6, A65-001). */
|
|
82
|
+
export function usePreferences(): DisplayPreferences {
|
|
83
|
+
return useContext(PreferencesContext);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The viewer's own time zone from the nearest PreferencesProvider, or null when the shell gave none. */
|
|
87
|
+
export function useViewerTimeZone(): string | null {
|
|
88
|
+
return useContext(ViewerZoneContext);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Formats schedule times in the zone the person chose (me.time_zone_display, Section 15): the event's zone by
|
|
93
|
+
* default, their own when they chose it. Every time carries its zone abbreviation.
|
|
94
|
+
*/
|
|
95
|
+
export function useZonedTime() {
|
|
96
|
+
const locale = useLocale();
|
|
97
|
+
const { timeZoneDisplay } = usePreferences();
|
|
98
|
+
const viewer = useViewerTimeZone();
|
|
99
|
+
return {
|
|
100
|
+
zone: (eventZone: string | null | undefined) =>
|
|
101
|
+
displayTimeZone(eventZone, timeZoneDisplay, viewer),
|
|
102
|
+
dateTime: (iso: string, eventZone: string | null | undefined) => {
|
|
103
|
+
const zone = displayTimeZone(eventZone, timeZoneDisplay, viewer);
|
|
104
|
+
return zone
|
|
105
|
+
? formatZonedDateTime(locale, iso, zone)
|
|
106
|
+
: formatZonedDateTime(locale, iso, "UTC");
|
|
107
|
+
},
|
|
108
|
+
time: (iso: string, eventZone: string | null | undefined) => {
|
|
109
|
+
const zone = displayTimeZone(eventZone, timeZoneDisplay, viewer);
|
|
110
|
+
return formatZonedDateTime(locale, iso, zone ?? "UTC", "time");
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The unit system to show values in (locale.units), given the page's project jurisdiction when it has one. */
|
|
116
|
+
export function useUnitSystem(jurisdiction?: UnitSystem | null): UnitSystem {
|
|
117
|
+
const locale = useLocale();
|
|
118
|
+
const { units } = usePreferences();
|
|
119
|
+
return resolveUnitSystem(units, { jurisdiction: jurisdiction ?? null, locale });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function useLocaleState(): LocaleState {
|
|
123
|
+
return useContext(LocaleContext) ?? currentConfig();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The active locale for Intl formatting. */
|
|
127
|
+
export function useLocale(): string {
|
|
128
|
+
return useLocaleState().locale;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The message overrides of the nearest provider, for components that take messages as a prop. */
|
|
132
|
+
export function useMessages(): Messages {
|
|
133
|
+
return useLocaleState().messages;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export type Translate = (key: string, vars?: MessageVars) => string;
|
|
137
|
+
|
|
138
|
+
/** A `t` bound to the provider's locale and messages. */
|
|
139
|
+
export function useT(): Translate {
|
|
140
|
+
const { locale, messages } = useLocaleState();
|
|
141
|
+
return (key, vars) => translate(locale, key, vars, messages);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Number, money, list and date formatters bound to the active locale and the person's date format. */
|
|
145
|
+
export function useFormat() {
|
|
146
|
+
const locale = useLocale();
|
|
147
|
+
const { dateFormat } = usePreferences();
|
|
148
|
+
return {
|
|
149
|
+
locale,
|
|
150
|
+
number: (value: number) => formatNumber(locale, value),
|
|
151
|
+
currency: (value: number, currency?: string) => formatCurrency(locale, value, currency),
|
|
152
|
+
compact: (value: number, currency?: string) => formatCompact(locale, value, currency),
|
|
153
|
+
list: (items: string[]) => formatList(locale, items),
|
|
154
|
+
date: (iso: string) => formatDate(locale, iso, dateFormat),
|
|
155
|
+
};
|
|
156
|
+
}
|
package/src/lib/cx.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Joins class names, dropping falsy entries. Every class string in this package goes through cx or a className literal, which the utility test scans. */
|
|
2
|
+
export function cx(...parts: (string | false | null | undefined)[]): string {
|
|
3
|
+
return parts.filter(Boolean).join(" ");
|
|
4
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/** Locale-aware formatting through Intl. Nothing here stores a display format; values are formatted at render. */
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Intl formatters are reused per locale and options. Building one loads locale data and costs far
|
|
5
|
+
* more than formatting with it: a table built a currency formatter per Money cell, and the first
|
|
6
|
+
* formatters of a page were a large part of its hydration time on a slow CPU (gate 10, Total
|
|
7
|
+
* Blocking Time).
|
|
8
|
+
*/
|
|
9
|
+
const numberFormats = new Map<string, Intl.NumberFormat>();
|
|
10
|
+
const dateTimeFormats = new Map<string, Intl.DateTimeFormat>();
|
|
11
|
+
const listFormats = new Map<string, Intl.ListFormat>();
|
|
12
|
+
|
|
13
|
+
function cached<T>(cache: Map<string, T>, key: string, make: () => T): T {
|
|
14
|
+
let formatter = cache.get(key);
|
|
15
|
+
if (formatter === undefined) {
|
|
16
|
+
formatter = make();
|
|
17
|
+
cache.set(key, formatter);
|
|
18
|
+
}
|
|
19
|
+
return formatter;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function numberFormat(locale: string, options: Intl.NumberFormatOptions = {}): Intl.NumberFormat {
|
|
23
|
+
return cached(numberFormats, `${locale}|${JSON.stringify(options)}`, () => {
|
|
24
|
+
return new Intl.NumberFormat(locale, options);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function dateTimeFormat(locale: string, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat {
|
|
29
|
+
return cached(dateTimeFormats, `${locale}|${JSON.stringify(options)}`, () => {
|
|
30
|
+
return new Intl.DateTimeFormat(locale, options);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function formatNumber(locale: string, value: number): string {
|
|
35
|
+
return numberFormat(locale).format(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A date and time in a given IANA zone, always labeled with the zone's abbreviation (Section 15: schedule
|
|
40
|
+
* times "always labeled with the zone abbreviation"), for example "Oct 21, 2026, 7:30 PM EDT". `timeZoneName`
|
|
41
|
+
* cannot be combined with dateStyle or timeStyle, so the parts are spelled out.
|
|
42
|
+
*/
|
|
43
|
+
export function formatZonedDateTime(
|
|
44
|
+
locale: string,
|
|
45
|
+
iso: string | Date,
|
|
46
|
+
timeZone: string,
|
|
47
|
+
parts: "dateTime" | "time" = "dateTime",
|
|
48
|
+
): string {
|
|
49
|
+
const options: Intl.DateTimeFormatOptions =
|
|
50
|
+
parts === "time"
|
|
51
|
+
? { hour: "numeric", minute: "2-digit", timeZone, timeZoneName: "short" }
|
|
52
|
+
: {
|
|
53
|
+
year: "numeric",
|
|
54
|
+
month: "short",
|
|
55
|
+
day: "numeric",
|
|
56
|
+
hour: "numeric",
|
|
57
|
+
minute: "2-digit",
|
|
58
|
+
timeZone,
|
|
59
|
+
timeZoneName: "short",
|
|
60
|
+
};
|
|
61
|
+
return dateTimeFormat(locale, options).format(typeof iso === "string" ? new Date(iso) : iso);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Whether a schedule shows times in the event's zone or the viewer's own (me.time_zone_display). */
|
|
65
|
+
export type TimeZoneDisplay = "event" | "viewer";
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The zone a schedule time is shown in (Section 15): the event's (the venue's or project's) by default, or
|
|
69
|
+
* the viewer's own when they chose that (me.time_zone_display) and their zone is known.
|
|
70
|
+
*/
|
|
71
|
+
export function displayTimeZone(
|
|
72
|
+
eventZone: string | null | undefined,
|
|
73
|
+
display: TimeZoneDisplay,
|
|
74
|
+
viewerZone: string | null | undefined,
|
|
75
|
+
): string | undefined {
|
|
76
|
+
if (display === "viewer" && viewerZone) return viewerZone;
|
|
77
|
+
return eventZone ?? viewerZone ?? undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function formatCurrency(locale: string, value: number, currency = "USD"): string {
|
|
81
|
+
return numberFormat(locale, { style: "currency", currency }).format(value);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Compact axis ticks (`notation: compact`), as the data visualization rules require. The minimum of no fraction
|
|
86
|
+
* digits is explicit: with currency, ICU versions disagree on the default (Node 22's gives "$5.0K", Chromium's "$5K"),
|
|
87
|
+
* and a server-rendered chart whose ticks differ from the browser's fails hydration.
|
|
88
|
+
*/
|
|
89
|
+
export function formatCompact(locale: string, value: number, currency?: string): string {
|
|
90
|
+
return numberFormat(
|
|
91
|
+
locale,
|
|
92
|
+
currency === undefined
|
|
93
|
+
? { notation: "compact", minimumFractionDigits: 0, maximumFractionDigits: 1 }
|
|
94
|
+
: {
|
|
95
|
+
style: "currency",
|
|
96
|
+
currency,
|
|
97
|
+
notation: "compact",
|
|
98
|
+
minimumFractionDigits: 0,
|
|
99
|
+
maximumFractionDigits: 1,
|
|
100
|
+
},
|
|
101
|
+
).format(value);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function formatList(locale: string, items: string[]): string {
|
|
105
|
+
return cached(listFormats, locale, () => new Intl.ListFormat(locale)).format(items);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
interface WeekInfoLocale {
|
|
109
|
+
getWeekInfo?: () => { firstDay: number };
|
|
110
|
+
weekInfo?: { firstDay: number };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* First day of the week, 0 for Sunday through 6 for Saturday: the person's choice when given
|
|
115
|
+
* (payroll.week_start), otherwise the locale's.
|
|
116
|
+
*/
|
|
117
|
+
export function weekStart(locale: string, override: number | null = null): number {
|
|
118
|
+
if (override !== null) return override;
|
|
119
|
+
try {
|
|
120
|
+
const intlLocale = new Intl.Locale(locale) as Intl.Locale & WeekInfoLocale;
|
|
121
|
+
const info = intlLocale.getWeekInfo ? intlLocale.getWeekInfo() : intlLocale.weekInfo;
|
|
122
|
+
return info ? info.firstDay % 7 : 0;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (error instanceof RangeError) return 0;
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface IsoParts {
|
|
130
|
+
y: number;
|
|
131
|
+
m: number;
|
|
132
|
+
d: number;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function parseIso(value: string): IsoParts {
|
|
136
|
+
const [y = "1970", m = "1", d = "1"] = value.split("-");
|
|
137
|
+
return { y: Number(y), m: Number(m) - 1, d: Number(d) };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function toIso(y: number, m: number, d: number): string {
|
|
141
|
+
return `${y}-${String(m + 1).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function monthTitle(locale: string, y: number, m: number): string {
|
|
145
|
+
return dateTimeFormat(locale, { month: "long", year: "numeric" }).format(new Date(y, m, 1));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Short weekday names starting from the first day of the week. */
|
|
149
|
+
export function weekdayNames(locale: string, override: number | null = null): string[] {
|
|
150
|
+
const formatter = dateTimeFormat(locale, { weekday: "short" });
|
|
151
|
+
const start = weekStart(locale, override);
|
|
152
|
+
// 2026-02-01 is a Sunday.
|
|
153
|
+
return Array.from({ length: 7 }, (_, i) =>
|
|
154
|
+
formatter.format(new Date(2026, 1, 1 + ((start + i) % 7))),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** A calendar date for display: by the locale, or year-month-day everywhere (locale.date_format iso). */
|
|
159
|
+
export function formatDate(
|
|
160
|
+
locale: string,
|
|
161
|
+
iso: string,
|
|
162
|
+
dateFormat: "locale" | "iso" = "locale",
|
|
163
|
+
): string {
|
|
164
|
+
const { y, m, d } = parseIso(iso);
|
|
165
|
+
if (dateFormat === "iso") return toIso(y, m, d);
|
|
166
|
+
return dateTimeFormat(locale, { month: "short", day: "numeric", year: "numeric" }).format(
|
|
167
|
+
new Date(y, m, d),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function formatTimeOfDay(locale: string, hours: number, minutes: number): string {
|
|
172
|
+
return dateTimeFormat(locale, { hour: "numeric", minute: "2-digit" }).format(
|
|
173
|
+
new Date(2026, 0, 1, hours, minutes),
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Month cells for a calendar grid: leading and trailing nulls pad whole weeks. */
|
|
178
|
+
export function monthCells(
|
|
179
|
+
locale: string,
|
|
180
|
+
y: number,
|
|
181
|
+
m: number,
|
|
182
|
+
override: number | null = null,
|
|
183
|
+
): (number | null)[][] {
|
|
184
|
+
const first = (new Date(y, m, 1).getDay() - weekStart(locale, override) + 7) % 7;
|
|
185
|
+
const days = new Date(y, m + 1, 0).getDate();
|
|
186
|
+
const cells: (number | null)[] = Array.from({ length: first }, () => null);
|
|
187
|
+
for (let d = 1; d <= days; d++) cells.push(d);
|
|
188
|
+
while (cells.length % 7) cells.push(null);
|
|
189
|
+
const weeks: (number | null)[][] = [];
|
|
190
|
+
for (let w = 0; w < cells.length; w += 7) weeks.push(cells.slice(w, w + 7));
|
|
191
|
+
return weeks;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Seconds as mm:ss; a negative value (overrun) reads +mm:ss. */
|
|
195
|
+
export function formatClock(seconds: number): string {
|
|
196
|
+
const over = seconds < 0;
|
|
197
|
+
const s = Math.abs(seconds);
|
|
198
|
+
const m = Math.floor(s / 60);
|
|
199
|
+
return `${over ? "+" : ""}${String(m).padStart(2, "0")}:${String(s % 60).padStart(2, "0")}`;
|
|
200
|
+
}
|
package/src/lib/hooks.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useLayoutEffect,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
useSyncExternalStore,
|
|
8
|
+
} from "react";
|
|
9
|
+
import { singleKeyShortcutsEnabled } from "./preferences.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Whether a media query matches. Server rendering and hydration see false, so a layout that switches
|
|
13
|
+
* on it renders the wide markup first and swaps after hydration.
|
|
14
|
+
*/
|
|
15
|
+
export function useMediaQuery(query: string): boolean {
|
|
16
|
+
const subscribe = useCallback(
|
|
17
|
+
(onChange: () => void) => {
|
|
18
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
19
|
+
return () => undefined;
|
|
20
|
+
}
|
|
21
|
+
const list = window.matchMedia(query);
|
|
22
|
+
list.addEventListener("change", onChange);
|
|
23
|
+
return () => list.removeEventListener("change", onChange);
|
|
24
|
+
},
|
|
25
|
+
[query],
|
|
26
|
+
);
|
|
27
|
+
const matches = useCallback(
|
|
28
|
+
() => typeof window.matchMedia === "function" && window.matchMedia(query).matches,
|
|
29
|
+
[query],
|
|
30
|
+
);
|
|
31
|
+
return useSyncExternalStore(subscribe, matches, () => false);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** True below md (48rem, a phone). */
|
|
35
|
+
export function useNarrow(): boolean {
|
|
36
|
+
return useMediaQuery("(max-width: 47.999rem)");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** State that a parent may control (`value` given) or leave to the component (`defaultValue`). */
|
|
40
|
+
export function useControllable<T>(
|
|
41
|
+
value: T | undefined,
|
|
42
|
+
defaultValue: T,
|
|
43
|
+
onChange?: (next: T) => void,
|
|
44
|
+
): [T, (next: T) => void] {
|
|
45
|
+
const [inner, setInner] = useState(defaultValue);
|
|
46
|
+
const controlled = value !== undefined;
|
|
47
|
+
const current = controlled ? value : inner;
|
|
48
|
+
const set = useCallback(
|
|
49
|
+
(next: T) => {
|
|
50
|
+
if (!controlled) setInner(next);
|
|
51
|
+
onChange?.(next);
|
|
52
|
+
},
|
|
53
|
+
[controlled, onChange],
|
|
54
|
+
);
|
|
55
|
+
return [current, set];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface HotkeyOptions {
|
|
59
|
+
/** Requires Cmd on Apple platforms or Ctrl elsewhere. */
|
|
60
|
+
mod?: boolean;
|
|
61
|
+
/** Ignore the key while typing in a field (default true for unmodified keys). */
|
|
62
|
+
ignoreInFields?: boolean;
|
|
63
|
+
enabled?: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function inField(target: EventTarget | null): boolean {
|
|
67
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
68
|
+
return (
|
|
69
|
+
target.isContentEditable ||
|
|
70
|
+
target instanceof HTMLInputElement ||
|
|
71
|
+
target instanceof HTMLTextAreaElement ||
|
|
72
|
+
target instanceof HTMLSelectElement
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Binds a document-level shortcut: `useHotkey("k", open, { mod: true })` for the command menu,
|
|
78
|
+
* `useHotkey("?", openSheet)` for the shortcut sheet. Unmodified keys are ignored while typing.
|
|
79
|
+
*/
|
|
80
|
+
export function useHotkey(
|
|
81
|
+
key: string,
|
|
82
|
+
handler: (event: KeyboardEvent) => void,
|
|
83
|
+
options: HotkeyOptions = {},
|
|
84
|
+
): void {
|
|
85
|
+
const { mod = false, ignoreInFields = !mod, enabled = true } = options;
|
|
86
|
+
const latest = useRef(handler);
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
latest.current = handler;
|
|
89
|
+
});
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (!enabled) return undefined;
|
|
92
|
+
const onKey = (event: KeyboardEvent) => {
|
|
93
|
+
if (event.key.toLowerCase() !== key.toLowerCase()) return;
|
|
94
|
+
if (mod !== (event.metaKey || event.ctrlKey)) return;
|
|
95
|
+
if (!mod && !singleKeyShortcutsEnabled()) return;
|
|
96
|
+
if (ignoreInFields && inField(event.target)) return;
|
|
97
|
+
event.preventDefault();
|
|
98
|
+
latest.current(event);
|
|
99
|
+
};
|
|
100
|
+
document.addEventListener("keydown", onKey);
|
|
101
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
102
|
+
}, [key, mod, ignoreInFields, enabled]);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Roving focus over a list of elements along one axis, honoring the text direction. */
|
|
106
|
+
export function rovingIndex(
|
|
107
|
+
key: string,
|
|
108
|
+
index: number,
|
|
109
|
+
count: number,
|
|
110
|
+
orientation: "horizontal" | "vertical",
|
|
111
|
+
dir: "ltr" | "rtl" = "ltr",
|
|
112
|
+
): number | null {
|
|
113
|
+
const next =
|
|
114
|
+
orientation === "horizontal" ? (dir === "rtl" ? "ArrowLeft" : "ArrowRight") : "ArrowDown";
|
|
115
|
+
const prev =
|
|
116
|
+
orientation === "horizontal" ? (dir === "rtl" ? "ArrowRight" : "ArrowLeft") : "ArrowUp";
|
|
117
|
+
if (key === next) return (index + 1) % count;
|
|
118
|
+
if (key === prev) return (index - 1 + count) % count;
|
|
119
|
+
if (key === "Home") return 0;
|
|
120
|
+
if (key === "End") return count - 1;
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The control that asked for an overlay a later render mounts (rememberOverlayOpener). */
|
|
125
|
+
let requestedBy: HTMLElement | null = null;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Remembers the focused control as the opener of an overlay that mounts after a navigation, such as
|
|
129
|
+
* the side peek a page renders for `?peek`. The route's loading state can hide that control first,
|
|
130
|
+
* which drops focus to the page body, so the overlay would find no opener when it mounts and could
|
|
131
|
+
* not hand focus back on close (WCAG 2.4.3). The next overlay that opens takes it.
|
|
132
|
+
*/
|
|
133
|
+
export function rememberOverlayOpener(): void {
|
|
134
|
+
const active = document.activeElement;
|
|
135
|
+
requestedBy = active instanceof HTMLElement && active !== document.body ? active : null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Whether an element and its ancestors are displayed (React hides a suspended boundary's content). */
|
|
139
|
+
function displayed(element: HTMLElement): boolean {
|
|
140
|
+
for (let node: HTMLElement | null = element; node; node = node.parentElement) {
|
|
141
|
+
if (getComputedStyle(node).display === "none") return false;
|
|
142
|
+
}
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Focus return for an overlay opened without its own trigger (a hotkey, a row click): remembers the
|
|
148
|
+
* element focused when `open` turns true (or, with focus on the page body, the control that asked
|
|
149
|
+
* for the overlay) and hands it back on close, so focus never drops to the page body (WCAG 2.4.3).
|
|
150
|
+
* Pass the result to Radix `onCloseAutoFocus`.
|
|
151
|
+
*
|
|
152
|
+
* A route's loading state can hide an open overlay together with its opener (React hides a
|
|
153
|
+
* suspended boundary's content and runs its effects again when it shows it). Radix then reports a
|
|
154
|
+
* close while both are hidden, and this effect runs again with focus on the page body; while `open`
|
|
155
|
+
* stays true the opener survives both and takes focus on the real close. Once `open` turns false
|
|
156
|
+
* the opener is used or dropped, so a later open never inherits it.
|
|
157
|
+
*/
|
|
158
|
+
export function useReturnFocus(open: boolean): (event: Event) => void {
|
|
159
|
+
const opener = useRef<HTMLElement | null>(null);
|
|
160
|
+
const isOpen = useRef(false);
|
|
161
|
+
/* A layout effect runs before the dialog's own focus move (a passive effect), so it still sees
|
|
162
|
+
the opener. */
|
|
163
|
+
useLayoutEffect(() => {
|
|
164
|
+
if (open) {
|
|
165
|
+
const active = document.activeElement;
|
|
166
|
+
const focused = active instanceof HTMLElement && active !== document.body ? active : null;
|
|
167
|
+
const requested = requestedBy?.isConnected ? requestedBy : null;
|
|
168
|
+
requestedBy = null;
|
|
169
|
+
// Kept from the run before only when shown again: the overlay has been open all along.
|
|
170
|
+
const kept = isOpen.current && opener.current?.isConnected ? opener.current : null;
|
|
171
|
+
opener.current = kept ?? focused ?? requested;
|
|
172
|
+
}
|
|
173
|
+
isOpen.current = open;
|
|
174
|
+
}, [open]);
|
|
175
|
+
return useCallback((event: Event) => {
|
|
176
|
+
const target = opener.current;
|
|
177
|
+
if (!target?.isConnected) {
|
|
178
|
+
opener.current = null;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
event.preventDefault();
|
|
182
|
+
// A loading state hid the open overlay and its opener: keep it for the real close.
|
|
183
|
+
if (isOpen.current && !displayed(target)) return;
|
|
184
|
+
opener.current = null;
|
|
185
|
+
target.focus();
|
|
186
|
+
}, []);
|
|
187
|
+
}
|
package/src/lib/i18n.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { defaultLocale } from "@xos/i18n/locales";
|
|
2
|
+
import enUSui from "@xos/i18n/messages/en-US/ui.json" with { type: "json" };
|
|
3
|
+
import { formatNumber } from "./format.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Component copy (ADR 0010, D6). Every visible string comes from the `ui` namespace of the
|
|
7
|
+
* @xos/i18n catalogs; keys are written without the namespace (`money.unpriced`) and this helper
|
|
8
|
+
* prefixes `ui.`. Only the en-US `ui` namespace ships with the library: it is the source locale
|
|
9
|
+
* and the fallback. Any other locale's messages come from `XOSProvider` or `configure`, so the
|
|
10
|
+
* browser never downloads a locale it does not show (A24-009). Keys those messages do not supply
|
|
11
|
+
* fall back to en-US.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type MessageVars = Record<string, string | number>;
|
|
15
|
+
export type Messages = Record<string, string>;
|
|
16
|
+
|
|
17
|
+
const NAMESPACE = "ui";
|
|
18
|
+
|
|
19
|
+
function flatten(value: unknown, prefix: string, out: Messages): Messages {
|
|
20
|
+
if (typeof value === "string") {
|
|
21
|
+
out[prefix] = value;
|
|
22
|
+
} else if (typeof value === "object" && value !== null) {
|
|
23
|
+
for (const [key, child] of Object.entries(value)) {
|
|
24
|
+
flatten(child, prefix ? `${prefix}.${key}` : key, out);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let source: Messages | undefined;
|
|
31
|
+
|
|
32
|
+
/** The flat en-US `ui` catalog, keyed without the namespace: the fallback for every locale. */
|
|
33
|
+
export function catalogFor(): Messages {
|
|
34
|
+
source ??= flatten(enUSui, "", {});
|
|
35
|
+
return source;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Flattens a nested `ui` catalog (such as a pseudo-locale file) into the flat form `configure` takes. */
|
|
39
|
+
export function flattenMessages(nested: unknown): Messages {
|
|
40
|
+
const root =
|
|
41
|
+
typeof nested === "object" && nested !== null && NAMESPACE in nested
|
|
42
|
+
? (nested as Record<string, unknown>)[NAMESPACE]
|
|
43
|
+
: nested;
|
|
44
|
+
return flatten(root, "", {});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface XOSConfig {
|
|
48
|
+
locale?: string;
|
|
49
|
+
messages?: Record<string, string>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const config: { locale: string; messages: Messages } = { locale: defaultLocale, messages: {} };
|
|
53
|
+
|
|
54
|
+
/** Sets the locale for Intl formatting and merges messages over the catalog, for code outside a provider. */
|
|
55
|
+
export function configure(next: XOSConfig): void {
|
|
56
|
+
if (next.locale !== undefined) config.locale = next.locale;
|
|
57
|
+
if (next.messages !== undefined) config.messages = { ...config.messages, ...next.messages };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The locale and message overrides set by `configure`. */
|
|
61
|
+
export function currentConfig(): { locale: string; messages: Messages } {
|
|
62
|
+
return config;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Fills `{name}` arguments; numbers are formatted for the locale, as an ICU simple argument is. */
|
|
66
|
+
export function format(message: string, locale: string, vars?: MessageVars): string {
|
|
67
|
+
if (vars === undefined) return message;
|
|
68
|
+
return message.replace(/\{(\w+)\}/g, (match, name: string) => {
|
|
69
|
+
const value = vars[name];
|
|
70
|
+
if (value === undefined) return match;
|
|
71
|
+
return typeof value === "number" ? formatNumber(locale, value) : value;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Looks up a `ui` message for a locale, with overrides first, and fills its arguments. Unknown keys return the key. */
|
|
76
|
+
export function translate(
|
|
77
|
+
locale: string,
|
|
78
|
+
key: string,
|
|
79
|
+
vars?: MessageVars,
|
|
80
|
+
overrides: Messages = {},
|
|
81
|
+
): string {
|
|
82
|
+
const message = overrides[key] ?? catalogFor()[key];
|
|
83
|
+
return message === undefined ? key : format(message, locale, vars);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Looks up a catalog message and fills its arguments, using the locale set by `configure`. */
|
|
87
|
+
export function t(key: string, vars?: MessageVars): string {
|
|
88
|
+
return translate(config.locale, key, vars, config.messages);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Right-to-left scripts, matched on the language subtag; `ar-XB` is the right-to-left pseudo-locale. */
|
|
92
|
+
const RTL_LANGUAGES = new Set(["ar", "fa", "he", "ps", "ur", "yi", "dv", "ckb"]);
|
|
93
|
+
|
|
94
|
+
export function directionOf(locale: string): "ltr" | "rtl" {
|
|
95
|
+
const language = locale.split("-")[0]?.toLowerCase() ?? "";
|
|
96
|
+
return RTL_LANGUAGES.has(language) ? "rtl" : "ltr";
|
|
97
|
+
}
|
package/src/lib/icons.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { isRegisteredIcon, lucideName } from "@xos/icons/names";
|
|
2
|
+
import sprite from "@xos/icons/sprite.svg";
|
|
3
|
+
|
|
4
|
+
export { lucideName };
|
|
5
|
+
|
|
6
|
+
/** Icons that point along the reading direction and mirror in right-to-left locales. */
|
|
7
|
+
const MIRRORED = new Set([
|
|
8
|
+
"ChevronRight",
|
|
9
|
+
"ChevronLeft",
|
|
10
|
+
"ArrowUpRight",
|
|
11
|
+
"ArrowRight",
|
|
12
|
+
"ArrowLeft",
|
|
13
|
+
"Undo2",
|
|
14
|
+
"Send",
|
|
15
|
+
"ExternalLink",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* URL of the icon sprite (ADR 0027): one SVG file of every registered icon, emitted by the bundler
|
|
20
|
+
* with a content hash so browsers cache it as a static asset. Next.js imports a static file as
|
|
21
|
+
* `{ src }`, Vite as a string.
|
|
22
|
+
*/
|
|
23
|
+
export const iconSpriteUrl: string = typeof sprite === "string" ? sprite : sprite.src;
|
|
24
|
+
|
|
25
|
+
const warned = new Set<string>();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The sprite symbol id for an icon name. In development a name outside the @xos/icons registry
|
|
29
|
+
* warns once, so it is added to the owner's registry list rather than shipping an empty square;
|
|
30
|
+
* production skips the check, so no name list reaches the page.
|
|
31
|
+
*/
|
|
32
|
+
export function iconSymbol(name: string): string {
|
|
33
|
+
const id = lucideName(name);
|
|
34
|
+
if (process.env["NODE_ENV"] !== "production" && !warned.has(name) && !isRegisteredIcon(name)) {
|
|
35
|
+
warned.add(name);
|
|
36
|
+
console.warn(
|
|
37
|
+
`@xos/ui Icon: "${name}" is not in the @xos/icons registry. Add ${id} to packages/icons/registry/<owner>.txt and run pnpm --filter @xos/icons generate:registry.`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return id;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function isMirrored(name: string): boolean {
|
|
44
|
+
return MIRRORED.has(lucideName(name));
|
|
45
|
+
}
|