diff --git a/src/components/Modal/Layouts/CallToActionModalContent.tsx b/src/components/Modal/Layouts/CallToActionModalContent.tsx
new file mode 100644
index 0000000..8aa63ad
--- /dev/null
+++ b/src/components/Modal/Layouts/CallToActionModalContent.tsx
@@ -0,0 +1,21 @@
+export interface CallToActionModalContentProps {
+ title: React.ReactNode;
+ content: React.ReactNode;
+ button: React.ReactNode;
+}
+
+export const CallToActionModalContent = ({
+ title,
+ content,
+ button,
+}: CallToActionModalContentProps) => {
+ return (
+
+ );
+};
diff --git a/src/components/Modal/Layouts/index.ts b/src/components/Modal/Layouts/index.ts
new file mode 100644
index 0000000..8568e68
--- /dev/null
+++ b/src/components/Modal/Layouts/index.ts
@@ -0,0 +1,4 @@
+export {
+ CallToActionModalContent as CallToAction,
+ type CallToActionModalContentProps as CallToActionProps,
+} from "./CallToActionModalContent";
diff --git a/src/components/Modal/Modal.stories.tsx b/src/components/Modal/Modal.stories.tsx
new file mode 100644
index 0000000..bd685a4
--- /dev/null
+++ b/src/components/Modal/Modal.stories.tsx
@@ -0,0 +1,125 @@
+import type { Meta, StoryObj } from "@storybook/react";
+
+import { Modal, ModalProps } from "./Modal";
+import { Button } from "../Button";
+import { ModalProvider } from "./ModalContext";
+import { useModalContext } from "./ModalContext";
+import { fn } from "@storybook/test";
+import { ModalLayout } from ".";
+
+const meta: Meta = {
+ title: "Components/Modal",
+ decorators: [
+ (Story, { args }) => {
+ function ModalContent() {
+ const { isModalOpen, openModal } = useModalContext(args.id);
+ return (
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+ },
+ ],
+ component: Modal,
+ parameters: {
+ backgrounds: {
+ default: "container",
+ },
+ },
+ tags: ["autodocs"],
+ argTypes: {},
+ args: {
+ id: "modal",
+ title: "Example modal",
+ hasBackdrop: false,
+ onClose: fn(),
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+const ModalContent = ({
+ closeModal,
+}: {
+ closeModal: (success: boolean) => void;
+}) => {
+ return (
+ closeModal(true)}
+ >
+ Let's go
+
+ }
+ />
+ );
+};
+
+export const Simple: Story = {
+ args: {
+ children: ModalContent,
+ },
+};
+
+export const Nested: Story = {
+ decorators: [
+ (Story) => {
+ return (
+ <>
+
+
+ {({ closeModal }) => (
+ closeModal()}
+ >
+ Close Nested
+
+ }
+ />
+ )}
+
+ >
+ );
+ },
+ ],
+ args: {
+ children: () => {
+ const { openModal } = useModalContext("nested");
+ return (
+
+
+
+ );
+ },
+ },
+};
diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx
new file mode 100644
index 0000000..7e10e4b
--- /dev/null
+++ b/src/components/Modal/Modal.tsx
@@ -0,0 +1,108 @@
+import {
+ Dialog,
+ DialogBackdrop,
+ DialogPanel,
+ DialogTitle,
+} from "@headlessui/react";
+import { useModalContext } from "./ModalContext";
+import {
+ classNames,
+ resetClasses,
+ variantClassNames,
+} from "../../util/classes";
+import { InkIcon } from "../..";
+import { useEffect, useRef } from "react";
+
+export interface ModalProps {
+ id: string;
+ title?: string;
+ size?: "lg" | "md";
+ hasBackdrop?: boolean;
+ openOnMount?: boolean;
+ onClose?: (props?: TOnCloseProps) => void;
+ children: ({
+ closeModal,
+ }: {
+ closeModal: (props?: TOnCloseProps) => void;
+ }) => React.ReactNode;
+}
+
+export const Modal = ({
+ id,
+ title,
+ size = "lg",
+ hasBackdrop,
+ openOnMount,
+ onClose,
+ children,
+}: ModalProps) => {
+ const { isModalOpen, closeModal, modalIndex, openModal } =
+ useModalContext(id);
+
+ const wasOpenedOnMount = useRef(false);
+ useEffect(() => {
+ if (openOnMount && !wasOpenedOnMount.current) {
+ openModal();
+ wasOpenedOnMount.current = true;
+ }
+ }, [openModal, openOnMount]);
+
+ const handleClose = (props?: TOnCloseProps) => {
+ closeModal();
+ onClose?.(props);
+ };
+
+ return (
+ <>
+
+ >
+ );
+};
diff --git a/src/components/Modal/ModalContext.tsx b/src/components/Modal/ModalContext.tsx
new file mode 100644
index 0000000..e4e2f93
--- /dev/null
+++ b/src/components/Modal/ModalContext.tsx
@@ -0,0 +1,73 @@
+import { createContext, useContext, useMemo, useState } from "react";
+
+export interface ModalManagementContextProps {
+ openModals: string[];
+ openModal: (id: string) => void;
+ closeModal: (id: string) => void;
+ isModalOpen: (id: string) => boolean;
+ closeAllModals: () => void;
+ getModalIndex: (id: string) => number;
+}
+
+export const ModalManagementContext =
+ createContext({
+ openModals: [],
+ openModal: () => {},
+ closeModal: () => {},
+ isModalOpen: () => false,
+ closeAllModals: () => {},
+ getModalIndex: () => 0,
+ });
+
+export interface ModalContextProps {
+ openModal: () => void;
+ closeModal: () => void;
+ isModalOpen: boolean;
+ modalIndex: number;
+}
+
+export const useModalContext = (id: string): ModalContextProps => {
+ const { openModals, openModal, closeModal, isModalOpen, getModalIndex } =
+ useContext(ModalManagementContext);
+ return useMemo(
+ () => ({
+ openModal: () => openModal(id),
+ closeModal: () => closeModal(id),
+ isModalOpen: isModalOpen(id),
+ modalIndex: getModalIndex(id),
+ }),
+ [id, openModals]
+ );
+};
+
+export const useModalManagementContext = (): ModalManagementContextProps => {
+ return useContext(ModalManagementContext);
+};
+
+export const ModalProvider = ({ children }: { children: React.ReactNode }) => {
+ const [openModals, setOpenModals] = useState([]);
+
+ const modalManagementContext: ModalManagementContextProps = useMemo(
+ () => ({
+ openModals,
+ openModal: (id: string) => {
+ setOpenModals((prev) => (prev.includes(id) ? prev : [...prev, id]));
+ },
+ closeModal: (id: string) => {
+ setOpenModals((prev) => prev.filter((modalId) => modalId !== id));
+ },
+ isModalOpen: (id: string) => openModals.includes(id),
+ closeAllModals: () => {
+ setOpenModals([]);
+ },
+ getModalIndex: (id: string) => openModals.indexOf(id),
+ }),
+ [openModals, setOpenModals]
+ );
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/components/Modal/index.ts b/src/components/Modal/index.ts
new file mode 100644
index 0000000..730b8bd
--- /dev/null
+++ b/src/components/Modal/index.ts
@@ -0,0 +1,3 @@
+export * from "./ModalContext";
+export * from "./Modal";
+export * as ModalLayout from "./Layouts";
diff --git a/src/components/index.ts b/src/components/index.ts
index 7331bcc..ba153d8 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -1,3 +1,4 @@
export * from "./Button";
+export * from "./Modal";
export * from "./SegmentedControl";
export * from "./Wallet";
diff --git a/src/styles/shadow.css b/src/styles/shadow.css
index adeb0a7..4939815 100644
--- a/src/styles/shadow.css
+++ b/src/styles/shadow.css
@@ -1,3 +1,4 @@
:root {
--ink-box-shadow-menu: 0px 8px 24px -8px #160f1f1a;
+ --ink-box-shadow-modal: 0px 16px 64px -32px #160f1f1a;
}
diff --git a/tailwind.config.mjs b/tailwind.config.mjs
index ee4d30f..4c7734f 100644
--- a/tailwind.config.mjs
+++ b/tailwind.config.mjs
@@ -1,4 +1,5 @@
export const spacing = {
+ 0: "0px",
0.5: "var(--ink-spacing-4)",
1: "var(--ink-spacing-8)",
1.5: "var(--ink-spacing-12)",
@@ -92,6 +93,7 @@ const config = {
},
boxShadow: {
menu: "var(--ink-box-shadow-menu)",
+ modal: "var(--ink-box-shadow-modal)",
},
},
plugins: [],