@lar-works/design-system 0.2.1 → 0.3.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/README.md +5 -1
- package/dist/actions/Button.d.ts +39 -1
- package/dist/actions/Button.js +39 -9
- package/dist/actions/Button.js.map +1 -1
- package/dist/actions/ButtonLink.d.ts +5 -1
- package/dist/actions/ButtonLink.js +28 -2
- package/dist/actions/ButtonLink.js.map +1 -1
- package/dist/actions/ButtonRecipes.js +7 -0
- package/dist/actions/ButtonRecipes.js.map +1 -0
- package/dist/actions/IconButton.d.ts +2 -2
- package/dist/actions/IconButton.js +1 -1
- package/dist/actions/IconButton.js.map +1 -1
- package/dist/actions/ThemeSwitch.js +1 -1
- package/dist/board/ActivityLine.d.ts +36 -0
- package/dist/board/ActivityLine.js +144 -0
- package/dist/board/ActivityLine.js.map +1 -0
- package/dist/board/ActivityTrack.d.ts +11 -0
- package/dist/board/ActivityTrack.js +25 -0
- package/dist/board/ActivityTrack.js.map +1 -0
- package/dist/board/AddItem.d.ts +82 -0
- package/dist/board/AddItem.js +185 -0
- package/dist/board/AddItem.js.map +1 -0
- package/dist/board/Archive.d.ts +21 -0
- package/dist/board/Archive.js +63 -0
- package/dist/board/Archive.js.map +1 -0
- package/dist/board/Attention.d.ts +66 -0
- package/dist/board/Attention.js +122 -0
- package/dist/board/Attention.js.map +1 -0
- package/dist/board/BoardShell.d.ts +16 -0
- package/dist/board/BoardShell.js +35 -0
- package/dist/board/BoardShell.js.map +1 -0
- package/dist/board/BoardTitleBar.d.ts +50 -0
- package/dist/board/BoardTitleBar.js +125 -0
- package/dist/board/BoardTitleBar.js.map +1 -0
- package/dist/board/ButtonKinds.d.ts +24 -0
- package/dist/board/ButtonKinds.js +19 -0
- package/dist/board/ButtonKinds.js.map +1 -0
- package/dist/board/Cleanup.d.ts +8 -0
- package/dist/board/Cleanup.js +15 -0
- package/dist/board/Cleanup.js.map +1 -0
- package/dist/board/Counters.d.ts +101 -0
- package/dist/board/Counters.js +202 -0
- package/dist/board/Counters.js.map +1 -0
- package/dist/board/DataAttributes.d.ts +6 -0
- package/dist/board/Dependencies.d.ts +17 -0
- package/dist/board/Dependencies.js +44 -0
- package/dist/board/Dependencies.js.map +1 -0
- package/dist/board/Desk.d.ts +52 -0
- package/dist/board/Desk.js +130 -0
- package/dist/board/Desk.js.map +1 -0
- package/dist/board/Dossier.d.ts +100 -0
- package/dist/board/Dossier.js +194 -0
- package/dist/board/Dossier.js.map +1 -0
- package/dist/board/Filters.d.ts +33 -0
- package/dist/board/Filters.js +64 -0
- package/dist/board/Filters.js.map +1 -0
- package/dist/board/ItemDetails.d.ts +167 -0
- package/dist/board/ItemDetails.js +339 -0
- package/dist/board/ItemDetails.js.map +1 -0
- package/dist/board/ItemDialog.d.ts +73 -0
- package/dist/board/ItemDialog.js +140 -0
- package/dist/board/ItemDialog.js.map +1 -0
- package/dist/board/ItemParts.d.ts +42 -0
- package/dist/board/ItemParts.js +82 -0
- package/dist/board/ItemParts.js.map +1 -0
- package/dist/board/ItemProgress.d.ts +56 -0
- package/dist/board/ItemProgress.js +138 -0
- package/dist/board/ItemProgress.js.map +1 -0
- package/dist/board/List.d.ts +65 -0
- package/dist/board/List.js +118 -0
- package/dist/board/List.js.map +1 -0
- package/dist/board/LoadingScreen.d.ts +38 -0
- package/dist/board/LoadingScreen.js +74 -0
- package/dist/board/LoadingScreen.js.map +1 -0
- package/dist/board/NeedsYou.d.ts +13 -0
- package/dist/board/NeedsYou.js +31 -0
- package/dist/board/NeedsYou.js.map +1 -0
- package/dist/board/Onboarding.d.ts +53 -0
- package/dist/board/Onboarding.js +122 -0
- package/dist/board/Onboarding.js.map +1 -0
- package/dist/board/PartsList.d.ts +60 -0
- package/dist/board/PartsList.js +115 -0
- package/dist/board/PartsList.js.map +1 -0
- package/dist/board/ProjectIcon.d.ts +51 -0
- package/dist/board/ProjectIcon.js +120 -0
- package/dist/board/ProjectIcon.js.map +1 -0
- package/dist/board/PullRequests.d.ts +43 -0
- package/dist/board/PullRequests.js +83 -0
- package/dist/board/PullRequests.js.map +1 -0
- package/dist/board/RowStyles.js +14 -0
- package/dist/board/RowStyles.js.map +1 -0
- package/dist/board/Settings.d.ts +54 -0
- package/dist/board/Settings.js +112 -0
- package/dist/board/Settings.js.map +1 -0
- package/dist/board/Slices.d.ts +32 -0
- package/dist/board/Slices.js +88 -0
- package/dist/board/Slices.js.map +1 -0
- package/dist/board/SourceIcon.d.ts +12 -0
- package/dist/board/SourceIcon.js +24 -0
- package/dist/board/SourceIcon.js.map +1 -0
- package/dist/board/StagePane.d.ts +43 -0
- package/dist/board/StagePane.js +97 -0
- package/dist/board/StagePane.js.map +1 -0
- package/dist/board/StageRow.d.ts +141 -0
- package/dist/board/StageRow.js +287 -0
- package/dist/board/StageRow.js.map +1 -0
- package/dist/board/StageTrack.d.ts +18 -0
- package/dist/board/StageTrack.js +38 -0
- package/dist/board/StageTrack.js.map +1 -0
- package/dist/board/StartControls.d.ts +30 -0
- package/dist/board/StartControls.js +51 -0
- package/dist/board/StartControls.js.map +1 -0
- package/dist/board/StatusColors.d.ts +5 -0
- package/dist/board/StatusColors.js +17 -0
- package/dist/board/StatusColors.js.map +1 -0
- package/dist/board/StatusGlyph.d.ts +18 -0
- package/dist/board/StatusGlyph.js +51 -0
- package/dist/board/StatusGlyph.js.map +1 -0
- package/dist/board/Threads.d.ts +13 -0
- package/dist/board/Threads.js +35 -0
- package/dist/board/Threads.js.map +1 -0
- package/dist/board/TrackerLabels.d.ts +27 -0
- package/dist/board/TrackerLabels.js +56 -0
- package/dist/board/TrackerLabels.js.map +1 -0
- package/dist/board/UpdatePill.d.ts +52 -0
- package/dist/board/UpdatePill.js +188 -0
- package/dist/board/UpdatePill.js.map +1 -0
- package/dist/board/Vocabulary.d.ts +56 -0
- package/dist/board/Vocabulary.js +104 -0
- package/dist/board/Vocabulary.js.map +1 -0
- package/dist/board/Warnings.d.ts +35 -0
- package/dist/board/Warnings.js +85 -0
- package/dist/board/Warnings.js.map +1 -0
- package/dist/board/WorkMap.d.ts +136 -0
- package/dist/board/WorkMap.js +274 -0
- package/dist/board/WorkMap.js.map +1 -0
- package/dist/board/index.d.ts +42 -0
- package/dist/collections/CollectionState.d.ts +10 -1
- package/dist/collections/CollectionState.js +12 -5
- package/dist/collections/CollectionState.js.map +1 -1
- package/dist/collections/ResultList.js +1 -1
- package/dist/collections/ScrollRow.js +1 -1
- package/dist/collections/VirtualList.js +1 -1
- package/dist/display/Avatar.d.ts +5 -2
- package/dist/display/Avatar.js +2 -2
- package/dist/display/Avatar.js.map +1 -1
- package/dist/display/Badge.d.ts +6 -1
- package/dist/display/Badge.js +9 -4
- package/dist/display/Badge.js.map +1 -1
- package/dist/display/CheckList.js +1 -1
- package/dist/display/Comment.d.ts +12 -0
- package/dist/display/Comment.js +39 -0
- package/dist/display/Comment.js.map +1 -0
- package/dist/display/CopyButton.d.ts +10 -2
- package/dist/display/CopyButton.js +29 -16
- package/dist/display/CopyButton.js.map +1 -1
- package/dist/display/Diagram.d.ts +17 -0
- package/dist/display/Diagram.js +49 -0
- package/dist/display/Diagram.js.map +1 -0
- package/dist/display/ImagePicker.js +1 -1
- package/dist/display/InfoBox.js +2 -2
- package/dist/display/InsetPanel.d.ts +3 -1
- package/dist/display/InsetPanel.js +7 -4
- package/dist/display/InsetPanel.js.map +1 -1
- package/dist/display/ItemAction.js +2 -2
- package/dist/display/LabelChip.d.ts +26 -0
- package/dist/display/LabelChip.js +41 -0
- package/dist/display/LabelChip.js.map +1 -0
- package/dist/display/LabelTint.js +60 -0
- package/dist/display/LabelTint.js.map +1 -0
- package/dist/display/Person.d.ts +5 -0
- package/dist/display/Person.js +19 -0
- package/dist/display/Person.js.map +1 -0
- package/dist/display/PropertySheet.js +1 -1
- package/dist/display/RadioChoice.d.ts +9 -2
- package/dist/display/RadioChoice.js +5 -1
- package/dist/display/RadioChoice.js.map +1 -1
- package/dist/display/RatingLink.js +1 -1
- package/dist/display/RichText.d.ts +12 -0
- package/dist/display/RichText.js +94 -0
- package/dist/display/RichText.js.map +1 -0
- package/dist/display/RowList.js +1 -1
- package/dist/display/index.d.ts +6 -1
- package/dist/feedback/Alert.js +1 -1
- package/dist/feedback/AsyncState.js +1 -1
- package/dist/feedback/DismissButton.d.ts +12 -0
- package/dist/feedback/DismissButton.js +20 -0
- package/dist/feedback/DismissButton.js.map +1 -0
- package/dist/feedback/ErrorNotification.js +2 -0
- package/dist/feedback/ErrorNotification.js.map +1 -1
- package/dist/feedback/FormResult.d.ts +16 -0
- package/dist/feedback/FormResult.js +30 -0
- package/dist/feedback/FormResult.js.map +1 -0
- package/dist/feedback/LiveRegion.d.ts +7 -0
- package/dist/feedback/LiveRegion.js +15 -0
- package/dist/feedback/LiveRegion.js.map +1 -0
- package/dist/feedback/Loading.d.ts +19 -0
- package/dist/feedback/Loading.js +63 -0
- package/dist/feedback/Loading.js.map +1 -0
- package/dist/feedback/Notifications.d.ts +35 -1
- package/dist/feedback/Notifications.js +196 -4
- package/dist/feedback/Notifications.js.map +1 -1
- package/dist/feedback/Progress.d.ts +15 -0
- package/dist/feedback/Progress.js +38 -0
- package/dist/feedback/Progress.js.map +1 -0
- package/dist/feedback/ProgressStatusRow.js +1 -1
- package/dist/feedback/StatusIndicator.d.ts +6 -1
- package/dist/feedback/StatusIndicator.js +19 -5
- package/dist/feedback/StatusIndicator.js.map +1 -1
- package/dist/feedback/index.d.ts +8 -4
- package/dist/forms/Checkbox.js +1 -1
- package/dist/forms/ColorSwatches.d.ts +21 -0
- package/dist/forms/ColorSwatches.js +26 -0
- package/dist/forms/ColorSwatches.js.map +1 -0
- package/dist/forms/FormSection.d.ts +68 -6
- package/dist/forms/FormSection.js +139 -8
- package/dist/forms/FormSection.js.map +1 -1
- package/dist/forms/HiddenField.d.ts +6 -0
- package/dist/forms/HiddenField.js +13 -0
- package/dist/forms/HiddenField.js.map +1 -0
- package/dist/forms/Input.d.ts +12 -2
- package/dist/forms/Input.js +10 -3
- package/dist/forms/Input.js.map +1 -1
- package/dist/forms/LabelPicker.d.ts +43 -0
- package/dist/forms/LabelPicker.js +107 -0
- package/dist/forms/LabelPicker.js.map +1 -0
- package/dist/forms/MarkdownEditor.d.ts +21 -0
- package/dist/forms/MarkdownEditor.js +175 -0
- package/dist/forms/MarkdownEditor.js.map +1 -0
- package/dist/forms/MarkdownToolbar.js +204 -0
- package/dist/forms/MarkdownToolbar.js.map +1 -0
- package/dist/forms/OptionSelect.d.ts +30 -9
- package/dist/forms/OptionSelect.js +53 -13
- package/dist/forms/OptionSelect.js.map +1 -1
- package/dist/forms/Select.d.ts +4 -1
- package/dist/forms/Select.js +3 -1
- package/dist/forms/Select.js.map +1 -1
- package/dist/forms/Switch.d.ts +4 -1
- package/dist/forms/Switch.js +5 -2
- package/dist/forms/Switch.js.map +1 -1
- package/dist/forms/TagInput.js +1 -1
- package/dist/forms/index.d.ts +7 -3
- package/dist/foundations/AmbientMotion.d.ts +2 -0
- package/dist/foundations/AmbientMotion.js +39 -0
- package/dist/foundations/AmbientMotion.js.map +1 -0
- package/dist/foundations/DesignSystemProvider.d.ts +34 -0
- package/dist/foundations/DesignSystemProvider.js +26 -1
- package/dist/foundations/DesignSystemProvider.js.map +1 -1
- package/dist/foundations/index.d.ts +2 -1
- package/dist/icons/BrandIcons.d.ts +8 -2
- package/dist/icons/BrandIcons.js +14 -3
- package/dist/icons/BrandIcons.js.map +1 -1
- package/dist/icons/dynamic.d.ts +2 -0
- package/dist/icons/dynamic.js +1 -0
- package/dist/icons/index.d.ts +2 -2
- package/dist/icons/index.js +1 -1
- package/dist/icons.d.ts +2 -2
- package/dist/icons.js +1 -1
- package/dist/index.d.ts +71 -10
- package/dist/index.js +78 -21
- package/dist/layout/Site.js +1 -1
- package/dist/layout/TitleBar.js +1 -1
- package/dist/layout/index.d.ts +2 -2
- package/dist/layout/index.js +3 -3
- package/dist/layout/toolbar/CommandPaletteTrigger.d.ts +17 -2
- package/dist/layout/toolbar/CommandPaletteTrigger.js +89 -6
- package/dist/layout/toolbar/CommandPaletteTrigger.js.map +1 -1
- package/dist/layout/toolbar/TriggerFit.js +56 -0
- package/dist/layout/toolbar/TriggerFit.js.map +1 -0
- package/dist/navigation/CollectionBrowseSelector.js +1 -1
- package/dist/navigation/CollectionViewToggle.d.ts +26 -0
- package/dist/navigation/CollectionViewToggle.js +54 -23
- package/dist/navigation/CollectionViewToggle.js.map +1 -1
- package/dist/navigation/DrilldownBrowseSelector.js +1 -1
- package/dist/navigation/FolderSelector.js +2 -2
- package/dist/navigation/SortMenu.d.ts +10 -0
- package/dist/navigation/SortMenu.js +33 -0
- package/dist/navigation/SortMenu.js.map +1 -0
- package/dist/navigation/index.d.ts +3 -2
- package/dist/overlays/CommandPalette.d.ts +38 -4
- package/dist/overlays/CommandPalette.js +140 -54
- package/dist/overlays/CommandPalette.js.map +1 -1
- package/dist/overlays/Drawer.js +1 -1
- package/dist/overlays/DropdownMenu.d.ts +7 -1
- package/dist/overlays/DropdownMenu.js +13 -2
- package/dist/overlays/DropdownMenu.js.map +1 -1
- package/dist/overlays/HelpTip.d.ts +8 -0
- package/dist/overlays/HelpTip.js +29 -0
- package/dist/overlays/HelpTip.js.map +1 -0
- package/dist/overlays/ImageViewer.js +2 -2
- package/dist/overlays/Modal.d.ts +5 -0
- package/dist/overlays/Modal.js +17 -2
- package/dist/overlays/Modal.js.map +1 -1
- package/dist/overlays/ModalExpandButton.d.ts +18 -0
- package/dist/overlays/ModalExpandButton.js +43 -0
- package/dist/overlays/ModalExpandButton.js.map +1 -0
- package/dist/overlays/Popover.d.ts +2 -1
- package/dist/overlays/Popover.js +1 -0
- package/dist/overlays/Popover.js.map +1 -1
- package/dist/overlays/index.d.ts +4 -2
- package/dist/showcase/Cards.js +1 -1
- package/dist/showcase/FeatureRow.js +1 -1
- package/dist/showcase/PhotoSearchConcept.js +1 -1
- package/dist/showcase/ProductBand.js +1 -1
- package/dist/showcase/Status.js +1 -1
- package/dist/typography/Code.d.ts +18 -0
- package/dist/typography/Code.js +24 -0
- package/dist/typography/Code.js.map +1 -0
- package/dist/typography/Link.d.ts +7 -2
- package/dist/typography/Link.js +15 -5
- package/dist/typography/Link.js.map +1 -1
- package/dist/typography/Typography.d.ts +19 -1
- package/dist/typography/Typography.js +11 -3
- package/dist/typography/Typography.js.map +1 -1
- package/dist/typography/index.d.ts +3 -3
- package/package.json +26 -3
- package/src/actions/Button.tsx +78 -11
- package/src/actions/ButtonLink.tsx +34 -0
- package/src/actions/ButtonRecipes.ts +10 -0
- package/src/actions/IconButton.tsx +7 -4
- package/src/board/ActivityLine.tsx +176 -0
- package/src/board/ActivityTrack.tsx +23 -0
- package/src/board/AddItem.tsx +259 -0
- package/src/board/Archive.tsx +75 -0
- package/src/board/Attention.tsx +135 -0
- package/src/board/BoardShell.tsx +30 -0
- package/src/board/BoardTitleBar.tsx +167 -0
- package/src/board/ButtonKinds.ts +32 -0
- package/src/board/Cleanup.tsx +19 -0
- package/src/board/Counters.tsx +224 -0
- package/src/board/DataAttributes.ts +3 -0
- package/src/board/Dependencies.tsx +49 -0
- package/src/board/Desk.tsx +155 -0
- package/src/board/Dossier.tsx +262 -0
- package/src/board/Filters.tsx +85 -0
- package/src/board/ItemDetails.tsx +435 -0
- package/src/board/ItemDialog.tsx +179 -0
- package/src/board/ItemParts.tsx +128 -0
- package/src/board/ItemProgress.tsx +164 -0
- package/src/board/List.tsx +154 -0
- package/src/board/LoadingScreen.tsx +78 -0
- package/src/board/NeedsYou.tsx +30 -0
- package/src/board/Onboarding.tsx +124 -0
- package/src/board/PartsList.tsx +160 -0
- package/src/board/ProjectIcon.tsx +172 -0
- package/src/board/PullRequests.tsx +106 -0
- package/src/board/RowStyles.ts +17 -0
- package/src/board/Settings.tsx +124 -0
- package/src/board/Slices.tsx +103 -0
- package/src/board/SourceIcon.tsx +33 -0
- package/src/board/StagePane.tsx +128 -0
- package/src/board/StageRow.tsx +320 -0
- package/src/board/StageTrack.tsx +61 -0
- package/src/board/StartControls.tsx +77 -0
- package/src/board/StatusColors.ts +28 -0
- package/src/board/StatusGlyph.tsx +70 -0
- package/src/board/Threads.tsx +38 -0
- package/src/board/TrackerLabels.tsx +75 -0
- package/src/board/UpdatePill.tsx +222 -0
- package/src/board/Vocabulary.tsx +155 -0
- package/src/board/Warnings.tsx +83 -0
- package/src/board/WorkMap.tsx +436 -0
- package/src/board/base.css +121 -0
- package/src/board/button-kinds.css +8 -0
- package/src/board/index.ts +224 -0
- package/src/board/items.css +75 -0
- package/src/board/loading-screen.css +171 -0
- package/src/board/rows.css +180 -0
- package/src/board/title-bar.css +56 -0
- package/src/board/tokens.css +52 -0
- package/src/board.css +19 -0
- package/src/collections/CollectionState.tsx +20 -7
- package/src/display/Avatar.tsx +6 -3
- package/src/display/Badge.tsx +25 -3
- package/src/display/Comment.tsx +26 -0
- package/src/display/CopyButton.tsx +48 -18
- package/src/display/Diagram.tsx +59 -0
- package/src/display/InsetPanel.tsx +4 -1
- package/src/display/LabelChip.tsx +73 -0
- package/src/display/LabelTint.ts +72 -0
- package/src/display/Person.tsx +11 -0
- package/src/display/RadioChoice.tsx +11 -2
- package/src/display/RichText.tsx +114 -0
- package/src/display/index.ts +5 -0
- package/src/feedback/DismissButton.tsx +28 -0
- package/src/feedback/ErrorNotification.ts +3 -0
- package/src/feedback/FormResult.tsx +50 -0
- package/src/feedback/LiveRegion.tsx +10 -0
- package/src/feedback/Loading.tsx +75 -0
- package/src/feedback/Notifications.tsx +267 -2
- package/src/feedback/Progress.tsx +44 -0
- package/src/feedback/StatusIndicator.tsx +34 -3
- package/src/feedback/index.ts +12 -2
- package/src/forms/ColorSwatches.tsx +50 -0
- package/src/forms/FormSection.tsx +245 -10
- package/src/forms/HiddenField.tsx +4 -0
- package/src/forms/Input.tsx +26 -4
- package/src/forms/LabelPicker.tsx +140 -0
- package/src/forms/MarkdownEditor.tsx +283 -0
- package/src/forms/MarkdownToolbar.tsx +267 -0
- package/src/forms/OptionSelect.tsx +110 -25
- package/src/forms/Select.tsx +20 -13
- package/src/forms/Switch.tsx +16 -2
- package/src/forms/index.ts +16 -2
- package/src/foundations/AmbientMotion.ts +51 -0
- package/src/foundations/DesignSystemProvider.tsx +19 -0
- package/src/foundations/index.ts +1 -0
- package/src/foundations/theme.css +64 -0
- package/src/icons/BrandIcons.tsx +12 -3
- package/src/icons/dynamic.ts +4 -0
- package/src/icons/index.ts +1 -1
- package/src/index.ts +2 -1
- package/src/layout/index.ts +1 -1
- package/src/layout/toolbar/CommandPaletteTrigger.tsx +187 -31
- package/src/layout/toolbar/TriggerFit.ts +84 -0
- package/src/navigation/CollectionViewToggle.tsx +94 -24
- package/src/navigation/SortMenu.tsx +41 -0
- package/src/navigation/index.ts +2 -1
- package/src/overlays/CommandPalette.tsx +190 -60
- package/src/overlays/DropdownMenu.tsx +12 -1
- package/src/overlays/HelpTip.tsx +29 -0
- package/src/overlays/Modal.tsx +23 -0
- package/src/overlays/ModalExpandButton.tsx +48 -0
- package/src/overlays/Popover.tsx +3 -1
- package/src/overlays/index.ts +9 -1
- package/src/styles.css +3 -1
- package/src/typography/Code.tsx +24 -0
- package/src/typography/Link.tsx +19 -10
- package/src/typography/Typography.tsx +25 -2
- package/src/typography/index.ts +1 -1
- package/dist/feedback/index.js +0 -11
- package/dist/typography/index.js +0 -4
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Renders a GitHub or Azure DevOps description or comment the way the provider shows it. Markdown is
|
|
2
|
+
// GitHub-flavoured and may hold raw HTML; an Azure DevOps field may be HTML. Both go through the same
|
|
3
|
+
// sanitizer, then images become links (the page never loads a remote image) and links open in a new tab. The output
|
|
4
|
+
// never makes its container scroll sideways.
|
|
5
|
+
import type { Element, ElementContent, Root } from 'hast';
|
|
6
|
+
import { toJsxRuntime } from 'hast-util-to-jsx-runtime';
|
|
7
|
+
import type { ReactNode } from 'react';
|
|
8
|
+
import Markdown from 'react-markdown';
|
|
9
|
+
import { Fragment, jsx, jsxs } from 'react/jsx-runtime';
|
|
10
|
+
import rehypeParse from 'rehype-parse';
|
|
11
|
+
import rehypeRaw from 'rehype-raw';
|
|
12
|
+
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
|
|
13
|
+
import remarkGfm from 'remark-gfm';
|
|
14
|
+
import { unified } from 'unified';
|
|
15
|
+
import type { PluggableList } from 'unified';
|
|
16
|
+
|
|
17
|
+
export type RichTextFormat = 'markdown' | 'html';
|
|
18
|
+
|
|
19
|
+
const link = (properties: Element['properties'], children: ElementContent[]): Element => ({
|
|
20
|
+
type: 'element',
|
|
21
|
+
tagName: 'a',
|
|
22
|
+
properties: { ...properties, target: '_blank', rel: ['noopener', 'noreferrer'] },
|
|
23
|
+
children,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/** Turns every image into a link to it and gives every link a new tab. An image inside a link, or without a
|
|
27
|
+
* usable source, becomes its text, since a link cannot hold a link. */
|
|
28
|
+
function rehypeImagesAsLinks() {
|
|
29
|
+
const walk = (node: Root | Element, inLink: boolean): void => {
|
|
30
|
+
node.children = node.children.map((child) => {
|
|
31
|
+
if (child.type !== 'element') return child;
|
|
32
|
+
|
|
33
|
+
if (child.tagName === 'img') {
|
|
34
|
+
const src = typeof child.properties.src === 'string' ? child.properties.src : '';
|
|
35
|
+
const text: ElementContent = { type: 'text', value: String(child.properties.alt || src) };
|
|
36
|
+
|
|
37
|
+
return src && !inLink ? link({ href: src }, [text]) : text;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
walk(child, inLink || child.tagName === 'a');
|
|
41
|
+
|
|
42
|
+
return child.tagName === 'a' ? link(child.properties, child.children) : child;
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return (tree: Root) => walk(tree, false);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// The default schema is GitHub's: it keeps <details>, tables and task-list checkboxes and drops scripts,
|
|
50
|
+
// handlers, javascript: URLs, iframes and styles. It keeps a dropped <style>'s text, so strip that element whole.
|
|
51
|
+
const schema = { ...defaultSchema, strip: [...(defaultSchema.strip ?? []), 'style'] };
|
|
52
|
+
const rehypePlugins: PluggableList = [rehypeRaw, [rehypeSanitize, schema], rehypeImagesAsLinks];
|
|
53
|
+
const htmlProcessor = unified()
|
|
54
|
+
.use(rehypeParse, { fragment: true })
|
|
55
|
+
.use(rehypeSanitize, schema)
|
|
56
|
+
.use(rehypeImagesAsLinks);
|
|
57
|
+
|
|
58
|
+
// Tailwind has no typography plugin and its preflight resets headings, lists and tables, so the elements a
|
|
59
|
+
// provider's text holds are styled here.
|
|
60
|
+
const body = [
|
|
61
|
+
'min-w-0 max-w-full text-sm leading-relaxed break-words [overflow-wrap:anywhere]',
|
|
62
|
+
'[&>:first-child]:mt-0 [&>:last-child]:mb-0 [&_p]:my-2 [&_ul]:my-2 [&_ol]:my-2 [&_blockquote]:my-2',
|
|
63
|
+
'[&_h1]:mt-4 [&_h1]:mb-2 [&_h1]:text-lg [&_h1]:font-semibold [&_h2]:mt-4 [&_h2]:mb-2 [&_h2]:text-base [&_h2]:font-semibold',
|
|
64
|
+
'[&_h3]:mt-3 [&_h3]:mb-1.5 [&_h3]:font-semibold [&_h4]:mt-3 [&_h4]:mb-1.5 [&_h4]:font-semibold [&_h5]:font-semibold [&_h6]:font-semibold',
|
|
65
|
+
'[&_ul]:list-disc [&_ul]:ps-6 [&_ol]:list-decimal [&_ol]:ps-6 [&_li]:my-0.5 [&_.task-list-item]:list-none [&_.task-list-item]:ms-[-1.5rem] [&_input]:me-1.5',
|
|
66
|
+
'[&_a]:text-(--lar-info-text) [&_a]:underline [&_a]:underline-offset-2',
|
|
67
|
+
'[&_blockquote]:border-s-2 [&_blockquote]:ps-3 [&_blockquote]:text-base-700',
|
|
68
|
+
'[&_hr]:my-4',
|
|
69
|
+
'[&_pre]:my-2 [&_pre]:max-w-full [&_pre]:overflow-x-hidden [&_pre]:whitespace-pre-wrap [&_pre]:rounded-md [&_pre]:bg-base-200 [&_pre]:p-3 [&_pre]:font-mono [&_pre]:text-xs',
|
|
70
|
+
'[&_:not(pre)>code]:rounded [&_:not(pre)>code]:bg-base-200 [&_:not(pre)>code]:px-1 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-xs',
|
|
71
|
+
'[&_table]:my-2 [&_table]:w-full [&_table]:max-w-full [&_table]:table-fixed [&_th]:border [&_th]:px-2 [&_th]:py-1 [&_th]:text-start [&_td]:border [&_td]:px-2 [&_td]:py-1',
|
|
72
|
+
'[&_details]:my-2 [&_summary]:cursor-pointer [&_summary]:font-medium',
|
|
73
|
+
].join(' ');
|
|
74
|
+
|
|
75
|
+
interface RichTextProps {
|
|
76
|
+
text: string;
|
|
77
|
+
format: RichTextFormat;
|
|
78
|
+
/** Draws each fenced ```mermaid block of Markdown, such as with `Diagram`, between the text around it. */
|
|
79
|
+
renderDiagram?: (source: string) => ReactNode;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A provider's text, sanitized and styled; with `renderDiagram`, Markdown's Mermaid blocks draw as diagrams. */
|
|
83
|
+
export function RichText({ text, format, renderDiagram }: RichTextProps) {
|
|
84
|
+
if (text === '') return null;
|
|
85
|
+
|
|
86
|
+
if (renderDiagram && format === 'markdown') {
|
|
87
|
+
const parts = text.split(/```mermaid[^\S\r\n]*\r?\n([\s\S]*?)```/g);
|
|
88
|
+
|
|
89
|
+
if (parts.length > 1)
|
|
90
|
+
return (
|
|
91
|
+
<div className="grid min-w-0 gap-3" data-slot="rich-text-with-diagrams">
|
|
92
|
+
{parts.map((part, index) =>
|
|
93
|
+
index % 2 === 1 ? (
|
|
94
|
+
<Fragment key={index}>{renderDiagram(part.trim())}</Fragment>
|
|
95
|
+
) : part.trim() ? (
|
|
96
|
+
<RichText key={index} text={part} format="markdown" />
|
|
97
|
+
) : null,
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div className={body} data-slot="rich-text">
|
|
105
|
+
{format === 'markdown' ? (
|
|
106
|
+
<Markdown rehypePlugins={rehypePlugins} remarkPlugins={[remarkGfm]}>
|
|
107
|
+
{text}
|
|
108
|
+
</Markdown>
|
|
109
|
+
) : (
|
|
110
|
+
toJsxRuntime(htmlProcessor.runSync(htmlProcessor.parse(text)), { Fragment, jsx, jsxs })
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
package/src/display/index.ts
CHANGED
|
@@ -7,11 +7,13 @@ export { BrandLink, BrandMark } from './BrandMark';
|
|
|
7
7
|
export { ChangeList, type ChangeListItem } from './ChangeList';
|
|
8
8
|
export { CheckList, type CheckListItem } from './CheckList';
|
|
9
9
|
export { CodeBlock } from './CodeBlock';
|
|
10
|
+
export { Comment, CommentList } from './Comment';
|
|
10
11
|
export { CopyButton } from './CopyButton';
|
|
11
12
|
export { DataTable, type DataTableCell, type DataTableRow } from './DataTable';
|
|
12
13
|
export { DetailHero, DetailHeroDetail, DetailHeroDetails, DetailHeroTags } from './DetailHero';
|
|
13
14
|
export { DetailList } from './DetailList';
|
|
14
15
|
export { DeviceCard } from './DeviceCard';
|
|
16
|
+
export { Diagram } from './Diagram';
|
|
15
17
|
export { Identity } from './Identity';
|
|
16
18
|
export { ImageCard } from './ImageCard';
|
|
17
19
|
export { ImagePicker, ImagePickerGrid, ImagePickerOption, ImageUpload } from './ImagePicker';
|
|
@@ -20,15 +22,18 @@ export { InsetPanel } from './InsetPanel';
|
|
|
20
22
|
export { ItemAction } from './ItemAction';
|
|
21
23
|
export { Kbd } from './Kbd';
|
|
22
24
|
export { KindIcon } from './KindIcon';
|
|
25
|
+
export { LabelChip } from './LabelChip';
|
|
23
26
|
export { NoticeCard } from './NoticeCard';
|
|
24
27
|
export { PairingList, type PairingListItem } from './PairingList';
|
|
25
28
|
export { PathList } from './PathList';
|
|
26
29
|
export { PeoplePreview } from './PeoplePreview';
|
|
30
|
+
export { Person } from './Person';
|
|
27
31
|
export { PropertySheet, type PropertySheetRow, type PropertySheetTag } from './PropertySheet';
|
|
28
32
|
export { QrCode } from './QrCode';
|
|
29
33
|
export { RadioChoice } from './RadioChoice';
|
|
30
34
|
export { RatingLink } from './RatingLink';
|
|
31
35
|
export { ResourceCard, ResourceCardSkeleton } from './ResourceCard';
|
|
36
|
+
export { RichText, type RichTextFormat } from './RichText';
|
|
32
37
|
export { RowList, RowListItem } from './RowList';
|
|
33
38
|
export { MeterLegend, type MeterSegment, type MeterTone, StackedMeter } from './StackedMeter';
|
|
34
39
|
export { StatTile } from './StatTile';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { XIcon } from 'lucide-react';
|
|
2
|
+
import type { MouseEvent } from 'react';
|
|
3
|
+
|
|
4
|
+
import { IconButton } from '../actions/IconButton';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The close button every failure, warning and notification carries; `label` names what it closes, such as
|
|
8
|
+
* "Dismiss: Moved #61". In the `corner` it floats to the top right of the text it closes; `inline` sits in a line of
|
|
9
|
+
* text, after an error message.
|
|
10
|
+
*/
|
|
11
|
+
export function DismissButton({
|
|
12
|
+
label,
|
|
13
|
+
onClick,
|
|
14
|
+
placement = 'corner',
|
|
15
|
+
}: {
|
|
16
|
+
label: string;
|
|
17
|
+
onClick: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
18
|
+
placement?: 'corner' | 'inline';
|
|
19
|
+
}) {
|
|
20
|
+
return (
|
|
21
|
+
<IconButton
|
|
22
|
+
className={`dismiss ${placement === 'inline' ? 'float-none -my-1 ml-0.5 align-middle' : 'float-right -mt-0.75 -mr-1.5 ml-2'}`}
|
|
23
|
+
aria-label={label}
|
|
24
|
+
icon={<XIcon />}
|
|
25
|
+
onClick={onClick}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { toast } from '@ninna-ui/feedback';
|
|
2
2
|
|
|
3
|
+
import { forgetNotificationSlots } from './Notifications';
|
|
4
|
+
|
|
3
5
|
export interface ErrorNotification {
|
|
4
6
|
description: string;
|
|
5
7
|
title: string;
|
|
@@ -7,6 +9,7 @@ export interface ErrorNotification {
|
|
|
7
9
|
|
|
8
10
|
export function dismissAllNotifications(): void {
|
|
9
11
|
toast.dismissAll();
|
|
12
|
+
forgetNotificationSlots();
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
export function showErrorNotification(notification: ErrorNotification): void {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { Alert } from './Alert';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A form's result, such as a dialog's validation message or what its submit did. It stays mounted as a live region,
|
|
7
|
+
* so screen readers hear each new message; empty, it has no border, padding or icon. With no tone it is a warning, as
|
|
8
|
+
* a validation message is. Line breaks in `text` show.
|
|
9
|
+
*/
|
|
10
|
+
export function FormResult({
|
|
11
|
+
id,
|
|
12
|
+
text,
|
|
13
|
+
tone = 'warning',
|
|
14
|
+
children,
|
|
15
|
+
list,
|
|
16
|
+
}: {
|
|
17
|
+
id: string;
|
|
18
|
+
text: string;
|
|
19
|
+
tone?: 'success' | 'warning' | 'error' | undefined;
|
|
20
|
+
/** Shown under the text. */
|
|
21
|
+
children?: ReactNode;
|
|
22
|
+
/** A list under the text, such as the paths a call touched, indented and wrapping normally. */
|
|
23
|
+
list?: readonly ReactNode[] | undefined;
|
|
24
|
+
}) {
|
|
25
|
+
return (
|
|
26
|
+
<Alert
|
|
27
|
+
id={id}
|
|
28
|
+
tone={tone === 'error' ? 'danger' : tone}
|
|
29
|
+
data-tone={tone}
|
|
30
|
+
role="status"
|
|
31
|
+
aria-atomic="true"
|
|
32
|
+
showIcon={Boolean(text)}
|
|
33
|
+
className={text ? 'wrap-anywhere whitespace-pre-line' : 'border-0 p-0'}
|
|
34
|
+
>
|
|
35
|
+
{text && (
|
|
36
|
+
<>
|
|
37
|
+
{text}
|
|
38
|
+
{children}
|
|
39
|
+
{list && list.length > 0 && (
|
|
40
|
+
<ul className="pl-4.5 whitespace-normal">
|
|
41
|
+
{list.map((entry, index) => (
|
|
42
|
+
<li key={index}>{entry}</li>
|
|
43
|
+
))}
|
|
44
|
+
</ul>
|
|
45
|
+
)}
|
|
46
|
+
</>
|
|
47
|
+
)}
|
|
48
|
+
</Alert>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/** An out-of-sight polite live region: screen readers read what it says each time that changes, such as "Copied". */
|
|
4
|
+
export function LiveRegion({ id, children }: { id?: string; children?: ReactNode }) {
|
|
5
|
+
return (
|
|
6
|
+
<div id={id} className="sr-only" role="status" aria-live="polite">
|
|
7
|
+
{children}
|
|
8
|
+
</div>
|
|
9
|
+
);
|
|
10
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Loading as NinnaLoading, type LoadingProps as NinnaLoadingProps } from '@ninna-ui/feedback';
|
|
2
|
+
import { LoaderCircleIcon, type LucideProps } from 'lucide-react';
|
|
3
|
+
import { forwardRef } from 'react';
|
|
4
|
+
|
|
5
|
+
import { ambientMotion } from '../foundations/AmbientMotion';
|
|
6
|
+
|
|
7
|
+
const motions = {
|
|
8
|
+
spin: 'motion-safe:animate-spin',
|
|
9
|
+
always: 'animate-spin',
|
|
10
|
+
status: 'status-spin',
|
|
11
|
+
still: '',
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
type SpinnerIconProps = LucideProps & {
|
|
15
|
+
/** The spin: `spin` turns unless the user prefers reduced motion, `always` turns regardless, as a pending action's
|
|
16
|
+
* does, `status` is a status glyph's own spin (its page defines `status-spin`), and `still` doesn't turn. */
|
|
17
|
+
motion?: keyof typeof motions;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The loader icon, spinning on a box around it: above a device scale of 1 Chromium can't run a root svg's transform on
|
|
22
|
+
* the compositor, so the page would render every frame. It pauses while out of view or in a hidden window.
|
|
23
|
+
*/
|
|
24
|
+
export function SpinnerIcon({ motion = 'spin', ...icon }: SpinnerIconProps) {
|
|
25
|
+
const spin = motions[motion];
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<span ref={spin ? ambientMotion : undefined} className={`flex w-fit shrink-0 ${spin}`}>
|
|
29
|
+
<LoaderCircleIcon {...icon} />
|
|
30
|
+
</span>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type LoadingProps = Omit<NinnaLoadingProps, 'title'> & {
|
|
35
|
+
/** What loads. Without a `presentation` it names ninna-ui's spinner for screen readers only; with one it shows
|
|
36
|
+
* beside the spinner. */
|
|
37
|
+
label?: string;
|
|
38
|
+
/** With no label, names the spinner alone, as a cue beside values already shown. */
|
|
39
|
+
title?: string | undefined;
|
|
40
|
+
/** Stops it spinning. */
|
|
41
|
+
still?: boolean;
|
|
42
|
+
/** Without it, ninna-ui's announced spinner. `inline` is a small spinner and what loads in a line of 12px text;
|
|
43
|
+
* `line` stands on its own line of body text; `status` is a step's own loading line, announced, muted body text
|
|
44
|
+
* with room above and below that keeps turning as a pending action's spinner does. */
|
|
45
|
+
presentation?: 'inline' | 'line' | 'status';
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** What shows in place of something still loading: ninna-ui's announced spinner, or with a `presentation`, a small
|
|
49
|
+
* spinner, still under reduced motion, and what loads. */
|
|
50
|
+
export const Loading = forwardRef<HTMLDivElement, LoadingProps>(
|
|
51
|
+
({ presentation, label, title, still = false, ...props }, ref) => {
|
|
52
|
+
if (!presentation) return <NinnaLoading ref={ref} label={label} title={title} {...props} />;
|
|
53
|
+
|
|
54
|
+
if (presentation === 'status')
|
|
55
|
+
return (
|
|
56
|
+
<p role="status" className="text-base-700 m-0 flex items-center gap-2 py-4 text-sm">
|
|
57
|
+
<SpinnerIcon motion={still ? 'still' : 'always'} aria-hidden="true" className="size-4" />
|
|
58
|
+
{label}
|
|
59
|
+
</p>
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<span data-loader className="inline-flex min-w-0 items-center gap-1.5" title={title}>
|
|
64
|
+
<SpinnerIcon
|
|
65
|
+
motion={still ? 'still' : 'spin'}
|
|
66
|
+
aria-hidden="true"
|
|
67
|
+
className={presentation === 'line' ? 'size-4' : 'size-3'}
|
|
68
|
+
/>
|
|
69
|
+
{label ? <span className="truncate">{label}</span> : title && <span className="sr-only">{title}</span>}
|
|
70
|
+
</span>
|
|
71
|
+
);
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
Loading.displayName = 'Loading';
|
|
@@ -1,5 +1,270 @@
|
|
|
1
|
-
|
|
1
|
+
/* oxlint-disable react/only-export-components -- The slot functions and the body's controls belong to the toaster. */
|
|
2
|
+
|
|
3
|
+
import { toast, Toaster } from '@ninna-ui/feedback';
|
|
4
|
+
import {
|
|
5
|
+
createContext,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
type RefObject,
|
|
8
|
+
useContext,
|
|
9
|
+
useEffect,
|
|
10
|
+
useLayoutEffect,
|
|
11
|
+
useRef,
|
|
12
|
+
useState,
|
|
13
|
+
} from 'react';
|
|
14
|
+
import { flushSync } from 'react-dom';
|
|
15
|
+
|
|
16
|
+
import { DismissButton } from './DismissButton';
|
|
17
|
+
|
|
18
|
+
/** How long a success stays once the pointer and the focus have left it. */
|
|
19
|
+
const SUCCESS_MS = 5_000;
|
|
20
|
+
|
|
21
|
+
/** Each notification's fade, as wide as a Modal body's: notifications scrolled past fade out at the stack's edges. */
|
|
22
|
+
const FADE_PX = 24;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The page's notifications. `corner` shows a few at a time in the bottom right corner. `stack` keeps every one, oldest
|
|
26
|
+
* above the newest, above every dialog: when they don't fit, the stack scrolls, follows the newest while you are at the
|
|
27
|
+
* bottom, and fades those scrolled past at its edges, as a Modal's body does. Mount one, once, outside the page's root.
|
|
28
|
+
*/
|
|
29
|
+
export function Notifications({ presentation = 'corner' }: { presentation?: 'corner' | 'stack' }) {
|
|
30
|
+
if (presentation === 'stack')
|
|
31
|
+
return (
|
|
32
|
+
<Toaster
|
|
33
|
+
ref={followNewest}
|
|
34
|
+
position="bottom-right"
|
|
35
|
+
max={Infinity}
|
|
36
|
+
className="notification-stack"
|
|
37
|
+
offset="calc(var(--spacing) * 1)"
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
2
40
|
|
|
3
|
-
export function Notifications() {
|
|
4
41
|
return <Toaster position="bottom-right" />;
|
|
5
42
|
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Follows the newest notification while the user is at the bottom: a new entry, a narrower or shorter window and a
|
|
46
|
+
* notification growing all keep it in view. Once the user scrolls away the stack stays where they left it, until they
|
|
47
|
+
* scroll back. Each notification carries its own slice of the edge fade: a mask on the stack would make it the
|
|
48
|
+
* notifications' backdrop root and cut their blur off from the page behind.
|
|
49
|
+
*/
|
|
50
|
+
function followNewest(viewport: HTMLDivElement | null) {
|
|
51
|
+
if (!viewport) return;
|
|
52
|
+
|
|
53
|
+
let following = true;
|
|
54
|
+
const entries = () => Array.from(viewport.children).filter((el): el is HTMLElement => el instanceof HTMLElement);
|
|
55
|
+
const fade = () => {
|
|
56
|
+
const { scrollTop, scrollHeight, clientHeight } = viewport;
|
|
57
|
+
const stack = viewport.getBoundingClientRect();
|
|
58
|
+
const top = Math.min(FADE_PX, Math.max(0, scrollTop));
|
|
59
|
+
const bottom = Math.min(FADE_PX, Math.max(0, scrollHeight - clientHeight - scrollTop));
|
|
60
|
+
|
|
61
|
+
for (const entry of entries()) {
|
|
62
|
+
const y = entry.getBoundingClientRect().top - stack.top;
|
|
63
|
+
|
|
64
|
+
entry.style.setProperty('--fade-from', `${-y}px`);
|
|
65
|
+
entry.style.setProperty('--fade-top', `${top - y}px`);
|
|
66
|
+
entry.style.setProperty('--fade-bottom', `${stack.height - bottom - y}px`);
|
|
67
|
+
entry.style.setProperty('--fade-to', `${stack.height - y}px`);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const toNewest = () => {
|
|
71
|
+
if (following) viewport.scrollTop = viewport.scrollHeight;
|
|
72
|
+
|
|
73
|
+
fade();
|
|
74
|
+
};
|
|
75
|
+
const scrolled = () => {
|
|
76
|
+
following = viewport.scrollHeight - viewport.clientHeight - viewport.scrollTop <= 1;
|
|
77
|
+
fade();
|
|
78
|
+
};
|
|
79
|
+
const resizes = new ResizeObserver(toNewest);
|
|
80
|
+
const observe = () => {
|
|
81
|
+
resizes.disconnect();
|
|
82
|
+
resizes.observe(viewport);
|
|
83
|
+
entries().forEach((entry) => resizes.observe(entry));
|
|
84
|
+
};
|
|
85
|
+
const arrivals = new MutationObserver((changes) => {
|
|
86
|
+
observe();
|
|
87
|
+
|
|
88
|
+
const newest = viewport.lastElementChild;
|
|
89
|
+
const arrived = changes.some(({ addedNodes }) => Array.from(addedNodes).includes(newest as Node));
|
|
90
|
+
|
|
91
|
+
// Only a new entry moves the stack; one closing or changing above leaves the user where they are.
|
|
92
|
+
if (arrived) toNewest();
|
|
93
|
+
else fade();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
arrivals.observe(viewport, { childList: true });
|
|
97
|
+
viewport.addEventListener('scroll', scrolled, { passive: true });
|
|
98
|
+
// Entering notifications slide in; measure them where they land.
|
|
99
|
+
viewport.addEventListener('animationend', fade);
|
|
100
|
+
viewport.addEventListener('transitionend', fade);
|
|
101
|
+
observe();
|
|
102
|
+
toNewest();
|
|
103
|
+
|
|
104
|
+
return () => {
|
|
105
|
+
arrivals.disconnect();
|
|
106
|
+
resizes.disconnect();
|
|
107
|
+
viewport.removeEventListener('scroll', scrolled);
|
|
108
|
+
viewport.removeEventListener('animationend', fade);
|
|
109
|
+
viewport.removeEventListener('transitionend', fade);
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface Notification {
|
|
114
|
+
/** One notification per slot: a new one replaces the last. */
|
|
115
|
+
slot: string;
|
|
116
|
+
/** A failure is red and stays until it's closed; a success is green and clears itself five seconds after the
|
|
117
|
+
* pointer and the keyboard focus leave it. */
|
|
118
|
+
tone?: 'success' | 'danger';
|
|
119
|
+
/** Names the close button, such as "Dismiss: Moved #61". */
|
|
120
|
+
dismissLabel: string;
|
|
121
|
+
/** What it says: text, links and buttons. */
|
|
122
|
+
body: ReactNode;
|
|
123
|
+
/** Called after it closes while the keyboard focus was inside it, to put the focus somewhere useful. */
|
|
124
|
+
onFocusLost?: () => void;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface NotificationControls {
|
|
128
|
+
/** Keeps a success from clearing itself while `held`, such as while an action it offers is under way. */
|
|
129
|
+
hold: (held: boolean) => void;
|
|
130
|
+
/** Shows a success as a failure while `failed`, such as when an action it offers failed. */
|
|
131
|
+
fail: (failed: boolean) => void;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const Controls = createContext<NotificationControls>({ hold: () => {}, fail: () => {} });
|
|
135
|
+
|
|
136
|
+
/** For a notification's body: what it can change about the notification it sits in. */
|
|
137
|
+
export function useNotificationControls(): NotificationControls {
|
|
138
|
+
return useContext(Controls);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The id of the notification in each slot, in the toast manager. */
|
|
142
|
+
const slots = new Map<string, string>();
|
|
143
|
+
|
|
144
|
+
/** Forgets every slot, as the toasts they named are gone. */
|
|
145
|
+
export function forgetNotificationSlots(): void {
|
|
146
|
+
slots.clear();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Closes the notification `id` in `slot`; if the focus was in it, `onFocusLost` puts it somewhere else. */
|
|
150
|
+
function close(slot: string, id: string, onFocusLost?: () => void): void {
|
|
151
|
+
const hadFocus = Boolean(document.querySelector(`[data-toast-id="${id}"]`)?.contains(document.activeElement));
|
|
152
|
+
|
|
153
|
+
flushSync(() => {
|
|
154
|
+
if (slots.get(slot) === id) slots.delete(slot);
|
|
155
|
+
|
|
156
|
+
toast.dismiss(id);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (hadFocus) onFocusLost?.();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Shows `notification` in its slot, replacing the one there. */
|
|
163
|
+
export function showNotification(notification: Notification): void {
|
|
164
|
+
clearNotification(notification.slot);
|
|
165
|
+
|
|
166
|
+
const id: string = toast.create({
|
|
167
|
+
color: notification.tone === 'danger' ? 'danger' : 'success',
|
|
168
|
+
// The body's own clock closes successes and its own button closes every notification, so the manager never does.
|
|
169
|
+
duration: 0,
|
|
170
|
+
closable: false,
|
|
171
|
+
description: (
|
|
172
|
+
<NotificationBody
|
|
173
|
+
{...notification}
|
|
174
|
+
onClose={() => close(notification.slot, id, notification.onFocusLost)}
|
|
175
|
+
/>
|
|
176
|
+
),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
slots.set(notification.slot, id);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Closes whatever notification holds this slot. */
|
|
183
|
+
export function clearNotification(slot: string): void {
|
|
184
|
+
const id = slots.get(slot);
|
|
185
|
+
|
|
186
|
+
if (id) close(slot, id);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Gives a success five seconds after hover and keyboard focus leave it. */
|
|
190
|
+
function useSuccessClose(ref: RefObject<Element | null>, enabled: boolean, onClose: () => void): void {
|
|
191
|
+
const latest = useRef(onClose);
|
|
192
|
+
|
|
193
|
+
useLayoutEffect(() => {
|
|
194
|
+
latest.current = onClose;
|
|
195
|
+
});
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (!enabled) return;
|
|
198
|
+
|
|
199
|
+
const entry = ref.current?.closest<HTMLElement>('[data-slot="toast"]');
|
|
200
|
+
|
|
201
|
+
if (!entry) return;
|
|
202
|
+
|
|
203
|
+
const held = {
|
|
204
|
+
hovered: entry.matches(':hover'),
|
|
205
|
+
focused: entry.contains(document.activeElement),
|
|
206
|
+
timer: undefined as ReturnType<typeof setTimeout> | undefined,
|
|
207
|
+
};
|
|
208
|
+
const restart = () => {
|
|
209
|
+
clearTimeout(held.timer);
|
|
210
|
+
|
|
211
|
+
if (!held.hovered && !held.focused) held.timer = setTimeout(() => latest.current(), SUCCESS_MS);
|
|
212
|
+
};
|
|
213
|
+
const enter = () => {
|
|
214
|
+
held.hovered = true;
|
|
215
|
+
restart();
|
|
216
|
+
};
|
|
217
|
+
const leave = () => {
|
|
218
|
+
held.hovered = false;
|
|
219
|
+
restart();
|
|
220
|
+
};
|
|
221
|
+
const focus = () => {
|
|
222
|
+
held.focused = true;
|
|
223
|
+
restart();
|
|
224
|
+
};
|
|
225
|
+
const blur = (event: FocusEvent) => {
|
|
226
|
+
held.focused = event.relatedTarget instanceof Node && entry.contains(event.relatedTarget);
|
|
227
|
+
restart();
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
entry.addEventListener('mouseenter', enter);
|
|
231
|
+
entry.addEventListener('mouseleave', leave);
|
|
232
|
+
entry.addEventListener('focusin', focus);
|
|
233
|
+
entry.addEventListener('focusout', blur);
|
|
234
|
+
restart();
|
|
235
|
+
|
|
236
|
+
return () => {
|
|
237
|
+
clearTimeout(held.timer);
|
|
238
|
+
entry.removeEventListener('mouseenter', enter);
|
|
239
|
+
entry.removeEventListener('mouseleave', leave);
|
|
240
|
+
entry.removeEventListener('focusin', focus);
|
|
241
|
+
entry.removeEventListener('focusout', blur);
|
|
242
|
+
};
|
|
243
|
+
}, [enabled, ref]);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** One notification's content. `onClose` runs when its close button is clicked or, for a success, when its time is up. */
|
|
247
|
+
function NotificationBody({
|
|
248
|
+
slot,
|
|
249
|
+
dismissLabel,
|
|
250
|
+
tone = 'success',
|
|
251
|
+
body,
|
|
252
|
+
onClose,
|
|
253
|
+
}: Notification & { onClose: () => void }) {
|
|
254
|
+
const ref = useRef<HTMLSpanElement>(null);
|
|
255
|
+
const [held, setHeld] = useState(false);
|
|
256
|
+
const [bodyFailed, setBodyFailed] = useState(false);
|
|
257
|
+
const failed = tone === 'danger' || bodyFailed;
|
|
258
|
+
|
|
259
|
+
useEffect(() => {
|
|
260
|
+
ref.current?.closest('[data-slot="toast"]')?.setAttribute('data-toast-color', failed ? 'danger' : 'success');
|
|
261
|
+
}, [failed]);
|
|
262
|
+
useSuccessClose(ref, !failed && !held, onClose);
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<span ref={ref} data-toast={slot}>
|
|
266
|
+
<DismissButton label={dismissLabel} onClick={onClose} />
|
|
267
|
+
<Controls.Provider value={{ hold: setHeld, fail: setBodyFailed }}>{body}</Controls.Provider>
|
|
268
|
+
</span>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { Text } from '../typography/Typography';
|
|
4
|
+
|
|
5
|
+
type ProgressProps = Omit<ComponentProps<'div'>, 'children' | 'className' | 'style'> & {
|
|
6
|
+
value: number;
|
|
7
|
+
max?: number;
|
|
8
|
+
/** Muted text after the bar, such as "Step 2 of 4". */
|
|
9
|
+
caption?: ReactNode;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A determinate progress bar: a rounded track filled to value / max, then an optional caption. Its props (an
|
|
14
|
+
* `aria-label`, say) go on the progressbar. Use ProgressStatusRow for a task's status row and StackedMeter for shares
|
|
15
|
+
* of a whole.
|
|
16
|
+
*/
|
|
17
|
+
export function Progress({ caption, value, max = 100, ...props }: ProgressProps) {
|
|
18
|
+
const share = max > 0 ? Math.min(Math.max(value / max, 0), 1) : 0;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="flex items-center gap-2" data-slot="progress">
|
|
22
|
+
<div
|
|
23
|
+
role="progressbar"
|
|
24
|
+
aria-valuemin={0}
|
|
25
|
+
aria-valuemax={max}
|
|
26
|
+
aria-valuenow={value}
|
|
27
|
+
className="bg-base-200 h-2 max-w-65 flex-1 overflow-hidden rounded-full"
|
|
28
|
+
data-slot="progress-bar"
|
|
29
|
+
{...props}
|
|
30
|
+
>
|
|
31
|
+
<div
|
|
32
|
+
className="bg-primary h-full rounded-[inherit]"
|
|
33
|
+
style={{ width: `${share * 100}%` }}
|
|
34
|
+
data-slot="progress-indicator"
|
|
35
|
+
/>
|
|
36
|
+
</div>
|
|
37
|
+
{caption && (
|
|
38
|
+
<Text as="span" variant="caption" tone="muted" data-slot="progress-caption">
|
|
39
|
+
{caption}
|
|
40
|
+
</Text>
|
|
41
|
+
)}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|