@uniai-fe/uds-templates 0.0.9 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +77 -1
  2. package/dist/styles.css +212 -267
  3. package/package.json +6 -4
  4. package/src/components/auth/index.tsx +11 -0
  5. package/src/components/auth/login/index.tsx +1 -1
  6. package/src/components/auth/login/markup/FormField.tsx +2 -2
  7. package/src/components/auth/login/types/props.ts +12 -12
  8. package/src/components/auth/login/types.ts +2 -2
  9. package/src/components/auth/signup/hooks/index.ts +3 -0
  10. package/src/components/auth/signup/hooks/useSignupAccountForm.ts +77 -0
  11. package/src/components/auth/signup/hooks/useSignupUserInfoForm.ts +81 -0
  12. package/src/components/auth/signup/hooks/useSignupVerificationForm.ts +77 -0
  13. package/src/components/auth/signup/index.ts +24 -0
  14. package/src/components/auth/signup/markup/AccountForm.tsx +124 -0
  15. package/src/components/auth/signup/markup/Complete.tsx +61 -0
  16. package/src/components/auth/signup/markup/UserInfoForm.tsx +97 -0
  17. package/src/components/auth/signup/markup/VerificationForm.tsx +155 -0
  18. package/src/components/auth/signup/markup/index.ts +4 -0
  19. package/src/components/auth/signup/styles/signup.scss +135 -0
  20. package/src/components/auth/signup/types/hooks.ts +85 -0
  21. package/src/components/auth/signup/types/index.ts +2 -0
  22. package/src/components/auth/signup/types/props.ts +105 -0
  23. package/src/components/auth/signup/utils/composeFieldProps.ts +50 -0
  24. package/src/components/modal/core/components/Container.tsx +41 -0
  25. package/src/components/modal/core/components/FooterButtons.tsx +132 -0
  26. package/src/components/modal/core/components/Provider.tsx +28 -0
  27. package/src/components/modal/core/components/Root.tsx +93 -0
  28. package/src/components/modal/core/hooks/useModal.ts +136 -0
  29. package/src/components/modal/core/jotai/atoms.ts +10 -0
  30. package/src/components/modal/index.scss +4 -0
  31. package/src/components/modal/index.tsx +16 -0
  32. package/src/components/modal/styles/animations.scss +24 -0
  33. package/src/components/modal/styles/base.scss +45 -0
  34. package/src/components/modal/styles/container.scss +138 -0
  35. package/src/components/modal/styles/dimmer.scss +23 -0
  36. package/src/components/modal/templates/Alert.tsx +104 -0
  37. package/src/components/modal/templates/Dialog.tsx +112 -0
  38. package/src/components/modal/types/footer.ts +36 -0
  39. package/src/components/modal/types/index.ts +21 -0
  40. package/src/components/modal/types/options.ts +6 -0
  41. package/src/components/modal/types/state.ts +31 -0
  42. package/src/components/modal/types/templates.ts +32 -0
  43. package/src/index.scss +1 -0
  44. package/src/index.tsx +1 -0
