@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,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session's undo stack (Section 11.7, ADR 0044, ADR 0082): the last 50 reversible actions in
|
|
3
|
+
* this tab, in order. Cmd or Ctrl+Z undoes the latest and Shift+Cmd or Ctrl+Z redoes it (each shell
|
|
4
|
+
* answers the registry's `undo` and `redo` shortcuts); inside a text field the keys stay the field's
|
|
5
|
+
* own. Atlas and Gateway share this module (`@xos/ui/undo`); each app's bundle holds its own stack.
|
|
6
|
+
*
|
|
7
|
+
* Every entry undoes through the server: a bulk change sends its inverse rows back through the
|
|
8
|
+
* same RPC, a trashed record is restored. The server refuses an undo that would overwrite a later
|
|
9
|
+
* change by someone else, and the entry then stays where it was, so nothing is lost silently. Toasts
|
|
10
|
+
* show one entry; their Undo button and Cmd or Ctrl+Z act on the same stack.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type UndoResult = { readonly ok: true } | { readonly ok: false; readonly message: string };
|
|
14
|
+
|
|
15
|
+
export interface UndoAction {
|
|
16
|
+
/** What the action did, in a sentence: "Changed 3 records." */
|
|
17
|
+
readonly label: string;
|
|
18
|
+
readonly undo: () => Promise<UndoResult>;
|
|
19
|
+
/** Redoes the action; without it, an undone action leaves the stack. */
|
|
20
|
+
readonly redo?: () => Promise<UndoResult>;
|
|
21
|
+
/** False when the caller shows its own Undo toast; the shell shows one otherwise. */
|
|
22
|
+
readonly toast?: boolean;
|
|
23
|
+
/** The toast button's label when undoing is not a plain undo, such as "Move back" (B4); Undo by default. */
|
|
24
|
+
readonly actionLabel?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface UndoEntry extends UndoAction {
|
|
28
|
+
readonly id: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type UndoEvent =
|
|
32
|
+
| { readonly kind: "pushed"; readonly entry: UndoEntry }
|
|
33
|
+
| { readonly kind: "undone" | "redone"; readonly entry: UndoEntry }
|
|
34
|
+
| { readonly kind: "failed"; readonly entry: UndoEntry; readonly message: string }
|
|
35
|
+
| { readonly kind: "empty"; readonly direction: "undo" | "redo" };
|
|
36
|
+
|
|
37
|
+
export const UNDO_LIMIT = 50;
|
|
38
|
+
|
|
39
|
+
export interface UndoStack {
|
|
40
|
+
push(action: UndoAction): UndoEntry;
|
|
41
|
+
/** Undoes the latest action, or the given one (a toast's Undo button). */
|
|
42
|
+
undo(id?: number): Promise<void>;
|
|
43
|
+
redo(): Promise<void>;
|
|
44
|
+
/** Whether an action is still undoable (a toast hides its button once it is not). */
|
|
45
|
+
has(id: number): boolean;
|
|
46
|
+
size(): { readonly past: number; readonly future: number };
|
|
47
|
+
subscribe(listener: (event: UndoEvent) => void): () => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createUndoStack(limit = UNDO_LIMIT): UndoStack {
|
|
51
|
+
let past: UndoEntry[] = [];
|
|
52
|
+
let future: UndoEntry[] = [];
|
|
53
|
+
let next = 1;
|
|
54
|
+
let busy = false;
|
|
55
|
+
const listeners = new Set<(event: UndoEvent) => void>();
|
|
56
|
+
const emit = (event: UndoEvent) => {
|
|
57
|
+
for (const listener of listeners) listener(event);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
push(action) {
|
|
62
|
+
const entry: UndoEntry = { ...action, id: next };
|
|
63
|
+
next += 1;
|
|
64
|
+
past = [...past, entry].slice(-limit);
|
|
65
|
+
future = [];
|
|
66
|
+
emit({ kind: "pushed", entry });
|
|
67
|
+
return entry;
|
|
68
|
+
},
|
|
69
|
+
async undo(id) {
|
|
70
|
+
if (busy) return;
|
|
71
|
+
const entry = id === undefined ? past.at(-1) : past.find((e) => e.id === id);
|
|
72
|
+
if (!entry) {
|
|
73
|
+
emit({ kind: "empty", direction: "undo" });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
busy = true;
|
|
77
|
+
try {
|
|
78
|
+
const result = await entry.undo();
|
|
79
|
+
if (!result.ok) {
|
|
80
|
+
emit({ kind: "failed", entry, message: result.message });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
past = past.filter((e) => e !== entry);
|
|
84
|
+
if (entry.redo) future = [...future, entry].slice(-limit);
|
|
85
|
+
emit({ kind: "undone", entry });
|
|
86
|
+
} catch (error) {
|
|
87
|
+
emit({ kind: "failed", entry, message: error instanceof Error ? error.message : "" });
|
|
88
|
+
} finally {
|
|
89
|
+
busy = false;
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
async redo() {
|
|
93
|
+
if (busy) return;
|
|
94
|
+
const entry = future.at(-1);
|
|
95
|
+
if (!entry?.redo) {
|
|
96
|
+
emit({ kind: "empty", direction: "redo" });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
busy = true;
|
|
100
|
+
try {
|
|
101
|
+
const result = await entry.redo();
|
|
102
|
+
if (!result.ok) {
|
|
103
|
+
emit({ kind: "failed", entry, message: result.message });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
future = future.filter((e) => e !== entry);
|
|
107
|
+
past = [...past, entry].slice(-limit);
|
|
108
|
+
emit({ kind: "redone", entry });
|
|
109
|
+
} catch (error) {
|
|
110
|
+
emit({ kind: "failed", entry, message: error instanceof Error ? error.message : "" });
|
|
111
|
+
} finally {
|
|
112
|
+
busy = false;
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
has: (id) => past.some((e) => e.id === id),
|
|
116
|
+
size: () => ({ past: past.length, future: future.length }),
|
|
117
|
+
subscribe(listener) {
|
|
118
|
+
listeners.add(listener);
|
|
119
|
+
return () => listeners.delete(listener);
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The tab's stack; it survives client navigation and ends with the tab. */
|
|
125
|
+
export const undoStack: UndoStack = createUndoStack();
|
package/src/marketing.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Marketing layer for public pages (design handoff section 10, decision D20). A server
|
|
3
|
+
* component imports from here: the components that hold no state use no hooks and no context, so
|
|
4
|
+
* they render on the server and send no JavaScript, and the five that hold state (MarketingNav,
|
|
5
|
+
* PersonaSwitcher, PricingTable, CodeSample and MarketingFooter) are client modules of their own
|
|
6
|
+
* (ADR 0027). The server-rendered ones that print a shared string or format a number take the
|
|
7
|
+
* page's `locale` and `messages` (`flattenMessages` turns a locale's nested `ui` catalog into that
|
|
8
|
+
* form); the five client ones read the nearest XOSProvider.
|
|
9
|
+
*
|
|
10
|
+
* Styles: import "@xos/ui/marketing.css" after "@xos/ui/styles.css".
|
|
11
|
+
*/
|
|
12
|
+
export type * from "./Marketing/Marketing.types.ts";
|
|
13
|
+
export { marketingCopy, type MarketingCopy } from "./Marketing/marketing-copy.ts";
|
|
14
|
+
export { flattenMessages, type Messages } from "./lib/i18n.ts";
|
|
15
|
+
export { ButtonLink } from "./Marketing/ButtonLink.tsx";
|
|
16
|
+
export { MarketingSection } from "./Marketing/MarketingSection.tsx";
|
|
17
|
+
export { MarketingHero } from "./Marketing/MarketingHero.tsx";
|
|
18
|
+
export { ScreenshotFrame } from "./Marketing/ScreenshotFrame.tsx";
|
|
19
|
+
export { ProofStatStrip } from "./Marketing/ProofStatStrip.tsx";
|
|
20
|
+
export { GateLifecycleRail } from "./Marketing/GateLifecycleRail.tsx";
|
|
21
|
+
export { SurfaceTriptych } from "./Marketing/SurfaceTriptych.tsx";
|
|
22
|
+
export { FeatureGrid } from "./Marketing/FeatureGrid.tsx";
|
|
23
|
+
export { ComparisonTable } from "./Marketing/ComparisonTable.tsx";
|
|
24
|
+
export { PriceDisplay } from "./Marketing/PriceDisplay.tsx";
|
|
25
|
+
export { FAQ } from "./Marketing/FAQ.tsx";
|
|
26
|
+
export { Testimonial } from "./Marketing/Testimonial.tsx";
|
|
27
|
+
export { LogoSlotWall } from "./Marketing/LogoSlotWall.tsx";
|
|
28
|
+
export { AIDisclosureBadge } from "./Marketing/AIDisclosureBadge.tsx";
|
|
29
|
+
export { MarketingNav } from "./Marketing/MarketingNav.tsx";
|
|
30
|
+
export { PersonaSwitcher } from "./Marketing/PersonaSwitcher.tsx";
|
|
31
|
+
export { PricingTable } from "./Marketing/PricingTable.tsx";
|
|
32
|
+
export { CodeSample } from "./Marketing/CodeSample.tsx";
|
|
33
|
+
export { MarketingFooter } from "./Marketing/MarketingFooter.tsx";
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useContext,
|
|
6
|
+
useEffect,
|
|
7
|
+
useLayoutEffect,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
} from "react";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Runs a callback when a form's server action succeeded, for funnel signals (A146, ADR 0052) and the like, while the
|
|
15
|
+
* form keeps the server action itself in useActionState, so it still posts without JavaScript (ADR 0001).
|
|
16
|
+
*
|
|
17
|
+
* A form that stays on the page sees its result in an effect. A form whose row leaves the page with the result (an
|
|
18
|
+
* answer that leaves the review queue once reviewed) never commits that result, so it cannot: the revalidated page
|
|
19
|
+
* drops it in the same commit. Wrap such rows in an ActionOutcomeScope, which stays mounted. A form that left while
|
|
20
|
+
* its action was pending counts as succeeded when the scope's `token`, the page's account of what those forms change,
|
|
21
|
+
* changed in the same commit; a refused action leaves the row and the token as they were.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
interface Waiting {
|
|
25
|
+
readonly succeeded: () => void;
|
|
26
|
+
left: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const OutcomeScope = createContext<Map<object, Waiting> | null>(null);
|
|
30
|
+
|
|
31
|
+
export function ActionOutcomeScope({ token, children }: { token: string; children: ReactNode }) {
|
|
32
|
+
const [waiting] = useState(() => new Map<object, Waiting>());
|
|
33
|
+
const last = useRef(token);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (last.current === token) return;
|
|
36
|
+
last.current = token;
|
|
37
|
+
for (const [form, entry] of waiting) {
|
|
38
|
+
if (!entry.left) continue;
|
|
39
|
+
waiting.delete(form);
|
|
40
|
+
entry.succeeded();
|
|
41
|
+
}
|
|
42
|
+
}, [token, waiting]);
|
|
43
|
+
return <OutcomeScope value={waiting}>{children}</OutcomeScope>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Calls `onSuccess` once for each result of the form's action that `succeeded` accepts, `state` and `pending` being
|
|
48
|
+
* what useActionState returned. Inside an ActionOutcomeScope it also covers a form that leaves with its result.
|
|
49
|
+
*/
|
|
50
|
+
export function useActionOutcome<S>(
|
|
51
|
+
state: S,
|
|
52
|
+
pending: boolean,
|
|
53
|
+
succeeded: (state: S) => boolean,
|
|
54
|
+
onSuccess: (() => void) | undefined,
|
|
55
|
+
): void {
|
|
56
|
+
const scope = useContext(OutcomeScope);
|
|
57
|
+
const [form] = useState(() => ({}));
|
|
58
|
+
const seen = useRef(state);
|
|
59
|
+
const latest = useRef({ succeeded, onSuccess });
|
|
60
|
+
useLayoutEffect(() => {
|
|
61
|
+
latest.current = { succeeded, onSuccess };
|
|
62
|
+
});
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (!pending || !scope) return;
|
|
65
|
+
scope.set(form, { succeeded: () => latest.current.onSuccess?.(), left: false });
|
|
66
|
+
}, [pending, scope, form]);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (state === seen.current) return;
|
|
69
|
+
seen.current = state;
|
|
70
|
+
scope?.delete(form);
|
|
71
|
+
if (latest.current.succeeded(state)) latest.current.onSuccess?.();
|
|
72
|
+
}, [state, scope, form]);
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
const entry = scope?.get(form);
|
|
75
|
+
if (entry) entry.left = false;
|
|
76
|
+
return () => {
|
|
77
|
+
const waiting = scope?.get(form);
|
|
78
|
+
if (waiting) waiting.left = true;
|
|
79
|
+
};
|
|
80
|
+
}, [scope, form]);
|
|
81
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser side of consent-gated product analytics (Section 11.14, A96, ADR 0052). Apps load this module only
|
|
3
|
+
* when their consent seam said yes, and it stops itself when the browser sends Global Privacy Control. Events carry
|
|
4
|
+
* a route template (never the path), a workflow and a step, keyed to a random session key kept in sessionStorage,
|
|
5
|
+
* and go to the app's own /api/product-events in small batches. Nothing goes to another origin.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type WorkflowEvent = "Workflow Started" | "Workflow Step" | "Workflow Completed";
|
|
9
|
+
|
|
10
|
+
interface QueuedEvent {
|
|
11
|
+
readonly occurredAt: string;
|
|
12
|
+
readonly name: "Page Viewed" | WorkflowEvent;
|
|
13
|
+
readonly route?: string;
|
|
14
|
+
readonly workflow?: string;
|
|
15
|
+
readonly step?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A workflow event a signal records (packages/data product-workflows.ts parseWorkflowSignal). */
|
|
19
|
+
export interface WorkflowSignalEvent {
|
|
20
|
+
readonly workflow: string;
|
|
21
|
+
readonly name: WorkflowEvent;
|
|
22
|
+
readonly step?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Workflow funnel signals (A146): the app's registry, passed in so this module carries none. A signal is
|
|
27
|
+
* `<workflow>:start`, `<workflow>:step<n>` or `<workflow>:done`, from a page view (`forRoute`), a `data-workflow`
|
|
28
|
+
* attribute or a window event named `eventName`.
|
|
29
|
+
*/
|
|
30
|
+
export interface WorkflowSignals {
|
|
31
|
+
readonly eventName: string;
|
|
32
|
+
readonly parse: (signal: unknown) => WorkflowSignalEvent | null;
|
|
33
|
+
readonly forRoute: (template: string) => readonly string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ProductAnalyticsOptions {
|
|
37
|
+
readonly endpoint: string;
|
|
38
|
+
/** Turns a path into its route template, so no slug, key or token leaves the browser. */
|
|
39
|
+
readonly template: (path: string) => string;
|
|
40
|
+
/** Sends a batch; defaults to a keepalive POST. */
|
|
41
|
+
readonly send?: (body: string) => void;
|
|
42
|
+
readonly now?: () => Date;
|
|
43
|
+
/** Records workflow funnel events from signals; without it only page views are recorded. */
|
|
44
|
+
readonly workflows?: WorkflowSignals;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ProductAnalytics {
|
|
48
|
+
pageViewed(path: string): void;
|
|
49
|
+
workflow(code: string, event: WorkflowEvent, step?: number): void;
|
|
50
|
+
flush(): void;
|
|
51
|
+
/** Sends what is queued and stops listening. */
|
|
52
|
+
stop(): void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const SESSION_KEY = "xos-analytics-session";
|
|
56
|
+
const STARTED_KEY = "xos-analytics-started";
|
|
57
|
+
// Elements that signal when activated; any other marked element (but a form) signals when it appears.
|
|
58
|
+
const INTERACTIVE =
|
|
59
|
+
"a,button,input,select,textarea,summary,[role=button],[role=link],[role=menuitem],[role=option],[role=tab]";
|
|
60
|
+
const BATCH = 10;
|
|
61
|
+
const FLUSH_MS = 5000;
|
|
62
|
+
|
|
63
|
+
function sessionKey(): string {
|
|
64
|
+
try {
|
|
65
|
+
const stored = window.sessionStorage.getItem(SESSION_KEY);
|
|
66
|
+
if (stored) return stored;
|
|
67
|
+
const fresh = crypto.randomUUID();
|
|
68
|
+
window.sessionStorage.setItem(SESSION_KEY, fresh);
|
|
69
|
+
return fresh;
|
|
70
|
+
} catch {
|
|
71
|
+
return crypto.randomUUID();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** True when the browser sends Global Privacy Control, which turns analytics off whatever was chosen. */
|
|
76
|
+
export function globalPrivacyControl(): boolean {
|
|
77
|
+
return (
|
|
78
|
+
(navigator as Navigator & { globalPrivacyControl?: boolean }).globalPrivacyControl === true
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Workflows this browser session started, so a step or completion counts only after its start. */
|
|
83
|
+
function startedWorkflows(): Set<string> {
|
|
84
|
+
try {
|
|
85
|
+
const stored = JSON.parse(window.sessionStorage.getItem(STARTED_KEY) ?? "[]") as unknown;
|
|
86
|
+
return new Set(Array.isArray(stored) ? stored.filter((v) => typeof v === "string") : []);
|
|
87
|
+
} catch {
|
|
88
|
+
return new Set();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function rememberStarted(started: Set<string>): void {
|
|
93
|
+
try {
|
|
94
|
+
window.sessionStorage.setItem(STARTED_KEY, JSON.stringify([...started]));
|
|
95
|
+
} catch {
|
|
96
|
+
// Storage blocked: the set lasts as long as the page.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const NOOP: ProductAnalytics = {
|
|
101
|
+
pageViewed: () => undefined,
|
|
102
|
+
workflow: () => undefined,
|
|
103
|
+
flush: () => undefined,
|
|
104
|
+
stop: () => undefined,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export function productAnalytics(options: ProductAnalyticsOptions): ProductAnalytics {
|
|
108
|
+
if (globalPrivacyControl()) return NOOP;
|
|
109
|
+
const key = sessionKey();
|
|
110
|
+
const now = options.now ?? (() => new Date());
|
|
111
|
+
const send =
|
|
112
|
+
options.send ??
|
|
113
|
+
((body: string) => {
|
|
114
|
+
void fetch(options.endpoint, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
keepalive: true,
|
|
117
|
+
credentials: "same-origin",
|
|
118
|
+
headers: { "Content-Type": "application/json" },
|
|
119
|
+
body,
|
|
120
|
+
}).catch(() => undefined);
|
|
121
|
+
});
|
|
122
|
+
let queue: QueuedEvent[] = [];
|
|
123
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
124
|
+
|
|
125
|
+
const flush = () => {
|
|
126
|
+
if (timer !== null) clearTimeout(timer);
|
|
127
|
+
timer = null;
|
|
128
|
+
if (queue.length === 0) return;
|
|
129
|
+
const events = queue.slice(0, 25);
|
|
130
|
+
queue = queue.slice(25);
|
|
131
|
+
send(JSON.stringify({ sessionKey: key, events }));
|
|
132
|
+
};
|
|
133
|
+
const add = (event: QueuedEvent) => {
|
|
134
|
+
queue.push(event);
|
|
135
|
+
if (queue.length >= BATCH) flush();
|
|
136
|
+
else timer ??= setTimeout(flush, FLUSH_MS);
|
|
137
|
+
};
|
|
138
|
+
const listening = new AbortController();
|
|
139
|
+
const { signal: until } = listening;
|
|
140
|
+
window.addEventListener("pagehide", flush, { signal: until });
|
|
141
|
+
document.addEventListener(
|
|
142
|
+
"visibilitychange",
|
|
143
|
+
() => {
|
|
144
|
+
if (document.visibilityState === "hidden") flush();
|
|
145
|
+
},
|
|
146
|
+
{ signal: until },
|
|
147
|
+
);
|
|
148
|
+
let observer: MutationObserver | null = null;
|
|
149
|
+
|
|
150
|
+
const workflows = options.workflows;
|
|
151
|
+
const started = workflows ? startedWorkflows() : new Set<string>();
|
|
152
|
+
// A signal the registry names becomes an event. A step or a completion counts only once the session started the
|
|
153
|
+
// workflow, so a return visit to a finished packet or an empty queue is not a completion.
|
|
154
|
+
const signal = (value: unknown) => {
|
|
155
|
+
const event = workflows?.parse(value);
|
|
156
|
+
if (!event) return;
|
|
157
|
+
if (event.name === "Workflow Started") {
|
|
158
|
+
started.add(event.workflow);
|
|
159
|
+
rememberStarted(started);
|
|
160
|
+
} else if (!started.has(event.workflow)) return;
|
|
161
|
+
add({
|
|
162
|
+
occurredAt: now().toISOString(),
|
|
163
|
+
name: event.name,
|
|
164
|
+
workflow: event.workflow,
|
|
165
|
+
...(event.step === undefined ? {} : { step: event.step }),
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
if (workflows) {
|
|
169
|
+
const seen = new WeakSet<Element>();
|
|
170
|
+
const appeared = (root: ParentNode) => {
|
|
171
|
+
const marked = [
|
|
172
|
+
...(root instanceof Element && root.matches("[data-workflow]") ? [root] : []),
|
|
173
|
+
...root.querySelectorAll("[data-workflow]"),
|
|
174
|
+
];
|
|
175
|
+
for (const el of marked) {
|
|
176
|
+
if (seen.has(el) || el.matches(INTERACTIVE) || el instanceof HTMLFormElement) continue;
|
|
177
|
+
seen.add(el);
|
|
178
|
+
signal(el.getAttribute("data-workflow"));
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
window.addEventListener(workflows.eventName, (e) => signal((e as CustomEvent).detail), {
|
|
182
|
+
signal: until,
|
|
183
|
+
});
|
|
184
|
+
document.addEventListener(
|
|
185
|
+
"click",
|
|
186
|
+
(e) => {
|
|
187
|
+
const el = e.target instanceof Element ? e.target.closest("[data-workflow]") : null;
|
|
188
|
+
if (el?.matches(INTERACTIVE)) signal(el.getAttribute("data-workflow"));
|
|
189
|
+
},
|
|
190
|
+
{ capture: true, signal: until },
|
|
191
|
+
);
|
|
192
|
+
document.addEventListener(
|
|
193
|
+
"submit",
|
|
194
|
+
(e) => {
|
|
195
|
+
if (e.target instanceof HTMLFormElement) signal(e.target.getAttribute("data-workflow"));
|
|
196
|
+
},
|
|
197
|
+
{ capture: true, signal: until },
|
|
198
|
+
);
|
|
199
|
+
appeared(document);
|
|
200
|
+
const watcher = new MutationObserver((records) => {
|
|
201
|
+
for (const r of records) {
|
|
202
|
+
if (r.type === "attributes" && r.target instanceof Element) appeared(r.target);
|
|
203
|
+
for (const node of r.addedNodes) if (node instanceof Element) appeared(node);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
watcher.observe(document.documentElement, {
|
|
207
|
+
childList: true,
|
|
208
|
+
subtree: true,
|
|
209
|
+
attributes: true,
|
|
210
|
+
attributeFilter: ["data-workflow"],
|
|
211
|
+
});
|
|
212
|
+
observer = watcher;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
pageViewed(path) {
|
|
217
|
+
const route = options.template(path);
|
|
218
|
+
add({ occurredAt: now().toISOString(), name: "Page Viewed", route });
|
|
219
|
+
for (const s of workflows?.forRoute(route) ?? []) signal(s);
|
|
220
|
+
},
|
|
221
|
+
workflow(code, event, step) {
|
|
222
|
+
add({
|
|
223
|
+
occurredAt: now().toISOString(),
|
|
224
|
+
name: event,
|
|
225
|
+
workflow: code,
|
|
226
|
+
...(step === undefined ? {} : { step }),
|
|
227
|
+
});
|
|
228
|
+
},
|
|
229
|
+
flush,
|
|
230
|
+
stop() {
|
|
231
|
+
flush();
|
|
232
|
+
listening.abort();
|
|
233
|
+
observer?.disconnect();
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, type ReactElement } from "react";
|
|
4
|
+
import { Button } from "../Button/Button.tsx";
|
|
5
|
+
import { Card } from "../Surface/Surface.tsx";
|
|
6
|
+
import {
|
|
7
|
+
currentPushSubscription,
|
|
8
|
+
pushSupported,
|
|
9
|
+
subscribeToPush,
|
|
10
|
+
unsubscribeFromPush,
|
|
11
|
+
} from "./client.ts";
|
|
12
|
+
|
|
13
|
+
/** The app's copy for the browser notifications setting (A96); apps pass their own catalog strings. */
|
|
14
|
+
export interface BrowserPushLabels {
|
|
15
|
+
readonly title: string;
|
|
16
|
+
readonly body: string;
|
|
17
|
+
readonly turnOn: string;
|
|
18
|
+
readonly turnOff: string;
|
|
19
|
+
readonly on: string;
|
|
20
|
+
readonly off: string;
|
|
21
|
+
readonly blocked: string;
|
|
22
|
+
readonly unsupported: string;
|
|
23
|
+
readonly unavailable: string;
|
|
24
|
+
readonly failed: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface BrowserPushSettingProps {
|
|
28
|
+
/** The VAPID public key, base64url; null when the platform has not set one up. */
|
|
29
|
+
readonly vapidPublicKey: string | null;
|
|
30
|
+
readonly labels: BrowserPushLabels;
|
|
31
|
+
/** Saves the subscription (PushSubscription.toJSON()) for the signed-in person; false when it did not. */
|
|
32
|
+
readonly onRegister: (subscription: PushSubscriptionJSON) => Promise<boolean>;
|
|
33
|
+
/** Revokes this browser's endpoint for the signed-in person. */
|
|
34
|
+
readonly onRevoke: (endpoint: string) => Promise<boolean>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type State = "loading" | "on" | "off" | "blocked" | "unsupported" | "unavailable";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Browser notifications for this browser (Section 4.10, A65-023): turned on only when the person asks, which is
|
|
41
|
+
* when the browser asks its permission, and off again in one action. Quiet hours and channel choices stay with the
|
|
42
|
+
* notification settings around it, since browser pushes ride the Push channel.
|
|
43
|
+
*/
|
|
44
|
+
export function BrowserPushSetting({
|
|
45
|
+
vapidPublicKey,
|
|
46
|
+
labels,
|
|
47
|
+
onRegister,
|
|
48
|
+
onRevoke,
|
|
49
|
+
}: BrowserPushSettingProps): ReactElement {
|
|
50
|
+
const [state, setState] = useState<State>("loading");
|
|
51
|
+
const [busy, setBusy] = useState(false);
|
|
52
|
+
const [failed, setFailed] = useState(false);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
let live = true;
|
|
56
|
+
const settle = (next: State) => {
|
|
57
|
+
if (live) setState(next);
|
|
58
|
+
};
|
|
59
|
+
if (vapidPublicKey === null) settle("unavailable");
|
|
60
|
+
else if (!pushSupported()) settle("unsupported");
|
|
61
|
+
else if (Notification.permission === "denied") settle("blocked");
|
|
62
|
+
else
|
|
63
|
+
void currentPushSubscription().then(
|
|
64
|
+
(s) => settle(s ? "on" : "off"),
|
|
65
|
+
() => settle("off"),
|
|
66
|
+
);
|
|
67
|
+
return () => {
|
|
68
|
+
live = false;
|
|
69
|
+
};
|
|
70
|
+
}, [vapidPublicKey]);
|
|
71
|
+
|
|
72
|
+
const turnOn = async () => {
|
|
73
|
+
if (vapidPublicKey === null) return;
|
|
74
|
+
setBusy(true);
|
|
75
|
+
setFailed(false);
|
|
76
|
+
const result = await subscribeToPush(vapidPublicKey);
|
|
77
|
+
if (!result.ok) {
|
|
78
|
+
if (result.reason === "blocked") setState("blocked");
|
|
79
|
+
else if (result.reason === "unsupported") setState("unsupported");
|
|
80
|
+
else setFailed(true);
|
|
81
|
+
} else if (await onRegister(result.subscription)) setState("on");
|
|
82
|
+
else {
|
|
83
|
+
await unsubscribeFromPush();
|
|
84
|
+
setFailed(true);
|
|
85
|
+
}
|
|
86
|
+
setBusy(false);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const turnOff = async () => {
|
|
90
|
+
setBusy(true);
|
|
91
|
+
setFailed(false);
|
|
92
|
+
const endpoint = await unsubscribeFromPush();
|
|
93
|
+
if (endpoint !== null && !(await onRevoke(endpoint))) setFailed(true);
|
|
94
|
+
setState("off");
|
|
95
|
+
setBusy(false);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const status =
|
|
99
|
+
state === "on"
|
|
100
|
+
? labels.on
|
|
101
|
+
: state === "off"
|
|
102
|
+
? labels.off
|
|
103
|
+
: state === "blocked"
|
|
104
|
+
? labels.blocked
|
|
105
|
+
: state === "unsupported"
|
|
106
|
+
? labels.unsupported
|
|
107
|
+
: state === "unavailable"
|
|
108
|
+
? labels.unavailable
|
|
109
|
+
: "";
|
|
110
|
+
return (
|
|
111
|
+
<Card title={labels.title}>
|
|
112
|
+
<p className="m-0 text-text-13 text-text-secondary">{labels.body}</p>
|
|
113
|
+
<p role="status" className="m-0 text-text-13" data-push-state={state}>
|
|
114
|
+
{status}
|
|
115
|
+
</p>
|
|
116
|
+
{state === "on" ? (
|
|
117
|
+
<Button onClick={() => void turnOff()} disabled={busy}>
|
|
118
|
+
{labels.turnOff}
|
|
119
|
+
</Button>
|
|
120
|
+
) : state === "off" ? (
|
|
121
|
+
<Button variant="primary" icon="Bell" onClick={() => void turnOn()} disabled={busy}>
|
|
122
|
+
{labels.turnOn}
|
|
123
|
+
</Button>
|
|
124
|
+
) : null}
|
|
125
|
+
{failed ? (
|
|
126
|
+
<span role="alert" className="text-text-12 text-danger-text">
|
|
127
|
+
{labels.failed}
|
|
128
|
+
</span>
|
|
129
|
+
) : null}
|
|
130
|
+
</Card>
|
|
131
|
+
);
|
|
132
|
+
}
|