@tpzdsp/next-toolkit 2.5.1 → 3.1.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 +21 -12
- 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 +34 -0
- package/src/components/ErrorModal/ErrorModal.stories.tsx +100 -0
- package/src/components/ErrorModal/ErrorModal.tsx +123 -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 +3 -2
- package/src/components/Footer/Footer.stories.tsx +27 -0
- package/src/components/Footer/Footer.tsx +46 -0
- package/src/components/Footer/Licence.tsx +18 -0
- package/src/components/Footer/MetaLinks.tsx +47 -0
- package/src/components/Footer/Small/SmallFooter.stories.tsx +3 -0
- package/src/components/Footer/Small/SmallFooter.tsx +53 -41
- 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 +304 -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 +111 -0
- package/src/components/Modal/Modal.stories.tsx +94 -155
- package/src/components/Modal/Modal.tsx +141 -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 +202 -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 +296 -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 +343 -61
- package/src/errors/errorDisplay.ts +70 -0
- package/src/errors/index.ts +1 -0
- package/src/http/constants.ts +2 -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/footer/MetaLinks.tsx +0 -36
- 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,15 @@
|
|
|
1
|
+
import { cn } from '../../utils/components';
|
|
2
|
+
|
|
3
|
+
export type LegendDotProps = {
|
|
4
|
+
color: string;
|
|
5
|
+
className?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const LegendDot = ({ color, className }: LegendDotProps) => (
|
|
9
|
+
<span
|
|
10
|
+
data-slot="legend-dot"
|
|
11
|
+
className={cn('cn-legend-dot', className)}
|
|
12
|
+
style={{ backgroundColor: color }}
|
|
13
|
+
aria-hidden
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
4
|
+
|
|
5
|
+
import { LegendPanel } from './LegendPanel';
|
|
6
|
+
import type { LegendOption } from './LegendPanel';
|
|
7
|
+
|
|
8
|
+
const HABITAT_ITEMS: LegendOption[] = [
|
|
9
|
+
{ value: 'bogs', label: 'Bogs', color: '#3b82f6' },
|
|
10
|
+
{ value: 'dwarf-shrub', label: 'Dwarf shrub heath', color: '#14b8a6' },
|
|
11
|
+
{ value: 'fens', label: 'Fens, marsh & swamp', color: '#ec4899' },
|
|
12
|
+
{ value: 'littoral', label: 'Littoral sediments', color: '#f97316' },
|
|
13
|
+
{ value: 'neutral', label: 'Neutral grassland', color: '#a855f7' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const meta: Meta<typeof LegendPanel> = {
|
|
17
|
+
title: 'Components/LegendPanel',
|
|
18
|
+
component: LegendPanel,
|
|
19
|
+
tags: ['autodocs'],
|
|
20
|
+
parameters: {
|
|
21
|
+
docs: {
|
|
22
|
+
description: {
|
|
23
|
+
component: `
|
|
24
|
+
Renders a list of \`LegendOption\` items (colour dot + label). This is the shared list rendering used inside \`Legend\`'s popover, and can also be used standalone.
|
|
25
|
+
|
|
26
|
+
Pass \`value\` and \`onSelectItem\` together to switch into single-select interactive mode (radio-group behaviour, one item selected at a time). Omit both for a static, display-only list.
|
|
27
|
+
|
|
28
|
+
\`LegendPanel\` itself has no border or background, that comes from whatever it's nested in (\`Legend\`'s \`PopoverContent\` already supplies it). For standalone use outside a popover, wrap it in a panel-styled container (the \`panel\` utility class, or your own bordered wrapper). \`Card\` is a different pattern now (a top-divider content tile), not a general-purpose bordered box.
|
|
29
|
+
|
|
30
|
+
**Props:**
|
|
31
|
+
- \`items\`: array of \`LegendOption\` objects (\`value\`, \`label\`, \`color\`)
|
|
32
|
+
- \`title\`: optional heading shown above the list
|
|
33
|
+
- \`value\` / \`onSelectItem\`: provide both to make the list selectable
|
|
34
|
+
`.trim(),
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default meta;
|
|
41
|
+
|
|
42
|
+
type Story = StoryObj<typeof LegendPanel>;
|
|
43
|
+
|
|
44
|
+
const InteractiveStory = () => {
|
|
45
|
+
const [active, setActive] = useState<string | null>('bogs');
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="panel p-gds-3 w-52">
|
|
49
|
+
<LegendPanel
|
|
50
|
+
items={HABITAT_ITEMS}
|
|
51
|
+
title="Habitat types"
|
|
52
|
+
value={active}
|
|
53
|
+
onSelectItem={setActive}
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const NoSelectionStory = () => {
|
|
60
|
+
const [active, setActive] = useState<string | null>(null);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="panel p-gds-3 w-52">
|
|
64
|
+
<LegendPanel
|
|
65
|
+
items={HABITAT_ITEMS}
|
|
66
|
+
title="Habitat types"
|
|
67
|
+
value={active}
|
|
68
|
+
onSelectItem={setActive}
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const Default: Story = {
|
|
75
|
+
render: () => (
|
|
76
|
+
<div className="panel p-gds-3 w-52">
|
|
77
|
+
<LegendPanel items={HABITAT_ITEMS} title="Habitat types" />
|
|
78
|
+
</div>
|
|
79
|
+
),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const Interactive: Story = {
|
|
83
|
+
parameters: {
|
|
84
|
+
docs: {
|
|
85
|
+
description: {
|
|
86
|
+
story:
|
|
87
|
+
'Pass `value` and `onSelectItem` to make the list selectable, one item at a time. The same pattern as `LayerSwitcher`, without a popover.',
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
render: () => <InteractiveStory />,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const NoSelection: Story = {
|
|
95
|
+
parameters: {
|
|
96
|
+
docs: {
|
|
97
|
+
description: { story: 'When `value` is `null` no option is shown as selected.' },
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
render: () => <NoSelectionStory />,
|
|
101
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
|
|
3
|
+
import { LegendDot } from './LegendDot';
|
|
4
|
+
import { cn } from '../../utils/components';
|
|
5
|
+
|
|
6
|
+
export type LegendOption = {
|
|
7
|
+
value: string;
|
|
8
|
+
label: string;
|
|
9
|
+
color: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type LegendPanelProps = ComponentProps<'div'> & {
|
|
13
|
+
items: LegendOption[];
|
|
14
|
+
title?: string;
|
|
15
|
+
} & (
|
|
16
|
+
| { value?: never; onSelectItem?: never }
|
|
17
|
+
| { value: string | null; onSelectItem: (value: string) => void }
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export const LegendPanel = ({
|
|
21
|
+
items,
|
|
22
|
+
title,
|
|
23
|
+
value,
|
|
24
|
+
onSelectItem,
|
|
25
|
+
className,
|
|
26
|
+
...props
|
|
27
|
+
}: LegendPanelProps) => (
|
|
28
|
+
<div data-slot="legend-panel" className={cn('cn-legend-panel', className)} {...props}>
|
|
29
|
+
{title !== undefined ? (
|
|
30
|
+
<p data-slot="legend-title" className="cn-legend-title">
|
|
31
|
+
{title}
|
|
32
|
+
</p>
|
|
33
|
+
) : null}
|
|
34
|
+
|
|
35
|
+
{onSelectItem !== undefined ? (
|
|
36
|
+
<div
|
|
37
|
+
data-slot="legend-items"
|
|
38
|
+
role="radiogroup"
|
|
39
|
+
aria-label={title ?? 'Legend'}
|
|
40
|
+
className="cn-legend-items"
|
|
41
|
+
>
|
|
42
|
+
{items.map((item) => (
|
|
43
|
+
<button
|
|
44
|
+
key={item.value}
|
|
45
|
+
type="button"
|
|
46
|
+
role="radio"
|
|
47
|
+
aria-checked={item.value === value}
|
|
48
|
+
onClick={() => onSelectItem(item.value)}
|
|
49
|
+
data-slot="legend-option"
|
|
50
|
+
className="cn-legend-option"
|
|
51
|
+
>
|
|
52
|
+
<LegendDot color={item.color} />
|
|
53
|
+
{item.label}
|
|
54
|
+
</button>
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
) : (
|
|
58
|
+
<ul data-slot="legend-items" className="cn-legend-items">
|
|
59
|
+
{items.map((item) => (
|
|
60
|
+
<li key={item.value} data-slot="legend-item" className="cn-legend-item">
|
|
61
|
+
<LegendDot color={item.color} />
|
|
62
|
+
{item.label}
|
|
63
|
+
</li>
|
|
64
|
+
))}
|
|
65
|
+
</ul>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
4
|
+
|
|
5
|
+
import { LegendPanel } from './LegendPanel';
|
|
6
|
+
import type { LegendOption } from './LegendPanel';
|
|
7
|
+
import { LegendSkeleton } from './LegendSkeleton';
|
|
8
|
+
|
|
9
|
+
const HABITAT_ITEMS: LegendOption[] = [
|
|
10
|
+
{ value: 'bogs', label: 'Bogs', color: '#3b82f6' },
|
|
11
|
+
{ value: 'dwarf-shrub', label: 'Dwarf shrub heath', color: '#14b8a6' },
|
|
12
|
+
{ value: 'fens', label: 'Fens, marsh & swamp', color: '#ec4899' },
|
|
13
|
+
{ value: 'littoral', label: 'Littoral sediments', color: '#f97316' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const meta: Meta<typeof LegendSkeleton> = {
|
|
17
|
+
title: 'Components/Legend/Skeleton',
|
|
18
|
+
component: LegendSkeleton,
|
|
19
|
+
tags: ['autodocs'],
|
|
20
|
+
parameters: {
|
|
21
|
+
docs: {
|
|
22
|
+
description: {
|
|
23
|
+
component:
|
|
24
|
+
'Placeholder for a LegendPanel while its items are loading (e.g. fetched from a WMS GetLegendGraphic call). Reuses cn-legend-panel/title/items/item directly, so it lines up exactly with the real panel once data arrives.',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default meta;
|
|
31
|
+
|
|
32
|
+
type Story = StoryObj<typeof LegendSkeleton>;
|
|
33
|
+
|
|
34
|
+
export const Default: Story = {
|
|
35
|
+
render: () => (
|
|
36
|
+
<div className="w-52">
|
|
37
|
+
<LegendSkeleton />
|
|
38
|
+
</div>
|
|
39
|
+
),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const SideBySide: Story = {
|
|
43
|
+
parameters: {
|
|
44
|
+
docs: {
|
|
45
|
+
description: { story: 'The skeleton and the real panel line up exactly once loaded.' },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
render: () => (
|
|
49
|
+
<div className="flex gap-gds-4">
|
|
50
|
+
<div className="w-52">
|
|
51
|
+
<LegendSkeleton items={HABITAT_ITEMS.length} />
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div className="w-52">
|
|
55
|
+
<LegendPanel items={HABITAT_ITEMS} title="Legend" />
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const InteractiveSideBySideExample = () => {
|
|
62
|
+
const [active, setActive] = useState<string | null>(HABITAT_ITEMS[0]?.value ?? null);
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div className="flex gap-gds-4">
|
|
66
|
+
<div className="w-52">
|
|
67
|
+
<LegendSkeleton items={HABITAT_ITEMS.length} interactive />
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div className="w-52">
|
|
71
|
+
<LegendPanel items={HABITAT_ITEMS} title="Legend" value={active} onSelectItem={setActive} />
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const InteractiveSideBySide: Story = {
|
|
78
|
+
parameters: {
|
|
79
|
+
docs: {
|
|
80
|
+
description: {
|
|
81
|
+
story:
|
|
82
|
+
'Interactive LegendPanel rows (a radiogroup of padded, full-width buttons) are noticeably taller/wider than the plain display rows above, set interactive on the skeleton to match.',
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
render: () => <InteractiveSideBySideExample />,
|
|
87
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../utils/components';
|
|
4
|
+
import { Skeleton } from '../Skeleton/Skeleton';
|
|
5
|
+
|
|
6
|
+
// Stable, fixed identifiers rather than the array index, even though these
|
|
7
|
+
// are decorative and never reorder, so an item's identity is never tied to
|
|
8
|
+
// its position at render time.
|
|
9
|
+
const ITEM_KEYS = [
|
|
10
|
+
'item-1',
|
|
11
|
+
'item-2',
|
|
12
|
+
'item-3',
|
|
13
|
+
'item-4',
|
|
14
|
+
'item-5',
|
|
15
|
+
'item-6',
|
|
16
|
+
'item-7',
|
|
17
|
+
'item-8',
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
export type LegendSkeletonProps = ComponentProps<'div'> & {
|
|
21
|
+
/** Explicitly disallow children, a skeleton is a placeholder shape, not a content wrapper. */
|
|
22
|
+
children?: never;
|
|
23
|
+
/** Number of item rows to show. Defaults to 4. */
|
|
24
|
+
items?: number;
|
|
25
|
+
/** Shows a title bar, matching LegendPanel's own optional title. Defaults to true. */
|
|
26
|
+
showTitle?: boolean;
|
|
27
|
+
/** Matches LegendPanel's interactive (radiogroup) mode: rows are padded, full-width
|
|
28
|
+
* cn-legend-option buttons rather than plain cn-legend-item rows, which is
|
|
29
|
+
* noticeably taller/wider, so the skeleton needs to reserve that same space. */
|
|
30
|
+
interactive?: boolean;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const LegendSkeleton = ({
|
|
34
|
+
className,
|
|
35
|
+
items = 4,
|
|
36
|
+
showTitle = true,
|
|
37
|
+
interactive = false,
|
|
38
|
+
...props
|
|
39
|
+
}: LegendSkeletonProps) => {
|
|
40
|
+
const itemKeys = ITEM_KEYS.slice(0, items);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div aria-hidden className={cn('cn-legend-panel', className)} {...props}>
|
|
44
|
+
{showTitle ? (
|
|
45
|
+
<div className="cn-legend-title">
|
|
46
|
+
<Skeleton className="cn-legend-skeleton-title" />
|
|
47
|
+
</div>
|
|
48
|
+
) : null}
|
|
49
|
+
|
|
50
|
+
{interactive ? (
|
|
51
|
+
<div className="cn-legend-items">
|
|
52
|
+
{itemKeys.map((key) => (
|
|
53
|
+
<div key={key} className="cn-legend-option">
|
|
54
|
+
<Skeleton className="cn-legend-skeleton-dot" />
|
|
55
|
+
|
|
56
|
+
<Skeleton className="cn-legend-skeleton-label" />
|
|
57
|
+
</div>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
) : (
|
|
61
|
+
<ul className="cn-legend-items">
|
|
62
|
+
{itemKeys.map((key) => (
|
|
63
|
+
<li key={key} className="cn-legend-item">
|
|
64
|
+
<Skeleton className="cn-legend-skeleton-dot" />
|
|
65
|
+
|
|
66
|
+
<Skeleton className="cn-legend-skeleton-label" />
|
|
67
|
+
</li>
|
|
68
|
+
))}
|
|
69
|
+
</ul>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
/* MARK: Legend dot
|
|
3
|
+
White ring separates the dot from the accent-coloured selected-option
|
|
4
|
+
background, ensuring visibility regardless of the dot colour. */
|
|
5
|
+
.cn-select-option-selected .cn-legend-dot {
|
|
6
|
+
outline: var(--focus-ring-width) solid var(--color-panel);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
|
|
3
|
+
import { LegendSelect, type LegendOption } from './LegendSelect';
|
|
4
|
+
import { ErrorText } from '../ErrorText/ErrorText';
|
|
5
|
+
import { FormGroup } from '../Form/FormGroup';
|
|
6
|
+
import { Label } from '../Label/Label';
|
|
7
|
+
|
|
8
|
+
const meta: Meta<typeof LegendSelect> = {
|
|
9
|
+
title: 'Components/LegendSelect',
|
|
10
|
+
component: LegendSelect,
|
|
11
|
+
tags: ['autodocs'],
|
|
12
|
+
parameters: {
|
|
13
|
+
docs: {
|
|
14
|
+
description: {
|
|
15
|
+
component:
|
|
16
|
+
'A Select variant where each option carries a `color` field rendered as a filled circle before the label. Useful for map legend filters, status indicators, and any dataset where colour encodes meaning. Extends `Select` directly, so all Select props (clearable, disabled, invalid, async, etc.) work without extra wiring. Pass `formatOptionLabel` if you need to override the rendering.',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default meta;
|
|
23
|
+
|
|
24
|
+
type Story = StoryObj<typeof LegendSelect>;
|
|
25
|
+
|
|
26
|
+
const LAND_COVER_OPTIONS: LegendOption[] = [
|
|
27
|
+
{ value: 'woodland', label: 'Woodland', color: '#2d6a4f' },
|
|
28
|
+
{ value: 'grassland', label: 'Grassland', color: '#95d5b2' },
|
|
29
|
+
{ value: 'arable', label: 'Arable land', color: '#e9c46a' },
|
|
30
|
+
{ value: 'wetland', label: 'Wetland', color: '#4895ef' },
|
|
31
|
+
{ value: 'urban', label: 'Urban / developed', color: '#adb5bd' },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export const Default: Story = {
|
|
35
|
+
args: {
|
|
36
|
+
options: LAND_COVER_OPTIONS,
|
|
37
|
+
placeholder: 'Select land cover type...',
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const WithDefault: Story = {
|
|
42
|
+
parameters: {
|
|
43
|
+
docs: {
|
|
44
|
+
description: {
|
|
45
|
+
story:
|
|
46
|
+
'The colour dot appears in both the dropdown and the selected-value slot in the control.',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
args: {
|
|
51
|
+
options: LAND_COVER_OPTIONS,
|
|
52
|
+
defaultValue: LAND_COVER_OPTIONS[0],
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const Clearable: Story = {
|
|
57
|
+
args: {
|
|
58
|
+
options: LAND_COVER_OPTIONS,
|
|
59
|
+
defaultValue: LAND_COVER_OPTIONS[2],
|
|
60
|
+
isClearable: true,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const Invalid: Story = {
|
|
65
|
+
render: () => (
|
|
66
|
+
<FormGroup invalid>
|
|
67
|
+
<Label htmlFor="legend-invalid">Land cover type</Label>
|
|
68
|
+
|
|
69
|
+
<ErrorText id="legend-invalid-error">Select a land cover type</ErrorText>
|
|
70
|
+
|
|
71
|
+
<LegendSelect
|
|
72
|
+
inputId="legend-invalid"
|
|
73
|
+
aria-describedby="legend-invalid-error"
|
|
74
|
+
options={LAND_COVER_OPTIONS}
|
|
75
|
+
placeholder="Select land cover type…"
|
|
76
|
+
invalid
|
|
77
|
+
/>
|
|
78
|
+
</FormGroup>
|
|
79
|
+
),
|
|
80
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { GroupBase } from 'react-select';
|
|
2
|
+
|
|
3
|
+
import { LegendDot } from '../Legend/LegendDot';
|
|
4
|
+
import type { LegendOption } from '../Legend/LegendPanel';
|
|
5
|
+
import { Select, type SelectProps } from '../Select/Select';
|
|
6
|
+
|
|
7
|
+
export type { LegendOption };
|
|
8
|
+
|
|
9
|
+
export type LegendSelectProps<IsMulti extends boolean = false> = Omit<
|
|
10
|
+
SelectProps<LegendOption, IsMulti, GroupBase<LegendOption>>,
|
|
11
|
+
'formatOptionLabel'
|
|
12
|
+
>;
|
|
13
|
+
|
|
14
|
+
export const LegendSelect = <IsMulti extends boolean = false>(
|
|
15
|
+
props: LegendSelectProps<IsMulti>,
|
|
16
|
+
) => (
|
|
17
|
+
<Select<LegendOption, IsMulti>
|
|
18
|
+
{...props}
|
|
19
|
+
formatOptionLabel={(option) => (
|
|
20
|
+
<span className="flex items-center gap-gds-1">
|
|
21
|
+
<LegendDot color={option.color} />
|
|
22
|
+
{option.label}
|
|
23
|
+
</span>
|
|
24
|
+
)}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/* MARK: Appearances
|
|
2
|
+
Defined as utilities so ButtonLink can @apply them without duplication. */
|
|
3
|
+
|
|
4
|
+
@utility cn-link-appearance-primary {
|
|
5
|
+
@apply text-link;
|
|
6
|
+
|
|
7
|
+
&:visited {
|
|
8
|
+
@apply text-link-visited;
|
|
9
|
+
}
|
|
10
|
+
&:hover:not(:disabled) {
|
|
11
|
+
@apply text-link-hover;
|
|
12
|
+
}
|
|
13
|
+
&:active:not(:disabled) {
|
|
14
|
+
@apply text-link-active;
|
|
15
|
+
}
|
|
16
|
+
&:focus:not(:disabled) {
|
|
17
|
+
@apply text-on-focus;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@utility cn-link-appearance-error {
|
|
22
|
+
color: var(--color-error);
|
|
23
|
+
|
|
24
|
+
&:visited {
|
|
25
|
+
color: var(--color-error);
|
|
26
|
+
}
|
|
27
|
+
&:hover:not(:disabled) {
|
|
28
|
+
color: color-mix(in srgb, var(--color-error) 80%, black);
|
|
29
|
+
}
|
|
30
|
+
&:active:not(:disabled) {
|
|
31
|
+
color: color-mix(in srgb, var(--color-error) 80%, black);
|
|
32
|
+
}
|
|
33
|
+
&:focus:not(:disabled) {
|
|
34
|
+
@apply text-on-focus;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@utility cn-link-appearance-inverse {
|
|
39
|
+
@apply text-white;
|
|
40
|
+
|
|
41
|
+
&:visited {
|
|
42
|
+
@apply text-white;
|
|
43
|
+
}
|
|
44
|
+
&:hover:not(:disabled) {
|
|
45
|
+
@apply text-white;
|
|
46
|
+
}
|
|
47
|
+
&:focus:not(:disabled) {
|
|
48
|
+
@apply text-on-focus;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@utility cn-link-appearance-contrast {
|
|
53
|
+
@apply text-body;
|
|
54
|
+
|
|
55
|
+
&:visited {
|
|
56
|
+
@apply text-body;
|
|
57
|
+
}
|
|
58
|
+
&:hover:not(:disabled),
|
|
59
|
+
&:active:not(:disabled) {
|
|
60
|
+
@apply text-body;
|
|
61
|
+
}
|
|
62
|
+
&:focus:not(:disabled) {
|
|
63
|
+
@apply text-on-focus;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@layer components {
|
|
68
|
+
/*
|
|
69
|
+
MARK: Link
|
|
70
|
+
GDS link pattern. Underline always visible, thicker on hover,
|
|
71
|
+
yellow highlight box on focus. Box shadow creates lines above
|
|
72
|
+
and below text (not a ring). Visited uses distinct purple.
|
|
73
|
+
*/
|
|
74
|
+
.cn-link {
|
|
75
|
+
@apply w-fit cn-link-decoration;
|
|
76
|
+
|
|
77
|
+
&:hover {
|
|
78
|
+
@apply cn-link-hover-decoration;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* GDS focus: yellow box with black underline shadow */
|
|
82
|
+
&:focus {
|
|
83
|
+
@apply text-on-focus bg-focus no-underline outline-focus outline-transparent;
|
|
84
|
+
/* Top shadow extends focus box above text. Bottom shadow replaces underline */
|
|
85
|
+
box-shadow:
|
|
86
|
+
0 -2px var(--color-focus),
|
|
87
|
+
0 4px var(--color-on-focus);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* MARK: Expand variant
|
|
92
|
+
Overrides the base w-fit: content (e.g. a title and a trailing chevron)
|
|
93
|
+
spreads across the full width of whatever the link sits in, rather than
|
|
94
|
+
the icon sitting flush against the text. */
|
|
95
|
+
.cn-link-variant-expand {
|
|
96
|
+
@apply flex w-full items-center justify-between gap-gds-2;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { LuChevronRight } from 'react-icons/lu';
|
|
2
|
+
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
4
|
+
|
|
5
|
+
import { Link } from './Link';
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof Link> = {
|
|
8
|
+
title: 'Components/Link',
|
|
9
|
+
component: Link,
|
|
10
|
+
tags: ['autodocs'],
|
|
11
|
+
parameters: {
|
|
12
|
+
docs: {
|
|
13
|
+
description: {
|
|
14
|
+
component: 'Use Link for accessible anchor navigation with multiple visual variants.',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
argTypes: {
|
|
19
|
+
appearance: {
|
|
20
|
+
control: { type: 'radio' },
|
|
21
|
+
options: ['primary', 'inverse', 'contrast'],
|
|
22
|
+
},
|
|
23
|
+
variant: {
|
|
24
|
+
control: { type: 'radio' },
|
|
25
|
+
options: ['default', 'icon', 'expand'],
|
|
26
|
+
},
|
|
27
|
+
href: { control: 'text' },
|
|
28
|
+
children: { control: 'text' },
|
|
29
|
+
},
|
|
30
|
+
args: {
|
|
31
|
+
appearance: 'primary',
|
|
32
|
+
variant: 'default',
|
|
33
|
+
href: '#',
|
|
34
|
+
children: 'Story link',
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export default meta;
|
|
39
|
+
|
|
40
|
+
type Story = StoryObj<typeof Link>;
|
|
41
|
+
|
|
42
|
+
export const Primary: Story = {
|
|
43
|
+
args: {
|
|
44
|
+
appearance: 'primary',
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const Inverse: Story = {
|
|
49
|
+
args: {
|
|
50
|
+
appearance: 'inverse',
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const Contrast: Story = {
|
|
55
|
+
args: {
|
|
56
|
+
appearance: 'contrast',
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const Expanded: Story = {
|
|
61
|
+
args: {
|
|
62
|
+
variant: 'expand',
|
|
63
|
+
},
|
|
64
|
+
render: (args) => (
|
|
65
|
+
<Link {...args}>
|
|
66
|
+
Explore
|
|
67
|
+
<LuChevronRight aria-hidden className="size-4" strokeWidth={2.5} />
|
|
68
|
+
</Link>
|
|
69
|
+
),
|
|
70
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ComponentProps } from 'react';
|
|
4
|
+
|
|
5
|
+
import { Slot } from 'radix-ui';
|
|
6
|
+
|
|
7
|
+
import { cn } from '../../utils/components';
|
|
8
|
+
|
|
9
|
+
const LINK_APPEARANCES = {
|
|
10
|
+
primary: 'cn-link-appearance-primary',
|
|
11
|
+
/**
|
|
12
|
+
* Typically reserved for use within an `Alert` component with the error appearance.
|
|
13
|
+
*/
|
|
14
|
+
error: 'cn-link-appearance-error',
|
|
15
|
+
inverse: 'cn-link-appearance-inverse',
|
|
16
|
+
contrast: 'cn-link-appearance-contrast',
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
export const LINK_VARIANTS = {
|
|
20
|
+
default: 'cn-link-variant-default',
|
|
21
|
+
icon: 'cn-link-variant-icon',
|
|
22
|
+
/** Full width, content spread apart, e.g. a title with a trailing chevron pushed to the end. */
|
|
23
|
+
expand: 'cn-link-variant-expand',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type LinkAppearance = keyof typeof LINK_APPEARANCES;
|
|
27
|
+
export type LinkVariant = keyof typeof LINK_VARIANTS;
|
|
28
|
+
|
|
29
|
+
export type LinkProps = ComponentProps<'a'> & {
|
|
30
|
+
asChild?: boolean;
|
|
31
|
+
appearance?: LinkAppearance;
|
|
32
|
+
variant?: LinkVariant;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const Link = ({
|
|
36
|
+
className,
|
|
37
|
+
asChild = false,
|
|
38
|
+
appearance = 'primary',
|
|
39
|
+
variant = 'default',
|
|
40
|
+
...props
|
|
41
|
+
}: LinkProps) => {
|
|
42
|
+
const Comp = asChild ? Slot.Root : 'a';
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Comp
|
|
46
|
+
{...props}
|
|
47
|
+
data-slot="link"
|
|
48
|
+
data-variant={variant}
|
|
49
|
+
className={cn('cn-link', LINK_APPEARANCES[appearance], LINK_VARIANTS[variant], className)}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
};
|