@@ -0,0 +1,155 @@
1
+ import { useMemo } from "react";
2
+ import { useForm } from "react-hook-form";
3
+ import {
4
+ Button,
5
+ CheckboxField,
6
+ EmailVerificationInput,
7
+ type EmailVerificationInputProps,
8
+ } from "@uniai-fe/uds-primitives";
9
+ import type {
10
+ AuthSignupAgreementOption,
11
+ AuthSignupVerificationProps,
12
+ AuthSignupVerificationValues,
13
+ } from "../types";
14
+ import { useSignupVerificationForm } from "../hooks";
15
+ import { composeSignupFieldProps } from "../utils/composeFieldProps";
16
+
17
+ const DEFAULT_SUBMIT_LABEL = "다음";
18
+
19
+ /**
20
+ * 회원가입 Step2; 약관 동의 + 이메일 인증
21
+ * @component
22
+ * @param {AuthSignupVerificationProps} props verification props
23
+ * @param {AuthSignupAgreementOption[]} props.agreements 약관 목록
24
+ * @param {Record<string, boolean>} props.agreementState 약관 체크 상태
25
+ * @param {(agreementId: string) => void} props.onToggleAgreement 단일 약관 토글
26
+ * @param {() => void} props.onToggleAll 전체 동의 토글
27
+ * @param {(agreementId: string) => void} [props.onOpenAgreementDetail] 약관 상세 보기
28
+ * @param {React.FormHTMLAttributes<HTMLFormElement>} [props.formAttr] form attr
29
+ * @param {import("react").ReactNode} [props.submitLabel] CTA 라벨
30
+ */
31
+ export function AuthSignupVerificationForm({
32
+ fields,
33
+ agreements,
34
+ agreementState,
35
+ onToggleAgreement,
36
+ onToggleAll,
37
+ onOpenAgreementDetail,
38
+ formAttr,
39
+ submitLabel,
40
+ onSubmit,
41
+ }: AuthSignupVerificationProps) {
42
+ // 이메일 필드 name attr 기반으로 RHF defaultValues를 설정한다.
43
+ const defaultValues = useMemo(
44
+ () =>
45
+ (Object.keys(fields) as Array<keyof typeof fields>).reduce(
46
+ (acc, fieldKey) => {
47
+ const config = fields[fieldKey];
48
+ const fieldName = config.attr?.name ?? String(fieldKey);
49
+ acc[fieldName] = "";
50
+ return acc;
51
+ },
52
+ {} as AuthSignupVerificationValues,
53
+ ),
54
+ [fields],
55
+ );
56
+
57
+ const form = useForm<AuthSignupVerificationValues>({
58
+ mode: "onChange",
59
+ defaultValues,
60
+ });
61
+
62
+ const {
63
+ register,
64
+ helpers,
65
+ disabled,
66
+ onSubmit: handleSubmit,
67
+ } = useSignupVerificationForm({
68
+ fields,
69
+ form,
70
+ onSubmit,
71
+ });
72
+
73
+ // 전체 동의 상태를 agreementState 집계로 계산한다.
74
+ const allChecked = agreements.every(
75
+ agreement => agreementState[agreement.id],
76
+ );
77
+
78
+ const renderAgreementLabel = (option: AuthSignupAgreementOption) => (
79
+ <span className="auth-signup-agreement-label">
80
+ <span className="auth-signup-agreement-title">{option.label}</span>
81
+ <span className="auth-signup-agreement-required">
82
+ {option.required ? "필수" : "선택"}
83
+ </span>
84
+ </span>
85
+ );
86
+
87
+ return (
88
+ <div className="auth-signup-verification">
89
+ <section className="auth-signup-agreements" aria-label="약관 동의">
90
+ <div className="auth-signup-agreement">
91
+ <CheckboxField
92
+ size="large"
93
+ label={
94
+ <span className="auth-signup-agreement-title">전체 동의</span>
95
+ }
96
+ helperText="필수 및 선택 약관 전체 동의"
97
+ checked={allChecked}
98
+ onCheckedChange={() => onToggleAll()}
99
+ />
100
+ </div>
101
+ <div className="auth-signup-agreement-divider" aria-hidden />
102
+ {agreements.map(option => (
103
+ <div
104
+ className="auth-signup-agreement"
105
+ key={option.id}
106
+ data-required={option.required ? "true" : undefined}
107
+ >
108
+ <CheckboxField
109
+ size="large"
110
+ label={renderAgreementLabel(option)}
111
+ helperText={option.description}
112
+ checked={Boolean(agreementState[option.id])}
113
+ onCheckedChange={() => onToggleAgreement(option.id)}
114
+ />
115
+ {onOpenAgreementDetail ? (
116
+ <button
117
+ type="button"
118
+ className="auth-signup-agreement-detail-button"
119
+ onClick={() => onOpenAgreementDetail(option.id)}
120
+ >
121
+ 자세히 보기
122
+ </button>
123
+ ) : null}
124
+ </div>
125
+ ))}
126
+ </section>
127
+ <form
128
+ className="auth-signup-form auth-signup-form--verification"
129
+ {...formAttr}
130
+ onSubmit={handleSubmit}
131
+ >
132
+ <div className="auth-signup-fields">
133
+ <EmailVerificationInput
134
+ {...composeSignupFieldProps<EmailVerificationInputProps>(
135
+ fields.email,
136
+ helpers.email,
137
+ )}
138
+ register={register.email}
139
+ />
140
+ </div>
141
+ <Button.Default
142
+ type="submit"
143
+ scale="solid-xlarge"
144
+ priority="primary"
145
+ block
146
+ disabled={disabled}
147
+ >
148
+ {submitLabel ?? DEFAULT_SUBMIT_LABEL}
149
+ </Button.Default>
150
+ </form>
151
+ </div>
152
+ );
153
+ }
154
+
155
+ export default AuthSignupVerificationForm;
@@ -0,0 +1,4 @@
1
+ export { AuthSignupUserInfoForm } from "./UserInfoForm";
2
+ export { AuthSignupVerificationForm } from "./VerificationForm";
3
+ export { AuthSignupAccountForm } from "./AccountForm";
4
+ export { AuthSignupComplete } from "./Complete";
@@ -0,0 +1,135 @@
1
+ .auth-signup-form {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--spacing-padding-8, 32px);
5
+ margin-top: var(--spacing-padding-9, 36px);
6
+ }
7
+
8
+ .auth-signup-fields {
9
+ display: flex;
10
+ flex-direction: column;
11
+ gap: var(--spacing-padding-5, 20px);
12
+ }
13
+
14
+ .auth-signup-verification {
15
+ display: flex;
16
+ flex-direction: column;
17
+ gap: var(--spacing-padding-9, 36px);
18
+ }
19
+
20
+ .auth-signup-agreements {
21
+ padding: var(--spacing-padding-7, 28px);
22
+ border-radius: var(--shape-rounded-3, 16px);
23
+ background: var(--color-background-subtle);
24
+ display: flex;
25
+ flex-direction: column;
26
+ gap: var(--spacing-padding-4, 16px);
27
+ }
28
+
29
+ .auth-signup-agreement {
30
+ display: flex;
31
+ align-items: flex-start;
32
+ justify-content: space-between;
33
+ gap: var(--spacing-padding-3, 12px);
34
+ }
35
+
36
+ .auth-signup-agreement-divider {
37
+ height: 1px;
38
+ width: 100%;
39
+ background: var(--color-border-default);
40
+ }
41
+
42
+ .auth-signup-agreement-label {
43
+ display: flex;
44
+ gap: var(--spacing-padding-2, 8px);
45
+ align-items: center;
46
+ }
47
+
48
+ .auth-signup-agreement-title {
49
+ font-weight: 600;
50
+ color: var(--color-label-standard);
51
+ }
52
+
53
+ .auth-signup-agreement-required {
54
+ font-size: 12px;
55
+ color: var(--color-label-assistive);
56
+ }
57
+
58
+ .auth-signup-agreement-detail-button {
59
+ border: none;
60
+ background: none;
61
+ font-size: 13px;
62
+ color: var(--color-primary-60, #1d4ed8);
63
+ cursor: pointer;
64
+
65
+ &:hover {
66
+ color: var(--color-primary-70, #1e40af);
67
+ }
68
+ }
69
+
70
+ .auth-signup-password-rules {
71
+ list-style: none;
72
+ padding: 0;
73
+ margin: 0;
74
+ display: flex;
75
+ flex-direction: column;
76
+ gap: var(--spacing-padding-2, 8px);
77
+ }
78
+
79
+ .auth-signup-password-rule {
80
+ display: flex;
81
+ align-items: center;
82
+ gap: var(--spacing-padding-3, 12px);
83
+ color: var(--color-label-assistive);
84
+
85
+ &[data-fulfilled="true"] {
86
+ color: var(--color-label-positive);
87
+ }
88
+ }
89
+
90
+ .auth-signup-password-rule-indicator {
91
+ width: 8px;
92
+ height: 8px;
93
+ border-radius: 50%;
94
+ background: var(--color-border-default);
95
+ flex-shrink: 0;
96
+ }
97
+
98
+ .auth-signup-password-rule[data-fulfilled="true"]
99
+ .auth-signup-password-rule-indicator {
100
+ background: var(--color-label-positive);
101
+ }
102
+
103
+ .auth-signup-password-rule-text {
104
+ font-size: 14px;
105
+ }
106
+
107
+ .auth-signup-complete {
108
+ display: flex;
109
+ flex-direction: column;
110
+ gap: var(--spacing-padding-7, 28px);
111
+ text-align: center;
112
+ }
113
+
114
+ .auth-signup-complete-illustration {
115
+ display: flex;
116
+ justify-content: center;
117
+ }
118
+
119
+ .auth-signup-complete-title {
120
+ font-size: 24px;
121
+ font-weight: 700;
122
+ color: var(--color-label-standard);
123
+ margin: 0;
124
+ }
125
+
126
+ .auth-signup-complete-description {
127
+ margin: var(--spacing-padding-2, 8px) 0 0;
128
+ color: var(--color-label-assistive);
129
+ }
130
+
131
+ .auth-signup-complete-actions {
132
+ display: flex;
133
+ flex-direction: column;
134
+ gap: var(--spacing-padding-4, 16px);
135
+ }
@@ -0,0 +1,85 @@
1
+ import type { ReactNode } from "react";
2
+ import type { InputState } from "@uniai-fe/uds-primitives";
3
+ import type {
4
+ AuthSignupFieldProps,
5
+ AuthSignupUserInfoFields,
6
+ AuthSignupUserInfoValues,
7
+ AuthSignupVerificationFields,
8
+ AuthSignupVerificationValues,
9
+ AuthSignupAccountFields,
10
+ AuthSignupAccountValues,
11
+ } from "./props";
12
+ import type {
13
+ SubmitHandler,
14
+ UseFormRegisterReturn,
15
+ UseFormReturn,
16
+ } from "react-hook-form";
17
+
18
+ /**
19
+ * 공용 Helper 타입
20
+ */
21
+ export type AuthSignupHelperMap<
22
+ TFields extends Record<string, AuthSignupFieldProps>,
23
+ > = Record<keyof TFields, { text?: ReactNode; state?: InputState }>;
24
+
25
+ /** Step1 옵션 */
26
+ export interface UseSignupUserInfoFormOptions<
27
+ TFields extends Record<string, AuthSignupFieldProps> =
28
+ AuthSignupUserInfoFields,
29
+ > {
30
+ fields: TFields;
31
+ form: UseFormReturn<AuthSignupUserInfoValues>;
32
+ onSubmit: SubmitHandler<AuthSignupUserInfoValues>;
33
+ }
34
+
35
+ export interface UseSignupUserInfoFormReturn<
36
+ TFields extends Record<string, AuthSignupFieldProps> =
37
+ AuthSignupUserInfoFields,
38
+ > {
39
+ register: Record<keyof TFields, UseFormRegisterReturn>;
40
+ helpers: AuthSignupHelperMap<TFields>;
41
+ disabled: boolean;
42
+ onSubmit: ReturnType<UseFormReturn<AuthSignupUserInfoValues>["handleSubmit"]>;
43
+ }
44
+
45
+ /** Step2 옵션 */
46
+ export interface UseSignupVerificationFormOptions<
47
+ TFields extends Record<string, AuthSignupFieldProps> =
48
+ AuthSignupVerificationFields,
49
+ > {
50
+ fields: TFields;
51
+ form: UseFormReturn<AuthSignupVerificationValues>;
52
+ onSubmit: SubmitHandler<AuthSignupVerificationValues>;
53
+ }
54
+
55
+ export interface UseSignupVerificationFormReturn<
56
+ TFields extends Record<string, AuthSignupFieldProps> =
57
+ AuthSignupVerificationFields,
58
+ > {
59
+ register: Record<keyof TFields, UseFormRegisterReturn>;
60
+ helpers: AuthSignupHelperMap<TFields>;
61
+ disabled: boolean;
62
+ onSubmit: ReturnType<
63
+ UseFormReturn<AuthSignupVerificationValues>["handleSubmit"]
64
+ >;
65
+ }
66
+
67
+ /** Step3 옵션 */
68
+ export interface UseSignupAccountFormOptions<
69
+ TFields extends Record<string, AuthSignupFieldProps> =
70
+ AuthSignupAccountFields,
71
+ > {
72
+ fields: TFields;
73
+ form: UseFormReturn<AuthSignupAccountValues>;
74
+ onSubmit: SubmitHandler<AuthSignupAccountValues>;
75
+ }
76
+
77
+ export interface UseSignupAccountFormReturn<
78
+ TFields extends Record<string, AuthSignupFieldProps> =
79
+ AuthSignupAccountFields,
80
+ > {
81
+ register: Record<keyof TFields, UseFormRegisterReturn>;
82
+ helpers: AuthSignupHelperMap<TFields>;
83
+ disabled: boolean;
84
+ onSubmit: ReturnType<UseFormReturn<AuthSignupAccountValues>["handleSubmit"]>;
85
+ }
@@ -0,0 +1,2 @@
1
+ export type * from "./props";
2
+ export type * from "./hooks";
@@ -0,0 +1,105 @@
1
+ import type { ReactNode } from "react";
2
+ import type React from "react";
3
+ import type { SubmitHandler } from "react-hook-form";
4
+ import type {
5
+ EmailVerificationInputProps,
6
+ InputFieldProps,
7
+ InputPasswordProps,
8
+ InputProps,
9
+ PhoneInputProps,
10
+ } from "@uniai-fe/uds-primitives";
11
+
12
+ /**
13
+ * 회원가입 공통 필드 props; primitives InputFieldProps를 그대로 사용한다.
14
+ */
15
+ export type AuthSignupFieldProps<TProps extends InputProps = InputProps> =
16
+ InputFieldProps<TProps>;
17
+
18
+ /** 사용자 기본 정보(이름/휴대폰) */
19
+ export type AuthSignupUserInfoFields = {
20
+ name: AuthSignupFieldProps<InputProps>;
21
+ phone: AuthSignupFieldProps<PhoneInputProps>;
22
+ };
23
+
24
+ export type AuthSignupUserInfoValues = Record<string, string>; // name/phone 값
25
+
26
+ export interface AuthSignupUserInfoProps {
27
+ fields: AuthSignupUserInfoFields;
28
+ submitLabel?: ReactNode;
29
+ formAttr?: React.FormHTMLAttributes<HTMLFormElement>;
30
+ onSubmit: SubmitHandler<AuthSignupUserInfoValues>;
31
+ }
32
+
33
+ /** 약관 + 이메일 인증 */
34
+ export interface AuthSignupAgreementOption {
35
+ id: string;
36
+ label: ReactNode;
37
+ description?: ReactNode;
38
+ required?: boolean;
39
+ }
40
+
41
+ export type AuthSignupAgreementState = Record<string, boolean>;
42
+
43
+ export type AuthSignupVerificationFields = {
44
+ email: AuthSignupFieldProps<EmailVerificationInputProps>;
45
+ };
46
+
47
+ export type AuthSignupVerificationValues = Record<string, string>;
48
+
49
+ export interface AuthSignupVerificationProps {
50
+ fields: AuthSignupVerificationFields;
51
+ agreements: AuthSignupAgreementOption[];
52
+ agreementState: AuthSignupAgreementState;
53
+ onToggleAgreement: (agreementId: string) => void;
54
+ onToggleAll: () => void;
55
+ onOpenAgreementDetail?: (agreementId: string) => void;
56
+ submitLabel?: ReactNode;
57
+ formAttr?: React.FormHTMLAttributes<HTMLFormElement>;
58
+ onSubmit: SubmitHandler<AuthSignupVerificationValues>;
59
+ }
60
+
61
+ /** 계정 정보(아이디/비밀번호) */
62
+ export type AuthSignupAccountFields = {
63
+ accountId: AuthSignupFieldProps<InputProps>;
64
+ password: AuthSignupFieldProps<InputPasswordProps>;
65
+ confirmPassword: AuthSignupFieldProps<InputPasswordProps>;
66
+ };
67
+
68
+ export type AuthSignupAccountValues = Record<string, string>;
69
+
70
+ export interface AuthSignupPasswordRule {
71
+ id: string;
72
+ label: ReactNode;
73
+ fulfilled: boolean;
74
+ }
75
+
76
+ export interface AuthSignupAccountProps {
77
+ fields: AuthSignupAccountFields;
78
+ passwordRules?: AuthSignupPasswordRule[];
79
+ submitLabel?: ReactNode;
80
+ formAttr?: React.FormHTMLAttributes<HTMLFormElement>;
81
+ onSubmit: SubmitHandler<AuthSignupAccountValues>;
82
+ }
83
+
84
+ /** 완료 CTA */
85
+ export interface AuthSignupCompleteAction {
86
+ label: ReactNode;
87
+ onClick: () => void;
88
+ disabled?: boolean;
89
+ }
90
+
91
+ export interface AuthSignupCompleteProps {
92
+ title: ReactNode;
93
+ description?: ReactNode;
94
+ illustration?: ReactNode;
95
+ primaryAction: AuthSignupCompleteAction;
96
+ secondaryAction?: AuthSignupCompleteAction;
97
+ }
98
+
99
+ /** 전체 플로우 옵션 */
100
+ export interface AuthSignupFlowProps {
101
+ userInfo: AuthSignupUserInfoProps;
102
+ verification: AuthSignupVerificationProps;
103
+ account: AuthSignupAccountProps;
104
+ complete: AuthSignupCompleteProps;
105
+ }
@@ -0,0 +1,50 @@
1
+ import type { ReactNode } from "react";
2
+ import type { InputProps, InputState } from "@uniai-fe/uds-primitives";
3
+ import type { AuthSignupFieldProps } from "../types";
4
+
5
+ /**
6
+ * 필드 helper 상태; helper 텍스트 및 상태를 전달한다.
7
+ */
8
+ export interface AuthSignupHelperState {
9
+ text?: ReactNode;
10
+ state?: InputState;
11
+ }
12
+
13
+ /**
14
+ * 입력 필드 구성 요소; attr/props를 병합해 label/helper/state를 일관되게 만든다.
15
+ * @param {AuthSignupFieldProps<TProps>} config 필드 설정
16
+ * @param {AuthSignupHelperState} [helper] 훅에서 계산한 helper 정보
17
+ */
18
+ export const composeSignupFieldProps = <TProps extends InputProps>(
19
+ config: AuthSignupFieldProps<TProps>,
20
+ helper?: AuthSignupHelperState,
21
+ ): TProps => {
22
+ // attr + props를 합쳐 RHF register/label/helper className을 동기화한다.
23
+ const baseProps = {
24
+ ...(config.attr ?? {}),
25
+ ...(config.props ?? ({} as TProps)),
26
+ } as TProps;
27
+ const mergedLabelProps = config.labelClassName
28
+ ? {
29
+ ...baseProps.labelProps,
30
+ className: config.labelClassName,
31
+ }
32
+ : baseProps.labelProps;
33
+ const mergedHelperProps = config.helperClassName
34
+ ? {
35
+ ...baseProps.helperProps,
36
+ className: config.helperClassName,
37
+ }
38
+ : baseProps.helperProps;
39
+
40
+ return {
41
+ ...baseProps,
42
+ label: config.label ?? baseProps.label,
43
+ helper: helper?.text ?? config.helper ?? baseProps.helper,
44
+ state: helper?.state ?? baseProps.state,
45
+ block: config.block ?? baseProps.block ?? true,
46
+ className: config.className ?? baseProps.className,
47
+ labelProps: mergedLabelProps,
48
+ helperProps: mergedHelperProps,
49
+ };
50
+ };
@@ -0,0 +1,41 @@
1
+ "use client";
2
+
3
+ import type {
4
+ ModalFooterButton,
5
+ ModalSections,
6
+ ModalStackKey,
7
+ } from "../../types";
8
+ import { ModalFooterButtons } from "./FooterButtons";
9
+
10
+ /**
11
+ * 모달 컨테이너; header/body/footer 슬롯만 담당한다.
12
+ * @component
13
+ * @param {ModalSections & {stackKey: ModalStackKey; footerButtons?: ModalFooterButton[]}} props
14
+ * @param {React.ReactNode} [props.header] 헤더 슬롯
15
+ * @param {React.ReactNode} props.body 본문 슬롯
16
+ * @param {React.ReactNode} [props.footer] footer 슬롯
17
+ * @param {ModalFooterButton[]} [props.footerButtons] 버튼 스펙 배열
18
+ */
19
+ export function ModalContainer({
20
+ stackKey,
21
+ header,
22
+ body,
23
+ footer,
24
+ footerButtons,
25
+ }: ModalSections & {
26
+ stackKey: ModalStackKey;
27
+ footerButtons?: ModalFooterButton[];
28
+ }) {
29
+ return (
30
+ <div className="uds-modal-container">
31
+ {header ? <div className="uds-modal-header">{header}</div> : null}
32
+ <div className="uds-modal-body">{body}</div>
33
+ {footer ? <div className="uds-modal-footer">{footer}</div> : null}
34
+ {!footer && footerButtons && footerButtons.length ? (
35
+ <div className="uds-modal-footer">
36
+ <ModalFooterButtons stackKey={stackKey} buttons={footerButtons} />
37
+ </div>
38
+ ) : null}
39
+ </div>
40
+ );
41
+ }