@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,210 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useId, useRef, type ReactElement, type ReactNode } from "react";
|
|
4
|
+
import { cx } from "../lib/cx.ts";
|
|
5
|
+
import { useT } from "../lib/context.tsx";
|
|
6
|
+
import { Button } from "../Button/Button.tsx";
|
|
7
|
+
import { Checkbox } from "../Forms/Checkbox.tsx";
|
|
8
|
+
import { Icon } from "../Icon/Icon.tsx";
|
|
9
|
+
import { Link, linkClass } from "../Link/Link.tsx";
|
|
10
|
+
import type { ConsentChoice } from "./consent.ts";
|
|
11
|
+
import {
|
|
12
|
+
tickedCategories,
|
|
13
|
+
useConsent,
|
|
14
|
+
type ConsentAction,
|
|
15
|
+
type ConsentRecording,
|
|
16
|
+
} from "./useConsent.ts";
|
|
17
|
+
|
|
18
|
+
/** A link the consent copy names: the Cookie Policy or the Cookie settings page. */
|
|
19
|
+
export interface ConsentLink {
|
|
20
|
+
label: string;
|
|
21
|
+
href: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Draws one of the consent links. An app with a router passes its own link component here, so the
|
|
26
|
+
* policy and the settings page open without a full page load; `className` is the library's link
|
|
27
|
+
* style. It is a function, so it comes from a client module.
|
|
28
|
+
*/
|
|
29
|
+
export type ConsentLinkRenderer = (link: ConsentLink, className: string) => ReactNode;
|
|
30
|
+
|
|
31
|
+
/** A consent link as the default anchor, or as the app's `renderLink` draws it. */
|
|
32
|
+
export function ConsentLinkText({
|
|
33
|
+
link,
|
|
34
|
+
renderLink,
|
|
35
|
+
}: {
|
|
36
|
+
link: ConsentLink;
|
|
37
|
+
renderLink: ConsentLinkRenderer | undefined;
|
|
38
|
+
}): ReactNode {
|
|
39
|
+
return renderLink ? renderLink(link, linkClass) : <Link href={link.href}>{link.label}</Link>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** An optional cookie category behind Choose. */
|
|
43
|
+
export interface ConsentCategory {
|
|
44
|
+
/**
|
|
45
|
+
* The field name the category posts under and the key it is recorded by. It must match
|
|
46
|
+
* `CONSENT_CATEGORY_NAME`: a letter, then letters, digits, underscores or hyphens, 64 characters
|
|
47
|
+
* at most (`preferences`, `analytics_storage`). The cookie adapter rejects a write with any
|
|
48
|
+
* other name.
|
|
49
|
+
*/
|
|
50
|
+
name: string;
|
|
51
|
+
label: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ConsentBannerProps {
|
|
55
|
+
title: string;
|
|
56
|
+
body?: string;
|
|
57
|
+
policy?: ConsentLink;
|
|
58
|
+
acceptLabel?: string;
|
|
59
|
+
rejectLabel?: string;
|
|
60
|
+
chooseLabel?: string;
|
|
61
|
+
necessary?: string;
|
|
62
|
+
categories?: ConsentCategory[];
|
|
63
|
+
saveLabel?: string;
|
|
64
|
+
/** The browser sends Global Privacy Control: show the confirmation instead of the choices. */
|
|
65
|
+
gpc?: boolean;
|
|
66
|
+
onChoice?: (choice: ConsentChoice) => void;
|
|
67
|
+
/** A second link after the policy, to the page where the choice can be changed. */
|
|
68
|
+
settings?: ConsentLink;
|
|
69
|
+
/** The Global Privacy Control confirmation; the `mk.consent.gpc` catalog message when omitted. */
|
|
70
|
+
gpcLabel?: string;
|
|
71
|
+
/** Posts the choice as plain forms, which work before any script loads. */
|
|
72
|
+
action?: ConsentAction;
|
|
73
|
+
/** Draws the policy and settings links with the app's router link; a plain anchor when omitted. */
|
|
74
|
+
renderLink?: ConsentLinkRenderer;
|
|
75
|
+
/**
|
|
76
|
+
* Lets the banner keep the decision itself: it reads the adapter once, stays hidden when a
|
|
77
|
+
* decision exists, and writes each choice with its timestamp, policy version and signal flag.
|
|
78
|
+
*/
|
|
79
|
+
record?: ConsentRecording;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const sectionClass = cx(
|
|
83
|
+
"border-b border-border-subtle bg-bg-surface p-16 font-sans text-text-primary print:hidden",
|
|
84
|
+
);
|
|
85
|
+
const innerClass = cx("mx-auto flex max-w-layout-reading flex-col gap-12");
|
|
86
|
+
const textClass = cx("m-0 text-text-13 text-text-secondary");
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The cookie banner both sites share (Section 14.3). Accept and Reject are the same Button, side by
|
|
90
|
+
* side, and no category behind Choose starts ticked. A Global Privacy Control signal, from the
|
|
91
|
+
* request (`gpc`) or from `navigator.globalPrivacyControl`, is a refusal: the banner shows the
|
|
92
|
+
* confirmation and offers no choices. Every string is a prop or comes from the `ui` catalog.
|
|
93
|
+
*/
|
|
94
|
+
export function ConsentBanner({
|
|
95
|
+
title,
|
|
96
|
+
body,
|
|
97
|
+
policy,
|
|
98
|
+
acceptLabel,
|
|
99
|
+
rejectLabel,
|
|
100
|
+
chooseLabel,
|
|
101
|
+
necessary,
|
|
102
|
+
categories,
|
|
103
|
+
saveLabel,
|
|
104
|
+
gpc,
|
|
105
|
+
onChoice,
|
|
106
|
+
settings,
|
|
107
|
+
gpcLabel,
|
|
108
|
+
action,
|
|
109
|
+
renderLink,
|
|
110
|
+
record,
|
|
111
|
+
}: ConsentBannerProps): ReactElement | null {
|
|
112
|
+
const t = useT();
|
|
113
|
+
const id = useId();
|
|
114
|
+
const custom = useRef<HTMLFormElement>(null);
|
|
115
|
+
const names = (categories ?? []).map((c) => c.name);
|
|
116
|
+
const { signal, stored, choose } = useConsent({
|
|
117
|
+
gpc,
|
|
118
|
+
categories: names,
|
|
119
|
+
record,
|
|
120
|
+
action,
|
|
121
|
+
onChoice,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (signal) {
|
|
125
|
+
return (
|
|
126
|
+
<section className={sectionClass} aria-label={title}>
|
|
127
|
+
<div className={innerClass}>
|
|
128
|
+
<p className="m-0 flex items-center gap-8 text-text-13" role="status">
|
|
129
|
+
<span className="flex text-success">
|
|
130
|
+
<Icon name="ShieldCheck" size={16} />
|
|
131
|
+
</span>
|
|
132
|
+
<span>{gpcLabel ?? t("mk.consent.gpc")}</span>
|
|
133
|
+
</p>
|
|
134
|
+
</div>
|
|
135
|
+
</section>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
// With an adapter the banner waits for the stored consent, and a visitor who decided sees nothing.
|
|
139
|
+
if (record !== undefined && stored !== null) return null;
|
|
140
|
+
|
|
141
|
+
const posts = action !== undefined;
|
|
142
|
+
const type = posts ? "submit" : "button";
|
|
143
|
+
return (
|
|
144
|
+
<section className={sectionClass} aria-labelledby={id}>
|
|
145
|
+
<div className={innerClass}>
|
|
146
|
+
<h2 id={id} className="m-0 text-heading-16 font-semibold">
|
|
147
|
+
{title}
|
|
148
|
+
</h2>
|
|
149
|
+
{body || policy || settings ? (
|
|
150
|
+
<p className={textClass}>
|
|
151
|
+
{body}
|
|
152
|
+
{policy ? (
|
|
153
|
+
<>
|
|
154
|
+
{" "}
|
|
155
|
+
<ConsentLinkText link={policy} renderLink={renderLink} />
|
|
156
|
+
</>
|
|
157
|
+
) : null}
|
|
158
|
+
{settings ? (
|
|
159
|
+
<>
|
|
160
|
+
{policy ? " · " : " "}
|
|
161
|
+
<ConsentLinkText link={settings} renderLink={renderLink} />
|
|
162
|
+
</>
|
|
163
|
+
) : null}
|
|
164
|
+
</p>
|
|
165
|
+
) : null}
|
|
166
|
+
<form
|
|
167
|
+
{...(posts ? { action } : {})}
|
|
168
|
+
onSubmit={posts ? undefined : (event) => event.preventDefault()}
|
|
169
|
+
className="flex flex-wrap items-center gap-8"
|
|
170
|
+
>
|
|
171
|
+
<Button type={type} name="choice" value="all" size="lg" onClick={() => choose("all")}>
|
|
172
|
+
{acceptLabel}
|
|
173
|
+
</Button>
|
|
174
|
+
<Button type={type} name="choice" value="none" size="lg" onClick={() => choose("none")}>
|
|
175
|
+
{rejectLabel}
|
|
176
|
+
</Button>
|
|
177
|
+
</form>
|
|
178
|
+
{categories ? (
|
|
179
|
+
<details>
|
|
180
|
+
<summary className="w-fit cursor-pointer text-text-13 font-medium text-accent-text">
|
|
181
|
+
{chooseLabel}
|
|
182
|
+
</summary>
|
|
183
|
+
{/* data-keep-input: the save always succeeds and the page renders again from the stored choice. */}
|
|
184
|
+
<form
|
|
185
|
+
ref={custom}
|
|
186
|
+
{...(posts ? { action } : {})}
|
|
187
|
+
onSubmit={posts ? undefined : (event) => event.preventDefault()}
|
|
188
|
+
data-keep-input="not-refusable"
|
|
189
|
+
className="mt-8 flex flex-col items-start gap-8 text-text-13"
|
|
190
|
+
>
|
|
191
|
+
<input type="hidden" name="choice" value="save" />
|
|
192
|
+
{necessary ? <p className={textClass}>{necessary}</p> : null}
|
|
193
|
+
{categories.map((c) => (
|
|
194
|
+
<Checkbox key={c.name} name={c.name} label={c.label} />
|
|
195
|
+
))}
|
|
196
|
+
<div>
|
|
197
|
+
<Button
|
|
198
|
+
type={type}
|
|
199
|
+
onClick={() => choose("save", tickedCategories(custom.current, names))}
|
|
200
|
+
>
|
|
201
|
+
{saveLabel}
|
|
202
|
+
</Button>
|
|
203
|
+
</div>
|
|
204
|
+
</form>
|
|
205
|
+
</details>
|
|
206
|
+
) : null}
|
|
207
|
+
</div>
|
|
208
|
+
</section>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import { buttonClass } from "../Button/button-class.ts";
|
|
3
|
+
import { Icon } from "../Icon/Icon.tsx";
|
|
4
|
+
import { cx } from "../lib/cx.ts";
|
|
5
|
+
import { linkClass } from "../Link/link-class.ts";
|
|
6
|
+
import type { ConsentCategory, ConsentLink } from "./ConsentBanner.tsx";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Draws one of the consent links in a server component. Unlike `ConsentLinkRenderer` it never
|
|
10
|
+
* crosses to the client, so a server component can pass its router's link component directly.
|
|
11
|
+
*/
|
|
12
|
+
export type ConsentStaticLinkRenderer = (link: ConsentLink, className: string) => ReactNode;
|
|
13
|
+
|
|
14
|
+
interface ConsentBannerStaticCopy {
|
|
15
|
+
title: string;
|
|
16
|
+
body?: string;
|
|
17
|
+
policy?: ConsentLink;
|
|
18
|
+
/** A second link after the policy, to the page where the choice can be changed. */
|
|
19
|
+
settings?: ConsentLink;
|
|
20
|
+
acceptLabel: string;
|
|
21
|
+
rejectLabel: string;
|
|
22
|
+
chooseLabel?: string;
|
|
23
|
+
necessary?: string;
|
|
24
|
+
categories?: ConsentCategory[];
|
|
25
|
+
saveLabel?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Where the forms post: a URL, or a function such as a Next.js server action. The fields are
|
|
28
|
+
* `choice` (`all`, `none` or `save`) and each ticked category by name with the value `on`.
|
|
29
|
+
*/
|
|
30
|
+
action: string | ((form: FormData) => void | Promise<void>);
|
|
31
|
+
/** Draws the policy and settings links with the app's router link; a plain anchor when omitted. */
|
|
32
|
+
renderLink?: ConsentStaticLinkRenderer;
|
|
33
|
+
/** The id of the heading the region is labelled by, when the page needs its own. */
|
|
34
|
+
headingId?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* `gpc`: the request carries `Sec-GPC: 1` (`sendsGlobalPrivacyControl`), so the banner shows the
|
|
39
|
+
* confirmation in place of the choices. The caller records the refusal; this component only says
|
|
40
|
+
* so. `gpcLabel` is the confirmation, and the type requires it whenever `gpc` can be true: there is
|
|
41
|
+
* no catalog to fall back on in a server component.
|
|
42
|
+
*/
|
|
43
|
+
type ConsentBannerStaticSignal =
|
|
44
|
+
| { gpc: true; gpcLabel: string }
|
|
45
|
+
| { gpc?: false; gpcLabel?: string }
|
|
46
|
+
| { gpc: boolean; gpcLabel: string };
|
|
47
|
+
|
|
48
|
+
export type ConsentBannerStaticProps = ConsentBannerStaticCopy & ConsentBannerStaticSignal;
|
|
49
|
+
|
|
50
|
+
const sectionClass = cx(
|
|
51
|
+
"border-b border-border-subtle bg-bg-surface p-16 font-sans text-text-primary print:hidden",
|
|
52
|
+
);
|
|
53
|
+
const innerClass = cx("mx-auto flex max-w-layout-reading flex-col gap-12");
|
|
54
|
+
const textClass = cx("m-0 text-text-13 text-text-secondary");
|
|
55
|
+
/** Accept and Reject are this one class: the same variant and the same size. */
|
|
56
|
+
const choiceClass = buttonClass("secondary", "lg");
|
|
57
|
+
|
|
58
|
+
function link(target: ConsentLink, renderLink: ConsentStaticLinkRenderer | undefined): ReactNode {
|
|
59
|
+
return renderLink ? (
|
|
60
|
+
renderLink(target, linkClass)
|
|
61
|
+
) : (
|
|
62
|
+
<a href={target.href} className={linkClass}>
|
|
63
|
+
{target.label}
|
|
64
|
+
</a>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The cookie banner with no script (Section 14.3): the markup and the equal choices of
|
|
70
|
+
* ConsentBanner as plain forms, with no hooks, context or event handlers, so a server component
|
|
71
|
+
* that renders it sends no JavaScript for it (ADR 0027). Atlas shows it on every route. It reads
|
|
72
|
+
* nothing from the browser: it does not see `navigator.globalPrivacyControl`, keeps no record and
|
|
73
|
+
* does not hide itself, so the server decides whether to render it and passes `gpc` from the
|
|
74
|
+
* request header. Use ConsentBanner where the banner keeps the decision through an adapter.
|
|
75
|
+
*/
|
|
76
|
+
export function ConsentBannerStatic({
|
|
77
|
+
title,
|
|
78
|
+
body,
|
|
79
|
+
policy,
|
|
80
|
+
settings,
|
|
81
|
+
acceptLabel,
|
|
82
|
+
rejectLabel,
|
|
83
|
+
chooseLabel,
|
|
84
|
+
necessary,
|
|
85
|
+
categories,
|
|
86
|
+
saveLabel,
|
|
87
|
+
action,
|
|
88
|
+
gpc,
|
|
89
|
+
gpcLabel,
|
|
90
|
+
renderLink,
|
|
91
|
+
headingId = "consent-banner-title",
|
|
92
|
+
}: ConsentBannerStaticProps): ReactElement {
|
|
93
|
+
if (gpc) {
|
|
94
|
+
return (
|
|
95
|
+
<section className={sectionClass} aria-label={title}>
|
|
96
|
+
<div className={innerClass}>
|
|
97
|
+
<p className="m-0 flex items-center gap-8 text-text-13" role="status">
|
|
98
|
+
<span className="flex text-success">
|
|
99
|
+
<Icon name="ShieldCheck" size={16} />
|
|
100
|
+
</span>
|
|
101
|
+
<span>{gpcLabel}</span>
|
|
102
|
+
</p>
|
|
103
|
+
</div>
|
|
104
|
+
</section>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return (
|
|
108
|
+
<section className={sectionClass} aria-labelledby={headingId}>
|
|
109
|
+
<div className={innerClass}>
|
|
110
|
+
<h2 id={headingId} className="m-0 text-heading-16 font-semibold">
|
|
111
|
+
{title}
|
|
112
|
+
</h2>
|
|
113
|
+
{body || policy || settings ? (
|
|
114
|
+
<p className={textClass}>
|
|
115
|
+
{body}
|
|
116
|
+
{policy ? <> {link(policy, renderLink)}</> : null}
|
|
117
|
+
{settings ? (
|
|
118
|
+
<>
|
|
119
|
+
{policy ? " · " : " "}
|
|
120
|
+
{link(settings, renderLink)}
|
|
121
|
+
</>
|
|
122
|
+
) : null}
|
|
123
|
+
</p>
|
|
124
|
+
) : null}
|
|
125
|
+
<form action={action} className="flex flex-wrap items-center gap-8">
|
|
126
|
+
<button type="submit" name="choice" value="all" className={choiceClass}>
|
|
127
|
+
{acceptLabel}
|
|
128
|
+
</button>
|
|
129
|
+
<button type="submit" name="choice" value="none" className={choiceClass}>
|
|
130
|
+
{rejectLabel}
|
|
131
|
+
</button>
|
|
132
|
+
</form>
|
|
133
|
+
{categories && chooseLabel && saveLabel ? (
|
|
134
|
+
<details>
|
|
135
|
+
<summary className="w-fit cursor-pointer text-text-13 font-medium text-accent-text">
|
|
136
|
+
{chooseLabel}
|
|
137
|
+
</summary>
|
|
138
|
+
{/* data-keep-input: the save always succeeds and the page renders again from the stored choice. */}
|
|
139
|
+
<form
|
|
140
|
+
action={action}
|
|
141
|
+
data-keep-input="not-refusable"
|
|
142
|
+
className="mt-8 flex flex-col items-start gap-8 text-text-13"
|
|
143
|
+
>
|
|
144
|
+
<input type="hidden" name="choice" value="save" />
|
|
145
|
+
{necessary ? <p className={textClass}>{necessary}</p> : null}
|
|
146
|
+
{categories.map((category) => (
|
|
147
|
+
<label key={category.name} className="flex items-start gap-8">
|
|
148
|
+
<input type="checkbox" name={category.name} className="mt-2" />
|
|
149
|
+
<span>{category.label}</span>
|
|
150
|
+
</label>
|
|
151
|
+
))}
|
|
152
|
+
<div>
|
|
153
|
+
<button type="submit" className={buttonClass("secondary", "md")}>
|
|
154
|
+
{saveLabel}
|
|
155
|
+
</button>
|
|
156
|
+
</div>
|
|
157
|
+
</form>
|
|
158
|
+
</details>
|
|
159
|
+
) : null}
|
|
160
|
+
</div>
|
|
161
|
+
</section>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRef, type ReactElement } from "react";
|
|
4
|
+
import { cx } from "../lib/cx.ts";
|
|
5
|
+
import { useT } from "../lib/context.tsx";
|
|
6
|
+
import { Button } from "../Button/Button.tsx";
|
|
7
|
+
import { Checkbox } from "../Forms/Checkbox.tsx";
|
|
8
|
+
import { InlineAlert } from "../Feedback/InlineAlert.tsx";
|
|
9
|
+
import type { ConsentChoice, ConsentState } from "./consent.ts";
|
|
10
|
+
import {
|
|
11
|
+
ConsentLinkText,
|
|
12
|
+
type ConsentCategory,
|
|
13
|
+
type ConsentLink,
|
|
14
|
+
type ConsentLinkRenderer,
|
|
15
|
+
} from "./ConsentBanner.tsx";
|
|
16
|
+
import {
|
|
17
|
+
tickedCategories,
|
|
18
|
+
useConsent,
|
|
19
|
+
type ConsentAction,
|
|
20
|
+
type ConsentRecording,
|
|
21
|
+
} from "./useConsent.ts";
|
|
22
|
+
|
|
23
|
+
/** An optional category on the settings page, with the choice in force. */
|
|
24
|
+
export interface ConsentSettingsCategory extends ConsentCategory {
|
|
25
|
+
/** The category is allowed now. With `record`, the stored consent decides instead. */
|
|
26
|
+
checked?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The table of cookies the site sets, which a cookie policy asks for by name. */
|
|
30
|
+
export interface ConsentCookieTable {
|
|
31
|
+
caption: string;
|
|
32
|
+
nameLabel: string;
|
|
33
|
+
categoryLabel: string;
|
|
34
|
+
purposeLabel: string;
|
|
35
|
+
rows: { name: string; category: string; purpose: string }[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ConsentSettingsProps {
|
|
39
|
+
title: string;
|
|
40
|
+
lead?: string;
|
|
41
|
+
/**
|
|
42
|
+
* The sentence that states the choice in force. A function receives the stored consent, or null
|
|
43
|
+
* before a choice, when `record` keeps the decision.
|
|
44
|
+
*/
|
|
45
|
+
status: string | ((state: ConsentState | null) => string);
|
|
46
|
+
/** The request carries Global Privacy Control. */
|
|
47
|
+
gpc?: boolean;
|
|
48
|
+
/** What the signal means here; the `mk.consent.gpc` catalog message when omitted. */
|
|
49
|
+
gpcNotice?: string;
|
|
50
|
+
categoriesLabel: string;
|
|
51
|
+
/** The strictly necessary category, shown ticked and locked. */
|
|
52
|
+
necessary: string;
|
|
53
|
+
categories: ConsentSettingsCategory[];
|
|
54
|
+
saveLabel: string;
|
|
55
|
+
acceptLabel: string;
|
|
56
|
+
rejectLabel: string;
|
|
57
|
+
cookies?: ConsentCookieTable;
|
|
58
|
+
policy?: ConsentLink;
|
|
59
|
+
/** The heading level of the title: 1 on a page of its own. */
|
|
60
|
+
headingLevel?: 1 | 2;
|
|
61
|
+
onChoice?: (choice: ConsentChoice) => void;
|
|
62
|
+
/** Posts the choice as plain forms, which work before any script loads. */
|
|
63
|
+
action?: ConsentAction;
|
|
64
|
+
/** Lets the page keep the decision itself, through an adapter. */
|
|
65
|
+
record?: ConsentRecording;
|
|
66
|
+
/** Draws the policy link with the app's router link; a plain anchor when omitted. */
|
|
67
|
+
renderLink?: ConsentLinkRenderer;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const cellClass = cx("py-4 pe-8 text-start");
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The settings view of the cookie banner (Section 14.3): the choice in force, the categories, the
|
|
74
|
+
* same Accept and Reject pair at equal weight, and the cookies by name. Under Global Privacy
|
|
75
|
+
* Control it says so, and the optional categories and Accept are locked while the signal is on.
|
|
76
|
+
*/
|
|
77
|
+
export function ConsentSettings({
|
|
78
|
+
title,
|
|
79
|
+
lead,
|
|
80
|
+
status,
|
|
81
|
+
gpc,
|
|
82
|
+
gpcNotice,
|
|
83
|
+
categoriesLabel,
|
|
84
|
+
necessary,
|
|
85
|
+
categories,
|
|
86
|
+
saveLabel,
|
|
87
|
+
acceptLabel,
|
|
88
|
+
rejectLabel,
|
|
89
|
+
cookies,
|
|
90
|
+
policy,
|
|
91
|
+
headingLevel = 1,
|
|
92
|
+
onChoice,
|
|
93
|
+
action,
|
|
94
|
+
record,
|
|
95
|
+
renderLink,
|
|
96
|
+
}: ConsentSettingsProps): ReactElement {
|
|
97
|
+
const t = useT();
|
|
98
|
+
const custom = useRef<HTMLFormElement>(null);
|
|
99
|
+
const names = categories.map((c) => c.name);
|
|
100
|
+
const { signal, browserOnly, stored, choose } = useConsent({
|
|
101
|
+
gpc,
|
|
102
|
+
categories: names,
|
|
103
|
+
record,
|
|
104
|
+
action,
|
|
105
|
+
onChoice,
|
|
106
|
+
});
|
|
107
|
+
const Heading = headingLevel === 1 ? "h1" : "h2";
|
|
108
|
+
const posts = action !== undefined;
|
|
109
|
+
const type = posts ? "submit" : "button";
|
|
110
|
+
const allowed = (c: ConsentSettingsCategory): boolean =>
|
|
111
|
+
record === undefined ? (c.checked ?? false) : stored?.categories[c.name] === true;
|
|
112
|
+
// The boxes are uncontrolled so they work before any script loads; a new stored choice remounts
|
|
113
|
+
// them, and only them, so the button that was pressed keeps the keyboard focus.
|
|
114
|
+
const boxesKey = record === undefined ? "props" : JSON.stringify(stored ?? null);
|
|
115
|
+
// Only the browser reports the signal: tell the server, which saw no Sec-GPC header.
|
|
116
|
+
const gpcField = browserOnly ? <input type="hidden" name="gpc" value="1" /> : null;
|
|
117
|
+
return (
|
|
118
|
+
<div className="mx-auto flex max-w-layout-reading flex-col gap-16 p-24">
|
|
119
|
+
<Heading className="m-0 text-heading-24 font-semibold">{title}</Heading>
|
|
120
|
+
{lead ? <p className="m-0 text-text-14 text-text-secondary">{lead}</p> : null}
|
|
121
|
+
{signal ? <InlineAlert tone="info">{gpcNotice ?? t("mk.consent.gpc")}</InlineAlert> : null}
|
|
122
|
+
<p className="m-0 text-text-14" role="status">
|
|
123
|
+
{typeof status === "function" ? status(stored ?? null) : status}
|
|
124
|
+
</p>
|
|
125
|
+
{/* data-keep-input: the save always succeeds and the page renders again from the stored choice. */}
|
|
126
|
+
<form
|
|
127
|
+
ref={custom}
|
|
128
|
+
{...(posts ? { action } : {})}
|
|
129
|
+
onSubmit={posts ? undefined : (event) => event.preventDefault()}
|
|
130
|
+
data-keep-input="not-refusable"
|
|
131
|
+
className="flex flex-col gap-12"
|
|
132
|
+
>
|
|
133
|
+
<input type="hidden" name="choice" value="save" />
|
|
134
|
+
{gpcField}
|
|
135
|
+
<fieldset key={boxesKey} className="m-0 flex flex-col items-start gap-8 border-0 p-0">
|
|
136
|
+
<legend className="mb-4 text-text-13 font-medium">{categoriesLabel}</legend>
|
|
137
|
+
<Checkbox label={necessary} checked disabled readOnly />
|
|
138
|
+
{categories.map((c) => (
|
|
139
|
+
<Checkbox
|
|
140
|
+
key={c.name}
|
|
141
|
+
name={c.name}
|
|
142
|
+
label={c.label}
|
|
143
|
+
defaultChecked={allowed(c)}
|
|
144
|
+
disabled={signal}
|
|
145
|
+
/>
|
|
146
|
+
))}
|
|
147
|
+
</fieldset>
|
|
148
|
+
<div className="flex flex-wrap gap-8">
|
|
149
|
+
<Button
|
|
150
|
+
type={type}
|
|
151
|
+
variant="primary"
|
|
152
|
+
onClick={() => choose("save", tickedCategories(custom.current, names))}
|
|
153
|
+
>
|
|
154
|
+
{saveLabel}
|
|
155
|
+
</Button>
|
|
156
|
+
</div>
|
|
157
|
+
</form>
|
|
158
|
+
<form
|
|
159
|
+
{...(posts ? { action } : {})}
|
|
160
|
+
onSubmit={posts ? undefined : (event) => event.preventDefault()}
|
|
161
|
+
className="flex flex-wrap gap-8"
|
|
162
|
+
>
|
|
163
|
+
{gpcField}
|
|
164
|
+
<Button
|
|
165
|
+
type={type}
|
|
166
|
+
name="choice"
|
|
167
|
+
value="all"
|
|
168
|
+
disabled={signal}
|
|
169
|
+
onClick={() => choose("all")}
|
|
170
|
+
>
|
|
171
|
+
{acceptLabel}
|
|
172
|
+
</Button>
|
|
173
|
+
<Button type={type} name="choice" value="none" onClick={() => choose("none")}>
|
|
174
|
+
{rejectLabel}
|
|
175
|
+
</Button>
|
|
176
|
+
</form>
|
|
177
|
+
{cookies ? (
|
|
178
|
+
<table className="w-full border-collapse text-start text-text-13">
|
|
179
|
+
<caption className="mb-8 text-start text-text-13 font-medium">{cookies.caption}</caption>
|
|
180
|
+
<thead>
|
|
181
|
+
<tr className="border-b border-border-subtle">
|
|
182
|
+
<th scope="col" className={cellClass}>
|
|
183
|
+
{cookies.nameLabel}
|
|
184
|
+
</th>
|
|
185
|
+
<th scope="col" className={cellClass}>
|
|
186
|
+
{cookies.categoryLabel}
|
|
187
|
+
</th>
|
|
188
|
+
<th scope="col" className="py-4 text-start">
|
|
189
|
+
{cookies.purposeLabel}
|
|
190
|
+
</th>
|
|
191
|
+
</tr>
|
|
192
|
+
</thead>
|
|
193
|
+
<tbody>
|
|
194
|
+
{cookies.rows.map((row) => (
|
|
195
|
+
<tr key={row.name} className="border-b border-border-subtle">
|
|
196
|
+
<th scope="row" className={cx(cellClass, "font-mono font-regular")}>
|
|
197
|
+
{row.name}
|
|
198
|
+
</th>
|
|
199
|
+
<td className={cellClass}>{row.category}</td>
|
|
200
|
+
<td className="py-4">{row.purpose}</td>
|
|
201
|
+
</tr>
|
|
202
|
+
))}
|
|
203
|
+
</tbody>
|
|
204
|
+
</table>
|
|
205
|
+
) : null}
|
|
206
|
+
{policy ? (
|
|
207
|
+
<p className="m-0 text-text-13 text-text-secondary">
|
|
208
|
+
<ConsentLinkText link={policy} renderLink={renderLink} />
|
|
209
|
+
</p>
|
|
210
|
+
) : null}
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# ConsentBanner
|
|
2
|
+
|
|
3
|
+
ConsentBanner asks a visitor once which cookies a site may set, and ConsentSettings is the page where the choice is changed. The marketing site renders both. Atlas renders ConsentSettings and, on every route, the script-free ConsentBannerStatic described below.
|
|
4
|
+
|
|
5
|
+
- Import them from `@xos/ui/consent`, or from `@xos/ui` on the client. Both components are client modules; the adapter contract in `consent.ts` has no React and runs anywhere.
|
|
6
|
+
- Accept and Reject are the same Button at the same size, side by side. Neither is primary, and neither has focus when the banner opens. No category behind Choose starts ticked.
|
|
7
|
+
- Global Privacy Control is a refusal. Pass `gpc` when the request carries `Sec-GPC: 1` (`sendsGlobalPrivacyControl`); the components also read `navigator.globalPrivacyControl`. Under the signal the banner shows the confirmation in place of the choices, and the settings page says so and locks the optional categories and Accept.
|
|
8
|
+
- Every string is a prop. The one exception is the confirmation, which falls back to the `mk.consent.gpc` message of the `ui` catalog when `gpcLabel` or `gpcNotice` is not passed.
|
|
9
|
+
- A category `name` is a letter followed by letters, digits, underscores or hyphens, 64 characters at most (`CONSENT_CATEGORY_NAME`): `preferences`, `analytics_storage` and `Marketing` fit. The cookie adapter rejects a write with any other name, and reads nothing from a cookie that holds one.
|
|
10
|
+
- The links are plain anchors. An app with a router passes `renderLink` to draw them with its own link component.
|
|
11
|
+
|
|
12
|
+
## With no script
|
|
13
|
+
|
|
14
|
+
`ConsentBannerStatic`, from `@xos/ui/consent/static`, is the same banner with no hooks, context or event handlers: a server component that renders it sends no JavaScript for it. Atlas shows it on every route, because the client ConsentBanner adds about 7 KB of script to each page that renders it.
|
|
15
|
+
|
|
16
|
+
- It takes the same copy props, a required `action` (a URL or a server action) and an optional `renderLink`, which a server component can pass directly because nothing crosses to the client.
|
|
17
|
+
- It reads nothing from the browser. The server decides whether to render it, and passes `gpc` and `gpcLabel` when the request carries `Sec-GPC: 1` to show the confirmation in place of the choices.
|
|
18
|
+
- It keeps no record and does not see `navigator.globalPrivacyControl`. Use ConsentBanner with `record` where the banner keeps the decision through an adapter, as a site with no server does.
|
|
19
|
+
|
|
20
|
+
## From a server component
|
|
21
|
+
|
|
22
|
+
A server component can pass only props that serialize: the copy, `gpc`, `policy`, `settings`, `categories`, `cookies`, a string `status`, and `action` as a URL or a server action. That is how Atlas renders ConsentSettings. The rule covers the two client components; ConsentBannerStatic is rendered on the server, so it takes any prop.
|
|
23
|
+
|
|
24
|
+
`record`, `onChoice`, `renderLink` and a function `status` are functions or hold functions. They must be passed from a file of the consumer's marked `"use client"`; a server component that passes them fails to render.
|
|
25
|
+
|
|
26
|
+
## Where the decision is kept
|
|
27
|
+
|
|
28
|
+
The components never touch storage. They work in one of three ways:
|
|
29
|
+
|
|
30
|
+
1. `action`: plain forms post the choice to a URL or a function (a Next.js server action). The fields are `choice` (`all`, `none` or `save`) and each ticked category by name with the value `on`. The forms work before any script loads. Atlas uses this for ConsentSettings.
|
|
31
|
+
2. `record`: an adapter and a policy version. The banner reads the adapter once, stays hidden when a decision exists, and writes each decision with its timestamp, the policy version and the Global Privacy Control flag.
|
|
32
|
+
3. `onChoice` alone: the consumer hears the choice and does the rest.
|
|
33
|
+
|
|
34
|
+
### Global Privacy Control that only the browser reports
|
|
35
|
+
|
|
36
|
+
When `navigator.globalPrivacyControl` is on and `gpc` was not passed, the server has not seen the signal. What happens depends on the mode:
|
|
37
|
+
|
|
38
|
+
- `record`: the refusal is written through the adapter once, flagged as a signal.
|
|
39
|
+
- `action` as a function: it is called once per mount with `choice=none` and `gpc=1`. The receiver should do nothing when it already holds a refusal from the signal, as Atlas's `saveCookieChoice` does.
|
|
40
|
+
- `action` as a URL: nothing is sent, because the banner shows no form under the signal and makes no request of its own. Record the refusal from `onChoice("none")`, or pass `gpc` from the request header.
|
|
41
|
+
- In every `action` mode, each form still on screen (the two on ConsentSettings) carries a hidden `gpc` field with the value `1`, so a choice made there keeps the flag.
|
|
42
|
+
|
|
43
|
+
`onChoice("none")` is called once in all of these.
|
|
44
|
+
|
|
45
|
+
## The adapter
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
interface ConsentState {
|
|
49
|
+
readonly categories: Readonly<Record<string, boolean>>;
|
|
50
|
+
readonly gpc: boolean;
|
|
51
|
+
}
|
|
52
|
+
interface ConsentDecision extends ConsentState {
|
|
53
|
+
readonly decidedAt: string; // ISO 8601, UTC
|
|
54
|
+
readonly policyVersion: string;
|
|
55
|
+
}
|
|
56
|
+
interface ConsentAdapter {
|
|
57
|
+
read(): Promise<ConsentState | null>;
|
|
58
|
+
write(decision: ConsentDecision): Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`cookieConsentAdapter()` is the reference implementation with no database: the whole decision lives in one first-party cookie, `xos-consent-decision`, for 12 months. It reads and writes `document.cookie` by default.
|
|
63
|
+
|
|
64
|
+
- It never throws. Storage that cannot be reached and a category name outside the rule reject; the components then keep the choices on screen.
|
|
65
|
+
- It takes any `store` with `get` and `set`, so a server can back it with its response cookies. Encoding is the store's job: `get` returns and `set` receives the decoded JSON value. A framework's cookie jar encodes and decodes by itself, as the default `document.cookie` store does, so a decision the server writes is read in the browser and the other way round.
|
|
66
|
+
- On a server with no `store`, `read` resolves null and `write` rejects.
|
|
67
|
+
|
|
68
|
+
A site with no database keeps the decision in the cookie from a small client file:
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
"use client";
|
|
72
|
+
|
|
73
|
+
import { ConsentBanner, cookieConsentAdapter, type ConsentBannerProps } from "@xos/ui/consent";
|
|
74
|
+
import { useState } from "react";
|
|
75
|
+
|
|
76
|
+
/** The copy comes in as props from the server page; the adapter is made here, in the browser. */
|
|
77
|
+
export function SiteConsentBanner(props: Omit<ConsentBannerProps, "record" | "action">) {
|
|
78
|
+
const [record] = useState(() => ({
|
|
79
|
+
adapter: cookieConsentAdapter(),
|
|
80
|
+
policyVersion: "cookie-policy 1.0.0",
|
|
81
|
+
}));
|
|
82
|
+
return <ConsentBanner {...props} record={record} />;
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
// A server page or layout: strings only.
|
|
88
|
+
<SiteConsentBanner
|
|
89
|
+
title={copy.title}
|
|
90
|
+
body={copy.body}
|
|
91
|
+
policy={{ label: copy.policy, href: "/legal/cookie-policy" }}
|
|
92
|
+
acceptLabel={copy.accept}
|
|
93
|
+
rejectLabel={copy.reject}
|
|
94
|
+
chooseLabel={copy.choose}
|
|
95
|
+
necessary={copy.necessary}
|
|
96
|
+
categories={[{ name: "preferences", label: copy.preferences }]}
|
|
97
|
+
saveLabel={copy.save}
|
|
98
|
+
/>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Atlas keeps its own adapter, over the consent log in the platform database, in `apps/atlas/lib/privacy/consent-adapter.ts`.
|