diff --git a/packages/components/src/core/Panel/__storybook__/constants.ts b/packages/components/src/core/Panel/__storybook__/constants.ts
new file mode 100644
index 000000000..594509c77
--- /dev/null
+++ b/packages/components/src/core/Panel/__storybook__/constants.ts
@@ -0,0 +1,6 @@
+export const PANEL_EXCLUDED_CONTROLS = [
+ "sdsType",
+ "position",
+ "width",
+ "disableScrollLock",
+];
diff --git a/packages/components/src/core/Panel/__storybook__/index.stories.tsx b/packages/components/src/core/Panel/__storybook__/index.stories.tsx
new file mode 100644
index 000000000..2365673b2
--- /dev/null
+++ b/packages/components/src/core/Panel/__storybook__/index.stories.tsx
@@ -0,0 +1,71 @@
+import { Args, Meta } from "@storybook/react";
+import { BADGE } from "@geometricpanda/storybook-addon-badges";
+import { Panel } from "./stories/default";
+import { PANEL_EXCLUDED_CONTROLS } from "./constants";
+import { TestDemo } from "./stories/test";
+import { CustomHeaderAndCloseButtonDemo } from "./stories/customCloseButton";
+
+export default {
+ argTypes: {
+ position: {
+ control: {
+ type: "select",
+ },
+ options: ["left", "right", "bottom"],
+ },
+ sdsType: {
+ control: {
+ type: "select",
+ },
+ options: ["basic", "overlay"],
+ },
+ width: {
+ control: {
+ type: "text",
+ },
+ },
+ },
+ component: Panel,
+ parameters: {
+ badges: [BADGE.BETA],
+ },
+ title: "Components/Panel",
+} as Meta;
+
+// Default
+
+export const Default = {
+ args: {
+ position: "left",
+ sdsType: "basic",
+ width: "320px",
+ },
+};
+
+// With Custom Close Button and Header
+
+export const CustomHeaderAndCloseButton = {
+ parameters: {
+ controls: {
+ exclude: PANEL_EXCLUDED_CONTROLS,
+ },
+ snapshot: {
+ skip: true,
+ },
+ },
+ render: (args: Args) => ,
+};
+
+// Test
+
+export const Test = {
+ parameters: {
+ controls: {
+ exclude: PANEL_EXCLUDED_CONTROLS,
+ },
+ snapshot: {
+ skip: true,
+ },
+ },
+ render: (args: Args) => ,
+};
diff --git a/packages/components/src/core/Panel/__storybook__/stories/customCloseButton.tsx b/packages/components/src/core/Panel/__storybook__/stories/customCloseButton.tsx
new file mode 100644
index 000000000..9c1825371
--- /dev/null
+++ b/packages/components/src/core/Panel/__storybook__/stories/customCloseButton.tsx
@@ -0,0 +1,56 @@
+import * as React from "react";
+import { Args } from "@storybook/react";
+import RawPanel from "src/core/Panel";
+import Button from "src/core/Button";
+import { Box, Typography } from "@mui/material";
+import { LONG_LOREM_IPSUM } from "src/common/storybook/loremIpsum";
+
+export const CustomHeaderAndCloseButtonDemo = (props: Args): JSX.Element => {
+ const [open, setOpen] = React.useState(true);
+
+ return (
+ <>
+
+
+
+ {
+ setOpen(false);
+ }}
+ HeaderComponent={
+
+ Panel Header
+
+ }
+ CloseButtonComponent={
+
+ }
+ data-testid="panel"
+ {...props}
+ >
+ {LONG_LOREM_IPSUM}
+
+ >
+ );
+};
diff --git a/packages/components/src/core/Panel/__storybook__/stories/default.tsx b/packages/components/src/core/Panel/__storybook__/stories/default.tsx
new file mode 100644
index 000000000..4f67f3afc
--- /dev/null
+++ b/packages/components/src/core/Panel/__storybook__/stories/default.tsx
@@ -0,0 +1,125 @@
+import { Box, useTheme } from "@mui/material";
+import { Args } from "@storybook/react";
+import React, { useState } from "react";
+import { LONG_LOREM_IPSUM } from "src/common/storybook/loremIpsum";
+import Button from "src/core/Button";
+import Callout from "src/core/Callout";
+import CalloutTitle from "src/core/Callout/components/CalloutTitle";
+import RawPanel, { PanelProps } from "src/core/Panel";
+
+const InvalidBasicPanelPropsError = (
+
+ Invalid Props!
+
+ The Basic Panel only supports left or{" "}
+ right positions. Please update the{" "}
+ position prop to one of these valid values.
+
+
+);
+
+const Main = (
+ props: PanelProps & { open: boolean; children?: React.ReactNode }
+) => {
+ const { open, sdsType, position, children, width } = props;
+
+ const margin =
+ sdsType === "basic"
+ ? position === "left"
+ ? `0 0 0 ${width}`
+ : position === "right"
+ ? `0 ${width} 0 0`
+ : "0" // Default to 0 if neither left nor right
+ : "0"; // Default to 0 for non-basic panels
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const Panel = (props: Args): JSX.Element => {
+ const { sdsType, position } = props;
+
+ const theme = useTheme();
+ const [open, setOpen] = useState(false);
+
+ const toggleDrawer = (newOpen: boolean) => () => {
+ setOpen(newOpen);
+ };
+
+ const DrawerList = (
+
+ [Panel Content]
+
+ );
+
+ const HeaderComponent = (
+
+ [Panel Header]
+
+ );
+
+ if (sdsType === "basic" && position === "bottom") {
+ return InvalidBasicPanelPropsError;
+ }
+
+ return (
+ <>
+
+ {DrawerList}
+
+
+
+
+
+ {LONG_LOREM_IPSUM}
+ {LONG_LOREM_IPSUM}
+ {LONG_LOREM_IPSUM}
+ {LONG_LOREM_IPSUM}
+
+ >
+ );
+};
diff --git a/packages/components/src/core/Panel/__storybook__/stories/test.tsx b/packages/components/src/core/Panel/__storybook__/stories/test.tsx
new file mode 100644
index 000000000..0fb63d5d7
--- /dev/null
+++ b/packages/components/src/core/Panel/__storybook__/stories/test.tsx
@@ -0,0 +1,10 @@
+import { Args } from "@storybook/react";
+import RawPanel from "src/core/Panel";
+
+export const TestDemo = (props: Args): JSX.Element => {
+ return (
+
+ [Panel Content]
+
+ );
+};
diff --git a/packages/components/src/core/Panel/__tests__/Panel.namespace-test.tsx b/packages/components/src/core/Panel/__tests__/Panel.namespace-test.tsx
new file mode 100644
index 000000000..04d8206e0
--- /dev/null
+++ b/packages/components/src/core/Panel/__tests__/Panel.namespace-test.tsx
@@ -0,0 +1,18 @@
+import React from "react";
+import { Panel, PanelProps } from "@czi-sds/components";
+import { noop } from "src/common/utils";
+
+const PanelNameSpaceTest = (props: PanelProps) => {
+ return (
+ Header
}
+ CloseButtonComponent={}
+ closeButtonOnClick={noop}
+ >
+ This is a Basic Panel!
+
+ );
+};
diff --git a/packages/components/src/core/Panel/__tests__/__snapshots__/index.test.tsx.snap b/packages/components/src/core/Panel/__tests__/__snapshots__/index.test.tsx.snap
new file mode 100644
index 000000000..07ef7858d
--- /dev/null
+++ b/packages/components/src/core/Panel/__tests__/__snapshots__/index.test.tsx.snap
@@ -0,0 +1,20 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` Default story renders snapshot 1`] = `
+
+`;
diff --git a/packages/components/src/core/Panel/__tests__/index.test.tsx b/packages/components/src/core/Panel/__tests__/index.test.tsx
new file mode 100644
index 000000000..d9af24a79
--- /dev/null
+++ b/packages/components/src/core/Panel/__tests__/index.test.tsx
@@ -0,0 +1,124 @@
+import { generateSnapshots } from "@chanzuckerberg/story-utils";
+import { composeStories } from "@storybook/react";
+import { fireEvent, render, screen } from "@testing-library/react";
+import * as stories from "../__storybook__/index.stories";
+import Button from "src/core/Button";
+
+// Returns a component that already contain all decorators from story level, meta level and global level.
+const { Test } = composeStories(stories);
+
+const PAPER_ROOT_CLASS_NAME = "MuiPaper-root";
+const MUI_DRAWER_ANCHOR_LEFT_CLASS_NAME = "MuiDrawer-paperAnchorLeft";
+const MUI_DRAWER_ANCHOR_RIGHT_CLASS_NAME = "MuiDrawer-paperAnchorRight";
+const MUI_DRAWER_ANCHOR_BOTTOM_CLASS_NAME = "MuiDrawer-paperAnchorBottom";
+
+describe("", () => {
+ generateSnapshots(stories);
+
+ it("renders panel component", () => {
+ render();
+ const panelElement = screen.getByTestId("panel");
+ expect(panelElement).not.toBeNull();
+ });
+
+ it("renders with overlay header and close button when sdsType is 'overlay'", () => {
+ render(
+ Header}
+ CloseButtonComponent={
+
+ }
+ />
+ );
+
+ // Check if HeaderComponent is rendered
+ const headerElement = screen.getByText("Header");
+ expect(headerElement).toBeInTheDocument();
+
+ // Check if close button is rendered
+ const closeButton = screen.getByTestId("panel-close-button");
+ expect(closeButton).toBeInTheDocument();
+ });
+
+ it("renders without header when sdsType is 'basic'", () => {
+ render();
+
+ // The header component should not be rendered for 'basic' panels
+ const headerElement = screen.queryByText("Header");
+ expect(headerElement).not.toBeInTheDocument();
+ });
+
+ it("applies the correct position based on the 'position' prop", () => {
+ const { rerender } = render();
+
+ let panelElementPaper = screen
+ .getByTestId("panel")
+ .getElementsByClassName(PAPER_ROOT_CLASS_NAME)[0];
+ expect(panelElementPaper).toHaveClass(MUI_DRAWER_ANCHOR_LEFT_CLASS_NAME);
+
+ rerender();
+ panelElementPaper = screen
+ .getByTestId("panel")
+ .getElementsByClassName(PAPER_ROOT_CLASS_NAME)[0];
+ expect(panelElementPaper).toHaveClass(MUI_DRAWER_ANCHOR_RIGHT_CLASS_NAME);
+
+ rerender();
+ panelElementPaper = screen
+ .getByTestId("panel")
+ .getElementsByClassName(PAPER_ROOT_CLASS_NAME)[0];
+ expect(panelElementPaper).toHaveClass(MUI_DRAWER_ANCHOR_BOTTOM_CLASS_NAME);
+ });
+
+ it("calls the onClick handler when close button is clicked", () => {
+ const handleClose = jest.fn();
+
+ render(
+ Header}
+ CloseButtonComponent={
+
+ }
+ />
+ );
+
+ const closeButton = screen.getByTestId("panel-close-button");
+ fireEvent.click(closeButton);
+
+ expect(handleClose).toHaveBeenCalledTimes(1);
+ });
+
+ it("renders with default 'sdsType' and 'position' props", () => {
+ render();
+
+ const panelElementPaper = screen
+ .getByTestId("panel")
+ .getElementsByClassName(PAPER_ROOT_CLASS_NAME)[0];
+
+ // Default props: sdsType should be 'basic' and position should be 'left'
+ expect(panelElementPaper).toHaveClass(MUI_DRAWER_ANCHOR_LEFT_CLASS_NAME);
+ });
+
+ it("does not accept position='bottom' for sdsType='basic', should default to position='left'", () => {
+ // Render the BasicPanel with an invalid position 'bottom'
+ render();
+
+ const panelElement = screen.getByTestId("panel");
+
+ expect(panelElement).not.toHaveClass(MUI_DRAWER_ANCHOR_BOTTOM_CLASS_NAME);
+ });
+});
diff --git a/packages/components/src/core/Panel/components/PanelHeader/index.tsx b/packages/components/src/core/Panel/components/PanelHeader/index.tsx
new file mode 100644
index 000000000..ac864b8a2
--- /dev/null
+++ b/packages/components/src/core/Panel/components/PanelHeader/index.tsx
@@ -0,0 +1,13 @@
+import { StyledPanelHeader } from "./style";
+
+export interface PanelHeaderProps {
+ children?: React.ReactNode;
+}
+
+const PanelHeader = (props: PanelHeaderProps) => {
+ const { children } = props;
+
+ return {children};
+};
+
+export default PanelHeader;
diff --git a/packages/components/src/core/Panel/components/PanelHeader/style.ts b/packages/components/src/core/Panel/components/PanelHeader/style.ts
new file mode 100644
index 000000000..63ef866f4
--- /dev/null
+++ b/packages/components/src/core/Panel/components/PanelHeader/style.ts
@@ -0,0 +1,19 @@
+import styled from "@emotion/styled";
+import { CommonThemeProps, getSpaces } from "src/core/styles";
+import { PanelHeaderProps } from ".";
+
+export interface ExtraPanelHeaderProps
+ extends CommonThemeProps,
+ PanelHeaderProps {}
+
+export const StyledPanelHeader = styled("div")`
+ ${(props: ExtraPanelHeaderProps) => {
+ const spaces = getSpaces(props);
+
+ return `
+ width: 100%;
+ height: 100%;
+ margin-right: ${spaces?.l}px;
+ `;
+ }}
+`;
diff --git a/packages/components/src/core/Panel/components/PanelHeaderClose/index.tsx b/packages/components/src/core/Panel/components/PanelHeaderClose/index.tsx
new file mode 100644
index 000000000..42c4e0e9b
--- /dev/null
+++ b/packages/components/src/core/Panel/components/PanelHeaderClose/index.tsx
@@ -0,0 +1,28 @@
+import Button from "src/core/Button";
+import { StyledPanelHeaderClose } from "./style";
+
+export interface PanelHeaderCloseProps {
+ onClick?: React.MouseEventHandler;
+ CloseButtonComponent?: React.ReactNode;
+}
+
+const PanelHeaderClose = (props: PanelHeaderCloseProps) => {
+ const { onClick, CloseButtonComponent } = props;
+
+ return (
+
+ {CloseButtonComponent ? (
+ CloseButtonComponent
+ ) : (
+
+ )}
+
+ );
+};
+
+export default PanelHeaderClose;
diff --git a/packages/components/src/core/Panel/components/PanelHeaderClose/style.ts b/packages/components/src/core/Panel/components/PanelHeaderClose/style.ts
new file mode 100644
index 000000000..4ebb4163e
--- /dev/null
+++ b/packages/components/src/core/Panel/components/PanelHeaderClose/style.ts
@@ -0,0 +1,7 @@
+import styled from "@emotion/styled";
+
+export const StyledPanelHeaderClose = styled("div")`
+ display: flex;
+ justify-content: end;
+ align-items: center;
+`;
diff --git a/packages/components/src/core/Panel/index.tsx b/packages/components/src/core/Panel/index.tsx
new file mode 100644
index 000000000..5ab0eda97
--- /dev/null
+++ b/packages/components/src/core/Panel/index.tsx
@@ -0,0 +1,93 @@
+import React from "react";
+import { StyledDrawer, StyledHeaderComponent } from "./style";
+import { DrawerProps } from "@mui/material";
+import PanelHeader from "./components/PanelHeader";
+import PanelHeaderClose, {
+ PanelHeaderCloseProps,
+} from "./components/PanelHeaderClose";
+
+export interface BasicPanelProps extends Omit {
+ sdsType?: "basic"; // Discriminator
+ position?: "left" | "right";
+ width?: number | string;
+}
+
+export interface OverlayPanelProps extends Omit {
+ sdsType?: "overlay"; // Discriminator
+ position?: "left" | "right" | "bottom";
+ width?: number | string;
+ HeaderComponent?: React.ReactNode;
+ closeButtonOnClick?: PanelHeaderCloseProps["onClick"];
+ CloseButtonComponent?: PanelHeaderCloseProps["CloseButtonComponent"];
+}
+
+// Discriminated Union
+export type PanelProps = BasicPanelProps | OverlayPanelProps;
+
+// Type guard to narrow the type
+function isOverlayPanelProps(props: PanelProps): props is OverlayPanelProps {
+ return props.sdsType === "overlay";
+}
+
+export const PANEL_BASIC_MIN_WIDTH_PX = 240;
+export const PANEL_OVERLAY_MIN_WIDTH_PX = 320;
+
+/**
+ * @see https://mui.com/material-ui/react-drawer/
+ */
+
+const Panel = React.forwardRef((props, ref) => {
+ const { children, sdsType = "basic", position = "left", width } = props;
+
+ const drawerWidth = width
+ ? width
+ : sdsType === "basic"
+ ? PANEL_BASIC_MIN_WIDTH_PX
+ : PANEL_OVERLAY_MIN_WIDTH_PX;
+ const drawerVariant = sdsType === "basic" ? "persistent" : "temporary";
+
+ // (masoudmanson): The basic Panel only supports "left" or "right" positions.
+ // If a "bottom" position is provided for a basic Panel, it defaults to "left".
+ const drawerAnchor =
+ sdsType === "overlay"
+ ? position
+ : position === "bottom"
+ ? "left"
+ : position;
+
+ return (
+ /**
+ * (masoudmanson): We need the props after {...props} to override any
+ * user-defined values. Placing them afterward ensures that our values
+ * take priority.
+ *
+ * For example, the SDS design specifies that a BasicPanel should only be anchored
+ * to the left or right. To prevent users from positioning it at the bottom,
+ * the 'anchor' prop must always be set to our predefined value.
+ */
+
+ {isOverlayPanelProps(props) && (
+
+ {props?.HeaderComponent && (
+ {props?.HeaderComponent}
+ )}
+ {
+
+ }
+
+ )}
+ {children}
+
+ );
+});
+
+export default Panel;
diff --git a/packages/components/src/core/Panel/style.ts b/packages/components/src/core/Panel/style.ts
new file mode 100644
index 000000000..e430cfd99
--- /dev/null
+++ b/packages/components/src/core/Panel/style.ts
@@ -0,0 +1,103 @@
+import styled from "@emotion/styled";
+import { Drawer, drawerClasses } from "@mui/material";
+import {
+ CommonThemeProps,
+ getSemanticColors,
+ getShadows,
+ getSpaces,
+} from "../styles";
+import {
+ PANEL_BASIC_MIN_WIDTH_PX,
+ PANEL_OVERLAY_MIN_WIDTH_PX,
+ PanelProps,
+} from ".";
+import { css, SerializedStyles } from "@emotion/react";
+
+type PanelExtraProps = PanelProps & CommonThemeProps;
+
+const doNotForwardProps = [
+ "sdsType",
+ "position",
+ "width",
+ "headerComponent",
+ "onClick",
+ "disableScrollLock",
+ "closeButtonOnClick",
+ "CloseButtonComponent",
+];
+
+const basicPanelStyles = (props: PanelExtraProps): SerializedStyles => {
+ const semanticColors = getSemanticColors(props);
+ const spaces = getSpaces(props);
+
+ return css`
+ .${drawerClasses.paper} {
+ background-color: ${semanticColors?.base?.surfacePrimary};
+ padding: ${spaces?.l}px;
+ min-width: ${PANEL_BASIC_MIN_WIDTH_PX}px;
+ min-height: ${PANEL_BASIC_MIN_WIDTH_PX}px;
+ }
+ `;
+};
+
+const overlayPanelStyles = (props: PanelExtraProps): SerializedStyles => {
+ const semanticColors = getSemanticColors(props);
+ const spaces = getSpaces(props);
+ const shadows = getShadows(props);
+
+ return css`
+ .${drawerClasses.paper} {
+ background-color: ${semanticColors?.base?.surfacePrimary};
+ padding: ${spaces?.xl}px;
+ min-width: ${PANEL_OVERLAY_MIN_WIDTH_PX}px;
+ min-height: ${PANEL_OVERLAY_MIN_WIDTH_PX}px;
+ box-shadow: ${shadows?.l};
+ background-image: none;
+ }
+ `;
+};
+
+export const StyledDrawer = styled(Drawer, {
+ shouldForwardProp: (prop: string) => !doNotForwardProps.includes(prop),
+})`
+ ${(props: PanelExtraProps) => {
+ const { sdsType = "basic", anchor = "left", width } = props;
+ const semanticColors = getSemanticColors(props);
+ const spaces = getSpaces(props);
+
+ const widthString = typeof width === "number" ? `${width}px` : width;
+
+ const panelWidth = anchor !== "bottom" ? widthString : "100%";
+ const panelHeight = anchor !== "bottom" ? "100%" : widthString;
+
+ return css`
+ .${drawerClasses.root} {
+ border-color: ${semanticColors?.base?.divider};
+ border-width: ${spaces?.xxxs}px;
+ height: ${panelHeight};
+ width: ${panelWidth};
+ }
+
+ .${drawerClasses.paper} {
+ height: ${panelHeight};
+ width: ${panelWidth};
+ }
+
+ ${sdsType === "basic" && basicPanelStyles(props)}
+ ${sdsType === "overlay" && overlayPanelStyles(props)}
+ `;
+ }}
+`;
+
+export const StyledHeaderComponent = styled("div")`
+ ${(props: CommonThemeProps) => {
+ const spaces = getSpaces(props);
+
+ return css`
+ margin-bottom: ${spaces?.xxl}px;
+ display: flex;
+ justify-content: space-between;
+ align-items: start;
+ `;
+ }}
+`;
diff --git a/packages/components/src/core/styles/common/makeThemeOptions.ts b/packages/components/src/core/styles/common/makeThemeOptions.ts
index b938e9817..69da63ca7 100644
--- a/packages/components/src/core/styles/common/makeThemeOptions.ts
+++ b/packages/components/src/core/styles/common/makeThemeOptions.ts
@@ -268,6 +268,11 @@ export function makeThemeOptions(
disableRipple: true,
},
},
+ MuiDrawer: {
+ defaultProps: {
+ hideBackdrop: true,
+ },
+ },
MuiLink: {
defaultProps: {
underline: "hover",
diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts
index 46af276c3..4f90c7cd0 100644
--- a/packages/components/src/index.ts
+++ b/packages/components/src/index.ts
@@ -81,6 +81,8 @@ export * from "./core/Notification";
export { default as Notification } from "./core/Notification";
export * from "./core/Pagination";
export { default as Pagination } from "./core/Pagination";
+export * from "./core/Panel";
+export { default as Panel } from "./core/Panel";
export * from "./core/SegmentedControl";
export { default as SegmentedControl } from "./core/SegmentedControl";
export * from "./core/Table";