@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,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The consent record behind ConsentBanner and ConsentSettings (Section 14.3). The components never
|
|
3
|
+
* know where a decision is kept: they read and write through `ConsentAdapter`. Atlas implements it
|
|
4
|
+
* over its consent log in the platform database (apps/atlas/lib/privacy/consent-adapter.ts); a site
|
|
5
|
+
* with no database uses `cookieConsentAdapter`, which keeps the whole decision in one first-party
|
|
6
|
+
* cookie. This module has no React and no DOM requirement, so a server can import it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** What the visitor pressed: accept every optional category, refuse them all, or save the ticked ones. */
|
|
10
|
+
export type ConsentChoice = "all" | "none" | "save";
|
|
11
|
+
|
|
12
|
+
/** The consent in force: each optional category by name, and whether it came from a privacy signal. */
|
|
13
|
+
export interface ConsentState {
|
|
14
|
+
/** Optional categories by name, true when allowed. Strictly necessary cookies are never listed. */
|
|
15
|
+
readonly categories: Readonly<Record<string, boolean>>;
|
|
16
|
+
/** The decision came from a Global Privacy Control signal, which refuses every optional category. */
|
|
17
|
+
readonly gpc: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** One decision as it is written: the consent, when it was made and the policy version shown. */
|
|
21
|
+
export interface ConsentDecision extends ConsentState {
|
|
22
|
+
/** When the visitor decided, ISO 8601 in UTC. */
|
|
23
|
+
readonly decidedAt: string;
|
|
24
|
+
/** The version of the cookie policy and banner the visitor saw. */
|
|
25
|
+
readonly policyVersion: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Where consent is kept. Implement both methods; the components call nothing else. */
|
|
29
|
+
export interface ConsentAdapter {
|
|
30
|
+
/** The consent in force for this visitor, or null when they have not decided. */
|
|
31
|
+
read(): Promise<ConsentState | null>;
|
|
32
|
+
/** Stores a decision. It replaces any earlier one. */
|
|
33
|
+
write(decision: ConsentDecision): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The rule for a category name, on write and on read alike: a letter, then letters, digits,
|
|
38
|
+
* underscores or hyphens, 64 characters at most. `preferences`, `analytics_storage` and `Marketing`
|
|
39
|
+
* fit; a name with a space or a semicolon does not.
|
|
40
|
+
*/
|
|
41
|
+
export const CONSENT_CATEGORY_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
|
|
42
|
+
|
|
43
|
+
/** True when a request carries Global Privacy Control (`Sec-GPC: 1`). */
|
|
44
|
+
export function sendsGlobalPrivacyControl(headers: { get(name: string): string | null }): boolean {
|
|
45
|
+
return headers.get("sec-gpc")?.trim() === "1";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** True when the browser reports Global Privacy Control (`navigator.globalPrivacyControl`). */
|
|
49
|
+
export function browserGlobalPrivacyControl(
|
|
50
|
+
nav: unknown = typeof navigator === "undefined" ? undefined : navigator,
|
|
51
|
+
): boolean {
|
|
52
|
+
if (typeof nav !== "object" || nav === null) return false;
|
|
53
|
+
const signal = (nav as { globalPrivacyControl?: unknown }).globalPrivacyControl;
|
|
54
|
+
return signal === true || signal === "1";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface DecideConsentInput {
|
|
58
|
+
readonly choice: ConsentChoice;
|
|
59
|
+
/** Every optional category the surface offers. */
|
|
60
|
+
readonly categories: readonly string[];
|
|
61
|
+
/** The categories ticked, read only for the `save` choice. */
|
|
62
|
+
readonly selected?: Iterable<string>;
|
|
63
|
+
/** A Global Privacy Control signal is present, from the request or the browser. */
|
|
64
|
+
readonly gpc: boolean;
|
|
65
|
+
readonly policyVersion: string;
|
|
66
|
+
/** The time of the decision; now when omitted. */
|
|
67
|
+
readonly now?: Date;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Turns a choice into the decision to write. A Global Privacy Control signal refuses every optional
|
|
72
|
+
* category whatever was pressed or ticked, and the decision records the signal.
|
|
73
|
+
*/
|
|
74
|
+
export function decideConsent(input: DecideConsentInput): ConsentDecision {
|
|
75
|
+
const selected = new Set(input.selected ?? []);
|
|
76
|
+
const allowed = (name: string): boolean =>
|
|
77
|
+
!input.gpc && (input.choice === "all" || (input.choice === "save" && selected.has(name)));
|
|
78
|
+
return {
|
|
79
|
+
categories: Object.fromEntries(input.categories.map((name) => [name, allowed(name)])),
|
|
80
|
+
gpc: input.gpc,
|
|
81
|
+
decidedAt: (input.now ?? new Date()).toISOString(),
|
|
82
|
+
policyVersion: input.policyVersion,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Whether a category is allowed now: consented, not decided by a signal, and no signal present. */
|
|
87
|
+
export function consentAllows(state: ConsentState | null, category: string, gpc: boolean): boolean {
|
|
88
|
+
return state !== null && !state.gpc && !gpc && state.categories[category] === true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// The database-free reference implementation: one first-party cookie -----------------------------
|
|
92
|
+
|
|
93
|
+
/** The cookie `cookieConsentAdapter` writes unless it is given another name. */
|
|
94
|
+
export const CONSENT_DECISION_COOKIE = "xos-consent-decision";
|
|
95
|
+
/** How long the decision is remembered before the banner asks again: 12 months. */
|
|
96
|
+
export const CONSENT_DECISION_MAX_AGE = 60 * 60 * 24 * 365;
|
|
97
|
+
|
|
98
|
+
export interface ConsentCookieOptions {
|
|
99
|
+
readonly path: "/";
|
|
100
|
+
readonly sameSite: "lax";
|
|
101
|
+
readonly secure: boolean;
|
|
102
|
+
/** Seconds. */
|
|
103
|
+
readonly maxAge: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The two cookie operations the reference adapter needs; a browser or a server response fits.
|
|
108
|
+
* Encoding is the store's job: `get` returns and `set` receives the decoded value, which is JSON
|
|
109
|
+
* with quotes and commas in it. A framework's cookie jar (Next.js `cookies()`) already encodes on
|
|
110
|
+
* the way out and decodes on the way in, as `documentCookieStore` does, so a decision one of them
|
|
111
|
+
* writes is read by the other.
|
|
112
|
+
*/
|
|
113
|
+
export interface ConsentCookieStore {
|
|
114
|
+
get(name: string): string | undefined;
|
|
115
|
+
set(name: string, value: string, options: ConsentCookieOptions): void;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The cookie value for a decision: versioned JSON, not encoded. Throws on a category name outside
|
|
120
|
+
* `CONSENT_CATEGORY_NAME`, which `parseConsentDecision` would refuse to read back.
|
|
121
|
+
*/
|
|
122
|
+
export function serializeConsentDecision(decision: ConsentDecision): string {
|
|
123
|
+
for (const name of Object.keys(decision.categories)) {
|
|
124
|
+
if (!CONSENT_CATEGORY_NAME.test(name))
|
|
125
|
+
throw new TypeError(
|
|
126
|
+
`Consent category name ${JSON.stringify(name)} must match ${String(CONSENT_CATEGORY_NAME)}.`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
return JSON.stringify({
|
|
130
|
+
v: 1,
|
|
131
|
+
at: decision.decidedAt,
|
|
132
|
+
policy: decision.policyVersion,
|
|
133
|
+
gpc: decision.gpc,
|
|
134
|
+
categories: decision.categories,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The decision a decoded cookie value holds, or null when it is missing or not one this version wrote. */
|
|
139
|
+
export function parseConsentDecision(value: string | undefined | null): ConsentDecision | null {
|
|
140
|
+
if (!value) return null;
|
|
141
|
+
let raw: unknown;
|
|
142
|
+
try {
|
|
143
|
+
raw = JSON.parse(value);
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
148
|
+
const { v, at, policy, gpc, categories } = raw as Record<string, unknown>;
|
|
149
|
+
if (v !== 1 || typeof gpc !== "boolean") return null;
|
|
150
|
+
if (typeof policy !== "string" || policy === "") return null;
|
|
151
|
+
if (typeof at !== "string" || Number.isNaN(Date.parse(at))) return null;
|
|
152
|
+
if (typeof categories !== "object" || categories === null || Array.isArray(categories))
|
|
153
|
+
return null;
|
|
154
|
+
const entries = Object.entries(categories);
|
|
155
|
+
if (!entries.every(([name, on]) => CONSENT_CATEGORY_NAME.test(name) && typeof on === "boolean"))
|
|
156
|
+
return null;
|
|
157
|
+
return {
|
|
158
|
+
categories: Object.fromEntries(entries) as Record<string, boolean>,
|
|
159
|
+
gpc,
|
|
160
|
+
decidedAt: new Date(at).toISOString(),
|
|
161
|
+
policyVersion: policy,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* A cookie store over `document.cookie`, for a page with no server of its own. It percent-encodes
|
|
167
|
+
* the value it writes and decodes the one it reads; a pair it cannot decode reads as missing.
|
|
168
|
+
*/
|
|
169
|
+
export function documentCookieStore(
|
|
170
|
+
doc: { cookie: string } = document as { cookie: string },
|
|
171
|
+
): ConsentCookieStore {
|
|
172
|
+
return {
|
|
173
|
+
get(name) {
|
|
174
|
+
const prefix = `${name}=`;
|
|
175
|
+
const pair = doc.cookie.split("; ").find((entry) => entry.startsWith(prefix));
|
|
176
|
+
if (pair === undefined) return undefined;
|
|
177
|
+
try {
|
|
178
|
+
return decodeURIComponent(pair.slice(prefix.length));
|
|
179
|
+
} catch {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
set(name, value, options) {
|
|
184
|
+
doc.cookie = [
|
|
185
|
+
`${name}=${encodeURIComponent(value)}`,
|
|
186
|
+
`Path=${options.path}`,
|
|
187
|
+
`Max-Age=${options.maxAge}`,
|
|
188
|
+
"SameSite=Lax",
|
|
189
|
+
...(options.secure ? ["Secure"] : []),
|
|
190
|
+
].join("; ");
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface CookieConsentAdapterOptions {
|
|
196
|
+
/** Where the cookie is read and written; `document.cookie` when omitted. */
|
|
197
|
+
readonly store?: ConsentCookieStore;
|
|
198
|
+
/** The cookie name; `CONSENT_DECISION_COOKIE` when omitted. */
|
|
199
|
+
readonly name?: string;
|
|
200
|
+
/** Seconds the decision is remembered; 12 months when omitted. */
|
|
201
|
+
readonly maxAge?: number;
|
|
202
|
+
/** Marks the cookie Secure; true on an https page when omitted. */
|
|
203
|
+
readonly secure?: boolean;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The reference `ConsentAdapter` with no database: the decision, its timestamp, the policy version
|
|
208
|
+
* and the Global Privacy Control flag live in one first-party cookie. The cookie is strictly
|
|
209
|
+
* necessary, since it only remembers the choice. `read` and `write` never throw: storage that
|
|
210
|
+
* cannot be reached (a sandboxed frame, a blocked cookie jar) and a category name outside
|
|
211
|
+
* `CONSENT_CATEGORY_NAME` reject. On a server with no `store`, `read` resolves null, since there
|
|
212
|
+
* is no browser to ask, and `write` rejects.
|
|
213
|
+
*/
|
|
214
|
+
export function cookieConsentAdapter(options: CookieConsentAdapterOptions = {}): ConsentAdapter {
|
|
215
|
+
const name = options.name ?? CONSENT_DECISION_COOKIE;
|
|
216
|
+
const maxAge = options.maxAge ?? CONSENT_DECISION_MAX_AGE;
|
|
217
|
+
const store = (): ConsentCookieStore | null =>
|
|
218
|
+
options.store ?? (typeof document === "undefined" ? null : documentCookieStore());
|
|
219
|
+
const secure = (): boolean =>
|
|
220
|
+
options.secure ?? (typeof location !== "undefined" && location.protocol === "https:");
|
|
221
|
+
return {
|
|
222
|
+
// Async, so a store that throws becomes a rejection the caller can handle.
|
|
223
|
+
async read() {
|
|
224
|
+
return parseConsentDecision(store()?.get(name));
|
|
225
|
+
},
|
|
226
|
+
async write(decision) {
|
|
227
|
+
const cookies = store();
|
|
228
|
+
if (cookies === null)
|
|
229
|
+
throw new Error("cookieConsentAdapter has no cookie store here: pass `store` on a server.");
|
|
230
|
+
cookies.set(name, serializeConsentDecision(decision), {
|
|
231
|
+
path: "/",
|
|
232
|
+
sameSite: "lax",
|
|
233
|
+
secure: secure(),
|
|
234
|
+
maxAge,
|
|
235
|
+
});
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export {
|
|
2
|
+
CONSENT_CATEGORY_NAME,
|
|
3
|
+
CONSENT_DECISION_COOKIE,
|
|
4
|
+
CONSENT_DECISION_MAX_AGE,
|
|
5
|
+
browserGlobalPrivacyControl,
|
|
6
|
+
consentAllows,
|
|
7
|
+
cookieConsentAdapter,
|
|
8
|
+
decideConsent,
|
|
9
|
+
documentCookieStore,
|
|
10
|
+
parseConsentDecision,
|
|
11
|
+
sendsGlobalPrivacyControl,
|
|
12
|
+
serializeConsentDecision,
|
|
13
|
+
type ConsentAdapter,
|
|
14
|
+
type ConsentChoice,
|
|
15
|
+
type ConsentCookieOptions,
|
|
16
|
+
type ConsentCookieStore,
|
|
17
|
+
type ConsentDecision,
|
|
18
|
+
type ConsentState,
|
|
19
|
+
type CookieConsentAdapterOptions,
|
|
20
|
+
type DecideConsentInput,
|
|
21
|
+
} from "./consent.ts";
|
|
22
|
+
export { type ConsentAction, type ConsentRecording } from "./useConsent.ts";
|
|
23
|
+
export {
|
|
24
|
+
ConsentBanner,
|
|
25
|
+
type ConsentBannerProps,
|
|
26
|
+
type ConsentCategory,
|
|
27
|
+
type ConsentLink,
|
|
28
|
+
type ConsentLinkRenderer,
|
|
29
|
+
} from "./ConsentBanner.tsx";
|
|
30
|
+
export {
|
|
31
|
+
ConsentSettings,
|
|
32
|
+
type ConsentCookieTable,
|
|
33
|
+
type ConsentSettingsCategory,
|
|
34
|
+
type ConsentSettingsProps,
|
|
35
|
+
} from "./ConsentSettings.tsx";
|
|
36
|
+
export {
|
|
37
|
+
ConsentBannerStatic,
|
|
38
|
+
type ConsentBannerStaticProps,
|
|
39
|
+
type ConsentStaticLinkRenderer,
|
|
40
|
+
} from "./ConsentBannerStatic.tsx";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cookie banner for a server component: no hooks, context or event handlers, so rendering it
|
|
3
|
+
* sends no JavaScript (ADR 0027). `@xos/ui/consent` has the client components and the adapter.
|
|
4
|
+
*/
|
|
5
|
+
export {
|
|
6
|
+
ConsentBannerStatic,
|
|
7
|
+
type ConsentBannerStaticProps,
|
|
8
|
+
type ConsentStaticLinkRenderer,
|
|
9
|
+
} from "./ConsentBannerStatic.tsx";
|
|
10
|
+
export type { ConsentCategory, ConsentLink } from "./ConsentBanner.tsx";
|
|
11
|
+
export { sendsGlobalPrivacyControl } from "./consent.ts";
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { startTransition, useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
4
|
+
import {
|
|
5
|
+
browserGlobalPrivacyControl,
|
|
6
|
+
decideConsent,
|
|
7
|
+
type ConsentAdapter,
|
|
8
|
+
type ConsentChoice,
|
|
9
|
+
type ConsentDecision,
|
|
10
|
+
type ConsentState,
|
|
11
|
+
} from "./consent.ts";
|
|
12
|
+
|
|
13
|
+
/** Where the components keep the decision themselves, and the policy version they record with it. */
|
|
14
|
+
export interface ConsentRecording {
|
|
15
|
+
readonly adapter: ConsentAdapter;
|
|
16
|
+
/** The version of the cookie policy the copy describes, written with every decision. */
|
|
17
|
+
readonly policyVersion: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A form action for the choice: a URL the forms post to, or a function React calls with the form
|
|
22
|
+
* data (a Next.js server action fits). The fields are `choice` (`all`, `none` or `save`), one
|
|
23
|
+
* field per ticked category with the value `on`, and `gpc` with the value `1` in every form that
|
|
24
|
+
* is on screen while only the browser reports Global Privacy Control (the request did not).
|
|
25
|
+
*
|
|
26
|
+
* That browser-only signal is also reported once without a form: a function action is called with
|
|
27
|
+
* `choice=none` and `gpc=1`. A URL action receives no such call, because the banner shows no form
|
|
28
|
+
* under the signal and sends no request of its own; a page that posts to a URL records the refusal
|
|
29
|
+
* from `onChoice("none")`, or passes `gpc` from the request header.
|
|
30
|
+
*/
|
|
31
|
+
export type ConsentAction = string | ((form: FormData) => void | Promise<void>);
|
|
32
|
+
|
|
33
|
+
interface ConsentOptions {
|
|
34
|
+
readonly gpc: boolean | undefined;
|
|
35
|
+
readonly categories: readonly string[];
|
|
36
|
+
readonly record: ConsentRecording | undefined;
|
|
37
|
+
readonly action: ConsentAction | undefined;
|
|
38
|
+
readonly onChoice: ((choice: ConsentChoice) => void) | undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface Consent {
|
|
42
|
+
/** A Global Privacy Control signal is present, from the request (`gpc`) or the browser. */
|
|
43
|
+
readonly signal: boolean;
|
|
44
|
+
/** Only the browser reports the signal, so the forms say so to the server with a `gpc` field. */
|
|
45
|
+
readonly browserOnly: boolean;
|
|
46
|
+
/** With an adapter: undefined until it has been read, then the stored consent or null. */
|
|
47
|
+
readonly stored: ConsentState | null | undefined;
|
|
48
|
+
/** Records a choice through the adapter, when there is one, and tells `onChoice`. */
|
|
49
|
+
choose(choice: ConsentChoice, selected?: Iterable<string>): void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const subscribe = (): (() => void) => () => undefined;
|
|
53
|
+
const serverSignal = (): boolean => false;
|
|
54
|
+
const browserSignal = (): boolean => browserGlobalPrivacyControl();
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The state ConsentBanner and ConsentSettings share. Global Privacy Control counts as a refusal
|
|
58
|
+
* from either source. When only the browser reports it, the refusal is recorded once: through the
|
|
59
|
+
* adapter, or by calling a function `action` with `choice=none` and `gpc=1`.
|
|
60
|
+
*/
|
|
61
|
+
export function useConsent(options: ConsentOptions): Consent {
|
|
62
|
+
const browser = useSyncExternalStore(subscribe, browserSignal, serverSignal);
|
|
63
|
+
const signal = options.gpc === true || browser;
|
|
64
|
+
const [stored, setStored] = useState<ConsentState | null | undefined>(undefined);
|
|
65
|
+
const latest = useRef(options);
|
|
66
|
+
const reported = useRef(false);
|
|
67
|
+
/** A decision was written here, so a read still on its way is out of date. */
|
|
68
|
+
const wrote = useRef(false);
|
|
69
|
+
const tracked = options.record !== undefined;
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
latest.current = options;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
const record = latest.current.record;
|
|
77
|
+
if (record === undefined) return undefined;
|
|
78
|
+
let current = true;
|
|
79
|
+
// Through a promise chain, so an adapter that throws is handled like one that rejects.
|
|
80
|
+
void Promise.resolve()
|
|
81
|
+
.then(() => record.adapter.read())
|
|
82
|
+
.then(
|
|
83
|
+
(state) => {
|
|
84
|
+
if (current && !wrote.current) setStored(state);
|
|
85
|
+
},
|
|
86
|
+
() => {
|
|
87
|
+
if (current && !wrote.current) setStored(null);
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
return () => {
|
|
91
|
+
current = false;
|
|
92
|
+
};
|
|
93
|
+
}, [tracked]);
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (!signal || reported.current) return;
|
|
97
|
+
const { record, action, categories, onChoice, gpc } = latest.current;
|
|
98
|
+
if (record !== undefined) {
|
|
99
|
+
if (stored === undefined) return;
|
|
100
|
+
reported.current = true;
|
|
101
|
+
if (stored?.gpc === true) return;
|
|
102
|
+
const decision = decideConsent({
|
|
103
|
+
choice: "none",
|
|
104
|
+
categories,
|
|
105
|
+
gpc: true,
|
|
106
|
+
policyVersion: record.policyVersion,
|
|
107
|
+
});
|
|
108
|
+
write(record, decision);
|
|
109
|
+
onChoice?.("none");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
reported.current = true;
|
|
113
|
+
// The request already carried the signal, so the server has recorded it.
|
|
114
|
+
if (gpc === true) return;
|
|
115
|
+
if (typeof action === "function") {
|
|
116
|
+
const form = new FormData();
|
|
117
|
+
form.set("choice", "none");
|
|
118
|
+
form.set("gpc", "1");
|
|
119
|
+
startTransition(() => {
|
|
120
|
+
void action(form);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
onChoice?.("none");
|
|
124
|
+
}, [signal, stored]);
|
|
125
|
+
|
|
126
|
+
/** A failed write, thrown or rejected, leaves the choices on screen so the visitor can choose again. */
|
|
127
|
+
function write(record: ConsentRecording, decision: ConsentDecision): void {
|
|
128
|
+
wrote.current = true;
|
|
129
|
+
void Promise.resolve()
|
|
130
|
+
.then(() => record.adapter.write(decision))
|
|
131
|
+
.then(
|
|
132
|
+
() => setStored(decision),
|
|
133
|
+
() => undefined,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function choose(choice: ConsentChoice, selected?: Iterable<string>): void {
|
|
138
|
+
const { record, categories, onChoice } = latest.current;
|
|
139
|
+
if (record !== undefined) {
|
|
140
|
+
const decision = decideConsent({
|
|
141
|
+
choice,
|
|
142
|
+
categories,
|
|
143
|
+
...(selected === undefined ? {} : { selected }),
|
|
144
|
+
gpc: signal,
|
|
145
|
+
policyVersion: record.policyVersion,
|
|
146
|
+
});
|
|
147
|
+
write(record, decision);
|
|
148
|
+
}
|
|
149
|
+
onChoice?.(choice);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { signal, browserOnly: signal && options.gpc !== true, stored, choose };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The ticked category names in a form, read when Save is pressed. */
|
|
156
|
+
export function tickedCategories(form: HTMLFormElement | null, names: readonly string[]): string[] {
|
|
157
|
+
if (form === null) return [];
|
|
158
|
+
const data = new FormData(form);
|
|
159
|
+
return names.filter((name) => data.get(name) === "on");
|
|
160
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# CoordinateMatrix
|
|
2
|
+
|
|
3
|
+
CoordinateMatrix crosses the 10 departments with the 9 phases (90 coordinates); each cell drills down to its records.
|
|
4
|
+
|
|
5
|
+
- Cell fill uses `viz-seq-*`; text switches from `viz-ink` to `accent-text-on` at step 6 to keep 4.5:1.
|
|
6
|
+
- Empty coordinates are outlined, not filled. The table is its own accessible alternative.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# CredentialBadge
|
|
2
|
+
|
|
3
|
+
CredentialBadge is the badge shown at access points: category band, holder, an offline QR code, zones and validity.
|
|
4
|
+
|
|
5
|
+
- One tap from the lock screen through the wallet pass; brightness rises while it is shown.
|
|
6
|
+
- The category band uses the Access Grid category color; the label is always written out.
|
|
7
|
+
- The QR payload is validated offline against the Access Grid in Compass Scan.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# CurrencyInput
|
|
2
|
+
|
|
3
|
+
CurrencyInput is NULL-aware: a blank value is unpriced and is never coerced to 0.
|
|
4
|
+
|
|
5
|
+
- Empty shows the hint text Unpriced; 0 is a claim and must be typed.
|
|
6
|
+
- Right-aligned tabular numerals; currency code beside the field.
|
|
7
|
+
- The consumer stores `null` for blank and a number otherwise.
|