diff --git a/packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx b/packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx index a68261c..8ccb379 100644 --- a/packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx +++ b/packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx @@ -32,6 +32,8 @@ import { TagStyles } from '../Tag/Tag.styles' import { TextFieldStyles } from '../TextField/TextField.styles' import { defaultThemes, Theme, withTheme } from '../Theme' import { TypographyStyles } from '../Typography/Typography.styles' +import { ModalStyles } from '../Modal/Modal.styles' +import { ModalBodyStyles } from '../ModalBody/ModalBody.styles' const componentStyles: Array | SerializedStyles> = [ @@ -66,6 +68,8 @@ const componentStyles: Array | SerializedStyles> = TableBodyStyles, TableItemStyles, TableRowStyles, + ModalStyles, + ModalBodyStyles, ] export const CSSBaseline: React.FC<{ theme?: Theme }> = ({ diff --git a/packages/lsd-react/src/components/Modal/Modal.classes.ts b/packages/lsd-react/src/components/Modal/Modal.classes.ts new file mode 100644 index 0000000..0d6ab9e --- /dev/null +++ b/packages/lsd-react/src/components/Modal/Modal.classes.ts @@ -0,0 +1,18 @@ +export const modalClasses = { + root: `lsd-modal`, + + small: 'lsd-modal--small', + medium: 'lsd-modal--medium', + large: 'lsd-modal--large', + extraSmall: 'lsd-modal--extra-small', + + modalContainer: 'lsd-modal__container', + + header: 'lsd-modal__header', + title: 'lsd-modal__title', + subtitle: 'lsd-modal__subtitle', + + titleAndSubtitleContainer: 'lsd-modal__title-and-subtitle-container', + + closeIcon: 'lsd-modal__close-icon', +} diff --git a/packages/lsd-react/src/components/Modal/Modal.stories.tsx b/packages/lsd-react/src/components/Modal/Modal.stories.tsx new file mode 100644 index 0000000..50e6640 --- /dev/null +++ b/packages/lsd-react/src/components/Modal/Modal.stories.tsx @@ -0,0 +1,55 @@ +import React, { useState } from 'react' +import { Meta, Story } from '@storybook/react' +import { ModalBody } from '../ModalBody' +import { Modal, ModalProps } from './Modal' +import { Button } from '../Button' + +export default { + title: 'Modal', + component: Modal, + argTypes: { + size: { + type: { + name: 'enum', + value: ['extraSmall', 'small', 'medium', 'large'], + }, + }, + }, +} as Meta + +export const Root: Story< + ModalProps & { + body: string + } +> = ({ body, ...args }) => { + const [isOpen, setIsOpen] = useState(false) // state to handle modal open/close + + return ( +
+ + setIsOpen(false)}> + {body} +
+ + +
+
+
+ ) +} + +Root.args = { + size: 'large', + title: 'Title', + subtitle: 'Subtitle goes here', + body: 'Content goes here.', +} diff --git a/packages/lsd-react/src/components/Modal/Modal.styles.ts b/packages/lsd-react/src/components/Modal/Modal.styles.ts new file mode 100644 index 0000000..9580fed --- /dev/null +++ b/packages/lsd-react/src/components/Modal/Modal.styles.ts @@ -0,0 +1,74 @@ +import { css } from '@emotion/react' +import { modalClasses } from './Modal.classes' + +export const ModalStyles = css` + .${modalClasses.root} { + box-sizing: border-box; + display: flex; + flex-direction: column; + + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); /* semi-transparent overlay */ + display: flex; + justify-content: center; + align-items: center; + + z-index: 9999; + } + + .${modalClasses.modalContainer} { + background: rgb(var(--lsd-surface-primary)); + padding: 20px; + + max-width: 90%; + } + + .${modalClasses.header} { + display: flex; + justify-content: space-between; + align-items: center; + } + + .${modalClasses.title} { + } + + .${modalClasses.subtitle} { + } + + .${modalClasses.closeIcon} { + cursor: pointer; + } + + .${modalClasses.titleAndSubtitleContainer} { + display: flex; + flex-direction: column; + } + + .${modalClasses.large} { + .${modalClasses.modalContainer} { + min-width: 960px; + } + } + + .${modalClasses.medium} { + .${modalClasses.modalContainer} { + min-width: 768px; + } + } + + .${modalClasses.small} { + .${modalClasses.modalContainer} { + min-width: 614px; + } + } + + .${modalClasses.extraSmall} { + .${modalClasses.modalContainer} { + min-width: 490px; + } + } +` diff --git a/packages/lsd-react/src/components/Modal/Modal.tsx b/packages/lsd-react/src/components/Modal/Modal.tsx new file mode 100644 index 0000000..faa1060 --- /dev/null +++ b/packages/lsd-react/src/components/Modal/Modal.tsx @@ -0,0 +1,94 @@ +import clsx from 'clsx' +import React from 'react' +import { + CommonProps, + omitCommonProps, + useCommonProps, +} from '../../utils/useCommonProps' +import { modalClasses } from './Modal.classes' +import { CloseIcon } from '../Icons' +import { Typography } from '../Typography' +import { IconButton } from '../IconButton' + +export type ModalProps = CommonProps & + Omit, 'label'> & { + isOpen: boolean // new prop to control visibility + size?: 'extraSmall' | 'small' | 'medium' | 'large' + title?: string + subtitle?: string + onClose?: () => void + } + +export const Modal: React.FC & { + classes: typeof modalClasses +} = ({ + isOpen, + size = 'large', + title, + subtitle, + onClose, + children, + ...props +}) => { + const commonProps = useCommonProps(props) + + // Close modal when overlay is clicked + const handleOverlayClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget && onClose) { + onClose() + } + } + + if (!isOpen) { + return null + } + + return ( +
+
+
+
+ {!!title && ( + + {title} + + )} + + {!!subtitle && ( + + {subtitle} + + )} +
+ + + + +
+ {children} +
+
+ ) +} + +Modal.classes = modalClasses diff --git a/packages/lsd-react/src/components/Modal/index.ts b/packages/lsd-react/src/components/Modal/index.ts new file mode 100644 index 0000000..8d3bcd7 --- /dev/null +++ b/packages/lsd-react/src/components/Modal/index.ts @@ -0,0 +1 @@ +export * from './Modal' diff --git a/packages/lsd-react/src/components/ModalBody/ModalBody.classes.ts b/packages/lsd-react/src/components/ModalBody/ModalBody.classes.ts new file mode 100644 index 0000000..e33da6b --- /dev/null +++ b/packages/lsd-react/src/components/ModalBody/ModalBody.classes.ts @@ -0,0 +1,3 @@ +export const modalBodyClasses = { + root: `lsd-modal-body`, +} diff --git a/packages/lsd-react/src/components/ModalBody/ModalBody.stories.tsx b/packages/lsd-react/src/components/ModalBody/ModalBody.stories.tsx new file mode 100644 index 0000000..5dcff29 --- /dev/null +++ b/packages/lsd-react/src/components/ModalBody/ModalBody.stories.tsx @@ -0,0 +1,30 @@ +import { Meta, Story } from '@storybook/react' +import { ModalBody, ModalBodyProps } from './ModalBody' + +export default { + title: 'ModalBody', + component: ModalBody, + argTypes: { + size: { + table: { + disable: true, + }, + }, + }, +} as Meta + +export const Root: Story< + ModalBodyProps & { + body: string + title: string + } +> = ({ body, ...args }) => ( +
+ {body} +
+) + +Root.args = { + title: 'Title', + body: 'A wise man can learn more from a foolish question than a fool can learn from a wise answer.', +} diff --git a/packages/lsd-react/src/components/ModalBody/ModalBody.styles.ts b/packages/lsd-react/src/components/ModalBody/ModalBody.styles.ts new file mode 100644 index 0000000..3f3a844 --- /dev/null +++ b/packages/lsd-react/src/components/ModalBody/ModalBody.styles.ts @@ -0,0 +1,16 @@ +import { css } from '@emotion/react' +import { modalBodyClasses } from './ModalBody.classes' + +export const ModalBodyStyles = css` + .${modalBodyClasses.root} { + box-sizing: border-box; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + border: 1px solid rgb(var(--lsd-border-primary)); + padding: 30px; + margin: 18px 0; + } +` diff --git a/packages/lsd-react/src/components/ModalBody/ModalBody.tsx b/packages/lsd-react/src/components/ModalBody/ModalBody.tsx new file mode 100644 index 0000000..5571928 --- /dev/null +++ b/packages/lsd-react/src/components/ModalBody/ModalBody.tsx @@ -0,0 +1,28 @@ +import clsx from 'clsx' +import React from 'react' +import { + CommonProps, + omitCommonProps, + useCommonProps, +} from '../../utils/useCommonProps' +import { modalBodyClasses } from './ModalBody.classes' + +export type ModalBodyProps = CommonProps & + Omit, 'label'> + +export const ModalBody: React.FC & { + classes: typeof modalBodyClasses +} = ({ children, ...props }) => { + const commonProps = useCommonProps(props) + + return ( +
+ {children} +
+ ) +} + +ModalBody.classes = modalBodyClasses diff --git a/packages/lsd-react/src/components/ModalBody/index.ts b/packages/lsd-react/src/components/ModalBody/index.ts new file mode 100644 index 0000000..dbac786 --- /dev/null +++ b/packages/lsd-react/src/components/ModalBody/index.ts @@ -0,0 +1 @@ +export * from './ModalBody' diff --git a/packages/lsd-react/src/index.ts b/packages/lsd-react/src/index.ts index 31605ee..e50a009 100644 --- a/packages/lsd-react/src/index.ts +++ b/packages/lsd-react/src/index.ts @@ -30,3 +30,5 @@ export * from './components/Tag' export * from './components/TextField' export * from './components/Theme' export * from './components/Typography' +export * from './components/Modal' +export * from './components/ModalBody'