Skip to content

Commit bec2646

Browse files
authored
feat: side panel
1 parent 3a475d4 commit bec2646

File tree

6 files changed

+315
-13
lines changed

6 files changed

+315
-13
lines changed
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { Meta, StoryFn } from '@storybook/react/*';
2+
import { useState } from 'react';
3+
4+
import { Box } from '../Box';
5+
import { Button } from '../Button';
6+
import { Dialog, DialogContent, DialogOverlay, DialogPortal } from '../Dialog';
7+
import { Text } from '../Text';
8+
import { SidePanel } from './SidePanel';
9+
10+
const Component: Meta<typeof SidePanel> = {
11+
title: 'Components/SidePanel',
12+
component: SidePanel,
13+
argTypes: {
14+
side: {
15+
options: ['right', 'left', 'top', 'bottom'],
16+
control: { type: 'radio' },
17+
},
18+
},
19+
};
20+
21+
const Content = ({ onClickBtn, ctaLabel }: { onClickBtn?: () => void; ctaLabel?: string }) => {
22+
const [count, setCount] = useState(1);
23+
24+
return (
25+
<>
26+
<Text size={2} as="h3" css={{ mb: '$3' }}>
27+
Hello, World!
28+
</Text>
29+
{[...Array(count)].map((_, i) => (
30+
<Text key={i} css={{ mb: '$1' }}>
31+
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
32+
ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
33+
ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
34+
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur
35+
sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
36+
est laborum.
37+
</Text>
38+
))}
39+
<Button
40+
css={{ mt: '$3' }}
41+
onClick={() => (onClickBtn ? onClickBtn() : setCount((c) => c + 1))}
42+
>
43+
{ctaLabel || 'Do some actions'}
44+
</Button>
45+
</>
46+
);
47+
};
48+
49+
export const Basic: StoryFn<typeof SidePanel> = (args) => {
50+
const [open, setOpen] = useState(false);
51+
52+
return (
53+
<>
54+
<Box>
55+
<Button onClick={() => setOpen(true)}>Open side panel</Button>
56+
<Box css={{ mt: '$4' }}>
57+
{[...Array(10)].map((_, i) => (
58+
<Text key={i} css={{ my: '$1' }}>
59+
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
60+
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
61+
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure
62+
dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
63+
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt
64+
mollit anim id est laborum.
65+
</Text>
66+
))}
67+
</Box>
68+
</Box>
69+
70+
<SidePanel {...args} open={open} onOpenChange={(isOpen) => setOpen(isOpen)}>
71+
<Content />
72+
</SidePanel>
73+
</>
74+
);
75+
};
76+
77+
Basic.args = {
78+
noOverlay: false,
79+
side: 'right',
80+
noCloseIcon: false,
81+
};
82+
83+
export const CombinedWithModal: StoryFn<typeof SidePanel> = (args) => {
84+
const [open, setOpen] = useState(false);
85+
const [isModalOpen, setModalOpen] = useState(false);
86+
87+
return (
88+
<>
89+
<Dialog open={isModalOpen} onOpenChange={(isModalOpen) => setModalOpen(isModalOpen)}>
90+
<DialogPortal>
91+
<DialogOverlay />
92+
<DialogContent>
93+
<Text css={{ m: '$2', mt: '$4' }}>This is a modal from the side panel</Text>
94+
</DialogContent>
95+
</DialogPortal>
96+
</Dialog>
97+
98+
<SidePanel {...args} open={open} onOpenChange={(isOpen) => setOpen(isOpen)}>
99+
<Content ctaLabel="Open modal" onClickBtn={() => setModalOpen(true)} />
100+
</SidePanel>
101+
102+
<Box>
103+
<Button onClick={() => setOpen(true)}>Open side panel</Button>
104+
<Box css={{ mt: '$4' }}>
105+
{[...Array(10)].map((_, i) => (
106+
<Text key={i} css={{ my: '$1' }}>
107+
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
108+
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
109+
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure
110+
dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
111+
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt
112+
mollit anim id est laborum.
113+
</Text>
114+
))}
115+
</Box>
116+
</Box>
117+
</>
118+
);
119+
};
120+
121+
CombinedWithModal.args = {
122+
noOverlay: true,
123+
side: 'right',
124+
noCloseIcon: true,
125+
};
126+
127+
export default Component;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Property } from '@stitches/react/types/css';
2+
3+
import { ColorInfo } from '../../utils/getPrimaryColorInfo';
4+
5+
export namespace Theme {
6+
type Colors = {
7+
sidePanelBackground: Property.Color;
8+
};
9+
10+
type Factory = (primaryColor?: ColorInfo) => Colors;
11+
12+
export const getLight: Factory = () => ({
13+
sidePanelBackground: '$deepBlue2',
14+
});
15+
16+
export const getDark: Factory = () => ({
17+
sidePanelBackground: '$deepBlue3',
18+
});
19+
}

