diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index b31070a..8c9c2b3 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -24,6 +24,7 @@ export default function Modal({ ariaLabelledby, ariaDescribedby, className, + contentClassName, closeOnOverlayClick = true, closeOnEscape = true, }: ModalProps) { @@ -55,7 +56,7 @@ export default function Modal({ role="presentation" >
.modalContent { + width: 384px; + height: 235px; + display: inline-flex; + padding: 16px 16px 32px; + flex-direction: column; + align-items: flex-start; + gap: 10px; + border-radius: 24px; + background: var(--Background-Primary, #fff); + box-sizing: border-box; +} + +.container { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: stretch; + gap: 10px; + box-sizing: border-box; +} + +.buttonContainer { + display: flex; + align-items: center; + justify-content: flex-end; + margin-right: 8px; +} + +.header { + display: flex; + width: 100%; + align-items: center; + justify-content: center; + gap: 12px; +} + +.title { + margin: 0; + color: var(--Text-Primary, #1e293b); + font-family: Pretendard; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 19px; +} + +.closeButton { + width: 24px; + height: 24px; + padding: 0; + border: none; + background: transparent; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.buttonContainer .closeButton, +.buttonContainer .closeButton:hover:not(:disabled), +.buttonContainer .closeButton:active:not(:disabled) { + border: none; + background: transparent; + color: inherit; +} + +.form { + display: flex; + flex: 1; + min-height: 0; + flex-direction: column; + gap: 24px; +} + +.form .input { + display: flex; + width: 280px; + height: 48px; + padding: 16px; + align-items: center; + gap: 10px; + border-radius: 12px; + border: 1px solid var(--Border-Primary, #e2e8f0); + background: var(--Background-Primary, #fff); + box-sizing: border-box; + margin: 0 auto; +} + +.form .input::placeholder { + color: var(--Text-Default, #64748b); + font-family: Pretendard; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 19px; +} + +.footer { + display: flex; + justify-content: center; + margin-top: auto; +} + +.button { + display: flex; + width: 280px; + height: 48px; + justify-content: center; + align-items: center; + gap: 10px; + flex-shrink: 0; + border: none; + border-radius: 12px; + background: var(--Color-Brand-Primary, #5189fa); + color: var(--Text-inverse, #fff); + text-align: center; + font-family: Pretendard; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 19px; +} + +@media (max-width: 480px) { + section > .modalContent { + border-radius: 24px 24px 0 0; + } +} diff --git a/src/components/Modal/domain/components/AddTodoList/AddTodoList.tsx b/src/components/Modal/domain/components/AddTodoList/AddTodoList.tsx new file mode 100644 index 0000000..934aa92 --- /dev/null +++ b/src/components/Modal/domain/components/AddTodoList/AddTodoList.tsx @@ -0,0 +1,90 @@ +'use client'; + +import Image from 'next/image'; +import type { FormEvent } from 'react'; +import BaseButton from '@/components/Button/base/BaseButton'; +import { Input } from '@/components/input'; +import Modal from '../../../Modal'; +import styles from './AddTodoList.module.css'; +import xMarkBig from '@/assets/icons/xMark/xMarkBig.svg'; +import { + CLOSE_BUTTON_ARIA_LABEL, + DEFAULT_PLACEHOLDER, + DEFAULT_SUBMIT_LABEL, + DEFAULT_TITLE, + TITLE_ID, +} from './AddTodoList.constants'; +import type { AddTodoListProps } from './AddTodoList.types'; +export type { AddTodoListProps } from './AddTodoList.types'; + +/** + * @param props.isOpen 모달 표시 여부를 boolean으로 전달합니다. + * @param props.onClose 모달을 닫을 때 실행할 함수를 전달합니다. + * @param props.onSubmit 할 일 생성 버튼 클릭 시 실행할 함수를 전달합니다. + * @param props.text 모달 제목과 버튼 문구 같은 텍스트 옵션을 객체로 전달합니다. + * @param props.input 할 일 입력창에 적용할 옵션을 객체로 전달합니다. + * @param props.closeOptions 오버레이 클릭과 Escape 닫힘 옵션을 객체로 전달합니다. + */ +export default function AddTodoList({ + isOpen, + onClose, + onSubmit, + text, + input, + closeOptions, +}: AddTodoListProps) { + const title = text?.title ?? DEFAULT_TITLE; + const submitLabel = text?.submitLabel ?? DEFAULT_SUBMIT_LABEL; + const inputPlaceholder = text?.inputPlaceholder ?? DEFAULT_PLACEHOLDER; + const closeOnOverlayClick = closeOptions?.overlayClick ?? true; + const closeOnEscape = closeOptions?.escape ?? true; + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + onSubmit(); + }; + + return ( + +
+
+ + + +
+
+

+ {title} +

+
+ +
+ +
+ + {submitLabel} + +
+
+
+
+ ); +} diff --git a/src/components/Modal/domain/components/AddTodoList/AddTodoList.types.ts b/src/components/Modal/domain/components/AddTodoList/AddTodoList.types.ts new file mode 100644 index 0000000..2833191 --- /dev/null +++ b/src/components/Modal/domain/components/AddTodoList/AddTodoList.types.ts @@ -0,0 +1,20 @@ +import type { InputProps } from '@/components/input/types/types'; +import type { BaseDomainModalProps } from '../../types/types'; + +export type TodoInputProps = Omit; + +export interface AddTodoListTextOptions { + title?: string; + submitLabel?: string; + inputPlaceholder?: string; +} + +export interface AddTodoListInputOptions { + props?: TodoInputProps; +} + +export interface AddTodoListProps extends BaseDomainModalProps { + onSubmit: () => void; + text?: AddTodoListTextOptions; + input?: AddTodoListInputOptions; +} diff --git a/src/components/Modal/domain/components/ChangePassword/ChangePassword.constants.ts b/src/components/Modal/domain/components/ChangePassword/ChangePassword.constants.ts new file mode 100644 index 0000000..752fa1b --- /dev/null +++ b/src/components/Modal/domain/components/ChangePassword/ChangePassword.constants.ts @@ -0,0 +1,10 @@ +export const TITLE_ID = 'change-password-title'; +export const NEW_PASSWORD_NAME = 'newPassword'; +export const CONFIRM_PASSWORD_NAME = 'confirmPassword'; +export const DEFAULT_TITLE = '비밀번호 변경하기'; +export const DEFAULT_NEW_PASSWORD_LABEL = '새 비밀번호'; +export const DEFAULT_CONFIRM_PASSWORD_LABEL = '새 비밀번호 확인'; +export const DEFAULT_NEW_PASSWORD_PLACEHOLDER = '새 비밀번호를 입력해주세요.'; +export const DEFAULT_CONFIRM_PASSWORD_PLACEHOLDER = '새 비밀번호를 다시 한 번 입력해주세요.'; +export const DEFAULT_CLOSE_LABEL = '닫기'; +export const DEFAULT_SUBMIT_LABEL = '변경하기'; diff --git a/src/components/Modal/domain/components/ChangePassword/ChangePassword.module.css b/src/components/Modal/domain/components/ChangePassword/ChangePassword.module.css new file mode 100644 index 0000000..05f3488 --- /dev/null +++ b/src/components/Modal/domain/components/ChangePassword/ChangePassword.module.css @@ -0,0 +1,132 @@ +section > .modalContent { + display: flex; + width: 384px; + height: 353px; + padding: 16px 16px 32px; + flex-direction: column; + align-items: stretch; + gap: 10px; + border-radius: 12px; + background: var(--Background-Primary, #fff); + box-sizing: border-box; +} + +.container { + width: 100%; + height: 100%; + padding-top: 24px; + display: flex; + flex-direction: column; + gap: 24px; + box-sizing: border-box; +} + +.title { + margin: 0; + width: 100%; + color: var(--Text-Primary, #1e293b); + text-align: center; + font-family: Pretendard; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 19px; +} + +.form { + display: flex; + flex-direction: column; + gap: 16px; + flex: 1; + min-height: 0; +} + +.field { + display: flex; + width: 280px; + margin: 0 auto; + flex-direction: column; + gap: 8px; +} + +.label { + color: var(--Text-Primary, #1e293b); + font-family: Pretendard; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 19px; +} + +.field .input { + display: flex; + width: 280px; + height: 48px; + padding: 16px; + align-items: center; + gap: 10px; + border-radius: 12px; + border: 1px solid var(--Border-Primary, #e2e8f0); + background: var(--Background-Primary, #fff); + box-sizing: border-box; +} + +.field .input::placeholder { + color: var(--Text-Default, #64748b); + font-family: Pretendard; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 19px; +} + +.actions { + display: flex; + width: 280px; + margin: auto auto 0; + gap: 8px; +} + +.closeButton { + display: flex; + width: 136px; + height: 48px; + justify-content: center; + align-items: center; + gap: 10px; + border-radius: 12px; + border: 1px solid var(--Color-Brand-Primary, #5189fa); + background: var(--Background-Primary, #fff); + color: var(--Color-Brand-Primary, #5189fa); + font-family: Pretendard; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 19px; + cursor: pointer; +} + +.submitButton { + display: flex; + width: 136px; + height: 48px; + justify-content: center; + align-items: center; + gap: 10px; + border: none; + border-radius: 12px; + background: var(--Color-Brand-Primary, #5189fa); + color: var(--Text-Inverse, #fff); + font-family: Pretendard; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 19px; + cursor: pointer; +} + +@media (max-width: 480px) { + section > .modalContent { + border-radius: 12px 12px 0 0; + } +} diff --git a/src/components/Modal/domain/components/ChangePassword/ChangePassword.tsx b/src/components/Modal/domain/components/ChangePassword/ChangePassword.tsx new file mode 100644 index 0000000..8aacd0b --- /dev/null +++ b/src/components/Modal/domain/components/ChangePassword/ChangePassword.tsx @@ -0,0 +1,117 @@ +'use client'; + +import { useId } from 'react'; + +import Modal from '../../../Modal'; +import BaseButton from '@/components/Button/base/BaseButton'; +import { Input } from '@/components/input'; +import styles from './ChangePassword.module.css'; +import { CONFIRM_PASSWORD_NAME, NEW_PASSWORD_NAME, TITLE_ID } from './ChangePassword.constants'; +import type { ChangePasswordProps } from './ChangePassword.types'; +import { + createSubmitHandler, + resolveChangePasswordText, + resolveCloseOptions, + resolvePasswordInputIds, +} from './ChangePassword.utils'; +export type { ChangePasswordProps } from './ChangePassword.types'; + +/** + * @param props.isOpen 모달 표시 여부를 boolean으로 전달합니다. + * @param props.onClose 모달을 닫을 때 실행할 함수를 전달합니다. + * @param props.onSubmit 비밀번호 변경 제출 시 실행할 함수를 전달합니다. + * @param props.text 제목과 버튼 문구와 라벨 같은 텍스트 옵션을 객체로 전달합니다. + * @param props.input 비밀번호 입력창들에 적용할 옵션을 객체로 전달합니다. + * @param props.closeOptions 오버레이 클릭과 Escape 닫힘 옵션을 객체로 전달합니다. + */ +export default function ChangePassword({ + isOpen, + onClose, + onSubmit, + text, + input, + closeOptions, +}: ChangePasswordProps) { + const { + title, + newPasswordLabel, + confirmPasswordLabel, + newPasswordPlaceholder, + confirmPasswordPlaceholder, + closeLabel, + submitLabel, + } = resolveChangePasswordText(text); + const { closeOnOverlayClick, closeOnEscape } = resolveCloseOptions(closeOptions); + + const generatedNewPasswordId = useId(); + const generatedConfirmPasswordId = useId(); + const { newPasswordId, confirmPasswordId } = resolvePasswordInputIds( + input, + generatedNewPasswordId, + generatedConfirmPasswordId, + ); + const handleSubmit = createSubmitHandler(onSubmit); + + return ( + +
+

+ {title} +

+ +
+
+ + +
+ +
+ + +
+ +
+ + {closeLabel} + + + {submitLabel} + +
+
+
+
+ ); +} diff --git a/src/components/Modal/domain/components/ChangePassword/ChangePassword.types.ts b/src/components/Modal/domain/components/ChangePassword/ChangePassword.types.ts new file mode 100644 index 0000000..1ef44b1 --- /dev/null +++ b/src/components/Modal/domain/components/ChangePassword/ChangePassword.types.ts @@ -0,0 +1,28 @@ +import type { InputProps } from '@/components/input/types/types'; +import type { BaseDomainModalProps } from '../../types/types'; + +export type PasswordInputFieldProps = Omit< + InputProps, + 'className' | 'type' | 'name' | 'autoComplete' | 'placeholder' +>; + +export interface ChangePasswordTextOptions { + title?: string; + newPasswordLabel?: string; + confirmPasswordLabel?: string; + newPasswordPlaceholder?: string; + confirmPasswordPlaceholder?: string; + closeLabel?: string; + submitLabel?: string; +} + +export interface ChangePasswordInputOptions { + newPassword?: PasswordInputFieldProps; + confirmPassword?: PasswordInputFieldProps; +} + +export interface ChangePasswordProps extends BaseDomainModalProps { + onSubmit: () => void; + text?: ChangePasswordTextOptions; + input?: ChangePasswordInputOptions; +} diff --git a/src/components/Modal/domain/components/ChangePassword/ChangePassword.utils.ts b/src/components/Modal/domain/components/ChangePassword/ChangePassword.utils.ts new file mode 100644 index 0000000..e39d516 --- /dev/null +++ b/src/components/Modal/domain/components/ChangePassword/ChangePassword.utils.ts @@ -0,0 +1,50 @@ +import type { FormEvent } from 'react'; +import type { DomainModalCloseOptions } from '../../types/types'; +import type { ChangePasswordInputOptions, ChangePasswordTextOptions } from './ChangePassword.types'; +import { + DEFAULT_CLOSE_LABEL, + DEFAULT_CONFIRM_PASSWORD_LABEL, + DEFAULT_CONFIRM_PASSWORD_PLACEHOLDER, + DEFAULT_NEW_PASSWORD_LABEL, + DEFAULT_NEW_PASSWORD_PLACEHOLDER, + DEFAULT_SUBMIT_LABEL, + DEFAULT_TITLE, +} from './ChangePassword.constants'; + +export function resolveChangePasswordText(text?: ChangePasswordTextOptions) { + return { + title: text?.title ?? DEFAULT_TITLE, + newPasswordLabel: text?.newPasswordLabel ?? DEFAULT_NEW_PASSWORD_LABEL, + confirmPasswordLabel: text?.confirmPasswordLabel ?? DEFAULT_CONFIRM_PASSWORD_LABEL, + newPasswordPlaceholder: text?.newPasswordPlaceholder ?? DEFAULT_NEW_PASSWORD_PLACEHOLDER, + confirmPasswordPlaceholder: + text?.confirmPasswordPlaceholder ?? DEFAULT_CONFIRM_PASSWORD_PLACEHOLDER, + closeLabel: text?.closeLabel ?? DEFAULT_CLOSE_LABEL, + submitLabel: text?.submitLabel ?? DEFAULT_SUBMIT_LABEL, + }; +} + +export function resolveCloseOptions(closeOptions?: DomainModalCloseOptions) { + return { + closeOnOverlayClick: closeOptions?.overlayClick ?? true, + closeOnEscape: closeOptions?.escape ?? true, + }; +} + +export function resolvePasswordInputIds( + input: ChangePasswordInputOptions | undefined, + fallbackNewPasswordId: string, + fallbackConfirmPasswordId: string, +) { + return { + newPasswordId: input?.newPassword?.id ?? fallbackNewPasswordId, + confirmPasswordId: input?.confirmPassword?.id ?? fallbackConfirmPasswordId, + }; +} + +export function createSubmitHandler(onSubmit: () => void) { + return (e: FormEvent) => { + e.preventDefault(); + onSubmit(); + }; +} diff --git a/src/components/Modal/domain/components/LogoutModal/LogoutModal.constants.ts b/src/components/Modal/domain/components/LogoutModal/LogoutModal.constants.ts new file mode 100644 index 0000000..4cc086c --- /dev/null +++ b/src/components/Modal/domain/components/LogoutModal/LogoutModal.constants.ts @@ -0,0 +1,4 @@ +export const TITLE_ID = 'logout-modal-title'; +export const DEFAULT_TITLE = '로그아웃 하시겠어요?'; +export const DEFAULT_CLOSE_LABEL = '닫기'; +export const DEFAULT_CONFIRM_LABEL = '로그아웃'; diff --git a/src/components/Modal/domain/components/LogoutModal/LogoutModal.module.css b/src/components/Modal/domain/components/LogoutModal/LogoutModal.module.css new file mode 100644 index 0000000..63b0d3c --- /dev/null +++ b/src/components/Modal/domain/components/LogoutModal/LogoutModal.module.css @@ -0,0 +1,107 @@ +section > .modalContent { + width: 384px; + height: 171px; + box-sizing: border-box; + display: inline-flex; + padding: 16px 16px 32px; + flex-direction: column; + align-items: flex-start; + gap: 10px; + border-radius: 24px; + background: var(--Background-Primary, #fff); +} + +.container { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + margin-top: 8px; + box-sizing: border-box; +} + +.title { + margin: 0; + color: var(--Text-Primary, #1e293b); + text-align: center; + font-family: Pretendard; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 19px; + margin-top: 12px; +} + +.actions { + display: flex; + width: 280px; + gap: 8px; + margin-top: auto; +} + +.closeButton { + display: flex; + width: 136px; + height: 48px; + justify-content: center; + align-items: center; + gap: 10px; + border-radius: 12px; + border: 1px solid var(--Border-Secondary, #cbd5e1); + background: var(--Background-Primary, #fff); + color: var(--Text-Default, #64748b); + text-align: center; + font-family: Pretendard; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 19px; + cursor: pointer; +} + +.actions .closeButton, +.actions .closeButton:hover:not(:disabled), +.actions .closeButton:active:not(:disabled) { + border: 1px solid var(--Border-Secondary, #cbd5e1); + background: var(--Background-Primary, #fff); + color: var(--Text-Default, #64748b); +} + +.confirmButton { + display: flex; + width: 136px; + height: 48px; + justify-content: center; + align-items: center; + gap: 10px; + border: none; + border-radius: 12px; + background: var(--color-status-danger, #fc4b4b); + color: var(--Text-inverse, #fff); + text-align: center; + font-family: Pretendard; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 19px; + cursor: pointer; +} + +.confirmButton:hover { + background: var(--color-status-danger-hover, #e53e3e); +} + +.confirmButton:active { + background: var(--color-status-danger-pressed, #c53030); +} + +@media (max-width: 480px) { + section > .modalContent { + width: 100%; + max-width: 375px; + border-radius: 24px 24px 0 0; + background: var(--Background-Primary, #fff); + } +} diff --git a/src/components/Modal/domain/components/LogoutModal/LogoutModal.tsx b/src/components/Modal/domain/components/LogoutModal/LogoutModal.tsx new file mode 100644 index 0000000..59fa57e --- /dev/null +++ b/src/components/Modal/domain/components/LogoutModal/LogoutModal.tsx @@ -0,0 +1,70 @@ +'use client'; + +import Modal from '../../../Modal'; +import BaseButton from '@/components/Button/base/BaseButton'; +import styles from './LogoutModal.module.css'; +import { + DEFAULT_CLOSE_LABEL, + DEFAULT_CONFIRM_LABEL, + DEFAULT_TITLE, + TITLE_ID, +} from './LogoutModal.constants'; +import type { LogoutModalProps } from './LogoutModal.types'; +export type { LogoutModalProps } from './LogoutModal.types'; + +/** + * @param props.isOpen 모달 표시 여부를 boolean으로 전달합니다. + * @param props.onClose 모달을 닫을 때 실행할 함수를 전달합니다. + * @param props.onConfirm 로그아웃 버튼 클릭 시 실행할 함수를 전달합니다. + * @param props.text 모달 제목과 버튼 문구 같은 텍스트 옵션을 객체로 전달합니다. + * @param props.closeOptions 오버레이 클릭과 Escape 닫힘 옵션을 객체로 전달합니다. + */ +export default function LogoutModal({ + isOpen, + onClose, + onConfirm, + text, + closeOptions, +}: LogoutModalProps) { + const title = text?.title ?? DEFAULT_TITLE; + const closeLabel = text?.closeLabel ?? DEFAULT_CLOSE_LABEL; + const confirmLabel = text?.confirmLabel ?? DEFAULT_CONFIRM_LABEL; + const closeOnOverlayClick = closeOptions?.overlayClick ?? true; + const closeOnEscape = closeOptions?.escape ?? true; + + return ( + +
+

+ {title} +

+ +
+ + {closeLabel} + + + {confirmLabel} + +
+
+
+ ); +} diff --git a/src/components/Modal/domain/components/LogoutModal/LogoutModal.types.ts b/src/components/Modal/domain/components/LogoutModal/LogoutModal.types.ts new file mode 100644 index 0000000..f87adca --- /dev/null +++ b/src/components/Modal/domain/components/LogoutModal/LogoutModal.types.ts @@ -0,0 +1,12 @@ +import type { BaseDomainModalProps } from '../../types/types'; + +export interface LogoutModalTextOptions { + title?: string; + closeLabel?: string; + confirmLabel?: string; +} + +export interface LogoutModalProps extends BaseDomainModalProps { + onConfirm: () => void; + text?: LogoutModalTextOptions; +} diff --git a/src/components/Modal/domain/components/MemberInvite/MemberInvite.constants.ts b/src/components/Modal/domain/components/MemberInvite/MemberInvite.constants.ts new file mode 100644 index 0000000..ab0e5b5 --- /dev/null +++ b/src/components/Modal/domain/components/MemberInvite/MemberInvite.constants.ts @@ -0,0 +1,6 @@ +export const TITLE_ID = 'member-invite-title'; +export const DESCRIPTION_ID = 'member-invite-description'; +export const CLOSE_BUTTON_ARIA_LABEL = '닫기'; +export const DEFAULT_TITLE = '멤버 초대'; +export const DEFAULT_DESCRIPTION = '그룹에 참여할 수 있는 링크를 복사합니다.'; +export const DEFAULT_COPY_LABEL = '링크 복사하기'; diff --git a/src/components/Modal/domain/components/MemberInvite/MemberInvite.module.css b/src/components/Modal/domain/components/MemberInvite/MemberInvite.module.css new file mode 100644 index 0000000..412ffc7 --- /dev/null +++ b/src/components/Modal/domain/components/MemberInvite/MemberInvite.module.css @@ -0,0 +1,98 @@ +@import '@shared/styles/color.css'; + +.container { + position: relative; + width: 384px; + height: 211px; + padding: 40px 24px 32px; + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + text-align: center; +} + +section > .modalContent { + border-radius: 24px; +} + +.closeButton { + position: absolute; + top: 16px; + right: 16px; + width: 24px; + height: 24px; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; + border: none; + background: transparent; + cursor: pointer; +} + +.container .closeButton, +.container .closeButton:hover:not(:disabled), +.container .closeButton:active:not(:disabled) { + border: none; + background: transparent; + color: inherit; +} + +.title { + margin: 8px 0 0; + color: var(--Text-Primary, #1e293b); + font-family: Pretendard, sans-serif; + font-size: 16px; + font-weight: 500; + line-height: 19px; +} + +.description { + margin: 4px 0 16px; + color: var(--Text-Secondary, #334155); + font-family: Pretendard, sans-serif; + font-size: 14px; + font-weight: 500; + line-height: 17px; +} + +.copyButton { + display: flex; + width: 280px; + height: 48px; + padding: 14px 24px; + justify-content: center; + align-items: center; + gap: 10px; + border-radius: 16px; + border: none; + background: var(--color-brand-primary); + color: var(--Text-inverse, #fff); + font-family: Pretendard, sans-serif; + font-size: 16px; + font-weight: 600; + line-height: 19px; + cursor: pointer; +} + +.copyButton:disabled { + background: var(--color-interaction-inactive); + cursor: not-allowed; +} + +@media (max-width: 480px) { + section > .modalContent { + border-radius: 24px 24px 0 0; + } + + .container { + width: 100%; + max-width: 375px; + height: 195px; + flex-shrink: 0; + border-radius: 24px 24px 0 0; + background: var(--Background-Primary, #fff); + box-sizing: border-box; + } +} diff --git a/src/components/Modal/domain/components/MemberInvite/MemberInvite.tsx b/src/components/Modal/domain/components/MemberInvite/MemberInvite.tsx new file mode 100644 index 0000000..f50f2d2 --- /dev/null +++ b/src/components/Modal/domain/components/MemberInvite/MemberInvite.tsx @@ -0,0 +1,77 @@ +'use client'; + +import Image from 'next/image'; +import Modal from '../../../Modal'; +import styles from './MemberInvite.module.css'; +import BaseButton from '@/components/Button/base/BaseButton'; +import xMarkBig from '@/assets/icons/xMark/xMarkBig.svg'; +import { + CLOSE_BUTTON_ARIA_LABEL, + DEFAULT_COPY_LABEL, + DEFAULT_DESCRIPTION, + DEFAULT_TITLE, + DESCRIPTION_ID, + TITLE_ID, +} from './MemberInvite.constants'; +import type { MemberInviteProps } from './MemberInvite.types'; +export type { MemberInviteProps } from './MemberInvite.types'; + +/** + * @param props.isOpen 모달 표시 여부를 boolean으로 전달합니다. + * @param props.onClose 모달을 닫을 때 실행할 함수를 전달합니다. + * @param props.invite 초대 링크와 복사 핸들러를 객체로 전달합니다. + * @param props.text 모달 제목과 설명과 버튼 문구를 객체로 전달합니다. + * @param props.closeOptions 오버레이 클릭과 Escape 닫힘 옵션을 객체로 전달합니다. + */ +export default function MemberInvite({ + isOpen, + onClose, + invite, + text, + closeOptions, +}: MemberInviteProps) { + const title = text?.title ?? DEFAULT_TITLE; + const description = text?.description ?? DEFAULT_DESCRIPTION; + const copyButtonLabel = text?.copyButtonLabel ?? DEFAULT_COPY_LABEL; + const closeOnOverlayClick = closeOptions?.overlayClick ?? true; + const closeOnEscape = closeOptions?.escape ?? true; + + const handleCopy = () => invite.onCopyLink?.(invite.link); + + return ( + +
+ + + +

+ {title} +

+

+ {description} +

+ + {copyButtonLabel} + +
+
+ ); +} diff --git a/src/components/Modal/domain/components/MemberInvite/MemberInvite.types.ts b/src/components/Modal/domain/components/MemberInvite/MemberInvite.types.ts new file mode 100644 index 0000000..7f487b8 --- /dev/null +++ b/src/components/Modal/domain/components/MemberInvite/MemberInvite.types.ts @@ -0,0 +1,17 @@ +import type { BaseDomainModalProps } from '../../types/types'; + +export interface MemberInviteTextOptions { + title?: string; + description?: string; + copyButtonLabel?: string; +} + +export interface MemberInviteInviteOptions { + link: string; + onCopyLink?: (link: string) => void; +} + +export interface MemberInviteProps extends BaseDomainModalProps { + invite: MemberInviteInviteOptions; + text?: MemberInviteTextOptions; +} diff --git a/src/components/Modal/domain/components/ProfileModal/ProfileModal.constants.ts b/src/components/Modal/domain/components/ProfileModal/ProfileModal.constants.ts new file mode 100644 index 0000000..0d10a46 --- /dev/null +++ b/src/components/Modal/domain/components/ProfileModal/ProfileModal.constants.ts @@ -0,0 +1,5 @@ +export const TITLE_ID = 'profile-modal-title'; +export const EMAIL_ID = 'profile-modal-email'; +export const CLOSE_BUTTON_ARIA_LABEL = '닫기'; +export const DEFAULT_COPY_LABEL = '이메일 복사하기'; +export const DEFAULT_PROFILE_ALT = '프로필 이미지'; diff --git a/src/components/Modal/domain/components/ProfileModal/ProfileModal.module.css b/src/components/Modal/domain/components/ProfileModal/ProfileModal.module.css new file mode 100644 index 0000000..4378c62 --- /dev/null +++ b/src/components/Modal/domain/components/ProfileModal/ProfileModal.module.css @@ -0,0 +1,130 @@ +section > .modalContent { + width: 344px; + height: 243px; + display: inline-flex; + padding: 16px 32px 32px; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 8px; + border-radius: 24px; + background: var(--Background-Primary, #fff); + box-sizing: border-box; +} + +.container { + position: relative; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + box-sizing: border-box; +} + +.content { + display: flex; + width: 100%; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 8px; + transform: translateY(12px); +} + +.closeButton { + position: absolute; + top: 0; + right: 0; + width: 24px; + height: 24px; + padding: 0; + border: none; + background: transparent; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.container .closeButton, +.container .closeButton:hover:not(:disabled), +.container .closeButton:active:not(:disabled) { + border: none; + background: transparent; + color: inherit; +} + +.profileImage { + display: flex; + width: 40px; + height: 40px; + justify-content: center; + align-items: center; + aspect-ratio: 1 / 1; + border-radius: 999px; + overflow: hidden; +} + +.profileImage :global(img) { + width: 100%; + height: 100%; + object-fit: cover; +} + +.title { + margin: 0; + color: var(--Text-Primary, #1e293b); + font-family: Pretendard; + font-size: 14px; + font-style: normal; + font-weight: 600; + line-height: 17px; +} + +.email { + margin: 0; + color: var(--Text-Secondary, #334155); + font-family: Pretendard; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 14px; +} + +.copyButton { + display: flex; + width: 280px; + height: 48px; + margin-top: 16px; + justify-content: center; + align-items: center; + gap: 10px; + border: none; + border-radius: 12px; + background: var(--Color-Brand-Primary, #5189fa); + color: var(--Text-inverse, #fff); + font-family: Pretendard; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 19px; + cursor: pointer; +} + +.copyButton:hover { + background: var(--color-interaction-hover, #416ec8); +} + +.copyButton:active { + background: var(--color-interaction-pressed, #3b63b5); +} + +@media (max-width: 480px) { + section > .modalContent { + width: 100%; + max-width: 375px; + border-radius: 24px 24px 0 0; + background: var(--Background-Primary, #fff); + } +} diff --git a/src/components/Modal/domain/components/ProfileModal/ProfileModal.tsx b/src/components/Modal/domain/components/ProfileModal/ProfileModal.tsx new file mode 100644 index 0000000..1cca58a --- /dev/null +++ b/src/components/Modal/domain/components/ProfileModal/ProfileModal.tsx @@ -0,0 +1,88 @@ +'use client'; + +import Image from 'next/image'; + +import Modal from '../../../Modal'; +import styles from './ProfileModal.module.css'; +import BaseButton from '@/components/Button/base/BaseButton'; +import profileFallback from '@/assets/icons/img/img.svg'; +import xMarkBig from '@/assets/icons/xMark/xMarkBig.svg'; +import { + CLOSE_BUTTON_ARIA_LABEL, + DEFAULT_COPY_LABEL, + DEFAULT_PROFILE_ALT, + EMAIL_ID, + TITLE_ID, +} from './ProfileModal.constants'; +import type { ProfileModalProps } from './ProfileModal.types'; +export type { ProfileModalProps } from './ProfileModal.types'; + +/** + * @param props.isOpen 모달 표시 여부를 boolean으로 전달합니다. + * @param props.onClose 모달을 닫을 때 실행할 함수를 전달합니다. + * @param props.onCopyEmail 이메일 복사 버튼 클릭 시 실행할 함수를 전달합니다. + * @param props.profile 프로필 정보와 표시 텍스트를 객체로 전달합니다. + * @param props.closeOptions 오버레이 클릭과 Escape 닫힘 옵션을 객체로 전달합니다. + */ +export default function ProfileModal({ + isOpen, + onClose, + onCopyEmail, + profile, + closeOptions, +}: ProfileModalProps) { + const profileImageAlt = profile.imageAlt ?? DEFAULT_PROFILE_ALT; + const copyButtonLabel = profile.copyButtonLabel ?? DEFAULT_COPY_LABEL; + const closeOnOverlayClick = closeOptions?.overlayClick ?? true; + const closeOnEscape = closeOptions?.escape ?? true; + + return ( + +
+ + + + +
+
+ {profileImageAlt} +
+ +

+ {profile.title} +

+

+ {profile.email} +

+ + + {copyButtonLabel} + +
+
+
+ ); +} diff --git a/src/components/Modal/domain/components/ProfileModal/ProfileModal.types.ts b/src/components/Modal/domain/components/ProfileModal/ProfileModal.types.ts new file mode 100644 index 0000000..33ebe1a --- /dev/null +++ b/src/components/Modal/domain/components/ProfileModal/ProfileModal.types.ts @@ -0,0 +1,15 @@ +import type { ImageProps } from 'next/image'; +import type { BaseDomainModalProps } from '../../types/types'; + +export interface ProfileModalProfileOptions { + title: string; + email: string; + imageSrc?: ImageProps['src']; + imageAlt?: string; + copyButtonLabel?: string; +} + +export interface ProfileModalProps extends BaseDomainModalProps { + onCopyEmail: () => void; + profile: ProfileModalProfileOptions; +} diff --git a/src/components/Modal/domain/components/ResetPassword/ResetPassword.constants.ts b/src/components/Modal/domain/components/ResetPassword/ResetPassword.constants.ts new file mode 100644 index 0000000..0938364 --- /dev/null +++ b/src/components/Modal/domain/components/ResetPassword/ResetPassword.constants.ts @@ -0,0 +1,7 @@ +export const TITLE_ID = 'reset-password-title'; +export const DESCRIPTION_ID = 'reset-password-description'; +export const DEFAULT_TITLE = '비밀번호 재설정'; +export const DEFAULT_DESCRIPTION = '비밀번호 재설정 링크를 보내드립니다.'; +export const DEFAULT_CLOSE_LABEL = '닫기'; +export const DEFAULT_SUBMIT_LABEL = '링크 보내기'; +export const DEFAULT_EMAIL_PLACEHOLDER = '이메일을 입력하세요'; diff --git a/src/components/Modal/domain/components/ResetPassword/ResetPassword.module.css b/src/components/Modal/domain/components/ResetPassword/ResetPassword.module.css new file mode 100644 index 0000000..15b2064 --- /dev/null +++ b/src/components/Modal/domain/components/ResetPassword/ResetPassword.module.css @@ -0,0 +1,123 @@ +section > .modalContent { + display: inline-flex; + padding: 16px 16px 32px 16px; + flex-direction: column; + align-items: stretch; + gap: 10px; + border-radius: 24px; + background: var(--Background-Primary, #fff); + box-sizing: border-box; + width: 384px; + height: 260px; +} + +.container { + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; + height: 100%; + box-sizing: border-box; + margin-top: 24px; +} + +.header { + display: flex; + flex-direction: column; + gap: 6px; + align-items: center; +} + +.title { + margin: 0; + color: var(--Text-Primary, #1e293b); + font-family: Pretendard; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 19px; + text-align: center; +} + +.description { + margin: 0; + color: var(--Text-Default, #64748b); + font-family: Pretendard; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 17px; + text-align: center; +} + +.form { + display: flex; + flex-direction: column; + gap: 16px; + flex: 1; +} + +.form .input { + display: flex; + width: 280px; + height: 48px; + padding: 16px; + align-items: center; + gap: 10px; + border-radius: 12px; + border: 1px solid var(--Border-Primary, #e2e8f0); + background: var(--Background-Primary, #fff); + box-sizing: border-box; + margin: 0 auto; +} + +.actions { + display: flex; + width: 280px; + gap: 8px; + margin: auto auto 0; +} + +.closeButton { + display: flex; + width: 136px; + height: 48px; + justify-content: center; + align-items: center; + gap: 10px; + border-radius: 12px; + border: 1px solid var(--Color-Brand-Primary, #5189fa); + background: transparent; + color: var(--Color-Brand-Primary, #5189fa); + font-family: Pretendard; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 19px; + cursor: pointer; +} + +.sendButton { + display: flex; + width: 136px; + height: 48px; + justify-content: center; + align-items: center; + gap: 10px; + border-radius: 12px; + border: none; + background: var(--Color-Brand-Primary, #5189fa); + color: #fff; + font-family: Pretendard; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 19px; + cursor: pointer; +} + +@media (max-width: 480px) { + section > .modalContent { + border-radius: 24px 24px 0 0; + } +} diff --git a/src/components/Modal/domain/components/ResetPassword/ResetPassword.tsx b/src/components/Modal/domain/components/ResetPassword/ResetPassword.tsx new file mode 100644 index 0000000..0e3a2d2 --- /dev/null +++ b/src/components/Modal/domain/components/ResetPassword/ResetPassword.tsx @@ -0,0 +1,97 @@ +'use client'; + +import type { FormEvent } from 'react'; + +import Modal from '../../../Modal'; +import BaseButton from '@/components/Button/base/BaseButton'; +import { Input } from '@/components/input'; +import styles from './ResetPassword.module.css'; +import { + DEFAULT_CLOSE_LABEL, + DEFAULT_DESCRIPTION, + DEFAULT_EMAIL_PLACEHOLDER, + DEFAULT_SUBMIT_LABEL, + DEFAULT_TITLE, + DESCRIPTION_ID, + TITLE_ID, +} from './ResetPassword.constants'; +import type { ResetPasswordProps } from './ResetPassword.types'; +export type { ResetPasswordProps } from './ResetPassword.types'; + +/** + * @param props.isOpen 모달 표시 여부를 boolean으로 전달합니다. + * @param props.onClose 모달을 닫을 때 실행할 함수를 전달합니다. + * @param props.onSubmit 링크 보내기 제출 시 실행할 함수를 전달합니다. + * @param props.text 제목과 버튼 문구와 안내 문구 같은 텍스트 옵션을 객체로 전달합니다. + * @param props.input 이메일 입력창에 적용할 옵션을 객체로 전달합니다. + * @param props.closeOptions 오버레이 클릭과 Escape 닫힘 옵션을 객체로 전달합니다. + */ +export default function ResetPassword({ + isOpen, + onClose, + onSubmit, + text, + input, + closeOptions, +}: ResetPasswordProps) { + const title = text?.title ?? DEFAULT_TITLE; + const description = text?.description ?? DEFAULT_DESCRIPTION; + const closeLabel = text?.closeLabel ?? DEFAULT_CLOSE_LABEL; + const submitLabel = text?.submitLabel ?? DEFAULT_SUBMIT_LABEL; + const emailPlaceholder = text?.emailPlaceholder ?? DEFAULT_EMAIL_PLACEHOLDER; + const closeOnOverlayClick = closeOptions?.overlayClick ?? true; + const closeOnEscape = closeOptions?.escape ?? true; + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + onSubmit(); + }; + + return ( + +
+
+

+ {title} +

+

+ {description} +

+
+ +
+ + +
+ + {closeLabel} + + + {submitLabel} + +
+
+
+
+ ); +} diff --git a/src/components/Modal/domain/components/ResetPassword/ResetPassword.types.ts b/src/components/Modal/domain/components/ResetPassword/ResetPassword.types.ts new file mode 100644 index 0000000..2f7f0a3 --- /dev/null +++ b/src/components/Modal/domain/components/ResetPassword/ResetPassword.types.ts @@ -0,0 +1,25 @@ +import type { InputProps } from '@/components/input/types/types'; +import type { BaseDomainModalProps } from '../../types/types'; + +export type EmailInputFieldProps = Omit< + InputProps, + 'className' | 'type' | 'name' | 'autoComplete' | 'placeholder' +>; + +export interface ResetPasswordTextOptions { + title?: string; + description?: string; + closeLabel?: string; + submitLabel?: string; + emailPlaceholder?: string; +} + +export interface ResetPasswordInputOptions { + email?: EmailInputFieldProps; +} + +export interface ResetPasswordProps extends BaseDomainModalProps { + onSubmit: () => void; + text?: ResetPasswordTextOptions; + input?: ResetPasswordInputOptions; +} diff --git a/src/components/Modal/domain/components/WarningModal/WarningModal.constants.ts b/src/components/Modal/domain/components/WarningModal/WarningModal.constants.ts new file mode 100644 index 0000000..d7d4e56 --- /dev/null +++ b/src/components/Modal/domain/components/WarningModal/WarningModal.constants.ts @@ -0,0 +1,7 @@ +export const TITLE_ID = 'warning-modal-title'; +export const DESCRIPTION_ID = 'warning-modal-description'; +export const DEFAULT_TITLE = '회원 탈퇴를 진행하시겠어요?'; +export const DEFAULT_DESCRIPTION = + '그룹장으로 있는 그룹은 자동으로 삭제되고,\n모든 그룹에서 나가집니다.'; +export const DEFAULT_CLOSE_LABEL = '닫기'; +export const DEFAULT_CONFIRM_LABEL = '회원 탈퇴'; diff --git a/src/components/Modal/domain/components/WarningModal/WarningModal.module.css b/src/components/Modal/domain/components/WarningModal/WarningModal.module.css new file mode 100644 index 0000000..cd3718a --- /dev/null +++ b/src/components/Modal/domain/components/WarningModal/WarningModal.module.css @@ -0,0 +1,128 @@ +section > .modalContent { + box-sizing: border-box; + display: inline-flex; + width: 384px; + padding: 16px 16px 32px; + flex-direction: column; + align-items: flex-start; + gap: 10px; + border-radius: 24px; + background: var(--Background-Primary, #fff); +} + +.container { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + margin-top: 20px; +} + +.header { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} + +.icon { + flex-shrink: 0; +} + +.title { + margin: 0; + color: var(--Text-Primary, #1e293b); + text-align: center; + font-family: Pretendard; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 19px; +} + +.description { + margin: 0; + width: 280px; + color: var(--Text-Secondary, #334155); + text-align: center; + font-family: Pretendard; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 17px; + white-space: pre-line; +} + +.actions { + display: flex; + width: 280px; + gap: 8px; + margin-top: 16px; +} + +.closeButton { + display: flex; + width: 136px; + height: 48px; + justify-content: center; + align-items: center; + gap: 10px; + border-radius: 12px; + border: 1px solid var(--Border-Secondary, #cbd5e1); + background: var(--Background-Primary, #fff); + color: var(--Text-Default, #64748b); + text-align: center; + font-family: Pretendard; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 19px; + cursor: pointer; +} + +.actions .closeButton, +.actions .closeButton:hover:not(:disabled), +.actions .closeButton:active:not(:disabled) { + border: 1px solid var(--Border-Secondary, #cbd5e1); + background: var(--Background-Primary, #fff); + color: var(--Text-Default, #64748b); +} + +.confirmButton { + display: flex; + width: 136px; + height: 48px; + justify-content: center; + align-items: center; + gap: 10px; + border: none; + border-radius: 12px; + background: var(--color-status-danger, #fc4b4b); + color: var(--Text-inverse, #fff); + text-align: center; + font-family: Pretendard; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 19px; + cursor: pointer; +} + +.confirmButton:hover { + background: var(--color-status-danger-hover, #e53e3e); +} + +.confirmButton:active { + background: var(--color-status-danger-pressed, #c53030); +} + +@media (max-width: 480px) { + section > .modalContent { + width: 100%; + max-width: 375px; + border-radius: 24px 24px 0 0; + background: var(--Background-Primary, #fff); + } +} diff --git a/src/components/Modal/domain/components/WarningModal/WarningModal.tsx b/src/components/Modal/domain/components/WarningModal/WarningModal.tsx new file mode 100644 index 0000000..89aaf44 --- /dev/null +++ b/src/components/Modal/domain/components/WarningModal/WarningModal.tsx @@ -0,0 +1,91 @@ +'use client'; + +import Image from 'next/image'; + +import Modal from '../../../Modal'; +import BaseButton from '@/components/Button/base/BaseButton'; +import styles from './WarningModal.module.css'; +import alertSmall from '@/assets/icons/alert/alertSmall.svg'; +import { + DEFAULT_CLOSE_LABEL, + DEFAULT_CONFIRM_LABEL, + DEFAULT_DESCRIPTION, + DEFAULT_TITLE, + DESCRIPTION_ID, + TITLE_ID, +} from './WarningModal.constants'; +import type { WarningModalProps } from './WarningModal.types'; +export type { WarningModalProps } from './WarningModal.types'; + +/** + * @param props.isOpen 모달 표시 여부를 boolean으로 전달합니다. + * @param props.onClose 모달을 닫을 때 실행할 함수를 전달합니다. + * @param props.onConfirm 회원 탈퇴 확인 버튼 클릭 시 실행할 함수를 전달합니다. + * @param props.text 경고 모달 제목과 설명과 버튼 문구를 객체로 전달합니다. + * @param props.closeOptions 오버레이 클릭과 Escape 닫힘 옵션을 객체로 전달합니다. + */ +export default function WarningModal({ + isOpen, + onClose, + onConfirm, + text, + closeOptions, +}: WarningModalProps) { + const title = text?.title ?? DEFAULT_TITLE; + const description = text?.description ?? DEFAULT_DESCRIPTION; + const closeLabel = text?.closeLabel ?? DEFAULT_CLOSE_LABEL; + const confirmLabel = text?.confirmLabel ?? DEFAULT_CONFIRM_LABEL; + const closeOnOverlayClick = closeOptions?.overlayClick ?? true; + const closeOnEscape = closeOptions?.escape ?? true; + + return ( + +
+
+ +

+ {title} +

+
+ +

+ {description} +

+ + +
+
+ ); +} diff --git a/src/components/Modal/domain/components/WarningModal/WarningModal.types.ts b/src/components/Modal/domain/components/WarningModal/WarningModal.types.ts new file mode 100644 index 0000000..4f9343e --- /dev/null +++ b/src/components/Modal/domain/components/WarningModal/WarningModal.types.ts @@ -0,0 +1,13 @@ +import type { BaseDomainModalProps } from '../../types/types'; + +export interface WarningModalTextOptions { + title?: string; + description?: string; + closeLabel?: string; + confirmLabel?: string; +} + +export interface WarningModalProps extends BaseDomainModalProps { + onConfirm: () => void; + text?: WarningModalTextOptions; +} diff --git a/src/components/Modal/domain/types/types.ts b/src/components/Modal/domain/types/types.ts new file mode 100644 index 0000000..c79b7c8 --- /dev/null +++ b/src/components/Modal/domain/types/types.ts @@ -0,0 +1,10 @@ +export interface DomainModalCloseOptions { + overlayClick?: boolean; + escape?: boolean; +} + +export interface BaseDomainModalProps { + isOpen: boolean; + onClose: () => void; + closeOptions?: DomainModalCloseOptions; +} diff --git a/src/components/Modal/types/types.ts b/src/components/Modal/types/types.ts index 6c53d07..98b4c1c 100644 --- a/src/components/Modal/types/types.ts +++ b/src/components/Modal/types/types.ts @@ -6,6 +6,7 @@ interface BaseModalProps { children?: ReactNode; ariaDescribedby?: string; className?: string; + contentClassName?: string; closeOnOverlayClick?: boolean; closeOnEscape?: boolean; }