diff --git a/packages/epo-react-lib/.storybook/decorators.tsx b/packages/epo-react-lib/.storybook/decorators.tsx
index d28a2d12..4eef05de 100644
--- a/packages/epo-react-lib/.storybook/decorators.tsx
+++ b/packages/epo-react-lib/.storybook/decorators.tsx
@@ -8,7 +8,7 @@ const withTheme: DecoratorFn = (StoryFn) => {
return (
<>
-
+ {StoryFn()}
>
);
};
diff --git a/packages/epo-react-lib/package.json b/packages/epo-react-lib/package.json
index 791b0a4f..968459a6 100644
--- a/packages/epo-react-lib/package.json
+++ b/packages/epo-react-lib/package.json
@@ -48,6 +48,7 @@
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.18.6",
"@castiron/style-mixins": "^1.0.6",
+ "@headlessui/react": "^1.7.5",
"@storybook/addon-actions": "^6.5.12",
"@storybook/addon-essentials": "^6.5.12",
"@storybook/addon-interactions": "^6.5.12",
diff --git a/packages/epo-react-lib/src/index.ts b/packages/epo-react-lib/src/index.ts
index 257a8c9a..a560a094 100644
--- a/packages/epo-react-lib/src/index.ts
+++ b/packages/epo-react-lib/src/index.ts
@@ -24,3 +24,5 @@ export * from "@/atomic/Share";
export { default as Columns } from "@/layout/Columns";
export { default as Container } from "@/layout/Container";
export { default as Grid } from "@/layout/Grid";
+export { default as MasonryGrid } from "@/layout/MasonryGrid";
+export { default as BasicModal } from "@/layout/BasicModal";
diff --git a/packages/epo-react-lib/src/layout/BasicModal/BasicModal.stories.tsx b/packages/epo-react-lib/src/layout/BasicModal/BasicModal.stories.tsx
new file mode 100644
index 00000000..f1a13105
--- /dev/null
+++ b/packages/epo-react-lib/src/layout/BasicModal/BasicModal.stories.tsx
@@ -0,0 +1,108 @@
+import {
+ ComponentMeta,
+ ComponentStoryObj,
+ ComponentStory,
+} from "@storybook/react";
+import { objChildren } from "@/storybook/utilities/argTypes";
+
+import BasicModal from ".";
+import Button from "@/atomic/Button";
+import { useState } from "react";
+
+const meta: ComponentMeta = {
+ component: BasicModal,
+ argTypes: {
+ children: objChildren,
+ open: {
+ type: "boolean",
+ description: "Determines if the modal is visible or not.",
+ table: {
+ type: {
+ summary: "boolean",
+ },
+ defaultValue: {
+ summary: false,
+ },
+ },
+ },
+ darkMode: {
+ type: "boolean",
+ description: "Sets the dark mode theme.",
+ table: {
+ type: {
+ summary: "boolean",
+ },
+ defaultValue: {
+ summary: false,
+ },
+ },
+ },
+ title: {
+ type: "string",
+ description: "Modal title displayed at the top.",
+ table: {
+ type: {
+ summary: "string",
+ },
+ },
+ },
+ description: {
+ type: "string",
+ description: "Modal description displayed after the title.",
+ table: {
+ type: {
+ summary: "string",
+ },
+ },
+ },
+ onClose: {
+ type: "function",
+ action: "Closed",
+ description:
+ "Callback either when user clicks outside the modal or clicks the close button. Close button will attach an event, click outside will not.",
+ table: {
+ type: {
+ summary: "(event?: MouseEvent) => void",
+ },
+ },
+ },
+ },
+};
+export default meta;
+
+const Template: ComponentStory = ({
+ open,
+ title,
+ description,
+ darkMode,
+ children,
+}) => {
+ const [isOpen, setIsOpen] = useState(open || false);
+
+ return (
+ <>
+
+ setIsOpen(false)}
+ {...{ title, description, darkMode }}
+ >
+ {children}
+
+ >
+ );
+};
+
+export const Primary: ComponentStoryObj = Template.bind({});
+Primary.args = {
+ title: "Modal Title",
+ description: "Modal description",
+ children:
+ "Cosmic ipsum universe right ascension pole star solstice cosmic rays extragalactic black body NASA cluster muttnik synodic superior planets gravitational constant new moon telescope inferior planets syzygy perturbation falling star quasar red dwarf satellite density day dust vernal equinox zodiac inclination azimuth weightlessness spectrum variable star magnitude flare Mir minor planet transparency cosmology full moon terrestrial quarter moon red shift seeing gravity binary star red giant star space station local group",
+};
+
+export const DarkMode: ComponentStoryObj = Template.bind({});
+DarkMode.args = {
+ ...Primary.args,
+ darkMode: true,
+};
diff --git a/packages/epo-react-lib/src/layout/BasicModal/BasicModal.test.tsx b/packages/epo-react-lib/src/layout/BasicModal/BasicModal.test.tsx
new file mode 100644
index 00000000..c8684547
--- /dev/null
+++ b/packages/epo-react-lib/src/layout/BasicModal/BasicModal.test.tsx
@@ -0,0 +1,39 @@
+import { render, screen, fireEvent } from "@testing-library/react";
+import BasicModal from ".";
+
+const props = {
+ children:
+ "Cosmic ipsum universe right ascension pole star solstice cosmic rays extragalactic black body NASA cluster muttnik synodic superior planets gravitational constant new moon telescope inferior planets syzygy perturbation falling star quasar red dwarf satellite density day dust vernal equinox zodiac inclination azimuth weightlessness spectrum variable star magnitude flare Mir minor planet transparency cosmology full moon terrestrial quarter moon red shift seeing gravity binary star red giant star space station local group",
+ open: true,
+ onClose: jest.fn(),
+ title: "Title",
+ description: "Description",
+};
+
+beforeEach(() => {
+ // IntersectionObserver isn't available in test environment
+ const mockIntersectionObserver = jest.fn();
+ mockIntersectionObserver.mockReturnValue({
+ observe: () => null,
+ unobserve: () => null,
+ disconnect: () => null,
+ });
+ window.IntersectionObserver = mockIntersectionObserver;
+});
+
+describe("BasicModal", () => {
+ it("should create a modal dialog", () => {
+ render();
+
+ const dialog = screen.getByRole("dialog");
+ expect(dialog).toBeDefined();
+ expect(dialog).toHaveAttribute("aria-modal", "true");
+ });
+ it("should fire a callback on close", () => {
+ render();
+
+ const closeButton = screen.getByRole("button");
+ fireEvent.click(closeButton);
+ expect(props.onClose).toBeCalled();
+ });
+});
diff --git a/packages/epo-react-lib/src/layout/BasicModal/BasicModal.tsx b/packages/epo-react-lib/src/layout/BasicModal/BasicModal.tsx
new file mode 100644
index 00000000..f894c5c8
--- /dev/null
+++ b/packages/epo-react-lib/src/layout/BasicModal/BasicModal.tsx
@@ -0,0 +1,43 @@
+import IconComposer from "@/svg/IconComposer";
+import * as Styled from "./styles";
+import { FunctionComponent, MouseEvent, ReactNode } from "react";
+
+interface BasicModalProps {
+ children: ReactNode;
+ open?: boolean;
+ onClose: (event?: MouseEvent) => void;
+ darkMode?: boolean;
+ title?: string;
+ description?: string;
+}
+
+const BasicModal: FunctionComponent = ({
+ children,
+ open = false,
+ onClose,
+ darkMode = false,
+ title,
+ description,
+}) => {
+ return (
+ onClose()}>
+
+
+
+
+
+
+ {title && {title}}
+ {description && (
+ {description}
+ )}
+ {children}
+
+
+
+ );
+};
+
+BasicModal.displayName = "Layout.Modal";
+
+export default BasicModal;
diff --git a/packages/epo-react-lib/src/layout/BasicModal/index.ts b/packages/epo-react-lib/src/layout/BasicModal/index.ts
new file mode 100644
index 00000000..fe36bdeb
--- /dev/null
+++ b/packages/epo-react-lib/src/layout/BasicModal/index.ts
@@ -0,0 +1 @@
+export { default } from "./BasicModal";
diff --git a/packages/epo-react-lib/src/layout/BasicModal/styles.ts b/packages/epo-react-lib/src/layout/BasicModal/styles.ts
new file mode 100644
index 00000000..47636a5b
--- /dev/null
+++ b/packages/epo-react-lib/src/layout/BasicModal/styles.ts
@@ -0,0 +1,89 @@
+import styled from "styled-components";
+import { fluidScale, zStack } from "@/styles/globalStyles";
+import { Dialog as BaseDialog } from "@headlessui/react";
+import BaseFormButtons from "@/form/FormButtons";
+import { ReactNode } from "react";
+import { protoButton } from "@/styles/mixins/appearance";
+
+interface InnerProps {
+ $darkMode: boolean;
+}
+
+interface DialogProps {
+ open: boolean;
+ onClose: () => void;
+ children: ReactNode;
+}
+
+export const Overlay = styled(BaseDialog.Overlay)`
+ background-color: rgba(0, 0, 0, 0.7);
+ position: fixed;
+ top: 0;
+ width: 100%;
+ height: 100%;
+`;
+
+export const Dialog = styled(BaseDialog)`
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: ${zStack.dialog};
+ overflow: auto;
+ padding: 1rem;
+ display: flex;
+ align-items: start;
+ justify-content: center;
+`;
+
+export const Title = BaseDialog.Title;
+
+export const Description = styled(BaseDialog.Description)`
+ font-size: 0.909em;
+ line-height: 1.5;
+
+ &:not(:first-child) {
+ margin-block-start: 1.4em;
+ }
+`;
+
+export const Inner = styled.div`
+ position: relative;
+ display: flex;
+ background: var(--white);
+ max-width: 100vw;
+
+ ${({ $darkMode }) =>
+ $darkMode &&
+ `
+ color: #fff;
+ background-color: #161818;
+ `}
+`;
+
+export const Content = styled.div`
+ flex: 1 1 auto;
+ padding: ${fluidScale("60px", "15px")};
+ padding-block-start: 60px;
+ width: calc(100vw - 30px);
+ max-width: 550px;
+`;
+
+export const CloseButton = styled.button`
+ ${protoButton()}
+ position: absolute;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ right: ${fluidScale("10px", "15px")};
+ top: ${fluidScale("10px", "15px")};
+ width: 42px;
+ height: 42px;
+ border-radius: 100%;
+ background: var(--neutral10);
+`;
+
+export const FormButtons = styled(BaseFormButtons)`
+ margin-block-start: 44px;
+`;