From bec264602d51d0c617f11503b09d9957684a81d8 Mon Sep 17 00:00:00 2001 From: "Gina A." <70909035+gndz07@users.noreply.github.com> Date: Fri, 13 Dec 2024 16:42:04 +0100 Subject: [PATCH] feat: side panel --- components/SidePanel/SidePanel.stories.tsx | 127 +++++++++++++++++ components/SidePanel/SidePanel.themes.ts | 19 +++ components/SidePanel/SidePanel.tsx | 152 +++++++++++++++++++++ components/SidePanel/index.ts | 1 + index.ts | 1 + stitches.config.ts | 28 ++-- 6 files changed, 315 insertions(+), 13 deletions(-) create mode 100644 components/SidePanel/SidePanel.stories.tsx create mode 100644 components/SidePanel/SidePanel.themes.ts create mode 100644 components/SidePanel/SidePanel.tsx create mode 100644 components/SidePanel/index.ts diff --git a/components/SidePanel/SidePanel.stories.tsx b/components/SidePanel/SidePanel.stories.tsx new file mode 100644 index 00000000..6e66b9c6 --- /dev/null +++ b/components/SidePanel/SidePanel.stories.tsx @@ -0,0 +1,127 @@ +import { Meta, StoryFn } from '@storybook/react/*'; +import { useState } from 'react'; + +import { Box } from '../Box'; +import { Button } from '../Button'; +import { Dialog, DialogContent, DialogOverlay, DialogPortal } from '../Dialog'; +import { Text } from '../Text'; +import { SidePanel } from './SidePanel'; + +const Component: Meta = { + title: 'Components/SidePanel', + component: SidePanel, + argTypes: { + side: { + options: ['right', 'left', 'top', 'bottom'], + control: { type: 'radio' }, + }, + }, +}; + +const Content = ({ onClickBtn, ctaLabel }: { onClickBtn?: () => void; ctaLabel?: string }) => { + const [count, setCount] = useState(1); + + return ( + <> + + Hello, World! + + {[...Array(count)].map((_, i) => ( + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. + + ))} + + + ); +}; + +export const Basic: StoryFn = (args) => { + const [open, setOpen] = useState(false); + + return ( + <> + + + + {[...Array(10)].map((_, i) => ( + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor + incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud + exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure + dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt + mollit anim id est laborum. + + ))} + + + + setOpen(isOpen)}> + + + + ); +}; + +Basic.args = { + noOverlay: false, + side: 'right', + noCloseIcon: false, +}; + +export const CombinedWithModal: StoryFn = (args) => { + const [open, setOpen] = useState(false); + const [isModalOpen, setModalOpen] = useState(false); + + return ( + <> + setModalOpen(isModalOpen)}> + + + + This is a modal from the side panel + + + + + setOpen(isOpen)}> + setModalOpen(true)} /> + + + + + + {[...Array(10)].map((_, i) => ( + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor + incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud + exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure + dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt + mollit anim id est laborum. + + ))} + + + + ); +}; + +CombinedWithModal.args = { + noOverlay: true, + side: 'right', + noCloseIcon: true, +}; + +export default Component; diff --git a/components/SidePanel/SidePanel.themes.ts b/components/SidePanel/SidePanel.themes.ts new file mode 100644 index 00000000..c9a93e38 --- /dev/null +++ b/components/SidePanel/SidePanel.themes.ts @@ -0,0 +1,19 @@ +import { Property } from '@stitches/react/types/css'; + +import { ColorInfo } from '../../utils/getPrimaryColorInfo'; + +export namespace Theme { + type Colors = { + sidePanelBackground: Property.Color; + }; + + type Factory = (primaryColor?: ColorInfo) => Colors; + + export const getLight: Factory = () => ({ + sidePanelBackground: '$deepBlue2', + }); + + export const getDark: Factory = () => ({ + sidePanelBackground: '$deepBlue3', + }); +} diff --git a/components/SidePanel/SidePanel.tsx b/components/SidePanel/SidePanel.tsx new file mode 100644 index 00000000..7f6c4e6d --- /dev/null +++ b/components/SidePanel/SidePanel.tsx @@ -0,0 +1,152 @@ +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import { Cross1Icon } from '@radix-ui/react-icons'; +import React, { ComponentProps } from 'react'; + +import { CSS, keyframes, styled, VariantProps } from '../../stitches.config'; +import { IconButton } from '../IconButton'; +import { overlayStyles } from '../Overlay'; + +const fadeIn = keyframes({ + from: { opacity: '0' }, + to: { opacity: '1' }, +}); + +const fadeOut = keyframes({ + from: { opacity: '1' }, + to: { opacity: '0' }, +}); + +const SidePanelOverlay = styled(DialogPrimitive.Overlay, overlayStyles, { + position: 'fixed', + top: 0, + right: 0, + bottom: 0, + left: 0, + + '&[data-state="open"]': { + animation: `${fadeIn} 150ms cubic-bezier(0.22, 1, 0.36, 1)`, + }, + + '&[data-state="closed"]': { + animation: `${fadeOut} 150ms cubic-bezier(0.22, 1, 0.36, 1)`, + }, +}); + +const slideIn = keyframes({ + from: { transform: '$$transformValue' }, + to: { transform: 'translate3d(0,0,0)' }, +}); + +const slideOut = keyframes({ + from: { transform: 'translate3d(0,0,0)' }, + to: { transform: '$$transformValue' }, +}); + +const StyledContent = styled(DialogPrimitive.Content, { + backgroundColor: '$sidePanelBackground', + boxShadow: 'inset 0 0 0 1px $colors$deepBlue4', + position: 'fixed', + top: 0, + bottom: 0, + width: 375, + p: '$4', + + '&[data-state="open"]': { + animation: `${slideIn} 150ms cubic-bezier(0.22, 1, 0.36, 1)`, + }, + + '&[data-state="closed"]': { + animation: `${slideOut} 150ms cubic-bezier(0.22, 1, 0.36, 1)`, + }, + + variants: { + side: { + top: { + $$transformValue: 'translate3d(0,-100%,0)', + width: '100%', + height: 250, + bottom: 'auto', + }, + right: { + $$transformValue: 'translate3d(100%,0,0)', + right: 0, + maxWidth: '50%', + }, + bottom: { + $$transformValue: 'translate3d(0,100%,0)', + width: '100%', + height: 250, + bottom: 0, + top: 'auto', + }, + left: { + $$transformValue: 'translate3d(-100%,0,0)', + left: 0, + maxWidth: '50%', + }, + }, + }, + + defaultVariants: { + side: 'right', + }, +}); + +const StyledCloseButton = styled(DialogPrimitive.Close, { + position: 'absolute', + top: '$2', + right: '$2', + cursor: 'pointer', +}); + +interface SidePanelCloseButtonProps + extends VariantProps, + ComponentProps {} + +const SidePanelCloseIconButton = React.forwardRef< + React.ElementRef, + SidePanelCloseButtonProps +>((props, forwardedRef) => ( + + + + + +)); + +type SidePanelContentVariants = VariantProps; +type SidePanelContentPrimitiveProps = React.ComponentProps; +type SidePanelProps = SidePanelContentPrimitiveProps & + SidePanelContentVariants & { + css?: CSS; + noOverlay?: boolean; + noCloseIcon?: boolean; + open?: boolean; + defaultOpen?: boolean; + onOpenChange(open: boolean): void; + }; + +export const SidePanel = React.forwardRef, SidePanelProps>( + ( + { + noOverlay = false, + noCloseIcon = false, + children, + onOpenChange, + open = false, + defaultOpen = false, + ...props + }, + forwardedRef, + ) => ( + + + {!noOverlay && } + + {children} + {!noCloseIcon && } + + + + ), +); diff --git a/components/SidePanel/index.ts b/components/SidePanel/index.ts new file mode 100644 index 00000000..1f6424a9 --- /dev/null +++ b/components/SidePanel/index.ts @@ -0,0 +1 @@ +export * from './SidePanel'; diff --git a/index.ts b/index.ts index 43c56065..db73d246 100644 --- a/index.ts +++ b/index.ts @@ -102,6 +102,7 @@ export { RadioAccordionTrigger, } from './components/RadioAccordion'; export { Select } from './components/Select'; +export { SidePanel } from './components/SidePanel'; export { Skeleton } from './components/Skeleton'; export { Switch } from './components/Switch'; export { Caption, Table, Tbody, Td, Tfoot, Th, Thead, Tr } from './components/Table'; diff --git a/stitches.config.ts b/stitches.config.ts index 50b42644..d7f83fa9 100644 --- a/stitches.config.ts +++ b/stitches.config.ts @@ -1,33 +1,32 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-nocheck import type * as Stitches from '@stitches/react'; import { createStitches, CSS as StitchesCSS } from '@stitches/react'; +import { Property } from '@stitches/react/types/css'; +import { darkColors, lightColors } from './colors'; import { Theme as AccordionTheme } from './components/Accordion/Accordion.themes'; import { Theme as BadgeTheme } from './components/Badge/Badge.themes'; import { Theme as ButtonTheme } from './components/Button/Button.themes'; import { Theme as ButtonSwitchTheme } from './components/ButtonSwitch/ButtonSwitch.themes'; -import { Theme as IconButtonTheme } from './components/IconButton/IconButton.themes'; -import { Theme as SwitchTheme } from './components/Switch/Switch.themes'; import { Theme as CardTheme } from './components/Card/Card.themes'; import { Theme as CheckboxTheme } from './components/Checkbox/Checkbox.themes'; +import { Theme as DialogTheme } from './components/Dialog/Dialog.themes'; +import { Theme as HeadingTheme } from './components/Heading/Heading.themes'; +import { Theme as IconButtonTheme } from './components/IconButton/IconButton.themes'; +import { Theme as InputTheme } from './components/Input/Input.themes'; import { Theme as LinkTheme } from './components/Link/Link.themes'; import { Theme as ListTheme } from './components/List/List.themes'; +import { Theme as NavigationTheme } from './components/Navigation/Navigation.themes'; import { Theme as RadioTheme } from './components/Radio/Radio.themes'; -import { Theme as TextTheme } from './components/Text/Text.themes'; -import { Theme as InputTheme } from './components/Input/Input.themes'; -import { Theme as TableTheme } from './components/Table/Table.themes'; import { Theme as SelectTheme } from './components/Select/Select.themes'; +import { Theme as SidePanelTheme } from './components/SidePanel/SidePanel.themes'; import { Theme as SkeletonTheme } from './components/Skeleton/Skeleton.themes'; -import { Theme as DialogTheme } from './components/Dialog/Dialog.themes'; -import { Theme as NavigationTheme } from './components/Navigation/Navigation.themes'; -import { Theme as TooltipTheme } from './components/Tooltip/Tooltip.themes'; +import { Theme as SwitchTheme } from './components/Switch/Switch.themes'; +import { Theme as TableTheme } from './components/Table/Table.themes'; +import { Theme as TextTheme } from './components/Text/Text.themes'; import { Theme as TextareaTheme } from './components/Textarea/Textarea.themes'; -import { Theme as HeadingTheme } from './components/Heading/Heading.themes'; - -import { lightColors, darkColors } from './colors'; +import { Theme as TooltipTheme } from './components/Tooltip/Tooltip.themes'; import getPrimaryColorInfo from './utils/getPrimaryColorInfo'; -import { Property } from '@stitches/react/types/css'; export type { VariantProps } from '@stitches/react'; @@ -74,6 +73,7 @@ const stitches = createStitches({ ...TooltipTheme.getLight(defaultPrimaryColor), ...TextareaTheme.getLight(defaultPrimaryColor), ...HeadingTheme.getLight(defaultPrimaryColor), + ...SidePanelTheme.getLight(defaultPrimaryColor), }, fonts: { rubik: @@ -307,6 +307,7 @@ export const darkTheme = (primary: PrimaryColor) => { ...TooltipTheme.getDark(darkPrimaryColor), ...TextareaTheme.getDark(darkPrimaryColor), ...HeadingTheme.getDark(darkPrimaryColor), + ...SidePanelTheme.getDark(darkPrimaryColor), }, }); }; @@ -338,6 +339,7 @@ export const lightTheme = (primary: PrimaryColor) => { ...TooltipTheme.getLight(lightPrimaryColor), ...TextareaTheme.getLight(lightPrimaryColor), ...HeadingTheme.getLight(lightPrimaryColor), + ...SidePanelTheme.getLight(lightPrimaryColor), }, }); };