@tpzdsp/next-toolkit 2.5.0 → 3.0.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/package.json +22 -13
- package/src/assets/styles/globals.css +75 -95
- package/src/assets/styles/ol.css +57 -16
- package/src/assets/styles/utils.css +80 -0
- package/src/components/Accordion/Accordion.css +85 -0
- package/src/components/Accordion/Accordion.stories.tsx +264 -0
- package/src/components/Accordion/Accordion.tsx +98 -0
- package/src/components/Alert/Alert.css +129 -0
- package/src/components/Alert/Alert.stories.tsx +291 -0
- package/src/components/Alert/Alert.tsx +76 -0
- package/src/components/BackLink/BackLink.css +12 -0
- package/src/components/BackLink/BackLink.stories.tsx +52 -0
- package/src/components/BackLink/BackLink.tsx +44 -0
- package/src/components/BackToTop/BackToTop.css +23 -0
- package/src/components/BackToTop/BackToTop.stories.tsx +41 -0
- package/src/components/BackToTop/BackToTop.tsx +48 -0
- package/src/components/Breadcrumb/Breadcrumb.css +42 -0
- package/src/components/Breadcrumb/Breadcrumb.stories.tsx +94 -0
- package/src/components/Breadcrumb/Breadcrumb.tsx +135 -0
- package/src/components/Button/Button.css +229 -0
- package/src/components/Button/Button.stories.tsx +140 -20
- package/src/components/Button/Button.tsx +53 -28
- package/src/components/Button/ButtonSkeleton.stories.tsx +50 -0
- package/src/components/Button/ButtonSkeleton.tsx +14 -0
- package/src/components/ButtonGroup/ButtonGroup.css +52 -0
- package/src/components/ButtonGroup/ButtonGroup.stories.tsx +156 -0
- package/src/components/ButtonGroup/ButtonGroup.tsx +50 -0
- package/src/components/ButtonLink/ButtonLink.css +114 -0
- package/src/components/ButtonLink/ButtonLink.stories.tsx +38 -51
- package/src/components/ButtonLink/ButtonLink.tsx +54 -23
- package/src/components/Card/Card.css +13 -0
- package/src/components/Card/Card.stories.tsx +98 -57
- package/src/components/Card/Card.tsx +4 -13
- package/src/components/CardGroup/CardGroup.css +10 -0
- package/src/components/CardGroup/CardGroup.stories.tsx +81 -0
- package/src/components/CardGroup/CardGroup.tsx +9 -0
- package/src/components/Checkbox/Checkbox.css +49 -0
- package/src/components/Checkbox/Checkbox.stories.tsx +86 -0
- package/src/components/Checkbox/Checkbox.tsx +32 -0
- package/src/components/Chip/Chip.css +54 -0
- package/src/components/Chip/Chip.stories.tsx +94 -0
- package/src/components/Chip/Chip.tsx +34 -0
- package/src/components/CookieBanner/CookieBanner.css +19 -0
- package/src/components/CookieBanner/CookieBanner.stories.tsx +52 -0
- package/src/components/CookieBanner/CookieBanner.tsx +55 -0
- package/src/components/CopyButton/CopyButton.stories.tsx +28 -0
- package/src/components/CopyButton/CopyButton.tsx +65 -0
- package/src/components/DateInput/DateInput.css +13 -0
- package/src/components/DateInput/DateInput.stories.tsx +131 -0
- package/src/components/DateInput/DateInput.test.tsx +21 -0
- package/src/components/DateInput/DateInput.tsx +98 -0
- package/src/components/Details/Details.css +28 -0
- package/src/components/Details/Details.stories.tsx +48 -0
- package/src/components/Details/Details.tsx +41 -0
- package/src/components/Dropdown/Dropdown.css +81 -0
- package/src/components/Dropdown/Dropdown.stories.tsx +131 -0
- package/src/components/Dropdown/Dropdown.tsx +69 -0
- package/src/components/ErrorBoundary/ErrorBoundary.stories.tsx +4 -39
- package/src/components/ErrorBoundary/ErrorBoundary.tsx +7 -4
- package/src/components/ErrorBoundary/ErrorFallback.tsx +32 -66
- package/src/components/ErrorModal/ErrorModal.css +32 -0
- package/src/components/ErrorModal/ErrorModal.stories.tsx +101 -0
- package/src/components/ErrorModal/ErrorModal.tsx +120 -0
- package/src/components/ErrorText/ErrorText.css +22 -0
- package/src/components/ErrorText/ErrorText.stories.tsx +42 -27
- package/src/components/ErrorText/ErrorText.tsx +10 -8
- package/src/components/ExternalLink/ExternalLink.css +12 -0
- package/src/components/ExternalLink/ExternalLink.stories.tsx +43 -0
- package/src/components/ExternalLink/ExternalLink.tsx +43 -0
- package/src/components/Fieldset/Fieldset.css +103 -0
- package/src/components/Fieldset/Fieldset.stories.tsx +304 -0
- package/src/components/Fieldset/Fieldset.tsx +38 -0
- package/src/components/FileUpload/FileUpload.css +85 -0
- package/src/components/FileUpload/FileUpload.stories.tsx +106 -0
- package/src/components/FileUpload/FileUpload.tsx +99 -0
- package/src/components/{layout/footer → Footer}/Copyright.tsx +4 -2
- package/src/components/Footer/Footer.stories.tsx +24 -0
- package/src/components/Footer/Footer.tsx +41 -0
- package/src/components/Footer/Licence.tsx +18 -0
- package/src/components/{layout/footer → Footer}/MetaLinks.tsx +8 -3
- package/src/components/Footer/Small/SmallFooter.tsx +43 -42
- package/src/components/Form/FormGroup.css +18 -0
- package/src/components/Form/FormGroup.stories.tsx +87 -0
- package/src/components/Form/FormGroup.tsx +16 -0
- package/src/components/{layout/header → Header}/Header.stories.tsx +26 -12
- package/src/components/Header/Header.test.tsx +87 -0
- package/src/components/Header/Header.tsx +303 -0
- package/src/components/Header/HeaderAppContent.tsx +120 -0
- package/src/components/Heading/Heading.css +40 -0
- package/src/components/Heading/Heading.stories.tsx +15 -5
- package/src/components/Heading/Heading.tsx +21 -44
- package/src/components/Heading/HeadingSkeleton.stories.tsx +30 -0
- package/src/components/Heading/HeadingSkeleton.tsx +14 -0
- package/src/components/Hero/Hero.css +19 -0
- package/src/components/Hero/Hero.stories.tsx +41 -0
- package/src/components/Hero/Hero.tsx +14 -0
- package/src/components/HintText/HintText.css +5 -0
- package/src/components/HintText/HintText.stories.tsx +29 -0
- package/src/components/HintText/HintText.tsx +13 -0
- package/src/components/InfoBox/InfoBox.css +38 -0
- package/src/components/InfoBox/InfoBox.stories.tsx +51 -403
- package/src/components/InfoBox/InfoBox.tsx +31 -158
- package/src/components/Input/Input.css +77 -0
- package/src/components/Input/Input.stories.tsx +123 -0
- package/src/components/Input/Input.tsx +74 -0
- package/src/components/InputGroup/InputGroup.css +140 -0
- package/src/components/InputGroup/InputGroup.stories.tsx +125 -0
- package/src/components/InputGroup/InputGroup.tsx +47 -0
- package/src/components/InsetText/InsetText.css +5 -0
- package/src/components/InsetText/InsetText.stories.tsx +30 -0
- package/src/components/InsetText/InsetText.tsx +9 -0
- package/src/components/Keyboard/Keyboard.css +9 -0
- package/src/components/Keyboard/Keyboard.stories.tsx +63 -0
- package/src/components/Keyboard/Keyboard.tsx +30 -0
- package/src/components/Label/Label.css +27 -0
- package/src/components/Label/Label.stories.tsx +40 -0
- package/src/components/Label/Label.tsx +17 -0
- package/src/components/LayerSwitcher/LayerSwitcher.css +59 -0
- package/src/components/LayerSwitcher/LayerSwitcher.stories.tsx +112 -0
- package/src/components/LayerSwitcher/LayerSwitcher.tsx +61 -0
- package/src/components/Layout/401/401.stories.tsx +23 -0
- package/src/components/Layout/401/401.tsx +1 -0
- package/src/components/Layout/403/403.stories.tsx +23 -0
- package/src/components/Layout/403/403.tsx +21 -0
- package/src/components/Layout/404/404.stories.tsx +23 -0
- package/src/components/Layout/404/404.tsx +28 -0
- package/src/components/Layout/500/500.stories.tsx +56 -0
- package/src/components/Layout/500/500.tsx +63 -0
- package/src/components/Layout/ButtonGroup/ButtonGroup.css +52 -0
- package/src/components/Layout/ButtonGroup/ButtonGroup.stories.tsx +156 -0
- package/src/components/Layout/ButtonGroup/ButtonGroup.tsx +50 -0
- package/src/components/Layout/HeadingGroup/HeadingGroup.css +21 -0
- package/src/components/Layout/HeadingGroup/HeadingGroup.stories.tsx +84 -0
- package/src/components/Layout/HeadingGroup/HeadingGroup.tsx +9 -0
- package/src/components/Layout/MainWrapper/MainWrapper.css +12 -0
- package/src/components/Layout/MainWrapper/MainWrapper.stories.tsx +36 -0
- package/src/components/Layout/MainWrapper/MainWrapper.tsx +14 -0
- package/src/components/Layout/PageShell/PageShell.css +42 -0
- package/src/components/Layout/PageShell/PageShell.stories.tsx +108 -0
- package/src/components/Layout/PageShell/PageShell.tsx +31 -0
- package/src/components/Layout/ParagraphGroup/ParagraphGroup.css +5 -0
- package/src/components/Layout/ParagraphGroup/ParagraphGroup.stories.tsx +34 -0
- package/src/components/Layout/ParagraphGroup/ParagraphGroup.tsx +9 -0
- package/src/components/Layout/Stack/Stack.css +29 -0
- package/src/components/Layout/Stack/Stack.stories.tsx +83 -0
- package/src/components/Layout/Stack/Stack.tsx +19 -0
- package/src/components/Layout/WidthContainer/WidthContainer.css +17 -0
- package/src/components/Layout/WidthContainer/WidthContainer.stories.tsx +59 -0
- package/src/components/Layout/WidthContainer/WidthContainer.tsx +13 -0
- package/src/components/Legend/Legend.css +95 -0
- package/src/components/Legend/Legend.stories.tsx +81 -0
- package/src/components/Legend/Legend.tsx +32 -0
- package/src/components/Legend/LegendDot.tsx +15 -0
- package/src/components/Legend/LegendPanel.stories.tsx +101 -0
- package/src/components/Legend/LegendPanel.tsx +68 -0
- package/src/components/Legend/LegendSkeleton.stories.tsx +87 -0
- package/src/components/Legend/LegendSkeleton.tsx +73 -0
- package/src/components/LegendSelect/LegendSelect.css +8 -0
- package/src/components/LegendSelect/LegendSelect.stories.tsx +80 -0
- package/src/components/LegendSelect/LegendSelect.tsx +26 -0
- package/src/components/Link/Link.css +98 -0
- package/src/components/Link/Link.stories.tsx +70 -0
- package/src/components/Link/Link.tsx +52 -0
- package/src/components/LinkButton/LinkButton.css +67 -0
- package/src/components/LinkButton/LinkButton.stories.tsx +90 -42
- package/src/components/LinkButton/LinkButton.tsx +50 -63
- package/src/components/LinkList/LinkList.css +28 -0
- package/src/components/LinkList/LinkList.stories.tsx +46 -0
- package/src/components/LinkList/LinkList.tsx +23 -0
- package/src/components/List/Li.tsx +9 -0
- package/src/components/List/List.css +9 -0
- package/src/components/List/OrderedList.stories.tsx +36 -0
- package/src/components/List/OrderedList.tsx +9 -0
- package/src/components/List/UnorderedList.stories.tsx +36 -0
- package/src/components/List/UnorderedList.tsx +9 -0
- package/src/components/LoadingBox/LoadingBox.css +27 -0
- package/src/components/LoadingBox/LoadingBox.stories.tsx +66 -0
- package/src/components/LoadingBox/LoadingBox.tsx +40 -0
- package/src/components/Modal/Modal.css +114 -0
- package/src/components/Modal/Modal.stories.tsx +41 -160
- package/src/components/Modal/Modal.tsx +132 -75
- package/src/components/NoScriptBanner/NoScriptBanner.css +17 -0
- package/src/components/NoScriptBanner/NoScriptBanner.stories.tsx +46 -0
- package/src/components/NoScriptBanner/NoScriptBanner.tsx +26 -0
- package/src/components/Paragraph/Paragraph.css +22 -0
- package/src/components/Paragraph/Paragraph.stories.tsx +1 -42
- package/src/components/Paragraph/Paragraph.tsx +4 -3
- package/src/components/Paragraph/ParagraphSkeleton.stories.tsx +44 -0
- package/src/components/Paragraph/ParagraphSkeleton.tsx +33 -0
- package/src/components/PasswordInput/PasswordInput.css +3 -0
- package/src/components/PasswordInput/PasswordInput.stories.tsx +47 -0
- package/src/components/PasswordInput/PasswordInput.tsx +45 -0
- package/src/components/PhaseBanner/PhaseBanner.css +12 -0
- package/src/components/PhaseBanner/PhaseBanner.stories.tsx +61 -0
- package/src/components/PhaseBanner/PhaseBanner.tsx +17 -0
- package/src/components/Popover/Popover.css +43 -0
- package/src/components/Popover/Popover.stories.tsx +102 -0
- package/src/components/Popover/Popover.tsx +40 -0
- package/src/components/Radio/Radio.css +69 -0
- package/src/components/Radio/Radio.stories.tsx +169 -0
- package/src/components/Radio/Radio.tsx +56 -0
- package/src/components/Resizable/Resizable.css +86 -0
- package/src/components/Resizable/Resizable.stories.tsx +123 -0
- package/src/components/Resizable/Resizable.tsx +86 -0
- package/src/components/ScrollArea/ScrollArea.css +9 -0
- package/src/components/ScrollArea/ScrollArea.stories.tsx +42 -0
- package/src/components/ScrollArea/ScrollArea.tsx +11 -0
- package/src/components/SearchBox/SearchBox.stories.tsx +88 -0
- package/src/components/SearchBox/SearchBox.tsx +36 -0
- package/src/components/Select/AsyncSelect.stories.tsx +84 -0
- package/src/components/Select/AsyncSelect.tsx +41 -0
- package/src/components/Select/Select.css +194 -0
- package/src/components/Select/Select.stories.tsx +162 -0
- package/src/components/Select/Select.test.tsx +36 -0
- package/src/components/Select/Select.tsx +291 -0
- package/src/components/Select/SelectSkeleton.stories.tsx +51 -0
- package/src/components/Select/SelectSkeleton.tsx +23 -0
- package/src/components/Separator/Separator.css +41 -0
- package/src/components/Separator/Separator.stories.tsx +63 -0
- package/src/components/Separator/Separator.tsx +42 -0
- package/src/components/Sidebar/ResizableSidebar.tsx +118 -0
- package/src/components/Sidebar/Sidebar.css +203 -0
- package/src/components/Sidebar/Sidebar.stories.tsx +438 -0
- package/src/components/Sidebar/Sidebar.tsx +225 -0
- package/src/components/Skeleton/Skeleton.css +16 -0
- package/src/components/Skeleton/Skeleton.stories.tsx +39 -0
- package/src/components/Skeleton/Skeleton.tsx +12 -0
- package/src/components/SkipLink/SkipLink.css +16 -0
- package/src/components/SkipLink/SkipLink.stories.tsx +40 -0
- package/src/components/SkipLink/SkipLink.tsx +15 -0
- package/src/components/Spinner/Spinner.css +30 -0
- package/src/components/Spinner/Spinner.stories.tsx +36 -0
- package/src/components/Spinner/Spinner.tsx +45 -0
- package/src/components/StartButton/StartButton.css +13 -0
- package/src/components/StartButton/StartButton.stories.tsx +68 -0
- package/src/components/StartButton/StartButton.tsx +50 -0
- package/src/components/SubmitButton/SubmitButton.stories.tsx +31 -0
- package/src/components/SubmitButton/SubmitButton.tsx +14 -0
- package/src/components/SummaryList/SummaryList.css +71 -0
- package/src/components/SummaryList/SummaryList.stories.tsx +211 -0
- package/src/components/SummaryList/SummaryList.tsx +41 -0
- package/src/components/SummaryList/SummaryListSkeleton.stories.tsx +67 -0
- package/src/components/SummaryList/SummaryListSkeleton.tsx +40 -0
- package/src/components/Tabs/Tabs.css +84 -0
- package/src/components/Tabs/Tabs.stories.tsx +133 -0
- package/src/components/Tabs/Tabs.tsx +66 -0
- package/src/components/Tag/Tag.css +8 -0
- package/src/components/Tag/Tag.stories.tsx +96 -0
- package/src/components/Tag/Tag.tsx +34 -0
- package/src/components/Textarea/Textarea.css +38 -0
- package/src/components/Textarea/Textarea.stories.tsx +131 -0
- package/src/components/Textarea/Textarea.tsx +50 -0
- package/src/components/TextareaCount/TextareaCount.css +17 -0
- package/src/components/TextareaCount/TextareaCount.stories.tsx +131 -0
- package/src/components/TextareaCount/TextareaCount.tsx +108 -0
- package/src/components/Tooltip/Tooltip.css +43 -0
- package/src/components/Tooltip/Tooltip.stories.tsx +110 -0
- package/src/components/Tooltip/Tooltip.tsx +53 -0
- package/src/components/WarningText/WarningText.css +20 -0
- package/src/components/WarningText/WarningText.stories.tsx +29 -0
- package/src/components/WarningText/WarningText.tsx +17 -0
- package/src/components/index.ts +342 -61
- package/src/errors/errorDisplay.ts +70 -0
- package/src/errors/index.ts +1 -0
- package/src/http/constants.ts +1 -0
- package/src/http/fetch.ts +7 -3
- package/src/map/LayerSwitcherPanel.tsx +2 -173
- package/src/map/LegendShim.tsx +2 -0
- package/src/map/MapComponent.tsx +7 -17
- package/src/map/MapControlsOverlay.tsx +68 -175
- package/src/map/Popup.tsx +71 -24
- package/src/map/geocoder/Geocoder.test.tsx +46 -3
- package/src/map/geocoder/Geocoder.tsx +155 -278
- package/src/map/index.ts +1 -1
- package/src/types/utils.ts +0 -10
- package/src/utils/components.ts +96 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/utils.ts +0 -12
- package/src/assets/images/defra-logo.svg +0 -51
- package/src/assets/images/ea-logo.svg +0 -58
- package/src/assets/images/ogl.svg +0 -1
- package/src/components/Button/Button.test.tsx +0 -53
- package/src/components/ButtonLink/ButtonLink.test.tsx +0 -154
- package/src/components/Card/Card.test.tsx +0 -51
- package/src/components/ErrorBoundary/ErrorBoundary.test.tsx +0 -75
- package/src/components/ErrorBoundary/ErrorFallback.test.tsx +0 -107
- package/src/components/ErrorText/ErrorText.test.tsx +0 -33
- package/src/components/Heading/Heading.test.tsx +0 -22
- package/src/components/Hint/Hint.stories.tsx +0 -58
- package/src/components/Hint/Hint.test.tsx +0 -33
- package/src/components/Hint/Hint.tsx +0 -12
- package/src/components/InfoBox/InfoBox.test.tsx +0 -330
- package/src/components/InfoBox/types.ts +0 -6
- package/src/components/LinkButton/LinkButton.test.tsx +0 -177
- package/src/components/Modal/Modal.test.tsx +0 -246
- package/src/components/NotificationBanner/NotificationBanner.stories.tsx +0 -45
- package/src/components/NotificationBanner/NotificationBanner.test.tsx +0 -58
- package/src/components/NotificationBanner/NotificationBanner.tsx +0 -45
- package/src/components/Paragraph/Paragraph.test.tsx +0 -10
- package/src/components/SlidingPanel/SlidingPanel.stories.tsx +0 -129
- package/src/components/SlidingPanel/SlidingPanel.test.tsx +0 -83
- package/src/components/SlidingPanel/SlidingPanel.tsx +0 -258
- package/src/components/accordion/Accordion.stories.tsx +0 -234
- package/src/components/accordion/Accordion.test.tsx +0 -192
- package/src/components/accordion/Accordion.tsx +0 -73
- package/src/components/backToTop/BackToTop.stories.tsx +0 -407
- package/src/components/backToTop/BackToTop.test.tsx +0 -57
- package/src/components/backToTop/BackToTop.tsx +0 -134
- package/src/components/chip/Chip.stories.tsx +0 -61
- package/src/components/chip/Chip.test.tsx +0 -31
- package/src/components/chip/Chip.tsx +0 -23
- package/src/components/container/Container.tsx +0 -42
- package/src/components/cookieBanner/CookieBanner.stories.tsx +0 -257
- package/src/components/cookieBanner/CookieBanner.test.tsx +0 -68
- package/src/components/cookieBanner/CookieBanner.tsx +0 -73
- package/src/components/divider/RuleDivider.stories.tsx +0 -254
- package/src/components/divider/RuleDivider.test.tsx +0 -164
- package/src/components/divider/RuleDivider.tsx +0 -23
- package/src/components/dropdown/DropdownMenu.test.tsx +0 -211
- package/src/components/dropdown/DropdownMenu.tsx +0 -108
- package/src/components/dropdown/useDropdownMenu.ts +0 -249
- package/src/components/form/Input.stories.tsx +0 -435
- package/src/components/form/Input.test.tsx +0 -206
- package/src/components/form/Input.tsx +0 -23
- package/src/components/form/TextArea.stories.tsx +0 -464
- package/src/components/form/TextArea.test.tsx +0 -232
- package/src/components/form/TextArea.tsx +0 -23
- package/src/components/layout/footer/Footer.tsx +0 -26
- package/src/components/layout/footer/Licence.tsx +0 -19
- package/src/components/layout/header/Header.test.tsx +0 -36
- package/src/components/layout/header/Header.tsx +0 -90
- package/src/components/layout/header/HeaderAuthClient.test.tsx +0 -45
- package/src/components/layout/header/HeaderAuthClient.tsx +0 -40
- package/src/components/layout/header/HeaderNavClient.test.tsx +0 -44
- package/src/components/layout/header/HeaderNavClient.tsx +0 -67
- package/src/components/link/ExternalLink.test.tsx +0 -102
- package/src/components/link/ExternalLink.tsx +0 -29
- package/src/components/link/Link.tsx +0 -26
- package/src/components/select/Select.stories.tsx +0 -336
- package/src/components/select/Select.test.tsx +0 -474
- package/src/components/select/Select.tsx +0 -125
- package/src/components/select/SelectSkeleton.stories.tsx +0 -194
- package/src/components/select/SelectSkeleton.test.tsx +0 -104
- package/src/components/select/SelectSkeleton.tsx +0 -23
- package/src/components/select/common.ts +0 -3
- package/src/components/select/index.ts +0 -18
- package/src/components/skipLink/SkipLink.stories.tsx +0 -346
- package/src/components/skipLink/SkipLink.test.tsx +0 -22
- package/src/components/skipLink/SkipLink.tsx +0 -49
- package/src/map/LegendPanel.tsx +0 -85
- /package/src/{components → assets}/images/DefraLogo.tsx +0 -0
- /package/src/{components → assets}/images/EaLogo.tsx +0 -0
- /package/src/{components → assets}/images/OglLogo.tsx +0 -0
- /package/src/components/{googleAnalytics → GoogleAnalytics}/GlobalVars.tsx +0 -0
- /package/src/components/{googleAnalytics → GoogleAnalytics}/GoogleAnalytics.tsx +0 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ComponentProps } from 'react';
|
|
4
|
+
|
|
5
|
+
import { Collapsible as CollapsiblePrimitive } from 'radix-ui';
|
|
6
|
+
import { LuChevronRight } from 'react-icons/lu';
|
|
7
|
+
|
|
8
|
+
import { cn } from '../../utils';
|
|
9
|
+
|
|
10
|
+
export type DetailsProps = ComponentProps<typeof CollapsiblePrimitive.Root>;
|
|
11
|
+
|
|
12
|
+
export const Details = ({ className, ...props }: DetailsProps) => (
|
|
13
|
+
<CollapsiblePrimitive.Root
|
|
14
|
+
data-slot="details"
|
|
15
|
+
className={cn('cn-details', className)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export type DetailsTriggerProps = ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>;
|
|
21
|
+
|
|
22
|
+
export const DetailsTrigger = ({ className, children, ...props }: DetailsTriggerProps) => (
|
|
23
|
+
<CollapsiblePrimitive.CollapsibleTrigger
|
|
24
|
+
data-slot="details-trigger"
|
|
25
|
+
className={cn('cn-details-trigger', className)}
|
|
26
|
+
{...props}
|
|
27
|
+
>
|
|
28
|
+
<LuChevronRight aria-hidden className="cn-details-trigger-icon" />
|
|
29
|
+
{children}
|
|
30
|
+
</CollapsiblePrimitive.CollapsibleTrigger>
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
export type DetailsContentProps = ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>;
|
|
34
|
+
|
|
35
|
+
export const DetailsContent = ({ className, ...props }: DetailsContentProps) => (
|
|
36
|
+
<CollapsiblePrimitive.CollapsibleContent
|
|
37
|
+
data-slot="details-content"
|
|
38
|
+
className={cn('cn-details-content', className)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
/* MARK: Content
|
|
3
|
+
Same panel treatment as Popover/Select's own menu (bg-panel + border),
|
|
4
|
+
portal-rendered so DOM order is always safe above page content, but
|
|
5
|
+
cn-z-overlay still matters against other elements with their own high
|
|
6
|
+
z-index (e.g. a resizable handle's cn-z-top). */
|
|
7
|
+
.cn-dropdown-menu-content {
|
|
8
|
+
@apply panel cn-z-overlay min-w-40 overflow-hidden;
|
|
9
|
+
|
|
10
|
+
&[data-state='open'] {
|
|
11
|
+
animation: cn-dropdown-menu-in 0.1s ease-out;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
&[data-state='closed'] {
|
|
15
|
+
animation: cn-dropdown-menu-out 0.075s ease-in;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@keyframes cn-dropdown-menu-in {
|
|
20
|
+
from {
|
|
21
|
+
opacity: 0;
|
|
22
|
+
transform: scale(0.97);
|
|
23
|
+
}
|
|
24
|
+
to {
|
|
25
|
+
opacity: 1;
|
|
26
|
+
transform: scale(1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@keyframes cn-dropdown-menu-out {
|
|
31
|
+
from {
|
|
32
|
+
opacity: 1;
|
|
33
|
+
}
|
|
34
|
+
to {
|
|
35
|
+
opacity: 0;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* MARK: Item
|
|
40
|
+
data-highlighted is Radix's own state for whichever item is currently
|
|
41
|
+
hovered or keyboard-focused (it also moves real DOM focus there when
|
|
42
|
+
keyboard-navigated, which is what lets a real Link's own :focus/:hover
|
|
43
|
+
apply at all, asChild renders this class directly onto whatever's
|
|
44
|
+
passed as a child, e.g. HeaderAppContentNavLink, there's no wrapper,
|
|
45
|
+
so a Link IS this element). Every item gets the same washed background
|
|
46
|
+
on highlight regardless of content, same as cn-select-option-focused,
|
|
47
|
+
so the menu reads consistently row to row.
|
|
48
|
+
|
|
49
|
+
border-none/shadow-none on top of that specifically neutralises a
|
|
50
|
+
Link's own :focus box-shadow (the GDS yellow-bar treatment), which
|
|
51
|
+
would otherwise show alongside the washed background rather than the
|
|
52
|
+
two picking one look. A highlighted link still reads as a link
|
|
53
|
+
though: :is(a) applies cn-link-hover-decoration, the same
|
|
54
|
+
thicker-underline Link.css itself uses on :hover, layered on top of
|
|
55
|
+
the shared background rather than replacing it. */
|
|
56
|
+
.cn-dropdown-menu-item {
|
|
57
|
+
@apply block w-full cursor-default px-gds-2 text-left text-base py-gds-1;
|
|
58
|
+
|
|
59
|
+
&:hover,
|
|
60
|
+
&:focus {
|
|
61
|
+
@apply outline-none;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
&[data-highlighted] {
|
|
65
|
+
@apply bg-wash border-none shadow-none;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
&[data-highlighted]:is(a) {
|
|
69
|
+
@apply cn-link-hover-decoration;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
&[data-disabled] {
|
|
73
|
+
@apply cursor-not-allowed opacity-50;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* MARK: Separator */
|
|
78
|
+
.cn-dropdown-menu-separator {
|
|
79
|
+
@apply my-gds-1 h-px bg-divider;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DropdownMenu,
|
|
5
|
+
DropdownMenuContent,
|
|
6
|
+
DropdownMenuItem,
|
|
7
|
+
DropdownMenuSeparator,
|
|
8
|
+
DropdownMenuTrigger,
|
|
9
|
+
} from './Dropdown';
|
|
10
|
+
import { Button } from '../Button/Button';
|
|
11
|
+
import { ExternalLink } from '../ExternalLink/ExternalLink';
|
|
12
|
+
import { Link } from '../Link/Link';
|
|
13
|
+
|
|
14
|
+
const meta: Meta<typeof DropdownMenu> = {
|
|
15
|
+
title: 'Components/Dropdown',
|
|
16
|
+
component: DropdownMenu,
|
|
17
|
+
tags: ['autodocs'],
|
|
18
|
+
parameters: {
|
|
19
|
+
docs: {
|
|
20
|
+
description: {
|
|
21
|
+
component:
|
|
22
|
+
"A floating menu of actions or links anchored to a trigger. Built for a flat list of nav links, not a full command menu, no checkbox/radio items, submenus, or keyboard shortcuts. Use DropdownMenuItem with asChild and a real Link (this library's own, a plain `<a>`, or a framework-aware wrapper like one using asChild internally with next/link). Every item, Link or not, gets the same washed background on highlight; a highlighted Link also keeps its own thicker-underline hover cue on top of that, its focus box-shadow is neutralised so the two don't compete. Whatever component you pass needs to forward extra props through to its own rendered element (via a rest spread), asChild relies on that to merge role=\"menuitem\" and keyboard handling onto it, see HeaderAppContent's own HeaderAppContentNavLink for a real example.",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default meta;
|
|
29
|
+
|
|
30
|
+
type Story = StoryObj<typeof DropdownMenu>;
|
|
31
|
+
|
|
32
|
+
export const Default: Story = {
|
|
33
|
+
parameters: {
|
|
34
|
+
docs: {
|
|
35
|
+
description: {
|
|
36
|
+
story:
|
|
37
|
+
"Using this library's own Link/ExternalLink with asChild, appearance=\"contrast\" for plain body-coloured text on the menu's white background. Highlighting an item (hover or arrow-key nav) shows the same washed background as any other item, plus the link's own thicker-underline hover cue.",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
render: () => (
|
|
42
|
+
<div className="flex min-h-40 items-start justify-center">
|
|
43
|
+
<DropdownMenu>
|
|
44
|
+
<DropdownMenuTrigger asChild>
|
|
45
|
+
<Button appearance="secondary">Account</Button>
|
|
46
|
+
</DropdownMenuTrigger>
|
|
47
|
+
|
|
48
|
+
<DropdownMenuContent>
|
|
49
|
+
<DropdownMenuItem asChild>
|
|
50
|
+
<ExternalLink variant="icon" href="#profile" appearance="contrast">
|
|
51
|
+
Profile
|
|
52
|
+
</ExternalLink>
|
|
53
|
+
</DropdownMenuItem>
|
|
54
|
+
|
|
55
|
+
<DropdownMenuItem asChild>
|
|
56
|
+
<ExternalLink variant="icon" href="#settings" appearance="contrast">
|
|
57
|
+
Settings
|
|
58
|
+
</ExternalLink>
|
|
59
|
+
</DropdownMenuItem>
|
|
60
|
+
|
|
61
|
+
<DropdownMenuItem asChild>
|
|
62
|
+
<Link href="#sign-out" appearance="contrast">
|
|
63
|
+
Sign out
|
|
64
|
+
</Link>
|
|
65
|
+
</DropdownMenuItem>
|
|
66
|
+
</DropdownMenuContent>
|
|
67
|
+
</DropdownMenu>
|
|
68
|
+
</div>
|
|
69
|
+
),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const WithSeparator: Story = {
|
|
73
|
+
parameters: {
|
|
74
|
+
docs: {
|
|
75
|
+
description: {
|
|
76
|
+
story: 'DropdownMenuSeparator splits related links from an unrelated one, e.g. sign out.',
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
render: () => (
|
|
81
|
+
<div className="flex min-h-40 items-start justify-center">
|
|
82
|
+
<DropdownMenu>
|
|
83
|
+
<DropdownMenuTrigger asChild>
|
|
84
|
+
<Button appearance="secondary">Account</Button>
|
|
85
|
+
</DropdownMenuTrigger>
|
|
86
|
+
|
|
87
|
+
<DropdownMenuContent>
|
|
88
|
+
<DropdownMenuItem asChild>
|
|
89
|
+
<ExternalLink variant="icon" href="#profile" appearance="contrast">
|
|
90
|
+
Profile
|
|
91
|
+
</ExternalLink>
|
|
92
|
+
</DropdownMenuItem>
|
|
93
|
+
|
|
94
|
+
<DropdownMenuItem asChild>
|
|
95
|
+
<ExternalLink variant="icon" href="#settings" appearance="contrast">
|
|
96
|
+
Settings
|
|
97
|
+
</ExternalLink>
|
|
98
|
+
</DropdownMenuItem>
|
|
99
|
+
|
|
100
|
+
<DropdownMenuSeparator />
|
|
101
|
+
|
|
102
|
+
<DropdownMenuItem asChild>
|
|
103
|
+
<Link href="#sign-out" appearance="contrast">
|
|
104
|
+
Sign out
|
|
105
|
+
</Link>
|
|
106
|
+
</DropdownMenuItem>
|
|
107
|
+
</DropdownMenuContent>
|
|
108
|
+
</DropdownMenu>
|
|
109
|
+
</div>
|
|
110
|
+
),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const WithDisabledItem: Story = {
|
|
114
|
+
render: () => (
|
|
115
|
+
<div className="flex min-h-40 items-start justify-center">
|
|
116
|
+
<DropdownMenu>
|
|
117
|
+
<DropdownMenuTrigger asChild>
|
|
118
|
+
<Button appearance="secondary">Sort</Button>
|
|
119
|
+
</DropdownMenuTrigger>
|
|
120
|
+
|
|
121
|
+
<DropdownMenuContent>
|
|
122
|
+
<DropdownMenuItem>Date</DropdownMenuItem>
|
|
123
|
+
|
|
124
|
+
<DropdownMenuItem disabled>Name (unavailable)</DropdownMenuItem>
|
|
125
|
+
|
|
126
|
+
<DropdownMenuItem>Type</DropdownMenuItem>
|
|
127
|
+
</DropdownMenuContent>
|
|
128
|
+
</DropdownMenu>
|
|
129
|
+
</div>
|
|
130
|
+
),
|
|
131
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ComponentProps } from 'react';
|
|
4
|
+
|
|
5
|
+
import { DropdownMenu as DropdownMenuPrimitive } from 'radix-ui';
|
|
6
|
+
|
|
7
|
+
import { cn } from '../../utils/components';
|
|
8
|
+
|
|
9
|
+
export const DropdownMenu = ({ ...props }: ComponentProps<typeof DropdownMenuPrimitive.Root>) => (
|
|
10
|
+
<DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
export const DropdownMenuTrigger = ({
|
|
14
|
+
...props
|
|
15
|
+
}: ComponentProps<typeof DropdownMenuPrimitive.Trigger>) => (
|
|
16
|
+
<DropdownMenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export const DropdownMenuContent = ({
|
|
20
|
+
className,
|
|
21
|
+
align = 'start',
|
|
22
|
+
sideOffset = 6,
|
|
23
|
+
...props
|
|
24
|
+
}: ComponentProps<typeof DropdownMenuPrimitive.Content>) => (
|
|
25
|
+
<DropdownMenuPrimitive.Portal>
|
|
26
|
+
<DropdownMenuPrimitive.Content
|
|
27
|
+
data-slot="dropdown-menu-content"
|
|
28
|
+
align={align}
|
|
29
|
+
sideOffset={sideOffset}
|
|
30
|
+
className={cn('cn-dropdown-menu-content', className)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
</DropdownMenuPrimitive.Portal>
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// asChild + a real Link for nav links (this library's own, or a
|
|
37
|
+
// framework-aware wrapper e.g. one using asChild internally with next/link).
|
|
38
|
+
// cn-dropdown-menu-item gives every item (Link or not) the same washed
|
|
39
|
+
// background on highlight, same as Select's own menu, rather than leaving
|
|
40
|
+
// a Link's own :focus box-shadow to fight that for the same row (which it
|
|
41
|
+
// specifically neutralises), a highlighted Link still gets its own cue on
|
|
42
|
+
// top, the same thicker-underline Link.css itself uses on :hover, see
|
|
43
|
+
// Dropdown.css for the exact rules. Whatever's passed here needs to
|
|
44
|
+
// forward arbitrary extra props through to its own rendered element
|
|
45
|
+
// (asChild relies on that to merge role="menuitem", the roving-tabindex
|
|
46
|
+
// keyboard handlers, and this className onto it), a component that only
|
|
47
|
+
// destructures its own known props without a rest spread will silently
|
|
48
|
+
// drop all of that.
|
|
49
|
+
export const DropdownMenuItem = ({
|
|
50
|
+
className,
|
|
51
|
+
...props
|
|
52
|
+
}: ComponentProps<typeof DropdownMenuPrimitive.Item>) => (
|
|
53
|
+
<DropdownMenuPrimitive.Item
|
|
54
|
+
data-slot="dropdown-menu-item"
|
|
55
|
+
className={cn('cn-dropdown-menu-item', className)}
|
|
56
|
+
{...props}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
export const DropdownMenuSeparator = ({
|
|
61
|
+
className,
|
|
62
|
+
...props
|
|
63
|
+
}: ComponentProps<typeof DropdownMenuPrimitive.Separator>) => (
|
|
64
|
+
<DropdownMenuPrimitive.Separator
|
|
65
|
+
data-slot="dropdown-menu-separator"
|
|
66
|
+
className={cn('cn-dropdown-menu-separator', className)}
|
|
67
|
+
{...props}
|
|
68
|
+
/>
|
|
69
|
+
);
|
|
@@ -1,33 +1,17 @@
|
|
|
1
|
-
import type { FallbackProps } from 'react-error-boundary';
|
|
2
|
-
|
|
3
1
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
4
2
|
|
|
5
3
|
import { ErrorBoundary } from './ErrorBoundary';
|
|
4
|
+
import { Paragraph } from '../Paragraph/Paragraph';
|
|
6
5
|
|
|
7
6
|
// Component that intentionally throws an error
|
|
8
7
|
const Bomb = () => {
|
|
9
8
|
throw new Error('Boom!');
|
|
10
9
|
};
|
|
11
10
|
|
|
12
|
-
// Simple custom fallback for demonstration
|
|
13
|
-
const CustomFallback = ({ error, resetErrorBoundary }: FallbackProps) => (
|
|
14
|
-
<div role="alert" className="p-4 border border-red-300 rounded-lg bg-red-50">
|
|
15
|
-
<p className="font-medium text-red-700">Custom Fallback:</p>
|
|
16
|
-
|
|
17
|
-
<p className="text-red-600">{error instanceof Error ? error.message : 'Unknown'}</p>
|
|
18
|
-
|
|
19
|
-
<button
|
|
20
|
-
className="px-3 py-1 mt-2 text-white bg-red-600 rounded hover:bg-red-700"
|
|
21
|
-
onClick={resetErrorBoundary}
|
|
22
|
-
>
|
|
23
|
-
Try Again
|
|
24
|
-
</button>
|
|
25
|
-
</div>
|
|
26
|
-
);
|
|
27
|
-
|
|
28
11
|
const meta = {
|
|
29
12
|
title: 'Components/ErrorBoundary',
|
|
30
13
|
component: ErrorBoundary,
|
|
14
|
+
tags: ['autodocs'],
|
|
31
15
|
parameters: {
|
|
32
16
|
layout: 'centered',
|
|
33
17
|
docs: {
|
|
@@ -37,7 +21,6 @@ const meta = {
|
|
|
37
21
|
},
|
|
38
22
|
},
|
|
39
23
|
},
|
|
40
|
-
tags: ['autodocs'],
|
|
41
24
|
} satisfies Meta<typeof ErrorBoundary>;
|
|
42
25
|
|
|
43
26
|
export default meta;
|
|
@@ -45,16 +28,12 @@ type Story = StoryObj<typeof meta>;
|
|
|
45
28
|
|
|
46
29
|
export const Default: Story = {
|
|
47
30
|
args: {
|
|
48
|
-
children:
|
|
49
|
-
<div className="p-4 text-green-800 border rounded-md bg-green-50">
|
|
50
|
-
<p>No errors here — everything renders normally.</p>
|
|
51
|
-
</div>
|
|
52
|
-
),
|
|
31
|
+
children: <Paragraph>No errors here — everything renders normally.</Paragraph>,
|
|
53
32
|
},
|
|
54
33
|
parameters: {
|
|
55
34
|
docs: {
|
|
56
35
|
description: {
|
|
57
|
-
story: 'Default behavior
|
|
36
|
+
story: 'Default behavior. Children render normally when no error is thrown.',
|
|
58
37
|
},
|
|
59
38
|
},
|
|
60
39
|
},
|
|
@@ -72,17 +51,3 @@ export const WithError: Story = {
|
|
|
72
51
|
},
|
|
73
52
|
},
|
|
74
53
|
};
|
|
75
|
-
|
|
76
|
-
export const WithCustomFallback: Story = {
|
|
77
|
-
args: {
|
|
78
|
-
children: <Bomb />,
|
|
79
|
-
fallback: CustomFallback,
|
|
80
|
-
},
|
|
81
|
-
parameters: {
|
|
82
|
-
docs: {
|
|
83
|
-
description: {
|
|
84
|
-
story: 'ErrorBoundary can render a custom fallback component when errors occur.',
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
};
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ComponentProps, ComponentType, ErrorInfo } from 'react';
|
|
2
4
|
|
|
3
5
|
import { ErrorBoundary as ReactErrorBoundary, type FallbackProps } from 'react-error-boundary';
|
|
4
6
|
|
|
5
7
|
import { ErrorFallback } from './ErrorFallback';
|
|
6
8
|
|
|
7
|
-
type ErrorBoundaryProps =
|
|
8
|
-
|
|
9
|
+
export type ErrorBoundaryProps = Omit<
|
|
10
|
+
ComponentProps<typeof ReactErrorBoundary>,
|
|
11
|
+
'onError' | 'FallbackComponent'
|
|
12
|
+
> & {
|
|
9
13
|
fallback?: ComponentType<FallbackProps>;
|
|
10
|
-
onReset?: () => void;
|
|
11
14
|
};
|
|
12
15
|
|
|
13
16
|
export const ErrorBoundary = ({ children, fallback, onReset }: ErrorBoundaryProps) => {
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useId } from 'react';
|
|
3
|
+
import { useId, type ComponentProps } from 'react';
|
|
4
4
|
|
|
5
5
|
import { type FallbackProps } from 'react-error-boundary';
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import type { ExtendProps } from '../../types';
|
|
7
|
+
import { formatErrorForClipboard, getErrorDisplayInfo } from '../../errors/errorDisplay';
|
|
9
8
|
import { cn } from '../../utils';
|
|
10
|
-
import { Accordion } from '../accordion/Accordion';
|
|
11
9
|
import { Button } from '../Button/Button';
|
|
12
|
-
import {
|
|
10
|
+
import { CopyButton } from '../CopyButton/CopyButton';
|
|
11
|
+
import { ErrorText } from '../ErrorText/ErrorText';
|
|
12
|
+
import { HintText } from '../HintText/HintText';
|
|
13
|
+
import { InfoBox } from '../InfoBox/InfoBox';
|
|
14
|
+
import { ButtonGroup } from '../Layout/ButtonGroup/ButtonGroup';
|
|
13
15
|
import { Paragraph } from '../Paragraph/Paragraph';
|
|
16
|
+
import { ScrollArea } from '../ScrollArea/ScrollArea';
|
|
14
17
|
|
|
15
|
-
export type ErrorFallbackProps =
|
|
18
|
+
export type ErrorFallbackProps = ComponentProps<'div'> & FallbackProps;
|
|
16
19
|
|
|
17
20
|
export const ErrorFallback = ({
|
|
18
21
|
resetErrorBoundary,
|
|
@@ -22,78 +25,41 @@ export const ErrorFallback = ({
|
|
|
22
25
|
}: ErrorFallbackProps) => {
|
|
23
26
|
const id = useId();
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
let digest = undefined;
|
|
28
|
-
|
|
29
|
-
if (error instanceof ApiError) {
|
|
30
|
-
message = error.message;
|
|
31
|
-
details = error.details;
|
|
32
|
-
digest = error.digest;
|
|
33
|
-
} else if (error instanceof Error) {
|
|
34
|
-
message = error.message;
|
|
35
|
-
details = error.cause?.toString();
|
|
36
|
-
} else {
|
|
37
|
-
message = 'Unknown error';
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const copyToClipboard = () => {
|
|
41
|
-
navigator.clipboard.writeText(
|
|
42
|
-
// eslint-disable-next-line sonarjs/no-nested-template-literals
|
|
43
|
-
`App Error: ${message}, ${details ? `Details: ${details}, ` : ''}${digest ? `Digest: ${digest}` : ''}`,
|
|
44
|
-
);
|
|
45
|
-
};
|
|
28
|
+
const info = getErrorDisplayInfo(error);
|
|
29
|
+
const clipboardContent = formatErrorForClipboard(info);
|
|
46
30
|
|
|
47
31
|
return (
|
|
48
32
|
<div
|
|
49
33
|
role="alert"
|
|
50
34
|
aria-labelledby={id}
|
|
51
|
-
className={cn(
|
|
52
|
-
'grid gap-2 border-form border-transparent border-l-error max-w-full pl-2',
|
|
53
|
-
className,
|
|
54
|
-
)}
|
|
55
|
-
style={{ gridTemplateColumns: '1fr auto' }}
|
|
35
|
+
className={cn('border-l-standard pl-gds-2 border-error', className)}
|
|
56
36
|
{...props}
|
|
57
37
|
>
|
|
58
|
-
<div className="flex
|
|
59
|
-
<
|
|
60
|
-
Something went wrong
|
|
61
|
-
</Heading>
|
|
38
|
+
<div className="flex items-center gap-gds-1">
|
|
39
|
+
<ErrorText id={id}>Something went wrong</ErrorText>
|
|
62
40
|
|
|
63
|
-
<
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
41
|
+
<InfoBox>
|
|
42
|
+
<div className="flex flex-col gap-gds-2">
|
|
43
|
+
<ScrollArea className="max-h-32 overflow-y-auto overflow-x-hidden">
|
|
44
|
+
<Paragraph className="leading-snug pb-0">
|
|
45
|
+
<strong>Reason:</strong> {info.message}
|
|
46
|
+
</Paragraph>
|
|
47
|
+
</ScrollArea>
|
|
67
48
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
</Button>
|
|
49
|
+
<div className="flex flex-col gap-gds-1">
|
|
50
|
+
<ButtonGroup>
|
|
51
|
+
<CopyButton appearance="secondary" content={clipboardContent} />
|
|
72
52
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
</Button>
|
|
76
|
-
</div>
|
|
53
|
+
<Button onClick={resetErrorBoundary}>Retry</Button>
|
|
54
|
+
</ButtonGroup>
|
|
77
55
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
<div className="flex gap-0.5 flex-col">
|
|
82
|
-
{digest && (
|
|
83
|
-
<Paragraph className="leading-snug pb-0">
|
|
84
|
-
<strong>Error ID:</strong> {digest}
|
|
85
|
-
</Paragraph>
|
|
86
|
-
)}
|
|
87
|
-
<pre
|
|
88
|
-
className="text-base bg-white border border-gray-200 p-2 overflow-y-auto max-h-32
|
|
89
|
-
whitespace-pre-wrap"
|
|
90
|
-
>
|
|
91
|
-
{details}
|
|
92
|
-
</pre>
|
|
56
|
+
<HintText className="text-sm">
|
|
57
|
+
Copy to share error details with the app administrator.
|
|
58
|
+
</HintText>
|
|
93
59
|
</div>
|
|
94
|
-
</
|
|
95
|
-
</
|
|
96
|
-
|
|
60
|
+
</div>
|
|
61
|
+
</InfoBox>
|
|
62
|
+
</div>
|
|
97
63
|
</div>
|
|
98
64
|
);
|
|
99
65
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
/* MARK: Error modal
|
|
3
|
+
Row layout: icon column beside a content column. cn-modal-content stays
|
|
4
|
+
a plain block by default (see Modal.css), so it's safe to turn into a
|
|
5
|
+
row here without fighting a hardcoded flex-direction. */
|
|
6
|
+
.cn-error-modal {
|
|
7
|
+
@apply flex flex-row gap-gds-3;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* items-start keeps the icon pinned to the top of the row rather than
|
|
11
|
+
stretching/centering across the full modal height (including the
|
|
12
|
+
footer), justify-center centres it within its own column. */
|
|
13
|
+
.cn-error-modal-icon {
|
|
14
|
+
@apply flex shrink-0 items-start justify-center text-error;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* min-w-0 lets this column shrink below its content's natural width
|
|
18
|
+
within the flex row, same reasoning as elsewhere in this project:
|
|
19
|
+
without it, long unbroken content (e.g. a digest UUID) could force the
|
|
20
|
+
row wider than the modal itself instead of wrapping/scrolling. */
|
|
21
|
+
.cn-error-modal-body {
|
|
22
|
+
@apply flex min-w-0 flex-col;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* Scoped to the summary list only (not the copy button/hint below it), an
|
|
26
|
+
arbitrary-length details string (e.g. a dumped API error body) or a long
|
|
27
|
+
digest can otherwise push the whole modal to an unreasonable height,
|
|
28
|
+
rather than just scrolling internally. */
|
|
29
|
+
.cn-error-modal-details {
|
|
30
|
+
@apply max-h-60 overflow-y-auto;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
4
|
+
|
|
5
|
+
import { ErrorModal } from './ErrorModal';
|
|
6
|
+
import { ApiError } from '../../errors/ApiError';
|
|
7
|
+
import { Button } from '../Button/Button';
|
|
8
|
+
import { Paragraph } from '../Paragraph/Paragraph';
|
|
9
|
+
|
|
10
|
+
const meta: Meta<typeof ErrorModal> = {
|
|
11
|
+
title: 'Components/ErrorModal',
|
|
12
|
+
component: ErrorModal,
|
|
13
|
+
tags: ['autodocs'],
|
|
14
|
+
parameters: {
|
|
15
|
+
docs: {
|
|
16
|
+
description: {
|
|
17
|
+
component:
|
|
18
|
+
'A Modal preset for reporting an error: an alert icon beside the message, with an optional expandable "Technical information" section (message, details, digest, and a copy-to-clipboard button that includes the stack trace when available) driven entirely by the `error` prop. Shares its error-describing logic with ErrorFallback via `getErrorDisplayInfo`/`formatErrorForClipboard`, so both surfaces describe the same error the same way.',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
argTypes: {
|
|
23
|
+
children: { control: false },
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default meta;
|
|
28
|
+
|
|
29
|
+
type Story = StoryObj<typeof ErrorModal>;
|
|
30
|
+
|
|
31
|
+
export const Default: Story = {
|
|
32
|
+
render: () => {
|
|
33
|
+
const [open, setOpen] = useState(false);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<>
|
|
37
|
+
<Button onClick={() => setOpen(true)}>Trigger error</Button>
|
|
38
|
+
|
|
39
|
+
<ErrorModal
|
|
40
|
+
open={open}
|
|
41
|
+
onOpenChange={setOpen}
|
|
42
|
+
error={new Error('Failed to fetch sampling points')}
|
|
43
|
+
>
|
|
44
|
+
<Paragraph>An error occurred while fetching sampling points.</Paragraph>
|
|
45
|
+
</ErrorModal>
|
|
46
|
+
</>
|
|
47
|
+
);
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const WithApiError: Story = {
|
|
52
|
+
parameters: {
|
|
53
|
+
docs: {
|
|
54
|
+
description: {
|
|
55
|
+
story:
|
|
56
|
+
'ApiError additionally carries `details` and a `digest`, both shown in the technical information section and included in the copied text.',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
render: () => {
|
|
61
|
+
const [open, setOpen] = useState(false);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<>
|
|
65
|
+
<Button onClick={() => setOpen(true)}>Trigger error</Button>
|
|
66
|
+
|
|
67
|
+
<ErrorModal
|
|
68
|
+
open={open}
|
|
69
|
+
onOpenChange={setOpen}
|
|
70
|
+
error={ApiError.internalServerError('The upstream sampling points service timed out')}
|
|
71
|
+
>
|
|
72
|
+
<Paragraph>An error occurred while fetching sampling points.</Paragraph>
|
|
73
|
+
</ErrorModal>
|
|
74
|
+
</>
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const WithoutError: Story = {
|
|
80
|
+
parameters: {
|
|
81
|
+
docs: {
|
|
82
|
+
description: {
|
|
83
|
+
story:
|
|
84
|
+
'No `error` prop, no technical information section is rendered, just the message and a close button.',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
render: () => {
|
|
89
|
+
const [open, setOpen] = useState(false);
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<>
|
|
93
|
+
<Button onClick={() => setOpen(true)}>Trigger error</Button>
|
|
94
|
+
|
|
95
|
+
<ErrorModal open={open} onOpenChange={setOpen}>
|
|
96
|
+
<Paragraph>Something went wrong. Try again in a few minutes.</Paragraph>
|
|
97
|
+
</ErrorModal>
|
|
98
|
+
</>
|
|
99
|
+
);
|
|
100
|
+
},
|
|
101
|
+
};
|