components/SidePanel/SidePanel.tsx

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import * as DialogPrimitive from '@radix-ui/react-dialog';
2+
import { Cross1Icon } from '@radix-ui/react-icons';
3+
import React, { ComponentProps } from 'react';
4+
5+
import { CSS, keyframes, styled, VariantProps } from '../../stitches.config';
6+
import { IconButton } from '../IconButton';
7+
import { overlayStyles } from '../Overlay';
8+
9+
const fadeIn = keyframes({
10+
from: { opacity: '0' },
11+
to: { opacity: '1' },
12+
});
13+
14+
const fadeOut = keyframes({
15+
from: { opacity: '1' },
16+
to: { opacity: '0' },
17+
});
18+
19+
const SidePanelOverlay = styled(DialogPrimitive.Overlay, overlayStyles, {
20+
position: 'fixed',
21+
top: 0,
22+
right: 0,
23+
bottom: 0,
24+
left: 0,
25+
26+
'&[data-state="open"]': {
27+
animation: `${fadeIn} 150ms cubic-bezier(0.22, 1, 0.36, 1)`,
28+
},
29+
30+
'&[data-state="closed"]': {
31+
animation: `${fadeOut} 150ms cubic-bezier(0.22, 1, 0.36, 1)`,
32+
},
33+
});
34+
35+
const slideIn = keyframes({
36+
from: { transform: '$$transformValue' },
37+
to: { transform: 'translate3d(0,0,0)' },
38+
});
39+
40+
const slideOut = keyframes({
41+
from: { transform: 'translate3d(0,0,0)' },
42+
to: { transform: '$$transformValue' },
43+
});
44+
45+
const StyledContent = styled(DialogPrimitive.Content, {
46+
backgroundColor: '$sidePanelBackground',
47+
boxShadow: 'inset 0 0 0 1px $colors$deepBlue4',
48+
position: 'fixed',
49+
top: 0,
50+
bottom: 0,
51+
width: 375,
52+
p: '$4',
53+
54+
'&[data-state="open"]': {
55+
animation: `${slideIn} 150ms cubic-bezier(0.22, 1, 0.36, 1)`,
56+
},
57+
58+
'&[data-state="closed"]': {
59+
animation: `${slideOut} 150ms cubic-bezier(0.22, 1, 0.36, 1)`,
60+
},
61+
62+
variants: {
63+
side: {
64+
top: {
65+
$$transformValue: 'translate3d(0,-100%,0)',
66+
width: '100%',
67+
height: 250,
68+
bottom: 'auto',
69+
},
70+
right: {
71+
$$transformValue: 'translate3d(100%,0,0)',
72+
right: 0,
73+
maxWidth: '50%',
74+
},
75+
bottom: {
76+
$$transformValue: 'translate3d(0,100%,0)',
77+
width: '100%',
78+
height: 250,
79+
bottom: 0,
80+
top: 'auto',
81+
},
82+
left: {
83+
$$transformValue: 'translate3d(-100%,0,0)',
84+
left: 0,
85+
maxWidth: '50%',
86+
},
87+
},
88+
},
89+
90+
defaultVariants: {
91+
side: 'right',
92+
},
93+
});
94+
95+
const StyledCloseButton = styled(DialogPrimitive.Close, {
96+
position: 'absolute',
97+
top: '$2',
98+
right: '$2',
99+
cursor: 'pointer',
100+
});
101+
102+
interface SidePanelCloseButtonProps
103+
extends VariantProps<typeof IconButton>,
104+
ComponentProps<typeof IconButton> {}
105+
106+
const SidePanelCloseIconButton = React.forwardRef<
107+
React.ElementRef<typeof IconButton>,
108+
SidePanelCloseButtonProps
109+
>((props, forwardedRef) => (
110+
<StyledCloseButton asChild>
111+
<IconButton ref={forwardedRef} css={{ color: '$hiContrast' }} {...props}>
112+
<Cross1Icon />
113+
</IconButton>
114+
</StyledCloseButton>
115+
));
116+
117+
type SidePanelContentVariants = VariantProps<typeof StyledContent>;
118+
type SidePanelContentPrimitiveProps = React.ComponentProps<typeof DialogPrimitive.Content>;
119+
type SidePanelProps = SidePanelContentPrimitiveProps &
120+
SidePanelContentVariants & {
121+
css?: CSS;
122+
noOverlay?: boolean;
123+
noCloseIcon?: boolean;
124+
open?: boolean;
125+
defaultOpen?: boolean;
126+
onOpenChange(open: boolean): void;
127+
};
128+
129+
export const SidePanel = React.forwardRef<React.ElementRef<typeof StyledContent>, SidePanelProps>(
130+
(
131+
{
132+
noOverlay = false,
133+
noCloseIcon = false,
134+
children,
135+
onOpenChange,
136+
open = false,
137+
defaultOpen = false,
138+
...props
139+
},
140+
forwardedRef,
141+
) => (
142+
<DialogPrimitive.Root open={open} onOpenChange={onOpenChange} defaultOpen={defaultOpen}>
143+
<DialogPrimitive.Portal>
144+
{!noOverlay && <SidePanelOverlay />}
145+
<StyledContent {...props} ref={forwardedRef}>
146+
{children}
147+
{!noCloseIcon && <SidePanelCloseIconButton />}
148+
</StyledContent>
149+
</DialogPrimitive.Portal>
150+
</DialogPrimitive.Root>
151+
),
152+
);

components/SidePanel/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './SidePanel';

index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ export {
102102
RadioAccordionTrigger,
103103
} from './components/RadioAccordion';
104104
export { Select } from './components/Select';
105+
export { SidePanel } from './components/SidePanel';
105106
export { Skeleton } from './components/Skeleton';
106107
export { Switch } from './components/Switch';
107108
export { Caption, Table, Tbody, Td, Tfoot, Th, Thead, Tr } from './components/Table';

stitches.config.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,32 @@
1-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
21
// @ts-nocheck
32
import type * as Stitches from '@stitches/react';
43
import { createStitches, CSS as StitchesCSS } from '@stitches/react';
4+
import { Property } from '@stitches/react/types/css';
55

6+
import { darkColors, lightColors } from './colors';
67
import { Theme as AccordionTheme } from './components/Accordion/Accordion.themes';
78
import { Theme as BadgeTheme } from './components/Badge/Badge.themes';
89
import { Theme as ButtonTheme } from './components/Button/Button.themes';
910
import { Theme as ButtonSwitchTheme } from './components/ButtonSwitch/ButtonSwitch.themes';
10-
import { Theme as IconButtonTheme } from './components/IconButton/IconButton.themes';
11-
import { Theme as SwitchTheme } from './components/Switch/Switch.themes';
1211
import { Theme as CardTheme } from './components/Card/Card.themes';
1312
import { Theme as CheckboxTheme } from './components/Checkbox/Checkbox.themes';
13+
import { Theme as DialogTheme } from './components/Dialog/Dialog.themes';
14+
import { Theme as HeadingTheme } from './components/Heading/Heading.themes';
15+
import { Theme as IconButtonTheme } from './components/IconButton/IconButton.themes';
16+
import { Theme as InputTheme } from './components/Input/Input.themes';
1417
import { Theme as LinkTheme } from './components/Link/Link.themes';
1518
import { Theme as ListTheme } from './components/List/List.themes';
19+
import { Theme as NavigationTheme } from './components/Navigation/Navigation.themes';
1620
import { Theme as RadioTheme } from './components/Radio/Radio.themes';
17-
import { Theme as TextTheme } from './components/Text/Text.themes';
18-
import { Theme as InputTheme } from './components/Input/Input.themes';
19-
import { Theme as TableTheme } from './components/Table/Table.themes';
2021
import { Theme as SelectTheme } from './components/Select/Select.themes';
22+
import { Theme as SidePanelTheme } from './components/SidePanel/SidePanel.themes';
2123
import { Theme as SkeletonTheme } from './components/Skeleton/Skeleton.themes';
22-
import { Theme as DialogTheme } from './components/Dialog/Dialog.themes';
23-
import { Theme as NavigationTheme } from './components/Navigation/Navigation.themes';
24-
import { Theme as TooltipTheme } from './components/Tooltip/Tooltip.themes';
24+
import { Theme as SwitchTheme } from './components/Switch/Switch.themes';
25+
import { Theme as TableTheme } from './components/Table/Table.themes';
26+
import { Theme as TextTheme } from './components/Text/Text.themes';
2527
import { Theme as TextareaTheme } from './components/Textarea/Textarea.themes';
26-
import { Theme as HeadingTheme } from './components/Heading/Heading.themes';
27-
28-
import { lightColors, darkColors } from './colors';
28+
import { Theme as TooltipTheme } from './components/Tooltip/Tooltip.themes';
2929
import getPrimaryColorInfo from './utils/getPrimaryColorInfo';
30-
import { Property } from '@stitches/react/types/css';
3130

3231
export type { VariantProps } from '@stitches/react';
3332

@@ -74,6 +73,7 @@ const stitches = createStitches({
7473
...TooltipTheme.getLight(defaultPrimaryColor),
7574
...TextareaTheme.getLight(defaultPrimaryColor),
7675
...HeadingTheme.getLight(defaultPrimaryColor),
76+
...SidePanelTheme.getLight(defaultPrimaryColor),
7777
},
7878
fonts: {
7979
rubik:
@@ -307,6 +307,7 @@ export const darkTheme = (primary: PrimaryColor) => {
307307
...TooltipTheme.getDark(darkPrimaryColor),
308308
...TextareaTheme.getDark(darkPrimaryColor),
309309
...HeadingTheme.getDark(darkPrimaryColor),
310+
...SidePanelTheme.getDark(darkPrimaryColor),
310311
},
311312
});
312313
};
@@ -338,6 +339,7 @@ export const lightTheme = (primary: PrimaryColor) => {
338339
...TooltipTheme.getLight(lightPrimaryColor),
339340
...TextareaTheme.getLight(lightPrimaryColor),
340341
...HeadingTheme.getLight(lightPrimaryColor),
342+
...SidePanelTheme.getLight(lightPrimaryColor),
341343
},
342344
});
343345
};

0 commit comments

Comments
 (0)